feat(rust): a Steam account linked on the site is a Rust link (D392, D396)
Registers core's MODULE_API 1.17.0 identity hooks (coreApi ^1.17.0): onIdentityLinking refuses a Steam id that is another website account's Rust link (D396); onIdentityLinked writes the Rust link with the PRIMARY KEY as the race guard, reconciles Teams and raises the linked notice, as /link does. Unlinking Steam leaves the Rust link (D393); the Rust account page says so. ci/core-ref.json still pins the 1.15.0 core and must move to the website branch head carrying 1.17.0 before CI can pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -286,6 +286,71 @@ async function unlinkFromGame(steamId) {
|
||||
return removed
|
||||
}
|
||||
|
||||
// ── Steam's own sign-in as a second way to link (D392, D393, D396) ─────────
|
||||
//
|
||||
// Core's "Link Steam" (STEAM_SIGNIN.md) proves the account with Steam's OpenID
|
||||
// `check_authentication`, which is at least as strong as a code typed in game:
|
||||
// so a Steam account linked on the site IS a linked Rust player, beside `/link`.
|
||||
// Core asks this module twice through MODULE_API 1.17.0 — before it writes the
|
||||
// sign-in identity, and after. Unlinking Steam on the site removes only the
|
||||
// sign-in (D393), so there is no third call: the Rust link stays until it is
|
||||
// removed on the Rust page or by `/unlink` in game, exactly as one made by code.
|
||||
|
||||
const STEAM_ID = /^\d{17}$/
|
||||
|
||||
/**
|
||||
* Core's pre-link check (`api.onIdentityLinking`). Refuses a Steam id that is
|
||||
* already ANOTHER website account's Rust link (D396) — the same rule as D23 for
|
||||
* a code, for the same reason: a link here is a privilege path, and moving it
|
||||
* would hand that account's grants to whoever signed in. Core shows "already
|
||||
* linked to another account" and nothing is written; staff remove the old link
|
||||
* if the claim is real. Our own words go to core's log, never to the page.
|
||||
*/
|
||||
async function checkSteamSignin({ userId, provider, subject }) {
|
||||
if (provider !== 'steam') return { ok: true }
|
||||
const held = await db.getBySteamId(String(subject))
|
||||
if (held && Number(held.userId) !== Number(userId)) {
|
||||
return { ok: false, reason: 'in_use', message: `steam ${subject} is the Rust link of user ${held.userId}` }
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Core's linked hook (`api.onIdentityLinked`): the Steam account becomes this
|
||||
* user's Rust link (D392).
|
||||
*
|
||||
* The same write as `confirmOne`, with the PRIMARY KEY as the race guard: if
|
||||
* somebody else's `/link` landed between core's check and this, the key refuses
|
||||
* ours and their link stands — the sign-in identity core already wrote is only a
|
||||
* way to sign in. `name` is the Steam persona name, which is what a Rust player is
|
||||
* called in game until they rename. No `server_id`: no server minted anything.
|
||||
*/
|
||||
async function linkFromSteamSignin({ userId, provider, subject, displayName }) {
|
||||
if (provider !== 'steam') return
|
||||
const steamId = String(subject)
|
||||
if (!STEAM_ID.test(steamId)) return
|
||||
|
||||
const held = await db.getBySteamId(steamId)
|
||||
if (held) {
|
||||
if (Number(held.userId) !== Number(userId)) {
|
||||
log.warn('steam sign-in linked an id that is another user\'s Rust link; the Rust link stands', { steamId, userId })
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
await db.insert({ steamId, userId, name: displayName || null, serverId: null })
|
||||
} catch (err) {
|
||||
if (err && (err.code === 'ER_DUP_ENTRY' || err.errno === 1062)) {
|
||||
log.warn('steam sign-in link lost a race for this id; the other link stands', { steamId, userId })
|
||||
return
|
||||
}
|
||||
throw err
|
||||
}
|
||||
log.info('steam account linked through steam sign-in', { steamId, userId })
|
||||
linksChanged('rust account linked via steam sign-in')
|
||||
engagement.linked({ userId, steamId, name: displayName || null })
|
||||
}
|
||||
|
||||
/** The admin panel's read: every link this user holds, with per-server totals. */
|
||||
async function forAdmin(userId) {
|
||||
const links = await db.listForUserWithPlayer(userId)
|
||||
@@ -326,5 +391,7 @@ module.exports = {
|
||||
unlinkOwned,
|
||||
unlinkAnyOwner,
|
||||
unlinkFromGame,
|
||||
checkSteamSignin,
|
||||
linkFromSteamSignin,
|
||||
forAdmin,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user