diff --git a/modules/rust/PLAN_REDESIGNS.md b/modules/rust/PLAN_REDESIGNS.md index be5cd3f..997555b 100644 --- a/modules/rust/PLAN_REDESIGNS.md +++ b/modules/rust/PLAN_REDESIGNS.md @@ -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. 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). **The §14.2 test phase ran the same day on both rigs (§14.2.1)**: the org lead ruled D361–D362 during it and answered its Q19–Q25 the same day (D363–D369). On 2026-10-16 the org lead added console commands as event steps and rewards (§16, D382–D384), to be built before RaidableBases; **they were built the same day (§16.10)**, with D385. The same day the paid RaidableBases copy arrived; **its spike ran on both rigs (§11.4.2)**, with D386 and four questions, Q33–Q36, answered the same day (D387–D390). **§11.4 was built the same day (§11.4.6)**, with D391, core's dry-run advice. **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). **The §14.2 test phase ran the same day on both rigs (§14.2.1)**: the org lead ruled D361–D362 during it and answered its Q19–Q25 the same day (D363–D369). On 2026-10-16 the org lead added console commands as event steps and rewards (§16, D382–D384), to be built before RaidableBases; **they were built the same day (§16.10)**, with D385. The same day the paid RaidableBases copy arrived; **its spike ran on both rigs (§11.4.2)**, with D386 and four questions, Q33–Q36, answered the same day (D387–D390). **§11.4 was built the same day (§11.4.6)**, with D391, core's dry-run advice. On 2026-10-19 the org lead answered Steam sign-in's questions (D392–D396, §15), and it is built on the website's `edge` to join the cutover. **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).** **The end walk's rows that need no player ran on 2026-10-16 and 2026-10-19, and all of them are done (§9.1)**: W1–W20 and N1 found, W4 and W19 the real ones. @@ -1214,6 +1214,15 @@ The org lead answered §14.3's Q26–Q32 on 2026-10-14: | **D390** | **Lockouts are dropped from the feed (Q36).** Neither edition raises a hook for them. On the paid edition the feed gains a line when a player buys a base (`OnRaidableBasePurchased`) and when an admin locks one to a player (`OnRaidableBaseLocked`), naming the player only for the presence audience (D376). This amends D202. | Dropping them with no new lines; reading lockouts out of the plugin's data by reflection. | 11.4.2 | | **D391** | **The editor's spot check is advice through core (2026-10-16, asked during the build).** Core's dry run gains a warning channel, MODULE_API 1.16.0: a module answering `verify: true` with `{ ok: true, detail: { warnings } }` has each line shown as a warning finding that never fails the dry run. A raid base's spot refused now, and the rocks and cliffs beside it (D388), show there as advice; the run itself checks again. Steam sign-in moves to 1.17.0. | A blocked spot failing the dry run, with rocks and cliffs only in the run log; no editor check. | 11.4.6 | +The org lead answered [`website/STEAM_SIGNIN.md`](../../website/STEAM_SIGNIN.md)'s Q1–Q4 on 2026-10-19, and ruled on the +case two of them raised. They are tabled there, in §7; the ones this module builds to are: + +| # | Decision | Rejected | § | +|---|---|---|---| +| **D392** | **A Steam account linked on the site is a linked Rust player** (Q1), beside the in-game `/link` code (R1). | Steam sign-in as sign-in only. | 15 | +| **D393** | **Unlinking Steam removes only the sign-in** (Q2). The Rust link it made stays until it is removed on the Rust page. | One link with two ways to make it, gone with either. | 15 | +| **D396** | **A Steam link whose id is already another website account's Rust link is refused.** Nothing changes; staff remove the old Rust link if the claim is real, and the player links again. | Steam wins and moves the Rust link; a sign-in-only link. | 15 | + Console commands as event steps and rewards, added on 2026-10-16, were decided as D382–D384, and D385 during the build; they are tabled with their section, in §16.7. ## 11. First-class optional plugins (D199–D202) @@ -3647,7 +3656,7 @@ RaidableBases (D359).** --- -## 15. Steam sign-in (D360): on the website's plan +## 15. Steam sign-in (D360, D392, D393, D396): 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)** @@ -3656,9 +3665,11 @@ 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.16.0 there; 1.15.0 went to §14.3.13). -- **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. +- **A verified Steam id for a signed-in account**, read through the module contract (MODULE_API 1.17.0 there; + 1.15.0 went to §14.3.13 and 1.16.0 to §11.4.6). +- **A Steam identity as a linked Rust player** (D392), beside the in-game `/link` code (R1). Linking Steam makes the + Rust link; unlinking Steam leaves it (D393); a Steam id that is already another account's Rust link is refused + before anything is written (D396), which the module answers through core's pre-link check. - 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. diff --git a/website/STEAM_SIGNIN.md b/website/STEAM_SIGNIN.md index e1604fc..deb97ed 100644 --- a/website/STEAM_SIGNIN.md +++ b/website/STEAM_SIGNIN.md @@ -1,6 +1,7 @@ # 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. +> **Status: plan, written 2026-10-14; its questions answered 2026-10-19 (§7, D392–D396). Nothing is built.** It is +> built on the website's `edge` and ships in the cutover in progress (the org lead, 2026-10-19). > 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 @@ -12,7 +13,8 @@ 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 follows the **same link-only policy**, more strictly: **Steam never signs anybody up**, whatever + `player_registration` says (D395). - 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. @@ -42,7 +44,7 @@ core that no module can weaken (MODULE_SYSTEM.md). | 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/`) | -| Email and name from the IdP | **No email.** No name either, unless the site holds a Steam Web API key (Q3) | +| Email and name from the IdP | **No email.** The name and avatar come from the Steam Web API, with the key the site must hold (D394) | 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 @@ -52,57 +54,91 @@ assertion is single-use, which the existing single-use tx cookie already enforce **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. + `registry.js` (`{ id: 'steam', kind: 'steam', name: 'Steam' }`). Its config needs no client id or secret, and + **needs a Steam Web API key to be enabled** (D394): saving Steam as enabled with no key is refused with a message. + The key is stored in `client_secret_enc`, AES-256-GCM through `secretBox`, and is write-only like a client secret. +- After the assertion is verified, `handleCallback` calls `ISteamUser/GetPlayerSummaries` for the persona name and + avatar. **That call never decides a sign-in:** if it fails, the sign-in goes ahead with the bare id and a warning + in the log. The proof is `check_authentication` alone (§5). +- `auth_providers.kind` gains `steam`, and `user_identities` gains nullable `display_name` and `avatar_url`, written at + link and refreshed at each Steam sign-in. Both schema changes are additive; other providers leave the columns + null. - `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.17.0 (additions only; 1.15.0 was taken by a step's `retryAfterMs` and 1.16.0 by a dry run's advice, Rust - PLAN_REDESIGNS §14.3.13):** 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. + only, and passes them to `handleCallback`. After that, `finishLogin`, `finishLink`, `finishMobileLogin` and the + TOTP gate are unchanged, with two differences for Steam: **an unlinked Steam id never reaches + `provisionSsoPlayer`** (D395) and gets the ordinary *"no account is linked"* answer, and **`finishLink` asks the + modules first** (below). +- Admin → Auth shows Steam with an enable switch and the Web API key field. +- The account page lists Steam identities with a *Link Steam* button. Each linked Steam account shows its persona + name and avatar, its id, and an *Unlink* button. Unlinking removes the sign-in identity and nothing else (D393). +- **Module contract, MODULE_API 1.17.0 (additions only; 1.15.0 was taken by a step's `retryAfterMs` and 1.16.0 by a + dry run's advice, Rust PLAN_REDESIGNS §14.3.13 and §11.4.6):** + - a **read** of a user's verified identities for one provider; + - a **pre-link check**: before an identity is written, each module that registered one may refuse it with a + message the player sees, and nothing is written (D396). A refusal only ever stops a link; it cannot let one + through that core would refuse, and it never touches sign-in or session creation; + - a **linked hook** after the identity is written. No unlink hook: D393 leaves nothing for a module to undo. + + Names are settled at build. Module-uo uses none of them, 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; + `MODULE_API.md` for 1.17.0. **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. +**Module-Rust:** a Steam identity linked on the site **is** a linked Rust player (D392), beside the in-game `/link` +code (R1): the linked hook writes the Rust link, and the pre-link check refuses a Steam id that is already another +website account's Rust link (D396). Unlinking Steam leaves the Rust link in place (D393); it is removed on the Rust +page as today. ## 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. +- Nothing secret ships to a client. The 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). + skipped. The Web API lookup only decorates an identity; it is never proof. +- **Link-only, with no exception:** a Steam id that is not linked signs nobody in, and SSO sign-up never applies to + Steam (D395). +- A module's pre-link check can only refuse. It runs inside `finishLink` for an already signed-in user, so it has no + path to a session. - 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. + assertion, a wrong `op_endpoint`, a failed Web API lookup that still signs in, enabling with no key refused. +- Flow: login, link and mobile modes against a stubbed Steam endpoint, including the TOTP gate, an unlinked Steam id + refused with SSO sign-up on, a module's pre-link refusal writing nothing, and the linked hook firing once. - 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. + It needs the org lead (a real Steam account, and a Web API key for the review stack), so it joins the end walk's + in-game rows rather than holding the build. -## 7. Questions for the org lead +## 7. Questions, answered 2026-10-19 + +The org lead answered Q1–Q4 and ruled on the case Q1 and Q2 raised together. The D-numbers continue Rust's +[`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §10, where D360 was recorded. - **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. + her kit rewards and permissions reach it, without the in-game `/link` code? *Proposed:* yes. **Answered: yes + (D392).** - **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. + too, and with it the grants D28 resolves to that Steam id? *Proposed:* both go. **Answered: only the sign-in goes + (D393).** - **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. + needs a Steam Web API key, which an admin would paste into Admin → Auth like a client secret. *Proposed:* an + optional key. **Answered: the key is required (D394).** - **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. + *Proposed:* allowed. **Answered: never (D395).** +- **The conflict.** Bob linked Steam id 7656…001 with the in-game `/link` code months ago. Alice, a different + website account, clicks *Link Steam* and signs in to steamcommunity.com as 7656…001. **Answered: Alice's link is + refused (D396).** + +| # | Decision | Rejected | +|---|---|---| +| **D392** | **A Steam account linked on the site is a linked Rust player**, beside the in-game `/link` code (R1). Steam's sign-in proves the account at least as well as the code does, and both routes stay. | Steam sign-in as sign-in only. | +| **D393** | **Unlinking Steam removes only the sign-in identity.** A Rust link it made stays, with its grants, until it is removed on the Rust page. | One link with two ways to make it, gone with either. | +| **D394** | **Steam needs a Steam Web API key to be enabled.** Admin → Auth refuses to enable Steam without one; with it, a linked Steam account shows its persona name and avatar. The key is stored like a client secret and is never returned. A failed lookup never stops a sign-in. | An optional key with bare ids without it; never calling the Web API. | +| **D395** | **Steam never signs anybody up.** An unlinked Steam id signs nobody in, even when `player_registration` allows SSO sign-up; a player links Steam to an account they already have. | Sign-up through Steam with no email. | +| **D396** | **A Steam link whose id is already another website account's Rust link is refused.** Alice sees *"This Steam account is already linked to another account on this site"*, and nothing changes. If her claim is real, staff remove Bob's Rust link and she links again. Core makes this possible through a module pre-link check (§4). | Steam wins and moves the Rust link to Alice; Alice gets the sign-in but not the Rust link. |