langmart-gateway-type3 3.0.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 (75) hide show
  1. package/.env.example +29 -0
  2. package/README.md +480 -0
  3. package/dist/bash-tools.d.ts +56 -0
  4. package/dist/bash-tools.d.ts.map +1 -0
  5. package/dist/bash-tools.js +188 -0
  6. package/dist/bash-tools.js.map +1 -0
  7. package/dist/core-tools.d.ts +94 -0
  8. package/dist/core-tools.d.ts.map +1 -0
  9. package/dist/core-tools.js +694 -0
  10. package/dist/core-tools.js.map +1 -0
  11. package/dist/debug-utils.d.ts +22 -0
  12. package/dist/debug-utils.d.ts.map +1 -0
  13. package/dist/debug-utils.js +37 -0
  14. package/dist/debug-utils.js.map +1 -0
  15. package/dist/devops-tools.d.ts +147 -0
  16. package/dist/devops-tools.d.ts.map +1 -0
  17. package/dist/devops-tools.js +718 -0
  18. package/dist/devops-tools.js.map +1 -0
  19. package/dist/gateway-config.d.ts +56 -0
  20. package/dist/gateway-config.d.ts.map +1 -0
  21. package/dist/gateway-config.js +198 -0
  22. package/dist/gateway-config.js.map +1 -0
  23. package/dist/gateway-mode.d.ts +58 -0
  24. package/dist/gateway-mode.d.ts.map +1 -0
  25. package/dist/gateway-mode.js +240 -0
  26. package/dist/gateway-mode.js.map +1 -0
  27. package/dist/gateway-server.d.ts +208 -0
  28. package/dist/gateway-server.d.ts.map +1 -0
  29. package/dist/gateway-server.js +1811 -0
  30. package/dist/gateway-server.js.map +1 -0
  31. package/dist/headless-session.d.ts +192 -0
  32. package/dist/headless-session.d.ts.map +1 -0
  33. package/dist/headless-session.js +584 -0
  34. package/dist/headless-session.js.map +1 -0
  35. package/dist/index-server.d.ts +4 -0
  36. package/dist/index-server.d.ts.map +1 -0
  37. package/dist/index-server.js +129 -0
  38. package/dist/index-server.js.map +1 -0
  39. package/dist/index.d.ts +6 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +101 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/key-vault.d.ts +102 -0
  44. package/dist/key-vault.d.ts.map +1 -0
  45. package/dist/key-vault.js +365 -0
  46. package/dist/key-vault.js.map +1 -0
  47. package/dist/local-vault.d.ts +195 -0
  48. package/dist/local-vault.d.ts.map +1 -0
  49. package/dist/local-vault.js +571 -0
  50. package/dist/local-vault.js.map +1 -0
  51. package/dist/marketplace-tools.d.ts +104 -0
  52. package/dist/marketplace-tools.d.ts.map +1 -0
  53. package/dist/marketplace-tools.js +2846 -0
  54. package/dist/marketplace-tools.js.map +1 -0
  55. package/dist/mcp-manager.d.ts +114 -0
  56. package/dist/mcp-manager.d.ts.map +1 -0
  57. package/dist/mcp-manager.js +338 -0
  58. package/dist/mcp-manager.js.map +1 -0
  59. package/dist/web-tools.d.ts +86 -0
  60. package/dist/web-tools.d.ts.map +1 -0
  61. package/dist/web-tools.js +431 -0
  62. package/dist/web-tools.js.map +1 -0
  63. package/dist/websocket-handler.d.ts +131 -0
  64. package/dist/websocket-handler.d.ts.map +1 -0
  65. package/dist/websocket-handler.js +596 -0
  66. package/dist/websocket-handler.js.map +1 -0
  67. package/dist/welcome-pages.d.ts +6 -0
  68. package/dist/welcome-pages.d.ts.map +1 -0
  69. package/dist/welcome-pages.js +200 -0
  70. package/dist/welcome-pages.js.map +1 -0
  71. package/package.json +168 -0
  72. package/scripts/install-remote.sh +282 -0
  73. package/scripts/start.sh +85 -0
  74. package/scripts/status.sh +79 -0
  75. package/scripts/stop.sh +67 -0
