@crowdedkingdoms/crowdyjs 17.1.0-test.1 → 17.2.0-dev.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/MIGRATION.md CHANGED
@@ -1,3 +1,32 @@
1
+ # CrowdyJS v17.2 — publish to Crowdy Games; sign-in under the shell
2
+
3
+ **Additive.** `17.2.0` (2026-09-14). Tracks ck-api `v2.1.0` (third-party game
4
+ hosting). Nothing existing changes shape; two things are new.
5
+
6
+ - **`client.hosting`** wraps the hosting surface: `claim({ appId, slug? })`,
7
+ `beginPublish({ slug, files })`, `completePublish(slug, publishId)`,
8
+ `abandonPublish`, `setEnabled`, `publishes`, `mine`; public `game(slug)` and
9
+ `listed()`; operator `all()`, `setListing`, `takeDown`. Every mutation needs an
10
+ identity session with `manage_apps` (a game's own app token is refused, so a
11
+ bundle can never publish a replacement for itself). The new subpath
12
+ `@crowdedkingdoms/crowdyjs/hosting` exports `publishDirectory(client, { dir,
13
+ slug })` and `manifestForDirectory(dir)` for Node; the root exports
14
+ `uploadPublishFiles` for a browser tool.
15
+ - **`EmbeddedHost`** (`client.embeddedHost`, `CrowdyClientConfig.embeddedHost`):
16
+ when a game published to Crowdy Games runs inside the first-party shell's
17
+ iframe, `portal.signIn` learns the shell's return URL and Studio origin from a
18
+ `crowdyjs:host-hello`, uses the shell's page as `redirect_uri`, and asks the
19
+ shell to navigate (`crowdyjs:navigate`) instead of `location.assign`. The PKCE
20
+ verifier stays in the game origin; `handleSignInCallback` is unchanged because
21
+ the shell relays `?code=&state=` into the iframe's `src`. Detection is a bounded
22
+ hello (1.5 s) and falls back to the ordinary flow, so a self-hosted or
23
+ top-level game behaves exactly as before. `SignInParams.embedded: false` or
24
+ `createCrowdyClient({ embeddedHost: false })` opts out. `portal.embeddedHostInfo()`
25
+ tells a game whether it is under a shell.
26
+
27
+ No removals. Error codes added: `CONTENT_HOSTING_DISABLED`,
28
+ `HOSTED_SLUG_UNAVAILABLE`, `HOSTED_MANIFEST_INVALID`, `HOSTED_PUBLISH_INCOMPLETE`,
29
+ `HOSTED_GAME_TAKEN_DOWN`.
1
30
  # CrowdyJS v17.1 — binary-relay sends are bundled
2
31
 
3
32
  **Nothing removed.** `17.1.0` (2026-09-13). On the binary relay
package/README.md CHANGED
@@ -98,6 +98,19 @@ sign-in mutations are refused from a non-first-party browser origin with
98
98
  customer's domain that collects it is indistinguishable, to the platform and to
99
99
  the player, from a phishing page.
100
100
 
