nebula-notebook 0.2.16 → 0.2.18

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 (40) hide show
  1. package/dist/assets/{errorwidget-DfcjRDKM.js → errorwidget-t1RLHQYw.js} +1 -1
  2. package/dist/assets/{index-CyyuPG9B.js → index-BMuaZuus.js} +1 -1
  3. package/dist/assets/index-Bvhz0ltE.css +32 -0
  4. package/dist/assets/{index-LZ-zzybU.js → index-Cq1BG1_T.js} +205 -200
  5. package/dist/assets/{index-DrhpYYGt.js → index-DTGJj27s.js} +1 -1
  6. package/dist/assets/{index-COAUb-T8.js → index-DzeeB-u3.js} +1 -1
  7. package/dist/assets/{services-shim-CCJm9nl7.js → services-shim-Dg9Zi6xv.js} +1 -1
  8. package/dist/index.html +2 -2
  9. package/node-server/dist/auth/auth-middleware.js +3 -1
  10. package/node-server/dist/fs/fs-service.js +104 -8
  11. package/node-server/dist/fs/index.d.ts +1 -0
  12. package/node-server/dist/fs/index.js +1 -0
  13. package/node-server/dist/fs/sealed-path.d.ts +41 -0
  14. package/node-server/dist/fs/sealed-path.js +228 -0
  15. package/node-server/dist/fs/types.d.ts +4 -2
  16. package/node-server/dist/fs/types.js +2 -2
  17. package/node-server/dist/index.js +22 -13
  18. package/node-server/dist/kernel/kernel-service.d.ts +2 -2
  19. package/node-server/dist/kernel/kernel-service.js +15 -5
  20. package/node-server/dist/kernel/types.d.ts +16 -0
  21. package/node-server/dist/notebook/operation-router.js +13 -2
  22. package/node-server/dist/provenance/canonical-json.d.ts +36 -0
  23. package/node-server/dist/provenance/canonical-json.js +218 -0
  24. package/node-server/dist/provenance/provenance-store.d.ts +63 -0
  25. package/node-server/dist/provenance/provenance-store.js +598 -0
  26. package/node-server/dist/provenance/replay-seal-service.d.ts +223 -0
  27. package/node-server/dist/provenance/replay-seal-service.js +1702 -0
  28. package/node-server/dist/provenance/types.d.ts +65 -0
  29. package/node-server/dist/provenance/types.js +2 -0
  30. package/node-server/dist/routes/fs.js +27 -0
  31. package/node-server/dist/routes/kernel.js +15 -0
  32. package/node-server/dist/routes/notebook.js +54 -0
  33. package/node-server/dist/routes/replay-seal.d.ts +9 -0
  34. package/node-server/dist/routes/replay-seal.js +66 -0
  35. package/node-server/dist/server/bind-host.d.ts +12 -0
  36. package/node-server/dist/server/bind-host.js +56 -0
  37. package/node-server/dist/server/cors-origin.d.ts +1 -0
  38. package/node-server/dist/server/cors-origin.js +32 -0
  39. package/package.json +1 -1
  40. package/dist/assets/index-Czch8hB-.css +0 -32
