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,315 @@
1
+ /**
2
+ * Decide, for a drop of files, which ones to SEND — before sending any (forge task 3579).
3
+ *
4
+ * The order of the work is the whole design, cheapest question first:
5
+ *
6
+ * 1. **Sizes, one request.** A file whose byte size the library holds nothing of — and that no
7
+ * other file in this drop shares — cannot be a copy of anything. It is uploaded at once, never
8
+ * hashed here (binary-server hashes what it receives anyway). A folder of new videos is decided
9
+ * almost entirely by this step.
10
+ * 2. **The memo.** A file this browser (or script) hashed before, at the same path with the same
11
+ * size and date, takes its remembered hash — a second import of the same huge folder reads none
12
+ * of its bytes.
13
+ * 3. **Hash the rest**, a few at a time and within a memory budget, and ask the library about the
14
+ * hashes AS they finish: one request in flight, each carrying every hash completed since the
15
+ * last. Uploads of the first new files begin while later ones are still being hashed.
16
+ * 4. **A repeat inside the drop** (the same bytes under two names) is caught by its hash without
17
+ * asking anybody, and only the first is sent.
18
+ *
19
+ * A pre-check that FAILS never blocks an upload: the sizes question failing sends everything as
20
+ * it would have before the pre-check existed (each app's own after-the-fact check still stands),
21
+ * and is reported as `degraded`. It costs bandwidth, never a file.
22
+ */
23
+ import { PRECHECK_MAX_HASHES, PRECHECK_MAX_SIZES, type PrecheckHit, type PrecheckRequest, type PrecheckResponse } from "./contract.js";
24
+ import { type HashOptions, NATIVE_HASH_MAX_BYTES, STREAM_SLICE_BYTES, sha256OfBlob } from "./hash.js";
25
+ import { type HashMemo, memoKey } from "./memo.js";
26
+
27
+ /** Asks the app's pre-check route. Each app supplies its own fetch. */
28
+ export type PrecheckAsk = (request: PrecheckRequest) => Promise<PrecheckResponse>;
29
+
30
+ export type Verdict<T> =
31
+ /** Send it. `sha256` is set when it was hashed on the way (an app may pass it on). */
32
+ | { kind: "upload"; item: T; sha256?: string }
33
+ /** The library already answers for these bytes. Do not send. */
34
+ | { kind: "duplicate"; item: T; sha256: string; hit: PrecheckHit }
35
+ /** Another file in this same drop has these bytes; only that one is sent. */
36
+ | { kind: "repeat"; item: T; sha256: string; of: T };
37
+
38
+ export interface PlanStats {
39
+ files: number;
40
+ bytes: number;
41
+ /** Files sent without being hashed, because their size matched nothing. */
42
+ sizeOnly: number;
43
+ /** Files whose hash came from the memo — no bytes read. */
44
+ memoHits: number;
45
+ /** Files hashed here, and how many bytes that read. */
46
+ hashedFiles: number;
47
+ hashedBytes: number;
48
+ /** Requests made to the pre-check route. */
49
+ asks: number;
50
+ /** Bytes the plan kept from being sent. */
51
+ savedBytes: number;
52
+ /** Files whose hash failed (an unreadable file) — sent unhashed rather than ending the plan. */
53
+ hashErrors: number;
54
+ /** Set when the pre-check could not be asked; the plan then sent what it could not rule out. */
55
+ degraded?: string;
56
+ }
57
+
58
+ export interface PlanResult<T> {
59
+ upload: Extract<Verdict<T>, { kind: "upload" }>[];
60
+ duplicates: Extract<Verdict<T>, { kind: "duplicate" }>[];
61
+ repeats: Extract<Verdict<T>, { kind: "repeat" }>[];
62
+ stats: PlanStats;
63
+ }
64
+
65
+ export interface PlanOptions<T> {
66
+ items: readonly T[];
67
+ /** The bytes of an item. Its `size` is the size asked about. */
68
+ blobOf: (item: T) => Blob;
69
+ /** The item's path inside the drop (or its name) — the memo's key. Without it there is no memo. */
70
+ pathOf?: (item: T) => string;
71
+ /** Modification time in ms (a `File`'s `lastModified`). Part of the memo's key. */
72
+ lastModifiedOf?: (item: T) => number | undefined;
73
+ /** A script's {@link statIdentity} of the item — part of the memo's key where it is known. */
74
+ identityOf?: (item: T) => string | undefined;
75
+ ask: PrecheckAsk;
76
+ memo?: HashMemo;
77
+ /** Defaults to {@link sha256OfBlob}. The specs count calls through it. */
78
+ hash?: (blob: Blob, options: HashOptions) => Promise<string>;
79
+ /** Files hashed at once. Default 3 — a disk is the bottleneck past that. */
80
+ concurrency?: number;
81
+ /** Bytes held in memory by hashes in flight. Default 320 MiB; one file is always allowed. */
82
+ maxInFlightBytes?: number;
83
+ /**
84
+ * How long a finished hash waits for company before it is asked about (default 250 ms), unless
85
+ * {@link batchSize} are already waiting or the hashing is over. Bounds a drop of 10,000 small
86
+ * files to about one request per batch rather than one per file, at a quarter-second's delay.
87
+ */
88
+ lingerMs?: number;
89
+ /** Hashes that are asked about at once without waiting out the linger. Default 200. */
90
+ batchSize?: number;
91
+ signal?: AbortSignal;
92
+ /** Each verdict as it is reached — start uploading on the first `upload`, not at the end. */
93
+ onVerdict?: (verdict: Verdict<T>) => void;
94
+ /** Hashing progress of one item, 0..1. */
95
+ onHashProgress?: (item: T, fraction: number) => void;
96
+ }
97
+
98
+ function chunks<V>(values: readonly V[], size: number): V[][] {
99
+ const out: V[][] = [];
100
+ for (let i = 0; i < values.length; i += size) out.push(values.slice(i, i + size));
101
+ return out;
102
+ }
103
+
104
+ const message = (error: unknown): string => (error instanceof Error ? error.message : String(error));
105
+
106
+ export async function planUploads<T>(options: PlanOptions<T>): Promise<PlanResult<T>> {
107
+ const {
108
+ items,
109
+ blobOf,
110
+ pathOf,
111
+ lastModifiedOf,
112
+ identityOf,
113
+ ask,
114
+ memo,
115
+ hash = sha256OfBlob,
116
+ concurrency = 3,
117
+ maxInFlightBytes = 320 * 1024 * 1024,
118
+ lingerMs = 250,
119
+ batchSize = 200,
120
+ signal,
121
+ onVerdict,
122
+ onHashProgress,
123
+ } = options;
124
+
125
+ const result: PlanResult<T> = {
126
+ upload: [],
127
+ duplicates: [],
128
+ repeats: [],
129
+ stats: {
130
+ files: items.length,
131
+ bytes: 0,
132
+ sizeOnly: 0,
133
+ memoHits: 0,
134
+ hashedFiles: 0,
135
+ hashedBytes: 0,
136
+ asks: 0,
137
+ savedBytes: 0,
138
+ hashErrors: 0,
139
+ },
140
+ };
141
+ const decide = (verdict: Verdict<T>): void => {
142
+ if (verdict.kind === "upload") result.upload.push(verdict);
143
+ else {
144
+ result.stats.savedBytes += blobOf(verdict.item).size;
145
+ if (verdict.kind === "duplicate") result.duplicates.push(verdict);
146
+ else result.repeats.push(verdict);
147
+ }
148
+ onVerdict?.(verdict);
149
+ };
150
+
151
+ const sizeOf = (item: T): number => blobOf(item).size;
152
+ const inDrop = new Map<number, number>();
153
+ for (const item of items) {
154
+ const size = sizeOf(item);
155
+ result.stats.bytes += size;
156
+ inDrop.set(size, (inDrop.get(size) ?? 0) + 1);
157
+ }
158
+ if (items.length === 0) return result;
159
+
160
+ // 1 — sizes
161
+ const held = new Set<number>();
162
+ try {
163
+ for (const batch of chunks([...inDrop.keys()], PRECHECK_MAX_SIZES)) {
164
+ result.stats.asks++;
165
+ for (const size of (await ask({ sizes: batch })).sizes) held.add(size);
166
+ }
167
+ } catch (error) {
168
+ if (signal?.aborted) throw error;
169
+ result.stats.degraded = `pre-check unavailable (${message(error)}) — sending without it`;
170
+ for (const item of items) decide({ kind: "upload", item });
171
+ return result;
172
+ }
173
+
174
+ const candidates: T[] = [];
175
+ for (const item of items) {
176
+ const size = sizeOf(item);
177
+ if (held.has(size) || (inDrop.get(size) ?? 0) > 1) candidates.push(item);
178
+ else {
179
+ result.stats.sizeOnly++;
180
+ decide({ kind: "upload", item });
181
+ }
182
+ }
183
+ if (candidates.length === 0) return result;
184
+
185
+ // 2 — the memo
186
+ const keyOf = pathOf
187
+ ? (item: T) => memoKey(pathOf(item), sizeOf(item), lastModifiedOf?.(item), identityOf?.(item))
188
+ : undefined;
189
+ const remembered = memo && keyOf ? await memo.get(candidates.map(keyOf)) : new Map<string, string>();
190
+
191
+ // 3 + 4 — hash, and ask as hashes finish
192
+ const firstWith = new Map<string, T>();
193
+ let pending: { item: T; sha256: string }[] = [];
194
+ let toRemember: [string, string][] = [];
195
+ let asking: Promise<void> | undefined;
196
+ let askFailed: string | undefined;
197
+ let hashingDone = false;
198
+ let linger: ReturnType<typeof setTimeout> | undefined;
199
+
200
+ const flush = async (): Promise<void> => {
201
+ while (pending.length > 0) {
202
+ const batch = pending.splice(0, PRECHECK_MAX_HASHES);
203
+ const remember = toRemember;
204
+ toRemember = [];
205
+ if (askFailed) {
206
+ for (const { item, sha256 } of batch) decide({ kind: "upload", item, sha256 });
207
+ continue;
208
+ }
209
+ let have: Record<string, PrecheckHit> = {};
210
+ try {
211
+ result.stats.asks++;
212
+ have = (await ask({ sha256: [...new Set(batch.map((b) => b.sha256))] })).have;
213
+ } catch (error) {
214
+ if (signal?.aborted) throw error;
215
+ askFailed = message(error);
216
+ result.stats.degraded = `pre-check failed mid-drop (${askFailed}) — sent the rest without it`;
217
+ }
218
+ for (const { item, sha256 } of batch) {
219
+ const hit = have[sha256];
220
+ decide(hit ? { kind: "duplicate", item, sha256, hit } : { kind: "upload", item, sha256 });
221
+ }
222
+ if (memo && remember.length > 0) await memo.set(remember);
223
+ }
224
+ };
225
+ const kick = (): void => {
226
+ if (linger !== undefined) {
227
+ clearTimeout(linger);
228
+ linger = undefined;
229
+ }
230
+ if (asking) return;
231
+ asking = flush().finally(() => {
232
+ asking = undefined;
233
+ if (pending.length > 0) kick();
234
+ });
235
+ // Awaited at the end; this keeps an abort mid-hashing from surfacing as unhandled first.
236
+ asking.catch(() => {});
237
+ };
238
+ const settle = (item: T, sha256: string): void => {
239
+ const first = firstWith.get(sha256);
240
+ if (first !== undefined) {
241
+ decide({ kind: "repeat", item, sha256, of: first });
242
+ return;
243
+ }
244
+ firstWith.set(sha256, item);
245
+ pending.push({ item, sha256 });
246
+ if (hashingDone || pending.length >= batchSize) kick();
247
+ else linger ??= setTimeout(kick, lingerMs);
248
+ };
249
+
250
+ const toHash: T[] = [];
251
+ for (const item of candidates) {
252
+ const known = keyOf ? remembered.get(keyOf(item)) : undefined;
253
+ if (known) {
254
+ result.stats.memoHits++;
255
+ settle(item, known);
256
+ } else toHash.push(item);
257
+ }
258
+
259
+ let next = 0;
260
+ let inFlightBytes = 0;
261
+ let running = 0;
262
+ const footprint = (size: number): number =>
263
+ size <= NATIVE_HASH_MAX_BYTES ? size : STREAM_SLICE_BYTES;
264
+ await new Promise<void>((resolve, reject) => {
265
+ let failed = false;
266
+ const pump = (): void => {
267
+ if (failed) return;
268
+ if (next >= toHash.length && running === 0) {
269
+ resolve();
270
+ return;
271
+ }
272
+ while (next < toHash.length && running < concurrency) {
273
+ const item = toHash[next];
274
+ const blob = blobOf(item);
275
+ const cost = footprint(blob.size);
276
+ if (running > 0 && inFlightBytes + cost > maxInFlightBytes) break;
277
+ next++;
278
+ running++;
279
+ inFlightBytes += cost;
280
+ hash(blob, { signal, onProgress: onHashProgress ? (f) => onHashProgress(item, f) : undefined })
281
+ .then((sha256) => {
282
+ result.stats.hashedFiles++;
283
+ result.stats.hashedBytes += blob.size;
284
+ if (keyOf) toRemember.push([keyOf(item), sha256]);
285
+ settle(item, sha256);
286
+ })
287
+ .catch((error) => {
288
+ // An abort ends the plan. One file that cannot be READ does not: it is sent
289
+ // unhashed like any file the pre-check cannot rule out, and its own upload is
290
+ // where the person learns it is unreadable (roms, forge task 3579).
291
+ if (signal?.aborted) {
292
+ failed = true;
293
+ reject(error);
294
+ return;
295
+ }
296
+ result.stats.hashErrors++;
297
+ decide({ kind: "upload", item });
298
+ })
299
+ .finally(() => {
300
+ running--;
301
+ inFlightBytes -= cost;
302
+ pump();
303
+ });
304
+ }
305
+ };
306
+ pump();
307
+ }).catch((error) => {
308
+ if (linger !== undefined) clearTimeout(linger);
309
+ throw error;
310
+ });
311
+ hashingDone = true;
312
+ if (pending.length > 0) kick();
313
+ while (asking) await asking;
314
+ return result;
315
+ }
@@ -0,0 +1,47 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Sha256 } from "./sha256.js";
3
+
4
+ const subtle = async (bytes: Uint8Array<ArrayBuffer>): Promise<string> =>
5
+ [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((b) => b.toString(16).padStart(2, "0")).join("");
6
+
7
+ const bytesOf = (n: number): Uint8Array<ArrayBuffer> => {
8
+ const out = new Uint8Array(n);
9
+ let x = 2463534242;
10
+ for (let i = 0; i < n; i++) {
11
+ x ^= x << 13;
12
+ x ^= x >>> 17;
13
+ x ^= x << 5;
14
+ out[i] = x & 0xff;
15
+ }
16
+ return out;
17
+ };
18
+
19
+ describe("Sha256", () => {
20
+ test("the empty message and 'abc' — the FIPS vectors", () => {
21
+ expect(new Sha256().hex()).toBe("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
22
+ expect(new Sha256().update(new TextEncoder().encode("abc")).hex()).toBe(
23
+ "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
24
+ );
25
+ });
26
+
27
+ // 55/56/64 are the padding boundaries: one length byte fits, does not fit, a whole block.
28
+ for (const n of [1, 55, 56, 63, 64, 65, 127, 128, 1000]) {
29
+ test(`agrees with WebCrypto on ${n} bytes, fed whole and byte by byte`, async () => {
30
+ const data = bytesOf(n);
31
+ const whole = new Sha256().update(data).hex();
32
+ const drip = new Sha256();
33
+ for (let i = 0; i < n; i++) drip.update(data.subarray(i, i + 1));
34
+ expect(whole).toBe(await subtle(data));
35
+ expect(drip.hex()).toBe(whole);
36
+ });
37
+ }
38
+
39
+ test("a megabyte split at an odd offset — the shape a resumed download has", async () => {
40
+ const data = bytesOf(1024 * 1024 + 7);
41
+ const hasher = new Sha256();
42
+ hasher.update(data.subarray(0, 333_333));
43
+ hasher.update(data.subarray(333_333, 700_001));
44
+ hasher.update(data.subarray(700_001));
45
+ expect(hasher.hex()).toBe(await subtle(data));
46
+ });
47
+ });
@@ -0,0 +1,127 @@
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
+
15
+ const K = new Uint32Array([
16
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98,
17
+ 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
18
+ 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8,
19
+ 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
20
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,
21
+ 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
22
+ 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
23
+ 0xc67178f2,
24
+ ]);
25
+
26
+ const INITIAL = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
27
+
28
+ export class Sha256 {
29
+ private readonly h = new Uint32Array(INITIAL);
30
+
31
+ /** Back to the empty message — a download that has to start over. */
32
+ reset(): this {
33
+ this.h.set(INITIAL);
34
+ this.tailLength = 0;
35
+ this.length = 0;
36
+ return this;
37
+ }
38
+ private readonly w = new Uint32Array(64);
39
+ private readonly tail = new Uint8Array(64);
40
+ private tailLength = 0;
41
+ /** Total bytes fed, as a double — exact to 2^53, far past any disc. */
42
+ private length = 0;
43
+
44
+ update(data: Uint8Array): this {
45
+ this.length += data.length;
46
+ let offset = 0;
47
+ if (this.tailLength > 0) {
48
+ const take = Math.min(64 - this.tailLength, data.length);
49
+ this.tail.set(data.subarray(0, take), this.tailLength);
50
+ this.tailLength += take;
51
+ offset = take;
52
+ if (this.tailLength < 64) return this;
53
+ this.block(this.tail, 0);
54
+ this.tailLength = 0;
55
+ }
56
+ const whole = data.length - ((data.length - offset) % 64);
57
+ for (; offset < whole; offset += 64) this.block(data, offset);
58
+ if (offset < data.length) {
59
+ this.tail.set(data.subarray(offset), 0);
60
+ this.tailLength = data.length - offset;
61
+ }
62
+ return this;
63
+ }
64
+
65
+ /** The digest as lowercase hex. The hasher is spent afterwards. */
66
+ hex(): string {
67
+ const bits = this.length * 8;
68
+ const pad = new Uint8Array(this.tailLength < 56 ? 64 : 128);
69
+ pad.set(this.tail.subarray(0, this.tailLength));
70
+ pad[this.tailLength] = 0x80;
71
+ const view = new DataView(pad.buffer);
72
+ view.setUint32(pad.length - 8, Math.floor(bits / 2 ** 32));
73
+ view.setUint32(pad.length - 4, bits >>> 0);
74
+ for (let offset = 0; offset < pad.length; offset += 64) this.block(pad, offset);
75
+ let out = "";
76
+ for (const word of this.h) out += word.toString(16).padStart(8, "0");
77
+ return out;
78
+ }
79
+
80
+ private block(data: Uint8Array, offset: number): void {
81
+ const w = this.w;
82
+ for (let i = 0; i < 16; i++) {
83
+ const j = offset + i * 4;
84
+ w[i] = (data[j] << 24) | (data[j + 1] << 16) | (data[j + 2] << 8) | data[j + 3];
85
+ }
86
+ for (let i = 16; i < 64; i++) {
87
+ const a = w[i - 15];
88
+ const b = w[i - 2];
89
+ const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);
90
+ const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);
91
+ w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0;
92
+ }
93
+ const h = this.h;
94
+ let a = h[0];
95
+ let b = h[1];
96
+ let c = h[2];
97
+ let d = h[3];
98
+ let e = h[4];
99
+ let f = h[5];
100
+ let g = h[6];
101
+ let hh = h[7];
102
+ for (let i = 0; i < 64; i++) {
103
+ const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
104
+ const ch = (e & f) ^ (~e & g);
105
+ const t1 = (hh + S1 + ch + K[i] + w[i]) | 0;
106
+ const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
107
+ const maj = (a & b) ^ (a & c) ^ (b & c);
108
+ const t2 = (S0 + maj) | 0;
109
+ hh = g;
110
+ g = f;
111
+ f = e;
112
+ e = (d + t1) | 0;
113
+ d = c;
114
+ c = b;
115
+ b = a;
116
+ a = (t1 + t2) | 0;
117
+ }
118
+ h[0] = (h[0] + a) | 0;
119
+ h[1] = (h[1] + b) | 0;
120
+ h[2] = (h[2] + c) | 0;
121
+ h[3] = (h[3] + d) | 0;
122
+ h[4] = (h[4] + e) | 0;
123
+ h[5] = (h[5] + f) | 0;
124
+ h[6] = (h[6] + g) | 0;
125
+ h[7] = (h[7] + hh) | 0;
126
+ }
127
+ }
@@ -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.
@@ -35,7 +35,7 @@ const registryWith = (marshaller: CCWireMarshaller) => {
35
35
  };
