@bitkyc08/opencodex 2.86.0 → 2.88.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/gui/dist/assets/App-CHC89EXQ.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
- package/gui/dist/assets/index-B5bsvxor.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-D8y2wTsB.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +9 -8
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/passthrough.ts +7 -23
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +151 -33
- 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/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +18 -2
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google-progress-history.ts +53 -0
- package/src/adapters/google.ts +52 -9
- 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-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 +4 -2
- 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 +4 -1
- package/src/advisor/context.ts +16 -4
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +79 -31
- package/src/advisor/settings.ts +11 -2
- package/src/bridge/errors.ts +19 -1
- package/src/bridge/internal.ts +7 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +59 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/claude/desktop-3p-library.ts +32 -7
- package/src/claude/desktop-3p.ts +55 -13
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +77 -0
- package/src/claude/inbound.ts +33 -0
- 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/outbound.ts +46 -13
- 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 +17 -3
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +5 -2
- package/src/cli/capabilities-base.ts +38 -10
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- 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 +33 -16
- package/src/cli/dispatch.ts +25 -4
- package/src/cli/doctor.ts +6 -1
- package/src/cli/ensure-desktop-cold-start.ts +60 -7
- package/src/cli/help.ts +3 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/loopback-family.ts +31 -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 +28 -8
- 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 +2 -1
- package/src/clients/config-export.ts +18 -44
- 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/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 +2 -2
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +9 -1
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +1 -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 +21 -16
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/parsing.ts +30 -86
- package/src/codex/catalog/provider-models.ts +36 -34
- package/src/codex/catalog/routed-gather.ts +14 -13
- 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 +23 -2
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app-restart.ts +21 -0
- package/src/codex/desktop-compatibility/installed-build.ts +24 -6
- package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
- package/src/codex/desktop-compatibility/runtime.ts +12 -3
- package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
- package/src/codex/features.ts +85 -45
- package/src/codex/inject/history-failure.ts +27 -0
- 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/main-account-hard-lock.ts +30 -1
- 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 +1 -1
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- 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/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- 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 +33 -6
- package/src/config/live-reconcile.ts +38 -2
- package/src/config/load-degrade.ts +17 -0
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +53 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +22 -0
- package/src/config/schema/leaf-validators.ts +38 -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 +657 -297
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +116 -27
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +8 -0
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-upstream-tunnel.ts +18 -53
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +19 -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/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-failure-model.ts +2 -0
- 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 +40 -11
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +184 -109
- 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/encoders/adapter-events.ts +6 -5
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/provider-summary.ts +2 -1
- 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 +3 -0
- package/src/providers/model-rename-migration.ts +2 -2
- 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/opencode-zen-rate-limit.ts +7 -2
- 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 +37 -8
- 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 +50 -5
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +74 -6
- 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/router.ts +8 -321
- package/src/routing/analytics.ts +7 -15
- package/src/routing/health.ts +37 -22
- package/src/routing/history/indexer.ts +6 -5
- package/src/routing/history/ingest.ts +6 -2
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +17 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +7 -1
- 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 +56 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/live-sideband.ts +3 -2
- 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 +20 -6
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +8 -8
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/lifecycle.ts +13 -1
- package/src/server/management/advisor-routes.ts +15 -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 +1 -1
- package/src/server/management/config-routes.ts +75 -4
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/model-routes.ts +11 -6
- 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/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 +10 -1
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +117 -24
- 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 +6 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +47 -19
- package/src/server/responses/adapter-delivery.ts +72 -12
- package/src/server/responses/adapter-dispatch.ts +46 -3
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/combo-stream-preflight.ts +3 -2
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +20 -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 +2 -0
- package/src/server/responses/core-combo.ts +121 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +8 -4
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +7 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +23 -3
- package/src/server/responses/core.ts +2 -115
- 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 +3 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -13
- package/src/server/responses/passthrough-execution.ts +3 -2
- package/src/server/responses/request-prepare.ts +77 -12
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +104 -27
- package/src/server/responses/response-effects.ts +2 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +63 -9
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/ws-bridge.ts +2 -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/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +76 -57
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +72 -9
- 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/types/config.ts +64 -3
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +35 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +31 -2
- package/src/types/wire.ts +31 -37
- package/src/types.ts +2 -0
- package/src/update/npm-invocation.mjs +12 -2
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +3 -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 +30 -11
- package/src/usage/telemetry-contract.ts +1 -0
- 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/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +236 -73
- package/src/web-search/progress-stream.ts +15 -2
- package/src/web-search/run-turn-loop.ts +9 -1
- package/gui/dist/assets/App-BjlttVLy.js +0 -58
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-BMssP4k3.js +0 -94
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
package/src/lib/errors.ts
CHANGED
|
@@ -650,10 +650,22 @@ export function adapterFailureFromMessage(message: string): { httpStatus: number
|
|
|
650
650
|
return { httpStatus, error };
|
|
651
651
|
}
|
|
652
652
|
|
|
653
|
+
/** A declared client refusal: retrying the identical request cannot change the verdict. */
|
|
654
|
+
const CLIENT_ERROR_STATUS: Record<string, 400 | 401 | 403 | 404> = {
|
|
655
|
+
invalid_request_error: 400,
|
|
656
|
+
authentication_error: 401,
|
|
657
|
+
permission_error: 403,
|
|
658
|
+
not_found_error: 404,
|
|
659
|
+
};
|
|
660
|
+
|
|
653
661
|
/** Narrow structured classes carried through a synthesized bare-error terminal. */
|
|
654
662
|
export function recognizedUpstreamError(error: { type?: unknown; code?: unknown }):
|
|
655
|
-
{ type: string; code: string; httpStatus: 429 | 503 } | undefined {
|
|
663
|
+
{ type: string; code: string; httpStatus: 400 | 401 | 403 | 404 | 429 | 503 } | undefined {
|
|
656
664
|
const code = typeof error.code === "string" ? error.code : undefined;
|
|
665
|
+
const clientClass = code ?? (typeof error.type === "string" ? error.type : undefined);
|
|
666
|
+
if (clientClass !== undefined && Object.hasOwn(CLIENT_ERROR_STATUS, clientClass)) {
|
|
667
|
+
return { type: clientClass, code: clientClass, httpStatus: CLIENT_ERROR_STATUS[clientClass] };
|
|
668
|
+
}
|
|
657
669
|
// An explicit unknown code must not gain a recognized class from its type or copy.
|
|
658
670
|
if (code !== undefined && !["rate_limit_exceeded", "rate_limit_error", "server_is_overloaded", "overloaded_error"].includes(code)) return undefined;
|
|
659
671
|
if (code === "rate_limit_exceeded" || code === "rate_limit_error"
|
|
@@ -680,8 +692,13 @@ export function httpStatusFromTerminalError(error: {
|
|
|
680
692
|
if (isCyberPolicyCode(error.code) || (error.message ? isCyberPolicyMessage(error.message) : false)) {
|
|
681
693
|
return 400;
|
|
682
694
|
}
|
|
695
|
+
const message = error.message ?? "";
|
|
683
696
|
const recognized = recognizedUpstreamError(error);
|
|
684
|
-
|
|
697
|
+
// Client-closed messages often arrive as invalid_request_error after classifyError, so a
|
|
698
|
+
// recognized 400 yields to the client-closed check below; every other class keeps its order.
|
|
699
|
+
if (recognized && !(recognized.httpStatus === 400 && message && isClientClosedMessage(message))) {
|
|
700
|
+
return recognized.httpStatus;
|
|
701
|
+
}
|
|
685
702
|
if (error.type === "rate_limit_error" || error.code === "rate_limit_exceeded") return 429;
|
|
686
703
|
if (error.type === "authentication_error" || error.code === "invalid_api_key") return 401;
|
|
687
704
|
if (
|
|
@@ -693,7 +710,6 @@ export function httpStatusFromTerminalError(error: {
|
|
|
693
710
|
if (error.type === "server_error" && error.code === "server_is_overloaded") return 503;
|
|
694
711
|
// Client-closed messages often arrive as invalid_request_error after classifyError; check message
|
|
695
712
|
// before treating every invalid_request_error as HTTP 400.
|
|
696
|
-
const message = error.message ?? "";
|
|
697
713
|
if (message && isClientClosedMessage(message)) return 499;
|
|
698
714
|
if (error.type === "invalid_request_error") return 400;
|
|
699
715
|
if (error.type === "proxy_error") return 500;
|
package/src/lib/gcp-adc.ts
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Application Default Credentials (ADC) resolution for Vertex AI.
|
|
3
3
|
*
|
|
4
4
|
* Direct WebCrypto + REST implementation (no `google-auth-library`). Sources, in priority order:
|
|
5
|
+
* 0. A `gcp-sa:<account>` reference (the provider's active key resolving to one) → the Service
|
|
6
|
+
* Account / authorized_user JSON stored in the OS credential store under that account
|
|
7
|
+
* (`keychain:<account>` secret, written by the key-store save path when the user pastes a
|
|
8
|
+
* credential JSON directly into the API-key slot). See resolveGcpCredentialMarker.
|
|
5
9
|
* 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT
|
|
6
10
|
* exchange) or `type: "authorized_user"` (refresh-token exchange).
|
|
7
11
|
* 2. gcloud user ADC (`CLOUDSDK_CONFIG`, else `%APPDATA%\gcloud` on Windows, else
|
|
@@ -18,6 +22,8 @@ import { Buffer } from "node:buffer";
|
|
|
18
22
|
import * as os from "node:os";
|
|
19
23
|
import * as path from "node:path";
|
|
20
24
|
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
25
|
+
import { GCP_CREDENTIAL_MARKER_PREFIX, providerKeychainEntry } from "../providers/api-key-resolve";
|
|
26
|
+
export { isGcpCredentialMaterial } from "./gcp-credential-input";
|
|
21
27
|
import {
|
|
22
28
|
captureConfigGeneration,
|
|
23
29
|
sweepExpiredOnWrite,
|
|
@@ -36,6 +42,50 @@ const TOKEN_TIMEOUT_MS = 15_000;
|
|
|
36
42
|
const TOKEN_ATTEMPTS = 3;
|
|
37
43
|
const TOKEN_RETRY_BASE_MS = 300;
|
|
38
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Reference prefix stored in `provider.apiKey` (and `apiKeyPool` entries) when the user pastes a
|
|
47
|
+
* GCP credential JSON directly into an API-key slot. The secret JSON lives in the OS credential
|
|
48
|
+
* store under the same service the `keychain:` references use; only the marker is persisted to
|
|
49
|
+
* `config.json`. The leaf reference prefix also identifies already-stored material for migration.
|
|
50
|
+
*/
|
|
51
|
+
export { GCP_CREDENTIAL_MARKER_PREFIX } from "../providers/api-key-resolve";
|
|
52
|
+
|
|
53
|
+
/** Extract a marker account; malformed explicit markers never opt into host ADC. */
|
|
54
|
+
export function gcpCredentialMarkerAccount(value: string | undefined): string | undefined {
|
|
55
|
+
if (typeof value !== "string" || !value.startsWith(GCP_CREDENTIAL_MARKER_PREFIX)) return undefined;
|
|
56
|
+
const account = value.slice(GCP_CREDENTIAL_MARKER_PREFIX.length);
|
|
57
|
+
if (!account.trim()) throw new Error("GCP credential marker requires a non-empty account");
|
|
58
|
+
return account;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether a pasted API-key value is in fact a GCP credential JSON. Used by the save path to
|
|
63
|
+
* divert JSON material out of the literal-key flow before it reaches `config.json`. Deliberately
|
|
64
|
+
* strict: only a `type` field with a known ADC credential type counts, so a user's literal API
|
|
65
|
+
* key that happens to start with `{` is never eaten.
|
|
66
|
+
*/
|
|
67
|
+
export function parseGcpCredentialJson(value: string): AdcFileCredentials | undefined {
|
|
68
|
+
const trimmed = (value ?? "").trim();
|
|
69
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return undefined;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
|
72
|
+
if (parsed?.type === "service_account") {
|
|
73
|
+
if (typeof parsed.client_email !== "string" || !parsed.client_email.trim()) return undefined;
|
|
74
|
+
if (typeof parsed.private_key !== "string" || !parsed.private_key.trim()) return undefined;
|
|
75
|
+
return parsed as unknown as AdcFileCredentials;
|
|
76
|
+
}
|
|
77
|
+
if (parsed?.type === "authorized_user") {
|
|
78
|
+
if (typeof parsed.client_id !== "string" || !parsed.client_id.trim()) return undefined;
|
|
79
|
+
if (typeof parsed.client_secret !== "string" || !parsed.client_secret.trim()) return undefined;
|
|
80
|
+
if (typeof parsed.refresh_token !== "string" || !parsed.refresh_token.trim()) return undefined;
|
|
81
|
+
return parsed as unknown as AdcFileCredentials;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
} catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
39
89
|
interface CachedToken {
|
|
40
90
|
token: string;
|
|
41
91
|
expiresAtMs: number;
|
|
@@ -81,7 +131,10 @@ export function sweepExpiredGcpAdcTokens(now = Date.now()): number {
|
|
|
81
131
|
|
|
82
132
|
export function reconcileGcpAdcTokens(context: GenerationContext): number {
|
|
83
133
|
if (context.generation <= lastReconciledGeneration) return 0;
|
|
84
|
-
|
|
134
|
+
// The periodic sweep registration has no request context, so no marker is active here: markers
|
|
135
|
+
// travel per-request via getVertexAccessToken(options.markerAccount), and a marker-token cache
|
|
136
|
+
// entry whose account no longer resolves is simply pruned by this sweep when it expires.
|
|
137
|
+
const currentSource = currentAdcSourceKey(undefined);
|
|
85
138
|
let removed = 0;
|
|
86
139
|
for (const source of tokenCache.keys()) {
|
|
87
140
|
if (source === currentSource) continue;
|
|
@@ -131,7 +184,29 @@ function readJsonFile<T>(filePath: string): T | undefined {
|
|
|
131
184
|
return JSON.parse(readFileSync(filePath, "utf8")) as T;
|
|
132
185
|
}
|
|
133
186
|
|
|
134
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Read the credential JSON for a `gcp-sa:<account>` marker from the OS credential store. Uses the
|
|
189
|
+
* same service name and the same injectable entry factory as the `keychain:` API-key references
|
|
190
|
+
* (api-key-resolve.providerKeychainEntry), so one probe surface and one test seam cover both.
|
|
191
|
+
* Mirrors `readKeychain`'s fail-closed policy: an unreadable entry yields no credential (the
|
|
192
|
+
* caller surfaces the failure instead of silently falling through to the next ADC source).
|
|
193
|
+
*/
|
|
194
|
+
function readMarkerCredential(account: string): AdcFileCredentials | undefined {
|
|
195
|
+
try {
|
|
196
|
+
const raw = providerKeychainEntry(account).getPassword();
|
|
197
|
+
if (typeof raw !== "string" || !raw.trim()) return undefined;
|
|
198
|
+
return parseGcpCredentialJson(raw);
|
|
199
|
+
} catch {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function loadAdcCredentials(markerAccount: string | undefined): { source: string; creds: AdcFileCredentials } | undefined {
|
|
205
|
+
if (markerAccount) {
|
|
206
|
+
const creds = readMarkerCredential(markerAccount);
|
|
207
|
+
if (!creds) throw new Error("GCP credential marker is set but its credential could not be read from the OS credential store");
|
|
208
|
+
return { source: `marker:${markerAccount}`, creds };
|
|
209
|
+
}
|
|
135
210
|
const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
136
211
|
if (gacPath) {
|
|
137
212
|
const creds = readJsonFile<AdcFileCredentials>(gacPath);
|
|
@@ -148,9 +223,10 @@ function loadAdcCredentials(): { source: string; creds: AdcFileCredentials } | u
|
|
|
148
223
|
* The cache key for the source the NEXT resolve would use, computed cheaply (no network). Lets the
|
|
149
224
|
* cache return a token only when it still matches the active credential source, so an in-process
|
|
150
225
|
* change to GOOGLE_APPLICATION_CREDENTIALS (or the user ADC file) does not keep serving a stale
|
|
151
|
-
* token from a different source. Falls back to "metadata" when no file/env ADC is present.
|
|
226
|
+
* token from a different source. Falls back to "metadata" when no marker/file/env ADC is present.
|
|
152
227
|
*/
|
|
153
|
-
function currentAdcSourceKey(): string {
|
|
228
|
+
function currentAdcSourceKey(markerAccount: string | undefined): string {
|
|
229
|
+
if (markerAccount) return `marker:${markerAccount}`;
|
|
154
230
|
const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
155
231
|
if (gacPath) return fileSourceTag("gac", gacPath);
|
|
156
232
|
const userPath = userAdcPath();
|
|
@@ -279,8 +355,8 @@ async function fetchMetadataToken(signal: AbortSignal | undefined, fetchImpl: Fe
|
|
|
279
355
|
}
|
|
280
356
|
}
|
|
281
357
|
|
|
282
|
-
async function resolveAccessTokenUncached(signal: AbortSignal | undefined, fetchImpl: FetchImpl): Promise<{ source: string; token: TokenResponse }> {
|
|
283
|
-
const adc = loadAdcCredentials();
|
|
358
|
+
async function resolveAccessTokenUncached(markerAccount: string | undefined, signal: AbortSignal | undefined, fetchImpl: FetchImpl): Promise<{ source: string; token: TokenResponse }> {
|
|
359
|
+
const adc = loadAdcCredentials(markerAccount);
|
|
284
360
|
if (adc) {
|
|
285
361
|
const token = adc.creds.type === "service_account"
|
|
286
362
|
? await exchangeJwtForToken(adc.creds, signal, fetchImpl)
|
|
@@ -295,14 +371,25 @@ async function resolveAccessTokenUncached(signal: AbortSignal | undefined, fetch
|
|
|
295
371
|
}
|
|
296
372
|
|
|
297
373
|
/** Returns a Bearer access token for the `Authorization` header on Vertex AI calls (cached + refreshed). */
|
|
298
|
-
export async function getVertexAccessToken(options?: { signal?: AbortSignal; fetch?: FetchImpl }): Promise<string> {
|
|
374
|
+
export async function getVertexAccessToken(options?: { signal?: AbortSignal; fetch?: FetchImpl; markerAccount?: string }): Promise<string> {
|
|
375
|
+
// The marker travels as an argument, never as process-global state: a module-level "active
|
|
376
|
+
// marker" written by the router would race across concurrent requests for different providers
|
|
377
|
+
// and could resolve one provider's Vertex call with another provider's service account. The
|
|
378
|
+
// adapter passes `gcpCredentialMarkerAccount(provider.apiKey)` per request instead.
|
|
379
|
+
const markerAccount = options?.markerAccount;
|
|
299
380
|
const fetchImpl = options?.fetch ?? globalThis.fetch.bind(globalThis);
|
|
300
381
|
const skew = getRefreshSkewMs();
|
|
301
382
|
const now = Date.now();
|
|
302
383
|
|
|
303
384
|
// Only serve a cached token that matches the source the next resolve would actually use; prune
|
|
304
385
|
// expired or now-stale (different-source) entries so a credential-source change is honored.
|
|
305
|
-
const expectedSource = currentAdcSourceKey();
|
|
386
|
+
const expectedSource = currentAdcSourceKey(markerAccount);
|
|
387
|
+
// A cached bearer token is usable only while the explicitly selected store entry remains
|
|
388
|
+
// readable. Removal or a locked store must fail closed before the cache can return it.
|
|
389
|
+
if (markerAccount && !readMarkerCredential(markerAccount)) {
|
|
390
|
+
tokenCache.delete(expectedSource);
|
|
391
|
+
throw new Error("GCP credential marker is set but its credential could not be read from the OS credential store");
|
|
392
|
+
}
|
|
306
393
|
const writerGeneration = captureConfigGeneration();
|
|
307
394
|
for (const [source, cached] of tokenCache) {
|
|
308
395
|
if (source === expectedSource && cached.expiresAtMs - skew > now) return cached.token;
|
|
@@ -317,7 +404,7 @@ export async function getVertexAccessToken(options?: { signal?: AbortSignal; fet
|
|
|
317
404
|
|
|
318
405
|
const promise = (async () => {
|
|
319
406
|
try {
|
|
320
|
-
const { source, token } = await resolveAccessTokenUncached(options?.signal, fetchImpl);
|
|
407
|
+
const { source, token } = await resolveAccessTokenUncached(markerAccount, options?.signal, fetchImpl);
|
|
321
408
|
const expiresAtMs = Date.now() + Math.max(0, token.expires_in * 1000);
|
|
322
409
|
if (writerGeneration >= lastReconciledGeneration || liveSources.has(source)) {
|
|
323
410
|
tokenCache.set(source, { token: token.access_token, expiresAtMs });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Structured JSON is never literal key material; malformed GCP text is also refused. */
|
|
2
|
+
export function isGcpCredentialMaterial(value: string): boolean {
|
|
3
|
+
const credentialField = /\b(?:service_account|authorized_user|external_account|private_key|refresh_token|client_secret)\b|-----BEGIN [A-Z ]*PRIVATE KEY-----/;
|
|
4
|
+
if (/[\[\{"']/.test(value) && credentialField.test(value)) return true;
|
|
5
|
+
// Reject structured carriers outright rather than recursively guessing their credential shape.
|
|
6
|
+
// This includes quoted/array/object wrappers with arbitrarily escaped or nested JSON inside.
|
|
7
|
+
try {
|
|
8
|
+
const parsed: unknown = JSON.parse(value);
|
|
9
|
+
return typeof parsed === "string" || (parsed !== null && typeof parsed === "object");
|
|
10
|
+
}
|
|
11
|
+
catch { return false; }
|
|
12
|
+
}
|
|
@@ -72,6 +72,11 @@ export async function buildLabProviderAuthHeaders(
|
|
|
72
72
|
&& configuredAnthropicInstance(config, routeContext.providerId) !== routeContext.providerId) {
|
|
73
73
|
throw new TransportError("auth_blocked", "anthropic instance unavailable");
|
|
74
74
|
}
|
|
75
|
+
// The DSH account token is sent only by its own adapter to its pinned endpoint; the generic
|
|
76
|
+
// probe transport would use another endpoint and auth scheme.
|
|
77
|
+
if (routeContext.providerId === "dsh-account") {
|
|
78
|
+
throw new TransportError("auth_blocked", "dsh-account probes are not supported");
|
|
79
|
+
}
|
|
75
80
|
try {
|
|
76
81
|
const snapshot = await getValidAccessTokenSnapshot(routeContext.providerId);
|
|
77
82
|
headers.Authorization = `Bearer ${snapshot.accessToken}`;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-route opaque proxy tunnel. The desktop upstream dialer retains its
|
|
3
|
+
* own fixed-target TLS contract.
|
|
4
|
+
* Shares OpenCodex's MIT SOCKS handshake; the caller owns all post-establishment
|
|
5
|
+
* cancellation and socket cleanup. Never replay or implicitly retry directly.
|
|
6
|
+
*/
|
|
7
|
+
import net, { type Socket } from 'node:net';
|
|
8
|
+
import tls from 'node:tls';
|
|
9
|
+
import { socks5Handshake, socks5Credentials, Socks5HandshakeError } from './socks5-handshake';
|
|
10
|
+
|
|
11
|
+
interface RouteEndpoint { host: string; port: number }
|
|
12
|
+
type RouteCredentials = { username?: undefined; password?: undefined } | { username: string; password: string };
|
|
13
|
+
type PlainProxy = { kind?: 'http'; proxyCa?: never };
|
|
14
|
+
type SecureProxy = { kind: 'https'; proxyCa?: string };
|
|
15
|
+
type SocksProxy = { kind: 'socks5' | 'socks5h'; proxyCa?: never };
|
|
16
|
+
export type OpaqueProxyRoute = RouteEndpoint & RouteCredentials & (PlainProxy | SecureProxy | SocksProxy);
|
|
17
|
+
export interface OpaqueTunnelOptions {
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
maxHeaderBytes?: number;
|
|
21
|
+
}
|
|
22
|
+
interface AttemptOptions {
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
timeoutMs: number;
|
|
25
|
+
maxHeaderBytes: number;
|
|
26
|
+
}
|
|
27
|
+
export class TunnelError extends Error {
|
|
28
|
+
override readonly name = 'TunnelError';
|
|
29
|
+
readonly code: string;
|
|
30
|
+
readonly status?: number;
|
|
31
|
+
constructor(code: string, message: string, { status }: { status?: number } = {}) {
|
|
32
|
+
super(message); this.code = code; this.status = status;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const aborted = () => new TunnelError('ABORTED', 'Tunnel establishment cancelled');
|
|
36
|
+
|
|
37
|
+
function validateAuthority(authority: string): string {
|
|
38
|
+
if (typeof authority !== 'string' || !/^(?:\[[0-9a-fA-F:]+\]|[a-zA-Z0-9._-]+):[0-9]+$/.test(authority)) {
|
|
39
|
+
throw new TunnelError('TARGET', 'An explicit host:port authority is required');
|
|
40
|
+
}
|
|
41
|
+
const port = Number(authority.slice(authority.lastIndexOf(':') + 1));
|
|
42
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new TunnelError('TARGET', 'Invalid target port');
|
|
43
|
+
return authority;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateOptions(p: OpaqueProxyRoute, timeoutMs: number, maxHeaderBytes: number): void {
|
|
47
|
+
if (!p || Array.isArray(p) || typeof p.host !== 'string' || !p.host || /[\s/@]/.test(p.host)
|
|
48
|
+
|| !Number.isInteger(p.port) || p.port < 1 || p.port > 65535
|
|
49
|
+
|| !['http', 'https', 'socks5', 'socks5h'].includes(p.kind ?? 'http')) throw new TunnelError('CONFIG', 'Invalid proxy route');
|
|
50
|
+
if ((p.username === undefined) !== (p.password === undefined)
|
|
51
|
+
|| (p.username !== undefined && (typeof p.username !== 'string' || typeof p.password !== 'string'
|
|
52
|
+
|| (!p.kind?.startsWith('socks') && p.username.includes(':')) || Buffer.byteLength(p.username + p.password) > 4096))) throw new TunnelError('CONFIG', 'Invalid proxy credentials');
|
|
53
|
+
if ((p.kind ?? 'http').startsWith('socks') && p.username !== undefined
|
|
54
|
+
&& (Buffer.byteLength(p.username) > 255 || Buffer.byteLength(p.password) > 255)) throw new TunnelError('CONFIG', 'SOCKS credentials must each fit in 255 UTF-8 bytes');
|
|
55
|
+
if (p.proxyCa !== undefined && p.kind !== 'https') throw new TunnelError('CONFIG', 'Proxy TLS settings require HTTPS proxy');
|
|
56
|
+
if (p.proxyCa !== undefined && typeof p.proxyCa !== 'string') throw new TunnelError('CONFIG', 'Invalid proxy CA');
|
|
57
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000
|
|
58
|
+
|| !Number.isSafeInteger(maxHeaderBytes) || maxHeaderBytes < 64 || maxHeaderBytes > 65536) {
|
|
59
|
+
throw new TunnelError('CONFIG', 'Invalid deadline or header limit');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function connectOne(authority: string, proxy: OpaqueProxyRoute, { signal, timeoutMs, maxHeaderBytes }: AttemptOptions): Promise<Socket> {
|
|
64
|
+
if (proxy.kind === 'socks5' || proxy.kind === 'socks5h') return connectSocks(authority, proxy, { signal, timeoutMs, maxHeaderBytes });
|
|
65
|
+
return new Promise<Socket>((resolve, reject) => {
|
|
66
|
+
if (signal?.aborted) return reject(aborted());
|
|
67
|
+
const secure = proxy.kind === 'https';
|
|
68
|
+
const identity = proxy.host;
|
|
69
|
+
const tlsOptions: tls.ConnectionOptions & { allowHalfOpen: boolean } = {
|
|
70
|
+
host: proxy.host, port: proxy.port, allowHalfOpen: true,
|
|
71
|
+
servername: net.isIP(identity) ? undefined : identity, ca: proxy.proxyCa,
|
|
72
|
+
checkServerIdentity: (_name, cert) => tls.checkServerIdentity(identity, cert),
|
|
73
|
+
rejectUnauthorized: true, ALPNProtocols: ['http/1.1'],
|
|
74
|
+
};
|
|
75
|
+
const socket = secure
|
|
76
|
+
? tls.connect(tlsOptions)
|
|
77
|
+
: net.createConnection({ host: proxy.host, port: proxy.port, allowHalfOpen: true });
|
|
78
|
+
// Keep a defensive observer across the promise-to-owner handoff. Errors still
|
|
79
|
+
// settle the establishment promise or are handled by the relay after handoff.
|
|
80
|
+
socket.on('error', () => {});
|
|
81
|
+
let pending: Buffer = Buffer.alloc(0); let settled = false, tlsReady = !secure;
|
|
82
|
+
const connectedEvent = secure ? 'secureConnect' : 'connect';
|
|
83
|
+
const cleanup = () => {
|
|
84
|
+
clearTimeout(timer); signal?.removeEventListener('abort', onAbort);
|
|
85
|
+
socket.removeListener(connectedEvent, onConnect); socket.removeListener('data', onData);
|
|
86
|
+
socket.removeListener('error', onError); socket.removeListener('end', onEnd); socket.removeListener('close', onClose);
|
|
87
|
+
};
|
|
88
|
+
const fail = (error: Error) => {
|
|
89
|
+
if (settled) return; settled = true; cleanup(); socket.destroy(); reject(error);
|
|
90
|
+
};
|
|
91
|
+
const onAbort = () => fail(aborted());
|
|
92
|
+
const establishmentFailure = (code: string, message: string) => fail(new TunnelError(code, message));
|
|
93
|
+
const onError = () => {
|
|
94
|
+
if (secure && !tlsReady) {
|
|
95
|
+
fail(new TunnelError('PROXY_TLS', 'HTTPS proxy establishment failed'));
|
|
96
|
+
} else establishmentFailure('TRANSPORT', 'HTTP proxy connection failed');
|
|
97
|
+
};
|
|
98
|
+
const onEnd = () => establishmentFailure('EARLY_CLOSE', 'HTTP proxy closed before establishing the tunnel');
|
|
99
|
+
const onClose = onEnd;
|
|
100
|
+
const onConnect = () => {
|
|
101
|
+
tlsReady = true;
|
|
102
|
+
const auth = proxy.username === undefined ? '' : `Proxy-Authorization: Basic ${Buffer.from(`${proxy.username}:${proxy.password}`).toString('base64')}\r\n`;
|
|
103
|
+
socket.write(`CONNECT ${authority} HTTP/1.1\r\nHost: ${authority}\r\n${auth}\r\n`);
|
|
104
|
+
};
|
|
105
|
+
const onData = (chunk: Buffer) => {
|
|
106
|
+
pending = Buffer.concat([pending, chunk]);
|
|
107
|
+
const lineEnd = pending.indexOf('\r\n');
|
|
108
|
+
if (lineEnd >= 0) {
|
|
109
|
+
const firstLine = pending.subarray(0, lineEnd).toString('latin1');
|
|
110
|
+
const firstStatus = /^HTTP\/1\.[01] ([0-9]{3})(?: [\t\x20-\x7e\x80-\xff]*)?$/.exec(firstLine);
|
|
111
|
+
if (!firstStatus) return fail(new TunnelError('PROTOCOL', 'Invalid HTTP proxy status line'));
|
|
112
|
+
const status = Number(firstStatus[1]);
|
|
113
|
+
if (status < 200 || status >= 300) return fail(new TunnelError('REFUSED', 'HTTP proxy refused CONNECT', { status }));
|
|
114
|
+
}
|
|
115
|
+
const end = pending.indexOf('\r\n\r\n');
|
|
116
|
+
if (end < 0) {
|
|
117
|
+
if (pending.length > maxHeaderBytes) fail(new TunnelError('PROTOCOL', 'HTTP proxy response headers exceed limit'));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (end + 4 > maxHeaderBytes) return fail(new TunnelError('PROTOCOL', 'HTTP proxy response headers exceed limit'));
|
|
121
|
+
const lines = pending.subarray(0, end).toString('latin1').split('\r\n');
|
|
122
|
+
const statusLine = lines[0];
|
|
123
|
+
const match = /^HTTP\/1\.[01] ([0-9]{3})(?: [^\r\n]*)?$/.exec(statusLine);
|
|
124
|
+
if (!match) return fail(new TunnelError('PROTOCOL', 'Invalid HTTP proxy status line'));
|
|
125
|
+
const status = Number(match[1]);
|
|
126
|
+
if (lines.slice(1).some(line => !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+:[\t\x20-\x7e\x80-\xff]*$/.test(line))) {
|
|
127
|
+
return fail(new TunnelError('PROTOCOL', 'Invalid HTTP proxy response header'));
|
|
128
|
+
}
|
|
129
|
+
if (status < 200 || status >= 300) return fail(new TunnelError('REFUSED', 'HTTP proxy refused CONNECT', { status }));
|
|
130
|
+
settled = true; socket.pause(); cleanup();
|
|
131
|
+
const head = pending.subarray(end + 4);
|
|
132
|
+
if (head.length) socket.unshift(head);
|
|
133
|
+
resolve(socket);
|
|
134
|
+
};
|
|
135
|
+
const timer = setTimeout(() => establishmentFailure('TIMEOUT', 'HTTP proxy establishment deadline exceeded'), timeoutMs);
|
|
136
|
+
socket.once(connectedEvent, onConnect); socket.on('data', onData);
|
|
137
|
+
socket.once('error', onError); socket.once('end', onEnd); socket.once('close', onClose);
|
|
138
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
139
|
+
if (signal?.aborted) onAbort();
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// The bounded socket reader owns setup cancellation and preserves queued bytes.
|
|
144
|
+
// Framing uses the existing
|
|
145
|
+
// MIT socks5-handshake module; neither shared framing nor its DNS policy changes.
|
|
146
|
+
async function connectSocks(authority: string, proxy: OpaqueProxyRoute, { signal, timeoutMs }: AttemptOptions): Promise<Socket> {
|
|
147
|
+
if (signal?.aborted) throw aborted();
|
|
148
|
+
const socket = net.createConnection({ host: proxy.host, port: proxy.port, allowHalfOpen: true });
|
|
149
|
+
socket.on('error', () => {});
|
|
150
|
+
let buffer: Buffer = Buffer.alloc(0), failure: Error | undefined;
|
|
151
|
+
let waiting: { length: number; resolve: () => void; reject: (error: Error) => void } | undefined;
|
|
152
|
+
const fail = (error: Error) => { if (failure) return; failure = error; const current = waiting; waiting = undefined; current?.reject(error); socket.destroy(); };
|
|
153
|
+
const transportError = (code: string, message: string) => fail(new TunnelError(code, message));
|
|
154
|
+
const onData = (chunk: Buffer) => {
|
|
155
|
+
// Consume at most the bytes needed by a bounded handshake read, then pause.
|
|
156
|
+
// A valid CONNECT reply can share a packet with arbitrary early target data;
|
|
157
|
+
// that remainder is not a malformed/oversized handshake and must survive.
|
|
158
|
+
socket.pause();
|
|
159
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
160
|
+
if (waiting && buffer.length >= waiting.length) { const current = waiting; waiting = undefined; current.resolve(); }
|
|
161
|
+
else if (waiting) socket.resume();
|
|
162
|
+
};
|
|
163
|
+
const onError = () => transportError('TRANSPORT', 'SOCKS proxy connection failed');
|
|
164
|
+
const onClose = () => transportError('EARLY_CLOSE', 'SOCKS proxy closed before tunnel establishment');
|
|
165
|
+
const onAbort = () => fail(aborted());
|
|
166
|
+
const timer = setTimeout(() => transportError('TIMEOUT', 'SOCKS establishment deadline exceeded'), timeoutMs);
|
|
167
|
+
socket.on('data', onData); socket.once('error', onError); socket.once('end', onClose); socket.once('close', onClose);
|
|
168
|
+
socket.pause();
|
|
169
|
+
signal?.addEventListener('abort', onAbort, { once: true }); if (signal?.aborted) onAbort();
|
|
170
|
+
const reader = {
|
|
171
|
+
write(bytes: Uint8Array): void { if (failure) throw failure; socket.write(bytes); },
|
|
172
|
+
async readExact(length: number): Promise<Buffer> {
|
|
173
|
+
if (failure) throw failure;
|
|
174
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > 1024) throw new TunnelError('PROTOCOL', 'Invalid handshake read size');
|
|
175
|
+
if (buffer.length < length) await new Promise<void>((resolve, reject) => {
|
|
176
|
+
if (waiting) { reject(new TunnelError('PROTOCOL', 'Concurrent handshake read')); return; }
|
|
177
|
+
waiting = { length, resolve, reject };
|
|
178
|
+
socket.resume();
|
|
179
|
+
});
|
|
180
|
+
if (failure) throw failure;
|
|
181
|
+
const result = buffer.subarray(0, length); buffer = buffer.subarray(length); return result;
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
try {
|
|
185
|
+
const credentialsUrl = new URL('socks5://fixture.invalid');
|
|
186
|
+
if (proxy.username !== undefined) { credentialsUrl.username = encodeURIComponent(proxy.username); credentialsUrl.password = encodeURIComponent(proxy.password); }
|
|
187
|
+
const colon = authority.lastIndexOf(':');
|
|
188
|
+
const target = { host: authority.slice(0, colon).replace(/^\[|\]$/g, ''), port: Number(authority.slice(colon + 1)) };
|
|
189
|
+
await socks5Handshake(reader, target, socks5Credentials(credentialsUrl), signal);
|
|
190
|
+
if (failure) throw failure;
|
|
191
|
+
if (signal?.aborted || socket.destroyed) throw aborted();
|
|
192
|
+
socket.pause(); if (buffer.length) socket.unshift(buffer); buffer = Buffer.alloc(0);
|
|
193
|
+
return socket;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
socket.destroy();
|
|
196
|
+
if (error instanceof Socks5HandshakeError) throw new TunnelError('SOCKS_REFUSED', 'SOCKS authentication, policy, or protocol refusal');
|
|
197
|
+
throw error;
|
|
198
|
+
} finally {
|
|
199
|
+
clearTimeout(timer); signal?.removeEventListener('abort', onAbort);
|
|
200
|
+
socket.removeListener('data', onData); socket.removeListener('error', onError);
|
|
201
|
+
socket.removeListener('end', onClose); socket.removeListener('close', onClose);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Establish only. There is no application data, implicit DIRECT, or replay here.
|
|
206
|
+
* HTTP/HTTPS CONNECT and SOCKS5/SOCKS5h yield an opaque raw target tunnel.
|
|
207
|
+
*/
|
|
208
|
+
export async function dialTunnel(authority: string, proxy: OpaqueProxyRoute, options: OpaqueTunnelOptions = {}): Promise<Socket> {
|
|
209
|
+
const { signal, timeoutMs = 10000, maxHeaderBytes = 65536 } = options;
|
|
210
|
+
validateAuthority(authority);
|
|
211
|
+
validateOptions(proxy, timeoutMs, maxHeaderBytes);
|
|
212
|
+
if (signal?.aborted) throw aborted();
|
|
213
|
+
// Snapshot the selected route. No alternate proxy or direct fallback exists.
|
|
214
|
+
const socket = await connectOne(authority, { ...proxy }, { signal, timeoutMs, maxHeaderBytes });
|
|
215
|
+
if (signal?.aborted || socket.destroyed) { socket.destroy(); throw aborted(); }
|
|
216
|
+
return socket;
|
|
217
|
+
}
|
package/src/lib/redact.ts
CHANGED
|
@@ -7,7 +7,7 @@ export const REDACTED_SECRET = "[REDACTED]";
|
|
|
7
7
|
* credentials over an unsafe channel (e.g. plaintext non-loopback HTTP) rather than
|
|
8
8
|
* re-deriving a narrower local list.
|
|
9
9
|
*/
|
|
10
|
-
export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|copilot[-_]?session[-_]?token|session[-_]?token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn|exa[-_]?api[-_]?key)$/i;
|
|
10
|
+
export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|x-dsh-auth-token|copilot[-_]?session[-_]?token|session[-_]?token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn|exa[-_]?api[-_]?key)$/i;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Colon-labelled credential headers echoed back inside an error body
|
|
@@ -40,7 +40,7 @@ export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cook
|
|
|
40
40
|
// Every letter position also accepts \u0001, the placeholder the fold emits for
|
|
41
41
|
// an unresolved HTML named reference: `authorⅈzation` is the label with one
|
|
42
42
|
// character we cannot name, and that is still the label.
|
|
43
|
-
const CREDENTIAL_HEADER_LABEL_RAW = "copilot[_-]?session[_-]?token|session[_-]?token|sessionToken|x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|exa[_-]?api[_-]?key|exaApiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token";
|
|
43
|
+
const CREDENTIAL_HEADER_LABEL_RAW = "copilot[_-]?session[_-]?token|session[_-]?token|sessionToken|x-api-key|x-goog-api-key|x-amz-security-token|x-dsh-auth-token|api[_-]?key|apiKey|exa[_-]?api[_-]?key|exaApiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token";
|
|
44
44
|
|
|
45
45
|
const CREDENTIAL_HEADER_LABEL = CREDENTIAL_HEADER_LABEL_RAW
|
|
46
46
|
.replace(/(?<![\[\\])([A-Za-z])(?![\]\-])/g, "[$1\u0001]");
|
|
@@ -223,6 +223,8 @@ const RECOVERY_KIND_CAUSE = {
|
|
|
223
223
|
"rate-limit-429": "rate-limit",
|
|
224
224
|
"anthropic-oauth-429": "rate-limit",
|
|
225
225
|
"oauth-account-429": "rate-limit",
|
|
226
|
+
// A 402 is the account's quota, not a transient limit; only another account can serve it.
|
|
227
|
+
"oauth-account-402": "quota-exhausted",
|
|
226
228
|
"image-413": "payload-too-large",
|
|
227
229
|
// The gateway rejects a body it accepts seconds later and the replay is byte-identical, so
|
|
228
230
|
// nothing about the payload was wrong; the origin declined to take it at that moment.
|
|
@@ -96,7 +96,7 @@ function headroomOf(provider: string, accountId: string, requestedModelId?: stri
|
|
|
96
96
|
const family = classifyModelFamilyForQuota(provider, requestedModelId);
|
|
97
97
|
if (family) {
|
|
98
98
|
const percents = (quota.customWindows ?? [])
|
|
99
|
-
.filter(window => windowMatchesFamily(window.label, family))
|
|
99
|
+
.filter(window => window.modelId === undefined && windowMatchesFamily(window.label, family))
|
|
100
100
|
.map(window => window.percent)
|
|
101
101
|
.filter((value): value is number => typeof value === "number");
|
|
102
102
|
if (percents.length === 0) return null;
|
|
@@ -106,7 +106,7 @@ function headroomOf(provider: string, accountId: string, requestedModelId?: stri
|
|
|
106
106
|
quota.fiveHourPercent,
|
|
107
107
|
quota.weeklyPercent,
|
|
108
108
|
quota.monthlyPercent,
|
|
109
|
-
...(quota.customWindows ?? []).map(window => window.percent),
|
|
109
|
+
...(quota.customWindows ?? []).filter(window => window.modelId === undefined).map(window => window.percent),
|
|
110
110
|
].filter((value): value is number => typeof value === "number");
|
|
111
111
|
if (percents.length === 0) return null;
|
|
112
112
|
return 100 - Math.max(...percents);
|
|
@@ -140,6 +140,73 @@ export function isAccountQuotaExhausted(
|
|
|
140
140
|
return headroom !== null && headroom <= 0;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Earliest future reset timestamp (in ms) for the relevant quota window, or null if unknown.
|
|
145
|
+
* Prefers weekly reset when multiple windows exist so expiring allowance is consumed first.
|
|
146
|
+
*/
|
|
147
|
+
export function accountResetTimestamp(
|
|
148
|
+
provider: string,
|
|
149
|
+
accountId: string,
|
|
150
|
+
requestedModelId?: string | null,
|
|
151
|
+
now = Date.now(),
|
|
152
|
+
): number | null {
|
|
153
|
+
const quota = getCachedProviderAccountQuota(provider, accountId);
|
|
154
|
+
if (!quota) return null;
|
|
155
|
+
const family = classifyModelFamilyForQuota(provider, requestedModelId);
|
|
156
|
+
const windows = (quota.customWindows ?? []).filter(w => !family || windowMatchesFamily(w.label, family));
|
|
157
|
+
const weekly = windows.find(w => /week/i.test(w.label));
|
|
158
|
+
let rawReset = weekly?.resetAt ?? quota.weeklyResetAt;
|
|
159
|
+
if (rawReset === undefined && provider !== "google-antigravity" && provider !== "anthropic") {
|
|
160
|
+
// Providers without a weekly cadence: prefer an explicitly 5h-labeled window before
|
|
161
|
+
// falling back to first-window order.
|
|
162
|
+
const fiveHour = windows.find(w => /5h|five/i.test(w.label)) ?? windows[0];
|
|
163
|
+
rawReset = fiveHour?.resetAt ?? quota.fiveHourResetAt;
|
|
164
|
+
} else if (rawReset === undefined) {
|
|
165
|
+
rawReset = windows[0]?.resetAt ?? quota.fiveHourResetAt;
|
|
166
|
+
}
|
|
167
|
+
if (typeof rawReset !== "number" || !Number.isFinite(rawReset)) return null;
|
|
168
|
+
const ms = rawReset < 1e11 ? rawReset * 1000 : rawReset;
|
|
169
|
+
return ms > now ? ms : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Rank candidates by soonest reset timestamp first among healthy accounts.
|
|
174
|
+
* Accounts whose quota will reset earliest get priority so allowances do not expire unused.
|
|
175
|
+
*
|
|
176
|
+
* When a passive provider has incomplete quota evidence (some ring members with no
|
|
177
|
+
* cached record), reset timestamps are unreliable: an unknown account converts to
|
|
178
|
+
* Infinity and always loses to a known near-exhausted one. Fall back to headroom
|
|
179
|
+
* order instead of inventing a reset ordering.
|
|
180
|
+
*/
|
|
181
|
+
export function rankAccountsByResetFirst(
|
|
182
|
+
provider: string,
|
|
183
|
+
ring: readonly string[],
|
|
184
|
+
requestedModelId?: string | null,
|
|
185
|
+
now = Date.now(),
|
|
186
|
+
accounts?: ReadonlyMap<string, ProviderAccount>,
|
|
187
|
+
): string[] {
|
|
188
|
+
if (ring.length < 2) return [...ring];
|
|
189
|
+
const ranked = rankAccountsByHeadroom(provider, ring, requestedModelId, accounts);
|
|
190
|
+
const healthy = ranked.filter(id => !isAccountQuotaExhausted(provider, id, requestedModelId, accounts?.get(id)));
|
|
191
|
+
if (healthy.length < 2) return ranked;
|
|
192
|
+
if (
|
|
193
|
+
hasPassiveAccountQuota(provider)
|
|
194
|
+
&& !ring.every(id => headroomOf(provider, id, requestedModelId, accounts?.get(id)) !== null)
|
|
195
|
+
) return ranked;
|
|
196
|
+
|
|
197
|
+
const withReset = healthy.map((id, index) => ({
|
|
198
|
+
id,
|
|
199
|
+
resetAt: accountResetTimestamp(provider, id, requestedModelId, now) ?? Number.POSITIVE_INFINITY,
|
|
200
|
+
headroom: headroomOf(provider, id, requestedModelId, accounts?.get(id)) ?? 0,
|
|
201
|
+
index,
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
withReset.sort((a, b) => (a.resetAt - b.resetAt) || (b.headroom - a.headroom) || (a.index - b.index));
|
|
205
|
+
const sortedHealthy = withReset.map(r => r.id);
|
|
206
|
+
const exhausted = ranked.filter(id => isAccountQuotaExhausted(provider, id, requestedModelId, accounts?.get(id)));
|
|
207
|
+
return [...sortedHealthy, ...exhausted];
|
|
208
|
+
}
|
|
209
|
+
|
|
143
210
|
/**
|
|
144
211
|
* Order candidates best-first.
|
|
145
212
|
*
|
|
@@ -31,6 +31,22 @@ export function bindAnthropicRefusalCredentialForSend(response: Response, owner:
|
|
|
31
31
|
responseCredentials.set(response, Object.freeze({ ...owner, providerAccountUuid, checkProviderUuid: arguments.length >= 3 }));
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** Preserve the original send proof across internal error normalization, without renewing it. */
|
|
35
|
+
export function carryAnthropicRefusalCredential(source: Response, target: Response): void {
|
|
36
|
+
const sent = responseCredentials.get(source);
|
|
37
|
+
if (sent) responseCredentials.set(target, sent);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Recheck after body consumption: a late refusal cannot acquire a replacement login's authority. */
|
|
41
|
+
export function anthropicRefusalCredentialIsCurrent(response: Response, instance: AnthropicInstanceId, accountId: string): boolean {
|
|
42
|
+
const sent = responseCredentials.get(response);
|
|
43
|
+
if (!sent || sent.provider !== instance || sent.accountId !== accountId
|
|
44
|
+
|| !anthropicPhysicalSendOwnershipIsCurrent(sent)) return false;
|
|
45
|
+
const row = getAccountCredentialWithStatus(instance, accountId);
|
|
46
|
+
return !!row && !row.paused && !row.needsReauth
|
|
47
|
+
&& (!sent.checkProviderUuid || row.credential.accountId === sent.providerAccountUuid);
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
async function isRevokedOAuthToken(response: Response, signal?: AbortSignal): Promise<boolean> {
|
|
35
51
|
if (response.status !== 401) return false;
|
|
36
52
|
try {
|
|
@@ -59,9 +75,11 @@ async function isAccountRefusal(response: Response, signal?: AbortSignal): Promi
|
|
|
59
75
|
// Whole-message patterns: request-policy/model/resource refusals and quoted diagnostics
|
|
60
76
|
// must not acquire account authority merely by containing entitlement keywords.
|
|
61
77
|
if ("code" in error && error.code != null
|
|
62
|
-
&& (typeof error.code !== "string" || !["subscription_required", "insufficient_quota", "permission_denied"].includes(error.code))) return false;
|
|
78
|
+
&& (typeof error.code !== "string" || !["subscription_required", "insufficient_quota", "permission_denied", "oauth_org_not_allowed"].includes(error.code))) return false;
|
|
79
|
+
// The organization disabled subscription access: an account-scoped verdict, cooled like its siblings.
|
|
80
|
+
if (error.type === "oauth_org_not_allowed" || ("code" in error && error.code === "oauth_org_not_allowed")) return true;
|
|
63
81
|
const message = error.message.trim();
|
|
64
|
-
if (error.type === "permission_error" && /^(?:Your account does not have access to Claude Code|Your (?:Claude )?subscription (?:has expired|is (?:expired|inactive))|Your account does not have an active subscription)[.!]?$/i.test(message)) return true;
|
|
82
|
+
if (error.type === "permission_error" && /^(?:Your account does not have access to Claude Code|Your (?:Claude )?subscription (?:has expired|is (?:expired|inactive))|Your account does not have an active subscription|(?:Your|This) organization has disabled Claude subscription access for Claude Code(?:\. Use an Anthropic API key instead, or ask your admin to enable access)?)[.!]?$/i.test(message)) return true;
|
|
65
83
|
return (error.type === "billing_error" || error.type === "permission_error")
|
|
66
84
|
&& /^Your credit balance is too low to access the Anthropic API\.(?: Please go to Plans & Billing to upgrade or purchase credits\.)?$/i.test(message);
|
|
67
85
|
} catch {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ConfigMutationLockError } from "../config";
|
|
2
2
|
import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
|
|
3
3
|
import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
|
|
4
|
-
import { loginAnthropic, refreshAnthropicToken } from "./anthropic";
|
|
4
|
+
import { ANTHROPIC_CODE_REDIRECT_URI, loginAnthropic, refreshAnthropicToken } from "./anthropic";
|
|
5
5
|
import type { OAuthProviderDef } from "./index";
|
|
6
6
|
import {
|
|
7
7
|
clearOAuthRefreshIntent, clearOAuthRefreshIntentIfMatch, markOAuthRefreshIntentCleanupPending,
|
|
@@ -16,8 +16,10 @@ export function anthropicOAuthDefinition(instance: AnthropicInstanceId): OAuthPr
|
|
|
16
16
|
return {
|
|
17
17
|
login: (ctrl, opts) => loginAnthropic(ctrl, {
|
|
18
18
|
instance, importLocal: instance === "anthropic2" || opts?.forceLogin ? "off" : "fallback",
|
|
19
|
+
codeMode: opts?.codeMode === true,
|
|
19
20
|
}),
|
|
20
21
|
refresh: refreshAnthropicToken,
|
|
22
|
+
codeRedirectUri: ANTHROPIC_CODE_REDIRECT_URI,
|
|
21
23
|
providerConfig,
|
|
22
24
|
defaultModel,
|
|
23
25
|
// Subscription OAuth must never generate unattended traffic by default.
|