feat(demo): DEMO_MODE lock, seed:demo and the demo banner (MODULE_API 1.18.0)
Core half of the public demo (docs/website/DEMO_MODE.md §10 step 1, website#222).
Nothing here runs unless DEMO_MODE=true.
- server/src/demo/: the lock at /api/v1, ahead of every router. It covers every
Tier A row of §3.3, the shared-login conditions, POST /auth/register (DM16),
DELETE /auth/me/sessions/:id on a demo account (DM17), and the PUT
/admin/settings keys that reach a Tier A lock. Any multipart request is refused
app-wide. noindex and robots.txt. A condition that cannot decide refuses.
- Loader: demoLocks in module.json (writes only, under the module's own mounts),
ctx.demoMode, and api.onDemoSeed (DM15). MODULE_API 1.18.0 on both halves.
- GET /public/settings carries demo: { resetsAt, logins } on the demo only.
- npm run seed:demo -- <fixture>: upserts the four accounts, sets the site live
with registration disabled, seeds content on the first run only, calls each
module's onDemoSeed, and stamps demo_state. It resolves $env: strings and
refuses to run off the demo.
- Client: the banner, one DEMO_LOCKED toast fed from the request primitive (so
module screens get it too), and the published logins on both sign-in pages.
Dashboard and register say the demo message instead of blaming a role.
- OpenAPI: DemoLocked schema, the demo block, and a note that any write may
answer DEMO_LOCKED on the demo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -44,7 +44,13 @@ const doc = {
|
||||
'- **Native / mobile clients** use bearer access tokens from ' +
|
||||
'`POST /api/v1/auth/mobile/login`, refreshed via `/auth/mobile/refresh`.\n\n' +
|
||||
'Endpoints under `/api/v1/admin/**` require a valid session; some are further ' +
|
||||
'restricted to the `admin` role (editors are limited to content).',
|
||||
'restricted to the `admin` role (editors are limited to content).\n\n' +
|
||||
'### The public demo\n' +
|
||||
'A deployment run with `DEMO_MODE=true` (the public demo, docs/website/DEMO_MODE.md) ' +
|
||||
'refuses a fixed set of write routes, and any `multipart/*` request, before the ' +
|
||||
'route runs. **Any write route may then answer** `403` with the `DemoLocked` body ' +
|
||||
'(`code: "DEMO_LOCKED"`); clients recognise the refusal by `code`, not by status. ' +
|
||||
'Reads are never refused this way. With the flag unset no route answers it.',
|
||||
},
|
||||
servers: [
|
||||
{ url: '/', description: 'Same-origin (current host)' },
|
||||
@@ -100,6 +106,17 @@ const doc = {
|
||||
type: 'object',
|
||||
properties: { message: { type: 'string', example: 'Not found' } },
|
||||
},
|
||||
DemoLocked: {
|
||||
type: 'object',
|
||||
description:
|
||||
'The public demo refused a write (DEMO_MODE.md §3). Only on a deployment with `DEMO_MODE=true`. `keys` is present on `PUT /admin/settings` and names the settings the demo keeps.',
|
||||
properties: {
|
||||
message: { type: 'string', example: 'This is turned off on the demo site.' },
|
||||
code: { type: 'string', enum: ['DEMO_LOCKED'] },
|
||||
keys: { type: 'array', items: { type: 'string' }, example: ['site_mode'] },
|
||||
},
|
||||
required: ['message', 'code'],
|
||||
},
|
||||
ValidationError: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -1080,6 +1097,25 @@ const doc = {
|
||||
ntfyUrl: { type: 'string', nullable: true, example: 'https://ntfy.example.com' },
|
||||
},
|
||||
},
|
||||
demo: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Present only on the public demo (`DEMO_MODE=true`, DEMO_MODE.md §4, §6). `resetsAt` is when the next wipe is due, for the banner; `logins` are the published static sign-ins — public by design, not secrets. Both are written by `npm run seed:demo` at every wipe.',
|
||||
properties: {
|
||||
resetsAt: { type: 'string', format: 'date-time', nullable: true, example: '2026-10-24T14:00:00.000Z' },
|
||||
logins: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
username: { type: 'string', example: 'demo-admin' },
|
||||
password: { type: 'string', example: 'demo-admin' },
|
||||
role: { type: 'string', nullable: true, example: 'admin' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: true,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user