@linxin666/dsh-remote-web-ui 0.4.2 → 0.4.4

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 (79) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +12 -10
  3. package/README.zh.md +12 -10
  4. package/lib/client.js +278 -599
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +396 -913
  7. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  8. package/lib/types/client/RemoteEntry.js +8 -9
  9. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  10. package/lib/types/client/RemoteSettingsCard.js +1 -1
  11. package/lib/types/client/index.d.ts +32 -3
  12. package/lib/types/client/index.d.ts.map +1 -1
  13. package/lib/types/client/index.js +51 -8
  14. package/lib/types/client/locales.d.ts +0 -84
  15. package/lib/types/client/locales.d.ts.map +1 -1
  16. package/lib/types/client/locales.js +0 -84
  17. package/lib/types/client/mobile-adapt.d.ts.map +1 -1
  18. package/lib/types/client/mobile-adapt.js +142 -8
  19. package/lib/types/client/settings-entry-form.d.ts +50 -0
  20. package/lib/types/client/settings-entry-form.d.ts.map +1 -0
  21. package/lib/types/client/settings-entry-form.js +98 -0
  22. package/lib/types/index.d.ts +99 -7
  23. package/lib/types/index.d.ts.map +1 -1
  24. package/lib/types/index.js +150 -149
  25. package/lib/types/lan-bind-plan.d.ts +14 -0
  26. package/lib/types/lan-bind-plan.d.ts.map +1 -1
  27. package/lib/types/lan-bind-plan.js +16 -0
  28. package/lib/types/lan-bind.d.ts +1 -1
  29. package/lib/types/lan-bind.js +1 -1
  30. package/lib/types/mount-once.d.ts +38 -7
  31. package/lib/types/mount-once.d.ts.map +1 -1
  32. package/lib/types/mount-once.js +96 -12
  33. package/lib/types/pairing.d.ts.map +1 -1
  34. package/lib/types/pairing.js +43 -0
  35. package/lib/types/remote-api.d.ts +13 -1
  36. package/lib/types/remote-api.d.ts.map +1 -1
  37. package/lib/types/remote-api.js +14 -1
  38. package/lib/types/tunnel.d.ts +60 -3
  39. package/lib/types/tunnel.d.ts.map +1 -1
  40. package/lib/types/tunnel.js +136 -3
  41. package/package.json +20 -20
  42. package/src/client/RemoteEntry.tsx +7 -9
  43. package/src/client/RemoteSettingsCard.tsx +0 -2
  44. package/src/client/index.ts +53 -9
  45. package/src/client/locales.ts +0 -84
  46. package/src/client/mobile-adapt.ts +144 -5
  47. package/src/client/remote.module.css +50 -200
  48. package/src/client/settings-entry-form.ts +142 -0
  49. package/src/index.ts +202 -164
  50. package/src/lan-bind-plan.ts +22 -0
  51. package/src/lan-bind.ts +1 -1
  52. package/src/mount-once.ts +107 -13
  53. package/src/pairing.ts +44 -1
  54. package/src/remote-api.ts +14 -1
  55. package/src/tunnel.ts +142 -3
  56. package/lib/types/client/UpdateEntry.d.ts +0 -14
  57. package/lib/types/client/UpdateEntry.d.ts.map +0 -1
  58. package/lib/types/client/UpdateEntry.js +0 -112
  59. package/lib/types/client/UpdatePanel.d.ts +0 -42
  60. package/lib/types/client/UpdatePanel.d.ts.map +0 -1
  61. package/lib/types/client/UpdatePanel.js +0 -101
  62. package/lib/types/client/update-api.d.ts +0 -29
  63. package/lib/types/client/update-api.d.ts.map +0 -1
  64. package/lib/types/client/update-api.js +0 -47
  65. package/lib/types/console-output.d.ts +0 -41
  66. package/lib/types/console-output.d.ts.map +0 -1
  67. package/lib/types/console-output.js +0 -89
  68. package/lib/types/update-routes.d.ts +0 -30
  69. package/lib/types/update-routes.d.ts.map +0 -1
  70. package/lib/types/update-routes.js +0 -47
  71. package/lib/types/update.d.ts +0 -282
  72. package/lib/types/update.d.ts.map +0 -1
  73. package/lib/types/update.js +0 -773
  74. package/src/client/UpdateEntry.tsx +0 -144
  75. package/src/client/UpdatePanel.tsx +0 -256
  76. package/src/client/update-api.ts +0 -49
  77. package/src/console-output.ts +0 -97
  78. package/src/update-routes.ts +0 -65
  79. package/src/update.ts +0 -915
