@obversa/runtime 0.1.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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +192 -0
  3. package/dist/api.d.ts +72 -0
  4. package/dist/api.js +9775 -0
  5. package/dist/api.js.map +1 -0
  6. package/dist/artifacts/conformance.d.ts +1 -0
  7. package/dist/artifacts/file-store.d.ts +8 -0
  8. package/dist/artifacts/store.d.ts +1 -0
  9. package/dist/callback/approval.d.ts +8 -0
  10. package/dist/callback/client.d.ts +38 -0
  11. package/dist/callback/gate.d.ts +1 -0
  12. package/dist/callback/stored-client.d.ts +5 -0
  13. package/dist/chunk-3L6YNPN6.js +3 -0
  14. package/dist/chunk-3L6YNPN6.js.map +1 -0
  15. package/dist/chunk-5GLEABOU.js +3 -0
  16. package/dist/chunk-5GLEABOU.js.map +1 -0
  17. package/dist/chunk-DEW5R23M.js +338 -0
  18. package/dist/chunk-DEW5R23M.js.map +1 -0
  19. package/dist/chunk-DV5P4QLI.js +3056 -0
  20. package/dist/chunk-DV5P4QLI.js.map +1 -0
  21. package/dist/chunk-NIBHM5I5.js +34 -0
  22. package/dist/chunk-NIBHM5I5.js.map +1 -0
  23. package/dist/chunk-RZLMX3IA.js +368 -0
  24. package/dist/chunk-RZLMX3IA.js.map +1 -0
  25. package/dist/core/agent-md.d.ts +36 -0
  26. package/dist/core/agent.d.ts +86 -0
  27. package/dist/core/approval-job.d.ts +43 -0
  28. package/dist/core/assert-graph.d.ts +34 -0
  29. package/dist/core/budget.d.ts +49 -0
  30. package/dist/core/concurrency.d.ts +2 -0
  31. package/dist/core/condition.d.ts +209 -0
  32. package/dist/core/context.d.ts +36 -0
  33. package/dist/core/cost.d.ts +59 -0
  34. package/dist/core/dag.d.ts +20 -0
  35. package/dist/core/decision.d.ts +29 -0
  36. package/dist/core/describe.d.ts +56 -0
  37. package/dist/core/engine-meta.d.ts +5 -0
  38. package/dist/core/env-overlay.d.ts +35 -0
  39. package/dist/core/errors.d.ts +46 -0
  40. package/dist/core/feedback.d.ts +68 -0
  41. package/dist/core/git.d.ts +152 -0
  42. package/dist/core/guards.d.ts +70 -0
  43. package/dist/core/isolated.d.ts +40 -0
  44. package/dist/core/job.d.ts +134 -0
  45. package/dist/core/limits.d.ts +22 -0
  46. package/dist/core/loop.d.ts +24 -0
  47. package/dist/core/merge.d.ts +30 -0
  48. package/dist/core/pipeline.d.ts +35 -0
  49. package/dist/core/process.d.ts +11 -0
  50. package/dist/core/progress.d.ts +82 -0
  51. package/dist/core/redact.d.ts +1 -0
  52. package/dist/core/stats.d.ts +63 -0
  53. package/dist/core/team.d.ts +34 -0
  54. package/dist/core/text.d.ts +8 -0
  55. package/dist/core/tournament.d.ts +25 -0
  56. package/dist/core/types.d.ts +650 -0
  57. package/dist/engines/command-runner.d.ts +1 -0
  58. package/dist/engines/conformance.d.ts +1 -0
  59. package/dist/engines/engine.d.ts +4 -0
  60. package/dist/engines/failure.d.ts +1 -0
  61. package/dist/engines/fallback.d.ts +35 -0
  62. package/dist/engines/message-map.d.ts +1 -0
  63. package/dist/engines/mock.d.ts +1 -0
  64. package/dist/engines/preflight.d.ts +36 -0
  65. package/dist/env/command.d.ts +50 -0
  66. package/dist/env/command.js +65 -0
  67. package/dist/env/command.js.map +1 -0
  68. package/dist/env/environment.d.ts +4 -0
  69. package/dist/env/mock.d.ts +24 -0
  70. package/dist/events/conformance.d.ts +1 -0
  71. package/dist/events/envelope.d.ts +1 -0
  72. package/dist/events/jsonl-store.d.ts +8 -0
  73. package/dist/events/store.d.ts +1 -0
  74. package/dist/graph/commands.d.ts +5 -0
  75. package/dist/graph/conformance.d.ts +32 -0
  76. package/dist/graph/kernel.d.ts +5 -0
  77. package/dist/graph/plan.d.ts +1 -0
  78. package/dist/graph/type.d.ts +7 -0
  79. package/dist/graph/value.d.ts +1 -0
  80. package/dist/graph-types/dag.d.ts +137 -0
  81. package/dist/graph-types/loop.d.ts +194 -0
  82. package/dist/graph-types/team.d.ts +68 -0
  83. package/dist/memory.d.ts +82 -0
  84. package/dist/memory.js +397 -0
  85. package/dist/memory.js.map +1 -0
  86. package/dist/proof/acceptance.d.ts +5 -0
  87. package/dist/proof/artifact.d.ts +6 -0
  88. package/dist/proof/cache.d.ts +4 -0
  89. package/dist/runtime/attempt.d.ts +19 -0
  90. package/dist/runtime/budget.d.ts +4 -0
  91. package/dist/runtime/engine-availability.d.ts +18 -0
  92. package/dist/runtime/graph-executor.d.ts +7 -0
  93. package/dist/runtime/monitor.d.ts +80 -0
  94. package/dist/runtime/node-lifecycle.d.ts +84 -0
  95. package/dist/runtime/paths.d.ts +2 -0
  96. package/dist/runtime/persist.d.ts +31 -0
  97. package/dist/runtime/preflight-record.d.ts +149 -0
  98. package/dist/runtime/process-tree.d.ts +1 -0
  99. package/dist/runtime/result-contract.d.ts +4 -0
  100. package/dist/runtime/result-parts.d.ts +1 -0
  101. package/dist/runtime/run-definition.d.ts +24 -0
  102. package/dist/runtime/run-event.d.ts +9 -0
  103. package/dist/runtime/runner.d.ts +139 -0
  104. package/dist/runtime/supervisor.d.ts +111 -0
  105. package/dist/runtime/team-rooms.d.ts +13 -0
  106. package/dist/runtime/workspace-policy.d.ts +25 -0
  107. package/dist/storage/error.d.ts +1 -0
  108. package/dist/storage/id.d.ts +1 -0
  109. package/dist/storage/local.d.ts +12 -0
  110. package/dist/storage/local.js +1492 -0
  111. package/dist/storage/local.js.map +1 -0
  112. package/dist/testing.d.ts +15 -0
  113. package/dist/testing.js +274 -0
  114. package/dist/testing.js.map +1 -0
  115. package/dist/workflow-agent-response.d.ts +3 -0
  116. package/dist/workflow-support.d.ts +36 -0
  117. package/dist/workflow-support.js +5 -0
  118. package/dist/workflow-support.js.map +1 -0
  119. package/dist/workflow.d.ts +73 -0
  120. package/dist/workspace/conformance.d.ts +1 -0
  121. package/dist/workspace/git-provider.d.ts +26 -0
  122. package/dist/workspace/provider.d.ts +2 -0
  123. package/package.json +91 -0
