@genex-ai/cli-demo 1.36.5-dev.858 → 1.36.6-dev.861

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "1.36.5-dev.858",
3
+ "version": "1.36.6-dev.861",
4
4
  "description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,8 @@
12
12
  // - "unlocked" → a bottom-center pill, "Click to aim — Esc pauses". Also shown on
13
13
  // the "needs-gesture" re-emit (a re-lock that needs a fresh click).
14
14
  // - "unavailable" → the same pill, "Drag to look" (lock was refused, e.g. a
15
- // third-party embed without allow="pointer-lock" — drag still works).
15
+ // sandboxed embed without the allow-pointer-lock sandbox token —
16
+ // drag still works).
16
17
  // - "off" / "paused" → nothing (aim disabled, or a menu owns the cursor).
17
18
  //
18
19
  // Games that want a custom HUD can skip this and read `onAimChange` directly.
@@ -116,7 +116,7 @@ export type FollowCameraAimState =
116
116
  | "unlocked" // aim available, waiting for a click
117
117
  | "locked" // pointer locked, mouse drives the view
118
118
  | "paused" // suspended by the game (menu open / driving) — clicks don't re-lock
119
- | "unavailable"; // lock permanently rejected (e.g. an iframe without allow="pointer-lock") → drag-orbit fallback
119
+ | "unavailable"; // lock permanently rejected (e.g. a sandboxed embed without the allow-pointer-lock sandbox token) → drag-orbit fallback
120
120
 
