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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CanonicalJsonValue } from './canonical-json';
|
|
2
|
+
export interface ProvenanceActor {
|
|
3
|
+
/** Authenticated principal category, for example user, agent, or system. */
|
|
4
|
+
kind: string;
|
|
5
|
+
/** Stable principal identifier within the actor category. */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Optional human-readable client/principal label. */
|
|
8
|
+
name?: string;
|
|
9
|
+
attributes?: Record<string, CanonicalJsonValue>;
|
|
10
|
+
}
|
|
11
|
+
export interface ProvenanceEventInput {
|
|
12
|
+
type: string;
|
|
13
|
+
actor: ProvenanceActor;
|
|
14
|
+
payload?: CanonicalJsonValue;
|
|
15
|
+
runId?: string;
|
|
16
|
+
taskId?: string;
|
|
17
|
+
sourceId?: string;
|
|
18
|
+
executionId?: string;
|
|
19
|
+
inputManifestSha256?: string;
|
|
20
|
+
environmentManifestSha256?: string;
|
|
21
|
+
idempotencyKey?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ProvenanceEvent {
|
|
24
|
+
schemaVersion: 1;
|
|
25
|
+
eventId: string;
|
|
26
|
+
seq: number;
|
|
27
|
+
timestamp: string;
|
|
28
|
+
notebookPath: string;
|
|
29
|
+
type: string;
|
|
30
|
+
actor: ProvenanceActor;
|
|
31
|
+
payload: CanonicalJsonValue;
|
|
32
|
+
runId?: string;
|
|
33
|
+
taskId?: string;
|
|
34
|
+
sourceId?: string;
|
|
35
|
+
executionId?: string;
|
|
36
|
+
inputManifestSha256?: string;
|
|
37
|
+
environmentManifestSha256?: string;
|
|
38
|
+
idempotencyKey?: string;
|
|
39
|
+
idempotencyRequestHash?: string;
|
|
40
|
+
prevHash: string | null;
|
|
41
|
+
eventHash: string;
|
|
42
|
+
}
|
|
43
|
+
export interface ProvenanceAppendResult {
|
|
44
|
+
event: ProvenanceEvent;
|
|
45
|
+
appended: boolean;
|
|
46
|
+
}
|
|
47
|
+
export interface ProvenanceVerification {
|
|
48
|
+
valid: boolean;
|
|
49
|
+
eventCount: number;
|
|
50
|
+
lastSeq: number;
|
|
51
|
+
headHash: string | null;
|
|
52
|
+
error?: string;
|
|
53
|
+
failedLine?: number;
|
|
54
|
+
}
|
|
55
|
+
export interface ProvenanceBlobReference {
|
|
56
|
+
sha256: string;
|
|
57
|
+
sizeBytes: number;
|
|
58
|
+
}
|
|
59
|
+
export interface ProvenanceBlobVerification {
|
|
60
|
+
valid: boolean;
|
|
61
|
+
expectedSha256: string;
|
|
62
|
+
actualSha256?: string;
|
|
63
|
+
sizeBytes?: number;
|
|
64
|
+
error?: string;
|
|
65
|
+
}
|
|
@@ -43,6 +43,12 @@ Object.defineProperty(exports, "fsService", { enumerable: true, get: function ()
|
|
|
43
43
|
const path = __importStar(require("path"));
|
|
44
44
|
const private_tmp_1 = require("../private-tmp");
|
|
45
45
|
const nodeFs = __importStar(require("fs"));
|
|
46
|
+
const sealed_path_1 = require("../fs/sealed-path");
|
|
47
|
+
function sendSealedError(reply, error) {
|
|
48
|
+
if (!(error instanceof sealed_path_1.SealedPathLockedError))
|
|
49
|
+
return null;
|
|
50
|
+
return reply.code(error.statusCode).send((0, sealed_path_1.sealedErrorBody)(error));
|
|
51
|
+
}
|
|
46
52
|
async function fsRoutes(fastify) {
|
|
47
53
|
/**
|
|
48
54
|
* List directory contents
|
|
@@ -176,6 +182,9 @@ async function fsRoutes(fastify) {
|
|
|
176
182
|
return reply.send({ status: 'ok', path: filePath });
|
|
177
183
|
}
|
|
178
184
|
catch (err) {
|
|
185
|
+
const sealed = sendSealedError(reply, err);
|
|
186
|
+
if (sealed)
|
|
187
|
+
return sealed;
|
|
179
188
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
180
189
|
return reply.code(500).send({ detail: message });
|
|
181
190
|
}
|
|
@@ -193,6 +202,9 @@ async function fsRoutes(fastify) {
|
|
|
193
202
|
return reply.send({ status: 'ok', file: info });
|
|
194
203
|
}
|
|
195
204
|
catch (err) {
|
|
205
|
+
const sealed = sendSealedError(reply, err);
|
|
206
|
+
if (sealed)
|
|
207
|
+
return sealed;
|
|
196
208
|
if (err instanceof Error && err.message.includes('exists')) {
|
|
197
209
|
return reply.code(409).send({ detail: err.message });
|
|
198
210
|
}
|
|
@@ -215,6 +227,9 @@ async function fsRoutes(fastify) {
|
|
|
215
227
|
return reply.send({ status: 'ok' });
|
|
216
228
|
}
|
|
217
229
|
catch (err) {
|
|
230
|
+
const sealed = sendSealedError(reply, err);
|
|
231
|
+
if (sealed)
|
|
232
|
+
return sealed;
|
|
218
233
|
if (err instanceof Error && err.message.includes('not found')) {
|
|
219
234
|
return reply.code(404).send({ detail: err.message });
|
|
220
235
|
}
|
|
@@ -237,6 +252,9 @@ async function fsRoutes(fastify) {
|
|
|
237
252
|
return reply.send({ status: 'ok', file: info });
|
|
238
253
|
}
|
|
239
254
|
catch (err) {
|
|
255
|
+
const sealed = sendSealedError(reply, err);
|
|
256
|
+
if (sealed)
|
|
257
|
+
return sealed;
|
|
240
258
|
if (err instanceof Error) {
|
|
241
259
|
if (err.message.includes('not found')) {
|
|
242
260
|
return reply.code(404).send({ detail: err.message });
|
|
@@ -266,6 +284,9 @@ async function fsRoutes(fastify) {
|
|
|
266
284
|
return reply.send({ status: 'ok', file: info });
|
|
267
285
|
}
|
|
268
286
|
catch (err) {
|
|
287
|
+
const sealed = sendSealedError(reply, err);
|
|
288
|
+
if (sealed)
|
|
289
|
+
return sealed;
|
|
269
290
|
if (err instanceof Error) {
|
|
270
291
|
if (err.message.includes('not found')) {
|
|
271
292
|
return reply.code(404).send({ detail: err.message });
|
|
@@ -373,6 +394,9 @@ async function fsRoutes(fastify) {
|
|
|
373
394
|
if (!destPath) {
|
|
374
395
|
return reply.code(400).send({ detail: 'path is required' });
|
|
375
396
|
}
|
|
397
|
+
// Reject sealed destinations before buffering the multipart payload to a
|
|
398
|
+
// temp file. FilesystemService repeats this at the actual write boundary.
|
|
399
|
+
(0, sealed_path_1.assertPathMutable)(fs_service_1.fsService.normalizePath(destPath), { operation: 'upload into' });
|
|
376
400
|
// Save the uploaded file to a temp location first
|
|
377
401
|
const tmpPath = path.join((0, private_tmp_1.privateTmpDir)('uploads'), `upload-${Date.now()}-${path.basename(data.filename || 'file')}`);
|
|
378
402
|
const writeStream = nodeFs.createWriteStream(tmpPath);
|
|
@@ -395,6 +419,9 @@ async function fsRoutes(fastify) {
|
|
|
395
419
|
return reply.send({ status: 'ok', file: info });
|
|
396
420
|
}
|
|
397
421
|
catch (err) {
|
|
422
|
+
const sealed = sendSealedError(reply, err);
|
|
423
|
+
if (sealed)
|
|
424
|
+
return sealed;
|
|
398
425
|
if (err instanceof Error) {
|
|
399
426
|
if (err.message.includes('already exists')) {
|
|
400
427
|
return reply.code(409).send({ detail: err.message });
|
|
@@ -52,6 +52,7 @@ const operation_router_1 = require("../notebook/operation-router");
|
|
|
52
52
|
const kernel_proxy_1 = require("../cluster/kernel-proxy");
|
|
53
53
|
const server_registry_1 = require("../cluster/server-registry");
|
|
54
54
|
const allocation_service_1 = require("../scheduler/allocation-service");
|
|
55
|
+
const sealed_path_1 = require("../fs/sealed-path");
|
|
55
56
|
// Track all WebSocket connections per kernel session for broadcasting
|
|
56
57
|
const sessionWebSockets = new Map();
|
|
57
58
|
// Only send streaming outputs to sockets after they've performed an initial output sync.
|
|
@@ -138,6 +139,12 @@ function sendProvisionError(reply, err) {
|
|
|
138
139
|
reply.code(status).send({ detail: err.message, code: err.code, install_hint: err.installHint });
|
|
139
140
|
return true;
|
|
140
141
|
}
|
|
142
|
+
function sendSealedError(reply, err) {
|
|
143
|
+
if (!(err instanceof sealed_path_1.SealedPathLockedError))
|
|
144
|
+
return false;
|
|
145
|
+
reply.code(err.statusCode).send((0, sealed_path_1.sealedErrorBody)(err));
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
141
148
|
async function persistNotebookKernelMetadata(filePath, kernelName) {
|
|
142
149
|
// Best-effort: persisting the kernel choice into the notebook's metadata is a
|
|
143
150
|
// secondary side-effect. If it fails (e.g. the notebook file doesn't exist yet,
|
|
@@ -484,6 +491,9 @@ async function kernelRoutes(fastify) {
|
|
|
484
491
|
fastify.post('/kernels/start', async (request, reply) => {
|
|
485
492
|
try {
|
|
486
493
|
const { kernel_name = 'python3', cwd, file_path, server_id, client_origin } = request.body;
|
|
494
|
+
if (file_path) {
|
|
495
|
+
(0, sealed_path_1.assertPathMutable)(kernelService.normalizeNotebookPath(file_path), { operation: 'start a kernel for' });
|
|
496
|
+
}
|
|
487
497
|
const localServerId = server_registry_1.serverRegistry.getLocalServerId();
|
|
488
498
|
// Check if we should start on a remote server
|
|
489
499
|
if (server_id && server_id !== localServerId && server_id !== 'local') {
|
|
@@ -518,6 +528,8 @@ async function kernelRoutes(fastify) {
|
|
|
518
528
|
return reply.send({ session_id: sessionId, kernel_name, server_id: localServerId, mtime: notebookMtime });
|
|
519
529
|
}
|
|
520
530
|
catch (err) {
|
|
531
|
+
if (sendSealedError(reply, err))
|
|
532
|
+
return reply;
|
|
521
533
|
if (sendProvisionError(reply, err))
|
|
522
534
|
return reply;
|
|
523
535
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
@@ -553,6 +565,7 @@ async function kernelRoutes(fastify) {
|
|
|
553
565
|
return reply.code(400).send({ detail: 'file_path is required' });
|
|
554
566
|
}
|
|
555
567
|
const normalizedFilePath = kernelService.normalizeNotebookPath(file_path);
|
|
568
|
+
(0, sealed_path_1.assertPathMutable)(normalizedFilePath, { operation: 'start a kernel for' });
|
|
556
569
|
let effectiveKernelName = kernel_name;
|
|
557
570
|
let effectiveServerId = server_id;
|
|
558
571
|
const localServerId = server_registry_1.serverRegistry.getLocalServerId();
|
|
@@ -635,6 +648,8 @@ async function kernelRoutes(fastify) {
|
|
|
635
648
|
});
|
|
636
649
|
}
|
|
637
650
|
catch (err) {
|
|
651
|
+
if (sendSealedError(reply, err))
|
|
652
|
+
return reply;
|
|
638
653
|
if (sendProvisionError(reply, err))
|
|
639
654
|
return reply;
|
|
640
655
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Notebook API Routes
|
|
4
4
|
*/
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
5
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
9
|
exports.headlessHandler = exports.operationRouter = exports.fsService = void 0;
|
|
7
10
|
exports.default = notebookRoutes;
|
|
@@ -11,11 +14,19 @@ const operation_router_1 = require("../notebook/operation-router");
|
|
|
11
14
|
Object.defineProperty(exports, "operationRouter", { enumerable: true, get: function () { return operation_router_1.operationRouter; } });
|
|
12
15
|
const headless_handler_1 = require("../notebook/headless-handler");
|
|
13
16
|
const kernel_1 = require("./kernel");
|
|
17
|
+
const sealed_path_1 = require("../fs/sealed-path");
|
|
18
|
+
const replay_seal_1 = __importDefault(require("./replay-seal"));
|
|
14
19
|
// Initialize headless handler with kernel service for cell execution
|
|
15
20
|
const headlessHandler = new headless_handler_1.HeadlessOperationHandler(fs_service_1.fsService, operation_router_1.operationRouter, kernel_1.kernelService);
|
|
16
21
|
exports.headlessHandler = headlessHandler;
|
|
17
22
|
operation_router_1.operationRouter.setHeadlessHandler(headlessHandler);
|
|
23
|
+
function sendSealedError(reply, error) {
|
|
24
|
+
if (!(error instanceof sealed_path_1.SealedPathLockedError))
|
|
25
|
+
return null;
|
|
26
|
+
return reply.code(error.statusCode).send((0, sealed_path_1.sealedErrorBody)(error));
|
|
27
|
+
}
|
|
18
28
|
async function notebookRoutes(fastify) {
|
|
29
|
+
await fastify.register(replay_seal_1.default);
|
|
19
30
|
/**
|
|
20
31
|
* Get cell metadata schema
|
|
21
32
|
*/
|
|
@@ -55,6 +66,16 @@ async function notebookRoutes(fastify) {
|
|
|
55
66
|
return reply.code(400).send({ detail: 'path query parameter is required' });
|
|
56
67
|
}
|
|
57
68
|
const normalizedPath = fs_service_1.fsService.normalizePath(filePath);
|
|
69
|
+
const sealInfo = (0, sealed_path_1.classifySealedPath)(normalizedPath);
|
|
70
|
+
const sealHint = request.query.seal;
|
|
71
|
+
if (sealHint && (!sealInfo.sealed || sealInfo.sealId !== sealHint)) {
|
|
72
|
+
return reply.code(409).send({
|
|
73
|
+
code: 'seal_mismatch',
|
|
74
|
+
detail: sealInfo.sealed
|
|
75
|
+
? `Seal hint ${sealHint} does not match canonical path seal ${sealInfo.sealId}`
|
|
76
|
+
: `Seal hint ${sealHint} does not identify the requested notebook path`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
58
79
|
const result = await fs_service_1.fsService.getNotebookCellsWithKernel(filePath);
|
|
59
80
|
return reply.send({
|
|
60
81
|
path: normalizedPath,
|
|
@@ -62,6 +83,9 @@ async function notebookRoutes(fastify) {
|
|
|
62
83
|
metadata: result.metadata,
|
|
63
84
|
kernelspec: result.kernelspec,
|
|
64
85
|
mtime: result.mtime,
|
|
86
|
+
access: sealInfo.sealed
|
|
87
|
+
? { read_only: true, reason: 'sealed', seal_id: sealInfo.sealId }
|
|
88
|
+
: { read_only: false },
|
|
65
89
|
});
|
|
66
90
|
}
|
|
67
91
|
catch (err) {
|
|
@@ -102,6 +126,9 @@ async function notebookRoutes(fastify) {
|
|
|
102
126
|
return reply.send({ status: 'ok', path: filePath, mtime: result.mtime });
|
|
103
127
|
}
|
|
104
128
|
catch (err) {
|
|
129
|
+
const sealed = sendSealedError(reply, err);
|
|
130
|
+
if (sealed)
|
|
131
|
+
return sealed;
|
|
105
132
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
106
133
|
return reply.code(500).send({ detail: message });
|
|
107
134
|
}
|
|
@@ -119,6 +146,9 @@ async function notebookRoutes(fastify) {
|
|
|
119
146
|
return reply.send({ notebook_path: notebookPath, history });
|
|
120
147
|
}
|
|
121
148
|
catch (err) {
|
|
149
|
+
const sealed = sendSealedError(reply, err);
|
|
150
|
+
if (sealed)
|
|
151
|
+
return sealed;
|
|
122
152
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
123
153
|
return reply.code(500).send({ detail: message });
|
|
124
154
|
}
|
|
@@ -136,6 +166,9 @@ async function notebookRoutes(fastify) {
|
|
|
136
166
|
return reply.send({ status: 'ok', notebook_path });
|
|
137
167
|
}
|
|
138
168
|
catch (err) {
|
|
169
|
+
const sealed = sendSealedError(reply, err);
|
|
170
|
+
if (sealed)
|
|
171
|
+
return sealed;
|
|
139
172
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
140
173
|
return reply.code(500).send({ detail: message });
|
|
141
174
|
}
|
|
@@ -170,6 +203,9 @@ async function notebookRoutes(fastify) {
|
|
|
170
203
|
return reply.send({ status: 'ok', notebook_path });
|
|
171
204
|
}
|
|
172
205
|
catch (err) {
|
|
206
|
+
const sealed = sendSealedError(reply, err);
|
|
207
|
+
if (sealed)
|
|
208
|
+
return sealed;
|
|
173
209
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
174
210
|
return reply.code(500).send({ detail: message });
|
|
175
211
|
}
|
|
@@ -195,6 +231,9 @@ async function notebookRoutes(fastify) {
|
|
|
195
231
|
});
|
|
196
232
|
}
|
|
197
233
|
catch (err) {
|
|
234
|
+
const sealed = sendSealedError(reply, err);
|
|
235
|
+
if (sealed)
|
|
236
|
+
return sealed;
|
|
198
237
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
199
238
|
return reply.code(500).send({ detail: message });
|
|
200
239
|
}
|
|
@@ -272,9 +311,15 @@ async function notebookRoutes(fastify) {
|
|
|
272
311
|
return reply.code(400).send({ detail: 'Operation with type is required' });
|
|
273
312
|
}
|
|
274
313
|
const result = await operation_router_1.operationRouter.applyOperation(operation);
|
|
314
|
+
if (result.code === 'sealed_read_only') {
|
|
315
|
+
return reply.code(403).send(result);
|
|
316
|
+
}
|
|
275
317
|
return reply.send(result);
|
|
276
318
|
}
|
|
277
319
|
catch (err) {
|
|
320
|
+
const sealed = sendSealedError(reply, err);
|
|
321
|
+
if (sealed)
|
|
322
|
+
return sealed;
|
|
278
323
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
279
324
|
return reply.send({ success: false, error: message });
|
|
280
325
|
}
|
|
@@ -297,6 +342,9 @@ async function notebookRoutes(fastify) {
|
|
|
297
342
|
});
|
|
298
343
|
}
|
|
299
344
|
catch (err) {
|
|
345
|
+
const sealed = sendSealedError(reply, err);
|
|
346
|
+
if (sealed)
|
|
347
|
+
return sealed;
|
|
300
348
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
301
349
|
return reply.code(500).send({ detail: message });
|
|
302
350
|
}
|
|
@@ -341,6 +389,9 @@ async function notebookRoutes(fastify) {
|
|
|
341
389
|
});
|
|
342
390
|
}
|
|
343
391
|
catch (err) {
|
|
392
|
+
const sealed = sendSealedError(reply, err);
|
|
393
|
+
if (sealed)
|
|
394
|
+
return sealed;
|
|
344
395
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
345
396
|
return reply.code(500).send({ detail: message });
|
|
346
397
|
}
|
|
@@ -368,6 +419,9 @@ async function notebookRoutes(fastify) {
|
|
|
368
419
|
return reply.send({ status: 'ok', notebook_path: filePath, mtime: updateResult.mtime });
|
|
369
420
|
}
|
|
370
421
|
catch (err) {
|
|
422
|
+
const sealed = sendSealedError(reply, err);
|
|
423
|
+
if (sealed)
|
|
424
|
+
return sealed;
|
|
371
425
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
372
426
|
return reply.code(500).send({ detail: message });
|
|
373
427
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import { type ReplaySealRequest, type ReplaySealStatusResponse } from '../provenance/replay-seal-service';
|
|
3
|
+
export interface ReplaySealRouteService {
|
|
4
|
+
submit(request: ReplaySealRequest, idempotencyKey: string): Promise<ReplaySealStatusResponse>;
|
|
5
|
+
getStatus(sealId: string): Promise<ReplaySealStatusResponse | null>;
|
|
6
|
+
}
|
|
7
|
+
/** Build the protected KOSMOS replay/seal routes with an injectable service. */
|
|
8
|
+
export declare function createReplaySealRoutes(service: ReplaySealRouteService): (fastify: FastifyInstance) => Promise<void>;
|
|
9
|
+
export default function replaySealRoutes(fastify: FastifyInstance): Promise<void>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createReplaySealRoutes = createReplaySealRoutes;
|
|
4
|
+
exports.default = replaySealRoutes;
|
|
5
|
+
const fs_service_1 = require("../fs/fs-service");
|
|
6
|
+
const replay_seal_service_1 = require("../provenance/replay-seal-service");
|
|
7
|
+
const provenance_store_1 = require("../provenance/provenance-store");
|
|
8
|
+
const kernel_1 = require("./kernel");
|
|
9
|
+
let productionService;
|
|
10
|
+
function getProductionService() {
|
|
11
|
+
productionService ??= new replay_seal_service_1.ReplaySealService({
|
|
12
|
+
rootDirectory: fs_service_1.fsService.getRootDirectory(),
|
|
13
|
+
kernelService: kernel_1.kernelService,
|
|
14
|
+
provenanceStore: new provenance_store_1.ProvenanceStore(),
|
|
15
|
+
});
|
|
16
|
+
return productionService;
|
|
17
|
+
}
|
|
18
|
+
function errorResponse(reply, error) {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
const code = error && typeof error === 'object' ? error.code : undefined;
|
|
21
|
+
if (error instanceof replay_seal_service_1.ReplaySealConflictError || code === 'IDEMPOTENCY_CONFLICT') {
|
|
22
|
+
return reply.code(409).send({ detail: message, code: 'idempotency_conflict' });
|
|
23
|
+
}
|
|
24
|
+
if (error instanceof replay_seal_service_1.ReplaySealValidationError || code === 'REPLAY_SEAL_VALIDATION_ERROR') {
|
|
25
|
+
return reply.code(400).send({ detail: message, code: 'invalid_replay_seal_request' });
|
|
26
|
+
}
|
|
27
|
+
return reply.code(500).send({ detail: message });
|
|
28
|
+
}
|
|
29
|
+
/** Build the protected KOSMOS replay/seal routes with an injectable service. */
|
|
30
|
+
function createReplaySealRoutes(service) {
|
|
31
|
+
return async function replaySealRoutes(fastify) {
|
|
32
|
+
fastify.post('/notebook/replay-seal', async (request, reply) => {
|
|
33
|
+
const header = request.headers['idempotency-key'];
|
|
34
|
+
const idempotencyKey = Array.isArray(header) ? header[0] : header;
|
|
35
|
+
if (typeof idempotencyKey !== 'string' || idempotencyKey.length === 0) {
|
|
36
|
+
return reply.code(400).send({ detail: 'Idempotency-Key header is required' });
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const status = await service.submit(request.body, idempotencyKey);
|
|
40
|
+
return reply.code(status.status === 'pending' || status.status === 'running' ? 202 : 200)
|
|
41
|
+
.send(status);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return errorResponse(reply, error);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
fastify.get('/notebook/replay-seal/:sealId', async (request, reply) => {
|
|
48
|
+
try {
|
|
49
|
+
const sealId = request.params.sealId;
|
|
50
|
+
if (typeof sealId !== 'string' || sealId.length === 0) {
|
|
51
|
+
return reply.code(400).send({ detail: 'sealId is required' });
|
|
52
|
+
}
|
|
53
|
+
const status = await service.getStatus(sealId);
|
|
54
|
+
if (!status)
|
|
55
|
+
return reply.code(404).send({ detail: `Replay seal not found: ${sealId}` });
|
|
56
|
+
return reply.send(status);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return errorResponse(reply, error);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function replaySealRoutes(fastify) {
|
|
65
|
+
await createReplaySealRoutes(getProductionService())(fastify);
|
|
66
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function isLoopbackBindHost(host: string): boolean;
|
|
2
|
+
export declare function assertSafeNoAuthBind(host: string): void;
|
|
3
|
+
/** The host the operator explicitly chose (CLI beats env), or undefined. */
|
|
4
|
+
export declare function explicitBindHost(argv: readonly string[], environment: Readonly<Record<string, string | undefined>>): string | undefined;
|
|
5
|
+
export declare function resolveBindHost(argv: readonly string[], environment: Readonly<Record<string, string | undefined>>): string;
|
|
6
|
+
/**
|
|
7
|
+
* Bind host for explicit no-auth mode. An unauthenticated server must never
|
|
8
|
+
* listen on the network ACCIDENTALLY, but the quickstart
|
|
9
|
+
* (`NEBULA_NO_AUTH=true npx nebula-notebook`) must still boot: no chosen host
|
|
10
|
+
* defaults to loopback, and only an explicitly non-loopback choice is refused.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveNoAuthBindHost(argv: readonly string[], environment: Readonly<Record<string, string | undefined>>): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isLoopbackBindHost = isLoopbackBindHost;
|
|
4
|
+
exports.assertSafeNoAuthBind = assertSafeNoAuthBind;
|
|
5
|
+
exports.explicitBindHost = explicitBindHost;
|
|
6
|
+
exports.resolveBindHost = resolveBindHost;
|
|
7
|
+
exports.resolveNoAuthBindHost = resolveNoAuthBindHost;
|
|
8
|
+
const net_1 = require("net");
|
|
9
|
+
const HOSTNAME_RE = /^(?:localhost|[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?)$/;
|
|
10
|
+
function isLoopbackBindHost(host) {
|
|
11
|
+
if (host.toLowerCase() === 'localhost')
|
|
12
|
+
return true;
|
|
13
|
+
const version = (0, net_1.isIP)(host);
|
|
14
|
+
if (version === 4)
|
|
15
|
+
return host.split('.')[0] === '127';
|
|
16
|
+
return version === 6 && host === '::1';
|
|
17
|
+
}
|
|
18
|
+
function assertSafeNoAuthBind(host) {
|
|
19
|
+
if (!isLoopbackBindHost(host)) {
|
|
20
|
+
throw new Error('Nebula no-auth mode requires a literal loopback bind host');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** The host the operator explicitly chose (CLI beats env), or undefined. */
|
|
24
|
+
function explicitBindHost(argv, environment) {
|
|
25
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
26
|
+
const value = argv[index];
|
|
27
|
+
if (value === '--host')
|
|
28
|
+
return argv[index + 1];
|
|
29
|
+
if (value.startsWith('--host='))
|
|
30
|
+
return value.slice('--host='.length);
|
|
31
|
+
}
|
|
32
|
+
return environment.NEBULA_BIND_HOST;
|
|
33
|
+
}
|
|
34
|
+
function resolveBindHost(argv, environment) {
|
|
35
|
+
const candidate = (explicitBindHost(argv, environment) ?? '0.0.0.0').trim();
|
|
36
|
+
if (candidate.length === 0
|
|
37
|
+
|| candidate.length > 253
|
|
38
|
+
|| /[\u0000-\u0020\u007f/\\?#@]/.test(candidate)
|
|
39
|
+
|| ((0, net_1.isIP)(candidate) === 0 && !HOSTNAME_RE.test(candidate))) {
|
|
40
|
+
throw new Error(`Invalid Nebula bind host: ${JSON.stringify(candidate)}`);
|
|
41
|
+
}
|
|
42
|
+
return candidate;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Bind host for explicit no-auth mode. An unauthenticated server must never
|
|
46
|
+
* listen on the network ACCIDENTALLY, but the quickstart
|
|
47
|
+
* (`NEBULA_NO_AUTH=true npx nebula-notebook`) must still boot: no chosen host
|
|
48
|
+
* defaults to loopback, and only an explicitly non-loopback choice is refused.
|
|
49
|
+
*/
|
|
50
|
+
function resolveNoAuthBindHost(argv, environment) {
|
|
51
|
+
if (explicitBindHost(argv, environment) === undefined)
|
|
52
|
+
return '127.0.0.1';
|
|
53
|
+
const host = resolveBindHost(argv, environment);
|
|
54
|
+
assertSafeNoAuthBind(host);
|
|
55
|
+
return host;
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isTrustedBrowserOrigin(origin: string, configuredOrigins?: readonly string[]): boolean;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isTrustedBrowserOrigin = isTrustedBrowserOrigin;
|
|
4
|
+
const net_1 = require("net");
|
|
5
|
+
function isLiteralLoopback(hostname) {
|
|
6
|
+
const normalized = hostname.startsWith('[') && hostname.endsWith(']')
|
|
7
|
+
? hostname.slice(1, -1)
|
|
8
|
+
: hostname;
|
|
9
|
+
const version = (0, net_1.isIP)(normalized);
|
|
10
|
+
if (version === 4)
|
|
11
|
+
return normalized.split('.')[0] === '127';
|
|
12
|
+
if (version === 6)
|
|
13
|
+
return normalized === '::1';
|
|
14
|
+
return normalized.toLowerCase() === 'localhost';
|
|
15
|
+
}
|
|
16
|
+
function isTrustedBrowserOrigin(origin, configuredOrigins = []) {
|
|
17
|
+
if (configuredOrigins.includes(origin))
|
|
18
|
+
return true;
|
|
19
|
+
try {
|
|
20
|
+
const parsed = new URL(origin);
|
|
21
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
22
|
+
return false;
|
|
23
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash)
|
|
24
|
+
return false;
|
|
25
|
+
if (parsed.pathname !== '/')
|
|
26
|
+
return false;
|
|
27
|
+
return isLiteralLoopback(parsed.hostname);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nebula-notebook",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
4
4
|
"description": "AI-native notebook computing environment — real Jupyter kernels, real filesystem, built to be driven by agents (Claude Code / Codex) via MCP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|