@@ -0,0 +1,1492 @@
1
+ import { StorageError, validateStorageId, validateRunStoragePolicy, validateRunStorageRecord, validateArtifactScope, validateArtifactReference, validateNewArtifact, validateEventStreamRef, validateStreamRevision, validateDomainEventBatch, validateDomainEventEnvelope, findKnownSecretInEvents } from '../chunk-RZLMX3IA.js';
2
+ import '../chunk-3L6YNPN6.js';
3
+ import { digestJson, cloneFrozenJson, canonicalJson } from '../chunk-5GLEABOU.js';
4
+ import { resolve, join, basename } from 'path';
5
+ import { constants } from 'fs';
6
+ import { rm, mkdir, readdir, lstat, open, link, unlink, realpath } from 'fs/promises';
7
+ import { randomUUID, createHash } from 'crypto';
8
+ import { TextDecoder as TextDecoder$1, isDeepStrictEqual } from 'util';
9
+
10
+ var DEFAULT_LIMITS = {
11
+ maxArtifactBytes: 1048576,
12
+ maxTotalArtifactBytesPerRun: 16777216
13
+ };
14
+ var ADMISSION_FILE = /^(\d{16})\.json$/u;
15
+ var MAX_ADMISSION_RECORD_BYTES = 4096;
16
+ var MAX_ADMISSION_RETRIES = 1e3;
17
+ var SHA256_DIGEST = /^sha256:[0-9a-f]{64}$/u;
18
+ var CONTENT_POLICY_RULES = Object.freeze({
19
+ exact: "reject-known-secret-bytes-v1",
20
+ stateJson: "parse-json-and-reject-known-secret-bytes-keys-values-v1",
21
+ stateOther: "reject-known-secret-bytes-v1",
22
+ freeText: "redact-known-secrets-then-reject-known-secret-output-v1"
23
+ });
24
+ var decoder = new TextDecoder("utf-8", { fatal: true });
25
+ var encoder = new TextEncoder();
26
+ function isJsonMediaType(mediaType) {
27
+ const value = mediaType.toLowerCase();
28
+ return value === "application/json" || value.startsWith("application/") && value.endsWith("+json") && value.length > "application/+json".length;
29
+ }
30
+ function storageError(code, message, details = {}) {
31
+ return new StorageError(code, message, details);
32
+ }
33
+ function nodeCode(error) {
34
+ return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
35
+ }
36
+ function digest(bytes) {
37
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
38
+ }
39
+ function artifactPolicyDigest(limits) {
40
+ return digest(Buffer.from(JSON.stringify({
41
+ schemaVersion: 1,
42
+ limits: {
43
+ maxArtifactBytes: limits.maxArtifactBytes,
44
+ maxTotalArtifactBytesPerRun: limits.maxTotalArtifactBytesPerRun
45
+ },
46
+ contentModeRules: CONTENT_POLICY_RULES
47
+ }), "utf8"));
48
+ }
49
+ function admissionChecksum(entry) {
50
+ return digest(Buffer.from(JSON.stringify({
51
+ schemaVersion: entry.schemaVersion,
52
+ revision: entry.revision,
53
+ limits: {
54
+ maxArtifactBytes: entry.limits.maxArtifactBytes,
55
+ maxTotalArtifactBytesPerRun: entry.limits.maxTotalArtifactBytesPerRun
56
+ },
57
+ policyDigest: entry.policyDigest,
58
+ reference: {
59
+ schemaVersion: entry.reference.schemaVersion,
60
+ digest: entry.reference.digest,
61
+ byteLength: entry.reference.byteLength,
62
+ mediaType: entry.reference.mediaType,
63
+ purpose: entry.reference.purpose
64
+ }
65
+ }), "utf8"));
66
+ }
67
+ function admissionRecord(entry) {
68
+ return Object.freeze({
69
+ schemaVersion: 1,
70
+ revision: entry.revision,
71
+ limits: Object.freeze({ ...entry.limits }),
72
+ policyDigest: entry.policyDigest,
73
+ reference: entry.reference,
74
+ checksum: admissionChecksum(entry)
75
+ });
76
+ }
77
+ function containsKnownSecret(bytes, knownSecrets) {
78
+ const buffer = Buffer.from(bytes);
79
+ return knownSecrets.some((value) => buffer.includes(Buffer.from(value, "utf8")));
80
+ }
81
+ function assertSecretFree(bytes, knownSecrets, message) {
82
+ if (containsKnownSecret(bytes, knownSecrets)) {
83
+ throw storageError("KNOWN_SECRET", message);
84
+ }
85
+ }
86
+ function screenedAdmissionBytes(entry, knownSecrets) {
87
+ const record = admissionRecord(entry);
88
+ for (const value of [
89
+ record.policyDigest,
90
+ record.checksum,
91
+ record.reference.digest,
92
+ record.reference.mediaType,
93
+ record.reference.purpose
94
+ ]) {
95
+ assertSecretFree(
96
+ Buffer.from(value, "utf8"),
97
+ knownSecrets,
98
+ "Artifact admission metadata contains a known secret."
99
+ );
100
+ }
101
+ const bytes = Buffer.from(JSON.stringify(record), "utf8");
102
+ assertSecretFree(
103
+ bytes,
104
+ knownSecrets,
105
+ "Artifact admission metadata contains a known secret."
106
+ );
107
+ return bytes;
108
+ }
109
+ function jsonContainsKnownSecret(value, knownSecrets) {
110
+ if (typeof value === "string") {
111
+ return containsKnownSecret(Buffer.from(value, "utf8"), knownSecrets);
112
+ }
113
+ if (value === null || typeof value !== "object") return false;
114
+ if (Array.isArray(value)) {
115
+ return value.some((item) => jsonContainsKnownSecret(item, knownSecrets));
116
+ }
117
+ return Object.entries(value).some(([key, item]) => containsKnownSecret(Buffer.from(key, "utf8"), knownSecrets) || jsonContainsKnownSecret(item, knownSecrets));
118
+ }
119
+ function hashedPathPart(value) {
120
+ return createHash("sha256").update(value, "utf8").digest("hex");
121
+ }
122
+ function validateLimits(options) {
123
+ const limits = {
124
+ maxArtifactBytes: options.maxArtifactBytes ?? DEFAULT_LIMITS.maxArtifactBytes,
125
+ maxTotalArtifactBytesPerRun: options.maxTotalArtifactBytesPerRun ?? DEFAULT_LIMITS.maxTotalArtifactBytesPerRun
126
+ };
127
+ for (const [name, value] of Object.entries(limits)) {
128
+ if (!Number.isSafeInteger(value) || value < 1) {
129
+ throw storageError(
130
+ "INVALID_STORED_VALUE",
131
+ `${name} must be a positive safe integer.`,
132
+ { path: `/${name}` }
133
+ );
134
+ }
135
+ }
136
+ if (limits.maxTotalArtifactBytesPerRun < limits.maxArtifactBytes) {
137
+ throw storageError(
138
+ "INVALID_STORED_VALUE",
139
+ "The per-run artifact limit cannot be smaller than the single-artifact limit.",
140
+ { path: "/maxTotalArtifactBytesPerRun" }
141
+ );
142
+ }
143
+ return Object.freeze(limits);
144
+ }
145
+ function validateKnownSecrets(values) {
146
+ const result = [...new Set(values ?? [])];
147
+ if (result.some((value) => typeof value !== "string" || value.length === 0 || /[\ud800-\udfff]/u.test(value))) {
148
+ throw storageError(
149
+ "INVALID_STORED_VALUE",
150
+ "Known secrets must be non-empty well-formed strings.",
151
+ { path: "/knownSecrets" }
152
+ );
153
+ }
154
+ return Object.freeze(result.sort((left, right) => right.length - left.length));
155
+ }
156
+ async function ensureDirectory(path) {
157
+ try {
158
+ await mkdir(path, { mode: 448 });
159
+ } catch (error) {
160
+ if (nodeCode(error) !== "EEXIST") throw error;
161
+ }
162
+ let stat;
163
+ try {
164
+ stat = await lstat(path);
165
+ } catch (error) {
166
+ throw storageError("UNSAFE_STORAGE_PATH", "Managed storage directory is unavailable.", {
167
+ path,
168
+ cause: nodeCode(error) ?? "unknown"
169
+ });
170
+ }
171
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
172
+ throw storageError(
173
+ "UNSAFE_STORAGE_PATH",
174
+ "Managed storage path must be a real directory.",
175
+ { path }
176
+ );
177
+ }
178
+ }
179
+ async function existingDirectory(path) {
180
+ try {
181
+ const stat = await lstat(path);
182
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
183
+ throw storageError(
184
+ "UNSAFE_STORAGE_PATH",
185
+ "Managed storage path must be a real directory.",
186
+ { path }
187
+ );
188
+ }
189
+ return true;
190
+ } catch (error) {
191
+ if (nodeCode(error) === "ENOENT") return false;
192
+ throw error;
193
+ }
194
+ }
195
+ async function assertRegularFile(path) {
196
+ try {
197
+ const stat = await lstat(path);
198
+ if (stat.isSymbolicLink() || !stat.isFile()) {
199
+ throw storageError(
200
+ "UNSAFE_STORAGE_PATH",
201
+ "Managed storage path must be a regular file.",
202
+ { path }
203
+ );
204
+ }
205
+ return true;
206
+ } catch (error) {
207
+ if (nodeCode(error) === "ENOENT") return false;
208
+ throw error;
209
+ }
210
+ }
211
+ function parseAdmission(value, expectedRevision, path) {
212
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
213
+ throw storageError("ARTIFACT_INTEGRITY", "Artifact admission is not an object.", {
214
+ path
215
+ });
216
+ }
217
+ const item = value;
218
+ const fields = [
219
+ "schemaVersion",
220
+ "revision",
221
+ "limits",
222
+ "policyDigest",
223
+ "reference",
224
+ "checksum"
225
+ ];
226
+ if (Object.keys(item).length !== fields.length || fields.some((field) => !Object.hasOwn(item, field)) || item.schemaVersion !== 1 || item.revision !== expectedRevision) {
227
+ throw storageError("ARTIFACT_INTEGRITY", "Artifact admission is invalid.", {
228
+ path
229
+ });
230
+ }
231
+ if (item.limits === null || typeof item.limits !== "object" || Array.isArray(item.limits)) {
232
+ throw storageError("ARTIFACT_INTEGRITY", "Artifact admission limits are invalid.", {
233
+ path
234
+ });
235
+ }
236
+ const rawLimits = item.limits;
237
+ if (Object.keys(rawLimits).length !== 2 || !Number.isSafeInteger(rawLimits.maxArtifactBytes) || rawLimits.maxArtifactBytes < 1 || !Number.isSafeInteger(rawLimits.maxTotalArtifactBytesPerRun) || rawLimits.maxTotalArtifactBytesPerRun < 1 || rawLimits.maxTotalArtifactBytesPerRun < rawLimits.maxArtifactBytes) {
238
+ throw storageError("ARTIFACT_INTEGRITY", "Artifact admission limits are invalid.", {
239
+ path
240
+ });
241
+ }
242
+ let reference;
243
+ try {
244
+ reference = validateArtifactReference(item.reference);
245
+ } catch (error) {
246
+ throw storageError("ARTIFACT_INTEGRITY", "Artifact admission reference is invalid.", {
247
+ path,
248
+ cause: error instanceof Error ? error.message : String(error)
249
+ });
250
+ }
251
+ if (typeof item.policyDigest !== "string" || !SHA256_DIGEST.test(item.policyDigest)) {
252
+ throw storageError(
253
+ "ARTIFACT_INTEGRITY",
254
+ "Artifact admission policy digest is invalid.",
255
+ { path }
256
+ );
257
+ }
258
+ const entry = {
259
+ schemaVersion: 1,
260
+ revision: expectedRevision,
261
+ limits: {
262
+ maxArtifactBytes: rawLimits.maxArtifactBytes,
263
+ maxTotalArtifactBytesPerRun: rawLimits.maxTotalArtifactBytesPerRun
264
+ },
265
+ policyDigest: item.policyDigest,
266
+ reference
267
+ };
268
+ if (item.checksum !== admissionChecksum(entry)) {
269
+ throw storageError("ARTIFACT_INTEGRITY", "Artifact admission checksum is invalid.", {
270
+ path
271
+ });
272
+ }
273
+ return admissionRecord(entry);
274
+ }
275
+ async function readBoundedFile(handle, path, maxByteLength, expectedByteLength, details = {}) {
276
+ const before = await handle.stat();
277
+ if (!before.isFile()) {
278
+ throw storageError(
279
+ "UNSAFE_STORAGE_PATH",
280
+ "Managed storage path must be a regular file.",
281
+ { ...details, path }
282
+ );
283
+ }
284
+ if (!Number.isSafeInteger(before.size) || before.size > maxByteLength || expectedByteLength !== void 0 && before.size !== expectedByteLength) {
285
+ throw storageError("ARTIFACT_INTEGRITY", "Stored bytes have an invalid length.", {
286
+ ...details,
287
+ path,
288
+ actualByteLength: before.size,
289
+ maxByteLength,
290
+ ...expectedByteLength === void 0 ? {} : { expectedByteLength }
291
+ });
292
+ }
293
+ const bytes = Buffer.alloc(before.size);
294
+ let offset = 0;
295
+ while (offset < bytes.byteLength) {
296
+ const read = await handle.read(bytes, offset, bytes.byteLength - offset, offset);
297
+ if (read.bytesRead === 0) {
298
+ throw storageError("ARTIFACT_INTEGRITY", "Stored bytes changed while being read.", {
299
+ ...details,
300
+ path,
301
+ actualByteLength: offset,
302
+ expectedByteLength: before.size
303
+ });
304
+ }
305
+ offset += read.bytesRead;
306
+ }
307
+ const extra = Buffer.allocUnsafe(1);
308
+ const extraRead = await handle.read(extra, 0, 1, bytes.byteLength);
309
+ const after = await handle.stat();
310
+ if (extraRead.bytesRead !== 0 || after.size !== before.size) {
311
+ throw storageError("ARTIFACT_INTEGRITY", "Stored bytes changed while being read.", {
312
+ ...details,
313
+ path,
314
+ actualByteLength: after.size,
315
+ expectedByteLength: before.size
316
+ });
317
+ }
318
+ return bytes;
319
+ }
320
+ async function readAdmission(path, revision) {
321
+ if (!await assertRegularFile(path)) {
322
+ throw storageError("ARTIFACT_INTEGRITY", "Artifact admission disappeared.", { path });
323
+ }
324
+ let handle;
325
+ try {
326
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
327
+ const text = (await readBoundedFile(
328
+ handle,
329
+ path,
330
+ MAX_ADMISSION_RECORD_BYTES
331
+ )).toString("utf8");
332
+ try {
333
+ return parseAdmission(JSON.parse(text), revision, path);
334
+ } catch (error) {
335
+ if (error instanceof StorageError) throw error;
336
+ throw storageError("ARTIFACT_INTEGRITY", "Artifact admission contains invalid JSON.", {
337
+ path
338
+ });
339
+ }
340
+ } catch (error) {
341
+ if (nodeCode(error) === "ELOOP") {
342
+ throw storageError("UNSAFE_STORAGE_PATH", "Artifact admission is a symlink.", { path });
343
+ }
344
+ throw error;
345
+ } finally {
346
+ await handle?.close();
347
+ }
348
+ }
349
+ async function readAdmissions(directory) {
350
+ const records = [];
351
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
352
+ const path = join(directory, entry.name);
353
+ if (entry.isSymbolicLink() || !entry.isFile()) {
354
+ throw storageError(
355
+ "UNSAFE_STORAGE_PATH",
356
+ "Artifact admission path must be a regular file.",
357
+ { path }
358
+ );
359
+ }
360
+ if (entry.name.startsWith(".tmp-")) continue;
361
+ const match = ADMISSION_FILE.exec(entry.name);
362
+ const revision = match ? Number(match[1]) : Number.NaN;
363
+ if (!Number.isSafeInteger(revision) || revision < 1) {
364
+ throw storageError("ARTIFACT_INTEGRITY", "Artifact admission filename is invalid.", {
365
+ path
366
+ });
367
+ }
368
+ records.push({ path, revision });
369
+ }
370
+ records.sort((left, right) => left.revision - right.revision);
371
+ const result = [];
372
+ for (const [index, record] of records.entries()) {
373
+ const expectedRevision = index + 1;
374
+ if (record.revision !== expectedRevision) {
375
+ throw storageError("ARTIFACT_INTEGRITY", "Artifact admission revisions are not contiguous.", {
376
+ expectedRevision,
377
+ actualRevision: record.revision
378
+ });
379
+ }
380
+ result.push(await readAdmission(record.path, expectedRevision));
381
+ }
382
+ return result;
383
+ }
384
+ function admissionFilename(revision) {
385
+ return `${String(revision).padStart(16, "0")}.json`;
386
+ }
387
+ async function syncDirectory(path) {
388
+ let handle;
389
+ try {
390
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
391
+ await handle.sync();
392
+ } catch (error) {
393
+ if (nodeCode(error) === "ELOOP") {
394
+ throw storageError("UNSAFE_STORAGE_PATH", "Managed storage directory is a symlink.", {
395
+ path
396
+ });
397
+ }
398
+ throw error;
399
+ } finally {
400
+ await handle?.close();
401
+ }
402
+ }
403
+ async function commitAdmission(directory, entry, knownSecrets) {
404
+ const temporary = join(directory, `.tmp-${randomUUID()}`);
405
+ const destination = join(directory, admissionFilename(entry.revision));
406
+ const bytes = screenedAdmissionBytes(entry, knownSecrets);
407
+ let handle;
408
+ try {
409
+ handle = await open(
410
+ temporary,
411
+ constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
412
+ 384
413
+ );
414
+ await handle.writeFile(bytes);
415
+ await handle.sync();
416
+ await handle.chmod(256);
417
+ await handle.sync();
418
+ await handle.close();
419
+ handle = void 0;
420
+ try {
421
+ await link(temporary, destination);
422
+ await syncDirectory(directory);
423
+ return "committed";
424
+ } catch (error) {
425
+ if (nodeCode(error) === "EEXIST") return "conflict";
426
+ throw error;
427
+ }
428
+ } finally {
429
+ await handle?.close();
430
+ await unlink(temporary).catch((error) => {
431
+ if (nodeCode(error) !== "ENOENT") throw error;
432
+ });
433
+ }
434
+ }
435
+ async function readBlob(path, reference) {
436
+ if (!await assertRegularFile(path)) {
437
+ throw storageError("ARTIFACT_NOT_FOUND", "Admitted artifact bytes are missing.", {
438
+ digest: reference.digest
439
+ });
440
+ }
441
+ let handle;
442
+ try {
443
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
444
+ const bytes = await readBoundedFile(
445
+ handle,
446
+ path,
447
+ reference.byteLength,
448
+ reference.byteLength,
449
+ { digest: reference.digest }
450
+ );
451
+ if (digest(bytes) !== reference.digest) {
452
+ throw storageError("ARTIFACT_INTEGRITY", "Artifact bytes do not match their reference.", {
453
+ digest: reference.digest
454
+ });
455
+ }
456
+ return Uint8Array.from(bytes);
457
+ } finally {
458
+ await handle?.close();
459
+ }
460
+ }
461
+ async function commitBlob(directory, reference, bytes) {
462
+ const path = join(directory, reference.digest.slice("sha256:".length));
463
+ if (await assertRegularFile(path)) {
464
+ await readBlob(path, reference);
465
+ return;
466
+ }
467
+ const temporary = join(directory, `.tmp-${randomUUID()}`);
468
+ let handle;
469
+ try {
470
+ handle = await open(
471
+ temporary,
472
+ constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
473
+ 384
474
+ );
475
+ await handle.writeFile(bytes);
476
+ await handle.sync();
477
+ await handle.chmod(256);
478
+ await handle.sync();
479
+ await handle.close();
480
+ handle = void 0;
481
+ try {
482
+ await link(temporary, path);
483
+ await syncDirectory(directory);
484
+ } catch (error) {
485
+ if (nodeCode(error) !== "EEXIST") throw error;
486
+ await readBlob(path, reference);
487
+ }
488
+ } finally {
489
+ await handle?.close();
490
+ await unlink(temporary).catch((error) => {
491
+ if (nodeCode(error) !== "ENOENT") throw error;
492
+ });
493
+ }
494
+ }
495
+ async function assertTreeHasNoSymlink(path) {
496
+ for (const entry of await readdir(path, { withFileTypes: true })) {
497
+ const child = join(path, entry.name);
498
+ const stat = await lstat(child);
499
+ if (stat.isSymbolicLink()) {
500
+ throw storageError("UNSAFE_STORAGE_PATH", "Managed storage contains a symlink.", {
501
+ path: child
502
+ });
503
+ }
504
+ if (stat.isDirectory()) await assertTreeHasNoSymlink(child);
505
+ }
506
+ }
507
+ var LocalArtifactStore = class {
508
+ #root;
509
+ #limits;
510
+ #knownSecrets;
511
+ #policyDigest;
512
+ constructor(options) {
513
+ if (typeof options.root !== "string" || options.root.trim() === "") {
514
+ throw storageError("INVALID_STORED_VALUE", "Artifact store root must be a path.", {
515
+ path: "/root"
516
+ });
517
+ }
518
+ this.#root = resolve(options.root);
519
+ this.#limits = validateLimits(options);
520
+ this.#knownSecrets = validateKnownSecrets(options.knownSecrets);
521
+ this.#policyDigest = artifactPolicyDigest(this.#limits);
522
+ }
523
+ async preflightWrite(scopeValue, artifactValues) {
524
+ const scope = validateArtifactScope(scopeValue);
525
+ if (!Array.isArray(artifactValues) || artifactValues.length === 0) {
526
+ throw storageError(
527
+ "INVALID_STORED_VALUE",
528
+ "Artifact preflight requires a non-empty array.",
529
+ { path: "/artifacts" }
530
+ );
531
+ }
532
+ const prepared = artifactValues.map((artifact) => this.#prepareArtifact(artifact));
533
+ const paths = await this.#paths(scope, false);
534
+ const entries = paths ? await readAdmissions(paths.admissions) : [];
535
+ this.#assertAdmissionPolicy(entries);
536
+ if (paths) {
537
+ const verified = /* @__PURE__ */ new Set();
538
+ for (const item of prepared) {
539
+ if (verified.has(item.reference.digest)) continue;
540
+ verified.add(item.reference.digest);
541
+ const path = this.#blobPath(paths, item.reference);
542
+ const admitted = entries.some((entry) => entry.reference.digest === item.reference.digest);
543
+ if (admitted || await assertRegularFile(path)) {
544
+ await readBlob(path, item.reference);
545
+ }
546
+ }
547
+ }
548
+ return this.#simulateAdmissions(entries, prepared);
549
+ }
550
+ async write(scopeValue, artifactValue) {
551
+ const scope = validateArtifactScope(scopeValue);
552
+ const prepared = this.#prepareArtifact(artifactValue);
553
+ const { bytes, reference } = prepared;
554
+ const paths = await this.#paths(scope, true);
555
+ const initialEntries = await readAdmissions(paths.admissions);
556
+ this.#assertAdmissionPolicy(initialEntries);
557
+ if (initialEntries.some((entry) => isDeepStrictEqual(entry.reference, reference))) {
558
+ await readBlob(this.#blobPath(paths, reference), reference);
559
+ return reference;
560
+ }
561
+ this.#assertAdmissionSafe(this.#newAdmission(reference, initialEntries.length + 1));
562
+ this.#assertQuota(initialEntries, reference);
563
+ await commitBlob(paths.blobs, reference, bytes);
564
+ for (let attempt = 0; attempt < MAX_ADMISSION_RETRIES; attempt += 1) {
565
+ const entries = await readAdmissions(paths.admissions);
566
+ this.#assertAdmissionPolicy(entries);
567
+ if (entries.some((entry2) => isDeepStrictEqual(entry2.reference, reference))) {
568
+ await readBlob(this.#blobPath(paths, reference), reference);
569
+ return reference;
570
+ }
571
+ const entry = this.#newAdmission(reference, entries.length + 1);
572
+ this.#assertAdmissionSafe(entry);
573
+ this.#assertQuota(entries, reference);
574
+ const result = await commitAdmission(paths.admissions, entry, this.#knownSecrets);
575
+ if (result === "committed") return reference;
576
+ }
577
+ throw storageError(
578
+ "REVISION_CONFLICT",
579
+ "Artifact admission changed too often to complete this write."
580
+ );
581
+ }
582
+ async read(scopeValue, referenceValue) {
583
+ const scope = validateArtifactScope(scopeValue);
584
+ const reference = validateArtifactReference(referenceValue);
585
+ const paths = await this.#paths(scope, false);
586
+ if (!paths) {
587
+ throw storageError("ARTIFACT_NOT_ADMITTED", "Artifact is not admitted for this run.", {
588
+ digest: reference.digest
589
+ });
590
+ }
591
+ const entries = await readAdmissions(paths.admissions);
592
+ this.#assertAdmissionPolicy(entries);
593
+ if (!entries.some((entry) => isDeepStrictEqual(entry.reference, reference))) {
594
+ throw storageError("ARTIFACT_NOT_ADMITTED", "Artifact is not admitted for this run.", {
595
+ digest: reference.digest
596
+ });
597
+ }
598
+ return readBlob(this.#blobPath(paths, reference), reference);
599
+ }
600
+ async deleteRun(scopeValue) {
601
+ const scope = validateArtifactScope(scopeValue);
602
+ const paths = await this.#paths(scope, false);
603
+ if (!paths) return;
604
+ await assertTreeHasNoSymlink(paths.run);
605
+ await rm(paths.run, { recursive: true });
606
+ }
607
+ #safeBytes(artifact) {
608
+ const source = Uint8Array.from(artifact.bytes);
609
+ if (artifact.contentMode === "exact") {
610
+ assertSecretFree(
611
+ source,
612
+ this.#knownSecrets,
613
+ "Exact artifact bytes contain a known secret."
614
+ );
615
+ return source;
616
+ }
617
+ if (artifact.contentMode === "state") {
618
+ assertSecretFree(
619
+ source,
620
+ this.#knownSecrets,
621
+ "State artifact bytes contain a known secret."
622
+ );
623
+ if (!isJsonMediaType(artifact.mediaType)) return source;
624
+ let parsed;
625
+ try {
626
+ const text2 = decoder.decode(source);
627
+ parsed = cloneFrozenJson(JSON.parse(text2));
628
+ } catch {
629
+ throw storageError(
630
+ "INVALID_STORED_VALUE",
631
+ "JSON state artifact bytes must contain strict JSON encoded as UTF-8.",
632
+ { path: "/bytes" }
633
+ );
634
+ }
635
+ if (jsonContainsKnownSecret(parsed, this.#knownSecrets)) {
636
+ throw storageError(
637
+ "KNOWN_SECRET",
638
+ "JSON state contains a known secret in a key or value."
639
+ );
640
+ }
641
+ return source;
642
+ }
643
+ let text;
644
+ try {
645
+ text = decoder.decode(source);
646
+ } catch {
647
+ throw storageError(
648
+ "INVALID_STORED_VALUE",
649
+ "Free-text artifact bytes must contain valid UTF-8.",
650
+ { path: "/bytes" }
651
+ );
652
+ }
653
+ for (const value of this.#knownSecrets) text = text.split(value).join("[redacted]");
654
+ const redacted = encoder.encode(text);
655
+ assertSecretFree(
656
+ redacted,
657
+ this.#knownSecrets,
658
+ "Redacted free text still contains a known secret."
659
+ );
660
+ return redacted;
661
+ }
662
+ #prepareArtifact(artifactValue) {
663
+ const artifact = validateNewArtifact(artifactValue);
664
+ if (artifact.sensitive) {
665
+ throw storageError("SENSITIVE_CONTENT", "Marked-sensitive content cannot be stored.");
666
+ }
667
+ const bytes = this.#safeBytes(artifact);
668
+ if (bytes.byteLength > this.#limits.maxArtifactBytes) {
669
+ throw storageError("STORAGE_LIMIT_EXCEEDED", "Artifact exceeds the single-artifact limit.", {
670
+ byteLength: bytes.byteLength,
671
+ maxArtifactBytes: this.#limits.maxArtifactBytes
672
+ });
673
+ }
674
+ const reference = Object.freeze({
675
+ schemaVersion: 1,
676
+ digest: digest(bytes),
677
+ byteLength: bytes.byteLength,
678
+ mediaType: artifact.mediaType,
679
+ purpose: artifact.purpose
680
+ });
681
+ return Object.freeze({ bytes, reference });
682
+ }
683
+ #newAdmission(reference, revision) {
684
+ return Object.freeze({
685
+ schemaVersion: 1,
686
+ revision,
687
+ limits: this.#limits,
688
+ policyDigest: this.#policyDigest,
689
+ reference
690
+ });
691
+ }
692
+ #assertAdmissionSafe(entry) {
693
+ screenedAdmissionBytes(entry, this.#knownSecrets);
694
+ }
695
+ #assertQuota(entries, reference) {
696
+ const unique = /* @__PURE__ */ new Map();
697
+ for (const entry of entries) {
698
+ unique.set(entry.reference.digest, entry.reference.byteLength);
699
+ }
700
+ const total = [...unique.values()].reduce((sum, size) => sum + size, 0);
701
+ if (!unique.has(reference.digest) && total + reference.byteLength > this.#limits.maxTotalArtifactBytesPerRun) {
702
+ throw storageError("STORAGE_LIMIT_EXCEEDED", "Artifact exceeds the per-run storage limit.", {
703
+ byteLength: reference.byteLength,
704
+ admittedBytes: total,
705
+ maxTotalArtifactBytesPerRun: this.#limits.maxTotalArtifactBytesPerRun
706
+ });
707
+ }
708
+ }
709
+ #simulateAdmissions(entries, artifacts) {
710
+ const simulated = [...entries];
711
+ const unique = /* @__PURE__ */ new Map();
712
+ for (const entry of entries) {
713
+ unique.set(entry.reference.digest, entry.reference.byteLength);
714
+ }
715
+ let total = [...unique.values()].reduce((sum, size) => sum + size, 0);
716
+ const references = [];
717
+ for (const artifact of artifacts) {
718
+ const { reference } = artifact;
719
+ references.push(reference);
720
+ if (simulated.some((entry2) => isDeepStrictEqual(entry2.reference, reference))) continue;
721
+ const entry = this.#newAdmission(reference, simulated.length + 1);
722
+ this.#assertAdmissionSafe(entry);
723
+ if (!unique.has(reference.digest) && total + reference.byteLength > this.#limits.maxTotalArtifactBytesPerRun) {
724
+ throw storageError(
725
+ "STORAGE_LIMIT_EXCEEDED",
726
+ "Artifact batch exceeds the per-run storage limit.",
727
+ {
728
+ byteLength: reference.byteLength,
729
+ admittedBytes: total,
730
+ maxTotalArtifactBytesPerRun: this.#limits.maxTotalArtifactBytesPerRun
731
+ }
732
+ );
733
+ }
734
+ if (!unique.has(reference.digest)) {
735
+ unique.set(reference.digest, reference.byteLength);
736
+ total += reference.byteLength;
737
+ }
738
+ simulated.push(admissionRecord(entry));
739
+ }
740
+ return Object.freeze(references);
741
+ }
742
+ #assertAdmissionIntegrity(entries) {
743
+ const first = entries[0];
744
+ if (entries.some((entry) => !isDeepStrictEqual(entry.limits, first?.limits) || entry.policyDigest !== first?.policyDigest)) {
745
+ throw storageError("ARTIFACT_INTEGRITY", "Artifact admissions change the run policy.");
746
+ }
747
+ }
748
+ #assertAdmissionPolicy(entries) {
749
+ this.#assertAdmissionIntegrity(entries);
750
+ const first = entries[0];
751
+ if (first && !isDeepStrictEqual(first.limits, this.#limits)) {
752
+ throw storageError(
753
+ "INVALID_STORED_VALUE",
754
+ "Artifact-store limits do not match the run admissions."
755
+ );
756
+ }
757
+ if (first && first.policyDigest !== this.#policyDigest) {
758
+ throw storageError(
759
+ "INVALID_STORED_VALUE",
760
+ "Artifact-store behavior does not match the run admissions."
761
+ );
762
+ }
763
+ }
764
+ #blobPath(paths, reference) {
765
+ return join(paths.blobs, reference.digest.slice("sha256:".length));
766
+ }
767
+ async #paths(scope, create) {
768
+ if (create) await mkdir(this.#root, { recursive: true, mode: 448 });
769
+ if (!await existingDirectory(this.#root)) return void 0;
770
+ const namespaces = join(this.#root, "namespaces");
771
+ const namespace = join(namespaces, hashedPathPart(scope.namespace));
772
+ const runs = join(namespace, "runs");
773
+ const run = join(runs, hashedPathPart(scope.runId));
774
+ const blobs = join(run, "blobs");
775
+ const admissions = join(run, "admissions");
776
+ for (const path of [namespaces, namespace, runs, run, blobs, admissions]) {
777
+ if (create) await ensureDirectory(path);
778
+ else if (!await existingDirectory(path)) return void 0;
779
+ }
780
+ return {
781
+ run,
782
+ blobs,
783
+ admissions
784
+ };
785
+ }
786
+ };
787
+ function createLocalArtifactStore(options) {
788
+ return new LocalArtifactStore(options);
789
+ }
790
+ var DEFAULT_MAX_EVENT_PAYLOAD_BYTES = 256 * 1024;
791
+ var DEFAULT_MAX_APPEND_BATCH_BYTES = 1024 * 1024;
792
+ var CHECKSUM_RECORD_ALLOWANCE = 1024;
793
+ var SEGMENT_NAME = /^(\d{16})\.jsonl$/u;
794
+ var SHA256 = /^sha256:[0-9a-f]{64}$/u;
795
+ var UTF8_DECODER = new TextDecoder$1("utf-8", { fatal: true });
796
+ function storageError2(code, message, details = {}) {
797
+ return new StorageError(code, message, details);
798
+ }
799
+ function positiveLimit(value, name, fallback) {
800
+ if (value === void 0) return fallback;
801
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
802
+ throw storageError2(
803
+ "INVALID_STORED_VALUE",
804
+ `${name} must be a positive safe integer.`,
805
+ { path: `/${name}` }
806
+ );
807
+ }
808
+ return value;
809
+ }
810
+ function resolveOptions(options) {
811
+ if (typeof options.root !== "string" || options.root.trim().length === 0) {
812
+ throw storageError2(
813
+ "INVALID_STORED_VALUE",
814
+ "Event-store root must be a non-empty path.",
815
+ { path: "/root" }
816
+ );
817
+ }
818
+ const knownSecrets = options.knownSecrets ?? [];
819
+ if (!Array.isArray(knownSecrets) || knownSecrets.some((secret) => typeof secret !== "string" || secret.length === 0)) {
820
+ throw storageError2(
821
+ "INVALID_STORED_VALUE",
822
+ "Known secrets must be non-empty strings.",
823
+ { path: "/knownSecrets" }
824
+ );
825
+ }
826
+ return Object.freeze({
827
+ root: resolve(options.root),
828
+ maxEventPayloadBytes: positiveLimit(
829
+ options.maxEventPayloadBytes,
830
+ "maxEventPayloadBytes",
831
+ DEFAULT_MAX_EVENT_PAYLOAD_BYTES
832
+ ),
833
+ maxAppendBatchBytes: positiveLimit(
834
+ options.maxAppendBatchBytes,
835
+ "maxAppendBatchBytes",
836
+ DEFAULT_MAX_APPEND_BATCH_BYTES
837
+ ),
838
+ knownSecrets: Object.freeze([...knownSecrets])
839
+ });
840
+ }
841
+ function isNodeError(error, code) {
842
+ return error instanceof Error && "code" in error && error.code === code;
843
+ }
844
+ async function existingDirectory2(path) {
845
+ try {
846
+ const item = await lstat(path);
847
+ if (item.isSymbolicLink() || !item.isDirectory()) {
848
+ throw storageError2(
849
+ "UNSAFE_STORAGE_PATH",
850
+ "Event-store path contains a symlink or non-directory component.",
851
+ { path }
852
+ );
853
+ }
854
+ return true;
855
+ } catch (error) {
856
+ if (isNodeError(error, "ENOENT")) return false;
857
+ throw error;
858
+ }
859
+ }
860
+ async function createContainedDirectory(path) {
861
+ try {
862
+ await mkdir(path, { mode: 448 });
863
+ } catch (error) {
864
+ if (!isNodeError(error, "EEXIST")) throw error;
865
+ }
866
+ if (!await existingDirectory2(path)) {
867
+ throw storageError2(
868
+ "UNSAFE_STORAGE_PATH",
869
+ "Event-store directory disappeared during creation.",
870
+ { path }
871
+ );
872
+ }
873
+ }
874
+ async function rootDirectory(options) {
875
+ await mkdir(options.root, { recursive: true, mode: 448 });
876
+ if (!await existingDirectory2(options.root)) {
877
+ throw storageError2(
878
+ "UNSAFE_STORAGE_PATH",
879
+ "Event-store root disappeared during creation.",
880
+ { path: options.root }
881
+ );
882
+ }
883
+ const root = await realpath(options.root);
884
+ const item = await lstat(root);
885
+ if (!item.isDirectory()) {
886
+ throw storageError2(
887
+ "UNSAFE_STORAGE_PATH",
888
+ "Event-store root is not a directory.",
889
+ { path: options.root }
890
+ );
891
+ }
892
+ return root;
893
+ }
894
+ var PATH_PARTS = ["namespaces", "streams", "segments"];
895
+ async function ensureStreamDirectories(options, stream) {
896
+ const root = await rootDirectory(options);
897
+ const namespaces = join(root, PATH_PARTS[0]);
898
+ await createContainedDirectory(namespaces);
899
+ const namespace = join(namespaces, storagePathComponent(stream.namespace));
900
+ await createContainedDirectory(namespace);
901
+ const streams = join(namespace, PATH_PARTS[1]);
902
+ await createContainedDirectory(streams);
903
+ const streamDirectory = join(streams, storagePathComponent(stream.streamId));
904
+ await createContainedDirectory(streamDirectory);
905
+ const segments = join(streamDirectory, PATH_PARTS[2]);
906
+ await createContainedDirectory(segments);
907
+ const temporary = join(streamDirectory, ".tmp");
908
+ await createContainedDirectory(temporary);
909
+ return { segments, temporary };
910
+ }
911
+ async function findStreamSegments(options, stream) {
912
+ if (!await existingDirectory2(options.root)) return null;
913
+ const root = await realpath(options.root);
914
+ const parts = [
915
+ "namespaces",
916
+ storagePathComponent(stream.namespace),
917
+ "streams",
918
+ storagePathComponent(stream.streamId),
919
+ "segments"
920
+ ];
921
+ let current = root;
922
+ for (const part of parts) {
923
+ current = join(current, part);
924
+ if (!await existingDirectory2(current)) return null;
925
+ }
926
+ return current;
927
+ }
928
+ function sha256(bytes) {
929
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
930
+ }
931
+ function eventPolicyDigest(options) {
932
+ return sha256(Buffer.from(canonicalJson({
933
+ schemaVersion: 1,
934
+ maxEventPayloadBytes: options.maxEventPayloadBytes,
935
+ maxAppendBatchBytes: options.maxAppendBatchBytes
936
+ }), "utf8"));
937
+ }
938
+ function storagePathComponent(value) {
939
+ return createHash("sha256").update(value, "utf8").digest("hex");
940
+ }
941
+ async function readBoundedFile2(path, maximumBytes) {
942
+ let file;
943
+ try {
944
+ file = await open(
945
+ path,
946
+ constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)
947
+ );
948
+ } catch (error) {
949
+ if (isNodeError(error, "ELOOP")) {
950
+ throw storageError2(
951
+ "UNSAFE_STORAGE_PATH",
952
+ "Event segment cannot be a symlink.",
953
+ { path }
954
+ );
955
+ }
956
+ throw error;
957
+ }
958
+ try {
959
+ const before = await file.stat();
960
+ if (!before.isFile() || before.size < 1 || before.size > maximumBytes) {
961
+ throw storageError2(
962
+ "CORRUPT_EVENT_STREAM",
963
+ "Event segment size is invalid or exceeds the configured read bound.",
964
+ { path, byteLength: before.size, maximumBytes }
965
+ );
966
+ }
967
+ const bytes = Buffer.alloc(before.size);
968
+ let offset = 0;
969
+ while (offset < bytes.length) {
970
+ const { bytesRead } = await file.read(
971
+ bytes,
972
+ offset,
973
+ bytes.length - offset,
974
+ offset
975
+ );
976
+ if (bytesRead === 0) break;
977
+ offset += bytesRead;
978
+ }
979
+ const extra = Buffer.alloc(1);
980
+ const extraRead = await file.read(extra, 0, 1, before.size);
981
+ const after = await file.stat();
982
+ if (offset !== bytes.length || extraRead.bytesRead !== 0 || after.size !== before.size) {
983
+ throw storageError2(
984
+ "CORRUPT_EVENT_STREAM",
985
+ "Event segment changed while it was being read.",
986
+ { path }
987
+ );
988
+ }
989
+ return bytes;
990
+ } finally {
991
+ await file.close();
992
+ }
993
+ }
994
+ function parseJsonLine(line, path) {
995
+ try {
996
+ return JSON.parse(line);
997
+ } catch {
998
+ throw storageError2(
999
+ "CORRUPT_EVENT_STREAM",
1000
+ "Event segment contains invalid JSON.",
1001
+ { path }
1002
+ );
1003
+ }
1004
+ }
1005
+ function validateChecksum(value, path) {
1006
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1007
+ throw storageError2(
1008
+ "CORRUPT_EVENT_STREAM",
1009
+ "Event segment checksum must be an object.",
1010
+ { path }
1011
+ );
1012
+ }
1013
+ const record = value;
1014
+ const fields = Object.keys(record).sort();
1015
+ const expected = [
1016
+ "endRevision",
1017
+ "eventCount",
1018
+ "policyDigest",
1019
+ "segmentVersion",
1020
+ "sha256",
1021
+ "startRevision"
1022
+ ];
1023
+ if (fields.length !== expected.length || fields.some((field, index) => field !== expected[index]) || record.segmentVersion !== 1 || typeof record.startRevision !== "number" || !Number.isSafeInteger(record.startRevision) || record.startRevision < 1 || typeof record.endRevision !== "number" || !Number.isSafeInteger(record.endRevision) || record.endRevision < record.startRevision || typeof record.eventCount !== "number" || !Number.isSafeInteger(record.eventCount) || record.eventCount < 1 || typeof record.policyDigest !== "string" || !SHA256.test(record.policyDigest) || typeof record.sha256 !== "string" || !SHA256.test(record.sha256)) {
1024
+ throw storageError2(
1025
+ "CORRUPT_EVENT_STREAM",
1026
+ "Event segment checksum record is invalid.",
1027
+ { path }
1028
+ );
1029
+ }
1030
+ return record;
1031
+ }
1032
+ function parseSegment(options, bytes, path, stream, expectedStart) {
1033
+ let text;
1034
+ try {
1035
+ text = UTF8_DECODER.decode(bytes);
1036
+ } catch {
1037
+ throw storageError2(
1038
+ "CORRUPT_EVENT_STREAM",
1039
+ "Event segment contains invalid UTF-8.",
1040
+ { path }
1041
+ );
1042
+ }
1043
+ if (!text.endsWith("\n")) {
1044
+ throw storageError2(
1045
+ "CORRUPT_EVENT_STREAM",
1046
+ "Event segment must end with a newline.",
1047
+ { path }
1048
+ );
1049
+ }
1050
+ const lines = text.slice(0, -1).split("\n");
1051
+ if (lines.length < 2 || lines.some((line) => line.length === 0)) {
1052
+ throw storageError2(
1053
+ "CORRUPT_EVENT_STREAM",
1054
+ "Event segment must contain events followed by one checksum record.",
1055
+ { path }
1056
+ );
1057
+ }
1058
+ const eventLines = lines.slice(0, -1);
1059
+ const eventBytes = Buffer.from(`${eventLines.join("\n")}
1060
+ `, "utf8");
1061
+ const checksum = validateChecksum(
1062
+ parseJsonLine(lines.at(-1), `${path}#checksum`),
1063
+ `${path}#checksum`
1064
+ );
1065
+ if (checksum.policyDigest !== eventPolicyDigest(options)) {
1066
+ throw storageError2(
1067
+ "INVALID_STORED_VALUE",
1068
+ "Event-store policy does not match the committed stream.",
1069
+ { path }
1070
+ );
1071
+ }
1072
+ if (checksum.startRevision !== expectedStart || checksum.endRevision !== expectedStart + eventLines.length - 1 || checksum.eventCount !== eventLines.length || checksum.sha256 !== sha256(eventBytes)) {
1073
+ throw storageError2(
1074
+ "CORRUPT_EVENT_STREAM",
1075
+ "Event segment checksum or revision range does not match its events.",
1076
+ { path }
1077
+ );
1078
+ }
1079
+ return Object.freeze(eventLines.map((line, index) => {
1080
+ let envelope;
1081
+ try {
1082
+ envelope = validateDomainEventEnvelope(
1083
+ parseJsonLine(line, `${path}#event-${index + 1}`)
1084
+ );
1085
+ } catch (error) {
1086
+ if (error instanceof StorageError) {
1087
+ throw storageError2(
1088
+ "CORRUPT_EVENT_STREAM",
1089
+ `Event segment contains an invalid envelope: ${error.message}`,
1090
+ { path, eventIndex: index }
1091
+ );
1092
+ }
1093
+ throw error;
1094
+ }
1095
+ if (envelope.streamId !== stream.streamId || envelope.revision !== expectedStart + index) {
1096
+ throw storageError2(
1097
+ "CORRUPT_EVENT_STREAM",
1098
+ "Event envelope does not match its stream or revision position.",
1099
+ { path, eventIndex: index }
1100
+ );
1101
+ }
1102
+ return envelope;
1103
+ }));
1104
+ }
1105
+ async function segmentPaths(directory) {
1106
+ const entries = await readdir(directory, { withFileTypes: true });
1107
+ const names = [];
1108
+ for (const entry of entries) {
1109
+ if (!SEGMENT_NAME.test(entry.name) || !entry.isFile()) {
1110
+ throw storageError2(
1111
+ entry.isSymbolicLink() ? "UNSAFE_STORAGE_PATH" : "CORRUPT_EVENT_STREAM",
1112
+ "Event segment directory contains an invalid entry.",
1113
+ { path: join(directory, entry.name) }
1114
+ );
1115
+ }
1116
+ names.push(entry.name);
1117
+ }
1118
+ names.sort();
1119
+ return Object.freeze(names.map((name) => join(directory, name)));
1120
+ }
1121
+ async function readStreamStateFromDirectory(options, stream, directory) {
1122
+ const paths = await segmentPaths(directory);
1123
+ const events = [];
1124
+ const eventIds = /* @__PURE__ */ new Set();
1125
+ const segments = /* @__PURE__ */ new Map();
1126
+ let nextRevision = 1;
1127
+ for (const path of paths) {
1128
+ const filename = basename(path);
1129
+ const match = SEGMENT_NAME.exec(filename);
1130
+ const namedStart = Number(match?.[1]);
1131
+ if (namedStart !== nextRevision) {
1132
+ throw storageError2(
1133
+ "CORRUPT_EVENT_STREAM",
1134
+ "Event segment filenames do not form one continuous stream.",
1135
+ { path, expectedRevision: nextRevision }
1136
+ );
1137
+ }
1138
+ const bytes = await readBoundedFile2(
1139
+ path,
1140
+ options.maxAppendBatchBytes + CHECKSUM_RECORD_ALLOWANCE
1141
+ );
1142
+ const segment = parseSegment(options, bytes, path, stream, nextRevision);
1143
+ segments.set(namedStart, bytes);
1144
+ for (const event of segment) {
1145
+ if (eventIds.has(event.eventId)) {
1146
+ throw storageError2(
1147
+ "CORRUPT_EVENT_STREAM",
1148
+ "Committed event stream contains a duplicate event identity.",
1149
+ { path, eventId: event.eventId }
1150
+ );
1151
+ }
1152
+ eventIds.add(event.eventId);
1153
+ events.push(event);
1154
+ }
1155
+ nextRevision += segment.length;
1156
+ }
1157
+ return Object.freeze({
1158
+ events: Object.freeze(events),
1159
+ revision: nextRevision - 1,
1160
+ segments
1161
+ });
1162
+ }
1163
+ async function readStreamState(options, stream) {
1164
+ const directory = await findStreamSegments(options, stream);
1165
+ if (directory === null) {
1166
+ return { events: [], revision: 0, segments: /* @__PURE__ */ new Map() };
1167
+ }
1168
+ return readStreamStateFromDirectory(options, stream, directory);
1169
+ }
1170
+ function segmentFilename(startRevision) {
1171
+ return `${String(startRevision).padStart(16, "0")}.jsonl`;
1172
+ }
1173
+ function serializedSegment(options, envelopes) {
1174
+ const eventBytes = Buffer.from(
1175
+ `${envelopes.map((event) => JSON.stringify(event)).join("\n")}
1176
+ `,
1177
+ "utf8"
1178
+ );
1179
+ const checksum = {
1180
+ segmentVersion: 1,
1181
+ policyDigest: eventPolicyDigest(options),
1182
+ startRevision: envelopes[0].revision,
1183
+ endRevision: envelopes.at(-1).revision,
1184
+ eventCount: envelopes.length,
1185
+ sha256: sha256(eventBytes)
1186
+ };
1187
+ return {
1188
+ eventBytes,
1189
+ bytes: Buffer.concat([
1190
+ eventBytes,
1191
+ Buffer.from(`${JSON.stringify(checksum)}
1192
+ `, "utf8")
1193
+ ])
1194
+ };
1195
+ }
1196
+ function validateLimitsAndSecrets(options, events, segment) {
1197
+ for (const event of events) {
1198
+ const byteLength = Buffer.byteLength(canonicalJson(event.payload), "utf8");
1199
+ if (byteLength > options.maxEventPayloadBytes) {
1200
+ throw storageError2(
1201
+ "STORAGE_LIMIT_EXCEEDED",
1202
+ "Event payload exceeds maxEventPayloadBytes.",
1203
+ {
1204
+ eventId: event.eventId,
1205
+ byteLength,
1206
+ maximumBytes: options.maxEventPayloadBytes
1207
+ }
1208
+ );
1209
+ }
1210
+ }
1211
+ if (segment.eventBytes.byteLength > options.maxAppendBatchBytes) {
1212
+ throw storageError2(
1213
+ "STORAGE_LIMIT_EXCEEDED",
1214
+ "Event append exceeds maxAppendBatchBytes.",
1215
+ {
1216
+ byteLength: segment.eventBytes.byteLength,
1217
+ maximumBytes: options.maxAppendBatchBytes
1218
+ }
1219
+ );
1220
+ }
1221
+ const exactBytes = segment.bytes.toString("utf8");
1222
+ const secret = findKnownSecretInEvents(
1223
+ options.knownSecrets,
1224
+ exactBytes,
1225
+ events
1226
+ );
1227
+ if (secret !== void 0) {
1228
+ throw storageError2(
1229
+ "KNOWN_SECRET",
1230
+ "Exact event bytes contain a configured known secret.",
1231
+ { eventIds: events.map((event) => event.eventId) }
1232
+ );
1233
+ }
1234
+ }
1235
+ function hasExactCommittedAppend(state, expectedRevision, segment, eventCount) {
1236
+ if (state.revision < expectedRevision + eventCount) return false;
1237
+ return state.segments.get(expectedRevision + 1)?.equals(segment.bytes) ?? false;
1238
+ }
1239
+ async function prepareAppend(options, unsafeStream, unsafeExpectedRevision, unsafeEvents, allowCommittedRetry = false) {
1240
+ const stream = validateEventStreamRef(unsafeStream);
1241
+ const expectedRevision = validateStreamRevision(
1242
+ unsafeExpectedRevision,
1243
+ "/expectedRevision"
1244
+ );
1245
+ const events = validateDomainEventBatch(unsafeEvents);
1246
+ if (expectedRevision + events.length > Number.MAX_SAFE_INTEGER) {
1247
+ throw storageError2(
1248
+ "STORAGE_LIMIT_EXCEEDED",
1249
+ "Event append would exceed the maximum safe stream revision.",
1250
+ { expectedRevision, eventCount: events.length }
1251
+ );
1252
+ }
1253
+ const envelopes = Object.freeze(events.map((event, index) => validateDomainEventEnvelope({
1254
+ ...event,
1255
+ envelopeVersion: 1,
1256
+ streamId: stream.streamId,
1257
+ revision: expectedRevision + index + 1
1258
+ })));
1259
+ const segment = serializedSegment(options, envelopes);
1260
+ validateLimitsAndSecrets(options, events, segment);
1261
+ const state = await readStreamState(options, stream);
1262
+ if (state.revision !== expectedRevision) {
1263
+ if (allowCommittedRetry && hasExactCommittedAppend(
1264
+ state,
1265
+ expectedRevision,
1266
+ segment,
1267
+ events.length
1268
+ )) {
1269
+ return {
1270
+ stream,
1271
+ expectedRevision,
1272
+ events,
1273
+ segment,
1274
+ alreadyCommitted: true
1275
+ };
1276
+ }
1277
+ throw storageError2(
1278
+ "REVISION_CONFLICT",
1279
+ "Event stream revision does not match the append expectation.",
1280
+ { expectedRevision, actualRevision: state.revision }
1281
+ );
1282
+ }
1283
+ const eventIds = new Set(state.events.map((event) => event.eventId));
1284
+ for (const event of events) {
1285
+ if (eventIds.has(event.eventId)) {
1286
+ throw storageError2(
1287
+ "DUPLICATE_EVENT_ID",
1288
+ "Event identity is already present in this stream or append batch.",
1289
+ { eventId: event.eventId }
1290
+ );
1291
+ }
1292
+ eventIds.add(event.eventId);
1293
+ }
1294
+ return {
1295
+ stream,
1296
+ expectedRevision,
1297
+ events,
1298
+ segment,
1299
+ alreadyCommitted: false
1300
+ };
1301
+ }
1302
+ async function syncDirectory2(path) {
1303
+ const directory = await open(path, constants.O_RDONLY);
1304
+ try {
1305
+ await directory.sync();
1306
+ } finally {
1307
+ await directory.close();
1308
+ }
1309
+ }
1310
+ async function commitSegment(directories, startRevision, bytes) {
1311
+ const temporary = join(
1312
+ directories.temporary,
1313
+ `${segmentFilename(startRevision)}.${randomUUID()}.tmp`
1314
+ );
1315
+ const target = join(directories.segments, segmentFilename(startRevision));
1316
+ const file = await open(temporary, "wx", 384);
1317
+ try {
1318
+ await file.writeFile(bytes);
1319
+ await file.sync();
1320
+ await file.chmod(256);
1321
+ try {
1322
+ await link(temporary, target);
1323
+ } catch (error) {
1324
+ if (isNodeError(error, "EEXIST")) return false;
1325
+ throw error;
1326
+ }
1327
+ await syncDirectory2(directories.segments);
1328
+ return true;
1329
+ } finally {
1330
+ await file.close();
1331
+ await unlink(temporary).catch(() => void 0);
1332
+ }
1333
+ }
1334
+ var LocalEventStore = class {
1335
+ #options;
1336
+ constructor(options) {
1337
+ this.#options = resolveOptions(options);
1338
+ }
1339
+ async preflightAppend(stream, expectedRevision, events) {
1340
+ await prepareAppend(this.#options, stream, expectedRevision, events);
1341
+ }
1342
+ async *read(unsafeStream, unsafeAfterRevision = 0) {
1343
+ const stream = validateEventStreamRef(unsafeStream);
1344
+ const afterRevision = validateStreamRevision(
1345
+ unsafeAfterRevision,
1346
+ "/afterRevision"
1347
+ );
1348
+ const state = await readStreamState(this.#options, stream);
1349
+ for (const event of state.events) {
1350
+ if (event.revision > afterRevision) yield event;
1351
+ }
1352
+ }
1353
+ async append(unsafeStream, unsafeExpectedRevision, unsafeEvents) {
1354
+ const prepared = await prepareAppend(
1355
+ this.#options,
1356
+ unsafeStream,
1357
+ unsafeExpectedRevision,
1358
+ unsafeEvents,
1359
+ true
1360
+ );
1361
+ if (prepared.alreadyCommitted) {
1362
+ return prepared.expectedRevision + prepared.events.length;
1363
+ }
1364
+ const directories = await ensureStreamDirectories(
1365
+ this.#options,
1366
+ prepared.stream
1367
+ );
1368
+ if (await commitSegment(
1369
+ directories,
1370
+ prepared.expectedRevision + 1,
1371
+ prepared.segment.bytes
1372
+ )) {
1373
+ return prepared.expectedRevision + prepared.events.length;
1374
+ }
1375
+ const actual = await readStreamState(this.#options, prepared.stream);
1376
+ if (hasExactCommittedAppend(
1377
+ actual,
1378
+ prepared.expectedRevision,
1379
+ prepared.segment,
1380
+ prepared.events.length
1381
+ )) {
1382
+ return prepared.expectedRevision + prepared.events.length;
1383
+ }
1384
+ throw storageError2(
1385
+ "REVISION_CONFLICT",
1386
+ "Another writer committed the expected stream revision first.",
1387
+ {
1388
+ expectedRevision: prepared.expectedRevision,
1389
+ actualRevision: actual.revision
1390
+ }
1391
+ );
1392
+ }
1393
+ };
1394
+ function createLocalEventStore(options) {
1395
+ return new LocalEventStore(options);
1396
+ }
1397
+
1398
+ // src/storage/local.ts
1399
+ function containsText(value, textValue) {
1400
+ if (typeof value === "string") return value.includes(textValue);
1401
+ if (value === null || typeof value !== "object") return false;
1402
+ if (Array.isArray(value)) {
1403
+ return value.some((item) => containsText(item, textValue));
1404
+ }
1405
+ return Object.entries(value).some(([key, item]) => key.includes(textValue) || containsText(item, textValue));
1406
+ }
1407
+ function rejectKnownSecret(value, knownSecrets, label) {
1408
+ const serialized = canonicalJson(value);
1409
+ if (knownSecrets.some((secret) => serialized.includes(secret) || containsText(value, secret))) {
1410
+ throw new StorageError(
1411
+ "KNOWN_SECRET",
1412
+ `${label} contains a configured known secret.`
1413
+ );
1414
+ }
1415
+ }
1416
+ function createLocalRunStorage(options) {
1417
+ if (typeof options.directory !== "string" || options.directory.trim().length === 0) {
1418
+ throw new StorageError(
1419
+ "INVALID_STORED_VALUE",
1420
+ "Local storage directory must be a non-empty path.",
1421
+ { path: "/directory" }
1422
+ );
1423
+ }
1424
+ const knownSecrets = options.knownSecrets ?? [];
1425
+ if (!Array.isArray(knownSecrets) || knownSecrets.some((secret) => typeof secret !== "string" || secret.length === 0)) {
1426
+ throw new StorageError(
1427
+ "INVALID_STORED_VALUE",
1428
+ "Known secrets must be non-empty strings.",
1429
+ { path: "/knownSecrets" }
1430
+ );
1431
+ }
1432
+ const frozenSecrets = Object.freeze([...knownSecrets]);
1433
+ const namespace = validateStorageId(options.namespace, "/namespace");
1434
+ const policy = validateRunStoragePolicy(options.policy);
1435
+ const directory = resolve(options.directory);
1436
+ const eventRoot = join(directory, "events");
1437
+ const artifactRoot = join(directory, "artifacts");
1438
+ const eventOptions = {
1439
+ root: eventRoot,
1440
+ maxEventPayloadBytes: policy.maxEventPayloadBytes,
1441
+ maxAppendBatchBytes: policy.maxAppendBatchBytes,
1442
+ knownSecrets: frozenSecrets
1443
+ };
1444
+ const artifactOptions = {
1445
+ root: artifactRoot,
1446
+ maxArtifactBytes: policy.maxArtifactBytes,
1447
+ maxTotalArtifactBytesPerRun: policy.maxTotalArtifactBytesPerRun,
1448
+ knownSecrets: frozenSecrets
1449
+ };
1450
+ const eventConfiguration = {
1451
+ schemaVersion: 1,
1452
+ root: eventRoot,
1453
+ maxEventPayloadBytes: policy.maxEventPayloadBytes,
1454
+ maxAppendBatchBytes: policy.maxAppendBatchBytes
1455
+ };
1456
+ const artifactConfiguration = {
1457
+ schemaVersion: 1,
1458
+ root: artifactRoot,
1459
+ maxArtifactBytes: policy.maxArtifactBytes,
1460
+ maxTotalArtifactBytesPerRun: policy.maxTotalArtifactBytesPerRun
1461
+ };
1462
+ rejectKnownSecret(eventConfiguration, frozenSecrets, "Event-store configuration");
1463
+ rejectKnownSecret(artifactConfiguration, frozenSecrets, "Artifact-store configuration");
1464
+ const record = validateRunStorageRecord({
1465
+ schemaVersion: 1,
1466
+ namespace,
1467
+ eventStore: {
1468
+ schemaVersion: 1,
1469
+ name: "local-jsonl",
1470
+ version: "1",
1471
+ configDigest: digestJson(eventConfiguration)
1472
+ },
1473
+ artifactStore: {
1474
+ schemaVersion: 1,
1475
+ name: "local-files",
1476
+ version: "1",
1477
+ configDigest: digestJson(artifactConfiguration)
1478
+ },
1479
+ policy
1480
+ });
1481
+ rejectKnownSecret(record, frozenSecrets, "Storage binding record");
1482
+ return Object.freeze({
1483
+ record,
1484
+ knownSecrets: frozenSecrets,
1485
+ eventStore: createLocalEventStore(eventOptions),
1486
+ artifactStore: createLocalArtifactStore(artifactOptions)
1487
+ });
1488
+ }
1489
+
1490
+ export { createLocalArtifactStore, createLocalEventStore, createLocalRunStorage };
1491
+ //# sourceMappingURL=local.js.map
1492
+ //# sourceMappingURL=local.js.map