@prometheus-ai/ai 0.5.0

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 (369) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1184 -0
  3. package/dist/types/api-registry.d.ts +30 -0
  4. package/dist/types/auth-broker/client.d.ts +66 -0
  5. package/dist/types/auth-broker/index.d.ts +6 -0
  6. package/dist/types/auth-broker/refresher.d.ts +25 -0
  7. package/dist/types/auth-broker/remote-store.d.ts +101 -0
  8. package/dist/types/auth-broker/server.d.ts +32 -0
  9. package/dist/types/auth-broker/snapshot-cache.d.ts +17 -0
  10. package/dist/types/auth-broker/types.d.ts +107 -0
  11. package/dist/types/auth-broker/wire-schemas.d.ts +412 -0
  12. package/dist/types/auth-gateway/http.d.ts +39 -0
  13. package/dist/types/auth-gateway/index.d.ts +3 -0
  14. package/dist/types/auth-gateway/server.d.ts +36 -0
  15. package/dist/types/auth-gateway/types.d.ts +117 -0
  16. package/dist/types/auth-storage.d.ts +762 -0
  17. package/dist/types/index.d.ts +49 -0
  18. package/dist/types/model-cache.d.ts +17 -0
  19. package/dist/types/model-manager.d.ts +64 -0
  20. package/dist/types/model-thinking.d.ts +100 -0
  21. package/dist/types/models.d.ts +12 -0
  22. package/dist/types/provider-details.d.ts +24 -0
  23. package/dist/types/provider-models/bundled-references.d.ts +4 -0
  24. package/dist/types/provider-models/descriptors.d.ts +50 -0
  25. package/dist/types/provider-models/google.d.ts +24 -0
  26. package/dist/types/provider-models/index.d.ts +5 -0
  27. package/dist/types/provider-models/ollama.d.ts +7 -0
  28. package/dist/types/provider-models/openai-compat.d.ts +323 -0
  29. package/dist/types/provider-models/special.d.ts +16 -0
  30. package/dist/types/providers/amazon-bedrock.d.ts +38 -0
  31. package/dist/types/providers/anthropic-client.d.ts +99 -0
  32. package/dist/types/providers/anthropic-messages-server-schema.d.ts +465 -0
  33. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  34. package/dist/types/providers/anthropic-wire.d.ts +262 -0
  35. package/dist/types/providers/anthropic.d.ts +206 -0
  36. package/dist/types/providers/aws-credentials.d.ts +43 -0
  37. package/dist/types/providers/aws-eventstream.d.ts +38 -0
  38. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  39. package/dist/types/providers/azure-openai-responses.d.ts +15 -0
  40. package/dist/types/providers/cursor/gen/agent_pb.d.ts +13022 -0
  41. package/dist/types/providers/cursor.d.ts +43 -0
  42. package/dist/types/providers/error-message.d.ts +27 -0
  43. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  44. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  45. package/dist/types/providers/google-auth.d.ts +24 -0
  46. package/dist/types/providers/google-gemini-cli.d.ts +81 -0
  47. package/dist/types/providers/google-gemini-headers.d.ts +18 -0
  48. package/dist/types/providers/google-shared.d.ts +171 -0
  49. package/dist/types/providers/google-types.d.ts +138 -0
  50. package/dist/types/providers/google-vertex.d.ts +7 -0
  51. package/dist/types/providers/google.d.ts +4 -0
  52. package/dist/types/providers/grammar.d.ts +1 -0
  53. package/dist/types/providers/kimi.d.ts +27 -0
  54. package/dist/types/providers/mock.d.ts +173 -0
  55. package/dist/types/providers/ollama.d.ts +6 -0
  56. package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
  57. package/dist/types/providers/openai-chat-server-schema.d.ts +817 -0
  58. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  59. package/dist/types/providers/openai-codex/constants.d.ts +26 -0
  60. package/dist/types/providers/openai-codex/request-transformer.d.ts +49 -0
  61. package/dist/types/providers/openai-codex/response-handler.d.ts +17 -0
  62. package/dist/types/providers/openai-codex-responses.d.ts +67 -0
  63. package/dist/types/providers/openai-completions-compat.d.ts +27 -0
  64. package/dist/types/providers/openai-completions.d.ts +54 -0
  65. package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
  66. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  67. package/dist/types/providers/openai-responses-shared.d.ts +105 -0
  68. package/dist/types/providers/openai-responses.d.ts +66 -0
  69. package/dist/types/providers/prometheus-native-client.d.ts +13 -0
  70. package/dist/types/providers/prometheus-native-server.d.ts +68 -0
  71. package/dist/types/providers/register-builtins.d.ts +31 -0
  72. package/dist/types/providers/synthetic.d.ts +26 -0
  73. package/dist/types/providers/transform-messages.d.ts +12 -0
  74. package/dist/types/providers/vision-guard.d.ts +20 -0
  75. package/dist/types/providers/xai-responses.d.ts +23 -0
  76. package/dist/types/rate-limit-utils.d.ts +19 -0
  77. package/dist/types/stream.d.ts +28 -0
  78. package/dist/types/types.d.ts +819 -0
  79. package/dist/types/usage/claude.d.ts +4 -0
  80. package/dist/types/usage/gemini.d.ts +2 -0
  81. package/dist/types/usage/github-copilot.d.ts +7 -0
  82. package/dist/types/usage/google-antigravity.d.ts +2 -0
  83. package/dist/types/usage/kimi.d.ts +2 -0
  84. package/dist/types/usage/minimax-code.d.ts +2 -0
  85. package/dist/types/usage/openai-codex.d.ts +3 -0
  86. package/dist/types/usage/shared.d.ts +1 -0
  87. package/dist/types/usage/zai.d.ts +2 -0
  88. package/dist/types/usage.d.ts +260 -0
  89. package/dist/types/utils/abort.d.ts +19 -0
  90. package/dist/types/utils/abortable-iterator.d.ts +4 -0
  91. package/dist/types/utils/anthropic-auth.d.ts +35 -0
  92. package/dist/types/utils/discovery/antigravity.d.ts +61 -0
  93. package/dist/types/utils/discovery/codex.d.ts +38 -0
  94. package/dist/types/utils/discovery/cursor.d.ts +23 -0
  95. package/dist/types/utils/discovery/gemini.d.ts +25 -0
  96. package/dist/types/utils/discovery/index.d.ts +4 -0
  97. package/dist/types/utils/discovery/openai-compatible.d.ts +72 -0
  98. package/dist/types/utils/event-stream.d.ts +28 -0
  99. package/dist/types/utils/fireworks-model-id.d.ts +10 -0
  100. package/dist/types/utils/foundry.d.ts +1 -0
  101. package/dist/types/utils/http-inspector.d.ts +31 -0
  102. package/dist/types/utils/idle-iterator.d.ts +78 -0
  103. package/dist/types/utils/json-parse.d.ts +37 -0
  104. package/dist/types/utils/oauth/__tests__/xai-oauth.test.d.ts +1 -0
  105. package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +18 -0
  106. package/dist/types/utils/oauth/anthropic.d.ts +22 -0
  107. package/dist/types/utils/oauth/api-key-login.d.ts +35 -0
  108. package/dist/types/utils/oauth/api-key-validation.d.ts +27 -0
  109. package/dist/types/utils/oauth/callback-server.d.ts +57 -0
  110. package/dist/types/utils/oauth/cerebras.d.ts +1 -0
  111. package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
  112. package/dist/types/utils/oauth/cursor.d.ts +15 -0
  113. package/dist/types/utils/oauth/deepseek.d.ts +10 -0
  114. package/dist/types/utils/oauth/firepass.d.ts +1 -0
  115. package/dist/types/utils/oauth/fireworks.d.ts +1 -0
  116. package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
  117. package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
  118. package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
  119. package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
  120. package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
  121. package/dist/types/utils/oauth/huggingface.d.ts +19 -0
  122. package/dist/types/utils/oauth/index.d.ts +38 -0
  123. package/dist/types/utils/oauth/kagi.d.ts +17 -0
  124. package/dist/types/utils/oauth/kilo.d.ts +5 -0
  125. package/dist/types/utils/oauth/kimi.d.ts +21 -0
  126. package/dist/types/utils/oauth/litellm.d.ts +18 -0
  127. package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
  128. package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
  129. package/dist/types/utils/oauth/moonshot.d.ts +1 -0
  130. package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
  131. package/dist/types/utils/oauth/nvidia.d.ts +18 -0
  132. package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
  133. package/dist/types/utils/oauth/ollama.d.ts +18 -0
  134. package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
  135. package/dist/types/utils/oauth/opencode.d.ts +18 -0
  136. package/dist/types/utils/oauth/openrouter.d.ts +1 -0
  137. package/dist/types/utils/oauth/parallel.d.ts +17 -0
  138. package/dist/types/utils/oauth/perplexity.d.ts +9 -0
  139. package/dist/types/utils/oauth/pkce.d.ts +8 -0
  140. package/dist/types/utils/oauth/qianfan.d.ts +17 -0
  141. package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
  142. package/dist/types/utils/oauth/synthetic.d.ts +1 -0
  143. package/dist/types/utils/oauth/tavily.d.ts +17 -0
  144. package/dist/types/utils/oauth/together.d.ts +1 -0
  145. package/dist/types/utils/oauth/types.d.ts +44 -0
  146. package/dist/types/utils/oauth/venice.d.ts +18 -0
  147. package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
  148. package/dist/types/utils/oauth/vllm.d.ts +16 -0
  149. package/dist/types/utils/oauth/wafer.d.ts +2 -0
  150. package/dist/types/utils/oauth/xai-oauth.d.ts +60 -0
  151. package/dist/types/utils/oauth/xiaomi.d.ts +25 -0
  152. package/dist/types/utils/oauth/zai.d.ts +18 -0
  153. package/dist/types/utils/oauth/zenmux.d.ts +1 -0
  154. package/dist/types/utils/oauth/zhipu.d.ts +18 -0
  155. package/dist/types/utils/overflow.d.ts +54 -0
  156. package/dist/types/utils/parse-bind.d.ts +23 -0
  157. package/dist/types/utils/provider-response.d.ts +3 -0
  158. package/dist/types/utils/request-debug.d.ts +29 -0
  159. package/dist/types/utils/retry-after.d.ts +3 -0
  160. package/dist/types/utils/retry.d.ts +26 -0
  161. package/dist/types/utils/schema/adapt.d.ts +24 -0
  162. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  163. package/dist/types/utils/schema/dereference.d.ts +11 -0
  164. package/dist/types/utils/schema/draft.d.ts +10 -0
  165. package/dist/types/utils/schema/equality.d.ts +4 -0
  166. package/dist/types/utils/schema/fields.d.ts +49 -0
  167. package/dist/types/utils/schema/index.d.ts +13 -0
  168. package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
  169. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  170. package/dist/types/utils/schema/normalize.d.ts +93 -0
  171. package/dist/types/utils/schema/spill.d.ts +8 -0
  172. package/dist/types/utils/schema/stamps.d.ts +25 -0
  173. package/dist/types/utils/schema/types.d.ts +4 -0
  174. package/dist/types/utils/schema/wire.d.ts +53 -0
  175. package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
  176. package/dist/types/utils/sdk-stream-timeout.d.ts +33 -0
  177. package/dist/types/utils/sse-debug.d.ts +10 -0
  178. package/dist/types/utils/stream-markup-healing.d.ts +80 -0
  179. package/dist/types/utils/tool-choice.d.ts +50 -0
  180. package/dist/types/utils/validation.d.ts +17 -0
  181. package/dist/types/utils.d.ts +28 -0
  182. package/package.json +142 -0
  183. package/src/api-registry.ts +96 -0
  184. package/src/auth-broker/client.ts +358 -0
  185. package/src/auth-broker/index.ts +6 -0
  186. package/src/auth-broker/refresher.ts +117 -0
  187. package/src/auth-broker/remote-store.ts +637 -0
  188. package/src/auth-broker/server.ts +644 -0
  189. package/src/auth-broker/snapshot-cache.ts +174 -0
  190. package/src/auth-broker/types.ts +130 -0
  191. package/src/auth-broker/wire-schemas.ts +200 -0
  192. package/src/auth-gateway/http.ts +194 -0
  193. package/src/auth-gateway/index.ts +3 -0
  194. package/src/auth-gateway/server.ts +822 -0
  195. package/src/auth-gateway/types.ts +143 -0
  196. package/src/auth-storage.ts +4608 -0
  197. package/src/index.ts +54 -0
  198. package/src/model-cache.ts +129 -0
  199. package/src/model-manager.ts +469 -0
  200. package/src/model-thinking.ts +756 -0
  201. package/src/models.json +60287 -0
  202. package/src/models.json.d.ts +9 -0
  203. package/src/models.ts +56 -0
  204. package/src/prompts/turn-aborted-guidance.md +4 -0
  205. package/src/provider-details.ts +90 -0
  206. package/src/provider-models/bundled-references.ts +38 -0
  207. package/src/provider-models/descriptors.ts +364 -0
  208. package/src/provider-models/google.ts +88 -0
  209. package/src/provider-models/index.ts +5 -0
  210. package/src/provider-models/ollama.ts +153 -0
  211. package/src/provider-models/openai-compat.ts +2904 -0
  212. package/src/provider-models/special.ts +67 -0
  213. package/src/providers/amazon-bedrock.ts +873 -0
  214. package/src/providers/anthropic-client.ts +318 -0
  215. package/src/providers/anthropic-messages-server-schema.ts +243 -0
  216. package/src/providers/anthropic-messages-server.ts +681 -0
  217. package/src/providers/anthropic-wire.ts +268 -0
  218. package/src/providers/anthropic.ts +3106 -0
  219. package/src/providers/aws-credentials.ts +501 -0
  220. package/src/providers/aws-eventstream.ts +185 -0
  221. package/src/providers/aws-sigv4.ts +218 -0
  222. package/src/providers/azure-openai-responses.ts +361 -0
  223. package/src/providers/cursor/gen/agent_pb.ts +15274 -0
  224. package/src/providers/cursor/proto/agent.proto +3526 -0
  225. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  226. package/src/providers/cursor/proto/buf.yaml +17 -0
  227. package/src/providers/cursor.ts +2621 -0
  228. package/src/providers/error-message.ts +21 -0
  229. package/src/providers/github-copilot-headers.ts +140 -0
  230. package/src/providers/gitlab-duo.ts +372 -0
  231. package/src/providers/google-auth.ts +252 -0
  232. package/src/providers/google-gemini-cli.ts +809 -0
  233. package/src/providers/google-gemini-headers.ts +41 -0
  234. package/src/providers/google-shared.ts +917 -0
  235. package/src/providers/google-types.ts +167 -0
  236. package/src/providers/google-vertex.ts +91 -0
  237. package/src/providers/google.ts +41 -0
  238. package/src/providers/grammar.ts +70 -0
  239. package/src/providers/kimi.ts +52 -0
  240. package/src/providers/mock.ts +496 -0
  241. package/src/providers/ollama.ts +644 -0
  242. package/src/providers/openai-anthropic-shim.ts +138 -0
  243. package/src/providers/openai-chat-server-schema.ts +252 -0
  244. package/src/providers/openai-chat-server.ts +647 -0
  245. package/src/providers/openai-codex/constants.ts +43 -0
  246. package/src/providers/openai-codex/request-transformer.ts +161 -0
  247. package/src/providers/openai-codex/response-handler.ts +81 -0
  248. package/src/providers/openai-codex-responses.ts +3027 -0
  249. package/src/providers/openai-completions-compat.ts +320 -0
  250. package/src/providers/openai-completions.ts +2002 -0
  251. package/src/providers/openai-responses-server-schema.ts +290 -0
  252. package/src/providers/openai-responses-server.ts +1183 -0
  253. package/src/providers/openai-responses-shared.ts +956 -0
  254. package/src/providers/openai-responses.ts +679 -0
  255. package/src/providers/prometheus-native-client.ts +228 -0
  256. package/src/providers/prometheus-native-server.ts +212 -0
  257. package/src/providers/register-builtins.ts +457 -0
  258. package/src/providers/synthetic.ts +50 -0
  259. package/src/providers/transform-messages.ts +382 -0
  260. package/src/providers/vision-guard.ts +52 -0
  261. package/src/providers/xai-responses.ts +82 -0
  262. package/src/rate-limit-utils.ts +91 -0
  263. package/src/stream.ts +1068 -0
  264. package/src/types.ts +965 -0
  265. package/src/usage/claude.ts +482 -0
  266. package/src/usage/gemini.ts +250 -0
  267. package/src/usage/github-copilot.ts +421 -0
  268. package/src/usage/google-antigravity.ts +201 -0
  269. package/src/usage/kimi.ts +271 -0
  270. package/src/usage/minimax-code.ts +31 -0
  271. package/src/usage/openai-codex.ts +503 -0
  272. package/src/usage/shared.ts +10 -0
  273. package/src/usage/zai.ts +247 -0
  274. package/src/usage.ts +185 -0
  275. package/src/utils/abort.ts +51 -0
  276. package/src/utils/abortable-iterator.ts +69 -0
  277. package/src/utils/anthropic-auth.ts +93 -0
  278. package/src/utils/discovery/antigravity.ts +261 -0
  279. package/src/utils/discovery/codex.ts +371 -0
  280. package/src/utils/discovery/cursor.ts +306 -0
  281. package/src/utils/discovery/gemini.ts +248 -0
  282. package/src/utils/discovery/index.ts +4 -0
  283. package/src/utils/discovery/openai-compatible.ts +224 -0
  284. package/src/utils/event-stream.ts +142 -0
  285. package/src/utils/fireworks-model-id.ts +30 -0
  286. package/src/utils/foundry.ts +8 -0
  287. package/src/utils/http-inspector.ts +176 -0
  288. package/src/utils/idle-iterator.ts +273 -0
  289. package/src/utils/json-parse.ts +182 -0
  290. package/src/utils/oauth/__tests__/xai-oauth.test.ts +107 -0
  291. package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
  292. package/src/utils/oauth/anthropic.ts +273 -0
  293. package/src/utils/oauth/api-key-login.ts +87 -0
  294. package/src/utils/oauth/api-key-validation.ts +92 -0
  295. package/src/utils/oauth/callback-server.ts +276 -0
  296. package/src/utils/oauth/cerebras.ts +16 -0
  297. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  298. package/src/utils/oauth/cursor.ts +157 -0
  299. package/src/utils/oauth/deepseek.ts +53 -0
  300. package/src/utils/oauth/firepass.ts +24 -0
  301. package/src/utils/oauth/fireworks.ts +15 -0
  302. package/src/utils/oauth/github-copilot.ts +362 -0
  303. package/src/utils/oauth/gitlab-duo.ts +123 -0
  304. package/src/utils/oauth/google-antigravity.ts +200 -0
  305. package/src/utils/oauth/google-gemini-cli.ts +256 -0
  306. package/src/utils/oauth/google-oauth-shared.ts +110 -0
  307. package/src/utils/oauth/huggingface.ts +62 -0
  308. package/src/utils/oauth/index.ts +502 -0
  309. package/src/utils/oauth/kagi.ts +47 -0
  310. package/src/utils/oauth/kilo.ts +87 -0
  311. package/src/utils/oauth/kimi.ts +254 -0
  312. package/src/utils/oauth/litellm.ts +47 -0
  313. package/src/utils/oauth/lm-studio.ts +38 -0
  314. package/src/utils/oauth/minimax-code.ts +80 -0
  315. package/src/utils/oauth/moonshot.ts +23 -0
  316. package/src/utils/oauth/nanogpt.ts +15 -0
  317. package/src/utils/oauth/nvidia.ts +70 -0
  318. package/src/utils/oauth/oauth.html +199 -0
  319. package/src/utils/oauth/ollama-cloud.ts +28 -0
  320. package/src/utils/oauth/ollama.ts +47 -0
  321. package/src/utils/oauth/openai-codex.ts +299 -0
  322. package/src/utils/oauth/opencode.ts +49 -0
  323. package/src/utils/oauth/openrouter.ts +20 -0
  324. package/src/utils/oauth/parallel.ts +46 -0
  325. package/src/utils/oauth/perplexity.ts +206 -0
  326. package/src/utils/oauth/pkce.ts +18 -0
  327. package/src/utils/oauth/qianfan.ts +58 -0
  328. package/src/utils/oauth/qwen-portal.ts +60 -0
  329. package/src/utils/oauth/synthetic.ts +15 -0
  330. package/src/utils/oauth/tavily.ts +46 -0
  331. package/src/utils/oauth/together.ts +16 -0
  332. package/src/utils/oauth/types.ts +102 -0
  333. package/src/utils/oauth/venice.ts +59 -0
  334. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  335. package/src/utils/oauth/vllm.ts +40 -0
  336. package/src/utils/oauth/wafer.ts +50 -0
  337. package/src/utils/oauth/xai-oauth.ts +342 -0
  338. package/src/utils/oauth/xiaomi.ts +194 -0
  339. package/src/utils/oauth/zai.ts +60 -0
  340. package/src/utils/oauth/zenmux.ts +15 -0
  341. package/src/utils/oauth/zhipu.ts +60 -0
  342. package/src/utils/overflow.ts +137 -0
  343. package/src/utils/parse-bind.ts +54 -0
  344. package/src/utils/provider-response.ts +30 -0
  345. package/src/utils/request-debug.ts +336 -0
  346. package/src/utils/retry-after.ts +110 -0
  347. package/src/utils/retry.ts +54 -0
  348. package/src/utils/schema/CONSTRAINTS.md +164 -0
  349. package/src/utils/schema/adapt.ts +36 -0
  350. package/src/utils/schema/compatibility.ts +435 -0
  351. package/src/utils/schema/dereference.ts +98 -0
  352. package/src/utils/schema/draft.ts +341 -0
  353. package/src/utils/schema/equality.ts +97 -0
  354. package/src/utils/schema/fields.ts +191 -0
  355. package/src/utils/schema/index.ts +13 -0
  356. package/src/utils/schema/json-schema-validator.ts +577 -0
  357. package/src/utils/schema/meta-validator.ts +167 -0
  358. package/src/utils/schema/normalize.ts +1588 -0
  359. package/src/utils/schema/spill.ts +43 -0
  360. package/src/utils/schema/stamps.ts +97 -0
  361. package/src/utils/schema/types.ts +10 -0
  362. package/src/utils/schema/wire.ts +293 -0
  363. package/src/utils/schema/zod-decontaminate.ts +331 -0
  364. package/src/utils/sdk-stream-timeout.ts +43 -0
  365. package/src/utils/sse-debug.ts +289 -0
  366. package/src/utils/stream-markup-healing.ts +612 -0
  367. package/src/utils/tool-choice.ts +99 -0
  368. package/src/utils/validation.ts +1024 -0
  369. package/src/utils.ts +166 -0
