@openchambery/web 1.17.1-beta.11 → 1.17.1-beta.13
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/dist/assets/{AgentAvatar-C6Hxu5_o.js → AgentAvatar-Coq-zdWI.js} +1 -1
- package/dist/assets/AssistantView-BXLxZXL5.js +1 -0
- package/dist/assets/{ChatContainer-CCyV53_o.js → ChatContainer-BJGlptbG.js} +22 -22
- package/dist/assets/DiagramView-Czx_DVw0.js +1 -0
- package/dist/assets/{FadeInOnReveal-CvNA_aGD.js → FadeInOnReveal-1KpYuooP.js} +1 -1
- package/dist/assets/{Icon-BPJ_quEe.js → Icon-Vc-aDche.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-DswfuLC-.js → MarkdownRendererImpl-B9Bv95Vd.js} +2 -2
- package/dist/assets/MobileDetailNavigation-RupyXzPu.js +1 -0
- package/dist/assets/MobileShareBridge-LVT1_q5C.js +4 -0
- package/dist/assets/MobileSurface-BVwSEqn4.js +2 -0
- package/dist/assets/MultiRunWindow-C7hbEpv9.js +1 -0
- package/dist/assets/OnboardingScreen-CrekVj31.js +1 -0
- package/dist/assets/OverlayScrollbar-vZXoSFA9.js +1 -0
- package/dist/assets/SettingsView-C1sYcHr1.js +25 -0
- package/dist/assets/SettingsWindow-MtcqpoiN.js +1 -0
- package/dist/assets/TerminalView-DXQZlHNA.js +8 -0
- package/dist/assets/{ToolOutputDialog-IVZYEtTZ.js → ToolOutputDialog-_Qzdo46T.js} +1 -1
- package/dist/assets/appThemeRegistry-Chv6_pxq.js +2 -0
- package/dist/assets/{button-BmNJBT13.js → button-rDAIP5kY.js} +1 -1
- package/dist/assets/{collapsible-68IqWBM1.js → collapsible-nLeZm1kw.js} +1 -1
- package/dist/assets/{context-menu-m2Ep287d.js → context-menu-DeJdA9F6.js} +1 -1
- package/dist/assets/desktopHosts-RE7NIkeq.js +1 -0
- package/dist/assets/desktopRelayRestore-BzyaU7-i.js +1 -0
- package/dist/assets/{diagram-YN0GZ1Q0.js → diagram-C7qK82ZL.js} +1 -1
- package/dist/assets/dialog-DlUK6mj-.js +1 -0
- package/dist/assets/es-DMX-Uo1i.js +15 -0
- package/dist/assets/{fileQueries-i4oziMnC.js → fileQueries-D8m3PQr0.js} +1 -1
- package/dist/assets/{fr-jWNPO91J.js → fr-CSFYzSd8.js} +2 -2
- package/dist/assets/{gitApi-CwRDHIOl.js → gitApi-DV6F5KKI.js} +1 -1
- package/dist/assets/gitApiHttp-BswZXfrf.js +2 -0
- package/dist/assets/i18n-BAqcSln8.js +1 -0
- package/dist/assets/{input-BLVhPvhN.js → input-CxulMZ9j.js} +1 -1
- package/dist/assets/insertionBoundaries-7-TvMfgn.js +1 -0
- package/dist/assets/{ja-Dcb0VtDE.js → ja-C3eCimlB.js} +2 -2
- package/dist/assets/{ko-CX71xNDX.js → ko-gnciIU9a.js} +2 -2
- package/dist/assets/main-CADHVH9Y.js +172 -0
- package/dist/assets/main-q1V-_wWx.js +2 -0
- package/dist/assets/{markdown-worker-Cq3Twp1A.js → markdown-worker-DmBYud1m.js} +1 -1
- package/dist/assets/{markdownTheme-CCnvI6L0.js → markdownTheme-CNJf24L-.js} +1 -1
- package/dist/assets/miniChat-VXuyA8mS.js +2 -0
- package/dist/assets/mobile-CaKZM4y3.js +2 -0
- package/dist/assets/multirun-QqMnIbXa.js +4 -0
- package/dist/assets/persistence-CFtygeYy.js +3 -0
- package/dist/assets/{pl-mR_3Sqr8.js → pl-CNk6BfNS.js} +1 -1
- package/dist/assets/projectMeta-WJhNyk90.js +3 -0
- package/dist/assets/pt-BR-D4G-0rLj.js +15 -0
- package/dist/assets/radio-C1riLnF0.js +1 -0
- package/dist/assets/renderElectronMiniChatApp-CnmMV1o9.js +2 -0
- package/dist/assets/renderMobileApp-Bhe41b2q.js +23 -0
- package/dist/assets/runtimeConfig-D4ETkuTj.js +1 -0
- package/dist/assets/runtimeEndpointReset-GtJt8YhT.js +145 -0
- package/dist/assets/sortable-tabs-strip-Fiq5nrkF.js +1 -0
- package/dist/assets/src-DrgsP0XF.css +2 -0
- package/dist/assets/{store-oojlrF4d.js → store-CS6l6pH7.js} +3 -3
- package/dist/assets/{terminalApi-BYTA045F.js → terminalApi-MQ285A_s.js} +1 -1
- package/dist/assets/{tooltip-BYGBBTc2.js → tooltip-CbxKYhKv.js} +1 -1
- package/dist/assets/ui-D5j3JxkE.js +475 -0
- package/dist/assets/uk-BUry1gqt.js +15 -0
- package/dist/assets/{url-CGHw6D-E.js → url-BSalv2ov.js} +1 -1
- package/dist/assets/useAppFontEffects-CaGwli-K.js +7 -0
- package/dist/assets/{useAssistantUIStore-ESTV1mEY.js → useAssistantUIStore-DGtjM827.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-DCZXksMp.js +1 -0
- package/dist/assets/useEffectiveDirectory-CssNtlqg.js +1 -0
- package/dist/assets/{useFontPreferences-Bu4ME5hk.js → useFontPreferences-C7FEHFns.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-B_mSwN7n.js +1 -0
- package/dist/assets/useUIStore-WQV8IuFw.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-BGHFpbvZ.js → useWorkerHighlightedLines-C74ZKDtw.js} +1 -1
- package/dist/assets/{utils-DyUyiLje.js → utils-Cwlw2GM0.js} +1 -1
- package/dist/assets/zh-CN-CJJZzHVW.js +15 -0
- package/dist/assets/zh-TW-DxhIXjND.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 +2 -0
- package/server/index.js +8 -0
- 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 +152 -0
- package/server/lib/desktop-hosts/routes.test.js +194 -0
- package/server/lib/opencode/bootstrap-runtime.js +5 -0
- package/server/lib/opencode/core-routes.js +3 -0
- package/server/lib/opencode/core-routes.test.js +15 -0
- package/server/lib/opencode/openchamber-routes.js +10 -0
- package/server/lib/package-manager.js +3 -1
- package/server/lib/relay/DOCUMENTATION.md +43 -0
- package/server/lib/relay/host-client.js +4 -1
- package/server/lib/relay/service.js +4 -0
- package/server/lib/relay/tunnel-host.js +57 -4
- package/server/lib/relay/tunnel-host.test.js +250 -2
- package/dist/assets/AssistantView-CCYDEoHv.js +0 -1
- package/dist/assets/DiagramView-ClKA8FV3.js +0 -1
- package/dist/assets/MobileDetailNavigation-DLoODj_O.js +0 -1
- package/dist/assets/MobileShareBridge-CAc7AFue.js +0 -4
- package/dist/assets/MobileSurface-76JUp8KU.js +0 -2
- package/dist/assets/MultiRunWindow-DkKQaIfX.js +0 -1
- package/dist/assets/OnboardingScreen-CdvaEBtt.js +0 -1
- package/dist/assets/OverlayScrollbar-B-K9NJfL.js +0 -1
- package/dist/assets/SettingsView-zu_QFVkf.js +0 -25
- package/dist/assets/SettingsWindow-D2q5mZqr.js +0 -1
- package/dist/assets/TerminalView-BSkAxeUb.js +0 -8
- package/dist/assets/appThemeRegistry-b9Hk2IDb.js +0 -2
- package/dist/assets/clipboard-CXWbHCAk.js +0 -1
- package/dist/assets/desktopHosts-BpjUSDz4.js +0 -1
- package/dist/assets/desktopRelayRestore-BzUEk1i7.js +0 -1
- package/dist/assets/dialog-BV1ENlLf.js +0 -1
- package/dist/assets/es-EXZUX1sB.js +0 -15
- package/dist/assets/gitApiHttp-C1DaEMKR.js +0 -2
- package/dist/assets/insertionBoundaries-hP0Vo_9C.js +0 -1
- package/dist/assets/main-BLqbjxPt.js +0 -2
- package/dist/assets/main-C7LjTesY.js +0 -172
- package/dist/assets/miniChat-Cr9SuyZk.js +0 -2
- package/dist/assets/mobile-Bp-qiKtn.js +0 -2
- package/dist/assets/multirun-_rwgORX-.js +0 -4
- package/dist/assets/persistence-C_KCiMcY.js +0 -3
- package/dist/assets/projectMeta-7TXLLSNB.js +0 -3
- package/dist/assets/pt-BR-D1RpVOvC.js +0 -15
- package/dist/assets/radio-CDEqI-yy.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-ChsEWTbg.js +0 -2
- package/dist/assets/renderMobileApp-B1pX9NK3.js +0 -23
- package/dist/assets/runtimeConfig-JSaYFA5x.js +0 -1
- package/dist/assets/runtimeEndpointReset-BS3Sd-xn.js +0 -145
- package/dist/assets/sortable-tabs-strip-DUk_Td2z.js +0 -1
- package/dist/assets/src-8zziWB78.css +0 -2
- package/dist/assets/uk-UvnVqWv-.js +0 -15
- package/dist/assets/useAppFontEffects-VKLeTcCY.js +0 -7
- package/dist/assets/useDesktopWindowControlsLayout-DLuCJzOs.js +0 -1
- package/dist/assets/useEffectiveDirectory-Bt1X5uGs.js +0 -1
- package/dist/assets/useMobileNavigationStore-DRbvzR8W.js +0 -475
- package/dist/assets/useSessionAutoCleanup-C8clHymf.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-q1V-_wWx.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-WQV8IuFw.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-CFtygeYy.js">
|
|
564
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-RE7NIkeq.js">
|
|
565
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-BzyaU7-i.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-BswZXfrf.js">
|
|
569
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-MQ285A_s.js">
|
|
570
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-D4ETkuTj.js">
|
|
571
|
+
<link rel="stylesheet" crossorigin href="/assets/src-DrgsP0XF.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-VXuyA8mS.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-WQV8IuFw.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-CFtygeYy.js">
|
|
86
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-RE7NIkeq.js">
|
|
87
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-BzyaU7-i.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-BswZXfrf.js">
|
|
91
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-MQ285A_s.js">
|
|
92
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-D4ETkuTj.js">
|
|
93
|
+
<link rel="stylesheet" crossorigin href="/assets/src-DrgsP0XF.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-CaKZM4y3.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-WQV8IuFw.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-CFtygeYy.js">
|
|
75
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-RE7NIkeq.js">
|
|
76
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-BzyaU7-i.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-BswZXfrf.js">
|
|
80
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-MQ285A_s.js">
|
|
81
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-D4ETkuTj.js">
|
|
82
|
+
<link rel="stylesheet" crossorigin href="/assets/src-DrgsP0XF.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,8 @@ 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 }[];
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export declare function startWebUiServer(
|
package/server/index.js
CHANGED
|
@@ -1241,6 +1241,11 @@ async function main(options = {}) {
|
|
|
1241
1241
|
const getDesktopRuntimeConfig = typeof options.getDesktopRuntimeConfig === 'function'
|
|
1242
1242
|
? options.getDesktopRuntimeConfig
|
|
1243
1243
|
: null;
|
|
1244
|
+
// Electron injects live SSH local-forward ports; non-desktop defaults to empty
|
|
1245
|
+
// so relay target-port routing never falls back to an unvalidated port.
|
|
1246
|
+
const getSshRoutingTable = typeof options.getSshRoutingTable === 'function'
|
|
1247
|
+
? options.getSshRoutingTable
|
|
1248
|
+
: () => [];
|
|
1244
1249
|
const sessionIndexDbPath = options.sessionIndexDbPath !== undefined
|
|
1245
1250
|
? options.sessionIndexDbPath
|
|
1246
1251
|
: process.env.OPENCHAMBER_SESSION_INDEX_DB_PATH;
|
|
@@ -1465,6 +1470,8 @@ async function main(options = {}) {
|
|
|
1465
1470
|
sessionIndexService,
|
|
1466
1471
|
sessionIndexSyncRuntime,
|
|
1467
1472
|
transcriptCacheService,
|
|
1473
|
+
getSshRoutingTable,
|
|
1474
|
+
getPairingSession: (id) => clientPairingRuntime.getPairingSession(id),
|
|
1468
1475
|
});
|
|
1469
1476
|
uiAuthController = bootstrapResult.uiAuthController;
|
|
1470
1477
|
realtimeProxyRuntime = attachRealtimeProxy({
|
|
@@ -1488,6 +1495,7 @@ async function main(options = {}) {
|
|
|
1488
1495
|
readSettingsStrict: readSettingsFromDiskStrict,
|
|
1489
1496
|
remoteClientAuthRuntime,
|
|
1490
1497
|
getLocalPort: () => tunnelRuntimeContext.getActivePort(),
|
|
1498
|
+
getSshRoutingTable,
|
|
1491
1499
|
// One relay host per machine: every instance sharing this data dir shares
|
|
1492
1500
|
// the relay identity (serverId), so concurrent hosts evict each other at
|
|
1493
1501
|
// the relay worker and devices land on a random local instance.
|
|
@@ -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,152 @@
|
|
|
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 for SSH instance hosts that already store one.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {import('express').Express} app
|
|
8
|
+
* @param {{
|
|
9
|
+
* readSettingsFromDiskMigrated: () => Promise<object>,
|
|
10
|
+
* getSshRoutingTable?: () => { id: string, localPort: number }[],
|
|
11
|
+
* getPairingSession?: (id: string) => Promise<{ id: string, sshHostId?: string | null, usedAt?: string | null } | null>,
|
|
12
|
+
* express?: typeof import('express'),
|
|
13
|
+
* logger?: Pick<Console, 'warn' | 'error'>,
|
|
14
|
+
* }} deps
|
|
15
|
+
*/
|
|
16
|
+
export const registerDesktopHostRoutes = (app, deps) => {
|
|
17
|
+
const {
|
|
18
|
+
readSettingsFromDiskMigrated,
|
|
19
|
+
getSshRoutingTable = () => [],
|
|
20
|
+
getPairingSession,
|
|
21
|
+
express,
|
|
22
|
+
logger = console,
|
|
23
|
+
} = deps;
|
|
24
|
+
|
|
25
|
+
const readSshInstanceIds = (settings) => {
|
|
26
|
+
const instances = Array.isArray(settings?.desktopSshInstances) ? settings.desktopSshInstances : [];
|
|
27
|
+
const ids = new Set();
|
|
28
|
+
for (const entry of instances) {
|
|
29
|
+
const id = typeof entry?.id === 'string' ? entry.id.trim() : '';
|
|
30
|
+
if (id) ids.add(id);
|
|
31
|
+
}
|
|
32
|
+
return ids;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const routingPortById = () => {
|
|
36
|
+
const table = typeof getSshRoutingTable === 'function' ? getSshRoutingTable() : [];
|
|
37
|
+
/** @type {Map<string, number>} */
|
|
38
|
+
const map = new Map();
|
|
39
|
+
if (!Array.isArray(table)) return map;
|
|
40
|
+
for (const entry of table) {
|
|
41
|
+
const id = typeof entry?.id === 'string' ? entry.id.trim() : '';
|
|
42
|
+
const localPort = Number(entry?.localPort);
|
|
43
|
+
if (!id || !Number.isFinite(localPort)) continue;
|
|
44
|
+
map.set(id, localPort);
|
|
45
|
+
}
|
|
46
|
+
return map;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
app.get('/api/openchamber/desktop-hosts', async (_req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
52
|
+
const sshIds = readSshInstanceIds(settings);
|
|
53
|
+
const instances = Array.isArray(settings?.desktopSshInstances) ? settings.desktopSshInstances : [];
|
|
54
|
+
const hostsRaw = Array.isArray(settings?.desktopHosts) ? settings.desktopHosts : [];
|
|
55
|
+
/** @type {Map<string, { label?: string }>} */
|
|
56
|
+
const hostById = new Map();
|
|
57
|
+
for (const entry of hostsRaw) {
|
|
58
|
+
const id = typeof entry?.id === 'string' ? entry.id.trim() : '';
|
|
59
|
+
if (id) hostById.set(id, entry);
|
|
60
|
+
}
|
|
61
|
+
const livePorts = routingPortById();
|
|
62
|
+
|
|
63
|
+
const hosts = [];
|
|
64
|
+
for (const instance of instances) {
|
|
65
|
+
const id = typeof instance?.id === 'string' ? instance.id.trim() : '';
|
|
66
|
+
if (!id || !sshIds.has(id)) continue;
|
|
67
|
+
const stored = hostById.get(id);
|
|
68
|
+
const nickname = typeof instance?.nickname === 'string' ? instance.nickname.trim() : '';
|
|
69
|
+
const labelFromHost = typeof stored?.label === 'string' ? stored.label.trim() : '';
|
|
70
|
+
const label = labelFromHost || nickname || id;
|
|
71
|
+
const livePort = livePorts.get(id);
|
|
72
|
+
const preferred = Number(instance?.localForward?.preferredLocalPort);
|
|
73
|
+
const localPort = Number.isFinite(livePort)
|
|
74
|
+
? livePort
|
|
75
|
+
: (Number.isFinite(preferred) ? preferred : 0);
|
|
76
|
+
hosts.push({
|
|
77
|
+
id,
|
|
78
|
+
label,
|
|
79
|
+
localPort,
|
|
80
|
+
reachable: Number.isFinite(livePort),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
res.json({ hosts });
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Failed to list desktop hosts:', error);
|
|
87
|
+
res.status(500).json({
|
|
88
|
+
error: error instanceof Error ? error.message : 'Failed to list desktop hosts',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const jsonParser = express && typeof express.json === 'function'
|
|
94
|
+
? express.json({ limit: '16kb' })
|
|
95
|
+
: (_req, _res, next) => next();
|
|
96
|
+
|
|
97
|
+
app.post('/api/openchamber/ssh-host-token', jsonParser, async (req, res) => {
|
|
98
|
+
try {
|
|
99
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
100
|
+
const hostId = typeof req.body?.hostId === 'string' ? req.body.hostId.trim() : '';
|
|
101
|
+
if (!hostId) {
|
|
102
|
+
return res.status(404).json({ error: 'host_not_found' });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
106
|
+
const sshIds = readSshInstanceIds(settings);
|
|
107
|
+
if (!sshIds.has(hostId)) {
|
|
108
|
+
return res.status(404).json({ error: 'host_not_found' });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const pairingId = typeof req.body?.pairingId === 'string' ? req.body.pairingId.trim() : '';
|
|
112
|
+
if (pairingId) {
|
|
113
|
+
// Bound mint: pairing must exist, target this host, and already be redeemed.
|
|
114
|
+
// Redeem itself still issues the desktop token; this gate only authorizes
|
|
115
|
+
// reading the SSH instance's stored clientToken.
|
|
116
|
+
const session = typeof getPairingSession === 'function'
|
|
117
|
+
? await getPairingSession(pairingId)
|
|
118
|
+
: null;
|
|
119
|
+
const sessionHostId = typeof session?.sshHostId === 'string' ? session.sshHostId.trim() : '';
|
|
120
|
+
const redeemed = Boolean(session?.usedAt);
|
|
121
|
+
if (!session || sessionHostId !== hostId || !redeemed) {
|
|
122
|
+
return res.status(403).json({ error: 'pairing-mismatch' });
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
// Legacy unscoped mint — any authenticated bearer. Remove next cycle.
|
|
126
|
+
logger.warn?.(
|
|
127
|
+
'[desktop-hosts] POST /api/openchamber/ssh-host-token without pairingId is deprecated; bind mint to a redeemed pairing session',
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const hostsRaw = Array.isArray(settings?.desktopHosts) ? settings.desktopHosts : [];
|
|
132
|
+
const host = hostsRaw.find((entry) => typeof entry?.id === 'string' && entry.id.trim() === hostId);
|
|
133
|
+
const token = typeof host?.clientToken === 'string' ? host.clientToken.trim() : '';
|
|
134
|
+
if (!token) {
|
|
135
|
+
return res.status(404).json({ error: 'host_not_found' });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const livePort = routingPortById().get(hostId);
|
|
139
|
+
const reachable = Number.isFinite(livePort);
|
|
140
|
+
res.json({
|
|
141
|
+
token,
|
|
142
|
+
localPort: reachable ? livePort : null,
|
|
143
|
+
reachable,
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
logger.error?.('Failed to issue ssh host token:', error);
|
|
147
|
+
res.status(500).json({
|
|
148
|
+
error: error instanceof Error ? error.message : 'Failed to issue ssh host token',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import request from 'supertest';
|
|
4
|
+
|
|
5
|
+
import { registerDesktopHostRoutes } from './routes.js';
|
|
6
|
+
|
|
7
|
+
const baseSettings = {
|
|
8
|
+
desktopSshInstances: [{ id: 'ssh-1' }, { id: 'ssh-2' }],
|
|
9
|
+
desktopHosts: [
|
|
10
|
+
{ id: 'ssh-1', clientToken: 'ssh-token-1' },
|
|
11
|
+
{ id: 'ssh-2', clientToken: 'ssh-token-2' },
|
|
12
|
+
{ id: 'lan-other', clientToken: 'other-token' },
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const createApp = ({
|
|
17
|
+
settings = baseSettings,
|
|
18
|
+
getSshRoutingTable = () => [],
|
|
19
|
+
getPairingSession,
|
|
20
|
+
logger = { warn: vi.fn(), error: vi.fn() },
|
|
21
|
+
} = {}) => {
|
|
22
|
+
const app = express();
|
|
23
|
+
registerDesktopHostRoutes(app, {
|
|
24
|
+
express,
|
|
25
|
+
readSettingsFromDiskMigrated: async () => settings,
|
|
26
|
+
getSshRoutingTable,
|
|
27
|
+
getPairingSession,
|
|
28
|
+
logger,
|
|
29
|
+
});
|
|
30
|
+
return { app, logger };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe('desktop host routes', () => {
|
|
34
|
+
it('lists SSH hosts with reachable from the routing table and never returns clientToken', async () => {
|
|
35
|
+
const { app } = createApp({
|
|
36
|
+
settings: {
|
|
37
|
+
desktopSshInstances: [
|
|
38
|
+
{ id: 'ssh-1', nickname: 'Remote A', localForward: { preferredLocalPort: 41001 } },
|
|
39
|
+
{ id: 'ssh-2', nickname: 'Remote B', localForward: { preferredLocalPort: 41002 } },
|
|
40
|
+
],
|
|
41
|
+
desktopHosts: [
|
|
42
|
+
{ id: 'ssh-1', label: 'Remote A', clientToken: 'secret-a' },
|
|
43
|
+
{ id: 'ssh-2', label: 'Remote B', clientToken: 'secret-b' },
|
|
44
|
+
{ id: 'lan-other', label: 'Other', clientToken: 'secret-other' },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
getSshRoutingTable: () => [{ id: 'ssh-1', localPort: 41234 }],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const res = await request(app).get('/api/openchamber/desktop-hosts');
|
|
51
|
+
expect(res.status).toBe(200);
|
|
52
|
+
expect(res.body).toEqual({
|
|
53
|
+
hosts: [
|
|
54
|
+
{ id: 'ssh-1', label: 'Remote A', localPort: 41234, reachable: true },
|
|
55
|
+
{ id: 'ssh-2', label: 'Remote B', localPort: 41002, reachable: false },
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
expect(JSON.stringify(res.body)).not.toContain('secret');
|
|
59
|
+
expect(JSON.stringify(res.body)).not.toContain('clientToken');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('issues token with localPort/reachable when pairingId matches a redeemed session', async () => {
|
|
63
|
+
const getPairingSession = vi.fn(async (id) => {
|
|
64
|
+
if (id !== 'pair_ok') return null;
|
|
65
|
+
return { id: 'pair_ok', sshHostId: 'ssh-1', usedAt: '2026-01-01T00:00:00.000Z' };
|
|
66
|
+
});
|
|
67
|
+
const { app, logger } = createApp({
|
|
68
|
+
getSshRoutingTable: () => [{ id: 'ssh-1', localPort: 41234 }],
|
|
69
|
+
getPairingSession,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const ok = await request(app)
|
|
73
|
+
.post('/api/openchamber/ssh-host-token')
|
|
74
|
+
.send({ hostId: 'ssh-1', pairingId: 'pair_ok' });
|
|
75
|
+
expect(ok.status).toBe(200);
|
|
76
|
+
expect(ok.headers['cache-control']).toBe('no-store');
|
|
77
|
+
expect(ok.body).toEqual({
|
|
78
|
+
token: 'ssh-token-1',
|
|
79
|
+
localPort: 41234,
|
|
80
|
+
reachable: true,
|
|
81
|
+
});
|
|
82
|
+
expect(getPairingSession).toHaveBeenCalledWith('pair_ok');
|
|
83
|
+
expect(logger.warn).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns 403 when pairingId sshHostId does not match hostId', async () => {
|
|
87
|
+
const { app } = createApp({
|
|
88
|
+
getPairingSession: async () => ({
|
|
89
|
+
id: 'pair_x',
|
|
90
|
+
sshHostId: 'ssh-2',
|
|
91
|
+
usedAt: '2026-01-01T00:00:00.000Z',
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const res = await request(app)
|
|
96
|
+
.post('/api/openchamber/ssh-host-token')
|
|
97
|
+
.send({ hostId: 'ssh-1', pairingId: 'pair_x' });
|
|
98
|
+
expect(res.status).toBe(403);
|
|
99
|
+
expect(res.body).toEqual({ error: 'pairing-mismatch' });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('returns 403 when pairing session is not redeemed', async () => {
|
|
103
|
+
const { app } = createApp({
|
|
104
|
+
getPairingSession: async () => ({
|
|
105
|
+
id: 'pair_pending',
|
|
106
|
+
sshHostId: 'ssh-1',
|
|
107
|
+
usedAt: null,
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const res = await request(app)
|
|
112
|
+
.post('/api/openchamber/ssh-host-token')
|
|
113
|
+
.send({ hostId: 'ssh-1', pairingId: 'pair_pending' });
|
|
114
|
+
expect(res.status).toBe(403);
|
|
115
|
+
expect(res.body).toEqual({ error: 'pairing-mismatch' });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('returns 403 when pairingId is unknown', async () => {
|
|
119
|
+
const { app } = createApp({
|
|
120
|
+
getPairingSession: async () => null,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const res = await request(app)
|
|
124
|
+
.post('/api/openchamber/ssh-host-token')
|
|
125
|
+
.send({ hostId: 'ssh-1', pairingId: 'pair_missing' });
|
|
126
|
+
expect(res.status).toBe(403);
|
|
127
|
+
expect(res.body).toEqual({ error: 'pairing-mismatch' });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('allows legacy mint without pairingId and logs a deprecation warning', async () => {
|
|
131
|
+
const { app, logger } = createApp({
|
|
132
|
+
getSshRoutingTable: () => [{ id: 'ssh-1', localPort: 41234 }],
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const ok = await request(app)
|
|
136
|
+
.post('/api/openchamber/ssh-host-token')
|
|
137
|
+
.send({ hostId: 'ssh-1' });
|
|
138
|
+
expect(ok.status).toBe(200);
|
|
139
|
+
expect(ok.headers['cache-control']).toBe('no-store');
|
|
140
|
+
expect(ok.body).toEqual({
|
|
141
|
+
token: 'ssh-token-1',
|
|
142
|
+
localPort: 41234,
|
|
143
|
+
reachable: true,
|
|
144
|
+
});
|
|
145
|
+
expect(logger.warn).toHaveBeenCalledTimes(1);
|
|
146
|
+
expect(String(logger.warn.mock.calls[0][0])).toMatch(/deprecated/i);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('returns reachable false and localPort null when routing table misses', async () => {
|
|
150
|
+
const { app } = createApp({
|
|
151
|
+
getSshRoutingTable: () => [],
|
|
152
|
+
getPairingSession: async () => ({
|
|
153
|
+
id: 'pair_ok',
|
|
154
|
+
sshHostId: 'ssh-1',
|
|
155
|
+
usedAt: '2026-01-01T00:00:00.000Z',
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const res = await request(app)
|
|
160
|
+
.post('/api/openchamber/ssh-host-token')
|
|
161
|
+
.send({ hostId: 'ssh-1', pairingId: 'pair_ok' });
|
|
162
|
+
expect(res.status).toBe(200);
|
|
163
|
+
expect(res.body).toEqual({
|
|
164
|
+
token: 'ssh-token-1',
|
|
165
|
+
localPort: null,
|
|
166
|
+
reachable: false,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('returns 404 for missing / non-SSH / tokenless hosts', async () => {
|
|
171
|
+
const { app } = createApp();
|
|
172
|
+
|
|
173
|
+
const missing = await request(app)
|
|
174
|
+
.post('/api/openchamber/ssh-host-token')
|
|
175
|
+
.send({ hostId: 'missing' });
|
|
176
|
+
expect(missing.status).toBe(404);
|
|
177
|
+
|
|
178
|
+
const nonSsh = await request(app)
|
|
179
|
+
.post('/api/openchamber/ssh-host-token')
|
|
180
|
+
.send({ hostId: 'lan-other' });
|
|
181
|
+
expect(nonSsh.status).toBe(404);
|
|
182
|
+
|
|
183
|
+
const { app: noTokenApp } = createApp({
|
|
184
|
+
settings: {
|
|
185
|
+
desktopSshInstances: [{ id: 'ssh-empty' }],
|
|
186
|
+
desktopHosts: [{ id: 'ssh-empty' }],
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
const noToken = await request(noTokenApp)
|
|
190
|
+
.post('/api/openchamber/ssh-host-token')
|
|
191
|
+
.send({ hostId: 'ssh-empty' });
|
|
192
|
+
expect(noToken.status).toBe(404);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -61,6 +61,8 @@ export const createBootstrapRuntime = (dependencies) => {
|
|
|
61
61
|
sessionIndexService,
|
|
62
62
|
sessionIndexSyncRuntime,
|
|
63
63
|
transcriptCacheService,
|
|
64
|
+
getSshRoutingTable,
|
|
65
|
+
getPairingSession,
|
|
64
66
|
} = options;
|
|
65
67
|
|
|
66
68
|
const uiAuthController = createUiAuth({
|
|
@@ -149,6 +151,9 @@ export const createBootstrapRuntime = (dependencies) => {
|
|
|
149
151
|
sessionIndexService,
|
|
150
152
|
sessionIndexSyncRuntime,
|
|
151
153
|
transcriptCacheService,
|
|
154
|
+
getSshRoutingTable,
|
|
155
|
+
getPairingSession,
|
|
156
|
+
express,
|
|
152
157
|
});
|
|
153
158
|
|
|
154
159
|
return {
|
|
@@ -871,6 +871,9 @@ 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,
|
|
874
877
|
});
|
|
875
878
|
void reconcileRelay();
|
|
876
879
|
res.setHeader('Cache-Control', 'no-store');
|
|
@@ -774,9 +774,24 @@ describe('core-routes', () => {
|
|
|
774
774
|
allowedClientKinds: ['mobile'],
|
|
775
775
|
createdByClientId: null,
|
|
776
776
|
usesRelay: false,
|
|
777
|
+
sshHostId: undefined,
|
|
777
778
|
});
|
|
778
779
|
});
|
|
779
780
|
|
|
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
|
+
|
|
780
795
|
it('advertises the caller-supplied serverUrl as the direct candidate over the request origin', async () => {
|
|
781
796
|
const { app } = createPairingRouteApp();
|
|
782
797
|
|