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:
@@ -1,5 +1,7 @@
|
||||
// Thin fetch wrapper. Always sends cookies (httpOnly JWT) and talks to the
|
||||
// same-origin API (/api/v1) — proxied to the Express server in dev.
|
||||
import { reportIfDemoLocked } from '../lib/demoNotice.js'
|
||||
|
||||
const BASE = '/api/v1'
|
||||
|
||||
// Prefix a non-empty query string with "?" (and nothing when it is empty), so
|
||||
@@ -28,6 +30,9 @@ async function req(path, { method = 'GET', body, headers, raw } = {}) {
|
||||
const text = await res.text()
|
||||
const data = text ? safeParse(text) : null
|
||||
if (!res.ok) {
|
||||
// The public demo's lock (DEMO_MODE.md §3.1): shown once, as a toast, for
|
||||
// every caller. The ApiError below still goes to the form as usual.
|
||||
reportIfDemoLocked(data)
|
||||
const message = (data && data.message) || res.statusText || 'Request failed'
|
||||
throw new ApiError(res.status, message, data)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user