cartethyia 2.4.6 → 2.5.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 (303) hide show
  1. package/README.md +45 -13
  2. package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js +1 -0
  3. package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-Bcy150Jo.js → CliToolDetailPage-Bb9CNuoh.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-Bb9CNuoh.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-DyNXefph.js → CliToolsPage-BXUaRCQQ.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-BXUaRCQQ.js.gz +0 -0
  8. package/dist/dashboard/assets/CombosPage-CisNZOUt.js +1 -0
  9. package/dist/dashboard/assets/CombosPage-CisNZOUt.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CATpAEOz.js → ConsoleLogPage-BOpI_LOH.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-BOpI_LOH.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-D6jEU7nL.js → CustomizationPage-DAkdvJ4k.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-DAkdvJ4k.js.gz +0 -0
  14. package/dist/dashboard/assets/{HealthEventsModal-CZkpn0a7.js → HealthEventsModal-DeT-VXHd.js} +1 -1
  15. package/dist/dashboard/assets/HealthEventsModal-DeT-VXHd.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-BJlMVN4U.js → LoginPage-CytxSeGP.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-CytxSeGP.js.gz +0 -0
  18. package/dist/dashboard/assets/{ModelPicker-Br3CXEau.js → ModelPicker-oQ-SDXp8.js} +1 -1
  19. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  20. package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js.gz +0 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js +3 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
  25. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  26. package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js +1 -0
  27. package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js.gz +0 -0
  28. package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js +7 -0
  29. package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js.gz +0 -0
  30. package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js +1 -0
  31. package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js.gz +0 -0
  32. package/dist/dashboard/assets/{SettingsPage-BHW4Zt06.js → SettingsPage-8-eVadhc.js} +1 -1
  33. package/dist/dashboard/assets/SettingsPage-8-eVadhc.js.gz +0 -0
  34. package/dist/dashboard/assets/{SetupPage-VOYC_lIa.js → SetupPage-uEIXPyGO.js} +1 -1
  35. package/dist/dashboard/assets/SetupPage-uEIXPyGO.js.gz +0 -0
  36. package/dist/dashboard/assets/SortableList-DCY0TPTZ.js +1 -0
  37. package/dist/dashboard/assets/SortableList-DCY0TPTZ.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-BNIDLiU6.js → StudioPage-DqK5E26W.js} +4 -4
  39. package/dist/dashboard/assets/StudioPage-DqK5E26W.js.gz +0 -0
  40. package/dist/dashboard/assets/UsagePage-38JfiZ0H.js +56 -0
  41. package/dist/dashboard/assets/UsagePage-38JfiZ0H.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BETfaMYZ.js → api-keys-BmDbsior.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  44. package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
  45. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
  47. package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-BTyGk_DL.js → badge-DpXT4Ixt.js} +1 -1
  49. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-bujTWYM4.js → banner-CSAAWLJv.js} +1 -1
  51. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-B0Bnx5CA.js → card-DEhRXXw4.js} +1 -1
  53. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-DE2ug812.js → cli-tools-B0wBq-RL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-R_rArH8S.js → clipboard-button-DOo6EU8O.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-BWIP_EvW.js → dialog-BvQeBMfP.js} +1 -1
  59. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-BKcgVugF.js → drawer-CMwPXwQB.js} +1 -1
  61. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  62. package/dist/dashboard/assets/entry--yj84zk3.js +4 -0
  63. package/dist/dashboard/assets/entry--yj84zk3.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-CcWkGR4a.js → entry-D4O2229X.js} +1 -1
  65. package/dist/dashboard/assets/{entry-CcWkGR4a.js.gz → entry-D4O2229X.js.gz} +0 -0
  66. package/dist/dashboard/assets/{entry-kTiTFrvh.js → entry-DKvr5Q9K.js} +1 -1
  67. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  68. package/dist/dashboard/assets/{github-badge-Booc5mNe.js → github-badge-zliLvBwr.js} +1 -1
  69. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-D70pChfn.js → index-CcD1eSJ6.js} +2 -2
  71. package/dist/dashboard/assets/index-CcD1eSJ6.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-D1TCqM2D.js → inline-D1IlZZ8H.js} +1 -1
  73. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-B9oyU93y.js → input-BpVZdrbc.js} +1 -1
  75. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-BArzXirQ.js → layout-BioLW7gN.js} +1 -1
  77. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-BVZzaoHF.js → list-row-CwVcGAIc.js} +1 -1
  79. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-EmuIayqp.js → live-yfEJFF9T.js} +1 -1
  81. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  82. package/dist/dashboard/assets/{model-bans-CSPQ9xB0.js → model-bans-CT-TxSNX.js} +1 -1
  83. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  84. package/dist/dashboard/assets/providers/cursor.webp +0 -0
  85. package/dist/dashboard/assets/providers/trae.webp +0 -0
  86. package/dist/dashboard/assets/{routing-BExgxxBU.js → routing-Cs6jl49L.js} +1 -1
  87. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  88. package/dist/dashboard/assets/select-XPJ-dIw3.js +41 -0
  89. package/dist/dashboard/assets/select-XPJ-dIw3.js.gz +0 -0
  90. package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
  91. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  92. package/dist/dashboard/assets/{stack-Ca5HxI5i.js → stack-C9_uUjaA.js} +1 -1
  93. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  94. package/dist/dashboard/assets/{state-CzUPfLXo.js → state-DA0B95UQ.js} +1 -1
  95. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  96. package/dist/dashboard/assets/{switch-DOUIky_i.js → switch-Cittxy-x.js} +1 -1
  97. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  98. package/dist/dashboard/assets/{toast-C76WrMhc.js → toast-CNt_jzO4.js} +1 -1
  99. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  100. package/dist/dashboard/assets/{useMutation-D87FZwH0.js → useMutation-BQKMcJ1H.js} +1 -1
  101. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
  102. package/dist/dashboard/index.html +1 -1
  103. package/dist/dashboard/index.html.gz +0 -0
  104. package/dist/dashboard/precompressed-assets.json +51 -52
  105. package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
  106. package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
  107. package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
  108. package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
  109. package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
  110. package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
  111. package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
  112. package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
  113. package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
  114. package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
  115. package/package.json +3 -2
  116. package/scripts/cli/command/console-menu.ts +30 -11
  117. package/scripts/cli/command/start.ts +77 -13
  118. package/scripts/cli/command/tray.ts +47 -8
  119. package/scripts/cli/command/update.ts +1 -1
  120. package/scripts/cli/gateway/instance-lock.ts +105 -0
  121. package/scripts/cli/gateway/spawn.ts +173 -101
  122. package/scripts/cli/ui/background.ts +110 -49
  123. package/src/boot/app.ts +20 -6
  124. package/src/boot/dependencies.ts +19 -0
  125. package/src/boot/elysia-typebox-bridge.ts +37 -0
  126. package/src/boot/lifecycle.ts +2 -0
  127. package/src/config/cooldowns.ts +138 -138
  128. package/src/config/providers.ts +5 -5
  129. package/src/config/runtime.ts +19 -3
  130. package/src/config/spec.ts +9 -0
  131. package/src/console/api-keys/contracts.ts +414 -391
  132. package/src/console/api-keys/routes.ts +880 -870
  133. package/src/console/auth/account-reset-store.ts +14 -0
  134. package/src/console/auth/account-reset.ts +3 -2
  135. package/src/console/auth/setup-service.ts +6 -3
  136. package/src/console/domain-registration.ts +9 -2
  137. package/src/console/observability/contracts.ts +475 -455
  138. package/src/console/observability/store.ts +1015 -952
  139. package/src/console/providers/catalog/contracts.ts +31 -0
  140. package/src/console/providers/catalog/provider-operations.ts +8 -0
  141. package/src/console/providers/catalog/store.ts +1532 -1525
  142. package/src/console/providers/oauth/callback-listener.ts +18 -3
  143. package/src/console/providers/oauth/routes.ts +38 -15
  144. package/src/console/quota/refresh.ts +19 -0
  145. package/src/console/updates/routes.ts +162 -4
  146. package/src/main.ts +3 -0
  147. package/src/network/http2-bidi.ts +85 -0
  148. package/src/network/outbound-fetch.ts +1 -26
  149. package/src/network/pool/agent.ts +478 -460
  150. package/src/network/pool/resolver.ts +1104 -544
  151. package/src/network/pool-health-machine.ts +12 -5
  152. package/src/observability/payload-capture.ts +344 -324
  153. package/src/persistence/db-pg.ts +9 -3
  154. package/src/persistence/db-sqlite.ts +497 -495
  155. package/src/persistence/health-event-flush.ts +137 -0
  156. package/src/persistence/migrate.ts +181 -129
  157. package/src/persistence/schema.ts +1360 -1295
  158. package/src/persistence/settings-revision.ts +25 -0
  159. package/src/persistence/shared-cache.ts +217 -0
  160. package/src/persistence/stores/share-store.ts +556 -528
  161. package/src/persistence/usage-flush.ts +61 -1
  162. package/src/protocol/canonical-model.ts +23 -1
  163. package/src/protocol/response/chat.ts +143 -10
  164. package/src/protocol/response/omp-tool-call-markup.ts +221 -0
  165. package/src/providers/default-registry.ts +173 -1
  166. package/src/providers/discovery/openai-model-discovery.ts +39 -3
  167. package/src/providers/discovery/openrouter-free-roster.ts +98 -0
  168. package/src/providers/discovery/probing-service.ts +1022 -1001
  169. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +39 -0
  170. package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
  171. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +18 -5
  172. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
  173. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
  174. package/src/providers/integrations/buddy/codebuddy/shared.ts +15 -6
  175. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
  176. package/src/providers/integrations/buddy/workbuddy/shared.ts +3 -3
  177. package/src/providers/integrations/claude/claude.ts +606 -582
  178. package/src/providers/integrations/cline/cline-quota.ts +18 -16
  179. package/src/providers/integrations/cline/cline.ts +539 -468
  180. package/src/providers/integrations/commandcode/commandcode.ts +675 -541
  181. package/src/providers/integrations/commandcode/discovery.ts +116 -0
  182. package/src/providers/integrations/commandcode/quota.ts +160 -0
  183. package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
  184. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
  185. package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
  186. package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
  187. package/src/providers/integrations/cursor/cursor-request.ts +471 -0
  188. package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
  189. package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
  190. package/src/providers/integrations/cursor/cursor.ts +960 -0
  191. package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
  192. package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
  193. package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
  194. package/src/providers/integrations/github/github.ts +4 -4
  195. package/src/providers/integrations/hermes.ts +79 -8
  196. package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
  197. package/src/providers/integrations/kilo/kilo.ts +153 -146
  198. package/src/providers/integrations/opencode/provider.ts +532 -538
  199. package/src/providers/integrations/openrouter/provider.ts +139 -281
  200. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
  201. package/src/providers/integrations/qoder/qoder-oauth.ts +310 -257
  202. package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
  203. package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
  204. package/src/providers/integrations/qoder/qoder.ts +865 -730
  205. package/src/providers/integrations/trae/trae-catalog.ts +190 -0
  206. package/src/providers/integrations/trae/trae-headers.ts +149 -0
  207. package/src/providers/integrations/trae/trae-oauth.ts +773 -0
  208. package/src/providers/integrations/trae/trae-protocol.ts +400 -0
  209. package/src/providers/integrations/trae/trae-realm.ts +220 -0
  210. package/src/providers/integrations/trae/trae.ts +458 -0
  211. package/src/providers/operations/account-health-service.ts +107 -10
  212. package/src/providers/operations/client-versions.ts +133 -6
  213. package/src/providers/operations/provider-catalog-seeder.ts +95 -86
  214. package/src/providers/operations/provider-credential-service.ts +46 -7
  215. package/src/providers/provider-contracts.ts +76 -0
  216. package/src/providers/provider-metadata.ts +78 -6
  217. package/src/providers/provider-model-definition.ts +373 -349
  218. package/src/security/api-key-auth.ts +126 -8
  219. package/src/transport/contracts.ts +3 -0
  220. package/src/transport/dispatch/attempt-finalize.ts +32 -1
  221. package/src/transport/dispatch/attempt-loop.ts +530 -515
  222. package/src/transport/dispatch/generation.ts +88 -21
  223. package/src/transport/dispatch/leases.ts +99 -2
  224. package/src/transport/dispatch/proxy-request.ts +60 -3
  225. package/src/transport/dispatch/streaming/finalize.ts +16 -1
  226. package/src/transport/dispatch/streaming/stream.ts +22 -2
  227. package/src/transport/dispatch/upstream.ts +19 -1
  228. package/src/transport/guards/auth-guard.ts +26 -1
  229. package/src/transport/guards/body-policy.ts +5 -4
  230. package/src/transport/request/preparer.ts +103 -0
  231. package/src/transport/request/state.ts +14 -0
  232. package/src/transport/routing/plan-gate.ts +45 -0
  233. package/src/transport/routing/route-catalog.ts +799 -777
  234. package/src/transport/routing/route-model.ts +52 -1
  235. package/src/transport/routing/router.ts +51 -0
  236. package/dist/dashboard/assets/BannedPage-QhvbJB9o.js +0 -1
  237. package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
  238. package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
  239. package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
  240. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +0 -1
  241. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
  242. package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
  243. package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
  244. package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
  245. package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
  246. package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
  247. package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js +0 -1
  248. package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
  249. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +0 -3
  250. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
  251. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
  252. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
  253. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +0 -1
  254. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
  255. package/dist/dashboard/assets/ProxyPage-CXiumA53.js +0 -7
  256. package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
  257. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +0 -1
  258. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
  259. package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
  260. package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
  261. package/dist/dashboard/assets/SortableList-DpOTnGuH.js +0 -1
  262. package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
  263. package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
  264. package/dist/dashboard/assets/UsagePage-BAFaSDQV.js +0 -56
  265. package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
  266. package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
  267. package/dist/dashboard/assets/app-BLCvGdzb.css +0 -2
  268. package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
  269. package/dist/dashboard/assets/assert-7J6_LVD3.js +0 -1
  270. package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
  271. package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
  272. package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
  273. package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
  274. package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
  275. package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
  276. package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
  277. package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
  278. package/dist/dashboard/assets/entry-7dpgvpJ3.js +0 -4
  279. package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
  280. package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
  281. package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
  282. package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
  283. package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
  284. package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
  285. package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
  286. package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
  287. package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
  288. package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
  289. package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
  290. package/dist/dashboard/assets/select-C9mMoEpJ.js +0 -41
  291. package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
  292. package/dist/dashboard/assets/settings-C4_Gn_wq.js +0 -1
  293. package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
  294. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
  295. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
  296. package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
  297. package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
  298. package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
  299. package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
  300. package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
  301. package/src/network/tls/bun-client-hello.ts +0 -112
  302. /package/dist/dashboard/assets/{app-Cjq7nrwu.js → app-C-Y4oZT0.js} +0 -0
  303. /package/dist/dashboard/assets/{app-Cjq7nrwu.js.gz → app-C-Y4oZT0.js.gz} +0 -0
