@yoooclaw/phone-notifications 1.7.1 → 1.7.3
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 +269 -38
- 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, "");
|
|
@@ -5814,11 +5856,96 @@ function buildDeviceAuthPayload(params) {
|
|
|
5814
5856
|
params.nonce
|
|
5815
5857
|
].join("|");
|
|
5816
5858
|
}
|
|
5817
|
-
function
|
|
5818
|
-
return resolveStateDir();
|
|
5859
|
+
function resolveClientStateDir(stateDir) {
|
|
5860
|
+
return stateDir ?? resolveStateDir();
|
|
5861
|
+
}
|
|
5862
|
+
function ensureDir(filePath) {
|
|
5863
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
5864
|
+
}
|
|
5865
|
+
function resolveIdentityPath(stateDir) {
|
|
5866
|
+
return path.join(stateDir, "identity", "device.json");
|
|
5867
|
+
}
|
|
5868
|
+
function normalizeDeviceAuthRole(role) {
|
|
5869
|
+
return role.trim();
|
|
5870
|
+
}
|
|
5871
|
+
function normalizeDeviceAuthScopes(scopes) {
|
|
5872
|
+
const out = /* @__PURE__ */ new Set();
|
|
5873
|
+
for (const scope of scopes) {
|
|
5874
|
+
const trimmed = scope.trim();
|
|
5875
|
+
if (trimmed) {
|
|
5876
|
+
out.add(trimmed);
|
|
5877
|
+
}
|
|
5878
|
+
}
|
|
5879
|
+
return [...out].sort();
|
|
5880
|
+
}
|
|
5881
|
+
function resolveDeviceAuthPath(stateDir) {
|
|
5882
|
+
return path.join(stateDir, "identity", "device-auth.json");
|
|
5883
|
+
}
|
|
5884
|
+
function readDeviceAuthStore(filePath) {
|
|
5885
|
+
try {
|
|
5886
|
+
if (!fs.existsSync(filePath)) return null;
|
|
5887
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
5888
|
+
const parsed = JSON.parse(raw);
|
|
5889
|
+
if (parsed?.version !== 1 || typeof parsed.deviceId !== "string") return null;
|
|
5890
|
+
if (!parsed.tokens || typeof parsed.tokens !== "object") return null;
|
|
5891
|
+
return parsed;
|
|
5892
|
+
} catch {
|
|
5893
|
+
return null;
|
|
5894
|
+
}
|
|
5895
|
+
}
|
|
5896
|
+
function writeDeviceAuthStore(filePath, store) {
|
|
5897
|
+
ensureDir(filePath);
|
|
5898
|
+
fs.writeFileSync(filePath, `${JSON.stringify(store, null, 2)}
|
|
5899
|
+
`, {
|
|
5900
|
+
mode: 384
|
|
5901
|
+
});
|
|
5902
|
+
try {
|
|
5903
|
+
fs.chmodSync(filePath, 384);
|
|
5904
|
+
} catch {
|
|
5905
|
+
}
|
|
5906
|
+
}
|
|
5907
|
+
function loadDeviceAuthToken(params) {
|
|
5908
|
+
const store = readDeviceAuthStore(resolveDeviceAuthPath(params.stateDir));
|
|
5909
|
+
if (!store || store.deviceId !== params.deviceId) return null;
|
|
5910
|
+
const entry = store.tokens[normalizeDeviceAuthRole(params.role)];
|
|
5911
|
+
if (!entry || typeof entry.token !== "string") return null;
|
|
5912
|
+
return entry;
|
|
5913
|
+
}
|
|
5914
|
+
function storeDeviceAuthToken(params) {
|
|
5915
|
+
const filePath = resolveDeviceAuthPath(params.stateDir);
|
|
5916
|
+
const existing = readDeviceAuthStore(filePath);
|
|
5917
|
+
const role = normalizeDeviceAuthRole(params.role);
|
|
5918
|
+
const next = {
|
|
5919
|
+
version: 1,
|
|
5920
|
+
deviceId: params.deviceId,
|
|
5921
|
+
tokens: existing && existing.deviceId === params.deviceId && existing.tokens ? { ...existing.tokens } : {}
|
|
5922
|
+
};
|
|
5923
|
+
const entry = {
|
|
5924
|
+
token: params.token,
|
|
5925
|
+
role,
|
|
5926
|
+
scopes: normalizeDeviceAuthScopes(params.scopes),
|
|
5927
|
+
updatedAtMs: Date.now()
|
|
5928
|
+
};
|
|
5929
|
+
next.tokens[role] = entry;
|
|
5930
|
+
writeDeviceAuthStore(filePath, next);
|
|
5931
|
+
return entry;
|
|
5932
|
+
}
|
|
5933
|
+
function clearDeviceAuthToken(params) {
|
|
5934
|
+
const filePath = resolveDeviceAuthPath(params.stateDir);
|
|
5935
|
+
const store = readDeviceAuthStore(filePath);
|
|
5936
|
+
if (!store || store.deviceId !== params.deviceId) return;
|
|
5937
|
+
const role = normalizeDeviceAuthRole(params.role);
|
|
5938
|
+
if (!store.tokens[role]) return;
|
|
5939
|
+
const next = {
|
|
5940
|
+
version: 1,
|
|
5941
|
+
deviceId: store.deviceId,
|
|
5942
|
+
tokens: { ...store.tokens }
|
|
5943
|
+
};
|
|
5944
|
+
delete next.tokens[role];
|
|
5945
|
+
writeDeviceAuthStore(filePath, next);
|
|
5819
5946
|
}
|
|
5820
|
-
function loadOrCreateDeviceIdentity() {
|
|
5821
|
-
const filePath =
|
|
5947
|
+
function loadOrCreateDeviceIdentity(stateDir) {
|
|
5948
|
+
const filePath = resolveIdentityPath(stateDir);
|
|
5822
5949
|
try {
|
|
5823
5950
|
if (fs.existsSync(filePath)) {
|
|
5824
5951
|
const raw = fs.readFileSync(filePath, "utf8");
|
|
@@ -5848,6 +5975,7 @@ function loadOrCreateDeviceIdentity() {
|
|
|
5848
5975
|
...identity,
|
|
5849
5976
|
createdAtMs: Date.now()
|
|
5850
5977
|
};
|
|
5978
|
+
ensureDir(filePath);
|
|
5851
5979
|
fs.writeFileSync(filePath, `${JSON.stringify(stored, null, 2)}
|
|
5852
5980
|
`, {
|
|
5853
5981
|
mode: 384
|
|
@@ -5857,7 +5985,8 @@ function loadOrCreateDeviceIdentity() {
|
|
|
5857
5985
|
var TunnelProxy = class _TunnelProxy {
|
|
5858
5986
|
constructor(opts) {
|
|
5859
5987
|
this.opts = opts;
|
|
5860
|
-
this.
|
|
5988
|
+
this.stateDir = resolveClientStateDir(opts.stateDir);
|
|
5989
|
+
this.deviceIdentity = loadOrCreateDeviceIdentity(this.stateDir);
|
|
5861
5990
|
opts.logger.info(
|
|
5862
5991
|
`TunnelProxy: loaded device identity (deviceId=${this.deviceIdentity.deviceId})`
|
|
5863
5992
|
);
|
|
@@ -5873,6 +6002,13 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
5873
6002
|
gatewayWsPending = [];
|
|
5874
6003
|
/** 设备身份,用于 Gateway connect 握手 */
|
|
5875
6004
|
deviceIdentity;
|
|
6005
|
+
stateDir;
|
|
6006
|
+
pushGatewayPending(payload, reason) {
|
|
6007
|
+
this.gatewayWsPending.push(payload);
|
|
6008
|
+
this.opts.logger.info(
|
|
6009
|
+
`TunnelProxy: gateway WS pending queue size=${this.gatewayWsPending.length} (${reason})`
|
|
6010
|
+
);
|
|
6011
|
+
}
|
|
5876
6012
|
resolveGatewayConnectAuth() {
|
|
5877
6013
|
const token = this.opts.gatewayToken?.trim() || void 0;
|
|
5878
6014
|
const password = this.opts.gatewayPassword?.trim() || void 0;
|
|
@@ -5884,6 +6020,65 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
5884
6020
|
password
|
|
5885
6021
|
};
|
|
5886
6022
|
}
|
|
6023
|
+
loadStoredDeviceToken(role) {
|
|
6024
|
+
return loadDeviceAuthToken({
|
|
6025
|
+
stateDir: this.stateDir,
|
|
6026
|
+
deviceId: this.deviceIdentity.deviceId,
|
|
6027
|
+
role
|
|
6028
|
+
})?.token ?? void 0;
|
|
6029
|
+
}
|
|
6030
|
+
buildGatewayConnectAuth(role) {
|
|
6031
|
+
const explicitAuth = this.resolveGatewayConnectAuth();
|
|
6032
|
+
const authPassword = explicitAuth?.password?.trim() || void 0;
|
|
6033
|
+
const explicitGatewayToken = explicitAuth?.token?.trim() || void 0;
|
|
6034
|
+
const deviceToken = explicitGatewayToken ? void 0 : this.loadStoredDeviceToken(role);
|
|
6035
|
+
const authToken = explicitGatewayToken ?? deviceToken;
|
|
6036
|
+
const auth = authToken || authPassword || deviceToken ? {
|
|
6037
|
+
token: authToken,
|
|
6038
|
+
deviceToken,
|
|
6039
|
+
password: authPassword
|
|
6040
|
+
} : void 0;
|
|
6041
|
+
return {
|
|
6042
|
+
auth,
|
|
6043
|
+
authToken,
|
|
6044
|
+
authPassword,
|
|
6045
|
+
deviceToken
|
|
6046
|
+
};
|
|
6047
|
+
}
|
|
6048
|
+
storeIssuedDeviceToken(params) {
|
|
6049
|
+
const token = params.authInfo?.deviceToken;
|
|
6050
|
+
if (typeof token !== "string" || !token.trim()) {
|
|
6051
|
+
return;
|
|
6052
|
+
}
|
|
6053
|
+
const role = typeof params.authInfo?.role === "string" && params.authInfo.role.trim() ? params.authInfo.role.trim() : params.fallbackRole;
|
|
6054
|
+
const scopes = Array.isArray(params.authInfo?.scopes) ? params.authInfo.scopes.filter(
|
|
6055
|
+
(scope) => typeof scope === "string" && !!scope.trim()
|
|
6056
|
+
) : params.fallbackScopes;
|
|
6057
|
+
storeDeviceAuthToken({
|
|
6058
|
+
stateDir: this.stateDir,
|
|
6059
|
+
deviceId: this.deviceIdentity.deviceId,
|
|
6060
|
+
role,
|
|
6061
|
+
token: token.trim(),
|
|
6062
|
+
scopes
|
|
6063
|
+
});
|
|
6064
|
+
}
|
|
6065
|
+
maybeClearStoredDeviceTokenOnMismatch(code, reason) {
|
|
6066
|
+
const explicitAuth = this.resolveGatewayConnectAuth();
|
|
6067
|
+
if (explicitAuth?.token || explicitAuth?.password) {
|
|
6068
|
+
return;
|
|
6069
|
+
}
|
|
6070
|
+
if (code !== 1008 || !reason.toLowerCase().includes("device token mismatch")) {
|
|
6071
|
+
return;
|
|
6072
|
+
}
|
|
6073
|
+
clearDeviceAuthToken({
|
|
6074
|
+
stateDir: this.stateDir,
|
|
6075
|
+
deviceId: this.deviceIdentity.deviceId,
|
|
6076
|
+
role: "operator"
|
|
6077
|
+
});
|
|
6078
|
+
this.opts.logger.warn(
|
|
6079
|
+
`TunnelProxy: cleared stale stored device token after gateway mismatch (deviceId=${this.deviceIdentity.deviceId})`
|
|
6080
|
+
);
|
|
6081
|
+
}
|
|
5887
6082
|
buildLocalGatewayAuthAttempts(baseHeaders) {
|
|
5888
6083
|
const auth = this.resolveGatewayConnectAuth();
|
|
5889
6084
|
const attempts = [];
|
|
@@ -5921,19 +6116,21 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
5921
6116
|
}
|
|
5922
6117
|
return attempts;
|
|
5923
6118
|
}
|
|
5924
|
-
async sendHttpResponse(
|
|
6119
|
+
async sendHttpResponse(params) {
|
|
6120
|
+
const { frameId, method, path: path2, authLabel, startedAtMs, res } = params;
|
|
5925
6121
|
const contentType = res.headers.get("content-type") ?? "";
|
|
5926
6122
|
const isStreaming = contentType.includes("text/event-stream");
|
|
6123
|
+
const elapsedMs = Date.now() - startedAtMs;
|
|
5927
6124
|
this.opts.logger.info(
|
|
5928
|
-
`TunnelProxy:
|
|
6125
|
+
`TunnelProxy: HTTP id=${frameId} ${method} ${path2} <= ${res.status} (${elapsedMs}ms, auth=${authLabel}, content-type=${contentType}, streaming=${isStreaming})`
|
|
5929
6126
|
);
|
|
5930
6127
|
if (isStreaming && res.body) {
|
|
5931
|
-
await this.streamResponse(frameId, res);
|
|
6128
|
+
await this.streamResponse(frameId, res, startedAtMs);
|
|
5932
6129
|
return;
|
|
5933
6130
|
}
|
|
5934
6131
|
const body = await res.text();
|
|
5935
6132
|
this.opts.logger.info(
|
|
5936
|
-
`TunnelProxy:
|
|
6133
|
+
`TunnelProxy: HTTP id=${frameId} response body=${previewText2(body)}`
|
|
5937
6134
|
);
|
|
5938
6135
|
const headers = {};
|
|
5939
6136
|
res.headers.forEach((value, key) => {
|
|
@@ -5982,7 +6179,7 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
5982
6179
|
if (this.gatewayWsReady && this.gatewayWs?.readyState === wrapper_default.OPEN) {
|
|
5983
6180
|
this.gatewayWs.send(payload);
|
|
5984
6181
|
} else {
|
|
5985
|
-
this.
|
|
6182
|
+
this.pushGatewayPending(payload, "raw frame queued before gateway WS ready");
|
|
5986
6183
|
}
|
|
5987
6184
|
}
|
|
5988
6185
|
/** 清理所有代理的 WebSocket 连接 */
|
|
@@ -6016,12 +6213,14 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6016
6213
|
this.gatewayWsConnecting = true;
|
|
6017
6214
|
this.gatewayWsReady = false;
|
|
6018
6215
|
const wsUrl = this.opts.gatewayBaseUrl.replace(/^http/, "ws");
|
|
6019
|
-
this.opts.logger.info(
|
|
6216
|
+
this.opts.logger.info(
|
|
6217
|
+
`TunnelProxy: RPC WS connecting to gateway ${wsUrl} (pending=${this.gatewayWsPending.length})`
|
|
6218
|
+
);
|
|
6020
6219
|
const ws = new wrapper_default(wsUrl);
|
|
6021
6220
|
ws.on("open", () => {
|
|
6022
6221
|
this.gatewayWs = ws;
|
|
6023
6222
|
this.opts.logger.info(
|
|
6024
|
-
|
|
6223
|
+
`TunnelProxy: RPC WS tcp connected, waiting for connect.challenge (pending=${this.gatewayWsPending.length})`
|
|
6025
6224
|
);
|
|
6026
6225
|
});
|
|
6027
6226
|
ws.on("message", (data) => {
|
|
@@ -6048,15 +6247,16 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6048
6247
|
}
|
|
6049
6248
|
if (frame.type === "event" && frame.event === "connect.challenge") {
|
|
6050
6249
|
const challengeNonce = frame.payload?.nonce ?? "";
|
|
6051
|
-
|
|
6052
|
-
`TunnelProxy: received connect.challenge (nonce=${challengeNonce}), sending connect request with device identity`
|
|
6053
|
-
);
|
|
6250
|
+
const connectRequestId = `tunnel-connect-${randomUUID2()}`;
|
|
6054
6251
|
const role = "operator";
|
|
6055
6252
|
const scopes = ["operator.admin"];
|
|
6253
|
+
const gatewayConnectAuth = this.buildGatewayConnectAuth(role);
|
|
6254
|
+
this.opts.logger.info(
|
|
6255
|
+
`TunnelProxy: received connect.challenge (nonce=${challengeNonce}, connectReqId=${connectRequestId}, hasToken=${!!gatewayConnectAuth.authToken}, hasPassword=${!!gatewayConnectAuth.authPassword}, hasDeviceToken=${!!gatewayConnectAuth.deviceToken}), sending connect request with device identity`
|
|
6256
|
+
);
|
|
6056
6257
|
const signedAtMs = Date.now();
|
|
6057
6258
|
const clientId = "gateway-client";
|
|
6058
6259
|
const clientMode = "backend";
|
|
6059
|
-
const gatewayAuth = this.resolveGatewayConnectAuth();
|
|
6060
6260
|
const authPayload = buildDeviceAuthPayload({
|
|
6061
6261
|
deviceId: this.deviceIdentity.deviceId,
|
|
6062
6262
|
clientId,
|
|
@@ -6064,7 +6264,7 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6064
6264
|
role,
|
|
6065
6265
|
scopes,
|
|
6066
6266
|
signedAtMs,
|
|
6067
|
-
token:
|
|
6267
|
+
token: gatewayConnectAuth.authToken ?? null,
|
|
6068
6268
|
nonce: challengeNonce
|
|
6069
6269
|
});
|
|
6070
6270
|
const signature = signDevicePayload(
|
|
@@ -6073,7 +6273,7 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6073
6273
|
);
|
|
6074
6274
|
const connectReq = {
|
|
6075
6275
|
type: "req",
|
|
6076
|
-
id:
|
|
6276
|
+
id: connectRequestId,
|
|
6077
6277
|
method: "connect",
|
|
6078
6278
|
params: {
|
|
6079
6279
|
minProtocol: 3,
|
|
@@ -6086,7 +6286,7 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6086
6286
|
},
|
|
6087
6287
|
role,
|
|
6088
6288
|
scopes,
|
|
6089
|
-
...
|
|
6289
|
+
...gatewayConnectAuth.auth ? { auth: gatewayConnectAuth.auth } : {},
|
|
6090
6290
|
device: {
|
|
6091
6291
|
id: this.deviceIdentity.deviceId,
|
|
6092
6292
|
publicKey: publicKeyRawBase64UrlFromPem(
|
|
@@ -6102,6 +6302,11 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6102
6302
|
return;
|
|
6103
6303
|
}
|
|
6104
6304
|
if (frame.type === "res" && frame.ok === true && frame.payload?.type === "hello-ok") {
|
|
6305
|
+
this.storeIssuedDeviceToken({
|
|
6306
|
+
fallbackRole: "operator",
|
|
6307
|
+
fallbackScopes: ["operator.admin"],
|
|
6308
|
+
authInfo: frame.payload?.auth
|
|
6309
|
+
});
|
|
6105
6310
|
this.gatewayWsReady = true;
|
|
6106
6311
|
this.gatewayWsConnecting = false;
|
|
6107
6312
|
this.opts.logger.info(
|
|
@@ -6115,7 +6320,7 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6115
6320
|
}
|
|
6116
6321
|
if (frame.type === "res" && frame.ok === false && !this.gatewayWsReady) {
|
|
6117
6322
|
this.opts.logger.error(
|
|
6118
|
-
`TunnelProxy: RPC WS handshake failed: ${JSON.stringify(frame.error)}`
|
|
6323
|
+
`TunnelProxy: RPC WS handshake failed (pending=${this.gatewayWsPending.length}): ${previewText2(JSON.stringify(frame.error), 500)}`
|
|
6119
6324
|
);
|
|
6120
6325
|
ws.close();
|
|
6121
6326
|
return;
|
|
@@ -6123,17 +6328,23 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6123
6328
|
this.opts.client.sendRaw(text);
|
|
6124
6329
|
});
|
|
6125
6330
|
ws.on("close", (code, reason) => {
|
|
6331
|
+
const wasReady = this.gatewayWsReady;
|
|
6332
|
+
const pendingCount = this.gatewayWsPending.length;
|
|
6333
|
+
const reasonText = reason.toString();
|
|
6126
6334
|
this.opts.logger.info(
|
|
6127
|
-
`TunnelProxy: RPC WS closed by gateway (code=${code}, reason=${
|
|
6335
|
+
`TunnelProxy: RPC WS closed by gateway (code=${code}, reason=${reasonText}, ready=${wasReady}, pending=${pendingCount}, activeWs=${this.wsConnections.size})`
|
|
6128
6336
|
);
|
|
6129
6337
|
if (this.gatewayWs === ws) {
|
|
6130
6338
|
this.gatewayWs = null;
|
|
6131
6339
|
this.gatewayWsReady = false;
|
|
6132
6340
|
}
|
|
6133
6341
|
this.gatewayWsConnecting = false;
|
|
6342
|
+
this.maybeClearStoredDeviceTokenOnMismatch(code, reasonText);
|
|
6134
6343
|
});
|
|
6135
6344
|
ws.on("error", (err) => {
|
|
6136
|
-
this.opts.logger.warn(
|
|
6345
|
+
this.opts.logger.warn(
|
|
6346
|
+
`TunnelProxy: RPC WS error: ${err.message} (ready=${this.gatewayWsReady}, pending=${this.gatewayWsPending.length}, activeWs=${this.wsConnections.size})`
|
|
6347
|
+
);
|
|
6137
6348
|
this.gatewayWsConnecting = false;
|
|
6138
6349
|
if (this.gatewayWs === ws) {
|
|
6139
6350
|
this.gatewayWs = null;
|
|
@@ -6154,7 +6365,10 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6154
6365
|
this.opts.logger.info(
|
|
6155
6366
|
`TunnelProxy: req id=${frame.id} queued, gateway WS not ready yet`
|
|
6156
6367
|
);
|
|
6157
|
-
this.
|
|
6368
|
+
this.pushGatewayPending(
|
|
6369
|
+
payload,
|
|
6370
|
+
`req id=${frame.id} queued before gateway WS handshake`
|
|
6371
|
+
);
|
|
6158
6372
|
}
|
|
6159
6373
|
}
|
|
6160
6374
|
// ─── 路径映射 ───
|
|
@@ -6166,7 +6380,9 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6166
6380
|
}
|
|
6167
6381
|
// ─── HTTP 请求代理 ───
|
|
6168
6382
|
async handleHttpRequest(frame) {
|
|
6169
|
-
const
|
|
6383
|
+
const mappedPath = this.mapPath(frame.path);
|
|
6384
|
+
const url = new URL(mappedPath, this.opts.gatewayBaseUrl);
|
|
6385
|
+
const startedAtMs = Date.now();
|
|
6170
6386
|
const localHeaders = {};
|
|
6171
6387
|
for (const [k, v] of Object.entries(frame.headers ?? {})) {
|
|
6172
6388
|
const lower = k.toLowerCase();
|
|
@@ -6176,11 +6392,14 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6176
6392
|
}
|
|
6177
6393
|
const authAttempts = this.buildLocalGatewayAuthAttempts(localHeaders);
|
|
6178
6394
|
this.opts.logger.info(
|
|
6179
|
-
`TunnelProxy: HTTP ${frame.method} ${frame.path} \u2192 ${url.toString()}, body=${frame.body
|
|
6395
|
+
`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
6396
|
);
|
|
6181
6397
|
try {
|
|
6182
6398
|
for (let attemptIndex = 0; attemptIndex < authAttempts.length; attemptIndex++) {
|
|
6183
6399
|
const attempt = authAttempts[attemptIndex];
|
|
6400
|
+
this.opts.logger.info(
|
|
6401
|
+
`TunnelProxy: HTTP id=${frame.id} attempt ${attemptIndex + 1}/${authAttempts.length} auth=${attempt.label}`
|
|
6402
|
+
);
|
|
6184
6403
|
const res = await fetch(url.toString(), {
|
|
6185
6404
|
method: frame.method,
|
|
6186
6405
|
headers: attempt.headers,
|
|
@@ -6190,23 +6409,34 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6190
6409
|
if (res.status === 401 && hasFallback) {
|
|
6191
6410
|
const body = await res.text();
|
|
6192
6411
|
this.opts.logger.warn(
|
|
6193
|
-
`TunnelProxy: local gateway auth via ${attempt.label} returned 401, retrying next credential${body ? `, body=${body
|
|
6412
|
+
`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
6413
|
);
|
|
6195
6414
|
continue;
|
|
6196
6415
|
}
|
|
6197
|
-
await this.sendHttpResponse(
|
|
6416
|
+
await this.sendHttpResponse({
|
|
6417
|
+
frameId: frame.id,
|
|
6418
|
+
method: frame.method,
|
|
6419
|
+
path: mappedPath,
|
|
6420
|
+
authLabel: attempt.label,
|
|
6421
|
+
startedAtMs,
|
|
6422
|
+
res
|
|
6423
|
+
});
|
|
6198
6424
|
return;
|
|
6199
6425
|
}
|
|
6200
6426
|
} catch (err) {
|
|
6427
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6428
|
+
this.opts.logger.error(
|
|
6429
|
+
`TunnelProxy: HTTP id=${frame.id} ${frame.method} ${mappedPath} failed after ${Date.now() - startedAtMs}ms: ${message}`
|
|
6430
|
+
);
|
|
6201
6431
|
this.opts.client.send({
|
|
6202
6432
|
type: "proxy_error",
|
|
6203
6433
|
id: frame.id,
|
|
6204
6434
|
status: 502,
|
|
6205
|
-
message: `gateway unreachable: ${
|
|
6435
|
+
message: `gateway unreachable: ${message}`
|
|
6206
6436
|
});
|
|
6207
6437
|
}
|
|
6208
6438
|
}
|
|
6209
|
-
async streamResponse(requestId, res) {
|
|
6439
|
+
async streamResponse(requestId, res, startedAtMs) {
|
|
6210
6440
|
const reader = res.body.getReader();
|
|
6211
6441
|
const decoder = new TextDecoder();
|
|
6212
6442
|
let chunkCount = 0;
|
|
@@ -6228,7 +6458,7 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6228
6458
|
});
|
|
6229
6459
|
}
|
|
6230
6460
|
this.opts.logger.info(
|
|
6231
|
-
`TunnelProxy: stream end id=${requestId}, total chunks=${chunkCount}`
|
|
6461
|
+
`TunnelProxy: stream end id=${requestId}, total chunks=${chunkCount}, totalElapsedMs=${Date.now() - startedAtMs}`
|
|
6232
6462
|
);
|
|
6233
6463
|
this.opts.client.send({
|
|
6234
6464
|
type: "stream",
|
|
@@ -6238,7 +6468,7 @@ var TunnelProxy = class _TunnelProxy {
|
|
|
6238
6468
|
});
|
|
6239
6469
|
} catch (err) {
|
|
6240
6470
|
this.opts.logger.error(
|
|
6241
|
-
`TunnelProxy: stream error id=${requestId} after ${chunkCount} chunks: ${err instanceof Error ? err.message : String(err)}`
|
|
6471
|
+
`TunnelProxy: stream error id=${requestId} after ${chunkCount} chunks and ${Date.now() - startedAtMs}ms: ${err instanceof Error ? err.message : String(err)}`
|
|
6242
6472
|
);
|
|
6243
6473
|
this.opts.client.send({
|
|
6244
6474
|
type: "proxy_error",
|
|
@@ -6473,6 +6703,7 @@ function createTunnelService(opts) {
|
|
|
6473
6703
|
logger
|
|
6474
6704
|
});
|
|
6475
6705
|
proxy = new TunnelProxy({
|
|
6706
|
+
stateDir: baseStateDir,
|
|
6476
6707
|
gatewayBaseUrl: opts.gatewayBaseUrl,
|
|
6477
6708
|
gatewayAuthMode: opts.gatewayAuthMode,
|
|
6478
6709
|
gatewayToken: opts.gatewayToken,
|