@antasphere/chassis-server 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 (798) hide show
  1. package/LICENSE +82 -0
  2. package/dist/accounts/deletion.d.ts +163 -0
  3. package/dist/accounts/deletion.d.ts.map +1 -0
  4. package/dist/accounts/deletion.js +360 -0
  5. package/dist/accounts/deletion.js.map +1 -0
  6. package/dist/accounts/erasure-log.d.ts +66 -0
  7. package/dist/accounts/erasure-log.d.ts.map +1 -0
  8. package/dist/accounts/erasure-log.js +117 -0
  9. package/dist/accounts/erasure-log.js.map +1 -0
  10. package/dist/accounts/index.d.ts +6 -0
  11. package/dist/accounts/index.d.ts.map +1 -0
  12. package/dist/accounts/index.js +6 -0
  13. package/dist/accounts/index.js.map +1 -0
  14. package/dist/accounts/mint-refusal.d.ts +47 -0
  15. package/dist/accounts/mint-refusal.d.ts.map +1 -0
  16. package/dist/accounts/mint-refusal.js +59 -0
  17. package/dist/accounts/mint-refusal.js.map +1 -0
  18. package/dist/accounts/signup-duplicate.d.ts +14 -0
  19. package/dist/accounts/signup-duplicate.d.ts.map +1 -0
  20. package/dist/accounts/signup-duplicate.js +25 -0
  21. package/dist/accounts/signup-duplicate.js.map +1 -0
  22. package/dist/accounts/superadmin.d.ts +27 -0
  23. package/dist/accounts/superadmin.d.ts.map +1 -0
  24. package/dist/accounts/superadmin.js +46 -0
  25. package/dist/accounts/superadmin.js.map +1 -0
  26. package/dist/api/apikeys.d.ts +14 -0
  27. package/dist/api/apikeys.d.ts.map +1 -0
  28. package/dist/api/apikeys.js +124 -0
  29. package/dist/api/apikeys.js.map +1 -0
  30. package/dist/api/audit.d.ts +4 -0
  31. package/dist/api/audit.d.ts.map +1 -0
  32. package/dist/api/audit.js +75 -0
  33. package/dist/api/audit.js.map +1 -0
  34. package/dist/api/break-glass.d.ts +48 -0
  35. package/dist/api/break-glass.d.ts.map +1 -0
  36. package/dist/api/break-glass.js +217 -0
  37. package/dist/api/break-glass.js.map +1 -0
  38. package/dist/api/cli-auth.d.ts +84 -0
  39. package/dist/api/cli-auth.d.ts.map +1 -0
  40. package/dist/api/cli-auth.js +177 -0
  41. package/dist/api/cli-auth.js.map +1 -0
  42. package/dist/api/create-api.d.ts +123 -0
  43. package/dist/api/create-api.d.ts.map +1 -0
  44. package/dist/api/create-api.js +1165 -0
  45. package/dist/api/create-api.js.map +1 -0
  46. package/dist/api/demo-passes.d.ts +45 -0
  47. package/dist/api/demo-passes.d.ts.map +1 -0
  48. package/dist/api/demo-passes.js +143 -0
  49. package/dist/api/demo-passes.js.map +1 -0
  50. package/dist/api/errors.d.ts +5 -0
  51. package/dist/api/errors.d.ts.map +1 -0
  52. package/dist/api/errors.js +5 -0
  53. package/dist/api/errors.js.map +1 -0
  54. package/dist/api/export.d.ts +63 -0
  55. package/dist/api/export.d.ts.map +1 -0
  56. package/dist/api/export.js +393 -0
  57. package/dist/api/export.js.map +1 -0
  58. package/dist/api/files.d.ts +40 -0
  59. package/dist/api/files.d.ts.map +1 -0
  60. package/dist/api/files.js +150 -0
  61. package/dist/api/files.js.map +1 -0
  62. package/dist/api/index.d.ts +17 -0
  63. package/dist/api/index.d.ts.map +1 -0
  64. package/dist/api/index.js +17 -0
  65. package/dist/api/index.js.map +1 -0
  66. package/dist/api/invitations.d.ts +50 -0
  67. package/dist/api/invitations.d.ts.map +1 -0
  68. package/dist/api/invitations.js +313 -0
  69. package/dist/api/invitations.js.map +1 -0
  70. package/dist/api/members.d.ts +25 -0
  71. package/dist/api/members.d.ts.map +1 -0
  72. package/dist/api/members.js +522 -0
  73. package/dist/api/members.js.map +1 -0
  74. package/dist/api/onboarding.d.ts +26 -0
  75. package/dist/api/onboarding.d.ts.map +1 -0
  76. package/dist/api/onboarding.js +23 -0
  77. package/dist/api/onboarding.js.map +1 -0
  78. package/dist/api/openapi-doc.d.ts +21 -0
  79. package/dist/api/openapi-doc.d.ts.map +1 -0
  80. package/dist/api/openapi-doc.js +25 -0
  81. package/dist/api/openapi-doc.js.map +1 -0
  82. package/dist/api/projects.d.ts +24 -0
  83. package/dist/api/projects.d.ts.map +1 -0
  84. package/dist/api/projects.js +624 -0
  85. package/dist/api/projects.js.map +1 -0
  86. package/dist/api/scope-routes.d.ts +12 -0
  87. package/dist/api/scope-routes.d.ts.map +1 -0
  88. package/dist/api/scope-routes.js +2 -0
  89. package/dist/api/scope-routes.js.map +1 -0
  90. package/dist/api/sso-connect.d.ts +70 -0
  91. package/dist/api/sso-connect.d.ts.map +1 -0
  92. package/dist/api/sso-connect.js +164 -0
  93. package/dist/api/sso-connect.js.map +1 -0
  94. package/dist/api/sso-logout.d.ts +41 -0
  95. package/dist/api/sso-logout.d.ts.map +1 -0
  96. package/dist/api/sso-logout.js +34 -0
  97. package/dist/api/sso-logout.js.map +1 -0
  98. package/dist/api/teams.d.ts +43 -0
  99. package/dist/api/teams.d.ts.map +1 -0
  100. package/dist/api/teams.js +447 -0
  101. package/dist/api/teams.js.map +1 -0
  102. package/dist/api/workspaces.d.ts +114 -0
  103. package/dist/api/workspaces.d.ts.map +1 -0
  104. package/dist/api/workspaces.js +462 -0
  105. package/dist/api/workspaces.js.map +1 -0
  106. package/dist/apikeys/index.d.ts +3 -0
  107. package/dist/apikeys/index.d.ts.map +1 -0
  108. package/dist/apikeys/index.js +3 -0
  109. package/dist/apikeys/index.js.map +1 -0
  110. package/dist/apikeys/peppers.d.ts +53 -0
  111. package/dist/apikeys/peppers.d.ts.map +1 -0
  112. package/dist/apikeys/peppers.js +97 -0
  113. package/dist/apikeys/peppers.js.map +1 -0
  114. package/dist/apikeys/service.d.ts +56 -0
  115. package/dist/apikeys/service.d.ts.map +1 -0
  116. package/dist/apikeys/service.js +196 -0
  117. package/dist/apikeys/service.js.map +1 -0
  118. package/dist/app.d.ts +65 -0
  119. package/dist/app.d.ts.map +1 -0
  120. package/dist/app.js +142 -0
  121. package/dist/app.js.map +1 -0
  122. package/dist/audit/filters.d.ts +38 -0
  123. package/dist/audit/filters.d.ts.map +1 -0
  124. package/dist/audit/filters.js +97 -0
  125. package/dist/audit/filters.js.map +1 -0
  126. package/dist/audit/index.d.ts +3 -0
  127. package/dist/audit/index.d.ts.map +1 -0
  128. package/dist/audit/index.js +3 -0
  129. package/dist/audit/index.js.map +1 -0
  130. package/dist/audit/service.d.ts +65 -0
  131. package/dist/audit/service.d.ts.map +1 -0
  132. package/dist/audit/service.js +103 -0
  133. package/dist/audit/service.js.map +1 -0
  134. package/dist/boot.d.ts +8 -0
  135. package/dist/boot.d.ts.map +1 -0
  136. package/dist/boot.js +822 -0
  137. package/dist/boot.js.map +1 -0
  138. package/dist/constant-time.d.ts +3 -0
  139. package/dist/constant-time.d.ts.map +1 -0
  140. package/dist/constant-time.js +10 -0
  141. package/dist/constant-time.js.map +1 -0
  142. package/dist/email/driver.d.ts +16 -0
  143. package/dist/email/driver.d.ts.map +1 -0
  144. package/dist/email/driver.js +136 -0
  145. package/dist/email/driver.js.map +1 -0
  146. package/dist/email/index.d.ts +4 -0
  147. package/dist/email/index.d.ts.map +1 -0
  148. package/dist/email/index.js +4 -0
  149. package/dist/email/index.js.map +1 -0
  150. package/dist/email/shell.d.ts +72 -0
  151. package/dist/email/shell.d.ts.map +1 -0
  152. package/dist/email/shell.js +146 -0
  153. package/dist/email/shell.js.map +1 -0
  154. package/dist/email/templates.d.ts +83 -0
  155. package/dist/email/templates.d.ts.map +1 -0
  156. package/dist/email/templates.js +96 -0
  157. package/dist/email/templates.js.map +1 -0
  158. package/dist/entitlements/check.d.ts +140 -0
  159. package/dist/entitlements/check.d.ts.map +1 -0
  160. package/dist/entitlements/check.js +276 -0
  161. package/dist/entitlements/check.js.map +1 -0
  162. package/dist/entitlements/clock-skew.d.ts +27 -0
  163. package/dist/entitlements/clock-skew.d.ts.map +1 -0
  164. package/dist/entitlements/clock-skew.js +58 -0
  165. package/dist/entitlements/clock-skew.js.map +1 -0
  166. package/dist/entitlements/gate.d.ts +113 -0
  167. package/dist/entitlements/gate.d.ts.map +1 -0
  168. package/dist/entitlements/gate.js +430 -0
  169. package/dist/entitlements/gate.js.map +1 -0
  170. package/dist/entitlements/hub-machine-token.d.ts +83 -0
  171. package/dist/entitlements/hub-machine-token.d.ts.map +1 -0
  172. package/dist/entitlements/hub-machine-token.js +175 -0
  173. package/dist/entitlements/hub-machine-token.js.map +1 -0
  174. package/dist/entitlements/hub-subject.d.ts +32 -0
  175. package/dist/entitlements/hub-subject.d.ts.map +1 -0
  176. package/dist/entitlements/hub-subject.js +68 -0
  177. package/dist/entitlements/hub-subject.js.map +1 -0
  178. package/dist/entitlements/index.d.ts +9 -0
  179. package/dist/entitlements/index.d.ts.map +1 -0
  180. package/dist/entitlements/index.js +9 -0
  181. package/dist/entitlements/index.js.map +1 -0
  182. package/dist/entitlements/poster.d.ts +81 -0
  183. package/dist/entitlements/poster.d.ts.map +1 -0
  184. package/dist/entitlements/poster.js +169 -0
  185. package/dist/entitlements/poster.js.map +1 -0
  186. package/dist/entitlements/profiles.d.ts +121 -0
  187. package/dist/entitlements/profiles.d.ts.map +1 -0
  188. package/dist/entitlements/profiles.js +212 -0
  189. package/dist/entitlements/profiles.js.map +1 -0
  190. package/dist/entitlements/slot.d.ts +33 -0
  191. package/dist/entitlements/slot.d.ts.map +1 -0
  192. package/dist/entitlements/slot.js +75 -0
  193. package/dist/entitlements/slot.js.map +1 -0
  194. package/dist/env.d.ts +293 -0
  195. package/dist/env.d.ts.map +1 -0
  196. package/dist/env.js +383 -0
  197. package/dist/env.js.map +1 -0
  198. package/dist/files/http.d.ts +23 -0
  199. package/dist/files/http.d.ts.map +1 -0
  200. package/dist/files/http.js +66 -0
  201. package/dist/files/http.js.map +1 -0
  202. package/dist/files/index.d.ts +5 -0
  203. package/dist/files/index.d.ts.map +1 -0
  204. package/dist/files/index.js +5 -0
  205. package/dist/files/index.js.map +1 -0
  206. package/dist/files/serve.d.ts +43 -0
  207. package/dist/files/serve.d.ts.map +1 -0
  208. package/dist/files/serve.js +71 -0
  209. package/dist/files/serve.js.map +1 -0
  210. package/dist/files/service.d.ts +69 -0
  211. package/dist/files/service.d.ts.map +1 -0
  212. package/dist/files/service.js +188 -0
  213. package/dist/files/service.js.map +1 -0
  214. package/dist/files/spool.d.ts +21 -0
  215. package/dist/files/spool.d.ts.map +1 -0
  216. package/dist/files/spool.js +47 -0
  217. package/dist/files/spool.js.map +1 -0
  218. package/dist/identity/better-auth.d.ts +3927 -0
  219. package/dist/identity/better-auth.d.ts.map +1 -0
  220. package/dist/identity/better-auth.js +896 -0
  221. package/dist/identity/better-auth.js.map +1 -0
  222. package/dist/identity/demo-pass-plugin.d.ts +29 -0
  223. package/dist/identity/demo-pass-plugin.d.ts.map +1 -0
  224. package/dist/identity/demo-pass-plugin.js +88 -0
  225. package/dist/identity/demo-pass-plugin.js.map +1 -0
  226. package/dist/identity/demo-pass-rules.d.ts +84 -0
  227. package/dist/identity/demo-pass-rules.d.ts.map +1 -0
  228. package/dist/identity/demo-pass-rules.js +223 -0
  229. package/dist/identity/demo-pass-rules.js.map +1 -0
  230. package/dist/identity/demo-pass.d.ts +159 -0
  231. package/dist/identity/demo-pass.d.ts.map +1 -0
  232. package/dist/identity/demo-pass.js +319 -0
  233. package/dist/identity/demo-pass.js.map +1 -0
  234. package/dist/identity/hub-grant.d.ts +182 -0
  235. package/dist/identity/hub-grant.d.ts.map +1 -0
  236. package/dist/identity/hub-grant.js +594 -0
  237. package/dist/identity/hub-grant.js.map +1 -0
  238. package/dist/identity/hub-jwt.d.ts +22 -0
  239. package/dist/identity/hub-jwt.d.ts.map +1 -0
  240. package/dist/identity/hub-jwt.js +77 -0
  241. package/dist/identity/hub-jwt.js.map +1 -0
  242. package/dist/identity/hub-live-gate.d.ts +17 -0
  243. package/dist/identity/hub-live-gate.d.ts.map +1 -0
  244. package/dist/identity/hub-live-gate.js +146 -0
  245. package/dist/identity/hub-live-gate.js.map +1 -0
  246. package/dist/identity/hub-logout.d.ts +88 -0
  247. package/dist/identity/hub-logout.d.ts.map +1 -0
  248. package/dist/identity/hub-logout.js +158 -0
  249. package/dist/identity/hub-logout.js.map +1 -0
  250. package/dist/identity/hub-projection.d.ts +103 -0
  251. package/dist/identity/hub-projection.d.ts.map +1 -0
  252. package/dist/identity/hub-projection.js +130 -0
  253. package/dist/identity/hub-projection.js.map +1 -0
  254. package/dist/identity/hub-reconcile.d.ts +151 -0
  255. package/dist/identity/hub-reconcile.d.ts.map +1 -0
  256. package/dist/identity/hub-reconcile.js +400 -0
  257. package/dist/identity/hub-reconcile.js.map +1 -0
  258. package/dist/identity/hub-sso.d.ts +363 -0
  259. package/dist/identity/hub-sso.d.ts.map +1 -0
  260. package/dist/identity/hub-sso.js +536 -0
  261. package/dist/identity/hub-sso.js.map +1 -0
  262. package/dist/identity/hub-user-client.d.ts +222 -0
  263. package/dist/identity/hub-user-client.d.ts.map +1 -0
  264. package/dist/identity/hub-user-client.js +406 -0
  265. package/dist/identity/hub-user-client.js.map +1 -0
  266. package/dist/identity/index.d.ts +13 -0
  267. package/dist/identity/index.d.ts.map +1 -0
  268. package/dist/identity/index.js +13 -0
  269. package/dist/identity/index.js.map +1 -0
  270. package/dist/identity/oauth-jwt.d.ts +32 -0
  271. package/dist/identity/oauth-jwt.d.ts.map +1 -0
  272. package/dist/identity/oauth-jwt.js +122 -0
  273. package/dist/identity/oauth-jwt.js.map +1 -0
  274. package/dist/identity/resolve-membership.d.ts +28 -0
  275. package/dist/identity/resolve-membership.d.ts.map +1 -0
  276. package/dist/identity/resolve-membership.js +69 -0
  277. package/dist/identity/resolve-membership.js.map +1 -0
  278. package/dist/identity/signing-key.d.ts +49 -0
  279. package/dist/identity/signing-key.d.ts.map +1 -0
  280. package/dist/identity/signing-key.js +144 -0
  281. package/dist/identity/signing-key.js.map +1 -0
  282. package/dist/index.d.ts +21 -0
  283. package/dist/index.d.ts.map +1 -0
  284. package/dist/index.js +11 -0
  285. package/dist/index.js.map +1 -0
  286. package/dist/invitations/index.d.ts +2 -0
  287. package/dist/invitations/index.d.ts.map +1 -0
  288. package/dist/invitations/index.js +2 -0
  289. package/dist/invitations/index.js.map +1 -0
  290. package/dist/invitations/service.d.ts +36 -0
  291. package/dist/invitations/service.d.ts.map +1 -0
  292. package/dist/invitations/service.js +122 -0
  293. package/dist/invitations/service.js.map +1 -0
  294. package/dist/jobs/index.d.ts +3 -0
  295. package/dist/jobs/index.d.ts.map +1 -0
  296. package/dist/jobs/index.js +3 -0
  297. package/dist/jobs/index.js.map +1 -0
  298. package/dist/jobs/pgboss.d.ts +119 -0
  299. package/dist/jobs/pgboss.d.ts.map +1 -0
  300. package/dist/jobs/pgboss.js +521 -0
  301. package/dist/jobs/pgboss.js.map +1 -0
  302. package/dist/jobs/timers.d.ts +127 -0
  303. package/dist/jobs/timers.d.ts.map +1 -0
  304. package/dist/jobs/timers.js +228 -0
  305. package/dist/jobs/timers.js.map +1 -0
  306. package/dist/logger.d.ts +17 -0
  307. package/dist/logger.d.ts.map +1 -0
  308. package/dist/logger.js +64 -0
  309. package/dist/logger.js.map +1 -0
  310. package/dist/mcp/errors.d.ts +37 -0
  311. package/dist/mcp/errors.d.ts.map +1 -0
  312. package/dist/mcp/errors.js +77 -0
  313. package/dist/mcp/errors.js.map +1 -0
  314. package/dist/mcp/http.d.ts +35 -0
  315. package/dist/mcp/http.d.ts.map +1 -0
  316. package/dist/mcp/http.js +80 -0
  317. package/dist/mcp/http.js.map +1 -0
  318. package/dist/mcp/index.d.ts +8 -0
  319. package/dist/mcp/index.d.ts.map +1 -0
  320. package/dist/mcp/index.js +8 -0
  321. package/dist/mcp/index.js.map +1 -0
  322. package/dist/mcp/inputs.d.ts +19 -0
  323. package/dist/mcp/inputs.d.ts.map +1 -0
  324. package/dist/mcp/inputs.js +28 -0
  325. package/dist/mcp/inputs.js.map +1 -0
  326. package/dist/mcp/projects.d.ts +31 -0
  327. package/dist/mcp/projects.d.ts.map +1 -0
  328. package/dist/mcp/projects.js +315 -0
  329. package/dist/mcp/projects.js.map +1 -0
  330. package/dist/mcp/server.d.ts +52 -0
  331. package/dist/mcp/server.d.ts.map +1 -0
  332. package/dist/mcp/server.js +93 -0
  333. package/dist/mcp/server.js.map +1 -0
  334. package/dist/mcp/teams.d.ts +7 -0
  335. package/dist/mcp/teams.d.ts.map +1 -0
  336. package/dist/mcp/teams.js +62 -0
  337. package/dist/mcp/teams.js.map +1 -0
  338. package/dist/mcp/tool-kit.d.ts +67 -0
  339. package/dist/mcp/tool-kit.d.ts.map +1 -0
  340. package/dist/mcp/tool-kit.js +74 -0
  341. package/dist/mcp/tool-kit.js.map +1 -0
  342. package/dist/members/removal.d.ts +95 -0
  343. package/dist/members/removal.d.ts.map +1 -0
  344. package/dist/members/removal.js +143 -0
  345. package/dist/members/removal.js.map +1 -0
  346. package/dist/middleware/auth-body.d.ts +25 -0
  347. package/dist/middleware/auth-body.d.ts.map +1 -0
  348. package/dist/middleware/auth-body.js +55 -0
  349. package/dist/middleware/auth-body.js.map +1 -0
  350. package/dist/middleware/auth-context.d.ts +170 -0
  351. package/dist/middleware/auth-context.d.ts.map +1 -0
  352. package/dist/middleware/auth-context.js +251 -0
  353. package/dist/middleware/auth-context.js.map +1 -0
  354. package/dist/middleware/body-cap.d.ts +17 -0
  355. package/dist/middleware/body-cap.d.ts.map +1 -0
  356. package/dist/middleware/body-cap.js +31 -0
  357. package/dist/middleware/body-cap.js.map +1 -0
  358. package/dist/middleware/body-refusal.d.ts +20 -0
  359. package/dist/middleware/body-refusal.d.ts.map +1 -0
  360. package/dist/middleware/body-refusal.js +5 -0
  361. package/dist/middleware/body-refusal.js.map +1 -0
  362. package/dist/middleware/cross-site.d.ts +61 -0
  363. package/dist/middleware/cross-site.d.ts.map +1 -0
  364. package/dist/middleware/cross-site.js +59 -0
  365. package/dist/middleware/cross-site.js.map +1 -0
  366. package/dist/middleware/hub-managed.d.ts +45 -0
  367. package/dist/middleware/hub-managed.d.ts.map +1 -0
  368. package/dist/middleware/hub-managed.js +56 -0
  369. package/dist/middleware/hub-managed.js.map +1 -0
  370. package/dist/middleware/idempotency.d.ts +14 -0
  371. package/dist/middleware/idempotency.d.ts.map +1 -0
  372. package/dist/middleware/idempotency.js +222 -0
  373. package/dist/middleware/idempotency.js.map +1 -0
  374. package/dist/middleware/index.d.ts +12 -0
  375. package/dist/middleware/index.d.ts.map +1 -0
  376. package/dist/middleware/index.js +12 -0
  377. package/dist/middleware/index.js.map +1 -0
  378. package/dist/middleware/json-depth.d.ts +32 -0
  379. package/dist/middleware/json-depth.d.ts.map +1 -0
  380. package/dist/middleware/json-depth.js +80 -0
  381. package/dist/middleware/json-depth.js.map +1 -0
  382. package/dist/middleware/no-store.d.ts +21 -0
  383. package/dist/middleware/no-store.d.ts.map +1 -0
  384. package/dist/middleware/no-store.js +32 -0
  385. package/dist/middleware/no-store.js.map +1 -0
  386. package/dist/middleware/oauth-public.d.ts +16 -0
  387. package/dist/middleware/oauth-public.d.ts.map +1 -0
  388. package/dist/middleware/oauth-public.js +50 -0
  389. package/dist/middleware/oauth-public.js.map +1 -0
  390. package/dist/middleware/rate-limit.d.ts +182 -0
  391. package/dist/middleware/rate-limit.d.ts.map +1 -0
  392. package/dist/middleware/rate-limit.js +277 -0
  393. package/dist/middleware/rate-limit.js.map +1 -0
  394. package/dist/middleware/request-id.d.ts +10 -0
  395. package/dist/middleware/request-id.d.ts.map +1 -0
  396. package/dist/middleware/request-id.js +42 -0
  397. package/dist/middleware/request-id.js.map +1 -0
  398. package/dist/middleware/scopes.d.ts +33 -0
  399. package/dist/middleware/scopes.d.ts.map +1 -0
  400. package/dist/middleware/scopes.js +139 -0
  401. package/dist/middleware/scopes.js.map +1 -0
  402. package/dist/middleware/security-headers.d.ts +53 -0
  403. package/dist/middleware/security-headers.d.ts.map +1 -0
  404. package/dist/middleware/security-headers.js +96 -0
  405. package/dist/middleware/security-headers.js.map +1 -0
  406. package/dist/observability/index.d.ts +3 -0
  407. package/dist/observability/index.d.ts.map +1 -0
  408. package/dist/observability/index.js +3 -0
  409. package/dist/observability/index.js.map +1 -0
  410. package/dist/observability/metrics.d.ts +17 -0
  411. package/dist/observability/metrics.d.ts.map +1 -0
  412. package/dist/observability/metrics.js +82 -0
  413. package/dist/observability/metrics.js.map +1 -0
  414. package/dist/observability/otel.d.ts +20 -0
  415. package/dist/observability/otel.d.ts.map +1 -0
  416. package/dist/observability/otel.js +54 -0
  417. package/dist/observability/otel.js.map +1 -0
  418. package/dist/pagination.d.ts +57 -0
  419. package/dist/pagination.d.ts.map +1 -0
  420. package/dist/pagination.js +90 -0
  421. package/dist/pagination.js.map +1 -0
  422. package/dist/platform/edition.d.ts +57 -0
  423. package/dist/platform/edition.d.ts.map +1 -0
  424. package/dist/platform/edition.js +90 -0
  425. package/dist/platform/edition.js.map +1 -0
  426. package/dist/platform/entitlements.d.ts +26 -0
  427. package/dist/platform/entitlements.d.ts.map +1 -0
  428. package/dist/platform/entitlements.js +28 -0
  429. package/dist/platform/entitlements.js.map +1 -0
  430. package/dist/platform/events.d.ts +87 -0
  431. package/dist/platform/events.d.ts.map +1 -0
  432. package/dist/platform/events.js +32 -0
  433. package/dist/platform/events.js.map +1 -0
  434. package/dist/platform/index.d.ts +8 -0
  435. package/dist/platform/index.d.ts.map +1 -0
  436. package/dist/platform/index.js +8 -0
  437. package/dist/platform/index.js.map +1 -0
  438. package/dist/platform/local-identity.d.ts +39 -0
  439. package/dist/platform/local-identity.d.ts.map +1 -0
  440. package/dist/platform/local-identity.js +87 -0
  441. package/dist/platform/local-identity.js.map +1 -0
  442. package/dist/platform/registry.d.ts +23 -0
  443. package/dist/platform/registry.d.ts.map +1 -0
  444. package/dist/platform/registry.js +5 -0
  445. package/dist/platform/registry.js.map +1 -0
  446. package/dist/platform/usage.d.ts +11 -0
  447. package/dist/platform/usage.d.ts.map +1 -0
  448. package/dist/platform/usage.js +12 -0
  449. package/dist/platform/usage.js.map +1 -0
  450. package/dist/platform/workspaces.d.ts +25 -0
  451. package/dist/platform/workspaces.d.ts.map +1 -0
  452. package/dist/platform/workspaces.js +44 -0
  453. package/dist/platform/workspaces.js.map +1 -0
  454. package/dist/projects/access.d.ts +40 -0
  455. package/dist/projects/access.d.ts.map +1 -0
  456. package/dist/projects/access.js +133 -0
  457. package/dist/projects/access.js.map +1 -0
  458. package/dist/projects/index.d.ts +2 -0
  459. package/dist/projects/index.d.ts.map +1 -0
  460. package/dist/projects/index.js +2 -0
  461. package/dist/projects/index.js.map +1 -0
  462. package/dist/route-label.d.ts +23 -0
  463. package/dist/route-label.d.ts.map +1 -0
  464. package/dist/route-label.js +29 -0
  465. package/dist/route-label.js.map +1 -0
  466. package/dist/routes/health.d.ts +30 -0
  467. package/dist/routes/health.d.ts.map +1 -0
  468. package/dist/routes/health.js +54 -0
  469. package/dist/routes/health.js.map +1 -0
  470. package/dist/routes/index.d.ts +3 -0
  471. package/dist/routes/index.d.ts.map +1 -0
  472. package/dist/routes/index.js +3 -0
  473. package/dist/routes/index.js.map +1 -0
  474. package/dist/routes/wellknown.d.ts +11 -0
  475. package/dist/routes/wellknown.d.ts.map +1 -0
  476. package/dist/routes/wellknown.js +45 -0
  477. package/dist/routes/wellknown.js.map +1 -0
  478. package/dist/secret.d.ts +30 -0
  479. package/dist/secret.d.ts.map +1 -0
  480. package/dist/secret.js +97 -0
  481. package/dist/secret.js.map +1 -0
  482. package/dist/setup-transport.d.ts +23 -0
  483. package/dist/setup-transport.d.ts.map +1 -0
  484. package/dist/setup-transport.js +52 -0
  485. package/dist/setup-transport.js.map +1 -0
  486. package/dist/state.d.ts +21 -0
  487. package/dist/state.d.ts.map +1 -0
  488. package/dist/state.js +4 -0
  489. package/dist/state.js.map +1 -0
  490. package/dist/storage/driver.d.ts +38 -0
  491. package/dist/storage/driver.d.ts.map +1 -0
  492. package/dist/storage/driver.js +14 -0
  493. package/dist/storage/driver.js.map +1 -0
  494. package/dist/storage/factory.d.ts +4 -0
  495. package/dist/storage/factory.d.ts.map +1 -0
  496. package/dist/storage/factory.js +20 -0
  497. package/dist/storage/factory.js.map +1 -0
  498. package/dist/storage/index.d.ts +4 -0
  499. package/dist/storage/index.d.ts.map +1 -0
  500. package/dist/storage/index.js +4 -0
  501. package/dist/storage/index.js.map +1 -0
  502. package/dist/storage/local.d.ts +21 -0
  503. package/dist/storage/local.d.ts.map +1 -0
  504. package/dist/storage/local.js +88 -0
  505. package/dist/storage/local.js.map +1 -0
  506. package/dist/storage/s3.d.ts +29 -0
  507. package/dist/storage/s3.d.ts.map +1 -0
  508. package/dist/storage/s3.js +82 -0
  509. package/dist/storage/s3.js.map +1 -0
  510. package/dist/testing/fake-hub.d.ts +425 -0
  511. package/dist/testing/fake-hub.d.ts.map +1 -0
  512. package/dist/testing/fake-hub.js +1074 -0
  513. package/dist/testing/fake-hub.js.map +1 -0
  514. package/dist/testing/helpers.d.ts +124 -0
  515. package/dist/testing/helpers.d.ts.map +1 -0
  516. package/dist/testing/helpers.js +174 -0
  517. package/dist/testing/helpers.js.map +1 -0
  518. package/dist/testing/index.d.ts +12 -0
  519. package/dist/testing/index.d.ts.map +1 -0
  520. package/dist/testing/index.js +12 -0
  521. package/dist/testing/index.js.map +1 -0
  522. package/dist/testing/setup-origins.d.ts +21 -0
  523. package/dist/testing/setup-origins.d.ts.map +1 -0
  524. package/dist/testing/setup-origins.js +57 -0
  525. package/dist/testing/setup-origins.js.map +1 -0
  526. package/dist/testing/sso-helpers.d.ts +43 -0
  527. package/dist/testing/sso-helpers.d.ts.map +1 -0
  528. package/dist/testing/sso-helpers.js +159 -0
  529. package/dist/testing/sso-helpers.js.map +1 -0
  530. package/dist/tool-definition.d.ts +382 -0
  531. package/dist/tool-definition.d.ts.map +1 -0
  532. package/dist/tool-definition.js +46 -0
  533. package/dist/tool-definition.js.map +1 -0
  534. package/dist/util/index.d.ts +8 -0
  535. package/dist/util/index.d.ts.map +1 -0
  536. package/dist/util/index.js +8 -0
  537. package/dist/util/index.js.map +1 -0
  538. package/package.json +161 -0
  539. package/src/accounts/deletion.ts +389 -0
  540. package/src/accounts/erasure-log.ts +140 -0
  541. package/src/accounts/index.ts +5 -0
  542. package/src/accounts/mint-refusal.ts +67 -0
  543. package/src/accounts/signup-duplicate.ts +21 -0
  544. package/src/accounts/superadmin.ts +46 -0
  545. package/src/api/apikeys.ts +147 -0
  546. package/src/api/audit.ts +83 -0
  547. package/src/api/break-glass.ts +294 -0
  548. package/src/api/cli-auth.ts +308 -0
  549. package/src/api/create-api.ts +1403 -0
  550. package/src/api/demo-passes.ts +207 -0
  551. package/src/api/errors.ts +13 -0
  552. package/src/api/export.ts +482 -0
  553. package/src/api/files.ts +200 -0
  554. package/src/api/index.ts +29 -0
  555. package/src/api/invitations.ts +396 -0
  556. package/src/api/members.ts +617 -0
  557. package/src/api/onboarding.ts +50 -0
  558. package/src/api/openapi-doc.ts +34 -0
  559. package/src/api/projects.ts +788 -0
  560. package/src/api/scope-routes.ts +12 -0
  561. package/src/api/sso-connect.ts +246 -0
  562. package/src/api/sso-logout.ts +76 -0
  563. package/src/api/teams.ts +575 -0
  564. package/src/api/workspaces.ts +649 -0
  565. package/src/apikeys/index.ts +2 -0
  566. package/src/apikeys/peppers.ts +118 -0
  567. package/src/apikeys/service.ts +223 -0
  568. package/src/app.ts +228 -0
  569. package/src/audit/filters.ts +101 -0
  570. package/src/audit/index.ts +9 -0
  571. package/src/audit/service.ts +154 -0
  572. package/src/boot.ts +953 -0
  573. package/src/constant-time.ts +9 -0
  574. package/src/email/driver.ts +139 -0
  575. package/src/email/index.ts +26 -0
  576. package/src/email/shell.ts +201 -0
  577. package/src/email/templates.ts +184 -0
  578. package/src/entitlements/check.ts +435 -0
  579. package/src/entitlements/clock-skew.ts +58 -0
  580. package/src/entitlements/gate.ts +636 -0
  581. package/src/entitlements/hub-machine-token.ts +205 -0
  582. package/src/entitlements/hub-subject.ts +77 -0
  583. package/src/entitlements/index.ts +37 -0
  584. package/src/entitlements/poster.ts +265 -0
  585. package/src/entitlements/profiles.ts +303 -0
  586. package/src/entitlements/slot.ts +105 -0
  587. package/src/env.ts +456 -0
  588. package/src/files/http.ts +77 -0
  589. package/src/files/index.ts +4 -0
  590. package/src/files/serve.ts +115 -0
  591. package/src/files/service.ts +209 -0
  592. package/src/files/spool.ts +58 -0
  593. package/src/identity/better-auth.ts +1103 -0
  594. package/src/identity/demo-pass-plugin.ts +121 -0
  595. package/src/identity/demo-pass-rules.ts +229 -0
  596. package/src/identity/demo-pass.ts +390 -0
  597. package/src/identity/hub-grant.ts +704 -0
  598. package/src/identity/hub-jwt.ts +78 -0
  599. package/src/identity/hub-live-gate.ts +167 -0
  600. package/src/identity/hub-logout.ts +174 -0
  601. package/src/identity/hub-projection.ts +251 -0
  602. package/src/identity/hub-reconcile.ts +572 -0
  603. package/src/identity/hub-sso.ts +696 -0
  604. package/src/identity/hub-user-client.ts +554 -0
  605. package/src/identity/index.ts +56 -0
  606. package/src/identity/oauth-jwt.ts +128 -0
  607. package/src/identity/resolve-membership.ts +104 -0
  608. package/src/identity/signing-key.ts +164 -0
  609. package/src/index.ts +65 -0
  610. package/src/invitations/index.ts +1 -0
  611. package/src/invitations/service.ts +171 -0
  612. package/src/jobs/index.ts +25 -0
  613. package/src/jobs/pgboss.ts +594 -0
  614. package/src/jobs/timers.ts +287 -0
  615. package/src/logger.ts +67 -0
  616. package/src/mcp/errors.ts +111 -0
  617. package/src/mcp/http.ts +129 -0
  618. package/src/mcp/index.ts +28 -0
  619. package/src/mcp/inputs.ts +33 -0
  620. package/src/mcp/projects.ts +480 -0
  621. package/src/mcp/server.ts +174 -0
  622. package/src/mcp/teams.ts +97 -0
  623. package/src/mcp/tool-kit.ts +148 -0
  624. package/src/members/removal.ts +216 -0
  625. package/src/middleware/auth-body.ts +58 -0
  626. package/src/middleware/auth-context.ts +384 -0
  627. package/src/middleware/body-cap.ts +33 -0
  628. package/src/middleware/body-refusal.ts +23 -0
  629. package/src/middleware/cross-site.ts +122 -0
  630. package/src/middleware/hub-managed.ts +61 -0
  631. package/src/middleware/idempotency.ts +264 -0
  632. package/src/middleware/index.ts +33 -0
  633. package/src/middleware/json-depth.ts +74 -0
  634. package/src/middleware/no-store.ts +30 -0
  635. package/src/middleware/oauth-public.ts +54 -0
  636. package/src/middleware/rate-limit.ts +432 -0
  637. package/src/middleware/request-id.ts +54 -0
  638. package/src/middleware/scopes.ts +150 -0
  639. package/src/middleware/security-headers.ts +129 -0
  640. package/src/observability/index.ts +2 -0
  641. package/src/observability/metrics.ts +102 -0
  642. package/src/observability/otel.ts +79 -0
  643. package/src/pagination.ts +118 -0
  644. package/src/platform/edition.ts +140 -0
  645. package/src/platform/entitlements.ts +47 -0
  646. package/src/platform/events.ts +71 -0
  647. package/src/platform/index.ts +7 -0
  648. package/src/platform/local-identity.ts +90 -0
  649. package/src/platform/registry.ts +28 -0
  650. package/src/platform/usage.ts +13 -0
  651. package/src/platform/workspaces.ts +41 -0
  652. package/src/projects/access.ts +160 -0
  653. package/src/projects/index.ts +7 -0
  654. package/src/route-label.ts +29 -0
  655. package/src/routes/health.ts +79 -0
  656. package/src/routes/index.ts +2 -0
  657. package/src/routes/wellknown.ts +67 -0
  658. package/src/secret.ts +115 -0
  659. package/src/setup-transport.ts +46 -0
  660. package/src/state.ts +23 -0
  661. package/src/storage/driver.ts +48 -0
  662. package/src/storage/factory.ts +34 -0
  663. package/src/storage/index.ts +3 -0
  664. package/src/storage/local.ts +93 -0
  665. package/src/storage/s3.ts +109 -0
  666. package/src/testing/fake-hub.ts +1316 -0
  667. package/src/testing/helpers.ts +254 -0
  668. package/src/testing/index.ts +11 -0
  669. package/src/testing/setup-origins.ts +58 -0
  670. package/src/testing/sso-helpers.ts +178 -0
  671. package/src/tool-definition.ts +463 -0
  672. package/src/util/index.ts +17 -0
  673. package/test/fixtures/onboarding-backfill.sql +13 -0
  674. package/test/fixtures/operator-user-id-backfill.sql +18 -0
  675. package/test/host/dr-lib.sh +253 -0
  676. package/test/host/drizzle/0000_chassis_baseline.sql +438 -0
  677. package/test/host/drizzle/0001_last_owner_guard.sql +42 -0
  678. package/test/host/drizzle/meta/0000_snapshot.json +3566 -0
  679. package/test/host/drizzle/meta/_journal.json +20 -0
  680. package/test/host/drizzle.config.ts +17 -0
  681. package/test/host/identity.ts +41 -0
  682. package/test/host/minimal-host.ts +35 -0
  683. package/test/host/minimal-tool.ts +123 -0
  684. package/test/integration/account-oracle-uniformity.test.ts +284 -0
  685. package/test/integration/account-recovery.test.ts +236 -0
  686. package/test/integration/account.test.ts +120 -0
  687. package/test/integration/api-quota.test.ts +208 -0
  688. package/test/integration/apikey-expiry.test.ts +178 -0
  689. package/test/integration/apikey-pepper-rotation.test.ts +190 -0
  690. package/test/integration/boot-posture.test.ts +169 -0
  691. package/test/integration/break-glass.test.ts +484 -0
  692. package/test/integration/cloud-lifecycle.test.ts +233 -0
  693. package/test/integration/credential-boundaries.test.ts +195 -0
  694. package/test/integration/default-workspace.test.ts +414 -0
  695. package/test/integration/demo-pass-rules.test.ts +332 -0
  696. package/test/integration/demo-pass.test.ts +1222 -0
  697. package/test/integration/dr-verify-real-dump.test.ts +154 -0
  698. package/test/integration/edition.test.ts +868 -0
  699. package/test/integration/email-change.test.ts +394 -0
  700. package/test/integration/empty-tool.test.ts +267 -0
  701. package/test/integration/entitlements-null-hook.test.ts +187 -0
  702. package/test/integration/entitlements.test.ts +693 -0
  703. package/test/integration/export-entries.test.ts +321 -0
  704. package/test/integration/files.test.ts +320 -0
  705. package/test/integration/first-boot-claim.test.ts +131 -0
  706. package/test/integration/gdpr.test.ts +532 -0
  707. package/test/integration/helpers.ts +29 -0
  708. package/test/integration/hub-grant.test.ts +505 -0
  709. package/test/integration/hub-live-gate.test.ts +612 -0
  710. package/test/integration/hub-managed-membership.test.ts +559 -0
  711. package/test/integration/hub-org-selector.test.ts +281 -0
  712. package/test/integration/hub-reconcile.test.ts +646 -0
  713. package/test/integration/hub-sso.test.ts +704 -0
  714. package/test/integration/hub-teams.test.ts +771 -0
  715. package/test/integration/idempotency.test.ts +261 -0
  716. package/test/integration/invite-verification.test.ts +210 -0
  717. package/test/integration/last-owner-race.test.ts +263 -0
  718. package/test/integration/m9-fixes.test.ts +290 -0
  719. package/test/integration/mcp-projects.test.ts +463 -0
  720. package/test/integration/member-removal-race.test.ts +466 -0
  721. package/test/integration/member-removal.test.ts +794 -0
  722. package/test/integration/oauth-mcp.test.ts +477 -0
  723. package/test/integration/oauth-workspace.test.ts +371 -0
  724. package/test/integration/observability.test.ts +183 -0
  725. package/test/integration/onboarding.test.ts +268 -0
  726. package/test/integration/orphan-purge.test.ts +451 -0
  727. package/test/integration/pagination.test.ts +344 -0
  728. package/test/integration/platform-core.test.ts +305 -0
  729. package/test/integration/platform.test.ts +246 -0
  730. package/test/integration/pool-survives-restart.test.ts +80 -0
  731. package/test/integration/projects-cloud.test.ts +193 -0
  732. package/test/integration/projects.test.ts +1310 -0
  733. package/test/integration/security-fixes.test.ts +190 -0
  734. package/test/integration/setup-transport.test.ts +118 -0
  735. package/test/integration/signin-wall.test.ts +103 -0
  736. package/test/integration/signing-key-rotation.test.ts +349 -0
  737. package/test/integration/sso-helpers.ts +15 -0
  738. package/test/integration/sso-logout.test.ts +283 -0
  739. package/test/integration/teams-cloud.test.ts +194 -0
  740. package/test/integration/teams.test.ts +551 -0
  741. package/test/integration/timers.test.ts +377 -0
  742. package/test/integration/two-factor.test.ts +301 -0
  743. package/test/integration/usage-window.test.ts +161 -0
  744. package/test/integration/workspace-create-cloud.test.ts +523 -0
  745. package/test/integration/workspace-create.test.ts +510 -0
  746. package/test/integration/workspace-scoping.test.ts +530 -0
  747. package/test/integration/zz-adv-replay-owner.test.ts +205 -0
  748. package/test/unit/apikey-peppers.test.ts +110 -0
  749. package/test/unit/apikey-prefix.test.ts +34 -0
  750. package/test/unit/audit-filters.test.ts +135 -0
  751. package/test/unit/auth-origins.test.ts +92 -0
  752. package/test/unit/body-cap.test.ts +84 -0
  753. package/test/unit/clock-skew.test.ts +68 -0
  754. package/test/unit/cross-site.test.ts +166 -0
  755. package/test/unit/declared-credits.test.ts +13 -0
  756. package/test/unit/demo-pass-rules.test.ts +192 -0
  757. package/test/unit/email-driver.test.ts +127 -0
  758. package/test/unit/end-pool.test.ts +181 -0
  759. package/test/unit/entitlement-check.test.ts +492 -0
  760. package/test/unit/entitlement-gate.test.ts +1333 -0
  761. package/test/unit/entitlement-profiles.test.ts +289 -0
  762. package/test/unit/erasure-log.test.ts +72 -0
  763. package/test/unit/error-mapping.test.ts +89 -0
  764. package/test/unit/export-entries.test.ts +85 -0
  765. package/test/unit/export-sanitize.test.ts +27 -0
  766. package/test/unit/files-head-route.test.ts +155 -0
  767. package/test/unit/files-http.test.ts +97 -0
  768. package/test/unit/head-rewrite.test.ts +72 -0
  769. package/test/unit/hub-create-org.test.ts +181 -0
  770. package/test/unit/hub-jwt.test.ts +93 -0
  771. package/test/unit/hub-sso-params.test.ts +39 -0
  772. package/test/unit/hub-subject.test.ts +83 -0
  773. package/test/unit/idempotency-crypto.test.ts +45 -0
  774. package/test/unit/json-depth.test.ts +88 -0
  775. package/test/unit/logger-redaction.test.ts +66 -0
  776. package/test/unit/mcp-errors.test.ts +110 -0
  777. package/test/unit/mcp-inputs.test.ts +106 -0
  778. package/test/unit/mcp-project-hints.test.ts +26 -0
  779. package/test/unit/no-store.test.ts +49 -0
  780. package/test/unit/no-tool-migration-filename.test.ts +70 -0
  781. package/test/unit/observability.test.ts +64 -0
  782. package/test/unit/openapi-doc.test.ts +93 -0
  783. package/test/unit/rate-limit-consume-on.test.ts +76 -0
  784. package/test/unit/rate-limit.test.ts +39 -0
  785. package/test/unit/readyz-storage.test.ts +157 -0
  786. package/test/unit/request-quota.test.ts +146 -0
  787. package/test/unit/scopes.test.ts +66 -0
  788. package/test/unit/security-headers.test.ts +131 -0
  789. package/test/unit/serve-content-type.test.ts +74 -0
  790. package/test/unit/setup-transport.test.ts +29 -0
  791. package/test/unit/storage-local.test.ts +53 -0
  792. package/test/unit/superadmin.test.ts +32 -0
  793. package/test/unit/timers.test.ts +287 -0
  794. package/test/unit/tool-definition-guards.test.ts +166 -0
  795. package/test/unit/tool-entitlements-guard.test.ts +168 -0
  796. package/test/unit/usage-poster.test.ts +542 -0
  797. package/test/unit/usage-window.test.ts +48 -0
  798. package/test/unit/workspace-create-audit.test.ts +48 -0
