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.
- package/dist/assets/{errorwidget-DfcjRDKM.js → errorwidget-t1RLHQYw.js} +1 -1
- package/dist/assets/{index-CyyuPG9B.js → index-BMuaZuus.js} +1 -1
- package/dist/assets/index-Bvhz0ltE.css +32 -0
- package/dist/assets/{index-LZ-zzybU.js → index-Cq1BG1_T.js} +205 -200
- package/dist/assets/{index-DrhpYYGt.js → index-DTGJj27s.js} +1 -1
- package/dist/assets/{index-COAUb-T8.js → index-DzeeB-u3.js} +1 -1
- package/dist/assets/{services-shim-CCJm9nl7.js → services-shim-Dg9Zi6xv.js} +1 -1
- package/dist/index.html +2 -2
- package/node-server/dist/auth/auth-middleware.js +3 -1
- package/node-server/dist/fs/fs-service.js +104 -8
- package/node-server/dist/fs/index.d.ts +1 -0
- package/node-server/dist/fs/index.js +1 -0
- package/node-server/dist/fs/sealed-path.d.ts +41 -0
- package/node-server/dist/fs/sealed-path.js +228 -0
- package/node-server/dist/fs/types.d.ts +4 -2
- package/node-server/dist/fs/types.js +2 -2
- package/node-server/dist/index.js +22 -13
- package/node-server/dist/kernel/kernel-service.d.ts +2 -2
- package/node-server/dist/kernel/kernel-service.js +15 -5
- package/node-server/dist/kernel/types.d.ts +16 -0
- package/node-server/dist/notebook/operation-router.js +13 -2
- package/node-server/dist/provenance/canonical-json.d.ts +36 -0
- package/node-server/dist/provenance/canonical-json.js +218 -0
- package/node-server/dist/provenance/provenance-store.d.ts +63 -0
- package/node-server/dist/provenance/provenance-store.js +598 -0
- package/node-server/dist/provenance/replay-seal-service.d.ts +223 -0
- package/node-server/dist/provenance/replay-seal-service.js +1702 -0
- package/node-server/dist/provenance/types.d.ts +65 -0
- package/node-server/dist/provenance/types.js +2 -0
- package/node-server/dist/routes/fs.js +27 -0
- package/node-server/dist/routes/kernel.js +15 -0
- package/node-server/dist/routes/notebook.js +54 -0
- package/node-server/dist/routes/replay-seal.d.ts +9 -0
- package/node-server/dist/routes/replay-seal.js +66 -0
- package/node-server/dist/server/bind-host.d.ts +12 -0
- package/node-server/dist/server/bind-host.js +56 -0
- package/node-server/dist/server/cors-origin.d.ts +1 -0
- package/node-server/dist/server/cors-origin.js +32 -0
- package/package.json +1 -1
- 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:
|
|
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
|
|
@@ -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 = !
|
|
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';
|