@openchambery/web 1.17.1 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib/DOCUMENTATION.md +1 -0
- package/dist/assets/{AgentAvatar-C6Hxu5_o.js → AgentAvatar-B2kZjgaf.js} +1 -1
- package/dist/assets/{AssistantView-CbnxMZU0.js → AssistantView-Dc9MhgcD.js} +1 -1
- package/dist/assets/{ChatContainer-DEDGosbz.js → ChatContainer-CA3RO1yP.js} +40 -40
- package/dist/assets/{DiagramView-Byxhaduz.js → DiagramView-CpT4q2Jg.js} +1 -1
- package/dist/assets/{FadeInOnReveal-CvNA_aGD.js → FadeInOnReveal-aeWlaqVB.js} +1 -1
- package/dist/assets/{Icon-BPJ_quEe.js → Icon-AXqa-fYB.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-Cv8V-Q5l.js → MarkdownRendererImpl-DLnXrjCS.js} +2 -2
- package/dist/assets/{MobileDetailNavigation-lVtCe-5P.js → MobileDetailNavigation-BYX8jFRg.js} +1 -1
- package/dist/assets/MobileShareBridge-BJzGsvUa.js +4 -0
- package/dist/assets/MobileSurface-Cfe17dw4.js +2 -0
- package/dist/assets/{MultiRunWindow-1kkKTh4h.js → MultiRunWindow-DEYIMkSg.js} +1 -1
- package/dist/assets/{OnboardingScreen-CdvaEBtt.js → OnboardingScreen-CCfdEtW6.js} +1 -1
- package/dist/assets/{OverlayScrollbar-Dqk1zGJB.js → OverlayScrollbar-CpQvV-qY.js} +1 -1
- package/dist/assets/SettingsView-PjA5aLCR.js +25 -0
- package/dist/assets/{SettingsWindow-DMQMRgc3.js → SettingsWindow-DOe4sCjl.js} +1 -1
- package/dist/assets/{TerminalView-jYq0JQPc.js → TerminalView-Bf-2wMV0.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DJefd2N6.js → ToolOutputDialog-DuT5ne12.js} +1 -1
- package/dist/assets/{appThemeRegistry-BXeIggYO.js → appThemeRegistry--wNZ5yxf.js} +2 -2
- package/dist/assets/{button-BmNJBT13.js → button-CDvMVaiO.js} +1 -1
- package/dist/assets/{clipboard-CXWbHCAk.js → clipboard-Jdqe05db.js} +1 -1
- package/dist/assets/{collapsible-68IqWBM1.js → collapsible-C2aamDvD.js} +1 -1
- package/dist/assets/{context-menu-BbRpMBQh.js → context-menu-CS_u7xxB.js} +1 -1
- package/dist/assets/desktopHosts-CBoBpJ1Z.js +1 -0
- package/dist/assets/desktopRelayRestore-l2R9muux.js +1 -0
- package/dist/assets/{diagram-YN0GZ1Q0.js → diagram-CEon0BQs.js} +1 -1
- package/dist/assets/{dialog-BV1ENlLf.js → dialog-BzZ3cKxG.js} +1 -1
- package/dist/assets/es-C25PRSjo.js +15 -0
- package/dist/assets/{fileQueries-i4oziMnC.js → fileQueries-CE_DaZWQ.js} +1 -1
- package/dist/assets/fr-Cv8ZWQdW.js +13 -0
- package/dist/assets/{gitApi-Bs_2iBAm.js → gitApi-DSLGdswL.js} +3 -3
- package/dist/assets/gitApiHttp-D8j1qRH4.js +2 -0
- package/dist/assets/{input-BLVhPvhN.js → input-k9DGGyZ6.js} +1 -1
- package/dist/assets/insertionBoundaries-BkoTIR-0.js +1 -0
- package/dist/assets/{ja-Dcb0VtDE.js → ja-Ue9m9eSe.js} +3 -3
- package/dist/assets/{ko-CX71xNDX.js → ko-DWBUCCV6.js} +3 -3
- package/dist/assets/main-CTnxV3y3.js +172 -0
- package/dist/assets/{main-CT0DuuLm.js → main-D1Xze0xA.js} +2 -2
- package/dist/assets/{markdown-worker-Cq3Twp1A.js → markdown-worker-DkeWiER6.js} +1 -1
- package/dist/assets/{markdownTheme-CCnvI6L0.js → markdownTheme-DNT93i5L.js} +1 -1
- package/dist/assets/miniChat-Dxajshg_.js +2 -0
- package/dist/assets/mobile-CisSv-ca.js +2 -0
- package/dist/assets/multirun-BOb2dW9T.js +4 -0
- package/dist/assets/persistence-DPNMIbF5.js +3 -0
- package/dist/assets/{pl-mR_3Sqr8.js → pl-DaBaPCyE.js} +3 -3
- package/dist/assets/projectMeta-WTDhB__y.js +3 -0
- package/dist/assets/pt-BR-C2cJZ43o.js +15 -0
- package/dist/assets/radio-DZwuHb7-.js +1 -0
- package/dist/assets/renderElectronMiniChatApp-DpYFpBSy.js +2 -0
- package/dist/assets/renderMobileApp-DB1TXtLg.js +23 -0
- package/dist/assets/runtimeConfig-BtWi74Pm.js +1 -0
- package/dist/assets/runtimeEndpointReset-BnA1DoNG.js +145 -0
- package/dist/assets/{sortable-tabs-strip-oKJ700WG.js → sortable-tabs-strip-DADoz-JR.js} +1 -1
- package/dist/assets/src-oHrYIqHF.css +2 -0
- package/dist/assets/{store-oojlrF4d.js → store-SqV3zfE8.js} +4 -4
- package/dist/assets/{terminalApi-BYTA045F.js → terminalApi-tB3qLcfa.js} +1 -1
- package/dist/assets/{tooltip-BYGBBTc2.js → tooltip-DMNR5rN0.js} +1 -1
- package/dist/assets/uk-DJz_P8OZ.js +15 -0
- package/dist/assets/{url-CGHw6D-E.js → url-7MqoLs9X.js} +1 -1
- package/dist/assets/useAppFontEffects-CeS0e0FK.js +7 -0
- package/dist/assets/{useAssistantUIStore-ESTV1mEY.js → useAssistantUIStore-Bq3cPj2x.js} +1 -1
- package/dist/assets/{useDesktopWindowControlsLayout-Cq5a2gcJ.js → useDesktopWindowControlsLayout-DwFMUFtC.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-DG6uPzBG.js → useEffectiveDirectory-Dp8McufI.js} +1 -1
- package/dist/assets/{useFontPreferences-Bu4ME5hk.js → useFontPreferences-CIurCeNv.js} +1 -1
- package/dist/assets/useMobileNavigationStore-kWPnKBP1.js +475 -0
- package/dist/assets/useSessionAutoCleanup-BLiIZW0x.js +1 -0
- package/dist/assets/useUIStore-5agS8bsW.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-ADgu9578.js → useWorkerHighlightedLines-Cnxz7HaG.js} +1 -1
- package/dist/assets/{utils-DyUyiLje.js → utils-Co19EJEL.js} +1 -1
- package/dist/assets/zh-CN-DLAKDOM9.js +15 -0
- package/dist/assets/zh-TW-s3Jjx1_n.js +15 -0
- package/dist/index.html +9 -9
- package/dist/mini-chat.html +9 -9
- package/dist/mobile.html +9 -9
- package/package.json +1 -1
- package/server/index.d.ts +4 -0
- package/server/index.js +29 -15
- package/server/lib/client-auth/pairing.js +7 -1
- package/server/lib/client-auth/pairing.test.js +45 -0
- package/server/lib/desktop-hosts/routes.js +157 -0
- package/server/lib/desktop-hosts/routes.test.js +227 -0
- package/server/lib/message-queue/DOCUMENTATION.md +1 -1
- package/server/lib/message-queue/routes.js +1 -1
- package/server/lib/message-queue/routes.test.js +1 -0
- package/server/lib/message-queue/service.js +12 -1
- package/server/lib/message-queue/service.test.js +30 -0
- package/server/lib/opencode/DOCUMENTATION.md +4 -4
- package/server/lib/opencode/bootstrap-runtime.js +7 -0
- package/server/lib/opencode/cli-entry-runtime.js +6 -0
- package/server/lib/opencode/cli-entry-runtime.test.js +43 -0
- package/server/lib/opencode/core-routes.js +4 -1
- package/server/lib/opencode/core-routes.test.js +29 -0
- package/server/lib/opencode/openchamber-routes.js +12 -0
- package/server/lib/opencode/settings-runtime.js +1 -1
- package/server/lib/package-manager.js +3 -1
- package/server/lib/relay/DOCUMENTATION.md +44 -1
- package/server/lib/relay/host-client.js +4 -1
- package/server/lib/relay/service.js +16 -8
- package/server/lib/relay/service.test.js +21 -6
- package/server/lib/relay/tunnel-host.js +57 -4
- package/server/lib/relay/tunnel-host.test.js +250 -2
- package/dist/assets/MobileShareBridge-DAlvgjo1.js +0 -4
- package/dist/assets/MobileSurface-Bde8yLWo.js +0 -2
- package/dist/assets/SettingsView-D0dhTIur.js +0 -25
- package/dist/assets/desktopHosts-BpjUSDz4.js +0 -1
- package/dist/assets/desktopRelayRestore-BzUEk1i7.js +0 -1
- package/dist/assets/es-EXZUX1sB.js +0 -15
- package/dist/assets/fr-jWNPO91J.js +0 -13
- package/dist/assets/gitApiHttp-C1DaEMKR.js +0 -2
- package/dist/assets/insertionBoundaries-BAFtAotq.js +0 -1
- package/dist/assets/main-G8tKFcTV.js +0 -172
- package/dist/assets/miniChat-BP3OHqNF.js +0 -2
- package/dist/assets/mobile-BtwuDs-C.js +0 -2
- package/dist/assets/multirun-B-L3JH60.js +0 -4
- package/dist/assets/persistence-C_KCiMcY.js +0 -3
- package/dist/assets/projectMeta-Cp74cItv.js +0 -3
- package/dist/assets/pt-BR-D1RpVOvC.js +0 -15
- package/dist/assets/radio-Bs4mdPv5.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-dgirEOtR.js +0 -2
- package/dist/assets/renderMobileApp-DK9qzy0P.js +0 -23
- package/dist/assets/runtimeConfig-JSaYFA5x.js +0 -1
- package/dist/assets/runtimeEndpointReset-C0eCWIE6.js +0 -145
- package/dist/assets/src-8zziWB78.css +0 -2
- package/dist/assets/uk-UvnVqWv-.js +0 -15
- package/dist/assets/useAppFontEffects-BhLcUfRB.js +0 -7
- package/dist/assets/useMobileNavigationStore-1xHjnAsg.js +0 -475
- package/dist/assets/useSessionAutoCleanup-BFletAeV.js +0 -1
- package/dist/assets/useUIStore-D6BTB4va.js +0 -1
- package/dist/assets/zh-CN-9v-RC966.js +0 -15
- package/dist/assets/zh-TW-BZ_wZdC-.js +0 -15
package/dist/index.html
CHANGED
|
@@ -552,23 +552,23 @@
|
|
|
552
552
|
opacity: 0.72;
|
|
553
553
|
}
|
|
554
554
|
</style>
|
|
555
|
-
<script type="module" crossorigin src="/assets/main-
|
|
555
|
+
<script type="module" crossorigin src="/assets/main-D1Xze0xA.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-
|
|
559
|
+
<link rel="modulepreload" crossorigin href="/assets/useUIStore-5agS8bsW.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-
|
|
564
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-
|
|
565
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-
|
|
563
|
+
<link rel="modulepreload" crossorigin href="/assets/persistence-DPNMIbF5.js">
|
|
564
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-CBoBpJ1Z.js">
|
|
565
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-l2R9muux.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/gitApiHttp-
|
|
569
|
-
<link rel="modulepreload" crossorigin href="/assets/terminalApi-
|
|
570
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
571
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
568
|
+
<link rel="modulepreload" crossorigin href="/assets/gitApiHttp-D8j1qRH4.js">
|
|
569
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-tB3qLcfa.js">
|
|
570
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BtWi74Pm.js">
|
|
571
|
+
<link rel="stylesheet" crossorigin href="/assets/src-oHrYIqHF.css">
|
|
572
572
|
</head>
|
|
573
573
|
<body class="h-full bg-background text-foreground">
|
|
574
574
|
<!-- Keep the startup layer outside #root. React replaces every child of
|
package/dist/mini-chat.html
CHANGED
|
@@ -74,23 +74,23 @@
|
|
|
74
74
|
}
|
|
75
75
|
</style>
|
|
76
76
|
|
|
77
|
-
<script type="module" crossorigin src="/assets/miniChat-
|
|
77
|
+
<script type="module" crossorigin src="/assets/miniChat-Dxajshg_.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-
|
|
81
|
+
<link rel="modulepreload" crossorigin href="/assets/useUIStore-5agS8bsW.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-
|
|
86
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-
|
|
87
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-
|
|
85
|
+
<link rel="modulepreload" crossorigin href="/assets/persistence-DPNMIbF5.js">
|
|
86
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-CBoBpJ1Z.js">
|
|
87
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-l2R9muux.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/gitApiHttp-
|
|
91
|
-
<link rel="modulepreload" crossorigin href="/assets/terminalApi-
|
|
92
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
93
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
90
|
+
<link rel="modulepreload" crossorigin href="/assets/gitApiHttp-D8j1qRH4.js">
|
|
91
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-tB3qLcfa.js">
|
|
92
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BtWi74Pm.js">
|
|
93
|
+
<link rel="stylesheet" crossorigin href="/assets/src-oHrYIqHF.css">
|
|
94
94
|
</head>
|
|
95
95
|
<body class="h-full bg-background text-foreground">
|
|
96
96
|
<div id="root" class="h-full">
|
package/dist/mobile.html
CHANGED
|
@@ -63,23 +63,23 @@
|
|
|
63
63
|
})();
|
|
64
64
|
</script>
|
|
65
65
|
|
|
66
|
-
<script type="module" crossorigin src="/assets/mobile-
|
|
66
|
+
<script type="module" crossorigin src="/assets/mobile-CisSv-ca.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-
|
|
70
|
+
<link rel="modulepreload" crossorigin href="/assets/useUIStore-5agS8bsW.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-
|
|
75
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-
|
|
76
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-
|
|
74
|
+
<link rel="modulepreload" crossorigin href="/assets/persistence-DPNMIbF5.js">
|
|
75
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-CBoBpJ1Z.js">
|
|
76
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-l2R9muux.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/gitApiHttp-
|
|
80
|
-
<link rel="modulepreload" crossorigin href="/assets/terminalApi-
|
|
81
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
82
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
79
|
+
<link rel="modulepreload" crossorigin href="/assets/gitApiHttp-D8j1qRH4.js">
|
|
80
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-tB3qLcfa.js">
|
|
81
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BtWi74Pm.js">
|
|
82
|
+
<link rel="stylesheet" crossorigin href="/assets/src-oHrYIqHF.css">
|
|
83
83
|
</head>
|
|
84
84
|
<body class="h-full bg-background text-foreground">
|
|
85
85
|
<div id="root" class="h-full"></div>
|
package/package.json
CHANGED
package/server/index.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ 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>;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
export declare function startWebUiServer(
|
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 } from './lib/relay/service.js';
|
|
93
|
+
import { createRelayService, isDesktopRelayHostRuntime } 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,7 +1184,9 @@ 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
|
-
|
|
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() };
|
|
1188
1190
|
};
|
|
1189
1191
|
// ALL direct LAN URLs this server is currently reachable on, for the
|
|
1190
1192
|
// candidates-refresh endpoint: the address the requesting client already
|
|
@@ -1241,6 +1243,11 @@ async function main(options = {}) {
|
|
|
1241
1243
|
const getDesktopRuntimeConfig = typeof options.getDesktopRuntimeConfig === 'function'
|
|
1242
1244
|
? options.getDesktopRuntimeConfig
|
|
1243
1245
|
: null;
|
|
1246
|
+
// Electron injects live SSH local-forward ports; non-desktop defaults to empty
|
|
1247
|
+
// so relay target-port routing never falls back to an unvalidated port.
|
|
1248
|
+
const getSshRoutingTable = typeof options.getSshRoutingTable === 'function'
|
|
1249
|
+
? options.getSshRoutingTable
|
|
1250
|
+
: () => [];
|
|
1244
1251
|
const sessionIndexDbPath = options.sessionIndexDbPath !== undefined
|
|
1245
1252
|
? options.sessionIndexDbPath
|
|
1246
1253
|
: process.env.OPENCHAMBER_SESSION_INDEX_DB_PATH;
|
|
@@ -1417,7 +1424,7 @@ async function main(options = {}) {
|
|
|
1417
1424
|
reconcileRelay: () => (relayServiceInstance ? relayServiceInstance.reconcile() : Promise.resolve()),
|
|
1418
1425
|
getPairingTransports: async (req) => {
|
|
1419
1426
|
const transports = resolvePairingTransports(req);
|
|
1420
|
-
if (!relayServiceInstance) return transports;
|
|
1427
|
+
if (!relayServiceInstance || !transports.relayAvailable) return transports;
|
|
1421
1428
|
const relay = await relayServiceInstance.getStatus();
|
|
1422
1429
|
return {
|
|
1423
1430
|
...transports,
|
|
@@ -1465,6 +1472,11 @@ async function main(options = {}) {
|
|
|
1465
1472
|
sessionIndexService,
|
|
1466
1473
|
sessionIndexSyncRuntime,
|
|
1467
1474
|
transcriptCacheService,
|
|
1475
|
+
getSshRoutingTable,
|
|
1476
|
+
mintSshHostToken: typeof options.mintSshHostToken === 'function'
|
|
1477
|
+
? options.mintSshHostToken
|
|
1478
|
+
: undefined,
|
|
1479
|
+
getPairingSession: (id) => clientPairingRuntime.getPairingSession(id),
|
|
1468
1480
|
});
|
|
1469
1481
|
uiAuthController = bootstrapResult.uiAuthController;
|
|
1470
1482
|
realtimeProxyRuntime = attachRealtimeProxy({
|
|
@@ -1488,6 +1500,7 @@ async function main(options = {}) {
|
|
|
1488
1500
|
readSettingsStrict: readSettingsFromDiskStrict,
|
|
1489
1501
|
remoteClientAuthRuntime,
|
|
1490
1502
|
getLocalPort: () => tunnelRuntimeContext.getActivePort(),
|
|
1503
|
+
getSshRoutingTable,
|
|
1491
1504
|
// One relay host per machine: every instance sharing this data dir shares
|
|
1492
1505
|
// the relay identity (serverId), so concurrent hosts evict each other at
|
|
1493
1506
|
// the relay worker and devices land on a random local instance.
|
|
@@ -1626,18 +1639,19 @@ async function main(options = {}) {
|
|
|
1626
1639
|
}
|
|
1627
1640
|
|
|
1628
1641
|
// Only Electron (OPENCHAMBER_RUNTIME=desktop) opens a relay host-control
|
|
1629
|
-
// socket
|
|
1630
|
-
//
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
// Relay demand can change outside our routes: pending sessions expire, or a
|
|
1635
|
-
// paired device is revoked, without hitting our management routes. Poll so
|
|
1636
|
-
// the desktop host picks demand up (or drops it) within a minute.
|
|
1637
|
-
const relayReconcileTimer = setInterval(() => {
|
|
1642
|
+
// socket or polls relay demand. Local `dev` / `web` never reconcile, so they
|
|
1643
|
+
// cannot inherit a leftover desktop env and start probing the relay.
|
|
1644
|
+
let relayReconcileTimer = null;
|
|
1645
|
+
if (isDesktopRelayHostRuntime()) {
|
|
1638
1646
|
void relayService.reconcile();
|
|
1639
|
-
|
|
1640
|
-
|
|
1647
|
+
// Relay demand can change outside our routes: pending sessions expire, or a
|
|
1648
|
+
// paired device is revoked, without hitting our management routes. Poll so
|
|
1649
|
+
// the desktop host picks demand up (or drops it) within a minute.
|
|
1650
|
+
relayReconcileTimer = setInterval(() => {
|
|
1651
|
+
void relayService.reconcile();
|
|
1652
|
+
}, 60_000);
|
|
1653
|
+
relayReconcileTimer.unref?.();
|
|
1654
|
+
}
|
|
1641
1655
|
|
|
1642
1656
|
return {
|
|
1643
1657
|
expressApp: app,
|
|
@@ -1683,7 +1697,7 @@ async function main(options = {}) {
|
|
|
1683
1697
|
console.warn('[message-queue] Failed to close durable database during shutdown');
|
|
1684
1698
|
}
|
|
1685
1699
|
realtimeProxyRuntime.stop();
|
|
1686
|
-
clearInterval(relayReconcileTimer);
|
|
1700
|
+
if (relayReconcileTimer) clearInterval(relayReconcileTimer);
|
|
1687
1701
|
try {
|
|
1688
1702
|
relayService.stop();
|
|
1689
1703
|
} catch {
|
|
@@ -72,6 +72,9 @@ 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,
|
|
75
78
|
});
|
|
76
79
|
|
|
77
80
|
// A pending session is one that can still be redeemed: not used, not cancelled,
|
|
@@ -138,6 +141,7 @@ export const createClientPairingRuntime = ({
|
|
|
138
141
|
allowedClientKinds: normalizeAllowedClientKinds(session.allowedClientKinds),
|
|
139
142
|
createdByClientId: normalizeOptionalString(session.createdByClientId),
|
|
140
143
|
usesRelay: session.usesRelay === true,
|
|
144
|
+
sshHostId: normalizeOptionalString(session.sshHostId),
|
|
141
145
|
}))
|
|
142
146
|
.filter((session) => session.secretHash.length > 0)
|
|
143
147
|
: [],
|
|
@@ -176,7 +180,7 @@ export const createClientPairingRuntime = ({
|
|
|
176
180
|
});
|
|
177
181
|
};
|
|
178
182
|
|
|
179
|
-
const createPairingSession = async ({ label, allowedClientKinds, createdByClientId, usesRelay } = {}) => {
|
|
183
|
+
const createPairingSession = async ({ label, allowedClientKinds, createdByClientId, usesRelay, sshHostId } = {}) => {
|
|
180
184
|
return withStoreMutation(async () => {
|
|
181
185
|
const store = await readStore();
|
|
182
186
|
sweepExpiredSessionsFromStore(store);
|
|
@@ -194,6 +198,8 @@ export const createClientPairingRuntime = ({
|
|
|
194
198
|
allowedClientKinds: normalizeAllowedClientKinds(allowedClientKinds),
|
|
195
199
|
createdByClientId: normalizeOptionalString(createdByClientId),
|
|
196
200
|
usesRelay: usesRelay === true,
|
|
201
|
+
// Binds later ssh-host-token mint to this pairing; not used by redeem.
|
|
202
|
+
sshHostId: normalizeOptionalString(sshHostId),
|
|
197
203
|
};
|
|
198
204
|
store.sessions.push(session);
|
|
199
205
|
await writeStore(store);
|
|
@@ -139,4 +139,49 @@ 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
|
+
});
|
|
142
187
|
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Desktop SSH host listing + short-lived token mint for mobile-over-relay.
|
|
2
|
+
// Protected by the global /api UI auth gate (registerAuthAndAccessRoutes).
|
|
3
|
+
// Never returns clientToken from the list endpoint; token is only issued via
|
|
4
|
+
// POST /ssh-host-token. Tokenless ready hosts may mint on demand.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {import('express').Express} app
|
|
8
|
+
* @param {{
|
|
9
|
+
* readSettingsFromDiskMigrated: () => Promise<object>,
|
|
10
|
+
* getSshRoutingTable?: () => { id: string, localPort: number }[],
|
|
11
|
+
* mintSshHostToken?: (hostId: string) => Promise<string>,
|
|
12
|
+
* getPairingSession?: (id: string) => Promise<{ id: string, sshHostId?: string | null, usedAt?: string | null } | null>,
|
|
13
|
+
* express?: typeof import('express'),
|
|
14
|
+
* logger?: Pick<Console, 'warn' | 'error'>,
|
|
15
|
+
* }} deps
|
|
16
|
+
*/
|
|
17
|
+
export const registerDesktopHostRoutes = (app, deps) => {
|
|
18
|
+
const {
|
|
19
|
+
readSettingsFromDiskMigrated,
|
|
20
|
+
getSshRoutingTable = () => [],
|
|
21
|
+
mintSshHostToken,
|
|
22
|
+
getPairingSession,
|
|
23
|
+
express,
|
|
24
|
+
logger = console,
|
|
25
|
+
} = deps;
|
|
26
|
+
|
|
27
|
+
const readSshInstanceIds = (settings) => {
|
|
28
|
+
const instances = Array.isArray(settings?.desktopSshInstances) ? settings.desktopSshInstances : [];
|
|
29
|
+
const ids = new Set();
|
|
30
|
+
for (const entry of instances) {
|
|
31
|
+
const id = typeof entry?.id === 'string' ? entry.id.trim() : '';
|
|
32
|
+
if (id) ids.add(id);
|
|
33
|
+
}
|
|
34
|
+
return ids;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const routingPortById = () => {
|
|
38
|
+
const table = typeof getSshRoutingTable === 'function' ? getSshRoutingTable() : [];
|
|
39
|
+
/** @type {Map<string, number>} */
|
|
40
|
+
const map = new Map();
|
|
41
|
+
if (!Array.isArray(table)) return map;
|
|
42
|
+
for (const entry of table) {
|
|
43
|
+
const id = typeof entry?.id === 'string' ? entry.id.trim() : '';
|
|
44
|
+
const localPort = Number(entry?.localPort);
|
|
45
|
+
if (!id || !Number.isFinite(localPort)) continue;
|
|
46
|
+
map.set(id, localPort);
|
|
47
|
+
}
|
|
48
|
+
return map;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
app.get('/api/openchamber/desktop-hosts', async (_req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
54
|
+
const sshIds = readSshInstanceIds(settings);
|
|
55
|
+
const instances = Array.isArray(settings?.desktopSshInstances) ? settings.desktopSshInstances : [];
|
|
56
|
+
const hostsRaw = Array.isArray(settings?.desktopHosts) ? settings.desktopHosts : [];
|
|
57
|
+
/** @type {Map<string, { label?: string }>} */
|
|
58
|
+
const hostById = new Map();
|
|
59
|
+
for (const entry of hostsRaw) {
|
|
60
|
+
const id = typeof entry?.id === 'string' ? entry.id.trim() : '';
|
|
61
|
+
if (id) hostById.set(id, entry);
|
|
62
|
+
}
|
|
63
|
+
const livePorts = routingPortById();
|
|
64
|
+
|
|
65
|
+
const hosts = [];
|
|
66
|
+
for (const instance of instances) {
|
|
67
|
+
const id = typeof instance?.id === 'string' ? instance.id.trim() : '';
|
|
68
|
+
if (!id || !sshIds.has(id)) continue;
|
|
69
|
+
const stored = hostById.get(id);
|
|
70
|
+
const nickname = typeof instance?.nickname === 'string' ? instance.nickname.trim() : '';
|
|
71
|
+
const labelFromHost = typeof stored?.label === 'string' ? stored.label.trim() : '';
|
|
72
|
+
const label = labelFromHost || nickname || id;
|
|
73
|
+
const livePort = livePorts.get(id);
|
|
74
|
+
const preferred = Number(instance?.localForward?.preferredLocalPort);
|
|
75
|
+
const localPort = Number.isFinite(livePort)
|
|
76
|
+
? livePort
|
|
77
|
+
: (Number.isFinite(preferred) ? preferred : 0);
|
|
78
|
+
hosts.push({
|
|
79
|
+
id,
|
|
80
|
+
label,
|
|
81
|
+
localPort,
|
|
82
|
+
reachable: Number.isFinite(livePort),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
res.json({ hosts });
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Failed to list desktop hosts:', error);
|
|
89
|
+
res.status(500).json({
|
|
90
|
+
error: error instanceof Error ? error.message : 'Failed to list desktop hosts',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const jsonParser = express && typeof express.json === 'function'
|
|
96
|
+
? express.json({ limit: '16kb' })
|
|
97
|
+
: (_req, _res, next) => next();
|
|
98
|
+
|
|
99
|
+
app.post('/api/openchamber/ssh-host-token', jsonParser, async (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
102
|
+
const hostId = typeof req.body?.hostId === 'string' ? req.body.hostId.trim() : '';
|
|
103
|
+
if (!hostId) {
|
|
104
|
+
return res.status(404).json({ error: 'host_not_found' });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
108
|
+
const sshIds = readSshInstanceIds(settings);
|
|
109
|
+
if (!sshIds.has(hostId)) {
|
|
110
|
+
return res.status(404).json({ error: 'host_not_found' });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const pairingId = typeof req.body?.pairingId === 'string' ? req.body.pairingId.trim() : '';
|
|
114
|
+
if (pairingId) {
|
|
115
|
+
// Bound mint: pairing must exist, target this host, and already be redeemed.
|
|
116
|
+
// Redeem itself still issues the desktop token; this gate only authorizes
|
|
117
|
+
// reading the SSH instance's stored clientToken.
|
|
118
|
+
const session = typeof getPairingSession === 'function'
|
|
119
|
+
? await getPairingSession(pairingId)
|
|
120
|
+
: null;
|
|
121
|
+
const sessionHostId = typeof session?.sshHostId === 'string' ? session.sshHostId.trim() : '';
|
|
122
|
+
const redeemed = Boolean(session?.usedAt);
|
|
123
|
+
if (!session || sessionHostId !== hostId || !redeemed) {
|
|
124
|
+
return res.status(403).json({ error: 'pairing-mismatch' });
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
// Legacy unscoped mint — any authenticated bearer. Remove next cycle.
|
|
128
|
+
logger.warn?.(
|
|
129
|
+
'[desktop-hosts] POST /api/openchamber/ssh-host-token without pairingId is deprecated; bind mint to a redeemed pairing session',
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const hostsRaw = Array.isArray(settings?.desktopHosts) ? settings.desktopHosts : [];
|
|
134
|
+
const host = hostsRaw.find((entry) => typeof entry?.id === 'string' && entry.id.trim() === hostId);
|
|
135
|
+
let token = typeof host?.clientToken === 'string' ? host.clientToken.trim() : '';
|
|
136
|
+
if (!token && typeof mintSshHostToken === 'function') {
|
|
137
|
+
token = String(await mintSshHostToken(hostId) || '').trim();
|
|
138
|
+
}
|
|
139
|
+
if (!token) {
|
|
140
|
+
return res.status(404).json({ error: 'host_not_found' });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const livePort = routingPortById().get(hostId);
|
|
144
|
+
const reachable = Number.isFinite(livePort);
|
|
145
|
+
res.json({
|
|
146
|
+
token,
|
|
147
|
+
localPort: reachable ? livePort : null,
|
|
148
|
+
reachable,
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
logger.error?.('Failed to issue ssh host token:', error);
|
|
152
|
+
res.status(500).json({
|
|
153
|
+
error: error instanceof Error ? error.message : 'Failed to issue ssh host token',
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
};
|