@@ -0,0 +1,704 @@
1
+ import pg from 'pg';
2
+ import { Counter } from 'prom-client';
3
+ import { and, eq } from 'drizzle-orm';
4
+ import { symmetricDecrypt, symmetricEncrypt } from 'better-auth/crypto';
5
+ import { account, hubGrantPresentations, type Db } from '@antasphere/chassis-db';
6
+ import type { Logger } from '../logger.js';
7
+ import { HUB_SSO_PROVIDER_ID } from './hub-sso.js';
8
+
9
+ /**
10
+ * The per-user hub grant (internal/federation.md, live user-scoped federation):
11
+ * a cloud tool holds each user's OWN `offline_access account:read` grant
12
+ * (plus `orgs:create` since PRDCT-2443 — hub-sso.ts HUB_SSO_SCOPES)
13
+ * — obtained at SSO login, persisted by Better Auth on the `account` row —
14
+ * and refreshes it against the hub's token endpoint to mint HUB-audienced
15
+ * access tokens (`resource=<hub>/mcp`, RFC 8707) that the hub's own
16
+ * `/api/v1` accepts. Everything the tool reads from the hub between logins
17
+ * is read AS THE USER with this grant; no service key exists anywhere in
18
+ * the user-facing path.
19
+ *
20
+ * Storage: the `account` row is the ONLY durable store (Better Auth already
21
+ * re-writes both tokens at every browser SSO login — the self-healing
22
+ * `hub_grant_expired` recovery path). With `account.encryptOAuthTokens: true`
23
+ * (identity/better-auth.ts) Better Auth encrypts what IT writes; this
24
+ * service encrypts/decrypts with the SAME primitive and the SAME key
25
+ * (`(await auth.$context).secretConfig` — with our string-secret config that
26
+ * is exactly the boot AUTH_SECRET), so the two writers are byte-compatible.
27
+ * Legacy plaintext rows pass through decrypt untouched (the 1.6.15
28
+ * `isLikelyEncrypted` rule, mirrored below).
29
+ *
30
+ * ── SECURITY-CRITICAL: refresh serialization ─────────────────────────────
31
+ * The hub rotates refresh tokens on EVERY use with RFC 9700 reuse
32
+ * detection: presenting a rotated-out token tears down the user's whole
33
+ * (client,user) grant family. A cross-replica double-refresh is therefore
34
+ * not a race, it is a GRANT-KILLING event. Discipline, in layers:
35
+ *
36
+ * 1. in-process single-flight per user (Map<userId, Promise>);
37
+ * 2. a cross-replica session-scoped `pg_advisory_lock(7432004,
38
+ * hashtext(userId))` on a DEDICATED pg client (the accounts/deletion.ts
39
+ * OWNER_GUARD_LOCK pattern). The two-int form keys a DIFFERENT advisory
40
+ * lock space than pg-boss's single-int `pg_advisory_lock(7432004)`
41
+ * (classid 7432004/objsubid 2 vs classid 0/objsubid 1) — no collision;
42
+ * 3. RE-READ AFTER LOCK: another replica may have refreshed while we
43
+ * waited — a changed refresh token (or a now-fresh stored access token)
44
+ * means we consume ITS result instead of presenting the stale token;
45
+ * 4. a ~10 s watchdog cuts the dedicated connection so a wedged refresh
46
+ * can never park the lock forever (a session lock dies with its
47
+ * session);
48
+ * 5. AMBIGUOUS OUTCOMES NEVER RE-PRESENT BLINDLY (PRDCT-1370). The hub
49
+ * rotates BEFORE it answers and cannot roll back, so a client-side
50
+ * timeout (or a lost response, or a 5xx after the commit) leaves us
51
+ * holding a token the hub may already have rotated out — and presenting
52
+ * it again is exactly the reuse-detection teardown above, reachable
53
+ * with no attacker, only a hub that is slow but alive. Every
54
+ * presentation is therefore recorded FIRST (`hub_grant_presentations`,
55
+ * keyed by the account row, carrying the ciphertext presented) and the
56
+ * record is cleared only once the hub's answer is known. A surviving
57
+ * record makes the next refresh PROBE the token through the hub's
58
+ * RFC 7662 introspection endpoint (`token_type_hint=refresh_token`,
59
+ * read-only: the pinned plugin answers `active:false` for a rotated-out
60
+ * token WITHOUT tearing the family down) and present it only when the
61
+ * hub still calls it active; an inactive answer marks the grant dead
62
+ * without ever presenting — the family, the CLI grant included, lives
63
+ * on at the hub and a browser SSO login heals this user.
64
+ *
65
+ * Failure taxonomy (the token endpoint's answer decides):
66
+ * - `invalid_grant` (expired / revoked / reuse-detected) → the grant is
67
+ * DEAD: tokens on the row are nulled (dead marker = refreshToken IS
68
+ * NULL); callers surface 401 `hub_grant_expired`; the next browser SSO
69
+ * login re-seeds the row and heals it.
70
+ * - `invalid_client` (broken client secret = OUR misconfiguration) →
71
+ * transient, logged LOUD, and it never kills grants — a config mistake
72
+ * must not mass-destroy every user's grant.
73
+ * - network / timeout / 5xx / malformed → transient (inconclusive).
74
+ *
75
+ * Access tokens are cached in-memory to exp − accessSkewMs; the account
76
+ * row's stored access token is trusted while fresh (it was minted with the
77
+ * same `resource` by either the login exchange or a sibling replica's
78
+ * refresh). An oss boot never constructs this class.
79
+ */
80
+
81
+ /** Advisory-lock classid for the per-user refresh lock (two-int form). */
82
+ const GRANT_REFRESH_LOCK_KEY = 7_432_004;
83
+ /** Headroom the lock watchdog keeps beyond probe + presentation (row reads/writes, lock latency). */
84
+ export const LOCK_WATCHDOG_HEADROOM_MS = 2_000;
85
+
86
+ export interface HubGrantDials {
87
+ /** Serve cached/stored access tokens only while exp − now exceeds this. */
88
+ accessSkewMs: number;
89
+ /** Token-endpoint fetch timeout. */
90
+ tokenTimeoutMs: number;
91
+ /** Introspection-probe fetch timeout (the unanswered-presentation path). */
92
+ introspectTimeoutMs: number;
93
+ /** Watchdog cutting the dedicated lock connection (wedged-refresh bound). */
94
+ lockWatchdogMs: number;
95
+ }
96
+
97
+ export const DEFAULT_GRANT_DIALS: HubGrantDials = {
98
+ accessSkewMs: 60_000,
99
+ tokenTimeoutMs: 5_000,
100
+ introspectTimeoutMs: 5_000,
101
+ // ≥ tokenTimeoutMs + introspectTimeoutMs + LOCK_WATCHDOG_HEADROOM_MS; the
102
+ // constructor clamps anything smaller (see there).
103
+ lockWatchdogMs: 12_000
104
+ };
105
+
106
+ /**
107
+ * One answer about a user's hub access.
108
+ * - 'ok': a hub-callable access token.
109
+ * - 'no_link': the user holds no hub identity at all (a purely local user)
110
+ * — nothing to refresh, and that IS the definitive answer.
111
+ * - 'grant_dead': the stored refresh token is gone (revoked / expired /
112
+ * reuse-torn-down, or never granted — e.g. a cli-connect JIT without the
113
+ * Stage F grant channel). Only a browser SSO login heals this.
114
+ * - 'inconclusive': transient failure; nothing was destroyed.
115
+ */
116
+ export type GrantAccess =
117
+ | { kind: 'ok'; accessToken: string }
118
+ | { kind: 'no_link' }
119
+ | { kind: 'grant_dead' }
120
+ | { kind: 'inconclusive' };
121
+
122
+ /** Key material for the symmetric token encryption (Better Auth's own). */
123
+ type SecretKeyMaterial = Parameters<typeof symmetricEncrypt>[0]['key'];
124
+
125
+ export interface HubGrantServiceOptions {
126
+ db: Db;
127
+ /** Hub OIDC issuer (HUB_ISSUER_URL) — the token endpoint derives from it. */
128
+ issuerUrl: string;
129
+ clientId: string;
130
+ clientSecret: string;
131
+ /**
132
+ * RFC 8707 `resource` sent on every refresh — `<hub>/mcp`, so the hub
133
+ * mints a JWT its own /api/v1 accepts. THE seam constant: boot passes the
134
+ * same value into the SSO code exchange (hub-sso.ts), and flipping it to
135
+ * null here (if the hub ever accepts opaque tokens) is a one-line change.
136
+ */
137
+ tokenResource: string | null;
138
+ /**
139
+ * Better Auth's token-encryption key material — `(await
140
+ * auth.$context).secretConfig`. Lazy because the auth context resolves
141
+ * async; with our config it equals the boot AUTH_SECRET.
142
+ */
143
+ key: () => Promise<SecretKeyMaterial>;
144
+ /** Dedicated advisory-lock connections (the migrate.ts / deletion.ts pattern). */
145
+ connectionString: string;
146
+ logger: Logger;
147
+ dials: HubGrantDials;
148
+ /** Test seams. */
149
+ fetchImpl?: typeof fetch;
150
+ now?: () => number;
151
+ }
152
+
153
+ interface GrantRow {
154
+ id: string;
155
+ accessToken: string | null;
156
+ refreshToken: string | null;
157
+ accessTokenExpiresAt: Date | null;
158
+ }
159
+
160
+ interface RefreshLock {
161
+ client: pg.Client;
162
+ released: boolean;
163
+ watchdog?: NodeJS.Timeout;
164
+ }
165
+
166
+ /**
167
+ * Mirror of Better Auth 1.6.15's `isLikelyEncrypted` (dist/oauth2/utils.mjs):
168
+ * its decrypt path passes LEGACY PLAINTEXT tokens through untouched, and so
169
+ * must ours — enabling encryption must never brick pre-existing grants.
170
+ * Exported for hub-logout.ts, whose id_token read mirrors the same
171
+ * tolerant rule. Re-verify on any Better Auth bump.
172
+ */
173
+ export function isLikelyEncrypted(token: string): boolean {
174
+ if (token.startsWith('$ba$')) return true;
175
+ return token.length % 2 === 0 && /^[0-9a-f]+$/i.test(token);
176
+ }
177
+
178
+ type RefreshOutcome =
179
+ | { kind: 'ok'; accessToken: string; expiresAt: Date; refreshToken: string | null }
180
+ | { kind: 'invalid_grant' }
181
+ | { kind: 'invalid_client' }
182
+ | { kind: 'transient' };
183
+
184
+ /** The RFC 7662 probe's answer about a refresh token the hub never confirmed consuming. */
185
+ type ProbeOutcome = 'active' | 'inactive' | 'transient';
186
+
187
+ export class HubGrantService {
188
+ /** userId → fresh access token (in-memory only; the row is the durable store). */
189
+ private readonly cache = new Map<string, { token: string; expiresAtMs: number }>();
190
+ private readonly inFlight = new Map<string, Promise<GrantAccess>>();
191
+ private readonly tokenUrl: string;
192
+ private readonly introspectUrl: string;
193
+ private readonly fetchImpl: typeof fetch;
194
+ private readonly now: () => number;
195
+ readonly dials: HubGrantDials;
196
+
197
+ /** Registered into the Prometheus registry by boot (cloud only). */
198
+ readonly promMetrics: Counter[];
199
+ private readonly refreshes: Counter;
200
+
201
+ constructor(private readonly opts: HubGrantServiceOptions) {
202
+ this.tokenUrl = opts.issuerUrl.replace(/\/+$/, '') + '/api/v1/auth/oauth2/token';
203
+ this.introspectUrl = opts.issuerUrl.replace(/\/+$/, '') + '/api/v1/auth/oauth2/introspect';
204
+ this.fetchImpl = opts.fetchImpl ?? fetch;
205
+ this.now = opts.now ?? Date.now;
206
+ // THE WATCHDOG MUST OUTLIVE THE LONGEST HOLD (PRDCT-1370 verifier
207
+ // finding): under the lock a refresh may now run the probe AND the
208
+ // presentation back to back, each up to its own timeout, plus the
209
+ // row reads and writes around them. A watchdog shorter than that cuts
210
+ // the lock mid-refresh, a sibling replica takes it and presents the
211
+ // SAME token — the double presentation the lock exists to prevent, and
212
+ // a family teardown at the hub. Clamped here, at the one place the
213
+ // dials are read, so no caller can under-size it.
214
+ const floor = opts.dials.tokenTimeoutMs + opts.dials.introspectTimeoutMs + LOCK_WATCHDOG_HEADROOM_MS;
215
+ this.dials = opts.dials.lockWatchdogMs >= floor ? opts.dials : { ...opts.dials, lockWatchdogMs: floor };
216
+ if (this.dials !== opts.dials) {
217
+ opts.logger.warn(
218
+ { requested: opts.dials.lockWatchdogMs, effective: floor },
219
+ 'hub grant: lockWatchdogMs raised to cover probe + presentation under the lock'
220
+ );
221
+ }
222
+ // registers: [] — boot attaches this to the app registry on cloud; an
223
+ // oss boot never constructs this class, so the metric never exists there.
224
+ this.refreshes = new Counter({
225
+ name: 'hub_grant_refreshes_total',
226
+ help: 'Hub user-grant refresh attempts by outcome',
227
+ labelNames: ['outcome'] as const,
228
+ registers: []
229
+ });
230
+ this.promMetrics = [this.refreshes];
231
+ }
232
+
233
+ /**
234
+ * Store the H3 offline grant — the raw one-time `hubRefreshToken` from
235
+ * the hub's `POST /sso/tool-token` response, delivered through
236
+ * `/sso/cli-connect` — on the user's `antasphere` account row, encrypted
237
+ * with the SAME primitive + key as every Better Auth write, so the
238
+ * ordinary refresh path consumes it exactly like a browser-SSO grant.
239
+ * This is what gives a HEADLESS CLI user live as-the-user org reads
240
+ * between browser logins (and replaces the Stage E interim, where a
241
+ * connect-minted key answered 401 hub_grant_expired until one browser
242
+ * SSO).
243
+ *
244
+ * Overwrites any previous grant unconditionally: the H3 mint is the
245
+ * NEWEST valid credential, and the replaced family simply idles to expiry
246
+ * at the hub. The stored access token is cleared (the new grant has none
247
+ * yet); the caller VALIDATES the grant end-to-end by running the
248
+ * fail-closed connect reconcile right after — a garbage token surfaces as
249
+ * `grant_dead` there and no key is minted.
250
+ *
251
+ * Throws when the user holds no `antasphere` account row — the connect
252
+ * flow provisions the link BEFORE acquiring the grant, so that is a
253
+ * wiring bug, never a user state.
254
+ */
255
+ async acquireFromConnect(userId: string, rawRefreshToken: string): Promise<void> {
256
+ const key = await this.opts.key();
257
+ const encrypted = await symmetricEncrypt({ key, data: rawRefreshToken });
258
+ const updated = await this.opts.db
259
+ .update(account)
260
+ .set({
261
+ accessToken: null,
262
+ accessTokenExpiresAt: null,
263
+ refreshToken: encrypted,
264
+ refreshTokenExpiresAt: null,
265
+ updatedAt: new Date()
266
+ })
267
+ .where(and(eq(account.userId, userId), eq(account.providerId, HUB_SSO_PROVIDER_ID)))
268
+ .returning({ id: account.id });
269
+ if (updated.length === 0) {
270
+ throw new Error('acquireFromConnect: no antasphere account row to store the grant on');
271
+ }
272
+ this.cache.delete(userId);
273
+ }
274
+
275
+ /**
276
+ * Whether a stored (possibly stale) grant exists — the connect route's
277
+ * born-dead guard for requests that carry no hubRefreshToken: with no
278
+ * stored grant either, minting a key would strand it on
279
+ * hub_grant_expired, so the connect refuses with steering instead.
280
+ */
281
+ async hasStoredGrant(userId: string): Promise<boolean> {
282
+ const row = await this.readRow(userId);
283
+ return Boolean(row?.refreshToken);
284
+ }
285
+
286
+ /**
287
+ * Seed the in-memory cache with the SSO callback's access token (already
288
+ * hub-audienced — the code exchange sends the same `resource`), so the
289
+ * login-path reads and the first post-login requests need no refresh.
290
+ */
291
+ prime(userId: string, accessToken: string, expiresAt: Date | null): void {
292
+ // Without a stated expiry, assume a conservative slice of the hub's
293
+ // 15-minute access tokens.
294
+ const expiresAtMs = expiresAt?.getTime() ?? this.now() + 5 * 60_000;
295
+ this.cache.set(userId, { token: accessToken, expiresAtMs });
296
+ }
297
+
298
+ /** Drop the cached access token (e.g. the hub just refused it). */
299
+ invalidateAccess(userId: string): void {
300
+ this.cache.delete(userId);
301
+ }
302
+
303
+ /** A hub-callable access token for the user, refresh-on-demand. */
304
+ async accessToken(userId: string): Promise<GrantAccess> {
305
+ const cached = this.cache.get(userId);
306
+ if (cached && cached.expiresAtMs - this.now() > this.dials.accessSkewMs) {
307
+ return { kind: 'ok', accessToken: cached.token };
308
+ }
309
+ return this.acquire(userId, false);
310
+ }
311
+
312
+ /**
313
+ * Force a refresh (the caller saw the hub refuse the current token).
314
+ * Serialized behind any in-flight acquisition; the advisory lock +
315
+ * re-read-after-lock still guarantee at most one live rotation.
316
+ */
317
+ async refresh(userId: string): Promise<GrantAccess> {
318
+ this.cache.delete(userId);
319
+ const existing = this.inFlight.get(userId);
320
+ if (existing) await existing.catch(() => {});
321
+ return this.acquire(userId, true);
322
+ }
323
+
324
+ private acquire(userId: string, force: boolean): Promise<GrantAccess> {
325
+ let flight = this.inFlight.get(userId);
326
+ if (!flight) {
327
+ flight = this.doAcquire(userId, force).finally(() => {
328
+ if (this.inFlight.get(userId) === flight) this.inFlight.delete(userId);
329
+ });
330
+ this.inFlight.set(userId, flight);
331
+ }
332
+ return flight;
333
+ }
334
+
335
+ private async doAcquire(userId: string, force: boolean): Promise<GrantAccess> {
336
+ try {
337
+ const first = await this.readRow(userId);
338
+ if (!first) return { kind: 'no_link' };
339
+ if (!first.refreshToken) return { kind: 'grant_dead' };
340
+
341
+ if (!force) {
342
+ const stored = await this.storedAccess(userId, first);
343
+ if (stored) return { kind: 'ok', accessToken: stored };
344
+ }
345
+
346
+ // Slow path: rotate under the cross-replica lock.
347
+ const lock = await this.acquireLock(userId);
348
+ try {
349
+ // RE-READ AFTER LOCK: a sibling replica (or a concurrent browser
350
+ // login) may have rotated while we waited. A changed refresh token
351
+ // means ITS access token is brand new — consume that instead of
352
+ // presenting our stale copy (which reuse detection would treat as
353
+ // an attack and kill the whole family).
354
+ const row = await this.readRow(userId);
355
+ if (!row) return { kind: 'no_link' };
356
+ if (!row.refreshToken) return { kind: 'grant_dead' };
357
+ if (!force || row.refreshToken !== first.refreshToken) {
358
+ const stored = await this.storedAccess(userId, row);
359
+ if (stored) return { kind: 'ok', accessToken: stored };
360
+ }
361
+
362
+ let refreshToken: string;
363
+ try {
364
+ refreshToken = await this.decrypt(row.refreshToken);
365
+ } catch (err) {
366
+ // Undecryptable ≠ revoked: a key/config problem must never null
367
+ // the row (the invalid_client stance). Loud, transient.
368
+ this.refreshes.inc({ outcome: 'error' });
369
+ this.opts.logger.error(
370
+ { err, userId },
371
+ 'hub grant: stored refresh token failed to decrypt — check AUTH_SECRET continuity; failing transient'
372
+ );
373
+ return { kind: 'inconclusive' };
374
+ }
375
+
376
+ // An UNANSWERED earlier presentation of this very ciphertext (layer
377
+ // 5): the hub may have rotated it out already. Probe, never guess.
378
+ if (await this.hasUnansweredPresentation(row)) {
379
+ const probe = await this.introspect(refreshToken);
380
+ if (probe === 'inactive') {
381
+ await this.markDead(row);
382
+ await this.clearPresentation(row.id);
383
+ this.cache.delete(userId);
384
+ this.refreshes.inc({ outcome: 'probe_dead' });
385
+ this.opts.logger.warn(
386
+ { userId },
387
+ 'hub grant: an earlier refresh went unanswered and the hub now reports the token inactive — ' +
388
+ 'grant marked dead WITHOUT re-presenting (the family survives at the hub); a browser SSO login heals it'
389
+ );
390
+ return { kind: 'grant_dead' };
391
+ }
392
+ if (probe === 'transient') {
393
+ this.refreshes.inc({ outcome: 'probe_transient' });
394
+ return { kind: 'inconclusive' };
395
+ }
396
+ this.refreshes.inc({ outcome: 'probe_live' });
397
+ // active: the unanswered presentation never reached the hub —
398
+ // presenting again is safe, and the record below is renewed.
399
+ }
400
+
401
+ // Record the presentation BEFORE the fetch: if the answer never
402
+ // arrives, this row is what makes the next attempt probe first.
403
+ await this.recordPresentation(row);
404
+ const outcome = await this.postRefresh(refreshToken);
405
+ if (outcome.kind === 'ok') {
406
+ await this.writeRotation(row, outcome);
407
+ await this.clearPresentation(row.id);
408
+ this.cache.set(userId, { token: outcome.accessToken, expiresAtMs: outcome.expiresAt.getTime() });
409
+ this.refreshes.inc({ outcome: 'ok' });
410
+ return { kind: 'ok', accessToken: outcome.accessToken };
411
+ }
412
+ if (outcome.kind === 'invalid_grant') {
413
+ // Definitive: expired, revoked, or reuse-torn-down.
414
+ await this.markDead(row);
415
+ await this.clearPresentation(row.id);
416
+ this.cache.delete(userId);
417
+ this.refreshes.inc({ outcome: 'invalid_grant' });
418
+ this.opts.logger.warn(
419
+ { userId },
420
+ 'hub grant: token endpoint answered invalid_grant — grant marked dead; a browser SSO login heals it'
421
+ );
422
+ return { kind: 'grant_dead' };
423
+ }
424
+ if (outcome.kind === 'invalid_client') {
425
+ // The hub answered, and it refused the CLIENT before touching the
426
+ // token (the pinned plugin validates client credentials before it
427
+ // rotates), so the presented token was not consumed: the record
428
+ // is cleared, not kept.
429
+ await this.clearPresentation(row.id);
430
+ this.refreshes.inc({ outcome: 'invalid_client' });
431
+ this.opts.logger.error(
432
+ 'hub grant: token endpoint answered invalid_client — HUB_CLIENT_ID/HUB_CLIENT_SECRET are broken; ' +
433
+ 'refreshes are failing transient (grants are NOT being killed); fix the client credentials'
434
+ );
435
+ return { kind: 'inconclusive' };
436
+ }
437
+ // Transient (timeout / network / 5xx / malformed): the answer is
438
+ // UNKNOWN, so the presentation record deliberately survives — the
439
+ // next attempt probes before it presents.
440
+ this.refreshes.inc({ outcome: 'error' });
441
+ return { kind: 'inconclusive' };
442
+ } finally {
443
+ await this.releaseLock(lock);
444
+ }
445
+ } catch (err) {
446
+ this.refreshes.inc({ outcome: 'error' });
447
+ this.opts.logger.warn({ err, userId }, 'hub grant: refresh attempt failed — failing transient');
448
+ return { kind: 'inconclusive' };
449
+ }
450
+ }
451
+
452
+ /** The user's hub account link, tokens as stored (possibly encrypted). */
453
+ /**
454
+ * Null the tokens (dead marker = refreshToken IS NULL) — CONDITIONAL on
455
+ * the ciphertext we hold, so a browser login that re-seeded the row
456
+ * mid-flight is never wiped.
457
+ */
458
+ private async markDead(row: GrantRow): Promise<void> {
459
+ await this.opts.db
460
+ .update(account)
461
+ .set({
462
+ accessToken: null,
463
+ refreshToken: null,
464
+ accessTokenExpiresAt: null,
465
+ refreshTokenExpiresAt: null,
466
+ updatedAt: new Date()
467
+ })
468
+ .where(and(eq(account.id, row.id), eq(account.refreshToken, row.refreshToken!)));
469
+ }
470
+
471
+ /** Whether an earlier presentation of THIS ciphertext is still unanswered. */
472
+ private async hasUnansweredPresentation(row: GrantRow): Promise<boolean> {
473
+ const [marker] = await this.opts.db
474
+ .select({ refreshToken: hubGrantPresentations.refreshToken })
475
+ .from(hubGrantPresentations)
476
+ .where(eq(hubGrantPresentations.accountId, row.id))
477
+ .limit(1);
478
+ // A marker for a DIFFERENT ciphertext is stale by construction (a
479
+ // browser login re-seeded the row): the token it names is gone.
480
+ return marker !== undefined && marker.refreshToken === row.refreshToken;
481
+ }
482
+
483
+ private async recordPresentation(row: GrantRow): Promise<void> {
484
+ await this.opts.db
485
+ .insert(hubGrantPresentations)
486
+ .values({ accountId: row.id, refreshToken: row.refreshToken!, presentedAt: new Date() })
487
+ .onConflictDoUpdate({
488
+ target: hubGrantPresentations.accountId,
489
+ set: { refreshToken: row.refreshToken!, presentedAt: new Date() }
490
+ });
491
+ }
492
+
493
+ private async clearPresentation(accountId: string): Promise<void> {
494
+ await this.opts.db.delete(hubGrantPresentations).where(eq(hubGrantPresentations.accountId, accountId));
495
+ }
496
+
497
+ /**
498
+ * RFC 7662 probe of a refresh token whose last presentation went
499
+ * unanswered. Read-only at the hub: the pinned oauth-provider plugin's
500
+ * introspection answers `active:false` for a rotated-out, expired, or
501
+ * unknown refresh token and never touches the family. Anything but a
502
+ * definite answer is transient — a probe must never be the reason a
503
+ * grant dies.
504
+ */
505
+ private async introspect(refreshToken: string): Promise<ProbeOutcome> {
506
+ const body = new URLSearchParams({
507
+ token: refreshToken,
508
+ token_type_hint: 'refresh_token',
509
+ client_id: this.opts.clientId,
510
+ client_secret: this.opts.clientSecret
511
+ });
512
+ let res: Response;
513
+ try {
514
+ res = await this.fetchImpl(this.introspectUrl, {
515
+ method: 'POST',
516
+ headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
517
+ body: body.toString(),
518
+ signal: AbortSignal.timeout(this.dials.introspectTimeoutMs)
519
+ });
520
+ } catch (err) {
521
+ this.opts.logger.warn({ err }, 'hub grant: introspection endpoint unreachable — probe inconclusive');
522
+ return 'transient';
523
+ }
524
+ let payload: unknown = null;
525
+ try {
526
+ payload = await res.json();
527
+ } catch {
528
+ // classified below by status
529
+ }
530
+ if (res.ok) {
531
+ const active = (payload as { active?: unknown } | null)?.active;
532
+ if (active === true) return 'active';
533
+ if (active === false) return 'inactive';
534
+ this.opts.logger.warn(
535
+ 'hub grant: introspection 200 without a boolean `active` — malformed, probe inconclusive'
536
+ );
537
+ return 'transient';
538
+ }
539
+ if (res.status === 401) {
540
+ this.opts.logger.error(
541
+ 'hub grant: introspection answered invalid_client — HUB_CLIENT_ID/HUB_CLIENT_SECRET are broken; probe inconclusive'
542
+ );
543
+ return 'transient';
544
+ }
545
+ this.opts.logger.warn(
546
+ { status: res.status },
547
+ 'hub grant: introspection answered non-2xx — probe inconclusive'
548
+ );
549
+ return 'transient';
550
+ }
551
+
552
+ private async readRow(userId: string): Promise<GrantRow | null> {
553
+ const [row] = await this.opts.db
554
+ .select({
555
+ id: account.id,
556
+ accessToken: account.accessToken,
557
+ refreshToken: account.refreshToken,
558
+ accessTokenExpiresAt: account.accessTokenExpiresAt
559
+ })
560
+ .from(account)
561
+ .where(and(eq(account.userId, userId), eq(account.providerId, HUB_SSO_PROVIDER_ID)))
562
+ .limit(1);
563
+ return row ?? null;
564
+ }
565
+
566
+ /** Decrypt-and-cache the row's access token when it is still fresh. */
567
+ private async storedAccess(userId: string, row: GrantRow): Promise<string | null> {
568
+ if (!row.accessToken || !row.accessTokenExpiresAt) return null;
569
+ const expiresAtMs = row.accessTokenExpiresAt.getTime();
570
+ if (expiresAtMs - this.now() <= this.dials.accessSkewMs) return null;
571
+ try {
572
+ const token = await this.decrypt(row.accessToken);
573
+ this.cache.set(userId, { token, expiresAtMs });
574
+ return token;
575
+ } catch (err) {
576
+ this.opts.logger.warn({ err, userId }, 'hub grant: stored access token failed to decrypt — refreshing');
577
+ return null;
578
+ }
579
+ }
580
+
581
+ /**
582
+ * Persist a rotation's result on the row, Better-Auth-compatible
583
+ * (encrypted with the same key + primitive setTokenUtil uses).
584
+ * Conditional on the ciphertext we rotated FROM: if a concurrent browser
585
+ * login re-wrote the row, its (equally valid, same-family) tokens win and
586
+ * our rotation is only served from memory.
587
+ */
588
+ private async writeRotation(
589
+ row: GrantRow,
590
+ outcome: Extract<RefreshOutcome, { kind: 'ok' }>
591
+ ): Promise<void> {
592
+ const key = await this.opts.key();
593
+ const encryptedAccess = await symmetricEncrypt({ key, data: outcome.accessToken });
594
+ const encryptedRefresh = outcome.refreshToken
595
+ ? await symmetricEncrypt({ key, data: outcome.refreshToken })
596
+ : row.refreshToken; // no rotation in the answer — keep the presented one
597
+ await this.opts.db
598
+ .update(account)
599
+ .set({
600
+ accessToken: encryptedAccess,
601
+ refreshToken: encryptedRefresh,
602
+ accessTokenExpiresAt: outcome.expiresAt,
603
+ updatedAt: new Date()
604
+ })
605
+ .where(and(eq(account.id, row.id), eq(account.refreshToken, row.refreshToken!)));
606
+ }
607
+
608
+ private async decrypt(stored: string): Promise<string> {
609
+ if (!isLikelyEncrypted(stored)) return stored; // legacy plaintext passthrough
610
+ return symmetricDecrypt({ key: await this.opts.key(), data: stored });
611
+ }
612
+
613
+ /** One refresh_token grant POST, classified per the failure taxonomy. */
614
+ private async postRefresh(refreshToken: string): Promise<RefreshOutcome> {
615
+ const body = new URLSearchParams({
616
+ grant_type: 'refresh_token',
617
+ refresh_token: refreshToken,
618
+ client_id: this.opts.clientId,
619
+ client_secret: this.opts.clientSecret
620
+ });
621
+ if (this.opts.tokenResource) body.set('resource', this.opts.tokenResource);
622
+ let res: Response;
623
+ try {
624
+ res = await this.fetchImpl(this.tokenUrl, {
625
+ method: 'POST',
626
+ headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
627
+ body: body.toString(),
628
+ signal: AbortSignal.timeout(this.dials.tokenTimeoutMs)
629
+ });
630
+ } catch (err) {
631
+ this.opts.logger.warn({ err }, 'hub grant: token endpoint unreachable');
632
+ return { kind: 'transient' };
633
+ }
634
+ let payload: unknown = null;
635
+ try {
636
+ payload = await res.json();
637
+ } catch {
638
+ // fall through — classified below by status
639
+ }
640
+ if (res.ok) {
641
+ const p = (payload ?? {}) as { access_token?: unknown; refresh_token?: unknown; expires_in?: unknown };
642
+ if (typeof p.access_token !== 'string' || !p.access_token) {
643
+ this.opts.logger.warn('hub grant: token endpoint 200 without an access_token — malformed');
644
+ return { kind: 'transient' };
645
+ }
646
+ const expiresInS =
647
+ typeof p.expires_in === 'number' && Number.isFinite(p.expires_in) && p.expires_in > 0
648
+ ? p.expires_in
649
+ : 300; // conservative default when the hub omits it
650
+ return {
651
+ kind: 'ok',
652
+ accessToken: p.access_token,
653
+ expiresAt: new Date(this.now() + expiresInS * 1000),
654
+ refreshToken: typeof p.refresh_token === 'string' && p.refresh_token ? p.refresh_token : null
655
+ };
656
+ }
657
+ // RFC 6749 errors ride 400/401; anything else (5xx…) is transient.
658
+ if (res.status === 400 || res.status === 401) {
659
+ const error = (payload as { error?: unknown } | null)?.error;
660
+ if (error === 'invalid_grant') return { kind: 'invalid_grant' };
661
+ if (error === 'invalid_client') return { kind: 'invalid_client' };
662
+ this.opts.logger.warn({ status: res.status, error }, 'hub grant: unrecognized token-endpoint error');
663
+ return { kind: 'transient' };
664
+ }
665
+ this.opts.logger.warn({ status: res.status }, 'hub grant: token endpoint answered non-2xx');
666
+ return { kind: 'transient' };
667
+ }
668
+
669
+ /**
670
+ * Session-scoped pg_advisory_lock on a dedicated client — never the pool
671
+ * (a session lock parked on a pooled connection would poison the pool).
672
+ * The watchdog cuts the connection after lockWatchdogMs; a session lock
673
+ * dies with its session, so a wedged refresh is strictly bounded.
674
+ */
675
+ private async acquireLock(userId: string): Promise<RefreshLock> {
676
+ const client = new pg.Client({ connectionString: this.opts.connectionString });
677
+ await client.connect();
678
+ const lock: RefreshLock = { client, released: false };
679
+ lock.watchdog = setTimeout(() => {
680
+ this.opts.logger.warn(
681
+ { userId },
682
+ 'hub grant: refresh lock watchdog fired — cutting the dedicated connection'
683
+ );
684
+ lock.released = true;
685
+ void client.end().catch(() => {});
686
+ }, this.dials.lockWatchdogMs);
687
+ lock.watchdog.unref();
688
+ try {
689
+ await client.query('SELECT pg_advisory_lock($1, hashtext($2))', [GRANT_REFRESH_LOCK_KEY, userId]);
690
+ } catch (cause) {
691
+ await this.releaseLock(lock);
692
+ throw cause;
693
+ }
694
+ return lock;
695
+ }
696
+
697
+ /** Idempotent; closing the client guarantees the session lock dies with it. */
698
+ private async releaseLock(lock: RefreshLock): Promise<void> {
699
+ if (lock.released) return;
700
+ lock.released = true;
701
+ if (lock.watchdog) clearTimeout(lock.watchdog);
702
+ await lock.client.end().catch(() => {});
703
+ }
704
+ }