@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
package/src/boot.ts ADDED
@@ -0,0 +1,953 @@
1
+ import { migrationStatus, runMigrations } from '@antasphere/chassis-db/migrate';
2
+ import { and, count, eq, isNull } from 'drizzle-orm';
3
+ import type { Hono } from 'hono';
4
+ import { join } from 'node:path';
5
+ import type { z } from 'zod';
6
+ import { APIError } from 'better-auth/api';
7
+ import { AccountDeletionService, LastOwnerError } from './accounts/index.js';
8
+ import { ErasureLog } from './accounts/index.js';
9
+ import { createApiApp } from './api/create-api.js';
10
+ import { buildPepperRegistry } from './apikeys/index.js';
11
+ import { ApiKeyService } from './apikeys/index.js';
12
+ import { createApp } from './app.js';
13
+ import { hubSubjectResolver } from './entitlements/hub-subject.js';
14
+ import { AuditService } from './audit/index.js';
15
+ import { createEmailDriver, emailAssetsAt, setEmailAssets } from './email/index.js';
16
+ import {
17
+ buildChangeEmailConfirmEmail,
18
+ buildOtpEmail,
19
+ buildPasswordResetEmail,
20
+ buildVerifyEmailEmail
21
+ } from './email/index.js';
22
+ import { hubConfig, parseEnv } from './env.js';
23
+ import { createAuth, mcpResourceUrl, type AccountEvent } from './identity/index.js';
24
+ import { HubSsoService } from './identity/index.js';
25
+ import { HubGrantService } from './identity/index.js';
26
+ import { HubLogoutService } from './identity/index.js';
27
+ import { hubApiResource, HubUserClient } from './identity/index.js';
28
+ import { DEFAULT_FEDERATION_DIALS, HubOrgReconciler, type HubFederationDials } from './identity/index.js';
29
+ import { OauthJwtVerifier } from './identity/index.js';
30
+ import { preflightSigningKey } from './identity/index.js';
31
+ import { mcpRoutes } from './mcp/index.js';
32
+ import { wellKnownRoutes } from './routes/index.js';
33
+ import { instanceSettings, user as userTable, workspaceMembers, workspaces } from '@antasphere/chassis-db';
34
+ import { FileService } from './files/index.js';
35
+ import { createJobs, DEFAULT_USAGE_RETRY, PgBossUsageSink, type UsageRetry } from './jobs/index.js';
36
+ import {
37
+ assertToolEntitlements,
38
+ describeHubClockSkew,
39
+ EMPTY_TOOL_ENTITLEMENTS,
40
+ EntitlementProfiles,
41
+ hubClockSkewMs,
42
+ HubCreditCheck,
43
+ HubMachineToken,
44
+ HubUsagePoster
45
+ } from './entitlements/index.js';
46
+ import { createLogger } from './logger.js';
47
+ import { createStorageDriver } from './storage/index.js';
48
+ import { createRateLimiters, makeClientIp, rateLimit } from './middleware/index.js';
49
+ import { hstsValue } from './middleware/index.js';
50
+ import { createMetrics } from './observability/index.js';
51
+ import { createOtel } from './observability/index.js';
52
+ import { bindEditionSeams } from './platform/index.js';
53
+ import { AllowAllEntitlements } from './platform/index.js';
54
+ import { EventBus } from './platform/index.js';
55
+ import { LocalIdentityProvider } from './platform/index.js';
56
+ import { createRegistry } from './platform/index.js';
57
+ import { NoopUsageSink } from './platform/index.js';
58
+ import { WorkspaceService } from './platform/index.js';
59
+ import { clearGeneratedSetupToken, resolveAuthSecret, resolveSetupToken } from './util/index.js';
60
+ import { createRuntimeState } from './util/index.js';
61
+ import {
62
+ assertToolCopy,
63
+ assertToolIdentity,
64
+ type BootOverrides,
65
+ type BootResult,
66
+ type PlatformCore,
67
+ type ServiceCore,
68
+ type ToolDefinition
69
+ } from './tool-definition.js';
70
+
71
+ /**
72
+ * The boot sequence, in its locked order: env → logger → db probe → secret →
73
+ * migrations under advisory lock → app assembly → ready.
74
+ */
75
+ export async function bootPlatform<
76
+ TEnvShape extends z.ZodRawShape,
77
+ TDomain,
78
+ TBuckets extends string,
79
+ TEvents,
80
+ TToolOverrides
81
+ >(
82
+ tool: ToolDefinition<TEnvShape, TDomain, TBuckets, TEvents, TToolOverrides>,
83
+ source: NodeJS.ProcessEnv = process.env,
84
+ overrides: BootOverrides<TToolOverrides> = {}
85
+ ): Promise<BootResult<TEnvShape, TDomain, TEvents>> {
86
+ assertToolIdentity(tool.identity);
87
+ assertToolCopy(tool.copy);
88
+ const env = parseEnv<TEnvShape>(source, {
89
+ version: tool.runtime.version,
90
+ ...(tool.env ? { extension: tool.env } : {})
91
+ });
92
+ const logger = createLogger(env);
93
+ const state = createRuntimeState();
94
+
95
+ logger.info({ serviceRole: env.SERVICE_ROLE, nodeEnv: env.NODE_ENV }, 'booting');
96
+
97
+ state.reason = 'connecting to database';
98
+ const db = tool.db(env.DATABASE_URL);
99
+ // createDb already survives an idle connection ended by the server (a
100
+ // Postgres restart); this listener is only what makes it visible in the log.
101
+ db.pool.on('error', (err) =>
102
+ logger.warn(
103
+ { err },
104
+ 'postgres ended an idle pooled connection; the pool opens a new one on the next query'
105
+ )
106
+ );
107
+ await db.pool.query('SELECT 1');
108
+ logger.info('database reachable');
109
+
110
+ const authSecret = await resolveAuthSecret(env.AUTH_SECRET, env.DATA_DIR, logger);
111
+
112
+ const migrationsFolder = tool.runtime.findMigrationsDir();
113
+ let migrationsPending = false;
114
+ /** The migration-stage reason /readyz must keep reporting (later stages overwrite state.reason). */
115
+ let readinessRefusal: string | null = null;
116
+ // Hash-based status FIRST, on every boot (OPS-6, PRDCT-1357): a database
117
+ // carrying migrations this image does not ship was migrated by a NEWER
118
+ // image — a rollback / `:next` behind `:latest`. drizzle's migrator would
119
+ // happily consider it up to date (it compares timestamps), and a
120
+ // count-based status called it "current". Refuse readiness instead: the
121
+ // schema is ahead of the code, and the newer version's share links and
122
+ // rows are exactly what the older code would corrupt.
123
+ const preStatus = await migrationStatus({ connectionString: env.DATABASE_URL, migrationsFolder });
124
+ if (preStatus.downgrade) {
125
+ migrationsPending = true;
126
+ state.reason =
127
+ `database is AHEAD of this image: ${preStatus.unknownApplied} applied migration(s) this ` +
128
+ `version does not ship (a downgrade, or edited migration files) — deploy the version that applied them`;
129
+ readinessRefusal = state.reason;
130
+ logger.error({ status: preStatus }, 'refusing readiness: ' + state.reason);
131
+ } else if (env.AUTO_MIGRATE) {
132
+ state.reason = 'applying migrations';
133
+ await runMigrations({
134
+ connectionString: env.DATABASE_URL,
135
+ migrationsFolder,
136
+ log: (msg) => logger.info({ scope: 'migrate' }, msg)
137
+ });
138
+ } else {
139
+ const status = preStatus;
140
+ migrationsPending = status.pending;
141
+ if (status.pending) {
142
+ state.reason = `migrations pending (${status.applied}/${status.onDisk} applied) and AUTO_MIGRATE=false`;
143
+ readinessRefusal = state.reason;
144
+ logger.error({ status }, 'refusing readiness: run migrations manually or set AUTO_MIGRATE=true');
145
+ // The process stays up (healthz green) but /readyz keeps failing.
146
+ } else {
147
+ logger.info({ status }, 'migrations current (AUTO_MIGRATE=false)');
148
+ }
149
+ }
150
+
151
+ // R7 edition-flip guard (internal/federation.md): EDITION selects the identity
152
+ // binding, so flipping it under an already-set-up instance silently changes
153
+ // who can log in and where workspace ownership is asserted. Setup stamps
154
+ // the instance's edition; a boot whose env EDITION differs refuses to start
155
+ // unless the operator explicitly acknowledges with EDITION_CHANGE_ALLOWED=true
156
+ // (which re-stamps and proceeds). A fresh database (no instance row yet)
157
+ // boots under any edition — cloud instances start from a fresh DB. The
158
+ // stamp is compared whenever it is READABLE — including while migrations
159
+ // are pending (readiness is red then, but the API still serves, so a
160
+ // skipped guard would be one whole mis-bound boot). Only a schema that
161
+ // predates the stamp column (migration 0020, AUTO_MIGRATE=false) has no
162
+ // stamp to compare; the first migrated boot enforces the guard.
163
+ let stampedEdition: string | undefined;
164
+ try {
165
+ const [instanceRow] = await db.db
166
+ .select({ edition: instanceSettings.edition })
167
+ .from(instanceSettings)
168
+ .limit(1);
169
+ stampedEdition = instanceRow?.edition;
170
+ } catch (cause) {
171
+ if (!migrationsPending) throw cause;
172
+ // Pending migrations + unreadable stamp: the edition column (or the
173
+ // table) is not migrated in yet on this database — nothing to compare.
174
+ }
175
+ if (stampedEdition !== undefined && stampedEdition !== env.EDITION) {
176
+ if (!env.EDITION_CHANGE_ALLOWED) {
177
+ const message =
178
+ `refusing to boot: EDITION=${env.EDITION} but this instance was set up as ` +
179
+ `'${stampedEdition}'. An edition flip on a populated instance changes identity ` +
180
+ `semantics for existing users and workspaces. If this is intentional, set ` +
181
+ `EDITION_CHANGE_ALLOWED=true for ONE boot to re-stamp it (internal/federation.md).`;
182
+ logger.error({ stamped: stampedEdition, env: env.EDITION }, message);
183
+ await db.pool.end(); // clean refusal — no leaked pool for the caller
184
+ throw new Error(message);
185
+ }
186
+ // Pre-flight (EDIT-1/3/4, PRDCT-1356): the acknowledgement alone is not
187
+ // enough — a flip that migrates nothing leaves the cloud enforcement
188
+ // model not applying to pre-flip data, so the flip is REFUSED unless the
189
+ // instance is already in the shape the target edition requires.
190
+ const refuseFlip = async (reason: string) => {
191
+ const message =
192
+ `refusing the edition flip '${stampedEdition}' → '${env.EDITION}' (EDITION_CHANGE_ALLOWED=true): ` +
193
+ reason;
194
+ logger.error({ stamped: stampedEdition, env: env.EDITION }, message);
195
+ await db.pool.end();
196
+ throw new Error(message);
197
+ };
198
+ if (stampedEdition === 'cloud' && env.EDITION === 'oss') {
199
+ // The reverse flip would leave every hub-minted session, OAuth grant
200
+ // and hub-origin membership behind as unrevocable LOCAL credentials
201
+ // (EDIT-4). Rebuild from a fresh database instead.
202
+ await refuseFlip(
203
+ 'a cloud instance cannot be flipped back to oss — hub-minted credentials would become ' +
204
+ 'unrevocable local ones (internal/federation.md). Set up a fresh oss instance and restore data into it.'
205
+ );
206
+ }
207
+ if (env.EDITION === 'cloud') {
208
+ const [legacy] = await db.db
209
+ .select({ n: count() })
210
+ .from(workspaces)
211
+ .where(isNull(workspaces.centralAccountId));
212
+ if ((legacy?.n ?? 0) > 0) {
213
+ // EDIT-1: a workspace without a hub projection is outside every
214
+ // hub gate (HubLiveGate returns ok before any hub read) — the whole
215
+ // cloud enforcement model would never apply to it.
216
+ await refuseFlip(
217
+ `${legacy?.n} workspace(s) carry no hub projection (central_account_id IS NULL). ` +
218
+ 'Project each one at the hub (or delete it) before flipping — cloud enforcement cannot cover an unprojected workspace.'
219
+ );
220
+ }
221
+ const [unverified] = await db.db
222
+ .select({ n: count() })
223
+ .from(userTable)
224
+ .where(eq(userTable.emailVerified, false));
225
+ if ((unverified?.n ?? 0) > 0) {
226
+ // EDIT-3: hub-only login links the trusted provider onto a VERIFIED
227
+ // local email only; every unverified user would be locked out.
228
+ await refuseFlip(
229
+ `${unverified?.n} user(s) have an unverified email and would be locked out under hub-only login. ` +
230
+ 'Have them verify (or remove them) before flipping.'
231
+ );
232
+ }
233
+ }
234
+ await db.db.update(instanceSettings).set({ edition: env.EDITION });
235
+ logger.warn(
236
+ { from: stampedEdition, to: env.EDITION },
237
+ 'edition re-stamped (EDITION_CHANGE_ALLOWED=true) — unset the flag again after this boot'
238
+ );
239
+ } else if (env.EDITION_CHANGE_ALLOWED) {
240
+ // A leftover acknowledgement must never rot silently: while the flag
241
+ // stays set, the guard above is disarmed — any future EDITION change
242
+ // would re-stamp without refusing. Say so on every boot.
243
+ logger.warn(
244
+ 'EDITION_CHANGE_ALLOWED=true but no edition change is pending — unset it: while it stays ' +
245
+ 'set, the R7 edition-flip guard is disarmed and a future EDITION change re-stamps ' +
246
+ 'without refusal (internal/federation.md)'
247
+ );
248
+ }
249
+
250
+ // The first-boot claim credential (PRDCT-1347): SETUP_TOKEN, or a token
251
+ // generated into the data volume while the instance is unclaimed. A boot
252
+ // that cannot read the stamp (pending migrations, pre-stamp schema) is
253
+ // treated as unclaimed — generating a token an already-set-up instance
254
+ // never needs is harmless; skipping it on an unclaimed one is the hole.
255
+ const setupToken = await resolveSetupToken(
256
+ env.SETUP_TOKEN,
257
+ env.DATA_DIR,
258
+ stampedEdition !== undefined,
259
+ logger
260
+ );
261
+
262
+ // OAuth signing-key preflight (ADR 023): one decrypt of the key the jwt
263
+ // plugin would sign with. A rotated AUTH_SECRET leaves the stored JWKS
264
+ // private key undecryptable and — with no rotationInterval — never
265
+ // replaced, so every token mint would 500 while authorize still 302s.
266
+ // Refuse to boot instead, with the remedies in the message. Skipped only
267
+ // while migrations are pending (the jwks table may not exist yet, and the
268
+ // instance already refuses readiness in that state).
269
+ if (!migrationsPending) {
270
+ state.reason = 'verifying OAuth signing key';
271
+ const preflight = await preflightSigningKey(db.db, authSecret);
272
+ if (!preflight.ok) {
273
+ logger.fatal({ kid: preflight.kid }, preflight.reason);
274
+ // Fail-fast means clean-fast: release the pool this boot opened so the
275
+ // refusing process (and the integration test that pins it) never
276
+ // strands an open connection.
277
+ await db.pool.end().catch(() => {});
278
+ throw new Error('OAuth signing-key preflight failed — see the preceding log line');
279
+ }
280
+ }
281
+
282
+ // Email first: whether it delivers decides whether email-OTP login,
283
+ // self-serve password reset, and self-serve email change exist.
284
+ const email = overrides.email ?? createEmailDriver(env, logger);
285
+ // the mails' band and mark are served from the dashboard's static folder
286
+ setEmailAssets(emailAssetsAt(env.PUBLIC_BASE_URL));
287
+
288
+ // Audit is built before auth so credential events on Better-Auth-native
289
+ // routes (password reset/change) can be recorded through it.
290
+ const audit = new AuditService(db.db, logger);
291
+
292
+ /**
293
+ * Best-effort audit for a Better-Auth credential event. Credential events
294
+ * are USER-level, so the row lands in every workspace the user is an
295
+ * ACTIVE member of (ADR 014) — each workspace's trail records its own
296
+ * members' password/2FA changes, and a workspace the user was deactivated
297
+ * from records nothing; a single-workspace instance gets exactly one row,
298
+ * as before.
299
+ */
300
+ const auditAccountEvent = async (event: AccountEvent, userId: string) => {
301
+ const rows = await db.db
302
+ .select({ workspaceId: workspaceMembers.workspaceId })
303
+ .from(workspaceMembers)
304
+ .where(and(eq(workspaceMembers.userId, userId), eq(workspaceMembers.isActive, true)));
305
+ for (const row of rows) {
306
+ await audit.write({
307
+ workspaceId: row.workspaceId,
308
+ principal: { userId, via: 'session' },
309
+ action: `user.${event}`,
310
+ resourceType: 'user',
311
+ resourceId: userId
312
+ });
313
+ }
314
+ };
315
+
316
+ // Account deletion: shared by the self-service /delete-user hooks below
317
+ // and the admin DELETE /members/{id} route. No storage dep — files are
318
+ // workspace data and survive the delete (ADR 006). The connection string
319
+ // feeds the dedicated advisory-lock clients that serialize last-owner
320
+ // removals (the migration-lock pattern).
321
+ const erasureLog = new ErasureLog(env.DATA_DIR, logger, authSecret);
322
+ const accountDeletion = new AccountDeletionService(db.db, audit, logger, env.DATABASE_URL, erasureLog);
323
+
324
+ // The event bus exists before auth so identity-layer hooks can publish:
325
+ // `user.created` fires from Better Auth's databaseHooks.user.create.after,
326
+ // covering EVERY account entrance by construction (setup, invitation
327
+ // accept, collaborator claim, future SSO JIT) — never from individual
328
+ // call sites.
329
+ const events = new EventBus<TEvents>(logger);
330
+
331
+ // The cloud edition's hub SSO binding (internal/federation.md): constructed
332
+ // iff EDITION=cloud — hubConfig() is the single switch, so an oss boot
333
+ // provably instantiates no SSO surface. Everything hub-SSO hangs off this
334
+ // one object: the genericOAuth provider inside createAuth, the per-login
335
+ // projection after-hook, and the login-scope wrap around the auth mount.
336
+ const hub = hubConfig(env);
337
+ // THE seam constant: the RFC 8707 `resource` for BOTH the SSO code
338
+ // exchange and every refresh — `<hub>/mcp`, so the tokens the tool holds
339
+ // are HUB-audienced and callable at hub /api/v1 as the user. Flipping it
340
+ // to null (if the hub ever accepts opaque tokens) is this one line.
341
+ const hubTokenResource = hub ? hubApiResource(hub.issuerUrl) : null;
342
+ const hubSso = hub
343
+ ? new HubSsoService({
344
+ db: db.db,
345
+ logger,
346
+ issuerUrl: hub.issuerUrl,
347
+ clientId: hub.clientId,
348
+ clientSecret: hub.clientSecret,
349
+ // Own resource URL — always derived from PUBLIC_BASE_URL, never
350
+ // configured. Pins H3 connect tokens only; login tokens are
351
+ // hub-audienced via tokenResource.
352
+ resourceUrl: mcpResourceUrl(env.PUBLIC_BASE_URL),
353
+ tokenResource: hubTokenResource,
354
+ publicBaseUrl: env.PUBLIC_BASE_URL
355
+ })
356
+ : undefined;
357
+
358
+ // ONE source for the never-trusted origins: Better Auth here, the cross-site
359
+ // guard of the API app below.
360
+ const untrustedOrigins = tool.api.untrustedOrigins?.(env) ?? [];
361
+
362
+ // Identity + seams: local defaults, swappable at this one point.
363
+ // The product's name and footer line in the four mails Better Auth sends (the fifth, the invitation, is api/invitations.ts).
364
+ const brand = { name: tool.identity.displayName, tagline: tool.copy.mail.tagline };
365
+ const auth = createAuth({
366
+ db: db.db,
367
+ env,
368
+ authSecret,
369
+ // The tool's values for the two generic-by-parameter points of the
370
+ // identity module: the published OAuth scope list (order included) and
371
+ // the origins that are never trusted (e.g. a tool's viewer origin,
372
+ // where author-controlled script runs, PRDCT-1352). None = nothing installed.
373
+ oauthScopes: tool.scopes.oauth,
374
+ untrustedOrigins,
375
+ hubSso,
376
+ // Demo sign-in (the demo pass spec): the redeem endpoint's deps, only while the switch is on.
377
+ ...(env.DEMO_SIGN_IN
378
+ ? { demoPass: { db: db.db, emailDomains: env.DEMO_SIGN_IN_EMAIL_DOMAINS, audit } }
379
+ : {}),
380
+ onAccountEvent: auditAccountEvent,
381
+ onUserCreated: (user) =>
382
+ events.chassisView().emit('user.created', { userId: user.id, email: user.email }),
383
+ beforeUserDelete: async (userId) => {
384
+ try {
385
+ await accountDeletion.beforeUserDelete(userId);
386
+ } catch (cause) {
387
+ // Better Auth turns APIError into the route's 400 response.
388
+ if (cause instanceof LastOwnerError) {
389
+ throw new APIError('BAD_REQUEST', { message: cause.message });
390
+ }
391
+ throw cause;
392
+ }
393
+ },
394
+ afterUserDelete: (user) => accountDeletion.afterUserDelete(user),
395
+ ...(email.delivers
396
+ ? {
397
+ sendOtp: async ({ email: to, otp, type }: { email: string; otp: string; type: string }) => {
398
+ const msg = buildOtpEmail({ brand, otp, type });
399
+ await email.send({ to, ...msg });
400
+ },
401
+ sendResetPassword: async ({ email: to, url }: { email: string; url: string; token: string }) => {
402
+ // Better Auth builds `url` as the API callback that 302s to the
403
+ // dashboard reset page with ?token — mail it verbatim.
404
+ const msg = buildPasswordResetEmail({
405
+ brand,
406
+ resetUrl: url,
407
+ expiresAt: new Date(Date.now() + 3600_000)
408
+ });
409
+ await email.send({ to, ...msg });
410
+ },
411
+ // Two-leg email change, leg 1 (verified users only): confirmation
412
+ // to the OLD address. Mail Better Auth's `url` verbatim.
413
+ sendChangeEmailConfirmation: async ({
414
+ email: to,
415
+ newEmail,
416
+ url
417
+ }: {
418
+ email: string;
419
+ newEmail: string;
420
+ url: string;
421
+ token: string;
422
+ }) => {
423
+ const msg = buildChangeEmailConfirmEmail({
424
+ brand,
425
+ newEmail,
426
+ confirmUrl: url,
427
+ expiresAt: new Date(Date.now() + 3600_000)
428
+ });
429
+ await email.send({ to, ...msg });
430
+ },
431
+ // Final leg of every email change: verification to the NEW address.
432
+ sendVerificationEmail: async ({
433
+ email: to,
434
+ url
435
+ }: {
436
+ email: string;
437
+ url: string;
438
+ token: string;
439
+ }) => {
440
+ const msg = buildVerifyEmailEmail({
441
+ brand,
442
+ verifyUrl: url,
443
+ expiresAt: new Date(Date.now() + 3600_000)
444
+ });
445
+ await email.send({ to, ...msg });
446
+ }
447
+ }
448
+ : {})
449
+ });
450
+
451
+ // OPS-3 (PRDCT-1357): replay the erasure tombstone before the app serves.
452
+ // A restore from an older dump brings erased users back; the tombstone
453
+ // (kept outside the dump, carried forward by restore.sh) says who must
454
+ // stay gone. Runs through the same Better Auth cascade every GDPR surface
455
+ // uses (memberships, sessions, accounts go with the user) and lands an
456
+ // instance-level audit row per replay. Skipped while migrations are
457
+ // pending (the tables may not exist; readiness is red anyway).
458
+ //
459
+ // PRDCT-1809: the replay runs under EVERY last-owner guard the subject is
460
+ // subject to (the same locks + re-check the HTTP surfaces use; the 0009
461
+ // trigger backstops), so a sole-owner subject is refused BEFORE any row is
462
+ // touched — Better Auth's cascade drops the account rows before the user
463
+ // row, and an unguarded refusal left a half-erased owner. A refused
464
+ // tombstone is a FAIL-CLOSED verdict: the subject asked to be forgotten
465
+ // and the product cannot decide who inherits their workspace, so the boot
466
+ // audits the refusal, keeps /readyz red with the reason, and CLOSES the
467
+ // service surface (state.closed → 503 everywhere but the probes) until an
468
+ // operator promotes another owner and restarts (docs/operations/
469
+ // backup-restore.md "Erasures hold across a restore"). Readiness alone is
470
+ // not a closure: the API keeps serving under a red /readyz.
471
+ if (!migrationsPending) {
472
+ state.reason = 'replaying erasure tombstones';
473
+ const authCtx = await auth.$context;
474
+ const { replayed, refused } = await erasureLog.replay(
475
+ async (userId) => {
476
+ const [row] = await db.db
477
+ .select({ id: userTable.id })
478
+ .from(userTable)
479
+ .where(eq(userTable.id, userId))
480
+ .limit(1);
481
+ return row !== undefined;
482
+ },
483
+ (userId) =>
484
+ accountDeletion.withAllLastOwnerGuards(userId, async () => {
485
+ await authCtx.internalAdapter.deleteUser(userId);
486
+ await authCtx.internalAdapter.deleteUserSessions(userId);
487
+ })
488
+ );
489
+ for (const userId of replayed) {
490
+ await audit.write({
491
+ workspaceId: null,
492
+ principal: null, // → actorVia 'system'
493
+ action: 'user.erasure_replayed',
494
+ resourceType: 'user',
495
+ resourceId: userId,
496
+ metadata: { reason: 'tombstoned user present after boot (restore from an older backup)' }
497
+ });
498
+ }
499
+ for (const r of refused) {
500
+ const lastOwner = r.cause instanceof LastOwnerError;
501
+ await audit.write({
502
+ workspaceId: null,
503
+ principal: null, // → actorVia 'system'
504
+ action: 'user.erasure_replay_refused',
505
+ resourceType: 'user',
506
+ resourceId: r.userId,
507
+ metadata: {
508
+ reason: lastOwner
509
+ ? 'tombstoned user is the last active owner of a workspace; the instance refuses to serve until another owner is promoted'
510
+ : 'tombstoned user could not be re-erased; the instance refuses to serve',
511
+ erasedAt: r.erasedAt
512
+ }
513
+ });
514
+ }
515
+ if (refused.length > 0) {
516
+ // The public reason stays COARSE: /readyz is unauthenticated and the
517
+ // subject asked to be forgotten, so their id lives in the log line and
518
+ // the audit row only. The remedy names the cause the operator faces.
519
+ const lastOwner = refused.filter((r) => r.cause instanceof LastOwnerError).length;
520
+ const other = refused.length - lastOwner;
521
+ const remedy = [
522
+ lastOwner > 0
523
+ ? `${lastOwner} subject(s) present again after a restore as the last active owner of a workspace — promote another member to owner, then restart`
524
+ : null,
525
+ other > 0
526
+ ? `${other} re-erasure(s) failed — see the container log, fix the cause, then restart`
527
+ : null
528
+ ]
529
+ .filter((s): s is string => s !== null)
530
+ .join('; ');
531
+ state.reason =
532
+ `refusing to serve: ${refused.length} erasure tombstone(s) could not be replayed: ${remedy} ` +
533
+ '(docs/operations/backup-restore.md, audit action user.erasure_replay_refused)';
534
+ readinessRefusal = state.reason;
535
+ state.closed = state.reason;
536
+ logger.error(
537
+ { refused: refused.map((r) => ({ userId: r.userId, lastOwner: r.cause instanceof LastOwnerError })) },
538
+ state.reason
539
+ );
540
+ }
541
+ }
542
+
543
+ // The live user-scoped federation stack (cloud only, internal/federation.md):
544
+ // each user's OWN hub grant (offline_access refresh token on the account
545
+ // row) → as-the-user `GET /orgs` reads → the org reconciler that the
546
+ // login pass, the live gate, and the unknown-workspace miss hook all
547
+ // share. Constructed AFTER createAuth because the grant encrypts tokens
548
+ // with Better Auth's own key material ((await auth.$context).secretConfig
549
+ // — equal to authSecret under our string-secret config; the fallback
550
+ // keeps the read total if a future Better Auth reshapes the context).
551
+ const hubDials: HubFederationDials = { ...DEFAULT_FEDERATION_DIALS, ...overrides.hubDials };
552
+ const hubGrant = hub
553
+ ? new HubGrantService({
554
+ db: db.db,
555
+ issuerUrl: hub.issuerUrl,
556
+ clientId: hub.clientId,
557
+ clientSecret: hub.clientSecret,
558
+ tokenResource: hubTokenResource,
559
+ key: async () =>
560
+ ((await auth.$context) as unknown as { secretConfig?: string }).secretConfig ?? authSecret,
561
+ connectionString: env.DATABASE_URL,
562
+ logger,
563
+ dials: {
564
+ accessSkewMs: hubDials.accessSkewMs,
565
+ tokenTimeoutMs: hubDials.tokenTimeoutMs,
566
+ // The RFC 7662 probe rides the token-endpoint budget: same hub,
567
+ // same hop, and it only runs after an unanswered presentation.
568
+ introspectTimeoutMs: hubDials.tokenTimeoutMs,
569
+ lockWatchdogMs: hubDials.lockWatchdogMs
570
+ }
571
+ })
572
+ : undefined;
573
+ // RP-initiated logout against the hub (SL-2, cloud only): builds the
574
+ // end-session URL /sso/logout returns. Same key seam as the grant store
575
+ // (it only decrypts if a future Better Auth starts encrypting id_tokens);
576
+ // discovery fetches ride the tight orgs timeout dial.
577
+ const hubLogout = hub
578
+ ? new HubLogoutService({
579
+ db: db.db,
580
+ issuerUrl: hub.issuerUrl,
581
+ publicBaseUrl: env.PUBLIC_BASE_URL,
582
+ key: async () =>
583
+ ((await auth.$context) as unknown as { secretConfig?: string }).secretConfig ?? authSecret,
584
+ logger,
585
+ discoveryTimeoutMs: hubDials.orgsTimeoutMs
586
+ })
587
+ : undefined;
588
+ // The ONE as-the-user hub client: the reconciler's GET /orgs reads and
589
+ // POST /workspaces' organization creation both present the user's own
590
+ // grant through it (never a second, hand-rolled token path).
591
+ const hubUserClient =
592
+ hub && hubGrant
593
+ ? new HubUserClient({
594
+ grant: hubGrant,
595
+ issuerUrl: hub.issuerUrl,
596
+ logger,
597
+ timeoutMs: hubDials.orgsTimeoutMs,
598
+ // A creation is one deliberate human act, not an identity-path
599
+ // read: it rides the token-endpoint budget, not the tight one.
600
+ createTimeoutMs: hubDials.tokenTimeoutMs
601
+ })
602
+ : undefined;
603
+ const hubReconciler =
604
+ hub && hubGrant && hubUserClient
605
+ ? new HubOrgReconciler({
606
+ db: db.db,
607
+ client: hubUserClient,
608
+ audit,
609
+ logger,
610
+ dials: hubDials,
611
+ onMembershipRemoval: tool.membershipRemoval,
612
+ ...(overrides.hubNow ? { now: overrides.hubNow } : {})
613
+ })
614
+ : undefined;
615
+ // The SSO login's fail-closed step 3 (assertLogin) runs THIS reconciler.
616
+ if (hubSso && hubReconciler) hubSso.bindReconciler(hubReconciler);
617
+ // The reconciler's CACHED pass (its TTL + retry throttle, so no caller can
618
+ // hammer the hub), run by two callers: the unknown-workspace retry of all
619
+ // three credential kinds (one pass + one re-lookup; the requested id is
620
+ // ignored), and the zero-membership /me (the live gate never reaches a
621
+ // principal-less session). undefined on oss.
622
+ const cachedReconcile = hubReconciler
623
+ ? async (userId: string) => {
624
+ await hubReconciler.reconcile(userId);
625
+ }
626
+ : undefined;
627
+
628
+ // Jobs: pg-boss (durable queue). The registry's UsageSink emits into it;
629
+ // the worker side hands batches to the downstream sink (no-op locally).
630
+ // A tool job may need a service built further down (the storage driver is
631
+ // probed later); it reads the domain through this holder at RUN time (nightly).
632
+ const domainRef: { current: TDomain | null } = { current: null };
633
+ // The billing rail's tool → hub machine channel (the spec, §6), cloud
634
+ // only: one client_credentials token on the instance's own hub client
635
+ // (scope usage:write), shared by the usage poster (the queue's downstream)
636
+ // and the entitlement profile reads. An oss boot constructs none of it.
637
+ const hubMachineToken =
638
+ hub && hubTokenResource
639
+ ? new HubMachineToken({
640
+ issuerUrl: hub.issuerUrl,
641
+ clientId: hub.clientId,
642
+ clientSecret: hub.clientSecret,
643
+ resource: hubTokenResource,
644
+ logger,
645
+ timeoutMs: hubDials.tokenTimeoutMs
646
+ })
647
+ : undefined;
648
+ const hubUsagePoster = hubMachineToken
649
+ ? new HubUsagePoster({ issuerUrl: hub!.issuerUrl, token: hubMachineToken, logger })
650
+ : undefined;
651
+ // The usage queue's retry budget and its hold (PRDCT-2635): stated once,
652
+ // shared by the sink that sends and the held queue that re-drives.
653
+ const usageRetry: UsageRetry = { ...DEFAULT_USAGE_RETRY, ...overrides.usageRetry };
654
+ const jobs = await createJobs(
655
+ env,
656
+ db.db,
657
+ db.pool,
658
+ logger,
659
+ overrides.usageDownstream ?? hubUsagePoster ?? new NoopUsageSink(),
660
+ auth,
661
+ audit,
662
+ tool.jobs?.({ env, db: db.db, logger, getTool: () => domainRef.current }) ?? [],
663
+ usageRetry
664
+ );
665
+ // The tool's billing-rail declarations (slot 22), asserted once here: a
666
+ // route that meters an action the price book does not know, or a limit no
667
+ // tier values, stops the boot naming the route.
668
+ // The slot runs before `services`; an actor hook of an anonymous surface
669
+ // reads the domain lazily through `getTool()` at request time (PRDCT-2634).
670
+ const entitlements =
671
+ tool.entitlements?.(env, { db: db.db, getTool: () => domainRef.current }) ?? EMPTY_TOOL_ENTITLEMENTS;
672
+ assertToolEntitlements(entitlements);
673
+ // The credit check at the hub (§7 steps 3 and 4), cloud only: the price
674
+ // of a metered request against the organization's balance, before the
675
+ // handler runs. Its metrics join /metrics beside the poster's.
676
+ const hubCreditCheck =
677
+ hub && hubMachineToken
678
+ ? new HubCreditCheck({
679
+ issuerUrl: hub.issuerUrl,
680
+ token: hubMachineToken,
681
+ logger,
682
+ dials: overrides.entitlementCheckDials
683
+ })
684
+ : undefined;
685
+ const entitlementCloud =
686
+ hub && hubMachineToken && hubCreditCheck
687
+ ? {
688
+ profiles: new EntitlementProfiles({
689
+ issuerUrl: hub.issuerUrl,
690
+ token: hubMachineToken,
691
+ logger,
692
+ dials: overrides.entitlementDials
693
+ }),
694
+ // The upgrade link of a plan refusal when the hub's answer carries
695
+ // no upgrade page of its own (an older hub): the hub root.
696
+ upgradeUrl: hub.issuerUrl,
697
+ credits: hubCreditCheck,
698
+ // The reported user of an event is the HUB user (the SSO `sub` on
699
+ // the account row), never the tool's local id; a user with no hub
700
+ // link (the operator) reports null. Cached per user for five
701
+ // minutes: an upload burst is one read, not one per event.
702
+ hubSubject: hubSubjectResolver(db.db)
703
+ }
704
+ : undefined;
705
+
706
+ // The clock check (PRDCT-2644), cloud only and off the boot's critical
707
+ // path: the hub judges every usage event's date against ITS clock and the
708
+ // poster against this instance's, so a clock more than five minutes ahead
709
+ // of the hub's stamps events the hub would refuse and the poster cannot
710
+ // see it. One read of the hub's Date header, a warning when they disagree
711
+ // beyond the forward bound, nothing refused.
712
+ if (hub) {
713
+ void hubClockSkewMs(hub.issuerUrl).then((skewMs) => {
714
+ if (skewMs === null) return;
715
+ const sentence = describeHubClockSkew(skewMs);
716
+ if (sentence) logger.warn({ skewMs }, `hub clock: ${sentence}`);
717
+ });
718
+ }
719
+
720
+ // The edition split (internal/federation.md): the local defaults below are the
721
+ // oss binding, passed through bindEditionSeams — the ONE place EDITION
722
+ // decides what the registry gets. oss returns them untouched; cloud
723
+ // rebinds the identity descriptor and wires the live principal gate off
724
+ // the reconciler built above. Entitlements stay LOCAL on both editions.
725
+ const seams = bindEditionSeams(
726
+ hub,
727
+ {
728
+ identity: new LocalIdentityProvider(
729
+ auth,
730
+ db.db,
731
+ Boolean(env.GOOGLE_CLIENT_ID),
732
+ email.delivers,
733
+ email.delivers, // self-serve password reset needs a delivering email driver
734
+ email.delivers, // self-serve email change needs one too
735
+ cachedReconcile
736
+ ),
737
+ entitlements: new AllowAllEntitlements({
738
+ maxFileSizeMb: env.MAX_FILE_SIZE_MB,
739
+ apiRequestsPerMinute: env.API_RATE_LIMIT_PER_MINUTE,
740
+ apiRequestsBurstPerSecond: env.API_RATE_LIMIT_BURST
741
+ }),
742
+ usage: new PgBossUsageSink(jobs.boss, logger, usageRetry)
743
+ },
744
+ logger,
745
+ { db: db.db, reconciler: hubReconciler }
746
+ );
747
+ const registry = createRegistry({
748
+ identity: seams.identity,
749
+ entitlements: seams.entitlements,
750
+ usage: seams.usage,
751
+ events,
752
+ workspaces: new WorkspaceService(db.db)
753
+ });
754
+
755
+ // API-key pepper registry (ADR 008): version 1 is the AUTH_SECRET-derived
756
+ // pepper; API_KEY_PEPPERS adds/pins versions so keys survive rotations.
757
+ const pepperRegistry = buildPepperRegistry(authSecret, env.API_KEY_PEPPERS);
758
+ if (env.API_KEY_PEPPERS !== undefined && !pepperRegistry.v1Pinned) {
759
+ logger.warn(
760
+ 'API_KEY_PEPPERS is set but does not pin version 1 — version-1 API keys still ' +
761
+ 'depend on the live AUTH_SECRET; pin `1:<historical secret>` before rotating it ' +
762
+ '(internal/security-runbooks.md).'
763
+ );
764
+ }
765
+ const apiKeys = new ApiKeyService(db.db, pepperRegistry, tool.identity.apiKeyPrefix, cachedReconcile);
766
+ // Storage: probed at boot — /readyz stays red on an unwritable volume.
767
+ state.reason = 'probing storage';
768
+ const storage = createStorageDriver(env);
769
+ await storage.healthcheck();
770
+ logger.info({ driver: storage.name }, 'storage writable');
771
+ const fileService = new FileService(db.db, storage, join(env.DATA_DIR, 'tmp'), logger);
772
+
773
+ // The tool's domain services, built ONCE, here: after the storage probe
774
+ // and BEFORE the rate limiters, which is where the first service a job
775
+ // reads lazily was always built (a nightly purge firing during boot finds
776
+ // it from this line on). A tool subscribes to the event bus inside this
777
+ // call (no request can arrive before boot returns, so a subscription made
778
+ // here precedes every emission).
779
+ //
780
+ // ONE step swapped sides in the move, and this is it. At base
781
+ // (`apps/server/src/boot.ts@461db68`) the tool's services STRADDLED
782
+ // `createRateLimiters`: the form and form-upload services were built before
783
+ // it (and the purge cell filled), the collaborator service after it. Here
784
+ // `createRateLimiters` runs after the whole `tool.services` slot. The one
785
+ // consequence: a Redis connect failure from the limiters now surfaces after
786
+ // the domain services exist rather than between them; what moved ahead of
787
+ // the connect is the collaborator service's constructor, pure field
788
+ // assignment with no I/O. The limiters are not put back first because the
789
+ // job pollers are live from `createJobs` above, well before this line (at
790
+ // base too): with the limiters first, the form-upload purge cell would be
791
+ // empty during the limiter connect, where at base it was already filled,
792
+ // and a purge job picked up in that window would run as a no-op and
793
+ // complete. One slot cannot sit on both sides of the limiters; this side is
794
+ // the unobservable one.
795
+ const serviceCore: ServiceCore<TEnvShape, TEvents, TToolOverrides> = {
796
+ db: db.db,
797
+ env,
798
+ logger,
799
+ storage,
800
+ fileService,
801
+ pepperRegistry,
802
+ email,
803
+ events,
804
+ authSecret,
805
+ timers: jobs.timers,
806
+ overrides: overrides.tool
807
+ };
808
+ const domain = tool.services(serviceCore);
809
+ domainRef.current = domain;
810
+
811
+ const limiters = await createRateLimiters<TBuckets>(env, logger, tool.rateLimiters);
812
+ const core: PlatformCore<TEnvShape, TBuckets, TEvents, TToolOverrides> = { ...serviceCore, limiters };
813
+
814
+ // OAuth-bearer verification: local JWKS (we minted the token) + live
815
+ // membership re-check. One instance, shared by the API and /mcp gates.
816
+ const oauthJwt = new OauthJwtVerifier(auth, db.db, env.PUBLIC_BASE_URL, cachedReconcile);
817
+
818
+ const api = createApiApp({
819
+ db: db.db,
820
+ env,
821
+ auth,
822
+ registry,
823
+ logger,
824
+ apiKeys,
825
+ audit,
826
+ email,
827
+ limiters,
828
+ storage,
829
+ fileService,
830
+ oauthJwt,
831
+ authSecret,
832
+ setupToken,
833
+ clearGeneratedSetupToken: () => clearGeneratedSetupToken(env.DATA_DIR),
834
+ accountDeletion,
835
+ tool,
836
+ domain,
837
+ untrustedOrigins,
838
+ hubSso,
839
+ // Cloud only: /sso/cli-connect stores the H3 offline grant through it.
840
+ hubGrant,
841
+ // Cloud only: /sso/logout builds the hub end-session leg through it.
842
+ hubLogout,
843
+ // Cloud only (internal/federation.md): the post-resolution LIVE hub gate —
844
+ // reconcile-as-the-user + suspension/revocation/grant-death verdicts —
845
+ // run by authContext on every authenticated request. undefined on oss.
846
+ principalGate: seams.principalGate,
847
+ entitlements,
848
+ entitlementCloud,
849
+ // Cloud only (PRDCT-2443): POST /workspaces creates the organization at
850
+ // the hub AS THE CALLER, then forces their reconcile. undefined on oss —
851
+ // the route creates locally there.
852
+ workspaceCloud:
853
+ hub && hubGrant && hubUserClient && hubReconciler
854
+ ? {
855
+ createOrg: overrides.hubCreateOrg ?? ((userId, name) => hubUserClient.createOrg(userId, name)),
856
+ forceReconcile: (userId) => hubReconciler.forceReconcile(userId),
857
+ hasHubLink: (userId) => hubGrant.hasStoredGrant(userId),
858
+ manageUrl: hub.issuerUrl
859
+ }
860
+ : undefined,
861
+ // Cloud only: the refusal page's hint (/me.hubDenied), read from the
862
+ // reconciler's memory of the last definitive hub list. undefined on oss.
863
+ hubDeniedOrgs: hubReconciler ? (userId) => hubReconciler.deniedOrgs(userId) : undefined,
864
+ // Cloud only: a person re-seated at the hub is re-admitted on a bare
865
+ // zero-state read. undefined on oss.
866
+ reconcileForZeroState: cachedReconcile
867
+ });
868
+
869
+ // The tool's public routes (e.g. a share-link viewer, Phase 4,
870
+ // ADR 012): anonymous, mounted in app.ts's public-route slot.
871
+ const publicRoutes = tool.app?.publicRoutes?.({ ...core, overrides: undefined }, domain);
872
+
873
+ // Observability: tracing (exporterless = zero phone-home) + Prometheus.
874
+ const otel = await createOtel(env, logger, { serviceName: tool.identity.otelServiceName });
875
+ const metrics = createMetrics(db.db, jobs.boss);
876
+ // The counters of the seams that talk to the hub join the app registry so
877
+ // a degraded hub, dying grants, a rail that rejects everything or usage
878
+ // held past its retry budget is visible on /metrics (the poster's are
879
+ // cloud only: oss never builds one).
880
+ for (const metric of [
881
+ ...(hubReconciler?.promMetrics ?? []),
882
+ ...(hubGrant?.promMetrics ?? []),
883
+ ...(hubUsagePoster?.promMetrics ?? []),
884
+ ...(hubCreditCheck?.promMetrics ?? []),
885
+ ...jobs.promMetrics
886
+ ]) {
887
+ metrics.registry.registerMetric(metric);
888
+ }
889
+
890
+ // MCP tools call the instance's own API in-process, forwarding the caller's
891
+ // bearer — MCP is just another API client. The root app does not exist yet
892
+ // at this point, so the fetcher closes over a holder filled right after
893
+ // createApp (before any request can arrive).
894
+ const rootApp: { current: Hono | null } = { current: null };
895
+ let cachedName: string | null = null;
896
+ const mcp = mcpRoutes({
897
+ publicBaseUrl: env.PUBLIC_BASE_URL,
898
+ version: env.APP_VERSION,
899
+ fetchApi: async (path, init) => {
900
+ if (!rootApp.current) throw new Error('mcp fetchApi called before app assembly');
901
+ return rootApp.current.request(path, init);
902
+ },
903
+ // The /mcp transport gate resolves the credential with NO workspace
904
+ // selector (default-org perspective): the per-tool-call `workspace`
905
+ // argument is mapped to the header at the in-process /api/v1 re-entry,
906
+ // where the real per-request selection happens.
907
+ resolveOauthJwt: (token) => oauthJwt.resolve(token, null),
908
+ resolveApiKey: (token) => apiKeys.resolve(token, null),
909
+ isApiKeyToken: apiKeys.isToken,
910
+ limiter: rateLimit(limiters.mcp, makeClientIp(env.TRUST_PROXY)),
911
+ tool: tool.mcp,
912
+ identity: tool.identity.mcp,
913
+ instanceName: async () => {
914
+ if (cachedName) return cachedName;
915
+ const [row] = await db.db.select({ name: instanceSettings.name }).from(instanceSettings).limit(1);
916
+ cachedName = row?.name ?? tool.identity.displayName;
917
+ return cachedName;
918
+ }
919
+ });
920
+
921
+ const app = await createApp({
922
+ logger,
923
+ state,
924
+ publicDir: tool.runtime.publicDir,
925
+ displayName: tool.identity.displayName,
926
+ api,
927
+ mcp,
928
+ wellKnown: wellKnownRoutes({ auth, publicBaseUrl: env.PUBLIC_BASE_URL, oauthScopes: tool.scopes.oauth }),
929
+ publicRoutes,
930
+ metricsMiddleware: metrics.middleware,
931
+ metricsRoutes: metrics.routes(env.METRICS_TOKEN),
932
+ otelMiddleware: otel.middleware,
933
+ // The boot probe above runs once. /readyz re-runs this one so a store
934
+ // that goes away later (unmounted volume, wiped /data after a failed
935
+ // restore, revoked S3 credentials) stops reporting ready.
936
+ probeStorage: () => storage.healthcheck(),
937
+ hsts: hstsValue(env),
938
+ rootMiddleware: tool.app?.rootMiddleware?.(env),
939
+ cspFrameSrc: tool.app?.cspFrameSrc?.(env) ?? [],
940
+ cspImgSrc: tool.app?.cspImgSrc?.(env) ?? []
941
+ });
942
+ rootApp.current = app;
943
+
944
+ if (!migrationsPending && !readinessRefusal) {
945
+ state.ready = true;
946
+ state.reason = '';
947
+ } else if (readinessRefusal) {
948
+ // Keep the migration-stage verdict on /readyz, not the last probe's label.
949
+ state.reason = readinessRefusal;
950
+ }
951
+
952
+ return { app, env, logger, state, db, auth, registry, jobs, email, otel, authSecret, tool: domain };
953
+ }