@thevinci/web 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -0
- package/bin/cli-entry.js +55 -0
- package/bin/cli-output.js +145 -0
- package/bin/cli.js +4887 -0
- package/bin/cli.test.js +64 -0
- package/dist/apple-touch-icon-120x120.png +0 -0
- package/dist/apple-touch-icon-152x152.png +0 -0
- package/dist/apple-touch-icon-167x167.png +0 -0
- package/dist/apple-touch-icon-180x180.png +0 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/apple-touch-icon.svg +528 -0
- package/dist/assets/JsonTreeView-CSm9OzXG.js +1 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/MarkdownRendererImpl-DensKOLc.js +6 -0
- package/dist/assets/MultiRunWindow-Bo7THayo.js +1 -0
- package/dist/assets/OnboardingScreen-BDqmzTVR.js +2 -0
- package/dist/assets/SettingsWindow-coz__Ykw.js +1 -0
- package/dist/assets/TerminalView-DrZ-i3Dr.js +1 -0
- package/dist/assets/ToolOutputDialog-Eglzslt3.js +16 -0
- package/dist/assets/es-4o9ciP61.js +15 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/assets/index-DLTDToSP.css +1 -0
- package/dist/assets/index-DgiFEKGN.js +1 -0
- package/dist/assets/ko-B20imCHE.js +15 -0
- package/dist/assets/main-BV3KOtdA.css +1 -0
- package/dist/assets/main-CDKJj0sH.js +226 -0
- package/dist/assets/main-LC-PSNVM.js +2 -0
- package/dist/assets/miniChat-CQUiG_cr.js +2 -0
- package/dist/assets/modelPrefsAutoSave-Dm799vzR.js +6986 -0
- package/dist/assets/pl-DQJ7LSzj.js +15 -0
- package/dist/assets/pt-BR-OmjHUz9y.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-CARbeW0G.js +2 -0
- package/dist/assets/uk-BNFxOlO4.js +15 -0
- package/dist/assets/vendor--DBfsbEis.css +1 -0
- package/dist/assets/vendor-.bun-B9l0ZNi2.js +4094 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/wasmSttWorker-Dtlxac_K.js +1 -0
- package/dist/assets/wasmSttWorker-oo7Dm_jy.js +1806 -0
- package/dist/assets/worker-CbT6TVo7.js +155 -0
- package/dist/assets/zh-CN-C6T-Ac7F.js +15 -0
- package/dist/favicon-16.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +528 -0
- package/dist/index.html +607 -0
- package/dist/logo-dark-192x192.png +0 -0
- package/dist/logo-dark-512x512.png +0 -0
- package/dist/logo-dark-512x512.svg +528 -0
- package/dist/logo-light-192x192.png +0 -0
- package/dist/logo-light-512x512.png +0 -0
- package/dist/logo-light-512x512.svg +528 -0
- package/dist/mini-chat.html +16 -0
- package/dist/pwa-192.png +0 -0
- package/dist/pwa-512.png +0 -0
- package/dist/pwa-maskable-192.png +0 -0
- package/dist/pwa-maskable-512.png +0 -0
- package/dist/site.webmanifest +21 -0
- package/dist/sw.js +1 -0
- package/package.json +118 -0
- package/public/apple-touch-icon-120x120.png +0 -0
- package/public/apple-touch-icon-152x152.png +0 -0
- package/public/apple-touch-icon-167x167.png +0 -0
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/apple-touch-icon.svg +528 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +528 -0
- package/public/logo-dark-192x192.png +0 -0
- package/public/logo-dark-512x512.png +0 -0
- package/public/logo-dark-512x512.svg +528 -0
- package/public/logo-light-192x192.png +0 -0
- package/public/logo-light-512x512.png +0 -0
- package/public/logo-light-512x512.svg +528 -0
- package/public/pwa-192.png +0 -0
- package/public/pwa-512.png +0 -0
- package/public/pwa-maskable-192.png +0 -0
- package/public/pwa-maskable-512.png +0 -0
- package/public/site.webmanifest +21 -0
- package/server/TERMINAL_WS_PROTOCOL.md +48 -0
- package/server/index.d.ts +39 -0
- package/server/index.js +1311 -0
- package/server/lib/cloudflare-tunnel.js +650 -0
- package/server/lib/event-stream/DOCUMENTATION.md +61 -0
- package/server/lib/event-stream/directory-ws-bridge.js +185 -0
- package/server/lib/event-stream/global-hub.js +158 -0
- package/server/lib/event-stream/global-hub.test.js +140 -0
- package/server/lib/event-stream/global-ws-bridge.js +206 -0
- package/server/lib/event-stream/index.js +25 -0
- package/server/lib/event-stream/protocol.js +131 -0
- package/server/lib/event-stream/protocol.test.js +182 -0
- package/server/lib/event-stream/runtime.js +180 -0
- package/server/lib/event-stream/runtime.test.js +512 -0
- package/server/lib/event-stream/upstream-reader.js +226 -0
- package/server/lib/event-stream/upstream-reader.test.js +276 -0
- package/server/lib/fs/DOCUMENTATION.md +36 -0
- package/server/lib/fs/routes.js +1040 -0
- package/server/lib/fs/search.js +238 -0
- package/server/lib/git/DOCUMENTATION.md +152 -0
- package/server/lib/git/credentials.js +74 -0
- package/server/lib/git/identity-storage.js +112 -0
- package/server/lib/git/index.js +6 -0
- package/server/lib/git/routes.js +972 -0
- package/server/lib/git/service.js +3432 -0
- package/server/lib/git/service.test.js +39 -0
- package/server/lib/github/DOCUMENTATION.md +171 -0
- package/server/lib/github/auth.js +307 -0
- package/server/lib/github/device-flow.js +50 -0
- package/server/lib/github/index.js +24 -0
- package/server/lib/github/octokit.js +10 -0
- package/server/lib/github/pr-status.js +519 -0
- package/server/lib/github/repo/fork-detection.js +102 -0
- package/server/lib/github/repo/index.js +55 -0
- package/server/lib/github/routes.js +1560 -0
- package/server/lib/magic-prompts/routes.js +63 -0
- package/server/lib/magic-prompts/runtime.js +119 -0
- package/server/lib/notifications/DOCUMENTATION.md +122 -0
- package/server/lib/notifications/emitter-runtime.js +102 -0
- package/server/lib/notifications/index.js +4 -0
- package/server/lib/notifications/message.js +52 -0
- package/server/lib/notifications/message.test.js +34 -0
- package/server/lib/notifications/push-runtime.js +304 -0
- package/server/lib/notifications/routes.js +315 -0
- package/server/lib/notifications/runtime.js +566 -0
- package/server/lib/notifications/template-runtime.js +349 -0
- package/server/lib/notifications/template-runtime.test.js +26 -0
- package/server/lib/opencode/DOCUMENTATION.md +362 -0
- package/server/lib/opencode/agents.js +634 -0
- package/server/lib/opencode/auth-state-runtime.js +88 -0
- package/server/lib/opencode/auth.js +83 -0
- package/server/lib/opencode/bootstrap-runtime.js +131 -0
- package/server/lib/opencode/cli-entry-runtime.js +43 -0
- package/server/lib/opencode/cli-options.js +128 -0
- package/server/lib/opencode/commands.js +339 -0
- package/server/lib/opencode/config-entity-routes.js +370 -0
- package/server/lib/opencode/core-routes.js +500 -0
- package/server/lib/opencode/core-routes.test.js +26 -0
- package/server/lib/opencode/env-config.js +74 -0
- package/server/lib/opencode/env-keys.js +68 -0
- package/server/lib/opencode/env-runtime.js +1162 -0
- package/server/lib/opencode/env-runtime.test.js +116 -0
- package/server/lib/opencode/feature-routes-runtime.js +244 -0
- package/server/lib/opencode/hmr-state-runtime.js +85 -0
- package/server/lib/opencode/index.js +66 -0
- package/server/lib/opencode/lifecycle.js +1019 -0
- package/server/lib/opencode/lifecycle.test.js +240 -0
- package/server/lib/opencode/mcp.js +278 -0
- package/server/lib/opencode/network-runtime.js +104 -0
- package/server/lib/opencode/network-runtime.test.js +37 -0
- package/server/lib/opencode/opencode-resolution-runtime.js +71 -0
- package/server/lib/opencode/path-utils.js +100 -0
- package/server/lib/opencode/path-utils.test.js +71 -0
- package/server/lib/opencode/project-directory-runtime.js +124 -0
- package/server/lib/opencode/project-icon-routes.js +399 -0
- package/server/lib/opencode/project-icon-routes.test.js +107 -0
- package/server/lib/opencode/providers.js +96 -0
- package/server/lib/opencode/proxy.js +445 -0
- package/server/lib/opencode/pwa-manifest-routes.js +257 -0
- package/server/lib/opencode/pwa-manifest-routes.test.js +133 -0
- package/server/lib/opencode/routes.js +541 -0
- package/server/lib/opencode/server-startup-runtime.js +156 -0
- package/server/lib/opencode/server-utils-runtime.js +168 -0
- package/server/lib/opencode/server-utils-runtime.test.js +135 -0
- package/server/lib/opencode/session-runtime.js +356 -0
- package/server/lib/opencode/session-runtime.test.js +151 -0
- package/server/lib/opencode/settings-helpers.js +770 -0
- package/server/lib/opencode/settings-helpers.test.js +109 -0
- package/server/lib/opencode/settings-normalization-runtime.js +428 -0
- package/server/lib/opencode/settings-runtime.js +826 -0
- package/server/lib/opencode/settings-runtime.test.js +85 -0
- package/server/lib/opencode/shared.js +615 -0
- package/server/lib/opencode/shutdown-runtime.js +139 -0
- package/server/lib/opencode/shutdown-runtime.test.js +58 -0
- package/server/lib/opencode/skill-routes.js +701 -0
- package/server/lib/opencode/skills.js +548 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +130 -0
- package/server/lib/opencode/static-routes-runtime.js +65 -0
- package/server/lib/opencode/theme-runtime.js +167 -0
- package/server/lib/opencode/tunnel-auth.js +591 -0
- package/server/lib/opencode/tunnel-wiring-runtime.js +94 -0
- package/server/lib/opencode/vinci-routes.js +76 -0
- package/server/lib/opencode/watcher.js +115 -0
- package/server/lib/opencode/watcher.test.js +239 -0
- package/server/lib/preview/proxy-runtime.js +1333 -0
- package/server/lib/preview/proxy-runtime.test.js +144 -0
- package/server/lib/projects/project-config.js +567 -0
- package/server/lib/projects/project-config.test.js +175 -0
- package/server/lib/projects/project-id.js +13 -0
- package/server/lib/quota/DOCUMENTATION.md +58 -0
- package/server/lib/quota/index.js +25 -0
- package/server/lib/quota/providers/claude.js +107 -0
- package/server/lib/quota/providers/codex.js +113 -0
- package/server/lib/quota/providers/copilot.js +165 -0
- package/server/lib/quota/providers/google/api.js +92 -0
- package/server/lib/quota/providers/google/auth.js +108 -0
- package/server/lib/quota/providers/google/index.js +124 -0
- package/server/lib/quota/providers/google/transforms.js +109 -0
- package/server/lib/quota/providers/index.js +168 -0
- package/server/lib/quota/providers/interface.js +55 -0
- package/server/lib/quota/providers/kimi.js +108 -0
- package/server/lib/quota/providers/minimax-cn-coding-plan.js +140 -0
- package/server/lib/quota/providers/minimax-coding-plan.js +139 -0
- package/server/lib/quota/providers/nanogpt.js +124 -0
- package/server/lib/quota/providers/ollama-cloud.js +112 -0
- package/server/lib/quota/providers/openai.js +91 -0
- package/server/lib/quota/providers/openrouter.js +92 -0
- package/server/lib/quota/providers/zai.js +91 -0
- package/server/lib/quota/providers/zhipuai-coding-plan.js +133 -0
- package/server/lib/quota/providers/zhipuai.js +114 -0
- package/server/lib/quota/routes.js +27 -0
- package/server/lib/quota/utils/auth.js +50 -0
- package/server/lib/quota/utils/formatters.js +85 -0
- package/server/lib/quota/utils/formatters.test.js +54 -0
- package/server/lib/quota/utils/index.js +10 -0
- package/server/lib/quota/utils/transformers.js +55 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +44 -0
- package/server/lib/scheduled-tasks/routes.js +235 -0
- package/server/lib/scheduled-tasks/runtime.js +773 -0
- package/server/lib/scheduled-tasks/runtime.test.js +100 -0
- package/server/lib/security/request-security.js +115 -0
- package/server/lib/session-folders/routes.js +63 -0
- package/server/lib/session-folders/routes.test.js +102 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
- package/server/lib/skills-catalog/cache.js +29 -0
- package/server/lib/skills-catalog/clawdhub/api.js +158 -0
- package/server/lib/skills-catalog/clawdhub/index.js +30 -0
- package/server/lib/skills-catalog/clawdhub/install.js +238 -0
- package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
- package/server/lib/skills-catalog/curated-sources.js +21 -0
- package/server/lib/skills-catalog/git.js +77 -0
- package/server/lib/skills-catalog/index.js +42 -0
- package/server/lib/skills-catalog/install.js +294 -0
- package/server/lib/skills-catalog/scan.js +221 -0
- package/server/lib/skills-catalog/source.js +87 -0
- package/server/lib/terminal/DOCUMENTATION.md +76 -0
- package/server/lib/terminal/index.js +31 -0
- package/server/lib/terminal/output-replay-buffer.js +78 -0
- package/server/lib/terminal/output-replay-buffer.test.js +75 -0
- package/server/lib/terminal/runtime.js +850 -0
- package/server/lib/terminal/runtime.test.js +96 -0
- package/server/lib/terminal/terminal-ws-protocol.js +68 -0
- package/server/lib/terminal/terminal-ws-protocol.test.js +145 -0
- package/server/lib/text/DOCUMENTATION.md +35 -0
- package/server/lib/text/summarization.js +138 -0
- package/server/lib/text/summarization.test.js +34 -0
- package/server/lib/tts/DOCUMENTATION.md +146 -0
- package/server/lib/tts/base-url.js +62 -0
- package/server/lib/tts/capability-runtime.js +31 -0
- package/server/lib/tts/index.js +19 -0
- package/server/lib/tts/routes.js +261 -0
- package/server/lib/tts/routes.test.js +53 -0
- package/server/lib/tts/service.js +178 -0
- package/server/lib/tts/stt.js +75 -0
- package/server/lib/tunnels/DOCUMENTATION.md +18 -0
- package/server/lib/tunnels/index.js +166 -0
- package/server/lib/tunnels/managed-config.js +201 -0
- package/server/lib/tunnels/providers/cloudflare.js +260 -0
- package/server/lib/tunnels/registry.js +51 -0
- package/server/lib/tunnels/routes.js +605 -0
- package/server/lib/tunnels/types.js +219 -0
- package/server/lib/ui-auth/DOCUMENTATION.md +38 -0
- package/server/lib/ui-auth/ui-auth.js +673 -0
- package/server/lib/ui-auth/ui-passkeys.js +545 -0
- package/server/opencode-proxy.test.js +151 -0
- package/server/proxy-headers.js +61 -0
- package/server/proxy-headers.test.js +58 -0
- package/server/sse-routes.test.js +152 -0
|
@@ -0,0 +1,605 @@
|
|
|
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/vinci/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
|
+
});
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.warn('Tunnel dependency check failed:', error);
|
|
226
|
+
res.json({ available: false, provider: null, version: null });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const handleTunnelDoctor = async (req, res) => {
|
|
231
|
+
try {
|
|
232
|
+
const params = req.query || {};
|
|
233
|
+
const body = req.body || {};
|
|
234
|
+
|
|
235
|
+
const providerId = typeof params.provider === 'string' && params.provider.trim().length > 0
|
|
236
|
+
? normalizeTunnelProvider(params.provider)
|
|
237
|
+
: await resolvePreferredTunnelProvider();
|
|
238
|
+
const modeFilter = typeof params.mode === 'string' && params.mode.trim().length > 0
|
|
239
|
+
? params.mode.trim().toLowerCase()
|
|
240
|
+
: null;
|
|
241
|
+
|
|
242
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
243
|
+
const selectedPresetId = typeof params.managedRemoteTunnelPresetId === 'string'
|
|
244
|
+
? params.managedRemoteTunnelPresetId.trim()
|
|
245
|
+
: '';
|
|
246
|
+
const requestConfigPath = normalizeOptionalPath(params.configPath)
|
|
247
|
+
?? normalizeOptionalPath(settings?.managedLocalTunnelConfigPath);
|
|
248
|
+
const requestManagedRemoteHostname = normalizeManagedRemoteTunnelHostname(params.managedRemoteTunnelHostname);
|
|
249
|
+
const requestTunnelHostname = normalizeManagedRemoteTunnelHostname(params.tunnelHostname);
|
|
250
|
+
const requestHostname = normalizeManagedRemoteTunnelHostname(params.hostname);
|
|
251
|
+
const hostnameFromSettings = normalizeManagedRemoteTunnelHostname(settings?.managedRemoteTunnelHostname);
|
|
252
|
+
const hostname = requestHostname || requestTunnelHostname || requestManagedRemoteHostname || hostnameFromSettings;
|
|
253
|
+
|
|
254
|
+
const requestManagedRemoteToken = typeof body.managedRemoteTunnelToken === 'string'
|
|
255
|
+
? body.managedRemoteTunnelToken.trim()
|
|
256
|
+
: '';
|
|
257
|
+
const requestTunnelToken = typeof body.tunnelToken === 'string'
|
|
258
|
+
? body.tunnelToken.trim()
|
|
259
|
+
: '';
|
|
260
|
+
const requestToken = typeof body.token === 'string'
|
|
261
|
+
? body.token.trim()
|
|
262
|
+
: '';
|
|
263
|
+
const requestTokenProvided = body.managedRemoteTunnelTokenProvided === true
|
|
264
|
+
|| body.tunnelTokenProvided === true
|
|
265
|
+
|| body.tokenProvided === true;
|
|
266
|
+
const requestHostnameProvided = body.managedRemoteTunnelHostnameProvided === true
|
|
267
|
+
|| body.tunnelHostnameProvided === true
|
|
268
|
+
|| body.hostnameProvided === true;
|
|
269
|
+
const storedManagedRemoteToken = typeof settings?.managedRemoteTunnelToken === 'string'
|
|
270
|
+
? settings.managedRemoteTunnelToken.trim()
|
|
271
|
+
: '';
|
|
272
|
+
const managedRemoteTunnelConfig = await readManagedRemoteTunnelConfigFromDisk();
|
|
273
|
+
const serverHasSavedManagedRemoteProfile = managedRemoteTunnelConfig.tunnels.some((entry) => {
|
|
274
|
+
const savedHostname = normalizeManagedRemoteTunnelHostname(entry?.hostname);
|
|
275
|
+
const savedToken = typeof entry?.token === 'string' ? entry.token.trim() : '';
|
|
276
|
+
return Boolean(savedHostname && savedToken);
|
|
277
|
+
});
|
|
278
|
+
const cliHasSavedManagedRemoteProfile = params.hasSavedManagedRemoteProfile === '1';
|
|
279
|
+
const hasSavedManagedRemoteProfile = serverHasSavedManagedRemoteProfile || cliHasSavedManagedRemoteProfile;
|
|
280
|
+
const configManagedRemoteToken = providerId === TUNNEL_PROVIDER_CLOUDFLARE
|
|
281
|
+
? await resolveManagedRemoteTunnelToken({ presetId: selectedPresetId, hostname })
|
|
282
|
+
: '';
|
|
283
|
+
const runtimeHostname = getRuntimeManagedRemoteTunnelHostname();
|
|
284
|
+
const runtimeToken = getRuntimeManagedRemoteTunnelToken();
|
|
285
|
+
const token = requestToken
|
|
286
|
+
|| requestTunnelToken
|
|
287
|
+
|| requestManagedRemoteToken
|
|
288
|
+
|| ((runtimeHostname && hostname && runtimeHostname === hostname) ? runtimeToken : '')
|
|
289
|
+
|| configManagedRemoteToken
|
|
290
|
+
|| storedManagedRemoteToken;
|
|
291
|
+
|
|
292
|
+
const doctorRequest = {
|
|
293
|
+
mode: modeFilter,
|
|
294
|
+
hostname,
|
|
295
|
+
token,
|
|
296
|
+
tokenProvided: requestTokenProvided,
|
|
297
|
+
hostnameProvided: requestHostnameProvided,
|
|
298
|
+
configPath: requestConfigPath,
|
|
299
|
+
hasSavedManagedRemoteProfile,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const result = await runTunnelDoctor({
|
|
303
|
+
providerId,
|
|
304
|
+
modeFilter,
|
|
305
|
+
doctorRequest,
|
|
306
|
+
});
|
|
307
|
+
return res.json(result);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (error instanceof TunnelServiceError) {
|
|
310
|
+
return res.status(400).json({ ok: false, error: error.message, code: error.code });
|
|
311
|
+
}
|
|
312
|
+
console.warn('Tunnel doctor failed:', error);
|
|
313
|
+
return res.status(500).json({ ok: false, error: 'Failed to run tunnel doctor' });
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
app.post('/api/vinci/tunnel/doctor', handleTunnelDoctor);
|
|
317
|
+
app.get('/api/vinci/tunnel/doctor', handleTunnelDoctor);
|
|
318
|
+
|
|
319
|
+
app.get('/api/vinci/tunnel/providers', (_req, res) => {
|
|
320
|
+
const providers = tunnelProviderRegistry.listCapabilities();
|
|
321
|
+
return res.json({ providers });
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
app.get('/api/vinci/tunnel/status', async (_req, res) => {
|
|
325
|
+
try {
|
|
326
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
327
|
+
const normalizedMode = normalizeTunnelMode(settings?.tunnelMode);
|
|
328
|
+
const managedRemoteHostname = normalizeManagedRemoteTunnelHostname(settings?.managedRemoteTunnelHostname);
|
|
329
|
+
const managedRemoteTunnelConfig = await readManagedRemoteTunnelConfigFromDisk();
|
|
330
|
+
const managedRemoteTunnelPresetSummaries = managedRemoteTunnelConfig.tunnels.map((entry) => ({
|
|
331
|
+
id: entry.id,
|
|
332
|
+
name: entry.name,
|
|
333
|
+
hostname: entry.hostname,
|
|
334
|
+
}));
|
|
335
|
+
const hasStoredManagedRemoteToken = typeof settings?.managedRemoteTunnelToken === 'string' && settings.managedRemoteTunnelToken.trim().length > 0;
|
|
336
|
+
const hasManagedRemoteTunnelToken = getRuntimeManagedRemoteTunnelToken().length > 0 || managedRemoteTunnelConfig.tunnels.length > 0 || hasStoredManagedRemoteToken;
|
|
337
|
+
const bootstrapTtlMs = settings?.tunnelBootstrapTtlMs === null
|
|
338
|
+
? null
|
|
339
|
+
: normalizeTunnelBootstrapTtlMs(settings?.tunnelBootstrapTtlMs);
|
|
340
|
+
const sessionTtlMs = normalizeTunnelSessionTtlMs(settings?.tunnelSessionTtlMs);
|
|
341
|
+
const activeSessions = tunnelAuthController.listTunnelSessions();
|
|
342
|
+
const activeProvider = tunnelService.resolveActiveProvider();
|
|
343
|
+
const provider = activeProvider || normalizeTunnelProvider(settings?.tunnelProvider);
|
|
344
|
+
|
|
345
|
+
const publicUrl = tunnelService.getPublicUrl();
|
|
346
|
+
if (!publicUrl) {
|
|
347
|
+
return res.json({
|
|
348
|
+
active: false,
|
|
349
|
+
url: null,
|
|
350
|
+
mode: normalizedMode,
|
|
351
|
+
provider,
|
|
352
|
+
providerMetadata: null,
|
|
353
|
+
hasManagedRemoteTunnelToken,
|
|
354
|
+
managedRemoteTunnelHostname: managedRemoteHostname || null,
|
|
355
|
+
managedRemoteTunnelPresets: managedRemoteTunnelPresetSummaries,
|
|
356
|
+
managedRemoteTunnelTokenPresetIds: managedRemoteTunnelConfig.tunnels.map((entry) => entry.id),
|
|
357
|
+
hasBootstrapToken: false,
|
|
358
|
+
bootstrapExpiresAt: null,
|
|
359
|
+
policy: 'tunnel-gated',
|
|
360
|
+
activeTunnelMode: tunnelAuthController.getActiveTunnelMode() || null,
|
|
361
|
+
activeSessions,
|
|
362
|
+
localPort: getActivePort(),
|
|
363
|
+
ttlConfig: {
|
|
364
|
+
bootstrapTtlMs,
|
|
365
|
+
sessionTtlMs,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const activeNormalizedMode = resolveActiveNormalizedTunnelMode();
|
|
371
|
+
const activeTunnelId = tunnelAuthController.getActiveTunnelId();
|
|
372
|
+
const activeTunnelHost = tunnelAuthController.getActiveTunnelHost();
|
|
373
|
+
const resolvedTunnelHost = resolveNormalizedTunnelHost(publicUrl);
|
|
374
|
+
const activeTunnelMode = tunnelAuthController.getActiveTunnelMode();
|
|
375
|
+
const needsActiveTunnelSync = !activeTunnelId
|
|
376
|
+
|| !activeTunnelHost
|
|
377
|
+
|| !resolvedTunnelHost
|
|
378
|
+
|| activeTunnelHost !== resolvedTunnelHost
|
|
379
|
+
|| activeTunnelMode !== activeNormalizedMode;
|
|
380
|
+
if (needsActiveTunnelSync) {
|
|
381
|
+
tunnelAuthController.setActiveTunnel({
|
|
382
|
+
tunnelId: activeTunnelId || crypto.randomUUID(),
|
|
383
|
+
publicUrl,
|
|
384
|
+
mode: activeNormalizedMode,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const bootstrapStatus = tunnelAuthController.getBootstrapStatus();
|
|
389
|
+
const providerMetadata = tunnelService.getProviderMetadata();
|
|
390
|
+
|
|
391
|
+
return res.json({
|
|
392
|
+
active: true,
|
|
393
|
+
url: publicUrl,
|
|
394
|
+
mode: activeNormalizedMode,
|
|
395
|
+
provider,
|
|
396
|
+
providerMetadata,
|
|
397
|
+
hasManagedRemoteTunnelToken,
|
|
398
|
+
managedRemoteTunnelHostname: managedRemoteHostname || null,
|
|
399
|
+
managedRemoteTunnelPresets: managedRemoteTunnelPresetSummaries,
|
|
400
|
+
managedRemoteTunnelTokenPresetIds: managedRemoteTunnelConfig.tunnels.map((entry) => entry.id),
|
|
401
|
+
hasBootstrapToken: bootstrapStatus.hasBootstrapToken,
|
|
402
|
+
bootstrapExpiresAt: bootstrapStatus.bootstrapExpiresAt,
|
|
403
|
+
policy: 'tunnel-gated',
|
|
404
|
+
activeTunnelMode: activeNormalizedMode,
|
|
405
|
+
activeSessions: tunnelAuthController.listTunnelSessions(),
|
|
406
|
+
localPort: getActivePort(),
|
|
407
|
+
ttlConfig: {
|
|
408
|
+
bootstrapTtlMs,
|
|
409
|
+
sessionTtlMs,
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
} catch (error) {
|
|
413
|
+
return res.status(500).json({ error: 'Failed to get tunnel status' });
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
app.put('/api/vinci/tunnel/managed-remote-token', async (req, res) => {
|
|
418
|
+
try {
|
|
419
|
+
const presetId = typeof req?.body?.presetId === 'string' ? req.body.presetId.trim() : '';
|
|
420
|
+
const presetName = typeof req?.body?.presetName === 'string' ? req.body.presetName.trim() : '';
|
|
421
|
+
const managedRemoteTunnelHostname = normalizeManagedRemoteTunnelHostname(req?.body?.managedRemoteTunnelHostname);
|
|
422
|
+
const managedRemoteTunnelToken = typeof req?.body?.managedRemoteTunnelToken === 'string' ? req.body.managedRemoteTunnelToken.trim() : '';
|
|
423
|
+
|
|
424
|
+
if (!presetId || !presetName || !managedRemoteTunnelHostname || !managedRemoteTunnelToken) {
|
|
425
|
+
return res.status(400).json({ ok: false, error: 'presetId, presetName, managedRemoteTunnelHostname and managedRemoteTunnelToken are required' });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
await upsertManagedRemoteTunnelToken({
|
|
429
|
+
id: presetId,
|
|
430
|
+
name: presetName,
|
|
431
|
+
hostname: managedRemoteTunnelHostname,
|
|
432
|
+
token: managedRemoteTunnelToken,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const managedRemoteTunnelConfig = await readManagedRemoteTunnelConfigFromDisk();
|
|
436
|
+
return res.json({ ok: true, managedRemoteTunnelTokenPresetIds: managedRemoteTunnelConfig.tunnels.map((entry) => entry.id) });
|
|
437
|
+
} catch (error) {
|
|
438
|
+
return res.status(500).json({ ok: false, error: 'Failed to save managed remote tunnel token' });
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
app.post('/api/vinci/tunnel/start', async (_req, res) => {
|
|
443
|
+
try {
|
|
444
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
445
|
+
if (typeof _req?.body?.provider === 'string' && _req.body.provider.trim().length > 0) {
|
|
446
|
+
const rawProvider = _req.body.provider.trim().toLowerCase();
|
|
447
|
+
if (!tunnelProviderRegistry.get(rawProvider)) {
|
|
448
|
+
return res.status(422).json({ ok: false, error: `Unsupported tunnel provider: ${rawProvider}`, code: 'provider_unsupported' });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const provider = normalizeTunnelProvider(_req?.body?.provider ?? settings?.tunnelProvider);
|
|
452
|
+
const modeInput = _req?.body?.mode ?? settings?.tunnelMode;
|
|
453
|
+
const intent = typeof _req?.body?.intent === 'string' ? _req.body.intent.trim().toLowerCase() : undefined;
|
|
454
|
+
const mode = typeof modeInput === 'string'
|
|
455
|
+
? modeInput.trim().toLowerCase()
|
|
456
|
+
: normalizeTunnelMode(modeInput);
|
|
457
|
+
if (typeof _req?.body?.mode === 'string' && _req.body.mode.trim().length > 0 && !isSupportedTunnelMode(mode)) {
|
|
458
|
+
return res.status(422).json({ ok: false, error: `Unsupported tunnel mode: ${mode}`, code: 'mode_unsupported' });
|
|
459
|
+
}
|
|
460
|
+
const selectedPresetId = typeof _req?.body?.managedRemoteTunnelPresetId === 'string' ? _req.body.managedRemoteTunnelPresetId.trim() : '';
|
|
461
|
+
const selectedPresetName = typeof _req?.body?.managedRemoteTunnelPresetName === 'string' ? _req.body.managedRemoteTunnelPresetName.trim() : '';
|
|
462
|
+
const requestConfigPath = normalizeOptionalPath(_req?.body?.configPath)
|
|
463
|
+
?? normalizeOptionalPath(settings?.managedLocalTunnelConfigPath);
|
|
464
|
+
const requestManagedRemoteHostname = normalizeManagedRemoteTunnelHostname(_req?.body?.managedRemoteTunnelHostname);
|
|
465
|
+
const requestTunnelHostname = normalizeManagedRemoteTunnelHostname(_req?.body?.tunnelHostname);
|
|
466
|
+
const requestHostname = normalizeManagedRemoteTunnelHostname(_req?.body?.hostname);
|
|
467
|
+
const hostnameFromSettings = normalizeManagedRemoteTunnelHostname(settings?.managedRemoteTunnelHostname);
|
|
468
|
+
const hostname = requestHostname || requestTunnelHostname || requestManagedRemoteHostname || hostnameFromSettings;
|
|
469
|
+
const requestManagedRemoteToken = typeof _req?.body?.managedRemoteTunnelToken === 'string' ? _req.body.managedRemoteTunnelToken.trim() : '';
|
|
470
|
+
const requestTunnelToken = typeof _req?.body?.tunnelToken === 'string' ? _req.body.tunnelToken.trim() : '';
|
|
471
|
+
const requestToken = typeof _req?.body?.token === 'string' ? _req.body.token.trim() : '';
|
|
472
|
+
const storedManagedRemoteToken = typeof settings?.managedRemoteTunnelToken === 'string' ? settings.managedRemoteTunnelToken.trim() : '';
|
|
473
|
+
const configManagedRemoteToken = provider === TUNNEL_PROVIDER_CLOUDFLARE
|
|
474
|
+
? await resolveManagedRemoteTunnelToken({ presetId: selectedPresetId, hostname })
|
|
475
|
+
: '';
|
|
476
|
+
const runtimeHostname = getRuntimeManagedRemoteTunnelHostname();
|
|
477
|
+
const runtimeToken = getRuntimeManagedRemoteTunnelToken();
|
|
478
|
+
const token = requestToken
|
|
479
|
+
|| requestTunnelToken
|
|
480
|
+
|| requestManagedRemoteToken
|
|
481
|
+
|| ((runtimeHostname && hostname && runtimeHostname === hostname) ? runtimeToken : '')
|
|
482
|
+
|| configManagedRemoteToken
|
|
483
|
+
|| storedManagedRemoteToken;
|
|
484
|
+
const requestConnectTtlMs = typeof _req?.body?.connectTtlMs === 'number' && Number.isFinite(_req.body.connectTtlMs)
|
|
485
|
+
? normalizeTunnelBootstrapTtlMs(_req.body.connectTtlMs)
|
|
486
|
+
: undefined;
|
|
487
|
+
const requestSessionTtlMs = typeof _req?.body?.sessionTtlMs === 'number' && Number.isFinite(_req.body.sessionTtlMs)
|
|
488
|
+
? normalizeTunnelSessionTtlMs(_req.body.sessionTtlMs)
|
|
489
|
+
: undefined;
|
|
490
|
+
const bootstrapTtlMs = requestConnectTtlMs ?? (settings?.tunnelBootstrapTtlMs === null
|
|
491
|
+
? null
|
|
492
|
+
: normalizeTunnelBootstrapTtlMs(settings?.tunnelBootstrapTtlMs));
|
|
493
|
+
const sessionTtlMs = requestSessionTtlMs ?? normalizeTunnelSessionTtlMs(settings?.tunnelSessionTtlMs);
|
|
494
|
+
|
|
495
|
+
const previousTunnelId = tunnelAuthController.getActiveTunnelId();
|
|
496
|
+
const previousMode = tunnelAuthController.getActiveTunnelMode();
|
|
497
|
+
const previousProvider = tunnelService.resolveActiveProvider();
|
|
498
|
+
const previousUrl = tunnelService.getPublicUrl();
|
|
499
|
+
|
|
500
|
+
const { publicUrl, provider: activeProvider, providerMetadata } = await startTunnelWithNormalizedRequest({
|
|
501
|
+
provider,
|
|
502
|
+
mode,
|
|
503
|
+
intent,
|
|
504
|
+
hostname,
|
|
505
|
+
token,
|
|
506
|
+
configPath: requestConfigPath,
|
|
507
|
+
selectedPresetId,
|
|
508
|
+
selectedPresetName,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const replacedTunnel = Boolean(previousTunnelId) && (
|
|
512
|
+
previousMode !== mode
|
|
513
|
+
|| previousProvider !== activeProvider
|
|
514
|
+
|| previousUrl !== publicUrl
|
|
515
|
+
);
|
|
516
|
+
let revokedBootstrapCount = 0;
|
|
517
|
+
let invalidatedSessionCount = 0;
|
|
518
|
+
if (replacedTunnel && previousTunnelId) {
|
|
519
|
+
const revoked = tunnelAuthController.revokeTunnelArtifacts(previousTunnelId);
|
|
520
|
+
revokedBootstrapCount = revoked.revokedBootstrapCount;
|
|
521
|
+
invalidatedSessionCount = revoked.invalidatedSessionCount;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
tunnelAuthController.setActiveTunnel({
|
|
525
|
+
tunnelId: replacedTunnel || !previousTunnelId ? crypto.randomUUID() : previousTunnelId,
|
|
526
|
+
publicUrl,
|
|
527
|
+
mode,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const bootstrapToken = tunnelAuthController.issueBootstrapToken({ ttlMs: bootstrapTtlMs });
|
|
531
|
+
const connectUrl = `${publicUrl.replace(/\/$/, '')}/connect?t=${encodeURIComponent(bootstrapToken.token)}`;
|
|
532
|
+
const managedRemoteTunnelConfig = await readManagedRemoteTunnelConfigFromDisk();
|
|
533
|
+
const isCloudflareProvider = activeProvider === TUNNEL_PROVIDER_CLOUDFLARE;
|
|
534
|
+
|
|
535
|
+
return res.json({
|
|
536
|
+
ok: true,
|
|
537
|
+
url: publicUrl,
|
|
538
|
+
mode,
|
|
539
|
+
provider: activeProvider,
|
|
540
|
+
providerMetadata,
|
|
541
|
+
managedRemoteTunnelHostname: isCloudflareProvider ? (hostname || null) : null,
|
|
542
|
+
managedRemoteTunnelTokenPresetIds: isCloudflareProvider ? managedRemoteTunnelConfig.tunnels.map((entry) => entry.id) : [],
|
|
543
|
+
connectUrl,
|
|
544
|
+
bootstrapExpiresAt: bootstrapToken.expiresAt,
|
|
545
|
+
replacedTunnel,
|
|
546
|
+
replaced: replacedTunnel
|
|
547
|
+
? {
|
|
548
|
+
mode: previousMode,
|
|
549
|
+
provider: previousProvider,
|
|
550
|
+
url: previousUrl,
|
|
551
|
+
}
|
|
552
|
+
: null,
|
|
553
|
+
revokedBootstrapCount,
|
|
554
|
+
invalidatedSessionCount,
|
|
555
|
+
policy: 'tunnel-gated',
|
|
556
|
+
activeTunnelMode: mode,
|
|
557
|
+
activeSessions: tunnelAuthController.listTunnelSessions(),
|
|
558
|
+
localPort: getActivePort(),
|
|
559
|
+
ttlConfig: {
|
|
560
|
+
bootstrapTtlMs,
|
|
561
|
+
sessionTtlMs,
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
} catch (error) {
|
|
565
|
+
console.error('Failed to start tunnel:', error);
|
|
566
|
+
setActiveTunnelController(null);
|
|
567
|
+
tunnelAuthController.clearActiveTunnel();
|
|
568
|
+
if (error instanceof TunnelServiceError) {
|
|
569
|
+
const status = error.code === 'missing_dependency'
|
|
570
|
+
? 400
|
|
571
|
+
: (error.code === 'validation_error' || error.code === 'provider_unsupported' || error.code === 'mode_unsupported'
|
|
572
|
+
? 422
|
|
573
|
+
: 500);
|
|
574
|
+
return res.status(status).json({ ok: false, error: error.message, code: error.code });
|
|
575
|
+
}
|
|
576
|
+
return res.status(500).json({ ok: false, error: 'Failed to start tunnel', code: 'startup_failed' });
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
app.post('/api/vinci/tunnel/stop', (_req, res) => {
|
|
581
|
+
let revokedBootstrapCount = 0;
|
|
582
|
+
let invalidatedSessionCount = 0;
|
|
583
|
+
const activeTunnelId = tunnelAuthController.getActiveTunnelId();
|
|
584
|
+
|
|
585
|
+
if (activeTunnelId) {
|
|
586
|
+
const revoked = tunnelAuthController.revokeTunnelArtifacts(activeTunnelId);
|
|
587
|
+
revokedBootstrapCount = revoked.revokedBootstrapCount;
|
|
588
|
+
invalidatedSessionCount = revoked.invalidatedSessionCount;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (getActiveTunnelController()) {
|
|
592
|
+
console.log('Stopping active tunnel (user requested)...');
|
|
593
|
+
tunnelService.stop();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
tunnelAuthController.clearActiveTunnel();
|
|
597
|
+
res.json({ ok: true, revokedBootstrapCount, invalidatedSessionCount });
|
|
598
|
+
});
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
registerRoutes,
|
|
603
|
+
startTunnelWithNormalizedRequest,
|
|
604
|
+
};
|
|
605
|
+
};
|