cartethyia 2.7.2 → 2.7.4
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/dist/dashboard/assets/{BannedPage-XaUmiBrC.js → BannedPage-ChG5svFb.js} +1 -1
- package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js +1 -0
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-CXGS_Ct-.js → CombosPage-DCgm5g6g.js} +1 -1
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-BKtAPYVF.js → HealthEventsModal-DzSWzE_j.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BdmRLnq2.js → LoginPage-U6AKaJcF.js} +1 -1
- package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BH8Tehdi.js → ModelPicker-BMS2t8BA.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +1 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +5 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js +9 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js +1 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js +1 -0
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-Dqf4zpS-.js → SortableList-ClzpwyET.js} +1 -1
- package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-Byj2lAEm.js → StudioPage-Cq5tW7hW.js} +3 -3
- package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +56 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js +1 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
- package/dist/dashboard/assets/app-a159k0ub.css +2 -0
- package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js +1 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js.gz +0 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js +15 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DC2L43Mj.js → badge-D9CS6dZk.js} +1 -1
- package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BBVcb0J3.js → banner-DympBNq4.js} +1 -1
- package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
- package/dist/dashboard/assets/{card-Cca1XC6G.js → card-ibRWU_uT.js} +1 -1
- package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js +1 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DUzEbynq.js → clipboard-button-DJm-9pz7.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-DFvWhwTa.js → dialog-8fiE6tec.js} +1 -1
- package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CqGYeOrd.js +1 -0
- package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
- package/dist/dashboard/assets/{entry-Bqy8RFvM.js → entry-BSAsYsmV.js} +1 -1
- package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js +4 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js +1 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
- package/dist/dashboard/assets/{concurrency-BDF0ok7y.js → fingerprint-pattern-BWuHtbW4.js} +1 -1
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-CWWZ-uCZ.js → formatters-D-MTJxMr.js} +1 -1
- package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-C9e8Ts7e.js → github-badge-CPK47xQx.js} +1 -1
- package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
- package/dist/dashboard/assets/{index-DFVFrnWW.js → index-DBNPOfhV.js} +2 -2
- package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BFUcvllj.js → inline-BT0zntEr.js} +1 -1
- package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
- package/dist/dashboard/assets/{input-C_ILAvBb.js → input-Da2ZViWg.js} +1 -1
- package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CBIyT6Rt.js → layout-BXNeLw6M.js} +1 -1
- package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-ebJ_-Nsb.js → list-row-BycOrrIV.js} +1 -1
- package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js +1 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-sMD965oO.js → mutation-Rr0TIvcj.js} +1 -1
- package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js +1 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BO9dKaFr.js → numbers-CEagj6ZZ.js} +2 -2
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CfL_Zdoh.js → query-keys-BloKtaXJ.js} +1 -1
- package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js +1 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js +1 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
- package/dist/dashboard/assets/{select-DBNVLdUu.js → select-BIYIBkMU.js} +2 -2
- package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js +1 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js.gz +0 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js +1 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
- package/dist/dashboard/assets/{stack-EMrOMt4I.js → stack-DfgnFDWY.js} +1 -1
- package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
- package/dist/dashboard/assets/{state-CWoo7ty5.js → state-CkfdYWel.js} +1 -1
- package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
- package/dist/dashboard/assets/{switch-CCjT6IkF.js → switch-SYm237HG.js} +1 -1
- package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DjkLHUcE.js → useMutation-BQqCRNIi.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
- package/dist/dashboard/index.html +3 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +118 -115
- package/migrations/full-postgres/0026_add_qoder_native_identity.sql +20 -0
- package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +21 -0
- package/migrations/full-postgres/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/full-postgres/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/full-postgres/0030_add_api_key_max_children.sql +10 -0
- package/migrations/full-postgres/0031_tenant_system_prompts.sql +16 -0
- package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
- package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
- package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
- package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
- package/migrations/lite-sqlite3/0026_add_qoder_native_identity.sql +15 -0
- package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +21 -0
- package/migrations/lite-sqlite3/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/lite-sqlite3/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/lite-sqlite3/0030_add_api_key_max_children.sql +10 -0
- package/migrations/lite-sqlite3/0031_tenant_system_prompts.sql +10 -0
- package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
- package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
- package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
- package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
- package/package.json +2 -1
- package/src/boot/app.ts +9 -8
- package/src/config/network.ts +7 -3
- package/src/console/api-keys/bucket-spend.ts +7 -7
- package/src/console/api-keys/contracts.ts +475 -410
- package/src/console/api-keys/routes.ts +211 -23
- package/src/console/audit/routes.ts +3 -3
- package/src/console/auth/account-reset.ts +4 -4
- package/src/console/auth/session.ts +3 -3
- package/src/console/backup/contracts.ts +2 -0
- package/src/console/backup/export-endpoint.ts +14 -17
- package/src/console/backup/routes.ts +43 -11
- package/src/console/backup/service.ts +11 -6
- package/src/console/backup/store.ts +384 -88
- package/src/console/cli-tools/routes.ts +3 -3
- package/src/console/console-router-guards.ts +2 -2
- package/src/console/console-router.ts +4 -4
- package/src/console/dashboard-assets.ts +16 -1
- package/src/console/data-management/routes.ts +3 -3
- package/src/console/domain-registration.ts +4 -2
- package/src/console/observability/contracts.ts +4 -0
- package/src/console/observability/live.ts +3 -3
- package/src/console/observability/logs.ts +3 -3
- package/src/console/observability/performance.ts +3 -3
- package/src/console/observability/routes.ts +3 -3
- package/src/console/observability/store.ts +16 -16
- package/src/console/providers/catalog/contracts.ts +43 -22
- package/src/console/providers/catalog/provider-operations.ts +17 -2
- package/src/console/providers/catalog/routes.ts +3 -3
- package/src/console/providers/catalog/store-helpers.ts +349 -326
- package/src/console/providers/catalog/store.ts +1523 -1292
- package/src/console/providers/detail/routes.ts +3 -3
- package/src/console/providers/oauth/routes.ts +3 -3
- package/src/console/quota/account-quota-tenant.ts +34 -2
- package/src/console/quota/account-quota.ts +4 -1
- package/src/console/quota/account-test-egress.ts +4 -4
- package/src/console/quota/native-identity.ts +189 -0
- package/src/console/remote-support/contracts.ts +12 -6
- package/src/console/remote-support/service.ts +2 -2
- package/src/console/routing/model/routes.ts +3 -3
- package/src/console/routing/pools/contracts.ts +11 -8
- package/src/console/routing/pools/operations.ts +14 -7
- package/src/console/routing/pools/probe-verdict.ts +8 -3
- package/src/console/routing/pools/relay-deploy.ts +9 -9
- package/src/console/routing/pools/routes.ts +3 -3
- package/src/console/routing/pools/store.ts +21 -1
- package/src/console/settings/routes.ts +3 -3
- package/src/console/share/share-router.ts +51 -18
- package/src/console/share/share-stats.ts +158 -49
- package/src/console/share/share-usage.ts +24 -7
- package/src/console/shared/read-through-cache.ts +38 -2
- package/src/console/system-prompt/contracts.ts +102 -0
- package/src/console/system-prompt/routes.ts +165 -0
- package/src/console/updates/routes.ts +7 -4
- package/src/main.ts +11 -0
- package/src/network/outbound-fetch.ts +75 -4
- package/src/network/pool/resolver.ts +39 -39
- package/src/network/pool-cooldown-store.ts +71 -16
- package/src/network/pool-health-machine.ts +21 -1
- package/src/network/rate-limit-verdict.ts +68 -0
- package/src/network/ssrf.ts +4 -4
- package/src/observability/event-query.ts +437 -22
- package/src/observability/telemetry-status.ts +1 -1
- package/src/persistence/db-pg.ts +42 -0
- package/src/persistence/db-sqlite.ts +7 -2
- package/src/persistence/readiness.ts +18 -6
- package/src/persistence/schema.ts +162 -1
- package/src/persistence/stores/api-key-store.ts +849 -581
- package/src/persistence/stores/share-store.ts +65 -13
- package/src/persistence/stores/telemetry-store.ts +58 -2
- package/src/persistence/system-prompt-store.ts +104 -0
- package/src/protocol/codecs.ts +6 -6
- package/src/protocol/gateway-error.ts +6 -3
- package/src/protocol/image-generation.ts +2 -2
- package/src/protocol/reasoning.ts +65 -1
- package/src/protocol/request/chat.ts +1 -0
- package/src/protocol/response/chat.ts +2 -2
- package/src/protocol/response/codex.ts +1 -1
- package/src/protocol/response/gemini.ts +1 -1
- package/src/protocol/response/messages.ts +5 -5
- package/src/protocol/response/responses.ts +2 -2
- package/src/protocol/sse-decoder.ts +14 -3
- package/src/protocol/translation/capabilities.ts +2 -2
- package/src/protocol/upstream/send-claude-messages.ts +3 -3
- package/src/providers/adapters/base-adapter.ts +1 -1
- package/src/providers/adapters/compatible-adapter.ts +2 -2
- package/src/providers/authentication/oauth-refresh-service.ts +2 -2
- package/src/providers/configured-provider.ts +1 -1
- package/src/providers/discovery/probe-phases.ts +2 -2
- package/src/providers/discovery/probing-service.ts +12 -12
- package/src/providers/integrations/agentrouter/agentrouter.ts +2 -3
- package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
- package/src/providers/integrations/anthropic.ts +4 -11
- package/src/providers/integrations/antigravity/antigravity.ts +33 -47
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +30 -2
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
- package/src/providers/integrations/buddy/buddy-identity.ts +22 -1
- package/src/providers/integrations/buddy/buddy-media.ts +21 -17
- package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
- package/src/providers/integrations/buddy/workbuddy/provider.ts +6 -2
- package/src/providers/integrations/claude/claude-betas.ts +1 -1
- package/src/providers/integrations/claude/claude-credentials.ts +1 -1
- package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
- package/src/providers/integrations/claude/claude.ts +3 -10
- package/src/providers/integrations/cline/cline.ts +8 -1
- package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
- package/src/providers/integrations/codex/codex-errors.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +14 -15
- package/src/providers/integrations/commandcode/commandcode.ts +4 -4
- package/src/providers/integrations/commandcode/quota.ts +5 -1
- package/src/providers/integrations/cursor/cursor-http2.ts +11 -11
- package/src/providers/integrations/cursor/cursor.ts +15 -15
- package/src/providers/integrations/factory/factory.ts +4 -4
- package/src/providers/integrations/gemini.ts +11 -11
- package/src/providers/integrations/github/github.ts +26 -7
- package/src/providers/integrations/grok/grok.ts +2 -6
- package/src/providers/integrations/hermes.ts +5 -1
- package/src/providers/integrations/inferhub.ts +4 -11
- package/src/providers/integrations/kilo/kilo.ts +4 -0
- package/src/providers/integrations/kimi/kimi.ts +4 -11
- package/src/providers/integrations/kiro/kiro.ts +10 -10
- package/src/providers/integrations/muse/muse.ts +6 -9
- package/src/providers/integrations/openrouter/provider.ts +4 -0
- package/src/providers/integrations/perplexity.ts +2 -2
- package/src/providers/integrations/qoder/qoder-catalog.ts +47 -9
- package/src/providers/integrations/qoder/qoder-checkin.ts +242 -51
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +157 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +12 -3
- package/src/providers/integrations/qoder/qoder-realm.ts +15 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +287 -0
- package/src/providers/integrations/qoder/qoder-virtual.ts +2 -2
- package/src/providers/integrations/qoder/qoder.ts +192 -24
- package/src/providers/integrations/tokenharbor.ts +2 -2
- package/src/providers/integrations/trae/trae.ts +6 -9
- package/src/providers/integrations/zcode/oauth.ts +1 -1
- package/src/providers/operations/account-error-classification.ts +6 -8
- package/src/providers/operations/account-health-service.ts +14 -16
- package/src/providers/operations/client-versions.ts +37 -22
- package/src/providers/operations/provider-catalog-service.ts +4 -4
- package/src/providers/operations/provider-credential-service.ts +16 -4
- package/src/providers/operations/upstream-deadline.ts +3 -3
- package/src/providers/provider-contracts.ts +55 -3
- package/src/providers/provider-credential-resolver.ts +1 -1
- package/src/providers/provider-registry.ts +4 -4
- package/src/providers/quota/quota-contracts.ts +24 -10
- package/src/providers/quota/refresh.ts +2 -2
- package/src/providers/search/search-provider.ts +5 -5
- package/src/security/abuse.ts +4 -4
- package/src/security/access-control.ts +19 -0
- package/src/security/admission/reasons.ts +17 -17
- package/src/security/api-key-auth.ts +18 -5
- package/src/shared/coerce.ts +19 -0
- package/src/transport/dispatch/attempt-finalize.ts +4 -3
- package/src/transport/dispatch/attempt-loop.ts +10 -7
- package/src/transport/dispatch/empty-stream.ts +6 -6
- package/src/transport/dispatch/generation.ts +9 -9
- package/src/transport/dispatch/leases.ts +8 -8
- package/src/transport/dispatch/proxy-request.ts +38 -9
- package/src/transport/dispatch/responses-compact.ts +11 -11
- package/src/transport/dispatch/retry-policy.ts +4 -67
- package/src/transport/dispatch/stream-bridge.ts +1 -1
- package/src/transport/dispatch/streaming/finalize.ts +9 -8
- package/src/transport/dispatch/streaming/stream.ts +16 -1
- package/src/transport/dispatch/systemone.ts +8 -8
- package/src/transport/dispatch/token-count.ts +9 -9
- package/src/transport/dispatch/upstream.ts +10 -0
- package/src/transport/dispatch/websearch.ts +6 -6
- package/src/transport/drain-endpoint.ts +2 -2
- package/src/transport/failure-policy.ts +13 -6
- package/src/transport/guards/auth-guard.ts +6 -5
- package/src/transport/guards/body-policy.ts +4 -4
- package/src/transport/guards/error-lifecycle.ts +18 -3
- package/src/transport/guards/guard-errors.ts +7 -3
- package/src/transport/guards/headroom-guard.ts +1 -1
- package/src/transport/guards/pipeline.ts +6 -6
- package/src/transport/guards/readiness-guard.ts +16 -2
- package/src/transport/guards/request-context.ts +3 -3
- package/src/transport/request/preparer.ts +37 -21
- package/src/transport/request/state.ts +4 -7
- package/src/transport/request/system-prompt-injection.ts +89 -0
- package/src/transport/request/tenant-preferences.ts +43 -0
- package/src/transport/routing/admission.ts +13 -3
- package/src/transport/routing/route-catalog.ts +104 -2
- package/src/transport/routing/route-model.ts +60 -7
- package/src/transport/routing/router.ts +306 -256
- package/src/transport/shutdown-notice.ts +19 -7
- package/src/transport/surface/completion.ts +1 -1
- package/src/transport/surface/messages/encode.ts +1 -1
- package/src/workers/daily-checkin.ts +2 -0
- package/dist/dashboard/assets/BannedPage-XaUmiBrC.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js +0 -1
- package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CXGS_Ct-.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BdmRLnq2.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js +0 -4
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js +0 -9
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js +0 -1
- package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js +0 -1
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-Dqf4zpS-.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-Byj2lAEm.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.js +0 -56
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js +0 -1
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css +0 -2
- package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
- package/dist/dashboard/assets/assert-67xXH5rf.js +0 -1
- package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
- package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
- package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
- package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js +0 -1
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
- package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
- package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CFM2sMX1.js +0 -1
- package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
- package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
- package/dist/dashboard/assets/entry-ClCMCWWQ.js +0 -4
- package/dist/dashboard/assets/entry-ClCMCWWQ.js.gz +0 -0
- package/dist/dashboard/assets/entry-_xHo5x6r.js +0 -1
- package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
- package/dist/dashboard/assets/formatters-CWWZ-uCZ.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
- package/dist/dashboard/assets/index-DFVFrnWW.js.gz +0 -0
- package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
- package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
- package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
- package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
- package/dist/dashboard/assets/live-D0mm--ME.js +0 -1
- package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
- package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
- package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
- package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
- package/dist/dashboard/assets/routing-BnIpi1IJ.js +0 -1
- package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
- package/dist/dashboard/assets/select-DBNVLdUu.js.gz +0 -0
- package/dist/dashboard/assets/settings-BAFhDgX8.js +0 -1
- package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
- package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
- package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
- package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js → app-BayTTy01.js} +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js.gz → app-BayTTy01.js.gz} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Elysia, t } from "elysia";
|
|
1
|
+
import { Elysia, t, type AnyElysia } from "elysia";
|
|
2
2
|
import { ConsoleDomainError, errorResponse, requireTenantScope } from "../../shared/errors";
|
|
3
3
|
import { literalUnion } from "../../shared/elysia-schema";
|
|
4
4
|
import type { AccessDecision } from "../../../security/access-control";
|
|
@@ -160,7 +160,7 @@ const updateRoutingBody = t.Object({
|
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
|
|
163
|
-
export function createProviderDetailRoutes(config: ProviderDetailConfig):
|
|
163
|
+
export function createProviderDetailRoutes(config: ProviderDetailConfig): AnyElysia {
|
|
164
164
|
const factory = createProviderDetailOperations(config);
|
|
165
165
|
return new Elysia({ prefix: "/providers" })
|
|
166
166
|
.get("/:providerId/routing", async ({ request, params, set }) => {
|
|
@@ -191,5 +191,5 @@ export function createProviderDetailRoutes(config: ProviderDetailConfig): Elysia
|
|
|
191
191
|
return providerDetailErrorResponse(e, set);
|
|
192
192
|
}
|
|
193
193
|
},
|
|
194
|
-
)
|
|
194
|
+
)
|
|
195
195
|
}
|
|
@@ -9,7 +9,7 @@ import type { ProviderRegistry } from "../../../providers/provider-registry";
|
|
|
9
9
|
import { browserAuthorizeRedirectUri } from "../../../config/providers";
|
|
10
10
|
import { log } from "../../../observability/logger";
|
|
11
11
|
import { GatewayError } from "../../../protocol/gateway-error";
|
|
12
|
-
import { Elysia, t } from "elysia";
|
|
12
|
+
import { Elysia, t, type AnyElysia } from "elysia";
|
|
13
13
|
import { ConsoleDomainError, errorResponse, requireScope } from "../../shared/errors";
|
|
14
14
|
import { generateOAuthState, generatePkcePair } from "../../../providers/authentication/oauth-flow-store";
|
|
15
15
|
import type { OAuthFlowStorage } from "../../../providers/authentication/oauth-flow-store";
|
|
@@ -639,7 +639,7 @@ const importBody = t.Object({
|
|
|
639
639
|
accountLabel: t.Optional(t.String()),
|
|
640
640
|
});
|
|
641
641
|
|
|
642
|
-
export function createOAuthLoginRoutes(config: OAuthLoginConfig):
|
|
642
|
+
export function createOAuthLoginRoutes(config: OAuthLoginConfig): AnyElysia {
|
|
643
643
|
const factory = createOAuthLoginOperations(config);
|
|
644
644
|
return new Elysia({ prefix: "/providers" })
|
|
645
645
|
.post(
|
|
@@ -721,6 +721,6 @@ export function createOAuthLoginRoutes(config: OAuthLoginConfig): Elysia {
|
|
|
721
721
|
return oauthErrorResponse(e, set);
|
|
722
722
|
}
|
|
723
723
|
},
|
|
724
|
-
)
|
|
724
|
+
)
|
|
725
725
|
}
|
|
726
726
|
|
|
@@ -43,6 +43,10 @@ import {
|
|
|
43
43
|
fetchQoderDailyCheckin,
|
|
44
44
|
isQoderCheckinProvider,
|
|
45
45
|
} from "../../providers/integrations/qoder/qoder-checkin";
|
|
46
|
+
import {
|
|
47
|
+
DrizzleQoderNativeIdentityStore,
|
|
48
|
+
type QoderNativeIdentityStore,
|
|
49
|
+
} from "../../providers/integrations/qoder/qoder-native-identity-store";
|
|
46
50
|
import { buddyAccountUid } from "../../providers/integrations/buddy/buddy-oauth-shared";
|
|
47
51
|
import {
|
|
48
52
|
supportsAccountReset,
|
|
@@ -50,6 +54,7 @@ import {
|
|
|
50
54
|
listAccountResetCredits,
|
|
51
55
|
} from "../../providers/operations/account-reset-service";
|
|
52
56
|
import {
|
|
57
|
+
enqueueBackgroundQuotaRefresh,
|
|
53
58
|
pendingQuotaRefreshes,
|
|
54
59
|
toQuotaAccountHealth,
|
|
55
60
|
toQuotaView,
|
|
@@ -108,6 +113,7 @@ async function runAccountCheckin(
|
|
|
108
113
|
account: { id: string; providerId: string },
|
|
109
114
|
credential: string,
|
|
110
115
|
forceClaim: boolean,
|
|
116
|
+
nativeIdentityStore?: QoderNativeIdentityStore,
|
|
111
117
|
): Promise<DailyCheckinResult> {
|
|
112
118
|
if (isQoderCheckinProvider(account.providerId)) {
|
|
113
119
|
return fetchQoderDailyCheckin({
|
|
@@ -115,6 +121,7 @@ async function runAccountCheckin(
|
|
|
115
121
|
accountId: account.id,
|
|
116
122
|
credential,
|
|
117
123
|
fetcher: globalThis.fetch,
|
|
124
|
+
...(nativeIdentityStore ? { nativeIdentityStore } : {}),
|
|
118
125
|
});
|
|
119
126
|
}
|
|
120
127
|
return fetchDailyCheckin({
|
|
@@ -202,6 +209,7 @@ async function runDailyGrowthForAccount(
|
|
|
202
209
|
accountId: account.id,
|
|
203
210
|
credential,
|
|
204
211
|
fetcher,
|
|
212
|
+
nativeIdentityStore: new DrizzleQoderNativeIdentityStore(refreshDeps.db),
|
|
205
213
|
})
|
|
206
214
|
: await fetchDailyCheckin({
|
|
207
215
|
providerId,
|
|
@@ -362,6 +370,15 @@ export function registerAccountQuotaTenantRoutes(
|
|
|
362
370
|
icon: providerId,
|
|
363
371
|
}));
|
|
364
372
|
let refreshing = 0;
|
|
373
|
+
// Cold-cache backfill: the overview renders whatever is cached, and the
|
|
374
|
+
// periodic sweep owns freshness — but with many accounts (e.g. a large
|
|
375
|
+
// Qoder roster) the sweep's per-pass cap cannot keep every cache warm,
|
|
376
|
+
// so a page open would show accounts with no badge for minutes. Enqueue
|
|
377
|
+
// a bounded background refresh for accounts whose cache entry is missing
|
|
378
|
+
// (not merely stale), so the badge fills in without waiting for the next
|
|
379
|
+
// sweep pass and without fanning out to the whole roster at once.
|
|
380
|
+
const coldCacheCap = 20;
|
|
381
|
+
let coldCacheEnqueued = 0;
|
|
365
382
|
// The sweep's day markers answer "attempted today" without an upstream
|
|
366
383
|
// request; only check-in-capable accounts can carry one, so only they
|
|
367
384
|
// are read.
|
|
@@ -397,6 +414,21 @@ export function registerAccountQuotaTenantRoutes(
|
|
|
397
414
|
const canRefresh = providerRegistry.hasQuotaCollector(row.providerId);
|
|
398
415
|
const isRefreshing = canRefresh && (pending.has(row.id) || entry === undefined);
|
|
399
416
|
if (isRefreshing) refreshing += 1;
|
|
417
|
+
// Backfill a missing cache entry on page open (bounded, see above).
|
|
418
|
+
if (
|
|
419
|
+
canRefresh &&
|
|
420
|
+
entry === undefined &&
|
|
421
|
+
!pending.has(row.id) &&
|
|
422
|
+
coldCacheEnqueued < coldCacheCap
|
|
423
|
+
) {
|
|
424
|
+
coldCacheEnqueued += 1;
|
|
425
|
+
enqueueBackgroundQuotaRefresh(refreshDeps, {
|
|
426
|
+
accountId: row.id,
|
|
427
|
+
providerId: row.providerId,
|
|
428
|
+
tenantId: row.tenantId,
|
|
429
|
+
credentialKind: row.credentialKind,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
400
432
|
return {
|
|
401
433
|
id: row.id,
|
|
402
434
|
provider: row.providerId,
|
|
@@ -518,7 +550,7 @@ export function registerAccountQuotaTenantRoutes(
|
|
|
518
550
|
throw new GatewayError(
|
|
519
551
|
"proxy_pool_unavailable",
|
|
520
552
|
503,
|
|
521
|
-
"Account test cannot select a proxy because proxy routing is not configured.",
|
|
553
|
+
"Account test cannot select a proxy because proxy routing is not configured (request {requestId}). No bridge to cross — configure a pool first 🏗️",
|
|
522
554
|
{ reason: "proxy_routing_unconfigured" },
|
|
523
555
|
"network",
|
|
524
556
|
);
|
|
@@ -884,7 +916,7 @@ export function registerAccountQuotaTenantRoutes(
|
|
|
884
916
|
account.providerId as ProviderId,
|
|
885
917
|
account.id,
|
|
886
918
|
);
|
|
887
|
-
result = await runAccountCheckin(account, credential, true);
|
|
919
|
+
result = await runAccountCheckin(account, credential, true, new DrizzleQoderNativeIdentityStore(db));
|
|
888
920
|
} catch (error) {
|
|
889
921
|
result = {
|
|
890
922
|
providerId: account.providerId,
|
|
@@ -2,13 +2,15 @@ import { Elysia } from "elysia";
|
|
|
2
2
|
import { consoleErrorHandler } from "../shared/errors";
|
|
3
3
|
import { registerAccountQuotaGlobalRoutes } from "./account-quota-global";
|
|
4
4
|
import { registerAccountQuotaTenantRoutes } from "./account-quota-tenant";
|
|
5
|
+
import { registerNativeIdentityRoutes } from "./native-identity";
|
|
5
6
|
import { createQuotaRefreshDeps, type AccountQuotaRoutesDeps } from "./account-quota-shared";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Mounts the account/quota console routes: the tenant-scoped group (the
|
|
9
10
|
* caller's own accounts) and the platform-admin group (shared accounts). Each
|
|
10
11
|
* group lives in its own module and authorises every route with its own
|
|
11
|
-
* access check.
|
|
12
|
+
* access check. The native-identity routes are mounted once, here, because
|
|
13
|
+
* they serve both owned and shared accounts with per-account authorization.
|
|
12
14
|
*/
|
|
13
15
|
export function createAccountQuotaRoutes(deps: AccountQuotaRoutesDeps): Elysia {
|
|
14
16
|
const refreshDeps = createQuotaRefreshDeps(deps);
|
|
@@ -18,5 +20,6 @@ export function createAccountQuotaRoutes(deps: AccountQuotaRoutesDeps): Elysia {
|
|
|
18
20
|
app.error(consoleErrorHandler("Quota operation failed"));
|
|
19
21
|
registerAccountQuotaTenantRoutes(app, deps, refreshDeps);
|
|
20
22
|
registerAccountQuotaGlobalRoutes(app, deps, refreshDeps);
|
|
23
|
+
registerNativeIdentityRoutes(app, { db: deps.db, accessResolver: deps.accessResolver });
|
|
21
24
|
return app;
|
|
22
25
|
}
|
|
@@ -138,7 +138,7 @@ export async function selectAccountTestEgress(
|
|
|
138
138
|
throw new GatewayError(
|
|
139
139
|
"proxy_pool_unavailable",
|
|
140
140
|
503,
|
|
141
|
-
"No active proxy pool is available because coordination is unavailable.",
|
|
141
|
+
"No active proxy pool is available because coordination is unavailable (request {requestId}). The bouncer stepped out — come back shortly 🚪",
|
|
142
142
|
{ reason: "coordination_unavailable" },
|
|
143
143
|
"network",
|
|
144
144
|
);
|
|
@@ -159,10 +159,10 @@ export async function selectAccountTestEgress(
|
|
|
159
159
|
const status = failure.reason === "at_capacity" ? 429 : 503;
|
|
160
160
|
const message =
|
|
161
161
|
failure.reason === "at_capacity"
|
|
162
|
-
? "Every proxy pool for this provider is at its concurrency limit. Retry shortly, or add a pool or raise its capacity."
|
|
162
|
+
? "Every proxy pool for this provider is at its concurrency limit (request {requestId}). Retry shortly, or add a pool or raise its capacity. The pools are full right now — chill 🌊"
|
|
163
163
|
: failure.reason === "cooldown"
|
|
164
|
-
? "Every proxy pool for this provider is cooling down after upstream failures. Retry once the cooldown resets."
|
|
165
|
-
: "No active proxy pool is configured for this tenant and provider. Add one, or route this provider through direct egress.";
|
|
164
|
+
? "Every proxy pool for this provider is cooling down after upstream failures (request {requestId}). Retry once the cooldown resets. The proxies are catching their breath — zzz 💤"
|
|
165
|
+
: "No active proxy pool is configured for this tenant and provider (request {requestId}). Add one, or route this provider through direct egress. No bridge to cross — configure a pool first 🏗️";
|
|
166
166
|
throw new GatewayError(
|
|
167
167
|
code,
|
|
168
168
|
status,
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qoder native-identity console operations, shared by the tenant-scoped and
|
|
3
|
+
* global-admin account route groups.
|
|
4
|
+
*
|
|
5
|
+
* The identity is the account's device identity (minted seed). The routes
|
|
6
|
+
* surface it for export/import/regenerate from the provider-detail screen;
|
|
7
|
+
* the device-uniqueness invariant lives in the store (and the partial unique
|
|
8
|
+
* index behind it).
|
|
9
|
+
*/
|
|
10
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
11
|
+
import { Elysia, t } from "elysia";
|
|
12
|
+
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
13
|
+
import { providerAccounts, type NativeIdentityRecord } from "../../persistence/schema";
|
|
14
|
+
import {
|
|
15
|
+
allocateQoderNativeIdentity,
|
|
16
|
+
DrizzleQoderNativeIdentityStore,
|
|
17
|
+
type QoderNativeIdentityStore,
|
|
18
|
+
} from "../../providers/integrations/qoder/qoder-native-identity-store";
|
|
19
|
+
import { ConsoleDomainError, requireGlobalAdmin, requireTenantScope } from "../shared/errors";
|
|
20
|
+
import type { ConsoleAccessResolver } from "../auth/access";
|
|
21
|
+
|
|
22
|
+
/** True when `token` looks like a plausible machine token. */
|
|
23
|
+
export function isValidNativeIdentityToken(token: string): boolean {
|
|
24
|
+
return typeof token === "string" && token.length >= 8 && token.length <= 512;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Allocates a fresh device identity for regenerate. Injected so tests can stub
|
|
29
|
+
* the native bridge; the default runs the real `runtime-info` bridge through
|
|
30
|
+
* the store.
|
|
31
|
+
*/
|
|
32
|
+
export type NativeIdentityAllocator = (args: {
|
|
33
|
+
providerId: string;
|
|
34
|
+
accountId: string;
|
|
35
|
+
avoidToken?: string;
|
|
36
|
+
}) => Promise<NativeIdentityRecord | null>;
|
|
37
|
+
|
|
38
|
+
function defaultAllocator(store: QoderNativeIdentityStore): NativeIdentityAllocator {
|
|
39
|
+
return ({ providerId, accountId, avoidToken }) =>
|
|
40
|
+
allocateQoderNativeIdentity(store, providerId, accountId, {
|
|
41
|
+
...(avoidToken !== undefined ? { avoidToken } : {}),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Loads one account row, scoped by tenant (null = global/shared). */
|
|
46
|
+
async function loadAccount(
|
|
47
|
+
db: CartethyiaDatabase,
|
|
48
|
+
accountId: string,
|
|
49
|
+
tenantId: string | null,
|
|
50
|
+
): Promise<{ providerId: string; nativeIdentity: NativeIdentityRecord | null } | undefined> {
|
|
51
|
+
const rows = await db
|
|
52
|
+
.select({
|
|
53
|
+
providerId: providerAccounts.providerId,
|
|
54
|
+
nativeIdentity: providerAccounts.nativeIdentity,
|
|
55
|
+
})
|
|
56
|
+
.from(providerAccounts)
|
|
57
|
+
.where(
|
|
58
|
+
tenantId === null
|
|
59
|
+
? and(eq(providerAccounts.id, accountId), isNull(providerAccounts.tenantId))
|
|
60
|
+
: and(eq(providerAccounts.id, accountId), eq(providerAccounts.tenantId, tenantId)),
|
|
61
|
+
)
|
|
62
|
+
.limit(1);
|
|
63
|
+
return rows[0];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface NativeIdentityOps {
|
|
67
|
+
get(accountId: string, tenantId: string | null): Promise<NativeIdentityRecord | null>;
|
|
68
|
+
put(accountId: string, tenantId: string | null, record: NativeIdentityRecord): Promise<NativeIdentityRecord>;
|
|
69
|
+
regenerate(accountId: string, tenantId: string | null): Promise<NativeIdentityRecord | null>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createNativeIdentityOps(
|
|
73
|
+
db: CartethyiaDatabase,
|
|
74
|
+
allocator: NativeIdentityAllocator = defaultAllocator(new DrizzleQoderNativeIdentityStore(db)),
|
|
75
|
+
): NativeIdentityOps {
|
|
76
|
+
const store = new DrizzleQoderNativeIdentityStore(db);
|
|
77
|
+
return {
|
|
78
|
+
async get(accountId, tenantId) {
|
|
79
|
+
const account = await loadAccount(db, accountId, tenantId);
|
|
80
|
+
// A missing row under the narrowed tenant scope means the account is not
|
|
81
|
+
// visible to this caller (cross-tenant read) — report not-found, the
|
|
82
|
+
// same convention the account routes use, rather than echoing null.
|
|
83
|
+
if (!account) throw new ConsoleDomainError("not_found", 404, "Account not found");
|
|
84
|
+
return account.nativeIdentity;
|
|
85
|
+
},
|
|
86
|
+
async put(accountId, tenantId, record) {
|
|
87
|
+
const account = await loadAccount(db, accountId, tenantId);
|
|
88
|
+
if (!account) throw new ConsoleDomainError("not_found", 404, "Account not found");
|
|
89
|
+
if (!isValidNativeIdentityToken(record.machineToken)) {
|
|
90
|
+
throw new ConsoleDomainError("invalid_identity", 400, "machineToken must be a non-empty string (8–512 chars)");
|
|
91
|
+
}
|
|
92
|
+
if (!record.machineType || !record.machineCode) {
|
|
93
|
+
throw new ConsoleDomainError("invalid_identity", 400, "machineToken, machineType, and machineCode are required");
|
|
94
|
+
}
|
|
95
|
+
// Device uniqueness: another account already owns this token? The pool is
|
|
96
|
+
// global across Qoder accounts (both realms), matching the unique index.
|
|
97
|
+
const occupied = await store.occupied(accountId);
|
|
98
|
+
if (occupied.has(record.machineToken)) {
|
|
99
|
+
throw new ConsoleDomainError("slot_taken", 409, `Native identity token is already bound to another account`);
|
|
100
|
+
}
|
|
101
|
+
await store.bind(account.providerId, accountId, record);
|
|
102
|
+
return record;
|
|
103
|
+
},
|
|
104
|
+
async regenerate(accountId, tenantId) {
|
|
105
|
+
const account = await loadAccount(db, accountId, tenantId);
|
|
106
|
+
if (!account) throw new ConsoleDomainError("not_found", 404, "Account not found");
|
|
107
|
+
return allocator({
|
|
108
|
+
providerId: account.providerId,
|
|
109
|
+
accountId,
|
|
110
|
+
...(account.nativeIdentity?.machineToken !== undefined ? { avoidToken: account.nativeIdentity.machineToken } : {}),
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The authorization decision for one account's native-identity route.
|
|
118
|
+
*
|
|
119
|
+
* Account ownership decides which rule applies: shared accounts
|
|
120
|
+
* (`tenant_id IS NULL`) need platform admin; tenant-owned accounts need the
|
|
121
|
+
* tenant scope for the owning tenant.
|
|
122
|
+
*/
|
|
123
|
+
export interface NativeIdentityScope {
|
|
124
|
+
readonly tenantId: string | null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Registers the native-identity routes on `app` (path
|
|
129
|
+
* `/provider-accounts/:id/native-identity`).
|
|
130
|
+
*
|
|
131
|
+
* `authorize` performs the access check based on the account's owner and
|
|
132
|
+
* returns the tenant scope to load the row under, so a shared account is
|
|
133
|
+
* reachable only by a platform admin and a tenant-owned account only by its
|
|
134
|
+
* tenant.
|
|
135
|
+
*/
|
|
136
|
+
export function registerNativeIdentityRoutes(
|
|
137
|
+
app: Elysia,
|
|
138
|
+
deps: { readonly db: CartethyiaDatabase; readonly accessResolver: ConsoleAccessResolver },
|
|
139
|
+
allocator?: NativeIdentityAllocator,
|
|
140
|
+
): void {
|
|
141
|
+
const ops = createNativeIdentityOps(deps.db, allocator);
|
|
142
|
+
const authorize = async (
|
|
143
|
+
request: Request,
|
|
144
|
+
accountId: string,
|
|
145
|
+
scope: "dashboard:read" | "dashboard:write",
|
|
146
|
+
): Promise<NativeIdentityScope> => {
|
|
147
|
+
const rows = await deps.db
|
|
148
|
+
.select({ tenantId: providerAccounts.tenantId })
|
|
149
|
+
.from(providerAccounts)
|
|
150
|
+
.where(eq(providerAccounts.id, accountId))
|
|
151
|
+
.limit(1);
|
|
152
|
+
if (rows.length === 0) {
|
|
153
|
+
throw new ConsoleDomainError("not_found", 404, "Account not found");
|
|
154
|
+
}
|
|
155
|
+
const ownerTenantId = rows[0]!.tenantId;
|
|
156
|
+
if (ownerTenantId === null) {
|
|
157
|
+
requireGlobalAdmin(deps.accessResolver(request));
|
|
158
|
+
return { tenantId: null };
|
|
159
|
+
}
|
|
160
|
+
const access = requireTenantScope(deps.accessResolver(request), scope);
|
|
161
|
+
return { tenantId: access.tenantId };
|
|
162
|
+
};
|
|
163
|
+
app
|
|
164
|
+
.get("/provider-accounts/:id/native-identity", async ({ request, params }) => {
|
|
165
|
+
const { tenantId } = await authorize(request, params.id, "dashboard:read");
|
|
166
|
+
return { identity: await ops.get(params.id, tenantId) };
|
|
167
|
+
})
|
|
168
|
+
.put(
|
|
169
|
+
"/provider-accounts/:id/native-identity",
|
|
170
|
+
{
|
|
171
|
+
body: t.Object({
|
|
172
|
+
machineToken: t.String(),
|
|
173
|
+
machineType: t.String(),
|
|
174
|
+
machineCode: t.String(),
|
|
175
|
+
seed: t.Optional(t.String()),
|
|
176
|
+
}),
|
|
177
|
+
},
|
|
178
|
+
async ({ request, params, body }) => {
|
|
179
|
+
const { tenantId } = await authorize(request, params.id, "dashboard:write");
|
|
180
|
+
const identity = await ops.put(params.id, tenantId, body as NativeIdentityRecord);
|
|
181
|
+
return { identity };
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
.post("/provider-accounts/:id/native-identity/regenerate", async ({ request, params }) => {
|
|
185
|
+
const { tenantId } = await authorize(request, params.id, "dashboard:write");
|
|
186
|
+
const identity = await ops.regenerate(params.id, tenantId);
|
|
187
|
+
return { identity };
|
|
188
|
+
});
|
|
189
|
+
}
|
|
@@ -51,12 +51,18 @@ export interface RemoteSupportStartResponse extends RemoteSupportStatus {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/** Human-readable install guidance, keyed by tool. Never an installer. */
|
|
54
|
-
export
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
export function remoteSupportInstallHint(tool: RemoteSupportTool): string {
|
|
55
|
+
// Cross-OS install hint: pick the host's package manager. A Windows-only
|
|
56
|
+
// hint on Linux/macOS would be nonsense (and vice versa).
|
|
57
|
+
const win = process.platform === "win32";
|
|
58
|
+
const mac = process.platform === "darwin";
|
|
59
|
+
const installCommand = (pkg: string): string =>
|
|
60
|
+
win ? `winget install ${pkg}` : mac ? `brew install ${pkg}` : `sudo apt-get install -y ${pkg}`;
|
|
61
|
+
if (tool === "cloudflared") {
|
|
62
|
+
return `cloudflared is not installed. Install it yourself (${installCommand("cloudflared")}) and try again.`;
|
|
63
|
+
}
|
|
64
|
+
return `tailscale is not installed. Install it yourself (${installCommand("tailscale")}), run \`tailscale up\` once, then try again.`;
|
|
65
|
+
}
|
|
60
66
|
|
|
61
67
|
export function isRemoteSupportTool(value: unknown): value is RemoteSupportTool {
|
|
62
68
|
return typeof value === "string" && (REMOTE_SUPPORT_TOOLS as readonly string[]).includes(value);
|
|
@@ -18,7 +18,7 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, write
|
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
import { resolveUserConfigDir } from "../../config/package-paths";
|
|
20
20
|
import {
|
|
21
|
-
|
|
21
|
+
remoteSupportInstallHint,
|
|
22
22
|
REMOTE_SUPPORT_TOOLS,
|
|
23
23
|
type RemoteSupportBlocker,
|
|
24
24
|
type RemoteSupportStatus,
|
|
@@ -430,7 +430,7 @@ export function createRemoteSupportService(config: RemoteSupportServiceConfig):
|
|
|
430
430
|
active: null,
|
|
431
431
|
url: null,
|
|
432
432
|
blocker: "not_installed",
|
|
433
|
-
message:
|
|
433
|
+
message: remoteSupportInstallHint(tool),
|
|
434
434
|
port,
|
|
435
435
|
installed: await detectInstalled(),
|
|
436
436
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Model routing control-plane HTTP routes. The only module that touches Elysia.
|
|
2
|
-
import { Elysia, t } from "elysia";
|
|
2
|
+
import { Elysia, t, type AnyElysia } from "elysia";
|
|
3
3
|
import { errorResponse } from "../../shared/errors";
|
|
4
4
|
import { literalUnion } from "../../shared/elysia-schema";
|
|
5
5
|
import { modelComboStrategy, type ComboStrategy } from "../../../persistence/schema";
|
|
@@ -41,7 +41,7 @@ const updateComboBody = t.Object({
|
|
|
41
41
|
members: t.Optional(t.Array(t.String(), { minItems: 1 })),
|
|
42
42
|
strategy: t.Optional(comboStrategySchema),
|
|
43
43
|
});
|
|
44
|
-
export function createModelRoutingRoutes(config: ModelRoutingConfig):
|
|
44
|
+
export function createModelRoutingRoutes(config: ModelRoutingConfig): AnyElysia {
|
|
45
45
|
const factory = createModelRoutingOperations(config);
|
|
46
46
|
return new Elysia({ prefix: "/routing" })
|
|
47
47
|
.get("/aliases", async ({ request, set }) => {
|
|
@@ -132,5 +132,5 @@ export function createModelRoutingRoutes(config: ModelRoutingConfig): Elysia {
|
|
|
132
132
|
} catch (e) {
|
|
133
133
|
return modelRoutingErrorResponse(e, set);
|
|
134
134
|
}
|
|
135
|
-
})
|
|
135
|
+
});
|
|
136
136
|
}
|
|
@@ -188,7 +188,7 @@ export function validateTransportConfig(
|
|
|
188
188
|
case "http":
|
|
189
189
|
case "https":
|
|
190
190
|
if (cfg.timeout !== undefined && typeof cfg.timeout !== "number")
|
|
191
|
-
throw new ConsoleDomainError("
|
|
191
|
+
throw new ConsoleDomainError("invalid_pool", 400, "HTTP timeout must be a number", { field: "timeout" });
|
|
192
192
|
return;
|
|
193
193
|
case "socks5":
|
|
194
194
|
return;
|
|
@@ -196,9 +196,10 @@ export function validateTransportConfig(
|
|
|
196
196
|
// Compile-time exhaustiveness: new TransportKind variants must add a case.
|
|
197
197
|
kind satisfies never;
|
|
198
198
|
throw new ConsoleDomainError(
|
|
199
|
-
"
|
|
199
|
+
"invalid_pool",
|
|
200
200
|
400,
|
|
201
201
|
`Unsupported transport kind: ${String(kind)}`,
|
|
202
|
+
{ field: "kind" },
|
|
202
203
|
);
|
|
203
204
|
}
|
|
204
205
|
}
|
|
@@ -494,10 +495,10 @@ export function validateEndpoint(
|
|
|
494
495
|
try {
|
|
495
496
|
u = new URL(candidate);
|
|
496
497
|
} catch {
|
|
497
|
-
throw new ConsoleDomainError("
|
|
498
|
+
throw new ConsoleDomainError("invalid_pool", 400, "Endpoint must be host:port", { field: "endpoint" });
|
|
498
499
|
}
|
|
499
500
|
if (!u.hostname || !u.port)
|
|
500
|
-
throw new ConsoleDomainError("
|
|
501
|
+
throw new ConsoleDomainError("invalid_pool", 400, "Endpoint must be host:port", { field: "endpoint" });
|
|
501
502
|
if (u.hostname === "localhost" || u.hostname.endsWith(".internal"))
|
|
502
503
|
throw new ConsoleDomainError("ssrf_rejected", 400, "Private endpoint rejected");
|
|
503
504
|
if (isIP(u.hostname) !== 0 && !isAddressAllowed(u.hostname, policy))
|
|
@@ -508,10 +509,10 @@ export function validateEndpoint(
|
|
|
508
509
|
try {
|
|
509
510
|
u = new URL(endpoint);
|
|
510
511
|
} catch {
|
|
511
|
-
throw new ConsoleDomainError("
|
|
512
|
+
throw new ConsoleDomainError("invalid_pool", 400, "Endpoint must be a valid http(s) URL", { field: "endpoint" });
|
|
512
513
|
}
|
|
513
514
|
if (!["http:", "https:"].includes(u.protocol)) {
|
|
514
|
-
throw new ConsoleDomainError("
|
|
515
|
+
throw new ConsoleDomainError("invalid_pool", 400, "Endpoint must be a valid http(s) URL", { field: "endpoint" });
|
|
515
516
|
}
|
|
516
517
|
if (u.hostname === "localhost" || u.hostname.endsWith(".internal")) {
|
|
517
518
|
throw new ConsoleDomainError("ssrf_rejected", 400, "Private endpoint rejected");
|
|
@@ -529,9 +530,10 @@ export function validatePoolLimits(maxInflight?: number, weight?: number): void
|
|
|
529
530
|
(!Number.isInteger(maxInflight) || maxInflight < 1 || maxInflight > MAX_POOL_INFLIGHT)
|
|
530
531
|
) {
|
|
531
532
|
throw new ConsoleDomainError(
|
|
532
|
-
"
|
|
533
|
+
"invalid_pool",
|
|
533
534
|
400,
|
|
534
535
|
"maxInflight must be an integer between 1 and 1000000",
|
|
536
|
+
{ field: "maxInflight" },
|
|
535
537
|
);
|
|
536
538
|
}
|
|
537
539
|
if (
|
|
@@ -539,9 +541,10 @@ export function validatePoolLimits(maxInflight?: number, weight?: number): void
|
|
|
539
541
|
(!Number.isInteger(weight) || weight < 1 || weight > MAX_POOL_WEIGHT)
|
|
540
542
|
) {
|
|
541
543
|
throw new ConsoleDomainError(
|
|
542
|
-
"
|
|
544
|
+
"invalid_pool",
|
|
543
545
|
400,
|
|
544
546
|
"weight must be an integer between 1 and 1000",
|
|
547
|
+
{ field: "weight" },
|
|
545
548
|
);
|
|
546
549
|
}
|
|
547
550
|
}
|
|
@@ -141,9 +141,10 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
|
|
|
141
141
|
const rotateCount = request.rotateCount ?? current.rotateCount;
|
|
142
142
|
if (!Number.isInteger(rotateCount) || rotateCount < 1 || rotateCount > 1000) {
|
|
143
143
|
throw new ConsoleDomainError(
|
|
144
|
-
"
|
|
144
|
+
"invalid_pool",
|
|
145
145
|
400,
|
|
146
146
|
"rotateCount must be an integer between 1 and 1000",
|
|
147
|
+
{ field: "rotateCount" },
|
|
147
148
|
);
|
|
148
149
|
}
|
|
149
150
|
const saved = await config.store.setStrategy(a.tenantId, { strategy, rotateCount });
|
|
@@ -447,6 +448,12 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
|
|
|
447
448
|
const a = requireTenantScope(access, "dashboard:write");
|
|
448
449
|
const recovered = await config.store.recover(a.tenantId, poolId);
|
|
449
450
|
if (!recovered) throw new ConsoleDomainError("pool_not_found", 404, `Pool ${poolId} not found`);
|
|
451
|
+
// Recover is the operator's "make this pool healthy again": besides
|
|
452
|
+
// resetting the pool-level state, drop every provider-rate-limit marker
|
|
453
|
+
// (volatile and durable) so a just-cleared pool is not still excluded
|
|
454
|
+
// by a stale 429 flag.
|
|
455
|
+
await config.poolSelector?.clearPoolCooldowns(poolId).catch(() => undefined);
|
|
456
|
+
await config.store.dropAllProviderCooldownMarkers?.(a.tenantId, poolId);
|
|
450
457
|
await config.poolAgentReleaser?.releasePool(poolId, a.tenantId);
|
|
451
458
|
await config.auditSink?.record({
|
|
452
459
|
access: a,
|
|
@@ -482,7 +489,7 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
|
|
|
482
489
|
): Promise<PoolSpeedTestResult> {
|
|
483
490
|
const a = requireTenantScope(access, "dashboard:read");
|
|
484
491
|
if (!config.store.speedTest) {
|
|
485
|
-
throw new ConsoleDomainError("
|
|
492
|
+
throw new ConsoleDomainError("capability_unsupported", 400, "Speed tests are unavailable");
|
|
486
493
|
}
|
|
487
494
|
if (!Number.isInteger(bytes) || bytes < SPEED_TEST_MIN_BYTES || bytes > SPEED_TEST_MAX_BYTES) {
|
|
488
495
|
throw new ConsoleDomainError(
|
|
@@ -507,7 +514,7 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
|
|
|
507
514
|
): Promise<PoolProviderProbeResult> {
|
|
508
515
|
const a = requireTenantScope(access, "dashboard:read");
|
|
509
516
|
if (!config.store.providerProbe) {
|
|
510
|
-
throw new ConsoleDomainError("
|
|
517
|
+
throw new ConsoleDomainError("capability_unsupported", 400, "Provider probing is unavailable");
|
|
511
518
|
}
|
|
512
519
|
const result = await config.store.providerProbe(a.tenantId, poolId, providerId);
|
|
513
520
|
if (!config.poolSelector) return result;
|
|
@@ -596,7 +603,7 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
|
|
|
596
603
|
);
|
|
597
604
|
}
|
|
598
605
|
if (!config.store.probeAdHocProvider) {
|
|
599
|
-
throw new ConsoleDomainError("
|
|
606
|
+
throw new ConsoleDomainError("capability_unsupported", 400, "Provider probing is unavailable");
|
|
600
607
|
}
|
|
601
608
|
const probe = config.store.probeAdHocProvider.bind(config.store);
|
|
602
609
|
const results: PoolBatchProviderProbeResult[] = new Array(requests.length);
|
|
@@ -632,7 +639,7 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
|
|
|
632
639
|
if (config.store.probeAdHoc) {
|
|
633
640
|
return config.store.probeAdHoc(a.tenantId, request);
|
|
634
641
|
}
|
|
635
|
-
throw new ConsoleDomainError("
|
|
642
|
+
throw new ConsoleDomainError("capability_unsupported", 400, "Ad-hoc pool probing is unavailable");
|
|
636
643
|
},
|
|
637
644
|
/**
|
|
638
645
|
* Probes many unsaved pool definitions in one call.
|
|
@@ -656,7 +663,7 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
|
|
|
656
663
|
);
|
|
657
664
|
}
|
|
658
665
|
if (!config.store.probeAdHoc) {
|
|
659
|
-
throw new ConsoleDomainError("
|
|
666
|
+
throw new ConsoleDomainError("capability_unsupported", 400, "Ad-hoc pool probing is unavailable");
|
|
660
667
|
}
|
|
661
668
|
const probe = config.store.probeAdHoc.bind(config.store);
|
|
662
669
|
const results: PoolBatchProbeResult[] = new Array(requests.length);
|
|
@@ -700,7 +707,7 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
|
|
|
700
707
|
): Promise<NetworkPoolResponse & { relayUrl: string }> {
|
|
701
708
|
const a = requireTenantScope(access, "dashboard:write");
|
|
702
709
|
if (!config.relayFetch)
|
|
703
|
-
throw new ConsoleDomainError("
|
|
710
|
+
throw new ConsoleDomainError("capability_unsupported", 503, "Relay deployment is unavailable");
|
|
704
711
|
if (!isRelayTarget(request.target))
|
|
705
712
|
throw new ConsoleDomainError("invalid_request", 422, `Unsupported relay target: ${String(request.target)}`);
|
|
706
713
|
const result = await deployRelayWorker(config.relayFetch, request);
|
|
@@ -15,8 +15,16 @@
|
|
|
15
15
|
* the address works (`allowed`, `throttled`) or says nothing about it
|
|
16
16
|
* (`unreachable`), and treating "says nothing" as "refused" would drop a usable
|
|
17
17
|
* pool for good.
|
|
18
|
+
*
|
|
19
|
+
* The 403-free-tier and 429 verdicts are the probe's reading of the same facts
|
|
20
|
+
* `classifyEgressFailure` (in `network/rate-limit-verdict`) reads on the
|
|
21
|
+
* dispatch path — for the probe they arrive as an HTTP status + body instead of
|
|
22
|
+
* a `GatewayError`, so the shared free-tier regex is borrowed from there to
|
|
23
|
+
* keep the two classifiers' boundaries honest.
|
|
18
24
|
*/
|
|
19
25
|
|
|
26
|
+
import { FREE_TIER_REFUSAL } from "../../../network/rate-limit-verdict";
|
|
27
|
+
|
|
20
28
|
export type ProbeVerdict = "allowed" | "blocked" | "throttled" | "unreachable";
|
|
21
29
|
|
|
22
30
|
export interface ProbeAnswer {
|
|
@@ -25,9 +33,6 @@ export interface ProbeAnswer {
|
|
|
25
33
|
readonly reason?: string;
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
/** The upstream's own wording for "this address is retired". */
|
|
29
|
-
const FREE_TIER_REFUSAL = /freetier|free tier can only be used/i;
|
|
30
|
-
|
|
31
36
|
/**
|
|
32
37
|
* Classifies one upstream response.
|
|
33
38
|
*
|