cursedbelt-core 2.13.2 → 2.14.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.
Files changed (96) hide show
  1. package/dist/core/analytics/range.d.ts +2 -2
  2. package/dist/core/analytics/range.js +2 -2
  3. package/dist/core/analytics/schedule.d.ts +1 -1
  4. package/dist/core/analytics/schedule.js +3 -3
  5. package/dist/core/duplicates/index.d.ts +2 -2
  6. package/dist/core/duplicates/index.js +1 -1
  7. package/dist/core/file-tree/fileTreeModel.d.ts +3 -3
  8. package/dist/core/file-tree/fileTreeModel.js +2 -2
  9. package/dist/core/folder-tree/actions.d.ts +1 -1
  10. package/dist/core/folder-tree/actions.js +1 -1
  11. package/dist/core/master-lock/kdf.js +1 -1
  12. package/dist/core/master-lock/presence.d.ts +1 -1
  13. package/dist/core/master-lock/presence.js +1 -1
  14. package/dist/core/master-lock/wire.d.ts +1 -1
  15. package/dist/core/media/mediaItem.js +1 -1
  16. package/dist/core/media/types.d.ts +4 -4
  17. package/dist/core/media/videoCuts.d.ts +1 -1
  18. package/dist/core/media/videoCuts.js +2 -2
  19. package/dist/core/navigation/index.js +1 -1
  20. package/dist/core/navigation/navigationModel.d.ts +1 -1
  21. package/dist/core/navigation/navigationModel.js +1 -1
  22. package/dist/core/navigation/navigationSelectors.d.ts +2 -2
  23. package/dist/core/navigation/navigationSelectors.js +2 -2
  24. package/dist/core/navigation/searchCatalogue.d.ts +2 -2
  25. package/dist/core/navigation/searchCatalogue.js +4 -4
  26. package/dist/core/palette/fuzzyRank.d.ts +1 -1
  27. package/dist/core/palette/fuzzyRank.js +3 -3
  28. package/dist/core/tokens/registry.d.ts +1 -1
  29. package/dist/core/tokens/registry.js +1 -1
  30. package/dist/core/upload-precheck/contract.d.ts +71 -0
  31. package/dist/core/upload-precheck/contract.js +76 -0
  32. package/dist/core/upload-precheck/hash.d.ts +26 -0
  33. package/dist/core/upload-precheck/hash.js +46 -0
  34. package/dist/core/upload-precheck/index.d.ts +21 -0
  35. package/dist/core/upload-precheck/index.js +21 -0
  36. package/dist/core/upload-precheck/memo.d.ts +52 -0
  37. package/dist/core/upload-precheck/memo.js +160 -0
  38. package/dist/core/upload-precheck/plan.d.ts +112 -0
  39. package/dist/core/upload-precheck/plan.js +242 -0
  40. package/dist/core/upload-precheck/sha256.d.ts +27 -0
  41. package/dist/core/upload-precheck/sha256.js +124 -0
  42. package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +2 -2
  43. package/dist/core/wire/index.d.ts +1 -1
  44. package/dist/core/wire/index.js +1 -1
  45. package/dist/core/wire/ingress.d.ts +4 -4
  46. package/dist/core/wire/ingress.js +12 -12
  47. package/dist/core/wire/marshalWorker.d.ts +1 -1
  48. package/dist/core/wire/marshalWorker.js +15 -15
  49. package/dist/core/wire/marshalling.d.ts +16 -16
  50. package/dist/core/wire/marshalling.js +13 -13
  51. package/dist/ctgrClient/convertRequest.js +2 -2
  52. package/dist/testing/bytes.d.ts +1 -1
  53. package/dist/testing/bytes.js +1 -1
  54. package/package.json +7 -1
  55. package/src/core/analytics/range.spec.ts +1 -1
  56. package/src/core/analytics/range.ts +2 -2
  57. package/src/core/analytics/schedule.spec.ts +3 -3
  58. package/src/core/analytics/schedule.ts +3 -3
  59. package/src/core/duplicates/index.ts +2 -2
  60. package/src/core/file-tree/fileTreeModel.ts +4 -4
  61. package/src/core/folder-tree/actions.ts +1 -1
  62. package/src/core/master-lock/kdf.ts +1 -1
  63. package/src/core/master-lock/presence.ts +1 -1
  64. package/src/core/master-lock/wire.ts +1 -1
  65. package/src/core/media/mediaItem.ts +1 -1
  66. package/src/core/media/types.ts +4 -4
  67. package/src/core/media/videoCuts.ts +2 -2
  68. package/src/core/navigation/index.ts +1 -1
  69. package/src/core/navigation/navigation.spec.ts +3 -3
  70. package/src/core/navigation/navigationModel.ts +2 -2
  71. package/src/core/navigation/navigationSelectors.ts +3 -3
  72. package/src/core/navigation/searchCatalogue.ts +4 -4
  73. package/src/core/palette/fuzzyRank.ts +3 -3
  74. package/src/core/tokens/registry.ts +2 -2
  75. package/src/core/upload-precheck/contract.spec.ts +67 -0
  76. package/src/core/upload-precheck/contract.ts +107 -0
  77. package/src/core/upload-precheck/hash.ts +57 -0
  78. package/src/core/upload-precheck/index.ts +46 -0
  79. package/src/core/upload-precheck/memo.ts +170 -0
  80. package/src/core/upload-precheck/plan.spec.ts +299 -0
  81. package/src/core/upload-precheck/plan.ts +315 -0
  82. package/src/core/upload-precheck/sha256.spec.ts +47 -0
  83. package/src/core/upload-precheck/sha256.ts +127 -0
  84. package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +2 -2
  85. package/src/core/wire/index.ts +1 -1
  86. package/src/core/wire/ingress.spec.ts +12 -12
  87. package/src/core/wire/ingress.ts +15 -15
  88. package/src/core/wire/marshalWorker.spec.ts +7 -7
  89. package/src/core/wire/marshalWorker.ts +15 -15
  90. package/src/core/wire/marshalling.spec.ts +3 -3
  91. package/src/core/wire/marshalling.ts +25 -25
  92. package/src/ctgrClient/convertRequest.ts +2 -2
  93. package/src/namedSubpathsResolve.spec.ts +1 -1
  94. package/src/publishShape.spec.ts +1 -1
  95. package/src/shippedFilesAreTracked.spec.ts +2 -2
  96. package/src/testing/bytes.ts +1 -1
