dsh-zen-remote 2.2.9 → 2.3.2

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 +3293 -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 +58 -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 +2224 -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,161 @@
1
+ /**
2
+ * hostPath probe (GW-8): the server machine walks its OWN public URL and
3
+ * classifies what answers, so the wizard can say something sharper than
4
+ * "it didn't work" — the pairing page showing up means TLS + proxying to
5
+ * the gateway both work; the admin surface answering 200 means the proxy
6
+ * is DROPPING the forwarded headers the gateway trusts; a redirect smells
7
+ * like Cloudflare Access or a proxy rewriting to a login page.
8
+ *
9
+ * Split exactly as the ticket asks: the two `classify*Response` functions
10
+ * are pure (the check scripts can drive them), `probeHostPath` owns the
11
+ * fetch I/O — `redirect: 'manual'` so a proxy's 3xx is SEEN, never
12
+ * followed — with the URL admission reusing client-pairing's
13
+ * `normalizeServerUrl` (https anywhere; plain http only for the explicit
14
+ * private-network allowances) and a 10 s ceiling per request.
15
+ */
16
+
17
+ import { normalizeServerUrl } from '../client-pairing.js'
18
+
19
+ /** What answered on `GET <publicUrl>/`. */
20
+ export type RootClassification =
21
+ | 'gateway-pairing-page'
22
+ | 'unexpected-401'
23
+ | 'unexpected-200'
24
+ | 'redirect'
25
+ | 'unexpected'
26
+
27
+ /** What answered on `GET <publicUrl>/lan-gate/admin`. */
28
+ export type AdminClassification =
29
+ | 'gateway-admin-403'
30
+ | 'admin-exposed'
31
+ | 'proxy-403'
32
+ | 'unexpected'
33
+
34
+ /**
35
+ * `GET /`: the ONE right answer is the gateway's pairing page — 401,
36
+ * text/html, carrying the claim-endpoint marker the page's own script
37
+ * posts to. A 200 means the URL belongs to something else entirely.
38
+ */
39
+ export function classifyRootResponse(status: number, contentType: string, body: string): RootClassification {
40
+ if (status === 401) {
41
+ if (contentType.includes('text/html') && body.includes('/lan-gate/pair/claim')) return 'gateway-pairing-page'
42
+ return 'unexpected-401'
43
+ }
44
+ if (status >= 200 && status < 300) return 'unexpected-200'
45
+ if (status >= 300 && status < 400) return 'redirect'
46
+ return 'unexpected'
47
+ }
48
+
49
+ /**
50
+ * `GET /lan-gate/admin`: right is the gateway's own 403 JSON — a public
51
+ * visitor must not get in. 200 + HTML is the DANGEROUS shape: something
52
+ * answered with a page, meaning the proxy forwarded without the
53
+ * forwarded headers and the gateway's local-only wall never saw a
54
+ * proxied request (the admin page leaked). 403 + HTML smells like
55
+ * Cloudflare Access interposing its own login.
56
+ */
57
+ export function classifyAdminResponse(status: number, contentType: string, body: string): AdminClassification {
58
+ const isHtml = contentType.includes('text/html')
59
+ if (status === 403) {
60
+ if (isHtml) return 'proxy-403'
61
+ if (contentType.includes('application/json')) {
62
+ try {
63
+ const parsed: unknown = JSON.parse(body)
64
+ if (parsed !== null && typeof parsed === 'object' && (parsed as { ok?: unknown }).ok === false) {
65
+ return 'gateway-admin-403'
66
+ }
67
+ } catch {
68
+ // JSON-shaped header with a non-JSON body: not our gateway's answer.
69
+ }
70
+ }
71
+ return 'unexpected'
72
+ }
73
+ if (status >= 200 && status < 300 && isHtml) return 'admin-exposed'
74
+ return 'unexpected'
75
+ }
76
+
77
+ export interface HostPathReached {
78
+ kind: 'reached'
79
+ root: RootClassification
80
+ admin: AdminClassification
81
+ /** Wall-clock cost of both requests, milliseconds. */
82
+ ms: number
83
+ }
84
+
85
+ export interface HostPathUnreachable {
86
+ kind: 'unreachable'
87
+ /** `invalid-url` / `insecure-http` — the URL never stood a chance;
88
+ * `timeout` — the 10 s ceiling fired; `network` — DNS, refused, reset. */
89
+ reason: 'invalid-url' | 'insecure-http' | 'timeout' | 'network'
90
+ detail?: string
91
+ ms: number
92
+ }
93
+
94
+ export type HostPathOutcome = HostPathReached | HostPathUnreachable
95
+
96
+ export interface ProbeOptions {
97
+ fetchImpl?: typeof fetch
98
+ /** Per-request ceiling. Default 10 000 ms (the ticket's value). */
99
+ timeoutMs?: number
100
+ /** Clock for the `ms` measurement (tests). */
101
+ now?: () => number
102
+ }
103
+
104
+ /** One manual-redirect GET, classified. Sends a browser-shaped `Accept:
105
+ * text/html` (never `application/json`, which tells `lan-gate-server.cjs`
106
+ * the caller is an XHR/API client and makes `GET /` answer 401 JSON
107
+ * `{"ok":false,"reason":"unpaired"}` instead of the HTML pairing page;
108
+ * `/lan-gate/admin` answers 403 JSON regardless of `Accept`). */
109
+ async function probeOnce(
110
+ fetchImpl: typeof fetch,
111
+ url: string,
112
+ timeoutMs: number,
113
+ ): Promise<{ status: number; contentType: string; body: string }> {
114
+ const response = await fetchImpl(url, {
115
+ redirect: 'manual',
116
+ signal: AbortSignal.timeout(timeoutMs),
117
+ headers: { Accept: 'text/html' },
118
+ })
119
+ const body = await response.text()
120
+ return { status: response.status, contentType: String(response.headers.get('content-type') ?? ''), body }
121
+ }
122
+
123
+ /**
124
+ * Probe the machine's public URL from here: both the root path and the
125
+ * admin surface, never following a redirect. The two requests run
126
+ * concurrently; the answer carries both classifications and the elapsed
127
+ * time. Every failure mode is a VALUE (`kind: 'unreachable'`), never a
128
+ * throw — the wizard routes render whichever verdict arrived.
129
+ */
130
+ export async function probeHostPath(publicUrl: unknown, options: ProbeOptions = {}): Promise<HostPathOutcome> {
131
+ const started = (options.now ?? Date.now)()
132
+ const elapsed = () => (options.now ?? Date.now)() - started
133
+ // Same admission the client pairing uses: https anywhere, plain http
134
+ // only into the explicit private-network allowances.
135
+ const normalized = normalizeServerUrl(typeof publicUrl === 'string' ? publicUrl : '')
136
+ if (!normalized.ok) {
137
+ return { kind: 'unreachable', reason: normalized.reason === 'insecure-http' ? 'insecure-http' : 'invalid-url', ms: 0 }
138
+ }
139
+ const fetchImpl = options.fetchImpl ?? fetch
140
+ const timeoutMs = options.timeoutMs ?? 10_000
141
+ try {
142
+ const [root, admin] = await Promise.all([
143
+ probeOnce(fetchImpl, `${normalized.url}/`, timeoutMs),
144
+ probeOnce(fetchImpl, `${normalized.url}/lan-gate/admin`, timeoutMs),
145
+ ])
146
+ return {
147
+ kind: 'reached',
148
+ root: classifyRootResponse(root.status, root.contentType, root.body),
149
+ admin: classifyAdminResponse(admin.status, admin.contentType, admin.body),
150
+ ms: elapsed(),
151
+ }
152
+ } catch (error) {
153
+ const name = (error as Error)?.name
154
+ return {
155
+ kind: 'unreachable',
156
+ reason: name === 'TimeoutError' || name === 'AbortError' ? 'timeout' : 'network',
157
+ detail: String((error as Error)?.message ?? error),
158
+ ms: elapsed(),
159
+ }
160
+ }
161
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Reverse-proxy configuration snippets (GW-5, 路 B) — for the user who
3
+ * ALREADY runs their own proxy, the wizard hands out copy-paste material
4
+ * instead of managing a process. Everything here is pure text generation:
5
+ * every interpolated value passes a charset gate (the domain through
6
+ * config.ts's `hostOf`, the upstream host likewise, the port in [1,
7
+ * 65535]), so a hostile form value cannot smuggle a directive into
8
+ * anyone's nginx.
9
+ *
10
+ * The upstream port is the gateway's ACTUAL port — the caller reads it
11
+ * from the running gateway (it may have retried past the configured one),
12
+ * never from config.
13
+ */
14
+
15
+ import { hostOf } from '../config.js'
16
+
17
+ export type SnippetResult =
18
+ | { ok: true; text: string }
19
+ | { ok: false; error: 'bad-domain' | 'bad-upstream-host' | 'bad-upstream-port' }
20
+
21
+ export interface SnippetInput {
22
+ /** Public domain the proxy terminates TLS for (bare hostname). */
23
+ domain: unknown
24
+ /** Where the gateway listens — a hostname or IP literal (e.g. the
25
+ * loopback default). */
26
+ upstreamHost: unknown
27
+ /** The gateway's ACTUAL port. */
28
+ upstreamPort: unknown
29
+ }
30
+
31
+ /** Integers in [1, 65535] only. */
32
+ function portOf(value: unknown): number | undefined {
33
+ return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 65535 ? value : undefined
34
+ }
35
+
36
+ function validatedParts(input: SnippetInput): { domain: string; upstream: string; port: number } | { error: SnippetResult & { ok: false } } {
37
+ const domain = hostOf(input.domain)
38
+ if (domain === undefined) return { error: { ok: false, error: 'bad-domain' } }
39
+ const upstream = hostOf(input.upstreamHost)
40
+ if (upstream === undefined) return { error: { ok: false, error: 'bad-upstream-host' } }
41
+ const port = portOf(input.upstreamPort)
42
+ if (port === undefined) return { error: { ok: false, error: 'bad-upstream-port' } }
43
+ return { domain, upstream, port }
44
+ }
45
+
46
+ /**
47
+ * nginx: a complete server block. The WebSocket upgrade map sits outside
48
+ * the server (http-level context) — the snippet says so in a comment, the
49
+ * user pastes it into the http/server scope their setup offers. Long
50
+ * idle tunnels need the 3600s read/send timeouts, uploads the 128m
51
+ * client body cap, and streaming the buffering switch — all three are
52
+ * the shapes DSH's relay traffic actually needs.
53
+ */
54
+ export function nginxSnippet(input: SnippetInput): SnippetResult {
55
+ const parts = validatedParts(input)
56
+ if ('error' in parts) return parts.error
57
+ const { domain, upstream, port } = parts
58
+ const text = [
59
+ '# 这段放在 http {} 层(若已有同名 map 则跳过)',
60
+ 'map $http_upgrade $connection_upgrade {',
61
+ ' default upgrade;',
62
+ " '' close;",
63
+ '}',
64
+ '',
65
+ 'server {',
66
+ ' listen 443 ssl;',
67
+ ` server_name ${domain};`,
68
+ '',
69
+ ' location / {',
70
+ ` proxy_pass http://${upstream}:${port};`,
71
+ ' proxy_http_version 1.1;',
72
+ ' proxy_set_header Upgrade $http_upgrade;',
73
+ ' proxy_set_header Connection $connection_upgrade;',
74
+ ' proxy_set_header Host $host;',
75
+ ' proxy_set_header X-Real-IP $remote_addr;',
76
+ ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;',
77
+ ' proxy_set_header X-Forwarded-Proto $scheme;',
78
+ ' proxy_buffering off;',
79
+ ' client_max_body_size 128m;',
80
+ ' proxy_read_timeout 3600s;',
81
+ ' proxy_send_timeout 3600s;',
82
+ ' }',
83
+ '}',
84
+ '',
85
+ ].join('\n')
86
+ return { ok: true, text }
87
+ }
88
+
89
+ /**
90
+ * Caddy (user-operated, not the wizard-managed one): the site block is
91
+ * one `reverse_proxy` line — Caddy routes the rest itself.
92
+ */
93
+ export function caddySnippet(input: SnippetInput): SnippetResult {
94
+ const parts = validatedParts(input)
95
+ if ('error' in parts) return parts.error
96
+ const { domain, upstream, port } = parts
97
+ const text = [
98
+ `${domain} {`,
99
+ ` reverse_proxy ${upstream}:${port}`,
100
+ '}',
101
+ '',
102
+ ].join('\n')
103
+ return { ok: true, text }
104
+ }
105
+
106
+ export type StepsResult =
107
+ | { ok: true; steps: string[] }
108
+ | { ok: false; error: 'bad-upstream-host' | 'bad-upstream-port' }
109
+
110
+ /**
111
+ * Lucky (Web 服务反代) three steps: listen 443, proxy to the upstream,
112
+ * flip the catch-all switch. Text only — Lucky has no config file format
113
+ * to generate.
114
+ */
115
+ export function luckySteps(input: { upstreamHost: unknown; upstreamPort: unknown }): StepsResult {
116
+ const upstream = hostOf(input.upstreamHost)
117
+ if (upstream === undefined) return { ok: false, error: 'bad-upstream-host' }
118
+ const port = portOf(input.upstreamPort)
119
+ if (port === undefined) return { ok: false, error: 'bad-upstream-port' }
120
+ return {
121
+ ok: true,
122
+ steps: [
123
+ '在 Lucky 的「Web 服务」里新建一条规则,监听 443 端口,绑定你的域名。',
124
+ `后端(反代目标)填 http://${upstream}:${port},开启 WebSocket 支持。`,
125
+ '打开「万事大吉」开关(放行全部路径),保存即可生效。',
126
+ ],
127
+ }
128
+ }