@crowdedkingdoms/construct 0.3.8 → 0.3.9-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/README.md CHANGED
@@ -30,7 +30,7 @@ Releases are on npm under one dist-tag per tier, and a game pins the exact
30
30
  version of its own tier, as it pins CrowdyJS:
31
31
 
32
32
  ```bash
33
- npm install --save-exact @crowdedkingdoms/construct # X.Y.Z on latest
33
+ npm install --save-exact @crowdedkingdoms/construct@dev # an X.Y.Z-dev.N build
34
34
  ```
35
35
 
36
36
  `dev` is `X.Y.Z-dev.N`, `test` is `X.Y.Z-test.N`, and `prod` is `X.Y.Z` on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdedkingdoms/construct",
3
- "version": "0.3.8",
3
+ "version": "0.3.9-dev.1",
4
4
  "description": "The Construct framework: the Crowded Kingdoms game platform layer (hosted sign-in, World Stores, voice/webcam/chat, Crowdy Studio dock, app and grid mods, grid programs) and engine loop, shared by the-construct starter and the games built on it. Ships TypeScript source for Vite.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -5,8 +5,16 @@ vi.mock('@crowdedkingdoms/crowdyjs', () => ({
5
5
  CROWDY_DEFAULT_TIER: 'test',
6
6
  }));
7
7
 
8
- const { ENV_HANDLE, envHandleFor, envScopedKey, ensureEnvScope, readScoped, writeScoped } =
9
- await import('./envScope');
8
+ const {
9
+ ENV_HANDLE,
10
+ appTokenStorageKey,
11
+ envHandleFor,
12
+ envScopedKey,
13
+ ensureEnvScope,
14
+ perGameBase,
15
+ readScoped,
16
+ writeScoped,
17
+ } = await import('./envScope');
10
18
 
