@crowdedkingdoms/crowdyjs 13.0.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,122 +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)).
4
-
5
- > **CrowdyJS v13 (breaking): unified galaxy API.** The platform merged the
6
- > Management API and Game API into ONE server on the shared galaxy database
7
- > (`managementUrl` and `httpUrl` may be the same origin now; the two-token
8
- > model is unchanged). The schema is resynced from the unified SDL, and the
9
- > retired surfaces are removed: `client.environments` (dedicated customer
10
- > environments no longer exist), the per-environment usage rollups and
11
- > capacity tier catalogs, and everything on `client.operator` except the
12
- > platform compute ceilings (infra operations moved to the separate
13
- > infra-control-plane service). See [MIGRATION.md](MIGRATION.md).
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.
14
8
 
15
- ## Install
16
-
17
- ```bash
18
- npm install @crowdedkingdoms/crowdyjs
19
- ```
20
-
21
- > **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).
22
-
23
- 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.
24
-
25
- > **CrowdyJS v12 (breaking): Agentic Crowdy Studio.** v12 adds the accepted
26
- > `crowdy.studio-agent/1`, `crowdy.agent-tools/1`, and
27
- > `crowdy.player-host/1` browser contracts. Import the provider-neutral durable
28
- > client and immutable tool registry from
29
- > `@crowdedkingdoms/crowdyjs/agent`, and generic game observations/control from
30
- > `@crowdedkingdoms/crowdyjs/player-host`. Crowdy Studio can mount the
31
- > integrated Ask/Build/Play dock when given an injectable agent transport.
32
- > There is deliberately no OpenRouter key/client, raw GraphQL executor, DOM
33
- > driver, `fetch`, shell, unrestricted SDK bridge, or client-mod `host_call` in
34
- > these surfaces. The reconciled Game API SDL, generated operations, and
35
- > production `CrowdyAgentGraphQLTransport` are included and exposed as
36
- > `client.crowdyStudioAgent`; tests may still inject
37
- > `CrowdyStudioAgentTransportV1`. Existing manual Studio mounting remains
38
- > unchanged when `agent` is omitted.
39
- >
40
- > **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.
41
- >
42
- > **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.
43
- >
44
- > **P1 player runtime compatibility:** `client.playerCompute`,
45
- > `client.playerModel` (owner/grid-confined flexible data + automations), first-class grid
46
- > ownership methods on `client.gameApps`, and app code-admission methods on
47
- > `client.apps` require the 2026-07-20 game-api/management-api player-runtime
48
- > schemas. Player-authorized one-chunk claims and owner release through
49
- > `marketplace.claimGridChunk` / `marketplace.releaseClaimedGrid` require the
50
- > corresponding 2026-07-22 Game API claim schema. Older servers reject only
51
- > those operations during GraphQL validation.
52
- >
53
- > **App-scoped player-count compatibility:** `gameModel.activePlayerCount(appId)`
54
- > and `gameModel.activePlayerCountChanged({ appId }, handlers)` require the
55
- > matching 2026-07-24 Game API schema and an app token scoped to that exact app;
56
- > the subscription also requires `wsUrl`. They count active gameplay sessions,
57
- > not distinct users or actors, and an abandoned session can remain counted for
58
- > approximately 120 seconds. Only `FRESH` snapshots are complete:
59
- > `PARTIAL`/`UNAVAILABLE` are not authoritative zeroes. The subscription is
60
- > best-effort and has no initial event; establish it, query the snapshot,
61
- > deduplicate by `revision`, and requery after reconnects or revision gaps.
62
-
63
- > **v8.21.1 Node realtime routing hotfix:** app-routing responses provide a
64
- > WebSocket base URL. CrowdyJS now normalizes that base to `/graphql` before
65
- > constructing the realtime client; previously Node/bot clients connected to
66
- > the origin root and remained stuck in `connecting` with no notifications.
67
- >
68
- > **Player-code authoring DX:** `playerCompute.setRequires`,
69
- > `marketplace.trustGridAuthor`, self-authored `gridClientMods` fields, and
70
- > version-keyed client artifact fetches require the 2026-07-22 authoring-DX
71
- > migration. The v11 `mountCrowdyStudio` surface is project-first: cloud project
72
- > revisions, target-scoped files, atomic autosave, and explicit draft/live/stop
73
- > orchestration. Its Rust language worker receives source plus the committed
74
- > platform index only—never a credential and never a server connection.
75
- > CrowdyJS 11 requires the matching Crowdy Studio Game API schema; the previous
76
- > package subpath, exports, client property, GraphQL roots, and schema types were
77
- > removed without aliases.
78
-
79
- > **v8.10 inventory authority:** generated craft/barter transactions work on
80
- > existing Model servers. Compute-refereed durable commits require Compute SDK
81
- > `0.1.3` / the `model_invoke` host call.
82
-
83
- ## Standalone builds and schema refresh
84
-
85
- CrowdyJS is a standalone public package: a clean clone builds with
86
- `npm install && npm run build` using the schema artifacts committed to this
87
- repo — no other repositories and no network access required:
88
-
89
- - `schema.gql` — merged Management API + Game API SDL.
90
- - `src/generated/graphql.ts` — generated TypeScript operation types.
91
-
92
- Schema refresh (maintainers) is explicit, from the published SDLs
93
- ([management-api.graphql](https://docs.crowdedkingdoms.com/schema/management-api.graphql),
94
- [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`.
95
12
 
96
- ```bash
97
- npm run schema:sync:prod
98
- npm run codegen
99
- ```
13
+ Start here:
100
14
 
101
- (`npm run schema:sync:paths -- --management <file-or-url> --game <file-or-url>`
102
- accepts explicit sources.) Commit `schema.gql` and `src/generated/graphql.ts`
103
- together whenever the public GraphQL surface changes; `npm run check:schema`
104
- 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.
105
21
 
106
- The browser authoring index follows the same boundary. `npm run build` validates
107
- only the committed internal `src/live-coding/assets/browser-authoring-index.json` and its
108
- generated TypeScript copy; it never discovers or reads a sibling game-api
109
- checkout. Coordinated maintainers compare an explicitly supplied exporter path:
22
+ ## Install
110
23
 
111
24
  ```bash
112
- npm run authoring-index:drift -- --source /path/to/browser-authoring-index.json
113
- # To accept that exact source, then regenerate the committed TypeScript:
114
- npm run authoring-index:drift -- --source /path/to/browser-authoring-index.json --write
115
- npm run authoring-index:generate
25
+ npm install @crowdedkingdoms/crowdyjs
116
26
  ```
117
27
 
118
- The drift command rejects a missing `--source`; cross-repository orchestration,
119
- 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).
120
32
 
121
33
  ## Quick start
122
34
 
@@ -130,7 +42,7 @@ const client = createCrowdyClient({
130
42
  // Game API (world data + UDP proxy)
131
43
  httpUrl: 'https://game.example.com',
132
44
  wsUrl: 'wss://game.example.com',
133
- // Management API (passwordless sign-in, profile)
45
+ // Management API (passwordless sign-in, profile, admin)
134
46
  managementUrl: 'https://management.example.com',
135
47
  tokenStore: new BrowserLocalStorageTokenStore(),
136
48
  realtime: {
@@ -155,390 +67,149 @@ const me = await client.users.me();
155
67
  console.log(me.email);
156
68
  ```
157
69
 
158
- **Gameplay needs an app-scoped token, not the session token.** Mint one per app and
159
- drive the Game API world/UDP surface (including `gameClientBootstrap`) from a
160
- per-game client — see [Overworld portals & app-scoped tokens (v7)](#overworld-portals--app-scoped-tokens-v7).
161
-
162
- If `managementUrl` is omitted, the SDK falls back to `httpUrl` for backwards-compat with the single-endpoint deployment.
163
-
164
- ## Sub-clients at a glance
165
-
166
- **Game-client surface** (end-user, browser-safe):
167
-
168
- | Sub-client | What it does |
169
- |---|---|
170
- | `client.auth` | Passwordless sign-in (magic link, social/OIDC, dev bypass), log out, and linked identities (`myIdentities`, `linkIdentity`/`unlinkIdentity`). |
171
- | `client.users` | `me`, `updateGamertag`, profile reads. |
172
- | `client.session` | Token store, `restore()`, `getToken()`, manual `setToken()`. |
173
- | `client.serverStatus` | `gameClientBootstrap(appId)` — per-app version info, UDP status, spatial limits. |
174
- | `client.chunks`, `client.voxels`, `client.actors`, `client.avatars`, `client.state` | World data reads + writes. |
175
- | `client.host` | Game-host election (`get`, `amIHost`) + actor liveness `heartbeat`. `amIHost` is UI convenience only — authoritative host gating uses `gameModelInvoke`'s `is_host` policy. |
176
- | `client.teleport` | Teleport requests. |
177
- | `client.channels`, `client.teams` | Messaging channels and app-scoped player teams (membership + roles). |
178
- | `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`, …). |
179
- | `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). |
180
- | `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. |
181
- | `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. |
182
- | `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. |
183
- | `client.playerModel` | Player-owned flexible model containers and grid-confined automations (`containers`, `createContainer`, `setProperty`, `automations`, `createAutomation`, …). |
184
- | `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. |
185
- | `client.udp` | UDP proxy subscriptions + spatial mutations (`sendActorUpdate`, `sendVoxelUpdate`, `sendAudioPacket`, `sendTextPacket`, `sendClientEvent`). |
186
- | `client.realtime` | Connection status, manual `connect()` / `disconnect()`, `onStatus()` listener. |
187
- | `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. |
188
- | `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`.
189
72
 
190
- `PlayerCodeBroker` transfers
191
- compiled client artifacts to a platform-owned worker, keeps tokens on the page,
192
- allow-lists host calls, and locally clamps chunk-targeted effects to the owned
193
- grid before the normal SDK path reaches server authorization. Crowdy Studio
194
- hot-swaps only the exact version-keyed artifact returned after a successful
195
- 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).
196
76
 
197
- ## Crowdy Studio
77
+ ## Authentication: session token vs app-scoped tokens
198
78
 
199
- `mountCrowdyStudio` is the project-first SERVER/CLIENT Rust authoring surface. A
200
- project has one cloud revision, target-scoped files, project metadata, separate
201
- server/client module names, and a pairing preference. Full-stack edits autosave
202
- as one optimistic-concurrency write; the UI renders **Saving**, **Saved**,
203
- **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.
204
83
 
205
- The mount fills its host and observes host element resizes, so games can embed
206
- it in draggable docks or split panes without manually relaying window resize
207
- events. Give the host an explicit width and height; Crowdy Studio relayouts
208
- 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.
209
86
 
210
87
  ```ts
211
- import { mountCrowdyStudio } from '@crowdedkingdoms/crowdyjs/crowdy-studio';
212
-
213
- const studio = await mountCrowdyStudio(host, {
214
- projectProvider: game.crowdyStudio,
215
- playerCompute: game.playerCompute,
216
- playerWallet: identity.playerWallet,
217
- appId,
218
- gridId,
219
- grid,
220
- workerUrl: playerCodeGlueWorkerUrl,
221
- onHostCall,
88
+ // Overworld/identity client
89
+ const overworld = createCrowdyClient({
90
+ managementUrl,
91
+ tokenStore: new BrowserLocalStorageTokenStore('crowdyjs:session'),
222
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);
223
96
 
224
- // Stops editor/runtime polling, the Rust worker, and any client broker owned
225
- // by this mount. It does not implicitly disable a deliberately live server.
226
- 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) => { /* ... */ } });
227
107
  ```
228
108
 
229
- Use `new CrowdyStudioController(options)` for a custom/headless presentation. Its
230
- public edits are file operations (`addFile`, `renameFile`, `deleteFile`,
231
- `updateFile`) and project settings—there is no session-only source blob, fixed
232
- module name, or template JSON API. The deploy conversion to the Game API's
233
- legacy `sourceFilesJson` input happens only inside the controller's
234
- `playerCompute.deploy` call.
235
-
236
- The built-in UI provides a project switcher/new-project wizard
237
- (server/client/full-stack), target explorer, personal library/common files,
238
- tabs, settings, Problems/Build/Logs/Runs/Invoke panels, and explicit **Test
239
- draft**, **Deploy live**, and **Stop project** actions. Full-stack deployment is
240
- ordered: save once → compile CLIENT → compile SERVER → set/clear the pairing
241
- requirement only after both compiles → enable SERVER → hot-swap the exact
242
- version-keyed CLIENT artifact. Partial compile failures never write a new
243
- requirement. Stop always attempts both server disable and client/poll cleanup
244
- and reports each failure.
245
-
246
- Monaco uses target-prefixed model URIs (`.../server/Cargo.toml` and
247
- `.../client/Cargo.toml`) and opens every loaded project/library/common Rust file
248
- in one bounded local worker workspace. Completion, hover, symbols, and
249
- definition therefore work across loaded files; lifecycle snippets and hover
250
- notes distinguish SERVER platform execution from CLIENT broker host calls.
251
- Local tree-sitter markers are labeled advisory. Platform rustc output is parsed
252
- separately into authoritative path/line/column markers and remains visible in
253
- Build.
254
-
255
- The Rust worker receives source files and the strict embedded platform index
256
- only. It receives no credential, opens no socket/fetch path, and never falls
257
- back to a server language service. If Monaco, Worker, a WASM asset, or a custom
258
- platform index fails, the same Crowdy Studio mount keeps project/target tabs and
259
- uses one file-aware textarea—never a raw JSON blob.
260
-
261
- Modern bundlers must preserve module workers and package `.wasm` assets. Custom
262
- pipelines can pass `languageWorkerFactory` and `editorWorkerFactory`. The
263
- embedded index remains the byte-identical game export (`schemaVersion: 2`, Rust
264
- 1.97.1, SDK 0.1.5, ABI 0, 725 symbols, content hash
265
- `3f5f39d4…18ffb`).
266
-
267
- ### Embed Crowdy Studio in your game (v12.1)
268
-
269
- Most games should not hand-roll the window chrome around `mountCrowdyStudio`.
270
- The embed kit ships the proven Blocks with Friends shell as reusable, game
271
- -agnostic components (exported from `@crowdedkingdoms/crowdyjs/crowdy-studio`):
272
-
273
- - **`createCrowdyStudioEmbed(options)` / `CrowdyStudioEmbed`** — a responsive
274
- panel that renders a resizable right dock on desktop and a focus-trapped
275
- fullscreen modal below 1000 px. It owns Escape/close-key semantics, the
276
- compact header (title, grid pill, on-demand Context drawer, Close), loading
277
- and retry chrome, and assembles the `mountCrowdyStudio` call — including the
278
- agent block when the client exposes `crowdyStudioAgent` and the game passes a
279
- `playerHost`.
280
- - **`CrowdyStudioEmbedDock`** — the accessible game/studio splitter with
281
- persisted width (`ck:crowdy-studio:embed:dock-width:v1`), arrow-key/Home/End
282
- resize, and ARIA value text.
283
- - **`CrowdyStudioTextHud`** — the text-only presentation sink for CLIENT-mod
284
- `hud_set` payloads plus the drawer HUD preview. Untrusted payloads render as
285
- text, never HTML.
286
- - **`ensureCrowdyStudioEmbedStyles()`** — injected `ck-crowdy-studio-embed-*`
287
- styles; games restyle by overriding classes. While docked, the panel sets
288
- `--ck-game-right-inset` on `document.body` so game HUDs can keep clear of
289
- the dock.
109
+ Browser cross-origin handoff is OAuth2 Authorization Code + PKCE — the
110
+ verifier never leaves the game origin:
290
111
 
291
112
  ```ts
292
- import { createCrowdyStudioEmbed } from '@crowdedkingdoms/crowdyjs/crowdy-studio';
293
- // Self-starting glue worker for CLIENT mods (Vite shown; any bundler that
294
- // packages module workers works):
295
- 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
+ }));
296
118
 
297
- const studio = createCrowdyStudioEmbed({
298
- client: game, // CrowdyClient: crowdyStudio, playerCompute, playerWallet, crowdyStudioAgent
299
- appId,
300
- gameName: 'My Game',
301
- suppressGameplayInput: () => pauseInput(),
302
- onLayoutChange: () => resizeCanvas(),
303
- });
119
+ // Overworld /authorize page (holds the session token):
120
+ location.assign(await overworld.portal.handleAuthorizeRequest());
304
121
 
305
- // Per open (for example after the player claims a grid):
306
- studio.toggle({
307
- gridId,
308
- targetPermissions: {
309
- SERVER: { canWrite: true, canRun: true },
310
- CLIENT: { canWrite: false, canRun: false }, // SERVER-only embeds omit workerUrl
311
- },
312
- });
122
+ // Game origin, on callback boot: exchange code+verifier -> app token (stored).
123
+ const entered = await game.portal.completeEntry();
313
124
  ```
314
125
 
315
- CLIENT-mod embeds additionally pass `grid`, `workerUrl`, `onHostCall`, an
316
- optional `hud` sink, and a `playerHost` adapter. Note that CLIENT mods outside
317
- Blocks with Friends remain gated by program decision D13; new games should
318
- start SERVER-only exactly as above.
126
+ Game-to-game routes through the Overworld for a fresh per-game token.
319
127
 
320
- The visible agent safety chrome is exported from
321
- `@crowdedkingdoms/crowdyjs/player-host`: **`PlayerControlGate`** (capture-phase
322
- human preemption seam with offline Stop) and **`AgentControlBanner`** (the
323
- always-visible-on-control Pause/Stop region, self-injected
324
- `ck-agent-control-*` styles).
128
+ Notes:
325
129
 
326
- ### 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).
327
140
 
328
- `CrowdyStudioAPI` implements the transport-neutral
329
- `CrowdyStudioProjectProvider`; generated GraphQL types remain inside the adapter.
330
- 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).
331
143
 
332
- - `crowdyStudioProjects(appId)` / `crowdyStudioProject(appId, projectId)`
333
- - `crowdyStudioProjectCreate(input)`
334
- - `crowdyStudioProjectSave(input)` for one atomic metadata/file delta
335
- - `crowdyStudioLibraryFiles(appId)` / `crowdyStudioLibrarySave(input)`
336
- - `crowdyStudioCommonFiles(appId)`
337
- - `crowdyStudioProjectImportFile(input)` for copy-by-value imports
144
+ ### Token refresh during gameplay
338
145
 
339
- The adapter maps the API's `PAIRED | INDEPENDENT | SERVER_ONLY | CLIENT_ONLY`
340
- presentation enum to Crowdy Studio's runtime-oriented project kind and
341
- `REQUIRED | OPTIONAL | NONE` setting. A stale expected `revision` arrives as
342
- `CONFLICT` with `CROWDY_STUDIO_REVISION_CONFLICT` in the message and becomes a
343
- `CrowdyStudioRevisionConflictError` with the latest cloud project when that
344
- 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.
345
153
 
346
- ### Agent SDK, transport, and integrated dock (v12)
154
+ `game.portal.refresh()` remains available for clients with no active UDP
155
+ lifecycle to preserve.
347
156
 
348
- The v12 agent API remains provider-neutral, while CrowdyJS now ships generated
349
- operations plus the production `CrowdyAgentGraphQLTransport` exposed as
350
- `client.crowdyStudioAgent`:
351
-
352
- ```ts
353
- import {
354
- CROWDY_AGENT_TOOL_REGISTRY_V1,
355
- CrowdyStudioAgentController,
356
- } from '@crowdedkingdoms/crowdyjs/agent';
357
-
358
- const agent = new CrowdyStudioAgentController({
359
- transport: game.crowdyStudioAgent,
360
- createSession: {
361
- appId,
362
- projectId,
363
- gridId,
364
- mode: 'BUILD',
365
- providerDataConsent: true,
366
- idempotencyKey: crypto.randomUUID(),
367
- },
368
- beforeAgentWork: () => studio.controller.prepareForAgentWork(),
369
- });
370
- await agent.initialize(); // attach epoch → durable replay/gap fill → live tail
371
- ```
372
-
373
- `CROWDY_AGENT_TOOL_REGISTRY_V1` is an immutable, digest-pinned registry of the
374
- minimum Studio/project/workspace/library/template/diagnostics/runtime and
375
- generic game tools. Every descriptor has exact logical and provider wire names,
376
- semantic version, bounded input/output JSON schemas,
377
- `additionalProperties:false`, executor, mode, risk/effect/reversibility,
378
- scope, approval, idempotency, timeout, and redaction metadata. Input schemas
379
- reject caller-supplied identity, epoch, lease, approval, endpoint, token, and
380
- other authority fields. `CrowdyAgentToolRegistry.fromWireName` is exact and
381
- case-sensitive; browser dispatch is execute-once by `toolCallId`, and ambiguous
382
- effects become `OUTCOME_UNKNOWN` instead of being repeated.
383
-
384
- `CrowdyStudioAgentTransportV1` owns no credentials and exposes only typed
385
- durable orchestration operations: session/history/descriptor/budget queries;
386
- create/attach/mode/event-ack/message/approval/tool-result/lease/pause/resume/
387
- cancel/close mutations; and the ordered event subscription. A generated adapter
388
- maps these methods to the operation names in
389
- `CROWDY_AGENT_GRAPHQL_OPERATIONS_V1`:
390
-
391
- - `crowdyStudioAgentSession`, `crowdyStudioAgentSessions`,
392
- `crowdyStudioAgentHistory`, `crowdyStudioAgentToolDescriptors`, and
393
- `crowdyStudioAgentBudget`;
394
- - `crowdyStudioAgentCreateSession`, `AttachClient`, `SetMode`,
395
- `AcknowledgeEvents`, `Heartbeat`, `SendMessage`, `ApproveTool`, `RejectTool`,
396
- `ToolResult`, `GrantLease`, `RevokeLease`, `Pause`, `Resume`, `CancelRun`,
397
- and `CloseSession` (all with the `crowdyStudioAgent` prefix);
398
- - `crowdyStudioAgentEvents(sessionId, afterSeq, clientEpoch)`.
399
-
400
- The controller applies only contiguous decimal-string sequences, deduplicates
401
- event IDs, fills gaps from durable history, acknowledges the highest
402
- contiguous cursor, fences old attach epochs, and explicitly reconnects without
403
- resuming a run or Play lease. Approval methods use the exact displayed argument
404
- hash. Human edit/game input calls local preemption before best-effort transport
405
- cleanup. Attach uses a stable `clientInstanceId` and the server
406
- `replayAfterSeq`; public message input maps to Game API `content`, cancellation
407
- always sends an explicit run id, and PLAY sends a two-second heartbeat only
408
- while attached, active, and visible. Heartbeat, kill, and stale-epoch failures
409
- immediately clear local authority.
410
-
411
- Mode changes adopt the complete server-repinned session (registry digest,
412
- provider/app policy revisions, and context version) before loading its new
413
- effective descriptors. The Studio mount resolves the selected saved project
414
- after `CrowdyStudioController.initialize()`, overrides caller guesses when
415
- creating a BUILD session, and rejects an existing session bound to another
416
- project. Because v1 has no set-project mutation, switching projects fences the
417
- agent and requires a new session/remount.
418
-
419
- An active BUILD workspace lease is renewed every ten seconds through the same
420
- authenticated heartbeat; the server returns its renewed 30-second expiry.
421
- Human edits, project/context changes, lease revocation, disconnect, and destroy
422
- stop renewal and abort browser handlers. Run events retain their stable
423
- `code`, `reason`, and typed safe `error`.
424
-
425
- The committed descriptor digest fixture is checked on every build against the
426
- canonical registry. Coordinated refreshes first run
427
- `npm run agent-descriptors:drift -- --source <game-api-fixture>`; builds then
428
- recompute the full and canonical 28-tool Game API subset digest (14 mandatory
429
- game plus 14 Studio/diagnostic/runtime tools) so the fixture cannot silently
430
- diverge.
431
-
432
- Mount the responsive agent dock by adding `agent`; omit it to retain the v11
433
- manual UI unchanged:
434
-
435
- ```ts
436
- const studio = await mountCrowdyStudio(host, {
437
- projectProvider: game.crowdyStudio,
438
- playerCompute: game.playerCompute,
439
- appId,
440
- gridId,
441
- agent: {
442
- transport: game.crowdyStudioAgent,
443
- sessionId,
444
- playerHost: bwfPlayerHostAdapter,
445
- },
446
- });
447
-
448
- studio.agent?.sendMessage('Explain the current diagnostics');
449
- studio.controlLeaseManager?.preempt('HUMAN_INPUT');
450
- ```
451
-
452
- The dock provides human-owned Ask/Build/Play selection, chat streaming,
453
- plan/tool status, exact approval hashes, diff/checkpoint cards, budget, visible
454
- lease scope/expiry, Pause/Resume/Stop, and checkpoint-restore requests. It uses
455
- text nodes for all untrusted content, ARIA status/log/group labels, visible
456
- focus, keyboard submission, and a container-query layout that moves the agent
457
- dock below narrow embeds.
458
-
459
- The headless `CrowdyStudioController` now also exposes:
157
+ ## Game-loop lifecycle
460
158
 
461
- - `prepareForAgentWork()` to flush autosave and fail closed on conflict/offline;
462
- - `applyAtomicPatch()` and `synchronizeProject()` for all-or-none multi-file
463
- updates, human-edit preemption, and Monaco synchronization;
464
- - `refreshCheckpoints()` / `restoreCheckpoint()` through the optional
465
- transport-neutral `CrowdyStudioSynchronizationProvider` (the provider is
466
- required for agent writes/restores and must make checkpoint + revision
467
- changes durable and atomic);
468
- - typed draft/live results plus `runtimeSync` (`RUNNING_SAVED`,
469
- `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.
470
174
 
471
- ### PlayerHostAdapter and Blocks with Friends integration
175
+ ## Sub-clients at a glance
472
176
 
473
- `@crowdedkingdoms/crowdyjs/player-host` exports
474
- `PlayerHostAdapterV1`, capability/observation/command/result types and schemas,
475
- `AgentControlLeaseManager`, and `createPlayerHostAgentTools`. A game adapter
476
- implements only:
177
+ **Game-client surface** (end-user, browser-safe):
477
178
 
478
- ```ts
479
- interface PlayerHostAdapterV1 {
480
- readonly contractVersion: 'crowdy.player-host/1';
481
- capabilities(): Promise<PlayerHostCapabilitiesV1>;
482
- observe(request: ObserveRequestV1): Promise<GameObservationV1>;
483
- dispatch(command: GameCommandV1, gate: ValidatedGateV1):
484
- Promise<GameCommandResultV1>;
485
- clearAgentIntent(reason: CrowdyAgentPreemptionReason): void;
486
- }
487
- ```
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). |
488
205
 
489
- For Blocks with Friends, implement this interface over the same typed intent
490
- methods used by `PlayerController`, `ActionService`, `InventoryService`,
491
- `FishingService`, `RideService`, and existing referee/model calls. Construct
492
- `createPlayerHostAgentTools(adapter)` and pass `adapter` as the mount's
493
- `playerHost`. The returned lease manager enforces TTL, scopes, client epoch,
494
- host/controlled-entity revision, observation freshness, modal/death state,
495
- per-command rate limits, approvals, and tool-call dedup. Call
496
- `leaseManager.preempt('HUMAN_INPUT')` synchronously on key/mouse/touch input,
497
- `ESCAPE` on Escape, and the corresponding reason on death, target/context
498
- change, disconnect, or Stop. `clearAgentIntent` must immediately zero movement,
499
- look, and pending action intent. BWF does not integrate through raw CrowdyJS,
500
- DOM input, UDP packets, `PlayerCodeBroker`, or client-mod host calls.
501
-
502
- BWF should pass only `game.crowdyStudioAgent` and its
503
- `BwfPlayerHostAdapter` to the Studio mount; the mount derives the selected
504
- project id. Its adapter must implement the exact 14 mandatory game surfaces,
505
- classify combat as conditional approval, return typed failed/denied/unknown
506
- outcomes, and make `clearAgentIntent` synchronously cancel shared movement and
507
- action intents when the dispatcher aborts.
508
-
509
- The reconciled Game API development pilot advertises the exact 28-tool
510
- follow-up subset, including all 14 mandatory game descriptors. CrowdyJS routes
511
- all game tools through
512
- `PlayerHostAdapterV1`, the scoped lease gate, conditional combat approval,
513
- safety stop, heartbeat fencing, and typed result continuation. BWF Play now
514
- remains blocked only on the concrete BWF adapter/shared-intent wiring and its
515
- host/app policy configuration.
516
-
517
- The same browser dispatcher routes backend-advertised `runtime.test_draft`,
518
- `runtime.deploy_live`, `runtime.invoke`, and `runtime.stop` through the
519
- headless Studio controller. Draft testing is routine BUILD work; live deploy
520
- and LIVE invoke require the exact approval grant; stop remains an idempotent
521
- safety action. Inner game `FAILED`, `DENIED`, or `OUTCOME_UNKNOWN` results are
522
- promoted to matching outer terminal tool results and are never reported as
523
- successful.
524
-
525
- Runtime execution is plan-bound: requested targets must exactly equal the
526
- selected project’s authoritative target set, so a full-stack project cannot run
527
- CLIENT after SERVER-only authorization. Live deploy additionally rechecks the
528
- saved revision, canonical project content hash (including module names/files),
529
- and pairing preference after autosave. Invoke checks the exact running
530
- DRAFT/LIVE environment and export; stop always stops the complete selected
531
- project as a safety action rather than interpreting a partial target.
532
-
533
- | `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. |
534
- | `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. |
535
-
536
- **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):
537
208
 
538
209
  | Sub-client | What it does |
539
210
  |---|---|
540
211
  | `client.organizations` | Orgs, members, RBAC roles, org API tokens. |
541
- | `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. |
542
213
  | `client.appAccess` | Access tiers + per-user grants. |
543
214
  | `client.billing` | Org wallet + per-app spend budgets. |
544
215
  | `client.payments` | Payment checkouts (wallet top-ups, plan purchases). |
@@ -546,36 +217,31 @@ project as a safety action rather than interpreting a partial target.
546
217
  | `client.usage` | Replication + GraphQL usage reporting. |
547
218
  | `client.sharedEnvironment` | Publish to shared, runtime gating, spend caps, auto-billing. |
548
219
  | `client.gameApps` | App grids (`createGrid` / `deleteGrid`), first-class grid ownership (`ownership` / `assignOwnership` / `transferOwnership`), and grid runtime-permission administration. |
549
- | `client.apps` | App registry/routing plus player-code admission mode and code/author/org allow-list administration. |
550
220
 
551
221
  **Operator surface** (platform operations; requires `is_operator`):
552
222
 
553
223
  | Sub-client | What it does |
554
224
  |---|---|
555
- | `client.operator` | Platform compute ceilings (v13: infra operations moved to the separate infra-control-plane service). |
225
+ | `client.operator` | Platform compute ceilings (`computePlatformCeilings`, `setComputePlatformCeilings`). Infrastructure operations live in the separate infra-control-plane service, not this SDK. |
556
226
 
557
- 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.
558
-
559
- ## Game-loop lifecycle
560
-
561
- 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).
562
- 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.
563
- 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).
564
- 4. Join a chunk by sending an initial actor update.
565
- 5. Send actor, voxel, text, audio, and client-event updates through `game.udp` or the higher-level `game.world(appId)` helpers.
566
- 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.
567
- 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.
568
231
 
