@openchambery/web 1.18.3 → 1.18.4-beta.10

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 (113) hide show
  1. package/bin/cli.test.js +25 -0
  2. package/bin/lib/DOCUMENTATION.md +2 -1
  3. package/bin/lib/cli-args.js +19 -0
  4. package/bin/lib/commands-serve.js +73 -4
  5. package/dist/assets/{AgentAvatar-BvhXxFvg.js → AgentAvatar-DAnelAnB.js} +1 -1
  6. package/dist/assets/{AssistantView-DvCGcMdh.js → AssistantView-C7zpkYc-.js} +1 -1
  7. package/dist/assets/{ChatContainer-B8XV-b62.js → ChatContainer-tzO5rvhQ.js} +20 -20
  8. package/dist/assets/{DiagramView-3oWJD3HI.js → DiagramView-D3WjJqff.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-98buIVdv.js → FadeInOnReveal-QxF1vuqX.js} +1 -1
  10. package/dist/assets/{Icon-BhSwbLaF.js → Icon-D-X70irR.js} +1 -1
  11. package/dist/assets/{MarkdownRendererImpl-ZBTLZ4_Z.js → MarkdownRendererImpl-CBtszecD.js} +1 -1
  12. package/dist/assets/{MobileDetailNavigation-C129Cfy_.js → MobileDetailNavigation-CP3L6x1-.js} +1 -1
  13. package/dist/assets/MobileShareBridge-BCuk5mbi.js +4 -0
  14. package/dist/assets/MobileSurface-Dz_oFrar.js +2 -0
  15. package/dist/assets/{MultiRunWindow-DdnQ8IVy.js → MultiRunWindow-2JHpZSxf.js} +1 -1
  16. package/dist/assets/{OnboardingScreen-C1n_wVb-.js → OnboardingScreen-BhSzVmYo.js} +1 -1
  17. package/dist/assets/{OverlayScrollbar-CXFN6Cqq.js → OverlayScrollbar-CHXk_jvF.js} +1 -1
  18. package/dist/assets/{SettingsView-B7ETzV-K.js → SettingsView-_x5DRAHK.js} +10 -10
  19. package/dist/assets/{SettingsWindow-C4qb5YJp.js → SettingsWindow-jWfQNCAS.js} +1 -1
  20. package/dist/assets/{TerminalView-DZ0003gd.js → TerminalView-DLlxlIWQ.js} +1 -1
  21. package/dist/assets/{ToolOutputDialog-C2KTvp-_.js → ToolOutputDialog-Cy-cG7Vn.js} +1 -1
  22. package/dist/assets/appThemeRegistry-DWUqOAxX.js +2 -0
  23. package/dist/assets/{button-BQY5ugjh.js → button-CwLFtUNT.js} +1 -1
  24. package/dist/assets/{clipboard-BvJ7NunE.js → clipboard-DhOMUCwT.js} +1 -1
  25. package/dist/assets/{collapsible-CqcZGw3W.js → collapsible-CP1-_pY6.js} +1 -1
  26. package/dist/assets/{context-menu-DuFC0HbN.js → context-menu-BOZfQke9.js} +1 -1
  27. package/dist/assets/desktopHosts-Cr3LE55y.js +1 -0
  28. package/dist/assets/desktopRelayRestore-VfMK9fdk.js +1 -0
  29. package/dist/assets/{diagram-B8D9Klct.js → diagram-rYL80jaS.js} +1 -1
  30. package/dist/assets/{dialog-sGFkIKP2.js → dialog-BoAAoL5Z.js} +1 -1
  31. package/dist/assets/{es-BuinIayO.js → es-BXQb1dPR.js} +2 -2
  32. package/dist/assets/{fileQueries-BSE4ycQt.js → fileQueries-CXaw4b1W.js} +1 -1
  33. package/dist/assets/{fr-CT7us58h.js → fr-BTyDIvxw.js} +3 -3
  34. package/dist/assets/{gitApi-7wspWRVB.js → gitApi-BNZDCH9W.js} +1 -1
  35. package/dist/assets/{input-CgIoxUJc.js → input-D52Jicb1.js} +1 -1
  36. package/dist/assets/{insertionBoundaries-DoxhiE_l.js → insertionBoundaries-CFZyQPIS.js} +1 -1
  37. package/dist/assets/{ja-CCuAH4nO.js → ja-fcQgtBFy.js} +2 -2
  38. package/dist/assets/{ko-lXbhGt6P.js → ko-CgoVt1eb.js} +2 -2
  39. package/dist/assets/{main-DcfiRQEP.js → main-5R9S2b57.js} +2 -2
  40. package/dist/assets/{main-DG8UmEpF.js → main-BNYmBvzd.js} +13 -13
  41. package/dist/assets/{markdown-worker-C5Om4TVK.js → markdown-worker-N9d_Z-7s.js} +1 -1
  42. package/dist/assets/{markdownTheme-BdF4C4-o.js → markdownTheme-Bl0Sy0Lf.js} +1 -1
  43. package/dist/assets/{miniChat-BL9ppFy8.js → miniChat-CExPRYpL.js} +2 -2
  44. package/dist/assets/mobile-C7QGpUuZ.js +2 -0
  45. package/dist/assets/{multirun-BXAN6NKd.js → multirun-QU06PjW9.js} +1 -1
  46. package/dist/assets/persistence-vZ7H8WKh.js +3 -0
  47. package/dist/assets/pl-DVDnk2fT.js +15 -0
  48. package/dist/assets/platform-CTaznShj.js +2 -0
  49. package/dist/assets/{projectMeta-Bail3NaE.js → projectMeta-CVVj8TB_.js} +2 -2
  50. package/dist/assets/{pt-BR-DWbGs1bc.js → pt-BR-ofV_Qcd_.js} +2 -2
  51. package/dist/assets/radio-CTMWDitv.js +1 -0
  52. package/dist/assets/{renderElectronMiniChatApp-CU-gCTkr.js → renderElectronMiniChatApp-QBAHpTW_.js} +2 -2
  53. package/dist/assets/{renderMobileApp-BG5Csi9J.js → renderMobileApp-CodaHpQJ.js} +5 -5
  54. package/dist/assets/runtimeConfig-BWMVRpO_.js +2 -0
  55. package/dist/assets/{runtimeEndpointReset-Bwss1HBr.js → runtimeEndpointReset-CqrqWzhd.js} +2 -2
  56. package/dist/assets/{sortable-tabs-strip-pSnPuBXA.js → sortable-tabs-strip-N3j5So4j.js} +1 -1
  57. package/dist/assets/src-Dbdlm5x1.css +2 -0
  58. package/dist/assets/{store-BuXogmxp.js → store-Hr-xvtDJ.js} +2 -2
  59. package/dist/assets/{terminalApi-jfes-HIU.js → terminalApi-KDUc5mh2.js} +1 -1
  60. package/dist/assets/{tooltip-rN8LrxMF.js → tooltip-CaCe-GAm.js} +1 -1
  61. package/dist/assets/{uk-DgOxtvS7.js → uk-Dzob0yMx.js} +2 -2
  62. package/dist/assets/{url-B0Ak7X6b.js → url-B-FWrnxu.js} +1 -1
  63. package/dist/assets/useAppFontEffects-CKhScw6W.js +7 -0
  64. package/dist/assets/{useAssistantUIStore-SPh025NE.js → useAssistantUIStore-ifqNyja4.js} +1 -1
  65. package/dist/assets/{useDesktopWindowControlsLayout-DNU1P1i5.js → useDesktopWindowControlsLayout-D9kVYRmX.js} +1 -1
  66. package/dist/assets/{useEffectiveDirectory-Cc1z5OgH.js → useEffectiveDirectory-srkYLmiC.js} +1 -1
  67. package/dist/assets/{useFontPreferences-Bz3crRRO.js → useFontPreferences-D_LFFhFS.js} +1 -1
  68. package/dist/assets/useMobileNavigationStore-DmXpNTih.js +476 -0
  69. package/dist/assets/useSessionAutoCleanup-BB45s--Y.js +1 -0
  70. package/dist/assets/useUIStore-NfqK-HA7.js +1 -0
  71. package/dist/assets/{useWorkerHighlightedLines-C3FzDa-K.js → useWorkerHighlightedLines-DyP0kh5l.js} +1 -1
  72. package/dist/assets/{utils-DfPDhqys.js → utils-C2P0wcWO.js} +1 -1
  73. package/dist/assets/{zh-CN-BPGatJp9.js → zh-CN-Sndj2nhS.js} +2 -2
  74. package/dist/assets/{zh-TW-qUWWsaxK.js → zh-TW-S24a8EMD.js} +2 -2
  75. package/dist/index.html +9 -9
  76. package/dist/mini-chat.html +9 -9
  77. package/dist/mobile.html +9 -9
  78. package/package.json +1 -1
  79. package/server/index.d.ts +0 -4
  80. package/server/index.js +11 -20
  81. package/server/lib/client-auth/pairing.js +1 -7
  82. package/server/lib/client-auth/pairing.test.js +0 -45
  83. package/server/lib/opencode/DOCUMENTATION.md +4 -4
  84. package/server/lib/opencode/bootstrap-runtime.js +0 -6
  85. package/server/lib/opencode/cli-entry-runtime.js +5 -1
  86. package/server/lib/opencode/cli-entry-runtime.test.js +17 -0
  87. package/server/lib/opencode/core-routes.js +0 -3
  88. package/server/lib/opencode/core-routes.test.js +0 -15
  89. package/server/lib/opencode/openchamber-routes.js +0 -11
  90. package/server/lib/relay/DOCUMENTATION.md +3 -45
  91. package/server/lib/relay/host-client.js +1 -4
  92. package/server/lib/relay/service.js +28 -16
  93. package/server/lib/relay/service.test.js +64 -4
  94. package/server/lib/relay/tunnel-host.js +2 -52
  95. package/server/lib/relay/tunnel-host.test.js +2 -250
  96. package/dist/assets/MobileShareBridge-DMka9_SA.js +0 -4
  97. package/dist/assets/MobileSurface-Bld2FOxI.js +0 -2
  98. package/dist/assets/appThemeRegistry-CscNaEL-.js +0 -2
  99. package/dist/assets/desktopHosts-aA40MHzB.js +0 -1
  100. package/dist/assets/desktopRelayRestore-CxUYRC6v.js +0 -1
  101. package/dist/assets/mobile-BRxbjeRa.js +0 -2
  102. package/dist/assets/persistence-DwXJK4-j.js +0 -3
  103. package/dist/assets/pl-DCsKBRAg.js +0 -15
  104. package/dist/assets/platform-CXJwZIZe.js +0 -2
  105. package/dist/assets/radio-CClZkzaR.js +0 -1
  106. package/dist/assets/runtimeConfig-DC0OJ777.js +0 -2
  107. package/dist/assets/src-DF4hiLrB.css +0 -2
  108. package/dist/assets/useAppFontEffects-DO3Jc7RO.js +0 -7
  109. package/dist/assets/useMobileNavigationStore-Dw9Ibked.js +0 -476
  110. package/dist/assets/useSessionAutoCleanup-Bpf4lLlD.js +0 -1
  111. package/dist/assets/useUIStore-BJpkPwky.js +0 -1
  112. package/server/lib/desktop-hosts/routes.js +0 -157
  113. package/server/lib/desktop-hosts/routes.test.js +0 -227
