@openchambery/web 1.17.1-beta.7 → 1.17.1-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +3 -39
  2. package/bin/cli-output.js +0 -16
  3. package/bin/cli.js +3 -66
  4. package/bin/cli.test.js +4 -81
  5. package/bin/lib/DOCUMENTATION.md +5 -19
  6. package/bin/lib/cli-args.js +3 -327
  7. package/bin/lib/cli-http.js +0 -16
  8. package/bin/lib/cli-lifecycle.js +2 -111
  9. package/bin/lib/cli-paths.js +0 -57
  10. package/dist/assets/{AgentAvatar-htFrIvQx.js → AgentAvatar-C6Hxu5_o.js} +1 -1
  11. package/dist/assets/{AssistantView-BZiJsNuI.js → AssistantView-BKg8KK5a.js} +1 -1
  12. package/dist/assets/{ChatContainer-CdmcMnL3.js → ChatContainer-CuxynsBz.js} +5 -5
  13. package/dist/assets/{DiagramView-B7sq74V-.js → DiagramView-CjMblW4W.js} +1 -1
  14. package/dist/assets/{FadeInOnReveal-8fY7XL23.js → FadeInOnReveal-CvNA_aGD.js} +1 -1
  15. package/dist/assets/{Icon-B7eLbnyj.js → Icon-BPJ_quEe.js} +1 -1
  16. package/dist/assets/{MarkdownRendererImpl-DICju9ve.js → MarkdownRendererImpl--oy9PEaq.js} +1 -1
  17. package/dist/assets/{MobileDetailNavigation-CuxWU1yf.js → MobileDetailNavigation-j5XTy0wI.js} +1 -1
  18. package/dist/assets/{MobileShareBridge-CPg9Eyro.js → MobileShareBridge-BnF86wui.js} +1 -1
  19. package/dist/assets/{MobileSurface-D7sDH_tx.js → MobileSurface-D0LTFt1w.js} +1 -1
  20. package/dist/assets/{MultiRunWindow-MSO0Equt.js → MultiRunWindow-BTTFYUzI.js} +1 -1
  21. package/dist/assets/{OnboardingScreen-C87u_MLg.js → OnboardingScreen-CdvaEBtt.js} +1 -1
  22. package/dist/assets/{OverlayScrollbar-Kmmybw-z.js → OverlayScrollbar-DyP73J3X.js} +1 -1
  23. package/dist/assets/SettingsView-JQxnmG9X.js +25 -0
  24. package/dist/assets/{SettingsWindow-Bl25cfXX.js → SettingsWindow-Ddr1B6Hc.js} +1 -1
  25. package/dist/assets/{TerminalView-jR5jP1b0.js → TerminalView-bHOuUIxH.js} +1 -1
  26. package/dist/assets/{ToolOutputDialog-DCEZb2DZ.js → ToolOutputDialog-CbGv7HQ5.js} +1 -1
  27. package/dist/assets/{appThemeRegistry-aiRF-s3c.js → appThemeRegistry-DIDGDdQq.js} +2 -2
  28. package/dist/assets/{button-bYWkDtDI.js → button-BmNJBT13.js} +1 -1
  29. package/dist/assets/{clipboard-CzpzN4Um.js → clipboard-CXWbHCAk.js} +1 -1
  30. package/dist/assets/{collapsible-A1PFvMRp.js → collapsible-68IqWBM1.js} +1 -1
  31. package/dist/assets/{context-menu-CkpHn-Bm.js → context-menu-B-8pAqjS.js} +1 -1
  32. package/dist/assets/desktopHosts-BpjUSDz4.js +1 -0
  33. package/dist/assets/{desktopRelayRestore-C5vYnSTd.js → desktopRelayRestore-BzUEk1i7.js} +1 -1
  34. package/dist/assets/{diagram-BkwKWaTO.js → diagram-YN0GZ1Q0.js} +1 -1
  35. package/dist/assets/{dialog-DW7-_iqo.js → dialog-BV1ENlLf.js} +1 -1
  36. package/dist/assets/es-EXZUX1sB.js +15 -0
  37. package/dist/assets/{fileQueries-P9SX4YwC.js → fileQueries-i4oziMnC.js} +1 -1
  38. package/dist/assets/fr-jWNPO91J.js +13 -0
  39. package/dist/assets/{gitApi-CMsZ2v08.js → gitApi-BuTpcy5P.js} +1 -1
  40. package/dist/assets/{gitApiHttp-DvIIcDAO.js → gitApiHttp-C1DaEMKR.js} +1 -1
  41. package/dist/assets/{input-B9aNttqV.js → input-BLVhPvhN.js} +1 -1
  42. package/dist/assets/{insertionBoundaries-Di3rCXgN.js → insertionBoundaries-D8FUCksc.js} +1 -1
  43. package/dist/assets/ja-Dcb0VtDE.js +15 -0
  44. package/dist/assets/ko-CX71xNDX.js +15 -0
  45. package/dist/assets/{main-DY27kMV5.js → main-Bz2igSmw.js} +2 -2
  46. package/dist/assets/{main-Chtf300b.js → main-C3JiVxSp.js} +5 -5
  47. package/dist/assets/{markdown-worker-BoB1_DPo.js → markdown-worker-Cq3Twp1A.js} +1 -1
  48. package/dist/assets/{markdownTheme-om5XcPqq.js → markdownTheme-CCnvI6L0.js} +1 -1
  49. package/dist/assets/{miniChat-BWbpl5SN.js → miniChat-6EMGKhlG.js} +2 -2
  50. package/dist/assets/mobile-DP5GfqmN.js +2 -0
  51. package/dist/assets/{multirun-BfPcVktg.js → multirun-9f6WZ3Jd.js} +1 -1
  52. package/dist/assets/persistence-C_KCiMcY.js +3 -0
  53. package/dist/assets/{pl-BSjLn4dm.js → pl-mR_3Sqr8.js} +2 -2
  54. package/dist/assets/{projectMeta-CiAR7MRG.js → projectMeta-Dsa2wr8n.js} +1 -1
  55. package/dist/assets/pt-BR-D1RpVOvC.js +15 -0
  56. package/dist/assets/radio-CrWrJ2b-.js +1 -0
  57. package/dist/assets/{renderElectronMiniChatApp-CiEXAVSt.js → renderElectronMiniChatApp-4adD-9dD.js} +2 -2
  58. package/dist/assets/{renderMobileApp-BHvgSVPk.js → renderMobileApp-bSjLTDsy.js} +3 -3
  59. package/dist/assets/{runtimeConfig-49YbZaqI.js → runtimeConfig-JSaYFA5x.js} +1 -1
  60. package/dist/assets/{runtimeEndpointReset-CdhtjuPe.js → runtimeEndpointReset-DW-MKG59.js} +1 -1
  61. package/dist/assets/{sortable-tabs-strip-XeYica3j.js → sortable-tabs-strip-NTPx-T_l.js} +1 -1
  62. package/dist/assets/src-B8mzksNL.css +2 -0
  63. package/dist/assets/store-oojlrF4d.js +15 -0
  64. package/dist/assets/{terminalApi-DAu7xKli.js → terminalApi-BYTA045F.js} +1 -1
  65. package/dist/assets/{tooltip-BSlxaNrR.js → tooltip-BYGBBTc2.js} +1 -1
  66. package/dist/assets/uk-UvnVqWv-.js +15 -0
  67. package/dist/assets/{url-DjkTDrad.js → url-CGHw6D-E.js} +1 -1
  68. package/dist/assets/useAppFontEffects-BIO3lPVM.js +7 -0
  69. package/dist/assets/{useDesktopWindowControlsLayout-BwYnhrEy.js → useDesktopWindowControlsLayout-Df2h_cM_.js} +1 -1
  70. package/dist/assets/{useEffectiveDirectory-CEmsrrEs.js → useEffectiveDirectory-D0vaM832.js} +1 -1
  71. package/dist/assets/{useFontPreferences-BxMT_kty.js → useFontPreferences-Bu4ME5hk.js} +1 -1
  72. package/dist/assets/useMobileNavigationStore-7HNp5YHY.js +475 -0
  73. package/dist/assets/useSessionAutoCleanup-BPNLtDRJ.js +1 -0
  74. package/dist/assets/{useWorkerHighlightedLines-CFW2AoPL.js → useWorkerHighlightedLines-DYfBlGb-.js} +1 -1
  75. package/dist/assets/{utils-RUACCsOs.js → utils-DyUyiLje.js} +1 -1
  76. package/dist/assets/zh-CN-9v-RC966.js +15 -0
  77. package/dist/assets/zh-TW-BZ_wZdC-.js +15 -0
  78. package/dist/index.html +8 -8
  79. package/dist/mini-chat.html +8 -8
  80. package/dist/mobile.html +8 -8
  81. package/package.json +1 -1
  82. package/server/index.d.ts +0 -6
  83. package/server/index.js +5 -153
  84. package/server/lib/opencode/DOCUMENTATION.md +6 -10
  85. package/server/lib/opencode/cli-entry-runtime.js +0 -10
  86. package/server/lib/opencode/cli-options.js +0 -65
  87. package/server/lib/opencode/core-routes.js +2 -4
  88. package/server/lib/opencode/routes.test.js +0 -1
  89. package/server/lib/opencode/server-startup-runtime.js +0 -55
  90. package/server/lib/opencode/settings-helpers.js +0 -55
  91. package/server/lib/opencode/settings-helpers.test.js +0 -20
  92. package/server/lib/opencode/settings-normalization-runtime.js +0 -87
  93. package/server/lib/opencode/settings-normalization-runtime.test.js +0 -3
  94. package/server/lib/opencode/settings-runtime.js +9 -102
  95. package/server/lib/opencode/settings-runtime.test.js +0 -10
  96. package/server/lib/opencode/shutdown-runtime.js +1 -9
  97. package/server/lib/opencode/shutdown-runtime.test.js +0 -2
  98. package/server/lib/opencode/startup-pipeline-runtime.js +0 -20
  99. package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -3
  100. package/server/lib/opencode/tunnel-wiring-runtime.js +2 -80
  101. package/server/lib/relay/DOCUMENTATION.md +1 -1
  102. package/bin/lib/cli-tunnel-capabilities.js +0 -9
  103. package/bin/lib/cli-tunnel-profiles.js +0 -366
  104. package/bin/lib/cli-tunnel-utils.js +0 -300
  105. package/bin/lib/commands-tunnel.js +0 -1761
  106. package/dist/assets/SettingsView-5YNu54KI.js +0 -25
  107. package/dist/assets/desktopHosts-D5m_ggY1.js +0 -1
  108. package/dist/assets/es-Cei_75Jt.js +0 -15
  109. package/dist/assets/fr-Dn7WSHh2.js +0 -13
  110. package/dist/assets/ja-DFARVqVj.js +0 -15
  111. package/dist/assets/ko-pyLnIyDY.js +0 -15
  112. package/dist/assets/mobile-DEQeGskq.js +0 -2
  113. package/dist/assets/persistence-CHA81Od7.js +0 -3
  114. package/dist/assets/pt-BR-DaWvr_0A.js +0 -15
  115. package/dist/assets/radio-BnF4XhCC.js +0 -1
  116. package/dist/assets/src-BtkWqMFu.css +0 -2
  117. package/dist/assets/store-DIpqFVjs.js +0 -15
  118. package/dist/assets/uk-CHCRqRKf.js +0 -15
  119. package/dist/assets/useAppFontEffects-BRNICZS2.js +0 -7
  120. package/dist/assets/useMobileNavigationStore-DryW-QdG.js +0 -475
  121. package/dist/assets/useSessionAutoCleanup-DMJ1LiW_.js +0 -1
  122. package/dist/assets/zh-CN-BWEQZKXk.js +0 -15
  123. package/dist/assets/zh-TW-BbO-gJq3.js +0 -15
  124. package/server/lib/cloudflare-tunnel.js +0 -618
  125. package/server/lib/ngrok-tunnel.js +0 -343
  126. package/server/lib/ngrok-tunnel.test.js +0 -50
  127. package/server/lib/tunnels/DOCUMENTATION.md +0 -21
  128. package/server/lib/tunnels/executable-search.js +0 -140
  129. package/server/lib/tunnels/executable-search.test.js +0 -96
  130. package/server/lib/tunnels/index.js +0 -179
  131. package/server/lib/tunnels/index.test.js +0 -93
  132. package/server/lib/tunnels/install-help.js +0 -56
  133. package/server/lib/tunnels/install-help.test.js +0 -31
  134. package/server/lib/tunnels/managed-config.js +0 -201
  135. package/server/lib/tunnels/providers/cloudflare.js +0 -266
  136. package/server/lib/tunnels/providers/ngrok.js +0 -123
  137. package/server/lib/tunnels/registry.js +0 -51
  138. package/server/lib/tunnels/routes.js +0 -610
  139. package/server/lib/tunnels/types.js +0 -245
  140. package/server/lib/tunnels/types.test.js +0 -34
