@linxin666/dsh-remote-web-ui 0.3.12 → 0.3.14

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 (65) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +26 -6
  3. package/README.zh.md +26 -6
  4. package/lib/client.js +77 -12
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +412 -52
  7. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  8. package/lib/types/client/RemoteEntry.js +1 -0
  9. package/lib/types/client/RemotePanel.d.ts +3 -1
  10. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  11. package/lib/types/client/RemotePanel.js +1 -1
  12. package/lib/types/client/RemoteSettingsCard.d.ts +16 -0
  13. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  14. package/lib/types/client/RemoteSettingsCard.js +6 -2
  15. package/lib/types/client/locales.d.ts +12 -0
  16. package/lib/types/client/locales.d.ts.map +1 -1
  17. package/lib/types/client/locales.js +16 -4
  18. package/lib/types/client/pair-api.d.ts +10 -0
  19. package/lib/types/client/pair-api.d.ts.map +1 -1
  20. package/lib/types/client/remote-channel.d.ts +0 -2
  21. package/lib/types/client/remote-channel.d.ts.map +1 -1
  22. package/lib/types/client/remote-channel.js +0 -4
  23. package/lib/types/index.d.ts +28 -2
  24. package/lib/types/index.d.ts.map +1 -1
  25. package/lib/types/index.js +138 -23
  26. package/lib/types/pairing.d.ts +17 -0
  27. package/lib/types/pairing.d.ts.map +1 -1
  28. package/lib/types/pairing.js +13 -0
  29. package/lib/types/relay-registry.d.ts +97 -0
  30. package/lib/types/relay-registry.d.ts.map +1 -0
  31. package/lib/types/relay-registry.js +191 -0
  32. package/lib/types/remote-api.d.ts +1 -1
  33. package/lib/types/remote-api.d.ts.map +1 -1
  34. package/lib/types/remote-api.js +1 -1
  35. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  36. package/lib/types/remote-channel-boot.js +0 -1
  37. package/lib/types/remote-channel-rules.d.ts +0 -1
  38. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  39. package/lib/types/remote-channel-rules.js +0 -1
  40. package/lib/types/remote-methods.d.ts +1 -4
  41. package/lib/types/remote-methods.d.ts.map +1 -1
  42. package/lib/types/remote-methods.js +1 -5
  43. package/lib/types/tunnel-plan.d.ts +36 -0
  44. package/lib/types/tunnel-plan.d.ts.map +1 -0
  45. package/lib/types/tunnel-plan.js +43 -0
  46. package/lib/types/tunnel.d.ts +66 -13
  47. package/lib/types/tunnel.d.ts.map +1 -1
  48. package/lib/types/tunnel.js +93 -21
  49. package/package.json +18 -18
  50. package/src/client/RemoteEntry.tsx +2 -1
  51. package/src/client/RemotePanel.tsx +11 -0
  52. package/src/client/RemoteSettingsCard.tsx +43 -1
  53. package/src/client/locales.ts +16 -4
  54. package/src/client/pair-api.ts +11 -0
  55. package/src/client/remote-channel.ts +0 -3
  56. package/src/client/settings-card.module.css +14 -8
  57. package/src/index.ts +153 -25
  58. package/src/pairing.ts +30 -0
  59. package/src/relay-registry.ts +232 -0
  60. package/src/remote-api.ts +0 -1
  61. package/src/remote-channel-boot.ts +0 -1
  62. package/src/remote-channel-rules.ts +0 -2
  63. package/src/remote-methods.ts +1 -6
  64. package/src/tunnel-plan.ts +51 -0
  65. package/src/tunnel.ts +120 -24
@@ -22,6 +22,8 @@ import { claimPostureKey, postureTargets, probePosture, releasePostureKey } from
22
22
  import { lanIPv4Addresses } from "./lan.js";
23
23
  import { ensureFirewallRule, firewallSummary, removeFirewallRule } from "./firewall.js";
24
24
  import { lanBindState, writeLanBind } from "./lan-bind.js";
25
+ import { isHttpUrl, tunnelPlanOf } from "./tunnel-plan.js";
26
+ import { loadRelayIdentity, RelayRegistrar } from "./relay-registry.js";
25
27
  import { desiredBindHost, desiredBindPort, firewallActionNeeded, pendingRestartOf } from "./lan-bind-plan.js";
26
28
  import { createInnerAuth } from "./inner-auth.js";
27
29
  import { TunnelManager } from "./tunnel.js";
