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:
@@ -311,6 +311,26 @@ function buildCtx(id, moduleRoot) {
|
||||
// exports creation, role changes and password handling, none of which is a
|
||||
// module's business.
|
||||
users: { getById: users.getById },
|
||||
// API 1.17.0 (STEAM_SIGNIN.md §4). READ only, one provider at a time: what a
|
||||
// user has linked, as core verified it — the Steam id a module can trust is
|
||||
// the one Steam's own `check_authentication` proved, never one a player typed.
|
||||
// Linking and unlinking stay core's (the hooks on `api` are how a module takes
|
||||
// part), and the row id and email are core's business, so they are not here.
|
||||
identities: {
|
||||
list: async (userId, provider) => {
|
||||
// eslint-disable-next-line global-require
|
||||
const rows = await require('../model/userIdentities/userIdentities.model').listForUser(userId)
|
||||
return rows
|
||||
.filter((r) => r.provider === provider)
|
||||
.map((r) => ({
|
||||
provider: r.provider,
|
||||
subject: r.subject,
|
||||
displayName: r.display_name || null,
|
||||
avatarUrl: r.avatar_url || null,
|
||||
linkedAt: r.created_at,
|
||||
}))
|
||||
},
|
||||
},
|
||||
// Where this deployment is reachable, for a module that has to build an
|
||||
// absolute link (an announcement carries one into a game window or a chat
|
||||
// message, where a relative path means nothing). §2.7 forbids a module
|
||||
@@ -472,6 +492,17 @@ function buildApi(record) {
|
||||
// never calls them.
|
||||
onBoot: hook('onBoot'),
|
||||
onShutdown: hook('onShutdown'),
|
||||
// API 1.17.0 (STEAM_SIGNIN.md §4). Core calling the module about an external
|
||||
// identity a signed-in user is linking, dispatched from auth/identityHooks.js.
|
||||
//
|
||||
// `onIdentityLinking` answers before anything is written and can only REFUSE
|
||||
// (D396: a Steam id already another account's Rust link). It runs inside the
|
||||
// link flow of a user who is already signed in, so it has no path to a session
|
||||
// and cannot let through a link core would refuse. `onIdentityLinked` hears a
|
||||
// link after it is written. There is no unlink hook: D393 leaves a module
|
||||
// nothing to undo when a sign-in identity goes.
|
||||
onIdentityLinking: hook('onIdentityLinking'),
|
||||
onIdentityLinked: hook('onIdentityLinked'),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -794,7 +825,7 @@ function load(tierRouters) {
|
||||
staged: registries.stage(id),
|
||||
tables: new Set(),
|
||||
called: new Set(),
|
||||
hooks: { onBoot: null, onShutdown: null },
|
||||
hooks: { onBoot: null, onShutdown: null, onIdentityLinking: null, onIdentityLinked: null },
|
||||
client: null,
|
||||
ctx: null,
|
||||
state: 'installed',
|
||||
@@ -1022,6 +1053,24 @@ function shutdownHooks() {
|
||||
.reverse()
|
||||
}
|
||||
|
||||
/**
|
||||
* The identity hooks of one name (`onIdentityLinking` | `onIdentityLinked`), in
|
||||
* registration order (API 1.17.0).
|
||||
*
|
||||
* `started` only, the rule every dispatch here follows: a module that is
|
||||
* disabled, or whose `onBoot` threw, is not serving and has no say in a link.
|
||||
* Empty in a process that never loaded modules (the seed script, unit tests), so
|
||||
* the link flow there behaves exactly as a bare core.
|
||||
*
|
||||
* @returns {{id: string, hook: Function}[]}
|
||||
*/
|
||||
function identityHooks(name) {
|
||||
if (!loaded) return []
|
||||
return [...modules.values()]
|
||||
.filter((r) => r.state === 'started' && r.hooks[name])
|
||||
.map((r) => ({ id: r.id, hook: r.hooks[name] }))
|
||||
}
|
||||
|
||||
/**
|
||||
* One module's `onShutdown`, for stopping it on its own rather than at exit.
|
||||
*
|
||||
@@ -1147,6 +1196,7 @@ module.exports = {
|
||||
fragments,
|
||||
bootable,
|
||||
shutdownHooks,
|
||||
identityHooks,
|
||||
stopHook,
|
||||
clientChunks,
|
||||
clientEntryUrls,
|
||||
|
||||
Reference in New Issue
Block a user