@@ -0,0 +1,27 @@
1
+ /**
2
+ * An INCREMENTAL SHA-256 — fed chunk by chunk, for a Blob too big to hold in one buffer.
3
+ *
4
+ * 🔴 Why not `crypto.subtle.digest`: it takes the whole message in one buffer, and a PS2 disc is
5
+ * up to 8.5 GB, a camera video several. Born in roms (`src/ps2/sha256.ts`, the disc download's
6
+ * verifier) and moved here so every app's upload pre-check hashes with the same code
7
+ * (forge task 3579). `sha256.spec.ts` pins it against WebCrypto across every chunk boundary that
8
+ * matters (55/56/64 bytes, and a 1 MB split at an odd offset).
9
+ *
10
+ * Measured on this Mac under Bun, 256 MiB in 8 MiB slices: 184 MiB/s here, 195 MiB/s for
11
+ * `@noble/hashes`, 1,960 MiB/s for `crypto.subtle` — which is why {@link sha256OfBlob} only falls
12
+ * back to this past the size it is willing to read into memory at once.
13
+ */
14
+ export declare class Sha256 {
15
+ private readonly h;
16
+ /** Back to the empty message — a download that has to start over. */
17
+ reset(): this;
18
+ private readonly w;
19
+ private readonly tail;
20
+ private tailLength;
21
+ /** Total bytes fed, as a double — exact to 2^53, far past any disc. */
22
+ private length;
23
+ update(data: Uint8Array): this;
24
+ /** The digest as lowercase hex. The hasher is spent afterwards. */
25
+ hex(): string;
26
+ private block;
27
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * An INCREMENTAL SHA-256 — fed chunk by chunk, for a Blob too big to hold in one buffer.
3
+ *
4
+ * 🔴 Why not `crypto.subtle.digest`: it takes the whole message in one buffer, and a PS2 disc is
5
+ * up to 8.5 GB, a camera video several. Born in roms (`src/ps2/sha256.ts`, the disc download's
6
+ * verifier) and moved here so every app's upload pre-check hashes with the same code
7
+ * (forge task 3579). `sha256.spec.ts` pins it against WebCrypto across every chunk boundary that
8
+ * matters (55/56/64 bytes, and a 1 MB split at an odd offset).
9
+ *
10
+ * Measured on this Mac under Bun, 256 MiB in 8 MiB slices: 184 MiB/s here, 195 MiB/s for
11
+ * `@noble/hashes`, 1,960 MiB/s for `crypto.subtle` — which is why {@link sha256OfBlob} only falls
12
+ * back to this past the size it is willing to read into memory at once.
13
+ */
14
+ const K = new Uint32Array([
15
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98,
16
+ 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
17
+ 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8,
18
+ 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
19
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,
20
+ 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
21
+ 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
22
+ 0xc67178f2,
23
+ ]);
24
+ const INITIAL = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
25
+ export class Sha256 {
26
+ h = new Uint32Array(INITIAL);
27
+ /** Back to the empty message — a download that has to start over. */
28
+ reset() {
29
+ this.h.set(INITIAL);
30
+ this.tailLength = 0;
31
+ this.length = 0;
32
+ return this;
33
+ }
34
+ w = new Uint32Array(64);
35
+ tail = new Uint8Array(64);
36
+ tailLength = 0;
37
+ /** Total bytes fed, as a double — exact to 2^53, far past any disc. */
38
+ length = 0;
39
+ update(data) {
40
+ this.length += data.length;
41
+ let offset = 0;
42
+ if (this.tailLength > 0) {
43
+ const take = Math.min(64 - this.tailLength, data.length);
44
+ this.tail.set(data.subarray(0, take), this.tailLength);
45
+ this.tailLength += take;
46
+ offset = take;
47
+ if (this.tailLength < 64)
48
+ return this;
49
+ this.block(this.tail, 0);
50
+ this.tailLength = 0;
51
+ }
52
+ const whole = data.length - ((data.length - offset) % 64);
53
+ for (; offset < whole; offset += 64)
54
+ this.block(data, offset);
55
+ if (offset < data.length) {
56
+ this.tail.set(data.subarray(offset), 0);
57
+ this.tailLength = data.length - offset;
58
+ }
59
+ return this;
60
+ }
61
+ /** The digest as lowercase hex. The hasher is spent afterwards. */
62
+ hex() {
63
+ const bits = this.length * 8;
64
+ const pad = new Uint8Array(this.tailLength < 56 ? 64 : 128);
65
+ pad.set(this.tail.subarray(0, this.tailLength));
66
+ pad[this.tailLength] = 0x80;
67
+ const view = new DataView(pad.buffer);
68
+ view.setUint32(pad.length - 8, Math.floor(bits / 2 ** 32));
69
+ view.setUint32(pad.length - 4, bits >>> 0);
70
+ for (let offset = 0; offset < pad.length; offset += 64)
71
+ this.block(pad, offset);
72
+ let out = "";
73
+ for (const word of this.h)
74
+ out += word.toString(16).padStart(8, "0");
75
+ return out;
76
+ }
77
+ block(data, offset) {
78
+ const w = this.w;
79
+ for (let i = 0; i < 16; i++) {
80
+ const j = offset + i * 4;
81
+ w[i] = (data[j] << 24) | (data[j + 1] << 16) | (data[j + 2] << 8) | data[j + 3];
82
+ }
83
+ for (let i = 16; i < 64; i++) {
84
+ const a = w[i - 15];
85
+ const b = w[i - 2];
86
+ const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);
87
+ const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);
88
+ w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0;
89
+ }
90
+ const h = this.h;
91
+ let a = h[0];
92
+ let b = h[1];
93
+ let c = h[2];
94
+ let d = h[3];
95
+ let e = h[4];
96
+ let f = h[5];
97
+ let g = h[6];
98
+ let hh = h[7];
99
+ for (let i = 0; i < 64; i++) {
100
+ const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
101
+ const ch = (e & f) ^ (~e & g);
102
+ const t1 = (hh + S1 + ch + K[i] + w[i]) | 0;
103
+ const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
104
+ const maj = (a & b) ^ (a & c) ^ (b & c);
105
+ const t2 = (S0 + maj) | 0;
106
+ hh = g;
107
+ g = f;
108
+ f = e;
109
+ e = (d + t1) | 0;
110
+ d = c;
111
+ c = b;
112
+ b = a;
113
+ a = (t1 + t2) | 0;
114
+ }
115
+ h[0] = (h[0] + a) | 0;
116
+ h[1] = (h[1] + b) | 0;
117
+ h[2] = (h[2] + c) | 0;
118
+ h[3] = (h[3] + d) | 0;
119
+ h[4] = (h[4] + e) | 0;
120
+ h[5] = (h[5] + f) | 0;
121
+ h[6] = (h[6] + g) | 0;
122
+ h[7] = (h[7] + hh) | 0;
123
+ }
124
+ }
@@ -1,9 +1,9 @@
1
1
  /**
2
- * A REAL marshalling worker entry, used by `marshalWorker.spec.ts` to prove the
2
+ * A REAL marshaling worker entry, used by `marshalWorker.spec.ts` to prove the
3
3
  * round-trip over an actual OS thread rather than over an injected fake.
4
4
  *
5
5
  * This is also the shape every production entry has, so it doubles as the
6
- * reference an app author copies: construct the marshaller, hand it to
6
+ * reference an app author copies: construct the marshaler, hand it to
7
7
  * `serveMarshalWorker`, done. The host never imports this file — it names it,
8
8
  * and the worker imports it.
9
9
  */
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * This entry exists because of Law #1, for the same reason `client/index.ts`
5
5
  * gives that reason for `createWireHub`: an ingress route has to be able to