121
121
  /**
122
122
  * Payload for {@link FollowCameraOptions.onAimChange}.
@@ -843,8 +843,9 @@ export class FollowCamera {
843
843
  * - No usable user gesture (a resume outside a click, or Chrome's post-Esc
844
844
  * cooldown): skip the doomed request, keep the "click to aim" cue up (a
845
845
  * same-state "needs-gesture" re-emit), and arm a keydown retry.
846
- * - SecurityError (permission denied — e.g. an iframe without
847
- * allow="pointer-lock"): permanent, fall back to drag-orbit ("unavailable").
846
+ * - SecurityError (permission denied — e.g. a sandboxed embed without the
847
+ * allow-pointer-lock sandbox token): permanent, fall back to drag-orbit
848
+ * ("unavailable").
848
849
  * - NotSupportedError from `unadjustedMovement`: retry the plain request (still
849
850
  * inside the transient-activation window, so no new gesture is needed).
850
851
  * - Any other rejection: transient — treat like the missing-gesture case.
@@ -78,10 +78,15 @@ const screen = new THREE.Mesh(
78
78
  );
79
79
  scene.add(screen);
80
80
 
81
- // start playback on a user gesture (never swallow the rejection):
82
- window.addEventListener("keydown", () => {
81
+ // start playback on the first input (never swallow the rejection). Both
82
+ // events: a click or tap is the first input a game usually gets, not a key.
83
+ const start = (): void => {
84
+ window.removeEventListener("pointerdown", start);
85
+ window.removeEventListener("keydown", start);
83
86
  video.play().catch((e) => console.warn("video play", e));
84
- }, { once: true });
87
+ };
88
+ window.addEventListener("pointerdown", start);
89
+ window.addEventListener("keydown", start);
85
90
  ```
86
91
 
87
92
  One `VideoTexture` can feed **many** surfaces — a bank of monitors, N sprays. Cost is
@@ -165,6 +165,7 @@ vendored code from memory of another engine.
165
165
  | players talking to each other by TEXT — chat, room chat, "let them type to each other" (`room.chat` + `npx genex controller chat`) | `$genex-threejs-multiplayer` |
166
166
  | players TALKING — voice chat, mic, "hear each other", proximity/positional voice (`room.voice` + `npx genex controller voice`; party-sized, max 6 per room — say that number out loud before building the feature) | `$genex-threejs-multiplayer` |
167
167
  | player identity, sign-in, guests, saves/progress, per-player state, shared persistent world, leaderboards—mandatory for every game | `$genex-threejs-embed-auth` |
168
+ | anything the game asks the BROWSER or the DEVICE for: share, an invite link, copy to clipboard, open a link, save or download a file, fullscreen, tilt or gyro, vibration, microphone / camera / location, keep the screen on, its own `?params` or `#hash`, a browser dialog (`alert` / `confirm` / `prompt`). What works differs between the game's Genex page, the Genex app and the game's own link, usually with no error — find the row before the first call | `$genex-threejs-embed-auth` (`references/shells.md`) |
168
169
  | the player EXPLICITLY asks to make an asset or publish something from their game as an asset for the free library | `$genex-asset-author` (`genex asset new`) — never offer this unprompted; no game duty routes here |
169
170
 
170
171
  Request-shape rules, working modes and the protected-lane handoffs stay in the
@@ -140,6 +140,14 @@ handler, before any `await`.** The approval popup is reserved synchronously off
140
140
  the gesture; an `await` in front of it loses the gesture and nothing opens.
141
141
  `generate({ grantId })` needs no gesture at all — that is what a grant buys.
142
142
 
143
+ **Two shells have no approval surface, and say so before anything is asked or
144
+ charged.** Inside the Genex app every call on this lane resolves
145
+ `{ status: 'failed', error: 'native_unsupported' }`; on a dev server in local
146
+ test mode it is `local_test_unsupported`. Neither is a fault in the game. When
147
+ `getShell()` from `@genex-ai/embed-sdk` is `"app"`, leave the feature's entry
148
+ point out and play the authored fallback; never show it as an error. Which
149
+ shell draws what is in `$genex-threejs-embed-auth` (`references/shells.md`).
150
+
143
151
  ## `maxCredits` is a ceiling, not a price
144
152
 
145
153
  The player is billed **as used**: the call's real provider cost plus the
@@ -389,6 +397,7 @@ These are source contracts, not a claim that every stand runs this lane —
389
397
  - [ ] Disclosure numbers derive from the real loop and the bench's measured average, and are written in `DESIGN.md`
390
398
  - [ ] Calls are batched and cached; nothing fires on an invisible timer
391
399
  - [ ] Every grant-ending code has in-fiction copy and a playable fallback
400
+ - [ ] In the Genex app (`getShell() === "app"`) the feature's entry point is left out and the authored fallback plays
392
401
  - [ ] The in-game readout comes from `getSpendGrant()`
393
402
  - [ ] Exactly one `applyGeneration()` writer; boot re-attaches with `waitForGeneration()`
394
403
  - [ ] `generationId` is saved BEFORE the await
@@ -234,6 +234,22 @@ price sheet, cannot skin it, and cannot complete a purchase itself. That is
234
234
  deliberate: it is what lets a player trust a purchase in a game they have never
235
235
  played before.
236
236
 
237
+ Where that surface is depends on the shell the game is in
238
+ (`$genex-threejs-embed-auth`, `references/shells.md`), and two of them change
239
+ what the game does:
240
+
241
+ - **On the game's Genex page the sheet is drawn by the page, above the game.**
242
+ To show it, the page leaves fullscreen first, and the SDK lets go of the
243
+ pointer lock before it asks (`@genex-ai/embed-sdk` 0.32.0+; a build on an
244
+ older SDK keeps the cursor captured until the player presses Escape). Treat
245
+ a purchase like a pause: re-enter fullscreen and re-lock the pointer from
246
+ the player's next Resume click, never by yourself.
247
+ - **The Genex app has no purchase surface yet.** There `buy()` resolves
248
+ `failed` without opening anything. That is the app, not an error: when
249
+ `getShell()` from `@genex-ai/embed-sdk` is `"app"`, show the shop as
250
+ browse-only or leave the buy buttons out, and never show a failure message
251
+ for it.
252
+
237
253
  ### Delivering
238
254
 
239
255
  ```ts
@@ -295,6 +311,8 @@ your own earnings. Use the test grant instead.
295
311
  - [ ] Every item price is on the 50/100/200/500/1000 grid
296
312
  - [ ] Every price renders `priceDisplayUsdCents` beside the coin figure
297
313
  - [ ] `buy()` is called synchronously inside a click/tap handler
314
+ - [ ] In the Genex app (`getShell() === "app"`) the buy buttons are hidden or
315
+ the shop is browse-only — never a failure message
298
316
  - [ ] `canceled` is silent; only real failures show a message
299
317
  - [ ] `deliverPending()` runs on every boot, before the player can act
300
318
  - [ ] `consumeEntitlement()` is awaited BEFORE the effect is applied
@@ -338,3 +356,11 @@ environment. Nothing to fix in the game; say so and build the rest.
338
356
  **Purchases do nothing in local testing** — local test mode has no wallet and no
339
357
  server. `buy()` returns `failed` immediately by design. Test purchases on a
340
358
  preview or published build.
359
+
360
+ **`buy()` returns `failed` inside the Genex app** — the app has no purchase
361
+ surface yet, so nothing opens. Not a bug to chase: hide the buy buttons there
362
+ (`getShell() === "app"`).
363
+
364
+ **The game dropped out of fullscreen when the shop's confirmation opened** — by
365
+ design on the game's Genex page: the confirmation is the page's, and the page
366
+ has to be visible to show it. Re-enter from the player's next click.
@@ -114,9 +114,9 @@ grants the permission.
114
114
  "click to resume" button.
115
115
  5. Always-visible state: reticle while locked; real cursor + "click to
116
116
  aim/resume" cue while unlocked. The cue is also the validation hook.
117
- 6. If the lock request is rejected (a third-party page embedding the game
118
- without `allow="pointer-lock"`), the mode falls back to drag-orbit — show
119
- "Drag to look" instead. Never a dead game.
117
+ 6. If the lock request is rejected (a sandboxed embed without the
118
+ `allow-pointer-lock` sandbox token), the mode falls back to drag-orbit —
119
+ show "Drag to look" instead. Never a dead game.
120
120
  7. Gate firing/actions on `followCam.aimState === "locked"` — the click that
121
121
  acquires the lock (and any drag in the fallback) must not also shoot.
122
122
  8. Do NOT exit the lock between rounds, respawns, or cutscenes — only for real
@@ -178,8 +178,8 @@ Want a custom HUD? Read `onAimChange: ({ state }) => …` yourself instead of th
178
178
  eye-height target fed to `moveTo`, `avatar.visible = false`, and
179
179
  `controller.setLockForward(true)`.
180
180
  - **Touch / rejected lock:** touch never locks (the mode reports `"off"`); a
181
- third-party embed without `allow="pointer-lock"` goes `"unavailable"` and
182
- drag-orbit stays as the fallback — never a dead camera.
181
+ sandboxed embed without the `allow-pointer-lock` sandbox token goes
182
+ `"unavailable"` and drag-orbit stays as the fallback — never a dead camera.
183
183
 
184
184
  The bucket rule (mandatory / recommended / never) and the full lock lifecycle
185
185
  contract live in `$genex-threejs-camera-direction`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: genex-threejs-embed-auth
3
- description: Wire up player identity AND durable game state for a Genex game via @genex-ai/embed-sdk. Load this UNCONDITIONALLY for every game, multiplayer or not, BEFORE writing any boot code — every player gets an identity (signed-in account or guest); per-player saves, shared world state, and leaderboards are one-line SDK calls; multiplayer requires the SDK's token either way.
3
+ description: Wire up player identity AND durable game state for a Genex game via @genex-ai/embed-sdk. Load this UNCONDITIONALLY for every game, multiplayer or not, BEFORE writing any boot code — every player gets an identity (signed-in account or guest); per-player saves, shared world state, and leaderboards are one-line SDK calls; multiplayer requires the SDK's token either way. Also holds the shells table: what a game may do on its Genex page, in the app and on its own link.
4
4
  ---
5
5
 
6
6
  # Genex Three.js Embed Auth
@@ -18,11 +18,13 @@ for both, using the SDK's token. The SDK handles every context with one
18
18
  straight into guest play. No login screen either way.
19
19
  - **Standalone (someone opens the game's link directly):** the visit bounces
20
20
  once through Genex (at the CDN edge in production, so the game only loads
21
- AFTER identity is resolved; the SDK does the same bounce itself in local
22
- dev) — signed-in visitors arrive already authenticated on ANY game with
23
- zero clicks; everyone else arrives as a guest with a small dismissible
24
- "sign in to save progress" popover (rendered by the SDK — don't build your
25
- own). Nobody ever hits a login wall on a published game.
21
+ AFTER identity is resolved; where the edge did not, the SDK makes the same
22
+ bounce itself) — signed-in visitors arrive already authenticated on ANY
23
+ game with zero clicks; everyone else arrives as a guest with a small
24
+ dismissible "sign in to save progress" popover (rendered by the SDK — don't
25
+ build your own). Nobody ever hits a login wall on a published game.
26
+ - **Inside the Genex mobile app:** the app hands the game its player — the
27
+ signed-in account or a guest — with no bounce and no login screen.
26
28
  - **Local test mode (your own self-testing):** `?genex_local_test=1` on the
27
29
  local dev server boots a credential-less local session — see "Self-testing
28
30
  a draft" below.
@@ -31,6 +33,32 @@ While identity is resolving (or blocked) the SDK shows its own full-screen
31
33
  overlay over the game, so never build a separate "connecting" screen for auth.
32
34
  Guest sessions have **no** overlay — the game just plays.
33
35
 
36
+ ## The four shells — read before you call a browser or device API
37
+
38
+ Those four contexts are the **shells** a game runs in, and they differ in far
39
+ more than identity. What a game may open, save, share, read from its URL or
40
+ ask of the player's device is different in each one, and a call the shell
41
+ refuses usually fails with no error at all.
42
+ **Find the row in [references/shells.md](references/shells.md) before you
43
+ write `navigator.*`, `window.open`, a download link, a dialog, or anything
44
+ that reads the URL.** The digest:
45
+
46
+ - **Which shell:** `getShell()` → `"page"` (the frame on the game's Genex
47
+ page, where most players are, and any other frame), `"app"`,
48
+ `"standalone"` or `"local"`.
49
+ - **Share, copy, open a link, save a file, fullscreen, keep the screen on,
50
+ vibrate:** `share()`, `copyText()`, `openExternal()`, `saveFile()`,
51
+ `setFullscreen()`, `keepAwake()`, `haptic()`, from a click or tap handler,
52
+ before any `await`. They resolve with a reason and never throw.
53
+ - **Your own URL parameters, and where the visitor came from:** `getEntry()`,
54
+ never `location` or `document.referrer`.
55
+ - **Microphone, camera, location:** the build declares them at
56
+ `npx genex preview` / `npx genex publish`; the app refuses all three.
57
+ - **Never:** `alert` / `confirm` / `prompt`, `window.open`, a link or form
58
+ that leaves the game, a service worker, reading the clipboard.
59
+ - **Local test mode is a plain browser tab, not the frame.** A call that works
60
+ there can still be dead on the game's Genex page.
61
+
34
62
  ## Install
35
63
 
36
64
  ```bash
@@ -86,8 +114,9 @@ const { user, guest } = await waitForPlayer();
86
114
  // ./assets/avatar.vrm behind it (absent on old APIs / local test mode).
87
115
  // Peers resolve the same way via the multiplayer SDK's player.avatarUrl.
88
116
 
89
- // ACCOUNT gate — resolves ONLY for signed-in players (stays pending for
90
- // guests; resolves later if they sign in mid-game). Use ONLY for /state
117
+ // ACCOUNT gate — resolves ONLY for signed-in players. For a guest it stays
118
+ // pending for as long as this page is loaded: signing in loads the game
119
+ // again, and the gate resolves on that load. Use ONLY for /state
91
120
  // saving/loading and other account-bound features.
92
121
  const { user: account } = await waitForAuth();
93
122
  ```
@@ -104,9 +133,10 @@ boot-path gate; `waitForAuth()` guards saves only.
104
133
  and player-name UI; resolves for guests and accounts alike, rejects only if
105
134
  the session ends up blocked.
106
135
  - `waitForAuth()` → `Promise<{ user }>` — the gate for `/state` calls and
107
- account-bound features. Stays PENDING for guests (resolves live if they
108
- sign in); rejects if blocked. Catch rejections and let the SDK's overlay
109
- handle the UX (don't build your own sign-in UI).
136
+ account-bound features. Stays PENDING for a guest for the whole page load
137
+ (signing in loads the game again, and it resolves on that next load);
138
+ rejects if blocked. Catch rejections and let the SDK's overlay handle the
139
+ UX (don't build your own sign-in UI).
110
140
  - `isEmbedded()` → `boolean` — structural "is in an iframe" check; NOT the same
111
141
  question as "is signed in".
112
142
  - `getAuthState()` → `"pending" | "authenticated" | "guest" | "blocked"` —
@@ -123,8 +153,44 @@ boot-path gate; `waitForAuth()` guards saves only.
123
153
  automatic re-seat reads a fresh token. Tokens rotate automatically (~every
124
154
  10 minutes). Never cache the value across joins and never log it.
125
155
  - `on(event, cb)` → unsubscribe fn. Events: `"authenticated"`, `"guest"`,
126
- `"blocked"`, `"error"`. A mid-game sign-in fires `"authenticated"` after
127
- `"guest"` — progress saving can start right then, no reload.
156
+ `"blocked"`, `"error"`. A page load ends in one of the first three. Signing
157
+ in loads the game again, so the account arrives as `"authenticated"` on
158
+ the next load, not as a second event on this one.
159
+
160
+ Where the game is running and what it was opened with (`@genex-ai/embed-sdk`
161
+ 0.32.0+; all three are safe to call before `initEmbed()`):
162
+
163
+ - `getShell()` → `"page" | "app" | "standalone" | "local"` — which shell this
164
+ is. A fact about the page: not who is signed in, and not a permission. Any
165
+ frame is `"page"`, whoever frames it, and the whole page of any React
166
+ Native web view is `"app"`.
167
+ - `getEntry()` → `{ search, params, hash, referrer }` — the link the game was
168
+ opened with: its own query (`search` as text, `params` as
169
+ `URLSearchParams`), its own fragment, and the origin of the site the visitor
170
+ came from, or `null`.
171
+ - `onShellClosing(cb)` → unsubscribe fn — `cb` runs when the page is being
172
+ taken away: the tab closed or reloaded, the app closing the game. The last
173
+ chance for a small `savePlayerState()`, never the only one.
174
+
175
+ Platform calls (0.32.0+): one call each that does the right thing in
176
+ whichever shell the game is in. **Call them from a click or tap handler,
177
+ before any `await`.** They resolve and never throw. What each one does in
178
+ each shell is in [references/shells.md](references/shells.md).
179
+
180
+ - `share({ text?, params? })` → `Promise<{ status, url }>`; `status` is
181
+ `"shared" | "copied" | "dismissed" | "unavailable"`. The SDK builds the
182
+ link; the game supplies only its own `params`.
183
+ - `copyText(text)`, `keepAwake(on)`, `openExternal(url)`,
184
+ `saveFile(blob, name)`, `setFullscreen(on)` →
185
+ `Promise<{ ok: true } | { ok: false, error }>`; `error` is `unsupported` |
186
+ `needs_gesture` | `dismissed` | `busy` | `rate_limited` | `invalid_request`
187
+ | `too_large` | `timeout`. `unsupported` and `timeout` are ordinary answers,
188
+ not bugs.
189
+ - `haptic(kind)` → nothing; `kind` is `"selection" | "light" | "medium" |
190
+ "heavy" | "success" | "warning" | "error"`.
191
+ - `supports(name)` → `boolean`, synchronous: whether to draw the button at
192
+ all. `name` is `"share" | "copy" | "awake" | "haptic" | "open" | "save" |
193
+ "fullscreen"`.
128
194
 
129
195
  Durable state + leaderboards (all six are safe to call from boot — they wait
130
196
  for identity internally, never throw for guests, and reject only when the
@@ -135,8 +201,9 @@ session is blocked):
135
201
  overwrite it). `{ data: null, version: 0 }` when they never saved.
136
202
  - `savePlayerState(data, { ifVersion? }?)` → `Promise<{ saved, version?,
137
203
  conflict?, guest?, queued?, staging? }>` — save any JSON ≤ 256KB to their
138
- slot. For guests the value is QUEUED in memory and auto-flushed if they sign
139
- in mid-game — no extra code. Small saves survive tab close automatically.
204
+ slot. For a guest nothing is saved: the value is held in memory for this
205
+ page load only (`{ saved: false, guest: true, queued: true }`), and signing
206
+ in loads the game again. Small saves survive tab close automatically.
140
207
  - `loadWorldState()` / `saveWorldState(data, { ifVersion? }?)` — the game's
141
208
  ONE shared world slot (≤ 1MB, every player reads/writes the same blob) —
142
209
  level layouts, persistent-world object positions. Multiplayer games: only
@@ -145,7 +212,8 @@ session is blocked):
145
212
  `{ conflict: true, version }` — reload, merge, retry.
146
213
  - `submitScore(score, { board?, mode? }?)` → `Promise<{ submitted, best?,
147
214
  improved?, guest?, queued? }>` — keep-best leaderboard submit (`mode:
148
- "min"` for lap-time boards). Guests: best value queues, flushes on sign-in.
215
+ "min"` for lap-time boards). A guest's score is not posted: the best value
216
+ is held in memory for this page load only.
149
217
  - `getLeaderboard({ board?, limit?, order? }?)` → `Promise<{ items, me }>` —
150
218
  top entries (verified display names — never trust client-side name input
151
219
  for this) + the signed-in player's own `{ rank, score }`. Works for guests
@@ -162,10 +230,11 @@ and saves there by design (a staging build with a changed save schema must
162
230
  never overwrite the save of the game people are playing). The helpers resolve
163
231
  `{ staging: true }` — `loadPlayerState()` → `{ data: null, version: 0,
164
232
  staging: true }`, `savePlayerState()` → `{ saved: false, staging: true }` —
165
- and never throw or fire `"error"` for it. Keep a localStorage fallback for
166
- progress, and if the game says anything about it, say "Preview build —
167
- progress isn't saved to your account". Never "offline" or "unavailable": the
168
- owner is testing their own game, and Genex is up.
233
+ and never throw or fire `"error"` for it. Keep a local copy of progress
234
+ (`localStorage`) so a preview stays playable — a cache for this device and
235
+ this shell, never the save — and if the game says anything about it, say
236
+ "Preview build — progress isn't saved to your account". Never "offline" or
237
+ "unavailable": the owner is testing their own game, and Genex is up.
169
238
 
170
239
 
171
240
  ## Saving progress (per-player — every player has their own slot)
@@ -186,11 +255,35 @@ applyProgress(data ?? defaultProgress());
186
255
  void savePlayerState(progress);
187
256
  ```
188
257
 
189
- Guests need zero special handling: their saves queue in memory and auto-flush
190
- the moment they sign in mid-game (so a guest's progress follows them into
191
- their new account), and `waitForAuth()` resolving is the signal that a real
192
- account now exists. The SDK already tells guests to sign in (its popover / the
193
- dashboard's card) — don't add another prompt.
258
+ Guests need no gating: the helpers resolve for them instead of failing. But
259
+ **a guest's progress is not saved, and it does not follow them into an
260
+ account.** The SDK holds a guest's latest save in memory for this page load
261
+ only, and signing in on the game's page or its own address loads the game
262
+ again (the SDK's popover and the Genex page both leave for the sign-in and
263
+ come back to a fresh load), so that value is gone when the account arrives.
264
+ Signing in from another tab changes nothing until the game is reloaded. If a
265
+ guest's run should survive signing in, keep your own copy on the device and
266
+ start from it when the account has no save of its own:
267
+
268
+ ```ts
269
+ // at every checkpoint: the device's copy, then the save
270
+ try { localStorage.setItem("progress", JSON.stringify(progress)); } catch {}
271
+ void savePlayerState(progress);
272
+
273
+ // at boot, in place of the plain load above. loadPlayerState() waits for the
274
+ // player, so on the load after a sign-in this is already the account.
275
+ const { data, guest, staging } = await loadPlayerState();
276
+ let kept: unknown = null;
277
+ try { kept = JSON.parse(localStorage.getItem("progress") ?? "null"); } catch {}
278
+ applyProgress(data ?? kept ?? defaultProgress());
279
+ // An account with no save of its own yet keeps what this device had.
280
+ if (data === null && kept !== null && !guest && !staging) void savePlayerState(kept);
281
+ ```
282
+
283
+ That copy is a cache for this device and this shell, never the save: an
284
+ account that already has a save keeps it, and the copy does not exist on
285
+ another device or on the game's other address. The SDK already tells guests
286
+ to sign in (its popover / the dashboard's card) — don't add another prompt.
194
287
 
195
288
  **Per-player vs world:** `savePlayerState` is each player's own progress;
196
289
  `saveWorldState` is the game's ONE shared world (persistent-world layouts —
@@ -213,8 +306,9 @@ const { items, me } = await getLeaderboard({ limit: 10 });
213
306
 
214
307
  Scores are per-account (one row per player per board, real display names from
215
308
  their Genex account) and keep-best — submitting a worse score changes nothing
216
- (`improved: false`). Guests can READ leaderboards; their submits queue and
217
- post when they sign in. Send a consistent `mode` per board. Scores are
309
+ (`improved: false`). Guests can READ leaderboards; a guest's submit is not
310
+ posted, and signing in loads the game again, so submit when a signed-in
311
+ player finishes a run. Send a consistent `mode` per board. Scores are
218
312
  client-reported (arcade-style trust) — don't present them as anti-cheat.
219
313
 
220
314
  ## NEVER log the tokens
@@ -252,8 +346,19 @@ carries a one-time pass (or an inert guest marker) in the URL that the SDK
252
346
  consumes and removes immediately. Unpublished drafts are the exception:
253
347
  strangers can't play them, so a draft link shows the SDK's sign-in gate
254
348
  instead (for self-testing, see local test mode below). Don't code around any
255
- of this: no `?`/`#` URL params of yours will be affected, and `isEmbedded()`
256
- / the return-trip handling are internal SDK concerns.
349
+ of this: `isEmbedded()` and the return-trip handling are internal SDK
350
+ concerns.
351
+
352
+ Your own `?query` comes through that round trip on every published game. Your
353
+ `#fragment` and the site the visitor came from need `@genex-ai/embed-sdk`
354
+ 0.32.0+, written in `package.json` as a range that starts there (`^0.32.0`)
355
+ when the build was shipped.
356
+ Read all three with `getEntry()`, never from `location` or
357
+ `document.referrer`: until `initEmbed()` has run the address bar still holds
358
+ Genex's own fragment, and `document.referrer` is Genex's address or empty.
359
+ `embed` and every key starting `genex_` are reserved. Sizes, the other
360
+ reserved names and what each shell passes on are in
361
+ [references/shells.md](references/shells.md).
257
362
 
258
363
  ### Self-testing a draft: local test mode
259
364
 
@@ -284,7 +389,10 @@ is ever minted): real sign-in (`waitForAuth()` stays pending, exactly like a
284
389
  guest), saves (they queue in memory), leaderboards (`getLeaderboard()`
285
390
  resolves `{ items: [], me: null }` locally), score submits, and multiplayer
286
391
  (`getColyseusAuth()` is `undefined`, so `connect()` fails at the relay —
287
- expected in this mode, not a bug to chase).
392
+ expected in this mode, not a bug to chase). Nor anything the game's Genex
393
+ page or the app refuses: this is a plain browser tab, so `window.open`, a
394
+ download link and a browser dialog all work here and are dead there
395
+ ([references/shells.md](references/shells.md)).
288
396
 
289
397
  Rules:
290
398
 
@@ -299,8 +407,12 @@ Rules:
299
407
  local test mode cannot open the hosted draft; hosted draft access stays
300
408
  owner-only. Do NOT work around that gate: no undocumented URL fragments,
301
409
  no auth mocks, and never drive the user's own signed-in browser.
302
- - Opening localhost WITHOUT the marker keeps the normal real-auth flow (the
303
- identity bounce) — that's for testing real sign-in, not for self-testing.
410
+ - Opening localhost WITHOUT the marker boots local test mode too on
411
+ `@genex-ai/embed-sdk` 0.14.0+ (an older SDK runs the identity bounce
412
+ there). Keep the marker in every URL you open: it says what you meant and
413
+ works on every SDK since 0.5.0. `?genex_auth=1` opts that dev-server tab
414
+ back into the real identity flow — that's for debugging real sign-in, not
415
+ for self-testing.
304
416
  - The owner's draft run-through stays a required beat, not a fallback: push
305
417
  `genex preview` at each playable milestone and hand it off plainly ("check
306
418
  the draft — you can now X; ping me if something feels off"), then keep
@@ -329,12 +441,21 @@ Rules:
329
441
  overlay and the dashboard own all of that UX.
330
442
  - [ ] Self-test evidence captured in local test mode is labeled as such in the
331
443
  handoff ("local test mode — auth, saves, and multiplayer not exercised").
444
+ - [ ] Every browser or device API the game calls has its row in
445
+ [references/shells.md](references/shells.md): sharing, copying, links
446
+ out, file saves and fullscreen go through the SDK calls, from a click
447
+ or tap handler; the game's own URL parameters come from `getEntry()`.
332
448
 
333
449
  ## Troubleshooting
334
450
 
335
- - **Game loads then immediately navigates away (standalone/local dev)** — the
451
+ - **Game loads then immediately navigates away (the game's own link)** — the
336
452
  identity bounce, working as designed for EVERY standalone visit. It returns
337
- to the game automatically (signed-in or as a guest) within a second.
453
+ to the game automatically (signed-in or as a guest) within a second. On a
454
+ dev server it only happens with `?genex_auth=1` or an SDK older than
455
+ 0.14.0: open with `?genex_local_test=1`.
456
+ - **Works locally or on the game's own link, does nothing on its Genex page
457
+ or in the app** — that shell refuses the call, with no error. Find the row
458
+ in [references/shells.md](references/shells.md) and make the call it names.
338
459
  - **`waitForAuth()` never resolves** — the player is a GUEST; that's the
339
460
  designed behavior. Anything that must run for guests belongs behind
340
461
  `waitForPlayer()` instead.
@@ -362,8 +483,9 @@ Rules:
362
483
  switch to `savePlayerState`/`saveWorldState`, which handle every case.
363
484
  - **Saves/loads resolve `{ staging: true }`** — the build is on the preview
364
485
  channel (the draft workspace, `preview--<slug>`), where server saves are off
365
- by design. Not an outage: keep the localStorage fallback and tell the player
366
- "Preview build — progress isn't saved to your account", never "offline".
486
+ by design. Not an outage: keep the local copy (a cache for this device and
487
+ this shell, never the save) and tell the player "Preview build — progress
488
+ isn't saved to your account", never "offline".
367
489
  - **Saves or score submits return 429** — writing too often (per-player limits:
368
490
  60 player-saves/min, 120 world-saves/min, 30 score submits/min). Debounce to
369
491
  ~1 write/sec at checkpoints and never save in the render loop; back off a