dsh-remote-control 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cordis.patch.yml +6 -0
- package/dist/bundle/client.cjs +146 -0
- package/dist/bundle/index.js +667 -264
- package/package.json +13 -6
- package/dist/src/core/clock.d.ts +0 -45
- package/dist/src/core/clock.js +0 -83
- package/dist/src/core/clock.js.map +0 -1
- package/dist/src/core/keys.d.ts +0 -98
- package/dist/src/core/keys.js +0 -176
- package/dist/src/core/keys.js.map +0 -1
- package/dist/src/core/pairing-window.d.ts +0 -95
- package/dist/src/core/pairing-window.js +0 -92
- package/dist/src/core/pairing-window.js.map +0 -1
- package/dist/src/core/runtime.d.ts +0 -104
- package/dist/src/core/runtime.js +0 -491
- package/dist/src/core/runtime.js.map +0 -1
- package/dist/src/core/sleep-policy.d.ts +0 -81
- package/dist/src/core/sleep-policy.js +0 -134
- package/dist/src/core/sleep-policy.js.map +0 -1
- package/dist/src/core/window.d.ts +0 -88
- package/dist/src/core/window.js +0 -185
- package/dist/src/core/window.js.map +0 -1
- package/dist/src/index.d.ts +0 -70
- package/dist/src/index.js +0 -882
- package/dist/src/index.js.map +0 -1
- package/dist/src/platform/carrier-services.d.ts +0 -96
- package/dist/src/platform/carrier-services.js +0 -982
- package/dist/src/platform/carrier-services.js.map +0 -1
- package/dist/src/platform/guard.d.ts +0 -60
- package/dist/src/platform/guard.js +0 -124
- package/dist/src/platform/guard.js.map +0 -1
- package/dist/src/platform/mock-kernel.d.ts +0 -95
- package/dist/src/platform/mock-kernel.js +0 -180
- package/dist/src/platform/mock-kernel.js.map +0 -1
- package/dist/src/platform/qr.d.ts +0 -28
- package/dist/src/platform/qr.js +0 -80
- package/dist/src/platform/qr.js.map +0 -1
- package/dist/src/platform/sleep-posix.d.ts +0 -17
- package/dist/src/platform/sleep-posix.js +0 -91
- package/dist/src/platform/sleep-posix.js.map +0 -1
- package/dist/src/ports/index.d.ts +0 -246
- package/dist/src/ports/index.js +0 -2
- package/dist/src/ports/index.js.map +0 -1
- package/dist/src/shell/config.d.ts +0 -56
- package/dist/src/shell/config.js +0 -212
- package/dist/src/shell/config.js.map +0 -1
- package/dist/src/shell/pairing-text.d.ts +0 -38
- package/dist/src/shell/pairing-text.js +0 -22
- package/dist/src/shell/pairing-text.js.map +0 -1
- package/dist/src/shell/status.d.ts +0 -39
- package/dist/src/shell/status.js +0 -119
- package/dist/src/shell/status.js.map +0 -1
- package/dist/src/transport/relay.d.ts +0 -107
- package/dist/src/transport/relay.js +0 -359
- package/dist/src/transport/relay.js.map +0 -1
- package/dist/tests/carrier-events.test.d.ts +0 -1
- package/dist/tests/carrier-events.test.js +0 -88
- package/dist/tests/carrier-events.test.js.map +0 -1
- package/dist/tests/carrier-services.test.d.ts +0 -1
- package/dist/tests/carrier-services.test.js +0 -562
- package/dist/tests/carrier-services.test.js.map +0 -1
- package/dist/tests/carrier-vocabulary.test.d.ts +0 -1
- package/dist/tests/carrier-vocabulary.test.js +0 -149
- package/dist/tests/carrier-vocabulary.test.js.map +0 -1
- package/dist/tests/clock-probe-timers.test.d.ts +0 -1
- package/dist/tests/clock-probe-timers.test.js +0 -73
- package/dist/tests/clock-probe-timers.test.js.map +0 -1
- package/dist/tests/command-result.test.d.ts +0 -1
- package/dist/tests/command-result.test.js +0 -185
- package/dist/tests/command-result.test.js.map +0 -1
- package/dist/tests/config.test.d.ts +0 -1
- package/dist/tests/config.test.js +0 -223
- package/dist/tests/config.test.js.map +0 -1
- package/dist/tests/guard.test.d.ts +0 -1
- package/dist/tests/guard.test.js +0 -175
- package/dist/tests/guard.test.js.map +0 -1
- package/dist/tests/ids-and-keys.test.d.ts +0 -1
- package/dist/tests/ids-and-keys.test.js +0 -211
- package/dist/tests/ids-and-keys.test.js.map +0 -1
- package/dist/tests/pairing-text.test.d.ts +0 -1
- package/dist/tests/pairing-text.test.js +0 -62
- package/dist/tests/pairing-text.test.js.map +0 -1
- package/dist/tests/pairing-window.test.d.ts +0 -1
- package/dist/tests/pairing-window.test.js +0 -165
- package/dist/tests/pairing-window.test.js.map +0 -1
- package/dist/tests/relay-client.test.d.ts +0 -1
- package/dist/tests/relay-client.test.js +0 -557
- package/dist/tests/relay-client.test.js.map +0 -1
- package/dist/tests/runtime.test.d.ts +0 -1
- package/dist/tests/runtime.test.js +0 -963
- package/dist/tests/runtime.test.js.map +0 -1
- package/dist/tests/sleep.test.d.ts +0 -1
- package/dist/tests/sleep.test.js +0 -264
- package/dist/tests/sleep.test.js.map +0 -1
- package/dist/tests/status.test.d.ts +0 -1
- package/dist/tests/status.test.js +0 -238
- package/dist/tests/status.test.js.map +0 -1
- package/dist/tests/window.test.d.ts +0 -1
- package/dist/tests/window.test.js +0 -210
- package/dist/tests/window.test.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-control",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "DSH Remote Control host plugin: pairs a running DeepSeek Harness with the DRC relay so a WeChat Mini Program can send prompts, watch streamed output, and answer approvals. Payload-level E2E encryption; the relay never sees plaintext.",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "DSH Remote Control host plugin: pairs a running DeepSeek Harness with the DRC relay so a WeChat Mini Program can send prompts, watch streamed output, and answer approvals. Payload-level E2E encryption; the relay never sees plaintext. Also renders the pairing QR into the DSH right sidebar (same-origin read-only route + browser half).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "providcc",
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"e2ee",
|
|
21
21
|
"tweetnacl",
|
|
22
22
|
"x25519",
|
|
23
|
-
"xsalsa20-poly1305"
|
|
23
|
+
"xsalsa20-poly1305",
|
|
24
|
+
"qrcode",
|
|
25
|
+
"sidebar"
|
|
24
26
|
],
|
|
25
27
|
"engines": {
|
|
26
28
|
"node": ">=20"
|
|
@@ -28,16 +30,21 @@
|
|
|
28
30
|
"main": "./dist/bundle/index.js",
|
|
29
31
|
"exports": {
|
|
30
32
|
".": "./dist/bundle/index.js",
|
|
33
|
+
"./client": "./dist/bundle/client.cjs",
|
|
31
34
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
32
35
|
"./package.json": "./package.json"
|
|
33
36
|
},
|
|
34
37
|
"files": [
|
|
35
|
-
"dist",
|
|
38
|
+
"dist/bundle",
|
|
36
39
|
"cordis.patch.yml"
|
|
37
40
|
],
|
|
38
41
|
"dsh": {
|
|
39
42
|
"bundle": {
|
|
40
43
|
"patch": "./cordis.patch.yml"
|
|
44
|
+
},
|
|
45
|
+
"client": {
|
|
46
|
+
"platform": "web",
|
|
47
|
+
"inject": []
|
|
41
48
|
}
|
|
42
49
|
},
|
|
43
50
|
"publishConfig": {
|
|
@@ -61,8 +68,8 @@
|
|
|
61
68
|
},
|
|
62
69
|
"scripts": {
|
|
63
70
|
"clean": "node --input-type=module -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\"",
|
|
64
|
-
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
65
|
-
"build": "tsc -p tsconfig.json && node ../../scripts/bundle-plugin.mjs",
|
|
71
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc -p client/tsconfig.json",
|
|
72
|
+
"build": "tsc -p tsconfig.json && tsc -p client/tsconfig.json && node ../../scripts/bundle-plugin.mjs",
|
|
66
73
|
"test": "pnpm build && node --test \"dist/tests/*.test.js\"",
|
|
67
74
|
"format": "prettier --write .",
|
|
68
75
|
"format:check": "prettier --check ."
|
package/dist/src/core/clock.d.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* clock — 时间与定时器的注入点。
|
|
3
|
-
*
|
|
4
|
-
* 为什么端口里要有时钟:合帧窗口、空闲放锁、重连退避、审批超时这几件事
|
|
5
|
-
* 全是**时间相关的策略**,没有注入点就只能靠真等(旧实现的窗口化与持锁策略
|
|
6
|
-
* 就是这么没被测到的)。测试里换成假时钟,可以把 300 秒的空闲释放压成一次 `advance()`。
|
|
7
|
-
*/
|
|
8
|
-
import type { Clock } from '../ports/index.js';
|
|
9
|
-
export declare const DEFAULT_SYSTEM_CLOCK: Clock;
|
|
10
|
-
/**
|
|
11
|
-
* 让一个定时器**不再拖住进程退出**。
|
|
12
|
-
*
|
|
13
|
-
* 为什么必须要这个东西:`Clock` 端口只承诺 `setTimeout/clearTimeout`,句柄是 `unknown`,
|
|
14
|
-
* 所以"别为了我留着事件循环"这件事在端口上表达不出来。而"载具探测的复查"这类定时器是
|
|
15
|
-
* **顺手看一眼**的取证动作——宿主要是还活着它就照跑,进程要是已经无别的事可做,它就该
|
|
16
|
-
* 立刻退,而不是被最长的那一发钉住(`node e2e/run.mjs` 1.2s 跑完却 60s 才退,就是这么来的;
|
|
17
|
-
* 同理任何 headless/CLI 跑法都会白等一分钟)。
|
|
18
|
-
*
|
|
19
|
-
* 对 `FakeClock` 的数字句柄是**空操作**:那种场景下定时器本来就归测试掌控,
|
|
20
|
-
* 没有"事件循环"可拖。所以这里只认 `unref` 存在与否,不做类型假设。
|
|
21
|
-
*/
|
|
22
|
-
export declare function unrefTimer(handle: unknown): void;
|
|
23
|
-
/**
|
|
24
|
-
* 一次性定时器集合:每个建出来都 **unref**(见上),并统一登记,好让调用方在停机时一次撤掉。
|
|
25
|
-
*
|
|
26
|
-
* 为什么 `unref` 与"集中撤销"要成对:只 unref 不撤销,宿主没退的情况下这些复查仍会在
|
|
27
|
-
* 任务结束之后去写 status.json(写的是过期的取证);只撤销不 unref,宿主空闲时进程又被钉住。
|
|
28
|
-
* 两件事都做,才既有"不拖尾"又有"任务结束就安静"。
|
|
29
|
-
*/
|
|
30
|
-
export declare function createOneShotTimers(clock: Clock): {
|
|
31
|
-
schedule(fn: () => void, delayMs: number): void;
|
|
32
|
-
cancelAll(): void;
|
|
33
|
-
};
|
|
34
|
-
/** 假时钟:`advance(ms)` 按到期时间依次触发定时器,完全确定。 */
|
|
35
|
-
export declare class FakeClock implements Clock {
|
|
36
|
-
private at;
|
|
37
|
-
private sequence;
|
|
38
|
-
private readonly timers;
|
|
39
|
-
now(): number;
|
|
40
|
-
setTimeout(fn: () => void, ms: number): unknown;
|
|
41
|
-
clearTimeout(handle: unknown): void;
|
|
42
|
-
/** 推进时间并触发所有到期定时器(按到期顺序,处理"定时器里又装定时器")。 */
|
|
43
|
-
advance(ms: number): Promise<void>;
|
|
44
|
-
get pending(): number;
|
|
45
|
-
}
|
package/dist/src/core/clock.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_SYSTEM_CLOCK = {
|
|
2
|
-
now: () => Date.now(),
|
|
3
|
-
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
4
|
-
clearTimeout: (handle) => clearTimeout(handle),
|
|
5
|
-
};
|
|
6
|
-
/**
|
|
7
|
-
* 让一个定时器**不再拖住进程退出**。
|
|
8
|
-
*
|
|
9
|
-
* 为什么必须要这个东西:`Clock` 端口只承诺 `setTimeout/clearTimeout`,句柄是 `unknown`,
|
|
10
|
-
* 所以"别为了我留着事件循环"这件事在端口上表达不出来。而"载具探测的复查"这类定时器是
|
|
11
|
-
* **顺手看一眼**的取证动作——宿主要是还活着它就照跑,进程要是已经无别的事可做,它就该
|
|
12
|
-
* 立刻退,而不是被最长的那一发钉住(`node e2e/run.mjs` 1.2s 跑完却 60s 才退,就是这么来的;
|
|
13
|
-
* 同理任何 headless/CLI 跑法都会白等一分钟)。
|
|
14
|
-
*
|
|
15
|
-
* 对 `FakeClock` 的数字句柄是**空操作**:那种场景下定时器本来就归测试掌控,
|
|
16
|
-
* 没有"事件循环"可拖。所以这里只认 `unref` 存在与否,不做类型假设。
|
|
17
|
-
*/
|
|
18
|
-
export function unrefTimer(handle) {
|
|
19
|
-
const maybe = handle;
|
|
20
|
-
if (maybe && typeof maybe.unref === 'function')
|
|
21
|
-
maybe.unref();
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* 一次性定时器集合:每个建出来都 **unref**(见上),并统一登记,好让调用方在停机时一次撤掉。
|
|
25
|
-
*
|
|
26
|
-
* 为什么 `unref` 与"集中撤销"要成对:只 unref 不撤销,宿主没退的情况下这些复查仍会在
|
|
27
|
-
* 任务结束之后去写 status.json(写的是过期的取证);只撤销不 unref,宿主空闲时进程又被钉住。
|
|
28
|
-
* 两件事都做,才既有"不拖尾"又有"任务结束就安静"。
|
|
29
|
-
*/
|
|
30
|
-
export function createOneShotTimers(clock) {
|
|
31
|
-
const handles = [];
|
|
32
|
-
return {
|
|
33
|
-
schedule(fn, delayMs) {
|
|
34
|
-
const handle = clock.setTimeout(fn, delayMs);
|
|
35
|
-
unrefTimer(handle);
|
|
36
|
-
handles.push(handle);
|
|
37
|
-
},
|
|
38
|
-
cancelAll() {
|
|
39
|
-
// splice 让"撤过的不再撤":stop() 可能被 effect 与 onDispose 两条路各走一次。
|
|
40
|
-
for (const handle of handles.splice(0))
|
|
41
|
-
clock.clearTimeout(handle);
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
/** 假时钟:`advance(ms)` 按到期时间依次触发定时器,完全确定。 */
|
|
46
|
-
export class FakeClock {
|
|
47
|
-
at = 1_700_000_000_000;
|
|
48
|
-
sequence = 0;
|
|
49
|
-
timers = new Map();
|
|
50
|
-
now() {
|
|
51
|
-
return this.at;
|
|
52
|
-
}
|
|
53
|
-
setTimeout(fn, ms) {
|
|
54
|
-
const id = ++this.sequence;
|
|
55
|
-
this.timers.set(id, { at: this.at + ms, fn });
|
|
56
|
-
return id;
|
|
57
|
-
}
|
|
58
|
-
clearTimeout(handle) {
|
|
59
|
-
this.timers.delete(handle);
|
|
60
|
-
}
|
|
61
|
-
/** 推进时间并触发所有到期定时器(按到期顺序,处理"定时器里又装定时器")。 */
|
|
62
|
-
async advance(ms) {
|
|
63
|
-
const target = this.at + ms;
|
|
64
|
-
for (;;) {
|
|
65
|
-
const due = [...this.timers.entries()]
|
|
66
|
-
.filter(([, timer]) => timer.at <= target)
|
|
67
|
-
.sort((a, b) => a[1].at - b[1].at)[0];
|
|
68
|
-
if (!due)
|
|
69
|
-
break;
|
|
70
|
-
const [id, timer] = due;
|
|
71
|
-
this.timers.delete(id);
|
|
72
|
-
this.at = Math.max(this.at, timer.at);
|
|
73
|
-
timer.fn();
|
|
74
|
-
await Promise.resolve();
|
|
75
|
-
}
|
|
76
|
-
this.at = target;
|
|
77
|
-
await Promise.resolve();
|
|
78
|
-
}
|
|
79
|
-
get pending() {
|
|
80
|
-
return this.timers.size;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
//# sourceMappingURL=clock.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"clock.js","sourceRoot":"","sources":["../../../src/core/clock.ts"],"names":[],"mappings":"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"}
|
package/dist/src/core/keys.d.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
export interface Conversation {
|
|
2
|
-
id: string;
|
|
3
|
-
clientIds: Set<string>;
|
|
4
|
-
kC2H: Uint8Array;
|
|
5
|
-
kH2C: Uint8Array;
|
|
6
|
-
psk: string;
|
|
7
|
-
/** host→client 方向的本地序号(中继也会编号,本端这一个是给"顺序可核对"用的)。 */
|
|
8
|
-
seqHost: number;
|
|
9
|
-
generation: number;
|
|
10
|
-
createdAt: number;
|
|
11
|
-
/** 最近一次有实际收发的时刻;剪枝只看这个,不看 `createdAt`。 */
|
|
12
|
-
lastActivityAt: number;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* 一本有界的密钥簿。
|
|
16
|
-
*
|
|
17
|
-
* 为什么必须有界(复核 🟡8):D3 之后配对通道跨断连长存,而**中继侧的空闲 TTL 是 7 天**
|
|
18
|
-
* (`apps/server/src/config.ts` 的 `DRC_CONV_IDLE_TTL_MS`)。也就是说"中继先把它忘掉"
|
|
19
|
-
* 这件事几乎不会发生,长期跑的主机会把每一条曾经配对过的通道连同一份 PSK 永远留在内存里,
|
|
20
|
-
* 并且每次广播都还对它们逐个密封一遍(白做功且随天数线性增长)。
|
|
21
|
-
* 剪枝只由 host 侧负责,且必须**同时通知中继与上层**(见 `RelayClient.pruneConversations`),
|
|
22
|
-
* 否则手机会对着一条主机已经不认得的通道说话。
|
|
23
|
-
*/
|
|
24
|
-
export interface PrunePolicy {
|
|
25
|
-
/** 距今超过这么久而没有任何收发的通道可以剪掉。 */
|
|
26
|
-
idleTtlMs: number;
|
|
27
|
-
/** 通道数硬上界:超了就从最不活跃的开始剪(剪枝不看创建时刻,看最后活动)。 */
|
|
28
|
-
maxConversations: number;
|
|
29
|
-
}
|
|
30
|
-
export declare const DEFAULT_PRUNE_POLICY: PrunePolicy;
|
|
31
|
-
export declare class ConversationBook {
|
|
32
|
-
private readonly byId;
|
|
33
|
-
/**
|
|
34
|
-
* 为一条新配对建立密钥。
|
|
35
|
-
*
|
|
36
|
-
* @param psk 必须来自 `PairingSlots.resolveFor(token)`——**不允许**"取最新的一个"。
|
|
37
|
-
*/
|
|
38
|
-
open(args: {
|
|
39
|
-
id: string;
|
|
40
|
-
psk: string;
|
|
41
|
-
generation: number;
|
|
42
|
-
now: number;
|
|
43
|
-
}): Conversation;
|
|
44
|
-
/** 有实际收发就记一笔活跃(剪枝只看这个时刻)。未知通道返回 false,不顺手建条目。 */
|
|
45
|
-
touch(id: string, now: number): boolean;
|
|
46
|
-
get(id: string): Conversation | undefined;
|
|
47
|
-
has(id: string): boolean;
|
|
48
|
-
close(id: string): boolean;
|
|
49
|
-
/** 中继重启(generation 变化)时把旧会话全部丢掉:它们再也解不开了。 */
|
|
50
|
-
closeBefore(generation: number): string[];
|
|
51
|
-
/**
|
|
52
|
-
* 按策略剪枝,返回被剪掉的 id(调用方负责通知中继与上层,见
|
|
53
|
-
* `RelayClient.pruneConversations`)。
|
|
54
|
-
*
|
|
55
|
-
* 两条规则都做,因为它们是两种不同的失控:
|
|
56
|
-
* - 空闲超 TTL:一条通道一天没有任何收发,就不会再有人用它了;
|
|
57
|
-
* - 超出硬上界:从**最不活跃**的开始剪。没有这条,一个每天配一次对的用户
|
|
58
|
-
* 会在两周内攒下 260 条密钥,并且每次广播都对它们逐个密封一遍。
|
|
59
|
-
*/
|
|
60
|
-
pruneStale(now: number, policy?: PrunePolicy): string[];
|
|
61
|
-
ids(): string[];
|
|
62
|
-
get size(): number;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* 待配对码槽位。
|
|
66
|
-
*
|
|
67
|
-
* 为什么必须是**多槽 Map** 而不是单个"当前 PSK"字段:主机可以一边自动补发一张码、
|
|
68
|
-
* 一边又被用户手动 `/drc pair` 出一张新码,两张都有效;旧实现用单槽,
|
|
69
|
-
* 于是手机用较早那张码配对成功、主机却拿最新那张的 PSK 派生密钥 → 全线解不开。
|
|
70
|
-
* 这是线上事故换来的结论(取证 HANDOFF.md §4.5 与 e2e/multi-pair.test.mjs)。
|
|
71
|
-
*/
|
|
72
|
-
export interface PairSlot {
|
|
73
|
-
token: string;
|
|
74
|
-
psk: string;
|
|
75
|
-
/** 本地认为的过期时刻;**收到中继的 `pair-ready.ttlMs` 后必须改写**(§2.3 硬约束 1)。 */
|
|
76
|
-
expiresAt: number;
|
|
77
|
-
createdAt: number;
|
|
78
|
-
}
|
|
79
|
-
export declare class PairingSlots {
|
|
80
|
-
private readonly now;
|
|
81
|
-
private readonly maxSlots;
|
|
82
|
-
private readonly slots;
|
|
83
|
-
constructor(now: () => number, maxSlots?: number);
|
|
84
|
-
/** 新建一张码。超上限时淘汰最旧的(而不是拒绝新建),因为"发不出去"比"少一张"更糟。 */
|
|
85
|
-
create(ttlMs: number): PairSlot;
|
|
86
|
-
/**
|
|
87
|
-
* 用服务端权威 TTL 改写本地过期时间。
|
|
88
|
-
* 忘了这一步的后果:主机在配对发生之前就清掉了 PSK,
|
|
89
|
-
* `peer-joined` 找不到密钥 → 静默丢 peer → 手机永远解不开。
|
|
90
|
-
*/
|
|
91
|
-
applyServerTtl(token: string, ttlMs: number): boolean;
|
|
92
|
-
/** 按手机实际使用的码取 PSK。取不到就是 null,调用方必须拒绝这条配对而不是猜。 */
|
|
93
|
-
resolveFor(token: string | undefined): PairSlot | null;
|
|
94
|
-
/** 最近一张仍有效的码(只用于 `/drc pair` 展示与状态快照,绝不用于取密钥)。 */
|
|
95
|
-
latest(): PairSlot | null;
|
|
96
|
-
prune(): number;
|
|
97
|
-
get size(): number;
|
|
98
|
-
}
|
package/dist/src/core/keys.js
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* keys — 会话密钥簿(配对通道 → 两把方向密钥)。
|
|
3
|
-
*
|
|
4
|
-
* 密钥全部由 `(psk, 方向, convId)` 派生(冻结项 B5),所以这本账的键只能是 convId。
|
|
5
|
-
* **两个方向必须分开存**:手机用 `c2h` 发、用 `h2c` 收,主机正好相反。
|
|
6
|
-
*
|
|
7
|
-
* 一条来自旧实现线上事故的硬教训(取证 docs/legacy-spec/relay-and-wireformat.md §2.3):
|
|
8
|
-
* 主机挂着多张配对码时,**必须按手机实际使用的那张码取 PSK**,
|
|
9
|
-
* 取错的结果是"配对显示成功、每一帧都解不开、会话列表空白"——极难定位。
|
|
10
|
-
* 因此这里不提供"取最近一个"的接口,只有 `resolveFor(token)`。
|
|
11
|
-
*/
|
|
12
|
-
import { generatePsk, randomPairingToken } from 'dsh-remote-wire';
|
|
13
|
-
import { derivePskKey } from 'dsh-remote-wire';
|
|
14
|
-
const C2H = 'c2h';
|
|
15
|
-
const H2C = 'h2c';
|
|
16
|
-
export const DEFAULT_PRUNE_POLICY = {
|
|
17
|
-
// 一天没有任何收发就认为这条通道不会再被用(手机真回来时会重新扫码,
|
|
18
|
-
// 而且它会先撞上 `unknown_session` → 中文提示,而不是"永远转圈")。
|
|
19
|
-
idleTtlMs: 24 * 3600 * 1000,
|
|
20
|
-
maxConversations: 64,
|
|
21
|
-
};
|
|
22
|
-
export class ConversationBook {
|
|
23
|
-
byId = new Map();
|
|
24
|
-
/**
|
|
25
|
-
* 为一条新配对建立密钥。
|
|
26
|
-
*
|
|
27
|
-
* @param psk 必须来自 `PairingSlots.resolveFor(token)`——**不允许**"取最新的一个"。
|
|
28
|
-
*/
|
|
29
|
-
open(args) {
|
|
30
|
-
const conversation = {
|
|
31
|
-
id: args.id,
|
|
32
|
-
clientIds: new Set(),
|
|
33
|
-
kC2H: derivePskKey(args.psk, C2H, args.id),
|
|
34
|
-
kH2C: derivePskKey(args.psk, H2C, args.id),
|
|
35
|
-
psk: args.psk,
|
|
36
|
-
seqHost: 0,
|
|
37
|
-
generation: args.generation,
|
|
38
|
-
createdAt: args.now,
|
|
39
|
-
lastActivityAt: args.now,
|
|
40
|
-
};
|
|
41
|
-
this.byId.set(args.id, conversation);
|
|
42
|
-
return conversation;
|
|
43
|
-
}
|
|
44
|
-
/** 有实际收发就记一笔活跃(剪枝只看这个时刻)。未知通道返回 false,不顺手建条目。 */
|
|
45
|
-
touch(id, now) {
|
|
46
|
-
const conversation = this.byId.get(id);
|
|
47
|
-
if (!conversation)
|
|
48
|
-
return false;
|
|
49
|
-
conversation.lastActivityAt = now;
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
52
|
-
get(id) {
|
|
53
|
-
return this.byId.get(id);
|
|
54
|
-
}
|
|
55
|
-
has(id) {
|
|
56
|
-
return this.byId.has(id);
|
|
57
|
-
}
|
|
58
|
-
close(id) {
|
|
59
|
-
return this.byId.delete(id);
|
|
60
|
-
}
|
|
61
|
-
/** 中继重启(generation 变化)时把旧会话全部丢掉:它们再也解不开了。 */
|
|
62
|
-
closeBefore(generation) {
|
|
63
|
-
const dropped = [];
|
|
64
|
-
for (const [id, conversation] of [...this.byId]) {
|
|
65
|
-
if (conversation.generation < generation) {
|
|
66
|
-
this.byId.delete(id);
|
|
67
|
-
dropped.push(id);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return dropped;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* 按策略剪枝,返回被剪掉的 id(调用方负责通知中继与上层,见
|
|
74
|
-
* `RelayClient.pruneConversations`)。
|
|
75
|
-
*
|
|
76
|
-
* 两条规则都做,因为它们是两种不同的失控:
|
|
77
|
-
* - 空闲超 TTL:一条通道一天没有任何收发,就不会再有人用它了;
|
|
78
|
-
* - 超出硬上界:从**最不活跃**的开始剪。没有这条,一个每天配一次对的用户
|
|
79
|
-
* 会在两周内攒下 260 条密钥,并且每次广播都对它们逐个密封一遍。
|
|
80
|
-
*/
|
|
81
|
-
pruneStale(now, policy = DEFAULT_PRUNE_POLICY) {
|
|
82
|
-
const dropped = [];
|
|
83
|
-
for (const [id, conversation] of [...this.byId]) {
|
|
84
|
-
if (now - conversation.lastActivityAt > policy.idleTtlMs) {
|
|
85
|
-
this.byId.delete(id);
|
|
86
|
-
dropped.push(id);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
if (this.byId.size > policy.maxConversations) {
|
|
90
|
-
const ordered = [...this.byId.values()].sort((a, b) => a.lastActivityAt - b.lastActivityAt);
|
|
91
|
-
for (const conversation of ordered.slice(0, this.byId.size - policy.maxConversations)) {
|
|
92
|
-
this.byId.delete(conversation.id);
|
|
93
|
-
dropped.push(conversation.id);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return dropped;
|
|
97
|
-
}
|
|
98
|
-
ids() {
|
|
99
|
-
return [...this.byId.keys()];
|
|
100
|
-
}
|
|
101
|
-
get size() {
|
|
102
|
-
return this.byId.size;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
export class PairingSlots {
|
|
106
|
-
now;
|
|
107
|
-
maxSlots;
|
|
108
|
-
slots = new Map();
|
|
109
|
-
constructor(now, maxSlots = 32) {
|
|
110
|
-
this.now = now;
|
|
111
|
-
this.maxSlots = maxSlots;
|
|
112
|
-
}
|
|
113
|
-
/** 新建一张码。超上限时淘汰最旧的(而不是拒绝新建),因为"发不出去"比"少一张"更糟。 */
|
|
114
|
-
create(ttlMs) {
|
|
115
|
-
if (this.slots.size >= this.maxSlots) {
|
|
116
|
-
const oldest = [...this.slots.values()].sort((a, b) => a.createdAt - b.createdAt)[0];
|
|
117
|
-
if (oldest)
|
|
118
|
-
this.slots.delete(oldest.token);
|
|
119
|
-
}
|
|
120
|
-
const created = {
|
|
121
|
-
token: randomToken(),
|
|
122
|
-
psk: generatePskValue(),
|
|
123
|
-
expiresAt: this.now() + ttlMs,
|
|
124
|
-
createdAt: this.now(),
|
|
125
|
-
};
|
|
126
|
-
this.slots.set(created.token, created);
|
|
127
|
-
return created;
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* 用服务端权威 TTL 改写本地过期时间。
|
|
131
|
-
* 忘了这一步的后果:主机在配对发生之前就清掉了 PSK,
|
|
132
|
-
* `peer-joined` 找不到密钥 → 静默丢 peer → 手机永远解不开。
|
|
133
|
-
*/
|
|
134
|
-
applyServerTtl(token, ttlMs) {
|
|
135
|
-
const slot = this.slots.get(token);
|
|
136
|
-
if (!slot)
|
|
137
|
-
return false;
|
|
138
|
-
slot.expiresAt = this.now() + ttlMs;
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
/** 按手机实际使用的码取 PSK。取不到就是 null,调用方必须拒绝这条配对而不是猜。 */
|
|
142
|
-
resolveFor(token) {
|
|
143
|
-
if (!token)
|
|
144
|
-
return null;
|
|
145
|
-
const slot = this.slots.get(token);
|
|
146
|
-
if (!slot || slot.expiresAt < this.now())
|
|
147
|
-
return null;
|
|
148
|
-
return slot;
|
|
149
|
-
}
|
|
150
|
-
/** 最近一张仍有效的码(只用于 `/drc pair` 展示与状态快照,绝不用于取密钥)。 */
|
|
151
|
-
latest() {
|
|
152
|
-
const alive = [...this.slots.values()].filter((slot) => slot.expiresAt >= this.now());
|
|
153
|
-
if (alive.length === 0)
|
|
154
|
-
return null;
|
|
155
|
-
return alive.sort((a, b) => b.createdAt - a.createdAt)[0] ?? null;
|
|
156
|
-
}
|
|
157
|
-
prune() {
|
|
158
|
-
const before = this.slots.size;
|
|
159
|
-
for (const [token, slot] of [...this.slots]) {
|
|
160
|
-
if (slot.expiresAt < this.now())
|
|
161
|
-
this.slots.delete(token);
|
|
162
|
-
}
|
|
163
|
-
return before - this.slots.size;
|
|
164
|
-
}
|
|
165
|
-
get size() {
|
|
166
|
-
return this.slots.size;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
/** 6 位配对码:直接用协议层的 CSPRNG 实现,避免在两处各写一遍拒绝采样。 */
|
|
170
|
-
function randomToken() {
|
|
171
|
-
return randomPairingToken();
|
|
172
|
-
}
|
|
173
|
-
function generatePskValue() {
|
|
174
|
-
return generatePsk();
|
|
175
|
-
}
|
|
176
|
-
//# sourceMappingURL=keys.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../../../src/core/keys.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pairing-window — "让屏幕上永远有一张能用的码"这条策略。
|
|
3
|
-
*
|
|
4
|
-
* 为什么要有这一层(而不是在启动时发一张就完事):取证
|
|
5
|
-
* `docs/legacy-spec/host-plugin-runtime.md` §6.2。旧实现把它写成挂在状态快照里的
|
|
6
|
-
* 3 秒循环,并且明确记着**三条 stale 判据各自防的事故**:
|
|
7
|
-
*
|
|
8
|
-
* - `!active` → 防"永远没有码"(一次发布失败之后就再也不发了);
|
|
9
|
-
* - `generationChanged` → 防"中继重启后一直挂一张死码":中继的 pending-pair 表是内存的,
|
|
10
|
-
* 重启即清空,那张码的 TTL 还没到但已经没人认得了;
|
|
11
|
-
* - `剩余寿命 < 半程` → 防"用户扫到一个刚刚过期的码"。半程 + 3s 轮询 ⇒ 一张码在
|
|
12
|
-
* status.json 里的实际存活期约 `ttl/2 ~ ttl`。
|
|
13
|
-
*
|
|
14
|
-
* 新实现再加第四条判据,它是这次重写才有的能力:
|
|
15
|
-
*
|
|
16
|
-
* - `consumed` → 这张码已经被某部手机用掉了(`peer-joined` 带回的就是它的 token)。
|
|
17
|
-
* 旧实现只能等半程刷新,于是"第二部手机要等新码"最坏要等半个 TTL;
|
|
18
|
-
* 而 e2e 连跑多轮时每一轮都要一张**新码**(一次性码不可复用),
|
|
19
|
-
* 没有这条判据的话多轮取证要么很慢、要么只能靠重启宿主。
|
|
20
|
-
*
|
|
21
|
-
* 还有一条不可让步的**发布闸门**:`relay === 'online'` 才发。
|
|
22
|
-
* 因为 `RelayClient.raw()` 对非 OPEN 的 socket 是静默丢弃(`transport/relay.ts`),
|
|
23
|
-
* 断连时"本地记了一张码、远端根本没收到"= 表里多一条永远配不上的 PSK,
|
|
24
|
-
* 而 status.json 里却显示它有效(取证 §6.2 第 5 行)。
|
|
25
|
-
*
|
|
26
|
-
* 本模块是纯逻辑:不认 socket、不认 cordis、时钟与发布动作都由调用方注入,
|
|
27
|
-
* 所以上面四条判据每一条都有单测(旧实现这三条**一条都没测过**)。
|
|
28
|
-
*/
|
|
29
|
-
import type { Clock } from '../ports/index.js';
|
|
30
|
-
/** 当前挂在 status.json 里的那张码。 */
|
|
31
|
-
export interface ActivePair {
|
|
32
|
-
token: string;
|
|
33
|
-
/** 发起 `pair-begin` 的时刻;TTL 的两条基线都以它为准(见 §6.3 末尾那条不一致的记录)。 */
|
|
34
|
-
createdAt: number;
|
|
35
|
-
expiresAt: number;
|
|
36
|
-
/** 发布时中继的代次;代次变了说明中继的 pending 表已经清空,这张码是死的。 */
|
|
37
|
-
generation: number;
|
|
38
|
-
}
|
|
39
|
-
export interface WindowInput {
|
|
40
|
-
active: ActivePair | null;
|
|
41
|
-
now: number;
|
|
42
|
-
/** 中继当前代次。 */
|
|
43
|
-
generation: number;
|
|
44
|
-
/** 每张码的请求寿命(毫秒);服务端 `pair-ready.ttlMs` 落地后以它为准。 */
|
|
45
|
-
ttlMs: number;
|
|
46
|
-
}
|
|
47
|
-
/** 需要重新发布的原因;返回 null 表示这张码仍然可用。 */
|
|
48
|
-
export declare function staleReason(input: WindowInput): string | null;
|
|
49
|
-
export interface WindowDeps {
|
|
50
|
-
clock: Clock;
|
|
51
|
-
/** 中继此刻是否在线。 */
|
|
52
|
-
relayOnline(): boolean;
|
|
53
|
-
/** 中继当前代次。 */
|
|
54
|
-
generation(): number;
|
|
55
|
-
/** 发一张新码,返回它的三个时间量;失败返回 null(保留旧码继续展示)。 */
|
|
56
|
-
publish(ttlMs: number): {
|
|
57
|
-
token: string;
|
|
58
|
-
createdAt: number;
|
|
59
|
-
expiresAt: number;
|
|
60
|
-
} | null;
|
|
61
|
-
/** 每轮最多发布几张:发布失败(socket 未 OPEN)时不许原地打转。 */
|
|
62
|
-
maxPublishPerTick?: number;
|
|
63
|
-
log(message: string, fields?: Record<string, string | number | boolean | undefined>): void;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* 由调用方按节拍驱动(本仓库里是 `StatusFile.start()` 的 3 秒 tick——
|
|
67
|
-
* 旧实现就是挂在这里,理由是"反正每 3 秒就要写一次状态")。
|
|
68
|
-
*
|
|
69
|
-
* 刻意**不自带定时器**:定时器属于宿主,一个纯策略类不该再占一只。
|
|
70
|
-
*/
|
|
71
|
-
export declare class PairingWindow {
|
|
72
|
-
private readonly deps;
|
|
73
|
-
private readonly pairSec;
|
|
74
|
-
active: ActivePair | null;
|
|
75
|
-
private requestedTtlMs;
|
|
76
|
-
constructor(deps: WindowDeps, pairSec: number);
|
|
77
|
-
/** 服务端权威 TTL(`pair-ready.ttlMs`):落地后覆盖请求值,并修正已挂出的那张码。 */
|
|
78
|
-
applyServerTtl(pairingToken: string, ttlMs: number): void;
|
|
79
|
-
/** 这张码已被某部手机用掉(`peer-joined` 带回的 token 就是它)。 */
|
|
80
|
-
markConsumed(pairingToken: string): void;
|
|
81
|
-
/**
|
|
82
|
-
* 认领一张**别处**发出去的码(手工 `/drc pair`、或 status 之外的入口)。
|
|
83
|
-
* 不认领的话窗口会认为"没有活动码"而再发一张,于是同一台主机挂两张有效码——
|
|
84
|
-
* 手机扫的是屏幕上那张,取错密钥就是全线解不开(多码事故的原样)。
|
|
85
|
-
*/
|
|
86
|
-
adopt(created: {
|
|
87
|
-
token: string;
|
|
88
|
-
createdAt: number;
|
|
89
|
-
expiresAt: number;
|
|
90
|
-
}): void;
|
|
91
|
-
/** 一轮检查。返回"这一轮是否发布了新码"。 */
|
|
92
|
-
tick(): boolean;
|
|
93
|
-
/** 请求值来自 `pairOnStartSec`,服务端一旦回过 `ttlMs` 就以它为准。 */
|
|
94
|
-
private ttlMs;
|
|
95
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/** 需要重新发布的原因;返回 null 表示这张码仍然可用。 */
|
|
2
|
-
export function staleReason(input) {
|
|
3
|
-
const { active, now, ttlMs, generation } = input;
|
|
4
|
-
if (!active)
|
|
5
|
-
return 'no-active-pair';
|
|
6
|
-
if (active.generation !== generation)
|
|
7
|
-
return 'relay-generation-changed';
|
|
8
|
-
if (active.expiresAt < now)
|
|
9
|
-
return 'expired';
|
|
10
|
-
if (active.expiresAt < now + ttlMs / 2)
|
|
11
|
-
return 'past-half-life';
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* 由调用方按节拍驱动(本仓库里是 `StatusFile.start()` 的 3 秒 tick——
|
|
16
|
-
* 旧实现就是挂在这里,理由是"反正每 3 秒就要写一次状态")。
|
|
17
|
-
*
|
|
18
|
-
* 刻意**不自带定时器**:定时器属于宿主,一个纯策略类不该再占一只。
|
|
19
|
-
*/
|
|
20
|
-
export class PairingWindow {
|
|
21
|
-
deps;
|
|
22
|
-
pairSec;
|
|
23
|
-
active = null;
|
|
24
|
-
requestedTtlMs;
|
|
25
|
-
constructor(deps, pairSec) {
|
|
26
|
-
this.deps = deps;
|
|
27
|
-
this.pairSec = pairSec;
|
|
28
|
-
}
|
|
29
|
-
/** 服务端权威 TTL(`pair-ready.ttlMs`):落地后覆盖请求值,并修正已挂出的那张码。 */
|
|
30
|
-
applyServerTtl(pairingToken, ttlMs) {
|
|
31
|
-
if (ttlMs > 0)
|
|
32
|
-
this.requestedTtlMs = ttlMs;
|
|
33
|
-
if (this.active && this.active.token === pairingToken) {
|
|
34
|
-
// 基线用**发起时刻**(createdAt),不是 pair-ready 到达时刻:
|
|
35
|
-
// 两条基线不一致时本地会比中继早失效(偏危险方向),取证 §6.3 末尾。
|
|
36
|
-
this.active = { ...this.active, expiresAt: this.active.createdAt + ttlMs };
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
/** 这张码已被某部手机用掉(`peer-joined` 带回的 token 就是它)。 */
|
|
40
|
-
markConsumed(pairingToken) {
|
|
41
|
-
if (this.active?.token === pairingToken) {
|
|
42
|
-
this.deps.log('pairing code consumed, publishing a fresh one', { token: pairingToken });
|
|
43
|
-
this.active = null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* 认领一张**别处**发出去的码(手工 `/drc pair`、或 status 之外的入口)。
|
|
48
|
-
* 不认领的话窗口会认为"没有活动码"而再发一张,于是同一台主机挂两张有效码——
|
|
49
|
-
* 手机扫的是屏幕上那张,取错密钥就是全线解不开(多码事故的原样)。
|
|
50
|
-
*/
|
|
51
|
-
adopt(created) {
|
|
52
|
-
this.active = { ...created, generation: this.deps.generation() };
|
|
53
|
-
}
|
|
54
|
-
/** 一轮检查。返回"这一轮是否发布了新码"。 */
|
|
55
|
-
tick() {
|
|
56
|
-
if (this.pairSec <= 0)
|
|
57
|
-
return false;
|
|
58
|
-
// 发布闸门:**不在线就一张都不发**。`RelayClient.raw()` 对非 OPEN 的 socket 是静默丢弃,
|
|
59
|
-
// 只记本地就会留下"status.json 显示有效、中继那边根本没收到"的死码(取证 §6.2)。
|
|
60
|
-
if (!this.deps.relayOnline())
|
|
61
|
-
return false;
|
|
62
|
-
let published = false;
|
|
63
|
-
const max = this.deps.maxPublishPerTick ?? 1;
|
|
64
|
-
for (let attempt = 0; attempt < max; attempt++) {
|
|
65
|
-
const ttlMs = this.ttlMs();
|
|
66
|
-
const reason = staleReason({
|
|
67
|
-
active: this.active,
|
|
68
|
-
now: this.deps.clock.now(),
|
|
69
|
-
generation: this.deps.generation(),
|
|
70
|
-
ttlMs,
|
|
71
|
-
});
|
|
72
|
-
if (!reason) {
|
|
73
|
-
// 不需要新码,但服务端 TTL 可能刚到:把已挂出那张的过期时间按权威值重算。
|
|
74
|
-
return published;
|
|
75
|
-
}
|
|
76
|
-
const created = this.deps.publish(ttlMs);
|
|
77
|
-
if (!created) {
|
|
78
|
-
this.deps.log('pairing publish failed', { reason });
|
|
79
|
-
return published;
|
|
80
|
-
}
|
|
81
|
-
this.active = { ...created, generation: this.deps.generation() };
|
|
82
|
-
this.deps.log('pairing published', { reason, token: created.token, ttlMs });
|
|
83
|
-
published = true;
|
|
84
|
-
}
|
|
85
|
-
return published;
|
|
86
|
-
}
|
|
87
|
-
/** 请求值来自 `pairOnStartSec`,服务端一旦回过 `ttlMs` 就以它为准。 */
|
|
88
|
-
ttlMs() {
|
|
89
|
-
return this.requestedTtlMs ?? Math.max(1_000, this.pairSec * 1_000);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
//# sourceMappingURL=pairing-window.js.map
|