@@ -0,0 +1,1811 @@
1
+ "use strict";
2
+ // File: gateway-type3/gateway-server.ts
3
+ // Gateway Type 3 - Seller-Managed Gateway Server with Local Vault
4
+ // Seller deploys this on their own infrastructure and manages provider credentials locally
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.Type3GatewayServer = void 0;
10
+ const ws_1 = __importDefault(require("ws"));
11
+ const express_1 = __importDefault(require("express"));
12
+ const uuid_1 = require("uuid");
13
+ const axios_1 = __importDefault(require("axios"));
14
+ const events_1 = require("events");
15
+ const local_vault_1 = require("./local-vault");
16
+ const gateway_config_1 = require("./gateway-config");
17
+ const headless_session_1 = require("./headless-session");
18
+ const gateway_mode_1 = require("./gateway-mode");
19
+ /**
20
+ * Type 3 Gateway Server
21
+ * Seller-managed gateway deployed on seller's infrastructure
22
+ *
23
+ * Key Differences from Type 2:
24
+ * - Uses local vault for provider credentials (NOT central database)
25
+ * - Seller manages their own API keys
26
+ * - More privacy and control for sellers
27
+ *
28
+ * Similarities to Type 2:
29
+ * - Same WebSocket communication protocol with Type 1
30
+ * - Same request/response format
31
+ * - Same registration and heartbeat mechanisms
32
+ */
33
+ class Type3GatewayServer extends events_1.EventEmitter {
34
+ constructor(config) {
35
+ super();
36
+ this.wss = null;
37
+ this.marketplaceWs = null;
38
+ this.activeRequests = new Map();
39
+ this.healthCheckInterval = null;
40
+ this.currentVersion = '3.0.0';
41
+ this.managementServer = null;
42
+ this.apiKey = ''; // API key for authentication
43
+ this.isAuthenticated = false;
44
+ this.currentUser = null; // Authenticated user info
45
+ this.reconnectInterval = null;
46
+ this.reconnectAttempts = 0;
47
+ this.maxReconnectAttempts = 0; // 0 = infinite
48
+ this.reconnectDelay = 15000; // 15 seconds
49
+ this.isShuttingDown = false;
50
+ this.headlessSessionManager = null; // For remote DevOps sessions
51
+ this.port = config.port;
52
+ this.marketplaceUrl = config.marketplaceUrl || 'wss://control.marketplace.ai';
53
+ this.marketplaceApiUrl = config.marketplaceUrl?.replace('wss://', 'https://').replace('control.', 'api.') || 'https://api.marketplace.ai';
54
+ // Set gateway mode (defaults to FULL for backward compatibility)
55
+ this.gatewayMode = config.mode ?? gateway_mode_1.GatewayMode.FULL;
56
+ this.enableRemoteLLMSession = config.enableRemoteLLMSession ?? true;
57
+ this.enableLLMRouting = config.enableLLMRouting ?? true;
58
+ const modeNames = { [gateway_mode_1.GatewayMode.REMOTE_LLM_SESSION_ONLY]: 'Remote LLM Session Only', [gateway_mode_1.GatewayMode.LLM_ROUTING_ONLY]: 'LLM Routing Only', [gateway_mode_1.GatewayMode.FULL]: 'Full' };
59
+ console.log(`[Gateway Type 3] Operating in ${modeNames[this.gatewayMode]} mode`);
60
+ console.log(`[Gateway Type 3] Remote LLM Sessions: ${this.enableRemoteLLMSession ? '✅ Enabled' : '❌ Disabled'}`);
61
+ console.log(`[Gateway Type 3] LLM Routing: ${this.enableLLMRouting ? '✅ Enabled' : '❌ Disabled'}`);
62
+ // Use persistent configuration manager for gateway and instance IDs
63
+ console.log('[Gateway Type 3] Initializing gateway configuration...');
64
+ const configManager = new gateway_config_1.GatewayConfigManager(config.configPath);
65
+ const gatewayConfig = configManager.getOrCreateConfig();
66
+ // Use config-based IDs with datetime seeding (persists across restarts)
67
+ this.instanceId = gatewayConfig.gateway_id;
68
+ this.nodeId = `node-${this.instanceId}-${process.pid}`;
69
+ console.log(`[Gateway Type 3] Using Gateway ID: ${this.instanceId}`);
70
+ console.log(`[Gateway Type 3] Using Node ID: ${this.nodeId}`);
71
+ this.apiKey = config.apiKey || '';
72
+ // Initialize local vault - KEY DIFFERENCE from Type 2
73
+ this.vault = new local_vault_1.LocalVault({
74
+ vaultPath: config.vaultPath,
75
+ masterPassword: config.vaultPassword
76
+ });
77
+ // Initialize management REST API
78
+ this.managementApp = (0, express_1.default)();
79
+ this.managementApp.use(express_1.default.json());
80
+ this.setupManagementEndpoints();
81
+ this.metrics = {
82
+ startTime: Date.now(),
83
+ requestsHandled: 0,
84
+ requestsActive: 0,
85
+ errorCount: 0,
86
+ avgLatency: 0,
87
+ lastHealthCheck: new Date()
88
+ };
89
+ }
90
+ /**
91
+ * Start the Type 3 gateway server
92
+ */
93
+ async start() {
94
+ console.log(`[${this.nodeId}] Starting Type 3 Gateway Server on port ${this.port}`);
95
+ // 1. Start management REST API server for CLI
96
+ await this.startManagementServer();
97
+ // 2. Connect to marketplace control plane (primary function)
98
+ await this.connectToMarketplace();
99
+ // 3. Load credentials from local vault (only if LLM routing is enabled)
100
+ if (this.enableLLMRouting) {
101
+ await this.loadCredentials();
102
+ }
103
+ // 4. Initialize session manager for remote sessions (only if remote sessions enabled)
104
+ if (this.enableRemoteLLMSession) {
105
+ this.initializeHeadlessSessionManager();
106
+ console.log(`[${this.nodeId}] Remote session manager enabled`);
107
+ }
108
+ // 5. Start health monitoring
109
+ this.startHealthMonitoring();
110
+ // 6. Register with marketplace (includes tool discovery)
111
+ await this.registerGateway();
112
+ console.log(`[${this.nodeId}] Type 3 Gateway ready and registered`);
113
+ console.log(`[${this.nodeId}] Management API: http://localhost:${this.port}`);
114
+ }
115
+ /**
116
+ * Initialize the headless session manager for remote DevOps automation
117
+ */
118
+ initializeHeadlessSessionManager() {
119
+ this.headlessSessionManager = new headless_session_1.HeadlessSessionManager({
120
+ gatewayId: this.instanceId,
121
+ vault: this.vault,
122
+ marketplaceUrl: this.marketplaceUrl,
123
+ apiKey: this.apiKey
124
+ });
125
+ // Listen for session events
126
+ this.headlessSessionManager.on('session_started', (data) => {
127
+ console.log(`[${this.nodeId}] Headless session started: ${data.sessionId}`);
128
+ });
129
+ this.headlessSessionManager.on('session_completed', (data) => {
130
+ console.log(`[${this.nodeId}] Headless session completed: ${data.sessionId}`);
131
+ });
132
+ this.headlessSessionManager.on('session_timeout', (data) => {
133
+ console.log(`[${this.nodeId}] Headless session timed out: ${data.sessionId}`);
134
+ });
135
+ console.log(`[${this.nodeId}] Headless session manager initialized`);
136
+ }
137
+ /**
138
+ * Setup management REST API endpoints for CLI
139
+ */
140
+ setupManagementEndpoints() {
141
+ // GET /status - Get running status
142
+ this.managementApp.get('/status', (req, res) => {
143
+ const uptime = Date.now() - this.metrics.startTime;
144
+ const modeNames = {
145
+ [gateway_mode_1.GatewayMode.REMOTE_LLM_SESSION_ONLY]: 'Remote LLM Session Only',
146
+ [gateway_mode_1.GatewayMode.LLM_ROUTING_ONLY]: 'LLM Routing Only',
147
+ [gateway_mode_1.GatewayMode.FULL]: 'Full'
148
+ };
149
+ res.json({
150
+ status: 'running',
151
+ version: this.currentVersion,
152
+ instance_id: this.instanceId,
153
+ node_id: this.nodeId,
154
+ uptime_ms: uptime,
155
+ uptime_human: this.formatUptime(uptime),
156
+ marketplace_connected: this.marketplaceWs?.readyState === ws_1.default.OPEN,
157
+ marketplace_authenticated: this.isAuthenticated,
158
+ mode: {
159
+ value: this.gatewayMode,
160
+ name: modeNames[this.gatewayMode],
161
+ remote_llm_session_enabled: this.enableRemoteLLMSession,
162
+ llm_routing_enabled: this.enableLLMRouting
163
+ },
164
+ metrics: {
165
+ requests_handled: this.metrics.requestsHandled,
166
+ requests_active: this.metrics.requestsActive,
167
+ error_count: this.metrics.errorCount,
168
+ avg_latency_ms: this.metrics.avgLatency
169
+ },
170
+ credentials: this.enableLLMRouting ? {
171
+ connections_count: this.vault.listAccessPoints().length,
172
+ connection_ids: this.vault.listAccessPoints()
173
+ } : {
174
+ connections_count: 0,
175
+ connection_ids: [],
176
+ note: 'LLM routing disabled in current mode'
177
+ }
178
+ });
179
+ });
180
+ // POST /shutdown - Gracefully shutdown
181
+ this.managementApp.post('/shutdown', async (req, res) => {
182
+ console.log(`[${this.nodeId}] Shutdown requested via management API`);
183
+ res.json({ status: 'shutting_down', message: 'Gateway shutdown initiated' });
184
+ // Shutdown after sending response
185
+ setTimeout(async () => {
186
+ await this.gracefulShutdown();
187
+ process.exit(0);
188
+ }, 500);
189
+ });
190
+ // GET /providers - List configured providers
191
+ this.managementApp.get('/providers', (req, res) => {
192
+ const accessPoints = this.vault.listAccessPoints();
193
+ res.json({
194
+ providers: accessPoints.map(apId => ({
195
+ connection_id: apId,
196
+ has_credential: this.vault.hasCredential(apId)
197
+ }))
198
+ });
199
+ });
200
+ // GET /providers/:id/api-key - Retrieve API key from vault
201
+ this.managementApp.get('/providers/:id/api-key', (req, res) => {
202
+ try {
203
+ const connectionId = req.params.id;
204
+ if (!this.vault.hasCredential(connectionId)) {
205
+ return res.status(404).json({
206
+ error: 'No API key found in vault for this connection',
207
+ code: 'not_found'
208
+ });
209
+ }
210
+ const apiKey = this.vault.getCredential(connectionId);
211
+ if (!apiKey) {
212
+ return res.status(404).json({
213
+ error: 'Failed to retrieve API key from vault',
214
+ code: 'vault_error'
215
+ });
216
+ }
217
+ res.json({
218
+ success: true,
219
+ connection_id: connectionId,
220
+ api_key: apiKey
221
+ });
222
+ }
223
+ catch (error) {
224
+ console.error(`[${this.nodeId}] Error retrieving API key:`, error.message);
225
+ res.status(500).json({ error: error.message });
226
+ }
227
+ });
228
+ // POST /providers - Add provider credential
229
+ this.managementApp.post('/providers', async (req, res) => {
230
+ try {
231
+ const { connection_id, api_key, provider_name, description } = req.body;
232
+ if (!connection_id || !api_key) {
233
+ return res.status(400).json({
234
+ error: 'Missing required fields: connection_id, api_key'
235
+ });
236
+ }
237
+ await this.vault.setCredential(connection_id, api_key, provider_name, description);
238
+ console.log(`[${this.nodeId}] Added provider credential via management API: ${connection_id}`);
239
+ res.json({
240
+ status: 'success',
241
+ message: 'Provider credential added',
242
+ connection_id
243
+ });
244
+ }
245
+ catch (error) {
246
+ console.error(`[${this.nodeId}] Error adding provider:`, error.message);
247
+ res.status(500).json({ error: error.message });
248
+ }
249
+ });
250
+ // DELETE /providers/:id - Remove provider credential
251
+ this.managementApp.delete('/providers/:id', async (req, res) => {
252
+ try {
253
+ const accessPointId = req.params.id;
254
+ if (!this.vault.hasCredential(accessPointId)) {
255
+ return res.status(404).json({
256
+ error: 'Provider not found'
257
+ });
258
+ }
259
+ await this.vault.removeCredential(accessPointId);
260
+ console.log(`[${this.nodeId}] Removed provider credential via management API: ${accessPointId}`);
261
+ res.json({
262
+ status: 'success',
263
+ message: 'Provider credential removed',
264
+ connection_id: accessPointId
265
+ });
266
+ }
267
+ catch (error) {
268
+ console.error(`[${this.nodeId}] Error removing provider:`, error.message);
269
+ res.status(500).json({ error: error.message });
270
+ }
271
+ });
272
+ // POST /providers/:id/test - Test provider connection
273
+ this.managementApp.post('/providers/:id/test', async (req, res) => {
274
+ try {
275
+ const connectionId = req.params.id;
276
+ // Check if credential exists in vault
277
+ if (!this.vault.hasCredential(connectionId)) {
278
+ return res.status(404).json({
279
+ error: 'No API key found in local vault for this connection',
280
+ code: 'missing_api_key'
281
+ });
282
+ }
283
+ // Get API key from vault
284
+ const apiKey = this.vault.getCredential(connectionId);
285
+ if (!apiKey) {
286
+ return res.status(404).json({
287
+ error: 'Failed to retrieve API key from vault',
288
+ code: 'vault_error'
289
+ });
290
+ }
291
+ // Query marketplace for connection details
292
+ const marketplaceUrl = this.marketplaceUrl.replace('ws://', 'http://').replace('wss://', 'https://');
293
+ const connectionResponse = await axios_1.default.get(`${marketplaceUrl}/api/connections`, {
294
+ headers: { 'Authorization': `Bearer ${this.apiKey}` },
295
+ timeout: 5000
296
+ });
297
+ const connection = connectionResponse.data.connections?.find((c) => c.id === connectionId);
298
+ if (!connection) {
299
+ return res.status(404).json({
300
+ error: 'Connection not found in marketplace',
301
+ code: 'connection_not_found'
302
+ });
303
+ }
304
+ // Determine test endpoint - provider.key is nested in the response
305
+ const providerKey = connection.provider?.key || 'openai';
306
+ const providerName = connection.provider?.name || 'Unknown';
307
+ const endpointType = connection.endpoint_type || 'openai-v1';
308
+ const testUrl = this.getTestUrl(providerKey, connection.endpoint_url, endpointType);
309
+ console.log(`[${this.nodeId}] Testing ${providerName} at ${testUrl}`);
310
+ // Make test request
311
+ const response = await axios_1.default.get(testUrl, {
312
+ headers: {
313
+ 'Authorization': `Bearer ${apiKey.trim()}`,
314
+ 'Content-Type': 'application/json'
315
+ },
316
+ timeout: 10000,
317
+ validateStatus: (status) => status < 500
318
+ });
319
+ // Success
320
+ if (response.status >= 200 && response.status < 300) {
321
+ return res.json({
322
+ success: true,
323
+ message: 'Connection test successful',
324
+ provider: providerName,
325
+ status: response.status,
326
+ models_available: Array.isArray(response.data?.data) ? response.data.data.length : undefined
327
+ });
328
+ }
329
+ // Auth error
330
+ if (response.status === 401 || response.status === 403) {
331
+ return res.status(400).json({
332
+ success: false,
333
+ error: 'Authentication failed - API key may be invalid or expired',
334
+ code: 'auth_failed',
335
+ status: response.status
336
+ });
337
+ }
338
+ // Other error
339
+ return res.status(400).json({
340
+ success: false,
341
+ error: `Provider returned error: ${response.statusText}`,
342
+ code: 'provider_error',
343
+ status: response.status
344
+ });
345
+ }
346
+ catch (error) {
347
+ if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
348
+ return res.status(400).json({
349
+ success: false,
350
+ error: 'Cannot reach provider endpoint - check endpoint URL',
351
+ code: 'connection_failed',
352
+ details: error.message
353
+ });
354
+ }
355
+ if (error.response) {
356
+ const status = error.response.status;
357
+ if (status === 401 || status === 403) {
358
+ return res.status(400).json({
359
+ success: false,
360
+ error: 'Authentication failed - API key may be invalid',
361
+ code: 'auth_failed',
362
+ status
363
+ });
364
+ }
365
+ else {
366
+ return res.status(400).json({
367
+ success: false,
368
+ error: error.response.data?.error?.message || 'Provider API error',
369
+ code: 'provider_error',
370
+ status
371
+ });
372
+ }
373
+ }
374
+ console.error(`[${this.nodeId}] Error testing provider:`, error.message);
375
+ return res.status(500).json({ error: error.message });
376
+ }
377
+ });
378
+ // POST /providers/:id/update-key - Update/add provider API key in vault
379
+ this.managementApp.post('/providers/:id/update-key', async (req, res) => {
380
+ try {
381
+ const connectionId = req.params.id;
382
+ const { api_key } = req.body;
383
+ if (!api_key) {
384
+ return res.status(400).json({
385
+ error: 'Missing required field: api_key'
386
+ });
387
+ }
388
+ // Store the API key in the vault
389
+ await this.vault.setCredential(connectionId, api_key, 'provider', `Connection ${connectionId}`);
390
+ console.log(`[${this.nodeId}] Updated API key for connection ${connectionId}`);
391
+ res.json({
392
+ success: true,
393
+ message: 'API key updated successfully',
394
+ connection_id: connectionId
395
+ });
396
+ }
397
+ catch (error) {
398
+ console.error(`[${this.nodeId}] Error updating API key:`, error.message);
399
+ res.status(500).json({ error: error.message });
400
+ }
401
+ });
402
+ // POST /auth - Update authentication API key
403
+ this.managementApp.post('/auth', async (req, res) => {
404
+ try {
405
+ const { api_key } = req.body;
406
+ if (!api_key) {
407
+ return res.status(400).json({
408
+ error: 'Missing required field: api_key'
409
+ });
410
+ }
411
+ // Clear current user information when API key changes
412
+ this.apiKey = api_key;
413
+ this.isAuthenticated = false;
414
+ this.currentUser = null;
415
+ console.log(`[${this.nodeId}] Updated authentication API key via management API`);
416
+ console.log(`[${this.nodeId}] Cleared current user - re-authentication required`);
417
+ res.json({
418
+ status: 'success',
419
+ message: 'Authentication API key updated. Gateway will automatically reconnect.',
420
+ note: 'User authentication will be refreshed on reconnection'
421
+ });
422
+ // Automatically trigger reconnection after response is sent
423
+ setTimeout(async () => {
424
+ console.log(`[${this.nodeId}] Auto-reconnecting with new API key...`);
425
+ await this.connectToMarketplace();
426
+ await this.registerGateway();
427
+ }, 500);
428
+ }
429
+ catch (error) {
430
+ console.error(`[${this.nodeId}] Error updating auth:`, error.message);
431
+ res.status(500).json({ error: error.message });
432
+ }
433
+ });
434
+ // POST /reconnect - Reconnect to marketplace
435
+ this.managementApp.post('/reconnect', async (req, res) => {
436
+ try {
437
+ console.log(`[${this.nodeId}] Reconnect requested via management API`);
438
+ res.json({
439
+ status: 'reconnecting',
440
+ message: 'Disconnecting and reconnecting to marketplace'
441
+ });
442
+ // Reconnect after sending response
443
+ setTimeout(async () => {
444
+ await this.connectToMarketplace();
445
+ await this.registerGateway();
446
+ }, 500);
447
+ }
448
+ catch (error) {
449
+ console.error(`[${this.nodeId}] Error reconnecting:`, error.message);
450
+ res.status(500).json({ error: error.message });
451
+ }
452
+ });
453
+ // GET /health - Health check endpoint
454
+ this.managementApp.get('/health', (req, res) => {
455
+ res.json({
456
+ status: 'healthy',
457
+ timestamp: new Date().toISOString(),
458
+ connected: this.marketplaceWs?.readyState === ws_1.default.OPEN,
459
+ authenticated: this.isAuthenticated,
460
+ current_user: this.currentUser ? {
461
+ user_id: this.currentUser.id,
462
+ user_email: this.currentUser.email,
463
+ organization_id: this.currentUser.organization_id
464
+ } : null
465
+ });
466
+ });
467
+ }
468
+ /**
469
+ * Start management REST API server
470
+ */
471
+ async startManagementServer() {
472
+ return new Promise((resolve, reject) => {
473
+ this.managementServer = this.managementApp.listen(this.port, () => {
474
+ console.log(`[${this.nodeId}] Management API started on port ${this.port}`);
475
+ console.log(`[${this.nodeId}] CLI endpoints available at http://localhost:${this.port}`);
476
+ resolve();
477
+ });
478
+ this.managementServer.on('error', (error) => {
479
+ console.error(`[${this.nodeId}] Management server error:`, error.message);
480
+ reject(error);
481
+ });
482
+ });
483
+ }
484
+ /**
485
+ * Format uptime in human-readable format
486
+ */
487
+ formatUptime(ms) {
488
+ const seconds = Math.floor(ms / 1000);
489
+ const minutes = Math.floor(seconds / 60);
490
+ const hours = Math.floor(minutes / 60);
491
+ const days = Math.floor(hours / 24);
492
+ if (days > 0)
493
+ return `${days}d ${hours % 24}h ${minutes % 60}m`;
494
+ if (hours > 0)
495
+ return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
496
+ if (minutes > 0)
497
+ return `${minutes}m ${seconds % 60}s`;
498
+ return `${seconds}s`;
499
+ }
500
+ /**
501
+ * Connect to marketplace control plane
502
+ */
503
+ async connectToMarketplace() {
504
+ return new Promise((resolve, reject) => {
505
+ if (this.reconnectInterval) {
506
+ clearTimeout(this.reconnectInterval);
507
+ this.reconnectInterval = null;
508
+ }
509
+ if (this.marketplaceWs) {
510
+ this.marketplaceWs.removeAllListeners();
511
+ if (this.marketplaceWs.readyState === ws_1.default.OPEN) {
512
+ this.marketplaceWs.close();
513
+ }
514
+ this.marketplaceWs = null;
515
+ }
516
+ console.log(`[${this.nodeId}] Connecting to marketplace at ${this.marketplaceUrl}...`);
517
+ this.marketplaceWs = new ws_1.default(this.marketplaceUrl, {
518
+ headers: {
519
+ 'X-Gateway-Type': '3',
520
+ 'X-Instance-ID': this.instanceId,
521
+ 'X-Node-ID': this.nodeId,
522
+ 'X-Version': this.currentVersion
523
+ }
524
+ });
525
+ this.marketplaceWs.on('open', () => {
526
+ console.log(`[${this.nodeId}] ✅ Connected to marketplace control plane`);
527
+ this.reconnectAttempts = 0;
528
+ this.isAuthenticated = false;
529
+ resolve();
530
+ });
531
+ this.marketplaceWs.on('message', (data) => {
532
+ this.handleMarketplaceMessage(data.toString());
533
+ });
534
+ this.marketplaceWs.on('error', (error) => {
535
+ console.error(`[${this.nodeId}] ❌ Marketplace connection error:`, error.message);
536
+ this.emit('error', error);
537
+ if (this.reconnectAttempts === 0) {
538
+ reject(error);
539
+ }
540
+ });
541
+ this.marketplaceWs.on('close', (code, reason) => {
542
+ console.log(`[${this.nodeId}] 🔌 Disconnected from marketplace (code: ${code}, reason: ${reason})`);
543
+ this.isAuthenticated = false;
544
+ this.currentUser = null; // Clear user info on disconnect
545
+ this.handleMarketplaceDisconnection();
546
+ });
547
+ });
548
+ }
549
+ /**
550
+ * Load API credentials from local vault
551
+ * KEY DIFFERENCE from Type 2: Reads from local vault instead of receiving from Type 1
552
+ */
553
+ async loadCredentials() {
554
+ console.log(`[${this.nodeId}] Loading credentials from local vault`);
555
+ // Initialize vault from environment if empty
556
+ const accessPoints = this.vault.listAccessPoints();
557
+ if (accessPoints.length === 0) {
558
+ console.log(`[${this.nodeId}] Vault is empty, initializing from environment by connection_id`);
559
+ await this.initializeVaultFromEnvironment();
560
+ }
561
+ const loadedAccessPoints = this.vault.listAccessPoints();
562
+ console.log(`[${this.nodeId}] Loaded ${loadedAccessPoints.length} credentials from vault`);
563
+ console.log(`[${this.nodeId}] Available connection_ids: ${loadedAccessPoints.join(', ')}`);
564
+ }
565
+ /**
566
+ * Initialize vault from environment variables using connection_id from database
567
+ */
568
+ async initializeVaultFromEnvironment() {
569
+ const { Pool } = require('pg');
570
+ const pool = new Pool({
571
+ host: process.env.DB_HOST || 'localhost',
572
+ port: parseInt(process.env.DB_PORT || '5432'),
573
+ database: process.env.DB_NAME || 'langmart',
574
+ user: process.env.DB_USER || 'langmart_admin',
575
+ password: process.env.DB_PASSWORD || 'langmart_secret_2024'
576
+ });
577
+ try {
578
+ // Query access points for this gateway's user
579
+ const result = await pool.query(`
580
+ SELECT ap.id, ap.access_key, p.provider_key
581
+ FROM connections ap
582
+ JOIN providers p ON ap.provider_id = p.id
583
+ WHERE ap.gateway_type = 3
584
+ AND ap.status = 'active'
585
+ `);
586
+ console.log(`[${this.nodeId}] Found ${result.rows.length} Type 3 access points in database`);
587
+ // Map environment variables to connection_ids
588
+ const providers = ['openai', 'anthropic', 'google', 'groq', 'deepseek', 'mistral'];
589
+ let stored = 0;
590
+ for (const row of result.rows) {
591
+ const providerKey = row.provider_key.toLowerCase();
592
+ const envKey = `${providerKey.toUpperCase()}_API_KEY`;
593
+ const apiKey = process.env[envKey];
594
+ if (apiKey && !apiKey.includes('mock')) {
595
+ // Store with provider name for config mapping
596
+ await this.vault.setCredential(row.id, apiKey, providerKey, `${providerKey} access point`);
597
+ console.log(`[${this.nodeId}] Stored ${providerKey} credential as connection_id: ${row.id}`);
598
+ stored++;
599
+ }
600
+ }
601
+ console.log(`[${this.nodeId}] Initialized ${stored} credentials from environment by UUID`);
602
+ }
603
+ catch (error) {
604
+ console.error(`[${this.nodeId}] Failed to initialize vault from environment:`, error.message);
605
+ }
606
+ finally {
607
+ await pool.end();
608
+ }
609
+ }
610
+ /**
611
+ * Register gateway with marketplace
612
+ * Includes tool discovery for remote DevOps sessions (if enabled)
613
+ */
614
+ async registerGateway() {
615
+ if (!this.apiKey) {
616
+ console.error(`[${this.nodeId}] FATAL: Cannot register without API key`);
617
+ console.error(`[${this.nodeId}] Gateway Type 3 requires API key for authentication`);
618
+ await this.gracefulShutdown();
619
+ process.exit(1);
620
+ }
621
+ // Get available tools from session manager (only if remote sessions enabled)
622
+ const availableTools = this.enableRemoteLLMSession ? (this.headlessSessionManager?.getAvailableTools() || []) : [];
623
+ // Get connections list (only if LLM routing is enabled)
624
+ const connections = this.enableLLMRouting ? this.vault.listAccessPoints() : [];
625
+ const modeNames = { [gateway_mode_1.GatewayMode.REMOTE_LLM_SESSION_ONLY]: 'remote_llm_session_only', [gateway_mode_1.GatewayMode.LLM_ROUTING_ONLY]: 'llm_routing_only', [gateway_mode_1.GatewayMode.FULL]: 'full' };
626
+ const registration = {
627
+ event: 'gateway_register',
628
+ gateway_type: 3,
629
+ instance_id: this.instanceId,
630
+ node_id: this.nodeId,
631
+ version: this.currentVersion,
632
+ endpoint: `wss://${this.instanceId}.gateway.seller.local:${this.port}`,
633
+ api_key: this.apiKey,
634
+ // Gateway mode - tells Type 1 what this gateway can handle
635
+ mode: modeNames[this.gatewayMode],
636
+ capabilities: {
637
+ // LLM routing capabilities (only advertised if LLM routing is enabled)
638
+ connections: connections,
639
+ max_concurrent: 100,
640
+ supports_streaming: this.enableLLMRouting,
641
+ supports_functions: this.enableLLMRouting,
642
+ // Remote session capabilities (only advertised if remote sessions enabled)
643
+ headless_inference: this.enableRemoteLLMSession,
644
+ available_tools: availableTools
645
+ },
646
+ metrics: {
647
+ uptime_seconds: Math.floor((Date.now() - this.metrics.startTime) / 1000),
648
+ cpu_usage: this.getCpuUsage(),
649
+ memory_usage_mb: Math.round(process.memoryUsage().heapUsed / 1024 / 1024)
650
+ }
651
+ };
652
+ // Log registration info based on mode
653
+ if (this.enableRemoteLLMSession) {
654
+ console.log(`[${this.nodeId}] Registering with ${availableTools.length} available tools for remote sessions`);
655
+ }
656
+ if (this.enableLLMRouting) {
657
+ console.log(`[${this.nodeId}] Registering with ${connections.length} connections for LLM routing`);
658
+ }
659
+ if (this.marketplaceWs?.readyState === ws_1.default.OPEN) {
660
+ this.marketplaceWs.send(JSON.stringify(registration));
661
+ }
662
+ }
663
+ /**
664
+ * Handle messages from marketplace control plane
665
+ */
666
+ async handleMarketplaceMessage(data) {
667
+ try {
668
+ const message = JSON.parse(data);
669
+ // Gateway Type 1 sends both 'event' and 'type' properties for compatibility
670
+ // Check both to handle all message types
671
+ const eventType = message.event || message.type;
672
+ switch (eventType) {
673
+ case 'register_ack':
674
+ console.log(`[${this.nodeId}] Registration acknowledged by Gateway Type 1`);
675
+ this.isAuthenticated = true;
676
+ // Capture authenticated user information
677
+ if (message.user_id) {
678
+ this.currentUser = {
679
+ id: message.user_id,
680
+ email: message.user_email,
681
+ organization_id: message.organization_id
682
+ };
683
+ console.log(`[${this.nodeId}] Authenticated as user: ${this.currentUser.email} (${this.currentUser.id})`);
684
+ }
685
+ break;
686
+ case 'auth_failed':
687
+ console.error(`[${this.nodeId}] FATAL: Authentication failed - ${message.reason || 'Invalid API key'}`);
688
+ console.error(`[${this.nodeId}] Gateway Type 3 cannot operate without valid authentication`);
689
+ this.isAuthenticated = false;
690
+ this.currentUser = null; // Clear user info on authentication failure
691
+ await this.gracefulShutdown();
692
+ process.exit(1);
693
+ break;
694
+ case 'auth_confirmed':
695
+ // Gateway Type 1 sends auth_confirmed after successful authentication
696
+ console.log(`[${this.nodeId}] Authentication confirmed by Gateway Type 1`);
697
+ this.isAuthenticated = true;
698
+ break;
699
+ case 'heartbeat':
700
+ case 'health_check':
701
+ this.sendHealthStatus();
702
+ break;
703
+ case 'heartbeat_ack':
704
+ // Acknowledge heartbeat response from Type 1 (silently)
705
+ break;
706
+ case 'inference_request':
707
+ // Only handle inference requests if LLM routing is enabled
708
+ if (!this.enableLLMRouting) {
709
+ console.log(`[${this.nodeId}] Rejecting inference request - LLM routing not enabled in this mode`);
710
+ this.sendErrorResponse(message.request_id, 'llm_routing_disabled', 'This gateway is not configured for LLM routing');
711
+ break;
712
+ }
713
+ await this.handleForwardedInferenceRequest(message);
714
+ break;
715
+ case 'http_request':
716
+ // Only handle HTTP requests if LLM routing is enabled
717
+ if (!this.enableLLMRouting) {
718
+ console.log(`[${this.nodeId}] Rejecting HTTP request - LLM routing not enabled in this mode`);
719
+ this.sendErrorResponse(message.request_id, 'llm_routing_disabled', 'This gateway is not configured for LLM routing');
720
+ break;
721
+ }
722
+ await this.handleHttpRequest(message);
723
+ break;
724
+ // ============= REMOTE SESSION EVENTS =============
725
+ case 'session_start':
726
+ // Only handle session requests if remote sessions enabled
727
+ if (!this.enableRemoteLLMSession) {
728
+ console.log(`[${this.nodeId}] Rejecting session start - remote sessions not enabled in this mode`);
729
+ this.sendSessionError(message.request_id, 'remote_session_disabled', 'This gateway is not configured for remote sessions');
730
+ break;
731
+ }
732
+ await this.handleSessionStart(message);
733
+ break;
734
+ case 'session_message':
735
+ if (!this.enableRemoteLLMSession) {
736
+ console.log(`[${this.nodeId}] Rejecting session message - remote sessions not enabled in this mode`);
737
+ this.sendSessionError(message.request_id, 'remote_session_disabled', 'This gateway is not configured for remote sessions');
738
+ break;
739
+ }
740
+ await this.handleSessionMessage(message);
741
+ break;
742
+ case 'session_cancel':
743
+ if (!this.enableRemoteLLMSession) {
744
+ break; // Silently ignore
745
+ }
746
+ await this.handleSessionCancel(message);
747
+ break;
748
+ case 'get_tools':
749
+ // Always respond to get_tools, but return empty array if remote sessions disabled
750
+ await this.handleGetTools(message);
751
+ break;
752
+ case 'execute_script':
753
+ // Handle script execution request from Type 1 (for remote server scripts)
754
+ if (!this.enableRemoteLLMSession) {
755
+ console.log(`[${this.nodeId}] Rejecting execute_script - remote sessions not enabled in this mode`);
756
+ this.sendScriptError(message.requestId, 'remote_session_disabled', 'This gateway is not configured for remote operations');
757
+ break;
758
+ }
759
+ await this.handleExecuteScript(message);
760
+ break;
761
+ case 'shutdown':
762
+ await this.gracefulShutdown();
763
+ break;
764
+ default:
765
+ console.log(`[${this.nodeId}] Unknown marketplace event: ${eventType}`);
766
+ }
767
+ }
768
+ catch (error) {
769
+ console.error(`[${this.nodeId}] Marketplace message error:`, error);
770
+ }
771
+ }
772
+ /**
773
+ * Handle inference request forwarded from Gateway Type 1
774
+ */
775
+ async handleForwardedInferenceRequest(message) {
776
+ const requestId = message.request_id || (0, uuid_1.v4)();
777
+ const startTime = Date.now();
778
+ console.log('\n============ GATEWAY TYPE 3 INFERENCE PROCESSING START ============');
779
+ console.log(`[Type3-${this.nodeId}] Step 0: Received forwarded inference request`);
780
+ console.log(`[Type3-${this.nodeId}] Request ID: ${requestId}`);
781
+ this.metrics.requestsActive++;
782
+ try {
783
+ console.log(`[Type3-${this.nodeId}] Step 1: Extracting request parameters`);
784
+ const provider = message.provider || 'openai';
785
+ const endpoint = message.endpoint || '/chat/completions';
786
+ const model = message.model || message.data?.model;
787
+ const messages = message.messages || message.data?.messages;
788
+ const stream = message.stream || message.data?.stream || false;
789
+ const temperature = message.temperature || message.data?.temperature;
790
+ const max_tokens = message.max_tokens || message.data?.max_tokens;
791
+ const tools = message.tools || message.data?.tools;
792
+ const endpointUrl = message.endpoint_url || message.endpointUrl;
793
+ console.log(`[Type3-${this.nodeId}] Step 2: Extracted parameters:`, {
794
+ provider,
795
+ endpoint,
796
+ model,
797
+ hasMessages: !!messages,
798
+ messageCount: messages?.length || 0,
799
+ stream,
800
+ hasTools: !!tools,
801
+ toolsCount: tools?.length || 0
802
+ });
803
+ // KEY FEATURE: Type 3 supports TWO credential sources
804
+ // Priority 1: connections table (from Type 1 message)
805
+ // Priority 2: local vault (indexed by connection_id)
806
+ console.log(`[Type3-${this.nodeId}] Step 3: Looking up provider API key`);
807
+ let apiKey = message.api_key || message.provider_api_key;
808
+ const accessPointId = message.accessPointId; // UUID from Type 1
809
+ if (apiKey) {
810
+ console.log(`[Type3-${this.nodeId}] ✓ Using API key from connections table`);
811
+ console.log(`[Type3-${this.nodeId}] Credential source: connections table (sent by Type 1)`);
812
+ console.log(`[Type3-${this.nodeId}] Access Point ID: ${accessPointId}`);
813
+ console.log(`[Type3-${this.nodeId}] API key:`, {
814
+ provider,
815
+ keyLength: apiKey.length,
816
+ keyPrefix: apiKey.substring(0, 10) + '...'
817
+ });
818
+ }
819
+ else {
820
+ // Fall back to local vault using connection_id
821
+ console.log(`[Type3-${this.nodeId}] No API key in message, checking local vault`);
822
+ if (!accessPointId) {
823
+ console.error(`[Type3-${this.nodeId}] ✗ ERROR: No connection_id provided`);
824
+ throw new Error(`No connection_id available for credential lookup`);
825
+ }
826
+ console.log(`[Type3-${this.nodeId}] Looking up by connection_id: ${accessPointId}`);
827
+ apiKey = this.vault.getCredential(accessPointId);
828
+ if (apiKey) {
829
+ console.log(`[Type3-${this.nodeId}] ✓ Using API key from local vault`);
830
+ console.log(`[Type3-${this.nodeId}] Credential source: local vault`);
831
+ console.log(`[Type3-${this.nodeId}] Access Point ID: ${accessPointId}`);
832
+ console.log(`[Type3-${this.nodeId}] API key:`, {
833
+ provider,
834
+ keyLength: apiKey.length,
835
+ keyPrefix: apiKey.substring(0, 10) + '...'
836
+ });
837
+ }
838
+ else {
839
+ console.error(`[Type3-${this.nodeId}] ✗ ERROR: No API key found for connection_id: ${accessPointId}`);
840
+ console.error(`[Type3-${this.nodeId}] Checked sources:`);
841
+ console.error(`[Type3-${this.nodeId}] 1. connections table (via message): NOT FOUND`);
842
+ console.error(`[Type3-${this.nodeId}] 2. local vault (by connection_id ${accessPointId}): NOT FOUND`);
843
+ console.error(`[Type3-${this.nodeId}] Available connection_ids in vault: ${this.vault.listAccessPoints().join(', ')}`);
844
+ throw new Error(`No API key available. Add credential to local vault using connection_id: ${accessPointId}`);
845
+ }
846
+ }
847
+ // Forward to provider API
848
+ console.log(`[Type3-${this.nodeId}] Step 4: Preparing to forward to provider`);
849
+ let response;
850
+ const requestPayload = {
851
+ model,
852
+ messages,
853
+ temperature,
854
+ max_tokens,
855
+ stream
856
+ };
857
+ if (tools) {
858
+ requestPayload.tools = tools;
859
+ }
860
+ console.log(`[Type3-${this.nodeId}] Step 5: Request payload prepared`);
861
+ if (stream) {
862
+ console.log(`[Type3-${this.nodeId}] Step 6: Forwarding STREAMING request to provider`);
863
+ response = await this.forwardToProviderStreaming(provider, endpoint, apiKey, requestPayload, endpointUrl, requestId);
864
+ }
865
+ else {
866
+ console.log(`[Type3-${this.nodeId}] Step 6: Forwarding NON-STREAMING request to provider`);
867
+ response = await this.forwardToProvider(provider, endpoint, apiKey, requestPayload, endpointUrl);
868
+ }
869
+ if (!stream) {
870
+ console.log(`[Type3-${this.nodeId}] Step 7: Received response from provider`);
871
+ if (this.marketplaceWs && this.marketplaceWs.readyState === ws_1.default.OPEN) {
872
+ const responseMessage = {
873
+ event: 'inference_response',
874
+ request_id: requestId,
875
+ data: response,
876
+ metrics: {
877
+ latency_ms: Date.now() - startTime,
878
+ gateway_type: 3,
879
+ node_id: this.nodeId
880
+ }
881
+ };
882
+ this.marketplaceWs.send(JSON.stringify(responseMessage));
883
+ console.log(`[Type3-${this.nodeId}] Step 8: Response sent successfully`);
884
+ }
885
+ }
886
+ else {
887
+ console.log(`[Type3-${this.nodeId}] Step 7: Streaming response handled via stream events`);
888
+ }
889
+ this.metrics.requestsHandled++;
890
+ console.log(`[Type3-${this.nodeId}] Step 9: Request completed successfully`);
891
+ console.log('============ GATEWAY TYPE 3 INFERENCE PROCESSING END ============\n');
892
+ }
893
+ catch (error) {
894
+ console.error(`[Type3-${this.nodeId}] ERROR in request ${requestId}:`, error.message);
895
+ if (this.marketplaceWs && this.marketplaceWs.readyState === ws_1.default.OPEN) {
896
+ this.marketplaceWs.send(JSON.stringify({
897
+ event: 'inference_error',
898
+ request_id: requestId,
899
+ error: {
900
+ code: error.response?.status || 500,
901
+ message: error.message,
902
+ type: 'provider_error'
903
+ },
904
+ metrics: {
905
+ latency_ms: Date.now() - startTime,
906
+ gateway_type: 3
907
+ }
908
+ }));
909
+ }
910
+ this.metrics.errorCount++;
911
+ }
912
+ finally {
913
+ this.metrics.requestsActive--;
914
+ }
915
+ }
916
+ /**
917
+ * Forward request to provider API (non-streaming)
918
+ */
919
+ async forwardToProvider(provider, endpoint, apiKey, payload, endpointUrl) {
920
+ // Type 3 is a PURE FORWARDER like Type 2
921
+ // Type 1 handles ALL translation
922
+ let url;
923
+ if (endpointUrl) {
924
+ if (endpointUrl.startsWith('http://') || endpointUrl.startsWith('https://')) {
925
+ url = endpointUrl + endpoint;
926
+ }
927
+ else {
928
+ url = endpointUrl + endpoint;
929
+ }
930
+ }
931
+ else {
932
+ console.warn(`[${this.nodeId}] No endpointUrl provided, using generic URL`);
933
+ url = `https://api.${provider}.com${endpoint}`;
934
+ }
935
+ if (provider.toLowerCase() === 'google' || provider.toLowerCase() === 'gemini') {
936
+ url = `${url}?key=${apiKey}`;
937
+ }
938
+ const headers = this.getProviderHeaders(provider, apiKey);
939
+ if (provider.toLowerCase() === 'google' || provider.toLowerCase() === 'gemini') {
940
+ delete headers['x-goog-api-key'];
941
+ delete headers['Authorization'];
942
+ }
943
+ const response = await axios_1.default.post(url, payload, {
944
+ headers,
945
+ timeout: 120000,
946
+ validateStatus: (status) => status < 500
947
+ });
948
+ if (response.headers['x-ratelimit-limit-requests']) {
949
+ this.reportRateLimits(provider, response.headers);
950
+ }
951
+ if (response.status >= 400) {
952
+ const error = new Error(response.data?.error?.message || 'Provider error');
953
+ error.response = { status: response.status, data: response.data };
954
+ throw error;
955
+ }
956
+ return response.data;
957
+ }
958
+ /**
959
+ * Forward streaming request to provider
960
+ */
961
+ async forwardToProviderStreaming(provider, endpoint, apiKey, payload, endpointUrl, requestId) {
962
+ console.log(`[${this.nodeId}] Starting streaming request to provider ${provider}`);
963
+ let url;
964
+ if (endpointUrl) {
965
+ if (endpointUrl.startsWith('http://') || endpointUrl.startsWith('https://')) {
966
+ url = endpointUrl + endpoint;
967
+ }
968
+ else {
969
+ url = endpointUrl + endpoint;
970
+ }
971
+ }
972
+ else {
973
+ url = `https://api.${provider}.com${endpoint}`;
974
+ }
975
+ if (provider.toLowerCase() === 'google' || provider.toLowerCase() === 'gemini') {
976
+ url = `${url}?key=${apiKey}&alt=sse`;
977
+ }
978
+ const headers = this.getProviderHeaders(provider, apiKey);
979
+ if (provider.toLowerCase() === 'google' || provider.toLowerCase() === 'gemini') {
980
+ delete headers['x-goog-api-key'];
981
+ delete headers['Authorization'];
982
+ }
983
+ try {
984
+ const response = await (0, axios_1.default)({
985
+ method: 'POST',
986
+ url,
987
+ headers,
988
+ data: payload,
989
+ responseType: 'stream',
990
+ timeout: 120000
991
+ });
992
+ response.data.on('data', (chunk) => {
993
+ const lines = chunk.toString().split('\n');
994
+ for (const line of lines) {
995
+ if (line.startsWith('data: ')) {
996
+ const data = line.substring(6);
997
+ if (data === '[DONE]') {
998
+ if (this.marketplaceWs && this.marketplaceWs.readyState === ws_1.default.OPEN) {
999
+ this.marketplaceWs.send(JSON.stringify({
1000
+ event: 'stream_end',
1001
+ request_id: requestId,
1002
+ timestamp: new Date().toISOString()
1003
+ }));
1004
+ }
1005
+ }
1006
+ else {
1007
+ try {
1008
+ const parsed = JSON.parse(data);
1009
+ if (this.marketplaceWs && this.marketplaceWs.readyState === ws_1.default.OPEN) {
1010
+ this.marketplaceWs.send(JSON.stringify({
1011
+ event: 'stream_chunk',
1012
+ request_id: requestId,
1013
+ data: parsed,
1014
+ timestamp: new Date().toISOString()
1015
+ }));
1016
+ }
1017
+ }
1018
+ catch (e) {
1019
+ // Skip invalid JSON
1020
+ }
1021
+ }
1022
+ }
1023
+ }
1024
+ });
1025
+ return new Promise((resolve, reject) => {
1026
+ response.data.on('end', () => {
1027
+ console.log(`[${this.nodeId}] Streaming completed for request ${requestId}`);
1028
+ if (this.marketplaceWs && this.marketplaceWs.readyState === ws_1.default.OPEN) {
1029
+ this.marketplaceWs.send(JSON.stringify({
1030
+ event: 'stream_complete',
1031
+ request_id: requestId,
1032
+ stream_completed: true,
1033
+ timestamp: new Date().toISOString()
1034
+ }));
1035
+ }
1036
+ resolve({ stream_completed: true });
1037
+ });
1038
+ response.data.on('error', (error) => {
1039
+ console.error(`[${this.nodeId}] Stream error for request ${requestId}:`, error);
1040
+ if (this.marketplaceWs && this.marketplaceWs.readyState === ws_1.default.OPEN) {
1041
+ this.marketplaceWs.send(JSON.stringify({
1042
+ event: 'stream_error',
1043
+ request_id: requestId,
1044
+ error: {
1045
+ message: error.message,
1046
+ code: 500
1047
+ },
1048
+ timestamp: new Date().toISOString()
1049
+ }));
1050
+ }
1051
+ reject(error);
1052
+ });
1053
+ });
1054
+ }
1055
+ catch (error) {
1056
+ console.error(`[${this.nodeId}] Failed to initiate streaming:`, error);
1057
+ throw error;
1058
+ }
1059
+ }
1060
+ /**
1061
+ * Get provider-specific headers
1062
+ */
1063
+ getProviderHeaders(provider, apiKey) {
1064
+ const baseHeaders = {
1065
+ 'Content-Type': 'application/json',
1066
+ 'User-Agent': `LangMartGateway-Type3/${this.currentVersion}`
1067
+ };
1068
+ switch (provider) {
1069
+ case 'openai':
1070
+ case 'deepseek':
1071
+ case 'groq':
1072
+ case 'mistral':
1073
+ return {
1074
+ ...baseHeaders,
1075
+ 'Authorization': `Bearer ${apiKey}`
1076
+ };
1077
+ case 'anthropic':
1078
+ return {
1079
+ ...baseHeaders,
1080
+ 'x-api-key': apiKey,
1081
+ 'anthropic-version': '2023-06-01'
1082
+ };
1083
+ case 'google':
1084
+ return {
1085
+ ...baseHeaders,
1086
+ 'x-goog-api-key': apiKey
1087
+ };
1088
+ default:
1089
+ return {
1090
+ ...baseHeaders,
1091
+ 'Authorization': `Bearer ${apiKey}`
1092
+ };
1093
+ }
1094
+ }
1095
+ /**
1096
+ * Generic HTTP request forwarder
1097
+ * Type 3 is ONLY a forwarder - it makes HTTP calls and returns RAW responses
1098
+ * Type 1 does all business logic, translation, and storage
1099
+ *
1100
+ * Message format:
1101
+ * {
1102
+ * event: 'http_request',
1103
+ * requestId: 'uuid',
1104
+ * connection_id: 'uuid',
1105
+ * method: 'GET' | 'POST' | etc,
1106
+ * url: 'http://localhost:11434/api/tags',
1107
+ * headers: { ... },
1108
+ * body: { ... } (optional)
1109
+ * }
1110
+ */
1111
+ async handleHttpRequest(message) {
1112
+ const requestId = message.requestId || (0, uuid_1.v4)();
1113
+ const { connection_id, method, url, headers = {}, body } = message;
1114
+ console.log(`[${this.nodeId}] Forwarding HTTP ${method} request to: ${url}`);
1115
+ try {
1116
+ // Get API key from local vault if connection_id provided
1117
+ let requestHeaders = { ...headers };
1118
+ if (connection_id) {
1119
+ const apiKey = this.vault.getCredential(connection_id);
1120
+ if (apiKey) {
1121
+ // Add authorization header if not already present
1122
+ if (!requestHeaders['Authorization'] && !requestHeaders['authorization']) {
1123
+ requestHeaders['Authorization'] = `Bearer ${apiKey}`;
1124
+ }
1125
+ }
1126
+ }
1127
+ // Make HTTP request - GENERIC forwarding
1128
+ const fetchOptions = {
1129
+ method: method,
1130
+ headers: requestHeaders
1131
+ };
1132
+ if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
1133
+ fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
1134
+ if (!requestHeaders['Content-Type']) {
1135
+ requestHeaders['Content-Type'] = 'application/json';
1136
+ }
1137
+ }
1138
+ console.log(`[${this.nodeId}] Making ${method} request to ${url}`);
1139
+ const response = await fetch(url, fetchOptions);
1140
+ const responseText = await response.text();
1141
+ // Try to parse as JSON, fallback to text
1142
+ let responseData;
1143
+ try {
1144
+ responseData = JSON.parse(responseText);
1145
+ }
1146
+ catch {
1147
+ responseData = responseText;
1148
+ }
1149
+ // Send RAW HTTP response back to Type 1
1150
+ const wsResponse = {
1151
+ event: 'http_response',
1152
+ requestId: requestId,
1153
+ success: response.ok,
1154
+ status: response.status,
1155
+ headers: Object.fromEntries(response.headers.entries()),
1156
+ body: responseData, // RAW response body
1157
+ timestamp: new Date().toISOString()
1158
+ };
1159
+ if (this.marketplaceWs && this.marketplaceWs.readyState === ws_1.default.OPEN) {
1160
+ this.marketplaceWs.send(JSON.stringify(wsResponse));
1161
+ console.log(`[${this.nodeId}] Sent HTTP response (${response.status}) to Type 1`);
1162
+ }
1163
+ }
1164
+ catch (error) {
1165
+ console.error(`[${this.nodeId}] HTTP forwarding error:`, error.message);
1166
+ if (this.marketplaceWs && this.marketplaceWs.readyState === ws_1.default.OPEN) {
1167
+ this.marketplaceWs.send(JSON.stringify({
1168
+ event: 'http_response',
1169
+ requestId: requestId,
1170
+ success: false,
1171
+ error: error.message,
1172
+ timestamp: new Date().toISOString()
1173
+ }));
1174
+ }
1175
+ }
1176
+ }
1177
+ /**
1178
+ * Send health status to marketplace
1179
+ */
1180
+ sendHealthStatus() {
1181
+ if (this.marketplaceWs && this.marketplaceWs.readyState === ws_1.default.OPEN) {
1182
+ const heartbeat = {
1183
+ type: 'heartbeat',
1184
+ timestamp: new Date().toISOString()
1185
+ };
1186
+ this.marketplaceWs.send(JSON.stringify(heartbeat));
1187
+ }
1188
+ const status = {
1189
+ event: 'health_status',
1190
+ instance_id: this.instanceId,
1191
+ node_id: this.nodeId,
1192
+ status: this.determineHealthStatus(),
1193
+ metrics: {
1194
+ uptime_seconds: Math.floor((Date.now() - this.metrics.startTime) / 1000),
1195
+ requests_handled: this.metrics.requestsHandled,
1196
+ requests_active: this.metrics.requestsActive,
1197
+ error_rate: this.metrics.errorCount / Math.max(1, this.metrics.requestsHandled),
1198
+ avg_latency_ms: Math.round(this.metrics.avgLatency),
1199
+ cpu_usage_percent: this.getCpuUsage(),
1200
+ memory_usage_mb: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
1201
+ connection_count: this.vault.listAccessPoints().length
1202
+ },
1203
+ timestamp: new Date().toISOString()
1204
+ };
1205
+ if (this.marketplaceWs?.readyState === ws_1.default.OPEN) {
1206
+ this.marketplaceWs.send(JSON.stringify(status));
1207
+ }
1208
+ this.metrics.lastHealthCheck = new Date();
1209
+ }
1210
+ /**
1211
+ * Determine health status
1212
+ */
1213
+ determineHealthStatus() {
1214
+ const errorRate = this.metrics.errorCount / Math.max(1, this.metrics.requestsHandled);
1215
+ if (errorRate > 0.1)
1216
+ return 'unhealthy';
1217
+ if (errorRate > 0.05)
1218
+ return 'degraded';
1219
+ if (this.metrics.requestsActive > 50)
1220
+ return 'overloaded';
1221
+ return 'healthy';
1222
+ }
1223
+ /**
1224
+ * Report rate limits
1225
+ */
1226
+ reportRateLimits(provider, headers) {
1227
+ const rateLimits = {
1228
+ event: 'rate_limits',
1229
+ instance_id: this.instanceId,
1230
+ provider,
1231
+ limits: {
1232
+ rpm: headers['x-ratelimit-limit-requests'],
1233
+ tpm: headers['x-ratelimit-limit-tokens'],
1234
+ remaining: headers['x-ratelimit-remaining-requests'],
1235
+ reset: headers['x-ratelimit-reset-requests']
1236
+ },
1237
+ timestamp: new Date().toISOString()
1238
+ };
1239
+ if (this.marketplaceWs?.readyState === ws_1.default.OPEN) {
1240
+ this.marketplaceWs.send(JSON.stringify(rateLimits));
1241
+ }
1242
+ }
1243
+ /**
1244
+ * Start health monitoring
1245
+ */
1246
+ startHealthMonitoring() {
1247
+ this.healthCheckInterval = setInterval(() => {
1248
+ this.sendHealthStatus();
1249
+ if (this.marketplaceWs?.readyState !== ws_1.default.OPEN) {
1250
+ console.warn(`[${this.nodeId}] Lost connection to marketplace, reconnecting...`);
1251
+ this.connectToMarketplace().catch(console.error);
1252
+ }
1253
+ }, 30000);
1254
+ }
1255
+ /**
1256
+ * Handle marketplace disconnection
1257
+ */
1258
+ handleMarketplaceDisconnection() {
1259
+ if (this.isShuttingDown) {
1260
+ console.log(`[${this.nodeId}] Shutdown in progress, skipping reconnection`);
1261
+ return;
1262
+ }
1263
+ if (this.reconnectInterval) {
1264
+ clearTimeout(this.reconnectInterval);
1265
+ this.reconnectInterval = null;
1266
+ }
1267
+ this.reconnectAttempts++;
1268
+ if (this.maxReconnectAttempts > 0 && this.reconnectAttempts > this.maxReconnectAttempts) {
1269
+ console.error(`[${this.nodeId}] ❌ Maximum reconnection attempts exceeded`);
1270
+ this.emit('max_reconnect_exceeded');
1271
+ return;
1272
+ }
1273
+ console.log(`[${this.nodeId}] 🔄 Scheduling reconnection attempt ${this.reconnectAttempts}...`);
1274
+ this.reconnectInterval = setTimeout(async () => {
1275
+ console.log(`[${this.nodeId}] 🔄 Attempting to reconnect (attempt ${this.reconnectAttempts})...`);
1276
+ try {
1277
+ await this.connectToMarketplace();
1278
+ if (this.marketplaceWs && this.marketplaceWs.readyState === ws_1.default.OPEN) {
1279
+ console.log(`[${this.nodeId}] 📝 Re-registering after reconnection...`);
1280
+ await this.registerGateway();
1281
+ }
1282
+ }
1283
+ catch (error) {
1284
+ console.error(`[${this.nodeId}] ❌ Reconnection attempt failed:`, error.message);
1285
+ }
1286
+ }, this.reconnectDelay);
1287
+ }
1288
+ /**
1289
+ * Get CPU usage
1290
+ */
1291
+ getCpuUsage() {
1292
+ const cpus = require('os').cpus();
1293
+ let totalIdle = 0;
1294
+ let totalTick = 0;
1295
+ cpus.forEach((cpu) => {
1296
+ for (const type in cpu.times) {
1297
+ totalTick += cpu.times[type];
1298
+ }
1299
+ totalIdle += cpu.times.idle;
1300
+ });
1301
+ const idle = totalIdle / cpus.length;
1302
+ const total = totalTick / cpus.length;
1303
+ return 100 - ~~(100 * idle / total);
1304
+ }
1305
+ /**
1306
+ * Get test URL for provider
1307
+ * Uses /v1/models endpoint for OpenAI-compatible providers
1308
+ */
1309
+ getTestUrl(providerKey, baseUrl, endpointType) {
1310
+ const cleanBaseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
1311
+ // Provider-specific test endpoints
1312
+ switch (providerKey.toLowerCase()) {
1313
+ case 'ollama':
1314
+ // If using OpenAI-compatible endpoint, use OpenAI pattern
1315
+ if (endpointType === 'openai-v1' || cleanBaseUrl.includes('/v1')) {
1316
+ // OpenAI-compatible Ollama: /v1/models
1317
+ if (cleanBaseUrl.includes('/v1')) {
1318
+ return `${cleanBaseUrl}/models`;
1319
+ }
1320
+ return `${cleanBaseUrl}/v1/models`;
1321
+ }
1322
+ // Native Ollama API: /api/tags
1323
+ if (cleanBaseUrl.includes('/api')) {
1324
+ return `${cleanBaseUrl}/tags`;
1325
+ }
1326
+ return `${cleanBaseUrl}/api/tags`;
1327
+ case 'openai':
1328
+ case 'groq':
1329
+ case 'anthropic':
1330
+ case 'together':
1331
+ case 'fireworks':
1332
+ case 'deepseek':
1333
+ case 'mistral':
1334
+ case 'perplexity':
1335
+ case 'openrouter':
1336
+ default:
1337
+ // OpenAI-compatible: /v1/models
1338
+ // If baseUrl already has /v1 path, just append /models
1339
+ if (cleanBaseUrl.includes('/v1')) {
1340
+ return `${cleanBaseUrl}/models`;
1341
+ }
1342
+ return `${cleanBaseUrl}/v1/models`;
1343
+ }
1344
+ }
1345
+ // ============= HEADLESS SESSION HANDLERS =============
1346
+ /**
1347
+ * Handle session start request from Type 1
1348
+ */
1349
+ async handleSessionStart(message) {
1350
+ const requestId = message.requestId || message.request_id;
1351
+ console.log(`[${this.nodeId}] Received session_start request: ${requestId}`);
1352
+ if (!this.headlessSessionManager) {
1353
+ this.sendSessionError(requestId, 'Headless session manager not initialized');
1354
+ return;
1355
+ }
1356
+ try {
1357
+ const config = {
1358
+ sessionId: message.sessionId,
1359
+ userId: message.userId,
1360
+ organizationId: message.organizationId,
1361
+ templateId: message.templateId,
1362
+ systemPrompt: message.systemPrompt,
1363
+ initialMessage: message.initialMessage,
1364
+ modelId: message.modelId,
1365
+ connectionId: message.connectionId,
1366
+ maxTurns: message.maxTurns,
1367
+ timeout: message.timeout,
1368
+ disabledTools: message.disabledTools || [] // List of tools to disable (empty = all enabled)
1369
+ };
1370
+ console.log(`[${this.nodeId}] session_start received disabledTools: [${(message.disabledTools || []).join(', ')}]`);
1371
+ const session = await this.headlessSessionManager.startSession(config);
1372
+ // Send acknowledgment to Type 1
1373
+ // NOTE: The initialMessage is the assistant's greeting, already added to session history
1374
+ // We do NOT call processSessionMessageInternal because the greeting doesn't need an LLM response
1375
+ if (this.marketplaceWs?.readyState === ws_1.default.OPEN) {
1376
+ this.marketplaceWs.send(JSON.stringify({
1377
+ event: 'session_started',
1378
+ requestId,
1379
+ sessionId: session.sessionId,
1380
+ gatewayId: this.instanceId,
1381
+ status: session.status,
1382
+ startedAt: session.startedAt.toISOString(),
1383
+ // Include the initial message so Type 1 can display it to the user
1384
+ initialMessage: message.initialMessage,
1385
+ timestamp: new Date().toISOString()
1386
+ }));
1387
+ }
1388
+ }
1389
+ catch (error) {
1390
+ console.error(`[${this.nodeId}] Session start error:`, error.message);
1391
+ this.sendSessionError(requestId, error.message);
1392
+ }
1393
+ }
1394
+ /**
1395
+ * Handle session message from Type 1
1396
+ */
1397
+ async handleSessionMessage(message) {
1398
+ const requestId = message.requestId || message.request_id;
1399
+ const sessionId = message.sessionId;
1400
+ const userMessage = message.message || message.content;
1401
+ const modelId = message.modelId; // Per-message model override
1402
+ console.log(`[${this.nodeId}] Received session_message for session: ${sessionId}${modelId ? ` with model: ${modelId}` : ''}`);
1403
+ if (!this.headlessSessionManager) {
1404
+ this.sendSessionError(requestId, 'Headless session manager not initialized');
1405
+ return;
1406
+ }
1407
+ await this.processSessionMessageInternal(sessionId, userMessage, requestId, modelId);
1408
+ }
1409
+ /**
1410
+ * Internal method to process session messages with streaming
1411
+ * @param modelId - Optional model ID to use for this specific message (overrides session default)
1412
+ */
1413
+ async processSessionMessageInternal(sessionId, userMessage, requestId, modelId) {
1414
+ try {
1415
+ // Stream handler to send chunks to Type 1
1416
+ const onChunk = (chunk) => {
1417
+ if (this.marketplaceWs?.readyState === ws_1.default.OPEN) {
1418
+ this.marketplaceWs.send(JSON.stringify({
1419
+ event: 'session_chunk',
1420
+ requestId,
1421
+ sessionId,
1422
+ chunk,
1423
+ timestamp: new Date().toISOString()
1424
+ }));
1425
+ }
1426
+ };
1427
+ const response = await this.headlessSessionManager.processMessage(sessionId, userMessage, onChunk, modelId // Pass per-message model ID
1428
+ );
1429
+ // Send final response to Type 1
1430
+ if (this.marketplaceWs?.readyState === ws_1.default.OPEN) {
1431
+ this.marketplaceWs.send(JSON.stringify({
1432
+ event: 'session_response',
1433
+ requestId,
1434
+ sessionId,
1435
+ status: response.status,
1436
+ message: response.message,
1437
+ toolResults: response.toolResults,
1438
+ isComplete: response.isComplete,
1439
+ error: response.error,
1440
+ requestIds: response.requestIds, // LLM request IDs for request log linking
1441
+ usage: response.usage, // Token usage for display
1442
+ timestamp: new Date().toISOString()
1443
+ }));
1444
+ }
1445
+ }
1446
+ catch (error) {
1447
+ console.error(`[${this.nodeId}] Session message error:`, error.message);
1448
+ this.sendSessionError(requestId, error.message, sessionId);
1449
+ }
1450
+ }
1451
+ /**
1452
+ * Handle session cancel request from Type 1
1453
+ */
1454
+ async handleSessionCancel(message) {
1455
+ const requestId = message.requestId || message.request_id;
1456
+ const sessionId = message.sessionId;
1457
+ console.log(`[${this.nodeId}] Received session_cancel for session: ${sessionId}`);
1458
+ if (!this.headlessSessionManager) {
1459
+ this.sendSessionError(requestId, 'Headless session manager not initialized');
1460
+ return;
1461
+ }
1462
+ const success = await this.headlessSessionManager.cancelSession(sessionId);
1463
+ if (this.marketplaceWs?.readyState === ws_1.default.OPEN) {
1464
+ this.marketplaceWs.send(JSON.stringify({
1465
+ event: 'session_cancelled',
1466
+ requestId,
1467
+ sessionId,
1468
+ success,
1469
+ timestamp: new Date().toISOString()
1470
+ }));
1471
+ }
1472
+ }
1473
+ /**
1474
+ * Handle tool discovery request from Type 1
1475
+ */
1476
+ async handleGetTools(message) {
1477
+ const requestId = message.requestId || message.request_id;
1478
+ console.log(`[${this.nodeId}] Received get_tools request`);
1479
+ const tools = this.headlessSessionManager?.getAvailableTools() || [];
1480
+ if (this.marketplaceWs?.readyState === ws_1.default.OPEN) {
1481
+ this.marketplaceWs.send(JSON.stringify({
1482
+ event: 'tools_response',
1483
+ requestId,
1484
+ gatewayId: this.instanceId,
1485
+ tools,
1486
+ timestamp: new Date().toISOString()
1487
+ }));
1488
+ }
1489
+ }
1490
+ /**
1491
+ * Send session error to Type 1
1492
+ */
1493
+ sendSessionError(requestId, error, sessionId) {
1494
+ if (this.marketplaceWs?.readyState === ws_1.default.OPEN) {
1495
+ this.marketplaceWs.send(JSON.stringify({
1496
+ event: 'session_error',
1497
+ requestId,
1498
+ sessionId,
1499
+ error,
1500
+ timestamp: new Date().toISOString()
1501
+ }));
1502
+ }
1503
+ }
1504
+ /**
1505
+ * Send inference error response to Type 1
1506
+ */
1507
+ sendErrorResponse(requestId, code, message) {
1508
+ if (this.marketplaceWs?.readyState === ws_1.default.OPEN) {
1509
+ this.marketplaceWs.send(JSON.stringify({
1510
+ event: 'inference_error',
1511
+ request_id: requestId,
1512
+ error: {
1513
+ code,
1514
+ message
1515
+ },
1516
+ timestamp: new Date().toISOString()
1517
+ }));
1518
+ }
1519
+ }
1520
+ // ============= END HEADLESS SESSION HANDLERS =============
1521
+ // ============= SCRIPT EXECUTION HANDLERS =============
1522
+ /**
1523
+ * Handle execute_script request from Type 1
1524
+ * Executes a remote server script via SSH
1525
+ *
1526
+ * SECURITY: This handler includes fixes for:
1527
+ * - #1: No hardcoded encryption key fallback
1528
+ * - #2/#3: Parameter name/value validation and shell escaping
1529
+ * - #4: Working directory path validation
1530
+ */
1531
+ async handleExecuteScript(message) {
1532
+ const requestId = message.requestId;
1533
+ const { script, server, parameters } = message;
1534
+ const startTime = Date.now();
1535
+ console.log(`[${this.nodeId}] Executing script "${script.name}" on server "${server.name}" (${server.hostname}:${server.port})`);
1536
+ try {
1537
+ // Import required modules for SSH execution
1538
+ const { Client: SSHClient } = require('ssh2');
1539
+ const crypto = require('crypto');
1540
+ // SECURITY FIX #1: Require encryption key - no hardcoded fallback
1541
+ const ENCRYPTION_KEY = process.env.SERVER_ENCRYPTION_KEY;
1542
+ if (!ENCRYPTION_KEY && process.env.NODE_ENV !== 'development' && process.env.DEV_MODE !== 'true') {
1543
+ throw new Error('SERVER_ENCRYPTION_KEY environment variable must be set in production');
1544
+ }
1545
+ const encryptionKey = ENCRYPTION_KEY || 'dev-only-key-not-for-production-use';
1546
+ // Decrypt function supporting both old and new format
1547
+ const decrypt = (encryptedText) => {
1548
+ const parts = encryptedText.split(':');
1549
+ let salt;
1550
+ let iv;
1551
+ let authTag;
1552
+ let encrypted;
1553
+ if (parts.length === 4) {
1554
+ // New format with random salt: salt:iv:authTag:encrypted
1555
+ salt = Buffer.from(parts[0], 'hex');
1556
+ iv = Buffer.from(parts[1], 'hex');
1557
+ authTag = Buffer.from(parts[2], 'hex');
1558
+ encrypted = parts[3];
1559
+ }
1560
+ else if (parts.length === 3) {
1561
+ // Legacy format with static salt: iv:authTag:encrypted
1562
+ console.warn(`[${this.nodeId}] Decrypting legacy format - consider re-encrypting`);
1563
+ salt = Buffer.from('salt');
1564
+ iv = Buffer.from(parts[0], 'hex');
1565
+ authTag = Buffer.from(parts[1], 'hex');
1566
+ encrypted = parts[2];
1567
+ }
1568
+ else {
1569
+ throw new Error('Invalid encrypted data format');
1570
+ }
1571
+ const key = crypto.scryptSync(encryptionKey, salt, 32);
1572
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
1573
+ decipher.setAuthTag(authTag);
1574
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
1575
+ decrypted += decipher.final('utf8');
1576
+ return decrypted;
1577
+ };
1578
+ // SECURITY FIX #2/#3: Shell-escape function
1579
+ const shellEscape = (value) => {
1580
+ return "'" + String(value).replace(/'/g, "'\\''") + "'";
1581
+ };
1582
+ // SECURITY FIX #2/#3: Validate parameter name pattern
1583
+ const VALID_PARAM_NAME_REGEX = /^[A-Z_][A-Z0-9_]*$/;
1584
+ // SECURITY FIX #4: Validate working directory path
1585
+ const VALID_PATH_REGEX = /^[a-zA-Z0-9_./-]+$|^~(\/[a-zA-Z0-9_./-]+)?$/;
1586
+ const validateWorkingDirectory = (path) => {
1587
+ if (!path || path === '~')
1588
+ return true;
1589
+ return VALID_PATH_REGEX.test(path) && !path.includes('..');
1590
+ };
1591
+ // Build the command with environment variables (with security validation)
1592
+ const envVars = [];
1593
+ for (const [key, value] of Object.entries(parameters || {})) {
1594
+ // SECURITY FIX #2/#3: Validate parameter name
1595
+ if (!VALID_PARAM_NAME_REGEX.test(key)) {
1596
+ throw new Error(`Invalid parameter name "${key}": must match pattern ${VALID_PARAM_NAME_REGEX}`);
1597
+ }
1598
+ // Shell-escape the value
1599
+ const escapedValue = shellEscape(String(value));
1600
+ envVars.push(`export ${key}=${escapedValue}`);
1601
+ }
1602
+ const envExports = envVars.length > 0 ? envVars.join('; ') + '; ' : '';
1603
+ // SECURITY FIX #4: Validate working directory
1604
+ if (!validateWorkingDirectory(script.working_directory || '')) {
1605
+ throw new Error(`Invalid working directory "${script.working_directory}": contains invalid characters or path traversal`);
1606
+ }
1607
+ const cdCommand = script.working_directory && script.working_directory !== '~'
1608
+ ? `cd ${shellEscape(script.working_directory)} 2>/dev/null || cd ~`
1609
+ : 'cd ~';
1610
+ const fullCommand = envExports
1611
+ ? `${cdCommand}; ${envExports}${script.content}`
1612
+ : `${cdCommand}; ${script.content}`;
1613
+ // Build SSH config
1614
+ const sshConfig = {
1615
+ host: server.hostname,
1616
+ port: server.port,
1617
+ username: server.username,
1618
+ readyTimeout: 30000,
1619
+ keepaliveInterval: 10000
1620
+ };
1621
+ // Set authentication
1622
+ if (server.auth_type === 'password' && server.encrypted_password) {
1623
+ sshConfig.password = decrypt(server.encrypted_password);
1624
+ }
1625
+ else if ((server.auth_type === 'ssh_key' || server.auth_type === 'ssh_key_passphrase') && server.encrypted_ssh_key) {
1626
+ sshConfig.privateKey = decrypt(server.encrypted_ssh_key);
1627
+ if (server.auth_type === 'ssh_key_passphrase' && server.encrypted_passphrase) {
1628
+ sshConfig.passphrase = decrypt(server.encrypted_passphrase);
1629
+ }
1630
+ }
1631
+ else {
1632
+ throw new Error(`No valid credentials for auth type: ${server.auth_type}`);
1633
+ }
1634
+ // Execute via SSH
1635
+ const result = await new Promise((resolve) => {
1636
+ const conn = new SSHClient();
1637
+ const timeoutMs = (script.timeout_seconds || 300) * 1000;
1638
+ const timeout = setTimeout(() => {
1639
+ conn.end();
1640
+ resolve({
1641
+ success: false,
1642
+ exit_code: -1,
1643
+ stdout: '',
1644
+ stderr: '',
1645
+ duration_ms: Date.now() - startTime,
1646
+ error_message: `Execution timeout after ${script.timeout_seconds || 300} seconds`
1647
+ });
1648
+ }, timeoutMs);
1649
+ conn.on('ready', () => {
1650
+ conn.exec(fullCommand, (err, stream) => {
1651
+ if (err) {
1652
+ clearTimeout(timeout);
1653
+ conn.end();
1654
+ resolve({
1655
+ success: false,
1656
+ exit_code: -1,
1657
+ stdout: '',
1658
+ stderr: '',
1659
+ duration_ms: Date.now() - startTime,
1660
+ error_message: `Command execution failed: ${err.message}`
1661
+ });
1662
+ return;
1663
+ }
1664
+ let stdout = '';
1665
+ let stderr = '';
1666
+ stream.on('data', (data) => {
1667
+ stdout += data.toString();
1668
+ });
1669
+ stream.stderr.on('data', (data) => {
1670
+ stderr += data.toString();
1671
+ });
1672
+ stream.on('close', (code) => {
1673
+ clearTimeout(timeout);
1674
+ conn.end();
1675
+ resolve({
1676
+ success: code === 0,
1677
+ exit_code: code,
1678
+ stdout,
1679
+ stderr,
1680
+ duration_ms: Date.now() - startTime
1681
+ });
1682
+ });
1683
+ });
1684
+ });
1685
+ conn.on('error', (err) => {
1686
+ clearTimeout(timeout);
1687
+ resolve({
1688
+ success: false,
1689
+ exit_code: -1,
1690
+ stdout: '',
1691
+ stderr: '',
1692
+ duration_ms: Date.now() - startTime,
1693
+ error_message: `SSH connection failed: ${err.message}`
1694
+ });
1695
+ });
1696
+ try {
1697
+ conn.connect(sshConfig);
1698
+ }
1699
+ catch (err) {
1700
+ clearTimeout(timeout);
1701
+ resolve({
1702
+ success: false,
1703
+ exit_code: -1,
1704
+ stdout: '',
1705
+ stderr: '',
1706
+ duration_ms: Date.now() - startTime,
1707
+ error_message: `Failed to initiate connection: ${err.message}`
1708
+ });
1709
+ }
1710
+ });
1711
+ console.log(`[${this.nodeId}] Script execution completed: ${result.success ? 'success' : 'failed'} (exit code: ${result.exit_code})`);
1712
+ // Send result back to Type 1
1713
+ if (this.marketplaceWs?.readyState === ws_1.default.OPEN) {
1714
+ this.marketplaceWs.send(JSON.stringify({
1715
+ event: 'script_result',
1716
+ requestId,
1717
+ success: true,
1718
+ result: {
1719
+ exit_code: result.exit_code,
1720
+ stdout: result.stdout,
1721
+ stderr: result.stderr,
1722
+ duration_ms: result.duration_ms,
1723
+ error_message: result.error_message
1724
+ },
1725
+ timestamp: new Date().toISOString()
1726
+ }));
1727
+ }
1728
+ }
1729
+ catch (error) {
1730
+ console.error(`[${this.nodeId}] Script execution error:`, error.message);
1731
+ this.sendScriptError(requestId, 'execution_failed', error.message);
1732
+ }
1733
+ }
1734
+ /**
1735
+ * Send script execution error to Type 1
1736
+ */
1737
+ sendScriptError(requestId, code, message) {
1738
+ if (this.marketplaceWs?.readyState === ws_1.default.OPEN) {
1739
+ this.marketplaceWs.send(JSON.stringify({
1740
+ event: 'script_result',
1741
+ requestId,
1742
+ success: false,
1743
+ error: message,
1744
+ code,
1745
+ timestamp: new Date().toISOString()
1746
+ }));
1747
+ }
1748
+ }
1749
+ // ============= END SCRIPT EXECUTION HANDLERS =============
1750
+ /**
1751
+ * Graceful shutdown
1752
+ */
1753
+ async gracefulShutdown() {
1754
+ console.log(`[${this.nodeId}] Initiating graceful shutdown...`);
1755
+ this.isShuttingDown = true;
1756
+ if (this.reconnectInterval) {
1757
+ clearTimeout(this.reconnectInterval);
1758
+ this.reconnectInterval = null;
1759
+ }
1760
+ if (this.healthCheckInterval) {
1761
+ clearInterval(this.healthCheckInterval);
1762
+ this.healthCheckInterval = null;
1763
+ }
1764
+ // Wait for active requests
1765
+ const maxWaitTime = 30000;
1766
+ const startTime = Date.now();
1767
+ while (this.activeRequests.size > 0 && Date.now() - startTime < maxWaitTime) {
1768
+ console.log(`[${this.nodeId}] Waiting for ${this.activeRequests.size} active requests...`);
1769
+ await new Promise(resolve => setTimeout(resolve, 1000));
1770
+ }
1771
+ // Close marketplace WebSocket
1772
+ if (this.marketplaceWs) {
1773
+ this.marketplaceWs.close();
1774
+ }
1775
+ // Close management API server
1776
+ if (this.managementServer) {
1777
+ await new Promise((resolve) => {
1778
+ this.managementServer.close(() => {
1779
+ console.log(`[${this.nodeId}] Management API server closed`);
1780
+ resolve();
1781
+ });
1782
+ });
1783
+ }
1784
+ console.log(`[${this.nodeId}] Shutdown complete`);
1785
+ }
1786
+ }
1787
+ exports.Type3GatewayServer = Type3GatewayServer;
1788
+ /**
1789
+ * Start Type 3 Gateway Server
1790
+ */
1791
+ if (require.main === module) {
1792
+ const gateway = new Type3GatewayServer({
1793
+ port: parseInt(process.env.GATEWAY_PORT || '8083'),
1794
+ marketplaceUrl: process.env.MARKETPLACE_URL,
1795
+ instanceId: process.env.INSTANCE_ID,
1796
+ apiKey: process.env.GATEWAY_API_KEY,
1797
+ vaultPath: process.env.VAULT_PATH,
1798
+ vaultPassword: process.env.VAULT_PASSWORD,
1799
+ configPath: process.env.GATEWAY_CONFIG_PATH // Optional: specify custom config path
1800
+ });
1801
+ gateway.start().catch(console.error);
1802
+ process.on('SIGTERM', () => {
1803
+ console.log('Received SIGTERM signal');
1804
+ gateway.gracefulShutdown().then(() => process.exit(0));
1805
+ });
1806
+ process.on('SIGINT', () => {
1807
+ console.log('Received SIGINT signal');
1808
+ gateway.gracefulShutdown().then(() => process.exit(0));
1809
+ });
1810
+ }
1811
+ //# sourceMappingURL=gateway-server.js.map