miawrouter 1.2.3 → 1.2.4
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 +7 -0
- package/app/.next-cli-build/BUILD_ID +1 -1
- package/app/.next-cli-build/app-path-routes-manifest.json +9 -9
- package/app/.next-cli-build/build-manifest.json +2 -2
- package/app/.next-cli-build/prerender-manifest.json +3 -3
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/a2a/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/a2a/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/activity/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/activity/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/agent-skills/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/agent-skills/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/api-endpoints/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/api-endpoints/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/basic-chat/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/basic-chat/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/batch/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/batch/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/cache/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/cache/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/cli-tools/[toolId]/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/cli-tools/[toolId]/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/cli-tools/page.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/cli-tools/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/cli-tools/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/cloud-agents/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/cloud-agents/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/combos/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/combos/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/console-log/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/console-log/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/costs/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/costs/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/discovery/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/discovery/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/endpoint/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/endpoint/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/free-provider-rankings/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/free-provider-rankings/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/free-tiers/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/free-tiers/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/guardrails/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/guardrails/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/health/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/health/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/logs/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/logs/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/mcp/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/mcp/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/media-providers/[kind]/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/media-providers/[kind]/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/media-providers/combo/[id]/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/media-providers/web/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/media-providers/web/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/memory/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/memory/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/mitm/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/mitm/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/playground/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/playground/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/privacy/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/privacy/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/profile/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/profile/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/provider-stats/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/provider-stats/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/providers/[id]/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/providers/[id]/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/providers/new/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/providers/new/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/providers/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/providers/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/proxy-pools/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/proxy-pools/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/pxpipe/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/pxpipe/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/quota/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/quota/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/runtime/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/runtime/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/settings/feature-flags/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/settings/feature-flags/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/settings/pricing/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/settings/pricing/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/skill-discovery/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/skill-discovery/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/skills/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/skills/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/token-saver/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/token-saver/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/translator/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/translator/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/usage/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/usage/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/webhooks/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/webhooks/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/.well-known/agent.json/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/_global-error/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/_global-error.html +1 -1
- package/app/.next-cli-build/server/app/_global-error.rsc +1 -1
- package/app/.next-cli-build/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/_not-found/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/_not-found.html +1 -1
- package/app/.next-cli-build/server/app/_not-found.rsc +5 -5
- package/app/.next-cli-build/server/app/_not-found.segments/_full.segment.rsc +5 -5
- package/app/.next-cli-build/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -5
- package/app/.next-cli-build/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/api/a2a/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/a2a/status/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/agents/[id]/actions/route.js +1 -1
- package/app/.next-cli-build/server/app/api/agents/[id]/actions/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/agents/route.js +1 -1
- package/app/.next-cli-build/server/app/api/agents/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/auth/login/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/auth/oidc/callback/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/auth/oidc/start/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/auth/oidc/test/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/auth/reset-password/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/auth/status/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/batch/[id]/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/batch/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/batches/[id]/cancel/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/batches/[id]/errors/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/batches/[id]/results/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/batches/[id]/retry/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/batches/[id]/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/batches/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cache/clear/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cache/stats/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/catalog/opencode/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/all-statuses/route.js +5 -5
- package/app/.next-cli-build/server/app/api/cli-tools/all-statuses/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/antigravity-mitm/alias/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/antigravity-mitm/alias/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/antigravity-mitm/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/claude-settings/route.js +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/claude-settings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/cline-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/cline-settings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/codex-settings/route.js +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/codex-settings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/copilot-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/copilot-settings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/cowork-mcp-registry/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/cowork-mcp-registry/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/cowork-mcp-tools/route.js +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/cowork-mcp-tools/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/cowork-settings/route.js +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/cowork-settings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/deepseek-tui-settings/route.js +3 -3
- package/app/.next-cli-build/server/app/api/cli-tools/deepseek-tui-settings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/devin-settings/route.js +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/devin-settings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/droid-settings/route.js +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/droid-settings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/grok-build-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/grok-build-settings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/hermes-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/hermes-settings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/jcode-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/jcode-settings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/kilo-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/kilo-settings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/oh-my-pi-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/oh-my-pi-settings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/openclaw-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/openclaw-settings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/opencode-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/opencode-settings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/subagents/route.js +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/subagents/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/cloud-agents/status/route.js +1 -1
- package/app/.next-cli-build/server/app/api/cloud-agents/status/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/combos/[id]/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/combos/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/dashboard/chat/completions/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/discovery/endpoints/route.js +1 -1
- package/app/.next-cli-build/server/app/api/discovery/endpoints/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/discovery/route.js +1 -1
- package/app/.next-cli-build/server/app/api/discovery/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/discovery/skills/route.js +1 -1
- package/app/.next-cli-build/server/app/api/discovery/skills/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/events/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/headroom/extras/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/headroom/proxy/[...path]/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/headroom/restart/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/headroom/start/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/headroom/status/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/headroom/stop/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/health/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/hermes/autonomy/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/init/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/keys/[id]/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/keys/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/locale/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/mcp/[plugin]/message/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/mcp/[plugin]/sse/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/mcp/route.js +1 -1
- package/app/.next-cli-build/server/app/api/mcp/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/mcp/status/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/media-providers/tts/deepgram/voices/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/media-providers/tts/elevenlabs/voices/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/media-providers/tts/inworld/voices/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/media-providers/tts/minimax/voices/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/media-providers/tts/voices/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/memory/[id]/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/memory/health/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/memory/reindex/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/memory/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/memory/search/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/metrics/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/models/alias/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/models/availability/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/models/custom/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/models/disabled/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/models/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/models/test/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/oauth/[provider]/[action]/route.js +1 -1
- package/app/.next-cli-build/server/app/api/oauth/[provider]/[action]/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/oauth/codex/bulk-import/route.js +1 -1
- package/app/.next-cli-build/server/app/api/oauth/codex/bulk-import/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/oauth/codex/import-token/route.js +1 -1
- package/app/.next-cli-build/server/app/api/oauth/codex/import-token/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/oauth/cursor/auto-import/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/oauth/cursor/import/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/oauth/gitlab/pat/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/oauth/iflow/cookie/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/oauth/kiro/api-key/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/oauth/kiro/auto-import/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/oauth/kiro/import/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/oauth/kiro/import-cli-proxy/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/oauth/kiro/social-authorize/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/oauth/kiro/social-exchange/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/pricing/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/provider-nodes/[id]/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/provider-nodes/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/provider-nodes/validate/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/providers/[id]/export/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/providers/[id]/models/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/providers/[id]/onboard/route.js +1 -1
- package/app/.next-cli-build/server/app/api/providers/[id]/onboard/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/providers/[id]/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/providers/[id]/test/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/providers/[id]/test-models/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/providers/client/route.js +1 -1
- package/app/.next-cli-build/server/app/api/providers/client/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/providers/kilo/free-models/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/providers/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/providers/suggested-models/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/providers/test-batch/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/providers/validate/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/proxy-pools/[id]/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/proxy-pools/[id]/test/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/proxy-pools/cloudflare-deploy/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/proxy-pools/deno-deploy/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/proxy-pools/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/proxy-pools/vercel-deploy/route.js +1 -1
- package/app/.next-cli-build/server/app/api/proxy-pools/vercel-deploy/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/pxpipe/health/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/pxpipe/install/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/pxpipe/logs/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/pxpipe/restart/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/pxpipe/start/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/pxpipe/stats/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/pxpipe/status/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/pxpipe/stop/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/saver/stats/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/settings/database/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/settings/proxy-test/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/settings/require-login/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/settings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/shutdown/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/skill-discovery/[id]/install/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/skill-discovery/[id]/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/skill-discovery/[id]/uninstall/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/skill-discovery/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/skill-discovery/sync/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/tags/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/translator/console-logs/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/translator/console-logs/stream/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/translator/load/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/translator/save/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/translator/send/route.js +1 -1
- package/app/.next-cli-build/server/app/api/translator/send/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/translator/translate/route.js +1 -1
- package/app/.next-cli-build/server/app/api/translator/translate/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/tunnel/disable/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/tunnel/enable/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/tunnel/status/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/tunnel/tailscale-check/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/tunnel/tailscale-disable/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/tunnel/tailscale-enable/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/tunnel/tailscale-install/route.js +2 -2
- package/app/.next-cli-build/server/app/api/tunnel/tailscale-install/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/usage/[connectionId]/codex-reset-credits/route.js +1 -1
- package/app/.next-cli-build/server/app/api/usage/[connectionId]/codex-reset-credits/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/usage/[connectionId]/route.js +1 -1
- package/app/.next-cli-build/server/app/api/usage/[connectionId]/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/usage/chart/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/usage/history/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/usage/local-snapshot/route.js +1 -1
- package/app/.next-cli-build/server/app/api/usage/local-snapshot/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/usage/logs/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/usage/providers/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/usage/request-details/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/usage/request-logs/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/usage/stats/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/usage/stream/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/api/chat/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/audio/speech/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/audio/transcriptions/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/audio/voices/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/chat/completions/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/embeddings/route.js +1 -1
- package/app/.next-cli-build/server/app/api/v1/embeddings/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/images/generations/route.js +1 -1
- package/app/.next-cli-build/server/app/api/v1/images/generations/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/messages/count_tokens/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/messages/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/models/[kind]/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/models/info/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/models/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/responses/compact/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/responses/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/search/route.js +1 -1
- package/app/.next-cli-build/server/app/api/v1/search/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/videos/[id]/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/videos/edits/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/videos/extensions/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/videos/generations/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1/web/fetch/route.js +1 -1
- package/app/.next-cli-build/server/app/api/v1/web/fetch/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1beta/models/[...path]/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/v1beta/models/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/version/route.js +1 -1
- package/app/.next-cli-build/server/app/api/version/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/version/shutdown/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/version/update/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/webhooks/[id]/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/webhooks/events/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/api/webhooks/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/callback/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/callback/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/callback.html +1 -1
- package/app/.next-cli-build/server/app/callback.rsc +5 -5
- package/app/.next-cli-build/server/app/callback.segments/_full.segment.rsc +5 -5
- package/app/.next-cli-build/server/app/callback.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/callback.segments/callback/__PAGE__.segment.rsc +5 -5
- package/app/.next-cli-build/server/app/dashboard/a2a.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/a2a.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/a2a.segments/!KGRhc2hib2FyZCk/dashboard/a2a/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/a2a.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/a2a.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/activity.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/activity.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/activity.segments/!KGRhc2hib2FyZCk/dashboard/activity/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/activity.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/activity.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/agent-skills.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/agent-skills.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/agent-skills.segments/!KGRhc2hib2FyZCk/dashboard/agent-skills/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/agent-skills.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/agent-skills.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/api-endpoints.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/api-endpoints.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/api-endpoints.segments/!KGRhc2hib2FyZCk/dashboard/api-endpoints/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/api-endpoints.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/api-endpoints.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/basic-chat.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/basic-chat.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/basic-chat.segments/!KGRhc2hib2FyZCk/dashboard/basic-chat/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/basic-chat.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/basic-chat.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/batch.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/batch.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/batch.segments/!KGRhc2hib2FyZCk/dashboard/batch/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/batch.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/batch.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/cache.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/cache.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/cache.segments/!KGRhc2hib2FyZCk/dashboard/cache/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/cache.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/cache.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/cli-tools.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/cli-tools.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/cli-tools.segments/!KGRhc2hib2FyZCk/dashboard/cli-tools/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/cli-tools.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/cli-tools.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/cloud-agents.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/cloud-agents.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/cloud-agents.segments/!KGRhc2hib2FyZCk/dashboard/cloud-agents/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/cloud-agents.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/cloud-agents.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/combos.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/combos.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/combos.segments/!KGRhc2hib2FyZCk/dashboard/combos/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/combos.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/combos.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/costs.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/costs.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/costs.segments/!KGRhc2hib2FyZCk/dashboard/costs/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/costs.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/costs.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/discovery.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/discovery.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/discovery.segments/!KGRhc2hib2FyZCk/dashboard/discovery/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/discovery.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/discovery.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/endpoint.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/endpoint.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/endpoint.segments/!KGRhc2hib2FyZCk/dashboard/endpoint/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/endpoint.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/endpoint.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/free-provider-rankings.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/free-provider-rankings.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/free-provider-rankings.segments/!KGRhc2hib2FyZCk/dashboard/free-provider-rankings/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/free-provider-rankings.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/free-provider-rankings.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/free-tiers.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/free-tiers.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/free-tiers.segments/!KGRhc2hib2FyZCk/dashboard/free-tiers/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/free-tiers.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/free-tiers.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/guardrails.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/guardrails.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/guardrails.segments/!KGRhc2hib2FyZCk/dashboard/guardrails/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/guardrails.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/guardrails.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/health.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/health.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/health.segments/!KGRhc2hib2FyZCk/dashboard/health/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/health.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/health.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/logs.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/logs.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/logs.segments/!KGRhc2hib2FyZCk/dashboard/logs/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/logs.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/logs.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/mcp.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/mcp.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/mcp.segments/!KGRhc2hib2FyZCk/dashboard/mcp/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/mcp.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/mcp.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/media-providers/web.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/media-providers/web.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/media-providers/web.segments/!KGRhc2hib2FyZCk/dashboard/media-providers/web/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/media-providers/web.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/media-providers/web.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/memory.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/memory.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/memory.segments/!KGRhc2hib2FyZCk/dashboard/memory/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/memory.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/memory.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/mitm.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/mitm.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/mitm.segments/!KGRhc2hib2FyZCk/dashboard/mitm/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/mitm.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/mitm.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/playground.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/playground.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/playground.segments/!KGRhc2hib2FyZCk/dashboard/playground/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/playground.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/playground.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/privacy.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/privacy.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/privacy.segments/!KGRhc2hib2FyZCk/dashboard/privacy/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/privacy.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/privacy.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/profile.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/profile.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/profile.segments/!KGRhc2hib2FyZCk/dashboard/profile/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/profile.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/profile.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/provider-stats.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/provider-stats.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/provider-stats.segments/!KGRhc2hib2FyZCk/dashboard/provider-stats/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/provider-stats.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/provider-stats.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/providers/new.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/providers/new.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/providers/new.segments/!KGRhc2hib2FyZCk/dashboard/providers/new/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/providers/new.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/providers/new.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/providers.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/providers.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/providers.segments/!KGRhc2hib2FyZCk/dashboard/providers/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/providers.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/providers.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/proxy-pools.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/proxy-pools.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/proxy-pools.segments/!KGRhc2hib2FyZCk/dashboard/proxy-pools/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/proxy-pools.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/proxy-pools.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/pxpipe.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/pxpipe.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/pxpipe.segments/!KGRhc2hib2FyZCk/dashboard/pxpipe/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/pxpipe.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/pxpipe.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/quota.html +2 -2
- package/app/.next-cli-build/server/app/dashboard/quota.rsc +8 -8
- package/app/.next-cli-build/server/app/dashboard/quota.segments/!KGRhc2hib2FyZCk/dashboard/quota/__PAGE__.segment.rsc +8 -8
- package/app/.next-cli-build/server/app/dashboard/quota.segments/_full.segment.rsc +8 -8
- package/app/.next-cli-build/server/app/dashboard/quota.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/runtime.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/runtime.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/runtime.segments/!KGRhc2hib2FyZCk/dashboard/runtime/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/runtime.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/runtime.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/settings/feature-flags.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/settings/feature-flags.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/settings/feature-flags.segments/!KGRhc2hib2FyZCk/dashboard/settings/feature-flags/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/settings/feature-flags.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/settings/feature-flags.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/settings/pricing.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/settings/pricing.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/settings/pricing.segments/!KGRhc2hib2FyZCk/dashboard/settings/pricing/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/settings/pricing.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/settings/pricing.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/skill-discovery.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/skill-discovery.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/skill-discovery.segments/!KGRhc2hib2FyZCk/dashboard/skill-discovery/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/skill-discovery.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/skill-discovery.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/skills.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/skills.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/skills.segments/!KGRhc2hib2FyZCk/dashboard/skills/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/skills.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/skills.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/token-saver.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/token-saver.rsc +8 -8
- package/app/.next-cli-build/server/app/dashboard/token-saver.segments/!KGRhc2hib2FyZCk/dashboard/token-saver/__PAGE__.segment.rsc +8 -8
- package/app/.next-cli-build/server/app/dashboard/token-saver.segments/_full.segment.rsc +8 -8
- package/app/.next-cli-build/server/app/dashboard/token-saver.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/translator.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/translator.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/translator.segments/!KGRhc2hib2FyZCk/dashboard/translator/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/translator.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/translator.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/usage.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/usage.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/usage.segments/!KGRhc2hib2FyZCk/dashboard/usage/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/usage.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/usage.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard/webhooks.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/webhooks.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/webhooks.segments/!KGRhc2hib2FyZCk/dashboard/webhooks/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/webhooks.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard/webhooks.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/dashboard.html +1 -1
- package/app/.next-cli-build/server/app/dashboard.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard.segments/!KGRhc2hib2FyZCk/dashboard/__PAGE__.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard.segments/_full.segment.rsc +7 -7
- package/app/.next-cli-build/server/app/dashboard.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/favicon.ico/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/index.html +1 -1
- package/app/.next-cli-build/server/app/index.rsc +5 -5
- package/app/.next-cli-build/server/app/index.segments/__PAGE__.segment.rsc +5 -5
- package/app/.next-cli-build/server/app/index.segments/_full.segment.rsc +5 -5
- package/app/.next-cli-build/server/app/index.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/landing/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/landing/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/landing.html +1 -1
- package/app/.next-cli-build/server/app/landing.rsc +5 -5
- package/app/.next-cli-build/server/app/landing.segments/_full.segment.rsc +5 -5
- package/app/.next-cli-build/server/app/landing.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/landing.segments/landing/__PAGE__.segment.rsc +5 -5
- package/app/.next-cli-build/server/app/login/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/login/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/login.html +1 -1
- package/app/.next-cli-build/server/app/login.rsc +6 -6
- package/app/.next-cli-build/server/app/login.segments/_full.segment.rsc +6 -6
- package/app/.next-cli-build/server/app/login.segments/_tree.segment.rsc +2 -2
- package/app/.next-cli-build/server/app/login.segments/login/__PAGE__.segment.rsc +6 -6
- package/app/.next-cli-build/server/app/manifest.webmanifest/route.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/page.js.nft.json +1 -1
- package/app/.next-cli-build/server/app/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app-paths-manifest.json +9 -9
- package/app/.next-cli-build/server/chunks/112.js +1 -1
- package/app/.next-cli-build/server/chunks/2638.js +1 -1
- package/app/.next-cli-build/server/chunks/2813.js +1 -1
- package/app/.next-cli-build/server/chunks/2974.js +1 -1
- package/app/.next-cli-build/server/chunks/318.js +1 -1
- package/app/.next-cli-build/server/chunks/3755.js +1 -1
- package/app/.next-cli-build/server/chunks/4037.js +1 -1
- package/app/.next-cli-build/server/chunks/4303.js +1 -1
- package/app/.next-cli-build/server/chunks/5243.js +1 -1
- package/app/.next-cli-build/server/chunks/5731.js +1 -1
- package/app/.next-cli-build/server/chunks/6270.js +1 -1
- package/app/.next-cli-build/server/chunks/6664.js +1 -1
- package/app/.next-cli-build/server/chunks/7403.js +1 -1
- package/app/.next-cli-build/server/chunks/772.js +1 -1
- package/app/.next-cli-build/server/chunks/{4018.js → 7937.js} +5 -5
- package/app/.next-cli-build/server/chunks/8499.js +1 -1
- package/app/.next-cli-build/server/chunks/8537.js +1 -1
- package/app/.next-cli-build/server/chunks/862.js +1 -1
- package/app/.next-cli-build/server/chunks/869.js +1 -1
- package/app/.next-cli-build/server/chunks/8750.js +1 -1
- package/app/.next-cli-build/server/instrumentation.js.nft.json +1 -1
- package/app/.next-cli-build/server/middleware-build-manifest.js +1 -1
- package/app/.next-cli-build/server/middleware.js +4 -4
- package/app/.next-cli-build/server/middleware.js.nft.json +1 -1
- package/app/.next-cli-build/server/pages/404.html +1 -1
- package/app/.next-cli-build/server/pages/500.html +1 -1
- package/app/.next-cli-build/server/server-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/server-reference-manifest.json +1 -1
- package/app/.next-cli-build/static/chunks/{1321-b974e2c6d758f2eb.js → 1321-6b997794bb843da8.js} +1 -1
- package/app/.next-cli-build/static/chunks/{7780-8fef40eb7ea5a323.js → 7780-f3508723257e47c1.js} +1 -1
- package/app/.next-cli-build/static/chunks/app/(dashboard)/dashboard/cli-tools/{page-8f34a6d3a99ebd37.js → page-f7cbd07558c7b42e.js} +1 -1
- package/app/.next-cli-build/static/css/01b1ec05918332c3.css +1 -0
- package/app/package.json +79 -78
- package/app/src/mitm/server.js +40 -42
- package/cli.js +10 -0
- package/hooks/opencodeBridgeRuntime.js +77 -0
- package/hooks/sqliteRuntime.js +156 -156
- package/hooks/trayRuntime.js +107 -107
- package/package.json +49 -49
- package/vendor/opencode-free-bridge/LICENSE +21 -0
- package/vendor/opencode-free-bridge/README.md +762 -0
- package/vendor/opencode-free-bridge/THIRD_PARTY_NOTICES.md +33 -0
- package/vendor/opencode-free-bridge/UPSTREAM_COMMIT +1 -0
- package/vendor/opencode-free-bridge/identity.js +2624 -0
- package/vendor/opencode-free-bridge/package.json +54 -0
- package/vendor/opencode-free-bridge/panel.js +515 -0
- package/vendor/opencode-free-bridge/proxy.js +980 -0
- package/vendor/opencode-free-bridge/server.js +3357 -0
- package/vendor/opencode-free-bridge/token-budget.js +107 -0
- package/vendor/opencode-free-bridge/transport-baseline.json +39 -0
- package/app/.next-cli-build/static/css/8ba5c8dbc4e30aa0.css +0 -1
- /package/app/.next-cli-build/static/{X4uxXfKI78mMCnlqFsTAr → DryEkcB2pIbzN1YYgNcUS}/_buildManifest.js +0 -0
- /package/app/.next-cli-build/static/{X4uxXfKI78mMCnlqFsTAr → DryEkcB2pIbzN1YYgNcUS}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* proxy.js: runtime-configurable egress, single proxy or pool.
|
|
4
|
+
*
|
|
5
|
+
* Every outbound request this bridge makes (inference, model discovery, identity
|
|
6
|
+
* sync, self-update) can be routed through an HTTP/HTTPS proxy so upstream sees
|
|
7
|
+
* that proxy's IP instead of this host's. Two shapes are supported:
|
|
8
|
+
*
|
|
9
|
+
* - one proxy (`url`), the original behaviour, edited from the panel at `/`;
|
|
10
|
+
* - a pool of proxies, each with its own health state, so a single bridge can
|
|
11
|
+
* spread requests across many egress IPs and move off one that upstream has
|
|
12
|
+
* throttled.
|
|
13
|
+
*
|
|
14
|
+
* Why a pool matters here: the free-tier allowance is per egress IP, not per
|
|
15
|
+
* account. Running one bridge per host therefore multiplies the number of
|
|
16
|
+
* processes, service units and tunnels without adding anything a pool does not
|
|
17
|
+
* already give — the pool rotates the IP while the process, the identity and the
|
|
18
|
+
* model catalog stay in one place.
|
|
19
|
+
*
|
|
20
|
+
* Protocol: HTTP/HTTPS only. Bun's `fetch` accepts `proxy:` for those schemes and
|
|
21
|
+
* tunnels TLS through CONNECT, so the ClientHello still comes from Bun, and the
|
|
22
|
+
* transport-fingerprint baseline is unchanged. SOCKS5 is rejected by Bun with
|
|
23
|
+
* `UnsupportedProxyProtocol`, so it is refused here with a message that says so
|
|
24
|
+
* instead of failing later inside a request.
|
|
25
|
+
*
|
|
26
|
+
* Credentials live in `<state dir>/proxy.json` and never leave the process:
|
|
27
|
+
* `describe()` is the only view the HTTP panel and `/health` get, and it carries
|
|
28
|
+
* host:port only.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const crypto = require("crypto");
|
|
32
|
+
const fs = require("fs");
|
|
33
|
+
const os = require("os");
|
|
34
|
+
const path = require("path");
|
|
35
|
+
|
|
36
|
+
const CONFIG_FILE_NAME = "proxy.json";
|
|
37
|
+
const HEALTH_FILE_NAME = "pool-health.json";
|
|
38
|
+
const HEALTH_VERSION = 1;
|
|
39
|
+
const ENV_OVERRIDE = "UPSTREAM_PROXY_URL";
|
|
40
|
+
const POOL_ENV = "UPSTREAM_PROXY_POOL";
|
|
41
|
+
const STRATEGY_ENV = "UPSTREAM_PROXY_STRATEGY";
|
|
42
|
+
|
|
43
|
+
const STRATEGIES = new Set(["round-robin", "failover", "random"]);
|
|
44
|
+
const DEFAULT_STRATEGY = "round-robin";
|
|
45
|
+
const DEFAULT_COOLDOWN_MS = 120000;
|
|
46
|
+
const DEFAULT_MAX_COOLDOWN_MS = 1800000;
|
|
47
|
+
|
|
48
|
+
function stateDir() {
|
|
49
|
+
const raw = process.env.OPENCODE_BRIDGE_STATE_DIR;
|
|
50
|
+
if (raw !== undefined && String(raw).trim()) return String(raw).trim();
|
|
51
|
+
return path.join(os.homedir(), ".opencode-free-bridge");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function configFile() {
|
|
55
|
+
return path.join(stateDir(), CONFIG_FILE_NAME);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Pool health lives beside the config but in its own file: the config is
|
|
60
|
+
* operator intent, health is runtime observation. Writing one must never
|
|
61
|
+
* rewrite the other, and a corrupt health file must not cost the pool.
|
|
62
|
+
*/
|
|
63
|
+
function healthFile() {
|
|
64
|
+
return path.join(stateDir(), HEALTH_FILE_NAME);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Canonical form of a proxy URL, or the reason it was refused.
|
|
69
|
+
*
|
|
70
|
+
* `""` is valid and means "direct": the panel clears the setting with an empty
|
|
71
|
+
* value rather than a separate verb, so both paths share one validator.
|
|
72
|
+
*/
|
|
73
|
+
function parseProxyUrl(raw) {
|
|
74
|
+
const value = String(raw === undefined || raw === null ? "" : raw).trim();
|
|
75
|
+
if (!value) return { ok: true, url: "" };
|
|
76
|
+
|
|
77
|
+
let u;
|
|
78
|
+
try {
|
|
79
|
+
u = new URL(value);
|
|
80
|
+
} catch {
|
|
81
|
+
return { ok: false, error: "URL proxy tidak valid. Contoh: http://user:pass@host:8080" };
|
|
82
|
+
}
|
|
83
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
error:
|
|
87
|
+
`Skema ${u.protocol} tidak didukung. Bun fetch hanya mendukung proxy http:// dan https:// ` +
|
|
88
|
+
"(tunnel CONNECT). SOCKS5 ditolak runtime, jadi tidak bisa dipakai di sini.",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (!u.hostname) return { ok: false, error: "Host proxy kosong." };
|
|
92
|
+
if (u.pathname !== "/" || u.search || u.hash) {
|
|
93
|
+
return { ok: false, error: "URL proxy hanya boleh berisi skema, kredensial, host, dan port." };
|
|
94
|
+
}
|
|
95
|
+
// `u.username`/`u.password` stay percent-encoded exactly as the user typed them,
|
|
96
|
+
// which is what Bun expects. Rebuilding avoids the trailing slash `URL`
|
|
97
|
+
// normalization adds, so the stored value matches what the panel shows back.
|
|
98
|
+
const auth = u.username || u.password
|
|
99
|
+
? `${u.username}${u.password ? `:${u.password}` : ""}@`
|
|
100
|
+
: "";
|
|
101
|
+
const port = u.port ? `:${u.port}` : "";
|
|
102
|
+
return { ok: true, url: `${u.protocol}//${auth}${u.hostname}${port}` };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** `host:port` for display. Credentials are never part of this string. */
|
|
106
|
+
function displayHost(url) {
|
|
107
|
+
if (!url) return null;
|
|
108
|
+
try {
|
|
109
|
+
const u = new URL(url);
|
|
110
|
+
return `${u.hostname}${u.port ? `:${u.port}` : ""}`;
|
|
111
|
+
} catch {
|
|
112
|
+
return "configured";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** `id` for a URL that has none: stable across reloads, never leaks credentials. */
|
|
117
|
+
function fallbackId(url) {
|
|
118
|
+
const host = displayHost(url) || "proxy";
|
|
119
|
+
return host.replace(/[^A-Za-z0-9.:-]/g, "");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* One pool entry, or the reason it was refused. A string entry is shorthand for
|
|
124
|
+
* `{ url }`, so a pool can be written as a plain list of URLs.
|
|
125
|
+
*
|
|
126
|
+
* `{ direct: true }` is the bridge's own address: it lets the hosting machine's
|
|
127
|
+
* IP take part in the rotation instead of being the fallback used only when
|
|
128
|
+
* every proxy is down. That is worth a first-class entry because the host that
|
|
129
|
+
* runs the bridge usually has a usable IP of its own.
|
|
130
|
+
*/
|
|
131
|
+
function parsePoolEntry(raw, index) {
|
|
132
|
+
const source = typeof raw === "string" ? { url: raw } : raw;
|
|
133
|
+
if (!source || typeof source !== "object") {
|
|
134
|
+
return { ok: false, error: `Entri #${index + 1} bukan objek atau URL.` };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const label = String(source.label == null ? "" : source.label).trim().slice(0, 64);
|
|
138
|
+
|
|
139
|
+
if (source.direct === true) {
|
|
140
|
+
const id = String(source.id == null ? "" : source.id).trim().slice(0, 64) || label || "direct";
|
|
141
|
+
const weight = Number(source.weight);
|
|
142
|
+
return {
|
|
143
|
+
ok: true,
|
|
144
|
+
entry: {
|
|
145
|
+
id,
|
|
146
|
+
label: label || id,
|
|
147
|
+
url: "",
|
|
148
|
+
direct: true,
|
|
149
|
+
enabled: source.enabled === undefined ? true : Boolean(source.enabled),
|
|
150
|
+
weight: Number.isFinite(weight) && weight > 0 ? Math.min(weight, 100) : 1,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const parsed = parseProxyUrl(source.url);
|
|
156
|
+
if (!parsed.ok) return { ok: false, error: `Entri #${index + 1}: ${parsed.error}` };
|
|
157
|
+
if (!parsed.url) return { ok: false, error: `Entri #${index + 1}: URL kosong.` };
|
|
158
|
+
|
|
159
|
+
const id = String(source.id == null ? "" : source.id).trim().slice(0, 64) || label || fallbackId(parsed.url);
|
|
160
|
+
// Weights stay optional: an operator that never sets one gets an even spread,
|
|
161
|
+
// and a weight only has to make relative sense within the pool.
|
|
162
|
+
const weight = Number(source.weight);
|
|
163
|
+
return {
|
|
164
|
+
ok: true,
|
|
165
|
+
entry: {
|
|
166
|
+
id,
|
|
167
|
+
label: label || id,
|
|
168
|
+
url: parsed.url,
|
|
169
|
+
direct: false,
|
|
170
|
+
enabled: source.enabled === undefined ? true : Boolean(source.enabled),
|
|
171
|
+
weight: Number.isFinite(weight) && weight > 0 ? Math.min(weight, 100) : 1,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse a `UPSTREAM_PROXY_POOL` value: URLs separated by commas or newlines,
|
|
178
|
+
* each optionally prefixed with `label=`. Empty entries are ignored so a
|
|
179
|
+
* trailing separator is not an error.
|
|
180
|
+
*/
|
|
181
|
+
function parsePoolSpec(raw) {
|
|
182
|
+
const out = [];
|
|
183
|
+
const errors = [];
|
|
184
|
+
String(raw || "")
|
|
185
|
+
.split(/[\n,]+/)
|
|
186
|
+
.map((s) => s.trim())
|
|
187
|
+
.filter(Boolean)
|
|
188
|
+
.forEach((piece, index) => {
|
|
189
|
+
const eq = piece.indexOf("=");
|
|
190
|
+
const hasLabel = eq > 0 && !piece.slice(0, eq).includes("/");
|
|
191
|
+
const parsed = parsePoolEntry(
|
|
192
|
+
hasLabel ? { label: piece.slice(0, eq).trim(), url: piece.slice(eq + 1).trim() } : piece,
|
|
193
|
+
index
|
|
194
|
+
);
|
|
195
|
+
if (parsed.ok) out.push(parsed.entry);
|
|
196
|
+
else errors.push(parsed.error);
|
|
197
|
+
});
|
|
198
|
+
return { entries: out, errors };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------- state ---
|
|
202
|
+
|
|
203
|
+
/** Single-proxy state (legacy). `pool` below is used only when it has entries. */
|
|
204
|
+
let current = { url: "", source: "none", error: null };
|
|
205
|
+
let pool = { entries: [], errors: [], strategy: DEFAULT_STRATEGY, source: "none", error: null };
|
|
206
|
+
let cooldowns = { baseMs: DEFAULT_COOLDOWN_MS, maxMs: DEFAULT_MAX_COOLDOWN_MS };
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Health is keyed by egress identity, not by proxy URL: the "direct" entry has
|
|
210
|
+
* no URL, and two entries must never share a health record.
|
|
211
|
+
*/
|
|
212
|
+
function keyOf(url) {
|
|
213
|
+
return url ? url : "@direct";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** key -> { failures, cooldownUntil, lastError, lastOkAt, lastUsedAt } */
|
|
217
|
+
const health = new Map();
|
|
218
|
+
let cursor = 0;
|
|
219
|
+
|
|
220
|
+
function healthFor(url) {
|
|
221
|
+
const key = keyOf(url);
|
|
222
|
+
let h = health.get(key);
|
|
223
|
+
if (!h) {
|
|
224
|
+
h = { failures: 0, cooldownUntil: 0, lastError: null, lastOkAt: 0, lastUsedAt: 0, lastStatus: null };
|
|
225
|
+
health.set(key, h);
|
|
226
|
+
}
|
|
227
|
+
return h;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Drop health for egresses no longer in the pool, so a removed entry leaves no
|
|
232
|
+
* ghost. Pure: the caller persists, so a reload never writes a half-restored map
|
|
233
|
+
* over a file it has not read yet.
|
|
234
|
+
*/
|
|
235
|
+
function pruneHealth(entries) {
|
|
236
|
+
const live = new Set(entries.map((e) => keyOf(e.url)));
|
|
237
|
+
if (current.url) live.add(current.url);
|
|
238
|
+
let removed = 0;
|
|
239
|
+
for (const key of [...health.keys()]) {
|
|
240
|
+
if (!live.has(key)) {
|
|
241
|
+
health.delete(key);
|
|
242
|
+
removed += 1;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return removed;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Stable, non-secret id for one egress. The health file sits beside a config
|
|
250
|
+
* that already holds proxy credentials; a second copy of every token would be
|
|
251
|
+
* a second thing to leak, and the record only has to be matchable, not readable.
|
|
252
|
+
*/
|
|
253
|
+
function healthId(key) {
|
|
254
|
+
return crypto.createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function healthLabel(key) {
|
|
258
|
+
if (key === "@direct") return "direct";
|
|
259
|
+
const entry = pool.entries.find((e) => keyOf(e.url) === key);
|
|
260
|
+
return entry ? entry.label : null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function healthSnapshot(now = Date.now()) {
|
|
264
|
+
return {
|
|
265
|
+
version: HEALTH_VERSION,
|
|
266
|
+
savedAt: new Date(now).toISOString(),
|
|
267
|
+
entries: [...health.entries()].map(([key, h]) => ({
|
|
268
|
+
id: healthId(key),
|
|
269
|
+
label: healthLabel(key),
|
|
270
|
+
failures: h.failures,
|
|
271
|
+
cooldownUntil: h.cooldownUntil,
|
|
272
|
+
lastError: h.lastError,
|
|
273
|
+
lastStatus: h.lastStatus,
|
|
274
|
+
lastOkAt: h.lastOkAt || null,
|
|
275
|
+
lastUsedAt: h.lastUsedAt || null,
|
|
276
|
+
})),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let healthWriteTimer = null;
|
|
281
|
+
|
|
282
|
+
/** Drop the health file: no egress has anything worth remembering. */
|
|
283
|
+
function removeHealthFile() {
|
|
284
|
+
try {
|
|
285
|
+
fs.unlinkSync(healthFile());
|
|
286
|
+
} catch (err) {
|
|
287
|
+
if (err.code !== "ENOENT") return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Write observed health. Atomic via rename so a crash mid-write leaves the
|
|
293
|
+
* previous file intact — a truncated file would silently cost the pool every
|
|
294
|
+
* cooldown it had learned.
|
|
295
|
+
*
|
|
296
|
+
* Failure is never surfaced: health is a memory optimization, and a request
|
|
297
|
+
* must not fail because a cache file could not be written.
|
|
298
|
+
*/
|
|
299
|
+
function writeHealthNow(now = Date.now()) {
|
|
300
|
+
if (healthWriteTimer) {
|
|
301
|
+
clearTimeout(healthWriteTimer);
|
|
302
|
+
healthWriteTimer = null;
|
|
303
|
+
}
|
|
304
|
+
if (health.size === 0) {
|
|
305
|
+
removeHealthFile();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
fs.mkdirSync(path.dirname(healthFile()), { recursive: true });
|
|
310
|
+
const tmp = `${healthFile()}.tmp`;
|
|
311
|
+
fs.writeFileSync(tmp, `${JSON.stringify(healthSnapshot(now), null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
312
|
+
fs.renameSync(tmp, healthFile());
|
|
313
|
+
fs.chmodSync(healthFile(), 0o600);
|
|
314
|
+
} catch {
|
|
315
|
+
// Best effort: the bridge keeps running with in-memory health.
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Coalesce rapid writes. A success arrives with every request, and rewriting a
|
|
321
|
+
* file per request would turn a cheap memory update into disk churn.
|
|
322
|
+
*/
|
|
323
|
+
function scheduleHealthWrite(delayMs = 1000) {
|
|
324
|
+
if (healthWriteTimer) return;
|
|
325
|
+
healthWriteTimer = setTimeout(() => {
|
|
326
|
+
healthWriteTimer = null;
|
|
327
|
+
writeHealthNow();
|
|
328
|
+
}, delayMs);
|
|
329
|
+
// Never hold the process open for a cache write.
|
|
330
|
+
if (typeof healthWriteTimer.unref === "function") healthWriteTimer.unref();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Re-apply health saved by an earlier process. An Instabox suspend or a service
|
|
335
|
+
* restart would otherwise forget which IPs are spent and re-burn all of them.
|
|
336
|
+
*
|
|
337
|
+
* The in-memory record wins when it already exists: a restart is the only thing
|
|
338
|
+
* that leaves the map empty, so a live record is by definition at least as
|
|
339
|
+
* fresh as the file.
|
|
340
|
+
*/
|
|
341
|
+
function restoreHealth(entries, now = Date.now()) {
|
|
342
|
+
let raw;
|
|
343
|
+
try {
|
|
344
|
+
raw = fs.readFileSync(healthFile(), "utf8");
|
|
345
|
+
} catch {
|
|
346
|
+
return 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let json = null;
|
|
350
|
+
try {
|
|
351
|
+
json = JSON.parse(raw);
|
|
352
|
+
} catch {
|
|
353
|
+
// A corrupt health file must cost nothing but the cooldowns.
|
|
354
|
+
removeHealthFile();
|
|
355
|
+
return 0;
|
|
356
|
+
}
|
|
357
|
+
if (!json || json.version !== HEALTH_VERSION || !Array.isArray(json.entries)) return 0;
|
|
358
|
+
|
|
359
|
+
const saved = new Map();
|
|
360
|
+
for (const record of json.entries) {
|
|
361
|
+
if (record && typeof record.id === "string") saved.set(record.id, record);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const keys = entries.map((e) => keyOf(e.url));
|
|
365
|
+
if (current.url) keys.push(current.url);
|
|
366
|
+
|
|
367
|
+
let restored = 0;
|
|
368
|
+
for (const key of keys) {
|
|
369
|
+
if (health.has(key)) continue;
|
|
370
|
+
const record = saved.get(healthId(key));
|
|
371
|
+
if (!record) continue;
|
|
372
|
+
const cooldownUntil = Number(record.cooldownUntil);
|
|
373
|
+
const failures = Number(record.failures);
|
|
374
|
+
health.set(key, {
|
|
375
|
+
failures: Number.isFinite(failures) && failures > 0 ? Math.floor(failures) : 0,
|
|
376
|
+
cooldownUntil: Number.isFinite(cooldownUntil) && cooldownUntil > now ? cooldownUntil : 0,
|
|
377
|
+
lastError: typeof record.lastError === "string" ? record.lastError : null,
|
|
378
|
+
lastOkAt: Number.isFinite(Number(record.lastOkAt)) ? Number(record.lastOkAt) : 0,
|
|
379
|
+
lastUsedAt: Number.isFinite(Number(record.lastUsedAt)) ? Number(record.lastUsedAt) : 0,
|
|
380
|
+
lastStatus: typeof record.lastStatus === "string" ? record.lastStatus : null,
|
|
381
|
+
});
|
|
382
|
+
restored += 1;
|
|
383
|
+
}
|
|
384
|
+
return restored;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function normalizeStrategy(raw) {
|
|
388
|
+
const value = String(raw || "").trim().toLowerCase();
|
|
389
|
+
return STRATEGIES.has(value) ? value : DEFAULT_STRATEGY;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function normalizeCooldowns(raw) {
|
|
393
|
+
const base = Number(raw?.cooldownMs);
|
|
394
|
+
const max = Number(raw?.maxCooldownMs);
|
|
395
|
+
return {
|
|
396
|
+
baseMs: Number.isFinite(base) && base >= 1000 ? Math.min(base, 3600000) : DEFAULT_COOLDOWN_MS,
|
|
397
|
+
maxMs: Number.isFinite(max) && max >= 1000 ? Math.min(max, 7200000) : DEFAULT_MAX_COOLDOWN_MS,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function loadFromDisk() {
|
|
402
|
+
const envPool = (process.env[POOL_ENV] || "").trim();
|
|
403
|
+
const envSingle = (process.env[ENV_OVERRIDE] || "").trim();
|
|
404
|
+
|
|
405
|
+
if (envPool) {
|
|
406
|
+
const spec = parsePoolSpec(envPool);
|
|
407
|
+
pool = {
|
|
408
|
+
entries: spec.entries,
|
|
409
|
+
errors: spec.errors,
|
|
410
|
+
strategy: normalizeStrategy(process.env[STRATEGY_ENV]),
|
|
411
|
+
source: "env",
|
|
412
|
+
error: spec.errors.length > 0 ? spec.errors.join(" ") : null,
|
|
413
|
+
};
|
|
414
|
+
current = { url: "", source: "env", error: null };
|
|
415
|
+
pruneHealth(pool.entries);
|
|
416
|
+
restoreHealth(pool.entries);
|
|
417
|
+
return pool;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (envSingle) {
|
|
421
|
+
const parsed = parseProxyUrl(envSingle);
|
|
422
|
+
if (parsed.ok) {
|
|
423
|
+
current = { url: parsed.url, source: "env", error: null };
|
|
424
|
+
} else {
|
|
425
|
+
current = { url: "", source: "env", error: `${ENV_OVERRIDE}: ${parsed.error}` };
|
|
426
|
+
}
|
|
427
|
+
pool = { entries: [], errors: [], strategy: DEFAULT_STRATEGY, source: "env", error: null };
|
|
428
|
+
pruneHealth([]);
|
|
429
|
+
restoreHealth([]);
|
|
430
|
+
return pool;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
let raw = null;
|
|
434
|
+
try {
|
|
435
|
+
raw = fs.readFileSync(configFile(), "utf8");
|
|
436
|
+
} catch {
|
|
437
|
+
current = { url: "", source: "none", error: null };
|
|
438
|
+
pool = { entries: [], errors: [], strategy: DEFAULT_STRATEGY, source: "none", error: null };
|
|
439
|
+
pruneHealth([]);
|
|
440
|
+
restoreHealth([]);
|
|
441
|
+
return pool;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let json = null;
|
|
445
|
+
try {
|
|
446
|
+
json = JSON.parse(raw);
|
|
447
|
+
} catch (err) {
|
|
448
|
+
// A corrupt file must not stop the bridge from starting. Direct egress is
|
|
449
|
+
// the safe fallback, and `error` surfaces the reason in the panel.
|
|
450
|
+
current = { url: "", source: "none", error: `proxy.json tidak terbaca: ${err.message}` };
|
|
451
|
+
pool = { entries: [], errors: [], strategy: DEFAULT_STRATEGY, source: "none", error: current.error };
|
|
452
|
+
pruneHealth([]);
|
|
453
|
+
restoreHealth([]);
|
|
454
|
+
return pool;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const entries = [];
|
|
458
|
+
const errors = [];
|
|
459
|
+
if (Array.isArray(json?.pool)) {
|
|
460
|
+
json.pool.forEach((item, index) => {
|
|
461
|
+
const parsed = parsePoolEntry(item, index);
|
|
462
|
+
if (parsed.ok) entries.push(parsed.entry);
|
|
463
|
+
else errors.push(parsed.error);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const single = parseProxyUrl(json?.url);
|
|
468
|
+
current = single.ok
|
|
469
|
+
? { url: single.url, source: single.url ? "file" : "none", error: null }
|
|
470
|
+
: { url: "", source: "none", error: single.error };
|
|
471
|
+
|
|
472
|
+
pool = {
|
|
473
|
+
entries,
|
|
474
|
+
errors,
|
|
475
|
+
strategy: normalizeStrategy(json?.strategy),
|
|
476
|
+
source: entries.length > 0 ? "file" : "none",
|
|
477
|
+
error: errors.length > 0 ? errors.join(" ") : null,
|
|
478
|
+
};
|
|
479
|
+
cooldowns = normalizeCooldowns(json);
|
|
480
|
+
pruneHealth(entries);
|
|
481
|
+
restoreHealth(entries);
|
|
482
|
+
return pool;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function getProxyUrl() {
|
|
486
|
+
return current.url;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Enabled entries in config order; the pool's working set. */
|
|
490
|
+
function activeEntries() {
|
|
491
|
+
return pool.entries.filter((e) => e.enabled);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function isCooling(url, now = Date.now()) {
|
|
495
|
+
const h = health.get(keyOf(url));
|
|
496
|
+
return Boolean(h && h.cooldownUntil > now);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* The egress for one request.
|
|
501
|
+
*
|
|
502
|
+
* Never returns empty while a pool has enabled entries: when every entry is
|
|
503
|
+
* cooling, the one whose cooldown ends first is used anyway. Refusing to send
|
|
504
|
+
* would turn a throttled pool into a dead bridge, and upstream answers a
|
|
505
|
+
* throttled IP with a normal refusal the caller already handles.
|
|
506
|
+
*
|
|
507
|
+
* `exclude` lets a retry avoid the egress that just failed.
|
|
508
|
+
*/
|
|
509
|
+
function pick({ exclude = null, now = Date.now() } = {}) {
|
|
510
|
+
const entries = activeEntries();
|
|
511
|
+
|
|
512
|
+
if (entries.length === 0) {
|
|
513
|
+
return { url: current.url, id: null, label: null, source: current.url ? "single" : "direct", cooling: false };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// `null` means "no preference", which must not be confused with the direct
|
|
517
|
+
// entry's empty URL — both would map to the same health key.
|
|
518
|
+
const excluded = exclude === null || exclude === undefined ? null : keyOf(exclude);
|
|
519
|
+
let candidates = excluded === null ? entries.slice() : entries.filter((e) => keyOf(e.url) !== excluded);
|
|
520
|
+
if (candidates.length === 0) candidates = entries.slice();
|
|
521
|
+
|
|
522
|
+
const healthy = candidates.filter((e) => !isCooling(e.url, now));
|
|
523
|
+
let chosen;
|
|
524
|
+
let cooling = false;
|
|
525
|
+
|
|
526
|
+
if (healthy.length > 0) {
|
|
527
|
+
if (pool.strategy === "random") {
|
|
528
|
+
// Weighted pick: a weight only has to be relative within the pool.
|
|
529
|
+
const total = healthy.reduce((sum, e) => sum + e.weight, 0);
|
|
530
|
+
let roll = Math.random() * total;
|
|
531
|
+
chosen = healthy[healthy.length - 1];
|
|
532
|
+
for (const e of healthy) {
|
|
533
|
+
roll -= e.weight;
|
|
534
|
+
if (roll <= 0) {
|
|
535
|
+
chosen = e;
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
} else if (pool.strategy === "failover") {
|
|
540
|
+
// Sticky: keep using the first healthy entry, which preserves upstream
|
|
541
|
+
// prompt caching on that IP until it is throttled.
|
|
542
|
+
chosen = healthy[0];
|
|
543
|
+
} else {
|
|
544
|
+
// Round-robin across healthy candidates, skipping ones that are cooling.
|
|
545
|
+
// `candidates` already has `exclude` removed, so a retry that must leave a
|
|
546
|
+
// failed egress does not come back to it here.
|
|
547
|
+
chosen = healthy[cursor % healthy.length];
|
|
548
|
+
cursor = (cursor + 1) % Number.MAX_SAFE_INTEGER;
|
|
549
|
+
}
|
|
550
|
+
} else {
|
|
551
|
+
// Everything is throttled: use whichever recovers first.
|
|
552
|
+
cooling = true;
|
|
553
|
+
chosen = candidates
|
|
554
|
+
.slice()
|
|
555
|
+
.sort((a, b) => (health.get(keyOf(a.url))?.cooldownUntil || 0) - (health.get(keyOf(b.url))?.cooldownUntil || 0))[0];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (!chosen) {
|
|
559
|
+
return { url: current.url, id: null, label: null, source: current.url ? "single" : "direct", cooling };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const h = healthFor(chosen.url);
|
|
563
|
+
h.lastUsedAt = now;
|
|
564
|
+
return {
|
|
565
|
+
url: chosen.url,
|
|
566
|
+
id: chosen.id,
|
|
567
|
+
label: chosen.label,
|
|
568
|
+
// `direct` means "this process's own address", whether it came from a pool
|
|
569
|
+
// entry or because no pool exists.
|
|
570
|
+
source: chosen.direct ? "direct" : "pool",
|
|
571
|
+
cooling,
|
|
572
|
+
cooldownUntil: h.cooldownUntil || 0,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Record that an egress was refused by upstream. The cooldown doubles per
|
|
578
|
+
* consecutive failure so a spent IP is retried less and less often, capped at
|
|
579
|
+
* `maxMs`; a success resets both.
|
|
580
|
+
*
|
|
581
|
+
* Only upstream throttling belongs here. A transport error (proxy unreachable)
|
|
582
|
+
* is also a reason to move on, but it must not look like a spent allowance, so
|
|
583
|
+
* the caller passes the reason and it is shown separately in the panel.
|
|
584
|
+
*/
|
|
585
|
+
function markFailure(egressUrl, { reason = "throttled", now = Date.now(), cooldownMs = null } = {}) {
|
|
586
|
+
// An empty string is the `direct` entry, which is a real egress; only a
|
|
587
|
+
// missing target (null/undefined) means "nothing to record".
|
|
588
|
+
if (egressUrl === null || egressUrl === undefined) return null;
|
|
589
|
+
const h = healthFor(egressUrl);
|
|
590
|
+
h.failures += 1;
|
|
591
|
+
h.lastError = String(reason).slice(0, 200);
|
|
592
|
+
const base = Number.isFinite(cooldownMs) && cooldownMs > 0 ? cooldownMs : cooldowns.baseMs;
|
|
593
|
+
const backoff = Math.min(base * Math.pow(2, h.failures - 1), Math.max(cooldowns.maxMs, base));
|
|
594
|
+
h.cooldownUntil = now + backoff;
|
|
595
|
+
h.lastStatus = reason;
|
|
596
|
+
// A refusal is the expensive thing to rediscover: write it before the caller
|
|
597
|
+
// can act on it, so a restart in the next second does not forget the IP.
|
|
598
|
+
writeHealthNow(now);
|
|
599
|
+
return { failures: h.failures, cooldownUntil: h.cooldownUntil, backoffMs: backoff };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Clear an egress's failure history; called after a request succeeds. */
|
|
603
|
+
function markSuccess(egressUrl, { now = Date.now() } = {}) {
|
|
604
|
+
if (egressUrl === null || egressUrl === undefined) return null;
|
|
605
|
+
const h = healthFor(egressUrl);
|
|
606
|
+
h.failures = 0;
|
|
607
|
+
h.cooldownUntil = 0;
|
|
608
|
+
h.lastError = null;
|
|
609
|
+
h.lastOkAt = now;
|
|
610
|
+
h.lastStatus = "ok";
|
|
611
|
+
// Every request lands here, so this one is coalesced.
|
|
612
|
+
scheduleHealthWrite();
|
|
613
|
+
return h;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function resetHealth() {
|
|
617
|
+
health.clear();
|
|
618
|
+
cursor = 0;
|
|
619
|
+
writeHealthNow();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Public view of one entry: host:port, health, and never the URL's credentials. */
|
|
623
|
+
function describeEntry(entry, now = Date.now()) {
|
|
624
|
+
const h = health.get(keyOf(entry.url));
|
|
625
|
+
const cooling = Boolean(h && h.cooldownUntil > now);
|
|
626
|
+
return {
|
|
627
|
+
id: entry.id,
|
|
628
|
+
label: entry.label,
|
|
629
|
+
host: entry.direct ? "direct" : displayHost(entry.url),
|
|
630
|
+
enabled: entry.enabled,
|
|
631
|
+
weight: entry.weight,
|
|
632
|
+
state: !entry.enabled ? "disabled" : cooling ? "cooling" : "ready",
|
|
633
|
+
cooldownMsLeft: cooling ? Math.max(0, h.cooldownUntil - now) : 0,
|
|
634
|
+
cooldownUntil: cooling ? h.cooldownUntil : 0,
|
|
635
|
+
failures: h ? h.failures : 0,
|
|
636
|
+
lastError: h ? h.lastError : null,
|
|
637
|
+
lastOkAt: h ? h.lastOkAt || null : null,
|
|
638
|
+
lastUsedAt: h ? h.lastUsedAt || null : null,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function describe(now = Date.now()) {
|
|
643
|
+
const entries = pool.entries;
|
|
644
|
+
const active = entries.filter((e) => e.enabled);
|
|
645
|
+
const ready = active.filter((e) => !isCooling(e.url, now));
|
|
646
|
+
const poolActive = active.length > 0;
|
|
647
|
+
const singleActive = !poolActive && Boolean(current.url);
|
|
648
|
+
|
|
649
|
+
const poolView = {
|
|
650
|
+
active: poolActive,
|
|
651
|
+
size: entries.length,
|
|
652
|
+
enabled: active.length,
|
|
653
|
+
ready: ready.length,
|
|
654
|
+
cooling: active.length - ready.length,
|
|
655
|
+
strategy: pool.strategy,
|
|
656
|
+
source: pool.source,
|
|
657
|
+
cooldownMs: cooldowns.baseMs,
|
|
658
|
+
maxCooldownMs: cooldowns.maxMs,
|
|
659
|
+
error: pool.error,
|
|
660
|
+
entries: entries.map((e) => describeEntry(e, now)),
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
mode: poolActive ? "pool" : singleActive ? "proxy" : "direct",
|
|
665
|
+
proxy: poolActive ? `${active.length} proxy` : displayHost(current.url),
|
|
666
|
+
hasCredentials: poolActive
|
|
667
|
+
? entries.some((e) => /\/\/[^/@]+@/.test(e.url))
|
|
668
|
+
: current.url
|
|
669
|
+
? /\/\/[^/@]+@/.test(current.url)
|
|
670
|
+
: false,
|
|
671
|
+
source: poolActive ? pool.source : current.source,
|
|
672
|
+
locked: (poolActive ? pool.source : current.source) === "env",
|
|
673
|
+
configPath: configFile(),
|
|
674
|
+
error: current.error || pool.error || null,
|
|
675
|
+
pool: poolView,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** Write the whole config atomically-ish: one file, owner-only. */
|
|
680
|
+
function writeConfig(next) {
|
|
681
|
+
try {
|
|
682
|
+
fs.mkdirSync(path.dirname(configFile()), { recursive: true });
|
|
683
|
+
fs.writeFileSync(configFile(), `${JSON.stringify(next, null, 2)}\n`, {
|
|
684
|
+
encoding: "utf8",
|
|
685
|
+
mode: 0o600,
|
|
686
|
+
});
|
|
687
|
+
// A file created earlier keeps its old mode; chmod so credentials created by
|
|
688
|
+
// an older release are not left readable by every local user.
|
|
689
|
+
fs.chmodSync(configFile(), 0o600);
|
|
690
|
+
return { ok: true };
|
|
691
|
+
} catch (err) {
|
|
692
|
+
return { ok: false, error: `Gagal menulis ${configFile()}: ${err.message}` };
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** Current on-disk shape, so one setter never drops the other's fields. */
|
|
697
|
+
function readConfigObject() {
|
|
698
|
+
try {
|
|
699
|
+
const json = JSON.parse(fs.readFileSync(configFile(), "utf8"));
|
|
700
|
+
return json && typeof json === "object" ? json : {};
|
|
701
|
+
} catch {
|
|
702
|
+
return {};
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function refuseWhenLocked() {
|
|
707
|
+
if (pool.source === "env" || current.source === "env") {
|
|
708
|
+
return {
|
|
709
|
+
ok: false,
|
|
710
|
+
error: `${POOL_ENV}/${ENV_OVERRIDE} diset di environment. Ubah variabel itu lalu restart service.`,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Persist a new single-proxy setting. `raw` may be empty to go back to direct
|
|
718
|
+
* egress.
|
|
719
|
+
*
|
|
720
|
+
* `UPSTREAM_PROXY_URL` is an operator override, so a runtime edit is refused
|
|
721
|
+
* instead of silently losing to the environment on the next restart.
|
|
722
|
+
*/
|
|
723
|
+
function setProxyUrl(raw) {
|
|
724
|
+
const locked = refuseWhenLocked();
|
|
725
|
+
if (locked) return locked;
|
|
726
|
+
const parsed = parseProxyUrl(raw);
|
|
727
|
+
if (!parsed.ok) return { ok: false, error: parsed.error };
|
|
728
|
+
|
|
729
|
+
const config = readConfigObject();
|
|
730
|
+
|
|
731
|
+
if (!parsed.url) {
|
|
732
|
+
delete config.url;
|
|
733
|
+
if (Array.isArray(config.pool) && config.pool.length > 0) {
|
|
734
|
+
if (!writeConfig(config).ok) return { ok: false, error: `Gagal menulis ${configFile()}` };
|
|
735
|
+
} else {
|
|
736
|
+
try {
|
|
737
|
+
fs.unlinkSync(configFile());
|
|
738
|
+
} catch (err) {
|
|
739
|
+
if (err.code !== "ENOENT") {
|
|
740
|
+
return { ok: false, error: `Gagal menghapus ${configFile()}: ${err.message}` };
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
current = { url: "", source: "none", error: null };
|
|
745
|
+
loadFromDisk();
|
|
746
|
+
return { ok: true, ...describe() };
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
config.url = parsed.url;
|
|
750
|
+
const written = writeConfig(config);
|
|
751
|
+
if (!written.ok) return written;
|
|
752
|
+
current = { url: parsed.url, source: "file", error: null };
|
|
753
|
+
return { ok: true, ...describe() };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Replace the pool. `raw` accepts:
|
|
758
|
+
* - an array of entries (`{id,label,url,enabled,weight}` or bare URL strings)
|
|
759
|
+
* - `{ entries: [...], strategy, cooldownMs, maxCooldownMs }`
|
|
760
|
+
* - `[]` / `{ entries: [] }` to clear it and fall back to direct/single egress
|
|
761
|
+
*/
|
|
762
|
+
function setPool(raw) {
|
|
763
|
+
const locked = refuseWhenLocked();
|
|
764
|
+
if (locked) return locked;
|
|
765
|
+
|
|
766
|
+
const spec = Array.isArray(raw) ? { entries: raw } : raw && typeof raw === "object" ? raw : null;
|
|
767
|
+
if (!spec) return { ok: false, error: "Format pool tidak valid." };
|
|
768
|
+
|
|
769
|
+
const source = Array.isArray(spec.entries) ? spec.entries : [];
|
|
770
|
+
const entries = [];
|
|
771
|
+
const errors = [];
|
|
772
|
+
source.forEach((item, index) => {
|
|
773
|
+
const parsed = parsePoolEntry(item, index);
|
|
774
|
+
if (parsed.ok) entries.push(parsed.entry);
|
|
775
|
+
else errors.push(parsed.error);
|
|
776
|
+
});
|
|
777
|
+
if (errors.length > 0) return { ok: false, error: errors.join(" ") };
|
|
778
|
+
|
|
779
|
+
const seen = new Set();
|
|
780
|
+
for (const entry of entries) {
|
|
781
|
+
const key = keyOf(entry.url);
|
|
782
|
+
if (seen.has(key)) {
|
|
783
|
+
return { ok: false, error: `Egress ganda di pool: ${entry.direct ? "direct" : displayHost(entry.url)}` };
|
|
784
|
+
}
|
|
785
|
+
seen.add(key);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const config = readConfigObject();
|
|
789
|
+
if (entries.length === 0) {
|
|
790
|
+
delete config.pool;
|
|
791
|
+
} else {
|
|
792
|
+
config.pool = entries;
|
|
793
|
+
}
|
|
794
|
+
if (spec.strategy !== undefined) config.strategy = normalizeStrategy(spec.strategy);
|
|
795
|
+
if (spec.cooldownMs !== undefined) config.cooldownMs = normalizeCooldowns({ cooldownMs: spec.cooldownMs }).baseMs;
|
|
796
|
+
if (spec.maxCooldownMs !== undefined) {
|
|
797
|
+
config.maxCooldownMs = normalizeCooldowns({ maxCooldownMs: spec.maxCooldownMs }).maxMs;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (!config.url && !config.pool) {
|
|
801
|
+
try {
|
|
802
|
+
fs.unlinkSync(configFile());
|
|
803
|
+
} catch (err) {
|
|
804
|
+
if (err.code !== "ENOENT") return { ok: false, error: `Gagal menghapus ${configFile()}: ${err.message}` };
|
|
805
|
+
}
|
|
806
|
+
} else {
|
|
807
|
+
const written = writeConfig(config);
|
|
808
|
+
if (!written.ok) return written;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Deliberately not `resetHealth()`: an operator saving the pool (even just to
|
|
812
|
+
// change the strategy) must not forget which IPs are spent. In-memory health
|
|
813
|
+
// wins over the file during reload, so unchanged entries keep their cooldown
|
|
814
|
+
// and removed ones were just pruned.
|
|
815
|
+
loadFromDisk();
|
|
816
|
+
cursor = 0;
|
|
817
|
+
for (const entry of entries) healthFor(entry.url);
|
|
818
|
+
writeHealthNow();
|
|
819
|
+
return { ok: true, ...describe() };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Add `proxy:` to a fetch init. Callers pass their own init untouched, so a
|
|
824
|
+
* request that pins its own proxy keeps it.
|
|
825
|
+
*
|
|
826
|
+
* With a pool this picks an egress per call. A caller that needs one egress for
|
|
827
|
+
* several fetches (a retry that must stay on the IP that answered) should call
|
|
828
|
+
* `pick()` once and pass the result explicitly instead.
|
|
829
|
+
*
|
|
830
|
+
* An empty target is meaningful: it selects the `direct` pool entry, which is
|
|
831
|
+
* this process's own address, so the fetch goes out without a proxy.
|
|
832
|
+
*/
|
|
833
|
+
function withProxy(init, egressUrl = null) {
|
|
834
|
+
if (init && init.proxy) return init;
|
|
835
|
+
// A caller that pins an egress passes the URL, and "" is the direct entry —
|
|
836
|
+
// distinct from passing nothing, which asks the pool to choose.
|
|
837
|
+
const target = egressUrl === null || egressUrl === undefined ? pick().url : egressUrl;
|
|
838
|
+
if (!target) return init;
|
|
839
|
+
return { ...(init || {}), proxy: target };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** fetch() through the resolved egress. The single egress entry point. */
|
|
843
|
+
function netFetch(url, init, egressUrl = null) {
|
|
844
|
+
return fetch(url, withProxy(init, egressUrl));
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Prove one proxy works before it becomes the live egress.
|
|
849
|
+
*
|
|
850
|
+
* Two probes: the API the bridge actually calls, and a trace endpoint that
|
|
851
|
+
* answers with the exit IP so the panel can show that egress moved. The API
|
|
852
|
+
* probe is the one that decides `ok`; the IP probe is best-effort.
|
|
853
|
+
*/
|
|
854
|
+
async function testProxy(url, { timeoutMs = 15000, probeUrl = null } = {}) {
|
|
855
|
+
if (typeof Bun === "undefined") {
|
|
856
|
+
return { ok: false, error: "Proxy egress hanya berjalan di Bun; runtime Node tidak mendukung opsi proxy." };
|
|
857
|
+
}
|
|
858
|
+
const parsed = parseProxyUrl(url);
|
|
859
|
+
if (!parsed.ok) return { ok: false, error: parsed.error };
|
|
860
|
+
if (!parsed.url) return { ok: false, error: "URL proxy kosong." };
|
|
861
|
+
|
|
862
|
+
const started = Date.now();
|
|
863
|
+
const exit = await probeExitIp(parsed.url, timeoutMs);
|
|
864
|
+
let res;
|
|
865
|
+
try {
|
|
866
|
+
// Default matches the shipped wire endpoint. The caller passes the live one
|
|
867
|
+
// so a catalog migration does not turn a working proxy into a false failure.
|
|
868
|
+
res = await fetch(probeUrl || "https://opencode.ai/inference/v1/models", {
|
|
869
|
+
proxy: parsed.url,
|
|
870
|
+
headers: { Authorization: "Bearer public" },
|
|
871
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
872
|
+
});
|
|
873
|
+
} catch (err) {
|
|
874
|
+
return {
|
|
875
|
+
ok: false,
|
|
876
|
+
exitIp: exit.ip,
|
|
877
|
+
latencyMs: Date.now() - started,
|
|
878
|
+
error: `Tidak bisa menghubungi upstream lewat proxy: ${err.message}`,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
const latencyMs = Date.now() - started;
|
|
882
|
+
if (!res.ok) {
|
|
883
|
+
return {
|
|
884
|
+
ok: false,
|
|
885
|
+
apiStatus: res.status,
|
|
886
|
+
exitIp: exit.ip,
|
|
887
|
+
latencyMs,
|
|
888
|
+
error:
|
|
889
|
+
res.status === 403
|
|
890
|
+
? "Proxy tersambung, tapi upstream menolak egress-nya (403). Coba proxy lain."
|
|
891
|
+
: `Proxy tersambung, upstream menjawab ${res.status}.`,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
return { ok: true, apiStatus: res.status, exitIp: exit.ip, latencyMs, exitError: exit.error || null };
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Probe every entry of the pool concurrently, and record the verdict in the
|
|
899
|
+
* health map so the panel's result is also what the next request uses. A
|
|
900
|
+
* throttled entry goes into cooldown; a working one is cleared.
|
|
901
|
+
*/
|
|
902
|
+
async function testPool({ probeUrl = null, timeoutMs = 15000 } = {}) {
|
|
903
|
+
const entries = pool.entries.slice();
|
|
904
|
+
if (entries.length === 0) return { ok: true, entries: [] };
|
|
905
|
+
const results = await Promise.all(
|
|
906
|
+
entries.map(async (entry) => {
|
|
907
|
+
if (!entry.enabled) return { id: entry.id, label: entry.label, host: displayHost(entry.url), skipped: true };
|
|
908
|
+
const verdict = await testProxy(entry.url, { probeUrl, timeoutMs });
|
|
909
|
+
if (verdict.ok) markSuccess(entry.url);
|
|
910
|
+
else if (verdict.apiStatus === 403) markFailure(entry.url, { reason: "upstream 403" });
|
|
911
|
+
const h = health.get(entry.url);
|
|
912
|
+
return {
|
|
913
|
+
id: entry.id,
|
|
914
|
+
label: entry.label,
|
|
915
|
+
host: displayHost(entry.url),
|
|
916
|
+
ok: verdict.ok,
|
|
917
|
+
apiStatus: verdict.apiStatus || null,
|
|
918
|
+
exitIp: verdict.exitIp || null,
|
|
919
|
+
latencyMs: verdict.latencyMs || null,
|
|
920
|
+
error: verdict.error || null,
|
|
921
|
+
state: h && h.cooldownUntil > Date.now() ? "cooling" : verdict.ok ? "ready" : "error",
|
|
922
|
+
};
|
|
923
|
+
})
|
|
924
|
+
);
|
|
925
|
+
return { ok: results.some((r) => r.ok), entries: results };
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
async function probeExitIp(proxyUrl, timeoutMs) {
|
|
929
|
+
try {
|
|
930
|
+
const res = await fetch("https://www.cloudflare.com/cdn-cgi/trace", {
|
|
931
|
+
proxy: proxyUrl,
|
|
932
|
+
signal: AbortSignal.timeout(Math.min(timeoutMs, 8000)),
|
|
933
|
+
});
|
|
934
|
+
if (!res.ok) return { ip: null, error: `trace HTTP ${res.status}` };
|
|
935
|
+
const text = await res.text();
|
|
936
|
+
const ip = (text.match(/^ip=(.+)$/m) || [])[1] || null;
|
|
937
|
+
return { ip, error: ip ? null : "trace tanpa field ip" };
|
|
938
|
+
} catch (err) {
|
|
939
|
+
return { ip: null, error: err.message };
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
loadFromDisk();
|
|
944
|
+
|
|
945
|
+
module.exports = {
|
|
946
|
+
ENV_OVERRIDE,
|
|
947
|
+
POOL_ENV,
|
|
948
|
+
STRATEGY_ENV,
|
|
949
|
+
configFile,
|
|
950
|
+
healthFile,
|
|
951
|
+
parseProxyUrl,
|
|
952
|
+
parsePoolEntry,
|
|
953
|
+
parsePoolSpec,
|
|
954
|
+
displayHost,
|
|
955
|
+
getProxyUrl,
|
|
956
|
+
getPool: () => pool.entries.slice(),
|
|
957
|
+
describe,
|
|
958
|
+
setProxyUrl,
|
|
959
|
+
setPool,
|
|
960
|
+
pick,
|
|
961
|
+
markFailure,
|
|
962
|
+
markSuccess,
|
|
963
|
+
resetHealth,
|
|
964
|
+
withProxy,
|
|
965
|
+
netFetch,
|
|
966
|
+
testProxy,
|
|
967
|
+
testPool,
|
|
968
|
+
__setForTest(state) {
|
|
969
|
+
current = state;
|
|
970
|
+
},
|
|
971
|
+
__reload: loadFromDisk,
|
|
972
|
+
/** Simulate a process start: in-memory state gone, everything re-read from disk. */
|
|
973
|
+
__restart() {
|
|
974
|
+
health.clear();
|
|
975
|
+
cursor = 0;
|
|
976
|
+
loadFromDisk();
|
|
977
|
+
},
|
|
978
|
+
/** Force the coalesced health write, so a test can assert persisted state. */
|
|
979
|
+
__flushHealth: () => writeHealthNow(),
|
|
980
|
+
};
|