dsh-deeppilot 0.6.2 → 0.7.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/COMPATIBILITY.md +21 -5
- package/README.md +30 -18
- package/README.zh-CN.md +22 -13
- package/SECURITY.md +1 -1
- package/lib/client.js +42 -27
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +34 -4
- package/lib/index.js +399 -68
- package/lib/index.js.map +1 -1
- package/package.json +19 -16
package/COMPATIBILITY.md
CHANGED
|
@@ -7,7 +7,7 @@ This file separates tested evidence from intended behavior. Passing unit tests d
|
|
|
7
7
|
| Component | Baseline | Evidence |
|
|
8
8
|
|---|---|---|
|
|
9
9
|
| Node.js | 22 or newer | package engine and CI |
|
|
10
|
-
| DSH CLI and Host API | `0.1.5-rc.1` minimum for plugin `0.
|
|
10
|
+
| DSH CLI and Host API | the `0.1.5-rc.*` and `0.1.6-*`/`0.1.6` lines, `0.1.5-rc.1` minimum, for plugin `0.7.x` | plugin source typecheck, unit suite (240 tests) and `tsdown` build run against both the `0.1.5-rc.2` and the `0.1.6-alpha.1` package families; bridge and `/phone` protocol tests |
|
|
11
11
|
| Host OS | macOS, Linux, Windows on packaged amd64/arm64 helper targets | helper checksums plus user-confirmed Windows/Linux Funnel launch and connection; local LAN validation remains part of alpha testing |
|
|
12
12
|
| Remote access | Tailscale Funnel, ports 443/8443/10000 | helper and supervisor tests |
|
|
13
13
|
| iOS | native DeepPilot client, protocol v2 | simulator build and v2 pairing/challenge evidence |
|
|
@@ -15,11 +15,27 @@ This file separates tested evidence from intended behavior. Passing unit tests d
|
|
|
15
15
|
## Protocol boundary
|
|
16
16
|
|
|
17
17
|
- Protocol v2 is the only supported wire version. Existing protocol-v1 devices must pair again after upgrading.
|
|
18
|
+
- Plugin `0.7.x` serves the LAN listener over self-signed TLS only; devices paired over LAN with `0.6.x` or older must pair again so the app receives the certificate fingerprint. The DSH web server no longer carries `/phone` compatibility routes. Funnel pairings are unaffected.
|
|
18
19
|
- Bearer authentication, URL credentials, and first-frame shared tokens are rejected. A supported client registers a P-256 public key through `/phone/pair` and signs each WebSocket challenge.
|
|
19
20
|
- Without a compatible embedded helper, the core bridge and trusted-LAN mode can still run; remote Funnel reports `unavailable`.
|
|
20
21
|
- If a DSH Host API is missing, only the dependent capability should be disabled. The plugin must not crash the Host.
|
|
21
|
-
- Plugin `0.
|
|
22
|
-
versions used developer-preview DSH builds and remain historical
|
|
22
|
+
- Plugin `0.7.x` requires DSH `0.1.5-rc.1` or newer within the `0.1.x` line.
|
|
23
|
+
Older plugin versions used developer-preview DSH builds and remain historical
|
|
24
|
+
artifacts.
|
|
25
|
+
- The declared peer range is a disjunction,
|
|
26
|
+
`^0.1.5-rc.1 || >=0.1.6-alpha.1 <0.2.0-0`, because npm's prerelease rule only
|
|
27
|
+
lets a prerelease version satisfy a range whose comparators carry a
|
|
28
|
+
prerelease on the same `major.minor.patch` tuple. Neither `^0.1.5-rc.1` nor
|
|
29
|
+
`>=0.1.5-rc.1` alone admits a `0.1.6` prerelease. `tests/compatibility-metadata.test.ts`
|
|
30
|
+
asserts the range admits `0.1.5-rc.1`, `0.1.5-rc.2`, `0.1.6-alpha.1`,
|
|
31
|
+
`0.1.6-beta.1` and `0.1.6`, and rejects `0.2.0`.
|
|
32
|
+
- DSH `0.1.6-alpha.1` replaced `agent/session-start` with `agent/created`,
|
|
33
|
+
deprecated the synchronous `snapshotEvents`/`eventAt`/`ownEvents` history
|
|
34
|
+
readers, and renamed the PTC packages to the `ptc-runtime` family. The plugin
|
|
35
|
+
uses none of those surfaces: every host dependency is resolved structurally
|
|
36
|
+
from the Cordis context (`sessionController`, `workspaceController`,
|
|
37
|
+
`directoryPickerController`, `sessionProjections`) and the `session/event`
|
|
38
|
+
and `api-session/*` events, all of which kept their `0.1.5-rc` shape.
|
|
23
39
|
- The compatibility facade converts the
|
|
24
40
|
current Session controller's raw event arrays into the stable wrapped
|
|
25
41
|
history entries consumed by the phone bridge. Sessions persisted by earlier
|
|
@@ -31,8 +47,8 @@ This file separates tested evidence from intended behavior. Passing unit tests d
|
|
|
31
47
|
|
|
32
48
|
- Intel macOS support for the embedded helper;
|
|
33
49
|
- signed/notarized helper distribution;
|
|
34
|
-
- repair or migration of persisted history rejected by the
|
|
35
|
-
|
|
50
|
+
- repair or migration of persisted history rejected by the Host's own session
|
|
51
|
+
reader;
|
|
36
52
|
- every DSH developer-preview revision;
|
|
37
53
|
- physical-device performance and every carrier/network combination;
|
|
38
54
|
- production APNs delivery without a real provider credential and device.
|
package/README.md
CHANGED
|
@@ -22,7 +22,8 @@ Host on your own Mac and does not replace or modify the DSH Web UI.
|
|
|
22
22
|
- Send prompts, switch models, create sessions, and answer approvals/questions.
|
|
23
23
|
- Pair with a five-minute single-use code and a per-device P-256 key; physical
|
|
24
24
|
iPhones keep the private key in Secure Enclave.
|
|
25
|
-
- Connect over
|
|
25
|
+
- Connect over the LAN (TLS with a Host-issued certificate pinned during
|
|
26
|
+
pairing) or the optional embedded Tailscale Funnel.
|
|
26
27
|
- Receive live notifications and optional APNs notifications while offline.
|
|
27
28
|
- Self-update hint: the settings page footer shows the installed plugin
|
|
28
29
|
version, with an inline "new version" link to the matching GitHub
|
|
@@ -37,10 +38,11 @@ does not require the helper.
|
|
|
37
38
|
|
|
38
39
|
| Plugin version | Required DSH | How to install |
|
|
39
40
|
|---|---|---|
|
|
40
|
-
| `0.
|
|
41
|
-
| `0.
|
|
41
|
+
| `0.7.x` (current stable, `latest`) | DSH `0.1.5-rc.1` or newer | `dsh plugin --profile web add dsh-deeppilot` |
|
|
42
|
+
| `0.6.x` (previous stable) | DSH `0.1.5-rc.1` or newer | `dsh plugin --profile web add dsh-deeppilot@0.6.2` |
|
|
43
|
+
| `0.5.x` (legacy) | DSH `0.1.1-rc.2`–`0.1.2-alpha.1` | `dsh plugin --profile web add dsh-deeppilot@0.5.0` |
|
|
42
44
|
|
|
43
|
-
`0.
|
|
45
|
+
`0.7.x` is built and typechecked against the DSH
|
|
44
46
|
[0.1.5-rc.1](https://www.npmjs.com/package/@deepseek-ai/dsh/v/0.1.5-rc.1)
|
|
45
47
|
Host and client package family. It uses the Gateway multi-client Remote Events
|
|
46
48
|
routing that keeps Web and DeepPilot independently answerable. Earlier plugin
|
|
@@ -59,10 +61,18 @@ After DSH restarts, open **Settings → DeepPilot**, enable the connection, show
|
|
|
59
61
|
the pairing QR code, and scan it in the DeepPilot app. The same panel also
|
|
60
62
|
shows a copyable pairing code for Simulator or manual entry.
|
|
61
63
|
|
|
62
|
-
LAN access is enabled by default on the plugin's independent TCP port `3098
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
LAN access is enabled by default on the plugin's independent TCP port `3098`
|
|
65
|
+
and is TLS-only. The plugin generates a self-signed certificate on first start
|
|
66
|
+
(key under `$DSH_HOME/deeppilot/lan-tls/`), and the pairing QR code carries its
|
|
67
|
+
public-key fingerprint so the app pins that exact Host. DSH may continue
|
|
68
|
+
listening only on `127.0.0.1:3080`; the plugin no longer registers any `/phone`
|
|
69
|
+
routes on the DSH web server. If a firewall is enabled, allow inbound TCP
|
|
70
|
+
`3098` on the networks you use. The port can be changed under **Advanced
|
|
71
|
+
settings**.
|
|
72
|
+
|
|
73
|
+
Upgrading from a plugin version that served plain `ws://` on the LAN: devices
|
|
74
|
+
paired over the LAN must pair again so they receive the certificate
|
|
75
|
+
fingerprint. Devices paired through Funnel are unaffected.
|
|
66
76
|
|
|
67
77
|
Package: [npmjs.com/package/dsh-deeppilot](https://www.npmjs.com/package/dsh-deeppilot)
|
|
68
78
|
|
|
@@ -78,14 +88,15 @@ DeepPilot state under `$DSH_HOME/deeppilot/`.
|
|
|
78
88
|
|
|
79
89
|
## Publishing (maintainers)
|
|
80
90
|
|
|
81
|
-
`0.
|
|
82
|
-
DSH `0.
|
|
91
|
+
`0.7.x` targets DSH `0.1.5-rc.1`+; `0.6.x` keeps the pre-TLS LAN transport for
|
|
92
|
+
the same DSH family during the migration window, and `0.5.x` stays compatible
|
|
93
|
+
with DSH `0.1.1-rc.2`–`0.1.2-alpha.1`. Keep them published:
|
|
83
94
|
|
|
84
95
|
1. Bump `version` in `package.json` and in the root `""` entry of
|
|
85
96
|
`package-lock.json`, then run `npm test && npm run typecheck && npm run build`
|
|
86
97
|
and inspect `npm pack --dry-run --json` (the check
|
|
87
|
-
`tests/compatibility-metadata.test.ts` enforces the
|
|
88
|
-
|
|
98
|
+
`tests/compatibility-metadata.test.ts` enforces the peer range and asserts it
|
|
99
|
+
admits both the `0.1.5-rc.*` and the `0.1.6-*` lines).
|
|
89
100
|
2. Commit the release and push it. `npm publish` runs `prepack` (build) and
|
|
90
101
|
`prepublishOnly` (test + typecheck) automatically.
|
|
91
102
|
3. Publish pre-releases without touching `latest`:
|
|
@@ -95,7 +106,7 @@ DSH `0.1.1-rc.2`–`0.1.2-alpha.1`. Keep both published:
|
|
|
95
106
|
```
|
|
96
107
|
|
|
97
108
|
After a successful publish, `npm view dsh-deeppilot dist-tags --json` shows
|
|
98
|
-
`"latest": "0.
|
|
109
|
+
`"latest": "0.7.x"` and `"alpha": "0.7.x-alpha.y"`. Verify the published
|
|
99
110
|
package by installing it into a DSH `0.1.5-rc.1` profile before pointing
|
|
100
111
|
users at it.
|
|
101
112
|
4. Tag the release commit `vX.Y.Z` and prepare a GitHub Release
|
|
@@ -109,11 +120,12 @@ Never run `npm publish` from a copy that still has an older version.
|
|
|
109
120
|
|
|
110
121
|
## Connection and privacy
|
|
111
122
|
|
|
112
|
-
Conversation traffic travels directly between the iPhone and your DSH Host
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
and
|
|
123
|
+
Conversation traffic travels directly between the iPhone and your DSH Host and
|
|
124
|
+
is always encrypted: the LAN listener on port `3098` serves TLS with a
|
|
125
|
+
self-signed certificate whose fingerprint the app receives during pairing, and
|
|
126
|
+
Funnel mode uses Tailscale-issued certificates. The app refuses plain `ws://`.
|
|
127
|
+
Both the LAN listener and optional Funnel mode expose only the DeepPilot
|
|
128
|
+
connection, one-time pairing, and health endpoints, not the complete DSH Web UI.
|
|
117
129
|
|
|
118
130
|
The DeepPilot settings page exposes **Connections per public source** under
|
|
119
131
|
the collapsed **Advanced settings** section. It defaults to `8`, accepts
|
package/README.zh-CN.md
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
- 发送提示词、切换模型、创建会话,并处理审批与提问;
|
|
22
22
|
- 使用 5 分钟有效的单次配对码与设备级 P-256 密钥;iPhone 真机私钥保存在
|
|
23
23
|
Secure Enclave;
|
|
24
|
-
-
|
|
24
|
+
- 局域网连接全程 TLS:插件自签证书,App 按配对二维码中的指纹固定信任;
|
|
25
|
+
也可选用内嵌 Tailscale Funnel 远程连接;
|
|
25
26
|
- 接收在线通知,以及可选的离线 APNs 推送;
|
|
26
27
|
- 更新提示:设置页底部显示当前插件版本,并在有新版本时附加一个指向
|
|
27
28
|
对应 GitHub Release 的链接(后台静默检查,仅比较稳定版,不引入第
|
|
@@ -34,10 +35,11 @@
|
|
|
34
35
|
|
|
35
36
|
| 插件版本 | 所需 DSH | 安装命令 |
|
|
36
37
|
|---|---|---|
|
|
37
|
-
| `0.
|
|
38
|
-
| `0.
|
|
38
|
+
| `0.7.x`(当前稳定版,`latest`) | DSH `0.1.5-rc.1` 或更高 | `dsh plugin --profile web add dsh-deeppilot` |
|
|
39
|
+
| `0.6.x`(旧版稳定版) | DSH `0.1.5-rc.1` 或更高 | `dsh plugin --profile web add dsh-deeppilot@0.6.2` |
|
|
40
|
+
| `0.5.x`(历史版本) | DSH `0.1.1-rc.2`–`0.1.2-alpha.1` | `dsh plugin --profile web add dsh-deeppilot@0.5.0` |
|
|
39
41
|
|
|
40
|
-
`0.
|
|
42
|
+
`0.7.x` 基于 DSH
|
|
41
43
|
[0.1.5-rc.1](https://www.npmjs.com/package/@deepseek-ai/dsh/v/0.1.5-rc.1)
|
|
42
44
|
的 Host 与 client 包族构建并完成类型检查。它使用 Gateway 多 Client Remote Events
|
|
43
45
|
路由,使 Web 与 DeepPilot 能独立处理同一交互。旧 alpha 仅保留为历史产物;安装
|
|
@@ -54,9 +56,14 @@ dsh web
|
|
|
54
56
|
DSH 重启后,打开 **设置 → DeepPilot**,启用连接并显示配对二维码,然后在
|
|
55
57
|
DeepPilot App 中扫码。同一面板也会显示可复制的配对码,供模拟器或手动输入使用。
|
|
56
58
|
|
|
57
|
-
局域网连接默认使用插件独立的 TCP `3098`
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
局域网连接默认使用插件独立的 TCP `3098` 端口,且只提供 TLS。插件首次启动时生成
|
|
60
|
+
自签证书(私钥位于 `$DSH_HOME/deeppilot/lan-tls/`),配对二维码携带其公钥指纹,
|
|
61
|
+
App 只信任与该指纹一致的主机。DSH 可以继续只监听 `127.0.0.1:3080`;插件不再在
|
|
62
|
+
DSH Web 服务上注册任何 `/phone` 路由。启用系统防火墙时,请仅在你使用的网络放行
|
|
63
|
+
入站 TCP `3098`;端口可在“高级设置”中修改。
|
|
64
|
+
|
|
65
|
+
从仍在局域网提供明文 `ws://` 的旧版本插件升级:通过局域网配对的设备需要重新扫码
|
|
66
|
+
配对,以接收证书指纹;经 Funnel 配对的设备不受影响。
|
|
60
67
|
|
|
61
68
|
npm 包:[npmjs.com/package/dsh-deeppilot](https://www.npmjs.com/package/dsh-deeppilot)
|
|
62
69
|
|
|
@@ -71,13 +78,14 @@ dsh plugin --profile web remove dsh-deeppilot
|
|
|
71
78
|
|
|
72
79
|
## 发布说明(维护者)
|
|
73
80
|
|
|
74
|
-
`0.
|
|
75
|
-
|
|
81
|
+
`0.7.x` 面向 DSH `0.1.5-rc.1`+;`0.6.x` 在同一 DSH 版本族上保留 TLS 化之前的
|
|
82
|
+
局域网传输,供迁移期使用;`0.5.x` 保持兼容 DSH
|
|
83
|
+
`0.1.1-rc.2`–`0.1.2-alpha.1`,各版本线都要保持发布:
|
|
76
84
|
|
|
77
85
|
1. 同步修改 `package.json` 与 `package-lock.json` 根 `""` 条目中的
|
|
78
86
|
`version`,然后运行 `npm test && npm run typecheck && npm run build`,
|
|
79
87
|
并检查 `npm pack --dry-run --json`(`tests/compatibility-metadata.test.ts`
|
|
80
|
-
会强制校验
|
|
88
|
+
会强制校验 peer 范围,并断言它同时接纳 `0.1.5-rc.*` 与 `0.1.6-*` 两条线)。
|
|
81
89
|
2. 提交发布并推送。`npm publish` 会自动执行 `prepack`(构建)与
|
|
82
90
|
`prepublishOnly`(测试 + 类型检查)。
|
|
83
91
|
3. 发布预发布版本,不要动 `latest`:
|
|
@@ -87,7 +95,7 @@ dsh plugin --profile web remove dsh-deeppilot
|
|
|
87
95
|
```
|
|
88
96
|
|
|
89
97
|
发布成功后,`npm view dsh-deeppilot dist-tags --json` 应显示
|
|
90
|
-
`"latest": "0.
|
|
98
|
+
`"latest": "0.7.x"` 与 `"alpha": "0.7.x-alpha.y"`。向用户推荐前,请先在
|
|
91
99
|
DSH `0.1.5-rc.1` profile 中安装验证发布的包。
|
|
92
100
|
4. 为发布提交打 `vX.Y.Z` tag,并准备包含英文与简体中文说明的
|
|
93
101
|
GitHub Release,链接本 README 的发布说明。
|
|
@@ -99,8 +107,9 @@ dsh plugin --profile web remove dsh-deeppilot
|
|
|
99
107
|
|
|
100
108
|
## 连接与隐私
|
|
101
109
|
|
|
102
|
-
完整会话流量由 iPhone 直接连接用户自己的 DSH Host
|
|
103
|
-
|
|
110
|
+
完整会话流量由 iPhone 直接连接用户自己的 DSH Host,且始终加密:独立 `3098`
|
|
111
|
+
端口的局域网监听器使用自签证书提供 TLS,指纹在配对时下发给 App;Funnel 模式
|
|
112
|
+
使用 Tailscale 签发的证书。App 拒绝明文 `ws://`。局域网监听器和可选 Funnel
|
|
104
113
|
模式都只暴露经过认证的 DeepPilot 连接、单次配对与健康检查端点,不会暴露完整
|
|
105
114
|
DSH Web UI。
|
|
106
115
|
|
package/SECURITY.md
CHANGED
|
@@ -17,7 +17,7 @@ Do not open a public issue containing pairing codes, APNs device tokens, provide
|
|
|
17
17
|
- Protocol v1, shared Bearer tokens, query credentials, and `c2s.hello.auth` are not accepted. There is no remote downgrade switch.
|
|
18
18
|
- The server enforces explicit protocol scopes, while the normal settings UI grants the default scope set and exposes only the global connection switch plus per-device deletion. Deleting a device disconnects it immediately.
|
|
19
19
|
- The canonical `$DSH_HOME/deeppilot` directory is repaired to mode `0700`. `host-id` and `devices-v2.json` are owner-only.
|
|
20
|
-
-
|
|
20
|
+
- The independent LAN listener (default port `3098`) is TLS-only: the plugin serves a self-signed EC P-256 certificate, and the app pins its SPKI fingerprint delivered inside the pairing QR. There is no trust-on-first-use; a regenerated key forces re-pairing. The private key lives owner-only under `$DSH_HOME/deeppilot/lan-tls/`. Never forward the LAN port to the public Internet; public access should use Funnel HTTPS/WSS.
|
|
21
21
|
- The LAN listener and Funnel helper publish only `/phone`, `/phone/pair`, and `/phone/health`, never the complete DSH Web UI.
|
|
22
22
|
- Logs must never include pairing codes, key material, APNs tokens, or message bodies.
|
|
23
23
|
|
package/lib/client.js
CHANGED
|
@@ -165,10 +165,16 @@ window.__ModuleLoader__.load({
|
|
|
165
165
|
if (!Array.isArray(endpoints) || endpoints.some((entry) => typeof entry !== "string")) reject("local.endpoints");
|
|
166
166
|
const message = s.message;
|
|
167
167
|
if (message !== void 0 && typeof message !== "string") reject("local.message");
|
|
168
|
+
const tlsFingerprint = s.tlsFingerprint;
|
|
169
|
+
if (tlsFingerprint !== void 0 && typeof tlsFingerprint !== "string") reject("local.tlsFingerprint");
|
|
170
|
+
const tlsIdentityRegenerated = s.tlsIdentityRegenerated;
|
|
171
|
+
if (tlsIdentityRegenerated !== void 0 && typeof tlsIdentityRegenerated !== "boolean") reject("local.tlsIdentityRegenerated");
|
|
168
172
|
return {
|
|
169
173
|
phase,
|
|
170
174
|
port: int(s, "port", "local.port"),
|
|
171
175
|
endpoints,
|
|
176
|
+
...typeof tlsFingerprint === "string" ? { tlsFingerprint } : {},
|
|
177
|
+
...tlsIdentityRegenerated === true ? { tlsIdentityRegenerated: true } : {},
|
|
172
178
|
...typeof message === "string" ? { message } : {},
|
|
173
179
|
updatedAt: int(s, "updatedAt", "local.updatedAt")
|
|
174
180
|
};
|
|
@@ -436,15 +442,17 @@ window.__ModuleLoader__.load({
|
|
|
436
442
|
"master.on": "已开启:接受手机连接。",
|
|
437
443
|
"master.off": "已关闭:不接受手机连接。",
|
|
438
444
|
"local.title": "局域网连接",
|
|
439
|
-
"local.on": "已开启:手机可通过独立端口 {port}
|
|
445
|
+
"local.on": "已开启:手机可通过独立端口 {port} 以 TLS 加密连接。",
|
|
440
446
|
"local.off": "已关闭:不接受局域网连接。",
|
|
447
|
+
"local.tlsFingerprint": "证书指纹",
|
|
448
|
+
"local.tlsRegenerated": "局域网 TLS 身份已重新生成:此前通过内网配对的设备需要重新扫码配对。",
|
|
441
449
|
"local.phaseDisabled": "局域网连接未启用",
|
|
442
450
|
"local.phaseStarting": "局域网连接正在启动",
|
|
443
451
|
"local.phaseOnline": "局域网连接已就绪",
|
|
444
452
|
"local.phaseError": "局域网连接失败",
|
|
445
453
|
"local.phaseStopped": "局域网连接已停止",
|
|
446
454
|
"local.portTitle": "局域网端口",
|
|
447
|
-
"local.portDescription": "默认 3098,范围 1024–65535
|
|
455
|
+
"local.portDescription": "默认 3098,范围 1024–65535。该端口只以 TLS 提供 DeepPilot 的三个连接端点,不会开放完整 DSH。",
|
|
448
456
|
"local.portApplied": "局域网端口已保存,监听器正在重新加载。",
|
|
449
457
|
"local.portInvalid": "请输入 1024–65535 之间的整数。",
|
|
450
458
|
"local.portFailed": "局域网端口保存失败:",
|
|
@@ -505,7 +513,7 @@ window.__ModuleLoader__.load({
|
|
|
505
513
|
"pair.codeCopyFailed": "配对码复制失败:",
|
|
506
514
|
"pair.publicCopyDone": "连接地址已复制",
|
|
507
515
|
"pair.publicCopyFailed": "复制失败:",
|
|
508
|
-
"pair.qrHint": "二维码包含{kind}
|
|
516
|
+
"pair.qrHint": "二维码包含{kind}地址、一次性配对码以及(内网时)证书指纹;配对码 5 分钟后失效,二维码将在 60 秒后自动隐藏。",
|
|
509
517
|
"advanced.summary": "高级信息",
|
|
510
518
|
"advanced.protocolVersion": "协议版本",
|
|
511
519
|
"advanced.serverVersion": "服务器版本",
|
|
@@ -618,15 +626,17 @@ window.__ModuleLoader__.load({
|
|
|
618
626
|
"master.on": "On: accepting phone connections.",
|
|
619
627
|
"master.off": "Off: not accepting phone connections.",
|
|
620
628
|
"local.title": "LAN connection",
|
|
621
|
-
"local.on": "On: phones
|
|
629
|
+
"local.on": "On: phones connect over TLS through the independent port {port}.",
|
|
622
630
|
"local.off": "Off: LAN connections are not accepted.",
|
|
631
|
+
"local.tlsFingerprint": "Certificate fingerprint",
|
|
632
|
+
"local.tlsRegenerated": "The LAN TLS identity was regenerated: devices previously paired over LAN must scan a new pairing QR.",
|
|
623
633
|
"local.phaseDisabled": "LAN connection disabled",
|
|
624
634
|
"local.phaseStarting": "LAN connection starting",
|
|
625
635
|
"local.phaseOnline": "LAN connection ready",
|
|
626
636
|
"local.phaseError": "LAN connection failed",
|
|
627
637
|
"local.phaseStopped": "LAN connection stopped",
|
|
628
638
|
"local.portTitle": "LAN port",
|
|
629
|
-
"local.portDescription": "Default 3098; range 1024–65535. This port
|
|
639
|
+
"local.portDescription": "Default 3098; range 1024–65535. This port serves only the three DeepPilot endpoints over TLS, never the full DSH server.",
|
|
630
640
|
"local.portApplied": "LAN port saved; the listener is reloading.",
|
|
631
641
|
"local.portInvalid": "Enter an integer from 1024 to 65535.",
|
|
632
642
|
"local.portFailed": "Failed to save LAN port: ",
|
|
@@ -687,7 +697,7 @@ window.__ModuleLoader__.load({
|
|
|
687
697
|
"pair.codeCopyFailed": "Failed to copy pairing code: ",
|
|
688
698
|
"pair.publicCopyDone": "Connection address copied",
|
|
689
699
|
"pair.publicCopyFailed": "Copy failed: ",
|
|
690
|
-
"pair.qrHint": "The QR contains a {kind} address
|
|
700
|
+
"pair.qrHint": "The QR contains a {kind} address, a single-use pairing code and, for LAN, the certificate fingerprint. The code expires after 5 minutes; the QR auto-hides after 60 seconds.",
|
|
691
701
|
"advanced.summary": "Advanced info",
|
|
692
702
|
"advanced.protocolVersion": "Protocol version",
|
|
693
703
|
"advanced.serverVersion": "Server version",
|
|
@@ -3352,17 +3362,25 @@ window.__ModuleLoader__.load({
|
|
|
3352
3362
|
});
|
|
3353
3363
|
})))(), 1);
|
|
3354
3364
|
const PAIRING_QR_TYPE = "deeppilot-pairing";
|
|
3365
|
+
/** `sha256:` + base64url(SHA-256(SubjectPublicKeyInfo DER)) of the LAN certificate. */
|
|
3366
|
+
const LAN_TLS_FINGERPRINT_PATTERN = /^sha256:[A-Za-z0-9_-]{43}$/;
|
|
3367
|
+
function isLanTlsFingerprint(value) {
|
|
3368
|
+
return typeof value === "string" && LAN_TLS_FINGERPRINT_PATTERN.test(value);
|
|
3369
|
+
}
|
|
3355
3370
|
/**
|
|
3356
3371
|
* Return every explicit DeepPilot transport target. LAN comes first because
|
|
3357
3372
|
* it is private and lower latency; an online Funnel remains available as a
|
|
3358
|
-
* separate choice instead of silently replacing the local address.
|
|
3373
|
+
* separate choice instead of silently replacing the local address. A LAN
|
|
3374
|
+
* listener without a certificate pin is never offered: the app could not
|
|
3375
|
+
* trust it.
|
|
3359
3376
|
*/
|
|
3360
3377
|
function selectPairingTargets(local, remote) {
|
|
3361
3378
|
const targets = [];
|
|
3362
|
-
if (local.phase === "online") {
|
|
3379
|
+
if (local.phase === "online" && isLanTlsFingerprint(local.tlsFingerprint)) {
|
|
3363
3380
|
for (const host of local.endpoints) if (validTargetHost(host)) targets.push({
|
|
3364
3381
|
host,
|
|
3365
|
-
kind: "lan"
|
|
3382
|
+
kind: "lan",
|
|
3383
|
+
tlsFingerprint: local.tlsFingerprint
|
|
3366
3384
|
});
|
|
3367
3385
|
}
|
|
3368
3386
|
if (remote.phase === "online" && remote.publicURL && validTargetHost(remote.publicURL)) targets.push({
|
|
@@ -3372,38 +3390,31 @@ window.__ModuleLoader__.load({
|
|
|
3372
3390
|
const seen = /* @__PURE__ */ new Set();
|
|
3373
3391
|
return targets.filter(({ host }) => !seen.has(host) && seen.add(host));
|
|
3374
3392
|
}
|
|
3393
|
+
/** Only encrypted transports are ever offered to the app. */
|
|
3394
|
+
const TARGET_PROTOCOLS = ["https:", "wss:"];
|
|
3375
3395
|
function validTargetHost(value) {
|
|
3376
3396
|
try {
|
|
3377
3397
|
const parsed = new URL(value);
|
|
3378
|
-
return
|
|
3379
|
-
"http:",
|
|
3380
|
-
"https:",
|
|
3381
|
-
"ws:",
|
|
3382
|
-
"wss:"
|
|
3383
|
-
].includes(parsed.protocol) && parsed.hostname !== "" && parsed.username === "" && parsed.password === "";
|
|
3398
|
+
return TARGET_PROTOCOLS.includes(parsed.protocol) && parsed.hostname !== "" && parsed.username === "" && parsed.password === "";
|
|
3384
3399
|
} catch {
|
|
3385
3400
|
return false;
|
|
3386
3401
|
}
|
|
3387
3402
|
}
|
|
3388
3403
|
/** Encode a short-lived, single-use pairing grant without URL credentials. */
|
|
3389
|
-
function encodePairingQRPayload(host, grant) {
|
|
3404
|
+
function encodePairingQRPayload(host, grant, tlsFingerprint) {
|
|
3390
3405
|
const normalizedHost = host.trim();
|
|
3391
|
-
|
|
3392
|
-
if (![
|
|
3393
|
-
"http:",
|
|
3394
|
-
"https:",
|
|
3395
|
-
"ws:",
|
|
3396
|
-
"wss:"
|
|
3397
|
-
].includes(parsed.protocol) || parsed.hostname === "" || parsed.username !== "" || parsed.password !== "") throw new TypeError("pairing QR requires a valid HTTP(S)/WS(S) host");
|
|
3406
|
+
if (!validTargetHost(normalizedHost)) throw new TypeError("pairing QR requires a valid HTTPS/WSS host");
|
|
3398
3407
|
if (grant.code.trim().length < 32 || !Number.isInteger(grant.expiresAt) || grant.expiresAt <= Date.now()) throw new TypeError("pairing grant is invalid or expired");
|
|
3399
3408
|
if (!grant.audience.startsWith("deeppilot:")) throw new TypeError("pairing audience is invalid");
|
|
3409
|
+
if (tlsFingerprint !== void 0 && !isLanTlsFingerprint(tlsFingerprint)) throw new TypeError("pairing TLS fingerprint is invalid");
|
|
3400
3410
|
const payload = {
|
|
3401
3411
|
v: 2,
|
|
3402
3412
|
type: PAIRING_QR_TYPE,
|
|
3403
3413
|
host: normalizedHost,
|
|
3404
3414
|
code: grant.code.trim(),
|
|
3405
3415
|
expiresAt: grant.expiresAt,
|
|
3406
|
-
audience: grant.audience
|
|
3416
|
+
audience: grant.audience,
|
|
3417
|
+
...tlsFingerprint !== void 0 ? { tlsFingerprint } : {}
|
|
3407
3418
|
};
|
|
3408
3419
|
return JSON.stringify(payload);
|
|
3409
3420
|
}
|
|
@@ -3714,7 +3725,7 @@ window.__ModuleLoader__.load({
|
|
|
3714
3725
|
setPairingGrant(null);
|
|
3715
3726
|
props.beginPairing().then(async (grant) => ({
|
|
3716
3727
|
grant,
|
|
3717
|
-
svg: await import_browser.toString(encodePairingQRPayload(pairingTarget.host, grant), {
|
|
3728
|
+
svg: await import_browser.toString(encodePairingQRPayload(pairingTarget.host, grant, pairingTarget.tlsFingerprint), {
|
|
3718
3729
|
type: "svg",
|
|
3719
3730
|
errorCorrectionLevel: "M",
|
|
3720
3731
|
margin: 2,
|
|
@@ -3805,7 +3816,7 @@ window.__ModuleLoader__.load({
|
|
|
3805
3816
|
role: "img",
|
|
3806
3817
|
"aria-label": report !== null ? translateWith(props.t, LOCAL_PHASE_META[report.local.phase].labelKey) : translateWith(props.t, "phase.unknown"),
|
|
3807
3818
|
title: report !== null ? translateWith(props.t, LOCAL_PHASE_META[report.local.phase].labelKey) : void 0
|
|
3808
|
-
}), translateWith(props.t, "local.title")), (0, react.createElement)("span", { className: "pbb-switchDesc" }, localSwitchReady ? localEnabled ? translateWith(props.t, "local.on", { port: localPort }) : translateWith(props.t, "local.off") : translateWith(props.t, "master.loading")), report !== null && report.local.message && report.local.phase === "error" ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, report.local.message) : null), (0, react.createElement)("button", {
|
|
3819
|
+
}), translateWith(props.t, "local.title")), (0, react.createElement)("span", { className: "pbb-switchDesc" }, localSwitchReady ? localEnabled ? translateWith(props.t, "local.on", { port: localPort }) : translateWith(props.t, "local.off") : translateWith(props.t, "master.loading")), report !== null && report.local.message && report.local.phase === "error" ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, report.local.message) : null, report !== null && report.local.tlsIdentityRegenerated === true ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, translateWith(props.t, "local.tlsRegenerated")) : null, report !== null && report.local.phase === "online" && report.local.tlsFingerprint ? (0, react.createElement)("p", { className: "pbb-diag" }, translateWith(props.t, "local.tlsFingerprint"), " ", (0, react.createElement)("code", { className: "pbb-token" }, report.local.tlsFingerprint)) : null), (0, react.createElement)("button", {
|
|
3809
3820
|
type: "button",
|
|
3810
3821
|
role: "switch",
|
|
3811
3822
|
"aria-checked": localEnabled,
|
|
@@ -3920,7 +3931,11 @@ window.__ModuleLoader__.load({
|
|
|
3920
3931
|
type: "button",
|
|
3921
3932
|
className: "pbb-action",
|
|
3922
3933
|
onClick: () => copyRemoteURL(pairingTarget.host)
|
|
3923
|
-
}, translateWith(props.t, "panel.tokenAction.copy"))), addressMessage ? (0, react.createElement)("p", { className: "pbb-diag" }, addressMessage) : null,
|
|
3934
|
+
}, translateWith(props.t, "panel.tokenAction.copy"))), addressMessage ? (0, react.createElement)("p", { className: "pbb-diag" }, addressMessage) : null, pairingTarget.tlsFingerprint === void 0 ? null : (0, react.createElement)("div", { className: "pbb-row" }, (0, react.createElement)("code", { className: "pbb-token" }, pairingTarget.tlsFingerprint), (0, react.createElement)("button", {
|
|
3935
|
+
type: "button",
|
|
3936
|
+
className: "pbb-action",
|
|
3937
|
+
onClick: () => copyRemoteURL(pairingTarget.tlsFingerprint ?? "")
|
|
3938
|
+
}, translateWith(props.t, "panel.tokenAction.copy"))), (0, react.createElement)("p", { className: "pbb-qrHint" }, translateWith(props.t, "pair.qrHint", { kind: pairingTarget.kind === "public" ? translateWith(props.t, "pair.kind.public") : translateWith(props.t, "pair.kind.lan") }))))), (0, react.createElement)("div", { className: "pbb-card" }, (0, react.createElement)("div", { className: "pbb-field" }, primaryRows, advancedRows.length > 0 ? (0, react.createElement)("details", { className: "pbb-help" }, (0, react.createElement)("summary", null, translateWith(props.t, "advanced.summary")), (0, react.createElement)("div", { className: "pbb-helpBody" }, advancedRows)) : null)), (0, react.createElement)("div", { className: "pbb-card" }, (0, react.createElement)("div", { className: "pbb-field" }, (0, react.createElement)("div", { className: "pbb-row" }, (0, react.createElement)("span", { className: "pbb-label" }, translateWith(props.t, "push.relayTitle")), (0, react.createElement)("span", { className: "pbb-tokenActions" }, (0, react.createElement)("button", {
|
|
3924
3939
|
type: "button",
|
|
3925
3940
|
className: "pbb-action",
|
|
3926
3941
|
disabled: relayTestBusy,
|