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,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
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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.16",
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",