docs(website): demo mode core built — DM15-DM17, MODULE_API 1.18.0
- DEMO_MODE.md: DM15 (onDemoSeed), DM16 (POST /auth/register locked), DM17 (sessions route locked outright on a demo account). It also covers the settings keys that reach a Tier A lock, how demo accounts are recognised, failing closed, the published logins, the toast, the seed's fixture shape and $env, and the fact that there is no seeded team (Teams come from the rig's clans). The §3.2 example drops the config-file POST that DM14 opened. Step 1 is marked built. - MODULE_API.md: 1.18.0. demoLocks in module.json, ctx.demoMode, and api.onDemoSeed, which must upsert because it runs on every seed. - BACKEND_DESIGN.md: §5.1 demo mode, and demo_state in the settings table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -338,7 +338,9 @@ Seeded keys: `site_mode` (default `maintenance`), `site_mode_changed_at`,
|
||||
`site_mode_changed_by`, `maintenance_message`, `status_message`, `homepage_teaser`,
|
||||
`contact_email` (seeded from `BRAND_CONTACT_EMAIL`; e.g. `UOMysticmoon@gmail.com` on the first
|
||||
instance), `site_title`, `player_registration`
|
||||
(default `disabled`), `mobile_app_links_enabled`, `module_source_hosts`.
|
||||
(default `disabled`), `mobile_app_links_enabled`, `module_source_hosts`. On the public demo only,
|
||||
`demo_state` (JSON `{ resetsAt, logins }`, written by `seed:demo`, never writable through
|
||||
`PUT /admin/settings`; §5.1).
|
||||
|
||||
`module_source_hosts` is the allowlist of hostnames a module may be installed from
|
||||
(MODULE_SYSTEM.md §2.7.2 decision 6), edited in Admin → Modules and audited as
|
||||
@@ -1808,6 +1810,34 @@ Admin "preview live" simply hits the content APIs with the admin cookie, which b
|
||||
Dashboard reads `site_mode` + `site_mode_changed_at`/`_by` for "current mode + last change +
|
||||
who"; `activity_log` provides the history feed.
|
||||
|
||||
### 5.1 Demo mode (`DEMO_MODE=true`)
|
||||
|
||||
The public demo, `demo.runicgateway.com`, runs the normal image with one env var. The design of
|
||||
record is `DEMO_MODE.md`; this is the contract in brief. With the flag unset, `app.js` mounts none of
|
||||
it.
|
||||
|
||||
- **The lock** (`server/src/demo/locks.js`). It is mounted at `/api/v1` ahead of every router, after the
|
||||
body parser and `cookieParser`. Each `POST`/`PUT`/`PATCH`/`DELETE` is checked against core's list
|
||||
(DEMO_MODE.md §3.3) and every installed module's `demoLocks` (`MODULE_API` 1.18.0). A match answers
|
||||
`403 { message: "This is turned off on the demo site.", code: "DEMO_LOCKED" }` before the route's own
|
||||
auth check. Reads are never locked. Matching follows express 4's defaults: case-insensitive, and the
|
||||
trailing slash optional. A few rows are conditional:
|
||||
- the target of `/admin/users/:id…` is a demo account;
|
||||
- the caller of `/auth/me/…` is signed in as one;
|
||||
- the `PUT /admin/settings` body names a protected key (`site_mode*`, `module_source_hosts`,
|
||||
`teams_voice_*`, `demo_*`).
|
||||
|
||||
A condition that cannot decide refuses.
|
||||
- **Uploads.** Any request with a `multipart/*` content type is refused app-wide, before a router or
|
||||
multer sees it.
|
||||
- **Search engines.** Every response carries `X-Robots-Tag: noindex, nofollow`. `/robots.txt` is
|
||||
`Disallow: /`, served ahead of the SPA's static file.
|
||||
- **`GET /public/settings`** gains `demo: { resetsAt, logins }`, from the `demo_state` settings row.
|
||||
- **`npm run seed:demo -- <fixture>`** (`server/db/seedDemo.js`) refuses to run without the flag. It
|
||||
upserts the four accounts (`demo-admin`, `demo-moderator`, `demo-player`, `play-review`, fixed in
|
||||
`server/src/demo/mode.js`), sets the site live with registration disabled, and seeds content on a
|
||||
database's first run only. It calls each module's `onDemoSeed`, then stamps `demo_state`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Auth & security
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Demo mode — `demo.runicgateway.com`
|
||||
|
||||
**Status:** design, **fully decided** 2026-10-23 (DM1-DM14, §9). Ready to build per §10. Nothing is
|
||||
built yet.
|
||||
**Status:** design **fully decided** 2026-10-23 (DM1-DM17, §9). §10 step 1 (core, `MODULE_API`
|
||||
1.18.0) is **built**; steps 2-6 are not.
|
||||
**Issue:** `RunicGateway/website#222`. **Why now:** Google Play (`../android/PLAN.md` item 9,
|
||||
M8) needs a public HTTPS site with working sign-ins for its reviewers and for the 12 testers who
|
||||
must be opted in for 14 days before the app can apply for production.
|
||||
@@ -67,16 +67,19 @@ routes in `module.json` (DM1, §3.2).
|
||||
```json
|
||||
"demoLocks": [
|
||||
{ "method": "PUT", "path": "/admin/rust/servers/:id" },
|
||||
{ "method": "DELETE", "path": "/admin/rust/servers/:id" },
|
||||
{ "method": "POST", "path": "/admin/rust/config/:serverId/file" }
|
||||
{ "method": "DELETE", "path": "/admin/rust/servers/:id" }
|
||||
]
|
||||
```
|
||||
|
||||
Paths use the same `:param` syntax as the module's routers and must sit under the module's own
|
||||
mounts, which the loader checks the same way it checks `mounts`. A module without the field is
|
||||
unaffected. It is a new manifest field, so `MODULE_API_VERSION` gets a **minor** bump,
|
||||
`MODULE_API.md` documents it, and the Integration Kit's module chapter gains a paragraph (its
|
||||
`ci/core-ref.json` pin then moves, as for any API bump).
|
||||
Paths use the same `:param` syntax as the module's routers, with an optional trailing `/*` for a
|
||||
path and everything under it. They must sit under the module's own mounts, which the loader checks
|
||||
the same way it checks `mounts`. Only writes can be declared. The config editor's one locked read
|
||||
(§3.5) depends on which file is asked for, so the module enforces it behind `ctx.demoMode` rather
|
||||
than by route. A module without the field is unaffected. The field is validated on every boot, so a
|
||||
wrong declaration fails on a developer's machine and not only on the demo. It is a new manifest field,
|
||||
so `MODULE_API_VERSION` gets a **minor** bump to **1.18.0**, `MODULE_API.md` documents it, and the
|
||||
Integration Kit's module chapter gains a paragraph (its `ci/core-ref.json` pin then moves, as for any
|
||||
API bump).
|
||||
|
||||
### 3.3 What is locked
|
||||
|
||||
@@ -96,8 +99,14 @@ wipe puts them back (§7).
|
||||
| Team voice | writes under `/admin/teams/voice` | Configure or sync Discord voice channels |
|
||||
| Demo accounts, by an admin | `PUT`/`DELETE /admin/users/:id`, `DELETE …/trusted-devices…`, `POST …/mfa/reset` **when `:id` is a demo account** | Demote, disable or delete the account everyone signs in with |
|
||||
| Own account, on a demo account | `PATCH /auth/me/account/username`, `…/password`, `…/email`; `POST /auth/me/account/totp/setup`, `…/enable`, `…/disable`; `POST /auth/me/account/recovery-codes/generate`; `DELETE /auth/me/account/identities/:provider` | Change the shared password, or turn on 2FA so nobody else can sign in |
|
||||
| Other people's sessions | `DELETE /auth/me/sessions/:id` for a session that is not the caller's | Sign every other visitor out of the shared account |
|
||||
| Other people's sessions | `DELETE /auth/me/sessions/:id` **on a demo account**, whichever session it names (DM17) | Sign every other visitor out of the shared account |
|
||||
| Password reset | `POST /auth/password/forgot`, `POST /auth/password/reset/:token` | Reset a demo account's password |
|
||||
| Registration | `POST /auth/register` (DM16) | Settings are open, so a visitor could switch `player_registration` on. The route is what keeps registration closed (§4) |
|
||||
| Settings that reach a lock | `PUT /admin/settings` when the body names `site_mode*`, `module_source_hosts`, `teams_voice_*` or `demo_*` | That route writes arbitrary keys, so these would get around the site-mode, Modules and team-voice rows above, or rewrite the banner and the published logins. The answer names the keys (`keys: [...]`) |
|
||||
|
||||
**Why DM17 locks the whole route.** A mobile access token carries no id for its refresh-token row,
|
||||
so the server cannot tell which row in `GET /auth/me/sessions` is the caller's own. Signing out is
|
||||
`POST /auth/mobile/logout`, which revokes the caller's own row and stays open.
|
||||
|
||||
**Tier A — always locked (Rust module)**
|
||||
|
||||
@@ -107,7 +116,17 @@ wipe puts them back (§7).
|
||||
| The bridge's config, **read or written** | `GET`/`POST /admin/rust/config/:serverId/file`, `GET …/writes…` for the `RunicGateway` plugin's files, which are also left out of `GET …/files` | It holds the bridge's link to the sidecar and its credentials. Read, it would let someone talk to the sidecar directly over HTTP (DM14, §3.5) |
|
||||
|
||||
**Settings stay open (DM2).** Theme, navigation and the other `PUT /admin/settings` values can be
|
||||
changed and come back at the wipe. Brand images are uploads, so they fall under the upload lock.
|
||||
changed and come back at the wipe, except the keys in the "Settings that reach a lock" row above.
|
||||
Brand images are uploads, so they fall under the upload lock.
|
||||
|
||||
**Which accounts are demo accounts.** The four usernames in §4 are fixed in core
|
||||
(`server/src/demo/mode.js`), not read from the seed fixture: the lock checks them on every request,
|
||||
and a fixture that dropped one would unlock the account everybody shares. The lock decides by the
|
||||
account row in the database (`users.getById`), not by the username claim in the token. An account an
|
||||
admin creates on the demo (`POST /admin/users` stays open) is an ordinary account. It is gone at the
|
||||
next wipe.
|
||||
|
||||
**Failing closed.** A conditional row that cannot decide, because the database is down, refuses.
|
||||
|
||||
**Uploads — locked hard (DM4)**
|
||||
|
||||
@@ -211,7 +230,9 @@ All of this is enforced in the module's server code behind `ctx.demoMode` (DM10)
|
||||
| `play-review` | admin | Google's reviewer. Same powers as `demo-admin`, but a separate account, so a reviewer is never signed out because someone else revoked a session on the shared one |
|
||||
|
||||
Usernames and passwords are published on the demo's sign-in page and on runicgateway.com `/beta`.
|
||||
They are not secrets. The seed recreates them at every wipe, so a password that somehow changed is
|
||||
They are not secrets. The seed writes them into `demo_state`, `GET /public/settings` serves them as
|
||||
`demo.logins`, and both sign-in pages (`/account/login`, `/admin/login`) list them with a **Use**
|
||||
button that fills the form. A fixture account with `"published": false` is created but not listed. The seed recreates them at every wipe, so a password that somehow changed is
|
||||
back within one cycle. Public registration is **closed** on the demo: there is no mail to verify an
|
||||
address with (§5), and the static logins cover every role.
|
||||
|
||||
@@ -226,10 +247,16 @@ address with (§5), and the static logins cover every role.
|
||||
|
||||
## 6. The banner
|
||||
|
||||
`GET /api/v1/public/settings` gains `demo: { resetsAt }` while the flag is on. The web client shows a
|
||||
thin strip on every page: "Demo site — anything you change is wiped at 14:00 UTC. Sign-ins:
|
||||
demo-admin / demo-player …". The app shows the same strip on Home. Both read `resetsAt`, so neither
|
||||
needs to know the schedule.
|
||||
`GET /api/v1/public/settings` gains `demo: { resetsAt, logins }` while the flag is on, and only then.
|
||||
The seed writes both into one settings row, `demo_state`, which `PUT /admin/settings` cannot touch
|
||||
(§3.3). The web client shows a thin strip on every page: "Demo site — anything you change is wiped at
|
||||
14:00 UTC. Sign-ins: demo-admin / demo-player … — passwords on the sign-in page". The app shows the same
|
||||
strip on Home. Both read `resetsAt`, so neither needs to know the schedule. The time is said in UTC,
|
||||
not converted, so it matches the schedule published on `/beta`.
|
||||
|
||||
**The toast.** Every web request goes through one request primitive (`client/src/api/client.js`),
|
||||
installed modules' requests included. A `DEMO_LOCKED` answer is reported from there to one toast
|
||||
(`DemoToast`), and the caller still gets its error as usual. So no screen has to learn the code.
|
||||
|
||||
## 7. The wipe
|
||||
|
||||
@@ -284,13 +311,28 @@ org lead created the subuser and added the secret on 2026-10-23.
|
||||
2. `docker compose pull && docker compose up -d` — the app boots on the latest `main` image, creates
|
||||
the schema, and installs the Rust module from `MODULES=rust@<version>=<manifest>`, which the workflow writes (§7.5).
|
||||
3. Wait for the app's healthcheck.
|
||||
4. `docker compose exec app npm run seed:demo` — one core script that, from a fixture file in the
|
||||
deploy directory:
|
||||
- creates the four accounts (§4) and `demo-player`'s stand-in Steam link (DM6);
|
||||
- creates the Rust server row with the rig's sidecar URL and token taken from `.env`, so the
|
||||
link survives every wipe without being stored in the fixture;
|
||||
- writes the seeded site content: news posts, wiki pages, the demo events and a team;
|
||||
- stamps `resetsAt` for the banner.
|
||||
4. `docker compose exec app npm run seed:demo -- <fixture.json>` (or `DEMO_SEED_FILE`) — one core
|
||||
script (`server/db/seedDemo.js`) that, from a fixture file in the deploy directory:
|
||||
- creates the four accounts (§4), or resets them if they exist: password, role, active, 2FA off;
|
||||
- sets the site live with registration disabled, plus any `settings` the fixture adds;
|
||||
- writes the seeded site content: news posts, wiki pages, pages and the demo events. It does
|
||||
this only on the first run against a database, so a re-run never duplicates it;
|
||||
- hands each installed module its `modules.<id>` section through the module's `onDemoSeed` hook
|
||||
(DM15, `MODULE_API` 1.18.0). The Rust module writes its own server row, with the rig's sidecar URL
|
||||
and token, and `demo-player`'s stand-in Steam link (DM6). Core never writes a module's tables;
|
||||
- stamps `demo_state`: `resetsAt`, the next `DEMO_RESET_EVERY_HOURS` slot (default 2) on the hour
|
||||
UTC, and the published logins.
|
||||
|
||||
The fixture is JSON: `{ accounts, settings?, content?: { posts, wiki, pages, events }, modules? }`.
|
||||
Any string `"$env:NAME"` in it is replaced with that variable from the host's `.env`, so the
|
||||
sidecar token is never in the repo. An unset variable fails the run instead of seeding an empty
|
||||
credential. The script checks the fixture before writing anything. It must name exactly the four
|
||||
accounts, each with a role and a password of 8 or more characters. It refuses to run at all without
|
||||
`DEMO_MODE=true`. The demo's `.env` leaves `ADMIN_USERNAME` unset, so the boot seed creates no fifth
|
||||
admin.
|
||||
|
||||
**There is no seeded team.** Core's Teams are synced from the game (a Rust clan becomes a Team), so
|
||||
the demo's team comes from a clan in the rig's baseline (§7.2), not from the fixture.
|
||||
5. The module's sync connects when the rig finishes booting, and its first import adopts the
|
||||
baseline's permissions and NPCs (§7.1).
|
||||
6. Smoke check, after a bounded wait: `GET /api/v1/public/status` answers, and the Rust server
|
||||
@@ -369,6 +411,9 @@ down.
|
||||
| DM12 | (F2) What "latest" means across the Rust link | The latest **Rust bundle** for the rig's plugins and sidecar, the newest module release on the same protocol, and the website's `main` image; the wipe updates the rig when the bundle changes (§7.5) |
|
||||
| DM13 | (F3) DM2 read correctly? | Yes: only the settings stay open; every other Tier A row stays locked |
|
||||
| DM14 | (after DM13) May visitors change plugin configs? | Yes, every plugin except the bridge. The bridge's config can be **neither read nor written**, because it holds credentials that would let someone talk to the sidecar directly (§3.5) |
|
||||
| DM15 | (build) How does the seed create rows in a module's tables (the Rust server link, `demo-player`'s Steam link)? | Through a new optional module hook, **`api.onDemoSeed(data, { accounts })`**, in the same `MODULE_API` minor as `demoLocks`. The module writes its own rows with its own model; core never writes a module's tables (§7.3) |
|
||||
| DM16 | (build) Settings are open, so `player_registration` can be switched on. Lock registration? | Yes: **`POST /auth/register`** is a Tier A lock. Invites stay open (§3.3) |
|
||||
| DM17 | (build) The server cannot tell which mobile session is the caller's own. What does the other-sessions lock do? | **`DELETE /auth/me/sessions/:id` is locked outright on a demo account**; `POST /auth/mobile/logout` stays open (§3.3) |
|
||||
|
||||
DNS and HTTPS for `demo.runicgateway.com` come from an external reverse proxy, which the org lead is
|
||||
setting up. The app sits behind it on plain HTTP; core already trusts one proxy hop for
|
||||
@@ -389,7 +434,12 @@ through that proxy.
|
||||
demo-account and other-session checks), `demoLocks` loading, `ctx.demoMode`, the banner field,
|
||||
`seed:demo`, `noindex`, the web banner and toast, the `MODULE_API` minor bump, and the OpenAPI
|
||||
note that a route may answer `DEMO_LOCKED`. Tests for every lock. One PR, plus `MODULE_API.md`
|
||||
and this doc.
|
||||
and this doc. **Built 2026-10-23** as `MODULE_API` 1.18.0, with `onDemoSeed` (DM15), the
|
||||
registration lock (DM16), the protected settings keys (§3.3) and the published logins on the
|
||||
sign-in pages (§4). The code is in `server/src/demo/` (`mode.js`, `paths.js`, `locks.js`),
|
||||
`server/db/seedDemo.js`, and the client's `DemoBanner`, `DemoToast` and `DemoLogins`.
|
||||
`test/demoLocks.test.js` holds every core row against `routes.manifest.json`, so a row that stops
|
||||
naming a real route fails the build.
|
||||
2. **Module** (`Module-Rust`): `demoLocks` in `module.json`; behind `ctx.demoMode`, the console-command
|
||||
deny list (§3.4) and the config-editor rules (§3.5); and `protocol` in the release's install
|
||||
manifest (§7.5). Each with tests. One PR.
|
||||
|
||||
@@ -26,13 +26,30 @@ here extends the contract first, in this file, before the module is written agai
|
||||
Core exports a single integer-major semver string from `server/src/modules/version.js`:
|
||||
|
||||
```js
|
||||
const MODULE_API_VERSION = '1.17.0'
|
||||
const MODULE_API_VERSION = '1.18.0'
|
||||
```
|
||||
|
||||
The client half carries the same number (`client/src/modules/version.js`) and a test asserts the two
|
||||
agree. Duplicated rather than fetched because the value has to be on `window.__rg` before the first
|
||||
module chunk evaluates, which is earlier than any network round trip could answer.
|
||||
|
||||
**1.18.0 — the public demo** (`website/DEMO_MODE.md` §3.2, §3.4, §7.3; website#222). Three optional
|
||||
additions and no removal, so minor. With `DEMO_MODE` unset, none of them changes anything a module sees,
|
||||
except that `demoLocks` is validated.
|
||||
|
||||
- **`demoLocks` in `module.json`** (§2.1): the module's own write routes that the demo refuses. Core
|
||||
answers a matching request `403 { message, code: 'DEMO_LOCKED' }` before the module's handler runs,
|
||||
exactly as it does for its own locked routes. Writes only, under the module's own mounts.
|
||||
- **`ctx.demoMode`** (§2.3): `true` on the demo. It is for the locks a route list cannot express,
|
||||
which the module enforces in its own handlers: a console command refused by its first word, or a
|
||||
config file refused by the plugin that owns it. A refusal should answer with the same body.
|
||||
- **`api.onDemoSeed(fn)`** (§2.4): the demo's seed hands the module its section of the fixture, so the
|
||||
module writes its own rows (DM15).
|
||||
- A module that **declares `demoLocks`** needs `coreApi` `^1.18.0`, because an older core rejects the
|
||||
unknown key. A module that only reads `ctx.demoMode` or registers `onDemoSeed` should declare it too:
|
||||
an older core has no `onDemoSeed` on `api`, and its `ctx.demoMode` is `undefined`. Module-uo's
|
||||
`^1.10.0` still resolves and uses none of them.
|
||||
|
||||
**1.17.0 — a module may take part in linking a sign-in identity** (`website/STEAM_SIGNIN.md` §4; Rust
|
||||
`modules/rust/PLAN_REDESIGNS.md` §15, D392 and D396). Three optional additions and no removal, so
|
||||
minor. Linking stays core's: a module can read what core verified, refuse a link, and hear one, and
|
||||
@@ -521,6 +538,10 @@ branches lacks — so it is the only place `permits` is true between two values
|
||||
between `admin` and `owner`, `members` or `subscribers`. `permits`, `meet` and `meetAll` are otherwise
|
||||
unchanged, and so is every rule about composition narrowing rather than widening.
|
||||
|
||||
**1.18.0 — the public demo** (`website/DEMO_MODE.md` §3.2, §3.4, §7.3). `demoLocks` in `module.json`,
|
||||
`ctx.demoMode` and `api.onDemoSeed`; three optional additions, so minor; see §1.1. Module-uo's
|
||||
`^1.10.0` still resolves.
|
||||
|
||||
**1.17.0 — identity links** (`website/STEAM_SIGNIN.md` §4; Rust PLAN_REDESIGNS §15, D392, D396).
|
||||
`ctx.identities.list`, and `api.onIdentityLinking` (refuse-only, fails closed) and
|
||||
`api.onIdentityLinked`; three optional additions, so minor; see §1.1. Module-uo's `^1.10.0` still
|
||||
@@ -788,6 +809,7 @@ rejected rather than ignored, so a typo is a loud failure and not a silently-ine
|
||||
| `mounts` | no | Declared prefixes per tier (§2.3). Declaration is the contract; the loader compares it against what the module actually registers and rejects a mismatch. |
|
||||
| `extensions` | no | Core extension slots this module mounts into (§2.4). |
|
||||
| `capabilities` | no | Opaque strings published by `GET /api/v1/public/modules` (§2.9), for clients (the SPA, the Android app) to feature-detect against. Published only while the module is `started`. |
|
||||
| `demoLocks` | no | *(1.18.0)* `[{ method, path }]`: the module's write routes the public demo refuses (`website/DEMO_MODE.md` §3.2). `method` is `POST`, `PUT`, `PATCH` or `DELETE`; `path` is relative to `/api/v1`, in the routers' own `:param` syntax with an optional trailing `/*`, and must sit under one of the module's declared `mounts`. Validated on every boot; read only with `DEMO_MODE=true`, where core answers a match `403 DEMO_LOCKED` before the module's handler runs. A module declaring it needs `coreApi` `^1.18.0`, since an older core rejects the unknown key. |
|
||||
|
||||
### 2.2 The entry point
|
||||
|
||||
@@ -838,6 +860,7 @@ module-uo does not need is on the list.
|
||||
| `ctx.events.expired` | `({ kind, ref }) => void`, fire-and-forget, owner bound | `events/cleanup` `expireResource` | `module-rust`'s ingest of `world.expired` (1.11.0) |
|
||||
| `ctx.inbox.push` | `(userId, item) => void`, fire-and-forget | the in-app channel (live since Phase 7) | `module-uo` reaches both through `server/core.js` (1.7.0) |
|
||||
| `ctx.identities.list` | `(userId, provider) => Promise<[{ provider, subject, displayName, avatarUrl, linkedAt }]>`, read only | `model/userIdentities` | Steam sign-in (1.17.0) |
|
||||
| `ctx.demoMode` | boolean, read once when `ctx` is built | `DEMO_MODE=true` (`demo/mode.js`) | Module-Rust's console-command deny list and config-editor rules (1.18.0) |
|
||||
|
||||
**`ctx.events.emit(triggerId, envelope)`** fires an event the module DECLARED with
|
||||
`api.registerEventTriggers` (§2.4). It is the push half of the engagement seam
|
||||
@@ -967,6 +990,7 @@ api.onBoot(async (ctx) => {})
|
||||
api.onShutdown(async () => {})
|
||||
api.onIdentityLinking(async ({ userId, provider, subject }) => ({ ok: true })) // 1.17.0
|
||||
api.onIdentityLinked(async ({ userId, provider, subject, displayName }) => {}) // 1.17.0
|
||||
api.onDemoSeed(async (data, { accounts }) => {}) // 1.18.0
|
||||
```
|
||||
|
||||
**Every call STAGES; nothing is committed until the module as a whole is known good.** A claim's
|
||||
@@ -1460,6 +1484,25 @@ api.onIdentityLinked(async ({ userId, provider, subject, displayName }) => { /*
|
||||
identity already linked to another account is refused by core before any module is asked). There is
|
||||
no unlink hook (D393).
|
||||
|
||||
**`onDemoSeed(fn)`** (1.18.0) — the module's part of the public demo's seed (`DEMO_MODE.md` §7.3,
|
||||
DM15). `npm run seed:demo` loads the installed modules the way the server does (but never calls
|
||||
`onBoot`) and calls `fn(data, { accounts })`, where `data` is the fixture's `modules.<id>` object with
|
||||
every `"$env:NAME"` already replaced, and `accounts` maps each demo username to its user id. The module
|
||||
writes its own rows through its own model; core never writes a module's tables.
|
||||
|
||||
```js
|
||||
api.onDemoSeed(async ({ servers = [], links = [] }, { accounts }) => {
|
||||
for (const s of servers) await serversModel.upsert({ ...s, tokenEnc: ctx.secretBox.encrypt(s.sidecarToken) })
|
||||
for (const l of links) await linksModel.upsert({ userId: accounts[l.username], steamId: l.steamId })
|
||||
})
|
||||
```
|
||||
|
||||
- **Upsert, never insert.** The seed may run more than once on one database. Core seeds content only
|
||||
on the first run, but it calls every module hook on every run.
|
||||
- Called only with `DEMO_MODE=true`, only for a module whose fixture section exists, and only in the
|
||||
seed's own process. A fixture section for a module that registered no hook fails the seed. So does a
|
||||
hook that throws.
|
||||
|
||||
### 2.5 Lifecycle
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user