dsh-zen-remote 2.2.9 → 2.3.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.
Files changed (149) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/docs/interface.md +1 -1
  4. package/docs/remote-access.md +20 -0
  5. package/lan-gate.mjs +11 -1
  6. package/lib/adapters/dsh-better-sidebar/index.js +6 -2
  7. package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
  8. package/lib/adapters/dsh-better-sidebar/methods.js +3 -0
  9. package/lib/adapters/dsh-better-sidebar/methods.js.map +1 -1
  10. package/lib/admin-routes.js +33 -2
  11. package/lib/admin-routes.js.map +1 -1
  12. package/lib/client.js +3289 -6
  13. package/lib/config.js +70 -0
  14. package/lib/config.js.map +1 -1
  15. package/lib/index.js +232 -2
  16. package/lib/index.js.map +1 -1
  17. package/lib/intercept.js +43 -3
  18. package/lib/intercept.js.map +1 -1
  19. package/lib/lan-gate-server.cjs +29 -3
  20. package/lib/merge-streams.js +42 -12
  21. package/lib/merge-streams.js.map +1 -1
  22. package/lib/types/adapters/dsh-better-sidebar/index.d.ts +1 -1
  23. package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
  24. package/lib/types/adapters/dsh-better-sidebar/methods.d.ts.map +1 -1
  25. package/lib/types/admin-routes.d.ts +13 -0
  26. package/lib/types/admin-routes.d.ts.map +1 -1
  27. package/lib/types/client/index.d.ts.map +1 -1
  28. package/lib/types/client/locales.d.ts +204 -1
  29. package/lib/types/client/locales.d.ts.map +1 -1
  30. package/lib/types/client/wizard/GatewayWizard.d.ts +29 -0
  31. package/lib/types/client/wizard/GatewayWizard.d.ts.map +1 -0
  32. package/lib/types/client/wizard/WizardCard.d.ts +30 -0
  33. package/lib/types/client/wizard/WizardCard.d.ts.map +1 -0
  34. package/lib/types/client/wizard/cards/MigrateCard.d.ts +15 -0
  35. package/lib/types/client/wizard/cards/MigrateCard.d.ts.map +1 -0
  36. package/lib/types/client/wizard/cards/PortForwardCard.d.ts +15 -0
  37. package/lib/types/client/wizard/cards/PortForwardCard.d.ts.map +1 -0
  38. package/lib/types/client/wizard/cards/ProxyCard.d.ts +19 -0
  39. package/lib/types/client/wizard/cards/ProxyCard.d.ts.map +1 -0
  40. package/lib/types/client/wizard/cards/TokenCard.d.ts +20 -0
  41. package/lib/types/client/wizard/cards/TokenCard.d.ts.map +1 -0
  42. package/lib/types/client/wizard/register-wizard.d.ts +51 -0
  43. package/lib/types/client/wizard/register-wizard.d.ts.map +1 -0
  44. package/lib/types/client/wizard/wizard-css.d.ts +10 -0
  45. package/lib/types/client/wizard/wizard-css.d.ts.map +1 -0
  46. package/lib/types/client-data/qr.d.ts +20 -0
  47. package/lib/types/client-data/qr.d.ts.map +1 -0
  48. package/lib/types/client-data/settings-form.d.ts +7 -2
  49. package/lib/types/client-data/settings-form.d.ts.map +1 -1
  50. package/lib/types/client-data/wizard.d.ts +247 -0
  51. package/lib/types/client-data/wizard.d.ts.map +1 -0
  52. package/lib/types/config.d.ts +27 -0
  53. package/lib/types/config.d.ts.map +1 -1
  54. package/lib/types/index.d.ts.map +1 -1
  55. package/lib/types/intercept.d.ts.map +1 -1
  56. package/lib/types/merge-streams.d.ts.map +1 -1
  57. package/lib/types/wizard/binary-io.d.ts +111 -0
  58. package/lib/types/wizard/binary-io.d.ts.map +1 -0
  59. package/lib/types/wizard/binary.d.ts +118 -0
  60. package/lib/types/wizard/binary.d.ts.map +1 -0
  61. package/lib/types/wizard/caddy-process.d.ts +101 -0
  62. package/lib/types/wizard/caddy-process.d.ts.map +1 -0
  63. package/lib/types/wizard/caddy.d.ts +53 -0
  64. package/lib/types/wizard/caddy.d.ts.map +1 -0
  65. package/lib/types/wizard/cloudflare.d.ts +112 -0
  66. package/lib/types/wizard/cloudflare.d.ts.map +1 -0
  67. package/lib/types/wizard/cloudflared.d.ts +71 -0
  68. package/lib/types/wizard/cloudflared.d.ts.map +1 -0
  69. package/lib/types/wizard/probe.d.ts +66 -0
  70. package/lib/types/wizard/probe.d.ts.map +1 -0
  71. package/lib/types/wizard/proxy-snippets.d.ts +60 -0
  72. package/lib/types/wizard/proxy-snippets.d.ts.map +1 -0
  73. package/lib/types/wizard/routes.d.ts +152 -0
  74. package/lib/types/wizard/routes.d.ts.map +1 -0
  75. package/lib/types/wizard/source-detect.d.ts +53 -0
  76. package/lib/types/wizard/source-detect.d.ts.map +1 -0
  77. package/lib/types/wizard/status.d.ts +115 -0
  78. package/lib/types/wizard/status.d.ts.map +1 -0
  79. package/lib/types/wizard/token-store.d.ts +27 -0
  80. package/lib/types/wizard/token-store.d.ts.map +1 -0
  81. package/lib/types/wizard/tunnel-orchestrator.d.ts +90 -0
  82. package/lib/types/wizard/tunnel-orchestrator.d.ts.map +1 -0
  83. package/lib/types/wizard/verify.d.ts +97 -0
  84. package/lib/types/wizard/verify.d.ts.map +1 -0
  85. package/lib/wizard/binary-io.js +386 -0
  86. package/lib/wizard/binary-io.js.map +1 -0
  87. package/lib/wizard/binary.js +138 -0
  88. package/lib/wizard/binary.js.map +1 -0
  89. package/lib/wizard/caddy-process.js +162 -0
  90. package/lib/wizard/caddy-process.js.map +1 -0
  91. package/lib/wizard/caddy.js +80 -0
  92. package/lib/wizard/caddy.js.map +1 -0
  93. package/lib/wizard/cloudflare.js +334 -0
  94. package/lib/wizard/cloudflare.js.map +1 -0
  95. package/lib/wizard/cloudflared.js +158 -0
  96. package/lib/wizard/cloudflared.js.map +1 -0
  97. package/lib/wizard/probe.js +118 -0
  98. package/lib/wizard/probe.js.map +1 -0
  99. package/lib/wizard/proxy-snippets.js +112 -0
  100. package/lib/wizard/proxy-snippets.js.map +1 -0
  101. package/lib/wizard/routes.js +462 -0
  102. package/lib/wizard/routes.js.map +1 -0
  103. package/lib/wizard/source-detect.js +77 -0
  104. package/lib/wizard/source-detect.js.map +1 -0
  105. package/lib/wizard/status.js +94 -0
  106. package/lib/wizard/status.js.map +1 -0
  107. package/lib/wizard/token-store.js +71 -0
  108. package/lib/wizard/token-store.js.map +1 -0
  109. package/lib/wizard/tunnel-orchestrator.js +155 -0
  110. package/lib/wizard/tunnel-orchestrator.js.map +1 -0
  111. package/lib/wizard/verify.js +171 -0
  112. package/lib/wizard/verify.js.map +1 -0
  113. package/package.json +37 -37
  114. package/scripts/check-settings-form.mjs +5 -4
  115. package/scripts/check-wizard-ui.mjs +690 -0
  116. package/src/adapters/dsh-better-sidebar/index.ts +6 -2
  117. package/src/adapters/dsh-better-sidebar/methods.ts +3 -0
  118. package/src/admin-routes.ts +44 -2
  119. package/src/client/index.tsx +9 -0
  120. package/src/client/locales.ts +410 -2
  121. package/src/client/wizard/GatewayWizard.tsx +2220 -0
  122. package/src/client/wizard/WizardCard.tsx +81 -0
  123. package/src/client/wizard/cards/MigrateCard.tsx +42 -0
  124. package/src/client/wizard/cards/PortForwardCard.tsx +37 -0
  125. package/src/client/wizard/cards/ProxyCard.tsx +56 -0
  126. package/src/client/wizard/cards/TokenCard.tsx +81 -0
  127. package/src/client/wizard/register-wizard.ts +99 -0
  128. package/src/client/wizard/wizard-css.ts +948 -0
  129. package/src/client-data/qr.ts +284 -0
  130. package/src/client-data/settings-form.ts +12 -3
  131. package/src/client-data/wizard.ts +432 -0
  132. package/src/config.ts +87 -0
  133. package/src/index.ts +227 -2
  134. package/src/intercept.ts +43 -3
  135. package/src/merge-streams.ts +41 -12
  136. package/src/wizard/binary-io.ts +449 -0
  137. package/src/wizard/binary.ts +177 -0
  138. package/src/wizard/caddy-process.ts +228 -0
  139. package/src/wizard/caddy.ts +97 -0
  140. package/src/wizard/cloudflare.ts +413 -0
  141. package/src/wizard/cloudflared.ts +196 -0
  142. package/src/wizard/probe.ts +161 -0
  143. package/src/wizard/proxy-snippets.ts +128 -0
  144. package/src/wizard/routes.ts +537 -0
  145. package/src/wizard/source-detect.ts +87 -0
  146. package/src/wizard/status.ts +180 -0
  147. package/src/wizard/token-store.ts +73 -0
  148. package/src/wizard/tunnel-orchestrator.ts +197 -0
  149. package/src/wizard/verify.ts +236 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Wizard status projection (GW-1): declared ingress config + observed
