@yoooclaw/phone-notifications 1.7.1 → 1.7.2
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.js +105 -29
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -5495,6 +5495,16 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
|
5495
5495
|
var wrapper_default = import_websocket.default;
|
|
5496
5496
|
|
|
5497
5497
|
// src/tunnel/relay-client.ts
|
|
5498
|
+
function previewText(text, max = 500) {
|
|
5499
|
+
return text.length <= max ? text : `${text.substring(0, max)}\u2026`;
|
|
5500
|
+
}
|
|
5501
|
+
function maskSecret(value) {
|
|
5502
|
+
if (!value) return "empty";
|
|
5503
|
+
if (value.length <= 8) {
|
|
5504
|
+
return `${value.slice(0, 2)}\u2026${value.slice(-2)}`;
|
|
5505
|
+
}
|
|
5506
|
+
return `${value.slice(0, 4)}\u2026${value.slice(-4)}`;
|
|
5507
|
+
}
|
|
5498
5508
|
var RelayClient = class {
|
|
5499
5509
|
constructor(opts) {
|
|
5500
5510
|
this.opts = opts;
|
|
@@ -5575,8 +5585,9 @@ var RelayClient = class {
|
|
|
5575
5585
|
async connect() {
|
|
5576
5586
|
if (this.aborted) return;
|
|
5577
5587
|
this.cleanup(true);
|
|
5588
|
+
const rawApiKey = this.opts.apiKey.startsWith("Bearer ") ? this.opts.apiKey.slice("Bearer ".length) : this.opts.apiKey;
|
|
5578
5589
|
this.opts.logger.info(
|
|
5579
|
-
`Relay tunnel: connecting to ${this.opts.tunnelUrl} (attempt=${this.reconnectAttempt}, heartbeat=${this.opts.heartbeatSec}s)`
|
|
5590
|
+
`Relay tunnel: connecting to ${this.opts.tunnelUrl} (attempt=${this.reconnectAttempt}, heartbeat=${this.opts.heartbeatSec}s, apiKey=${maskSecret(rawApiKey)})`
|
|
5580
5591
|
);
|
|
5581
5592
|
this.writeStatus("connecting");
|
|
5582
5593
|
return new Promise((resolve) => {
|
|
@@ -5587,7 +5598,6 @@ var RelayClient = class {
|
|
|
5587
5598
|
resolve();
|
|
5588
5599
|
}
|
|
5589
5600
|
};
|
|
5590
|
-
const rawApiKey = this.opts.apiKey.startsWith("Bearer ") ? this.opts.apiKey.slice("Bearer ".length) : this.opts.apiKey;
|
|
5591
5601
|
const wsUrl = new URL(this.opts.tunnelUrl);
|
|
5592
5602
|
if (!wsUrl.searchParams.get("apiKey")) {
|
|
5593
5603
|
wsUrl.searchParams.set("apiKey", rawApiKey);
|
|
@@ -5631,8 +5641,9 @@ var RelayClient = class {
|
|
|
5631
5641
|
});
|
|
5632
5642
|
ws.on("close", (code, reason) => {
|
|
5633
5643
|
const reasonStr = reason.toString();
|
|
5644
|
+
const lastInboundAgoMs = this.lastInboundAt ? Date.now() - this.lastInboundAt : null;
|
|
5634
5645
|
this.opts.logger.warn(
|
|
5635
|
-
`Relay tunnel: disconnected (code=${code}, reason=${reasonStr})`
|
|
5646
|
+
`Relay tunnel: disconnected (code=${code}, reason=${previewText(reasonStr, 200)}, lastInboundAgoMs=${lastInboundAgoMs ?? "N/A"}, reconnectAttempt=${this.reconnectAttempt})`
|
|
5636
5647
|
);
|
|
5637
5648
|
if (this.ws === ws) {
|
|
5638
5649
|
this.stopHeartbeat();
|
|
@@ -5643,7 +5654,9 @@ var RelayClient = class {
|
|
|
5643
5654
|
settle();
|
|
5644
5655
|
});
|
|
5645
5656
|
ws.on("error", (err) => {
|
|
5646
|
-
this.opts.logger.error(
|
|
5657
|
+
this.opts.logger.error(
|
|
5658
|
+
`Relay tunnel: WebSocket error: ${err.message} (readyState=${ws.readyState}, reconnectAttempt=${this.reconnectAttempt}, url=${wsUrl.toString()})`
|
|
5659
|
+
);
|
|
5647
5660
|
settle();
|
|
5648
5661
|
});
|
|
5649
5662
|
});
|
|
@@ -5654,13 +5667,16 @@ var RelayClient = class {
|
|
|
5654
5667
|
if (text === "pong") {
|
|
5655
5668
|
return;
|
|
5656
5669
|
}
|
|
5657
|
-
|
|
5658
|
-
|
|
5670
|
+
this.opts.logger.info(
|
|
5671
|
+
`Relay tunnel: \u2605 received message (${text.length} chars): ${previewText(text)}`
|
|
5672
|
+
);
|
|
5659
5673
|
let frame;
|
|
5660
5674
|
try {
|
|
5661
5675
|
frame = JSON.parse(text);
|
|
5662
5676
|
} catch {
|
|
5663
|
-
this.opts.logger.warn(
|
|
5677
|
+
this.opts.logger.warn(
|
|
5678
|
+
`Relay tunnel: received invalid frame, ignoring (preview=${previewText(text, 200)})`
|
|
5679
|
+
);
|
|
5664
5680
|
return;
|
|
5665
5681
|
}
|
|
5666
5682
|
this.opts.logger.info(`Relay tunnel: parsed frame type=${frame.type}, id=${"id" in frame ? frame.id : "N/A"}`);
|
|
@@ -5775,6 +5791,32 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
5775
5791
|
import crypto from "crypto";
|
|
5776
5792
|
import fs from "fs";
|
|
5777
5793
|
import path from "path";
|
|
5794
|
+
function previewText2(text, max = 200) {
|
|
5795
|
+
if (!text) return "";
|
|
5796
|
+
return text.length <= max ? text : `${text.substring(0, max)}\u2026`;
|
|
5797
|
+
}
|
|
5798
|
+
function findHeaderValue(headers, key) {
|
|
5799
|
+
if (!headers) return void 0;
|
|
5800
|
+
const lowerKey = key.toLowerCase();
|
|
5801
|
+
for (const [headerKey, headerValue] of Object.entries(headers)) {
|
|
5802
|
+
if (headerKey.toLowerCase() === lowerKey) {
|
|
5803
|
+
return headerValue;
|
|
5804
|
+
}
|
|
5805
|
+
}
|
|
5806
|
+
return void 0;
|
|
5807
|
+
}
|
|
5808
|
+
function summarizeRequestHeaders(headers) {
|
|
5809
|
+
const contentType = findHeaderValue(headers, "content-type");
|
|
5810
|
+
const requestId = findHeaderValue(headers, "x-request-id");
|
|
5811
|
+
const parts = [];
|
|
5812
|
+
if (contentType) {
|
|
5813
|
+
parts.push(`contentType=${contentType}`);
|
|
5814
|
+
}
|
|
5815
|
+
if (requestId) {
|
|
5816
|
+
parts.push(`xRequestId=${previewText2(requestId, 120)}`);
|
|
5817
|
+
}
|
|
5818
|
+
return parts.length ? `, ${parts.join(", ")}` : "";
|
|
5819
|
+
}
|
|
5778
5820
|
var ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
5779
5821
|
function base64UrlEncode(buf) {
|
|
5780
5822
|
return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
|
|
@@ -5873,6 +5915,12 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
5873
5915
|
gatewayWsPending = [];
|
|
5874
5916
|
/** 设备身份,用于 Gateway connect 握手 */
|
|
5875
5917
|
deviceIdentity;
|
|
5918
|
+
pushGatewayPending(payload, reason) {
|
|
5919
|
+
this.gatewayWsPending.push(payload);
|
|
5920
|
+
this.opts.logger.info(
|
|
5921
|
+
`TunnelProxy: gateway WS pending queue size=${this.gatewayWsPending.length} (${reason})`
|
|
5922
|
+
);
|
|
5923
|
+
}
|
|
5876
5924
|
resolveGatewayConnectAuth() {
|
|
5877
5925
|
const token = this.opts.gatewayToken?.trim() || void 0;
|
|
5878
5926
|
const password = this.opts.gatewayPassword?.trim() || void 0;
|
|
@@ -5921,19 +5969,21 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
5921
5969
|
}
|
|
5922
5970
|
return attempts;
|
|
5923
5971
|
}
|
|
5924
|
-
async sendHttpResponse(
|
|
5972
|
+
async sendHttpResponse(params) {
|
|
5973
|
+
const { frameId, method, path: path2, authLabel, startedAtMs, res } = params;
|
|
5925
5974
|
const contentType = res.headers.get("content-type") ?? "";
|
|
5926
5975
|
const isStreaming = contentType.includes("text/event-stream");
|
|
5976
|
+
const elapsedMs = Date.now() - startedAtMs;
|
|
5927
5977
|
this.opts.logger.info(
|
|
5928
|
-
`TunnelProxy:
|
|
5978
|
+
`TunnelProxy: HTTP id=${frameId} ${method} ${path2} <= ${res.status} (${elapsedMs}ms, auth=${authLabel}, content-type=${contentType}, streaming=${isStreaming})`
|
|
5929
5979
|
);
|
|
5930
5980
|
if (isStreaming && res.body) {
|
|
5931
|
-
await this.streamResponse(frameId, res);
|
|
5981
|
+
await this.streamResponse(frameId, res, startedAtMs);
|
|
5932
5982
|
return;
|
|
5933
5983
|
}
|
|
5934
5984
|
const body = await res.text();
|
|
5935
5985
|
this.opts.logger.info(
|
|
5936
|
-
`TunnelProxy:
|
|
5986
|
+
`TunnelProxy: HTTP id=${frameId} response body=${previewText2(body)}`
|
|
5937
5987
|
);
|
|
5938
5988
|
const headers = {};
|
|
5939
5989
|
res.headers.forEach((value, key) => {
|
|
@@ -5982,7 +6032,7 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
5982
6032
|
if (this.gatewayWsReady && this.gatewayWs?.readyState === wrapper_default.OPEN) {
|
|
5983
6033
|
this.gatewayWs.send(payload);
|
|
5984
6034
|
} else {
|
|
5985
|
-
this.
|
|
6035
|
+
this.pushGatewayPending(payload, "raw frame queued before gateway WS ready");
|
|
5986
6036
|
}
|
|
5987
6037
|
}
|
|
5988
6038
|
/** 清理所有代理的 WebSocket 连接 */
|
|
@@ -6016,12 +6066,14 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6016
6066
|
this.gatewayWsConnecting = true;
|
|
6017
6067
|
this.gatewayWsReady = false;
|
|
6018
6068
|
const wsUrl = this.opts.gatewayBaseUrl.replace(/^http/, "ws");
|
|
6019
|
-
this.opts.logger.info(
|
|
6069
|
+
this.opts.logger.info(
|
|
6070
|
+
`TunnelProxy: RPC WS connecting to gateway ${wsUrl} (pending=${this.gatewayWsPending.length})`
|
|
6071
|
+
);
|
|
6020
6072
|
const ws = new wrapper_default(wsUrl);
|
|
6021
6073
|
ws.on("open", () => {
|
|
6022
6074
|
this.gatewayWs = ws;
|
|
6023
6075
|
this.opts.logger.info(
|
|
6024
|
-
|
|
6076
|
+
`TunnelProxy: RPC WS tcp connected, waiting for connect.challenge (pending=${this.gatewayWsPending.length})`
|
|
6025
6077
|
);
|
|
6026
6078
|
});
|
|
6027
6079
|
ws.on("message", (data) => {
|
|
@@ -6048,15 +6100,16 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6048
6100
|
}
|
|
6049
6101
|
if (frame.type === "event" && frame.event === "connect.challenge") {
|
|
6050
6102
|
const challengeNonce = frame.payload?.nonce ?? "";
|
|
6103
|
+
const connectRequestId = `tunnel-connect-${randomUUID2()}`;
|
|
6104
|
+
const gatewayAuth = this.resolveGatewayConnectAuth();
|
|
6051
6105
|
this.opts.logger.info(
|
|
6052
|
-
`TunnelProxy: received connect.challenge (nonce=${challengeNonce}), sending connect request with device identity`
|
|
6106
|
+
`TunnelProxy: received connect.challenge (nonce=${challengeNonce}, connectReqId=${connectRequestId}, hasToken=${!!gatewayAuth?.token}, hasPassword=${!!gatewayAuth?.password}), sending connect request with device identity`
|
|
6053
6107
|
);
|
|
6054
6108
|
const role = "operator";
|
|
6055
6109
|
const scopes = ["operator.admin"];
|
|
6056
6110
|
const signedAtMs = Date.now();
|
|
6057
6111
|
const clientId = "gateway-client";
|
|
6058
6112
|
const clientMode = "backend";
|
|
6059
|
-
const gatewayAuth = this.resolveGatewayConnectAuth();
|
|
6060
6113
|
const authPayload = buildDeviceAuthPayload({
|
|
6061
6114
|
deviceId: this.deviceIdentity.deviceId,
|
|
6062
6115
|
clientId,
|
|
@@ -6073,7 +6126,7 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6073
6126
|
);
|
|
6074
6127
|
const connectReq = {
|
|
6075
6128
|
type: "req",
|
|
6076
|
-
id:
|
|
6129
|
+
id: connectRequestId,
|
|
6077
6130
|
method: "connect",
|
|
6078
6131
|
params: {
|
|
6079
6132
|
minProtocol: 3,
|
|
@@ -6115,7 +6168,7 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6115
6168
|
}
|
|
6116
6169
|
if (frame.type === "res" && frame.ok === false && !this.gatewayWsReady) {
|
|
6117
6170
|
this.opts.logger.error(
|
|
6118
|
-
`TunnelProxy: RPC WS handshake failed: ${JSON.stringify(frame.error)}`
|
|
6171
|
+
`TunnelProxy: RPC WS handshake failed (pending=${this.gatewayWsPending.length}): ${previewText2(JSON.stringify(frame.error), 500)}`
|
|
6119
6172
|
);
|
|
6120
6173
|
ws.close();
|
|
6121
6174
|
return;
|
|
@@ -6123,8 +6176,10 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6123
6176
|
this.opts.client.sendRaw(text);
|
|
6124
6177
|
});
|
|
6125
6178
|
ws.on("close", (code, reason) => {
|
|
6179
|
+
const wasReady = this.gatewayWsReady;
|
|
6180
|
+
const pendingCount = this.gatewayWsPending.length;
|
|
6126
6181
|
this.opts.logger.info(
|
|
6127
|
-
`TunnelProxy: RPC WS closed by gateway (code=${code}, reason=${reason.toString()})`
|
|
6182
|
+
`TunnelProxy: RPC WS closed by gateway (code=${code}, reason=${reason.toString()}, ready=${wasReady}, pending=${pendingCount}, activeWs=${this.wsConnections.size})`
|
|
6128
6183
|
);
|
|
6129
6184
|
if (this.gatewayWs === ws) {
|
|
6130
6185
|
this.gatewayWs = null;
|
|
@@ -6133,7 +6188,9 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6133
6188
|
this.gatewayWsConnecting = false;
|
|
6134
6189
|
});
|
|
6135
6190
|
ws.on("error", (err) => {
|
|
6136
|
-
this.opts.logger.warn(
|
|
6191
|
+
this.opts.logger.warn(
|
|
6192
|
+
`TunnelProxy: RPC WS error: ${err.message} (ready=${this.gatewayWsReady}, pending=${this.gatewayWsPending.length}, activeWs=${this.wsConnections.size})`
|
|
6193
|
+
);
|
|
6137
6194
|
this.gatewayWsConnecting = false;
|
|
6138
6195
|
if (this.gatewayWs === ws) {
|
|
6139
6196
|
this.gatewayWs = null;
|
|
@@ -6154,7 +6211,10 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6154
6211
|
this.opts.logger.info(
|
|
6155
6212
|
`TunnelProxy: req id=${frame.id} queued, gateway WS not ready yet`
|
|
6156
6213
|
);
|
|
6157
|
-
this.
|
|
6214
|
+
this.pushGatewayPending(
|
|
6215
|
+
payload,
|
|
6216
|
+
`req id=${frame.id} queued before gateway WS handshake`
|
|
6217
|
+
);
|
|
6158
6218
|
}
|
|
6159
6219
|
}
|
|
6160
6220
|
// ─── 路径映射 ───
|
|
@@ -6166,7 +6226,9 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6166
6226
|
}
|
|
6167
6227
|
// ─── HTTP 请求代理 ───
|
|
6168
6228
|
async handleHttpRequest(frame) {
|
|
6169
|
-
const
|
|
6229
|
+
const mappedPath = this.mapPath(frame.path);
|
|
6230
|
+
const url = new URL(mappedPath, this.opts.gatewayBaseUrl);
|
|
6231
|
+
const startedAtMs = Date.now();
|
|
6170
6232
|
const localHeaders = {};
|
|
6171
6233
|
for (const [k, v] of Object.entries(frame.headers ?? {})) {
|
|
6172
6234
|
const lower = k.toLowerCase();
|
|
@@ -6176,11 +6238,14 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6176
6238
|
}
|
|
6177
6239
|
const authAttempts = this.buildLocalGatewayAuthAttempts(localHeaders);
|
|
6178
6240
|
this.opts.logger.info(
|
|
6179
|
-
`TunnelProxy: HTTP ${frame.method} ${frame.path} \u2192 ${url.toString()}, body=${frame.body
|
|
6241
|
+
`TunnelProxy: HTTP id=${frame.id} ${frame.method} ${frame.path} \u2192 ${url.toString()}${summarizeRequestHeaders(frame.headers)}, authAttempts=${authAttempts.map((attempt) => attempt.label).join(" -> ")}, body=${previewText2(frame.body)}`
|
|
6180
6242
|
);
|
|
6181
6243
|
try {
|
|
6182
6244
|
for (let attemptIndex = 0; attemptIndex < authAttempts.length; attemptIndex++) {
|
|
6183
6245
|
const attempt = authAttempts[attemptIndex];
|
|
6246
|
+
this.opts.logger.info(
|
|
6247
|
+
`TunnelProxy: HTTP id=${frame.id} attempt ${attemptIndex + 1}/${authAttempts.length} auth=${attempt.label}`
|
|
6248
|
+
);
|
|
6184
6249
|
const res = await fetch(url.toString(), {
|
|
6185
6250
|
method: frame.method,
|
|
6186
6251
|
headers: attempt.headers,
|
|
@@ -6190,23 +6255,34 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6190
6255
|
if (res.status === 401 && hasFallback) {
|
|
6191
6256
|
const body = await res.text();
|
|
6192
6257
|
this.opts.logger.warn(
|
|
6193
|
-
`TunnelProxy: local gateway auth via ${attempt.label} returned 401, retrying next credential${body ? `, body=${body
|
|
6258
|
+
`TunnelProxy: HTTP id=${frame.id} local gateway auth via ${attempt.label} returned 401 after ${Date.now() - startedAtMs}ms, retrying next credential${body ? `, body=${previewText2(body)}` : ""}`
|
|
6194
6259
|
);
|
|
6195
6260
|
continue;
|
|
6196
6261
|
}
|
|
6197
|
-
await this.sendHttpResponse(
|
|
6262
|
+
await this.sendHttpResponse({
|
|
6263
|
+
frameId: frame.id,
|
|
6264
|
+
method: frame.method,
|
|
6265
|
+
path: mappedPath,
|
|
6266
|
+
authLabel: attempt.label,
|
|
6267
|
+
startedAtMs,
|
|
6268
|
+
res
|
|
6269
|
+
});
|
|
6198
6270
|
return;
|
|
6199
6271
|
}
|
|
6200
6272
|
} catch (err) {
|
|
6273
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6274
|
+
this.opts.logger.error(
|
|
6275
|
+
`TunnelProxy: HTTP id=${frame.id} ${frame.method} ${mappedPath} failed after ${Date.now() - startedAtMs}ms: ${message}`
|
|
6276
|
+
);
|
|
6201
6277
|
this.opts.client.send({
|
|
6202
6278
|
type: "proxy_error",
|
|
6203
6279
|
id: frame.id,
|
|
6204
6280
|
status: 502,
|
|
6205
|
-
message: `gateway unreachable: ${
|
|
6281
|
+
message: `gateway unreachable: ${message}`
|
|
6206
6282
|
});
|
|
6207
6283
|
}
|
|
6208
6284
|
}
|
|
6209
|
-
async streamResponse(requestId, res) {
|
|
6285
|
+
async streamResponse(requestId, res, startedAtMs) {
|
|
6210
6286
|
const reader = res.body.getReader();
|
|
6211
6287
|
const decoder = new TextDecoder();
|
|
6212
6288
|
let chunkCount = 0;
|
|
@@ -6228,7 +6304,7 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6228
6304
|
});
|
|
6229
6305
|
}
|
|
6230
6306
|
this.opts.logger.info(
|
|
6231
|
-
`TunnelProxy: stream end id=${requestId}, total chunks=${chunkCount}`
|
|
6307
|
+
`TunnelProxy: stream end id=${requestId}, total chunks=${chunkCount}, totalElapsedMs=${Date.now() - startedAtMs}`
|
|
6232
6308
|
);
|
|
6233
6309
|
this.opts.client.send({
|
|
6234
6310
|
type: "stream",
|
|
@@ -6238,7 +6314,7 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6238
6314
|
});
|
|
6239
6315
|
} catch (err) {
|
|
6240
6316
|
this.opts.logger.error(
|
|
6241
|
-
`TunnelProxy: stream error id=${requestId} after ${chunkCount} chunks: ${err instanceof Error ? err.message : String(err)}`
|
|
6317
|
+
`TunnelProxy: stream error id=${requestId} after ${chunkCount} chunks and ${Date.now() - startedAtMs}ms: ${err instanceof Error ? err.message : String(err)}`
|
|
6242
6318
|
);
|
|
6243
6319
|
this.opts.client.send({
|
|
6244
6320
|
type: "proxy_error",
|