@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,594 @@
1
+ import PgBoss from 'pg-boss';
2
+ import pg from 'pg';
3
+ import { Counter } from 'prom-client';
4
+ import { sql } from 'drizzle-orm';
5
+ import type { Db } from '@antasphere/chassis-db';
6
+ import type { UsageDownstream, UsageEvent, UsageSink } from '@antasphere/chassis-contract';
7
+ import type { Env } from '../env.js';
8
+ import type { Logger } from '../logger.js';
9
+ import type { Auth } from '../identity/better-auth.js';
10
+ import type { AuditService } from '../audit/service.js';
11
+ import { parseSuperadminEmails } from '../accounts/superadmin.js';
12
+ import { PGBOSS_SCHEMA, PgBossTimers, type TimerJob, type Timers } from './timers.js';
13
+
14
+ /**
15
+ * pg-boss job runtime. Queue creation and worker registration follow
16
+ * SERVICE_ROLE:
17
+ * - api: start({ supervise:false, schedule:false, migrate:false }) — can
18
+ * send jobs, runs no maintenance, needs no DDL rights.
19
+ * - worker: full start, registers handlers.
20
+ * - all: both in one process (single-container default).
21
+ * pg-boss serializes its own SCHEMA install internally, but NOT createQueue —
22
+ * so the whole install section runs under the app's advisory lock (below).
23
+ * First boot must run with role all|worker once.
24
+ */
25
+ export const USAGE_QUEUE = 'usage-events';
26
+ export const AUDIT_PURGE_QUEUE = 'audit-purge';
27
+ /**
28
+ * Where a usage batch goes when the retry budget of `USAGE_QUEUE` runs out
29
+ * (pg-boss's dead letter, PRDCT-2635): a worker logs it at error level,
30
+ * counts it, and re-sends every event to `USAGE_QUEUE` after
31
+ * `heldDelaySeconds` — forever. Failed usage is held, never lost.
32
+ */
33
+ export const USAGE_HELD_QUEUE = 'usage-events-held';
34
+ /**
35
+ * The singleton key a window hold (PRDCT-2644) carries on the held queue:
36
+ * pg-boss's dead-letter copy carries none, so the held worker tells a
37
+ * retry-budget hold from a window hold by it. The held queue's policy is
38
+ * `standard` and the send names no `singletonSeconds`, so the key enforces
39
+ * nothing; it is a label.
40
+ */
41
+ const WINDOW_HOLD_PREFIX = 'occurred_at_window:';
42
+ const windowHoldKey = (id: string): string => WINDOW_HOLD_PREFIX + id;
43
+ const isWindowHoldKey = (key: string | null | undefined): boolean =>
44
+ typeof key === 'string' && key.startsWith(WINDOW_HOLD_PREFIX);
45
+ export const APIKEY_EXPIRY_QUEUE = 'apikey-expiry-sweep';
46
+ export const IDEMPOTENCY_PURGE_QUEUE = 'idempotency-purge';
47
+ export const ORPHAN_USER_PURGE_QUEUE = 'orphan-user-purge';
48
+
49
+ /**
50
+ * A TOOL's own job: its queue, its optional nightly schedule, its handler.
51
+ * The chassis creates the queue (and sets or clears the schedule) INSIDE the
52
+ * install advisory lock, between its own queues, in declaration order, and
53
+ * registers the handler as a poller after the lock is released — exactly
54
+ * where a chassis job of the same kind sits.
55
+ *
56
+ * `schedule.enabled === false` UNSCHEDULES: the queue still exists, so the
57
+ * schedule can be flipped later (the audit-purge pattern). A job with no
58
+ * `schedule` is a queue and a poller only: the queue a ONE-OFF TIMER rides
59
+ * (`Timers.schedule`, `./timers.ts`), whose handler receives the payload the
60
+ * timer was scheduled with and the job's id and key. A run the cron fires
61
+ * carries no payload: `data` is null there. The handler resolves whatever
62
+ * domain service it needs LAZILY, at run time: the jobs are created before
63
+ * the services that need the storage driver.
64
+ *
65
+ * `handler` is a method signature on purpose: a `JobDeclaration<MyPayload>`
66
+ * then sits in the slot's `JobDeclaration[]` without a cast, and a handler
67
+ * that takes no arguments (every cron before the timers) still fits.
68
+ */
69
+ export interface JobDeclaration<TData extends object = object> {
70
+ queue: string;
71
+ schedule?: { cron: string; enabled?: boolean };
72
+ handler(data: TData | null, job: TimerJob): Promise<void>;
73
+ }
74
+
75
+ /**
76
+ * Advisory-lock key serializing pg-boss's install DDL across replicas.
77
+ *
78
+ * pg-boss wraps its schema migration in its own advisory lock, but
79
+ * `createQueue()` calls `pgboss.create_queue()` (queue-row insert + per-queue
80
+ * partition CREATE TABLE/attach) with NO lock: two or more all|worker
81
+ * replicas booting a fresh database reliably deadlock there (Postgres
82
+ * DeadLockReport; scale drill, I2). Same session-scoped pattern as the
83
+ * migration lock (packages/chassis-db/src/migrate.ts). The key is distinct from the
84
+ * migration lock (7432001) and the last-owner locks (7432002 trigger xact /
85
+ * 7432003 session) — see internal/scale-drill-findings.md.
86
+ */
87
+ const PGBOSS_INSTALL_LOCK_KEY = 7_432_004;
88
+
89
+ /**
90
+ * Run pg-boss's one-time install section (schema + queues + schedules) under
91
+ * a session-scoped pg_advisory_lock on a dedicated client — never the pool
92
+ * (a session lock parked on a pooled connection would poison the pool). Held
93
+ * across boot setup ONLY; steady-state send/work never touches it.
94
+ */
95
+ async function withInstallLock(
96
+ connectionString: string,
97
+ logger: Logger,
98
+ fn: () => Promise<void>
99
+ ): Promise<void> {
100
+ const client = new pg.Client({ connectionString });
101
+ await client.connect();
102
+ try {
103
+ await client.query('SELECT pg_advisory_lock($1)', [PGBOSS_INSTALL_LOCK_KEY]);
104
+ logger.info({ scope: 'pgboss' }, 'advisory lock acquired, installing pg-boss schema and queues');
105
+ await fn();
106
+ logger.info({ scope: 'pgboss' }, 'pg-boss install up to date');
107
+ } finally {
108
+ try {
109
+ await client.query('SELECT pg_advisory_unlock($1)', [PGBOSS_INSTALL_LOCK_KEY]);
110
+ } finally {
111
+ await client.end();
112
+ }
113
+ }
114
+ }
115
+
116
+ export interface Jobs {
117
+ boss: PgBoss;
118
+ /** One-off timers on the tool's declared queues (`./timers.ts`). */
119
+ timers: Timers;
120
+ stop: () => Promise<void>;
121
+ /** The counters boot registers on the app's /metrics registry. */
122
+ promMetrics: Counter<string>[];
123
+ }
124
+
125
+ export async function createJobs(
126
+ env: Pick<
127
+ Env,
128
+ | 'DATABASE_URL'
129
+ | 'SERVICE_ROLE'
130
+ | 'AUDIT_RETENTION_DAYS'
131
+ | 'ORPHAN_USER_RETENTION_HOURS'
132
+ | 'SUPERADMIN_EMAILS'
133
+ >,
134
+ db: Db,
135
+ /** The pool behind `db`: a keyed timer's replace runs on a client of its own (`./timers.ts`). */
136
+ pool: pg.Pool,
137
+ logger: Logger,
138
+ downstreamUsage: UsageDownstream,
139
+ /** Orphan purge deletes through Better Auth's own internalAdapter (FK-safe cascade). */
140
+ auth: Auth,
141
+ /** System-actor audit rows for the orphan purge. */
142
+ audit: AuditService,
143
+ /** The tool's own jobs, installed and polled between the chassis ones. */
144
+ toolJobs: readonly JobDeclaration[] = [],
145
+ /** The usage queue's retry budget and the hold before a re-drive (a test shrinks both). */
146
+ usageRetry: UsageRetry = DEFAULT_USAGE_RETRY
147
+ ): Promise<Jobs> {
148
+ const isWorker = env.SERVICE_ROLE === 'all' || env.SERVICE_ROLE === 'worker';
149
+ // registers: [] — boot attaches it to the app registry.
150
+ const held = new Counter({
151
+ name: 'usage_events_held_total',
152
+ help: 'Usage events held for a re-drive, by reason: retry_budget (the retry budget ran out), occurred_at_window (the hub would refuse the date)',
153
+ labelNames: ['reason'] as const,
154
+ registers: []
155
+ });
156
+
157
+ const boss = new PgBoss({
158
+ connectionString: env.DATABASE_URL,
159
+ schema: PGBOSS_SCHEMA,
160
+ // Completed jobs archive after an hour and the archive is dropped after a
161
+ // week — the pgboss tables stay bounded on a busy instance.
162
+ archiveCompletedAfterSeconds: 3600,
163
+ deleteAfterDays: 7,
164
+ ...(isWorker ? {} : { supervise: false, schedule: false, migrate: false })
165
+ });
166
+ boss.on('error', (err) => logger.error({ err }, 'pg-boss error'));
167
+
168
+ const retentionDays = env.AUDIT_RETENTION_DAYS;
169
+ const orphanRetentionHours = env.ORPHAN_USER_RETENTION_HOURS;
170
+
171
+ if (isWorker) {
172
+ // The install section — pg-boss schema (start) + queue partitions
173
+ // (createQueue) + nightly schedules — is one-time DDL and runs under the
174
+ // install advisory lock so concurrent fresh-DB boots serialize instead
175
+ // of deadlocking in pgboss.create_queue. Handler registration below runs
176
+ // AFTER release: work() is an in-process poller, no DDL, and steady-state
177
+ // must never hold the lock.
178
+ await withInstallLock(env.DATABASE_URL, logger, async () => {
179
+ await boss.start();
180
+ // The held queue first, then the usage queue pointed at it as its dead
181
+ // letter — ON THE QUEUE, never on a send: an api-role replica sends
182
+ // without DDL rights and may roll before the worker of this release,
183
+ // and pg-boss's job row holds a foreign key on the dead letter it
184
+ // names, so a per-send dead letter would fail every send until the
185
+ // held queue exists (/code-review, lane D). pg-boss stamps the queue's
186
+ // dead letter on each job row at insert, so it covers every job sent
187
+ // after `updateQueue` ran, whoever sent it; on an installation that
188
+ // predates the held queue, a job an api-role replica sends between its
189
+ // own boot and this worker's install carries none (verifier round 2,
190
+ // accepted: one rollout's gap, and only a long outage on top would end
191
+ // it in a terminal failure). What pg-boss 10 gives a
192
+ // held job: the dead-letter copy carries the ORIGINAL retry limit (ten)
193
+ // and no delay, so a re-send that throws (a local insert refused while
194
+ // fetches still succeed) is retried ten times back to back, then that
195
+ // held job fails for good; the retry options on the held queue below
196
+ // apply to jobs sent to it, not to the copies pg-boss makes (verifier
197
+ // round 1, accepted: the path needs the database to refuse eleven
198
+ // inserts in a row while serving the fetches).
199
+ await boss.createQueue(USAGE_HELD_QUEUE, {
200
+ name: USAGE_HELD_QUEUE,
201
+ retryLimit: 10,
202
+ retryDelay: 60,
203
+ retryBackoff: true
204
+ });
205
+ await boss.createQueue(USAGE_QUEUE);
206
+ await boss.updateQueue(USAGE_QUEUE, { name: USAGE_QUEUE, deadLetter: USAGE_HELD_QUEUE });
207
+ // Audit retention queue: 0 = keep forever; the queue always exists so
208
+ // the schedule can be flipped later.
209
+ await boss.createQueue(AUDIT_PURGE_QUEUE);
210
+ if (retentionDays > 0) {
211
+ await boss.schedule(AUDIT_PURGE_QUEUE, '0 3 * * *');
212
+ } else {
213
+ await boss.unschedule(AUDIT_PURGE_QUEUE).catch(() => {});
214
+ }
215
+ await boss.createQueue(APIKEY_EXPIRY_QUEUE);
216
+ await boss.schedule(APIKEY_EXPIRY_QUEUE, '0 3 * * *');
217
+ await boss.createQueue(IDEMPOTENCY_PURGE_QUEUE);
218
+ await boss.schedule(IDEMPOTENCY_PURGE_QUEUE, '0 3 * * *');
219
+ // The tool's queues: same install section, same lock, declaration order.
220
+ for (const job of toolJobs) {
221
+ await boss.createQueue(job.queue);
222
+ if (!job.schedule) continue;
223
+ if (job.schedule.enabled !== false) {
224
+ await boss.schedule(job.queue, job.schedule.cron);
225
+ } else {
226
+ await boss.unschedule(job.queue).catch(() => {});
227
+ }
228
+ }
229
+ // Orphaned-user GC: 0 = disabled; the queue always exists so the
230
+ // schedule can be flipped later (audit-purge pattern).
231
+ await boss.createQueue(ORPHAN_USER_PURGE_QUEUE);
232
+ if (orphanRetentionHours > 0) {
233
+ await boss.schedule(ORPHAN_USER_PURGE_QUEUE, '0 3 * * *');
234
+ } else {
235
+ await boss.unschedule(ORPHAN_USER_PURGE_QUEUE).catch(() => {});
236
+ }
237
+ });
238
+
239
+ // Usage events flow through the durable queue (at-least-once); the
240
+ // downstream sink (no-op locally, the rail in cloud) dedupes by ULID.
241
+ // A downstream that takes batches (the hub poster) gets the drained
242
+ // batch whole; a throw fails every job of it back to the queue's retry.
243
+ //
244
+ // A batch downstream may answer an outcome naming events it HELD rather
245
+ // than posted (PRDCT-2644: an `occurredAt` outside the hub's window).
246
+ // Each is sent to the held queue, which re-drives it here after
247
+ // `heldDelaySeconds`, forever, as it does a retry-budget hold: a
248
+ // future-dated event is posted once its time has come, a stale one stays
249
+ // held until an operator acts. No event the hub would refuse on its date
250
+ // is ever posted, and none is dropped.
251
+ await boss.work<UsageEvent>(USAGE_QUEUE, { batchSize: 50 }, async (jobs) => {
252
+ if (downstreamUsage.emitBatch) {
253
+ const outcome = await downstreamUsage.emitBatch(jobs.map((job) => job.data));
254
+ if (outcome && outcome.held.length > 0) {
255
+ const heldIds = new Set(outcome.held.map((h) => h.id));
256
+ const heldJobs = jobs.filter((job) => heldIds.has(job.data.id));
257
+ for (const job of heldJobs) {
258
+ await boss.send(USAGE_HELD_QUEUE, job.data, { singletonKey: windowHoldKey(job.data.id) });
259
+ }
260
+ logger.warn(
261
+ {
262
+ ids: heldJobs.map((job) => job.data.id),
263
+ reason: 'occurred_at_window',
264
+ retryAfterSeconds: usageRetry.heldDelaySeconds
265
+ },
266
+ 'usage: events outside the hub’s occurredAt window — held, never posted, re-driven after the hold; check this instance’s clock (usage_events_held_total{reason="occurred_at_window"})'
267
+ );
268
+ // Counted once the sends are in, for the reason the held worker
269
+ // below gives: a send that throws fails this batch back to its
270
+ // retry, which would count the held events again.
271
+ held.inc({ reason: 'occurred_at_window' }, heldJobs.length);
272
+ }
273
+ } else {
274
+ for (const job of jobs) {
275
+ await downstreamUsage.emit(job.data);
276
+ }
277
+ }
278
+ logger.debug({ count: jobs.length }, 'usage events flushed');
279
+ });
280
+
281
+ // The budget's end is a HOLD, never a drop (PRDCT-2635): pg-boss moves a
282
+ // batch that exhausted its retries here; each event is logged where an
283
+ // operator sees it, counted on /metrics, and re-sent to the usage queue
284
+ // after the hold — round after round, until the hub accepts it. An
285
+ // outage of a day, an older hub, a wrong secret: the usage waits.
286
+ //
287
+ // Two kinds of job land here, told apart by the singleton key: pg-boss's
288
+ // dead-letter copy of a batch that ran out of retries carries none, the
289
+ // usage worker's window hold (PRDCT-2644) carries `windowHoldKey`. A
290
+ // window hold was logged and counted where it was held, so it is only
291
+ // re-driven here: logging or counting it again would name it a
292
+ // retry-budget hold, which it is not.
293
+ await boss.work<UsageEvent>(USAGE_HELD_QUEUE, { batchSize: 50, includeMetadata: true }, async (jobs) => {
294
+ const budget = jobs.filter((job) => !isWindowHoldKey(job.singletonKey));
295
+ if (budget.length > 0) {
296
+ logger.error(
297
+ {
298
+ count: budget.length,
299
+ ids: budget.map((job) => job.data.id),
300
+ retryAfterSeconds: usageRetry.heldDelaySeconds
301
+ },
302
+ 'usage: events exhausted the retry budget — held, re-driven after the hold; check the hub, HUB_CLIENT_ID/HUB_CLIENT_SECRET and the instance’s registry entry (usage_events_held_total)'
303
+ );
304
+ }
305
+ for (const job of jobs) {
306
+ await boss.send(USAGE_QUEUE, job.data, {
307
+ ...usageSendOptions(job.data, usageRetry),
308
+ startAfter: usageRetry.heldDelaySeconds
309
+ });
310
+ }
311
+ // Counted once the re-sends are in: a re-send that throws midway
312
+ // fails the held batch back to its own retries, which re-send the
313
+ // whole batch (the hub answers duplicate for what already went) and
314
+ // would count it again if the count came first.
315
+ if (budget.length > 0) held.inc({ reason: 'retry_budget' }, budget.length);
316
+ });
317
+
318
+ // Audit retention: a nightly purge keeps audit_log from growing without
319
+ // bound (machine reads land a row each — see audit/service.ts).
320
+ await boss.work(AUDIT_PURGE_QUEUE, async () => {
321
+ if (retentionDays <= 0) return;
322
+ // Delete in bounded batches: one huge DELETE would seq-scan and blow the
323
+ // pool's statement_timeout on a large table (retention then silently
324
+ // never runs). Each batch touches at most BATCH rows (fast, index-backed
325
+ // by audit_log_created_idx) and loops until the tail is drained.
326
+ const BATCH = 5000;
327
+ let total = 0;
328
+ for (let i = 0; i < 10_000; i++) {
329
+ const res = await db.execute(sql`
330
+ DELETE FROM audit_log
331
+ WHERE id IN (
332
+ SELECT id FROM audit_log
333
+ WHERE created_at < now() - make_interval(days => ${retentionDays})
334
+ ORDER BY created_at
335
+ LIMIT ${BATCH}
336
+ )
337
+ `);
338
+ const deleted = res.rowCount ?? 0;
339
+ total += deleted;
340
+ if (deleted < BATCH) break;
341
+ }
342
+ logger.info({ retentionDays, deleted: total }, 'audit retention purge ran');
343
+ });
344
+
345
+ // API-key expiry: resolution already rejects expired keys fail-closed —
346
+ // this nightly sweep only normalizes them to the visible terminal state
347
+ // (revoked_at = expires_at). api_keys is a tiny bounded table, so one
348
+ // unbatched UPDATE is fine (the audit-purge batching lesson applies to
349
+ // unbounded tables only).
350
+ await boss.work(APIKEY_EXPIRY_QUEUE, async () => {
351
+ const res = await db.execute(sql`
352
+ UPDATE api_keys
353
+ SET revoked_at = expires_at
354
+ WHERE expires_at < now() AND revoked_at IS NULL
355
+ `);
356
+ logger.info({ retired: res.rowCount ?? 0 }, 'api key expiry sweep ran');
357
+ });
358
+
359
+ // Idempotency replay rows carry a 24h TTL (a protocol constant, the
360
+ // Stripe convention — not an env knob), so the table can never accumulate
361
+ // beyond a day's create traffic: one unbatched, index-backed DELETE is
362
+ // fine (the audit-purge batching lesson applies to unbounded tables only).
363
+ await boss.work(IDEMPOTENCY_PURGE_QUEUE, async () => {
364
+ const res = await db.execute(sql`
365
+ DELETE FROM idempotency_keys WHERE expires_at < now()
366
+ `);
367
+ logger.info({ deleted: res.rowCount ?? 0 }, 'idempotency purge ran');
368
+ });
369
+
370
+ // The tool's pollers, registered where they always were: after the
371
+ // chassis purges, before the orphan GC. One job per fetch (pg-boss's
372
+ // default batch), its payload and its key handed to the handler: a timer
373
+ // (`./timers.ts`) rides the same poller as a cron.
374
+ for (const job of toolJobs) {
375
+ await boss.work<object>(job.queue, { includeMetadata: true }, async (jobs) => {
376
+ for (const fetched of jobs) {
377
+ await job.handler(fetched.data ?? null, {
378
+ id: fetched.id,
379
+ queue: job.queue,
380
+ key: fetched.singletonKey ?? null
381
+ });
382
+ }
383
+ });
384
+ }
385
+
386
+ // Orphaned-user GC: setup-race losers, fail-closed SSO login strands
387
+ // (cloud), and any other path leave Better Auth `user` rows with NO
388
+ // workspace_members row — they can sign in but 401 everywhere, and they
389
+ // accumulate forever. Cloud makes some of them CREDENTIAL-BEARING: a
390
+ // fail-closed hub login strands a user + `antasphere` account row
391
+ // HOLDING a live encrypted offline grant (Better Auth writes tokens
392
+ // before the after-hook revokes the session), minted at an
393
+ // unauthenticated-reachable rate during a hub outage — so a hub link
394
+ // must NEVER protect a row from collection (that would accumulate
395
+ // dormant refresh families without bound).
396
+ //
397
+ // What distinguishes the LEGIT zero-membership user (the cloud operator
398
+ // pre-break-glass, a hub user whose last org was removed — both real
399
+ // dashboard users of the /me zero state) from the strand is the LIVE
400
+ // SESSION: the strand's was revoked by the fail-closed login; the legit
401
+ // user holds one. So the sweep collects zero-membership users with NO
402
+ // unexpired session, REGARDLESS of any provider link, plus two explicit
403
+ // exclusions: a live pending invitation (mid-onboarding) and the
404
+ // SUPERADMIN_EMAILS allowlist (the break-glass operator must survive
405
+ // even after their session lapses — ADR 010's "arm the allowlist"
406
+ // guidance, now enforced instead of advised).
407
+ //
408
+ // SAFETY INVARIANTS:
409
+ // - a user with ANY membership row — even a deactivated one — is a
410
+ // real member and is NEVER touched;
411
+ // - ── HARD CONSTRAINT (user-scoped federation) ──────────────────────
412
+ // the purge must NEVER delete an `antasphere` account row while
413
+ // leaving an `origin='hub'` membership row behind: that would make
414
+ // the reconciler's `no_link` branch — today unreachable for
415
+ // hub-origin principals, and the one unconditional fail-open —
416
+ // REACHABLE, i.e. a fail-open authorization hole. Held by
417
+ // construction: the only deletion here is the WHOLE user through
418
+ // Better Auth's internalAdapter.deleteUser (the exact path both GDPR
419
+ // delete surfaces use), whose FK cascade removes account rows AND
420
+ // membership rows together, and only ever for users the immediately
421
+ // preceding re-check proved to have ZERO membership rows. Never
422
+ // replace this with a partial cleanup (account rows, tokens) — whole
423
+ // user or nothing.
424
+ await boss.work(ORPHAN_USER_PURGE_QUEUE, async () => {
425
+ if (orphanRetentionHours <= 0) return; // 0 = disabled (schedule is off too)
426
+ const authCtx = await auth.$context;
427
+ // As ONE json parameter (never a JS array param — driver array
428
+ // serialization is not worth trusting in a deletion query).
429
+ const superadmins = JSON.stringify([...parseSuperadminEmails(env.SUPERADMIN_EMAILS)]);
430
+ const BATCH = 50;
431
+ let total = 0;
432
+ const sample: string[] = [];
433
+ for (let i = 0; i < 200; i++) {
434
+ // Candidates: zero memberships, past the grace period, NO live
435
+ // (unexpired) session, no LIVE pending invitation for their email —
436
+ // an orphan someone just re-invited is mid-onboarding, not garbage
437
+ // (the invitation row exists before its accept URL does, so this
438
+ // closes the scan-then-accept race for the invited case) — and not
439
+ // a superadmin-allowlisted address. The exclusions live in the
440
+ // query (not a JS skip): a skipped candidate would otherwise fill
441
+ // the batch forever and starve the real orphans behind it.
442
+ const res = await db.execute(sql`
443
+ SELECT u.id, u.email FROM "user" u
444
+ WHERE NOT EXISTS (
445
+ SELECT 1 FROM workspace_members wm WHERE wm.user_id = u.id
446
+ )
447
+ AND u.created_at < now() - make_interval(hours => ${orphanRetentionHours})
448
+ AND NOT EXISTS (
449
+ SELECT 1 FROM session s WHERE s.user_id = u.id AND s.expires_at > now()
450
+ )
451
+ AND NOT EXISTS (
452
+ SELECT 1 FROM invitations inv
453
+ WHERE lower(inv.email) = lower(u.email)
454
+ AND inv.accepted_at IS NULL
455
+ AND inv.revoked_at IS NULL
456
+ AND inv.expires_at > now()
457
+ )
458
+ -- BG-2: the allowlist protects exactly what break-glass ACCEPTS —
459
+ -- a VERIFIED allowlisted address. An unverified squat of an
460
+ -- allowlisted email is no operator and gets no shelter.
461
+ AND NOT EXISTS (
462
+ SELECT 1 FROM jsonb_array_elements_text(${superadmins}::jsonb) AS sa(email)
463
+ WHERE sa.email = lower(u.email) AND u.email_verified
464
+ )
465
+ -- CLOUD-3: the setup operator (instance_settings.operator_user_id)
466
+ -- is NEVER an orphan — on cloud they hold no membership by
467
+ -- design, and deleting them loses the only local credential and
468
+ -- the only break-glass identity. Durable, allowlist or not.
469
+ AND u.id IS DISTINCT FROM (SELECT operator_user_id FROM instance_settings LIMIT 1)
470
+ LIMIT ${BATCH}
471
+ `);
472
+ const rows = res.rows as unknown as Array<{ id: string; email: string }>;
473
+ if (rows.length === 0) break;
474
+ let progressed = 0;
475
+ for (const row of rows) {
476
+ // Final re-check immediately before the delete: a membership OR a
477
+ // live session (the user signing in mid-sweep) can appear between
478
+ // the candidate scan and here — either one disqualifies.
479
+ const still = await db.execute(sql`
480
+ SELECT 1 FROM workspace_members WHERE user_id = ${row.id}
481
+ UNION ALL
482
+ SELECT 1 FROM session WHERE user_id = ${row.id} AND expires_at > now()
483
+ LIMIT 1
484
+ `);
485
+ if ((still.rows?.length ?? 0) > 0) continue;
486
+ try {
487
+ await authCtx.internalAdapter.deleteUser(row.id);
488
+ await authCtx.internalAdapter.deleteUserSessions(row.id);
489
+ total += 1;
490
+ progressed += 1;
491
+ if (sample.length < 20) sample.push(row.email);
492
+ } catch (err) {
493
+ // Skip and keep sweeping: a failed delete (e.g. the 0009
494
+ // last-owner trigger backstopping the pathological race where
495
+ // the user gained a sole-owner membership mid-sweep) aborts
496
+ // THIS user only.
497
+ logger.warn({ err, userId: row.id }, 'orphan purge: skipping user (delete failed)');
498
+ }
499
+ }
500
+ if (rows.length < BATCH) break;
501
+ if (progressed === 0) break; // every candidate failed/was skipped — no hot loop
502
+ }
503
+ if (total > 0) {
504
+ // INSTANCE-attributed system row (workspace_id NULL, ADR 014):
505
+ // orphans belong to no workspace, so no workspace's audit trail is
506
+ // the honest home — the row is an operator-level record.
507
+ await audit.write({
508
+ workspaceId: null,
509
+ principal: null, // → actorVia 'system'
510
+ action: 'user.orphan_purge',
511
+ resourceType: 'user',
512
+ metadata: { deleted: total, retentionHours: orphanRetentionHours, sample }
513
+ });
514
+ }
515
+ logger.info({ retentionHours: orphanRetentionHours, deleted: total }, 'orphan user purge ran');
516
+ });
517
+ } else {
518
+ // api role: migrate:false start() runs no DDL and creates no queues — it
519
+ // only verifies the pgboss schema exists ('pg-boss is not installed' on a
520
+ // fresh DB until an all|worker boot installs it; internal/scale-drill-findings.md), so it
521
+ // needs neither the install lock nor DDL rights.
522
+ await boss.start();
523
+ }
524
+
525
+ return {
526
+ boss,
527
+ timers: new PgBossTimers(boss, pool, logger),
528
+ stop: async () => {
529
+ await boss.stop({ graceful: true, wait: true });
530
+ },
531
+ promMetrics: [held]
532
+ };
533
+ }
534
+
535
+ /**
536
+ * The UsageSink bound into the registry: durable, batched emission via
537
+ * pg-boss. Domain code calls emit() and never blocks on the downstream.
538
+ */
539
+ export interface UsageRetry {
540
+ /** Retries before a batch is held (pg-boss `retryLimit`). */
541
+ limit: number;
542
+ /** The first retry's delay in seconds, doubled at each retry (`retryDelay` + `retryBackoff`). */
543
+ delaySeconds: number;
544
+ /** How long a held event waits before its next round through the queue. */
545
+ heldDelaySeconds: number;
546
+ }
547
+
548
+ /**
549
+ * The queue's retry budget: ten retries with exponential backoff from thirty
550
+ * seconds, about eight and a half hours in all — a hub deploy (the hub
551
+ * deploys first, and answers 404 until it has) or an outage of an afternoon
552
+ * costs nothing but time. The budget's END is a hold, not a loss
553
+ * (PRDCT-2635): the batch moves to `USAGE_HELD_QUEUE`, is logged and
554
+ * counted, and comes back through the queue an hour later, and an hour
555
+ * after that, until the hub accepts it. At-least-once is the posture; the
556
+ * hub dedupes by ULID.
557
+ */
558
+ export const DEFAULT_USAGE_RETRY: UsageRetry = { limit: 10, delaySeconds: 30, heldDelaySeconds: 3600 };
559
+
560
+ /**
561
+ * The ONE statement of how a usage event is sent: the sink and the held
562
+ * queue's re-drive share it. The dead letter (where the batch goes when the
563
+ * budget runs out: held, never dropped) is the QUEUE's setting, installed
564
+ * by the worker boot, so a send names none and an api-role replica needs
565
+ * nothing to exist beyond the usage queue itself.
566
+ */
567
+ export function usageSendOptions(event: UsageEvent, retry: UsageRetry): PgBoss.SendOptions {
568
+ return {
569
+ // The event's ULID, for a reader of the queue table; on a standard
570
+ // pg-boss queue a singleton key enforces nothing (verifier round 1), and
571
+ // idempotency is the receiver's job anyway (dedupe by ULID, per contract).
572
+ singletonKey: event.id,
573
+ retryLimit: retry.limit,
574
+ retryDelay: retry.delaySeconds,
575
+ retryBackoff: true
576
+ };
577
+ }
578
+
579
+ export class PgBossUsageSink implements UsageSink {
580
+ constructor(
581
+ private readonly boss: PgBoss,
582
+ private readonly logger: Logger,
583
+ private readonly retry: UsageRetry = DEFAULT_USAGE_RETRY
584
+ ) {}
585
+
586
+ async emit(event: UsageEvent): Promise<void> {
587
+ try {
588
+ await this.boss.send(USAGE_QUEUE, event, usageSendOptions(event, this.retry));
589
+ } catch (err) {
590
+ // Usage must never break the request path.
591
+ this.logger.error({ err, meter: event.meter }, 'usage emit failed');
592
+ }
593
+ }
594
+ }