cursedbelt-core 2.1.1 → 2.3.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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `app-config.json` — the single schema the compiler AND the runtime import
3
3
  * (02-MASTER-ENGINES §2). Nothing else may re-declare this shape: a second copy
4
- * is how `privateMedia` without `guard` parsed as valid for a year (J2-1).
4
+ * is how a feature without the engine it needs parsed as valid for a year (J2-1).
5
5
  *
6
6
  * Two laws are encoded here rather than in prose:
7
7
  *
@@ -9,8 +9,8 @@
9
9
  * `features` block — is a parse ERROR (`unknown-config-key`), never silently
10
10
  * stripped. A typo'd flag that gets dropped reads as "the feature is off" at
11
11
  * runtime, which is indistinguishable from a bug in the feature.
12
- * 2. `.superRefine()` cross-engine dependencies. `privateMedia` needs `guard`,
13
- * `oralHistory` needs `asset`, `auth.enabled` needs `guard`, and the saas
12
+ * 2. `.superRefine()` cross-engine dependencies. `oralHistory` needs `asset`,
13
+ * `auth.enabled` needs `guard`, and the saas
14
14
  * profile needs `guard` + `rbac:true`. These are structural: the config
15
15
  * references something that will not exist at runtime, so it dies at V0 —
16
16
  * fail-closed on structure (04 §0).
@@ -35,7 +35,9 @@ export const AssetFeatures = z
35
35
  editor: z.boolean().default(false), // lazy chunk; joins bundleKey
36
36
  fileBrowser: z.boolean().default(false),
37
37
  taggedFiles: z.boolean().default(false),
38
- privateMedia: z.boolean().default(false), // requires guard engine — enforced in superRefine
38
+ // `privateMedia` was here until 2.3.0 (task 330). Its server module and the `pfs/` keyspace
39
+ // were deleted from cursedbelt-server in 4.0.0, so the flag named a tier that no longer
40
+ // exists; `.strict()` now refuses it as `unknown-config-key`, which is the honest answer.
39
41
  vaultSource: z.boolean().default(false),
40
42
  ocr: z.boolean().default(false),
41
43
  imageStudio: z.boolean().default(false),
@@ -326,11 +328,6 @@ export const AppConfigSchema = z
326
328
  const f = parsed.data as Record<string, unknown>;
327
329
 
328
330
  // (c) cross-engine dependencies — hard blocks, not comments (J2-1)
329
- if (inst.engine === 'asset' && f.privateMedia === true && !engineSet.has('guard'))
330
- issue(
331
- ['instances', i, 'features', 'privateMedia'],
332
- `'privateMedia' requires the 'guard' engine in engines[] (engine-dependency-missing).`,
333
- );
334
331
  if (inst.engine === 'knowledge' && f.oralHistory === true && !engineSet.has('asset'))