@@ -1,610 +0,0 @@
1
- export const createTunnelRoutesRuntime = (dependencies) => {
2
- const {
3
- crypto,
4
- URL,
5
- tunnelService,
6
- tunnelProviderRegistry,
7
- tunnelAuthController,
8
- readSettingsFromDiskMigrated,
9
- readManagedRemoteTunnelConfigFromDisk,
10
- normalizeTunnelProvider,
11
- normalizeTunnelMode,
12
- normalizeOptionalPath,
13
- normalizeManagedRemoteTunnelHostname,
14
- normalizeTunnelBootstrapTtlMs,
15
- normalizeTunnelSessionTtlMs,
16
- isSupportedTunnelMode,
17
- upsertManagedRemoteTunnelToken,
18
- resolveManagedRemoteTunnelToken,
19
- TUNNEL_MODE_QUICK,
20
- TUNNEL_MODE_MANAGED_LOCAL,
21
- TUNNEL_MODE_MANAGED_REMOTE,
22
- TUNNEL_PROVIDER_CLOUDFLARE,
23
- TunnelServiceError,
24
- getActivePort,
25
- getRuntimeManagedRemoteTunnelHostname,
26
- setRuntimeManagedRemoteTunnelHostname,
27
- getRuntimeManagedRemoteTunnelToken,
28
- setRuntimeManagedRemoteTunnelToken,
29
- getActiveTunnelController,
30
- setActiveTunnelController,
31
- } = dependencies;
32
-
33
- const resolveActiveNormalizedTunnelMode = () => {
34
- const mode = tunnelService.resolveActiveMode();
35
- if (mode === TUNNEL_MODE_MANAGED_LOCAL) {
36
- return TUNNEL_MODE_MANAGED_LOCAL;
37
- }
38
- if (mode === TUNNEL_MODE_MANAGED_REMOTE) {
39
- return TUNNEL_MODE_MANAGED_REMOTE;
40
- }
41
- return TUNNEL_MODE_QUICK;
42
- };
43
-
44
- const resolveNormalizedTunnelHost = (publicUrl) => {
45
- if (typeof publicUrl !== 'string' || publicUrl.trim().length === 0) {
46
- return null;
47
- }
48
- try {
49
- return new URL(publicUrl).hostname.toLowerCase();
50
- } catch {
51
- return null;
52
- }
53
- };
54
-
55
- const resolvePreferredTunnelProvider = async (reqBody = null) => {
56
- if (typeof reqBody?.provider === 'string' && reqBody.provider.trim().length > 0) {
57
- return normalizeTunnelProvider(reqBody.provider);
58
- }
59
- const activeProvider = tunnelService.resolveActiveProvider();
60
- if (activeProvider) {
61
- return normalizeTunnelProvider(activeProvider);
62
- }
63
- const settings = await readSettingsFromDiskMigrated();
64
- return normalizeTunnelProvider(settings?.tunnelProvider);
65
- };
66
-
67
- const startTunnelWithNormalizedRequest = async ({
68
- provider,
69
- mode,
70
- intent,
71
- hostname,
72
- token,
73
- configPath,
74
- selectedPresetId,
75
- selectedPresetName,
76
- }) => {
77
- if (provider === TUNNEL_PROVIDER_CLOUDFLARE && mode === TUNNEL_MODE_MANAGED_REMOTE) {
78
- setRuntimeManagedRemoteTunnelHostname(hostname);
79
- setRuntimeManagedRemoteTunnelToken(token);
80
-
81
- if (token && hostname) {
82
- await upsertManagedRemoteTunnelToken({
83
- id: selectedPresetId || hostname,
84
- name: selectedPresetName || hostname,
85
- hostname,
86
- token,
87
- });
88
- }
89
- }
90
-
91
- const result = await tunnelService.start({
92
- provider,
93
- mode,
94
- intent,
95
- configPath,
96
- token,
97
- hostname,
98
- });
99
-
100
- console.log(`Tunnel active (${result.provider}): ${result.publicUrl}`);
101
- return {
102
- publicUrl: result.publicUrl,
103
- mode: result.activeMode,
104
- provider: result.provider,
105
- providerMetadata: result.providerMetadata,
106
- };
107
- };
108
-
109
- const createGenericModeChecks = ({ modeKey, requiredFields, doctorRequest, startupReady }) => {
110
- const checks = [
111
- {
112
- id: 'startup_readiness',
113
- label: 'Provider startup readiness',
114
- status: startupReady ? 'pass' : 'fail',
115
- detail: startupReady
116
- ? 'Provider dependency checks passed.'
117
- : 'Resolve provider checks before starting tunnels.',
118
- },
119
- ];
120
-
121
- for (const field of requiredFields) {
122
- const value = doctorRequest?.[field];
123
- const present = typeof value === 'string' ? value.trim().length > 0 : Boolean(value);
124
- checks.push({
125
- id: `requirement_${field}`,
126
- label: `Required: ${field}`,
127
- status: present ? 'pass' : 'fail',
128
- detail: present
129
- ? `${field} is configured.`
130
- : `${field} is required for ${modeKey}.`,
131
- });
132
- }
133
-
134
- const failures = checks.filter((entry) => entry.status === 'fail').length;
135
- const warnings = checks.filter((entry) => entry.status === 'warn').length;
136
- return {
137
- mode: modeKey,
138
- checks,
139
- summary: {
140
- ready: failures === 0,
141
- failures,
142
- warnings,
143
- },
144
- ready: failures === 0,
145
- blockers: checks
146
- .filter((entry) => entry.status === 'fail' && entry.id !== 'startup_readiness')
147
- .map((entry) => entry.detail || entry.label || entry.id),
148
- };
149
- };
150
-
151
- const runTunnelDoctor = async ({ providerId, modeFilter, doctorRequest }) => {
152
- const provider = tunnelProviderRegistry.get(providerId);
153
- if (!provider) {
154
- throw new TunnelServiceError('provider_unsupported', `Unsupported tunnel provider: ${providerId}`);
155
- }
156
-
157
- const capabilities = provider.capabilities || {};
158
- const modeKeys = Array.isArray(capabilities.modes)
159
- ? capabilities.modes.map((entry) => entry?.key).filter((key) => typeof key === 'string' && key.length > 0)
160
- : [];
161
-
162
- if (modeFilter && !modeKeys.includes(modeFilter)) {
163
- throw new TunnelServiceError('mode_unsupported', `Provider '${providerId}' does not support mode '${modeFilter}'`);
164
- }
165
-
166
- if (typeof provider.diagnose === 'function') {
167
- const diagnosed = await provider.diagnose({
168
- ...doctorRequest,
169
- mode: modeFilter || doctorRequest?.mode,
170
- }, {
171
- capabilities,
172
- });
173
- const providerChecks = Array.isArray(diagnosed?.providerChecks) ? diagnosed.providerChecks : [];
174
- const allModes = Array.isArray(diagnosed?.modes) ? diagnosed.modes : [];
175
- const modes = modeFilter ? allModes.filter((entry) => entry?.mode === modeFilter) : allModes;
176
- return {
177
- ok: true,
178
- provider: providerId,
179
- providerChecks,
180
- modes,
181
- };
182
- }
183
-
184
- const availability = await tunnelService.checkAvailability(providerId);
185
- const dependencyAvailable = Boolean(availability?.available);
186
- const providerChecks = [{
187
- id: 'dependency',
188
- label: 'Provider dependency',
189
- status: dependencyAvailable ? 'pass' : 'fail',
190
- detail: dependencyAvailable
191
- ? (availability?.version || 'available')
192
- : (availability?.message || 'Required provider dependency is unavailable.'),
193
- }];
194
-
195
- const targetModes = (Array.isArray(capabilities.modes) ? capabilities.modes : [])
196
- .filter((entry) => !modeFilter || entry?.key === modeFilter);
197
- const modes = targetModes.map((entry) => createGenericModeChecks({
198
- modeKey: entry.key,
199
- requiredFields: Array.isArray(entry?.requires) ? entry.requires : [],
200
- doctorRequest,
201
- startupReady: dependencyAvailable,
202
- }));
203
-
204
- return {
205
- ok: true,
206
- provider: providerId,
207
- providerChecks,
208
- modes,
209
- };
210
- };
211
-
212
- const registerRoutes = (app) => {
213
- app.get('/api/openchamber/tunnel/check', async (req, res) => {
214
- try {
215
- const requestedProvider = typeof req?.query?.provider === 'string' && req.query.provider.trim().length > 0
216
- ? normalizeTunnelProvider(req.query.provider)
217
- : await resolvePreferredTunnelProvider();
218
- const result = await tunnelService.checkAvailability(requestedProvider);
219
- res.json({
220
- available: result.available,
221
- provider: requestedProvider,
222
- version: result.version || null,
223
- dependency: result.dependency || null,
224
- installCommand: result.installCommand || null,
225
- installUrl: result.installUrl || null,
226
- platform: result.platform || process.platform,
227
- message: result.message || null,
228
- });
229
- } catch (error) {
230
- console.warn('Tunnel dependency check failed:', error);
231
- res.json({ available: false, provider: null, version: null, dependency: null, installCommand: null, installUrl: null, platform: process.platform, message: null });
232
- }
233
- });
234
-
235
- const handleTunnelDoctor = async (req, res) => {
236
- try {
237
- const params = req.query || {};
238
- const body = req.body || {};
239
-
240
- const providerId = typeof params.provider === 'string' && params.provider.trim().length > 0
241
- ? normalizeTunnelProvider(params.provider)
242
- : await resolvePreferredTunnelProvider();
243
- const modeFilter = typeof params.mode === 'string' && params.mode.trim().length > 0
244
- ? params.mode.trim().toLowerCase()
245
- : null;
246
-
247
- const settings = await readSettingsFromDiskMigrated();
248
- const selectedPresetId = typeof params.managedRemoteTunnelPresetId === 'string'
249
- ? params.managedRemoteTunnelPresetId.trim()
250
- : '';
251
- const requestConfigPath = normalizeOptionalPath(params.configPath)
252
- ?? normalizeOptionalPath(settings?.managedLocalTunnelConfigPath);
253
- const requestManagedRemoteHostname = normalizeManagedRemoteTunnelHostname(params.managedRemoteTunnelHostname);
254
- const requestTunnelHostname = normalizeManagedRemoteTunnelHostname(params.tunnelHostname);
255
- const requestHostname = normalizeManagedRemoteTunnelHostname(params.hostname);
256
- const hostnameFromSettings = normalizeManagedRemoteTunnelHostname(settings?.managedRemoteTunnelHostname);
257
- const hostname = requestHostname || requestTunnelHostname || requestManagedRemoteHostname || hostnameFromSettings;
258
-
259
- const requestManagedRemoteToken = typeof body.managedRemoteTunnelToken === 'string'
260
- ? body.managedRemoteTunnelToken.trim()
261
- : '';
262
- const requestTunnelToken = typeof body.tunnelToken === 'string'
263
- ? body.tunnelToken.trim()
264
- : '';
265
- const requestToken = typeof body.token === 'string'
266
- ? body.token.trim()
267
- : '';
268
- const requestTokenProvided = body.managedRemoteTunnelTokenProvided === true
269
- || body.tunnelTokenProvided === true
270
- || body.tokenProvided === true;
271
- const requestHostnameProvided = body.managedRemoteTunnelHostnameProvided === true
272
- || body.tunnelHostnameProvided === true
273
- || body.hostnameProvided === true;
274
- const storedManagedRemoteToken = typeof settings?.managedRemoteTunnelToken === 'string'
275
- ? settings.managedRemoteTunnelToken.trim()
276
- : '';
277
- const managedRemoteTunnelConfig = await readManagedRemoteTunnelConfigFromDisk();
278
- const serverHasSavedManagedRemoteProfile = managedRemoteTunnelConfig.tunnels.some((entry) => {
279
- const savedHostname = normalizeManagedRemoteTunnelHostname(entry?.hostname);
280
- const savedToken = typeof entry?.token === 'string' ? entry.token.trim() : '';
281
- return Boolean(savedHostname && savedToken);
282
- });
283
- const cliHasSavedManagedRemoteProfile = params.hasSavedManagedRemoteProfile === '1';
284
- const hasSavedManagedRemoteProfile = serverHasSavedManagedRemoteProfile || cliHasSavedManagedRemoteProfile;
285
- const configManagedRemoteToken = providerId === TUNNEL_PROVIDER_CLOUDFLARE
286
- ? await resolveManagedRemoteTunnelToken({ presetId: selectedPresetId, hostname })
287
- : '';
288
- const runtimeHostname = getRuntimeManagedRemoteTunnelHostname();
289
- const runtimeToken = getRuntimeManagedRemoteTunnelToken();
290
- const token = requestToken
291
- || requestTunnelToken
292
- || requestManagedRemoteToken
293
- || ((runtimeHostname && hostname && runtimeHostname === hostname) ? runtimeToken : '')
294
- || configManagedRemoteToken
295
- || storedManagedRemoteToken;
296
-
297
- const doctorRequest = {
298
- mode: modeFilter,
299
- hostname,
300
- token,
301
- tokenProvided: requestTokenProvided,
302
- hostnameProvided: requestHostnameProvided,
303
- configPath: requestConfigPath,
304
- hasSavedManagedRemoteProfile,
305
- };
306
-
307
- const result = await runTunnelDoctor({
308
- providerId,
309
- modeFilter,
310
- doctorRequest,
311
- });
312
- return res.json(result);
313
- } catch (error) {
314
- if (error instanceof TunnelServiceError) {
315
- return res.status(400).json({ ok: false, error: error.message, code: error.code });
316
- }
317
- console.warn('Tunnel doctor failed:', error);
318
- return res.status(500).json({ ok: false, error: 'Failed to run tunnel doctor' });
319
- }
320
- };
321
- app.post('/api/openchamber/tunnel/doctor', handleTunnelDoctor);
322
- app.get('/api/openchamber/tunnel/doctor', handleTunnelDoctor);
323
-
324
- app.get('/api/openchamber/tunnel/providers', (_req, res) => {
325
- const providers = tunnelProviderRegistry.listCapabilities();
326
- return res.json({ providers });
327
- });
328
-
329
- app.get('/api/openchamber/tunnel/status', async (_req, res) => {
330
- try {
331
- const settings = await readSettingsFromDiskMigrated();
332
- const normalizedMode = normalizeTunnelMode(settings?.tunnelMode);
333
- const managedRemoteHostname = normalizeManagedRemoteTunnelHostname(settings?.managedRemoteTunnelHostname);
334
- const managedRemoteTunnelConfig = await readManagedRemoteTunnelConfigFromDisk();
335
- const managedRemoteTunnelPresetSummaries = managedRemoteTunnelConfig.tunnels.map((entry) => ({
336
- id: entry.id,
337
- name: entry.name,
338
- hostname: entry.hostname,
339
- }));
340
- const hasStoredManagedRemoteToken = typeof settings?.managedRemoteTunnelToken === 'string' && settings.managedRemoteTunnelToken.trim().length > 0;
341
- const hasManagedRemoteTunnelToken = getRuntimeManagedRemoteTunnelToken().length > 0 || managedRemoteTunnelConfig.tunnels.length > 0 || hasStoredManagedRemoteToken;
342
- const bootstrapTtlMs = settings?.tunnelBootstrapTtlMs === null
343
- ? null
344
- : normalizeTunnelBootstrapTtlMs(settings?.tunnelBootstrapTtlMs);
345
- const sessionTtlMs = normalizeTunnelSessionTtlMs(settings?.tunnelSessionTtlMs);
346
- const activeSessions = tunnelAuthController.listTunnelSessions();
347
- const activeProvider = tunnelService.resolveActiveProvider();
348
- const provider = activeProvider || normalizeTunnelProvider(settings?.tunnelProvider);
349
-
350
- const publicUrl = tunnelService.getPublicUrl();
351
- if (!publicUrl) {
352
- return res.json({
353
- active: false,
354
- url: null,
355
- mode: normalizedMode,
356
- provider,
357
- providerMetadata: null,
358
- hasManagedRemoteTunnelToken,
359
- managedRemoteTunnelHostname: managedRemoteHostname || null,
360
- managedRemoteTunnelPresets: managedRemoteTunnelPresetSummaries,
361
- managedRemoteTunnelTokenPresetIds: managedRemoteTunnelConfig.tunnels.map((entry) => entry.id),
362
- hasBootstrapToken: false,
363
- bootstrapExpiresAt: null,
364
- policy: 'tunnel-gated',
365
- activeTunnelMode: tunnelAuthController.getActiveTunnelMode() || null,
366
- activeSessions,
367
- localPort: getActivePort(),
368
- ttlConfig: {
369
- bootstrapTtlMs,
370
- sessionTtlMs,
371
- },
372
- });
373
- }
374
-
375
- const activeNormalizedMode = resolveActiveNormalizedTunnelMode();
376
- const activeTunnelId = tunnelAuthController.getActiveTunnelId();
377
- const activeTunnelHost = tunnelAuthController.getActiveTunnelHost();
378
- const resolvedTunnelHost = resolveNormalizedTunnelHost(publicUrl);
379
- const activeTunnelMode = tunnelAuthController.getActiveTunnelMode();
380
- const needsActiveTunnelSync = !activeTunnelId
381
- || !activeTunnelHost
382
- || !resolvedTunnelHost
383
- || activeTunnelHost !== resolvedTunnelHost
384
- || activeTunnelMode !== activeNormalizedMode;
385
- if (needsActiveTunnelSync) {
386
- tunnelAuthController.setActiveTunnel({
387
- tunnelId: activeTunnelId || crypto.randomUUID(),
388
- publicUrl,
389
- mode: activeNormalizedMode,
390
- });
391
- }
392
-
393
- const bootstrapStatus = tunnelAuthController.getBootstrapStatus();
394
- const providerMetadata = tunnelService.getProviderMetadata();
395
-
396
- return res.json({
397
- active: true,
398
- url: publicUrl,
399
- mode: activeNormalizedMode,
400
- provider,
401
- providerMetadata,
402
- hasManagedRemoteTunnelToken,
403
- managedRemoteTunnelHostname: managedRemoteHostname || null,
404
- managedRemoteTunnelPresets: managedRemoteTunnelPresetSummaries,
405
- managedRemoteTunnelTokenPresetIds: managedRemoteTunnelConfig.tunnels.map((entry) => entry.id),
406
- hasBootstrapToken: bootstrapStatus.hasBootstrapToken,
407
- bootstrapExpiresAt: bootstrapStatus.bootstrapExpiresAt,
408
- policy: 'tunnel-gated',
409
- activeTunnelMode: activeNormalizedMode,
410
- activeSessions: tunnelAuthController.listTunnelSessions(),
411
- localPort: getActivePort(),
412
- ttlConfig: {
413
- bootstrapTtlMs,
414
- sessionTtlMs,
415
- },
416
- });
417
- } catch (error) {
418
- return res.status(500).json({ error: 'Failed to get tunnel status' });
419
- }
420
- });
421
-
422
- app.put('/api/openchamber/tunnel/managed-remote-token', async (req, res) => {
423
- try {
424
- const presetId = typeof req?.body?.presetId === 'string' ? req.body.presetId.trim() : '';
425
- const presetName = typeof req?.body?.presetName === 'string' ? req.body.presetName.trim() : '';
426
- const managedRemoteTunnelHostname = normalizeManagedRemoteTunnelHostname(req?.body?.managedRemoteTunnelHostname);
427
- const managedRemoteTunnelToken = typeof req?.body?.managedRemoteTunnelToken === 'string' ? req.body.managedRemoteTunnelToken.trim() : '';
428
-
429
- if (!presetId || !presetName || !managedRemoteTunnelHostname || !managedRemoteTunnelToken) {
430
- return res.status(400).json({ ok: false, error: 'presetId, presetName, managedRemoteTunnelHostname and managedRemoteTunnelToken are required' });
431
- }
432
-
433
- await upsertManagedRemoteTunnelToken({
434
- id: presetId,
435
- name: presetName,
436
- hostname: managedRemoteTunnelHostname,
437
- token: managedRemoteTunnelToken,
438
- });
439
-
440
- const managedRemoteTunnelConfig = await readManagedRemoteTunnelConfigFromDisk();
441
- return res.json({ ok: true, managedRemoteTunnelTokenPresetIds: managedRemoteTunnelConfig.tunnels.map((entry) => entry.id) });
442
- } catch (error) {
443
- return res.status(500).json({ ok: false, error: 'Failed to save managed remote tunnel token' });
444
- }
445
- });
446
-
447
- app.post('/api/openchamber/tunnel/start', async (_req, res) => {
448
- try {
449
- const settings = await readSettingsFromDiskMigrated();
450
- if (typeof _req?.body?.provider === 'string' && _req.body.provider.trim().length > 0) {
451
- const rawProvider = _req.body.provider.trim().toLowerCase();
452
- if (!tunnelProviderRegistry.get(rawProvider)) {
453
- return res.status(422).json({ ok: false, error: `Unsupported tunnel provider: ${rawProvider}`, code: 'provider_unsupported' });
454
- }
455
- }
456
- const provider = normalizeTunnelProvider(_req?.body?.provider ?? settings?.tunnelProvider);
457
- const modeInput = _req?.body?.mode ?? settings?.tunnelMode;
458
- const intent = typeof _req?.body?.intent === 'string' ? _req.body.intent.trim().toLowerCase() : undefined;
459
- const mode = typeof modeInput === 'string'
460
- ? modeInput.trim().toLowerCase()
461
- : normalizeTunnelMode(modeInput);
462
- if (typeof _req?.body?.mode === 'string' && _req.body.mode.trim().length > 0 && !isSupportedTunnelMode(mode)) {
463
- return res.status(422).json({ ok: false, error: `Unsupported tunnel mode: ${mode}`, code: 'mode_unsupported' });
464
- }
465
- const selectedPresetId = typeof _req?.body?.managedRemoteTunnelPresetId === 'string' ? _req.body.managedRemoteTunnelPresetId.trim() : '';
466
- const selectedPresetName = typeof _req?.body?.managedRemoteTunnelPresetName === 'string' ? _req.body.managedRemoteTunnelPresetName.trim() : '';
467
- const requestConfigPath = normalizeOptionalPath(_req?.body?.configPath)
468
- ?? normalizeOptionalPath(settings?.managedLocalTunnelConfigPath);
469
- const requestManagedRemoteHostname = normalizeManagedRemoteTunnelHostname(_req?.body?.managedRemoteTunnelHostname);
470
- const requestTunnelHostname = normalizeManagedRemoteTunnelHostname(_req?.body?.tunnelHostname);
471
- const requestHostname = normalizeManagedRemoteTunnelHostname(_req?.body?.hostname);
472
- const hostnameFromSettings = normalizeManagedRemoteTunnelHostname(settings?.managedRemoteTunnelHostname);
473
- const hostname = requestHostname || requestTunnelHostname || requestManagedRemoteHostname || hostnameFromSettings;
474
- const requestManagedRemoteToken = typeof _req?.body?.managedRemoteTunnelToken === 'string' ? _req.body.managedRemoteTunnelToken.trim() : '';
475
- const requestTunnelToken = typeof _req?.body?.tunnelToken === 'string' ? _req.body.tunnelToken.trim() : '';
476
- const requestToken = typeof _req?.body?.token === 'string' ? _req.body.token.trim() : '';
477
- const storedManagedRemoteToken = typeof settings?.managedRemoteTunnelToken === 'string' ? settings.managedRemoteTunnelToken.trim() : '';
478
- const configManagedRemoteToken = provider === TUNNEL_PROVIDER_CLOUDFLARE
479
- ? await resolveManagedRemoteTunnelToken({ presetId: selectedPresetId, hostname })
480
- : '';
481
- const runtimeHostname = getRuntimeManagedRemoteTunnelHostname();
482
- const runtimeToken = getRuntimeManagedRemoteTunnelToken();
483
- const token = requestToken
484
- || requestTunnelToken
485
- || requestManagedRemoteToken
486
- || ((runtimeHostname && hostname && runtimeHostname === hostname) ? runtimeToken : '')
487
- || configManagedRemoteToken
488
- || storedManagedRemoteToken;
489
- const requestConnectTtlMs = typeof _req?.body?.connectTtlMs === 'number' && Number.isFinite(_req.body.connectTtlMs)
490
- ? normalizeTunnelBootstrapTtlMs(_req.body.connectTtlMs)
491
- : undefined;
492
- const requestSessionTtlMs = typeof _req?.body?.sessionTtlMs === 'number' && Number.isFinite(_req.body.sessionTtlMs)
493
- ? normalizeTunnelSessionTtlMs(_req.body.sessionTtlMs)
494
- : undefined;
495
- const bootstrapTtlMs = requestConnectTtlMs ?? (settings?.tunnelBootstrapTtlMs === null
496
- ? null
497
- : normalizeTunnelBootstrapTtlMs(settings?.tunnelBootstrapTtlMs));
498
- const sessionTtlMs = requestSessionTtlMs ?? normalizeTunnelSessionTtlMs(settings?.tunnelSessionTtlMs);
499
-
500
- const previousTunnelId = tunnelAuthController.getActiveTunnelId();
501
- const previousMode = tunnelAuthController.getActiveTunnelMode();
502
- const previousProvider = tunnelService.resolveActiveProvider();
503
- const previousUrl = tunnelService.getPublicUrl();
504
-
505
- const { publicUrl, provider: activeProvider, providerMetadata } = await startTunnelWithNormalizedRequest({
506
- provider,
507
- mode,
508
- intent,
509
- hostname,
510
- token,
511
- configPath: requestConfigPath,
512
- selectedPresetId,
513
- selectedPresetName,
514
- });
515
-
516
- const replacedTunnel = Boolean(previousTunnelId) && (
517
- previousMode !== mode
518
- || previousProvider !== activeProvider
519
- || previousUrl !== publicUrl
520
- );
521
- let revokedBootstrapCount = 0;
522
- let invalidatedSessionCount = 0;
523
- if (replacedTunnel && previousTunnelId) {
524
- const revoked = tunnelAuthController.revokeTunnelArtifacts(previousTunnelId);
525
- revokedBootstrapCount = revoked.revokedBootstrapCount;
526
- invalidatedSessionCount = revoked.invalidatedSessionCount;
527
- }
528
-
529
- tunnelAuthController.setActiveTunnel({
530
- tunnelId: replacedTunnel || !previousTunnelId ? crypto.randomUUID() : previousTunnelId,
531
- publicUrl,
532
- mode,
533
- });
534
-
535
- const bootstrapToken = tunnelAuthController.issueBootstrapToken({ ttlMs: bootstrapTtlMs });
536
- const connectUrl = `${publicUrl.replace(/\/$/, '')}/connect?t=${encodeURIComponent(bootstrapToken.token)}`;
537
- const managedRemoteTunnelConfig = await readManagedRemoteTunnelConfigFromDisk();
538
- const isCloudflareProvider = activeProvider === TUNNEL_PROVIDER_CLOUDFLARE;
539
-
540
- return res.json({
541
- ok: true,
542
- url: publicUrl,
543
- mode,
544
- provider: activeProvider,
545
- providerMetadata,
546
- managedRemoteTunnelHostname: isCloudflareProvider ? (hostname || null) : null,
547
- managedRemoteTunnelTokenPresetIds: isCloudflareProvider ? managedRemoteTunnelConfig.tunnels.map((entry) => entry.id) : [],
548
- connectUrl,
549
- bootstrapExpiresAt: bootstrapToken.expiresAt,
550
- replacedTunnel,
551
- replaced: replacedTunnel
552
- ? {
553
- mode: previousMode,
554
- provider: previousProvider,
555
- url: previousUrl,
556
- }
557
- : null,
558
- revokedBootstrapCount,
559
- invalidatedSessionCount,
560
- policy: 'tunnel-gated',
561
- activeTunnelMode: mode,
562
- activeSessions: tunnelAuthController.listTunnelSessions(),
563
- localPort: getActivePort(),
564
- ttlConfig: {
565
- bootstrapTtlMs,
566
- sessionTtlMs,
567
- },
568
- });
569
- } catch (error) {
570
- console.error('Failed to start tunnel:', error);
571
- setActiveTunnelController(null);
572
- tunnelAuthController.clearActiveTunnel();
573
- if (error instanceof TunnelServiceError) {
574
- const status = error.code === 'missing_dependency'
575
- ? 400
576
- : (error.code === 'validation_error' || error.code === 'provider_unsupported' || error.code === 'mode_unsupported'
577
- ? 422
578
- : 500);
579
- return res.status(status).json({ ok: false, error: error.message, code: error.code });
580
- }
581
- return res.status(500).json({ ok: false, error: 'Failed to start tunnel', code: 'startup_failed' });
582
- }
583
- });
584
-
585
- app.post('/api/openchamber/tunnel/stop', (_req, res) => {
586
- let revokedBootstrapCount = 0;
587
- let invalidatedSessionCount = 0;
588
- const activeTunnelId = tunnelAuthController.getActiveTunnelId();
589
-
590
- if (activeTunnelId) {
591
- const revoked = tunnelAuthController.revokeTunnelArtifacts(activeTunnelId);
592
- revokedBootstrapCount = revoked.revokedBootstrapCount;
593
- invalidatedSessionCount = revoked.invalidatedSessionCount;
594
- }
595
-
596
- if (getActiveTunnelController()) {
597
- console.log('Stopping active tunnel (user requested)...');
598
- tunnelService.stop();
599
- }
600
-
601
- tunnelAuthController.clearActiveTunnel();
602
- res.json({ ok: true, revokedBootstrapCount, invalidatedSessionCount });
603
- });
604
- };
605
-
606
- return {
607
- registerRoutes,
608
- startTunnelWithNormalizedRequest,
609
- };
610
- };