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:
2026-10-19 14:02:53 -05:00
parent f93ba86883
commit b89039142e
8 changed files with 148 additions and 2 deletions

View File

@@ -166,6 +166,9 @@ function fakeApi() {
registerSlashCommands(commands) { once('registerSlashCommands'); record.slashCommands = commands },
onBoot(fn) { once('onBoot'); record.hooks.onBoot = fn },
onShutdown(fn) { once('onShutdown'); record.hooks.onShutdown = fn },
// MODULE_API 1.17.0 (Steam sign-in, PLAN_REDESIGNS §15).
onIdentityLinking(fn) { once('onIdentityLinking'); record.hooks.onIdentityLinking = fn },
onIdentityLinked(fn) { once('onIdentityLinked'); record.hooks.onIdentityLinked = fn },
}
api.record = record
return api

View File

@@ -85,6 +85,13 @@ test('registers both lifecycle hooks', () => {
assert.strictEqual(typeof api.record.hooks.onShutdown, 'function')
})
test('registers both identity hooks, so a Steam sign-in link is a Rust link (D392, D396)', () => {
const { api } = register()
const links = require('../model/links/links.model')
assert.strictEqual(api.record.hooks.onIdentityLinking, links.checkSteamSignin)
assert.strictEqual(api.record.hooks.onIdentityLinked, links.linkFromSteamSignin)
})
test('the manifest declares what the loader requires', () => {
assert.match(manifest.id, /^[a-z][a-z0-9-]{1,31}$/)
assert.match(manifest.version, /^\d+\.\d+\.\d+/)

View File

@@ -429,3 +429,53 @@ test('a state read that fails asks everybody, which is slow and never wrong', as
assert.equal((await links.redeem({ code: 'ZZZZZZ', userId: 4 })).reason, 'rejected')
assert.equal(asked.length, 2)
})
// ── Steam's own sign-in as a second way to link (D392, D393, D396) ─────────
const STEAM = '76561197960287930'
test('core’s pre-link check refuses a Steam id that is another account’s Rust link (D396)', async () => {
withCore({ select: [[{ steamId: STEAM, userId: 9, username: 'bob' }]] })
const links = require('../model/links/links.model')
const answer = await links.checkSteamSignin({ userId: 4, provider: 'steam', subject: STEAM })
assert.equal(answer.ok, false)
assert.equal(answer.reason, 'in_use')
})
test('the pre-link check passes a free Steam id, one already yours, and every other provider', async () => {
withCore({ select: [[], [{ steamId: STEAM, userId: 4 }]] })
const links = require('../model/links/links.model')
assert.deepEqual(await links.checkSteamSignin({ userId: 4, provider: 'steam', subject: STEAM }), { ok: true })
assert.deepEqual(await links.checkSteamSignin({ userId: 4, provider: 'steam', subject: STEAM }), { ok: true })
assert.deepEqual(await links.checkSteamSignin({ userId: 4, provider: 'google', subject: 'x' }), { ok: true })
})
test('a Steam account linked on the site becomes a linked Rust player (D392)', async () => {
let inserted = null
const { ctx } = withCore({ select: [[]], onInsert: (params) => { inserted = params; return Promise.resolve({ affectedRows: 1 }) } })
const links = require('../model/links/links.model')
await links.linkFromSteamSignin({ userId: 4, provider: 'steam', subject: STEAM, displayName: 'CoolGuy99' })
assert.deepEqual(inserted, [STEAM, 4, 'CoolGuy99', null])
assert.equal(ctx.teams.reconcile.calls.length, 1)
assert.equal(ctx.events.emit.calls.length, 1, 'the player hears about the new link, as with /link')
})
test('the linked hook writes nothing for a link that is already there, or for another provider', async () => {
let inserts = 0
const { ctx } = withCore({ select: [[{ steamId: STEAM, userId: 4 }]], onInsert: () => { inserts += 1; return Promise.resolve({}) } })
const links = require('../model/links/links.model')
await links.linkFromSteamSignin({ userId: 4, provider: 'steam', subject: STEAM, displayName: 'x' })
await links.linkFromSteamSignin({ userId: 4, provider: 'discord', subject: '123', displayName: 'x' })
await links.linkFromSteamSignin({ userId: 4, provider: 'steam', subject: 'not-an-id', displayName: 'x' })
assert.equal(inserts, 0)
assert.equal(ctx.teams.reconcile.calls.length, 0)
})
test('a /link that lands between core’s check and the hook keeps its link; the hook does not throw', async () => {
const dup = Object.assign(new Error('dup'), { code: 'ER_DUP_ENTRY' })
const { ctx } = withCore({ select: [[]], onInsert: () => Promise.reject(dup) })
const links = require('../model/links/links.model')
await links.linkFromSteamSignin({ userId: 4, provider: 'steam', subject: STEAM, displayName: 'x' })
assert.equal(ctx.teams.reconcile.calls.length, 0)
})