@@ -0,0 +1,598 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ProvenanceStore = exports.BlobIntegrityError = exports.IdempotencyConflictError = exports.ProvenanceIntegrityError = void 0;
37
+ const crypto_1 = require("crypto");
38
+ const fsp = __importStar(require("fs/promises"));
39
+ const path = __importStar(require("path"));
40
+ const canonical_json_1 = require("./canonical-json");
41
+ const SHA256_RE = /^[a-f0-9]{64}$/;
42
+ class ProvenanceIntegrityError extends Error {
43
+ code = 'PROVENANCE_INTEGRITY_ERROR';
44
+ failedLine;
45
+ constructor(message, failedLine) {
46
+ super(message);
47
+ this.name = 'ProvenanceIntegrityError';
48
+ this.failedLine = failedLine;
49
+ }
50
+ }
51
+ exports.ProvenanceIntegrityError = ProvenanceIntegrityError;
52
+ class IdempotencyConflictError extends Error {
53
+ code = 'IDEMPOTENCY_CONFLICT';
54
+ constructor(key) {
55
+ super(`Idempotency key "${key}" was already used with different provenance input`);
56
+ this.name = 'IdempotencyConflictError';
57
+ }
58
+ }
59
+ exports.IdempotencyConflictError = IdempotencyConflictError;
60
+ class BlobIntegrityError extends Error {
61
+ code = 'PROVENANCE_BLOB_INTEGRITY_ERROR';
62
+ constructor(message) {
63
+ super(message);
64
+ this.name = 'BlobIntegrityError';
65
+ }
66
+ }
67
+ exports.BlobIntegrityError = BlobIntegrityError;
68
+ /**
69
+ * Server-owned append-only scientific provenance storage.
70
+ *
71
+ * Undo/redo history remains a separate, client-replaceable projection. This
72
+ * store writes one canonical event per JSONL line and binds every event to the
73
+ * previous event hash. All mutations for a notebook are serialized in-process.
74
+ */
75
+ class ProvenanceStore {
76
+ static writeQueues = new Map();
77
+ durable;
78
+ clock;
79
+ idFactory;
80
+ constructor(options = {}) {
81
+ this.durable = options.durable !== false;
82
+ this.clock = options.clock ?? (() => new Date());
83
+ this.idFactory = options.idFactory ?? crypto_1.randomUUID;
84
+ }
85
+ getLedgerPath(notebookPath) {
86
+ const normalizedPath = this.normalizeNotebookPath(notebookPath);
87
+ return path.join(path.dirname(normalizedPath), '.nebula', `${path.basename(normalizedPath)}.provenance.jsonl`);
88
+ }
89
+ getBlobPath(notebookPath, sha256) {
90
+ this.assertSha256(sha256, 'blob sha256');
91
+ const normalizedPath = this.normalizeNotebookPath(notebookPath);
92
+ return path.join(path.dirname(normalizedPath), '.nebula', 'provenance', 'blobs', 'sha256', sha256.slice(0, 2), sha256);
93
+ }
94
+ async append(notebookPath, input) {
95
+ const normalizedPath = this.normalizeNotebookPath(notebookPath);
96
+ const normalizedInput = this.normalizeInput(input);
97
+ const ledgerPath = this.getLedgerPath(normalizedPath);
98
+ return this.runSerialized(`ledger:${ledgerPath}`, async () => {
99
+ const events = await this.readVerified(normalizedPath);
100
+ const requestHash = this.hashRequest(normalizedPath, normalizedInput);
101
+ if (normalizedInput.idempotencyKey) {
102
+ const existing = events.find((event) => event.idempotencyKey === normalizedInput.idempotencyKey);
103
+ if (existing) {
104
+ if (existing.idempotencyRequestHash !== requestHash) {
105
+ throw new IdempotencyConflictError(normalizedInput.idempotencyKey);
106
+ }
107
+ return { event: existing, appended: false };
108
+ }
109
+ }
110
+ const eventId = this.idFactory();
111
+ if (typeof eventId !== 'string' || eventId.length === 0) {
112
+ throw new TypeError('Provenance event id must be a non-empty string');
113
+ }
114
+ if (events.some((event) => event.eventId === eventId)) {
115
+ throw new ProvenanceIntegrityError(`Duplicate provenance event id: ${eventId}`);
116
+ }
117
+ const timestamp = this.clock().toISOString();
118
+ const previous = events.at(-1);
119
+ const unsignedEvent = {
120
+ schemaVersion: 1,
121
+ eventId,
122
+ seq: (previous?.seq ?? 0) + 1,
123
+ timestamp,
124
+ notebookPath: normalizedPath,
125
+ type: normalizedInput.type,
126
+ actor: normalizedInput.actor,
127
+ payload: normalizedInput.payload ?? null,
128
+ ...(normalizedInput.runId !== undefined ? { runId: normalizedInput.runId } : {}),
129
+ ...(normalizedInput.taskId !== undefined ? { taskId: normalizedInput.taskId } : {}),
130
+ ...(normalizedInput.sourceId !== undefined ? { sourceId: normalizedInput.sourceId } : {}),
131
+ ...(normalizedInput.executionId !== undefined ? { executionId: normalizedInput.executionId } : {}),
132
+ ...(normalizedInput.inputManifestSha256 !== undefined
133
+ ? { inputManifestSha256: normalizedInput.inputManifestSha256 }
134
+ : {}),
135
+ ...(normalizedInput.environmentManifestSha256 !== undefined
136
+ ? { environmentManifestSha256: normalizedInput.environmentManifestSha256 }
137
+ : {}),
138
+ ...(normalizedInput.idempotencyKey !== undefined
139
+ ? {
140
+ idempotencyKey: normalizedInput.idempotencyKey,
141
+ idempotencyRequestHash: requestHash,
142
+ }
143
+ : {}),
144
+ prevHash: previous?.eventHash ?? null,
145
+ };
146
+ const event = {
147
+ ...unsignedEvent,
148
+ eventHash: (0, canonical_json_1.sha256Canonical)(unsignedEvent),
149
+ };
150
+ await this.appendLine(ledgerPath, `${(0, canonical_json_1.canonicalJson)(event)}\n`);
151
+ return { event, appended: true };
152
+ });
153
+ }
154
+ async read(notebookPath) {
155
+ return this.readVerified(this.normalizeNotebookPath(notebookPath));
156
+ }
157
+ async verify(notebookPath) {
158
+ const normalizedPath = this.normalizeNotebookPath(notebookPath);
159
+ try {
160
+ const events = await this.readVerified(normalizedPath);
161
+ const last = events.at(-1);
162
+ return {
163
+ valid: true,
164
+ eventCount: events.length,
165
+ lastSeq: last?.seq ?? 0,
166
+ headHash: last?.eventHash ?? null,
167
+ };
168
+ }
169
+ catch (error) {
170
+ const integrityError = error instanceof ProvenanceIntegrityError
171
+ ? error
172
+ : new ProvenanceIntegrityError(error instanceof Error ? error.message : String(error));
173
+ return {
174
+ valid: false,
175
+ eventCount: 0,
176
+ lastSeq: 0,
177
+ headHash: null,
178
+ error: integrityError.message,
179
+ ...(integrityError.failedLine !== undefined
180
+ ? { failedLine: integrityError.failedLine }
181
+ : {}),
182
+ };
183
+ }
184
+ }
185
+ async findByIdempotencyKey(notebookPath, idempotencyKey) {
186
+ if (typeof idempotencyKey !== 'string' || idempotencyKey.length === 0) {
187
+ throw new TypeError('Idempotency key must be a non-empty string');
188
+ }
189
+ const events = await this.read(notebookPath);
190
+ return events.find((event) => event.idempotencyKey === idempotencyKey) ?? null;
191
+ }
192
+ async putBlob(notebookPath, value) {
193
+ const bytes = this.toBuffer(value);
194
+ const sha256 = (0, canonical_json_1.sha256Hex)(bytes);
195
+ const blobPath = this.getBlobPath(notebookPath, sha256);
196
+ return this.runSerialized(`blob:${blobPath}`, async () => {
197
+ const existing = await this.readIfExists(blobPath);
198
+ if (existing) {
199
+ this.assertBlobBytes(existing, sha256, blobPath);
200
+ return { sha256, sizeBytes: existing.length };
201
+ }
202
+ const directory = path.dirname(blobPath);
203
+ await fsp.mkdir(directory, { recursive: true, mode: 0o700 });
204
+ const tempPath = path.join(directory, `.${sha256}.${process.pid}.${(0, crypto_1.randomUUID)()}.tmp`);
205
+ try {
206
+ const handle = await fsp.open(tempPath, 'wx', 0o600);
207
+ try {
208
+ await handle.writeFile(bytes);
209
+ if (this.durable)
210
+ await handle.sync();
211
+ }
212
+ finally {
213
+ await handle.close();
214
+ }
215
+ try {
216
+ // A fully-written temp file becomes visible atomically. link() is
217
+ // create-only, so a competing writer can never overwrite content.
218
+ await fsp.link(tempPath, blobPath);
219
+ if (this.durable)
220
+ await this.syncDirectory(directory);
221
+ }
222
+ catch (error) {
223
+ if (!this.isNodeError(error, 'EEXIST'))
224
+ throw error;
225
+ const winner = await fsp.readFile(blobPath);
226
+ this.assertBlobBytes(winner, sha256, blobPath);
227
+ }
228
+ }
229
+ finally {
230
+ await fsp.unlink(tempPath).catch(() => undefined);
231
+ }
232
+ return { sha256, sizeBytes: bytes.length };
233
+ });
234
+ }
235
+ async readBlob(notebookPath, sha256) {
236
+ const blobPath = this.getBlobPath(notebookPath, sha256);
237
+ let bytes;
238
+ try {
239
+ bytes = await fsp.readFile(blobPath);
240
+ }
241
+ catch (error) {
242
+ if (this.isNodeError(error, 'ENOENT')) {
243
+ throw new BlobIntegrityError(`Provenance blob not found: ${sha256}`);
244
+ }
245
+ throw error;
246
+ }
247
+ this.assertBlobBytes(bytes, sha256, blobPath);
248
+ return bytes;
249
+ }
250
+ async verifyBlob(notebookPath, sha256) {
251
+ const blobPath = this.getBlobPath(notebookPath, sha256);
252
+ let bytes;
253
+ try {
254
+ bytes = await fsp.readFile(blobPath);
255
+ }
256
+ catch (error) {
257
+ if (this.isNodeError(error, 'ENOENT')) {
258
+ return {
259
+ valid: false,
260
+ expectedSha256: sha256,
261
+ error: `Provenance blob not found: ${sha256}`,
262
+ };
263
+ }
264
+ return {
265
+ valid: false,
266
+ expectedSha256: sha256,
267
+ error: error instanceof Error ? error.message : String(error),
268
+ };
269
+ }
270
+ const actualSha256 = (0, canonical_json_1.sha256Hex)(bytes);
271
+ return {
272
+ valid: actualSha256 === sha256,
273
+ expectedSha256: sha256,
274
+ actualSha256,
275
+ sizeBytes: bytes.length,
276
+ ...(actualSha256 === sha256
277
+ ? {}
278
+ : { error: `Provenance blob hash mismatch: expected ${sha256}, got ${actualSha256}` }),
279
+ };
280
+ }
281
+ normalizeNotebookPath(notebookPath) {
282
+ if (typeof notebookPath !== 'string' || notebookPath.trim().length === 0) {
283
+ throw new TypeError('Notebook path must be a non-empty string');
284
+ }
285
+ return path.resolve(notebookPath);
286
+ }
287
+ normalizeInput(input) {
288
+ if (!input || typeof input !== 'object') {
289
+ throw new TypeError('Provenance event input must be an object');
290
+ }
291
+ if (typeof input.type !== 'string' || input.type.trim().length === 0) {
292
+ throw new TypeError('Provenance event type must be a non-empty string');
293
+ }
294
+ if (!input.actor || typeof input.actor !== 'object') {
295
+ throw new TypeError('Provenance actor is required');
296
+ }
297
+ if (typeof input.actor.kind !== 'string' || input.actor.kind.trim().length === 0) {
298
+ throw new TypeError('Provenance actor kind must be a non-empty string');
299
+ }
300
+ if (input.idempotencyKey !== undefined && input.idempotencyKey.length === 0) {
301
+ throw new TypeError('Idempotency key must be a non-empty string');
302
+ }
303
+ if (input.inputManifestSha256 !== undefined) {
304
+ this.assertSha256(input.inputManifestSha256, 'input manifest sha256');
305
+ }
306
+ if (input.environmentManifestSha256 !== undefined) {
307
+ this.assertSha256(input.environmentManifestSha256, 'environment manifest sha256');
308
+ }
309
+ // Canonical round-tripping both validates the full input as JSON and
310
+ // detaches it from caller-owned mutable objects before any await point.
311
+ const actor = this.canonicalClone(input.actor);
312
+ const payload = input.payload === undefined
313
+ ? undefined
314
+ : this.canonicalClone(input.payload);
315
+ const normalized = {
316
+ type: input.type,
317
+ actor,
318
+ ...(payload !== undefined ? { payload } : {}),
319
+ };
320
+ for (const key of [
321
+ 'runId',
322
+ 'taskId',
323
+ 'sourceId',
324
+ 'executionId',
325
+ 'inputManifestSha256',
326
+ 'environmentManifestSha256',
327
+ 'idempotencyKey',
328
+ ]) {
329
+ const value = input[key];
330
+ if (value !== undefined) {
331
+ if (typeof value !== 'string') {
332
+ throw new TypeError(`${key} must be a string`);
333
+ }
334
+ normalized[key] = value;
335
+ }
336
+ }
337
+ return normalized;
338
+ }
339
+ canonicalClone(value) {
340
+ return JSON.parse((0, canonical_json_1.canonicalJson)(value));
341
+ }
342
+ hashRequest(notebookPath, input) {
343
+ return (0, canonical_json_1.sha256Canonical)(this.requestEnvelope(notebookPath, input));
344
+ }
345
+ requestEnvelope(notebookPath, input) {
346
+ return {
347
+ notebookPath,
348
+ type: input.type,
349
+ actor: input.actor,
350
+ payload: input.payload ?? null,
351
+ ...(input.runId !== undefined ? { runId: input.runId } : {}),
352
+ ...(input.taskId !== undefined ? { taskId: input.taskId } : {}),
353
+ ...(input.sourceId !== undefined ? { sourceId: input.sourceId } : {}),
354
+ ...(input.executionId !== undefined ? { executionId: input.executionId } : {}),
355
+ ...(input.inputManifestSha256 !== undefined
356
+ ? { inputManifestSha256: input.inputManifestSha256 }
357
+ : {}),
358
+ ...(input.environmentManifestSha256 !== undefined
359
+ ? { environmentManifestSha256: input.environmentManifestSha256 }
360
+ : {}),
361
+ };
362
+ }
363
+ inputFromEvent(event) {
364
+ return {
365
+ type: event.type,
366
+ actor: event.actor,
367
+ payload: event.payload,
368
+ ...(event.runId !== undefined ? { runId: event.runId } : {}),
369
+ ...(event.taskId !== undefined ? { taskId: event.taskId } : {}),
370
+ ...(event.sourceId !== undefined ? { sourceId: event.sourceId } : {}),
371
+ ...(event.executionId !== undefined ? { executionId: event.executionId } : {}),
372
+ ...(event.inputManifestSha256 !== undefined
373
+ ? { inputManifestSha256: event.inputManifestSha256 }
374
+ : {}),
375
+ ...(event.environmentManifestSha256 !== undefined
376
+ ? { environmentManifestSha256: event.environmentManifestSha256 }
377
+ : {}),
378
+ ...(event.idempotencyKey !== undefined ? { idempotencyKey: event.idempotencyKey } : {}),
379
+ };
380
+ }
381
+ async readVerified(notebookPath) {
382
+ const ledgerPath = this.getLedgerPath(notebookPath);
383
+ let raw;
384
+ try {
385
+ raw = await fsp.readFile(ledgerPath, 'utf8');
386
+ }
387
+ catch (error) {
388
+ if (this.isNodeError(error, 'ENOENT'))
389
+ return [];
390
+ throw error;
391
+ }
392
+ if (raw.length === 0)
393
+ return [];
394
+ const lines = raw.split('\n');
395
+ if (lines.at(-1) === '')
396
+ lines.pop();
397
+ const events = [];
398
+ const eventIds = new Set();
399
+ const idempotencyKeys = new Set();
400
+ for (let index = 0; index < lines.length; index += 1) {
401
+ const lineNumber = index + 1;
402
+ const line = lines[index];
403
+ if (line.trim().length === 0) {
404
+ throw new ProvenanceIntegrityError('Blank line in provenance ledger', lineNumber);
405
+ }
406
+ let record;
407
+ try {
408
+ const parsed = JSON.parse(line);
409
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
410
+ throw new Error('event is not an object');
411
+ }
412
+ record = parsed;
413
+ }
414
+ catch (error) {
415
+ throw new ProvenanceIntegrityError(`Invalid provenance JSON at line ${lineNumber}: ${error instanceof Error ? error.message : String(error)}`, lineNumber);
416
+ }
417
+ const event = record;
418
+ this.validateEventShape(event, notebookPath, lineNumber);
419
+ const expectedSeq = index + 1;
420
+ if (event.seq !== expectedSeq) {
421
+ throw new ProvenanceIntegrityError(`Invalid provenance sequence at line ${lineNumber}: expected ${expectedSeq}, got ${event.seq}`, lineNumber);
422
+ }
423
+ const expectedPrevHash = events.at(-1)?.eventHash ?? null;
424
+ if (event.prevHash !== expectedPrevHash) {
425
+ throw new ProvenanceIntegrityError(`Invalid previous event hash at line ${lineNumber}`, lineNumber);
426
+ }
427
+ const { eventHash, ...unsignedEvent } = record;
428
+ let actualEventHash;
429
+ try {
430
+ actualEventHash = (0, canonical_json_1.sha256Canonical)(unsignedEvent);
431
+ }
432
+ catch (error) {
433
+ throw new ProvenanceIntegrityError(`Invalid canonical event at line ${lineNumber}: ${error instanceof Error ? error.message : String(error)}`, lineNumber);
434
+ }
435
+ if (actualEventHash !== eventHash) {
436
+ throw new ProvenanceIntegrityError(`Invalid event hash at line ${lineNumber}: expected ${String(eventHash)}, got ${actualEventHash}`, lineNumber);
437
+ }
438
+ if (eventIds.has(event.eventId)) {
439
+ throw new ProvenanceIntegrityError(`Duplicate provenance event id at line ${lineNumber}: ${event.eventId}`, lineNumber);
440
+ }
441
+ eventIds.add(event.eventId);
442
+ if (event.idempotencyKey !== undefined) {
443
+ if (idempotencyKeys.has(event.idempotencyKey)) {
444
+ throw new ProvenanceIntegrityError(`Duplicate idempotency key at line ${lineNumber}: ${event.idempotencyKey}`, lineNumber);
445
+ }
446
+ idempotencyKeys.add(event.idempotencyKey);
447
+ const requestHash = this.hashRequest(notebookPath, this.inputFromEvent(event));
448
+ if (event.idempotencyRequestHash !== requestHash) {
449
+ throw new ProvenanceIntegrityError(`Invalid idempotency request hash at line ${lineNumber}`, lineNumber);
450
+ }
451
+ }
452
+ else if (event.idempotencyRequestHash !== undefined) {
453
+ throw new ProvenanceIntegrityError(`Idempotency request hash has no key at line ${lineNumber}`, lineNumber);
454
+ }
455
+ events.push(event);
456
+ }
457
+ return events;
458
+ }
459
+ validateEventShape(event, notebookPath, lineNumber) {
460
+ const fail = (message) => {
461
+ throw new ProvenanceIntegrityError(`${message} at line ${lineNumber}`, lineNumber);
462
+ };
463
+ if (event.schemaVersion !== 1)
464
+ fail('Unsupported provenance schema version');
465
+ if (typeof event.eventId !== 'string' || event.eventId.length === 0)
466
+ fail('Invalid event id');
467
+ if (!Number.isSafeInteger(event.seq) || event.seq < 1)
468
+ fail('Invalid event sequence');
469
+ if (typeof event.timestamp !== 'string' || Number.isNaN(Date.parse(event.timestamp)))
470
+ fail('Invalid event timestamp');
471
+ if (event.notebookPath !== notebookPath)
472
+ fail('Provenance notebook path mismatch');
473
+ if (typeof event.type !== 'string' || event.type.length === 0)
474
+ fail('Invalid event type');
475
+ if (!event.actor || typeof event.actor !== 'object' || typeof event.actor.kind !== 'string' || event.actor.kind.length === 0) {
476
+ fail('Invalid provenance actor');
477
+ }
478
+ if (!('payload' in event))
479
+ fail('Missing event payload');
480
+ if (event.prevHash !== null && !SHA256_RE.test(event.prevHash))
481
+ fail('Invalid previous event hash');
482
+ if (typeof event.eventHash !== 'string' || !SHA256_RE.test(event.eventHash))
483
+ fail('Invalid event hash');
484
+ if (event.inputManifestSha256 !== undefined && !SHA256_RE.test(event.inputManifestSha256)) {
485
+ fail('Invalid input manifest hash');
486
+ }
487
+ if (event.environmentManifestSha256 !== undefined && !SHA256_RE.test(event.environmentManifestSha256)) {
488
+ fail('Invalid environment manifest hash');
489
+ }
490
+ if (event.idempotencyKey !== undefined && (typeof event.idempotencyKey !== 'string' || event.idempotencyKey.length === 0)) {
491
+ fail('Invalid idempotency key');
492
+ }
493
+ if (event.idempotencyRequestHash !== undefined && !SHA256_RE.test(event.idempotencyRequestHash)) {
494
+ fail('Invalid idempotency request hash');
495
+ }
496
+ }
497
+ async appendLine(ledgerPath, line) {
498
+ const directory = path.dirname(ledgerPath);
499
+ await fsp.mkdir(directory, { recursive: true, mode: 0o700 });
500
+ // A complete final JSON value without a trailing newline is valid JSONL.
501
+ // Preserve append-only semantics while restoring the separator before the
502
+ // next event; otherwise two individually valid events would concatenate.
503
+ const prefix = await this.ledgerNeedsNewline(ledgerPath) ? '\n' : '';
504
+ const handle = await fsp.open(ledgerPath, 'a', 0o600);
505
+ try {
506
+ await handle.appendFile(`${prefix}${line}`, 'utf8');
507
+ if (this.durable)
508
+ await handle.sync();
509
+ }
510
+ finally {
511
+ await handle.close();
512
+ }
513
+ if (this.durable)
514
+ await this.syncDirectory(directory);
515
+ }
516
+ async ledgerNeedsNewline(ledgerPath) {
517
+ let handle;
518
+ try {
519
+ handle = await fsp.open(ledgerPath, 'r');
520
+ }
521
+ catch (error) {
522
+ if (this.isNodeError(error, 'ENOENT'))
523
+ return false;
524
+ throw error;
525
+ }
526
+ try {
527
+ const stat = await handle.stat();
528
+ if (stat.size === 0)
529
+ return false;
530
+ const lastByte = Buffer.alloc(1);
531
+ const { bytesRead } = await handle.read(lastByte, 0, 1, stat.size - 1);
532
+ return bytesRead === 1 && lastByte[0] !== 0x0a;
533
+ }
534
+ finally {
535
+ await handle.close();
536
+ }
537
+ }
538
+ async syncDirectory(directory) {
539
+ try {
540
+ const handle = await fsp.open(directory, 'r');
541
+ try {
542
+ await handle.sync();
543
+ }
544
+ finally {
545
+ await handle.close();
546
+ }
547
+ }
548
+ catch {
549
+ // Directory fsync is unsupported on some filesystems/platforms. The
550
+ // file itself has still been fsynced.
551
+ }
552
+ }
553
+ async readIfExists(filePath) {
554
+ try {
555
+ return await fsp.readFile(filePath);
556
+ }
557
+ catch (error) {
558
+ if (this.isNodeError(error, 'ENOENT'))
559
+ return null;
560
+ throw error;
561
+ }
562
+ }
563
+ assertBlobBytes(bytes, expectedSha256, blobPath) {
564
+ const actualSha256 = (0, canonical_json_1.sha256Hex)(bytes);
565
+ if (actualSha256 !== expectedSha256) {
566
+ throw new BlobIntegrityError(`Provenance blob hash mismatch at ${blobPath}: expected ${expectedSha256}, got ${actualSha256}`);
567
+ }
568
+ }
569
+ toBuffer(value) {
570
+ if (typeof value === 'string')
571
+ return Buffer.from(value, 'utf8');
572
+ if (Buffer.isBuffer(value))
573
+ return Buffer.from(value);
574
+ if (value instanceof Uint8Array)
575
+ return Buffer.from(value);
576
+ throw new TypeError('Provenance blob must be a string, Buffer, or Uint8Array');
577
+ }
578
+ assertSha256(value, label) {
579
+ if (typeof value !== 'string' || !SHA256_RE.test(value)) {
580
+ throw new TypeError(`${label} must be a lowercase 64-character SHA-256 hex digest`);
581
+ }
582
+ }
583
+ runSerialized(key, operation) {
584
+ const previous = ProvenanceStore.writeQueues.get(key) ?? Promise.resolve();
585
+ const run = previous.catch(() => undefined).then(operation);
586
+ const tail = run.then(() => undefined, () => undefined);
587
+ ProvenanceStore.writeQueues.set(key, tail);
588
+ return run.finally(() => {
589
+ if (ProvenanceStore.writeQueues.get(key) === tail) {
590
+ ProvenanceStore.writeQueues.delete(key);
591
+ }
592
+ });
593
+ }
594
+ isNodeError(error, code) {
595
+ return error instanceof Error && error.code === code;
596
+ }
597
+ }
598
+ exports.ProvenanceStore = ProvenanceStore;