569
- `refreshGameplayToken()` deliberately stops on the first failed stage. If the
570
- old proxy cannot confirm disconnect, no refresh is attempted and the old token
571
- remains active. If refresh fails, the old token is still retained (although its
572
- proxy was closed and may be reopened). If the new proxy connect fails, the
573
- fresh token remains stored; surface the error and retry `game.udp.connect()`
574
- 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.
575
239
 
576
240
  ## Per-app routing
577
241
 
578
- 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:
579
245
 
580
246
  ```graphql
581
247
  query AppForRouting($appId: BigInt!) {
@@ -588,23 +254,21 @@ query AppForRouting($appId: BigInt!) {
588
254
  }
589
255
  ```
590
256
 
591
- `gameApiUrl` is populated for **both** dedicated (`splitMode`) and shared
592
- (`deploymentTarget: "shared"`) apps. When it's set, build a **second**
593
- `CrowdyClient` with `httpUrl: gameApiUrl` (and the matching `wsUrl`) holding that
594
- app's **app-scoped token** (`identity.portal.mintAppToken(appId)` do **not**
595
- reuse the identity client's session token store), then drive gameplay through that
596
- client. In practice `mintAppToken` already returns `gameApiUrl` / `gameApiWsUrl`,
597
- so you rarely need this separate routing query. Apps with no `gameApiUrl` keep
598
- 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).
599
263
 
