cartethyia 2.8.0 → 2.8.1
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 +3 -3
- package/dist/dashboard/assets/{BannedPage-Cc0vKpMG.js → BannedPage-Dq530awD.js} +1 -1
- package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-Bqj2aa1T.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-CXsaOAO2.js → CliToolsPage-DaZZ3cqP.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BeLpJ4ma.js → CombosPage-BlC_xdkX.js} +1 -1
- package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-DnV0uZ3m.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-CBxAjo63.js → CustomizationPage-7xT5yg2E.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-B5yEE0k6.js → HealthEventsModal-Clq8E9cW.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-BWFLECmz.js → InjectSystemPromptPanel-6Teoq9Se.js} +1 -1
- package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-CgoPc5B9.js → LoginPage-CZuI0MAR.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-D9bFQern.js → ModelPicker-C1hNTlkr.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-B_p4tL8k.js → OverviewPage-0yfhSI1C.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
- package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
- package/dist/dashboard/assets/{ProviderIcon-D_0PiZtg.js → ProviderIcon-Ddhm1InL.js} +1 -1
- package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-CLI2bJTK.js → ProvidersPage-C3fYuu6i.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
- package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-DA-a1iSN.js → QuotaPage-D1RV_2BX.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-CctTga2C.js → SettingsPage-BzZAvYSX.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-febLU4r1.js → SetupPage--s-EfEIL.js} +1 -1
- package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-M6fvR5Wz.js → StudioPage-BjXFGSK4.js} +1 -1
- package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-nyeuRqz9.js → UsagePage-CP8F8BN7.js} +1 -1
- package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-BFOlvrJC.js → api-keys-2hc12Uz2.js} +1 -1
- package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
- package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
- package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
- package/dist/dashboard/assets/{assert-CqTLw5e7.js → assert-BMcj1xZE.js} +1 -1
- package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
- package/dist/dashboard/assets/{badge-C83EqLB0.js → badge-DkRUuVfz.js} +1 -1
- package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
- package/dist/dashboard/assets/{banner-D1xn7pIe.js → banner-CDjW841Q.js} +1 -1
- package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
- package/dist/dashboard/assets/{card-DU3Lq5Lm.js → card-Bto5UlL9.js} +1 -1
- package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-Dw1U_drK.js → cli-tools-DaH6g9hL.js} +1 -1
- package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-CEL5Sg_J.js → clipboard-button-BYKBCzjk.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BjFR5UHy.js → dialog-DL0Zftwv.js} +1 -1
- package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-HvBW0AjI.js → drawer-Dpx_UXaS.js} +1 -1
- package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CECVELyy.js → entry-BV7-1NoM.js} +1 -1
- package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
- package/dist/dashboard/assets/{entry-DYYofQ7W.js → entry-CGvdNU6r.js} +1 -1
- package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BSu3gFlt.js → entry-DScKYvZj.js} +3 -3
- package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-DLW4i8zK.js → formatters-BS3DZlZj.js} +1 -1
- package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
- package/dist/dashboard/assets/{index-1-fCqe64.js → index-SQnvvTWX.js} +2 -2
- package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DGvU2oLK.js → inline-Dl-cuLK0.js} +1 -1
- package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
- package/dist/dashboard/assets/{input-iNnO1X2K.js → input-Bfx0EzRc.js} +1 -1
- package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
- package/dist/dashboard/assets/{layout-0q8UGypb.js → layout-BxhbQ_JS.js} +1 -1
- package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-C0w2bQjJ.js → list-row-D49Y4ptn.js} +1 -1
- package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
- package/dist/dashboard/assets/{live-M5aJ_xCa.js → live-CbVTdX9V.js} +1 -1
- package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
- package/dist/dashboard/assets/{network-BKeQGT3K.js → network-JGvDveof.js} +1 -1
- package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-CivAk3da.js → numbers-Dw6kiew-.js} +1 -1
- package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-CpzvI5ep.js → pagination-iUoI8GeE.js} +1 -1
- package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
- package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
- package/dist/dashboard/assets/providers/sensenova.png +0 -0
- package/dist/dashboard/assets/{routing-BFQX2VBc.js → routing-8jZr-3WE.js} +1 -1
- package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
- package/dist/dashboard/assets/{select-CXbck0jG.js → select-oO1FxMVb.js} +1 -1
- package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
- package/dist/dashboard/assets/{settings-C_XvMHh9.js → settings-CLr3kdIx.js} +1 -1
- package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DggRiCxE.js → stack-Y3pDteZr.js} +1 -1
- package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
- package/dist/dashboard/assets/{state-ClFzfbYp.js → state-CArX_Iak.js} +1 -1
- package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
- package/dist/dashboard/assets/{switch-CTIFxKqS.js → switch-DwlwmDO-.js} +1 -1
- package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BClG3yWj.js → useMutation-DJ9GYEgf.js} +1 -1
- package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +51 -51
- package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
- package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
- package/package.json +1 -1
- package/src/boot/app.ts +1 -1
- package/src/boot/dependencies.ts +4 -6
- package/src/config/providers.ts +6 -5
- package/src/config/spec.ts +8 -8
- package/src/console/backup/nine-router.ts +71 -10
- package/src/console/backup/service.ts +8 -1
- package/src/console/console-router.ts +1 -1
- package/src/console/domain-registration.ts +7 -2
- package/src/console/inferhub/client.ts +1 -1
- package/src/console/observability/live.ts +2 -2
- package/src/console/providers/catalog/contracts.ts +9 -3
- package/src/console/providers/catalog/model-operations.ts +2 -3
- package/src/console/providers/catalog/provider-operations.ts +1 -1
- package/src/console/providers/catalog/routes.ts +2 -2
- package/src/console/providers/catalog/store.ts +1914 -1907
- package/src/console/providers/detail/routes.ts +1 -0
- package/src/console/providers/detail/store.ts +4 -0
- package/src/console/providers/oauth/callback-listener.ts +30 -11
- package/src/console/providers/oauth/device-egress.ts +123 -0
- package/src/console/providers/oauth/routes.ts +123 -16
- package/src/console/quota/account-quota-tenant.ts +5 -5
- package/src/console/quota/account-test-egress.ts +2 -2
- package/src/console/quota/daily-growth-job.ts +1 -1
- package/src/console/routing/pools/contracts.ts +1 -1
- package/src/console/routing/pools/operations.ts +1 -1
- package/src/console/routing/pools/probe-verdict.ts +2 -2
- package/src/console/routing/pools/relay-deploy.ts +1 -1
- package/src/console/routing/pools/store.ts +4 -4
- package/src/network/client.ts +67 -0
- package/src/network/egress/bidi-session.ts +322 -0
- package/src/network/egress/dial.ts +201 -0
- package/src/network/egress/factory.ts +191 -0
- package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
- package/src/network/egress/tls-profile.ts +123 -0
- package/src/network/egress/ws.ts +175 -0
- package/src/network/{types.ts → pool/agent-config.ts} +1 -1
- package/src/network/pool/agent.ts +2 -2
- package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +7 -7
- package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
- package/src/network/pool/resolver.ts +10 -849
- package/src/network/pool/selector.ts +2 -2
- package/src/network/ssrf.ts +1 -1
- package/src/network/{retry.ts → util/retry.ts} +1 -1
- package/src/observability/telemetry-buffer.ts +1 -1
- package/src/persistence/schema.ts +9 -0
- package/src/persistence/ttl-cache.ts +1 -1
- package/src/protocol/codecs.ts +58 -1
- package/src/protocol/request/messages.ts +31 -18
- package/src/protocol/response/messages.ts +34 -7
- package/src/protocol/translation/quirks.ts +15 -0
- package/src/protocol/upstream/send-claude-messages.ts +2 -2
- package/src/providers/adapters/compatible-adapter.ts +5 -0
- package/src/providers/authentication/oauth-client.ts +40 -6
- package/src/providers/authentication/oauth-device-flow.ts +7 -3
- package/src/providers/authentication/oauth-flow-store.ts +82 -1
- package/src/providers/authentication/oauth-refresh-service.ts +766 -739
- package/src/providers/default-registry.ts +76 -0
- package/src/providers/discovery/discovery-types.ts +7 -0
- package/src/providers/discovery/probe-phases.ts +6 -2
- package/src/providers/discovery/probing-service.ts +7 -0
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +11 -0
- package/src/providers/integrations/buddy/account.ts +969 -0
- package/src/providers/integrations/buddy/capabilities.ts +10 -10
- package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
- package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +5 -5
- package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
- package/src/providers/integrations/buddy/codebuddy/shared.ts +4 -4
- package/src/providers/integrations/buddy/{buddy-identity.ts → headers.ts} +138 -140
- package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
- package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
- package/src/providers/integrations/cline/cline-oauth.ts +10 -5
- package/src/providers/integrations/cline/cline.ts +3 -4
- package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
- package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
- package/src/providers/integrations/clixad/provider.ts +145 -0
- package/src/providers/integrations/codex/codex-oauth.ts +8 -5
- package/src/providers/integrations/cursor/cursor-auth.ts +1 -1
- package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
- package/src/providers/integrations/cursor/cursor-shared.ts +1 -1
- package/src/providers/integrations/cursor/cursor.ts +485 -70
- package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
- package/src/providers/integrations/deepai/deepai-common.ts +145 -0
- package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
- package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
- package/src/providers/integrations/deepai/provider.ts +42 -0
- package/src/providers/integrations/devin/devin-auth.ts +7 -0
- package/src/providers/integrations/factory/factory-oauth.ts +16 -7
- package/src/providers/integrations/freebuff/freebuff.ts +2 -2
- package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
- package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
- package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
- package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
- package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
- package/src/providers/integrations/freepi/freepi-session.ts +158 -0
- package/src/providers/integrations/freepi/freepi.ts +80 -54
- package/src/providers/integrations/github/github-discovery.ts +30 -13
- package/src/providers/integrations/github/github-oauth.ts +217 -44
- package/src/providers/integrations/github/github-quota.ts +22 -5
- package/src/providers/integrations/github/github.ts +40 -32
- package/src/providers/integrations/grok/grok-oauth.ts +4 -4
- package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
- package/src/providers/integrations/hermes/hermes.ts +34 -5
- package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
- package/src/providers/integrations/kilo/kilo.ts +1 -1
- package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
- package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
- package/src/providers/integrations/muse/muse-oauth.ts +5 -5
- package/src/providers/integrations/opencode/fingerprint.ts +1 -1
- package/src/providers/integrations/openrouter/provider.ts +1 -1
- package/src/providers/integrations/perch/perch-protocol.ts +0 -3
- package/src/providers/integrations/perch/perch.ts +3 -3
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +1 -1
- package/src/providers/integrations/qoder/qoder-adapter.ts +19 -113
- package/src/providers/integrations/qoder/qoder-auth.ts +8 -1
- package/src/providers/integrations/qoder/qoder-catalog.ts +1 -1
- package/src/providers/integrations/qoder/qoder-chat.ts +25 -8
- package/src/providers/integrations/qoder/qoder-checkin.ts +1 -1
- package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
- package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
- package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
- package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
- package/src/providers/integrations/qoder/qoder-types.ts +9 -0
- package/src/providers/integrations/qoder/qoder.ts +3 -25
- package/src/providers/integrations/sensenova/provider.ts +144 -0
- package/src/providers/integrations/trae/trae-quota.ts +15 -1
- package/src/providers/integrations/vercel/provider.ts +117 -0
- package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
- package/src/providers/integrations/xai/xai-oauth.ts +5 -5
- package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
- package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
- package/src/providers/integrations/zro/zro.ts +2 -1
- package/src/providers/operations/account-refresh-status.ts +14 -0
- package/src/providers/operations/client-versions.ts +95 -30
- package/src/providers/operations/oauth-refresh-lead.ts +46 -22
- package/src/providers/operations/provider-caches.ts +1 -1
- package/src/providers/operations/provider-credential-service.ts +22 -25
- package/src/providers/operations/session-resolution.ts +17 -1
- package/src/providers/provider-contracts.ts +2 -2
- package/src/providers/provider-metadata.ts +30 -0
- package/src/providers/quota/quota-contracts.ts +2 -0
- package/src/providers/quota/refresh.ts +25 -3
- package/src/security/ip-boundary.ts +1 -1
- package/src/transport/dispatch/attempt-finalize.ts +1 -1
- package/src/transport/dispatch/attempt-loop.ts +3 -3
- package/src/transport/dispatch/handler-deps.ts +1 -1
- package/src/transport/dispatch/leases.ts +1 -1
- package/src/transport/dispatch/retry-policy.ts +1 -1
- package/src/transport/dispatch/streaming/finalize.ts +2 -2
- package/src/transport/dispatch/streaming/stream.ts +1 -1
- package/src/transport/dispatch/upstream.ts +1 -1
- package/src/transport/shutdown-notice.ts +1 -1
- package/src/workers/buddy-growth-tasks.ts +8 -8
- package/src/workers/daily-checkin.ts +4 -4
- package/src/workers/quota-refresh-sweep.ts +1 -1
- package/src/workers/zcode-gift-claim.ts +1 -1
- package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +0 -8
- package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js +0 -9
- package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
- package/dist/dashboard/assets/app-Dx2QgsNj.css +0 -2
- package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
- package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
- package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
- package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
- package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
- package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
- package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
- package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
- package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
- package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
- package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
- package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
- package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
- package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
- package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
- package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
- package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
- package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
- package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
- package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
- package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
- package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
- package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
- package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
- package/src/console/providers/catalog/discover-constants.ts +0 -6
- package/src/network/pool-health.ts +0 -198
- package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
- package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
- package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
- package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
- package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -188
- package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
- package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -179
- package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
- package/src/providers/integrations/qoder/chat-proto.ts +0 -328
- package/src/providers/integrations/qoder/qoder-grpc.ts +0 -870
- package/src/providers/integrations/qoder/qoder-ide.ts +0 -481
- /package/dist/dashboard/assets/{app-DIXLjojg.js → app-C0gTXEoN.js} +0 -0
- /package/dist/dashboard/assets/{app-DIXLjojg.js.gz → app-C0gTXEoN.js.gz} +0 -0
- /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
- /package/src/network/pool/{byte-accounting.ts → accounting.ts} +0 -0
- /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
- /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
- /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
- /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
- /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
- /package/src/network/{rate-limit-verdict.ts → verdict.ts} +0 -0
|
@@ -163,6 +163,7 @@ const updateRoutingBody = t.Object({
|
|
|
163
163
|
usageFloor: t.Optional(t.Nullable(t.Integer({ minimum: 0, maximum: 1_000_000 }))),
|
|
164
164
|
usageFloorUnit: t.Optional(t.Union([t.Literal("credit"), t.Literal("percent")])),
|
|
165
165
|
skipProxy: t.Optional(t.Boolean()),
|
|
166
|
+
deviceFlowProxy: t.Optional(t.Union([t.Literal("random"), t.Literal("direct")])),
|
|
166
167
|
userAgent: t.Optional(t.String({ minLength: 0, maxLength: 4096 })),
|
|
167
168
|
customUserAgent: t.Optional(t.Nullable(t.String({ minLength: 0, maxLength: 4096 }))),
|
|
168
169
|
});
|
|
@@ -40,6 +40,7 @@ function mapRow(row: typeof providerRoutingSettings.$inferSelect): ProviderRouti
|
|
|
40
40
|
usageFloorUnit: row.usageFloorUnit === "credit" ? "credit" : "percent",
|
|
41
41
|
enabled: row.enabled,
|
|
42
42
|
skipProxy: row.skipProxy,
|
|
43
|
+
deviceFlowProxy: row.deviceFlowProxy === "direct" ? "direct" : "random",
|
|
43
44
|
userAgent: row.userAgent,
|
|
44
45
|
customUserAgent: row.customUserAgent,
|
|
45
46
|
};
|
|
@@ -94,6 +95,7 @@ export class DrizzleProviderDetailStore implements ProviderDetailStore {
|
|
|
94
95
|
usageFloorUnit: "percent",
|
|
95
96
|
enabled: false,
|
|
96
97
|
skipProxy: defaultSkipProxy(providerId),
|
|
98
|
+
deviceFlowProxy: "random",
|
|
97
99
|
userAgent: buildClaudeCliUserAgent(),
|
|
98
100
|
customUserAgent: null,
|
|
99
101
|
};
|
|
@@ -117,6 +119,7 @@ export class DrizzleProviderDetailStore implements ProviderDetailStore {
|
|
|
117
119
|
usageFloorUnit: patch.usageFloorUnit ?? "percent",
|
|
118
120
|
enabled: patch.enabled ?? false,
|
|
119
121
|
skipProxy: patch.skipProxy ?? defaultSkipProxy(providerId),
|
|
122
|
+
deviceFlowProxy: patch.deviceFlowProxy ?? "random",
|
|
120
123
|
userAgent: patch.userAgent ?? buildClaudeCliUserAgent(),
|
|
121
124
|
customUserAgent: patch.customUserAgent ?? null,
|
|
122
125
|
};
|
|
@@ -130,6 +133,7 @@ export class DrizzleProviderDetailStore implements ProviderDetailStore {
|
|
|
130
133
|
if (patch.usageFloorUnit !== undefined) setClause.usageFloorUnit = patch.usageFloorUnit;
|
|
131
134
|
if (patch.enabled !== undefined) setClause.enabled = patch.enabled;
|
|
132
135
|
if (patch.skipProxy !== undefined) setClause.skipProxy = patch.skipProxy;
|
|
136
|
+
if (patch.deviceFlowProxy !== undefined) setClause.deviceFlowProxy = patch.deviceFlowProxy;
|
|
133
137
|
if (patch.userAgent !== undefined) setClause.userAgent = patch.userAgent;
|
|
134
138
|
if (patch.customUserAgent !== undefined) setClause.customUserAgent = patch.customUserAgent;
|
|
135
139
|
const upserted = await this.db
|
|
@@ -158,14 +158,14 @@ export class OAuthCallbackListener {
|
|
|
158
158
|
/**
|
|
159
159
|
* The paths one login may deliver its callback on. Kiro's portal is observed
|
|
160
160
|
* (unpacked IDE 1.4.62) to redirect to both `/oauth/callback` and
|
|
161
|
-
* `/signin/callback`;
|
|
162
|
-
*
|
|
163
|
-
*
|
|
161
|
+
* `/signin/callback`; DeepAI's desktop flow lands on `/auth-callback`.
|
|
162
|
+
* Binding only the advertised path left a redirect to the other one hitting
|
|
163
|
+
* a 404 and the login hanging. Other providers neither register nor expect
|
|
164
|
+
* these, so adding them is harmless.
|
|
164
165
|
*/
|
|
165
166
|
#callbackPaths(advertised: string): readonly string[] {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
: [advertised, "/signin/callback"];
|
|
167
|
+
const extras = advertised === "/signin/callback" ? [] : ["/signin/callback", "/auth-callback"];
|
|
168
|
+
return [advertised, ...extras];
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
/** Drops a flow and frees its port when nothing else is waiting on it. */
|
|
@@ -235,13 +235,32 @@ export class OAuthCallbackListener {
|
|
|
235
235
|
listener.flows.set(state, { providerId, paths, timer, inFlight: false });
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
#forget(
|
|
238
|
+
#forget(
|
|
239
|
+
port: number,
|
|
240
|
+
listener: PortListener,
|
|
241
|
+
state: string,
|
|
242
|
+
options: { readonly deferStop?: boolean } = {},
|
|
243
|
+
): void {
|
|
239
244
|
const flow = listener.flows.get(state);
|
|
240
245
|
if (flow !== undefined) clearTimeout(flow.timer);
|
|
241
246
|
listener.flows.delete(state);
|
|
242
247
|
if (listener.flows.size > 0) return;
|
|
243
248
|
this.#ports.delete(port);
|
|
244
|
-
|
|
249
|
+
const stopServers = () => {
|
|
250
|
+
for (const server of listener.servers) void server.stop(true);
|
|
251
|
+
};
|
|
252
|
+
if (options.deferStop === true) {
|
|
253
|
+
// The stop is called from inside a callback handler that is about to
|
|
254
|
+
// return its Response. `stop(true)` closes active connections, so
|
|
255
|
+
// stopping synchronously here kills the very response the browser is
|
|
256
|
+
// waiting for — the login "succeeds" server-side while the callback tab
|
|
257
|
+
// sees a connection reset (observed live: HTTP 000, and more reliably on
|
|
258
|
+
// deployed/docker hosts). Defer the stop one tick so the response is
|
|
259
|
+
// flushed first; the port is freed a moment later.
|
|
260
|
+
setTimeout(stopServers, 50);
|
|
261
|
+
} else {
|
|
262
|
+
stopServers();
|
|
263
|
+
}
|
|
245
264
|
log.info(`[oauth] callback listener released on 127.0.0.1:${port}`);
|
|
246
265
|
}
|
|
247
266
|
|
|
@@ -311,7 +330,7 @@ export class OAuthCallbackListener {
|
|
|
311
330
|
if (error !== null) {
|
|
312
331
|
// A denial carries the state only when the server echoes one; clear it
|
|
313
332
|
// when present so a retry starts a fresh flow.
|
|
314
|
-
if (state.length > 0) this.#forget(endpoint.port, listener, state);
|
|
333
|
+
if (state.length > 0) this.#forget(endpoint.port, listener, state, { deferStop: true });
|
|
315
334
|
return new Response(
|
|
316
335
|
`<!doctype html><html><body><p>OAuth login failed: ${escapeHtml(error)}</p></body></html>`,
|
|
317
336
|
{ status: 400, headers: { "content-type": "text/html; charset=utf-8" } },
|
|
@@ -374,7 +393,7 @@ export class OAuthCallbackListener {
|
|
|
374
393
|
Object.keys(callbackParams).length > 0 ? callbackParams : undefined,
|
|
375
394
|
);
|
|
376
395
|
} catch (failure) {
|
|
377
|
-
if (matchedState !== undefined) this.#forget(endpoint.port, listener, matchedState);
|
|
396
|
+
if (matchedState !== undefined) this.#forget(endpoint.port, listener, matchedState, { deferStop: true });
|
|
378
397
|
const message = failure instanceof Error ? failure.message : "OAuth login failed";
|
|
379
398
|
log.error(`[oauth] callback completion failed for ${matched.providerId}: ${message}`);
|
|
380
399
|
return new Response(
|
|
@@ -393,7 +412,7 @@ export class OAuthCallbackListener {
|
|
|
393
412
|
headers: { location: result.redirectUrl },
|
|
394
413
|
});
|
|
395
414
|
}
|
|
396
|
-
if (matchedState !== undefined) this.#forget(endpoint.port, listener, matchedState);
|
|
415
|
+
if (matchedState !== undefined) this.#forget(endpoint.port, listener, matchedState, { deferStop: true });
|
|
397
416
|
if (!result.ok) {
|
|
398
417
|
return new Response(
|
|
399
418
|
`<!doctype html><html><body><p>OAuth login failed: ${escapeHtml(result.message)}</p></body></html>`,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Device-flow OAuth egress resolution.
|
|
2
|
+
//
|
|
3
|
+
// Device-code flows (start + poll) previously always dialed from the
|
|
4
|
+
// gateway's own egress, so every code minted on one host appeared to come
|
|
5
|
+
// from a single IP — which let Cursor/Grok/other device-code providers flag
|
|
6
|
+
// "multiple codes from one IP". The `device_flow_proxy` routing setting says
|
|
7
|
+
// whether to route these requests through one of the tenant's active network
|
|
8
|
+
// pools ("random", the default) or direct ("direct").
|
|
9
|
+
//
|
|
10
|
+
// The resolved fetch is a validated fetch bound to either a pool or the
|
|
11
|
+
// gateway's direct egress, and is injected into the OAuth flow context so the
|
|
12
|
+
// provider client uses it for its device-authorization and token-poll calls.
|
|
13
|
+
|
|
14
|
+
import { eq, and, isNull } from "drizzle-orm";
|
|
15
|
+
import type { DeviceFlowProxyMode } from "../../../persistence/schema";
|
|
16
|
+
import { networkPools, providerRoutingSettings } from "../../../persistence/schema";
|
|
17
|
+
import type { CartethyiaDatabase } from "../../../persistence/db-handle";
|
|
18
|
+
import type { NetworkPoolSelector } from "../../../network/pool/selector";
|
|
19
|
+
import type { ValidatedNetworkBindingFactory } from "../../../network/egress/factory";
|
|
20
|
+
import type { FetchLike } from "../../../providers/authentication/oauth-flow-store";
|
|
21
|
+
|
|
22
|
+
/** A resolved device-flow egress: the fetch to use, plus its release hook. */
|
|
23
|
+
export interface DeviceEgress {
|
|
24
|
+
readonly fetch: FetchLike;
|
|
25
|
+
/** Release an acquired pool slot (no-op for direct egress). */
|
|
26
|
+
readonly release: () => void;
|
|
27
|
+
/** The pool selected, when one was (for correlation/persistence). */
|
|
28
|
+
readonly networkPoolId?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Dependencies the device-egress resolver needs. */
|
|
32
|
+
export interface DeviceEgressDeps {
|
|
33
|
+
readonly db: CartethyiaDatabase;
|
|
34
|
+
readonly poolSelector: NetworkPoolSelector;
|
|
35
|
+
readonly networkBindingFactory: ValidatedNetworkBindingFactory;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Reads the provider's `device_flow_proxy` mode; absent rows default to `random`. */
|
|
39
|
+
async function resolveDeviceFlowProxyMode(
|
|
40
|
+
db: CartethyiaDatabase,
|
|
41
|
+
providerId: string,
|
|
42
|
+
tenantId: string | null,
|
|
43
|
+
): Promise<DeviceFlowProxyMode> {
|
|
44
|
+
if (tenantId !== null) {
|
|
45
|
+
const tenantRow = await db
|
|
46
|
+
.select({ mode: providerRoutingSettings.deviceFlowProxy })
|
|
47
|
+
.from(providerRoutingSettings)
|
|
48
|
+
.where(
|
|
49
|
+
and(
|
|
50
|
+
eq(providerRoutingSettings.providerId, providerId),
|
|
51
|
+
eq(providerRoutingSettings.tenantId, tenantId),
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
.limit(1);
|
|
55
|
+
if (tenantRow[0]) return tenantRow[0].mode === "direct" ? "direct" : "random";
|
|
56
|
+
}
|
|
57
|
+
const globalRow = await db
|
|
58
|
+
.select({ mode: providerRoutingSettings.deviceFlowProxy })
|
|
59
|
+
.from(providerRoutingSettings)
|
|
60
|
+
.where(
|
|
61
|
+
and(
|
|
62
|
+
eq(providerRoutingSettings.providerId, providerId),
|
|
63
|
+
isNull(providerRoutingSettings.tenantId),
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
.limit(1);
|
|
67
|
+
return globalRow[0]?.mode === "direct" ? "direct" : "random";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolves the egress for one device-code flow. `random` selects an active
|
|
72
|
+
* network pool (weighted, cooldown-aware) when one is available and falls
|
|
73
|
+
* back to direct egress when the tenant has no usable pool; `direct` always
|
|
74
|
+
* dials direct. The returned fetch is what the provider client must use for
|
|
75
|
+
* its device-authorization and token-poll requests.
|
|
76
|
+
*/
|
|
77
|
+
export async function resolveDeviceEgress(
|
|
78
|
+
deps: DeviceEgressDeps,
|
|
79
|
+
providerId: string,
|
|
80
|
+
tenantId: string | null,
|
|
81
|
+
): Promise<DeviceEgress> {
|
|
82
|
+
const mode = await resolveDeviceFlowProxyMode(deps.db, providerId, tenantId);
|
|
83
|
+
const direct = (): DeviceEgress => ({
|
|
84
|
+
fetch: deps.networkBindingFactory.fetch(undefined, tenantId ?? undefined),
|
|
85
|
+
release: () => undefined,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (mode === "direct" || tenantId === null) return direct();
|
|
89
|
+
|
|
90
|
+
const poolRows = await deps.db
|
|
91
|
+
.select({
|
|
92
|
+
id: networkPools.id,
|
|
93
|
+
maxInflight: networkPools.maxInflight,
|
|
94
|
+
weight: networkPools.weight,
|
|
95
|
+
})
|
|
96
|
+
.from(networkPools)
|
|
97
|
+
.where(and(eq(networkPools.tenantId, tenantId), eq(networkPools.status, "active")));
|
|
98
|
+
|
|
99
|
+
if (poolRows.length > 0) {
|
|
100
|
+
const limits: Record<string, number> = {};
|
|
101
|
+
const weights: Record<string, number> = {};
|
|
102
|
+
for (const pool of poolRows) {
|
|
103
|
+
limits[pool.id] = pool.maxInflight ?? 10;
|
|
104
|
+
weights[pool.id] = pool.weight ?? 100;
|
|
105
|
+
}
|
|
106
|
+
const slot = await deps.poolSelector.tryAcquireAvailablePool(
|
|
107
|
+
poolRows.map((pool) => pool.id),
|
|
108
|
+
providerId,
|
|
109
|
+
limits,
|
|
110
|
+
weights,
|
|
111
|
+
);
|
|
112
|
+
if (slot) {
|
|
113
|
+
return {
|
|
114
|
+
fetch: deps.networkBindingFactory.fetch(slot.poolId, tenantId ?? undefined),
|
|
115
|
+
release: slot.release,
|
|
116
|
+
networkPoolId: slot.poolId,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// No active/usable pool: direct is the safe fallback (never block a login).
|
|
122
|
+
return direct();
|
|
123
|
+
}
|
|
@@ -2,11 +2,17 @@ import { providerAccounts, providerOauthStates } from "../../../persistence/sche
|
|
|
2
2
|
import { and, eq, isNull, sql } from "drizzle-orm";
|
|
3
3
|
import { encryptCredential, hashSecret } from "../../../security/crypto";
|
|
4
4
|
import type { CartethyiaDatabase } from "../../../persistence/db-handle";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
OAuthDeviceStartResult,
|
|
7
|
+
OAuthExchangeResult,
|
|
8
|
+
OAuthLoginClient,
|
|
9
|
+
} from "../../../providers/authentication/oauth-flow-store";
|
|
10
|
+
import { oauthAccountIdentity } from "../../../providers/authentication/oauth-flow-store";
|
|
6
11
|
import type { OAuthDevicePollResponse } from "../catalog/contracts";
|
|
7
12
|
import { providerJwtVerification } from "../../../providers/provider-metadata";
|
|
8
13
|
import type { ProviderRegistry } from "../../../providers/provider-registry";
|
|
9
14
|
import { browserAuthorizeRedirectUri } from "../../../config/providers";
|
|
15
|
+
import { resolveDeviceEgress, type DeviceEgressDeps } from "./device-egress";
|
|
10
16
|
import { log } from "../../../observability/logger";
|
|
11
17
|
import { GatewayError } from "../../../protocol/gateway-error";
|
|
12
18
|
import { Elysia, t, type AnyElysia } from "elysia";
|
|
@@ -41,11 +47,53 @@ export function oauthIdentityFingerprint(input: {
|
|
|
41
47
|
readonly label: string;
|
|
42
48
|
readonly providerId: string;
|
|
43
49
|
} & OAuthExchangeResult): string {
|
|
50
|
+
const identity = input.accountEmail?.trim() ?? input.accountName?.trim();
|
|
51
|
+
if (identity !== undefined && identity.length > 0) return hashSecret(identity);
|
|
44
52
|
const label = input.label.trim();
|
|
45
|
-
const resolved =
|
|
53
|
+
const resolved =
|
|
54
|
+
label.length > 0 &&
|
|
55
|
+
label.toLowerCase() !== input.providerId.trim().toLowerCase() &&
|
|
56
|
+
!isGeneratedOAuthLabel(label, input.providerId);
|
|
46
57
|
return hashSecret(resolved ? label : input.refresh);
|
|
47
58
|
}
|
|
48
59
|
|
|
60
|
+
/** First unused `<providerId>-N` label for an OAuth account with no upstream identity. */
|
|
61
|
+
export function nextGeneratedOAuthAccountLabel(
|
|
62
|
+
providerId: string,
|
|
63
|
+
existingLabels: readonly string[],
|
|
64
|
+
): string {
|
|
65
|
+
const taken = new Set(existingLabels.map((label) => label.trim().toLowerCase()));
|
|
66
|
+
for (let n = 1; ; n += 1) {
|
|
67
|
+
const candidate = `${providerId}-${n}`;
|
|
68
|
+
if (!taken.has(candidate.toLowerCase())) return candidate;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Generated labels are display-only and must never become re-login identity. */
|
|
73
|
+
export function isGeneratedOAuthLabel(label: string, providerId: string): boolean {
|
|
74
|
+
const value = label.trim().toLowerCase();
|
|
75
|
+
const prefix = `${providerId.trim().toLowerCase()}-`;
|
|
76
|
+
return value === providerId.trim().toLowerCase() || (value.startsWith(prefix) && /^\d+$/.test(value.slice(prefix.length)));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Merges provider-reported identity with the operator label without inventing data. */
|
|
80
|
+
function resolvedOAuthIdentity(
|
|
81
|
+
input: { readonly label: string } & OAuthExchangeResult,
|
|
82
|
+
providerId: string,
|
|
83
|
+
): { readonly label: string; readonly accountEmail?: string; readonly accountName?: string } {
|
|
84
|
+
const tokenIdentity = oauthAccountIdentity(input);
|
|
85
|
+
const accountEmail = input.accountEmail?.trim() ?? tokenIdentity.accountEmail;
|
|
86
|
+
const accountName = input.accountName?.trim() ?? tokenIdentity.accountName;
|
|
87
|
+
const existing = input.label.trim();
|
|
88
|
+
const unresolved = existing.length === 0 || isGeneratedOAuthLabel(existing, providerId);
|
|
89
|
+
const label = unresolved ? (accountEmail ?? accountName ?? existing) : existing;
|
|
90
|
+
return {
|
|
91
|
+
label,
|
|
92
|
+
...(accountEmail === undefined ? {} : { accountEmail }),
|
|
93
|
+
...(accountName === undefined ? {} : { accountName }),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
49
97
|
export class DrizzleOAuthAccountStore implements OAuthAccountStore {
|
|
50
98
|
constructor(private readonly db: CartethyiaDatabase) {}
|
|
51
99
|
|
|
@@ -54,7 +102,8 @@ export class DrizzleOAuthAccountStore implements OAuthAccountStore {
|
|
|
54
102
|
providerId: string,
|
|
55
103
|
input: { readonly label: string } & OAuthExchangeResult,
|
|
56
104
|
): Promise<PersistedOAuthAccount> {
|
|
57
|
-
const
|
|
105
|
+
const identity = resolvedOAuthIdentity(input, providerId);
|
|
106
|
+
const credentialFingerprint = oauthIdentityFingerprint({ ...input, ...identity, providerId });
|
|
58
107
|
const sameAccount = and(
|
|
59
108
|
eq(providerAccounts.providerId, providerId),
|
|
60
109
|
// The identity index coalesces a null tenant to a sentinel, so "same
|
|
@@ -68,6 +117,20 @@ export class DrizzleOAuthAccountStore implements OAuthAccountStore {
|
|
|
68
117
|
const refreshCiphertext = encryptCredential(input.refresh);
|
|
69
118
|
const clientSecretCiphertext =
|
|
70
119
|
input.client_secret === undefined ? undefined : encryptCredential(input.client_secret);
|
|
120
|
+
const identityColumns = {
|
|
121
|
+
...(input.auth_state === undefined && identity.accountName === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: {
|
|
124
|
+
authState: {
|
|
125
|
+
...(input.auth_state ?? {}),
|
|
126
|
+
...(identity.accountName === undefined ? {} : { accountName: identity.accountName }),
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
...(identity.accountEmail === undefined ? {} : { accountEmail: identity.accountEmail }),
|
|
130
|
+
...(identity.accountEmail !== undefined || identity.accountName !== undefined
|
|
131
|
+
? { identityCheckedAt: new Date() }
|
|
132
|
+
: {}),
|
|
133
|
+
};
|
|
71
134
|
try {
|
|
72
135
|
return await this.db.transaction(async (tx) => {
|
|
73
136
|
const existing = await tx
|
|
@@ -76,6 +139,7 @@ export class DrizzleOAuthAccountStore implements OAuthAccountStore {
|
|
|
76
139
|
.where(sameAccount)
|
|
77
140
|
.limit(1);
|
|
78
141
|
const existingId = existing[0]?.id;
|
|
142
|
+
const label = isGeneratedOAuthLabel(input.label, providerId) ? identity.label : input.label;
|
|
79
143
|
if (existingId !== undefined) {
|
|
80
144
|
// A re-login replaces the account it belongs to rather than adding a
|
|
81
145
|
// second row: the same email is the same account, and the newer
|
|
@@ -90,7 +154,7 @@ export class DrizzleOAuthAccountStore implements OAuthAccountStore {
|
|
|
90
154
|
// the learned label — but never over an operator's custom rename.
|
|
91
155
|
// The CASE keeps the read and the write in one statement, so a
|
|
92
156
|
// concurrent rename cannot lose to the login.
|
|
93
|
-
label: sql`CASE WHEN lower(trim(${providerAccounts.label})) = lower(trim(${providerAccounts.providerId}))
|
|
157
|
+
label: sql`CASE WHEN lower(trim(${providerAccounts.label})) = lower(trim(${providerAccounts.providerId})) OR lower(trim(${providerAccounts.label})) = lower(trim(${label})) THEN ${label} ELSE ${providerAccounts.label} END`,
|
|
94
158
|
credentialCiphertext,
|
|
95
159
|
credentialKind: "oauth",
|
|
96
160
|
status: "active",
|
|
@@ -106,7 +170,7 @@ export class DrizzleOAuthAccountStore implements OAuthAccountStore {
|
|
|
106
170
|
staticToken: false,
|
|
107
171
|
// Only what this login reported: a flow that learned no profile or
|
|
108
172
|
// region leaves the stored configuration alone rather than blanking it.
|
|
109
|
-
...
|
|
173
|
+
...identityColumns,
|
|
110
174
|
})
|
|
111
175
|
.where(eq(providerAccounts.id, existingId));
|
|
112
176
|
await tx
|
|
@@ -119,17 +183,34 @@ export class DrizzleOAuthAccountStore implements OAuthAccountStore {
|
|
|
119
183
|
.where(eq(providerOauthStates.providerAccountId, existingId));
|
|
120
184
|
return { accountId: existingId };
|
|
121
185
|
}
|
|
186
|
+
const existingLabels = await tx
|
|
187
|
+
.select({ label: providerAccounts.label })
|
|
188
|
+
.from(providerAccounts)
|
|
189
|
+
.where(
|
|
190
|
+
and(
|
|
191
|
+
eq(providerAccounts.providerId, providerId),
|
|
192
|
+
tenantId === null
|
|
193
|
+
? isNull(providerAccounts.tenantId)
|
|
194
|
+
: eq(providerAccounts.tenantId, tenantId),
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
const generatedLabel = isGeneratedOAuthLabel(label, providerId)
|
|
198
|
+
? nextGeneratedOAuthAccountLabel(
|
|
199
|
+
providerId,
|
|
200
|
+
existingLabels.map((row) => row.label).filter((value): value is string => typeof value === "string"),
|
|
201
|
+
)
|
|
202
|
+
: label;
|
|
122
203
|
const rows = await tx
|
|
123
204
|
.insert(providerAccounts)
|
|
124
205
|
.values({
|
|
125
206
|
providerId,
|
|
126
207
|
tenantId,
|
|
127
|
-
label:
|
|
208
|
+
label: generatedLabel,
|
|
128
209
|
credentialKind: "oauth",
|
|
129
210
|
credentialCiphertext,
|
|
130
211
|
credentialFingerprint,
|
|
131
212
|
status: "active",
|
|
132
|
-
...
|
|
213
|
+
...identityColumns,
|
|
133
214
|
})
|
|
134
215
|
.returning({ id: providerAccounts.id });
|
|
135
216
|
const row = rows[0];
|
|
@@ -194,6 +275,8 @@ export interface OAuthLoginConfig {
|
|
|
194
275
|
* itself instead of waiting for the operator to paste the URL back.
|
|
195
276
|
*/
|
|
196
277
|
readonly callbackListener: OAuthCallbackListener;
|
|
278
|
+
/** Resolves device-flow OAuth egress per the `device_flow_proxy` setting. */
|
|
279
|
+
readonly deviceEgress: DeviceEgressDeps;
|
|
197
280
|
}
|
|
198
281
|
|
|
199
282
|
async function requireClient(
|
|
@@ -290,7 +373,7 @@ export async function completeLogin(
|
|
|
290
373
|
);
|
|
291
374
|
}
|
|
292
375
|
await config.accountStore.persistAccount(flow.tenantId, providerId, {
|
|
293
|
-
label: result.accountLabel ?? flow.accountLabel,
|
|
376
|
+
label: result.accountLabel ?? (flow.accountLabel.trim().toLowerCase() === providerId.trim().toLowerCase() ? "" : flow.accountLabel),
|
|
294
377
|
...result,
|
|
295
378
|
});
|
|
296
379
|
// The exchange succeeded and the account is persisted; now the flow is
|
|
@@ -442,7 +525,7 @@ function createOAuthLoginOperations(config: OAuthLoginConfig) {
|
|
|
442
525
|
{ providerId },
|
|
443
526
|
);
|
|
444
527
|
}
|
|
445
|
-
const state = generateOAuthState();
|
|
528
|
+
const state = client.generateState?.() ?? generateOAuthState();
|
|
446
529
|
const { codeVerifier, codeChallenge } = generatePkcePair();
|
|
447
530
|
/**
|
|
448
531
|
* A client whose authorize URL depends on an upstream discovery runs it
|
|
@@ -547,12 +630,25 @@ function createOAuthLoginOperations(config: OAuthLoginConfig) {
|
|
|
547
630
|
`Provider ${providerId} does not support device-code login`,
|
|
548
631
|
{ providerId },
|
|
549
632
|
);
|
|
550
|
-
const
|
|
633
|
+
const egress = await resolveDeviceEgress(
|
|
634
|
+
config.deviceEgress,
|
|
551
635
|
providerId,
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
636
|
+
a.tenantId,
|
|
637
|
+
);
|
|
638
|
+
let started: OAuthDeviceStartResult;
|
|
639
|
+
try {
|
|
640
|
+
started = await client.startDeviceAuth({
|
|
641
|
+
providerId,
|
|
642
|
+
tenantId: a.tenantId,
|
|
643
|
+
accountLabel,
|
|
644
|
+
fetchFn: egress.fetch,
|
|
645
|
+
...(parameters === undefined ? {} : { parameters }),
|
|
646
|
+
});
|
|
647
|
+
} finally {
|
|
648
|
+
// The flow has been started; the pool slot is only needed for the
|
|
649
|
+
// start request. Polls re-resolve their own egress.
|
|
650
|
+
egress.release();
|
|
651
|
+
}
|
|
556
652
|
const { providerState, ...publicStarted } = started;
|
|
557
653
|
await config.oauthFlowStore.saveDevice(started.deviceAuthId, {
|
|
558
654
|
providerId,
|
|
@@ -585,15 +681,22 @@ function createOAuthLoginOperations(config: OAuthLoginConfig) {
|
|
|
585
681
|
409,
|
|
586
682
|
"Provider does not support device-code login",
|
|
587
683
|
);
|
|
684
|
+
const egress = await resolveDeviceEgress(
|
|
685
|
+
config.deviceEgress,
|
|
686
|
+
providerId,
|
|
687
|
+
a.tenantId,
|
|
688
|
+
);
|
|
588
689
|
const polled = await client.pollDeviceAuth(deviceAuthId, {
|
|
589
690
|
providerId,
|
|
590
691
|
tenantId: a.tenantId,
|
|
591
692
|
accountLabel: correlation.accountLabel,
|
|
693
|
+
fetchFn: egress.fetch,
|
|
592
694
|
...(correlation.providerState === undefined
|
|
593
695
|
? {}
|
|
594
696
|
: { providerState: correlation.providerState }),
|
|
595
697
|
...(correlation.parameters === undefined ? {} : { parameters: correlation.parameters }),
|
|
596
698
|
});
|
|
699
|
+
egress.release();
|
|
597
700
|
if (polled.status === "pending")
|
|
598
701
|
return {
|
|
599
702
|
status: "pending",
|
|
@@ -634,7 +737,11 @@ function createOAuthLoginOperations(config: OAuthLoginConfig) {
|
|
|
634
737
|
correlation.tenantId,
|
|
635
738
|
providerId,
|
|
636
739
|
{
|
|
637
|
-
label:
|
|
740
|
+
label:
|
|
741
|
+
polled.result.accountLabel ??
|
|
742
|
+
(correlation.accountLabel.trim().toLowerCase() === providerId.trim().toLowerCase()
|
|
743
|
+
? ""
|
|
744
|
+
: correlation.accountLabel),
|
|
638
745
|
...polled.result,
|
|
639
746
|
},
|
|
640
747
|
);
|
|
@@ -675,7 +782,7 @@ function createOAuthLoginOperations(config: OAuthLoginConfig) {
|
|
|
675
782
|
);
|
|
676
783
|
}
|
|
677
784
|
const persisted = await config.accountStore.persistAccount(a.tenantId, providerId, {
|
|
678
|
-
label: result.accountLabel ?? accountLabel,
|
|
785
|
+
label: result.accountLabel ?? (accountLabel.trim().toLowerCase() === providerId.trim().toLowerCase() ? "" : accountLabel),
|
|
679
786
|
...result,
|
|
680
787
|
});
|
|
681
788
|
await config.snapshotInvalidator?.invalidate();
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
type QuotaRefreshFailureCode,
|
|
30
30
|
} from "../../providers/quota/refresh";
|
|
31
31
|
import { selectAccountTestEgress } from "./account-test-egress";
|
|
32
|
-
import type { ValidatedFetch } from "../../network/
|
|
32
|
+
import type { ValidatedFetch } from "../../network/egress/fetch";
|
|
33
33
|
import type { ProviderId } from "../../providers/provider-contracts";
|
|
34
34
|
import {
|
|
35
35
|
DAILY_CHECKIN_PROVIDER_IDS,
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
fetchDailyCheckin,
|
|
44
44
|
type BuddyActivityReportResult,
|
|
45
45
|
type DailyCheckinResult,
|
|
46
|
-
} from "../../providers/integrations/buddy/
|
|
46
|
+
} from "../../providers/integrations/buddy/lifecycle";
|
|
47
47
|
import {
|
|
48
48
|
fetchQoderDailyCheckin,
|
|
49
49
|
isQoderCheckinProvider,
|
|
@@ -194,7 +194,7 @@ async function runDailyGrowthForAccount(
|
|
|
194
194
|
// The buddy-oauth-shared graph is lazy-loaded: the uid decoder is only
|
|
195
195
|
// needed for buddy accounts' report leg (B-02).
|
|
196
196
|
const { buddyAccountUid } = await import(
|
|
197
|
-
"../../providers/integrations/buddy/
|
|
197
|
+
"../../providers/integrations/buddy/account"
|
|
198
198
|
);
|
|
199
199
|
const userId = buddyAccountUid(credential);
|
|
200
200
|
let report: { ok: boolean; message: string };
|
|
@@ -427,7 +427,7 @@ export function registerAccountQuotaTenantRoutes(
|
|
|
427
427
|
// The buddy-usage-store graph is lazy-loaded: it is only needed when the
|
|
428
428
|
// page actually renders buddy accounts (B-02).
|
|
429
429
|
const { readBuddyUsageSnapshots } = await import(
|
|
430
|
-
"../../providers/integrations/buddy/
|
|
430
|
+
"../../providers/integrations/buddy/account"
|
|
431
431
|
);
|
|
432
432
|
const usageByAccount = await readBuddyUsageSnapshots(
|
|
433
433
|
db,
|
|
@@ -1250,7 +1250,7 @@ export function registerAccountQuotaTenantRoutes(
|
|
|
1250
1250
|
// The buddy-oauth-shared graph is lazy-loaded: the uid decoder is only
|
|
1251
1251
|
// needed on this buddy-only surface (B-02).
|
|
1252
1252
|
const { buddyAccountUid } = await import(
|
|
1253
|
-
"../../providers/integrations/buddy/
|
|
1253
|
+
"../../providers/integrations/buddy/account"
|
|
1254
1254
|
);
|
|
1255
1255
|
const userId = buddyAccountUid(credential);
|
|
1256
1256
|
if (!userId) {
|
|
@@ -11,8 +11,8 @@ import { networkPools, poolRoutingSettings, providerRoutingSettings } from "../.
|
|
|
11
11
|
import { resolveTenantOverride } from "../../persistence/tenant-scope";
|
|
12
12
|
import { GatewayError } from "../../protocol/gateway-error";
|
|
13
13
|
import type { NetworkPoolSelector, PoolRotation } from "../../network/pool/selector";
|
|
14
|
-
import type { ValidatedFetch } from "../../network/
|
|
15
|
-
import type { ValidatedNetworkBindingFactory } from "../../network/
|
|
14
|
+
import type { ValidatedFetch } from "../../network/egress/fetch";
|
|
15
|
+
import type { ValidatedNetworkBindingFactory } from "../../network/egress/factory";
|
|
16
16
|
import { DEFAULT_PROXY_SKIP_PROVIDER_IDS } from "../../providers/provider-metadata";
|
|
17
17
|
import type { FetchLike } from "../../providers/quota/quota-contracts";
|
|
18
18
|
|
|
@@ -31,7 +31,7 @@ import { randomUUID } from "node:crypto";
|
|
|
31
31
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
32
32
|
import type { RedisClient } from "../../persistence/redis";
|
|
33
33
|
import { checkinAttemptedToday } from "../../workers/daily-checkin";
|
|
34
|
-
import type { ValidatedFetch } from "../../network/
|
|
34
|
+
import type { ValidatedFetch } from "../../network/egress/fetch";
|
|
35
35
|
|
|
36
36
|
/** Lifecycle states of a daily-growth job. */
|
|
37
37
|
export type DailyGrowthJobState = "queued" | "running" | "done" | "failed";
|
|
@@ -10,7 +10,7 @@ import { isAddressAllowed } from "../../../network/ssrf";
|
|
|
10
10
|
import { TRANSPORT_KINDS, type TransportKind } from "../../../network/pool/agent";
|
|
11
11
|
import type { SsrfPolicy } from "../../../config/network";
|
|
12
12
|
import type { NetworkPoolSelector } from "../../../network/pool/selector";
|
|
13
|
-
import type { PoolHealthEvent } from "../../../network/pool
|
|
13
|
+
import type { PoolHealthEvent } from "../../../network/pool/health";
|
|
14
14
|
|
|
15
15
|
export interface CreateNetworkPoolRequest {
|
|
16
16
|
kind: TransportKind;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { ConsoleDomainError, requireTenantScope } from "../../shared/errors";
|
|
5
5
|
import { providerRateLimitIsIpScoped } from "../../../providers/provider-metadata";
|
|
6
6
|
import { resolvePoolProviderRateLimitCooldownMs } from "../../../config/cooldowns";
|
|
7
|
-
import type { PoolHealthEvent } from "../../../network/pool
|
|
7
|
+
import type { PoolHealthEvent } from "../../../network/pool/health";
|
|
8
8
|
import type { AccessDecision } from "../../../security/access-control";
|
|
9
9
|
import {
|
|
10
10
|
deployRelay as deployRelayWorker,
|
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
* pool for good.
|
|
18
18
|
*
|
|
19
19
|
* The 403-free-tier and 429 verdicts are the probe's reading of the same facts
|
|
20
|
-
* `classifyEgressFailure` (in `network/
|
|
20
|
+
* `classifyEgressFailure` (in `network/verdict`) reads on the
|
|
21
21
|
* dispatch path — for the probe they arrive as an HTTP status + body instead of
|
|
22
22
|
* a `GatewayError`, so the shared free-tier regex is borrowed from there to
|
|
23
23
|
* keep the two classifiers' boundaries honest.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import { FREE_TIER_REFUSAL } from "../../../network/
|
|
26
|
+
import { FREE_TIER_REFUSAL } from "../../../network/verdict";
|
|
27
27
|
|
|
28
28
|
export type ProbeVerdict = "allowed" | "blocked" | "throttled" | "unreachable";
|
|
29
29
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Cartethyia already knows how to *use* a hosted relay: a pool whose endpoint
|
|
6
6
|
* host is `*.workers.dev` / `*.vercel.app` / `*.deno.dev` is classified as an
|
|
7
7
|
* application relay and dialed with `x-relay-target` / `x-relay-path` headers
|
|
8
|
-
* (see `network/pool/agent.ts` and `network/
|
|
8
|
+
* (see `network/pool/agent.ts` and `network/egress/fetch.ts`). This module
|
|
9
9
|
* supplies the other half — the operator does not have to write and deploy the
|
|
10
10
|
* worker by hand.
|
|
11
11
|
*
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "./contracts";
|
|
17
17
|
import { classifyPoolConnectError, classifyPoolProbeResponse, parseEgressIp } from "./probe-result";
|
|
18
18
|
import { encryptCredential } from "../../../security/crypto";
|
|
19
|
-
import { createValidatedFetch } from "../../../network/
|
|
19
|
+
import { createValidatedFetch } from "../../../network/egress/fetch";
|
|
20
20
|
import { classifyProviderProbeAnswer } from "./probe-verdict";
|
|
21
21
|
import { buildProviderProbeRequest } from "../../../providers/provider-probe";
|
|
22
22
|
import type { StoredProviderProbe } from "./contracts";
|
|
@@ -34,9 +34,9 @@ import {
|
|
|
34
34
|
disablePoolForProxyHttpStatus,
|
|
35
35
|
listNetworkPoolHealthEvents,
|
|
36
36
|
recoverNetworkPool,
|
|
37
|
-
} from "../../../network/pool
|
|
38
|
-
import { drainPoolByteDelta, recordPoolBytes } from "../../../network/pool/
|
|
39
|
-
import * as poolCooldownStore from "../../../network/pool
|
|
37
|
+
} from "../../../network/pool/health";
|
|
38
|
+
import { drainPoolByteDelta, recordPoolBytes } from "../../../network/pool/accounting";
|
|
39
|
+
import * as poolCooldownStore from "../../../network/pool/cooldown-store";
|
|
40
40
|
// Cloudflare's trace endpoint answers with plain `key=value` lines including
|
|
41
41
|
// `ip=`, the address the request egressed from. Dialing it THROUGH the pool
|
|
42
42
|
// therefore reports the pool's public address — which is what an operator
|