@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 +1 -1
- package/package.json +1 -1
- package/src/platform/envScope.test.ts +23 -2
- package/src/platform/envScope.ts +23 -0
- package/src/platform/network/NetworkManager.ts +18 -10
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
|
|
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.
|
|
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 {
|
|
9
|
-
|
|
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');
|
package/src/platform/envScope.ts
CHANGED
|
@@ -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 {
|
|
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 (
|
|
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
|
-
/**
|
|
65
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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);
|