dsh-deeppilot 0.7.0 → 0.7.1
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 +24 -1
- package/README.md +6 -2
- package/README.zh-CN.md +3 -1
- package/lib/client.js +257 -151
- package/lib/client.js.map +1 -1
- package/lib/index.js +38 -48
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
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 | 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 (
|
|
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 (265 tests) and `tsdown` build run against the `0.1.5-rc.2`, `0.1.6-alpha.1` and `0.1.6-alpha.2` package families; the `deeppilot/report` contribution registered through both the `0.1.6-alpha.1` and the `0.1.6-alpha.2` `TypertRegistry`; 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,6 +15,7 @@ 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
|
+
- The pairing QR now encodes a `deeppilot://pair` link, and the settings page shows that same string in one copy field. An app build that predates the link only parses the JSON payload, so it must be updated before pairing against this plugin; a link pasted into an older app is rejected instead of being half-applied. The current app still accepts the JSON payload.
|
|
18
19
|
- 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.
|
|
19
20
|
- 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.
|
|
20
21
|
- Without a compatible embedded helper, the core bridge and trusted-LAN mode can still run; remote Funnel reports `unavailable`.
|
|
@@ -36,6 +37,28 @@ This file separates tested evidence from intended behavior. Passing unit tests d
|
|
|
36
37
|
from the Cordis context (`sessionController`, `workspaceController`,
|
|
37
38
|
`directoryPickerController`, `sessionProjections`) and the `session/event`
|
|
38
39
|
and `api-session/*` events, all of which kept their `0.1.5-rc` shape.
|
|
40
|
+
- DSH `0.1.6-alpha.2` changed the strict codec shape carried by an
|
|
41
|
+
`InvocationDescriptor`. Through `0.1.6-alpha.1` a descriptor published the
|
|
42
|
+
schema value itself and every consumer called `codec.schema.parse(value)`;
|
|
43
|
+
`alpha.2` instead validates a lazy `create()` factory at registration and the
|
|
44
|
+
Gateway parses through `codec.create().parse(value)`. A codec carrying only
|
|
45
|
+
`schema` is rejected by the `alpha.2` registry
|
|
46
|
+
(`typert: … strict codec has no create() factory`), which aborts plugin
|
|
47
|
+
activation. One `strictCodec()` helper in `src/report-wire.ts` emits both
|
|
48
|
+
keys — the same hand-written, dependency-free codec — so a single published
|
|
49
|
+
package registers and parses on either generation, and
|
|
50
|
+
`tests/report-wire.test.ts` pins both access paths.
|
|
51
|
+
The other `alpha.2` Typert change — a contributed schema's materialized
|
|
52
|
+
`schema` becoming a lazy `create()` factory — does not apply here: this
|
|
53
|
+
Remote contributes no schemas, which the same test asserts.
|
|
54
|
+
- DSH `0.1.6-alpha.2` made runtime plugin removal real, through both the
|
|
55
|
+
Plugin Manager and the client entry reconciler. Every long-lived resource is
|
|
56
|
+
registered through `ctx.effect`, and the settings-page stylesheet stamps
|
|
57
|
+
`data-plugin="dsh-deeppilot"` so the module system's `removeOwnedStyles()`
|
|
58
|
+
can delete it; the module system only auto-claims styles present during
|
|
59
|
+
factory materialization, while this sheet is injected from `apply()`.
|
|
60
|
+
Without that stamp, disabling the plugin would leave its sheet styling the
|
|
61
|
+
page.
|
|
39
62
|
- The compatibility facade converts the
|
|
40
63
|
current Session controller's raw event arrays into the stable wrapped
|
|
41
64
|
history entries consumed by the phone bridge. Sessions persisted by earlier
|
package/README.md
CHANGED
|
@@ -58,8 +58,12 @@ dsh web
|
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
After DSH restarts, open **Settings → DeepPilot**, enable the connection, show
|
|
61
|
-
the pairing QR code, and scan it in the DeepPilot app.
|
|
62
|
-
shows
|
|
61
|
+
the pairing QR code, and scan it in the DeepPilot app. Below the QR code the same
|
|
62
|
+
panel shows one copyable piece of pairing info (address, single-use code and
|
|
63
|
+
certificate fingerprint) that can be pasted into the app in one step — useful for
|
|
64
|
+
Simulator or when the camera is unavailable. Switching between the LAN and
|
|
65
|
+
public address issues a matching new QR code and string immediately, with the
|
|
66
|
+
panel staying open.
|
|
63
67
|
|
|
64
68
|
LAN access is enabled by default on the plugin's independent TCP port `3098`
|
|
65
69
|
and is TLS-only. The plugin generates a self-signed certificate on first start
|
package/README.zh-CN.md
CHANGED
|
@@ -54,7 +54,9 @@ dsh web
|
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
DSH 重启后,打开 **设置 → DeepPilot**,启用连接并显示配对二维码,然后在
|
|
57
|
-
DeepPilot App
|
|
57
|
+
DeepPilot App 中扫码。二维码下方同时显示一段可复制的「配对信息」(含地址、一次性配对码
|
|
58
|
+
与证书指纹),在 App 里粘贴一次即可完成配对,适合模拟器或相机不可用时使用;在「内网 /
|
|
59
|
+
公网」之间切换会立即为所选地址生成新的二维码与配对信息,面板保持展开。
|
|
58
60
|
|
|
59
61
|
局域网连接默认使用插件独立的 TCP `3098` 端口,且只提供 TLS。插件首次启动时生成
|
|
60
62
|
自签证书(私钥位于 `$DSH_HOME/deeppilot/lan-tls/`),配对二维码携带其公钥指纹,
|
package/lib/client.js
CHANGED
|
@@ -286,6 +286,35 @@ window.__ModuleLoader__.load({
|
|
|
286
286
|
if (!Array.isArray(value) || value.some((scope) => typeof scope !== "string" || !DEVICE_SCOPES.includes(scope))) reject("scopes");
|
|
287
287
|
return normalizeDeviceScopes(value);
|
|
288
288
|
} };
|
|
289
|
+
const booleanSchema = { parse(value) {
|
|
290
|
+
if (typeof value !== "boolean") reject("boolean");
|
|
291
|
+
return value;
|
|
292
|
+
} };
|
|
293
|
+
/**
|
|
294
|
+
* One strict boundary codec carrying both generation shapes of the supported
|
|
295
|
+
* `0.1.x` host family.
|
|
296
|
+
*
|
|
297
|
+
* Through `0.1.6-alpha.1` a descriptor published the schema value itself and
|
|
298
|
+
* every consumer parsed through `codec.schema.parse(value)`. `0.1.6-alpha.2`
|
|
299
|
+
* switched to a lazily materialized process-realm factory: `TypertRegistry`
|
|
300
|
+
* now rejects a strict codec without `create()`, and the Gateway parses
|
|
301
|
+
* through `codec.create().parse(value)`.
|
|
302
|
+
*
|
|
303
|
+
* Publishing both keys — `create()` returning the same hand-written,
|
|
304
|
+
* dependency-free codec — keeps one package valid across the entire declared
|
|
305
|
+
* peer range instead of forking the wire contract per host generation. The
|
|
306
|
+
* cast is required because each generation's `TypertCodec` declares only its
|
|
307
|
+
* own key, so a fresh object literal carrying both fails excess-property
|
|
308
|
+
* checking against either one.
|
|
309
|
+
*/
|
|
310
|
+
function strictCodec(typeSymbol, schema) {
|
|
311
|
+
return {
|
|
312
|
+
mode: "strict",
|
|
313
|
+
typeSymbol,
|
|
314
|
+
schema,
|
|
315
|
+
create: () => schema
|
|
316
|
+
};
|
|
317
|
+
}
|
|
289
318
|
const REPORT_REMOTE_CONTRIBUTION = {
|
|
290
319
|
package: REPORT_REMOTE_PACKAGE,
|
|
291
320
|
descriptors: [
|
|
@@ -296,11 +325,7 @@ window.__ModuleLoader__.load({
|
|
|
296
325
|
method: "report",
|
|
297
326
|
invocation: { kind: "direct" },
|
|
298
327
|
parameters: [],
|
|
299
|
-
result: {
|
|
300
|
-
mode: "strict",
|
|
301
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeepPilotReport`,
|
|
302
|
-
schema: reportSchema
|
|
303
|
-
}
|
|
328
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeepPilotReport`, reportSchema)
|
|
304
329
|
},
|
|
305
330
|
{
|
|
306
331
|
id: `${REPORT_REMOTE_PACKAGE}#${BEGIN_PAIRING_ENDPOINT}`,
|
|
@@ -309,11 +334,7 @@ window.__ModuleLoader__.load({
|
|
|
309
334
|
method: "beginPairing",
|
|
310
335
|
invocation: { kind: "direct" },
|
|
311
336
|
parameters: [],
|
|
312
|
-
result: {
|
|
313
|
-
mode: "strict",
|
|
314
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#PairingGrantSnapshot`,
|
|
315
|
-
schema: pairingGrantSchema
|
|
316
|
-
}
|
|
337
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#PairingGrantSnapshot`, pairingGrantSchema)
|
|
317
338
|
},
|
|
318
339
|
{
|
|
319
340
|
id: `${REPORT_REMOTE_PACKAGE}#${REVOKE_DEVICE_ENDPOINT}`,
|
|
@@ -325,20 +346,9 @@ window.__ModuleLoader__.load({
|
|
|
325
346
|
name: "deviceId",
|
|
326
347
|
wire: "deviceId",
|
|
327
348
|
source: "json",
|
|
328
|
-
codec: {
|
|
329
|
-
mode: "strict",
|
|
330
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeviceId`,
|
|
331
|
-
schema: deviceIdSchema
|
|
332
|
-
}
|
|
349
|
+
codec: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceId`, deviceIdSchema)
|
|
333
350
|
}],
|
|
334
|
-
result: {
|
|
335
|
-
mode: "strict",
|
|
336
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#Boolean`,
|
|
337
|
-
schema: { parse(value) {
|
|
338
|
-
if (typeof value !== "boolean") reject("boolean");
|
|
339
|
-
return value;
|
|
340
|
-
} }
|
|
341
|
-
}
|
|
351
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#Boolean`, booleanSchema)
|
|
342
352
|
},
|
|
343
353
|
{
|
|
344
354
|
id: `${REPORT_REMOTE_PACKAGE}#${SET_DEVICE_SCOPES_ENDPOINT}`,
|
|
@@ -350,26 +360,14 @@ window.__ModuleLoader__.load({
|
|
|
350
360
|
name: "deviceId",
|
|
351
361
|
wire: "deviceId",
|
|
352
362
|
source: "json",
|
|
353
|
-
codec: {
|
|
354
|
-
mode: "strict",
|
|
355
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeviceId`,
|
|
356
|
-
schema: deviceIdSchema
|
|
357
|
-
}
|
|
363
|
+
codec: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceId`, deviceIdSchema)
|
|
358
364
|
}, {
|
|
359
365
|
name: "scopes",
|
|
360
366
|
wire: "scopes",
|
|
361
367
|
source: "json",
|
|
362
|
-
codec: {
|
|
363
|
-
mode: "strict",
|
|
364
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeviceScopes`,
|
|
365
|
-
schema: scopesSchema
|
|
366
|
-
}
|
|
368
|
+
codec: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceScopes`, scopesSchema)
|
|
367
369
|
}],
|
|
368
|
-
result: {
|
|
369
|
-
mode: "strict",
|
|
370
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeviceScopes`,
|
|
371
|
-
schema: scopesSchema
|
|
372
|
-
}
|
|
370
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceScopes`, scopesSchema)
|
|
373
371
|
},
|
|
374
372
|
{
|
|
375
373
|
id: `${REPORT_REMOTE_PACKAGE}#deeppilot/testRelay`,
|
|
@@ -378,11 +376,7 @@ window.__ModuleLoader__.load({
|
|
|
378
376
|
method: "testRelay",
|
|
379
377
|
invocation: { kind: "direct" },
|
|
380
378
|
parameters: [],
|
|
381
|
-
result: {
|
|
382
|
-
mode: "strict",
|
|
383
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#RelayTestResult`,
|
|
384
|
-
schema: relayTestSchema
|
|
385
|
-
}
|
|
379
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#RelayTestResult`, relayTestSchema)
|
|
386
380
|
},
|
|
387
381
|
{
|
|
388
382
|
id: `${REPORT_REMOTE_PACKAGE}#deeppilot/testPush`,
|
|
@@ -391,11 +385,7 @@ window.__ModuleLoader__.load({
|
|
|
391
385
|
method: "testPush",
|
|
392
386
|
invocation: { kind: "direct" },
|
|
393
387
|
parameters: [],
|
|
394
|
-
result: {
|
|
395
|
-
mode: "strict",
|
|
396
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#PushTestResult`,
|
|
397
|
-
schema: pushTestSchema
|
|
398
|
-
}
|
|
388
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#PushTestResult`, pushTestSchema)
|
|
399
389
|
}
|
|
400
390
|
]
|
|
401
391
|
};
|
|
@@ -506,14 +496,13 @@ window.__ModuleLoader__.load({
|
|
|
506
496
|
"pair.qrShow": "显示二维码",
|
|
507
497
|
"pair.qrHide": "隐藏二维码",
|
|
508
498
|
"pair.qrGenerating": "生成中…",
|
|
509
|
-
"pair.
|
|
499
|
+
"pair.qrExpired": "配对码已过期,请重新显示二维码",
|
|
510
500
|
"pair.qrFailed": "二维码生成失败:",
|
|
511
|
-
"pair.
|
|
512
|
-
"pair.
|
|
513
|
-
"pair.
|
|
514
|
-
"pair.
|
|
515
|
-
"pair.
|
|
516
|
-
"pair.qrHint": "二维码包含{kind}地址、一次性配对码以及(内网时)证书指纹;配对码 5 分钟后失效,二维码将在 60 秒后自动隐藏。",
|
|
501
|
+
"pair.qrRetry": "重试",
|
|
502
|
+
"pair.infoLabel": "配对信息(复制一次,粘贴到 App 即可)",
|
|
503
|
+
"pair.infoCopyDone": "配对信息已复制",
|
|
504
|
+
"pair.infoCopyFailed": "配对信息复制失败:",
|
|
505
|
+
"pair.qrHint": "二维码与下面的「配对信息」内容相同:包含{kind}地址、一次性配对码、证书指纹(内网时)与主机标识。在局域网与公网之间切换会立即生成对应的新配对信息,配对码 5 分钟后失效。",
|
|
517
506
|
"advanced.summary": "高级信息",
|
|
518
507
|
"advanced.protocolVersion": "协议版本",
|
|
519
508
|
"advanced.serverVersion": "服务器版本",
|
|
@@ -690,14 +679,13 @@ window.__ModuleLoader__.load({
|
|
|
690
679
|
"pair.qrShow": "Show QR code",
|
|
691
680
|
"pair.qrHide": "Hide QR code",
|
|
692
681
|
"pair.qrGenerating": "Generating…",
|
|
693
|
-
"pair.
|
|
682
|
+
"pair.qrExpired": "Pairing code expired — show the QR code again",
|
|
694
683
|
"pair.qrFailed": "QR generation failed: ",
|
|
695
|
-
"pair.
|
|
696
|
-
"pair.
|
|
697
|
-
"pair.
|
|
698
|
-
"pair.
|
|
699
|
-
"pair.
|
|
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.",
|
|
684
|
+
"pair.qrRetry": "Retry",
|
|
685
|
+
"pair.infoLabel": "Pairing info (copy once, paste it in the app)",
|
|
686
|
+
"pair.infoCopyDone": "Pairing info copied",
|
|
687
|
+
"pair.infoCopyFailed": "Failed to copy pairing info: ",
|
|
688
|
+
"pair.qrHint": "The QR code and the pairing info below carry the same {kind} address, single-use code, certificate fingerprint (for LAN) and host identity. Switching between LAN and public issues a matching new string immediately; the code expires after 5 minutes.",
|
|
701
689
|
"advanced.summary": "Advanced info",
|
|
702
690
|
"advanced.protocolVersion": "Protocol version",
|
|
703
691
|
"advanced.serverVersion": "Server version",
|
|
@@ -948,11 +936,29 @@ window.__ModuleLoader__.load({
|
|
|
948
936
|
".pbb-versionFooter a{color:var(--dsw-alias-state-success-primary,#22a06b);text-decoration:none}",
|
|
949
937
|
".pbb-versionFooter a:hover{text-decoration:underline;text-underline-offset:2px}"
|
|
950
938
|
].join("\n");
|
|
939
|
+
/**
|
|
940
|
+
* Client bundle id, identical to the `window.__ModuleLoader__.load({ id })`
|
|
941
|
+
* identity emitted by tsdown.config.ts. DSH's module system owns style
|
|
942
|
+
* teardown by package id, so this tag must carry the same value.
|
|
943
|
+
*/
|
|
944
|
+
const PLUGIN_ID = "dsh-deeppilot";
|
|
945
|
+
/**
|
|
946
|
+
* Claim this plugin's page stylesheet against DSH's style ownership.
|
|
947
|
+
*
|
|
948
|
+
* `ClientModuleSystem` tags untagged `<style>` elements only during factory
|
|
949
|
+
* materialization; this sheet is injected from `apply()`, which runs after
|
|
950
|
+
* that point, so it would never be attributed to us. Stamping `data-plugin`
|
|
951
|
+
* ourselves lets the module system's `removeOwnedStyles(id)` delete the sheet
|
|
952
|
+
* when the plugin is unloaded at runtime — the Plugin Manager and the client
|
|
953
|
+
* entry reconciler both support live removal from 0.1.6-alpha.2 on, and an
|
|
954
|
+
* orphaned sheet would otherwise keep styling a page we no longer own.
|
|
955
|
+
*/
|
|
951
956
|
function injectCss() {
|
|
952
957
|
if (typeof document === "undefined") return;
|
|
953
958
|
const id = "dsh-deeppilot/page.css";
|
|
954
959
|
if (document.querySelector("style[data-plugin-css=\"" + id + "\"]") !== null) return;
|
|
955
960
|
const tag = document.createElement("style");
|
|
961
|
+
tag.setAttribute("data-plugin", PLUGIN_ID);
|
|
956
962
|
tag.setAttribute("data-plugin-css", id);
|
|
957
963
|
tag.textContent = CSS;
|
|
958
964
|
document.head.appendChild(tag);
|
|
@@ -3361,7 +3367,6 @@ window.__ModuleLoader__.load({
|
|
|
3361
3367
|
return SvgRenderer.render(data, opts);
|
|
3362
3368
|
});
|
|
3363
3369
|
})))(), 1);
|
|
3364
|
-
const PAIRING_QR_TYPE = "deeppilot-pairing";
|
|
3365
3370
|
/** `sha256:` + base64url(SHA-256(SubjectPublicKeyInfo DER)) of the LAN certificate. */
|
|
3366
3371
|
const LAN_TLS_FINGERPRINT_PATTERN = /^sha256:[A-Za-z0-9_-]{43}$/;
|
|
3367
3372
|
function isLanTlsFingerprint(value) {
|
|
@@ -3400,23 +3405,105 @@ window.__ModuleLoader__.load({
|
|
|
3400
3405
|
return false;
|
|
3401
3406
|
}
|
|
3402
3407
|
}
|
|
3403
|
-
/**
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3408
|
+
/**
|
|
3409
|
+
* One copyable string carrying everything the app needs to pair: host, the
|
|
3410
|
+
* single-use code, the LAN certificate pin and the host audience.
|
|
3411
|
+
*
|
|
3412
|
+
* Users copy this once instead of moving three separate values, and the app
|
|
3413
|
+
* accepts it from the paste field, a deep link and (from this version on) the
|
|
3414
|
+
* QR code. Field names are short because the whole string ends up in a QR code
|
|
3415
|
+
* and in a text field: `h` host, `c` code, `f` fingerprint, `a` audience,
|
|
3416
|
+
* `e` expiry in milliseconds.
|
|
3417
|
+
*/
|
|
3418
|
+
function encodePairingLink(host, grant, tlsFingerprint) {
|
|
3419
|
+
assertPairingInput(host, grant, tlsFingerprint);
|
|
3420
|
+
const params = new URLSearchParams();
|
|
3421
|
+
params.set("h", host.trim());
|
|
3422
|
+
params.set("c", grant.code.trim());
|
|
3423
|
+
if (tlsFingerprint !== void 0) params.set("f", tlsFingerprint);
|
|
3424
|
+
params.set("a", grant.audience);
|
|
3425
|
+
params.set("e", String(grant.expiresAt));
|
|
3426
|
+
return `${PAIRING_LINK_PREFIX}?${params.toString()}`;
|
|
3427
|
+
}
|
|
3428
|
+
/** `deeppilot://pair` — the deep-link route the iOS app parses. */
|
|
3429
|
+
const PAIRING_LINK_PREFIX = "deeppilot://pair";
|
|
3430
|
+
function assertPairingInput(host, grant, tlsFingerprint) {
|
|
3431
|
+
if (!validTargetHost(host.trim())) throw new TypeError("pairing QR requires a valid HTTPS/WSS host");
|
|
3407
3432
|
if (grant.code.trim().length < 32 || !Number.isInteger(grant.expiresAt) || grant.expiresAt <= Date.now()) throw new TypeError("pairing grant is invalid or expired");
|
|
3408
3433
|
if (!grant.audience.startsWith("deeppilot:")) throw new TypeError("pairing audience is invalid");
|
|
3409
3434
|
if (tlsFingerprint !== void 0 && !isLanTlsFingerprint(tlsFingerprint)) throw new TypeError("pairing TLS fingerprint is invalid");
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3435
|
+
}
|
|
3436
|
+
//#endregion
|
|
3437
|
+
//#region src/client/pairing-panel.ts
|
|
3438
|
+
const initialPairingPanelState = {
|
|
3439
|
+
target: null,
|
|
3440
|
+
busy: false,
|
|
3441
|
+
grant: null,
|
|
3442
|
+
link: null,
|
|
3443
|
+
qrDataURL: null,
|
|
3444
|
+
message: "",
|
|
3445
|
+
notice: "",
|
|
3446
|
+
requestId: 0
|
|
3447
|
+
};
|
|
3448
|
+
function pairingPanelReduce(state, action) {
|
|
3449
|
+
switch (action.type) {
|
|
3450
|
+
case "show": return {
|
|
3451
|
+
target: action.target,
|
|
3452
|
+
busy: true,
|
|
3453
|
+
grant: null,
|
|
3454
|
+
link: null,
|
|
3455
|
+
qrDataURL: null,
|
|
3456
|
+
message: "",
|
|
3457
|
+
notice: "",
|
|
3458
|
+
requestId: action.requestId
|
|
3459
|
+
};
|
|
3460
|
+
case "issued":
|
|
3461
|
+
if (action.requestId !== state.requestId) return state;
|
|
3462
|
+
return {
|
|
3463
|
+
...state,
|
|
3464
|
+
busy: false,
|
|
3465
|
+
grant: action.grant,
|
|
3466
|
+
link: action.link,
|
|
3467
|
+
qrDataURL: action.qrDataURL
|
|
3468
|
+
};
|
|
3469
|
+
case "failed":
|
|
3470
|
+
if (action.requestId !== state.requestId) return state;
|
|
3471
|
+
return {
|
|
3472
|
+
...state,
|
|
3473
|
+
busy: false,
|
|
3474
|
+
grant: null,
|
|
3475
|
+
link: null,
|
|
3476
|
+
qrDataURL: null,
|
|
3477
|
+
message: action.message
|
|
3478
|
+
};
|
|
3479
|
+
case "notice": return {
|
|
3480
|
+
...state,
|
|
3481
|
+
notice: action.message
|
|
3482
|
+
};
|
|
3483
|
+
case "dismissNotice": return state.notice === "" ? state : {
|
|
3484
|
+
...state,
|
|
3485
|
+
notice: ""
|
|
3486
|
+
};
|
|
3487
|
+
case "close": return {
|
|
3488
|
+
...initialPairingPanelState,
|
|
3489
|
+
requestId: state.requestId + 1,
|
|
3490
|
+
message: action.message ?? ""
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
function pairingPanelView(state) {
|
|
3495
|
+
const open = state.target !== null;
|
|
3496
|
+
return {
|
|
3497
|
+
open,
|
|
3498
|
+
busy: state.busy,
|
|
3499
|
+
failed: open && !state.busy && state.grant === null,
|
|
3500
|
+
action: state.busy ? "pair.qrGenerating" : state.target === null ? "pair.qrShow" : "pair.qrHide",
|
|
3501
|
+
target: state.target,
|
|
3502
|
+
link: state.link,
|
|
3503
|
+
qrDataURL: state.qrDataURL,
|
|
3504
|
+
message: state.message,
|
|
3505
|
+
notice: state.notice
|
|
3418
3506
|
};
|
|
3419
|
-
return JSON.stringify(payload);
|
|
3420
3507
|
}
|
|
3421
3508
|
function normalizeFunnelConnectionLimit(value) {
|
|
3422
3509
|
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 16 ? value : 8;
|
|
@@ -3519,16 +3606,13 @@ window.__ModuleLoader__.load({
|
|
|
3519
3606
|
};
|
|
3520
3607
|
/** Slot component: hooks come from the slot renderer, named use<Key>. */
|
|
3521
3608
|
function DeepPilotSettingsPage(props) {
|
|
3522
|
-
const [addressMessage, setAddressMessage] = (0, react.useState)("");
|
|
3523
3609
|
const [remoteLimitDraft, setRemoteLimitDraft] = (0, react.useState)(String(8));
|
|
3524
3610
|
const [remoteLimitMessage, setRemoteLimitMessage] = (0, react.useState)("");
|
|
3525
3611
|
const [localPortDraft, setLocalPortDraft] = (0, react.useState)(String(DEFAULT_LOCAL_PORT));
|
|
3526
3612
|
const [localPortMessage, setLocalPortMessage] = (0, react.useState)("");
|
|
3527
3613
|
const [selectedPairingHost, setSelectedPairingHost] = (0, react.useState)(null);
|
|
3528
|
-
const [
|
|
3529
|
-
const
|
|
3530
|
-
const [qrBusy, setQRBusy] = (0, react.useState)(false);
|
|
3531
|
-
const [qrMessage, setQRMessage] = (0, react.useState)("");
|
|
3614
|
+
const [pairingPanel, dispatchPairingPanel] = (0, react.useReducer)(pairingPanelReduce, initialPairingPanelState);
|
|
3615
|
+
const qrRequestId = (0, react.useRef)(0);
|
|
3532
3616
|
const [relayTestBusy, setRelayTestBusy] = (0, react.useState)(false);
|
|
3533
3617
|
const [relayTestResult, setRelayTestResult] = (0, react.useState)(null);
|
|
3534
3618
|
const [relayTestError, setRelayTestError] = (0, react.useState)("");
|
|
@@ -3575,11 +3659,6 @@ window.__ModuleLoader__.load({
|
|
|
3575
3659
|
setRelayTestBusy(false);
|
|
3576
3660
|
});
|
|
3577
3661
|
};
|
|
3578
|
-
(0, react.useEffect)(() => {
|
|
3579
|
-
if (!addressMessage) return;
|
|
3580
|
-
const timer = globalThis.setTimeout(() => setAddressMessage(""), 2500);
|
|
3581
|
-
return () => globalThis.clearTimeout(timer);
|
|
3582
|
-
}, [addressMessage]);
|
|
3583
3662
|
(0, react.useEffect)(() => {
|
|
3584
3663
|
if (!remoteLimitMessage) return;
|
|
3585
3664
|
const timer = globalThis.setTimeout(() => setRemoteLimitMessage(""), 4e3);
|
|
@@ -3591,19 +3670,21 @@ window.__ModuleLoader__.load({
|
|
|
3591
3670
|
return () => globalThis.clearTimeout(timer);
|
|
3592
3671
|
}, [localPortMessage]);
|
|
3593
3672
|
(0, react.useEffect)(() => {
|
|
3594
|
-
if (
|
|
3673
|
+
if (pairingPanel.grant === null) return;
|
|
3674
|
+
const remaining = Math.max(0, pairingPanel.grant.expiresAt - Date.now());
|
|
3595
3675
|
const timer = globalThis.setTimeout(() => {
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3676
|
+
dispatchPairingPanel({
|
|
3677
|
+
type: "close",
|
|
3678
|
+
message: translateWith(props.t, "pair.qrExpired")
|
|
3679
|
+
});
|
|
3680
|
+
}, remaining);
|
|
3600
3681
|
return () => globalThis.clearTimeout(timer);
|
|
3601
|
-
}, [
|
|
3682
|
+
}, [pairingPanel.grant]);
|
|
3602
3683
|
(0, react.useEffect)(() => {
|
|
3603
|
-
if (!
|
|
3604
|
-
const timer = globalThis.setTimeout(() =>
|
|
3684
|
+
if (!pairingPanel.notice) return;
|
|
3685
|
+
const timer = globalThis.setTimeout(() => dispatchPairingPanel({ type: "dismissNotice" }), 2500);
|
|
3605
3686
|
return () => globalThis.clearTimeout(timer);
|
|
3606
|
-
}, [
|
|
3687
|
+
}, [pairingPanel.notice]);
|
|
3607
3688
|
const diag = [];
|
|
3608
3689
|
let report = null;
|
|
3609
3690
|
let enabled = true;
|
|
@@ -3705,50 +3786,75 @@ window.__ModuleLoader__.load({
|
|
|
3705
3786
|
};
|
|
3706
3787
|
const pairingTargets = report === null ? [] : selectPairingTargets(report.local, report.remote);
|
|
3707
3788
|
const pairingTarget = pairingTargets.find(({ host }) => host === selectedPairingHost) ?? pairingTargets[0] ?? null;
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3789
|
+
const panel = pairingPanelView(pairingPanel);
|
|
3790
|
+
/**
|
|
3791
|
+
* Issue a grant and render the panel for one explicit target.
|
|
3792
|
+
*
|
|
3793
|
+
* The target is a parameter rather than component state on purpose: a click
|
|
3794
|
+
* handler must not depend on whether React has already re-rendered with the
|
|
3795
|
+
* newly selected host. A grant is bound to the host it was issued for, so
|
|
3796
|
+
* switching between LAN and public always mints a fresh one.
|
|
3797
|
+
*/
|
|
3798
|
+
const showPairingQR = (target) => {
|
|
3799
|
+
if (typeof props.beginPairing !== "function") return;
|
|
3800
|
+
const requestId = qrRequestId.current + 1;
|
|
3801
|
+
qrRequestId.current = requestId;
|
|
3802
|
+
dispatchPairingPanel({
|
|
3803
|
+
type: "show",
|
|
3804
|
+
target,
|
|
3805
|
+
requestId
|
|
3718
3806
|
});
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3807
|
+
props.beginPairing().then(async (grant) => {
|
|
3808
|
+
const link = encodePairingLink(target.host, grant, target.tlsFingerprint);
|
|
3809
|
+
return {
|
|
3810
|
+
grant,
|
|
3811
|
+
link,
|
|
3812
|
+
svg: await import_browser.toString(link, {
|
|
3813
|
+
type: "svg",
|
|
3814
|
+
errorCorrectionLevel: "M",
|
|
3815
|
+
margin: 2,
|
|
3816
|
+
width: 512
|
|
3817
|
+
})
|
|
3818
|
+
};
|
|
3819
|
+
}).then(({ grant, link, svg }) => {
|
|
3820
|
+
if (qrRequestId.current !== requestId) return;
|
|
3821
|
+
dispatchPairingPanel({
|
|
3822
|
+
type: "issued",
|
|
3823
|
+
requestId,
|
|
3824
|
+
grant,
|
|
3825
|
+
link,
|
|
3826
|
+
qrDataURL: "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg)
|
|
3827
|
+
});
|
|
3737
3828
|
}, (error) => {
|
|
3738
|
-
|
|
3739
|
-
|
|
3829
|
+
if (qrRequestId.current !== requestId) return;
|
|
3830
|
+
dispatchPairingPanel({
|
|
3831
|
+
type: "failed",
|
|
3832
|
+
requestId,
|
|
3833
|
+
message: translateWith(props.t, "pair.qrFailed") + (error instanceof Error ? error.message : String(error))
|
|
3834
|
+
});
|
|
3835
|
+
});
|
|
3740
3836
|
};
|
|
3741
3837
|
const hidePairingQR = () => {
|
|
3742
|
-
|
|
3743
|
-
|
|
3838
|
+
qrRequestId.current += 1;
|
|
3839
|
+
dispatchPairingPanel({ type: "close" });
|
|
3744
3840
|
};
|
|
3745
|
-
const
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3841
|
+
const copyPairingInfo = () => {
|
|
3842
|
+
const link = pairingPanel.link;
|
|
3843
|
+
if (link === null) return;
|
|
3844
|
+
dispatchPairingPanel({
|
|
3845
|
+
type: "notice",
|
|
3846
|
+
message: ""
|
|
3847
|
+
});
|
|
3848
|
+
writeClipboard(props.t, link).then(() => {
|
|
3849
|
+
dispatchPairingPanel({
|
|
3850
|
+
type: "notice",
|
|
3851
|
+
message: translateWith(props.t, "pair.infoCopyDone")
|
|
3852
|
+
});
|
|
3750
3853
|
}, (error) => {
|
|
3751
|
-
|
|
3854
|
+
dispatchPairingPanel({
|
|
3855
|
+
type: "notice",
|
|
3856
|
+
message: translateWith(props.t, "pair.infoCopyFailed") + (error instanceof Error ? error.message : String(error))
|
|
3857
|
+
});
|
|
3752
3858
|
});
|
|
3753
3859
|
};
|
|
3754
3860
|
const primaryRows = [];
|
|
@@ -3906,36 +4012,36 @@ window.__ModuleLoader__.load({
|
|
|
3906
4012
|
}, translateWith(props.t, "help.funnelDocs"))))))), report === null ? 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, "pair.qrPanelTitle")), pairingTarget === null ? (0, react.createElement)("span", { className: "pbb-value pbb-bad" }, translateWith(props.t, "pair.noAddress")) : (0, react.createElement)("span", { className: "pbb-tokenActions" }, (0, react.createElement)("span", { className: "pbb-badge" }, pairingTarget.kind === "public" ? translateWith(props.t, "pair.kind.public") : translateWith(props.t, "pair.kind.lan")), (0, react.createElement)("button", {
|
|
3907
4013
|
type: "button",
|
|
3908
4014
|
className: "pbb-action",
|
|
3909
|
-
disabled:
|
|
4015
|
+
disabled: panel.busy || !report.pairingReady,
|
|
3910
4016
|
onClick: () => {
|
|
3911
|
-
if (
|
|
3912
|
-
else
|
|
4017
|
+
if (panel.open) hidePairingQR();
|
|
4018
|
+
else showPairingQR(pairingTarget);
|
|
3913
4019
|
}
|
|
3914
|
-
},
|
|
4020
|
+
}, translateWith(props.t, panel.action)))), pairingTarget === null ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, translateWith(props.t, "pair.noAddressHelp")) : null, pairingTargets.length < 2 ? null : (0, react.createElement)("div", { className: "pbb-targetList" }, pairingTargets.map((target) => (0, react.createElement)("button", {
|
|
3915
4021
|
type: "button",
|
|
3916
4022
|
key: target.host,
|
|
3917
4023
|
className: "pbb-action" + (target.host === pairingTarget?.host ? " pbb-actionSelected" : ""),
|
|
4024
|
+
disabled: panel.busy,
|
|
3918
4025
|
onClick: () => {
|
|
3919
4026
|
setSelectedPairingHost(target.host);
|
|
3920
|
-
|
|
4027
|
+
if (panel.open) showPairingQR(target);
|
|
4028
|
+
else hidePairingQR();
|
|
3921
4029
|
}
|
|
3922
|
-
}, (target.kind === "public" ? translateWith(props.t, "pair.kind.public") : translateWith(props.t, "pair.kind.lan")) + " · " + target.host))),
|
|
3923
|
-
className: "pbb-qrImage",
|
|
3924
|
-
src: qrDataURL,
|
|
3925
|
-
alt: translateWith(props.t, "pair.qrAlt")
|
|
3926
|
-
}), (0, react.createElement)("div", { className: "pbb-pairCodeBlock" }, (0, react.createElement)("span", { className: "pbb-pairCodeLabel" }, translateWith(props.t, "pair.codeLabel")), (0, react.createElement)("div", { className: "pbb-pairCodeRow" }, (0, react.createElement)("code", { className: "pbb-pairCode" }, pairingGrant.code), (0, react.createElement)("button", {
|
|
4030
|
+
}, (target.kind === "public" ? translateWith(props.t, "pair.kind.public") : translateWith(props.t, "pair.kind.lan")) + " · " + target.host))), panel.message ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, panel.message) : null, panel.notice ? (0, react.createElement)("p", { className: "pbb-diag" }, panel.notice) : null, panel.failed && pairingTarget !== null ? (0, react.createElement)("div", { className: "pbb-rowAction" }, (0, react.createElement)("button", {
|
|
3927
4031
|
type: "button",
|
|
3928
4032
|
className: "pbb-action",
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
className: "pbb-
|
|
3933
|
-
|
|
3934
|
-
|
|
4033
|
+
disabled: panel.busy || !report.pairingReady,
|
|
4034
|
+
onClick: () => showPairingQR(pairingTarget)
|
|
4035
|
+
}, translateWith(props.t, "pair.qrRetry"))) : null, panel.open ? (0, react.createElement)("div", { className: "pbb-qrPanel" }, panel.qrDataURL === null ? (0, react.createElement)("p", { className: "pbb-diag" }, translateWith(props.t, "pair.qrGenerating")) : (0, react.createElement)("img", {
|
|
4036
|
+
className: "pbb-qrImage",
|
|
4037
|
+
src: panel.qrDataURL,
|
|
4038
|
+
alt: translateWith(props.t, "pair.qrAlt")
|
|
4039
|
+
}), panel.link === null ? null : (0, react.createElement)("div", { className: "pbb-pairCodeBlock" }, (0, react.createElement)("span", { className: "pbb-pairCodeLabel" }, translateWith(props.t, "pair.infoLabel")), (0, react.createElement)("div", { className: "pbb-pairCodeRow" }, (0, react.createElement)("code", { className: "pbb-pairCode" }, panel.link), (0, react.createElement)("button", {
|
|
3935
4040
|
type: "button",
|
|
3936
4041
|
className: "pbb-action",
|
|
3937
|
-
onClick:
|
|
3938
|
-
|
|
4042
|
+
onClick: copyPairingInfo,
|
|
4043
|
+
"data-testid": "pairingInfoCopy"
|
|
4044
|
+
}, translateWith(props.t, "panel.tokenAction.copy")))), (0, react.createElement)("p", { className: "pbb-qrHint" }, translateWith(props.t, "pair.qrHint", { kind: panel.target?.kind === "public" ? translateWith(props.t, "pair.kind.public") : translateWith(props.t, "pair.kind.lan") }))) : null)), (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", {
|
|
3939
4045
|
type: "button",
|
|
3940
4046
|
className: "pbb-action",
|
|
3941
4047
|
disabled: relayTestBusy,
|