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.
- package/.env.example +29 -0
- package/README.md +480 -0
- package/dist/bash-tools.d.ts +56 -0
- package/dist/bash-tools.d.ts.map +1 -0
- package/dist/bash-tools.js +188 -0
- package/dist/bash-tools.js.map +1 -0
- package/dist/core-tools.d.ts +94 -0
- package/dist/core-tools.d.ts.map +1 -0
- package/dist/core-tools.js +694 -0
- package/dist/core-tools.js.map +1 -0
- package/dist/debug-utils.d.ts +22 -0
- package/dist/debug-utils.d.ts.map +1 -0
- package/dist/debug-utils.js +37 -0
- package/dist/debug-utils.js.map +1 -0
- package/dist/devops-tools.d.ts +147 -0
- package/dist/devops-tools.d.ts.map +1 -0
- package/dist/devops-tools.js +718 -0
- package/dist/devops-tools.js.map +1 -0
- package/dist/gateway-config.d.ts +56 -0
- package/dist/gateway-config.d.ts.map +1 -0
- package/dist/gateway-config.js +198 -0
- package/dist/gateway-config.js.map +1 -0
- package/dist/gateway-mode.d.ts +58 -0
- package/dist/gateway-mode.d.ts.map +1 -0
- package/dist/gateway-mode.js +240 -0
- package/dist/gateway-mode.js.map +1 -0
- package/dist/gateway-server.d.ts +208 -0
- package/dist/gateway-server.d.ts.map +1 -0
- package/dist/gateway-server.js +1811 -0
- package/dist/gateway-server.js.map +1 -0
- package/dist/headless-session.d.ts +192 -0
- package/dist/headless-session.d.ts.map +1 -0
- package/dist/headless-session.js +584 -0
- package/dist/headless-session.js.map +1 -0
- package/dist/index-server.d.ts +4 -0
- package/dist/index-server.d.ts.map +1 -0
- package/dist/index-server.js +129 -0
- package/dist/index-server.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +101 -0
- package/dist/index.js.map +1 -0
- package/dist/key-vault.d.ts +102 -0
- package/dist/key-vault.d.ts.map +1 -0
- package/dist/key-vault.js +365 -0
- package/dist/key-vault.js.map +1 -0
- package/dist/local-vault.d.ts +195 -0
- package/dist/local-vault.d.ts.map +1 -0
- package/dist/local-vault.js +571 -0
- package/dist/local-vault.js.map +1 -0
- package/dist/marketplace-tools.d.ts +104 -0
- package/dist/marketplace-tools.d.ts.map +1 -0
- package/dist/marketplace-tools.js +2846 -0
- package/dist/marketplace-tools.js.map +1 -0
- package/dist/mcp-manager.d.ts +114 -0
- package/dist/mcp-manager.d.ts.map +1 -0
- package/dist/mcp-manager.js +338 -0
- package/dist/mcp-manager.js.map +1 -0
- package/dist/web-tools.d.ts +86 -0
- package/dist/web-tools.d.ts.map +1 -0
- package/dist/web-tools.js +431 -0
- package/dist/web-tools.js.map +1 -0
- package/dist/websocket-handler.d.ts +131 -0
- package/dist/websocket-handler.d.ts.map +1 -0
- package/dist/websocket-handler.js +596 -0
- package/dist/websocket-handler.js.map +1 -0
- package/dist/welcome-pages.d.ts +6 -0
- package/dist/welcome-pages.d.ts.map +1 -0
- package/dist/welcome-pages.js +200 -0
- package/dist/welcome-pages.js.map +1 -0
- package/package.json +168 -0
- package/scripts/install-remote.sh +282 -0
- package/scripts/start.sh +85 -0
- package/scripts/status.sh +79 -0
- 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
|