@@ -1,257 +1,310 @@
1
- /**
2
- * Qoder device-code OAuth client.
3
- *
4
- * Qoder's login is a custom device flow, not RFC 8628: the PKCE pair, nonce,
5
- * and machine id are generated locally, the user opens
6
- * `qoder.com/device/selectAccounts` in a browser, and the gateway polls the
7
- * region's deviceToken endpoint until a `dt-...` token appears. The flow uses
8
- * the same endpoints as the upstream client.
9
- *
10
- * The issued token is used directly at dispatch: `dt-...` tokens skip the
11
- * PAT→jobToken exchange. There is no working refresh grant —
12
- * the upstream refresh endpoint answers 403 for this flow — so no refresher
13
- * is registered and the token is used exactly as issued until the operator
14
- * re-runs login. The account's Qoder user id and machine id travel in
15
- * `auth_state`, because the token itself carries neither and every dispatch
16
- * needs both for COSY signing.
17
- */
18
- import { createHash, randomBytes, randomUUID } from "node:crypto";
19
- import {
20
- base64UrlEncode,
21
- nonEmptyTrimmedString,
22
- record,
23
- } from "../../authentication/oauth-flow-store";
24
- import type {
25
- OAuthDeviceFlowContext,
26
- OAuthDevicePollResult,
27
- OAuthDeviceStartResult,
28
- OAuthExchangeResult,
29
- } from "../../authentication/oauth-flow-store";
30
- import type { OAuthTokenRefreshResult } from "../../authentication/oauth-refresh-service";
31
- import { OAuthDeviceFlow } from "../../authentication/oauth-device-flow";
32
-
33
- export const QODER_LOGIN_URL = "https://qoder.com/device/selectAccounts" as const;
34
- export const QODER_DEVICE_TOKEN_URL = "https://openapi.qoder.sh/api/v1/deviceToken/poll" as const;
35
- export const QODER_USERINFO_URL = "https://openapi.qoder.sh/api/v1/userinfo" as const;
36
-
37
- /** Device authorizations live 5 minutes in the dashboard before expiring. */
38
- const DEVICE_EXPIRY_SECONDS = 300;
39
- /** Dashboard poll cadence for the device authorization. */
40
- const DEVICE_INTERVAL_SECONDS = 5;
41
- /** Individual poll request timeout; a stalled socket is a failed attempt, retried next tick. */
42
- const POLL_TIMEOUT_MS = 15_000;
43
- /** Token lifetime assumed when the upstream omits every expiry hint. */
44
- const EXPIRY_FALLBACK_MS = 30 * 24 * 60 * 60 * 1000;
45
-
46
- /** Non-secret per-account Qoder identity persisted in `auth_state`. */
47
- export interface QoderOAuthState {
48
- readonly userId: string;
49
- readonly machineId: string;
50
- }
51
-
52
- /** Reads the persisted Qoder identity; `undefined` when the account predates it. */
53
- export function parseQoderOAuthState(value: unknown): QoderOAuthState | undefined {
54
- const root = record(value);
55
- if (root === undefined) return undefined;
56
- const userId = nonEmptyTrimmedString(root.userId);
57
- const machineId = nonEmptyTrimmedString(root.machineId);
58
- if (userId === undefined || machineId === undefined) return undefined;
59
- return { userId, machineId };
60
- }
61
-
62
-
63
- /**
64
- * Converts the upstream's expiry hint into a Unix-millisecond timestamp.
65
- *
66
- * Accepts a numeric ms-epoch (number or numeric string), an RFC3339 string,
67
- * or seconds-from-now. Falls back to now + 30 days when both are missing.
68
- * Numeric strings are checked before `Date.parse` because it accepts short
69
- * numerics like "2026" as years and would otherwise return a misleading date.
70
- */
71
- export function parseQoderExpiry(expiresAt: unknown, expiresInSeconds: unknown): number {
72
- if (typeof expiresAt === "number" && Number.isFinite(expiresAt) && expiresAt > 0) {
73
- return expiresAt;
74
- }
75
- const trimmed = typeof expiresAt === "string" ? expiresAt.trim() : "";
76
- if (trimmed) {
77
- if (/^\d+$/.test(trimmed)) {
78
- const ms = Number.parseInt(trimmed, 10);
79
- if (Number.isFinite(ms) && ms > 0) return ms;
80
- }
81
- const parsed = Date.parse(trimmed);
82
- if (!Number.isNaN(parsed)) return parsed;
83
- }
84
- if (
85
- typeof expiresInSeconds === "number" &&
86
- Number.isFinite(expiresInSeconds) &&
87
- expiresInSeconds >= 0
88
- ) {
89
- return Date.now() + expiresInSeconds * 1000;
90
- }
91
- return Date.now() + EXPIRY_FALLBACK_MS;
92
- }
93
-
94
- interface QoderDeviceState {
95
- readonly verifier: string;
96
- readonly machineId: string;
97
- }
98
-
99
- function parseDeviceState(value: string | undefined): QoderDeviceState | undefined {
100
- if (!value) return undefined;
101
- let parsed: unknown;
102
- try {
103
- parsed = JSON.parse(value) as unknown;
104
- } catch {
105
- return undefined;
106
- }
107
- const root = record(parsed);
108
- const verifier = root === undefined ? undefined : nonEmptyTrimmedString(root.verifier);
109
- const machineId = root === undefined ? undefined : nonEmptyTrimmedString(root.machineId);
110
- if (verifier === undefined || machineId === undefined) return undefined;
111
- return { verifier, machineId };
112
- }
113
-
114
- export class QoderOAuthClient extends OAuthDeviceFlow {
115
- override readonly supportsDeviceCode = true;
116
- override readonly supportsBrowserCode = false;
117
-
118
- protected override readonly providerLabel = "Qoder";
119
- // The device endpoints take no client id; the abstract field stays empty
120
- // rather than inventing one, and every request below omits it.
121
- protected override readonly clientId = "";
122
- protected override readonly tokenUrl = QODER_DEVICE_TOKEN_URL;
123
- protected override readonly scopes = "";
124
-
125
- override async startDeviceAuth(
126
- _context?: OAuthDeviceFlowContext,
127
- ): Promise<OAuthDeviceStartResult> {
128
- const verifier = base64UrlEncode(randomBytes(32));
129
- const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
130
- const nonce = randomUUID();
131
- const machineId = randomUUID();
132
- const params = new URLSearchParams({
133
- challenge,
134
- challenge_method: "S256",
135
- machine_id: machineId,
136
- nonce,
137
- });
138
- return {
139
- verificationUri: `${QODER_LOGIN_URL}?${params.toString()}`,
140
- // This flow shows no user code: approval happens against the
141
- // nonce-bound browser session, so there is nothing to type back.
142
- userCode: "",
143
- deviceAuthId: nonce,
144
- intervalSeconds: DEVICE_INTERVAL_SECONDS,
145
- expiresInSeconds: DEVICE_EXPIRY_SECONDS,
146
- providerState: JSON.stringify({ verifier, machineId }),
147
- };
148
- }
149
-
150
- override async pollDeviceAuth(
151
- deviceAuthId: string,
152
- context?: OAuthDeviceFlowContext,
153
- ): Promise<OAuthDevicePollResult> {
154
- const state = parseDeviceState(context?.providerState);
155
- if (state === undefined) return { status: "failed", reason: "unknown device authorization" };
156
- const url =
157
- `${QODER_DEVICE_TOKEN_URL}?nonce=${encodeURIComponent(deviceAuthId)}` +
158
- `&verifier=${encodeURIComponent(state.verifier)}&challenge_method=S256`;
159
- let response: Response;
160
- try {
161
- response = await this.fetchFn(url, {
162
- method: "GET",
163
- headers: { accept: "application/json", "user-agent": "Go-http-client/2.0" },
164
- signal: AbortSignal.timeout(POLL_TIMEOUT_MS),
165
- });
166
- } catch (error) {
167
- return {
168
- status: "failed",
169
- reason: error instanceof Error ? error.message : "Qoder device polling failed",
170
- };
171
- }
172
- // 202/404 are the endpoint's "not approved yet" answers, not failures.
173
- if (response.status === 202 || response.status === 404) return { status: "pending" };
174
- let payload: unknown;
175
- try {
176
- payload = await response.json();
177
- } catch {
178
- return { status: "failed", reason: "Qoder device polling returned invalid JSON" };
179
- }
180
- if (!response.ok) {
181
- const message = record(payload);
182
- return {
183
- status: "failed",
184
- reason:
185
- nonEmptyTrimmedString(message?.message) ??
186
- `Qoder device polling failed (${response.status})`,
187
- };
188
- }
189
- const body = record(payload);
190
- const accessToken = body === undefined ? undefined : nonEmptyTrimmedString(body.token);
191
- // A 200 without a token means the upstream changed shape — fail loudly
192
- // rather than storing an empty credential that 401s every dispatch.
193
- if (accessToken === undefined) {
194
- return { status: "failed", reason: "Qoder device polling returned no token" };
195
- }
196
- const refreshToken = nonEmptyTrimmedString(body?.refresh_token);
197
- const expireTime = parseQoderExpiry(body?.expires_at, body?.expires_in);
198
- const info = await this.fetchUserInfo(accessToken);
199
- const label = info.name || info.email || undefined;
200
- // The token response does not always name the user id; the profile
201
- // endpoint does, so it is the fallback before giving up on the identity
202
- // dispatch needs for COSY signing.
203
- const resolvedUserId = nonEmptyTrimmedString(body?.user_id) ?? info.id;
204
- const result: OAuthExchangeResult = {
205
- access: accessToken,
206
- // No refresh grant exists, so the upstream refresh token (when present)
207
- // is mirrored here for the identity fingerprint that keys a repeated
208
- // login to the same account. It is never sent to a token endpoint —
209
- // no refresher is registered for this provider.
210
- refresh: refreshToken ?? accessToken,
211
- expiresAt: new Date(expireTime),
212
- ...(label === undefined ? {} : { accountLabel: label }),
213
- // The token carries neither the user id nor the machine id, and every
214
- // dispatch needs both for COSY signing — persist them beside the tokens.
215
- auth_state: { userId: resolvedUserId, machineId: state.machineId },
216
- };
217
- return { status: "complete", result };
218
- }
219
-
220
- /** Best-effort profile lookup for the account label; never blocks login. */
221
- async fetchUserInfo(accessToken: string): Promise<{ name: string; email: string; id: string }> {
222
- try {
223
- const response = await this.fetchFn(QODER_USERINFO_URL, {
224
- method: "GET",
225
- headers: {
226
- authorization: `Bearer ${accessToken}`,
227
- accept: "application/json",
228
- "user-agent": "Go-http-client/2.0",
229
- },
230
- signal: AbortSignal.timeout(POLL_TIMEOUT_MS),
231
- });
232
- if (!response.ok) return { name: "", email: "", id: "" };
233
- const body = record(await response.json());
234
- if (body === undefined) return { name: "", email: "", id: "" };
235
- return {
236
- name: (nonEmptyTrimmedString(body.name) ?? nonEmptyTrimmedString(body.username) ?? "").trim(),
237
- email: (nonEmptyTrimmedString(body.email) ?? "").trim(),
238
- id:
239
- nonEmptyTrimmedString(body.id) ??
240
- nonEmptyTrimmedString(body.userId) ??
241
- nonEmptyTrimmedString(body.user_id) ??
242
- "",
243
- };
244
- } catch {
245
- return { name: "", email: "", id: "" };
246
- }
247
- }
248
-
249
- override async refresh(
250
- _refreshToken: string,
251
- _signal?: AbortSignal,
252
- ): Promise<OAuthTokenRefreshResult> {
253
- throw new Error("Qoder does not support token refresh; sign in again");
254
- }
255
- }
256
-
257
- export const qoderOAuthClient = new QoderOAuthClient();
1
+ /**
2
+ * Qoder device-code OAuth client.
3
+ *
4
+ * Qoder's login is a custom device flow, not RFC 8628: the PKCE pair, nonce,
5
+ * and machine id are generated locally, the user opens
6
+ * `qoder.com/device/selectAccounts` in a browser, and the gateway polls the
7
+ * region's deviceToken endpoint until a `dt-...` token appears. The flow uses
8
+ * the same endpoints as the upstream client.
9
+ *
10
+ * The issued token is used directly at dispatch: `dt-...` tokens skip the
11
+ * PAT→jobToken exchange. There is no working refresh grant —
12
+ * the upstream refresh endpoint answers 403 for this flow — so no refresher
13
+ * is registered and the token is used exactly as issued until the operator
14
+ * re-runs login. The account's Qoder user id and machine id travel in
15
+ * `auth_state`, because the token itself carries neither and every dispatch
16
+ * needs both for COSY signing.
17
+ */
18
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
19
+ import { QODER_REALMS, type QoderRealm } from "./qoder-realm";
20
+ import {
21
+ base64UrlEncode,
22
+ nonEmptyTrimmedString,
23
+ record,
24
+ } from "../../authentication/oauth-flow-store";
25
+ import type {
26
+ OAuthDeviceFlowContext,
27
+ OAuthDevicePollResult,
28
+ OAuthDeviceStartResult,
29
+ OAuthExchangeResult,
30
+ } from "../../authentication/oauth-flow-store";
31
+ import type { OAuthTokenRefreshResult } from "../../authentication/oauth-refresh-service";
32
+ import { OAuthDeviceFlow } from "../../authentication/oauth-device-flow";
33
+
34
+ /** Global/intl endpoints. Kept as the default realm's values, unchanged. */
35
+ export const QODER_LOGIN_URL = "https://qoder.com/device/selectAccounts" as const;
36
+ export const QODER_DEVICE_TOKEN_URL = "https://openapi.qoder.sh/api/v1/deviceToken/poll" as const;
37
+ export const QODER_USERINFO_URL = "https://openapi.qoder.sh/api/v1/userinfo" as const;
38
+
39
+ /**
40
+ * Per-realm device endpoints, derived from `QODER_REALMS`.
41
+ *
42
+ * Every realm's openapi host differs, so the three URLs are built from the
43
+ * table rather than hardcoded — a CN login pointed at `openapi.qoder.sh` is
44
+ * refused before it ever reaches an approval screen.
45
+ */
46
+ export function qoderOAuthUrls(realm: QoderRealm): {
47
+ readonly loginUrl: string;
48
+ readonly deviceTokenUrl: string;
49
+ readonly userinfoUrl: string;
50
+ } {
51
+ const config = QODER_REALMS[realm];
52
+ return {
53
+ loginUrl: `${config.website}/device/selectAccounts`,
54
+ deviceTokenUrl: `${config.openapi}/api/v1/deviceToken/poll`,
55
+ userinfoUrl: `${config.openapi}/api/v1/userinfo`,
56
+ };
57
+ }
58
+
59
+ /** Device authorizations live 5 minutes in the dashboard before expiring. */
60
+ const DEVICE_EXPIRY_SECONDS = 300;
61
+ /** Dashboard poll cadence for the device authorization. */
62
+ const DEVICE_INTERVAL_SECONDS = 5;
63
+ /** Individual poll request timeout; a stalled socket is a failed attempt, retried next tick. */
64
+ const POLL_TIMEOUT_MS = 15_000;
65
+ /** Token lifetime assumed when the upstream omits every expiry hint. */
66
+ const EXPIRY_FALLBACK_MS = 30 * 24 * 60 * 60 * 1000;
67
+
68
+ /** Non-secret per-account Qoder identity persisted in `auth_state`. */
69
+ export interface QoderOAuthState {
70
+ readonly userId: string;
71
+ readonly machineId: string;
72
+ }
73
+
74
+ /** Reads the persisted Qoder identity; `undefined` when the account predates it. */
75
+ export function parseQoderOAuthState(value: unknown): QoderOAuthState | undefined {
76
+ const root = record(value);
77
+ if (root === undefined) return undefined;
78
+ const userId = nonEmptyTrimmedString(root.userId);
79
+ const machineId = nonEmptyTrimmedString(root.machineId);
80
+ if (userId === undefined || machineId === undefined) return undefined;
81
+ return { userId, machineId };
82
+ }
83
+
84
+
85
+ /**
86
+ * Converts the upstream's expiry hint into a Unix-millisecond timestamp.
87
+ *
88
+ * Accepts a numeric ms-epoch (number or numeric string), an RFC3339 string,
89
+ * or seconds-from-now. Falls back to now + 30 days when both are missing.
90
+ * Numeric strings are checked before `Date.parse` because it accepts short
91
+ * numerics like "2026" as years and would otherwise return a misleading date.
92
+ */
93
+ export function parseQoderExpiry(expiresAt: unknown, expiresInSeconds: unknown): number {
94
+ if (typeof expiresAt === "number" && Number.isFinite(expiresAt) && expiresAt > 0) {
95
+ return expiresAt;
96
+ }
97
+ const trimmed = typeof expiresAt === "string" ? expiresAt.trim() : "";
98
+ if (trimmed) {
99
+ if (/^\d+$/.test(trimmed)) {
100
+ const ms = Number.parseInt(trimmed, 10);
101
+ if (Number.isFinite(ms) && ms > 0) return ms;
102
+ }
103
+ const parsed = Date.parse(trimmed);
104
+ if (!Number.isNaN(parsed)) return parsed;
105
+ }
106
+ if (
107
+ typeof expiresInSeconds === "number" &&
108
+ Number.isFinite(expiresInSeconds) &&
109
+ expiresInSeconds >= 0
110
+ ) {
111
+ return Date.now() + expiresInSeconds * 1000;
112
+ }
113
+ return Date.now() + EXPIRY_FALLBACK_MS;
114
+ }
115
+
116
+ interface QoderDeviceState {
117
+ readonly verifier: string;
118
+ readonly machineId: string;
119
+ }
120
+
121
+ function parseDeviceState(value: string | undefined): QoderDeviceState | undefined {
122
+ if (!value) return undefined;
123
+ let parsed: unknown;
124
+ try {
125
+ parsed = JSON.parse(value) as unknown;
126
+ } catch {
127
+ return undefined;
128
+ }
129
+ const root = record(parsed);
130
+ const verifier = root === undefined ? undefined : nonEmptyTrimmedString(root.verifier);
131
+ const machineId = root === undefined ? undefined : nonEmptyTrimmedString(root.machineId);
132
+ if (verifier === undefined || machineId === undefined) return undefined;
133
+ return { verifier, machineId };
134
+ }
135
+
136
+ export class QoderOAuthClient extends OAuthDeviceFlow {
137
+ override readonly supportsDeviceCode = true;
138
+ override readonly supportsBrowserCode = false;
139
+
140
+ /**
141
+ * Which deployment this login talks to.
142
+ *
143
+ * Defaults to intl, whose parameter set is left exactly as it was working:
144
+ * the CN shape below is the only branch that changes behavior, so adding a
145
+ * realm cannot regress an existing global login.
146
+ */
147
+ private readonly realm: QoderRealm;
148
+
149
+ constructor(fetchFn: typeof fetch = globalThis.fetch, realm: QoderRealm = "intl") {
150
+ super(fetchFn);
151
+ this.realm = realm;
152
+ }
153
+
154
+ /** The three device endpoints for this client's realm. */
155
+ get urls(): { loginUrl: string; deviceTokenUrl: string; userinfoUrl: string } {
156
+ return qoderOAuthUrls(this.realm);
157
+ }
158
+
159
+ protected override readonly providerLabel = "Qoder";
160
+ // The device endpoints take no client id; the abstract field stays empty
161
+ // rather than inventing one, and every request below omits it.
162
+ protected override readonly clientId = "";
163
+ protected override readonly tokenUrl = QODER_DEVICE_TOKEN_URL;
164
+ protected override readonly scopes = "";
165
+
166
+ override async startDeviceAuth(
167
+ _context?: OAuthDeviceFlowContext,
168
+ ): Promise<OAuthDeviceStartResult> {
169
+ const verifier = base64UrlEncode(randomBytes(32));
170
+ const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
171
+ // CN carries a dashed nonce and additionally states the client id and
172
+ // native redirect; intl stays on the 32-hex nonce and the two params it
173
+ // has always sent. Both shapes are upstream fact from the donor, and only
174
+ // the CN branch is new, so the working intl URL is untouched.
175
+ const nonce = this.realm === "cn" ? randomUUID() : randomUUID().replaceAll("-", "");
176
+ const machineId = randomUUID();
177
+ const params = new URLSearchParams({
178
+ challenge,
179
+ challenge_method: "S256",
180
+ machine_id: machineId,
181
+ nonce,
182
+ });
183
+ if (this.realm === "cn") {
184
+ const config = QODER_REALMS.cn;
185
+ params.set("client_id", config.clientId);
186
+ params.set("redirect_uri", config.redirectUri);
187
+ }
188
+ return {
189
+ verificationUri: `${qoderOAuthUrls(this.realm).loginUrl}?${params.toString()}`,
190
+ // This flow shows no user code: approval happens against the
191
+ // nonce-bound browser session, so there is nothing to type back.
192
+ userCode: "",
193
+ deviceAuthId: nonce,
194
+ intervalSeconds: DEVICE_INTERVAL_SECONDS,
195
+ expiresInSeconds: DEVICE_EXPIRY_SECONDS,
196
+ providerState: JSON.stringify({ verifier, machineId }),
197
+ };
198
+ }
199
+
200
+ override async pollDeviceAuth(
201
+ deviceAuthId: string,
202
+ context?: OAuthDeviceFlowContext,
203
+ ): Promise<OAuthDevicePollResult> {
204
+ const state = parseDeviceState(context?.providerState);
205
+ if (state === undefined) return { status: "failed", reason: "unknown device authorization" };
206
+ const url =
207
+ `${qoderOAuthUrls(this.realm).deviceTokenUrl}?nonce=${encodeURIComponent(deviceAuthId)}` +
208
+ `&verifier=${encodeURIComponent(state.verifier)}&challenge_method=S256`;
209
+ let response: Response;
210
+ try {
211
+ response = await this.fetchFn(url, {
212
+ method: "GET",
213
+ headers: { accept: "application/json", "user-agent": "Go-http-client/2.0" },
214
+ signal: AbortSignal.timeout(POLL_TIMEOUT_MS),
215
+ });
216
+ } catch (error) {
217
+ return {
218
+ status: "failed",
219
+ reason: error instanceof Error ? error.message : "Qoder device polling failed",
220
+ };
221
+ }
222
+ // 202/404 are the endpoint's "not approved yet" answers, not failures.
223
+ if (response.status === 202 || response.status === 404) return { status: "pending" };
224
+ let payload: unknown;
225
+ try {
226
+ payload = await response.json();
227
+ } catch {
228
+ return { status: "failed", reason: "Qoder device polling returned invalid JSON" };
229
+ }
230
+ if (!response.ok) {
231
+ const message = record(payload);
232
+ return {
233
+ status: "failed",
234
+ reason:
235
+ nonEmptyTrimmedString(message?.message) ??
236
+ `Qoder device polling failed (${response.status})`,
237
+ };
238
+ }
239
+ const body = record(payload);
240
+ const accessToken = body === undefined ? undefined : nonEmptyTrimmedString(body.token);
241
+ // A 200 without a token means the upstream changed shape — fail loudly
242
+ // rather than storing an empty credential that 401s every dispatch.
243
+ if (accessToken === undefined) {
244
+ return { status: "failed", reason: "Qoder device polling returned no token" };
245
+ }
246
+ const refreshToken = nonEmptyTrimmedString(body?.refresh_token);
247
+ const expireTime = parseQoderExpiry(body?.expires_at, body?.expires_in);
248
+ const info = await this.fetchUserInfo(accessToken);
249
+ const label = info.name || info.email || undefined;
250
+ // The token response does not always name the user id; the profile
251
+ // endpoint does, so it is the fallback before giving up on the identity
252
+ // dispatch needs for COSY signing.
253
+ const resolvedUserId = nonEmptyTrimmedString(body?.user_id) ?? info.id;
254
+ const result: OAuthExchangeResult = {
255
+ access: accessToken,
256
+ // No refresh grant exists, so the upstream refresh token (when present)
257
+ // is mirrored here for the identity fingerprint that keys a repeated
258
+ // login to the same account. It is never sent to a token endpoint —
259
+ // no refresher is registered for this provider.
260
+ refresh: refreshToken ?? accessToken,
261
+ expiresAt: new Date(expireTime),
262
+ ...(label === undefined ? {} : { accountLabel: label }),
263
+ // The token carries neither the user id nor the machine id, and every
264
+ // dispatch needs both for COSY signing — persist them beside the tokens.
265
+ auth_state: { userId: resolvedUserId, machineId: state.machineId },
266
+ };
267
+ return { status: "complete", result };
268
+ }
269
+
270
+ /** Best-effort profile lookup for the account label; never blocks login. */
271
+ async fetchUserInfo(accessToken: string): Promise<{ name: string; email: string; id: string }> {
272
+ try {
273
+ const response = await this.fetchFn(qoderOAuthUrls(this.realm).userinfoUrl, {
274
+ method: "GET",
275
+ headers: {
276
+ authorization: `Bearer ${accessToken}`,
277
+ accept: "application/json",
278
+ "user-agent": "Go-http-client/2.0",
279
+ },
280
+ signal: AbortSignal.timeout(POLL_TIMEOUT_MS),
281
+ });
282
+ if (!response.ok) return { name: "", email: "", id: "" };
283
+ const body = record(await response.json());
284
+ if (body === undefined) return { name: "", email: "", id: "" };
285
+ return {
286
+ name: (nonEmptyTrimmedString(body.name) ?? nonEmptyTrimmedString(body.username) ?? "").trim(),
287
+ email: (nonEmptyTrimmedString(body.email) ?? "").trim(),
288
+ id:
289
+ nonEmptyTrimmedString(body.id) ??
290
+ nonEmptyTrimmedString(body.userId) ??
291
+ nonEmptyTrimmedString(body.user_id) ??
292
+ "",
293
+ };
294
+ } catch {
295
+ return { name: "", email: "", id: "" };
296
+ }
297
+ }
298
+
299
+ override async refresh(
300
+ _refreshToken: string,
301
+ _signal?: AbortSignal,
302
+ ): Promise<OAuthTokenRefreshResult> {
303
+ throw new Error("Qoder does not support token refresh; sign in again");
304
+ }
305
+ }
306
+
307
+ export const qoderOAuthClient = new QoderOAuthClient();
308
+
309
+ /** Device-login client for the China deployment. */
310
+ export const qoderCnOAuthClient = new QoderOAuthClient(globalThis.fetch, "cn");