6
- * IMPORT the marshalling seam, and a worker entry has to be able to import
6
+ * IMPORT the marshaling seam, and a worker entry has to be able to import
7
7
  * `serveMarshalWorker`. Until the Worker commit (05 §B1) neither symbol had any
8
8
  * export path at all — `marshalInbound` was reachable only from inside this
9
9
  * package, which is most of why it went three arcs with zero callers.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * This entry exists because of Law #1, for the same reason `client/index.ts`
5
5
  * gives that reason for `createWireHub`: an ingress route has to be able to
6
- * IMPORT the marshalling seam, and a worker entry has to be able to import
6
+ * IMPORT the marshaling seam, and a worker entry has to be able to import
7
7
  * `serveMarshalWorker`. Until the Worker commit (05 §B1) neither symbol had any
8
8
  * export path at all — `marshalInbound` was reachable only from inside this
9
9
  * package, which is most of why it went three arcs with zero callers.
@@ -1,11 +1,11 @@
1
1
  import { type MarshallerRegistry } from './marshalling.js';
2
- /** No marshaller is registered for the token an ingress was asked to feed. */
2
+ /** No marshaler is registered for the token an ingress was asked to feed. */
3
3
  export declare const INGRESS_TOKEN_UNKNOWN = "INGRESS_TOKEN_UNKNOWN";