11
19
  describe('envScope', () => {
12
20
  beforeEach(() => localStorage.clear());
@@ -23,6 +31,19 @@ describe('envScope', () => {
23
31
  expect(envScopedKey('x', 'other')).toBe('x:other');
24
32
  });
25
33
 
34
+ it('keys a saved app token by game path and API host', () => {
35
+ expect(appTokenStorageKey('/matchstick/')).toBe('crowdyjs:app:/matchstick/:ck.test.example');
36
+ expect(appTokenStorageKey('/blocks-with-friends/', 'ck.dev.example')).toBe(
37
+ 'crowdyjs:app:/blocks-with-friends/:ck.dev.example',
38
+ );
39
+ expect(appTokenStorageKey('/matchstick/', 'ck.dev.example')).not.toBe(
40
+ appTokenStorageKey('/other-game/', 'ck.dev.example'),
41
+ );
42
+ expect(perGameBase('construct:app-route', '/matchstick/')).toBe(
43
+ 'construct:app-route:/matchstick/',
44
+ );
45
+ });
46
+
26
47
  it('drops per-environment values when the environment changes', () => {
27
48
  localStorage.setItem('construct:env-handle', 'ck.old.example');
28
49
  localStorage.setItem('construct:app-id:ck.old.example', '111');
@@ -30,6 +30,29 @@ export function envScopedKey(base: string, handle: string = ENV_HANDLE): string
30
30
  return `${base}:${handle}`;
31
31
  }
32
32
 
33
+ /**
34
+ * `<base>:<game-path>`, for `readScoped` / `writeScoped`, which append the host.
35
+ *
36
+ * The host alone is one key for every Construct game on a games host. Blocks
37
+ * with Friends keys its app token by `import.meta.env.BASE_URL` so two games
38
+ * do not share a session. Pass that base here.
39
+ */
40
+ export function perGameBase(base: string, gamePath: string): string {
41
+ return `${base}:${gamePath}`;
42
+ }
43
+
44
+ /**
45
+ * localStorage key for the saved app token.
46
+ *
47
+ * Shape is `BrowserLocalStorageTokenStore.appKey(gamePath)` plus the API-host
48
+ * handle: `crowdyjs:app:/matchstick/:ck.dev.crowdedkingdoms.com`. The path
49
+ * keeps a second Construct game from restoring Matchstick's token. The handle
50
+ * keeps a localhost build pointed at another tier from restoring the first.
51
+ */
52
+ export function appTokenStorageKey(gamePath: string, handle: string = ENV_HANDLE): string {
53
+ return envScopedKey(`crowdyjs:app:${gamePath}`, handle);
54
+ }
55
+
33
56
  /** Keys that describe the previous environment and must not survive a switch. */
34
57
  const SCOPED_LOCAL_PREFIXES = ['construct:app-id', 'construct:last-program'];
35
58
 
@@ -40,7 +40,13 @@ import {
40
40
  APP_TOKEN_REFRESH_RETRY_MS,
41
41
  AUTHORIZE_URL,
42
42
  } from '../config';
43
- import { envScopedKey, readScoped, writeScoped } from '../envScope';
43
+ import {
44
+ appTokenStorageKey,
45
+ envScopedKey,
46
+ perGameBase,
47
+ readScoped,
48
+ writeScoped,
49
+ } from '../envScope';
44
50
  import { WorldHub } from '../exec/worldHub';
45
51
  import { Emitter } from '../util/Emitter';
46
52
 
@@ -58,11 +64,13 @@ export interface AppRoute {
58
64
  expiresAt: string;
59
65
  }
60
66
 
61
- /** Where the entered app's route is remembered across a reload (env-scoped). */
67
+ /** Where the entered app's route is remembered across a reload (per game path, per API host). */
62
68
  export const APP_ROUTE_STORAGE_KEY = 'construct:app-route';
63
69
 
64
- /** The token store key for the app token (env-scoped). One app at a time. */
65
- const APP_TOKEN_STORE_KEY = 'construct:app-token';
70
+ /** Route key for this game's Vite base, before the API-host suffix `readScoped` adds. */
71
+ function savedRouteBase(): string {
72
+ return perGameBase(APP_ROUTE_STORAGE_KEY, import.meta.env.BASE_URL);
73
+ }
66
74
 
67
75
  export interface BootstrapInfo {
68
76
  minimumClientVersion?: string;
@@ -125,7 +133,7 @@ export class NetworkManager {
125
133
  this.platform = createCrowdyClient({
126
134
  httpUrl: API_HTTP_URL,
127
135
  wsUrl: API_WS_URL,
128
- tokenStore: new BrowserLocalStorageTokenStore(envScopedKey(APP_TOKEN_STORE_KEY)),
136
+ tokenStore: new BrowserLocalStorageTokenStore(appTokenStorageKey(import.meta.env.BASE_URL)),
129
137
  });
130
138
  }
131
139
 
@@ -183,7 +191,7 @@ export class NetworkManager {
183
191
  const minted = await this.platform.portal.handleSignInCallback();
184
192
  if (!minted) return null;
185
193
  const route = this.routeFrom(minted, minted.appId);
186
- writeScoped(APP_ROUTE_STORAGE_KEY, JSON.stringify(route));
194
+ writeScoped(savedRouteBase(), JSON.stringify(route));
187
195
  await this.hydrateUser();
188
196
  this.log(`Hosted sign-in complete for app ${route.appId}`);
189
197
  return route;
@@ -200,7 +208,7 @@ export class NetworkManager {
200
208
  async restore(): Promise<AppRoute | null> {
201
209
  await this.platform.session.restore();
202
210
  if (!this.platform.session.getToken()) return null;
203
- const raw = readScoped(APP_ROUTE_STORAGE_KEY);
211
+ const raw = readScoped(savedRouteBase());
204
212
  let route: AppRoute | null;
205
213
  try {
206
214
  route = raw ? (JSON.parse(raw) as AppRoute) : null;
@@ -231,7 +239,7 @@ export class NetworkManager {
231
239
 
232
240
  private forgetCredentials(): void {
233
241
  this.platform.session.setToken(null);
234
- writeScoped(APP_ROUTE_STORAGE_KEY, null);
242
+ writeScoped(savedRouteBase(), null);
235
243
  }
236
244
 
237
245
  private async hydrateUser(): Promise<SessionUser | null> {
@@ -346,7 +354,7 @@ export class NetworkManager {
346
354
  }
347
355
  this.gameClient.setToken(token);
348
356
  this.route = route;
349
- writeScoped(APP_ROUTE_STORAGE_KEY, JSON.stringify(route));
357
+ writeScoped(savedRouteBase(), JSON.stringify(route));
350
358
  this.refreshFailures = 0;
351
359
  this.scheduleRefresh(APP_TOKEN_REFRESH_MS);
352
360
  this.log(`Entered app ${route.appId} via ${httpUrl}`);
@@ -417,7 +425,7 @@ export class NetworkManager {
417
425
  const refreshed = await this.gameClient.refreshGameplayToken();
418
426
  this.platform.session.setToken(refreshed.token);
419
427
  this.route = { ...this.route, expiresAt: refreshed.expiresAt };
420
- writeScoped(APP_ROUTE_STORAGE_KEY, JSON.stringify(this.route));
428
+ writeScoped(savedRouteBase(), JSON.stringify(this.route));
421
429
  this.refreshFailures = 0;
422
430
  this.log('App token rotated');
423
431
  this.scheduleRefresh(APP_TOKEN_REFRESH_MS);