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.
Files changed (97) hide show
  1. package/README.md +222 -0
  2. package/bin/nebula-notebook.js +33 -0
  3. package/dist/assets/index-C1h_sArD.css +32 -0
  4. package/dist/assets/index-CDSTBon8.js +658 -0
  5. package/dist/favicon.svg +11 -0
  6. package/dist/index.html +73 -0
  7. package/node-server/dist/app.d.ts +5 -0
  8. package/node-server/dist/app.js +38 -0
  9. package/node-server/dist/auth/auth-middleware.d.ts +24 -0
  10. package/node-server/dist/auth/auth-middleware.js +276 -0
  11. package/node-server/dist/auth/auth-service.d.ts +84 -0
  12. package/node-server/dist/auth/auth-service.js +265 -0
  13. package/node-server/dist/auth/index.d.ts +3 -0
  14. package/node-server/dist/auth/index.js +8 -0
  15. package/node-server/dist/cluster/client-registration.d.ts +43 -0
  16. package/node-server/dist/cluster/client-registration.js +217 -0
  17. package/node-server/dist/cluster/cluster-secret.d.ts +11 -0
  18. package/node-server/dist/cluster/cluster-secret.js +90 -0
  19. package/node-server/dist/cluster/kernel-proxy.d.ts +100 -0
  20. package/node-server/dist/cluster/kernel-proxy.js +361 -0
  21. package/node-server/dist/cluster/server-registry.d.ts +109 -0
  22. package/node-server/dist/cluster/server-registry.js +217 -0
  23. package/node-server/dist/config/output-limits.d.ts +7 -0
  24. package/node-server/dist/config/output-limits.js +10 -0
  25. package/node-server/dist/discovery/discovery-service.d.ts +198 -0
  26. package/node-server/dist/discovery/discovery-service.js +811 -0
  27. package/node-server/dist/discovery/index.d.ts +5 -0
  28. package/node-server/dist/discovery/index.js +21 -0
  29. package/node-server/dist/discovery/types.d.ts +48 -0
  30. package/node-server/dist/discovery/types.js +24 -0
  31. package/node-server/dist/fs/fs-service.d.ts +218 -0
  32. package/node-server/dist/fs/fs-service.js +1422 -0
  33. package/node-server/dist/fs/index.d.ts +5 -0
  34. package/node-server/dist/fs/index.js +21 -0
  35. package/node-server/dist/fs/types.d.ts +132 -0
  36. package/node-server/dist/fs/types.js +5 -0
  37. package/node-server/dist/index.d.ts +13 -0
  38. package/node-server/dist/index.js +556 -0
  39. package/node-server/dist/kernel/default-kernel.d.ts +5 -0
  40. package/node-server/dist/kernel/default-kernel.js +138 -0
  41. package/node-server/dist/kernel/index.d.ts +7 -0
  42. package/node-server/dist/kernel/index.js +23 -0
  43. package/node-server/dist/kernel/kernel-service.d.ts +290 -0
  44. package/node-server/dist/kernel/kernel-service.js +1714 -0
  45. package/node-server/dist/kernel/kernelspec.d.ts +29 -0
  46. package/node-server/dist/kernel/kernelspec.js +210 -0
  47. package/node-server/dist/kernel/session-store.d.ts +87 -0
  48. package/node-server/dist/kernel/session-store.js +303 -0
  49. package/node-server/dist/kernel/types.d.ts +143 -0
  50. package/node-server/dist/kernel/types.js +17 -0
  51. package/node-server/dist/llm/index.d.ts +5 -0
  52. package/node-server/dist/llm/index.js +21 -0
  53. package/node-server/dist/llm/llm-service.d.ts +77 -0
  54. package/node-server/dist/llm/llm-service.js +454 -0
  55. package/node-server/dist/llm/types.d.ts +40 -0
  56. package/node-server/dist/llm/types.js +15 -0
  57. package/node-server/dist/notebook/cell-metadata.d.ts +27 -0
  58. package/node-server/dist/notebook/cell-metadata.js +76 -0
  59. package/node-server/dist/notebook/headless-handler.d.ts +127 -0
  60. package/node-server/dist/notebook/headless-handler.js +1530 -0
  61. package/node-server/dist/notebook/notebook-websocket.d.ts +12 -0
  62. package/node-server/dist/notebook/notebook-websocket.js +103 -0
  63. package/node-server/dist/notebook/operation-router.d.ts +115 -0
  64. package/node-server/dist/notebook/operation-router.js +641 -0
  65. package/node-server/dist/notebook/undoRedoManager.d.ts +194 -0
  66. package/node-server/dist/notebook/undoRedoManager.js +558 -0
  67. package/node-server/dist/output/display-data.d.ts +14 -0
  68. package/node-server/dist/output/display-data.js +134 -0
  69. package/node-server/dist/resources/resource-service.d.ts +69 -0
  70. package/node-server/dist/resources/resource-service.js +363 -0
  71. package/node-server/dist/routes/auth.d.ts +5 -0
  72. package/node-server/dist/routes/auth.js +61 -0
  73. package/node-server/dist/routes/cluster.d.ts +7 -0
  74. package/node-server/dist/routes/cluster.js +94 -0
  75. package/node-server/dist/routes/fs.d.ts +7 -0
  76. package/node-server/dist/routes/fs.js +392 -0
  77. package/node-server/dist/routes/kernel.d.ts +13 -0
  78. package/node-server/dist/routes/kernel.js +637 -0
  79. package/node-server/dist/routes/llm.d.ts +8 -0
  80. package/node-server/dist/routes/llm.js +105 -0
  81. package/node-server/dist/routes/notebook.d.ts +10 -0
  82. package/node-server/dist/routes/notebook.js +335 -0
  83. package/node-server/dist/routes/python.d.ts +8 -0
  84. package/node-server/dist/routes/python.js +187 -0
  85. package/node-server/dist/routes/resources.d.ts +7 -0
  86. package/node-server/dist/routes/resources.js +77 -0
  87. package/node-server/dist/scripts/show-auth-qr.d.ts +1 -0
  88. package/node-server/dist/scripts/show-auth-qr.js +81 -0
  89. package/node-server/dist/terminal/pty-manager.d.ts +100 -0
  90. package/node-server/dist/terminal/pty-manager.js +246 -0
  91. package/node-server/dist/terminal/server.d.ts +19 -0
  92. package/node-server/dist/terminal/server.js +254 -0
  93. package/node-server/dist/terminal/types.d.ts +50 -0
  94. package/node-server/dist/terminal/types.js +9 -0
  95. package/node-server/package.json +45 -0
  96. package/package.json +99 -0
  97. 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,3 @@
1
+ export { authService } from './auth-service';
2
+ export type { AuthConfig, AuthStatus, VerifyResult, JWTPayload } from './auth-service';
3
+ export { authMiddleware, authWebSocketMiddleware } from './auth-middleware';
@@ -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
+ }