4
4
  export interface MarshalledIngressOptions {
5
5
  /** The registry the app's wire tokens were registered on. */
6
6
  readonly registry: MarshallerRegistry;
7
7
  /**
8
- * Per-payload worker ceiling. A marshalling worker that hangs would otherwise
8
+ * Per-payload worker ceiling. A marshaling worker that hangs would otherwise
9
9
  * hold the request open for as long as the client is willing to wait.
10
10
  */
11
11
  readonly workerTimeoutMs?: number;
@@ -21,12 +21,12 @@ export interface MarshalledIngress {
21
21
  *
22
22
  * @throws {AppError} `INGRESS_TOKEN_UNKNOWN` (404) when nothing is registered;
23
23
  * `MARSHALLER_PAYLOAD_TOO_LARGE` (413/503), `MARSHALLER_VALIDATION_FAILED`
24
- * (400) or `MARSHALLER_SCHEMA_VIOLATION` (502) from the marshaller itself.
24
+ * (400) or `MARSHALLER_SCHEMA_VIOLATION` (502) from the marshaler itself.
25
25
  */
26
26
  ingest<Out = unknown>(tokenId: string, payload: unknown): Promise<Out>;
27
27
  /**
28
28
  * Read a `Request` body and marshal it for `tokenId`. JSON bodies are parsed;
29
- * anything else is handed to the marshaller as bytes, because a marshaller for
29
+ * anything else is handed to the marshaler as bytes, because a marshaler for
30
30
  * a CSV or a binary feed wants the bytes and re-encoding them would be lossy.
31
31
  */
32
32
  ingestRequest<Out = unknown>(tokenId: string, request: Request): Promise<Out>;
@@ -13,25 +13,25 @@
13
13
  * webhook endpoint that no product asked for would create a new orphan seam of
14
14
  * exactly the kind Law #1 exists to prevent, and it would guess wrong about the
15
15
  * part that actually differs between a webhook and an import (authentication,
16
- * routing, what the marshalled value is handed to). What does NOT differ — and
16
+ * routing, what the marshaled value is handed to). What does NOT differ — and
17
17
  * what every ingress would otherwise re-derive, differently — is this:
18
18
  *
19
19
  * 1. reading the body as bytes without buffering a stream into RAM twice;
20
- * 2. finding the marshaller for the token being fed;
20
+ * 2. finding the marshaler for the token being fed;
21
21
  * 3. owning a worker per `workerEntry` — a live OS thread, so exactly one
22
22
  * thing must own disposing of it — and resolving it lazily and cached;
23
23
  * 4. handing those failures up as errors that already KNOW their HTTP status,
24
- * so no route re-derives the mapping. The marshalling tier's codes are
24
+ * so no route re-derives the mapping. The marshaling tier's codes are
25
25
  * unchanged and deliberately distinct: 413 over the declared ceiling, 400
26
26
  * for a payload `validate` refuses (the caller's shape is wrong), 502 when
27
27
  * the TRANSFORM OUTPUT violates the token's canonical schema (an upstream
28
28
  * provider changed shape under us — not our error budget), 503 for a
29
- * payload that needs offload from a marshaller with no worker.
29
+ * payload that needs offload from a marshaler with no worker.
30
30
  *
31
31
  * Step 3 is the one that would go wrong. `createMarshalWorkerClient` spawns a
32
32
  * thread per call; a route handler that called it per request would leak one
33
33
  * OS thread per webhook, and the symptom would be a server that gets slower for
34
- * a week and then dies. Nothing about that is discoverable from the marshalling
34
+ * a week and then dies. Nothing about that is discoverable from the marshaling
35
35
  * API, which is precisely why it belongs here once instead of in each ingress.
36
36
  *
37
37
  * ## Adding an ingress later
@@ -47,13 +47,13 @@
47
47
  * });
