@crowdedkingdoms/crowdyjs 6.1.1 → 7.0.0

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/MIGRATION.md CHANGED
@@ -1,3 +1,58 @@
1
+ # CrowdyJS v7 — Overworld portals & app-scoped tokens (BREAKING)
2
+
3
+ v7 splits the single app-agnostic game token into two credentials and makes
4
+ gameplay require an **app-scoped token**. This is a breaking change requiring
5
+ servers on the matching release (management-api + game-api + Buddy with the
6
+ app-scoped-token feature).
7
+
8
+ **The two credential kinds**
9
+
10
+ - **Identity SESSION token** — returned by `client.auth.login()` / `register()`.
11
+ It talks to the **Management API only** (account, studio admin, and minting
12
+ app tokens). It is **no longer valid for gameplay**: the Game API and Buddy
13
+ reject it. Never hand it to a game stack.
14
+ - **App-scoped GAMEPLAY token** — short-lived (default ~30 min), confined to one
15
+ app. Minted from a session token via the portal flow; used against that app's
16
+ Game API + realtime surface.
17
+
18
+ **What breaks**
19
+
20
+ - Driving `client.udp`, `client.world(appId)`, `serverWithLeastClients`,
21
+ `connectUdpProxy`, world reads/writes, etc. with a plain login token now fails
22
+ (`APP_TOKEN_REQUIRED` on `udpNotifications`; `FORBIDDEN`/`SCOPE_MISSING` on
23
+ HTTP). You must obtain an app token first.
24
+ - A single client can no longer be both your identity client and your game
25
+ client. Use the two-client pattern: an Overworld/identity client (holds the
26
+ session token) and a per-game client (holds that game's app token).
27
+
28
+ **New: `client.portal`**
29
+
30
+ ```ts
31
+ // Native / same-origin: mint directly with the session token.
32
+ const appToken = await overworld.portal.mintAppToken(appId);
33
+ const game = createCrowdyClient({ httpUrl: appToken.gameApiUrl!, wsUrl: appToken.gameApiWsUrl!,
34
+ managementUrl, tokenStore: new BrowserLocalStorageTokenStore('crowdyjs:token:' + appId) });
35
+ game.setToken(appToken.token);
36
+
37
+ // Browser cross-origin handoff (OAuth2 Authorization Code + PKCE):
38
+ // game origin, on "enter":
39
+ const url = await game.portal.beginEntry({ appId, authorizeUrl: 'https://overworld.example.com/authorize',
40
+ redirectUri: location.origin + location.pathname });
41
+ location.assign(url);
42
+ // Overworld /authorize page (holds the session token):
43
+ location.assign(await overworld.portal.handleAuthorizeRequest());
44
+ // game origin, on callback boot:
45
+ const token = await game.portal.completeEntry(); // exchanges code+verifier, stores app token
46
+ // keep playing past expiry without re-portaling:
47
+ await game.portal.refresh();
48
+ ```
49
+
50
+ The session token never reaches the game origin — only the app token does. New
51
+ realtime `RealtimeConnectionEvent` codes: `APP_TOKEN_REQUIRED`,
52
+ `APP_SCOPE_MISMATCH`. New `UdpErrorCode`: `TOKEN_EXPIRED`.
53
+
54
+ ---
55
+
1
56
  # CrowdyJS — npm org rename (v6 version line kept)
2
57
 
3
58
  The package moved to the **`@crowdedkingdoms`** npm organization. The version line
package/README.md CHANGED
@@ -260,6 +260,52 @@ The key parameter is optional and trailing, so it's safe to omit. Requires a ser
260
260
  - `client.session.restore()` reads from the configured `tokenStore`. `BrowserLocalStorageTokenStore` is provided; bring your own for SSR or Node usage.
261
261
  - A single `AuthState` is observed by both the HTTP client and the realtime socket, so HTTP and WebSocket auth can never drift.
262
262
 
263
+ ## Overworld portals & app-scoped tokens (v7)
264
+
265
+ As of v7 gameplay requires an **app-scoped token**, not the login token. Login
266
+ returns an **identity session token** (Management API only — account, studio
267
+ admin, and minting); each game is entered with a short-lived token confined to
268
+ that one app, so a game stack never receives the player's full session.
269
+
270
+ Use two clients: an Overworld/identity client (session token) and a per-game
271
+ client (app token), sharing only the Management URL.
272
+
273
+ ```ts
274
+ // Overworld/identity client
275
+ const overworld = createCrowdyClient({ managementUrl, tokenStore: new BrowserLocalStorageTokenStore('crowdyjs:session') });
276
+ await overworld.auth.login({ email, password });
277
+
278
+ // Native / same-origin: mint directly, then build a game client.
279
+ const t = await overworld.portal.mintAppToken(appId);
280
+ const game = createCrowdyClient({ httpUrl: t.gameApiUrl!, wsUrl: t.gameApiWsUrl!, managementUrl,
281
+ tokenStore: new BrowserLocalStorageTokenStore('crowdyjs:app:' + appId) });
282
+ game.setToken(t.token);
283
+ game.world(appId).subscribe({ actorUpdate: (n) => { /* ... */ } });
284
+ ```
285
+
286
+ Browser cross-origin handoff is OAuth2 Authorization Code + PKCE — the verifier
287
+ never leaves the game origin:
288
+
289
+ ```ts
290
+ // Game origin, on "enter": redirect to the Overworld authorize page.
291
+ location.assign(await game.portal.beginEntry({
292
+ appId, authorizeUrl: 'https://overworld.example.com/authorize',
293
+ redirectUri: location.origin + location.pathname,
294
+ }));
295
+
296
+ // Overworld /authorize page (holds the session token):
297
+ location.assign(await overworld.portal.handleAuthorizeRequest());
298
+
299
+ // Game origin, on callback boot: exchange code+verifier -> app token (stored).
300
+ const entered = await game.portal.completeEntry();
301
+ // Keep playing past expiry without re-portaling (same app):
302
+ await game.portal.refresh();
303
+ ```
304
+
305
+ Game-to-game routes through the Overworld for a fresh per-game token. New
306
+ realtime codes: `APP_TOKEN_REQUIRED`, `APP_SCOPE_MISMATCH`; new `UdpErrorCode`:
307
+ `TOKEN_EXPIRED`. See [MIGRATION.md](MIGRATION.md) for the full v7 breaking guide.
308
+
263
309
  ## Surface scope & security
264
310
 
265
311
  As of v6 (completed in v6.1), CrowdyJS wraps the **full** management-api + game-api
@@ -29,6 +29,7 @@ import { WorldClient } from './world.js';
29
29
  import { AuthAPI } from './domains/auth.js';
30
30
  import { UsersAPI } from './domains/users.js';
31
31
  import { AppsAPI } from './domains/apps.js';
32
+ import { PortalAPI } from './domains/portal.js';
32
33
  import { PlatformAPI } from './domains/platform.js';
33
34
  import { OrganizationsAPI } from './domains/organizations.js';
34
35
  import { AppAccessAPI } from './domains/appAccess.js';
@@ -109,6 +110,12 @@ export declare class CrowdyClient {
109
110
  readonly users: UsersAPI;
110
111
  /** App discovery + routing (which game-api serves a given app). */
111
112
  readonly apps: AppsAPI;
113
+ /**
114
+ * Overworld portal: mint/exchange/refresh app-scoped gameplay tokens and the
115
+ * PKCE browser handoff. Identity session token mints; games receive only an
116
+ * app token.
117
+ */
118
+ readonly portal: PortalAPI;
112
119
  /** Public platform discovery (shared game-api URL, free app quota). */
113
120
  readonly platform: PlatformAPI;
114
121
  /** Organizations, members, RBAC roles, and org API tokens (studio admin). */
@@ -1 +1 @@
1
- {"version":3,"file":"crowdy-client.d.ts","sourceRoot":"","sources":["../src/crowdy-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"crowdy-client.d.ts","sourceRoot":"","sources":["../src/crowdy-client.ts"],"names":[],"mappings":"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"}
@@ -27,6 +27,7 @@ import { WorldClient } from './world.js';
27
27
  import { AuthAPI } from './domains/auth.js';
28
28
  import { UsersAPI } from './domains/users.js';
29
29
  import { AppsAPI } from './domains/apps.js';
30
+ import { PortalAPI } from './domains/portal.js';
30
31
  import { PlatformAPI } from './domains/platform.js';
31
32
  import { OrganizationsAPI } from './domains/organizations.js';
32
33
  import { AppAccessAPI } from './domains/appAccess.js';
@@ -81,6 +82,7 @@ export class CrowdyClient {
81
82
  this.auth = new AuthAPI(this.management, this.session);
82
83
  this.users = new UsersAPI(this.management);
83
84
  this.apps = new AppsAPI(this.management);
85
+ this.portal = new PortalAPI(this.management, this.session);
84
86
  this.platform = new PlatformAPI(this.management);
85
87
  this.organizations = new OrganizationsAPI(this.management);
86
88
  this.appAccess = new AppAccessAPI(this.management);
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Overworld portal: the client side of app-scoped tokens.
3
+ *
4
+ * Two distinct credential kinds replace the old single app-agnostic token:
5
+ * - an identity SESSION token (from `client.auth.login` / `register`) that
6
+ * talks to the Management API and is the ONLY thing that can mint app
7
+ * tokens. Never send it to a game stack.
8
+ * - short-lived, app-scoped GAMEPLAY tokens, one per game, used against that
9
+ * game's Game API + realtime surface.
10
+ *
11
+ * Typical wiring is two `CrowdyClient`s sharing nothing but the same Management
12
+ * URL: an "Overworld"/identity client holding the session token, and a per-game
13
+ * client whose token store holds that game's app token.
14
+ *
15
+ * Browser handoff (cross-origin) is an OAuth2 Authorization-Code + PKCE flow:
16
+ * 1. game origin: `beginEntry()` -> redirect the player to the Overworld
17
+ * `/authorize` page carrying a PKCE challenge + the game's redirect URI.
18
+ * 2. Overworld origin (holds the session): `handleAuthorizeRequest()` ->
19
+ * mints a one-time code and redirects back to the game.
20
+ * 3. game origin: `completeEntry()` -> exchanges the code (+ the PKCE verifier
21
+ * it kept locally) for an app token and stores it.
22
+ *
23
+ * Native / same-origin callers can skip the redirect dance and call
24
+ * `mintAppToken(appId)` directly with a session token.
25
+ */
26
+ import type { GraphQLClient } from '../client.js';
27
+ import type { SessionStore } from '../session.js';
28
+ export interface AppTokenResponse {
29
+ /** Opaque app-scoped gameplay token. Send to the app's Game API as a Bearer. */
30
+ token: string;
31
+ gameTokenId: string;
32
+ /** The app this token is confined to (decimal string). */
33
+ appId: string;
34
+ /** ISO-8601 UTC expiry. Refresh (same app) or re-portal before this. */
35
+ expiresAt: string;
36
+ /** Base HTTPS URL of the Game API serving this app (null if unrouted). */
37
+ gameApiUrl: string | null;
38
+ /** WebSocket URL of the Game API serving this app. */
39
+ gameApiWsUrl: string | null;
40
+ /** Browser launch URL for this app, if configured. */
41
+ launchUrl: string | null;
42
+ }
43
+ export interface PortalAuthorizationCode {
44
+ code: string;
45
+ redirectUri: string;
46
+ expiresAt: string;
47
+ }
48
+ /** Persists the PKCE verifier across the cross-origin redirect round-trip. */
49
+ export interface PkceStore {
50
+ get(state: string): string | null | Promise<string | null>;
51
+ set(state: string, verifier: string): void | Promise<void>;
52
+ remove(state: string): void | Promise<void>;
53
+ }
54
+ /** Default PKCE store backed by sessionStorage; no-op when unavailable (SSR). */
55
+ export declare class BrowserSessionPkceStore implements PkceStore {
56
+ private readonly prefix;
57
+ constructor(prefix?: string);
58
+ private ss;
59
+ get(state: string): string | null;
60
+ set(state: string, verifier: string): void;
61
+ remove(state: string): void;
62
+ }
63
+ export interface BeginEntryParams {
64
+ /** Target app id (decimal string). */
65
+ appId: string;
66
+ /** The Overworld identity origin's authorize page, e.g. `https://overworld.example.com/authorize`. */
67
+ authorizeUrl: string;
68
+ /** Where the Overworld should send the player back (this game's callback). */
69
+ redirectUri: string;
70
+ /** Optional CSRF/correlation state; one is generated if omitted. */
71
+ state?: string;
72
+ }
73
+ export declare class PortalAPI {
74
+ private readonly management;
75
+ private readonly session;
76
+ private readonly pkceStore;
77
+ constructor(management: GraphQLClient, session: SessionStore, pkceStore?: PkceStore);
78
+ /**
79
+ * Native/direct mint: exchange the caller's identity session token for an
80
+ * app-scoped gameplay token. Returns the token; it is NOT stored on this
81
+ * client (build a per-game client with it). Free/open apps auto-grant access;
82
+ * paid apps require an existing entitlement.
83
+ */
84
+ mintAppToken(appId: string): Promise<AppTokenResponse>;
85
+ /**
86
+ * Overworld/identity side: mint a one-time authorization code for a target
87
+ * app, bound to the destination game's PKCE challenge + redirect URI.
88
+ * Requires the identity session token.
89
+ */
90
+ createAuthorizationCode(params: {
91
+ appId: string;
92
+ codeChallenge: string;
93
+ codeChallengeMethod?: string;
94
+ redirectUri: string;
95
+ }): Promise<PortalAuthorizationCode>;
96
+ /**
97
+ * Destination-game side: exchange a one-time code (+ PKCE verifier) for an app
98
+ * token and store it on this client's session so subsequent Game API calls are
99
+ * authenticated. Public — no session token required.
100
+ */
101
+ exchangeCode(code: string, codeVerifier?: string): Promise<AppTokenResponse>;
102
+ /**
103
+ * Same-app refresh: rotate the current app token for a fresh one (extended
104
+ * TTL) and store it. Call before expiry to keep playing without bouncing
105
+ * through the Overworld. Requires the current app token on this session.
106
+ */
107
+ refresh(): Promise<AppTokenResponse>;
108
+ /**
109
+ * Destination-game side, step 1: generate a PKCE pair, persist the verifier,
110
+ * and return the Overworld authorize URL to navigate to. The caller does
111
+ * `window.location.assign(url)`.
112
+ */
113
+ beginEntry(params: BeginEntryParams): Promise<string>;
114
+ /**
115
+ * Overworld/identity side: handle an incoming `/authorize` request. Reads the
116
+ * game's params from the URL, mints a code with the session token, and returns
117
+ * the URL to redirect the player back to (carrying `code` + `state`).
118
+ */
119
+ handleAuthorizeRequest(search?: string): Promise<string>;
120
+ /**
121
+ * Destination-game side, step 3: read `code` + `state` from the callback URL,
122
+ * load the stored verifier, exchange for an app token, and store it. Returns
123
+ * null when there is no `code` param (so it's safe to call unconditionally on
124
+ * boot).
125
+ */
126
+ completeEntry(search?: string): Promise<AppTokenResponse | null>;
127
+ }
128
+ //# sourceMappingURL=portal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portal.d.ts","sourceRoot":"","sources":["../../src/domains/portal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Overworld portal: the client side of app-scoped tokens.
3
+ *
4
+ * Two distinct credential kinds replace the old single app-agnostic token:
5
+ * - an identity SESSION token (from `client.auth.login` / `register`) that
6
+ * talks to the Management API and is the ONLY thing that can mint app
7
+ * tokens. Never send it to a game stack.
8
+ * - short-lived, app-scoped GAMEPLAY tokens, one per game, used against that
9
+ * game's Game API + realtime surface.
10
+ *
11
+ * Typical wiring is two `CrowdyClient`s sharing nothing but the same Management
12
+ * URL: an "Overworld"/identity client holding the session token, and a per-game
13
+ * client whose token store holds that game's app token.
14
+ *
15
+ * Browser handoff (cross-origin) is an OAuth2 Authorization-Code + PKCE flow:
16
+ * 1. game origin: `beginEntry()` -> redirect the player to the Overworld
17
+ * `/authorize` page carrying a PKCE challenge + the game's redirect URI.
18
+ * 2. Overworld origin (holds the session): `handleAuthorizeRequest()` ->
19
+ * mints a one-time code and redirects back to the game.
20
+ * 3. game origin: `completeEntry()` -> exchanges the code (+ the PKCE verifier
21
+ * it kept locally) for an app token and stores it.
22
+ *
23
+ * Native / same-origin callers can skip the redirect dance and call
24
+ * `mintAppToken(appId)` directly with a session token.
25
+ */
26
+ import { parse } from 'graphql';
27
+ import { generatePkcePair, generateState } from '../pkce.js';
28
+ /** Default PKCE store backed by sessionStorage; no-op when unavailable (SSR). */
29
+ export class BrowserSessionPkceStore {
30
+ constructor(prefix = 'crowdyjs:pkce:') {
31
+ this.prefix = prefix;
32
+ }
33
+ ss() {
34
+ return typeof sessionStorage === 'undefined' ? null : sessionStorage;
35
+ }
36
+ get(state) {
37
+ return this.ss()?.getItem(this.prefix + state) ?? null;
38
+ }
39
+ set(state, verifier) {
40
+ this.ss()?.setItem(this.prefix + state, verifier);
41
+ }
42
+ remove(state) {
43
+ this.ss()?.removeItem(this.prefix + state);
44
+ }
45
+ }
46
+ const APP_TOKEN_FIELDS = 'token gameTokenId appId expiresAt gameApiUrl gameApiWsUrl launchUrl';
47
+ const MintAppTokenDocument = parse(`mutation MintAppToken($input: MintAppTokenInput!) { mintAppToken(input: $input) { ${APP_TOKEN_FIELDS} } }`);
48
+ const CreatePortalAuthorizationCodeDocument = parse(`mutation CreatePortalAuthorizationCode($input: CreatePortalAuthorizationCodeInput!) { createPortalAuthorizationCode(input: $input) { code redirectUri expiresAt } }`);
49
+ const ExchangePortalCodeDocument = parse(`mutation ExchangePortalCode($input: ExchangePortalCodeInput!) { exchangePortalCode(input: $input) { ${APP_TOKEN_FIELDS} } }`);
50
+ const RefreshAppTokenDocument = parse(`mutation RefreshAppToken { refreshAppToken { ${APP_TOKEN_FIELDS} } }`);
51
+ export class PortalAPI {
52
+ constructor(management, session, pkceStore = new BrowserSessionPkceStore()) {
53
+ this.management = management;
54
+ this.session = session;
55
+ this.pkceStore = pkceStore;
56
+ }
57
+ /**
58
+ * Native/direct mint: exchange the caller's identity session token for an
59
+ * app-scoped gameplay token. Returns the token; it is NOT stored on this
60
+ * client (build a per-game client with it). Free/open apps auto-grant access;
61
+ * paid apps require an existing entitlement.
62
+ */
63
+ async mintAppToken(appId) {
64
+ const data = await this.management.request(MintAppTokenDocument, {
65
+ input: { appId },
66
+ });
67
+ return data.mintAppToken;
68
+ }
69
+ /**
70
+ * Overworld/identity side: mint a one-time authorization code for a target
71
+ * app, bound to the destination game's PKCE challenge + redirect URI.
72
+ * Requires the identity session token.
73
+ */
74
+ async createAuthorizationCode(params) {
75
+ const data = await this.management.request(CreatePortalAuthorizationCodeDocument, { input: params });
76
+ return data.createPortalAuthorizationCode;
77
+ }
78
+ /**
79
+ * Destination-game side: exchange a one-time code (+ PKCE verifier) for an app
80
+ * token and store it on this client's session so subsequent Game API calls are
81
+ * authenticated. Public — no session token required.
82
+ */
83
+ async exchangeCode(code, codeVerifier) {
84
+ const data = await this.management.request(ExchangePortalCodeDocument, {
85
+ input: { code, codeVerifier },
86
+ });
87
+ this.session.setToken(data.exchangePortalCode.token);
88
+ return data.exchangePortalCode;
89
+ }
90
+ /**
91
+ * Same-app refresh: rotate the current app token for a fresh one (extended
92
+ * TTL) and store it. Call before expiry to keep playing without bouncing
93
+ * through the Overworld. Requires the current app token on this session.
94
+ */
95
+ async refresh() {
96
+ const data = await this.management.request(RefreshAppTokenDocument, {});
97
+ this.session.setToken(data.refreshAppToken.token);
98
+ return data.refreshAppToken;
99
+ }
100
+ // ----- Browser PKCE redirect helpers -------------------------------------
101
+ /**
102
+ * Destination-game side, step 1: generate a PKCE pair, persist the verifier,
103
+ * and return the Overworld authorize URL to navigate to. The caller does
104
+ * `window.location.assign(url)`.
105
+ */
106
+ async beginEntry(params) {
107
+ const state = params.state ?? generateState();
108
+ const pkce = await generatePkcePair();
109
+ await this.pkceStore.set(state, pkce.verifier);
110
+ const url = new URL(params.authorizeUrl);
111
+ url.searchParams.set('app_id', params.appId);
112
+ url.searchParams.set('code_challenge', pkce.challenge);
113
+ url.searchParams.set('code_challenge_method', pkce.method);
114
+ url.searchParams.set('redirect_uri', params.redirectUri);
115
+ url.searchParams.set('state', state);
116
+ return url.toString();
117
+ }
118
+ /**
119
+ * Overworld/identity side: handle an incoming `/authorize` request. Reads the
120
+ * game's params from the URL, mints a code with the session token, and returns
121
+ * the URL to redirect the player back to (carrying `code` + `state`).
122
+ */
123
+ async handleAuthorizeRequest(search) {
124
+ const params = new URLSearchParams(search ?? defaultSearch());
125
+ const appId = params.get('app_id');
126
+ const codeChallenge = params.get('code_challenge');
127
+ const redirectUri = params.get('redirect_uri');
128
+ const state = params.get('state') ?? '';
129
+ const codeChallengeMethod = params.get('code_challenge_method') ?? 'S256';
130
+ if (!appId || !codeChallenge || !redirectUri) {
131
+ throw new Error('authorize request missing app_id, code_challenge, or redirect_uri');
132
+ }
133
+ const result = await this.createAuthorizationCode({
134
+ appId,
135
+ codeChallenge,
136
+ codeChallengeMethod,
137
+ redirectUri,
138
+ });
139
+ const back = new URL(result.redirectUri);
140
+ back.searchParams.set('code', result.code);
141
+ if (state)
142
+ back.searchParams.set('state', state);
143
+ return back.toString();
144
+ }
145
+ /**
146
+ * Destination-game side, step 3: read `code` + `state` from the callback URL,
147
+ * load the stored verifier, exchange for an app token, and store it. Returns
148
+ * null when there is no `code` param (so it's safe to call unconditionally on
149
+ * boot).
150
+ */
151
+ async completeEntry(search) {
152
+ const params = new URLSearchParams(search ?? defaultSearch());
153
+ const code = params.get('code');
154
+ if (!code)
155
+ return null;
156
+ const state = params.get('state') ?? '';
157
+ const verifier = (await this.pkceStore.get(state)) ?? undefined;
158
+ const token = await this.exchangeCode(code, verifier);
159
+ if (state)
160
+ await this.pkceStore.remove(state);
161
+ return token;
162
+ }
163
+ }
164
+ function defaultSearch() {
165
+ const loc = globalThis.location;
166
+ return loc?.search ?? '';
167
+ }
@@ -392,6 +392,24 @@ export declare enum AppStatus {
392
392
  /** Published and purchasable/playable; eligible for the public marketplace when visibility=PUBLIC. */
393
393
  Live = "LIVE"
394
394
  }
395
+ /** A short-lived, app-scoped gameplay token (Overworld portal). Confined to a single app: usable only against that app's Game API + Buddy realtime surface (plus read-only `me` and same-app `refreshAppToken`). It CANNOT perform management operations and CANNOT mint tokens for other apps, so a game stack that receives it never gets the player's full identity session. */
396
+ export type AppTokenResponse = {
397
+ __typename?: 'AppTokenResponse';
398
+ /** The app this token is confined to, as a String. */
399
+ appId: Scalars['String']['output'];
400
+ /** ISO-8601 UTC expiry. Call `refreshAppToken` (same app) before this, or re-portal through the Overworld for a different app. */
401
+ expiresAt: Scalars['String']['output'];
402
+ /** Base HTTPS URL of the Game API that serves this app (null if the app has no dedicated/shared game-api route yet). */
403
+ gameApiUrl: Maybe<Scalars['String']['output']>;
404
+ /** WebSocket URL of the Game API that serves this app (wss://), for realtime subscriptions. */
405
+ gameApiWsUrl: Maybe<Scalars['String']['output']>;
406
+ /** Identifier of the underlying game_token row, as a String. */
407
+ gameTokenId: Scalars['String']['output'];
408
+ /** Browser launch URL for this app (where the player's browser plays it), if configured. */
409
+ launchUrl: Maybe<Scalars['String']['output']>;
410
+ /** Opaque app-scoped gameplay token. Send to the target app's Game API as `Authorization: Bearer <token>` (and in the realtime `connectionParams`). Do NOT send it to the Management API for anything other than `me`/`refreshAppToken`. */
411
+ token: Scalars['String']['output'];
412
+ };
395
413
  /** Aggregate byte totals for one app over the requested window. All *Bytes fields are string counters (may exceed Int range). */
396
414
  export type AppUsageRollupRow = {
397
415
  __typename?: 'AppUsageRollupRow';
@@ -1728,6 +1746,17 @@ export type CreateOrganizationInput = {
1728
1746
  /** Unique URL slug; lowercase letters, numbers, and dashes only (1-128 characters). */
1729
1747
  slug: Scalars['String']['input'];
1730
1748
  };
1749
+ /** Input for createPortalAuthorizationCode: the Overworld (identity origin, holding the session token) mints a one-time code the destination game exchanges for an app token. Browser handoff path; pair with a PKCE verifier held by the destination game origin. */
1750
+ export type CreatePortalAuthorizationCodeInput = {
1751
+ /** Numeric id of the target app the player is portaling into. */
1752
+ appId: Scalars['BigInt']['input'];
1753
+ /** PKCE code challenge (recommended). Base64url(SHA-256(verifier)) when method is S256. The destination game generates the verifier+challenge so the verifier never leaves its origin. */
1754
+ codeChallenge: Scalars['String']['input'];
1755
+ /** PKCE method: "S256" (default, recommended) or "plain". */
1756
+ codeChallengeMethod?: InputMaybe<Scalars['String']['input']>;
1757
+ /** Where to redirect the player after issuing the code. Must match the target app's configured launch_url origin when set. */
1758
+ redirectUri: Scalars['String']['input'];
1759
+ };
1731
1760
  /** Create a runtime session. */
1732
1761
  export type CreateSessionInput = {
1733
1762
  /** The app (tenant) the session belongs to. */
@@ -1848,6 +1877,13 @@ export type EnvironmentUsageSummary = {
1848
1877
  /** Peak/average replication send rates. */
1849
1878
  replicationRates: UsageRatePeaks;
1850
1879
  };
1880
+ /** Input for exchangePortalCode: the destination game (public client) trades a one-time portal code for an app-scoped gameplay token. Public (the code + PKCE verifier authorize the call). */
1881
+ export type ExchangePortalCodeInput = {
1882
+ /** The one-time authorization code received on the redirect. */
1883
+ code: Scalars['String']['input'];
1884
+ /** PKCE code verifier matching the challenge supplied when the code was created. Required when the code was created with a challenge. */
1885
+ codeVerifier?: InputMaybe<Scalars['String']['input']>;
1886
+ };
1851
1887
  /** An org's free shared app slot quota usage. */
1852
1888
  export type FreeAppQuota = {
1853
1889
  __typename?: 'FreeAppQuota';
@@ -2860,6 +2896,11 @@ export type LoginUserInput = {
2860
2896
  /** Account password (min 8 characters). */
2861
2897
  password: Scalars['String']['input'];
2862
2898
  };
2899
+ /** Input for mintAppToken: directly mint an app-scoped gameplay token for the calling user (native/direct path, no browser redirect). */
2900
+ export type MintAppTokenInput = {
2901
+ /** Numeric id of the app to mint a confined gameplay token for. Free/open apps are auto-granted access; paid apps require an existing entitlement (else FORBIDDEN). */
2902
+ appId: Scalars['BigInt']['input'];
2903
+ };
2863
2904
  export type Mutation = {
2864
2905
  __typename?: 'Mutation';
2865
2906
  /** Liveness heartbeat for the authenticated user's actors in an app. Refreshes actors.updated_at for every actor row the user owns so the user stays host-eligible, then returns the freshly-elected host (same shape as the gameHost query) so a client can fold its poll and heartbeat into one round-trip. Call on an interval shorter than HOST_ACTOR_FRESHNESS_SECONDS. Only refreshes rows that already exist (created by Buddy on chunk entry); returns null when no fresh actors exist for the app. */
@@ -2910,6 +2951,8 @@ export type Mutation = {
2910
2951
  createOrgToken: OrgTokenWithSecret;
2911
2952
  /** Creates a new organization and makes the authenticated caller its owner (with full permissions). Requires a valid session token. */
2912
2953
  createOrganization: Organization;
2954
+ /** Create a one-time, PKCE-bound portal authorization code (browser handoff). The Overworld identity origin (holding the SESSION token) calls this; redirect the player to the destination game carrying the code, which the game exchanges via exchangePortalCode. Requires a SESSION token. */
2955
+ createPortalAuthorizationCode: PortalAuthorizationCode;
2913
2956
  /** Create a team. Whether the caller may create one is governed by the per-app team policy (app_group_policies: admin | member | anyone). The caller becomes the owner and is granted a system 'leader' role holding every team permission. New teams default to the app's default membership policy unless overridden. */
2914
2957
  createTeam: Group;
2915
2958
  /** Create a custom (non-system) team role granting the given team permission keys. Requires the 'manage_roles' team permission (app admins bypass). Permission keys must be valid team permission keys (group_permission_defs). */
@@ -2942,6 +2985,8 @@ export type Mutation = {
2942
2985
  destroyEnvironment: CksEnvironmentChangeOrder;
2943
2986
  /** Close the UDP proxy session and socket for this game token. Unsubscribing from udpNotifications does not disconnect; use this mutation (or rely on server inactivity timeout). */
2944
2987
  disconnectUdpProxy: Scalars['Boolean']['output'];
2988
+ /** Exchange a one-time portal authorization code (with the matching PKCE verifier) for an app-scoped gameplay token. Public (the code + verifier authorize the call); called by the destination game at its own origin so the game never sees the player's session token. */
2989
+ exchangePortalCode: AppTokenResponse;
2945
2990
  /** ADMIN/DESTRUCTIVE: revokes ALL of the target user’s sessions by deleting every game_token row, forcing re-authentication on every device. Returns true if at least one session was revoked. Requires a super-admin bearer game token (and the management API enabled). NOTE: management-owned in cks-game-api (throws ForbiddenException) — use cks-management-api. */
2946
2991
  forceLogoutUser: Scalars['Boolean']['output'];
2947
2992
  /** Create a directed relationship edge between two containers (the game model is a graph), with a relationship type and optional weight. Requires a valid token. */
@@ -3016,6 +3061,8 @@ export type Mutation = {
3016
3061
  logout: Scalars['Boolean']['output'];
3017
3062
  /** Ends every active session for the authenticated user (deletes all their game_tokens and records revocations). Requires a valid session token. Use logout to end only the current session. */
3018
3063
  logoutAllDevices: Scalars['Boolean']['output'];
3064
+ /** Mint a short-lived, app-scoped gameplay token for the calling user (native/direct path; no browser redirect). Requires an identity SESSION token (app tokens cannot mint). Free/open apps auto-grant access; paid apps require an existing entitlement (else FORBIDDEN). Side effect: may create an app_user_access row on the app's free default tier. */
3065
+ mintAppToken: AppTokenResponse;
3019
3066
  /** Publishes an app to the shared game-api environment. Free under the org's app-slot quota (result.free = true); beyond the quota, publish still succeeds and hourly usage is debited from the org wallet. Requires the 'manage_apps' permission on the app's org. Blocked when SHARED_GAME_API_URL is not configured. */
3020
3067
  publishAppToShared: PublishAppResult;
3021
3068
  /** Operator only (is_operator). Cuts a new environment release from a cks-game-api git tag: ingests it as available and commits the manifest to the git ref. SIDE EFFECT: makes the version the new redeploy target and writes to GitHub. Use force to overwrite. Writes an audit entry. */
@@ -3028,6 +3075,8 @@ export type Mutation = {
3028
3075
  putCpSecret: CpSecretRow;
3029
3076
  /** Redeploys the environment to a target release version (input.version) or, when omitted, the latest available version for its class, reusing its current flavors/scaling and linked apps. Preserves the environment URLs. No-op-safe: re-running when already at latest still redeploys. If a prior deploy failed but stayed in_progress, it is abandoned first so the redeploy can proceed. Requires the 'manage_environments' org permission. */
3030
3077
  redeployEnvironment: CksEnvironmentChangeOrder;
3078
+ /** Rotate the calling app token for a fresh one (same app, extended TTL) and revoke the old. Call before the current token expires to keep playing without bouncing back through the Overworld. Allowed for app-scoped tokens; re-checks entitlement. */
3079
+ refreshAppToken: AppTokenResponse;
3031
3080
  /** Creates a new (initially unconfirmed) account, sends a confirmation email, and returns an AuthResponse with a session `token` for immediate login (send as `Authorization: Bearer <token>`). Public; throws if the email already exists. */
3032
3081
  register: AuthResponse;
3033
3082
  /** Remove a member from a channel. Requires the 'manage_members' channel permission, except that any member may remove themselves. Notifies Buddy to stop routing to the removed member. Returns true if a membership was removed. */
@@ -3237,6 +3286,9 @@ export type MutationCreateOrgTokenArgs = {
3237
3286
  export type MutationCreateOrganizationArgs = {
3238
3287
  input: CreateOrganizationInput;
3239
3288
  };
3289
+ export type MutationCreatePortalAuthorizationCodeArgs = {
3290
+ input: CreatePortalAuthorizationCodeInput;
3291
+ };
3240
3292
  export type MutationCreateTeamArgs = {
3241
3293
  input: CreateTeamInput;
3242
3294
  };
@@ -3285,6 +3337,9 @@ export type MutationDeleteUserAppStateArgs = {
3285
3337
  export type MutationDestroyEnvironmentArgs = {
3286
3338
  input: DestroyEnvironmentInput;
3287
3339
  };
3340
+ export type MutationExchangePortalCodeArgs = {
3341
+ input: ExchangePortalCodeInput;
3342
+ };
3288
3343
  export type MutationForceLogoutUserArgs = {
3289
3344
  userId: Scalars['BigInt']['input'];
3290
3345
  };
@@ -3397,6 +3452,9 @@ export type MutationLinkAppToEnvironmentArgs = {
3397
3452
  export type MutationLoginArgs = {
3398
3453
  loginUserInput: LoginUserInput;
3399
3454
  };
3455
+ export type MutationMintAppTokenArgs = {
3456
+ input: MintAppTokenInput;
3457
+ };
3400
3458
  export type MutationPublishAppToSharedArgs = {
3401
3459
  appId: Scalars['BigInt']['input'];
3402
3460
  cancelUrl?: InputMaybe<Scalars['String']['input']>;
@@ -3944,6 +4002,16 @@ export type PlayerPulse = {
3944
4002
  /** Number of studios in the percentile comparison pool (studios with all_time_peak > 0). */
3945
4003
  poolSize: Scalars['Int']['output'];
3946
4004
  };
4005
+ /** A one-time portal authorization code. Redirect the player to `redirectUri` carrying `code`; the destination game exchanges it (with its PKCE verifier) via exchangePortalCode for an app token. Single-use and short-lived. */
4006
+ export type PortalAuthorizationCode = {
4007
+ __typename?: 'PortalAuthorizationCode';
4008
+ /** The one-time authorization code. Deliver it to the destination game origin only (e.g. as a `code` query param on redirectUri). */
4009
+ code: Scalars['String']['output'];
4010
+ /** ISO-8601 UTC expiry of the code (typically ~60s). */
4011
+ expiresAt: Scalars['String']['output'];
4012
+ /** The validated redirect URI the player should be sent to. */
4013
+ redirectUri: Scalars['String']['output'];
4014
+ };
3947
4015
  /** Postgres billing tier: bandwidth allotment and capacity charge. Usage metering deferred. */
3948
4016
  export type PostgresBillingTier = {
3949
4017
  __typename?: 'PostgresBillingTier';
@@ -5320,6 +5388,8 @@ export declare enum UdpErrorCode {
5320
5388
  PasswordTooLong = "PASSWORD_TOO_LONG",
5321
5389
  /** Password failed minimum-length validation. */
5322
5390
  PasswordTooShort = "PASSWORD_TOO_SHORT",
5391
+ /** The app-scoped gameplay token has expired. Refresh it (same app, via refreshAppToken) before it lapses, or re-portal through the Overworld for a fresh token, then re-authorize the realtime session. */
5392
+ TokenExpired = "TOKEN_EXPIRED",
5323
5393
  /** The caller lacks the runtime/grid permission required for this action. Grid permissions can load asynchronously, so the first message to a newly entered region may transiently return this — retry shortly. */
5324
5394
  Unauthorized = "UNAUTHORIZED",
5325
5395
  /** Unspecified server error (1). Retry; if it persists, report it. */