package/dist/index.html CHANGED
@@ -552,25 +552,25 @@
552
552
  opacity: 0.72;
553
553
  }
554
554
  </style>
555
- <script type="module" crossorigin src="/assets/main-DcfiRQEP.js"></script>
555
+ <script type="module" crossorigin src="/assets/main-5R9S2b57.js"></script>
556
556
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
557
557
  <link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
558
558
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
559
- <link rel="modulepreload" crossorigin href="/assets/useUIStore-BJpkPwky.js">
559
+ <link rel="modulepreload" crossorigin href="/assets/useUIStore-NfqK-HA7.js">
560
560
  <link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
561
561
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
562
562
  <link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
563
- <link rel="modulepreload" crossorigin href="/assets/persistence-DwXJK4-j.js">
564
- <link rel="modulepreload" crossorigin href="/assets/desktopHosts-aA40MHzB.js">
565
- <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-CxUYRC6v.js">
563
+ <link rel="modulepreload" crossorigin href="/assets/persistence-vZ7H8WKh.js">
564
+ <link rel="modulepreload" crossorigin href="/assets/desktopHosts-Cr3LE55y.js">
565
+ <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-VfMK9fdk.js">
566
566
  <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-C07HQypa.js">
567
567
  <link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
568
- <link rel="modulepreload" crossorigin href="/assets/platform-CXJwZIZe.js">
569
- <link rel="modulepreload" crossorigin href="/assets/terminalApi-jfes-HIU.js">
568
+ <link rel="modulepreload" crossorigin href="/assets/platform-CTaznShj.js">
569
+ <link rel="modulepreload" crossorigin href="/assets/terminalApi-KDUc5mh2.js">
570
570
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
571
571
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
572
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DC0OJ777.js">
573
- <link rel="stylesheet" crossorigin href="/assets/src-DF4hiLrB.css">
572
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BWMVRpO_.js">
573
+ <link rel="stylesheet" crossorigin href="/assets/src-Dbdlm5x1.css">
574
574
  </head>
