wyxrouter 0.4.71
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/CHANGELOG.md +222 -0
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/README.zh-CN.md +1311 -0
- package/assets/pixel-router-2d.png +0 -0
- package/cli/LICENSE +42 -0
- package/cli/README.md +125 -0
- package/cli/app/package.json +58 -0
- package/cli/cli.js +806 -0
- package/cli/package.json +48 -0
- package/i18n/README.ja-JP.md +1209 -0
- package/i18n/README.ru.md +1311 -0
- package/i18n/README.vi.md +1310 -0
- package/i18n/README.zh-CN.md +1306 -0
- package/images/9router.png +0 -0
- package/jsconfig.json +12 -0
- package/next.config.mjs +71 -0
- package/open-sse/config/appConstants.js +203 -0
- package/open-sse/config/codexInstructions.js +119 -0
- package/open-sse/config/constants.js +4 -0
- package/open-sse/config/defaultThinkingSignature.js +12 -0
- package/open-sse/config/errorConfig.js +85 -0
- package/open-sse/config/googleTtsLanguages.js +62 -0
- package/open-sse/config/kiroConstants.js +322 -0
- package/open-sse/config/models.js +13 -0
- package/open-sse/config/ollamaModels.js +19 -0
- package/open-sse/config/providerModels.js +944 -0
- package/open-sse/config/providers.js +458 -0
- package/open-sse/config/runtimeConfig.js +72 -0
- package/open-sse/config/ttsModels.js +129 -0
- package/open-sse/executors/antigravity.js +504 -0
- package/open-sse/executors/azure.js +57 -0
- package/open-sse/executors/base.js +185 -0
- package/open-sse/executors/codex.js +469 -0
- package/open-sse/executors/commandcode.js +88 -0
- package/open-sse/executors/cursor.js +795 -0
- package/open-sse/executors/default.js +497 -0
- package/open-sse/executors/gemini-cli.js +89 -0
- package/open-sse/executors/github.js +379 -0
- package/open-sse/executors/grok-web.js +345 -0
- package/open-sse/executors/iflow.js +108 -0
- package/open-sse/executors/index.js +75 -0
- package/open-sse/executors/kiro.js +508 -0
- package/open-sse/executors/ollama-local.js +14 -0
- package/open-sse/executors/opencode-go.js +41 -0
- package/open-sse/executors/opencode.js +32 -0
- package/open-sse/executors/perplexity-web.js +507 -0
- package/open-sse/executors/qoder.js +450 -0
- package/open-sse/executors/qwen.js +129 -0
- package/open-sse/executors/vertex.js +131 -0
- package/open-sse/executors/xiaomi-tokenplan.js +19 -0
- package/open-sse/handlers/chatCore/nonStreamingHandler.js +230 -0
- package/open-sse/handlers/chatCore/requestDetail.js +102 -0
- package/open-sse/handlers/chatCore/sseToJsonHandler.js +231 -0
- package/open-sse/handlers/chatCore/streamingHandler.js +103 -0
- package/open-sse/handlers/chatCore.js +287 -0
- package/open-sse/handlers/embeddingProviders/_base.js +4 -0
- package/open-sse/handlers/embeddingProviders/gemini.js +54 -0
- package/open-sse/handlers/embeddingProviders/index.js +23 -0
- package/open-sse/handlers/embeddingProviders/openai.js +39 -0
- package/open-sse/handlers/embeddingProviders/openaiCompatNode.js +13 -0
- package/open-sse/handlers/embeddingsCore.js +126 -0
- package/open-sse/handlers/fetch/index.js +237 -0
- package/open-sse/handlers/imageGenerationCore.js +189 -0
- package/open-sse/handlers/imageProviders/_base.js +31 -0
- package/open-sse/handlers/imageProviders/blackForestLabs.js +43 -0
- package/open-sse/handlers/imageProviders/cloudflareAi.js +178 -0
- package/open-sse/handlers/imageProviders/codex.js +198 -0
- package/open-sse/handlers/imageProviders/comfyui.js +8 -0
- package/open-sse/handlers/imageProviders/falAi.js +41 -0
- package/open-sse/handlers/imageProviders/gemini.js +25 -0
- package/open-sse/handlers/imageProviders/huggingface.js +22 -0
- package/open-sse/handlers/imageProviders/index.js +40 -0
- package/open-sse/handlers/imageProviders/nanobanana.js +58 -0
- package/open-sse/handlers/imageProviders/openai.js +40 -0
- package/open-sse/handlers/imageProviders/runwayml.js +47 -0
- package/open-sse/handlers/imageProviders/sdwebui.js +17 -0
- package/open-sse/handlers/imageProviders/stabilityAi.js +34 -0
- package/open-sse/handlers/responsesHandler.js +103 -0
- package/open-sse/handlers/search/callers.js +371 -0
- package/open-sse/handlers/search/chatSearch.js +409 -0
- package/open-sse/handlers/search/index.js +201 -0
- package/open-sse/handlers/search/normalizers.js +223 -0
- package/open-sse/handlers/sttCore.js +194 -0
- package/open-sse/handlers/ttsCore.js +74 -0
- package/open-sse/handlers/ttsProviders/_base.js +39 -0
- package/open-sse/handlers/ttsProviders/edgeTts.js +89 -0
- package/open-sse/handlers/ttsProviders/elevenlabs.js +48 -0
- package/open-sse/handlers/ttsProviders/gemini.js +117 -0
- package/open-sse/handlers/ttsProviders/genericFormats.js +169 -0
- package/open-sse/handlers/ttsProviders/googleTts.js +54 -0
- package/open-sse/handlers/ttsProviders/index.js +50 -0
- package/open-sse/handlers/ttsProviders/localDevice.js +87 -0
- package/open-sse/handlers/ttsProviders/minimax.js +59 -0
- package/open-sse/handlers/ttsProviders/openai.js +30 -0
- package/open-sse/handlers/ttsProviders/openrouter.js +70 -0
- package/open-sse/index.js +82 -0
- package/open-sse/rtk/applyFilter.js +15 -0
- package/open-sse/rtk/autodetect.js +111 -0
- package/open-sse/rtk/caveman.js +100 -0
- package/open-sse/rtk/cavemanPrompts.js +78 -0
- package/open-sse/rtk/constants.js +55 -0
- package/open-sse/rtk/filters/buildOutput.js +127 -0
- package/open-sse/rtk/filters/dedupLog.js +44 -0
- package/open-sse/rtk/filters/find.js +49 -0
- package/open-sse/rtk/filters/gitDiff.js +92 -0
- package/open-sse/rtk/filters/gitStatus.js +117 -0
- package/open-sse/rtk/filters/grep.js +48 -0
- package/open-sse/rtk/filters/ls.js +79 -0
- package/open-sse/rtk/filters/readNumbered.js +27 -0
- package/open-sse/rtk/filters/searchList.js +52 -0
- package/open-sse/rtk/filters/smartTruncate.js +15 -0
- package/open-sse/rtk/filters/tree.js +32 -0
- package/open-sse/rtk/index.js +155 -0
- package/open-sse/rtk/registry.js +38 -0
- package/open-sse/services/accountFallback.js +238 -0
- package/open-sse/services/combo.js +198 -0
- package/open-sse/services/compact.js +71 -0
- package/open-sse/services/kiroModels.js +332 -0
- package/open-sse/services/model.js +261 -0
- package/open-sse/services/oauthCredentialManager.js +151 -0
- package/open-sse/services/projectId.js +306 -0
- package/open-sse/services/provider.js +356 -0
- package/open-sse/services/qoderModels.js +214 -0
- package/open-sse/services/tokenRefresh.js +939 -0
- package/open-sse/services/usage.js +1496 -0
- package/open-sse/transformer/responsesTransformer.js +439 -0
- package/open-sse/transformer/streamToJsonConverter.js +103 -0
- package/open-sse/translator/formats.js +36 -0
- package/open-sse/translator/helpers/claudeHelper.js +216 -0
- package/open-sse/translator/helpers/geminiHelper.js +372 -0
- package/open-sse/translator/helpers/imageHelper.js +34 -0
- package/open-sse/translator/helpers/maxTokensHelper.js +27 -0
- package/open-sse/translator/helpers/openaiHelper.js +130 -0
- package/open-sse/translator/helpers/responsesApiHelper.js +139 -0
- package/open-sse/translator/helpers/toolCallHelper.js +148 -0
- package/open-sse/translator/index.js +251 -0
- package/open-sse/translator/request/antigravity-to-openai.js +229 -0
- package/open-sse/translator/request/claude-to-openai.js +232 -0
- package/open-sse/translator/request/gemini-to-openai.js +147 -0
- package/open-sse/translator/request/openai-responses.js +318 -0
- package/open-sse/translator/request/openai-to-claude.js +401 -0
- package/open-sse/translator/request/openai-to-commandcode.js +170 -0
- package/open-sse/translator/request/openai-to-cursor.js +183 -0
- package/open-sse/translator/request/openai-to-gemini.js +470 -0
- package/open-sse/translator/request/openai-to-kiro.js +629 -0
- package/open-sse/translator/request/openai-to-kiro.old.js +278 -0
- package/open-sse/translator/request/openai-to-ollama.js +192 -0
- package/open-sse/translator/request/openai-to-vertex.js +42 -0
- package/open-sse/translator/response/claude-to-openai.js +206 -0
- package/open-sse/translator/response/commandcode-to-openai.js +197 -0
- package/open-sse/translator/response/cursor-to-openai.js +30 -0
- package/open-sse/translator/response/gemini-to-openai.js +245 -0
- package/open-sse/translator/response/kiro-to-openai.js +195 -0
- package/open-sse/translator/response/ollama-to-openai.js +152 -0
- package/open-sse/translator/response/openai-responses.js +590 -0
- package/open-sse/translator/response/openai-to-antigravity.js +122 -0
- package/open-sse/translator/response/openai-to-claude.js +266 -0
- package/open-sse/utils/bypassHandler.js +298 -0
- package/open-sse/utils/claudeCloaking.js +155 -0
- package/open-sse/utils/claudeHeaderCache.js +70 -0
- package/open-sse/utils/clientDetector.js +63 -0
- package/open-sse/utils/cursorChecksum.js +149 -0
- package/open-sse/utils/cursorProtobuf.js +904 -0
- package/open-sse/utils/debugLog.js +14 -0
- package/open-sse/utils/error.js +147 -0
- package/open-sse/utils/ollamaTransform.js +85 -0
- package/open-sse/utils/proxyFetch.js +368 -0
- package/open-sse/utils/reasoningContentInjector.js +79 -0
- package/open-sse/utils/requestLogger.js +260 -0
- package/open-sse/utils/responsesStreamHelpers.js +49 -0
- package/open-sse/utils/sessionManager.js +82 -0
- package/open-sse/utils/stream.js +462 -0
- package/open-sse/utils/streamHandler.js +250 -0
- package/open-sse/utils/streamHelpers.js +122 -0
- package/open-sse/utils/toolDeduper.js +49 -0
- package/open-sse/utils/usageTracking.js +347 -0
- package/package.json +100 -0
- package/postcss.config.mjs +12 -0
- package/public/favicon.svg +11 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/i18n/literals/ar.json +195 -0
- package/public/i18n/literals/bn.json +195 -0
- package/public/i18n/literals/cs.json +195 -0
- package/public/i18n/literals/da.json +195 -0
- package/public/i18n/literals/de.json +195 -0
- package/public/i18n/literals/el.json +195 -0
- package/public/i18n/literals/es.json +195 -0
- package/public/i18n/literals/fi.json +195 -0
- package/public/i18n/literals/fr.json +195 -0
- package/public/i18n/literals/he.json +195 -0
- package/public/i18n/literals/hi.json +195 -0
- package/public/i18n/literals/hu.json +195 -0
- package/public/i18n/literals/id.json +195 -0
- package/public/i18n/literals/it.json +195 -0
- package/public/i18n/literals/ja.json +195 -0
- package/public/i18n/literals/ko.json +195 -0
- package/public/i18n/literals/nl.json +195 -0
- package/public/i18n/literals/no.json +195 -0
- package/public/i18n/literals/pl.json +195 -0
- package/public/i18n/literals/pt-BR.json +195 -0
- package/public/i18n/literals/pt-PT.json +195 -0
- package/public/i18n/literals/ro.json +195 -0
- package/public/i18n/literals/ru.json +195 -0
- package/public/i18n/literals/sv.json +195 -0
- package/public/i18n/literals/th.json +195 -0
- package/public/i18n/literals/tl.json +195 -0
- package/public/i18n/literals/tr.json +195 -0
- package/public/i18n/literals/uk.json +195 -0
- package/public/i18n/literals/ur.json +195 -0
- package/public/i18n/literals/vi.json +195 -0
- package/public/i18n/literals/zh-CN.json +772 -0
- package/public/i18n/literals/zh-TW.json +195 -0
- package/public/icons/discord.svg +4 -0
- package/public/icons/icon-192.svg +4 -0
- package/public/icons/icon-512.svg +4 -0
- package/public/next.svg +1 -0
- package/public/providers/alicode-intl.png +0 -0
- package/public/providers/alicode.png +0 -0
- package/public/providers/amp.png +0 -0
- package/public/providers/anthropic-m.png +0 -0
- package/public/providers/anthropic.png +0 -0
- package/public/providers/antigravity.png +0 -0
- package/public/providers/assemblyai.png +0 -0
- package/public/providers/aws-polly.png +0 -0
- package/public/providers/azure.png +0 -0
- package/public/providers/black-forest-labs.png +0 -0
- package/public/providers/blackbox.png +0 -0
- package/public/providers/brave-search.png +0 -0
- package/public/providers/byteplus.png +0 -0
- package/public/providers/cartesia.png +0 -0
- package/public/providers/cerebras.png +0 -0
- package/public/providers/chutes.png +0 -0
- package/public/providers/claude.png +0 -0
- package/public/providers/cline.png +0 -0
- package/public/providers/cloudflare-ai.png +0 -0
- package/public/providers/codebuddy.svg +37 -0
- package/public/providers/codex.png +0 -0
- package/public/providers/cohere.png +0 -0
- package/public/providers/comfyui.png +0 -0
- package/public/providers/commandcode.png +0 -0
- package/public/providers/continue.png +0 -0
- package/public/providers/copilot.png +0 -0
- package/public/providers/coqui.png +0 -0
- package/public/providers/cursor.png +0 -0
- package/public/providers/deepgram.png +0 -0
- package/public/providers/deepseek-tui.png +0 -0
- package/public/providers/deepseek.png +0 -0
- package/public/providers/droid.png +0 -0
- package/public/providers/edge-tts.png +0 -0
- package/public/providers/elevenlabs.png +0 -0
- package/public/providers/exa.png +0 -0
- package/public/providers/fal-ai.png +0 -0
- package/public/providers/firecrawl.png +0 -0
- package/public/providers/fireworks.png +0 -0
- package/public/providers/gemini-cli.png +0 -0
- package/public/providers/gemini.png +0 -0
- package/public/providers/github.png +0 -0
- package/public/providers/glm-cn.png +0 -0
- package/public/providers/glm.png +0 -0
- package/public/providers/google-pse.png +0 -0
- package/public/providers/google-tts.png +0 -0
- package/public/providers/grok-web.png +0 -0
- package/public/providers/groq.png +0 -0
- package/public/providers/hermes.png +0 -0
- package/public/providers/huggingface.png +0 -0
- package/public/providers/hyperbolic.png +0 -0
- package/public/providers/iflow.png +0 -0
- package/public/providers/inworld.png +0 -0
- package/public/providers/jcode.png +0 -0
- package/public/providers/jina-ai.png +0 -0
- package/public/providers/jina-reader.png +0 -0
- package/public/providers/kilocode.png +0 -0
- package/public/providers/kimi-coding.png +0 -0
- package/public/providers/kimi.png +0 -0
- package/public/providers/kiro.png +0 -0
- package/public/providers/linkup.png +0 -0
- package/public/providers/local-device.png +0 -0
- package/public/providers/minimax-cn.png +0 -0
- package/public/providers/minimax.png +0 -0
- package/public/providers/mistral.png +0 -0
- package/public/providers/nanobanana.png +0 -0
- package/public/providers/nebius.png +0 -0
- package/public/providers/nvidia.png +0 -0
- package/public/providers/oai-cc.png +0 -0
- package/public/providers/oai-r.png +0 -0
- package/public/providers/ollama-local.png +0 -0
- package/public/providers/ollama.png +0 -0
- package/public/providers/openai.png +0 -0
- package/public/providers/openclaw.png +0 -0
- package/public/providers/opencode-go.png +0 -0
- package/public/providers/opencode.png +0 -0
- package/public/providers/openrouter.png +0 -0
- package/public/providers/perplexity-web.png +0 -0
- package/public/providers/perplexity.png +0 -0
- package/public/providers/playht.png +0 -0
- package/public/providers/qoder.png +0 -0
- package/public/providers/qwen.png +0 -0
- package/public/providers/recraft.png +0 -0
- package/public/providers/roo.png +0 -0
- package/public/providers/runwayml.png +0 -0
- package/public/providers/sdwebui.png +0 -0
- package/public/providers/searchapi.png +0 -0
- package/public/providers/searxng.png +0 -0
- package/public/providers/serper.png +0 -0
- package/public/providers/siliconflow.png +0 -0
- package/public/providers/stability-ai.png +0 -0
- package/public/providers/tavily.png +0 -0
- package/public/providers/together.png +0 -0
- package/public/providers/topaz.png +0 -0
- package/public/providers/tortoise.png +0 -0
- package/public/providers/vertex-partner.png +0 -0
- package/public/providers/vertex.png +0 -0
- package/public/providers/volcengine-ark.png +0 -0
- package/public/providers/voyage-ai.png +0 -0
- package/public/providers/xai.png +0 -0
- package/public/providers/xiaomi-mimo.png +0 -0
- package/public/providers/xiaomi-tokenplan.png +0 -0
- package/public/providers/youcom.png +0 -0
- package/public/sw.js +22 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/scripts/compact-request-details.mjs +71 -0
- package/scripts/import-codex-gptjson.mjs +342 -0
- package/scripts/start-standalone.mjs +25 -0
- package/scripts/translate-readme.js +201 -0
- package/src/app/(dashboard)/dashboard/automation/page.js +294 -0
- package/src/app/(dashboard)/dashboard/basic-chat/BasicChatPageClient.js +967 -0
- package/src/app/(dashboard)/dashboard/basic-chat/page.js +5 -0
- package/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js +66 -0
- package/src/app/(dashboard)/dashboard/cli-tools/[toolId]/ToolDetailClient.js +173 -0
- package/src/app/(dashboard)/dashboard/cli-tools/[toolId]/page.js +11 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/AntigravityToolCard.js +481 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/ApiKeySelect.js +66 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/BaseUrlSelect.js +174 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js +390 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/ClineToolCard.js +301 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js +458 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/CopilotToolCard.js +323 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js +640 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/DeepSeekTuiToolCard.js +338 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js +271 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.js +410 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/EndpointPresetControl.js +128 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/HermesToolCard.js +317 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/JcodeToolCard.js +380 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/KiloToolCard.js +275 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/MitmLinkCard.js +40 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js +329 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/MitmToolCard.js +318 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.js +388 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/OpenCodeToolCard.js +500 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/ToolSummaryCard.js +39 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/cliEndpointMatch.js +13 -0
- package/src/app/(dashboard)/dashboard/cli-tools/components/index.js +19 -0
- package/src/app/(dashboard)/dashboard/cli-tools/page.js +7 -0
- package/src/app/(dashboard)/dashboard/combos/page.js +612 -0
- package/src/app/(dashboard)/dashboard/console-log/ConsoleLogClient.js +91 -0
- package/src/app/(dashboard)/dashboard/console-log/page.js +8 -0
- package/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js +1555 -0
- package/src/app/(dashboard)/dashboard/endpoint/page.js +7 -0
- package/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.js +1903 -0
- package/src/app/(dashboard)/dashboard/media-providers/[kind]/page.js +289 -0
- package/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js +410 -0
- package/src/app/(dashboard)/dashboard/media-providers/web/page.js +209 -0
- package/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js +117 -0
- package/src/app/(dashboard)/dashboard/mitm/page.js +5 -0
- package/src/app/(dashboard)/dashboard/page.js +7 -0
- package/src/app/(dashboard)/dashboard/profile/page.js +1140 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js +389 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/AddCustomModelModal.js +125 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/CompatibleModelsSection.js +250 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js +299 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/CooldownTimer.js +42 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/EditCompatibleNodeModal.js +161 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/ModelRow.js +95 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/PassthroughModelsSection.js +183 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/page.js +2053 -0
- package/src/app/(dashboard)/dashboard/providers/[id]/page.new.js +1724 -0
- package/src/app/(dashboard)/dashboard/providers/components/ConnectionsCard.js +497 -0
- package/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityBadge.js +185 -0
- package/src/app/(dashboard)/dashboard/providers/components/ModelsCard.js +294 -0
- package/src/app/(dashboard)/dashboard/providers/new/page.js +220 -0
- package/src/app/(dashboard)/dashboard/providers/page.js +1365 -0
- package/src/app/(dashboard)/dashboard/proxy-pools/page.js +1092 -0
- package/src/app/(dashboard)/dashboard/quota/page.js +11 -0
- package/src/app/(dashboard)/dashboard/skills/page.js +112 -0
- package/src/app/(dashboard)/dashboard/translator/page.js +303 -0
- package/src/app/(dashboard)/dashboard/usage/components/OverviewCards.js +35 -0
- package/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.js +185 -0
- package/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaProgressBar.js +127 -0
- package/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.js +259 -0
- package/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js +1394 -0
- package/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js +244 -0
- package/src/app/(dashboard)/dashboard/usage/components/ProviderTopology.js +327 -0
- package/src/app/(dashboard)/dashboard/usage/components/RequestDetailsTab.js +433 -0
- package/src/app/(dashboard)/dashboard/usage/components/UsageChart.js +141 -0
- package/src/app/(dashboard)/dashboard/usage/components/UsageTable.js +247 -0
- package/src/app/(dashboard)/dashboard/usage/page.js +75 -0
- package/src/app/(dashboard)/layout.js +6 -0
- package/src/app/api/auth/login/route.js +76 -0
- package/src/app/api/auth/logout/route.js +12 -0
- package/src/app/api/auth/oidc/callback/route.js +87 -0
- package/src/app/api/auth/oidc/start/route.js +52 -0
- package/src/app/api/auth/oidc/test/route.js +84 -0
- package/src/app/api/auth/status/route.js +45 -0
- package/src/app/api/cli-tools/all-statuses/route.js +46 -0
- package/src/app/api/cli-tools/antigravity-mitm/alias/route.js +53 -0
- package/src/app/api/cli-tools/antigravity-mitm/route.js +202 -0
- package/src/app/api/cli-tools/claude-settings/route.js +203 -0
- package/src/app/api/cli-tools/cline-settings/route.js +133 -0
- package/src/app/api/cli-tools/codex-gateway/accounts/route.js +16 -0
- package/src/app/api/cli-tools/codex-settings/route.js +239 -0
- package/src/app/api/cli-tools/copilot-settings/route.js +148 -0
- package/src/app/api/cli-tools/cowork-mcp-registry/route.js +77 -0
- package/src/app/api/cli-tools/cowork-mcp-tools/route.js +95 -0
- package/src/app/api/cli-tools/cowork-settings/route.js +412 -0
- package/src/app/api/cli-tools/deepseek-tui-settings/route.js +164 -0
- package/src/app/api/cli-tools/droid-settings/route.js +213 -0
- package/src/app/api/cli-tools/hermes-settings/route.js +175 -0
- package/src/app/api/cli-tools/jcode-settings/route.js +216 -0
- package/src/app/api/cli-tools/kilo-settings/route.js +131 -0
- package/src/app/api/cli-tools/openclaw-settings/route.js +292 -0
- package/src/app/api/cli-tools/opencode-settings/route.js +259 -0
- package/src/app/api/combos/[id]/route.js +81 -0
- package/src/app/api/combos/route.js +48 -0
- package/src/app/api/health/route.js +15 -0
- package/src/app/api/init/route.js +4 -0
- package/src/app/api/keys/[id]/route.js +58 -0
- package/src/app/api/keys/route.js +42 -0
- package/src/app/api/locale/route.js +30 -0
- package/src/app/api/mcp/[plugin]/message/route.js +21 -0
- package/src/app/api/mcp/[plugin]/sse/route.js +37 -0
- package/src/app/api/media-providers/tts/deepgram/voices/route.js +65 -0
- package/src/app/api/media-providers/tts/elevenlabs/voices/route.js +71 -0
- package/src/app/api/media-providers/tts/inworld/voices/route.js +61 -0
- package/src/app/api/media-providers/tts/minimax/voices/route.js +113 -0
- package/src/app/api/media-providers/tts/voices/route.js +99 -0
- package/src/app/api/models/alias/route.js +53 -0
- package/src/app/api/models/availability/route.js +103 -0
- package/src/app/api/models/custom/route.js +48 -0
- package/src/app/api/models/disabled/route.js +50 -0
- package/src/app/api/models/route.js +64 -0
- package/src/app/api/models/test/ping.js +191 -0
- package/src/app/api/models/test/route.js +14 -0
- package/src/app/api/oauth/[provider]/[action]/route.js +343 -0
- package/src/app/api/oauth/codebuddy/bulk-import/[jobId]/cancel/route.js +19 -0
- package/src/app/api/oauth/codebuddy/bulk-import/[jobId]/manual/[workerId]/route.js +30 -0
- package/src/app/api/oauth/codebuddy/bulk-import/[jobId]/route.js +23 -0
- package/src/app/api/oauth/codebuddy/bulk-import/latest/route.js +25 -0
- package/src/app/api/oauth/codebuddy/bulk-import/route.js +49 -0
- package/src/app/api/oauth/codebuddy/quota-cookie/route.js +133 -0
- package/src/app/api/oauth/codex/import-token/route.js +96 -0
- package/src/app/api/oauth/cursor/auto-import/route.js +258 -0
- package/src/app/api/oauth/cursor/import/route.js +100 -0
- package/src/app/api/oauth/gitlab/pat/route.js +62 -0
- package/src/app/api/oauth/iflow/cookie/route.js +137 -0
- package/src/app/api/oauth/kiro/auto-import/route.js +85 -0
- package/src/app/api/oauth/kiro/bulk-import/[jobId]/cancel/route.js +18 -0
- package/src/app/api/oauth/kiro/bulk-import/[jobId]/manual/[workerId]/route.js +29 -0
- package/src/app/api/oauth/kiro/bulk-import/[jobId]/route.js +22 -0
- package/src/app/api/oauth/kiro/bulk-import/latest/route.js +25 -0
- package/src/app/api/oauth/kiro/bulk-import/route.js +49 -0
- package/src/app/api/oauth/kiro/import/route.js +110 -0
- package/src/app/api/oauth/kiro/social-authorize/route.js +27 -0
- package/src/app/api/oauth/kiro/social-exchange/route.js +41 -0
- package/src/app/api/pricing/route.js +134 -0
- package/src/app/api/provider-nodes/[id]/route.js +101 -0
- package/src/app/api/provider-nodes/route.js +104 -0
- package/src/app/api/provider-nodes/validate/route.js +201 -0
- package/src/app/api/providers/[id]/models/route.js +526 -0
- package/src/app/api/providers/[id]/route.js +189 -0
- package/src/app/api/providers/[id]/test/route.js +23 -0
- package/src/app/api/providers/[id]/test/testUtils.js +714 -0
- package/src/app/api/providers/[id]/test-models/route.js +66 -0
- package/src/app/api/providers/client/route.js +126 -0
- package/src/app/api/providers/kilo/free-models/route.js +55 -0
- package/src/app/api/providers/route.js +206 -0
- package/src/app/api/providers/suggested-models/filters.js +20 -0
- package/src/app/api/providers/suggested-models/route.js +32 -0
- package/src/app/api/providers/test-batch/route.js +131 -0
- package/src/app/api/providers/validate/route.js +637 -0
- package/src/app/api/proxy-pools/[id]/route.js +123 -0
- package/src/app/api/proxy-pools/[id]/test/route.js +70 -0
- package/src/app/api/proxy-pools/cloudflare-deploy/route.js +145 -0
- package/src/app/api/proxy-pools/deno-deploy/route.js +175 -0
- package/src/app/api/proxy-pools/route.js +93 -0
- package/src/app/api/proxy-pools/vercel-deploy/route.js +142 -0
- package/src/app/api/settings/database/route.js +36 -0
- package/src/app/api/settings/proxy-test/route.js +23 -0
- package/src/app/api/settings/require-login/route.js +15 -0
- package/src/app/api/settings/route.js +100 -0
- package/src/app/api/shutdown/route.js +24 -0
- package/src/app/api/tags/route.js +18 -0
- package/src/app/api/translator/console-logs/route.js +24 -0
- package/src/app/api/translator/console-logs/stream/route.js +79 -0
- package/src/app/api/translator/load/route.js +45 -0
- package/src/app/api/translator/save/route.js +44 -0
- package/src/app/api/translator/send/route.js +94 -0
- package/src/app/api/translator/translate/route.js +90 -0
- package/src/app/api/tunnel/disable/route.js +12 -0
- package/src/app/api/tunnel/enable/route.js +16 -0
- package/src/app/api/tunnel/status/route.js +13 -0
- package/src/app/api/tunnel/tailscale-check/route.js +50 -0
- package/src/app/api/tunnel/tailscale-disable/route.js +12 -0
- package/src/app/api/tunnel/tailscale-enable/route.js +12 -0
- package/src/app/api/tunnel/tailscale-install/route.js +72 -0
- package/src/app/api/usage/[connectionId]/route.js +188 -0
- package/src/app/api/usage/chart/route.js +21 -0
- package/src/app/api/usage/history/route.js +12 -0
- package/src/app/api/usage/logs/route.js +12 -0
- package/src/app/api/usage/providers/route.js +42 -0
- package/src/app/api/usage/request-details/route.js +57 -0
- package/src/app/api/usage/request-logs/route.js +13 -0
- package/src/app/api/usage/stats/route.js +23 -0
- package/src/app/api/usage/stream/route.js +79 -0
- package/src/app/api/v1/api/chat/route.js +37 -0
- package/src/app/api/v1/audio/speech/route.js +16 -0
- package/src/app/api/v1/audio/transcriptions/route.js +19 -0
- package/src/app/api/v1/audio/voices/route.js +68 -0
- package/src/app/api/v1/chat/completions/route.js +35 -0
- package/src/app/api/v1/embeddings/route.js +21 -0
- package/src/app/api/v1/images/generations/route.js +16 -0
- package/src/app/api/v1/messages/count_tokens/route.js +52 -0
- package/src/app/api/v1/messages/route.js +36 -0
- package/src/app/api/v1/models/[kind]/route.js +55 -0
- package/src/app/api/v1/models/info/route.js +110 -0
- package/src/app/api/v1/models/route.js +451 -0
- package/src/app/api/v1/responses/compact/route.js +37 -0
- package/src/app/api/v1/responses/route.js +30 -0
- package/src/app/api/v1/route.js +1 -0
- package/src/app/api/v1/search/route.js +21 -0
- package/src/app/api/v1/web/fetch/route.js +21 -0
- package/src/app/api/v1beta/models/[...path]/route.js +328 -0
- package/src/app/api/v1beta/models/route.js +44 -0
- package/src/app/api/version/route.js +45 -0
- package/src/app/api/version/shutdown/route.js +15 -0
- package/src/app/api/version/update/route.js +21 -0
- package/src/app/callback/page.js +148 -0
- package/src/app/dashboard/settings/pricing/page.js +173 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +496 -0
- package/src/app/landing/components/AnimatedBackground.js +57 -0
- package/src/app/landing/components/Features.js +133 -0
- package/src/app/landing/components/FlowAnimation.js +175 -0
- package/src/app/landing/components/Footer.js +61 -0
- package/src/app/landing/components/GetStarted.js +97 -0
- package/src/app/landing/components/HeroSection.js +47 -0
- package/src/app/landing/components/HowItWorks.js +66 -0
- package/src/app/landing/components/Navigation.js +72 -0
- package/src/app/landing/page.js +106 -0
- package/src/app/layout.js +49 -0
- package/src/app/login/page.js +197 -0
- package/src/app/manifest.js +30 -0
- package/src/app/page.js +5 -0
- package/src/dashboardGuard.js +242 -0
- package/src/i18n/RuntimeI18nProvider.js +27 -0
- package/src/i18n/config.js +146 -0
- package/src/i18n/runtime.js +162 -0
- package/src/lib/appUpdater.js +200 -0
- package/src/lib/auth/dashboardSession.js +68 -0
- package/src/lib/auth/loginLimiter.js +52 -0
- package/src/lib/auth/oidc.js +234 -0
- package/src/lib/consoleLogBuffer.js +79 -0
- package/src/lib/dataDir.js +29 -0
- package/src/lib/db/adapters/betterSqliteAdapter.js +55 -0
- package/src/lib/db/adapters/bunSqliteAdapter.js +63 -0
- package/src/lib/db/adapters/nodeSqliteAdapter.js +84 -0
- package/src/lib/db/adapters/sqljsAdapter.js +115 -0
- package/src/lib/db/backup.js +35 -0
- package/src/lib/db/driver.js +85 -0
- package/src/lib/db/helpers/jsonCol.js +9 -0
- package/src/lib/db/helpers/kvStore.js +39 -0
- package/src/lib/db/helpers/metaStore.js +22 -0
- package/src/lib/db/index.js +171 -0
- package/src/lib/db/migrate.js +286 -0
- package/src/lib/db/migrations/001-initial.js +14 -0
- package/src/lib/db/migrations/index.js +10 -0
- package/src/lib/db/paths.js +18 -0
- package/src/lib/db/repos/aliasRepo.js +62 -0
- package/src/lib/db/repos/apiKeysRepo.js +75 -0
- package/src/lib/db/repos/combosRepo.js +73 -0
- package/src/lib/db/repos/connectionsRepo.js +226 -0
- package/src/lib/db/repos/disabledModelsRepo.js +56 -0
- package/src/lib/db/repos/nodesRepo.js +95 -0
- package/src/lib/db/repos/pricingRepo.js +108 -0
- package/src/lib/db/repos/proxyPoolsRepo.js +103 -0
- package/src/lib/db/repos/requestDetailsRepo.js +259 -0
- package/src/lib/db/repos/settingsRepo.js +104 -0
- package/src/lib/db/repos/usageRepo.js +731 -0
- package/src/lib/db/schema.js +157 -0
- package/src/lib/db/version.js +21 -0
- package/src/lib/disabledModelsDb.js +4 -0
- package/src/lib/localDb.js +21 -0
- package/src/lib/mcp/stdioSseBridge.js +198 -0
- package/src/lib/mitmAliasCache.js +46 -0
- package/src/lib/network/connectionProxy.js +160 -0
- package/src/lib/network/initOutboundProxy.js +25 -0
- package/src/lib/network/outboundProxy.js +68 -0
- package/src/lib/network/proxyTest.js +91 -0
- package/src/lib/oauth/constants/oauth.js +284 -0
- package/src/lib/oauth/constants/xai.js +61 -0
- package/src/lib/oauth/providers.js +1506 -0
- package/src/lib/oauth/services/antigravity.js +321 -0
- package/src/lib/oauth/services/claude.js +136 -0
- package/src/lib/oauth/services/codebuddyBulkImportManager.js +454 -0
- package/src/lib/oauth/services/codex.js +144 -0
- package/src/lib/oauth/services/cursor.js +179 -0
- package/src/lib/oauth/services/gemini.js +240 -0
- package/src/lib/oauth/services/github.js +225 -0
- package/src/lib/oauth/services/iflow.js +202 -0
- package/src/lib/oauth/services/index.js +17 -0
- package/src/lib/oauth/services/kiro.js +334 -0
- package/src/lib/oauth/services/kiroBulkImportManager.js +778 -0
- package/src/lib/oauth/services/kiroConnections.js +93 -0
- package/src/lib/oauth/services/kiroGoogleAutomation.js +1136 -0
- package/src/lib/oauth/services/oauth.js +157 -0
- package/src/lib/oauth/services/openai.js +123 -0
- package/src/lib/oauth/services/qoder.js +216 -0
- package/src/lib/oauth/services/qwen.js +170 -0
- package/src/lib/oauth/services/xai.js +238 -0
- package/src/lib/oauth/utils/banner.js +63 -0
- package/src/lib/oauth/utils/pkce.js +39 -0
- package/src/lib/oauth/utils/server.js +415 -0
- package/src/lib/oauth/utils/ui.js +48 -0
- package/src/lib/providerNormalization.js +45 -0
- package/src/lib/qoder/constants.js +64 -0
- package/src/lib/qoder/cosy.js +175 -0
- package/src/lib/qoder/encoding.js +55 -0
- package/src/lib/requestDetailsDb.js +4 -0
- package/src/lib/tunnel/cloudflare/cloudflared.js +449 -0
- package/src/lib/tunnel/cloudflare/config.js +9 -0
- package/src/lib/tunnel/cloudflare/healthCheck.js +29 -0
- package/src/lib/tunnel/cloudflare/manager.js +151 -0
- package/src/lib/tunnel/cloudflare/pid.js +23 -0
- package/src/lib/tunnel/index.js +48 -0
- package/src/lib/tunnel/shared/dnsResolver.js +17 -0
- package/src/lib/tunnel/shared/internetCheck.js +26 -0
- package/src/lib/tunnel/shared/state.js +41 -0
- package/src/lib/tunnel/shared/watchdogConfig.js +8 -0
- package/src/lib/tunnel/tailscale/config.js +7 -0
- package/src/lib/tunnel/tailscale/healthCheck.js +29 -0
- package/src/lib/tunnel/tailscale/manager.js +129 -0
- package/src/lib/tunnel/tailscale/tailscale.js +790 -0
- package/src/lib/updater/updater.js +235 -0
- package/src/lib/usage/fetcher.js +208 -0
- package/src/lib/usageDb.js +7 -0
- package/src/mitm/antigravityIdeVersion.js +50 -0
- package/src/mitm/cert/generate.js +32 -0
- package/src/mitm/cert/install.js +269 -0
- package/src/mitm/cert/rootCA.js +173 -0
- package/src/mitm/config.js +87 -0
- package/src/mitm/dbReader.js +22 -0
- package/src/mitm/dns/dnsConfig.js +266 -0
- package/src/mitm/handlers/antigravity.js +33 -0
- package/src/mitm/handlers/base.js +226 -0
- package/src/mitm/handlers/copilot.js +35 -0
- package/src/mitm/handlers/cursor.js +15 -0
- package/src/mitm/handlers/kiro.js +526 -0
- package/src/mitm/logger.js +106 -0
- package/src/mitm/manager.js +851 -0
- package/src/mitm/paths.js +32 -0
- package/src/mitm/server.js +435 -0
- package/src/mitm/winElevated.js +81 -0
- package/src/models/index.js +38 -0
- package/src/proxy.js +5 -0
- package/src/shared/components/AddCustomEmbeddingModal.js +183 -0
- package/src/shared/components/Avatar.js +88 -0
- package/src/shared/components/Badge.js +54 -0
- package/src/shared/components/BulkAccountAutomationModal.js +508 -0
- package/src/shared/components/Button.js +56 -0
- package/src/shared/components/Card.js +116 -0
- package/src/shared/components/ChangelogModal.js +97 -0
- package/src/shared/components/CodeBuddyQuotaCookieModal.js +109 -0
- package/src/shared/components/ComboFormModal.js +176 -0
- package/src/shared/components/CursorAuthModal.js +212 -0
- package/src/shared/components/DonateModal.js +136 -0
- package/src/shared/components/Drawer.js +82 -0
- package/src/shared/components/EditConnectionModal.js +286 -0
- package/src/shared/components/Footer.js +132 -0
- package/src/shared/components/GitLabAuthModal.js +194 -0
- package/src/shared/components/Header.js +380 -0
- package/src/shared/components/HeaderLanguage.js +46 -0
- package/src/shared/components/HeaderMenu.js +126 -0
- package/src/shared/components/IFlowCookieModal.js +132 -0
- package/src/shared/components/Input.js +65 -0
- package/src/shared/components/KiroAuthModal.js +1171 -0
- package/src/shared/components/KiroOAuthWrapper.js +149 -0
- package/src/shared/components/KiroSocialOAuthModal.js +205 -0
- package/src/shared/components/LanguageSwitcher.js +190 -0
- package/src/shared/components/Loading.js +75 -0
- package/src/shared/components/ManualConfigModal.js +44 -0
- package/src/shared/components/McpMarketplaceModal.js +255 -0
- package/src/shared/components/Modal.js +146 -0
- package/src/shared/components/ModelSelectModal.js +537 -0
- package/src/shared/components/NineRemoteButton.js +23 -0
- package/src/shared/components/NineRemotePromoModal.js +99 -0
- package/src/shared/components/NoAuthProxyCard.js +86 -0
- package/src/shared/components/OAuthModal.js +682 -0
- package/src/shared/components/Pagination.js +150 -0
- package/src/shared/components/PricingModal.js +208 -0
- package/src/shared/components/ProviderIcon.js +63 -0
- package/src/shared/components/ProviderInfoCard.js +82 -0
- package/src/shared/components/RequestLogger.js +121 -0
- package/src/shared/components/SegmentedControl.js +48 -0
- package/src/shared/components/Select.js +67 -0
- package/src/shared/components/Sidebar.js +441 -0
- package/src/shared/components/ThemeProvider.js +15 -0
- package/src/shared/components/ThemeToggle.js +42 -0
- package/src/shared/components/Toggle.js +69 -0
- package/src/shared/components/Tooltip.js +25 -0
- package/src/shared/components/UsageStats.js +505 -0
- package/src/shared/components/index.js +46 -0
- package/src/shared/components/layouts/AuthLayout.js +29 -0
- package/src/shared/components/layouts/DashboardLayout.js +104 -0
- package/src/shared/components/layouts/index.js +4 -0
- package/src/shared/constants/cliTools.js +397 -0
- package/src/shared/constants/colors.js +77 -0
- package/src/shared/constants/config.js +99 -0
- package/src/shared/constants/coworkPlugins.js +75 -0
- package/src/shared/constants/index.js +4 -0
- package/src/shared/constants/locales.js +36 -0
- package/src/shared/constants/mitmToolHosts.js +12 -0
- package/src/shared/constants/models.js +38 -0
- package/src/shared/constants/pricing.js +303 -0
- package/src/shared/constants/providers.js +289 -0
- package/src/shared/constants/skills.js +78 -0
- package/src/shared/constants/ttsProviders.js +138 -0
- package/src/shared/hooks/index.js +2 -0
- package/src/shared/hooks/useCopyToClipboard.js +43 -0
- package/src/shared/hooks/useTheme.js +60 -0
- package/src/shared/services/bootstrap.js +12 -0
- package/src/shared/services/initializeApp.js +268 -0
- package/src/shared/utils/api.js +93 -0
- package/src/shared/utils/apiKey.js +98 -0
- package/src/shared/utils/clineAuth.js +37 -0
- package/src/shared/utils/cn.js +11 -0
- package/src/shared/utils/connectionStatus.js +162 -0
- package/src/shared/utils/index.js +40 -0
- package/src/shared/utils/machine.js +6 -0
- package/src/shared/utils/machineId.js +66 -0
- package/src/shared/utils/providerModelsFetcher.js +30 -0
- package/src/sse/handlers/chat.js +261 -0
- package/src/sse/handlers/embeddings.js +141 -0
- package/src/sse/handlers/fetch.js +213 -0
- package/src/sse/handlers/imageGeneration.js +142 -0
- package/src/sse/handlers/search.js +206 -0
- package/src/sse/handlers/stt.js +88 -0
- package/src/sse/handlers/tts.js +114 -0
- package/src/sse/services/auth.js +346 -0
- package/src/sse/services/codexGateway.js +215 -0
- package/src/sse/services/model.js +99 -0
- package/src/sse/services/tokenRefresh.js +319 -0
- package/src/sse/utils/logger.js +75 -0
- package/src/store/headerSearchStore.js +19 -0
- package/src/store/index.js +6 -0
- package/src/store/notificationStore.js +45 -0
- package/src/store/providerStore.js +55 -0
- package/src/store/settingsStore.js +51 -0
- package/src/store/themeStore.js +54 -0
- package/src/store/userStore.js +20 -0
- package/start.sh +4 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
import { execSync, exec, spawn } from "child_process";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
import { execWithPassword } from "@/mitm/dns/dnsConfig";
|
|
8
|
+
import { DATA_DIR } from "@/lib/dataDir.js";
|
|
9
|
+
|
|
10
|
+
const execAsync = promisify(exec);
|
|
11
|
+
|
|
12
|
+
const BIN_DIR = path.join(DATA_DIR, "bin");
|
|
13
|
+
const IS_MAC = os.platform() === "darwin";
|
|
14
|
+
const IS_LINUX = os.platform() === "linux";
|
|
15
|
+
const IS_WINDOWS = os.platform() === "win32";
|
|
16
|
+
const TAILSCALE_BIN = path.join(BIN_DIR, IS_WINDOWS ? "tailscale.exe" : "tailscale");
|
|
17
|
+
|
|
18
|
+
// Custom socket for userspace-networking mode (no root required)
|
|
19
|
+
const TAILSCALE_DIR = path.join(DATA_DIR, "tailscale");
|
|
20
|
+
export const TAILSCALE_SOCKET = path.join(TAILSCALE_DIR, "tailscaled.sock");
|
|
21
|
+
const SOCKET_FLAG = IS_WINDOWS ? [] : ["--socket", TAILSCALE_SOCKET];
|
|
22
|
+
|
|
23
|
+
// Well-known Windows install path
|
|
24
|
+
const WINDOWS_TAILSCALE_BIN = "C:\\Program Files\\Tailscale\\tailscale.exe";
|
|
25
|
+
|
|
26
|
+
// Common Unix install paths to probe synchronously (system tailscale)
|
|
27
|
+
const UNIX_TAILSCALE_CANDIDATES = [
|
|
28
|
+
"/usr/local/bin/tailscale",
|
|
29
|
+
"/opt/homebrew/bin/tailscale",
|
|
30
|
+
"/usr/bin/tailscale",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// ─── Cache + background refresh (avoid blocking event loop on dead daemon) ──
|
|
34
|
+
const PROBE_TTL_MS = 10000;
|
|
35
|
+
const PROBE_TIMEOUT_MS = 1500;
|
|
36
|
+
|
|
37
|
+
const binCache = { value: undefined, fetchedAt: 0, refreshing: false };
|
|
38
|
+
const runningCache = { value: false, fetchedAt: 0, refreshing: false };
|
|
39
|
+
const funnelUrlCache = { value: null, port: null, fetchedAt: 0, refreshing: false };
|
|
40
|
+
|
|
41
|
+
function fallbackBin() {
|
|
42
|
+
if (fs.existsSync(TAILSCALE_BIN)) return TAILSCALE_BIN;
|
|
43
|
+
if (IS_WINDOWS && fs.existsSync(WINDOWS_TAILSCALE_BIN)) return WINDOWS_TAILSCALE_BIN;
|
|
44
|
+
if (!IS_WINDOWS) return UNIX_TAILSCALE_CANDIDATES.find((p) => fs.existsSync(p)) || null;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function bgRefreshBin() {
|
|
49
|
+
if (binCache.refreshing) return;
|
|
50
|
+
binCache.refreshing = true;
|
|
51
|
+
const cmd = IS_WINDOWS ? "where tailscale 2>nul" : "which tailscale 2>/dev/null";
|
|
52
|
+
execAsync(cmd, { windowsHide: true, timeout: PROBE_TIMEOUT_MS })
|
|
53
|
+
.then(({ stdout }) => {
|
|
54
|
+
const sys = stdout.trim();
|
|
55
|
+
binCache.value = sys || fallbackBin();
|
|
56
|
+
})
|
|
57
|
+
.catch(() => { binCache.value = fallbackBin(); })
|
|
58
|
+
.finally(() => {
|
|
59
|
+
binCache.fetchedAt = Date.now();
|
|
60
|
+
binCache.refreshing = false;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Sync getter: returns cached value, triggers background refresh if stale
|
|
65
|
+
function getTailscaleBin() {
|
|
66
|
+
if (Date.now() - binCache.fetchedAt > PROBE_TTL_MS) bgRefreshBin();
|
|
67
|
+
// First call: synchronously probe common install paths (no exec, no event-loop block)
|
|
68
|
+
if (binCache.value === undefined) {
|
|
69
|
+
if (fs.existsSync(TAILSCALE_BIN)) binCache.value = TAILSCALE_BIN;
|
|
70
|
+
else if (IS_WINDOWS && fs.existsSync(WINDOWS_TAILSCALE_BIN)) binCache.value = WINDOWS_TAILSCALE_BIN;
|
|
71
|
+
else if (!IS_WINDOWS) {
|
|
72
|
+
const found = UNIX_TAILSCALE_CANDIDATES.find((p) => fs.existsSync(p));
|
|
73
|
+
binCache.value = found || null;
|
|
74
|
+
} else binCache.value = null;
|
|
75
|
+
}
|
|
76
|
+
return binCache.value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isTailscaleInstalled() {
|
|
80
|
+
return getTailscaleBin() !== null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Build tailscale CLI args with custom socket (no root needed) */
|
|
84
|
+
function tsArgs(...args) {
|
|
85
|
+
return [...SOCKET_FLAG, ...args];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isTailscaleLoggedIn() {
|
|
89
|
+
const bin = getTailscaleBin();
|
|
90
|
+
if (!bin) return false;
|
|
91
|
+
try {
|
|
92
|
+
const out = execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} status --json`, {
|
|
93
|
+
encoding: "utf8",
|
|
94
|
+
windowsHide: true,
|
|
95
|
+
env: { ...process.env, PATH: EXTENDED_PATH },
|
|
96
|
+
timeout: 5000
|
|
97
|
+
});
|
|
98
|
+
const json = JSON.parse(out);
|
|
99
|
+
// BackendState=Running + Self.Online=true → device still exists in tailnet
|
|
100
|
+
return json.BackendState === "Running" && json.Self?.Online === true;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function bgRefreshRunning() {
|
|
107
|
+
if (runningCache.refreshing) return;
|
|
108
|
+
const bin = getTailscaleBin();
|
|
109
|
+
if (!bin) {
|
|
110
|
+
runningCache.value = false;
|
|
111
|
+
runningCache.fetchedAt = Date.now();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
runningCache.refreshing = true;
|
|
115
|
+
execAsync(`"${bin}" ${SOCKET_FLAG.join(" ")} funnel status --json`, { windowsHide: true, timeout: PROBE_TIMEOUT_MS })
|
|
116
|
+
.then(({ stdout }) => {
|
|
117
|
+
try {
|
|
118
|
+
const json = JSON.parse(stdout);
|
|
119
|
+
runningCache.value = Object.keys(json.AllowFunnel || {}).length > 0;
|
|
120
|
+
} catch { runningCache.value = false; }
|
|
121
|
+
})
|
|
122
|
+
.catch(() => { runningCache.value = false; })
|
|
123
|
+
.finally(() => {
|
|
124
|
+
runningCache.fetchedAt = Date.now();
|
|
125
|
+
runningCache.refreshing = false;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Sync getter: never blocks; returns last known state, refreshes in background
|
|
130
|
+
export function isTailscaleRunning() {
|
|
131
|
+
if (Date.now() - runningCache.fetchedAt > PROBE_TTL_MS) bgRefreshRunning();
|
|
132
|
+
return runningCache.value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Synchronous strict probe for hot user-initiated paths (enable/connect flow).
|
|
136
|
+
// Blocks ~PROBE_TIMEOUT_MS at most; updates cache as a side effect.
|
|
137
|
+
export function isTailscaleRunningStrict() {
|
|
138
|
+
const bin = getTailscaleBin();
|
|
139
|
+
if (!bin) return false;
|
|
140
|
+
try {
|
|
141
|
+
const out = execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} funnel status --json`, {
|
|
142
|
+
encoding: "utf8",
|
|
143
|
+
windowsHide: true,
|
|
144
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
145
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
146
|
+
});
|
|
147
|
+
const json = JSON.parse(out);
|
|
148
|
+
const running = Object.keys(json.AllowFunnel || {}).length > 0;
|
|
149
|
+
runningCache.value = running;
|
|
150
|
+
runningCache.fetchedAt = Date.now();
|
|
151
|
+
return running;
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function bgRefreshFunnelUrl(port) {
|
|
158
|
+
if (funnelUrlCache.refreshing) return;
|
|
159
|
+
const bin = getTailscaleBin();
|
|
160
|
+
if (!bin) return;
|
|
161
|
+
funnelUrlCache.refreshing = true;
|
|
162
|
+
execAsync(`"${bin}" ${SOCKET_FLAG.join(" ")} status --json`, { windowsHide: true, timeout: PROBE_TIMEOUT_MS })
|
|
163
|
+
.then(({ stdout }) => {
|
|
164
|
+
try {
|
|
165
|
+
const json = JSON.parse(stdout);
|
|
166
|
+
const dnsName = json.Self?.DNSName?.replace(/\.$/, "");
|
|
167
|
+
funnelUrlCache.value = dnsName ? `https://${dnsName}` : null;
|
|
168
|
+
} catch { /* keep prev */ }
|
|
169
|
+
})
|
|
170
|
+
.catch(() => { /* keep prev */ })
|
|
171
|
+
.finally(() => {
|
|
172
|
+
funnelUrlCache.port = port;
|
|
173
|
+
funnelUrlCache.fetchedAt = Date.now();
|
|
174
|
+
funnelUrlCache.refreshing = false;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Get actual funnel URL from Self.DNSName (sync, authoritative — avoids hostname-conflict suffix). */
|
|
179
|
+
function getActualFunnelUrl() {
|
|
180
|
+
const bin = getTailscaleBin();
|
|
181
|
+
if (!bin) return null;
|
|
182
|
+
try {
|
|
183
|
+
const out = execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} status --json`, {
|
|
184
|
+
encoding: "utf8",
|
|
185
|
+
windowsHide: true,
|
|
186
|
+
env: { ...process.env, PATH: EXTENDED_PATH },
|
|
187
|
+
timeout: 5000,
|
|
188
|
+
});
|
|
189
|
+
const json = JSON.parse(out);
|
|
190
|
+
const dnsName = json.Self?.DNSName?.replace(/\.$/, "");
|
|
191
|
+
return dnsName ? `https://${dnsName}` : null;
|
|
192
|
+
} catch { return null; }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Get funnel URL from tailscale status (cached, non-blocking) */
|
|
196
|
+
export function getTailscaleFunnelUrl(port) {
|
|
197
|
+
if (Date.now() - funnelUrlCache.fetchedAt > PROBE_TTL_MS || funnelUrlCache.port !== port) {
|
|
198
|
+
bgRefreshFunnelUrl(port);
|
|
199
|
+
}
|
|
200
|
+
return funnelUrlCache.value;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Install tailscale.
|
|
205
|
+
* - macOS + brew: brew install tailscale (no sudo needed)
|
|
206
|
+
* - macOS no brew: download .pkg then sudo installer -pkg
|
|
207
|
+
* - Linux: fetch install.sh, pipe to sudo -S sh via stdin
|
|
208
|
+
* - Windows: download MSI via UAC-elevated PowerShell
|
|
209
|
+
*/
|
|
210
|
+
export async function installTailscale(sudoPassword, hostname, onProgress) {
|
|
211
|
+
const log = onProgress || (() => {});
|
|
212
|
+
if (IS_WINDOWS) {
|
|
213
|
+
await installTailscaleWindows(log);
|
|
214
|
+
return { success: true };
|
|
215
|
+
}
|
|
216
|
+
if (IS_MAC) await installTailscaleMac(sudoPassword, log);
|
|
217
|
+
else await installTailscaleLinux(sudoPassword, log);
|
|
218
|
+
|
|
219
|
+
log("Starting daemon...");
|
|
220
|
+
await startDaemonWithPassword(sudoPassword);
|
|
221
|
+
log("Logging in...");
|
|
222
|
+
return startLogin(hostname);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const EXTENDED_PATH = `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${process.env.PATH || ""}`;
|
|
226
|
+
|
|
227
|
+
function hasBrew() {
|
|
228
|
+
try { execSync("which brew", { stdio: "ignore", windowsHide: true, env: { ...process.env, PATH: EXTENDED_PATH } }); return true; } catch { return false; }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function installTailscaleMac(sudoPassword, log) {
|
|
232
|
+
if (hasBrew()) {
|
|
233
|
+
log("Installing via Homebrew...");
|
|
234
|
+
await new Promise((resolve, reject) => {
|
|
235
|
+
const child = spawn("brew", ["install", "tailscale"], {
|
|
236
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
237
|
+
windowsHide: true,
|
|
238
|
+
env: { ...process.env, PATH: EXTENDED_PATH }
|
|
239
|
+
});
|
|
240
|
+
child.stdout.on("data", (d) => {
|
|
241
|
+
const line = d.toString().trim();
|
|
242
|
+
if (line) log(line);
|
|
243
|
+
});
|
|
244
|
+
child.stderr.on("data", (d) => {
|
|
245
|
+
const line = d.toString().trim();
|
|
246
|
+
if (line) log(line);
|
|
247
|
+
});
|
|
248
|
+
child.on("close", (c) => {
|
|
249
|
+
if (c === 0) resolve();
|
|
250
|
+
else reject(new Error(`brew install failed (code ${c})`));
|
|
251
|
+
});
|
|
252
|
+
child.on("error", reject);
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// No brew: download .pkg and install via sudo installer
|
|
258
|
+
const pkgUrl = "https://pkgs.tailscale.com/stable/tailscale-latest.pkg";
|
|
259
|
+
const pkgPath = path.join(os.tmpdir(), "tailscale.pkg");
|
|
260
|
+
|
|
261
|
+
log("Downloading Tailscale package...");
|
|
262
|
+
await new Promise((resolve, reject) => {
|
|
263
|
+
const child = spawn("curl", ["-fL", "--progress-bar", pkgUrl, "-o", pkgPath], {
|
|
264
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
265
|
+
windowsHide: true
|
|
266
|
+
});
|
|
267
|
+
child.stderr.on("data", (d) => {
|
|
268
|
+
const line = d.toString().trim();
|
|
269
|
+
if (line) log(line);
|
|
270
|
+
});
|
|
271
|
+
child.on("close", (c) => {
|
|
272
|
+
if (c === 0) resolve();
|
|
273
|
+
else reject(new Error("Download failed"));
|
|
274
|
+
});
|
|
275
|
+
child.on("error", reject);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
log("Installing package...");
|
|
279
|
+
await new Promise((resolve, reject) => {
|
|
280
|
+
const child = spawn("sudo", ["-S", "installer", "-pkg", pkgPath, "-target", "/"], {
|
|
281
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
282
|
+
windowsHide: true
|
|
283
|
+
});
|
|
284
|
+
let stderr = "";
|
|
285
|
+
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
286
|
+
child.stdout.on("data", (d) => {
|
|
287
|
+
const line = d.toString().trim();
|
|
288
|
+
if (line) log(line);
|
|
289
|
+
});
|
|
290
|
+
child.on("close", (c) => {
|
|
291
|
+
try { execSync(`rm -f ${pkgPath}`, { stdio: "ignore", windowsHide: true }); } catch { /* ignore */ }
|
|
292
|
+
if (c === 0) resolve();
|
|
293
|
+
else {
|
|
294
|
+
const msg = (stderr.includes("incorrect password") || stderr.includes("Sorry"))
|
|
295
|
+
? "Wrong sudo password"
|
|
296
|
+
: stderr || `Exit code ${c}`;
|
|
297
|
+
reject(new Error(msg));
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
child.on("error", reject);
|
|
301
|
+
child.stdin.write(`${sudoPassword}\n`);
|
|
302
|
+
child.stdin.end();
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function installTailscaleLinux(sudoPassword, log) {
|
|
307
|
+
// Reject password containing newline → prevents stdin command injection
|
|
308
|
+
if (typeof sudoPassword !== "string" || sudoPassword.includes("\n")) {
|
|
309
|
+
throw new Error("Invalid sudo password");
|
|
310
|
+
}
|
|
311
|
+
log("Downloading install script...");
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
const curlChild = spawn("curl", ["-fsSL", "https://tailscale.com/install.sh"], {
|
|
314
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
315
|
+
windowsHide: true
|
|
316
|
+
});
|
|
317
|
+
let scriptContent = "";
|
|
318
|
+
let curlErr = "";
|
|
319
|
+
curlChild.stdout.on("data", (d) => { scriptContent += d.toString(); });
|
|
320
|
+
curlChild.stderr.on("data", (d) => { curlErr += d.toString(); });
|
|
321
|
+
curlChild.on("exit", (code) => {
|
|
322
|
+
if (code !== 0) return reject(new Error(`Failed to download install script: ${curlErr}`));
|
|
323
|
+
log("Running install script...");
|
|
324
|
+
// Persist script to temp file → exec by path (NOT via stdin) → sh never reads attacker-controlled stdin
|
|
325
|
+
const tmpScript = path.join(os.tmpdir(), `tailscale-install-${crypto.randomBytes(8).toString("hex")}.sh`);
|
|
326
|
+
try {
|
|
327
|
+
fs.writeFileSync(tmpScript, scriptContent, { mode: 0o700 });
|
|
328
|
+
} catch (e) {
|
|
329
|
+
return reject(new Error(`Failed to write install script: ${e.message}`));
|
|
330
|
+
}
|
|
331
|
+
const cleanup = () => { try { fs.unlinkSync(tmpScript); } catch {} };
|
|
332
|
+
const child = spawn("sudo", ["-S", "sh", tmpScript], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
333
|
+
let stderr = "";
|
|
334
|
+
child.stdout.on("data", (d) => {
|
|
335
|
+
const line = d.toString().trim();
|
|
336
|
+
if (line) log(line);
|
|
337
|
+
});
|
|
338
|
+
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
339
|
+
child.on("close", (c) => {
|
|
340
|
+
cleanup();
|
|
341
|
+
if (c === 0) resolve();
|
|
342
|
+
else {
|
|
343
|
+
const msg = (stderr.includes("incorrect password") || stderr.includes("Sorry"))
|
|
344
|
+
? "Wrong sudo password"
|
|
345
|
+
: stderr || `Exit code ${c}`;
|
|
346
|
+
reject(new Error(msg));
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
child.on("error", (e) => { cleanup(); reject(e); });
|
|
350
|
+
child.stdin.write(`${sudoPassword}\n`);
|
|
351
|
+
child.stdin.end();
|
|
352
|
+
});
|
|
353
|
+
curlChild.on("error", reject);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function installTailscaleWindows(log) {
|
|
358
|
+
const msiUrl = "https://pkgs.tailscale.com/stable/tailscale-setup-latest-amd64.msi";
|
|
359
|
+
const msiPath = path.join(os.tmpdir(), "tailscale-setup.msi");
|
|
360
|
+
|
|
361
|
+
// Download MSI via curl.exe (built-in on Win10+) — no PowerShell window, streams progress
|
|
362
|
+
log("Downloading Tailscale installer...");
|
|
363
|
+
await new Promise((resolve, reject) => {
|
|
364
|
+
const child = spawn("curl.exe", ["-L", "-#", "-o", msiPath, msiUrl], {
|
|
365
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
366
|
+
windowsHide: true
|
|
367
|
+
});
|
|
368
|
+
// curl outputs progress to stderr with -# flag
|
|
369
|
+
let lastPct = "";
|
|
370
|
+
child.stderr.on("data", (d) => {
|
|
371
|
+
const text = d.toString();
|
|
372
|
+
const match = text.match(/(\d+\.\d)%/);
|
|
373
|
+
if (match && match[1] !== lastPct) {
|
|
374
|
+
lastPct = match[1];
|
|
375
|
+
log(`Downloading... ${lastPct}%`);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
child.on("close", (c) => c === 0 ? resolve() : reject(new Error("Download failed")));
|
|
379
|
+
child.on("error", reject);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Install MSI with UAC elevation via PowerShell Start-Process -Verb RunAs
|
|
383
|
+
log("Installing Tailscale (UAC prompt may appear)...");
|
|
384
|
+
await new Promise((resolve, reject) => {
|
|
385
|
+
const args = `'/i','${msiPath}','TS_NOLAUNCH=true','/quiet','/norestart'`;
|
|
386
|
+
const child = spawn("powershell", [
|
|
387
|
+
"-NoProfile", "-NonInteractive", "-Command",
|
|
388
|
+
`Start-Process msiexec -ArgumentList ${args} -Verb RunAs -Wait`
|
|
389
|
+
], { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
390
|
+
child.stderr.on("data", (d) => { const l = d.toString().trim(); if (l) log(l); });
|
|
391
|
+
child.on("close", (c) => {
|
|
392
|
+
try { fs.unlinkSync(msiPath); } catch { /* ignore */ }
|
|
393
|
+
c === 0 ? resolve() : reject(new Error(`msiexec failed (code ${c})`));
|
|
394
|
+
});
|
|
395
|
+
child.on("error", reject);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Verify tailscale.exe exists after install
|
|
399
|
+
log("Verifying installation...");
|
|
400
|
+
const maxWait = 10000;
|
|
401
|
+
const start = Date.now();
|
|
402
|
+
while (Date.now() - start < maxWait) {
|
|
403
|
+
if (fs.existsSync(WINDOWS_TAILSCALE_BIN)) {
|
|
404
|
+
log("Installation complete.");
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
408
|
+
}
|
|
409
|
+
throw new Error("Installation finished but tailscale.exe not found");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Self-heal: if state dir/files were previously created by root (e.g. legacy sudo daemon),
|
|
413
|
+
// reclaim ownership recursively so the user-mode daemon can read/write state files.
|
|
414
|
+
async function ensureUserOwnedDir(dir) {
|
|
415
|
+
try {
|
|
416
|
+
if (!fs.existsSync(dir)) {
|
|
417
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const uid = process.getuid();
|
|
421
|
+
const gid = process.getgid();
|
|
422
|
+
|
|
423
|
+
// Walk dir + all entries to find any non-user-owned items
|
|
424
|
+
const needsChown = (() => {
|
|
425
|
+
const stack = [dir];
|
|
426
|
+
while (stack.length) {
|
|
427
|
+
const cur = stack.pop();
|
|
428
|
+
try {
|
|
429
|
+
const st = fs.statSync(cur);
|
|
430
|
+
if (st.uid !== uid) return true;
|
|
431
|
+
if (st.isDirectory()) {
|
|
432
|
+
for (const name of fs.readdirSync(cur)) stack.push(path.join(cur, name));
|
|
433
|
+
}
|
|
434
|
+
} catch { /* ignore */ }
|
|
435
|
+
}
|
|
436
|
+
return false;
|
|
437
|
+
})();
|
|
438
|
+
|
|
439
|
+
if (!needsChown) return;
|
|
440
|
+
|
|
441
|
+
// Try direct chown first (works if already owned). Fallback to passwordless sudo.
|
|
442
|
+
try {
|
|
443
|
+
execSync(`chown -R ${uid}:${gid} "${dir}"`, { stdio: "ignore", timeout: 3000 });
|
|
444
|
+
} catch {
|
|
445
|
+
try { execSync(`sudo -n chown -R ${uid}:${gid} "${dir}"`, { stdio: "ignore", timeout: 3000 }); } catch { /* ignore */ }
|
|
446
|
+
}
|
|
447
|
+
} catch { /* ignore */ }
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Check if running daemon uses TUN mode (Funnel TLS requires TUN). */
|
|
451
|
+
function isDaemonTunMode() {
|
|
452
|
+
try {
|
|
453
|
+
const ps = execSync(`pgrep -af "tailscaled.*${TAILSCALE_SOCKET}"`, { encoding: "utf8", timeout: 2000 }).trim();
|
|
454
|
+
if (!ps) return null;
|
|
455
|
+
return !ps.includes("--tun=userspace-networking");
|
|
456
|
+
} catch { return null; }
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Start tailscaled.
|
|
461
|
+
* - With sudoPassword: TUN mode (root) → Funnel TLS works
|
|
462
|
+
* - Without: userspace-networking fallback (no sudo, but Funnel TLS unstable)
|
|
463
|
+
* State always lives in ~/.9router/tailscale/ via --statedir.
|
|
464
|
+
*/
|
|
465
|
+
export async function startDaemonWithPassword(sudoPassword) {
|
|
466
|
+
if (IS_WINDOWS) {
|
|
467
|
+
// Windows: tailscale runs as a Windows Service. Start it then poll BackendState
|
|
468
|
+
// until daemon finishes init (avoids "NoState" errors when calling funnel/up too early).
|
|
469
|
+
const bin = getTailscaleBin();
|
|
470
|
+
console.log("[Tailscale] win: net start Tailscale");
|
|
471
|
+
try { execSync("net start Tailscale", { stdio: "ignore", windowsHide: true, timeout: 10000 }); }
|
|
472
|
+
catch { /* may need admin, or already running */ }
|
|
473
|
+
if (!bin) return;
|
|
474
|
+
// Poll up to ~10s for backend to leave NoState
|
|
475
|
+
for (let i = 0; i < 20; i++) {
|
|
476
|
+
try {
|
|
477
|
+
const out = execSync(`"${bin}" status --json`, { encoding: "utf8", windowsHide: true, timeout: 2000 });
|
|
478
|
+
const j = JSON.parse(out);
|
|
479
|
+
if (j.BackendState && j.BackendState !== "NoState") {
|
|
480
|
+
console.log(`[Tailscale] win: BackendState=${j.BackendState} after ${i*500}ms`);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
} catch { /* daemon not ready */ }
|
|
484
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
485
|
+
}
|
|
486
|
+
console.log("[Tailscale] win: BackendState still NoState after poll");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const wantTun = !!sudoPassword;
|
|
491
|
+
const currentMode = isDaemonTunMode(); // true=TUN, false=userspace, null=not running
|
|
492
|
+
|
|
493
|
+
// Daemon already running in correct mode → reuse
|
|
494
|
+
if (currentMode !== null && currentMode === wantTun) {
|
|
495
|
+
try {
|
|
496
|
+
const bin = getTailscaleBin() || "tailscale";
|
|
497
|
+
execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} status --json`, {
|
|
498
|
+
stdio: "ignore", windowsHide: true,
|
|
499
|
+
env: { ...process.env, PATH: EXTENDED_PATH }, timeout: 3000
|
|
500
|
+
});
|
|
501
|
+
return;
|
|
502
|
+
} catch { /* unresponsive, restart below */ }
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Mode mismatch or unresponsive → kill all daemons on our socket
|
|
506
|
+
try { execSync(`pkill -9 -f "tailscaled.*${TAILSCALE_SOCKET}"`, { stdio: "ignore", timeout: 3000 }); } catch { /* ignore */ }
|
|
507
|
+
if (sudoPassword) {
|
|
508
|
+
try { await execWithPassword(`pkill -9 -f "tailscaled.*${TAILSCALE_SOCKET}"`, sudoPassword); } catch { /* ignore */ }
|
|
509
|
+
} else {
|
|
510
|
+
try { execSync(`sudo -n pkill -9 -f "tailscaled.*${TAILSCALE_SOCKET}"`, { stdio: "ignore", timeout: 3000 }); } catch { /* ignore */ }
|
|
511
|
+
}
|
|
512
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
513
|
+
|
|
514
|
+
// Reclaim folder ownership (previous root daemon may have locked it)
|
|
515
|
+
await ensureUserOwnedDir(TAILSCALE_DIR);
|
|
516
|
+
|
|
517
|
+
const tailscaledBin = IS_MAC ? "/usr/local/bin/tailscaled" : "tailscaled";
|
|
518
|
+
const daemonArgs = [
|
|
519
|
+
`--socket=${TAILSCALE_SOCKET}`,
|
|
520
|
+
`--statedir=${TAILSCALE_DIR}`,
|
|
521
|
+
];
|
|
522
|
+
if (!wantTun) daemonArgs.push("--tun=userspace-networking");
|
|
523
|
+
|
|
524
|
+
if (wantTun) {
|
|
525
|
+
// TUN mode: spawn via sudo, password via stdin. Detached so it survives parent exit.
|
|
526
|
+
const child = spawn("sudo", ["-S", tailscaledBin, ...daemonArgs], {
|
|
527
|
+
detached: true,
|
|
528
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
529
|
+
cwd: os.tmpdir(),
|
|
530
|
+
env: { ...process.env, PATH: EXTENDED_PATH },
|
|
531
|
+
});
|
|
532
|
+
child.stdin.write(`${sudoPassword}\n`);
|
|
533
|
+
child.stdin.end();
|
|
534
|
+
child.unref();
|
|
535
|
+
} else {
|
|
536
|
+
const child = spawn(tailscaledBin, daemonArgs, {
|
|
537
|
+
detached: true,
|
|
538
|
+
stdio: "ignore",
|
|
539
|
+
cwd: os.tmpdir(),
|
|
540
|
+
env: { ...process.env, PATH: EXTENDED_PATH },
|
|
541
|
+
});
|
|
542
|
+
child.unref();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Wait for socket ready
|
|
546
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Best-effort: ensure daemon running (used for login flow) */
|
|
550
|
+
function ensureDaemon() {
|
|
551
|
+
startDaemonWithPassword("").catch(() => {});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Read AuthURL from `tailscale status --json` (Win exposes it there, not stdout). */
|
|
555
|
+
function getAuthUrlFromStatus() {
|
|
556
|
+
const bin = getTailscaleBin();
|
|
557
|
+
if (!bin) return null;
|
|
558
|
+
try {
|
|
559
|
+
const out = execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} status --json`, {
|
|
560
|
+
encoding: "utf8", windowsHide: true, timeout: 2000
|
|
561
|
+
});
|
|
562
|
+
const j = JSON.parse(out);
|
|
563
|
+
if (j.AuthURL) return j.AuthURL;
|
|
564
|
+
return null;
|
|
565
|
+
} catch { return null; }
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Run `tailscale up` and capture the auth URL for browser login.
|
|
570
|
+
* Resolves with { authUrl } or { alreadyLoggedIn: true }.
|
|
571
|
+
* On Windows, AuthURL comes from `status --json` (not stdout) — must poll status.
|
|
572
|
+
*/
|
|
573
|
+
export function startLogin(hostname) {
|
|
574
|
+
const bin = getTailscaleBin();
|
|
575
|
+
if (!bin) return Promise.reject(new Error("Tailscale not installed"));
|
|
576
|
+
|
|
577
|
+
return new Promise((resolve, reject) => {
|
|
578
|
+
// Ensure daemon is running (best-effort, no sudo)
|
|
579
|
+
ensureDaemon();
|
|
580
|
+
|
|
581
|
+
// Check if already logged in
|
|
582
|
+
if (isTailscaleLoggedIn()) {
|
|
583
|
+
resolve({ alreadyLoggedIn: true });
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const args = tsArgs("up", "--accept-routes");
|
|
588
|
+
if (hostname) args.push(`--hostname=${hostname}`);
|
|
589
|
+
const child = spawn(bin, args, {
|
|
590
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
591
|
+
detached: true,
|
|
592
|
+
windowsHide: true
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
let resolved = false;
|
|
596
|
+
let output = "";
|
|
597
|
+
|
|
598
|
+
const parseAuthUrl = (text) => {
|
|
599
|
+
const match = text.match(/https:\/\/login\.tailscale\.com\/a\/[a-zA-Z0-9]+/);
|
|
600
|
+
return match ? match[0] : null;
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const finishWithUrl = (url, source) => {
|
|
604
|
+
if (resolved) return;
|
|
605
|
+
resolved = true;
|
|
606
|
+
clearTimeout(timeout);
|
|
607
|
+
clearInterval(statusPoll);
|
|
608
|
+
console.log(`[Tailscale] login authUrl detected (${source})`);
|
|
609
|
+
child.unref();
|
|
610
|
+
resolve({ authUrl: url });
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// Poll status --json every 500ms — Windows exposes AuthURL only there
|
|
614
|
+
const statusPoll = setInterval(() => {
|
|
615
|
+
if (resolved) return;
|
|
616
|
+
const url = getAuthUrlFromStatus();
|
|
617
|
+
if (url) finishWithUrl(url, "status");
|
|
618
|
+
}, 500);
|
|
619
|
+
|
|
620
|
+
const timeout = setTimeout(() => {
|
|
621
|
+
if (resolved) return;
|
|
622
|
+
resolved = true;
|
|
623
|
+
clearInterval(statusPoll);
|
|
624
|
+
child.unref();
|
|
625
|
+
const url = parseAuthUrl(output) || getAuthUrlFromStatus();
|
|
626
|
+
if (url) resolve({ authUrl: url });
|
|
627
|
+
else reject(new Error("tailscale up timed out without auth URL"));
|
|
628
|
+
}, 15000);
|
|
629
|
+
|
|
630
|
+
const handleData = (data) => {
|
|
631
|
+
output += data.toString();
|
|
632
|
+
const url = parseAuthUrl(output);
|
|
633
|
+
if (url) finishWithUrl(url, "stdout");
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
child.stdout.on("data", handleData);
|
|
637
|
+
child.stderr.on("data", handleData);
|
|
638
|
+
|
|
639
|
+
child.on("error", (err) => {
|
|
640
|
+
if (resolved) return;
|
|
641
|
+
resolved = true;
|
|
642
|
+
clearTimeout(timeout);
|
|
643
|
+
clearInterval(statusPoll);
|
|
644
|
+
console.error(`[Tailscale] login spawn error: ${err.message}`);
|
|
645
|
+
reject(err);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
child.on("exit", (code) => {
|
|
649
|
+
if (resolved) return;
|
|
650
|
+
console.log(`[Tailscale] login exit code=${code}`);
|
|
651
|
+
// Don't trust exit code alone — Win `tailscale up` exits 0 even when not logged in.
|
|
652
|
+
// Let status poll continue until AuthURL appears or timeout.
|
|
653
|
+
const url = parseAuthUrl(output) || getAuthUrlFromStatus();
|
|
654
|
+
if (url) {
|
|
655
|
+
finishWithUrl(url, "exit");
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
// Only resolve alreadyLoggedIn if status confirms BackendState=Running
|
|
659
|
+
if (isTailscaleLoggedIn()) {
|
|
660
|
+
resolved = true;
|
|
661
|
+
clearTimeout(timeout);
|
|
662
|
+
clearInterval(statusPoll);
|
|
663
|
+
resolve({ alreadyLoggedIn: true });
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
// Otherwise keep polling — daemon may publish AuthURL shortly after exit
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Start tailscale funnel for the given port */
|
|
672
|
+
export async function startFunnel(port) {
|
|
673
|
+
const bin = getTailscaleBin();
|
|
674
|
+
if (!bin) throw new Error("Tailscale not installed");
|
|
675
|
+
|
|
676
|
+
// Reset any existing funnel
|
|
677
|
+
try { execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} funnel --bg reset`, { stdio: "ignore", windowsHide: true }); } catch (e) { /* ignore */ }
|
|
678
|
+
|
|
679
|
+
return new Promise((resolve, reject) => {
|
|
680
|
+
const child = spawn(bin, tsArgs("funnel", "--bg", `${port}`), {
|
|
681
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
682
|
+
windowsHide: true
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
let resolved = false;
|
|
686
|
+
let output = "";
|
|
687
|
+
|
|
688
|
+
const timeout = setTimeout(() => {
|
|
689
|
+
if (resolved) return;
|
|
690
|
+
resolved = true;
|
|
691
|
+
// --bg exits after setup, read actual hostname from status
|
|
692
|
+
const url = getActualFunnelUrl() || getTailscaleFunnelUrl(port);
|
|
693
|
+
if (url) resolve({ tunnelUrl: url });
|
|
694
|
+
else reject(new Error(`Tailscale funnel timed out: ${output.trim() || "no output"}`));
|
|
695
|
+
}, 30000);
|
|
696
|
+
|
|
697
|
+
// Always resolve via Self.DNSName to get the real hostname (avoids -1 suffix from conflicts)
|
|
698
|
+
const parseFunnelUrl = () => getActualFunnelUrl();
|
|
699
|
+
|
|
700
|
+
let funnelNotEnabled = false;
|
|
701
|
+
|
|
702
|
+
const handleData = (data) => {
|
|
703
|
+
output += data.toString();
|
|
704
|
+
|
|
705
|
+
if (output.includes("Funnel is not enabled")) funnelNotEnabled = true;
|
|
706
|
+
|
|
707
|
+
// Wait for the enable URL to arrive in a later chunk
|
|
708
|
+
if (funnelNotEnabled && !resolved) {
|
|
709
|
+
const enableMatch = output.match(/https:\/\/login\.tailscale\.com\/[^\s]+/);
|
|
710
|
+
if (enableMatch) {
|
|
711
|
+
resolved = true;
|
|
712
|
+
clearTimeout(timeout);
|
|
713
|
+
child.kill();
|
|
714
|
+
resolve({ funnelNotEnabled: true, enableUrl: enableMatch[0] });
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const url = parseFunnelUrl();
|
|
720
|
+
if (url && !resolved) {
|
|
721
|
+
resolved = true;
|
|
722
|
+
clearTimeout(timeout);
|
|
723
|
+
resolve({ tunnelUrl: url });
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
child.stdout.on("data", handleData);
|
|
728
|
+
child.stderr.on("data", handleData);
|
|
729
|
+
|
|
730
|
+
child.on("exit", (code) => {
|
|
731
|
+
if (resolved) return;
|
|
732
|
+
resolved = true;
|
|
733
|
+
clearTimeout(timeout);
|
|
734
|
+
console.log(`[Tailscale] funnel exit code=${code} output="${output.trim().slice(0, 200)}"`);
|
|
735
|
+
const url = parseFunnelUrl() || getTailscaleFunnelUrl(port);
|
|
736
|
+
if (url) resolve({ tunnelUrl: url });
|
|
737
|
+
else reject(new Error(`tailscale funnel failed (code ${code}): ${output.trim()}`));
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
child.on("error", (err) => {
|
|
741
|
+
if (resolved) return;
|
|
742
|
+
resolved = true;
|
|
743
|
+
clearTimeout(timeout);
|
|
744
|
+
reject(err);
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** Provision TLS cert for funnel domain (required before Funnel serves HTTPS). Best-effort. */
|
|
750
|
+
export async function provisionCert(hostname) {
|
|
751
|
+
const bin = getTailscaleBin();
|
|
752
|
+
if (!bin || !hostname) return;
|
|
753
|
+
const certsDir = path.join(TAILSCALE_DIR, "certs");
|
|
754
|
+
fs.mkdirSync(certsDir, { recursive: true });
|
|
755
|
+
const certFile = path.join(certsDir, `${hostname}.crt`);
|
|
756
|
+
const keyFile = path.join(certsDir, `${hostname}.key`);
|
|
757
|
+
try {
|
|
758
|
+
await execAsync(
|
|
759
|
+
`"${bin}" ${SOCKET_FLAG.join(" ")} cert --cert-file "${certFile}" --key-file "${keyFile}" "${hostname}"`,
|
|
760
|
+
{ windowsHide: true, env: { ...process.env, PATH: EXTENDED_PATH }, timeout: 30000 }
|
|
761
|
+
);
|
|
762
|
+
console.log(`[Tailscale] cert provisioned for ${hostname}`);
|
|
763
|
+
} catch (e) {
|
|
764
|
+
console.warn(`[Tailscale] cert provision failed (non-fatal): ${e.message}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** Stop tailscale funnel */
|
|
769
|
+
export function stopFunnel() {
|
|
770
|
+
const bin = getTailscaleBin();
|
|
771
|
+
if (!bin) return;
|
|
772
|
+
try { execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} funnel --bg reset`, { stdio: "ignore", windowsHide: true }); } catch (e) { /* ignore */ }
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** Kill tailscaled daemon (runs as root, needs sudo) */
|
|
776
|
+
export async function stopDaemon(sudoPassword) {
|
|
777
|
+
// Try non-sudo first
|
|
778
|
+
try { execSync("pkill -x tailscaled", { stdio: "ignore", windowsHide: true, timeout: 3000 }); } catch { /* ignore */ }
|
|
779
|
+
|
|
780
|
+
// Check if still alive
|
|
781
|
+
try { execSync("pgrep -x tailscaled", { stdio: "ignore", windowsHide: true, timeout: 2000 }); } catch { return; } // Dead, done
|
|
782
|
+
|
|
783
|
+
// Kill with sudo password
|
|
784
|
+
if (!IS_WINDOWS) {
|
|
785
|
+
try { await execWithPassword("pkill -x tailscaled", sudoPassword || ""); } catch { /* ignore */ }
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Cleanup socket
|
|
789
|
+
try { if (fs.existsSync(TAILSCALE_SOCKET)) fs.unlinkSync(TAILSCALE_SOCKET); } catch { /* ignore */ }
|
|
790
|
+
}
|