3
+ * runtime facts → one sanitized JSON shape the browser may see.
4
+ *
5
+ * This module is the SECURITY BOUNDARY for everything the wizard surfaces
6
+ * on `GET /_dsh/zen-remote/admin/wizard/status`. The two properties it must
7
+ * hold, regardless of what the callers hand it:
8
+ *
9
+ * 1. No secret material. The Cloudflare API token and the cloudflared
10
+ * tunnel token never enter this module — the runtime side reports only
11
+ * `*Configured` booleans (the files live 0600 under
12
+ * `<DSH_HOME>/zen-remote-ingress/`, and their contents stay there).
13
+ * 2. No file paths. The runtime side knows where binaries, token files
14
+ * and the Caddyfile live; the projection carries none of it — every
15
+ * output field is built from a whitelist below, and input objects are
16
+ * never spread through.
17
+ *
18
+ * Pure logic on purpose: zero imports, so the check scripts and the tests
19
+ * can drive it through Node's strip-only type stripping, and the wizard
20
+ * routes can call it per request without re-resolving anything.
21
+ *
22
+ * Every input is coerced defensively. The declared side normally arrives
23
+ * from `resolveConfig(...).values` (already legal), but the projection is
24
+ * also the last line of defense for the wire: an off-enum mode answers
25
+ * `'off'`, a junk port answers the 443 default, a non-string domain answers
26
+ * `''` — same transparency discipline as the config resolver, applied at
27
+ * the rendering edge.
28
+ */
29
+ /** The ingress modes `resolveConfig` can legally produce. */
30
+ export const WIZARD_INGRESS_MODES = ['off', 'caddy', 'external', 'cloudflared'];
31
+ function modeOf(value) {
32
+ return typeof value === 'string' && WIZARD_INGRESS_MODES.includes(value)
33
+ ? value
34
+ : 'off';
35
+ }
36
+ function stringOf(value) {
37
+ return typeof value === 'string' ? value : '';
38
+ }
39
+ function portOf(value) {
40
+ return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 65535 ? value : null;
41
+ }
42
+ /**
43
+ * Project one probe / verification memory down to its three wire fields.
44
+ * A record without a string `result` is not a memory — it answers null, so
45
+ * a corrupt runtime entry can never render as a half-valid verdict.
46
+ */
47
+ function recordOf(input) {
48
+ if (input === null || input === undefined || typeof input !== 'object')
49
+ return null;
50
+ const { result, deviceId, at } = input;
51
+ if (typeof result !== 'string' || result === '')
52
+ return null;
53
+ return {
54
+ result,
55
+ deviceId: typeof deviceId === 'string' && deviceId !== '' ? deviceId : null,
56
+ at: typeof at === 'number' && Number.isFinite(at) ? at : null,
57
+ };
58
+ }
59
+ /** Project one untrusted-proxy detection down to its two wire fields. */
60
+ function forwardedSourceOf(input) {
61
+ if (input === null || input === undefined || typeof input !== 'object')
62
+ return null;
63
+ const { ip, at } = input;
64
+ if (typeof ip !== 'string' || ip === '')
65
+ return null;
66
+ if (typeof at !== 'number' || !Number.isFinite(at))
67
+ return null;
68
+ return { ip, at };
69
+ }
70
+ /**
71
+ * Build the sanitized wizard status. Both arguments are optional so a
72
+ * composition can answer before anything is configured; the all-empty
73
+ * answer is the honest "wizard not started" state (`mode: 'off'`, no ports,
74
+ * no tokens configured, no memories).
75
+ */
76
+ export function projectWizardStatus(declared = {}, runtime = {}) {
77
+ return {
78
+ ok: true,
79
+ mode: modeOf(declared.ingressMode),
80
+ domain: stringOf(declared.ingressDomain),
81
+ httpsPort: portOf(declared.ingressHttpsPort) ?? 443,
82
+ publicUrl: stringOf(declared.ingressPublicUrl),
83
+ cfTokenConfigured: runtime.cfTokenConfigured === true,
84
+ tunnelTokenConfigured: runtime.tunnelTokenConfigured === true,
85
+ caddyRunning: runtime.caddyRunning === true,
86
+ cloudflaredRunning: runtime.cloudflaredRunning === true,
87
+ gatewayActualPort: portOf(runtime.gatewayActualPort),
88
+ ...(typeof runtime.lanAddress === 'string' && runtime.lanAddress !== '' ? { lanAddress: runtime.lanAddress } : {}),
89
+ lastHostProbe: recordOf(runtime.lastHostProbe),
90
+ lastExternalVerification: recordOf(runtime.lastExternalVerification),
91
+ lastForwardedSource: forwardedSourceOf(runtime.lastForwardedSource),
92
+ };
93
+ }
94
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/wizard/status.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Wizard token storage (GW-13a): the secrets that must never enter the
3
+ * plugin row or any wire response live as 0600 files under
4
+ * `<DSH_HOME>/zen-remote-ingress/`, written through a temp-file rename so
5
+ * a crash mid-write can never leave a half token at the real path. The
6
+ * tunnel-token half lives in cloudflared.ts (it is the cloudflared
7
+ * process's own input); this module owns the shared Cloudflare API token.
8
+ *
9
+ * Reads answer presence or hand the raw string ONLY to in-process callers
10
+ * (the Caddy manager's env) — nothing here is ever logged or serialized
11
+ * into a response.
12
+ */
13
+ import { mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { ingressDir } from './binary.js';
16
+ /** `<DSH_HOME>/zen-remote-ingress/cf-api-token`. */
17
+ export function cfTokenPath(dshHome) {
18
+ return join(ingressDir(dshHome), 'cf-api-token');
19
+ }
20
+ /** Atomic 0600 write (temp sibling + rename), with a direct-write fallback
21
+ * on Windows if an existing file or antivirus scan refuses `renameSync` with
22
+ * EPERM/EACCES. */
23
+ export function writeCfTokenFile(dshHome, token) {
24
+ const finalPath = cfTokenPath(dshHome);
25
+ const partPath = `${finalPath}.part`;
26
+ try {
27
+ mkdirSync(ingressDir(dshHome), { recursive: true });
28
+ writeFileSync(partPath, token, { mode: 0o600 });
29
+ try {
30
+ renameSync(partPath, finalPath);
31
+ }
32
+ catch {
33
+ writeFileSync(finalPath, token, { mode: 0o600 });
34
+ rmSync(partPath, { force: true });
35
+ }
36
+ }
37
+ catch (error) {
38
+ rmSync(partPath, { force: true });
39
+ throw error;
40
+ }
41
+ return finalPath;
42
+ }
43
+ /** The stored token, or undefined when absent/empty. Callers feed it to the
44
+ * Caddy manager's environment; it must never reach a response or a log. */
45
+ export function readCfTokenFile(dshHome) {
46
+ try {
47
+ const token = readRaw(dshHome);
48
+ return token.trim() === '' ? undefined : token;
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ }
54
+ /** Whether a non-empty token file exists (the projection's boolean only). */
55
+ export function cfTokenConfigured(dshHome) {
56
+ try {
57
+ return statSync(cfTokenPath(dshHome)).isFile() && readRaw(dshHome).trim() !== '';
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ }
63
+ /** Delete the stored token (the DELETE route; callers stop the processes
64
+ * that consume it first). */
65
+ export function deleteCfTokenFile(dshHome) {
66
+ rmSync(cfTokenPath(dshHome), { force: true });
67
+ }
68
+ function readRaw(dshHome) {
69
+ return readFileSync(cfTokenPath(dshHome), 'utf8');
70
+ }
71
+ //# sourceMappingURL=token-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-store.js","sourceRoot":"","sources":["../../src/wizard/token-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAC9F,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,oDAAoD;AACpD,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC,CAAA;AAClD,CAAC;AAED;;mBAEmB;AACnB,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,KAAa;IAC7D,MAAM,SAAS,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;IACtC,MAAM,QAAQ,GAAG,GAAG,SAAS,OAAO,CAAA;IACpC,IAAI,CAAC;QACH,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACnD,aAAa,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;QAC/C,IAAI,CAAC;YACH,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,aAAa,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;YAChD,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACnC,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACjC,MAAM,KAAK,CAAA;IACb,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;2EAC2E;AAC3E,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;QAC9B,OAAO,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAA;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAA;IAClF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED;6BAC6B;AAC7B,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;AAC/C,CAAC;AAED,SAAS,OAAO,CAAC,OAAe;IAC9B,OAAO,YAAY,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAAA;AACnD,CAAC"}
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Tunnel orchestration (GW-13b, 路 C's autopilot): one call drives the
3
+ * whole Cloudflare-side setup the design promises — zone/account lookup
4
+ * (through the ancestor-chain check), tunnel create-or-adopt, ingress
5
+ * configuration against the gateway's ACTUAL port, the proxied CNAME,
6
+ * and the cloudflared child.
7
+ *
8
+ * Rules the review and the design pin down:
9
+ *
10
+ * - **已有同名隧道**:the first setup pass answers `tunnel-conflict`
11
+ * with the same-name list; the UI lets the user pick one and re-runs
12
+ * with `tunnelId`, which adopts the existing tunnel through the token
13
+ * endpoint instead of creating anything.
14
+ * - **模式切换**:starting the tunnel stops the managed Caddy first
15
+ * (`stopCaddy`) — never two ingress processes at once.
16
+ * - The connector token travels from the Cloudflare answer into the
17
+ * 0600 token file and the child's `--token-file`; it never reaches a
18
+ * response or a log.
19
+ * - Every failure is a VALUE with a stable `reason` the UI maps to
20
+ * locale copy; CloudflareError codes map losslessly (`cf-*` → the
21
+ * orchestration's own reasons), unknown ones degrade to `api`.
22
+ */
23
+ import { hostOf } from '../config.js';
24
+ import { createCloudflareApi } from './cloudflare.js';
25
+ import { writeTunnelTokenFile } from './cloudflared.js';
26
+ /** The tunnel name the wizard mints and looks for. */
27
+ export const TUNNEL_NAME = 'dsh-zen-remote';
28
+ /** CloudflareError.code → the orchestration's reason vocabulary. */
29
+ function mapCloudflareError(error) {
30
+ if (error !== null && typeof error === 'object' && 'code' in error) {
31
+ const code = error.code;
32
+ const detail = error.message;
33
+ if (code === 'cf-auth-failed')
34
+ return { ok: false, reason: 'auth', detail: String(detail ?? '') };
35
+ if (code === 'cf-network')
36
+ return { ok: false, reason: 'network', detail: String(detail ?? '') };
37
+ if (code === 'cf-zone-not-found')
38
+ return { ok: false, reason: 'zone-not-found' };
39
+ }
40
+ return { ok: false, reason: 'api', detail: String(error?.message ?? error) };
41
+ }
42
+ /**
43
+ * Drive the whole tunnel setup. Throws nothing — every outcome is a value.
44
+ */
45
+ export async function setupTunnel(options, input) {
46
+ const token = options.readToken();
47
+ if (token === undefined || token.trim() === '')
48
+ return { ok: false, reason: 'no-token' };
49
+ const host = hostOf(input.domain);
50
+ if (host === undefined)
51
+ return { ok: false, reason: 'bad-domain' };
52
+ const api = options.apiFactory !== undefined
53
+ ? options.apiFactory(token)
54
+ : createCloudflareApi({ token, fetchImpl: options.fetchImpl });
55
+ // Zone + account: the ancestor-chain check answers both in one verdict.
56
+ let zoneId;
57
+ let accountId;
58
+ try {
59
+ const check = await api.checkDomainOnCloudflare(host);
60
+ if (!check.onCloudflare || check.zoneId === null || check.accountId === null) {
61
+ return { ok: false, reason: 'zone-not-found' };
62
+ }
63
+ zoneId = check.zoneId;
64
+ accountId = check.accountId;
65
+ }
66
+ catch (error) {
67
+ return mapCloudflareError(error);
68
+ }
69
+ // Tunnel: adopt the user's choice, or create a fresh one — but a fresh
70
+ // create refuses when same-name tunnels already exist (the UI offers the
71
+ // list; the re-run carries `tunnelId`).
72
+ let tunnelId;
73
+ let tunnelToken;
74
+ try {
75
+ if (input.tunnelId !== undefined && input.tunnelId !== '') {
76
+ tunnelId = input.tunnelId;
77
+ tunnelToken = await api.getTunnelToken(accountId, tunnelId);
78
+ }
79
+ else {
80
+ const existing = await api.listTunnels(accountId);
81
+ const sameName = existing.filter((tunnel) => tunnel.name === TUNNEL_NAME);
82
+ if (sameName.length > 0)
83
+ return { ok: false, reason: 'tunnel-conflict', existing: sameName };
84
+ const created = await api.createTunnel(accountId, TUNNEL_NAME);
85
+ tunnelId = created.id;
86
+ tunnelToken = created.token;
87
+ }
88
+ }
89
+ catch (error) {
90
+ return mapCloudflareError(error);
91
+ }
92
+ const gatewayPort = await options.gatewayPort();
93
+ // The ingress mapping: the public hostname serves the local gateway.
94
+ try {
95
+ await api.configureIngress(accountId, tunnelId, host, `http://127.0.0.1:${gatewayPort}`);
96
+ }
97
+ catch (error) {
98
+ return mapCloudflareError(error);
99
+ }
100
+ // DNS idempotence next (GW-fix-4/M3): an existing CNAME for this name
101
+ // pointing at the SAME tunnel is a no-op; pointing elsewhere is a real
102
+ // conflict we refuse instead of silently overwriting.
103
+ try {
104
+ const existing = await api.listDnsRecords(zoneId, host);
105
+ const prior = existing.find((record) => record.name === host);
106
+ if (prior !== undefined) {
107
+ const expected = `${tunnelId}.cfargotunnel.com`;
108
+ if (prior.content !== expected) {
109
+ return { ok: false, reason: 'dns-conflict', detail: prior.content };
110
+ }
111
+ }
112
+ else {
113
+ await api.createDnsRecord(zoneId, host, `${tunnelId}.cfargotunnel.com`);
114
+ }
115
+ }
116
+ catch (error) {
117
+ return mapCloudflareError(error);
118
+ }
119
+ // L1: the Cloudflare half is DONE — only now stop the managed Caddy and
120
+ // start the tunnel, so a failed API call leaves the old path untouched.
121
+ options.cloudflared.stopCaddy();
122
+ // Connector secret → 0600 file → child.
123
+ let tokenFile;
124
+ try {
125
+ tokenFile = writeTunnelTokenFile(options.dshHome, tunnelToken);
126
+ }
127
+ catch (error) {
128
+ return { ok: false, reason: 'spawn-failed', detail: String(error?.message ?? error) };
129
+ }
130
+ const started = await options.cloudflared.start({ tunnelToken });
131
+ if (!started.ok) {
132
+ return { ok: false, reason: 'spawn-failed', detail: started.error };
133
+ }
134
+ return { ok: true, tunnelId, publicUrl: `https://${host}`, tokenFile };
135
+ }
136
+ export async function checkDomainOnCloudflare(options, domain) {
137
+ const token = options.readToken();
138
+ if (token === undefined || token.trim() === '')
139
+ return { ok: false, reason: 'no-token' };
140
+ const host = hostOf(domain);
141
+ if (host === undefined)
142
+ return { ok: false, reason: 'bad-domain' };
143
+ const api = options.apiFactory !== undefined
144
+ ? options.apiFactory(token)
145
+ : createCloudflareApi({ token, fetchImpl: options.fetchImpl });
146
+ try {
147
+ const check = await api.checkDomainOnCloudflare(host);
148
+ return { ok: true, onCloudflare: check.onCloudflare, zoneId: check.zoneId, accountId: check.accountId };
149
+ }
150
+ catch (error) {
151
+ const mapped = mapCloudflareError(error);
152
+ return { ok: false, reason: mapped.reason === 'auth' ? 'auth' : mapped.reason === 'network' ? 'network' : 'api', detail: mapped.detail };
153
+ }
154
+ }
155
+ //# sourceMappingURL=tunnel-orchestrator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel-orchestrator.js","sourceRoot":"","sources":["../../src/wizard/tunnel-orchestrator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Final-verification orchestration (GW-9): the wizard's last mile, where
3
+ * 「完成」 means a real phone really paired through the real public URL —
4
+ * never a green light from internal checks alone (设计: 不做已联通的假绿灯).
5
+ *
6
+ * The flow, spread over two entry points the UI drives:
7
+ *
8
+ * start({ publicUrl }): runtime ready? → walk the public URL
9
+ * (GW-8's probe; BOTH the pairing page and the admin-403 shape must be
10
+ * right) → snapshot the gateway's device ids (the baseline) → mint a WEB
11
+ * pairing code → remember the session IN MEMORY and answer with the
12
+ * code, the URL and the expiry. The code never touches the row config,
13
+ * and minting it restarts nothing — trustedProxies changes are what
14
+ * restart the gateway, and any such change invalidates the session
15
+ * instead (the config fingerprint below).
16
+ *
17
+ * check(): re-read the gateway status. A device id the baseline did not
18
+ * have is a freshly paired phone; the gateway's lastWebClaim record must
19
+ * name THAT device with secure === true (the proxy passed
20
+ * X-Forwarded-Proto: https through, so the issued cookie carries the
21
+ * Secure flag). All green → pass. The claim recorded an insecure cookie →
22
+ * cookie-insecure. Past the code's expiry → code-expired. Anything else →
23
+ * pending, keep polling.
24
+ *
25
+ * The session binds to a CONFIG FINGERPRINT (ingressMode, domain,
26
+ * httpsPort, publicUrl, the gateway's ACTUAL port, host, trustedProxies):
27
+ * any of those moving between start and check answers stale and discards
28
+ * the session, because the verification proved a URL that no longer exists.
29
+ * The session is deliberately memory-only — a row reload (which a
30
+ * trustedProxies change causes) builds a fresh manager and the session is
31
+ * gone, exactly the same verdict the fingerprint would have produced.
32
+ */
33
+ import { normalizeServerUrl } from '../client-pairing.js';
34
+ import { probeHostPath } from './probe.js';
35
+ /**
36
+ * The config fingerprint a session binds to: any drift between start and
37
+ * check means the verified URL is no longer the deployed URL.
38
+ */
39
+ export function verificationFingerprint(values, gatewayPort) {
40
+ return JSON.stringify({
41
+ ingressMode: values.ingressMode,
42
+ ingressDomain: values.ingressDomain,
43
+ ingressHttpsPort: values.ingressHttpsPort,
44
+ ingressPublicUrl: values.ingressPublicUrl,
45
+ gatewayPort,
46
+ host: values.host,
47
+ trustedProxies: values.trustedProxies,
48
+ });
49
+ }
50
+ export function createVerificationManager(options) {
51
+ const fetchImpl = options.fetchImpl ?? fetch;
52
+ const timeoutMs = options.timeoutMs ?? 5000;
53
+ const now = options.now ?? Date.now;
54
+ let session = null;
55
+ /** One loopback gateway call AS the local machine. Answer status+body or
56
+ * null on any failure — every caller degrades, none throws. */
57
+ const callGateway = async (method, path, payload) => {
58
+ try {
59
+ const response = await fetchImpl(`${options.gatewayBase}${path}`, {
60
+ method,
61
+ headers: payload === undefined ? undefined : { 'Content-Type': 'application/json' },
62
+ ...(payload === undefined ? {} : { body: JSON.stringify(payload) }),
63
+ signal: AbortSignal.timeout(timeoutMs),
64
+ });
65
+ const body = await response.json();
66
+ return { status: response.status, body };
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ };
72
+ const readStatus = async () => {
73
+ const reply = await callGateway('GET', '/lan-gate/status');
74
+ if (reply === null || reply.status < 200 || reply.status >= 300)
75
+ return null;
76
+ const body = reply.body;
77
+ if (body === null || typeof body !== 'object')
78
+ return null;
79
+ const deviceIds = Array.isArray(body.devices)
80
+ ? body.devices.map((device) => (device !== null && typeof device === 'object' ? device.id : undefined))
81
+ .filter((id) => typeof id === 'string')
82
+ : [];
83
+ const port = typeof body.port === 'number' ? body.port : null;
84
+ const claim = body.lastWebClaim;
85
+ const lastWebClaim = claim !== null && typeof claim === 'object'
86
+ ? {
87
+ id: typeof claim.id === 'string' ? claim.id : '',
88
+ secure: claim.secure === true,
89
+ at: typeof claim.at === 'number' ? claim.at : 0,
90
+ }
91
+ : null;
92
+ return { deviceIds, port, lastWebClaim };
93
+ };
94
+ return {
95
+ async start({ publicUrl, probeOnly }) {
96
+ if (typeof publicUrl !== 'string' || publicUrl.trim() === '')
97
+ return { ok: false, reason: 'no-public-url' };
98
+ // Same admission as the probe: https anywhere, plain http only into
99
+ // the private-network allowances. The URL the phone will open must
100
+ // be one this machine is willing to verify at all.
101
+ const normalized = normalizeServerUrl(publicUrl);
102
+ if (!normalized.ok)
103
+ return { ok: false, reason: 'bad-public-url', detail: normalized.reason };
104
+ if ((await options.runtimeReady?.()) === false)
105
+ return { ok: false, reason: 'runtime-not-ready' };
106
+ const statusBefore = await readStatus();
107
+ if (statusBefore === null)
108
+ return { ok: false, reason: 'gateway-unreachable' };
109
+ const outcome = await (options.probe ?? probeHostPath)(normalized.url);
110
+ if (outcome.kind !== 'reached' || outcome.root !== 'gateway-pairing-page' || outcome.admin !== 'gateway-admin-403') {
111
+ return { ok: false, reason: 'probe-failed', detail: outcome };
112
+ }
113
+ if (probeOnly === true) {
114
+ return { ok: true, pairingCode: '', publicUrl: normalized.url, expiresAt: 0 };
115
+ }
116
+ const pair = await callGateway('POST', '/lan-gate/pair', { role: 'web' });
117
+ const pairBody = pair?.body;
118
+ if (pair === null || pair.status < 200 || pair.status >= 300 || pairBody?.ok !== true || typeof pairBody.code !== 'string' || typeof pairBody.expiresAt !== 'number') {
119
+ return { ok: false, reason: 'pair-refused', detail: pair === null ? null : pair.status };
120
+ }
121
+ session = {
122
+ publicUrl: normalized.url,
123
+ code: pairBody.code,
124
+ expiresAt: pairBody.expiresAt,
125
+ knownDeviceIds: new Set(statusBefore.deviceIds),
126
+ fingerprint: verificationFingerprint(options.getValues(), statusBefore.port),
127
+ };
128
+ return { ok: true, pairingCode: session.code, publicUrl: session.publicUrl, expiresAt: session.expiresAt };
129
+ },
130
+ async check() {
131
+ if (session === null)
132
+ return { result: 'not-started' };
133
+ const status = await readStatus();
134
+ if (status === null)
135
+ return { result: 'pending' };
136
+ // The verified URL must still BE the deployed URL: a config or
137
+ // gateway-port drift discards the session outright.
138
+ if (verificationFingerprint(options.getValues(), status.port) !== session.fingerprint) {
139
+ session = null;
140
+ return { result: 'stale' };
141
+ }
142
+ // New devices first, expiry second: the claim itself proves the code
143
+ // was still valid when it was redeemed, so a poll that lands moments
144
+ // past the expiry must not swallow a pass.
145
+ const newcomers = status.deviceIds.filter((id) => !session.knownDeviceIds.has(id));
146
+ if (newcomers.length > 0) {
147
+ const deviceId = newcomers[newcomers.length - 1];
148
+ const claim = status.lastWebClaim;
149
+ if (claim !== null && claim.id === deviceId) {
150
+ const isHttps = session.publicUrl.startsWith('https://');
151
+ session = null;
152
+ // Plain http:// LAN/Tailscale pairing does not sit behind a TLS
153
+ // reverse proxy and intentionally issues a non-Secure cookie.
154
+ if (isHttps && !claim.secure)
155
+ return { result: 'cookie-insecure', deviceId };
156
+ return { result: 'pass', deviceId, at: claim.at };
157
+ }
158
+ // A claim record that has not surfaced (or names a different,
159
+ // later claim): keep polling rather than bless an unverified cookie.
160
+ return { result: 'pending' };
161
+ }
162
+ if (now() >= session.expiresAt) {
163
+ session = null;
164
+ return { result: 'code-expired' };
165
+ }
166
+ return { result: 'pending' };
167
+ },
168
+ hasSession: () => session !== null,
169
+ };
170
+ }
171
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/wizard/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,aAAa,EAAwB,MAAM,YAAY,CAAA;AA8ChE;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAuB,EAAE,WAA0B;IACzF,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;QACzC,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;QACzC,WAAW;QACX,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,cAAc,EAAE,MAAM,CAAC,cAAc;KACtC,CAAC,CAAA;AACJ,CAAC;AAmBD,MAAM,UAAU,yBAAyB,CAAC,OAAmC;IAC3E,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAA;IAC5C,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,IAAI,CAAA;IAC3C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAA;IACnC,IAAI,OAAO,GAA+B,IAAI,CAAA;IAE9C;mEAC+D;IAC/D,MAAM,WAAW,GAAG,KAAK,EAAE,MAAsB,EAAE,IAAY,EAAE,OAAiB,EAAqD,EAAE;QACvI,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,EAAE,EAAE;gBAChE,MAAM;gBACN,OAAO,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBACnF,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;gBACnE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC;aACvC,CAAC,CAAA;YACF,MAAM,IAAI,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;YAC3C,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,CAAA;QAC1C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC,CAAA;IAED,MAAM,UAAU,GAAG,KAAK,IAAuC,EAAE;QAC/D,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,KAAK,EAAE,kBAAkB,CAAC,CAAA;QAC1D,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG;YAAE,OAAO,IAAI,CAAA;QAC5E,MAAM,IAAI,GAAG,KAAK,CAAC,IAAqE,CAAA;QACxF,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAA;QAC1D,MAAM,SAAS,GAAa,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;YACrD,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAE,MAA2B,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;iBACxH,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,OAAO,EAAE,KAAK,QAAQ,CAAC;YACzD,CAAC,CAAC,EAAE,CAAA;QACN,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;QAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAA;QAC/B,MAAM,YAAY,GAAG,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;YAC9D,CAAC,CAAC;gBACE,EAAE,EAAE,OAAQ,KAA0B,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAE,KAAwB,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;gBAC1F,MAAM,EAAG,KAA8B,CAAC,MAAM,KAAK,IAAI;gBACvD,EAAE,EAAE,OAAQ,KAA0B,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAE,KAAwB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;aAC1F;YACH,CAAC,CAAC,IAAI,CAAA;QACR,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,CAAA;IAC1C,CAAC,CAAA;IAED,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE;YAClC,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,IAAI,EAAE,KAAK,EAAE;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAA;YAC3G,oEAAoE;YACpE,mEAAmE;YACnE,mDAAmD;YACnD,MAAM,UAAU,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAA;YAChD,IAAI,CAAC,UAAU,CAAC,EAAE;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAA;YAE7F,IAAI,CAAC,MAAM,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,KAAK,KAAK;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAA;YAEjG,MAAM,YAAY,GAAG,MAAM,UAAU,EAAE,CAAA;YACvC,IAAI,YAAY,KAAK,IAAI;gBAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAA;YAE9E,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,aAAa,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;YACtE,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,KAAK,sBAAsB,IAAI,OAAO,CAAC,KAAK,KAAK,mBAAmB,EAAE,CAAC;gBACnH,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;YAC/D,CAAC;YAED,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;gBACvB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,SAAS,EAAE,UAAU,CAAC,GAAG,EAAE,SAAS,EAAE,CAAC,EAAE,CAAA;YAC/E,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,MAAM,EAAE,gBAAgB,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;YACzE,MAAM,QAAQ,GAAG,IAAI,EAAE,IAAyE,CAAA;YAChG,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,MAAM,IAAI,GAAG,IAAI,QAAQ,EAAE,EAAE,KAAK,IAAI,IAAI,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,QAAQ,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;gBACrK,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,CAAA;YAC1F,CAAC;YAED,OAAO,GAAG;gBACR,SAAS,EAAE,UAAU,CAAC,GAAG;gBACzB,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,SAAS,EAAE,QAAQ,CAAC,SAAS;gBAC7B,cAAc,EAAE,IAAI,GAAG,CAAC,YAAY,CAAC,SAAS,CAAC;gBAC/C,WAAW,EAAE,uBAAuB,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC;aAC7E,CAAA;YACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAA;QAC5G,CAAC;QAED,KAAK,CAAC,KAAK;YACT,IAAI,OAAO,KAAK,IAAI;gBAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,CAAA;YACtD,MAAM,MAAM,GAAG,MAAM,UAAU,EAAE,CAAA;YACjC,IAAI,MAAM,KAAK,IAAI;gBAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;YACjD,+DAA+D;YAC/D,oDAAoD;YACpD,IAAI,uBAAuB,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC;gBACtF,OAAO,GAAG,IAAI,CAAA;gBACd,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;YAC5B,CAAC;YACD,qEAAqE;YACrE,qEAAqE;YACrE,2CAA2C;YAC3C,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,OAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;YACnF,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACzB,MAAM,QAAQ,GAAG,SAAS,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;gBAChD,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAA;gBACjC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,EAAE,KAAK,QAAQ,EAAE,CAAC;oBAC5C,MAAM,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC,UAAU,CAAC,UAAU,CAAC,CAAA;oBACxD,OAAO,GAAG,IAAI,CAAA;oBACd,gEAAgE;oBAChE,8DAA8D;oBAC9D,IAAI,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM;wBAAE,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,QAAQ,EAAE,CAAA;oBAC5E,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,CAAA;gBACnD,CAAC;gBACD,8DAA8D;gBAC9D,qEAAqE;gBACrE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;YAC9B,CAAC;YACD,IAAI,GAAG,EAAE,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;gBAC/B,OAAO,GAAG,IAAI,CAAA;gBACd,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA;YACnC,CAAC;YACD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;QAC9B,CAAC;QAED,UAAU,EAAE,GAAG,EAAE,CAAC,OAAO,KAAK,IAAI;KACnC,CAAA;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-zen-remote",
3
- "version": "2.2.9",
3
+ "version": "2.3.1",
4
4
  "description": "一个插件、两种角色:主服务端把 DeepSeek Harness 变成手机能安全访问的 PWA(移动端界面、配对码网关、Web Push),并把选定会话共享给已配对的桌面端;子客户端在另一台 DSH 桌面端的侧边栏里远程使用这些会话。One DSH plugin, two roles: the host turns DeepSeek Harness into a phone-reachable PWA (mobile UI, pairing-code gateway behind your own reverse proxy, Web Push) and shares chosen sessions to paired desktops; the client uses those sessions remotely from another DSH desktop app's sidebar.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -117,41 +117,41 @@
117
117
  "devDependencies": {
118
118
  "@deepseek-ai/cordis": "4.0.5",
119
119
  "@deepseek-ai/cosmokit": "1.8.5",
120
- "@deepseek-ai/dsh-agent": "0.2.1-rc.3",
121
- "@deepseek-ai/dsh-api-remotes": "0.2.1-rc.3",
122
- "@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.3",
123
- "@deepseek-ai/dsh-api-workspace-controller": "0.2.1-rc.3",
124
- "@deepseek-ai/dsh-brand": "0.2.1-rc.3",
125
- "@deepseek-ai/dsh-client-connection": "0.2.1-rc.3",
126
- "@deepseek-ai/dsh-client-locale": "0.2.1-rc.3",
127
- "@deepseek-ai/dsh-client-store": "0.2.1-rc.3",
128
- "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.3",
129
- "@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.3",
130
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.3",
131
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.3",
132
- "@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.3",
133
- "@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-rc.3",
134
- "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.3",
135
- "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.3",
136
- "@deepseek-ai/dsh-llm": "0.2.1-rc.3",
137
- "@deepseek-ai/dsh-ptc-runtime": "0.2.1-rc.3",
138
- "@deepseek-ai/dsh-sandbox": "0.2.1-rc.3",
139
- "@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.3",
140
- "@deepseek-ai/dsh-scope": "0.2.1-rc.3",
141
- "@deepseek-ai/dsh-session": "0.2.1-rc.3",
142
- "@deepseek-ai/dsh-session-log-export": "0.2.1-rc.3",
143
- "@deepseek-ai/dsh-session-projection": "0.2.1-rc.3",
144
- "@deepseek-ai/dsh-session-query": "0.2.1-rc.3",
145
- "@deepseek-ai/dsh-session-stats": "0.2.1-rc.3",
146
- "@deepseek-ai/dsh-subagent": "0.2.1-rc.3",
147
- "@deepseek-ai/dsh-timeout": "0.2.1-rc.3",
148
- "@deepseek-ai/dsh-token-meter": "0.2.1-rc.3",
149
- "@deepseek-ai/dsh-tools": "0.2.1-rc.3",
150
- "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.3",
151
- "@deepseek-ai/dsh-util-crypto": "0.2.1-rc.3",
152
- "@deepseek-ai/dsh-util-values": "0.2.1-rc.3",
153
- "@deepseek-ai/dsh-workspace": "0.2.1-rc.3",
154
- "@deepseek-ai/schemastery": "3.18.4",
120
+ "@deepseek-ai/dsh-agent": "0.2.1-rc.4",
121
+ "@deepseek-ai/dsh-api-remotes": "0.2.1-rc.4",
122
+ "@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.4",
123
+ "@deepseek-ai/dsh-api-workspace-controller": "0.2.1-rc.4",
124
+ "@deepseek-ai/dsh-brand": "0.2.1-rc.4",
125
+ "@deepseek-ai/dsh-client-connection": "0.2.1-rc.4",
126
+ "@deepseek-ai/dsh-client-locale": "0.2.1-rc.4",
127
+ "@deepseek-ai/dsh-client-store": "0.2.1-rc.4",
128
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.4",
129
+ "@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.4",
130
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.4",
131
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.4",
132
+ "@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.4",
133
+ "@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-rc.4",
134
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.4",
135
+ "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.4",
136
+ "@deepseek-ai/dsh-llm": "0.2.1-rc.4",
137
+ "@deepseek-ai/dsh-ptc-runtime": "0.2.1-rc.4",
138
+ "@deepseek-ai/dsh-sandbox": "0.2.1-rc.4",
139
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.4",
140
+ "@deepseek-ai/dsh-scope": "0.2.1-rc.4",
141
+ "@deepseek-ai/dsh-session": "0.2.1-rc.4",
142
+ "@deepseek-ai/dsh-session-log-export": "0.2.1-rc.4",
143
+ "@deepseek-ai/dsh-session-projection": "0.2.1-rc.4",
144
+ "@deepseek-ai/dsh-session-query": "0.2.1-rc.4",
145
+ "@deepseek-ai/dsh-session-stats": "0.2.1-rc.4",
146
+ "@deepseek-ai/dsh-subagent": "0.2.1-rc.4",
147
+ "@deepseek-ai/dsh-timeout": "0.2.1-rc.4",
148
+ "@deepseek-ai/dsh-token-meter": "0.2.1-rc.4",
149
+ "@deepseek-ai/dsh-tools": "0.2.1-rc.4",
150
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.4",
151
+ "@deepseek-ai/dsh-util-crypto": "0.2.1-rc.4",
152
+ "@deepseek-ai/dsh-util-values": "0.2.1-rc.4",
153
+ "@deepseek-ai/dsh-workspace": "0.2.1-rc.4",
154
+ "@deepseek-ai/schemastery": "3.18.5",
155
155
  "@types/node": "^26.2.0",
156
156
  "@types/react": "~18.3.1",
157
157
  "@types/react-dom": "~18.3.0",
@@ -160,7 +160,7 @@
160
160
  "scripts": {
161
161
  "build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json && node scripts/build-client.mjs",
162
162
  "verify": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
163
- "test": "node --test \"test/*.test.cjs\" && node scripts/check-sunk-viewport.mjs && node scripts/check-keyboard-avoid.mjs && node scripts/check-attach-upload.mjs && node scripts/check-share-image.mjs && node scripts/check-upload-endpoint.mjs && node scripts/check-host-shadow.mjs && node scripts/check-compat-store.mjs && node scripts/check-settings-form.mjs && node scripts/check-shares.mjs && node scripts/check-plugin-bridge-ui.mjs && node scripts/check-remote-drop.mjs && node scripts/check-client-externals.mjs && node scripts/check-info-sheet-stacking.mjs && node scripts/sync-doc-version.mjs --check",
163
+ "test": "node --test \"test/*.test.cjs\" && node scripts/check-sunk-viewport.mjs && node scripts/check-keyboard-avoid.mjs && node scripts/check-attach-upload.mjs && node scripts/check-share-image.mjs && node scripts/check-upload-endpoint.mjs && node scripts/check-host-shadow.mjs && node scripts/check-compat-store.mjs && node scripts/check-settings-form.mjs && node scripts/check-shares.mjs && node scripts/check-plugin-bridge-ui.mjs && node scripts/check-remote-drop.mjs && node scripts/check-client-externals.mjs && node scripts/check-info-sheet-stacking.mjs && node scripts/check-wizard-ui.mjs && node scripts/sync-doc-version.mjs --check",
164
164
  "version": "node scripts/sync-doc-version.mjs && git add README.md README.zh-CN.md"
165
165
  }
166
166
  }
@@ -619,10 +619,11 @@ test('CLIENT_RECONNECT_ROUTE is the one reconnect endpoint', () => {
619
619
  assert.equal(CLIENT_RECONNECT_ROUTE, '/_dsh/zen-remote/client/reconnect')
620
620
  })
621
621
 
622
- test('normalizePairingCode uppercases and strips spaces and hyphens', () => {
623
- assert.equal(normalizePairingCode('ab cd-12'), 'ABCD12')
624
- assert.equal(normalizePairingCode(' abcd12ef '), 'ABCD12EF')
625
- assert.equal(normalizePairingCode('abcd-12-ef'), 'ABCD12EF')
622
+ test('normalizePairingCode uppercases, filters to the 32-char code alphabet (no 0/O/1/I/L or symbols), and caps at 8 chars', () => {
623
+ assert.equal(normalizePairingCode('ab cd-23'), 'ABCD23')
624
+ assert.equal(normalizePairingCode(' abcd23ef '), 'ABCD23EF')
625
+ assert.equal(normalizePairingCode('abcd-23-ef-gh'), 'ABCD23EF')
626
+ assert.equal(normalizePairingCode('0O1IL!@#$%^&*()_+=abcd2345'), 'ABCD2345')
626
627
  assert.equal(normalizePairingCode(''), '')
627
628
  })
628
629