nebula-notebook 0.2.15 → 0.2.17

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 (56) hide show
  1. package/dist/assets/{errorwidget-B2XZ3hoa.js → errorwidget-t1RLHQYw.js} +1 -1
  2. package/dist/assets/{index-DqHzn4vg.js → index-BMuaZuus.js} +1 -1
  3. package/dist/assets/index-Bvhz0ltE.css +32 -0
  4. package/dist/assets/{index-NfxQwPBq.js → index-Cq1BG1_T.js} +205 -200
  5. package/dist/assets/{index-a9gKLTlv.js → index-DTGJj27s.js} +1 -1
  6. package/dist/assets/{index-CV0ZPmTH.js → index-DzeeB-u3.js} +1 -1
  7. package/dist/assets/{services-shim-D_SeYcB9.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/cluster/kernel-proxy.d.ts +15 -0
  11. package/node-server/dist/cluster/kernel-proxy.js +75 -0
  12. package/node-server/dist/fs/fs-service.js +104 -8
  13. package/node-server/dist/fs/index.d.ts +1 -0
  14. package/node-server/dist/fs/index.js +1 -0
  15. package/node-server/dist/fs/sealed-path.d.ts +41 -0
  16. package/node-server/dist/fs/sealed-path.js +228 -0
  17. package/node-server/dist/fs/types.d.ts +4 -2
  18. package/node-server/dist/fs/types.js +2 -2
  19. package/node-server/dist/index.js +24 -13
  20. package/node-server/dist/kernel/kernel-service.d.ts +2 -2
  21. package/node-server/dist/kernel/kernel-service.js +15 -5
  22. package/node-server/dist/kernel/types.d.ts +16 -0
  23. package/node-server/dist/notebook/headless-handler.d.ts +10 -0
  24. package/node-server/dist/notebook/headless-handler.js +64 -10
  25. package/node-server/dist/notebook/operation-router.js +13 -2
  26. package/node-server/dist/provenance/canonical-json.d.ts +36 -0
  27. package/node-server/dist/provenance/canonical-json.js +218 -0
  28. package/node-server/dist/provenance/provenance-store.d.ts +63 -0
  29. package/node-server/dist/provenance/provenance-store.js +598 -0
  30. package/node-server/dist/provenance/replay-seal-service.d.ts +223 -0
  31. package/node-server/dist/provenance/replay-seal-service.js +1702 -0
  32. package/node-server/dist/provenance/types.d.ts +65 -0
  33. package/node-server/dist/provenance/types.js +2 -0
  34. package/node-server/dist/routes/fs.js +27 -0
  35. package/node-server/dist/routes/kernel.js +15 -0
  36. package/node-server/dist/routes/notebook.js +54 -0
  37. package/node-server/dist/routes/replay-seal.d.ts +9 -0
  38. package/node-server/dist/routes/replay-seal.js +66 -0
  39. package/node-server/dist/scheduler/allocation-service.d.ts +3 -2
  40. package/node-server/dist/scheduler/allocation-service.js +45 -2
  41. package/node-server/dist/scheduler/arch.d.ts +30 -0
  42. package/node-server/dist/scheduler/arch.js +113 -0
  43. package/node-server/dist/scheduler/job-template.d.ts +11 -0
  44. package/node-server/dist/scheduler/mock-scheduler.d.ts +1 -0
  45. package/node-server/dist/scheduler/mock-scheduler.js +3 -0
  46. package/node-server/dist/scheduler/slurm-scheduler.d.ts +1 -0
  47. package/node-server/dist/scheduler/slurm-scheduler.js +34 -0
  48. package/node-server/dist/scheduler/types.d.ts +9 -0
  49. package/node-server/dist/scheduler/util.d.ts +6 -0
  50. package/node-server/dist/scheduler/util.js +16 -0
  51. package/node-server/dist/server/bind-host.d.ts +12 -0
  52. package/node-server/dist/server/bind-host.js +56 -0
  53. package/node-server/dist/server/cors-origin.d.ts +1 -0
  54. package/node-server/dist/server/cors-origin.js +32 -0
  55. package/package.json +1 -1
  56. package/dist/assets/index-Czch8hB-.css +0 -32
@@ -4,7 +4,7 @@
4
4
  * Manages Jupyter kernel sessions - spawning, execution, and lifecycle.
5
5
  * Uses ZeroMQ for kernel communication following the Jupyter messaging protocol.
6
6
  */
7
- import { KernelOutput, ExecutionResult, ExecutionQueueInfo, StartKernelOptions, SessionInfo, KernelServiceConfig } from './types';
7
+ import { KernelOutput, ExecutionResult, ExecutionQueueInfo, InternalExecutionOptions, StartKernelOptions, SessionInfo, KernelServiceConfig } from './types';
8
8
  import { SessionStore } from './session-store';
9
9
  import { KernelSpec } from './kernelspec';
10
10
  /** Kernel-originated comm event surfaced to onComm listeners. */
@@ -256,7 +256,7 @@ export declare class KernelService {
256
256
  /**
257
257
  * Execute code in a kernel session
258
258
  */
259
- executeCode(sessionId: string, code: string, onOutput: (output: KernelOutput, cellId?: string | null) => Promise<void>, onQueueInfo?: (info: ExecutionQueueInfo) => void, cellId?: string | null): Promise<ExecutionResult>;
259
+ executeCode(sessionId: string, code: string, onOutput: (output: KernelOutput, cellId?: string | null) => Promise<void>, onQueueInfo?: (info: ExecutionQueueInfo) => void, cellId?: string | null, internalOptions?: InternalExecutionOptions): Promise<ExecutionResult>;
260
260
  private enqueueExecution;
261
261
  /**
262
262
  * Serialize shell socket SENDS. Receives are owned by the unified shell
@@ -635,6 +635,12 @@ class KernelService {
635
635
  */
636
636
  async startKernel(options = {}) {
637
637
  const kernelName = options.kernelName || 'python3';
638
+ if (options.internalEnv
639
+ && (Object.keys(options.internalEnv).some((key) => key !== 'PYTHONNOUSERSITE')
640
+ || (options.internalEnv.PYTHONNOUSERSITE !== undefined
641
+ && options.internalEnv.PYTHONNOUSERSITE !== '1'))) {
642
+ throw new Error('Unsupported internal kernel environment override');
643
+ }
638
644
  // env:<pythonPath> kernels raw-launch a Python environment directly
639
645
  // (VSCode-style) — no kernelspec registration involved. Preflight here so
640
646
  // callers get a crisp, coded error instead of a spawn failure + ready
@@ -672,7 +678,7 @@ class KernelService {
672
678
  const cwd = options.cwd || (normalizedFilePath ? path.dirname(normalizedFilePath) : process.cwd());
673
679
  const proc = (0, child_process_1.spawn)(argv[0], argv.slice(1), {
674
680
  cwd,
675
- env: { ...process.env, ...spec.env },
681
+ env: { ...process.env, ...spec.env, ...options.internalEnv },
676
682
  stdio: ['pipe', 'pipe', 'pipe'],
677
683
  });
678
684
  // Log kernel stdout/stderr for debugging
@@ -1408,7 +1414,7 @@ class KernelService {
1408
1414
  /**
1409
1415
  * Execute code in a kernel session
1410
1416
  */
1411
- async executeCode(sessionId, code, onOutput, onQueueInfo, cellId) {
1417
+ async executeCode(sessionId, code, onOutput, onQueueInfo, cellId, internalOptions) {
1412
1418
  // Polyfill `!command` lines for kernels whose language lacks them (see
1413
1419
  // rewriteShellLines) — rewritten code still runs IN the kernel process,
1414
1420
  // so cwd/env/node are the kernel's, exactly like IPython's native `!`.
@@ -1433,7 +1439,7 @@ class KernelService {
1433
1439
  for (const o of stored) {
1434
1440
  await onOutput(o, cellId);
1435
1441
  }
1436
- });
1442
+ }, internalOptions?.storeHistory !== false);
1437
1443
  return { ...result, ...queueInfo };
1438
1444
  }
1439
1445
  catch (err) {
@@ -1506,7 +1512,7 @@ class KernelService {
1506
1512
  }
1507
1513
  return message;
1508
1514
  }
1509
- async executeCodeInternal(sessionId, code, onOutput) {
1515
+ async executeCodeInternal(sessionId, code, onOutput, storeHistory = true) {
1510
1516
  const session = this.sessions.get(sessionId);
1511
1517
  if (!session) {
1512
1518
  throw new Error(`Session ${sessionId} not found`);
@@ -1537,7 +1543,7 @@ class KernelService {
1537
1543
  const content = {
1538
1544
  code,
1539
1545
  silent: false,
1540
- store_history: true,
1546
+ store_history: storeHistory,
1541
1547
  user_expressions: {},
1542
1548
  allow_stdin: false,
1543
1549
  stop_on_error: true,
@@ -1571,6 +1577,10 @@ class KernelService {
1571
1577
  else if (msgType === 'execute_result' || msgType === 'display_data') {
1572
1578
  const output = this.formatDisplayData(msgContent.data || {}, msgContent.metadata || undefined);
1573
1579
  if (output) {
1580
+ output.jupyterOutputType = msgType;
1581
+ if (msgType === 'execute_result' && Number.isSafeInteger(msgContent.execution_count)) {
1582
+ output.jupyterExecutionCount = msgContent.execution_count;
1583
+ }
1574
1584
  await onOutput(output);
1575
1585
  }
1576
1586
  }
@@ -82,6 +82,14 @@ export interface KernelOutput {
82
82
  mimeBundle?: MimeBundle;
83
83
  metadata?: Record<string, JsonValue>;
84
84
  preferredMimeType?: string;
85
+ /** Exact protocol shape retained for immutable replay sealing. */
86
+ jupyterOutputType?: 'execute_result' | 'display_data';
87
+ /** Present for execute_result protocol messages. */
88
+ jupyterExecutionCount?: number;
89
+ }
90
+ export interface InternalExecutionOptions {
91
+ /** Server-authored probes use false so they cannot perturb notebook counts. */
92
+ storeHistory?: false;
85
93
  }
86
94
  export interface SequencedKernelOutput {
87
95
  seq: number;
@@ -114,6 +122,14 @@ export interface StartKernelOptions {
114
122
  kernelName?: string;
115
123
  cwd?: string;
116
124
  filePath?: string;
125
+ /**
126
+ * Server-only launch hardening. This is intentionally not accepted by the
127
+ * public kernel HTTP route; replay sealing uses it to exclude user-site
128
+ * packages before the Python process starts.
129
+ */
130
+ internalEnv?: {
131
+ PYTHONNOUSERSITE?: '1';
132
+ };
117
133
  }
118
134
  /**
119
135
  * Session info returned by API
@@ -138,6 +138,16 @@ export declare class HeadlessOperationHandler {
138
138
  * Used by startAgentSession to inform agent what changed between sessions.
139
139
  */
140
140
  getUpdatesSince(notebookPath: string, sinceTimestamp: number): UpdateSummary[];
141
+ /**
142
+ * Get-or-create the execution session for a notebook: on its bound cluster
143
+ * server when a binding exists (proxied session id), locally otherwise.
144
+ * A dead binding falls back to a fresh local kernel — allocations end
145
+ * routinely (walltime, cancel), and that fallback is the documented
146
+ * "falls back to a new kernel on next use" behavior.
147
+ */
148
+ private resolveKernelSession;
149
+ /** Start (get-or-create) a kernel on the bound server; null if it is gone. */
150
+ private startBoundKernel;
141
151
  private startKernelOp;
142
152
  private shutdownKernelOp;
143
153
  private restartKernelOp;
@@ -49,6 +49,7 @@ const registry_1 = require("../fs/notebook-formats/registry");
49
49
  const cell_metadata_1 = require("./cell-metadata");
50
50
  const undoRedoManager_1 = require("./undoRedoManager");
51
51
  const kernelspec_1 = require("../kernel/kernelspec");
52
+ const kernel_proxy_1 = require("../cluster/kernel-proxy");
52
53
  function copyCellOutput(output) {
53
54
  return {
54
55
  type: (output.type || 'stdout'),
@@ -1345,11 +1346,16 @@ class HeadlessOperationHandler {
1345
1346
  error: 'Kernel service not available. Make sure the Node.js server is properly initialized.',
1346
1347
  };
1347
1348
  }
1348
- // Get or create a kernel session for this notebook
1349
+ // Get or create a kernel session for this notebook. A kernel preference
1350
+ // with a serverId means the notebook is bound to a cluster peer (compute
1351
+ // allocation) — the kernel must live THERE, not on this server: resolving
1352
+ // locally would run cells on the login node, and on a cross-arch
1353
+ // allocation the local ipykernel probe can't even run the env's python.
1349
1354
  const requestedSessionId = operation.sessionId ?? operation.session_id;
1350
- const preferredKernelName = this.kernelService.getNotebookKernelPreference(notebookPath)?.kernelName || 'python3';
1355
+ const kernelPref = this.kernelService.getNotebookKernelPreference(notebookPath);
1356
+ const preferredKernelName = kernelPref?.kernelName || 'python3';
1351
1357
  let sessionId = null;
1352
- if (requestedSessionId && this.kernelService.hasSession(requestedSessionId)) {
1358
+ if (requestedSessionId && (this.kernelService.hasSession(requestedSessionId) || (0, kernel_proxy_1.isProxiedSession)(requestedSessionId))) {
1353
1359
  sessionId = requestedSessionId;
1354
1360
  }
1355
1361
  else {
@@ -1357,8 +1363,7 @@ class HeadlessOperationHandler {
1357
1363
  }
1358
1364
  if (!sessionId) {
1359
1365
  try {
1360
- const result = await this.kernelService.getOrCreateKernel(notebookPath, preferredKernelName);
1361
- sessionId = result.sessionId;
1366
+ sessionId = await this.resolveKernelSession(notebookPath, preferredKernelName, kernelPref?.serverId ?? null);
1362
1367
  }
1363
1368
  catch (err) {
1364
1369
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -1409,9 +1414,11 @@ class HeadlessOperationHandler {
1409
1414
  // Create execution promise
1410
1415
  const executeTask = async () => {
1411
1416
  try {
1412
- const result = await this.kernelService.executeCode(sessionId, code, outputCallback, (info) => {
1413
- queueInfo = info;
1414
- }, actualCellId);
1417
+ const result = (0, kernel_proxy_1.isProxiedSession)(sessionId)
1418
+ ? await (0, kernel_proxy_1.executeRemoteCode)(sessionId, code, outputCallback, actualCellId)
1419
+ : await this.kernelService.executeCode(sessionId, code, outputCallback, (info) => {
1420
+ queueInfo = info;
1421
+ }, actualCellId);
1415
1422
  executionCount = result.executionCount;
1416
1423
  if (!queueInfo && result.queuePosition !== undefined && result.queueLength !== undefined) {
1417
1424
  queueInfo = { queuePosition: result.queuePosition, queueLength: result.queueLength };
@@ -1579,14 +1586,61 @@ class HeadlessOperationHandler {
1579
1586
  // -------------------------------------------------------------------------
1580
1587
  // Kernel Operations
1581
1588
  // -------------------------------------------------------------------------
1589
+ /**
1590
+ * Get-or-create the execution session for a notebook: on its bound cluster
1591
+ * server when a binding exists (proxied session id), locally otherwise.
1592
+ * A dead binding falls back to a fresh local kernel — allocations end
1593
+ * routinely (walltime, cancel), and that fallback is the documented
1594
+ * "falls back to a new kernel on next use" behavior.
1595
+ */
1596
+ async resolveKernelSession(notebookPath, kernelName, boundServerId) {
1597
+ if (boundServerId) {
1598
+ const remote = await this.startBoundKernel(notebookPath, kernelName, boundServerId);
1599
+ if (remote)
1600
+ return remote.sessionId;
1601
+ }
1602
+ const result = await this.kernelService.getOrCreateKernel(notebookPath, kernelName);
1603
+ return result.sessionId;
1604
+ }
1605
+ /** Start (get-or-create) a kernel on the bound server; null if it is gone. */
1606
+ async startBoundKernel(notebookPath, kernelName, serverId) {
1607
+ try {
1608
+ return await (0, kernel_proxy_1.startRemoteKernel)(serverId, kernelName, notebookPath);
1609
+ }
1610
+ catch (err) {
1611
+ const msg = err instanceof Error ? err.message : String(err);
1612
+ console.warn(`[Headless] Bound server ${serverId} unavailable (${msg}) — falling back to a local kernel`);
1613
+ return null;
1614
+ }
1615
+ }
1582
1616
  async startKernelOp(operation, notebookPath) {
1583
1617
  if (!this.kernelService) {
1584
1618
  return { success: false, error: 'Kernel service not available' };
1585
1619
  }
1586
1620
  const kernelName = operation.kernelName || 'python3';
1587
1621
  try {
1588
- const { sessionId, created } = await this.kernelService.getOrCreateKernel(notebookPath, kernelName);
1589
- this.kernelService.saveNotebookKernelPreference(notebookPath, kernelName);
1622
+ // Bound notebooks start their kernel on the bound server, and the
1623
+ // binding must survive the preference re-save — dropping serverId here
1624
+ // silently unbound notebooks from their allocations.
1625
+ const boundServerId = this.kernelService.getNotebookKernelPreference(notebookPath)?.serverId ?? null;
1626
+ let sessionId;
1627
+ let created;
1628
+ if (boundServerId) {
1629
+ const remote = await this.startBoundKernel(notebookPath, kernelName, boundServerId);
1630
+ if (remote) {
1631
+ sessionId = remote.sessionId;
1632
+ created = remote.created;
1633
+ this.kernelService.saveNotebookKernelPreference(notebookPath, kernelName, boundServerId);
1634
+ }
1635
+ else {
1636
+ ({ sessionId, created } = await this.kernelService.getOrCreateKernel(notebookPath, kernelName));
1637
+ this.kernelService.saveNotebookKernelPreference(notebookPath, kernelName);
1638
+ }
1639
+ }
1640
+ else {
1641
+ ({ sessionId, created } = await this.kernelService.getOrCreateKernel(notebookPath, kernelName));
1642
+ this.kernelService.saveNotebookKernelPreference(notebookPath, kernelName);
1643
+ }
1590
1644
  const spec = (0, kernelspec_1.getKernelSpec)(kernelName);
1591
1645
  const metadataResult = await this.fsService.updateNotebookMetadata(notebookPath, {
1592
1646
  kernelspec: {
@@ -49,8 +49,10 @@ const os = __importStar(require("os"));
49
49
  const ws_1 = require("ws");
50
50
  const cell_hash_1 = require("./cell-hash");
51
51
  const registry_1 = require("../fs/notebook-formats/registry");
52
+ const sealed_path_1 = require("../fs/sealed-path");
52
53
  const AGENT_LOCK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
53
54
  const UI_STALE_TIMEOUT_MS = 45 * 1000; // 45 seconds
55
+ const READ_ONLY_OPERATIONS = new Set(['readCell', 'readCellOutput', 'searchCells', 'readNotebook', 'getUpdatesSince']);
54
56
  function normalizeNotebookPath(notebookPath) {
55
57
  // IMPORTANT: This must match how the kernel service normalizes file paths.
56
58
  // If the UI registers `~/foo.ipynb` but the kernel associates the session
@@ -320,6 +322,16 @@ class OperationRouter {
320
322
  const normalizedPath = normalizeNotebookPath(notebookPath);
321
323
  const opType = operation.type;
322
324
  const agentId = operation.agentId;
325
+ const sealInfo = (0, sealed_path_1.classifySealedPath)(normalizedPath);
326
+ if (sealInfo.sealed && !READ_ONLY_OPERATIONS.has(opType)) {
327
+ return {
328
+ success: false,
329
+ error: `Cannot ${opType || 'modify notebook'}: sealed evidence ${sealInfo.sealId} is read-only`,
330
+ code: 'sealed_read_only',
331
+ seal_id: sealInfo.sealId,
332
+ path: sealInfo.canonicalPath,
333
+ };
334
+ }
323
335
  // For createNotebook, route to ANY connected UI (not path-specific)
324
336
  // This allows the UI to open the new notebook in a new tab.
325
337
  // EXCEPT text notebook formats (.py/.qmd): the UI's create handler only
@@ -375,10 +387,9 @@ class OperationRouter {
375
387
  return { ...result, backend };
376
388
  }
377
389
  // Enforce agent session for write operations
378
- const readOnlyOps = new Set(['readCell', 'readCellOutput', 'searchCells', 'readNotebook', 'getUpdatesSince']);
379
390
  const sessionOps = new Set(['startAgentSession', 'endAgentSession']);
380
391
  const creationOps = new Set(['createNotebook']); // Operations that create files (don't require session)
381
- const isWrite = !readOnlyOps.has(opType) && !sessionOps.has(opType) && !creationOps.has(opType);
392
+ const isWrite = !READ_ONLY_OPERATIONS.has(opType) && !sessionOps.has(opType) && !creationOps.has(opType);
382
393
  if (isWrite) {
383
394
  if (!isLocked) {
384
395
  console.log(` -> BLOCKED: Write requires active agent session`);
@@ -0,0 +1,36 @@
1
+ export type CanonicalJsonValue = null | boolean | number | string | CanonicalJsonValue[] | {
2
+ [key: string]: CanonicalJsonValue;
3
+ };
4
+ /**
5
+ * Cross-runtime hash profile used by every structured Nebula provenance hash.
6
+ *
7
+ * This value is included in the encoded root, so future profiles cannot share
8
+ * a digest with this one even when their value encodings happen to match.
9
+ */
10
+ export declare const CANONICAL_HASH_PROFILE: "nebula-canonical-hash-v1";
11
+ /**
12
+ * Serialize a JSON value deterministically.
13
+ *
14
+ * Object keys are sorted recursively. Values that regular JSON.stringify
15
+ * silently drops or coerces are rejected: provenance hashes must never depend
16
+ * on an implicit lossy conversion.
17
+ */
18
+ export declare function canonicalJson(value: unknown): string;
19
+ export declare function sha256Hex(value: string | Buffer | Uint8Array): string;
20
+ /**
21
+ * Convert a JSON-like value to an injective, language-neutral typed tree.
22
+ *
23
+ * - Object keys use Unicode scalar/code-point order rather than JavaScript's
24
+ * UTF-16 code-unit order.
25
+ * - Safe integral numbers share one integer representation.
26
+ * - Every other finite Number, including -0 and unsafe integral Numbers, is
27
+ * represented by its exact big-endian IEEE-754 binary64 bits.
28
+ * - Strings containing unpaired UTF-16 surrogates are rejected.
29
+ *
30
+ * The returned tree itself contains only JSON values, and contains no dynamic
31
+ * object keys, so regular JSON serialization is deterministic cross-runtime.
32
+ */
33
+ export declare function canonicalizeForHash(value: unknown): CanonicalJsonValue;
34
+ /** Serialize the typed cross-runtime representation used as SHA-256 input. */
35
+ export declare function hashCanonicalJson(value: unknown): string;
36
+ export declare function sha256Canonical(value: unknown): string;
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CANONICAL_HASH_PROFILE = void 0;
4
+ exports.canonicalJson = canonicalJson;
5
+ exports.sha256Hex = sha256Hex;
6
+ exports.canonicalizeForHash = canonicalizeForHash;
7
+ exports.hashCanonicalJson = hashCanonicalJson;
8
+ exports.sha256Canonical = sha256Canonical;
9
+ const crypto_1 = require("crypto");
10
+ /**
11
+ * Cross-runtime hash profile used by every structured Nebula provenance hash.
12
+ *
13
+ * This value is included in the encoded root, so future profiles cannot share
14
+ * a digest with this one even when their value encodings happen to match.
15
+ */
16
+ exports.CANONICAL_HASH_PROFILE = 'nebula-canonical-hash-v1';
17
+ /**
18
+ * Serialize a JSON value deterministically.
19
+ *
20
+ * Object keys are sorted recursively. Values that regular JSON.stringify
21
+ * silently drops or coerces are rejected: provenance hashes must never depend
22
+ * on an implicit lossy conversion.
23
+ */
24
+ function canonicalJson(value) {
25
+ return serializeCanonical(value, new WeakSet(), '$');
26
+ }
27
+ function sha256Hex(value) {
28
+ return (0, crypto_1.createHash)('sha256').update(value).digest('hex');
29
+ }
30
+ /**
31
+ * Convert a JSON-like value to an injective, language-neutral typed tree.
32
+ *
33
+ * - Object keys use Unicode scalar/code-point order rather than JavaScript's
34
+ * UTF-16 code-unit order.
35
+ * - Safe integral numbers share one integer representation.
36
+ * - Every other finite Number, including -0 and unsafe integral Numbers, is
37
+ * represented by its exact big-endian IEEE-754 binary64 bits.
38
+ * - Strings containing unpaired UTF-16 surrogates are rejected.
39
+ *
40
+ * The returned tree itself contains only JSON values, and contains no dynamic
41
+ * object keys, so regular JSON serialization is deterministic cross-runtime.
42
+ */
43
+ function canonicalizeForHash(value) {
44
+ return [
45
+ exports.CANONICAL_HASH_PROFILE,
46
+ encodeForHash(value, new WeakSet(), '$'),
47
+ ];
48
+ }
49
+ /** Serialize the typed cross-runtime representation used as SHA-256 input. */
50
+ function hashCanonicalJson(value) {
51
+ return canonicalJson(canonicalizeForHash(value));
52
+ }
53
+ function sha256Canonical(value) {
54
+ return sha256Hex(hashCanonicalJson(value));
55
+ }
56
+ function encodeForHash(value, ancestors, location) {
57
+ if (value === null)
58
+ return ['null'];
59
+ switch (typeof value) {
60
+ case 'boolean':
61
+ return ['boolean', value];
62
+ case 'string':
63
+ assertUnicodeScalarString(value, location);
64
+ return ['string', value];
65
+ case 'number':
66
+ if (!Number.isFinite(value)) {
67
+ throw new TypeError(`Canonical hash requires finite numbers at ${location}`);
68
+ }
69
+ if (Number.isSafeInteger(value) && !Object.is(value, -0)) {
70
+ return ['integer', value];
71
+ }
72
+ return ['float64', float64Hex(value)];
73
+ case 'undefined':
74
+ throw new TypeError(`Canonical hash cannot encode undefined at ${location}`);
75
+ case 'bigint':
76
+ case 'function':
77
+ case 'symbol':
78
+ throw new TypeError(`Canonical hash cannot encode ${typeof value} at ${location}`);
79
+ case 'object':
80
+ break;
81
+ default:
82
+ throw new TypeError(`Canonical hash cannot encode value at ${location}`);
83
+ }
84
+ const objectValue = value;
85
+ if (ancestors.has(objectValue)) {
86
+ throw new TypeError(`Canonical hash cannot encode a cyclic value at ${location}`);
87
+ }
88
+ ancestors.add(objectValue);
89
+ try {
90
+ if (Array.isArray(value)) {
91
+ const items = [];
92
+ for (let index = 0; index < value.length; index += 1) {
93
+ if (!(index in value)) {
94
+ throw new TypeError(`Canonical hash cannot encode a sparse array at ${location}[${index}]`);
95
+ }
96
+ items.push(encodeForHash(value[index], ancestors, `${location}[${index}]`));
97
+ }
98
+ if (Object.getOwnPropertySymbols(value).length > 0) {
99
+ throw new TypeError(`Canonical hash cannot encode symbol keys at ${location}`);
100
+ }
101
+ return ['array', items];
102
+ }
103
+ const prototype = Object.getPrototypeOf(value);
104
+ if (prototype !== Object.prototype && prototype !== null) {
105
+ throw new TypeError(`Canonical hash requires a plain object at ${location}`);
106
+ }
107
+ if (Object.getOwnPropertySymbols(value).length > 0) {
108
+ throw new TypeError(`Canonical hash cannot encode symbol keys at ${location}`);
109
+ }
110
+ const record = value;
111
+ const pairs = [];
112
+ const keys = Object.keys(record);
113
+ for (const key of keys)
114
+ assertUnicodeScalarString(key, `${location} object key`);
115
+ keys.sort(compareUnicodeCodePoints);
116
+ for (const key of keys) {
117
+ pairs.push([
118
+ key,
119
+ encodeForHash(record[key], ancestors, `${location}.${key}`),
120
+ ]);
121
+ }
122
+ return ['object', pairs];
123
+ }
124
+ finally {
125
+ ancestors.delete(objectValue);
126
+ }
127
+ }
128
+ function float64Hex(value) {
129
+ const bytes = Buffer.allocUnsafe(8);
130
+ bytes.writeDoubleBE(value, 0);
131
+ return bytes.toString('hex');
132
+ }
133
+ function assertUnicodeScalarString(value, location) {
134
+ for (let index = 0; index < value.length; index += 1) {
135
+ const codeUnit = value.charCodeAt(index);
136
+ if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
137
+ const following = value.charCodeAt(index + 1);
138
+ if (!(following >= 0xdc00 && following <= 0xdfff)) {
139
+ throw new TypeError(`Canonical hash requires Unicode scalar strings at ${location}`);
140
+ }
141
+ index += 1;
142
+ }
143
+ else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
144
+ throw new TypeError(`Canonical hash requires Unicode scalar strings at ${location}`);
145
+ }
146
+ }
147
+ }
148
+ function compareUnicodeCodePoints(left, right) {
149
+ let leftIndex = 0;
150
+ let rightIndex = 0;
151
+ while (leftIndex < left.length && rightIndex < right.length) {
152
+ const leftPoint = left.codePointAt(leftIndex);
153
+ const rightPoint = right.codePointAt(rightIndex);
154
+ if (leftPoint !== rightPoint)
155
+ return leftPoint - rightPoint;
156
+ leftIndex += leftPoint > 0xffff ? 2 : 1;
157
+ rightIndex += rightPoint > 0xffff ? 2 : 1;
158
+ }
159
+ return (left.length - leftIndex) - (right.length - rightIndex);
160
+ }
161
+ function serializeCanonical(value, ancestors, location) {
162
+ if (value === null)
163
+ return 'null';
164
+ switch (typeof value) {
165
+ case 'boolean':
166
+ return value ? 'true' : 'false';
167
+ case 'string':
168
+ return JSON.stringify(value);
169
+ case 'number':
170
+ if (!Number.isFinite(value)) {
171
+ throw new TypeError(`Canonical JSON requires finite numbers at ${location}`);
172
+ }
173
+ // JSON.stringify canonicalizes -0 to 0, which is the desired JSON value.
174
+ return JSON.stringify(value);
175
+ case 'undefined':
176
+ throw new TypeError(`Canonical JSON cannot encode undefined at ${location}`);
177
+ case 'bigint':
178
+ case 'function':
179
+ case 'symbol':
180
+ throw new TypeError(`Canonical JSON cannot encode ${typeof value} at ${location}`);
181
+ case 'object':
182
+ break;
183
+ default:
184
+ throw new TypeError(`Canonical JSON cannot encode value at ${location}`);
185
+ }
186
+ const objectValue = value;
187
+ if (ancestors.has(objectValue)) {
188
+ throw new TypeError(`Canonical JSON cannot encode a cyclic value at ${location}`);
189
+ }
190
+ ancestors.add(objectValue);
191
+ try {
192
+ if (Array.isArray(value)) {
193
+ const items = [];
194
+ for (let index = 0; index < value.length; index += 1) {
195
+ if (!(index in value)) {
196
+ throw new TypeError(`Canonical JSON cannot encode a sparse array at ${location}[${index}]`);
197
+ }
198
+ items.push(serializeCanonical(value[index], ancestors, `${location}[${index}]`));
199
+ }
200
+ return `[${items.join(',')}]`;
201
+ }
202
+ const prototype = Object.getPrototypeOf(value);
203
+ if (prototype !== Object.prototype && prototype !== null) {
204
+ throw new TypeError(`Canonical JSON requires a plain object at ${location}`);
205
+ }
206
+ if (Object.getOwnPropertySymbols(value).length > 0) {
207
+ throw new TypeError(`Canonical JSON cannot encode symbol keys at ${location}`);
208
+ }
209
+ const record = value;
210
+ const properties = Object.keys(record)
211
+ .sort()
212
+ .map((key) => `${JSON.stringify(key)}:${serializeCanonical(record[key], ancestors, `${location}.${key}`)}`);
213
+ return `{${properties.join(',')}}`;
214
+ }
215
+ finally {
216
+ ancestors.delete(objectValue);
217
+ }
218
+ }
@@ -0,0 +1,63 @@
1
+ import type { ProvenanceAppendResult, ProvenanceBlobReference, ProvenanceBlobVerification, ProvenanceEvent, ProvenanceEventInput, ProvenanceVerification } from './types';
2
+ export declare class ProvenanceIntegrityError extends Error {
3
+ readonly code = "PROVENANCE_INTEGRITY_ERROR";
4
+ readonly failedLine?: number;
5
+ constructor(message: string, failedLine?: number);
6
+ }
7
+ export declare class IdempotencyConflictError extends Error {
8
+ readonly code = "IDEMPOTENCY_CONFLICT";
9
+ constructor(key: string);
10
+ }
11
+ export declare class BlobIntegrityError extends Error {
12
+ readonly code = "PROVENANCE_BLOB_INTEGRITY_ERROR";
13
+ constructor(message: string);
14
+ }
15
+ export interface ProvenanceStoreOptions {
16
+ /** fsync ledger/blob writes. Defaults to true. */
17
+ durable?: boolean;
18
+ /** Test seam; production events always use the server clock. */
19
+ clock?: () => Date;
20
+ /** Test seam; production events use cryptographically random UUIDs. */
21
+ idFactory?: () => string;
22
+ }
23
+ /**
24
+ * Server-owned append-only scientific provenance storage.
25
+ *
26
+ * Undo/redo history remains a separate, client-replaceable projection. This
27
+ * store writes one canonical event per JSONL line and binds every event to the
28
+ * previous event hash. All mutations for a notebook are serialized in-process.
29
+ */
30
+ export declare class ProvenanceStore {
31
+ private static readonly writeQueues;
32
+ private readonly durable;
33
+ private readonly clock;
34
+ private readonly idFactory;
35
+ constructor(options?: ProvenanceStoreOptions);
36
+ getLedgerPath(notebookPath: string): string;
37
+ getBlobPath(notebookPath: string, sha256: string): string;
38
+ append(notebookPath: string, input: ProvenanceEventInput): Promise<ProvenanceAppendResult>;
39
+ read(notebookPath: string): Promise<ProvenanceEvent[]>;
40
+ verify(notebookPath: string): Promise<ProvenanceVerification>;
41
+ findByIdempotencyKey(notebookPath: string, idempotencyKey: string): Promise<ProvenanceEvent | null>;
42
+ putBlob(notebookPath: string, value: string | Buffer | Uint8Array): Promise<ProvenanceBlobReference>;
43
+ readBlob(notebookPath: string, sha256: string): Promise<Buffer>;
44
+ verifyBlob(notebookPath: string, sha256: string): Promise<ProvenanceBlobVerification>;
45
+ private normalizeNotebookPath;
46
+ private normalizeInput;
47
+ private canonicalClone;
48
+ private hashRequest;
49
+ private requestEnvelope;
50
+ private inputFromEvent;
51
+ private readVerified;
52
+ private validateEventShape;
53
+ private appendLine;
54
+ private ledgerNeedsNewline;
55
+ private syncDirectory;
56
+ private readIfExists;
57
+ private assertBlobBytes;
58
+ private toBuffer;
59
+ private assertSha256;
60
+ private runSerialized;
61
+ private isNodeError;
62
+ }
63
+ export type { ProvenanceActor, ProvenanceAppendResult, ProvenanceBlobReference, ProvenanceBlobVerification, ProvenanceEvent, ProvenanceEventInput, ProvenanceVerification, } from './types';