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:
31
link/PLAN.md
31
link/PLAN.md
@@ -307,6 +307,7 @@ Counts in `hello` are a live snapshot taken on the Core thread, not a cached val
|
||||
2. ~~**Cheap event streams.**~~ **Done.** `BridgeEvents` subscribes the streams selected below. All observed on the live shard; evidence in §12.
|
||||
3. ~~**Sweeps.**~~ **Done.** `BridgeSweeps`: vitals / decay-on-transition / economy, all Core-thread timers, re-armable. Evidence in §13.
|
||||
4. ~~**Request/response.**~~ **Done.** `BridgeProfile` + `BridgeRequests`: `char.profile` (by account+slot or serial), `account.roster`, `vendor.snapshot`, `bridge.error`. Evidence in §14. Sidecar should cache profiles and rate-limit requests.
|
||||
5. ~~**`[link` account linking.**~~ **Done.** `BridgeAccountLink`: `[link` → one-time code → `link.confirm` → `WebsiteUserId` tag, persisted to `accounts.xml`. `mob.login` carries `webId`. Evidence in §15.
|
||||
5. **`[link` account linking.** `CommandSystem.Register("link", AccessLevel.Player, …)`, one-time short-TTL codes in a main-thread dict, `Account.SetTag("WebsiteUserId", id)` — persists to `accounts.xml` for free. Loopback-only is the trust boundary; add a shared secret if the sidecar is ever exposed.
|
||||
6. **Town-crier inbound.** `GlobalTownCrierEntryList.Instance.AddEntry(lines, duration)` (`Scripts/Mobiles/NPCs/TownCrier.cs:96`), marshaled to the Core thread. Cap line count/length and active entries.
|
||||
7. **Core edit: `PlayerVendorSale`** (§6). Then the cheat-detection feed.
|
||||
@@ -348,6 +349,36 @@ Two defects were found this way and fixed:
|
||||
|
||||
---
|
||||
|
||||
## 15. Phase 5 acceptance
|
||||
|
||||
`BridgeAccountLink.cs` implements `[link` and the inbound `link.confirm`. A player runs `[link`; the shard mints a one-time, expiring code (5 min TTL, unambiguous alphabet — no O/0/I/1), holds it in a Core-thread dict keyed to the account, and emits `link.request`. The player enters the code on the website; the sidecar sends `link.confirm`; the shard validates, writes the `WebsiteUserId` account tag, and replies `link.ok`.
|
||||
|
||||
Verified end to end with a smart stub (`tools/scaffolding/BridgeLinkProbe.cs` + a sidecar that reads the code and confirms it):
|
||||
|
||||
```
|
||||
<- link.request code=77M9TK account=seed_001 char=Seed001A ttlSec=300
|
||||
-> link.confirm code=77M9TK websiteUserId=web-9931
|
||||
<- link.ok code=77M9TK account=seed_001 websiteUserId=web-9931
|
||||
-> link.confirm code=BADCOD ...
|
||||
<- link.error code=BADCOD reason="unknown or expired code"
|
||||
```
|
||||
|
||||
**The tag persists.** After a `World.Save()`, `accounts.xml` contained:
|
||||
|
||||
```xml
|
||||
<tags>
|
||||
<tag name="WebsiteUserId">web-9931</tag>
|
||||
</tags>
|
||||
```
|
||||
|
||||
This is ServUO's standard account-tag format, read by `LoadTags` at boot, so the link survives restarts with no new persistence layer — as the plan promised.
|
||||
|
||||
Safeguards in place: codes are one-time and short-TTL; only the newest code per account is valid (a new `[link` drops prior codes); `[link` is rate-limited per account (30 s) against code spam; a 1-minute purge timer bounds the code table; and the `websiteUserId` is trusted only because the socket is loopback-only. `mob.login` now carries `webId` when the account is linked, so the sidecar can attribute the session without a lookup.
|
||||
|
||||
Note: the tag is written to memory on `link.confirm` but only reaches disk on the next world save (AutoSave, clean shutdown, or an explicit save). A hard crash between the two loses it — acceptable, since the player simply re-runs `[link`.
|
||||
|
||||
---
|
||||
|
||||
## 14. Phase 4 acceptance
|
||||
|
||||
`BridgeProfile.cs` builds the read-models; `BridgeRequests.cs` registers the inbound handlers (`char.request`, `account.roster`, `vendor.snapshot`). Each request may carry a `reqId` the reply echoes; an unresolvable request gets a `bridge.error` reply, never silence.
|
||||
|
||||
Reference in New Issue
Block a user