@willjackson/claude-code-bridge 0.6.2 → 0.7.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/dist/{chunk-XOAR3DQB.js → chunk-2O352EPO.js} +745 -154
- package/dist/chunk-2O352EPO.js.map +1 -0
- package/dist/cli.js +96 -8
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +421 -112
- package/dist/index.js +26 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-XOAR3DQB.js.map +0 -1
|
@@ -148,9 +148,137 @@ var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
|
|
|
148
148
|
return ConnectionState2;
|
|
149
149
|
})(ConnectionState || {});
|
|
150
150
|
|
|
151
|
+
// src/utils/tls.ts
|
|
152
|
+
import * as fs from "fs";
|
|
153
|
+
var logger = createLogger("tls");
|
|
154
|
+
function readCertFile(filePath) {
|
|
155
|
+
try {
|
|
156
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
157
|
+
} catch (error) {
|
|
158
|
+
const err = error;
|
|
159
|
+
if (err.code === "ENOENT") {
|
|
160
|
+
throw new Error(`Certificate file not found: ${filePath}`);
|
|
161
|
+
}
|
|
162
|
+
if (err.code === "EACCES") {
|
|
163
|
+
throw new Error(`Permission denied reading certificate file: ${filePath}`);
|
|
164
|
+
}
|
|
165
|
+
throw new Error(`Failed to read certificate file ${filePath}: ${err.message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function loadCertificates(config) {
|
|
169
|
+
const options = {};
|
|
170
|
+
if (config.cert) {
|
|
171
|
+
logger.debug({ path: config.cert }, "Loading certificate");
|
|
172
|
+
options.cert = readCertFile(config.cert);
|
|
173
|
+
}
|
|
174
|
+
if (config.key) {
|
|
175
|
+
logger.debug({ path: config.key }, "Loading private key");
|
|
176
|
+
options.key = readCertFile(config.key);
|
|
177
|
+
}
|
|
178
|
+
if (config.ca) {
|
|
179
|
+
logger.debug({ path: config.ca }, "Loading CA certificate");
|
|
180
|
+
options.ca = readCertFile(config.ca);
|
|
181
|
+
}
|
|
182
|
+
options.rejectUnauthorized = config.rejectUnauthorized ?? true;
|
|
183
|
+
if (config.passphrase) {
|
|
184
|
+
options.passphrase = config.passphrase;
|
|
185
|
+
}
|
|
186
|
+
logger.info(
|
|
187
|
+
{
|
|
188
|
+
hasCert: !!options.cert,
|
|
189
|
+
hasKey: !!options.key,
|
|
190
|
+
hasCa: !!options.ca,
|
|
191
|
+
rejectUnauthorized: options.rejectUnauthorized
|
|
192
|
+
},
|
|
193
|
+
"TLS certificates loaded"
|
|
194
|
+
);
|
|
195
|
+
return options;
|
|
196
|
+
}
|
|
197
|
+
function loadCertificatesSync(config) {
|
|
198
|
+
const options = {};
|
|
199
|
+
if (config.cert) {
|
|
200
|
+
options.cert = readCertFile(config.cert);
|
|
201
|
+
}
|
|
202
|
+
if (config.key) {
|
|
203
|
+
options.key = readCertFile(config.key);
|
|
204
|
+
}
|
|
205
|
+
if (config.ca) {
|
|
206
|
+
options.ca = readCertFile(config.ca);
|
|
207
|
+
}
|
|
208
|
+
options.rejectUnauthorized = config.rejectUnauthorized ?? true;
|
|
209
|
+
if (config.passphrase) {
|
|
210
|
+
options.passphrase = config.passphrase;
|
|
211
|
+
}
|
|
212
|
+
return options;
|
|
213
|
+
}
|
|
214
|
+
function isFileReadable(filePath) {
|
|
215
|
+
try {
|
|
216
|
+
fs.accessSync(filePath, fs.constants.R_OK);
|
|
217
|
+
return true;
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function validateTLSConfig(config) {
|
|
223
|
+
const errors = [];
|
|
224
|
+
const warnings = [];
|
|
225
|
+
if (config.cert && !config.key) {
|
|
226
|
+
errors.push("Certificate provided without private key");
|
|
227
|
+
}
|
|
228
|
+
if (config.key && !config.cert) {
|
|
229
|
+
errors.push("Private key provided without certificate");
|
|
230
|
+
}
|
|
231
|
+
if (config.cert) {
|
|
232
|
+
if (!isFileReadable(config.cert)) {
|
|
233
|
+
errors.push(`Certificate file not readable: ${config.cert}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (config.key) {
|
|
237
|
+
if (!isFileReadable(config.key)) {
|
|
238
|
+
errors.push(`Private key file not readable: ${config.key}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (config.ca) {
|
|
242
|
+
if (!isFileReadable(config.ca)) {
|
|
243
|
+
errors.push(`CA certificate file not readable: ${config.ca}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (config.rejectUnauthorized === false) {
|
|
247
|
+
warnings.push("TLS certificate verification is disabled - this is insecure for production use");
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
valid: errors.length === 0,
|
|
251
|
+
errors,
|
|
252
|
+
warnings
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function isTLSEnabled(config) {
|
|
256
|
+
if (!config) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
return !!(config.cert && config.key);
|
|
260
|
+
}
|
|
261
|
+
function createSecureContextOptions(loaded) {
|
|
262
|
+
const options = {};
|
|
263
|
+
if (loaded.cert) {
|
|
264
|
+
options.cert = loaded.cert;
|
|
265
|
+
}
|
|
266
|
+
if (loaded.key) {
|
|
267
|
+
options.key = loaded.key;
|
|
268
|
+
}
|
|
269
|
+
if (loaded.ca) {
|
|
270
|
+
options.ca = loaded.ca;
|
|
271
|
+
}
|
|
272
|
+
if (loaded.passphrase) {
|
|
273
|
+
options.passphrase = loaded.passphrase;
|
|
274
|
+
}
|
|
275
|
+
return options;
|
|
276
|
+
}
|
|
277
|
+
|
|
151
278
|
// src/transport/websocket.ts
|
|
152
279
|
import WebSocket from "ws";
|
|
153
|
-
|
|
280
|
+
import * as https from "https";
|
|
281
|
+
var logger2 = createLogger("websocket-transport");
|
|
154
282
|
var DEFAULT_RECONNECT_INTERVAL = 1e3;
|
|
155
283
|
var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
|
|
156
284
|
var DEFAULT_HEARTBEAT_INTERVAL = 3e4;
|
|
@@ -176,6 +304,7 @@ var WebSocketTransport = class {
|
|
|
176
304
|
reconnectingHandlers = [];
|
|
177
305
|
/**
|
|
178
306
|
* Build the WebSocket URL from the connection configuration
|
|
307
|
+
* Uses wss:// if TLS is configured, ws:// otherwise
|
|
179
308
|
*/
|
|
180
309
|
buildUrl(config) {
|
|
181
310
|
if (config.url) {
|
|
@@ -183,7 +312,79 @@ var WebSocketTransport = class {
|
|
|
183
312
|
}
|
|
184
313
|
const host = config.host ?? "localhost";
|
|
185
314
|
const port = config.port ?? 8765;
|
|
186
|
-
|
|
315
|
+
const useTls = config.tls?.ca || config.tls?.rejectUnauthorized !== void 0;
|
|
316
|
+
const protocol = useTls ? "wss" : "ws";
|
|
317
|
+
return `${protocol}://${host}:${port}`;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Build WebSocket options including TLS and auth configuration
|
|
321
|
+
*/
|
|
322
|
+
buildWsOptions(config) {
|
|
323
|
+
const options = {};
|
|
324
|
+
if (config.tls) {
|
|
325
|
+
try {
|
|
326
|
+
const tlsOptions = loadCertificatesSync(config.tls);
|
|
327
|
+
const agentOptions = {
|
|
328
|
+
rejectUnauthorized: tlsOptions.rejectUnauthorized ?? true
|
|
329
|
+
};
|
|
330
|
+
if (tlsOptions.ca) {
|
|
331
|
+
agentOptions.ca = tlsOptions.ca;
|
|
332
|
+
}
|
|
333
|
+
if (tlsOptions.cert) {
|
|
334
|
+
agentOptions.cert = tlsOptions.cert;
|
|
335
|
+
}
|
|
336
|
+
if (tlsOptions.key) {
|
|
337
|
+
agentOptions.key = tlsOptions.key;
|
|
338
|
+
}
|
|
339
|
+
if (tlsOptions.passphrase) {
|
|
340
|
+
agentOptions.passphrase = tlsOptions.passphrase;
|
|
341
|
+
}
|
|
342
|
+
options.agent = new https.Agent(agentOptions);
|
|
343
|
+
if (tlsOptions.rejectUnauthorized !== void 0) {
|
|
344
|
+
options.rejectUnauthorized = tlsOptions.rejectUnauthorized;
|
|
345
|
+
}
|
|
346
|
+
if (tlsOptions.ca) {
|
|
347
|
+
options.ca = tlsOptions.ca;
|
|
348
|
+
}
|
|
349
|
+
logger2.debug(
|
|
350
|
+
{ hasCa: !!tlsOptions.ca, rejectUnauthorized: tlsOptions.rejectUnauthorized },
|
|
351
|
+
"TLS options configured for client"
|
|
352
|
+
);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
logger2.error({ error: error.message }, "Failed to load TLS certificates");
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (config.auth && config.auth.type !== "none") {
|
|
359
|
+
options.headers = options.headers || {};
|
|
360
|
+
if (config.auth.token) {
|
|
361
|
+
options.headers["Authorization"] = `Bearer ${config.auth.token}`;
|
|
362
|
+
}
|
|
363
|
+
if (config.auth.password) {
|
|
364
|
+
options.headers["X-Auth-Password"] = config.auth.password;
|
|
365
|
+
}
|
|
366
|
+
logger2.debug(
|
|
367
|
+
{ hasToken: !!config.auth.token, hasPassword: !!config.auth.password },
|
|
368
|
+
"Auth headers configured"
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return options;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Build URL with auth query parameters (fallback for servers that don't support headers)
|
|
375
|
+
*/
|
|
376
|
+
buildUrlWithAuth(baseUrl, config) {
|
|
377
|
+
if (!config.auth || config.auth.type === "none") {
|
|
378
|
+
return baseUrl;
|
|
379
|
+
}
|
|
380
|
+
const url = new URL(baseUrl);
|
|
381
|
+
if (config.auth.token) {
|
|
382
|
+
url.searchParams.set("token", config.auth.token);
|
|
383
|
+
}
|
|
384
|
+
if (config.auth.password) {
|
|
385
|
+
url.searchParams.set("password", config.auth.password);
|
|
386
|
+
}
|
|
387
|
+
return url.toString();
|
|
187
388
|
}
|
|
188
389
|
/**
|
|
189
390
|
* Establish connection to a remote peer
|
|
@@ -206,15 +407,20 @@ var WebSocketTransport = class {
|
|
|
206
407
|
throw new Error("No configuration set");
|
|
207
408
|
}
|
|
208
409
|
this.state = "CONNECTING" /* CONNECTING */;
|
|
209
|
-
const
|
|
210
|
-
|
|
410
|
+
const baseUrl = this.buildUrl(this.config);
|
|
411
|
+
const wsOptions = this.buildWsOptions(this.config);
|
|
412
|
+
const url = this.buildUrlWithAuth(baseUrl, this.config);
|
|
413
|
+
logger2.debug(
|
|
414
|
+
{ url: baseUrl, attempt: this.reconnectAttempts, hasOptions: Object.keys(wsOptions).length > 0 },
|
|
415
|
+
"Connecting to WebSocket server"
|
|
416
|
+
);
|
|
211
417
|
return new Promise((resolve, reject) => {
|
|
212
418
|
try {
|
|
213
|
-
this.ws = new WebSocket(url);
|
|
419
|
+
this.ws = Object.keys(wsOptions).length > 0 ? new WebSocket(url, wsOptions) : new WebSocket(url);
|
|
214
420
|
this.ws.on("open", () => {
|
|
215
421
|
this.state = "CONNECTED" /* CONNECTED */;
|
|
216
422
|
this.reconnectAttempts = 0;
|
|
217
|
-
|
|
423
|
+
logger2.info({ url }, "WebSocket connection established");
|
|
218
424
|
this.startHeartbeat();
|
|
219
425
|
this.flushMessageQueue();
|
|
220
426
|
resolve();
|
|
@@ -229,7 +435,7 @@ var WebSocketTransport = class {
|
|
|
229
435
|
const wasConnected = this.state === "CONNECTED" /* CONNECTED */;
|
|
230
436
|
const wasReconnecting = this.state === "RECONNECTING" /* RECONNECTING */;
|
|
231
437
|
this.stopHeartbeat();
|
|
232
|
-
|
|
438
|
+
logger2.info({ code, reason: reason.toString() }, "WebSocket connection closed");
|
|
233
439
|
if (wasConnected) {
|
|
234
440
|
this.notifyDisconnect();
|
|
235
441
|
}
|
|
@@ -240,7 +446,7 @@ var WebSocketTransport = class {
|
|
|
240
446
|
}
|
|
241
447
|
});
|
|
242
448
|
this.ws.on("error", (error) => {
|
|
243
|
-
|
|
449
|
+
logger2.error({ error: error.message }, "WebSocket error");
|
|
244
450
|
if (this.state === "CONNECTING" /* CONNECTING */ && this.reconnectAttempts === 0) {
|
|
245
451
|
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
246
452
|
reject(error);
|
|
@@ -266,7 +472,7 @@ var WebSocketTransport = class {
|
|
|
266
472
|
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
267
473
|
return;
|
|
268
474
|
}
|
|
269
|
-
|
|
475
|
+
logger2.debug("Disconnecting WebSocket");
|
|
270
476
|
return new Promise((resolve) => {
|
|
271
477
|
if (!this.ws) {
|
|
272
478
|
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
@@ -316,11 +522,11 @@ var WebSocketTransport = class {
|
|
|
316
522
|
throw new Error("Not connected");
|
|
317
523
|
}
|
|
318
524
|
const serialized = serializeMessage(message);
|
|
319
|
-
|
|
525
|
+
logger2.debug({ messageId: message.id, type: message.type }, "Sending message");
|
|
320
526
|
return new Promise((resolve, reject) => {
|
|
321
527
|
this.ws.send(serialized, (error) => {
|
|
322
528
|
if (error) {
|
|
323
|
-
|
|
529
|
+
logger2.error({ error: error.message, messageId: message.id }, "Failed to send message");
|
|
324
530
|
reject(error);
|
|
325
531
|
} else {
|
|
326
532
|
resolve();
|
|
@@ -333,7 +539,7 @@ var WebSocketTransport = class {
|
|
|
333
539
|
*/
|
|
334
540
|
queueMessage(message) {
|
|
335
541
|
this.messageQueue.push(message);
|
|
336
|
-
|
|
542
|
+
logger2.debug({ messageId: message.id, queueLength: this.messageQueue.length }, "Message queued for delivery");
|
|
337
543
|
}
|
|
338
544
|
/**
|
|
339
545
|
* Flush all queued messages after reconnection
|
|
@@ -342,14 +548,14 @@ var WebSocketTransport = class {
|
|
|
342
548
|
if (this.messageQueue.length === 0) {
|
|
343
549
|
return;
|
|
344
550
|
}
|
|
345
|
-
|
|
551
|
+
logger2.info({ queueLength: this.messageQueue.length }, "Flushing message queue");
|
|
346
552
|
const messages = [...this.messageQueue];
|
|
347
553
|
this.messageQueue = [];
|
|
348
554
|
for (const message of messages) {
|
|
349
555
|
try {
|
|
350
556
|
await this.sendImmediate(message);
|
|
351
557
|
} catch (error) {
|
|
352
|
-
|
|
558
|
+
logger2.error({ error: error.message, messageId: message.id }, "Failed to send queued message");
|
|
353
559
|
this.messageQueue.unshift(message);
|
|
354
560
|
break;
|
|
355
561
|
}
|
|
@@ -396,20 +602,20 @@ var WebSocketTransport = class {
|
|
|
396
602
|
*/
|
|
397
603
|
handleIncomingMessage(data) {
|
|
398
604
|
const messageString = data.toString();
|
|
399
|
-
|
|
605
|
+
logger2.debug({ dataLength: messageString.length }, "Received message");
|
|
400
606
|
const result = safeDeserializeMessage(messageString);
|
|
401
607
|
if (!result.success) {
|
|
402
|
-
|
|
608
|
+
logger2.warn({ error: result.error.message }, "Failed to parse incoming message");
|
|
403
609
|
this.notifyError(new Error(`Invalid message format: ${result.error.message}`));
|
|
404
610
|
return;
|
|
405
611
|
}
|
|
406
612
|
const message = result.data;
|
|
407
|
-
|
|
613
|
+
logger2.debug({ messageId: message.id, type: message.type }, "Parsed message");
|
|
408
614
|
for (const handler of this.messageHandlers) {
|
|
409
615
|
try {
|
|
410
616
|
handler(message);
|
|
411
617
|
} catch (error) {
|
|
412
|
-
|
|
618
|
+
logger2.error({ error: error.message }, "Message handler threw error");
|
|
413
619
|
}
|
|
414
620
|
}
|
|
415
621
|
}
|
|
@@ -421,7 +627,7 @@ var WebSocketTransport = class {
|
|
|
421
627
|
try {
|
|
422
628
|
handler();
|
|
423
629
|
} catch (error) {
|
|
424
|
-
|
|
630
|
+
logger2.error({ error: error.message }, "Disconnect handler threw error");
|
|
425
631
|
}
|
|
426
632
|
}
|
|
427
633
|
}
|
|
@@ -433,7 +639,7 @@ var WebSocketTransport = class {
|
|
|
433
639
|
try {
|
|
434
640
|
handler(error);
|
|
435
641
|
} catch (handlerError) {
|
|
436
|
-
|
|
642
|
+
logger2.error({ error: handlerError.message }, "Error handler threw error");
|
|
437
643
|
}
|
|
438
644
|
}
|
|
439
645
|
}
|
|
@@ -445,7 +651,7 @@ var WebSocketTransport = class {
|
|
|
445
651
|
try {
|
|
446
652
|
handler(attempt, maxAttempts);
|
|
447
653
|
} catch (error) {
|
|
448
|
-
|
|
654
|
+
logger2.error({ error: error.message }, "Reconnecting handler threw error");
|
|
449
655
|
}
|
|
450
656
|
}
|
|
451
657
|
}
|
|
@@ -473,7 +679,7 @@ var WebSocketTransport = class {
|
|
|
473
679
|
this.reconnectAttempts++;
|
|
474
680
|
const maxAttempts = this.config?.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
475
681
|
const interval = this.config?.reconnectInterval ?? DEFAULT_RECONNECT_INTERVAL;
|
|
476
|
-
|
|
682
|
+
logger2.info(
|
|
477
683
|
{ attempt: this.reconnectAttempts, maxAttempts, interval },
|
|
478
684
|
"Scheduling reconnection attempt"
|
|
479
685
|
);
|
|
@@ -482,13 +688,13 @@ var WebSocketTransport = class {
|
|
|
482
688
|
this.reconnectTimer = null;
|
|
483
689
|
try {
|
|
484
690
|
await this.establishConnection();
|
|
485
|
-
|
|
691
|
+
logger2.info({ attempts: this.reconnectAttempts }, "Reconnection successful");
|
|
486
692
|
} catch (error) {
|
|
487
|
-
|
|
693
|
+
logger2.warn({ error: error.message }, "Reconnection attempt failed");
|
|
488
694
|
if (this.shouldReconnect()) {
|
|
489
695
|
this.scheduleReconnect();
|
|
490
696
|
} else {
|
|
491
|
-
|
|
697
|
+
logger2.error({ maxAttempts }, "Max reconnection attempts reached, giving up");
|
|
492
698
|
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
493
699
|
this.notifyError(new Error(`Failed to reconnect after ${maxAttempts} attempts`));
|
|
494
700
|
}
|
|
@@ -515,7 +721,7 @@ var WebSocketTransport = class {
|
|
|
515
721
|
this.heartbeatInterval = setInterval(() => {
|
|
516
722
|
this.sendPing();
|
|
517
723
|
}, DEFAULT_HEARTBEAT_INTERVAL);
|
|
518
|
-
|
|
724
|
+
logger2.debug({ interval: DEFAULT_HEARTBEAT_INTERVAL }, "Heartbeat monitoring started");
|
|
519
725
|
}
|
|
520
726
|
/**
|
|
521
727
|
* Stop the heartbeat monitoring
|
|
@@ -539,7 +745,7 @@ var WebSocketTransport = class {
|
|
|
539
745
|
return;
|
|
540
746
|
}
|
|
541
747
|
if (this.awaitingPong) {
|
|
542
|
-
|
|
748
|
+
logger2.warn("No pong received, connection may be dead");
|
|
543
749
|
this.handleHeartbeatTimeout();
|
|
544
750
|
return;
|
|
545
751
|
}
|
|
@@ -550,7 +756,7 @@ var WebSocketTransport = class {
|
|
|
550
756
|
this.handleHeartbeatTimeout();
|
|
551
757
|
}
|
|
552
758
|
}, HEARTBEAT_TIMEOUT);
|
|
553
|
-
|
|
759
|
+
logger2.debug("Ping sent");
|
|
554
760
|
}
|
|
555
761
|
/**
|
|
556
762
|
* Handle pong response from peer
|
|
@@ -561,13 +767,13 @@ var WebSocketTransport = class {
|
|
|
561
767
|
clearTimeout(this.heartbeatTimeout);
|
|
562
768
|
this.heartbeatTimeout = null;
|
|
563
769
|
}
|
|
564
|
-
|
|
770
|
+
logger2.debug("Pong received");
|
|
565
771
|
}
|
|
566
772
|
/**
|
|
567
773
|
* Handle heartbeat timeout (no pong received)
|
|
568
774
|
*/
|
|
569
775
|
handleHeartbeatTimeout() {
|
|
570
|
-
|
|
776
|
+
logger2.warn("Heartbeat timeout - closing connection");
|
|
571
777
|
this.awaitingPong = false;
|
|
572
778
|
if (this.heartbeatTimeout) {
|
|
573
779
|
clearTimeout(this.heartbeatTimeout);
|
|
@@ -594,6 +800,290 @@ var WebSocketTransport = class {
|
|
|
594
800
|
}
|
|
595
801
|
};
|
|
596
802
|
|
|
803
|
+
// src/utils/auth.ts
|
|
804
|
+
import * as crypto from "crypto";
|
|
805
|
+
var logger3 = createLogger("auth");
|
|
806
|
+
function ipv4ToInt(ip) {
|
|
807
|
+
const parts = ip.split(".").map(Number);
|
|
808
|
+
if (parts.length !== 4 || parts.some((p) => isNaN(p) || p < 0 || p > 255)) {
|
|
809
|
+
return -1;
|
|
810
|
+
}
|
|
811
|
+
return parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3];
|
|
812
|
+
}
|
|
813
|
+
function matchesCidr(clientIp, cidr) {
|
|
814
|
+
let normalizedIp = clientIp;
|
|
815
|
+
if (normalizedIp.startsWith("::ffff:")) {
|
|
816
|
+
normalizedIp = normalizedIp.substring(7);
|
|
817
|
+
}
|
|
818
|
+
const [network, prefixStr] = cidr.split("/");
|
|
819
|
+
const prefix = prefixStr ? parseInt(prefixStr, 10) : 32;
|
|
820
|
+
if (prefix < 0 || prefix > 32) {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
const clientInt = ipv4ToInt(normalizedIp);
|
|
824
|
+
const networkInt = ipv4ToInt(network);
|
|
825
|
+
if (clientInt === -1 || networkInt === -1) {
|
|
826
|
+
return normalizedIp === network || clientIp === cidr;
|
|
827
|
+
}
|
|
828
|
+
const mask = prefix === 0 ? 0 : -1 << 32 - prefix >>> 0;
|
|
829
|
+
return (clientInt & mask) >>> 0 === (networkInt & mask) >>> 0;
|
|
830
|
+
}
|
|
831
|
+
function validateIp(clientIp, allowedCidrs) {
|
|
832
|
+
if (!allowedCidrs || allowedCidrs.length === 0) {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
for (const cidr of allowedCidrs) {
|
|
836
|
+
if (matchesCidr(clientIp, cidr)) {
|
|
837
|
+
logger3.debug({ clientIp, matchedCidr: cidr }, "IP matched allowed range");
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
logger3.debug({ clientIp, allowedCidrs }, "IP did not match any allowed range");
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
function timingSafeCompare(provided, expected) {
|
|
845
|
+
const providedBuffer = Buffer.from(provided, "utf-8");
|
|
846
|
+
const expectedBuffer = Buffer.from(expected, "utf-8");
|
|
847
|
+
if (providedBuffer.length !== expectedBuffer.length) {
|
|
848
|
+
crypto.timingSafeEqual(expectedBuffer, expectedBuffer);
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
return crypto.timingSafeEqual(providedBuffer, expectedBuffer);
|
|
852
|
+
}
|
|
853
|
+
function validateToken(provided, expected) {
|
|
854
|
+
if (!provided) {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
return timingSafeCompare(provided, expected);
|
|
858
|
+
}
|
|
859
|
+
function validatePassword(provided, expected) {
|
|
860
|
+
if (!provided) {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
return timingSafeCompare(provided, expected);
|
|
864
|
+
}
|
|
865
|
+
function getClientIp(request) {
|
|
866
|
+
const forwarded = request.headers["x-forwarded-for"];
|
|
867
|
+
if (forwarded) {
|
|
868
|
+
const ips = Array.isArray(forwarded) ? forwarded[0] : forwarded;
|
|
869
|
+
const clientIp = ips.split(",")[0].trim();
|
|
870
|
+
if (clientIp) {
|
|
871
|
+
return clientIp;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return request.socket.remoteAddress || "unknown";
|
|
875
|
+
}
|
|
876
|
+
function extractCredentials(request) {
|
|
877
|
+
const credentials = {
|
|
878
|
+
clientIp: getClientIp(request)
|
|
879
|
+
};
|
|
880
|
+
const url = new URL(request.url || "/", `http://${request.headers.host || "localhost"}`);
|
|
881
|
+
const queryToken = url.searchParams.get("token");
|
|
882
|
+
if (queryToken) {
|
|
883
|
+
credentials.token = queryToken;
|
|
884
|
+
}
|
|
885
|
+
const queryPassword = url.searchParams.get("password");
|
|
886
|
+
if (queryPassword) {
|
|
887
|
+
credentials.password = queryPassword;
|
|
888
|
+
}
|
|
889
|
+
const authHeader = request.headers.authorization;
|
|
890
|
+
if (authHeader) {
|
|
891
|
+
if (authHeader.startsWith("Bearer ")) {
|
|
892
|
+
credentials.token = authHeader.substring(7);
|
|
893
|
+
} else if (authHeader.startsWith("Basic ")) {
|
|
894
|
+
try {
|
|
895
|
+
const decoded = Buffer.from(authHeader.substring(6), "base64").toString("utf-8");
|
|
896
|
+
const [, password] = decoded.split(":");
|
|
897
|
+
if (password) {
|
|
898
|
+
credentials.password = password;
|
|
899
|
+
}
|
|
900
|
+
} catch {
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const tokenHeader = request.headers["x-auth-token"];
|
|
905
|
+
if (tokenHeader && !credentials.token) {
|
|
906
|
+
credentials.token = Array.isArray(tokenHeader) ? tokenHeader[0] : tokenHeader;
|
|
907
|
+
}
|
|
908
|
+
const passwordHeader = request.headers["x-auth-password"];
|
|
909
|
+
if (passwordHeader && !credentials.password) {
|
|
910
|
+
credentials.password = Array.isArray(passwordHeader) ? passwordHeader[0] : passwordHeader;
|
|
911
|
+
}
|
|
912
|
+
return credentials;
|
|
913
|
+
}
|
|
914
|
+
var Authenticator = class {
|
|
915
|
+
config;
|
|
916
|
+
/**
|
|
917
|
+
* Create a new Authenticator
|
|
918
|
+
* @param config - Authentication configuration
|
|
919
|
+
*/
|
|
920
|
+
constructor(config) {
|
|
921
|
+
this.config = config;
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Authenticate a request
|
|
925
|
+
* @param request - The incoming HTTP request (WebSocket upgrade request)
|
|
926
|
+
* @returns Authentication result
|
|
927
|
+
*/
|
|
928
|
+
authenticate(request) {
|
|
929
|
+
if (this.config.type === "none") {
|
|
930
|
+
return { success: true };
|
|
931
|
+
}
|
|
932
|
+
const credentials = extractCredentials(request);
|
|
933
|
+
return this.authenticateWithCredentials(credentials);
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Authenticate with extracted credentials
|
|
937
|
+
* @param credentials - The extracted credentials
|
|
938
|
+
* @returns Authentication result
|
|
939
|
+
*/
|
|
940
|
+
authenticateWithCredentials(credentials) {
|
|
941
|
+
const { clientIp } = credentials;
|
|
942
|
+
if (this.config.type === "none") {
|
|
943
|
+
return { success: true, clientIp };
|
|
944
|
+
}
|
|
945
|
+
const results = [];
|
|
946
|
+
if (this.config.token) {
|
|
947
|
+
const tokenValid = validateToken(credentials.token, this.config.token);
|
|
948
|
+
results.push({ method: "token", success: tokenValid });
|
|
949
|
+
if (tokenValid) {
|
|
950
|
+
logger3.debug({ clientIp }, "Token authentication succeeded");
|
|
951
|
+
} else {
|
|
952
|
+
logger3.debug({ clientIp }, "Token authentication failed");
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (this.config.password) {
|
|
956
|
+
const passwordValid = validatePassword(credentials.password, this.config.password);
|
|
957
|
+
results.push({ method: "password", success: passwordValid });
|
|
958
|
+
if (passwordValid) {
|
|
959
|
+
logger3.debug({ clientIp }, "Password authentication succeeded");
|
|
960
|
+
} else {
|
|
961
|
+
logger3.debug({ clientIp }, "Password authentication failed");
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (this.config.allowedIps && this.config.allowedIps.length > 0) {
|
|
965
|
+
const ipValid = validateIp(clientIp, this.config.allowedIps);
|
|
966
|
+
results.push({ method: "ip", success: ipValid });
|
|
967
|
+
if (ipValid) {
|
|
968
|
+
logger3.debug({ clientIp }, "IP authentication succeeded");
|
|
969
|
+
} else {
|
|
970
|
+
logger3.debug({ clientIp }, "IP authentication failed");
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
if (results.length === 0) {
|
|
974
|
+
logger3.warn("Authentication configured but no methods available");
|
|
975
|
+
return {
|
|
976
|
+
success: false,
|
|
977
|
+
error: "Authentication required but not configured",
|
|
978
|
+
clientIp
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
if (this.config.requireAll) {
|
|
982
|
+
const allPassed = results.every((r) => r.success);
|
|
983
|
+
if (allPassed) {
|
|
984
|
+
logger3.info({ clientIp, methods: results.map((r) => r.method) }, "All authentication methods passed");
|
|
985
|
+
return { success: true, clientIp };
|
|
986
|
+
} else {
|
|
987
|
+
const failed = results.filter((r) => !r.success).map((r) => r.method);
|
|
988
|
+
logger3.warn({ clientIp, failedMethods: failed }, "Authentication failed (requireAll mode)");
|
|
989
|
+
return {
|
|
990
|
+
success: false,
|
|
991
|
+
error: `Authentication failed for methods: ${failed.join(", ")}`,
|
|
992
|
+
clientIp
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
} else {
|
|
996
|
+
const passed = results.find((r) => r.success);
|
|
997
|
+
if (passed) {
|
|
998
|
+
logger3.info({ clientIp, method: passed.method }, "Authentication succeeded");
|
|
999
|
+
return { success: true, method: passed.method, clientIp };
|
|
1000
|
+
} else {
|
|
1001
|
+
logger3.warn({ clientIp }, "All authentication methods failed");
|
|
1002
|
+
return {
|
|
1003
|
+
success: false,
|
|
1004
|
+
error: "Authentication failed",
|
|
1005
|
+
clientIp
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Get the configured authentication type
|
|
1012
|
+
*/
|
|
1013
|
+
getType() {
|
|
1014
|
+
return this.config.type;
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Check if authentication is required
|
|
1018
|
+
*/
|
|
1019
|
+
isRequired() {
|
|
1020
|
+
return this.config.type !== "none";
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
function createAuthConfigFromOptions(options) {
|
|
1024
|
+
const hasToken = !!options.authToken;
|
|
1025
|
+
const hasPassword = !!options.authPassword;
|
|
1026
|
+
const hasIp = options.authIp && options.authIp.length > 0;
|
|
1027
|
+
let type = "none";
|
|
1028
|
+
if (hasToken || hasPassword || hasIp) {
|
|
1029
|
+
const count = [hasToken, hasPassword, hasIp].filter(Boolean).length;
|
|
1030
|
+
if (count > 1) {
|
|
1031
|
+
type = "combined";
|
|
1032
|
+
} else if (hasToken) {
|
|
1033
|
+
type = "token";
|
|
1034
|
+
} else if (hasPassword) {
|
|
1035
|
+
type = "password";
|
|
1036
|
+
} else if (hasIp) {
|
|
1037
|
+
type = "ip";
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return {
|
|
1041
|
+
type,
|
|
1042
|
+
token: options.authToken,
|
|
1043
|
+
password: options.authPassword,
|
|
1044
|
+
allowedIps: options.authIp,
|
|
1045
|
+
requireAll: options.authRequireAll ?? false
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function validateAuthConfig(config) {
|
|
1049
|
+
const errors = [];
|
|
1050
|
+
const warnings = [];
|
|
1051
|
+
if (config.type === "token" && !config.token) {
|
|
1052
|
+
errors.push("Token authentication requires a token");
|
|
1053
|
+
}
|
|
1054
|
+
if (config.type === "password" && !config.password) {
|
|
1055
|
+
errors.push("Password authentication requires a password");
|
|
1056
|
+
}
|
|
1057
|
+
if (config.type === "ip" && (!config.allowedIps || config.allowedIps.length === 0)) {
|
|
1058
|
+
errors.push("IP authentication requires at least one allowed IP/CIDR");
|
|
1059
|
+
}
|
|
1060
|
+
if (config.allowedIps) {
|
|
1061
|
+
for (const cidr of config.allowedIps) {
|
|
1062
|
+
const [ip, prefix] = cidr.split("/");
|
|
1063
|
+
if (ipv4ToInt(ip) === -1) {
|
|
1064
|
+
warnings.push(`IP address may not be valid IPv4: ${ip}`);
|
|
1065
|
+
}
|
|
1066
|
+
if (prefix !== void 0) {
|
|
1067
|
+
const prefixNum = parseInt(prefix, 10);
|
|
1068
|
+
if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 32) {
|
|
1069
|
+
errors.push(`Invalid CIDR prefix: ${cidr}`);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (config.token && config.token.length < 16) {
|
|
1075
|
+
warnings.push("Token is shorter than 16 characters - consider using a longer token");
|
|
1076
|
+
}
|
|
1077
|
+
if (config.password && config.password.length < 8) {
|
|
1078
|
+
warnings.push("Password is shorter than 8 characters - consider using a longer password");
|
|
1079
|
+
}
|
|
1080
|
+
return {
|
|
1081
|
+
valid: errors.length === 0,
|
|
1082
|
+
errors,
|
|
1083
|
+
warnings
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
597
1087
|
// src/bridge/messages.ts
|
|
598
1088
|
function createContextSyncMessage(source, context) {
|
|
599
1089
|
const message = createMessage("context_sync", source);
|
|
@@ -644,17 +1134,20 @@ function createNotificationMessage(source, notification) {
|
|
|
644
1134
|
|
|
645
1135
|
// src/bridge/core.ts
|
|
646
1136
|
import { WebSocketServer } from "ws";
|
|
1137
|
+
import * as https2 from "https";
|
|
647
1138
|
import { v4 as uuidv42 } from "uuid";
|
|
648
|
-
import * as
|
|
1139
|
+
import * as fs2 from "fs";
|
|
649
1140
|
import * as path from "path";
|
|
650
1141
|
import * as os from "os";
|
|
651
|
-
var
|
|
1142
|
+
var logger4 = createLogger("bridge");
|
|
652
1143
|
var Bridge = class {
|
|
653
1144
|
config;
|
|
654
1145
|
server = null;
|
|
1146
|
+
httpsServer = null;
|
|
655
1147
|
clientTransport = null;
|
|
656
1148
|
peers = /* @__PURE__ */ new Map();
|
|
657
1149
|
started = false;
|
|
1150
|
+
authenticator = null;
|
|
658
1151
|
// Event handlers
|
|
659
1152
|
peerConnectedHandlers = [];
|
|
660
1153
|
peerDisconnectedHandlers = [];
|
|
@@ -675,7 +1168,7 @@ var Bridge = class {
|
|
|
675
1168
|
constructor(config) {
|
|
676
1169
|
this.config = config;
|
|
677
1170
|
this.validateConfig();
|
|
678
|
-
|
|
1171
|
+
logger4.info({ instanceName: config.instanceName, mode: config.mode }, "Bridge instance created");
|
|
679
1172
|
}
|
|
680
1173
|
/**
|
|
681
1174
|
* Validate the configuration based on mode requirements
|
|
@@ -688,28 +1181,32 @@ var Bridge = class {
|
|
|
688
1181
|
if (mode === "client" && !connect) {
|
|
689
1182
|
throw new Error("'client' mode requires 'connect' configuration");
|
|
690
1183
|
}
|
|
1184
|
+
if (mode === "peer" && !listen && !connect) {
|
|
1185
|
+
throw new Error("'peer' mode requires either 'listen' or 'connect' configuration");
|
|
1186
|
+
}
|
|
691
1187
|
}
|
|
692
1188
|
/**
|
|
693
1189
|
* Start the bridge based on configured mode
|
|
694
1190
|
* - 'host': Starts WebSocket server, sends commands via MCP
|
|
695
1191
|
* - 'client': Connects to host, receives and executes commands
|
|
1192
|
+
* - 'peer': Starts server (if listen configured) and connects to remote (if connect configured)
|
|
696
1193
|
*/
|
|
697
1194
|
async start() {
|
|
698
1195
|
if (this.started) {
|
|
699
1196
|
throw new Error("Bridge is already started");
|
|
700
1197
|
}
|
|
701
1198
|
const { mode } = this.config;
|
|
702
|
-
|
|
1199
|
+
logger4.info({ mode }, "Starting bridge");
|
|
703
1200
|
try {
|
|
704
|
-
if (mode === "host" && this.config.listen) {
|
|
1201
|
+
if ((mode === "host" || mode === "peer") && this.config.listen) {
|
|
705
1202
|
await this.startServer();
|
|
706
1203
|
}
|
|
707
|
-
if (mode === "client" && this.config.connect) {
|
|
1204
|
+
if ((mode === "client" || mode === "peer") && this.config.connect) {
|
|
708
1205
|
await this.connectToRemote();
|
|
709
1206
|
}
|
|
710
1207
|
this.started = true;
|
|
711
1208
|
this.writeStatusFile();
|
|
712
|
-
|
|
1209
|
+
logger4.info({ mode, instanceName: this.config.instanceName }, "Bridge started successfully");
|
|
713
1210
|
} catch (error) {
|
|
714
1211
|
await this.cleanup();
|
|
715
1212
|
throw error;
|
|
@@ -722,10 +1219,10 @@ var Bridge = class {
|
|
|
722
1219
|
if (!this.started) {
|
|
723
1220
|
return;
|
|
724
1221
|
}
|
|
725
|
-
|
|
1222
|
+
logger4.info("Stopping bridge");
|
|
726
1223
|
await this.cleanup();
|
|
727
1224
|
this.started = false;
|
|
728
|
-
|
|
1225
|
+
logger4.info("Bridge stopped");
|
|
729
1226
|
}
|
|
730
1227
|
/**
|
|
731
1228
|
* Get list of connected peers
|
|
@@ -766,9 +1263,9 @@ var Bridge = class {
|
|
|
766
1263
|
});
|
|
767
1264
|
transport._peerId = peerId;
|
|
768
1265
|
this.notifyPeerConnected(peerInfo);
|
|
769
|
-
|
|
1266
|
+
logger4.info({ peerId, url }, "Connected to remote peer");
|
|
770
1267
|
} catch (error) {
|
|
771
|
-
|
|
1268
|
+
logger4.error({ error: error.message, url }, "Failed to connect to remote peer");
|
|
772
1269
|
throw error;
|
|
773
1270
|
}
|
|
774
1271
|
}
|
|
@@ -789,7 +1286,7 @@ var Bridge = class {
|
|
|
789
1286
|
}
|
|
790
1287
|
this.peers.delete(peerId);
|
|
791
1288
|
this.notifyPeerDisconnected(peer.info);
|
|
792
|
-
|
|
1289
|
+
logger4.info({ peerId }, "Disconnected from peer");
|
|
793
1290
|
}
|
|
794
1291
|
/**
|
|
795
1292
|
* Send a message to a specific peer
|
|
@@ -817,7 +1314,7 @@ var Bridge = class {
|
|
|
817
1314
|
} else {
|
|
818
1315
|
throw new Error("No transport available for peer");
|
|
819
1316
|
}
|
|
820
|
-
|
|
1317
|
+
logger4.debug({ peerId, messageId: message.id, type: message.type }, "Sent message to peer");
|
|
821
1318
|
}
|
|
822
1319
|
/**
|
|
823
1320
|
* Broadcast a message to all connected peers
|
|
@@ -826,11 +1323,11 @@ var Bridge = class {
|
|
|
826
1323
|
async broadcast(message) {
|
|
827
1324
|
const sendPromises = Array.from(this.peers.keys()).map(
|
|
828
1325
|
(peerId) => this.sendToPeer(peerId, message).catch((error) => {
|
|
829
|
-
|
|
1326
|
+
logger4.error({ error: error.message, peerId }, "Failed to send to peer");
|
|
830
1327
|
})
|
|
831
1328
|
);
|
|
832
1329
|
await Promise.all(sendPromises);
|
|
833
|
-
|
|
1330
|
+
logger4.debug({ messageId: message.id, peerCount: this.peers.size }, "Broadcast message sent");
|
|
834
1331
|
}
|
|
835
1332
|
// ============================================================================
|
|
836
1333
|
// Event Registration
|
|
@@ -920,7 +1417,7 @@ var Bridge = class {
|
|
|
920
1417
|
this.pendingTasks.delete(task.id);
|
|
921
1418
|
reject(error);
|
|
922
1419
|
});
|
|
923
|
-
|
|
1420
|
+
logger4.debug({ taskId: task.id, peerId, timeout }, "Task delegated");
|
|
924
1421
|
});
|
|
925
1422
|
}
|
|
926
1423
|
// ============================================================================
|
|
@@ -936,10 +1433,10 @@ var Bridge = class {
|
|
|
936
1433
|
const message = createContextSyncMessage(this.config.instanceName, contextToSync);
|
|
937
1434
|
if (peerId) {
|
|
938
1435
|
await this.sendToPeer(peerId, message);
|
|
939
|
-
|
|
1436
|
+
logger4.debug({ peerId, messageId: message.id }, "Context synced to peer");
|
|
940
1437
|
} else {
|
|
941
1438
|
await this.broadcast(message);
|
|
942
|
-
|
|
1439
|
+
logger4.debug({ peerCount: this.peers.size, messageId: message.id }, "Context synced to all peers");
|
|
943
1440
|
}
|
|
944
1441
|
}
|
|
945
1442
|
/**
|
|
@@ -982,7 +1479,7 @@ var Bridge = class {
|
|
|
982
1479
|
this.pendingContextRequests.delete(message.id);
|
|
983
1480
|
reject(error);
|
|
984
1481
|
});
|
|
985
|
-
|
|
1482
|
+
logger4.debug({ requestId: message.id, peerId, query }, "Context requested");
|
|
986
1483
|
});
|
|
987
1484
|
}
|
|
988
1485
|
/**
|
|
@@ -998,10 +1495,10 @@ var Bridge = class {
|
|
|
998
1495
|
const context = contextProvider ? await contextProvider() : void 0;
|
|
999
1496
|
await this.syncContext(context);
|
|
1000
1497
|
} catch (error) {
|
|
1001
|
-
|
|
1498
|
+
logger4.error({ error: error.message }, "Auto-sync error");
|
|
1002
1499
|
}
|
|
1003
1500
|
}, interval);
|
|
1004
|
-
|
|
1501
|
+
logger4.info({ interval }, "Auto-sync started");
|
|
1005
1502
|
}
|
|
1006
1503
|
/**
|
|
1007
1504
|
* Stop automatic context synchronization
|
|
@@ -1010,7 +1507,7 @@ var Bridge = class {
|
|
|
1010
1507
|
if (this.autoSyncIntervalId) {
|
|
1011
1508
|
clearInterval(this.autoSyncIntervalId);
|
|
1012
1509
|
this.autoSyncIntervalId = null;
|
|
1013
|
-
|
|
1510
|
+
logger4.info("Auto-sync stopped");
|
|
1014
1511
|
}
|
|
1015
1512
|
}
|
|
1016
1513
|
// ============================================================================
|
|
@@ -1018,30 +1515,97 @@ var Bridge = class {
|
|
|
1018
1515
|
// ============================================================================
|
|
1019
1516
|
/**
|
|
1020
1517
|
* Start the WebSocket server
|
|
1518
|
+
* If TLS is configured, starts an HTTPS server with WebSocket upgrade
|
|
1519
|
+
* If auth is configured, validates connections before accepting
|
|
1021
1520
|
*/
|
|
1022
1521
|
async startServer() {
|
|
1023
1522
|
const { listen } = this.config;
|
|
1024
1523
|
if (!listen) {
|
|
1025
1524
|
throw new Error("Listen configuration is required");
|
|
1026
1525
|
}
|
|
1526
|
+
const host = listen.host ?? "0.0.0.0";
|
|
1527
|
+
const port = listen.port;
|
|
1528
|
+
const useTls = isTLSEnabled(listen.tls);
|
|
1529
|
+
if (listen.auth && listen.auth.type !== "none") {
|
|
1530
|
+
this.authenticator = new Authenticator(listen.auth);
|
|
1531
|
+
logger4.info({ authType: listen.auth.type }, "Authentication enabled");
|
|
1532
|
+
}
|
|
1027
1533
|
return new Promise((resolve, reject) => {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1534
|
+
logger4.debug({ host, port, tls: useTls, auth: !!this.authenticator }, "Starting WebSocket server");
|
|
1535
|
+
try {
|
|
1536
|
+
if (useTls && listen.tls) {
|
|
1537
|
+
const tlsOptions = loadCertificatesSync(listen.tls);
|
|
1538
|
+
logger4.info({ host, port }, "Starting secure WebSocket server (wss://)");
|
|
1539
|
+
this.httpsServer = https2.createServer({
|
|
1540
|
+
cert: tlsOptions.cert,
|
|
1541
|
+
key: tlsOptions.key,
|
|
1542
|
+
ca: tlsOptions.ca,
|
|
1543
|
+
passphrase: tlsOptions.passphrase
|
|
1544
|
+
});
|
|
1545
|
+
const wsOptions = {
|
|
1546
|
+
server: this.httpsServer
|
|
1547
|
+
};
|
|
1548
|
+
if (this.authenticator) {
|
|
1549
|
+
wsOptions.verifyClient = (info, callback) => {
|
|
1550
|
+
this.verifyClient(info, callback);
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
this.server = new WebSocketServer(wsOptions);
|
|
1554
|
+
this.httpsServer.listen(port, host, () => {
|
|
1555
|
+
logger4.info({ host, port, protocol: "wss" }, "Secure WebSocket server listening");
|
|
1556
|
+
resolve();
|
|
1557
|
+
});
|
|
1558
|
+
this.httpsServer.on("error", (error) => {
|
|
1559
|
+
logger4.error({ error: error.message }, "HTTPS server error");
|
|
1560
|
+
reject(error);
|
|
1561
|
+
});
|
|
1562
|
+
} else {
|
|
1563
|
+
const wsOptions = {
|
|
1564
|
+
host,
|
|
1565
|
+
port
|
|
1566
|
+
};
|
|
1567
|
+
if (this.authenticator) {
|
|
1568
|
+
wsOptions.verifyClient = (info, callback) => {
|
|
1569
|
+
this.verifyClient(info, callback);
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
this.server = new WebSocketServer(wsOptions);
|
|
1573
|
+
this.server.on("listening", () => {
|
|
1574
|
+
logger4.info({ host, port, protocol: "ws" }, "WebSocket server listening");
|
|
1575
|
+
resolve();
|
|
1576
|
+
});
|
|
1577
|
+
this.server.on("error", (error) => {
|
|
1578
|
+
logger4.error({ error: error.message }, "WebSocket server error");
|
|
1579
|
+
reject(error);
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
this.server.on("connection", (ws, request) => {
|
|
1583
|
+
this.handleNewConnection(ws, request);
|
|
1584
|
+
});
|
|
1585
|
+
} catch (error) {
|
|
1586
|
+
logger4.error({ error: error.message }, "Failed to start server");
|
|
1038
1587
|
reject(error);
|
|
1039
|
-
}
|
|
1040
|
-
this.server.on("connection", (ws, request) => {
|
|
1041
|
-
this.handleNewConnection(ws, request);
|
|
1042
|
-
});
|
|
1588
|
+
}
|
|
1043
1589
|
});
|
|
1044
1590
|
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Verify client connection for authentication
|
|
1593
|
+
* Used as WebSocketServer verifyClient callback
|
|
1594
|
+
*/
|
|
1595
|
+
verifyClient(info, callback) {
|
|
1596
|
+
if (!this.authenticator) {
|
|
1597
|
+
callback(true);
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
const result = this.authenticator.authenticate(info.req);
|
|
1601
|
+
if (result.success) {
|
|
1602
|
+
logger4.info({ clientIp: result.clientIp, method: result.method }, "Client authenticated");
|
|
1603
|
+
callback(true);
|
|
1604
|
+
} else {
|
|
1605
|
+
logger4.warn({ clientIp: result.clientIp, error: result.error }, "Client authentication failed");
|
|
1606
|
+
callback(false, 4001, result.error || "Authentication failed");
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1045
1609
|
/**
|
|
1046
1610
|
* Handle a new incoming connection
|
|
1047
1611
|
*/
|
|
@@ -1058,7 +1622,7 @@ var Bridge = class {
|
|
|
1058
1622
|
info: peerInfo,
|
|
1059
1623
|
ws
|
|
1060
1624
|
});
|
|
1061
|
-
|
|
1625
|
+
logger4.info({ peerId, url: request.url }, "New peer connected");
|
|
1062
1626
|
ws.on("message", (data) => {
|
|
1063
1627
|
const messageString = data.toString();
|
|
1064
1628
|
const result = safeDeserializeMessage(messageString);
|
|
@@ -1066,16 +1630,16 @@ var Bridge = class {
|
|
|
1066
1630
|
peerInfo.lastActivity = Date.now();
|
|
1067
1631
|
this.handleMessage(result.data, ws, peerId);
|
|
1068
1632
|
} else {
|
|
1069
|
-
|
|
1633
|
+
logger4.warn({ peerId, error: result.error.message }, "Invalid message received");
|
|
1070
1634
|
}
|
|
1071
1635
|
});
|
|
1072
1636
|
ws.on("close", (code, reason) => {
|
|
1073
|
-
|
|
1637
|
+
logger4.info({ peerId, code, reason: reason.toString() }, "Peer disconnected");
|
|
1074
1638
|
this.peers.delete(peerId);
|
|
1075
1639
|
this.notifyPeerDisconnected(peerInfo);
|
|
1076
1640
|
});
|
|
1077
1641
|
ws.on("error", (error) => {
|
|
1078
|
-
|
|
1642
|
+
logger4.error({ peerId, error: error.message }, "Peer connection error");
|
|
1079
1643
|
});
|
|
1080
1644
|
this.notifyPeerConnected(peerInfo);
|
|
1081
1645
|
}
|
|
@@ -1094,7 +1658,8 @@ var Bridge = class {
|
|
|
1094
1658
|
if (!url) {
|
|
1095
1659
|
const host = connect.hostGateway ? "host.docker.internal" : "localhost";
|
|
1096
1660
|
const port = connect.port ?? 8765;
|
|
1097
|
-
|
|
1661
|
+
const protocol = isTLSEnabled(connect.tls) || connect.tls?.ca ? "wss" : "ws";
|
|
1662
|
+
url = `${protocol}://${host}:${port}`;
|
|
1098
1663
|
}
|
|
1099
1664
|
this.clientTransport = new WebSocketTransport();
|
|
1100
1665
|
this.clientTransport.onMessage((message) => {
|
|
@@ -1108,7 +1673,9 @@ var Bridge = class {
|
|
|
1108
1673
|
url,
|
|
1109
1674
|
reconnect: true,
|
|
1110
1675
|
reconnectInterval: 1e3,
|
|
1111
|
-
maxReconnectAttempts: 10
|
|
1676
|
+
maxReconnectAttempts: 10,
|
|
1677
|
+
tls: connect.tls,
|
|
1678
|
+
auth: connect.auth
|
|
1112
1679
|
});
|
|
1113
1680
|
const peerId = uuidv42();
|
|
1114
1681
|
const peerInfo = {
|
|
@@ -1123,9 +1690,9 @@ var Bridge = class {
|
|
|
1123
1690
|
});
|
|
1124
1691
|
this.clientTransport._peerId = peerId;
|
|
1125
1692
|
this.notifyPeerConnected(peerInfo);
|
|
1126
|
-
|
|
1693
|
+
logger4.info({ peerId, url }, "Connected to remote bridge");
|
|
1127
1694
|
} catch (error) {
|
|
1128
|
-
|
|
1695
|
+
logger4.error({ error: error.message }, "Failed to connect to remote bridge");
|
|
1129
1696
|
this.clientTransport = null;
|
|
1130
1697
|
throw error;
|
|
1131
1698
|
}
|
|
@@ -1141,7 +1708,7 @@ var Bridge = class {
|
|
|
1141
1708
|
if (peer) {
|
|
1142
1709
|
this.peers.delete(peerId);
|
|
1143
1710
|
this.notifyPeerDisconnected(peer.info);
|
|
1144
|
-
|
|
1711
|
+
logger4.info({ peerId }, "Client transport disconnected");
|
|
1145
1712
|
}
|
|
1146
1713
|
}
|
|
1147
1714
|
}
|
|
@@ -1165,14 +1732,14 @@ var Bridge = class {
|
|
|
1165
1732
|
}
|
|
1166
1733
|
}
|
|
1167
1734
|
if (!peerId) {
|
|
1168
|
-
|
|
1735
|
+
logger4.warn({ messageId: message.id }, "Received message from unknown peer");
|
|
1169
1736
|
return;
|
|
1170
1737
|
}
|
|
1171
1738
|
const peer = this.peers.get(peerId);
|
|
1172
1739
|
if (peer) {
|
|
1173
1740
|
peer.info.lastActivity = Date.now();
|
|
1174
1741
|
}
|
|
1175
|
-
|
|
1742
|
+
logger4.debug({ peerId, messageId: message.id, type: message.type }, "Received message");
|
|
1176
1743
|
if (message.type === "task_delegate" && message.task) {
|
|
1177
1744
|
this.handleTaskDelegate(message, peerId);
|
|
1178
1745
|
return;
|
|
@@ -1200,22 +1767,22 @@ var Bridge = class {
|
|
|
1200
1767
|
*/
|
|
1201
1768
|
async handleTaskDelegate(message, peerId) {
|
|
1202
1769
|
const task = message.task;
|
|
1203
|
-
|
|
1770
|
+
logger4.debug({ taskId: task.id, peerId }, "Received task delegation");
|
|
1204
1771
|
if (!this.taskReceivedHandler) {
|
|
1205
1772
|
const otherPeers = Array.from(this.peers.keys()).filter((id) => id !== peerId);
|
|
1206
1773
|
if (otherPeers.length > 0) {
|
|
1207
1774
|
const targetPeerId = otherPeers[0];
|
|
1208
|
-
|
|
1775
|
+
logger4.info({ taskId: task.id, targetPeerId }, "Forwarding task to another peer");
|
|
1209
1776
|
try {
|
|
1210
1777
|
await this.sendToPeer(targetPeerId, message);
|
|
1211
1778
|
const forwardKey = `forward:${task.id}`;
|
|
1212
1779
|
this[forwardKey] = peerId;
|
|
1213
1780
|
return;
|
|
1214
1781
|
} catch (err) {
|
|
1215
|
-
|
|
1782
|
+
logger4.error({ error: err.message, taskId: task.id }, "Failed to forward task");
|
|
1216
1783
|
}
|
|
1217
1784
|
}
|
|
1218
|
-
|
|
1785
|
+
logger4.warn({ taskId: task.id }, "No task handler registered and no peers to forward to");
|
|
1219
1786
|
const response = createTaskResponseMessage(
|
|
1220
1787
|
this.config.instanceName,
|
|
1221
1788
|
task.id,
|
|
@@ -1226,7 +1793,7 @@ var Bridge = class {
|
|
|
1226
1793
|
}
|
|
1227
1794
|
);
|
|
1228
1795
|
await this.sendToPeer(peerId, response).catch((err) => {
|
|
1229
|
-
|
|
1796
|
+
logger4.error({ error: err.message, taskId: task.id }, "Failed to send error response");
|
|
1230
1797
|
});
|
|
1231
1798
|
return;
|
|
1232
1799
|
}
|
|
@@ -1238,7 +1805,7 @@ var Bridge = class {
|
|
|
1238
1805
|
result
|
|
1239
1806
|
);
|
|
1240
1807
|
await this.sendToPeer(peerId, response);
|
|
1241
|
-
|
|
1808
|
+
logger4.debug({ taskId: task.id, success: result.success }, "Task response sent");
|
|
1242
1809
|
} catch (error) {
|
|
1243
1810
|
const response = createTaskResponseMessage(
|
|
1244
1811
|
this.config.instanceName,
|
|
@@ -1250,9 +1817,9 @@ var Bridge = class {
|
|
|
1250
1817
|
}
|
|
1251
1818
|
);
|
|
1252
1819
|
await this.sendToPeer(peerId, response).catch((err) => {
|
|
1253
|
-
|
|
1820
|
+
logger4.error({ error: err.message, taskId: task.id }, "Failed to send error response");
|
|
1254
1821
|
});
|
|
1255
|
-
|
|
1822
|
+
logger4.error({ taskId: task.id, error: error.message }, "Task handler error");
|
|
1256
1823
|
}
|
|
1257
1824
|
}
|
|
1258
1825
|
/**
|
|
@@ -1264,15 +1831,15 @@ var Bridge = class {
|
|
|
1264
1831
|
const originalSender = this[forwardKey];
|
|
1265
1832
|
if (originalSender) {
|
|
1266
1833
|
delete this[forwardKey];
|
|
1267
|
-
|
|
1834
|
+
logger4.info({ taskId, originalSender }, "Forwarding task response to original sender");
|
|
1268
1835
|
await this.sendToPeer(originalSender, message).catch((err) => {
|
|
1269
|
-
|
|
1836
|
+
logger4.error({ error: err.message, taskId }, "Failed to forward response");
|
|
1270
1837
|
});
|
|
1271
1838
|
return;
|
|
1272
1839
|
}
|
|
1273
1840
|
const pending = this.pendingTasks.get(taskId);
|
|
1274
1841
|
if (!pending) {
|
|
1275
|
-
|
|
1842
|
+
logger4.warn({ taskId }, "Received response for unknown task");
|
|
1276
1843
|
return;
|
|
1277
1844
|
}
|
|
1278
1845
|
clearTimeout(pending.timeoutId);
|
|
@@ -1285,7 +1852,7 @@ var Bridge = class {
|
|
|
1285
1852
|
followUp: message.result.followUp,
|
|
1286
1853
|
error: message.result.error
|
|
1287
1854
|
};
|
|
1288
|
-
|
|
1855
|
+
logger4.debug({ taskId, success: result.success }, "Task result received");
|
|
1289
1856
|
pending.resolve(result);
|
|
1290
1857
|
}
|
|
1291
1858
|
/**
|
|
@@ -1293,7 +1860,7 @@ var Bridge = class {
|
|
|
1293
1860
|
*/
|
|
1294
1861
|
handleContextSync(message, peerId) {
|
|
1295
1862
|
const context = message.context;
|
|
1296
|
-
|
|
1863
|
+
logger4.debug({ peerId, messageId: message.id }, "Received context sync");
|
|
1297
1864
|
this.notifyContextReceived(context, peerId);
|
|
1298
1865
|
}
|
|
1299
1866
|
/**
|
|
@@ -1301,22 +1868,22 @@ var Bridge = class {
|
|
|
1301
1868
|
*/
|
|
1302
1869
|
async handleContextRequest(message, peerId) {
|
|
1303
1870
|
const query = message.context.summary;
|
|
1304
|
-
|
|
1871
|
+
logger4.debug({ peerId, messageId: message.id, query }, "Received context request");
|
|
1305
1872
|
if (!this.contextRequestedHandler) {
|
|
1306
1873
|
const otherPeers = Array.from(this.peers.keys()).filter((id) => id !== peerId);
|
|
1307
1874
|
if (otherPeers.length > 0) {
|
|
1308
1875
|
const targetPeerId = otherPeers[0];
|
|
1309
|
-
|
|
1876
|
+
logger4.info({ messageId: message.id, targetPeerId }, "Forwarding context request to another peer");
|
|
1310
1877
|
try {
|
|
1311
1878
|
await this.sendToPeer(targetPeerId, message);
|
|
1312
1879
|
const forwardKey = `ctxfwd:${message.id}`;
|
|
1313
1880
|
this[forwardKey] = peerId;
|
|
1314
1881
|
return;
|
|
1315
1882
|
} catch (err) {
|
|
1316
|
-
|
|
1883
|
+
logger4.error({ error: err.message, messageId: message.id }, "Failed to forward context request");
|
|
1317
1884
|
}
|
|
1318
1885
|
}
|
|
1319
|
-
|
|
1886
|
+
logger4.warn({ messageId: message.id }, "No context request handler registered and no peers to forward to");
|
|
1320
1887
|
const response = createContextSyncMessage(this.config.instanceName, { files: [] });
|
|
1321
1888
|
const responseMessage = {
|
|
1322
1889
|
...response,
|
|
@@ -1327,7 +1894,7 @@ var Bridge = class {
|
|
|
1327
1894
|
}
|
|
1328
1895
|
};
|
|
1329
1896
|
await this.sendToPeer(peerId, responseMessage).catch((err) => {
|
|
1330
|
-
|
|
1897
|
+
logger4.error({ error: err.message, messageId: message.id }, "Failed to send empty context response");
|
|
1331
1898
|
});
|
|
1332
1899
|
return;
|
|
1333
1900
|
}
|
|
@@ -1343,7 +1910,7 @@ var Bridge = class {
|
|
|
1343
1910
|
}
|
|
1344
1911
|
};
|
|
1345
1912
|
await this.sendToPeer(peerId, responseMessage);
|
|
1346
|
-
|
|
1913
|
+
logger4.debug({ messageId: message.id, fileCount: files.length }, "Context response sent");
|
|
1347
1914
|
} catch (error) {
|
|
1348
1915
|
const response = createContextSyncMessage(this.config.instanceName, {
|
|
1349
1916
|
files: [],
|
|
@@ -1358,9 +1925,9 @@ var Bridge = class {
|
|
|
1358
1925
|
}
|
|
1359
1926
|
};
|
|
1360
1927
|
await this.sendToPeer(peerId, responseMessage).catch((err) => {
|
|
1361
|
-
|
|
1928
|
+
logger4.error({ error: err.message, messageId: message.id }, "Failed to send error context response");
|
|
1362
1929
|
});
|
|
1363
|
-
|
|
1930
|
+
logger4.error({ messageId: message.id, error: error.message }, "Context request handler error");
|
|
1364
1931
|
}
|
|
1365
1932
|
}
|
|
1366
1933
|
/**
|
|
@@ -1369,22 +1936,22 @@ var Bridge = class {
|
|
|
1369
1936
|
async handleContextResponse(message) {
|
|
1370
1937
|
const requestId = message.context?.variables?.requestId;
|
|
1371
1938
|
if (!requestId) {
|
|
1372
|
-
|
|
1939
|
+
logger4.warn({ messageId: message.id }, "Context response without requestId");
|
|
1373
1940
|
return;
|
|
1374
1941
|
}
|
|
1375
1942
|
const forwardKey = `ctxfwd:${requestId}`;
|
|
1376
1943
|
const originalSender = this[forwardKey];
|
|
1377
1944
|
if (originalSender) {
|
|
1378
1945
|
delete this[forwardKey];
|
|
1379
|
-
|
|
1946
|
+
logger4.info({ requestId, originalSender }, "Forwarding context response to original sender");
|
|
1380
1947
|
await this.sendToPeer(originalSender, message).catch((err) => {
|
|
1381
|
-
|
|
1948
|
+
logger4.error({ error: err.message, requestId }, "Failed to forward context response");
|
|
1382
1949
|
});
|
|
1383
1950
|
return;
|
|
1384
1951
|
}
|
|
1385
1952
|
const pending = this.pendingContextRequests.get(requestId);
|
|
1386
1953
|
if (!pending) {
|
|
1387
|
-
|
|
1954
|
+
logger4.warn({ requestId }, "Received response for unknown context request");
|
|
1388
1955
|
return;
|
|
1389
1956
|
}
|
|
1390
1957
|
clearTimeout(pending.timeoutId);
|
|
@@ -1395,7 +1962,7 @@ var Bridge = class {
|
|
|
1395
1962
|
return;
|
|
1396
1963
|
}
|
|
1397
1964
|
const files = message.context?.files ?? [];
|
|
1398
|
-
|
|
1965
|
+
logger4.debug({ requestId, fileCount: files.length }, "Context response received");
|
|
1399
1966
|
pending.resolve(files);
|
|
1400
1967
|
}
|
|
1401
1968
|
// ============================================================================
|
|
@@ -1407,7 +1974,7 @@ var Bridge = class {
|
|
|
1407
1974
|
try {
|
|
1408
1975
|
handler(peer);
|
|
1409
1976
|
} catch (error) {
|
|
1410
|
-
|
|
1977
|
+
logger4.error({ error: error.message }, "Peer connected handler error");
|
|
1411
1978
|
}
|
|
1412
1979
|
}
|
|
1413
1980
|
}
|
|
@@ -1430,7 +1997,7 @@ var Bridge = class {
|
|
|
1430
1997
|
try {
|
|
1431
1998
|
handler(peer);
|
|
1432
1999
|
} catch (error) {
|
|
1433
|
-
|
|
2000
|
+
logger4.error({ error: error.message }, "Peer disconnected handler error");
|
|
1434
2001
|
}
|
|
1435
2002
|
}
|
|
1436
2003
|
this.writeStatusFile();
|
|
@@ -1440,7 +2007,7 @@ var Bridge = class {
|
|
|
1440
2007
|
try {
|
|
1441
2008
|
handler(message, peerId);
|
|
1442
2009
|
} catch (error) {
|
|
1443
|
-
|
|
2010
|
+
logger4.error({ error: error.message }, "Message received handler error");
|
|
1444
2011
|
}
|
|
1445
2012
|
}
|
|
1446
2013
|
}
|
|
@@ -1449,7 +2016,7 @@ var Bridge = class {
|
|
|
1449
2016
|
try {
|
|
1450
2017
|
handler(context, peerId);
|
|
1451
2018
|
} catch (error) {
|
|
1452
|
-
|
|
2019
|
+
logger4.error({ error: error.message }, "Context received handler error");
|
|
1453
2020
|
}
|
|
1454
2021
|
}
|
|
1455
2022
|
}
|
|
@@ -1460,9 +2027,9 @@ var Bridge = class {
|
|
|
1460
2027
|
* Clean up all resources
|
|
1461
2028
|
*/
|
|
1462
2029
|
async cleanup() {
|
|
1463
|
-
|
|
2030
|
+
logger4.debug("Starting cleanup");
|
|
1464
2031
|
this.stopAutoSync();
|
|
1465
|
-
|
|
2032
|
+
logger4.debug("Auto-sync stopped");
|
|
1466
2033
|
const pendingTaskCount = this.pendingTasks.size;
|
|
1467
2034
|
for (const [taskId, pending] of this.pendingTasks) {
|
|
1468
2035
|
clearTimeout(pending.timeoutId);
|
|
@@ -1470,7 +2037,7 @@ var Bridge = class {
|
|
|
1470
2037
|
}
|
|
1471
2038
|
this.pendingTasks.clear();
|
|
1472
2039
|
if (pendingTaskCount > 0) {
|
|
1473
|
-
|
|
2040
|
+
logger4.debug({ count: pendingTaskCount }, "Pending tasks cancelled");
|
|
1474
2041
|
}
|
|
1475
2042
|
const pendingContextCount = this.pendingContextRequests.size;
|
|
1476
2043
|
for (const [requestId, pending] of this.pendingContextRequests) {
|
|
@@ -1479,20 +2046,20 @@ var Bridge = class {
|
|
|
1479
2046
|
}
|
|
1480
2047
|
this.pendingContextRequests.clear();
|
|
1481
2048
|
if (pendingContextCount > 0) {
|
|
1482
|
-
|
|
2049
|
+
logger4.debug({ count: pendingContextCount }, "Pending context requests cancelled");
|
|
1483
2050
|
}
|
|
1484
2051
|
if (this.clientTransport) {
|
|
1485
|
-
|
|
2052
|
+
logger4.debug("Disconnecting client transport");
|
|
1486
2053
|
try {
|
|
1487
2054
|
await this.clientTransport.disconnect();
|
|
1488
|
-
|
|
2055
|
+
logger4.debug("Client transport disconnected");
|
|
1489
2056
|
} catch {
|
|
1490
2057
|
}
|
|
1491
2058
|
this.clientTransport = null;
|
|
1492
2059
|
}
|
|
1493
2060
|
const peerCount = this.peers.size;
|
|
1494
2061
|
if (peerCount > 0) {
|
|
1495
|
-
|
|
2062
|
+
logger4.debug({ count: peerCount }, "Closing peer connections");
|
|
1496
2063
|
}
|
|
1497
2064
|
for (const [peerId, peer] of this.peers) {
|
|
1498
2065
|
if (peer.ws) {
|
|
@@ -1507,21 +2074,31 @@ var Bridge = class {
|
|
|
1507
2074
|
} catch {
|
|
1508
2075
|
}
|
|
1509
2076
|
}
|
|
1510
|
-
|
|
2077
|
+
logger4.debug({ peerId }, "Peer disconnected");
|
|
1511
2078
|
}
|
|
1512
2079
|
this.peers.clear();
|
|
1513
2080
|
if (this.server) {
|
|
1514
|
-
|
|
2081
|
+
logger4.debug("Closing WebSocket server");
|
|
1515
2082
|
await new Promise((resolve) => {
|
|
1516
2083
|
this.server.close(() => {
|
|
1517
2084
|
resolve();
|
|
1518
2085
|
});
|
|
1519
2086
|
});
|
|
1520
2087
|
this.server = null;
|
|
1521
|
-
|
|
2088
|
+
logger4.debug("WebSocket server closed");
|
|
2089
|
+
}
|
|
2090
|
+
if (this.httpsServer) {
|
|
2091
|
+
logger4.debug("Closing HTTPS server");
|
|
2092
|
+
await new Promise((resolve) => {
|
|
2093
|
+
this.httpsServer.close(() => {
|
|
2094
|
+
resolve();
|
|
2095
|
+
});
|
|
2096
|
+
});
|
|
2097
|
+
this.httpsServer = null;
|
|
2098
|
+
logger4.debug("HTTPS server closed");
|
|
1522
2099
|
}
|
|
1523
2100
|
this.removeStatusFile();
|
|
1524
|
-
|
|
2101
|
+
logger4.debug("Cleanup complete");
|
|
1525
2102
|
}
|
|
1526
2103
|
// ============================================================================
|
|
1527
2104
|
// Status File Management
|
|
@@ -1538,8 +2115,8 @@ var Bridge = class {
|
|
|
1538
2115
|
*/
|
|
1539
2116
|
ensureBridgeDir() {
|
|
1540
2117
|
const bridgeDir = path.join(os.homedir(), ".claude-bridge");
|
|
1541
|
-
if (!
|
|
1542
|
-
|
|
2118
|
+
if (!fs2.existsSync(bridgeDir)) {
|
|
2119
|
+
fs2.mkdirSync(bridgeDir, { recursive: true });
|
|
1543
2120
|
}
|
|
1544
2121
|
}
|
|
1545
2122
|
/**
|
|
@@ -1560,10 +2137,10 @@ var Bridge = class {
|
|
|
1560
2137
|
lastActivity: new Date(p.lastActivity).toISOString()
|
|
1561
2138
|
}))
|
|
1562
2139
|
};
|
|
1563
|
-
|
|
1564
|
-
|
|
2140
|
+
fs2.writeFileSync(statusFile, JSON.stringify(status, null, 2), "utf-8");
|
|
2141
|
+
logger4.debug({ statusFile }, "Status file updated");
|
|
1565
2142
|
} catch (error) {
|
|
1566
|
-
|
|
2143
|
+
logger4.error({ error: error.message }, "Failed to write status file");
|
|
1567
2144
|
}
|
|
1568
2145
|
}
|
|
1569
2146
|
/**
|
|
@@ -1572,12 +2149,12 @@ var Bridge = class {
|
|
|
1572
2149
|
removeStatusFile() {
|
|
1573
2150
|
try {
|
|
1574
2151
|
const statusFile = this.getStatusFilePath();
|
|
1575
|
-
if (
|
|
1576
|
-
|
|
1577
|
-
|
|
2152
|
+
if (fs2.existsSync(statusFile)) {
|
|
2153
|
+
fs2.unlinkSync(statusFile);
|
|
2154
|
+
logger4.debug({ statusFile }, "Status file removed");
|
|
1578
2155
|
}
|
|
1579
2156
|
} catch (error) {
|
|
1580
|
-
|
|
2157
|
+
logger4.error({ error: error.message }, "Failed to remove status file");
|
|
1581
2158
|
}
|
|
1582
2159
|
}
|
|
1583
2160
|
// ============================================================================
|
|
@@ -1610,7 +2187,7 @@ var Bridge = class {
|
|
|
1610
2187
|
};
|
|
1611
2188
|
|
|
1612
2189
|
// src/utils/config.ts
|
|
1613
|
-
import * as
|
|
2190
|
+
import * as fs3 from "fs";
|
|
1614
2191
|
import * as path2 from "path";
|
|
1615
2192
|
import * as os2 from "os";
|
|
1616
2193
|
import { parse as parseYaml } from "yaml";
|
|
@@ -1656,10 +2233,10 @@ function mergeConfig(partial) {
|
|
|
1656
2233
|
}
|
|
1657
2234
|
function readConfigFile(filePath) {
|
|
1658
2235
|
try {
|
|
1659
|
-
if (!
|
|
2236
|
+
if (!fs3.existsSync(filePath)) {
|
|
1660
2237
|
return null;
|
|
1661
2238
|
}
|
|
1662
|
-
const content =
|
|
2239
|
+
const content = fs3.readFileSync(filePath, "utf-8");
|
|
1663
2240
|
const parsed = parseYaml(content);
|
|
1664
2241
|
return parsed;
|
|
1665
2242
|
} catch {
|
|
@@ -1715,7 +2292,7 @@ function loadConfigSync(configPath) {
|
|
|
1715
2292
|
|
|
1716
2293
|
// src/mcp/tools.ts
|
|
1717
2294
|
import { z as z2 } from "zod";
|
|
1718
|
-
var
|
|
2295
|
+
var logger5 = createLogger("mcp:tools");
|
|
1719
2296
|
var ReadFileInputSchema = z2.object({
|
|
1720
2297
|
path: z2.string().describe("Path to the file to read")
|
|
1721
2298
|
});
|
|
@@ -1798,7 +2375,7 @@ function createToolHandlers(bridge) {
|
|
|
1798
2375
|
}
|
|
1799
2376
|
handlers["bridge_read_file"] = async (args) => {
|
|
1800
2377
|
const input = ReadFileInputSchema.parse(args);
|
|
1801
|
-
|
|
2378
|
+
logger5.debug({ path: input.path }, "Reading file");
|
|
1802
2379
|
try {
|
|
1803
2380
|
const result = await delegateFileTask(
|
|
1804
2381
|
"read_file",
|
|
@@ -1810,13 +2387,13 @@ function createToolHandlers(bridge) {
|
|
|
1810
2387
|
}
|
|
1811
2388
|
return successResponse(result.data.content);
|
|
1812
2389
|
} catch (err) {
|
|
1813
|
-
|
|
2390
|
+
logger5.error({ error: err.message, path: input.path }, "Failed to read file");
|
|
1814
2391
|
return errorResponse(err.message);
|
|
1815
2392
|
}
|
|
1816
2393
|
};
|
|
1817
2394
|
handlers["bridge_write_file"] = async (args) => {
|
|
1818
2395
|
const input = WriteFileInputSchema.parse(args);
|
|
1819
|
-
|
|
2396
|
+
logger5.debug({ path: input.path, contentLength: input.content.length }, "Writing file");
|
|
1820
2397
|
try {
|
|
1821
2398
|
const result = await delegateFileTask(
|
|
1822
2399
|
"write_file",
|
|
@@ -1828,13 +2405,13 @@ function createToolHandlers(bridge) {
|
|
|
1828
2405
|
}
|
|
1829
2406
|
return successResponse(`File written successfully: ${input.path} (${result.data.bytesWritten} bytes)`);
|
|
1830
2407
|
} catch (err) {
|
|
1831
|
-
|
|
2408
|
+
logger5.error({ error: err.message, path: input.path }, "Failed to write file");
|
|
1832
2409
|
return errorResponse(err.message);
|
|
1833
2410
|
}
|
|
1834
2411
|
};
|
|
1835
2412
|
handlers["bridge_delete_file"] = async (args) => {
|
|
1836
2413
|
const input = DeleteFileInputSchema.parse(args);
|
|
1837
|
-
|
|
2414
|
+
logger5.debug({ path: input.path }, "Deleting file");
|
|
1838
2415
|
try {
|
|
1839
2416
|
const result = await delegateFileTask(
|
|
1840
2417
|
"delete_file",
|
|
@@ -1846,13 +2423,13 @@ function createToolHandlers(bridge) {
|
|
|
1846
2423
|
}
|
|
1847
2424
|
return successResponse(`File deleted successfully: ${input.path}`);
|
|
1848
2425
|
} catch (err) {
|
|
1849
|
-
|
|
2426
|
+
logger5.error({ error: err.message, path: input.path }, "Failed to delete file");
|
|
1850
2427
|
return errorResponse(err.message);
|
|
1851
2428
|
}
|
|
1852
2429
|
};
|
|
1853
2430
|
handlers["bridge_list_directory"] = async (args) => {
|
|
1854
2431
|
const input = ListDirectoryInputSchema.parse(args);
|
|
1855
|
-
|
|
2432
|
+
logger5.debug({ path: input.path }, "Listing directory");
|
|
1856
2433
|
try {
|
|
1857
2434
|
const result = await delegateFileTask(
|
|
1858
2435
|
"list_directory",
|
|
@@ -1870,13 +2447,13 @@ function createToolHandlers(bridge) {
|
|
|
1870
2447
|
return successResponse(`Contents of ${input.path}:
|
|
1871
2448
|
${listing}`);
|
|
1872
2449
|
} catch (err) {
|
|
1873
|
-
|
|
2450
|
+
logger5.error({ error: err.message, path: input.path }, "Failed to list directory");
|
|
1874
2451
|
return errorResponse(err.message);
|
|
1875
2452
|
}
|
|
1876
2453
|
};
|
|
1877
2454
|
handlers["bridge_delegate_task"] = async (args) => {
|
|
1878
2455
|
const input = DelegateTaskInputSchema.parse(args);
|
|
1879
|
-
|
|
2456
|
+
logger5.debug({ description: input.description, scope: input.scope }, "Delegating task");
|
|
1880
2457
|
try {
|
|
1881
2458
|
const taskId = `mcp-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
1882
2459
|
const result = await bridge.delegateTask({
|
|
@@ -1890,13 +2467,13 @@ ${listing}`);
|
|
|
1890
2467
|
}
|
|
1891
2468
|
return successResponse(JSON.stringify(result.data, null, 2));
|
|
1892
2469
|
} catch (err) {
|
|
1893
|
-
|
|
2470
|
+
logger5.error({ error: err.message }, "Failed to delegate task");
|
|
1894
2471
|
return errorResponse(err.message);
|
|
1895
2472
|
}
|
|
1896
2473
|
};
|
|
1897
2474
|
handlers["bridge_request_context"] = async (args) => {
|
|
1898
2475
|
const input = RequestContextInputSchema.parse(args);
|
|
1899
|
-
|
|
2476
|
+
logger5.debug({ query: input.query }, "Requesting context");
|
|
1900
2477
|
try {
|
|
1901
2478
|
const files = await bridge.requestContext(input.query);
|
|
1902
2479
|
if (files.length === 0) {
|
|
@@ -1912,12 +2489,12 @@ ${content}`;
|
|
|
1912
2489
|
|
|
1913
2490
|
${fileResults}`);
|
|
1914
2491
|
} catch (err) {
|
|
1915
|
-
|
|
2492
|
+
logger5.error({ error: err.message }, "Failed to request context");
|
|
1916
2493
|
return errorResponse(err.message);
|
|
1917
2494
|
}
|
|
1918
2495
|
};
|
|
1919
2496
|
handlers["bridge_status"] = async () => {
|
|
1920
|
-
|
|
2497
|
+
logger5.debug("Getting bridge status");
|
|
1921
2498
|
const peers = bridge.getPeers();
|
|
1922
2499
|
const peerCount = bridge.getPeerCount();
|
|
1923
2500
|
const isStarted = bridge.isStarted();
|
|
@@ -1992,7 +2569,7 @@ import {
|
|
|
1992
2569
|
CallToolRequestSchema,
|
|
1993
2570
|
ListToolsRequestSchema
|
|
1994
2571
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
1995
|
-
var
|
|
2572
|
+
var logger6 = createLogger("mcp:server");
|
|
1996
2573
|
var BridgeMcpServer = class {
|
|
1997
2574
|
server;
|
|
1998
2575
|
bridge;
|
|
@@ -2015,7 +2592,9 @@ var BridgeMcpServer = class {
|
|
|
2015
2592
|
mode: "client",
|
|
2016
2593
|
instanceName: config.instanceName ?? `mcp-server-${process.pid}`,
|
|
2017
2594
|
connect: {
|
|
2018
|
-
url: config.bridgeUrl
|
|
2595
|
+
url: config.bridgeUrl,
|
|
2596
|
+
tls: config.tls,
|
|
2597
|
+
auth: config.auth
|
|
2019
2598
|
},
|
|
2020
2599
|
taskTimeout: config.taskTimeout ?? 6e4
|
|
2021
2600
|
};
|
|
@@ -2027,7 +2606,7 @@ var BridgeMcpServer = class {
|
|
|
2027
2606
|
*/
|
|
2028
2607
|
registerHandlers() {
|
|
2029
2608
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2030
|
-
|
|
2609
|
+
logger6.debug("Listing tools");
|
|
2031
2610
|
return {
|
|
2032
2611
|
tools: TOOL_DEFINITIONS.map((tool) => ({
|
|
2033
2612
|
name: tool.name,
|
|
@@ -2038,13 +2617,13 @@ var BridgeMcpServer = class {
|
|
|
2038
2617
|
});
|
|
2039
2618
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2040
2619
|
const { name, arguments: args } = request.params;
|
|
2041
|
-
|
|
2620
|
+
logger6.debug({ tool: name }, "Tool call received");
|
|
2042
2621
|
if (!this.toolHandlers) {
|
|
2043
2622
|
this.toolHandlers = createToolHandlers(this.bridge);
|
|
2044
2623
|
}
|
|
2045
2624
|
const handler = this.toolHandlers[name];
|
|
2046
2625
|
if (!handler) {
|
|
2047
|
-
|
|
2626
|
+
logger6.warn({ tool: name }, "Unknown tool requested");
|
|
2048
2627
|
return {
|
|
2049
2628
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
2050
2629
|
isError: true
|
|
@@ -2052,13 +2631,13 @@ var BridgeMcpServer = class {
|
|
|
2052
2631
|
}
|
|
2053
2632
|
try {
|
|
2054
2633
|
const result = await handler(args ?? {});
|
|
2055
|
-
|
|
2634
|
+
logger6.debug({ tool: name, isError: result.isError }, "Tool call completed");
|
|
2056
2635
|
return {
|
|
2057
2636
|
content: result.content,
|
|
2058
2637
|
isError: result.isError
|
|
2059
2638
|
};
|
|
2060
2639
|
} catch (err) {
|
|
2061
|
-
|
|
2640
|
+
logger6.error({ tool: name, error: err.message }, "Tool call failed");
|
|
2062
2641
|
return {
|
|
2063
2642
|
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
2064
2643
|
isError: true
|
|
@@ -2080,7 +2659,7 @@ var BridgeMcpServer = class {
|
|
|
2080
2659
|
const transport = new StdioServerTransport();
|
|
2081
2660
|
await this.server.connect(transport);
|
|
2082
2661
|
console.error("[MCP] MCP server started and listening on stdio");
|
|
2083
|
-
|
|
2662
|
+
logger6.info("MCP server started");
|
|
2084
2663
|
} catch (err) {
|
|
2085
2664
|
console.error(`[MCP] Failed to start: ${err.message}`);
|
|
2086
2665
|
throw err;
|
|
@@ -2095,7 +2674,7 @@ var BridgeMcpServer = class {
|
|
|
2095
2674
|
await this.bridge.stop();
|
|
2096
2675
|
await this.server.close();
|
|
2097
2676
|
console.error("[MCP] MCP server stopped");
|
|
2098
|
-
|
|
2677
|
+
logger6.info("MCP server stopped");
|
|
2099
2678
|
} catch (err) {
|
|
2100
2679
|
console.error(`[MCP] Error during shutdown: ${err.message}`);
|
|
2101
2680
|
}
|
|
@@ -2131,7 +2710,19 @@ export {
|
|
|
2131
2710
|
deserializeMessage,
|
|
2132
2711
|
safeDeserializeMessage,
|
|
2133
2712
|
ConnectionState,
|
|
2713
|
+
loadCertificates,
|
|
2714
|
+
loadCertificatesSync,
|
|
2715
|
+
validateTLSConfig,
|
|
2716
|
+
isTLSEnabled,
|
|
2717
|
+
createSecureContextOptions,
|
|
2134
2718
|
WebSocketTransport,
|
|
2719
|
+
validateIp,
|
|
2720
|
+
validateToken,
|
|
2721
|
+
validatePassword,
|
|
2722
|
+
extractCredentials,
|
|
2723
|
+
Authenticator,
|
|
2724
|
+
createAuthConfigFromOptions,
|
|
2725
|
+
validateAuthConfig,
|
|
2135
2726
|
createContextSyncMessage,
|
|
2136
2727
|
createTaskDelegateMessage,
|
|
2137
2728
|
createTaskResponseMessage,
|
|
@@ -2147,4 +2738,4 @@ export {
|
|
|
2147
2738
|
BridgeMcpServer,
|
|
2148
2739
|
startMcpServer
|
|
2149
2740
|
};
|
|
2150
|
-
//# sourceMappingURL=chunk-
|
|
2741
|
+
//# sourceMappingURL=chunk-2O352EPO.js.map
|