101
+ **A game published to Crowdy Games takes the first row too, with one twist the SDK
102
+ handles for you** (17.2.0). Such a game is reached at `https://<games host>/<slug>/`,
103
+ which is a first-party *shell* page, and runs inside that page's iframe on its own
104
+ origin, `https://<slug>.<content host>`. The iframe cannot navigate the tab and
105
+ Studio refuses to be framed, so `portal.signIn` asks the shell to navigate
106
+ (`crowdyjs:navigate`; the shell honours exactly the tier's Studio `/authorize`) and
107
+ uses the shell's page as the `redirect_uri` -- the shell then hands the returned
108
+ `?code=&state=` into the iframe, where `portal.handleSignInCallback` runs exactly as
109
+ it does self-hosted. Nothing changes in your code: the bridge (`client.embeddedHost`,
110
+ `EmbeddedHost`) detects the shell with a bounded hello and falls back to the
111
+ top-level flow when nobody answers. Pass `embeddedHost: false` to `createCrowdyClient`
112
+ to never consult a framing page.
113
+
101
114
  **Hosted sign-in** (browser game, own domain) is OAuth2 Authorization Code +
102
115
  PKCE; the verifier never leaves your origin and your page never sees a
103
116
  credential:
@@ -208,6 +221,7 @@ never told about, so a native refresh without it is a re-placement.
208
221
  | `client.users` | `me`, `updateGamertag`, profile reads. |
209
222
  | `client.session` | Token store, `restore()`, `getToken()`, manual `setToken()`. |
210
223
  | `client.portal` | App-scoped token minting (`mintAppToken`) and the cross-origin PKCE entry flow (`beginEntry` / `handleAuthorizeRequest` / `completeEntry` / `refresh`). |
224
+ | `client.hosting` | Third-party hosting on Crowdy Games (17.2.0): `claim` a slug for an app, `beginPublish` / `completePublish` / `abandonPublish` a built bundle, `setEnabled`; public `game(slug)` / `listed()`; operator `all` / `setListing` / `takeDown`. Mutations need an identity session with `manage_apps`. `@crowdedkingdoms/crowdyjs/hosting` exports `publishDirectory(client, { dir, slug })`, the Node helper that does the whole publish for a `dist/`. |
211
225
  | `client.platform` | Public platform configuration (`config()`). |
212
226
  | `client.serverStatus` | `gameClientBootstrap(appId)` — per-app version info, UDP status, spatial limits. |
213
227
  | `client.chunks`, `client.voxels`, `client.actors`, `client.avatars`, `client.state` | World data reads + writes: terrain/LODs, voxel edit + history/rollback, durable actors, avatars, per-user app state blobs. |
@@ -634,6 +648,38 @@ See the docs guides [Modeling game concepts](https://docs.crowdedkingdoms.com/ga
634
648
  (the SDK surface + the simulation-tier / notify-to-pull / timer / hidden-info
635
649
  / anti-cheat patterns).
636
650
 
651
+ ## Hosting a game on Crowdy Games
652
+
653
+ Since 17.2.0 (ck-api v2.1) a developer can publish a built static bundle to the
654
+ platform instead of hosting it: players reach it at
655
+ `https://<games host>/<slug>/` and it executes on an origin of its own,
656
+ `https://<slug>.<content host>`, behind the same security headers `the-construct`'s
657
+ `docs/HOSTING.md` asks a self-hoster to serve. The API returns both URLs
658
+ (`HostedGame.launchUrl`, `.contentOrigin`); nothing in your code names a host.
659
+
660
+ From Node, with an identity session (never from a browser page, by design):
661
+
662
+ ```ts
663
+ import { createCrowdyClient } from '@crowdedkingdoms/crowdyjs';
664
+ import { publishDirectory } from '@crowdedkingdoms/crowdyjs/hosting';
665
+
666
+ const client = createCrowdyClient();
667
+ await client.auth.login({ email, password }); // identity session, Node only
668
+ const game = await client.hosting.claim({ appId, slug: 'my-game' }); // once; idempotent
669
+ const result = await publishDirectory(client, { dir: 'dist', slug: game.slug });
670
+ console.log(result.game.launchUrl); // https://.../my-game/
671
+ ```
672
+
673
+ `claim` registers the shell page and the game origin as the app's redirect URIs
674
+ and sets `launch_url`; `publishDirectory` hashes `dist/`, declares the manifest
675
+ (`beginGamePublish` validates it as a whole and returns one presigned PUT per file),
676
+ uploads with the exact signed headers, and `completeGamePublish` verifies every
677
+ object, promotes it and invalidates the CDN. The slug is a DNS label, global on the
678
+ tier, and first-party names are reserved (`HOSTED_SLUG_UNAVAILABLE`); a tier without
679
+ a content CDN answers `CONTENT_HOSTING_DISABLED`. Being *listed* in the lobby is an
680
+ operator decision (`setListing`); publishing is self-serve. `the-construct`'s
681
+ `npm run publish` is this section as a command.
682
+
637
683
  ## Crowdy Studio
638
684
 
639
685
  Crowdy Studio is the in-game SERVER/CLIENT Rust authoring surface for player
@@ -32,6 +32,8 @@ import { GameKitClient, type GameKitOptions } from './kit/index.js';
32
32
  import { AuthAPI } from './domains/auth.js';
33
33
  import { UsersAPI } from './domains/users.js';
34
34
  import { AppsAPI } from './domains/apps.js';
35
+ import { EmbeddedHost } from './domains/embedded-host.js';
36
+ import { HostingAPI } from './domains/hosting.js';
35
37
  import { PortalAPI, type AppTokenResponse, type PkceStore } from './domains/portal.js';
36
38
  import { PlatformAPI } from './domains/platform.js';
37
39
  import { OrganizationsAPI } from './domains/organizations.js';
@@ -99,6 +101,15 @@ export interface CrowdyClientConfig {
99
101
  * or tests where sessionStorage is unavailable.
100
102
  */
101
103
  pkceStore?: PkceStore;
104
+ /**
105
+ * The Crowdy Games shell bridge (third-party hosting, 2026-09-13). When a game
106
+ * published to Crowdy Games runs inside the first-party shell's iframe,
107
+ * `portal.signIn` must ask the shell to navigate to Studio and must use the shell's
108
+ * page as the redirect URI; this bridge is how it learns both. Default: detect in a
109
+ * browser (`new EmbeddedHost()`), nothing in Node. Pass `false` to never consult a
110
+ * framing page, or your own instance to control timeouts.
111
+ */
112
+ embeddedHost?: EmbeddedHost | false;
102
113
  /** Realtime (WebSocket) tuning for reconnect backoff and `...AndWait` timeouts. */
103
114
  realtime?: {
104
115
  /** Max reconnect attempts before giving up (default tuned for browsers). */
@@ -203,6 +214,15 @@ export declare class CrowdyClient {
203
214
  * app token.
204
215
  */
205
216
  readonly portal: PortalAPI;
217
+ /**
218
+ * Third-party game hosting on Crowdy Games: claim a slug, publish a bundle, list
219
+ * hosted games. Mutations need an identity session with manage_apps; the reads a
220
+ * page needs are public. `@crowdedkingdoms/crowdyjs/hosting` has the Node helper
221
+ * that publishes a directory.
222
+ */
223
+ readonly hosting: HostingAPI;
224
+ /** The Crowdy Games shell bridge (see `CrowdyClientConfig.embeddedHost`); null in Node or when opted out. */
225
+ readonly embeddedHost: EmbeddedHost | null;
206
226
  /**
207
227
  * Where each app lives. Unauthenticated, and meant to be called BEFORE login against
208
228
  * the shared origin, so the session and the app token are both written in the app's
@@ -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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"}
@@ -33,6 +33,8 @@ import { GameKitClient } from './kit/index.js';
33
33
  import { AuthAPI } from './domains/auth.js';
34
34
  import { UsersAPI } from './domains/users.js';
35
35
  import { AppsAPI } from './domains/apps.js';
36
+ import { EmbeddedHost } from './domains/embedded-host.js';
37
+ import { HostingAPI } from './domains/hosting.js';
36
38
  import { PortalAPI, } from './domains/portal.js';
37
39
  import { PlatformAPI } from './domains/platform.js';
38
40
  import { OrganizationsAPI } from './domains/organizations.js';
@@ -184,7 +186,12 @@ export class CrowdyClient {
184
186
  this.auth = new AuthAPI(this.graphql, this.session);
185
187
  this.users = new UsersAPI(this.graphql);
186
188
  this.apps = new AppsAPI(this.graphql);
187
- this.portal = new PortalAPI(this.graphql, this.session, config.pkceStore);
189
+ this.embeddedHost =
190
+ config.embeddedHost === false
191
+ ? null
192
+ : config.embeddedHost ?? (typeof globalThis.window !== 'undefined' ? new EmbeddedHost() : null);
193
+ this.portal = new PortalAPI(this.graphql, this.session, config.pkceStore, this.embeddedHost);
194
+ this.hosting = new HostingAPI(this.graphql);
188
195
  this.discovery = new DiscoveryDomain(this.graphql);
189
196
  this.platform = new PlatformAPI(this.graphql);
190
197
  this.organizations = new OrganizationsAPI(this.graphql);
@@ -343,6 +350,7 @@ export class CrowdyClient {
343
350
  close() {
344
351
  this.realtime.close();
345
352
  this.session.setToken(null);
353
+ this.embeddedHost?.close();
346
354
  }
347
355
  /** The configured or discovered GraphQL endpoint URL. */
348
356
  get graphqlEndpoint() {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * GENERATED — do not edit. The operator tooling's
3
- * `sync-client-origins.mjs --write --tier test` writes this file, and
3
+ * `sync-client-origins.mjs --write --tier dev` writes this file, and
4
4
  * `check-sdk-default-origin.mjs` refuses it when it names the wrong tier or a
5
5
  * host that declaration does not carry. Regenerate; never hand-edit.
6
6
  *
@@ -10,14 +10,14 @@
10
10
  * than no default at all. Every fallback this SDK ever removed named a host that
11
11
  * was already dead. A generated file plus a gate is what makes this one different.
12
12
  *
13
- * Source: the operator's per-tier public CK API origin declaration, tier 'test'
13
+ * Source: the operator's per-tier public CK API origin declaration, tier 'dev'
14
14
  */
15
15
  /** The tier this build of the SDK is published for. */
16
- export declare const CROWDY_DEFAULT_TIER = "test";
16
+ export declare const CROWDY_DEFAULT_TIER = "dev";
17
17
  /** The public CK API origin for that tier. */
18
- export declare const CROWDY_DEFAULT_HTTP_ORIGIN = "https://ck.test.crowdedkingdoms.com";
18
+ export declare const CROWDY_DEFAULT_HTTP_ORIGIN = "https://ck.dev.crowdedkingdoms.com";
19
19
  /** The same host over WebSocket. A scheme is composed; a hostname is looked up. */
20
- export declare const CROWDY_DEFAULT_WS_ORIGIN = "wss://ck.test.crowdedkingdoms.com";
20
+ export declare const CROWDY_DEFAULT_WS_ORIGIN = "wss://ck.dev.crowdedkingdoms.com";
21
21
  /** The bare hostname, for callers that need to compare rather than dial. */
22
- export declare const CROWDY_DEFAULT_HOST = "ck.test.crowdedkingdoms.com";
22
+ export declare const CROWDY_DEFAULT_HOST = "ck.dev.crowdedkingdoms.com";
23
23
  //# sourceMappingURL=default-origin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"default-origin.d.ts","sourceRoot":"","sources":["../src/default-origin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,uDAAuD;AACvD,eAAO,MAAM,mBAAmB,SAAS,CAAC;AAE1C,8CAA8C;AAC9C,eAAO,MAAM,0BAA0B,wCAAwC,CAAC;AAEhF,mFAAmF;AACnF,eAAO,MAAM,wBAAwB,sCAAsC,CAAC;AAE5E,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB,gCAAgC,CAAC"}
1
+ {"version":3,"file":"default-origin.d.ts","sourceRoot":"","sources":["../src/default-origin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,uDAAuD;AACvD,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAEzC,8CAA8C;AAC9C,eAAO,MAAM,0BAA0B,uCAAuC,CAAC;AAE/E,mFAAmF;AACnF,eAAO,MAAM,wBAAwB,qCAAqC,CAAC;AAE3E,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB,+BAA+B,CAAC"}
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * GENERATED — do not edit. The operator tooling's
3
- * `sync-client-origins.mjs --write --tier test` writes this file, and
3
+ * `sync-client-origins.mjs --write --tier dev` writes this file, and
4
4
  * `check-sdk-default-origin.mjs` refuses it when it names the wrong tier or a
5
5
  * host that declaration does not carry. Regenerate; never hand-edit.
6
6
  *
@@ -10,13 +10,13 @@
10
10
  * than no default at all. Every fallback this SDK ever removed named a host that
11
11
  * was already dead. A generated file plus a gate is what makes this one different.
12
12
  *
13
- * Source: the operator's per-tier public CK API origin declaration, tier 'test'
13
+ * Source: the operator's per-tier public CK API origin declaration, tier 'dev'
14
14
  */
15
15
  /** The tier this build of the SDK is published for. */
16
- export const CROWDY_DEFAULT_TIER = 'test';
16
+ export const CROWDY_DEFAULT_TIER = 'dev';
17
17
  /** The public CK API origin for that tier. */
18
- export const CROWDY_DEFAULT_HTTP_ORIGIN = 'https://ck.test.crowdedkingdoms.com';
18
+ export const CROWDY_DEFAULT_HTTP_ORIGIN = 'https://ck.dev.crowdedkingdoms.com';
19
19
  /** The same host over WebSocket. A scheme is composed; a hostname is looked up. */
20
- export const CROWDY_DEFAULT_WS_ORIGIN = 'wss://ck.test.crowdedkingdoms.com';
20
+ export const CROWDY_DEFAULT_WS_ORIGIN = 'wss://ck.dev.crowdedkingdoms.com';
21
21
  /** The bare hostname, for callers that need to compare rather than dial. */
22
- export const CROWDY_DEFAULT_HOST = 'ck.test.crowdedkingdoms.com';
22
+ export const CROWDY_DEFAULT_HOST = 'ck.dev.crowdedkingdoms.com';
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The embedded-host bridge: how a game framed by the Crowdy Games SHELL signs in.
3
+ *
4
+ * A game published to Crowdy Games (`client.hosting`, the-construct's `npm run
5
+ * publish`) is reached at `https://<games host>/<slug>/`, which is a first-party
6
+ * shell page, and executes inside that page's iframe on its own origin,
7
+ * `https://<slug>.<content host>`. Two things about that arrangement change hosted
8
+ * sign-in, and this module is the whole of the change:
9
+ *
10
+ * 1. The game cannot navigate the tab. Its iframe has no `allow-top-navigation`
11
+ * (a game that could send the player to a lookalike sign-in page would have the
12
+ * phishing surface the shell exists to remove), and Studio refuses to be framed.
13
+ * So `portal.signIn` asks the SHELL to navigate -- `crowdyjs:navigate` -- and
14
+ * the shell honours exactly one destination: the tier's Studio `/authorize`.
15
+ *
16
+ * 2. The redirect URI is the SHELL's page, not the iframe's. Studio sends the code
17
+ * back to `https://<games host>/<slug>/` (the app's registered redirect URI, which
18
+ * `claimGameHosting` registered); the shell moves `?code=&state=` onto the iframe's
19
+ * `src` and strips it from its own address bar; the game's
20
+ * `handleSignInCallback()` then reads its own `location.search` exactly as it does
21
+ * when self-hosted. The shell tells the game which return URL to use in
22
+ * `crowdyjs:host-hello`.
23
+ *
24
+ * WHAT DOES NOT CHANGE. The PKCE pair is generated here, in the game's origin, and the
25
+ * verifier stays in the game origin's sessionStorage. The shell relays a code it cannot
26
+ * spend. The app token is exchanged and stored by the game as before.
27
+ *
28
+ * WHY TRUSTING THE PARENT IS SAFE. The bridge accepts `crowdyjs:host-hello` only from
29
+ * `window.parent`. When the game is served by the Crowdy Games content edge, its
30
+ * `frame-ancestors` names the tier's games host and nothing else, so the parent IS the
31
+ * shell. A self-hosted game framed by a hostile page could be told a hostile
32
+ * `returnUrl` -- and Studio would refuse it, because it is not one of the app's
33
+ * registered redirect URIs; and even a code delivered to a registered-but-wrong page
34
+ * is useless without the verifier in this origin. The bridge also refuses a
35
+ * `returnUrl` whose origin is not https (or the parent's own origin, for a local dev
36
+ * shell), so the failure is loud rather than a silent detour.
37
+ *
38
+ * The message shapes mirror Crowdy-Games `shell/src/protocol.ts` (version 1).
39
+ */
40
+ export declare const EMBEDDED_HOST_PROTOCOL_VERSION = 1;
41
+ export interface HostHelloMessage {
42
+ type: 'crowdyjs:host-hello';
43
+ version: number;
44
+ /** The URL hosted sign-in must return to: the shell's page for this game. */
45
+ returnUrl: string;
46
+ /** The Studio origin the shell will navigate to for `/authorize`. */
47
+ authorizeOrigin: string;
48
+ /** The slug the shell is showing. */
49
+ slug: string;
50
+ }
51
+ export interface HostHelloRequestMessage {
52
+ type: 'crowdyjs:host-hello-request';
53
+ version: number;
54
+ }
55
+ export interface NavigateMessage {
56
+ type: 'crowdyjs:navigate';
57
+ version: number;
58
+ url: string;
59
+ }
60
+ /** What the game learned from the shell. */
61
+ export interface EmbeddedHostInfo {
62
+ /** The shell's origin (`event.origin` of the hello) -- the only target navigate posts to. */
63
+ hostOrigin: string;
64
+ returnUrl: string;
65
+ authorizeOrigin: string;
66
+ slug: string;
67
+ }
68
+ /** The DOM surface the bridge needs, narrow so tests can hand it a fake. */
69
+ export interface EmbeddedWindow {
70
+ parent: unknown;
71
+ addEventListener(type: 'message', listener: (event: EmbeddedMessageEvent) => void): void;
72
+ removeEventListener(type: 'message', listener: (event: EmbeddedMessageEvent) => void): void;
73
+ location?: {
74
+ origin?: string;
75
+ };
76
+ }
77
+ export interface EmbeddedMessageEvent {
78
+ data: unknown;
79
+ origin: string;
80
+ source: unknown;
81
+ }
82
+ export interface ParentWindow {
83
+ postMessage(message: unknown, targetOrigin: string): void;
84
+ }
85
+ export interface EmbeddedHostOptions {
86
+ /** How long `hello()` waits for the shell before deciding the game is not framed by one. */
87
+ helloTimeoutMs?: number;
88
+ }
89
+ /**
90
+ * Detects and talks to a Crowdy Games shell framing this page. Constructed once by
91
+ * `createCrowdyClient` (see `CrowdyClientConfig.embeddedHost`); `portal.signIn`
92
+ * consults it.
93
+ */
94
+ export declare class EmbeddedHost {
95
+ private readonly win;
96
+ private info;
97
+ private pending;
98
+ private readonly listener;
99
+ private readonly helloTimeoutMs;
100
+ constructor(win?: EmbeddedWindow | null, options?: EmbeddedHostOptions);
101
+ /** True when this page is inside another (any) frame. Not the same as "framed by a shell". */
102
+ isFramed(): boolean;
103
+ /** The shell's facts, once a hello has arrived; null before that or when not framed. */
104
+ current(): EmbeddedHostInfo | null;
105
+ /**
106
+ * Ask the parent to identify itself and wait (bounded) for a hello. Resolves null
107
+ * when this page is not framed, or the parent never answers -- which is how a game
108
+ * that is simply embedded in somebody's blog behaves like a top-level page.
109
+ */
110
+ hello(timeoutMs?: number): Promise<EmbeddedHostInfo | null>;
111
+ /**
112
+ * Ask the shell to take the tab to `url`. The shell honours only its tier's Studio
113
+ * `/authorize` with a redirect_uri of its own page; anything else is dropped there.
114
+ */
115
+ navigate(url: string): void;
116
+ close(): void;
117
+ private onHello;
118
+ private onMessage;
119
+ /**
120
+ * The return URL must be a page ON THE SHELL'S OWN ORIGIN: the shell is the page
121
+ * that will receive the code, so a hello naming anywhere else is malformed (or
122
+ * hostile) and is ignored rather than acted on.
123
+ */
124
+ private acceptableReturnUrl;
125
+ }
126
+ //# sourceMappingURL=embedded-host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedded-host.d.ts","sourceRoot":"","sources":["../../src/domains/embedded-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,eAAO,MAAM,8BAA8B,IAAI,CAAC;AAEhD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,qBAAqB,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,eAAe,EAAE,MAAM,CAAC;IACxB,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,6BAA6B,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,4CAA4C;AAC5C,MAAM,WAAW,gBAAgB;IAC/B,6FAA6F;IAC7F,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,4EAA4E;AAC5E,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,OAAO,CAAC;IAChB,gBAAgB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,IAAI,CAAC;IACzF,mBAAmB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,IAAI,CAAC;IAC5F,QAAQ,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAChC;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3D;AAED,MAAM,WAAW,mBAAmB;IAClC,4FAA4F;IAC5F,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAeD;;;;GAIG;AACH,qBAAa,YAAY;IAOrB,OAAO,CAAC,QAAQ,CAAC,GAAG;IANtB,OAAO,CAAC,IAAI,CAAiC;IAC7C,OAAO,CAAC,OAAO,CAAiD;IAChE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwC;IACjE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;gBAGrB,GAAG,GAAE,cAAc,GAAG,IAAsB,EAC7D,OAAO,GAAE,mBAAwB;IASnC,8FAA8F;IAC9F,QAAQ,IAAI,OAAO;IAUnB,wFAAwF;IACxF,OAAO,IAAI,gBAAgB,GAAG,IAAI;IAIlC;;;;OAIG;IACH,KAAK,CAAC,SAAS,SAAsB,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IA2BxE;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAU3B,KAAK,IAAI,IAAI;IAIb,OAAO,CAAC,OAAO,CAA6B;IAE5C,OAAO,CAAC,SAAS;IAmBjB;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;CAQ5B"}
@@ -0,0 +1,175 @@
1
+ /**
2
+ * The embedded-host bridge: how a game framed by the Crowdy Games SHELL signs in.
3
+ *
4
+ * A game published to Crowdy Games (`client.hosting`, the-construct's `npm run
5
+ * publish`) is reached at `https://<games host>/<slug>/`, which is a first-party
6
+ * shell page, and executes inside that page's iframe on its own origin,
7
+ * `https://<slug>.<content host>`. Two things about that arrangement change hosted
8
+ * sign-in, and this module is the whole of the change:
9
+ *
10
+ * 1. The game cannot navigate the tab. Its iframe has no `allow-top-navigation`
11
+ * (a game that could send the player to a lookalike sign-in page would have the
12
+ * phishing surface the shell exists to remove), and Studio refuses to be framed.
13
+ * So `portal.signIn` asks the SHELL to navigate -- `crowdyjs:navigate` -- and
14
+ * the shell honours exactly one destination: the tier's Studio `/authorize`.
15
+ *
16
+ * 2. The redirect URI is the SHELL's page, not the iframe's. Studio sends the code
17
+ * back to `https://<games host>/<slug>/` (the app's registered redirect URI, which
18
+ * `claimGameHosting` registered); the shell moves `?code=&state=` onto the iframe's
19
+ * `src` and strips it from its own address bar; the game's
20
+ * `handleSignInCallback()` then reads its own `location.search` exactly as it does
21
+ * when self-hosted. The shell tells the game which return URL to use in
22
+ * `crowdyjs:host-hello`.
23
+ *
24
+ * WHAT DOES NOT CHANGE. The PKCE pair is generated here, in the game's origin, and the
25
+ * verifier stays in the game origin's sessionStorage. The shell relays a code it cannot
26
+ * spend. The app token is exchanged and stored by the game as before.
27
+ *
28
+ * WHY TRUSTING THE PARENT IS SAFE. The bridge accepts `crowdyjs:host-hello` only from
29
+ * `window.parent`. When the game is served by the Crowdy Games content edge, its
30
+ * `frame-ancestors` names the tier's games host and nothing else, so the parent IS the
31
+ * shell. A self-hosted game framed by a hostile page could be told a hostile
32
+ * `returnUrl` -- and Studio would refuse it, because it is not one of the app's
33
+ * registered redirect URIs; and even a code delivered to a registered-but-wrong page
34
+ * is useless without the verifier in this origin. The bridge also refuses a
35
+ * `returnUrl` whose origin is not https (or the parent's own origin, for a local dev
36
+ * shell), so the failure is loud rather than a silent detour.
37
+ *
38
+ * The message shapes mirror Crowdy-Games `shell/src/protocol.ts` (version 1).
39
+ */
40
+ export const EMBEDDED_HOST_PROTOCOL_VERSION = 1;
41
+ const DEFAULT_HELLO_TIMEOUT_MS = 1500;
42
+ function isHostHello(data) {
43
+ if (typeof data !== 'object' || data === null)
44
+ return false;
45
+ const d = data;
46
+ return (d.type === 'crowdyjs:host-hello' &&
47
+ typeof d.returnUrl === 'string' &&
48
+ typeof d.authorizeOrigin === 'string' &&
49
+ typeof d.slug === 'string');
50
+ }
51
+ /**
52
+ * Detects and talks to a Crowdy Games shell framing this page. Constructed once by
53
+ * `createCrowdyClient` (see `CrowdyClientConfig.embeddedHost`); `portal.signIn`
54
+ * consults it.
55
+ */
56
+ export class EmbeddedHost {
57
+ constructor(win = defaultWindow(), options = {}) {
58
+ this.win = win;
59
+ this.info = null;
60
+ this.pending = null;
61
+ this.onHello = null;
62
+ this.helloTimeoutMs = options.helloTimeoutMs ?? DEFAULT_HELLO_TIMEOUT_MS;
63
+ this.listener = (event) => this.onMessage(event);
64
+ if (this.isFramed()) {
65
+ this.win.addEventListener('message', this.listener);
66
+ }
67
+ }
68
+ /** True when this page is inside another (any) frame. Not the same as "framed by a shell". */
69
+ isFramed() {
70
+ const w = this.win;
71
+ if (!w)
72
+ return false;
73
+ try {
74
+ return w.parent !== null && w.parent !== undefined && w.parent !== w;
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ /** The shell's facts, once a hello has arrived; null before that or when not framed. */
81
+ current() {
82
+ return this.info;
83
+ }
84
+ /**
85
+ * Ask the parent to identify itself and wait (bounded) for a hello. Resolves null
86
+ * when this page is not framed, or the parent never answers -- which is how a game
87
+ * that is simply embedded in somebody's blog behaves like a top-level page.
88
+ */
89
+ hello(timeoutMs = this.helloTimeoutMs) {
90
+ if (!this.isFramed())
91
+ return Promise.resolve(null);
92
+ if (this.info)
93
+ return Promise.resolve(this.info);
94
+ if (this.pending)
95
+ return this.pending;
96
+ this.pending = new Promise((resolve) => {
97
+ const timer = setTimeout(() => {
98
+ this.pending = null;
99
+ resolve(this.info);
100
+ }, timeoutMs);
101
+ const check = () => {
102
+ if (this.info) {
103
+ clearTimeout(timer);
104
+ this.pending = null;
105
+ resolve(this.info);
106
+ }
107
+ };
108
+ this.onHello = check;
109
+ const request = {
110
+ type: 'crowdyjs:host-hello-request',
111
+ version: EMBEDDED_HOST_PROTOCOL_VERSION,
112
+ };
113
+ // '*': the request carries nothing and the parent's origin is not yet known.
114
+ this.win.parent.postMessage(request, '*');
115
+ });
116
+ return this.pending;
117
+ }
118
+ /**
119
+ * Ask the shell to take the tab to `url`. The shell honours only its tier's Studio
120
+ * `/authorize` with a redirect_uri of its own page; anything else is dropped there.
121
+ */
122
+ navigate(url) {
123
+ if (!this.info)
124
+ throw new Error('embedded host: no shell has said hello; call hello() first');
125
+ const message = {
126
+ type: 'crowdyjs:navigate',
127
+ version: EMBEDDED_HOST_PROTOCOL_VERSION,
128
+ url,
129
+ };
130
+ this.win.parent.postMessage(message, this.info.hostOrigin);
131
+ }
132
+ close() {
133
+ this.win?.removeEventListener('message', this.listener);
134
+ }
135
+ onMessage(event) {
136
+ if (!this.win || event.source !== this.win.parent)
137
+ return;
138
+ if (!isHostHello(event.data))
139
+ return;
140
+ if (!this.acceptableReturnUrl(event.data.returnUrl, event.origin))
141
+ return;
142
+ let authorizeOrigin;
143
+ try {
144
+ authorizeOrigin = new URL(event.data.authorizeOrigin).origin;
145
+ }
146
+ catch {
147
+ return;
148
+ }
149
+ this.info = {
150
+ hostOrigin: event.origin,
151
+ returnUrl: event.data.returnUrl,
152
+ authorizeOrigin,
153
+ slug: event.data.slug,
154
+ };
155
+ this.onHello?.();
156
+ }
157
+ /**
158
+ * The return URL must be a page ON THE SHELL'S OWN ORIGIN: the shell is the page
159
+ * that will receive the code, so a hello naming anywhere else is malformed (or
160
+ * hostile) and is ignored rather than acted on.
161
+ */
162
+ acceptableReturnUrl(returnUrl, hostOrigin) {
163
+ try {
164
+ const u = new URL(returnUrl);
165
+ return u.origin === hostOrigin && (u.protocol === 'https:' || u.hostname === 'localhost' || u.hostname === '127.0.0.1');
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ }
171
+ }
172
+ function defaultWindow() {
173
+ const g = globalThis;
174
+ return g.window ?? null;
175
+ }
@@ -0,0 +1,75 @@
1
+ import type { GraphQLClient } from '../client.js';
2
+ import { type BeginGamePublishMutation, type CompleteGamePublishMutation, type HostedGameFieldsFragment, type HostedGamePublishFieldsFragment, type PublishFileInput } from '../generated/graphql.js';
3
+ export type HostedGame = HostedGameFieldsFragment;
4
+ export type HostedGamePublish = HostedGamePublishFieldsFragment;
5
+ export type BeginGamePublishResult = BeginGamePublishMutation['beginGamePublish'];
6
+ export type HostedGameUpload = BeginGamePublishResult['uploads'][number];
7
+ export type CompleteGamePublishResult = CompleteGamePublishMutation['completeGamePublish'];
8
+ export type { PublishFileInput };
9
+ /**
10
+ * Third-party game hosting on Crowdy Games (ck-api v2.1, 2026-09-13).
11
+ *
12
+ * A developer publishes a built static bundle to the platform; players reach it at
13
+ * `https://<games host>/<slug>/` (a first-party shell page) while it executes on
14
+ * `https://<slug>.<content host>`, an origin of its own. The API returns both URLs
15
+ * (`launchUrl`, `contentOrigin`); nothing here spells a hostname.
16
+ *
17
+ * WHICH TOKEN. Every mutation here needs an IDENTITY SESSION (`client.auth.login`
18
+ * from Node, or a Studio session) with `manage_apps` on the app -- never an app
19
+ * token. A game's own bundle can therefore never publish a replacement for itself.
20
+ * The two reads a page needs (`game`, `listed`) are public.
21
+ *
22
+ * THE FLOW: `claim` once per app -> build -> `beginPublish` with the manifest ->
23
+ * PUT every file to its presigned URL with exactly the returned headers ->
24
+ * `completePublish`. `@crowdedkingdoms/crowdyjs/hosting`'s `publishDirectory` does
25
+ * the last three steps for a `dist/` directory in Node.
26
+ */
27
+ export declare class HostingAPI {
28
+ private readonly api;
29
+ constructor(api: GraphQLClient);
30
+ /** Public: a hosted game by slug, or null. The shell's question. */
31
+ game(slug: string): Promise<HostedGame | null>;
32
+ /** Public: the LIVE, LISTED hosted games -- the lobby's list. */
33
+ listed(): Promise<HostedGame[]>;
34
+ /** Operator: every hosted game on the tier. */
35
+ all(): Promise<HostedGame[]>;
36
+ /** The hosted games the caller can manage. Session token. */
37
+ mine(): Promise<HostedGame[]>;
38
+ /** Publish history for a game, newest first. Session token + manage_apps. */
39
+ publishes(slug: string, limit?: number): Promise<HostedGamePublish[]>;
40
+ /**
41
+ * Claim (or re-assert) the hosting slug for an app. Idempotent; a different slug
42
+ * MOVES the game. Registers the shell page and the game origin as redirect URIs
43
+ * and sets `launch_url`. Refuses reserved, taken or malformed slugs
44
+ * (`HOSTED_SLUG_UNAVAILABLE`) and a tier without hosting (`CONTENT_HOSTING_DISABLED`).
45
+ */
46
+ claim(params: {
47
+ appId: string;
48
+ slug?: string;
49
+ }): Promise<HostedGame>;
50
+ /** Declare a publish; returns one presigned PUT per file. */
51
+ beginPublish(params: {
52
+ slug: string;
53
+ files: PublishFileInput[];
54
+ }): Promise<BeginGamePublishResult>;
55
+ /** Verify, promote, record LIVE, invalidate. */
56
+ completePublish(slug: string, publishId: string): Promise<CompleteGamePublishResult>;
57
+ abandonPublish(slug: string, publishId: string): Promise<HostedGamePublish>;
58
+ /** Developer switch: off (DISABLED) or on (LIVE). */
59
+ setEnabled(slug: string, enabled: boolean): Promise<HostedGame>;
60
+ /** Operator: list or unlist in the lobby and the management UI. */
61
+ setListing(slug: string, listed: boolean): Promise<HostedGame>;
62
+ /** Operator: take down (or restore with `false`). */
63
+ takeDown(slug: string, takenDown?: boolean): Promise<HostedGame>;
64
+ }
65
+ /**
66
+ * Upload every file of a publish to its presigned URL, with bounded concurrency and
67
+ * a retry per file. Works in Node 20+ and in a browser (`fetch`, `Blob`/`Uint8Array`
68
+ * bodies). `read(path)` returns the bytes for a manifest path.
69
+ */
70
+ export declare function uploadPublishFiles(uploads: HostedGameUpload[], read: (path: string) => Promise<Uint8Array | Blob>, options?: {
71
+ concurrency?: number;
72
+ retries?: number;
73
+ onFile?: (path: string, ok: boolean) => void;
74
+ }): Promise<void>;
75
+ //# sourceMappingURL=hosting.d.ts.map