@openchambery/web 1.18.4-beta.2 → 1.18.4-beta.20
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/cli.test.js +25 -0
- package/bin/lib/DOCUMENTATION.md +2 -1
- package/bin/lib/cli-args.js +19 -0
- package/bin/lib/commands-serve.js +73 -4
- package/dist/assets/{AgentAvatar-BvhXxFvg.js → AgentAvatar-DDcjamcJ.js} +1 -1
- package/dist/assets/AssistantView-BKtWtK9a.js +1 -0
- package/dist/assets/{ChatContainer-Ba2Qqd2R.js → ChatContainer-CmZuBd4k.js} +42 -42
- package/dist/assets/{DiagramView--CkYLtF9.js → DiagramView-C3H0iaE1.js} +1 -1
- package/dist/assets/{FadeInOnReveal-98buIVdv.js → FadeInOnReveal-By2F9vcI.js} +1 -1
- package/dist/assets/{Icon-BhSwbLaF.js → Icon-DpjYnXxn.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-CuaQbwUx.js → MarkdownRendererImpl-Oq7v-1r2.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-ld2mS8hq.js → MobileDetailNavigation-Bh_f-iat.js} +1 -1
- package/dist/assets/MobileShareBridge-BvcbH_WJ.js +4 -0
- package/dist/assets/MobileSurface-BTMvLLPa.js +2 -0
- package/dist/assets/{MultiRunWindow-kryfNvF6.js → MultiRunWindow-Cv4K6i1U.js} +1 -1
- package/dist/assets/{OnboardingScreen-C1n_wVb-.js → OnboardingScreen-BKmuT7AL.js} +1 -1
- package/dist/assets/{OverlayScrollbar-CXFN6Cqq.js → OverlayScrollbar-Ch3J6ASR.js} +1 -1
- package/dist/assets/{SettingsView-BBTUVI-a.js → SettingsView-DZT_nuTt.js} +11 -11
- package/dist/assets/{SettingsWindow-4RS_41Wb.js → SettingsWindow-Bjb84x0C.js} +1 -1
- package/dist/assets/{TerminalView-B1he4xRe.js → TerminalView-CXq2MDyL.js} +1 -1
- package/dist/assets/{ToolOutputDialog-ak07C60J.js → ToolOutputDialog-CTrqAwo1.js} +1 -1
- package/dist/assets/appThemeRegistry-D6EEEMQw.js +2 -0
- package/dist/assets/{button-BQY5ugjh.js → button-C7OXr4ae.js} +1 -1
- package/dist/assets/{clipboard-BvJ7NunE.js → clipboard-DWuULPzo.js} +1 -1
- package/dist/assets/{collapsible-CqcZGw3W.js → collapsible-DaEya6b6.js} +1 -1
- package/dist/assets/{context-menu-DSUJY2i3.js → context-menu-Cjof3V7x.js} +1 -1
- package/dist/assets/desktopHosts-Txp4xOVQ.js +1 -0
- package/dist/assets/desktopRelayRestore-C_FSEHKM.js +1 -0
- package/dist/assets/{diagram-B8D9Klct.js → diagram-CHfginjS.js} +1 -1
- package/dist/assets/{dialog-sGFkIKP2.js → dialog-DPS0W9eS.js} +1 -1
- package/dist/assets/{es-BuinIayO.js → es-BX0Vm3Ac.js} +4 -4
- package/dist/assets/fileQueries-BrUKKgPW.js +1 -0
- package/dist/assets/{fr-CT7us58h.js → fr-CuPzXTYx.js} +4 -4
- package/dist/assets/{gitApi-DtV2V_Fp.js → gitApi-mV826uOd.js} +2 -2
- package/dist/assets/{input-CgIoxUJc.js → input-BzYA603C.js} +1 -1
- package/dist/assets/insertionBoundaries-fFm8sTaK.js +1 -0
- package/dist/assets/{ja-CCuAH4nO.js → ja-DGtzjrFO.js} +4 -4
- package/dist/assets/{ko-lXbhGt6P.js → ko-DZ9Di0je.js} +4 -4
- package/dist/assets/{main-CoNVZvj1.js → main-CqaUoTu5.js} +2 -2
- package/dist/assets/main-DaX6y8EG.js +172 -0
- package/dist/assets/{markdown-worker-C5Om4TVK.js → markdown-worker-W06-MhF5.js} +1 -1
- package/dist/assets/{markdownTheme-BdF4C4-o.js → markdownTheme-B89dbj3N.js} +1 -1
- package/dist/assets/{miniChat-CYd1PMdJ.js → miniChat-CX64FVKM.js} +2 -2
- package/dist/assets/mobile-BljNuNl6.js +2 -0
- package/dist/assets/multirun-Bdv-VeHM.js +4 -0
- package/dist/assets/persistence-6sclr7B6.js +3 -0
- package/dist/assets/pl-BuCO3qb1.js +15 -0
- package/dist/assets/platform-CbeLpKPk.js +2 -0
- package/dist/assets/projectMeta-EGeQpzvX.js +3 -0
- package/dist/assets/{pt-BR-DWbGs1bc.js → pt-BR-Brf4RMsw.js} +4 -4
- package/dist/assets/radio-BPJ5XDOL.js +1 -0
- package/dist/assets/renderElectronMiniChatApp-7_MeDuZ2.js +2 -0
- package/dist/assets/renderMobileApp-J09WW2wY.js +23 -0
- package/dist/assets/runtimeConfig-DbkphGhQ.js +2 -0
- package/dist/assets/runtimeEndpointReset-DooWSsoH.js +147 -0
- package/dist/assets/{sortable-tabs-strip-pSnPuBXA.js → sortable-tabs-strip-nLsPC06f.js} +1 -1
- package/dist/assets/src-Yqt5hwRm.css +2 -0
- package/dist/assets/{store-BuXogmxp.js → store-DQY8Fkb0.js} +4 -4
- package/dist/assets/{terminalApi-jfes-HIU.js → terminalApi-BpPeXWIu.js} +1 -1
- package/dist/assets/{tooltip-rN8LrxMF.js → tooltip-D_orJMYd.js} +1 -1
- package/dist/assets/{uk-DgOxtvS7.js → uk-C-wmaPTi.js} +4 -4
- package/dist/assets/{url-B0Ak7X6b.js → url-BsaBG5-1.js} +1 -1
- package/dist/assets/useAppFontEffects-DFRR7hqN.js +7 -0
- package/dist/assets/{useAssistantUIStore-SPh025NE.js → useAssistantUIStore-BHsHXCKE.js} +1 -1
- package/dist/assets/{useDesktopWindowControlsLayout-BSHPIcFE.js → useDesktopWindowControlsLayout-7oe0nJkK.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-Cbsu48gT.js → useEffectiveDirectory-DEbnpVAu.js} +1 -1
- package/dist/assets/{useFontPreferences-Bz3crRRO.js → useFontPreferences-ckzpXQaJ.js} +1 -1
- package/dist/assets/useMobileNavigationStore-CPi-bAr0.js +475 -0
- package/dist/assets/useSessionAutoCleanup-BFwpZAij.js +1 -0
- package/dist/assets/useUIStore-DzwCfRqr.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-D7ZRwkm3.js → useWorkerHighlightedLines-Cb7i5p7s.js} +1 -1
- package/dist/assets/{utils-DfPDhqys.js → utils-B1EK5zKa.js} +1 -1
- package/dist/assets/{zh-CN-BPGatJp9.js → zh-CN-DueRrIql.js} +4 -4
- package/dist/assets/{zh-TW-qUWWsaxK.js → zh-TW-6875X80V.js} +4 -4
- 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 +0 -4
- package/server/index.js +11 -20
- package/server/lib/client-auth/pairing.js +1 -7
- package/server/lib/client-auth/pairing.test.js +0 -45
- package/server/lib/config-sync/config-sync.test.js +52 -0
- package/server/lib/config-sync/scripts.js +10 -2
- package/server/lib/notifications/DOCUMENTATION.md +2 -1
- package/server/lib/notifications/emitter-runtime.js +78 -1
- package/server/lib/notifications/emitter-runtime.test.js +94 -1
- package/server/lib/opencode/DOCUMENTATION.md +4 -4
- package/server/lib/opencode/bootstrap-runtime.js +0 -6
- package/server/lib/opencode/cli-entry-runtime.js +5 -1
- package/server/lib/opencode/cli-entry-runtime.test.js +17 -0
- package/server/lib/opencode/core-routes.js +0 -3
- package/server/lib/opencode/core-routes.test.js +0 -15
- package/server/lib/opencode/openchamber-routes.js +0 -11
- package/server/lib/relay/DOCUMENTATION.md +3 -45
- package/server/lib/relay/host-client.js +1 -4
- package/server/lib/relay/service.js +28 -16
- package/server/lib/relay/service.test.js +64 -4
- package/server/lib/relay/tunnel-host.js +2 -52
- package/server/lib/relay/tunnel-host.test.js +2 -250
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -1
- package/server/lib/scheduled-tasks/routes.js +51 -8
- package/server/lib/session-assist/DOCUMENTATION.md +4 -2
- package/server/lib/session-assist/runtime.js +67 -30
- package/server/lib/session-assist/runtime.test.js +114 -0
- package/server/lib/session-title/DOCUMENTATION.md +15 -10
- package/server/lib/session-title/runtime.js +24 -0
- package/server/lib/session-title/runtime.test.js +143 -0
- package/server/lib/ui-auth/ui-auth.js +3 -1
- package/server/lib/ui-auth/ui-auth.test.js +25 -0
- package/server/sse-routes.test.js +64 -1
- package/dist/assets/AssistantView-DMK-KCPX.js +0 -1
- package/dist/assets/MobileShareBridge-lTfofNXr.js +0 -4
- package/dist/assets/MobileSurface-Bc_pAGrN.js +0 -2
- package/dist/assets/appThemeRegistry-ChhJvOPo.js +0 -2
- package/dist/assets/desktopHosts-aA40MHzB.js +0 -1
- package/dist/assets/desktopRelayRestore-CxUYRC6v.js +0 -1
- package/dist/assets/fileQueries-BSE4ycQt.js +0 -1
- package/dist/assets/insertionBoundaries-BIS62xbD.js +0 -1
- package/dist/assets/main-ClQdYXuu.js +0 -172
- package/dist/assets/mobile-CJkbELE1.js +0 -2
- package/dist/assets/multirun-DfYluvqf.js +0 -4
- package/dist/assets/persistence-DwXJK4-j.js +0 -3
- package/dist/assets/pl-DCsKBRAg.js +0 -15
- package/dist/assets/platform-CXJwZIZe.js +0 -2
- package/dist/assets/projectMeta-jG2rzIks.js +0 -3
- package/dist/assets/radio-CIR_xKSG.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-D6cOGBOZ.js +0 -2
- package/dist/assets/renderMobileApp-BuiAqCNI.js +0 -23
- package/dist/assets/runtimeConfig-B9P24SI-.js +0 -2
- package/dist/assets/runtimeEndpointReset-BLmUiRJq.js +0 -147
- package/dist/assets/src-DF4hiLrB.css +0 -2
- package/dist/assets/useAppFontEffects-DCQnaU6M.js +0 -7
- package/dist/assets/useMobileNavigationStore-BYiwDqQe.js +0 -476
- package/dist/assets/useSessionAutoCleanup-DDdudIWc.js +0 -1
- package/dist/assets/useUIStore-BJpkPwky.js +0 -1
- package/server/lib/desktop-hosts/routes.js +0 -157
- 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-
|
|
555
|
+
<script type="module" crossorigin src="/assets/main-CqaUoTu5.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-DzwCfRqr.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-6sclr7B6.js">
|
|
564
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-Txp4xOVQ.js">
|
|
565
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-C_FSEHKM.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-
|
|
569
|
-
<link rel="modulepreload" crossorigin href="/assets/terminalApi-
|
|
568
|
+
<link rel="modulepreload" crossorigin href="/assets/platform-CbeLpKPk.js">
|
|
569
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-BpPeXWIu.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-
|
|
573
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
572
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DbkphGhQ.js">
|
|
573
|
+
<link rel="stylesheet" crossorigin href="/assets/src-Yqt5hwRm.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
|
package/dist/mini-chat.html
CHANGED
|
@@ -74,25 +74,25 @@
|
|
|
74
74
|
}
|
|
75
75
|
</style>
|
|
76
76
|
|
|
77
|
-
<script type="module" crossorigin src="/assets/miniChat-
|
|
77
|
+
<script type="module" crossorigin src="/assets/miniChat-CX64FVKM.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-DzwCfRqr.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-6sclr7B6.js">
|
|
86
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-Txp4xOVQ.js">
|
|
87
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-C_FSEHKM.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-
|
|
91
|
-
<link rel="modulepreload" crossorigin href="/assets/terminalApi-
|
|
90
|
+
<link rel="modulepreload" crossorigin href="/assets/platform-CbeLpKPk.js">
|
|
91
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-BpPeXWIu.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-
|
|
95
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
94
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DbkphGhQ.js">
|
|
95
|
+
<link rel="stylesheet" crossorigin href="/assets/src-Yqt5hwRm.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-
|
|
66
|
+
<script type="module" crossorigin src="/assets/mobile-BljNuNl6.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-DzwCfRqr.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-6sclr7B6.js">
|
|
75
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-Txp4xOVQ.js">
|
|
76
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-C_FSEHKM.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-
|
|
80
|
-
<link rel="modulepreload" crossorigin href="/assets/terminalApi-
|
|
79
|
+
<link rel="modulepreload" crossorigin href="/assets/platform-CbeLpKPk.js">
|
|
80
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-BpPeXWIu.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-
|
|
84
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
83
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DbkphGhQ.js">
|
|
84
|
+
<link rel="stylesheet" crossorigin href="/assets/src-Yqt5hwRm.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
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,
|
|
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
|
|
1188
|
-
// servers must not present "Anywhere" as
|
|
1189
|
-
|
|
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
|
|
1648
|
-
// socket or
|
|
1649
|
-
// cannot inherit a leftover desktop env and start
|
|
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 (
|
|
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
|
|
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
|
|
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
|
});
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
assertTargetCapability,
|
|
15
15
|
buildDefaultSyncSelections,
|
|
16
16
|
buildRemoteSyncFinalizeScript,
|
|
17
|
+
buildRemoteSyncInventoryScript,
|
|
17
18
|
buildRemoteSyncPrepareScript,
|
|
18
19
|
createSshSyncTarget,
|
|
19
20
|
filterPlanBySelections,
|
|
@@ -195,3 +196,54 @@ describe('generational prepare backups', () => {
|
|
|
195
196
|
})).toThrow();
|
|
196
197
|
});
|
|
197
198
|
});
|
|
199
|
+
|
|
200
|
+
describe('remote inventory script', () => {
|
|
201
|
+
it('emits SYNC_INVENTORY as valid JSON with quoted path values (regression: bare quotes broke JSON.parse)', async () => {
|
|
202
|
+
const home = await makeTempDir();
|
|
203
|
+
const configDir = path.join(home, '.config', 'opencode');
|
|
204
|
+
await fsp.mkdir(path.join(configDir, 'agents'), { recursive: true });
|
|
205
|
+
await fsp.writeFile(path.join(configDir, 'opencode.jsonc'), '{"a":1}\n');
|
|
206
|
+
await fsp.writeFile(path.join(configDir, 'AGENTS.md'), '# agents\n');
|
|
207
|
+
await fsp.writeFile(path.join(configDir, 'agents', 'helper.md'), 'x'.repeat(7));
|
|
208
|
+
await fsp.mkdir(path.join(home, '.local', 'share', 'opencode'), { recursive: true });
|
|
209
|
+
await fsp.writeFile(path.join(home, '.local', 'share', 'opencode', 'auth.json'), '{}\n');
|
|
210
|
+
|
|
211
|
+
const stdout = execFileSync('sh', ['-c', buildRemoteSyncInventoryScript()], {
|
|
212
|
+
env: { ...process.env, HOME: home },
|
|
213
|
+
});
|
|
214
|
+
const line = stdout.toString().split(/\r?\n/).find((entry) => entry.startsWith('SYNC_INVENTORY='));
|
|
215
|
+
expect(line).toBeTruthy();
|
|
216
|
+
|
|
217
|
+
// This threw SyntaxError ("Unexpected token") before path values were
|
|
218
|
+
// escaped for the surrounding shell double-quoted string.
|
|
219
|
+
const inventory = JSON.parse(line.slice('SYNC_INVENTORY='.length));
|
|
220
|
+
const files = inventory.files.map((entry) => entry.path);
|
|
221
|
+
expect(files).toContain('opencode.jsonc');
|
|
222
|
+
expect(files).toContain('AGENTS.md');
|
|
223
|
+
const directories = inventory.directories.map((entry) => entry.path);
|
|
224
|
+
expect(directories).toContain('agents');
|
|
225
|
+
expect(inventory.authFile).toEqual({ bytes: 3 });
|
|
226
|
+
for (const entry of inventory.files) {
|
|
227
|
+
expect(typeof entry.path).toBe('string');
|
|
228
|
+
expect(typeof entry.bytes).toBe('number');
|
|
229
|
+
}
|
|
230
|
+
for (const entry of inventory.directories) {
|
|
231
|
+
expect(typeof entry.path).toBe('string');
|
|
232
|
+
expect(typeof entry.fileCount).toBe('number');
|
|
233
|
+
expect(typeof entry.bytes).toBe('number');
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('still parses when nothing exists on the remote', async () => {
|
|
238
|
+
const home = await makeTempDir();
|
|
239
|
+
const stdout = execFileSync('sh', ['-c', buildRemoteSyncInventoryScript()], {
|
|
240
|
+
env: { ...process.env, HOME: home },
|
|
241
|
+
});
|
|
242
|
+
const line = stdout.toString().split(/\r?\n/).find((entry) => entry.startsWith('SYNC_INVENTORY='));
|
|
243
|
+
const inventory = JSON.parse(line.slice('SYNC_INVENTORY='.length));
|
|
244
|
+
expect(inventory.files).toEqual([]);
|
|
245
|
+
expect(inventory.directories).toEqual([]);
|
|
246
|
+
expect(inventory.agentsRoot).toBeNull();
|
|
247
|
+
expect(inventory.authFile).toBeNull();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -12,6 +12,14 @@ import {
|
|
|
12
12
|
|
|
13
13
|
const shellQuote = (value) => `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* JSON.stringify yields bare double quotes, which terminate the surrounding
|
|
17
|
+
* shell double-quoted string in inventory lines. Escape them so the assembled
|
|
18
|
+
* SYNC_INVENTORY payload stays valid JSON after sh evaluation.
|
|
19
|
+
* @param {string} value
|
|
20
|
+
*/
|
|
21
|
+
const jsonShellEscape = (value) => JSON.stringify(value).replace(/"/g, '\\"');
|
|
22
|
+
|
|
15
23
|
/**
|
|
16
24
|
* Sanitize syncRunId for use as a single path segment under backup roots.
|
|
17
25
|
* @param {string} syncRunId
|
|
@@ -202,7 +210,7 @@ export const buildRemoteSyncInventoryScript = () => {
|
|
|
202
210
|
const quoted = shellQuote(rel);
|
|
203
211
|
lines.push(`if [ -f "$CFG"/${quoted} ]; then`);
|
|
204
212
|
lines.push(` BYTES="$(wc -c < "$CFG"/${quoted} | tr -d ' ')"`);
|
|
205
|
-
lines.push(` FILES="$FILES{\\"path\\":${
|
|
213
|
+
lines.push(` FILES="$FILES{\\"path\\":${jsonShellEscape(rel)},\\"bytes\\":$BYTES},"`);
|
|
206
214
|
lines.push('fi');
|
|
207
215
|
}
|
|
208
216
|
for (const rel of dirPaths) {
|
|
@@ -210,7 +218,7 @@ export const buildRemoteSyncInventoryScript = () => {
|
|
|
210
218
|
lines.push(`if [ -d "$CFG"/${quoted} ]; then`);
|
|
211
219
|
lines.push(` COUNT="$(find "$CFG"/${quoted} -type f ! -path '*/node_modules/*' ! -name '*.backup' 2>/dev/null | wc -l | tr -d ' ')"`);
|
|
212
220
|
lines.push(` BYTES="$(find "$CFG"/${quoted} -type f ! -path '*/node_modules/*' ! -name '*.backup' -exec wc -c {} + 2>/dev/null | awk 'END{print $1+0}')"`);
|
|
213
|
-
lines.push(` DIRS="$DIRS{\\"path\\":${
|
|
221
|
+
lines.push(` DIRS="$DIRS{\\"path\\":${jsonShellEscape(rel)},\\"fileCount\\":$COUNT,\\"bytes\\":$BYTES},"`);
|
|
214
222
|
lines.push('fi');
|
|
215
223
|
}
|
|
216
224
|
lines.push('AGENTS="null"');
|
|
@@ -77,9 +77,10 @@ This module provides notification message preparation utilities for the web serv
|
|
|
77
77
|
### Emitter runtime API (emitter-runtime.js)
|
|
78
78
|
- `createNotificationEmitterRuntime(dependencies)`: creates runtime for unified notification emission channels.
|
|
79
79
|
- Returned API:
|
|
80
|
-
- `writeSseEvent(res, payload)`
|
|
80
|
+
- `writeSseEvent(res, payload)` — best-effort SSE write with backpressure: when `res.write` returns `false`, the client is marked paused and further writes are skipped until `drain`; buffered bytes above `SSE_CLIENT_MAX_BUFFERED_BYTES` (2 MiB, aligned with relay-server) destroy the response. Authoritative data remains HTTP pull.
|
|
81
81
|
- `emitDesktopNotification(payload)`
|
|
82
82
|
- `broadcastUiNotification(payload)`
|
|
83
|
+
- Exported constant: `SSE_CLIENT_MAX_BUFFERED_BYTES`
|
|
83
84
|
|
|
84
85
|
### Template runtime API (template-runtime.js)
|
|
85
86
|
- `createNotificationTemplateRuntime(dependencies)`: creates shared notification/template runtime. Model-backed summarization was retired after the Zen provider became unavailable.
|
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
// Align with relay-server per-connection queue cap (2 MiB).
|
|
2
|
+
export const SSE_CLIENT_MAX_BUFFERED_BYTES = 2 * 1024 * 1024;
|
|
3
|
+
|
|
4
|
+
const isSseResponseClosed = (res) => Boolean(res?.writableEnded || res?.destroyed);
|
|
5
|
+
|
|
6
|
+
const getSseBufferedBytes = (res) => {
|
|
7
|
+
if (typeof res?.writableLength === 'number' && Number.isFinite(res.writableLength)) {
|
|
8
|
+
return res.writableLength;
|
|
9
|
+
}
|
|
10
|
+
const tracked = res?.__sseBufferedBytes;
|
|
11
|
+
return typeof tracked === 'number' && Number.isFinite(tracked) ? tracked : 0;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const attachSseDrainListener = (res) => {
|
|
15
|
+
if (res.__sseDrainAttached) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
res.__sseDrainAttached = true;
|
|
19
|
+
res.once('drain', () => {
|
|
20
|
+
res.__ssePaused = false;
|
|
21
|
+
res.__sseBufferedBytes = 0;
|
|
22
|
+
res.__sseDrainAttached = false;
|
|
23
|
+
res.__ssePausedHeartbeatCycles = 0;
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Intentional resource protection for slow/zombie SSE clients.
|
|
28
|
+
// SSE is best-effort; authoritative data is fetched over HTTP.
|
|
29
|
+
const destroySseClientForBackpressure = (res) => {
|
|
30
|
+
try {
|
|
31
|
+
if (!res.destroyed) {
|
|
32
|
+
res.destroy();
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// ignore — cleanup must stay idempotent
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
1
39
|
export const createNotificationEmitterRuntime = (dependencies) => {
|
|
2
40
|
const {
|
|
3
41
|
process,
|
|
@@ -23,7 +61,46 @@ export const createNotificationEmitterRuntime = (dependencies) => {
|
|
|
23
61
|
};
|
|
24
62
|
|
|
25
63
|
const writeSseEvent = (res, payload) => {
|
|
26
|
-
res
|
|
64
|
+
if (!res || isSseResponseClosed(res)) {
|
|
65
|
+
throw new Error('SSE response closed');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Drop while backpressured — SSE is best-effort; authoritative data is HTTP pull.
|
|
69
|
+
if (res.__ssePaused) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const data = `data: ${JSON.stringify(payload)}\n\n`;
|
|
74
|
+
const byteLength = Buffer.byteLength(data);
|
|
75
|
+
|
|
76
|
+
let ok;
|
|
77
|
+
try {
|
|
78
|
+
ok = res.write(data);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
destroySseClientForBackpressure(res);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const bufferedBytes = ok === false
|
|
85
|
+
? Math.max(getSseBufferedBytes(res), (res.__sseBufferedBytes || 0) + byteLength)
|
|
86
|
+
: getSseBufferedBytes(res);
|
|
87
|
+
res.__sseBufferedBytes = bufferedBytes;
|
|
88
|
+
|
|
89
|
+
if (bufferedBytes > SSE_CLIENT_MAX_BUFFERED_BYTES) {
|
|
90
|
+
// Intentional resource protection: a slow client must not unbounded-buffer
|
|
91
|
+
// response body in host memory. SSE is best-effort; authoritative data is
|
|
92
|
+
// fetched over HTTP.
|
|
93
|
+
destroySseClientForBackpressure(res);
|
|
94
|
+
throw new Error('SSE client buffer exceeded');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (ok === false) {
|
|
98
|
+
res.__ssePaused = true;
|
|
99
|
+
attachSseDrainListener(res);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return true;
|
|
27
104
|
};
|
|
28
105
|
|
|
29
106
|
const emitDesktopNotification = (payload) => {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createNotificationEmitterRuntime,
|
|
5
|
+
SSE_CLIENT_MAX_BUFFERED_BYTES,
|
|
6
|
+
} from './emitter-runtime.js';
|
|
4
7
|
|
|
5
8
|
const createRuntime = (overrides = {}) => createNotificationEmitterRuntime({
|
|
6
9
|
process: { stdout: { write: vi.fn() } },
|
|
@@ -11,6 +14,59 @@ const createRuntime = (overrides = {}) => createNotificationEmitterRuntime({
|
|
|
11
14
|
...overrides,
|
|
12
15
|
});
|
|
13
16
|
|
|
17
|
+
const createMockSseResponse = ({ writeImpl } = {}) => {
|
|
18
|
+
const listeners = new Map();
|
|
19
|
+
let destroyed = false;
|
|
20
|
+
let writableEnded = false;
|
|
21
|
+
let bufferedBytes = 0;
|
|
22
|
+
|
|
23
|
+
const res = {
|
|
24
|
+
get destroyed() {
|
|
25
|
+
return destroyed;
|
|
26
|
+
},
|
|
27
|
+
get writableEnded() {
|
|
28
|
+
return writableEnded;
|
|
29
|
+
},
|
|
30
|
+
get writableLength() {
|
|
31
|
+
return bufferedBytes;
|
|
32
|
+
},
|
|
33
|
+
on(event, handler) {
|
|
34
|
+
listeners.set(event, handler);
|
|
35
|
+
return this;
|
|
36
|
+
},
|
|
37
|
+
once(event, handler) {
|
|
38
|
+
listeners.set(event, handler);
|
|
39
|
+
return this;
|
|
40
|
+
},
|
|
41
|
+
emit(event) {
|
|
42
|
+
const handler = listeners.get(event);
|
|
43
|
+
listeners.delete(event);
|
|
44
|
+
if (typeof handler === 'function') {
|
|
45
|
+
handler();
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
write(chunk) {
|
|
49
|
+
if (typeof writeImpl === 'function') {
|
|
50
|
+
return writeImpl(chunk, {
|
|
51
|
+
addBuffered(bytes) {
|
|
52
|
+
bufferedBytes += bytes;
|
|
53
|
+
},
|
|
54
|
+
getBuffered() {
|
|
55
|
+
return bufferedBytes;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
},
|
|
61
|
+
destroy() {
|
|
62
|
+
destroyed = true;
|
|
63
|
+
this.emit('error');
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return res;
|
|
68
|
+
};
|
|
69
|
+
|
|
14
70
|
describe('notification emitter runtime', () => {
|
|
15
71
|
it('reports desktop delivery through the injected native callback', () => {
|
|
16
72
|
const onDesktopNotification = vi.fn();
|
|
@@ -44,4 +100,41 @@ describe('notification emitter runtime', () => {
|
|
|
44
100
|
},
|
|
45
101
|
});
|
|
46
102
|
});
|
|
103
|
+
|
|
104
|
+
it('pauses slow SSE clients when write returns false and skips further buffering until drain', () => {
|
|
105
|
+
const runtime = createRuntime();
|
|
106
|
+
let writeCount = 0;
|
|
107
|
+
const res = createMockSseResponse({
|
|
108
|
+
writeImpl(chunk, { addBuffered }) {
|
|
109
|
+
writeCount += 1;
|
|
110
|
+
addBuffered(Buffer.byteLength(String(chunk)));
|
|
111
|
+
return false;
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(runtime.writeSseEvent(res, { type: 'a' })).toBe(false);
|
|
116
|
+
expect(res.__ssePaused).toBe(true);
|
|
117
|
+
expect(writeCount).toBe(1);
|
|
118
|
+
|
|
119
|
+
expect(runtime.writeSseEvent(res, { type: 'b' })).toBe(false);
|
|
120
|
+
expect(writeCount).toBe(1);
|
|
121
|
+
|
|
122
|
+
res.emit('drain');
|
|
123
|
+
expect(res.__ssePaused).toBe(false);
|
|
124
|
+
expect(runtime.writeSseEvent(res, { type: 'c' })).toBe(false);
|
|
125
|
+
expect(writeCount).toBe(2);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('destroys an SSE client whose buffered bytes exceed the per-client cap', () => {
|
|
129
|
+
const runtime = createRuntime();
|
|
130
|
+
const res = createMockSseResponse({
|
|
131
|
+
writeImpl(chunk, { addBuffered }) {
|
|
132
|
+
addBuffered(SSE_CLIENT_MAX_BUFFERED_BYTES + Buffer.byteLength(String(chunk)));
|
|
133
|
+
return false;
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(() => runtime.writeSseEvent(res, { type: 'overflow' })).toThrow(/buffer exceeded/);
|
|
138
|
+
expect(res.destroyed).toBe(true);
|
|
139
|
+
});
|
|
47
140
|
});
|
|
@@ -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
|
|
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
|
|
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.
|