From fb7511d700dd2468201698f4091a6b030bcc1f9f Mon Sep 17 00:00:00 2001 From: wtclaude Date: Wed, 28 Oct 2026 04:44:28 -0500 Subject: [PATCH] fix(demo): lock removing a Steam link on the demo (DM27) The step-5 walk found demo-player could remove its stand-in Steam link from its Rust page, and staff could remove it from Admin -> Users. That link is what the player tour's Rust pages, rewards and grants hang on. - module.json demoLocks gains DELETE /player/rust/links/:steamId. - The staff route, DELETE /admin/users/:id/rust/links/:steamId, serves under core's users mount, which a module's demoLocks may not name, so its handler refuses with DEMO_LOCKED behind ctx.demoMode before the link is touched. Off the demo both behave as before. Checked on a real core with the demo seed: both answer 403 DEMO_LOCKED and the link stays. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- module.json | 3 +- server/demo.js | 10 +++++-- server/router/admin/usersRust.controller.js | 5 ++++ server/test/demo.test.js | 31 ++++++++++++++++++++- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/module.json b/module.json index 0c849d4..df76427 100644 --- a/module.json +++ b/module.json @@ -16,6 +16,7 @@ "capabilities": ["rust", "servers", "killfeed", "leaderboard", "presence", "wipes", "identity", "map"], "demoLocks": [ { "method": "PUT", "path": "/admin/rust/servers/:id" }, - { "method": "DELETE", "path": "/admin/rust/servers/:id" } + { "method": "DELETE", "path": "/admin/rust/servers/:id" }, + { "method": "DELETE", "path": "/player/rust/links/:steamId" } ] } diff --git a/server/demo.js b/server/demo.js index 1527b9c..cfdf7a9 100644 --- a/server/demo.js +++ b/server/demo.js @@ -1,9 +1,13 @@ // ── The public demo's module-side locks (DEMO_MODE.md §3.4, §3.5) ────────── // -// Core locks this module's two link routes by `demoLocks` in module.json. What a -// route list cannot express lives here, behind `ctx.demoMode` (MODULE_API -// 1.18.0), and every handler that enforces one of these asks this file: +// Core locks this module's two rig-link routes and the player's Steam unlink by +// `demoLocks` in module.json. What a route list cannot express lives here, behind +// `ctx.demoMode` (MODULE_API 1.18.0), and every handler that enforces one of these +// asks this file: // +// the staff Steam unlink on Admin → Users (step-5 walk, W4): it serves under +// core's users mount, which a module's demoLocks may not name, so its handler +// refuses instead. The demo player's seeded link stays put either way; // the console-command deny list (§3.4, DM11): the commands that would break // the rig for every visitor until the next wipe. Asked at save, through // core's `demoRefuses` hook (1.19.0, DM18), and again at run; diff --git a/server/router/admin/usersRust.controller.js b/server/router/admin/usersRust.controller.js index 1355ed3..6380a8b 100644 --- a/server/router/admin/usersRust.controller.js +++ b/server/router/admin/usersRust.controller.js @@ -8,6 +8,7 @@ const crypto = require('node:crypto') const core = require('../../core') +const demo = require('../../demo') const backpacks = require('../../model/backpacks/backpacks.model') const economics = require('../../model/economics/economics.model') @@ -54,6 +55,10 @@ async function removeLink(req, res) { const { steamId } = req.params const userId = req.params.id + // On the demo the player's own unlink is locked by demoLocks; this route serves + // under core's users mount, which a module may not lock by route, so it asks here. + if (demo.isDemo()) return demo.refuse(res) + try { const removed = await links.unlinkOwned(steamId, userId) diff --git a/server/test/demo.test.js b/server/test/demo.test.js index ff4459b..7c7bf22 100644 --- a/server/test/demo.test.js +++ b/server/test/demo.test.js @@ -447,11 +447,40 @@ test('the seed sets the faction table, saves each profile, and styles each group // ── The manifest (§3.2) ──────────────────────────────────────────────────── -test('module.json locks the two link routes and needs a core that asks demoRefuses', () => { +test('module.json locks the two rig-link routes and the player Steam unlink, and needs a core that asks demoRefuses', () => { const manifest = require('../../module.json') assert.deepEqual(manifest.demoLocks, [ { method: 'PUT', path: '/admin/rust/servers/:id' }, { method: 'DELETE', path: '/admin/rust/servers/:id' }, + { method: 'DELETE', path: '/player/rust/links/:steamId' }, ]) assert.equal(manifest.coreApi, '^1.19.0') }) + +// ── The staff Steam unlink (W4) ──────────────────────────────────────────── + +test('staff cannot sever a Steam link on the demo, and the link is never touched', async () => { + for (const demoMode of [true, false]) { + withCore({ demo: demoMode }) + const links = require('../model/links/links.model') + const asked = [] + links.unlinkOwned = async (steamId, userId) => { + asked.push([steamId, userId]) + return true + } + const res = fakeRes() + await require('../router/admin/usersRust.controller').removeLink( + { params: { id: '3', steamId: '76561190000000001' }, user: { id: 1 } }, + res, + ) + if (demoMode) { + assert.equal(res.statusCode, 403) + assert.equal(res.body.code, 'DEMO_LOCKED') + assert.deepEqual(asked, []) + } else { + assert.equal(res.statusCode, 200) + assert.deepEqual(res.body, { unlinked: true }) + assert.deepEqual(asked, [['76561190000000001', '3']]) + } + } +}) -- 2.49.1