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.
Files changed (348) hide show
  1. package/.env.example +3 -3
  2. package/dist/dashboard/assets/{BannedPage-Cc0vKpMG.js → BannedPage-Dq530awD.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-Bqj2aa1T.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-CXsaOAO2.js → CliToolsPage-DaZZ3cqP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BeLpJ4ma.js → CombosPage-BlC_xdkX.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-DnV0uZ3m.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-CBxAjo63.js → CustomizationPage-7xT5yg2E.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
  14. package/dist/dashboard/assets/{HealthEventsModal-B5yEE0k6.js → HealthEventsModal-Clq8E9cW.js} +1 -1
  15. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BWFLECmz.js → InjectSystemPromptPanel-6Teoq9Se.js} +1 -1
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-CgoPc5B9.js → LoginPage-CZuI0MAR.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-D9bFQern.js → ModelPicker-C1hNTlkr.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-B_p4tL8k.js → OverviewPage-0yfhSI1C.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-D_0PiZtg.js → ProviderIcon-Ddhm1InL.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
  28. package/dist/dashboard/assets/{ProvidersPage-CLI2bJTK.js → ProvidersPage-C3fYuu6i.js} +1 -1
  29. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
  31. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
  32. package/dist/dashboard/assets/{QuotaPage-DA-a1iSN.js → QuotaPage-D1RV_2BX.js} +1 -1
  33. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-CctTga2C.js → SettingsPage-BzZAvYSX.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-febLU4r1.js → SetupPage--s-EfEIL.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-M6fvR5Wz.js → StudioPage-BjXFGSK4.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-nyeuRqz9.js → UsagePage-CP8F8BN7.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BFOlvrJC.js → api-keys-2hc12Uz2.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
  44. package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
  45. package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
  46. package/dist/dashboard/assets/{assert-CqTLw5e7.js → assert-BMcj1xZE.js} +1 -1
  47. package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-C83EqLB0.js → badge-DkRUuVfz.js} +1 -1
  49. package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-D1xn7pIe.js → banner-CDjW841Q.js} +1 -1
  51. package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-DU3Lq5Lm.js → card-Bto5UlL9.js} +1 -1
  53. package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-Dw1U_drK.js → cli-tools-DaH6g9hL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CEL5Sg_J.js → clipboard-button-BYKBCzjk.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-BjFR5UHy.js → dialog-DL0Zftwv.js} +1 -1
  59. package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-HvBW0AjI.js → drawer-Dpx_UXaS.js} +1 -1
  61. package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-CECVELyy.js → entry-BV7-1NoM.js} +1 -1
  63. package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-DYYofQ7W.js → entry-CGvdNU6r.js} +1 -1
  65. package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
  66. package/dist/dashboard/assets/{entry-BSu3gFlt.js → entry-DScKYvZj.js} +3 -3
  67. package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-DLW4i8zK.js → formatters-BS3DZlZj.js} +1 -1
  69. package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-1-fCqe64.js → index-SQnvvTWX.js} +2 -2
  71. package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-DGvU2oLK.js → inline-Dl-cuLK0.js} +1 -1
  73. package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-iNnO1X2K.js → input-Bfx0EzRc.js} +1 -1
  75. package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-0q8UGypb.js → layout-BxhbQ_JS.js} +1 -1
  77. package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-C0w2bQjJ.js → list-row-D49Y4ptn.js} +1 -1
  79. package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-M5aJ_xCa.js → live-CbVTdX9V.js} +1 -1
  81. package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-BKeQGT3K.js → network-JGvDveof.js} +1 -1
  83. package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-CivAk3da.js → numbers-Dw6kiew-.js} +1 -1
  85. package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-CpzvI5ep.js → pagination-iUoI8GeE.js} +1 -1
  87. package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
  89. package/dist/dashboard/assets/providers/sensenova.png +0 -0
  90. package/dist/dashboard/assets/{routing-BFQX2VBc.js → routing-8jZr-3WE.js} +1 -1
  91. package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
  92. package/dist/dashboard/assets/{select-CXbck0jG.js → select-oO1FxMVb.js} +1 -1
  93. package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
  94. package/dist/dashboard/assets/{settings-C_XvMHh9.js → settings-CLr3kdIx.js} +1 -1
  95. package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
  96. package/dist/dashboard/assets/{stack-DggRiCxE.js → stack-Y3pDteZr.js} +1 -1
  97. package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
  98. package/dist/dashboard/assets/{state-ClFzfbYp.js → state-CArX_Iak.js} +1 -1
  99. package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
  100. package/dist/dashboard/assets/{switch-CTIFxKqS.js → switch-DwlwmDO-.js} +1 -1
  101. package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
  102. package/dist/dashboard/assets/{useMutation-BClG3yWj.js → useMutation-DJ9GYEgf.js} +1 -1
  103. package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
  104. package/dist/dashboard/index.html +2 -2
  105. package/dist/dashboard/index.html.gz +0 -0
  106. package/dist/dashboard/precompressed-assets.json +51 -51
  107. package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
  108. package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
  109. package/package.json +1 -1
  110. package/src/boot/app.ts +1 -1
  111. package/src/boot/dependencies.ts +4 -6
  112. package/src/config/providers.ts +6 -5
  113. package/src/config/spec.ts +8 -8
  114. package/src/console/backup/nine-router.ts +71 -10
  115. package/src/console/backup/service.ts +8 -1
  116. package/src/console/console-router.ts +1 -1
  117. package/src/console/domain-registration.ts +7 -2
  118. package/src/console/inferhub/client.ts +1 -1
  119. package/src/console/observability/live.ts +2 -2
  120. package/src/console/providers/catalog/contracts.ts +9 -3
  121. package/src/console/providers/catalog/model-operations.ts +2 -3
  122. package/src/console/providers/catalog/provider-operations.ts +1 -1
  123. package/src/console/providers/catalog/routes.ts +2 -2
  124. package/src/console/providers/catalog/store.ts +1914 -1907
  125. package/src/console/providers/detail/routes.ts +1 -0
  126. package/src/console/providers/detail/store.ts +4 -0
  127. package/src/console/providers/oauth/callback-listener.ts +30 -11
  128. package/src/console/providers/oauth/device-egress.ts +123 -0
  129. package/src/console/providers/oauth/routes.ts +123 -16
  130. package/src/console/quota/account-quota-tenant.ts +5 -5
  131. package/src/console/quota/account-test-egress.ts +2 -2
  132. package/src/console/quota/daily-growth-job.ts +1 -1
  133. package/src/console/routing/pools/contracts.ts +1 -1
  134. package/src/console/routing/pools/operations.ts +1 -1
  135. package/src/console/routing/pools/probe-verdict.ts +2 -2
  136. package/src/console/routing/pools/relay-deploy.ts +1 -1
  137. package/src/console/routing/pools/store.ts +4 -4
  138. package/src/network/client.ts +67 -0
  139. package/src/network/egress/bidi-session.ts +322 -0
  140. package/src/network/egress/dial.ts +201 -0
  141. package/src/network/egress/factory.ts +191 -0
  142. package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
  143. package/src/network/egress/tls-profile.ts +123 -0
  144. package/src/network/egress/ws.ts +175 -0
  145. package/src/network/{types.ts → pool/agent-config.ts} +1 -1
  146. package/src/network/pool/agent.ts +2 -2
  147. package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +7 -7
  148. package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
  149. package/src/network/pool/resolver.ts +10 -849
  150. package/src/network/pool/selector.ts +2 -2
  151. package/src/network/ssrf.ts +1 -1
  152. package/src/network/{retry.ts → util/retry.ts} +1 -1
  153. package/src/observability/telemetry-buffer.ts +1 -1
  154. package/src/persistence/schema.ts +9 -0
  155. package/src/persistence/ttl-cache.ts +1 -1
  156. package/src/protocol/codecs.ts +58 -1
  157. package/src/protocol/request/messages.ts +31 -18
  158. package/src/protocol/response/messages.ts +34 -7
  159. package/src/protocol/translation/quirks.ts +15 -0
  160. package/src/protocol/upstream/send-claude-messages.ts +2 -2
  161. package/src/providers/adapters/compatible-adapter.ts +5 -0
  162. package/src/providers/authentication/oauth-client.ts +40 -6
  163. package/src/providers/authentication/oauth-device-flow.ts +7 -3
  164. package/src/providers/authentication/oauth-flow-store.ts +82 -1
  165. package/src/providers/authentication/oauth-refresh-service.ts +766 -739
  166. package/src/providers/default-registry.ts +76 -0
  167. package/src/providers/discovery/discovery-types.ts +7 -0
  168. package/src/providers/discovery/probe-phases.ts +6 -2
  169. package/src/providers/discovery/probing-service.ts +7 -0
  170. package/src/providers/integrations/antigravity/antigravity-protocol.ts +11 -0
  171. package/src/providers/integrations/buddy/account.ts +969 -0
  172. package/src/providers/integrations/buddy/capabilities.ts +10 -10
  173. package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
  174. package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +5 -5
  175. package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
  176. package/src/providers/integrations/buddy/codebuddy/shared.ts +4 -4
  177. package/src/providers/integrations/buddy/{buddy-identity.ts → headers.ts} +138 -140
  178. package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
  179. package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
  180. package/src/providers/integrations/cline/cline-oauth.ts +10 -5
  181. package/src/providers/integrations/cline/cline.ts +3 -4
  182. package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
  183. package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
  184. package/src/providers/integrations/clixad/provider.ts +145 -0
  185. package/src/providers/integrations/codex/codex-oauth.ts +8 -5
  186. package/src/providers/integrations/cursor/cursor-auth.ts +1 -1
  187. package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
  188. package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
  189. package/src/providers/integrations/cursor/cursor-shared.ts +1 -1
  190. package/src/providers/integrations/cursor/cursor.ts +485 -70
  191. package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
  192. package/src/providers/integrations/deepai/deepai-common.ts +145 -0
  193. package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
  194. package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
  195. package/src/providers/integrations/deepai/provider.ts +42 -0
  196. package/src/providers/integrations/devin/devin-auth.ts +7 -0
  197. package/src/providers/integrations/factory/factory-oauth.ts +16 -7
  198. package/src/providers/integrations/freebuff/freebuff.ts +2 -2
  199. package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
  200. package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
  201. package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
  202. package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
  203. package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
  204. package/src/providers/integrations/freepi/freepi-session.ts +158 -0
  205. package/src/providers/integrations/freepi/freepi.ts +80 -54
  206. package/src/providers/integrations/github/github-discovery.ts +30 -13
  207. package/src/providers/integrations/github/github-oauth.ts +217 -44
  208. package/src/providers/integrations/github/github-quota.ts +22 -5
  209. package/src/providers/integrations/github/github.ts +40 -32
  210. package/src/providers/integrations/grok/grok-oauth.ts +4 -4
  211. package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
  212. package/src/providers/integrations/hermes/hermes.ts +34 -5
  213. package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
  214. package/src/providers/integrations/kilo/kilo.ts +1 -1
  215. package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
  216. package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
  217. package/src/providers/integrations/muse/muse-oauth.ts +5 -5
  218. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  219. package/src/providers/integrations/openrouter/provider.ts +1 -1
  220. package/src/providers/integrations/perch/perch-protocol.ts +0 -3
  221. package/src/providers/integrations/perch/perch.ts +3 -3
  222. package/src/providers/integrations/qoder/claim-qoder-credits.ts +1 -1
  223. package/src/providers/integrations/qoder/qoder-adapter.ts +19 -113
  224. package/src/providers/integrations/qoder/qoder-auth.ts +8 -1
  225. package/src/providers/integrations/qoder/qoder-catalog.ts +1 -1
  226. package/src/providers/integrations/qoder/qoder-chat.ts +25 -8
  227. package/src/providers/integrations/qoder/qoder-checkin.ts +1 -1
  228. package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
  229. package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
  230. package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
  231. package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
  232. package/src/providers/integrations/qoder/qoder-types.ts +9 -0
  233. package/src/providers/integrations/qoder/qoder.ts +3 -25
  234. package/src/providers/integrations/sensenova/provider.ts +144 -0
  235. package/src/providers/integrations/trae/trae-quota.ts +15 -1
  236. package/src/providers/integrations/vercel/provider.ts +117 -0
  237. package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
  238. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  239. package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
  240. package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
  241. package/src/providers/integrations/zro/zro.ts +2 -1
  242. package/src/providers/operations/account-refresh-status.ts +14 -0
  243. package/src/providers/operations/client-versions.ts +95 -30
  244. package/src/providers/operations/oauth-refresh-lead.ts +46 -22
  245. package/src/providers/operations/provider-caches.ts +1 -1
  246. package/src/providers/operations/provider-credential-service.ts +22 -25
  247. package/src/providers/operations/session-resolution.ts +17 -1
  248. package/src/providers/provider-contracts.ts +2 -2
  249. package/src/providers/provider-metadata.ts +30 -0
  250. package/src/providers/quota/quota-contracts.ts +2 -0
  251. package/src/providers/quota/refresh.ts +25 -3
  252. package/src/security/ip-boundary.ts +1 -1
  253. package/src/transport/dispatch/attempt-finalize.ts +1 -1
  254. package/src/transport/dispatch/attempt-loop.ts +3 -3
  255. package/src/transport/dispatch/handler-deps.ts +1 -1
  256. package/src/transport/dispatch/leases.ts +1 -1
  257. package/src/transport/dispatch/retry-policy.ts +1 -1
  258. package/src/transport/dispatch/streaming/finalize.ts +2 -2
  259. package/src/transport/dispatch/streaming/stream.ts +1 -1
  260. package/src/transport/dispatch/upstream.ts +1 -1
  261. package/src/transport/shutdown-notice.ts +1 -1
  262. package/src/workers/buddy-growth-tasks.ts +8 -8
  263. package/src/workers/daily-checkin.ts +4 -4
  264. package/src/workers/quota-refresh-sweep.ts +1 -1
  265. package/src/workers/zcode-gift-claim.ts +1 -1
  266. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
  267. package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
  268. package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
  269. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
  270. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
  271. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
  272. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
  273. package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
  274. package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
  275. package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
  276. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
  277. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +0 -8
  278. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
  279. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
  280. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
  281. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js +0 -9
  282. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
  283. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
  284. package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
  285. package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
  286. package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
  287. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
  288. package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
  289. package/dist/dashboard/assets/app-Dx2QgsNj.css +0 -2
  290. package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
  291. package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
  292. package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
  293. package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
  294. package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
  295. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
  296. package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
  297. package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
  298. package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
  299. package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
  300. package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
  301. package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
  302. package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
  303. package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
  304. package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
  305. package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
  306. package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
  307. package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
  308. package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
  309. package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
  310. package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
  311. package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
  312. package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
  313. package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
  314. package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
  315. package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
  316. package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
  317. package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
  318. package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
  319. package/src/console/providers/catalog/discover-constants.ts +0 -6
  320. package/src/network/pool-health.ts +0 -198
  321. package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
  322. package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
  323. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
  324. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
  325. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
  326. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
  327. package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
  328. package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
  329. package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
  330. package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
  331. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
  332. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -188
  333. package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
  334. package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -179
  335. package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
  336. package/src/providers/integrations/qoder/chat-proto.ts +0 -328
  337. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -870
  338. package/src/providers/integrations/qoder/qoder-ide.ts +0 -481
  339. /package/dist/dashboard/assets/{app-DIXLjojg.js → app-C0gTXEoN.js} +0 -0
  340. /package/dist/dashboard/assets/{app-DIXLjojg.js.gz → app-C0gTXEoN.js.gz} +0 -0
  341. /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
  342. /package/src/network/pool/{byte-accounting.ts → accounting.ts} +0 -0
  343. /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
  344. /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
  345. /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
  346. /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
  347. /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
  348. /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`; binding only the advertised path left a redirect to the
162
- * other one hitting a 404 and the login hanging. Other providers neither
163
- * register nor expect `/signin/callback`, so adding it is harmless.
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
- return advertised === "/signin/callback"
167
- ? [advertised]
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(port: number, listener: PortListener, state: string): void {
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
- for (const server of listener.servers) void server.stop(true);
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 { OAuthExchangeResult, OAuthLoginClient } from "../../../providers/authentication/oauth-flow-store";
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 = label.length > 0 && label.toLowerCase() !== input.providerId.trim().toLowerCase();
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 credentialFingerprint = oauthIdentityFingerprint({ ...input, providerId });
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})) THEN ${input.label} ELSE ${providerAccounts.label} END`,
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
- ...(input.auth_state === undefined ? {} : { authState: input.auth_state }),
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: input.label,
208
+ label: generatedLabel,
128
209
  credentialKind: "oauth",
129
210
  credentialCiphertext,
130
211
  credentialFingerprint,
131
212
  status: "active",
132
- ...(input.auth_state === undefined ? {} : { authState: input.auth_state }),
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 started = await client.startDeviceAuth({
633
+ const egress = await resolveDeviceEgress(
634
+ config.deviceEgress,
551
635
  providerId,
552
- tenantId: a.tenantId,
553
- accountLabel,
554
- ...(parameters === undefined ? {} : { parameters }),
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: polled.result.accountLabel ?? correlation.accountLabel,
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/outbound-fetch";
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/buddy-checkin";
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/buddy-oauth-shared"
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/buddy-usage-store"
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/buddy-oauth-shared"
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/outbound-fetch";
15
- import type { ValidatedNetworkBindingFactory } from "../../network/pool/resolver";
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/outbound-fetch";
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-health-machine";
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-health-machine";
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/rate-limit-verdict`) reads on the
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/rate-limit-verdict";
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/outbound-fetch.ts`). This module
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/outbound-fetch";
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-health-machine";
38
- import { drainPoolByteDelta, recordPoolBytes } from "../../../network/pool/byte-accounting";
39
- import * as poolCooldownStore from "../../../network/pool-cooldown-store";
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