@@ -0,0 +1,637 @@
1
+ /**
2
+ * Client-side {@link AuthCredentialStore} that mirrors a remote broker's
3
+ * snapshot. Refresh tokens never leave the broker; mutating methods (`replace*`,
4
+ * `upsert*`, `delete*ForProvider`) throw because login flows are server-side.
5
+ *
6
+ * Cache (`getCache`/`setCache`/`cleanExpiredCache`) is in-memory and ephemeral —
7
+ * usage reports cache TTL is 5 minutes per credential, so durability across
8
+ * runs isn't required.
9
+ */
10
+ import { scheduler } from "node:timers/promises";
11
+ import { logger } from "@prometheus-ai/utils";
12
+ import {
13
+ type AuthCredential,
14
+ type AuthCredentialSnapshotEntry,
15
+ type AuthCredentialStore,
16
+ type OAuthCredential,
17
+ REMOTE_REFRESH_SENTINEL,
18
+ type StoredAuthCredential,
19
+ } from "../auth-storage";
20
+ import type { Provider } from "../types";
21
+ import type { UsageReport } from "../usage";
22
+ import type { OAuthCredentials } from "../utils/oauth/types";
23
+ import { type AuthBrokerClient, AuthBrokerStreamUnsupportedError } from "./client";
24
+ import type { RefresherSchedule, SnapshotEntry, SnapshotResponse, SnapshotStreamEvent } from "./types";
25
+
26
+ /**
27
+ * Client-side TTL for the aggregate `/v1/usage` response. Set below the
28
+ * broker server's own 30s usage cache so we typically pick up the broker's
29
+ * cached value instead of re-walking the network — but high enough to absorb
30
+ * the parallel fan-out from `#rankOAuthSelections` into a single round-trip.
31
+ */
32
+ const USAGE_CACHE_TTL_MS = 15_000;
33
+ const WAIT_THRESHOLD_MS = 1_000;
34
+ const MAX_WAIT_MS = 5_000;
35
+ const BACKGROUND_WAIT_MS = 30_000;
36
+ const BACKGROUND_BACKOFF_INITIAL_MS = 500;
37
+ const BACKGROUND_BACKOFF_MAX_MS = 30_000;
38
+
39
+ function emptySnapshot(): SnapshotResponse {
40
+ return {
41
+ generation: 0,
42
+ generatedAt: 0,
43
+ serverNowMs: 0,
44
+ refresher: {
45
+ enabled: false,
46
+ intervalMs: 0,
47
+ skewMs: 0,
48
+ nextSweepInMs: Number.MAX_SAFE_INTEGER,
49
+ },
50
+ credentials: [],
51
+ };
52
+ }
53
+
54
+ interface CacheEntry {
55
+ value: string;
56
+ expiresAtSec: number;
57
+ }
58
+
59
+ interface UsageCacheEntry {
60
+ reports: UsageReport[];
61
+ fetchedAt: number;
62
+ }
63
+
64
+ export interface RemoteAuthCredentialStoreOptions {
65
+ client: AuthBrokerClient;
66
+ /**
67
+ * Initial snapshot. When omitted, callers must call
68
+ * {@link RemoteAuthCredentialStore.refreshSnapshot} before the first read.
69
+ */
70
+ initialSnapshot?: SnapshotResponse;
71
+ /**
72
+ * Subscribe to the broker's SSE snapshot stream when available. Falls back
73
+ * to long-poll permanently when the broker returns 404. Default `true`.
74
+ */
75
+ streamSnapshots?: boolean;
76
+ /**
77
+ * Called after broker-sourced full snapshots are applied. The constructor's
78
+ * initial snapshot intentionally does not trigger this hook.
79
+ */
80
+ onSnapshot?: (snapshot: SnapshotResponse, generation: number) => void;
81
+ }
82
+
83
+ export class RemoteAuthCredentialStore implements AuthCredentialStore {
84
+ readonly #client: AuthBrokerClient;
85
+ readonly #streamSnapshots: boolean;
86
+ readonly #onSnapshot?: (snapshot: SnapshotResponse, generation: number) => void;
87
+ #snapshot: SnapshotResponse = emptySnapshot();
88
+ #snapshotReceivedAt = Date.now();
89
+ #generation = 0;
90
+ #backgroundAbort = new AbortController();
91
+ #cache: Map<string, CacheEntry> = new Map();
92
+ #usageCache?: UsageCacheEntry;
93
+ #usageInflight?: Promise<UsageReport[] | null>;
94
+ #closed = false;
95
+ /**
96
+ * `true` once the SSE consumer received its first frame and hasn't dropped
97
+ * since. Writes consult this to suppress the otherwise-mandatory
98
+ * `refreshSnapshot()` follow-up — the stream will deliver the new
99
+ * generation without an extra GET.
100
+ */
101
+ #streamingActive = false;
102
+ /** Latched once the broker has answered 404 — never try the stream again. */
103
+ #streamingUnsupported = false;
104
+
105
+ constructor(opts: RemoteAuthCredentialStoreOptions) {
106
+ this.#client = opts.client;
107
+ this.#streamSnapshots = opts.streamSnapshots ?? true;
108
+ this.#applySnapshot(opts.initialSnapshot ?? emptySnapshot(), opts.initialSnapshot?.generation ?? 0);
109
+ this.#onSnapshot = opts.onSnapshot;
110
+ void this.#runBackground();
111
+ }
112
+
113
+ get client(): AuthBrokerClient {
114
+ return this.#client;
115
+ }
116
+
117
+ get snapshot(): SnapshotResponse {
118
+ return this.#snapshot;
119
+ }
120
+
121
+ #applySnapshot(snapshot: SnapshotResponse, generation: number): void {
122
+ this.#snapshot = snapshot;
123
+ this.#generation = generation;
124
+ this.#snapshotReceivedAt = Date.now();
125
+ const onSnapshot = this.#onSnapshot;
126
+ if (!onSnapshot) return;
127
+ try {
128
+ onSnapshot(snapshot, generation);
129
+ } catch (error) {
130
+ logger.debug("auth-broker snapshot callback failed", { error: String(error) });
131
+ }
132
+ }
133
+
134
+ async #runBackground(): Promise<void> {
135
+ let backoffMs = BACKGROUND_BACKOFF_INITIAL_MS;
136
+ while (!this.#closed && !this.#backgroundAbort.signal.aborted) {
137
+ if (this.#streamSnapshots && !this.#streamingUnsupported) {
138
+ try {
139
+ await this.#consumeSnapshotStream();
140
+ backoffMs = BACKGROUND_BACKOFF_INITIAL_MS;
141
+ continue;
142
+ } catch (error) {
143
+ if (this.#closed || this.#backgroundAbort.signal.aborted) break;
144
+ if (error instanceof AuthBrokerStreamUnsupportedError) {
145
+ this.#streamingUnsupported = true;
146
+ logger.debug("auth-broker snapshot stream unsupported; falling back to long-poll");
147
+ continue;
148
+ }
149
+ logger.debug("auth-broker snapshot stream failed; backing off", { error: String(error) });
150
+ await scheduler.wait(backoffMs, { signal: this.#backgroundAbort.signal }).catch(() => {});
151
+ backoffMs = Math.min(BACKGROUND_BACKOFF_MAX_MS, backoffMs * 2);
152
+ continue;
153
+ }
154
+ }
155
+ try {
156
+ const result = await this.#client.fetchSnapshot({
157
+ ifGenerationGt: this.#generation,
158
+ waitMs: BACKGROUND_WAIT_MS,
159
+ signal: this.#backgroundAbort.signal,
160
+ });
161
+ if (result.status === 200) this.#applySnapshot(result.snapshot, result.generation);
162
+ backoffMs = BACKGROUND_BACKOFF_INITIAL_MS;
163
+ } catch (error) {
164
+ if (this.#closed || this.#backgroundAbort.signal.aborted) break;
165
+ logger.debug("auth-broker background snapshot sync failed", { error: String(error) });
166
+ await scheduler.wait(backoffMs, { signal: this.#backgroundAbort.signal }).catch(() => {});
167
+ backoffMs = Math.min(BACKGROUND_BACKOFF_MAX_MS, backoffMs * 2);
168
+ }
169
+ }
170
+ }
171
+
172
+ async #consumeSnapshotStream(): Promise<void> {
173
+ const iterator = this.#client.openSnapshotStream({ signal: this.#backgroundAbort.signal });
174
+ try {
175
+ for await (const event of iterator) {
176
+ if (this.#closed || this.#backgroundAbort.signal.aborted) break;
177
+ this.#streamingActive = true;
178
+ this.#applyStreamEvent(event);
179
+ }
180
+ } finally {
181
+ this.#streamingActive = false;
182
+ }
183
+ }
184
+
185
+ #applyStreamEvent(event: SnapshotStreamEvent): void {
186
+ switch (event.kind) {
187
+ case "snapshot": {
188
+ // Strip the discriminator so we store the wire-shape SnapshotResponse.
189
+ const { kind: _kind, ...snapshot } = event;
190
+ if (snapshot.generation < this.#generation) {
191
+ logger.debug("auth-broker stream snapshot older than local; ignoring", {
192
+ local: this.#generation,
193
+ incoming: snapshot.generation,
194
+ });
195
+ return;
196
+ }
197
+ this.#applySnapshot(snapshot, snapshot.generation);
198
+ return;
199
+ }
200
+ case "entry": {
201
+ if (event.generation < this.#generation) return;
202
+ this.#applyStreamEntry(event.entry, event.refresher, event.generation, event.serverNowMs);
203
+ return;
204
+ }
205
+ case "removed": {
206
+ if (event.generation < this.#generation) return;
207
+ this.#removeStreamCredential(event.id, event.refresher, event.generation, event.serverNowMs);
208
+ return;
209
+ }
210
+ }
211
+ }
212
+
213
+ #applyStreamEntry(
214
+ entry: SnapshotEntry,
215
+ refresher: RefresherSchedule,
216
+ generation: number,
217
+ serverNowMs: number,
218
+ ): void {
219
+ const index = this.#snapshot.credentials.findIndex(candidate => candidate.id === entry.id);
220
+ const credentials =
221
+ index === -1
222
+ ? [...this.#snapshot.credentials, entry]
223
+ : this.#snapshot.credentials.map((candidate, i) => (i === index ? entry : candidate));
224
+ this.#snapshot = { ...this.#snapshot, generation, serverNowMs, refresher, credentials };
225
+ this.#generation = generation;
226
+ this.#snapshotReceivedAt = Date.now();
227
+ }
228
+
229
+ #removeStreamCredential(id: number, refresher: RefresherSchedule, generation: number, serverNowMs: number): void {
230
+ const credentials = this.#snapshot.credentials.filter(entry => entry.id !== id);
231
+ this.#snapshot = { ...this.#snapshot, generation, serverNowMs, refresher, credentials };
232
+ this.#generation = generation;
233
+ this.#snapshotReceivedAt = Date.now();
234
+ }
235
+
236
+ /** Re-hydrate the in-memory snapshot from the broker. */
237
+ async refreshSnapshot(): Promise<SnapshotResponse> {
238
+ const result = await this.#client.fetchSnapshot();
239
+ if (result.status === 200) this.#applySnapshot(result.snapshot, result.generation);
240
+ return this.#snapshot;
241
+ }
242
+
243
+ listAuthCredentials(provider?: string): StoredAuthCredential[] {
244
+ const out: StoredAuthCredential[] = [];
245
+ for (const entry of this.#snapshot.credentials) {
246
+ if (provider !== undefined && entry.provider !== provider) continue;
247
+ out.push({
248
+ id: entry.id,
249
+ provider: entry.provider,
250
+ credential: entry.credential as AuthCredential,
251
+ disabledCause: null,
252
+ });
253
+ }
254
+ return out;
255
+ }
256
+
257
+ /**
258
+ * In-memory update from a successful refresh through the broker. AuthStorage
259
+ * calls this after `#replaceCredentialAt`; the broker already persisted the
260
+ * authoritative row, so we just mirror it.
261
+ */
262
+ updateAuthCredential(id: number, credential: AuthCredential): void {
263
+ for (const entry of this.#snapshot.credentials) {
264
+ if (entry.id !== id) continue;
265
+ entry.credential = credential as typeof entry.credential;
266
+ return;
267
+ }
268
+ }
269
+
270
+ deleteAuthCredential(id: number, disabledCause: string): void {
271
+ this.#removeCredentialById(id);
272
+ // Fire-and-forget: tell the broker to persist the disable.
273
+ this.#client.disableCredential(id, disabledCause).catch(error => {
274
+ logger.warn("auth-broker disable propagation failed", { id, error: String(error) });
275
+ });
276
+ }
277
+
278
+ tryDisableAuthCredentialIfMatches(id: number, _expectedData: string, disabledCause: string): boolean {
279
+ const found = this.#snapshot.credentials.find(entry => entry.id === id);
280
+ if (!found) return false;
281
+ this.deleteAuthCredential(id, disabledCause);
282
+ return true;
283
+ }
284
+
285
+ async waitForFreshSnapshot(maxWaitMs: number, opts: { signal?: AbortSignal } = {}): Promise<boolean> {
286
+ const previousGeneration = this.#generation;
287
+ const result = await this.#client.fetchSnapshot({
288
+ ifGenerationGt: this.#generation,
289
+ waitMs: maxWaitMs,
290
+ signal: opts.signal,
291
+ });
292
+ if (result.status === 200) this.#applySnapshot(result.snapshot, result.generation);
293
+ return this.#generation !== previousGeneration;
294
+ }
295
+
296
+ async prepareForRequest(credentialId: number, opts: { signal?: AbortSignal } = {}): Promise<boolean> {
297
+ const entry = this.#snapshot.credentials.find(candidate => candidate.id === credentialId);
298
+ if (entry?.credential.type !== "oauth" || entry.rotatesInMs === null) return false;
299
+ const remainingMs = this.#snapshotReceivedAt + entry.rotatesInMs - Date.now();
300
+ if (remainingMs > WAIT_THRESHOLD_MS) return false;
301
+ return this.waitForFreshSnapshot(MAX_WAIT_MS, opts);
302
+ }
303
+
304
+ async markCredentialSuspect(credentialId: number, opts: { signal?: AbortSignal } = {}): Promise<void> {
305
+ const { entry } = await this.#client.refreshCredential(credentialId, opts.signal);
306
+ if (entry.credential.type !== "oauth") {
307
+ throw new Error(`Broker returned non-OAuth credential for id=${credentialId}`);
308
+ }
309
+ this.#applyCredentialEntry(entry);
310
+ this.#maybeRefreshSnapshot("suspect credential refresh");
311
+ }
312
+
313
+ replaceAuthCredentialsForProvider(_provider: string, _credentials: AuthCredential[]): StoredAuthCredential[] {
314
+ throw new Error(
315
+ "RemoteAuthCredentialStore is read-only on the client. Use `prometheus auth-broker login <provider>` to mutate credentials.",
316
+ );
317
+ }
318
+
319
+ upsertAuthCredentialForProvider(_provider: string, _credential: AuthCredential): StoredAuthCredential[] {
320
+ throw new Error(
321
+ "RemoteAuthCredentialStore is read-only on the client. Use `prometheus auth-broker login <provider>` to mutate credentials.",
322
+ );
323
+ }
324
+
325
+ deleteAuthCredentialsForProvider(_provider: string, _disabledCause: string): void {
326
+ throw new Error(
327
+ "RemoteAuthCredentialStore is read-only on the client. Use `prometheus auth-broker logout <provider>` to mutate credentials.",
328
+ );
329
+ }
330
+
331
+ /**
332
+ * Upsert a single credential through the broker. The broker server is the
333
+ * canonical writer — see `POST /v1/credential`. The redacted snapshot
334
+ * entries returned by the server replace the provider's rows in our local
335
+ * snapshot, and the global snapshot is then refreshed in the background so
336
+ * any concurrent peer (refresh, generation bump) stays in sync.
337
+ */
338
+ async upsertAuthCredentialRemote(provider: string, credential: AuthCredential): Promise<StoredAuthCredential[]> {
339
+ const { entries } = await this.#client.uploadCredential(provider, credential);
340
+ this.#applyProviderEntries(provider, entries);
341
+ this.#maybeRefreshSnapshot("upload");
342
+ return this.listAuthCredentials(provider);
343
+ }
344
+
345
+ /**
346
+ * Replace-all semantics: disable every active credential for the provider,
347
+ * then upload each of the new credentials. Used by API-key login so a new
348
+ * key clobbers any previously stored key for the same provider.
349
+ */
350
+ async replaceAuthCredentialsRemote(
351
+ provider: string,
352
+ credentials: AuthCredential[],
353
+ ): Promise<StoredAuthCredential[]> {
354
+ const existing = this.listAuthCredentials(provider);
355
+ for (const entry of existing) {
356
+ try {
357
+ await this.#client.disableCredential(entry.id, "replaced by newer credential");
358
+ } catch (error) {
359
+ logger.warn("auth-broker disable during replace failed", {
360
+ provider,
361
+ id: entry.id,
362
+ error: String(error),
363
+ });
364
+ }
365
+ }
366
+ // Snapshot reflects the disables before we add the new rows so a concurrent
367
+ // reader cannot momentarily see old + new together for the same provider.
368
+ this.#removeProviderEntries(provider);
369
+ for (const credential of credentials) {
370
+ const { entries } = await this.#client.uploadCredential(provider, credential);
371
+ this.#applyProviderEntries(provider, entries);
372
+ }
373
+ this.#maybeRefreshSnapshot("replace");
374
+ return this.listAuthCredentials(provider);
375
+ }
376
+
377
+ /**
378
+ * Logout: disable every active credential for the provider on the broker,
379
+ * then drop them from the local snapshot. Refresh fetches the authoritative
380
+ * post-state in the background.
381
+ */
382
+ async deleteAuthCredentialsRemote(provider: string, disabledCause: string): Promise<void> {
383
+ const existing = this.listAuthCredentials(provider);
384
+ for (const entry of existing) {
385
+ try {
386
+ await this.#client.disableCredential(entry.id, disabledCause);
387
+ } catch (error) {
388
+ logger.warn("auth-broker disable during delete failed", {
389
+ provider,
390
+ id: entry.id,
391
+ error: String(error),
392
+ });
393
+ }
394
+ }
395
+ this.#removeProviderEntries(provider);
396
+ this.#maybeRefreshSnapshot("delete");
397
+ }
398
+
399
+ #applyProviderEntries(provider: string, entries: AuthCredentialSnapshotEntry[]): void {
400
+ // `entries` is the broker's authoritative post-upsert list of rows for
401
+ // `provider`. Drop our existing rows for the same provider and splice in
402
+ // the fresh set — preserving every other provider's rows in place.
403
+ const others = this.#snapshot.credentials.filter(entry => entry.provider !== provider);
404
+ const incoming = entries.map(entry => ({ ...entry, rotatesInMs: null }));
405
+ this.#snapshot = { ...this.#snapshot, credentials: [...others, ...incoming] };
406
+ }
407
+ #applyCredentialEntry(entry: AuthCredentialSnapshotEntry): void {
408
+ const incoming = { ...entry, rotatesInMs: null };
409
+ const index = this.#snapshot.credentials.findIndex(candidate => candidate.id === entry.id);
410
+ if (index === -1) {
411
+ this.#snapshot = { ...this.#snapshot, credentials: [...this.#snapshot.credentials, incoming] };
412
+ return;
413
+ }
414
+ const credentials = [...this.#snapshot.credentials];
415
+ credentials[index] = incoming;
416
+ this.#snapshot = { ...this.#snapshot, credentials };
417
+ }
418
+
419
+ #removeProviderEntries(provider: string): void {
420
+ const next = this.#snapshot.credentials.filter(entry => entry.provider !== provider);
421
+ this.#snapshot = { ...this.#snapshot, credentials: next };
422
+ }
423
+
424
+ #removeCredentialById(id: number): void {
425
+ const next = this.#snapshot.credentials.filter(entry => entry.id !== id);
426
+ this.#snapshot = { ...this.#snapshot, credentials: next };
427
+ }
428
+
429
+ /**
430
+ * Fire-and-forget `refreshSnapshot()` after a write. When the SSE stream is
431
+ * active the broker will deliver the new generation push, so the extra GET
432
+ * is wasted bandwidth and we skip it.
433
+ */
434
+ #maybeRefreshSnapshot(reason: string): void {
435
+ if (this.#streamingActive) return;
436
+ void this.refreshSnapshot().catch(error => {
437
+ logger.debug("auth-broker snapshot refresh after write failed", { reason, error: String(error) });
438
+ });
439
+ }
440
+
441
+ getCache(key: string): string | null {
442
+ const entry = this.#cache.get(key);
443
+ if (!entry) return null;
444
+ if (entry.expiresAtSec * 1000 <= Date.now()) {
445
+ this.#cache.delete(key);
446
+ return null;
447
+ }
448
+ return entry.value;
449
+ }
450
+
451
+ setCache(key: string, value: string, expiresAtSec: number): void {
452
+ this.#cache.set(key, { value, expiresAtSec });
453
+ }
454
+
455
+ cleanExpiredCache(): void {
456
+ const nowSec = Math.floor(Date.now() / 1000);
457
+ for (const [key, entry] of this.#cache) {
458
+ if (entry.expiresAtSec <= nowSec) this.#cache.delete(key);
459
+ }
460
+ }
461
+
462
+ /**
463
+ * Store-level hook consumed by `AuthStorage` — routes refresh through the
464
+ * broker so the actual refresh token never leaves the broker host. Returns
465
+ * the broker-redacted credential with {@link REMOTE_REFRESH_SENTINEL} in
466
+ * the `refresh` slot.
467
+ */
468
+ async refreshOAuthCredential(
469
+ _provider: Provider,
470
+ credentialId: number,
471
+ _credential: OAuthCredential,
472
+ signal?: AbortSignal,
473
+ ): Promise<OAuthCredentials> {
474
+ const { entry } = await this.#client.refreshCredential(credentialId, signal);
475
+ if (!this.#streamingActive) {
476
+ await this.refreshSnapshot().catch(error => {
477
+ logger.debug("auth-broker snapshot refresh after credential refresh failed", { error: String(error) });
478
+ });
479
+ }
480
+ if (entry.credential.type !== "oauth") {
481
+ throw new Error(`Broker returned non-OAuth credential for id=${credentialId}`);
482
+ }
483
+ const refreshed = entry.credential;
484
+ return {
485
+ access: refreshed.access,
486
+ refresh: REMOTE_REFRESH_SENTINEL,
487
+ expires: refreshed.expires,
488
+ accountId: refreshed.accountId,
489
+ email: refreshed.email,
490
+ projectId: refreshed.projectId,
491
+ enterpriseUrl: refreshed.enterpriseUrl,
492
+ };
493
+ }
494
+
495
+ /**
496
+ * Store-level hook consumed by `AuthStorage.fetchUsageReports()` — proxies
497
+ * to the broker's `/v1/usage` endpoint. The broker's egress IP isn't
498
+ * rate-limited by Anthropic's per-IP `/usage` cap the way a heavy
499
+ * residential laptop is, so all credentials surface every cycle.
500
+ */
501
+ async fetchUsageReports(signal?: AbortSignal): Promise<UsageReport[] | null> {
502
+ return this.#raceWithSignal(this.#loadUsageReports(), signal);
503
+ }
504
+
505
+ /**
506
+ * Per-credential usage hook consumed by `AuthStorage.#getUsageReport`. Pulls
507
+ * the aggregate broker `/v1/usage` once and serves all callers from the
508
+ * same response (coalesced + cached), then matches the credential to a
509
+ * report by provider + identity (accountId / email / projectId).
510
+ *
511
+ * The broker already aggregates with its own 30s TTL on the server side; our
512
+ * 15s client TTL is below that so we usually re-use the broker's cache too.
513
+ */
514
+ async getUsageReport(
515
+ provider: Provider,
516
+ credential: OAuthCredential,
517
+ signal?: AbortSignal,
518
+ ): Promise<UsageReport | null> {
519
+ const reports = await this.#raceWithSignal(this.#loadUsageReports(), signal);
520
+ if (!reports) return null;
521
+ return matchUsageReport(reports, provider, credential);
522
+ }
523
+
524
+ /**
525
+ * Reject the awaited promise when the caller's signal aborts, without
526
+ * affecting the shared upstream fetch. Used to give each caller their
527
+ * own cancel without one caller's abort cascading into a peer's in-flight
528
+ * request through the single-flight `#usageInflight`.
529
+ */
530
+ #raceWithSignal<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
531
+ if (!signal) return promise;
532
+ if (signal.aborted) return Promise.reject(new Error("auth-broker request aborted"));
533
+ return new Promise<T>((resolve, reject) => {
534
+ const onAbort = (): void => {
535
+ signal.removeEventListener("abort", onAbort);
536
+ reject(new Error("auth-broker request aborted"));
537
+ };
538
+ signal.addEventListener("abort", onAbort, { once: true });
539
+ promise.then(
540
+ value => {
541
+ signal.removeEventListener("abort", onAbort);
542
+ resolve(value);
543
+ },
544
+ err => {
545
+ signal.removeEventListener("abort", onAbort);
546
+ reject(err);
547
+ },
548
+ );
549
+ });
550
+ }
551
+
552
+ #loadUsageReports(): Promise<UsageReport[] | null> {
553
+ const cached = this.#usageCache;
554
+ if (cached && Date.now() - cached.fetchedAt < USAGE_CACHE_TTL_MS) {
555
+ return Promise.resolve(cached.reports);
556
+ }
557
+ if (this.#usageInflight) return this.#usageInflight;
558
+ const inflight = this.#client
559
+ .fetchUsage()
560
+ .then(body => {
561
+ this.#usageCache = { reports: body.reports, fetchedAt: Date.now() };
562
+ return body.reports;
563
+ })
564
+ .catch(error => {
565
+ logger.warn("auth-broker usage fetch failed", { error: String(error) });
566
+ return null;
567
+ })
568
+ .finally(() => {
569
+ this.#usageInflight = undefined;
570
+ });
571
+ this.#usageInflight = inflight;
572
+ return inflight;
573
+ }
574
+
575
+ close(): void {
576
+ if (this.#closed) return;
577
+ this.#closed = true;
578
+ this.#backgroundAbort.abort();
579
+ this.#cache.clear();
580
+ }
581
+ }
582
+
583
+ /**
584
+ * Match a broker-supplied usage report to a specific OAuth credential. The
585
+ * broker returns aggregate reports across all credentials it manages, so we
586
+ * pick the one whose identity (accountId / email / projectId) lines up with
587
+ * the credential the caller is asking about.
588
+ *
589
+ * Falls back to the lone candidate when only one matches the provider; falls
590
+ * through to `null` when nothing matches, which `AuthStorage` treats as "no
591
+ * usage data" (ranking proceeds without a usage signal for this credential).
592
+ */
593
+ function matchUsageReport(reports: UsageReport[], provider: Provider, credential: OAuthCredential): UsageReport | null {
594
+ const candidates = reports.filter(report => report.provider === provider);
595
+ if (candidates.length === 0) return null;
596
+ if (candidates.length === 1) return candidates[0];
597
+ const accountId = credential.accountId?.trim().toLowerCase();
598
+ const email = credential.email?.trim().toLowerCase();
599
+ const projectId = credential.projectId?.trim().toLowerCase();
600
+ for (const report of candidates) {
601
+ if (reportMatchesIdentity(report, accountId, email, projectId)) return report;
602
+ }
603
+ return null;
604
+ }
605
+
606
+ function reportMatchesIdentity(
607
+ report: UsageReport,
608
+ accountId: string | undefined,
609
+ email: string | undefined,
610
+ projectId: string | undefined,
611
+ ): boolean {
612
+ const metadata = (report.metadata ?? {}) as Record<string, unknown>;
613
+ if (accountId) {
614
+ const metaAccount = readMetadataString(metadata, "accountId") ?? readMetadataString(metadata, "account_id");
615
+ if (metaAccount && metaAccount.toLowerCase() === accountId) return true;
616
+ for (const limit of report.limits) {
617
+ if (limit.scope.accountId?.toLowerCase() === accountId) return true;
618
+ }
619
+ }
620
+ if (email) {
621
+ const metaEmail = readMetadataString(metadata, "email");
622
+ if (metaEmail && metaEmail.toLowerCase() === email) return true;
623
+ }
624
+ if (projectId) {
625
+ const metaProject = readMetadataString(metadata, "projectId") ?? readMetadataString(metadata, "project_id");
626
+ if (metaProject && metaProject.toLowerCase() === projectId) return true;
627
+ for (const limit of report.limits) {
628
+ if (limit.scope.projectId?.toLowerCase() === projectId) return true;
629
+ }
630
+ }
631
+ return false;
632
+ }
633
+
634
+ function readMetadataString(metadata: Record<string, unknown>, key: string): string | undefined {
635
+ const value = metadata[key];
636
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
637
+ }