BridgeAccountLink ties a game account to a website account. [link mints a one-time, 5-minute code from an unambiguous alphabet (no O/0/I/1), holds it in a Core-thread dict keyed to the account, and emits link.request. The website relays the code back through the sidecar as link.confirm; the shard validates, writes the WebsiteUserId account tag, and replies link.ok. A bad or expired code gets link.error. The tag persists to accounts.xml in ServUO's standard <tags> format, read by LoadTags at boot, so a link survives restarts with no new persistence layer. mob.login now carries webId when the account is linked, so the sidecar can attribute a session to a site user without a lookup. Safeguards: one-time codes; only the newest code per account is valid; per-account 30s rate limit against code spam; a 1-minute purge bounds the code table; the websiteUserId is trusted only because the socket is loopback-only. The tag reaches memory on confirm but disk only on the next save — a hard crash between loses it, and the player just re-runs [link. Verified end to end with a smart stub that reads the emitted code and confirms it: link.request -> link.confirm -> link.ok, a bad code -> link.error, and the tag observed in accounts.xml after a save. Evidence in docs/PLAN.md §15. The [link command body is exposed as RequestLink(Mobile) so it can be driven in tests without a client. Adds tools/stub_sidecar_link.ps1 and tools/scaffolding/BridgeLinkProbe.cs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
65 lines
2.5 KiB
C#
65 lines
2.5 KiB
C#
using System;
|
|
|
|
using Server.Accounting;
|
|
using Server.Custom.Bridge;
|
|
using Server.Mobiles;
|
|
|
|
namespace Server.Custom
|
|
{
|
|
/// <summary>
|
|
/// Triggers the [link flow for a seeded character without a game client, so the account
|
|
/// linking round-trip can be tested end to end: RequestLink emits link.request, the test
|
|
/// sidecar reads the code and sends link.confirm, and the account gets tagged.
|
|
///
|
|
/// Test scaffolding. Never deployed. Writes an account tag (persisted on save).
|
|
/// </summary>
|
|
public static class BridgeLinkProbe
|
|
{
|
|
public static void Initialize()
|
|
{
|
|
if (Config.Get("Bridge.LinkProbeOnStart", false))
|
|
EventSink.ServerStarted += () => Timer.DelayCall(TimeSpan.FromSeconds(3.0), Run);
|
|
}
|
|
|
|
private static void Run()
|
|
{
|
|
try
|
|
{
|
|
// Use a seed account not already linked. seed_001 slot 0.
|
|
var acct = Accounting.Accounts.GetAccount("seed_001") as Account;
|
|
var pm = acct == null ? null : acct[0] as PlayerMobile;
|
|
|
|
if (pm == null)
|
|
{
|
|
Console.WriteLine("[LinkProbe] seed_001 slot 0 not found; seed the world first");
|
|
return;
|
|
}
|
|
|
|
var existing = acct.GetTag("WebsiteUserId");
|
|
if (existing != null)
|
|
Console.WriteLine("[LinkProbe] seed_001 already linked to {0}; re-running anyway", existing);
|
|
|
|
Console.WriteLine("[LinkProbe] requesting link for seed_001 / {0}", pm.Name);
|
|
BridgeAccountLink.RequestLink(pm);
|
|
Console.WriteLine("[LinkProbe] link.request emitted; watch for the code -> link.confirm -> link.ok");
|
|
|
|
// Give the confirm time to land and set the tag, then save so it reaches
|
|
// accounts.xml. This proves persistence across a restart.
|
|
Timer.DelayCall(TimeSpan.FromSeconds(5.0), () =>
|
|
{
|
|
var tag = acct.GetTag("WebsiteUserId");
|
|
Console.WriteLine("[LinkProbe] after confirm, seed_001 WebsiteUserId tag = {0}",
|
|
tag ?? "(null)");
|
|
Console.WriteLine("[LinkProbe] saving world to persist the tag...");
|
|
World.Save();
|
|
Console.WriteLine("[LinkProbe] saved");
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("[LinkProbe] FAILED: " + ex);
|
|
}
|
|
}
|
|
}
|
|
}
|