@remodex/rmx 1.0.2
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/AGENTS_INSTALL.md +91 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +584 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-CZqebSPQ.css +1 -0
- package/gui/dist/assets/index-CkETtt7P.js +71 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/fonts/google-sans-cyrillic.woff2 +0 -0
- package/gui/dist/fonts/google-sans-latin.woff2 +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +2 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +118 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic.ts +1205 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +83 -0
- package/src/adapters/client-fingerprint.ts +59 -0
- package/src/adapters/command-code.ts +453 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/cursor-errors.ts +165 -0
- package/src/adapters/cursor/discovery.ts +276 -0
- package/src/adapters/cursor/effort-map.ts +139 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +250 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +153 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1235 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +59 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +332 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +548 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +604 -0
- package/src/adapters/cursor/protobuf-events.ts +735 -0
- package/src/adapters/cursor/protobuf-request.ts +719 -0
- package/src/adapters/cursor/request-builder.ts +280 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +621 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +57 -0
- package/src/adapters/cursor/types.ts +59 -0
- package/src/adapters/cursor.ts +196 -0
- package/src/adapters/google-antigravity-replay.ts +520 -0
- package/src/adapters/google-antigravity-wire.ts +140 -0
- package/src/adapters/google-errors.ts +85 -0
- package/src/adapters/google-http.ts +100 -0
- package/src/adapters/google-tool-schema.ts +173 -0
- package/src/adapters/google-truncation.ts +24 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +859 -0
- package/src/adapters/identity.ts +77 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +208 -0
- package/src/adapters/kiro-events.ts +197 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +104 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +224 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1924 -0
- package/src/adapters/mimo-free.ts +263 -0
- package/src/adapters/openai-chat.ts +1265 -0
- package/src/adapters/openai-responses.ts +1309 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/android-remote/assets.ts +218 -0
- package/src/android-remote/attachments.ts +168 -0
- package/src/android-remote/auth.ts +237 -0
- package/src/android-remote/cloudflare-provisioning.ts +409 -0
- package/src/android-remote/cloudflare-secret.ts +106 -0
- package/src/android-remote/cloudflare-tunnel.ts +488 -0
- package/src/android-remote/cloudflared.ts +286 -0
- package/src/android-remote/codex-app-server.ts +565 -0
- package/src/android-remote/desktop-history-page.ts +936 -0
- package/src/android-remote/desktop-ipc.ts +3643 -0
- package/src/android-remote/desktop-ownership-store.ts +98 -0
- package/src/android-remote/desktop-project-registration.ts +129 -0
- package/src/android-remote/desktop-session-stream.ts +1110 -0
- package/src/android-remote/desktop-workspace-state.ts +443 -0
- package/src/android-remote/file-change-parser.ts +112 -0
- package/src/android-remote/gateway.ts +9108 -0
- package/src/android-remote/mutation-store.ts +299 -0
- package/src/android-remote/projection.ts +1780 -0
- package/src/android-remote/queued-turn-store.ts +248 -0
- package/src/android-remote/session-command-recovery.ts +1384 -0
- package/src/android-remote/store.ts +466 -0
- package/src/android-remote/thread-reconciliation.ts +133 -0
- package/src/android-remote/thread-source-paths.ts +307 -0
- package/src/android-remote/thread-stream.ts +546 -0
- package/src/android-remote/turn-activity.ts +235 -0
- package/src/android-remote/user-message-identity.ts +94 -0
- package/src/bridge.ts +1793 -0
- package/src/chat/inbound.ts +295 -0
- package/src/chat/outbound.ts +821 -0
- package/src/claude/agents-inject.ts +267 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +189 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +601 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +70 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +519 -0
- package/src/claude/model-info.ts +154 -0
- package/src/claude/outbound.ts +898 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +296 -0
- package/src/cli/account-auth.ts +250 -0
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +476 -0
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +297 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +184 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-desktop.ts +211 -0
- package/src/cli/claude.ts +302 -0
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/codex-shim-readiness.ts +69 -0
- package/src/cli/combo.ts +124 -0
- package/src/cli/config-command.ts +183 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/desktop-first-run.ts +25 -0
- package/src/cli/doctor.ts +1022 -0
- package/src/cli/export-command.ts +201 -0
- package/src/cli/help.ts +370 -0
- package/src/cli/index.ts +1565 -0
- package/src/cli/init.ts +224 -0
- package/src/cli/integrations.ts +225 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/internal-dispatch.ts +35 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/models-runtime.ts +224 -0
- package/src/cli/models.ts +340 -0
- package/src/cli/observe.ts +170 -0
- package/src/cli/opencode.ts +587 -0
- package/src/cli/provider-runtime.ts +179 -0
- package/src/cli/provider.ts +476 -0
- package/src/cli/ready.ts +301 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +328 -0
- package/src/cli/star-prompt.ts +211 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +321 -0
- package/src/cli/system-command.ts +196 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +205 -0
- package/src/cli/v2.ts +200 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +1109 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +34 -0
- package/src/codex/account-lifecycle.ts +172 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +195 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +517 -0
- package/src/codex/account-usability.ts +40 -0
- package/src/codex/admission.ts +263 -0
- package/src/codex/app-server-processes.ts +799 -0
- package/src/codex/auth-api.ts +2098 -0
- package/src/codex/auth-collision.ts +107 -0
- package/src/codex/auth-context.ts +480 -0
- package/src/codex/autostart-health.ts +156 -0
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +471 -0
- package/src/codex/catalog/bundled.ts +533 -0
- package/src/codex/catalog/effort.ts +432 -0
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +287 -0
- package/src/codex/catalog/native-models.ts +7 -0
- package/src/codex/catalog/parsing.ts +503 -0
- package/src/codex/catalog/provider-fetch.ts +2267 -0
- package/src/codex/catalog/sync.ts +1606 -0
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-refresh-status.ts +87 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +15 -0
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +580 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/desktop-client-processes.ts +521 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +1091 -0
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +347 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +115 -0
- package/src/codex/history-provider.ts +1075 -0
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +204 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject-coordination.ts +257 -0
- package/src/codex/inject.ts +1857 -0
- package/src/codex/injected-marker.ts +79 -0
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +105 -0
- package/src/codex/journal.ts +172 -0
- package/src/codex/main-account-cache.ts +56 -0
- package/src/codex/main-account.ts +40 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/model-cache.ts +267 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +348 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +691 -0
- package/src/codex/paths.ts +78 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +295 -0
- package/src/codex/project-config-warnings.ts +426 -0
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/provider-adoption.ts +242 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +494 -0
- package/src/codex/refresh.ts +60 -0
- package/src/codex/routing.ts +1855 -0
- package/src/codex/runtime.ts +659 -0
- package/src/codex/shim.ts +1215 -0
- package/src/codex/subagent-defaults.ts +557 -0
- package/src/codex/subagent-model-fallback.ts +560 -0
- package/src/codex/sync.ts +238 -0
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +374 -0
- package/src/codex/warmup.ts +192 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +140 -0
- package/src/combos/index.ts +44 -0
- package/src/combos/request.ts +64 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +392 -0
- package/src/config.ts +3270 -0
- package/src/generated/model-metadata.ts +144 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +540 -0
- package/src/grok/inspect.ts +45 -0
- package/src/grok/status.ts +127 -0
- package/src/grok/sync.ts +66 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +922 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +108 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +242 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +184 -0
- package/src/lib/bun-stream-caps.ts +127 -0
- package/src/lib/config-ownership.ts +438 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +316 -0
- package/src/lib/errors.ts +364 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/pinned-http.ts +182 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +168 -0
- package/src/lib/provider-environment.ts +470 -0
- package/src/lib/provider-outbound.ts +203 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +510 -0
- package/src/lib/remodex-home.ts +616 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/service-secrets.ts +178 -0
- package/src/lib/shadow-call.ts +54 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +364 -0
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/test-home-guard.ts +98 -0
- package/src/lib/token-estimate.ts +69 -0
- package/src/lib/translator-budget.ts +366 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +508 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-elevation.ts +705 -0
- package/src/lib/windows-secret-acl.ts +817 -0
- package/src/lib/windows-user-principal.ts +283 -0
- package/src/lib/winsw.ts +402 -0
- package/src/model-sources.ts +73 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +177 -0
- package/src/oauth/callback-server.ts +294 -0
- package/src/oauth/chatgpt.ts +150 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +231 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +230 -0
- package/src/oauth/health.ts +443 -0
- package/src/oauth/index.ts +1280 -0
- package/src/oauth/key-providers.ts +128 -0
- package/src/oauth/kimi.ts +213 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +621 -0
- package/src/oauth/local-token-detect.ts +121 -0
- package/src/oauth/log.ts +48 -0
- package/src/oauth/login-cli.ts +163 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +655 -0
- package/src/oauth/token-guardian.ts +309 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +317 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +64 -0
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/command-code-efforts.ts +85 -0
- package/src/providers/context-cap.ts +73 -0
- package/src/providers/derive.ts +451 -0
- package/src/providers/free-directory.ts +187 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +271 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +361 -0
- package/src/providers/openai-sidecar.ts +235 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +301 -0
- package/src/providers/openai-virtual-models.ts +83 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +179 -0
- package/src/providers/quota.ts +1942 -0
- package/src/providers/reasoning-capabilities.ts +336 -0
- package/src/providers/registry.ts +2375 -0
- package/src/providers/slug-codec.ts +74 -0
- package/src/providers/xai-transport.ts +149 -0
- package/src/reasoning-effort.ts +243 -0
- package/src/responses/compaction.ts +124 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +714 -0
- package/src/responses/reasoning-envelope.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +106 -0
- package/src/responses/schema.ts +159 -0
- package/src/responses/spill-store.ts +431 -0
- package/src/responses/state.ts +1039 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +802 -0
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +205 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +424 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/adapter-resolve.ts +83 -0
- package/src/server/auth-cors.ts +606 -0
- package/src/server/chat-completions.ts +379 -0
- package/src/server/claude-messages.ts +980 -0
- package/src/server/effort-policy.ts +251 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +152 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +485 -0
- package/src/server/index.ts +1633 -0
- package/src/server/lifecycle.ts +482 -0
- package/src/server/live.ts +609 -0
- package/src/server/management/agent-settings-routes.ts +1180 -0
- package/src/server/management/android-remote-routes.ts +390 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +244 -0
- package/src/server/management/config-routes.ts +602 -0
- package/src/server/management/context.ts +88 -0
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +516 -0
- package/src/server/management/model-routes.ts +519 -0
- package/src/server/management/model-rows.ts +143 -0
- package/src/server/management/native-integration-routes.ts +781 -0
- package/src/server/management/oauth-account-routes.ts +573 -0
- package/src/server/management/provider-routes.ts +781 -0
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +384 -0
- package/src/server/management/shared.ts +277 -0
- package/src/server/management/sidebar-routes.ts +106 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +433 -0
- package/src/server/management/system-routes.ts +141 -0
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +269 -0
- package/src/server/management-auth.ts +353 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +156 -0
- package/src/server/proxy-liveness.ts +326 -0
- package/src/server/proxy-stop.ts +92 -0
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +353 -0
- package/src/server/relay.ts +1179 -0
- package/src/server/request-decompress.ts +132 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1072 -0
- package/src/server/responses/collaboration.ts +409 -0
- package/src/server/responses/compact.ts +710 -0
- package/src/server/responses/core.ts +3561 -0
- package/src/server/responses/encrypted-payload.ts +308 -0
- package/src/server/responses/fetch-helpers.ts +171 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/policy-fallback.ts +152 -0
- package/src/server/responses/terminal-guard.ts +230 -0
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +272 -0
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses.ts +10 -0
- package/src/server/search.ts +181 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +263 -0
- package/src/server/startup-action-control.ts +308 -0
- package/src/server/startup-health-cache.ts +119 -0
- package/src/server/system-env.ts +418 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +470 -0
- package/src/service-manager-probe.ts +824 -0
- package/src/service.ts +3011 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +364 -0
- package/src/tray/windows.ts +738 -0
- package/src/types.ts +1531 -0
- package/src/update/badge.ts +72 -0
- package/src/update/desktop-release.ts +1620 -0
- package/src/update/index.ts +402 -0
- package/src/update/job.ts +1906 -0
- package/src/update/notify.ts +261 -0
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +283 -0
- package/src/usage/log.ts +695 -0
- package/src/usage/summary.ts +585 -0
- package/src/usage/totals.ts +14 -0
- package/src/vision/anthropic-describe.ts +185 -0
- package/src/vision/describe.ts +127 -0
- package/src/vision/index.ts +558 -0
- package/src/vision/reasoning.ts +55 -0
- package/src/web-search/anthropic-executor.ts +189 -0
- package/src/web-search/executor.ts +105 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/index.ts +196 -0
- package/src/web-search/loop.ts +791 -0
- package/src/web-search/parse.ts +235 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/synthetic-tool.ts +47 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
AndroidRemoteState,
|
|
4
|
+
AndroidRemoteStore,
|
|
5
|
+
AndroidRemoteStoredClient,
|
|
6
|
+
} from "./store";
|
|
7
|
+
|
|
8
|
+
const PAIRING_TTL_MS = 5 * 60 * 1000;
|
|
9
|
+
const ACCESS_TOKEN_TTL_MS = 180 * 24 * 60 * 60 * 1000;
|
|
10
|
+
const WEBSOCKET_TICKET_TTL_MS = 30 * 1000;
|
|
11
|
+
const CLIENT_TOUCH_INTERVAL_MS = 60 * 1000;
|
|
12
|
+
|
|
13
|
+
export const ANDROID_REMOTE_SCOPES = [
|
|
14
|
+
"orchestration:read",
|
|
15
|
+
"orchestration:operate",
|
|
16
|
+
"terminal:operate",
|
|
17
|
+
"review:write",
|
|
18
|
+
"relay:read",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export type AndroidRemoteClientMetadata = {
|
|
22
|
+
label?: string;
|
|
23
|
+
deviceType?: string;
|
|
24
|
+
os?: string;
|
|
25
|
+
installationId?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type AndroidRemotePairingPayload = {
|
|
29
|
+
type: "remodex-mobile-pairing";
|
|
30
|
+
version: 1;
|
|
31
|
+
desktopName: string;
|
|
32
|
+
localUrls: string[];
|
|
33
|
+
cloudflareUrl: string | null;
|
|
34
|
+
directUrl: string;
|
|
35
|
+
pairingToken: string;
|
|
36
|
+
pairingLinkId: string;
|
|
37
|
+
expiresAt: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type AndroidRemotePairingInvitation = {
|
|
41
|
+
id: string;
|
|
42
|
+
expiresAt: string;
|
|
43
|
+
payload: AndroidRemotePairingPayload;
|
|
44
|
+
qrPayload: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type StoredInvitation = {
|
|
48
|
+
id: string;
|
|
49
|
+
tokenDigest: string;
|
|
50
|
+
expiresAtMs: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type StoredTicket = {
|
|
54
|
+
clientId: string;
|
|
55
|
+
expiresAtMs: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type AuthenticatedAndroidClient = {
|
|
59
|
+
client: AndroidRemoteStoredClient;
|
|
60
|
+
state: AndroidRemoteState;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function token(prefix: string, bytes = 32): string {
|
|
64
|
+
return `${prefix}${randomBytes(bytes).toString("base64url")}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function digestAndroidRemoteCredential(value: string): string {
|
|
68
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function equalDigest(left: string, right: string): boolean {
|
|
72
|
+
if (left.length !== right.length) return false;
|
|
73
|
+
try {
|
|
74
|
+
return timingSafeEqual(Buffer.from(left, "hex"), Buffer.from(right, "hex"));
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function cleanLabel(value: string | undefined): string {
|
|
81
|
+
const trimmed = value?.trim().slice(0, 160);
|
|
82
|
+
return trimmed || "Android phone";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function cleanOs(value: string | undefined): string {
|
|
86
|
+
const trimmed = value?.trim().slice(0, 64);
|
|
87
|
+
return trimmed || "android";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function safeRemoteAddress(value: string | undefined): string | undefined {
|
|
91
|
+
const trimmed = value?.trim().slice(0, 256);
|
|
92
|
+
return trimmed || undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class AndroidRemoteAuth {
|
|
96
|
+
private readonly invitations = new Map<string, StoredInvitation>();
|
|
97
|
+
private readonly websocketTickets = new Map<string, StoredTicket>();
|
|
98
|
+
private readonly lastTouchedAt = new Map<string, number>();
|
|
99
|
+
|
|
100
|
+
constructor(
|
|
101
|
+
private readonly store: AndroidRemoteStore,
|
|
102
|
+
private readonly now: () => number = Date.now,
|
|
103
|
+
) {}
|
|
104
|
+
|
|
105
|
+
createInvitation(input: {
|
|
106
|
+
desktopName: string;
|
|
107
|
+
localUrls: string[];
|
|
108
|
+
cloudflareUrl?: string | null;
|
|
109
|
+
}): AndroidRemotePairingInvitation {
|
|
110
|
+
this.pruneExpired();
|
|
111
|
+
const id = randomUUID();
|
|
112
|
+
const rawToken = token("ocx_pair_");
|
|
113
|
+
const expiresAtMs = this.now() + PAIRING_TTL_MS;
|
|
114
|
+
const expiresAt = new Date(expiresAtMs).toISOString();
|
|
115
|
+
this.invitations.set(id, {
|
|
116
|
+
id,
|
|
117
|
+
tokenDigest: digestAndroidRemoteCredential(rawToken),
|
|
118
|
+
expiresAtMs,
|
|
119
|
+
});
|
|
120
|
+
const cloudflareUrl = input.cloudflareUrl?.trim() || null;
|
|
121
|
+
const directUrl = cloudflareUrl ?? input.localUrls[0] ?? "http://127.0.0.1:10105";
|
|
122
|
+
const payload: AndroidRemotePairingPayload = {
|
|
123
|
+
type: "remodex-mobile-pairing",
|
|
124
|
+
version: 1,
|
|
125
|
+
desktopName: input.desktopName.trim() || "Remodex Desktop",
|
|
126
|
+
localUrls: [...new Set(input.localUrls)],
|
|
127
|
+
cloudflareUrl,
|
|
128
|
+
directUrl,
|
|
129
|
+
pairingToken: rawToken,
|
|
130
|
+
pairingLinkId: id,
|
|
131
|
+
expiresAt,
|
|
132
|
+
};
|
|
133
|
+
return { id, expiresAt, payload, qrPayload: JSON.stringify(payload) };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
exchangePairingToken(input: {
|
|
137
|
+
pairingToken: string;
|
|
138
|
+
metadata: AndroidRemoteClientMetadata;
|
|
139
|
+
address?: string;
|
|
140
|
+
}): { accessToken: string; expiresAt: string; client: AndroidRemoteStoredClient } | null {
|
|
141
|
+
this.pruneExpired();
|
|
142
|
+
const presentedDigest = digestAndroidRemoteCredential(input.pairingToken);
|
|
143
|
+
const invitation = [...this.invitations.values()].find(row =>
|
|
144
|
+
row.expiresAtMs > this.now() && equalDigest(row.tokenDigest, presentedDigest));
|
|
145
|
+
if (!invitation) return null;
|
|
146
|
+
this.invitations.delete(invitation.id);
|
|
147
|
+
|
|
148
|
+
const accessToken = token("ocx_android_");
|
|
149
|
+
const createdAtMs = this.now();
|
|
150
|
+
const expiresAt = new Date(createdAtMs + ACCESS_TOKEN_TTL_MS).toISOString();
|
|
151
|
+
const client: AndroidRemoteStoredClient = {
|
|
152
|
+
id: randomUUID(),
|
|
153
|
+
label: cleanLabel(input.metadata.label),
|
|
154
|
+
deviceType: "mobile",
|
|
155
|
+
os: cleanOs(input.metadata.os),
|
|
156
|
+
scopes: [...ANDROID_REMOTE_SCOPES],
|
|
157
|
+
createdAt: new Date(createdAtMs).toISOString(),
|
|
158
|
+
lastSeenAt: new Date(createdAtMs).toISOString(),
|
|
159
|
+
credentialExpiresAt: expiresAt,
|
|
160
|
+
credentialDigest: digestAndroidRemoteCredential(accessToken),
|
|
161
|
+
...(safeRemoteAddress(input.address) ? { address: safeRemoteAddress(input.address) } : {}),
|
|
162
|
+
};
|
|
163
|
+
this.store.upsertClient(client);
|
|
164
|
+
return { accessToken, expiresAt, client };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
authenticateAccessToken(rawToken: string, address?: string): AuthenticatedAndroidClient | null {
|
|
168
|
+
if (!rawToken.startsWith("ocx_android_")) return null;
|
|
169
|
+
const digest = digestAndroidRemoteCredential(rawToken);
|
|
170
|
+
const state = this.store.read();
|
|
171
|
+
const now = this.now();
|
|
172
|
+
const client = state.clients.find(row => equalDigest(row.credentialDigest, digest));
|
|
173
|
+
if (!client) return null;
|
|
174
|
+
const expiresAtMs = client.credentialExpiresAt ? Date.parse(client.credentialExpiresAt) : NaN;
|
|
175
|
+
if (Number.isFinite(expiresAtMs) && expiresAtMs <= now) return null;
|
|
176
|
+
const previousTouch = this.lastTouchedAt.get(client.id) ?? 0;
|
|
177
|
+
if (now - previousTouch >= CLIENT_TOUCH_INTERVAL_MS) {
|
|
178
|
+
this.lastTouchedAt.set(client.id, now);
|
|
179
|
+
const next = this.store.touchClient(client.id, {
|
|
180
|
+
lastSeenAt: new Date(now).toISOString(),
|
|
181
|
+
...(safeRemoteAddress(address) ? { address: safeRemoteAddress(address) } : {}),
|
|
182
|
+
});
|
|
183
|
+
return { client: next.clients.find(row => row.id === client.id) ?? client, state: next };
|
|
184
|
+
}
|
|
185
|
+
return { client, state };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
issueWebSocketTicket(clientId: string): { ticket: string; expiresAt: string } {
|
|
189
|
+
this.pruneExpired();
|
|
190
|
+
const ticket = token("ocx_ws_", 24);
|
|
191
|
+
const expiresAtMs = this.now() + WEBSOCKET_TICKET_TTL_MS;
|
|
192
|
+
this.websocketTickets.set(digestAndroidRemoteCredential(ticket), { clientId, expiresAtMs });
|
|
193
|
+
return { ticket, expiresAt: new Date(expiresAtMs).toISOString() };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
updateClientMetadata(
|
|
197
|
+
clientId: string,
|
|
198
|
+
metadata: AndroidRemoteClientMetadata,
|
|
199
|
+
): AndroidRemoteStoredClient | null {
|
|
200
|
+
const state = this.store.updateClientMetadata(clientId, {
|
|
201
|
+
...(metadata.label ? { label: cleanLabel(metadata.label) } : {}),
|
|
202
|
+
...(metadata.os ? { os: cleanOs(metadata.os) } : {}),
|
|
203
|
+
});
|
|
204
|
+
return state.clients.find(client => client.id === clientId) ?? null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
consumeWebSocketTicket(ticket: string): AndroidRemoteStoredClient | null {
|
|
208
|
+
this.pruneExpired();
|
|
209
|
+
const digest = digestAndroidRemoteCredential(ticket);
|
|
210
|
+
const stored = this.websocketTickets.get(digest);
|
|
211
|
+
if (!stored || stored.expiresAtMs <= this.now()) return null;
|
|
212
|
+
this.websocketTickets.delete(digest);
|
|
213
|
+
return this.store.read().clients.find(client => client.id === stored.clientId) ?? null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
revokeClient(clientId: string): void {
|
|
217
|
+
for (const [digest, ticket] of this.websocketTickets) {
|
|
218
|
+
if (ticket.clientId === clientId) this.websocketTickets.delete(digest);
|
|
219
|
+
}
|
|
220
|
+
this.lastTouchedAt.delete(clientId);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
hasInvitation(id: string): boolean {
|
|
224
|
+
this.pruneExpired();
|
|
225
|
+
return this.invitations.has(id);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private pruneExpired(): void {
|
|
229
|
+
const now = this.now();
|
|
230
|
+
for (const [id, invitation] of this.invitations) {
|
|
231
|
+
if (invitation.expiresAtMs <= now) this.invitations.delete(id);
|
|
232
|
+
}
|
|
233
|
+
for (const [digest, ticket] of this.websocketTickets) {
|
|
234
|
+
if (ticket.expiresAtMs <= now) this.websocketTickets.delete(digest);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { normalizeNamedTunnelHostname, validateCloudflareTunnelToken } from "./cloudflare-tunnel";
|
|
3
|
+
|
|
4
|
+
const CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4/";
|
|
5
|
+
const CLOUDFLARE_API_TIMEOUT_MS = 10_000;
|
|
6
|
+
const MAX_RESPONSE_BYTES = 512 * 1024;
|
|
7
|
+
const PAGE_SIZE = 50;
|
|
8
|
+
const MAX_PAGES = 4;
|
|
9
|
+
const GATEWAY_ORIGIN = "http://127.0.0.1:10105";
|
|
10
|
+
|
|
11
|
+
export type CloudflareZoneStatus = "active" | "pending" | "initializing" | "moved" | "deactivated" | "unknown";
|
|
12
|
+
|
|
13
|
+
export type CloudflareAccountDTO = {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type CloudflareZoneDTO = {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
accountId: string;
|
|
22
|
+
accountName: string;
|
|
23
|
+
status: CloudflareZoneStatus;
|
|
24
|
+
nameServers: string[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type CloudflareDiscoveryDTO = {
|
|
28
|
+
version: 1;
|
|
29
|
+
accounts: CloudflareAccountDTO[];
|
|
30
|
+
zones: CloudflareZoneDTO[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type CloudflareProvisioningInput = {
|
|
34
|
+
apiToken: string;
|
|
35
|
+
accountId: string;
|
|
36
|
+
zoneId: string;
|
|
37
|
+
hostname: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type CloudflareProvisioningResult = {
|
|
41
|
+
hostname: string;
|
|
42
|
+
connectorToken: string;
|
|
43
|
+
tunnelCreated: boolean;
|
|
44
|
+
dnsCreated: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type CloudflareProvisioningErrorCode =
|
|
48
|
+
| "invalid_input"
|
|
49
|
+
| "authorization_failed"
|
|
50
|
+
| "permission_missing"
|
|
51
|
+
| "zone_not_found"
|
|
52
|
+
| "zone_not_active"
|
|
53
|
+
| "hostname_outside_zone"
|
|
54
|
+
| "hostname_conflict"
|
|
55
|
+
| "tunnel_conflict"
|
|
56
|
+
| "too_many_results"
|
|
57
|
+
| "rate_limited"
|
|
58
|
+
| "cloudflare_unavailable"
|
|
59
|
+
| "invalid_response";
|
|
60
|
+
|
|
61
|
+
const SAFE_ERROR_MESSAGES: Record<CloudflareProvisioningErrorCode, string> = {
|
|
62
|
+
invalid_input: "Enter a valid scoped Cloudflare API token and connection details.",
|
|
63
|
+
authorization_failed: "Cloudflare rejected this API token.",
|
|
64
|
+
permission_missing: "The API token needs Cloudflare Tunnel Edit and DNS Edit permissions for the selected domain.",
|
|
65
|
+
zone_not_found: "The selected Cloudflare domain is no longer available to this token.",
|
|
66
|
+
zone_not_active: "This domain is not active on Cloudflare yet. Finish the nameserver change, then try again.",
|
|
67
|
+
hostname_outside_zone: "Choose one subdomain inside the selected Cloudflare domain.",
|
|
68
|
+
hostname_conflict: "That hostname already has a DNS record. Remodex did not replace it.",
|
|
69
|
+
tunnel_conflict: "An existing tunnel prevents safe automatic setup. Remove the conflicting tunnel or use Advanced manual setup.",
|
|
70
|
+
too_many_results: "This token can access too many domains. Create a token scoped to the domain you want to use.",
|
|
71
|
+
rate_limited: "Cloudflare rate-limited this request. Wait briefly, then try again.",
|
|
72
|
+
cloudflare_unavailable: "Cloudflare could not complete the request. Try again.",
|
|
73
|
+
invalid_response: "Cloudflare returned an unexpected response. Try again.",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export class CloudflareProvisioningError extends Error {
|
|
77
|
+
constructor(readonly code: CloudflareProvisioningErrorCode) {
|
|
78
|
+
super(SAFE_ERROR_MESSAGES[code]);
|
|
79
|
+
this.name = "CloudflareProvisioningError";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface AndroidRemoteCloudflareProvisioner {
|
|
84
|
+
discover(apiToken: string): Promise<CloudflareDiscoveryDTO>;
|
|
85
|
+
provision(input: CloudflareProvisioningInput): Promise<CloudflareProvisioningResult>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type AndroidRemoteCloudflareProvisionerDeps = {
|
|
89
|
+
fetch: typeof fetch;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
type JsonRecord = Record<string, unknown>;
|
|
93
|
+
|
|
94
|
+
function record(value: unknown): JsonRecord | null {
|
|
95
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
96
|
+
? value as JsonRecord
|
|
97
|
+
: null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function boundedString(value: unknown, maximum: number): string | null {
|
|
101
|
+
return typeof value === "string" && value.length > 0 && value.length <= maximum ? value : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function cloudflareId(value: unknown): string | null {
|
|
105
|
+
return typeof value === "string" && /^[a-f0-9]{32}$/iu.test(value) ? value.toLowerCase() : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function cloudflareTunnelId(value: unknown): string | null {
|
|
109
|
+
return typeof value === "string"
|
|
110
|
+
&& /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/iu.test(value)
|
|
111
|
+
? value.toLowerCase()
|
|
112
|
+
: null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function validateCloudflareApiToken(value: string): string {
|
|
116
|
+
const token = value.trim();
|
|
117
|
+
if (token.length < 20 || token.length > 1_024 || /\s/u.test(token)) {
|
|
118
|
+
throw new CloudflareProvisioningError("invalid_input");
|
|
119
|
+
}
|
|
120
|
+
return token;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function zoneStatus(value: unknown): CloudflareZoneStatus {
|
|
124
|
+
if (value === "active" || value === "pending" || value === "initializing"
|
|
125
|
+
|| value === "moved" || value === "deactivated") return value;
|
|
126
|
+
return "unknown";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function readBoundedJson(response: Response): Promise<unknown> {
|
|
130
|
+
if (!response.body) throw new CloudflareProvisioningError("invalid_response");
|
|
131
|
+
const reader = response.body.getReader();
|
|
132
|
+
const chunks: Uint8Array[] = [];
|
|
133
|
+
let length = 0;
|
|
134
|
+
try {
|
|
135
|
+
for (;;) {
|
|
136
|
+
const { done, value } = await reader.read();
|
|
137
|
+
if (done) break;
|
|
138
|
+
length += value.byteLength;
|
|
139
|
+
if (length > MAX_RESPONSE_BYTES) {
|
|
140
|
+
await reader.cancel();
|
|
141
|
+
throw new CloudflareProvisioningError("invalid_response");
|
|
142
|
+
}
|
|
143
|
+
chunks.push(value);
|
|
144
|
+
}
|
|
145
|
+
} finally {
|
|
146
|
+
reader.releaseLock();
|
|
147
|
+
}
|
|
148
|
+
const joined = new Uint8Array(length);
|
|
149
|
+
let offset = 0;
|
|
150
|
+
for (const chunk of chunks) {
|
|
151
|
+
joined.set(chunk, offset);
|
|
152
|
+
offset += chunk.byteLength;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(new TextDecoder().decode(joined)) as unknown;
|
|
156
|
+
} catch {
|
|
157
|
+
throw new CloudflareProvisioningError("invalid_response");
|
|
158
|
+
} finally {
|
|
159
|
+
joined.fill(0);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function envelopeResult(value: unknown): unknown {
|
|
164
|
+
const envelope = record(value);
|
|
165
|
+
if (!envelope || envelope.success !== true || !("result" in envelope)) {
|
|
166
|
+
throw new CloudflareProvisioningError("invalid_response");
|
|
167
|
+
}
|
|
168
|
+
return envelope.result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function envelopeTotalPages(value: unknown): number {
|
|
172
|
+
const info = record(record(value)?.result_info);
|
|
173
|
+
const pages = info?.total_pages;
|
|
174
|
+
return typeof pages === "number" && Number.isInteger(pages) && pages > 0 ? pages : 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function mapHttpFailure(status: number): CloudflareProvisioningError {
|
|
178
|
+
if (status === 401) return new CloudflareProvisioningError("authorization_failed");
|
|
179
|
+
if (status === 403) return new CloudflareProvisioningError("permission_missing");
|
|
180
|
+
if (status === 404) return new CloudflareProvisioningError("zone_not_found");
|
|
181
|
+
if (status === 409) return new CloudflareProvisioningError("hostname_conflict");
|
|
182
|
+
if (status === 429) return new CloudflareProvisioningError("rate_limited");
|
|
183
|
+
return new CloudflareProvisioningError("cloudflare_unavailable");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function expectedIngress(hostname: string): JsonRecord[] {
|
|
187
|
+
return [
|
|
188
|
+
{ hostname, service: GATEWAY_ORIGIN, originRequest: {} },
|
|
189
|
+
{ service: "http_status:404" },
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isExpectedIngress(value: unknown, hostname: string): boolean {
|
|
194
|
+
if (!Array.isArray(value) || value.length !== 2) return false;
|
|
195
|
+
const route = record(value[0]);
|
|
196
|
+
const fallback = record(value[1]);
|
|
197
|
+
if (!route || !fallback) return false;
|
|
198
|
+
const originRequest = route.originRequest;
|
|
199
|
+
return route.hostname === hostname
|
|
200
|
+
&& route.service === GATEWAY_ORIGIN
|
|
201
|
+
&& (originRequest === undefined || (record(originRequest) !== null && Object.keys(record(originRequest)!).length === 0))
|
|
202
|
+
&& Object.keys(route).every(key => key === "hostname" || key === "service" || key === "originRequest")
|
|
203
|
+
&& fallback.service === "http_status:404"
|
|
204
|
+
&& Object.keys(fallback).length === 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function automaticTunnelName(hostname: string): string {
|
|
208
|
+
const digest = createHash("sha256").update(hostname).digest("hex").slice(0, 20);
|
|
209
|
+
return `opencodex-android-v1-${digest}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function normalizeProvisionedHostname(hostnameInput: string, zoneNameInput: string): string {
|
|
213
|
+
const hostname = normalizeNamedTunnelHostname(hostnameInput);
|
|
214
|
+
const zoneName = normalizeNamedTunnelHostname(zoneNameInput);
|
|
215
|
+
if (!hostname.endsWith(`.${zoneName}`)) {
|
|
216
|
+
throw new CloudflareProvisioningError("hostname_outside_zone");
|
|
217
|
+
}
|
|
218
|
+
const subdomain = hostname.slice(0, -(zoneName.length + 1));
|
|
219
|
+
if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(subdomain)) {
|
|
220
|
+
throw new CloudflareProvisioningError("hostname_outside_zone");
|
|
221
|
+
}
|
|
222
|
+
return hostname;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export class ManagedAndroidRemoteCloudflareProvisioner implements AndroidRemoteCloudflareProvisioner {
|
|
226
|
+
constructor(private readonly deps: AndroidRemoteCloudflareProvisionerDeps = { fetch }) {}
|
|
227
|
+
|
|
228
|
+
private async request(apiToken: string, path: string, init: RequestInit = {}): Promise<unknown> {
|
|
229
|
+
if (!path.startsWith("/") || path.startsWith("//")) {
|
|
230
|
+
throw new CloudflareProvisioningError("invalid_input");
|
|
231
|
+
}
|
|
232
|
+
const url = new URL(path.slice(1), CLOUDFLARE_API_BASE);
|
|
233
|
+
let response: Response;
|
|
234
|
+
try {
|
|
235
|
+
response = await this.deps.fetch(url, {
|
|
236
|
+
...init,
|
|
237
|
+
redirect: "error",
|
|
238
|
+
signal: AbortSignal.timeout(CLOUDFLARE_API_TIMEOUT_MS),
|
|
239
|
+
headers: {
|
|
240
|
+
Authorization: `Bearer ${apiToken}`,
|
|
241
|
+
Accept: "application/json",
|
|
242
|
+
...(init.body ? { "Content-Type": "application/json" } : {}),
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
} catch {
|
|
246
|
+
throw new CloudflareProvisioningError("cloudflare_unavailable");
|
|
247
|
+
}
|
|
248
|
+
const body = await readBoundedJson(response);
|
|
249
|
+
if (!response.ok) throw mapHttpFailure(response.status);
|
|
250
|
+
return body;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private async verifyToken(apiToken: string): Promise<void> {
|
|
254
|
+
const body = await this.request(apiToken, "/user/tokens/verify");
|
|
255
|
+
const result = record(envelopeResult(body));
|
|
256
|
+
if (!result || result.status !== "active") {
|
|
257
|
+
throw new CloudflareProvisioningError("authorization_failed");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async discover(apiTokenInput: string): Promise<CloudflareDiscoveryDTO> {
|
|
262
|
+
const apiToken = validateCloudflareApiToken(apiTokenInput);
|
|
263
|
+
await this.verifyToken(apiToken);
|
|
264
|
+
const zones: CloudflareZoneDTO[] = [];
|
|
265
|
+
for (let page = 1; page <= MAX_PAGES; page += 1) {
|
|
266
|
+
const body = await this.request(apiToken, `/zones?per_page=${PAGE_SIZE}&page=${page}`);
|
|
267
|
+
const result = envelopeResult(body);
|
|
268
|
+
if (!Array.isArray(result)) throw new CloudflareProvisioningError("invalid_response");
|
|
269
|
+
for (const value of result) {
|
|
270
|
+
const row = record(value);
|
|
271
|
+
const account = record(row?.account);
|
|
272
|
+
const id = cloudflareId(row?.id);
|
|
273
|
+
const name = boundedString(row?.name, 253);
|
|
274
|
+
const accountId = cloudflareId(account?.id);
|
|
275
|
+
const accountName = boundedString(account?.name, 128);
|
|
276
|
+
if (!id || !name || !accountId || !accountName) {
|
|
277
|
+
throw new CloudflareProvisioningError("invalid_response");
|
|
278
|
+
}
|
|
279
|
+
const rawNameServers = Array.isArray(row?.name_servers) ? row.name_servers : [];
|
|
280
|
+
const nameServers = rawNameServers
|
|
281
|
+
.map(value => boundedString(value, 253))
|
|
282
|
+
.filter((value): value is string => Boolean(value))
|
|
283
|
+
.slice(0, 8);
|
|
284
|
+
zones.push({
|
|
285
|
+
id,
|
|
286
|
+
name: name.toLowerCase(),
|
|
287
|
+
accountId,
|
|
288
|
+
accountName,
|
|
289
|
+
status: zoneStatus(row?.status),
|
|
290
|
+
nameServers,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const totalPages = envelopeTotalPages(body);
|
|
294
|
+
if (totalPages > MAX_PAGES) throw new CloudflareProvisioningError("too_many_results");
|
|
295
|
+
if (page >= totalPages) break;
|
|
296
|
+
}
|
|
297
|
+
const accountsById = new Map<string, CloudflareAccountDTO>();
|
|
298
|
+
for (const zone of zones) accountsById.set(zone.accountId, { id: zone.accountId, name: zone.accountName });
|
|
299
|
+
return {
|
|
300
|
+
version: 1,
|
|
301
|
+
accounts: [...accountsById.values()].sort((left, right) => left.name.localeCompare(right.name)),
|
|
302
|
+
zones: zones.sort((left, right) => left.name.localeCompare(right.name)),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async provision(input: CloudflareProvisioningInput): Promise<CloudflareProvisioningResult> {
|
|
307
|
+
const apiToken = validateCloudflareApiToken(input.apiToken);
|
|
308
|
+
const accountId = cloudflareId(input.accountId);
|
|
309
|
+
const zoneId = cloudflareId(input.zoneId);
|
|
310
|
+
if (!accountId || !zoneId) throw new CloudflareProvisioningError("invalid_input");
|
|
311
|
+
await this.verifyToken(apiToken);
|
|
312
|
+
|
|
313
|
+
const zoneBody = await this.request(apiToken, `/zones/${zoneId}`);
|
|
314
|
+
const zone = record(envelopeResult(zoneBody));
|
|
315
|
+
const zoneAccount = record(zone?.account);
|
|
316
|
+
const zoneName = boundedString(zone?.name, 253);
|
|
317
|
+
if (!zoneName || cloudflareId(zone?.id) !== zoneId || cloudflareId(zoneAccount?.id) !== accountId) {
|
|
318
|
+
throw new CloudflareProvisioningError("zone_not_found");
|
|
319
|
+
}
|
|
320
|
+
if (zone?.status !== "active") throw new CloudflareProvisioningError("zone_not_active");
|
|
321
|
+
const hostname = normalizeProvisionedHostname(input.hostname, zoneName);
|
|
322
|
+
const tunnelName = automaticTunnelName(hostname);
|
|
323
|
+
const tunnelTarget = (tunnelId: string) => `${tunnelId}.cfargotunnel.com`;
|
|
324
|
+
let tunnelId: string | null = null;
|
|
325
|
+
let createdTunnel = false;
|
|
326
|
+
let createdDnsId: string | null = null;
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const tunnelsBody = await this.request(
|
|
330
|
+
apiToken,
|
|
331
|
+
`/accounts/${accountId}/cfd_tunnel?is_deleted=false&name=${encodeURIComponent(tunnelName)}&per_page=100`,
|
|
332
|
+
);
|
|
333
|
+
const tunnelRows = envelopeResult(tunnelsBody);
|
|
334
|
+
if (!Array.isArray(tunnelRows)) throw new CloudflareProvisioningError("invalid_response");
|
|
335
|
+
const exactTunnels = tunnelRows.filter(value => record(value)?.name === tunnelName);
|
|
336
|
+
if (exactTunnels.length > 1) throw new CloudflareProvisioningError("tunnel_conflict");
|
|
337
|
+
if (exactTunnels.length === 1) {
|
|
338
|
+
const tunnel = record(exactTunnels[0]);
|
|
339
|
+
tunnelId = cloudflareTunnelId(tunnel?.id);
|
|
340
|
+
if (!tunnelId || tunnel?.tun_type !== "cfd_tunnel" || tunnel?.remote_config !== true) {
|
|
341
|
+
throw new CloudflareProvisioningError("tunnel_conflict");
|
|
342
|
+
}
|
|
343
|
+
const configurationBody = await this.request(
|
|
344
|
+
apiToken,
|
|
345
|
+
`/accounts/${accountId}/cfd_tunnel/${tunnelId}/configurations`,
|
|
346
|
+
);
|
|
347
|
+
const configuration = record(envelopeResult(configurationBody));
|
|
348
|
+
if (!isExpectedIngress(record(configuration?.config)?.ingress, hostname)) {
|
|
349
|
+
throw new CloudflareProvisioningError("tunnel_conflict");
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
const createdBody = await this.request(apiToken, `/accounts/${accountId}/cfd_tunnel`, {
|
|
353
|
+
method: "POST",
|
|
354
|
+
body: JSON.stringify({ name: tunnelName, config_src: "cloudflare" }),
|
|
355
|
+
});
|
|
356
|
+
const created = record(envelopeResult(createdBody));
|
|
357
|
+
tunnelId = cloudflareTunnelId(created?.id);
|
|
358
|
+
if (!tunnelId) throw new CloudflareProvisioningError("invalid_response");
|
|
359
|
+
createdTunnel = true;
|
|
360
|
+
await this.request(apiToken, `/accounts/${accountId}/cfd_tunnel/${tunnelId}/configurations`, {
|
|
361
|
+
method: "PUT",
|
|
362
|
+
body: JSON.stringify({ config: { ingress: expectedIngress(hostname) } }),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const dnsBody = await this.request(
|
|
367
|
+
apiToken,
|
|
368
|
+
`/zones/${zoneId}/dns_records?name=${encodeURIComponent(hostname)}&per_page=100`,
|
|
369
|
+
);
|
|
370
|
+
const dnsRows = envelopeResult(dnsBody);
|
|
371
|
+
if (!Array.isArray(dnsRows)) throw new CloudflareProvisioningError("invalid_response");
|
|
372
|
+
if (dnsRows.length > 1) throw new CloudflareProvisioningError("hostname_conflict");
|
|
373
|
+
const target = tunnelTarget(tunnelId);
|
|
374
|
+
if (dnsRows.length === 1) {
|
|
375
|
+
const dns = record(dnsRows[0]);
|
|
376
|
+
if (dns?.type !== "CNAME" || dns.name !== hostname || dns.content !== target || dns.proxied !== true) {
|
|
377
|
+
throw new CloudflareProvisioningError("hostname_conflict");
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
const createdDnsBody = await this.request(apiToken, `/zones/${zoneId}/dns_records`, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
body: JSON.stringify({ type: "CNAME", proxied: true, name: hostname, content: target }),
|
|
383
|
+
});
|
|
384
|
+
const createdDns = record(envelopeResult(createdDnsBody));
|
|
385
|
+
createdDnsId = cloudflareId(createdDns?.id);
|
|
386
|
+
if (!createdDnsId) throw new CloudflareProvisioningError("invalid_response");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const tokenBody = await this.request(apiToken, `/accounts/${accountId}/cfd_tunnel/${tunnelId}/token`);
|
|
390
|
+
const tokenResult = envelopeResult(tokenBody);
|
|
391
|
+
const connectorToken = validateCloudflareTunnelToken(typeof tokenResult === "string" ? tokenResult : "");
|
|
392
|
+
const verifyBody = await this.request(apiToken, `/accounts/${accountId}/cfd_tunnel/${tunnelId}`);
|
|
393
|
+
const verifiedTunnel = record(envelopeResult(verifyBody));
|
|
394
|
+
if (cloudflareTunnelId(verifiedTunnel?.id) !== tunnelId || verifiedTunnel?.tun_type !== "cfd_tunnel") {
|
|
395
|
+
throw new CloudflareProvisioningError("invalid_response");
|
|
396
|
+
}
|
|
397
|
+
return { hostname, connectorToken, tunnelCreated: createdTunnel, dnsCreated: createdDnsId !== null };
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (createdDnsId) {
|
|
400
|
+
await this.request(apiToken, `/zones/${zoneId}/dns_records/${createdDnsId}`, { method: "DELETE" }).catch(() => undefined);
|
|
401
|
+
}
|
|
402
|
+
if (createdTunnel && tunnelId) {
|
|
403
|
+
await this.request(apiToken, `/accounts/${accountId}/cfd_tunnel/${tunnelId}`, { method: "DELETE" }).catch(() => undefined);
|
|
404
|
+
}
|
|
405
|
+
if (error instanceof CloudflareProvisioningError || error instanceof TypeError) throw error;
|
|
406
|
+
throw new CloudflareProvisioningError("cloudflare_unavailable");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|