docs: record Q12-Q18 (D353-D359) and plan Steam sign-in on the website (D360)

The org lead answered §13's and §14's questions on 2026-10-14:
D353 public feed with name and grid, staff adds the Steam id; D354 an
event can call a vanilla event, kept at teardown; D355 every world event
is a trigger; D356 Safecracker and Scavenger; D357 protocol 14; D358
BotReSpawn stays out; D359 the end walk waits for RaidableBases (against
the proposal), recorded in §9 too.

D360: Steam sign-in in the same manner as Google and Discord, kept on
the website's plan (D171). New website/STEAM_SIGNIN.md (OpenID 2.0
adapter, the build per repo, security, tests, Q1-Q4), indexed in the
README, linked from PLAN_REDESIGNS §15 and PLAN_FIXES §4.8, and on the
roadmap as the website milestone "Steam sign-in" (website#214).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-14 05:09:26 -05:00
parent 6959f764db
commit 52bf9ca203
4 changed files with 150 additions and 2 deletions

View File

@@ -36,6 +36,7 @@ sidecar as a service, and hands you the values the website needs.
| [WIKI_UPGRADE.md](website/WIKI_UPGRADE.md) | Wiki subsystem upgrade notes |
| [SHARD_VISIBILITY.md](website/SHARD_VISIBILITY.md) | Who sees which shard data — the admin-configurable audience framework |
| [TRUSTED_DEVICES_MFA.md](website/TRUSTED_DEVICES_MFA.md) | TOTP two-factor, trusted devices and recovery codes |
| [STEAM_SIGNIN.md](website/STEAM_SIGNIN.md) | **Plan** — Sign in with Steam as a built-in provider beside Google and Discord (OpenID 2.0), and a Steam id for modules |
| [MODERATION_APPEALS.md](website/MODERATION_APPEALS.md) | Moderation actions, content reports and the appeals flow |
| [SPAWN_ATLAS.md](website/SPAWN_ATLAS.md) | The bestiary / spawn atlas: what the shard contains, parsed from the shard's own ServUO files — served over the bridge since Protocol 8, so no shared filesystem |
| [CLILOCS.md](website/CLILOCS.md) | UO's id → name table, so items have names. The shard decompresses and serves it over the bridge; the desktop conversion it replaced is gone |

View File

@@ -374,6 +374,9 @@ a display name. The goal the org lead named is server customisation — NPCs wit
### 4.8 Steam sign-in (moved to the website's plan, D171)
**Planned 2026-10-14 in [`../../website/STEAM_SIGNIN.md`](../../website/STEAM_SIGNIN.md)** (PLAN_REDESIGNS D360),
on the roadmap as the `website` milestone *Steam sign-in*.
Steam as a sign-in provider beside Google and Discord is part of the base website and is planned there, not
here. What this module needs from it: a way to learn a signed-in account's verified Steam id, so linking by
Steam can stand beside the in-game code (R1). Steam is OpenID 2.0, not OAuth2/OIDC, so the website needs its

View File

@@ -11,7 +11,7 @@ chose neither route: Runic Gateway writes its own NPC plugin, RunicNPC, planned
[`../../runicnpc/PLAN.md`](../../runicnpc/PLAN.md) (D214–D314), and the rest of this plan waits for it.** RunicNPC v1.0.0 shipped on 2026-10-09, and the plan
resumed with §2 the same day (D331–D334); §2 was built on 2026-10-12, and §11's Economics spike the same day
raised D335–D336 (§11.2.1). Economics was built on 2026-10-12 too (§11.2.4). The Backpacks spike followed and raised D337–D339 (§11.3.1), and Backpacks was built the same day (D340, D341, §11.3.4). The org lead then added permission rewards (§12,
D342–D346), planned and built the same day. On 2026-10-14 RaidableBases was deferred until the current paid copy arrives (D347), with both editions to be supported (D348, §11.4.2). The same day the org lead added Rust's own world events to the base module (§13, D349) and six more first-class optional plugins (§14, D350–D352), to be tested together in one phase before their integration plan. **Every item left is built, tested and PR'd first; every walk —
D342–D346), planned and built the same day. On 2026-10-14 RaidableBases was deferred until the current paid copy arrives (D347), with both editions to be supported (D348, §11.4.2). The same day the org lead added Rust's own world events to the base module (§13, D349) and six more first-class optional plugins (§14, D350–D352), to be tested together in one phase before their integration plan. Their questions were answered the same day (D353–D359, the end walk now waiting for RaidableBases), and Steam sign-in stays on the website's plan, linked as §15 (D360). **Every item left is built, tested and PR'd first; every walk —
theirs and the ones still owed by §3 and §5 — happens together at the end (D331, §9).**
This is a companion to [`PLAN_FIXES.md`](PLAN_FIXES.md) and [`PLAN.md`](PLAN.md). Where they disagree, this
@@ -29,6 +29,7 @@ document is later and wins. Its decisions continue PLAN_FIXES' numbering at **D1
| 12 | Permissions as event rewards: until the event ends, or for good | — (added 2026-10-12) | **core** (website `main`, MODULE_API 1.14.0), Module-Rust |
| 13 | Rust's own world events (helicopter, Bradley, cargo, Chinook, airdrops, locked crates) in the base module | — (added 2026-10-14) | Rust-Plugins, Rust-Link, Module-Rust |
| 14 | Six more optional plugins: TruePVE, Dynamic PVP, Dangerous Treasures, Guarded Crate, ZLevels, Server Rewards | — (added 2026-10-14) | Rust-Plugins, Rust-Link, Module-Rust, Android-app |
| 15 | Steam sign-in: a link to the website's own plan, [`website/STEAM_SIGNIN.md`](../../website/STEAM_SIGNIN.md) | §4.8, D171 | **core** (website), Android-app, Module-Rust |
**Protocol.** §1, §3, §4 and §5 joined **protocol 13**, which shipped on 2026-10-09 with RunicNPC v1.0.0
(bundle 2026.10.09). The wire changes still to come, §2's and §11's, **open protocol 14** on `edge` and are
@@ -931,7 +932,8 @@ pulls) is research, not a walk, so it still comes first:
**Then one walk, on Oxide and Carbon, of everything still owed (D331):** §2.7; §11's rows; §12.9; §13's and
§14's rows; the in-game rows
of §5.7 and §3.4; PLAN_FIXES §8's re-walk, including the Carbon pass of the first player walk. What it finds
is fixed, protocol 14 is released, and the cutover follows.
is fixed, protocol 14 is released, and the cutover follows. **The walk waits for RaidableBases (D359)**, so it
runs only once §11.4 is built from the current paid copy (D347).
## 10. Decisions of record
@@ -1043,6 +1045,19 @@ The plugin survey the same day led to four more, decided by the org lead on 2026
| **D351** | **As few helper plugins as possible, and nothing that runs afoul of a licence.** Runic Gateway never copies, modifies, bundles or redistributes a third-party plugin; it uses only their public hooks, methods and console commands, and operators install them from uMod. | A helper per plugin by default; shipping plugins with the installer or egg. | 14 |
| **D352** | **One test phase covers §13 and all six plugins together, on both rigs; then one integration plan, answered before code.** | A spike and a plan per plugin, as §11 did. | 14.2, 14.3 |
The org lead answered §13's and §14's questions the same day, and added Steam sign-in:
| # | Decision | Rejected | § |
|---|---|---|---|
| **D353** | **The public feed shows a world event with its grid and the player's name; the staff feed adds the Steam id** (Q12). | Staff only. | 13.4 |
| **D354** | **An event can call a vanilla event** (patrol helicopter, cargo ship, Chinook, airdrop at a grid or position) through a new step whose result is kept (`reversible: 'none'`): nothing is removed when the run ends. A new verb, not an exception to D88 (Q13). | No such step; removing the called event at teardown. | 13.4 |
| **D355** | **Every world event in §13.2 is an event-system trigger** (Q14). | Feed only. | 13.4 |
| **D356** | **Two new counts with titles: locked crates hacked (*Safecracker*) and airdrops looted first (*Scavenger*)** (Q15). Barrels broken, K/D ratio and time played are not added. | All of PlayerRanks' extra counts. | 13.4 |
| **D357** | **§13 and §14 join protocol 14** (D332), released with the rest after the end walk (Q16). | A protocol of their own. | 13.4 |
| **D358** | **BotReSpawn stays out** (Q17). | Feed and kill-count support for its bots. | 14.4 |
| **D359** | **The end walk waits for RaidableBases** (Q18). D331's one walk runs once every item is coded, §11.4 included, and §11.4 waits for the current paid copy (D347). | Walking the rest without it and giving RaidableBases its own walk later. | 9, 14.4 |
| **D360** | **Steam sign-in is supported in the same manner as the other providers:** a built-in provider beside Google and Discord, link-only, through the one session service, configured on Admin → Auth, on the website and in the Android app. **It stays on the website's plan (D171)**, now written as [`website/STEAM_SIGNIN.md`](../../website/STEAM_SIGNIN.md) and on the roadmap as the `website` milestone *Steam sign-in*. §15 links it. | Planning it inside this document. | 15 |
## 11. First-class optional plugins (D199–D202)
The org lead named four plugins the module supports as first-class integrations. Kits already is one. The other
@@ -1982,6 +1997,8 @@ Kill credit reuses D174's rule, so the feed and *Rotorbane* never disagree about
### 13.4 Questions for the org lead
**Answered on 2026-10-14: every proposal below was accepted (D353–D357).**
- **Q12 — Who sees each line in the feed?** For example, *"Patrol helicopter downed by Alice at G7"* and
*"Bob started hacking the locked crate on Large Oil Rig"*. Everyone, or staff only? **Proposed:** the public feed
shows the event, the grid and the player's name; the staff feed adds the Steam id. The map already shows
@@ -2091,6 +2108,9 @@ PVP, then the other four.
### 14.4 Questions for the org lead
**Answered on 2026-10-14:** Q17 as proposed, out (D358). **Q18 against the proposal: the end walk waits for
RaidableBases (D359).**
- **Q17 — Is BotReSpawn in or out?** It is paid ($40) and runs its own NPC AI, profiles and monument spawns,
which RunicNPC already does. It cannot drive RunicNPC. The most it could add is its bots named in the feed
and kill counts. **Proposed:** out. It can be added later under D348's rules, with a licensed copy for the rigs.
@@ -2098,3 +2118,20 @@ PVP, then the other four.
RaidableBases now waits for a copy with no date (D347). For example, if the paid copy arrives in a month, do §2,
§11.2, §11.3, §12, §13 and §14 wait a month for their walk? **Proposed:** no. The walk runs when §13 and §14
are coded, and RaidableBases gets its own walk after its build.
---
## 15. Steam sign-in (D360): on the website's plan
The org lead asked on 2026-10-14 for Steam sign-in **in the same manner as the other providers**, and for it to
stay on the website's plan (D171). It is planned in **[`website/STEAM_SIGNIN.md`](../../website/STEAM_SIGNIN.md)**
and is on the roadmap as the `website` milestone *Steam sign-in*. It is core work, not an item of this plan, and
it does not hold the end walk (D359).
What this module takes from it:
- **A verified Steam id for a signed-in account**, read through the module contract (MODULE_API 1.15.0 there).
- **A Steam identity as a linked Rust player**, beside the in-game `/link` code (R1). Whether it counts as a link
is that plan's Q1, and what unlinking removes is its Q2.
- When it lands, Module-Rust's account screens and the link flow are updated in their own PR. Rust's grants
already follow the website user to every linked Steam id (D28), so nothing else changes.

