@yansigit/opencodex 2.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/LICENSE +21 -0
- package/README.md +303 -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 +587 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
- package/gui/dist/assets/index-CGoDO3uO.css +1 -0
- package/gui/dist/favicon.png +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 +108 -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-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +1327 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +121 -0
- package/src/adapters/client-fingerprint.ts +65 -0
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +601 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +288 -0
- package/src/adapters/cursor/discovery.ts +333 -0
- package/src/adapters/cursor/effort-map.ts +151 -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/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +269 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1653 -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 +76 -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 +547 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +663 -0
- package/src/adapters/cursor/protobuf-events.ts +1381 -0
- package/src/adapters/cursor/protobuf-request.ts +1032 -0
- package/src/adapters/cursor/request-builder.ts +461 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +735 -0
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +79 -0
- package/src/adapters/cursor/types.ts +79 -0
- package/src/adapters/cursor.ts +322 -0
- package/src/adapters/google-antigravity-hosts.ts +48 -0
- package/src/adapters/google-antigravity-replay.ts +827 -0
- package/src/adapters/google-antigravity-tools.ts +105 -0
- package/src/adapters/google-antigravity-wire.ts +141 -0
- package/src/adapters/google-errors.ts +92 -0
- package/src/adapters/google-http.ts +460 -0
- package/src/adapters/google-tool-schema.ts +238 -0
- package/src/adapters/google-truncation.ts +24 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +1377 -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 +112 -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 +1936 -0
- package/src/adapters/mimo-free.ts +280 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1980 -0
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1911 -0
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +154 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +1986 -0
- package/src/chat/inbound.ts +319 -0
- package/src/chat/outbound.ts +821 -0
- package/src/claude/agents-inject.ts +266 -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 +205 -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 +615 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +107 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +578 -0
- package/src/claude/model-info.ts +174 -0
- package/src/claude/outbound.ts +926 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +302 -0
- package/src/cli/account-auth.ts +250 -0
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +737 -0
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +299 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +290 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +213 -0
- package/src/cli/claude.ts +355 -0
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +47 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +127 -0
- package/src/cli/config-command.ts +209 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/dispatch.ts +585 -0
- package/src/cli/doctor.ts +1202 -0
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +213 -0
- package/src/cli/help.ts +101 -0
- package/src/cli/index.ts +973 -0
- package/src/cli/init.ts +211 -0
- package/src/cli/integrations.ts +260 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +245 -0
- package/src/cli/models.ts +422 -0
- package/src/cli/observe.ts +206 -0
- package/src/cli/opencode.ts +588 -0
- package/src/cli/provider-replit.ts +232 -0
- package/src/cli/provider-runtime.ts +179 -0
- package/src/cli/provider.ts +492 -0
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +422 -0
- package/src/cli/replit-gateway-key-input.ts +138 -0
- package/src/cli/root.ts +86 -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 +328 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +199 -0
- package/src/cli/v2.ts +268 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +1704 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +47 -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 +544 -0
- package/src/codex/account-usability.ts +43 -0
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/agent-roles-sync.ts +225 -0
- package/src/codex/agent-roles.ts +238 -0
- package/src/codex/app-server-processes.ts +1143 -0
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +2147 -0
- package/src/codex/auth-collision.ts +109 -0
- package/src/codex/auth-context.ts +665 -0
- package/src/codex/autostart-health.ts +156 -0
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +436 -0
- package/src/codex/catalog/bundled.ts +549 -0
- package/src/codex/catalog/effort.ts +446 -0
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +664 -0
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +650 -0
- package/src/codex/catalog/provider-fetch.ts +2064 -0
- package/src/codex/catalog/sync.ts +1883 -0
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +14 -0
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +651 -0
- package/src/codex/coordinator-doctor.ts +332 -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/exec-invocation.ts +22 -0
- package/src/codex/features.ts +1566 -0
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +407 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +108 -0
- package/src/codex/history-provider.ts +979 -0
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1733 -0
- package/src/codex/injected-marker.ts +106 -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 +80 -0
- package/src/codex/journal.ts +225 -0
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +56 -0
- package/src/codex/main-account.ts +68 -0
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +273 -0
- package/src/codex/model-entitlements.ts +353 -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 +178 -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 +492 -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 +682 -0
- package/src/codex/paths.ts +144 -0
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +295 -0
- package/src/codex/project-config-warnings.ts +425 -0
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +573 -0
- package/src/codex/refresh.ts +62 -0
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +1888 -0
- package/src/codex/runtime.ts +659 -0
- package/src/codex/shim.ts +2170 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +784 -0
- package/src/codex/sync.ts +319 -0
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +298 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +160 -0
- package/src/combos/index.ts +45 -0
- package/src/combos/request.ts +94 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +398 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +4041 -0
- package/src/fork/register.ts +3 -0
- package/src/generated/compatibility-version.json +3116 -0
- package/src/generated/model-metadata.ts +106 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +530 -0
- package/src/grok/inspect.ts +45 -0
- package/src/grok/status.ts +121 -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 +955 -0
- package/src/images/plan.ts +143 -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 +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +159 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +361 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +691 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -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 +195 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +346 -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 +130 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +364 -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 +380 -0
- package/src/lib/errors.ts +406 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/pinned-http.ts +270 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +168 -0
- package/src/lib/provider-outbound.ts +210 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +521 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +61 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +364 -0
- package/src/lib/state-store-registrations.ts +119 -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 +90 -0
- package/src/lib/token-estimate.ts +86 -0
- package/src/lib/tool-argument-integers.ts +202 -0
- package/src/lib/translator-budget.ts +400 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +392 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-atomic-replace.ts +156 -0
- package/src/lib/windows-elevation.ts +773 -0
- package/src/lib/windows-secret-acl.ts +854 -0
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +403 -0
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +188 -0
- package/src/oauth/antigravity-routing.ts +151 -0
- package/src/oauth/callback-server.ts +300 -0
- package/src/oauth/chatgpt.ts +161 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +252 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +262 -0
- package/src/oauth/health.ts +407 -0
- package/src/oauth/index.ts +1504 -0
- package/src/oauth/key-providers.ts +124 -0
- package/src/oauth/kimi.ts +227 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +621 -0
- package/src/oauth/local-token-detect.ts +130 -0
- package/src/oauth/log.ts +50 -0
- package/src/oauth/login-cli.ts +223 -0
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +728 -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 +695 -0
- package/src/providers/antigravity-quota.ts +216 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +74 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +82 -0
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +586 -0
- package/src/providers/fastwire.ts +501 -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 +449 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-sidecar.ts +243 -0
- package/src/providers/openai-tier-startup.ts +56 -0
- package/src/providers/openai-tiers.ts +423 -0
- package/src/providers/openai-virtual-models.ts +83 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +185 -0
- package/src/providers/quota.ts +2345 -0
- package/src/providers/registry.ts +2918 -0
- package/src/providers/replit/constants.ts +27 -0
- package/src/providers/replit/derive.ts +85 -0
- package/src/providers/replit/headers.ts +28 -0
- package/src/providers/replit/origin.ts +55 -0
- package/src/providers/replit/pair-install-response.ts +72 -0
- package/src/providers/replit/probe.ts +199 -0
- package/src/providers/replit/setup.ts +350 -0
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +103 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +148 -0
- package/src/reasoning-effort.ts +183 -0
- package/src/responses/compaction.ts +142 -0
- package/src/responses/custom-tool-compat.ts +266 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +355 -0
- package/src/responses/parser.ts +838 -0
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +426 -0
- package/src/responses/schema.ts +165 -0
- package/src/responses/spill-store.ts +459 -0
- package/src/responses/state.ts +1433 -0
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +761 -0
- package/src/routing/analytics.ts +378 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -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 +547 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +53 -0
- package/src/server/auth-cors.ts +751 -0
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +442 -0
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +1030 -0
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +190 -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 +568 -0
- package/src/server/index.ts +1813 -0
- package/src/server/lifecycle.ts +498 -0
- package/src/server/live.ts +717 -0
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +1433 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +193 -0
- package/src/server/management/body.ts +41 -0
- package/src/server/management/combo-routes.ts +263 -0
- package/src/server/management/config-routes.ts +835 -0
- package/src/server/management/context.ts +113 -0
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +586 -0
- package/src/server/management/model-routes.ts +560 -0
- package/src/server/management/model-rows.ts +163 -0
- package/src/server/management/native-integration-routes.ts +769 -0
- package/src/server/management/oauth-account-routes.ts +637 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +1033 -0
- package/src/server/management/replit-provider-routes.ts +86 -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 +385 -0
- package/src/server/management/shared.ts +286 -0
- package/src/server/management/sidebar-routes.ts +106 -0
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +435 -0
- package/src/server/management/system-routes.ts +194 -0
- package/src/server/management/usage-summary-cache.ts +94 -0
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +314 -0
- package/src/server/management-auth.ts +482 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +156 -0
- package/src/server/proxy-liveness.ts +328 -0
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +353 -0
- package/src/server/relay.ts +1209 -0
- package/src/server/request-decompress.ts +239 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1259 -0
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/collaboration.ts +551 -0
- package/src/server/responses/compact.ts +771 -0
- package/src/server/responses/core.ts +5389 -0
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +331 -0
- package/src/server/responses/fetch-helpers.ts +232 -0
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +251 -0
- package/src/server/responses/terminal-guard.ts +251 -0
- package/src/server/responses/upstream-error.ts +53 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -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 +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/responses.ts +25 -0
- package/src/server/search.ts +201 -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 +315 -0
- package/src/server/startup-health-cache.ts +131 -0
- package/src/server/system-env.ts +484 -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 +472 -0
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +3575 -0
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -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 +757 -0
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +876 -0
- package/src/types/provider.ts +545 -0
- package/src/types/request.ts +384 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +106 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +415 -0
- package/src/update/job.ts +1887 -0
- package/src/update/notify.ts +263 -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/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -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 +625 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +416 -0
- package/src/usage/log.ts +1223 -0
- package/src/usage/summary.ts +753 -0
- package/src/usage/totals.ts +14 -0
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +189 -0
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +131 -0
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +681 -0
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +195 -0
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/executor.ts +113 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +331 -0
- package/src/web-search/loop.ts +896 -0
- package/src/web-search/parse.ts +315 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/synthetic-tool.ts +47 -0
- package/src/web-search/xai-executor.ts +219 -0
|
@@ -0,0 +1,1504 @@
|
|
|
1
|
+
import type { KiroOAuthMetadata, OAuthController, OAuthCredentials } from "./types";
|
|
2
|
+
import { parseCallbackInput } from "./callback-server";
|
|
3
|
+
import type { OcxConfig, OcxProviderConfig, RefreshPolicy } from "../types";
|
|
4
|
+
import { loadConfig, resolveEnvValue, saveConfig } from "../config";
|
|
5
|
+
import { maskEmail } from "../lib/privacy";
|
|
6
|
+
import { KiroTokenRefreshError, environmentKiroRoutingMetadata, loginKiro, refreshKiroToken, settleKiroLoginTransaction } from "./kiro";
|
|
7
|
+
import { getAccountCredential, getAccountSet, removeAccount, saveAccountCredential, saveCredential, setActiveAccount, getCredential, credentialGeneration, createOAuthRefreshIntentLock, mergeAccountCredential, markAccountNeedsReauthIfGeneration, readOAuthRefreshIntent, writeOAuthRefreshIntent, markOAuthRefreshIntentStaleOwner, clearOAuthRefreshIntent, normalizeAuthStoreBuffer, OAuthMutationBusyError } from "./store";
|
|
8
|
+
import { loginXai, refreshXaiToken, XAI_LOCAL_CLI_DETACH_WARNING, XaiTokenRequestError } from "./xai";
|
|
9
|
+
import { ANTHROPIC_OAUTH_BETA, AnthropicTokenError, loginAnthropic, refreshAnthropicToken } from "./anthropic";
|
|
10
|
+
import { loginKimi, refreshKimiToken } from "./kimi";
|
|
11
|
+
import { loginNous, NousTokenError, refreshNousToken, clearNousRefreshIntent, RefreshIntentIOError } from "./nous";
|
|
12
|
+
import { loginChatGPT, refreshChatGPTToken } from "./chatgpt";
|
|
13
|
+
import { loginAntigravity, refreshAntigravityToken } from "./google-antigravity";
|
|
14
|
+
import { loginCursor, refreshCursorToken } from "./cursor";
|
|
15
|
+
import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUrl } from "./github-copilot";
|
|
16
|
+
import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
|
|
17
|
+
import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire";
|
|
18
|
+
import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
|
|
19
|
+
import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys";
|
|
20
|
+
import { effectiveGoogleMode, getProviderRegistryEntry, mergeRegistryStaticHeaders, providerMatchesRegistryTransport } from "../providers/registry";
|
|
21
|
+
import { resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
|
|
22
|
+
import { resolveProviderTransport } from "../providers/xai-transport";
|
|
23
|
+
import { detectClaudeCodeToken, detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
|
|
24
|
+
import { logOAuthEvent } from "./log";
|
|
25
|
+
import { captureConfigGeneration, sweepExpiredOnWrite, type GenerationContext } from "../lib/state-store-sweeper";
|
|
26
|
+
import { retainedUtf8Bytes } from "../lib/admission";
|
|
27
|
+
import { randomUUID } from "node:crypto";
|
|
28
|
+
export {
|
|
29
|
+
CODEX_HEALTH_AUTH_FAILED_NOTE,
|
|
30
|
+
CODEX_HEALTH_MANAGEMENT_API_UNAVAILABLE_NOTE,
|
|
31
|
+
CODEX_HEALTH_UNAVAILABLE_NOTE,
|
|
32
|
+
MASKED_ACCOUNT_FALLBACK,
|
|
33
|
+
collectOAuthHealthEntries,
|
|
34
|
+
collectOAuthHealthEntriesForCli,
|
|
35
|
+
detectOAuthWarning,
|
|
36
|
+
oauthAccountHealthFields,
|
|
37
|
+
oauthHealthLabel,
|
|
38
|
+
oauthHealthSummary,
|
|
39
|
+
projectCodexAccountHealth,
|
|
40
|
+
projectOAuthAccountHealth,
|
|
41
|
+
projectStoredOAuthAccountHealth,
|
|
42
|
+
type CodexHealthSource,
|
|
43
|
+
type OAuthAccountHealth,
|
|
44
|
+
type OAuthAccountHealthFields,
|
|
45
|
+
type OAuthCliHealthReport,
|
|
46
|
+
type OAuthHealthEntry,
|
|
47
|
+
type OAuthHealthLabel,
|
|
48
|
+
} from "./health";
|
|
49
|
+
export { OAUTH_REFRESH_LOCK_WAIT_MS, peekAuthStore, peekOAuthRefreshIntent } from "./store";
|
|
50
|
+
import { codexAccountNamespaceProviderCollisionError } from "../codex/account-namespace-match";
|
|
51
|
+
|
|
52
|
+
const REFRESH_SKEW_MS = 60_000;
|
|
53
|
+
export interface OAuthAccessSnapshot {
|
|
54
|
+
provider: string;
|
|
55
|
+
accountId: string;
|
|
56
|
+
generation: string;
|
|
57
|
+
accessToken: string;
|
|
58
|
+
/** Cloud Code Assist project selected during Antigravity login. */
|
|
59
|
+
projectId?: string;
|
|
60
|
+
/** Safe request-routing subset; refresh-only Kiro client secrets never leave the credential store. */
|
|
61
|
+
kiro?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion">;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ObservedOAuthAccessSnapshot extends OAuthAccessSnapshot {
|
|
65
|
+
/** Allowlisted provider API origin consumed by GitHub Copilot model discovery. */
|
|
66
|
+
apiBaseUrl?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type OAuthActiveTokenObservation =
|
|
70
|
+
| { readonly kind: "available"; readonly snapshot: ObservedOAuthAccessSnapshot }
|
|
71
|
+
| { readonly kind: "missing" }
|
|
72
|
+
| { readonly kind: "malformed" }
|
|
73
|
+
| { readonly kind: "needs-reauth" }
|
|
74
|
+
| { readonly kind: "expired" }
|
|
75
|
+
| { readonly kind: "near-expiry" }
|
|
76
|
+
| { readonly kind: "unsupported" };
|
|
77
|
+
|
|
78
|
+
const MAX_OAUTH_TOKEN_REFRESH_FLIGHTS = 32;
|
|
79
|
+
const OAUTH_TOKEN_REFRESH_FLIGHT_STALE_MS = 120_000;
|
|
80
|
+
interface OAuthRefreshFlightEvidence { flightId: string; dispatched: boolean }
|
|
81
|
+
interface OAuthTokenRefreshFlight extends OAuthRefreshFlightEvidence { promise: Promise<OAuthAccessSnapshot>; startedAt: number; abort: AbortController }
|
|
82
|
+
const tokenRefreshes = new Map<string, OAuthTokenRefreshFlight>();
|
|
83
|
+
export class OAuthTokenRefreshBusyError extends Error {
|
|
84
|
+
readonly code = "OAUTH_TOKEN_REFRESH_BUSY";
|
|
85
|
+
readonly retryable = true;
|
|
86
|
+
constructor() { super("OAuth token refresh capacity reached"); this.name = "OAuthTokenRefreshBusyError"; }
|
|
87
|
+
}
|
|
88
|
+
export class OAuthTokenRefreshStaleError extends Error {
|
|
89
|
+
readonly code = "OAUTH_TOKEN_REFRESH_STALE";
|
|
90
|
+
readonly retryable = true;
|
|
91
|
+
constructor() { super("OAuth token refresh owner became stale"); this.name = "OAuthTokenRefreshStaleError"; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Focused owner-identity tests only. Synthetic owners retain no account data. */
|
|
95
|
+
export function seedOAuthTokenRefreshFlightsForTests(rows: Array<{ key: string; startedAt?: number; flightId?: string; dispatched?: boolean }>): {
|
|
96
|
+
promises: Promise<OAuthAccessSnapshot>[];
|
|
97
|
+
cleanup: () => void;
|
|
98
|
+
} {
|
|
99
|
+
const inserted: OAuthTokenRefreshFlight[] = [];
|
|
100
|
+
const promises = rows.map(({ key, startedAt, flightId, dispatched }) => {
|
|
101
|
+
const abort = new AbortController();
|
|
102
|
+
const promise = new Promise<OAuthAccessSnapshot>((_resolve, reject) => {
|
|
103
|
+
abort.signal.addEventListener("abort", () => reject(abort.signal.reason), { once: true });
|
|
104
|
+
});
|
|
105
|
+
const flight = { promise, startedAt: startedAt ?? Date.now(), abort, flightId: flightId ?? randomUUID(), dispatched: dispatched ?? false };
|
|
106
|
+
tokenRefreshes.set(key, flight);
|
|
107
|
+
inserted.push(flight);
|
|
108
|
+
return promise;
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
promises,
|
|
112
|
+
cleanup() {
|
|
113
|
+
for (const [key, flight] of tokenRefreshes) {
|
|
114
|
+
if (!inserted.includes(flight)) continue;
|
|
115
|
+
tokenRefreshes.delete(key);
|
|
116
|
+
flight.abort.abort(new Error("test cleanup"));
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const XAI_PERMANENT_FAILURE_TTL_MS=30_000;
|
|
122
|
+
const permanentRefreshFailures=new Map<string,number>();
|
|
123
|
+
interface XaiRefreshDeps { intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; now?:()=>number; afterPrePersistRead?:()=>void|Promise<void>; signal?: AbortSignal }
|
|
124
|
+
interface AnthropicRefreshDeps { intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; now?:()=>number; afterPrePersistRead?:()=>void|Promise<void>; signal?: AbortSignal; flight?: OAuthRefreshFlightEvidence; replacedStaleFlight?: OAuthRefreshFlightEvidence }
|
|
125
|
+
interface GenericRefreshDeps { intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; afterPrePersistRead?:()=>void|Promise<void>; signal?: AbortSignal }
|
|
126
|
+
function verdictKey(p:string,a:string,c:OAuthCredentials){return `${p}\0${a}\0${credentialGeneration(c)}`;}
|
|
127
|
+
function cached(p:string,a:string,c:OAuthCredentials,now:()=>number){const k=verdictKey(p,a,c),u=permanentRefreshFailures.get(k);if(u===undefined)return false;if(u<=now()){permanentRefreshFailures.delete(k);return false;}return true;}
|
|
128
|
+
export function sweepExpiredXaiPermanentFailureVerdicts(now=Date.now()):number{let removed=0;for(const[key,until]of permanentRefreshFailures){if(until>now)continue;permanentRefreshFailures.delete(key);removed+=1;}return removed;}
|
|
129
|
+
|
|
130
|
+
export interface LoginOpts { forceLogin?: boolean; /** When set, persist into this account slot and require matching identity. */ reauthAccountId?: string }
|
|
131
|
+
|
|
132
|
+
export interface LoginFlowLifecycle {
|
|
133
|
+
/** Runs after background credential/config persistence settles, before status becomes done. */
|
|
134
|
+
onSettled?: () => void | Promise<void>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface OAuthProviderDef {
|
|
138
|
+
login(ctrl: OAuthController, opts?: LoginOpts): Promise<OAuthCredentials>;
|
|
139
|
+
refresh(
|
|
140
|
+
refreshToken: string,
|
|
141
|
+
signal?: AbortSignal,
|
|
142
|
+
credential?: OAuthCredentials,
|
|
143
|
+
): Promise<OAuthCredentials>;
|
|
144
|
+
/** provider entry written into config.json on first login. */
|
|
145
|
+
providerConfig: OcxProviderConfig;
|
|
146
|
+
defaultModel: string;
|
|
147
|
+
/**
|
|
148
|
+
* Built-in proactive-refresh policy, risk-tiered by the provider's ToS exposure (devlog
|
|
149
|
+
* 260703_oauth-multi-account-refresh-and-tos). A user's per-provider `config.providers[x].refreshPolicy`
|
|
150
|
+
* overrides this. Default when unset here: "lazy-only".
|
|
151
|
+
*/
|
|
152
|
+
defaultRefreshPolicy?: RefreshPolicy;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function oauthConfig(id: string): OcxProviderConfig {
|
|
156
|
+
const config = deriveOAuthProviderConfig(id);
|
|
157
|
+
if (!config) throw new Error(`OAuth provider missing from registry: ${id}`);
|
|
158
|
+
return config;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function oauthDefaultModel(id: string): string {
|
|
162
|
+
const model = deriveOAuthDefaultModel(id);
|
|
163
|
+
if (!model) throw new Error(`OAuth provider missing default model in registry: ${id}`);
|
|
164
|
+
return model;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
|
|
168
|
+
"command-code": {
|
|
169
|
+
// Add-account/reauth must not reimport the current local CLI credential.
|
|
170
|
+
login: (ctrl, opts) => loginCommandCode(ctrl, { importLocal: opts?.forceLogin ? "off" : "fallback" }),
|
|
171
|
+
refresh: refreshCommandCodeToken,
|
|
172
|
+
providerConfig: oauthConfig("command-code"),
|
|
173
|
+
defaultModel: oauthDefaultModel("command-code"),
|
|
174
|
+
defaultRefreshPolicy: "disabled",
|
|
175
|
+
},
|
|
176
|
+
xai: {
|
|
177
|
+
// forceLogin skips the local grok-cli import so a SECOND account can be chosen in the browser.
|
|
178
|
+
login: (ctrl, opts) => loginXai(ctrl, { importLocal: opts?.forceLogin ? "off" : "fallback" }),
|
|
179
|
+
refresh: refreshXaiToken,
|
|
180
|
+
providerConfig: oauthConfig("xai"),
|
|
181
|
+
defaultModel: oauthDefaultModel("xai"),
|
|
182
|
+
},
|
|
183
|
+
anthropic: {
|
|
184
|
+
login: (ctrl, opts) => loginAnthropic(ctrl, { importLocal: opts?.forceLogin ? "off" : "fallback" }),
|
|
185
|
+
refresh: refreshAnthropicToken,
|
|
186
|
+
providerConfig: oauthConfig("anthropic"),
|
|
187
|
+
defaultModel: oauthDefaultModel("anthropic"),
|
|
188
|
+
// Anthropic actively server-side-blocks subscription OAuth outside its own clients (Feb 2026).
|
|
189
|
+
// Never generate background refresh traffic for it — grade 20, highest ToS risk.
|
|
190
|
+
defaultRefreshPolicy: "disabled",
|
|
191
|
+
},
|
|
192
|
+
kimi: {
|
|
193
|
+
login: (ctrl) => loginKimi(ctrl),
|
|
194
|
+
refresh: refreshKimiToken,
|
|
195
|
+
providerConfig: oauthConfig("kimi"),
|
|
196
|
+
defaultModel: oauthDefaultModel("kimi"),
|
|
197
|
+
},
|
|
198
|
+
nous: {
|
|
199
|
+
// Nous Portal device-grant login (RFC 8628) against portal.nousresearch.com.
|
|
200
|
+
// The access token is the per-request inference JWT (scope inference:invoke).
|
|
201
|
+
// Refresh tokens are single-use and rotated server-side on every refresh:
|
|
202
|
+
// keep background refresh lazy-only (the default) so concurrent refreshes
|
|
203
|
+
// cannot trip the Portal's token-reuse revocation.
|
|
204
|
+
login: (ctrl) => loginNous(ctrl),
|
|
205
|
+
refresh: (rt, signal) => refreshNousToken(rt, signal),
|
|
206
|
+
providerConfig: oauthConfig("nous"),
|
|
207
|
+
defaultModel: oauthDefaultModel("nous"),
|
|
208
|
+
// Single-use rotating refresh tokens must never be background-refreshed
|
|
209
|
+
// proactively: concurrent refreshes would trip the Portal's reuse
|
|
210
|
+
// revocation. Anchor the lazy-only default explicitly so it cannot be
|
|
211
|
+
// silently overridden to proactive.
|
|
212
|
+
defaultRefreshPolicy: "lazy-only",
|
|
213
|
+
},
|
|
214
|
+
kiro: {
|
|
215
|
+
login: (ctrl, opts) => loginKiro(ctrl, { forceLogin: opts?.forceLogin }),
|
|
216
|
+
refresh: (rt, signal, credential) => refreshKiroToken(rt, signal, credential),
|
|
217
|
+
providerConfig: oauthConfig("kiro"),
|
|
218
|
+
defaultModel: oauthDefaultModel("kiro"),
|
|
219
|
+
},
|
|
220
|
+
"google-antigravity": {
|
|
221
|
+
login: (ctrl, opts) => loginAntigravity(ctrl, { forceAccountSelect: opts?.forceLogin === true }),
|
|
222
|
+
refresh: refreshAntigravityToken,
|
|
223
|
+
providerConfig: oauthConfig("google-antigravity"),
|
|
224
|
+
defaultModel: oauthDefaultModel("google-antigravity"),
|
|
225
|
+
},
|
|
226
|
+
cursor: {
|
|
227
|
+
login: (ctrl, opts) => loginCursor(ctrl, undefined, { forceLogin: opts?.forceLogin }),
|
|
228
|
+
refresh: refreshCursorToken,
|
|
229
|
+
providerConfig: oauthConfig("cursor"),
|
|
230
|
+
defaultModel: oauthDefaultModel("cursor"),
|
|
231
|
+
},
|
|
232
|
+
"github-copilot": {
|
|
233
|
+
login: (ctrl) => loginGithubCopilot(ctrl),
|
|
234
|
+
refresh: (rt, signal) => refreshGithubCopilotToken(rt, signal),
|
|
235
|
+
providerConfig: oauthConfig("github-copilot"),
|
|
236
|
+
defaultModel: oauthDefaultModel("github-copilot"),
|
|
237
|
+
// Unofficial Copilot bridge — keep proactive traffic lazy-only (no background guardian spam).
|
|
238
|
+
defaultRefreshPolicy: "lazy-only",
|
|
239
|
+
},
|
|
240
|
+
chatgpt: {
|
|
241
|
+
login: loginChatGPT,
|
|
242
|
+
refresh: (rt) => refreshChatGPTToken(rt),
|
|
243
|
+
providerConfig: { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward" as const },
|
|
244
|
+
defaultModel: "gpt-5.4",
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export function isOAuthProvider(name: string): boolean {
|
|
249
|
+
return name in OAUTH_PROVIDERS;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function isPublicOAuthProvider(name: string): boolean {
|
|
253
|
+
return name !== "chatgpt" && isOAuthProvider(name);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function isRefreshPolicy(value: unknown): value is RefreshPolicy {
|
|
257
|
+
return value === "proactive" || value === "lazy-only" || value === "disabled";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The effective proactive-refresh policy for a provider: the user's per-provider
|
|
262
|
+
* `config.providers[provider].refreshPolicy` if set, else the provider def's risk-tiered default,
|
|
263
|
+
* else "lazy-only". The guardian acts only when this resolves to "proactive".
|
|
264
|
+
*/
|
|
265
|
+
export function resolveRefreshPolicy(provider: string, config: OcxConfig): RefreshPolicy {
|
|
266
|
+
const override = config.providers[provider]?.refreshPolicy;
|
|
267
|
+
if (isRefreshPolicy(override)) return override;
|
|
268
|
+
const def = OAUTH_PROVIDERS[provider];
|
|
269
|
+
return def?.defaultRefreshPolicy ?? "lazy-only";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The discovered project id stored on an OAuth credential (Antigravity CCA), if any. */
|
|
273
|
+
export function getOAuthCredentialProjectId(provider: string): string | undefined {
|
|
274
|
+
return getCredential(provider)?.projectId;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Allowlisted Copilot API origin from the active credential, if still valid. */
|
|
278
|
+
export function getOAuthCredentialApiBaseUrl(provider: string): string | undefined {
|
|
279
|
+
return validateCopilotApiBaseUrl(getCredential(provider)?.apiBaseUrl);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Provider ids that support real OAuth login (drives the GUI's "Log in with …" buttons). */
|
|
283
|
+
export function listOAuthProviders(): string[] {
|
|
284
|
+
return Object.keys(OAUTH_PROVIDERS).filter(isPublicOAuthProvider);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export class UnsupportedOAuthProviderError extends Error {
|
|
288
|
+
constructor(provider: string) {
|
|
289
|
+
super(`Unsupported OAuth provider in config: ${provider}`);
|
|
290
|
+
this.name = "UnsupportedOAuthProviderError";
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export class OAuthLoginRequiredError extends Error {
|
|
295
|
+
readonly provider: string;
|
|
296
|
+
|
|
297
|
+
constructor(provider: string) {
|
|
298
|
+
super(`Not logged in to ${provider}. Run: ocx login ${provider}`);
|
|
299
|
+
this.name = "OAuthLoginRequiredError";
|
|
300
|
+
this.provider = provider;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export class OAuthProviderPublicationError extends Error {
|
|
305
|
+
constructor() {
|
|
306
|
+
super("OAuth credential was saved, but the provider entry was not written. Resolve the account namespace collision, then retry login.");
|
|
307
|
+
this.name = "OAuthProviderPublicationError";
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export class OAuthReauthIdentityMismatchError extends Error {
|
|
312
|
+
constructor() {
|
|
313
|
+
super("Signed-in account does not match the selected account. Sign in with the same account.");
|
|
314
|
+
this.name = "OAuthReauthIdentityMismatchError";
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export class OAuthReauthIdentityUnverifiedError extends Error {
|
|
319
|
+
constructor() {
|
|
320
|
+
super("Could not verify signed-in account identity for reauth.");
|
|
321
|
+
this.name = "OAuthReauthIdentityUnverifiedError";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
class OAuthLoginSupersededError extends Error {
|
|
326
|
+
constructor() {
|
|
327
|
+
super("OAuth login was superseded before credential persistence");
|
|
328
|
+
this.name = "OAuthLoginSupersededError";
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Project arbitrary OAuth failures onto the small, stable public error vocabulary. */
|
|
333
|
+
export function publicOAuthAuthenticationErrorMessage(error: unknown): string {
|
|
334
|
+
if (error instanceof OAuthMutationBusyError) {
|
|
335
|
+
return error.message === "OAuth mutation queue wait timed out"
|
|
336
|
+
? "OAuth mutation queue wait timed out"
|
|
337
|
+
: "OAuth mutation queue is busy";
|
|
338
|
+
}
|
|
339
|
+
if (
|
|
340
|
+
(error instanceof OAuthLoginRequiredError && isOAuthProvider(error.provider))
|
|
341
|
+
|| error instanceof OAuthProviderPublicationError
|
|
342
|
+
// Reauth identity outcomes carry fixed, account-free remediation text. Dropping them to the
|
|
343
|
+
// generic message hides WHICH failure the user must fix (sign in with the selected account).
|
|
344
|
+
|| error instanceof OAuthReauthIdentityMismatchError
|
|
345
|
+
|| error instanceof OAuthReauthIdentityUnverifiedError
|
|
346
|
+
|| error instanceof OAuthTokenRefreshBusyError
|
|
347
|
+
|| error instanceof OAuthTokenRefreshStaleError
|
|
348
|
+
) return error.message;
|
|
349
|
+
return "OAuth authentication failed. Check the OpenCodex account status and retry.";
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function accessSnapshot(provider: string, accountId: string, cred: OAuthCredentials): OAuthAccessSnapshot {
|
|
353
|
+
const storedKiroRouting = {
|
|
354
|
+
...(cred.kiro?.profileArn ? { profileArn: cred.kiro.profileArn } : {}),
|
|
355
|
+
...(cred.kiro?.apiRegion ? { apiRegion: cred.kiro.apiRegion } : {}),
|
|
356
|
+
...(cred.kiro?.ssoRegion ? { ssoRegion: cred.kiro.ssoRegion } : {}),
|
|
357
|
+
};
|
|
358
|
+
return {
|
|
359
|
+
provider,
|
|
360
|
+
accountId,
|
|
361
|
+
generation: credentialGeneration(cred),
|
|
362
|
+
accessToken: cred.access,
|
|
363
|
+
...(cred.projectId ? { projectId: cred.projectId } : {}),
|
|
364
|
+
// Stored account metadata remains authoritative. Metadata-less legacy/environment credentials
|
|
365
|
+
// may use explicit environment routing, but never borrow the currently signed-in local CLI account.
|
|
366
|
+
...(provider === "kiro"
|
|
367
|
+
? {
|
|
368
|
+
kiro: Object.keys(storedKiroRouting).length > 0
|
|
369
|
+
? storedKiroRouting
|
|
370
|
+
: environmentKiroRoutingMetadata() ?? {},
|
|
371
|
+
}
|
|
372
|
+
: {}),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Observe the active OAuth token from an auth-store buffer supplied by its filesystem owner.
|
|
378
|
+
* Missing, malformed, reauth-required, and expiring credentials are typed no-token outcomes;
|
|
379
|
+
* this path never refreshes, locks, hardens, backs up, or persists credentials.
|
|
380
|
+
*/
|
|
381
|
+
export function observeActiveOAuthAccessToken(
|
|
382
|
+
provider: string,
|
|
383
|
+
authStoreBuffer: Uint8Array | null,
|
|
384
|
+
now = Date.now(),
|
|
385
|
+
): OAuthActiveTokenObservation {
|
|
386
|
+
const authStore = normalizeAuthStoreBuffer(authStoreBuffer);
|
|
387
|
+
if (authStore.kind === "absent") return { kind: "missing" };
|
|
388
|
+
if (authStore.kind === "malformed") return { kind: "malformed" };
|
|
389
|
+
if (!isOAuthProvider(provider)) return { kind: "unsupported" };
|
|
390
|
+
|
|
391
|
+
const accountSet = authStore.store[provider];
|
|
392
|
+
const account = accountSet?.accounts.find(candidate => candidate.id === accountSet.activeAccountId);
|
|
393
|
+
if (!account) return { kind: "missing" };
|
|
394
|
+
if (account.needsReauth) return { kind: "needs-reauth" };
|
|
395
|
+
if (account.credential.expires <= now) return { kind: "expired" };
|
|
396
|
+
if (account.credential.expires <= now + REFRESH_SKEW_MS) return { kind: "near-expiry" };
|
|
397
|
+
|
|
398
|
+
const apiBaseUrl = validateCopilotApiBaseUrl(account.credential.apiBaseUrl);
|
|
399
|
+
return {
|
|
400
|
+
kind: "available",
|
|
401
|
+
snapshot: {
|
|
402
|
+
...accessSnapshot(provider, account.id, account.credential),
|
|
403
|
+
...(apiBaseUrl ? { apiBaseUrl } : {}),
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function resolveAccessSnapshotForAccount(
|
|
409
|
+
provider: string,
|
|
410
|
+
accountId: string,
|
|
411
|
+
rejectedGeneration?: string,
|
|
412
|
+
): Promise<OAuthAccessSnapshot> {
|
|
413
|
+
const def = OAUTH_PROVIDERS[provider];
|
|
414
|
+
if (!def) throw new UnsupportedOAuthProviderError(provider);
|
|
415
|
+
const cred = getAccountCredential(provider, accountId);
|
|
416
|
+
if (!cred) throw new OAuthLoginRequiredError(provider);
|
|
417
|
+
const current = accessSnapshot(provider, accountId, cred);
|
|
418
|
+
if (rejectedGeneration !== undefined && current.generation !== rejectedGeneration) return current;
|
|
419
|
+
if (rejectedGeneration === undefined && cred.expires > Date.now() + REFRESH_SKEW_MS) return current;
|
|
420
|
+
|
|
421
|
+
const key = `${provider}\u0000${accountId}`;
|
|
422
|
+
let existing = tokenRefreshes.get(key);
|
|
423
|
+
let replacedStaleFlight: OAuthRefreshFlightEvidence | undefined;
|
|
424
|
+
if (existing && Date.now() - existing.startedAt <= OAUTH_TOKEN_REFRESH_FLIGHT_STALE_MS) {
|
|
425
|
+
logOAuthEvent("OAuth refresh joined existing operation", { provider, accountId });
|
|
426
|
+
return existing.promise;
|
|
427
|
+
}
|
|
428
|
+
if (existing) {
|
|
429
|
+
replacedStaleFlight = { flightId: existing.flightId, dispatched: existing.dispatched };
|
|
430
|
+
existing.abort.abort(new OAuthTokenRefreshStaleError());
|
|
431
|
+
if (tokenRefreshes.get(key) === existing) tokenRefreshes.delete(key);
|
|
432
|
+
existing = undefined;
|
|
433
|
+
}
|
|
434
|
+
if (tokenRefreshes.size >= MAX_OAUTH_TOKEN_REFRESH_FLIGHTS) throw new OAuthTokenRefreshBusyError();
|
|
435
|
+
|
|
436
|
+
const abort = new AbortController();
|
|
437
|
+
const flight: OAuthTokenRefreshFlight = {
|
|
438
|
+
promise: undefined as unknown as Promise<OAuthAccessSnapshot>,
|
|
439
|
+
startedAt: Date.now(),
|
|
440
|
+
abort,
|
|
441
|
+
flightId: randomUUID(),
|
|
442
|
+
dispatched: false,
|
|
443
|
+
};
|
|
444
|
+
const refresh = (async (): Promise<OAuthAccessSnapshot> => {
|
|
445
|
+
const accessToken = await refreshAndPersistAccessToken(provider, accountId, def, cred, abort.signal, flight, replacedStaleFlight);
|
|
446
|
+
const persisted = getAccountCredential(provider, accountId);
|
|
447
|
+
if (!persisted) throw new OAuthLoginRequiredError(provider);
|
|
448
|
+
if (persisted.access !== accessToken) {
|
|
449
|
+
throw new Error(`OAuth refresh persisted an unexpected access token for ${provider}`);
|
|
450
|
+
}
|
|
451
|
+
return accessSnapshot(provider, accountId, persisted);
|
|
452
|
+
})().catch(error => {
|
|
453
|
+
if (abort.signal.reason instanceof OAuthTokenRefreshStaleError) throw abort.signal.reason;
|
|
454
|
+
throw error;
|
|
455
|
+
}).finally(() => {
|
|
456
|
+
if (tokenRefreshes.get(key) === flight) tokenRefreshes.delete(key);
|
|
457
|
+
});
|
|
458
|
+
flight.promise = refresh;
|
|
459
|
+
tokenRefreshes.set(key, flight);
|
|
460
|
+
return refresh;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export async function getValidAccessTokenSnapshot(provider: string): Promise<OAuthAccessSnapshot> {
|
|
464
|
+
const set = getAccountSet(provider);
|
|
465
|
+
if (!set) throw new OAuthLoginRequiredError(provider);
|
|
466
|
+
return resolveAccessSnapshotForAccount(provider, set.activeAccountId);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Providers whose upstream-401 replay path may force a snapshot refresh. */
|
|
470
|
+
const FORCE_REFRESH_PROVIDERS = new Set(["xai", "github-copilot", "kiro"]);
|
|
471
|
+
|
|
472
|
+
export async function forceRefreshOAuthAccessSnapshot(
|
|
473
|
+
rejected: OAuthAccessSnapshot,
|
|
474
|
+
): Promise<OAuthAccessSnapshot> {
|
|
475
|
+
if (!FORCE_REFRESH_PROVIDERS.has(rejected.provider)) throw new UnsupportedOAuthProviderError(rejected.provider);
|
|
476
|
+
return resolveAccessSnapshotForAccount(rejected.provider, rejected.accountId, rejected.generation);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Return a valid access token for the ACTIVE account, refreshing + persisting if expired. */
|
|
480
|
+
export async function getValidAccessToken(provider: string): Promise<string> {
|
|
481
|
+
return (await getValidAccessTokenSnapshot(provider)).accessToken;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Account-scoped token resolver (multiauth): refresh is single-flighted per
|
|
486
|
+
* (provider, account), and the rotated credential is persisted for THAT account only —
|
|
487
|
+
* a guardian refresh of a background account never switches the active account.
|
|
488
|
+
*/
|
|
489
|
+
export async function getValidAccessTokenForAccount(provider: string, accountId: string): Promise<string> {
|
|
490
|
+
return (await resolveAccessSnapshotForAccount(provider, accountId)).accessToken;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Terminal refresh failures (revoked/rotated-away grants) — retrying cannot succeed. */
|
|
494
|
+
function isTerminalRefreshError(err: unknown): boolean {
|
|
495
|
+
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
496
|
+
return msg.includes("invalid_grant")
|
|
497
|
+
|| msg.includes("refresh_token_reused")
|
|
498
|
+
|| msg.includes("revoked")
|
|
499
|
+
// GitHub Copilot refresh surfaces allowlisted OAuth codes (github-copilot.ts):
|
|
500
|
+
|| msg.includes("access_denied")
|
|
501
|
+
|| msg.includes("expired_token");
|
|
502
|
+
}
|
|
503
|
+
function terminal(error:unknown):boolean{
|
|
504
|
+
if(error instanceof XaiTokenRequestError)return ["invalid_grant","refresh_token_reused","revoked_token"].includes(error.oauthError??"");
|
|
505
|
+
if(error instanceof AnthropicTokenError)return (error.httpStatus===400||error.httpStatus===401)&&["invalid_grant","refresh_token_reused","revoked","revoked_token","refresh_token_revoked"].includes(error.oauthError??"");
|
|
506
|
+
if(error instanceof KiroTokenRefreshError)return (error.httpStatus===400||error.httpStatus===401)&&error.oauthError!==undefined;
|
|
507
|
+
if(error instanceof NousTokenError)return error.terminal===true||["invalid_grant","refresh_token_reused","revoked","revoked_token","expired_token"].includes(error.oauthError??"");
|
|
508
|
+
// Local durable-write/read/cleanup failures are operational, not credential
|
|
509
|
+
// death: the provider credential was never rejected or consumed. Never mark
|
|
510
|
+
// the account needsReauth for broken local persistence infrastructure.
|
|
511
|
+
if (error instanceof RefreshIntentIOError) return false;
|
|
512
|
+
return isTerminalRefreshError(error);
|
|
513
|
+
}
|
|
514
|
+
function authoritative(stored:OAuthCredentials,active:boolean,now:()=>number):OAuthCredentials{if(stored.source!=="local-cli")return stored;const disk=detectGrokCliToken();if(!disk)return stored;const allowed=isSameGrokIdentity(stored,disk)||(active&&!hasComparableGrokIdentity(stored,disk));return allowed&&shouldAdoptGrokGeneration(stored,disk,now(),REFRESH_SKEW_MS)?disk:stored;}
|
|
515
|
+
function merged(fresh: OAuthCredentials, previous: OAuthCredentials): OAuthCredentials {
|
|
516
|
+
return {
|
|
517
|
+
...fresh,
|
|
518
|
+
source: previous.source === "local-cli" ? "oauth" : fresh.source ?? previous.source ?? "oauth",
|
|
519
|
+
...(fresh.projectId === undefined && previous.projectId ? { projectId: previous.projectId } : {}),
|
|
520
|
+
...(fresh.apiBaseUrl === undefined && previous.apiBaseUrl ? { apiBaseUrl: previous.apiBaseUrl } : {}),
|
|
521
|
+
...(fresh.email === undefined && previous.email ? { email: previous.email } : {}),
|
|
522
|
+
...(fresh.accountId === undefined && previous.accountId ? { accountId: previous.accountId } : {}),
|
|
523
|
+
...(fresh.kiro === undefined && previous.kiro ? { kiro: previous.kiro } : {}),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
export async function refreshXaiAccountWithLock(provider:string,accountId:string,def:OAuthProviderDef,callerCredential:OAuthCredentials,deps:XaiRefreshDeps={}):Promise<string>{const writerGeneration=captureConfigGeneration();const now=deps.now??Date.now;const guard=await(deps.intentLock??createOAuthRefreshIntentLock(provider,accountId)).acquire();try{const stored=getAccountCredential(provider,accountId);if(!stored)throw new OAuthLoginRequiredError(provider);const active=getAccountSet(provider)?.activeAccountId===accountId,candidate=authoritative(stored,active,now);if(credentialGeneration(candidate)!==credentialGeneration(callerCredential)&&candidate.expires>now()+REFRESH_SKEW_MS){if(credentialGeneration(candidate)!==credentialGeneration(stored)){const o=await mergeAccountCredential(provider,accountId,candidate,{expectedGeneration:credentialGeneration(stored),afterPrePersistRead:deps.afterPrePersistRead});if(o.superseded){if(o.stored.expires>now()+REFRESH_SKEW_MS)return o.stored.access;throw new OAuthLoginRequiredError(provider);}}return candidate.access;}if(cached(provider,accountId,candidate,now))throw new OAuthLoginRequiredError(provider);const generation=credentialGeneration(candidate);try{const fresh=merged(await def.refresh(candidate.refresh,deps.signal),candidate);const o=await mergeAccountCredential(provider,accountId,fresh,{expectedGeneration:generation,afterPrePersistRead:deps.afterPrePersistRead});if(o.superseded){if(o.stored.expires>now()+REFRESH_SKEW_MS)return o.stored.access;throw new OAuthLoginRequiredError(provider);}permanentRefreshFailures.delete(verdictKey(provider,accountId,candidate));if(candidate.source==="local-cli")console.warn(XAI_LOCAL_CLI_DETACH_WARNING);return fresh.access;}catch(error){if(error instanceof OAuthMutationBusyError){permanentRefreshFailures.delete(verdictKey(provider,accountId,candidate));throw error;}if(!terminal(error))throw error;const failedAt=now();permanentRefreshFailures.set(verdictKey(provider,accountId,candidate),failedAt+XAI_PERMANENT_FAILURE_TTL_MS);sweepExpiredOnWrite(failedAt);await markAccountNeedsReauthIfGeneration(provider,accountId,generation,writerGeneration);throw new OAuthLoginRequiredError(provider);}}finally{guard.release();}}
|
|
527
|
+
|
|
528
|
+
function newerClaudeCredential(stored: OAuthCredentials, now: number): OAuthCredentials | undefined {
|
|
529
|
+
if (stored.source !== "local-cli") return undefined;
|
|
530
|
+
const disk = detectClaudeCodeToken();
|
|
531
|
+
if (!disk || disk.expires <= now + REFRESH_SKEW_MS) return undefined;
|
|
532
|
+
return credentialGeneration(disk) !== credentialGeneration(stored) ? disk : undefined;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Preserve an already-rotated Nous refresh token (RT-B) after a terminal refresh
|
|
537
|
+
* error (e.g. the returned access JWT lacked `inference:invoke`). The unusable
|
|
538
|
+
* access token is NOT persisted as valid: the recovery credential carries an
|
|
539
|
+
* empty access placeholder with a past expiry so it can never be routed, and the
|
|
540
|
+
* account is marked needsReauth by the caller. Generation-safe: a concurrent
|
|
541
|
+
* newer write wins and is never overwritten.
|
|
542
|
+
*/
|
|
543
|
+
async function preserveNousRotatedRefresh(
|
|
544
|
+
provider: string,
|
|
545
|
+
accountId: string,
|
|
546
|
+
rotatedRefresh: string,
|
|
547
|
+
expectedGeneration: string,
|
|
548
|
+
previous: OAuthCredentials,
|
|
549
|
+
): Promise<{ kind: "persisted"; generation: string } | { kind: "superseded" } | { kind: "failed" }> {
|
|
550
|
+
try {
|
|
551
|
+
const recovery: OAuthCredentials = {
|
|
552
|
+
refresh: rotatedRefresh,
|
|
553
|
+
// Never persist the unusable access token: an empty placeholder with a
|
|
554
|
+
// past expiry can never be observed as a valid credential.
|
|
555
|
+
access: "",
|
|
556
|
+
expires: 0,
|
|
557
|
+
...(previous.accountId ? { accountId: previous.accountId } : {}),
|
|
558
|
+
...(previous.email ? { email: previous.email } : {}),
|
|
559
|
+
...(previous.source ? { source: previous.source } : {}),
|
|
560
|
+
};
|
|
561
|
+
const outcome = await mergeAccountCredential(provider, accountId, recovery, { expectedGeneration });
|
|
562
|
+
if (outcome.superseded) return { kind: "superseded" };
|
|
563
|
+
// Return the exact generation this write produced, so the caller never
|
|
564
|
+
// re-reads the store (a concurrent writer could otherwise supply a different
|
|
565
|
+
// credential generation and be marked needsReauth by mistake).
|
|
566
|
+
return { kind: "persisted", generation: credentialGeneration(recovery) };
|
|
567
|
+
} catch (error) {
|
|
568
|
+
// A store-mutation busy outcome is transient and retryable; surface it
|
|
569
|
+
// unchanged so the caller can retry rather than treating it as a permanent
|
|
570
|
+
// RT-B persistence failure.
|
|
571
|
+
if (error instanceof OAuthMutationBusyError) throw error;
|
|
572
|
+
return { kind: "failed" };
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export async function refreshAnthropicAccountWithLock(
|
|
577
|
+
provider: string,
|
|
578
|
+
accountId: string,
|
|
579
|
+
def: OAuthProviderDef,
|
|
580
|
+
callerCredential: OAuthCredentials,
|
|
581
|
+
deps: AnthropicRefreshDeps = {},
|
|
582
|
+
): Promise<string> {
|
|
583
|
+
const writerGeneration = captureConfigGeneration();
|
|
584
|
+
const now = deps.now ?? Date.now;
|
|
585
|
+
const guard = await (deps.intentLock ?? createOAuthRefreshIntentLock(provider, accountId)).acquire();
|
|
586
|
+
try {
|
|
587
|
+
const stored = getAccountCredential(provider, accountId);
|
|
588
|
+
if (!stored) throw new OAuthLoginRequiredError(provider);
|
|
589
|
+
const account = getAccountSet(provider)?.accounts.find(candidate => candidate.id === accountId);
|
|
590
|
+
const generation = credentialGeneration(stored);
|
|
591
|
+
let pendingIntent = readOAuthRefreshIntent(provider, accountId);
|
|
592
|
+
const disk = newerClaudeCredential(stored, now());
|
|
593
|
+
if (disk) {
|
|
594
|
+
const outcome = await mergeAccountCredential(provider, accountId, disk, {
|
|
595
|
+
expectedGeneration: credentialGeneration(stored),
|
|
596
|
+
afterPrePersistRead: deps.afterPrePersistRead,
|
|
597
|
+
});
|
|
598
|
+
if (outcome.superseded) {
|
|
599
|
+
if (pendingIntent) clearOAuthRefreshIntent(provider, accountId, pendingIntent.generation);
|
|
600
|
+
if (outcome.stored.expires > now() + REFRESH_SKEW_MS) return outcome.stored.access;
|
|
601
|
+
throw new OAuthLoginRequiredError(provider);
|
|
602
|
+
}
|
|
603
|
+
if (pendingIntent) clearOAuthRefreshIntent(provider, accountId, pendingIntent.generation);
|
|
604
|
+
return disk.access;
|
|
605
|
+
}
|
|
606
|
+
if (!pendingIntent?.uncertain && pendingIntent?.generation === generation) {
|
|
607
|
+
if (pendingIntent.staleOwner) throw new OAuthTokenRefreshStaleError();
|
|
608
|
+
if (deps.replacedStaleFlight && pendingIntent.flightId === deps.replacedStaleFlight.flightId) {
|
|
609
|
+
if (deps.replacedStaleFlight.dispatched) {
|
|
610
|
+
markOAuthRefreshIntentStaleOwner(provider, accountId, generation, deps.replacedStaleFlight.flightId);
|
|
611
|
+
throw new OAuthTokenRefreshStaleError();
|
|
612
|
+
}
|
|
613
|
+
clearOAuthRefreshIntent(provider, accountId, generation);
|
|
614
|
+
pendingIntent = undefined;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (pendingIntent?.uncertain || pendingIntent?.generation === generation) {
|
|
618
|
+
await markAccountNeedsReauthIfGeneration(provider, accountId, generation, writerGeneration);
|
|
619
|
+
throw new OAuthLoginRequiredError(provider);
|
|
620
|
+
}
|
|
621
|
+
if (pendingIntent) clearOAuthRefreshIntent(provider, accountId, pendingIntent.generation);
|
|
622
|
+
if (account?.needsReauth) {
|
|
623
|
+
throw new OAuthLoginRequiredError(provider);
|
|
624
|
+
}
|
|
625
|
+
if (credentialGeneration(stored) !== credentialGeneration(callerCredential) && stored.expires > now() + REFRESH_SKEW_MS) {
|
|
626
|
+
return stored.access;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
try {
|
|
630
|
+
writeOAuthRefreshIntent(provider, accountId, generation, now(), deps.flight?.flightId);
|
|
631
|
+
if (deps.signal?.aborted) throw deps.signal.reason;
|
|
632
|
+
if (deps.flight) deps.flight.dispatched = true;
|
|
633
|
+
const fresh = merged(await def.refresh(stored.refresh, deps.signal), stored);
|
|
634
|
+
const outcome = await mergeAccountCredential(provider, accountId, fresh, {
|
|
635
|
+
expectedGeneration: generation,
|
|
636
|
+
afterPrePersistRead: deps.afterPrePersistRead,
|
|
637
|
+
});
|
|
638
|
+
if (outcome.superseded) {
|
|
639
|
+
clearOAuthRefreshIntent(provider, accountId, generation);
|
|
640
|
+
if (outcome.stored.expires > now() + REFRESH_SKEW_MS) return outcome.stored.access;
|
|
641
|
+
throw new OAuthLoginRequiredError(provider);
|
|
642
|
+
}
|
|
643
|
+
clearOAuthRefreshIntent(provider, accountId, generation);
|
|
644
|
+
return fresh.access;
|
|
645
|
+
} catch (error) {
|
|
646
|
+
if (error instanceof OAuthMutationBusyError) throw error;
|
|
647
|
+
if (!terminal(error)) throw error;
|
|
648
|
+
await markAccountNeedsReauthIfGeneration(provider, accountId, generation, writerGeneration);
|
|
649
|
+
clearOAuthRefreshIntent(provider, accountId, generation);
|
|
650
|
+
throw new OAuthLoginRequiredError(provider);
|
|
651
|
+
}
|
|
652
|
+
} finally {
|
|
653
|
+
guard.release();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export async function refreshGenericAccountWithLock(
|
|
658
|
+
provider: string,
|
|
659
|
+
accountId: string,
|
|
660
|
+
def: OAuthProviderDef,
|
|
661
|
+
callerCredential: OAuthCredentials,
|
|
662
|
+
deps: GenericRefreshDeps = {},
|
|
663
|
+
): Promise<string> {
|
|
664
|
+
const writerGeneration = captureConfigGeneration();
|
|
665
|
+
logOAuthEvent("OAuth refresh started", { provider, accountId });
|
|
666
|
+
const guard = await (deps.intentLock ?? createOAuthRefreshIntentLock(provider, accountId)).acquire();
|
|
667
|
+
try {
|
|
668
|
+
const stored = getAccountCredential(provider, accountId);
|
|
669
|
+
if (!stored) throw new OAuthLoginRequiredError(provider);
|
|
670
|
+
if (
|
|
671
|
+
credentialGeneration(stored) !== credentialGeneration(callerCredential)
|
|
672
|
+
&& stored.expires > Date.now() + REFRESH_SKEW_MS
|
|
673
|
+
) {
|
|
674
|
+
logOAuthEvent("OAuth refresh joined existing operation", { provider, accountId });
|
|
675
|
+
return stored.access;
|
|
676
|
+
}
|
|
677
|
+
const generation = credentialGeneration(stored);
|
|
678
|
+
try {
|
|
679
|
+
const fresh = merged(await def.refresh(stored.refresh, deps.signal, stored), stored);
|
|
680
|
+
const outcome = await mergeAccountCredential(provider, accountId, fresh, {
|
|
681
|
+
expectedGeneration: generation,
|
|
682
|
+
afterPrePersistRead: deps.afterPrePersistRead,
|
|
683
|
+
});
|
|
684
|
+
if (outcome.superseded) {
|
|
685
|
+
if (outcome.stored.expires > Date.now() + REFRESH_SKEW_MS) return outcome.stored.access;
|
|
686
|
+
throw new OAuthLoginRequiredError(provider);
|
|
687
|
+
}
|
|
688
|
+
logOAuthEvent("OAuth credentials rotated and persisted", { provider, accountId });
|
|
689
|
+
// Best-effort bookkeeping cleanup: the rotated credential is already
|
|
690
|
+
// durably persisted. A failure to unlink the old-token intent file
|
|
691
|
+
// (EACCES/EPERM/EBUSY/EROFS) must not turn a committed rotation into a
|
|
692
|
+
// failed refresh. The stale intent keys the OLD token, which is no longer
|
|
693
|
+
// stored, so leaving it behind blocks nothing and is safe. It must also
|
|
694
|
+
// never route through the generic refresh error path (no needsReauth).
|
|
695
|
+
if (provider === "nous") {
|
|
696
|
+
try {
|
|
697
|
+
clearNousRefreshIntent(stored.refresh);
|
|
698
|
+
} catch (cleanupErr) {
|
|
699
|
+
logOAuthEvent("OAuth refresh intent cleanup failed (non-fatal)", {
|
|
700
|
+
provider,
|
|
701
|
+
accountId,
|
|
702
|
+
cause: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr),
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return fresh.access;
|
|
707
|
+
} catch (error) {
|
|
708
|
+
if (error instanceof OAuthMutationBusyError) throw error;
|
|
709
|
+
if (!terminal(error)) throw error;
|
|
710
|
+
// Nous-specific failure-atomicity: a terminal refresh error that carries
|
|
711
|
+
// an already-issued rotated refresh token (e.g. the access JWT lacked the
|
|
712
|
+
// required `inference:invoke` scope) means the server consumed RT-A and
|
|
713
|
+
// issued RT-B. RT-B must be preserved generation-safely BEFORE forcing
|
|
714
|
+
// reauthentication; discarding it would lose the only usable refresh
|
|
715
|
+
// material and force a full re-auth for no reason.
|
|
716
|
+
if (provider === "nous" && error instanceof NousTokenError) {
|
|
717
|
+
const rotated = error.getRotatedRefresh();
|
|
718
|
+
if (rotated !== undefined && rotated !== stored.refresh) {
|
|
719
|
+
const outcome = await preserveNousRotatedRefresh(
|
|
720
|
+
provider,
|
|
721
|
+
accountId,
|
|
722
|
+
rotated,
|
|
723
|
+
generation,
|
|
724
|
+
stored,
|
|
725
|
+
);
|
|
726
|
+
if (outcome.kind === "persisted") {
|
|
727
|
+
// RT-A's intent is cleared only after RT-B is durably persisted;
|
|
728
|
+
// cleanup itself stays best-effort (a stale RT-A intent keys a token
|
|
729
|
+
// that is no longer stored).
|
|
730
|
+
try {
|
|
731
|
+
clearNousRefreshIntent(stored.refresh);
|
|
732
|
+
} catch (cleanupErr) {
|
|
733
|
+
logOAuthEvent("OAuth refresh intent cleanup failed (non-fatal)", {
|
|
734
|
+
provider,
|
|
735
|
+
accountId,
|
|
736
|
+
cause: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr),
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
// Mark exactly the generation this write produced — never a
|
|
740
|
+
// credential written by a concurrent login between the merge and
|
|
741
|
+
// this step.
|
|
742
|
+
await markAccountNeedsReauthIfGeneration(provider, accountId, outcome.generation, writerGeneration);
|
|
743
|
+
} else {
|
|
744
|
+
// RT-B persistence failed or a newer generation superseded it:
|
|
745
|
+
// never clear RT-A's intent (RT-A was consumed), and mark the old
|
|
746
|
+
// generation needsReauth (a no-op if a newer generation won).
|
|
747
|
+
await markAccountNeedsReauthIfGeneration(provider, accountId, generation, writerGeneration);
|
|
748
|
+
}
|
|
749
|
+
throw new OAuthLoginRequiredError(provider);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
await markAccountNeedsReauthIfGeneration(provider, accountId, generation, writerGeneration);
|
|
753
|
+
throw new OAuthLoginRequiredError(provider);
|
|
754
|
+
}
|
|
755
|
+
} finally {
|
|
756
|
+
guard.release();
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function refreshAndPersistAccessToken(
|
|
761
|
+
provider: string,
|
|
762
|
+
accountId: string,
|
|
763
|
+
def: OAuthProviderDef,
|
|
764
|
+
cred: OAuthCredentials,
|
|
765
|
+
signal?: AbortSignal,
|
|
766
|
+
flight?: OAuthRefreshFlightEvidence,
|
|
767
|
+
replacedStaleFlight?: OAuthRefreshFlightEvidence,
|
|
768
|
+
): Promise<string> {
|
|
769
|
+
if (provider === "xai") return refreshXaiAccountWithLock(provider, accountId, def, cred, { signal });
|
|
770
|
+
if (provider === "anthropic") return refreshAnthropicAccountWithLock(provider, accountId, def, cred, { signal, flight, replacedStaleFlight });
|
|
771
|
+
return refreshGenericAccountWithLock(provider, accountId, def, cred, { signal });
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Shared bearer-token resolver for /models listing — used by BOTH server.ts:fetchAllModels and
|
|
776
|
+
* codex-catalog.ts:fetchProviderModels so OAuth providers' models are listed once logged in.
|
|
777
|
+
* Returns undefined for forward-mode or oauth-not-logged-in (caller skips).
|
|
778
|
+
*/
|
|
779
|
+
export async function resolveModelsAuthToken(name: string, prov: OcxProviderConfig): Promise<string | undefined> {
|
|
780
|
+
if (prov.authMode === "forward") return undefined;
|
|
781
|
+
if (prov.authMode === "oauth") {
|
|
782
|
+
try {
|
|
783
|
+
return await getValidAccessToken(name);
|
|
784
|
+
} catch {
|
|
785
|
+
return undefined;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return resolveEnvValue(prov.apiKey);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function modelDiscoveryTransportSeed(providerName: string, prov: OcxProviderConfig): OcxProviderConfig {
|
|
792
|
+
const entry = getProviderRegistryEntry(providerName);
|
|
793
|
+
if (
|
|
794
|
+
prov.authMode !== "oauth"
|
|
795
|
+
|| entry?.authKind !== "oauth"
|
|
796
|
+
|| entry.allowBaseUrlOverride === true
|
|
797
|
+
|| /\{[^}]*\}/.test(entry.baseUrl)
|
|
798
|
+
|| !providerMatchesRegistryTransport(providerName, prov)
|
|
799
|
+
) {
|
|
800
|
+
return prov;
|
|
801
|
+
}
|
|
802
|
+
// Normal routing pins fixed OAuth presets before adapter-specific transport resolution.
|
|
803
|
+
// Discovery must do the same so a stale or modified config baseUrl never receives a token.
|
|
804
|
+
return { ...prov, adapter: entry.adapter, baseUrl: entry.baseUrl };
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Provider-correct model-discovery request (URL + headers), so both model-listing paths fetch the
|
|
809
|
+
* LIVE catalog correctly per adapter. Anthropic is the special case: its endpoint is `/v1/models`
|
|
810
|
+
* (not `/models`), it needs `anthropic-version`, and it authenticates with `x-api-key` by default
|
|
811
|
+
* (or `Authorization: Bearer` when `apiKeyTransport = "bearer"`), plus the OAuth beta for oauth
|
|
812
|
+
* mode — not a bare Bearer. Google (ai-studio mode)
|
|
813
|
+
* is the other special case: `x-goog-api-key` + `/v1beta/models`, returning `{ models: [...] }`.
|
|
814
|
+
* The catalog authority gate intentionally degrades that non-OpenAI shape to stale/static data.
|
|
815
|
+
* Antigravity uses its CCA `:fetchAvailableModels` RPC; everyone else uses the OpenAI-style
|
|
816
|
+
* `/models` + Bearer with a `{ data: [{ id, owned_by? }] }` response.
|
|
817
|
+
*/
|
|
818
|
+
export interface ModelsRequestObservedAuth {
|
|
819
|
+
readonly oauthApiBaseUrl?: string;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
export function buildModelsRequest(
|
|
823
|
+
prov: OcxProviderConfig,
|
|
824
|
+
apiKey: string | undefined,
|
|
825
|
+
providerName = "",
|
|
826
|
+
observedAuth?: ModelsRequestObservedAuth,
|
|
827
|
+
): { method?: "POST"; url: string; headers: Record<string, string> } {
|
|
828
|
+
const transportSeed = modelDiscoveryTransportSeed(providerName, prov);
|
|
829
|
+
const copilotApiBaseUrl = observedAuth === undefined
|
|
830
|
+
? (providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(providerName) : undefined)
|
|
831
|
+
: observedAuth.oauthApiBaseUrl;
|
|
832
|
+
const effectiveProvider = resolveProviderTransport(
|
|
833
|
+
providerName,
|
|
834
|
+
transportSeed,
|
|
835
|
+
undefined,
|
|
836
|
+
copilotApiBaseUrl,
|
|
837
|
+
);
|
|
838
|
+
// Model discovery is an upstream request like any other, so it carries the same registry
|
|
839
|
+
// static headers the inference path does. Without this a provider is identified correctly
|
|
840
|
+
// when it answers a completion but anonymously when it lists its own models, which is the
|
|
841
|
+
// kind of split fingerprint an upstream rate limiter reads as two different clients.
|
|
842
|
+
const registryStaticHeaders = providerMatchesRegistryTransport(providerName, effectiveProvider)
|
|
843
|
+
? getProviderRegistryEntry(providerName)?.staticHeaders
|
|
844
|
+
: undefined;
|
|
845
|
+
const headers: Record<string, string> = {
|
|
846
|
+
...(mergeRegistryStaticHeaders(registryStaticHeaders, effectiveProvider.headers) ?? {}),
|
|
847
|
+
};
|
|
848
|
+
const discoveryUrl = (defaultUrl: string): string => resolveProviderModelDiscoveryUrl(
|
|
849
|
+
providerName,
|
|
850
|
+
prov,
|
|
851
|
+
effectiveProvider.baseUrl,
|
|
852
|
+
defaultUrl,
|
|
853
|
+
);
|
|
854
|
+
if (effectiveGoogleMode(providerName, effectiveProvider) === "cloud-code-assist") {
|
|
855
|
+
headers.Accept = "application/json";
|
|
856
|
+
headers["Content-Type"] = "application/json";
|
|
857
|
+
headers["User-Agent"] = ANTIGRAVITY_REQUEST_UA;
|
|
858
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
859
|
+
return {
|
|
860
|
+
method: "POST",
|
|
861
|
+
url: discoveryUrl(`${effectiveProvider.baseUrl.replace(/\/+$/, "")}/v1internal:fetchAvailableModels`),
|
|
862
|
+
headers,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
if (effectiveGoogleMode(providerName, effectiveProvider) === "ai-studio") {
|
|
866
|
+
// Generative Language API: API key goes in x-goog-api-key (never Authorization: Bearer),
|
|
867
|
+
// models live under /v1beta (v1 misses preview models), and pageSize maxes at 1000 —
|
|
868
|
+
// enough to list everything without a pageToken loop. Vertex/antigravity keep the
|
|
869
|
+
// generic branch (they fall back to their static model lists).
|
|
870
|
+
if (apiKey) headers["x-goog-api-key"] = apiKey;
|
|
871
|
+
return { url: discoveryUrl(`${effectiveProvider.baseUrl}/v1beta/models?pageSize=1000`), headers };
|
|
872
|
+
}
|
|
873
|
+
if (effectiveProvider.adapter === "anthropic") {
|
|
874
|
+
const base = effectiveProvider.baseUrl.replace(/\/v1\/?$/, "");
|
|
875
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
876
|
+
if (effectiveProvider.authMode === "oauth") {
|
|
877
|
+
headers["anthropic-beta"] = ANTHROPIC_OAUTH_BETA;
|
|
878
|
+
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
879
|
+
} else if (apiKey) {
|
|
880
|
+
if (effectiveProvider.apiKeyTransport === "bearer") headers["Authorization"] = `Bearer ${apiKey}`;
|
|
881
|
+
else headers["x-api-key"] = apiKey;
|
|
882
|
+
}
|
|
883
|
+
return { url: discoveryUrl(`${base}/v1/models?limit=1000`), headers };
|
|
884
|
+
}
|
|
885
|
+
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
886
|
+
return { url: discoveryUrl(`${effectiveProvider.baseUrl}/models`), headers };
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Refresh OAuth-managed provider presets (`models`, `noReasoningModels`, and a stale `defaultModel`)
|
|
891
|
+
* from the registry so a proxy update that revises a provider's models — e.g. dropping deprecated
|
|
892
|
+
* Claude snapshots or adding a new grok endpoint not in the live `/models` — reaches EXISTING
|
|
893
|
+
* configs on the next `ocx start`, instead of only fresh installs. The live `/models` fetch stays
|
|
894
|
+
* the primary source; this keeps the static fallback (and models-not-in-/models) current.
|
|
895
|
+
*
|
|
896
|
+
* Only touches providers that are registry-managed AND still `authMode: "oauth"`. Preset fields
|
|
897
|
+
* are refreshed, while the registry's `liveModels` default is normally filled only when no value
|
|
898
|
+
* is stored. Persists + returns true when anything changed.
|
|
899
|
+
*/
|
|
900
|
+
function cloneProviderField(value: unknown): unknown {
|
|
901
|
+
if (Array.isArray(value)) return [...value];
|
|
902
|
+
if (value && typeof value === "object") return JSON.parse(JSON.stringify(value));
|
|
903
|
+
return value;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const OAUTH_RECONCILE_FIELDS: (keyof OcxProviderConfig)[] = [
|
|
907
|
+
"models",
|
|
908
|
+
"contextWindow",
|
|
909
|
+
"modelContextWindows",
|
|
910
|
+
"defaultMaxOutputTokens",
|
|
911
|
+
"modelMaxOutputTokens",
|
|
912
|
+
"modelInputModalities",
|
|
913
|
+
"noReasoningModels",
|
|
914
|
+
"noVisionModels",
|
|
915
|
+
"reasoningEfforts",
|
|
916
|
+
"modelReasoningEfforts",
|
|
917
|
+
"reasoningEffortMap",
|
|
918
|
+
"modelReasoningEffortMap",
|
|
919
|
+
"noTemperatureModels",
|
|
920
|
+
"noTopPModels",
|
|
921
|
+
"noPenaltyModels",
|
|
922
|
+
"autoToolChoiceOnlyModels",
|
|
923
|
+
"preserveReasoningContentModels",
|
|
924
|
+
];
|
|
925
|
+
// `requiresReasoningPlaceholderModels` is deliberately NOT reconciled here: no
|
|
926
|
+
// OAuth preset seeds it, so the delete-when-preset-undefined branch would wipe
|
|
927
|
+
// an explicit user opt-out (`[]`) on every startup. Registry seeds still reach
|
|
928
|
+
// existing rows through enrichProviderFromRegistry, which is fill-only and
|
|
929
|
+
// preserves explicit saved values.
|
|
930
|
+
|
|
931
|
+
const GOOGLE_ANTIGRAVITY_PROVIDER = "google-antigravity";
|
|
932
|
+
const GOOGLE_ANTIGRAVITY_LIVE_DISCOVERY_VERSION = 2 as const;
|
|
933
|
+
|
|
934
|
+
/** Only migrate the three-model experimental seed; an operator's later `liveModels: false` wins. */
|
|
935
|
+
function isLegacyCommandCodeStaticCatalog(provider: OcxProviderConfig): boolean {
|
|
936
|
+
return provider.liveModels === false
|
|
937
|
+
&& provider.defaultModel === "deepseek-v4-flash"
|
|
938
|
+
&& JSON.stringify(provider.models) === JSON.stringify(["deepseek-v4-flash", "kimi-k3", "glm-5.2"]);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function isLegacyAntigravityStaticCatalog(provider: OcxProviderConfig): boolean {
|
|
942
|
+
// A fingerprint of the shape version 1 actually shipped, NOT of the current registry.
|
|
943
|
+
// These literals must stay frozen as the model list moves on: matching them is how we
|
|
944
|
+
// know the row is the untouched v1 seed rather than a user's own selection.
|
|
945
|
+
return provider.liveModels === false
|
|
946
|
+
&& provider.adapter === "google"
|
|
947
|
+
&& provider.baseUrl === "https://daily-cloudcode-pa.googleapis.com"
|
|
948
|
+
&& provider.authMode === "oauth"
|
|
949
|
+
&& provider.googleMode === "cloud-code-assist"
|
|
950
|
+
&& provider.defaultModel === "gemini-3.6-flash"
|
|
951
|
+
&& JSON.stringify(provider.models) === JSON.stringify([
|
|
952
|
+
"gemini-3.6-flash",
|
|
953
|
+
"gemini-3.1-pro",
|
|
954
|
+
"gemini-3.1-flash-image",
|
|
955
|
+
"claude-sonnet-4-6",
|
|
956
|
+
"claude-opus-4-6-thinking",
|
|
957
|
+
"gpt-oss-120b-medium",
|
|
958
|
+
]);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/** Promote only the versioned canonical static seed; unmarked `liveModels: false` remains user intent. */
|
|
962
|
+
function migrateLegacyAntigravityStaticCatalog(config: OcxConfig): boolean {
|
|
963
|
+
if (config.googleAntigravityStaticCatalogVersion !== 1) return false;
|
|
964
|
+
const provider = config.providers[GOOGLE_ANTIGRAVITY_PROVIDER];
|
|
965
|
+
if (provider && isLegacyAntigravityStaticCatalog(provider)) provider.liveModels = true;
|
|
966
|
+
config.googleAntigravityStaticCatalogVersion = GOOGLE_ANTIGRAVITY_LIVE_DISCOVERY_VERSION;
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export function reconcileOAuthProviders(config: OcxConfig): boolean {
|
|
971
|
+
let changed = migrateLegacyAntigravityStaticCatalog(config);
|
|
972
|
+
for (const [name, prov] of Object.entries(config.providers)) {
|
|
973
|
+
const def = OAUTH_PROVIDERS[name];
|
|
974
|
+
if (name === "command-code" && isLegacyCommandCodeStaticCatalog(prov)) {
|
|
975
|
+
// The former experimental preset was the exact three-model seed above. It was not a user
|
|
976
|
+
// choice to disable discovery, so promote only that shape to the account live catalog.
|
|
977
|
+
prov.liveModels = true;
|
|
978
|
+
changed = true;
|
|
979
|
+
}
|
|
980
|
+
if (!def || prov.authMode !== "oauth") continue;
|
|
981
|
+
const preset = def.providerConfig;
|
|
982
|
+
for (const field of OAUTH_RECONCILE_FIELDS) {
|
|
983
|
+
if (JSON.stringify(prov[field]) === JSON.stringify(preset[field])) continue;
|
|
984
|
+
if (preset[field] !== undefined) {
|
|
985
|
+
prov[field] = cloneProviderField(preset[field]) as never;
|
|
986
|
+
} else {
|
|
987
|
+
delete prov[field];
|
|
988
|
+
}
|
|
989
|
+
changed = true;
|
|
990
|
+
}
|
|
991
|
+
if (prov.liveModels === undefined && preset.liveModels !== undefined) {
|
|
992
|
+
prov.liveModels = preset.liveModels;
|
|
993
|
+
changed = true;
|
|
994
|
+
}
|
|
995
|
+
// Heal a defaultModel that no longer exists in the refreshed list (e.g. a deprecated snapshot).
|
|
996
|
+
// Skip providers without a static preset `models` list: for live-discovery providers
|
|
997
|
+
// (e.g. command-code OAuth) the account-scoped catalog is not enumerable here, so any
|
|
998
|
+
// persisted defaultModel is a user selection and must not be overwritten by the seed.
|
|
999
|
+
if (prov.defaultModel && preset.defaultModel && preset.models && preset.models.length > 0 && !(prov.models ?? []).includes(prov.defaultModel)) {
|
|
1000
|
+
prov.defaultModel = preset.defaultModel;
|
|
1001
|
+
changed = true;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
if (changed) saveConfig(config);
|
|
1005
|
+
return changed;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/** Runtime guards: provider config is intentionally passthrough, so persisted fields may be malformed. */
|
|
1009
|
+
function preservableApiKeyPool(value: unknown): NonNullable<OcxProviderConfig["apiKeyPool"]> | undefined {
|
|
1010
|
+
if (!Array.isArray(value)) return undefined;
|
|
1011
|
+
const pool: NonNullable<OcxProviderConfig["apiKeyPool"]> = [];
|
|
1012
|
+
const ids = new Set<string>();
|
|
1013
|
+
const keys = new Set<string>();
|
|
1014
|
+
for (const entry of value as unknown[]) {
|
|
1015
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
1016
|
+
const candidate = entry as Record<string, unknown>;
|
|
1017
|
+
const id = typeof candidate.id === "string" ? candidate.id.trim() : "";
|
|
1018
|
+
const key = sanitizeApiKeyValue(candidate.key);
|
|
1019
|
+
if (!id || !key || ids.has(id) || keys.has(key)) continue;
|
|
1020
|
+
const label = typeof candidate.label === "string" ? candidate.label : undefined;
|
|
1021
|
+
const addedAt = typeof candidate.addedAt === "number" && Number.isFinite(candidate.addedAt)
|
|
1022
|
+
? candidate.addedAt
|
|
1023
|
+
: undefined;
|
|
1024
|
+
ids.add(id);
|
|
1025
|
+
keys.add(key);
|
|
1026
|
+
pool.push({
|
|
1027
|
+
id,
|
|
1028
|
+
key,
|
|
1029
|
+
...(label !== undefined ? { label } : {}),
|
|
1030
|
+
...(addedAt !== undefined ? { addedAt } : {}),
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
// `apiKey` remains the routing source of truth. Keep valid alternate slots even when a
|
|
1034
|
+
// hand-edited config left the pool out of sync, rather than deleting usable credentials.
|
|
1035
|
+
return pool.length > 0 ? pool : undefined;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Add/refresh an OAuth provider's config entry on a config object (does not persist).
|
|
1040
|
+
*
|
|
1041
|
+
* Providers whose registry entry sets `allowKeyAuthOverride` (xai, github-copilot) can be
|
|
1042
|
+
* billed through a stored API key instead of the OAuth login (router.ts honors
|
|
1043
|
+
* `authMode: "key"` for them). A blind preset overwrite here deletes `apiKey`/`apiKeyPool`
|
|
1044
|
+
* on every OAuth login, silently destroying the stored key and forcing a re-paste — and it
|
|
1045
|
+
* flips billing back to the subscription without the user asking. Carry the key fields over
|
|
1046
|
+
* and keep key billing while usable key material remains and the user was not explicitly on
|
|
1047
|
+
* oauth. If the final key was removed and only the old key mode remains, let the OAuth
|
|
1048
|
+
* preset restore `authMode: "oauth"` so the newly saved OAuth credential can be used.
|
|
1049
|
+
*
|
|
1050
|
+
* After preservation, `apiKey` always has exactly one matching pool entry (inserting via the
|
|
1051
|
+
* same content-derived id as the API-key manager when the active key was missing from the
|
|
1052
|
+
* pool). Key mode reflects stored user intent (explicit `"key"` or omitted mode with safe
|
|
1053
|
+
* key material) — never whether the login CLI process can resolve an env reference. Env-backed
|
|
1054
|
+
* availability is decided at proxy routing time in `router.ts`.
|
|
1055
|
+
*/
|
|
1056
|
+
export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
|
|
1057
|
+
if (provider === "chatgpt") return;
|
|
1058
|
+
const def = OAUTH_PROVIDERS[provider];
|
|
1059
|
+
if (!def) return;
|
|
1060
|
+
if (provider === GOOGLE_ANTIGRAVITY_PROVIDER) migrateLegacyAntigravityStaticCatalog(config);
|
|
1061
|
+
const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, provider);
|
|
1062
|
+
if (namespaceCollision) throw new Error(namespaceCollision);
|
|
1063
|
+
const existing = config.providers[provider];
|
|
1064
|
+
const next: OcxProviderConfig = { ...def.providerConfig };
|
|
1065
|
+
// `liveModels` is a user-facing provider toggle. Preserve either explicit setting across login;
|
|
1066
|
+
// Antigravity's CCA discovery now uses its real RPC, so legacy `true` remains a valid choice.
|
|
1067
|
+
if (typeof existing?.liveModels === "boolean" && !isLegacyCommandCodeStaticCatalog(existing)) {
|
|
1068
|
+
next.liveModels = existing.liveModels;
|
|
1069
|
+
}
|
|
1070
|
+
// The Command Code protocol-version pin is an operator compatibility control. A re-login,
|
|
1071
|
+
// add-account, or reauth rebuilds the row from the preset, which has no version; carry the
|
|
1072
|
+
// existing pin so authentication changes do not silently revert the documented control.
|
|
1073
|
+
if (existing?.commandCodeVersion !== undefined) {
|
|
1074
|
+
next.commandCodeVersion = existing.commandCodeVersion;
|
|
1075
|
+
}
|
|
1076
|
+
// User-configured price overlays are operator data, not preset state; a
|
|
1077
|
+
// re-login, add-account, or reauth must not silently drop them from the
|
|
1078
|
+
// Logs/Usage estimates.
|
|
1079
|
+
if (existing?.modelCosts !== undefined) {
|
|
1080
|
+
next.modelCosts = existing.modelCosts;
|
|
1081
|
+
}
|
|
1082
|
+
if (existing && getProviderRegistryEntry(provider)?.allowKeyAuthOverride === true) {
|
|
1083
|
+
// Shared sanitizeApiKeyValue trim / no-CRLF checks from api-key pool writes.
|
|
1084
|
+
let storedApiKey = sanitizeApiKeyValue(existing.apiKey);
|
|
1085
|
+
const storedApiKeyPool = preservableApiKeyPool(existing.apiKeyPool);
|
|
1086
|
+
// Unsafe/blank active key with a usable pool: promote the first safe pool entry so
|
|
1087
|
+
// key billing keeps working instead of falling back to oauth while pool keys remain.
|
|
1088
|
+
if (storedApiKey === undefined && storedApiKeyPool && storedApiKeyPool.length > 0) {
|
|
1089
|
+
storedApiKey = storedApiKeyPool[0]!.key;
|
|
1090
|
+
}
|
|
1091
|
+
if (storedApiKey !== undefined) {
|
|
1092
|
+
const pool = storedApiKeyPool ? [...storedApiKeyPool] : [];
|
|
1093
|
+
// Keep routing and listProviderApiKeys in sync: never leave a hidden active key that
|
|
1094
|
+
// is absent from the pool (listing would fall back to pool[0] as "active").
|
|
1095
|
+
if (!pool.some(entry => entry.key === storedApiKey)) {
|
|
1096
|
+
pool.push({ id: apiKeyPoolEntryId(storedApiKey), key: storedApiKey });
|
|
1097
|
+
}
|
|
1098
|
+
next.apiKey = storedApiKey;
|
|
1099
|
+
next.apiKeyPool = pool;
|
|
1100
|
+
const previousModeAllowsKey = existing.authMode === "key" || existing.authMode === undefined;
|
|
1101
|
+
if (previousModeAllowsKey) next.authMode = "key";
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
config.providers[provider] = next;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
interface RunLoginDeps {
|
|
1108
|
+
saveCredential?: typeof saveCredential;
|
|
1109
|
+
saveAccountCredential?: typeof saveAccountCredential;
|
|
1110
|
+
loadConfig?: typeof loadConfig;
|
|
1111
|
+
saveConfig?: typeof saveConfig;
|
|
1112
|
+
settleKiroLoginTransaction?: typeof settleKiroLoginTransaction;
|
|
1113
|
+
removeAccount?: typeof removeAccount;
|
|
1114
|
+
setActiveAccount?: typeof setActiveAccount;
|
|
1115
|
+
assertCurrentOwner?: () => void;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/** Roll back only accounts created by this forced login, preserving concurrent refreshes of others. */
|
|
1119
|
+
async function rollbackForcedKiroAccountWrite(
|
|
1120
|
+
provider: string,
|
|
1121
|
+
previousActiveId: string | undefined,
|
|
1122
|
+
previousAccountIds: ReadonlySet<string>,
|
|
1123
|
+
deps: Pick<RunLoginDeps, "removeAccount" | "setActiveAccount">,
|
|
1124
|
+
): Promise<void> {
|
|
1125
|
+
const set = getAccountSet(provider);
|
|
1126
|
+
if (!set) return;
|
|
1127
|
+
for (const account of [...set.accounts]) {
|
|
1128
|
+
if (previousAccountIds.has(account.id)) continue;
|
|
1129
|
+
await (deps.removeAccount ?? removeAccount)(provider, account.id);
|
|
1130
|
+
}
|
|
1131
|
+
if (previousActiveId && getAccountCredential(provider, previousActiveId)) {
|
|
1132
|
+
await (deps.setActiveAccount ?? setActiveAccount)(provider, previousActiveId);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/** Run the login flow, persist the credential + upsert the provider entry to disk, return cred. */
|
|
1137
|
+
export async function runLogin(
|
|
1138
|
+
provider: string,
|
|
1139
|
+
ctrl: OAuthController,
|
|
1140
|
+
opts?: LoginOpts,
|
|
1141
|
+
deps: RunLoginDeps = {},
|
|
1142
|
+
): Promise<OAuthCredentials> {
|
|
1143
|
+
const def = OAUTH_PROVIDERS[provider];
|
|
1144
|
+
if (!def) throw new UnsupportedOAuthProviderError(provider);
|
|
1145
|
+
const loadLatestConfig = deps.loadConfig ?? loadConfig;
|
|
1146
|
+
const saveLatestConfig = deps.saveConfig ?? saveConfig;
|
|
1147
|
+
if (provider !== "chatgpt") {
|
|
1148
|
+
const preflightConfig = loadLatestConfig();
|
|
1149
|
+
const namespaceCollision = codexAccountNamespaceProviderCollisionError(
|
|
1150
|
+
preflightConfig.codexAccountNamespaces,
|
|
1151
|
+
provider,
|
|
1152
|
+
);
|
|
1153
|
+
if (namespaceCollision) throw new Error(namespaceCollision);
|
|
1154
|
+
}
|
|
1155
|
+
// loginKiro keys its pending CLI-session transaction by object identity. Keep this exact object
|
|
1156
|
+
// for settlement even when source normalization below creates a derived credential object.
|
|
1157
|
+
const shouldRollbackKiroAccounts = provider === "kiro" && opts?.forceLogin === true;
|
|
1158
|
+
const previousKiroAccounts = shouldRollbackKiroAccounts ? getAccountSet(provider) : undefined;
|
|
1159
|
+
const previousKiroActiveId = previousKiroAccounts?.activeAccountId;
|
|
1160
|
+
const previousKiroAccountIds = new Set(previousKiroAccounts?.accounts.map(account => account.id) ?? []);
|
|
1161
|
+
const rawCred = await def.login(ctrl, opts);
|
|
1162
|
+
const cred: OAuthCredentials = rawCred.source ? rawCred : { ...rawCred, source: "oauth" };
|
|
1163
|
+
const settleKiroTransaction = deps.settleKiroLoginTransaction ?? settleKiroLoginTransaction;
|
|
1164
|
+
try {
|
|
1165
|
+
deps.assertCurrentOwner?.();
|
|
1166
|
+
// Validate the provider row before credential persistence. A namespace claimed during the
|
|
1167
|
+
// credential write is handled again below before the latest row is re-upserted.
|
|
1168
|
+
if (provider !== "chatgpt") {
|
|
1169
|
+
const preCommitConfig = loadLatestConfig();
|
|
1170
|
+
upsertOAuthProvider(preCommitConfig, provider);
|
|
1171
|
+
}
|
|
1172
|
+
if (opts?.reauthAccountId) {
|
|
1173
|
+
const existing = getAccountCredential(provider, opts.reauthAccountId);
|
|
1174
|
+
if (!existing) throw new Error(`Unknown account for reauth: ${opts.reauthAccountId}`);
|
|
1175
|
+
if (!existing.accountId && !existing.email) {
|
|
1176
|
+
throw new OAuthReauthIdentityUnverifiedError();
|
|
1177
|
+
}
|
|
1178
|
+
const identityMatches = existing.accountId && cred.accountId
|
|
1179
|
+
? existing.accountId === cred.accountId
|
|
1180
|
+
: existing.email && cred.email
|
|
1181
|
+
? existing.email.toLowerCase() === cred.email.toLowerCase()
|
|
1182
|
+
: false;
|
|
1183
|
+
if (!identityMatches) {
|
|
1184
|
+
throw new OAuthReauthIdentityMismatchError();
|
|
1185
|
+
}
|
|
1186
|
+
await (deps.saveAccountCredential ?? saveAccountCredential)(provider, opts.reauthAccountId, cred, {
|
|
1187
|
+
assertBeforePersist: deps.assertCurrentOwner,
|
|
1188
|
+
});
|
|
1189
|
+
} else {
|
|
1190
|
+
await (deps.saveCredential ?? saveCredential)(provider, cred, {
|
|
1191
|
+
preserveIdentityless: opts?.forceLogin === true,
|
|
1192
|
+
assertBeforePersist: deps.assertCurrentOwner,
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
if (provider !== "chatgpt") {
|
|
1196
|
+
// Re-run against post-credential state so same-provider API-key additions, removals,
|
|
1197
|
+
// and active-key switches survive. A late namespace claim wins over provider creation.
|
|
1198
|
+
const latestConfig = loadLatestConfig();
|
|
1199
|
+
const lateCollision = codexAccountNamespaceProviderCollisionError(
|
|
1200
|
+
latestConfig.codexAccountNamespaces,
|
|
1201
|
+
provider,
|
|
1202
|
+
);
|
|
1203
|
+
if (lateCollision) {
|
|
1204
|
+
throw new OAuthProviderPublicationError();
|
|
1205
|
+
}
|
|
1206
|
+
upsertOAuthProvider(latestConfig, provider);
|
|
1207
|
+
saveLatestConfig(latestConfig);
|
|
1208
|
+
}
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
const errors: unknown[] = [error];
|
|
1211
|
+
if (shouldRollbackKiroAccounts) {
|
|
1212
|
+
try {
|
|
1213
|
+
await rollbackForcedKiroAccountWrite(provider, previousKiroActiveId, previousKiroAccountIds, deps);
|
|
1214
|
+
} catch (rollbackError) {
|
|
1215
|
+
errors.push(rollbackError);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
try {
|
|
1219
|
+
settleKiroTransaction(rawCred, false);
|
|
1220
|
+
} catch (restoreError) {
|
|
1221
|
+
errors.push(restoreError);
|
|
1222
|
+
}
|
|
1223
|
+
if (errors.length > 1) {
|
|
1224
|
+
throw new AggregateError(
|
|
1225
|
+
errors,
|
|
1226
|
+
"Kiro login persistence failed and the previous Kiro CLI session could not be restored.",
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
throw error;
|
|
1230
|
+
}
|
|
1231
|
+
settleKiroTransaction(rawCred, true);
|
|
1232
|
+
if (provider !== "chatgpt") {
|
|
1233
|
+
try {
|
|
1234
|
+
const { clearModelCache } = await import("../codex/model-cache");
|
|
1235
|
+
const { clearGatherRoutedModelsInflight } = await import("../codex/catalog");
|
|
1236
|
+
clearModelCache(provider);
|
|
1237
|
+
clearGatherRoutedModelsInflight();
|
|
1238
|
+
const { clearAccountQuotaCache, clearProviderQuotaCache } = await import("../providers/quota");
|
|
1239
|
+
clearProviderQuotaCache();
|
|
1240
|
+
clearAccountQuotaCache(provider);
|
|
1241
|
+
} catch {
|
|
1242
|
+
// Optional state modules may be unavailable in tightly scoped unit tests.
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return cred;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* GUI async login: start the flow, return the auth URL EARLY (the flow keeps running in the
|
|
1250
|
+
* background until the callback server captures the redirect), with a concurrency guard and an
|
|
1251
|
+
* error surfaced via getLoginStatus().
|
|
1252
|
+
*
|
|
1253
|
+
* Manual fallback: when the browser cannot reach the loopback callback (remote GUI, SSH, blocked
|
|
1254
|
+
* localhost), the GUI can POST the final redirect URL or authorization code via
|
|
1255
|
+
* submitManualLoginCode(), which feeds OAuthController.onManualCodeInput.
|
|
1256
|
+
*/
|
|
1257
|
+
const loginState = new Map<string, { error?: string; done: boolean }>();
|
|
1258
|
+
const loginAbort = new Map<string, AbortController>();
|
|
1259
|
+
const kiroLoginSettling = new Set<string>();
|
|
1260
|
+
|
|
1261
|
+
/** Pending paste for a login in progress: either a waiter or a stashed early submission. */
|
|
1262
|
+
interface ManualCodeSlot {
|
|
1263
|
+
pendingInput?: string;
|
|
1264
|
+
resolve?: (value: string) => void;
|
|
1265
|
+
/** Registered by the callback flow so submits can validate state synchronously. */
|
|
1266
|
+
expectedState?: string;
|
|
1267
|
+
}
|
|
1268
|
+
const loginManual = new Map<string, ManualCodeSlot>();
|
|
1269
|
+
const OAUTH_PENDING_CODE_MAX_BYTES = 4 * 1024;
|
|
1270
|
+
let lastOAuthFlowReconciledGeneration = 0;
|
|
1271
|
+
|
|
1272
|
+
export function reconcileOAuthFlowState(context: GenerationContext): number {
|
|
1273
|
+
if (context.generation <= lastOAuthFlowReconciledGeneration) return 0;
|
|
1274
|
+
let removed = 0;
|
|
1275
|
+
for (const [provider, state] of loginState) {
|
|
1276
|
+
if (context.providerNames.has(provider) || !state.done || loginAbort.has(provider)) continue;
|
|
1277
|
+
if (loginState.delete(provider)) removed += 1;
|
|
1278
|
+
if (loginManual.delete(provider)) removed += 1;
|
|
1279
|
+
if (loginAbort.delete(provider)) removed += 1;
|
|
1280
|
+
}
|
|
1281
|
+
lastOAuthFlowReconciledGeneration = context.generation;
|
|
1282
|
+
return removed;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function clearManualCodeSlot(provider: string): void {
|
|
1286
|
+
loginManual.delete(provider);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function ensureManualCodeSlot(provider: string): ManualCodeSlot {
|
|
1290
|
+
let slot = loginManual.get(provider);
|
|
1291
|
+
if (!slot) {
|
|
1292
|
+
slot = {};
|
|
1293
|
+
loginManual.set(provider, slot);
|
|
1294
|
+
}
|
|
1295
|
+
return slot;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/** Wait for a GUI/CLI paste of the OAuth redirect URL or code (or return a stashed early submit). */
|
|
1299
|
+
function waitForManualLoginCode(provider: string, signal: AbortSignal, expectedState?: string): Promise<string> {
|
|
1300
|
+
if (signal.aborted) {
|
|
1301
|
+
return Promise.reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
|
|
1302
|
+
}
|
|
1303
|
+
const slot = ensureManualCodeSlot(provider);
|
|
1304
|
+
if (expectedState !== undefined) slot.expectedState = expectedState;
|
|
1305
|
+
if (slot.pendingInput !== undefined) {
|
|
1306
|
+
const value = slot.pendingInput;
|
|
1307
|
+
slot.pendingInput = undefined;
|
|
1308
|
+
return Promise.resolve(value);
|
|
1309
|
+
}
|
|
1310
|
+
return new Promise<string>((resolve, reject) => {
|
|
1311
|
+
const onAbort = () => {
|
|
1312
|
+
if (slot.resolve === resolve) slot.resolve = undefined;
|
|
1313
|
+
reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
|
|
1314
|
+
};
|
|
1315
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1316
|
+
slot.resolve = (value: string) => {
|
|
1317
|
+
signal.removeEventListener("abort", onAbort);
|
|
1318
|
+
if (slot.resolve === resolve) slot.resolve = undefined;
|
|
1319
|
+
resolve(value);
|
|
1320
|
+
};
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* Feed a pasted redirect URL or authorization code into an in-progress GUI login.
|
|
1326
|
+
* Returns ok:false when no login is waiting (or input is empty). Invalid pastes are accepted
|
|
1327
|
+
* here and re-prompted by the OAuth callback loop if they cannot be parsed / fail state checks.
|
|
1328
|
+
*/
|
|
1329
|
+
export function submitManualLoginCode(provider: string, input: string): { ok: true } | { ok: false; error: string } {
|
|
1330
|
+
const trimmed = input.trim();
|
|
1331
|
+
if (!trimmed) return { ok: false, error: "empty code" };
|
|
1332
|
+
if (retainedUtf8Bytes(trimmed) > OAUTH_PENDING_CODE_MAX_BYTES) return { ok: false, error: "code too large" };
|
|
1333
|
+
const st = loginState.get(provider);
|
|
1334
|
+
if (!st || st.done) return { ok: false, error: "no login in progress" };
|
|
1335
|
+
const slot = ensureManualCodeSlot(provider);
|
|
1336
|
+
// Synchronous validation (validated request/ack): reject un-parseable input and
|
|
1337
|
+
// authorization responses (url/query kind) whose state is missing or mismatched
|
|
1338
|
+
// once the flow has registered its expected state. Raw codes stay in-session-PKCE
|
|
1339
|
+
// protected. Early posts (flow not yet waiting, no expectedState) are stashed and
|
|
1340
|
+
// re-validated by the callback loop.
|
|
1341
|
+
const parsed = parseCallbackInput(trimmed);
|
|
1342
|
+
// Command Code's manual fallback accepts a pasted JSON callback payload
|
|
1343
|
+
// (`{ apiKey, state, ... }`) which has no `code` param. Let it through the
|
|
1344
|
+
// shared gate so the provider-specific parser can validate it.
|
|
1345
|
+
const isCommandCodeJson = provider === "command-code" && trimmed.startsWith("{") && !parsed.code;
|
|
1346
|
+
if (!parsed.code && !isCommandCodeJson) return { ok: false, error: "no authorization code found in input" };
|
|
1347
|
+
if (parsed.kind !== "raw" && slot.expectedState !== undefined) {
|
|
1348
|
+
if (parsed.state === undefined) return { ok: false, error: "redirect URL is missing the state parameter" };
|
|
1349
|
+
if (parsed.state !== slot.expectedState) return { ok: false, error: "state mismatch — paste the redirect URL from THIS login attempt" };
|
|
1350
|
+
}
|
|
1351
|
+
if (slot.resolve) {
|
|
1352
|
+
const resolve = slot.resolve;
|
|
1353
|
+
slot.resolve = undefined;
|
|
1354
|
+
resolve(trimmed);
|
|
1355
|
+
} else {
|
|
1356
|
+
// Race: GUI may POST before the flow reaches onManualCodeInput — stash for the waiter.
|
|
1357
|
+
slot.pendingInput = trimmed;
|
|
1358
|
+
}
|
|
1359
|
+
return { ok: true };
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
export interface OAuthAccountSummary { id: string; alias?: string; email?: string; active: boolean; needsReauth?: boolean; expiresAt?: number }
|
|
1363
|
+
|
|
1364
|
+
export function getLoginStatus(provider: string): { loggedIn: boolean; email?: string; source?: OAuthCredentials["source"]; error?: string; done: boolean; activeAccountId?: string; accounts?: OAuthAccountSummary[] } {
|
|
1365
|
+
const cred = getCredential(provider);
|
|
1366
|
+
const st = loginState.get(provider);
|
|
1367
|
+
const set = getAccountSet(provider);
|
|
1368
|
+
const accounts: OAuthAccountSummary[] | undefined = set?.accounts.map(a => ({
|
|
1369
|
+
id: a.id,
|
|
1370
|
+
...(a.alias ? { alias: a.alias } : {}),
|
|
1371
|
+
email: maskEmail(a.credential.email) ?? undefined,
|
|
1372
|
+
active: a.id === set.activeAccountId,
|
|
1373
|
+
...(a.needsReauth ? { needsReauth: true } : {}),
|
|
1374
|
+
expiresAt: a.credential.expires,
|
|
1375
|
+
}));
|
|
1376
|
+
|
|
1377
|
+
// A stored credential counts as "logged in" when it exists and is not marked for
|
|
1378
|
+
// re-authentication. An expired access token with a valid refresh token is still
|
|
1379
|
+
// logged in: request resolution refreshes expired/near-expiry credentials lazily.
|
|
1380
|
+
// Invalid/unknown local-import expiries are handled at parse/adoption time
|
|
1381
|
+
// (local-token-detect.ts), never by over-reporting login state here.
|
|
1382
|
+
const activeNeedsReauth = set?.accounts
|
|
1383
|
+
.find(a => a.id === set.activeAccountId)?.needsReauth === true;
|
|
1384
|
+
return {
|
|
1385
|
+
loggedIn: !!cred && !activeNeedsReauth,
|
|
1386
|
+
email: maskEmail(cred?.email) ?? undefined,
|
|
1387
|
+
source: cred?.source,
|
|
1388
|
+
error: st?.error,
|
|
1389
|
+
done: st?.done ?? false,
|
|
1390
|
+
...(set ? { activeAccountId: set.activeAccountId, accounts } : {}),
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/** Token-safe per-provider login state for the CLI `ocx status` logins section (no tokens, masked email). */
|
|
1395
|
+
export function oauthLoginSummary(): Array<{ provider: string; loggedIn: boolean; email?: string }> {
|
|
1396
|
+
return listOAuthProviders().map(provider => {
|
|
1397
|
+
const status = getLoginStatus(provider);
|
|
1398
|
+
return { provider, loggedIn: status.loggedIn, ...(status.email ? { email: status.email } : {}) };
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
export function clearLoginState(provider: string): void {
|
|
1403
|
+
loginAbort.get(provider)?.abort("cleared");
|
|
1404
|
+
loginAbort.delete(provider);
|
|
1405
|
+
clearManualCodeSlot(provider);
|
|
1406
|
+
loginState.delete(provider);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
export function cancelLoginFlow(provider: string): boolean {
|
|
1410
|
+
const ctrl = loginAbort.get(provider);
|
|
1411
|
+
const existing = loginState.get(provider);
|
|
1412
|
+
if (!ctrl && (!existing || existing.done)) return false;
|
|
1413
|
+
ctrl?.abort("cancelled");
|
|
1414
|
+
loginAbort.delete(provider);
|
|
1415
|
+
clearManualCodeSlot(provider);
|
|
1416
|
+
loginState.set(provider, { done: true, error: "Login cancelled" });
|
|
1417
|
+
return true;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
export async function startLoginFlow(
|
|
1421
|
+
provider: string,
|
|
1422
|
+
opts?: LoginOpts,
|
|
1423
|
+
lifecycle?: LoginFlowLifecycle,
|
|
1424
|
+
): Promise<{ url: string; instructions?: string; deviceCode?: string }> {
|
|
1425
|
+
const def = OAUTH_PROVIDERS[provider];
|
|
1426
|
+
if (!def) throw new UnsupportedOAuthProviderError(provider);
|
|
1427
|
+
const existing = loginState.get(provider);
|
|
1428
|
+
if ((existing && !existing.done) || (provider === "kiro" && kiroLoginSettling.has(provider))) {
|
|
1429
|
+
throw new Error(`A login for ${provider} is already in progress`);
|
|
1430
|
+
}
|
|
1431
|
+
clearManualCodeSlot(provider);
|
|
1432
|
+
loginState.set(provider, { done: false });
|
|
1433
|
+
const abort = new AbortController();
|
|
1434
|
+
loginAbort.set(provider, abort);
|
|
1435
|
+
if (provider === "kiro") kiroLoginSettling.add(provider);
|
|
1436
|
+
return new Promise((resolve, reject) => {
|
|
1437
|
+
let urlResolved = false;
|
|
1438
|
+
const ctrl: OAuthController = {
|
|
1439
|
+
onAuth: ({ url, instructions, deviceCode }) => {
|
|
1440
|
+
urlResolved = true;
|
|
1441
|
+
resolve({ url, instructions, deviceCode });
|
|
1442
|
+
},
|
|
1443
|
+
onProgress: () => {},
|
|
1444
|
+
// GUI fallback when the browser cannot hit the loopback callback server.
|
|
1445
|
+
onManualCodeInput: (expectedState?: string) => waitForManualLoginCode(provider, abort.signal, expectedState),
|
|
1446
|
+
signal: abort.signal,
|
|
1447
|
+
};
|
|
1448
|
+
const abandonIfNotOwner = (error?: unknown): boolean => {
|
|
1449
|
+
if (loginAbort.get(provider) === abort) return false;
|
|
1450
|
+
if (!urlResolved) reject(error ?? new Error("OAuth login was superseded"));
|
|
1451
|
+
return true;
|
|
1452
|
+
};
|
|
1453
|
+
const settle = async (error?: unknown): Promise<void> => {
|
|
1454
|
+
// Cancellation deletes this controller and records its own terminal result. A late provider
|
|
1455
|
+
// rejection (or an older flow settling after a replacement starts) must not overwrite that
|
|
1456
|
+
// state or delete the replacement flow's controller/manual-code slot.
|
|
1457
|
+
if (abandonIfNotOwner(error)) return;
|
|
1458
|
+
let finalError = error;
|
|
1459
|
+
try {
|
|
1460
|
+
await lifecycle?.onSettled?.();
|
|
1461
|
+
} catch (settleError) {
|
|
1462
|
+
// A successful credential/config commit is not fully live until its owner reconciles the
|
|
1463
|
+
// runtime config. For an already-failed login, keep the original recovery error.
|
|
1464
|
+
if (finalError === undefined) finalError = settleError;
|
|
1465
|
+
}
|
|
1466
|
+
if (abandonIfNotOwner(finalError)) return;
|
|
1467
|
+
if (finalError === undefined) {
|
|
1468
|
+
loginAbort.delete(provider);
|
|
1469
|
+
clearManualCodeSlot(provider);
|
|
1470
|
+
loginState.set(provider, { done: true });
|
|
1471
|
+
// Local-token import (grok-cli / Claude Code keychain) completes WITHOUT firing onAuth —
|
|
1472
|
+
// resolve so the GUI call returns instead of hanging.
|
|
1473
|
+
if (!urlResolved) resolve({ url: "", instructions: "Logged in via an existing local CLI/keychain token — no browser needed." });
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
const e = finalError;
|
|
1478
|
+
loginAbort.delete(provider);
|
|
1479
|
+
clearManualCodeSlot(provider);
|
|
1480
|
+
const msg = publicOAuthAuthenticationErrorMessage(e);
|
|
1481
|
+
loginState.set(provider, { done: true, error: msg });
|
|
1482
|
+
if (!urlResolved) reject(e);
|
|
1483
|
+
};
|
|
1484
|
+
// Background: runLogin persists the credential + provider entry to disk. The lifecycle hook
|
|
1485
|
+
// lets a long-lived server config adopt that settled state before clients observe done=true.
|
|
1486
|
+
const assertCurrentOwner = (): void => {
|
|
1487
|
+
if (loginAbort.get(provider) !== abort) throw new OAuthLoginSupersededError();
|
|
1488
|
+
};
|
|
1489
|
+
void runLogin(provider, ctrl, opts, { assertCurrentOwner }).then(
|
|
1490
|
+
() => settle(),
|
|
1491
|
+
(e: unknown) => settle(e),
|
|
1492
|
+
).catch((e: unknown) => {
|
|
1493
|
+
// settle catches lifecycle failures, so this is only a defensive promise-boundary guard.
|
|
1494
|
+
if (abandonIfNotOwner(e)) return;
|
|
1495
|
+
loginAbort.delete(provider);
|
|
1496
|
+
clearManualCodeSlot(provider);
|
|
1497
|
+
const msg = publicOAuthAuthenticationErrorMessage(e);
|
|
1498
|
+
loginState.set(provider, { done: true, error: msg });
|
|
1499
|
+
if (!urlResolved) reject(e);
|
|
1500
|
+
}).finally(() => {
|
|
1501
|
+
if (provider === "kiro") kiroLoginSettling.delete(provider);
|
|
1502
|
+
});
|
|
1503
|
+
});
|
|
1504
|
+
}
|