Compare commits

...

6 Commits

Author SHA1 Message Date
c24443f721 Merge pull request 'docs(kit): re-pin to core 1.21.0 and teach report targets' (#19) from docs/core-1.21.0 into main
Reviewed-on: #19
2026-10-30 14:19:42 +00:00
3f5cd72ed6 docs(kit): re-pin to core 1.21.0 and teach report targets
All checks were successful
PR Checks / prose (pull_request) Successful in 11s
PR Checks / template (pull_request) Successful in 43s
ci/core-ref.json moves from 7bd93b8 (1.20.0) to f13b8fe (1.21.0,
website#233, report, block and hide). Chapter 2 gains a section on
registerReportTargets, registerGameIdentities and ctx.safety, and its call
count becomes twenty-three. template/module.json's coreApi becomes ^1.21.0
for the equality check; the template declares none of the new members.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-30 09:04:26 -05:00
be767776ac Merge pull request 'docs(kit): pin core 1.20.0; teach onUserDeleting' (#18) from docs/core-1.20.0 into main
Reviewed-on: #18
2026-10-30 04:47:17 +00:00
36f29a9cac docs(kit): pin core 1.20.0; teach onUserDeleting
Some checks failed
PR Checks / template (pull_request) Successful in 54s
PR Checks / prose (pull_request) Failing after -24s
Moves ci/core-ref.json from 69998ec (1.19.0) to 7bd93b8 (1.20.0, website#232,
account deletion). Chapter 2 gains "When a member deletes their account": the
hook runs before the row goes, a cascade is not an unlink, it cannot stop the
deletion, so a module that talks to a game needs a record and a retry. Its call
count becomes twenty-one. The demo section now says core refuses multipart bodies
and data: URIs (website#229) on module routes without them being listed. The
template's coreApi becomes ^1.20.0 for the equality check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-29 22:59:09 -05:00
045679bbbc Merge pull request 'docs(kit): pin core 1.19.0; teach the public demo's module members' (#17) from docs/core-1.19.0 into main
Reviewed-on: #17
2026-10-26 11:02:54 +00:00
00f5234f97 docs(kit): pin core 1.19.0; teach the public demo's module members
All checks were successful
PR Checks / prose (pull_request) Successful in 10s
PR Checks / template (pull_request) Successful in 44s
The pin moves from 6890a74 (1.17.0) to 69998ec (1.19.0, website#224),
past 1.18.0 (website#223). Both are the public demo (DEMO_MODE.md §10
step 3). The chapters were read again for both versions:

- Chapter 2: a new section on a public demo site, covering demoLocks in
  module.json, ctx.demoMode and api.onDemoSeed (1.18.0). The call count
  becomes twenty.
- Chapter 5: a paragraph on an event action's demoRefuses (1.19.0), and
  a line in the action listing.

The one other core change in the window (website#221) is a wording fix
on core's Teams panel and nothing a module sees. The template declares
none of the new members, so template/module.json's coreApi becomes
^1.19.0 for the equality check only.

Checks run at the new pin:
- checkCoreApi, links, rename sites and chapter paths all pass, and so
  do the script tests (10 and 11).
- The template's imports, build, externals and swagger checks pass.
- Tests pass: server 82, client 20.
- The template was loaded as `examplegame` into a real core at this ref
  through core's own loader, with DEMO_MODE unset and with it true, and
  it registered both times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-26 05:58:34 -05:00
4 changed files with 187 additions and 4 deletions

View File

@@ -147,8 +147,9 @@ statement of your dependencies, and it makes a test double for it — see
## What you register
Nineteen calls, all synchronous and all listed in [§2.4][api]: fifteen
registrations, the two lifecycle hooks, and the two identity-link hooks. Five of the
Twenty-three calls, all synchronous and all listed in [§2.4][api]: seventeen
registrations, the two lifecycle hooks, the two identity-link hooks, the demo's
seed hook, and the account-deletion hook. Five of the
registrations are the event system's, and [chapter 5](05-events.md) has those. What
is worth knowing is not their signatures but the model behind them.
@@ -537,6 +538,133 @@ api.onIdentityLinked(async ({ userId, provider, subject, displayName }) => {})
module made from it is yours to keep or remove.
- Neither hook can mint a session or change who is signed in.
### When a member deletes their account
A member can delete their own site account, and staff can delete one from the
admin panel (`MODULE_API_VERSION` 1.20.0). Core removes the row, and every table
with a foreign key to it follows: `ON DELETE CASCADE` for what was theirs alone,
`SET NULL` for what they made that others still read. Your module hears about it
once, first:
```js
api.onUserDeleting(async ({ userId, deletedBy }) => {}) // deletedBy: 'self' | 'staff'
```
- **It runs before the row goes**, so you can still read what the person holds.
- **A cascade is not an unlink.** If your game holds anything for the person, such
as a link it reports back on every login or a grant it enforces, undo it here
through your module's own unlink, the same call a player's own unlink makes. That
way the game sees exactly what it would see if they had unlinked by hand. Clean up
any data you keep without a foreign key here too.
- **It cannot stop the deletion.** A throw is logged, and so is no answer within
10 s, and the account is deleted anyway, because a person who asks to be deleted
must be. So a module that talks to a game needs **a record of what is left and a
retry**: write down what you are about to undo *before* the first call, keep what
the game could not take (it was down, or it refused), and try again when the game
comes back. That record cannot have a foreign key to `users`, because it outlives
the user it names.
- **Your hook keeps running past the 10 s.** Core stops waiting, not your code, so
the user may already be gone when your last call finishes. Anything you write
afterwards must not need the row: an activity entry, for instance, names the id in
its `detail`, not its `userId`.
- **Every module that holds per-person game state should register it.** Declaring it
needs `coreApi` `^1.20.0`, because an older core has no `api.onUserDeleting`, and
calling a missing function throws during `register()`.
### When members report, hide or block what your game shows
If your module shows what players wrote, such as chat from the game, members can
report it, hide it for themselves, and block whoever wrote it, and staff can hide
it for everyone (`MODULE_API_VERSION` 1.21.0, [`USER_SAFETY.md`][safety]). Core
keeps the Reports queue and each member's blocks and hides. You keep the content,
and you tell core three things about it:
```js
api.registerReportTargets([{ id: 'mygame.chat', label: 'Chat line', snapshot, hide, unhide }])
api.registerGameIdentities([{ kind: 'steam', label: 'Steam account', ownersOf }])
// and, from your own per-reader routes: ctx.safety.forViewer(userId), ctx.safety.staffAmong(userIds)
```
- **`snapshot({ targetId, viewer })` answers for that reader, and `null` covers two
cases.** An item that is gone and an item they could not have seen get the same
`null`, so core gives them the same 404. Otherwise a report or a block would tell
anybody who said line 4410 on a server whose player list is hidden from them. Run
the same visibility check your page runs.
- **Name the author by their site account when they have one.** `author.userId` is
the linked account and `author.identity` is the game id. `ownersOf(ids)` answers
which ids are linked. A block then lands on the account, and only an unlinked id
is blocked as itself. Core rewrites that block as an account block once the id
links, so you never have to.
- **Filter on the server, in your query.** A route that shows the content to
signed-in readers calls `ctx.safety.forViewer(userId)` and leaves out what that
reader blocked or hid. Do it in the `WHERE` clause, so a page of 100 is still 100
long, and keep the route per-viewer (`Cache-Control: private`), or a shared cache
hands one reader's filter to the next. If your page offers *Block*, mark each item
with whether it can be blocked. Use `ctx.safety.staffAmong` for that, because
staff cannot be blocked.
- **Hide for everyone is a flag you keep.** `hide` is called from core's queue.
Leave the item out for everyone but staff, and show it to staff marked, with an
undo where it lives. Core never calls `unhide` itself.
- **Each call has 5 s.** A module that does not answer is a 503 to the member,
never a silent success. Declaring any of it needs `coreApi` `^1.21.0`.
Module-Rust's `server/safety.js` is a complete worked example: chat lines, Steam
ids and the feed's own filter.
### On a public demo site
A site can run as a public demo (`MODULE_API_VERSION` 1.18.0). Anybody signs in with
published logins, nothing destructive is allowed, and the site is wiped and seeded
again every couple of hours. It is the same image with one flag set,
`DEMO_MODE=true`. Core locks its own routes; yours are yours to declare, and three
members are for that. With the flag off none of them changes anything, except that
`demoLocks` is still validated.
```json
"demoLocks": [
{ "method": "PUT", "path": "/admin/mygame/servers/:id" },
{ "method": "DELETE", "path": "/admin/mygame/servers/:id" }
]
```
(The template has no write routes, so this is a shape rather than one of its files.)
- **`demoLocks` in `module.json` lists your write routes that the demo refuses.**
Core answers a match `403 { code: 'DEMO_LOCKED', message }` before your handler
runs, the same body it gives for its own locks, so the site's one toast explains
it. Writes only (`POST`, `PUT`, `PATCH`, `DELETE`), in your routers' own `:param`
syntax with an optional trailing `/*`, and under one of your declared `mounts`.
**It is validated on every boot, demo or not**, so a wrong entry fails on your
machine rather than only on the demo.
- **Two refusals reach your routes without being listed.** On a demo, core refuses
any request with a multipart body, and any write whose JSON body carries a
`data:` URI in any string (a pasted image is an upload by another name), with the
same `403` body. An upload route of yours is locked on a demo whether or not
`demoLocks` names it.
- **`ctx.demoMode` is for a lock a route list cannot express**, one that depends on
what is in the request rather than which URL it went to. The Rust module refuses a
console command by its first word, and a plugin's config file by the plugin that
owns it. Check it in your handler and answer with the same `403` body. It is also
how you hide the rare read that must never be shown on a demo, such as a file
holding credentials; `demoLocks` takes writes only.
- **`api.onDemoSeed(fn)` writes your rows into the demo's fresh database.** The seed
calls `fn(data, { accounts })`: `data` is your section of the fixture
(`modules.<id>`, with every `"$env:NAME"` string already replaced) and `accounts`
maps each demo username to its user id. Core never writes your tables. **Upsert,
never insert**: the seed may run more than once on one database, and core seeds
its own content only the first time but calls your hook on every run. A fixture section for a module with no hook fails the seed, and so does a
hook that throws.
- **Lock at the server, never in the UI.** A request sent with `curl`, or from an
old app build, must be refused exactly like a click. Your pages may explain a lock;
they must not hide or disable a control as the way of locking it.
- **Declaring `demoLocks` needs `coreApi` `^1.18.0`**, because an older core rejects
the unknown key. Using only the other two should declare it as well: an older core
has no `onDemoSeed`, and its `ctx.demoMode` is `undefined`, which reads as "not a
demo".
An event step the demo should refuse when it is saved is
[chapter 5](05-events.md)'s `demoRefuses`.
## The schema fragment
`template/server/db/schema.sql` is your tables. It is replayed **in full, at every
@@ -851,3 +979,4 @@ the next chapter is for.
[api]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_API.md
[acceptance]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/modules/kit-acceptance.md
[safety]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/USER_SAFETY.md

View File

@@ -150,6 +150,7 @@ api.registerEventActions([{
async revert({ runId, resources, idempotencyKey }) {}, // required iff 'ledger'
async reconcile({ runId, resources }) {}, // optional
async progress({ runId, stepId, idempotencyKey, params, resources, viewer }) {}, // optional, 1.12.0
async demoRefuses(params) {}, // optional, 1.19.0
}])
```
@@ -189,6 +190,22 @@ event's public page and in the app.
already cache, not from a fresh round trip to the game per reader.
- **Failure.** A throw or a late answer costs a line, never the page.
**`demoRefuses` keeps a step off a public demo** (MODULE_API 1.19.0). A demo site
([chapter 2](02-website-module.md)) lets anybody author an event, and some steps must
never be saved there: the Rust module refuses a console command that would stop the
server. Core asks `demoRefuses(params)` only with `DEMO_MODE` on, for every step,
when a definition is created, saved or published, with the step's checked params.
- **The answer.** A sentence refuses the whole write with `403 DEMO_LOCKED`, and the
author reads it as written, so phrase it for them: *"quit" is turned off on the
demo site.* `null` lets the step through.
- **It is a pure check.** No database, no game, nothing changed. A throw refuses too,
with core's own sentence, because a lock that cannot decide fails closed.
- **Check again in `perform()`**, behind `ctx.demoMode`. A version published before
your list grew is never asked again, so the save cannot be the only lock.
- **It needs `coreApi` `^1.19.0`.** An older core never asks it, and the save goes
through without a word.
**`example` is required on every param, optional ones included.** It is the
authoring form's placeholder. It is one word at declaration time and it is
unreconstructable afterwards by anybody who did not write the action.

View File

@@ -1,10 +1,47 @@
{
"repo": "https://gitea.whitlocktech.com/RunicGateway/website.git",
"branch": "main",
"ref": "6890a74c7c4ad5690509e8c163e477d7f7b399e7",
"ref": "f13b8fe1fec9dc330a985430b8bd4f67c9ec26c5",
"why": [
"The core this kit is written against, pinned to a commit rather than a branch.",
"",
"2026-10-30: moved from 7bd93b8 (1.20.0) to f13b8fe (1.21.0, website#233,",
"report, block and hide, USER_SAFETY.md). Chapter 2 was read again:",
"- 1.21.0: api.registerReportTargets, api.registerGameIdentities and",
" ctx.safety.forViewer / staffAmong (chapter 2 gains a section, and its call",
" count becomes twenty-three).",
"website#233 is the only core change in the window. The template declares",
"none of the new members, so nothing it does changed meaning; module.json's",
"coreApi becomes ^1.21.0 for the equality check. The real core run was",
"repeated at this ref, with the template as the only module in the directory.",
"",
"2026-10-30: moved from 69998ec (1.19.0) to 7bd93b8 (1.20.0, website#232,",
"account deletion, ACCOUNT_DELETION.md). Chapter 2 was read again:",
"- 1.20.0: api.onUserDeleting (chapter 2 gains a section, and its call count",
" becomes twenty-one).",
"The other core changes in the window (website#225-#231) are the public",
"demo's deploy, seed and locks. One reaches a module: website#229 refuses, on",
"a demo, any write whose JSON body carries a data: URI, beside the multipart",
"refusal 1.18.0 already had, and neither needs listing in demoLocks. Chapter",
"2's demo section now says so; it had not taught the multipart one either.",
"The template declares none of the new members, so nothing it does changed",
"meaning; module.json's coreApi becomes ^1.20.0 for the equality check. The",
"real core run was repeated at this ref, with the template as the only",
"module in the directory.",
"",
"2026-10-26: moved from 6890a74 (1.17.0) to 69998ec (1.19.0, website#224),",
"past 1.18.0 (website#223). Both are the public demo (DEMO_MODE.md), and the",
"chapters were read again for both versions:",
"- 1.18.0: demoLocks in module.json, ctx.demoMode and api.onDemoSeed",
" (chapter 2 gains a section, and its call count becomes twenty);",
"- 1.19.0: an event action's demoRefuses (chapter 5, a paragraph and a line",
" in the action listing).",
"The one other core change in the window (website#221) is a wording fix on",
"core's Teams panel and nothing a module sees. The template declares none of",
"the new members, so nothing it does changed meaning; module.json's coreApi",
"becomes ^1.19.0 for the equality check. The real core run was repeated at",
"this ref, with the template as the only module in the directory.",
"",
"2026-10-23: moved from 0a37a44 (1.12.0) to 6890a74 (1.17.0, website#220,",
"the cutover that brought Steam sign-in and the Rust last fixes to `main`),",
"past 1.13.0 to 1.16.0, which reached `main` together on 2026-10-22. The",

View File

@@ -2,7 +2,7 @@
"id": "examplegame",
"name": "Example Game",
"version": "0.1.0",
"coreApi": "^1.17.0",
"coreApi": "^1.21.0",
"server": "server/index.js",
"client": { "entry": "client/dist/entry.js" },
"schema": "server/db/schema.sql",