@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
@@ -25,7 +25,7 @@ import { readCookie } from "./gate.js";
25
25
  import { writeJson } from "./http.js";
26
26
  import { proxyLoopbackHttp, proxyLoopbackUpgrade } from "./loopback-proxy.js";
27
27
  import { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY, REMOTE_PREFIX, REMOTE_UPGRADE_PATHS, localOnlyDenial, } from "./remote-methods.js";
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.js";
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.js";
29
29
  export { REMOTE_API_PREFIX } from "./remote-methods.js";
30
30
  const ALLOWED_METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']);
31
31
  /** Reject traversal and empty segments; allow plugin file-path characters. */
@@ -1 +1 @@
1
- {"version":3,"file":"remote-channel-boot.d.ts","sourceRoot":"","sources":["../../src/remote-channel-boot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAoD,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAErH;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,GAAE,kBAAyC,GAAG,MAAM,CA+HrG;AAED,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,2BAA2B,CAAA;AAQzD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,SAAwB,EAAE,MAAM,SAAwB,GAAG,MAAM,CAgB9G;AAED,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,EAAE,MAAuC,CAAA"}
1
+ {"version":3,"file":"remote-channel-boot.d.ts","sourceRoot":"","sources":["../../src/remote-channel-boot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAoD,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAErH;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,GAAE,kBAAyC,GAAG,MAAM,CA8HrG;AAED,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,2BAA2B,CAAA;AAQzD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,SAAwB,EAAE,MAAM,SAAwB,GAAG,MAAM,CAgB9G;AAED,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,EAAE,MAAuC,CAAA"}
@@ -57,7 +57,6 @@ export function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
57
57
  'function sf(p){' +
58
58
  'if(p.indexOf(R.pairPrefix)===0)return false;' +
59
59
  'if(p.indexOf(R.updatePrefix)===0)return false;' +
60
- 'if(p===R.desktopLauncherPrefix||p.indexOf(R.desktopLauncherPrefix+"/")===0)return false;' +
61
60
  'if(p===R.settingsBridgePrefix||p.indexOf(R.settingsBridgePrefix+"/")===0)return false;' +
62
61
  'if(p.indexOf(R.apiPrefix)===0)return true;' +
63
62
  'if(p.indexOf(R.sidebarPrefix)===0||p==="/sidebar")return true;' +
@@ -17,7 +17,6 @@ export interface RemoteChannelRules {
17
17
  readonly apiPrefix: string;
18
18
  readonly pairPrefix: string;
19
19
  readonly updatePrefix: string;
20
- readonly desktopLauncherPrefix: string;
21
20
  readonly settingsBridgePrefix: string;
22
21
  readonly sidebarPrefix: string;
23
22
  readonly gitPrefix: string;
@@ -1 +1 @@
1
- {"version":3,"file":"remote-channel-rules.d.ts","sourceRoot":"","sources":["../../src/remote-channel-rules.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,kEAAkE;AAClE,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAE/E,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,CAAA;AAEpD,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD,6EAA6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAA;IACtC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAA;IACrC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,yEAAyE;IACzE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED,yBAAyB;AACzB,eAAO,MAAM,oBAAoB,EAAE,kBAsBlC,CAAA;AAED,iEAAiE;AACjE,eAAO,MAAM,0BAA0B,gCAAgC,CAAA;AAEvE;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC/B,QAAQ,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC7B,eAAe,EAAE,OAAO,CAAA;IACxB,OAAO,IAAI,IAAI,CAAA;CAChB"}
1
+ {"version":3,"file":"remote-channel-rules.d.ts","sourceRoot":"","sources":["../../src/remote-channel-rules.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,kEAAkE;AAClE,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAE/E,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,CAAA;AAEpD,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD,6EAA6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAA;IACrC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,yEAAyE;IACzE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED,yBAAyB;AACzB,eAAO,MAAM,oBAAoB,EAAE,kBAqBlC,CAAA;AAED,iEAAiE;AACjE,eAAO,MAAM,0BAA0B,gCAAgC,CAAA;AAEvE;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC/B,QAAQ,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC7B,eAAe,EAAE,OAAO,CAAA;IACxB,OAAO,IAAI,IAAI,CAAA;CAChB"}
@@ -17,7 +17,6 @@ export const REMOTE_CHANNEL_RULES = {
17
17
  apiPrefix: '/api/',
18
18
  pairPrefix: '/api/pair/',
19
19
  updatePrefix: '/api/update/',
20
- desktopLauncherPrefix: '/api/dsh-desktop-launcher',
21
20
  settingsBridgePrefix: '/api/dsh-web-ui-settings',
22
21
  sidebarPrefix: '/sidebar/',
23
22
  gitPrefix: '/git/',
@@ -34,8 +34,6 @@ export declare const REMOTE_API_PATHS: {
34
34
  export declare const REMOTE_UPGRADE_PATHS: readonly ["/remote/api/remote.mux", "/remote/sidebar/ws/terminal", "/remote/sidebar/ws/agent-terminals", "/remote/api/dsh-ssh/terminal"];
35
35
  /** Plugin-manager HTTP prefix: install/remove stay physically local. */
36
36
  export declare const PLUGIN_MANAGER_PATH = "/api/plugin-manager";
37
- /** Desktop-launcher HTTP prefix: shortcut create and host shutdown stay physically local. */
38
- export declare const DESKTOP_LAUNCHER_PATH = "/api/dsh-desktop-launcher";
39
37
  /** Family settings-bridge HTTP prefix — re-exposed to paired devices (plain settings parity). */
40
38
  export declare const WEB_UI_SETTINGS_BRIDGE_PATH = "/api/dsh-web-ui-settings";
41
39
  /**
@@ -54,8 +52,7 @@ export declare const REMOTE_DEVICE_QUERY = "device";
54
52
  * paired remote desktop may use the full host API (chat, sessions,
55
53
  * settings, credentials, presets — it is a full-control credential), but it
56
54
  * must not reach the machine-control planes: pairing control itself, the
57
- * dsh-web self-update installer, plugin install/remove, and desktop
58
- * launcher actions (host shutdown, shortcuts).
55
+ * dsh-web self-update installer, and plugin install/remove.
59
56
  */
60
57
  export declare const LOCAL_ONLY_PREFIXES: readonly string[];
61
58
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"remote-methods.d.ts","sourceRoot":"","sources":["../../src/remote-methods.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,gFAAgF;AAChF,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAEV;;;GAGG;AACH,eAAO,MAAM,oBAAoB,0IAKvB,CAAA;AAEV,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,wBAAwB,CAAA;AAExD,6FAA6F;AAC7F,eAAO,MAAM,qBAAqB,8BAA8B,CAAA;AAEhE,iGAAiG;AACjG,eAAO,MAAM,2BAA2B,6BAA6B,CAAA;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,wBAAwB,CAAA;AACzD,eAAO,MAAM,mBAAmB,WAAW,CAAA;AAE3C;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAKvC,CAAA;AAEV;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOrE"}
1
+ {"version":3,"file":"remote-methods.d.ts","sourceRoot":"","sources":["../../src/remote-methods.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,gFAAgF;AAChF,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAEV;;;GAGG;AACH,eAAO,MAAM,oBAAoB,0IAKvB,CAAA;AAEV,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,wBAAwB,CAAA;AAExD,iGAAiG;AACjG,eAAO,MAAM,2BAA2B,6BAA6B,CAAA;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,wBAAwB,CAAA;AACzD,eAAO,MAAM,mBAAmB,WAAW,CAAA;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAIvC,CAAA;AAEV;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOrE"}
@@ -39,8 +39,6 @@ export const REMOTE_UPGRADE_PATHS = [
39
39
  ];
40
40
  /** Plugin-manager HTTP prefix: install/remove stay physically local. */
41
41
  export const PLUGIN_MANAGER_PATH = '/api/plugin-manager';
42
- /** Desktop-launcher HTTP prefix: shortcut create and host shutdown stay physically local. */
43
- export const DESKTOP_LAUNCHER_PATH = '/api/dsh-desktop-launcher';
44
42
  /** Family settings-bridge HTTP prefix — re-exposed to paired devices (plain settings parity). */
45
43
  export const WEB_UI_SETTINGS_BRIDGE_PATH = '/api/dsh-web-ui-settings';
46
44
  /**
@@ -59,14 +57,12 @@ export const REMOTE_DEVICE_QUERY = 'device';
59
57
  * paired remote desktop may use the full host API (chat, sessions,
60
58
  * settings, credentials, presets — it is a full-control credential), but it
61
59
  * must not reach the machine-control planes: pairing control itself, the
62
- * dsh-web self-update installer, plugin install/remove, and desktop
63
- * launcher actions (host shutdown, shortcuts).
60
+ * dsh-web self-update installer, and plugin install/remove.
64
61
  */
65
62
  export const LOCAL_ONLY_PREFIXES = [
66
63
  '/api/pair',
67
64
  '/api/update',
68
65
  PLUGIN_MANAGER_PATH,
69
- DESKTOP_LAUNCHER_PATH,
70
66
  ];
71
67
  /**
72
68
  * Whether a paired inner path must stay physically local.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Pure planner for the settings-sync tunnel branch: given the resolved
3
+ * settings slice, decide which tunnel mode runs and with what target. Kept
4
+ * free of cordis types so the precedence matrix is unit-testable without a
5
+ * host context.
6
+ */
7
+ /** Whether a configured public base is a parseable http(s) URL with a host. */
8
+ export declare function isHttpUrl(value: string): boolean;
9
+ /** How the settings sync should drive the tunnel for one resolved value. */
10
+ export type TunnelPlan = {
11
+ mode: 'off';
12
+ } | {
13
+ mode: 'quick';
14
+ targetUrl: string;
15
+ ignored: Array<'tunnelToken' | 'publicBaseUrl'>;
16
+ } | {
17
+ mode: 'named';
18
+ token: string;
19
+ publicUrl: string;
20
+ };
21
+ /**
22
+ * Decide the tunnel mode and target from one resolved settings value.
23
+ * Precedence: the quick tunnel wins when `autoTunnel` is on (anything else
24
+ * configured is reported as ignored); the named tunnel runs only with a
25
+ * token AND a valid public hostname (the token does not carry the hostname,
26
+ * so the QR base and the fence trust would have no source without it);
27
+ * otherwise no tunnel and the manual public base applies.
28
+ * @param value - the resolved settings slice that drives the tunnel.
29
+ * @param port - the local webServer port the tunnel forwards to.
30
+ */
31
+ export declare function tunnelPlanOf(value: {
32
+ autoTunnel?: boolean;
33
+ tunnelToken?: string;
34
+ publicBaseUrl?: string;
35
+ }, port: number): TunnelPlan;
36
+ //# sourceMappingURL=tunnel-plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel-plan.d.ts","sourceRoot":"","sources":["../../src/tunnel-plan.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAOhD;AAED,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,GACf;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC,aAAa,GAAG,eAAe,CAAC,CAAA;CAAE,GACrF;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,EAC7E,IAAI,EAAE,MAAM,GACX,UAAU,CAcZ"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Pure planner for the settings-sync tunnel branch: given the resolved
3
+ * settings slice, decide which tunnel mode runs and with what target. Kept
4
+ * free of cordis types so the precedence matrix is unit-testable without a
5
+ * host context.
6
+ */
7
+ /** Whether a configured public base is a parseable http(s) URL with a host. */
8
+ export function isHttpUrl(value) {
9
+ try {
10
+ const url = new URL(value);
11
+ return (url.protocol === 'http:' || url.protocol === 'https:') && url.hostname !== '';
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ /**
18
+ * Decide the tunnel mode and target from one resolved settings value.
19
+ * Precedence: the quick tunnel wins when `autoTunnel` is on (anything else
20
+ * configured is reported as ignored); the named tunnel runs only with a
21
+ * token AND a valid public hostname (the token does not carry the hostname,
22
+ * so the QR base and the fence trust would have no source without it);
23
+ * otherwise no tunnel and the manual public base applies.
24
+ * @param value - the resolved settings slice that drives the tunnel.
25
+ * @param port - the local webServer port the tunnel forwards to.
26
+ */
27
+ export function tunnelPlanOf(value, port) {
28
+ if (value.autoTunnel === true) {
29
+ const ignored = [];
30
+ if (value.tunnelToken !== undefined && value.tunnelToken !== '')
31
+ ignored.push('tunnelToken');
32
+ if (value.publicBaseUrl !== undefined && value.publicBaseUrl !== '')
33
+ ignored.push('publicBaseUrl');
34
+ return { mode: 'quick', targetUrl: `http://127.0.0.1:${String(port)}`, ignored };
35
+ }
36
+ if (value.tunnelToken !== undefined && value.tunnelToken !== '') {
37
+ if (value.publicBaseUrl !== undefined && isHttpUrl(value.publicBaseUrl)) {
38
+ return { mode: 'named', token: value.tunnelToken, publicUrl: value.publicBaseUrl };
39
+ }
40
+ return { mode: 'off' };
41
+ }
42
+ return { mode: 'off' };
43
+ }
@@ -1,9 +1,12 @@
1
1
  /**
2
- * Auto-tunnel manager: spawns a Cloudflare quick tunnel (`cloudflared
3
- * tunnel --url <local>`) through the `cloudflared` npm package — its
4
- * postinstall downloads the platform binary, so no user-side tooling is
5
- * involved — surfaces the minted `https://xxx.trycloudflare.com` URL, and
6
- * restarts the process after unexpected exits with exponential backoff.
2
+ * Auto-tunnel manager: spawns a Cloudflare tunnel through the `cloudflared`
3
+ * npm package — its postinstall downloads the platform binary, so no
4
+ * user-side tooling is involved — surfaces the public URL, and restarts the
5
+ * process after unexpected exits with exponential backoff. Two modes: the
6
+ * accountless quick tunnel (`https://xxx.trycloudflare.com`, hostname minted
7
+ * per start) and an account named tunnel (`cloudflared tunnel run --token`,
8
+ * fixed public hostname — the mode that lets a paired phone keep its
9
+ * bookmark and pairing cookie across restarts).
7
10
  *
8
11
  * The cloudflared package's Tunnel is a thin spawn wrapper; this manager
9
12
  * owns the lifecycle policy (binary readiness, URL timeout, restart
@@ -13,6 +16,42 @@
13
16
  */
14
17
  /** The observable tunnel lifecycle the settings/panel surfaces render. */
15
18
  export type TunnelPhase = 'stopped' | 'starting' | 'running' | 'failed';
19
+ /**
20
+ * What the manager should run. `quick` is the accountless trycloudflare
21
+ * tunnel whose hostname is minted per start (and dies with the process);
22
+ * `named` is an account tunnel whose public hostname is fixed — the point
23
+ * of the named mode is that a paired phone keeps its bookmark and its
24
+ * pairing cookie across restarts.
25
+ */
26
+ export type TunnelTarget = {
27
+ kind: 'quick';
28
+ targetUrl: string;
29
+ originHostHeader?: string;
30
+ } | {
31
+ kind: 'named';
32
+ token: string;
33
+ publicUrl: string;
34
+ };
35
+ /** The event face the named adapter wraps (a cloudflared package Tunnel). */
36
+ interface NamedTunnelProcess {
37
+ on(event: 'connected', listener: () => void): unknown;
38
+ on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
39
+ on(event: 'error', listener: (value: Error) => void): unknown;
40
+ off(event: string, listener: (...args: any[]) => void): unknown;
41
+ stop(): boolean;
42
+ }
43
+ /**
44
+ * Wrap a named-tunnel process so it fits the quick-tunnel handle shape: the
45
+ * fixed public URL is reported through the same `url` event once the first
46
+ * edge connection registers (cloudflared fires `connected` per connection,
47
+ * so only the first is taken), and exit/error pass through. The manager's
48
+ * URL timeout, crash-restart backoff, and stop semantics then stay fully
49
+ * mode-agnostic.
50
+ * @param inner - the running named-tunnel process.
51
+ * @param publicUrl - the fixed public hostname ingress maps to this server.
52
+ * @returns a handle emitting `url` once the tunnel is reachable.
53
+ */
54
+ export declare function namedTunnelHandle(inner: NamedTunnelProcess, publicUrl: string): TunnelHandle;
16
55
  /** One tunnel status frame. */
17
56
  export interface TunnelInfo {
18
57
  phase: TunnelPhase;
@@ -29,8 +68,8 @@ export interface TunnelHandle {
29
68
  }
30
69
  /** Injectable seams (defaults are the real cloudflared package + node timers). */
31
70
  export interface TunnelManagerOptions {
32
- /** Spawn one quick tunnel toward the local target URL. */
33
- factory?: (targetUrl: string) => TunnelHandle;
71
+ /** Spawn one tunnel process for the target (quick or named). */
72
+ factory?: (target: TunnelTarget) => TunnelHandle;
34
73
  /** Make sure the cloudflared binary exists, downloading it when absent. */
35
74
  ensureBinary?: () => Promise<void>;
36
75
  /** Wait up to this long for the tunnel URL before failing the attempt. */
@@ -45,6 +84,15 @@ export interface TunnelManagerOptions {
45
84
  clearTimeout(t: unknown): void;
46
85
  };
47
86
  }
87
+ /**
88
+ * Spawn flags for one quick target. `originHostHeader` stamps the Host the
89
+ * local webserver sees: the plugin's phone-facing fence trusts the configured
90
+ * public host (the stable relay origin, Host-bound) and the harness
91
+ * browser-auth binds cookies to the Host authority, while the Workers relay
92
+ * cannot control the origin-side Host at all (fetch forces it to the URL
93
+ * authority) — so the connector must restamp it with the stable origin.
94
+ */
95
+ export declare function quickTunnelFlags(originHostHeader?: string): Record<string, string | boolean>;
48
96
  /**
49
97
  * Own the lifecycle of one auto-tunnel: start/stop, URL surfacing, and
50
98
  * crash-restart backoff.
@@ -59,7 +107,7 @@ export declare class TunnelManager {
59
107
  private phase;
60
108
  private url;
61
109
  private error;
62
- private targetUrl;
110
+ private target;
63
111
  private handle;
64
112
  private urlTimer;
65
113
  private restartTimer;
@@ -75,12 +123,16 @@ export declare class TunnelManager {
75
123
  /** The current status frame. */
76
124
  get info(): TunnelInfo;
77
125
  /**
78
- * Start (or keep) a quick tunnel toward `targetUrl`. Restarting with a
79
- * different target tears the old tunnel down first; restarting with the
80
- * same target while running is a no-op.
81
- * @param targetUrl - the local URL to expose, e.g. `http://127.0.0.1:3080`.
126
+ * Start (or keep) a tunnel toward `target`. Restarting with a different
127
+ * target tears the old tunnel down first; restarting with the same target
128
+ * while running is a no-op. A string is the quick mode's local target URL.
129
+ *
130
+ * In quick mode the URL surfaces when cloudflared mints the ephemeral
131
+ * trycloudflare hostname; in named mode the fixed `publicUrl` surfaces
132
+ * once the first edge connection registers (see {@link namedTunnelHandle}).
133
+ * @param target - what to run (a string means quick toward that local URL).
82
134
  */
83
- start(targetUrl: string): void;
135
+ start(target: TunnelTarget | string): void;
84
136
  /** Stop the tunnel for good: no restarts, no state. */
85
137
  stop(): void;
86
138
  /** Alias of {@link stop} for plugin-effect disposal. */
@@ -97,4 +149,5 @@ export declare class TunnelManager {
97
149
  private teardown;
98
150
  private setPhase;
99
151
  }
152
+ export {};
100
153
  //# sourceMappingURL=tunnel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/tunnel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,0EAA0E;AAC1E,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;AAEvE,+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,0DAA0D;IAC1D,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,YAAY,CAAA;IAC7C,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;AAwBD;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAC7D,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,SAAS,CAAoB;IACrC,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;;;;;OAKG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAU9B,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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"}
@@ -1,9 +1,12 @@
1
1
  /**
2
- * Auto-tunnel manager: spawns a Cloudflare quick tunnel (`cloudflared
3
- * tunnel --url <local>`) through the `cloudflared` npm package — its
4
- * postinstall downloads the platform binary, so no user-side tooling is
5
- * involved — surfaces the minted `https://xxx.trycloudflare.com` URL, and
6
- * restarts the process after unexpected exits with exponential backoff.
2
+ * Auto-tunnel manager: spawns a Cloudflare tunnel through the `cloudflared`
3
+ * npm package — its postinstall downloads the platform binary, so no
4
+ * user-side tooling is involved — surfaces the public URL, and restarts the
5
+ * process after unexpected exits with exponential backoff. Two modes: the
6
+ * accountless quick tunnel (`https://xxx.trycloudflare.com`, hostname minted
7
+ * per start) and an account named tunnel (`cloudflared tunnel run --token`,
8
+ * fixed public hostname — the mode that lets a paired phone keep its
9
+ * bookmark and pairing cookie across restarts).
7
10
  *
8
11
  * The cloudflared package's Tunnel is a thin spawn wrapper; this manager
9
12
  * owns the lifecycle policy (binary readiness, URL timeout, restart
@@ -12,15 +15,76 @@
12
15
  * a real binary or network.
13
16
  */
14
17
  import { existsSync } from 'node:fs';
18
+ import { EventEmitter } from 'node:events';
15
19
  import { bin, install, Tunnel } from 'cloudflared';
20
+ /** Compare two targets for the start idempotence check. */
21
+ function sameTarget(left, right) {
22
+ if (left === undefined)
23
+ return false;
24
+ return JSON.stringify(left) === JSON.stringify(right);
25
+ }
26
+ /**
27
+ * Wrap a named-tunnel process so it fits the quick-tunnel handle shape: the
28
+ * fixed public URL is reported through the same `url` event once the first
29
+ * edge connection registers (cloudflared fires `connected` per connection,
30
+ * so only the first is taken), and exit/error pass through. The manager's
31
+ * URL timeout, crash-restart backoff, and stop semantics then stay fully
32
+ * mode-agnostic.
33
+ * @param inner - the running named-tunnel process.
34
+ * @param publicUrl - the fixed public hostname ingress maps to this server.
35
+ * @returns a handle emitting `url` once the tunnel is reachable.
36
+ */
37
+ export function namedTunnelHandle(inner, publicUrl) {
38
+ const handle = new EventEmitter();
39
+ let registered = false;
40
+ const onConnected = () => {
41
+ // cloudflared fires `connected` per edge connection (four on a healthy
42
+ // registration); the fixed URL only needs reporting once.
43
+ if (registered)
44
+ return;
45
+ registered = true;
46
+ handle.emit('url', publicUrl);
47
+ };
48
+ const onExit = (code, signal) => {
49
+ handle.emit('exit', code, signal);
50
+ };
51
+ const onError = (value) => {
52
+ handle.emit('error', value);
53
+ };
54
+ inner.on('connected', onConnected);
55
+ inner.on('exit', onExit);
56
+ inner.on('error', onError);
57
+ handle.stop = () => {
58
+ inner.off('connected', onConnected);
59
+ inner.off('exit', onExit);
60
+ inner.off('error', onError);
61
+ return inner.stop();
62
+ };
63
+ return handle;
64
+ }
16
65
  /** Default binary readiness: download the platform binary on first use. */
17
66
  async function defaultEnsureBinary() {
18
67
  if (existsSync(bin))
19
68
  return;
20
69
  await install(bin);
21
70
  }
22
- /** Default factory: the cloudflared package's quick tunnel (no account). */
23
- function defaultFactory(targetUrl) {
71
+ /** The flags every tunnel mode shares (see the quick factory comment). */
72
+ const SHARED_TUNNEL_FLAGS = { '--no-autoupdate': true, '--protocol': 'http2' };
73
+ /**
74
+ * Spawn flags for one quick target. `originHostHeader` stamps the Host the
75
+ * local webserver sees: the plugin's phone-facing fence trusts the configured
76
+ * public host (the stable relay origin, Host-bound) and the harness
77
+ * browser-auth binds cookies to the Host authority, while the Workers relay
78
+ * cannot control the origin-side Host at all (fetch forces it to the URL
79
+ * authority) — so the connector must restamp it with the stable origin.
80
+ */
81
+ export function quickTunnelFlags(originHostHeader) {
82
+ return originHostHeader === undefined
83
+ ? { ...SHARED_TUNNEL_FLAGS }
84
+ : { ...SHARED_TUNNEL_FLAGS, '--http-host-header': originHostHeader };
85
+ }
86
+ /** Default factory: the cloudflared package's quick and named tunnels. */
87
+ function defaultFactory(target) {
24
88
  // `--no-autoupdate`: the binary must never upgrade itself out from under
25
89
  // the manager (a self-updated binary would break the pinned lifecycle).
26
90
  // `--protocol http2`: the default `auto` prefers QUIC (UDP 7844), which
@@ -29,7 +93,10 @@ function defaultFactory(targetUrl) {
29
93
  // and the auto fallback never fires on a half-open QUIC handshake. http2
30
94
  // rides TCP through the same paths reliably (verified live: auto stuck at
31
95
  // ready 0 on two fresh registrations; http2 ready immediately).
32
- return Tunnel.quick(targetUrl, { '--no-autoupdate': true, '--protocol': 'http2' });
96
+ if (target.kind === 'quick') {
97
+ return Tunnel.quick(target.targetUrl, quickTunnelFlags(target.originHostHeader));
98
+ }
99
+ return namedTunnelHandle(Tunnel.withToken(target.token, { ...SHARED_TUNNEL_FLAGS }), target.publicUrl);
33
100
  }
34
101
  /** Node timers. */
35
102
  const nodeTimer = { setTimeout, clearTimeout };
@@ -47,7 +114,7 @@ export class TunnelManager {
47
114
  phase = 'stopped';
48
115
  url;
49
116
  error;
50
- targetUrl;
117
+ target;
51
118
  handle;
52
119
  urlTimer;
53
120
  restartTimer;
@@ -78,17 +145,22 @@ export class TunnelManager {
78
145
  };
79
146
  }
80
147
  /**
81
- * Start (or keep) a quick tunnel toward `targetUrl`. Restarting with a
82
- * different target tears the old tunnel down first; restarting with the
83
- * same target while running is a no-op.
84
- * @param targetUrl - the local URL to expose, e.g. `http://127.0.0.1:3080`.
148
+ * Start (or keep) a tunnel toward `target`. Restarting with a different
149
+ * target tears the old tunnel down first; restarting with the same target
150
+ * while running is a no-op. A string is the quick mode's local target URL.
151
+ *
152
+ * In quick mode the URL surfaces when cloudflared mints the ephemeral
153
+ * trycloudflare hostname; in named mode the fixed `publicUrl` surfaces
154
+ * once the first edge connection registers (see {@link namedTunnelHandle}).
155
+ * @param target - what to run (a string means quick toward that local URL).
85
156
  */
86
- start(targetUrl) {
87
- if (this.targetUrl === targetUrl && (this.phase === 'starting' || this.phase === 'running'))
157
+ start(target) {
158
+ const resolved = typeof target === 'string' ? { kind: 'quick', targetUrl: target } : target;
159
+ if (sameTarget(this.target, resolved) && (this.phase === 'starting' || this.phase === 'running'))
88
160
  return;
89
161
  this.teardown();
90
162
  this.stopping = false;
91
- this.targetUrl = targetUrl;
163
+ this.target = resolved;
92
164
  this.attempts = 0;
93
165
  this.generation += 1;
94
166
  this.attempt();
@@ -97,7 +169,7 @@ export class TunnelManager {
97
169
  stop() {
98
170
  this.teardown();
99
171
  this.stopping = false;
100
- this.targetUrl = undefined;
172
+ this.target = undefined;
101
173
  this.setPhase('stopped');
102
174
  }
103
175
  /** Alias of {@link stop} for plugin-effect disposal. */
@@ -115,7 +187,7 @@ export class TunnelManager {
115
187
  return () => { this.phaseListeners.delete(listener); };
116
188
  }
117
189
  attempt() {
118
- if (this.stopping || this.targetUrl === undefined)
190
+ if (this.stopping || this.target === undefined)
119
191
  return;
120
192
  const gen = this.generation;
121
193
  this.setPhase('starting');
@@ -123,9 +195,9 @@ export class TunnelManager {
123
195
  this.url = undefined;
124
196
  this.error = undefined;
125
197
  void this.ensureBinary().then(() => {
126
- if (this.stopping || this.targetUrl === undefined || gen !== this.generation)
198
+ if (this.stopping || this.target === undefined || gen !== this.generation)
127
199
  return;
128
- const handle = this.factory(this.targetUrl);
200
+ const handle = this.factory(this.target);
129
201
  this.handle = handle;
130
202
  this.urlTimer = this.timer.setTimeout(() => {
131
203
  // The tunnel never reported a URL: kill it and retry with backoff.
@@ -150,7 +222,7 @@ export class TunnelManager {
150
222
  });
151
223
  }).catch((value) => {
152
224
  // Binary install failed (no network, no platform build): report it.
153
- if (this.stopping || this.targetUrl === undefined || gen !== this.generation)
225
+ if (this.stopping || this.target === undefined || gen !== this.generation)
154
226
  return;
155
227
  const message = value instanceof Error ? value.message : String(value);
156
228
  this.fail(`could not obtain the cloudflared binary: ${message}`);
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.3.12",
4
+ "version": "0.3.14",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -36,7 +36,7 @@
36
36
  "license": "Apache-2.0",
37
37
  "dsh": {
38
38
  "engines": {
39
- "dsh": ">=0.1.2-alpha.4"
39
+ "dsh": ">=0.1.2-rc.1"
40
40
  },
41
41
  "bundle": {
42
42
  "patch": "./cordis.patch.yml"
@@ -63,22 +63,22 @@
63
63
  },
64
64
  "devDependencies": {
65
65
  "@deepseek-ai/cordis": "^4.0.2",
66
- "@deepseek-ai/dsh-agent": "^0.1.2-alpha.4",
67
- "@deepseek-ai/dsh-api-remotes": "^0.1.2-alpha.4",
68
- "@deepseek-ai/dsh-api-session-controller": "^0.1.2-alpha.4",
69
- "@deepseek-ai/dsh-api-workspace-controller": "^0.1.2-alpha.4",
70
- "@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.4",
71
- "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.4",
72
- "@deepseek-ai/dsh-client-store": "^0.1.2-alpha.4",
73
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.4",
74
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-alpha.4",
75
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.4",
76
- "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.2-alpha.4",
77
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.4",
78
- "@deepseek-ai/dsh-commands": "^0.1.2-alpha.4",
79
- "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.4",
80
- "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.4",
81
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.4",
66
+ "@deepseek-ai/dsh-agent": "^0.1.2-rc.1",
67
+ "@deepseek-ai/dsh-api-remotes": "^0.1.2-rc.1",
68
+ "@deepseek-ai/dsh-api-session-controller": "^0.1.2-rc.1",
69
+ "@deepseek-ai/dsh-api-workspace-controller": "^0.1.2-rc.1",
70
+ "@deepseek-ai/dsh-client-connection": "^0.1.2-rc.1",
71
+ "@deepseek-ai/dsh-client-locale": "^0.1.2-rc.1",
72
+ "@deepseek-ai/dsh-client-store": "^0.1.2-rc.1",
73
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-rc.1",
74
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-rc.1",
75
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-rc.1",
76
+ "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.2-rc.1",
77
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1",
78
+ "@deepseek-ai/dsh-commands": "^0.1.2-rc.1",
79
+ "@deepseek-ai/dsh-host-webserver": "^0.1.2-rc.1",
80
+ "@deepseek-ai/dsh-invariants": "^0.1.2-rc.1",
81
+ "@deepseek-ai/dsh-settings": "^0.1.2-rc.1",
82
82
  "@testing-library/dom": "^10.4.1",
83
83
  "@testing-library/react": "^16.3.2",
84
84
  "@types/node": "^22.20.0",
@@ -12,7 +12,7 @@ import { createPortal } from 'react-dom'
12
12
  import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
13
13
  import type { PairingPhase } from '../pairing.ts'
14
14
  import { RemotePanel, type PanelState } from './RemotePanel.tsx'
15
- import { copyText, issuePair, revokePair, stopPair, type DeviceFrame, type IssueResponse, type PairStateFrame, type TunnelStatusFrame } from './pair-api.ts'
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
17
  import { UpdateEntry } from './UpdateEntry.tsx'
18
18
  import css from './remote.module.css'
@@ -43,6 +43,7 @@ function mergeFrame(state: PanelState, frame: PairStateFrame): PanelState {
43
43
  onlineCount: frame.onlineCount,
44
44
  devices: frame.devices ?? [],
45
45
  ...(frame.tunnel !== undefined ? { tunnel: frame.tunnel as TunnelStatusFrame } : {}),
46
+ ...(frame.relay !== undefined ? { relay: frame.relay } : {}),
46
47
  ...(frame.posture !== undefined ? { posture: frame.posture } : {}),
47
48
  }
48
49
  }
@@ -16,6 +16,7 @@ import {
16
16
  formatLastSeen,
17
17
  type DeviceFrame,
18
18
  type PostureFrame,
19
+ type RelayStatusFrame,
19
20
  type TunnelStatusFrame,
20
21
  } from './pair-api.ts'
21
22
  import { deviceNameFromUserAgent } from './device-name.ts'
@@ -46,6 +47,8 @@ export type PanelState =
46
47
  publicBaseUrl?: string
47
48
  /** Auto-tunnel status, while the auto-tunnel feature is active. */
48
49
  tunnel?: TunnelStatusFrame
50
+ /** Relay-registry status, while the stable-origin relay is in play. */
51
+ relay?: RelayStatusFrame
49
52
  /** Latest /api posture probe, once a round has completed. */
50
53
  posture?: PostureFrame
51
54
  }
@@ -164,6 +167,14 @@ export function RemotePanel({ t, state, copied, onClose, onStop, onRefresh, onCo
164
167
  : t('tunnel.starting')}
165
168
  </p>
166
169
  )}
170
+ {state.relay?.state === 'registering' && (
171
+ <p className={css.tunnelNote} role="status">{t('relay.registering')}</p>
172
+ )}
173
+ {state.relay?.state === 'failed' && (
174
+ <p className={css.tunnelFailed} role="status">
175
+ {t('relay.failed', { error: state.relay.error ?? t('tunnel.unknownError') })}
176
+ </p>
177
+ )}
167
178
 
168
179
  {(state.publicBaseUrl !== undefined || state.lanAddresses.length > 1) && (
169
180
  <fieldset className={css.addresses}>