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.
- package/dist/core/analytics/range.d.ts +2 -2
- package/dist/core/analytics/range.js +2 -2
- package/dist/core/analytics/schedule.d.ts +1 -1
- package/dist/core/analytics/schedule.js +3 -3
- package/dist/core/duplicates/index.d.ts +2 -2
- package/dist/core/duplicates/index.js +1 -1
- package/dist/core/file-tree/fileTreeModel.d.ts +3 -3
- package/dist/core/file-tree/fileTreeModel.js +2 -2
- package/dist/core/folder-tree/actions.d.ts +1 -1
- package/dist/core/folder-tree/actions.js +1 -1
- package/dist/core/master-lock/kdf.js +1 -1
- package/dist/core/master-lock/presence.d.ts +1 -1
- package/dist/core/master-lock/presence.js +1 -1
- package/dist/core/master-lock/wire.d.ts +1 -1
- package/dist/core/media/mediaItem.js +1 -1
- package/dist/core/media/types.d.ts +4 -4
- package/dist/core/media/videoCuts.d.ts +1 -1
- package/dist/core/media/videoCuts.js +2 -2
- package/dist/core/navigation/index.js +1 -1
- package/dist/core/navigation/navigationModel.d.ts +1 -1
- package/dist/core/navigation/navigationModel.js +1 -1
- package/dist/core/navigation/navigationSelectors.d.ts +2 -2
- package/dist/core/navigation/navigationSelectors.js +2 -2
- package/dist/core/navigation/searchCatalogue.d.ts +2 -2
- package/dist/core/navigation/searchCatalogue.js +4 -4
- package/dist/core/palette/fuzzyRank.d.ts +1 -1
- package/dist/core/palette/fuzzyRank.js +3 -3
- package/dist/core/tokens/registry.d.ts +1 -1
- package/dist/core/tokens/registry.js +1 -1
- package/dist/core/upload-precheck/contract.d.ts +71 -0
- package/dist/core/upload-precheck/contract.js +76 -0
- package/dist/core/upload-precheck/hash.d.ts +26 -0
- package/dist/core/upload-precheck/hash.js +46 -0
- package/dist/core/upload-precheck/index.d.ts +21 -0
- package/dist/core/upload-precheck/index.js +21 -0
- package/dist/core/upload-precheck/memo.d.ts +52 -0
- package/dist/core/upload-precheck/memo.js +160 -0
- package/dist/core/upload-precheck/plan.d.ts +112 -0
- package/dist/core/upload-precheck/plan.js +242 -0
- package/dist/core/upload-precheck/sha256.d.ts +27 -0
- package/dist/core/upload-precheck/sha256.js +124 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +2 -2
- package/dist/core/wire/index.d.ts +1 -1
- package/dist/core/wire/index.js +1 -1
- package/dist/core/wire/ingress.d.ts +4 -4
- package/dist/core/wire/ingress.js +12 -12
- package/dist/core/wire/marshalWorker.d.ts +1 -1
- package/dist/core/wire/marshalWorker.js +15 -15
- package/dist/core/wire/marshalling.d.ts +16 -16
- package/dist/core/wire/marshalling.js +13 -13
- package/dist/ctgrClient/convertRequest.js +2 -2
- package/dist/testing/bytes.d.ts +1 -1
- package/dist/testing/bytes.js +1 -1
- package/package.json +7 -1
- package/src/core/analytics/range.spec.ts +1 -1
- package/src/core/analytics/range.ts +2 -2
- package/src/core/analytics/schedule.spec.ts +3 -3
- package/src/core/analytics/schedule.ts +3 -3
- package/src/core/duplicates/index.ts +2 -2
- package/src/core/file-tree/fileTreeModel.ts +4 -4
- package/src/core/folder-tree/actions.ts +1 -1
- package/src/core/master-lock/kdf.ts +1 -1
- package/src/core/master-lock/presence.ts +1 -1
- package/src/core/master-lock/wire.ts +1 -1
- package/src/core/media/mediaItem.ts +1 -1
- package/src/core/media/types.ts +4 -4
- package/src/core/media/videoCuts.ts +2 -2
- package/src/core/navigation/index.ts +1 -1
- package/src/core/navigation/navigation.spec.ts +3 -3
- package/src/core/navigation/navigationModel.ts +2 -2
- package/src/core/navigation/navigationSelectors.ts +3 -3
- package/src/core/navigation/searchCatalogue.ts +4 -4
- package/src/core/palette/fuzzyRank.ts +3 -3
- package/src/core/tokens/registry.ts +2 -2
- package/src/core/upload-precheck/contract.spec.ts +67 -0
- package/src/core/upload-precheck/contract.ts +107 -0
- package/src/core/upload-precheck/hash.ts +57 -0
- package/src/core/upload-precheck/index.ts +46 -0
- package/src/core/upload-precheck/memo.ts +170 -0
- package/src/core/upload-precheck/plan.spec.ts +299 -0
- package/src/core/upload-precheck/plan.ts +315 -0
- package/src/core/upload-precheck/sha256.spec.ts +47 -0
- package/src/core/upload-precheck/sha256.ts +127 -0
- package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +2 -2
- package/src/core/wire/index.ts +1 -1
- package/src/core/wire/ingress.spec.ts +12 -12
- package/src/core/wire/ingress.ts +15 -15
- package/src/core/wire/marshalWorker.spec.ts +7 -7
- package/src/core/wire/marshalWorker.ts +15 -15
- package/src/core/wire/marshalling.spec.ts +3 -3
- package/src/core/wire/marshalling.ts +25 -25
- package/src/ctgrClient/convertRequest.ts +2 -2
- package/src/namedSubpathsResolve.spec.ts +1 -1
- package/src/publishShape.spec.ts +1 -1
- package/src/shippedFilesAreTracked.spec.ts +2 -2
- 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
|
|
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
|
|
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
|
|
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.
|
package/dist/core/wire/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
110
|
-
// it would have to parse itself (and parse DIFFERENTLY per
|
|
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
|
|
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
|
|
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
|
|
11
|
+
* ## Why the marshaler names its own worker entry
|
|
12
12
|
*
|
|
13
|
-
* A
|
|
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
|
-
*
|
|
17
|
-
* and keep in sync a second module listing every
|
|
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
|
|
21
|
-
* exactly that
|
|
22
|
-
* therefore only ever run the one
|
|
23
|
-
* nothing, and — the part that matters for the gate — "can this
|
|
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
|
|
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
|
|
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(`
|
|
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('
|
|
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(`
|
|
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('
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
63
|
+
* Can this marshaler ever reach the offload path? A DERIVED fact (05 §B1).
|
|
64
64
|
*
|
|
65
|
-
* A
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
106
|
+
* Run a payload through its marshaler, offloading past the size gate.
|
|
107
107
|
*
|
|
108
|
-
* Order matters: the
|
|
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
|
|
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
|
|
5
|
+
* Can this marshaler ever reach the offload path? A DERIVED fact (05 §B1).
|
|
6
6
|
*
|
|
7
|
-
* A
|
|
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
|
|
44
|
+
* Run a payload through its marshaler, offloading past the size gate.
|
|
45
45
|
*
|
|
46
|
-
* Order matters: the
|
|
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
|
|
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
|
|
62
|
-
// resolver. Neither present means this
|
|
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
|
|
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
|
|
77
|
-
: 'Give the
|
|
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
|
|
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(`
|
|
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(`
|
|
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-
|
|
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`
|
|
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;
|
package/dist/testing/bytes.d.ts
CHANGED
|
@@ -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
|
|
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>;
|
package/dist/testing/bytes.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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', () => {
|