@linxiraos/pi-ai 1.0.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 (639) hide show
  1. package/CHANGELOG.md +5066 -0
  2. package/README.md +1195 -0
  3. package/dist/types/api-registry.d.ts +30 -0
  4. package/dist/types/auth/sqlite-credential-store.d.ts +117 -0
  5. package/dist/types/auth-broker/client.d.ts +93 -0
  6. package/dist/types/auth-broker/discover.d.ts +39 -0
  7. package/dist/types/auth-broker/index.d.ts +7 -0
  8. package/dist/types/auth-broker/refresher.d.ts +25 -0
  9. package/dist/types/auth-broker/remote-store.d.ts +136 -0
  10. package/dist/types/auth-broker/server.d.ts +48 -0
  11. package/dist/types/auth-broker/snapshot-cache.d.ts +17 -0
  12. package/dist/types/auth-broker/types.d.ts +152 -0
  13. package/dist/types/auth-broker/wire-schema-resource.d.ts +53 -0
  14. package/dist/types/auth-broker/wire-schemas.d.ts +5 -0
  15. package/dist/types/auth-gateway/http.d.ts +56 -0
  16. package/dist/types/auth-gateway/index.d.ts +3 -0
  17. package/dist/types/auth-gateway/server.d.ts +36 -0
  18. package/dist/types/auth-gateway/types.d.ts +127 -0
  19. package/dist/types/auth-retry.d.ts +150 -0
  20. package/dist/types/auth-storage.d.ts +1258 -0
  21. package/dist/types/dialect/anthropic.d.ts +15 -0
  22. package/dist/types/dialect/catalog.d.ts +3 -0
  23. package/dist/types/dialect/coercion.d.ts +23 -0
  24. package/dist/types/dialect/deepseek.d.ts +14 -0
  25. package/dist/types/dialect/demotion.d.ts +30 -0
  26. package/dist/types/dialect/examples.d.ts +18 -0
  27. package/dist/types/dialect/factory.d.ts +3 -0
  28. package/dist/types/dialect/fenced-thinking.d.ts +53 -0
  29. package/dist/types/dialect/gemini.d.ts +17 -0
  30. package/dist/types/dialect/gemma.d.ts +15 -0
  31. package/dist/types/dialect/glm.d.ts +9 -0
  32. package/dist/types/dialect/harmony.d.ts +8 -0
  33. package/dist/types/dialect/hermes.d.ts +9 -0
  34. package/dist/types/dialect/history.d.ts +3 -0
  35. package/dist/types/dialect/index.d.ts +11 -0
  36. package/dist/types/dialect/inventory.d.ts +9 -0
  37. package/dist/types/dialect/kimi.d.ts +14 -0
  38. package/dist/types/dialect/minimax.d.ts +3 -0
  39. package/dist/types/dialect/owned-stream.d.ts +4 -0
  40. package/dist/types/dialect/qwen3.d.ts +9 -0
  41. package/dist/types/dialect/rendering.d.ts +54 -0
  42. package/dist/types/dialect/thinking.d.ts +6 -0
  43. package/dist/types/dialect/types.d.ts +68 -0
  44. package/dist/types/dialect/xml.d.ts +9 -0
  45. package/dist/types/error/abort.d.ts +14 -0
  46. package/dist/types/error/auth-classify.d.ts +20 -0
  47. package/dist/types/error/auth.d.ts +27 -0
  48. package/dist/types/error/aws.d.ts +27 -0
  49. package/dist/types/error/classes.d.ts +106 -0
  50. package/dist/types/error/finalize.d.ts +39 -0
  51. package/dist/types/error/flags.d.ts +94 -0
  52. package/dist/types/error/format.d.ts +20 -0
  53. package/dist/types/error/gateway.d.ts +20 -0
  54. package/dist/types/error/index.d.ts +14 -0
  55. package/dist/types/error/oauth.d.ts +43 -0
  56. package/dist/types/error/provider.d.ts +42 -0
  57. package/dist/types/error/rate-limit.d.ts +80 -0
  58. package/dist/types/error/retryable.d.ts +27 -0
  59. package/dist/types/error/validation.d.ts +32 -0
  60. package/dist/types/index.d.ts +51 -0
  61. package/dist/types/provider-details.d.ts +24 -0
  62. package/dist/types/providers/amazon-bedrock.d.ts +39 -0
  63. package/dist/types/providers/anthropic-client.d.ts +106 -0
  64. package/dist/types/providers/anthropic-messages-server-schema.d.ts +937 -0
  65. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  66. package/dist/types/providers/anthropic-wire.d.ts +345 -0
  67. package/dist/types/providers/anthropic.d.ts +261 -0
  68. package/dist/types/providers/aws-credentials.d.ts +48 -0
  69. package/dist/types/providers/aws-eventstream.d.ts +39 -0
  70. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  71. package/dist/types/providers/azure-openai-responses.d.ts +16 -0
  72. package/dist/types/providers/bedrock-mantle.d.ts +13 -0
  73. package/dist/types/providers/claude-code-fingerprint.d.ts +19 -0
  74. package/dist/types/providers/cowork-fetch.d.ts +3 -0
  75. package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
  76. package/dist/types/providers/cursor-pi-args.d.ts +105 -0
  77. package/dist/types/providers/cursor.d.ts +221 -0
  78. package/dist/types/providers/devin.d.ts +12 -0
  79. package/dist/types/providers/error-message.d.ts +25 -0
  80. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  81. package/dist/types/providers/gitlab-duo-workflow.d.ts +254 -0
  82. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  83. package/dist/types/providers/google-auth.d.ts +24 -0
  84. package/dist/types/providers/google-gemini-cli.d.ts +120 -0
  85. package/dist/types/providers/google-shared.d.ts +203 -0
  86. package/dist/types/providers/google-types.d.ts +155 -0
  87. package/dist/types/providers/google-vertex.d.ts +7 -0
  88. package/dist/types/providers/google.d.ts +4 -0
  89. package/dist/types/providers/grammar.d.ts +1 -0
  90. package/dist/types/providers/kimi.d.ts +27 -0
  91. package/dist/types/providers/mock.d.ts +179 -0
  92. package/dist/types/providers/ollama.d.ts +8 -0
  93. package/dist/types/providers/openai-anthropic-shim.d.ts +35 -0
  94. package/dist/types/providers/openai-chat-server-schema.d.ts +1311 -0
  95. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  96. package/dist/types/providers/openai-chat-wire.d.ts +669 -0
  97. package/dist/types/providers/openai-codex/request-transformer.d.ts +109 -0
  98. package/dist/types/providers/openai-codex/response-handler.d.ts +26 -0
  99. package/dist/types/providers/openai-codex-responses.d.ts +238 -0
  100. package/dist/types/providers/openai-completions.d.ts +48 -0
  101. package/dist/types/providers/openai-reasoning-fallback.d.ts +25 -0
  102. package/dist/types/providers/openai-responses-server-schema.d.ts +1314 -0
  103. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  104. package/dist/types/providers/openai-responses-wire.d.ts +6099 -0
  105. package/dist/types/providers/openai-responses.d.ts +131 -0
  106. package/dist/types/providers/openai-shared.d.ts +624 -0
  107. package/dist/types/providers/pi-native-client.d.ts +13 -0
  108. package/dist/types/providers/pi-native-server.d.ts +69 -0
  109. package/dist/types/providers/register-builtins.d.ts +37 -0
  110. package/dist/types/providers/synthetic.d.ts +26 -0
  111. package/dist/types/providers/transform-messages.d.ts +32 -0
  112. package/dist/types/providers/vision-guard.d.ts +20 -0
  113. package/dist/types/registry/aiand.d.ts +7 -0
  114. package/dist/types/registry/aimlapi.d.ts +4 -0
  115. package/dist/types/registry/alibaba-coding-plan.d.ts +8 -0
  116. package/dist/types/registry/alibaba-token-plan.d.ts +18 -0
  117. package/dist/types/registry/amazon-bedrock.d.ts +5 -0
  118. package/dist/types/registry/anthropic.d.ts +10 -0
  119. package/dist/types/registry/api-key-login.d.ts +42 -0
  120. package/dist/types/registry/api-key-validation.d.ts +43 -0
  121. package/dist/types/registry/aws.d.ts +13 -0
  122. package/dist/types/registry/azure.d.ts +4 -0
  123. package/dist/types/registry/baseten.d.ts +7 -0
  124. package/dist/types/registry/bedrock-mantle.d.ts +22 -0
  125. package/dist/types/registry/cerebras.d.ts +7 -0
  126. package/dist/types/registry/cloudflare-ai-gateway.d.ts +13 -0
  127. package/dist/types/registry/coreweave.d.ts +7 -0
  128. package/dist/types/registry/cursor.d.ts +7 -0
  129. package/dist/types/registry/deepseek.d.ts +8 -0
  130. package/dist/types/registry/derived.d.ts +5 -0
  131. package/dist/types/registry/devin.d.ts +8 -0
  132. package/dist/types/registry/exa.d.ts +8 -0
  133. package/dist/types/registry/firepass.d.ts +16 -0
  134. package/dist/types/registry/fireworks.d.ts +7 -0
  135. package/dist/types/registry/github-copilot.d.ts +7 -0
  136. package/dist/types/registry/gitlab-duo-workflow.d.ts +10 -0
  137. package/dist/types/registry/gitlab-duo.d.ts +9 -0
  138. package/dist/types/registry/gmi-cloud.d.ts +7 -0
  139. package/dist/types/registry/google-antigravity.d.ts +9 -0
  140. package/dist/types/registry/google-gemini-cli.d.ts +9 -0
  141. package/dist/types/registry/google-vertex.d.ts +5 -0
  142. package/dist/types/registry/google.d.ts +4 -0
  143. package/dist/types/registry/groq.d.ts +4 -0
  144. package/dist/types/registry/huggingface.d.ts +7 -0
  145. package/dist/types/registry/index.d.ts +4 -0
  146. package/dist/types/registry/kagi.d.ts +14 -0
  147. package/dist/types/registry/kilo.d.ts +7 -0
  148. package/dist/types/registry/kimi-code.d.ts +7 -0
  149. package/dist/types/registry/litellm.d.ts +13 -0
  150. package/dist/types/registry/llama-cpp.d.ts +8 -0
  151. package/dist/types/registry/lm-studio.d.ts +8 -0
  152. package/dist/types/registry/meta.d.ts +7 -0
  153. package/dist/types/registry/minimax-code-cn.d.ts +6 -0
  154. package/dist/types/registry/minimax-code.d.ts +6 -0
  155. package/dist/types/registry/minimax.d.ts +4 -0
  156. package/dist/types/registry/mistral.d.ts +4 -0
  157. package/dist/types/registry/moonshot.d.ts +7 -0
  158. package/dist/types/registry/nanogpt.d.ts +7 -0
  159. package/dist/types/registry/novita.d.ts +6 -0
  160. package/dist/types/registry/nvidia.d.ts +7 -0
  161. package/dist/types/registry/oauth/anthropic-constants.d.ts +12 -0
  162. package/dist/types/registry/oauth/anthropic.d.ts +24 -0
  163. package/dist/types/registry/oauth/callback-server.d.ts +74 -0
  164. package/dist/types/registry/oauth/cursor.d.ts +16 -0
  165. package/dist/types/registry/oauth/device-code.d.ts +25 -0
  166. package/dist/types/registry/oauth/devin.d.ts +5 -0
  167. package/dist/types/registry/oauth/github-copilot.d.ts +30 -0
  168. package/dist/types/registry/oauth/gitlab-duo-workflow.d.ts +6 -0
  169. package/dist/types/registry/oauth/gitlab-duo.d.ts +3 -0
  170. package/dist/types/registry/oauth/google-antigravity.d.ts +11 -0
  171. package/dist/types/registry/oauth/google-gemini-cli.d.ts +22 -0
  172. package/dist/types/registry/oauth/google-oauth-shared.d.ts +56 -0
  173. package/dist/types/registry/oauth/index.d.ts +45 -0
  174. package/dist/types/registry/oauth/kimi.d.ts +21 -0
  175. package/dist/types/registry/oauth/minimax-code.d.ts +27 -0
  176. package/dist/types/registry/oauth/openai-codex.d.ts +33 -0
  177. package/dist/types/registry/oauth/opencode.d.ts +18 -0
  178. package/dist/types/registry/oauth/perplexity.d.ts +9 -0
  179. package/dist/types/registry/oauth/pkce.d.ts +8 -0
  180. package/dist/types/registry/oauth/types.d.ts +87 -0
  181. package/dist/types/registry/oauth/wafer.d.ts +1 -0
  182. package/dist/types/registry/oauth/xai-oauth.d.ts +46 -0
  183. package/dist/types/registry/oauth/xiaomi.d.ts +25 -0
  184. package/dist/types/registry/oauth/zai.d.ts +25 -0
  185. package/dist/types/registry/ollama-cloud.d.ts +7 -0
  186. package/dist/types/registry/ollama.d.ts +12 -0
  187. package/dist/types/registry/openai-codex-device.d.ts +8 -0
  188. package/dist/types/registry/openai-codex.d.ts +9 -0
  189. package/dist/types/registry/openai.d.ts +4 -0
  190. package/dist/types/registry/opencode-go.d.ts +6 -0
  191. package/dist/types/registry/opencode-zen.d.ts +6 -0
  192. package/dist/types/registry/openrouter.d.ts +13 -0
  193. package/dist/types/registry/parallel.d.ts +14 -0
  194. package/dist/types/registry/perplexity.d.ts +7 -0
  195. package/dist/types/registry/qianfan.d.ts +7 -0
  196. package/dist/types/registry/qwen-portal.d.ts +7 -0
  197. package/dist/types/registry/registry.d.ts +367 -0
  198. package/dist/types/registry/sakana.d.ts +7 -0
  199. package/dist/types/registry/siliconflow-cn.d.ts +7 -0
  200. package/dist/types/registry/siliconflow.d.ts +7 -0
  201. package/dist/types/registry/synthetic.d.ts +6 -0
  202. package/dist/types/registry/tavily.d.ts +14 -0
  203. package/dist/types/registry/together.d.ts +6 -0
  204. package/dist/types/registry/types.d.ts +75 -0
  205. package/dist/types/registry/umans.d.ts +7 -0
  206. package/dist/types/registry/venice.d.ts +13 -0
  207. package/dist/types/registry/vercel-ai-gateway.d.ts +7 -0
  208. package/dist/types/registry/vllm.d.ts +7 -0
  209. package/dist/types/registry/wafer-serverless.d.ts +6 -0
  210. package/dist/types/registry/xai-oauth.d.ts +7 -0
  211. package/dist/types/registry/xai.d.ts +7 -0
  212. package/dist/types/registry/xiaomi-token-plan-ams.d.ts +6 -0
  213. package/dist/types/registry/xiaomi-token-plan-cn.d.ts +6 -0
  214. package/dist/types/registry/xiaomi-token-plan-sgp.d.ts +6 -0
  215. package/dist/types/registry/xiaomi.d.ts +6 -0
  216. package/dist/types/registry/zai.d.ts +15 -0
  217. package/dist/types/registry/zenmux.d.ts +7 -0
  218. package/dist/types/registry/zhipu-coding-plan.d.ts +7 -0
  219. package/dist/types/stream.d.ts +46 -0
  220. package/dist/types/types.d.ts +1064 -0
  221. package/dist/types/usage/alibaba-token-plan.d.ts +3 -0
  222. package/dist/types/usage/claude.d.ts +4 -0
  223. package/dist/types/usage/cursor.d.ts +4 -0
  224. package/dist/types/usage/gemini.d.ts +2 -0
  225. package/dist/types/usage/github-copilot.d.ts +7 -0
  226. package/dist/types/usage/google-antigravity.d.ts +15 -0
  227. package/dist/types/usage/kimi.d.ts +2 -0
  228. package/dist/types/usage/minimax-code.d.ts +3 -0
  229. package/dist/types/usage/ollama.d.ts +5 -0
  230. package/dist/types/usage/openai-codex-base-url.d.ts +18 -0
  231. package/dist/types/usage/openai-codex-reset.d.ts +88 -0
  232. package/dist/types/usage/openai-codex.d.ts +10 -0
  233. package/dist/types/usage/opencode-go.d.ts +2 -0
  234. package/dist/types/usage/shared.d.ts +1 -0
  235. package/dist/types/usage/synthetic.d.ts +2 -0
  236. package/dist/types/usage/umans.d.ts +2 -0
  237. package/dist/types/usage/xai-oauth.d.ts +12 -0
  238. package/dist/types/usage/zai.d.ts +3 -0
  239. package/dist/types/usage.d.ts +527 -0
  240. package/dist/types/utils/abort.d.ts +25 -0
  241. package/dist/types/utils/anthropic-auth.d.ts +35 -0
  242. package/dist/types/utils/aws-profile.d.ts +17 -0
  243. package/dist/types/utils/block-symbols.d.ts +62 -0
  244. package/dist/types/utils/deterministic-id.d.ts +16 -0
  245. package/dist/types/utils/empty-completion-retry.d.ts +21 -0
  246. package/dist/types/utils/event-stream.d.ts +39 -0
  247. package/dist/types/utils/foundry.d.ts +1 -0
  248. package/dist/types/utils/google-validation.d.ts +2 -0
  249. package/dist/types/utils/harmony-leak.d.ts +135 -0
  250. package/dist/types/utils/http-inspector.d.ts +49 -0
  251. package/dist/types/utils/idle-iterator.d.ts +149 -0
  252. package/dist/types/utils/leaked-thinking-stream.d.ts +33 -0
  253. package/dist/types/utils/openai-http.d.ts +48 -0
  254. package/dist/types/utils/openrouter-headers.d.ts +1 -0
  255. package/dist/types/utils/parse-bind.d.ts +23 -0
  256. package/dist/types/utils/provider-response.d.ts +3 -0
  257. package/dist/types/utils/proxy.d.ts +39 -0
  258. package/dist/types/utils/request-debug.d.ts +29 -0
  259. package/dist/types/utils/retry-after.d.ts +4 -0
  260. package/dist/types/utils/retry.d.ts +14 -0
  261. package/dist/types/utils/schema/adapt.d.ts +24 -0
  262. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  263. package/dist/types/utils/schema/dereference.d.ts +11 -0
  264. package/dist/types/utils/schema/draft.d.ts +10 -0
  265. package/dist/types/utils/schema/equality.d.ts +4 -0
  266. package/dist/types/utils/schema/fields.d.ts +54 -0
  267. package/dist/types/utils/schema/index.d.ts +14 -0
  268. package/dist/types/utils/schema/json-schema-validator.d.ts +20 -0
  269. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  270. package/dist/types/utils/schema/normalize.d.ts +153 -0
  271. package/dist/types/utils/schema/spill.d.ts +8 -0
  272. package/dist/types/utils/schema/stamps.d.ts +17 -0
  273. package/dist/types/utils/schema/strict-tool-validation.d.ts +16 -0
  274. package/dist/types/utils/schema/types.d.ts +4 -0
  275. package/dist/types/utils/schema/typescript.d.ts +24 -0
  276. package/dist/types/utils/schema/wire.d.ts +52 -0
  277. package/dist/types/utils/sdk-stream-timeout.d.ts +33 -0
  278. package/dist/types/utils/sse-debug.d.ts +5 -0
  279. package/dist/types/utils/stream-markup-healing.d.ts +87 -0
  280. package/dist/types/utils/thinking-loop.d.ts +102 -0
  281. package/dist/types/utils/tool-call-loop-guard.d.ts +26 -0
  282. package/dist/types/utils/tool-choice.d.ts +52 -0
  283. package/dist/types/utils/validation.d.ts +28 -0
  284. package/dist/types/utils.d.ts +57 -0
  285. package/package.json +138 -0
  286. package/src/api-registry.ts +109 -0
  287. package/src/auth/sqlite-credential-store.ts +2066 -0
  288. package/src/auth-broker/client.ts +471 -0
  289. package/src/auth-broker/discover.ts +310 -0
  290. package/src/auth-broker/index.ts +7 -0
  291. package/src/auth-broker/refresher.ts +117 -0
  292. package/src/auth-broker/remote-store.ts +1332 -0
  293. package/src/auth-broker/server.ts +898 -0
  294. package/src/auth-broker/snapshot-cache.ts +200 -0
  295. package/src/auth-broker/types.ts +193 -0
  296. package/src/auth-broker/wire-schema-resource.ts +487 -0
  297. package/src/auth-broker/wire-schemas.ts +43 -0
  298. package/src/auth-gateway/http.ts +227 -0
  299. package/src/auth-gateway/index.ts +3 -0
  300. package/src/auth-gateway/server.ts +836 -0
  301. package/src/auth-gateway/types.ts +153 -0
  302. package/src/auth-retry.ts +401 -0
  303. package/src/auth-storage.ts +6540 -0
  304. package/src/dialect/anthropic.md +31 -0
  305. package/src/dialect/anthropic.ts +608 -0
  306. package/src/dialect/catalog.ts +29 -0
  307. package/src/dialect/coercion.ts +136 -0
  308. package/src/dialect/deepseek.md +24 -0
  309. package/src/dialect/deepseek.ts +609 -0
  310. package/src/dialect/demotion.ts +40 -0
  311. package/src/dialect/examples.ts +71 -0
  312. package/src/dialect/factory.ts +34 -0
  313. package/src/dialect/fenced-thinking.ts +184 -0
  314. package/src/dialect/gemini.md +44 -0
  315. package/src/dialect/gemini.ts +583 -0
  316. package/src/dialect/gemma.md +33 -0
  317. package/src/dialect/gemma.ts +387 -0
  318. package/src/dialect/glm.md +32 -0
  319. package/src/dialect/glm.ts +579 -0
  320. package/src/dialect/harmony.md +31 -0
  321. package/src/dialect/harmony.ts +345 -0
  322. package/src/dialect/hermes.md +25 -0
  323. package/src/dialect/hermes.ts +206 -0
  324. package/src/dialect/history.ts +81 -0
  325. package/src/dialect/index.ts +15 -0
  326. package/src/dialect/inventory.ts +30 -0
  327. package/src/dialect/kimi.md +24 -0
  328. package/src/dialect/kimi.ts +340 -0
  329. package/src/dialect/minimax.md +31 -0
  330. package/src/dialect/minimax.ts +95 -0
  331. package/src/dialect/owned-stream.ts +481 -0
  332. package/src/dialect/prompt-template.md +12 -0
  333. package/src/dialect/qwen3.md +28 -0
  334. package/src/dialect/qwen3.ts +240 -0
  335. package/src/dialect/rendering.ts +304 -0
  336. package/src/dialect/thinking.ts +292 -0
  337. package/src/dialect/types.ts +56 -0
  338. package/src/dialect/xml.md +22 -0
  339. package/src/dialect/xml.ts +90 -0
  340. package/src/error/abort.ts +18 -0
  341. package/src/error/auth-classify.ts +47 -0
  342. package/src/error/auth.ts +48 -0
  343. package/src/error/aws.ts +35 -0
  344. package/src/error/classes.ts +281 -0
  345. package/src/error/finalize.ts +69 -0
  346. package/src/error/flags.ts +602 -0
  347. package/src/error/format.ts +45 -0
  348. package/src/error/gateway.ts +96 -0
  349. package/src/error/index.ts +14 -0
  350. package/src/error/oauth.ts +58 -0
  351. package/src/error/provider.ts +63 -0
  352. package/src/error/rate-limit.ts +303 -0
  353. package/src/error/retryable.ts +70 -0
  354. package/src/error/validation.ts +44 -0
  355. package/src/index.ts +51 -0
  356. package/src/provider-details.ts +90 -0
  357. package/src/providers/amazon-bedrock.ts +1064 -0
  358. package/src/providers/anthropic-client.ts +317 -0
  359. package/src/providers/anthropic-messages-server-schema.ts +252 -0
  360. package/src/providers/anthropic-messages-server.ts +818 -0
  361. package/src/providers/anthropic-wire.ts +359 -0
  362. package/src/providers/anthropic.ts +4539 -0
  363. package/src/providers/aws-credentials.ts +772 -0
  364. package/src/providers/aws-eventstream.ts +181 -0
  365. package/src/providers/aws-sigv4.ts +218 -0
  366. package/src/providers/azure-openai-responses.ts +438 -0
  367. package/src/providers/bedrock-mantle.ts +110 -0
  368. package/src/providers/claude-code-fingerprint.ts +20 -0
  369. package/src/providers/cowork-fetch.ts +201 -0
  370. package/src/providers/cursor/exec-modern.ts +496 -0
  371. package/src/providers/cursor/proto/agent.proto +4533 -0
  372. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  373. package/src/providers/cursor/proto/buf.yaml +17 -0
  374. package/src/providers/cursor-pi-args.ts +165 -0
  375. package/src/providers/cursor.ts +4689 -0
  376. package/src/providers/devin/proto/buf/validate/validate.proto +468 -0
  377. package/src/providers/devin/proto/buf.gen.yaml +33 -0
  378. package/src/providers/devin/proto/buf.yaml +17 -0
  379. package/src/providers/devin/proto/cel/expr/checked.proto +103 -0
  380. package/src/providers/devin/proto/cel/expr/eval.proto +38 -0
  381. package/src/providers/devin/proto/cel/expr/explain.proto +15 -0
  382. package/src/providers/devin/proto/cel/expr/syntax.proto +113 -0
  383. package/src/providers/devin/proto/cel/expr/value.proto +41 -0
  384. package/src/providers/devin/proto/connectext/grpc/status/v1/status.proto +11 -0
  385. package/src/providers/devin/proto/errorspb/errors.proto +56 -0
  386. package/src/providers/devin/proto/errorspb/hintdetail.proto +7 -0
  387. package/src/providers/devin/proto/errorspb/markers.proto +10 -0
  388. package/src/providers/devin/proto/errorspb/tags.proto +12 -0
  389. package/src/providers/devin/proto/errorspb/testing.proto +6 -0
  390. package/src/providers/devin/proto/exa/analytics_pb/analytics.proto +188 -0
  391. package/src/providers/devin/proto/exa/api_server_pb/api_server.proto +2461 -0
  392. package/src/providers/devin/proto/exa/auth_pb/auth.proto +19 -0
  393. package/src/providers/devin/proto/exa/auto_cascade_common_pb/auto_cascade_common.proto +79 -0
  394. package/src/providers/devin/proto/exa/browser_preview_pb/browser_preview.proto +32 -0
  395. package/src/providers/devin/proto/exa/bug_checker_pb/bug_checker.proto +22 -0
  396. package/src/providers/devin/proto/exa/cascade_plugins_pb/cascade_plugins.proto +262 -0
  397. package/src/providers/devin/proto/exa/chat_client_server_pb/chat_client_server.proto +57 -0
  398. package/src/providers/devin/proto/exa/chat_pb/chat.proto +449 -0
  399. package/src/providers/devin/proto/exa/code_edit/code_edit_pb/code_edit.proto +186 -0
  400. package/src/providers/devin/proto/exa/codeium_common_pb/codeium_common.proto +4157 -0
  401. package/src/providers/devin/proto/exa/context_module_pb/context_module.proto +175 -0
  402. package/src/providers/devin/proto/exa/cortex_pb/cortex.proto +3268 -0
  403. package/src/providers/devin/proto/exa/dev_pb/dev.proto +26 -0
  404. package/src/providers/devin/proto/exa/diff_action_pb/diff_action.proto +75 -0
  405. package/src/providers/devin/proto/exa/eval/pr_eval/datasets_pb/datasets.proto +103 -0
  406. package/src/providers/devin/proto/exa/eval_pb/eval.proto +1315 -0
  407. package/src/providers/devin/proto/exa/extension_server_pb/extension_server.proto +556 -0
  408. package/src/providers/devin/proto/exa/file_system_provider_pb/file_system_provider.proto +75 -0
  409. package/src/providers/devin/proto/exa/index_pb/index.proto +461 -0
  410. package/src/providers/devin/proto/exa/knowledge_base_pb/knowledge_base.proto +144 -0
  411. package/src/providers/devin/proto/exa/language_server_pb/language_server.proto +2385 -0
  412. package/src/providers/devin/proto/exa/model_management_pb/model_management.proto +186 -0
  413. package/src/providers/devin/proto/exa/opensearch_clients_pb/opensearch_clients.proto +503 -0
  414. package/src/providers/devin/proto/exa/product_analytics_pb/product_analytics.proto +37 -0
  415. package/src/providers/devin/proto/exa/prompt_pb/prompt.proto +92 -0
  416. package/src/providers/devin/proto/exa/reactive_component_pb/reactive_component.proto +96 -0
  417. package/src/providers/devin/proto/exa/seat_management_pb/seat_management.proto +2680 -0
  418. package/src/providers/devin/proto/exa/tokenizer_pb/tokenizer.proto +37 -0
  419. package/src/providers/devin/proto/exa/trainer_pb/config.proto +647 -0
  420. package/src/providers/devin/proto/exa/tree_sitter/language_data_pb/language_data.proto +14 -0
  421. package/src/providers/devin/proto/exa/trust_pb/trust.proto +157 -0
  422. package/src/providers/devin/proto/exa/user_analytics_pb/user_analytics.proto +519 -0
  423. package/src/providers/devin/proto/google.golang.org/appengine/internal/base/api_base.proto +28 -0
  424. package/src/providers/devin/proto/google.golang.org/appengine/internal/datastore/datastore_v3.proto +484 -0
  425. package/src/providers/devin/proto/google.golang.org/appengine/internal/log/log_service.proto +136 -0
  426. package/src/providers/devin/proto/google.golang.org/appengine/internal/remote_api/remote_api.proto +42 -0
  427. package/src/providers/devin/proto/google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto +61 -0
  428. package/src/providers/devin/proto/grpc/binlog/v1/binarylog.proto +84 -0
  429. package/src/providers/devin/proto/io/prometheus/client/metrics.proto +98 -0
  430. package/src/providers/devin.ts +679 -0
  431. package/src/providers/error-message.ts +23 -0
  432. package/src/providers/github-copilot-headers.ts +141 -0
  433. package/src/providers/gitlab-duo-workflow-chatml-note.md +1 -0
  434. package/src/providers/gitlab-duo-workflow.ts +3135 -0
  435. package/src/providers/gitlab-duo.ts +399 -0
  436. package/src/providers/google-auth.ts +330 -0
  437. package/src/providers/google-gemini-cli.ts +1370 -0
  438. package/src/providers/google-shared.ts +1122 -0
  439. package/src/providers/google-types.ts +180 -0
  440. package/src/providers/google-vertex.ts +135 -0
  441. package/src/providers/google.ts +47 -0
  442. package/src/providers/grammar.ts +70 -0
  443. package/src/providers/kimi.ts +51 -0
  444. package/src/providers/mock.ts +514 -0
  445. package/src/providers/ollama.ts +776 -0
  446. package/src/providers/openai-anthropic-shim.ts +166 -0
  447. package/src/providers/openai-chat-server-schema.ts +243 -0
  448. package/src/providers/openai-chat-server.ts +752 -0
  449. package/src/providers/openai-chat-wire.ts +859 -0
  450. package/src/providers/openai-codex/request-transformer.ts +491 -0
  451. package/src/providers/openai-codex/response-handler.ts +102 -0
  452. package/src/providers/openai-codex-responses.ts +4716 -0
  453. package/src/providers/openai-completions.ts +2389 -0
  454. package/src/providers/openai-reasoning-fallback.ts +269 -0
  455. package/src/providers/openai-responses-server-schema.ts +397 -0
  456. package/src/providers/openai-responses-server.ts +1466 -0
  457. package/src/providers/openai-responses-wire.ts +6416 -0
  458. package/src/providers/openai-responses.ts +1393 -0
  459. package/src/providers/openai-shared.ts +3500 -0
  460. package/src/providers/pi-native-client.ts +275 -0
  461. package/src/providers/pi-native-server.ts +245 -0
  462. package/src/providers/register-builtins.ts +503 -0
  463. package/src/providers/synthetic.ts +50 -0
  464. package/src/providers/transform-messages.ts +1083 -0
  465. package/src/providers/vision-guard.ts +54 -0
  466. package/src/registry/aiand.ts +22 -0
  467. package/src/registry/aimlapi.ts +6 -0
  468. package/src/registry/alibaba-coding-plan.ts +104 -0
  469. package/src/registry/alibaba-token-plan.ts +125 -0
  470. package/src/registry/amazon-bedrock.ts +22 -0
  471. package/src/registry/anthropic.ts +26 -0
  472. package/src/registry/api-key-login.ts +115 -0
  473. package/src/registry/api-key-validation.ts +145 -0
  474. package/src/registry/aws.ts +57 -0
  475. package/src/registry/azure.ts +6 -0
  476. package/src/registry/baseten.ts +22 -0
  477. package/src/registry/bedrock-mantle.ts +34 -0
  478. package/src/registry/cerebras.ts +23 -0
  479. package/src/registry/cloudflare-ai-gateway.ts +45 -0
  480. package/src/registry/coreweave.ts +40 -0
  481. package/src/registry/cursor.ts +20 -0
  482. package/src/registry/deepseek.ts +46 -0
  483. package/src/registry/derived.ts +9 -0
  484. package/src/registry/devin.ts +15 -0
  485. package/src/registry/exa.ts +19 -0
  486. package/src/registry/firepass.ts +32 -0
  487. package/src/registry/fireworks.ts +28 -0
  488. package/src/registry/github-copilot.ts +22 -0
  489. package/src/registry/gitlab-duo-workflow.ts +20 -0
  490. package/src/registry/gitlab-duo.ts +19 -0
  491. package/src/registry/gmi-cloud.ts +22 -0
  492. package/src/registry/google-antigravity.ts +22 -0
  493. package/src/registry/google-gemini-cli.ts +22 -0
  494. package/src/registry/google-vertex.ts +38 -0
  495. package/src/registry/google.ts +6 -0
  496. package/src/registry/groq.ts +6 -0
  497. package/src/registry/huggingface.ts +29 -0
  498. package/src/registry/index.ts +4 -0
  499. package/src/registry/kagi.ts +46 -0
  500. package/src/registry/kilo.ts +114 -0
  501. package/src/registry/kimi-code.ts +17 -0
  502. package/src/registry/litellm.ts +45 -0
  503. package/src/registry/llama-cpp.ts +35 -0
  504. package/src/registry/lm-studio.ts +31 -0
  505. package/src/registry/meta.ts +22 -0
  506. package/src/registry/minimax-code-cn.ts +12 -0
  507. package/src/registry/minimax-code.ts +12 -0
  508. package/src/registry/minimax.ts +6 -0
  509. package/src/registry/mistral.ts +6 -0
  510. package/src/registry/moonshot.ts +28 -0
  511. package/src/registry/nanogpt.ts +22 -0
  512. package/src/registry/novita.ts +25 -0
  513. package/src/registry/nvidia.ts +61 -0
  514. package/src/registry/oauth/anthropic-constants.ts +12 -0
  515. package/src/registry/oauth/anthropic.ts +346 -0
  516. package/src/registry/oauth/callback-server.ts +438 -0
  517. package/src/registry/oauth/cursor.ts +187 -0
  518. package/src/registry/oauth/device-code.ts +92 -0
  519. package/src/registry/oauth/devin.ts +124 -0
  520. package/src/registry/oauth/github-copilot.ts +369 -0
  521. package/src/registry/oauth/gitlab-duo-workflow.ts +146 -0
  522. package/src/registry/oauth/gitlab-duo.ts +222 -0
  523. package/src/registry/oauth/google-antigravity.ts +225 -0
  524. package/src/registry/oauth/google-gemini-cli.ts +297 -0
  525. package/src/registry/oauth/google-oauth-shared.ts +211 -0
  526. package/src/registry/oauth/index.ts +187 -0
  527. package/src/registry/oauth/kimi.ts +297 -0
  528. package/src/registry/oauth/minimax-code.ts +53 -0
  529. package/src/registry/oauth/oauth.html +317 -0
  530. package/src/registry/oauth/openai-codex.ts +384 -0
  531. package/src/registry/oauth/opencode.ts +50 -0
  532. package/src/registry/oauth/perplexity.ts +228 -0
  533. package/src/registry/oauth/pkce.ts +18 -0
  534. package/src/registry/oauth/types.ts +96 -0
  535. package/src/registry/oauth/wafer.ts +24 -0
  536. package/src/registry/oauth/xai-oauth.ts +559 -0
  537. package/src/registry/oauth/xiaomi.ts +211 -0
  538. package/src/registry/oauth/zai.ts +285 -0
  539. package/src/registry/ollama-cloud.ts +36 -0
  540. package/src/registry/ollama.ts +43 -0
  541. package/src/registry/openai-codex-device.ts +18 -0
  542. package/src/registry/openai-codex.ts +19 -0
  543. package/src/registry/openai.ts +6 -0
  544. package/src/registry/opencode-go.ts +12 -0
  545. package/src/registry/opencode-zen.ts +12 -0
  546. package/src/registry/openrouter.ts +28 -0
  547. package/src/registry/parallel.ts +45 -0
  548. package/src/registry/perplexity.ts +13 -0
  549. package/src/registry/qianfan.ts +27 -0
  550. package/src/registry/qwen-portal.ts +50 -0
  551. package/src/registry/registry.ts +182 -0
  552. package/src/registry/sakana.ts +22 -0
  553. package/src/registry/siliconflow-cn.ts +22 -0
  554. package/src/registry/siliconflow.ts +22 -0
  555. package/src/registry/synthetic.ts +21 -0
  556. package/src/registry/tavily.ts +45 -0
  557. package/src/registry/together.ts +22 -0
  558. package/src/registry/types.ts +86 -0
  559. package/src/registry/umans.ts +23 -0
  560. package/src/registry/venice.ts +33 -0
  561. package/src/registry/vercel-ai-gateway.ts +38 -0
  562. package/src/registry/vllm.ts +34 -0
  563. package/src/registry/wafer-serverless.ts +12 -0
  564. package/src/registry/xai-oauth.ts +17 -0
  565. package/src/registry/xai.ts +22 -0
  566. package/src/registry/xiaomi-token-plan-ams.ts +12 -0
  567. package/src/registry/xiaomi-token-plan-cn.ts +12 -0
  568. package/src/registry/xiaomi-token-plan-sgp.ts +12 -0
  569. package/src/registry/xiaomi.ts +12 -0
  570. package/src/registry/zai.ts +41 -0
  571. package/src/registry/zenmux.ts +22 -0
  572. package/src/registry/zhipu-coding-plan.ts +27 -0
  573. package/src/stream.ts +1944 -0
  574. package/src/types.ts +1243 -0
  575. package/src/usage/alibaba-token-plan.ts +230 -0
  576. package/src/usage/claude.ts +830 -0
  577. package/src/usage/cursor.ts +335 -0
  578. package/src/usage/gemini.ts +258 -0
  579. package/src/usage/github-copilot.ts +424 -0
  580. package/src/usage/google-antigravity.ts +497 -0
  581. package/src/usage/kimi.ts +277 -0
  582. package/src/usage/minimax-code.ts +291 -0
  583. package/src/usage/ollama.ts +41 -0
  584. package/src/usage/openai-codex-base-url.ts +35 -0
  585. package/src/usage/openai-codex-reset.ts +205 -0
  586. package/src/usage/openai-codex.ts +627 -0
  587. package/src/usage/opencode-go.ts +89 -0
  588. package/src/usage/shared.ts +10 -0
  589. package/src/usage/synthetic.ts +180 -0
  590. package/src/usage/umans.ts +192 -0
  591. package/src/usage/xai-oauth.ts +414 -0
  592. package/src/usage/zai.ts +370 -0
  593. package/src/usage.ts +411 -0
  594. package/src/utils/abort.ts +67 -0
  595. package/src/utils/anthropic-auth.ts +93 -0
  596. package/src/utils/aws-profile.ts +88 -0
  597. package/src/utils/block-symbols.ts +78 -0
  598. package/src/utils/deterministic-id.ts +20 -0
  599. package/src/utils/empty-completion-retry.ts +161 -0
  600. package/src/utils/event-stream.ts +202 -0
  601. package/src/utils/foundry.ts +8 -0
  602. package/src/utils/google-validation.ts +25 -0
  603. package/src/utils/harmony-leak.ts +500 -0
  604. package/src/utils/http-inspector.ts +196 -0
  605. package/src/utils/idle-iterator.ts +531 -0
  606. package/src/utils/leaked-thinking-stream.ts +483 -0
  607. package/src/utils/openai-http.ts +119 -0
  608. package/src/utils/openrouter-headers.ts +12 -0
  609. package/src/utils/parse-bind.ts +56 -0
  610. package/src/utils/provider-response.ts +30 -0
  611. package/src/utils/proxy.ts +314 -0
  612. package/src/utils/request-debug.ts +351 -0
  613. package/src/utils/retry-after.ts +121 -0
  614. package/src/utils/retry.ts +77 -0
  615. package/src/utils/schema/CONSTRAINTS.md +168 -0
  616. package/src/utils/schema/adapt.ts +36 -0
  617. package/src/utils/schema/compatibility.ts +435 -0
  618. package/src/utils/schema/dereference.ts +98 -0
  619. package/src/utils/schema/draft.ts +341 -0
  620. package/src/utils/schema/equality.ts +97 -0
  621. package/src/utils/schema/fields.ts +210 -0
  622. package/src/utils/schema/index.ts +14 -0
  623. package/src/utils/schema/json-schema-validator.ts +595 -0
  624. package/src/utils/schema/meta-validator.ts +167 -0
  625. package/src/utils/schema/normalize.ts +2314 -0
  626. package/src/utils/schema/spill.ts +43 -0
  627. package/src/utils/schema/stamps.ts +109 -0
  628. package/src/utils/schema/strict-tool-validation.ts +117 -0
  629. package/src/utils/schema/types.ts +10 -0
  630. package/src/utils/schema/typescript.ts +212 -0
  631. package/src/utils/schema/wire.ts +662 -0
  632. package/src/utils/sdk-stream-timeout.ts +43 -0
  633. package/src/utils/sse-debug.ts +18 -0
  634. package/src/utils/stream-markup-healing.ts +247 -0
  635. package/src/utils/thinking-loop.ts +552 -0
  636. package/src/utils/tool-call-loop-guard.ts +107 -0
  637. package/src/utils/tool-choice.ts +101 -0
  638. package/src/utils/validation.ts +1932 -0
  639. package/src/utils.ts +492 -0