575
575
  <body class="h-full bg-background text-foreground">
576
576
  <!-- Keep the startup layer outside #root. React replaces every child of
@@ -74,25 +74,25 @@
74
74
  }
75
75
  </style>
76
76
 
77
- <script type="module" crossorigin src="/assets/miniChat-BL9ppFy8.js"></script>
77
+ <script type="module" crossorigin src="/assets/miniChat-CExPRYpL.js"></script>
78
78
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
79
79
  <link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
80
80
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
81
- <link rel="modulepreload" crossorigin href="/assets/useUIStore-BJpkPwky.js">
81
+ <link rel="modulepreload" crossorigin href="/assets/useUIStore-NfqK-HA7.js">
82
82
  <link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
83
83
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
84
84
  <link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
85
- <link rel="modulepreload" crossorigin href="/assets/persistence-DwXJK4-j.js">
86
- <link rel="modulepreload" crossorigin href="/assets/desktopHosts-aA40MHzB.js">
87
- <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-CxUYRC6v.js">
85
+ <link rel="modulepreload" crossorigin href="/assets/persistence-vZ7H8WKh.js">
86
+ <link rel="modulepreload" crossorigin href="/assets/desktopHosts-Cr3LE55y.js">
87
+ <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-VfMK9fdk.js">
88
88
  <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-C07HQypa.js">
89
89
  <link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
90
- <link rel="modulepreload" crossorigin href="/assets/platform-CXJwZIZe.js">
91
- <link rel="modulepreload" crossorigin href="/assets/terminalApi-jfes-HIU.js">
90
+ <link rel="modulepreload" crossorigin href="/assets/platform-CTaznShj.js">
91
+ <link rel="modulepreload" crossorigin href="/assets/terminalApi-KDUc5mh2.js">
92
92
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
93
93
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
94
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DC0OJ777.js">
95
- <link rel="stylesheet" crossorigin href="/assets/src-DF4hiLrB.css">
94
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BWMVRpO_.js">
95
+ <link rel="stylesheet" crossorigin href="/assets/src-Dbdlm5x1.css">
96
96
  </head>
97
97
  <body class="h-full bg-background text-foreground">
98
98
  <div id="root" class="h-full">
package/dist/mobile.html CHANGED
@@ -63,25 +63,25 @@
63
63
  })();
64
64
  </script>
65
65
 
66
- <script type="module" crossorigin src="/assets/mobile-BRxbjeRa.js"></script>
66
+ <script type="module" crossorigin src="/assets/mobile-C7QGpUuZ.js"></script>
67
67
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
68
68
  <link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
69
69
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
70
- <link rel="modulepreload" crossorigin href="/assets/useUIStore-BJpkPwky.js">
70
+ <link rel="modulepreload" crossorigin href="/assets/useUIStore-NfqK-HA7.js">
71
71
  <link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
72
72
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
73
73
  <link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
74
- <link rel="modulepreload" crossorigin href="/assets/persistence-DwXJK4-j.js">
75
- <link rel="modulepreload" crossorigin href="/assets/desktopHosts-aA40MHzB.js">
76
- <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-CxUYRC6v.js">
74
+ <link rel="modulepreload" crossorigin href="/assets/persistence-vZ7H8WKh.js">
75
+ <link rel="modulepreload" crossorigin href="/assets/desktopHosts-Cr3LE55y.js">
76
+ <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-VfMK9fdk.js">
77
77
  <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-C07HQypa.js">
78
78
  <link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
79
- <link rel="modulepreload" crossorigin href="/assets/platform-CXJwZIZe.js">
80
- <link rel="modulepreload" crossorigin href="/assets/terminalApi-jfes-HIU.js">
79
+ <link rel="modulepreload" crossorigin href="/assets/platform-CTaznShj.js">
80
+ <link rel="modulepreload" crossorigin href="/assets/terminalApi-KDUc5mh2.js">
81
81
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
82
82
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
83
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DC0OJ777.js">
84
- <link rel="stylesheet" crossorigin href="/assets/src-DF4hiLrB.css">
83
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BWMVRpO_.js">
84
+ <link rel="stylesheet" crossorigin href="/assets/src-Dbdlm5x1.css">
85
85
  </head>
86
86
  <body class="h-full bg-background text-foreground">
87
87
  <div id="root" class="h-full"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openchambery/web",
