@iislee/opencodex 2.11.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/LICENSE +21 -0
- package/README.md +225 -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 +451 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-DTpMHS4F.js +67 -0
- package/gui/dist/assets/index-ZNVDE3C7.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/antigravity.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/claude.svg +1 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/copilot.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/cursor.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/gemini.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-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/kiro.svg +14 -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 +1 -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 +102 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic.ts +1003 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +72 -0
- package/src/adapters/client-fingerprint.ts +59 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/cursor-errors.ts +165 -0
- package/src/adapters/cursor/discovery.ts +276 -0
- package/src/adapters/cursor/effort-map.ts +127 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +211 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +153 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1214 -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 +55 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +329 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +548 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +576 -0
- package/src/adapters/cursor/protobuf-events.ts +563 -0
- package/src/adapters/cursor/protobuf-request.ts +714 -0
- package/src/adapters/cursor/request-builder.ts +255 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +505 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +57 -0
- package/src/adapters/cursor/types.ts +52 -0
- package/src/adapters/cursor.ts +196 -0
- package/src/adapters/google-antigravity-replay.ts +303 -0
- package/src/adapters/google-antigravity-wire.ts +108 -0
- package/src/adapters/google-errors.ts +85 -0
- package/src/adapters/google-http.ts +100 -0
- package/src/adapters/google-tool-schema.ts +173 -0
- package/src/adapters/google-truncation.ts +13 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +758 -0
- package/src/adapters/identity.ts +44 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +197 -0
- package/src/adapters/kiro-events.ts +179 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +96 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +215 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1898 -0
- package/src/adapters/mimo-free.ts +263 -0
- package/src/adapters/openai-chat.ts +1005 -0
- package/src/adapters/openai-responses.ts +1137 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/bridge.ts +1619 -0
- package/src/chat/inbound.ts +295 -0
- package/src/chat/outbound.ts +765 -0
- package/src/claude/agents-inject.ts +243 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +189 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +381 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +70 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +509 -0
- package/src/claude/model-info.ts +151 -0
- package/src/claude/outbound.ts +872 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +268 -0
- package/src/cli/account-auth.ts +223 -0
- package/src/cli/account-extended.ts +350 -0
- package/src/cli/account.ts +275 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +184 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-desktop.ts +188 -0
- package/src/cli/claude.ts +286 -0
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/combo.ts +119 -0
- package/src/cli/config-command.ts +145 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/doctor.ts +930 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +354 -0
- package/src/cli/index.ts +1113 -0
- package/src/cli/init.ts +224 -0
- package/src/cli/integrations.ts +142 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/internal-dispatch.ts +20 -0
- package/src/cli/models-runtime.ts +212 -0
- package/src/cli/models.ts +336 -0
- package/src/cli/observe.ts +117 -0
- package/src/cli/opencode.ts +586 -0
- package/src/cli/pi.ts +188 -0
- package/src/cli/provider-runtime.ts +162 -0
- package/src/cli/provider.ts +463 -0
- package/src/cli/runtime-api.ts +325 -0
- package/src/cli/star-prompt.ts +155 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +321 -0
- package/src/cli/sync-cloud.ts +283 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/tray-proxy.ts +52 -0
- package/src/cli/v2.ts +173 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +377 -0
- package/src/clients/effective-status.ts +385 -0
- package/src/clients/probes/agy.ts +55 -0
- package/src/clients/probes/cc-switch.ts +110 -0
- package/src/clients/probes/claude.ts +90 -0
- package/src/clients/probes/codex.ts +125 -0
- package/src/clients/probes/grok.ts +29 -0
- package/src/clients/probes/opencode.ts +109 -0
- package/src/clients/probes/paseo.ts +55 -0
- package/src/clients/probes/pi.ts +55 -0
- package/src/cloud/onedrive-auth.ts +666 -0
- package/src/cloud/onedrive-graph.ts +108 -0
- package/src/cloud/settings.ts +75 -0
- package/src/cloud/sync.ts +212 -0
- package/src/cloud/types.ts +56 -0
- package/src/cloud/vault.ts +89 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +34 -0
- package/src/codex/account-lifecycle.ts +55 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +517 -0
- package/src/codex/account-usability.ts +20 -0
- package/src/codex/app-server-processes.ts +756 -0
- package/src/codex/auth-api.ts +1540 -0
- package/src/codex/auth-collision.ts +107 -0
- package/src/codex/auth-context.ts +352 -0
- package/src/codex/autostart-health.ts +149 -0
- package/src/codex/catalog/aggregation.ts +378 -0
- package/src/codex/catalog/bundled.ts +251 -0
- package/src/codex/catalog/effort.ts +355 -0
- package/src/codex/catalog/metadata.ts +180 -0
- package/src/codex/catalog/parsing.ts +456 -0
- package/src/codex/catalog/provider-fetch.ts +902 -0
- package/src/codex/catalog/sync.ts +620 -0
- package/src/codex/catalog.ts +12 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +969 -0
- package/src/codex/history-migration-guardian.ts +102 -0
- package/src/codex/history-provider.ts +776 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject.ts +799 -0
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +163 -0
- package/src/codex/main-account-cache.ts +32 -0
- package/src/codex/main-account.ts +40 -0
- package/src/codex/model-cache.ts +227 -0
- package/src/codex/paths.ts +65 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +225 -0
- package/src/codex/project-config-warnings.ts +411 -0
- package/src/codex/quota.ts +411 -0
- package/src/codex/refresh.ts +53 -0
- package/src/codex/routing.ts +1477 -0
- package/src/codex/runtime.ts +538 -0
- package/src/codex/shim.ts +1189 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +469 -0
- package/src/codex/sync.ts +130 -0
- package/src/codex/warmup.ts +192 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/combos/failover.ts +140 -0
- package/src/combos/index.ts +41 -0
- package/src/combos/request.ts +62 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +326 -0
- package/src/config.ts +2356 -0
- package/src/generated/jawcode-model-metadata.ts +104 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +545 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +103 -0
- package/src/grok/usage-hook/report.mjs +348 -0
- package/src/grok/usage-hook.ts +278 -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 +829 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +202 -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 +71 -0
- package/src/lib/bun-stream-caps.ts +126 -0
- package/src/lib/config-ownership.ts +360 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +316 -0
- package/src/lib/errors.ts +364 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +165 -0
- package/src/lib/provider-outbound.ts +170 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +105 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +323 -0
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +69 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/upstream-retry.ts +239 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-elevation.ts +705 -0
- package/src/lib/windows-secret-acl.ts +514 -0
- package/src/lib/winsw.ts +375 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +177 -0
- package/src/oauth/callback-server.ts +294 -0
- package/src/oauth/chatgpt.ts +150 -0
- package/src/oauth/cursor.ts +211 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +230 -0
- package/src/oauth/health.ts +399 -0
- package/src/oauth/index.ts +1174 -0
- package/src/oauth/key-providers.ts +108 -0
- package/src/oauth/kimi.ts +213 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +577 -0
- package/src/oauth/local-token-detect.ts +121 -0
- package/src/oauth/log.ts +48 -0
- package/src/oauth/login-cli.ts +163 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +630 -0
- package/src/oauth/token-guardian.ts +303 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/pi/extensions.ts +72 -0
- package/src/pi/home.ts +42 -0
- package/src/pi/index.ts +40 -0
- package/src/pi/models.ts +278 -0
- package/src/pi/packages.ts +219 -0
- package/src/pi/settings.ts +365 -0
- package/src/pi/status.ts +68 -0
- package/src/pi/sync.ts +75 -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 +205 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +64 -0
- package/src/providers/context-cap.ts +65 -0
- package/src/providers/derive.ts +339 -0
- package/src/providers/free-directory.ts +184 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/key-failover.ts +203 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/openai-sidecar.ts +175 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +301 -0
- package/src/providers/openai-virtual-models.ts +82 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/quota.ts +1260 -0
- package/src/providers/registry.ts +1600 -0
- package/src/providers/slug-codec.ts +67 -0
- package/src/providers/xai-transport.ts +141 -0
- package/src/reasoning-effort.ts +135 -0
- package/src/responses/compaction.ts +117 -0
- package/src/responses/parser.ts +656 -0
- package/src/responses/reasoning-envelope.ts +52 -0
- package/src/responses/schema.ts +159 -0
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +895 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +425 -0
- package/src/server/adapter-resolve.ts +80 -0
- package/src/server/auth-cors.ts +530 -0
- package/src/server/chat-completions.ts +368 -0
- package/src/server/claude-messages.ts +914 -0
- package/src/server/effort-policy.ts +172 -0
- package/src/server/gui-static.ts +123 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +476 -0
- package/src/server/index.ts +1126 -0
- package/src/server/lifecycle.ts +227 -0
- package/src/server/live.ts +598 -0
- package/src/server/management/agent-settings-routes.ts +1169 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/clients-routes.ts +63 -0
- package/src/server/management/cloud-sync-routes.ts +266 -0
- package/src/server/management/combo-routes.ts +220 -0
- package/src/server/management/config-routes.ts +422 -0
- package/src/server/management/context.ts +31 -0
- package/src/server/management/logs-usage-routes.ts +707 -0
- package/src/server/management/model-routes.ts +525 -0
- package/src/server/management/oauth-account-routes.ts +563 -0
- package/src/server/management/provider-routes.ts +556 -0
- package/src/server/management/shared.ts +277 -0
- package/src/server/management/sidebar-routes.ts +90 -0
- package/src/server/management/system-restart.ts +179 -0
- package/src/server/management/system-routes.ts +117 -0
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +215 -0
- package/src/server/management-auth.ts +267 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +116 -0
- package/src/server/proxy-liveness.ts +201 -0
- package/src/server/relay-eager.ts +313 -0
- package/src/server/relay.ts +1049 -0
- package/src/server/request-decompress.ts +132 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1046 -0
- package/src/server/responses/collaboration.ts +354 -0
- package/src/server/responses/compact.ts +384 -0
- package/src/server/responses/core.ts +2758 -0
- package/src/server/responses/encrypted-payload.ts +308 -0
- package/src/server/responses/fetch-helpers.ts +157 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/terminal-guard.ts +230 -0
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +224 -0
- package/src/server/responses.ts +9 -0
- package/src/server/search.ts +136 -0
- package/src/server/sse-payload-rewrite.ts +175 -0
- package/src/server/startup-action-control.ts +308 -0
- package/src/server/startup-health-cache.ts +113 -0
- package/src/server/system-env.ts +413 -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 +471 -0
- package/src/service.ts +2554 -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 +290 -0
- package/src/tray/windows.ts +730 -0
- package/src/types.ts +1237 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +407 -0
- package/src/update/job.ts +1520 -0
- package/src/update/notify.ts +257 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +164 -0
- package/src/usage/log.ts +658 -0
- package/src/usage/summary.ts +585 -0
- package/src/usage/totals.ts +14 -0
- package/src/vision/anthropic-describe.ts +185 -0
- package/src/vision/describe.ts +125 -0
- package/src/vision/index.ts +467 -0
- package/src/web-search/anthropic-executor.ts +189 -0
- package/src/web-search/executor.ts +105 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/index.ts +196 -0
- package/src/web-search/loop.ts +664 -0
- package/src/web-search/parse.ts +220 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/synthetic-tool.ts +47 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/** Anthropic OAuth flow (Claude Pro/Max). Ported from jawcode oauth/anthropic.ts. */
|
|
2
|
+
import { OAuthCallbackFlow } from "./callback-server";
|
|
3
|
+
import { generatePKCE } from "./pkce";
|
|
4
|
+
import type { LocalTokenImportMode, OAuthController, OAuthCredentials } from "./types";
|
|
5
|
+
|
|
6
|
+
const CLIENT_ID = atob("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl");
|
|
7
|
+
const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
8
|
+
const TOKEN_URL = "https://api.anthropic.com/v1/oauth/token";
|
|
9
|
+
const CALLBACK_PORT = 54545;
|
|
10
|
+
const CALLBACK_PATH = "/callback";
|
|
11
|
+
const SCOPES = "org:create_api_key user:profile user:inference";
|
|
12
|
+
|
|
13
|
+
// ── OAuth-request requirements applied by the anthropic adapter when authMode==="oauth" ──
|
|
14
|
+
export const ANTHROPIC_OAUTH_BETA = "claude-code-20250219,oauth-2025-04-20";
|
|
15
|
+
export const CLAUDE_CODE_SYSTEM_INSTRUCTION = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
16
|
+
const CLAUDE_TOOL_PREFIX = "custom_";
|
|
17
|
+
const ANTHROPIC_BUILTIN_TOOLS = new Set(["web_search", "code_execution", "text_editor", "computer"]);
|
|
18
|
+
|
|
19
|
+
/** OAuth tokens reject arbitrary tool names; prefix custom tools (Anthropic builtins are exempt). */
|
|
20
|
+
export function applyClaudeToolPrefix(name: string): string {
|
|
21
|
+
if (ANTHROPIC_BUILTIN_TOOLS.has(name.toLowerCase()) || name.toLowerCase().startsWith(CLAUDE_TOOL_PREFIX)) return name;
|
|
22
|
+
return CLAUDE_TOOL_PREFIX + name;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Strip the custom_ prefix from a returned tool_use name so the caller (Codex) sees the original. */
|
|
26
|
+
export function stripClaudeToolPrefix(name: string): string {
|
|
27
|
+
return name.startsWith(CLAUDE_TOOL_PREFIX) ? name.slice(CLAUDE_TOOL_PREFIX.length) : name;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface AnthropicTokenResponse {
|
|
31
|
+
access_token: string;
|
|
32
|
+
refresh_token: string;
|
|
33
|
+
expires_in: number;
|
|
34
|
+
account?: { uuid?: string; email_address?: string };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class AnthropicTokenError extends Error {
|
|
38
|
+
constructor(
|
|
39
|
+
message: string,
|
|
40
|
+
readonly httpStatus: number | undefined,
|
|
41
|
+
readonly oauthError: string | undefined,
|
|
42
|
+
) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "AnthropicTokenError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function postJson(url: string, body: Record<string, string | number>): Promise<string> {
|
|
49
|
+
const response = await fetch(url, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
52
|
+
body: JSON.stringify(body),
|
|
53
|
+
signal: AbortSignal.timeout(30_000),
|
|
54
|
+
});
|
|
55
|
+
const responseBody = await response.text();
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
let oauthError: string | undefined;
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(responseBody) as { error?: unknown };
|
|
60
|
+
if (typeof parsed.error === "string") oauthError = parsed.error;
|
|
61
|
+
} catch {
|
|
62
|
+
// Best-effort only: malformed error bodies remain structured by HTTP status.
|
|
63
|
+
}
|
|
64
|
+
throw new AnthropicTokenError(
|
|
65
|
+
`Anthropic OAuth HTTP ${response.status}: ${responseBody}`,
|
|
66
|
+
response.status,
|
|
67
|
+
oauthError,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return responseBody;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseTokenResponse(responseBody: string): AnthropicTokenResponse {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(responseBody) as AnthropicTokenResponse;
|
|
76
|
+
} catch {
|
|
77
|
+
throw new Error(`Anthropic OAuth returned invalid JSON: ${responseBody.slice(0, 200)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function credsFrom(data: AnthropicTokenResponse, refreshFallback?: string): OAuthCredentials {
|
|
82
|
+
const accountUuid = data.account?.uuid;
|
|
83
|
+
const email = data.account?.email_address;
|
|
84
|
+
return {
|
|
85
|
+
refresh: data.refresh_token || refreshFallback || "",
|
|
86
|
+
access: data.access_token,
|
|
87
|
+
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
|
|
88
|
+
accountId: typeof accountUuid === "string" && accountUuid.length > 0 ? accountUuid : undefined,
|
|
89
|
+
email: typeof email === "string" && email.length > 0 ? email : undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class AnthropicOAuthFlow extends OAuthCallbackFlow {
|
|
94
|
+
#verifier = "";
|
|
95
|
+
|
|
96
|
+
constructor(ctrl: OAuthController) {
|
|
97
|
+
super(ctrl, CALLBACK_PORT, CALLBACK_PATH);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
|
|
101
|
+
const pkce = await generatePKCE();
|
|
102
|
+
this.#verifier = pkce.verifier;
|
|
103
|
+
const authParams = new URLSearchParams({
|
|
104
|
+
code: "true",
|
|
105
|
+
client_id: CLIENT_ID,
|
|
106
|
+
response_type: "code",
|
|
107
|
+
redirect_uri: redirectUri,
|
|
108
|
+
scope: SCOPES,
|
|
109
|
+
code_challenge: pkce.challenge,
|
|
110
|
+
code_challenge_method: "S256",
|
|
111
|
+
state,
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
url: `${AUTHORIZE_URL}?${authParams.toString()}`,
|
|
115
|
+
instructions:
|
|
116
|
+
"Complete Claude login in your browser. If the browser cannot reach this machine, paste the final redirect URL or authorization code when prompted.",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async exchangeToken(code: string, state: string, redirectUri: string): Promise<OAuthCredentials> {
|
|
121
|
+
let exchangeCode = code;
|
|
122
|
+
let exchangeState = state;
|
|
123
|
+
const hash = code.indexOf("#");
|
|
124
|
+
if (hash >= 0) {
|
|
125
|
+
exchangeCode = code.slice(0, hash);
|
|
126
|
+
const frag = code.slice(hash + 1);
|
|
127
|
+
if (frag.length > 0) exchangeState = frag;
|
|
128
|
+
}
|
|
129
|
+
const responseBody = await postJson(TOKEN_URL, {
|
|
130
|
+
grant_type: "authorization_code",
|
|
131
|
+
client_id: CLIENT_ID,
|
|
132
|
+
code: exchangeCode,
|
|
133
|
+
state: exchangeState,
|
|
134
|
+
redirect_uri: redirectUri,
|
|
135
|
+
code_verifier: this.#verifier,
|
|
136
|
+
});
|
|
137
|
+
return credsFrom(parseTokenResponse(responseBody));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function loginAnthropic(
|
|
142
|
+
ctrl: OAuthController,
|
|
143
|
+
opts?: { importLocal?: LocalTokenImportMode },
|
|
144
|
+
): Promise<OAuthCredentials> {
|
|
145
|
+
const importLocal = opts?.importLocal ?? "off";
|
|
146
|
+
if (importLocal !== "off") {
|
|
147
|
+
const { detectClaudeCodeToken } = await import("./local-token-detect");
|
|
148
|
+
const local = detectClaudeCodeToken();
|
|
149
|
+
if (local) {
|
|
150
|
+
ctrl.onProgress?.("Found Claude Code token, importing automatically");
|
|
151
|
+
if (local.expires >= Date.now() + 60_000) return local;
|
|
152
|
+
try {
|
|
153
|
+
return { ...(await refreshAnthropicToken(local.refresh)), source: "local-cli" };
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (importLocal === "only") {
|
|
156
|
+
throw new Error(`Claude Code token expired and could not be refreshed: ${error instanceof Error ? error.message : String(error)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} else if (importLocal === "only") {
|
|
160
|
+
throw new Error(
|
|
161
|
+
process.platform === "darwin"
|
|
162
|
+
? "No Claude Code token found (Keychain or ~/.claude/.credentials.json). Run 'ocx login anthropic' for browser OAuth."
|
|
163
|
+
: "No Claude Code token found (~/.claude/.credentials.json). Run 'ocx login anthropic' for browser OAuth.",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return new AnthropicOAuthFlow(ctrl).login();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function refreshAnthropicToken(refreshToken: string): Promise<OAuthCredentials> {
|
|
171
|
+
const responseBody = await postJson(TOKEN_URL, {
|
|
172
|
+
grant_type: "refresh_token",
|
|
173
|
+
client_id: CLIENT_ID,
|
|
174
|
+
refresh_token: refreshToken,
|
|
175
|
+
});
|
|
176
|
+
return credsFrom(parseTokenResponse(responseBody), refreshToken);
|
|
177
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract base class for OAuth flows with local callback servers.
|
|
3
|
+
* Ported from jawcode packages/ai/src/utils/oauth/callback-server.ts.
|
|
4
|
+
*
|
|
5
|
+
* Change vs source: the success/error page is an inline HTML constant. opencodex's GUI polls
|
|
6
|
+
* GET /api/oauth/status, so it does not need OAuth state injected into the callback page.
|
|
7
|
+
*
|
|
8
|
+
* Handles: port allocation (preferred → random fallback), callback server, CSRF state,
|
|
9
|
+
* manual-input race, 300s timeout. Providers implement generateAuthUrl() + exchangeToken().
|
|
10
|
+
*/
|
|
11
|
+
import { isAddrInUse } from "../server/ports";
|
|
12
|
+
import type { OAuthController, OAuthCredentials } from "./types";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_TIMEOUT = 300_000;
|
|
15
|
+
const DEFAULT_HOSTNAME = "localhost";
|
|
16
|
+
const DEFAULT_BIND_HOSTNAME = "127.0.0.1";
|
|
17
|
+
const CALLBACK_PATH = "/callback";
|
|
18
|
+
|
|
19
|
+
const SUCCESS_HTML =
|
|
20
|
+
"<!doctype html><html><head><meta charset='utf-8'><title>opencodex</title></head>" +
|
|
21
|
+
"<body style='font-family:system-ui,sans-serif;text-align:center;padding:4rem;color:#111'>" +
|
|
22
|
+
"<h2>✅ Login complete</h2><p>You can close this tab and return to opencodex.</p></body></html>";
|
|
23
|
+
|
|
24
|
+
function escapeHtml(s: string): string {
|
|
25
|
+
return s.replace(/[&<>"']/g, (c) =>
|
|
26
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] ?? c,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function errorHtml(message: string): string {
|
|
31
|
+
return (
|
|
32
|
+
"<!doctype html><html><head><meta charset='utf-8'><title>opencodex</title></head>" +
|
|
33
|
+
"<body style='font-family:system-ui,sans-serif;text-align:center;padding:4rem;color:#111'>" +
|
|
34
|
+
`<h2>⚠ Login failed</h2><p>${escapeHtml(message)}</p></body></html>`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type CallbackResult = { code: string; state: string };
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The redirect URI advertised to providers must stay `localhost` (it is what the OAuth
|
|
42
|
+
* apps have registered), but Windows commonly resolves `localhost` to `::1` first while
|
|
43
|
+
* we historically bound IPv4-only — the browser then hits refusal/timeouts/wrong server.
|
|
44
|
+
* When advertising `localhost` over an IPv4 loopback bind, also bind `::1` best-effort.
|
|
45
|
+
*/
|
|
46
|
+
export function loopbackBindHostnames(callbackHostname: string, bindHostname: string): string[] {
|
|
47
|
+
if (callbackHostname.trim().toLowerCase() === "localhost" && bindHostname === "127.0.0.1") {
|
|
48
|
+
return ["127.0.0.1", "::1"];
|
|
49
|
+
}
|
|
50
|
+
return [bindHostname];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface OAuthCallbackFlowOptions {
|
|
54
|
+
preferredPort: number;
|
|
55
|
+
callbackPath?: string;
|
|
56
|
+
callbackHostname?: string;
|
|
57
|
+
/** Local listener hostname; defaults to callbackHostname when omitted. */
|
|
58
|
+
callbackBindHostname?: string;
|
|
59
|
+
/** Exact redirect URI advertised to the provider; disables port fallback. */
|
|
60
|
+
redirectUri?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type BunServer = ReturnType<typeof Bun.serve>;
|
|
64
|
+
|
|
65
|
+
export abstract class OAuthCallbackFlow {
|
|
66
|
+
ctrl: OAuthController;
|
|
67
|
+
preferredPort: number;
|
|
68
|
+
callbackPath: string;
|
|
69
|
+
callbackHostname: string;
|
|
70
|
+
callbackBindHostname: string;
|
|
71
|
+
redirectUri?: string;
|
|
72
|
+
#callbackResolve?: (result: CallbackResult) => void;
|
|
73
|
+
#callbackReject?: (error: string) => void;
|
|
74
|
+
|
|
75
|
+
constructor(
|
|
76
|
+
ctrl: OAuthController,
|
|
77
|
+
preferredPortOrOptions: number | OAuthCallbackFlowOptions,
|
|
78
|
+
callbackPath: string = CALLBACK_PATH,
|
|
79
|
+
) {
|
|
80
|
+
this.ctrl = ctrl;
|
|
81
|
+
if (typeof preferredPortOrOptions === "number") {
|
|
82
|
+
this.preferredPort = preferredPortOrOptions;
|
|
83
|
+
this.callbackPath = callbackPath;
|
|
84
|
+
this.callbackHostname = DEFAULT_HOSTNAME;
|
|
85
|
+
this.callbackBindHostname = DEFAULT_BIND_HOSTNAME;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.preferredPort = preferredPortOrOptions.preferredPort;
|
|
89
|
+
this.callbackPath = preferredPortOrOptions.callbackPath ?? CALLBACK_PATH;
|
|
90
|
+
this.callbackHostname = preferredPortOrOptions.callbackHostname ?? DEFAULT_HOSTNAME;
|
|
91
|
+
this.callbackBindHostname = preferredPortOrOptions.callbackBindHostname ?? DEFAULT_BIND_HOSTNAME;
|
|
92
|
+
this.redirectUri = preferredPortOrOptions.redirectUri;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Build provider-specific authorization URL. */
|
|
96
|
+
abstract generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }>;
|
|
97
|
+
|
|
98
|
+
/** Exchange authorization code for OAuth tokens. */
|
|
99
|
+
abstract exchangeToken(code: string, state: string, redirectUri: string): Promise<OAuthCredentials>;
|
|
100
|
+
|
|
101
|
+
/** Generate CSRF state token. */
|
|
102
|
+
generateState(): string {
|
|
103
|
+
const bytes = new Uint8Array(16);
|
|
104
|
+
crypto.getRandomValues(bytes);
|
|
105
|
+
return Array.from(bytes)
|
|
106
|
+
.map((value) => value.toString(16).padStart(2, "0"))
|
|
107
|
+
.join("");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Execute the OAuth login flow. */
|
|
111
|
+
async login(): Promise<OAuthCredentials> {
|
|
112
|
+
const state = this.generateState();
|
|
113
|
+
const { servers, redirectUri } = await this.#startCallbackServer(state);
|
|
114
|
+
try {
|
|
115
|
+
const { url: authUrl, instructions } = await this.generateAuthUrl(state, redirectUri);
|
|
116
|
+
this.ctrl.onAuth?.({ url: authUrl, instructions });
|
|
117
|
+
this.ctrl.onProgress?.("Waiting for browser authentication...");
|
|
118
|
+
const { code } = await this.#waitForCallback(state);
|
|
119
|
+
this.ctrl.onProgress?.("Exchanging authorization code for tokens...");
|
|
120
|
+
return await this.exchangeToken(code, state, redirectUri);
|
|
121
|
+
} finally {
|
|
122
|
+
for (const server of servers) server.stop();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async #startCallbackServer(expectedState: string): Promise<{ servers: BunServer[]; redirectUri: string }> {
|
|
127
|
+
try {
|
|
128
|
+
const servers = this.#createServers(this.preferredPort, expectedState);
|
|
129
|
+
if (this.redirectUri) {
|
|
130
|
+
return { servers, redirectUri: this.redirectUri };
|
|
131
|
+
}
|
|
132
|
+
const redirectUri = `http://${this.callbackHostname}:${this.preferredPort}${this.callbackPath}`;
|
|
133
|
+
return { servers, redirectUri };
|
|
134
|
+
} catch {
|
|
135
|
+
if (this.redirectUri) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`OAuth callback port ${this.preferredPort} unavailable; cannot fall back to a random port when redirectUri is set`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const servers = this.#createServers(0, expectedState);
|
|
141
|
+
const actualPort = servers[0].port;
|
|
142
|
+
const redirectUri = `http://${this.callbackHostname}:${actualPort}${this.callbackPath}`;
|
|
143
|
+
this.ctrl.onProgress?.(`Preferred port ${this.preferredPort} unavailable, using port ${actualPort}`);
|
|
144
|
+
return { servers, redirectUri };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#createServers(port: number, expectedState: string): BunServer[] {
|
|
149
|
+
const fetch = (req: Request) => this.#handleCallback(req, expectedState);
|
|
150
|
+
const [primaryHost, ...extraHosts] = loopbackBindHostnames(this.callbackHostname, this.callbackBindHostname);
|
|
151
|
+
const primary = Bun.serve({ hostname: primaryHost, port, reusePort: false, fetch });
|
|
152
|
+
const servers = [primary];
|
|
153
|
+
for (const host of extraHosts) {
|
|
154
|
+
try {
|
|
155
|
+
servers.push(Bun.serve({ hostname: host, port: primary.port, reusePort: false, fetch }));
|
|
156
|
+
} catch (err) {
|
|
157
|
+
// extraHosts is only non-empty when we advertise ambiguous `localhost`. A foreign
|
|
158
|
+
// process HOLDING the IPv6 loopback port could then receive the browser's OAuth
|
|
159
|
+
// callback (localhost may resolve to ::1 first) — treat the whole port as unusable
|
|
160
|
+
// so the caller falls back to a fresh one. IPv6 merely unsupported/unavailable
|
|
161
|
+
// (EAFNOSUPPORT etc.) keeps the IPv4-only degradation.
|
|
162
|
+
if (isAddrInUse(err)) {
|
|
163
|
+
for (const server of servers) server.stop(true);
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return servers;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#handleCallback(req: Request, expectedState: string): Response {
|
|
172
|
+
const url = new URL(req.url);
|
|
173
|
+
if (url.pathname !== this.callbackPath) {
|
|
174
|
+
return new Response("Not Found", { status: 404 });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const code = url.searchParams.get("code");
|
|
178
|
+
const state = url.searchParams.get("state") || "";
|
|
179
|
+
const error = url.searchParams.get("error") || "";
|
|
180
|
+
const errorDescription = url.searchParams.get("error_description") || error;
|
|
181
|
+
|
|
182
|
+
let ok = false;
|
|
183
|
+
let consumeFlow = false;
|
|
184
|
+
let errMessage = "";
|
|
185
|
+
const stateMatches = !expectedState || state === expectedState;
|
|
186
|
+
if (error) {
|
|
187
|
+
errMessage = `Authorization failed: ${errorDescription}`;
|
|
188
|
+
consumeFlow = stateMatches;
|
|
189
|
+
} else if (!code) {
|
|
190
|
+
errMessage = "Missing authorization code";
|
|
191
|
+
} else if (!stateMatches) {
|
|
192
|
+
errMessage = "State mismatch - possible CSRF attack";
|
|
193
|
+
} else {
|
|
194
|
+
ok = true;
|
|
195
|
+
consumeFlow = true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (consumeFlow) {
|
|
199
|
+
// Capture refs before they could be cleared, then resolve on the next microtask.
|
|
200
|
+
const resolve = this.#callbackResolve;
|
|
201
|
+
const reject = this.#callbackReject;
|
|
202
|
+
queueMicrotask(() => {
|
|
203
|
+
if (ok && code) {
|
|
204
|
+
resolve?.({ code, state });
|
|
205
|
+
} else {
|
|
206
|
+
reject?.(errMessage || "Unknown error");
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return new Response(ok ? SUCCESS_HTML : errorHtml(errMessage), {
|
|
212
|
+
status: ok ? 200 : consumeFlow ? 500 : 400,
|
|
213
|
+
headers: { "Content-Type": "text/html" },
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#waitForCallback(expectedState: string): Promise<CallbackResult> {
|
|
218
|
+
const timeoutSignal = AbortSignal.timeout(DEFAULT_TIMEOUT);
|
|
219
|
+
const signal = this.ctrl.signal ? AbortSignal.any([this.ctrl.signal, timeoutSignal]) : timeoutSignal;
|
|
220
|
+
|
|
221
|
+
const callbackPromise = new Promise<CallbackResult>((resolve, reject) => {
|
|
222
|
+
this.#callbackResolve = resolve;
|
|
223
|
+
this.#callbackReject = (e: string) => reject(new Error(e));
|
|
224
|
+
|
|
225
|
+
signal.addEventListener("abort", () => {
|
|
226
|
+
this.#callbackResolve = undefined;
|
|
227
|
+
this.#callbackReject = undefined;
|
|
228
|
+
reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (this.ctrl.onManualCodeInput) {
|
|
233
|
+
const requestManualInput = this.ctrl.onManualCodeInput;
|
|
234
|
+
const manualPromise = (async (): Promise<CallbackResult> => {
|
|
235
|
+
while (true) {
|
|
236
|
+
const result = await Promise.race([
|
|
237
|
+
callbackPromise,
|
|
238
|
+
requestManualInput(expectedState)
|
|
239
|
+
.then((input): CallbackResult | null => {
|
|
240
|
+
const parsed = parseCallbackInput(input);
|
|
241
|
+
if (!parsed.code) return null;
|
|
242
|
+
// Kind-aware state enforcement: url/query-shaped input is an authorization
|
|
243
|
+
// RESPONSE and must carry a matching state — missing state is rejected, not
|
|
244
|
+
// downgraded to raw. Only a syntactically raw code (same PKCE session) is
|
|
245
|
+
// exempt, so the CLI/GUI paste fallback still works.
|
|
246
|
+
if (parsed.kind !== "raw" && expectedState && parsed.state !== expectedState) return null;
|
|
247
|
+
return { code: parsed.code, state: parsed.state ?? expectedState };
|
|
248
|
+
})
|
|
249
|
+
.catch((): CallbackResult | null => null),
|
|
250
|
+
]);
|
|
251
|
+
if (result) return result;
|
|
252
|
+
}
|
|
253
|
+
})();
|
|
254
|
+
|
|
255
|
+
return Promise.race([callbackPromise, manualPromise]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return callbackPromise;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Parse a redirect URL or code string to extract code and state.
|
|
264
|
+
* `kind` records the syntactic shape so callers can enforce state on authorization
|
|
265
|
+
* responses (url/query) while exempting raw in-session codes.
|
|
266
|
+
*/
|
|
267
|
+
export function parseCallbackInput(input: string): { kind: "url" | "query" | "raw"; code?: string; state?: string } {
|
|
268
|
+
const value = input.trim();
|
|
269
|
+
if (!value) return { kind: "raw" };
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const url = new URL(value);
|
|
273
|
+
return {
|
|
274
|
+
kind: "url",
|
|
275
|
+
code: url.searchParams.get("code") ?? undefined,
|
|
276
|
+
state: url.searchParams.get("state") ?? undefined,
|
|
277
|
+
};
|
|
278
|
+
} catch {
|
|
279
|
+
// Not a URL - check for query string format
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (value.includes("code=")) {
|
|
283
|
+
const params = new URLSearchParams(value.replace(/^[?#]/, ""));
|
|
284
|
+
return {
|
|
285
|
+
kind: "query",
|
|
286
|
+
code: params.get("code") ?? undefined,
|
|
287
|
+
state: params.get("state") ?? undefined,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Assume raw code, possibly with state after #
|
|
292
|
+
const [code, state] = value.split("#", 2);
|
|
293
|
+
return { kind: "raw", code, state };
|
|
294
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { OAuthCallbackFlow } from "./callback-server";
|
|
2
|
+
import type { OAuthController, OAuthCredentials } from "./types";
|
|
3
|
+
import { generatePKCE } from "./pkce";
|
|
4
|
+
|
|
5
|
+
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
6
|
+
const AUTH_URL = "https://auth.openai.com/oauth/authorize";
|
|
7
|
+
const TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
8
|
+
const SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
|
|
9
|
+
const CALLBACK_PORT = 1455;
|
|
10
|
+
const CALLBACK_PATH = "/auth/callback";
|
|
11
|
+
const ORIGINATOR = "opencodex";
|
|
12
|
+
|
|
13
|
+
export function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
|
|
14
|
+
const parts = token.split(".");
|
|
15
|
+
if (parts.length !== 3 || !parts[1]) return undefined;
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) as Record<string, unknown>;
|
|
18
|
+
} catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function extractAccountId(idToken?: string, accessToken?: string): string | undefined {
|
|
24
|
+
for (const token of [idToken, accessToken]) {
|
|
25
|
+
if (!token) continue;
|
|
26
|
+
const payload = decodeJwtPayload(token);
|
|
27
|
+
if (!payload) continue;
|
|
28
|
+
if (typeof payload.chatgpt_account_id === "string") return payload.chatgpt_account_id;
|
|
29
|
+
const ns = payload["https://api.openai.com/auth"];
|
|
30
|
+
if (ns && typeof ns === "object" && typeof (ns as Record<string, unknown>).chatgpt_account_id === "string") {
|
|
31
|
+
return (ns as Record<string, unknown>).chatgpt_account_id as string;
|
|
32
|
+
}
|
|
33
|
+
const orgs = payload.organizations;
|
|
34
|
+
if (Array.isArray(orgs) && orgs[0] && typeof orgs[0].id === "string") return orgs[0].id as string;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function extractEmail(idToken?: string, accessToken?: string): string | undefined {
|
|
40
|
+
for (const token of [idToken, accessToken]) {
|
|
41
|
+
if (!token) continue;
|
|
42
|
+
const payload = decodeJwtPayload(token);
|
|
43
|
+
if (!payload) continue;
|
|
44
|
+
if (typeof payload.email === "string") return payload.email.toLowerCase();
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function credsFromToken(data: Record<string, unknown>): OAuthCredentials {
|
|
50
|
+
const idToken = typeof data.id_token === "string" ? data.id_token : undefined;
|
|
51
|
+
const accessToken = data.access_token as string;
|
|
52
|
+
return {
|
|
53
|
+
access: accessToken,
|
|
54
|
+
refresh: (data.refresh_token as string) ?? "",
|
|
55
|
+
expires: Date.now() + ((data.expires_in as number) ?? 3600) * 1000,
|
|
56
|
+
accountId: extractAccountId(idToken, accessToken),
|
|
57
|
+
email: extractEmail(idToken, accessToken),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class ChatGPTOAuthFlow extends OAuthCallbackFlow {
|
|
62
|
+
#verifier = "";
|
|
63
|
+
forceLogin = false;
|
|
64
|
+
|
|
65
|
+
constructor(ctrl: OAuthController) {
|
|
66
|
+
super(ctrl, {
|
|
67
|
+
preferredPort: CALLBACK_PORT,
|
|
68
|
+
callbackPath: CALLBACK_PATH,
|
|
69
|
+
callbackHostname: "localhost",
|
|
70
|
+
callbackBindHostname: "127.0.0.1",
|
|
71
|
+
redirectUri: `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
|
|
76
|
+
const pkce = await generatePKCE();
|
|
77
|
+
this.#verifier = pkce.verifier;
|
|
78
|
+
const params = new URLSearchParams({
|
|
79
|
+
response_type: "code",
|
|
80
|
+
client_id: CLIENT_ID,
|
|
81
|
+
redirect_uri: redirectUri,
|
|
82
|
+
scope: SCOPE,
|
|
83
|
+
code_challenge: pkce.challenge,
|
|
84
|
+
code_challenge_method: "S256",
|
|
85
|
+
state,
|
|
86
|
+
codex_cli_simplified_flow: "true",
|
|
87
|
+
originator: ORIGINATOR,
|
|
88
|
+
});
|
|
89
|
+
params.set("id_token_add_organizations", "true");
|
|
90
|
+
if (this.forceLogin) params.set("prompt", "login");
|
|
91
|
+
return {
|
|
92
|
+
url: `${AUTH_URL}?${params}`,
|
|
93
|
+
instructions: "Complete ChatGPT login in your browser.",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {
|
|
98
|
+
if (!this.#verifier) throw new Error("ChatGPT PKCE verifier not initialized");
|
|
99
|
+
const resp = await fetch(TOKEN_URL, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
102
|
+
body: new URLSearchParams({
|
|
103
|
+
grant_type: "authorization_code",
|
|
104
|
+
client_id: CLIENT_ID,
|
|
105
|
+
code,
|
|
106
|
+
redirect_uri: redirectUri,
|
|
107
|
+
code_verifier: this.#verifier,
|
|
108
|
+
}).toString(),
|
|
109
|
+
});
|
|
110
|
+
if (!resp.ok) {
|
|
111
|
+
const errDesc = await safeErrorDescription(resp);
|
|
112
|
+
throw new Error(`ChatGPT token exchange failed: ${resp.status} ${errDesc}`);
|
|
113
|
+
}
|
|
114
|
+
return credsFromToken((await resp.json()) as Record<string, unknown>);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function safeErrorDescription(resp: Response): Promise<string> {
|
|
119
|
+
return resp.text().catch(() => "").then(text => {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(text) as { error?: string; error_description?: string };
|
|
122
|
+
return [parsed.error, parsed.error_description].filter(Boolean).join(": ") || `HTTP ${resp.status}`;
|
|
123
|
+
} catch { return `HTTP ${resp.status}`; }
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function loginChatGPT(ctrl: OAuthController, opts?: { forceLogin?: boolean }): Promise<OAuthCredentials> {
|
|
128
|
+
const flow = new ChatGPTOAuthFlow(ctrl);
|
|
129
|
+
if (opts?.forceLogin) flow.forceLogin = true;
|
|
130
|
+
return flow.login();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Note: uses form-urlencoded per OAuth 2.0 spec (RFC 6749 §6).
|
|
134
|
+
// Codex-rs uses JSON for refresh — intentional divergence; both accepted by auth.openai.com.
|
|
135
|
+
export async function refreshChatGPTToken(refreshToken: string): Promise<OAuthCredentials> {
|
|
136
|
+
const resp = await fetch(TOKEN_URL, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
139
|
+
body: new URLSearchParams({
|
|
140
|
+
grant_type: "refresh_token",
|
|
141
|
+
client_id: CLIENT_ID,
|
|
142
|
+
refresh_token: refreshToken,
|
|
143
|
+
}).toString(),
|
|
144
|
+
});
|
|
145
|
+
if (!resp.ok) {
|
|
146
|
+
const errDesc = await safeErrorDescription(resp);
|
|
147
|
+
throw new Error(`ChatGPT refresh failed: ${resp.status} ${errDesc}`);
|
|
148
|
+
}
|
|
149
|
+
return credsFromToken((await resp.json()) as Record<string, unknown>);
|
|
150
|
+
}
|