feat(demo): the public demo's module-side locks and seed hook
website/DEMO_MODE.md §3.2, §3.4, §3.5, §7.3 and §7.5 (step 2 of §10). Nothing changes off the demo; everything below sits behind ctx.demoMode. - demoLocks in module.json: PUT and DELETE /admin/rust/servers/:id. - The console-command deny list (DM11): quit, restart, oxide.unload, oxide.reload, rcon.*, fps.limit and server.tickrate, by first word, with global. stripped and o. read as oxide. Refused at save through core's demoRefuses (MODULE_API 1.19.0, DM18) and again at run, with retry: false, before the rig is asked. Teardown commands too. - The config editor (DM14): the bridge's files are left out of the list and refused for reading, writing and in the write history, by name (canonicalised) and by the catalogue's self. Other plugins' credential fields are redacted in the text and the form and made read-only; raw-JSON saves are refused; a file that does not parse is withheld. The admin server list leaves out the sidecar URL. - onDemoSeed (DM15): upserts the rig's server row with an encrypted token and demo-player's stand-in Steam link. - The release's install manifest carries protocol, read from PROTOCOL_VERSION, for the wipe's bundle match (DM12). coreApi is ^1.19.0; ci/core-ref.json pins website#224's head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
97
server/demoSeed.js
Normal file
97
server/demoSeed.js
Normal file
@@ -0,0 +1,97 @@
|
||||
// ── The public demo's seed: this module's rows (MODULE_API 1.18.0, DM15) ────
|
||||
//
|
||||
// `npm run seed:demo` hands each module its `modules.<id>` section of the
|
||||
// fixture, with every `"$env:NAME"` already replaced, and the demo accounts' ids
|
||||
// (DEMO_MODE.md §7.3). This module writes two kinds of row from it, through its
|
||||
// own models; core never writes them:
|
||||
//
|
||||
// servers the link to the rig: the sidecar URL and token come from the host's
|
||||
// .env, so the token is never in the repo
|
||||
// links demo-player's stand-in Steam id (DM6), so the player screens have
|
||||
// somebody to show
|
||||
//
|
||||
// { "servers": [{ "id": "main", "name": "Demo", "sidecarBaseUrl": "$env:RUST_SIDECAR_URL",
|
||||
// "sidecarToken": "$env:RUST_SIDECAR_TOKEN" }],
|
||||
// "links": [{ "username": "demo-player", "steamId": "7656119…", "serverId": "main" }] }
|
||||
//
|
||||
// **It runs on every seed, so it upserts.** Core seeds content on a database's
|
||||
// first run only, but calls this every time. Everything is checked before
|
||||
// anything is written, and a problem throws, which fails the seed.
|
||||
|
||||
const linksDb = require('./model/links/links.db')
|
||||
const servers = require('./model/servers/servers.model')
|
||||
const serversDb = require('./model/servers/servers.db')
|
||||
const sidecar = require('./sidecarClient')
|
||||
const core = require('./core')
|
||||
|
||||
const log = core.logger('demo')
|
||||
|
||||
const SERVER_ID = /^[a-z0-9][a-z0-9-]{0,63}$/
|
||||
const STEAM_ID = /^7656119\d{10}$/
|
||||
|
||||
/** Every problem with the section, as sentences. Empty when it is good to write. */
|
||||
function checkSection(data, accounts = {}) {
|
||||
const problems = []
|
||||
if (!data || typeof data !== 'object' || Array.isArray(data)) return ['modules.rust must be an object']
|
||||
|
||||
const list = (key) => (data[key] === undefined ? [] : data[key])
|
||||
for (const key of ['servers', 'links']) {
|
||||
if (data[key] !== undefined && !Array.isArray(data[key])) problems.push(`modules.rust.${key} must be an array`)
|
||||
}
|
||||
if (problems.length) return problems
|
||||
|
||||
list('servers').forEach((s, i) => {
|
||||
const at = `modules.rust.servers[${i}]`
|
||||
if (!s || typeof s !== 'object') return problems.push(`${at} must be an object`)
|
||||
if (!SERVER_ID.test(String(s.id || ''))) problems.push(`${at}.id must be lowercase letters, digits and hyphens`)
|
||||
if (typeof s.name !== 'string' || !s.name.trim()) problems.push(`${at}.name is required`)
|
||||
if (!/^https?:\/\//.test(String(s.sidecarBaseUrl || ''))) problems.push(`${at}.sidecarBaseUrl must be an http(s) URL`)
|
||||
if (typeof s.sidecarToken !== 'string' || !s.sidecarToken) problems.push(`${at}.sidecarToken is required`)
|
||||
return undefined
|
||||
})
|
||||
|
||||
list('links').forEach((l, i) => {
|
||||
const at = `modules.rust.links[${i}]`
|
||||
if (!l || typeof l !== 'object') return problems.push(`${at} must be an object`)
|
||||
if (!accounts[l.username]) problems.push(`${at}.username must be one of the demo accounts`)
|
||||
if (!STEAM_ID.test(String(l.steamId || ''))) problems.push(`${at}.steamId must be a 17-digit Steam id`)
|
||||
if (l.serverId !== undefined && !SERVER_ID.test(String(l.serverId))) problems.push(`${at}.serverId must be a server id`)
|
||||
return undefined
|
||||
})
|
||||
|
||||
return problems
|
||||
}
|
||||
|
||||
/** The hook core calls: `onDemoSeed(data, { accounts })`. */
|
||||
async function onDemoSeed(data, { accounts = {} } = {}) {
|
||||
const problems = checkSection(data, accounts)
|
||||
if (problems.length) throw new Error(`the Rust demo seed has problems: ${problems.join('; ')}`)
|
||||
|
||||
for (const s of data.servers || []) {
|
||||
await serversDb.upsertServer({
|
||||
id: s.id,
|
||||
name: s.name.trim(),
|
||||
sidecarBaseUrl: s.sidecarBaseUrl,
|
||||
sidecarTokenEnc: servers.encryptToken(s.sidecarToken),
|
||||
protocol: s.protocol === undefined ? sidecar.PROTOCOL_VERSION : Number(s.protocol),
|
||||
enabled: s.enabled === undefined ? true : Boolean(s.enabled),
|
||||
sortOrder: s.sortOrder === undefined ? 0 : Number(s.sortOrder),
|
||||
})
|
||||
log.info('demo server ready', { server: s.id })
|
||||
}
|
||||
|
||||
// A link is a plain insert by design (D23), so the upsert is spelled out here:
|
||||
// the right owner already holds it, or it is moved to them. Only the seed may
|
||||
// move a link, and only to a demo account.
|
||||
for (const l of data.links || []) {
|
||||
const userId = accounts[l.username]
|
||||
const steamId = String(l.steamId)
|
||||
const existing = await linksDb.getBySteamId(steamId)
|
||||
if (existing && Number(existing.userId) === Number(userId)) continue
|
||||
if (existing) await linksDb.removeBySteamId(steamId)
|
||||
await linksDb.insert({ steamId, userId, name: l.name || null, serverId: l.serverId || null })
|
||||
log.info('demo link ready', { username: l.username })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { checkSection, onDemoSeed }
|
||||
Reference in New Issue
Block a user