@@ -50,6 +52,8 @@ export const Config = z.object({
50
52
  publicBaseUrl: z.string(),
51
53
  devicesFile: z.string(),
52
54
  autoTunnel: z.boolean().default(false),
55
+ tunnelToken: z.string().role('secret'),
56
+ relay: z.boolean().default(true),
53
57
  lanBind: z.boolean(),
54
58
  profile: z.string().pattern(/^[A-Za-z0-9][A-Za-z0-9._-]*$/),
55
59
  enabled: z.boolean().default(true),
@@ -87,6 +91,8 @@ const DEFAULTS = {
87
91
  publicBaseUrl: undefined,
88
92
  devicesFile: defaultDevicesFile(),
89
93
  autoTunnel: false,
94
+ tunnelToken: undefined,
95
+ relay: true,
90
96
  lanBind: undefined,
91
97
  profile: process.env.DSH_PROFILE ?? 'web',
92
98
  enabled: true,
@@ -108,6 +114,8 @@ function applyImpl(ctx, config) {
108
114
  publicBaseUrl: config?.publicBaseUrl,
109
115
  devicesFile: config?.devicesFile ?? DEFAULTS.devicesFile,
110
116
  autoTunnel: config?.autoTunnel ?? DEFAULTS.autoTunnel,
117
+ tunnelToken: config?.tunnelToken,
118
+ relay: config?.relay ?? DEFAULTS.relay,
111
119
  lanBind: config?.lanBind,
112
120
  profile: config?.profile ?? process.env.DSH_PROFILE ?? DEFAULTS.profile,
113
121
  enabled: config?.enabled ?? DEFAULTS.enabled,
@@ -128,6 +136,8 @@ function applyImpl(ctx, config) {
128
136
  publicBaseUrl: value.publicBaseUrl,
129
137
  devicesFile: value.devicesFile ?? DEFAULTS.devicesFile,
130
138
  autoTunnel: value.autoTunnel ?? DEFAULTS.autoTunnel,
139
+ tunnelToken: value.tunnelToken,
140
+ relay: value.relay ?? DEFAULTS.relay,
131
141
  lanBind: value.lanBind,
132
142
  profile: value.profile ?? process.env.DSH_PROFILE ?? DEFAULTS.profile,
133
143
  enabled: value.enabled ?? DEFAULTS.enabled,
@@ -141,27 +151,100 @@ function applyImpl(ctx, config) {
141
151
  // mutation is needed here (a distributable plugin must not change the
142
152
  // harness's connection plugin).
143
153
  const tunnel = new TunnelManager();
144
- let autoTunnel = resolved.autoTunnel;
154
+ // Relay registry: one stable `<id>.dsh-market.com` subdomain per profile
155
+ // in front of the quick tunnel. The QR base prefers the relay origin once
156
+ // it is registered; until then (or when the registry is unreachable) the
157
+ // raw quick URL is used, exactly as before. Named tunnels keep their fixed
158
+ // dashboard hostname and never touch the relay.
159
+ let relayRegistrar;
160
+ let relayUrl;
161
+ let rawTunnelUrl;
162
+ /** The tunnel target the registrar last announced (dedupes sync re-runs). */
163
+ let relayAnnouncedFor;
164
+ const setPublicBase = () => {
165
+ service.setPublicBaseUrl(relayUrl ?? rawTunnelUrl);
166
+ };
167
+ const disposeRelayRegistrar = (unregister = false) => {
168
+ const registrar = relayRegistrar;
169
+ relayRegistrar = undefined;
170
+ relayUrl = undefined;
171
+ relayAnnouncedFor = undefined;
172
+ if (registrar === undefined)
173
+ return;
174
+ // Toggle-off removes the registry row so the stable origin stops
175
+ // proxying; teardown/mode changes keep the row (the phone then sees the
176
+ // relay's offline page instead of a dangling claim, and re-enabling
177
+ // reuses the same identity).
178
+ if (unregister)
179
+ void registrar.unregister().catch(() => undefined);
180
+ registrar.dispose();
181
+ };
182
+ const ensureRelayRegistrar = () => {
183
+ if (resolve().relay === false)
184
+ return undefined;
185
+ if (relayRegistrar === undefined) {
186
+ try {
187
+ const identity = loadRelayIdentity(resolve().profile);
188
+ relayRegistrar = new RelayRegistrar(identity, (state) => {
189
+ service.setRelayStatus(state.state === 'off' ? undefined : state);
190
+ if (state.state === 'running') {
191
+ relayUrl = state.url;
192
+ }
193
+ else if (state.state === 'off') {
194
+ relayUrl = undefined;
195
+ }
196
+ else if (state.state === 'failed') {
197
+ // Keep the last relay URL on failures: the phone origin only
198
+ // breaks when the mapping itself goes stale, not when one
199
+ // refresh call fails. The registrar retries with backoff.
200
+ console.warn(`remote-web-ui: relay registration failed (${state.error}) — the stable origin may serve its offline page until the retry lands`);
201
+ }
202
+ setPublicBase();
203
+ });
204
+ }
205
+ catch (error) {
206
+ console.warn(`remote-web-ui: relay registry unavailable (${error instanceof Error ? error.message : String(error)}) — the quick URL is the QR base`);
207
+ }
208
+ }
209
+ return relayRegistrar;
210
+ };
211
+ const announceRelay = (registrar, target) => {
212
+ if (relayAnnouncedFor === target)
213
+ return;
214
+ relayAnnouncedFor = target;
215
+ void registrar.announce(target);
216
+ };
217
+ // 'off' until a sync pass turns a mode on; the phase listener only feeds
218
+ // the public base while a plugin-managed tunnel (quick or named) runs.
219
+ let tunnelMode = resolved.autoTunnel ? 'quick' : 'off';
145
220
  tunnel.onPhase((info) => {
146
- if (!autoTunnel)
221
+ if (tunnelMode === 'off')
147
222
  return;
148
223
  if (info.phase === 'running' && info.url !== undefined) {
149
- service.setPublicBaseUrl(info.url);
224
+ rawTunnelUrl = info.url;
225
+ setPublicBase();
150
226
  service.setTunnelStatus({ state: 'running', url: info.url });
227
+ const registrar = tunnelMode === 'quick' ? ensureRelayRegistrar() : undefined;
228
+ if (registrar !== undefined)
229
+ announceRelay(registrar, info.url);
151
230
  runPostureProbe();
152
231
  }
153
232
  else if (info.phase === 'starting') {
154
233
  // A restart mints a NEW hostname: the previous URL dies with the old
155
234
  // process, so clear it now rather than advertising a dead link.
156
- service.setPublicBaseUrl(undefined);
235
+ rawTunnelUrl = undefined;
236
+ relayUrl = undefined;
237
+ setPublicBase();
157
238
  service.setTunnelStatus({ state: 'starting' });
158
239
  }
159
240
  else if (info.phase === 'failed') {
160
- service.setPublicBaseUrl(undefined);
241
+ rawTunnelUrl = undefined;
242
+ setPublicBase();
161
243
  service.setTunnelStatus(info.error === undefined ? { state: 'failed' } : { state: 'failed', error: info.error });
162
244
  }
163
245
  });
164
246
  ctx.effect(() => () => {
247
+ disposeRelayRegistrar();
165
248
  tunnel.dispose();
166
249
  }, 'remote-web-ui: auto tunnel');
167
250
  // The bind facts are known by now (webServer is an inject edge): the LAN
@@ -510,18 +593,60 @@ function applyImpl(ctx, config) {
510
593
  }
511
594
  }
512
595
  }
513
- // The auto tunnel owns the public base while enabled: the minted URL
514
- // lands in the service through the tunnel's phase listener. The manual
515
- // publicBaseUrl applies only when the auto tunnel is off.
516
- autoTunnel = value.autoTunnel === true;
517
- if (autoTunnel) {
518
- if (value.publicBaseUrl !== undefined) {
519
- console.warn('remote-web-ui: autoTunnel is on — ignoring the manually configured publicBaseUrl');
596
+ // The plugin-managed tunnels own the public base while one runs: the URL
597
+ // lands in the service through the tunnel's phase listener (the minted
598
+ // quick URL, or the named tunnel's fixed public hostname). The manual
599
+ // publicBaseUrl applies only when no tunnel runs.
600
+ const plan = tunnelPlanOf(value, ctx.webServer.port);
601
+ tunnelMode = plan.mode;
602
+ if (plan.mode !== 'quick') {
603
+ // The relay only fronts the quick tunnel; named mode owns its fixed
604
+ // dashboard hostname and the off mode has no public base at all.
605
+ disposeRelayRegistrar();
606
+ if (plan.mode !== 'named')
607
+ setPublicBase();
608
+ }
609
+ else if (value.relay === false) {
610
+ // The relay toggle is off: no stable origin, the raw quick URL is the
611
+ // QR base exactly as before the relay existed.
612
+ disposeRelayRegistrar(true);
613
+ setPublicBase();
614
+ }
615
+ else if (rawTunnelUrl !== undefined) {
616
+ // The relay just turned on (or the registrar is new) while the tunnel
617
+ // already runs: announce now — no phase event will fire for an
618
+ // unchanged target.
619
+ const registrar = ensureRelayRegistrar();
620
+ if (registrar !== undefined)
621
+ announceRelay(registrar, rawTunnelUrl);
622
+ }
623
+ if (plan.mode === 'quick') {
624
+ for (const ignored of plan.ignored) {
625
+ console.warn(`remote-web-ui: autoTunnel is on — ignoring the configured ${ignored}`);
520
626
  }
521
- tunnel.start(`http://127.0.0.1:${String(ctx.webServer.port)}`);
627
+ // With the relay on, the connector stamps the stable origin as the
628
+ // origin-side Host (`--http-host-header`): the plugin fence and the
629
+ // harness browser-auth are Host-bound to the relay origin, and the
630
+ // Workers relay cannot control the origin-side Host (fetch forces it
631
+ // to the URL authority). The target identity change is also what
632
+ // restarts a running tunnel on a relay toggle.
633
+ const registrar = ensureRelayRegistrar();
634
+ const originHostHeader = registrar === undefined ? undefined : new URL(registrar.baseUrl).host;
635
+ tunnel.start(originHostHeader === undefined
636
+ ? plan.targetUrl
637
+ : { kind: 'quick', targetUrl: plan.targetUrl, originHostHeader });
638
+ }
639
+ else if (plan.mode === 'named') {
640
+ tunnel.start({ kind: 'named', token: plan.token, publicUrl: plan.publicUrl });
522
641
  }
523
642
  else {
524
643
  tunnel.stop();
644
+ // A named-tunnel token without a usable public hostname cannot serve
645
+ // the QR: stay off and say exactly what is missing instead of running
646
+ // a tunnel nothing points at.
647
+ if (value.tunnelToken !== undefined && value.tunnelToken !== '') {
648
+ console.warn('remote-web-ui: tunnelToken is set but publicBaseUrl is missing or not a valid URL — fill the fixed public hostname of the named tunnel (e.g. https://dsh.example.com) to run it');
649
+ }
525
650
  // A malformed public base is ignored with a warning — LAN-only behavior
526
651
  // stays intact rather than silently minting unusable QR links.
527
652
  if (value.publicBaseUrl !== undefined && !isHttpUrl(value.publicBaseUrl)) {
@@ -594,13 +719,3 @@ function applyImpl(ctx, config) {
594
719
  });
595
720
  sync();
596
721
  }
597
- /** Whether a configured public base is a parseable http(s) URL with a host. */
598
- function isHttpUrl(value) {
599
- try {
600
- const url = new URL(value);
601
- return (url.protocol === 'http:' || url.protocol === 'https:') && url.hostname !== '';
602
- }
603
- catch {
604
- return false;
605
- }
606
- }
@@ -89,6 +89,18 @@ export interface TunnelStatus {
89
89
  /** Human-readable failure detail. */
90
90
  error?: string;
91
91
  }
92
+ /**
93
+ * One relay-registry status frame (undefined while the stable-origin relay
94
+ * is not in play: autoTunnel off, or the relay toggle disabled).
95
+ */
96
+ export interface RelayStatus {
97
+ /** registering: a sync is in flight; running: the mapping is accepted. */
98
+ state: 'off' | 'registering' | 'running' | 'failed';
99
+ /** The stable relay origin (`https://<id>.dsh-market.com`). */
100
+ url?: string;
101
+ /** Human-readable failure detail of the last sync attempt. */
102
+ error?: string;
103
+ }
92
104
  /** One snapshot frame pushed to desktop status streams. */
93
105
  export interface PairingSnapshot {
94
106
  phase: PairingPhase;
@@ -100,6 +112,8 @@ export interface PairingSnapshot {
100
112
  publicUrl?: string;
101
113
  /** Auto-tunnel status, while the auto-tunnel feature is active. */
102
114
  tunnel?: TunnelStatus;
115
+ /** Relay-registry status, while the stable-origin relay is in play. */
116
+ relay?: RelayStatus;
103
117
  /** Latest /api posture probe (undefined until the first round completes). */
104
118
  posture?: PostureSnapshot;
105
119
  /** Opaque (non-secret) id of the active token (undefined when stopped/lan-required). */
@@ -182,6 +196,7 @@ export declare class PairingService {
182
196
  private publicBase;
183
197
  /** Auto-tunnel status, while the auto-tunnel feature is active. */
184
198
  private tunnelStatus;
199
+ private relayStatus;
185
200
  private posture;
186
201
  /** True when lastSeenAt changed since the last persist (flushed on sweep). */
187
202
  private dirty;
@@ -230,6 +245,8 @@ export declare class PairingService {
230
245
  setPublicBaseUrl(url: string | undefined): void;
231
246
  /** Set or clear the auto-tunnel status frame (undefined when the feature is off). */
232
247
  setTunnelStatus(status: TunnelStatus | undefined): void;
248
+ /** Set or clear the relay-registry status frame (undefined when not in play). */
249
+ setRelayStatus(status: RelayStatus | undefined): void;
233
250
  /** Set the latest /api posture probe result (see posture.ts). */
234
251
  setPosture(snapshot: PostureSnapshot | undefined): void;
235
252
  /**
@@ -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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"}
@@ -74,6 +74,7 @@ export class PairingService {
74
74
  publicBase;
75
75
  /** Auto-tunnel status, while the auto-tunnel feature is active. */
76
76
  tunnelStatus;
77
+ relayStatus;
77
78
  posture;
78
79
  /** True when lastSeenAt changed since the last persist (flushed on sweep). */
79
80
  dirty = false;
@@ -205,6 +206,11 @@ export class PairingService {
205
206
  this.tunnelStatus = status;
206
207
  this.notify();
207
208
  }
209
+ /** Set or clear the relay-registry status frame (undefined when not in play). */
210
+ setRelayStatus(status) {
211
+ this.relayStatus = status;
212
+ this.notify();
213
+ }
208
214
  /** Set the latest /api posture probe result (see posture.ts). */
209
215
  setPosture(snapshot) {
210
216
  this.posture = snapshot;
@@ -361,6 +367,7 @@ export class PairingService {
361
367
  lanAddresses: [...this.lanBases.keys()],
362
368
  ...(this.publicBase !== undefined ? { publicUrl: this.publicBase } : {}),
363
369
  ...(this.tunnelStatus !== undefined ? { tunnel: this.tunnelStatus } : {}),
370
+ ...(this.relayStatus !== undefined ? { relay: this.relayStatus } : {}),
364
371
  ...(this.posture !== undefined ? { posture: this.posture } : {}),
365
372
  ...(token !== undefined ? { tokenId: token.record.id, tokenExpiresAt: token.record.expiresAt } : {}),
366
373
  deviceCount: this.devices.size,
@@ -454,6 +461,7 @@ function snapshotsEqual(a, b) {
454
461
  && sameStrings(a.lanAddresses, b.lanAddresses)
455
462
  && a.publicUrl === b.publicUrl
456
463
  && tunnelEqual(a.tunnel, b.tunnel)
464
+ && relayEqual(a.relay, b.relay)
457
465
  && a.tokenId === b.tokenId
458
466
  && a.tokenExpiresAt === b.tokenExpiresAt
459
467
  && a.deviceCount === b.deviceCount
@@ -477,6 +485,11 @@ function tunnelEqual(a, b) {
477
485
  return a === b || (a !== undefined && b !== undefined
478
486
  && a.state === b.state && a.url === b.url && a.error === b.error);
479
487
  }
488
+ /** Relay frame equality (same shape as the tunnel frame). */
489
+ function relayEqual(a, b) {
490
+ return a === b || (a !== undefined && b !== undefined
491
+ && a.state === b.state && a.url === b.url && a.error === b.error);
492
+ }
480
493
  /** Element-wise string list equality (interface order is meaningful). */
481
494
  function sameStrings(a, b) {
482
495
  return a.length === b.length && a.every((value, index) => value === b[index]);
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Relay registry client: a stable `https://<id>.dsh-market.com` origin in
3
+ * front of the plugin-managed quick tunnel, so a paired phone keeps one
4
+ * bookmark and one cookie context across `dsh web` restarts.
5
+ *
6
+ * The plugin mints an id + secret on first run and persists them per profile
7
+ * under `$DSH_HOME/remote-web-ui-registry/`; every tunnel (re)start
8
+ * re-registers the current quick URL with the market worker. Registration is
9
+ * best-effort: a registry outage never blocks the tunnel — the QR just falls
10
+ * back to the raw quick URL for that session (the pre-relay behavior).
11
+ */
12
+ /** The market worker's registration endpoints (dsh-market.com edge API). */
13
+ export declare const RELAY_REGISTER_URL = "https://dsh-market.com/api/relay/register";
14
+ export declare const RELAY_UNREGISTER_URL = "https://dsh-market.com/api/relay/unregister";
15
+ /**
16
+ * The public origin template: one stable single-label subdomain per
17
+ * registration id. Single label on purpose — Universal SSL covers exactly
18
+ * one subdomain level, so a two-level hostname would get no certificate.
19
+ */
20
+ export declare const RELAY_BASE_SUFFIX = ".dsh-market.com";
21
+ export declare const RELAY_ID_RE: RegExp;
22
+ export declare const RELAY_SECRET_RE: RegExp;
23
+ /** Persisted per-profile relay identity. */
24
+ export interface RelayIdentity {
25
+ id: string;
26
+ secret: string;
27
+ /**
28
+ * True only for an identity minted in this process that has never been
29
+ * accepted by the registry: the first register carries `new_secret` so
30
+ * the endpoint may create the row; refreshes authenticate with `secret`
31
+ * alone and must not re-claim an id that already exists.
32
+ */
33
+ fresh?: boolean;
34
+ }
35
+ /** The observable relay lifecycle the settings card renders. */
36
+ export type RelayState = {
37
+ state: 'off';
38
+ } | {
39
+ state: 'registering';
40
+ } | {
41
+ state: 'running';
42
+ url: string;
43
+ } | {
44
+ state: 'failed';
45
+ error: string;
46
+ };
47
+ /** Registry storage layout: one file per profile under the relay directory. */
48
+ export declare function relayIdentityFile(profile: string, home?: string): string;
49
+ /** Generate one identity pair (id: 8-byte hex slug; secret: 32-byte base64url). */
50
+ export declare function generateRelayIdentity(random?: (size: number) => Buffer): RelayIdentity;
51
+ /**
52
+ * Load (or lazily mint) the per-profile relay identity. The file is written
53
+ * owner-only on first mint; a corrupt file is reminted (the phone then
54
+ * re-pairs once — the same cost as losing the devices file).
55
+ */
56
+ export declare function loadRelayIdentity(profile: string, home?: string, random?: (size: number) => Buffer): RelayIdentity;
57
+ /** Best-effort removal of the mapping (used on disable; errors are swallowed). */
58
+ export declare function unregisterRelay(identity: RelayIdentity, endpoint?: string, fetchFn?: typeof fetch): Promise<void>;
59
+ /**
60
+ * The stable relay base for one identity — the origin the QR and the phone
61
+ * bookmark use while (or after) the registration is accepted.
62
+ */
63
+ export declare function relayBaseOf(identity: RelayIdentity): string;
64
+ /**
65
+ * Registration loop with capped exponential backoff. Re-registration happens
66
+ * on every tunnel start (the quick URL can change after a crash-restart);
67
+ * failures retry with backoff and surface the last error to the card.
68
+ */
69
+ export declare class RelayRegistrar {
70
+ private readonly identity;
71
+ private readonly setState;
72
+ private readonly options;
73
+ private timer;
74
+ private attempts;
75
+ private lastError;
76
+ private disposed;
77
+ constructor(identity: RelayIdentity, setState: (state: RelayState) => void, options?: {
78
+ fetchFn?: typeof fetch;
79
+ registerUrl?: string;
80
+ unregisterUrl?: string;
81
+ baseDelayMs?: number;
82
+ maxDelayMs?: number;
83
+ timerFn?: (fn: () => void, ms: number) => NodeJS.Timeout;
84
+ });
85
+ /** The stable relay base for the QR while registered. */
86
+ get baseUrl(): string;
87
+ /** The last registration failure detail (undefined while healthy). */
88
+ get error(): string | undefined;
89
+ /** Push one tunnel target; resolves the stable base once accepted. */
90
+ announce(target: string): Promise<string | undefined>;
91
+ /** Best-effort removal of the registry row (the relay toggle turned off). */
92
+ unregister(): Promise<void>;
93
+ private scheduleRetry;
94
+ /** Stop retrying and clear the card state (tunnel stopped or disabled). */
95
+ dispose(): void;
96
+ }
97
+ //# sourceMappingURL=relay-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay-registry.d.ts","sourceRoot":"","sources":["../../src/relay-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,8CAA8C,CAAA;AAC7E,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AACjF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,oBAAoB,CAAA;AAElD,eAAO,MAAM,WAAW,QAAmB,CAAA;AAC3C,eAAO,MAAM,eAAe,QAAwB,CAAA;AAEpD,4CAA4C;AAC5C,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,gEAAgE;AAChE,MAAM,MAAM,UAAU,GAClB;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,GAChB;IAAE,KAAK,EAAE,aAAa,CAAA;CAAE,GACxB;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAEtC,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG,MAAM,CAKnF;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,MAAM,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAoB,GAAG,aAAa,CAMnG;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,EAAE,MAAM,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAoB,GAAG,aAAa,CAmB1I;AAYD,kFAAkF;AAClF,wBAAsB,eAAe,CACnC,QAAQ,EAAE,aAAa,EACvB,QAAQ,GAAE,MAA6B,EACvC,OAAO,GAAE,OAAO,KAAa,GAC5B,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,CAE3D;AAED;;;;GAIG;AACH,qBAAa,cAAc;IAOvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAR1B,OAAO,CAAC,KAAK,CAA4B;IACzC,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,QAAQ,CAAQ;gBAGL,QAAQ,EAAE,aAAa,EACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,EACrC,OAAO,GAAE;QACxB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAA;QACtB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,aAAa,CAAC,EAAE,MAAM,CAAA;QACtB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC,OAAO,CAAA;KACpD;IAGR,yDAAyD;IACzD,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,sEAAsE;IACtE,IAAI,KAAK,IAAI,MAAM,GAAG,SAAS,CAE9B;IAED,sEAAsE;IAChE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAyC3D,6EAA6E;IACvE,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC,OAAO,CAAC,aAAa;IAiBrB,2EAA2E;IAC3E,OAAO,IAAI,IAAI;CAQhB"}
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Relay registry client: a stable `https://<id>.dsh-market.com` origin in
3
+ * front of the plugin-managed quick tunnel, so a paired phone keeps one
4
+ * bookmark and one cookie context across `dsh web` restarts.
5
+ *
6
+ * The plugin mints an id + secret on first run and persists them per profile
7
+ * under `$DSH_HOME/remote-web-ui-registry/`; every tunnel (re)start
8
+ * re-registers the current quick URL with the market worker. Registration is
9
+ * best-effort: a registry outage never blocks the tunnel — the QR just falls
10
+ * back to the raw quick URL for that session (the pre-relay behavior).
11
+ */
12
+ import { randomBytes } from 'node:crypto';
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
14
+ import { dirname, join } from 'node:path';
15
+ import { dshHome } from "./dsh-home.js";
16
+ /** The market worker's registration endpoints (dsh-market.com edge API). */
17
+ export const RELAY_REGISTER_URL = 'https://dsh-market.com/api/relay/register';
18
+ export const RELAY_UNREGISTER_URL = 'https://dsh-market.com/api/relay/unregister';
19
+ /**
20
+ * The public origin template: one stable single-label subdomain per
21
+ * registration id. Single label on purpose — Universal SSL covers exactly
22
+ * one subdomain level, so a two-level hostname would get no certificate.
23
+ */
24
+ export const RELAY_BASE_SUFFIX = '.dsh-market.com';
25
+ export const RELAY_ID_RE = /^[a-z0-9]{16}$/;
26
+ export const RELAY_SECRET_RE = /^[A-Za-z0-9_-]{43}$/;
27
+ /** Registry storage layout: one file per profile under the relay directory. */
28
+ export function relayIdentityFile(profile, home = dshHome()) {
29
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(profile)) {
30
+ throw new Error(`relay registry: profile ${JSON.stringify(profile)} is not a safe path segment`);
31
+ }
32
+ return join(home, 'remote-web-ui-registry', `${profile}.json`);
33
+ }
34
+ /** Generate one identity pair (id: 8-byte hex slug; secret: 32-byte base64url). */
35
+ export function generateRelayIdentity(random = randomBytes) {
36
+ return {
37
+ id: random(8).toString('hex'),
38
+ secret: random(32).toString('base64url'),
39
+ fresh: true,
40
+ };
41
+ }
42
+ /**
43
+ * Load (or lazily mint) the per-profile relay identity. The file is written
44
+ * owner-only on first mint; a corrupt file is reminted (the phone then
45
+ * re-pairs once — the same cost as losing the devices file).
46
+ */
47
+ export function loadRelayIdentity(profile, home = dshHome(), random = randomBytes) {
48
+ const file = relayIdentityFile(profile, home);
49
+ try {
50
+ if (existsSync(file)) {
51
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
52
+ if (typeof parsed.id === 'string' && RELAY_ID_RE.test(parsed.id)
53
+ && typeof parsed.secret === 'string' && RELAY_SECRET_RE.test(parsed.secret)) {
54
+ // A persisted identity has certainly been registered before.
55
+ return { id: parsed.id, secret: parsed.secret };
56
+ }
57
+ console.warn(`relay registry: ${file} is malformed — reminting the identity (paired devices must scan the QR once more)`);
58
+ }
59
+ }
60
+ catch (error) {
61
+ console.warn(`relay registry: could not read ${file} (${error instanceof Error ? error.message : String(error)}) — reminting`);
62
+ }
63
+ const identity = generateRelayIdentity(random);
64
+ mkdirSync(dirname(file), { recursive: true });
65
+ writeFileSync(file, JSON.stringify({ id: identity.id, secret: identity.secret }, null, 2) + '\n', { mode: 0o600 });
66
+ return identity;
67
+ }
68
+ /** Build the registration request body for one identity + tunnel target. */
69
+ function registrationBody(identity, target) {
70
+ return JSON.stringify({
71
+ id: identity.id,
72
+ secret: identity.secret,
73
+ new_secret: identity.fresh === true ? identity.secret : undefined,
74
+ target,
75
+ });
76
+ }
77
+ /** Best-effort removal of the mapping (used on disable; errors are swallowed). */
78
+ export async function unregisterRelay(identity, endpoint = RELAY_UNREGISTER_URL, fetchFn = fetch) {
79
+ await fetchFn(endpoint, {
80
+ method: 'POST',
81
+ headers: { 'content-type': 'application/json' },
82
+ body: JSON.stringify({ id: identity.id, secret: identity.secret }),
83
+ });
84
+ }
85
+ /**
86
+ * The stable relay base for one identity — the origin the QR and the phone
87
+ * bookmark use while (or after) the registration is accepted.
88
+ */
89
+ export function relayBaseOf(identity) {
90
+ return `https://${identity.id}${RELAY_BASE_SUFFIX}`;
91
+ }
92
+ /**
93
+ * Registration loop with capped exponential backoff. Re-registration happens
94
+ * on every tunnel start (the quick URL can change after a crash-restart);
95
+ * failures retry with backoff and surface the last error to the card.
96
+ */
97
+ export class RelayRegistrar {
98
+ identity;
99
+ setState;
100
+ options;
101
+ timer;
102
+ attempts = 0;
103
+ lastError;
104
+ disposed = false;
105
+ constructor(identity, setState, options = {}) {
106
+ this.identity = identity;
107
+ this.setState = setState;
108
+ this.options = options;
109
+ }
110
+ /** The stable relay base for the QR while registered. */
111
+ get baseUrl() {
112
+ return relayBaseOf(this.identity);
113
+ }
114
+ /** The last registration failure detail (undefined while healthy). */
115
+ get error() {
116
+ return this.lastError;
117
+ }
118
+ /** Push one tunnel target; resolves the stable base once accepted. */
119
+ async announce(target) {
120
+ // A disposed registrar stays disposed: a pending retry timer must not
121
+ // resurrect it (re-enabling creates a fresh registrar instead).
122
+ if (this.disposed)
123
+ return undefined;
124
+ if (this.timer !== undefined) {
125
+ clearTimeout(this.timer);
126
+ this.timer = undefined;
127
+ }
128
+ this.setState({ state: 'registering' });
129
+ try {
130
+ const fetchFn = this.options.fetchFn ?? fetch;
131
+ const response = await fetchFn(this.options.registerUrl ?? RELAY_REGISTER_URL, {
132
+ method: 'PUT',
133
+ headers: { 'content-type': 'application/json' },
134
+ body: registrationBody(this.identity, target),
135
+ });
136
+ if (!response.ok) {
137
+ const detail = await response.json().catch(() => undefined);
138
+ const error = detail && typeof detail.error === 'string' ? detail.error : `HTTP ${String(response.status)}`;
139
+ throw new Error(error);
140
+ }
141
+ // The row exists now: never claim a fresh id twice.
142
+ this.identity.fresh = false;
143
+ this.attempts = 0;
144
+ this.lastError = undefined;
145
+ this.setState({ state: 'running', url: this.baseUrl });
146
+ return this.baseUrl;
147
+ }
148
+ catch (error) {
149
+ this.lastError = error instanceof Error ? error.message : String(error);
150
+ if (this.lastError === 'invalid-params' && this.identity.fresh !== true) {
151
+ // The registry lost the row (a D1 restore or manual wipe): re-claim
152
+ // the id with a fresh claim on the next retry. A 403 auth-failed
153
+ // means the id is owned by another secret and stays failed.
154
+ this.identity.fresh = true;
155
+ }
156
+ this.setState({ state: 'failed', error: this.lastError });
157
+ this.scheduleRetry(target);
158
+ return undefined;
159
+ }
160
+ }
161
+ /** Best-effort removal of the registry row (the relay toggle turned off). */
162
+ async unregister() {
163
+ await unregisterRelay(this.identity, this.options.unregisterUrl, this.options.fetchFn);
164
+ }
165
+ scheduleRetry(target) {
166
+ if (this.disposed)
167
+ return;
168
+ const base = this.options.baseDelayMs ?? 5_000;
169
+ const max = this.options.maxDelayMs ?? 60_000;
170
+ const delay = Math.min(base * 2 ** this.attempts, max);
171
+ this.attempts += 1;
172
+ const timerFn = this.options.timerFn ?? ((fn, ms) => {
173
+ const timer = setTimeout(fn, ms);
174
+ timer.unref?.();
175
+ return timer;
176
+ });
177
+ this.timer = timerFn(() => {
178
+ this.timer = undefined;
179
+ void this.announce(target);
180
+ }, delay);
181
+ }
182
+ /** Stop retrying and clear the card state (tunnel stopped or disabled). */
183
+ dispose() {
184
+ this.disposed = true;
185
+ if (this.timer !== undefined) {
186
+ clearTimeout(this.timer);
187
+ this.timer = undefined;
188
+ }
189
+ this.setState({ state: 'off' });
190
+ }
191
+ }
@@ -25,7 +25,7 @@ import type { IncomingMessage } from 'node:http';
25
25
  import type { WebRoute, WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver';
26
26
  import type { PairingService } from './pairing.ts';
27
27
  import type { InnerAuth } from './inner-auth.ts';
28
- export { DESKTOP_LAUNCHER_PATH, LOCAL_ONLY_PREFIXES, PLUGIN_MANAGER_PATH, REMOTE_API_PATHS, REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY, REMOTE_PREFIX, REMOTE_UPGRADE_PATHS, WEB_UI_SETTINGS_BRIDGE_PATH, localOnlyDenial, } from './remote-methods.ts';
28
+ export { LOCAL_ONLY_PREFIXES, PLUGIN_MANAGER_PATH, REMOTE_API_PATHS, REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY, REMOTE_PREFIX, REMOTE_UPGRADE_PATHS, WEB_UI_SETTINGS_BRIDGE_PATH, localOnlyDenial, } from './remote-methods.ts';
29
29
  export { REMOTE_API_PREFIX } from './remote-methods.ts';
30
30
  /** Route-family dependencies. */
31
31
  export interface RemoteApiDeps {
@@ -1 +1 @@
1
- {"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/remote-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;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,qBAAqB,EACrB,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,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAA;IAChD;;;;;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,CA0CnE;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,CA2CjF"}
1
+ {"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/remote-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;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,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAA;IAChD;;;;;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,CA0CnE;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,CA2CjF"}