@@ -5,38 +5,122 @@
5
5
  * the loader accepts a standalone install of the same package side by side;
6
6
  * without this guard the second instance would still re-register the same
7
7
  * webserver routes, tools, settings namespaces, and system-prompt sections
8
- * and fail the boot. mountOnce makes the second host apply a no-op for the
9
- * lifetime of the first instance (the browser half is already deduped by
10
- * package name in the client module host).
8
+ * and fail the boot. mountOnce makes a mount of an already-mounted package a
9
+ * no-op for as long as the first instance lives (the browser half is already
10
+ * deduped by package name in the client module host).
11
+ *
12
+ * A no-op is only safe while the holder is ALIVE. The holder can be disposed
13
+ * long after the refused mount ran its course: the Host reloads a profile by
14
+ * creating the new loader entries before the old ones are torn down (a
15
+ * plugin-manager enable/disable/install write, a settings-driven row reload,
16
+ * HMR), so the new aggregate shell entry mounts its family plugin while the
17
+ * previous entry still owns the name. Dropping that refused mount lost the
18
+ * plugin for good - the previous entry then disposed its own mount, releasing
19
+ * the name with nobody left to take it, and the family row stayed listed as
20
+ * active while its host routes 404ed (the task-board panel showed
21
+ * "board.hostError.notMounted", no degraded record appeared, and only a Host
22
+ * restart recovered it).
23
+ *
24
+ * The refused mount is therefore QUEUED, not dropped, and replayed the moment
25
+ * the holder releases the name - if the waiting fiber is still alive then. The
26
+ * single-instance guarantee is unchanged: exactly one mount is live per
27
+ * package name, and the replay re-enters the guard so a later mount still
28
+ * dedupes against it.
11
29
  *
12
30
  * The registry rides a global symbol so two module instances of the same
13
- * package (npm copy vs repository link) still share one verdict. cordis
14
- * `ctx.effect` runs its callback immediately and treats the callback's
31
+ * package (npm copy vs repository link) still share one verdict. That symbol
32
+ * is a CROSS-REPOSITORY contract, not this file's private state: the four
33
+ * satellite packages (dsh-skins / dsh-pet / dsh-presets /
34
+ * dsh-community-plugins) are separate repositories carrying their own copy of
35
+ * this guard, rebuilt on their own schedule, so the value under `MOUNTED`
36
+ * must keep the shape every published copy reads (a `Set` of package names
37
+ * with `has`/`add`/`delete`). The wait queues this guard added therefore live
38
+ * under their own additive key, and the registry reads back a `Set` even when
39
+ * some other build left a different value there. Changing `MOUNTED`'s shape
40
+ * in place broke that contract once: a satellite's legacy copy created a
41
+ * `Set`, the family's new copy read it as a `Map`, and every family row
42
+ * mounted after it failed with "claims.get is not a function".
43
+ *
44
+ * cordis `ctx.effect` runs its callback immediately and treats the callback's
15
45
  * return value as the fiber disposer, so the unmarker is returned, not run.
16
46
  */
47
+ /** Published cross-repository contract: package names currently mounted. */
17
48
  const MOUNTED = Symbol.for('dsh-web.mounted-plugins');
