@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,666 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Microsoft identity platform — OAuth for OneDrive (public client, no secret).
|
|
3
|
+
*
|
|
4
|
+
* Preferred: authorization-code + PKCE via loopback http://127.0.0.1:<port>/callback
|
|
5
|
+
* Fallback: device-code (requires Azure “Allow public client flows” / mobile platform)
|
|
6
|
+
*
|
|
7
|
+
* Authority:
|
|
8
|
+
* - "consumers" — personal Microsoft accounts only (AADSTS9002346 if you used /common)
|
|
9
|
+
* - "common" — work/school + personal (app must allow both)
|
|
10
|
+
* - "organizations" — work/school only
|
|
11
|
+
*/
|
|
12
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
13
|
+
import { createServer, type Server } from "node:http";
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { getConfigDir } from "../config";
|
|
17
|
+
import { hardenSecretPath } from "../lib/windows-secret-acl";
|
|
18
|
+
import type { MsAuthority, OneDriveTokenStore } from "./types";
|
|
19
|
+
import { readCloudSyncSettings, writeCloudSyncSettings } from "./settings";
|
|
20
|
+
|
|
21
|
+
const SCOPES = ["offline_access", "User.Read", "Files.ReadWrite"].join(" ");
|
|
22
|
+
/** Default fixed loopback port — register this exact URI in Azure (Mobile and desktop). */
|
|
23
|
+
export const DEFAULT_LOOPBACK_PORT = 18765;
|
|
24
|
+
|
|
25
|
+
const VALID_AUTHORITIES = new Set<MsAuthority>(["common", "consumers", "organizations"]);
|
|
26
|
+
|
|
27
|
+
export function normalizeMsAuthority(raw?: string | null): MsAuthority | undefined {
|
|
28
|
+
const v = raw?.trim().toLowerCase();
|
|
29
|
+
if (v === "common" || v === "consumers" || v === "organizations") return v;
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Prefer token store → settings → env → consumers (personal OneDrive default). */
|
|
34
|
+
export function resolveMsAuthority(explicit?: string): MsAuthority {
|
|
35
|
+
const fromFlag = normalizeMsAuthority(explicit);
|
|
36
|
+
if (fromFlag) return fromFlag;
|
|
37
|
+
const fromEnv = normalizeMsAuthority(process.env.OPENCODEX_MS_AUTHORITY);
|
|
38
|
+
if (fromEnv) return fromEnv;
|
|
39
|
+
const token = readOneDriveToken();
|
|
40
|
+
if (token?.authority && VALID_AUTHORITIES.has(token.authority)) return token.authority;
|
|
41
|
+
const fromSettings = normalizeMsAuthority(readCloudSyncSettings().msAuthority);
|
|
42
|
+
if (fromSettings) return fromSettings;
|
|
43
|
+
// Personal MSA apps are common; /consumers avoids AADSTS9002346.
|
|
44
|
+
return "consumers";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function deviceCodeUrl(authority: MsAuthority): string {
|
|
48
|
+
return `https://login.microsoftonline.com/${authority}/oauth2/v2.0/devicecode`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function tokenUrl(authority: MsAuthority): string {
|
|
52
|
+
return `https://login.microsoftonline.com/${authority}/oauth2/v2.0/token`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Fixed loopback port from settings / env / default. */
|
|
56
|
+
export function resolveLoopbackPort(): number {
|
|
57
|
+
const fromEnv = Number(process.env.OPENCODEX_MS_LOOPBACK_PORT || "");
|
|
58
|
+
if (Number.isInteger(fromEnv) && fromEnv >= 1024 && fromEnv <= 65535) return fromEnv;
|
|
59
|
+
const fromSettings = readCloudSyncSettings().loopbackPort;
|
|
60
|
+
if (typeof fromSettings === "number" && Number.isInteger(fromSettings) && fromSettings >= 1024 && fromSettings <= 65535) {
|
|
61
|
+
return fromSettings;
|
|
62
|
+
}
|
|
63
|
+
return DEFAULT_LOOPBACK_PORT;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Exact redirect URI that must be registered on the Azure app (Mobile and desktop). */
|
|
67
|
+
export function azureRedirectUri(port = resolveLoopbackPort()): string {
|
|
68
|
+
return `http://localhost:${port}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function onedriveAuthPath(): string {
|
|
72
|
+
return join(getConfigDir(), "onedrive-auth.json");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function readOneDriveToken(): OneDriveTokenStore | null {
|
|
76
|
+
const path = onedriveAuthPath();
|
|
77
|
+
if (!existsSync(path)) return null;
|
|
78
|
+
try {
|
|
79
|
+
const raw = JSON.parse(readFileSync(path, "utf8")) as OneDriveTokenStore;
|
|
80
|
+
if (!raw.accessToken || !raw.refreshToken || !raw.clientId) return null;
|
|
81
|
+
return raw;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function writeOneDriveToken(store: OneDriveTokenStore): void {
|
|
88
|
+
const path = onedriveAuthPath();
|
|
89
|
+
writeFileSync(path, `${JSON.stringify(store, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
90
|
+
hardenSecretPath(path, { required: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function clearOneDriveToken(): void {
|
|
94
|
+
const path = onedriveAuthPath();
|
|
95
|
+
try { unlinkSync(path); } catch { /* missing ok */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface DeviceCodeStart {
|
|
99
|
+
deviceCode: string;
|
|
100
|
+
userCode: string;
|
|
101
|
+
verificationUri: string;
|
|
102
|
+
verificationUriComplete?: string;
|
|
103
|
+
expiresIn: number;
|
|
104
|
+
interval: number;
|
|
105
|
+
message: string;
|
|
106
|
+
authority: MsAuthority;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isConsumersOnlyError(status: number, json: Record<string, unknown>): boolean {
|
|
110
|
+
if (status !== 400) return false;
|
|
111
|
+
const codes = Array.isArray(json.error_codes) ? json.error_codes : [];
|
|
112
|
+
if (codes.includes(9002346)) return true;
|
|
113
|
+
const desc = String(json.error_description ?? "");
|
|
114
|
+
return desc.includes("AADSTS9002346") || desc.includes("/consumers endpoint");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function requestDeviceCode(clientId: string, authority: MsAuthority): Promise<{
|
|
118
|
+
ok: true; start: DeviceCodeStart;
|
|
119
|
+
} | {
|
|
120
|
+
ok: false; status: number; json: Record<string, unknown>;
|
|
121
|
+
}> {
|
|
122
|
+
const body = new URLSearchParams({
|
|
123
|
+
client_id: clientId,
|
|
124
|
+
scope: SCOPES,
|
|
125
|
+
});
|
|
126
|
+
const res = await fetch(deviceCodeUrl(authority), {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
129
|
+
body,
|
|
130
|
+
signal: AbortSignal.timeout(30_000),
|
|
131
|
+
});
|
|
132
|
+
const json = await res.json().catch(() => ({})) as Record<string, unknown>;
|
|
133
|
+
if (!res.ok) return { ok: false, status: res.status, json };
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
start: {
|
|
137
|
+
deviceCode: String(json.device_code ?? ""),
|
|
138
|
+
userCode: String(json.user_code ?? ""),
|
|
139
|
+
verificationUri: String(json.verification_uri ?? "https://microsoft.com/devicelogin"),
|
|
140
|
+
verificationUriComplete: typeof json.verification_uri_complete === "string" ? json.verification_uri_complete : undefined,
|
|
141
|
+
expiresIn: Number(json.expires_in) || 900,
|
|
142
|
+
interval: Number(json.interval) || 5,
|
|
143
|
+
message: String(json.message ?? "Sign in with the user code shown above."),
|
|
144
|
+
authority,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function rememberAuthority(authority: MsAuthority): void {
|
|
150
|
+
const settings = readCloudSyncSettings();
|
|
151
|
+
if (settings.msAuthority === authority) return;
|
|
152
|
+
writeCloudSyncSettings({ ...settings, msAuthority: authority });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Start device-code login. If the app is personal-MSA-only and authority is
|
|
157
|
+
* common (or unset), automatically retries with /consumers (AADSTS9002346).
|
|
158
|
+
*/
|
|
159
|
+
export async function startDeviceCodeLogin(
|
|
160
|
+
clientId: string,
|
|
161
|
+
authority?: MsAuthority,
|
|
162
|
+
): Promise<DeviceCodeStart> {
|
|
163
|
+
const preferred = authority ?? resolveMsAuthority();
|
|
164
|
+
const first = await requestDeviceCode(clientId, preferred);
|
|
165
|
+
if (first.ok) {
|
|
166
|
+
rememberAuthority(first.start.authority);
|
|
167
|
+
return first.start;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// MSA-only apps must use /consumers, not /common.
|
|
171
|
+
if (preferred === "common" && isConsumersOnlyError(first.status, first.json)) {
|
|
172
|
+
const second = await requestDeviceCode(clientId, "consumers");
|
|
173
|
+
if (second.ok) {
|
|
174
|
+
rememberAuthority("consumers");
|
|
175
|
+
return second.start;
|
|
176
|
+
}
|
|
177
|
+
throw new Error(`device code start failed (${second.status}): ${JSON.stringify(second.json)}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Org-only apps may need /organizations or /common — give a clear hint.
|
|
181
|
+
if (preferred === "consumers") {
|
|
182
|
+
const desc = String(first.json.error_description ?? "");
|
|
183
|
+
if (desc.includes("AADSTS") || first.status === 400) {
|
|
184
|
+
// try common as secondary for multi-tenant apps mis-defaulted to consumers
|
|
185
|
+
const second = await requestDeviceCode(clientId, "common");
|
|
186
|
+
if (second.ok) {
|
|
187
|
+
rememberAuthority("common");
|
|
188
|
+
return second.start;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const hint = formatAzureClientHint(first.status, first.json);
|
|
194
|
+
throw new Error(
|
|
195
|
+
`device code start failed (${first.status}): ${JSON.stringify(first.json)}`
|
|
196
|
+
+ (hint ? `\n\n${hint}` : ""),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function formatAzureClientHint(status: number, json: Record<string, unknown>): string {
|
|
201
|
+
const codes = Array.isArray(json.error_codes) ? json.error_codes : [];
|
|
202
|
+
const desc = String(json.error_description ?? "");
|
|
203
|
+
if (desc.includes("client_secret") || desc.includes("client secret")) {
|
|
204
|
+
return [
|
|
205
|
+
"Azure treats this app as a confidential (Web) client and requires client_secret.",
|
|
206
|
+
"Pick ONE fix:",
|
|
207
|
+
" A) Preferred — make it a public client (no secret):",
|
|
208
|
+
" Authentication → Advanced → Allow public client flows = Yes → Save",
|
|
209
|
+
" Manifest → \"allowPublicClient\": true",
|
|
210
|
+
" Then login again (do not send a secret).",
|
|
211
|
+
" B) Or keep Web app — create a client secret under Certificates & secrets,",
|
|
212
|
+
" paste it in OpenCodex cloud sync (or OPENCODEX_MS_CLIENT_SECRET), then login again.",
|
|
213
|
+
` Redirect URI must still be exactly: ${azureRedirectUri()}`,
|
|
214
|
+
].join("\n");
|
|
215
|
+
}
|
|
216
|
+
if (status === 401 || codes.includes(70002) || desc.includes("AADSTS70002") || desc.includes("marked as 'mobile'")) {
|
|
217
|
+
return [
|
|
218
|
+
"Azure app is not a public/mobile client (AADSTS70002).",
|
|
219
|
+
"Fix in Azure portal → your app:",
|
|
220
|
+
" 1) Authentication → Advanced → Allow public client flows = Yes (save)",
|
|
221
|
+
" 2) Authentication → Add a platform → Mobile and desktop applications",
|
|
222
|
+
` 3) Redirect URI exactly: ${azureRedirectUri()}`,
|
|
223
|
+
" 4) Manifest: \"allowPublicClient\": true",
|
|
224
|
+
"Then wait ~1 minute and try again.",
|
|
225
|
+
].join("\n");
|
|
226
|
+
}
|
|
227
|
+
return "";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Optional confidential-client secret (settings / env). Empty = public client. */
|
|
231
|
+
export function resolveClientSecret(): string | undefined {
|
|
232
|
+
const fromEnv = process.env.OPENCODEX_MS_CLIENT_SECRET?.trim();
|
|
233
|
+
if (fromEnv) return fromEnv;
|
|
234
|
+
const fromSettings = readCloudSyncSettings().clientSecret?.trim();
|
|
235
|
+
return fromSettings || undefined;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ─── Browser loopback + PKCE (preferred) ─────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
interface BrowserLoginSession {
|
|
241
|
+
clientId: string;
|
|
242
|
+
authority: MsAuthority;
|
|
243
|
+
codeVerifier: string;
|
|
244
|
+
state: string;
|
|
245
|
+
redirectUri: string;
|
|
246
|
+
authUrl: string;
|
|
247
|
+
server: Server;
|
|
248
|
+
expiresAt: number;
|
|
249
|
+
result:
|
|
250
|
+
| { status: "pending" }
|
|
251
|
+
| { status: "ok"; store: OneDriveTokenStore; account?: string }
|
|
252
|
+
| { status: "error"; error: string };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let browserLogin: BrowserLoginSession | null = null;
|
|
256
|
+
|
|
257
|
+
function base64Url(buf: Buffer): string {
|
|
258
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function pkcePair(): { verifier: string; challenge: string } {
|
|
262
|
+
const verifier = base64Url(randomBytes(32));
|
|
263
|
+
const challenge = base64Url(createHash("sha256").update(verifier).digest());
|
|
264
|
+
return { verifier, challenge };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function authorizeUrl(opts: {
|
|
268
|
+
clientId: string;
|
|
269
|
+
authority: MsAuthority;
|
|
270
|
+
redirectUri: string;
|
|
271
|
+
state: string;
|
|
272
|
+
codeChallenge: string;
|
|
273
|
+
}): string {
|
|
274
|
+
const u = new URL(`https://login.microsoftonline.com/${opts.authority}/oauth2/v2.0/authorize`);
|
|
275
|
+
u.searchParams.set("client_id", opts.clientId);
|
|
276
|
+
u.searchParams.set("response_type", "code");
|
|
277
|
+
u.searchParams.set("redirect_uri", opts.redirectUri);
|
|
278
|
+
u.searchParams.set("response_mode", "query");
|
|
279
|
+
u.searchParams.set("scope", SCOPES);
|
|
280
|
+
u.searchParams.set("state", opts.state);
|
|
281
|
+
u.searchParams.set("code_challenge", opts.codeChallenge);
|
|
282
|
+
u.searchParams.set("code_challenge_method", "S256");
|
|
283
|
+
// Force account picker when re-auth
|
|
284
|
+
u.searchParams.set("prompt", "select_account");
|
|
285
|
+
return u.toString();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function htmlPage(title: string, body: string): string {
|
|
289
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>${title}</title>
|
|
290
|
+
<style>body{font-family:system-ui,sans-serif;max-width:36rem;margin:3rem auto;padding:0 1rem;line-height:1.5}
|
|
291
|
+
.ok{color:#0a7} .err{color:#c22}</style></head><body>${body}</body></html>`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function cancelBrowserLogin(): void {
|
|
295
|
+
if (!browserLogin) return;
|
|
296
|
+
try { browserLogin.server.close(); } catch { /* ignore */ }
|
|
297
|
+
browserLogin = null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function getBrowserLoginStatus(): BrowserLoginSession["result"] | { status: "idle" } {
|
|
301
|
+
if (!browserLogin) return { status: "idle" };
|
|
302
|
+
if (Date.now() > browserLogin.expiresAt && browserLogin.result.status === "pending") {
|
|
303
|
+
cancelBrowserLogin();
|
|
304
|
+
return { status: "error", error: "browser login timed out" };
|
|
305
|
+
}
|
|
306
|
+
return browserLogin.result;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function startBrowserLoopbackLogin(
|
|
310
|
+
clientId: string,
|
|
311
|
+
authority?: MsAuthority,
|
|
312
|
+
): Promise<{ authUrl: string; redirectUri: string; authority: MsAuthority; expiresIn: number; azureRedirectUri: string }> {
|
|
313
|
+
cancelBrowserLogin();
|
|
314
|
+
const auth = authority ?? resolveMsAuthority();
|
|
315
|
+
const { verifier, challenge } = pkcePair();
|
|
316
|
+
const state = base64Url(randomBytes(16));
|
|
317
|
+
const port = resolveLoopbackPort();
|
|
318
|
+
// Exact match required on many personal-MSA apps — register this in Azure as-is.
|
|
319
|
+
const redirectUri = azureRedirectUri(port);
|
|
320
|
+
|
|
321
|
+
const server = createServer();
|
|
322
|
+
await new Promise<void>((resolve, reject) => {
|
|
323
|
+
server.once("error", (err: NodeJS.ErrnoException) => {
|
|
324
|
+
if (err.code === "EADDRINUSE") {
|
|
325
|
+
reject(new Error(
|
|
326
|
+
`loopback port ${port} is in use. Free it, or set OPENCODEX_MS_LOOPBACK_PORT / cloud-sync loopbackPort `
|
|
327
|
+
+ `and register the matching http://localhost:<port> in Azure.`,
|
|
328
|
+
));
|
|
329
|
+
} else {
|
|
330
|
+
reject(err);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
server.listen(port, "127.0.0.1", () => resolve());
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const authUrl = authorizeUrl({
|
|
337
|
+
clientId,
|
|
338
|
+
authority: auth,
|
|
339
|
+
redirectUri,
|
|
340
|
+
state,
|
|
341
|
+
codeChallenge: challenge,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const session: BrowserLoginSession = {
|
|
345
|
+
clientId,
|
|
346
|
+
authority: auth,
|
|
347
|
+
codeVerifier: verifier,
|
|
348
|
+
state,
|
|
349
|
+
redirectUri,
|
|
350
|
+
authUrl,
|
|
351
|
+
server,
|
|
352
|
+
expiresAt: Date.now() + 10 * 60 * 1000,
|
|
353
|
+
result: { status: "pending" },
|
|
354
|
+
};
|
|
355
|
+
browserLogin = session;
|
|
356
|
+
|
|
357
|
+
server.on("request", (req, res) => {
|
|
358
|
+
void (async () => {
|
|
359
|
+
try {
|
|
360
|
+
const url = new URL(req.url || "/", redirectUri);
|
|
361
|
+
// Root only — redirect_uri has no path (must match Azure registration exactly).
|
|
362
|
+
if (url.pathname !== "/" && url.pathname !== "") {
|
|
363
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
364
|
+
res.end(`Not found — OAuth callback is ${redirectUri}`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const err = url.searchParams.get("error");
|
|
368
|
+
const errDesc = url.searchParams.get("error_description");
|
|
369
|
+
const code = url.searchParams.get("code");
|
|
370
|
+
const st = url.searchParams.get("state");
|
|
371
|
+
if (err) {
|
|
372
|
+
session.result = { status: "error", error: errDesc || err };
|
|
373
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
374
|
+
res.end(htmlPage("OpenCodex", `<p class="err">Login failed: ${escapeHtml(errDesc || err)}</p><p>You can close this tab.</p>`));
|
|
375
|
+
setTimeout(() => { try { server.close(); } catch { /* */ } }, 500);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (!code || st !== session.state) {
|
|
379
|
+
session.result = { status: "error", error: "invalid OAuth callback (state/code)" };
|
|
380
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
381
|
+
res.end(htmlPage("OpenCodex", `<p class="err">Invalid callback.</p>`));
|
|
382
|
+
setTimeout(() => { try { server.close(); } catch { /* */ } }, 500);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
const store = await exchangeAuthCode({
|
|
387
|
+
clientId: session.clientId,
|
|
388
|
+
authority: session.authority,
|
|
389
|
+
code,
|
|
390
|
+
redirectUri: session.redirectUri,
|
|
391
|
+
codeVerifier: session.codeVerifier,
|
|
392
|
+
});
|
|
393
|
+
writeOneDriveToken(store);
|
|
394
|
+
rememberAuthority(session.authority);
|
|
395
|
+
const account = await fetchOneDriveAccountEmail(store.accessToken);
|
|
396
|
+
if (account) {
|
|
397
|
+
store.account = account;
|
|
398
|
+
writeOneDriveToken(store);
|
|
399
|
+
}
|
|
400
|
+
session.result = { status: "ok", store, account };
|
|
401
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
402
|
+
res.end(htmlPage(
|
|
403
|
+
"OpenCodex",
|
|
404
|
+
`<p class="ok">Signed in to OneDrive${account ? ` as <b>${escapeHtml(account)}</b>` : ""}.</p><p>You can close this tab and return to OpenCodex.</p>`,
|
|
405
|
+
));
|
|
406
|
+
} catch (e) {
|
|
407
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
408
|
+
session.result = { status: "error", error: msg };
|
|
409
|
+
res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
|
410
|
+
res.end(htmlPage("OpenCodex", `<p class="err">Token exchange failed: ${escapeHtml(msg)}</p>`));
|
|
411
|
+
}
|
|
412
|
+
setTimeout(() => { try { server.close(); } catch { /* */ } }, 500);
|
|
413
|
+
} catch (e) {
|
|
414
|
+
session.result = { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
415
|
+
try {
|
|
416
|
+
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
417
|
+
res.end("error");
|
|
418
|
+
} catch { /* */ }
|
|
419
|
+
}
|
|
420
|
+
})();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
authUrl,
|
|
425
|
+
redirectUri,
|
|
426
|
+
azureRedirectUri: redirectUri,
|
|
427
|
+
authority: auth,
|
|
428
|
+
expiresIn: 600,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function escapeHtml(s: string): string {
|
|
433
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function exchangeAuthCode(opts: {
|
|
437
|
+
clientId: string;
|
|
438
|
+
authority: MsAuthority;
|
|
439
|
+
code: string;
|
|
440
|
+
redirectUri: string;
|
|
441
|
+
codeVerifier: string;
|
|
442
|
+
}): Promise<OneDriveTokenStore> {
|
|
443
|
+
const body = new URLSearchParams({
|
|
444
|
+
grant_type: "authorization_code",
|
|
445
|
+
client_id: opts.clientId,
|
|
446
|
+
code: opts.code,
|
|
447
|
+
redirect_uri: opts.redirectUri,
|
|
448
|
+
code_verifier: opts.codeVerifier,
|
|
449
|
+
scope: SCOPES,
|
|
450
|
+
});
|
|
451
|
+
// Confidential (Web) apps require client_secret; public clients must omit it.
|
|
452
|
+
const secret = resolveClientSecret();
|
|
453
|
+
if (secret) body.set("client_secret", secret);
|
|
454
|
+
|
|
455
|
+
const res = await fetch(tokenUrl(opts.authority), {
|
|
456
|
+
method: "POST",
|
|
457
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
458
|
+
body,
|
|
459
|
+
signal: AbortSignal.timeout(30_000),
|
|
460
|
+
});
|
|
461
|
+
const json = await res.json().catch(() => ({})) as Record<string, unknown>;
|
|
462
|
+
if (!res.ok || !json.access_token) {
|
|
463
|
+
const hint = formatAzureClientHint(res.status, json);
|
|
464
|
+
throw new Error(
|
|
465
|
+
`token exchange failed (${res.status}): ${JSON.stringify(json)}`
|
|
466
|
+
+ (hint ? `\n\n${hint}` : ""),
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
const store: OneDriveTokenStore = {
|
|
470
|
+
accessToken: String(json.access_token),
|
|
471
|
+
refreshToken: String(json.refresh_token ?? ""),
|
|
472
|
+
expiresAt: Date.now() + (Number(json.expires_in) || 3600) * 1000,
|
|
473
|
+
scope: typeof json.scope === "string" ? json.scope : undefined,
|
|
474
|
+
clientId: opts.clientId,
|
|
475
|
+
authority: opts.authority,
|
|
476
|
+
};
|
|
477
|
+
if (!store.refreshToken) {
|
|
478
|
+
throw new Error("token response missing refresh_token — ensure offline_access scope on the Azure app");
|
|
479
|
+
}
|
|
480
|
+
return store;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Blocking browser login for CLI: open URL, wait until loopback callback finishes.
|
|
485
|
+
*/
|
|
486
|
+
export async function loginWithBrowserLoopback(
|
|
487
|
+
clientId: string,
|
|
488
|
+
authority?: MsAuthority,
|
|
489
|
+
onReady?: (info: { authUrl: string; redirectUri: string }) => void,
|
|
490
|
+
): Promise<OneDriveTokenStore> {
|
|
491
|
+
const start = await startBrowserLoopbackLogin(clientId, authority);
|
|
492
|
+
onReady?.({ authUrl: start.authUrl, redirectUri: start.redirectUri });
|
|
493
|
+
const deadline = Date.now() + start.expiresIn * 1000;
|
|
494
|
+
while (Date.now() < deadline) {
|
|
495
|
+
const st = getBrowserLoginStatus();
|
|
496
|
+
if (st.status === "ok") {
|
|
497
|
+
cancelBrowserLogin();
|
|
498
|
+
return st.store;
|
|
499
|
+
}
|
|
500
|
+
if (st.status === "error") {
|
|
501
|
+
cancelBrowserLogin();
|
|
502
|
+
throw new Error(st.error);
|
|
503
|
+
}
|
|
504
|
+
await Bun.sleep(400);
|
|
505
|
+
}
|
|
506
|
+
cancelBrowserLogin();
|
|
507
|
+
throw new Error("browser login timed out");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export async function pollDeviceCodeToken(
|
|
511
|
+
clientId: string,
|
|
512
|
+
deviceCode: string,
|
|
513
|
+
intervalSec: number,
|
|
514
|
+
expiresInSec: number,
|
|
515
|
+
onProgress?: (msg: string) => void,
|
|
516
|
+
authority?: MsAuthority,
|
|
517
|
+
): Promise<OneDriveTokenStore> {
|
|
518
|
+
const auth = authority ?? resolveMsAuthority();
|
|
519
|
+
const deadline = Date.now() + expiresInSec * 1000;
|
|
520
|
+
let interval = Math.max(3, intervalSec) * 1000;
|
|
521
|
+
while (Date.now() < deadline) {
|
|
522
|
+
await Bun.sleep(interval);
|
|
523
|
+
const body = new URLSearchParams({
|
|
524
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
525
|
+
client_id: clientId,
|
|
526
|
+
device_code: deviceCode,
|
|
527
|
+
});
|
|
528
|
+
const res = await fetch(tokenUrl(auth), {
|
|
529
|
+
method: "POST",
|
|
530
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
531
|
+
body,
|
|
532
|
+
signal: AbortSignal.timeout(30_000),
|
|
533
|
+
});
|
|
534
|
+
const json = await res.json().catch(() => ({})) as Record<string, unknown>;
|
|
535
|
+
if (res.ok && json.access_token) {
|
|
536
|
+
const store: OneDriveTokenStore = {
|
|
537
|
+
accessToken: String(json.access_token),
|
|
538
|
+
refreshToken: String(json.refresh_token ?? ""),
|
|
539
|
+
expiresAt: Date.now() + (Number(json.expires_in) || 3600) * 1000,
|
|
540
|
+
scope: typeof json.scope === "string" ? json.scope : undefined,
|
|
541
|
+
clientId,
|
|
542
|
+
authority: auth,
|
|
543
|
+
};
|
|
544
|
+
if (!store.refreshToken) throw new Error("token response missing refresh_token — ensure offline_access scope");
|
|
545
|
+
writeOneDriveToken(store);
|
|
546
|
+
rememberAuthority(auth);
|
|
547
|
+
return store;
|
|
548
|
+
}
|
|
549
|
+
const err = String(json.error ?? "");
|
|
550
|
+
if (err === "authorization_pending") {
|
|
551
|
+
onProgress?.("waiting for browser authorization…");
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (err === "slow_down") {
|
|
555
|
+
interval += 2000;
|
|
556
|
+
onProgress?.("slowing poll rate…");
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (err === "expired_token") throw new Error("device code expired — run login again");
|
|
560
|
+
if (err === "authorization_declined") throw new Error("authorization declined in browser");
|
|
561
|
+
throw new Error(`token poll failed: ${err || res.status} ${JSON.stringify(json)}`);
|
|
562
|
+
}
|
|
563
|
+
throw new Error("device code timed out — run login again");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** One-shot token poll for GUI (no sleep loop). */
|
|
567
|
+
export async function pollDeviceCodeTokenOnce(
|
|
568
|
+
clientId: string,
|
|
569
|
+
deviceCode: string,
|
|
570
|
+
authority: MsAuthority,
|
|
571
|
+
): Promise<
|
|
572
|
+
| { status: "ok"; store: OneDriveTokenStore }
|
|
573
|
+
| { status: "pending"; error: string }
|
|
574
|
+
| { status: "error"; error: string }
|
|
575
|
+
> {
|
|
576
|
+
const body = new URLSearchParams({
|
|
577
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
578
|
+
client_id: clientId,
|
|
579
|
+
device_code: deviceCode,
|
|
580
|
+
});
|
|
581
|
+
const res = await fetch(tokenUrl(authority), {
|
|
582
|
+
method: "POST",
|
|
583
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
584
|
+
body,
|
|
585
|
+
signal: AbortSignal.timeout(30_000),
|
|
586
|
+
});
|
|
587
|
+
const json = await res.json().catch(() => ({})) as Record<string, unknown>;
|
|
588
|
+
if (res.ok && json.access_token) {
|
|
589
|
+
const store: OneDriveTokenStore = {
|
|
590
|
+
accessToken: String(json.access_token),
|
|
591
|
+
refreshToken: String(json.refresh_token ?? ""),
|
|
592
|
+
expiresAt: Date.now() + (Number(json.expires_in) || 3600) * 1000,
|
|
593
|
+
scope: typeof json.scope === "string" ? json.scope : undefined,
|
|
594
|
+
clientId,
|
|
595
|
+
authority,
|
|
596
|
+
};
|
|
597
|
+
if (!store.refreshToken) {
|
|
598
|
+
return { status: "error", error: "missing refresh_token — ensure offline_access" };
|
|
599
|
+
}
|
|
600
|
+
writeOneDriveToken(store);
|
|
601
|
+
rememberAuthority(authority);
|
|
602
|
+
return { status: "ok", store };
|
|
603
|
+
}
|
|
604
|
+
const err = String(json.error ?? "");
|
|
605
|
+
if (err === "authorization_pending" || err === "slow_down") {
|
|
606
|
+
return { status: "pending", error: err };
|
|
607
|
+
}
|
|
608
|
+
return { status: "error", error: err || `token poll failed (${res.status})` };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export async function refreshOneDriveToken(store: OneDriveTokenStore): Promise<OneDriveTokenStore> {
|
|
612
|
+
const auth = store.authority ?? resolveMsAuthority();
|
|
613
|
+
const body = new URLSearchParams({
|
|
614
|
+
grant_type: "refresh_token",
|
|
615
|
+
client_id: store.clientId,
|
|
616
|
+
refresh_token: store.refreshToken,
|
|
617
|
+
scope: SCOPES,
|
|
618
|
+
});
|
|
619
|
+
const secret = resolveClientSecret();
|
|
620
|
+
if (secret) body.set("client_secret", secret);
|
|
621
|
+
const res = await fetch(tokenUrl(auth), {
|
|
622
|
+
method: "POST",
|
|
623
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
624
|
+
body,
|
|
625
|
+
signal: AbortSignal.timeout(30_000),
|
|
626
|
+
});
|
|
627
|
+
const json = await res.json().catch(() => ({})) as Record<string, unknown>;
|
|
628
|
+
if (!res.ok || !json.access_token) {
|
|
629
|
+
throw new Error(`OneDrive token refresh failed (${res.status}): ${JSON.stringify(json)}`);
|
|
630
|
+
}
|
|
631
|
+
const next: OneDriveTokenStore = {
|
|
632
|
+
accessToken: String(json.access_token),
|
|
633
|
+
refreshToken: String(json.refresh_token ?? store.refreshToken),
|
|
634
|
+
expiresAt: Date.now() + (Number(json.expires_in) || 3600) * 1000,
|
|
635
|
+
scope: typeof json.scope === "string" ? json.scope : store.scope,
|
|
636
|
+
account: store.account,
|
|
637
|
+
clientId: store.clientId,
|
|
638
|
+
authority: auth,
|
|
639
|
+
};
|
|
640
|
+
writeOneDriveToken(next);
|
|
641
|
+
return next;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Return a valid access token, refreshing if within 2 minutes of expiry. */
|
|
645
|
+
export async function getValidOneDriveAccessToken(): Promise<{ token: string; store: OneDriveTokenStore }> {
|
|
646
|
+
let store = readOneDriveToken();
|
|
647
|
+
if (!store) throw new Error("not logged in to OneDrive — run: ocx sync-cloud login");
|
|
648
|
+
if (store.expiresAt <= Date.now() + 120_000) {
|
|
649
|
+
store = await refreshOneDriveToken(store);
|
|
650
|
+
}
|
|
651
|
+
return { token: store.accessToken, store };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export async function fetchOneDriveAccountEmail(accessToken: string): Promise<string | undefined> {
|
|
655
|
+
try {
|
|
656
|
+
const res = await fetch("https://graph.microsoft.com/v1.0/me?$select=displayName,userPrincipalName,mail", {
|
|
657
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
658
|
+
signal: AbortSignal.timeout(15_000),
|
|
659
|
+
});
|
|
660
|
+
if (!res.ok) return undefined;
|
|
661
|
+
const json = await res.json() as Record<string, unknown>;
|
|
662
|
+
return String(json.mail || json.userPrincipalName || json.displayName || "") || undefined;
|
|
663
|
+
} catch {
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
666
|
+
}
|