feat(auth): sign in with Steam (MODULE_API 1.17.0)

Steam is a built-in provider beside Google and Discord (STEAM_SIGNIN.md,
D392-D396). OpenID 2.0: the assertion is verified server to server with
check_authentication, return_to must match byte for byte, and the claimed id
must be a 17-digit SteamID64. A Steam Web API key is required to enable it
(D394); persona name and avatar are stored on the identity and refreshed at
each sign-in, and a failed lookup never blocks one. Steam never signs anybody
up (D395), on the web or the mobile bridge. Several Steam accounts may be
linked; DELETE /auth/me/account/identities/:provider takes ?id= to unlink one.

MODULE_API 1.17.0: ctx.identities.list, api.onIdentityLinking (refuse-only,
fails closed, module text never shown) and api.onIdentityLinked. No unlink
hook (D393). Schema additive; OpenAPI, routes and engagement manifests
regenerated with no modules installed.

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-19 14:02:49 -05:00
parent 6ba2ed90ed
commit 5e398d8acd
30 changed files with 1346 additions and 126 deletions

View File

@@ -133,8 +133,12 @@ export const api = {
totpDisable: (code) => req('/auth/me/account/totp/disable', { method: 'POST', body: { code } }),
// Linked SSO identities (self-service). Linking starts at /auth/sso/:id/link.
myIdentities: () => req('/auth/me/account/identities'),
unlinkIdentity: (provider) =>
req(`/auth/me/account/identities/${encodeURIComponent(provider)}`, { method: 'DELETE' }),
// With an identity id, only that one goes (a person may hold several Steam accounts).
unlinkIdentity: (provider, id) =>
req(
`/auth/me/account/identities/${encodeURIComponent(provider)}${id != null ? `?id=${encodeURIComponent(id)}` : ''}`,
{ method: 'DELETE' },
),
// Recovery (backup) codes. status → remaining count; generate → a fresh set,
// returned ONCE (password step-up for accounts that have a password).
recoveryCodesStatus: () => req('/auth/me/account/recovery-codes/status'),