107
website/STEAM_SIGNIN.md Normal file
View File

@@ -0,0 +1,107 @@
# Sign in with Steam — plan
> **Status: plan, written 2026-10-14. Nothing is built.** Its four questions (§7) are for the org lead before code.
> It is the website's own plan for Steam sign-in, as Rust's
> [`PLAN_FIXES.md`](../modules/rust/PLAN_FIXES.md) D171 set out (§4.8 there), and it is on the roadmap as the
> `website` milestone *Steam sign-in*, issue website#214. Rust's [`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §15 links
> here for what the Rust module needs from it.
## 1. Goal
**Steam becomes a sign-in provider in the same manner as the others** (the org lead, 2026-10-14; D360 in
PLAN_REDESIGNS). That means:
- It is a **built-in provider beside Google and Discord**. An admin turns it on in Admin → Auth.
- It follows the **same policy**: link-only, with opt-in sign-up when `player_registration` allows SSO.
- It creates sessions through the **same session service**, with the TOTP gate and the trusted-device skip.
- It is linked and unlinked from a player's account page, as the others are.
- It works **in the Android app** through the existing mobile SSO bridge (M9), like the others.
What it adds beyond that: **modules can learn a signed-in account's verified Steam ids**. That is what the Rust
module has needed since its dry run (`modules/rust-dryrun.md` finding 1, MODULE_SYSTEM.md's "a module cannot
register an identity provider").
Out of scope: a module registering its own identity provider. Steam is core's, so session creation stays a part of
core that no module can weaken (MODULE_SYSTEM.md).
## 2. What exists
| Piece | Where | Shape |
|---|---|---|
| Providers | `server/src/auth/providers/` | `registry.js` maps a `kind` to a class. Built-ins `google` and `discord` always exist as defaults; `oidc`/`oauth2` are custom. Each class implements `getAuthorizationUrl(state, options)` and `handleCallback(params)` → `{ subject, email, emailVerified, name }` |
| Provider config | `auth_providers` | `kind ENUM('google','discord','oidc','oauth2')`, `client_id`, `client_secret_enc` (AES-256-GCM via `secretBox`), URLs, scopes |
| Identities | `user_identities` | `(provider, subject)` unique, `user_id` indexed. **Several identities per provider per user are already allowed**, which several Steam accounts need |
| The flow | `router/v1/auth/sso.controller.js` | `beginFlow` (modes `login`, `link`, `mobile`) signs a single-use `sso_tx` cookie. `callback` reads `code` and `state` from the query, verifies the tx, calls `handleCallback`, then `finishLogin` / `finishLink` / `finishMobileLogin`. `provisionSsoPlayer` creates a player when registration allows SSO sign-up |
| Android | `/auth/mobile/sso/*` (M9) | A Custom Tab runs the same `/auth/sso/:provider/*` flow; the app receives a one-time code and exchanges it with PKCE |
## 3. How Steam differs: OpenID 2.0, not OAuth2
| OAuth2 / OIDC (today) | Steam |
|---|---|
| Redirect to an authorize URL with `client_id`, `state`, PKCE | Redirect to `https://steamcommunity.com/openid/login` with `openid.mode=checkid_setup`, `openid.return_to`, `openid.realm` and the identifier-select fields. **No client id, no secret, no `state` parameter** |
| Callback carries `code` and `state` | Callback carries `openid.*` fields. **Our `state` must ride inside `return_to`**, and the tx cookie verifies it as today |
| Exchange the code at the token endpoint | **Verify the assertion** by POSTing the same fields back to Steam with `openid.mode=check_authentication`, and accepting only `is_valid:true` |
| `subject` from the ID token or userinfo | `subject` is the 17-digit SteamID64 at the end of `openid.claimed_id` (`https://steamcommunity.com/openid/id/<id>`) |
| Email and name from the IdP | **No email.** No name either, unless the site holds a Steam Web API key (Q3) |
The checks the adapter must make: `openid.op_endpoint` is Steam's, `return_to` matches the URL we sent, the
claimed id matches `^https://steamcommunity\.com/openid/id/\d{17}$`, and `check_authentication` says valid. Each
assertion is single-use, which the existing single-use tx cookie already enforces.
## 4. The build
**Core (`website`):**
- `steam.provider.js`, a `BaseProvider` with `getAuthorizationUrl` and `handleCallback` doing §3. It is a built-in in
`registry.js` (`{ id: 'steam', kind: 'steam', name: 'Steam' }`), and its config validates with no client id or secret.
- `auth_providers.kind` gains `steam`. The schema change is additive.
- `sso.controller.js`'s `callback` accepts Steam's `openid.*` fields where it now requires `code`, for `kind: steam`
only, and passes them to `handleCallback`. Everything after `handleCallback` is unchanged: `finishLogin`,
`finishLink`, `finishMobileLogin`, the TOTP gate and provisioning.
- Admin → Auth shows Steam with an enable switch and, per Q3, an optional Web API key stored like a client secret.
- The account page lists Steam identities with a *Link Steam* button, and each linked Steam account is shown with its
id (or name, Q3) and an *Unlink* button.
- **Module contract, MODULE_API 1.15.0 (additions only):** a read of a user's verified identities for one provider,
and a hook when one is linked or unlinked. Names are settled at build. Module-uo uses neither, and runs its usual
frozen-manifest proof against the new core.
- OpenAPI annotations and the regenerated spec for anything new or changed; `BACKEND_DESIGN.md` §3/§4/§6.
**Android (`Android-app`):** the provider list comes from `/auth/providers`, so Steam appears where the app lists
providers. The work is the button's mark and a check of the M9 bridge with Steam's callback. Confirmed in the build.
**Module-Rust:** a Steam identity on the account becomes a linked Rust player (Q1), beside the in-game `/link` code
(R1). Unlinking follows Q2.
## 5. Security
- Nothing secret ships to a client. The optional Web API key is server-side and write-only in the API, like client
secrets.
- The `check_authentication` call is the only proof of a Steam assertion. It is made server to server, and never
skipped.
- **Link-only stays the rule:** a Steam id that is not linked signs nobody in, unless registration opts into SSO
sign-up (Q4).
- Login hardening (rate limits, bot score, honeypot) applies to the Steam routes as to the others.
## 6. Tests
- Unit: the claimed-id parser, `return_to` matching, a `check_authentication` reply of `is_valid:false`, a replayed
assertion, a wrong `op_endpoint`.
- Flow: login, link and mobile modes against a stubbed Steam endpoint, including the TOTP gate and provisioning.
- Module-uo proof against the new core.
- A live walk: sign in with a real Steam account on the local review stack, link, unlink, and the same in the app.
## 7. Questions for the org lead
- **Q1 — Does a Steam sign-in count as a linked Rust player?** For example, Alice clicks *Link Steam* on her account
page and approves it on steamcommunity.com. Does the Rust module then treat her as owning that Steam account, so
her kit rewards and permissions reach it, without the in-game `/link` code? **Proposed:** yes. Steam's own sign-in
proves the account at least as well as the code does, and both routes stay.
- **Q2 — What does unlinking remove?** For example, Alice unlinks Steam on her account page. Does her Rust link go
too, and with it the grants D28 resolves to that Steam id? **Proposed:** the two are one link with two ways to
make it, so removing either removes the Rust link. The site says so before it does.
- **Q3 — Names and avatars.** Steam's sign-in returns only the Steam id. Showing *"Steam: CoolGuy99"* with an avatar
needs a Steam Web API key, which an admin would paste into Admin → Auth like a client secret. **Proposed:** an
optional key. Without it the account page shows the Steam id, and a provisioned username is `steam_` plus the
last digits of the id.
- **Q4 — Sign-up through Steam.** When registration allows SSO sign-up, a new account from Steam has **no email**.
**Proposed:** allowed, as for any provider that sends no email. The player adds and verifies an address through
the ordinary flow later.