3
- "version": "1.18.3",
3
+ "version": "1.18.4-beta.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
package/server/index.d.ts CHANGED
@@ -21,10 +21,6 @@ export interface StartWebUiServerOptions {
21
21
  transcriptCacheDbPath?: string | null;
22
22
  messageQueueDbPath?: string | null;
23
23
  messageQueueAttachmentRoot?: string | null;
24
- /** Live SSH local-forward ports for relay target-port routing (Electron). */
25
- getSshRoutingTable?: () => { id: string; localPort: number }[];
26
- /** Mint a stored SSH host clientToken for a ready session (Electron). */
27
- mintSshHostToken?: (hostId: string) => Promise<string>;
28
24
  /**
29
25
  * Optional process-local lock for settings persistence (Electron in-process).
30
26
  * When provided, every settings write/migrate joins this exclusive chain so
package/server/index.js CHANGED
@@ -90,7 +90,7 @@ import { applyRuntimeCorsHeaders } from './lib/request-cors.js';
90
90
  import { createClientPairingRuntime } from './lib/client-auth/pairing.js';
91
91
  import { createPreviewProxyRuntime } from './lib/preview/proxy-runtime.js';
92
92
  import { attachRealtimeProxy } from './lib/realtime-proxy.js';
93
- import { createRelayService, isDesktopRelayHostRuntime } from './lib/relay/service.js';
93
+ import { createRelayService, isRelayHostRuntime } from './lib/relay/service.js';
94
94
  import { createRelayHostLock } from './lib/relay/host-lock.js';
95
95
  import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware';
96
96
  import webPush from 'web-push';
@@ -1184,9 +1184,10 @@ async function main(options = {}) {
1184
1184
  if (h && h !== '127.0.0.1' && h !== 'localhost' && h !== '::1') lanHost = effectiveBindHost;
1185
1185
  }
1186
1186
  const lan = lanHost ? `http://${lanHost.includes(':') ? `[${lanHost}]` : lanHost}:${activePort}` : null;
1187
- // Only the desktop host may advertise relay. Local `dev` / `web` / CLI
1188
- // servers must not present "Anywhere" as available or trigger a probe.
1189
- return { local, lan, relayAvailable: isDesktopRelayHostRuntime() };
1187
+ // Only desktop / SSH-managed remote hosts may advertise relay. Local
1188
+ // `dev` / `web` / ordinary CLI servers must not present "Anywhere" as
1189
+ // available or trigger a probe.
1190
+ return { local, lan, relayAvailable: isRelayHostRuntime() };
1190
1191
  };
1191
1192
  // ALL direct LAN URLs this server is currently reachable on, for the
1192
1193
  // candidates-refresh endpoint: the address the requesting client already
@@ -1248,11 +1249,6 @@ async function main(options = {}) {
1248
1249
  const getDesktopRuntimeConfig = typeof options.getDesktopRuntimeConfig === 'function'
1249
1250
  ? options.getDesktopRuntimeConfig
1250
1251
  : null;
1251
- // Electron injects live SSH local-forward ports; non-desktop defaults to empty
1252
- // so relay target-port routing never falls back to an unvalidated port.
1253
- const getSshRoutingTable = typeof options.getSshRoutingTable === 'function'
1254
- ? options.getSshRoutingTable
1255
- : () => [];
1256
1252
  const sessionIndexDbPath = options.sessionIndexDbPath !== undefined
1257
1253
  ? options.sessionIndexDbPath
1258
1254
  : process.env.OPENCHAMBER_SESSION_INDEX_DB_PATH;
@@ -1478,11 +1474,6 @@ async function main(options = {}) {
1478
1474
  sessionIndexService,
1479
1475
  sessionIndexSyncRuntime,
1480
1476
  transcriptCacheService,
1481
- getSshRoutingTable,
1482
- mintSshHostToken: typeof options.mintSshHostToken === 'function'
1483
- ? options.mintSshHostToken
1484
- : undefined,
1485
- getPairingSession: (id) => clientPairingRuntime.getPairingSession(id),
1486
1477
  });
1487
1478
  uiAuthController = bootstrapResult.uiAuthController;
1488
1479
  realtimeProxyRuntime = attachRealtimeProxy({
@@ -1506,7 +1497,6 @@ async function main(options = {}) {
1506
1497
  readSettingsStrict: readSettingsFromDiskStrict,
1507
1498
  remoteClientAuthRuntime,
1508
1499
  getLocalPort: () => tunnelRuntimeContext.getActivePort(),
1509
- getSshRoutingTable,
1510
1500
  // One relay host per machine: every instance sharing this data dir shares
1511
1501
  // the relay identity (serverId), so concurrent hosts evict each other at
1512
1502
  // the relay worker and devices land on a random local instance.
@@ -1644,15 +1634,16 @@ async function main(options = {}) {
1644
1634
  console.warn('[ScheduledTasks] Failed to start runtime:', error?.message || error);
1645
1635
  }
1646
1636
 
1647
- // Only Electron (OPENCHAMBER_RUNTIME=desktop) opens a relay host-control
1648
- // socket or polls relay demand. Local `dev` / `web` never reconcile, so they
1649
- // cannot inherit a leftover desktop env and start probing the relay.
1637
+ // Only desktop / SSH-managed remotes (OPENCHAMBER_RUNTIME=desktop|ssh-remote)
1638
+ // open a relay host-control socket or poll relay demand. Local `dev` / `web`
1639
+ // never reconcile, so they cannot inherit a leftover desktop env and start
1640
+ // probing the relay.
1650
1641
  let relayReconcileTimer = null;
1651
- if (isDesktopRelayHostRuntime()) {
1642
+ if (isRelayHostRuntime()) {
1652
1643
  void relayService.reconcile();
1653
1644
  // Relay demand can change outside our routes: pending sessions expire, or a
1654
1645
  // paired device is revoked, without hitting our management routes. Poll so
1655
- // the desktop host picks demand up (or drops it) within a minute.
1646
+ // the host picks demand up (or drops it) within a minute.
1656
1647
  relayReconcileTimer = setInterval(() => {
1657
1648
  void relayService.reconcile();
1658
1649
  }, 60_000);
@@ -72,9 +72,6 @@ const publicSession = (session) => ({
72
72
  allowedClientKinds: session.allowedClientKinds,
73
73
  createdByClientId: session.createdByClientId,
74
74
  usesRelay: session.usesRelay === true,
75
- // Optional SSH instance binding for mobile-over-relay host-token mint.
76
- // Null when the pairing is for the desktop local origin (default).
77
- sshHostId: session.sshHostId ?? null,
78
75
  });
79
76
 
80
77
  // A pending session is one that can still be redeemed: not used, not cancelled,
@@ -141,7 +138,6 @@ export const createClientPairingRuntime = ({
141
138
  allowedClientKinds: normalizeAllowedClientKinds(session.allowedClientKinds),
142
139
  createdByClientId: normalizeOptionalString(session.createdByClientId),
143
140
  usesRelay: session.usesRelay === true,
144
- sshHostId: normalizeOptionalString(session.sshHostId),
145
141
  }))
146
142
  .filter((session) => session.secretHash.length > 0)
147
143
  : [],
@@ -180,7 +176,7 @@ export const createClientPairingRuntime = ({
180
176
  });
181
177
  };
182
178
 
183
- const createPairingSession = async ({ label, allowedClientKinds, createdByClientId, usesRelay, sshHostId } = {}) => {
179
+ const createPairingSession = async ({ label, allowedClientKinds, createdByClientId, usesRelay } = {}) => {
184
180
  return withStoreMutation(async () => {
185
181
  const store = await readStore();
186
182
  sweepExpiredSessionsFromStore(store);
@@ -198,8 +194,6 @@ export const createClientPairingRuntime = ({
198
194
  allowedClientKinds: normalizeAllowedClientKinds(allowedClientKinds),
199
195
  createdByClientId: normalizeOptionalString(createdByClientId),
200
196
  usesRelay: usesRelay === true,
201
- // Binds later ssh-host-token mint to this pairing; not used by redeem.
202
- sshHostId: normalizeOptionalString(sshHostId),
203
197
  };
204
198
  store.sessions.push(session);
205
199
  await writeStore(store);
@@ -139,49 +139,4 @@ describe('client auth pairing runtime', () => {
139
139
  expect(ids).not.toContain(expired.pairing.id);
140
140
  expect(ids).toHaveLength(1);
141
141
  });
142
-
143
- it('persists optional sshHostId and exposes it on query after redeem', async () => {
144
- const { dir, runtime } = await makeRuntime();
145
- const created = await runtime.createPairingSession({
146
- allowedClientKinds: ['mobile'],
147
- sshHostId: 'ssh-remote-1',
148
- });
149
- expect(created.pairing.sshHostId).toBe('ssh-remote-1');
150
-
151
- const pending = await runtime.listPendingSessions();
152
- expect(pending).toEqual(expect.arrayContaining([
153
- expect.objectContaining({ id: created.pairing.id, sshHostId: 'ssh-remote-1', usedAt: null }),
154
- ]));
155
-
156
- const lookedUp = await runtime.getPairingSession(created.pairing.id);
157
- expect(lookedUp).toMatchObject({ id: created.pairing.id, sshHostId: 'ssh-remote-1', usedAt: null });
158
-
159
- const storePath = path.join(dir, 'pairing.json');
160
- const storeBefore = JSON.parse(await fs.readFile(storePath, 'utf8'));
161
- expect(storeBefore.sessions.find((s) => s.id === created.pairing.id)?.sshHostId).toBe('ssh-remote-1');
162
-
163
- const redeemed = await runtime.redeemPairingSession({
164
- pairingId: created.pairing.id,
165
- secret: created.pairing.secret,
166
- clientKind: 'mobile',
167
- });
168
- // Redeem still returns the desktop client token; sshHostId is metadata only.
169
- expect(redeemed.token).toBe('token-1');
170
- expect(redeemed.pairing).toMatchObject({
171
- id: created.pairing.id,
172
- sshHostId: 'ssh-remote-1',
173
- });
174
- expect(redeemed.pairing.usedAt).toBeTruthy();
175
-
176
- const after = await runtime.getPairingSession(created.pairing.id);
177
- expect(after).toMatchObject({
178
- id: created.pairing.id,
179
- sshHostId: 'ssh-remote-1',
180
- });
181
- expect(after.usedAt).toBeTruthy();
182
-
183
- // Default pairing has null sshHostId.
184
- const plain = await runtime.createPairingSession();
185
- expect(plain.pairing.sshHostId).toBeNull();
186
- });
187
142
  });
@@ -8,7 +8,7 @@ This module provides OpenCode server integration utilities for the web server ru
8
8
  - `packages/web/server/lib/opencode/auth.js`: provider authentication file operations.
9
9
  - `packages/web/server/lib/opencode/auth-state-runtime.js`: managed OpenCode server auth password/header runtime.
10
10
  - `packages/web/server/lib/opencode/cli-options.js`: CLI/environment option parsing for server startup arguments.
11
- - `packages/web/server/lib/opencode/cli-entry-runtime.js`: CLI entrypoint runtime that detects direct execution, forces `OPENCHAMBER_RUNTIME=web` (so `dev:server` cannot inherit a leftover desktop env and host a relay), parses CLI options, and starts server bootstrap.
11
+ - `packages/web/server/lib/opencode/cli-entry-runtime.js`: CLI entrypoint runtime that detects direct execution, forces `OPENCHAMBER_RUNTIME=web` unless already `ssh-remote` (so `dev:server` cannot inherit a leftover desktop env and host a relay, while SSH-managed remotes may), parses CLI options, and starts server bootstrap.
12
12
  - `packages/web/server/lib/opencode/routes.js`: OpenCode/provider settings and auth-related route registration.
13
13
  - `packages/web/server/lib/opencode/lifecycle.js`: OpenCode process lifecycle runtime (startup, restart, readiness, health monitoring). Managed child env inherits the user shell environment unchanged; experimental flags such as `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` follow the user's own environment and are never injected by OpenChamber.
14
14
  - `packages/web/server/lib/opencode/managed-capabilities-runtime.js`: managed-child scheduled-task resources, config injection, rotating bridge identity, and bridge authorization.
@@ -33,7 +33,7 @@ This module provides OpenCode server integration utilities for the web server ru
33
33
  - `packages/web/server/lib/opencode/tunnel-wiring-runtime.js`: active loopback-port wiring shared by pairing/LAN URLs and the private relay host.
34
34
  - `packages/web/server/lib/opencode/startup-pipeline-runtime.js`: server startup tail orchestration runtime for terminal/proxy/static/start-listen flow.
35
35
  - `packages/web/server/lib/opencode/server-utils-runtime.js`: shared server runtime utilities for OpenCode proxy wiring, OpenCode port/readiness helpers, and snapshot fetchers.
36
- - `packages/web/server/lib/opencode/openchamber-routes.js`: OpenChamber update, models metadata, session-index, transcript-cache, and desktop SSH host-token route registration. Tokenless ready SSH hosts may mint a client token on demand via Electron `mintSshHostToken`, which authenticates to the forwarded remote with the in-memory tunnel UI password.
36
+ - `packages/web/server/lib/opencode/openchamber-routes.js`: OpenChamber update, models metadata, session-index, and transcript-cache route registration.
37
37
  - `packages/web/server/lib/transcript-cache/`: opt-in Electron/local SQLite transcript cache (`/api/openchamber/transcript-cache`); default path is null so remote Web servers do not persist conversation bodies. See `transcript-cache/DOCUMENTATION.md`.
38
38
  - `packages/web/server/lib/opencode/pwa-manifest-routes.js`: PWA manifest route registration with recent-session shortcut resolution and short-lived caching.
39
39
  - `packages/web/server/lib/opencode/project-icon-routes.js`: project icon upload/read/discovery route registration and icon storage orchestration.
@@ -276,7 +276,7 @@ When adding or changing Host HTTP APIs that mobile/desktop clients reach over Pr
276
276
  - `DELETE /api/passkeys/:id`
277
277
  - `POST /api/auth/reset`
278
278
  - `POST /api/client-auth/pairing/sessions`: accepts an optional `relayUrl` when Relay is included. An owner UI session or the local `desktop-local` shell client may set a custom endpoint (other client bearers receive HTTP 403). Canonical form is `ws://`/`wss://` scheme/host/path: userinfo is rejected (HTTP 400); query and fragment are stripped and are not part of endpoint identity. The Host persists the switch and reconnects its control connection before returning the pairing-v2 Relay candidate; an `OPENCHAMBER_RELAY_URL` override stays authoritative.
279
- - `GET /api/client-auth/pairing/transports`: returns direct transport availability. `relayAvailable` is true only for the Electron desktop host; local `dev` / `web` / CLI servers report false and omit Relay URL fields so the create-device dialog never defaults to Anywhere or probes the relay.
279
+ - `GET /api/client-auth/pairing/transports`: returns direct transport availability. `relayAvailable` is true for desktop and SSH-managed remote hosts (`OPENCHAMBER_RUNTIME=desktop|ssh-remote`); local `dev` / `web` / ordinary CLI servers report false and omit Relay URL fields so the create-device dialog never defaults to Anywhere or probes the relay.
280
280
  - `GET /connect`
281
281
  - `POST /api/system/probe-url`
282
282
  - `app.use('/api', ...)` auth/tunnel guard
@@ -297,7 +297,7 @@ When adding or changing Host HTTP APIs that mobile/desktop clients reach over Pr
297
297
  - Legacy `--tunnel` shorthand normalization
298
298
 
299
299
  ## Public exports (cli-entry-runtime.js)
300
- - `runCliEntryIfMain(dependencies)`: detects direct CLI execution, forces `OPENCHAMBER_RUNTIME=web` so a leftover desktop env cannot host or probe a relay, and runs server startup with parsed CLI options.
300
+ - `runCliEntryIfMain(dependencies)`: detects direct CLI execution, forces `OPENCHAMBER_RUNTIME=web` (preserving `ssh-remote`) so a leftover desktop env cannot host or probe a relay, and runs server startup with parsed CLI options.
301
301
 
302
302
  ## Public exports (server-utils-runtime.js)
303
303
  - `createServerUtilsRuntime(dependencies)`: creates server utility runtime for OpenCode orchestration helpers.
@@ -62,9 +62,6 @@ export const createBootstrapRuntime = (dependencies) => {
62
62
  sessionIndexService,
63
63
  sessionIndexSyncRuntime,
64
64
  transcriptCacheService,
65
- getSshRoutingTable,
66
- mintSshHostToken,
67
- getPairingSession,
68
65
  } = options;
69
66
 
70
67
  const uiAuthController = createUiAuth({
@@ -154,9 +151,6 @@ export const createBootstrapRuntime = (dependencies) => {
154
151
  sessionIndexService,
155
152
  sessionIndexSyncRuntime,
156
153
  transcriptCacheService,
157
- getSshRoutingTable,
158
- mintSshHostToken,
159
- getPairingSession,
160
154
  express,
161
155
  });
162
156
 
@@ -17,7 +17,11 @@ export const runCliEntryIfMain = (dependencies) => {
17
17
  // OPENCHAMBER_RUNTIME=desktop from the parent shell — that would open a relay
18
18
  // host-control socket from `bun run dev` / `dev:server`. Electron sets desktop
19
19
  // before importing this module and never takes this CLI path.
20
- process.env.OPENCHAMBER_RUNTIME = 'web';
20
+ // SSH-manager remotes set OPENCHAMBER_RUNTIME=ssh-remote (and may pass
21
+ // --relay-host); preserve that so the remote can host a private relay.
22
+ if (process.env.OPENCHAMBER_RUNTIME !== 'ssh-remote') {
23
+ process.env.OPENCHAMBER_RUNTIME = 'web';
24
+ }
21
25
 
22
26
  const cliOptions = parseServeCliOptions({
23
27
  argv: process.argv.slice(2),
@@ -26,6 +26,23 @@ describe('cli-entry-runtime', () => {
26
26
  expect(startServer).toHaveBeenCalledTimes(1);
27
27
  });
28
28
 
29
+ it('preserves OPENCHAMBER_RUNTIME=ssh-remote on the direct node server path', () => {
30
+ process.env.OPENCHAMBER_RUNTIME = 'ssh-remote';
31
+ const startServer = vi.fn(async () => {});
32
+
33
+ runCliEntryIfMain({
34
+ process: { argv: ['node', '/tmp/server/index.js', '--port', '3001'], env: process.env, exit: vi.fn() },
35
+ currentFilename: '/tmp/server/index.js',
36
+ parseServeCliOptions: () => ({ port: 3001, host: undefined, uiPassword: null, apiOnly: false }),
37
+ defaultPort: 3001,
38
+ setExitOnShutdown: vi.fn(),
39
+ startServer,
40
+ });
41
+
42
+ expect(process.env.OPENCHAMBER_RUNTIME).toBe('ssh-remote');
43
+ expect(startServer).toHaveBeenCalledTimes(1);
44
+ });
45
+
29
46
  it('does not rewrite OPENCHAMBER_RUNTIME when imported as a library (Electron in-process)', () => {
30
47
  process.env.OPENCHAMBER_RUNTIME = 'desktop';
31
48
 
@@ -871,9 +871,6 @@ export const registerAuthAndAccessRoutes = (app, dependencies) => {
871
871
  allowedClientKinds: req.body?.allowedClientKinds,
872
872
  createdByClientId: clientIdFromAuthContext(authContext),
873
873
  usesRelay,
874
- // Optional SSH instance id: binds mobile ssh-host-token mint to this
875
- // pairing after redeem. Does not affect redeem itself.
876
- sshHostId: req.body?.sshHostId,
877
874
  });
878
875
  void reconcileRelay();
879
876
  res.setHeader('Cache-Control', 'no-store');
@@ -774,24 +774,9 @@ describe('core-routes', () => {
774
774
  allowedClientKinds: ['mobile'],
775
775
  createdByClientId: null,
776
776
  usesRelay: false,
777
- sshHostId: undefined,
778
777
  });
779
778
  });
780
779
 
781
- it('forwards optional sshHostId into createPairingSession', async () => {
782
- const { app, dependencies } = createPairingRouteApp();
783
-
784
- await request(app)
785
- .post('/api/client-auth/pairing/sessions')
786
- .set('Host', 'runtime.example')
787
- .send({ label: 'SSH phone', allowedClientKinds: ['mobile'], sshHostId: 'ssh-1', includeRelay: true })
788
- .expect(201);
789
-
790
- expect(dependencies.clientPairingRuntime.createPairingSession).toHaveBeenCalledWith(
791
- expect.objectContaining({ sshHostId: 'ssh-1' }),
792
- );
793
- });
794
-
795
780
  it('advertises the caller-supplied serverUrl as the direct candidate over the request origin', async () => {
796
781
  const { app } = createPairingRouteApp();
797
782
 
@@ -1,6 +1,5 @@
1
1
  import { registerSessionIndexRoutes } from '../session-index/routes.js';
2
2
  import { registerTranscriptCacheRoutes } from '../transcript-cache/routes.js';
3
- import { registerDesktopHostRoutes } from '../desktop-hosts/routes.js';
4
3
  import { registerConfigSyncRoutes } from '../config-sync/routes.js';
5
4
 
6
5
  export const registerOpenChamberRoutes = (app, dependencies) => {
@@ -18,21 +17,11 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
18
17
  sessionIndexService,
19
18
  sessionIndexSyncRuntime,
20
19
  transcriptCacheService,
21
- getSshRoutingTable,
22
- mintSshHostToken,
23
- getPairingSession,
24
20
  express,
25
21
  } = dependencies;
26
22
 
27
23
  registerSessionIndexRoutes(app, { sessionIndexService, sessionIndexSyncRuntime });
28
24
  registerTranscriptCacheRoutes(app, { transcriptCacheService });
29
- registerDesktopHostRoutes(app, {
30
- readSettingsFromDiskMigrated,
31
- getSshRoutingTable,
32
- mintSshHostToken,
33
- getPairingSession,
34
- express,
35
- });
36
25
  registerConfigSyncRoutes(app, {
37
26
  readSettingsFromDiskMigrated,
38
27
  persistSettings,
@@ -6,7 +6,7 @@ The private relay lets an OpenChamber client (mobile app, browser, or another de
6
6
 
7
7
  Traffic is **end-to-end encrypted between the two endpoints** (client and host instance). The relay infrastructure forwards opaque ciphertext and cannot read application traffic — it is an untrusted transport, not a trusted middlebox.
8
8
 
9
- This module (`packages/web/server/lib/relay/`) is the **Host side** of the private relay. **Only the Electron desktop runtime may act as a relay host** (`OPENCHAMBER_RUNTIME=desktop`, set by Electron before it starts the in-process web server). Plain `node` / `dev:web:hmr` / CLI / VS Code web servers must not open the host-control socket, claim `relay-host.lock`, or advertise a host pairing candidate — even when they share the same data dir as desktop. Those processes may still run the API that *clients* use; mobile and other desktops remain **relay clients**. The **Client side** lives in `packages/ui/src/lib/relay/`. Hosted Relay instances may run in a Worker. The self-hosted **Relay server** belongs to `packages/relay-server/` and brokers Layer 1 connections.
9
+ This module (`packages/web/server/lib/relay/`) is the **Host side** of the private relay. **Only the Electron desktop runtime or an SSH-managed remote server may act as a relay host** (`OPENCHAMBER_RUNTIME=desktop` or `ssh-remote`). Electron sets `desktop` before it starts the in-process web server; SSH manager sets `ssh-remote` when launching a managed remote `openchamber serve` (optionally with `--relay-host`). Plain `node` / `dev:web:hmr` / ordinary CLI / VS Code web servers must not open the host-control socket, claim `relay-host.lock`, or advertise a host pairing candidate — even when they share the same data dir as desktop. Those processes may still run the API that *clients* use; mobile and other desktops remain **relay clients**. The **Client side** lives in `packages/ui/src/lib/relay/`. Hosted Relay instances may run in a Worker. The self-hosted **Relay server** belongs to `packages/relay-server/` and brokers Layer 1 connections.
10
10
 
11
11
  ## The three layers
12
12
 
@@ -19,7 +19,7 @@ Traffic is modeled as three stacked layers. The relay understands only Layer 1;
19
19
  ## Entrypoints and structure
20
20
 
21
21
  Host side (`packages/web/server/lib/relay/`):
22
- - `service.js` — thin entrypoint: relay config (enabled flag + relay URL), the management routes (`GET/POST /api/openchamber/relay/{status,enable,disable}`), a `getPairingCandidate()` accessor (the relay transport candidate folded into pairing-v2 links when enabled, consumed by the pairing-session route in `core-routes.js`), and lifecycle wiring. Host lifecycle is gated to Electron: non-desktop runtimes (`bun run dev`, `dev:web:hmr`, CLI `serve`, VS Code, plain `node server`) report `state: 'unavailable'`, refuse `/relay/enable`, `/relay/disable`, and relay pairing with 403, never mint a relay identity, never advertise `relayAvailable`, and never call `startRelayHost`. Direct `node server/index.js` forces `OPENCHAMBER_RUNTIME=web` so a leftover desktop env cannot open the host-control socket. On desktop, started from `packages/web/server/index.js` only when demand/opt-in enables the relay. The relay endpoint defaults to the OpenChamber-hosted relay but can be pinned to a self-hosted relay via the `OPENCHAMBER_RELAY_URL` env var; when set it overrides the stored setting for the host connection, the pairing candidate, and status, so paired clients inherit the endpoint automatically. Endpoint identity is scheme/host/path only: accepted schemes are `ws://` and `wss://`; URLs with userinfo are rejected (no silent default fallback); query and fragment are stripped before persistence and pairing candidates. Custom endpoint persistence and Host control-connection switching are explicit authenticated management actions. Pairing creation with `relayUrl` requires an owner UI session or the local `desktop-local` shell client; `/relay/enable` follows its API auth gate.
22
+ - `service.js` — thin entrypoint: relay config (enabled flag + relay URL), the management routes (`GET/POST /api/openchamber/relay/{status,enable,disable}`), a `getPairingCandidate()` accessor (the relay transport candidate folded into pairing-v2 links when enabled, consumed by the pairing-session route in `core-routes.js`), and lifecycle wiring. Host lifecycle is gated by `isRelayHostRuntime()` (`desktop` or `ssh-remote`): other runtimes (`bun run dev`, `dev:web:hmr`, ordinary CLI `serve`, VS Code, plain `node server`) report `state: 'unavailable'`, refuse `/relay/enable`, `/relay/disable`, and relay pairing with 403, never mint a relay identity, never advertise `relayAvailable`, and never call `startRelayHost`. Direct `node server/index.js` forces `OPENCHAMBER_RUNTIME=web` unless it is already `ssh-remote`, so a leftover desktop env cannot open the host-control socket. On host runtimes, started from `packages/web/server/index.js` when demand/opt-in enables the relay. Desktop auto-clears `privateRelay.enabled` when demand drops; `ssh-remote` keeps an explicit opt-in (e.g. `openchamber serve --relay-host`) sticky until disabled. The relay endpoint defaults to the OpenChamber-hosted relay but can be pinned to a self-hosted relay via the `OPENCHAMBER_RELAY_URL` env var; when set it overrides the stored setting for the host connection, the pairing candidate, and status, so paired clients inherit the endpoint automatically. Endpoint identity is scheme/host/path only: accepted schemes are `ws://` and `wss://`; URLs with userinfo are rejected (no silent default fallback); query and fragment are stripped before persistence and pairing candidates. Custom endpoint persistence and Host control-connection switching are explicit authenticated management actions. Pairing creation with `relayUrl` requires an owner UI session or the local `desktop-local` shell client; `/relay/enable` follows its API auth gate.
23
23
  - `identity.js` — the host's stable identity: the long-lived signing keypair (shared with the push relay, defines the routing id) plus a long-lived encryption keypair (the E2EE trust anchor). Reused across restarts; never rotated implicitly.
24
24
  - `signing-key.js` — storage/derivation of the signing keypair and the routing id, shared with the notifications runtime.
25
25
  - `host-client.js` — the long-lived connection manager: one outbound control connection to the relay, a per-client data connection for each connected device, reconnect/backoff, and the E2EE responder handshake per connection.
@@ -104,48 +104,6 @@ Relay mode plugs into the existing client transport layer rather than a parallel
104
104
 
105
105
  Catalog loaders (`loadProviders` / `loadAgents`) and assistant Query keys gate writes/caches on the **transport fingerprint**, not `runtimeKey`. On endpoint reset and same-device transport switch, `runtimeEndpointReset.ts` must set `useConfigStore.catalogTransportIdentity` to `getRuntimeTransportIdentity()`. Writing `detail.runtimeKey` there silently discards provider/agent catalog refreshes under Relay, which then hides capability-gated surfaces such as Assistants.
106
106
 
107
- ## SSH host routing
108
-
109
- Mobile (and other relay clients) can reach a desktop's **SSH-forwarded remote OpenChamber instances** through the same private-relay tunnel that already reaches the desktop's local loopback origin. Routing is host-side only: the client selects a target local-forward port; the tunnel dispatcher dials that port on `127.0.0.1` when — and only when — it is present in the live SSH routing table.
110
-
111
- ### Header contract
112
-
113
- - Header name: `x-openchamber-target-port`
114
- - Value: decimal port number string (e.g. `"41234"`)
115
- - HTTP: carried on the tunneled request headers
116
- - WebSocket: optional `headers` field on the `WsOpen` payload (same header name/value)
117
- - The dispatcher **strips** this header before forwarding to loopback (it is routing metadata, not an upstream header)
118
-
119
- ### Resolution rules (`resolveTargetPort` in `tunnel-host.js`)
120
-
121
- | Condition | Result |
122
- |---|---|
123
- | Header absent | Dial `getLocalPort()` (desktop local origin) — unchanged default behavior |
124
- | Header present and `localPort` is in `getSshRoutingTable()` | Dial that port |
125
- | Header present but port missing/invalid/not in table | **Do not** fall back to the default port |
126
-
127
- HTTP miss → synthetic **503** with body `{ error: 'ssh-host-unreachable', …, source: 'relay-tunnel-host' }`.
128
- WS miss → stream abort with reason `ssh-host-unreachable`.
129
-
130
- ### Routing table (authoritative in memory)
131
-
132
- - Electron injects `getSshRoutingTable: () => sshManager.getRoutingTable()` into `startWebUiServer`.
133
- - Table entries are `{ id, localPort }` for SSH sessions whose status phase is **`ready`** and whose `localPort` is finite.
134
- - Degraded / connecting / disconnected sessions are absent. Settings.json is **not** the allowlist for dialing — only the live table is.
135
- - Non-desktop runtimes default to `() => []`.
136
-
137
- ### Companion HTTP APIs (authenticated `/api` gate)
138
-
139
- - `GET /api/openchamber/desktop-hosts` → `{ hosts: [{ id, label, localPort, reachable }] }` for configured SSH instances. `reachable` is true iff the host id is in the routing table. **Never returns `clientToken`.**
140
- - `POST /api/openchamber/ssh-host-token` body `{ hostId }` → `{ token }` from `settings.json` `desktopHosts[].clientToken` for SSH instance hosts only. Missing / non-SSH / no token → **404**. Response includes `Cache-Control: no-store`.
141
-
142
- ### Security invariants (do not regress)
143
-
144
- - Path allowlists (`isAllowedHttpPath` / `ALLOWED_WS_PATHS`) are **not** relaxed for SSH targets.
145
- - The dispatcher **never injects credentials**; the client still authenticates with its own bearer / `oc_url_token` against the target instance.
146
- - Port legitimacy is solely the in-memory routing table, not client-supplied trust and not settings alone.
147
- - List endpoints must not leak `clientToken`; token mint is a separate authenticated POST.
148
-
149
107
  ## Design invariants (do not regress)
150
108
 
151
109
  - The relay never sees plaintext application traffic; it sees only routing metadata (routing id, connection identifiers, timestamps, coarse counts).
@@ -154,6 +112,6 @@ WS miss → stream abort with reason `ssh-host-unreachable`.
154
112
  - The host dispatcher never injects credentials; the server authenticates each tunneled request.
155
113
  - The tunnel is transparent to the app: adding relay support to a feature should not require the feature to know the relay exists — it goes through the shared runtime transport helpers.
156
114
  - The two implementations stay byte-compatible and the wire format is versioned/negotiated so mixed client/host app versions degrade gracefully rather than break.
157
- - SSH target-port routing never falls back to the default local port on a routing-table miss (see "SSH host routing").
115
+ - Tunneled HTTP/WS always dial `getLocalPort()` on loopback; there is no client-selected target-port override on the host dispatcher.
158
116
 
159
117
  For the operational rules that keep future changes (new WebSocket endpoints, transport refactors, terminal/voice porting) from breaking this, load the `relay-transport` skill.
@@ -54,14 +54,12 @@ const resolveBatchWindowMs = (option) => {
54
54
  * identity: { serverId: string, hostEncPrivateKey: CryptoKey, signRelayAuth: (role: string, connectionId?: string | null) => { ts: number, sig: string, pk: string } },
55
55
  * localPort?: number,
56
56
  * getLocalPort?: () => number,
57
- * getSshRoutingTable?: () => { id: string, localPort: number }[],
58
57
  * onStatus?: (status: { state: string, lastError: string | null, connectedClients: number }) => void,
59
58
  * logger?: Pick<Console, 'warn'>,
60
59
  * }} options
61
60
  */
62
- export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, getSshRoutingTable = () => [], onStatus, logger = console, batchWindowMs, batch }) => {
61
+ export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, onStatus, logger = console, batchWindowMs, batch }) => {
63
62
  const resolveLocalPort = typeof getLocalPort === 'function' ? getLocalPort : () => localPort;
64
- const resolveSshRoutingTable = typeof getSshRoutingTable === 'function' ? getSshRoutingTable : () => [];
65
63
  const localBatch = batch !== false;
66
64
  const resolvedBatchWindowMs = resolveBatchWindowMs(batchWindowMs);
67
65
 
@@ -190,7 +188,6 @@ export const startRelayHost = ({ relayUrl, identity, localPort, getLocalPort, ge
190
188
  entry.tunnel = createTunnelHost({
191
189
  connectionId,
192
190
  getLocalPort: resolveLocalPort,
193
- getSshRoutingTable: resolveSshRoutingTable,
194
191
  getBufferedAmount: () => socket.bufferedAmount,
195
192
  sendFrame: (plaintextFrame) => {
196
193
  if (dataSockets.get(connectionId) !== entry || socket.readyState !== WebSocket.OPEN) return;