@dsh-remote/relay 3.1.7 → 3.1.9
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/README.md +15 -13
- package/dist/bundle/main.js +182 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,19 +18,21 @@ curl -s http://127.0.0.1:8787/healthz
|
|
|
18
18
|
|
|
19
19
|
### 常用配置
|
|
20
20
|
|
|
21
|
-
| 变量 | 必填 | 默认 | 说明
|
|
22
|
-
| ----------------------------- | ---- | ----------- |
|
|
23
|
-
| `DRC_HOST_TOKEN` | ✅ | — | 主机出站认证凭据。`openssl rand -hex 32` 生成。**与主机插件那份必须逐字一致**
|
|
24
|
-
| `DRC_PORT` | | `8787` | 监听端口。`0` = 让系统分配(测试与容器用)
|
|
25
|
-
| `DRC_BIND` | | `127.0.0.1` | 绑定地址。默认只绑回环:TLS 由反代终止。**只有自己就是边缘(容器直接发布端口)时才设 `0.0.0.0`**
|
|
26
|
-
| `DRC_PUBLIC_URL` | | 空 | `/api/info` 返回的对外地址。公网填 `wss://你的域名`
|
|
27
|
-
| `DRC_LOG_LEVEL` | | `info` | `debug` / `info` / `warn` / `error` / `silent`。**区分大小写**
|
|
28
|
-
| `DRC_PAIR_TTL_MS` | | `120000` | 配对码的**服务端权威**寿命(毫秒)
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
21
|
+
| 变量 | 必填 | 默认 | 说明 |
|
|
22
|
+
| ----------------------------- | ---- | ----------- | -------------------------------------------------------------------------------------------------------- |
|
|
23
|
+
| `DRC_HOST_TOKEN` | ✅ | — | 主机出站认证凭据。`openssl rand -hex 32` 生成。**与主机插件那份必须逐字一致** |
|
|
24
|
+
| `DRC_PORT` | | `8787` | 监听端口。`0` = 让系统分配(测试与容器用) |
|
|
25
|
+
| `DRC_BIND` | | `127.0.0.1` | 绑定地址。默认只绑回环:TLS 由反代终止。**只有自己就是边缘(容器直接发布端口)时才设 `0.0.0.0`** |
|
|
26
|
+
| `DRC_PUBLIC_URL` | | 空 | `/api/info` 返回的对外地址。公网填 `wss://你的域名` |
|
|
27
|
+
| `DRC_LOG_LEVEL` | | `info` | `debug` / `info` / `warn` / `error` / `silent`。**区分大小写** |
|
|
28
|
+
| `DRC_PAIR_TTL_MS` | | `120000` | 配对码的**服务端权威**寿命(毫秒) |
|
|
29
|
+
| `DRC_MP_APPID` | | — | 小程序 appid。与 `DRC_MP_SECRET` 同时配置才启用 `GET /code`(生成小程序码);不配则插件回退旧 `dshr:` 码 |
|
|
30
|
+
| `DRC_MP_SECRET` | | — | 小程序 secret。**只放在中继**,永不进插件包;换了 secret 记得同步这里 |
|
|
31
|
+
| `DRC_STATE_FILE` | | 空(关闭) | 会话表落盘路径。默认关闭 = 纯内存;配上后启动先读它、会话表有增删时写回 |
|
|
32
|
+
| `DRC_STATE_SAVE_MS` | | `60000` | 周期补写状态文件的间隔(只为刷新 `lastActivityAt`) |
|
|
33
|
+
| `DRC_MAX_BUFFERED_BYTES` | | `1048576` | 慢消费者阈值:发送缓冲区**连续**超限达**本角色窗口**才 1008 断开 |
|
|
34
|
+
| `DRC_SLOW_CONSUMER_HOST_MS` | | `10000` | **主机**侧慢消费者窗口(毫秒)。主机卡住就是所有人卡住,所以窗口短 |
|
|
35
|
+
| `DRC_SLOW_CONSUMER_CLIENT_MS` | | `45000` | **客户端**侧慢消费者窗口(毫秒)。**必须大于手机自己的重连周期**(≈42.5 秒) |
|
|
34
36
|
|
|
35
37
|
### 端点
|
|
36
38
|
|
package/dist/bundle/main.js
CHANGED
|
@@ -23443,7 +23443,7 @@ function date4(params) {
|
|
|
23443
23443
|
return _coercedDate(ZodDate, params);
|
|
23444
23444
|
}
|
|
23445
23445
|
|
|
23446
|
-
// node_modules/.pnpm/@dsh-remote+protocol@3.1.
|
|
23446
|
+
// node_modules/.pnpm/@dsh-remote+protocol@3.1.9/node_modules/@dsh-remote/protocol/dist/src/wire/limits.js
|
|
23447
23447
|
var MAX_RELAY_MESSAGE_BYTES = 1024 * 1024;
|
|
23448
23448
|
var MAX_CIPHERTEXT_BYTES = MAX_RELAY_MESSAGE_BYTES - 8 * 1024;
|
|
23449
23449
|
var MAX_IMAGE_ATTACHMENTS = 4;
|
|
@@ -23452,7 +23452,7 @@ var MAX_ATTACHMENT_BYTES = 512 * 1024;
|
|
|
23452
23452
|
var MAX_ATTACHMENT_TOTAL_BYTES = 512 * 1024;
|
|
23453
23453
|
var IDEMPOTENCY_WINDOW_MS = 5 * 60 * 1e3;
|
|
23454
23454
|
|
|
23455
|
-
// node_modules/.pnpm/@dsh-remote+protocol@3.1.
|
|
23455
|
+
// node_modules/.pnpm/@dsh-remote+protocol@3.1.9/node_modules/@dsh-remote/protocol/dist/src/wire/frames.js
|
|
23456
23456
|
var PROTOCOL_VERSION = 1;
|
|
23457
23457
|
var PAIRING_TOKEN_RE = /^\d{6}$/;
|
|
23458
23458
|
var nonEmpty = external_exports.string().min(1);
|
|
@@ -23481,13 +23481,40 @@ var helloFrame = external_exports.object({
|
|
|
23481
23481
|
clientId: external_exports.string().min(1).max(128).optional(),
|
|
23482
23482
|
clientMeta: clientMeta.optional()
|
|
23483
23483
|
});
|
|
23484
|
+
var publicKeyB64 = external_exports.string().regex(/^[A-Za-z0-9+/]{43}=$/, "\u5FC5\u987B\u662F 32 \u5B57\u8282 base64 \u516C\u94A5");
|
|
23484
23485
|
var pairBeginFrame = external_exports.object({
|
|
23485
23486
|
t: external_exports.literal("pair-begin"),
|
|
23486
23487
|
pairingToken
|
|
23487
23488
|
});
|
|
23488
23489
|
var pairClaimFrame = external_exports.object({
|
|
23489
23490
|
t: external_exports.literal("pair-begin-client"),
|
|
23490
|
-
pairingToken
|
|
23491
|
+
pairingToken,
|
|
23492
|
+
clientPub: publicKeyB64.optional()
|
|
23493
|
+
});
|
|
23494
|
+
var pairRequestFrame = external_exports.object({
|
|
23495
|
+
t: external_exports.literal("pair-request"),
|
|
23496
|
+
pairingToken,
|
|
23497
|
+
clientPub: publicKeyB64,
|
|
23498
|
+
/**
|
|
23499
|
+
* 认领这条码时**新开的会话号**。中继在 claim 的同一时刻就知道它,一并带来:
|
|
23500
|
+
* 主机回 `pair-grant` 必须带会话号(见 pairGrantFrame),否则中继无处可送。
|
|
23501
|
+
*/
|
|
23502
|
+
sessionId: nonEmpty,
|
|
23503
|
+
hostId: nonEmpty.optional()
|
|
23504
|
+
});
|
|
23505
|
+
var pairGrantFrame = external_exports.object({
|
|
23506
|
+
t: external_exports.literal("pair-grant"),
|
|
23507
|
+
/**
|
|
23508
|
+
* **带 sessionId 而不是 pairingToken**(2026-10-21 拍板)。
|
|
23509
|
+
*
|
|
23510
|
+
* 中继在 `claim()` 之后就没有 token→会话 的映射了(pendingPairs 当场消费),
|
|
23511
|
+
* 若按 token 转发就得再养一张 60s 的短命表。而主机在 `peer-joined` 里**已经拿到
|
|
23512
|
+
* sessionId**,让它把会话号原样带回来,中继按会话成员转发即可,零新状态。
|
|
23513
|
+
*/
|
|
23514
|
+
sessionId: nonEmpty,
|
|
23515
|
+
hostPub: publicKeyB64,
|
|
23516
|
+
nonce: nonEmpty,
|
|
23517
|
+
wrappedPsk: nonEmpty
|
|
23491
23518
|
});
|
|
23492
23519
|
var encFrame = external_exports.object({
|
|
23493
23520
|
t: external_exports.literal("enc"),
|
|
@@ -23525,6 +23552,8 @@ var endpointFrame = external_exports.discriminatedUnion("t", [
|
|
|
23525
23552
|
helloFrame,
|
|
23526
23553
|
pairBeginFrame,
|
|
23527
23554
|
pairClaimFrame,
|
|
23555
|
+
// 主机发出去的:psk 密文(中继原样转交给手机)
|
|
23556
|
+
pairGrantFrame,
|
|
23528
23557
|
resyncFrame,
|
|
23529
23558
|
encFrame,
|
|
23530
23559
|
encBatchFrame,
|
|
@@ -23678,6 +23707,9 @@ var pongFrame = external_exports.object({ t: external_exports.literal("pong"), t
|
|
|
23678
23707
|
var relayFrame = external_exports.discriminatedUnion("t", [
|
|
23679
23708
|
helloOkFrame,
|
|
23680
23709
|
pairReadyFrame,
|
|
23710
|
+
// 中继发出的两条:认领请求带给主机、psk 密文带给手机
|
|
23711
|
+
pairRequestFrame,
|
|
23712
|
+
pairGrantFrame,
|
|
23681
23713
|
pairedFrame,
|
|
23682
23714
|
pairFailFrame,
|
|
23683
23715
|
peerJoinedFrame,
|
|
@@ -23844,6 +23876,8 @@ function loadConfig(env, version2) {
|
|
|
23844
23876
|
}
|
|
23845
23877
|
const config2 = {
|
|
23846
23878
|
hostToken,
|
|
23879
|
+
mpAppId: (env.DRC_MP_APPID ?? "").trim(),
|
|
23880
|
+
mpSecret: (env.DRC_MP_SECRET ?? "").trim(),
|
|
23847
23881
|
port: portNumber(env.DRC_PORT, 8787, "DRC_PORT", problems),
|
|
23848
23882
|
// 默认只绑回环:TLS 由反代终止,中继没理由出现在局域网里。
|
|
23849
23883
|
// 只有自己就是边缘(容器直接发布端口)时才设 DRC_BIND=0.0.0.0。
|
|
@@ -23930,7 +23964,7 @@ var import_subprotocol = __toESM(require_subprotocol(), 1);
|
|
|
23930
23964
|
var import_websocket = __toESM(require_websocket(), 1);
|
|
23931
23965
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
23932
23966
|
|
|
23933
|
-
// node_modules/.pnpm/@dsh-remote+protocol@3.1.
|
|
23967
|
+
// node_modules/.pnpm/@dsh-remote+protocol@3.1.9/node_modules/@dsh-remote/protocol/dist/src/identity/ids.js
|
|
23934
23968
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
23935
23969
|
var CONVERSATION_ID_PREFIX = "c_";
|
|
23936
23970
|
function newConversationId() {
|
|
@@ -23940,7 +23974,7 @@ function newHostId() {
|
|
|
23940
23974
|
return randomBytes(4).toString("hex");
|
|
23941
23975
|
}
|
|
23942
23976
|
|
|
23943
|
-
// node_modules/.pnpm/@dsh-remote+protocol@3.1.
|
|
23977
|
+
// node_modules/.pnpm/@dsh-remote+protocol@3.1.9/node_modules/@dsh-remote/protocol/dist/src/wire/negotiate.js
|
|
23944
23978
|
function negotiateProtocol(peerProtocol, own2 = PROTOCOL_VERSION) {
|
|
23945
23979
|
if (peerProtocol === void 0 || peerProtocol === null) {
|
|
23946
23980
|
return {
|
|
@@ -23976,16 +24010,84 @@ function negotiateProtocol(peerProtocol, own2 = PROTOCOL_VERSION) {
|
|
|
23976
24010
|
return { ok: true, version: peerProtocol };
|
|
23977
24011
|
}
|
|
23978
24012
|
|
|
23979
|
-
//
|
|
24013
|
+
// src/mpcode.ts
|
|
24014
|
+
var TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
|
|
24015
|
+
var CODE_URL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit";
|
|
24016
|
+
var LANDING_PAGE = "pages/sessions/sessions";
|
|
24017
|
+
var TOKEN_SKEW_MS = 6e4;
|
|
24018
|
+
var DEFAULT_TOKEN_TTL_MS = 7e6;
|
|
24019
|
+
function createMpCodeIssuer(options) {
|
|
24020
|
+
const appId = options.appId.trim();
|
|
24021
|
+
const secret = options.secret.trim();
|
|
24022
|
+
const doFetch = options.fetch ?? ((...args) => fetch(...args));
|
|
24023
|
+
const now = options.now ?? (() => Date.now());
|
|
24024
|
+
let cachedToken;
|
|
24025
|
+
const images = /* @__PURE__ */ new Map();
|
|
24026
|
+
const IMAGE_TTL_MS = 36e5;
|
|
24027
|
+
async function fetchToken() {
|
|
24028
|
+
const url2 = `${TOKEN_URL}?grant_type=client_credential&appid=${encodeURIComponent(appId)}&secret=${encodeURIComponent(secret)}`;
|
|
24029
|
+
const response = await doFetch(url2);
|
|
24030
|
+
const body = await response.json();
|
|
24031
|
+
if (typeof body.access_token !== "string" || body.access_token === "") {
|
|
24032
|
+
throw new Error(`\u5FAE\u4FE1 access_token \u83B7\u53D6\u5931\u8D25\uFF1A${body.errcode ?? "?"} ${body.errmsg ?? ""}`.trim());
|
|
24033
|
+
}
|
|
24034
|
+
const ttl = typeof body.expires_in === "number" && body.expires_in > 0 ? body.expires_in * 1e3 : DEFAULT_TOKEN_TTL_MS;
|
|
24035
|
+
cachedToken = { value: body.access_token, expiresAt: now() + Math.max(0, ttl - TOKEN_SKEW_MS) };
|
|
24036
|
+
return body.access_token;
|
|
24037
|
+
}
|
|
24038
|
+
async function token() {
|
|
24039
|
+
if (cachedToken !== void 0 && cachedToken.expiresAt > now()) return cachedToken.value;
|
|
24040
|
+
return fetchToken();
|
|
24041
|
+
}
|
|
24042
|
+
async function requestCode(accessToken, pairingToken2) {
|
|
24043
|
+
const response = await doFetch(`${CODE_URL}?access_token=${encodeURIComponent(accessToken)}`, {
|
|
24044
|
+
method: "POST",
|
|
24045
|
+
headers: { "content-type": "application/json" },
|
|
24046
|
+
body: JSON.stringify({ scene: pairingToken2, page: LANDING_PAGE })
|
|
24047
|
+
});
|
|
24048
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
24049
|
+
if (bytes.length > 2 && bytes[0] === 255 && bytes[1] === 216) return bytes;
|
|
24050
|
+
try {
|
|
24051
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
24052
|
+
return { errcode: typeof parsed.errcode === "number" ? parsed.errcode : -1 };
|
|
24053
|
+
} catch {
|
|
24054
|
+
return { errcode: -1 };
|
|
24055
|
+
}
|
|
24056
|
+
}
|
|
24057
|
+
return {
|
|
24058
|
+
enabled: appId !== "" && secret !== "",
|
|
24059
|
+
async codeFor(pairingToken2) {
|
|
24060
|
+
if (appId === "" || secret === "") throw new Error("\u672A\u914D\u7F6E DRC_MP_APPID / DRC_MP_SECRET");
|
|
24061
|
+
const hit = images.get(pairingToken2);
|
|
24062
|
+
if (hit !== void 0 && hit.expiresAt > now()) return hit.bytes;
|
|
24063
|
+
let accessToken = await token();
|
|
24064
|
+
let result = await requestCode(accessToken, pairingToken2);
|
|
24065
|
+
if (!(result instanceof Uint8Array) && (result.errcode === 40001 || result.errcode === 42001)) {
|
|
24066
|
+
accessToken = await fetchToken();
|
|
24067
|
+
result = await requestCode(accessToken, pairingToken2);
|
|
24068
|
+
}
|
|
24069
|
+
if (!(result instanceof Uint8Array)) {
|
|
24070
|
+
throw new Error(`\u5C0F\u7A0B\u5E8F\u7801\u751F\u6210\u5931\u8D25\uFF1Aerrcode=${result.errcode}`);
|
|
24071
|
+
}
|
|
24072
|
+
images.set(pairingToken2, { bytes: result, expiresAt: now() + IMAGE_TTL_MS });
|
|
24073
|
+
if (images.size > 512) {
|
|
24074
|
+
for (const [key, value] of images) if (value.expiresAt <= now()) images.delete(key);
|
|
24075
|
+
}
|
|
24076
|
+
return result;
|
|
24077
|
+
}
|
|
24078
|
+
};
|
|
24079
|
+
}
|
|
24080
|
+
|
|
24081
|
+
// node_modules/.pnpm/@dsh-remote+protocol@3.1.9/node_modules/@dsh-remote/protocol/dist/src/wire/errors.js
|
|
23980
24082
|
var WAIT_HINTABLE = /* @__PURE__ */ new Set(["rate_limited", "pair_table_full"]);
|
|
23981
24083
|
function wantsRetryAfter(code) {
|
|
23982
24084
|
return typeof code === "string" && WAIT_HINTABLE.has(code);
|
|
23983
24085
|
}
|
|
23984
24086
|
|
|
23985
|
-
// node_modules/.pnpm/@dsh-remote+protocol@3.1.
|
|
24087
|
+
// node_modules/.pnpm/@dsh-remote+protocol@3.1.9/node_modules/@dsh-remote/protocol/dist/src/platform/sleep.js
|
|
23986
24088
|
var SLEEP_BACKENDS = ["caffeinate", "systemd-inhibit", "unsupported"];
|
|
23987
24089
|
|
|
23988
|
-
// node_modules/.pnpm/@dsh-remote+protocol@3.1.
|
|
24090
|
+
// node_modules/.pnpm/@dsh-remote+protocol@3.1.9/node_modules/@dsh-remote/protocol/dist/src/wire/payloads.js
|
|
23989
24091
|
var SESSION_STATES = [
|
|
23990
24092
|
"idle",
|
|
23991
24093
|
"running",
|
|
@@ -24411,7 +24513,7 @@ var PAYLOAD_TYPES = {
|
|
|
24411
24513
|
evSessionHistory: "ev.session_history"
|
|
24412
24514
|
};
|
|
24413
24515
|
|
|
24414
|
-
// node_modules/.pnpm/@dsh-remote+protocol@3.1.
|
|
24516
|
+
// node_modules/.pnpm/@dsh-remote+protocol@3.1.9/node_modules/@dsh-remote/protocol/dist/src/wire/outbound.js
|
|
24415
24517
|
function helloOkForClient(clientId, protocol) {
|
|
24416
24518
|
return { t: "hello-ok", role: "client", clientId, ...protocol === void 0 ? {} : { protocol } };
|
|
24417
24519
|
}
|
|
@@ -24427,6 +24529,15 @@ function pairReady(pairingToken2, ttlMs) {
|
|
|
24427
24529
|
function paired(sessionId, hostId) {
|
|
24428
24530
|
return { t: "paired", sessionId, hostId };
|
|
24429
24531
|
}
|
|
24532
|
+
function pairRequest(pairingToken2, clientPub, sessionId, hostId) {
|
|
24533
|
+
return {
|
|
24534
|
+
t: "pair-request",
|
|
24535
|
+
pairingToken: pairingToken2,
|
|
24536
|
+
clientPub,
|
|
24537
|
+
sessionId,
|
|
24538
|
+
...hostId === void 0 ? {} : { hostId }
|
|
24539
|
+
};
|
|
24540
|
+
}
|
|
24430
24541
|
function pairFail(reason, pairingToken2) {
|
|
24431
24542
|
return { t: "pair-fail", reason, ...pairingToken2 === void 0 ? {} : { pairingToken: pairingToken2 } };
|
|
24432
24543
|
}
|
|
@@ -24489,11 +24600,12 @@ function encBatchToRelay(sessionId, items) {
|
|
|
24489
24600
|
return { t: "enc-batch", sessionId, items: items.map((item) => ({ ...item })) };
|
|
24490
24601
|
}
|
|
24491
24602
|
|
|
24492
|
-
// node_modules/.pnpm/@dsh-remote+protocol@3.1.
|
|
24603
|
+
// node_modules/.pnpm/@dsh-remote+protocol@3.1.9/node_modules/@dsh-remote/protocol/dist/src/wire/registry.js
|
|
24493
24604
|
var ENDPOINT_FRAME_NAMES = [
|
|
24494
24605
|
"hello",
|
|
24495
24606
|
"pair-begin",
|
|
24496
24607
|
"pair-begin-client",
|
|
24608
|
+
"pair-grant",
|
|
24497
24609
|
"resync",
|
|
24498
24610
|
"enc",
|
|
24499
24611
|
"enc-batch",
|
|
@@ -24503,6 +24615,8 @@ var ENDPOINT_FRAME_NAMES = [
|
|
|
24503
24615
|
var RELAY_FRAME_NAMES = [
|
|
24504
24616
|
"hello-ok",
|
|
24505
24617
|
"pair-ready",
|
|
24618
|
+
"pair-request",
|
|
24619
|
+
"pair-grant",
|
|
24506
24620
|
"paired",
|
|
24507
24621
|
"pair-fail",
|
|
24508
24622
|
"peer-joined",
|
|
@@ -24551,7 +24665,7 @@ var EV_TYPES = [
|
|
|
24551
24665
|
];
|
|
24552
24666
|
var PAYLOAD_TYPE_SET = /* @__PURE__ */ new Set([...CMD_TYPES, ...EV_TYPES]);
|
|
24553
24667
|
|
|
24554
|
-
// node_modules/.pnpm/@dsh-remote+protocol@3.1.
|
|
24668
|
+
// node_modules/.pnpm/@dsh-remote+protocol@3.1.9/node_modules/@dsh-remote/protocol/dist/src/wire/classify.js
|
|
24555
24669
|
function frameTypeOf(value) {
|
|
24556
24670
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
24557
24671
|
return void 0;
|
|
@@ -25616,6 +25730,12 @@ function createRelay(config2) {
|
|
|
25616
25730
|
}
|
|
25617
25731
|
sendToPeer(peer, paired(claimed.conversationId, claimed.hostId));
|
|
25618
25732
|
sendToPeer(peerOfHost(claimed.hostId), peerJoinedForHost(claimed.conversationId, peer.clientId, frame.pairingToken));
|
|
25733
|
+
if (frame.clientPub !== void 0) {
|
|
25734
|
+
const hostPeer = peerOfHost(claimed.hostId);
|
|
25735
|
+
if (hostPeer !== void 0) {
|
|
25736
|
+
sendToPeer(hostPeer, pairRequest(frame.pairingToken, frame.clientPub, claimed.conversationId, claimed.hostId));
|
|
25737
|
+
}
|
|
25738
|
+
}
|
|
25619
25739
|
markStateChanged();
|
|
25620
25740
|
log.info("paired", {
|
|
25621
25741
|
sessionId: claimed.conversationId,
|
|
@@ -25701,6 +25821,29 @@ function createRelay(config2) {
|
|
|
25701
25821
|
return handlePairBegin(peer, frame);
|
|
25702
25822
|
case "pair-begin-client":
|
|
25703
25823
|
return handlePairClaim(peer, frame);
|
|
25824
|
+
/**
|
|
25825
|
+
* 主机 → 中继 → 手机:psk 的密文(2026-10-21)。
|
|
25826
|
+
*
|
|
25827
|
+
* **只有主机能发**;中继按 `sessionId` 找会话成员,把帧原样转过去。
|
|
25828
|
+
* 它不解析 `wrappedPsk`,也没有能解开的密钥 —— 零知识的前提不变。
|
|
25829
|
+
*/
|
|
25830
|
+
case "pair-grant": {
|
|
25831
|
+
if (peer.role !== "host" || !peer.hostId) {
|
|
25832
|
+
sendError(peer, "need_host");
|
|
25833
|
+
return;
|
|
25834
|
+
}
|
|
25835
|
+
const conv = state.conversations.get(frame.sessionId);
|
|
25836
|
+
if (conv === void 0) {
|
|
25837
|
+
log.info("pair-grant dropped, unknown session", { sessionId: frame.sessionId });
|
|
25838
|
+
return;
|
|
25839
|
+
}
|
|
25840
|
+
for (const clientId of conv.clients) {
|
|
25841
|
+
const clientPeer = state.clients.get(clientId);
|
|
25842
|
+
const socket = clientPeer?.ws;
|
|
25843
|
+
if (socket !== void 0) sendTo(socket, frame);
|
|
25844
|
+
}
|
|
25845
|
+
return;
|
|
25846
|
+
}
|
|
25704
25847
|
case "resync": {
|
|
25705
25848
|
if (peer.role !== "host" || !peer.hostId) {
|
|
25706
25849
|
sendError(peer, "need_host");
|
|
@@ -26059,7 +26202,7 @@ function createRelay(config2) {
|
|
|
26059
26202
|
log.info("host token claimed", { ip, hostId: rawHostId, hostTokens: state.hostTokens.size });
|
|
26060
26203
|
res.end(JSON.stringify({ token, hostId: rawHostId, rotatedAt: new Date(now).toISOString() }));
|
|
26061
26204
|
}
|
|
26062
|
-
function httpHandler(req, res) {
|
|
26205
|
+
async function httpHandler(req, res) {
|
|
26063
26206
|
try {
|
|
26064
26207
|
const url2 = new URL(req.url ?? "/", "http://relay.invalid");
|
|
26065
26208
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
@@ -26078,6 +26221,31 @@ function createRelay(config2) {
|
|
|
26078
26221
|
});
|
|
26079
26222
|
return;
|
|
26080
26223
|
}
|
|
26224
|
+
if (url2.pathname === "/code") {
|
|
26225
|
+
if (!mpCodes.enabled) {
|
|
26226
|
+
res.statusCode = 501;
|
|
26227
|
+
res.end(JSON.stringify({ error: "mp_not_configured" }));
|
|
26228
|
+
return;
|
|
26229
|
+
}
|
|
26230
|
+
const token = url2.searchParams.get("token") ?? "";
|
|
26231
|
+
if (!/^\d{6}$/.test(token)) {
|
|
26232
|
+
res.statusCode = 400;
|
|
26233
|
+
res.end(JSON.stringify({ error: "bad_token" }));
|
|
26234
|
+
return;
|
|
26235
|
+
}
|
|
26236
|
+
try {
|
|
26237
|
+
const bytes = await mpCodes.codeFor(token);
|
|
26238
|
+
res.statusCode = 200;
|
|
26239
|
+
res.setHeader("content-type", "image/jpeg");
|
|
26240
|
+
res.setHeader("cache-control", "no-store");
|
|
26241
|
+
res.end(Buffer.from(bytes));
|
|
26242
|
+
} catch (error62) {
|
|
26243
|
+
log.warn("mp code generation failed", { error: String(error62?.message ?? error62).slice(0, 160) });
|
|
26244
|
+
res.statusCode = 502;
|
|
26245
|
+
res.end(JSON.stringify({ error: "mp_code_failed" }));
|
|
26246
|
+
}
|
|
26247
|
+
return;
|
|
26248
|
+
}
|
|
26081
26249
|
if (url2.pathname === "/api/info") {
|
|
26082
26250
|
res.end(JSON.stringify({ publicUrl: config2.publicUrl, protocol: PROTOCOL_VERSION }));
|
|
26083
26251
|
return;
|
|
@@ -26113,6 +26281,7 @@ function createRelay(config2) {
|
|
|
26113
26281
|
}
|
|
26114
26282
|
}
|
|
26115
26283
|
}
|
|
26284
|
+
const mpCodes = createMpCodeIssuer({ appId: config2.mpAppId, secret: config2.mpSecret });
|
|
26116
26285
|
const http = createServer(httpHandler);
|
|
26117
26286
|
http.on("error", (error62) => {
|
|
26118
26287
|
log.warn("http server error", { message: String(error62?.message ?? error62) });
|
|
@@ -26264,7 +26433,7 @@ function registerSignalHandlers(shutdown) {
|
|
|
26264
26433
|
|
|
26265
26434
|
// src/main.ts
|
|
26266
26435
|
function readVersion() {
|
|
26267
|
-
if ("3.1.
|
|
26436
|
+
if ("3.1.9") return "3.1.9";
|
|
26268
26437
|
try {
|
|
26269
26438
|
const pkg = JSON.parse(readFileSync2(new URL("../../package.json", import.meta.url), "utf8"));
|
|
26270
26439
|
if (typeof pkg.version === "string") return pkg.version;
|