@@ -0,0 +1,2066 @@
1
+ /**
2
+ * SQLite-backed credential persistence for AuthStorage.
3
+ *
4
+ * The public AuthCredentialStore interface remains in ../auth-storage so local
5
+ * and remote stores share the same contract.
6
+ */
7
+ import { Database, type Statement } from "bun:sqlite";
8
+ import * as fs from "node:fs/promises";
9
+ import * as path from "node:path";
10
+ import { parseAlibabaTokenPlanCredential } from "@linxiraos/pi-catalog/wire/alibaba-token-plan";
11
+ import { getAgentDbPath, getDbBusyTimeoutMs, logger } from "@linxiraos/pi-utils";
12
+ import type {
13
+ AuthCredential,
14
+ AuthCredentialStore,
15
+ CredentialRefreshLeaseFence,
16
+ DisabledCredentialSummary,
17
+ OAuthCredential,
18
+ StoredAuthCredential,
19
+ StoredCredentialBlock,
20
+ } from "../auth-storage";
21
+ import * as AIError from "../error";
22
+ import type { OAuthCredentials } from "../registry/oauth/types";
23
+ import type { Provider } from "../types";
24
+ import type {
25
+ ClientProviderUsage,
26
+ ClientUsageReport,
27
+ ClientUsageSummary,
28
+ UsageCostHistoryEntry,
29
+ UsageCostHistoryQuery,
30
+ UsageHistoryEntry,
31
+ UsageHistoryQuery,
32
+ } from "../usage";
33
+
34
+ // 5 min stale tolerance. Anthropic / OpenAI rate-limit /usage hard at the IP
35
+ // level so we can't fetch all N credentials every cycle; with a long cache
36
+ // each credential's last-known value sticks visible while peers retry. UI
37
+ // data (5h / 7d / monthly limits) is fine being a few minutes stale.
38
+ export const USAGE_REPORT_TTL_MS = 5 * 60_000;
39
+
40
+ /**
41
+ * Downsample usage history to at most one row per hour per account window: a
42
+ * snapshot landing in the same hour bucket as the series' latest row
43
+ * overwrites it in place. That bound makes further retention pruning
44
+ * unnecessary — 1 row/hour is ~9k rows per account window per year.
45
+ */
46
+ const USAGE_HISTORY_BUCKET_MS = 60 * 60_000;
47
+
48
+ /**
49
+ * Merge client observed-usage flushes into at most one row per 5 minutes per
50
+ * (install, provider, model): ~300 rows/day per active model per client
51
+ * instead of one row per 10s flush.
52
+ */
53
+ const CLIENT_USAGE_BUCKET_MS = 5 * 60_000;
54
+
55
+ // ─────────────────────────────────────────────────────────────────────────────
56
+ // SqliteAuthCredentialStore
57
+ // ─────────────────────────────────────────────────────────────────────────────
58
+
59
+ /** Row shape for auth_credentials table queries */
60
+ type AuthRow = {
61
+ id: number;
62
+ provider: string;
63
+ credential_type: string;
64
+ data: string;
65
+ disabled_cause: string | null;
66
+ identity_key: string | null;
67
+ };
68
+
69
+ /** {@link AuthRow} plus `updated_at` — disabled-tombstone queries surface when the row was torn down. */
70
+ type DisabledAuthRow = AuthRow & { updated_at: number | null };
71
+
72
+ type CredentialBlockRow = {
73
+ credential_id: number;
74
+ provider_key: string;
75
+ block_scope: string;
76
+ blocked_until_ms: number;
77
+ updated_at: number;
78
+ };
79
+
80
+ type SerializedCredentialRecord = {
81
+ credentialType: AuthCredential["type"];
82
+ data: string;
83
+ identityKey: string | null;
84
+ };
85
+
86
+ const AUTH_SCHEMA_VERSION = 7;
87
+ const SQLITE_NOW_EPOCH = "CAST(strftime('%s','now') AS INTEGER)";
88
+ const LEGACY_CODEX_BLOCK_PROVIDER_KEY = "openai-codex:oauth";
89
+ const LEGACY_CODEX_BLOCK_SCOPE = "shared";
90
+ const CODEX_METER_BLOCK_SCOPES = ["chat", "spark"] as const;
91
+
92
+ /**
93
+ * SQLite's busy result code family — base `SQLITE_BUSY` plus the extended
94
+ * variants `SQLITE_BUSY_RECOVERY` (concurrent WAL recovery), `SQLITE_BUSY_SNAPSHOT`,
95
+ * and `SQLITE_BUSY_TIMEOUT`. All warrant the same backoff-and-retry treatment.
96
+ */
97
+ export function isSqliteBusyError(err: unknown): boolean {
98
+ if (err === null || typeof err !== "object") return false;
99
+ const code = (err as { code?: unknown }).code;
100
+ return typeof code === "string" && code.startsWith("SQLITE_BUSY");
101
+ }
102
+
103
+ /**
104
+ * SQLite's unrecoverable-corruption result codes — the `SQLITE_CORRUPT` family
105
+ * (base plus extended variants like `SQLITE_CORRUPT_VTAB` / `SQLITE_CORRUPT_INDEX`)
106
+ * and `SQLITE_NOTADB` (the file header is not a database). Unlike
107
+ * {@link isSqliteBusyError}, these never clear by retrying: the store must be
108
+ * repaired or replaced, so callers latch and stop touching it.
109
+ */
110
+ export function isSqliteCorruptionError(err: unknown): boolean {
111
+ if (err === null || typeof err !== "object" || !("code" in err)) return false;
112
+ const code = err.code;
113
+ return typeof code === "string" && (code.startsWith("SQLITE_CORRUPT") || code === "SQLITE_NOTADB");
114
+ }
115
+
116
+ function normalizeStoredAccountId(accountId: string | null | undefined): string | null {
117
+ const normalized = accountId?.trim();
118
+ return normalized && normalized.length > 0 ? normalized : null;
119
+ }
120
+
121
+ function normalizeStoredEmail(email: string | null | undefined): string | null {
122
+ const normalized = email?.trim().toLowerCase();
123
+ return normalized && normalized.length > 0 ? normalized : null;
124
+ }
125
+
126
+ function normalizeStoredIdentityKey(identityKey: string | null | undefined): string | null {
127
+ const normalized = identityKey?.trim();
128
+ return normalized && normalized.length > 0 ? normalized : null;
129
+ }
130
+
131
+ export function serializeCredential(provider: string, credential: AuthCredential): SerializedCredentialRecord | null {
132
+ if (credential.type === "api_key") {
133
+ const data = credential.source === "login" ? { key: credential.key, source: "login" } : { key: credential.key };
134
+ return {
135
+ credentialType: "api_key",
136
+ data: JSON.stringify(data),
137
+ identityKey: null,
138
+ };
139
+ }
140
+ if (credential.type === "oauth") {
141
+ const { type: _type, ...rest } = credential;
142
+ return {
143
+ credentialType: "oauth",
144
+ data: JSON.stringify(rest),
145
+ identityKey: resolveCredentialIdentityKey(provider, credential),
146
+ };
147
+ }
148
+ return null;
149
+ }
150
+
151
+ function deserializeCredential(row: AuthRow): AuthCredential | null {
152
+ let parsed: unknown;
153
+ try {
154
+ parsed = JSON.parse(row.data);
155
+ } catch {
156
+ return null;
157
+ }
158
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
159
+ return null;
160
+ }
161
+ if (row.credential_type === "api_key") {
162
+ const data = parsed as Record<string, unknown>;
163
+ if (typeof data.key === "string") {
164
+ const source = data.source === "login" ? "login" : undefined;
165
+ return source ? { type: "api_key", key: data.key, source } : { type: "api_key", key: data.key };
166
+ }
167
+ }
168
+ if (row.credential_type === "oauth") {
169
+ return { type: "oauth", ...(parsed as Record<string, unknown>) } as AuthCredential;
170
+ }
171
+ return null;
172
+ }
173
+
174
+ function normalizeDisabledCause(disabledCause: string): string {
175
+ const normalized = disabledCause.trim();
176
+ return normalized.length > 0 ? normalized : "disabled";
177
+ }
178
+
179
+ function toStoredAuthCredential(row: AuthRow, credential: AuthCredential): StoredAuthCredential {
180
+ return { id: row.id, provider: row.provider, credential, disabledCause: row.disabled_cause };
181
+ }
182
+
183
+ function resolveProviderCredentialIdentityKey(provider: string, identifiers: string[]): string | null {
184
+ const emailIdentifier = identifiers.find(identifier => identifier.startsWith("email:"));
185
+ if (provider === "anthropic" || provider === "openai-codex") {
186
+ // One account email can hold several organizations/workspaces (e.g. a
187
+ // Team seat plus a personal plan), each with its own org-scoped token
188
+ // and limit pools. Scope identity by org so both subscriptions can be
189
+ // stored side by side. The qualifier rides on whichever base identity
190
+ // is available, so an unqualified account/project fallback would
191
+ // still collapse two subscriptions whenever the email could not be
192
+ // recovered. Org-less credentials (rows written before org capture
193
+ // existed) keep their bare key.
194
+ const base =
195
+ emailIdentifier ??
196
+ identifiers.find(identifier => identifier.startsWith("account:")) ??
197
+ identifiers.find(identifier => identifier.startsWith("project:"));
198
+ const orgIdentifier = identifiers.find(identifier => identifier.startsWith("org:"));
199
+ if (base) return orgIdentifier ? `${base}|${orgIdentifier}` : base;
200
+ // No base identity at all: the org alone still distinguishes the row.
201
+ return orgIdentifier ?? null;
202
+ }
203
+ const accountIdentifier = identifiers.find(identifier => identifier.startsWith("account:"));
204
+ if (accountIdentifier) return accountIdentifier;
205
+ if (emailIdentifier) return emailIdentifier;
206
+ const projectIdentifier = identifiers.find(identifier => identifier.startsWith("project:"));
207
+ if (projectIdentifier) return projectIdentifier;
208
+ return null;
209
+ }
210
+
211
+ export function resolveCredentialIdentityKey(provider: string, credential: AuthCredential): string | null {
212
+ if (credential.type === "api_key") return null;
213
+ return resolveProviderCredentialIdentityKey(provider, extractOAuthCredentialIdentifiers(credential));
214
+ }
215
+
216
+ function resolveRowCredentialIdentityKey(provider: string, row: AuthRow): string | null {
217
+ const identityKey = normalizeStoredIdentityKey(row.identity_key);
218
+ if (identityKey) return identityKey;
219
+ const credential = deserializeCredential(row);
220
+ return credential?.type === "oauth" ? resolveCredentialIdentityKey(provider, credential) : null;
221
+ }
222
+
223
+ function matchesReplacementCredential(
224
+ provider: string,
225
+ existing: AuthCredential | null,
226
+ existingIdentityKey: string | null,
227
+ incoming: AuthCredential,
228
+ ): boolean {
229
+ if (!existing || existing.type !== incoming.type) return false;
230
+ if (incoming.type === "api_key") {
231
+ if (existing.type !== "api_key") return false;
232
+ if (existing.key === incoming.key) return true;
233
+ if (provider !== "alibaba-token-plan") return false;
234
+ const existingToken = parseAlibabaTokenPlanCredential(existing.key)?.token;
235
+ const incomingToken = parseAlibabaTokenPlanCredential(incoming.key)?.token;
236
+ return existingToken !== undefined && existingToken === incomingToken;
237
+ }
238
+ const incomingIdentifiers = extractOAuthCredentialIdentifiers(incoming);
239
+ const incomingIdentityKey = resolveProviderCredentialIdentityKey(provider, incomingIdentifiers);
240
+ if (incomingIdentityKey === null) return false;
241
+ if (incomingIdentityKey === existingIdentityKey) return true;
242
+ if (existingIdentityKey === null) return false;
243
+ // One-way upgrade, applied only when the INCOMING identity key carries the
244
+ // org qualifier (only anthropic and openai-codex keys do, so other
245
+ // providers never reach the checks below). An org-scoped login `org:<o>`
246
+ // claims (and re-keys) any existing row that denotes the same subscription:
247
+ // - `org:<o>` — org-only row stored when identity recovery failed, claimed
248
+ // once a later same-org login recovers a base identity;
249
+ // - `<b>` for any base identity `<b>` (email/account/project) the incoming
250
+ // credential carries — a pre-org legacy row, mirroring the pre-org
251
+ // replace behavior;
252
+ // - `<b>|org:<o>` for any such base — the same subscription keyed by a
253
+ // different base, e.g. an account-keyed row stored while the email could
254
+ // not be recovered, claimed once a later login recovers the email;
255
+ // - any same-org row whose STORED credential shares a base identity with
256
+ // the incoming one — a stored credential can retain identifiers its key
257
+ // does not use (an email-keyed row also carries the account UUID), so a
258
+ // later login that loses the email but keeps the account still updates
259
+ // its row instead of duplicating the subscription.
260
+ // The reverse stays a non-match: an org-less credential only ever replaces
261
+ // via exact key equality above and must never clobber an org-scoped row.
262
+ const orgIdentifier = incomingIdentifiers.find(identifier => identifier.startsWith("org:"));
263
+ if (orgIdentifier === undefined) return false;
264
+ if (incomingIdentityKey !== orgIdentifier && !incomingIdentityKey.endsWith(`|${orgIdentifier}`)) return false;
265
+ if (existingIdentityKey === orgIdentifier) return true;
266
+ const existingIdentifiers =
267
+ existing.type === "oauth" && existingIdentityKey.endsWith(`|${orgIdentifier}`)
268
+ ? extractOAuthCredentialIdentifiers(existing)
269
+ : null;
270
+ // A base identifier that merely repeats the org qualifier's id carries no
271
+ // per-user identity (openai-codex stores the ChatGPT workspace id as both
272
+ // accountId and orgId, shared by every member) — letting it act as a
273
+ // claimable base would re-key another member's same-org row.
274
+ const orgQualifierId = orgIdentifier.slice("org:".length);
275
+ for (const identifier of incomingIdentifiers) {
276
+ const isBase =
277
+ identifier.startsWith("email:") || identifier.startsWith("account:") || identifier.startsWith("project:");
278
+ if (!isBase) continue;
279
+ if (identifier.slice(identifier.indexOf(":") + 1) === orgQualifierId) continue;
280
+ if (existingIdentityKey === identifier) return true;
281
+ if (existingIdentityKey === `${identifier}|${orgIdentifier}`) return true;
282
+ if (existingIdentifiers?.includes(identifier)) return true;
283
+ }
284
+ return false;
285
+ }
286
+
287
+ function extractOAuthCredentialIdentifiers(credential: OAuthCredential): string[] {
288
+ const identifiers = new Set<string>();
289
+ const accountId = normalizeStoredAccountId(credential.accountId);
290
+ if (accountId) identifiers.add(`account:${accountId}`);
291
+ const email = normalizeStoredEmail(credential.email);
292
+ if (email) identifiers.add(`email:${email}`);
293
+ const projectId = normalizeStoredAccountId(credential.projectId);
294
+ if (projectId) identifiers.add(`project:${projectId}`);
295
+ const orgId = normalizeStoredAccountId(credential.orgId);
296
+ if (orgId) identifiers.add(`org:${orgId}`);
297
+ const accessIdentifiers = extractOAuthTokenIdentifiers(credential.access) ?? [];
298
+ for (const identifier of accessIdentifiers) {
299
+ identifiers.add(identifier);
300
+ }
301
+ const refreshIdentifiers = extractOAuthTokenIdentifiers(credential.refresh) ?? [];
302
+ for (const identifier of refreshIdentifiers) {
303
+ identifiers.add(identifier);
304
+ }
305
+ return [...identifiers];
306
+ }
307
+
308
+ function extractOAuthTokenIdentifiers(token: string | undefined): string[] | undefined {
309
+ if (!token) return undefined;
310
+ const parts = token.split(".");
311
+ if (parts.length !== 3) return undefined;
312
+ try {
313
+ const payload = JSON.parse(
314
+ new TextDecoder("utf-8").decode(Uint8Array.fromBase64(parts[1], { alphabet: "base64url" })),
315
+ ) as Record<string, unknown>;
316
+ const identifiers = new Set<string>();
317
+ const directEmail = normalizeStoredEmail(typeof payload.email === "string" ? payload.email : undefined);
318
+ if (directEmail) identifiers.add(`email:${directEmail}`);
319
+ const openAiProfile = payload["https://api.openai.com/profile"];
320
+ if (typeof openAiProfile === "object" && openAiProfile !== null && !Array.isArray(openAiProfile)) {
321
+ const claimEmail = normalizeStoredEmail(
322
+ (openAiProfile as Record<string, unknown>).email as string | undefined,
323
+ );
324
+ if (claimEmail) identifiers.add(`email:${claimEmail}`);
325
+ }
326
+ const openAiAuth = payload["https://api.openai.com/auth"];
327
+ const authClaims =
328
+ typeof openAiAuth === "object" && openAiAuth !== null && !Array.isArray(openAiAuth)
329
+ ? (openAiAuth as Record<string, unknown>)
330
+ : undefined;
331
+ const accountId = normalizeStoredAccountId(
332
+ typeof payload.account_id === "string"
333
+ ? payload.account_id
334
+ : typeof payload.accountId === "string"
335
+ ? payload.accountId
336
+ : typeof payload.user_id === "string"
337
+ ? payload.user_id
338
+ : typeof payload.sub === "string"
339
+ ? payload.sub
340
+ : typeof authClaims?.chatgpt_account_id === "string"
341
+ ? authClaims.chatgpt_account_id
342
+ : undefined,
343
+ );
344
+ if (accountId) identifiers.add(`account:${accountId}`);
345
+ return identifiers.size > 0 ? [...identifiers] : undefined;
346
+ } catch {
347
+ return undefined;
348
+ }
349
+ }
350
+ /**
351
+ * Default SQLite-backed implementation of {@link AuthCredentialStore}.
352
+ *
353
+ * Used by the pi-ai CLI and as the default store for `AuthStorage.create()`.
354
+ * Also exposes convenience methods (`saveOAuth`, `getOAuth`, `saveApiKey`,
355
+ * `getApiKey`, `listProviders`, `deleteProvider`) that callers can use directly
356
+ * without going through `AuthStorage`.
357
+ */
358
+ export class SqliteAuthCredentialStore implements AuthCredentialStore {
359
+ #db: Database;
360
+ #listActiveStmt: Statement;
361
+ #listActiveByProviderStmt: Statement;
362
+ #listDisabledStmt: Statement;
363
+ #listDisabledByProviderStmt: Statement;
364
+ #insertStmt: Statement;
365
+ #updateStmt: Statement;
366
+ #deleteStmt: Statement;
367
+ #deleteIfMatchesStmt: Statement;
368
+ #updateIfMatchesStmt: Statement;
369
+ #deleteByProviderStmt: Statement;
370
+ #hardDeleteStmt: Statement;
371
+ #getCacheStmt: Statement;
372
+ #getCacheIncludingExpiredStmt: Statement;
373
+ #upsertCacheStmt: Statement;
374
+ #deleteCachePrefixStmt: Statement;
375
+ #deleteExpiredCacheStmt: Statement;
376
+ #updateIfMatchesWithLeaseStmt: Statement;
377
+ #deleteIfMatchesWithLeaseStmt: Statement;
378
+ #getCredentialBlockStmt: Statement;
379
+ #listCredentialBlocksByCredentialStmt: Statement;
380
+ #upsertCredentialBlockStmt: Statement;
381
+ #deleteCredentialBlocksStmt: Statement;
382
+ #deleteCredentialBlockStmt: Statement;
383
+ #deleteExpiredCredentialBlocksStmt: Statement;
384
+ #acquireCredentialRefreshLeaseStmt: Statement;
385
+ #getCredentialRefreshLeaseStmt: Statement;
386
+ #renewCredentialRefreshLeaseStmt: Statement;
387
+ #releaseCredentialRefreshLeaseStmt: Statement;
388
+ #credentialBlockReconcileAfter: Map<string, number> = new Map();
389
+ #insertUsageHistoryStmt: Statement;
390
+ #insertUsageCostStmt: Statement;
391
+ #listUsageCostsStmt: Statement;
392
+ #lastUsageHistoryStmt: Statement;
393
+ #listUsageHistoryStmt: Statement;
394
+ #updateUsageHistoryStmt: Statement;
395
+ #dataVersion: number;
396
+ #authRevision: number;
397
+ #localAuthRevision: number;
398
+ #closed = false;
399
+
400
+ constructor(db: Database) {
401
+ this.#db = db;
402
+ this.#initializeSchema();
403
+ this.#dataVersion = this.#readDataVersion();
404
+ this.#authRevision = this.#readAuthRevision();
405
+ this.#localAuthRevision = this.#readLocalAuthRevision();
406
+
407
+ this.#listActiveStmt = this.#db.prepare(
408
+ "SELECT id, provider, credential_type, data, disabled_cause, identity_key FROM auth_credentials WHERE disabled_cause IS NULL ORDER BY id ASC",
409
+ );
410
+ this.#listActiveByProviderStmt = this.#db.prepare(
411
+ "SELECT id, provider, credential_type, data, disabled_cause, identity_key FROM auth_credentials WHERE provider = ? AND disabled_cause IS NULL ORDER BY id ASC",
412
+ );
413
+ this.#listDisabledStmt = this.#db.prepare(
414
+ "SELECT id, provider, credential_type, data, disabled_cause, identity_key, updated_at FROM auth_credentials WHERE disabled_cause IS NOT NULL ORDER BY id ASC",
415
+ );
416
+ this.#listDisabledByProviderStmt = this.#db.prepare(
417
+ "SELECT id, provider, credential_type, data, disabled_cause, identity_key, updated_at FROM auth_credentials WHERE provider = ? AND disabled_cause IS NOT NULL ORDER BY id ASC",
418
+ );
419
+ this.#insertStmt = this.#db.prepare(
420
+ `INSERT INTO auth_credentials (provider, credential_type, data, identity_key, created_at, updated_at) VALUES (?, ?, ?, ?, ${SQLITE_NOW_EPOCH}, ${SQLITE_NOW_EPOCH}) RETURNING id`,
421
+ );
422
+ this.#updateStmt = this.#db.prepare(
423
+ `UPDATE auth_credentials SET credential_type = ?, data = ?, identity_key = ?, updated_at = ${SQLITE_NOW_EPOCH} WHERE id = ?`,
424
+ );
425
+ this.#updateIfMatchesStmt = this.#db.prepare(
426
+ `UPDATE auth_credentials SET credential_type = ?, data = ?, identity_key = ?, updated_at = ${SQLITE_NOW_EPOCH} WHERE id = ? AND data = ? AND disabled_cause IS NULL`,
427
+ );
428
+ this.#updateIfMatchesWithLeaseStmt = this.#db.prepare(
429
+ `UPDATE auth_credentials
430
+ SET credential_type = ?, data = ?, identity_key = ?, updated_at = ${SQLITE_NOW_EPOCH}
431
+ WHERE id = ? AND data = ? AND disabled_cause IS NULL
432
+ AND EXISTS (
433
+ SELECT 1 FROM auth_credential_refresh_leases
434
+ WHERE credential_id = ? AND owner = ? AND expires_at_ms > ?
435
+ )`,
436
+ );
437
+ this.#deleteStmt = this.#db.prepare(
438
+ `UPDATE auth_credentials SET disabled_cause = ?, updated_at = ${SQLITE_NOW_EPOCH} WHERE id = ?`,
439
+ );
440
+ this.#deleteIfMatchesStmt = this.#db.prepare(
441
+ `UPDATE auth_credentials SET disabled_cause = ?, updated_at = ${SQLITE_NOW_EPOCH} WHERE id = ? AND data = ? AND disabled_cause IS NULL`,
442
+ );
443
+ this.#deleteIfMatchesWithLeaseStmt = this.#db.prepare(
444
+ `UPDATE auth_credentials
445
+ SET disabled_cause = ?, updated_at = ${SQLITE_NOW_EPOCH}
446
+ WHERE id = ? AND data = ? AND disabled_cause IS NULL
447
+ AND EXISTS (
448
+ SELECT 1 FROM auth_credential_refresh_leases
449
+ WHERE credential_id = ? AND owner = ? AND expires_at_ms > ?
450
+ )`,
451
+ );
452
+ this.#deleteByProviderStmt = this.#db.prepare(
453
+ `UPDATE auth_credentials SET disabled_cause = ?, updated_at = ${SQLITE_NOW_EPOCH} WHERE provider = ? AND disabled_cause IS NULL`,
454
+ );
455
+ this.#hardDeleteStmt = this.#db.prepare("DELETE FROM auth_credentials WHERE id = ?");
456
+ this.#getCacheStmt = this.#db.prepare(
457
+ `SELECT value FROM cache WHERE key = ? AND expires_at > ${SQLITE_NOW_EPOCH}`,
458
+ );
459
+ this.#getCacheIncludingExpiredStmt = this.#db.prepare("SELECT value FROM cache WHERE key = ?");
460
+ this.#upsertCacheStmt = this.#db.prepare(
461
+ "INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at",
462
+ );
463
+ this.#deleteCachePrefixStmt = this.#db.prepare("DELETE FROM cache WHERE substr(key, 1, ?) = ?");
464
+ this.#deleteExpiredCacheStmt = this.#db.prepare(`DELETE FROM cache WHERE expires_at <= ${SQLITE_NOW_EPOCH}`);
465
+ this.#getCredentialBlockStmt = this.#db.prepare(
466
+ "SELECT blocked_until_ms, updated_at FROM auth_credential_blocks WHERE credential_id = ? AND provider_key = ? AND block_scope = ? AND blocked_until_ms > ?",
467
+ );
468
+ this.#listCredentialBlocksByCredentialStmt = this.#db.prepare(
469
+ `SELECT credential_id, provider_key, block_scope, blocked_until_ms, updated_at
470
+ FROM auth_credential_blocks
471
+ WHERE credential_id = ? AND blocked_until_ms > ?
472
+ AND NOT (provider_key = ? AND block_scope = ?)
473
+ ORDER BY provider_key ASC, block_scope ASC`,
474
+ );
475
+ this.#upsertCredentialBlockStmt = this.#db.prepare(
476
+ `INSERT INTO auth_credential_blocks (credential_id, provider_key, block_scope, blocked_until_ms, updated_at)
477
+ VALUES (?, ?, ?, ?, ${SQLITE_NOW_EPOCH})
478
+ ON CONFLICT(credential_id, provider_key, block_scope) DO UPDATE SET
479
+ blocked_until_ms = MAX(blocked_until_ms, excluded.blocked_until_ms),
480
+ updated_at = excluded.updated_at`,
481
+ );
482
+ this.#deleteCredentialBlocksStmt = this.#db.prepare("DELETE FROM auth_credential_blocks WHERE credential_id = ?");
483
+ this.#deleteCredentialBlockStmt = this.#db.prepare(
484
+ "DELETE FROM auth_credential_blocks WHERE credential_id = ? AND provider_key = ? AND block_scope = ?",
485
+ );
486
+ this.#deleteExpiredCredentialBlocksStmt = this.#db.prepare(
487
+ "DELETE FROM auth_credential_blocks WHERE blocked_until_ms <= ?",
488
+ );
489
+ this.#acquireCredentialRefreshLeaseStmt = this.#db.prepare(
490
+ `INSERT INTO auth_credential_refresh_leases (credential_id, owner, expires_at_ms, updated_at)
491
+ VALUES (?, ?, ?, ${SQLITE_NOW_EPOCH})
492
+ ON CONFLICT(credential_id) DO UPDATE SET
493
+ owner = excluded.owner,
494
+ expires_at_ms = excluded.expires_at_ms,
495
+ updated_at = excluded.updated_at
496
+ WHERE auth_credential_refresh_leases.expires_at_ms <= ?`,
497
+ );
498
+ this.#getCredentialRefreshLeaseStmt = this.#db.prepare(
499
+ "SELECT expires_at_ms FROM auth_credential_refresh_leases WHERE credential_id = ?",
500
+ );
501
+ this.#renewCredentialRefreshLeaseStmt = this.#db.prepare(
502
+ `UPDATE auth_credential_refresh_leases SET expires_at_ms = ?, updated_at = ${SQLITE_NOW_EPOCH} WHERE credential_id = ? AND owner = ?`,
503
+ );
504
+ this.#releaseCredentialRefreshLeaseStmt = this.#db.prepare(
505
+ "DELETE FROM auth_credential_refresh_leases WHERE credential_id = ? AND owner = ?",
506
+ );
507
+ this.#insertUsageHistoryStmt = this.#db.prepare(
508
+ "INSERT INTO usage_history (recorded_at, provider, account_key, email, account_id, limit_id, label, window_label, used_fraction, status, resets_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
509
+ );
510
+ this.#lastUsageHistoryStmt = this.#db.prepare(
511
+ "SELECT id, recorded_at FROM usage_history WHERE provider = ? AND account_key = ? AND limit_id = ? ORDER BY recorded_at DESC LIMIT 1",
512
+ );
513
+ this.#updateUsageHistoryStmt = this.#db.prepare(
514
+ "UPDATE usage_history SET recorded_at = ?, email = ?, account_id = ?, label = ?, window_label = ?, used_fraction = ?, status = ?, resets_at = ? WHERE id = ?",
515
+ );
516
+ this.#listUsageHistoryStmt = this.#db.prepare(
517
+ "SELECT recorded_at, provider, account_key, email, account_id, limit_id, label, window_label, used_fraction, status, resets_at FROM usage_history WHERE recorded_at >= ? AND (? IS NULL OR provider = ?) ORDER BY recorded_at ASC",
518
+ );
519
+ this.#insertUsageCostStmt = this.#db.prepare(
520
+ "INSERT INTO usage_cost_history (recorded_at, provider, account_key, cost_usd) VALUES (?, ?, ?, ?)",
521
+ );
522
+ this.#listUsageCostsStmt = this.#db.prepare(
523
+ "SELECT recorded_at, provider, account_key, cost_usd FROM usage_cost_history WHERE recorded_at >= ? AND (? IS NULL OR provider = ?) AND (? IS NULL OR account_key = ?) ORDER BY recorded_at ASC",
524
+ );
525
+ }
526
+
527
+ static async open(dbPath: string = getAgentDbPath()): Promise<SqliteAuthCredentialStore> {
528
+ const dir = path.dirname(dbPath);
529
+ const dirExists = await fs
530
+ .stat(dir)
531
+ .then(s => s.isDirectory())
532
+ .catch(() => false);
533
+ if (!dirExists) {
534
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
535
+ }
536
+
537
+ // Concurrent omp startups can race against WAL recovery and the schema
538
+ // init's first lock-taking statement. Bun's default `busy_timeout` is 0,
539
+ // so retry the open on `SQLITE_BUSY` / `SQLITE_BUSY_RECOVERY` with bounded
540
+ // exponential backoff before surfacing the failure. See issue #2421.
541
+ const maxAttempts = 4;
542
+ const baseDelayMs = 100;
543
+ let lastBusyError: Error | undefined;
544
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
545
+ let db: Database | undefined;
546
+ try {
547
+ db = new Database(dbPath);
548
+ // Install the busy handler BEFORE the first lock-taking statement
549
+ // on this connection. The leases DDL below and the constructor's
550
+ // schema init both acquire locks during WAL recovery; without a
551
+ // non-zero `busy_timeout` they fail immediately with SQLITE_BUSY.
552
+ // See issue #2421.
553
+ SqliteAuthCredentialStore.#installBusyTimeout(db);
554
+ try {
555
+ await fs.chmod(dbPath, 0o600);
556
+ } catch {
557
+ // Ignore chmod failures (e.g., Windows)
558
+ }
559
+ SqliteAuthCredentialStore.#ensureAuthCredentialRefreshLeasesTable(db);
560
+ return new SqliteAuthCredentialStore(db);
561
+ } catch (err) {
562
+ db?.close();
563
+ if (!isSqliteBusyError(err)) {
564
+ throw err;
565
+ }
566
+ lastBusyError = err instanceof Error ? err : new Error(String(err));
567
+ if (attempt < maxAttempts - 1) {
568
+ await Bun.sleep(baseDelayMs * 2 ** attempt);
569
+ }
570
+ }
571
+ }
572
+ throw new AIError.ConfigurationError(
573
+ `Failed to open auth database at '${dbPath}' after ${maxAttempts} attempts: ${lastBusyError?.message}`,
574
+ { cause: lastBusyError },
575
+ );
576
+ }
577
+
578
+ static #ensureAuthCredentialRefreshLeasesTable(db: Database): void {
579
+ db.run(`
580
+ CREATE TABLE IF NOT EXISTS auth_credential_refresh_leases (
581
+ credential_id INTEGER PRIMARY KEY,
582
+ owner TEXT NOT NULL,
583
+ expires_at_ms INTEGER NOT NULL,
584
+ updated_at INTEGER NOT NULL
585
+ );
586
+ CREATE INDEX IF NOT EXISTS idx_auth_credential_refresh_leases_expires ON auth_credential_refresh_leases(expires_at_ms);
587
+ `);
588
+ }
589
+
590
+ /**
591
+ * Install the per-connection busy handler so lock-taking statements wait for
592
+ * a contended writer instead of failing immediately (Bun defaults
593
+ * `busy_timeout` to 0). MUST run before the first lock-taking statement on
594
+ * the connection: concurrent omp startups race WAL recovery and the leases
595
+ * DDL. Uses the centralized timeout so headless hosts keep their bounded
596
+ * busy wait instead of the interactive 5s value. See issues #2421, #7298.
597
+ */
598
+ static #installBusyTimeout(db: Database): void {
599
+ db.run(`PRAGMA busy_timeout = ${getDbBusyTimeoutMs()}`);
600
+ }
601
+
602
+ #initializeSchema(): void {
603
+ // Install the busy handler BEFORE any lock-taking statement (incl.
604
+ // `PRAGMA journal_mode=WAL`, which acquires an exclusive lock during WAL
605
+ // recovery). Without this, concurrent omp startups can crash here with
606
+ // `SQLITE_BUSY` / `SQLITE_BUSY_RECOVERY`. Re-setting when opened via
607
+ // `open()` (which already installed it) is idempotent. See issue #2421.
608
+ SqliteAuthCredentialStore.#installBusyTimeout(this.#db);
609
+ this.#db.run(`
610
+ PRAGMA journal_mode=WAL;
611
+ PRAGMA synchronous=NORMAL;
612
+ CREATE TABLE IF NOT EXISTS auth_schema_version (
613
+ id INTEGER PRIMARY KEY CHECK (id = 1),
614
+ version INTEGER NOT NULL
615
+ );
616
+ CREATE TABLE IF NOT EXISTS cache (
617
+ key TEXT PRIMARY KEY,
618
+ value TEXT NOT NULL,
619
+ expires_at INTEGER NOT NULL
620
+ );
621
+ CREATE INDEX IF NOT EXISTS idx_cache_expires ON cache(expires_at);
622
+ CREATE TABLE IF NOT EXISTS usage_history (
623
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
624
+ recorded_at INTEGER NOT NULL,
625
+ provider TEXT NOT NULL,
626
+ account_key TEXT NOT NULL,
627
+ email TEXT,
628
+ account_id TEXT,
629
+ limit_id TEXT NOT NULL,
630
+ label TEXT NOT NULL,
631
+ window_label TEXT,
632
+ used_fraction REAL,
633
+ status TEXT,
634
+ resets_at INTEGER
635
+ );
636
+ CREATE INDEX IF NOT EXISTS idx_usage_history_series ON usage_history(provider, account_key, limit_id, recorded_at);
637
+ CREATE TABLE IF NOT EXISTS usage_cost_history (
638
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
639
+ recorded_at INTEGER NOT NULL,
640
+ provider TEXT NOT NULL,
641
+ account_key TEXT NOT NULL,
642
+ cost_usd REAL NOT NULL
643
+ );
644
+ CREATE INDEX IF NOT EXISTS idx_usage_cost_history_lookup ON usage_cost_history(provider, account_key, recorded_at);
645
+ CREATE INDEX IF NOT EXISTS idx_usage_history_recorded ON usage_history(recorded_at);
646
+ CREATE TABLE IF NOT EXISTS clients (
647
+ install_id TEXT PRIMARY KEY,
648
+ hostname TEXT,
649
+ first_seen INTEGER NOT NULL,
650
+ last_seen INTEGER NOT NULL
651
+ );
652
+ CREATE TABLE IF NOT EXISTS client_usage (
653
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
654
+ recorded_at INTEGER NOT NULL,
655
+ install_id TEXT NOT NULL,
656
+ provider TEXT NOT NULL,
657
+ model TEXT NOT NULL,
658
+ requests INTEGER NOT NULL,
659
+ input_tokens INTEGER NOT NULL,
660
+ output_tokens INTEGER NOT NULL,
661
+ cache_read_tokens INTEGER NOT NULL,
662
+ cache_write_tokens INTEGER NOT NULL,
663
+ cost_usd REAL NOT NULL DEFAULT 0
664
+ );
665
+ CREATE INDEX IF NOT EXISTS idx_client_usage_series ON client_usage(install_id, provider, model, recorded_at);
666
+ CREATE INDEX IF NOT EXISTS idx_client_usage_recorded ON client_usage(recorded_at);
667
+ `);
668
+
669
+ if (!this.#authCredentialsTableExists()) {
670
+ this.#createAuthCredentialsTable();
671
+ this.#createAuthCredentialBlocksTable();
672
+ this.#createAuthCredentialRefreshLeasesTable();
673
+ this.#createAuthCredentialBlockCompatibilityObjects();
674
+ this.#createAuthChangeTrackingObjects();
675
+ this.#writeAuthSchemaVersion(AUTH_SCHEMA_VERSION);
676
+ return;
677
+ }
678
+
679
+ const recordedVersion = this.#readAuthSchemaVersion();
680
+ const schemaVersion = recordedVersion ?? this.#inferAuthSchemaVersion();
681
+ if (schemaVersion > AUTH_SCHEMA_VERSION) {
682
+ logger.warn("SqliteAuthCredentialStore schema version mismatch", {
683
+ current: schemaVersion,
684
+ expected: AUTH_SCHEMA_VERSION,
685
+ });
686
+ } else if (schemaVersion < AUTH_SCHEMA_VERSION) {
687
+ this.#migrateAuthSchema(schemaVersion);
688
+ }
689
+
690
+ this.#createAuthCredentialIndexes();
691
+ this.#createAuthCredentialBlocksTable();
692
+ this.#createAuthCredentialRefreshLeasesTable();
693
+ if (schemaVersion <= AUTH_SCHEMA_VERSION) {
694
+ this.#createAuthCredentialBlockCompatibilityObjects();
695
+ }
696
+ this.#createAuthChangeTrackingObjects();
697
+ this.#backfillCredentialIdentityKeys();
698
+ // Rewriting an already-current version row is a no-op write transaction
699
+ // on every boot; only persist when the recorded version actually changes.
700
+ if (recordedVersion !== AUTH_SCHEMA_VERSION && schemaVersion <= AUTH_SCHEMA_VERSION) {
701
+ this.#writeAuthSchemaVersion(AUTH_SCHEMA_VERSION);
702
+ }
703
+ }
704
+
705
+ #authCredentialsTableExists(): boolean {
706
+ const stmt = this.#db.prepare(
707
+ "SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'auth_credentials'",
708
+ );
709
+ try {
710
+ const row = stmt.get() as { present?: number } | undefined;
711
+ return row?.present === 1;
712
+ } finally {
713
+ stmt.finalize();
714
+ }
715
+ }
716
+
717
+ #readAuthSchemaVersion(): number | null {
718
+ const stmt = this.#db.prepare("SELECT version FROM auth_schema_version WHERE id = 1");
719
+ try {
720
+ const row = stmt.get() as { version?: number } | undefined;
721
+ return typeof row?.version === "number" ? row.version : null;
722
+ } finally {
723
+ stmt.finalize();
724
+ }
725
+ }
726
+
727
+ #writeAuthSchemaVersion(version: number): void {
728
+ const stmt = this.#db.prepare("INSERT OR REPLACE INTO auth_schema_version(id, version) VALUES (1, ?)");
729
+ try {
730
+ stmt.run(version);
731
+ } finally {
732
+ stmt.finalize();
733
+ }
734
+ }
735
+
736
+ #inferAuthSchemaVersion(): number {
737
+ const stmt = this.#db.prepare("PRAGMA table_info(auth_credentials)");
738
+ try {
739
+ const cols = stmt.all() as Array<{ name?: string }>;
740
+ return this.#inferAuthSchemaVersionFromColumns(cols);
741
+ } finally {
742
+ stmt.finalize();
743
+ }
744
+ }
745
+
746
+ #inferAuthSchemaVersionFromColumns(cols: Array<{ name?: string }>): number {
747
+ const hasDisabledCause = cols.some(column => column.name === "disabled_cause");
748
+ const hasIdentityKey = cols.some(column => column.name === "identity_key");
749
+ const hasAccountId = cols.some(column => column.name === "account_id");
750
+ const hasEmail = cols.some(column => column.name === "email");
751
+ if (hasIdentityKey) return 3;
752
+ if (hasAccountId || hasEmail) return 2;
753
+ if (hasDisabledCause) return 1;
754
+ return 0;
755
+ }
756
+
757
+ #createAuthCredentialsTable(): void {
758
+ this.#db.run(`
759
+ CREATE TABLE IF NOT EXISTS auth_credentials (
760
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
761
+ provider TEXT NOT NULL,
762
+ credential_type TEXT NOT NULL,
763
+ data TEXT NOT NULL,
764
+ disabled_cause TEXT DEFAULT NULL,
765
+ identity_key TEXT DEFAULT NULL,
766
+ created_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH}),
767
+ updated_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
768
+ );
769
+ `);
770
+ this.#createAuthCredentialIndexes();
771
+ }
772
+
773
+ #createAuthCredentialIndexes(): void {
774
+ this.#db.run(`
775
+ CREATE INDEX IF NOT EXISTS idx_auth_provider ON auth_credentials(provider);
776
+ CREATE INDEX IF NOT EXISTS idx_auth_provider_identity ON auth_credentials(provider, identity_key) WHERE identity_key IS NOT NULL;
777
+ `);
778
+ }
779
+
780
+ #createAuthCredentialBlocksTable(): void {
781
+ this.#db.run(`
782
+ CREATE TABLE IF NOT EXISTS auth_credential_blocks (
783
+ credential_id INTEGER NOT NULL,
784
+ provider_key TEXT NOT NULL,
785
+ block_scope TEXT NOT NULL DEFAULT '',
786
+ blocked_until_ms INTEGER NOT NULL,
787
+ updated_at INTEGER NOT NULL,
788
+ PRIMARY KEY (credential_id, provider_key, block_scope)
789
+ );
790
+ CREATE INDEX IF NOT EXISTS idx_auth_credential_blocks_expires ON auth_credential_blocks(blocked_until_ms);
791
+ `);
792
+ }
793
+
794
+ #createAuthChangeTrackingObjects(): void {
795
+ this.#db.run(`
796
+ CREATE TABLE IF NOT EXISTS auth_change_revision (
797
+ id INTEGER PRIMARY KEY CHECK (id = 1),
798
+ revision INTEGER NOT NULL
799
+ );
800
+ INSERT OR IGNORE INTO auth_change_revision (id, revision) VALUES (1, 0);
801
+ CREATE TEMP TABLE IF NOT EXISTS auth_local_change_revision (
802
+ id INTEGER PRIMARY KEY CHECK (id = 1),
803
+ revision INTEGER NOT NULL
804
+ );
805
+ INSERT OR IGNORE INTO auth_local_change_revision (id, revision) VALUES (1, 0);
806
+ `);
807
+ for (const table of ["auth_credentials", "auth_credential_blocks"] as const) {
808
+ for (const event of ["INSERT", "UPDATE", "DELETE"] as const) {
809
+ this.#db.run(`
810
+ CREATE TRIGGER IF NOT EXISTS auth_change_revision_${table}_${event.toLowerCase()}
811
+ AFTER ${event} ON ${table}
812
+ BEGIN
813
+ UPDATE auth_change_revision SET revision = revision + 1 WHERE id = 1;
814
+ END;
815
+ `);
816
+ this.#db.run(`
817
+ CREATE TEMP TRIGGER IF NOT EXISTS auth_local_change_revision_${table}_${event.toLowerCase()}
818
+ AFTER ${event} ON main.${table}
819
+ BEGIN
820
+ UPDATE auth_local_change_revision SET revision = revision + 1 WHERE id = 1;
821
+ END;
822
+ `);
823
+ }
824
+ }
825
+ }
826
+
827
+ #createAuthCredentialBlockMirrorGuardTable(): void {
828
+ this.#db.run(`
829
+ CREATE TABLE IF NOT EXISTS auth_credential_block_mirror_guard (
830
+ credential_id INTEGER PRIMARY KEY
831
+ ) WITHOUT ROWID;
832
+ `);
833
+ }
834
+
835
+ /**
836
+ * Keep a physical Codex `shared` row for pre-meter binaries that read this
837
+ * database directly. Meter rows are canonical for current code. The guard
838
+ * suppresses feedback while triggers update the compatibility projection.
839
+ */
840
+ #createAuthCredentialBlockCompatibilityTriggers(): void {
841
+ for (const event of ["INSERT", "UPDATE"] as const) {
842
+ const eventName = event.toLowerCase();
843
+ this.#db.run(`
844
+ CREATE TRIGGER IF NOT EXISTS auth_codex_shared_${eventName}_to_meters
845
+ AFTER ${event} ON auth_credential_blocks
846
+ WHEN NEW.provider_key = 'openai-codex:oauth'
847
+ AND NEW.block_scope = 'shared'
848
+ AND NOT EXISTS (
849
+ SELECT 1 FROM auth_credential_block_mirror_guard
850
+ WHERE credential_id = NEW.credential_id
851
+ )
852
+ BEGIN
853
+ INSERT OR IGNORE INTO auth_credential_block_mirror_guard (credential_id)
854
+ VALUES (NEW.credential_id);
855
+ INSERT INTO auth_credential_blocks (
856
+ credential_id,
857
+ provider_key,
858
+ block_scope,
859
+ blocked_until_ms,
860
+ updated_at
861
+ )
862
+ VALUES (
863
+ NEW.credential_id,
864
+ NEW.provider_key,
865
+ 'chat',
866
+ NEW.blocked_until_ms,
867
+ NEW.updated_at
868
+ )
869
+ ON CONFLICT(credential_id, provider_key, block_scope) DO UPDATE SET
870
+ blocked_until_ms = MAX(auth_credential_blocks.blocked_until_ms, excluded.blocked_until_ms),
871
+ updated_at = MAX(auth_credential_blocks.updated_at, excluded.updated_at);
872
+ INSERT INTO auth_credential_blocks (
873
+ credential_id,
874
+ provider_key,
875
+ block_scope,
876
+ blocked_until_ms,
877
+ updated_at
878
+ )
879
+ VALUES (
880
+ NEW.credential_id,
881
+ NEW.provider_key,
882
+ 'spark',
883
+ NEW.blocked_until_ms,
884
+ NEW.updated_at
885
+ )
886
+ ON CONFLICT(credential_id, provider_key, block_scope) DO UPDATE SET
887
+ blocked_until_ms = MAX(auth_credential_blocks.blocked_until_ms, excluded.blocked_until_ms),
888
+ updated_at = MAX(auth_credential_blocks.updated_at, excluded.updated_at);
889
+ DELETE FROM auth_credential_block_mirror_guard
890
+ WHERE credential_id = NEW.credential_id;
891
+ END;
892
+
893
+ CREATE TRIGGER IF NOT EXISTS auth_codex_meter_${eventName}_to_shared
894
+ AFTER ${event} ON auth_credential_blocks
895
+ WHEN NEW.provider_key = 'openai-codex:oauth'
896
+ AND NEW.block_scope IN ('chat', 'spark')
897
+ AND NOT EXISTS (
898
+ SELECT 1 FROM auth_credential_block_mirror_guard
899
+ WHERE credential_id = NEW.credential_id
900
+ )
901
+ BEGIN
902
+ INSERT OR IGNORE INTO auth_credential_block_mirror_guard (credential_id)
903
+ VALUES (NEW.credential_id);
904
+ DELETE FROM auth_credential_blocks
905
+ WHERE credential_id = NEW.credential_id
906
+ AND provider_key = NEW.provider_key
907
+ AND block_scope = 'shared';
908
+ INSERT INTO auth_credential_blocks (
909
+ credential_id,
910
+ provider_key,
911
+ block_scope,
912
+ blocked_until_ms,
913
+ updated_at
914
+ )
915
+ SELECT
916
+ NEW.credential_id,
917
+ NEW.provider_key,
918
+ 'shared',
919
+ MAX(blocked_until_ms),
920
+ MAX(updated_at)
921
+ FROM auth_credential_blocks
922
+ WHERE credential_id = NEW.credential_id
923
+ AND provider_key = NEW.provider_key
924
+ AND block_scope IN ('chat', 'spark')
925
+ GROUP BY credential_id, provider_key;
926
+ DELETE FROM auth_credential_block_mirror_guard
927
+ WHERE credential_id = NEW.credential_id;
928
+ END;
929
+ `);
930
+ }
931
+
932
+ this.#db.run(`
933
+ CREATE TRIGGER IF NOT EXISTS auth_codex_shared_delete_to_meters
934
+ AFTER DELETE ON auth_credential_blocks
935
+ WHEN OLD.provider_key = 'openai-codex:oauth'
936
+ AND OLD.block_scope = 'shared'
937
+ AND NOT EXISTS (
938
+ SELECT 1 FROM auth_credential_block_mirror_guard
939
+ WHERE credential_id = OLD.credential_id
940
+ )
941
+ BEGIN
942
+ INSERT OR IGNORE INTO auth_credential_block_mirror_guard (credential_id)
943
+ VALUES (OLD.credential_id);
944
+ DELETE FROM auth_credential_blocks
945
+ WHERE credential_id = OLD.credential_id
946
+ AND provider_key = OLD.provider_key
947
+ AND block_scope IN ('chat', 'spark');
948
+ DELETE FROM auth_credential_block_mirror_guard
949
+ WHERE credential_id = OLD.credential_id;
950
+ END;
951
+
952
+ CREATE TRIGGER IF NOT EXISTS auth_codex_meter_delete_to_shared
953
+ AFTER DELETE ON auth_credential_blocks
954
+ WHEN OLD.provider_key = 'openai-codex:oauth'
955
+ AND OLD.block_scope IN ('chat', 'spark')
956
+ AND NOT EXISTS (
957
+ SELECT 1 FROM auth_credential_block_mirror_guard
958
+ WHERE credential_id = OLD.credential_id
959
+ )
960
+ BEGIN
961
+ INSERT OR IGNORE INTO auth_credential_block_mirror_guard (credential_id)
962
+ VALUES (OLD.credential_id);
963
+ DELETE FROM auth_credential_blocks
964
+ WHERE credential_id = OLD.credential_id
965
+ AND provider_key = OLD.provider_key
966
+ AND block_scope = 'shared';
967
+ INSERT INTO auth_credential_blocks (
968
+ credential_id,
969
+ provider_key,
970
+ block_scope,
971
+ blocked_until_ms,
972
+ updated_at
973
+ )
974
+ SELECT
975
+ OLD.credential_id,
976
+ OLD.provider_key,
977
+ 'shared',
978
+ MAX(blocked_until_ms),
979
+ MAX(updated_at)
980
+ FROM auth_credential_blocks
981
+ WHERE credential_id = OLD.credential_id
982
+ AND provider_key = OLD.provider_key
983
+ AND block_scope IN ('chat', 'spark')
984
+ GROUP BY credential_id, provider_key;
985
+ DELETE FROM auth_credential_block_mirror_guard
986
+ WHERE credential_id = OLD.credential_id;
987
+ END;
988
+ `);
989
+ }
990
+
991
+ #createAuthCredentialBlockCompatibilityObjects(): void {
992
+ this.#createAuthCredentialBlockMirrorGuardTable();
993
+ this.#createAuthCredentialBlockCompatibilityTriggers();
994
+ }
995
+
996
+ #createAuthCredentialRefreshLeasesTable(): void {
997
+ SqliteAuthCredentialStore.#ensureAuthCredentialRefreshLeasesTable(this.#db);
998
+ }
999
+
1000
+ #migrateAuthSchema(fromVersion: number): void {
1001
+ if (fromVersion < 1) {
1002
+ this.#migrateAuthSchemaV0ToV1();
1003
+ }
1004
+ if (fromVersion < 3) {
1005
+ this.#migrateAuthSchemaV1OrV2ToV3();
1006
+ }
1007
+ if (fromVersion < 4) {
1008
+ this.#migrateAuthSchemaV3ToV4();
1009
+ }
1010
+ if (fromVersion < 5) {
1011
+ this.#migrateAuthSchemaV4ToV5();
1012
+ }
1013
+ if (fromVersion < 6) {
1014
+ this.#migrateAuthSchemaV5ToV6();
1015
+ }
1016
+ if (fromVersion < 7) {
1017
+ this.#migrateAuthSchemaV6ToV7();
1018
+ }
1019
+ }
1020
+
1021
+ #migrateAuthSchemaV0ToV1(): void {
1022
+ const migrate = this.#db.transaction(() => {
1023
+ const stmt = this.#db.prepare("PRAGMA table_info(auth_credentials)");
1024
+ let hasDisabled = false;
1025
+ try {
1026
+ const v0Cols = stmt.all() as Array<{ name?: string }>;
1027
+ hasDisabled = v0Cols.some(col => col.name === "disabled");
1028
+ } finally {
1029
+ stmt.finalize();
1030
+ }
1031
+
1032
+ this.#db.run("ALTER TABLE auth_credentials RENAME TO auth_credentials_v0");
1033
+ this.#db.run(`
1034
+ CREATE TABLE auth_credentials (
1035
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1036
+ provider TEXT NOT NULL,
1037
+ credential_type TEXT NOT NULL,
1038
+ data TEXT NOT NULL,
1039
+ disabled_cause TEXT DEFAULT NULL,
1040
+ created_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH}),
1041
+ updated_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
1042
+ );
1043
+ `);
1044
+ this.#db.run(`
1045
+ INSERT INTO auth_credentials (id, provider, credential_type, data, disabled_cause, created_at, updated_at)
1046
+ SELECT
1047
+ id,
1048
+ provider,
1049
+ credential_type,
1050
+ data,
1051
+ ${hasDisabled ? "CASE WHEN disabled = 1 THEN 'disabled' ELSE NULL END" : "NULL"},
1052
+ created_at,
1053
+ updated_at
1054
+ FROM auth_credentials_v0
1055
+ `);
1056
+ this.#db.run("DROP TABLE auth_credentials_v0");
1057
+ });
1058
+ migrate();
1059
+ }
1060
+
1061
+ #migrateAuthSchemaV1OrV2ToV3(): void {
1062
+ const migrate = this.#db.transaction(() => {
1063
+ this.#db.run("ALTER TABLE auth_credentials RENAME TO auth_credentials_legacy");
1064
+ this.#createAuthCredentialsTable();
1065
+ this.#db.run(`
1066
+ INSERT INTO auth_credentials (id, provider, credential_type, data, disabled_cause, identity_key, created_at, updated_at)
1067
+ SELECT
1068
+ id,
1069
+ provider,
1070
+ credential_type,
1071
+ data,
1072
+ disabled_cause,
1073
+ NULL,
1074
+ created_at,
1075
+ updated_at
1076
+ FROM auth_credentials_legacy
1077
+ `);
1078
+ this.#db.run("DROP TABLE auth_credentials_legacy");
1079
+ });
1080
+ migrate();
1081
+ }
1082
+
1083
+ #migrateAuthSchemaV3ToV4(): void {
1084
+ const migrate = this.#db.transaction(() => {
1085
+ this.#db.run("ALTER TABLE auth_credentials RENAME TO auth_credentials_v3");
1086
+ this.#createAuthCredentialsTable();
1087
+ this.#db.run(`
1088
+ INSERT INTO auth_credentials (id, provider, credential_type, data, disabled_cause, identity_key, created_at, updated_at)
1089
+ SELECT
1090
+ id,
1091
+ provider,
1092
+ credential_type,
1093
+ data,
1094
+ disabled_cause,
1095
+ identity_key,
1096
+ created_at,
1097
+ updated_at
1098
+ FROM auth_credentials_v3
1099
+ `);
1100
+ this.#db.run("DROP TABLE auth_credentials_v3");
1101
+ });
1102
+ migrate();
1103
+ }
1104
+
1105
+ #migrateAuthSchemaV4ToV5(): void {
1106
+ const migrate = this.#db.transaction(() => {
1107
+ this.#createAuthCredentialBlocksTable();
1108
+ });
1109
+ migrate();
1110
+ }
1111
+
1112
+ #migrateAuthSchemaV5ToV6(): void {
1113
+ const migrate = this.#db.transaction(() => {
1114
+ this.#createAuthCredentialRefreshLeasesTable();
1115
+ });
1116
+ migrate();
1117
+ }
1118
+
1119
+ #migrateAuthSchemaV6ToV7(): void {
1120
+ const migrate = this.#db.transaction(() => {
1121
+ this.#createAuthCredentialBlocksTable();
1122
+ this.#createAuthCredentialBlockMirrorGuardTable();
1123
+ this.#db.run(`
1124
+ DELETE FROM auth_credential_block_mirror_guard;
1125
+ INSERT OR IGNORE INTO auth_credential_block_mirror_guard (credential_id)
1126
+ SELECT DISTINCT credential_id
1127
+ FROM auth_credential_blocks
1128
+ WHERE provider_key = 'openai-codex:oauth'
1129
+ AND block_scope IN ('chat', 'spark', 'shared');
1130
+
1131
+ INSERT INTO auth_credential_blocks (
1132
+ credential_id,
1133
+ provider_key,
1134
+ block_scope,
1135
+ blocked_until_ms,
1136
+ updated_at
1137
+ )
1138
+ SELECT credential_id, provider_key, 'chat', blocked_until_ms, updated_at
1139
+ FROM auth_credential_blocks
1140
+ WHERE provider_key = 'openai-codex:oauth'
1141
+ AND block_scope = 'shared'
1142
+ ON CONFLICT(credential_id, provider_key, block_scope) DO UPDATE SET
1143
+ blocked_until_ms = MAX(auth_credential_blocks.blocked_until_ms, excluded.blocked_until_ms),
1144
+ updated_at = MAX(auth_credential_blocks.updated_at, excluded.updated_at);
1145
+
1146
+ INSERT INTO auth_credential_blocks (
1147
+ credential_id,
1148
+ provider_key,
1149
+ block_scope,
1150
+ blocked_until_ms,
1151
+ updated_at
1152
+ )
1153
+ SELECT credential_id, provider_key, 'spark', blocked_until_ms, updated_at
1154
+ FROM auth_credential_blocks
1155
+ WHERE provider_key = 'openai-codex:oauth'
1156
+ AND block_scope = 'shared'
1157
+ ON CONFLICT(credential_id, provider_key, block_scope) DO UPDATE SET
1158
+ blocked_until_ms = MAX(auth_credential_blocks.blocked_until_ms, excluded.blocked_until_ms),
1159
+ updated_at = MAX(auth_credential_blocks.updated_at, excluded.updated_at);
1160
+
1161
+ INSERT INTO auth_credential_blocks (
1162
+ credential_id,
1163
+ provider_key,
1164
+ block_scope,
1165
+ blocked_until_ms,
1166
+ updated_at
1167
+ )
1168
+ SELECT
1169
+ credential_id,
1170
+ provider_key,
1171
+ 'shared',
1172
+ MAX(blocked_until_ms),
1173
+ MAX(updated_at)
1174
+ FROM auth_credential_blocks
1175
+ WHERE provider_key = 'openai-codex:oauth'
1176
+ AND block_scope IN ('chat', 'spark')
1177
+ GROUP BY credential_id, provider_key
1178
+ ON CONFLICT(credential_id, provider_key, block_scope) DO UPDATE SET
1179
+ blocked_until_ms = excluded.blocked_until_ms,
1180
+ updated_at = excluded.updated_at;
1181
+
1182
+ DELETE FROM auth_credential_block_mirror_guard;
1183
+ `);
1184
+ this.#createAuthCredentialBlockCompatibilityTriggers();
1185
+ this.#writeAuthSchemaVersion(7);
1186
+ });
1187
+ migrate.immediate();
1188
+ }
1189
+
1190
+ #backfillCredentialIdentityKeys(): void {
1191
+ const selectRowsStmt = this.#db.prepare(
1192
+ "SELECT id, provider, credential_type, data, disabled_cause, identity_key FROM auth_credentials WHERE identity_key IS NULL ORDER BY id ASC",
1193
+ );
1194
+ let rows: AuthRow[];
1195
+ try {
1196
+ rows = selectRowsStmt.all() as AuthRow[];
1197
+ } finally {
1198
+ selectRowsStmt.finalize();
1199
+ }
1200
+ if (rows.length === 0) return;
1201
+
1202
+ let updateIdentity: Statement | null = null;
1203
+ try {
1204
+ for (const row of rows) {
1205
+ const identityKey = resolveRowCredentialIdentityKey(row.provider, row);
1206
+ // Rows whose identity cannot be derived stay NULL; writing NULL over
1207
+ // NULL would just burn a write transaction on every boot.
1208
+ if (identityKey === null) continue;
1209
+ updateIdentity ??= this.#db.prepare("UPDATE auth_credentials SET identity_key = ? WHERE id = ?");
1210
+ updateIdentity.run(identityKey, row.id);
1211
+ }
1212
+ } finally {
1213
+ updateIdentity?.finalize();
1214
+ }
1215
+ }
1216
+
1217
+ // ─── AuthCredentialStore interface ──────────────────────────────────────
1218
+
1219
+ listAuthCredentials(provider?: string): StoredAuthCredential[] {
1220
+ const rows =
1221
+ (provider
1222
+ ? (this.#listActiveByProviderStmt.all(provider) as AuthRow[])
1223
+ : (this.#listActiveStmt.all() as AuthRow[])) ?? [];
1224
+
1225
+ const results: StoredAuthCredential[] = [];
1226
+ for (const row of rows) {
1227
+ const credential = deserializeCredential(row);
1228
+ if (!credential) continue;
1229
+ results.push(toStoredAuthCredential(row, credential));
1230
+ }
1231
+ return results;
1232
+ }
1233
+
1234
+ async listDisabledCredentials(provider?: string): Promise<DisabledCredentialSummary[]> {
1235
+ const rows =
1236
+ (provider
1237
+ ? (this.#listDisabledByProviderStmt.all(provider) as DisabledAuthRow[])
1238
+ : (this.#listDisabledStmt.all() as DisabledAuthRow[])) ?? [];
1239
+ const results: DisabledCredentialSummary[] = [];
1240
+ for (const row of rows) {
1241
+ const credential = deserializeCredential(row);
1242
+ const summary: DisabledCredentialSummary = {
1243
+ id: row.id,
1244
+ provider: row.provider,
1245
+ type: row.credential_type === "api_key" ? "api_key" : "oauth",
1246
+ cause: row.disabled_cause ?? "disabled",
1247
+ };
1248
+ if (credential?.type === "oauth") {
1249
+ if (credential.email) summary.email = credential.email;
1250
+ if (credential.accountId) summary.accountId = credential.accountId;
1251
+ if (credential.orgId) summary.orgId = credential.orgId;
1252
+ if (credential.orgName) summary.orgName = credential.orgName;
1253
+ }
1254
+ if (typeof row.updated_at === "number" && Number.isFinite(row.updated_at)) {
1255
+ summary.disabledAtMs = row.updated_at * 1000;
1256
+ }
1257
+ results.push(summary);
1258
+ }
1259
+ return results;
1260
+ }
1261
+
1262
+ replaceAuthCredentialsForProvider(provider: string, credentials: AuthCredential[]): StoredAuthCredential[] {
1263
+ const replace = this.#db.transaction((providerName: string, items: AuthCredential[]) => {
1264
+ const existingRows = this.#listActiveByProviderStmt.all(providerName) as AuthRow[];
1265
+ const existing = existingRows.map(row => ({
1266
+ id: row.id,
1267
+ credential: deserializeCredential(row),
1268
+ identityKey: resolveRowCredentialIdentityKey(providerName, row),
1269
+ }));
1270
+
1271
+ const result: StoredAuthCredential[] = [];
1272
+ const matchedExistingIds = new Set<number>();
1273
+
1274
+ for (const credential of items) {
1275
+ const serialized = serializeCredential(providerName, credential);
1276
+ if (!serialized) continue;
1277
+ const match = existing.find(
1278
+ entry =>
1279
+ !matchedExistingIds.has(entry.id) &&
1280
+ matchesReplacementCredential(providerName, entry.credential, entry.identityKey, credential),
1281
+ );
1282
+ if (match) {
1283
+ matchedExistingIds.add(match.id);
1284
+ this.#updateStmt.run(serialized.credentialType, serialized.data, serialized.identityKey, match.id);
1285
+ result.push({ id: match.id, provider: providerName, credential, disabledCause: null });
1286
+ } else {
1287
+ const row = this.#insertStmt.get(
1288
+ providerName,
1289
+ serialized.credentialType,
1290
+ serialized.data,
1291
+ serialized.identityKey,
1292
+ ) as { id?: number } | undefined;
1293
+ if (row?.id) {
1294
+ result.push({ id: row.id, provider: providerName, credential, disabledCause: null });
1295
+ }
1296
+ }
1297
+ }
1298
+
1299
+ for (const row of existing) {
1300
+ if (!matchedExistingIds.has(row.id)) {
1301
+ this.#deleteStmt.run("replaced by newer credential", row.id);
1302
+ }
1303
+ }
1304
+
1305
+ return result;
1306
+ });
1307
+
1308
+ const result = replace(provider, credentials);
1309
+ this.#purgeSupersededDisabledRows(provider, result);
1310
+ return result;
1311
+ }
1312
+
1313
+ upsertAuthCredentialForProvider(provider: string, credential: AuthCredential): StoredAuthCredential[] {
1314
+ const upsert = this.#db.transaction((providerName: string, item: AuthCredential) => {
1315
+ const serialized = serializeCredential(providerName, item);
1316
+ if (!serialized) return this.listAuthCredentials(providerName);
1317
+ const existingRows = this.#listActiveByProviderStmt.all(providerName) as AuthRow[];
1318
+ const existing = existingRows.map(row => ({
1319
+ id: row.id,
1320
+ credential: deserializeCredential(row),
1321
+ identityKey: resolveRowCredentialIdentityKey(providerName, row),
1322
+ }));
1323
+
1324
+ if (item.type === "oauth") {
1325
+ for (const row of existing) {
1326
+ if (row.credential && row.credential.type === "api_key") {
1327
+ this.#deleteStmt.run("replaced by oauth login", row.id);
1328
+ }
1329
+ }
1330
+ }
1331
+
1332
+ let targetId: number | null = null;
1333
+ for (const row of existing) {
1334
+ if (!matchesReplacementCredential(providerName, row.credential, row.identityKey, item)) continue;
1335
+ if (targetId === null) {
1336
+ targetId = row.id;
1337
+ this.#updateStmt.run(serialized.credentialType, serialized.data, serialized.identityKey, row.id);
1338
+ continue;
1339
+ }
1340
+ this.#deleteStmt.run("replaced by newer credential", row.id);
1341
+ }
1342
+
1343
+ if (targetId === null) {
1344
+ const row = this.#insertStmt.get(
1345
+ providerName,
1346
+ serialized.credentialType,
1347
+ serialized.data,
1348
+ serialized.identityKey,
1349
+ ) as { id?: number } | undefined;
1350
+ targetId = row?.id ?? null;
1351
+ }
1352
+
1353
+ const activeRows = this.#listActiveByProviderStmt.all(providerName) as AuthRow[];
1354
+ const result: StoredAuthCredential[] = [];
1355
+ for (const row of activeRows) {
1356
+ const activeCredential = deserializeCredential(row);
1357
+ if (!activeCredential) continue;
1358
+ result.push(toStoredAuthCredential(row, activeCredential));
1359
+ }
1360
+ return result;
1361
+ });
1362
+
1363
+ const result = upsert(provider, credential);
1364
+ this.#purgeSupersededDisabledRows(provider, result);
1365
+ return result;
1366
+ }
1367
+
1368
+ /**
1369
+ * Hard-deletes disabled rows for a provider when an active replacement exists.
1370
+ * OAuth credentials match by identity key; API keys match by provider and type.
1371
+ * Disabled rows without an active same-type replacement remain recoverable.
1372
+ */
1373
+ #purgeSupersededDisabledRows(provider: string, activeRows: StoredAuthCredential[]): void {
1374
+ try {
1375
+ let hasActiveApiKey = false;
1376
+ const activeIdentityKeys = new Set<string>();
1377
+ const activeOAuthCredentials: AuthCredential[] = [];
1378
+ for (const row of activeRows) {
1379
+ if (row.credential.type === "api_key") {
1380
+ hasActiveApiKey = true;
1381
+ continue;
1382
+ }
1383
+ activeOAuthCredentials.push(row.credential);
1384
+ const identityKey = resolveCredentialIdentityKey(provider, row.credential);
1385
+ if (identityKey) activeIdentityKeys.add(identityKey);
1386
+ }
1387
+ if (!hasActiveApiKey && activeIdentityKeys.size === 0) return;
1388
+
1389
+ const disabledRows = this.#listDisabledByProviderStmt.all(provider) as AuthRow[];
1390
+ for (const row of disabledRows) {
1391
+ if (hasActiveApiKey && row.credential_type === "api_key") {
1392
+ this.#hardDeleteStmt.run(row.id);
1393
+ continue;
1394
+ }
1395
+ const identityKey = resolveRowCredentialIdentityKey(provider, row);
1396
+ if (identityKey && activeIdentityKeys.has(identityKey)) {
1397
+ this.#hardDeleteStmt.run(row.id);
1398
+ continue;
1399
+ }
1400
+ // Exact key equality misses a tombstone whose key predates a format
1401
+ // the active row now uses (pre-org `<b>` vs `<b>|org:<o>`). An active
1402
+ // credential that WOULD have replaced this row had it still been
1403
+ // active supersedes its tombstone too, so mirror the replacement
1404
+ // matcher rather than restating a weaker rule. The one-way upgrade
1405
+ // and shared-workspace guards in matchesReplacementCredential carry
1406
+ // over, so this never over-deletes another member's or subscription's
1407
+ // row.
1408
+ const disabledCredential = deserializeCredential(row);
1409
+ if (disabledCredential === null) continue;
1410
+ const superseded = activeOAuthCredentials.some(active =>
1411
+ matchesReplacementCredential(provider, disabledCredential, identityKey, active),
1412
+ );
1413
+ if (superseded) this.#hardDeleteStmt.run(row.id);
1414
+ }
1415
+ } catch {
1416
+ // Best-effort cleanup; don't let it break the main operation
1417
+ }
1418
+ }
1419
+
1420
+ updateAuthCredential(id: number, credential: AuthCredential): void {
1421
+ try {
1422
+ const providerStmt = this.#db.prepare("SELECT provider FROM auth_credentials WHERE id = ?");
1423
+ let providerRow: { provider?: string } | undefined;
1424
+ try {
1425
+ providerRow = providerStmt.get(id) as { provider?: string } | undefined;
1426
+ } finally {
1427
+ providerStmt.finalize();
1428
+ }
1429
+ const provider = providerRow?.provider ?? "";
1430
+ const serialized = serializeCredential(provider, credential);
1431
+ if (!serialized) return;
1432
+ this.#updateStmt.run(serialized.credentialType, serialized.data, serialized.identityKey, id);
1433
+ if (provider) {
1434
+ this.#purgeSupersededDisabledRows(provider, this.listAuthCredentials(provider));
1435
+ }
1436
+ } catch {
1437
+ // Ignore update failures
1438
+ }
1439
+ }
1440
+
1441
+ tryUpdateAuthCredentialIfMatches(
1442
+ id: number,
1443
+ expectedData: string,
1444
+ credential: AuthCredential,
1445
+ lease?: CredentialRefreshLeaseFence,
1446
+ ): boolean {
1447
+ const providerStmt = this.#db.prepare("SELECT provider FROM auth_credentials WHERE id = ?");
1448
+ let providerRow: { provider?: string } | undefined;
1449
+ try {
1450
+ providerRow = providerStmt.get(id) as { provider?: string } | undefined;
1451
+ } finally {
1452
+ providerStmt.finalize();
1453
+ }
1454
+ const provider = providerRow?.provider ?? "";
1455
+ const serialized = serializeCredential(provider, credential);
1456
+ if (!serialized) return false;
1457
+ const result = lease
1458
+ ? (this.#updateIfMatchesWithLeaseStmt.run(
1459
+ serialized.credentialType,
1460
+ serialized.data,
1461
+ serialized.identityKey,
1462
+ id,
1463
+ expectedData,
1464
+ id,
1465
+ lease.owner,
1466
+ lease.nowMs,
1467
+ ) as { changes: number })
1468
+ : (this.#updateIfMatchesStmt.run(
1469
+ serialized.credentialType,
1470
+ serialized.data,
1471
+ serialized.identityKey,
1472
+ id,
1473
+ expectedData,
1474
+ ) as { changes: number });
1475
+ if (result.changes === 0) return false;
1476
+ if (provider) {
1477
+ this.#purgeSupersededDisabledRows(provider, this.listAuthCredentials(provider));
1478
+ }
1479
+ return true;
1480
+ }
1481
+
1482
+ deleteAuthCredential(id: number, disabledCause: string): void {
1483
+ try {
1484
+ this.#deleteStmt.run(normalizeDisabledCause(disabledCause), id);
1485
+ } catch {
1486
+ // Ignore delete failures
1487
+ }
1488
+ }
1489
+
1490
+ /**
1491
+ * CAS-style disable: only soft-deletes the row when its `data` column still
1492
+ * matches `expectedData` and the row has not already been disabled. Used by
1493
+ * the OAuth refresh-failure path to avoid clobbering a peer that rotated the
1494
+ * row between our pre-check and the disable.
1495
+ */
1496
+ tryDisableAuthCredentialIfMatches(
1497
+ id: number,
1498
+ expectedData: string,
1499
+ disabledCause: string,
1500
+ lease?: CredentialRefreshLeaseFence,
1501
+ ): boolean {
1502
+ const result = lease
1503
+ ? (this.#deleteIfMatchesWithLeaseStmt.run(
1504
+ normalizeDisabledCause(disabledCause),
1505
+ id,
1506
+ expectedData,
1507
+ id,
1508
+ lease.owner,
1509
+ lease.nowMs,
1510
+ ) as { changes: number })
1511
+ : (this.#deleteIfMatchesStmt.run(normalizeDisabledCause(disabledCause), id, expectedData) as {
1512
+ changes: number;
1513
+ });
1514
+ return result.changes > 0;
1515
+ }
1516
+ deleteAuthCredentialsForProvider(provider: string, disabledCause: string): void {
1517
+ try {
1518
+ this.#deleteByProviderStmt.run(normalizeDisabledCause(disabledCause), provider);
1519
+ } catch {
1520
+ // Ignore delete failures
1521
+ }
1522
+ }
1523
+
1524
+ getCache(key: string, options?: { includeExpired?: boolean }): string | null {
1525
+ try {
1526
+ const stmt = options?.includeExpired === true ? this.#getCacheIncludingExpiredStmt : this.#getCacheStmt;
1527
+ const row = stmt.get(key) as { value?: string } | undefined;
1528
+ return row?.value ?? null;
1529
+ } catch {
1530
+ return null;
1531
+ }
1532
+ }
1533
+
1534
+ setCache(key: string, value: string, expiresAtSec: number): void {
1535
+ try {
1536
+ this.#upsertCacheStmt.run(key, value, expiresAtSec);
1537
+ } catch {
1538
+ // Ignore cache set failures
1539
+ }
1540
+ }
1541
+
1542
+ /** Drop all cache rows whose keys start with the supplied prefix. */
1543
+ deleteCachePrefix(prefix: string): void {
1544
+ try {
1545
+ this.#deleteCachePrefixStmt.run(prefix.length, prefix);
1546
+ } catch {
1547
+ // Ignore cache delete failures
1548
+ }
1549
+ }
1550
+
1551
+ cleanExpiredCache(): void {
1552
+ try {
1553
+ this.#deleteExpiredCacheStmt.run();
1554
+ } catch {
1555
+ // Ignore cleanup errors
1556
+ }
1557
+ }
1558
+
1559
+ getCredentialBlock(credentialId: number, providerKey: string, blockScope: string): number | undefined {
1560
+ const nowMs = Date.now();
1561
+ const isCodexBlock = providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY;
1562
+ // Current callers use meter scopes. The physical shared row exists only
1563
+ // for direct SQLite readers from pre-meter releases.
1564
+ if (isCodexBlock && blockScope === LEGACY_CODEX_BLOCK_SCOPE) {
1565
+ return undefined;
1566
+ }
1567
+ if (!isCodexBlock) this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
1568
+ const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope, nowMs) as
1569
+ | { blocked_until_ms?: number; updated_at?: number }
1570
+ | undefined;
1571
+ return typeof row?.blocked_until_ms === "number" ? row.blocked_until_ms : undefined;
1572
+ }
1573
+
1574
+ getCredentialBlockReconcileAfter(credentialId: number, providerKey: string, blockScope: string): number | undefined {
1575
+ const nowMs = Date.now();
1576
+ const isCodexBlock = providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY;
1577
+ if (isCodexBlock && blockScope === LEGACY_CODEX_BLOCK_SCOPE) {
1578
+ return undefined;
1579
+ }
1580
+ if (!isCodexBlock) this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
1581
+ const row = this.#getCredentialBlockStmt.get(credentialId, providerKey, blockScope, nowMs) as
1582
+ | { blocked_until_ms?: number; updated_at?: number }
1583
+ | undefined;
1584
+ if (typeof row?.blocked_until_ms !== "number") return undefined;
1585
+ const memoryReconcileAfter =
1586
+ this.#credentialBlockReconcileAfter.get(`${credentialId}\0${providerKey}\0${blockScope}`) ?? 0;
1587
+ const persistedReconcileAfter =
1588
+ typeof row.updated_at === "number" ? row.updated_at * 1000 + USAGE_REPORT_TTL_MS : 0;
1589
+ const reconcileAfter = Math.max(memoryReconcileAfter, persistedReconcileAfter);
1590
+ return reconcileAfter > nowMs ? Math.min(row.blocked_until_ms, reconcileAfter) : undefined;
1591
+ }
1592
+
1593
+ upsertCredentialBlock(block: StoredCredentialBlock): void {
1594
+ const isLegacyCodexBlock =
1595
+ block.providerKey === LEGACY_CODEX_BLOCK_PROVIDER_KEY && block.blockScope === LEGACY_CODEX_BLOCK_SCOPE;
1596
+ const blockScopes = isLegacyCodexBlock ? CODEX_METER_BLOCK_SCOPES : [block.blockScope];
1597
+ const upsert = this.#db.transaction(() => {
1598
+ for (const blockScope of blockScopes) {
1599
+ this.#upsertCredentialBlockStmt.run(
1600
+ block.credentialId,
1601
+ block.providerKey,
1602
+ blockScope,
1603
+ block.blockedUntilMs,
1604
+ );
1605
+ }
1606
+ });
1607
+ upsert.immediate();
1608
+
1609
+ const reconcileAfterMs = Math.min(block.blockedUntilMs, Date.now() + USAGE_REPORT_TTL_MS);
1610
+ for (const blockScope of blockScopes) {
1611
+ this.#credentialBlockReconcileAfter.set(
1612
+ `${block.credentialId}\0${block.providerKey}\0${blockScope}`,
1613
+ reconcileAfterMs,
1614
+ );
1615
+ }
1616
+ if (isLegacyCodexBlock) {
1617
+ this.#credentialBlockReconcileAfter.delete(
1618
+ `${block.credentialId}\0${block.providerKey}\0${LEGACY_CODEX_BLOCK_SCOPE}`,
1619
+ );
1620
+ }
1621
+ }
1622
+
1623
+ deleteCredentialBlock(credentialId: number, providerKey: string, blockScope: string): void {
1624
+ this.#deleteCredentialBlockStmt.run(credentialId, providerKey, blockScope);
1625
+ this.#credentialBlockReconcileAfter.delete(`${credentialId}\0${providerKey}\0${blockScope}`);
1626
+ }
1627
+
1628
+ deleteCredentialBlocks(credentialId: number): void {
1629
+ this.#deleteCredentialBlocksStmt.run(credentialId);
1630
+ for (const key of this.#credentialBlockReconcileAfter.keys()) {
1631
+ if (key.startsWith(`${credentialId}\0`)) this.#credentialBlockReconcileAfter.delete(key);
1632
+ }
1633
+ }
1634
+
1635
+ cleanExpiredCredentialBlocks(nowMs: number): void {
1636
+ this.#deleteExpiredCredentialBlocksStmt.run(nowMs);
1637
+ for (const [key, reconcileAfterMs] of this.#credentialBlockReconcileAfter) {
1638
+ if (reconcileAfterMs <= nowMs) this.#credentialBlockReconcileAfter.delete(key);
1639
+ }
1640
+ }
1641
+
1642
+ listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[] {
1643
+ if (credentialIds.length === 0) return [];
1644
+ const nowMs = Date.now();
1645
+ this.cleanExpiredCredentialBlocks(nowMs);
1646
+ const seenCredentialIds = new Set<number>();
1647
+ const blocks: StoredCredentialBlock[] = [];
1648
+ for (const credentialId of credentialIds) {
1649
+ if (seenCredentialIds.has(credentialId)) continue;
1650
+ seenCredentialIds.add(credentialId);
1651
+ const rows = this.#listCredentialBlocksByCredentialStmt.all(
1652
+ credentialId,
1653
+ nowMs,
1654
+ LEGACY_CODEX_BLOCK_PROVIDER_KEY,
1655
+ LEGACY_CODEX_BLOCK_SCOPE,
1656
+ ) as CredentialBlockRow[];
1657
+ for (const row of rows) {
1658
+ blocks.push({
1659
+ credentialId: row.credential_id,
1660
+ providerKey: row.provider_key,
1661
+ blockScope: row.block_scope,
1662
+ blockedUntilMs: row.blocked_until_ms,
1663
+ updatedAtMs: row.updated_at * 1000,
1664
+ });
1665
+ }
1666
+ }
1667
+ return blocks;
1668
+ }
1669
+
1670
+ tryAcquireCredentialRefreshLease(credentialId: number, owner: string, expiresAtMs: number): boolean {
1671
+ const result = this.#acquireCredentialRefreshLeaseStmt.run(credentialId, owner, expiresAtMs, Date.now()) as {
1672
+ changes: number;
1673
+ };
1674
+ return result.changes === 1;
1675
+ }
1676
+
1677
+ getCredentialRefreshLeaseExpiresAt(credentialId: number): number | undefined {
1678
+ const row = this.#getCredentialRefreshLeaseStmt.get(credentialId) as { expires_at_ms?: number } | undefined;
1679
+ if (typeof row?.expires_at_ms !== "number") return undefined;
1680
+ if (row.expires_at_ms <= Date.now()) return undefined;
1681
+ return row.expires_at_ms;
1682
+ }
1683
+
1684
+ renewCredentialRefreshLease(credentialId: number, owner: string, expiresAtMs: number): boolean {
1685
+ const result = this.#renewCredentialRefreshLeaseStmt.run(expiresAtMs, credentialId, owner) as {
1686
+ changes: number;
1687
+ };
1688
+ return result.changes === 1;
1689
+ }
1690
+
1691
+ releaseCredentialRefreshLease(credentialId: number, owner: string): void {
1692
+ try {
1693
+ this.#releaseCredentialRefreshLeaseStmt.run(credentialId, owner);
1694
+ } catch {
1695
+ // Ignore lease release failures; expired leases are stealable.
1696
+ }
1697
+ }
1698
+
1699
+ recordUsageSnapshots(entries: UsageHistoryEntry[]): void {
1700
+ try {
1701
+ for (const entry of entries) {
1702
+ const bucket = Math.floor(entry.recordedAt / USAGE_HISTORY_BUCKET_MS);
1703
+ const last = this.#lastUsageHistoryStmt.get(entry.provider, entry.accountKey, entry.limitId) as
1704
+ | { id: number; recorded_at: number }
1705
+ | undefined;
1706
+ if (last && Math.floor(last.recorded_at / USAGE_HISTORY_BUCKET_MS) === bucket) {
1707
+ this.#updateUsageHistoryStmt.run(
1708
+ entry.recordedAt,
1709
+ entry.email ?? null,
1710
+ entry.accountId ?? null,
1711
+ entry.label,
1712
+ entry.windowLabel ?? null,
1713
+ entry.usedFraction ?? null,
1714
+ entry.status ?? null,
1715
+ entry.resetsAt ?? null,
1716
+ last.id,
1717
+ );
1718
+ continue;
1719
+ }
1720
+ this.#insertUsageHistoryStmt.run(
1721
+ entry.recordedAt,
1722
+ entry.provider,
1723
+ entry.accountKey,
1724
+ entry.email ?? null,
1725
+ entry.accountId ?? null,
1726
+ entry.limitId,
1727
+ entry.label,
1728
+ entry.windowLabel ?? null,
1729
+ entry.usedFraction ?? null,
1730
+ entry.status ?? null,
1731
+ entry.resetsAt ?? null,
1732
+ );
1733
+ }
1734
+ } catch {
1735
+ // History is best-effort; never break the usage fetch path.
1736
+ }
1737
+ }
1738
+
1739
+ listUsageHistory(query?: UsageHistoryQuery): UsageHistoryEntry[] {
1740
+ try {
1741
+ const provider = query?.provider ?? null;
1742
+ const rows = this.#listUsageHistoryStmt.all(query?.sinceMs ?? 0, provider, provider) as Array<{
1743
+ recorded_at: number;
1744
+ provider: string;
1745
+ account_key: string;
1746
+ email: string | null;
1747
+ account_id: string | null;
1748
+ limit_id: string;
1749
+ label: string;
1750
+ window_label: string | null;
1751
+ used_fraction: number | null;
1752
+ status: string | null;
1753
+ resets_at: number | null;
1754
+ }>;
1755
+ return rows.map(row => ({
1756
+ recordedAt: row.recorded_at,
1757
+ provider: row.provider as Provider,
1758
+ accountKey: row.account_key,
1759
+ email: row.email ?? undefined,
1760
+ accountId: row.account_id ?? undefined,
1761
+ limitId: row.limit_id,
1762
+ label: row.label,
1763
+ windowLabel: row.window_label ?? undefined,
1764
+ usedFraction: row.used_fraction ?? undefined,
1765
+ status: (row.status ?? undefined) as UsageHistoryEntry["status"],
1766
+ resetsAt: row.resets_at ?? undefined,
1767
+ }));
1768
+ } catch {
1769
+ return [];
1770
+ }
1771
+ }
1772
+ recordUsageCosts(entries: UsageCostHistoryEntry[]): void {
1773
+ try {
1774
+ for (const entry of entries) {
1775
+ this.#insertUsageCostStmt.run(entry.recordedAt, entry.provider, entry.accountKey, entry.costUsd);
1776
+ }
1777
+ } catch {
1778
+ // Cost history is best-effort; never break request persistence.
1779
+ }
1780
+ }
1781
+
1782
+ listUsageCosts(query?: UsageCostHistoryQuery): UsageCostHistoryEntry[] {
1783
+ try {
1784
+ const provider = query?.provider ?? null;
1785
+ const accountKey = query?.accountKey ?? null;
1786
+ const rows = this.#listUsageCostsStmt.all(
1787
+ query?.sinceMs ?? 0,
1788
+ provider,
1789
+ provider,
1790
+ accountKey,
1791
+ accountKey,
1792
+ ) as Array<{
1793
+ recorded_at: number;
1794
+ provider: string;
1795
+ account_key: string;
1796
+ cost_usd: number;
1797
+ }>;
1798
+ return rows.map(row => ({
1799
+ recordedAt: row.recorded_at,
1800
+ provider: row.provider as Provider,
1801
+ accountKey: row.account_key,
1802
+ costUsd: row.cost_usd,
1803
+ }));
1804
+ } catch {
1805
+ return [];
1806
+ }
1807
+ }
1808
+
1809
+ recordClientUsage(report: ClientUsageReport): void {
1810
+ const now = Date.now();
1811
+ this.#db
1812
+ .query(
1813
+ `INSERT INTO clients (install_id, hostname, first_seen, last_seen) VALUES (?, ?, ?, ?)
1814
+ ON CONFLICT(install_id) DO UPDATE SET hostname = COALESCE(excluded.hostname, hostname), last_seen = excluded.last_seen`,
1815
+ )
1816
+ .run(report.installId, report.hostname ?? null, now, now);
1817
+ const findBucket = this.#db.query(
1818
+ `SELECT id FROM client_usage
1819
+ WHERE install_id = ? AND provider = ? AND model = ? AND recorded_at >= ?
1820
+ ORDER BY recorded_at DESC LIMIT 1`,
1821
+ );
1822
+ const merge = this.#db.query(
1823
+ `UPDATE client_usage SET recorded_at = ?, requests = requests + ?, input_tokens = input_tokens + ?,
1824
+ output_tokens = output_tokens + ?, cache_read_tokens = cache_read_tokens + ?,
1825
+ cache_write_tokens = cache_write_tokens + ?, cost_usd = cost_usd + ? WHERE id = ?`,
1826
+ );
1827
+ const insert = this.#db.query(
1828
+ `INSERT INTO client_usage (recorded_at, install_id, provider, model, requests, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_usd)
1829
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1830
+ );
1831
+ for (const entry of report.entries) {
1832
+ // Merge into the newest row of the same (install, provider, model)
1833
+ // bucket so 10s client flushes don't accrete one row apiece forever.
1834
+ const bucketFloor = entry.at - CLIENT_USAGE_BUCKET_MS;
1835
+ const existing = findBucket.get(report.installId, entry.provider, entry.model, bucketFloor) as {
1836
+ id: number;
1837
+ } | null;
1838
+ if (existing) {
1839
+ merge.run(
1840
+ entry.at,
1841
+ entry.requests,
1842
+ entry.inputTokens,
1843
+ entry.outputTokens,
1844
+ entry.cacheReadTokens,
1845
+ entry.cacheWriteTokens,
1846
+ entry.costUsd,
1847
+ existing.id,
1848
+ );
1849
+ continue;
1850
+ }
1851
+ insert.run(
1852
+ entry.at,
1853
+ report.installId,
1854
+ entry.provider,
1855
+ entry.model,
1856
+ entry.requests,
1857
+ entry.inputTokens,
1858
+ entry.outputTokens,
1859
+ entry.cacheReadTokens,
1860
+ entry.cacheWriteTokens,
1861
+ entry.costUsd,
1862
+ );
1863
+ }
1864
+ }
1865
+
1866
+ getClientUsageSummary(sinceMs: number): ClientUsageSummary {
1867
+ const clients = this.#db
1868
+ .query("SELECT install_id, hostname, first_seen, last_seen FROM clients ORDER BY last_seen DESC")
1869
+ .all() as Array<{ install_id: string; hostname: string | null; first_seen: number; last_seen: number }>;
1870
+ const aggregates = this.#db
1871
+ .query(
1872
+ `SELECT install_id, provider, SUM(requests) requests, SUM(input_tokens) input_tokens,
1873
+ SUM(output_tokens) output_tokens, SUM(cache_read_tokens) cache_read_tokens,
1874
+ SUM(cache_write_tokens) cache_write_tokens, SUM(cost_usd) cost_usd
1875
+ FROM client_usage WHERE recorded_at >= ? GROUP BY install_id, provider
1876
+ ORDER BY install_id, SUM(input_tokens + output_tokens + cache_read_tokens + cache_write_tokens) DESC`,
1877
+ )
1878
+ .all(sinceMs) as Array<{
1879
+ install_id: string;
1880
+ provider: string;
1881
+ requests: number;
1882
+ input_tokens: number;
1883
+ output_tokens: number;
1884
+ cache_read_tokens: number;
1885
+ cache_write_tokens: number;
1886
+ cost_usd: number;
1887
+ }>;
1888
+ const providersByInstall = new Map<string, ClientProviderUsage[]>();
1889
+ for (const row of aggregates) {
1890
+ let list = providersByInstall.get(row.install_id);
1891
+ if (!list) {
1892
+ list = [];
1893
+ providersByInstall.set(row.install_id, list);
1894
+ }
1895
+ list.push({
1896
+ provider: row.provider,
1897
+ requests: row.requests,
1898
+ inputTokens: row.input_tokens,
1899
+ outputTokens: row.output_tokens,
1900
+ cacheReadTokens: row.cache_read_tokens,
1901
+ cacheWriteTokens: row.cache_write_tokens,
1902
+ costUsd: row.cost_usd,
1903
+ });
1904
+ }
1905
+ return {
1906
+ clients: clients.map(client => ({
1907
+ installId: client.install_id,
1908
+ hostname: client.hostname ?? undefined,
1909
+ firstSeen: client.first_seen,
1910
+ lastSeen: client.last_seen,
1911
+ providers: providersByInstall.get(client.install_id) ?? [],
1912
+ })),
1913
+ };
1914
+ }
1915
+
1916
+ // ─── Convenience methods for CLI ────────────────────────────────────────
1917
+
1918
+ /**
1919
+ * Save OAuth credentials for a provider.
1920
+ * Preserves unrelated identities and replaces only the matching credential.
1921
+ */
1922
+ saveOAuth(provider: string, credentials: OAuthCredentials): void {
1923
+ const credential: AuthCredential = { type: "oauth", ...credentials };
1924
+ this.upsertAuthCredentialForProvider(provider, credential);
1925
+ }
1926
+
1927
+ /**
1928
+ * Get OAuth credentials for a provider.
1929
+ */
1930
+ getOAuth(provider: string): OAuthCredentials | null {
1931
+ const rows = this.#listActiveByProviderStmt.all(provider) as AuthRow[];
1932
+ for (const row of rows) {
1933
+ const credential = deserializeCredential(row);
1934
+ if (credential && credential.type === "oauth") {
1935
+ const { type: _type, ...oauth } = credential;
1936
+ return oauth as OAuthCredentials;
1937
+ }
1938
+ }
1939
+ return null;
1940
+ }
1941
+
1942
+ /**
1943
+ * Save API key for a provider (replaces existing).
1944
+ */
1945
+ saveApiKey(provider: string, apiKey: string): void {
1946
+ const credential: AuthCredential = { type: "api_key", key: apiKey };
1947
+ this.replaceAuthCredentialsForProvider(provider, [credential]);
1948
+ }
1949
+
1950
+ /**
1951
+ * Get API key for a provider.
1952
+ */
1953
+ getApiKey(provider: string): string | null {
1954
+ const rows = this.#listActiveByProviderStmt.all(provider) as AuthRow[];
1955
+ for (const row of rows) {
1956
+ const credential = deserializeCredential(row);
1957
+ if (credential && credential.type === "api_key") {
1958
+ return credential.key;
1959
+ }
1960
+ }
1961
+ return null;
1962
+ }
1963
+
1964
+ /**
1965
+ * List all providers with credentials.
1966
+ */
1967
+ listProviders(): string[] {
1968
+ const rows = this.#listActiveStmt.all() as AuthRow[];
1969
+ const providers = new Set<string>();
1970
+ for (const row of rows) {
1971
+ providers.add(row.provider);
1972
+ }
1973
+ return Array.from(providers);
1974
+ }
1975
+
1976
+ /**
1977
+ * Delete all credentials for a provider.
1978
+ */
1979
+ deleteProvider(provider: string): void {
1980
+ this.deleteAuthCredentialsForProvider(provider, "deleted by user");
1981
+ }
1982
+
1983
+ /**
1984
+ * SQLite increments `data_version` when another connection commits. Own
1985
+ * writes leave it unchanged and already notify AuthStorage directly.
1986
+ */
1987
+ pollExternalChanges(): boolean {
1988
+ this.#acknowledgeLocalAuthChanges();
1989
+ const dataVersion = this.#readDataVersion();
1990
+ if (dataVersion === this.#dataVersion) return false;
1991
+ this.#dataVersion = dataVersion;
1992
+ const authRevision = this.#readAuthRevision();
1993
+ if (authRevision === this.#authRevision) return false;
1994
+ this.#authRevision = authRevision;
1995
+ return true;
1996
+ }
1997
+
1998
+ acknowledgeLocalChanges(): void {
1999
+ this.#acknowledgeLocalAuthChanges();
2000
+ }
2001
+
2002
+ #acknowledgeLocalAuthChanges(): void {
2003
+ const localAuthRevision = this.#readLocalAuthRevision();
2004
+ this.#authRevision += localAuthRevision - this.#localAuthRevision;
2005
+ this.#localAuthRevision = localAuthRevision;
2006
+ }
2007
+
2008
+ #readDataVersion(): number {
2009
+ const row = this.#db.query("PRAGMA data_version").get() as { data_version?: number } | null;
2010
+ return row?.data_version ?? 0;
2011
+ }
2012
+
2013
+ #readAuthRevision(): number {
2014
+ const row = this.#db.query("SELECT revision FROM auth_change_revision WHERE id = 1").get() as {
2015
+ revision?: number;
2016
+ } | null;
2017
+ return row?.revision ?? 0;
2018
+ }
2019
+
2020
+ #readLocalAuthRevision(): number {
2021
+ const row = this.#db.query("SELECT revision FROM auth_local_change_revision WHERE id = 1").get() as {
2022
+ revision?: number;
2023
+ } | null;
2024
+ return row?.revision ?? 0;
2025
+ }
2026
+
2027
+ close(): void {
2028
+ if (this.#closed) return;
2029
+ this.#closed = true;
2030
+ this.#listActiveStmt.finalize();
2031
+ this.#listActiveByProviderStmt.finalize();
2032
+ this.#listDisabledStmt.finalize();
2033
+ this.#listDisabledByProviderStmt.finalize();
2034
+ this.#insertStmt.finalize();
2035
+ this.#updateStmt.finalize();
2036
+ this.#deleteStmt.finalize();
2037
+ this.#deleteIfMatchesStmt.finalize();
2038
+ this.#deleteByProviderStmt.finalize();
2039
+ this.#hardDeleteStmt.finalize();
2040
+ this.#getCacheStmt.finalize();
2041
+ this.#getCacheIncludingExpiredStmt.finalize();
2042
+ this.#upsertCacheStmt.finalize();
2043
+ this.#deleteExpiredCacheStmt.finalize();
2044
+ this.#getCredentialBlockStmt.finalize();
2045
+ this.#listCredentialBlocksByCredentialStmt.finalize();
2046
+ this.#upsertCredentialBlockStmt.finalize();
2047
+ this.#deleteCredentialBlocksStmt.finalize();
2048
+ this.#deleteCredentialBlockStmt.finalize();
2049
+ this.#deleteExpiredCredentialBlocksStmt.finalize();
2050
+ this.#insertUsageHistoryStmt.finalize();
2051
+ this.#lastUsageHistoryStmt.finalize();
2052
+ this.#listUsageHistoryStmt.finalize();
2053
+ this.#updateUsageHistoryStmt.finalize();
2054
+ this.#insertUsageCostStmt.finalize();
2055
+ this.#listUsageCostsStmt.finalize();
2056
+ this.#updateIfMatchesStmt.finalize();
2057
+ this.#updateIfMatchesWithLeaseStmt.finalize();
2058
+ this.#deleteIfMatchesWithLeaseStmt.finalize();
2059
+ this.#deleteCachePrefixStmt.finalize();
2060
+ this.#acquireCredentialRefreshLeaseStmt.finalize();
2061
+ this.#getCredentialRefreshLeaseStmt.finalize();
2062
+ this.#renewCredentialRefreshLeaseStmt.finalize();
2063
+ this.#releaseCredentialRefreshLeaseStmt.finalize();
2064
+ this.#db.close();
2065
+ }
2066
+ }