cartethyia 2.7.4 → 2.7.5
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/.env.example +2 -0
- package/README.md +17 -5
- package/dist/dashboard/assets/{BannedPage-ChG5svFb.js → BannedPage-Cj15GsMp.js} +1 -1
- package/dist/dashboard/assets/BannedPage-Cj15GsMp.js.gz +0 -0
- package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js +1 -0
- package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-BIcWQun7.js → CliToolDetailPage-DZKmVuJN.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-Bf2VelDe.js → CliToolsPage-BIccZmeL.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DEEKkYD-.js +1 -0
- package/dist/dashboard/assets/CombosPage-DEEKkYD-.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-ggeMSjte.js → CustomizationPage-BKj_r0rp.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js +656 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-U6AKaJcF.js → LoginPage-DkXb6dx-.js} +1 -1
- package/dist/dashboard/assets/LoginPage-DkXb6dx-.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BMS2t8BA.js → ModelPicker-DnQPSIA8.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-DnQPSIA8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js +1 -0
- package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js +5 -0
- package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-lsrx7NYg.js → ProxyPage-5SnUSy3j.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js +1 -0
- package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js +1 -0
- package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-f0DE1h0-.js → SetupPage-WTa0sktC.js} +1 -1
- package/dist/dashboard/assets/SetupPage-WTa0sktC.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-Cq5tW7hW.js → StudioPage-BfAb5-o_.js} +3 -3
- package/dist/dashboard/assets/StudioPage-BfAb5-o_.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js +56 -0
- package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-l29JROPv.js +1 -0
- package/dist/dashboard/assets/api-keys-l29JROPv.js.gz +0 -0
- package/dist/dashboard/assets/app--PJWq8_C.css +2 -0
- package/dist/dashboard/assets/app--PJWq8_C.css.gz +0 -0
- package/dist/dashboard/assets/assert-C0K-c4eo.js +1 -0
- package/dist/dashboard/assets/assert-C0K-c4eo.js.gz +0 -0
- package/dist/dashboard/assets/{badge-D9CS6dZk.js → badge-DfbhQU4k.js} +1 -1
- package/dist/dashboard/assets/badge-DfbhQU4k.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DympBNq4.js → banner-BllT4LcF.js} +1 -1
- package/dist/dashboard/assets/banner-BllT4LcF.js.gz +0 -0
- package/dist/dashboard/assets/brand/login_register.jpg +0 -0
- package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
- package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
- package/dist/dashboard/assets/brand/og_image.jpg +0 -0
- package/dist/dashboard/assets/{card-ibRWU_uT.js → card-CtkX66Wd.js} +1 -1
- package/dist/dashboard/assets/card-CtkX66Wd.js.gz +0 -0
- package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js +1 -0
- package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CQXVMn4P.js +1 -0
- package/dist/dashboard/assets/cli-tools-CQXVMn4P.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DJm-9pz7.js → clipboard-button-BQqBQd2n.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-BQqBQd2n.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-8fiE6tec.js → dialog-BEK7KP0H.js} +1 -1
- package/dist/dashboard/assets/dialog-BEK7KP0H.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CqGYeOrd.js → drawer-BrTEZ8zu.js} +1 -1
- package/dist/dashboard/assets/drawer-BrTEZ8zu.js.gz +0 -0
- package/dist/dashboard/assets/entry-CHWxbS56.js +1 -0
- package/dist/dashboard/assets/entry-CHWxbS56.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BSAsYsmV.js → entry-EBHJ3DrE.js} +1 -1
- package/dist/dashboard/assets/entry-EBHJ3DrE.js.gz +0 -0
- package/dist/dashboard/assets/entry-LCS85hiq.js +4 -0
- package/dist/dashboard/assets/entry-LCS85hiq.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-D-MTJxMr.js → formatters-BAACA727.js} +1 -1
- package/dist/dashboard/assets/formatters-BAACA727.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-CPK47xQx.js → github-badge-YSCE4UC2.js} +1 -1
- package/dist/dashboard/assets/github-badge-YSCE4UC2.js.gz +0 -0
- package/dist/dashboard/assets/{index-DBNPOfhV.js → index-FQlj0HUT.js} +2 -2
- package/dist/dashboard/assets/index-FQlj0HUT.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BT0zntEr.js → inline-C4gU5iOL.js} +1 -1
- package/dist/dashboard/assets/inline-C4gU5iOL.js.gz +0 -0
- package/dist/dashboard/assets/{input-Da2ZViWg.js → input-BHHX_Tmm.js} +1 -1
- package/dist/dashboard/assets/input-BHHX_Tmm.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BXNeLw6M.js → layout-CajrYUnD.js} +1 -1
- package/dist/dashboard/assets/layout-CajrYUnD.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BycOrrIV.js → list-row-Bx2I7XUe.js} +1 -1
- package/dist/dashboard/assets/list-row-Bx2I7XUe.js.gz +0 -0
- package/dist/dashboard/assets/live-DNNqRbj2.js +1 -0
- package/dist/dashboard/assets/live-DNNqRbj2.js.gz +0 -0
- package/dist/dashboard/assets/network-C5UBACrJ.js +1 -0
- package/dist/dashboard/assets/network-C5UBACrJ.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CUWAZK4J.js +2 -0
- package/dist/dashboard/assets/numbers-CUWAZK4J.js.gz +0 -0
- package/dist/dashboard/assets/pagination-Xsi9Qx04.js +1 -0
- package/dist/dashboard/assets/pagination-Xsi9Qx04.js.gz +0 -0
- package/dist/dashboard/assets/quota-DvQbD_sr.js +1 -0
- package/dist/dashboard/assets/quota-DvQbD_sr.js.gz +0 -0
- package/dist/dashboard/assets/routing-in0f_IfK.js +1 -0
- package/dist/dashboard/assets/routing-in0f_IfK.js.gz +0 -0
- package/dist/dashboard/assets/{select-BIYIBkMU.js → select-BLULNqTp.js} +1 -1
- package/dist/dashboard/assets/select-BLULNqTp.js.gz +0 -0
- package/dist/dashboard/assets/settings-CcGczj6I.js +1 -0
- package/dist/dashboard/assets/settings-CcGczj6I.js.gz +0 -0
- package/dist/dashboard/assets/shield-BMQNxORh.js +1 -0
- package/dist/dashboard/assets/shield-BMQNxORh.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DfgnFDWY.js → stack-D2lIR0tg.js} +1 -1
- package/dist/dashboard/assets/stack-D2lIR0tg.js.gz +0 -0
- package/dist/dashboard/assets/{state-CkfdYWel.js → state-DTARjfiW.js} +1 -1
- package/dist/dashboard/assets/state-DTARjfiW.js.gz +0 -0
- package/dist/dashboard/assets/{switch-SYm237HG.js → switch-c0oPSnqW.js} +1 -1
- package/dist/dashboard/assets/switch-c0oPSnqW.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BQqCRNIi.js → useMutation-m8WGNPrz.js} +1 -1
- package/dist/dashboard/assets/useMutation-m8WGNPrz.js.gz +0 -0
- package/dist/dashboard/console-backgrounds.jpg +0 -0
- package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +56 -55
- package/migrations/full-postgres/0036_system_prompt_presets_refs.sql +10 -0
- package/migrations/full-postgres/0037_share_concurrency_scope.sql +3 -0
- package/migrations/full-postgres/0038_add_accounts_quota_snapshot.sql +7 -0
- package/migrations/full-postgres/0039_add_telemetry_provider_model_indexes.sql +13 -0
- package/migrations/full-postgres/0040_add_models_dialect.sql +3 -0
- package/migrations/full-postgres/0041_add_daily_checkin_ledger_account_fk.sql +7 -0
- package/migrations/lite-sqlite3/0036_system_prompt_presets_refs.sql +14 -0
- package/migrations/lite-sqlite3/0037_share_concurrency_scope.sql +3 -0
- package/migrations/lite-sqlite3/0038_add_accounts_quota_snapshot.sql +4 -0
- package/migrations/lite-sqlite3/0039_add_telemetry_provider_model_indexes.sql +6 -0
- package/migrations/lite-sqlite3/0040_add_models_dialect.sql +3 -0
- package/migrations/lite-sqlite3/0041_add_daily_checkin_ledger_account_fk.sql +26 -0
- package/package.json +4 -6
- package/scripts/cli/update.ts +15 -41
- package/src/boot/app.ts +58 -2
- package/src/boot/dependencies.ts +786 -732
- package/src/boot/lifecycle.ts +11 -12
- package/src/config/cooldowns.ts +34 -55
- package/src/config/network.ts +1 -1
- package/src/config/package-paths.ts +1 -1
- package/src/config/providers.ts +13 -14
- package/src/config/resources.ts +0 -5
- package/src/config/runtime.ts +2 -5
- package/src/config/spec.ts +26 -6
- package/src/console/api-keys/ban-constants.ts +38 -0
- package/src/console/api-keys/contracts.ts +71 -62
- package/src/console/api-keys/routes.ts +48 -12
- package/src/console/auth/access.ts +4 -4
- package/src/console/auth/account-reset.ts +1 -1
- package/src/console/auth/audit-recorder.ts +1 -1
- package/src/console/auth/lockout.ts +7 -3
- package/src/console/auth/row-coercion.ts +57 -12
- package/src/console/auth/session-resolver.ts +1 -1
- package/src/console/auth/session-service.ts +8 -2
- package/src/console/auth/session.ts +37 -31
- package/src/console/auth/setup-service.ts +4 -3
- package/src/console/auth/user-store.ts +1 -1
- package/src/console/backup/9router-map.ts +0 -3
- package/src/console/backup/contracts.ts +9 -9
- package/src/console/backup/export-endpoint.ts +5 -6
- package/src/console/backup/nine-router.ts +1 -7
- package/src/console/backup/routes.ts +29 -33
- package/src/console/backup/service.ts +468 -468
- package/src/console/backup/store.ts +47 -29
- package/src/console/backup/validate.ts +11 -1
- package/src/console/cli-tools/contracts.ts +7 -7
- package/src/console/cli-tools/injectors/cline.ts +110 -110
- package/src/console/cli-tools/injectors/codex.ts +181 -181
- package/src/console/cli-tools/injectors/copilot.ts +108 -108
- package/src/console/cli-tools/injectors/driver.ts +247 -247
- package/src/console/cli-tools/injectors/jcode.ts +115 -115
- package/src/console/cli-tools/injectors/openclaw.ts +161 -161
- package/src/console/cli-tools/injectors/opencode.ts +153 -153
- package/src/console/cli-tools/routes.ts +1 -1
- package/src/console/cli-tools/service.ts +1 -1
- package/src/console/cli-tools/store.ts +2 -2
- package/src/console/console-router.ts +32 -0
- package/src/console/dashboard-assets.ts +9 -9
- package/src/console/domain-registration.ts +16 -2
- package/src/console/inferhub/client.ts +0 -7
- package/src/console/inferhub/key-store.ts +1 -1
- package/src/console/inferhub/normalize.ts +2 -2
- package/src/console/inferhub/routes.ts +1 -1
- package/src/console/inferhub/types.ts +1 -1
- package/src/console/observability/contracts.ts +1 -1
- package/src/console/observability/live.ts +31 -18
- package/src/console/observability/logs.ts +1 -1
- package/src/console/observability/sse.ts +62 -8
- package/src/console/observability/store.ts +20 -4
- package/src/console/providers/catalog/catalog-projections.ts +1 -1
- package/src/console/providers/catalog/contracts.ts +39 -0
- package/src/console/providers/catalog/model-operations.ts +1 -1
- package/src/console/providers/catalog/provider-operations.ts +95 -17
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/routes.ts +27 -3
- package/src/console/providers/catalog/store.ts +147 -8
- package/src/console/providers/detail/routes.ts +1 -1
- package/src/console/providers/oauth/callback-listener.ts +100 -18
- package/src/console/providers/oauth/routes.ts +92 -13
- package/src/console/quota/account-quota-global.ts +41 -3
- package/src/console/quota/account-quota-shared.ts +10 -3
- package/src/console/quota/account-quota-tenant.ts +259 -66
- package/src/console/quota/daily-growth-job.ts +308 -0
- package/src/console/quota/native-identity.ts +2 -2
- package/src/console/quota/quota-view.ts +1 -1
- package/src/console/remote-support/routes.ts +1 -1
- package/src/console/remote-support/service.ts +2 -2
- package/src/console/routing/model/contracts.ts +1 -1
- package/src/console/routing/pools/contracts.ts +2 -2
- package/src/console/routing/pools/import-parse.ts +3 -3
- package/src/console/routing/pools/relay-deploy.ts +3 -3
- package/src/console/settings/contracts.ts +29 -2
- package/src/console/settings/store.ts +20 -0
- package/src/console/share/share-router.ts +100 -32
- package/src/console/share/share-stats.ts +4 -4
- package/src/console/share/share-usage.ts +1 -1
- package/src/console/shared/errors.ts +2 -2
- package/src/console/shared/object.ts +18 -0
- package/src/console/shared/read-through-cache.ts +4 -4
- package/src/console/shared/transaction.ts +14 -0
- package/src/console/system-prompt/contracts.ts +89 -12
- package/src/console/system-prompt/routes.ts +98 -45
- package/src/console/updates/apply-update.ts +244 -0
- package/src/console/updates/routes.ts +252 -97
- package/src/main.ts +11 -5
- package/src/network/http2-bidi.ts +1 -1
- package/src/network/ip-utils.ts +298 -0
- package/src/network/outbound-fetch.ts +1 -1
- package/src/network/pool/agent.ts +7 -6
- package/src/network/pool/byte-accounting.ts +1 -1
- package/src/network/pool/resolver.ts +16 -12
- package/src/network/pool/selector.ts +150 -136
- package/src/network/pool-health-machine.ts +1 -1
- package/src/network/rate-limit-verdict.ts +1 -1
- package/src/network/retry.ts +1 -1
- package/src/network/ssrf.ts +20 -71
- package/src/observability/event-log.ts +2 -2
- package/src/observability/event-query.ts +59 -291
- package/src/observability/log-sanitize.ts +1 -1
- package/src/observability/metrics.ts +4 -4
- package/src/observability/payload-capture.ts +6 -5
- package/src/observability/payload-store.ts +1 -1
- package/src/observability/runtime-metrics.ts +1 -1
- package/src/observability/telemetry-buffer.ts +12 -3
- package/src/observability/telemetry-reconciliation.ts +3 -3
- package/src/observability/token-speed.ts +1 -1
- package/src/persistence/connection-url.ts +1 -1
- package/src/persistence/db-handle.ts +1 -1
- package/src/persistence/db-pg.ts +2 -2
- package/src/persistence/db-sqlite.ts +3 -3
- package/src/persistence/dialect.ts +5 -1
- package/src/persistence/readiness.ts +1 -1
- package/src/persistence/redis.ts +1 -1
- package/src/persistence/schema.ts +113 -40
- package/src/persistence/shared-cache.ts +2 -2
- package/src/persistence/sort-order.ts +1 -1
- package/src/persistence/stores/api-key-store.ts +10 -1
- package/src/persistence/stores/share-store.ts +20 -8
- package/src/persistence/system-prompt-store.ts +63 -18
- package/src/persistence/tenant-preferences.ts +1 -1
- package/src/persistence/ttl-cache.ts +3 -3
- package/src/persistence/usage-flush.ts +7 -2
- package/src/protocol/canonical-model.ts +1 -1
- package/src/protocol/codecs.ts +6 -10
- package/src/protocol/dsml.ts +3 -49
- package/src/protocol/image-clamp.ts +2 -2
- package/src/protocol/reasoning-text.ts +1 -1
- package/src/protocol/reasoning.ts +7 -52
- package/src/protocol/registry.ts +50 -15
- package/src/protocol/request/chat.ts +46 -19
- package/src/protocol/request/codex.ts +1 -1
- package/src/protocol/request/messages.ts +46 -14
- package/src/protocol/request/responses.ts +19 -10
- package/src/protocol/response/chat.ts +25 -10
- package/src/protocol/response/codex.ts +1 -1
- package/src/protocol/response/messages.ts +1 -1
- package/src/protocol/response/omp-tool-call-markup.ts +1 -1
- package/src/protocol/response/responses.ts +1 -1
- package/src/protocol/sse-decoder.ts +50 -1
- package/src/protocol/stop-reason.ts +2 -2
- package/src/protocol/think-split.ts +347 -0
- package/src/protocol/translation/attachment-integrity.ts +3 -3
- package/src/protocol/translation/cache-controls.ts +8 -20
- package/src/protocol/translation/capabilities.ts +6 -6
- package/src/protocol/translation/dialect/deepseek.ts +10 -0
- package/src/protocol/translation/dialect/inband.ts +39 -0
- package/src/protocol/translation/dialect/model-thinking.ts +89 -0
- package/src/protocol/translation/dialect/qwen.ts +114 -0
- package/src/protocol/translation/dialect/resolver.ts +83 -0
- package/src/protocol/translation/dialect/rules.ts +93 -0
- package/src/protocol/translation/dialect/thinking-ladder.ts +245 -0
- package/src/protocol/translation/dialect/types.ts +58 -0
- package/src/protocol/translation/quirks.ts +18 -4
- package/src/protocol/translation/thinking.ts +12 -252
- package/src/protocol/translation/tool-id.ts +3 -3
- package/src/protocol/translation/tool-repair.ts +1 -1
- package/src/protocol/upstream/post-json.ts +6 -1
- package/src/providers/adapters/compatible-adapter.ts +42 -3
- package/src/providers/authentication/jwt-validator.ts +0 -5
- package/src/providers/authentication/oauth-client.ts +254 -253
- package/src/providers/authentication/oauth-flow-store.ts +42 -7
- package/src/providers/bundled-discovery-providers.ts +6 -30
- package/src/providers/configured-openai-providers.ts +82 -97
- package/src/providers/default-registry.ts +23 -35
- package/src/providers/discovery/catalog/catalog-format.ts +13 -2
- package/src/providers/discovery/discovery-types.ts +0 -1
- package/src/providers/discovery/openai-model-discovery.ts +2 -2
- package/src/providers/discovery/probe-phases.ts +48 -13
- package/src/providers/discovery/probing-service.ts +6 -8
- package/src/providers/integrations/agentrouter/agentrouter.ts +12 -2
- package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +0 -14
- package/src/providers/integrations/antigravity/antigravity.ts +2 -2
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +20 -4
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +19 -52
- package/src/providers/integrations/buddy/buddy-identity.ts +86 -0
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +1 -1
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +1 -2
- package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -15
- package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
- package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -9
- package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
- package/src/providers/integrations/claude/claude-cch.ts +148 -148
- package/src/providers/integrations/claude/fallback-credit.ts +3 -7
- package/src/providers/integrations/cline/cline.ts +1 -1
- package/src/providers/integrations/codex/codex-device-code.ts +101 -101
- package/src/providers/integrations/codex/codex-oauth.ts +0 -1
- package/src/providers/integrations/commandcode/commandcode.ts +75 -115
- package/src/providers/integrations/commandcode/quota.ts +131 -51
- package/src/providers/integrations/cursor/cursor-http2.ts +0 -3
- package/src/providers/integrations/cursor/cursor-quota.ts +2 -6
- package/src/providers/integrations/cursor/cursor-request.ts +0 -87
- package/src/providers/integrations/cursor/cursor-shared.ts +0 -4
- package/src/providers/integrations/cursor/cursor.ts +38 -5
- package/src/providers/integrations/cursor/protocol/framing.ts +0 -56
- package/src/providers/integrations/cursor/protocol/messages.ts +0 -96
- package/src/providers/integrations/factory/factory.ts +9 -9
- package/src/providers/integrations/github/github-discovery.ts +55 -10
- package/src/providers/integrations/github/github-oauth.ts +6 -5
- package/src/providers/integrations/github/github.ts +41 -14
- package/src/providers/integrations/grok/grok-oauth.ts +261 -261
- package/src/providers/integrations/grok/grok-quota-frame.ts +1 -1
- package/src/providers/integrations/grok/grok-quota.ts +16 -7
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +0 -5
- package/src/providers/integrations/grok/grok-ratelimit.ts +3 -0
- package/src/providers/integrations/grok/grok.ts +86 -36
- package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
- package/src/providers/integrations/kiro/kiro-cache.ts +31 -0
- package/src/providers/integrations/kiro/kiro-catalog.ts +31 -5
- package/src/providers/integrations/kiro/kiro-discovery.ts +47 -43
- package/src/providers/integrations/kiro/kiro-machine-id.ts +6 -3
- package/src/providers/integrations/kiro/kiro-oauth.ts +523 -255
- package/src/providers/integrations/kiro/kiro-quota.ts +35 -47
- package/src/providers/integrations/kiro/kiro-request.ts +8 -11
- package/src/providers/integrations/kiro/kiro-stream.ts +13 -0
- package/src/providers/integrations/kiro/kiro.ts +81 -68
- package/src/providers/integrations/muse/muse-oauth.ts +0 -1
- package/src/providers/integrations/muse/muse-quota.ts +0 -4
- package/src/providers/integrations/opencode/fingerprint.ts +7 -5
- package/src/providers/integrations/opencode/provider.ts +17 -24
- package/src/providers/integrations/qoder/qoder-catalog.ts +85 -8
- package/src/providers/integrations/qoder/qoder-oauth.ts +165 -25
- package/src/providers/integrations/qoder/qoder-quota.ts +48 -6
- package/src/providers/integrations/qoder/qoder-realm.ts +10 -31
- package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +21 -25
- package/src/providers/integrations/qoder/qoder-virtual.ts +12 -0
- package/src/providers/integrations/qoder/qoder.ts +354 -62
- package/src/providers/integrations/shared/connect.ts +0 -54
- package/src/providers/integrations/shared/hash.ts +15 -0
- package/src/providers/integrations/shared/session-cache.ts +136 -0
- package/src/providers/integrations/trae/trae-cache.ts +7 -87
- package/src/providers/integrations/trae/trae-catalog.ts +1 -6
- package/src/providers/integrations/trae/trae-protocol.ts +17 -8
- package/src/providers/integrations/trae/trae-realm.ts +0 -7
- package/src/providers/integrations/trae/trae.ts +53 -10
- package/src/providers/integrations/xai/xai-oauth.ts +5 -5
- package/src/providers/integrations/zcode-start-plan/gifts.ts +265 -0
- package/src/providers/integrations/zcode-start-plan/oauth.ts +154 -0
- package/src/providers/integrations/zcode-start-plan/provider.ts +131 -0
- package/src/providers/integrations/zcode-start-plan/quota.ts +152 -0
- package/src/providers/integrations/zcode-start-plan/zcode_prompt.json +10 -0
- package/src/providers/operations/account-error-classification.ts +131 -116
- package/src/providers/operations/account-health-service.ts +31 -39
- package/src/providers/operations/account-reset-service.ts +58 -1
- package/src/providers/operations/cli-platform.ts +31 -5
- package/src/providers/operations/client-version-resolver.ts +4 -6
- package/src/providers/operations/client-versions.ts +10 -7
- package/src/providers/operations/provider-catalog-seeder.ts +2 -0
- package/src/providers/operations/provider-catalog-service.ts +18 -2
- package/src/providers/operations/provider-credential-service.ts +19 -2
- package/src/providers/provider-contracts.ts +18 -7
- package/src/providers/provider-metadata.ts +17 -2
- package/src/providers/provider-model-definition.ts +22 -1
- package/src/providers/provider-probe.ts +4 -2
- package/src/providers/provider-usage.ts +81 -16
- package/src/providers/quota/cache.ts +29 -0
- package/src/providers/quota/quota-contracts.ts +4 -0
- package/src/providers/quota/refresh.ts +27 -4
- package/src/providers/quota/snapshot.ts +94 -0
- package/src/providers/search/search-provider.ts +1 -3
- package/src/providers/search/search-providers.ts +2 -1
- package/src/security/abuse.ts +4 -4
- package/src/security/admission/service.ts +2 -2
- package/src/security/admission/ttl.ts +61 -61
- package/src/security/api-key-auth.ts +16 -4
- package/src/security/client-router-fingerprint.ts +4 -4
- package/src/security/credential-codec.ts +3 -3
- package/src/security/csrf.ts +2 -2
- package/src/security/ip-boundary.ts +16 -189
- package/src/security/model-access-rule.ts +1 -1
- package/src/security/outbound-headers.ts +18 -0
- package/src/shared/invoke-guard.ts +47 -0
- package/src/transport/contracts.ts +13 -4
- package/src/transport/dispatch/attempt-finalize.ts +1 -1
- package/src/transport/dispatch/capture-policy.ts +34 -37
- package/src/transport/dispatch/empty-stream.ts +23 -20
- package/src/transport/dispatch/generation.ts +25 -45
- package/src/transport/dispatch/native-shared.ts +127 -0
- package/src/transport/dispatch/proxy-request.ts +3 -1
- package/src/transport/dispatch/responses-compact.ts +25 -46
- package/src/transport/dispatch/streaming/encoder.ts +18 -72
- package/src/transport/dispatch/streaming/finalize.ts +2 -2
- package/src/transport/dispatch/streaming/stream.ts +7 -3
- package/src/transport/dispatch/streaming/thinking-loop.ts +1 -1
- package/src/transport/dispatch/systemone.ts +20 -48
- package/src/transport/dispatch/token-count.ts +41 -38
- package/src/transport/dispatch/websearch.ts +19 -45
- package/src/transport/drain-endpoint.ts +1 -1
- package/src/transport/failure-policy.ts +6 -0
- package/src/transport/guards/auth-guard.ts +0 -6
- package/src/transport/guards/body-policy.ts +3 -3
- package/src/transport/guards/headroom-guard.ts +1 -1
- package/src/transport/guards/pipeline.ts +1 -1
- package/src/transport/guards/request-context.ts +12 -17
- package/src/transport/request/preparer.ts +286 -7
- package/src/transport/request/rtk/compress-request.ts +3 -3
- package/src/transport/request/rtk/rtk-constants.ts +2 -2
- package/src/transport/request/rtk/rtk-filters.ts +8 -8
- package/src/transport/request/state.ts +1 -13
- package/src/transport/request/system-prompt-injection.ts +74 -11
- package/src/transport/request/tenant-preferences.ts +22 -0
- package/src/transport/routing/candidate-index.ts +2 -2
- package/src/transport/routing/round-robin.ts +1 -1
- package/src/transport/routing/route-catalog.ts +17 -18
- package/src/transport/routing/route-model.ts +126 -33
- package/src/transport/routing/router.ts +152 -37
- package/src/transport/shutdown-notice.ts +2 -2
- package/src/transport/surface/adapters.ts +3 -3
- package/src/transport/surface/chat/encode.ts +7 -7
- package/src/transport/surface/chat/parse.ts +7 -7
- package/src/transport/surface/completion.ts +2 -2
- package/src/transport/surface/content-parts.ts +1 -1
- package/src/transport/surface/dialects.ts +2 -2
- package/src/transport/surface/messages/parse.ts +3 -3
- package/src/transport/surface/responses/adapter.ts +1 -1
- package/src/transport/surface/responses/errors.ts +0 -9
- package/src/transport/vision-handoff-cache.ts +58 -0
- package/src/transport/vision-handoff.ts +376 -0
- package/src/workers/account-identity-sweep.ts +2 -2
- package/src/workers/buddy-growth-tasks.ts +392 -346
- package/src/workers/daily-checkin.ts +102 -49
- package/src/workers/maintenance-lease.ts +2 -35
- package/src/workers/oauth-refresh-sweep.ts +8 -1
- package/src/workers/quota-refresh-sweep.ts +120 -82
- package/src/workers/scheduler.ts +67 -4
- package/src/workers/sweep-runner.ts +24 -5
- package/src/workers/zcode-gift-claim.ts +108 -0
- package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js +0 -1
- package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
- package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js +0 -1
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +0 -1
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +0 -5
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js +0 -1
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-ClzpwyET.js +0 -1
- package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +0 -56
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js +0 -1
- package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
- package/dist/dashboard/assets/app-a159k0ub.css +0 -2
- package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js +0 -15
- package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
- package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
- package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
- package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js +0 -1
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
- package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
- package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js +0 -4
- package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js +0 -1
- package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
- package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
- package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
- package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
- package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
- package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
- package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js +0 -1
- package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js +0 -1
- package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js +0 -2
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js +0 -1
- package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js +0 -1
- package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
- package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js +0 -1
- package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
- package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
- package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js +0 -1
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
- package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
- package/src/network/response-headers.ts +0 -39
- package/src/protocol/response/dsml.ts +0 -38
- package/src/providers/integrations/xiaomi-mimo/think-stream.ts +0 -229
- /package/dist/dashboard/assets/{app-BayTTy01.js → app-Bgd3zANQ.js} +0 -0
- /package/dist/dashboard/assets/{app-BayTTy01.js.gz → app-Bgd3zANQ.js.gz} +0 -0
- /package/dist/dashboard/assets/brand/{og_bansos.webp → og_bansos_initial.webp} +0 -0
- /package/dist/dashboard/assets/brand/{og_image.webp → og_image_initial.webp} +0 -0
|
@@ -10,7 +10,7 @@ import { globalOrOwnedBy, ownedByOnly } from "../../../persistence/tenant-scope"
|
|
|
10
10
|
import { nextSortIndex, reorderSortIndex } from "../../../persistence/sort-order";
|
|
11
11
|
import type { CartethyiaDatabase } from "../../../persistence/db-handle";
|
|
12
12
|
import { buddyTaskLedger, models, networkPools, providerAccounts, providerOauthStates, providerPoolAccounts, providers, telemetryEvents, telemetryUsageTotals, tenantDisabledModels } from "../../../persistence/schema";
|
|
13
|
-
import {
|
|
13
|
+
import { IS_GATEWAY_ERROR_SQL } from "../../../observability/event-query";
|
|
14
14
|
import type { WireFamily } from "../../../protocol/canonical-model";
|
|
15
15
|
import { listAccountHealthEvents, recoverAccount, deleteAccountHealthEvents, type AccountHealthEventRecord } from "../../../providers/operations/account-health-service";
|
|
16
16
|
import { invalidateCredentialCache } from "../../../providers/operations/provider-credential-service";
|
|
@@ -679,15 +679,13 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
|
|
|
679
679
|
rows: readonly (typeof providerAccounts.$inferSelect)[],
|
|
680
680
|
): Promise<readonly ProviderAccountResponse[]> {
|
|
681
681
|
if (rows.length === 0) return [];
|
|
682
|
-
const todayStart = new Date();
|
|
683
|
-
todayStart.setUTCHours(0, 0, 0, 0);
|
|
684
682
|
const accountIds = rows.map((row) => row.id);
|
|
685
683
|
// Today's per-account usage is one GROUP BY, not a read of every event
|
|
686
|
-
// since midnight.
|
|
687
|
-
//
|
|
688
|
-
//
|
|
689
|
-
//
|
|
690
|
-
const todayGroups = await
|
|
684
|
+
// since midnight. The read is scoped to the accounts on this page via the
|
|
685
|
+
// `(tenant, account, created_at)` index — a full-tenant breakdown scanned
|
|
686
|
+
// every of today's events and capped at 100 groups, so an account past
|
|
687
|
+
// the cap silently read as "no usage today".
|
|
688
|
+
const todayGroups = await this.accountTodayUsage(tenantId, accountIds);
|
|
691
689
|
const todayByAccount = new Map(
|
|
692
690
|
todayGroups.map((group) => [
|
|
693
691
|
group.key,
|
|
@@ -740,6 +738,52 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
|
|
|
740
738
|
});
|
|
741
739
|
}
|
|
742
740
|
|
|
741
|
+
/**
|
|
742
|
+
* Today's per-account usage for a concrete id set. Scoped to those ids so
|
|
743
|
+
* the `(tenant, account, created_at)` index serves the range instead of a
|
|
744
|
+
* full-tenant window scan; returns exactly one group per account with
|
|
745
|
+
* traffic, so an account is never lost to a top-N cap.
|
|
746
|
+
*/
|
|
747
|
+
private async accountTodayUsage(
|
|
748
|
+
tenantId: string,
|
|
749
|
+
accountIds: readonly string[],
|
|
750
|
+
): Promise<
|
|
751
|
+
Array<{ key: string; requests: number; errors: number; inputTokens: number; outputTokens: number }>
|
|
752
|
+
> {
|
|
753
|
+
if (accountIds.length === 0) return [];
|
|
754
|
+
const todayStart = new Date();
|
|
755
|
+
todayStart.setUTCHours(0, 0, 0, 0);
|
|
756
|
+
const rows = await this.db
|
|
757
|
+
.select({
|
|
758
|
+
accountId: telemetryEvents.accountId,
|
|
759
|
+
requests: sql<number>`count(*)`,
|
|
760
|
+
errors: sql<number>`sum(case when ${IS_GATEWAY_ERROR_SQL} then 1 else 0 end)`,
|
|
761
|
+
inputTokens: sql<number>`coalesce(sum(${telemetryEvents.inputTokens}), 0)`,
|
|
762
|
+
outputTokens: sql<number>`coalesce(sum(${telemetryEvents.outputTokens}), 0)`,
|
|
763
|
+
})
|
|
764
|
+
.from(telemetryEvents)
|
|
765
|
+
.where(
|
|
766
|
+
and(
|
|
767
|
+
eq(telemetryEvents.tenantId, tenantId),
|
|
768
|
+
gte(telemetryEvents.createdAt, todayStart),
|
|
769
|
+
inArray(telemetryEvents.accountId, accountIds),
|
|
770
|
+
),
|
|
771
|
+
)
|
|
772
|
+
.groupBy(telemetryEvents.accountId);
|
|
773
|
+
return rows.flatMap((row) => {
|
|
774
|
+
if (row.accountId === null) return [];
|
|
775
|
+
return [
|
|
776
|
+
{
|
|
777
|
+
key: row.accountId,
|
|
778
|
+
requests: Number(row.requests),
|
|
779
|
+
errors: Number(row.errors),
|
|
780
|
+
inputTokens: Number(row.inputTokens),
|
|
781
|
+
outputTokens: Number(row.outputTokens),
|
|
782
|
+
},
|
|
783
|
+
];
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
743
787
|
private async accountInflightFor(
|
|
744
788
|
tenantId: string,
|
|
745
789
|
providerId: string | undefined,
|
|
@@ -873,6 +917,27 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
|
|
|
873
917
|
return [...accounts, ...poolAccounts];
|
|
874
918
|
}
|
|
875
919
|
|
|
920
|
+
/**
|
|
921
|
+
* One cheap id-only read of an account set, for callers that need the set
|
|
922
|
+
* (e.g. the routing-inflight reader) without the usage aggregates
|
|
923
|
+
* `listAccounts` computes (P-02). No ORDER BY: the set is order-insensitive.
|
|
924
|
+
*/
|
|
925
|
+
async listAccountIds(
|
|
926
|
+
tenantId: string,
|
|
927
|
+
providerId: string,
|
|
928
|
+
): Promise<readonly string[]> {
|
|
929
|
+
const rows = await this.db
|
|
930
|
+
.select({ id: providerAccounts.id })
|
|
931
|
+
.from(providerAccounts)
|
|
932
|
+
.where(
|
|
933
|
+
and(
|
|
934
|
+
eq(providerAccounts.providerId, providerId),
|
|
935
|
+
globalOrOwnedBy(providerAccounts.tenantId, tenantId),
|
|
936
|
+
),
|
|
937
|
+
);
|
|
938
|
+
return rows.map((row) => row.id);
|
|
939
|
+
}
|
|
940
|
+
|
|
876
941
|
async listAllAccounts(tenantId: string): Promise<readonly ProviderAccountResponse[]> {
|
|
877
942
|
const rows = await this.db
|
|
878
943
|
.select()
|
|
@@ -885,6 +950,77 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
|
|
|
885
950
|
return [...accounts, ...poolAccounts];
|
|
886
951
|
}
|
|
887
952
|
|
|
953
|
+
async listAllAccountSummaries(
|
|
954
|
+
tenantId: string,
|
|
955
|
+
): Promise<readonly { providerId: string; status: string }[]> {
|
|
956
|
+
const rows = await this.db
|
|
957
|
+
.select({ providerId: providerAccounts.providerId, status: providerAccounts.status })
|
|
958
|
+
.from(providerAccounts)
|
|
959
|
+
.where(globalOrOwnedBy(providerAccounts.tenantId, tenantId));
|
|
960
|
+
return rows.map((row) => ({ providerId: row.providerId, status: row.status }));
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Per-provider account totals for one tenant, from a single grouped read (one
|
|
965
|
+
* statement, served by the `(provider_id, tenant_id)` index). `active` counts
|
|
966
|
+
* every status except `disabled` — an account parked by the health machine
|
|
967
|
+
* (`cooldown`) is still a connection, matching how the account-overview badge
|
|
968
|
+
* reads status. Used by the provider list overview (`accountCount` /
|
|
969
|
+
* `activeAccountCount`); totals are never counted in a per-provider loop.
|
|
970
|
+
*/
|
|
971
|
+
async providerAccountCounts(
|
|
972
|
+
tenantId: string,
|
|
973
|
+
): Promise<readonly { providerId: string; total: number; active: number }[]> {
|
|
974
|
+
const rows = await this.db
|
|
975
|
+
.select({
|
|
976
|
+
providerId: providerAccounts.providerId,
|
|
977
|
+
total: sql<number>`count(*)`,
|
|
978
|
+
active: sql<number>`sum(case when ${providerAccounts.status} <> 'disabled' then 1 else 0 end)`,
|
|
979
|
+
})
|
|
980
|
+
.from(providerAccounts)
|
|
981
|
+
.where(globalOrOwnedBy(providerAccounts.tenantId, tenantId))
|
|
982
|
+
.groupBy(providerAccounts.providerId);
|
|
983
|
+
return rows.map((row) => ({
|
|
984
|
+
providerId: row.providerId,
|
|
985
|
+
total: Number(row.total),
|
|
986
|
+
active: Number(row.active),
|
|
987
|
+
}));
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Per-provider enabled model-row counts for one tenant, from a single grouped
|
|
992
|
+
* read over `models ⋈ providers`. A model counts only when the row is enabled
|
|
993
|
+
* and the tenant has not suppressed it via `tenant_disabled_models` — the
|
|
994
|
+
* same visibility rule the per-provider model listing applies, so the
|
|
995
|
+
* overview badge never disagrees with what the detail page would list.
|
|
996
|
+
*/
|
|
997
|
+
async providerModelCounts(
|
|
998
|
+
tenantId: string,
|
|
999
|
+
): Promise<readonly { providerId: string; count: number }[]> {
|
|
1000
|
+
const rows = await this.db
|
|
1001
|
+
.select({ providerId: models.providerId, count: sql<number>`count(*)` })
|
|
1002
|
+
.from(models)
|
|
1003
|
+
.innerJoin(providers, eq(models.providerId, providers.id))
|
|
1004
|
+
.leftJoin(
|
|
1005
|
+
tenantDisabledModels,
|
|
1006
|
+
and(
|
|
1007
|
+
eq(tenantDisabledModels.tenantId, tenantId),
|
|
1008
|
+
eq(tenantDisabledModels.providerId, models.providerId),
|
|
1009
|
+
eq(tenantDisabledModels.modelId, models.modelId),
|
|
1010
|
+
eq(tenantDisabledModels.endpointPath, models.endpointPath),
|
|
1011
|
+
),
|
|
1012
|
+
)
|
|
1013
|
+
.where(
|
|
1014
|
+
and(
|
|
1015
|
+
eq(models.enabled, true),
|
|
1016
|
+
globalOrOwnedBy(providers.tenantId, tenantId),
|
|
1017
|
+
isNull(tenantDisabledModels.id),
|
|
1018
|
+
),
|
|
1019
|
+
)
|
|
1020
|
+
.groupBy(models.providerId);
|
|
1021
|
+
return rows.map((row) => ({ providerId: row.providerId, count: Number(row.count) }));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
888
1024
|
/**
|
|
889
1025
|
* Pool-as-account rows for one provider. Only providers whose rate limit
|
|
890
1026
|
* follows the egress address materialize these; every other provider's pool
|
|
@@ -1391,6 +1527,8 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
|
|
|
1391
1527
|
typeof authState?.["tokenExpiresAt"] === "string" ? authState["tokenExpiresAt"] : undefined;
|
|
1392
1528
|
const jwtIssuer =
|
|
1393
1529
|
typeof authState?.["jwtIssuer"] === "string" ? authState["jwtIssuer"] : undefined;
|
|
1530
|
+
const authMethod =
|
|
1531
|
+
typeof authState?.["authMethod"] === "string" ? authState["authMethod"] : undefined;
|
|
1394
1532
|
return {
|
|
1395
1533
|
id: row.id,
|
|
1396
1534
|
providerId: row.providerId,
|
|
@@ -1400,6 +1538,7 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
|
|
|
1400
1538
|
...(credentialMode === undefined ? {} : { credentialMode }),
|
|
1401
1539
|
...(tokenExpiresAt === undefined ? {} : { tokenExpiresAt }),
|
|
1402
1540
|
...(jwtIssuer === undefined ? {} : { jwtIssuer }),
|
|
1541
|
+
...(authMethod === undefined ? {} : { authMethod }),
|
|
1403
1542
|
status: row.status,
|
|
1404
1543
|
...(usage.inflight === undefined ? {} : { inflight: usage.inflight }),
|
|
1405
1544
|
usageToday: usage.today,
|
|
@@ -30,7 +30,7 @@ const ROUTING_BOUNDS = {
|
|
|
30
30
|
maxInflightPerProxy: PROVIDER_ROUTING_MAX_INFLIGHT_BOUNDS,
|
|
31
31
|
} as const;
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
function createProviderDetailOperations(deps: ProviderDetailConfig) {
|
|
34
34
|
const operations = {
|
|
35
35
|
async getRouting(
|
|
36
36
|
access: AccessDecision | undefined,
|
|
@@ -28,7 +28,7 @@ import { log } from "../../../observability/logger";
|
|
|
28
28
|
import { ConsoleDomainError } from "../../shared/errors";
|
|
29
29
|
|
|
30
30
|
/** A loopback endpoint a callback can actually be delivered to. */
|
|
31
|
-
|
|
31
|
+
interface LoopbackCallbackEndpoint {
|
|
32
32
|
readonly port: number;
|
|
33
33
|
readonly path: string;
|
|
34
34
|
}
|
|
@@ -44,7 +44,7 @@ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
|
44
44
|
* installed app and a remote host is delivered over the network; neither is
|
|
45
45
|
* something this process can bind, and both keep the manual path.
|
|
46
46
|
*/
|
|
47
|
-
|
|
47
|
+
function loopbackCallbackEndpoint(redirectUri: string): LoopbackCallbackEndpoint | undefined {
|
|
48
48
|
let url: URL;
|
|
49
49
|
try {
|
|
50
50
|
url = new URL(redirectUri);
|
|
@@ -59,12 +59,19 @@ export function loopbackCallbackEndpoint(redirectUri: string): LoopbackCallbackE
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/** Completes one callback once its code and state have been read. */
|
|
62
|
-
|
|
62
|
+
interface OAuthCallbackCompleter {
|
|
63
63
|
complete(
|
|
64
64
|
providerId: string,
|
|
65
65
|
code: string,
|
|
66
66
|
state: string,
|
|
67
|
-
|
|
67
|
+
/** Extra query params the callback carried (e.g. Kiro's `login_option`). */
|
|
68
|
+
callbackParams?: Readonly<Record<string, string>>,
|
|
69
|
+
): Promise<{
|
|
70
|
+
readonly ok: boolean;
|
|
71
|
+
readonly message: string;
|
|
72
|
+
readonly redirectUrl?: string;
|
|
73
|
+
readonly continueState?: string;
|
|
74
|
+
}>;
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
/** Minimal `Bun.serve` surface this module depends on. */
|
|
@@ -79,7 +86,7 @@ type ServeFn = (options: {
|
|
|
79
86
|
readonly fetch: (request: Request) => Response | Promise<Response>;
|
|
80
87
|
}) => LoopbackServer;
|
|
81
88
|
|
|
82
|
-
|
|
89
|
+
interface CallbackListenerOptions {
|
|
83
90
|
readonly completer: OAuthCallbackCompleter;
|
|
84
91
|
/** Injected in tests; defaults to `Bun.serve`. */
|
|
85
92
|
readonly serve?: ServeFn;
|
|
@@ -92,7 +99,10 @@ const DEFAULT_TTL_MS = 900_000;
|
|
|
92
99
|
|
|
93
100
|
interface RegisteredFlow {
|
|
94
101
|
readonly providerId: string;
|
|
102
|
+
/** Paths this flow's callback may arrive on (e.g. Kiro serves both `/oauth/callback` and `/signin/callback`). */
|
|
103
|
+
readonly paths: readonly string[];
|
|
95
104
|
readonly timer: ReturnType<typeof setTimeout>;
|
|
105
|
+
inFlight: boolean;
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
interface PortListener {
|
|
@@ -134,17 +144,30 @@ export class OAuthCallbackListener {
|
|
|
134
144
|
if (endpoint === undefined) return false;
|
|
135
145
|
const existing = this.#ports.get(endpoint.port);
|
|
136
146
|
if (existing !== undefined) {
|
|
137
|
-
this.#remember(existing, state, providerId);
|
|
147
|
+
this.#remember(existing, state, providerId, this.#callbackPaths(endpoint.path));
|
|
138
148
|
return true;
|
|
139
149
|
}
|
|
140
150
|
const servers = this.#bind(endpoint);
|
|
141
151
|
const listener: PortListener = { port: endpoint.port, servers, flows: new Map() };
|
|
142
152
|
this.#ports.set(endpoint.port, listener);
|
|
143
|
-
this.#remember(listener, state, providerId);
|
|
153
|
+
this.#remember(listener, state, providerId, this.#callbackPaths(endpoint.path));
|
|
144
154
|
log.info(`[oauth] callback listener bound on 127.0.0.1:${endpoint.port}${endpoint.path}`);
|
|
145
155
|
return true;
|
|
146
156
|
}
|
|
147
157
|
|
|
158
|
+
/**
|
|
159
|
+
* The paths one login may deliver its callback on. Kiro's portal is observed
|
|
160
|
+
* (unpacked IDE 1.4.62) to redirect to both `/oauth/callback` and
|
|
161
|
+
* `/signin/callback`; binding only the advertised path left a redirect to the
|
|
162
|
+
* other one hitting a 404 and the login hanging. Other providers neither
|
|
163
|
+
* register nor expect `/signin/callback`, so adding it is harmless.
|
|
164
|
+
*/
|
|
165
|
+
#callbackPaths(advertised: string): readonly string[] {
|
|
166
|
+
return advertised === "/signin/callback"
|
|
167
|
+
? [advertised]
|
|
168
|
+
: [advertised, "/signin/callback"];
|
|
169
|
+
}
|
|
170
|
+
|
|
148
171
|
/** Drops a flow and frees its port when nothing else is waiting on it. */
|
|
149
172
|
release(redirectUri: string, state: string): void {
|
|
150
173
|
const endpoint = loopbackCallbackEndpoint(redirectUri);
|
|
@@ -154,6 +177,25 @@ export class OAuthCallbackListener {
|
|
|
154
177
|
this.#forget(endpoint.port, listener, state);
|
|
155
178
|
}
|
|
156
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Re-registers an existing flow state under a new callback state.
|
|
182
|
+
*
|
|
183
|
+
* Kiro's two-phase portal login starts a second authorization leg with a
|
|
184
|
+
* *fresh* state (the IDE mints a new UUID for the IdC authorize step), so
|
|
185
|
+
* the loopback listener must retain the pending flow under that second
|
|
186
|
+
* state too — otherwise the phase-2 callback answers "OAuth session not
|
|
187
|
+
* found or expired" even though the flow is still alive.
|
|
188
|
+
*/
|
|
189
|
+
track(redirectUri: string, state: string, newState: string): void {
|
|
190
|
+
const endpoint = loopbackCallbackEndpoint(redirectUri);
|
|
191
|
+
if (endpoint === undefined) return;
|
|
192
|
+
const listener = this.#ports.get(endpoint.port);
|
|
193
|
+
if (listener === undefined) return;
|
|
194
|
+
const flow = listener.flows.get(state);
|
|
195
|
+
if (flow === undefined) return;
|
|
196
|
+
this.#remember(listener, newState, flow.providerId, flow.paths);
|
|
197
|
+
}
|
|
198
|
+
|
|
157
199
|
/**
|
|
158
200
|
* Drops every flow for `providerId` on `redirectUri`'s port.
|
|
159
201
|
*
|
|
@@ -182,7 +224,7 @@ export class OAuthCallbackListener {
|
|
|
182
224
|
}
|
|
183
225
|
}
|
|
184
226
|
|
|
185
|
-
#remember(listener: PortListener, state: string, providerId: string): void {
|
|
227
|
+
#remember(listener: PortListener, state: string, providerId: string, paths: readonly string[]): void {
|
|
186
228
|
const previous = listener.flows.get(state);
|
|
187
229
|
if (previous !== undefined) clearTimeout(previous.timer);
|
|
188
230
|
const timer = setTimeout(() => {
|
|
@@ -190,7 +232,7 @@ export class OAuthCallbackListener {
|
|
|
190
232
|
}, this.#ttlMs);
|
|
191
233
|
// A pending login must not hold the process open on its own.
|
|
192
234
|
(timer as unknown as { unref?: () => void }).unref?.();
|
|
193
|
-
listener.flows.set(state, { providerId, timer });
|
|
235
|
+
listener.flows.set(state, { providerId, paths, timer, inFlight: false });
|
|
194
236
|
}
|
|
195
237
|
|
|
196
238
|
#forget(port: number, listener: PortListener, state: string): void {
|
|
@@ -250,9 +292,6 @@ export class OAuthCallbackListener {
|
|
|
250
292
|
endpoint: LoopbackCallbackEndpoint,
|
|
251
293
|
): Promise<Response> {
|
|
252
294
|
const url = new URL(request.url);
|
|
253
|
-
if (url.pathname !== endpoint.path) {
|
|
254
|
-
return new Response("Not found", { status: 404 });
|
|
255
|
-
}
|
|
256
295
|
const listener = this.#ports.get(endpoint.port);
|
|
257
296
|
if (listener === undefined) {
|
|
258
297
|
return new Response("OAuth session not found or expired.", { status: 400 });
|
|
@@ -278,7 +317,7 @@ export class OAuthCallbackListener {
|
|
|
278
317
|
{ status: 400, headers: { "content-type": "text/html; charset=utf-8" } },
|
|
279
318
|
);
|
|
280
319
|
}
|
|
281
|
-
if (code.length === 0) {
|
|
320
|
+
if (code.length === 0 && !url.searchParams.has("login_option")) {
|
|
282
321
|
return new Response("Missing authorization code.", { status: 400 });
|
|
283
322
|
}
|
|
284
323
|
// `state` is the correlation key and the replay guard. An authorization
|
|
@@ -297,14 +336,45 @@ export class OAuthCallbackListener {
|
|
|
297
336
|
if (matched === undefined) {
|
|
298
337
|
return new Response("OAuth session not found or expired.", { status: 400 });
|
|
299
338
|
}
|
|
300
|
-
//
|
|
301
|
-
//
|
|
339
|
+
// Some providers (Kiro's portal) can land the callback on any of several
|
|
340
|
+
// registered paths — `/oauth/callback` or `/signin/callback`. Accept the
|
|
341
|
+
// path the flow registered for, not only the advertised default.
|
|
342
|
+
if (!matched.paths.includes(url.pathname)) {
|
|
343
|
+
return new Response("Not found", { status: 404 });
|
|
344
|
+
}
|
|
345
|
+
if (matched.inFlight) {
|
|
346
|
+
return new Response("OAuth callback is already being processed.", { status: 409 });
|
|
347
|
+
}
|
|
348
|
+
// Claim while the exchange is in flight. A multi-step flow (Kiro OIDC) may
|
|
349
|
+
// return a redirect and reuse this listener/state for its next callback.
|
|
350
|
+
matched.inFlight = true;
|
|
302
351
|
const matchedState = state.length > 0 ? state : this.#stateOf(listener, matched);
|
|
303
|
-
|
|
304
|
-
|
|
352
|
+
let result: {
|
|
353
|
+
ok: boolean;
|
|
354
|
+
message: string;
|
|
355
|
+
redirectUrl?: string;
|
|
356
|
+
continueState?: string;
|
|
357
|
+
};
|
|
358
|
+
// Some authorization servers (Kiro's portal) append provider-specific query
|
|
359
|
+
// params — `login_option` names the social/enterprise family that approved.
|
|
360
|
+
// The exchange needs them to rebuild the exact redirect URI the portal saw,
|
|
361
|
+
// and the OIDC families carry `issuer_url`/`idc_region`/`client_id`/`scopes`
|
|
362
|
+
// that the continuation flow needs. Forward every param other than the ones
|
|
363
|
+
// this handler already consumed.
|
|
364
|
+
const callbackParams: Record<string, string> = {};
|
|
365
|
+
for (const [key, value] of url.searchParams) {
|
|
366
|
+
if (key === "code" || key === "state" || key === "error" || key === "error_description") continue;
|
|
367
|
+
callbackParams[key] = value;
|
|
368
|
+
}
|
|
305
369
|
try {
|
|
306
|
-
result = await this.#completer.complete(
|
|
370
|
+
result = await this.#completer.complete(
|
|
371
|
+
matched.providerId,
|
|
372
|
+
code,
|
|
373
|
+
state,
|
|
374
|
+
Object.keys(callbackParams).length > 0 ? callbackParams : undefined,
|
|
375
|
+
);
|
|
307
376
|
} catch (failure) {
|
|
377
|
+
if (matchedState !== undefined) this.#forget(endpoint.port, listener, matchedState);
|
|
308
378
|
const message = failure instanceof Error ? failure.message : "OAuth login failed";
|
|
309
379
|
log.error(`[oauth] callback completion failed for ${matched.providerId}: ${message}`);
|
|
310
380
|
return new Response(
|
|
@@ -312,6 +382,18 @@ export class OAuthCallbackListener {
|
|
|
312
382
|
{ status: 500, headers: { "content-type": "text/html; charset=utf-8" } },
|
|
313
383
|
);
|
|
314
384
|
}
|
|
385
|
+
if (result.redirectUrl !== undefined) {
|
|
386
|
+
// Phase 1 of a two-phase login (Kiro OIDC): the flow is still pending, so
|
|
387
|
+
// continue by sending the browser to the phase-2 authorize URL. The phase-2
|
|
388
|
+
// authorize URL carries a fresh state (`continueState`), which the completer
|
|
389
|
+
// already registered under — keep the current matcher alive for it.
|
|
390
|
+
matched.inFlight = false;
|
|
391
|
+
return new Response(null, {
|
|
392
|
+
status: 302,
|
|
393
|
+
headers: { location: result.redirectUrl },
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
if (matchedState !== undefined) this.#forget(endpoint.port, listener, matchedState);
|
|
315
397
|
if (!result.ok) {
|
|
316
398
|
return new Response(
|
|
317
399
|
`<!doctype html><html><body><p>OAuth login failed: ${escapeHtml(result.message)}</p></body></html>`,
|
|
@@ -171,7 +171,7 @@ export class DrizzleOAuthAccountStore implements OAuthAccountStore {
|
|
|
171
171
|
* server the redirect arrives at; `callback-listener.ts` is what makes those
|
|
172
172
|
* the same endpoint.
|
|
173
173
|
*/
|
|
174
|
-
|
|
174
|
+
interface PersistedOAuthAccount {
|
|
175
175
|
readonly accountId: string;
|
|
176
176
|
}
|
|
177
177
|
|
|
@@ -212,9 +212,20 @@ async function requireClient(
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/** The result of exchanging one authorization code, as the caller should report it. */
|
|
215
|
-
|
|
215
|
+
interface OAuthCompleteOutcome {
|
|
216
216
|
readonly ok: boolean;
|
|
217
217
|
readonly message: string;
|
|
218
|
+
/**
|
|
219
|
+
* When set, the exchange is not finished: the browser must be redirected to
|
|
220
|
+
* this URL to continue (Kiro's OIDC families need a second authorization
|
|
221
|
+
* leg after the portal callback).
|
|
222
|
+
*/
|
|
223
|
+
readonly redirectUrl?: string;
|
|
224
|
+
/**
|
|
225
|
+
* New callback state the phase-2 authorize URL carries (Kiro mints a fresh
|
|
226
|
+
* state for the IdC leg; the console retains the pending flow under it).
|
|
227
|
+
*/
|
|
228
|
+
readonly continueState?: string;
|
|
218
229
|
}
|
|
219
230
|
|
|
220
231
|
/**
|
|
@@ -231,29 +242,41 @@ export async function completeLogin(
|
|
|
231
242
|
providerId: string,
|
|
232
243
|
code: string,
|
|
233
244
|
state: string,
|
|
245
|
+
callbackParams?: Readonly<Record<string, string>>,
|
|
234
246
|
): Promise<OAuthCompleteOutcome> {
|
|
235
247
|
// An empty `state` means the authorization server does not echo one
|
|
236
248
|
// (OpenRouter omits it from the callback entirely); correlate by provider in
|
|
237
249
|
// that case, and by state otherwise so the stronger check stays in force
|
|
238
250
|
// wherever the server supports it.
|
|
251
|
+
//
|
|
252
|
+
// The flow is peeked, not consumed, until the exchange succeeds: consuming
|
|
253
|
+
// first means a failed exchange (a transient upstream blip, a paste that
|
|
254
|
+
// missed a parameter) burns the state and every retry answers "unknown or
|
|
255
|
+
// expired state". The state is deleted only after the account persists.
|
|
239
256
|
const flow = state
|
|
240
|
-
? await config.oauthFlowStore.
|
|
257
|
+
? await config.oauthFlowStore.peekPending(state)
|
|
241
258
|
: await config.oauthFlowStore.consumePendingByProvider(providerId);
|
|
242
259
|
if (!flow || flow.providerId !== providerId) {
|
|
243
260
|
return { ok: false, message: "unknown or expired state" };
|
|
244
261
|
}
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
else config.callbackListener.releaseProvider(flow.redirectUri, providerId);
|
|
262
|
+
// Release the listener registration for this flow. The pending state itself
|
|
263
|
+
// stays available so a manual re-paste can retry the exchange. The release is
|
|
264
|
+
// deferred until after the exchange for two-phase flows: Kiro's OIDC portal
|
|
265
|
+
// callback (phase 1) must keep the loopback listener alive so the second
|
|
266
|
+
// authorization leg can land on it.
|
|
251
267
|
const client = await requireClient(config.providerRegistry, providerId);
|
|
252
268
|
if (!client.exchangeCode) {
|
|
253
269
|
return { ok: false, message: "provider does not support browser authorization" };
|
|
254
270
|
}
|
|
271
|
+
const releaseListener = () => {
|
|
272
|
+
if (state.length > 0) config.callbackListener.release(flow.redirectUri, state);
|
|
273
|
+
else config.callbackListener.releaseProvider(flow.redirectUri, providerId);
|
|
274
|
+
};
|
|
255
275
|
try {
|
|
256
|
-
const result = await client.exchangeCode(code, flow.codeVerifier, flow.redirectUri, state
|
|
276
|
+
const result = await client.exchangeCode(code, flow.codeVerifier, flow.redirectUri, state, {
|
|
277
|
+
...(callbackParams === undefined ? {} : { parameters: callbackParams }),
|
|
278
|
+
...(flow.providerState === undefined ? {} : { providerState: flow.providerState }),
|
|
279
|
+
});
|
|
257
280
|
const tokenCheck = await validateIssuedAccessToken(
|
|
258
281
|
result.access,
|
|
259
282
|
providerJwtVerification(providerId),
|
|
@@ -270,9 +293,51 @@ export async function completeLogin(
|
|
|
270
293
|
label: result.accountLabel ?? flow.accountLabel,
|
|
271
294
|
...result,
|
|
272
295
|
});
|
|
296
|
+
// The exchange succeeded and the account is persisted; now the flow is
|
|
297
|
+
// spent. Consuming it here (peeked above) frees the state and the provider
|
|
298
|
+
// pointer so a replayed callback cannot re-create the account.
|
|
299
|
+
releaseListener();
|
|
300
|
+
if (state.length > 0) await config.oauthFlowStore.consumePending(state);
|
|
301
|
+
else await config.oauthFlowStore.consumePendingByProvider(providerId);
|
|
273
302
|
await config.snapshotInvalidator?.invalidate();
|
|
274
303
|
return { ok: true, message: `${providerId} account connected. You may close this tab.` };
|
|
275
304
|
} catch (error) {
|
|
305
|
+
// Kiro's OIDC families: the portal callback carries no code — only
|
|
306
|
+
// `login_option` + issuer/region. The client responded with the second-leg
|
|
307
|
+
// authorize URL plus a fresh PKCE verifier and the OIDC registration.
|
|
308
|
+
// Rewrite the pending flow so phase 2 exchanges against that verifier, keep
|
|
309
|
+
// the listener alive, and tell the caller to redirect the browser there.
|
|
310
|
+
// dynamic-import: B-02 lazy graph — 58KB kiro OIDC path, only for Kiro flows.
|
|
311
|
+
// The kiro-oauth graph is 58KB, so it is imported lazily here — only the
|
|
312
|
+
// Kiro OIDC path needs the error class — instead of on the console boot
|
|
313
|
+
// graph (B-02).
|
|
314
|
+
const { KiroOidcPendingError } = await import(
|
|
315
|
+
"../../../providers/integrations/kiro/kiro-oauth"
|
|
316
|
+
);
|
|
317
|
+
if (error instanceof KiroOidcPendingError) {
|
|
318
|
+
// Phase 1 of Kiro's OIDC portal login: the portal callback carried no
|
|
319
|
+
// code — only `login_option` + issuer/region. The client answered with the
|
|
320
|
+
// phase-2 authorize URL, a fresh PKCE verifier, and the OIDC client
|
|
321
|
+
// registration. Mirror the 1.4.62 IDE: the second leg uses a *new* state
|
|
322
|
+
// (not the portal's), so mint it here, keep the pending flow reachable
|
|
323
|
+
// under both states, and tell the caller which state to expect back.
|
|
324
|
+
const continueState = error.continueState;
|
|
325
|
+
if (state.length > 0 && continueState.length > 0) {
|
|
326
|
+
await config.oauthFlowStore.savePending(continueState, {
|
|
327
|
+
...flow,
|
|
328
|
+
codeVerifier: error.codeVerifier,
|
|
329
|
+
providerState: error.providerState,
|
|
330
|
+
});
|
|
331
|
+
config.callbackListener.track(flow.redirectUri, state, continueState);
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
ok: false,
|
|
335
|
+
message: error.message,
|
|
336
|
+
redirectUrl: error.authorizeUrl,
|
|
337
|
+
continueState,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
releaseListener();
|
|
276
341
|
// Upstream token-endpoint bodies can echo credentials or sensitive
|
|
277
342
|
// diagnostics — the browser gets a generic failure; the bounded detail
|
|
278
343
|
// stays server-side in the console log.
|
|
@@ -351,7 +416,7 @@ function failurePage(message: string, wantsJson: boolean): Response {
|
|
|
351
416
|
);
|
|
352
417
|
}
|
|
353
418
|
|
|
354
|
-
|
|
419
|
+
function createOAuthLoginOperations(config: OAuthLoginConfig) {
|
|
355
420
|
const operations = {
|
|
356
421
|
async beginAuthorize(
|
|
357
422
|
access: AccessDecision | undefined,
|
|
@@ -437,8 +502,15 @@ export function createOAuthLoginOperations(config: OAuthLoginConfig) {
|
|
|
437
502
|
code: string,
|
|
438
503
|
state: string,
|
|
439
504
|
wantsJson = false,
|
|
505
|
+
callbackParams?: Readonly<Record<string, string>>,
|
|
440
506
|
): Promise<Response> {
|
|
441
|
-
const outcome = await completeLogin(config, providerId, code, state);
|
|
507
|
+
const outcome = await completeLogin(config, providerId, code, state, callbackParams);
|
|
508
|
+
if (outcome.redirectUrl !== undefined) {
|
|
509
|
+
return new Response(null, {
|
|
510
|
+
status: 302,
|
|
511
|
+
headers: { location: outcome.redirectUrl },
|
|
512
|
+
});
|
|
513
|
+
}
|
|
442
514
|
return outcome.ok ? successPage(providerId, wantsJson) : failurePage(outcome.message, wantsJson);
|
|
443
515
|
},
|
|
444
516
|
async handleCallbackError(
|
|
@@ -670,7 +742,14 @@ export function createOAuthLoginRoutes(config: OAuthLoginConfig): AnyElysia {
|
|
|
670
742
|
// `state` is absent for authorization servers that do not echo it
|
|
671
743
|
// (OpenRouter). The flow is then correlated by provider instead — one
|
|
672
744
|
// browser login is in flight per provider at a time.
|
|
673
|
-
|
|
745
|
+
const loginOption = typeof query["login_option"] === "string" ? query["login_option"] : undefined;
|
|
746
|
+
return factory.handleCallback(
|
|
747
|
+
params.providerId,
|
|
748
|
+
code,
|
|
749
|
+
state,
|
|
750
|
+
wantsJson,
|
|
751
|
+
loginOption === undefined ? undefined : { login_option: loginOption },
|
|
752
|
+
);
|
|
674
753
|
})
|
|
675
754
|
.post(
|
|
676
755
|
"/:providerId/oauth/device/start",
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { t } from "elysia";
|
|
2
2
|
import type { Elysia } from "elysia";
|
|
3
|
-
import { and, eq, isNull } from "drizzle-orm";
|
|
3
|
+
import { and, eq, inArray, isNull } from "drizzle-orm";
|
|
4
4
|
import { adminAuditLog, providerAccounts } from "../../persistence/schema";
|
|
5
5
|
import { requireGlobalAdmin } from "../shared/errors";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
GLOBAL_QUOTA_LENS,
|
|
8
|
+
invalidateQuotaCache,
|
|
9
|
+
invalidateQuotaCaches,
|
|
10
|
+
} from "../../providers/quota/cache";
|
|
11
|
+
import {
|
|
12
|
+
invalidateCredentialCache,
|
|
13
|
+
invalidateCredentialCaches,
|
|
14
|
+
} from "../../providers/operations/provider-credential-service";
|
|
8
15
|
import {
|
|
9
16
|
QUOTA_REFRESH_TIMEOUT_MS,
|
|
10
17
|
loadQuotaTarget,
|
|
@@ -119,6 +126,37 @@ export function registerAccountQuotaGlobalRoutes(
|
|
|
119
126
|
});
|
|
120
127
|
return { success: true };
|
|
121
128
|
})
|
|
129
|
+
.post("/global/accounts/batch-delete", {
|
|
130
|
+
body: t.Object({
|
|
131
|
+
accountIds: t.Array(t.String()),
|
|
132
|
+
}),
|
|
133
|
+
}, async ({ request, body }) => {
|
|
134
|
+
const access = requireGlobalAdmin(accessResolver(request));
|
|
135
|
+
const accountIds = [...new Set(body.accountIds)];
|
|
136
|
+
if (accountIds.length === 0) {
|
|
137
|
+
return { ok: true, deleted: [], missing: [] };
|
|
138
|
+
}
|
|
139
|
+
// One DELETE statement scoped to shared accounts only: a tenant-owned row
|
|
140
|
+
// (`tenant_id IS NOT NULL`) is never matchable by a global admin, so it
|
|
141
|
+
// lands in `missing`.
|
|
142
|
+
const deleted = await db
|
|
143
|
+
.delete(providerAccounts)
|
|
144
|
+
.where(and(inArray(providerAccounts.id, accountIds), isNull(providerAccounts.tenantId)))
|
|
145
|
+
.returning({ id: providerAccounts.id });
|
|
146
|
+
const deletedIds = deleted.map((row) => row.id);
|
|
147
|
+
const deletedSet = new Set(deletedIds);
|
|
148
|
+
const missing = accountIds.filter((id) => !deletedSet.has(id));
|
|
149
|
+
invalidateCredentialCaches(deletedIds);
|
|
150
|
+
await invalidateQuotaCaches(GLOBAL_QUOTA_LENS, deletedIds, redis);
|
|
151
|
+
await snapshotInvalidator?.invalidate();
|
|
152
|
+
await db.insert(adminAuditLog).values({
|
|
153
|
+
actor: access.admissionIdentity,
|
|
154
|
+
action: "provider_account.global.batch_deleted",
|
|
155
|
+
target: "global",
|
|
156
|
+
detail: { deleted: deletedIds, missing },
|
|
157
|
+
});
|
|
158
|
+
return { ok: true, deleted: deletedIds, missing };
|
|
159
|
+
})
|
|
122
160
|
.post("/global/accounts/:id/quota/refresh", async ({ request, params, set }) => {
|
|
123
161
|
const access = requireGlobalAdmin(accessResolver(request));
|
|
124
162
|
const target = await loadQuotaTarget(db, params.id, null);
|