cursedbelt-core 2.15.0 → 2.16.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.
@@ -18,8 +18,9 @@ import { sha256Hex } from './digest.js';
18
18
  import { ABBREV_TO_METHOD, CTGR_WIRE_VERSION, METHOD_TO_ABBREV, } from './types.js';
19
19
  const DEFAULT_CHUNK_SIZE = 1800;
20
20
  const DEFAULT_GZIP_THRESHOLD = 1024;
21
- const textEncoder = new TextEncoder();
22
- const textDecoder = new TextDecoder();
21
+ // Pure, so a bundler can leave the codec out of a chunk that only lazy-loads it (forge task 4805).
22
+ const textEncoder = /* @__PURE__ */ new TextEncoder();
23
+ const textDecoder = /* @__PURE__ */ new TextDecoder();
23
24
  /**
24
25
  * Encode a mutating request into one or more `/ctgr<XX>` GET-twin chunks. Small
25
26
  * bodies become a single chunk; large bodies gzip (when over the threshold) and split
@@ -3,7 +3,9 @@
3
3
  * native addon. Client and server share this exact implementation so a chunk's
4
4
  * `_crc` integrity check computes the same value on both ends of the tunnel.
5
5
  */
6
- const TABLE = (() => {
6
+ // Marked pure so a bundler drops the table with the codec: a client that lazy-loads the codec
7
+ // (`installCtgrFetch`) must not carry it in its entry chunk (forge task 4805).
8
+ const TABLE = /* @__PURE__ */ (() => {
7
9
  const table = new Uint32Array(256);
8
10
  for (let n = 0; n < 256; n++) {
9
11
  let c = n;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The same-origin `/api/…` path (with its query) a URL names, or null when it is another
3
+ * origin or a path the request layer does not own (Cloudflare's `/cdn-cgi/rum` beacon, an
4
+ * asset, a twin that is already converted).
5
+ */
6
+ export declare const ownApiPath: (rawUrl: string, base: string) => string | null;
7
+ /**
8
+ * Arm the guard, then route every same-origin `/api/` mutation through the GET tunnel while
9
+ * the mode is on. Call once at boot, after `applyCtgrFromLocation()` and before anything
10
+ * fetches. Idempotent; a no-op with no `window.fetch`.
11
+ */
12
+ export declare const installCtgrFetch: () => void;
@@ -0,0 +1,88 @@
1
+ /*
2
+ * GET-only mode for a whole site in ONE boot call: `installCtgrFetch()`.
3
+ *
4
+ * auth and desk route each request through `convertIfOnlyGETRequests` + `sendCtgrRequests`
5
+ * by hand, inside their own API client. An app whose mutations are spread over dozens of
6
+ * `fetch` call sites (music, 2026-11-09: the owner opened it with `?ctgr=1` on a machine whose
7
+ * network refuses every non-GET, and nothing in it went through the tunnel) would need each
8
+ * one rewritten. This wraps `window.fetch` instead: while the device's mode is on, a mutating
9
+ * request to this origin's own `/api/…` leaves as its `/ctgr<xx>` GET twin(s) and answers with
10
+ * the one real response; everything else — a GET, another origin, a path the app does not own
11
+ * — passes through untouched. With the mode off it is a pass-through.
12
+ *
13
+ * 🔴 It arms the guard FIRST and wraps it, never the other way round: wrapped by the guard,
14
+ * a POST would be refused before it could be converted. The server half is `mountCtgrTunnel`
15
+ * (`cursedbelt-server/ctgr-tunnel`), which re-dispatches each twin into the real route.
16
+ */
17
+ import { isCtgrModeOn } from './ctgrConfig.js';
18
+ import { installCtgrGuard } from './ctgrGuard.js';
19
+ /**
20
+ * The codec (gzip, sha-256, crc, base64url) loads only on a device that is in the mode: in an
21
+ * app's entry chunk it cost every visitor ~4.5 kB gzip (music, measured 2026-11-09) for a path
22
+ * almost none of them take. A bundler splits it out at this `import()`.
23
+ */
24
+ const loadTunnel = () => Promise.all([import('./convertRequest.js'), import('./sendRequests.js')]).then(([c, s]) => ({
25
+ convertIfOnlyGETRequests: c.convertIfOnlyGETRequests,
26
+ sendCtgrRequests: s.sendCtgrRequests,
27
+ }));
28
+ const SAFE_METHODS = new Set(['GET', 'HEAD']);
29
+ /**
30
+ * The same-origin `/api/…` path (with its query) a URL names, or null when it is another
31
+ * origin or a path the request layer does not own (Cloudflare's `/cdn-cgi/rum` beacon, an
32
+ * asset, a twin that is already converted).
33
+ */
34
+ export const ownApiPath = (rawUrl, base) => {
35
+ try {
36
+ const u = new URL(rawUrl, base);
37
+ if (u.origin !== new URL(base).origin)
38
+ return null;
39
+ return u.pathname.startsWith('/api/') ? `${u.pathname}${u.search}` : null;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ };
45
+ /** A `Request`'s body as something the converter carries: its form, or its text. */
46
+ const bodyOfRequest = async (request) => {
47
+ if (!request.body)
48
+ return undefined;
49
+ const type = request.headers.get('content-type') ?? '';
50
+ return type.includes('multipart/form-data') ? request.clone().formData() : request.clone().text();
51
+ };
52
+ let installed = false;
53
+ /**
54
+ * Arm the guard, then route every same-origin `/api/` mutation through the GET tunnel while
55
+ * the mode is on. Call once at boot, after `applyCtgrFromLocation()` and before anything
56
+ * fetches. Idempotent; a no-op with no `window.fetch`.
57
+ */
58
+ export const installCtgrFetch = () => {
59
+ if (installed || typeof window === 'undefined' || typeof window.fetch !== 'function')
60
+ return;
61
+ installed = true;
62
+ installCtgrGuard();
63
+ const inner = window.fetch.bind(window);
64
+ const tunnelled = async (input, init) => {
65
+ if (!isCtgrModeOn())
66
+ return inner(input, init);
67
+ const request = input instanceof Request ? input : null;
68
+ const method = (init?.method ?? request?.method ?? 'GET').toUpperCase();
69
+ if (SAFE_METHODS.has(method))
70
+ return inner(input, init);
71
+ const href = request ? request.url : input instanceof URL ? input.href : String(input);
72
+ const path = ownApiPath(href, window.location.href);
73
+ if (path === null)
74
+ return inner(input, init);
75
+ const effective = { ...init, method };
76
+ if (request) {
77
+ effective.headers ??= request.headers;
78
+ effective.credentials ??= request.credentials;
79
+ effective.signal ??= request.signal;
80
+ if (init?.body === undefined)
81
+ effective.body = await bodyOfRequest(request);
82
+ }
83
+ const { convertIfOnlyGETRequests, sendCtgrRequests } = await loadTunnel();
84
+ const { requests } = await convertIfOnlyGETRequests(path, effective);
85
+ return sendCtgrRequests(requests, (twin) => inner(twin.url, twin.init));
86
+ };
87
+ window.fetch = tunnelled;
88
+ };
@@ -1,4 +1,5 @@
1
1
  export { applyCtgrFromLocation, applyCtgrIntent, isCtgrModeOn, readCtgrIntent, setCtgrMode, } from './ctgrConfig.js';
2
2
  export { type CtgrFetchRequest, convertIfOnlyGETRequests } from './convertRequest.js';
3
+ export { installCtgrFetch, ownApiPath } from './ctgrFetch.js';
3
4
  export { installCtgrGuard } from './ctgrGuard.js';
4
5
  export { CTGR_INCOMPLETE_STATUS, type CtgrSend, type SendCtgrOptions, sendCtgrRequests, } from './sendRequests.js';
@@ -9,9 +9,12 @@
9
9
  * The wire codec is `cursedbelt-core/ctgr`; the server that decodes the twins is
10
10
  * `cursedbelt-server` (`mountCtgrTunnel`). A site opts in with two boot calls —
11
11
  * `applyCtgrFromLocation()` then `installCtgrGuard()` — and routes its requests
12
- * through `convertIfOnlyGETRequests`, then `sendCtgrRequests`.
12
+ * through `convertIfOnlyGETRequests`, then `sendCtgrRequests`. A site whose mutations
13
+ * are spread over many `fetch` calls calls `installCtgrFetch()` instead of the guard,
14
+ * and every same-origin `/api/` mutation takes the tunnel with no call site changed.
13
15
  */
14
16
  export { applyCtgrFromLocation, applyCtgrIntent, isCtgrModeOn, readCtgrIntent, setCtgrMode, } from './ctgrConfig.js';
15
17
  export { convertIfOnlyGETRequests } from './convertRequest.js';
18
+ export { installCtgrFetch, ownApiPath } from './ctgrFetch.js';
16
19
  export { installCtgrGuard } from './ctgrGuard.js';
17
20
  export { CTGR_INCOMPLETE_STATUS, sendCtgrRequests, } from './sendRequests.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt-core",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "Framework-agnostic core of the cursedbelt split — isomorphic domain logic. cwip below it, cursedbelt-server and cursedbelt (react) above it.",
package/src/ctgr/codec.ts CHANGED
@@ -32,8 +32,9 @@ import {
32
32
  const DEFAULT_CHUNK_SIZE = 1800;
33
33
  const DEFAULT_GZIP_THRESHOLD = 1024;
34
34
 
35
- const textEncoder = new TextEncoder();
36
- const textDecoder = new TextDecoder();
35
+ // Pure, so a bundler can leave the codec out of a chunk that only lazy-loads it (forge task 4805).
36
+ const textEncoder = /* @__PURE__ */ new TextEncoder();
37
+ const textDecoder = /* @__PURE__ */ new TextDecoder();
37
38
 
38
39
  /**
39
40
  * Encode a mutating request into one or more `/ctgr<XX>` GET-twin chunks. Small
package/src/ctgr/crc32.ts CHANGED
@@ -3,7 +3,9 @@
3
3
  * native addon. Client and server share this exact implementation so a chunk's
4
4
  * `_crc` integrity check computes the same value on both ends of the tunnel.
5
5
  */
6
- const TABLE = (() => {
6
+ // Marked pure so a bundler drops the table with the codec: a client that lazy-loads the codec
7
+ // (`installCtgrFetch`) must not carry it in its entry chunk (forge task 4805).
8
+ const TABLE = /* @__PURE__ */ (() => {
7
9
  const table = new Uint32Array(256);
8
10
  for (let n = 0; n < 256; n++) {
9
11
  let c = n;
@@ -0,0 +1,111 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
2
+ import { decodeRequest } from '../ctgr/index.js';
3
+ import { installCtgrFetch, ownApiPath, setCtgrMode } from './index.js';
4
+
5
+ /** What reached the network under the shim: the URL and the method, in order. */
6
+ let wire: { url: string; method: string }[] = [];
7
+ const original = window.fetch;
8
+
9
+ beforeAll(() => {
10
+ // The "network": records what it was handed and answers 200 with the URL it saw.
11
+ window.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
12
+ const url = input instanceof Request ? input.url : String(input);
13
+ const method = (init?.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();
14
+ wire.push({ url, method });
15
+ return new Response(JSON.stringify({ url }), { status: 200 });
16
+ }) as typeof window.fetch;
17
+ installCtgrFetch();
18
+ });
19
+
20
+ afterAll(() => {
21
+ window.fetch = original;
22
+ });
23
+
24
+ beforeEach(() => {
25
+ wire = [];
26
+ localStorage.clear();
27
+ });
28
+
29
+ const decodedTwin = async (url: string) => {
30
+ const decoded = await decodeRequest(new URL(url, 'http://x').searchParams);
31
+ if (decoded === 'partial') throw new Error('expected a single-chunk twin');
32
+ return decoded;
33
+ };
34
+
35
+ describe('installCtgrFetch — every own-API mutation takes the GET tunnel', () => {
36
+ test('mode OFF: a POST goes out as the POST it is', async () => {
37
+ await fetch('/api/plays', { method: 'POST', body: JSON.stringify({ id: 1 }) });
38
+ expect(wire).toEqual([{ url: '/api/plays', method: 'POST' }]);
39
+ });
40
+
41
+ test('mode ON: a POST leaves as its GET twin, carrying the body and the query', async () => {
42
+ setCtgrMode(true);
43
+ const res = await fetch('/api/plays?x=1', {
44
+ method: 'POST',
45
+ headers: { 'content-type': 'application/json' },
46
+ body: JSON.stringify({ id: 1 }),
47
+ });
48
+ expect(res.status).toBe(200);
49
+ expect(wire).toHaveLength(1);
50
+ expect(wire[0]?.method).toBe('GET');
51
+ expect(wire[0]?.url.startsWith('/ctgrpo/api/plays?x=1&')).toBe(true);
52
+ const decoded = await decodedTwin(wire[0]?.url ?? '');
53
+ expect(decoded.method).toBe('POST');
54
+ expect(decoded.body).toEqual({ id: 1 });
55
+ });
56
+
57
+ test('mode ON: a Request object is read for its method and body', async () => {
58
+ setCtgrMode(true);
59
+ const url = new URL('/api/playlists/7', window.location.href).href;
60
+ await fetch(new Request(url, { method: 'PATCH', body: JSON.stringify({ name: 'b' }) }));
61
+ expect(wire[0]?.method).toBe('GET');
62
+ const decoded = await decodedTwin(wire[0]?.url ?? '');
63
+ expect(decoded.method).toBe('PATCH');
64
+ expect(decoded.body).toEqual({ name: 'b' });
65
+ });
66
+
67
+ test('mode ON: GETs, other origins and paths outside /api/ pass through untouched', async () => {
68
+ setCtgrMode(true);
69
+ await fetch('/api/library');
70
+ await fetch('https://media.example.com/api/upload', { method: 'POST', body: 'x' });
71
+ await fetch('/cdn-cgi/rum', { method: 'POST', body: 'x' });
72
+ expect(wire).toEqual([
73
+ { url: '/api/library', method: 'GET' },
74
+ { url: 'https://media.example.com/api/upload', method: 'POST' },
75
+ { url: '/cdn-cgi/rum', method: 'POST' },
76
+ ]);
77
+ });
78
+ });
79
+
80
+ describe('ownApiPath', () => {
81
+ const base = 'https://music.example.com/library';
82
+ test('a same-origin /api/ path, absolute or relative, keeps its query', () => {
83
+ expect(ownApiPath('/api/a?b=1', base)).toBe('/api/a?b=1');
84
+ expect(ownApiPath('https://music.example.com/api/a', base)).toBe('/api/a');
85
+ });
86
+ test('another origin, or a path the app does not own, is null', () => {
87
+ expect(ownApiPath('https://auth.example.com/api/a', base)).toBeNull();
88
+ expect(ownApiPath('/ctgrpo/api/a', base)).toBeNull();
89
+ expect(ownApiPath('/assets/x.js', base)).toBeNull();
90
+ });
91
+ });
92
+
93
+ describe('the codec stays out of an entry chunk that only boots the mode (forge task 4805)', () => {
94
+ const read = (rel: string) => Bun.file(new URL(rel, import.meta.url)).text();
95
+
96
+ test('ctgrFetch reaches the converter and the sender only through import()', async () => {
97
+ const src = await read('./ctgrFetch.ts');
98
+ expect(src).not.toMatch(/^import [^;]*from '\.\/(convertRequest|sendRequests)\.js'/m);
99
+ expect(src).toContain("import('./convertRequest.js')");
100
+ expect(src).toContain("import('./sendRequests.js')");
101
+ });
102
+
103
+ test('every top-level `new` or IIFE in the codec is marked pure, so a bundler can drop it', async () => {
104
+ for (const rel of ['../ctgr/codec.ts', '../ctgr/crc32.ts']) {
105
+ const offenders = (await read(rel))
106
+ .split('\n')
107
+ .filter((line) => /^(const|let) \w+ = (new |\()/.test(line));
108
+ expect({ rel, offenders }).toEqual({ rel, offenders: [] });
109
+ }
110
+ });
111
+ });
@@ -0,0 +1,88 @@
1
+ /*
2
+ * GET-only mode for a whole site in ONE boot call: `installCtgrFetch()`.
3
+ *
4
+ * auth and desk route each request through `convertIfOnlyGETRequests` + `sendCtgrRequests`
5
+ * by hand, inside their own API client. An app whose mutations are spread over dozens of
6
+ * `fetch` call sites (music, 2026-11-09: the owner opened it with `?ctgr=1` on a machine whose
7
+ * network refuses every non-GET, and nothing in it went through the tunnel) would need each
8
+ * one rewritten. This wraps `window.fetch` instead: while the device's mode is on, a mutating
9
+ * request to this origin's own `/api/…` leaves as its `/ctgr<xx>` GET twin(s) and answers with
10
+ * the one real response; everything else — a GET, another origin, a path the app does not own
11
+ * — passes through untouched. With the mode off it is a pass-through.
12
+ *
13
+ * 🔴 It arms the guard FIRST and wraps it, never the other way round: wrapped by the guard,
14
+ * a POST would be refused before it could be converted. The server half is `mountCtgrTunnel`
15
+ * (`cursedbelt-server/ctgr-tunnel`), which re-dispatches each twin into the real route.
16
+ */
17
+ import { isCtgrModeOn } from './ctgrConfig.js';
18
+ import { installCtgrGuard } from './ctgrGuard.js';
19
+
20
+ /**
21
+ * The codec (gzip, sha-256, crc, base64url) loads only on a device that is in the mode: in an
22
+ * app's entry chunk it cost every visitor ~4.5 kB gzip (music, measured 2026-11-09) for a path
23
+ * almost none of them take. A bundler splits it out at this `import()`.
24
+ */
25
+ const loadTunnel = () =>
26
+ Promise.all([import('./convertRequest.js'), import('./sendRequests.js')]).then(([c, s]) => ({
27
+ convertIfOnlyGETRequests: c.convertIfOnlyGETRequests,
28
+ sendCtgrRequests: s.sendCtgrRequests,
29
+ }));
30
+
31
+ const SAFE_METHODS = new Set(['GET', 'HEAD']);
32
+
33
+ /**
34
+ * The same-origin `/api/…` path (with its query) a URL names, or null when it is another
35
+ * origin or a path the request layer does not own (Cloudflare's `/cdn-cgi/rum` beacon, an
36
+ * asset, a twin that is already converted).
37
+ */
38
+ export const ownApiPath = (rawUrl: string, base: string): string | null => {
39
+ try {
40
+ const u = new URL(rawUrl, base);
41
+ if (u.origin !== new URL(base).origin) return null;
42
+ return u.pathname.startsWith('/api/') ? `${u.pathname}${u.search}` : null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ };
47
+
48
+ /** A `Request`'s body as something the converter carries: its form, or its text. */
49
+ const bodyOfRequest = async (request: Request): Promise<BodyInit | undefined> => {
50
+ if (!request.body) return undefined;
51
+ const type = request.headers.get('content-type') ?? '';
52
+ return type.includes('multipart/form-data') ? request.clone().formData() : request.clone().text();
53
+ };
54
+
55
+ let installed = false;
56
+
57
+ /**
58
+ * Arm the guard, then route every same-origin `/api/` mutation through the GET tunnel while
59
+ * the mode is on. Call once at boot, after `applyCtgrFromLocation()` and before anything
60
+ * fetches. Idempotent; a no-op with no `window.fetch`.
61
+ */
62
+ export const installCtgrFetch = (): void => {
63
+ if (installed || typeof window === 'undefined' || typeof window.fetch !== 'function') return;
64
+ installed = true;
65
+ installCtgrGuard();
66
+ const inner = window.fetch.bind(window);
67
+ const tunnelled = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
68
+ if (!isCtgrModeOn()) return inner(input, init);
69
+ const request = input instanceof Request ? input : null;
70
+ const method = (init?.method ?? request?.method ?? 'GET').toUpperCase();
71
+ if (SAFE_METHODS.has(method)) return inner(input, init);
72
+ const href = request ? request.url : input instanceof URL ? input.href : String(input);
73
+ const path = ownApiPath(href, window.location.href);
74
+ if (path === null) return inner(input, init);
75
+
76
+ const effective: RequestInit = { ...init, method };
77
+ if (request) {
78
+ effective.headers ??= request.headers;
79
+ effective.credentials ??= request.credentials;
80
+ effective.signal ??= request.signal;
81
+ if (init?.body === undefined) effective.body = await bodyOfRequest(request);
82
+ }
83
+ const { convertIfOnlyGETRequests, sendCtgrRequests } = await loadTunnel();
84
+ const { requests } = await convertIfOnlyGETRequests(path, effective);
85
+ return sendCtgrRequests(requests, (twin) => inner(twin.url, twin.init));
86
+ };
87
+ window.fetch = tunnelled as typeof window.fetch;
88
+ };
@@ -9,7 +9,9 @@
9
9
  * The wire codec is `cursedbelt-core/ctgr`; the server that decodes the twins is
10
10
  * `cursedbelt-server` (`mountCtgrTunnel`). A site opts in with two boot calls —
11
11
  * `applyCtgrFromLocation()` then `installCtgrGuard()` — and routes its requests
12
- * through `convertIfOnlyGETRequests`, then `sendCtgrRequests`.
12
+ * through `convertIfOnlyGETRequests`, then `sendCtgrRequests`. A site whose mutations
13
+ * are spread over many `fetch` calls calls `installCtgrFetch()` instead of the guard,
14
+ * and every same-origin `/api/` mutation takes the tunnel with no call site changed.
13
15
  */
14
16
  export {
15
17
  applyCtgrFromLocation,
@@ -19,6 +21,7 @@ export {
19
21
  setCtgrMode,
20
22
  } from './ctgrConfig.js';
21
23
  export { type CtgrFetchRequest, convertIfOnlyGETRequests } from './convertRequest.js';
24
+ export { installCtgrFetch, ownApiPath } from './ctgrFetch.js';
22
25
  export { installCtgrGuard } from './ctgrGuard.js';
23
26
  export {
24
27
  CTGR_INCOMPLETE_STATUS,