48
48
  * ```
49
49
  *
50
- * A CSV import is the same three lines with a different marshaller and a
50
+ * A CSV import is the same three lines with a different marshaler and a
51
51
  * different service call. Neither needs to know that a worker exists.
52
52
  */
53
53
  import { AppError } from '../../shared/errors.js';
54
54
  import { createMarshalWorkerClient } from './marshalWorker.js';
55
55
  import { marshalInbound, } from './marshalling.js';
56
- /** No marshaller is registered for the token an ingress was asked to feed. */
56
+ /** No marshaler is registered for the token an ingress was asked to feed. */
57
57
  export const INGRESS_TOKEN_UNKNOWN = 'INGRESS_TOKEN_UNKNOWN';
58
58
  /**
59
59
  * Build the ingress. One per app — it caches a worker per `workerEntry`, and two
@@ -68,7 +68,7 @@ export const createMarshalledIngress = (options) => {
68
68
  if (entry === undefined)
69
69
  return undefined;
70
70
  if (disposed)
71
- throw new AppError('This ingress has been disposed and cannot spawn a marshalling worker.', {
71
+ throw new AppError('This ingress has been disposed and cannot spawn a marshaling worker.', {
72
72
  code: 'INGRESS_DISPOSED',
73
73
  status: 503,
74
74
  isOperational: false,
@@ -88,10 +88,10 @@ export const createMarshalledIngress = (options) => {
88
88
  const marshallerFor = (tokenId) => {
89
89
  const marshaller = options.registry.forToken(tokenId);
90
90
  if (!marshaller)
91
- // 404, not 400: from outside, a token with no marshaller and a token that
91
+ // 404, not 400: from outside, a token with no marshaler and a token that
92
92
  // does not exist are the same fact, and distinguishing them would make
93
93
  // this endpoint an enumeration oracle for the app's wire surface.
94
- throw new AppError(`No marshaller is registered for wire token '${tokenId}'.`, {
94
+ throw new AppError(`No marshaler is registered for wire token '${tokenId}'.`, {
95
95
  code: INGRESS_TOKEN_UNKNOWN,
96
96
  status: 404,
97
97
  context: { tokenId },
@@ -106,8 +106,8 @@ export const createMarshalledIngress = (options) => {
106
106
  },
107
107
  async ingestRequest(tokenId, request) {
108
108
  const contentType = request.headers.get('content-type') ?? '';
109
- // JSON is parsed here so the marshaller sees a value rather than a string
110
- // it would have to parse itself (and parse DIFFERENTLY per marshaller).
109
+ // JSON is parsed here so the marshaler sees a value rather than a string
110
+ // it would have to parse itself (and parse DIFFERENTLY per marshaler).
111
111
  // A malformed body is the CALLER's error — 400 — and must not read as a
112
112
  // validation failure of a payload we never managed to decode.
113
113
  const payload = contentType.includes('application/json')
@@ -58,7 +58,7 @@ export interface MarshalWorkerClient extends MarshalWorker {
58
58
  dispose(): Promise<void>;
59
59
  }
60
60
  /**
61
- * A long-lived worker for ONE marshaller, multiplexing concurrent payloads.
61
+ * A long-lived worker for ONE marshaler, multiplexing concurrent payloads.
62
62
  *
63
63
  * Long-lived rather than one-worker-per-payload deliberately: thread startup
64
64
  * plus module load is tens of milliseconds, which on a burst of imports would
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The marshalling Worker (05 §B1, second half) — `validate` + `transform` for a
2
+ * The marshaling Worker (05 §B1, second half) — `validate` + `transform` for a
3
3
  * large inbound payload, off the thread that has to answer requests.
4
4
  *
5
5
  * `marshalling.ts` has always had the SIZE BOUNDARY and the injection seam; what
@@ -8,19 +8,19 @@
8
8
  * the missing half: a real, long-lived worker with a request/response protocol,
9
9
  * on both runtimes cursedbelt ships to.
10
10
  *
11
- * ## Why the marshaller names its own worker entry
11
+ * ## Why the marshaler names its own worker entry
12
12
  *
13
- * A marshaller is mostly FUNCTIONS (`validate`, `transform`, a Zod schema), and
13
+ * A marshaler is mostly FUNCTIONS (`validate`, `transform`, a Zod schema), and
14
14
  * functions do not survive structured clone — a worker cannot be handed one, it
15
15
  * has to `import` it. The obvious design (one shared worker that resolves any
16
- * marshaller by id from a registry module) needs the host to declare, register
17
- * and keep in sync a second module listing every marshaller, and it gives that
16
+ * marshaler by id from a registry module) needs the host to declare, register
17
+ * and keep in sync a second module listing every marshaler, and it gives that
18
18
  * one worker the authority to run all of them.
19
19
  *
20
- * Instead each marshaller declares `workerEntry`: the module that constructs
21
- * exactly that marshaller and calls {@link serveMarshalWorker}. The worker can
22
- * therefore only ever run the one marshaller it was built for, the host wires
23
- * nothing, and — the part that matters for the gate — "can this marshaller
20
+ * Instead each marshaler declares `workerEntry`: the module that constructs
21
+ * exactly that marshaler and calls {@link serveMarshalWorker}. The worker can
22
+ * therefore only ever run the one marshaler it was built for, the host wires
23
+ * nothing, and — the part that matters for the gate — "can this marshaler
24
24
  * offload?" stops being a boolean somebody remembered to set and becomes a
25
25
  * question about an artifact that either exists or does not. V3 checks it
26
26
  * statically (`marshaller-worker-offload-missing`), so the 503 path below is
@@ -94,7 +94,7 @@ export async function serveMarshalWorker(marshaller) {
94
94
  id: request.id,
95
95
  ok: false,
96
96
  code: MARSHALLER_VALIDATION_FAILED,
97
- message: `Inbound payload rejected by marshaller '${marshaller.id}'.`,
97
+ message: `Inbound payload rejected by marshaler '${marshaller.id}'.`,
98
98
  });
99
99
  return;
100
100
  }
@@ -185,7 +185,7 @@ function spawnHostWorker(entry) {
185
185
  };
186
186
  }
187
187
  /**
188
- * A long-lived worker for ONE marshaller, multiplexing concurrent payloads.
188
+ * A long-lived worker for ONE marshaler, multiplexing concurrent payloads.
189
189
  *
190
190
  * Long-lived rather than one-worker-per-payload deliberately: thread startup
191
191
  * plus module load is tens of milliseconds, which on a burst of imports would
@@ -229,7 +229,7 @@ export function createMarshalWorkerClient(options) {
229
229
  worker.onError((error) => {
230
230
  const message = error instanceof Error ? error.message : String(error);
231
231
  for (const id of [...pending.keys()])
232
- settle(id, (p) => p.reject(new AppError(`Marshalling worker failed: ${message}`, {
232
+ settle(id, (p) => p.reject(new AppError(`Marshaling worker failed: ${message}`, {
233
233
  code: MARSHALLER_WORKER_FAILED,
234
234
  status: 500,
235
235
  context: { worker: String(options.entry) },
@@ -238,7 +238,7 @@ export function createMarshalWorkerClient(options) {
238
238
  return {
239
239
  run(_marshallerId, payload) {
240
240
  if (disposed)
241
- return Promise.reject(new AppError('Marshalling worker has been disposed.', {
241
+ return Promise.reject(new AppError('Marshaling worker has been disposed.', {
242
242
  code: MARSHALLER_WORKER_FAILED,
243
243
  status: 500,
244
244
  context: { worker: String(options.entry) },
@@ -247,7 +247,7 @@ export function createMarshalWorkerClient(options) {
247
247
  return new Promise((resolve, reject) => {
248
248
  const timer = setTimeout(() => {
249
249
  pending.delete(id);
250
- reject(new AppError(`Marshalling worker did not answer within ${timeoutMs}ms.`, {
250
+ reject(new AppError(`Marshaling worker did not answer within ${timeoutMs}ms.`, {
251
251
  code: MARSHALLER_WORKER_TIMEOUT,
252
252
  status: 504,
253
253
  context: { worker: String(options.entry), timeoutMs },
@@ -260,7 +260,7 @@ export function createMarshalWorkerClient(options) {
260
260
  async dispose() {
261
261
  disposed = true;
262
262
  for (const id of [...pending.keys()])
263
- settle(id, (p) => p.reject(new AppError('Marshalling worker was disposed while a payload was in flight.', {
263
+ settle(id, (p) => p.reject(new AppError('Marshaling worker was disposed while a payload was in flight.', {
264
264
  code: MARSHALLER_WORKER_FAILED,
265
265
  status: 503,
266
266
  context: { worker: String(options.entry) },
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Port marshalling + the 512KB worker gate (02 §7.1).
2
+ * Port marshaling + the 512KB worker gate (02 §7.1).
3
3
  *
4
4
  * External payloads — webhooks, third-party feeds, spreadsheet imports — never
5
- * touch component code. A marshaller registered on the token is the only entry,
5
+ * touch component code. A marshaler registered on the token is the only entry,
6
6
  * and its `transform` output is checked against the token's canonical Zod schema
7
7
  * (V3 asserts this statically; here it is enforced at runtime too, because a
8
8
  * provider can change its shape without anyone recompiling).
@@ -18,7 +18,7 @@ import type { z } from 'zod';
18
18
  /** Above this, `validate` + `transform` are offloaded to a Worker (02 §7.1). */
19
19
  export declare const MARSHALLER_WORKER_OFFLOAD_BYTES: number;
20
20
  /**
21
- * The ONE marshaller contract (05 §B1, unified 2026-07-26).
21
+ * The ONE marshaler contract (05 §B1, unified 2026-07-26).
22
22
  *
23
23
  * There were TWO interfaces of this name in this package: this one, and a
24
24
  * divergent copy in `core/tokens/registry.ts` that additionally required
@@ -32,24 +32,24 @@ export declare const MARSHALLER_WORKER_OFFLOAD_BYTES: number;
32
32
  */
33
33
  export interface CCWireMarshaller<Inbound = unknown, Out = unknown> {
34
34
  id: string;
35
- /** The wire token this marshaller feeds — V3 resolves it against the registry. */
35
+ /** The wire token this marshaler feeds — V3 resolves it against the registry. */
36
36
  tokenId: string;
37
37
  /** REQUIRED — V3 `marshaller-payload-unbounded` when absent or non-positive. */
38
38
  maxPayloadBytes: number;
39
39
  /**
40
- * The module that runs this marshaller's `validate`+`transform` off the main
40
+ * The module that runs this marshaler's `validate`+`transform` off the main
41
41
  * thread — an entry calling `serveMarshalWorker` (`core/wire/marshalWorker.ts`).
42
42
  *
43
43
  * **This replaced a `workerOffload: boolean` in the Worker commit (05 §B1).**
44
44
  * That flag was authored by hand and asserted a capability the codebase did
45
- * not have: every marshaller in the repo set it `true` while no worker
45
+ * not have: every marshaler in the repo set it `true` while no worker
46
46
  * existed anywhere, so the V3 gate it fed was checking that someone had
47
- * remembered to type `true`. Whether a marshaller can offload is now a
47
+ * remembered to type `true`. Whether a marshaler can offload is now a
48
48
  * question about an ARTIFACT — this entry either exists or it does not — and
49
49
  * whether it NEEDS to is {@link requiresWorkerOffload}, derived from
50
50
  * `maxPayloadBytes`. Neither is a promise anyone can make by hand.
51
51
  *
52
- * Optional because a marshaller whose `maxPayloadBytes` sits under the
52
+ * Optional because a marshaler whose `maxPayloadBytes` sits under the
53
53
  * main-thread limit can never reach the offload path; V3 requires it of
54
54
  * exactly those that can.
55
55
  */
@@ -60,9 +60,9 @@ export interface CCWireMarshaller<Inbound = unknown, Out = unknown> {
60
60
  targetZodSchema: z.ZodType<Out>;
61
61
  }
62
62
  /**
63
- * Can this marshaller ever reach the offload path? A DERIVED fact (05 §B1).
63
+ * Can this marshaler ever reach the offload path? A DERIVED fact (05 §B1).
64
64
  *
65
- * A marshaller that refuses anything over 256KB can never produce a payload
65
+ * A marshaler that refuses anything over 256KB can never produce a payload
66
66
  * above the 512KB main-thread limit, so it needs no worker and demanding one
67
67
  * would be noise. One whose ceiling is 2MB certainly can, and without a
68
68
  * `workerEntry` its large payloads are a guaranteed 503 — which is why V3
@@ -87,13 +87,13 @@ export interface MarshalWorker {
87
87
  }
88
88
  export interface MarshalOptions {
89
89
  /**
90
- * Use THIS worker instead of the one the marshaller's `workerEntry` names.
90
+ * Use THIS worker instead of the one the marshaler's `workerEntry` names.
91
91
  * Left unset in production: offload is the default path, resolved from the
92
- * marshaller itself, so an ingress route needs no worker wiring of its own.
92
+ * marshaler itself, so an ingress route needs no worker wiring of its own.
93
93
  */
94
94
  worker?: MarshalWorker;
95
95
  /**
96
- * Resolves a worker for a marshaller that declares a `workerEntry`. The host
96
+ * Resolves a worker for a marshaler that declares a `workerEntry`. The host
97
97
  * supplies this once (`createMarshalWorkerClient`, cached per entry) — it is a
98
98
  * parameter rather than a module-level singleton because a worker is a live
99
99
  * OS thread, and something has to own disposing of it.
@@ -103,14 +103,14 @@ export interface MarshalOptions {
103
103
  offloadThresholdBytes?: number;
104
104
  }
105
105
  /**
106
- * Run a payload through its marshaller, offloading past the size gate.
106
+ * Run a payload through its marshaler, offloading past the size gate.
107
107
  *
108
- * Order matters: the marshaller's OWN `maxPayloadBytes` is checked first, so an
108
+ * Order matters: the marshaler's OWN `maxPayloadBytes` is checked first, so an
109
109
  * oversized payload is refused with a typed diagnostic before any work — worker
110
110
  * or otherwise — is scheduled for it.
111
111
  */
112
112
  export declare function marshalInbound<Inbound, Out>(marshaller: CCWireMarshaller<Inbound, Out>, payload: Inbound, options?: MarshalOptions): Promise<Out>;
113
- /** The registry of marshallers, keyed by token id (02 §7.1: registered ON the token). */
113
+ /** The registry of marshalers, keyed by token id (02 §7.1: registered ON the token). */
114
114
  export interface MarshallerRegistry {
115
115
  register(tokenId: string, marshaller: CCWireMarshaller): void;
116
116
  forToken(tokenId: string): CCWireMarshaller | undefined;
@@ -2,9 +2,9 @@ import { AppError } from '../../shared/errors.js';
2
2
  /** Above this, `validate` + `transform` are offloaded to a Worker (02 §7.1). */
3
3
  export const MARSHALLER_WORKER_OFFLOAD_BYTES = 512 * 1024;
4
4
  /**
5
- * Can this marshaller ever reach the offload path? A DERIVED fact (05 §B1).
5
+ * Can this marshaler ever reach the offload path? A DERIVED fact (05 §B1).
6
6
  *
7
- * A marshaller that refuses anything over 256KB can never produce a payload
7
+ * A marshaler that refuses anything over 256KB can never produce a payload
8
8
  * above the 512KB main-thread limit, so it needs no worker and demanding one
9
9
  * would be noise. One whose ceiling is 2MB certainly can, and without a
10
10
  * `workerEntry` its large payloads are a guaranteed 503 — which is why V3
@@ -41,16 +41,16 @@ export function payloadBytes(payload) {
41
41
  }
42
42
  }
43
43
  /**
44
- * Run a payload through its marshaller, offloading past the size gate.
44
+ * Run a payload through its marshaler, offloading past the size gate.
45
45
  *
46
- * Order matters: the marshaller's OWN `maxPayloadBytes` is checked first, so an
46
+ * Order matters: the marshaler's OWN `maxPayloadBytes` is checked first, so an
47
47
  * oversized payload is refused with a typed diagnostic before any work — worker
48
48
  * or otherwise — is scheduled for it.
49
49
  */
50
50
  export async function marshalInbound(marshaller, payload, options = {}) {
51
51
  const bytes = payloadBytes(payload);
52
52
  if (bytes > marshaller.maxPayloadBytes)
53
- throw new AppError(`Payload of ${bytes} bytes exceeds marshaller '${marshaller.id}' limit of ${marshaller.maxPayloadBytes}.`, {
53
+ throw new AppError(`Payload of ${bytes} bytes exceeds marshaler '${marshaller.id}' limit of ${marshaller.maxPayloadBytes}.`, {
54
54
  code: MARSHALLER_PAYLOAD_TOO_LARGE,
55
55
  status: 413,
56
56
  context: { marshaller: marshaller.id, bytes, maxPayloadBytes: marshaller.maxPayloadBytes },
@@ -58,13 +58,13 @@ export async function marshalInbound(marshaller, payload, options = {}) {
58
58
  const threshold = options.offloadThresholdBytes ?? MARSHALLER_WORKER_OFFLOAD_BYTES;
59
59
  if (bytes > threshold) {
60
60
  // Offload is the DEFAULT path (05 §B1): an explicitly injected worker wins,
61
- // otherwise the marshaller's own `workerEntry` is resolved through the host's
62
- // resolver. Neither present means this marshaller has no offload path at all
61
+ // otherwise the marshaler's own `workerEntry` is resolved through the host's
62
+ // resolver. Neither present means this marshaler has no offload path at all
63
63
  // — which V3 refuses to verify, so a verified app never reaches this throw.
64
64
  const worker = options.worker ??
65
65
  (marshaller.workerEntry ? options.resolveWorker?.(marshaller) : undefined);
66
66
  if (!worker)
67
- throw new AppError(`Payload of ${bytes} bytes exceeds the ${threshold}-byte main-thread limit and marshaller '${marshaller.id}' has no marshalling worker.`, {
67
+ throw new AppError(`Payload of ${bytes} bytes exceeds the ${threshold}-byte main-thread limit and marshaler '${marshaller.id}' has no marshaling worker.`, {
68
68
  code: MARSHALLER_PAYLOAD_TOO_LARGE,
69
69
  status: 503,
70
70
  context: {
@@ -73,8 +73,8 @@ export async function marshalInbound(marshaller, payload, options = {}) {
73
73
  threshold,
74
74
  workerEntry: marshaller.workerEntry ?? null,
75
75
  hint: marshaller.workerEntry
76
- ? 'The marshaller declares a workerEntry but no resolveWorker was supplied — pass createMarshalWorkerClient through MarshalOptions.resolveWorker.'
77
- : 'Give the marshaller a workerEntry (a module calling serveMarshalWorker), or lower maxPayloadBytes below the main-thread limit.',
76
+ ? 'The marshaler declares a workerEntry but no resolveWorker was supplied — pass createMarshalWorkerClient through MarshalOptions.resolveWorker.'
77
+ : 'Give the marshaler a workerEntry (a module calling serveMarshalWorker), or lower maxPayloadBytes below the main-thread limit.',
78
78
  },
79
79
  });
80
80
  // The worker validates AND transforms; the result still gets schema-checked
@@ -83,7 +83,7 @@ export async function marshalInbound(marshaller, payload, options = {}) {
83
83
  return assertSchema(marshaller, result);
84
84
  }
85
85
  if (!marshaller.validate(payload))
86
- throw new AppError(`Inbound payload rejected by marshaller '${marshaller.id}'.`, {
86
+ throw new AppError(`Inbound payload rejected by marshaler '${marshaller.id}'.`, {
87
87
  code: MARSHALLER_VALIDATION_FAILED,
88
88
  status: 400,
89
89
  context: { marshaller: marshaller.id },
@@ -98,7 +98,7 @@ export async function marshalInbound(marshaller, payload, options = {}) {
98
98
  function assertSchema(marshaller, value) {
99
99
  const parsed = marshaller.targetZodSchema.safeParse(value);
100
100
  if (!parsed.success)
101
- throw new AppError(`Marshaller '${marshaller.id}' produced output that violates its token's schema.`, {
101
+ throw new AppError(`Marshaler '${marshaller.id}' produced output that violates its token's schema.`, {
102
102
  code: MARSHALLER_SCHEMA_VIOLATION,
103
103
  status: 502,
104
104
  context: {
@@ -113,7 +113,7 @@ export function createMarshallerRegistry() {
113
113
  return {
114
114
  register(tokenId, marshaller) {
115
115
  if (marshaller.maxPayloadBytes === undefined || marshaller.maxPayloadBytes <= 0)
116
- throw new AppError(`Marshaller '${marshaller.id}' must declare a positive maxPayloadBytes.`, {
116
+ throw new AppError(`Marshaler '${marshaller.id}' must declare a positive maxPayloadBytes.`, {
117
117
  code: 'marshaller-payload-unbounded',
118
118
  status: 500,
119
119
  context: { marshaller: marshaller.id, tokenId },
@@ -66,7 +66,7 @@ async function formDataToBody(formData) {
66
66
  * The request body as a JS value: a JSON string is parsed, a plain string rides as itself.
67
67
  *
68
68
  * 🔴 Anything else is REFUSED. The tunnel carries a JSON value or a form's files; a `File`, a
69
- * `Blob`, bytes, a stream or `URLSearchParams` handed through here was JSON-serialised as `{}` —
69
+ * `Blob`, bytes, a stream or `URLSearchParams` handed through here was JSON-serialized as `{}` —
70
70
  * desk's import preview posted a 3.4 MB workbook and the server described "a JSON file" with `{}`
71
71
  * as its sample (forge task 3443). A loud error at the call is the bug; a silent `{}` is data loss.
72
72
  * Send an upload as `FormData`, which is carried whole.
@@ -75,7 +75,7 @@ function bodyFromInit(body, method, url) {
75
75
  if (body === null || body === undefined)
76
76
  return undefined;
77
77
  if (typeof body !== 'string') {
78
- // A plain object or array a caller cast to `BodyInit` serialises as itself — carried as before.
78
+ // A plain object or array a caller cast to `BodyInit` serializes as itself — carried as before.
79
79
  const proto = Object.getPrototypeOf(body);
80
80
  if (Array.isArray(body) || proto === Object.prototype || proto === null)
81
81
  return body;
@@ -28,5 +28,5 @@
28
28
  export declare const asBody: (bytes: Uint8Array) => Uint8Array<ArrayBuffer>;
29
29
  /** `new Response(bytes)` with the assertion applied — the common case. */
30
30
  export declare const bytesResponse: (bytes: Uint8Array, init?: ResponseInit) => Response;
31
- /** A readable stream of `bytes` — what the media catalogue's ingest paths take. */
31
+ /** A readable stream of `bytes` — what the media catalog's ingest paths take. */
32
32
  export declare const bytesStream: (bytes: Uint8Array | string) => ReadableStream<Uint8Array>;
@@ -28,6 +28,6 @@
28
28
  export const asBody = (bytes) => bytes;
29
29
  /** `new Response(bytes)` with the assertion applied — the common case. */
30
30
  export const bytesResponse = (bytes, init) => new Response(asBody(bytes), init);
31
- /** A readable stream of `bytes` — what the media catalogue's ingest paths take. */
31
+ /** A readable stream of `bytes` — what the media catalog's ingest paths take. */
32
32
  export const bytesStream = (bytes) => new Response(typeof bytes === 'string' ? asBody(new TextEncoder().encode(bytes)) : asBody(bytes))
33
33
  .body;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt-core",
3
- "version": "2.13.2",
3
+ "version": "2.14.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.",
@@ -133,6 +133,12 @@
133
133
  "source": "./src/core/duplicates/index.ts",
134
134
  "import": "./dist/core/duplicates/index.js"
135
135
  },
136
+ "./upload-precheck": {
137
+ "types": "./dist/core/upload-precheck/index.d.ts",
138
+ "bun": "./src/core/upload-precheck/index.ts",
139
+ "source": "./src/core/upload-precheck/index.ts",
140
+ "import": "./dist/core/upload-precheck/index.js"
141
+ },
136
142
  "./errors": {
137
143
  "types": "./dist/shared/errors.d.ts",
138
144
  "bun": "./src/shared/errors.ts",
@@ -18,7 +18,7 @@ import {
18
18
  unitForSpan,
19
19
  } from './range.js';
20
20
 
21
- /** 2026-08-13 14:30 LOCAL — the tests are about local calendar behaviour. */
21
+ /** 2026-08-13 14:30 LOCAL — the tests are about local calendar behavior. */
22
22
  const NOW = new Date(2026, 7, 13, 14, 30, 0).getTime();
23
23
 
24
24
  describe('the presets', () => {