dsh-deeppilot 0.6.2 → 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 +44 -5
- package/README.md +37 -21
- package/README.zh-CN.md +25 -14
- package/SECURITY.md +1 -1
- package/lib/client.js +287 -166
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +34 -4
- package/lib/index.js +437 -116
- package/lib/index.js.map +1 -1
- package/package.json +19 -16
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
|
};
|
|
@@ -280,6 +286,35 @@ window.__ModuleLoader__.load({
|
|
|
280
286
|
if (!Array.isArray(value) || value.some((scope) => typeof scope !== "string" || !DEVICE_SCOPES.includes(scope))) reject("scopes");
|
|
281
287
|
return normalizeDeviceScopes(value);
|
|
282
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
|
+
}
|
|
283
318
|
const REPORT_REMOTE_CONTRIBUTION = {
|
|
284
319
|
package: REPORT_REMOTE_PACKAGE,
|
|
285
320
|
descriptors: [
|
|
@@ -290,11 +325,7 @@ window.__ModuleLoader__.load({
|
|
|
290
325
|
method: "report",
|
|
291
326
|
invocation: { kind: "direct" },
|
|
292
327
|
parameters: [],
|
|
293
|
-
result: {
|
|
294
|
-
mode: "strict",
|
|
295
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeepPilotReport`,
|
|
296
|
-
schema: reportSchema
|
|
297
|
-
}
|
|
328
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeepPilotReport`, reportSchema)
|
|
298
329
|
},
|
|
299
330
|
{
|
|
300
331
|
id: `${REPORT_REMOTE_PACKAGE}#${BEGIN_PAIRING_ENDPOINT}`,
|
|
@@ -303,11 +334,7 @@ window.__ModuleLoader__.load({
|
|
|
303
334
|
method: "beginPairing",
|
|
304
335
|
invocation: { kind: "direct" },
|
|
305
336
|
parameters: [],
|
|
306
|
-
result: {
|
|
307
|
-
mode: "strict",
|
|
308
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#PairingGrantSnapshot`,
|
|
309
|
-
schema: pairingGrantSchema
|
|
310
|
-
}
|
|
337
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#PairingGrantSnapshot`, pairingGrantSchema)
|
|
311
338
|
},
|
|
312
339
|
{
|
|
313
340
|
id: `${REPORT_REMOTE_PACKAGE}#${REVOKE_DEVICE_ENDPOINT}`,
|
|
@@ -319,20 +346,9 @@ window.__ModuleLoader__.load({
|
|
|
319
346
|
name: "deviceId",
|
|
320
347
|
wire: "deviceId",
|
|
321
348
|
source: "json",
|
|
322
|
-
codec: {
|
|
323
|
-
mode: "strict",
|
|
324
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeviceId`,
|
|
325
|
-
schema: deviceIdSchema
|
|
326
|
-
}
|
|
349
|
+
codec: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceId`, deviceIdSchema)
|
|
327
350
|
}],
|
|
328
|
-
result: {
|
|
329
|
-
mode: "strict",
|
|
330
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#Boolean`,
|
|
331
|
-
schema: { parse(value) {
|
|
332
|
-
if (typeof value !== "boolean") reject("boolean");
|
|
333
|
-
return value;
|
|
334
|
-
} }
|
|
335
|
-
}
|
|
351
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#Boolean`, booleanSchema)
|
|
336
352
|
},
|
|
337
353
|
{
|
|
338
354
|
id: `${REPORT_REMOTE_PACKAGE}#${SET_DEVICE_SCOPES_ENDPOINT}`,
|
|
@@ -344,26 +360,14 @@ window.__ModuleLoader__.load({
|
|
|
344
360
|
name: "deviceId",
|
|
345
361
|
wire: "deviceId",
|
|
346
362
|
source: "json",
|
|
347
|
-
codec: {
|
|
348
|
-
mode: "strict",
|
|
349
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeviceId`,
|
|
350
|
-
schema: deviceIdSchema
|
|
351
|
-
}
|
|
363
|
+
codec: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceId`, deviceIdSchema)
|
|
352
364
|
}, {
|
|
353
365
|
name: "scopes",
|
|
354
366
|
wire: "scopes",
|
|
355
367
|
source: "json",
|
|
356
|
-
codec: {
|
|
357
|
-
mode: "strict",
|
|
358
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeviceScopes`,
|
|
359
|
-
schema: scopesSchema
|
|
360
|
-
}
|
|
368
|
+
codec: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceScopes`, scopesSchema)
|
|
361
369
|
}],
|
|
362
|
-
result: {
|
|
363
|
-
mode: "strict",
|
|
364
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#DeviceScopes`,
|
|
365
|
-
schema: scopesSchema
|
|
366
|
-
}
|
|
370
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceScopes`, scopesSchema)
|
|
367
371
|
},
|
|
368
372
|
{
|
|
369
373
|
id: `${REPORT_REMOTE_PACKAGE}#deeppilot/testRelay`,
|
|
@@ -372,11 +376,7 @@ window.__ModuleLoader__.load({
|
|
|
372
376
|
method: "testRelay",
|
|
373
377
|
invocation: { kind: "direct" },
|
|
374
378
|
parameters: [],
|
|
375
|
-
result: {
|
|
376
|
-
mode: "strict",
|
|
377
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#RelayTestResult`,
|
|
378
|
-
schema: relayTestSchema
|
|
379
|
-
}
|
|
379
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#RelayTestResult`, relayTestSchema)
|
|
380
380
|
},
|
|
381
381
|
{
|
|
382
382
|
id: `${REPORT_REMOTE_PACKAGE}#deeppilot/testPush`,
|
|
@@ -385,11 +385,7 @@ window.__ModuleLoader__.load({
|
|
|
385
385
|
method: "testPush",
|
|
386
386
|
invocation: { kind: "direct" },
|
|
387
387
|
parameters: [],
|
|
388
|
-
result: {
|
|
389
|
-
mode: "strict",
|
|
390
|
-
typeSymbol: `${REPORT_REMOTE_PACKAGE}#PushTestResult`,
|
|
391
|
-
schema: pushTestSchema
|
|
392
|
-
}
|
|
388
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#PushTestResult`, pushTestSchema)
|
|
393
389
|
}
|
|
394
390
|
]
|
|
395
391
|
};
|
|
@@ -436,15 +432,17 @@ window.__ModuleLoader__.load({
|
|
|
436
432
|
"master.on": "已开启:接受手机连接。",
|
|
437
433
|
"master.off": "已关闭:不接受手机连接。",
|
|
438
434
|
"local.title": "局域网连接",
|
|
439
|
-
"local.on": "已开启:手机可通过独立端口 {port}
|
|
435
|
+
"local.on": "已开启:手机可通过独立端口 {port} 以 TLS 加密连接。",
|
|
440
436
|
"local.off": "已关闭:不接受局域网连接。",
|
|
437
|
+
"local.tlsFingerprint": "证书指纹",
|
|
438
|
+
"local.tlsRegenerated": "局域网 TLS 身份已重新生成:此前通过内网配对的设备需要重新扫码配对。",
|
|
441
439
|
"local.phaseDisabled": "局域网连接未启用",
|
|
442
440
|
"local.phaseStarting": "局域网连接正在启动",
|
|
443
441
|
"local.phaseOnline": "局域网连接已就绪",
|
|
444
442
|
"local.phaseError": "局域网连接失败",
|
|
445
443
|
"local.phaseStopped": "局域网连接已停止",
|
|
446
444
|
"local.portTitle": "局域网端口",
|
|
447
|
-
"local.portDescription": "默认 3098,范围 1024–65535
|
|
445
|
+
"local.portDescription": "默认 3098,范围 1024–65535。该端口只以 TLS 提供 DeepPilot 的三个连接端点,不会开放完整 DSH。",
|
|
448
446
|
"local.portApplied": "局域网端口已保存,监听器正在重新加载。",
|
|
449
447
|
"local.portInvalid": "请输入 1024–65535 之间的整数。",
|
|
450
448
|
"local.portFailed": "局域网端口保存失败:",
|
|
@@ -498,14 +496,13 @@ window.__ModuleLoader__.load({
|
|
|
498
496
|
"pair.qrShow": "显示二维码",
|
|
499
497
|
"pair.qrHide": "隐藏二维码",
|
|
500
498
|
"pair.qrGenerating": "生成中…",
|
|
501
|
-
"pair.
|
|
499
|
+
"pair.qrExpired": "配对码已过期,请重新显示二维码",
|
|
502
500
|
"pair.qrFailed": "二维码生成失败:",
|
|
503
|
-
"pair.
|
|
504
|
-
"pair.
|
|
505
|
-
"pair.
|
|
506
|
-
"pair.
|
|
507
|
-
"pair.
|
|
508
|
-
"pair.qrHint": "二维码包含{kind}地址和一次性配对码;配对码 5 分钟后失效,二维码将在 60 秒后自动隐藏。",
|
|
501
|
+
"pair.qrRetry": "重试",
|
|
502
|
+
"pair.infoLabel": "配对信息(复制一次,粘贴到 App 即可)",
|
|
503
|
+
"pair.infoCopyDone": "配对信息已复制",
|
|
504
|
+
"pair.infoCopyFailed": "配对信息复制失败:",
|
|
505
|
+
"pair.qrHint": "二维码与下面的「配对信息」内容相同:包含{kind}地址、一次性配对码、证书指纹(内网时)与主机标识。在局域网与公网之间切换会立即生成对应的新配对信息,配对码 5 分钟后失效。",
|
|
509
506
|
"advanced.summary": "高级信息",
|
|
510
507
|
"advanced.protocolVersion": "协议版本",
|
|
511
508
|
"advanced.serverVersion": "服务器版本",
|
|
@@ -618,15 +615,17 @@ window.__ModuleLoader__.load({
|
|
|
618
615
|
"master.on": "On: accepting phone connections.",
|
|
619
616
|
"master.off": "Off: not accepting phone connections.",
|
|
620
617
|
"local.title": "LAN connection",
|
|
621
|
-
"local.on": "On: phones
|
|
618
|
+
"local.on": "On: phones connect over TLS through the independent port {port}.",
|
|
622
619
|
"local.off": "Off: LAN connections are not accepted.",
|
|
620
|
+
"local.tlsFingerprint": "Certificate fingerprint",
|
|
621
|
+
"local.tlsRegenerated": "The LAN TLS identity was regenerated: devices previously paired over LAN must scan a new pairing QR.",
|
|
623
622
|
"local.phaseDisabled": "LAN connection disabled",
|
|
624
623
|
"local.phaseStarting": "LAN connection starting",
|
|
625
624
|
"local.phaseOnline": "LAN connection ready",
|
|
626
625
|
"local.phaseError": "LAN connection failed",
|
|
627
626
|
"local.phaseStopped": "LAN connection stopped",
|
|
628
627
|
"local.portTitle": "LAN port",
|
|
629
|
-
"local.portDescription": "Default 3098; range 1024–65535. This port
|
|
628
|
+
"local.portDescription": "Default 3098; range 1024–65535. This port serves only the three DeepPilot endpoints over TLS, never the full DSH server.",
|
|
630
629
|
"local.portApplied": "LAN port saved; the listener is reloading.",
|
|
631
630
|
"local.portInvalid": "Enter an integer from 1024 to 65535.",
|
|
632
631
|
"local.portFailed": "Failed to save LAN port: ",
|
|
@@ -680,14 +679,13 @@ window.__ModuleLoader__.load({
|
|
|
680
679
|
"pair.qrShow": "Show QR code",
|
|
681
680
|
"pair.qrHide": "Hide QR code",
|
|
682
681
|
"pair.qrGenerating": "Generating…",
|
|
683
|
-
"pair.
|
|
682
|
+
"pair.qrExpired": "Pairing code expired — show the QR code again",
|
|
684
683
|
"pair.qrFailed": "QR generation failed: ",
|
|
685
|
-
"pair.
|
|
686
|
-
"pair.
|
|
687
|
-
"pair.
|
|
688
|
-
"pair.
|
|
689
|
-
"pair.
|
|
690
|
-
"pair.qrHint": "The QR contains a {kind} address and a single-use pairing code. 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.",
|
|
691
689
|
"advanced.summary": "Advanced info",
|
|
692
690
|
"advanced.protocolVersion": "Protocol version",
|
|
693
691
|
"advanced.serverVersion": "Server version",
|
|
@@ -938,11 +936,29 @@ window.__ModuleLoader__.load({
|
|
|
938
936
|
".pbb-versionFooter a{color:var(--dsw-alias-state-success-primary,#22a06b);text-decoration:none}",
|
|
939
937
|
".pbb-versionFooter a:hover{text-decoration:underline;text-underline-offset:2px}"
|
|
940
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
|
+
*/
|
|
941
956
|
function injectCss() {
|
|
942
957
|
if (typeof document === "undefined") return;
|
|
943
958
|
const id = "dsh-deeppilot/page.css";
|
|
944
959
|
if (document.querySelector("style[data-plugin-css=\"" + id + "\"]") !== null) return;
|
|
945
960
|
const tag = document.createElement("style");
|
|
961
|
+
tag.setAttribute("data-plugin", PLUGIN_ID);
|
|
946
962
|
tag.setAttribute("data-plugin-css", id);
|
|
947
963
|
tag.textContent = CSS;
|
|
948
964
|
document.head.appendChild(tag);
|
|
@@ -3351,18 +3367,25 @@ window.__ModuleLoader__.load({
|
|
|
3351
3367
|
return SvgRenderer.render(data, opts);
|
|
3352
3368
|
});
|
|
3353
3369
|
})))(), 1);
|
|
3354
|
-
|
|
3370
|
+
/** `sha256:` + base64url(SHA-256(SubjectPublicKeyInfo DER)) of the LAN certificate. */
|
|
3371
|
+
const LAN_TLS_FINGERPRINT_PATTERN = /^sha256:[A-Za-z0-9_-]{43}$/;
|
|
3372
|
+
function isLanTlsFingerprint(value) {
|
|
3373
|
+
return typeof value === "string" && LAN_TLS_FINGERPRINT_PATTERN.test(value);
|
|
3374
|
+
}
|
|
3355
3375
|
/**
|
|
3356
3376
|
* Return every explicit DeepPilot transport target. LAN comes first because
|
|
3357
3377
|
* it is private and lower latency; an online Funnel remains available as a
|
|
3358
|
-
* separate choice instead of silently replacing the local address.
|
|
3378
|
+
* separate choice instead of silently replacing the local address. A LAN
|
|
3379
|
+
* listener without a certificate pin is never offered: the app could not
|
|
3380
|
+
* trust it.
|
|
3359
3381
|
*/
|
|
3360
3382
|
function selectPairingTargets(local, remote) {
|
|
3361
3383
|
const targets = [];
|
|
3362
|
-
if (local.phase === "online") {
|
|
3384
|
+
if (local.phase === "online" && isLanTlsFingerprint(local.tlsFingerprint)) {
|
|
3363
3385
|
for (const host of local.endpoints) if (validTargetHost(host)) targets.push({
|
|
3364
3386
|
host,
|
|
3365
|
-
kind: "lan"
|
|
3387
|
+
kind: "lan",
|
|
3388
|
+
tlsFingerprint: local.tlsFingerprint
|
|
3366
3389
|
});
|
|
3367
3390
|
}
|
|
3368
3391
|
if (remote.phase === "online" && remote.publicURL && validTargetHost(remote.publicURL)) targets.push({
|
|
@@ -3372,40 +3395,115 @@ window.__ModuleLoader__.load({
|
|
|
3372
3395
|
const seen = /* @__PURE__ */ new Set();
|
|
3373
3396
|
return targets.filter(({ host }) => !seen.has(host) && seen.add(host));
|
|
3374
3397
|
}
|
|
3398
|
+
/** Only encrypted transports are ever offered to the app. */
|
|
3399
|
+
const TARGET_PROTOCOLS = ["https:", "wss:"];
|
|
3375
3400
|
function validTargetHost(value) {
|
|
3376
3401
|
try {
|
|
3377
3402
|
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 === "";
|
|
3403
|
+
return TARGET_PROTOCOLS.includes(parsed.protocol) && parsed.hostname !== "" && parsed.username === "" && parsed.password === "";
|
|
3384
3404
|
} catch {
|
|
3385
3405
|
return false;
|
|
3386
3406
|
}
|
|
3387
3407
|
}
|
|
3388
|
-
/**
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
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");
|
|
3398
3432
|
if (grant.code.trim().length < 32 || !Number.isInteger(grant.expiresAt) || grant.expiresAt <= Date.now()) throw new TypeError("pairing grant is invalid or expired");
|
|
3399
3433
|
if (!grant.audience.startsWith("deeppilot:")) throw new TypeError("pairing audience is invalid");
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3434
|
+
if (tlsFingerprint !== void 0 && !isLanTlsFingerprint(tlsFingerprint)) throw new TypeError("pairing TLS fingerprint is invalid");
|
|
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
|
|
3407
3506
|
};
|
|
3408
|
-
return JSON.stringify(payload);
|
|
3409
3507
|
}
|
|
3410
3508
|
function normalizeFunnelConnectionLimit(value) {
|
|
3411
3509
|
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 16 ? value : 8;
|
|
@@ -3508,16 +3606,13 @@ window.__ModuleLoader__.load({
|
|
|
3508
3606
|
};
|
|
3509
3607
|
/** Slot component: hooks come from the slot renderer, named use<Key>. */
|
|
3510
3608
|
function DeepPilotSettingsPage(props) {
|
|
3511
|
-
const [addressMessage, setAddressMessage] = (0, react.useState)("");
|
|
3512
3609
|
const [remoteLimitDraft, setRemoteLimitDraft] = (0, react.useState)(String(8));
|
|
3513
3610
|
const [remoteLimitMessage, setRemoteLimitMessage] = (0, react.useState)("");
|
|
3514
3611
|
const [localPortDraft, setLocalPortDraft] = (0, react.useState)(String(DEFAULT_LOCAL_PORT));
|
|
3515
3612
|
const [localPortMessage, setLocalPortMessage] = (0, react.useState)("");
|
|
3516
3613
|
const [selectedPairingHost, setSelectedPairingHost] = (0, react.useState)(null);
|
|
3517
|
-
const [
|
|
3518
|
-
const
|
|
3519
|
-
const [qrBusy, setQRBusy] = (0, react.useState)(false);
|
|
3520
|
-
const [qrMessage, setQRMessage] = (0, react.useState)("");
|
|
3614
|
+
const [pairingPanel, dispatchPairingPanel] = (0, react.useReducer)(pairingPanelReduce, initialPairingPanelState);
|
|
3615
|
+
const qrRequestId = (0, react.useRef)(0);
|
|
3521
3616
|
const [relayTestBusy, setRelayTestBusy] = (0, react.useState)(false);
|
|
3522
3617
|
const [relayTestResult, setRelayTestResult] = (0, react.useState)(null);
|
|
3523
3618
|
const [relayTestError, setRelayTestError] = (0, react.useState)("");
|
|
@@ -3564,11 +3659,6 @@ window.__ModuleLoader__.load({
|
|
|
3564
3659
|
setRelayTestBusy(false);
|
|
3565
3660
|
});
|
|
3566
3661
|
};
|
|
3567
|
-
(0, react.useEffect)(() => {
|
|
3568
|
-
if (!addressMessage) return;
|
|
3569
|
-
const timer = globalThis.setTimeout(() => setAddressMessage(""), 2500);
|
|
3570
|
-
return () => globalThis.clearTimeout(timer);
|
|
3571
|
-
}, [addressMessage]);
|
|
3572
3662
|
(0, react.useEffect)(() => {
|
|
3573
3663
|
if (!remoteLimitMessage) return;
|
|
3574
3664
|
const timer = globalThis.setTimeout(() => setRemoteLimitMessage(""), 4e3);
|
|
@@ -3580,19 +3670,21 @@ window.__ModuleLoader__.load({
|
|
|
3580
3670
|
return () => globalThis.clearTimeout(timer);
|
|
3581
3671
|
}, [localPortMessage]);
|
|
3582
3672
|
(0, react.useEffect)(() => {
|
|
3583
|
-
if (
|
|
3673
|
+
if (pairingPanel.grant === null) return;
|
|
3674
|
+
const remaining = Math.max(0, pairingPanel.grant.expiresAt - Date.now());
|
|
3584
3675
|
const timer = globalThis.setTimeout(() => {
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3676
|
+
dispatchPairingPanel({
|
|
3677
|
+
type: "close",
|
|
3678
|
+
message: translateWith(props.t, "pair.qrExpired")
|
|
3679
|
+
});
|
|
3680
|
+
}, remaining);
|
|
3589
3681
|
return () => globalThis.clearTimeout(timer);
|
|
3590
|
-
}, [
|
|
3682
|
+
}, [pairingPanel.grant]);
|
|
3591
3683
|
(0, react.useEffect)(() => {
|
|
3592
|
-
if (!
|
|
3593
|
-
const timer = globalThis.setTimeout(() =>
|
|
3684
|
+
if (!pairingPanel.notice) return;
|
|
3685
|
+
const timer = globalThis.setTimeout(() => dispatchPairingPanel({ type: "dismissNotice" }), 2500);
|
|
3594
3686
|
return () => globalThis.clearTimeout(timer);
|
|
3595
|
-
}, [
|
|
3687
|
+
}, [pairingPanel.notice]);
|
|
3596
3688
|
const diag = [];
|
|
3597
3689
|
let report = null;
|
|
3598
3690
|
let enabled = true;
|
|
@@ -3694,50 +3786,75 @@ window.__ModuleLoader__.load({
|
|
|
3694
3786
|
};
|
|
3695
3787
|
const pairingTargets = report === null ? [] : selectPairingTargets(report.local, report.remote);
|
|
3696
3788
|
const pairingTarget = pairingTargets.find(({ host }) => host === selectedPairingHost) ?? pairingTargets[0] ?? null;
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
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
|
|
3707
3806
|
});
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
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
|
+
});
|
|
3726
3828
|
}, (error) => {
|
|
3727
|
-
|
|
3728
|
-
|
|
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
|
+
});
|
|
3729
3836
|
};
|
|
3730
3837
|
const hidePairingQR = () => {
|
|
3731
|
-
|
|
3732
|
-
|
|
3838
|
+
qrRequestId.current += 1;
|
|
3839
|
+
dispatchPairingPanel({ type: "close" });
|
|
3733
3840
|
};
|
|
3734
|
-
const
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
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
|
+
});
|
|
3739
3853
|
}, (error) => {
|
|
3740
|
-
|
|
3854
|
+
dispatchPairingPanel({
|
|
3855
|
+
type: "notice",
|
|
3856
|
+
message: translateWith(props.t, "pair.infoCopyFailed") + (error instanceof Error ? error.message : String(error))
|
|
3857
|
+
});
|
|
3741
3858
|
});
|
|
3742
3859
|
};
|
|
3743
3860
|
const primaryRows = [];
|
|
@@ -3805,7 +3922,7 @@ window.__ModuleLoader__.load({
|
|
|
3805
3922
|
role: "img",
|
|
3806
3923
|
"aria-label": report !== null ? translateWith(props.t, LOCAL_PHASE_META[report.local.phase].labelKey) : translateWith(props.t, "phase.unknown"),
|
|
3807
3924
|
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", {
|
|
3925
|
+
}), 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
3926
|
type: "button",
|
|
3810
3927
|
role: "switch",
|
|
3811
3928
|
"aria-checked": localEnabled,
|
|
@@ -3895,32 +4012,36 @@ window.__ModuleLoader__.load({
|
|
|
3895
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", {
|
|
3896
4013
|
type: "button",
|
|
3897
4014
|
className: "pbb-action",
|
|
3898
|
-
disabled:
|
|
4015
|
+
disabled: panel.busy || !report.pairingReady,
|
|
3899
4016
|
onClick: () => {
|
|
3900
|
-
if (
|
|
3901
|
-
else
|
|
4017
|
+
if (panel.open) hidePairingQR();
|
|
4018
|
+
else showPairingQR(pairingTarget);
|
|
3902
4019
|
}
|
|
3903
|
-
},
|
|
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", {
|
|
3904
4021
|
type: "button",
|
|
3905
4022
|
key: target.host,
|
|
3906
4023
|
className: "pbb-action" + (target.host === pairingTarget?.host ? " pbb-actionSelected" : ""),
|
|
4024
|
+
disabled: panel.busy,
|
|
3907
4025
|
onClick: () => {
|
|
3908
4026
|
setSelectedPairingHost(target.host);
|
|
3909
|
-
|
|
4027
|
+
if (panel.open) showPairingQR(target);
|
|
4028
|
+
else hidePairingQR();
|
|
3910
4029
|
}
|
|
3911
|
-
}, (target.kind === "public" ? translateWith(props.t, "pair.kind.public") : translateWith(props.t, "pair.kind.lan")) + " · " + target.host))),
|
|
3912
|
-
className: "pbb-qrImage",
|
|
3913
|
-
src: qrDataURL,
|
|
3914
|
-
alt: translateWith(props.t, "pair.qrAlt")
|
|
3915
|
-
}), (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", {
|
|
3916
4031
|
type: "button",
|
|
3917
4032
|
className: "pbb-action",
|
|
3918
|
-
|
|
3919
|
-
|
|
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", {
|
|
3920
4040
|
type: "button",
|
|
3921
4041
|
className: "pbb-action",
|
|
3922
|
-
onClick:
|
|
3923
|
-
|
|
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", {
|
|
3924
4045
|
type: "button",
|
|
3925
4046
|
className: "pbb-action",
|
|
3926
4047
|
disabled: relayTestBusy,
|