@yoooclaw/phone-notifications 1.11.12 → 1.11.14
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/index.cjs +516 -137
- package/dist/index.cjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -5574,7 +5574,7 @@ function readBuildInjectedVersion() {
|
|
|
5574
5574
|
if (false) {
|
|
5575
5575
|
return void 0;
|
|
5576
5576
|
}
|
|
5577
|
-
const version = "1.11.
|
|
5577
|
+
const version = "1.11.14".trim();
|
|
5578
5578
|
return version || void 0;
|
|
5579
5579
|
}
|
|
5580
5580
|
function readPluginVersionFromPackageJson() {
|
|
@@ -12303,19 +12303,72 @@ var import_websocket = __toESM(require_websocket(), 1);
|
|
|
12303
12303
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
12304
12304
|
var wrapper_default = import_websocket.default;
|
|
12305
12305
|
|
|
12306
|
-
// src/tunnel/
|
|
12307
|
-
function previewText(text, max =
|
|
12306
|
+
// src/tunnel/utils.ts
|
|
12307
|
+
function previewText(text, max = 200) {
|
|
12308
|
+
if (!text) return "";
|
|
12308
12309
|
return text.length <= max ? text : `${text.substring(0, max)}\u2026`;
|
|
12309
12310
|
}
|
|
12310
|
-
var HANDSHAKE_TIMEOUT_MS = 15e3;
|
|
12311
|
-
var CONNECT_WATCHDOG_MS = 2e4;
|
|
12312
12311
|
function maskSecret(value) {
|
|
12313
12312
|
if (!value) return "empty";
|
|
12314
12313
|
if (value.length <= 8) {
|
|
12315
|
-
return `${value.slice(0, 2)}
|
|
12314
|
+
return `${value.slice(0, 2)}...${value.slice(-2)}`;
|
|
12315
|
+
}
|
|
12316
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
12317
|
+
}
|
|
12318
|
+
function redactUrlSecrets(rawUrl) {
|
|
12319
|
+
try {
|
|
12320
|
+
const url = new URL(rawUrl);
|
|
12321
|
+
for (const key of ["apiKey", "token", "access_token"]) {
|
|
12322
|
+
const value = url.searchParams.get(key);
|
|
12323
|
+
if (value) {
|
|
12324
|
+
url.searchParams.set(key, maskSecret(value));
|
|
12325
|
+
}
|
|
12326
|
+
}
|
|
12327
|
+
return url.toString();
|
|
12328
|
+
} catch {
|
|
12329
|
+
return rawUrl.replace(
|
|
12330
|
+
/([?&](?:apiKey|token|access_token)=)([^&]+)/gi,
|
|
12331
|
+
(_match, prefix, value) => {
|
|
12332
|
+
let decoded = value;
|
|
12333
|
+
try {
|
|
12334
|
+
decoded = decodeURIComponent(value);
|
|
12335
|
+
} catch {
|
|
12336
|
+
}
|
|
12337
|
+
return `${prefix}${maskSecret(decoded)}`;
|
|
12338
|
+
}
|
|
12339
|
+
);
|
|
12316
12340
|
}
|
|
12317
|
-
return `${value.slice(0, 4)}\u2026${value.slice(-4)}`;
|
|
12318
12341
|
}
|
|
12342
|
+
function findHeaderValue(headers, key) {
|
|
12343
|
+
if (!headers) return void 0;
|
|
12344
|
+
const lowerKey = key.toLowerCase();
|
|
12345
|
+
for (const [headerKey, headerValue] of Object.entries(headers)) {
|
|
12346
|
+
if (headerKey.toLowerCase() === lowerKey) {
|
|
12347
|
+
return headerValue;
|
|
12348
|
+
}
|
|
12349
|
+
}
|
|
12350
|
+
return void 0;
|
|
12351
|
+
}
|
|
12352
|
+
function summarizeRequestHeaders(headers) {
|
|
12353
|
+
const contentType = findHeaderValue(headers, "content-type");
|
|
12354
|
+
const requestId = findHeaderValue(headers, "x-request-id");
|
|
12355
|
+
const parts = [];
|
|
12356
|
+
if (contentType) {
|
|
12357
|
+
parts.push(`contentType=${contentType}`);
|
|
12358
|
+
}
|
|
12359
|
+
if (requestId) {
|
|
12360
|
+
parts.push(`xRequestId=${previewText(requestId, 120)}`);
|
|
12361
|
+
}
|
|
12362
|
+
return parts.length ? `, ${parts.join(", ")}` : "";
|
|
12363
|
+
}
|
|
12364
|
+
|
|
12365
|
+
// src/tunnel/relay-client.ts
|
|
12366
|
+
function previewText2(text, max = 500) {
|
|
12367
|
+
return text.length <= max ? text : `${text.substring(0, max)}\u2026`;
|
|
12368
|
+
}
|
|
12369
|
+
var HANDSHAKE_TIMEOUT_MS = 15e3;
|
|
12370
|
+
var CONNECT_WATCHDOG_MS = 2e4;
|
|
12371
|
+
var SEND_SKIPPED_LOG_INTERVAL_MS = 3e4;
|
|
12319
12372
|
var RelayClient = class {
|
|
12320
12373
|
constructor(opts) {
|
|
12321
12374
|
this.opts = opts;
|
|
@@ -12326,9 +12379,12 @@ var RelayClient = class {
|
|
|
12326
12379
|
reconnectTimer = null;
|
|
12327
12380
|
handlers = [];
|
|
12328
12381
|
connectedHandlers = [];
|
|
12382
|
+
disconnectedHandlers = [];
|
|
12329
12383
|
aborted = false;
|
|
12330
12384
|
lastInboundAt = 0;
|
|
12331
12385
|
stopPromise = null;
|
|
12386
|
+
skippedSendLogLastAt = null;
|
|
12387
|
+
skippedSendLogSuppressed = 0;
|
|
12332
12388
|
/** 写连接状态到磁盘 */
|
|
12333
12389
|
writeStatus(state, lastDisconnectReason) {
|
|
12334
12390
|
if (!this.opts.statusFilePath) return;
|
|
@@ -12352,6 +12408,10 @@ var RelayClient = class {
|
|
|
12352
12408
|
onConnected(handler) {
|
|
12353
12409
|
this.connectedHandlers.push(handler);
|
|
12354
12410
|
}
|
|
12411
|
+
/** 注册 Relay 连接断开后的回调 */
|
|
12412
|
+
onDisconnected(handler) {
|
|
12413
|
+
this.disconnectedHandlers.push(handler);
|
|
12414
|
+
}
|
|
12355
12415
|
/** 当前是否已连上 Relay */
|
|
12356
12416
|
isConnected() {
|
|
12357
12417
|
return this.ws?.readyState === wrapper_default.OPEN;
|
|
@@ -12425,29 +12485,43 @@ var RelayClient = class {
|
|
|
12425
12485
|
}
|
|
12426
12486
|
/** 发送出站帧 */
|
|
12427
12487
|
send(frame) {
|
|
12428
|
-
|
|
12488
|
+
const ws = this.ws;
|
|
12489
|
+
if (ws?.readyState === wrapper_default.OPEN) {
|
|
12429
12490
|
const payload = JSON.stringify(frame);
|
|
12430
12491
|
this.opts.logger.info(
|
|
12431
12492
|
`Relay tunnel: \u25B6 send frame type=${frame.type}, id=${"id" in frame ? frame.id : "N/A"} (${payload.length} chars)`
|
|
12432
12493
|
);
|
|
12433
|
-
|
|
12494
|
+
try {
|
|
12495
|
+
ws.send(payload);
|
|
12496
|
+
} catch (err2) {
|
|
12497
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
12498
|
+
this.opts.logger.warn(
|
|
12499
|
+
`Relay tunnel: send failed, forcing reconnect: ${message}`
|
|
12500
|
+
);
|
|
12501
|
+
this.forceReconnectFromSocket(ws, `send-failed: ${message}`);
|
|
12502
|
+
}
|
|
12434
12503
|
} else {
|
|
12435
|
-
this.
|
|
12436
|
-
`Relay tunnel: \u25B6 send skipped, ws not open (readyState=${this.ws?.readyState ?? "null"}), frame type=${frame.type}`
|
|
12437
|
-
);
|
|
12504
|
+
this.logSendSkipped("send", `frame type=${frame.type}`);
|
|
12438
12505
|
}
|
|
12439
12506
|
}
|
|
12440
12507
|
/** 原样透传文本到 Relay(用于 Gateway WS 响应直接回传) */
|
|
12441
12508
|
sendRaw(text) {
|
|
12442
|
-
|
|
12509
|
+
const ws = this.ws;
|
|
12510
|
+
if (ws?.readyState === wrapper_default.OPEN) {
|
|
12443
12511
|
this.opts.logger.info(
|
|
12444
12512
|
`Relay tunnel: \u25B6 sendRaw (${text.length} chars): ${text.length <= 500 ? text : text.substring(0, 500) + "\u2026"}`
|
|
12445
12513
|
);
|
|
12446
|
-
|
|
12514
|
+
try {
|
|
12515
|
+
ws.send(text);
|
|
12516
|
+
} catch (err2) {
|
|
12517
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
12518
|
+
this.opts.logger.warn(
|
|
12519
|
+
`Relay tunnel: sendRaw failed, forcing reconnect: ${message}`
|
|
12520
|
+
);
|
|
12521
|
+
this.forceReconnectFromSocket(ws, `sendRaw-failed: ${message}`);
|
|
12522
|
+
}
|
|
12447
12523
|
} else {
|
|
12448
|
-
this.
|
|
12449
|
-
`Relay tunnel: \u25B6 sendRaw skipped, ws not open (readyState=${this.ws?.readyState ?? "null"})`
|
|
12450
|
-
);
|
|
12524
|
+
this.logSendSkipped("sendRaw");
|
|
12451
12525
|
}
|
|
12452
12526
|
}
|
|
12453
12527
|
/** 启动连接,带自动重连,直到 abortSignal 触发 */
|
|
@@ -12473,7 +12547,7 @@ var RelayClient = class {
|
|
|
12473
12547
|
this.cleanup(true);
|
|
12474
12548
|
const rawApiKey = this.opts.apiKey.startsWith("Bearer ") ? this.opts.apiKey.slice("Bearer ".length) : this.opts.apiKey;
|
|
12475
12549
|
this.opts.logger.info(
|
|
12476
|
-
`Relay tunnel: connecting to ${this.opts.tunnelUrl} (attempt=${this.reconnectAttempt}, heartbeat=${this.opts.heartbeatSec}s, apiKey=${maskSecret(rawApiKey)})`
|
|
12550
|
+
`Relay tunnel: connecting to ${redactUrlSecrets(this.opts.tunnelUrl)} (attempt=${this.reconnectAttempt}, heartbeat=${this.opts.heartbeatSec}s, apiKey=${maskSecret(rawApiKey)})`
|
|
12477
12551
|
);
|
|
12478
12552
|
this.writeStatus("connecting");
|
|
12479
12553
|
return new Promise((resolve) => {
|
|
@@ -12496,33 +12570,20 @@ var RelayClient = class {
|
|
|
12496
12570
|
handshakeTimeout: HANDSHAKE_TIMEOUT_MS
|
|
12497
12571
|
});
|
|
12498
12572
|
this.ws = ws;
|
|
12499
|
-
let connectWatchdog = setTimeout(
|
|
12500
|
-
|
|
12501
|
-
|
|
12502
|
-
|
|
12503
|
-
|
|
12504
|
-
|
|
12505
|
-
|
|
12506
|
-
|
|
12507
|
-
|
|
12508
|
-
|
|
12509
|
-
|
|
12510
|
-
}
|
|
12511
|
-
|
|
12512
|
-
|
|
12513
|
-
if (this.ws === ws) {
|
|
12514
|
-
this.opts.logger.warn(
|
|
12515
|
-
"Relay tunnel: terminate did not emit close, forcing reconnect"
|
|
12516
|
-
);
|
|
12517
|
-
this.stopHeartbeat();
|
|
12518
|
-
this.ws = null;
|
|
12519
|
-
this.writeStatus("disconnected", "watchdog-force");
|
|
12520
|
-
this.scheduleReconnect();
|
|
12521
|
-
settle();
|
|
12522
|
-
}
|
|
12523
|
-
}, 1e3);
|
|
12524
|
-
}
|
|
12525
|
-
}, CONNECT_WATCHDOG_MS);
|
|
12573
|
+
let connectWatchdog = setTimeout(
|
|
12574
|
+
() => {
|
|
12575
|
+
connectWatchdog = null;
|
|
12576
|
+
if (this.ws !== ws || ws.readyState === wrapper_default.OPEN) {
|
|
12577
|
+
return;
|
|
12578
|
+
}
|
|
12579
|
+
this.opts.logger.warn(
|
|
12580
|
+
`Relay tunnel: connect watchdog fired (readyState=${ws.readyState}, attempt=${this.reconnectAttempt}), forcing reconnect`
|
|
12581
|
+
);
|
|
12582
|
+
this.forceReconnectFromSocket(ws, "connect-watchdog-timeout");
|
|
12583
|
+
settle();
|
|
12584
|
+
},
|
|
12585
|
+
CONNECT_WATCHDOG_MS
|
|
12586
|
+
);
|
|
12526
12587
|
const clearConnectWatchdog = () => {
|
|
12527
12588
|
if (connectWatchdog) {
|
|
12528
12589
|
clearTimeout(connectWatchdog);
|
|
@@ -12567,7 +12628,7 @@ var RelayClient = class {
|
|
|
12567
12628
|
const reasonStr = reason.toString();
|
|
12568
12629
|
const lastInboundAgoMs = this.lastInboundAt ? Date.now() - this.lastInboundAt : null;
|
|
12569
12630
|
const isCurrentSocket = this.ws === ws;
|
|
12570
|
-
const logMessage = `Relay tunnel: disconnected (code=${code}, reason=${
|
|
12631
|
+
const logMessage = `Relay tunnel: disconnected (code=${code}, reason=${previewText2(reasonStr, 200)}, lastInboundAgoMs=${lastInboundAgoMs ?? "N/A"}, reconnectAttempt=${this.reconnectAttempt})`;
|
|
12571
12632
|
if (this.aborted || !isCurrentSocket) {
|
|
12572
12633
|
this.opts.logger.info(logMessage);
|
|
12573
12634
|
} else {
|
|
@@ -12576,17 +12637,20 @@ var RelayClient = class {
|
|
|
12576
12637
|
if (isCurrentSocket) {
|
|
12577
12638
|
this.stopHeartbeat();
|
|
12578
12639
|
this.ws = null;
|
|
12579
|
-
|
|
12640
|
+
const disconnectReason = `code=${code}, reason=${reasonStr}`;
|
|
12641
|
+
this.writeStatus("disconnected", disconnectReason);
|
|
12642
|
+
this.emitDisconnected(disconnectReason);
|
|
12580
12643
|
this.scheduleReconnect();
|
|
12581
12644
|
}
|
|
12582
12645
|
settle();
|
|
12583
12646
|
});
|
|
12584
12647
|
ws.on("error", (err2) => {
|
|
12648
|
+
clearConnectWatchdog();
|
|
12585
12649
|
this.opts.logger.error(
|
|
12586
|
-
`Relay tunnel: WebSocket error: ${err2.message} (readyState=${ws.readyState}, reconnectAttempt=${this.reconnectAttempt}, url=${wsUrl.toString()})`
|
|
12650
|
+
`Relay tunnel: WebSocket error: ${err2.message} (readyState=${ws.readyState}, reconnectAttempt=${this.reconnectAttempt}, url=${redactUrlSecrets(wsUrl.toString())})`
|
|
12587
12651
|
);
|
|
12588
12652
|
if (this.ws === ws) {
|
|
12589
|
-
this.
|
|
12653
|
+
this.forceReconnectFromSocket(ws, `error: ${err2.message}`);
|
|
12590
12654
|
}
|
|
12591
12655
|
settle();
|
|
12592
12656
|
});
|
|
@@ -12601,6 +12665,15 @@ var RelayClient = class {
|
|
|
12601
12665
|
});
|
|
12602
12666
|
}
|
|
12603
12667
|
}
|
|
12668
|
+
emitDisconnected(reason) {
|
|
12669
|
+
for (const handler of this.disconnectedHandlers) {
|
|
12670
|
+
Promise.resolve(handler(reason)).catch((err2) => {
|
|
12671
|
+
this.opts.logger.warn(
|
|
12672
|
+
`Relay tunnel: onDisconnected handler failed: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
12673
|
+
);
|
|
12674
|
+
});
|
|
12675
|
+
}
|
|
12676
|
+
}
|
|
12604
12677
|
handleMessage(data) {
|
|
12605
12678
|
const text = typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString() : String(data);
|
|
12606
12679
|
this.markInboundActivity();
|
|
@@ -12608,14 +12681,14 @@ var RelayClient = class {
|
|
|
12608
12681
|
return;
|
|
12609
12682
|
}
|
|
12610
12683
|
this.opts.logger.info(
|
|
12611
|
-
`Relay tunnel: \u2605 received message (${text.length} chars): ${
|
|
12684
|
+
`Relay tunnel: \u2605 received message (${text.length} chars): ${previewText2(text)}`
|
|
12612
12685
|
);
|
|
12613
12686
|
let frame;
|
|
12614
12687
|
try {
|
|
12615
12688
|
frame = JSON.parse(text);
|
|
12616
12689
|
} catch {
|
|
12617
12690
|
this.opts.logger.warn(
|
|
12618
|
-
`Relay tunnel: received invalid frame, ignoring (preview=${
|
|
12691
|
+
`Relay tunnel: received invalid frame, ignoring (preview=${previewText2(text, 200)})`
|
|
12619
12692
|
);
|
|
12620
12693
|
return;
|
|
12621
12694
|
}
|
|
@@ -12651,7 +12724,8 @@ var RelayClient = class {
|
|
|
12651
12724
|
return this.opts.heartbeatSec * 3 * 1e3;
|
|
12652
12725
|
}
|
|
12653
12726
|
sendHeartbeat() {
|
|
12654
|
-
|
|
12727
|
+
const ws = this.ws;
|
|
12728
|
+
if (ws?.readyState !== wrapper_default.OPEN) {
|
|
12655
12729
|
return;
|
|
12656
12730
|
}
|
|
12657
12731
|
const idleMs = Date.now() - this.lastInboundAt;
|
|
@@ -12660,20 +12734,28 @@ var RelayClient = class {
|
|
|
12660
12734
|
this.opts.logger.warn(
|
|
12661
12735
|
`Relay tunnel: heartbeat timeout, no inbound activity for ${idleMs}ms (threshold=${timeoutMs}ms)`
|
|
12662
12736
|
);
|
|
12663
|
-
|
|
12664
|
-
this.ws.terminate();
|
|
12665
|
-
} catch {
|
|
12666
|
-
}
|
|
12737
|
+
this.forceReconnectFromSocket(ws, `heartbeat-timeout idleMs=${idleMs}`);
|
|
12667
12738
|
return;
|
|
12668
12739
|
}
|
|
12669
12740
|
this.opts.logger.info('Relay tunnel: \u2192 heartbeat "ping"');
|
|
12670
12741
|
try {
|
|
12671
|
-
|
|
12672
|
-
} catch {
|
|
12742
|
+
ws.send("ping");
|
|
12743
|
+
} catch (err2) {
|
|
12744
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
12745
|
+
this.opts.logger.warn(
|
|
12746
|
+
`Relay tunnel: heartbeat text ping failed, forcing reconnect: ${message}`
|
|
12747
|
+
);
|
|
12748
|
+
this.forceReconnectFromSocket(ws, `heartbeat-send-failed: ${message}`);
|
|
12749
|
+
return;
|
|
12673
12750
|
}
|
|
12674
12751
|
try {
|
|
12675
|
-
|
|
12676
|
-
} catch {
|
|
12752
|
+
ws.ping();
|
|
12753
|
+
} catch (err2) {
|
|
12754
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
12755
|
+
this.opts.logger.warn(
|
|
12756
|
+
`Relay tunnel: heartbeat control ping failed, forcing reconnect: ${message}`
|
|
12757
|
+
);
|
|
12758
|
+
this.forceReconnectFromSocket(ws, `heartbeat-ping-failed: ${message}`);
|
|
12677
12759
|
}
|
|
12678
12760
|
}
|
|
12679
12761
|
markInboundActivity() {
|
|
@@ -12700,6 +12782,35 @@ var RelayClient = class {
|
|
|
12700
12782
|
this.ws = null;
|
|
12701
12783
|
}
|
|
12702
12784
|
}
|
|
12785
|
+
forceReconnectFromSocket(ws, reason) {
|
|
12786
|
+
if (this.aborted || this.ws !== ws) return;
|
|
12787
|
+
this.stopHeartbeat();
|
|
12788
|
+
this.ws = null;
|
|
12789
|
+
this.writeStatus("disconnected", reason);
|
|
12790
|
+
this.emitDisconnected(reason);
|
|
12791
|
+
this.scheduleReconnect();
|
|
12792
|
+
try {
|
|
12793
|
+
if (ws.readyState !== wrapper_default.CLOSED) {
|
|
12794
|
+
ws.terminate();
|
|
12795
|
+
}
|
|
12796
|
+
} catch {
|
|
12797
|
+
}
|
|
12798
|
+
}
|
|
12799
|
+
logSendSkipped(kind, detail) {
|
|
12800
|
+
const now = Date.now();
|
|
12801
|
+
const shouldLog = this.skippedSendLogLastAt === null || now - this.skippedSendLogLastAt >= SEND_SKIPPED_LOG_INTERVAL_MS;
|
|
12802
|
+
if (!shouldLog) {
|
|
12803
|
+
this.skippedSendLogSuppressed++;
|
|
12804
|
+
return;
|
|
12805
|
+
}
|
|
12806
|
+
const suppressedSuffix = this.skippedSendLogSuppressed > 0 ? `, suppressed=${this.skippedSendLogSuppressed}` : "";
|
|
12807
|
+
const detailSuffix = detail ? `, ${detail}` : "";
|
|
12808
|
+
this.opts.logger.warn(
|
|
12809
|
+
`Relay tunnel: \u25B6 ${kind} skipped, ws not open (readyState=${this.ws?.readyState ?? "null"}${detailSuffix}${suppressedSuffix})`
|
|
12810
|
+
);
|
|
12811
|
+
this.skippedSendLogLastAt = now;
|
|
12812
|
+
this.skippedSendLogSuppressed = 0;
|
|
12813
|
+
}
|
|
12703
12814
|
scheduleReconnect() {
|
|
12704
12815
|
if (this.aborted) return;
|
|
12705
12816
|
if (this.reconnectTimer) {
|
|
@@ -12727,36 +12838,6 @@ var RelayClient = class {
|
|
|
12727
12838
|
|
|
12728
12839
|
// src/tunnel/proxy.ts
|
|
12729
12840
|
var import_node_crypto5 = require("crypto");
|
|
12730
|
-
|
|
12731
|
-
// src/tunnel/utils.ts
|
|
12732
|
-
function previewText2(text, max = 200) {
|
|
12733
|
-
if (!text) return "";
|
|
12734
|
-
return text.length <= max ? text : `${text.substring(0, max)}\u2026`;
|
|
12735
|
-
}
|
|
12736
|
-
function findHeaderValue(headers, key) {
|
|
12737
|
-
if (!headers) return void 0;
|
|
12738
|
-
const lowerKey = key.toLowerCase();
|
|
12739
|
-
for (const [headerKey, headerValue] of Object.entries(headers)) {
|
|
12740
|
-
if (headerKey.toLowerCase() === lowerKey) {
|
|
12741
|
-
return headerValue;
|
|
12742
|
-
}
|
|
12743
|
-
}
|
|
12744
|
-
return void 0;
|
|
12745
|
-
}
|
|
12746
|
-
function summarizeRequestHeaders(headers) {
|
|
12747
|
-
const contentType = findHeaderValue(headers, "content-type");
|
|
12748
|
-
const requestId = findHeaderValue(headers, "x-request-id");
|
|
12749
|
-
const parts = [];
|
|
12750
|
-
if (contentType) {
|
|
12751
|
-
parts.push(`contentType=${contentType}`);
|
|
12752
|
-
}
|
|
12753
|
-
if (requestId) {
|
|
12754
|
-
parts.push(`xRequestId=${previewText2(requestId, 120)}`);
|
|
12755
|
-
}
|
|
12756
|
-
return parts.length ? `, ${parts.join(", ")}` : "";
|
|
12757
|
-
}
|
|
12758
|
-
|
|
12759
|
-
// src/tunnel/proxy.ts
|
|
12760
12841
|
init_host();
|
|
12761
12842
|
|
|
12762
12843
|
// src/tunnel/device-identity.ts
|
|
@@ -12995,7 +13076,7 @@ async function handleHttpRequest(opts, frame) {
|
|
|
12995
13076
|
localHeaders[RELAY_INTERNAL_HTTP_HEADER] = "1";
|
|
12996
13077
|
const authAttempts = buildLocalGatewayAuthAttempts(opts, localHeaders);
|
|
12997
13078
|
opts.logger.info(
|
|
12998
|
-
`TunnelProxy: HTTP id=${frame.id} ${frame.method} ${frame.path} \u2192 ${url.toString()}${summarizeRequestHeaders(frame.headers)}, authAttempts=${authAttempts.map((a) => a.label).join(" -> ")}, body=${
|
|
13079
|
+
`TunnelProxy: HTTP id=${frame.id} ${frame.method} ${frame.path} \u2192 ${url.toString()}${summarizeRequestHeaders(frame.headers)}, authAttempts=${authAttempts.map((a) => a.label).join(" -> ")}, body=${previewText(frame.body)}`
|
|
12999
13080
|
);
|
|
13000
13081
|
try {
|
|
13001
13082
|
for (let attemptIndex = 0; attemptIndex < authAttempts.length; attemptIndex++) {
|
|
@@ -13006,13 +13087,14 @@ async function handleHttpRequest(opts, frame) {
|
|
|
13006
13087
|
const res = await fetch(url.toString(), {
|
|
13007
13088
|
method: frame.method,
|
|
13008
13089
|
headers: attempt.headers,
|
|
13090
|
+
signal: opts.abortSignal,
|
|
13009
13091
|
body: frame.method !== "GET" && frame.method !== "HEAD" ? frame.body : void 0
|
|
13010
13092
|
});
|
|
13011
13093
|
const hasFallback = attemptIndex < authAttempts.length - 1;
|
|
13012
13094
|
if (res.status === 401 && hasFallback) {
|
|
13013
13095
|
const body = await res.text();
|
|
13014
13096
|
opts.logger.warn(
|
|
13015
|
-
`TunnelProxy: HTTP id=${frame.id} local gateway auth via ${attempt.label} returned 401 after ${Date.now() - startedAtMs}ms, retrying next credential${body ? `, body=${
|
|
13097
|
+
`TunnelProxy: HTTP id=${frame.id} local gateway auth via ${attempt.label} returned 401 after ${Date.now() - startedAtMs}ms, retrying next credential${body ? `, body=${previewText(body)}` : ""}`
|
|
13016
13098
|
);
|
|
13017
13099
|
continue;
|
|
13018
13100
|
}
|
|
@@ -13053,7 +13135,7 @@ async function sendHttpResponse(opts, params) {
|
|
|
13053
13135
|
}
|
|
13054
13136
|
const body = await res.text();
|
|
13055
13137
|
opts.logger.info(
|
|
13056
|
-
`TunnelProxy: HTTP id=${frameId} response body=${
|
|
13138
|
+
`TunnelProxy: HTTP id=${frameId} response body=${previewText(body)}`
|
|
13057
13139
|
);
|
|
13058
13140
|
const headers = {};
|
|
13059
13141
|
res.headers.forEach((value, key) => {
|
|
@@ -13074,6 +13156,9 @@ async function streamResponse(opts, requestId, res, startedAtMs) {
|
|
|
13074
13156
|
opts.logger.info(`TunnelProxy: stream start id=${requestId}`);
|
|
13075
13157
|
try {
|
|
13076
13158
|
while (true) {
|
|
13159
|
+
if (opts.abortSignal?.aborted) {
|
|
13160
|
+
throw new DOMException("relay tunnel disconnected", "AbortError");
|
|
13161
|
+
}
|
|
13077
13162
|
const { done, value } = await reader.read();
|
|
13078
13163
|
if (done) break;
|
|
13079
13164
|
chunkCount++;
|
|
@@ -13111,6 +13196,7 @@ async function streamResponse(opts, requestId, res, startedAtMs) {
|
|
|
13111
13196
|
}
|
|
13112
13197
|
|
|
13113
13198
|
// src/tunnel/ws-proxy.ts
|
|
13199
|
+
var GATEWAY_WS_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
13114
13200
|
var WsProxy = class {
|
|
13115
13201
|
constructor(opts) {
|
|
13116
13202
|
this.opts = opts;
|
|
@@ -13121,14 +13207,15 @@ var WsProxy = class {
|
|
|
13121
13207
|
return this.connections.size;
|
|
13122
13208
|
}
|
|
13123
13209
|
cleanup() {
|
|
13124
|
-
|
|
13210
|
+
const connections = [...this.connections.entries()];
|
|
13211
|
+
this.connections.clear();
|
|
13212
|
+
for (const [id, ws] of connections) {
|
|
13125
13213
|
this.opts.logger.info(`WsProxy: closing WS id=${id}`);
|
|
13126
13214
|
try {
|
|
13127
13215
|
ws.close();
|
|
13128
13216
|
} catch {
|
|
13129
13217
|
}
|
|
13130
13218
|
}
|
|
13131
|
-
this.connections.clear();
|
|
13132
13219
|
}
|
|
13133
13220
|
handleWsOpen(frame) {
|
|
13134
13221
|
const wsUrl = this.opts.gatewayBaseUrl.replace(/^http/, "ws") + mapPath(frame.path);
|
|
@@ -13136,11 +13223,72 @@ var WsProxy = class {
|
|
|
13136
13223
|
`TunnelProxy: WS open id=${frame.id}, path=${frame.path} \u2192 ${wsUrl}`
|
|
13137
13224
|
);
|
|
13138
13225
|
try {
|
|
13226
|
+
const existing = this.connections.get(frame.id);
|
|
13227
|
+
if (existing) {
|
|
13228
|
+
this.opts.logger.warn(
|
|
13229
|
+
`TunnelProxy: WS id=${frame.id} already exists, closing previous gateway connection`
|
|
13230
|
+
);
|
|
13231
|
+
try {
|
|
13232
|
+
existing.close(1e3, "replaced by new relay ws_open");
|
|
13233
|
+
} catch {
|
|
13234
|
+
}
|
|
13235
|
+
}
|
|
13139
13236
|
const ws = new wrapper_default(wsUrl, {
|
|
13140
|
-
headers: frame.headers
|
|
13237
|
+
headers: frame.headers,
|
|
13238
|
+
handshakeTimeout: GATEWAY_WS_HANDSHAKE_TIMEOUT_MS
|
|
13141
13239
|
});
|
|
13240
|
+
this.connections.set(frame.id, ws);
|
|
13241
|
+
let notifiedClosed = false;
|
|
13242
|
+
let connectTimer = setTimeout(
|
|
13243
|
+
() => {
|
|
13244
|
+
connectTimer = null;
|
|
13245
|
+
if (this.connections.get(frame.id) !== ws || ws.readyState === wrapper_default.OPEN) {
|
|
13246
|
+
return;
|
|
13247
|
+
}
|
|
13248
|
+
this.opts.logger.warn(
|
|
13249
|
+
`TunnelProxy: WS id=${frame.id} connect timed out after ${GATEWAY_WS_HANDSHAKE_TIMEOUT_MS}ms`
|
|
13250
|
+
);
|
|
13251
|
+
notifyClosed(1011, "gateway websocket connect timeout");
|
|
13252
|
+
try {
|
|
13253
|
+
if (typeof ws.terminate === "function") {
|
|
13254
|
+
ws.terminate();
|
|
13255
|
+
} else {
|
|
13256
|
+
ws.close();
|
|
13257
|
+
}
|
|
13258
|
+
} catch {
|
|
13259
|
+
}
|
|
13260
|
+
},
|
|
13261
|
+
GATEWAY_WS_HANDSHAKE_TIMEOUT_MS
|
|
13262
|
+
);
|
|
13263
|
+
const clearConnectTimer = () => {
|
|
13264
|
+
if (connectTimer) {
|
|
13265
|
+
clearTimeout(connectTimer);
|
|
13266
|
+
connectTimer = null;
|
|
13267
|
+
}
|
|
13268
|
+
};
|
|
13269
|
+
const notifyClosed = (code, reason) => {
|
|
13270
|
+
if (notifiedClosed) return;
|
|
13271
|
+
notifiedClosed = true;
|
|
13272
|
+
clearConnectTimer();
|
|
13273
|
+
if (this.connections.get(frame.id) === ws) {
|
|
13274
|
+
this.connections.delete(frame.id);
|
|
13275
|
+
}
|
|
13276
|
+
this.opts.client.send({
|
|
13277
|
+
type: "ws_close",
|
|
13278
|
+
id: frame.id,
|
|
13279
|
+
code,
|
|
13280
|
+
reason
|
|
13281
|
+
});
|
|
13282
|
+
};
|
|
13142
13283
|
ws.on("open", () => {
|
|
13143
|
-
|
|
13284
|
+
clearConnectTimer();
|
|
13285
|
+
if (this.connections.get(frame.id) !== ws) {
|
|
13286
|
+
try {
|
|
13287
|
+
ws.close(1e3, "stale relay ws_open");
|
|
13288
|
+
} catch {
|
|
13289
|
+
}
|
|
13290
|
+
return;
|
|
13291
|
+
}
|
|
13144
13292
|
this.opts.logger.info(
|
|
13145
13293
|
`TunnelProxy: WS id=${frame.id} connected to gateway, active=${this.connections.size}`
|
|
13146
13294
|
);
|
|
@@ -13150,6 +13298,9 @@ var WsProxy = class {
|
|
|
13150
13298
|
});
|
|
13151
13299
|
});
|
|
13152
13300
|
ws.on("message", (data) => {
|
|
13301
|
+
if (this.connections.get(frame.id) !== ws) {
|
|
13302
|
+
return;
|
|
13303
|
+
}
|
|
13153
13304
|
const text = typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString() : String(data);
|
|
13154
13305
|
this.opts.logger.info(
|
|
13155
13306
|
`TunnelProxy: WS id=${frame.id} \u2190 gateway data (${text.length} chars): ${text.substring(0, 200)}`
|
|
@@ -13161,28 +13312,33 @@ var WsProxy = class {
|
|
|
13161
13312
|
});
|
|
13162
13313
|
});
|
|
13163
13314
|
ws.on("close", (code, reason) => {
|
|
13164
|
-
|
|
13315
|
+
clearConnectTimer();
|
|
13316
|
+
const reasonText = reason.toString();
|
|
13317
|
+
const isCurrent = this.connections.get(frame.id) === ws;
|
|
13318
|
+
if (isCurrent) {
|
|
13319
|
+
this.connections.delete(frame.id);
|
|
13320
|
+
}
|
|
13165
13321
|
this.opts.logger.info(
|
|
13166
|
-
`TunnelProxy: WS id=${frame.id} closed by gateway (code=${code}, reason=${
|
|
13322
|
+
`TunnelProxy: WS id=${frame.id} closed by gateway (code=${code}, reason=${reasonText}), active=${this.connections.size}`
|
|
13167
13323
|
);
|
|
13168
|
-
|
|
13169
|
-
|
|
13170
|
-
|
|
13171
|
-
code,
|
|
13172
|
-
reason: reason.toString()
|
|
13173
|
-
});
|
|
13324
|
+
if (isCurrent) {
|
|
13325
|
+
notifyClosed(code, reasonText);
|
|
13326
|
+
}
|
|
13174
13327
|
});
|
|
13175
13328
|
ws.on("error", (err2) => {
|
|
13329
|
+
clearConnectTimer();
|
|
13176
13330
|
this.opts.logger.warn(
|
|
13177
13331
|
`TunnelProxy: WS id=${frame.id} error: ${err2.message}, active=${this.connections.size}`
|
|
13178
13332
|
);
|
|
13179
|
-
this.connections.
|
|
13180
|
-
|
|
13181
|
-
|
|
13182
|
-
|
|
13183
|
-
|
|
13184
|
-
|
|
13185
|
-
|
|
13333
|
+
if (this.connections.get(frame.id) === ws) {
|
|
13334
|
+
notifyClosed(1011, err2.message);
|
|
13335
|
+
}
|
|
13336
|
+
try {
|
|
13337
|
+
if (typeof ws.terminate === "function") {
|
|
13338
|
+
ws.terminate();
|
|
13339
|
+
}
|
|
13340
|
+
} catch {
|
|
13341
|
+
}
|
|
13186
13342
|
});
|
|
13187
13343
|
} catch (err2) {
|
|
13188
13344
|
this.opts.logger.error(
|
|
@@ -13232,6 +13388,8 @@ var WsProxy = class {
|
|
|
13232
13388
|
var RELAY_TUNNEL_GATEWAY_CLIENT_INSTANCE_ID = "phone-notifications-relay-tunnel";
|
|
13233
13389
|
var MAX_AUTO_PAIRING_APPROVALS = 3;
|
|
13234
13390
|
var RECENT_ABORTED_CHAT_RUN_TTL_MS = 6e4;
|
|
13391
|
+
var GATEWAY_RPC_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
13392
|
+
var MAX_GATEWAY_WS_PENDING = 200;
|
|
13235
13393
|
var approveDevicePairingPromise = null;
|
|
13236
13394
|
var approveDevicePairingWarned = false;
|
|
13237
13395
|
function formatErrorMessage(err2) {
|
|
@@ -13283,6 +13441,8 @@ var TunnelProxy = class {
|
|
|
13283
13441
|
gatewayWsReady = false;
|
|
13284
13442
|
/** 收到本地自动配对成功后,在 close 回调里触发重连 */
|
|
13285
13443
|
gatewayWsReconnectRequested = false;
|
|
13444
|
+
/** 本地自动配对审批进行中,期间 Gateway 关闭连接时先保留 pending 请求。 */
|
|
13445
|
+
gatewayWsPairingApprovalPending = false;
|
|
13286
13446
|
/** 防止配对失败时无限重连 */
|
|
13287
13447
|
gatewayWsAutoPairingApprovals = 0;
|
|
13288
13448
|
/** 等待 Gateway WS 握手完成后发送的帧队列 */
|
|
@@ -13291,6 +13451,8 @@ var TunnelProxy = class {
|
|
|
13291
13451
|
gatewayReqMetaById = /* @__PURE__ */ new Map();
|
|
13292
13452
|
/** 最近由用户手动中断的 chat.run,用于过滤 runtime 误补发的 synthetic failure。 */
|
|
13293
13453
|
recentAbortedChatRuns = /* @__PURE__ */ new Map();
|
|
13454
|
+
/** Relay 断开或服务停止时要中断的 HTTP 代理请求。 */
|
|
13455
|
+
httpAbortControllers = /* @__PURE__ */ new Map();
|
|
13294
13456
|
/** 设备身份,用于 Gateway connect 握手 */
|
|
13295
13457
|
deviceIdentity;
|
|
13296
13458
|
stateDir;
|
|
@@ -13304,7 +13466,7 @@ var TunnelProxy = class {
|
|
|
13304
13466
|
);
|
|
13305
13467
|
switch (frame.type) {
|
|
13306
13468
|
case "request":
|
|
13307
|
-
await
|
|
13469
|
+
await this.handleRequestFrame(frame);
|
|
13308
13470
|
break;
|
|
13309
13471
|
case "req":
|
|
13310
13472
|
this.handleReqFrame(frame);
|
|
@@ -13332,35 +13494,73 @@ var TunnelProxy = class {
|
|
|
13332
13494
|
`TunnelProxy: cleanup, closing ${this.wsProxy.activeCount} active WS connections, gatewayWs=${!!this.gatewayWs}`
|
|
13333
13495
|
);
|
|
13334
13496
|
this.wsProxy.cleanup();
|
|
13335
|
-
|
|
13336
|
-
|
|
13337
|
-
|
|
13338
|
-
} catch {
|
|
13339
|
-
}
|
|
13340
|
-
this.gatewayWs = null;
|
|
13497
|
+
for (const [id, controller] of this.httpAbortControllers) {
|
|
13498
|
+
this.opts.logger.info(`TunnelProxy: aborting HTTP proxy id=${id}`);
|
|
13499
|
+
controller.abort();
|
|
13341
13500
|
}
|
|
13501
|
+
this.httpAbortControllers.clear();
|
|
13502
|
+
const gatewayWs = this.gatewayWs;
|
|
13503
|
+
this.gatewayWs = null;
|
|
13342
13504
|
this.gatewayWsReady = false;
|
|
13343
13505
|
this.gatewayWsConnecting = false;
|
|
13344
13506
|
this.gatewayWsReconnectRequested = false;
|
|
13507
|
+
this.gatewayWsPairingApprovalPending = false;
|
|
13345
13508
|
this.gatewayWsAutoPairingApprovals = 0;
|
|
13346
13509
|
this.gatewayWsPending = [];
|
|
13347
13510
|
this.gatewayReqMetaById.clear();
|
|
13348
13511
|
this.recentAbortedChatRuns.clear();
|
|
13512
|
+
if (gatewayWs) {
|
|
13513
|
+
try {
|
|
13514
|
+
gatewayWs.close();
|
|
13515
|
+
} catch {
|
|
13516
|
+
}
|
|
13517
|
+
}
|
|
13349
13518
|
}
|
|
13350
13519
|
// ─── Gateway RPC WebSocket ───
|
|
13351
|
-
|
|
13352
|
-
|
|
13520
|
+
async handleRequestFrame(frame) {
|
|
13521
|
+
const controller = new AbortController();
|
|
13522
|
+
this.httpAbortControllers.set(frame.id, controller);
|
|
13523
|
+
try {
|
|
13524
|
+
await handleHttpRequest(
|
|
13525
|
+
{
|
|
13526
|
+
...this.opts,
|
|
13527
|
+
abortSignal: controller.signal
|
|
13528
|
+
},
|
|
13529
|
+
frame
|
|
13530
|
+
);
|
|
13531
|
+
} finally {
|
|
13532
|
+
this.httpAbortControllers.delete(frame.id);
|
|
13533
|
+
}
|
|
13534
|
+
}
|
|
13535
|
+
pushGatewayPending(entry, reason) {
|
|
13536
|
+
if (this.gatewayWsPending.length >= MAX_GATEWAY_WS_PENDING) {
|
|
13537
|
+
this.opts.logger.error(
|
|
13538
|
+
`TunnelProxy: gateway WS pending queue overflow (${this.gatewayWsPending.length}/${MAX_GATEWAY_WS_PENDING}); failing ${entry.reqId ? `req id=${entry.reqId}` : "raw frame"}`
|
|
13539
|
+
);
|
|
13540
|
+
this.failGatewayPendingEntry(
|
|
13541
|
+
entry,
|
|
13542
|
+
"GATEWAY_RPC_QUEUE_FULL",
|
|
13543
|
+
"local gateway RPC queue is full"
|
|
13544
|
+
);
|
|
13545
|
+
return false;
|
|
13546
|
+
}
|
|
13547
|
+
this.gatewayWsPending.push(entry);
|
|
13353
13548
|
this.opts.logger.info(
|
|
13354
13549
|
`TunnelProxy: gateway WS pending queue size=${this.gatewayWsPending.length} (${reason})`
|
|
13355
13550
|
);
|
|
13551
|
+
return true;
|
|
13356
13552
|
}
|
|
13357
13553
|
/** 将未知帧原样转发到 Gateway WS */
|
|
13358
13554
|
forwardRawToGateway(payload) {
|
|
13359
|
-
this.ensureGatewayWs();
|
|
13360
13555
|
if (this.gatewayWsReady && this.gatewayWs?.readyState === wrapper_default.OPEN) {
|
|
13361
|
-
this.gatewayWs
|
|
13556
|
+
this.sendGatewayPayload(this.gatewayWs, { payload }, "raw frame");
|
|
13362
13557
|
} else {
|
|
13363
|
-
this.pushGatewayPending(
|
|
13558
|
+
if (this.pushGatewayPending(
|
|
13559
|
+
{ payload },
|
|
13560
|
+
"raw frame queued before gateway WS ready"
|
|
13561
|
+
)) {
|
|
13562
|
+
this.ensureGatewayWs();
|
|
13563
|
+
}
|
|
13364
13564
|
}
|
|
13365
13565
|
}
|
|
13366
13566
|
/** 处理 Relay 转发的 Gateway RPC 请求帧,原样通过 WebSocket 发给本地 Gateway */
|
|
@@ -13373,17 +13573,99 @@ var TunnelProxy = class {
|
|
|
13373
13573
|
this.opts.logger.info(
|
|
13374
13574
|
`TunnelProxy: req id=${frame.id} method=${frame.method} \u2192 gateway WS (${payload.length} chars)`
|
|
13375
13575
|
);
|
|
13376
|
-
this.ensureGatewayWs();
|
|
13377
13576
|
if (this.gatewayWsReady && this.gatewayWs?.readyState === wrapper_default.OPEN) {
|
|
13378
|
-
this.
|
|
13577
|
+
this.sendGatewayPayload(
|
|
13578
|
+
this.gatewayWs,
|
|
13579
|
+
{ payload, reqId: frame.id },
|
|
13580
|
+
`req id=${frame.id}`
|
|
13581
|
+
);
|
|
13379
13582
|
} else {
|
|
13380
13583
|
this.opts.logger.info(
|
|
13381
13584
|
`TunnelProxy: req id=${frame.id} queued, gateway WS not ready yet`
|
|
13382
13585
|
);
|
|
13383
|
-
this.pushGatewayPending(
|
|
13384
|
-
payload,
|
|
13586
|
+
if (this.pushGatewayPending(
|
|
13587
|
+
{ payload, reqId: frame.id },
|
|
13385
13588
|
`req id=${frame.id} queued before gateway WS handshake`
|
|
13589
|
+
)) {
|
|
13590
|
+
this.ensureGatewayWs();
|
|
13591
|
+
}
|
|
13592
|
+
}
|
|
13593
|
+
}
|
|
13594
|
+
sendGatewayRpcError(id, code, message) {
|
|
13595
|
+
this.gatewayReqMetaById.delete(id);
|
|
13596
|
+
this.opts.client.sendRaw(
|
|
13597
|
+
JSON.stringify({
|
|
13598
|
+
type: "res",
|
|
13599
|
+
id,
|
|
13600
|
+
ok: false,
|
|
13601
|
+
error: {
|
|
13602
|
+
code,
|
|
13603
|
+
message
|
|
13604
|
+
}
|
|
13605
|
+
})
|
|
13606
|
+
);
|
|
13607
|
+
}
|
|
13608
|
+
failGatewayPendingEntry(entry, code, message) {
|
|
13609
|
+
if (!entry.reqId) return;
|
|
13610
|
+
this.sendGatewayRpcError(entry.reqId, code, message);
|
|
13611
|
+
}
|
|
13612
|
+
failGatewayPending(code, message) {
|
|
13613
|
+
if (this.gatewayWsPending.length === 0) return;
|
|
13614
|
+
const pending = this.gatewayWsPending;
|
|
13615
|
+
this.gatewayWsPending = [];
|
|
13616
|
+
this.opts.logger.warn(
|
|
13617
|
+
`TunnelProxy: failing ${pending.length} pending gateway RPC frame(s): ${message}`
|
|
13618
|
+
);
|
|
13619
|
+
for (const entry of pending) {
|
|
13620
|
+
this.failGatewayPendingEntry(entry, code, message);
|
|
13621
|
+
}
|
|
13622
|
+
}
|
|
13623
|
+
failGatewayInflight(code, message) {
|
|
13624
|
+
if (this.gatewayReqMetaById.size === 0) return;
|
|
13625
|
+
const ids = [...this.gatewayReqMetaById.keys()];
|
|
13626
|
+
this.opts.logger.warn(
|
|
13627
|
+
`TunnelProxy: failing ${ids.length} in-flight gateway RPC request(s): ${message}`
|
|
13628
|
+
);
|
|
13629
|
+
for (const id of ids) {
|
|
13630
|
+
this.sendGatewayRpcError(id, code, message);
|
|
13631
|
+
}
|
|
13632
|
+
}
|
|
13633
|
+
failAllGatewayRequests(code, message) {
|
|
13634
|
+
this.failGatewayPending(code, message);
|
|
13635
|
+
this.failGatewayInflight(code, message);
|
|
13636
|
+
}
|
|
13637
|
+
sendGatewayPayload(ws, entry, context) {
|
|
13638
|
+
try {
|
|
13639
|
+
ws.send(entry.payload);
|
|
13640
|
+
return true;
|
|
13641
|
+
} catch (err2) {
|
|
13642
|
+
const message = formatErrorMessage(err2);
|
|
13643
|
+
this.opts.logger.warn(
|
|
13644
|
+
`TunnelProxy: RPC WS send failed for ${context}: ${message}`
|
|
13386
13645
|
);
|
|
13646
|
+
this.failGatewayPendingEntry(
|
|
13647
|
+
entry,
|
|
13648
|
+
"GATEWAY_RPC_SEND_FAILED",
|
|
13649
|
+
`local gateway RPC send failed: ${message}`
|
|
13650
|
+
);
|
|
13651
|
+
this.failGatewayInflight(
|
|
13652
|
+
"GATEWAY_RPC_SEND_FAILED",
|
|
13653
|
+
"local gateway RPC connection closed after send failure"
|
|
13654
|
+
);
|
|
13655
|
+
if (this.gatewayWs === ws) {
|
|
13656
|
+
this.gatewayWs = null;
|
|
13657
|
+
this.gatewayWsReady = false;
|
|
13658
|
+
this.gatewayWsConnecting = false;
|
|
13659
|
+
}
|
|
13660
|
+
try {
|
|
13661
|
+
if (typeof ws.terminate === "function") {
|
|
13662
|
+
ws.terminate();
|
|
13663
|
+
} else {
|
|
13664
|
+
ws.close();
|
|
13665
|
+
}
|
|
13666
|
+
} catch {
|
|
13667
|
+
}
|
|
13668
|
+
return false;
|
|
13387
13669
|
}
|
|
13388
13670
|
}
|
|
13389
13671
|
// ─── Gateway connect auth helpers ───
|
|
@@ -13463,13 +13745,16 @@ var TunnelProxy = class {
|
|
|
13463
13745
|
);
|
|
13464
13746
|
return false;
|
|
13465
13747
|
}
|
|
13748
|
+
this.gatewayWsPairingApprovalPending = true;
|
|
13466
13749
|
try {
|
|
13467
13750
|
const approveDevicePairing = await loadApproveDevicePairing(this.opts.logger);
|
|
13468
13751
|
if (!approveDevicePairing) {
|
|
13752
|
+
this.gatewayWsPairingApprovalPending = false;
|
|
13469
13753
|
return false;
|
|
13470
13754
|
}
|
|
13471
13755
|
const approved = await approveDevicePairing(requestId, this.hostStateDir);
|
|
13472
13756
|
if (!approved) {
|
|
13757
|
+
this.gatewayWsPairingApprovalPending = false;
|
|
13473
13758
|
this.opts.logger.warn(
|
|
13474
13759
|
`TunnelProxy: gateway pairing request ${requestId} not found in host state ${this.hostStateDir}`
|
|
13475
13760
|
);
|
|
@@ -13477,11 +13762,13 @@ var TunnelProxy = class {
|
|
|
13477
13762
|
}
|
|
13478
13763
|
this.gatewayWsAutoPairingApprovals += 1;
|
|
13479
13764
|
this.gatewayWsReconnectRequested = true;
|
|
13765
|
+
this.gatewayWsPairingApprovalPending = false;
|
|
13480
13766
|
this.opts.logger.info(
|
|
13481
13767
|
`TunnelProxy: auto-approved local gateway pairing request ${requestId} (reason=${reason || "not-paired"}, hostStateDir=${this.hostStateDir}, approval=${this.gatewayWsAutoPairingApprovals}/${MAX_AUTO_PAIRING_APPROVALS})`
|
|
13482
13768
|
);
|
|
13483
13769
|
return true;
|
|
13484
13770
|
} catch (err2) {
|
|
13771
|
+
this.gatewayWsPairingApprovalPending = false;
|
|
13485
13772
|
this.opts.logger.warn(
|
|
13486
13773
|
`TunnelProxy: failed to auto-approve gateway pairing request ${requestId}: ${err2?.message ?? String(err2)}`
|
|
13487
13774
|
);
|
|
@@ -13610,9 +13897,56 @@ var TunnelProxy = class {
|
|
|
13610
13897
|
this.opts.logger.info(
|
|
13611
13898
|
`TunnelProxy: RPC WS connecting to gateway ${wsUrl} (pending=${this.gatewayWsPending.length})`
|
|
13612
13899
|
);
|
|
13613
|
-
|
|
13900
|
+
let ws;
|
|
13901
|
+
try {
|
|
13902
|
+
ws = new wrapper_default(wsUrl, {
|
|
13903
|
+
handshakeTimeout: GATEWAY_RPC_HANDSHAKE_TIMEOUT_MS
|
|
13904
|
+
});
|
|
13905
|
+
} catch (err2) {
|
|
13906
|
+
const message = formatErrorMessage(err2);
|
|
13907
|
+
this.gatewayWsConnecting = false;
|
|
13908
|
+
this.opts.logger.error(
|
|
13909
|
+
`TunnelProxy: RPC WS failed to create gateway connection: ${message}`
|
|
13910
|
+
);
|
|
13911
|
+
this.failAllGatewayRequests(
|
|
13912
|
+
"GATEWAY_RPC_CONNECT_FAILED",
|
|
13913
|
+
`local gateway RPC connect failed: ${message}`
|
|
13914
|
+
);
|
|
13915
|
+
return;
|
|
13916
|
+
}
|
|
13917
|
+
this.gatewayWs = ws;
|
|
13918
|
+
let handshakeTimer = setTimeout(
|
|
13919
|
+
() => {
|
|
13920
|
+
handshakeTimer = null;
|
|
13921
|
+
if (this.gatewayWs !== ws || this.gatewayWsReady) return;
|
|
13922
|
+
this.opts.logger.warn(
|
|
13923
|
+
`TunnelProxy: RPC WS handshake timed out after ${GATEWAY_RPC_HANDSHAKE_TIMEOUT_MS}ms (pending=${this.gatewayWsPending.length})`
|
|
13924
|
+
);
|
|
13925
|
+
this.gatewayWs = null;
|
|
13926
|
+
this.gatewayWsReady = false;
|
|
13927
|
+
this.gatewayWsConnecting = false;
|
|
13928
|
+
this.failAllGatewayRequests(
|
|
13929
|
+
"GATEWAY_RPC_HANDSHAKE_TIMEOUT",
|
|
13930
|
+
"local gateway RPC handshake timed out"
|
|
13931
|
+
);
|
|
13932
|
+
try {
|
|
13933
|
+
if (typeof ws.terminate === "function") {
|
|
13934
|
+
ws.terminate();
|
|
13935
|
+
} else {
|
|
13936
|
+
ws.close();
|
|
13937
|
+
}
|
|
13938
|
+
} catch {
|
|
13939
|
+
}
|
|
13940
|
+
},
|
|
13941
|
+
GATEWAY_RPC_HANDSHAKE_TIMEOUT_MS
|
|
13942
|
+
);
|
|
13943
|
+
const clearHandshakeTimer = () => {
|
|
13944
|
+
if (handshakeTimer) {
|
|
13945
|
+
clearTimeout(handshakeTimer);
|
|
13946
|
+
handshakeTimer = null;
|
|
13947
|
+
}
|
|
13948
|
+
};
|
|
13614
13949
|
ws.on("open", () => {
|
|
13615
|
-
this.gatewayWs = ws;
|
|
13616
13950
|
this.opts.logger.info(
|
|
13617
13951
|
`TunnelProxy: RPC WS tcp connected, waiting for connect.challenge (pending=${this.gatewayWsPending.length})`
|
|
13618
13952
|
);
|
|
@@ -13644,6 +13978,7 @@ var TunnelProxy = class {
|
|
|
13644
13978
|
return;
|
|
13645
13979
|
}
|
|
13646
13980
|
if (frame.type === "res" && frame.ok === true && frame.payload?.type === "hello-ok") {
|
|
13981
|
+
clearHandshakeTimer();
|
|
13647
13982
|
this.storeIssuedDeviceToken({
|
|
13648
13983
|
fallbackRole: "operator",
|
|
13649
13984
|
fallbackScopes: ["operator.admin"],
|
|
@@ -13656,10 +13991,14 @@ var TunnelProxy = class {
|
|
|
13656
13991
|
this.opts.logger.info(
|
|
13657
13992
|
`TunnelProxy: RPC WS handshake done (hello-ok), flushing ${this.gatewayWsPending.length} pending frames`
|
|
13658
13993
|
);
|
|
13659
|
-
|
|
13660
|
-
ws.send(pending);
|
|
13661
|
-
}
|
|
13994
|
+
const pending = this.gatewayWsPending;
|
|
13662
13995
|
this.gatewayWsPending = [];
|
|
13996
|
+
for (let i = 0; i < pending.length; i++) {
|
|
13997
|
+
const entry = pending[i];
|
|
13998
|
+
if (!this.sendGatewayPayload(ws, entry, entry.reqId ?? "pending frame")) {
|
|
13999
|
+
return;
|
|
14000
|
+
}
|
|
14001
|
+
}
|
|
13663
14002
|
return;
|
|
13664
14003
|
}
|
|
13665
14004
|
if (frame.type === "res" && frame.ok === false && !this.gatewayWsReady) {
|
|
@@ -13678,7 +14017,12 @@ var TunnelProxy = class {
|
|
|
13678
14017
|
return;
|
|
13679
14018
|
}
|
|
13680
14019
|
this.opts.logger.error(
|
|
13681
|
-
`TunnelProxy: RPC WS handshake failed (pending=${this.gatewayWsPending.length}): ${
|
|
14020
|
+
`TunnelProxy: RPC WS handshake failed (pending=${this.gatewayWsPending.length}): ${previewText(JSON.stringify(frame.error), 500)}`
|
|
14021
|
+
);
|
|
14022
|
+
clearHandshakeTimer();
|
|
14023
|
+
this.failAllGatewayRequests(
|
|
14024
|
+
"GATEWAY_RPC_HANDSHAKE_FAILED",
|
|
14025
|
+
`local gateway RPC handshake failed: ${previewText(JSON.stringify(frame.error), 500)}`
|
|
13682
14026
|
);
|
|
13683
14027
|
ws.close();
|
|
13684
14028
|
return;
|
|
@@ -13689,10 +14033,12 @@ var TunnelProxy = class {
|
|
|
13689
14033
|
}
|
|
13690
14034
|
});
|
|
13691
14035
|
ws.on("close", (code, reason) => {
|
|
14036
|
+
clearHandshakeTimer();
|
|
13692
14037
|
const wasReady = this.gatewayWsReady;
|
|
13693
14038
|
const pendingCount = this.gatewayWsPending.length;
|
|
13694
14039
|
const reasonText = reason.toString();
|
|
13695
14040
|
const shouldReconnect = this.gatewayWsReconnectRequested && pendingCount > 0;
|
|
14041
|
+
const shouldHoldForPairing = this.gatewayWsPairingApprovalPending && pendingCount > 0;
|
|
13696
14042
|
this.opts.logger.info(
|
|
13697
14043
|
`TunnelProxy: RPC WS closed by gateway (code=${code}, reason=${reasonText}, ready=${wasReady}, pending=${pendingCount}, activeWs=${this.wsProxy.activeCount})`
|
|
13698
14044
|
);
|
|
@@ -13708,17 +14054,36 @@ var TunnelProxy = class {
|
|
|
13708
14054
|
`TunnelProxy: retrying RPC WS after local pairing approval (pending=${this.gatewayWsPending.length})`
|
|
13709
14055
|
);
|
|
13710
14056
|
queueMicrotask(() => this.ensureGatewayWs());
|
|
14057
|
+
} else if (!shouldHoldForPairing && (pendingCount > 0 || this.gatewayReqMetaById.size > 0)) {
|
|
14058
|
+
this.failAllGatewayRequests(
|
|
14059
|
+
"GATEWAY_RPC_DISCONNECTED",
|
|
14060
|
+
`local gateway RPC disconnected: code=${code}, reason=${reasonText}`
|
|
14061
|
+
);
|
|
13711
14062
|
}
|
|
13712
14063
|
});
|
|
13713
14064
|
ws.on("error", (err2) => {
|
|
14065
|
+
clearHandshakeTimer();
|
|
13714
14066
|
this.opts.logger.warn(
|
|
13715
14067
|
`TunnelProxy: RPC WS error: ${err2.message} (ready=${this.gatewayWsReady}, pending=${this.gatewayWsPending.length}, activeWs=${this.wsProxy.activeCount})`
|
|
13716
14068
|
);
|
|
14069
|
+
const shouldFailRequests = this.gatewayWs === ws && (this.gatewayWsPending.length > 0 || this.gatewayReqMetaById.size > 0);
|
|
13717
14070
|
this.gatewayWsConnecting = false;
|
|
13718
14071
|
if (this.gatewayWs === ws) {
|
|
13719
14072
|
this.gatewayWs = null;
|
|
13720
14073
|
this.gatewayWsReady = false;
|
|
13721
14074
|
}
|
|
14075
|
+
if (shouldFailRequests) {
|
|
14076
|
+
this.failAllGatewayRequests(
|
|
14077
|
+
"GATEWAY_RPC_ERROR",
|
|
14078
|
+
`local gateway RPC error: ${err2.message}`
|
|
14079
|
+
);
|
|
14080
|
+
}
|
|
14081
|
+
try {
|
|
14082
|
+
if (typeof ws.terminate === "function") {
|
|
14083
|
+
ws.terminate();
|
|
14084
|
+
}
|
|
14085
|
+
} catch {
|
|
14086
|
+
}
|
|
13722
14087
|
});
|
|
13723
14088
|
}
|
|
13724
14089
|
handleConnectChallenge(ws, frame) {
|
|
@@ -13854,7 +14219,7 @@ function createTunnelService(opts) {
|
|
|
13854
14219
|
JSON.stringify({
|
|
13855
14220
|
pid: process.pid,
|
|
13856
14221
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13857
|
-
tunnelUrl: opts.tunnelUrl
|
|
14222
|
+
tunnelUrl: redactUrlSecrets(opts.tunnelUrl)
|
|
13858
14223
|
}) + "\n"
|
|
13859
14224
|
);
|
|
13860
14225
|
lockFilePath = filePath;
|
|
@@ -13920,7 +14285,7 @@ function createTunnelService(opts) {
|
|
|
13920
14285
|
const { logger } = opts;
|
|
13921
14286
|
const baseStateDir = (0, import_node_path24.join)(ctx.stateDir, "plugins", "phone-notifications");
|
|
13922
14287
|
logger.info(
|
|
13923
|
-
`Relay tunnel: starting (pid=${process.pid}, url=${opts.tunnelUrl}, heartbeat=${opts.heartbeatSec ?? DEFAULT_HEARTBEAT_SEC}s, backoff=${opts.reconnectBackoffMs ?? DEFAULT_RECONNECT_BACKOFF_MS}ms, gateway=${opts.gatewayBaseUrl}, hasGatewayToken=${!!opts.gatewayToken}, hasGatewayPwd=${!!opts.gatewayPassword})`
|
|
14288
|
+
`Relay tunnel: starting (pid=${process.pid}, url=${redactUrlSecrets(opts.tunnelUrl)}, heartbeat=${opts.heartbeatSec ?? DEFAULT_HEARTBEAT_SEC}s, backoff=${opts.reconnectBackoffMs ?? DEFAULT_RECONNECT_BACKOFF_MS}ms, gateway=${opts.gatewayBaseUrl}, hasGatewayToken=${!!opts.gatewayToken}, hasGatewayPwd=${!!opts.gatewayPassword})`
|
|
13924
14289
|
);
|
|
13925
14290
|
const statusFilePath = (0, import_node_path24.join)(baseStateDir, "tunnel-status.json");
|
|
13926
14291
|
const lockPath = (0, import_node_path24.join)(baseStateDir, "relay-tunnel.lock");
|
|
@@ -13950,6 +14315,12 @@ function createTunnelService(opts) {
|
|
|
13950
14315
|
client.onConnected(() => {
|
|
13951
14316
|
emitPendingPluginUpdate("relay connected");
|
|
13952
14317
|
});
|
|
14318
|
+
client.onDisconnected((reason) => {
|
|
14319
|
+
logger.warn(
|
|
14320
|
+
`Relay tunnel: relay disconnected, cleaning local proxy state (${reason})`
|
|
14321
|
+
);
|
|
14322
|
+
proxy?.cleanup();
|
|
14323
|
+
});
|
|
13953
14324
|
abortController = new AbortController();
|
|
13954
14325
|
client.connectWithAutoReconnect(abortController.signal).catch((err2) => {
|
|
13955
14326
|
releaseLock();
|
|
@@ -14072,10 +14443,18 @@ function resolveLocalGatewayAuth(params) {
|
|
|
14072
14443
|
gatewayPassword: envGatewayPassword ?? configGatewayPassword
|
|
14073
14444
|
};
|
|
14074
14445
|
}
|
|
14446
|
+
var DISABLED_TAILSCALE_MODES = /* @__PURE__ */ new Set([
|
|
14447
|
+
"off",
|
|
14448
|
+
"disabled",
|
|
14449
|
+
"disable",
|
|
14450
|
+
"none",
|
|
14451
|
+
"false",
|
|
14452
|
+
"no"
|
|
14453
|
+
]);
|
|
14075
14454
|
function resolveExclusiveTunnelHint(params) {
|
|
14076
14455
|
const configData = readHostGatewayConfig(params);
|
|
14077
14456
|
const tailscaleMode = trimToUndefined2(configData?.gateway?.tailscale?.mode);
|
|
14078
|
-
if (tailscaleMode) {
|
|
14457
|
+
if (tailscaleMode && !DISABLED_TAILSCALE_MODES.has(tailscaleMode.toLowerCase())) {
|
|
14079
14458
|
return `gateway.tailscale.mode=${tailscaleMode}`;
|
|
14080
14459
|
}
|
|
14081
14460
|
const remoteUrl = trimToUndefined2(configData?.gateway?.remote?.url);
|