335
332
  issue(
336
333
  ['instances', i, 'features', 'oralHistory'],
@@ -0,0 +1,392 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import {
3
+ classifyRequest,
4
+ installShellWorker,
5
+ mayFallBackToShell,
6
+ type RequestFacts,
7
+ resolveShellWorkerConfig,
8
+ type ShellExtendableEvent,
9
+ type ShellFetchEvent,
10
+ type ShellWorkerCache,
11
+ type ShellWorkerConfig,
12
+ type ShellWorkerRequest,
13
+ type ShellWorkerRequestInfo,
14
+ type ShellWorkerResponse,
15
+ type ShellWorkerScope,
16
+ shouldCache,
17
+ } from './shellWorker.js';
18
+
19
+ /** roms' worker, expressed as a config — the two special cases are the point. */
20
+ const ROMS: ShellWorkerConfig = {
21
+ cachePrefix: 'roms-shell-',
22
+ version: 'v3',
23
+ precache: ['/', '/emulator-frame.html', '/emulator-runtime.js'],
24
+ passthroughPrefixes: ['/_nes-roms/'],
25
+ noShellFallback: ['/emulator-frame.html'],
26
+ };
27
+
28
+ const facts = (pathname: string, over: Partial<RequestFacts> = {}): RequestFacts => ({
29
+ method: 'GET',
30
+ sameOrigin: true,
31
+ pathname,
32
+ isNavigate: false,
33
+ ...over,
34
+ });
35
+
36
+ describe('classifyRequest — roms parity', () => {
37
+ const table: Array<[string, RequestFacts, string]> = [
38
+ ['non-GET is passthrough', facts('/app.js', { method: 'POST' }), 'passthrough'],
39
+ ['cross-origin is passthrough', facts('/app.js', { sameOrigin: false }), 'passthrough'],
40
+ ['/api/ is api', facts('/api/saves'), 'api'],
41
+ ['/api/ wins over the .json asset extension', facts('/api/library.json'), 'api'],
42
+ ['/api/ wins over a navigation', facts('/api/me', { isNavigate: true }), 'api'],
43
+ ['roms ROM bytes are passthrough (config)', facts('/_nes-roms/mario.nes'), 'passthrough'],
44
+ ['roms ROM path as a .json is still passthrough', facts('/_nes-roms/index.json'), 'passthrough'],
45
+ ['a navigation is navigate', facts('/library/nes', { isNavigate: true }), 'navigate'],
46
+ ['the frame document is a navigation', facts('/emulator-frame.html', { isNavigate: true }), 'navigate'],
47
+ ['hashed JS is an asset', facts('/assets/index-abc123.js'), 'asset'],
48
+ ['WASM is an asset', facts('/cores/snes9x.wasm'), 'asset'],
49
+ ['a font is an asset', facts('/fonts/inter.woff2'), 'asset'],
50
+ ['upper-case extension is an asset', facts('/icons/LOGO.PNG'), 'asset'],
51
+ ['an extensionless non-navigation is passthrough', facts('/healthz'), 'passthrough'],
52
+ ['a .nes file outside the ROM path is passthrough', facts('/other/mario.nes'), 'passthrough'],
53
+ ];
54
+ for (const [name, input, route] of table) {
55
+ it(name, () => expect(classifyRequest(input, ROMS)).toBe(route));
56
+ }
57
+
58
+ it('without the passthrough config the ROM path would be an asset — the config is what decides', () => {
59
+ expect(classifyRequest(facts('/_nes-roms/index.json'), {})).toBe('asset');
60
+ });
61
+
62
+ it('/api/ stays api even when a config lists only other API prefixes', () => {
63
+ expect(classifyRequest(facts('/api/me'), { apiPrefixes: ['/rpc/'] })).toBe('api');
64
+ expect(classifyRequest(facts('/rpc/call.json'), { apiPrefixes: ['/rpc/'] })).toBe('api');
65
+ });
66
+
67
+ it('a custom asset regex replaces the default', () => {
68
+ expect(classifyRequest(facts('/a.js'), { assetExtensions: /\.css$/ })).toBe('passthrough');
69
+ expect(classifyRequest(facts('/a.css'), { assetExtensions: /\.css$/ })).toBe('asset');
70
+ });
71
+ });
72
+
73
+ describe('shouldCache / mayFallBackToShell', () => {
74
+ it('caches assets and navigations, never the API or passthrough', () => {
75
+ expect(shouldCache('asset')).toBe(true);
76
+ expect(shouldCache('navigate')).toBe(true);
77
+ expect(shouldCache('api')).toBe(false);
78
+ expect(shouldCache('passthrough')).toBe(false);
79
+ });
80
+
81
+ it('refuses the shell to roms’ frame and allows it everywhere else', () => {
82
+ expect(mayFallBackToShell('/emulator-frame.html', ROMS)).toBe(false);
83
+ expect(mayFallBackToShell('/library/nes', ROMS)).toBe(true);
84
+ expect(mayFallBackToShell('/emulator-frame.html', {})).toBe(true);
85
+ });
86
+ });
87
+
88
+ describe('resolveShellWorkerConfig — the refusals', () => {
89
+ it('names the cache prefix+version and always precaches the shell', () => {
90
+ const resolved = resolveShellWorkerConfig({ cachePrefix: 'music-shell-', version: 'v1', precache: ['/player.js'] });
91
+ expect(resolved.cacheName).toBe('music-shell-v1');
92
+ expect(resolved.precache).toEqual(['/', '/player.js']);
93
+ expect(resolved.apiPrefixes).toEqual(['/api/']);
94
+ });
95
+
96
+ it('keeps roms’ precache as written when the shell is already listed', () => {
97
+ expect(resolveShellWorkerConfig(ROMS).precache).toEqual(ROMS.precache);
98
+ });
99
+
100
+ it('refuses an empty prefix — activate would delete every cache on the origin', () => {
101
+ expect(() => resolveShellWorkerConfig({ ...ROMS, cachePrefix: '' })).toThrow(/cachePrefix/);
102
+ });
103
+
104
+ it('refuses to precache anything under an API prefix', () => {
105
+ expect(() => resolveShellWorkerConfig({ ...ROMS, precache: ['/', '/api/me'] })).toThrow(/API is never cached/);
106
+ expect(() => resolveShellWorkerConfig({ ...ROMS, apiPrefixes: ['/rpc/'], precache: ['/rpc/x'] })).toThrow(
107
+ /API is never cached/,
108
+ );
109
+ });
110
+
111
+ it('refuses a relative precache entry and a shell that may not be its own fallback', () => {
112
+ expect(() => resolveShellWorkerConfig({ ...ROMS, precache: ['app.js'] })).toThrow(/starting with "\/"/);
113
+ expect(() => resolveShellWorkerConfig({ ...ROMS, noShellFallback: ['/'] })).toThrow(/noShellFallback/);
114
+ });
115
+ });
116
+
117
+ // ── A fake worker scope ─────────────────────────────────────────────────────────
118
+
119
+ const ORIGIN = 'https://app.test';
120
+
121
+ class FakeResponse implements ShellWorkerResponse {
122
+ constructor(
123
+ readonly body: string,
124
+ readonly ok = true,
125
+ ) {}
126
+ clone(): FakeResponse {
127
+ return new FakeResponse(this.body, this.ok);
128
+ }
129
+ }
130
+
131
+ const req = (path: string, over: Partial<ShellWorkerRequest> = {}): ShellWorkerRequest => ({
132
+ url: path.startsWith('http') ? path : `${ORIGIN}${path}`,
133
+ method: 'GET',
134
+ mode: 'cors',
135
+ ...over,
136
+ });
137
+ const nav = (path: string): ShellWorkerRequest => req(path, { mode: 'navigate' });
138
+
139
+ const urlOf = (info: ShellWorkerRequestInfo): string =>
140
+ typeof info === 'string' ? new URL(info, ORIGIN).href : 'href' in info ? info.href : info.url;
141
+
142
+ const stripSearch = (url: string): string => {
143
+ const u = new URL(url);
144
+ return `${u.origin}${u.pathname}`;
145
+ };
146
+
147
+ /**
148
+ * A worker scope backed by Maps: `server` is what the network answers when `online`,
149
+ * `cacheStore` is CacheStorage. Every network call and every cache write is recorded,
150
+ * so a test can prove a request was NEVER touched, not merely answered correctly.
151
+ */
152
+ const makeWorld = () => {
153
+ const server = new Map<string, FakeResponse>();
154
+ const cacheStore = new Map<string, Map<string, FakeResponse>>();
155
+ const networkCalls: string[] = [];
156
+ const cacheWrites: string[] = [];
157
+ const listeners: Record<string, (event: never) => void> = {};
158
+ let online = true;
159
+ let skipped = false;
160
+ let claimed = false;
161
+
162
+ const network = async (url: string): Promise<FakeResponse> => {
163
+ networkCalls.push(url);
164
+ if (!online) throw new TypeError('Failed to fetch');
165
+ return server.get(url) ?? new FakeResponse('not found', false);
166
+ };
167
+
168
+ const openCache = (name: string): ShellWorkerCache => {
169
+ let entries = cacheStore.get(name);
170
+ if (!entries) {
171
+ entries = new Map();
172
+ cacheStore.set(name, entries);
173
+ }
174
+ const bucket = entries;
175
+ return {
176
+ async addAll(paths) {
177
+ for (const path of paths) {
178
+ const res = await network(`${ORIGIN}${path}`);
179
+ if (!res.ok) throw new TypeError(`addAll: ${path} answered not-ok`);
180
+ bucket.set(`${ORIGIN}${path}`, res);
181
+ }
182
+ },
183
+ async put(request, response) {
184
+ cacheWrites.push(urlOf(request));
185
+ bucket.set(urlOf(request), response as FakeResponse);
186
+ },
187
+ };
188
+ };
189
+
190
+ const scope: ShellWorkerScope = {
191
+ addEventListener(type: string, listener: (event: never) => void) {
192
+ listeners[type] = listener;
193
+ },
194
+ caches: {
195
+ open: async (name) => openCache(name),
196
+ keys: async () => [...cacheStore.keys()],
197
+ delete: async (name) => cacheStore.delete(name),
198
+ async match(request, options) {
199
+ const raw = urlOf(request);
200
+ for (const entries of cacheStore.values()) {
201
+ const exact = entries.get(raw);
202
+ if (exact) return exact;
203
+ if (options?.ignoreSearch) {
204
+ for (const [key, value] of entries) if (stripSearch(key) === stripSearch(raw)) return value;
205
+ }
206
+ }
207
+ return undefined;
208
+ },
209
+ },
210
+ clients: {
211
+ async claim() {
212
+ claimed = true;
213
+ },
214
+ },
215
+ location: { origin: ORIGIN },
216
+ fetch: (request) => network(urlOf(request)),
217
+ async skipWaiting() {
218
+ skipped = true;
219
+ },
220
+ };
221
+
222
+ const extendable = async (type: 'install' | 'activate'): Promise<void> => {
223
+ const waits: Promise<unknown>[] = [];
224
+ const event: ShellExtendableEvent = { waitUntil: (p) => void waits.push(p) };
225
+ (listeners[type] as (e: ShellExtendableEvent) => void)(event);
226
+ await Promise.all(waits);
227
+ };
228
+
229
+ /** Dispatch a fetch. `response` is undefined when the worker did not call respondWith. */
230
+ const dispatch = (request: ShellWorkerRequest) => {
231
+ const waits: Promise<unknown>[] = [];
232
+ let response: Promise<ShellWorkerResponse> | undefined;
233
+ const event: ShellFetchEvent = {
234
+ request,
235
+ waitUntil: (p) => void waits.push(p),
236
+ respondWith: (r) => {
237
+ response = r;
238
+ },
239
+ };
240
+ (listeners.fetch as (e: ShellFetchEvent) => void)(event);
241
+ return {
242
+ response,
243
+ /** Resolve the response, then let every deferred cache write land. */
244
+ settled: async () => {
245
+ const body = response ? await response : undefined;
246
+ await Promise.all(waits);
247
+ return body as FakeResponse | undefined;
248
+ },
249
+ };
250
+ };
251
+
252
+ return {
253
+ scope,
254
+ server,
255
+ cacheStore,
256
+ networkCalls,
257
+ cacheWrites,
258
+ extendable,
259
+ dispatch,
260
+ goOffline: () => {
261
+ online = false;
262
+ },
263
+ get skipped() {
264
+ return skipped;
265
+ },
266
+ get claimed() {
267
+ return claimed;
268
+ },
269
+ };
270
+ };
271
+
272
+ /** A roms-shaped world, installed while online. */
273
+ const installedRoms = async () => {
274
+ const world = makeWorld();
275
+ world.server.set(`${ORIGIN}/`, new FakeResponse('<shell>'));
276
+ world.server.set(`${ORIGIN}/emulator-frame.html`, new FakeResponse('<frame>'));
277
+ world.server.set(`${ORIGIN}/emulator-runtime.js`, new FakeResponse('runtime'));
278
+ installShellWorker(world.scope, ROMS);
279
+ await world.extendable('install');
280
+ return world;
281
+ };
282
+
283
+ describe('installShellWorker — install and activate', () => {
284
+ it('precaches the configured paths into prefix+version and skips waiting', async () => {
285
+ const world = await installedRoms();
286
+ expect([...(world.cacheStore.get('roms-shell-v3')?.keys() ?? [])]).toEqual([
287
+ `${ORIGIN}/`,
288
+ `${ORIGIN}/emulator-frame.html`,
289
+ `${ORIGIN}/emulator-runtime.js`,
290
+ ]);
291
+ expect(world.skipped).toBe(true);
292
+ });
293
+
294
+ it('does not skip waiting when the precache fails — the old worker keeps serving', async () => {
295
+ const world = makeWorld(); // nothing on the server: addAll rejects
296
+ installShellWorker(world.scope, ROMS);
297
+ await expect(world.extendable('install')).rejects.toThrow(/answered not-ok/);
298
+ expect(world.skipped).toBe(false);
299
+ });
300
+
301
+ it('deletes old versions of its own prefix on activate, keeps everything else, and claims', async () => {
302
+ const world = await installedRoms();
303
+ world.cacheStore.set('roms-shell-v2', new Map());
304
+ world.cacheStore.set('roms-shell-v1', new Map());
305
+ world.cacheStore.set('roms-rom-digests', new Map()); // the app's own cache
306
+ await world.extendable('activate');
307
+ expect([...world.cacheStore.keys()].sort()).toEqual(['roms-rom-digests', 'roms-shell-v3']);
308
+ expect(world.claimed).toBe(true);
309
+ });
310
+ });
311
+
312
+ describe('installShellWorker — fetch', () => {
313
+ it('serves an online navigation from the network and caches it', async () => {
314
+ const world = await installedRoms();
315
+ world.server.set(`${ORIGIN}/library/nes`, new FakeResponse('<library>'));
316
+ const body = await world.dispatch(nav('/library/nes')).settled();
317
+ expect(body?.body).toBe('<library>');
318
+ expect(world.cacheWrites).toContain(`${ORIGIN}/library/nes`);
319
+ });
320
+
321
+ it('falls back to the SHELL for an offline navigation nothing cached', async () => {
322
+ const world = await installedRoms();
323
+ world.goOffline();
324
+ const body = await world.dispatch(nav('/library/snes?sort=recent')).settled();
325
+ expect(body?.body).toBe('<shell>');
326
+ });
327
+
328
+ it('answers the frame offline from its own cached copy, ignoring the query', async () => {
329
+ const world = await installedRoms();
330
+ world.goOffline();
331
+ const body = await world.dispatch(nav('/emulator-frame.html?system=nes')).settled();
332
+ expect(body?.body).toBe('<frame>');
333
+ });
334
+
335
+ it('🔴 never hands the SHELL to a noShellFallback path — it rejects instead (the 2026-08-12 incident)', async () => {
336
+ const world = makeWorld();
337
+ world.server.set(`${ORIGIN}/`, new FakeResponse('<shell>'));
338
+ // The 2026-08-12 shape: the frame is NOT precached, the shell is.
339
+ installShellWorker(world.scope, { ...ROMS, precache: ['/'] });
340
+ await world.extendable('install');
341
+ world.goOffline();
342
+ const { response } = world.dispatch(nav('/emulator-frame.html?system=nes'));
343
+ expect(response).toBeDefined();
344
+ await expect(response as Promise<ShellWorkerResponse>).rejects.toThrow('Failed to fetch');
345
+ });
346
+
347
+ it('rethrows the network error when offline with nothing cached at all, rather than resolving anything', async () => {
348
+ const world = makeWorld();
349
+ installShellWorker(world.scope, ROMS); // never installed: no shell in any cache
350
+ world.goOffline();
351
+ await expect(world.dispatch(nav('/library/nes')).response as Promise<ShellWorkerResponse>).rejects.toThrow(
352
+ 'Failed to fetch',
353
+ );
354
+ });
355
+
356
+ it('serves assets cache-first and caches only an ok response', async () => {
357
+ const world = await installedRoms();
358
+ world.server.set(`${ORIGIN}/assets/app-1.js`, new FakeResponse('app'));
359
+ expect((await world.dispatch(req('/assets/app-1.js')).settled())?.body).toBe('app');
360
+ expect((await world.dispatch(req('/assets/missing.js')).settled())?.ok).toBe(false);
361
+ expect(world.cacheWrites).toEqual([`${ORIGIN}/assets/app-1.js`]);
362
+ world.goOffline();
363
+ const calls = world.networkCalls.length;
364
+ expect((await world.dispatch(req('/assets/app-1.js')).settled())?.body).toBe('app');
365
+ expect(world.networkCalls.length).toBe(calls); // from the cache, not the network
366
+ });
367
+
368
+ it('🔴 never intercepts, reads or caches /api/ — even with a stale copy planted in the cache', async () => {
369
+ const world = await installedRoms();
370
+ world.cacheStore.get('roms-shell-v3')?.set(`${ORIGIN}/api/me`, new FakeResponse('{"user":"revoked"}'));
371
+ for (const request of [req('/api/me'), nav('/api/me'), req('/api/library.json')]) {
372
+ const { response } = world.dispatch(request);
373
+ expect(response, `${request.url} (${request.mode}) must not be answered by the worker`).toBeUndefined();
374
+ }
375
+ world.goOffline();
376
+ expect(world.dispatch(req('/api/me')).response).toBeUndefined();
377
+ expect(world.networkCalls.filter((u) => u.includes('/api/'))).toEqual([]);
378
+ expect(world.cacheWrites.filter((u) => u.includes('/api/'))).toEqual([]);
379
+ });
380
+
381
+ it('leaves non-GET, cross-origin and passthrough-prefixed requests to the browser', async () => {
382
+ const world = await installedRoms();
383
+ for (const request of [
384
+ req('/assets/app.js', { method: 'POST' }),
385
+ req('https://cdn.other.test/lib.js'),
386
+ req('/_nes-roms/mario.nes'),
387
+ req('/healthz'),
388
+ ]) {
389
+ expect(world.dispatch(request).response, request.url).toBeUndefined();
390
+ }
391
+ });
392
+ });