49
+ /** Additive key this guard owns: refused mounts waiting for the name. */
50
+ const WAITERS = Symbol.for('dsh-web.mounted-plugins.waiters');
51
+ /**
52
+ * The shared name registry, always a `Set` whatever another build stored here:
53
+ * a foreign value (an interim shape, a hand-written global) must not take every
54
+ * family plugin down with it.
55
+ * @returns the process-wide set of mounted package names.
56
+ */
18
57
  function mountedSet() {
19
58
  const registry = globalThis;
20
- return (registry[MOUNTED] ??= new Set());
59
+ const existing = registry[MOUNTED];
60
+ if (existing instanceof Set)
61
+ return existing;
62
+ const created = new Set();
63
+ registry[MOUNTED] = created;
64
+ return created;
65
+ }
66
+ /** Queue per package name for mounts refused while a holder was alive. */
67
+ function mountWaiters() {
68
+ const registry = globalThis;
69
+ return (registry[WAITERS] ??= new Map());
21
70
  }
22
71
  /**
23
72
  * Wrap a cordis plugin apply so the package runs at most once per process.
24
- * The first mount registers normally and unmarks when its fiber disposes;
25
- * any later mount of the same package name is a no-op.
73
+ * The first mount registers normally and releases the name when its fiber
74
+ * disposes; a mount refused while that name is held waits for the release and
75
+ * then runs, unless its own fiber disposes first.
26
76
  * @param packageName - npm package identity shared by every install source.
27
77
  * @param fn - the original plugin apply.
28
78
  * @returns an apply of the same shape.
29
79
  */
30
80
  export function mountOnce(packageName, fn) {
31
- return ((...args) => {
81
+ const mount = (...args) => {
32
82
  const mounted = mountedSet();
33
- if (mounted.has(packageName))
83
+ const ctx = args[0];
84
+ if (mounted.has(packageName)) {
85
+ // Another fiber (the aggregate row, a standalone install, or a copy built
86
+ // from an older revision of this guard) owns the name. Queue instead of
87
+ // dropping: dropping lost the plugin for the rest of the process when the
88
+ // holder was disposed by a reload.
89
+ const waiters = mountWaiters();
90
+ const queue = waiters.get(packageName) ?? [];
91
+ let alive = true;
92
+ const pending = {
93
+ run: () => {
94
+ if (alive)
95
+ mount(...args);
96
+ },
97
+ };
98
+ // A fiber that dies while it waits must not be replayed afterwards: its
99
+ // context is disposed and registering effects on it would throw.
100
+ ctx?.effect?.(() => () => {
101
+ alive = false;
102
+ const index = queue.indexOf(pending);
103
+ if (index >= 0)
104
+ queue.splice(index, 1);
105
+ });
106
+ queue.push(pending);
107
+ waiters.set(packageName, queue);
34
108
  return;
109
+ }
35
110
  mounted.add(packageName);
36
- const ctx = args[0];
37
111
  ctx?.effect?.(() => () => {
38
112
  mounted.delete(packageName);
113
+ const waiters = mountWaiters();
114
+ const queue = waiters.get(packageName);
115
+ if (queue === undefined)
116
+ return;
117
+ waiters.delete(packageName);
118
+ // Defer the handover one microtask so the disposing mount's own effects
119
+ // (routes, ledger, timers) are torn down before the next one registers.
120
+ for (const waiter of queue.splice(0))
121
+ queueMicrotask(() => { waiter.run(); });
39
122
  });
40
123
  return fn(...args);
41
- });
124
+ };
125
+ return mount;
42
126
  }
