nebula-notebook 0.1.0
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/README.md +222 -0
- package/bin/nebula-notebook.js +33 -0
- package/dist/assets/index-C1h_sArD.css +32 -0
- package/dist/assets/index-CDSTBon8.js +658 -0
- package/dist/favicon.svg +11 -0
- package/dist/index.html +73 -0
- package/node-server/dist/app.d.ts +5 -0
- package/node-server/dist/app.js +38 -0
- package/node-server/dist/auth/auth-middleware.d.ts +24 -0
- package/node-server/dist/auth/auth-middleware.js +276 -0
- package/node-server/dist/auth/auth-service.d.ts +84 -0
- package/node-server/dist/auth/auth-service.js +265 -0
- package/node-server/dist/auth/index.d.ts +3 -0
- package/node-server/dist/auth/index.js +8 -0
- package/node-server/dist/cluster/client-registration.d.ts +43 -0
- package/node-server/dist/cluster/client-registration.js +217 -0
- package/node-server/dist/cluster/cluster-secret.d.ts +11 -0
- package/node-server/dist/cluster/cluster-secret.js +90 -0
- package/node-server/dist/cluster/kernel-proxy.d.ts +100 -0
- package/node-server/dist/cluster/kernel-proxy.js +361 -0
- package/node-server/dist/cluster/server-registry.d.ts +109 -0
- package/node-server/dist/cluster/server-registry.js +217 -0
- package/node-server/dist/config/output-limits.d.ts +7 -0
- package/node-server/dist/config/output-limits.js +10 -0
- package/node-server/dist/discovery/discovery-service.d.ts +198 -0
- package/node-server/dist/discovery/discovery-service.js +811 -0
- package/node-server/dist/discovery/index.d.ts +5 -0
- package/node-server/dist/discovery/index.js +21 -0
- package/node-server/dist/discovery/types.d.ts +48 -0
- package/node-server/dist/discovery/types.js +24 -0
- package/node-server/dist/fs/fs-service.d.ts +218 -0
- package/node-server/dist/fs/fs-service.js +1422 -0
- package/node-server/dist/fs/index.d.ts +5 -0
- package/node-server/dist/fs/index.js +21 -0
- package/node-server/dist/fs/types.d.ts +132 -0
- package/node-server/dist/fs/types.js +5 -0
- package/node-server/dist/index.d.ts +13 -0
- package/node-server/dist/index.js +556 -0
- package/node-server/dist/kernel/default-kernel.d.ts +5 -0
- package/node-server/dist/kernel/default-kernel.js +138 -0
- package/node-server/dist/kernel/index.d.ts +7 -0
- package/node-server/dist/kernel/index.js +23 -0
- package/node-server/dist/kernel/kernel-service.d.ts +290 -0
- package/node-server/dist/kernel/kernel-service.js +1714 -0
- package/node-server/dist/kernel/kernelspec.d.ts +29 -0
- package/node-server/dist/kernel/kernelspec.js +210 -0
- package/node-server/dist/kernel/session-store.d.ts +87 -0
- package/node-server/dist/kernel/session-store.js +303 -0
- package/node-server/dist/kernel/types.d.ts +143 -0
- package/node-server/dist/kernel/types.js +17 -0
- package/node-server/dist/llm/index.d.ts +5 -0
- package/node-server/dist/llm/index.js +21 -0
- package/node-server/dist/llm/llm-service.d.ts +77 -0
- package/node-server/dist/llm/llm-service.js +454 -0
- package/node-server/dist/llm/types.d.ts +40 -0
- package/node-server/dist/llm/types.js +15 -0
- package/node-server/dist/notebook/cell-metadata.d.ts +27 -0
- package/node-server/dist/notebook/cell-metadata.js +76 -0
- package/node-server/dist/notebook/headless-handler.d.ts +127 -0
- package/node-server/dist/notebook/headless-handler.js +1530 -0
- package/node-server/dist/notebook/notebook-websocket.d.ts +12 -0
- package/node-server/dist/notebook/notebook-websocket.js +103 -0
- package/node-server/dist/notebook/operation-router.d.ts +115 -0
- package/node-server/dist/notebook/operation-router.js +641 -0
- package/node-server/dist/notebook/undoRedoManager.d.ts +194 -0
- package/node-server/dist/notebook/undoRedoManager.js +558 -0
- package/node-server/dist/output/display-data.d.ts +14 -0
- package/node-server/dist/output/display-data.js +134 -0
- package/node-server/dist/resources/resource-service.d.ts +69 -0
- package/node-server/dist/resources/resource-service.js +363 -0
- package/node-server/dist/routes/auth.d.ts +5 -0
- package/node-server/dist/routes/auth.js +61 -0
- package/node-server/dist/routes/cluster.d.ts +7 -0
- package/node-server/dist/routes/cluster.js +94 -0
- package/node-server/dist/routes/fs.d.ts +7 -0
- package/node-server/dist/routes/fs.js +392 -0
- package/node-server/dist/routes/kernel.d.ts +13 -0
- package/node-server/dist/routes/kernel.js +637 -0
- package/node-server/dist/routes/llm.d.ts +8 -0
- package/node-server/dist/routes/llm.js +105 -0
- package/node-server/dist/routes/notebook.d.ts +10 -0
- package/node-server/dist/routes/notebook.js +335 -0
- package/node-server/dist/routes/python.d.ts +8 -0
- package/node-server/dist/routes/python.js +187 -0
- package/node-server/dist/routes/resources.d.ts +7 -0
- package/node-server/dist/routes/resources.js +77 -0
- package/node-server/dist/scripts/show-auth-qr.d.ts +1 -0
- package/node-server/dist/scripts/show-auth-qr.js +81 -0
- package/node-server/dist/terminal/pty-manager.d.ts +100 -0
- package/node-server/dist/terminal/pty-manager.js +246 -0
- package/node-server/dist/terminal/server.d.ts +19 -0
- package/node-server/dist/terminal/server.js +254 -0
- package/node-server/dist/terminal/types.d.ts +50 -0
- package/node-server/dist/terminal/types.js +9 -0
- package/node-server/package.json +45 -0
- package/package.json +99 -0
- package/scripts/postinstall.cjs +26 -0
package/dist/favicon.svg
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="nebula" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" style="stop-color:#8b5cf6"/>
|
|
5
|
+
<stop offset="50%" style="stop-color:#6366f1"/>
|
|
6
|
+
<stop offset="100%" style="stop-color:#3b82f6"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
<rect width="32" height="32" rx="6" fill="url(#nebula)"/>
|
|
10
|
+
<path d="M8 10h16M8 16h12M8 22h14" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
|
|
11
|
+
</svg>
|
package/dist/index.html
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" id="favicon" />
|
|
7
|
+
<title>Nebula Notebook</title>
|
|
8
|
+
<style>
|
|
9
|
+
/* Custom scrollbar for a cleaner look */
|
|
10
|
+
::-webkit-scrollbar {
|
|
11
|
+
width: 8px;
|
|
12
|
+
height: 8px;
|
|
13
|
+
}
|
|
14
|
+
::-webkit-scrollbar-track {
|
|
15
|
+
background: transparent;
|
|
16
|
+
}
|
|
17
|
+
::-webkit-scrollbar-thumb {
|
|
18
|
+
background: #cbd5e1;
|
|
19
|
+
border-radius: 4px;
|
|
20
|
+
}
|
|
21
|
+
::-webkit-scrollbar-thumb:hover {
|
|
22
|
+
background: #94a3b8;
|
|
23
|
+
}
|
|
24
|
+
.cm-editor { font-family: 'Fira Code', monospace; }
|
|
25
|
+
|
|
26
|
+
/* Pandas Dataframe Styling */
|
|
27
|
+
.dataframe {
|
|
28
|
+
font-size: 0.85rem;
|
|
29
|
+
border-collapse: collapse;
|
|
30
|
+
border: 1px solid #e2e8f0;
|
|
31
|
+
width: 100%;
|
|
32
|
+
max-width: 100%;
|
|
33
|
+
overflow-x: auto;
|
|
34
|
+
display: block;
|
|
35
|
+
}
|
|
36
|
+
.dataframe th {
|
|
37
|
+
background-color: #f8fafc;
|
|
38
|
+
font-weight: 600;
|
|
39
|
+
text-align: left;
|
|
40
|
+
padding: 0.5rem;
|
|
41
|
+
border-bottom: 2px solid #e2e8f0;
|
|
42
|
+
color: #475569;
|
|
43
|
+
}
|
|
44
|
+
.dataframe td {
|
|
45
|
+
padding: 0.5rem;
|
|
46
|
+
border-bottom: 1px solid #e2e8f0;
|
|
47
|
+
color: #334155;
|
|
48
|
+
}
|
|
49
|
+
.dataframe tr:nth-child(even) {
|
|
50
|
+
background-color: #fcfcfc;
|
|
51
|
+
}
|
|
52
|
+
.dataframe tr:hover {
|
|
53
|
+
background-color: #f1f5f9;
|
|
54
|
+
}
|
|
55
|
+
</style>
|
|
56
|
+
|
|
57
|
+
<script type="importmap">
|
|
58
|
+
{
|
|
59
|
+
"imports": {
|
|
60
|
+
"lucide-react": "https://esm.sh/lucide-react@^0.561.0",
|
|
61
|
+
"react/": "https://esm.sh/react@^19.2.3/",
|
|
62
|
+
"react": "https://esm.sh/react@^19.2.3",
|
|
63
|
+
"react-dom/": "https://esm.sh/react-dom@^19.2.3/"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
67
|
+
<script type="module" crossorigin src="/assets/index-CDSTBon8.js"></script>
|
|
68
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C1h_sArD.css">
|
|
69
|
+
</head>
|
|
70
|
+
<body class="bg-slate-50 text-slate-900 antialiased">
|
|
71
|
+
<div id="root"></div>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Fastify App Setup - Shared middleware and configuration
|
|
4
|
+
*/
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createApp = createApp;
|
|
10
|
+
const fastify_1 = __importDefault(require("fastify"));
|
|
11
|
+
function createApp() {
|
|
12
|
+
const bodyLimit = process.env.NEBULA_BODY_LIMIT ||
|
|
13
|
+
process.env.NEBULA_MAX_BODY_SIZE ||
|
|
14
|
+
'200mb';
|
|
15
|
+
const app = (0, fastify_1.default)({
|
|
16
|
+
bodyLimit: parseBodyLimit(bodyLimit),
|
|
17
|
+
});
|
|
18
|
+
return app;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Parse a human-readable body limit string (e.g., '200mb', '1gb') to bytes.
|
|
22
|
+
*/
|
|
23
|
+
function parseBodyLimit(limit) {
|
|
24
|
+
const match = limit.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb|tb)?$/i);
|
|
25
|
+
if (!match) {
|
|
26
|
+
return 200 * 1024 * 1024; // default 200mb
|
|
27
|
+
}
|
|
28
|
+
const value = parseFloat(match[1]);
|
|
29
|
+
const unit = (match[2] || 'b').toLowerCase();
|
|
30
|
+
const multipliers = {
|
|
31
|
+
b: 1,
|
|
32
|
+
kb: 1024,
|
|
33
|
+
mb: 1024 * 1024,
|
|
34
|
+
gb: 1024 * 1024 * 1024,
|
|
35
|
+
tb: 1024 * 1024 * 1024 * 1024,
|
|
36
|
+
};
|
|
37
|
+
return Math.floor(value * (multipliers[unit] || 1));
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Middleware - Protect routes and WebSocket connections
|
|
3
|
+
*
|
|
4
|
+
* Public routes (no auth required):
|
|
5
|
+
* - /api/health
|
|
6
|
+
* - /api/ready
|
|
7
|
+
* - /api/auth/*
|
|
8
|
+
*/
|
|
9
|
+
import { FastifyRequest, FastifyReply } from 'fastify';
|
|
10
|
+
import { IncomingMessage } from 'http';
|
|
11
|
+
/**
|
|
12
|
+
* Persist a session token to disk so MCP servers and CLI tools
|
|
13
|
+
* can piggyback on the browser's authentication.
|
|
14
|
+
*/
|
|
15
|
+
export declare function persistSessionToken(token: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Fastify onRequest hook for authentication
|
|
18
|
+
*/
|
|
19
|
+
export declare function authMiddleware(request: FastifyRequest, reply: FastifyReply): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* WebSocket authentication middleware
|
|
22
|
+
* Returns true if the connection is authenticated
|
|
23
|
+
*/
|
|
24
|
+
export declare function authWebSocketMiddleware(request: IncomingMessage): boolean;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Auth Middleware - Protect routes and WebSocket connections
|
|
4
|
+
*
|
|
5
|
+
* Public routes (no auth required):
|
|
6
|
+
* - /api/health
|
|
7
|
+
* - /api/ready
|
|
8
|
+
* - /api/auth/*
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.persistSessionToken = persistSessionToken;
|
|
45
|
+
exports.authMiddleware = authMiddleware;
|
|
46
|
+
exports.authWebSocketMiddleware = authWebSocketMiddleware;
|
|
47
|
+
const url_1 = require("url");
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const os = __importStar(require("os"));
|
|
51
|
+
const auth_service_1 = require("./auth-service");
|
|
52
|
+
const cluster_secret_1 = require("../cluster/cluster-secret");
|
|
53
|
+
const SESSION_TOKEN_PATH = path.join(os.homedir(), '.nebula', 'session-token');
|
|
54
|
+
/**
|
|
55
|
+
* Persist a session token to disk so MCP servers and CLI tools
|
|
56
|
+
* can piggyback on the browser's authentication.
|
|
57
|
+
*/
|
|
58
|
+
function persistSessionToken(token) {
|
|
59
|
+
try {
|
|
60
|
+
const dir = path.dirname(SESSION_TOKEN_PATH);
|
|
61
|
+
if (!fs.existsSync(dir))
|
|
62
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
63
|
+
fs.writeFileSync(SESSION_TOKEN_PATH, token, { mode: 0o600 });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Non-critical — MCP just won't be able to auto-auth
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Read the persisted session token (written when browser authenticates).
|
|
71
|
+
*/
|
|
72
|
+
function readSessionToken() {
|
|
73
|
+
try {
|
|
74
|
+
if (!fs.existsSync(SESSION_TOKEN_PATH))
|
|
75
|
+
return undefined;
|
|
76
|
+
const token = fs.readFileSync(SESSION_TOKEN_PATH, 'utf-8').trim();
|
|
77
|
+
return token || undefined;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Routes that don't require authentication
|
|
84
|
+
const PUBLIC_ROUTES = [
|
|
85
|
+
'/api/health',
|
|
86
|
+
'/api/ready',
|
|
87
|
+
'/api/auth/status',
|
|
88
|
+
'/api/auth/verify',
|
|
89
|
+
];
|
|
90
|
+
const CLUSTER_SECRET_HEADER = 'x-nebula-cluster-secret';
|
|
91
|
+
function isClientMode() {
|
|
92
|
+
const explicit = process.env.NEBULA_CLIENT_MODE;
|
|
93
|
+
if (explicit !== undefined) {
|
|
94
|
+
return explicit === 'true';
|
|
95
|
+
}
|
|
96
|
+
return !!process.env.NEBULA_MAIN_SERVER;
|
|
97
|
+
}
|
|
98
|
+
function getClusterSecret() {
|
|
99
|
+
return process.env.NEBULA_CLUSTER_SECRET || (0, cluster_secret_1.readClusterSecret)() || null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Extract auth token from request
|
|
103
|
+
* Checks Authorization header (Bearer token) and query parameter
|
|
104
|
+
*/
|
|
105
|
+
function extractToken(req) {
|
|
106
|
+
// Check Authorization header
|
|
107
|
+
const authHeader = req.headers.authorization;
|
|
108
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
109
|
+
return authHeader.slice(7);
|
|
110
|
+
}
|
|
111
|
+
// Check query parameter (for WebSocket and MCP connections)
|
|
112
|
+
// FastifyRequest has parsed query, IncomingMessage needs URL parsing
|
|
113
|
+
if ('query' in req && typeof req.query === 'object' && req.query?.token) {
|
|
114
|
+
return req.query.token;
|
|
115
|
+
}
|
|
116
|
+
// Fallback: parse URL manually (for IncomingMessage / WebSocket upgrade)
|
|
117
|
+
const url = req.url;
|
|
118
|
+
if (url) {
|
|
119
|
+
const parsed = (0, url_1.parse)(url, true);
|
|
120
|
+
const token = parsed.query.token;
|
|
121
|
+
if (typeof token === 'string') {
|
|
122
|
+
return token;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
function extractClusterSecret(req) {
|
|
128
|
+
const headers = req.headers;
|
|
129
|
+
const value = headers[CLUSTER_SECRET_HEADER];
|
|
130
|
+
if (Array.isArray(value)) {
|
|
131
|
+
return value[0];
|
|
132
|
+
}
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
function hasValidClusterSecret(req) {
|
|
136
|
+
const clusterSecret = getClusterSecret();
|
|
137
|
+
if (!clusterSecret) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const provided = extractClusterSecret(req);
|
|
141
|
+
return Boolean(provided && provided === clusterSecret);
|
|
142
|
+
}
|
|
143
|
+
function isClusterRoute(pathname) {
|
|
144
|
+
return pathname.startsWith('/api/servers') || pathname.startsWith('/servers');
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Check if a path is a public route
|
|
148
|
+
*/
|
|
149
|
+
function isPublicRoute(pathname) {
|
|
150
|
+
// Exact matches
|
|
151
|
+
if (PUBLIC_ROUTES.includes(pathname)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
// Static files (frontend assets)
|
|
155
|
+
if (!pathname.startsWith('/api/')) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Fastify onRequest hook for authentication
|
|
162
|
+
*/
|
|
163
|
+
async function authMiddleware(request, reply) {
|
|
164
|
+
// Reconstruct the full pathname including the prefix
|
|
165
|
+
// Fastify strips the prefix when registering under /api, so we need the raw URL
|
|
166
|
+
const pathname = request.url.split('?')[0];
|
|
167
|
+
if (isClientMode()) {
|
|
168
|
+
// Allow public routes regardless
|
|
169
|
+
if (isPublicRoute(pathname)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (!getClusterSecret()) {
|
|
173
|
+
return reply.code(503).send({
|
|
174
|
+
error: 'cluster_secret_required',
|
|
175
|
+
message: 'Cluster secret required for client mode. Set NEBULA_CLUSTER_SECRET.',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (!hasValidClusterSecret(request)) {
|
|
179
|
+
return reply.code(403).send({
|
|
180
|
+
error: 'cluster_auth_required',
|
|
181
|
+
message: 'Cluster authentication required.',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (isClusterRoute(pathname) && hasValidClusterSecret(request)) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (auth_service_1.authService.isAuthDisabled()) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Allow public routes
|
|
193
|
+
if (isPublicRoute(pathname)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// Check if auth is configured
|
|
197
|
+
if (!auth_service_1.authService.isSetupComplete()) {
|
|
198
|
+
// During setup, only allow auth routes
|
|
199
|
+
return reply.code(503).send({
|
|
200
|
+
error: 'setup_required',
|
|
201
|
+
message: '2FA setup not complete. Check server terminal for QR code.',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// Extract and validate token
|
|
205
|
+
let token = extractToken(request);
|
|
206
|
+
// Fallback: if no token in request, check the persisted session token file.
|
|
207
|
+
// This allows MCP servers and CLI tools to piggyback on the browser's auth
|
|
208
|
+
// without needing their own TOTP flow.
|
|
209
|
+
if (!token) {
|
|
210
|
+
token = readSessionToken();
|
|
211
|
+
}
|
|
212
|
+
if (!token) {
|
|
213
|
+
return reply.code(401).send({
|
|
214
|
+
error: 'auth_required',
|
|
215
|
+
message: 'Authentication required. Please log in.',
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
const payload = auth_service_1.authService.validateToken(token);
|
|
219
|
+
if (!payload) {
|
|
220
|
+
return reply.code(401).send({
|
|
221
|
+
error: 'invalid_token',
|
|
222
|
+
message: 'Invalid or expired token. Please log in again.',
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// Persist valid tokens from request headers so MCP/CLI can piggyback.
|
|
226
|
+
// Only write once (when file doesn't exist or token differs).
|
|
227
|
+
const requestToken = extractToken(request);
|
|
228
|
+
if (requestToken) {
|
|
229
|
+
const existing = readSessionToken();
|
|
230
|
+
if (existing !== requestToken) {
|
|
231
|
+
persistSessionToken(requestToken);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Token is valid, continue (simply return)
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* WebSocket authentication middleware
|
|
238
|
+
* Returns true if the connection is authenticated
|
|
239
|
+
*/
|
|
240
|
+
function authWebSocketMiddleware(request) {
|
|
241
|
+
if (isClientMode()) {
|
|
242
|
+
const clusterSecret = getClusterSecret();
|
|
243
|
+
if (!clusterSecret) {
|
|
244
|
+
console.log('[Auth] WebSocket rejected - cluster secret not configured');
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
const provided = extractClusterSecret(request);
|
|
248
|
+
if (provided !== clusterSecret) {
|
|
249
|
+
console.log('[Auth] WebSocket rejected - invalid cluster secret');
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
if (auth_service_1.authService.isAuthDisabled()) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
const url = request.url || '';
|
|
258
|
+
const pathname = (0, url_1.parse)(url).pathname || '';
|
|
259
|
+
// Check if auth is configured
|
|
260
|
+
if (!auth_service_1.authService.isSetupComplete()) {
|
|
261
|
+
console.log('[Auth] WebSocket rejected - setup not complete');
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
// Extract and validate token from query parameter
|
|
265
|
+
const token = extractToken(request);
|
|
266
|
+
if (!token) {
|
|
267
|
+
console.log('[Auth] WebSocket rejected - no token provided');
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
const payload = auth_service_1.authService.validateToken(token);
|
|
271
|
+
if (!payload) {
|
|
272
|
+
console.log('[Auth] WebSocket rejected - invalid token');
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Service - TOTP-based 2FA authentication
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - TOTP secret generation and verification
|
|
6
|
+
* - JWT session token management
|
|
7
|
+
* - Auth config persistence (~/.nebula/auth.json)
|
|
8
|
+
*/
|
|
9
|
+
export interface AuthConfig {
|
|
10
|
+
totpSecret: string;
|
|
11
|
+
jwtSecret: string;
|
|
12
|
+
setupComplete: boolean;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
}
|
|
15
|
+
export interface JWTPayload {
|
|
16
|
+
iat: number;
|
|
17
|
+
exp: number;
|
|
18
|
+
trusted: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface AuthStatus {
|
|
21
|
+
configured: boolean;
|
|
22
|
+
authenticated: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface VerifyResult {
|
|
25
|
+
success: boolean;
|
|
26
|
+
token?: string;
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
declare class AuthService {
|
|
30
|
+
private config;
|
|
31
|
+
private initialized;
|
|
32
|
+
private failedAttempts;
|
|
33
|
+
private disabled;
|
|
34
|
+
/**
|
|
35
|
+
* Disable authentication (for local/dev usage only)
|
|
36
|
+
*/
|
|
37
|
+
disableAuth(): void;
|
|
38
|
+
isAuthDisabled(): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Initialize the auth service
|
|
41
|
+
* Returns true if setup is needed (first run)
|
|
42
|
+
*/
|
|
43
|
+
initialize(): Promise<boolean>;
|
|
44
|
+
/**
|
|
45
|
+
* Generate a secure random JWT secret
|
|
46
|
+
*/
|
|
47
|
+
private generateJWTSecret;
|
|
48
|
+
/**
|
|
49
|
+
* Save config to disk with secure permissions
|
|
50
|
+
*/
|
|
51
|
+
private saveConfig;
|
|
52
|
+
/**
|
|
53
|
+
* Print QR code and manual key to terminal for initial setup
|
|
54
|
+
*/
|
|
55
|
+
printSetupInstructions(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Check if 2FA setup is complete
|
|
58
|
+
*/
|
|
59
|
+
isSetupComplete(): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Verify a TOTP code and issue a JWT token
|
|
62
|
+
*/
|
|
63
|
+
verifyCode(code: string, trustBrowser?: boolean): VerifyResult;
|
|
64
|
+
/**
|
|
65
|
+
* Issue a new JWT token
|
|
66
|
+
*/
|
|
67
|
+
private issueToken;
|
|
68
|
+
/**
|
|
69
|
+
* Validate a JWT token
|
|
70
|
+
* Returns the payload if valid, null if invalid
|
|
71
|
+
*/
|
|
72
|
+
validateToken(token: string): JWTPayload | null;
|
|
73
|
+
/**
|
|
74
|
+
* Get auth status for a request
|
|
75
|
+
*/
|
|
76
|
+
getAuthStatus(token?: string): AuthStatus;
|
|
77
|
+
/**
|
|
78
|
+
* Get the TOTP secret (for testing only)
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
_getTotpSecret(): string | null;
|
|
82
|
+
}
|
|
83
|
+
export declare const authService: AuthService;
|
|
84
|
+
export {};
|