36
36
 
37
37
  describe('createMarshalledIngress', () => {
38
- it('marshals a payload through the token’s registered marshaller', async () => {
38
+ it('marshals a payload through the token’s registered marshaler', async () => {
39
39
  const ingress = createMarshalledIngress({ registry: registryWith(rowsMarshaller()) });
40
40
  await expect(ingress.ingest('knowledge:records', { rows: [{ id: 'a' }] })).resolves.toEqual({
41
41
  rows: [{ id: 'a' }],
@@ -45,7 +45,7 @@ describe('createMarshalledIngress', () => {
45
45
 
46
46
  it('is a 404 for an unregistered token, not a 400', async () => {
47
47
  const ingress = createMarshalledIngress({ registry: createMarshallerRegistry() });
48
- // From outside, "no marshaller" and "no such token" are the same fact.
48
+ // From outside, "no marshaler" and "no such token" are the same fact.
49
49
  // Distinguishing them would make an ingress an enumeration oracle for the
50
50
  // app's whole wire surface.
51
51
  await expect(ingress.ingest('nope', {})).rejects.toMatchObject({
@@ -55,20 +55,20 @@ describe('createMarshalledIngress', () => {
55
55
  await ingress.dispose();
56
56
  });
57
57
 
58
- it('carries the marshaller’s own typed refusals through unchanged', async () => {
58
+ it('carries the marshaler’s own typed refusals through unchanged', async () => {
59
59
  const ingress = createMarshalledIngress({
60
60
  registry: registryWith(rowsMarshaller({ maxPayloadBytes: 8 })),
61
61
  });
62
- // 413, from the marshaller's declared ceiling — the ingress adds no policy
63
- // of its own about size, because the marshaller is where that is declared.
62
+ // 413, from the marshaler's declared ceiling — the ingress adds no policy
63
+ // of its own about size, because the marshaler is where that is declared.
64
64
  await expect(ingress.ingest('knowledge:records', { rows: [{ id: 'a-very-long-id' }] })).rejects.toMatchObject({
65
65
  status: 413,
66
66
  });
67
67
  await ingress.dispose();
68
68
  });
69
69
 
70
- it('preserves the marshalling tier’s status codes rather than flattening them', async () => {
71
- // These codes are the marshalling tier's, unchanged, and the distinction is
70
+ it('preserves the marshaling tier’s status codes rather than flattening them', async () => {
71
+ // These codes are the marshaling tier's, unchanged, and the distinction is
72
72
  // load-bearing: a payload `validate` refuses is the CALLER's wrong shape
73
73
  // (400), while a transform whose OUTPUT violates the token schema means an
74
74
  // upstream provider changed shape under us (502 — not our error budget, and
@@ -114,10 +114,10 @@ describe('createMarshalledIngress', () => {
114
114
  * The reason this module exists at all. `createMarshalWorkerClient` spawns a
115
115
  * thread PER CALL, so a route handler that called it per request would leak one
116
116
  * OS thread per webhook — a server that gets slower for a week and then dies,
117
- * with nothing in the marshalling API to warn you. Caching per entry is not an
117
+ * with nothing in the marshaling API to warn you. Caching per entry is not an
118
118
  * optimization here; it is the whole point of having one owner.
119
119
  */
120
- it('reuses ONE thread for a marshaller that offloads, and disposes it once', async () => {
120
+ it('reuses ONE thread for a marshaler that offloads, and disposes it once', async () => {
121
121
  const disposed: string[] = [];
122
122
  const ingress = createMarshalledIngress({
123
123
  registry: registryWith(
@@ -156,7 +156,7 @@ describe('createMarshalledIngress', () => {
156
156
 
157
157
  /* --------------------------------------------------------- request bodies */
158
158
 
159
- it('parses a JSON body and hands the marshaller a VALUE', async () => {
159
+ it('parses a JSON body and hands the marshaler a VALUE', async () => {
160
160
  const ingress = createMarshalledIngress({ registry: registryWith(rowsMarshaller()) });
161
161
  const request = new Request('http://x/hook', {
162
162
  method: 'POST',
@@ -235,7 +235,7 @@ describe('adding an ingress later is a ROUTE, not a refactor (E16/D8)', () => {
235
235
  expect(ok.status).toBe(202);
236
236
  expect(accepted).toEqual([{ rows: [{ id: 'from-the-wire' }] }]);
237
237
 
238
- // And a CSV import is the SAME route with a different marshaller and a
238
+ // And a CSV import is the SAME route with a different marshaler and a
239
239
  // different service call — which is the claim the owner's condition asked
240
240
  // to be able to rely on.
241
241
  await ingress.dispose();
@@ -253,7 +253,7 @@ describe('adding an ingress later is a ROUTE, not a refactor (E16/D8)', () => {
253
253
  } catch (error) {
254
254
  // A real mount uses the app's error middleware; the point asserted here
255
255
  // is that the ingress hands up an error that already KNOWS its status,
256
- // so a route never has to map marshalling failures itself.
256
+ // so a route never has to map marshaling failures itself.
257
257
  return c.json({ success: false }, (error as { status?: number }).status as 413);
258
258
  }
259
259
  });