@@ -1 +1 @@
1
- {"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"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"}
@@ -509,6 +509,7 @@ function snapshotsEqual(a, b) {
509
509
  && a.publicUrl === b.publicUrl
510
510
  && tunnelEqual(a.tunnel, b.tunnel)
511
511
  && relayEqual(a.relay, b.relay)
512
+ && postureEqual(a.posture, b.posture)
512
513
  && a.tokenId === b.tokenId
513
514
  && a.tokenExpiresAt === b.tokenExpiresAt
514
515
  && a.deviceCount === b.deviceCount
@@ -537,6 +538,48 @@ function relayEqual(a, b) {
537
538
  return a === b || (a !== undefined && b !== undefined
538
539
  && a.state === b.state && a.url === b.url && a.error === b.error);
539
540
  }
541
+ /**
542
+ * Posture equality. A probe round can change this frame while every other field
543
+ * stays put — the common case, since the probe re-runs on a settings change and
544
+ * on a tunnel reaching running, and a quick-tunnel restart churns the probed
545
+ * host set. Leaving the frame out of the comparison suppressed those emits, so
546
+ * the loopback SSE stream and the desktop panel could sit on a stale "exposed"
547
+ * (or stale "clean") verdict for as long as the phase and roster stayed put.
548
+ */
549
+ function postureEqual(a, b) {
550
+ if (a === b)
551
+ return true;
552
+ // A frame that is not an object at all (a hand-built context can pass
553
+ // anything) counts as changed instead of throwing out of the setter.
554
+ if (!isPostureFrame(a) || !isPostureFrame(b))
555
+ return false;
556
+ return a.checkedAt === b.checkedAt && hostsEqual(a.hosts, b.hosts);
557
+ }
558
+ /** Whether a value has the shape a posture frame is compared on. */
559
+ function isPostureFrame(value) {
560
+ return typeof value === 'object' && value !== null;
561
+ }
562
+ /**
563
+ * Per-host posture equality (a round's verdicts, in probe order). Tolerates a
564
+ * frame that is not the expected shape: this runs inside the emit path, outside
565
+ * the listener error containment, so a malformed snapshot handed in by another
566
+ * host-fiber caller must degrade to "changed" instead of throwing out of the
567
+ * setter that produced it.
568
+ */
569
+ function hostsEqual(a, b) {
570
+ if (a === b)
571
+ return true;
572
+ if (!Array.isArray(a) || !Array.isArray(b))
573
+ return false;
574
+ return a.length === b.length && a.every((host, index) => {
575
+ const other = b[index];
576
+ return isPostureHost(host) && isPostureHost(other) && host.host === other.host && host.exposed === other.exposed;
577
+ });
578
+ }
579
+ /** Whether a value has the shape one probed host is compared on. */
580
+ function isPostureHost(value) {
581
+ return typeof value === 'object' && value !== null;
582
+ }
540
583
  /** Element-wise string list equality (interface order is meaningful). */
541
584
  function sameStrings(a, b) {
542
585
  return a.length === b.length && a.every((value, index) => value === b[index]);
@@ -70,7 +70,19 @@ export declare function pairedDeviceIdOf(req: IncomingMessage, service: PairingS
70
70
  */
71
71
  export declare function makeRemoteApiRoutes(deps: RemoteApiDeps): WebRoute[];
72
72
  /**
73
- * Map one outer upgrade URL onto the loopback path (query string included).
73
+ * Map one outer upgrade URL onto the loopback path, dropping the cookieless
74
+ * device credential on the way in.
75
+ *
76
+ * The outer handshake carries the device id as `?device=<id>` because WebSocket
77
+ * handshakes cannot carry headers from the Web API. That id is a live session
78
+ * credential — it opens the full host API through this same channel — and the
79
+ * inner leg is a request the rest of the loopback origin sees (on a WebSocket
80
+ * leg, a raw handshake request line written to a loopback socket), so forwarding
81
+ * it handed a working credential to every other observer of the local port for
82
+ * no benefit: the gate above has already authenticated the caller, and no
83
+ * upstream route reads the parameter (the gateway mux matches on path alone).
84
+ * The rest of the query rides through unchanged, so a caller's own parameters
85
+ * still reach the upstream.
74
86
  */
75
87
  export declare function upgradeInnerPath(reqUrl: string | undefined, fallbackPath: string): string;
76
88
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/remote-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,WAAW,CAAA;AAEhE,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAUhD,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,aAAa,EACb,oBAAoB,EACpB,2BAA2B,EAC3B,eAAe,GAChB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAgBvD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUhE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAQlG;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CAwCnE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAWzF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,GAAG,eAAe,EAAE,CAyCjF"}
1
+ {"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/remote-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,WAAW,CAAA;AAEhE,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAUhD,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,aAAa,EACb,oBAAoB,EACpB,2BAA2B,EAC3B,eAAe,GAChB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAgBvD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUhE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAQlG;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CAwCnE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAYzF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,GAAG,eAAe,EAAE,CAyCjF"}
@@ -137,7 +137,19 @@ export function makeRemoteApiRoutes(deps) {
137
137
  return [{ kind: 'prefix', path: REMOTE_PREFIX, handler }];
138
138
  }
139
139
  /**
140
- * Map one outer upgrade URL onto the loopback path (query string included).
140
+ * Map one outer upgrade URL onto the loopback path, dropping the cookieless
141
+ * device credential on the way in.
142
+ *
143
+ * The outer handshake carries the device id as `?device=<id>` because WebSocket
144
+ * handshakes cannot carry headers from the Web API. That id is a live session
145
+ * credential — it opens the full host API through this same channel — and the
146
+ * inner leg is a request the rest of the loopback origin sees (on a WebSocket
147
+ * leg, a raw handshake request line written to a loopback socket), so forwarding
148
+ * it handed a working credential to every other observer of the local port for
149
+ * no benefit: the gate above has already authenticated the caller, and no
150
+ * upstream route reads the parameter (the gateway mux matches on path alone).
151
+ * The rest of the query rides through unchanged, so a caller's own parameters
152
+ * still reach the upstream.
141
153
  */
142
154
  export function upgradeInnerPath(reqUrl, fallbackPath) {
143
155
  if (reqUrl === undefined || reqUrl === '')
@@ -152,6 +164,7 @@ export function upgradeInnerPath(reqUrl, fallbackPath) {
152
164
  const inner = innerPathOf(url.pathname);
153
165
  if (inner === undefined)
154
166
  return fallbackPath;
167
+ url.searchParams.delete(REMOTE_DEVICE_QUERY);
155
168
  return `${inner}${url.search}`;
156
169
  }
157
170
  /**
@@ -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 tunnel factory, binary readiness,
16
- * timers — are injectable so the whole lifecycle is unit-testable without
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;;;;;;;;;;;;;;;;;GAiBG;AAOH,0EAA0E;AAC1E,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;AAEvE;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAA;AAQvD,6EAA6E;AAC7E,UAAU,kBAAkB;IAC1B,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAA;IACrD,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,KAAK,IAAI,GAAG,OAAO,CAAA;IAClG,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAA;IAC7D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;IAC/D,IAAI,IAAI,OAAO,CAAA;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,CA4B5F;AAED,+BAA+B;AAC/B,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAA;IAClB,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;IAC9D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;IAC/D,IAAI,IAAI,OAAO,CAAA;CAChB;AAED,kFAAkF;AAClF,MAAM,WAAW,oBAAoB;IACnC,gEAAgE;IAChE,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,YAAY,CAAA;IAChD,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wCAAwC;IACxC,KAAK,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAA;CAC5F;AAKD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAU/D;AAED,8EAA8E;AAC9E,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;IAClC,IAAI,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC/C,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAC7C;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,GAAE,oBAAyB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CA+B/G;AAED,2EAA2E;AAC3E,eAAO,MAAM,2BAA2B,IAAI,CAAA;AAa5C;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAI5F;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,GAAG,CAAC,CAS5E;AA0BD;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwC;IAChE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqF;IAE3G,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,GAAG,CAAoB;IAC/B,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAI;IAGpB,OAAO,CAAC,UAAU,CAAI;IACtB,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmC;IAChE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwC;IAEvE;;OAEG;gBACS,OAAO,GAAE,oBAAyB;IAS9C,gCAAgC;IAChC,IAAI,IAAI,IAAI,UAAU,CAMrB;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,GAAG,IAAI;IAW1C,uDAAuD;IACvD,IAAI,IAAI,IAAI;IAOZ,wDAAwD;IACxD,OAAO,IAAI,IAAI;IAIf,4EAA4E;IAC5E,KAAK,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAKlD,uCAAuC;IACvC,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI;IAKzD,OAAO,CAAC,OAAO;IAqCf,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,UAAU;IAOlB,OAAO,CAAC,IAAI;IAqBZ,iFAAiF;IACjF,OAAO,CAAC,QAAQ;IAgBhB,OAAO,CAAC,QAAQ;CAWjB"}
1
+ {"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/tunnel.ts"],"names":[],"mappings":"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"}
@@ -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 tunnel factory, binary readiness,
16
- * timers — are injectable so the whole lifecycle is unit-testable without
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;