@genex-ai/cli-demo 0.10.0 → 0.12.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/dist/index.js CHANGED
@@ -1095,6 +1095,17 @@ async function detectEmbedSdkVersion(cwd = process.cwd()) {
1095
1095
  return null;
1096
1096
  }
1097
1097
  }
1098
+ async function detectMultiplayer(cwd = process.cwd()) {
1099
+ try {
1100
+ const raw = await fs8.readFile(path9.join(cwd, "package.json"), "utf8");
1101
+ const pkg = JSON.parse(raw);
1102
+ return Boolean(
1103
+ pkg.dependencies?.["@genex-ai/multiplayer"] ?? pkg.devDependencies?.["@genex-ai/multiplayer"]
1104
+ );
1105
+ } catch {
1106
+ return false;
1107
+ }
1108
+ }
1098
1109
  async function runPublish(opts) {
1099
1110
  const log = createLogger({ quiet: opts.quiet });
1100
1111
  log.plain(c.bold("genex publish"));
@@ -1132,8 +1143,10 @@ async function runPublish(opts) {
1132
1143
  if (opts.title) body.title = opts.title;
1133
1144
  if (opts.description) body.description = opts.description;
1134
1145
  if (opts.regenerateCover) body.regenerateCover = true;
1146
+ if (opts.categories?.length) body.categories = opts.categories;
1135
1147
  const embedSdkVersion = await detectEmbedSdkVersion();
1136
1148
  if (embedSdkVersion) body.embedSdkVersion = embedSdkVersion;
1149
+ body.multiplayer = await detectMultiplayer();
1137
1150
  res = await fetch(`${apiUrl}/api/projects/${meta.id}/publish`, {
1138
1151
  method: "POST",
1139
1152
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
@@ -1486,6 +1499,8 @@ ${c.bold("Options for `preview` / `publish`")}
1486
1499
  --no-push (publish) Skip build + push; only flip the gallery flag.
1487
1500
  --title <title> (publish) Gallery title.
1488
1501
  --description <text> (publish) Gallery description.
1502
+ --categories <list> (publish) 1-3 gallery categories, comma-separated.
1503
+ Valid: games, assets, physics, terrain, lighting, vfx.
1489
1504
  --regenerate-cover (publish) Re-mint the disc cover (concept changed).
1490
1505
  --api-url <url> (publish) Override the API base URL.
1491
1506
  --env <path> Token env file (default: ~/.genex/env).
@@ -1506,6 +1521,7 @@ ${c.bold("Examples")}
1506
1521
  genex init my-game --api-url http://localhost:3000 --auth-url http://localhost:5173
1507
1522
  genex preview
1508
1523
  genex publish
1524
+ genex publish --categories games,vfx
1509
1525
  genex publish --no-push --title "My Game"
1510
1526
  genex model "weathered wooden barrel with iron bands"
1511
1527
  genex skybox "golden hour over a misty mountain range"
@@ -1529,6 +1545,7 @@ function parseArgs(argv) {
1529
1545
  "--name",
1530
1546
  "--title",
1531
1547
  "--description",
1548
+ "--categories",
1532
1549
  "--timeout",
1533
1550
  "--duration"
1534
1551
  ]);
@@ -1623,6 +1640,9 @@ function applyValueFlag(options, flag, value) {
1623
1640
  case "--description":
1624
1641
  options.description = value;
1625
1642
  break;
1643
+ case "--categories":
1644
+ options.categories = value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
1645
+ break;
1626
1646
  case "--timeout": {
1627
1647
  const n = Number(value);
1628
1648
  if (!Number.isFinite(n) || n <= 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.10.0",
3
+ "version": "0.12.1",
4
4
  "description": "Set up your ~/.claude workspace, authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,14 @@
15
15
  "engines": {
16
16
  "node": ">=20"
17
17
  },
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "prepack": "pnpm build",
21
+ "start": "node src/index.ts",
22
+ "dev": "node --watch src/index.ts",
23
+ "typecheck": "tsc --noEmit",
24
+ "test": "node --test test/*.test.ts"
25
+ },
18
26
  "keywords": [
19
27
  "cli",
20
28
  "claude",
@@ -38,12 +46,5 @@
38
46
  "bugs": {
39
47
  "url": "https://github.com/me-ai-org/genex-demo/issues"
40
48
  },
41
- "homepage": "https://github.com/me-ai-org/genex-demo/tree/main/apps/cli#readme",
42
- "scripts": {
43
- "build": "tsup",
44
- "start": "node src/index.ts",
45
- "dev": "node --watch src/index.ts",
46
- "typecheck": "tsc --noEmit",
47
- "test": "node --test test/*.test.ts"
48
- }
49
- }
49
+ "homepage": "https://github.com/me-ai-org/genex-demo/tree/main/apps/cli#readme"
50
+ }
@@ -45,6 +45,19 @@ npx genex texture "mossy cobblestone" --terrain # a tiling surface texture
45
45
  Each has a focused skill with the exact loader code — `$genex-ai-model`,
46
46
  `$genex-ai-skybox`, `$genex-ai-sfx`, `$genex-ai-texture`.
47
47
 
48
+ ## Publishing
49
+
50
+ `npx genex preview` deploys to your unlisted draft URL; `npx genex publish`
51
+ lists the game in the public gallery. When publishing, pick 1–3 gallery
52
+ categories from what you actually built — `games`, `assets`, `physics`,
53
+ `terrain`, `lighting`, `vfx` — and pass them comma-separated:
54
+
55
+ ```bash
56
+ npx genex publish --categories games,vfx
57
+ ```
58
+
59
+ If unsure, use `games` (also the server's fallback for anything unrecognized).
60
+
48
61
  Your own files were left untouched. `genex init` only adds missing files and
49
62
  refreshes the genex-owned ones.
50
63
 
@@ -1,24 +1,31 @@
1
1
  ---
2
2
  name: genex-threejs-embed-auth
3
- description: Wire up signed-in identity for a Genex game via @genex-ai/embed-sdk. Load this UNCONDITIONALLY for every game, multiplayer or not, BEFORE writing any boot code — identity is required for play, saving progress, and multiplayer; there is no anonymous mode.
3
+ description: Wire up player identity 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); saving progress requires sign-in, and multiplayer requires the SDK's token either way.
4
4
  ---
5
5
 
6
6
  # Genex Three.js Embed Auth
7
7
 
8
8
  `@genex-ai/embed-sdk` is how a Genex game learns **who is playing it**. Every
9
- game needs it there is **no anonymous play**: without a signed-in identity
10
- the game is not playable, cannot save progress, and cannot join multiplayer.
11
- The SDK handles both contexts with one `initEmbed(...)` call:
12
-
13
- - **Embedded in the Genex dashboard (an iframe):** a silent handshake with the
14
- dashboard signs the player in within a couple of seconds — no login screen.
15
- - **Standalone (someone opens the game's link directly):** the SDK redirects
16
- through Genex sign-in (first-time visitors sign up there) and returns to the
17
- game automatically. This is a real page navigation, by design — game code
18
- does not need to handle any of it.
19
-
20
- While identity is resolving (or missing) the SDK shows its own full-screen
9
+ game needs it. Published games are playable by **guests** (no account the
10
+ SDK mints a temporary identity like `Guest-1234` automatically), while
11
+ **signing in** unlocks saving/loading progress; multiplayer works for both,
12
+ using the SDK's token. The SDK handles every context with one
13
+ `initEmbed(...)` call:
14
+
15
+ - **Embedded in the Genex dashboard (an iframe):** a silent handshake signs
16
+ the viewer in within a couple of seconds or drops a signed-out viewer
17
+ straight into guest play. No login screen either way.
18
+ - **Standalone (someone opens the game's link directly):** the visit bounces
19
+ once through Genex (at the CDN edge in production, so the game only loads
20
+ AFTER identity is resolved; the SDK does the same bounce itself in local
21
+ dev) — signed-in visitors arrive already authenticated on ANY game with
22
+ zero clicks; everyone else arrives as a guest with a small dismissible
23
+ "sign in to save progress" popover (rendered by the SDK — don't build your
24
+ own). Nobody ever hits a login wall on a published game.
25
+
26
+ While identity is resolving (or blocked) the SDK shows its own full-screen
21
27
  overlay over the game, so never build a separate "connecting" screen for auth.
28
+ Guest sessions have **no** overlay — the game just plays.
22
29
 
23
30
  ## Install
24
31
 
@@ -50,42 +57,69 @@ initEmbed({
50
57
  });
51
58
  ```
52
59
 
60
+ Also give `<body>` a dark background in `index.html` (e.g.
61
+ `<body style="margin:0;background:#080a14">`) — it makes the pre-boot frame
62
+ (before any JS runs) match the SDK's own loading overlay instead of flashing
63
+ white, and every Genex surface assumes a dark canvas anyway.
64
+
53
65
  Scene setup and asset loading may continue immediately after this call — auth
54
- never blocks rendering. Gate the **identity-dependent** parts (multiplayer
55
- connect, saving/loading progress, showing the player's name) on `waitForAuth()`:
66
+ never blocks rendering. There are **two gates**, and picking the right one
67
+ matters:
56
68
 
57
69
  ```ts
58
- import { waitForAuth, getColyseusAuth, getEmbedToken } from "@genex-ai/embed-sdk";
59
-
60
- const { user } = await waitForAuth(); // resolves when signed in; rejects if blocked
61
- // user.id / user.name are the real Genex identity
70
+ import { waitForPlayer, waitForAuth, getColyseusAuth, getEmbedToken } from "@genex-ai/embed-sdk";
71
+
72
+ // PLAYER gate resolves for guests AND signed-in players. Use for
73
+ // multiplayer connect() and player-name UI. This is the gate almost
74
+ // everything wants.
75
+ const { user, guest } = await waitForPlayer();
76
+ // user.id / user.name — real account identity, or guest:<id> / "Guest-1234"
77
+
78
+ // ACCOUNT gate — resolves ONLY for signed-in players (stays pending for
79
+ // guests; resolves later if they sign in mid-game). Use ONLY for /state
80
+ // saving/loading and other account-bound features.
81
+ const { user: account } = await waitForAuth();
62
82
  ```
63
83
 
84
+ **NEVER gate scene boot or `connect()` on `waitForAuth()`** — for a guest it
85
+ stays pending forever and your game would sit empty. `waitForPlayer()` is the
86
+ boot-path gate; `waitForAuth()` guards saves only.
87
+
64
88
  ## API surface (exact — do not invent methods)
65
89
 
66
90
  - `initEmbed({ slug, apiUrl, dashboardOrigins })` — call once, first. All three
67
91
  fields required (from `genex.config.ts`).
68
- - `waitForAuth()` → `Promise<{ user }>` — THE gate for `connect()` and `/state`
69
- calls. Rejects if the session ends up blocked catch it and let the SDK's
70
- overlay handle the UX (don't build your own sign-in UI).
92
+ - `waitForPlayer()` → `Promise<{ user, guest }>` — THE gate for `connect()`
93
+ and player-name UI; resolves for guests and accounts alike, rejects only if
94
+ the session ends up blocked.
95
+ - `waitForAuth()` → `Promise<{ user }>` — the gate for `/state` calls and
96
+ account-bound features. Stays PENDING for guests (resolves live if they
97
+ sign in); rejects if blocked. Catch rejections and let the SDK's overlay
98
+ handle the UX (don't build your own sign-in UI).
71
99
  - `isEmbedded()` → `boolean` — structural "is in an iframe" check; NOT the same
72
100
  question as "is signed in".
73
- - `getAuthState()` → `"pending" | "authenticated" | "blocked"` — synchronous.
74
- - `getUser()` → `{ id, name, image? } | null` — non-null only when authenticated.
101
+ - `getAuthState()` → `"pending" | "authenticated" | "guest" | "blocked"` —
102
+ synchronous.
103
+ - `getUser()` → `{ id, name, image? } | null` — non-null once authenticated OR
104
+ guest. Guest ids are prefixed `guest:`.
75
105
  - `getEmbedToken()` → `string | undefined` — for `Authorization: Bearer` on
76
- `GET`/`PUT ${GENEX.apiUrl}/api/projects/${GENEX.slug}/state` (both REQUIRE it).
106
+ `GET`/`PUT ${GENEX.apiUrl}/api/projects/${GENEX.slug}/state`. Defined for
107
+ guests too, but `/state` answers guests with `403 { "error": "guest_no_save" }`
108
+ — that's why saves gate on `waitForAuth()`, not on the token existing.
77
109
  - `getColyseusAuth()` → `{ embedToken } | undefined` — pass as `connect()`'s
78
- `auth` option (REQUIRED — the relay rejects tokenless joins). Read it fresh
79
- at every `connect()` call; tokens rotate automatically (~every 10 minutes).
80
- - `on(event, cb)` → unsubscribe fn. Events: `"authenticated"`, `"blocked"`,
81
- `"error"`.
110
+ `auth` option (REQUIRED — the relay rejects tokenless joins; guest tokens
111
+ are accepted). Read it fresh at every `connect()` call; tokens rotate
112
+ automatically (~every 10 minutes).
113
+ - `on(event, cb)` → unsubscribe fn. Events: `"authenticated"`, `"guest"`,
114
+ `"blocked"`, `"error"`. A mid-game sign-in fires `"authenticated"` after
115
+ `"guest"` — progress saving can start right then, no reload.
82
116
 
83
117
  From `@genex-ai/embed-sdk/sentry` (crash reporting; exactly these two):
84
118
 
85
119
  - `initGameSentry({ slug, dsn?, environment? })` — call once, BEFORE
86
120
  `initEmbed()`. Only `slug` is required; the shared Genex Sentry project DSN
87
- is built in. Errors, tracing, and session replay all start here; the signed-in
88
- user is attached automatically after auth (no code needed).
121
+ is built in. Errors, tracing, and session replay all start here; the current
122
+ player (account or guest) is attached automatically (no code needed).
89
123
  - `sentryCanvasSnapshot(canvas)` — session replay records the DOM, not the 3D
90
124
  canvas; call this once per frame at the END of the render loop so replays
91
125
  show actual gameplay. Works for BOTH WebGL and WebGPU renderers; internally
@@ -100,6 +134,31 @@ function animate() {
100
134
  }
101
135
  ```
102
136
 
137
+ ## Saving progress with guests around
138
+
139
+ Guests play but cannot save — design the save path accordingly:
140
+
141
+ ```ts
142
+ // Fire-and-forget save that is simply OFF for guests:
143
+ async function saveProgress(data: unknown) {
144
+ const token = getEmbedToken();
145
+ if (getAuthState() !== "authenticated" || !token) return; // guest: skip silently
146
+ await fetch(`${GENEX.apiUrl}/api/projects/${GENEX.slug}/state`, {
147
+ method: "PUT",
148
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
149
+ body: JSON.stringify(data),
150
+ });
151
+ }
152
+
153
+ // And start loading/saving the moment a guest upgrades mid-game:
154
+ waitForAuth().then(({ user }) => {
155
+ // signed in (possibly after starting as a guest) — load their save now
156
+ }).catch(() => { /* blocked — SDK overlay owns the UX */ });
157
+ ```
158
+
159
+ The SDK already tells guests to sign in (its popover / the dashboard's card) —
160
+ don't add another prompt.
161
+
103
162
  ## Crash reporting rules
104
163
 
105
164
  - `initGameSentry` has token scrubbing built in (the sign-in return-trip pass
@@ -136,13 +195,17 @@ If `src/genex.config.ts` is somehow missing (e.g. it was deleted), re-run
136
195
 
137
196
  ## Standalone behavior (what to expect, not something to code)
138
197
 
139
- Opening the game's link directly (not from the dashboard) bounces through the
140
- Genex sign-in page and back before the game becomes playable including during
141
- your own local `npm run dev` checks (sign in once; the bounce is instant after
142
- that). The return trip carries a one-time pass in the URL that the SDK consumes
143
- and removes immediately. Don't code around any of this: no `?`/`#` URL params
144
- of yours will be affected, and `isEmbedded()` / the return-trip handling are
145
- internal SDK concerns.
198
+ Opening a published game's link directly (not from the dashboard) shows a
199
+ brief "Loading…" overlay while the SDK round-trips through Genex once, then
200
+ the game starts: already-signed-in visitors come back authenticated (zero
201
+ clicks on every game), everyone else comes back playing as a guest with the
202
+ SDK's own top-right "sign in to save progress" popover. The return trip
203
+ carries a one-time pass (or an inert guest marker) in the URL that the SDK
204
+ consumes and removes immediately. Unpublished drafts are the exception:
205
+ strangers can't play them, so a draft link shows the SDK's sign-in gate
206
+ instead. Don't code around any of this: no `?`/`#` URL params of yours will
207
+ be affected, and `isEmbedded()` / the return-trip handling are internal SDK
208
+ concerns.
146
209
 
147
210
  ## Checklist
148
211
 
@@ -151,24 +214,33 @@ internal SDK concerns.
151
214
  - [ ] `sentryCanvasSnapshot(renderer.domElement)` runs after `renderer.render()`
152
215
  in the main loop (WebGL and WebGPU alike).
153
216
  - [ ] `genex.config.ts` includes `dashboardOrigins` (from `.genex/project.json`).
154
- - [ ] Everything identity-dependent awaits `waitForAuth()` (multiplayer `connect`,
155
- `/state` load/save, player-name UI).
156
- - [ ] `/state` fetches send `Authorization: Bearer ${getEmbedToken()}`.
217
+ - [ ] Multiplayer `connect()` and player-name UI await `waitForPlayer()`
218
+ NEVER `waitForAuth()` (guests would hang forever).
219
+ - [ ] `/state` load/save gates on `waitForAuth()` / `getAuthState() ===
220
+ "authenticated"` and sends `Authorization: Bearer ${getEmbedToken()}`;
221
+ guests skip saves silently (the server answers them `403 guest_no_save`).
157
222
  - [ ] No token value is ever logged or sent to analytics.
158
- - [ ] No custom sign-in/anonymous-fallback UI the SDK overlay + dashboard own that.
223
+ - [ ] No custom sign-in prompt, guest badge, or auth overlay the SDK popover/
224
+ overlay and the dashboard own all of that UX.
159
225
 
160
226
  ## Troubleshooting
161
227
 
162
- - **Game loads then immediately navigates away (standalone/local dev)** — that's
163
- the sign-in bounce working as designed. Sign in once in the browser; it
164
- returns to the game automatically.
165
- - **`waitForAuth()` rejects / state is `"blocked"`** the visitor declined or
166
- failed sign-in, or the dashboard session ended. The SDK overlay (or the
167
- dashboard, when embedded) shows the sign-in prompt; the game just stays
168
- paused behind it. Don't retry in a loop.
228
+ - **Game loads then immediately navigates away (standalone/local dev)** — the
229
+ identity bounce, working as designed for EVERY standalone visit. It returns
230
+ to the game automatically (signed-in or as a guest) within a second.
231
+ - **`waitForAuth()` never resolves** the player is a GUEST; that's the
232
+ designed behavior. Anything that must run for guests belongs behind
233
+ `waitForPlayer()` instead.
234
+ - **State is `"blocked"` / `waitForPlayer()` rejects** — an unpublished draft
235
+ opened by a non-owner, or auth infrastructure was unreachable. The SDK
236
+ overlay (or the dashboard, when embedded) shows the sign-in prompt; the game
237
+ just stays paused behind it. Don't retry in a loop.
169
238
  - **Multiplayer join rejected with 401** — `connect()` ran before
170
- `waitForAuth()` resolved, without `auth: getColyseusAuth()!`, or with a stale
171
- cached token on reconnect (read it fresh each call).
172
- - **`/state` returns 401/403** — missing `Authorization` header (401) or the
173
- token belongs to a different game (403): `GENEX.slug` doesn't match this
174
- project.
239
+ `waitForPlayer()` resolved, without `auth: getColyseusAuth()!`, or with a
240
+ stale cached token on reconnect (read it fresh each call).
241
+ - **Multiplayer join rejected with 403 "guest capacity"** — the room is at its
242
+ guest limit; only signing in gets the player a seat right now. Surface the
243
+ relay's message as-is.
244
+ - **`/state` returns 401/403** — missing `Authorization` header (401), a guest
245
+ token (403 `guest_no_save` — expected, skip saves for guests), or the token
246
+ belongs to a different game (403): `GENEX.slug` doesn't match this project.
@@ -44,17 +44,19 @@ npm i @genex-ai/multiplayer
44
44
  Pick your own per-player state shape (any JSON). `room` is the **project slug**
45
45
  (printed by `genex init`) — same id = same room, different ids are fully isolated.
46
46
 
47
- **Joining requires a signed-in identity — the relay rejects joins without one.**
47
+ **Joining requires the SDK's player identity — the relay rejects tokenless
48
+ joins, but accepts guests** (accountless players named like `Guest-1234`).
48
49
  Load the `genex-threejs-embed-auth` skill first (it sets up `initEmbed(...)`),
49
- then gate `connect()` on `waitForAuth()`:
50
+ then gate `connect()` on `waitForPlayer()` — NOT `waitForAuth()`, which stays
51
+ pending for guests and would keep them out of multiplayer forever:
50
52
 
51
53
  ```ts
52
54
  import { connect } from "@genex-ai/multiplayer";
53
- import { waitForAuth, getColyseusAuth } from "@genex-ai/embed-sdk";
55
+ import { waitForPlayer, getColyseusAuth } from "@genex-ai/embed-sdk";
54
56
 
55
57
  type State = { x: number; z: number; q: number[] }; // YOUR per-player state (rotation as quaternion)
56
58
 
57
- const { user } = await waitForAuth(); // identity gate — rejects if the session is blocked
59
+ const { user } = await waitForPlayer(); // player gate (guest OR signed-in) — rejects only if blocked
58
60
  const room = await connect<State>({
59
61
  url: GENEX.colyseusUrl, // e.g. "wss://demo-colyseus.glotech.world" — see config wiring below
60
62
  room: GENEX.slug, // the project slug — everyone with this id shares a room
@@ -207,9 +209,11 @@ file layout.
207
209
  The relay is in-memory: room state is gone when everyone leaves or the server restarts. For a world
208
210
  that persists, save/load one JSON blob keyed by the project slug, from **one authority** (the host):
209
211
 
210
- Both calls **require the embed identity** (`Authorization: Bearer` with the token from
211
- `getEmbedToken()`) — there is no anonymous read or save. Call them only after
212
- `await waitForAuth()` (see the `genex-threejs-embed-auth` skill):
212
+ Both calls **require a signed-in identity** (`Authorization: Bearer` with the token
213
+ from `getEmbedToken()`) — guests play multiplayer but cannot read or write saves
214
+ (the server answers their token `403 guest_no_save`). Gate them on
215
+ `await waitForAuth()` — the ACCOUNT gate, deliberately stricter than the
216
+ `waitForPlayer()` gate `connect()` uses (see the `genex-threejs-embed-auth` skill):
213
217
 
214
218
  ```ts
215
219
  import { getEmbedToken } from "@genex-ai/embed-sdk";
@@ -239,8 +243,9 @@ single writer.
239
243
  ## Checklist
240
244
 
241
245
  - [ ] `npm i @genex-ai/multiplayer` (≥ 0.4.0 for objects/host); config wired into the build.
242
- - [ ] `connect()` runs AFTER `await waitForAuth()` and passes `auth: getColyseusAuth()!`
243
- (the relay rejects tokenless joins — see `genex-threejs-embed-auth`).
246
+ - [ ] `connect()` runs AFTER `await waitForPlayer()` (never `waitForAuth()` — guests would
247
+ hang) and passes `auth: getColyseusAuth()!` (the relay rejects tokenless joins —
248
+ see `genex-threejs-embed-auth`).
244
249
  - [ ] `room` is the **project slug**.
245
250
  - [ ] `me.set` on a fixed **10–20 Hz** tick; full object each time.
246
251
  - [ ] Skip yourself in `room.players` (`id === room.id`).
@@ -255,6 +260,7 @@ single writer.
255
260
  ## Troubleshooting auth
256
261
 
257
262
  - **`connect()` rejects with 401/403** — 401 "auth required"/"invalid token": you joined
258
- without `auth` or before `waitForAuth()` resolved (or the token expired mid-reconnect —
263
+ without `auth` or before `waitForPlayer()` resolved (or the token expired mid-reconnect —
259
264
  read `getColyseusAuth()` fresh at every connect). 403 "wrong game": the `room` value
260
- doesn't match this game's own slug.
265
+ doesn't match this game's own slug. 403 "guest capacity": the room is at its guest
266
+ limit — signing in gets the player a seat; surface the message as-is.