Phase 5: [link account linking

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>
This commit is contained in:
2026-07-10 11:47:17 -05:00
parent 7e48d60a8b
commit feeb904bfe
7 changed files with 399 additions and 3 deletions

View File

@@ -0,0 +1,64 @@
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);
}
}
}
}

View File

@@ -10,6 +10,7 @@ These two scripts produced the measured budget in `docs/PLAN.md` §1. They are k
| `BridgeProbe.cs` | `Scripts/Custom/BridgeProbe.cs` | Times every read the plugin performs, on the Core thread. Read-only. |
| `BridgeEventProbe.cs` | `Scripts/Custom/BridgeEventProbe.cs` | Fires gold/fame/karma/save events through their real code paths so the emit path can be verified without a game client. **Mutates the world and saves.** Flag: `EventProbeOnStart`. |
| `BridgeSweepProbe.cs` | `Scripts/Custom/BridgeSweepProbe.cs` | Bumps one seeded house's decay stage after baseline so the decay sweep's transition detection can be observed without waiting a real IDOC stage. Flag: `SweepProbeOnStart`. Pair with short `*SweepSeconds` overrides. |
| `BridgeLinkProbe.cs` | `Scripts/Custom/BridgeLinkProbe.cs` | Triggers `[link` for seed_001 without a client, then saves so the `WebsiteUserId` tag reaches `accounts.xml`. Flag: `LinkProbeOnStart`. Pair with a sidecar that reads the code and sends `link.confirm`. |
## Deploy overwrites Bridge.cfg