@linxin666/dsh-remote-web-ui 0.4.1 → 0.4.3
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/README.i18n.yaml +2 -2
- package/README.md +9 -7
- package/README.zh.md +9 -7
- package/lib/client.js +282 -619
- package/lib/client.js.map +1 -1
- package/lib/index.js +418 -901
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +9 -10
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +1 -1
- package/lib/types/client/index.d.ts +32 -3
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +56 -11
- package/lib/types/client/locales.d.ts +0 -84
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +2 -86
- package/lib/types/client/pair-api.d.ts +4 -0
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +11 -7
- package/lib/types/client/remote-channel.d.ts +27 -7
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +25 -17
- package/lib/types/client/settings-entry-form.d.ts +50 -0
- package/lib/types/client/settings-entry-form.d.ts.map +1 -0
- package/lib/types/client/settings-entry-form.js +98 -0
- package/lib/types/index.d.ts +99 -7
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +104 -137
- package/lib/types/lan-bind-plan.d.ts +14 -0
- package/lib/types/lan-bind-plan.d.ts.map +1 -1
- package/lib/types/lan-bind-plan.js +16 -0
- package/lib/types/lan-bind.d.ts +5 -1
- package/lib/types/lan-bind.d.ts.map +1 -1
- package/lib/types/lan-bind.js +65 -2
- package/lib/types/mount-once.d.ts +38 -7
- package/lib/types/mount-once.d.ts.map +1 -1
- package/lib/types/mount-once.js +96 -12
- package/lib/types/pairing.d.ts +15 -2
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +26 -14
- package/lib/types/pairing.persist.test.js +43 -3
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +13 -3
- package/lib/types/remote-channel-rules.d.ts +74 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +104 -0
- package/lib/types/tunnel.d.ts +60 -3
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +136 -3
- package/package.json +21 -20
- package/src/client/RemoteEntry.tsx +8 -10
- package/src/client/RemoteSettingsCard.tsx +0 -2
- package/src/client/index.ts +58 -12
- package/src/client/locales.ts +2 -86
- package/src/client/pair-api.ts +11 -7
- package/src/client/remote-channel.ts +38 -15
- package/src/client/remote.module.css +61 -193
- package/src/client/settings-entry-form.ts +142 -0
- package/src/index.ts +156 -152
- package/src/lan-bind-plan.ts +22 -0
- package/src/lan-bind.ts +63 -2
- package/src/mount-once.ts +107 -13
- package/src/pairing.persist.test.ts +47 -3
- package/src/pairing.ts +26 -12
- package/src/remote-channel-boot.ts +13 -3
- package/src/remote-channel-rules.ts +113 -0
- package/src/tunnel.ts +142 -3
- package/lib/types/client/UpdateEntry.d.ts +0 -14
- package/lib/types/client/UpdateEntry.d.ts.map +0 -1
- package/lib/types/client/UpdateEntry.js +0 -112
- package/lib/types/client/UpdatePanel.d.ts +0 -42
- package/lib/types/client/UpdatePanel.d.ts.map +0 -1
- package/lib/types/client/UpdatePanel.js +0 -101
- package/lib/types/client/update-api.d.ts +0 -29
- package/lib/types/client/update-api.d.ts.map +0 -1
- package/lib/types/client/update-api.js +0 -47
- package/lib/types/console-output.d.ts +0 -41
- package/lib/types/console-output.d.ts.map +0 -1
- package/lib/types/console-output.js +0 -89
- package/lib/types/update-routes.d.ts +0 -30
- package/lib/types/update-routes.d.ts.map +0 -1
- package/lib/types/update-routes.js +0 -47
- package/lib/types/update.d.ts +0 -272
- package/lib/types/update.d.ts.map +0 -1
- package/lib/types/update.js +0 -744
- package/src/client/UpdateEntry.tsx +0 -144
- package/src/client/UpdatePanel.tsx +0 -256
- package/src/client/update-api.ts +0 -49
- package/src/console-output.ts +0 -97
- package/src/update-routes.ts +0 -65
- package/src/update.ts +0 -882
package/lib/types/tunnel.d.ts
CHANGED
|
@@ -12,9 +12,16 @@
|
|
|
12
12
|
*
|
|
13
13
|
* The cloudflared package's Tunnel is a thin spawn wrapper; this manager
|
|
14
14
|
* owns the lifecycle policy (binary readiness, URL timeout, restart
|
|
15
|
-
* backoff) around it. All seams — the
|
|
16
|
-
*
|
|
17
|
-
* a real binary or network.
|
|
15
|
+
* backoff, and the public-URL readiness watchdog) around it. All seams — the
|
|
16
|
+
* tunnel factory, binary readiness, the URL probe, timers — are injectable
|
|
17
|
+
* so the whole lifecycle is unit-testable without a real binary or network.
|
|
18
|
+
*
|
|
19
|
+
* 'running' is not a terminal state: the connector can lose its edge
|
|
20
|
+
* registration while the process keeps living, which leaves a hostname that
|
|
21
|
+
* no longer resolves. The watchdog below probes the public URL of a running
|
|
22
|
+
* tunnel and reuses the crash-restart path when it stops answering, so the
|
|
23
|
+
* relay is re-registered against a freshly minted URL instead of proxying a
|
|
24
|
+
* dead one (issue #1723).
|
|
18
25
|
*/
|
|
19
26
|
/** The observable tunnel lifecycle the settings/panel surfaces render. */
|
|
20
27
|
export type TunnelPhase = 'stopped' | 'starting' | 'running' | 'failed';
|
|
@@ -68,6 +75,21 @@ export interface TunnelHandle {
|
|
|
68
75
|
off(event: string, listener: (...args: any[]) => void): unknown;
|
|
69
76
|
stop(): boolean;
|
|
70
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Probe one public tunnel URL from the host. Resolves true when the URL is
|
|
80
|
+
* served (any HTTP answer below 500 — a harness without credentials answers
|
|
81
|
+
* 401/403, which is a healthy round trip), false for a DNS failure, a refused
|
|
82
|
+
* connection, a timeout, or a Cloudflare 5xx (530 "Origin DNS error" / 1033)
|
|
83
|
+
* — the shapes a tunnel that lost its edge registration produces.
|
|
84
|
+
*
|
|
85
|
+
* The probe must never reject: the watchdog reads a rejection as a dead
|
|
86
|
+
* tunnel and restarts on it, so a local programming error would otherwise
|
|
87
|
+
* recycle working tunnels forever.
|
|
88
|
+
* @param url - the tunnel's public URL.
|
|
89
|
+
* @param timeoutMs - how long one probe may take.
|
|
90
|
+
* @returns whether the URL answered as a live origin.
|
|
91
|
+
*/
|
|
92
|
+
export declare function probeTunnelUrl(url: string, timeoutMs: number): Promise<boolean>;
|
|
71
93
|
/** Injectable seams (defaults are the real cloudflared package + node timers). */
|
|
72
94
|
export interface TunnelManagerOptions {
|
|
73
95
|
/** Spawn one tunnel process for the target (quick or named). */
|
|
@@ -85,6 +107,14 @@ export interface TunnelManagerOptions {
|
|
|
85
107
|
setTimeout(fn: () => void, ms: number): unknown;
|
|
86
108
|
clearTimeout(t: unknown): void;
|
|
87
109
|
};
|
|
110
|
+
/** Probe one public URL; false means the tunnel stopped answering. */
|
|
111
|
+
probe?: (url: string, timeoutMs: number) => Promise<boolean>;
|
|
112
|
+
/** How often to probe the public URL of a running tunnel (0 disables it). */
|
|
113
|
+
healthCheckIntervalMs?: number;
|
|
114
|
+
/** Consecutive probe failures after which the tunnel is restarted. */
|
|
115
|
+
healthCheckFailures?: number;
|
|
116
|
+
/** How long one probe may take. */
|
|
117
|
+
probeTimeoutMs?: number;
|
|
88
118
|
}
|
|
89
119
|
/**
|
|
90
120
|
* Probe whether the binary at `path` actually executes on this machine. The
|
|
@@ -109,6 +139,12 @@ export interface BinaryReadinessSeams {
|
|
|
109
139
|
export declare function createBinaryReadiness(executable: string, seams?: BinaryReadinessSeams): () => Promise<void>;
|
|
110
140
|
/** How many archive downloads one process may attempt before giving up. */
|
|
111
141
|
export declare const MAX_BINARY_INSTALL_ATTEMPTS = 3;
|
|
142
|
+
/** How often a running tunnel's public URL is probed (issue #1723). */
|
|
143
|
+
export declare const DEFAULT_HEALTH_CHECK_INTERVAL_MS = 60000;
|
|
144
|
+
/** Consecutive failed probes before the tunnel is declared dead. */
|
|
145
|
+
export declare const DEFAULT_HEALTH_CHECK_FAILURES = 2;
|
|
146
|
+
/** How long one public-URL probe may take. */
|
|
147
|
+
export declare const DEFAULT_PROBE_TIMEOUT_MS = 10000;
|
|
112
148
|
/**
|
|
113
149
|
* Spawn flags for one quick target. `originHostHeader` stamps the Host the
|
|
114
150
|
* local webserver sees: the plugin's phone-facing fence trusts the configured
|
|
@@ -150,6 +186,10 @@ export declare class TunnelManager {
|
|
|
150
186
|
private readonly restartBaseMs;
|
|
151
187
|
private readonly restartMaxMs;
|
|
152
188
|
private readonly timer;
|
|
189
|
+
private readonly probe;
|
|
190
|
+
private readonly healthCheckIntervalMs;
|
|
191
|
+
private readonly healthCheckFailures;
|
|
192
|
+
private readonly probeTimeoutMs;
|
|
153
193
|
private phase;
|
|
154
194
|
private url;
|
|
155
195
|
private error;
|
|
@@ -157,6 +197,8 @@ export declare class TunnelManager {
|
|
|
157
197
|
private handle;
|
|
158
198
|
private urlTimer;
|
|
159
199
|
private restartTimer;
|
|
200
|
+
private healthTimer;
|
|
201
|
+
private healthFailures;
|
|
160
202
|
private attempts;
|
|
161
203
|
private generation;
|
|
162
204
|
private stopping;
|
|
@@ -189,6 +231,21 @@ export declare class TunnelManager {
|
|
|
189
231
|
onPhase(listener: (info: TunnelInfo) => void): () => void;
|
|
190
232
|
private attempt;
|
|
191
233
|
private handleUrl;
|
|
234
|
+
/**
|
|
235
|
+
* Watch the readiness of a running tunnel. cloudflared can keep its process
|
|
236
|
+
* (and its metrics port) alive while every edge connection is gone, leaving
|
|
237
|
+
* a minted hostname that no longer resolves: the manager used to report
|
|
238
|
+
* 'running' forever and the relay kept forwarding to that dead address, so
|
|
239
|
+
* the phone hit Cloudflare 1016 instead of the relay's offline page
|
|
240
|
+
* (issue #1723). A bounded number of consecutive probe failures ends the
|
|
241
|
+
* attempt through the ordinary fail/backoff path, which mints a new URL and
|
|
242
|
+
* re-announces the relay.
|
|
243
|
+
*/
|
|
244
|
+
private startHealthWatch;
|
|
245
|
+
/** One probe round of the running tunnel's public URL. */
|
|
246
|
+
private healthCheck;
|
|
247
|
+
/** Cancel the readiness watchdog (teardown, restart, stop). */
|
|
248
|
+
private stopHealthWatch;
|
|
192
249
|
private handleExit;
|
|
193
250
|
private fail;
|
|
194
251
|
/** Stop the current process and cancel every pending timer (no phase change). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/tunnel.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/tunnel.ts"],"names":[],"mappings":"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"}
|
package/lib/types/tunnel.js
CHANGED
|
@@ -12,9 +12,16 @@
|
|
|
12
12
|
*
|
|
13
13
|
* The cloudflared package's Tunnel is a thin spawn wrapper; this manager
|
|
14
14
|
* owns the lifecycle policy (binary readiness, URL timeout, restart
|
|
15
|
-
* backoff) around it. All seams — the
|
|
16
|
-
*
|
|
17
|
-
* a real binary or network.
|
|
15
|
+
* backoff, and the public-URL readiness watchdog) around it. All seams — the
|
|
16
|
+
* tunnel factory, binary readiness, the URL probe, timers — are injectable
|
|
17
|
+
* so the whole lifecycle is unit-testable without a real binary or network.
|
|
18
|
+
*
|
|
19
|
+
* 'running' is not a terminal state: the connector can lose its edge
|
|
20
|
+
* registration while the process keeps living, which leaves a hostname that
|
|
21
|
+
* no longer resolves. The watchdog below probes the public URL of a running
|
|
22
|
+
* tunnel and reuses the crash-restart path when it stops answering, so the
|
|
23
|
+
* relay is re-registered against a freshly minted URL instead of proxying a
|
|
24
|
+
* dead one (issue #1723).
|
|
18
25
|
*/
|
|
19
26
|
import { existsSync } from 'node:fs';
|
|
20
27
|
import { spawn } from 'node:child_process';
|
|
@@ -65,6 +72,45 @@ export function namedTunnelHandle(inner, publicUrl) {
|
|
|
65
72
|
};
|
|
66
73
|
return handle;
|
|
67
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Probe one public tunnel URL from the host. Resolves true when the URL is
|
|
77
|
+
* served (any HTTP answer below 500 — a harness without credentials answers
|
|
78
|
+
* 401/403, which is a healthy round trip), false for a DNS failure, a refused
|
|
79
|
+
* connection, a timeout, or a Cloudflare 5xx (530 "Origin DNS error" / 1033)
|
|
80
|
+
* — the shapes a tunnel that lost its edge registration produces.
|
|
81
|
+
*
|
|
82
|
+
* The probe must never reject: the watchdog reads a rejection as a dead
|
|
83
|
+
* tunnel and restarts on it, so a local programming error would otherwise
|
|
84
|
+
* recycle working tunnels forever.
|
|
85
|
+
* @param url - the tunnel's public URL.
|
|
86
|
+
* @param timeoutMs - how long one probe may take.
|
|
87
|
+
* @returns whether the URL answered as a live origin.
|
|
88
|
+
*/
|
|
89
|
+
export async function probeTunnelUrl(url, timeoutMs) {
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
const timer = setTimeout(() => { controller.abort(); }, timeoutMs);
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch(url, {
|
|
94
|
+
method: 'GET',
|
|
95
|
+
redirect: 'manual',
|
|
96
|
+
cache: 'no-store',
|
|
97
|
+
signal: controller.signal,
|
|
98
|
+
});
|
|
99
|
+
// Drain so the connection is reusable instead of half-read.
|
|
100
|
+
await response.body?.cancel().catch(() => undefined);
|
|
101
|
+
return response.status < 500;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Default probe: the real network round trip. */
|
|
111
|
+
async function defaultProbe(url, timeoutMs) {
|
|
112
|
+
return await probeTunnelUrl(url, timeoutMs);
|
|
113
|
+
}
|
|
68
114
|
/** Time box for the `--version` probe that validates a staged binary. */
|
|
69
115
|
const BINARY_PROBE_TIMEOUT_MS = 10_000;
|
|
70
116
|
/**
|
|
@@ -128,6 +174,12 @@ export function createBinaryReadiness(executable, seams = {}) {
|
|
|
128
174
|
}
|
|
129
175
|
/** How many archive downloads one process may attempt before giving up. */
|
|
130
176
|
export const MAX_BINARY_INSTALL_ATTEMPTS = 3;
|
|
177
|
+
/** How often a running tunnel's public URL is probed (issue #1723). */
|
|
178
|
+
export const DEFAULT_HEALTH_CHECK_INTERVAL_MS = 60_000;
|
|
179
|
+
/** Consecutive failed probes before the tunnel is declared dead. */
|
|
180
|
+
export const DEFAULT_HEALTH_CHECK_FAILURES = 2;
|
|
181
|
+
/** How long one public-URL probe may take. */
|
|
182
|
+
export const DEFAULT_PROBE_TIMEOUT_MS = 10_000;
|
|
131
183
|
/** The readiness policy the manager uses by default: the package's real binary path. */
|
|
132
184
|
const defaultBinaryReadiness = createBinaryReadiness(bin);
|
|
133
185
|
/** Default binary readiness: keep a working platform binary at `bin`. */
|
|
@@ -216,6 +268,10 @@ export class TunnelManager {
|
|
|
216
268
|
restartBaseMs;
|
|
217
269
|
restartMaxMs;
|
|
218
270
|
timer;
|
|
271
|
+
probe;
|
|
272
|
+
healthCheckIntervalMs;
|
|
273
|
+
healthCheckFailures;
|
|
274
|
+
probeTimeoutMs;
|
|
219
275
|
phase = 'stopped';
|
|
220
276
|
url;
|
|
221
277
|
error;
|
|
@@ -223,6 +279,8 @@ export class TunnelManager {
|
|
|
223
279
|
handle;
|
|
224
280
|
urlTimer;
|
|
225
281
|
restartTimer;
|
|
282
|
+
healthTimer;
|
|
283
|
+
healthFailures = 0;
|
|
226
284
|
attempts = 0;
|
|
227
285
|
// Generation counter: a stale ensureBinary resolution from an earlier
|
|
228
286
|
// start() must not spawn a second handle after a stop/start cycle.
|
|
@@ -240,6 +298,10 @@ export class TunnelManager {
|
|
|
240
298
|
this.restartBaseMs = options.restartBaseMs ?? 5_000;
|
|
241
299
|
this.restartMaxMs = options.restartMaxMs ?? 60_000;
|
|
242
300
|
this.timer = options.timer ?? nodeTimer;
|
|
301
|
+
this.probe = options.probe ?? defaultProbe;
|
|
302
|
+
this.healthCheckIntervalMs = options.healthCheckIntervalMs ?? DEFAULT_HEALTH_CHECK_INTERVAL_MS;
|
|
303
|
+
this.healthCheckFailures = options.healthCheckFailures ?? DEFAULT_HEALTH_CHECK_FAILURES;
|
|
304
|
+
this.probeTimeoutMs = options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
243
305
|
}
|
|
244
306
|
/** The current status frame. */
|
|
245
307
|
get info() {
|
|
@@ -342,6 +404,9 @@ export class TunnelManager {
|
|
|
342
404
|
this.error = undefined;
|
|
343
405
|
this.attempts = 0;
|
|
344
406
|
this.setPhase('running');
|
|
407
|
+
// The URL is out; from here the connector can still lose its edge
|
|
408
|
+
// registration without the process exiting, so readiness is supervised.
|
|
409
|
+
this.startHealthWatch(value);
|
|
345
410
|
for (const listener of this.urlListeners) {
|
|
346
411
|
try {
|
|
347
412
|
listener(value);
|
|
@@ -351,6 +416,72 @@ export class TunnelManager {
|
|
|
351
416
|
}
|
|
352
417
|
}
|
|
353
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Watch the readiness of a running tunnel. cloudflared can keep its process
|
|
421
|
+
* (and its metrics port) alive while every edge connection is gone, leaving
|
|
422
|
+
* a minted hostname that no longer resolves: the manager used to report
|
|
423
|
+
* 'running' forever and the relay kept forwarding to that dead address, so
|
|
424
|
+
* the phone hit Cloudflare 1016 instead of the relay's offline page
|
|
425
|
+
* (issue #1723). A bounded number of consecutive probe failures ends the
|
|
426
|
+
* attempt through the ordinary fail/backoff path, which mints a new URL and
|
|
427
|
+
* re-announces the relay.
|
|
428
|
+
*/
|
|
429
|
+
startHealthWatch(url) {
|
|
430
|
+
this.stopHealthWatch();
|
|
431
|
+
// A phase listener may have stopped the tunnel or replaced its target
|
|
432
|
+
// while this URL was being announced; only a live running phase schedules.
|
|
433
|
+
if (this.stopping || this.phase !== 'running')
|
|
434
|
+
return;
|
|
435
|
+
if (this.healthCheckIntervalMs <= 0 || this.healthCheckFailures <= 0)
|
|
436
|
+
return;
|
|
437
|
+
this.healthFailures = 0;
|
|
438
|
+
const schedule = () => {
|
|
439
|
+
this.healthTimer = this.timer.setTimeout(() => {
|
|
440
|
+
this.healthTimer = undefined;
|
|
441
|
+
void this.healthCheck(url);
|
|
442
|
+
}, this.healthCheckIntervalMs);
|
|
443
|
+
};
|
|
444
|
+
schedule();
|
|
445
|
+
}
|
|
446
|
+
/** One probe round of the running tunnel's public URL. */
|
|
447
|
+
async healthCheck(url) {
|
|
448
|
+
// The tunnel may have been stopped, failed, or replaced while the probe
|
|
449
|
+
// was in flight; only a still-running tunnel toward this same URL is
|
|
450
|
+
// judged by the result.
|
|
451
|
+
if (this.stopping || this.phase !== 'running' || this.url !== url)
|
|
452
|
+
return;
|
|
453
|
+
let alive = false;
|
|
454
|
+
try {
|
|
455
|
+
alive = await this.probe(url, this.probeTimeoutMs);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
alive = false;
|
|
459
|
+
}
|
|
460
|
+
if (this.stopping || this.phase !== 'running' || this.url !== url)
|
|
461
|
+
return;
|
|
462
|
+
if (alive) {
|
|
463
|
+
this.healthFailures = 0;
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
this.healthFailures += 1;
|
|
467
|
+
if (this.healthFailures >= this.healthCheckFailures) {
|
|
468
|
+
this.fail('the tunnel stopped answering on its public URL');
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
this.healthTimer = this.timer.setTimeout(() => {
|
|
473
|
+
this.healthTimer = undefined;
|
|
474
|
+
void this.healthCheck(url);
|
|
475
|
+
}, this.healthCheckIntervalMs);
|
|
476
|
+
}
|
|
477
|
+
/** Cancel the readiness watchdog (teardown, restart, stop). */
|
|
478
|
+
stopHealthWatch() {
|
|
479
|
+
if (this.healthTimer !== undefined) {
|
|
480
|
+
this.timer.clearTimeout(this.healthTimer);
|
|
481
|
+
this.healthTimer = undefined;
|
|
482
|
+
}
|
|
483
|
+
this.healthFailures = 0;
|
|
484
|
+
}
|
|
354
485
|
handleExit() {
|
|
355
486
|
// The exit handler is detached during teardown, so reaching this point
|
|
356
487
|
// means an unexpected death: fail the current phase and schedule a retry.
|
|
@@ -371,6 +502,7 @@ export class TunnelManager {
|
|
|
371
502
|
this.timer.clearTimeout(this.urlTimer);
|
|
372
503
|
this.urlTimer = undefined;
|
|
373
504
|
}
|
|
505
|
+
this.stopHealthWatch();
|
|
374
506
|
this.setPhase('failed');
|
|
375
507
|
this.attempts += 1;
|
|
376
508
|
const delay = Math.min(this.restartBaseMs * 2 ** (this.attempts - 1), this.restartMaxMs);
|
|
@@ -382,6 +514,7 @@ export class TunnelManager {
|
|
|
382
514
|
/** Stop the current process and cancel every pending timer (no phase change). */
|
|
383
515
|
teardown() {
|
|
384
516
|
this.stopping = true;
|
|
517
|
+
this.stopHealthWatch();
|
|
385
518
|
if (this.urlTimer !== undefined) {
|
|
386
519
|
this.timer.clearTimeout(this.urlTimer);
|
|
387
520
|
this.urlTimer = undefined;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-remote-web-ui",
|
|
3
3
|
"description": "Scan-to-pair remote access for the dsh web GUI that shares one official interface: a QR beside the settings button pairs phones and PCs into the same Web GUI (a portrait-touch adaptation layer for phones, full desktop on PCs) through one-time tokens and revocable device sessions, with a LAN bind toggle, optional Cloudflare tunnel, and one-click family self-update",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.3",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"license": "Apache-2.0",
|
|
39
39
|
"dsh": {
|
|
40
40
|
"engines": {
|
|
41
|
-
"dsh": ">=0.1.7-rc.
|
|
41
|
+
"dsh": ">=0.1.7-rc.2"
|
|
42
42
|
},
|
|
43
43
|
"bundle": {
|
|
44
44
|
"patch": "./cordis.patch.yml"
|
|
@@ -53,35 +53,36 @@
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
+
"@deepseek-ai/schemastery": "^3.18.4",
|
|
56
57
|
"cloudflared": "^0.7.3",
|
|
57
58
|
"clsx": "^2.1.1",
|
|
58
59
|
"qrcode.react": "^4.2.0",
|
|
59
|
-
"
|
|
60
|
+
"yaml": "^2.8.2",
|
|
60
61
|
"zod": "^4.4.3"
|
|
61
62
|
},
|
|
62
63
|
"peerDependencies": {
|
|
63
|
-
"@deepseek-ai/dsh": ">=0.1.7-rc.
|
|
64
|
+
"@deepseek-ai/dsh": ">=0.1.7-rc.2",
|
|
64
65
|
"react": "^18.2.0",
|
|
65
66
|
"react-dom": "^18.2.0"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
69
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
69
|
-
"@deepseek-ai/dsh-agent": "^0.1.7-rc.
|
|
70
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.7-rc.
|
|
71
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.1.7-rc.
|
|
72
|
-
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.7-rc.
|
|
73
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.7-rc.
|
|
74
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.7-rc.
|
|
75
|
-
"@deepseek-ai/dsh-client-store": "^0.1.7-rc.
|
|
76
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.7-rc.
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.
|
|
78
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.
|
|
79
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.7-rc.
|
|
80
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.
|
|
81
|
-
"@deepseek-ai/dsh-commands": "^0.1.7-rc.
|
|
82
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.
|
|
83
|
-
"@deepseek-ai/dsh-invariants": "^0.1.7-rc.
|
|
84
|
-
"@deepseek-ai/dsh-settings": "^0.1.7-rc.
|
|
70
|
+
"@deepseek-ai/dsh-agent": "^0.1.7-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.7-rc.2",
|
|
72
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.1.7-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.7-rc.2",
|
|
74
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.7-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.7-rc.2",
|
|
76
|
+
"@deepseek-ai/dsh-client-store": "^0.1.7-rc.2",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.7-rc.2",
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.2",
|
|
79
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.2",
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.7-rc.2",
|
|
81
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.2",
|
|
82
|
+
"@deepseek-ai/dsh-commands": "^0.1.7-rc.2",
|
|
83
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2",
|
|
84
|
+
"@deepseek-ai/dsh-invariants": "^0.1.7-rc.2",
|
|
85
|
+
"@deepseek-ai/dsh-settings": "^0.1.7-rc.2",
|
|
85
86
|
"@testing-library/dom": "^10.4.1",
|
|
86
87
|
"@testing-library/react": "^16.3.2",
|
|
87
88
|
"@types/node": "^22.20.0",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The sidebar remote-control seat: the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* The sidebar remote-control seat: the phone-icon trigger beside the
|
|
3
|
+
* settings button, and the pairing panel modal. Owns the panel behavior —
|
|
4
|
+
* token minting on open, the status SSE subscription, stop/refresh/copy — and
|
|
5
|
+
* renders the pure {@link RemotePanel} body. The family self-update seat is
|
|
6
|
+
* its own plugin (dsh-update) and no longer rides this row. Component-local
|
|
7
|
+
* state per the client stack rules: nothing here survives remounts or crosses
|
|
8
|
+
* entries.
|
|
9
9
|
*/
|
|
10
10
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
11
11
|
import { createPortal } from 'react-dom'
|
|
@@ -14,7 +14,6 @@ import type { PairingPhase } from '../pairing.ts'
|
|
|
14
14
|
import { RemotePanel, type PanelState } from './RemotePanel.tsx'
|
|
15
15
|
import { copyText, issuePair, revokePair, stopPair, type DeviceFrame, type IssueResponse, type PairStateFrame, type RelayStatusFrame, type TunnelStatusFrame } from './pair-api.ts'
|
|
16
16
|
import { PhoneIcon } from './PhoneIcon.tsx'
|
|
17
|
-
import { UpdateEntry } from './UpdateEntry.tsx'
|
|
18
17
|
import css from './remote.module.css'
|
|
19
18
|
|
|
20
19
|
/** Entry props: the sidebar column state and the standard locale seat. */
|
|
@@ -128,7 +127,7 @@ export function RemoteEntry({ wide, t }: RemoteEntryProps) {
|
|
|
128
127
|
// unreachable origins are fenced out of the events endpoint, so opening
|
|
129
128
|
// it there would just start a doomed reconnect loop.
|
|
130
129
|
if (next.kind !== 'ready' && next.kind !== 'lan-required') return
|
|
131
|
-
const source = new EventSource('
|
|
130
|
+
const source = new EventSource('api/pair/events')
|
|
132
131
|
eventSource.current = source
|
|
133
132
|
source.onmessage = (event) => {
|
|
134
133
|
try {
|
|
@@ -233,7 +232,6 @@ export function RemoteEntry({ wide, t }: RemoteEntryProps) {
|
|
|
233
232
|
return (
|
|
234
233
|
<>
|
|
235
234
|
<div className={css.entryRow} data-rail={wide ? undefined : 'rail'}>
|
|
236
|
-
<UpdateEntry wide={wide} t={t} />
|
|
237
235
|
<TooltipAnchor wide={wide} label={t('entry.label')} onClick={openPanel} expanded={open} />
|
|
238
236
|
</div>
|
|
239
237
|
{open && createPortal((
|
|
@@ -175,8 +175,6 @@ export function RemoteSettingsCard(props: RemoteSettingsCardProps) {
|
|
|
175
175
|
descriptionKey="settings.description"
|
|
176
176
|
defaultOpen={false}
|
|
177
177
|
state={state}
|
|
178
|
-
renderChildrenWhenNotExposed
|
|
179
|
-
hideNotExposedNotice
|
|
180
178
|
onSave={props.save}
|
|
181
179
|
onDiscard={props.discard}
|
|
182
180
|
>
|
package/src/client/index.ts
CHANGED
|
@@ -31,15 +31,17 @@ import { readPairGatePolicy, sendHeartbeat, shouldStopHeartbeat } from './pair-a
|
|
|
31
31
|
import {
|
|
32
32
|
channelTransition,
|
|
33
33
|
installRemoteChannel,
|
|
34
|
-
|
|
34
|
+
pageOriginFacts,
|
|
35
35
|
remoteChannelRequired,
|
|
36
36
|
REMOTE_CHANNEL_BOOT_GLOBAL,
|
|
37
37
|
type RemoteChannelBootSeat,
|
|
38
38
|
} from './remote-channel.ts'
|
|
39
|
+
import { isLocalPage } from '../remote-channel-rules.ts'
|
|
39
40
|
import { FenceNotice } from './FenceNotice.tsx'
|
|
40
41
|
import { reportDailyHeartbeat } from './telemetry.ts'
|
|
41
42
|
import { startMobileAdapt, type RemoteAdaptGlobal } from './mobile-adapt.ts'
|
|
42
43
|
import { installPluginCard } from './plugin-card-seat.ts'
|
|
44
|
+
import { createServedEntryForm } from './settings-entry-form.ts'
|
|
43
45
|
|
|
44
46
|
// Portrait-touch adaptation of the official UI: installed under the plugin
|
|
45
47
|
// lifecycle (apply) so disabling the plugin in cordis patch (disabled: true)
|
|
@@ -50,8 +52,6 @@ export type { PanelState, RemotePanelProps } from './RemotePanel.tsx'
|
|
|
50
52
|
export type { PairFailedNoticeProps } from './PairFailedNotice.tsx'
|
|
51
53
|
export type { RemoteKey } from './locales.ts'
|
|
52
54
|
export type { RemoteSettingsCardFace, RemoteSettingsCardState } from './RemoteSettingsCard.tsx'
|
|
53
|
-
export type { UpdateEntryProps } from './UpdateEntry.tsx'
|
|
54
|
-
export type { UpdatePanelProps, UpdateView } from './UpdatePanel.tsx'
|
|
55
55
|
|
|
56
56
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
57
57
|
interface LocaleNamespaceMap {
|
|
@@ -109,22 +109,70 @@ declare module '@deepseek-ai/cordis' {
|
|
|
109
109
|
/** Dictionary namespace owned by this plugin. */
|
|
110
110
|
const NS = 'remote'
|
|
111
111
|
|
|
112
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Settings namespace the remote-control card edits: the family identity of
|
|
114
|
+
* this plugin's own settings form, and the row id a standalone bundle install
|
|
115
|
+
* carries.
|
|
116
|
+
*/
|
|
113
117
|
const REMOTE_WEB_UI_NS = 'remote-web-ui'
|
|
118
|
+
|
|
119
|
+
/** Profile entry id the family aggregate's generated row carries. */
|
|
114
120
|
const AGGREGATE_ENTRY_ID = 'web-ui-remote-web-ui'
|
|
121
|
+
|
|
122
|
+
/** Profile entry ids this package's patch rows carry, most specific first. */
|
|
115
123
|
const REMOTE_WEB_UI_ENTRY_IDS: readonly string[] = [AGGREGATE_ENTRY_ID, 'ui-remote-web-ui', REMOTE_WEB_UI_NS]
|
|
116
124
|
|
|
117
|
-
|
|
125
|
+
/**
|
|
126
|
+
* The profile entry id this package's own row carries, for a page that serves
|
|
127
|
+
* no family binder.
|
|
128
|
+
*
|
|
129
|
+
* The shared describe mirror answers asynchronously, so at plugin activation it
|
|
130
|
+
* usually holds nothing yet — and an unanswered or empty mirror is not evidence
|
|
131
|
+
* of absence. Binding the bare namespace there left the card bound to an entry
|
|
132
|
+
* the Host does not serve, and the form is bound once per session, so the card
|
|
133
|
+
* never recovered (every save answered `No configurable plugin entry`). The
|
|
134
|
+
* aggregate row id matches nearly every deployment; only a mirror that answers
|
|
135
|
+
* with OTHER plugins' rows falls back to the namespace itself (the pre-0.1.7
|
|
136
|
+
* keying shape), because that answer is the one that actually proves this
|
|
137
|
+
* package's own row is not served.
|
|
138
|
+
* @param forms - the shared configuration forms service.
|
|
139
|
+
* @returns the entry id to bind.
|
|
140
|
+
*/
|
|
141
|
+
export function servedEntryId(forms: ConfigForms): string {
|
|
118
142
|
let served: readonly string[] | undefined
|
|
119
143
|
try {
|
|
120
144
|
served = forms.describe().getSnapshot().view?.namespaces.map(view => view.ns)
|
|
121
145
|
} catch {
|
|
146
|
+
// A mirror that refuses the read is unanswered too, not absent.
|
|
122
147
|
served = undefined
|
|
123
148
|
}
|
|
124
|
-
if (
|
|
149
|
+
if (served === undefined || served.length === 0) return AGGREGATE_ENTRY_ID
|
|
125
150
|
return REMOTE_WEB_UI_ENTRY_IDS.find(id => served.includes(id)) ?? REMOTE_WEB_UI_NS
|
|
126
151
|
}
|
|
127
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Bind the settings form the remote-control card reads and writes.
|
|
155
|
+
*
|
|
156
|
+
* The family binder comes first: it resolves this package's family namespace
|
|
157
|
+
* onto the profile entry id the Host serves the form under, and keeps the
|
|
158
|
+
* loopback bridge as its own fallback. A page without that group (or one where
|
|
159
|
+
* its client half has not applied yet) binds through the shared forms service,
|
|
160
|
+
* on the entry id the describe mirror justifies and rebound as soon as the
|
|
161
|
+
* mirror answers — see {@link createServedEntryForm}.
|
|
162
|
+
* @param ctx - client context carrying the family binder and/or the shared forms service.
|
|
163
|
+
* @returns the form the card stages and saves through.
|
|
164
|
+
*/
|
|
165
|
+
export function bindSettingsForm(ctx: ClientContext): ConfigForm<RemoteSettings> {
|
|
166
|
+
const family = ctx.get('webUiSettings')
|
|
167
|
+
if (family !== undefined && typeof family.bind === 'function') {
|
|
168
|
+
return family.bind<RemoteSettings>({ namespace: REMOTE_WEB_UI_NS })
|
|
169
|
+
}
|
|
170
|
+
return createServedEntryForm<RemoteSettings>({
|
|
171
|
+
forms: ctx.configForms,
|
|
172
|
+
entryIds: REMOTE_WEB_UI_ENTRY_IDS,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
128
176
|
/** Heartbeat cadence from a paired phone (presence + revocation liveness). */
|
|
129
177
|
const HEARTBEAT_INTERVAL_MS = 10_000
|
|
130
178
|
|
|
@@ -203,10 +251,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
203
251
|
// shared form (natively, or over its loopback bridge); without that group the
|
|
204
252
|
// namespace IS the owning entry id, so the official service is addressed
|
|
205
253
|
// directly.
|
|
206
|
-
const
|
|
207
|
-
const settingsForm: ConfigForm<RemoteSettings> = family !== undefined && typeof family.bind === 'function'
|
|
208
|
-
? family.bind<RemoteSettings>({ namespace: REMOTE_WEB_UI_NS })
|
|
209
|
-
: ctx.configForms.get<RemoteSettings>(servedEntryId(ctx.configForms))
|
|
254
|
+
const settingsForm: ConfigForm<RemoteSettings> = bindSettingsForm(ctx)
|
|
210
255
|
const enabled = (): boolean => {
|
|
211
256
|
const snapshot = settingsForm.getSnapshot()
|
|
212
257
|
return snapshot.status === 'ready'
|
|
@@ -333,7 +378,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
333
378
|
showFenceNotice()
|
|
334
379
|
}
|
|
335
380
|
const channelActive = (): boolean => remoteChannelRequired(
|
|
336
|
-
window
|
|
381
|
+
pageOriginFacts(window),
|
|
337
382
|
settingsForm.getSnapshot(),
|
|
338
383
|
hostPairingPolicy,
|
|
339
384
|
)
|
|
@@ -387,7 +432,8 @@ export function apply(ctx: ClientContext): void {
|
|
|
387
432
|
}
|
|
388
433
|
settingsForm.subscribe(syncChannel)
|
|
389
434
|
syncChannel()
|
|
390
|
-
|
|
435
|
+
const pageOrigin = pageOriginFacts(window)
|
|
436
|
+
if (!isLocalPage(pageOrigin.hostname, pageOrigin.protocol, pageOrigin.transportOwnsHost) && settingsForm.getSnapshot().status !== 'ready') {
|
|
391
437
|
void readPairGatePolicy().then((policy) => {
|
|
392
438
|
hostPairingPolicy = policy.requirePairingForLan
|
|
393
439
|
syncChannel()
|