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
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Nebula Node Server - Main Entry Point
|
|
4
|
+
*
|
|
5
|
+
* Unified Node.js server for:
|
|
6
|
+
* - Jupyter kernel management
|
|
7
|
+
* - LLM providers (Google, OpenAI, Anthropic)
|
|
8
|
+
* - Filesystem operations
|
|
9
|
+
* - Python environment discovery
|
|
10
|
+
* - Terminal PTY management
|
|
11
|
+
*
|
|
12
|
+
* Uses Fastify with HTTP/2 support.
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
48
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
49
|
+
};
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
const fastify_1 = __importDefault(require("fastify"));
|
|
52
|
+
const cors_1 = __importDefault(require("@fastify/cors"));
|
|
53
|
+
const multipart_1 = __importDefault(require("@fastify/multipart"));
|
|
54
|
+
const static_1 = __importDefault(require("@fastify/static"));
|
|
55
|
+
const ws_1 = require("ws");
|
|
56
|
+
const url_1 = require("url");
|
|
57
|
+
const path = __importStar(require("path"));
|
|
58
|
+
const fs = __importStar(require("fs"));
|
|
59
|
+
const os = __importStar(require("os"));
|
|
60
|
+
const crypto = __importStar(require("crypto"));
|
|
61
|
+
const crypto_1 = require("crypto");
|
|
62
|
+
// Import routes
|
|
63
|
+
const kernel_1 = __importStar(require("./routes/kernel"));
|
|
64
|
+
const llm_1 = __importStar(require("./routes/llm"));
|
|
65
|
+
const fs_1 = __importDefault(require("./routes/fs"));
|
|
66
|
+
const notebook_1 = __importDefault(require("./routes/notebook"));
|
|
67
|
+
const python_1 = __importDefault(require("./routes/python"));
|
|
68
|
+
const auth_1 = __importDefault(require("./routes/auth"));
|
|
69
|
+
const cluster_1 = __importDefault(require("./routes/cluster"));
|
|
70
|
+
const resources_1 = __importDefault(require("./routes/resources"));
|
|
71
|
+
// Import cluster
|
|
72
|
+
const server_registry_1 = require("./cluster/server-registry");
|
|
73
|
+
const cluster_secret_1 = require("./cluster/cluster-secret");
|
|
74
|
+
const client_registration_1 = require("./cluster/client-registration");
|
|
75
|
+
// Import auth
|
|
76
|
+
const auth_2 = require("./auth");
|
|
77
|
+
const fs_service_1 = require("./fs/fs-service");
|
|
78
|
+
// Import terminal routes (existing)
|
|
79
|
+
const server_1 = require("./terminal/server");
|
|
80
|
+
// Import notebook WebSocket
|
|
81
|
+
const notebook_websocket_1 = require("./notebook/notebook-websocket");
|
|
82
|
+
const PORT = process.env.PORT || process.env.NODE_SERVER_PORT || 3000;
|
|
83
|
+
const DEV_MODE = process.env.DEV_MODE === 'true' || process.argv.includes('--dev');
|
|
84
|
+
const BODY_LIMIT = process.env.NEBULA_BODY_LIMIT ||
|
|
85
|
+
process.env.NEBULA_MAX_BODY_SIZE ||
|
|
86
|
+
'1gb';
|
|
87
|
+
const CLIENT_MODE = process.argv.includes('--client') ||
|
|
88
|
+
process.argv.includes('--client-mode') ||
|
|
89
|
+
process.env.NEBULA_CLIENT === 'true' ||
|
|
90
|
+
process.env.NEBULA_CLIENT_MODE === 'true' ||
|
|
91
|
+
process.env.npm_config_client === 'true' ||
|
|
92
|
+
process.env.npm_config_client === '1';
|
|
93
|
+
const hasCliFlag = (names) => names.some(name => process.argv.includes(name));
|
|
94
|
+
const parseOptionalBoolean = (value) => {
|
|
95
|
+
if (value === undefined)
|
|
96
|
+
return null;
|
|
97
|
+
const normalized = value.trim().toLowerCase();
|
|
98
|
+
if (['true', '1', 'yes', 'on'].includes(normalized))
|
|
99
|
+
return true;
|
|
100
|
+
if (['false', '0', 'no', 'off'].includes(normalized))
|
|
101
|
+
return false;
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
const resolveBooleanFlag = (enabledCliFlags, disabledCliFlags, envNames, npmConfigNames, defaultValue) => {
|
|
105
|
+
if (hasCliFlag(disabledCliFlags))
|
|
106
|
+
return false;
|
|
107
|
+
if (hasCliFlag(enabledCliFlags))
|
|
108
|
+
return true;
|
|
109
|
+
for (const envName of [...envNames, ...npmConfigNames]) {
|
|
110
|
+
const resolved = parseOptionalBoolean(process.env[envName]);
|
|
111
|
+
if (resolved !== null) {
|
|
112
|
+
return resolved;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return defaultValue;
|
|
116
|
+
};
|
|
117
|
+
const getArgValue = (name) => {
|
|
118
|
+
const idx = process.argv.findIndex(arg => arg === name);
|
|
119
|
+
if (idx !== -1 && process.argv[idx + 1]) {
|
|
120
|
+
return process.argv[idx + 1];
|
|
121
|
+
}
|
|
122
|
+
const prefix = `${name}=`;
|
|
123
|
+
const found = process.argv.find(arg => arg.startsWith(prefix));
|
|
124
|
+
if (found) {
|
|
125
|
+
return found.slice(prefix.length);
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
};
|
|
129
|
+
const WORKDIR = getArgValue('--workdir') || process.env.NEBULA_WORKDIR || process.env.npm_config_workdir;
|
|
130
|
+
const PRESERVE_KERNELS = resolveBooleanFlag(['--preserve-kernels', '--preserve-kernel'], ['--no-preserve-kernels', '--no-preserve-kernel'], ['NEBULA_PRESERVE_KERNELS'], ['npm_config_preserve_kernels'], DEV_MODE);
|
|
131
|
+
const REATTACH_KERNELS = resolveBooleanFlag(['--reattach-kernels', '--reattach-kernel'], ['--no-reattach-kernels', '--no-reattach-kernel'], ['NEBULA_REATTACH_KERNELS'], ['npm_config_reattach_kernels'], DEV_MODE);
|
|
132
|
+
// Log kernel preservation settings
|
|
133
|
+
if (PRESERVE_KERNELS)
|
|
134
|
+
console.log('[Server] Kernel preservation ENABLED');
|
|
135
|
+
if (REATTACH_KERNELS)
|
|
136
|
+
console.log('[Server] Kernel reattachment ENABLED');
|
|
137
|
+
/**
|
|
138
|
+
* Parse a human-readable body limit string (e.g., '200mb', '1gb') to bytes.
|
|
139
|
+
*/
|
|
140
|
+
function parseBodyLimit(limit) {
|
|
141
|
+
const match = limit.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb|tb)?$/i);
|
|
142
|
+
if (!match) {
|
|
143
|
+
return 1024 * 1024 * 1024; // default 1gb
|
|
144
|
+
}
|
|
145
|
+
const value = parseFloat(match[1]);
|
|
146
|
+
const unit = (match[2] || 'b').toLowerCase();
|
|
147
|
+
const multipliers = {
|
|
148
|
+
b: 1,
|
|
149
|
+
kb: 1024,
|
|
150
|
+
mb: 1024 * 1024,
|
|
151
|
+
gb: 1024 * 1024 * 1024,
|
|
152
|
+
tb: 1024 * 1024 * 1024 * 1024,
|
|
153
|
+
};
|
|
154
|
+
return Math.floor(value * (multipliers[unit] || 1));
|
|
155
|
+
}
|
|
156
|
+
// HTTP/1.1 only for now — HTTP/2 streaming requires a separate HTTPS server.
|
|
157
|
+
// Returning from a function (vs a literal null const) keeps the TLS branch
|
|
158
|
+
// typechecking so the build stays green while the feature is parked.
|
|
159
|
+
function loadTlsCert() {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Generate or load self-signed TLS certificate for HTTP/2
|
|
164
|
+
*/
|
|
165
|
+
function getOrCreateTlsCert() {
|
|
166
|
+
const tlsDir = path.join(os.homedir(), '.nebula', 'tls');
|
|
167
|
+
const keyPath = path.join(tlsDir, 'server.key');
|
|
168
|
+
const certPath = path.join(tlsDir, 'server.cert');
|
|
169
|
+
try {
|
|
170
|
+
// Check if cert already exists and is still valid
|
|
171
|
+
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
172
|
+
const key = fs.readFileSync(keyPath);
|
|
173
|
+
const cert = fs.readFileSync(certPath);
|
|
174
|
+
return { key, cert };
|
|
175
|
+
}
|
|
176
|
+
// Generate self-signed certificate using Node.js crypto
|
|
177
|
+
// Only available in Node.js 15+
|
|
178
|
+
if (!('generateKeyPairSync' in crypto)) {
|
|
179
|
+
console.log('[TLS] crypto.generateKeyPairSync not available, falling back to HTTP/1.1');
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
console.log('[TLS] Generating self-signed certificate for HTTP/2...');
|
|
183
|
+
// Create TLS directory
|
|
184
|
+
fs.mkdirSync(tlsDir, { recursive: true, mode: 0o700 });
|
|
185
|
+
// Use child_process to generate cert with openssl (most portable approach)
|
|
186
|
+
const { execSync } = require('child_process');
|
|
187
|
+
try {
|
|
188
|
+
execSync(`openssl req -x509 -newkey rsa:2048 -keyout "${keyPath}" -out "${certPath}" -days 365 -nodes -subj "/CN=localhost"`, { stdio: 'pipe' });
|
|
189
|
+
fs.chmodSync(keyPath, 0o600);
|
|
190
|
+
fs.chmodSync(certPath, 0o600);
|
|
191
|
+
const key = fs.readFileSync(keyPath);
|
|
192
|
+
const cert = fs.readFileSync(certPath);
|
|
193
|
+
console.log('[TLS] Self-signed certificate generated and cached at ~/.nebula/tls/');
|
|
194
|
+
return { key, cert };
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
console.log('[TLS] openssl not available, falling back to HTTP/1.1');
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
console.log('[TLS] Failed to generate certificate, falling back to HTTP/1.1');
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Create and configure Fastify app
|
|
208
|
+
*/
|
|
209
|
+
async function createApp() {
|
|
210
|
+
const bodyLimitBytes = parseBodyLimit(BODY_LIMIT);
|
|
211
|
+
const tlsCert = loadTlsCert();
|
|
212
|
+
let fastify;
|
|
213
|
+
if (tlsCert) {
|
|
214
|
+
// Cast to FastifyInstance to unify the type with the HTTP/1 branch.
|
|
215
|
+
// The HTTP/2 Fastify instance is a superset but TypeScript infers a
|
|
216
|
+
// different generic specialisation; the cast is safe because we only
|
|
217
|
+
// use the common API surface.
|
|
218
|
+
fastify = (0, fastify_1.default)({
|
|
219
|
+
http2: true,
|
|
220
|
+
https: {
|
|
221
|
+
key: tlsCert.key,
|
|
222
|
+
cert: tlsCert.cert,
|
|
223
|
+
allowHTTP1: true,
|
|
224
|
+
},
|
|
225
|
+
bodyLimit: bodyLimitBytes,
|
|
226
|
+
});
|
|
227
|
+
console.log('[Server] HTTP/2 with TLS enabled');
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
fastify = (0, fastify_1.default)({
|
|
231
|
+
bodyLimit: bodyLimitBytes,
|
|
232
|
+
});
|
|
233
|
+
console.log('[Server] HTTP/1.1 mode (no TLS)');
|
|
234
|
+
}
|
|
235
|
+
// Request timing hook
|
|
236
|
+
fastify.addHook('onResponse', (request, reply, done) => {
|
|
237
|
+
const duration = reply.elapsedTime;
|
|
238
|
+
if (duration > 1000) {
|
|
239
|
+
console.log(`[Slow Request] ${request.method} ${request.url} - ${Math.round(duration)}ms`);
|
|
240
|
+
}
|
|
241
|
+
done();
|
|
242
|
+
});
|
|
243
|
+
// Register CORS
|
|
244
|
+
await fastify.register(cors_1.default, {
|
|
245
|
+
origin: '*',
|
|
246
|
+
credentials: true,
|
|
247
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
248
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-API-Provider'],
|
|
249
|
+
});
|
|
250
|
+
// Register multipart support (replaces multer)
|
|
251
|
+
await fastify.register(multipart_1.default, {
|
|
252
|
+
limits: {
|
|
253
|
+
fileSize: bodyLimitBytes,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
// Allow empty body with Content-Type: application/json.
|
|
257
|
+
// MCP clients and some tools send DELETE/GET with an empty JSON body.
|
|
258
|
+
// Fastify rejects this by default (FST_ERR_CTP_EMPTY_JSON_BODY).
|
|
259
|
+
fastify.addContentTypeParser('application/json', { parseAs: 'string' }, (req, body, done) => {
|
|
260
|
+
if (!body || (typeof body === 'string' && body.trim() === '')) {
|
|
261
|
+
done(null, undefined);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
done(null, JSON.parse(body));
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
done(err, undefined);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
// Health check endpoints (public - no auth required)
|
|
272
|
+
fastify.get('/api/health', async (_request, reply) => {
|
|
273
|
+
// Use configured root directory (from .nebula-config.json or fallback to cwd)
|
|
274
|
+
const rootDir = fs_service_1.fsService.normalizePath('~');
|
|
275
|
+
return reply.send({
|
|
276
|
+
status: 'ok',
|
|
277
|
+
version: '1.0.0',
|
|
278
|
+
ready: kernel_1.kernelService.isReady,
|
|
279
|
+
llm_providers: Object.keys(llm_1.llmService.getAvailableProviders()),
|
|
280
|
+
cwd: rootDir,
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
fastify.get('/api/ready', async (_request, reply) => {
|
|
284
|
+
if (!kernel_1.kernelService.isReady) {
|
|
285
|
+
return reply.code(503).send({
|
|
286
|
+
detail: 'Service initializing, kernel discovery in progress',
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return reply.send({ status: 'ready' });
|
|
290
|
+
});
|
|
291
|
+
// Auth routes (public - no auth required)
|
|
292
|
+
await fastify.register(auth_1.default, { prefix: '/api' });
|
|
293
|
+
// Auth middleware - protect all other API routes
|
|
294
|
+
// Applied as an onRequest hook for /api/* routes (excluding public ones)
|
|
295
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
296
|
+
const pathname = request.url.split('?')[0];
|
|
297
|
+
// Skip health, ready, and auth routes (they are public)
|
|
298
|
+
if (pathname === '/api/health' ||
|
|
299
|
+
pathname === '/api/ready' ||
|
|
300
|
+
pathname.startsWith('/api/auth/')) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// Skip non-API routes (static files etc)
|
|
304
|
+
if (!pathname.startsWith('/api/')) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// Apply auth middleware
|
|
308
|
+
await (0, auth_2.authMiddleware)(request, reply);
|
|
309
|
+
});
|
|
310
|
+
// API routes (protected)
|
|
311
|
+
await fastify.register(kernel_1.default, { prefix: '/api' });
|
|
312
|
+
await fastify.register(llm_1.default, { prefix: '/api' });
|
|
313
|
+
await fastify.register(fs_1.default, { prefix: '/api' });
|
|
314
|
+
await fastify.register(notebook_1.default, { prefix: '/api' });
|
|
315
|
+
await fastify.register(python_1.default, { prefix: '/api' });
|
|
316
|
+
await fastify.register(cluster_1.default, { prefix: '/api' });
|
|
317
|
+
await fastify.register(resources_1.default, { prefix: '/api/resources' });
|
|
318
|
+
// Terminal routes (registered directly on the app, not under /api prefix)
|
|
319
|
+
await fastify.register(server_1.setupTerminalRoutes);
|
|
320
|
+
return fastify;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Setup WebSocket routing (kernel, terminal, notebook)
|
|
324
|
+
* Uses the raw Node.js HTTP(S) server from Fastify for upgrade handling
|
|
325
|
+
*/
|
|
326
|
+
function setupWebSockets(server) {
|
|
327
|
+
// Create WebSocket server with noServer mode for path-based routing
|
|
328
|
+
// Disable per-message deflate to avoid compression issues with proxies/browsers
|
|
329
|
+
const wss = new ws_1.WebSocketServer({
|
|
330
|
+
noServer: true,
|
|
331
|
+
perMessageDeflate: false,
|
|
332
|
+
});
|
|
333
|
+
// Handle upgrade requests
|
|
334
|
+
server.on('upgrade', (request, socket, head) => {
|
|
335
|
+
const pathname = (0, url_1.parse)(request.url || '').pathname || '';
|
|
336
|
+
// Route to kernel WebSocket
|
|
337
|
+
if (pathname.match(/^\/api\/kernels\/[^/]+\/ws$/)) {
|
|
338
|
+
// Authenticate WebSocket connection
|
|
339
|
+
if (!(0, auth_2.authWebSocketMiddleware)(request)) {
|
|
340
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
341
|
+
socket.destroy();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
345
|
+
wss.emit('connection', ws, request);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
// Terminal WebSocket (/ws) is handled by setupTerminalWebSocket
|
|
349
|
+
// Don't destroy socket for other paths - let other handlers deal with them
|
|
350
|
+
});
|
|
351
|
+
// Setup kernel WebSocket handler
|
|
352
|
+
(0, kernel_1.setupKernelWebSocket)(wss);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Setup static file serving for frontend
|
|
356
|
+
*/
|
|
357
|
+
async function setupStaticServing(fastify) {
|
|
358
|
+
const distDir = path.join(__dirname, '../../dist');
|
|
359
|
+
if (DEV_MODE) {
|
|
360
|
+
console.log('[Server] Development mode - frontend should run separately with npm run dev');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// Production mode: serve from dist
|
|
364
|
+
if (fs.existsSync(distDir)) {
|
|
365
|
+
// Serve static files
|
|
366
|
+
await fastify.register(static_1.default, {
|
|
367
|
+
root: distDir,
|
|
368
|
+
prefix: '/',
|
|
369
|
+
});
|
|
370
|
+
// SPA fallback - serve index.html for all non-API routes
|
|
371
|
+
fastify.setNotFoundHandler(async (request, reply) => {
|
|
372
|
+
const pathname = request.url.split('?')[0];
|
|
373
|
+
if (pathname.startsWith('/api/')) {
|
|
374
|
+
return reply.code(404).send({ detail: 'Not found' });
|
|
375
|
+
}
|
|
376
|
+
if (pathname.startsWith('/assets/') || path.extname(pathname)) {
|
|
377
|
+
return reply.code(404).type('text/plain').send('Not found');
|
|
378
|
+
}
|
|
379
|
+
return reply.sendFile('index.html');
|
|
380
|
+
});
|
|
381
|
+
console.log(`[Server] Serving frontend from ${distDir}`);
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
console.log(`[Server] Warning: dist directory not found at ${distDir}`);
|
|
385
|
+
console.log('[Server] Run "npm run build" to create the production build');
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Main entry point
|
|
390
|
+
*/
|
|
391
|
+
async function main() {
|
|
392
|
+
console.log('[Server] Starting Nebula Node Server...');
|
|
393
|
+
const mainServerUrl = process.env.NEBULA_MAIN_SERVER;
|
|
394
|
+
if (CLIENT_MODE && !mainServerUrl) {
|
|
395
|
+
console.error('[Cluster] Client mode requested but NEBULA_MAIN_SERVER is not set.');
|
|
396
|
+
console.error('[Cluster] Set NEBULA_MAIN_SERVER or remove --client.');
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
process.env.NEBULA_CLIENT_MODE = CLIENT_MODE ? 'true' : 'false';
|
|
400
|
+
if (!process.env.NEBULA_CLUSTER_SECRET) {
|
|
401
|
+
const allowCreate = !CLIENT_MODE;
|
|
402
|
+
const secret = (0, cluster_secret_1.getOrCreateClusterSecret)({ allowCreate });
|
|
403
|
+
if (secret) {
|
|
404
|
+
process.env.NEBULA_CLUSTER_SECRET = secret;
|
|
405
|
+
if (allowCreate) {
|
|
406
|
+
console.log('[Cluster] Generated cluster secret (stored at ~/.nebula/cluster.json)');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else if (CLIENT_MODE) {
|
|
410
|
+
console.error('[Cluster] No NEBULA_CLUSTER_SECRET set and no ~/.nebula/cluster.json found.');
|
|
411
|
+
console.error('[Cluster] Set NEBULA_CLUSTER_SECRET or copy ~/.nebula/cluster.json from the main server.');
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const authDisabled = process.argv.includes('--noauth') ||
|
|
416
|
+
process.argv.includes('--no-auth') ||
|
|
417
|
+
process.env.NO_AUTH === 'true' ||
|
|
418
|
+
process.env.NEBULA_NO_AUTH === 'true' ||
|
|
419
|
+
process.env.npm_config_noauth === 'true' ||
|
|
420
|
+
process.env.npm_config_noauth === '1' ||
|
|
421
|
+
process.env.npm_config_no_auth === 'true' ||
|
|
422
|
+
process.env.npm_config_no_auth === '1' ||
|
|
423
|
+
CLIENT_MODE;
|
|
424
|
+
if (authDisabled) {
|
|
425
|
+
auth_2.authService.disableAuth();
|
|
426
|
+
console.log(`[Auth] Disabled (${CLIENT_MODE ? 'client mode' : '--noauth'})`);
|
|
427
|
+
}
|
|
428
|
+
if (WORKDIR) {
|
|
429
|
+
try {
|
|
430
|
+
const updated = fs_service_1.fsService.setRootDirectory(WORKDIR);
|
|
431
|
+
console.log(`[Server] Root directory set to ${updated}`);
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
435
|
+
console.warn(`[Server] Failed to set root directory: ${message}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (PRESERVE_KERNELS) {
|
|
439
|
+
console.log('[Kernel] Preserve kernels enabled');
|
|
440
|
+
}
|
|
441
|
+
if (REATTACH_KERNELS) {
|
|
442
|
+
console.log('[Kernel] Reattach kernels on startup enabled');
|
|
443
|
+
}
|
|
444
|
+
// Initialize authentication
|
|
445
|
+
const setupNeeded = await auth_2.authService.initialize();
|
|
446
|
+
if (setupNeeded) {
|
|
447
|
+
auth_2.authService.printSetupInstructions();
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
console.log('[Auth] 2FA configured and ready');
|
|
451
|
+
}
|
|
452
|
+
// Set local server ID from hostname
|
|
453
|
+
const localServerId = `${os.hostname()}:${PORT}`;
|
|
454
|
+
server_registry_1.serverRegistry.setLocalServerId(localServerId);
|
|
455
|
+
console.log(`[Cluster] Local server ID: ${localServerId}`);
|
|
456
|
+
const serverInstanceId = (0, crypto_1.randomUUID)();
|
|
457
|
+
process.env.NEBULA_SERVER_ID = localServerId;
|
|
458
|
+
process.env.NEBULA_SERVER_INSTANCE_ID = serverInstanceId;
|
|
459
|
+
kernel_1.kernelService.setServerIdentity(localServerId, serverInstanceId);
|
|
460
|
+
// Create Fastify app (HTTP — always works, no cert warnings)
|
|
461
|
+
const fastify = await createApp();
|
|
462
|
+
// Setup static file serving
|
|
463
|
+
await setupStaticServing(fastify);
|
|
464
|
+
// Start HTTP server on main port
|
|
465
|
+
await fastify.listen({ port: Number(PORT), host: '0.0.0.0' });
|
|
466
|
+
// Get the raw Node.js server for WebSocket handling
|
|
467
|
+
const server = fastify.server;
|
|
468
|
+
// Setup WebSocket routing (kernel)
|
|
469
|
+
setupWebSockets(server);
|
|
470
|
+
// Setup terminal WebSocket (now using noServer mode)
|
|
471
|
+
(0, server_1.setupTerminalWebSocket)(server);
|
|
472
|
+
// Setup notebook operations WebSocket
|
|
473
|
+
(0, notebook_websocket_1.setupNotebookWebSocket)(server);
|
|
474
|
+
if (REATTACH_KERNELS) {
|
|
475
|
+
try {
|
|
476
|
+
const result = await kernel_1.kernelService.reattachOrphanedSessions();
|
|
477
|
+
if (result.attempted > 0) {
|
|
478
|
+
console.log(`[Kernel] Reattach summary: ${result.reattached} reattached, ${result.failed} failed, ${result.skipped} skipped`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
catch (err) {
|
|
482
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
483
|
+
console.warn(`[Kernel] Reattach failed: ${message}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Graceful shutdown
|
|
487
|
+
let shuttingDown = false;
|
|
488
|
+
const shutdown = async () => {
|
|
489
|
+
if (shuttingDown)
|
|
490
|
+
return; // ignore repeat signals (tsx watch can send several)
|
|
491
|
+
shuttingDown = true;
|
|
492
|
+
console.log('\n[Server] Shutting down...');
|
|
493
|
+
console.log(`[Server] PRESERVE_KERNELS=${PRESERVE_KERNELS}`);
|
|
494
|
+
// Safety net: shutdown must never hang the process. fastify.close() blocks
|
|
495
|
+
// until every open connection drains, and Nebula holds long-lived WebSockets
|
|
496
|
+
// (kernel/notebook/terminal), so without this a SIGTERM from `tsx watch` would
|
|
497
|
+
// leave the old process alive — and backend code changes would silently never
|
|
498
|
+
// reload. Force-exit if cleanup stalls.
|
|
499
|
+
const forceExit = setTimeout(() => {
|
|
500
|
+
console.warn('[Server] Shutdown exceeded 3s — forcing exit');
|
|
501
|
+
process.exit(0);
|
|
502
|
+
}, 3000);
|
|
503
|
+
forceExit.unref();
|
|
504
|
+
// Cleanup terminals
|
|
505
|
+
(0, server_1.cleanupTerminals)();
|
|
506
|
+
// Cleanup kernel sessions (fast when preserving; awaited so kernel state is saved)
|
|
507
|
+
try {
|
|
508
|
+
await kernel_1.kernelService.shutdown({ preserveKernels: PRESERVE_KERNELS });
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
console.error('[Server] Error during kernel cleanup:', err);
|
|
512
|
+
}
|
|
513
|
+
// Cleanup cluster registration
|
|
514
|
+
try {
|
|
515
|
+
await client_registration_1.clientRegistration.shutdown();
|
|
516
|
+
server_registry_1.serverRegistry.shutdown();
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
console.error('[Server] Error during cluster cleanup:', err);
|
|
520
|
+
}
|
|
521
|
+
// Close the HTTP/WS server, but don't wait forever on open WebSocket clients —
|
|
522
|
+
// race the close against a short timeout so dev restarts stay snappy.
|
|
523
|
+
try {
|
|
524
|
+
await Promise.race([
|
|
525
|
+
fastify.close(),
|
|
526
|
+
new Promise((resolve) => setTimeout(resolve, 750)),
|
|
527
|
+
]);
|
|
528
|
+
}
|
|
529
|
+
catch (err) {
|
|
530
|
+
console.error('[Server] Error during server close:', err);
|
|
531
|
+
}
|
|
532
|
+
console.log('[Server] Server closed');
|
|
533
|
+
clearTimeout(forceExit);
|
|
534
|
+
process.exit(0);
|
|
535
|
+
};
|
|
536
|
+
process.on('SIGINT', shutdown);
|
|
537
|
+
process.on('SIGTERM', shutdown);
|
|
538
|
+
const protocol = fastify.server.constructor.name.includes('Secure') ? 'https' : 'http';
|
|
539
|
+
const wsProtocol = protocol === 'https' ? 'wss' : 'ws';
|
|
540
|
+
const mode = DEV_MODE ? 'development' : 'production';
|
|
541
|
+
console.log(`[Server] Nebula running on ${protocol}://localhost:${PORT} (${mode} mode)`);
|
|
542
|
+
console.log(`[Server] API endpoints: ${protocol}://localhost:${PORT}/api/*`);
|
|
543
|
+
console.log(`[Server] Kernel WebSocket: ${wsProtocol}://localhost:${PORT}/api/kernels/{session_id}/ws`);
|
|
544
|
+
console.log(`[Server] Notebook WebSocket: ${wsProtocol}://localhost:${PORT}/api/notebook/{path}/ws`);
|
|
545
|
+
console.log(`[Server] Terminal WebSocket: ${wsProtocol}://localhost:${PORT}/ws?id={terminal_id}`);
|
|
546
|
+
console.log(`[Server] Root directory: ${fs_service_1.fsService.getRootDirectory()} (change with --workdir)`);
|
|
547
|
+
// Initialize client registration (explicit client mode only)
|
|
548
|
+
if (CLIENT_MODE) {
|
|
549
|
+
client_registration_1.clientRegistration.initFromEnv(Number(PORT));
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
// Run
|
|
553
|
+
main().catch((err) => {
|
|
554
|
+
console.error('[Server] Fatal error:', err);
|
|
555
|
+
process.exit(1);
|
|
556
|
+
});
|