@crowdedkingdoms/crowdyjs 12.2.0 → 13.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,112 +1,34 @@
1
1
  # CrowdyJS
2
2
 
3
- The official browser-first TypeScript SDK for **Crowded Kingdoms**. CrowdyJS gives you typed clients for auth, the world/replication GraphQL API, and the UDP proxy subscription stream. As of **v7** it follows the Overworld two-token model: an identity **session token** for the Management API, and short-lived **app-scoped tokens** for gameplay via `client.portal` (see [Overworld portals & app-scoped tokens (v7)](#overworld-portals--app-scoped-tokens-v7)).
3
+ The official browser-first TypeScript SDK for **Crowded Kingdoms**. CrowdyJS
4
+ gives you typed clients for the whole platform: passwordless identity and
5
+ studio administration (the Management surface), world data and the abstract
6
+ game model (the Game surface), and the UDP proxy realtime stream — all over
7
+ one unified GraphQL API.
4
8
 
5
- ## Install
6
-
7
- ```bash
8
- npm install @crowdedkingdoms/crowdyjs
9
- ```
10
-
11
- > **Renamed package, same version line.** This SDK moved to the `@crowdedkingdoms` npm org but **kept its v6 version line**. `@crowdedkingdoms/crowdyjs@6.1.1` continues directly from the former `@crowdedkingdomstudios/crowdyjs@6.1.0` — same code, new package name. (Two interim `1.0.x` publishes during the org move reset the version by mistake; they remain on npm but are superseded by `6.1.1`, which is `latest`.) See [MIGRATION.md](MIGRATION.md).
12
-
13
- CrowdyJS v4 targets browsers by default and uses native `fetch`, `WebSocket`, `crypto`, `btoa`, and `atob`. Node tools can still use the SDK, but must provide browser-compatible globals when opening realtime connections.
14
-
15
- > **CrowdyJS v12 (breaking): Agentic Crowdy Studio.** v12 adds the accepted
16
- > `crowdy.studio-agent/1`, `crowdy.agent-tools/1`, and
17
- > `crowdy.player-host/1` browser contracts. Import the provider-neutral durable
18
- > client and immutable tool registry from
19
- > `@crowdedkingdoms/crowdyjs/agent`, and generic game observations/control from
20
- > `@crowdedkingdoms/crowdyjs/player-host`. Crowdy Studio can mount the
21
- > integrated Ask/Build/Play dock when given an injectable agent transport.
22
- > There is deliberately no OpenRouter key/client, raw GraphQL executor, DOM
23
- > driver, `fetch`, shell, unrestricted SDK bridge, or client-mod `host_call` in
24
- > these surfaces. The reconciled Game API SDL, generated operations, and
25
- > production `CrowdyAgentGraphQLTransport` are included and exposed as
26
- > `client.crowdyStudioAgent`; tests may still inject
27
- > `CrowdyStudioAgentTransportV1`. Existing manual Studio mounting remains
28
- > unchanged when `agent` is omitted.
29
- >
30
- > **Server compatibility (v8.13):** v8.13 adds the **flow-correlation surface**: `gameModel.flow({ appId, flowId })` stitches one flow correlation id into a single cross-engine timeline (model events + automation runs + compute module runs, each time-ascending), and the default event/run fragments now select the nullable `flowId` field on `GmEvent` / `GmAutomationRun` / `WasmModuleRun`. Both require the 2026-07-19 `cks-game-api` dev line (the `2026-07-19-compute-fleet-hardening` migration for the columns and the `gameModelFlow` query for the timeline); older servers reject the operations with a GraphQL validation error, and everything else keeps working. `gameModel.flow` requires the app-admin `manage_apps` permission (it is a diagnostics surface). v8.13 also extends the kit invoke helpers' gameplay-verdict mapping: the typed invoke-contract violation `computeInvoke` raises (`BAD_REQUEST` with an "Invoke params violate ..." message, game-api 2026-07-19+) now maps onto `{ success: false, errorMessage }` (`kitInvoke`) / `{ success: false, reason }` (engine invokes) instead of throwing — see the exported `isKitVerdictError` predicate.
31
- >
32
- > **Server compatibility:** v8.12's **container query predicates** (`where`/`limit`/`offset` on `gameModel.containers`), **automation compute actions** (`actionKind: 'compute_invoke'` with `computeModuleName`/`computeExport` on `gameModel.upsertAutomation`; `property_changed` events additionally carry `oldValue`/`newValue` to compute modules), **container-change push** (`gameModel.containerChanged` — metadata-only pull-on-push over graphql-transport-ws; on Node ≤ 21 pass `webSocketImpl`), and **invoke-trigger contracts** (`contractJson` on `compute.upsertTrigger`/`moduleTriggers`, validated server-side pre-sandbox) require the 2026-07 `cks-game-api` dev line with the `2026-07-19-automation-compute-action` migration; older servers reject the new arguments/fields (omit them and everything else keeps working). (npm 8.11.0 is a partial cut of this surface — prefer 8.12.0.) v8.12 also adds the `runOptimisticAction` kit helper (client-only: the packaged optimistic apply → referee invoke → confirm/rollback loop with actionId receipts). v5.2+ targets environments on release **v0.1.19 or later** (`cks-game-api >= v0.10.3`, `cks-management-api >= v0.1.70`). The destructive mutations send an `idempotencyKey` argument that older servers don't define. v6.1's `client.gameApps.deleteGrid` additionally requires release **v0.1.33+** (`cks-game-api >= v0.12.3`). The game-model **permission effects** fields (`permissionEffects` on `gameModel.upsertFunction`/`seed`, `permissionEffectsAppliedJson` on events) require a `cks-game-api` build with the `2026-07-17-model-permission-effects` migration (v0.13.11+); older servers reject queries/mutations that include them (omit the fields and everything else keeps working). The **permission-read** surface (the `has_grid_permission`/`grid_at`/`has_chunk_permission` expression builtins the kit's `chunkPermission` locks compile to, and selector `*PermissionWhere` predicates) additionally requires `cks-game-api` **v0.13.12+**. Older `cks-game-api` builds report game-model **invoke policy denials** as `FORBIDDEN` GraphQL errors instead of resolving with `success: false`; as of this version the kit's invoke helpers map that error onto the documented `{ success: false, errorMessage }` result, so kit callers behave identically against both server generations. v8.6's **`client.compute`** (Compute Modules — server-side Rust/WASM logic) requires a `cks-game-api` build with the compute surface (the `compute*` root fields, v0.13.13+ dev line); older servers reject these operations with a GraphQL validation error, and everything else keeps working. v8.9's **realtime + live-ops surfaces** (`kit.abilities`/`movement`/`territory`/`racing`/`liveops`/`moderation`/`telemetry`, the loot engine path, `client.compute.deployTemplate` + `kit.deploy({engines})`, and the type-94..98 event parsers) complete the 30-abstraction catalog; v8.8's **session-genre engine surfaces** (`kit.instances`/`director`/`matchmaking`/`minigames`, the engine paths on matches/decks/leaderboards, `economy.orderBook`, and the type-91/92/93 event parsers) talk to the Wave 2 engine templates; capability detection keeps model-only deployments on today's behavior. v8.7's **engine kit surfaces** (`kit.mobs`, `kit.pets`, `kit.combat.attackRouted`, `kit.worldsim.forecast`, and the `kit/wire` pose/lane registry) talk to compute-module game engines built on the Wave 0/1 `cks-game-api` dev line (`crowdy-game-kit` crates); capability detection makes them degrade gracefully — model-only deployments keep today's behavior.
33
- >
34
- > **P1 player runtime compatibility:** `client.playerCompute`,
35
- > `client.playerModel` (owner/grid-confined flexible data + automations), first-class grid
36
- > ownership methods on `client.gameApps`, and app code-admission methods on
37
- > `client.apps` require the 2026-07-20 game-api/management-api player-runtime
38
- > schemas. Player-authorized one-chunk claims and owner release through
39
- > `marketplace.claimGridChunk` / `marketplace.releaseClaimedGrid` require the
40
- > corresponding 2026-07-22 Game API claim schema. Older servers reject only
41
- > those operations during GraphQL validation.
42
- >
43
- > **App-scoped player-count compatibility:** `gameModel.activePlayerCount(appId)`
44
- > and `gameModel.activePlayerCountChanged({ appId }, handlers)` require the
45
- > matching 2026-07-24 Game API schema and an app token scoped to that exact app;
46
- > the subscription also requires `wsUrl`. They count active gameplay sessions,
47
- > not distinct users or actors, and an abandoned session can remain counted for
48
- > approximately 120 seconds. Only `FRESH` snapshots are complete:
49
- > `PARTIAL`/`UNAVAILABLE` are not authoritative zeroes. The subscription is
50
- > best-effort and has no initial event; establish it, query the snapshot,
51
- > deduplicate by `revision`, and requery after reconnects or revision gaps.
52
-
53
- > **v8.21.1 Node realtime routing hotfix:** app-routing responses provide a
54
- > WebSocket base URL. CrowdyJS now normalizes that base to `/graphql` before
55
- > constructing the realtime client; previously Node/bot clients connected to
56
- > the origin root and remained stuck in `connecting` with no notifications.
57
- >
58
- > **Player-code authoring DX:** `playerCompute.setRequires`,
59
- > `marketplace.trustGridAuthor`, self-authored `gridClientMods` fields, and
60
- > version-keyed client artifact fetches require the 2026-07-22 authoring-DX
61
- > migration. The v11 `mountCrowdyStudio` surface is project-first: cloud project
62
- > revisions, target-scoped files, atomic autosave, and explicit draft/live/stop
63
- > orchestration. Its Rust language worker receives source plus the committed
64
- > platform index only—never a credential and never a server connection.
65
- > CrowdyJS 11 requires the matching Crowdy Studio Game API schema; the previous
66
- > package subpath, exports, client property, GraphQL roots, and schema types were
67
- > removed without aliases.
68
-
69
- > **v8.10 inventory authority:** generated craft/barter transactions work on
70
- > existing Model servers. Compute-refereed durable commits require Compute SDK
71
- > `0.1.3` / the `model_invoke` host call.
72
-
73
- ## Standalone builds and schema refresh
74
-
75
- CrowdyJS is a standalone public package: a clean clone builds with
76
- `npm install && npm run build` using the schema artifacts committed to this
77
- repo — no other repositories and no network access required:
78
-
79
- - `schema.gql` — merged Management API + Game API SDL.
80
- - `src/generated/graphql.ts` — generated TypeScript operation types.
81
-
82
- Schema refresh (maintainers) is explicit, from the published SDLs
83
- ([management-api.graphql](https://docs.crowdedkingdoms.com/schema/management-api.graphql),
84
- [game-api.graphql](https://docs.crowdedkingdoms.com/schema/game-api.graphql)):
9
+ Authentication follows a two-token model: an identity **session token** for
10
+ account and admin operations, and short-lived **app-scoped tokens** for
11
+ gameplay, minted per app through `client.portal`.
85
12
 
86
- ```bash
87
- npm run schema:sync:prod
88
- npm run codegen
89
- ```
13
+ Start here:
90
14
 
91
- (`npm run schema:sync:paths -- --management <file-or-url> --game <file-or-url>`
92
- accepts explicit sources.) Commit `schema.gql` and `src/generated/graphql.ts`
93
- together whenever the public GraphQL surface changes; `npm run check:schema`
94
- detects drift in CI/release work.
15
+ - [SDK guide](https://docs.crowdedkingdoms.com/crowdyjs/readme) the canonical
16
+ walkthrough of everything in this README.
17
+ - [Build a game](https://docs.crowdedkingdoms.com/build-a-game/intro) a
18
+ hands-on tutorial from sign-in to a playable voxel game.
19
+ - [Platform overview](https://docs.crowdedkingdoms.com/overview/client-workflow)
20
+ — how a client session flows through the platform.
95
21
 
96
- The browser authoring index follows the same boundary. `npm run build` validates
97
- only the committed internal `src/live-coding/assets/browser-authoring-index.json` and its
98
- generated TypeScript copy; it never discovers or reads a sibling game-api
99
- checkout. Coordinated maintainers compare an explicitly supplied exporter path:
22
+ ## Install
100
23
 
101
24
  ```bash
102
- npm run authoring-index:drift -- --source /path/to/browser-authoring-index.json
103
- # To accept that exact source, then regenerate the committed TypeScript:
104
- npm run authoring-index:drift -- --source /path/to/browser-authoring-index.json --write
105
- npm run authoring-index:generate
25
+ npm install @crowdedkingdoms/crowdyjs
106
26
  ```
107
27
 
108
- The drift command rejects a missing `--source`; cross-repository orchestration,
109
- not standalone package build or tests, owns that comparison.
28
+ CrowdyJS targets browsers by default and uses native `fetch`, `WebSocket`,
29
+ `crypto`, `btoa`, and `atob`. Node tools can still use the SDK, but must
30
+ provide browser-compatible globals when opening realtime connections (e.g.
31
+ pass `webSocketImpl` on Node ≤ 21 for subscriptions).
110
32
 
111
33
  ## Quick start
112
34
 
@@ -120,7 +42,7 @@ const client = createCrowdyClient({
120
42
  // Game API (world data + UDP proxy)
121
43
  httpUrl: 'https://game.example.com',
122
44
  wsUrl: 'wss://game.example.com',
123
- // Management API (passwordless sign-in, profile)
45
+ // Management API (passwordless sign-in, profile, admin)
124
46
  managementUrl: 'https://management.example.com',
125
47
  tokenStore: new BrowserLocalStorageTokenStore(),
126
48
  realtime: {
@@ -145,428 +67,181 @@ const me = await client.users.me();
145
67
  console.log(me.email);
146
68
  ```
147
69
 
148
- **Gameplay needs an app-scoped token, not the session token.** Mint one per app and
149
- drive the Game API world/UDP surface (including `gameClientBootstrap`) from a
150
- per-game client — see [Overworld portals & app-scoped tokens (v7)](#overworld-portals--app-scoped-tokens-v7).
151
-
152
- If `managementUrl` is omitted, the SDK falls back to `httpUrl` for backwards-compat with the single-endpoint deployment.
153
-
154
- ## Sub-clients at a glance
155
-
156
- **Game-client surface** (end-user, browser-safe):
157
-
158
- | Sub-client | What it does |
159
- |---|---|
160
- | `client.auth` | Passwordless sign-in (magic link, social/OIDC, dev bypass), log out, and linked identities (`myIdentities`, `linkIdentity`/`unlinkIdentity`). |
161
- | `client.users` | `me`, `updateGamertag`, profile reads. |
162
- | `client.session` | Token store, `restore()`, `getToken()`, manual `setToken()`. |
163
- | `client.serverStatus` | `gameClientBootstrap(appId)` — per-app version info, UDP status, spatial limits. |
164
- | `client.chunks`, `client.voxels`, `client.actors`, `client.avatars`, `client.state` | World data reads + writes. |
165
- | `client.host` | Game-host election (`get`, `amIHost`) + actor liveness `heartbeat`. `amIHost` is UI convenience only — authoritative host gating uses `gameModelInvoke`'s `is_host` policy. |
166
- | `client.teleport` | Teleport requests. |
167
- | `client.channels`, `client.teams` | Messaging channels and app-scoped player teams (membership + roles). |
168
- | `client.gameModel` | Abstract game model: containers, properties, functions (incl. model-driven `notify_*` effects), sessions, **app-scoped active-session counts** (`activePlayerCount`, `activePlayerCountChanged`), and **automations / NPCs** (`upsertAutomation`, `runAutomation`, `automationRuns`, `automationStats`, …). |
169
- | `client.compute` | **Compute Modules** — server-side Rust/WASM logic: author + deploy source (`upsertModule`, `deployVersion`, `waitForCompile`), triggers + policy, synchronous `invoke`, and monitoring (`moduleRuns`, `moduleStats`, `moduleLogs`, `appDiagnostics`). Modules run server-only; see [Compute Modules docs](https://docs.crowdedkingdoms.com/game-api/compute-modules). |
170
- | `client.playerCompute` | Player-authored SERVER/CLIENT Rust/WASM bound to player-owned grids: deploy source, activate/deactivate, list modules/versions, and delete self-authored modules. |
171
- | `client.crowdyStudio` | Cloud project, personal-library, and common-file APIs for Crowdy Studio: target-scoped files, metadata/module names, pairing preference, optimistic revisions, copy-by-value imports, and atomic metadata/file saves. Generated operations are pinned to the committed merged SDL. |
172
- | `client.crowdyStudioAgent` | Generated, app-token Game API transport for durable agent sessions, Relay history/session pages, descriptors/budgets, exact approvals, browser tool results, heartbeat, control mutations, and ordered event subscriptions. |
173
- | `client.playerModel` | Player-owned flexible model containers and grid-confined automations (`containers`, `createContainer`, `setProperty`, `automations`, `createAutomation`, …). |
174
- | `client.marketplace` | Player-code store/install/consent flows plus player-authorized grid claims: `claimGridOwnership` preserves the existing-grid policy flow, while `claimGridChunk` atomically creates and owns one chunk under `SELF_CLAIM` and `releaseClaimedGrid` releases an eligible owner-created claim. |
175
- | `client.udp` | UDP proxy subscriptions + spatial mutations (`sendActorUpdate`, `sendVoxelUpdate`, `sendAudioPacket`, `sendTextPacket`, `sendClientEvent`). |
176
- | `client.realtime` | Connection status, manual `connect()` / `disconnect()`, `onStatus()` listener. |
177
- | `client.refreshGameplayToken()` | Safely rotates an active game client's app token: disconnects the old-token UDP proxy, refreshes/stores the token, and opens the new-token proxy while existing realtime handlers resubscribe in place. |
178
- | `client.world(appId)` | Higher-level helpers for browser games (`actor.join`, `actor.sendState`, `actor.sendText`). |
70
+ The Management and Game APIs may share one origin or be split; if
71
+ `managementUrl` is omitted, the SDK falls back to `httpUrl`.
179
72
 
180
- `PlayerCodeBroker` transfers
181
- compiled client artifacts to a platform-owned worker, keeps tokens on the page,
182
- allow-lists host calls, and locally clamps chunk-targeted effects to the owned
183
- grid before the normal SDK path reaches server authorization. Crowdy Studio
184
- hot-swaps only the exact version-keyed artifact returned after a successful
185
- CLIENT compile.
73
+ **Gameplay needs an app-scoped token, not the session token.** Mint one per
74
+ app and drive the Game API world/UDP surface from a per-game client — see
75
+ [Authentication](#authentication-session-token-vs-app-scoped-tokens).
186
76
 
187
- ## Crowdy Studio
77
+ ## Authentication: session token vs app-scoped tokens
188
78
 
189
- `mountCrowdyStudio` is the project-first SERVER/CLIENT Rust authoring surface. A
190
- project has one cloud revision, target-scoped files, project metadata, separate
191
- server/client module names, and a pairing preference. Full-stack edits autosave
192
- as one optimistic-concurrency write; the UI renders **Saving**, **Saved**,
193
- **Conflict**, or **Offline**, with retry and conflict-resolution actions.
79
+ Passwordless sign-in returns an **identity session token** that works only
80
+ against the Management API — account, studio admin, and token minting. Each
81
+ game is entered with a short-lived **app-scoped token** confined to that one
82
+ app, so a game stack never receives the player's full session.
194
83
 
195
- The mount fills its host and observes host element resizes, so games can embed
196
- it in draggable docks or split panes without manually relaying window resize
197
- events. Give the host an explicit width and height; Crowdy Studio relayouts
198
- Monaco and collapses secondary panes from its own container width.
84
+ Use two clients: an Overworld/identity client (session token) and a per-game
85
+ client (app token), sharing only the Management URL.
199
86
 
200
87
  ```ts
201
- import { mountCrowdyStudio } from '@crowdedkingdoms/crowdyjs/crowdy-studio';
202
-
203
- const studio = await mountCrowdyStudio(host, {
204
- projectProvider: game.crowdyStudio,
205
- playerCompute: game.playerCompute,
206
- playerWallet: identity.playerWallet,
207
- appId,
208
- gridId,
209
- grid,
210
- workerUrl: playerCodeGlueWorkerUrl,
211
- onHostCall,
88
+ // Overworld/identity client
89
+ const overworld = createCrowdyClient({
90
+ managementUrl,
91
+ tokenStore: new BrowserLocalStorageTokenStore('crowdyjs:session'),
212
92
  });
93
+ // Passwordless sign-in (magic link, social/OIDC, or dev bypass) yields the session token.
94
+ await overworld.auth.requestLoginLink({ email, redirectUri });
95
+ await overworld.auth.completeLoginLink(tokenFromLink);
213
96
 
214
- // Stops editor/runtime polling, the Rust worker, and any client broker owned
215
- // by this mount. It does not implicitly disable a deliberately live server.
216
- studio.destroy();
97
+ // Native / same-origin: mint directly, then build a game client.
98
+ const t = await overworld.portal.mintAppToken(appId);
99
+ const game = createCrowdyClient({
100
+ httpUrl: t.gameApiUrl!,
101
+ wsUrl: t.gameApiWsUrl!,
102
+ managementUrl,
103
+ tokenStore: new BrowserLocalStorageTokenStore('crowdyjs:app:' + appId),
104
+ });
105
+ game.setToken(t.token);
106
+ game.world(appId).subscribe({ actorUpdate: (n) => { /* ... */ } });
217
107
  ```
218
108
 
219
- Use `new CrowdyStudioController(options)` for a custom/headless presentation. Its
220
- public edits are file operations (`addFile`, `renameFile`, `deleteFile`,
221
- `updateFile`) and project settings—there is no session-only source blob, fixed
222
- module name, or template JSON API. The deploy conversion to the Game API's
223
- legacy `sourceFilesJson` input happens only inside the controller's
224
- `playerCompute.deploy` call.
225
-
226
- The built-in UI provides a project switcher/new-project wizard
227
- (server/client/full-stack), target explorer, personal library/common files,
228
- tabs, settings, Problems/Build/Logs/Runs/Invoke panels, and explicit **Test
229
- draft**, **Deploy live**, and **Stop project** actions. Full-stack deployment is
230
- ordered: save once → compile CLIENT → compile SERVER → set/clear the pairing
231
- requirement only after both compiles → enable SERVER → hot-swap the exact
232
- version-keyed CLIENT artifact. Partial compile failures never write a new
233
- requirement. Stop always attempts both server disable and client/poll cleanup
234
- and reports each failure.
235
-
236
- Monaco uses target-prefixed model URIs (`.../server/Cargo.toml` and
237
- `.../client/Cargo.toml`) and opens every loaded project/library/common Rust file
238
- in one bounded local worker workspace. Completion, hover, symbols, and
239
- definition therefore work across loaded files; lifecycle snippets and hover
240
- notes distinguish SERVER platform execution from CLIENT broker host calls.
241
- Local tree-sitter markers are labeled advisory. Platform rustc output is parsed
242
- separately into authoritative path/line/column markers and remains visible in
243
- Build.
244
-
245
- The Rust worker receives source files and the strict embedded platform index
246
- only. It receives no credential, opens no socket/fetch path, and never falls
247
- back to a server language service. If Monaco, Worker, a WASM asset, or a custom
248
- platform index fails, the same Crowdy Studio mount keeps project/target tabs and
249
- uses one file-aware textarea—never a raw JSON blob.
250
-
251
- Modern bundlers must preserve module workers and package `.wasm` assets. Custom
252
- pipelines can pass `languageWorkerFactory` and `editorWorkerFactory`. The
253
- embedded index remains the byte-identical game export (`schemaVersion: 2`, Rust
254
- 1.97.1, SDK 0.1.5, ABI 0, 725 symbols, content hash
255
- `3f5f39d4…18ffb`).
256
-
257
- ### Embed Crowdy Studio in your game (v12.1)
258
-
259
- Most games should not hand-roll the window chrome around `mountCrowdyStudio`.
260
- The embed kit ships the proven Blocks with Friends shell as reusable, game
261
- -agnostic components (exported from `@crowdedkingdoms/crowdyjs/crowdy-studio`):
262
-
263
- - **`createCrowdyStudioEmbed(options)` / `CrowdyStudioEmbed`** — a responsive
264
- panel that renders a resizable right dock on desktop and a focus-trapped
265
- fullscreen modal below 1000 px. It owns Escape/close-key semantics, the
266
- compact header (title, grid pill, on-demand Context drawer, Close), loading
267
- and retry chrome, and assembles the `mountCrowdyStudio` call — including the
268
- agent block when the client exposes `crowdyStudioAgent` and the game passes a
269
- `playerHost`.
270
- - **`CrowdyStudioEmbedDock`** — the accessible game/studio splitter with
271
- persisted width (`ck:crowdy-studio:embed:dock-width:v1`), arrow-key/Home/End
272
- resize, and ARIA value text.
273
- - **`CrowdyStudioTextHud`** — the text-only presentation sink for CLIENT-mod
274
- `hud_set` payloads plus the drawer HUD preview. Untrusted payloads render as
275
- text, never HTML.
276
- - **`ensureCrowdyStudioEmbedStyles()`** — injected `ck-crowdy-studio-embed-*`
277
- styles; games restyle by overriding classes. While docked, the panel sets
278
- `--ck-game-right-inset` on `document.body` so game HUDs can keep clear of
279
- the dock.
109
+ Browser cross-origin handoff is OAuth2 Authorization Code + PKCE — the
110
+ verifier never leaves the game origin:
280
111
 
281
112
  ```ts
282
- import { createCrowdyStudioEmbed } from '@crowdedkingdoms/crowdyjs/crowdy-studio';
283
- // Self-starting glue worker for CLIENT mods (Vite shown; any bundler that
284
- // packages module workers works):
285
- import workerUrl from '@crowdedkingdoms/crowdyjs/player-glue-worker?worker&url';
113
+ // Game origin, on "enter": redirect to the Overworld authorize page.
114
+ location.assign(await game.portal.beginEntry({
115
+ appId, authorizeUrl: 'https://overworld.example.com/authorize',
116
+ redirectUri: location.origin + location.pathname,
117
+ }));
286
118
 
287
- const studio = createCrowdyStudioEmbed({
288
- client: game, // CrowdyClient: crowdyStudio, playerCompute, playerWallet, crowdyStudioAgent
289
- appId,
290
- gameName: 'My Game',
291
- suppressGameplayInput: () => pauseInput(),
292
- onLayoutChange: () => resizeCanvas(),
293
- });
119
+ // Overworld /authorize page (holds the session token):
120
+ location.assign(await overworld.portal.handleAuthorizeRequest());
294
121
 
295
- // Per open (for example after the player claims a grid):
296
- studio.toggle({
297
- gridId,
298
- targetPermissions: {
299
- SERVER: { canWrite: true, canRun: true },
300
- CLIENT: { canWrite: false, canRun: false }, // SERVER-only embeds omit workerUrl
301
- },
302
- });
122
+ // Game origin, on callback boot: exchange code+verifier -> app token (stored).
123
+ const entered = await game.portal.completeEntry();
303
124
  ```
304
125
 
305
- CLIENT-mod embeds additionally pass `grid`, `workerUrl`, `onHostCall`, an
306
- optional `hud` sink, and a `playerHost` adapter. Note that CLIENT mods outside
307
- Blocks with Friends remain gated by program decision D13; new games should
308
- start SERVER-only exactly as above.
126
+ Game-to-game routes through the Overworld for a fresh per-game token.
309
127
 
310
- The visible agent safety chrome is exported from
311
- `@crowdedkingdoms/crowdyjs/player-host`: **`PlayerControlGate`** (capture-phase
312
- human preemption seam with offline Stop) and **`AgentControlBanner`** (the
313
- always-visible-on-control Pause/Stop region, self-injected
314
- `ck-agent-control-*` styles).
128
+ Notes:
315
129
 
316
- ### Game API project contract
130
+ - `portal.mintAppToken(appId)` returns the token plus `gameApiUrl` /
131
+ `gameApiWsUrl` / `expiresAt`; it does **not** store the token on the identity
132
+ client — set it on the per-game client.
133
+ - Each client's `AuthState` is observed by both its HTTP client and its
134
+ realtime socket, so HTTP and WebSocket auth never drift within a client.
135
+ - `client.session.restore()` reads from the configured `tokenStore`.
136
+ `BrowserLocalStorageTokenStore` is provided; bring your own `TokenStore` for
137
+ SSR or Node. PKCE state uses `BrowserSessionPkceStore` by default.
138
+ - Use `client.auth.setToken(token)` to seed a token externally (e.g. when
139
+ restoring auth from a non-default storage).
317
140
 
318
- `CrowdyStudioAPI` implements the transport-neutral
319
- `CrowdyStudioProjectProvider`; generated GraphQL types remain inside the adapter.
320
- The committed merged schema and generated operations cover:
141
+ Deeper reading: [Portals & app-scoped tokens](https://docs.crowdedkingdoms.com/management-api/portals-and-app-tokens)
142
+ and [Sign in (passwordless)](https://docs.crowdedkingdoms.com/management-api/authentication).
321
143
 
322
- - `crowdyStudioProjects(appId)` / `crowdyStudioProject(appId, projectId)`
323
- - `crowdyStudioProjectCreate(input)`
324
- - `crowdyStudioProjectSave(input)` for one atomic metadata/file delta
325
- - `crowdyStudioLibraryFiles(appId)` / `crowdyStudioLibrarySave(input)`
326
- - `crowdyStudioCommonFiles(appId)`
327
- - `crowdyStudioProjectImportFile(input)` for copy-by-value imports
144
+ ### Token refresh during gameplay
328
145
 
329
- The adapter maps the API's `PAIRED | INDEPENDENT | SERVER_ONLY | CLIENT_ONLY`
330
- presentation enum to Crowdy Studio's runtime-oriented project kind and
331
- `REQUIRED | OPTIONAL | NONE` setting. A stale expected `revision` arrives as
332
- `CONFLICT` with `CROWDY_STUDIO_REVISION_CONFLICT` in the message and becomes a
333
- `CrowdyStudioRevisionConflictError` with the latest cloud project when that
334
- follow-up read succeeds.
146
+ Before an app token expires, call `game.refreshGameplayToken()` while gameplay
147
+ is active. It closes the old-token UDP proxy, refreshes and stores the token,
148
+ and opens the new-token proxy while existing realtime handlers resubscribe in
149
+ place. It deliberately stops on the first failed stage: if the old proxy cannot
150
+ confirm disconnect, no refresh is attempted; if refresh fails, the old token is
151
+ retained; if the new proxy connect fails, the fresh token remains stored —
152
+ surface the error and retry `game.udp.connect()` instead of rotating again.
335
153
 
336
- ### Agent SDK, transport, and integrated dock (v12)
154
+ `game.portal.refresh()` remains available for clients with no active UDP
155
+ lifecycle to preserve.
337
156
 
338
- The v12 agent API remains provider-neutral, while CrowdyJS now ships generated
339
- operations plus the production `CrowdyAgentGraphQLTransport` exposed as
340
- `client.crowdyStudioAgent`:
341
-
342
- ```ts
343
- import {
344
- CROWDY_AGENT_TOOL_REGISTRY_V1,
345
- CrowdyStudioAgentController,
346
- } from '@crowdedkingdoms/crowdyjs/agent';
347
-
348
- const agent = new CrowdyStudioAgentController({
349
- transport: game.crowdyStudioAgent,
350
- createSession: {
351
- appId,
352
- projectId,
353
- gridId,
354
- mode: 'BUILD',
355
- providerDataConsent: true,
356
- idempotencyKey: crypto.randomUUID(),
357
- },
358
- beforeAgentWork: () => studio.controller.prepareForAgentWork(),
359
- });
360
- await agent.initialize(); // attach epoch → durable replay/gap fill → live tail
361
- ```
362
-
363
- `CROWDY_AGENT_TOOL_REGISTRY_V1` is an immutable, digest-pinned registry of the
364
- minimum Studio/project/workspace/library/template/diagnostics/runtime and
365
- generic game tools. Every descriptor has exact logical and provider wire names,
366
- semantic version, bounded input/output JSON schemas,
367
- `additionalProperties:false`, executor, mode, risk/effect/reversibility,
368
- scope, approval, idempotency, timeout, and redaction metadata. Input schemas
369
- reject caller-supplied identity, epoch, lease, approval, endpoint, token, and
370
- other authority fields. `CrowdyAgentToolRegistry.fromWireName` is exact and
371
- case-sensitive; browser dispatch is execute-once by `toolCallId`, and ambiguous
372
- effects become `OUTCOME_UNKNOWN` instead of being repeated.
373
-
374
- `CrowdyStudioAgentTransportV1` owns no credentials and exposes only typed
375
- durable orchestration operations: session/history/descriptor/budget queries;
376
- create/attach/mode/event-ack/message/approval/tool-result/lease/pause/resume/
377
- cancel/close mutations; and the ordered event subscription. A generated adapter
378
- maps these methods to the operation names in
379
- `CROWDY_AGENT_GRAPHQL_OPERATIONS_V1`:
380
-
381
- - `crowdyStudioAgentSession`, `crowdyStudioAgentSessions`,
382
- `crowdyStudioAgentHistory`, `crowdyStudioAgentToolDescriptors`, and
383
- `crowdyStudioAgentBudget`;
384
- - `crowdyStudioAgentCreateSession`, `AttachClient`, `SetMode`,
385
- `AcknowledgeEvents`, `Heartbeat`, `SendMessage`, `ApproveTool`, `RejectTool`,
386
- `ToolResult`, `GrantLease`, `RevokeLease`, `Pause`, `Resume`, `CancelRun`,
387
- and `CloseSession` (all with the `crowdyStudioAgent` prefix);
388
- - `crowdyStudioAgentEvents(sessionId, afterSeq, clientEpoch)`.
389
-
390
- The controller applies only contiguous decimal-string sequences, deduplicates
391
- event IDs, fills gaps from durable history, acknowledges the highest
392
- contiguous cursor, fences old attach epochs, and explicitly reconnects without
393
- resuming a run or Play lease. Approval methods use the exact displayed argument
394
- hash. Human edit/game input calls local preemption before best-effort transport
395
- cleanup. Attach uses a stable `clientInstanceId` and the server
396
- `replayAfterSeq`; public message input maps to Game API `content`, cancellation
397
- always sends an explicit run id, and PLAY sends a two-second heartbeat only
398
- while attached, active, and visible. Heartbeat, kill, and stale-epoch failures
399
- immediately clear local authority.
400
-
401
- Mode changes adopt the complete server-repinned session (registry digest,
402
- provider/app policy revisions, and context version) before loading its new
403
- effective descriptors. The Studio mount resolves the selected saved project
404
- after `CrowdyStudioController.initialize()`, overrides caller guesses when
405
- creating a BUILD session, and rejects an existing session bound to another
406
- project. Because v1 has no set-project mutation, switching projects fences the
407
- agent and requires a new session/remount.
408
-
409
- An active BUILD workspace lease is renewed every ten seconds through the same
410
- authenticated heartbeat; the server returns its renewed 30-second expiry.
411
- Human edits, project/context changes, lease revocation, disconnect, and destroy
412
- stop renewal and abort browser handlers. Run events retain their stable
413
- `code`, `reason`, and typed safe `error`.
414
-
415
- The committed descriptor digest fixture is checked on every build against the
416
- canonical registry. Coordinated refreshes first run
417
- `npm run agent-descriptors:drift -- --source <game-api-fixture>`; builds then
418
- recompute the full and canonical 28-tool Game API subset digest (14 mandatory
419
- game plus 14 Studio/diagnostic/runtime tools) so the fixture cannot silently
420
- diverge.
421
-
422
- Mount the responsive agent dock by adding `agent`; omit it to retain the v11
423
- manual UI unchanged:
424
-
425
- ```ts
426
- const studio = await mountCrowdyStudio(host, {
427
- projectProvider: game.crowdyStudio,
428
- playerCompute: game.playerCompute,
429
- appId,
430
- gridId,
431
- agent: {
432
- transport: game.crowdyStudioAgent,
433
- sessionId,
434
- playerHost: bwfPlayerHostAdapter,
435
- },
436
- });
437
-
438
- studio.agent?.sendMessage('Explain the current diagnostics');
439
- studio.controlLeaseManager?.preempt('HUMAN_INPUT');
440
- ```
441
-
442
- The dock provides human-owned Ask/Build/Play selection, chat streaming,
443
- plan/tool status, exact approval hashes, diff/checkpoint cards, budget, visible
444
- lease scope/expiry, Pause/Resume/Stop, and checkpoint-restore requests. It uses
445
- text nodes for all untrusted content, ARIA status/log/group labels, visible
446
- focus, keyboard submission, and a container-query layout that moves the agent
447
- dock below narrow embeds.
448
-
449
- The headless `CrowdyStudioController` now also exposes:
157
+ ## Game-loop lifecycle
450
158
 
451
- - `prepareForAgentWork()` to flush autosave and fail closed on conflict/offline;
452
- - `applyAtomicPatch()` and `synchronizeProject()` for all-or-none multi-file
453
- updates, human-edit preemption, and Monaco synchronization;
454
- - `refreshCheckpoints()` / `restoreCheckpoint()` through the optional
455
- transport-neutral `CrowdyStudioSynchronizationProvider` (the provider is
456
- required for agent writes/restores and must make checkpoint + revision
457
- changes durable and atomic);
458
- - typed draft/live results plus `runtimeSync` (`RUNNING_SAVED`,
459
- `RUNNING_STALE`, `STOPPED`, or `NEVER_RUN`).
159
+ 1. Sign in (passwordless) on the identity client with `client.auth`, or
160
+ restore a stored session with `client.session.restore()`. This yields the
161
+ **session token** (Management API only).
162
+ 2. Mint an **app-scoped token** for the app (`identity.portal.mintAppToken(appId)`,
163
+ or the PKCE portal flow across origins) and build a per-game client holding
164
+ it (`game.setToken(token)`). The gameplay steps below run on that **game**
165
+ client.
166
+ 3. Subscribe to UDP proxy notifications with `game.udp.subscribe(handlers, appId)`
167
+ `appId` is **required** (the SDK opens the realtime socket on demand and
168
+ scopes it to that app).
169
+ 4. Join a chunk by sending an initial actor update.
170
+ 5. Send actor, voxel, text, audio, and client-event updates through `game.udp`
171
+ or the higher-level `game.world(appId)` helpers.
172
+ 6. Before the app token expires, call `game.refreshGameplayToken()`.
173
+ 7. Call `client.close()` (and `game.close()`) when disposing the SDK instances.
460
174
 
461
- ### PlayerHostAdapter and Blocks with Friends integration
175
+ ## Sub-clients at a glance
462
176
 
463
- `@crowdedkingdoms/crowdyjs/player-host` exports
464
- `PlayerHostAdapterV1`, capability/observation/command/result types and schemas,
465
- `AgentControlLeaseManager`, and `createPlayerHostAgentTools`. A game adapter
466
- implements only:
177
+ **Game-client surface** (end-user, browser-safe):
467
178
 
468
- ```ts
469
- interface PlayerHostAdapterV1 {
470
- readonly contractVersion: 'crowdy.player-host/1';
471
- capabilities(): Promise<PlayerHostCapabilitiesV1>;
472
- observe(request: ObserveRequestV1): Promise<GameObservationV1>;
473
- dispatch(command: GameCommandV1, gate: ValidatedGateV1):
474
- Promise<GameCommandResultV1>;
475
- clearAgentIntent(reason: CrowdyAgentPreemptionReason): void;
476
- }
477
- ```
179
+ | Sub-client | What it does |
180
+ |---|---|
181
+ | `client.auth` | Passwordless sign-in (magic link, social/OIDC, dev bypass), log out, and linked identities (`myIdentities`, `linkIdentity`/`unlinkIdentity`). |
182
+ | `client.users` | `me`, `updateGamertag`, profile reads. |
183
+ | `client.session` | Token store, `restore()`, `getToken()`, manual `setToken()`. |
184
+ | `client.portal` | App-scoped token minting (`mintAppToken`) and the cross-origin PKCE entry flow (`beginEntry` / `handleAuthorizeRequest` / `completeEntry` / `refresh`). |
185
+ | `client.platform` | Public platform configuration (`config()`). |
186
+ | `client.serverStatus` | `gameClientBootstrap(appId)` — per-app version info, UDP status, spatial limits. |
187
+ | `client.chunks`, `client.voxels`, `client.actors`, `client.avatars`, `client.state` | World data reads + writes: terrain/LODs, voxel edit + history/rollback, durable actors, avatars, per-user app state blobs. |
188
+ | `client.host` | Game-host election (`get`, `amIHost`) + actor liveness `heartbeat`. `amIHost` is UI convenience only — authoritative host gating uses `gameModelInvoke`'s `is_host` policy. |
189
+ | `client.teleport` | Teleport requests. |
190
+ | `client.channels`, `client.teams` | Messaging channels and app-scoped player teams (membership + roles). |
191
+ | `client.gameModel` | Abstract game model: containers, properties, functions (incl. model-driven `notify_*` effects), sessions, app-scoped active-session counts (`activePlayerCount`, `activePlayerCountChanged`), container-change push (`containerChanged`), flow-correlation timelines (`flow`), and automations / NPCs (`upsertAutomation`, `runAutomation`, `automationRuns`, `automationStats`, …). |
192
+ | `client.compute` | Compute Modules — server-side Rust/WASM logic: author + deploy source (`upsertModule`, `deployVersion`, `deployTemplate`, `waitForCompile`), triggers + policy, synchronous `invoke`, and monitoring (`moduleRuns`, `moduleStats`, `moduleLogs`, `appDiagnostics`). See [Compute Modules](https://docs.crowdedkingdoms.com/game-api/compute-modules). |
193
+ | `client.playerCompute` | Player-authored SERVER/CLIENT Rust/WASM bound to player-owned grids: deploy source, activate/deactivate, list modules/versions, delete self-authored modules. |
194
+ | `client.playerModel` | Player-owned flexible model containers and grid-confined automations (`containers`, `createContainer`, `setProperty`, `automations`, `createAutomation`, …). |
195
+ | `client.playerWallet` | Player spend: balance, spend caps, card setup, policy, charges. |
196
+ | `client.marketplace` | Player-code store/install/consent flows plus player-authorized grid claims (`claimGridOwnership`, `claimGridChunk`, `releaseClaimedGrid`) and client-mod artifact fetches. |
197
+ | `client.crowdyStudio` | Cloud project, personal-library, and common-file APIs for Crowdy Studio: target-scoped files, metadata/module names, optimistic revisions, copy-by-value imports, atomic saves. |
198
+ | `client.crowdyStudioAgent` | Generated, app-token Game API transport for durable agent sessions: history/session pages, descriptors/budgets, approvals, tool results, heartbeat, control mutations, ordered event subscriptions. |
199
+ | `client.udp` | UDP proxy subscriptions + spatial mutations (`sendActorUpdate`, `sendVoxelUpdate`, `sendAudioPacket`, `sendTextPacket`, `sendClientEvent`, `sendSingleActorMessage`, `sendChannelMessage`). |
200
+ | `client.realtime` | Connection status, manual `connect()` / `disconnect()`, `onStatus()` listener. |
201
+ | `client.refreshGameplayToken()` | Safely rotates an active game client's app token (see [Token refresh](#token-refresh-during-gameplay)). |
202
+ | `client.world(appId)` | Higher-level helpers for browser games (`actor.join`, `actor.sendState`, `actor.sendText`, `actor.sendToActor`). |
203
+ | `client.kit(appId)` | Game Kit: ready-made mappings of game concepts onto the game model — see [Game Kit](#game-kit). |
204
+ | `createWorldSession(client, appId, config)` | World Stores: opt-in, SDK-managed game state from the `@crowdedkingdoms/crowdyjs/stores` subpath — see [World Stores](#world-stores). |
478
205
 
479
- For Blocks with Friends, implement this interface over the same typed intent
480
- methods used by `PlayerController`, `ActionService`, `InventoryService`,
481
- `FishingService`, `RideService`, and existing referee/model calls. Construct
482
- `createPlayerHostAgentTools(adapter)` and pass `adapter` as the mount's
483
- `playerHost`. The returned lease manager enforces TTL, scopes, client epoch,
484
- host/controlled-entity revision, observation freshness, modal/death state,
485
- per-command rate limits, approvals, and tool-call dedup. Call
486
- `leaseManager.preempt('HUMAN_INPUT')` synchronously on key/mouse/touch input,
487
- `ESCAPE` on Escape, and the corresponding reason on death, target/context
488
- change, disconnect, or Stop. `clearAgentIntent` must immediately zero movement,
489
- look, and pending action intent. BWF does not integrate through raw CrowdyJS,
490
- DOM input, UDP packets, `PlayerCodeBroker`, or client-mod host calls.
491
-
492
- BWF should pass only `game.crowdyStudioAgent` and its
493
- `BwfPlayerHostAdapter` to the Studio mount; the mount derives the selected
494
- project id. Its adapter must implement the exact 14 mandatory game surfaces,
495
- classify combat as conditional approval, return typed failed/denied/unknown
496
- outcomes, and make `clearAgentIntent` synchronously cancel shared movement and
497
- action intents when the dispatcher aborts.
498
-
499
- The reconciled Game API development pilot advertises the exact 28-tool
500
- follow-up subset, including all 14 mandatory game descriptors. CrowdyJS routes
501
- all game tools through
502
- `PlayerHostAdapterV1`, the scoped lease gate, conditional combat approval,
503
- safety stop, heartbeat fencing, and typed result continuation. BWF Play now
504
- remains blocked only on the concrete BWF adapter/shared-intent wiring and its
505
- host/app policy configuration.
506
-
507
- The same browser dispatcher routes backend-advertised `runtime.test_draft`,
508
- `runtime.deploy_live`, `runtime.invoke`, and `runtime.stop` through the
509
- headless Studio controller. Draft testing is routine BUILD work; live deploy
510
- and LIVE invoke require the exact approval grant; stop remains an idempotent
511
- safety action. Inner game `FAILED`, `DENIED`, or `OUTCOME_UNKNOWN` results are
512
- promoted to matching outer terminal tool results and are never reported as
513
- successful.
514
-
515
- Runtime execution is plan-bound: requested targets must exactly equal the
516
- selected project’s authoritative target set, so a full-stack project cannot run
517
- CLIENT after SERVER-only authorization. Live deploy additionally rechecks the
518
- saved revision, canonical project content hash (including module names/files),
519
- and pairing preference after autosave. Invoke checks the exact running
520
- DRAFT/LIVE environment and export; stop always stops the complete selected
521
- project as a safety action rather than interpreting a partial target.
522
-
523
- | `createWorldSession(client, appId, config)` (from `@crowdedkingdoms/crowdyjs/stores`) | World Stores: opt-in, SDK-managed game state — typed codecs (`structCodec` binary DSL), your own actor with a 5 Hz send loop (`session.self`), a remote-actor registry with lanes/history/staleness (`session.actors`), attributed send errors (`session.errors`), a chunk/voxel cache with realtime merge + worldgen write-back (`session.chunks`), channel/direct-message inboxes + a typed event router, host tracking, typed save/avatar state, and a game-model container mirror. Only configured stores exist (compile-time + runtime); unimported stores tree-shake away. |
524
- | `client.kit(appId)` | Game Kit: ready-made mappings of game concepts onto the game model — `kit.inventory`, `kit.objects` (lockable doors/chests with custom permissions), `kit.npcs`, `kit.plots` (buy/rent land with transactional, replication-enforced grid grants), and the genre layers `kit.economy` (wallets/shops/trades/market), `kit.progression` (xp/skills/achievements/rating), `kit.loot`, `kit.quests`, `kit.combat`, `kit.matches` (session lobbies/turns/scores with notify-to-pull channels), `kit.decks` (hidden hands), `kit.worldsim` (clock/nodes/crops/waves), `kit.social` (parties/guilds/chat over teams+channels), `kit.leaderboards`, `kit.features` (tier gates), and the engine-aware helpers `kit.mobs` (refereed attacks, defs/slots, contact-damage parsing), `kit.pets` (adopt/summon/dismiss/rename), `kit.instances` (private world slices, seeded runs), `kit.director` (encounter runs), `kit.matchmaking` (queues/proposals/rating), `kit.minigames` (invoke-loop wrapper), `kit.economy.orderBook` (escrowed bid/ask market), engine paths on `kit.matches`/`kit.decks`/`kit.leaderboards`, `kit.quests` tutorial sequencing, `kit.engines` (compute capability detection) with `kit/wire` (the engine pose codec, `engineLanes()`, and the 77/90/91/92/93 event parsers) — plus blueprint builders + `kit.deploy(...)` for the admin "load the rules" step. |
525
-
526
- **Studio-admin surface** (privileged; drive with a server-side / studio token, grouped under `client.admin`):
206
+ **Studio-admin surface** (privileged; drive with a server-side / studio token,
207
+ grouped under `client.admin` and mirrored at the top level):
527
208
 
528
209
  | Sub-client | What it does |
529
210
  |---|---|
530
211
  | `client.organizations` | Orgs, members, RBAC roles, org API tokens. |
531
- | `client.apps` | App discovery + routing (`createApp` etc. via the management API directly). |
212
+ | `client.apps` | App registry, discovery + routing (`create`, `routeFor`, `marketplace`), visibility, and player-code admission mode / allow-list administration. |
532
213
  | `client.appAccess` | Access tiers + per-user grants. |
533
214
  | `client.billing` | Org wallet + per-app spend budgets. |
534
215
  | `client.payments` | Payment checkouts (wallet top-ups, plan purchases). |
535
216
  | `client.quotas` | Usage quotas at the org/app scope. |
536
- | `client.environments` | Dedicated environments: quote, provision, scale, deploy, link apps. |
537
217
  | `client.usage` | Replication + GraphQL usage reporting. |
538
218
  | `client.sharedEnvironment` | Publish to shared, runtime gating, spend caps, auto-billing. |
539
219
  | `client.gameApps` | App grids (`createGrid` / `deleteGrid`), first-class grid ownership (`ownership` / `assignOwnership` / `transferOwnership`), and grid runtime-permission administration. |
540
- | `client.apps` | App registry/routing plus player-code admission mode and code/author/org allow-list administration. |
541
220
 
542
221
  **Operator surface** (platform operations; requires `is_operator`):
543
222
 
544
223
  | Sub-client | What it does |
545
224
  |---|---|
546
- | `client.operator` | Control plane: cross-org environments, change orders, secrets, release management, audit. |
225
+ | `client.operator` | Platform compute ceilings (`computePlatformCeilings`, `setComputePlatformCeilings`). Infrastructure operations live in the separate infra-control-plane service, not this SDK. |
547
226
 
548
- Auth, user reads, and the studio-admin / operator surfaces target `managementUrl` and use the **identity session token**; the game-client world/UDP surfaces target `httpUrl` / `wsUrl` and require an **app-scoped token** for that app. Use one identity client plus a per-game client (see [Overworld portals & app-scoped tokens (v7)](#overworld-portals--app-scoped-tokens-v7)); each client's `AuthState` carries its token to its own endpoints, so HTTP and WebSocket auth never drift within a client.
549
-
550
- ## Game-loop lifecycle
551
-
552
- 1. Sign in (passwordless) on the identity client with `client.auth` — `requestLoginLink`/`completeLoginLink` (magic link), `socialLoginStart`/`socialLoginComplete` (social/OIDC), or `devLogin` (dev/test only) — or `client.session.restore()`. This yields the **session token** (Management API only).
553
- 2. Mint an **app-scoped token** for the app (`identity.portal.mintAppToken(appId)`, or the PKCE portal flow across origins) and build a per-game client holding it (`game.setToken(token)`). The gameplay steps below run on that **game** client.
554
- 3. Subscribe to UDP proxy notifications with `game.udp.subscribe(handlers, appId)` — `appId` is **required** (the SDK opens the realtime socket on demand and scopes it to that app).
555
- 4. Join a chunk by sending an initial actor update.
556
- 5. Send actor, voxel, text, audio, and client-event updates through `game.udp` or the higher-level `game.world(appId)` helpers.
557
- 6. Before the app token expires, call `game.refreshGameplayToken()` while gameplay is active. It closes the old-token UDP proxy before rotating the token, opens the new-token proxy, and lets the existing realtime subscription restart without adding handlers. Use `game.portal.refresh()` directly only when no UDP proxy lifecycle needs to be preserved.
558
- 7. Call `client.close()` (and `game.close()`) when disposing the SDK instances.
227
+ Auth, user reads, and the studio-admin / operator surfaces target
228
+ `managementUrl` and use the **identity session token**; the game-client
229
+ world/UDP surfaces target `httpUrl` / `wsUrl` and require an **app-scoped
230
+ token** for that app.
559
231
 
560
- `refreshGameplayToken()` deliberately stops on the first failed stage. If the
561
- old proxy cannot confirm disconnect, no refresh is attempted and the old token
562
- remains active. If refresh fails, the old token is still retained (although its
563
- proxy was closed and may be reopened). If the new proxy connect fails, the
564
- fresh token remains stored; surface the error and retry `game.udp.connect()`
565
- instead of rotating again.
232
+ CrowdyJS wraps the full public Management + Game API surface — every
233
+ non-deprecated public root field has a typed method, with Relay `*Connection`
234
+ cursor-pagination variants alongside the legacy offset lists. The SDK never
235
+ relaxes server-side authorization: exposing an operation here just gives you a
236
+ typed wrapper; the caller still needs the right token and permission. Drive
237
+ privileged surfaces from a studio backend with an org-scoped or admin token,
238
+ not from an untrusted browser.
566
239
 
567
240
  ## Per-app routing
568
241
 
569
- When a player is about to join an app, query its routing fields on the management API first:
242
+ `mintAppToken` returns `gameApiUrl` / `gameApiWsUrl`, so you rarely need a
243
+ separate routing query. When you do (e.g. pre-flight discovery), query the
244
+ app's routing fields on the Management API:
570
245
 
571
246
  ```graphql
572
247
  query AppForRouting($appId: BigInt!) {
@@ -579,23 +254,21 @@ query AppForRouting($appId: BigInt!) {
579
254
  }
580
255
  ```
581
256
 
582
- `gameApiUrl` is populated for **both** dedicated (`splitMode`) and shared
583
- (`deploymentTarget: "shared"`) apps. When it's set, build a **second**
584
- `CrowdyClient` with `httpUrl: gameApiUrl` (and the matching `wsUrl`) holding that
585
- app's **app-scoped token** (`identity.portal.mintAppToken(appId)` do **not**
586
- reuse the identity client's session token store), then drive gameplay through that
587
- client. In practice `mintAppToken` already returns `gameApiUrl` / `gameApiWsUrl`,
588
- so you rarely need this separate routing query. Apps with no `gameApiUrl` keep
589
- working against the default `httpUrl` you configured.
257
+ `gameApiUrl` is populated for both dedicated (`splitMode`) and shared
258
+ (`deploymentTarget: "shared"`) apps. When it's set, build a second
259
+ `CrowdyClient` with `httpUrl: gameApiUrl` (and the matching `wsUrl`) holding
260
+ that app's app-scoped token, then drive gameplay through that client. Apps
261
+ with no `gameApiUrl` keep working against the default `httpUrl` you
262
+ configured. See [Loading an app's Game API](https://docs.crowdedkingdoms.com/crowdyjs/shared-environment-routing).
590
263
 
591
264
  ## Realtime notifications
592
265
 
593
- `subscribe` takes the handlers **and a required `appId`** (second argument). The
594
- Game API scopes the realtime session to that app and rejects an app-agnostic
595
- subscription with a `RealtimeConnectionEvent` (`code: 'APP_ID_REQUIRED'`). It also
596
- rejects an identity session token (`APP_TOKEN_REQUIRED`) or a token scoped to a
597
- different app (`APP_SCOPE_MISMATCH`). Run one client per app (each holding that
598
- app's app-scoped token) when a player is in multiple apps at once.
266
+ `subscribe` takes the handlers **and a required `appId`** (second argument).
267
+ The Game API scopes the realtime session to that app and rejects an
268
+ app-agnostic subscription with a `RealtimeConnectionEvent`
269
+ (`code: 'APP_ID_REQUIRED'`); a missing or invalid gameplay token is rejected
270
+ with `AUTH_REQUIRED`. Run one client per app (each holding that app's
271
+ app-scoped token) when a player is in multiple apps at once.
599
272
 
600
273
  ```ts
601
274
  const appId = '1';
@@ -614,6 +287,10 @@ const unsubscribe = client.udp.subscribe(
614
287
  // A direct actor-to-actor message addressed to you.
615
288
  console.log(event.uuid, event.payload); // payload is base64
616
289
  },
290
+ channelMessage: (event) => {
291
+ // A message broadcast on a channel you're subscribed to.
292
+ console.log(event.channelId, event.payload); // payload is base64
293
+ },
617
294
  genericError: (event) => {
618
295
  console.warn(event.sequenceNumber, event.errorCode);
619
296
  },
@@ -638,7 +315,13 @@ client.realtime.onStatus((status) => {
638
315
  unsubscribe();
639
316
  ```
640
317
 
641
- The SDK uses the `graphql-transport-ws` protocol through `graphql-ws`, reconnects with backoff, re-reads the current token before reconnecting, and resubscribes automatically.
318
+ The SDK uses the `graphql-transport-ws` protocol through `graphql-ws`,
319
+ reconnects with backoff, re-reads the current token before reconnecting, and
320
+ resubscribes automatically. `RealtimeConnectionEvent` carries a `retryable`
321
+ flag: `UDP_PROXY_CONNECTION_FAILED` is transient (back off and resubscribe),
322
+ while `AUTH_REQUIRED` / `APP_ID_REQUIRED` must be fixed by the caller first.
323
+ Unsubscribing stops delivery only; call `client.udp.disconnect()` to close the
324
+ UDP proxy session.
642
325
 
643
326
  ## Spatial sends
644
327
 
@@ -655,15 +338,19 @@ const response = await client.udp.sendActorUpdateAndWait({
655
338
  console.log(response.__typename, response.sequenceNumber);
656
339
  ```
657
340
 
658
- The plain `sendActorUpdate`, `sendVoxelUpdate`, `sendAudioPacket`, `sendTextPacket`, and `sendClientEvent` methods return the GraphQL mutation result immediately. The `AndWait` variants allocate a `sequenceNumber` when one is missing and wait for either a matching notification or `GenericErrorResponse`.
341
+ The plain `sendActorUpdate`, `sendVoxelUpdate`, `sendAudioPacket`,
342
+ `sendTextPacket`, and `sendClientEvent` methods return the GraphQL mutation
343
+ result immediately. The `AndWait` variants allocate a `sequenceNumber` when
344
+ one is missing and wait for either a matching notification or
345
+ `GenericErrorResponse`. `sendChannelMessage` broadcasts an opaque payload on a
346
+ channel.
659
347
 
660
348
  ### Actor-to-actor messages
661
349
 
662
350
  ```ts
663
351
  // Delivered only to the actor whose UUID matches `targetUuid`; you must know
664
- // that actor's current chunk. Fire-and-forget — the sender gets no echo, so
665
- // there is no `AndWait` variant. The target receives a
666
- // `SingleActorMessageNotification` on its subscription.
352
+ // that actor's current chunk. Fire-and-forget — the sender gets no echo. The
353
+ // target receives a `SingleActorMessageNotification` on its subscription.
667
354
  await client.udp.sendSingleActorMessage({
668
355
  appId: '1',
669
356
  chunk: { x: '7', y: '1', z: '2' }, // the TARGET actor's chunk
@@ -690,23 +377,24 @@ await actor.sendToActor(
690
377
  );
691
378
  ```
692
379
 
693
- The world helpers are thin wrappers over `client.udp.*` with the appId pre-bound — convenient for browser games. Advanced callers can always use `client.udp.*` with the generated GraphQL input types directly.
380
+ The world helpers are thin wrappers over `client.udp.*` with the appId
381
+ pre-bound — convenient for browser games. Advanced callers can always use
382
+ `client.udp.*` with the generated GraphQL input types directly.
694
383
 
695
384
  ## World Stores
696
385
 
697
386
  The core client is a thin transport; the **World Stores** layer
698
- (`@crowdedkingdoms/crowdyjs/stores`, 8.4+) adds the source-of-truth data
699
- structures every game otherwise hand-writes: actor registries, chunk/voxel
700
- caches, error attribution, message inboxes, host tracking, and typed
701
- durable-state wrappers — all driven by ONE shared `udpNotifications`
702
- subscription and ONE scheduler.
387
+ (`@crowdedkingdoms/crowdyjs/stores`) adds the source-of-truth data structures
388
+ every game otherwise hand-writes: actor registries, chunk/voxel caches, error
389
+ attribution, message inboxes, host tracking, and typed durable-state wrappers
390
+ — all driven by ONE shared `udpNotifications` subscription and ONE scheduler.
703
391
 
704
392
  ```ts
705
393
  import {
706
394
  createWorldSession, structCodec, f32, u8, jsonCodec, workerTicker,
707
395
  } from '@crowdedkingdoms/crowdyjs/stores';
708
396
 
709
- // Describe your replication state ONCE (48-byte binary layouts, declaratively):
397
+ // Describe your replication state ONCE (binary layouts, declaratively):
710
398
  const poseCodec = structCodec({
711
399
  x: f32(), y: f32(), z: f32(), yaw: f32(),
712
400
  flags: u8(), held: u8(),
@@ -741,6 +429,13 @@ session.errors.onError((e) => console.warn(e.errorCode, e.send?.kind));
741
429
  session.dispose();
742
430
  ```
743
431
 
432
+ The available stores are `self` (your actor + send loop), `actors` (remote
433
+ actor registry with lanes/history/staleness), `errors` (attributed send
434
+ errors), `chunks` (chunk/voxel cache with realtime merge + worldgen
435
+ write-back), `channelInbox` / `actorInbox` (message inboxes), `events` (typed
436
+ event router), `host` (host tracking), `save` / `avatar` (typed durable
437
+ state), and `model` (game-model container mirror).
438
+
744
439
  Every store is **opt-in twice over**: only configured stores are constructed
745
440
  (and only they exist on the session's TYPE — `session.host` without
746
441
  `host: ...` in the config is a compile error), and the layer lives behind the
@@ -786,8 +481,7 @@ const result = await kit.objects.open(doorId, { keyId });
786
481
  if (!result.success) console.warn('locked:', result.errorMessage);
787
482
  ```
788
483
 
789
- Land sale closes the permission loop end to end (requires game-api v0.13.11+ for
790
- effects, v0.13.12+ for the chunk-permission reads):
484
+ Land sale closes the permission loop end to end:
791
485
 
792
486
  ```ts
793
487
  // Studio: sell a plot over a grid; doors on it honor the purchase automatically.
@@ -807,28 +501,138 @@ selector has `candidatePermissionWhere: [{ userFrom: { property: 'owner_user_id'
807
501
  op: 'lacks', key: 'access', grid: { property: 'grid_id' } }]` reacts only to
808
502
  intruders.
809
503
 
810
- As of 8.3.0 the kit covers the common genre staples end to end — every layer
811
- is a blueprint builder + typed runtime helper:
504
+ The kit covers the common genre staples end to end — each layer is a blueprint
505
+ builder plus a typed runtime helper:
812
506
 
813
507
  | Layer | Builder → helper | Highlights |
814
508
  | --- | --- | --- |
815
- | Economy | `economyBlueprint` → `kit.economy` | multi-currency wallets, atomic shop buys, escrow trades, player market, restock automation |
509
+ | Inventory | `inventoryBlueprint` → `kit.inventory` | bags/stacks, grant/consume/transfer, craft, barter |
510
+ | Objects | `lockBlueprint` → `kit.objects` | lockable doors/chests with key or permission authority |
511
+ | NPCs | `npcBlueprint` → `kit.npcs` | automation-driven NPC instances (spawn, runNow, enable) |
512
+ | Plots | `plotBlueprint` → `kit.plots` | buy/rent land with transactional, replication-enforced grid grants |
513
+ | Economy | `economyBlueprint` → `kit.economy` | multi-currency wallets, atomic shop buys, escrow trades, player market, escrowed order book (`kit.economy.orderBook`) |
816
514
  | Progression | `progressionBlueprint` → `kit.progression` | xp/levels via the `fn:` curve helper, skill prerequisite chains, achievements, host-gated rating |
817
515
  | Loot | `lootBlueprint` → `kit.loot` | weighted tables unrolled into seed-driven expressions, atomic single-claim, event-triggered drops |
818
- | Quests | `questsBlueprint` → `kit.quests` | event-automation progress, atomic claim into stack+wallet, cron daily resets |
819
- | Combat | `combatBlueprint` → `kit.combat` | server-side damage/death, status-effect tick automation (selector join), `turnBased`/`hostSynced` |
516
+ | Quests | `questsBlueprint` → `kit.quests` | event-automation progress, atomic claim into stack+wallet, cron daily resets, tutorial sequencing |
517
+ | Combat | `combatBlueprint` → `kit.combat` | server-side damage/death, status-effect tick automation, `turnBased`/`hostSynced`, routed attacks |
820
518
  | Matches | `matchesBlueprint` → `kit.matches` | session lobbies/rounds/turns/scores, per-match channel + `onMatchChanged` (notify-to-pull) |
821
519
  | Decks | `decksBlueprint` → `kit.decks` | hidden hands via owner-visibility `card_id`, shuffle-by-position automation |
822
- | World sim | `worldsimBlueprint` → `kit.worldsim` | day/night clock with spatial notify, node regen + atomic gather, crops, wave counters |
520
+ | World sim | `worldsimBlueprint` → `kit.worldsim` | day/night clock with spatial notify, node regen + atomic gather, crops, wave counters, forecasts |
823
521
  | Social | `guildBlueprint` → `kit.social` | parties/guilds/chat over teams+channels, grid territory grants, guild hall + bank composite |
824
522
  | Leaderboards | `leaderboardsBlueprint` → `kit.leaderboards` | trusted keep-best submits, client-side ranking, cron seasons |
523
+ | Live ops | `liveopsBlueprint` → `kit.liveops` | timed event windows and seasons |
524
+ | Moderation | `moderationBlueprint` → `kit.moderation` | reports, queues, mutes |
525
+ | Telemetry | `telemetryBlueprint` → `kit.telemetry` | counters and lightweight event tracking |
825
526
  | Monetization | `featureGate` → `kit.features` | feature keys, tier grants, `*policyExtra` gating on builders |
527
+ | Abilities | — → `kit.abilities` | ability definitions, casts, loadouts |
528
+ | Movement | — → `kit.movement` | movement warden configs + violation parsing |
529
+ | Territory | — → `kit.territory` | control points, factions, enrollment |
530
+ | Racing | — → `kit.racing` | courses, entries, possession (claim/pass/shoot) |
531
+
532
+ Engine-aware layers talk to compute-module game engines when they are
533
+ deployed, and degrade gracefully on model-only apps via capability detection
534
+ (`kit.engines`): `kit.mobs` (refereed attacks, defs/slots, contact-damage
535
+ parsing), `kit.pets` (adopt/summon/dismiss/rename), `kit.instances` (private
536
+ world slices, seeded runs), `kit.director` (encounter runs),
537
+ `kit.matchmaking` (queues/proposals/rating), `kit.minigames` (invoke-loop
538
+ wrapper), plus engine paths on `kit.matches` / `kit.decks` /
539
+ `kit.leaderboards` and the `kit/wire` pose codec + event parsers. Deploy
540
+ engines alongside blueprints with `kit.deploy(blueprints, { engines })`.
826
541
 
827
542
  See the docs guides [Modeling game concepts](https://docs.crowdedkingdoms.com/game-api/modeling-game-concepts)
828
543
  (the underlying model + genre map) and [Game Kit](https://docs.crowdedkingdoms.com/crowdyjs/game-kit)
829
544
  (the SDK surface + the simulation-tier / notify-to-pull / timer / hidden-info
830
545
  / anti-cheat patterns).
831
546
 
547
+ ## Crowdy Studio
548
+
549
+ Crowdy Studio is the in-game SERVER/CLIENT Rust authoring surface for player
550
+ code: cloud projects with target-scoped files, optimistic-concurrency
551
+ autosave, draft/live/stop orchestration, and a Monaco editor backed by a local
552
+ Rust language worker. The worker receives source files and the embedded
553
+ platform index only — never a credential and never a server connection.
554
+ Compiled CLIENT artifacts run through `PlayerCodeBroker`, which keeps tokens
555
+ on the page, allow-lists host calls, and locally clamps chunk-targeted effects
556
+ to the owned grid before the normal SDK path reaches server authorization.
557
+
558
+ Most games embed the ready-made shell rather than hand-rolling window chrome
559
+ around `mountCrowdyStudio`:
560
+
561
+ ```ts
562
+ import { createCrowdyStudioEmbed } from '@crowdedkingdoms/crowdyjs/crowdy-studio';
563
+ // Self-starting glue worker for CLIENT mods (Vite shown; any bundler that
564
+ // packages module workers works):
565
+ import workerUrl from '@crowdedkingdoms/crowdyjs/player-glue-worker?worker&url';
566
+
567
+ const studio = createCrowdyStudioEmbed({
568
+ client: game, // CrowdyClient: crowdyStudio, playerCompute, playerWallet, crowdyStudioAgent
569
+ appId,
570
+ gameName: 'My Game',
571
+ suppressGameplayInput: () => pauseInput(),
572
+ onLayoutChange: () => resizeCanvas(),
573
+ });
574
+
575
+ // Per open (for example after the player claims a grid):
576
+ studio.toggle({
577
+ gridId,
578
+ targetPermissions: {
579
+ SERVER: { canWrite: true, canRun: true },
580
+ CLIENT: { canWrite: false, canRun: false }, // SERVER-only embeds omit workerUrl
581
+ },
582
+ });
583
+ ```
584
+
585
+ The embed renders a resizable right dock on desktop and a focus-trapped
586
+ fullscreen modal on narrow screens, and mounts the agent dock automatically
587
+ when the client exposes `crowdyStudioAgent` and the game passes a
588
+ `playerHost`. For custom chrome, call `mountCrowdyStudio(host, options)`
589
+ directly; for a headless integration, use `new CrowdyStudioController(options)`.
590
+ New games should start SERVER-only. Untrusted HUD payloads always render as
591
+ text, never HTML.
592
+
593
+ See [Crowdy Studio & player client mods](https://docs.crowdedkingdoms.com/crowdyjs/player-client-mods)
594
+ and [Embed Crowdy Studio in your game](https://docs.crowdedkingdoms.com/crowdyjs/crowdy-studio-embed).
595
+
596
+ ### Agentic Crowdy Studio
597
+
598
+ The agent surface adds an Ask/Build/Play AI dock on top of Crowdy Studio,
599
+ built from three browser contracts (`crowdy.studio-agent/1`,
600
+ `crowdy.agent-tools/1`, `crowdy.player-host/1`):
601
+
602
+ - `@crowdedkingdoms/crowdyjs/agent` exports `CrowdyStudioAgentController` (the
603
+ durable session client: contiguous event ordering, replay/gap fill,
604
+ attach-epoch fencing, exact approval hashes, budgets, pause/resume/stop) and
605
+ `CROWDY_AGENT_TOOL_REGISTRY_V1`, an immutable digest-pinned registry of
606
+ bounded, schema-validated tools. There is deliberately no raw GraphQL
607
+ executor, DOM driver, `fetch`, shell, or unrestricted SDK bridge in these
608
+ surfaces.
609
+ - `client.crowdyStudioAgent` is the production GraphQL transport for durable
610
+ agent sessions; pass it (plus a `playerHost` adapter) to the Studio mount's
611
+ `agent` option to get the integrated dock.
612
+ - `@crowdedkingdoms/crowdyjs/player-host` exports the generic game
613
+ observation/control contract: implement `PlayerHostAdapterV1` over your
614
+ game's typed intent methods, and the exported `AgentControlLeaseManager`,
615
+ `PlayerControlGate`, and `AgentControlBanner` enforce scoped leases, TTLs,
616
+ synchronous human preemption, and always-visible Pause/Stop chrome.
617
+
618
+ ```ts
619
+ import { CrowdyStudioAgentController } from '@crowdedkingdoms/crowdyjs/agent';
620
+
621
+ const agent = new CrowdyStudioAgentController({
622
+ transport: game.crowdyStudioAgent,
623
+ createSession: {
624
+ appId, projectId, gridId,
625
+ mode: 'BUILD',
626
+ providerDataConsent: true,
627
+ idempotencyKey: crypto.randomUUID(),
628
+ },
629
+ });
630
+ await agent.initialize(); // attach epoch → durable replay/gap fill → live tail
631
+ ```
632
+
633
+ See [Agentic Crowdy Studio](https://docs.crowdedkingdoms.com/crowdyjs/agentic-crowdy-studio)
634
+ for the full session, lease, and approval model.
635
+
832
636
  ## Errors
833
637
 
834
638
  Transport and protocol failures throw structured error classes:
@@ -840,11 +644,18 @@ Transport and protocol failures throw structured error classes:
840
644
  - `CrowdyRealtimeError` — realtime subscription couldn't be established or was dropped.
841
645
  - `CrowdyProtocolError` — server response failed schema validation.
842
646
 
843
- GraphQL errors carry a stable `extensions.code` (e.g. `UNAUTHENTICATED`, `SCOPE_MISSING`, `FORBIDDEN`, `IDEMPOTENCY_CONFLICT`) plus, where applicable, `extensions.remediation` and `extensions.requiredPermission`. Branch on `error.extensions?.code` rather than parsing messages.
647
+ GraphQL errors carry a stable `extensions.code` (e.g. `UNAUTHENTICATED`,
648
+ `SCOPE_MISSING`, `FORBIDDEN`, `IDEMPOTENCY_CONFLICT`, `RATE_LIMITED`) plus,
649
+ where applicable, `extensions.remediation` and `extensions.requiredPermission`.
650
+ Branch on `error.extensions?.code` rather than parsing messages.
844
651
 
845
652
  ## Idempotent retries
846
653
 
847
- Destructive game-client mutations accept an optional **idempotency key**. Pass a stable key (e.g. `crypto.randomUUID()`) and a network retry replays the first result instead of applying the side effect twice. Reusing a key with different arguments throws a `CrowdyGraphQLError` with `extensions.code === 'IDEMPOTENCY_CONFLICT'`. Keys expire server-side after 24h.
654
+ Destructive game-client mutations accept an optional **idempotency key**. Pass
655
+ a stable key (e.g. `crypto.randomUUID()`) and a network retry replays the
656
+ first result instead of applying the side effect twice. Reusing a key with
657
+ different arguments throws a `CrowdyGraphQLError` with
658
+ `extensions.code === 'IDEMPOTENCY_CONFLICT'`. Keys expire server-side after 24h.
848
659
 
849
660
  ```ts
850
661
  const key = crypto.randomUUID();
@@ -855,100 +666,56 @@ await client.teams.leave(groupId, key); // leaveTeam
855
666
  await client.voxels.rollback({ ...input, idempotencyKey: key }); // input field
856
667
  ```
857
668
 
858
- The key parameter is optional and trailing, so it's safe to omit. Requires a server on release v0.1.19+ (see Server compatibility above).
859
-
860
- ## Auth notes
669
+ The key parameter is optional and trailing, so it's safe to omit.
861
670
 
862
- - Use `client.auth.setToken(token)` if you need to seed a token externally (e.g. when restoring auth from a non-default storage).
863
- - `client.session.restore()` reads from the configured `tokenStore`. `BrowserLocalStorageTokenStore` is provided; bring your own for SSR or Node usage.
864
- - Each client's `AuthState` is observed by both its HTTP client and its realtime socket, so HTTP and WebSocket auth never drift within a client. Hold the **identity session token** on the management/identity client and an **app-scoped token** on each per-game client (`client.portal` — see the [v7 section](#overworld-portals--app-scoped-tokens-v7)).
865
-
866
- ## Overworld portals & app-scoped tokens (v7)
867
-
868
- As of v7 gameplay requires an **app-scoped token**, not the session token.
869
- Passwordless sign-in returns an **identity session token** (Management API only —
870
- account, studio admin, and minting); each game is entered with a short-lived token
871
- confined to that one app, so a game stack never receives the player's full session.
671
+ ## Low-level GraphQL access
872
672
 
873
- Use two clients: an Overworld/identity client (session token) and a per-game
874
- client (app token), sharing only the Management URL.
673
+ Typed sub-client methods are first-class, but generated operation documents
674
+ are also available through a transport escape hatch — `client.graphql` for the
675
+ Game API and `client.management` for the Management API:
875
676
 
876
677
  ```ts
877
- // Overworld/identity client
878
- const overworld = createCrowdyClient({ managementUrl, tokenStore: new BrowserLocalStorageTokenStore('crowdyjs:session') });
879
- // Passwordless sign-in (magic link, social/OIDC, or dev bypass) yields the session token.
880
- await overworld.auth.requestLoginLink({ email, redirectUri });
881
- await overworld.auth.completeLoginLink(tokenFromLink);
678
+ import { VersionInfoDocument } from '@crowdedkingdoms/crowdyjs/generated';
882
679
 
883
- // Native / same-origin: mint directly, then build a game client.
884
- const t = await overworld.portal.mintAppToken(appId);
885
- const game = createCrowdyClient({ httpUrl: t.gameApiUrl!, wsUrl: t.gameApiWsUrl!, managementUrl,
886
- tokenStore: new BrowserLocalStorageTokenStore('crowdyjs:app:' + appId) });
887
- game.setToken(t.token);
888
- game.world(appId).subscribe({ actorUpdate: (n) => { /* ... */ } });
680
+ const data = await client.graphql.request(VersionInfoDocument);
889
681
  ```
890
682
 
891
- Browser cross-origin handoff is OAuth2 Authorization Code + PKCE — the verifier
892
- never leaves the game origin:
893
-
894
- ```ts
895
- // Game origin, on "enter": redirect to the Overworld authorize page.
896
- location.assign(await game.portal.beginEntry({
897
- appId, authorizeUrl: 'https://overworld.example.com/authorize',
898
- redirectUri: location.origin + location.pathname,
899
- }));
683
+ For any brand-new server field not yet wrapped, `client.graphql.request(...)` /
684
+ `client.management.request(...)` always works.
900
685
 
901
- // Overworld /authorize page (holds the session token):
902
- location.assign(await overworld.portal.handleAuthorizeRequest());
686
+ ## Maintainers: schema artifacts and fixtures
903
687
 
904
- // Game origin, on callback boot: exchange code+verifier -> app token (stored).
905
- const entered = await game.portal.completeEntry();
906
- // Keep active UDP gameplay running past expiry without orphaning the old proxy:
907
- await game.refreshGameplayToken();
908
- ```
688
+ CrowdyJS is a standalone public package: a clean clone builds with
689
+ `npm install && npm run build` using the artifacts committed to this repo —
690
+ no other repositories and no network access required:
909
691
 
910
- `portal.refresh()` remains available for clients with no active UDP lifecycle.
911
- Once a proxy or realtime gameplay session is active, prefer
912
- `refreshGameplayToken()` so the old Bearer closes its proxy before the token is
913
- revoked.
914
-
915
- Game-to-game routes through the Overworld for a fresh per-game token. New
916
- realtime codes: `APP_TOKEN_REQUIRED`, `APP_SCOPE_MISMATCH`; new `UdpErrorCode`:
917
- `TOKEN_EXPIRED`. See [MIGRATION.md](MIGRATION.md) for the full v7 breaking guide.
918
-
919
- ## Surface scope & security
920
-
921
- As of v6 (completed in v6.1), CrowdyJS wraps the **full** management-api + game-api
922
- public surface, not just the game-client subset — every non-deprecated public root
923
- field has a typed method, with Relay `*Connection` cursor-pagination variants
924
- alongside the legacy offset lists. The surfaces are namespaced by audience:
925
-
926
- - **Game-client** (`client.auth`, `client.users`, `client.udp`, `client.world(...)`,
927
- `client.chunks`/`voxels`/`actors`/`avatars`/`state`/`teleport`/`channels`/`teams`/
928
- `gameModel`/`host`) — safe for untrusted browser clients with an end-user token.
929
- - **Studio-admin** (`client.admin.*` — also reachable at the top level, e.g.
930
- `client.billing`) — privileged organization/app administration. Drive these from a
931
- **studio backend** with an org-scoped or admin token, **not** from an untrusted
932
- browser; the server still enforces the relevant org/app permission on every call.
933
- - **Operator** (`client.operator`) — platform control-plane operations that require
934
- `users.is_operator`. For internal operator tooling only.
935
-
936
- The SDK never relaxes server-side authorization — exposing an operation here just
937
- gives you a typed wrapper; the caller still needs the right token and permission. For
938
- any brand-new server field not yet wrapped, the low-level escape hatch
939
- (`client.graphql.request(...)` / `client.management.request(...)`) always works.
692
+ - `schema.gql` merged Management API + Game API SDL.
693
+ - `src/generated/graphql.ts` generated TypeScript operation types.
940
694
 
941
- ## Low-level GraphQL access
695
+ Schema refresh is explicit, from the published SDLs
696
+ ([management-api.graphql](https://docs.crowdedkingdoms.com/schema/management-api.graphql),
697
+ [game-api.graphql](https://docs.crowdedkingdoms.com/schema/game-api.graphql)):
942
698
 
943
- Game-client methods are first-class, but generated operation documents are also available through a transport escape hatch:
699
+ ```bash
700
+ npm run schema:sync:prod
701
+ npm run codegen
702
+ ```
944
703
 
945
- ```ts
946
- import { VersionInfoDocument } from '@crowdedkingdoms/crowdyjs/generated';
704
+ (`npm run schema:sync:paths -- --management <file-or-url> --game <file-or-url>`
705
+ accepts explicit sources.) Commit `schema.gql` and `src/generated/graphql.ts`
706
+ together whenever the public GraphQL surface changes; `npm run check:schema`
707
+ detects drift in CI/release work.
947
708
 
948
- const data = await client.graphql.request(VersionInfoDocument);
949
- ```
709
+ Two more committed fixtures follow the same boundary — the build validates
710
+ them locally and never reads a sibling checkout; coordinated maintainers
711
+ refresh them from an explicitly supplied source:
950
712
 
951
- Most consumers should prefer the typed methods on `client.auth`, `client.users`, `client.udp`, `client.serverStatus`, and `client.world()`.
713
+ - The browser Rust authoring index:
714
+ `npm run authoring-index:drift -- --source <exporter-json> [--write]`, then
715
+ `npm run authoring-index:generate`.
716
+ - The agent tool-descriptor fixture:
717
+ `npm run agent-descriptors:drift -- --source <game-api-fixture>`; every
718
+ build re-checks the digests.
952
719
 
953
720
  ## Migration
954
721