Compare commits

..

4 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
3 changed files with 108 additions and 5 deletions

View File

@@ -147,9 +147,9 @@ statement of your dependencies, and it makes a test double for it — see
## What you register ## What you register
Twenty calls, all synchronous and all listed in [§2.4][api]: fifteen Twenty-three calls, all synchronous and all listed in [§2.4][api]: seventeen
registrations, the two lifecycle hooks, the two identity-link hooks, and the demo's registrations, the two lifecycle hooks, the two identity-link hooks, the demo's
seed hook. Five of the 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 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. is worth knowing is not their signatures but the model behind them.
@@ -538,6 +538,79 @@ api.onIdentityLinked(async ({ userId, provider, subject, displayName }) => {})
module made from it is yours to keep or remove. module made from it is yours to keep or remove.
- Neither hook can mint a session or change who is signed in. - 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 ### On a public demo site
A site can run as a public demo (`MODULE_API_VERSION` 1.18.0). Anybody signs in with A site can run as a public demo (`MODULE_API_VERSION` 1.18.0). Anybody signs in with
@@ -563,6 +636,11 @@ members are for that. With the flag off none of them changes anything, except th
syntax with an optional trailing `/*`, and under one of your declared `mounts`. 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 **It is validated on every boot, demo or not**, so a wrong entry fails on your
machine rather than only on the demo. 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 - **`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 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 console command by its first word, and a plugin's config file by the plugin that
@@ -901,3 +979,4 @@ the next chapter is for.
[api]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_API.md [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 [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

@@ -1,10 +1,34 @@
{ {
"repo": "https://gitea.whitlocktech.com/RunicGateway/website.git", "repo": "https://gitea.whitlocktech.com/RunicGateway/website.git",
"branch": "main", "branch": "main",
"ref": "69998ec54854d46681def92985a2b299ee3582a6", "ref": "f13b8fe1fec9dc330a985430b8bd4f67c9ec26c5",
"why": [ "why": [
"The core this kit is written against, pinned to a commit rather than a branch.", "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),", "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", "past 1.18.0 (website#223). Both are the public demo (DEMO_MODE.md), and the",
"chapters were read again for both versions:", "chapters were read again for both versions:",

View File

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