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,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Auth Service - TOTP-based 2FA authentication
|
|
4
|
+
*
|
|
5
|
+
* Handles:
|
|
6
|
+
* - TOTP secret generation and verification
|
|
7
|
+
* - JWT session token management
|
|
8
|
+
* - Auth config persistence (~/.nebula/auth.json)
|
|
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.authService = void 0;
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const os = __importStar(require("os"));
|
|
48
|
+
const otplib_1 = require("otplib");
|
|
49
|
+
const jwt = __importStar(require("jsonwebtoken"));
|
|
50
|
+
const qrcode = __importStar(require("qrcode-terminal"));
|
|
51
|
+
// Config file location
|
|
52
|
+
const NEBULA_DIR = path.join(os.homedir(), '.nebula');
|
|
53
|
+
const AUTH_CONFIG_FILE = path.join(NEBULA_DIR, 'auth.json');
|
|
54
|
+
// JWT settings
|
|
55
|
+
const JWT_SECRET_LENGTH = 64;
|
|
56
|
+
const SHORT_SESSION_HOURS = 24;
|
|
57
|
+
const LONG_SESSION_DAYS = 30;
|
|
58
|
+
// Rate limiting: max 5 attempts per 30 seconds
|
|
59
|
+
const MAX_ATTEMPTS = 5;
|
|
60
|
+
const WINDOW_MS = 30000;
|
|
61
|
+
// Temporary debugging setting: accept up to five TOTP steps before and after
|
|
62
|
+
// current time while investigating clock skew and login flow issues.
|
|
63
|
+
const TOTP_WINDOW_STEPS = 5;
|
|
64
|
+
otplib_1.authenticator.options = {
|
|
65
|
+
...otplib_1.authenticator.options,
|
|
66
|
+
window: [TOTP_WINDOW_STEPS, TOTP_WINDOW_STEPS],
|
|
67
|
+
};
|
|
68
|
+
class AuthService {
|
|
69
|
+
config = null;
|
|
70
|
+
initialized = false;
|
|
71
|
+
failedAttempts = []; // timestamps of failed attempts
|
|
72
|
+
disabled = false;
|
|
73
|
+
/**
|
|
74
|
+
* Disable authentication (for local/dev usage only)
|
|
75
|
+
*/
|
|
76
|
+
disableAuth() {
|
|
77
|
+
this.disabled = true;
|
|
78
|
+
}
|
|
79
|
+
isAuthDisabled() {
|
|
80
|
+
return this.disabled;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Initialize the auth service
|
|
84
|
+
* Returns true if setup is needed (first run)
|
|
85
|
+
*/
|
|
86
|
+
async initialize() {
|
|
87
|
+
if (this.initialized) {
|
|
88
|
+
return !this.config?.setupComplete;
|
|
89
|
+
}
|
|
90
|
+
if (this.disabled) {
|
|
91
|
+
this.initialized = true;
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
// Ensure .nebula directory exists
|
|
95
|
+
if (!fs.existsSync(NEBULA_DIR)) {
|
|
96
|
+
fs.mkdirSync(NEBULA_DIR, { recursive: true, mode: 0o700 });
|
|
97
|
+
}
|
|
98
|
+
// Try to load existing config
|
|
99
|
+
if (fs.existsSync(AUTH_CONFIG_FILE)) {
|
|
100
|
+
try {
|
|
101
|
+
const data = fs.readFileSync(AUTH_CONFIG_FILE, 'utf-8');
|
|
102
|
+
this.config = JSON.parse(data);
|
|
103
|
+
this.initialized = true;
|
|
104
|
+
return !this.config.setupComplete;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.error('[Auth] Failed to load auth config:', err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// No config exists - generate new secrets
|
|
111
|
+
this.config = {
|
|
112
|
+
totpSecret: otplib_1.authenticator.generateSecret(),
|
|
113
|
+
jwtSecret: this.generateJWTSecret(),
|
|
114
|
+
setupComplete: false,
|
|
115
|
+
createdAt: Date.now(),
|
|
116
|
+
};
|
|
117
|
+
// Save immediately so secret persists across restarts during setup
|
|
118
|
+
this.saveConfig();
|
|
119
|
+
this.initialized = true;
|
|
120
|
+
return true; // Setup needed
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Generate a secure random JWT secret
|
|
124
|
+
*/
|
|
125
|
+
generateJWTSecret() {
|
|
126
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
127
|
+
let result = '';
|
|
128
|
+
const randomBytes = require('crypto').randomBytes(JWT_SECRET_LENGTH);
|
|
129
|
+
for (let i = 0; i < JWT_SECRET_LENGTH; i++) {
|
|
130
|
+
result += chars[randomBytes[i] % chars.length];
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Save config to disk with secure permissions
|
|
136
|
+
*/
|
|
137
|
+
saveConfig() {
|
|
138
|
+
if (!this.config)
|
|
139
|
+
return;
|
|
140
|
+
fs.writeFileSync(AUTH_CONFIG_FILE, JSON.stringify(this.config, null, 2), {
|
|
141
|
+
mode: 0o600, // Owner read/write only
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Print QR code and manual key to terminal for initial setup
|
|
146
|
+
*/
|
|
147
|
+
printSetupInstructions() {
|
|
148
|
+
if (!this.config) {
|
|
149
|
+
console.error('[Auth] Cannot print setup - not initialized');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const issuer = 'NebulaNotebook';
|
|
153
|
+
const accountName = 'local';
|
|
154
|
+
const otpAuthUrl = otplib_1.authenticator.keyuri(accountName, issuer, this.config.totpSecret);
|
|
155
|
+
console.log('\n' + '='.repeat(60));
|
|
156
|
+
console.log(' NEBULA NOTEBOOK - 2FA SETUP');
|
|
157
|
+
console.log('='.repeat(60));
|
|
158
|
+
console.log('\nScan this QR code with your authenticator app:\n');
|
|
159
|
+
// Print QR code to terminal
|
|
160
|
+
qrcode.generate(otpAuthUrl, { small: true });
|
|
161
|
+
console.log('\nOr enter this key manually: ' + this.config.totpSecret);
|
|
162
|
+
console.log('\nThen open the Nebula UI and enter the 6-digit code.');
|
|
163
|
+
console.log('='.repeat(60) + '\n');
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Check if 2FA setup is complete
|
|
167
|
+
*/
|
|
168
|
+
isSetupComplete() {
|
|
169
|
+
if (this.disabled) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return this.config?.setupComplete ?? false;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Verify a TOTP code and issue a JWT token
|
|
176
|
+
*/
|
|
177
|
+
verifyCode(code, trustBrowser = false) {
|
|
178
|
+
if (!this.config) {
|
|
179
|
+
return { success: false, error: 'Auth not initialized' };
|
|
180
|
+
}
|
|
181
|
+
// Rate limiting: clean old attempts and check
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
this.failedAttempts = this.failedAttempts.filter(t => now - t < WINDOW_MS);
|
|
184
|
+
if (this.failedAttempts.length >= MAX_ATTEMPTS) {
|
|
185
|
+
const oldestAttempt = this.failedAttempts[0];
|
|
186
|
+
const waitSeconds = Math.ceil((WINDOW_MS - (now - oldestAttempt)) / 1000);
|
|
187
|
+
return { success: false, error: `Too many attempts. Try again in ${waitSeconds}s` };
|
|
188
|
+
}
|
|
189
|
+
// Verify TOTP code
|
|
190
|
+
const isValid = otplib_1.authenticator.verify({
|
|
191
|
+
token: code,
|
|
192
|
+
secret: this.config.totpSecret,
|
|
193
|
+
});
|
|
194
|
+
if (!isValid) {
|
|
195
|
+
this.failedAttempts.push(now);
|
|
196
|
+
const remaining = MAX_ATTEMPTS - this.failedAttempts.length;
|
|
197
|
+
return { success: false, error: `Invalid code. ${remaining} attempts remaining` };
|
|
198
|
+
}
|
|
199
|
+
// Success - clear failed attempts
|
|
200
|
+
this.failedAttempts = [];
|
|
201
|
+
// Mark setup as complete on first successful verification
|
|
202
|
+
if (!this.config.setupComplete) {
|
|
203
|
+
this.config.setupComplete = true;
|
|
204
|
+
this.saveConfig();
|
|
205
|
+
console.log('[Auth] Setup complete - first verification successful');
|
|
206
|
+
}
|
|
207
|
+
// Issue JWT token
|
|
208
|
+
const token = this.issueToken(trustBrowser);
|
|
209
|
+
return { success: true, token };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Issue a new JWT token
|
|
213
|
+
*/
|
|
214
|
+
issueToken(trusted) {
|
|
215
|
+
if (!this.config) {
|
|
216
|
+
throw new Error('Auth not initialized');
|
|
217
|
+
}
|
|
218
|
+
const now = Math.floor(Date.now() / 1000);
|
|
219
|
+
const expiresIn = trusted
|
|
220
|
+
? LONG_SESSION_DAYS * 24 * 60 * 60 // 30 days
|
|
221
|
+
: SHORT_SESSION_HOURS * 60 * 60; // 24 hours
|
|
222
|
+
const payload = {
|
|
223
|
+
iat: now,
|
|
224
|
+
exp: now + expiresIn,
|
|
225
|
+
trusted,
|
|
226
|
+
};
|
|
227
|
+
return jwt.sign(payload, this.config.jwtSecret);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Validate a JWT token
|
|
231
|
+
* Returns the payload if valid, null if invalid
|
|
232
|
+
*/
|
|
233
|
+
validateToken(token) {
|
|
234
|
+
if (!this.config) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const payload = jwt.verify(token, this.config.jwtSecret);
|
|
239
|
+
return payload;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get auth status for a request
|
|
247
|
+
*/
|
|
248
|
+
getAuthStatus(token) {
|
|
249
|
+
if (this.disabled) {
|
|
250
|
+
return { configured: true, authenticated: true };
|
|
251
|
+
}
|
|
252
|
+
const configured = this.config?.setupComplete ?? false;
|
|
253
|
+
const authenticated = token ? this.validateToken(token) !== null : false;
|
|
254
|
+
return { configured, authenticated };
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Get the TOTP secret (for testing only)
|
|
258
|
+
* @internal
|
|
259
|
+
*/
|
|
260
|
+
_getTotpSecret() {
|
|
261
|
+
return this.config?.totpSecret ?? null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Export singleton instance
|
|
265
|
+
exports.authService = new AuthService();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authWebSocketMiddleware = exports.authMiddleware = exports.authService = void 0;
|
|
4
|
+
var auth_service_1 = require("./auth-service");
|
|
5
|
+
Object.defineProperty(exports, "authService", { enumerable: true, get: function () { return auth_service_1.authService; } });
|
|
6
|
+
var auth_middleware_1 = require("./auth-middleware");
|
|
7
|
+
Object.defineProperty(exports, "authMiddleware", { enumerable: true, get: function () { return auth_middleware_1.authMiddleware; } });
|
|
8
|
+
Object.defineProperty(exports, "authWebSocketMiddleware", { enumerable: true, get: function () { return auth_middleware_1.authWebSocketMiddleware; } });
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client Registration
|
|
3
|
+
*
|
|
4
|
+
* Handles auto-registration with a main server when this server
|
|
5
|
+
* is started as a client/peer in a cluster.
|
|
6
|
+
*/
|
|
7
|
+
declare class ClientRegistration {
|
|
8
|
+
private config;
|
|
9
|
+
private heartbeatInterval;
|
|
10
|
+
private registrationRetryTimeout;
|
|
11
|
+
private registered;
|
|
12
|
+
private serverId;
|
|
13
|
+
/**
|
|
14
|
+
* Initialize client registration from environment variables
|
|
15
|
+
*/
|
|
16
|
+
initFromEnv(localPort: number): void;
|
|
17
|
+
/**
|
|
18
|
+
* Register with the main server
|
|
19
|
+
*/
|
|
20
|
+
private register;
|
|
21
|
+
/**
|
|
22
|
+
* Start heartbeat interval
|
|
23
|
+
*/
|
|
24
|
+
private startHeartbeat;
|
|
25
|
+
/**
|
|
26
|
+
* Send heartbeat to main server
|
|
27
|
+
*/
|
|
28
|
+
private sendHeartbeat;
|
|
29
|
+
/**
|
|
30
|
+
* Unregister from main server (called on shutdown)
|
|
31
|
+
*/
|
|
32
|
+
shutdown(): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Check if we're running as a client (registered with main server)
|
|
35
|
+
*/
|
|
36
|
+
isClient(): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Get main server URL (if we're a client)
|
|
39
|
+
*/
|
|
40
|
+
getMainServerUrl(): string | null;
|
|
41
|
+
}
|
|
42
|
+
export declare const clientRegistration: ClientRegistration;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Client Registration
|
|
4
|
+
*
|
|
5
|
+
* Handles auto-registration with a main server when this server
|
|
6
|
+
* is started as a client/peer in a cluster.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.clientRegistration = void 0;
|
|
43
|
+
const server_registry_1 = require("./server-registry");
|
|
44
|
+
const resource_service_1 = require("../resources/resource-service");
|
|
45
|
+
const os = __importStar(require("os"));
|
|
46
|
+
const HEARTBEAT_INTERVAL_MS = 30_000; // 30 seconds
|
|
47
|
+
const REGISTRATION_RETRY_MS = 10_000; // 10 seconds retry on failure
|
|
48
|
+
class ClientRegistration {
|
|
49
|
+
config = null;
|
|
50
|
+
heartbeatInterval = null;
|
|
51
|
+
registrationRetryTimeout = null;
|
|
52
|
+
registered = false;
|
|
53
|
+
serverId = null;
|
|
54
|
+
/**
|
|
55
|
+
* Initialize client registration from environment variables
|
|
56
|
+
*/
|
|
57
|
+
initFromEnv(localPort) {
|
|
58
|
+
const mainServerUrl = process.env.NEBULA_MAIN_SERVER;
|
|
59
|
+
if (!mainServerUrl) {
|
|
60
|
+
console.log('[ClientRegistration] No NEBULA_MAIN_SERVER set, running as standalone');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const localHost = process.env.NEBULA_HOST || os.hostname();
|
|
64
|
+
const serverName = process.env.NEBULA_SERVER_NAME || os.hostname();
|
|
65
|
+
const secret = process.env.NEBULA_CLUSTER_SECRET;
|
|
66
|
+
this.config = {
|
|
67
|
+
mainServerUrl: mainServerUrl.replace(/\/$/, ''), // Remove trailing slash
|
|
68
|
+
localHost,
|
|
69
|
+
localPort,
|
|
70
|
+
serverName,
|
|
71
|
+
secret,
|
|
72
|
+
};
|
|
73
|
+
// Set local server ID
|
|
74
|
+
const serverId = `${localHost}:${localPort}`;
|
|
75
|
+
server_registry_1.serverRegistry.setLocalServerId(serverId);
|
|
76
|
+
console.log(`[ClientRegistration] Will register with main server: ${mainServerUrl}`);
|
|
77
|
+
console.log(`[ClientRegistration] Local server ID: ${serverId}`);
|
|
78
|
+
// Start registration
|
|
79
|
+
this.register();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Register with the main server
|
|
83
|
+
*/
|
|
84
|
+
async register() {
|
|
85
|
+
if (!this.config)
|
|
86
|
+
return;
|
|
87
|
+
try {
|
|
88
|
+
// Get resources to send with registration
|
|
89
|
+
const resourceService = (0, resource_service_1.getResourceService)();
|
|
90
|
+
const resources = resourceService.getResources();
|
|
91
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
92
|
+
if (this.config.secret) {
|
|
93
|
+
headers['X-Nebula-Cluster-Secret'] = this.config.secret;
|
|
94
|
+
}
|
|
95
|
+
const response = await fetch(`${this.config.mainServerUrl}/api/servers/register`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers,
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
host: this.config.localHost,
|
|
100
|
+
port: this.config.localPort,
|
|
101
|
+
name: this.config.serverName,
|
|
102
|
+
secret: this.config.secret,
|
|
103
|
+
resources, // Include resources in registration
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
108
|
+
throw new Error(errorData.error || `HTTP ${response.status}`);
|
|
109
|
+
}
|
|
110
|
+
const result = await response.json();
|
|
111
|
+
this.serverId = result.serverId;
|
|
112
|
+
this.registered = true;
|
|
113
|
+
console.log(`[ClientRegistration] Successfully registered as ${this.serverId}`);
|
|
114
|
+
// Start heartbeat interval
|
|
115
|
+
this.startHeartbeat();
|
|
116
|
+
// Send immediate heartbeat to ensure resources are available right away
|
|
117
|
+
this.sendHeartbeat();
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
console.error(`[ClientRegistration] Registration failed:`, error);
|
|
121
|
+
console.log(`[ClientRegistration] Retrying in ${REGISTRATION_RETRY_MS / 1000}s...`);
|
|
122
|
+
// Retry registration
|
|
123
|
+
this.registrationRetryTimeout = setTimeout(() => {
|
|
124
|
+
this.register();
|
|
125
|
+
}, REGISTRATION_RETRY_MS);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Start heartbeat interval
|
|
130
|
+
*/
|
|
131
|
+
startHeartbeat() {
|
|
132
|
+
if (this.heartbeatInterval) {
|
|
133
|
+
clearInterval(this.heartbeatInterval);
|
|
134
|
+
}
|
|
135
|
+
this.heartbeatInterval = setInterval(async () => {
|
|
136
|
+
await this.sendHeartbeat();
|
|
137
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Send heartbeat to main server
|
|
141
|
+
*/
|
|
142
|
+
async sendHeartbeat() {
|
|
143
|
+
if (!this.config || !this.serverId)
|
|
144
|
+
return;
|
|
145
|
+
try {
|
|
146
|
+
// Get resources (cached, never blocks)
|
|
147
|
+
const resourceService = (0, resource_service_1.getResourceService)();
|
|
148
|
+
const resources = resourceService.getResources();
|
|
149
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
150
|
+
if (this.config.secret) {
|
|
151
|
+
headers['X-Nebula-Cluster-Secret'] = this.config.secret;
|
|
152
|
+
}
|
|
153
|
+
const response = await fetch(`${this.config.mainServerUrl}/api/servers/${encodeURIComponent(this.serverId)}/heartbeat`, {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers,
|
|
156
|
+
body: JSON.stringify({ resources }),
|
|
157
|
+
});
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
// If server not found, try to re-register
|
|
160
|
+
if (response.status === 404) {
|
|
161
|
+
console.log('[ClientRegistration] Server not found on main, re-registering...');
|
|
162
|
+
this.registered = false;
|
|
163
|
+
this.register();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.error('[ClientRegistration] Heartbeat failed:', error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Unregister from main server (called on shutdown)
|
|
173
|
+
*/
|
|
174
|
+
async shutdown() {
|
|
175
|
+
// Clear intervals
|
|
176
|
+
if (this.heartbeatInterval) {
|
|
177
|
+
clearInterval(this.heartbeatInterval);
|
|
178
|
+
this.heartbeatInterval = null;
|
|
179
|
+
}
|
|
180
|
+
if (this.registrationRetryTimeout) {
|
|
181
|
+
clearTimeout(this.registrationRetryTimeout);
|
|
182
|
+
this.registrationRetryTimeout = null;
|
|
183
|
+
}
|
|
184
|
+
// Unregister from main server
|
|
185
|
+
if (this.config && this.serverId && this.registered) {
|
|
186
|
+
try {
|
|
187
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
188
|
+
if (this.config.secret) {
|
|
189
|
+
headers['X-Nebula-Cluster-Secret'] = this.config.secret;
|
|
190
|
+
}
|
|
191
|
+
await fetch(`${this.config.mainServerUrl}/api/servers/${encodeURIComponent(this.serverId)}`, {
|
|
192
|
+
method: 'DELETE',
|
|
193
|
+
headers,
|
|
194
|
+
body: JSON.stringify({ secret: this.config.secret }),
|
|
195
|
+
});
|
|
196
|
+
console.log('[ClientRegistration] Unregistered from main server');
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
console.error('[ClientRegistration] Failed to unregister:', error);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Check if we're running as a client (registered with main server)
|
|
205
|
+
*/
|
|
206
|
+
isClient() {
|
|
207
|
+
return this.config !== null;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Get main server URL (if we're a client)
|
|
211
|
+
*/
|
|
212
|
+
getMainServerUrl() {
|
|
213
|
+
return this.config?.mainServerUrl || null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Global singleton
|
|
217
|
+
exports.clientRegistration = new ClientRegistration();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cluster Secret
|
|
3
|
+
*
|
|
4
|
+
* Shared secret for inter-server auth in client mode.
|
|
5
|
+
* Stored at ~/.nebula/cluster.json with 0600 perms.
|
|
6
|
+
*/
|
|
7
|
+
export declare function readClusterSecret(): string | null;
|
|
8
|
+
export declare function writeClusterSecret(secret: string): void;
|
|
9
|
+
export declare function getOrCreateClusterSecret({ allowCreate }: {
|
|
10
|
+
allowCreate: boolean;
|
|
11
|
+
}): string | null;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cluster Secret
|
|
4
|
+
*
|
|
5
|
+
* Shared secret for inter-server auth in client mode.
|
|
6
|
+
* Stored at ~/.nebula/cluster.json with 0600 perms.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.readClusterSecret = readClusterSecret;
|
|
43
|
+
exports.writeClusterSecret = writeClusterSecret;
|
|
44
|
+
exports.getOrCreateClusterSecret = getOrCreateClusterSecret;
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const os = __importStar(require("os"));
|
|
48
|
+
const crypto_1 = require("crypto");
|
|
49
|
+
const NEBULA_DIR = path.join(os.homedir(), '.nebula');
|
|
50
|
+
const CLUSTER_CONFIG_FILE = path.join(NEBULA_DIR, 'cluster.json');
|
|
51
|
+
function ensureNebulaDir() {
|
|
52
|
+
if (!fs.existsSync(NEBULA_DIR)) {
|
|
53
|
+
fs.mkdirSync(NEBULA_DIR, { recursive: true, mode: 0o700 });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function readClusterSecret() {
|
|
57
|
+
try {
|
|
58
|
+
if (!fs.existsSync(CLUSTER_CONFIG_FILE)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const data = fs.readFileSync(CLUSTER_CONFIG_FILE, 'utf-8');
|
|
62
|
+
const parsed = JSON.parse(data);
|
|
63
|
+
return parsed.secret || null;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function writeClusterSecret(secret) {
|
|
70
|
+
ensureNebulaDir();
|
|
71
|
+
const payload = {
|
|
72
|
+
secret,
|
|
73
|
+
createdAt: Date.now(),
|
|
74
|
+
};
|
|
75
|
+
fs.writeFileSync(CLUSTER_CONFIG_FILE, JSON.stringify(payload, null, 2), {
|
|
76
|
+
mode: 0o600,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function getOrCreateClusterSecret({ allowCreate }) {
|
|
80
|
+
const existing = readClusterSecret();
|
|
81
|
+
if (existing) {
|
|
82
|
+
return existing;
|
|
83
|
+
}
|
|
84
|
+
if (!allowCreate) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const secret = (0, crypto_1.randomBytes)(32).toString('hex');
|
|
88
|
+
writeClusterSecret(secret);
|
|
89
|
+
return secret;
|
|
90
|
+
}
|