600
264
  ## Realtime notifications
601
265
 
602
- `subscribe` takes the handlers **and a required `appId`** (second argument). The
603
- Game API scopes the realtime session to that app and rejects an app-agnostic
604
- subscription with a `RealtimeConnectionEvent` (`code: 'APP_ID_REQUIRED'`). It also
605
- rejects an identity session token (`APP_TOKEN_REQUIRED`) or a token scoped to a
606
- different app (`APP_SCOPE_MISMATCH`). Run one client per app (each holding that
607
- 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.
608
272
 
609
273
  ```ts
610
274
  const appId = '1';
@@ -623,6 +287,10 @@ const unsubscribe = client.udp.subscribe(
623
287
  // A direct actor-to-actor message addressed to you.
624
288
  console.log(event.uuid, event.payload); // payload is base64
625
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
+ },
626
294
  genericError: (event) => {
627
295
  console.warn(event.sequenceNumber, event.errorCode);
628
296
  },
@@ -647,7 +315,13 @@ client.realtime.onStatus((status) => {
647
315
  unsubscribe();
648
316
  ```
649
317
 
650
- 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.
651
325
 
652
326
  ## Spatial sends
653
327
 
@@ -664,15 +338,19 @@ const response = await client.udp.sendActorUpdateAndWait({
664
338
  console.log(response.__typename, response.sequenceNumber);
665
339
  ```
666
340
 
667
- 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.
668
347
 
669
348
  ### Actor-to-actor messages
670
349
 
671
350
  ```ts
672
351
  // Delivered only to the actor whose UUID matches `targetUuid`; you must know
673
- // that actor's current chunk. Fire-and-forget — the sender gets no echo, so
674
- // there is no `AndWait` variant. The target receives a
675
- // `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.
676
354
  await client.udp.sendSingleActorMessage({
677
355
  appId: '1',
678
356
  chunk: { x: '7', y: '1', z: '2' }, // the TARGET actor's chunk
@@ -699,23 +377,24 @@ await actor.sendToActor(
699
377
  );
700
378
  ```
701
379
 
702
- 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.
703
383
 
704
384
  ## World Stores
705
385
 
706
386
  The core client is a thin transport; the **World Stores** layer
707
- (`@crowdedkingdoms/crowdyjs/stores`, 8.4+) adds the source-of-truth data
708
- structures every game otherwise hand-writes: actor registries, chunk/voxel
709
- caches, error attribution, message inboxes, host tracking, and typed
710
- durable-state wrappers — all driven by ONE shared `udpNotifications`
711
- 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.
712
391
 
713
392
  ```ts
714
393
  import {
715
394
  createWorldSession, structCodec, f32, u8, jsonCodec, workerTicker,
716
395
  } from '@crowdedkingdoms/crowdyjs/stores';
717
396
 
718
- // Describe your replication state ONCE (48-byte binary layouts, declaratively):
397
+ // Describe your replication state ONCE (binary layouts, declaratively):
719
398
  const poseCodec = structCodec({
720
399
  x: f32(), y: f32(), z: f32(), yaw: f32(),
721
400
  flags: u8(), held: u8(),
@@ -750,6 +429,13 @@ session.errors.onError((e) => console.warn(e.errorCode, e.send?.kind));
750
429
  session.dispose();
751
430
  ```
752
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
+
753
439
  Every store is **opt-in twice over**: only configured stores are constructed
754
440
  (and only they exist on the session's TYPE — `session.host` without
755
441
  `host: ...` in the config is a compile error), and the layer lives behind the
@@ -795,8 +481,7 @@ const result = await kit.objects.open(doorId, { keyId });
795
481
  if (!result.success) console.warn('locked:', result.errorMessage);
796
482
  ```
797
483
 
798
- Land sale closes the permission loop end to end (requires game-api v0.13.11+ for
799
- effects, v0.13.12+ for the chunk-permission reads):
484
+ Land sale closes the permission loop end to end:
800
485
 
801
486
  ```ts
802
487
  // Studio: sell a plot over a grid; doors on it honor the purchase automatically.
@@ -816,28 +501,138 @@ selector has `candidatePermissionWhere: [{ userFrom: { property: 'owner_user_id'
816
501
  op: 'lacks', key: 'access', grid: { property: 'grid_id' } }]` reacts only to
817
502
  intruders.
818
503
 
819
- As of 8.3.0 the kit covers the common genre staples end to end — every layer
820
- 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:
821
506
 
822
507
  | Layer | Builder → helper | Highlights |
823
508
  | --- | --- | --- |
824
- | 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`) |
825
514
  | Progression | `progressionBlueprint` → `kit.progression` | xp/levels via the `fn:` curve helper, skill prerequisite chains, achievements, host-gated rating |
826
515
  | Loot | `lootBlueprint` → `kit.loot` | weighted tables unrolled into seed-driven expressions, atomic single-claim, event-triggered drops |
827
- | Quests | `questsBlueprint` → `kit.quests` | event-automation progress, atomic claim into stack+wallet, cron daily resets |
828
- | 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 |
829
518
  | Matches | `matchesBlueprint` → `kit.matches` | session lobbies/rounds/turns/scores, per-match channel + `onMatchChanged` (notify-to-pull) |
830
519
  | Decks | `decksBlueprint` → `kit.decks` | hidden hands via owner-visibility `card_id`, shuffle-by-position automation |
831
- | 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 |
832
521
  | Social | `guildBlueprint` → `kit.social` | parties/guilds/chat over teams+channels, grid territory grants, guild hall + bank composite |
833
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 |
834
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 })`.
835
541
 
836
542
  See the docs guides [Modeling game concepts](https://docs.crowdedkingdoms.com/game-api/modeling-game-concepts)
837
543
  (the underlying model + genre map) and [Game Kit](https://docs.crowdedkingdoms.com/crowdyjs/game-kit)
838
544
  (the SDK surface + the simulation-tier / notify-to-pull / timer / hidden-info
839
545
  / anti-cheat patterns).
840
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
+
841
636
  ## Errors
842
637
 
843
638
  Transport and protocol failures throw structured error classes:
@@ -849,11 +644,18 @@ Transport and protocol failures throw structured error classes:
849
644
  - `CrowdyRealtimeError` — realtime subscription couldn't be established or was dropped.
850
645
  - `CrowdyProtocolError` — server response failed schema validation.
851
646
 
852
- 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.
853
651
 
854
652
  ## Idempotent retries
855
653
 
856
- 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.
857
659
 
858
660
  ```ts
859
661
  const key = crypto.randomUUID();
@@ -864,102 +666,56 @@ await client.teams.leave(groupId, key); // leaveTeam
864
666
  await client.voxels.rollback({ ...input, idempotencyKey: key }); // input field
865
667
  ```
866
668
 
867
- 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).
868
-
869
- ## Auth notes
669
+ The key parameter is optional and trailing, so it's safe to omit.
870
670
 
871
- - Use `client.auth.setToken(token)` if you need to seed a token externally (e.g. when restoring auth from a non-default storage).
872
- - `client.session.restore()` reads from the configured `tokenStore`. `BrowserLocalStorageTokenStore` is provided; bring your own for SSR or Node usage.
873
- - 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)).
874
-
875
- ## Overworld portals & app-scoped tokens (v7)
876
-
877
- As of v7 gameplay requires an **app-scoped token**, not the session token.
878
- Passwordless sign-in returns an **identity session token** (Management API only —
879
- account, studio admin, and minting); each game is entered with a short-lived token
880
- confined to that one app, so a game stack never receives the player's full session.
671
+ ## Low-level GraphQL access
881
672
 
882
- Use two clients: an Overworld/identity client (session token) and a per-game
883
- 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:
884
676
 
885
677
  ```ts
886
- // Overworld/identity client
887
- const overworld = createCrowdyClient({ managementUrl, tokenStore: new BrowserLocalStorageTokenStore('crowdyjs:session') });
888
- // Passwordless sign-in (magic link, social/OIDC, or dev bypass) yields the session token.
889
- await overworld.auth.requestLoginLink({ email, redirectUri });
890
- await overworld.auth.completeLoginLink(tokenFromLink);
678
+ import { VersionInfoDocument } from '@crowdedkingdoms/crowdyjs/generated';
891
679
 
892
- // Native / same-origin: mint directly, then build a game client.
893
- const t = await overworld.portal.mintAppToken(appId);
894
- const game = createCrowdyClient({ httpUrl: t.gameApiUrl!, wsUrl: t.gameApiWsUrl!, managementUrl,
895
- tokenStore: new BrowserLocalStorageTokenStore('crowdyjs:app:' + appId) });
896
- game.setToken(t.token);
897
- game.world(appId).subscribe({ actorUpdate: (n) => { /* ... */ } });
680
+ const data = await client.graphql.request(VersionInfoDocument);
898
681
  ```
899
682
 
900
- Browser cross-origin handoff is OAuth2 Authorization Code + PKCE — the verifier
901
- never leaves the game origin:
902
-
903
- ```ts
904
- // Game origin, on "enter": redirect to the Overworld authorize page.
905
- location.assign(await game.portal.beginEntry({
906
- appId, authorizeUrl: 'https://overworld.example.com/authorize',
907
- redirectUri: location.origin + location.pathname,
908
- }));
683
+ For any brand-new server field not yet wrapped, `client.graphql.request(...)` /
684
+ `client.management.request(...)` always works.
909
685
 
910
- // Overworld /authorize page (holds the session token):
911
- location.assign(await overworld.portal.handleAuthorizeRequest());
686
+ ## Maintainers: schema artifacts and fixtures
912
687
 
913
- // Game origin, on callback boot: exchange code+verifier -> app token (stored).
914
- const entered = await game.portal.completeEntry();
915
- // Keep active UDP gameplay running past expiry without orphaning the old proxy:
916
- await game.refreshGameplayToken();
917
- ```
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:
918
691
 
919
- `portal.refresh()` remains available for clients with no active UDP lifecycle.
920
- Once a proxy or realtime gameplay session is active, prefer
921
- `refreshGameplayToken()` so the old Bearer closes its proxy before the token is
922
- revoked.
923
-
924
- Game-to-game routes through the Overworld for a fresh per-game token. New
925
- realtime codes: `APP_TOKEN_REQUIRED`, `APP_SCOPE_MISMATCH`; new `UdpErrorCode`:
926
- `TOKEN_EXPIRED`. See [MIGRATION.md](MIGRATION.md) for the full v7 breaking guide.
927
-
928
- ## Surface scope & security
929
-
930
- As of v6 (completed in v6.1), CrowdyJS wraps the **full** management-api + game-api
931
- public surface, not just the game-client subset — every non-deprecated public root
932
- field has a typed method, with Relay `*Connection` cursor-pagination variants
933
- alongside the legacy offset lists. The surfaces are namespaced by audience:
934
-
935
- - **Game-client** (`client.auth`, `client.users`, `client.udp`, `client.world(...)`,
936
- `client.chunks`/`voxels`/`actors`/`avatars`/`state`/`teleport`/`channels`/`teams`/
937
- `gameModel`/`host`) — safe for untrusted browser clients with an end-user token.
938
- - **Studio-admin** (`client.admin.*` — also reachable at the top level, e.g.
939
- `client.billing`) — privileged organization/app administration. Drive these from a
940
- **studio backend** with an org-scoped or admin token, **not** from an untrusted
941
- browser; the server still enforces the relevant org/app permission on every call.
942
- - **Operator** (`client.operator`) — platform-policy operations (compute
943
- ceilings) that require `users.is_operator`. For internal operator tooling
944
- only. Infrastructure operations live in the separate infra-control-plane
945
- service as of v13, not this SDK.
946
-
947
- The SDK never relaxes server-side authorization — exposing an operation here just
948
- gives you a typed wrapper; the caller still needs the right token and permission. For
949
- any brand-new server field not yet wrapped, the low-level escape hatch
950
- (`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.
951
694
 
952
- ## 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)):
953
698
 
954
- 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
+ ```
955
703
 
956
- ```ts
957
- 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.
958
708
 
959
- const data = await client.graphql.request(VersionInfoDocument);
960
- ```
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:
961
712
 
962
- 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.
963
719
 
964
720
  ## Migration
965
721