@bitkyc08/opencodex 2.84.0 → 2.87.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/README.md +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DCd-A1az.js} +1 -1
- package/gui/dist/assets/index-CfYDMj34.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +91 -11
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +16 -3
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic.ts +108 -21
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +40 -11
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +37 -10
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +44 -3
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +223 -0
- package/src/advisor/context.ts +238 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +353 -0
- package/src/advisor/settings.ts +144 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +24 -1
- package/src/bridge/internal.ts +3 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +58 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +72 -7
- package/src/claude/desktop-3p.ts +75 -19
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +11 -0
- package/src/claude/inbound.ts +154 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +51 -19
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +165 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +23 -2
- package/src/cli/capabilities-base.ts +39 -11
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +17 -14
- package/src/cli/dispatch.ts +30 -5
- package/src/cli/doctor.ts +15 -1
- package/src/cli/ensure-desktop-cold-start.ts +382 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +4 -2
- package/src/cli/index.ts +14 -5
- package/src/cli/integration-plan-dto.ts +3 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +41 -9
- package/src/cli/restart-scope.ts +3 -1
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +3 -1
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +35 -48
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +81 -7
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +19 -2
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +42 -19
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +35 -86
- package/src/codex/catalog/provider-models.ts +40 -34
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +33 -16
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +63 -4
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +25 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +73 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +182 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +165 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/features.ts +85 -45
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/native-profile-startup.ts +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +2 -2
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +35 -7
- package/src/config/live-reconcile.ts +51 -2
- package/src/config/load-degrade.ts +23 -1
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +52 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/paths.ts +10 -3
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +39 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +39 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +962 -350
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +46 -8
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +2 -0
- package/src/integrations/registry.ts +15 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +72 -0
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +26 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/redact.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +62 -12
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +188 -111
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/encoders/adapter-events.ts +5 -4
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/provider-summary.ts +2 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +5 -0
- package/src/providers/model-rename-migration.ts +7 -5
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +44 -14
- package/src/providers/registry/entries-extended.ts +19 -0
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +78 -22
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +24 -2
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/responses/state.ts +32 -29
- package/src/router.ts +8 -321
- package/src/routing/health.ts +37 -22
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +254 -221
- package/src/routing/history/ingest.ts +242 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +21 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +15 -0
- package/src/server/chat-completions.ts +82 -10
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +164 -433
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +11 -3
- package/src/server/index/startup-warnings.ts +11 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +14 -11
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +202 -0
- package/src/server/management/agent-settings-routes.ts +22 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +77 -6
- package/src/server/management/context.ts +4 -0
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +13 -8
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +21 -1
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +9 -1
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +106 -22
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +13 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +48 -19
- package/src/server/responses/adapter-delivery.ts +43 -8
- package/src/server/responses/adapter-dispatch.ts +48 -4
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +368 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +18 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +12 -1
- package/src/server/responses/core-combo.ts +141 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +7 -2
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +51 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +25 -6
- package/src/server/responses/core.ts +3 -116
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +19 -11
- package/src/server/responses/passthrough-dispatch.ts +29 -16
- package/src/server/responses/request-prepare.ts +144 -17
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +120 -27
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +166 -38
- package/src/server/responses/sidecar-execution.ts +98 -5
- package/src/server/responses/terminal-guard.ts +4 -17
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -0
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/desktop-startup.ts +14 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +99 -61
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +135 -0
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +124 -6
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +29 -0
- package/src/types/provider.ts +39 -6
- package/src/types/request.ts +38 -2
- package/src/types/tools.ts +2 -0
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/npm-invocation.mjs +12 -2
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +15 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +22 -9
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/gemini-executor.ts +2 -2
- package/src/web-search/loop.ts +55 -5
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -1
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AnthropicTokenError } from "./anthropic";
|
|
2
|
+
import { OAuthProviderNamespaceCollisionError, OAuthProviderPublicationError, publicOAuthAuthenticationErrorMessage } from "./index";
|
|
3
|
+
import { XaiTokenRequestError } from "./xai";
|
|
4
|
+
import { AnthropicInstanceCollisionError, AnthropicInstanceConfigReadError } from "./store-anthropic-instance";
|
|
5
|
+
|
|
6
|
+
// Standard RFC 6749 / RFC 6750 / RFC 8628 error codes. A provider's `error` field is printed only
|
|
7
|
+
// when it is one of these, so a body that echoes request parameters there still prints nothing.
|
|
8
|
+
const STANDARD_OAUTH_ERROR_CODES = new Set([
|
|
9
|
+
"invalid_request", "invalid_client", "invalid_grant", "unauthorized_client",
|
|
10
|
+
"unsupported_grant_type", "invalid_scope", "access_denied", "unsupported_response_type",
|
|
11
|
+
"server_error", "temporarily_unavailable", "invalid_token", "insufficient_scope",
|
|
12
|
+
"authorization_pending", "slow_down", "expired_token",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
/** Fixed, machine-readable reason for a failed login; never derived from free-form text. */
|
|
16
|
+
export function oauthLoginFailureDiagnostic(error: unknown): string {
|
|
17
|
+
if (error instanceof OAuthProviderNamespaceCollisionError || error instanceof OAuthProviderPublicationError
|
|
18
|
+
|| error instanceof AnthropicInstanceCollisionError || error instanceof AnthropicInstanceConfigReadError) {
|
|
19
|
+
return "configuration_error";
|
|
20
|
+
}
|
|
21
|
+
const status = error instanceof AnthropicTokenError ? error.httpStatus
|
|
22
|
+
: error instanceof XaiTokenRequestError ? error.status : undefined;
|
|
23
|
+
const oauthError = error instanceof AnthropicTokenError || error instanceof XaiTokenRequestError
|
|
24
|
+
? error.oauthError : undefined;
|
|
25
|
+
const parts: string[] = [];
|
|
26
|
+
if (typeof status === "number" && Number.isInteger(status) && status >= 400 && status <= 599) parts.push(`HTTP ${status}`);
|
|
27
|
+
if (typeof oauthError === "string" && STANDARD_OAUTH_ERROR_CODES.has(oauthError)) parts.push(oauthError);
|
|
28
|
+
if (parts.length > 0) return parts.join(" ");
|
|
29
|
+
const name = error instanceof Error ? error.name : "";
|
|
30
|
+
if (name === "TimeoutError" || name === "AbortError") return "timeout";
|
|
31
|
+
// fetch() reports refused connections and DNS failures as a TypeError carrying a string code; a
|
|
32
|
+
// programming TypeError carries none and stays in the generic bucket.
|
|
33
|
+
const code = error instanceof TypeError ? (error as { code?: unknown }).code : undefined;
|
|
34
|
+
if ((error instanceof AnthropicTokenError && error.requestNotSent)
|
|
35
|
+
|| (error instanceof XaiTokenRequestError && error.status === undefined)
|
|
36
|
+
|| (typeof code === "string" && code !== "")) return "network_error";
|
|
37
|
+
return "oauth_authentication_failed";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Keep terminal login failures within the public OAuth error vocabulary. */
|
|
41
|
+
export function reportOAuthLoginError(error: unknown): never {
|
|
42
|
+
console.error(`\n❌ OAuth login failed (${oauthLoginFailureDiagnostic(error)}): ${publicOAuthAuthenticationErrorMessage(error)}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
package/src/oauth/login-cli.ts
CHANGED
|
@@ -9,12 +9,14 @@ import {
|
|
|
9
9
|
requestBoundLocalProviderReload,
|
|
10
10
|
type LocalProviderReloadResult,
|
|
11
11
|
} from "../server/local-provider-reload-client";
|
|
12
|
-
import { DEPRECATED_OAUTH_PROVIDER_ALIASES, isPublicOAuthProvider, listOAuthProviders, runLogin } from "./index";
|
|
12
|
+
import { DEPRECATED_OAUTH_PROVIDER_ALIASES, isPublicOAuthProvider, listOAuthProviders, publicOAuthAuthenticationErrorMessage, runLogin, supportsCodeLoginMode } from "./index";
|
|
13
|
+
import { classifyLoginCodeError } from "./login-flow-state";
|
|
13
14
|
import { KEY_LOGIN_PROVIDERS, isKeyLoginProvider, validateApiKey, type KeyLoginProvider } from "./key-providers";
|
|
14
15
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
15
16
|
import { configuredAdminToken } from "../lib/admin-secrets";
|
|
16
17
|
import { codexAccountNamespaceProviderCollisionError } from "../codex/account-namespace-match";
|
|
17
18
|
import { assertAnthropicInstanceLoginConfig } from "./store-anthropic-instance";
|
|
19
|
+
import { reportOAuthLoginError } from "./login-cli-errors";
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Seams a test drives in place of a browser, a terminal and a real provider. Production passes
|
|
@@ -113,37 +115,97 @@ export function warnIfLiveReloadSkipped(result: LocalProviderReloadResult | null
|
|
|
113
115
|
* that used to be their only pointer to it.
|
|
114
116
|
*/
|
|
115
117
|
export function loginUsageMessage(): string {
|
|
116
|
-
return `Usage: ocx login <provider
|
|
118
|
+
return `Usage: ocx login <provider> [--headless]\n`
|
|
119
|
+
+ ` --headless: no browser and no local port — approve in any browser, paste the code\n`
|
|
120
|
+
+ ` (${listCodeLoginProviders().join(", ")})\n`
|
|
117
121
|
+ ` Codex / ChatGPT: ocx login codex (account pool, needs a running proxy; 'chatgpt' and\n`
|
|
118
122
|
+ ` 'openai' are the same route. An OpenAI platform key is 'openai-apikey'.)\n`
|
|
119
123
|
+ ` OAuth login: ${listOAuthProviders().join(", ")}\n`
|
|
120
124
|
+ ` API-key login: ${Object.keys(KEY_LOGIN_PROVIDERS).join(", ")}`;
|
|
121
125
|
}
|
|
122
126
|
|
|
123
|
-
export
|
|
127
|
+
export interface LoginCliOptions {
|
|
128
|
+
/** `--headless`: no callback server, no browser launch — print the URL and read back the code. */
|
|
129
|
+
codeMode?: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function handleLogin(provider?: string, deps: LoginCliDeps = {}, opts: LoginCliOptions = {}): Promise<void> {
|
|
124
133
|
const name = (provider ?? "").trim().toLowerCase();
|
|
134
|
+
try {
|
|
135
|
+
// Check raw ownership before tolerant loading can print diagnostics or fall back to defaults.
|
|
136
|
+
if (name === "anthropic2") assertAnthropicInstanceLoginConfig({ providers: {} }, name);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
reportOAuthLoginError(error);
|
|
139
|
+
}
|
|
125
140
|
// A removed provider id reached through its alias still logs in — the merged
|
|
126
141
|
// successor owns the flow. Warn rather than silently reroute so scripts and
|
|
127
142
|
// docs that still say `devin-cli` surface the rename to whoever runs them.
|
|
128
143
|
const alias = DEPRECATED_OAUTH_PROVIDER_ALIASES[name];
|
|
129
144
|
if (alias) {
|
|
130
145
|
console.error(`${name} is deprecated; logging in as ${alias}`);
|
|
131
|
-
return handleOAuthLogin(alias, deps);
|
|
146
|
+
return handleOAuthLogin(alias, deps, opts);
|
|
147
|
+
}
|
|
148
|
+
if (isPublicOAuthProvider(name)) return handleOAuthLogin(name, deps, opts);
|
|
149
|
+
if (opts.codeMode) {
|
|
150
|
+
console.error(`--headless needs a provider with a code-display login: ${listCodeLoginProviders().join(", ")}`);
|
|
151
|
+
process.exit(1);
|
|
132
152
|
}
|
|
133
|
-
if (isPublicOAuthProvider(name)) return handleOAuthLogin(name, deps);
|
|
134
153
|
if (isKeyLoginProvider(name)) return handleKeyLogin(name, deps);
|
|
135
154
|
console.error(loginUsageMessage());
|
|
136
155
|
process.exit(1);
|
|
137
156
|
}
|
|
138
157
|
|
|
139
|
-
|
|
158
|
+
/** OAuth providers that accept `ocx login <provider> --headless`. */
|
|
159
|
+
export function listCodeLoginProviders(): string[] {
|
|
160
|
+
return listOAuthProviders().filter(supportsCodeLoginMode);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* `--headless`: the provider displays the authorization code instead of redirecting anywhere, so
|
|
165
|
+
* nothing is launched and nothing listens. Used where the browser is on another machine.
|
|
166
|
+
*/
|
|
167
|
+
async function handleCodeLogin(name: string, deps: LoginCliDeps): Promise<void> {
|
|
168
|
+
const login = deps.runLogin ?? runLogin;
|
|
169
|
+
if (!supportsCodeLoginMode(name)) {
|
|
170
|
+
console.error(`${name} has no code-display login. Providers that do: ${listCodeLoginProviders().join(", ")}`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
await withPrompt(deps.ask, async (ask) => {
|
|
175
|
+
await login(name, {
|
|
176
|
+
onAuth: ({ url, instructions }) => {
|
|
177
|
+
console.log(`\n🔐 Open this URL in any browser to approve ${name}:\n${url}\n`);
|
|
178
|
+
if (instructions) console.log(instructions);
|
|
179
|
+
},
|
|
180
|
+
onProgress: (m) => console.log(` ${m}`),
|
|
181
|
+
onManualCodeInput: async () => await ask("Paste the code shown after approval: "),
|
|
182
|
+
}, { codeMode: true });
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
// Never let the raw error reach stderr: a token endpoint's error body can quote the pasted
|
|
186
|
+
// code back. Only fixed, allowlisted text and the machine-readable reason are printed.
|
|
187
|
+
console.error(`\n❌ ${name} login failed (${classifyLoginCodeError(error)}): ${publicOAuthAuthenticationErrorMessage(error)}`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
const reload = await notifyRunningProxyAfterOAuthLogin(name);
|
|
191
|
+
console.log(`\n✅ Logged in to ${name}. Try: ocx sync`);
|
|
192
|
+
for (const line of modelSelectionGuidance(name)) console.log(line);
|
|
193
|
+
warnIfLiveReloadSkipped(reload);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function handleOAuthLogin(name: string, deps: LoginCliDeps = {}, opts: LoginCliOptions = {}): Promise<void> {
|
|
140
197
|
if (name === "anthropic2") assertAnthropicInstanceLoginConfig(loadConfig(), name);
|
|
198
|
+
if (opts.codeMode) return handleCodeLogin(name, deps);
|
|
141
199
|
const login = deps.runLogin ?? runLogin;
|
|
142
200
|
const launch = deps.openUrl ?? openUrl;
|
|
143
201
|
const browser = createBrowserLaunchReport(deps.warn);
|
|
144
202
|
await withPrompt(deps.ask, async (ask) => {
|
|
145
203
|
await login(name, {
|
|
146
204
|
onAuth: ({ url, instructions }) => {
|
|
205
|
+
if (!url) {
|
|
206
|
+
if (instructions) console.log(instructions);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
147
209
|
console.log(`\n🔐 Opening browser for ${name} login...\n${url}\n`);
|
|
148
210
|
if (instructions) console.log(instructions);
|
|
149
211
|
// The controller does not await onAuth, so the launcher's answer cannot be reported from
|
|
@@ -159,7 +221,7 @@ export async function handleOAuthLogin(name: string, deps: LoginCliDeps = {}): P
|
|
|
159
221
|
return await ask("Paste redirect URL or code (or wait for browser): ");
|
|
160
222
|
},
|
|
161
223
|
});
|
|
162
|
-
});
|
|
224
|
+
}).catch(reportOAuthLoginError);
|
|
163
225
|
// A device or polling provider never prompts, so nothing above waited on the launcher. It is
|
|
164
226
|
// still owed an answer before this claims the login worked.
|
|
165
227
|
await browser.settled();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AnthropicTokenError } from "./anthropic";
|
|
1
2
|
import { parseCallbackInput } from "./callback-server";
|
|
2
3
|
import { retainedUtf8Bytes } from "../lib/admission";
|
|
3
4
|
import type { GenerationContext } from "../lib/state-store-sweeper";
|
|
@@ -9,6 +10,12 @@ import type { GenerationContext } from "../lib/state-store-sweeper";
|
|
|
9
10
|
* lines. The cut follows a seam rather than a line count: everything here is bookkeeping for
|
|
10
11
|
* a login that has started and not yet settled, and none of it reads or writes a stored
|
|
11
12
|
* credential. `index.ts` re-exports the two public names, so existing importers are unaffected.
|
|
13
|
+
*
|
|
14
|
+
* Every map here is keyed by FLOW ID, not by provider. A code-display flow binds no socket, so
|
|
15
|
+
* a hosted login host can have several in flight for one provider at once; each must have its
|
|
16
|
+
* own paste slot and its own abort scope, or one user's paste settles another's login. The
|
|
17
|
+
* provider-only public APIs (`getLoginStatus`, `submitManualLoginCode`, `cancelLoginFlow`)
|
|
18
|
+
* resolve through `latestLoginFlowKey`, which is the one place that rule is written down.
|
|
12
19
|
*/
|
|
13
20
|
/** Human-facing continuation only: never retain credentials or provider response objects. */
|
|
14
21
|
export interface OAuthLoginHint {
|
|
@@ -16,16 +23,301 @@ export interface OAuthLoginHint {
|
|
|
16
23
|
instructions?: string;
|
|
17
24
|
deviceCode?: string;
|
|
18
25
|
}
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Machine-readable outcome of a code-display login, so a caller that only ever sees one HTTP
|
|
29
|
+
* response can tell a bad paste from a dead flow from an unreachable provider. Never carries
|
|
30
|
+
* any part of the code, verifier, state or token.
|
|
31
|
+
*/
|
|
32
|
+
export type LoginCodeError =
|
|
33
|
+
| "state_mismatch"
|
|
34
|
+
| "invalid_or_expired_code"
|
|
35
|
+
| "no_pending_login"
|
|
36
|
+
| "provider_unreachable"
|
|
37
|
+
| "malformed_input"
|
|
38
|
+
| "code_mode_unsupported";
|
|
39
|
+
|
|
40
|
+
export interface LoginFlowState {
|
|
41
|
+
error?: string;
|
|
42
|
+
done: boolean;
|
|
43
|
+
hint?: OAuthLoginHint;
|
|
44
|
+
/** Absent means the default localhost-callback flow; "code" is the code-display flow. */
|
|
45
|
+
mode?: "callback" | "code";
|
|
46
|
+
/**
|
|
47
|
+
* Which provider this flow logs into, since the key no longer says. Absent on a row written
|
|
48
|
+
* straight into this map by a caller that predates flow-id keying, where the key IS the
|
|
49
|
+
* provider name — `loginFlowProvider` resolves both.
|
|
50
|
+
*/
|
|
51
|
+
provider?: string;
|
|
52
|
+
flowId?: string;
|
|
53
|
+
/** Epoch ms after which a paste is refused as `no_pending_login`. */
|
|
54
|
+
expiresAt?: number;
|
|
55
|
+
errorCode?: LoginCodeError;
|
|
56
|
+
/** Account row this flow persisted, once it settled successfully. An id, never a credential. */
|
|
57
|
+
accountId?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Keyed by flow id. */
|
|
61
|
+
export const loginState = new Map<string, LoginFlowState>();
|
|
62
|
+
/** Keyed by flow id; `provider` is what a provider-scoped caller is checked against. */
|
|
63
|
+
export const loginAbort = new Map<string, { controller: AbortController; provider?: string }>();
|
|
21
64
|
export const kiroLoginSettling = new Set<string>();
|
|
22
65
|
|
|
66
|
+
/** The provider a flow belongs to, or undefined when no such flow is recorded. */
|
|
67
|
+
export function loginFlowProvider(flowKey: string): string | undefined {
|
|
68
|
+
const state = loginState.get(flowKey);
|
|
69
|
+
return state === undefined ? undefined : state.provider ?? flowKey;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Flow ids recorded for a provider, oldest first (Map iteration is insertion order). */
|
|
73
|
+
export function loginFlowKeys(provider: string): string[] {
|
|
74
|
+
// ponytail: linear scan over in-flight logins (bounded by MAX_LOGIN_FLOWS_PER_PROVIDER);
|
|
75
|
+
// add a provider -> keys index if that bound ever grows.
|
|
76
|
+
const keys: string[] = [];
|
|
77
|
+
// `(state.provider ?? key)` must be parenthesised: `===` binds tighter than `??`, so the
|
|
78
|
+
// unparenthesised form reads as `state.provider ?? (key === provider)` and returns the truthy
|
|
79
|
+
// provider string for every flow -- matching every provider against every other.
|
|
80
|
+
for (const [key, state] of loginState) if ((state.provider ?? key) === provider) keys.push(key);
|
|
81
|
+
return keys;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The flow that a provider-only API acts on: the NEWEST flow still in progress, or — when none
|
|
86
|
+
* is — the newest flow overall, so a login that just settled still reports its outcome. With a
|
|
87
|
+
* single flow (every callback login, and every GUI/CLI login) this is that flow, unchanged.
|
|
88
|
+
*/
|
|
89
|
+
export function latestLoginFlowKey(provider: string): string | undefined {
|
|
90
|
+
const keys = loginFlowKeys(provider);
|
|
91
|
+
return keys.findLast(key => loginState.get(key)?.done === false) ?? keys.at(-1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Rows kept per provider. Concurrent code flows mean an authenticated caller can start logins
|
|
96
|
+
* nobody ever finishes, each holding a PKCE verifier, an AbortController and an open paste
|
|
97
|
+
* promise. Expired flows are reaped first; this is the backstop that bounds the rest.
|
|
98
|
+
*/
|
|
99
|
+
const MAX_LOGIN_FLOWS_PER_PROVIDER = 64;
|
|
100
|
+
|
|
101
|
+
/** Abort a flow, drop its paste slot, and record a terminal outcome for anything awaiting it. */
|
|
102
|
+
export function abandonLoginFlow(flowKey: string, outcome: { error: string; errorCode: LoginCodeError }): void {
|
|
103
|
+
loginAbort.get(flowKey)?.controller.abort("cancelled");
|
|
104
|
+
loginAbort.delete(flowKey);
|
|
105
|
+
clearManualCodeSlot(flowKey);
|
|
106
|
+
settleLoginFlow(flowKey, outcome);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Refuse a login that cannot safely share this process with what is already in flight, after
|
|
111
|
+
* reaping dead rows. A code-display flow binds nothing, so several can run at once for one
|
|
112
|
+
* provider — that is what lets a hosted login host serve more than one user. The callback flow
|
|
113
|
+
* binds the fixed loopback port and genuinely cannot run twice, so any overlap involving one is
|
|
114
|
+
* refused, as is anything past the per-provider row cap.
|
|
115
|
+
*/
|
|
116
|
+
export function admitLoginFlow(provider: string, codeMode: boolean): void {
|
|
117
|
+
reapLoginFlows(provider);
|
|
118
|
+
const inFlight = loginFlowKeys(provider)
|
|
119
|
+
.map(key => loginState.get(key))
|
|
120
|
+
.filter(state => state?.done === false);
|
|
121
|
+
const overlapAllowed = codeMode && inFlight.every(state => state?.mode === "code");
|
|
122
|
+
if ((inFlight.length > 0 && !overlapAllowed) || (provider === "kiro" && kiroLoginSettling.has(provider))) {
|
|
123
|
+
throw new Error(`A login for ${provider} is already in progress`);
|
|
124
|
+
}
|
|
125
|
+
if (loginFlowKeys(provider).length >= MAX_LOGIN_FLOWS_PER_PROVIDER) {
|
|
126
|
+
throw new Error(`Too many logins in progress for ${provider}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Drop a provider's dead rows: code flows past their paste window (they can never complete),
|
|
132
|
+
* then the oldest settled rows once the cap is reached. Called when a flow starts, which is
|
|
133
|
+
* the only moment the count can grow. Never evicts a flow still in progress.
|
|
134
|
+
*/
|
|
135
|
+
function reapLoginFlows(provider: string): void {
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
for (const key of loginFlowKeys(provider)) {
|
|
138
|
+
const state = loginState.get(key);
|
|
139
|
+
if (state && !state.done && state.expiresAt !== undefined && now > state.expiresAt) {
|
|
140
|
+
abandonLoginFlow(key, { error: "Login expired", errorCode: "no_pending_login" });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const keys = loginFlowKeys(provider);
|
|
144
|
+
// +1 leaves room for the flow about to be recorded.
|
|
145
|
+
const excess = keys.length - MAX_LOGIN_FLOWS_PER_PROVIDER + 1;
|
|
146
|
+
if (excess <= 0) return;
|
|
147
|
+
for (const key of keys.filter(key => loginState.get(key)?.done === true).slice(0, excess)) {
|
|
148
|
+
clearManualCodeSlot(key);
|
|
149
|
+
dropLoginFlow(key);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A code-display flow the user never completes must stop accepting pastes. */
|
|
154
|
+
export const CODE_LOGIN_TTL_MS = 10 * 60_000;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Record a just-admitted flow under its id and return its paste deadline (code mode only).
|
|
158
|
+
* Lives here rather than in `startLoginFlow` to keep `src/oauth/index.ts` under the size ratchet.
|
|
159
|
+
*/
|
|
160
|
+
export function recordStartedLoginFlow(provider: string, flowId: string, codeMode: boolean): number | undefined {
|
|
161
|
+
clearManualCodeSlot(flowId);
|
|
162
|
+
const expiresAt = codeMode ? Date.now() + CODE_LOGIN_TTL_MS : undefined;
|
|
163
|
+
loginState.set(flowId, { done: false, provider, flowId, ...(codeMode ? { mode: "code" as const, expiresAt } : {}) });
|
|
164
|
+
return expiresAt;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export class CodeLoginUnsupportedError extends Error {
|
|
168
|
+
constructor(provider: string) {
|
|
169
|
+
super(`${provider} has no code-display login`);
|
|
170
|
+
this.name = "CodeLoginUnsupportedError";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Map a login failure onto the code a code-mode caller receives. The error itself is never
|
|
176
|
+
* forwarded: a provider body can quote the submitted code back, and this response is the one
|
|
177
|
+
* surface a hosted caller renders to a user.
|
|
178
|
+
*
|
|
179
|
+
* `AnthropicTokenError` is named because anthropic is the only provider with a code-display
|
|
180
|
+
* redirect today; every other failure falls through the generic branches.
|
|
181
|
+
*/
|
|
182
|
+
export function classifyLoginCodeError(error: unknown): LoginCodeError {
|
|
183
|
+
const message = error instanceof Error ? error.message : "";
|
|
184
|
+
if (message.startsWith("OAuth callback cancelled")) return "no_pending_login";
|
|
185
|
+
if (error instanceof AnthropicTokenError) {
|
|
186
|
+
return error.httpStatus !== undefined && error.httpStatus >= 500
|
|
187
|
+
? "provider_unreachable"
|
|
188
|
+
: "invalid_or_expired_code";
|
|
189
|
+
}
|
|
190
|
+
const name = error instanceof Error ? error.name : "";
|
|
191
|
+
if (name === "TimeoutError" || name === "AbortError") return "provider_unreachable";
|
|
192
|
+
// fetch() reports a dead network as a TypeError, but so does any bug in this code. Bun tags
|
|
193
|
+
// the network kind with a string `code` (`ConnectionRefused`, `ENOTFOUND`, ...) and Node puts
|
|
194
|
+
// one on `cause`; a plain programming TypeError carries neither, and must not be reported to
|
|
195
|
+
// the user as "Anthropic is unreachable".
|
|
196
|
+
if (error instanceof TypeError) {
|
|
197
|
+
const code = (error as { code?: unknown }).code ?? (error as { cause?: { code?: unknown } }).cause?.code;
|
|
198
|
+
if (typeof code === "string" && code !== "") return "provider_unreachable";
|
|
199
|
+
}
|
|
200
|
+
return "invalid_or_expired_code";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Awaiting a terminal outcome for one flow (code mode answers its POST from this). */
|
|
204
|
+
const loginSettleWaiters = new Map<string, Set<() => void>>();
|
|
205
|
+
|
|
206
|
+
function wakeLoginWaiters(flowKey: string): void {
|
|
207
|
+
const waiters = loginSettleWaiters.get(flowKey);
|
|
208
|
+
if (!waiters) return;
|
|
209
|
+
loginSettleWaiters.delete(flowKey);
|
|
210
|
+
for (const wake of waiters) wake();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Record a login's terminal outcome and wake anything awaiting it. Flow identity
|
|
215
|
+
* (mode/flowId/expiresAt) is preserved so a settled flow still reports what it was. The hint is
|
|
216
|
+
* dropped: `getLoginStatus` already hides it once done, and a settled flow has no next step.
|
|
217
|
+
*/
|
|
218
|
+
export function settleLoginFlow(
|
|
219
|
+
flowKey: string,
|
|
220
|
+
outcome: { error?: string; errorCode?: LoginCodeError; accountId?: string } = {},
|
|
221
|
+
): void {
|
|
222
|
+
const current = loginState.get(flowKey);
|
|
223
|
+
// A flow already dropped (logout, reap) has nothing left to report; releasing its waiters is
|
|
224
|
+
// still owed, and fabricating a row here would be a flow belonging to no provider.
|
|
225
|
+
if (current) {
|
|
226
|
+
loginState.set(flowKey, {
|
|
227
|
+
...current,
|
|
228
|
+
done: true,
|
|
229
|
+
hint: undefined,
|
|
230
|
+
error: outcome.error,
|
|
231
|
+
errorCode: outcome.errorCode,
|
|
232
|
+
accountId: outcome.accountId,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
wakeLoginWaiters(flowKey);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** True when the named flow (or, without one, the provider's newest flow) is this provider's code flow. */
|
|
239
|
+
export function isCodeLoginFlow(provider: string, flowId?: string): boolean {
|
|
240
|
+
const flowKey = flowId ?? latestLoginFlowKey(provider);
|
|
241
|
+
return flowKey !== undefined && loginFlowProvider(flowKey) === provider && loginState.get(flowKey)?.mode === "code";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The account a settled, successful flow persisted; undefined for any other flow or provider. */
|
|
245
|
+
export function loginFlowAccountId(provider: string, flowId: string): string | undefined {
|
|
246
|
+
const state = loginState.get(flowId);
|
|
247
|
+
if (!state?.done || state.error || loginFlowProvider(flowId) !== provider) return undefined;
|
|
248
|
+
return state.accountId;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Drop one flow's state entirely; waiters are released rather than left hanging. */
|
|
252
|
+
export function dropLoginFlow(flowKey: string): void {
|
|
253
|
+
loginState.delete(flowKey);
|
|
254
|
+
wakeLoginWaiters(flowKey);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Resolve true once the login is terminal, false if `timeoutMs` elapses first. `flowId` picks
|
|
259
|
+
* one of several concurrent flows; without it the provider's newest flow is used.
|
|
260
|
+
*/
|
|
261
|
+
export function waitForLoginSettled(provider: string, timeoutMs: number, flowId?: string): Promise<boolean> {
|
|
262
|
+
const flowKey = flowId ?? latestLoginFlowKey(provider);
|
|
263
|
+
const current = flowKey === undefined ? undefined : loginState.get(flowKey);
|
|
264
|
+
if (!current || current.done || loginFlowProvider(flowKey!) !== provider) return Promise.resolve(true);
|
|
265
|
+
return new Promise<boolean>(resolve => {
|
|
266
|
+
let waiters = loginSettleWaiters.get(flowKey!);
|
|
267
|
+
if (!waiters) {
|
|
268
|
+
waiters = new Set();
|
|
269
|
+
loginSettleWaiters.set(flowKey!, waiters);
|
|
270
|
+
}
|
|
271
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
272
|
+
const wake = (): void => {
|
|
273
|
+
clearTimeout(timer);
|
|
274
|
+
resolve(true);
|
|
275
|
+
};
|
|
276
|
+
timer = setTimeout(() => {
|
|
277
|
+
waiters.delete(wake);
|
|
278
|
+
resolve(false);
|
|
279
|
+
}, timeoutMs);
|
|
280
|
+
waiters.add(wake);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Forget every login flow for a provider (logout): it invalidates all of them, not just the
|
|
286
|
+
* newest. Any code-mode POST awaiting one is released rather than left to time out.
|
|
287
|
+
*/
|
|
288
|
+
export function clearLoginState(provider: string): void {
|
|
289
|
+
for (const flowKey of loginFlowKeys(provider)) {
|
|
290
|
+
loginAbort.get(flowKey)?.controller.abort("cleared");
|
|
291
|
+
loginAbort.delete(flowKey);
|
|
292
|
+
clearManualCodeSlot(flowKey);
|
|
293
|
+
dropLoginFlow(flowKey);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Cancel one login. `flowId` names it exactly; without one the provider's newest in-flight flow
|
|
299
|
+
* is cancelled, which is every single-flow caller (GUI, CLI) unchanged. A flow id belonging to a
|
|
300
|
+
* different provider is refused rather than cancelled.
|
|
301
|
+
*/
|
|
302
|
+
export function cancelLoginFlow(provider: string, flowId?: string): boolean {
|
|
303
|
+
const flowKey = flowId ?? latestLoginFlowKey(provider);
|
|
304
|
+
if (flowKey === undefined) return false;
|
|
305
|
+
const active = loginAbort.get(flowKey);
|
|
306
|
+
const existing = loginState.get(flowKey);
|
|
307
|
+
if ((active?.provider ?? loginFlowProvider(flowKey)) !== provider) return false;
|
|
308
|
+
if (!active && (!existing || existing.done)) return false;
|
|
309
|
+
abandonLoginFlow(flowKey, { error: "Login cancelled", errorCode: "no_pending_login" });
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
|
|
23
313
|
/** Pending paste for a login in progress: either a waiter or a stashed early submission. */
|
|
24
314
|
export interface ManualCodeSlot {
|
|
25
315
|
pendingInput?: string;
|
|
26
316
|
resolve?: (value: string) => void;
|
|
27
317
|
/** Registered by the callback flow so submits can validate state synchronously. */
|
|
28
318
|
expectedState?: string;
|
|
319
|
+
/** Code-display flows are single-use: set once a paste is handed to the exchange. */
|
|
320
|
+
delivered?: boolean;
|
|
29
321
|
}
|
|
30
322
|
const loginManual = new Map<string, ManualCodeSlot>();
|
|
31
323
|
const OAUTH_PENDING_CODE_MAX_BYTES = 4 * 1024;
|
|
@@ -34,35 +326,41 @@ let lastOAuthFlowReconciledGeneration = 0;
|
|
|
34
326
|
export function reconcileOAuthFlowState(context: GenerationContext): number {
|
|
35
327
|
if (context.generation <= lastOAuthFlowReconciledGeneration) return 0;
|
|
36
328
|
let removed = 0;
|
|
37
|
-
for (const [
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
if (
|
|
41
|
-
if (
|
|
329
|
+
for (const [flowKey, state] of loginState) {
|
|
330
|
+
const provider = state.provider ?? flowKey!;
|
|
331
|
+
if (context.providerNames.has(provider) || !state.done || loginAbort.has(flowKey)) continue;
|
|
332
|
+
if (loginState.delete(flowKey)) removed += 1;
|
|
333
|
+
if (loginManual.delete(flowKey)) removed += 1;
|
|
334
|
+
if (loginAbort.delete(flowKey)) removed += 1;
|
|
42
335
|
}
|
|
43
336
|
lastOAuthFlowReconciledGeneration = context.generation;
|
|
44
337
|
return removed;
|
|
45
338
|
}
|
|
46
339
|
|
|
47
|
-
|
|
48
|
-
|
|
340
|
+
/** Test-only counterpart to `resetOAuthReauthStateForTests`, for the same leak. */
|
|
341
|
+
export function resetOAuthFlowReconcileStateForTests(): void {
|
|
342
|
+
lastOAuthFlowReconciledGeneration = 0;
|
|
49
343
|
}
|
|
50
344
|
|
|
51
|
-
export function
|
|
52
|
-
|
|
345
|
+
export function clearManualCodeSlot(flowKey: string): void {
|
|
346
|
+
loginManual.delete(flowKey);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function ensureManualCodeSlot(flowKey: string): ManualCodeSlot {
|
|
350
|
+
let slot = loginManual.get(flowKey);
|
|
53
351
|
if (!slot) {
|
|
54
352
|
slot = {};
|
|
55
|
-
loginManual.set(
|
|
353
|
+
loginManual.set(flowKey, slot);
|
|
56
354
|
}
|
|
57
355
|
return slot;
|
|
58
356
|
}
|
|
59
357
|
|
|
60
358
|
/** Wait for a GUI/CLI paste of the OAuth redirect URL or code (or return a stashed early submit). */
|
|
61
|
-
export function waitForManualLoginCode(
|
|
359
|
+
export function waitForManualLoginCode(flowKey: string, signal: AbortSignal, expectedState?: string): Promise<string> {
|
|
62
360
|
if (signal.aborted) {
|
|
63
361
|
return Promise.reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
|
|
64
362
|
}
|
|
65
|
-
const slot = ensureManualCodeSlot(
|
|
363
|
+
const slot = ensureManualCodeSlot(flowKey);
|
|
66
364
|
if (expectedState !== undefined) slot.expectedState = expectedState;
|
|
67
365
|
if (slot.pendingInput !== undefined) {
|
|
68
366
|
const value = slot.pendingInput;
|
|
@@ -84,17 +382,61 @@ export function waitForManualLoginCode(provider: string, signal: AbortSignal, ex
|
|
|
84
382
|
}
|
|
85
383
|
|
|
86
384
|
/**
|
|
87
|
-
* Feed a pasted redirect URL or authorization code into an in-progress GUI login.
|
|
385
|
+
* Feed a pasted redirect URL or authorization code into an in-progress GUI login. Provider-only,
|
|
386
|
+
* so it feeds the provider's newest in-flight flow — see `latestLoginFlowKey`.
|
|
88
387
|
* Returns ok:false when no login is waiting (or input is empty). Invalid pastes are accepted
|
|
89
388
|
* here and re-prompted by the OAuth callback loop if they cannot be parsed / fail state checks.
|
|
90
389
|
*/
|
|
91
390
|
export function submitManualLoginCode(provider: string, input: string): { ok: true } | { ok: false; error: string } {
|
|
391
|
+
const result = submitLoginCode(provider, input);
|
|
392
|
+
// The machine-readable code is dropped here on purpose: this return shape is the
|
|
393
|
+
// long-standing contract of the callback flow's paste route. Code mode calls
|
|
394
|
+
// submitLoginCode directly because it must answer with the code.
|
|
395
|
+
return result.ok ? { ok: true } : { ok: false, error: result.error };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Same validation as `submitManualLoginCode`, with the machine-readable reason attached, and
|
|
400
|
+
* the id of the flow the paste was applied to so the caller can await THAT flow rather than
|
|
401
|
+
* whichever one is newest by the time it looks.
|
|
402
|
+
*
|
|
403
|
+
* `expectedFlowId` pins the submission to one flow: a paste from a superseded attempt, or one
|
|
404
|
+
* aimed at another concurrent login, is refused rather than fed to whatever is running now.
|
|
405
|
+
*/
|
|
406
|
+
export function submitLoginCode(
|
|
407
|
+
provider: string,
|
|
408
|
+
input: string,
|
|
409
|
+
expectedFlowId?: string,
|
|
410
|
+
): { ok: true; flowId: string } | { ok: false; error: string; code: LoginCodeError } {
|
|
92
411
|
const trimmed = input.trim();
|
|
93
|
-
if (!trimmed) return { ok: false, error: "empty code" };
|
|
94
|
-
if (retainedUtf8Bytes(trimmed) > OAUTH_PENDING_CODE_MAX_BYTES)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
412
|
+
if (!trimmed) return { ok: false, error: "empty code", code: "malformed_input" };
|
|
413
|
+
if (retainedUtf8Bytes(trimmed) > OAUTH_PENDING_CODE_MAX_BYTES) {
|
|
414
|
+
return { ok: false, error: "code too large", code: "malformed_input" };
|
|
415
|
+
}
|
|
416
|
+
// The flow id IS the key, so an id belonging to another provider's flow (or to no flow at
|
|
417
|
+
// all) resolves to nothing here rather than reaching a login it does not own.
|
|
418
|
+
const flowKey = expectedFlowId ?? latestLoginFlowKey(provider);
|
|
419
|
+
const st = flowKey === undefined ? undefined : loginState.get(flowKey);
|
|
420
|
+
if (!st || st.done || loginFlowProvider(flowKey!) !== provider) {
|
|
421
|
+
return { ok: false, error: "no login in progress", code: "no_pending_login" };
|
|
422
|
+
}
|
|
423
|
+
if (st.expiresAt !== undefined && Date.now() > st.expiresAt) {
|
|
424
|
+
return { ok: false, error: "login expired", code: "no_pending_login" };
|
|
425
|
+
}
|
|
426
|
+
const slot = ensureManualCodeSlot(flowKey!);
|
|
427
|
+
// A code-display flow exchanges exactly one paste and then settles, success or not. A second
|
|
428
|
+
// paste while that exchange runs would otherwise be stashed and answered with the first
|
|
429
|
+
// paste's outcome, so it is refused instead.
|
|
430
|
+
if (st.mode === "code" && slot.delivered) {
|
|
431
|
+
return { ok: false, error: "login already received a code", code: "no_pending_login" };
|
|
432
|
+
}
|
|
433
|
+
// Without the expected state a paste cannot be checked here, and stashing it would spend the
|
|
434
|
+
// flow's single use on input the callback loop later throws away. The URL a caller needs to
|
|
435
|
+
// obtain a code is published only after the state is registered, so this is only reachable
|
|
436
|
+
// by racing the start.
|
|
437
|
+
if (st.mode === "code" && slot.expectedState === undefined) {
|
|
438
|
+
return { ok: false, error: "login is not ready for a code yet", code: "no_pending_login" };
|
|
439
|
+
}
|
|
98
440
|
// Synchronous validation (validated request/ack): reject un-parseable input and
|
|
99
441
|
// authorization responses (url/query kind) whose state is missing or mismatched
|
|
100
442
|
// once the flow has registered its expected state. Raw codes stay in-session-PKCE
|
|
@@ -106,15 +448,20 @@ export function submitManualLoginCode(provider: string, input: string): { ok: tr
|
|
|
106
448
|
// (`{ apiKey, state, ... }`). Keep that opaque to the generic raw parser so
|
|
107
449
|
// hashes in JSON strings do not become a fake state suffix; its provider parser validates state.
|
|
108
450
|
const isCommandCodeJson = provider === "command-code" && trimmed.startsWith("{");
|
|
109
|
-
if (!parsed.code && !isCommandCodeJson)
|
|
451
|
+
if (!parsed.code && !isCommandCodeJson) {
|
|
452
|
+
return { ok: false, error: "no authorization code found in input", code: "malformed_input" };
|
|
453
|
+
}
|
|
110
454
|
// A raw paste carrying an explicit code#state suffix is state-bearing too: it
|
|
111
455
|
// must match the expected state rather than bypass validation.
|
|
112
456
|
const stateBearing = !isCommandCodeJson && (parsed.kind !== "raw" || parsed.state !== undefined);
|
|
113
457
|
if (stateBearing && slot.expectedState !== undefined) {
|
|
114
|
-
if (parsed.state === undefined)
|
|
458
|
+
if (parsed.state === undefined) {
|
|
459
|
+
return { ok: false, error: "redirect URL is missing the state parameter", code: "state_mismatch" };
|
|
460
|
+
}
|
|
115
461
|
if (parsed.state !== slot.expectedState) {
|
|
116
462
|
return {
|
|
117
463
|
ok: false,
|
|
464
|
+
code: "state_mismatch",
|
|
118
465
|
error: parsed.kind === "raw"
|
|
119
466
|
? "state mismatch — paste the bare code, or the correct code#state from THIS login attempt"
|
|
120
467
|
: "state mismatch — paste the redirect URL from THIS login attempt",
|
|
@@ -129,5 +476,6 @@ export function submitManualLoginCode(provider: string, input: string): { ok: tr
|
|
|
129
476
|
// Race: GUI may POST before the flow reaches onManualCodeInput — stash for the waiter.
|
|
130
477
|
slot.pendingInput = trimmed;
|
|
131
478
|
}
|
|
132
|
-
|
|
479
|
+
if (st.mode === "code") slot.delivered = true;
|
|
480
|
+
return { ok: true, flowId: flowKey! };
|
|
133
481
|
}
|