@agentplat/mesh-sim-local 0.3.0-beta.4

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/index.js ADDED
@@ -0,0 +1,1171 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { lstat, link, mkdir, open, readdir, rename, rm, } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { COLLECTIVE_STATISTICAL_CAMPAIGN_MAXIMUM_ARTIFACT_BYTES_V1, digestCollectiveStatisticalCampaignArtifactV1, } from "@agentplat/mesh-sim";
6
+ export const COLLECTIVE_STATISTICAL_CAMPAIGN_LOCAL_STORE_SCHEMA_VERSION_V1 = 1;
7
+ const MAXIMUM_ARTIFACT_STREAM_CHUNKS_V1 = 65_536;
8
+ export const DEFAULT_COLLECTIVE_STATISTICAL_CAMPAIGN_LOCAL_STORE_LIMITS_V1 = Object.freeze({
9
+ maximumArtifactBytes: 16 * 1024 * 1024,
10
+ maximumFiles: 16_384,
11
+ maximumArtifactsPerSlot: 16,
12
+ maximumReadKeys: 4_096,
13
+ });
14
+ export class CollectiveStatisticalCampaignLocalStoreError extends Error {
15
+ name = "CollectiveStatisticalCampaignLocalStoreError";
16
+ }
17
+ /**
18
+ * Creates an immutable logical-artifact writer over the local CAS. The
19
+ * artifact ID is committed through the existing no-replace slot boundary, so
20
+ * same ID/different bytes fails even when both contents already exist in CAS.
21
+ */
22
+ export function createLocalCollectiveStatisticalCampaignArtifactWriterV1(store) {
23
+ if (!store || typeof store !== "object")
24
+ fail("local artifact store is invalid");
25
+ return Object.freeze({
26
+ schemaVersion: 1,
27
+ putArtifactV1: (input) => putLocalArtifactV1(store, input),
28
+ });
29
+ }
30
+ /**
31
+ * Creates the trusted-clock writer required by protected local operations.
32
+ * Bytes may be staged in the content-addressed store before expiry, but the
33
+ * logical artifact binding is checked and committed only while authorization
34
+ * remains active. Unbound content is unreachable campaign evidence.
35
+ */
36
+ export function createLocalCollectiveStatisticalCampaignDeadlineArtifactWriterV1(store, clockSource = Date.now) {
37
+ if (!store || typeof store !== "object")
38
+ fail("local artifact store is invalid");
39
+ if (typeof clockSource !== "function")
40
+ fail("clock must be a function");
41
+ return Object.freeze({
42
+ schemaVersion: 1,
43
+ putArtifactV1: (input) => putLocalArtifactV1(store, input),
44
+ putArtifactBeforeDeadlineV1: async (input) => {
45
+ exactObject(input, ["artifactId", "bytes", "kind", "maximumBytes", "operationExpiresAtMs"], "deadline artifact stream write");
46
+ assertActiveDeadlineV1(input.operationExpiresAtMs, clockSource);
47
+ return putLocalArtifactV1(store, {
48
+ artifactId: input.artifactId,
49
+ bytes: input.bytes,
50
+ kind: input.kind,
51
+ maximumBytes: input.maximumBytes,
52
+ }, () => assertActiveDeadlineV1(input.operationExpiresAtMs, clockSource));
53
+ },
54
+ });
55
+ }
56
+ async function putLocalArtifactV1(store, input, beforeLogicalCommit = () => undefined) {
57
+ exactObject(input, ["artifactId", "bytes", "kind", "maximumBytes"], "artifact stream write");
58
+ assertToken(input.artifactId, "artifactId");
59
+ if (input.artifactId.length > 256)
60
+ fail("artifact stream artifactId is invalid");
61
+ if (!Number.isSafeInteger(input.maximumBytes) ||
62
+ input.maximumBytes < 1 ||
63
+ input.maximumBytes >
64
+ COLLECTIVE_STATISTICAL_CAMPAIGN_MAXIMUM_ARTIFACT_BYTES_V1)
65
+ fail("artifact stream maximumBytes is invalid");
66
+ const bytes = await collectArtifactStreamV1(input.bytes, input.maximumBytes);
67
+ let value;
68
+ try {
69
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
70
+ }
71
+ catch {
72
+ fail("artifact stream is not valid UTF-8 JSON");
73
+ }
74
+ const canonicalDigest = digestCollectiveStatisticalCampaignArtifactV1(input.kind, value);
75
+ const stored = await store.putArtifactV1(bytes);
76
+ const entry = Object.freeze({
77
+ schemaVersion: 1,
78
+ artifactId: input.artifactId,
79
+ kind: input.kind,
80
+ path: `artifacts/sha256/${stored.sha256}.json`,
81
+ byteLength: stored.byteLength,
82
+ sha256: stored.sha256,
83
+ canonicalDigest,
84
+ });
85
+ const binding = await store.putArtifactV1(JSON.stringify({
86
+ schemaVersion: 1,
87
+ artifactId: entry.artifactId,
88
+ kind: entry.kind,
89
+ path: entry.path,
90
+ byteLength: entry.byteLength,
91
+ sha256: entry.sha256,
92
+ canonicalDigest: entry.canonicalDigest,
93
+ }));
94
+ beforeLogicalCommit();
95
+ await store.commitSlotV1({
96
+ runKey: artifactRunKeyV1(input.artifactId),
97
+ artifactSha256: [binding.sha256],
98
+ });
99
+ return entry;
100
+ }
101
+ function assertActiveDeadlineV1(operationExpiresAtMs, clockSource) {
102
+ const current = clockSource();
103
+ if (!Number.isSafeInteger(operationExpiresAtMs) ||
104
+ operationExpiresAtMs < 0 ||
105
+ !Number.isSafeInteger(current) ||
106
+ current < 0 ||
107
+ current >= operationExpiresAtMs)
108
+ fail("artifact stream operation deadline expired");
109
+ }
110
+ /**
111
+ * Opens only the immutable logical IDs represented by one verified index.
112
+ * Each read rechecks the no-replace slot binding before yielding CAS bytes.
113
+ */
114
+ export function createLocalCollectiveStatisticalCampaignArtifactReaderV1(store, artifacts) {
115
+ if (!store || typeof store !== "object")
116
+ fail("local artifact store is invalid");
117
+ if (!Array.isArray(artifacts))
118
+ fail("local artifact index is invalid");
119
+ const indexed = new Map();
120
+ for (const entry of artifacts) {
121
+ if (!entry || typeof entry !== "object")
122
+ fail("local artifact index is invalid");
123
+ assertToken(entry.artifactId, "artifactId");
124
+ if (entry.artifactId.length > 256)
125
+ fail("local artifact index artifactId is invalid");
126
+ assertSha256(entry.sha256, "artifact sha256");
127
+ if (indexed.has(entry.artifactId))
128
+ fail("local artifact index is duplicated");
129
+ indexed.set(entry.artifactId, Object.freeze({ ...entry }));
130
+ }
131
+ const artifactIds = Object.freeze([...indexed.keys()].sort());
132
+ return Object.freeze({
133
+ schemaVersion: 1,
134
+ async listArtifactIdsV1() {
135
+ return artifactIds;
136
+ },
137
+ async *openArtifactV1(artifactId) {
138
+ assertToken(artifactId, "artifactId");
139
+ const entry = indexed.get(artifactId);
140
+ if (!entry)
141
+ fail("local artifact is not indexed");
142
+ const [binding] = await store.readSlotCommitsV1([
143
+ artifactRunKeyV1(artifactId),
144
+ ]);
145
+ if (binding?.commit === null ||
146
+ binding?.commit === undefined ||
147
+ binding.commit.artifactSha256.length !== 1)
148
+ fail("local artifact logical binding is missing or changed");
149
+ let semanticBinding;
150
+ try {
151
+ semanticBinding = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(await store.readArtifactV1(binding.commit.artifactSha256[0])));
152
+ }
153
+ catch {
154
+ fail("local artifact logical binding is invalid");
155
+ }
156
+ exactObject(semanticBinding, [
157
+ "artifactId",
158
+ "byteLength",
159
+ "canonicalDigest",
160
+ "kind",
161
+ "path",
162
+ "schemaVersion",
163
+ "sha256",
164
+ ], "local artifact logical binding");
165
+ const expectedBinding = {
166
+ schemaVersion: 1,
167
+ artifactId: entry.artifactId,
168
+ kind: entry.kind,
169
+ path: entry.path,
170
+ byteLength: entry.byteLength,
171
+ sha256: entry.sha256,
172
+ canonicalDigest: entry.canonicalDigest,
173
+ };
174
+ if (JSON.stringify(semanticBinding) !== JSON.stringify(expectedBinding))
175
+ fail("local artifact logical binding is missing or changed");
176
+ yield await store.readArtifactV1(entry.sha256);
177
+ },
178
+ });
179
+ }
180
+ async function collectArtifactStreamV1(stream, maximumBytes) {
181
+ if (!stream || typeof stream[Symbol.asyncIterator] !== "function")
182
+ fail("artifact byte stream is invalid");
183
+ const bytes = new Uint8Array(maximumBytes);
184
+ let byteLength = 0;
185
+ let chunkCount = 0;
186
+ for await (const chunk of stream) {
187
+ chunkCount += 1;
188
+ if (chunkCount > MAXIMUM_ARTIFACT_STREAM_CHUNKS_V1 ||
189
+ !(chunk instanceof Uint8Array) ||
190
+ chunk.byteLength === 0)
191
+ fail("artifact stream chunk is invalid");
192
+ const nextByteLength = byteLength + chunk.byteLength;
193
+ if (nextByteLength > maximumBytes)
194
+ fail("artifact stream exceeds byte limit");
195
+ bytes.set(chunk, byteLength);
196
+ byteLength = nextByteLength;
197
+ }
198
+ if (byteLength < 1)
199
+ fail("artifact stream is empty");
200
+ return bytes.subarray(0, byteLength);
201
+ }
202
+ function artifactRunKeyV1(artifactId) {
203
+ return `artifact-v1:${createHash("sha256").update(artifactId).digest("hex")}`;
204
+ }
205
+ /**
206
+ * Adapts the local CAS/slot-commit store to the portable execution service.
207
+ * One canonical execution record is published per runKey; an orphan content
208
+ * blob is harmless until its immutable slot commit becomes visible.
209
+ */
210
+ export function createLocalCollectiveStatisticalCampaignExecutionStoreV1(store, clockSource = Date.now) {
211
+ if (!store ||
212
+ typeof store !== "object" ||
213
+ typeof store.commitFencedExecutionRecordV1 !== "function")
214
+ fail("local execution store is invalid");
215
+ if (typeof clockSource !== "function")
216
+ fail("clock must be a function");
217
+ const commitFencedExecutionRecordV1 = store.commitFencedExecutionRecordV1.bind(store);
218
+ return Object.freeze({
219
+ schemaVersion: 1,
220
+ readExecutionStateV1: (input) => store.readExecutionStateV1(input),
221
+ compareAndSwapExecutionStateV1: (input) => store.compareAndSwapExecutionStateV1(input),
222
+ compareAndSwapExecutionStateWithDeadlineV1: (input) => store.compareAndSwapExecutionStateWithDeadlineV1(input, clockSource),
223
+ async readExecutionsV1(runKeys) {
224
+ const commits = await store.readSlotCommitsV1(runKeys);
225
+ const result = [];
226
+ for (const entry of commits) {
227
+ if (entry.commit === null) {
228
+ result.push(Object.freeze({ runKey: entry.runKey, execution: null }));
229
+ continue;
230
+ }
231
+ if (entry.commit.artifactSha256.length !== 1 &&
232
+ entry.commit.artifactSha256.length !== 2)
233
+ fail("execution slot commit has an invalid record count");
234
+ const bytes = await store.readArtifactV1(entry.commit.artifactSha256[0]);
235
+ let execution;
236
+ try {
237
+ execution = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
238
+ }
239
+ catch {
240
+ fail("execution slot record is not valid UTF-8 JSON");
241
+ }
242
+ result.push(Object.freeze({ runKey: entry.runKey, execution }));
243
+ }
244
+ return Object.freeze(result);
245
+ },
246
+ async readExecutionWithFenceV1(input) {
247
+ exactObject(input, [
248
+ "cellId",
249
+ "executionId",
250
+ "fence",
251
+ "operationExpiresAtMs",
252
+ "registrationDigest",
253
+ "runKey",
254
+ ], "fenced execution read", true);
255
+ assertToken(input.executionId, "executionId");
256
+ assertBundleDigest(input.registrationDigest);
257
+ assertToken(input.cellId, "cellId");
258
+ assertBundleDigest(input.runKey);
259
+ const fence = normalizeFence(input.fence);
260
+ const operationExpiresAtMs = input.operationExpiresAtMs ?? null;
261
+ if (operationExpiresAtMs !== null &&
262
+ (!Number.isSafeInteger(operationExpiresAtMs) ||
263
+ operationExpiresAtMs < 0))
264
+ fail("operation expiry is invalid");
265
+ const [entry] = await store.readSlotCommitsV1([input.runKey]);
266
+ if (!entry || entry.commit === null)
267
+ return null;
268
+ if (entry.commit.artifactSha256.length !== 2)
269
+ fail("fenced execution provenance is missing");
270
+ const [executionBytes, provenanceBytes] = await Promise.all([
271
+ store.readArtifactV1(entry.commit.artifactSha256[0]),
272
+ store.readArtifactV1(entry.commit.artifactSha256[1]),
273
+ ]);
274
+ const provenance = parseObject(provenanceBytes, "execution provenance");
275
+ exactObject(provenance, [
276
+ "cellId",
277
+ "executionId",
278
+ "fence",
279
+ "operationExpiresAtMs",
280
+ "registrationDigest",
281
+ "runKey",
282
+ "schemaVersion",
283
+ ], "execution provenance");
284
+ const persistedFence = normalizeFence(provenance.fence);
285
+ if (provenance.schemaVersion !== 1 ||
286
+ provenance.executionId !== input.executionId ||
287
+ provenance.registrationDigest !== input.registrationDigest ||
288
+ provenance.cellId !== input.cellId ||
289
+ provenance.runKey !== input.runKey ||
290
+ provenance.operationExpiresAtMs !== operationExpiresAtMs ||
291
+ !sameStoredFence(persistedFence, fence))
292
+ fail("fenced execution provenance does not match");
293
+ try {
294
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(executionBytes));
295
+ }
296
+ catch {
297
+ fail("execution slot record is not valid UTF-8 JSON");
298
+ }
299
+ },
300
+ async commitExecutionV1(input) {
301
+ exactObject(input, ["execution", "runKey"], "execution commit");
302
+ assertToken(input.runKey, "runKey");
303
+ const artifact = await store.putArtifactV1(JSON.stringify(input.execution));
304
+ const committed = await store.commitSlotV1({
305
+ runKey: input.runKey,
306
+ artifactSha256: [artifact.sha256],
307
+ });
308
+ return committed.status;
309
+ },
310
+ commitExecutionWithFenceV1: (input) => commitFencedExecutionRecordV1({ ...input, operationExpiresAtMs: input.operationExpiresAtMs ?? null }, clockSource),
311
+ });
312
+ }
313
+ const digestPattern = /^[0-9a-f]{64}$/u;
314
+ const bundleDigestPattern = /^sha256:[0-9a-f]{64}$/u;
315
+ const tokenPattern = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/u;
316
+ /** Opens an explicit local store. Existing symlinks anywhere below root reject. */
317
+ export async function openCollectiveStatisticalCampaignLocalStoreV1(input) {
318
+ exactObject(input, ["root", "limits"], "open input", true);
319
+ if (typeof input.root !== "string" || !path.isAbsolute(input.root))
320
+ fail("root must be an absolute path");
321
+ const root = path.resolve(input.root);
322
+ const limits = normalizeLimits(input.limits);
323
+ await ensureDirectory(root);
324
+ for (const relative of [
325
+ "content",
326
+ "content/sha256",
327
+ "slots",
328
+ "states",
329
+ "bundles",
330
+ "locks",
331
+ "tmp",
332
+ ])
333
+ await ensureDirectory(safeChild(root, relative));
334
+ await assertStoreDirectories(root);
335
+ return new LocalStore(root, limits);
336
+ }
337
+ class LocalStore {
338
+ root;
339
+ #mutationLockPath;
340
+ #limits;
341
+ constructor(root, limits) {
342
+ this.root = root;
343
+ this.#limits = limits;
344
+ this.#mutationLockPath = safeChild(root, "locks/store-mutation.lock");
345
+ }
346
+ async putArtifactV1(input) {
347
+ await this.#assertStoreDirectories();
348
+ const bytes = normalizeBytes(input, this.#limits.maximumArtifactBytes);
349
+ const sha256 = digestBytes(bytes);
350
+ return this.#withMutationLock(async () => {
351
+ const destination = this.#contentPath(sha256);
352
+ if (await exists(destination)) {
353
+ const existing = await readRegularFile(destination, this.#limits.maximumArtifactBytes);
354
+ if (digestBytes(existing) !== sha256)
355
+ fail("content-addressed artifact is corrupt");
356
+ return Object.freeze({
357
+ sha256,
358
+ byteLength: existing.byteLength,
359
+ duplicate: true,
360
+ });
361
+ }
362
+ await this.#reserveFiles(1);
363
+ await publishNoReplace(destination, bytes);
364
+ return Object.freeze({
365
+ sha256,
366
+ byteLength: bytes.byteLength,
367
+ duplicate: false,
368
+ });
369
+ });
370
+ }
371
+ async readArtifactV1(sha256) {
372
+ await this.#assertStoreDirectories();
373
+ assertSha256(sha256, "artifact sha256");
374
+ const bytes = await readRegularFile(this.#contentPath(sha256), this.#limits.maximumArtifactBytes);
375
+ if (digestBytes(bytes) !== sha256)
376
+ fail("content-addressed artifact is corrupt");
377
+ return bytes;
378
+ }
379
+ async commitSlotV1(input) {
380
+ await this.#assertStoreDirectories();
381
+ exactObject(input, ["runKey", "artifactSha256"], "slot commit");
382
+ assertToken(input.runKey, "runKey");
383
+ const artifactSha256 = normalizeDigests(input.artifactSha256, this.#limits.maximumArtifactsPerSlot);
384
+ for (const digest of artifactSha256)
385
+ await this.readArtifactV1(digest);
386
+ const commit = Object.freeze({
387
+ schemaVersion: 1,
388
+ runKey: input.runKey,
389
+ artifactSha256: Object.freeze([...artifactSha256]),
390
+ });
391
+ const bytes = encodeCanonical(commit);
392
+ return this.#withMutationLock(async () => {
393
+ const destination = this.#slotPath(input.runKey);
394
+ if (await exists(destination)) {
395
+ const existing = await readRegularFile(destination, this.#limits.maximumArtifactBytes);
396
+ const parsed = parseSlotCommit(existing, this.#limits.maximumArtifactsPerSlot);
397
+ if (!equalBytes(existing, encodeCanonical(parsed)))
398
+ fail("slot commit is not canonical");
399
+ if (!equalBytes(existing, bytes))
400
+ fail("slot commit conflicts with existing runKey");
401
+ return Object.freeze({ status: "duplicate", commit: parsed });
402
+ }
403
+ await this.#reserveFiles(1);
404
+ await publishNoReplace(destination, bytes);
405
+ return Object.freeze({ status: "committed", commit });
406
+ });
407
+ }
408
+ async readSlotCommitsV1(runKeys) {
409
+ await this.#assertStoreDirectories();
410
+ if (!Array.isArray(runKeys) ||
411
+ runKeys.length > this.#limits.maximumReadKeys)
412
+ fail("requested run keys exceed limit");
413
+ const normalized = runKeys.map((key) => {
414
+ assertToken(key, "runKey");
415
+ return key;
416
+ });
417
+ if (new Set(normalized).size !== normalized.length)
418
+ fail("requested run keys are duplicated");
419
+ const result = [];
420
+ for (const runKey of normalized) {
421
+ const file = this.#slotPath(runKey);
422
+ if (!(await exists(file))) {
423
+ result.push(Object.freeze({ runKey, commit: null }));
424
+ continue;
425
+ }
426
+ const raw = await readRegularFile(file, this.#limits.maximumArtifactBytes);
427
+ const commit = parseSlotCommit(raw, this.#limits.maximumArtifactsPerSlot);
428
+ if (!equalBytes(raw, encodeCanonical(commit)))
429
+ fail("slot commit is not canonical");
430
+ if (commit.runKey !== runKey)
431
+ fail("slot file does not bind requested runKey");
432
+ for (const digest of commit.artifactSha256)
433
+ await this.readArtifactV1(digest);
434
+ result.push(Object.freeze({ runKey, commit }));
435
+ }
436
+ return Object.freeze(result);
437
+ }
438
+ async readExecutionStateV1(input) {
439
+ exactObject(input, ["executionId", "registrationDigest"], "execution state read");
440
+ assertToken(input.executionId, "executionId");
441
+ assertBundleDigest(input.registrationDigest);
442
+ await this.#assertStoreDirectories();
443
+ const file = this.#statePath(input.executionId);
444
+ if (!(await exists(file)))
445
+ return null;
446
+ const bytes = await readRegularFile(file, this.#limits.maximumArtifactBytes);
447
+ const state = parseExecutionState(bytes);
448
+ if (!equalBytes(bytes, encodeCanonical(state)))
449
+ fail("execution state is not canonical");
450
+ if (state.executionId !== input.executionId ||
451
+ state.registrationDigest !== input.registrationDigest)
452
+ fail("execution state does not bind requested identity");
453
+ return state;
454
+ }
455
+ async compareAndSwapExecutionStateV1(input) {
456
+ const result = await this.#compareAndSwapExecutionState(input, null, Date.now);
457
+ if (result === "expired")
458
+ fail("unexpected operation expiry");
459
+ return result;
460
+ }
461
+ async compareAndSwapExecutionStateWithDeadlineV1(input, clockSource) {
462
+ exactObject(input, [
463
+ "executionId",
464
+ "expectedExecutionDigest",
465
+ "operationExpiresAtMs",
466
+ "state",
467
+ ], "deadline execution state compare-and-swap");
468
+ if (typeof clockSource !== "function" ||
469
+ !Number.isSafeInteger(input.operationExpiresAtMs) ||
470
+ input.operationExpiresAtMs < 0)
471
+ fail("operation expiry is invalid");
472
+ return this.#compareAndSwapExecutionState({
473
+ executionId: input.executionId,
474
+ expectedExecutionDigest: input.expectedExecutionDigest,
475
+ state: input.state,
476
+ }, input.operationExpiresAtMs, clockSource);
477
+ }
478
+ async #compareAndSwapExecutionState(input, operationExpiresAtMs, clockSource) {
479
+ exactObject(input, ["executionId", "expectedExecutionDigest", "state"], "execution state compare-and-swap");
480
+ assertToken(input.executionId, "executionId");
481
+ if (input.expectedExecutionDigest !== null)
482
+ assertBundleDigest(input.expectedExecutionDigest);
483
+ const state = assertExecutionState(input.state, input.executionId);
484
+ const bytes = encodeCanonical(state);
485
+ if (bytes.byteLength > this.#limits.maximumArtifactBytes)
486
+ fail("execution state exceeds artifact byte limit");
487
+ return this.#withMutationLock(async () => {
488
+ if (operationExpiresAtMs !== null &&
489
+ localClock(clockSource) >= operationExpiresAtMs)
490
+ return "expired";
491
+ const destination = this.#statePath(input.executionId);
492
+ if (!(await exists(destination))) {
493
+ if (input.expectedExecutionDigest !== null)
494
+ return "conflict";
495
+ await this.#reserveFiles(1);
496
+ await replaceAtomically(this.root, destination, bytes, "state");
497
+ return "committed";
498
+ }
499
+ const currentBytes = await readRegularFile(destination, this.#limits.maximumArtifactBytes);
500
+ const current = parseExecutionState(currentBytes);
501
+ if (!equalBytes(currentBytes, encodeCanonical(current)))
502
+ fail("execution state is not canonical");
503
+ if (current.executionId !== input.executionId)
504
+ fail("execution state does not bind requested identity");
505
+ if (current.executionDigest !== input.expectedExecutionDigest)
506
+ return "conflict";
507
+ if (equalBytes(currentBytes, bytes))
508
+ return "duplicate";
509
+ await replaceAtomically(this.root, destination, bytes, "state");
510
+ return "committed";
511
+ });
512
+ }
513
+ async commitFencedExecutionRecordV1(input, clockSource) {
514
+ exactObject(input, [
515
+ "cellId",
516
+ "execution",
517
+ "executionId",
518
+ "fence",
519
+ "operationExpiresAtMs",
520
+ "registrationDigest",
521
+ "runKey",
522
+ ], "fenced execution commit");
523
+ if (typeof clockSource !== "function")
524
+ fail("clock must be a function");
525
+ assertToken(input.executionId, "executionId");
526
+ assertBundleDigest(input.registrationDigest);
527
+ assertToken(input.cellId, "cellId");
528
+ assertBundleDigest(input.runKey);
529
+ if (input.execution.executionId !== input.executionId ||
530
+ input.execution.cellId !== input.cellId ||
531
+ input.execution.runKey !== input.runKey)
532
+ fail("fenced execution record does not bind requested scope");
533
+ const fence = normalizeFence(input.fence);
534
+ if (input.operationExpiresAtMs !== null &&
535
+ (!Number.isSafeInteger(input.operationExpiresAtMs) ||
536
+ input.operationExpiresAtMs < 0))
537
+ fail("operation expiry is invalid");
538
+ const bytes = normalizeBytes(JSON.stringify(input.execution), this.#limits.maximumArtifactBytes);
539
+ const sha256 = digestBytes(bytes);
540
+ const provenanceBytes = encodeCanonical({
541
+ schemaVersion: 1,
542
+ executionId: input.executionId,
543
+ registrationDigest: input.registrationDigest,
544
+ cellId: input.cellId,
545
+ runKey: input.runKey,
546
+ fence,
547
+ operationExpiresAtMs: input.operationExpiresAtMs,
548
+ });
549
+ const provenanceSha256 = digestBytes(provenanceBytes);
550
+ const commit = Object.freeze({
551
+ schemaVersion: 1,
552
+ runKey: input.runKey,
553
+ artifactSha256: Object.freeze([sha256, provenanceSha256]),
554
+ });
555
+ const commitBytes = encodeCanonical(commit);
556
+ return this.#withMutationLock(async () => {
557
+ const statePath = this.#statePath(input.executionId);
558
+ if (!(await exists(statePath)))
559
+ return "stale_fence";
560
+ const stateBytes = await readRegularFile(statePath, this.#limits.maximumArtifactBytes);
561
+ const state = parseExecutionState(stateBytes);
562
+ if (!equalBytes(stateBytes, encodeCanonical(state)))
563
+ fail("execution state is not canonical");
564
+ const nowMs = localClock(clockSource);
565
+ if ((input.operationExpiresAtMs !== null &&
566
+ nowMs >= input.operationExpiresAtMs) ||
567
+ state.registrationDigest !== input.registrationDigest ||
568
+ !activeStoredFence(state, input.cellId, input.runKey, fence, nowMs))
569
+ return "stale_fence";
570
+ // Fence validation deliberately precedes duplicate detection.
571
+ const slotPath = this.#slotPath(input.runKey);
572
+ if (await exists(slotPath)) {
573
+ const existing = await readRegularFile(slotPath, this.#limits.maximumArtifactBytes);
574
+ const parsed = parseSlotCommit(existing, this.#limits.maximumArtifactsPerSlot);
575
+ if (!equalBytes(existing, encodeCanonical(parsed)))
576
+ fail("slot commit is not canonical");
577
+ if (!equalBytes(existing, commitBytes))
578
+ fail("slot commit conflicts with existing runKey");
579
+ for (const digest of [sha256, provenanceSha256]) {
580
+ const content = await readRegularFile(this.#contentPath(digest), this.#limits.maximumArtifactBytes);
581
+ if (digestBytes(content) !== digest)
582
+ fail("content-addressed artifact is corrupt");
583
+ }
584
+ return "duplicate";
585
+ }
586
+ const contentPath = this.#contentPath(sha256);
587
+ const contentExists = await exists(contentPath);
588
+ const provenancePath = this.#contentPath(provenanceSha256);
589
+ const provenanceExists = await exists(provenancePath);
590
+ await this.#reserveFiles(1 + (contentExists ? 0 : 1) + (provenanceExists ? 0 : 1));
591
+ if (contentExists) {
592
+ const existing = await readRegularFile(contentPath, this.#limits.maximumArtifactBytes);
593
+ if (digestBytes(existing) !== sha256)
594
+ fail("content-addressed artifact is corrupt");
595
+ }
596
+ else
597
+ await publishNoReplace(contentPath, bytes);
598
+ if (provenanceExists) {
599
+ const existing = await readRegularFile(provenancePath, this.#limits.maximumArtifactBytes);
600
+ if (digestBytes(existing) !== provenanceSha256)
601
+ fail("content-addressed artifact is corrupt");
602
+ }
603
+ else
604
+ await publishNoReplace(provenancePath, provenanceBytes);
605
+ await publishNoReplace(slotPath, commitBytes);
606
+ return "committed";
607
+ });
608
+ }
609
+ async inspectMutationLockV1() {
610
+ await this.#assertStoreDirectories();
611
+ if (!(await exists(this.#mutationLockPath)))
612
+ return Object.freeze({ lockId: null });
613
+ const lock = parseMutationLock(await readRegularFile(this.#mutationLockPath, this.#limits.maximumArtifactBytes));
614
+ return Object.freeze({ lockId: lock.lockId });
615
+ }
616
+ async recoverMutationLockV1(lockId) {
617
+ assertLockId(lockId);
618
+ await this.#assertStoreDirectories();
619
+ if (!(await exists(this.#mutationLockPath)))
620
+ return "missing";
621
+ const bytes = await readRegularFile(this.#mutationLockPath, this.#limits.maximumArtifactBytes);
622
+ const lock = parseMutationLock(bytes);
623
+ if (!equalBytes(bytes, encodeCanonical(lock)) || lock.lockId !== lockId)
624
+ fail("store mutation lock ownership changed");
625
+ await rm(this.#mutationLockPath, { force: false });
626
+ await syncDirectory(path.dirname(this.#mutationLockPath));
627
+ return "recovered";
628
+ }
629
+ async acquireCampaignLockV1(campaignKey) {
630
+ await this.#assertStoreDirectories();
631
+ assertToken(campaignKey, "campaignKey");
632
+ const lockId = randomUUID();
633
+ const destination = safeChild(this.root, `locks/campaign-${digestText(campaignKey)}.lock`);
634
+ try {
635
+ await publishNoReplace(destination, encodeCanonical({ schemaVersion: 1, campaignKey, lockId }));
636
+ }
637
+ catch (error) {
638
+ if (isExists(error))
639
+ fail("campaign lock is already held; locks are never broken automatically");
640
+ throw error;
641
+ }
642
+ let released = false;
643
+ return Object.freeze({
644
+ campaignKey,
645
+ lockId,
646
+ release: async () => {
647
+ if (released)
648
+ return;
649
+ await this.#assertStoreDirectories();
650
+ const bytes = await readRegularFile(destination, this.#limits.maximumArtifactBytes);
651
+ const value = parseObject(bytes, "campaign lock");
652
+ if (value.campaignKey !== campaignKey || value.lockId !== lockId)
653
+ fail("campaign lock ownership changed");
654
+ await rm(destination, { force: false });
655
+ await syncDirectory(path.dirname(destination));
656
+ released = true;
657
+ },
658
+ });
659
+ }
660
+ async publishBundleV1(input) {
661
+ await this.#assertStoreDirectories();
662
+ exactObject(input, ["bundleDigest", "bytes"], "bundle publication");
663
+ assertBundleDigest(input.bundleDigest);
664
+ const artifact = await this.putArtifactV1(input.bytes);
665
+ const publication = Object.freeze({
666
+ schemaVersion: 1,
667
+ bundleDigest: input.bundleDigest,
668
+ contentSha256: artifact.sha256,
669
+ byteLength: artifact.byteLength,
670
+ });
671
+ const bytes = encodeCanonical(publication);
672
+ return this.#withMutationLock(async () => {
673
+ const destination = this.#bundlePath(input.bundleDigest);
674
+ const currentAbsent = !(await exists(safeChild(this.root, "bundles/CURRENT")));
675
+ let duplicate = false;
676
+ if (await exists(destination)) {
677
+ const existing = await readRegularFile(destination, this.#limits.maximumArtifactBytes);
678
+ if (!equalBytes(existing, bytes))
679
+ fail("bundle digest conflicts with existing publication");
680
+ duplicate = true;
681
+ if (currentAbsent)
682
+ await this.#reserveFiles(1);
683
+ }
684
+ else {
685
+ await this.#reserveFiles(currentAbsent ? 2 : 1);
686
+ await publishNoReplace(destination, bytes);
687
+ }
688
+ await this.#writeCurrent(publication);
689
+ return Object.freeze({
690
+ bundleDigest: input.bundleDigest,
691
+ contentSha256: artifact.sha256,
692
+ byteLength: artifact.byteLength,
693
+ duplicate,
694
+ });
695
+ });
696
+ }
697
+ async readBundleV1(bundleDigest, verify) {
698
+ await this.#assertStoreDirectories();
699
+ assertBundleDigest(bundleDigest);
700
+ assertVerifier(verify);
701
+ const publication = await this.#readPublication(bundleDigest);
702
+ const bytes = await this.readArtifactV1(publication.contentSha256);
703
+ if (bytes.byteLength !== publication.byteLength)
704
+ fail("bundle publication byteLength does not match content");
705
+ await verify(new Uint8Array(bytes), publication.bundleDigest);
706
+ return bytes;
707
+ }
708
+ async readCurrentBundleV1(verify) {
709
+ await this.#assertStoreDirectories();
710
+ assertVerifier(verify);
711
+ const current = safeChild(this.root, "bundles/CURRENT");
712
+ if (!(await exists(current)))
713
+ return null;
714
+ const pointerBytes = await readRegularFile(current, this.#limits.maximumArtifactBytes);
715
+ const pointer = parsePublication(pointerBytes);
716
+ if (!equalBytes(pointerBytes, encodeCanonical(pointer)))
717
+ fail("CURRENT is not canonical");
718
+ const published = await this.#readPublication(pointer.bundleDigest);
719
+ if (!samePublication(pointer, published))
720
+ fail("CURRENT does not match immutable bundle publication");
721
+ const bytes = await this.readArtifactV1(published.contentSha256);
722
+ if (bytes.byteLength !== published.byteLength)
723
+ fail("bundle publication byteLength does not match content");
724
+ await verify(new Uint8Array(bytes), published.bundleDigest);
725
+ return Object.freeze({ ...published, duplicate: true, bytes });
726
+ }
727
+ #contentPath(sha256) {
728
+ return safeChild(this.root, `content/sha256/${sha256}`);
729
+ }
730
+ #slotPath(runKey) {
731
+ return safeChild(this.root, `slots/${digestText(runKey)}.json`);
732
+ }
733
+ #statePath(executionId) {
734
+ return safeChild(this.root, `states/${digestText(executionId)}.json`);
735
+ }
736
+ #bundlePath(bundleDigest) {
737
+ return safeChild(this.root, `bundles/${digestText(bundleDigest)}.json`);
738
+ }
739
+ async #readPublication(bundleDigest) {
740
+ const bytes = await readRegularFile(this.#bundlePath(bundleDigest), this.#limits.maximumArtifactBytes);
741
+ const value = parsePublication(bytes);
742
+ if (!equalBytes(bytes, encodeCanonical(value)))
743
+ fail("bundle publication is not canonical");
744
+ if (value.bundleDigest !== bundleDigest)
745
+ fail("bundle publication does not bind requested digest");
746
+ return value;
747
+ }
748
+ async #writeCurrent(publication) {
749
+ await this.#assertStoreDirectories();
750
+ const current = safeChild(this.root, "bundles/CURRENT");
751
+ await replaceAtomically(this.root, current, encodeCanonical(publication), "current");
752
+ }
753
+ async #reserveFiles(additional) {
754
+ if (!Number.isSafeInteger(additional) || additional < 0)
755
+ fail("store file reservation is invalid");
756
+ const count = await countRegularFiles(this.root);
757
+ if (count + additional > this.#limits.maximumFiles)
758
+ fail("store file limit is exceeded");
759
+ }
760
+ async #assertStoreDirectories() {
761
+ await assertStoreDirectories(this.root);
762
+ }
763
+ async #withMutationLock(operation) {
764
+ await this.#assertStoreDirectories();
765
+ const lockId = randomUUID();
766
+ const lockBytes = encodeCanonical({ schemaVersion: 1, lockId });
767
+ try {
768
+ await publishNoReplace(this.#mutationLockPath, lockBytes);
769
+ }
770
+ catch (error) {
771
+ if (isExists(error))
772
+ fail("local store is busy");
773
+ throw error;
774
+ }
775
+ try {
776
+ await this.#assertStoreDirectories();
777
+ return await operation();
778
+ }
779
+ finally {
780
+ const current = await readRegularFile(this.#mutationLockPath, this.#limits.maximumArtifactBytes);
781
+ if (!equalBytes(current, lockBytes))
782
+ fail("store mutation lock ownership changed");
783
+ await rm(this.#mutationLockPath, { force: false });
784
+ await syncDirectory(path.dirname(this.#mutationLockPath));
785
+ }
786
+ }
787
+ }
788
+ function assertExecutionState(value, executionId) {
789
+ if (value === null || typeof value !== "object" || Array.isArray(value))
790
+ fail("execution state is invalid");
791
+ const record = value;
792
+ if (record.executionId !== executionId)
793
+ fail("execution state executionId conflicts");
794
+ assertBundleDigest(record.registrationDigest);
795
+ assertBundleDigest(record.executionDigest);
796
+ return value;
797
+ }
798
+ function normalizeFence(value) {
799
+ exactObject(value, ["expiresAtMs", "generation", "leaseToken", "workerId"], "execution fence");
800
+ const fence = value;
801
+ assertToken(fence.workerId, "fence workerId");
802
+ assertToken(fence.leaseToken, "fence leaseToken");
803
+ if (!Number.isSafeInteger(fence.generation) ||
804
+ fence.generation < 1 ||
805
+ !Number.isSafeInteger(fence.expiresAtMs) ||
806
+ fence.expiresAtMs < 0)
807
+ fail("execution fence is invalid");
808
+ return Object.freeze({
809
+ workerId: fence.workerId,
810
+ leaseToken: fence.leaseToken,
811
+ generation: fence.generation,
812
+ expiresAtMs: fence.expiresAtMs,
813
+ });
814
+ }
815
+ function sameStoredFence(left, right) {
816
+ return (left.workerId === right.workerId &&
817
+ left.leaseToken === right.leaseToken &&
818
+ left.generation === right.generation &&
819
+ left.expiresAtMs === right.expiresAtMs);
820
+ }
821
+ function activeStoredFence(state, cellId, runKey, fence, nowMs) {
822
+ const cell = state.cells.find((candidate) => candidate.cellId === cellId);
823
+ const current = cell?.lease;
824
+ const slot = cell?.runs.find((candidate) => candidate.runKey === runKey);
825
+ return (cell?.status === "running" &&
826
+ slot?.status === "running" &&
827
+ current !== null &&
828
+ current !== undefined &&
829
+ current.workerId === fence.workerId &&
830
+ current.leaseToken === fence.leaseToken &&
831
+ current.generation === fence.generation &&
832
+ current.expiresAtMs === fence.expiresAtMs &&
833
+ current.expiresAtMs > nowMs);
834
+ }
835
+ function localClock(clockSource) {
836
+ const value = clockSource();
837
+ if (!Number.isSafeInteger(value) || value < 0)
838
+ fail("clock is invalid");
839
+ return value;
840
+ }
841
+ function parseExecutionState(bytes) {
842
+ const value = parseObject(bytes, "execution state");
843
+ if (typeof value.executionId !== "string")
844
+ fail("execution state executionId is invalid");
845
+ return assertExecutionState(value, value.executionId);
846
+ }
847
+ function normalizeLimits(input) {
848
+ if (input === undefined)
849
+ return DEFAULT_COLLECTIVE_STATISTICAL_CAMPAIGN_LOCAL_STORE_LIMITS_V1;
850
+ exactObject(input, [
851
+ "maximumArtifactBytes",
852
+ "maximumFiles",
853
+ "maximumArtifactsPerSlot",
854
+ "maximumReadKeys",
855
+ ], "limits", true);
856
+ const value = input;
857
+ const result = {
858
+ maximumArtifactBytes: limitOrDefault(value.maximumArtifactBytes, DEFAULT_COLLECTIVE_STATISTICAL_CAMPAIGN_LOCAL_STORE_LIMITS_V1.maximumArtifactBytes),
859
+ maximumFiles: limitOrDefault(value.maximumFiles, DEFAULT_COLLECTIVE_STATISTICAL_CAMPAIGN_LOCAL_STORE_LIMITS_V1.maximumFiles),
860
+ maximumArtifactsPerSlot: limitOrDefault(value.maximumArtifactsPerSlot, DEFAULT_COLLECTIVE_STATISTICAL_CAMPAIGN_LOCAL_STORE_LIMITS_V1.maximumArtifactsPerSlot),
861
+ maximumReadKeys: limitOrDefault(value.maximumReadKeys, DEFAULT_COLLECTIVE_STATISTICAL_CAMPAIGN_LOCAL_STORE_LIMITS_V1.maximumReadKeys),
862
+ };
863
+ for (const [name, value] of Object.entries(result))
864
+ if (!Number.isSafeInteger(value) || value < 1)
865
+ fail(`limit is invalid: ${name}`);
866
+ return Object.freeze(result);
867
+ }
868
+ function limitOrDefault(value, fallback) {
869
+ return value === undefined
870
+ ? fallback
871
+ : typeof value === "number"
872
+ ? value
873
+ : Number.NaN;
874
+ }
875
+ function normalizeBytes(input, maximum) {
876
+ const bytes = typeof input === "string"
877
+ ? new TextEncoder().encode(input)
878
+ : input instanceof Uint8Array
879
+ ? new Uint8Array(input)
880
+ : fail("artifact bytes are invalid");
881
+ if (bytes.byteLength > maximum)
882
+ fail("artifact byte limit is exceeded");
883
+ return bytes;
884
+ }
885
+ function normalizeDigests(value, maximum) {
886
+ if (!Array.isArray(value) || value.length === 0 || value.length > maximum)
887
+ fail("slot artifact digests are invalid");
888
+ const digests = value.map((item) => {
889
+ assertSha256(item, "slot artifact sha256");
890
+ return item;
891
+ });
892
+ if (new Set(digests).size !== digests.length)
893
+ fail("slot artifact digests are duplicated");
894
+ return Object.freeze(digests);
895
+ }
896
+ function parseSlotCommit(bytes, maximumArtifacts) {
897
+ const value = parseObject(bytes, "slot commit");
898
+ exactObject(value, ["schemaVersion", "runKey", "artifactSha256"], "slot commit");
899
+ if (value.schemaVersion !== 1)
900
+ fail("slot commit schema is invalid");
901
+ assertToken(value.runKey, "slot runKey");
902
+ return Object.freeze({
903
+ schemaVersion: 1,
904
+ runKey: value.runKey,
905
+ artifactSha256: normalizeDigests(value.artifactSha256, maximumArtifacts),
906
+ });
907
+ }
908
+ function parseMutationLock(bytes) {
909
+ const value = parseObject(bytes, "store mutation lock");
910
+ exactObject(value, ["schemaVersion", "lockId"], "store mutation lock");
911
+ if (value.schemaVersion !== 1)
912
+ fail("store mutation lock schema is invalid");
913
+ assertLockId(value.lockId);
914
+ return Object.freeze({ schemaVersion: 1, lockId: value.lockId });
915
+ }
916
+ function parsePublication(bytes) {
917
+ const value = parseObject(bytes, "bundle publication");
918
+ exactObject(value, ["schemaVersion", "bundleDigest", "contentSha256", "byteLength"], "bundle publication");
919
+ if (value.schemaVersion !== 1 ||
920
+ !Number.isSafeInteger(value.byteLength) ||
921
+ value.byteLength < 0)
922
+ fail("bundle publication is invalid");
923
+ assertBundleDigest(value.bundleDigest);
924
+ assertSha256(value.contentSha256, "bundle contentSha256");
925
+ return Object.freeze({
926
+ schemaVersion: 1,
927
+ bundleDigest: value.bundleDigest,
928
+ contentSha256: value.contentSha256,
929
+ byteLength: value.byteLength,
930
+ });
931
+ }
932
+ function parseObject(bytes, label) {
933
+ let value;
934
+ try {
935
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
936
+ }
937
+ catch {
938
+ fail(`${label} is not valid UTF-8 JSON`);
939
+ }
940
+ if (value === null || typeof value !== "object" || Array.isArray(value))
941
+ fail(`${label} is invalid`);
942
+ return value;
943
+ }
944
+ function encodeCanonical(value) {
945
+ return new TextEncoder().encode(canonical(value));
946
+ }
947
+ function canonical(value) {
948
+ if (value === null)
949
+ return "null";
950
+ if (typeof value === "string")
951
+ return JSON.stringify(value);
952
+ if (typeof value === "boolean")
953
+ return value ? "true" : "false";
954
+ if (typeof value === "number") {
955
+ if (!Number.isFinite(value) || !Number.isSafeInteger(value))
956
+ fail("canonical number is invalid");
957
+ return JSON.stringify(value);
958
+ }
959
+ if (Array.isArray(value))
960
+ return `[${value.map(canonical).join(",")}]`;
961
+ if (typeof value !== "object")
962
+ fail("canonical value is invalid");
963
+ const record = value;
964
+ return `{${Object.keys(record)
965
+ .sort()
966
+ .map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`)
967
+ .join(",")}}`;
968
+ }
969
+ async function ensureDirectory(directory) {
970
+ await mkdir(directory, { recursive: true, mode: 0o700 });
971
+ const info = await lstat(directory);
972
+ if (!info.isDirectory() || info.isSymbolicLink())
973
+ fail("store directory is invalid or symbolic link");
974
+ }
975
+ const fixedStoreDirectories = Object.freeze([
976
+ "content",
977
+ "content/sha256",
978
+ "slots",
979
+ "states",
980
+ "bundles",
981
+ "locks",
982
+ "tmp",
983
+ ]);
984
+ /** Checks every segment separately, so an intermediate symlink cannot escape root. */
985
+ async function assertStoreDirectories(root) {
986
+ await assertDirectory(root);
987
+ for (const relative of fixedStoreDirectories) {
988
+ let current = root;
989
+ for (const segment of relative.split("/")) {
990
+ current = safeChild(current, segment);
991
+ await assertDirectory(current);
992
+ }
993
+ }
994
+ }
995
+ async function assertDirectory(directory) {
996
+ const info = await lstat(directory);
997
+ if (!info.isDirectory() || info.isSymbolicLink())
998
+ fail("store directory is invalid or symbolic link");
999
+ }
1000
+ async function publishNoReplace(destination, bytes) {
1001
+ await ensureDirectory(path.dirname(destination));
1002
+ const temporary = path.join(path.dirname(destination), `.${path.basename(destination)}.${randomUUID()}.tmp`);
1003
+ await writeSyncedTemp(temporary, bytes);
1004
+ try {
1005
+ await link(temporary, destination);
1006
+ await syncDirectory(path.dirname(destination));
1007
+ }
1008
+ finally {
1009
+ await rm(temporary, { force: true });
1010
+ }
1011
+ }
1012
+ async function replaceAtomically(root, destination, bytes, label) {
1013
+ const temporary = safeChild(root, `tmp/${label}-${randomUUID()}`);
1014
+ await writeSyncedTemp(temporary, bytes);
1015
+ try {
1016
+ await rename(temporary, destination);
1017
+ await syncDirectory(path.dirname(destination));
1018
+ }
1019
+ finally {
1020
+ await rm(temporary, { force: true });
1021
+ }
1022
+ }
1023
+ async function writeSyncedTemp(file, bytes) {
1024
+ const handle = await open(file, "wx", 0o600);
1025
+ try {
1026
+ await handle.writeFile(bytes);
1027
+ await handle.sync();
1028
+ }
1029
+ finally {
1030
+ await handle.close();
1031
+ }
1032
+ }
1033
+ async function readRegularFile(file, maximum) {
1034
+ const pathInfo = await lstat(file);
1035
+ if (!pathInfo.isFile() || pathInfo.isSymbolicLink())
1036
+ fail("stored file is invalid or symbolic link");
1037
+ const flags = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
1038
+ const handle = await open(file, flags);
1039
+ try {
1040
+ const info = await handle.stat();
1041
+ if (!info.isFile() ||
1042
+ info.size > maximum ||
1043
+ info.ino !== pathInfo.ino ||
1044
+ info.dev !== pathInfo.dev)
1045
+ fail("stored file is invalid or exceeds limit");
1046
+ const bytes = new Uint8Array(await handle.readFile());
1047
+ if (bytes.byteLength !== info.size || bytes.byteLength > maximum)
1048
+ fail("stored file changed while reading");
1049
+ return bytes;
1050
+ }
1051
+ finally {
1052
+ await handle.close();
1053
+ }
1054
+ }
1055
+ async function countRegularFiles(root) {
1056
+ let count = 0;
1057
+ for (const directory of ["content/sha256", "slots", "states", "bundles"]) {
1058
+ const absolute = safeChild(root, directory);
1059
+ for (const entry of await readdir(absolute, { withFileTypes: true })) {
1060
+ const candidate = safeChild(absolute, entry.name);
1061
+ const info = await lstat(candidate);
1062
+ if (info.isSymbolicLink())
1063
+ fail("store contains a symbolic link");
1064
+ if (info.isFile())
1065
+ count += 1;
1066
+ else if (info.isDirectory() && directory === "content/sha256") {
1067
+ // Content files stay flat; nested directories are a malformed store.
1068
+ fail("store contains an unexpected nested directory");
1069
+ }
1070
+ else if (!info.isDirectory())
1071
+ fail("store contains an invalid entry");
1072
+ }
1073
+ }
1074
+ return count;
1075
+ }
1076
+ async function syncDirectory(directory) {
1077
+ try {
1078
+ const handle = await open(directory, constants.O_RDONLY);
1079
+ try {
1080
+ await handle.sync();
1081
+ }
1082
+ finally {
1083
+ await handle.close();
1084
+ }
1085
+ }
1086
+ catch (error) {
1087
+ if (error.code !== "EINVAL" &&
1088
+ error.code !== "EPERM")
1089
+ throw error;
1090
+ }
1091
+ }
1092
+ async function exists(file) {
1093
+ try {
1094
+ await lstat(file);
1095
+ return true;
1096
+ }
1097
+ catch (error) {
1098
+ if (error.code === "ENOENT")
1099
+ return false;
1100
+ throw error;
1101
+ }
1102
+ }
1103
+ function safeChild(root, relative) {
1104
+ const candidate = path.resolve(root, relative);
1105
+ if (candidate !== root && !candidate.startsWith(`${root}${path.sep}`))
1106
+ fail("derived store path escapes root");
1107
+ return candidate;
1108
+ }
1109
+ function digestBytes(bytes) {
1110
+ return createHash("sha256").update(bytes).digest("hex");
1111
+ }
1112
+ function digestText(value) {
1113
+ return digestBytes(new TextEncoder().encode(value));
1114
+ }
1115
+ function equalBytes(left, right) {
1116
+ return (left.byteLength === right.byteLength &&
1117
+ left.every((value, index) => value === right[index]));
1118
+ }
1119
+ function samePublication(left, right) {
1120
+ return (left.bundleDigest === right.bundleDigest &&
1121
+ left.contentSha256 === right.contentSha256 &&
1122
+ left.byteLength === right.byteLength);
1123
+ }
1124
+ function assertSha256(value, label) {
1125
+ if (typeof value !== "string" || !digestPattern.test(value))
1126
+ fail(`${label} is invalid`);
1127
+ }
1128
+ function assertBundleDigest(value) {
1129
+ if (typeof value !== "string" || !bundleDigestPattern.test(value))
1130
+ fail("bundleDigest is invalid");
1131
+ }
1132
+ function assertToken(value, label) {
1133
+ if (typeof value !== "string" || !tokenPattern.test(value))
1134
+ fail(`${label} is invalid`);
1135
+ }
1136
+ function assertVerifier(value) {
1137
+ if (typeof value !== "function")
1138
+ fail("bundle verifier is required");
1139
+ }
1140
+ function assertLockId(value) {
1141
+ if (typeof value !== "string" ||
1142
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(value))
1143
+ fail("store mutation lockId is invalid");
1144
+ }
1145
+ function exactObject(value, keys, label, optional = false) {
1146
+ if (value === null ||
1147
+ typeof value !== "object" ||
1148
+ Array.isArray(value) ||
1149
+ Object.getPrototypeOf(value) !== Object.prototype ||
1150
+ Object.getOwnPropertySymbols(value).length !== 0)
1151
+ fail(`${label} must be a plain object`);
1152
+ const actual = Object.keys(value).sort();
1153
+ const expected = [...keys].sort();
1154
+ if (optional
1155
+ ? actual.some((key) => !expected.includes(key))
1156
+ : actual.length !== expected.length ||
1157
+ actual.some((key, index) => key !== expected[index]))
1158
+ fail(`${label} has invalid keys`);
1159
+ for (const key of actual) {
1160
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1161
+ if (!descriptor || !descriptor.enumerable || !("value" in descriptor))
1162
+ fail(`${label} contains an accessor`);
1163
+ }
1164
+ }
1165
+ function isExists(error) {
1166
+ return error.code === "EEXIST";
1167
+ }
1168
+ function fail(message) {
1169
+ throw new CollectiveStatisticalCampaignLocalStoreError(`collective_statistical_campaign_local_store_invalid: ${message}`);
1170
+ }
1171
+ //# sourceMappingURL=index.js.map