@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,1103 @@
1
+ import { betterAuth } from 'better-auth';
2
+ import { APIError } from 'better-auth/api';
3
+ import { createAuthMiddleware } from 'better-auth/api';
4
+ import { drizzleAdapter } from 'better-auth/adapters/drizzle';
5
+ import { emailOTP, genericOAuth, jwt, twoFactor } from 'better-auth/plugins';
6
+ import { deleteSessionCookie } from 'better-auth/cookies';
7
+ import { generateRandomString } from 'better-auth/crypto';
8
+ import { oauthProvider } from '@better-auth/oauth-provider';
9
+ import { and, eq } from 'drizzle-orm';
10
+ import {
11
+ account,
12
+ instanceSettings,
13
+ jwks,
14
+ oauthAccessToken,
15
+ oauthClient,
16
+ oauthConsent,
17
+ oauthRefreshToken,
18
+ session,
19
+ twoFactor as twoFactorTable,
20
+ user,
21
+ verification,
22
+ workspaceMembers,
23
+ type Db
24
+ } from '@antasphere/chassis-db';
25
+ import type { Env } from '../env.js';
26
+ import { HUB_SSO_PROVIDER_ID, HubSsoLoginError, type HubSsoService } from './hub-sso.js';
27
+ import { parseSuperadminEmails } from '../accounts/superadmin.js';
28
+ import { demoPassPlugin, type DemoPassPluginDeps } from './demo-pass-plugin.js';
29
+ // Types only, erased at emit: this package EMITS DECLARATIONS (the app bundled
30
+ // the file and never did), and `Auth = ReturnType<typeof createAuth>` is an
31
+ // inferred type that names these two modules. TypeScript can only spell a
32
+ // module it has resolved in this program (TS2742 otherwise), so they are
33
+ // resolved here. Nothing is imported at run time.
34
+ import type {} from '@better-auth/core/db/adapter';
35
+ import type {} from 'better-call';
36
+
37
+ /**
38
+ * The only file that touches better-auth's constructor. Everything else goes
39
+ * through the identity module's exports or the IdentityProvider seam, so a
40
+ * Better Auth upgrade (or replacement) stays contained.
41
+ *
42
+ * Password login is the self-host default (no SMTP dependency). Email OTP
43
+ * auto-enables when an email driver is configured; Google when client
44
+ * credentials are set. The twoFactor plugin offers OPT-IN per-user TOTP +
45
+ * backup codes (ADR 009) — password and email-OTP sign-ins by an enrolled
46
+ * user require the second factor; machine credentials never see one. The
47
+ * jwt + oauthProvider plugins make every instance an OAuth 2.1 authorization
48
+ * server for its own bundled /mcp endpoint (M6): RS256 access JWTs (15 min),
49
+ * rotating refresh tokens, PKCE + dynamic client registration, issuance
50
+ * gated on a LIVE active workspace membership.
51
+ */
52
+ /** Credential events audited on Better-Auth-native routes. */
53
+ export type AccountEvent =
54
+ | 'password_reset'
55
+ | 'password_change'
56
+ | 'email_change_request'
57
+ | 'email_change'
58
+ | 'two_factor_enroll'
59
+ | 'two_factor_disable';
60
+
61
+ export interface CreateAuthOptions {
62
+ db: Db;
63
+ env: Pick<
64
+ Env,
65
+ | 'PUBLIC_BASE_URL'
66
+ | 'GOOGLE_CLIENT_ID'
67
+ | 'GOOGLE_CLIENT_SECRET'
68
+ | 'OAUTH_DYNAMIC_CLIENT_REGISTRATION'
69
+ | 'SUPERADMIN_EMAILS'
70
+ >;
71
+ authSecret: string;
72
+ /**
73
+ * The tool's OAuth scope list, in the ORDER it is published (discovery
74
+ * `scopes_supported`, the consent screen, client registration defaults).
75
+ * The chassis spells no scope name: the tool defines the list and hands it
76
+ * in; the well-known documents read the same array from the tool.
77
+ */
78
+ oauthScopes: readonly string[];
79
+ /**
80
+ * Origins that are NEVER trusted, serving origin or not (PRDCT-1352): each
81
+ * is filtered out of `trustedOrigins` and refused outright by the sign-in
82
+ * Origin lock. Empty = nothing installed. A tool with a viewer passes its
83
+ * viewer origin (author-controlled script runs there).
84
+ */
85
+ untrustedOrigins: readonly string[];
86
+ /** When provided (an email driver delivers), the email-OTP login auto-enables. */
87
+ sendOtp?: (params: { email: string; otp: string; type: string }) => Promise<void>;
88
+ /**
89
+ * When provided (an email driver delivers), self-serve password reset
90
+ * auto-enables — POST /request-password-reset mails a link. Without it that
91
+ * endpoint stays closed (400) and only the admin-generated reset link works.
92
+ * On EDITION=cloud it is IGNORED: the whole reset surface refuses there
93
+ * (D1 hub-only posture — see isPasswordResetPath and the before-hook).
94
+ */
95
+ sendResetPassword?: (params: { email: string; url: string; token: string }) => Promise<void>;
96
+ /**
97
+ * Confirmation mail to the OLD address when a VERIFIED user requests an
98
+ * email change (the two-leg flow). Unverified users — the template's common
99
+ * case — skip straight to the verification mail below.
100
+ */
101
+ sendChangeEmailConfirmation?: (params: {
102
+ email: string;
103
+ newEmail: string;
104
+ url: string;
105
+ token: string;
106
+ }) => Promise<void>;
107
+ /**
108
+ * Verification mail to the NEW address — the final leg of every self-serve
109
+ * email change. When provided (an email driver delivers), POST /change-email
110
+ * opens; without it the endpoint answers 400 BEFORE any email lookup (no
111
+ * enumeration oracle) and only the admin-generated change link works.
112
+ */
113
+ sendVerificationEmail?: (params: { email: string; url: string; token: string }) => Promise<void>;
114
+ /**
115
+ * Audit hook for credential events on Better-Auth-native routes (which
116
+ * bypass the /api/v1 audit middleware). Fired best-effort by the caller.
117
+ */
118
+ onAccountEvent?: (event: AccountEvent, userId: string) => Promise<void>;
119
+ /**
120
+ * Fired from Better Auth's databaseHooks.user.create.after — the ONE seam
121
+ * every account entrance passes through (setup, invitation accept, the
122
+ * collaborator claim endpoint, any future SSO JIT), so a `user.created`
123
+ * event is trustworthy by construction instead of depending on each call
124
+ * site remembering to emit. Boot wires this to the platform event bus.
125
+ */
126
+ onUserCreated?: (user: { id: string; email: string }) => void;
127
+ /**
128
+ * Self-service account deletion guard, run by POST /delete-user BEFORE the
129
+ * row cascade. Boot wires the AccountDeletionService here and translates
130
+ * LastOwnerError into an APIError('BAD_REQUEST') so the route answers 400.
131
+ */
132
+ beforeUserDelete?: (userId: string) => Promise<void>;
133
+ /** Completion hook, run AFTER the cascade (the audit row's system actor). */
134
+ afterUserDelete?: (user: { id: string; email: string }) => Promise<void>;
135
+ /**
136
+ * The cloud edition's hub SSO binding (internal/federation.md, ADR 015).
137
+ * Present ONLY when the instance boots EDITION=cloud: registers the
138
+ * `antasphere` genericOAuth relying party, the D9 trusted-link config,
139
+ * and the per-login callback after-hook (JIT projection + re-sync). An
140
+ * oss boot passes nothing and carries zero SSO surface at runtime.
141
+ */
142
+ hubSso?: HubSsoService | undefined;
143
+ /**
144
+ * Demo sign-in (identity/demo-pass-plugin.ts): handed in by boot only while
145
+ * DEMO_SIGN_IN is on. The redeem endpoint is registered only then, and
146
+ * only on the self-hosted edition.
147
+ */
148
+ demoPass?: DemoPassPluginDeps | undefined;
149
+ }
150
+
151
+ export const AUTH_BASE_PATH = '/api/v1/auth';
152
+
153
+ /**
154
+ * The canonical OAuth resource identifier (JWT `aud`, RFC 8707) — the bundled
155
+ * MCP endpoint. Derived from PUBLIC_BASE_URL at boot, never from its own env
156
+ * var: AS and resource live on one origin, so the byte-identical-URL footgun
157
+ * of the split dashboard+MCP deployment cannot exist here.
158
+ */
159
+ export function mcpResourceUrl(publicBaseUrl: string): string {
160
+ return publicBaseUrl.replace(/\/+$/, '') + '/mcp';
161
+ }
162
+
163
+ export type Auth = ReturnType<typeof createAuth>;
164
+
165
+ /** Metadata URI fields a registering client may present to users (RFC 7591). */
166
+ const CLIENT_METADATA_URI_FIELDS = ['client_uri', 'logo_uri', 'tos_uri', 'policy_uri'] as const;
167
+
168
+ /**
169
+ * Every route that can CHANGE the account's email address from inside the
170
+ * tool, enumerated against the pinned 1.6.22 surface (re-verify on ANY Better
171
+ * Auth bump):
172
+ *
173
+ * - POST /change-email: the core self-serve change (`user.changeEmail`);
174
+ * - POST /email-otp/request-email-change + POST /email-otp/change-email: the
175
+ * emailOTP plugin's pair. Both answer 400 today (the plugin's own
176
+ * `changeEmail.enabled` is never set); closed as config insurance, the
177
+ * stance /set-password has in isPasswordResetPath.
178
+ *
179
+ * On EDITION=cloud the address is the hub's: the sign-in re-syncs it at every
180
+ * login (identity/hub-sso.ts), so a change made here is overwritten at the
181
+ * next one, and until then the person's address here differs from the one
182
+ * their hub identity carries. The dashboard hides the form
183
+ * (`emailChange: false`); the route itself refuses too (PRDCT-2818), like the
184
+ * owner's change-email link (api/members.ts, `email_change_disabled`).
185
+ * Ruled OUT on purpose: the tokened GET /verify-email stays open, since it
186
+ * also lands the address verification the instance sends, and with both
187
+ * mints closed no change token can be issued on cloud.
188
+ */
189
+ function isEmailChangePath(path: string): boolean {
190
+ return (
191
+ path.startsWith('/change-email') ||
192
+ path.startsWith('/email-otp/request-email-change') ||
193
+ path.startsWith('/email-otp/change-email')
194
+ );
195
+ }
196
+
197
+ /**
198
+ * Every Better Auth route that can SET or RESET a local password with no
199
+ * current-password proof, enumerated against the pinned 1.6.22 surface
200
+ * (re-enumerated on the 1.6.15 → 1.6.22 bump, 2026-08-29, ADR 001; the
201
+ * dist-wide createAuthEndpoint path+method diff was one added route in the
202
+ * unregistered oauth-popup plugin, nothing removed)
203
+ * (re-verify on ANY Better Auth bump):
204
+ *
205
+ * - core emailAndPassword: POST /request-password-reset,
206
+ * POST /reset-password, GET /reset-password/:token (the mailed callback);
207
+ * - emailOTP plugin (active whenever a mailer delivers):
208
+ * POST /email-otp/request-password-reset, POST /email-otp/reset-password,
209
+ * and the deprecated POST /forget-password/email-otp alias;
210
+ * - core /set-password (the passwordless-ADD route): included as bump
211
+ * insurance. In 1.6.22 its endpoint is `createAuthEndpoint.serverOnly`;
212
+ * in 1.6.15 it was registered PATHLESS
213
+ * (update-user.mjs — `createAuthEndpoint({ method, body, use })` with no
214
+ * path arg), so better-call's router SKIPS it (`!endpoint.path` →
215
+ * `continue`, router.mjs) and it is unreachable over HTTP today — the one
216
+ * route that could otherwise let a passwordless hub-JIT user ADD a
217
+ * credential and then /sign-in/email. A future bump could give it a path;
218
+ * a security allowlist must not depend on that accident, so guard it now
219
+ * (denying an unreachable path is a no-op until it isn't).
220
+ *
221
+ * Ruled OUT of this predicate on purpose, against the same 1.6.22 surface:
222
+ * - /change-password proves the CURRENT password AND requires an existing
223
+ * credential account (CREDENTIAL_ACCOUNT_NOT_FOUND otherwise), so a
224
+ * hub-JIT user — who has neither — cannot set a password through it;
225
+ * - admin /admin/set-user-password and the phoneNumber reset routes live in
226
+ * the admin / phoneNumber plugins, neither of which this instance
227
+ * registers (only genericOAuth, emailOTP, twoFactor, jwt, oauthProvider),
228
+ * so they are never mounted;
229
+ * - /sign-up/email is closed by the sign-up switches + the before-hook.
230
+ *
231
+ * On EDITION=cloud these are the SSO-bypass entrance the D1 hub-only posture
232
+ * closes (internal/federation.md, ADR 017): a hub-JIT user (no credential
233
+ * account) could otherwise mail themselves a reset, SET a local password,
234
+ * and mint sessions via /sign-in/email that skip the per-login hub re-sync.
235
+ * P4's re-assertion still gates every such session, so this is posture, not
236
+ * an attacker hole — but the entrance must not exist. /sign-in/email itself
237
+ * stays wired (the break-glass operator door).
238
+ * `ctx.path` here is the ROUTE PATTERN (e.g. '/reset-password/:token'), so
239
+ * prefix matching covers the tokened callback.
240
+ */
241
+ function isPasswordResetPath(path: string): boolean {
242
+ return (
243
+ path.startsWith('/request-password-reset') ||
244
+ path.startsWith('/reset-password') ||
245
+ path.startsWith('/forget-password') ||
246
+ path.startsWith('/email-otp/request-password-reset') ||
247
+ path.startsWith('/email-otp/reset-password') ||
248
+ path.startsWith('/set-password')
249
+ );
250
+ }
251
+
252
+ /**
253
+ * Every emailOTP route that can MINT A SESSION from an emailed code — or
254
+ * mail such a code — enumerated against the pinned 1.6.22 surface
255
+ * (re-enumerated on the 1.6.15 → 1.6.22 bump, 2026-08-29: the plugin's nine
256
+ * routes are byte-identical in path and method)
257
+ * (plugins/email-otp/routes.mjs; re-verify on ANY Better Auth bump):
258
+ *
259
+ * - POST /sign-in/email-otp: the sign-in mint — verifies the code, then
260
+ * UNCONDITIONALLY createSession + setSessionCookie;
261
+ * - POST /email-otp/verify-email: flips emailVerified, and mints a session
262
+ * ONLY under emailVerification.autoSignInAfterVerification — UNSET on
263
+ * this instance, so it mints nothing today. Guarded as config insurance
264
+ * (the same stance as /set-password in isPasswordResetPath): a future
265
+ * config change must not silently open an SSO-bypassing session mint,
266
+ * and no client of this instance calls the route (the email-change
267
+ * landing is the core tokened GET /verify-email, untouched);
268
+ * - POST /email-otp/send-verification-otp: the mail leg. With this close,
269
+ * EVERY redemption route for every OTP type refuses on cloud (sign-in +
270
+ * verify-email here, the reset trio in isPasswordResetPath), so a sent
271
+ * code could only ever be a dead letter — refuse the send rather than
272
+ * mail codes that cannot work.
273
+ *
274
+ * Ruled OUT on purpose, against the same 1.6.22 surface:
275
+ * - POST /email-otp/check-verification-otp verifies WITHOUT consuming and
276
+ * mints nothing (no createSession in its handler); with the send leg
277
+ * closed there is nothing to check anyway;
278
+ * - POST /email-otp/request-email-change + POST /email-otp/change-email
279
+ * (the plugin's two remaining routes, unchanged at 1.6.22): both run behind
280
+ * sensitiveSessionMiddleware — an EXISTING fresh session is their entry
281
+ * condition, so neither is a session ENTRANCE (change-email's
282
+ * setSessionCookie only re-sets the cookie of the session it already
283
+ * required) — and both answer 400 unless the plugin option
284
+ * `changeEmail.enabled` is set, which this instance never sets (the
285
+ * core tokened /change-email flow is the email-change surface; on cloud
286
+ * it is hidden and D10 re-syncs email from the hub anyway). If
287
+ * changeEmail is ever enabled, re-rule them here;
288
+ * - createVerificationOTP / getVerificationOTP are registered PATHLESS in
289
+ * 1.6.15 and `createAuthEndpoint.serverOnly` in 1.6.22 (routes.mjs), so
290
+ * better-call's router skips them: server-side only, unreachable over
291
+ * HTTP (same accident /set-password documents — do not rely on it);
292
+ * - /sign-in/email-otp is the plugin's ONLY /sign-in/* route; the twoFactor
293
+ * plugin mints only from a pending first factor, magic-link/phone are not
294
+ * registered here.
295
+ *
296
+ * On EDITION=cloud this pair is the last non-SSO HUMAN session entrance
297
+ * under the D1 hub-only posture (the reset surface closed at P8; ADR 017 §7
298
+ * recorded the OTP sign-in KNOWN-OPEN pending a charter call — now taken):
299
+ * every cloud credential must trace through "Sign in with Antasphere" so
300
+ * the hub's audit log is the complete access record. P4's re-assertion
301
+ * already gated every OTP session, so this is posture, not an attacker
302
+ * hole. The CLI counterpart (/cli/auth/*) closes in api/cli-auth.ts;
303
+ * /sign-in/email stays wired (the break-glass operator door, which the
304
+ * string '/sign-in/email-otp' does not prefix-match); oss keeps the full
305
+ * OTP login unchanged.
306
+ */
307
+ function isOtpSignInPath(path: string): boolean {
308
+ return (
309
+ path.startsWith('/sign-in/email-otp') ||
310
+ path.startsWith('/email-otp/verify-email') ||
311
+ path.startsWith('/email-otp/send-verification-otp')
312
+ );
313
+ }
314
+
315
+ /**
316
+ * The two Better Auth core routes that hand a caller the PROVIDER GRANT
317
+ * stored on their own `account` row, in PLAINTEXT (PRDCT-1354, findings
318
+ * AUTH-3 + AUTH-7; better-auth 1.6.22 `api/routes/account.mjs` (both routes
319
+ * present unchanged at the 1.6.15 → 1.6.22 bump, 2026-08-29) — re-verify
320
+ * the enumeration on ANY bump):
321
+ *
322
+ * - POST /get-access-token → `{ accessToken, idToken, accessTokenExpiresAt,
323
+ * scopes }`, refreshing first when the stored token is within 5 s of
324
+ * expiry;
325
+ * - POST /refresh-token → the same PLUS `refreshToken`, and it ALWAYS
326
+ * refreshes.
327
+ *
328
+ * Both are closed here, on BOTH editions, because both are pure subtraction:
329
+ * nothing in this repo, the SDK, the CLI, the dashboard, or the MCP server
330
+ * calls either one (grep `get-access-token` / `refresh-token`), and no
331
+ * server-side caller exists either — the hub grant's own refresh is
332
+ * `HubGrantService.postRefresh`, which posts to the hub's token endpoint
333
+ * directly and never goes through this route. The plugin-owned OAuth
334
+ * endpoints this instance DOES serve (`/oauth2/token`, `/oauth2/authorize`,
335
+ * …) are separate paths and untouched.
336
+ *
337
+ * Why they must not exist, even though `resolveUserId` requires a session
338
+ * for any HTTP caller (so they are not a cross-USER surface):
339
+ *
340
+ * 1. The `/auth/*` mount is registered BEFORE `authContext`
341
+ * (`api/index.ts`), so neither route sees the fail-closed scope
342
+ * allowlist, the per-principal quota, the idempotency claim, or the
343
+ * audit middleware. Any session-bearing caller — including a browser
344
+ * tricked into a same-site POST, or anything that reaches the cookie —
345
+ * reads the grant with no record left behind, which defeats the whole
346
+ * point of `encryptOAuthTokens: true` (encryption at rest is worthless
347
+ * next to an unlogged plaintext read endpoint).
348
+ * 2. On cloud the grant in question is the HUB grant, i.e. the credential
349
+ * the user-scoped federation is built on (ADR 019). Handing it out in
350
+ * plaintext hands out the ability to read that user's hub orgs directly.
351
+ * 3. `/refresh-token` additionally ROTATES that grant OUTSIDE the
352
+ * `pg_advisory_lock(7432004, hashtext(userId))` single-flight ADR 019
353
+ * requires. The hub's RFC 9700 reuse detection makes an unserialized
354
+ * double-refresh a grant-family-KILLING event, so this route is a
355
+ * denial-of-service any logged-in user can trigger from a browser tab.
356
+ * (The hub-side half of that hardening is PRDCT-1370.)
357
+ *
358
+ * `ctx.path` is the route pattern, and neither route has path params, so an
359
+ * exact match is enough; startsWith would also swallow future siblings, but
360
+ * an allowlist-shaped closure should deny exactly what it has verified.
361
+ */
362
+ function isProviderGrantPath(path: string): boolean {
363
+ return path === '/get-access-token' || path === '/refresh-token';
364
+ }
365
+
366
+ function isHttpUrl(value: unknown): boolean {
367
+ if (typeof value !== 'string') return false;
368
+ try {
369
+ const url = new URL(value);
370
+ return url.protocol === 'http:' || url.protocol === 'https:';
371
+ } catch {
372
+ return false;
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Better Auth `trustedOrigins` (PRDCT-1352): the configured public origin plus
378
+ * the origin the request was served on (localhost, previews, any domain behind
379
+ * a TLS-terminating proxy) — MINUS the viewer origin, which is author-
380
+ * controlled script's origin and is never trusted, serving origin or
381
+ * not. The host gate keeps the auth surface off that hostname; this is the
382
+ * second lock behind it. Exported pure so a unit test can pin the exclusion.
383
+ */
384
+ export function trustedOriginsFor(
385
+ publicBaseUrl: string,
386
+ untrustedOrigins: readonly string[]
387
+ ): (request?: Request) => string[] {
388
+ return (request) => {
389
+ const origins = [publicBaseUrl];
390
+ try {
391
+ if (request) origins.push(new URL(request.url).origin);
392
+ } catch {
393
+ // unparseable request URL — explicit origin only
394
+ }
395
+ return untrustedOrigins.length > 0
396
+ ? origins.filter((origin) => !untrustedOrigins.includes(origin))
397
+ : origins;
398
+ };
399
+ }
400
+
401
+ /**
402
+ * The sign-in Origin lock (M9 login-CSRF hardening + PRDCT-1352): a sign-in
403
+ * that presents an Origin must present a trusted one — the serving origin or
404
+ * Better Auth's own trusted list — and the viewer origin is refused OUTRIGHT,
405
+ * even as the serving origin: author script must never mint a session on the
406
+ * hostname it runs on. Exported pure so a unit test can pin both arms.
407
+ */
408
+ export function signInOriginRefused(input: {
409
+ origin: string;
410
+ servingOrigin: string | null;
411
+ untrustedOrigins: readonly string[];
412
+ isTrustedOrigin: (origin: string) => boolean;
413
+ }): boolean {
414
+ const { origin, servingOrigin, untrustedOrigins, isTrustedOrigin } = input;
415
+ if (untrustedOrigins.includes(origin)) return true;
416
+ return origin !== servingOrigin && !isTrustedOrigin(origin);
417
+ }
418
+
419
+ export function createAuth({
420
+ db,
421
+ env,
422
+ authSecret,
423
+ oauthScopes,
424
+ untrustedOrigins,
425
+ sendOtp,
426
+ sendResetPassword,
427
+ sendChangeEmailConfirmation,
428
+ sendVerificationEmail,
429
+ onAccountEvent,
430
+ onUserCreated,
431
+ beforeUserDelete,
432
+ afterUserDelete,
433
+ hubSso,
434
+ demoPass
435
+ }: CreateAuthOptions) {
436
+ const isHttps = env.PUBLIC_BASE_URL.startsWith('https://');
437
+ const resource = mcpResourceUrl(env.PUBLIC_BASE_URL);
438
+
439
+ /**
440
+ * Cloud only: may this email take the local password door? The setup
441
+ * operator (instance_settings.operator_user_id — the durable record,
442
+ * CLOUD-3) and the SUPERADMIN_EMAILS allowlist; nobody else.
443
+ */
444
+ const isLocalSignInAllowed = async (email: string): Promise<boolean> => {
445
+ if (parseSuperadminEmails(env.SUPERADMIN_EMAILS).has(email)) return true;
446
+ const [row] = await db
447
+ .select({ email: user.email })
448
+ .from(instanceSettings)
449
+ .innerJoin(user, eq(user.id, instanceSettings.operatorUserId))
450
+ .limit(1);
451
+ return row?.email?.toLowerCase() === email;
452
+ };
453
+
454
+ /**
455
+ * The JWT issuance gate (user-scoped credential model): a grant is the
456
+ * USER — "consent to act as you" — so issuance requires only that the
457
+ * user holds AT LEAST ONE live active membership on this instance, and
458
+ * the minted claims carry NO workspace. The request-time workspace comes
459
+ * from the X-Workspace-Id selector against the user's live memberships
460
+ * (identity/oauth-jwt.ts); a claim could only ever go stale against that.
461
+ *
462
+ * Runs on the authorization_code AND refresh_token grants; throwing
463
+ * aborts issuance with an RFC 6749 error body. It is NOT what revokes a
464
+ * deactivated member's access, though: a refresh without the RFC 8707
465
+ * `resource` param still mints an OPAQUE token that never passes through
466
+ * here. The real enforcement is resource-side — every request re-checks
467
+ * the live membership (and an opaque token fails the bearer gate's
468
+ * looksLikeJwt outright), so whatever a refresh mints is rejected at
469
+ * every resource. Verified live (M9).
470
+ */
471
+ async function userAccessClaims(user: { id: string; email: string } | null | undefined) {
472
+ if (!user?.id) {
473
+ throw new APIError('FORBIDDEN', {
474
+ error: 'access_denied',
475
+ error_description: 'Token issuance requires a user-bound grant'
476
+ });
477
+ }
478
+ const [row] = await db
479
+ .select({ id: workspaceMembers.id })
480
+ .from(workspaceMembers)
481
+ .where(and(eq(workspaceMembers.userId, user.id), eq(workspaceMembers.isActive, true)))
482
+ .limit(1);
483
+ if (!row) {
484
+ throw new APIError('FORBIDDEN', {
485
+ error: 'access_denied',
486
+ error_description: 'This account has no active membership on this instance'
487
+ });
488
+ }
489
+ return { email: user.email };
490
+ }
491
+
492
+ return betterAuth({
493
+ plugins: [
494
+ // Cloud edition only (hubSso is constructed iff EDITION=cloud): the
495
+ // "Sign in with Antasphere" relying party. `disableSignUp` is
496
+ // DELIBERATELY unset on this provider — hub SSO is the sanctioned
497
+ // account entrance on cloud, the conscious FOURTH switch next to the
498
+ // three closed-signup switches (internal/federation.md). Discovery, token
499
+ // exchange (with RFC 8707 `resource`), and token verification all
500
+ // live in the provider config hub-sso.ts builds.
501
+ ...(hubSso ? [genericOAuth({ config: [hubSso.providerConfig()] })] : []),
502
+ // Demo sign-in, self-hosted only: POST /demo/redeem signs a demo pass's
503
+ // person in (identity/demo-pass-plugin.ts). Never on cloud, where every
504
+ // session entrance goes through the hub.
505
+ ...(demoPass && !hubSso ? [demoPassPlugin(demoPass)] : []),
506
+ ...(sendOtp
507
+ ? [
508
+ emailOTP({
509
+ // Closed instance: OTP signs in EXISTING accounts only — without
510
+ // this, an OTP send to any unknown email would mint a user.
511
+ disableSignUp: true,
512
+ async sendVerificationOTP({ email, otp, type }) {
513
+ await sendOtp({ email, otp, type });
514
+ }
515
+ })
516
+ ]
517
+ : []),
518
+ // Optional per-user 2FA: TOTP + one-time backup codes (ADR 009). NO
519
+ // otpOptions.sendOTP — an emailed code as "second factor" would collapse
520
+ // into the first factor for anyone who controls the mailbox. Enrollment
521
+ // is password-gated and pending until the user proves a working
522
+ // authenticator by verifying one TOTP code; disable is password-gated.
523
+ // Sign-up stays closed: the plugin adds no account-creating route.
524
+ twoFactor({
525
+ // Label shown in authenticator apps — the instance's own host.
526
+ issuer: new URL(env.PUBLIC_BASE_URL).host
527
+ }),
528
+ jwt({
529
+ // JWTs exist ONLY as OAuth access tokens — never attach a signed JWT
530
+ // to plain session responses (recommended with an oauth provider).
531
+ disableSettingJwtHeader: true,
532
+ // RS256 over the default EdDSA: broadest verifier compatibility.
533
+ jwks: { keyPairConfig: { alg: 'RS256' } },
534
+ // Clean origin (no /api/v1/auth path) as the OAuth issuer — the exact
535
+ // `iss` oauth-jwt.ts pins, and the root of the RFC 8414 discovery
536
+ // documents re-served by routes/wellknown.ts.
537
+ jwt: { issuer: env.PUBLIC_BASE_URL }
538
+ }),
539
+ oauthProvider({
540
+ // SPA routes; the API-side validation is Better Auth's signed query.
541
+ loginPage: '/login',
542
+ consentPage: '/oauth/consent',
543
+ scopes: [...oauthScopes],
544
+ // `resource` (RFC 8707) must be the bundled MCP endpoint — its exact `aud`.
545
+ validAudiences: [resource],
546
+ // No M2M clients: every token is bound to a consenting user.
547
+ grantTypes: ['authorization_code', 'refresh_token'],
548
+ // MCP clients (claude.ai, Claude Desktop) register themselves via
549
+ // RFC 7591 without a session. Registration alone grants nothing —
550
+ // tokens still require login + consent + an active membership — and
551
+ // the register endpoint is rate-limited in api/index.ts.
552
+ //
553
+ // PLT-29: it is still an anonymous write to the authorization server's
554
+ // client table, so an operator gets a switch rather than a hardcoded
555
+ // posture. OAUTH_DYNAMIC_CLIENT_REGISTRATION=false turns BOTH flags
556
+ // off and /oauth2/register refuses everyone.
557
+ //
558
+ // THE DEFAULT IS DELIBERATELY UNCHANGED (true): the MCP connector
559
+ // story depends on open registration, and the fleet-wide decision on
560
+ // whether that posture should change belongs to the OIDC audit, not to
561
+ // this hardening pass. This adds the control and nothing else.
562
+ allowDynamicClientRegistration: env.OAUTH_DYNAMIC_CLIENT_REGISTRATION,
563
+ allowUnauthenticatedClientRegistration: env.OAUTH_DYNAMIC_CLIENT_REGISTRATION,
564
+ clientRegistrationDefaultScopes: [...oauthScopes],
565
+ clientRegistrationAllowedScopes: [...oauthScopes],
566
+ // Short access tokens: stateless JWTs can't be revoked, so revocation
567
+ // latency == remaining lifetime. Refresh tokens rotate (reuse detected).
568
+ accessTokenExpiresIn: 900, // 15 min
569
+ refreshTokenExpiresIn: 60 * 60 * 24 * 365, // 365 days (sliding; see the session note)
570
+ customAccessTokenClaims: async ({ user }) => userAccessClaims(user),
571
+ // Grants are user-scoped ("act as you"): consents bind NOTHING —
572
+ // the explicit `undefined` referenceId is the no-binding wiring
573
+ // (the plugin type requires the key whenever postLogin exists).
574
+ // `page` + `shouldRedirect` are required by the type too; the
575
+ // constant false keeps the flow on the consent page itself.
576
+ postLogin: {
577
+ page: '/oauth/consent',
578
+ shouldRedirect: () => false,
579
+ consentReferenceId: async () => undefined
580
+ },
581
+ // Root discovery documents are re-served by routes/wellknown.ts.
582
+ silenceWarnings: { oauthAuthServerConfig: true, openidConfig: true }
583
+ })
584
+ ],
585
+ // Google social sign-in is a SELF-HOST option only. On EDITION=cloud
586
+ // (hubSso present) the provider is NOT registered at all — even with
587
+ // GOOGLE_CLIENT_ID/SECRET set — so /sign-in/social and
588
+ // /oauth2/callback/google mint nothing there. Under the D1 hub-only
589
+ // posture the sole sanctioned human entrance is "Sign in with
590
+ // Antasphere" (the `antasphere` genericOAuth provider), with
591
+ // /sign-in/email kept as the deliberate break-glass door; a live Google
592
+ // provider would be a NON-hub session entrance that defeats the
593
+ // audit-completeness guarantee (every cloud login must trace through the
594
+ // hub). Gating on !hubSso — not on "operator didn't set the env var" —
595
+ // makes the guarantee hold by construction (defense in depth against
596
+ // misconfiguration), mirroring the OTP closures. oss is unchanged: with
597
+ // the credentials set, Google signs in accounts that already exist
598
+ // (disableSignUp — the callback never creates), one of the three
599
+ // closed-signup switches.
600
+ socialProviders:
601
+ !hubSso && env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET
602
+ ? {
603
+ google: {
604
+ clientId: env.GOOGLE_CLIENT_ID,
605
+ clientSecret: env.GOOGLE_CLIENT_SECRET,
606
+ disableSignUp: true
607
+ }
608
+ }
609
+ : {},
610
+ account: {
611
+ // OAuth provider tokens on the `account` row are encrypted at rest —
612
+ // on cloud that row IS the user's hub grant (offline_access refresh
613
+ // token, identity/hub-grant.ts), which must never sit plaintext in a
614
+ // dump. Enabled on BOTH editions (oss: Google tokens, when
615
+ // configured). CONFIG-ONLY: no schema change (drift:check proves),
616
+ // and retroactively safe — the decrypt passes legacy PLAINTEXT values
617
+ // through untouched (dist/oauth2/utils.mjs isLikelyEncrypted, verified
618
+ // on 1.6.15 and again on 1.6.22;
619
+ // hub-grant.ts mirrors the same rule). The key is the auth context's
620
+ // secretConfig (= this instance's AUTH_SECRET under our string-secret
621
+ // config). Re-verify both facts on any Better Auth bump.
622
+ encryptOAuthTokens: true,
623
+ // D9 (cloud only): the hub is a TRUSTED provider — its verified email
624
+ // assertion may link onto an existing local account of the same
625
+ // address (the setup operator's entrance under hub-only login).
626
+ // Keeping requireLocalEmailVerified true (stated, not defaulted)
627
+ // means a parked UNVERIFIED local account can never be taken over via
628
+ // SSO; the cloud setup flow mints the operator emailVerified=true for
629
+ // exactly this reason (internal/federation.md, pinned by edition tests).
630
+ ...(hubSso
631
+ ? {
632
+ accountLinking: {
633
+ trustedProviders: [HUB_SSO_PROVIDER_ID],
634
+ requireLocalEmailVerified: true
635
+ }
636
+ }
637
+ : {})
638
+ },
639
+ baseURL: env.PUBLIC_BASE_URL,
640
+ basePath: AUTH_BASE_PATH,
641
+ secret: authSecret,
642
+ // Cloud only: OAuth-callback error redirects land on the LOGIN PAGE.
643
+ // Verified on 1.6.15 and 1.6.22 (re-verify on ANY bump): the genericOAuth callback
644
+ // resolves `onAPIError.errorURL || <authBase>/error` and redirects an
645
+ // AS error (`?error=login_required` from a prompt=none authorize,
646
+ // `?error=access_denied`, …) there BEFORE parseState runs
647
+ // (generic-oauth/routes.mjs) — errorCallbackURL never sees these, so
648
+ // without this the silent-connect bounce would dead-end on better-auth's
649
+ // own /error page. The dashboard maps the ?error=… code to copy (and the
650
+ // silent-connect lattice treats the login_required family as a quiet
651
+ // hint-clear, Stage E). oss stays byte-identical: key absent, the
652
+ // pre-change default error landing untouched.
653
+ ...(hubSso ? { onAPIError: { errorURL: env.PUBLIC_BASE_URL.replace(/\/+$/, '') + '/login' } } : {}),
654
+ database: drizzleAdapter(db, {
655
+ provider: 'pg',
656
+ schema: {
657
+ user,
658
+ session,
659
+ account,
660
+ verification,
661
+ jwks,
662
+ oauthClient,
663
+ oauthAccessToken,
664
+ oauthRefreshToken,
665
+ oauthConsent,
666
+ twoFactor: twoFactorTable
667
+ }
668
+ }),
669
+ user: {
670
+ // Self-serve email change. Enabled unconditionally: without a
671
+ // delivering email driver POST /change-email answers 400 BEFORE the
672
+ // email lookup (no enumeration oracle), so the switch is safe. Tokens
673
+ // are stateless HS256 JWTs signed with the auth secret (1h expiry,
674
+ // not revocable). Template users are typically emailVerified=false, so
675
+ // the COMMON flow is single-leg: one verification mail to the NEW
676
+ // address; the confirmation-to-old-address leg only runs for verified
677
+ // users.
678
+ changeEmail: {
679
+ enabled: true,
680
+ ...(sendChangeEmailConfirmation
681
+ ? {
682
+ sendChangeEmailConfirmation: async ({
683
+ user,
684
+ newEmail,
685
+ url,
686
+ token
687
+ }: {
688
+ user: { email: string };
689
+ newEmail: string;
690
+ url: string;
691
+ token: string;
692
+ }) => {
693
+ await sendChangeEmailConfirmation({ email: user.email, newEmail, url, token });
694
+ }
695
+ }
696
+ : {})
697
+ },
698
+ // Self-service account deletion (GDPR erasure). NO
699
+ // sendDeleteAccountVerification: the password path works with zero
700
+ // SMTP. Known Better Auth default, relied on rather than fought: a
701
+ // session younger than freshAge (24h) may delete WITHOUT a password
702
+ // server-side. The dashboard collects the password where the instance
703
+ // offers one (`/instance` auth.methods has `password`); a hub-JIT user
704
+ // on cloud has NO credential account, so there the dashboard sends the
705
+ // person through a fresh hub sign-in first and the new session is the
706
+ // proof (PRDCT-2640). Deletion is local either way: it erases the
707
+ // instance's user row, never the hub identity (accounts/deletion.ts).
708
+ // See ADR 006.
709
+ deleteUser: {
710
+ enabled: true,
711
+ beforeDelete: async (user: { id: string }) => {
712
+ await beforeUserDelete?.(user.id);
713
+ },
714
+ afterDelete: async (user: { id: string; email: string }) => {
715
+ await afterUserDelete?.({ id: user.id, email: user.email });
716
+ }
717
+ }
718
+ },
719
+ emailVerification: {
720
+ expiresIn: 3600,
721
+ ...(sendVerificationEmail
722
+ ? {
723
+ sendVerificationEmail: async ({
724
+ user,
725
+ url,
726
+ token
727
+ }: {
728
+ user: { email: string };
729
+ url: string;
730
+ token: string;
731
+ }) => {
732
+ // `user.email` is already the NEW address on the change-email leg.
733
+ await sendVerificationEmail({ email: user.email, url, token });
734
+ }
735
+ }
736
+ : {}),
737
+ // Fires when GET /verify-email flips a user to verified — the email
738
+ // change landing. Also fires on a hypothetical plain verify-email
739
+ // (acceptable: the audit row records a verification event either way).
740
+ afterEmailVerification: async (user: { id: string }) => {
741
+ if (user?.id) await onAccountEvent?.('email_change', user.id);
742
+ }
743
+ },
744
+ emailAndPassword: {
745
+ enabled: true,
746
+ minPasswordLength: 12,
747
+ // Recovery/change implies a possibly-compromised account: drop the
748
+ // other sessions on a reset. Applies to self-serve AND admin-minted
749
+ // reset links (both land on POST /reset-password).
750
+ revokeSessionsOnPasswordReset: true,
751
+ onPasswordReset: async ({ user }: { user: { id: string } }) => {
752
+ await onAccountEvent?.('password_reset', user.id);
753
+ },
754
+ // Self-serve reset exists only with a delivering email driver; without
755
+ // it, /request-password-reset returns 400 (no user-enumeration oracle).
756
+ // Cloud never wires it (defense in depth under the before-hook refusal
757
+ // above): no reset mail can even be BUILT on that edition.
758
+ ...(sendResetPassword && !hubSso
759
+ ? {
760
+ resetPasswordTokenExpiresIn: 3600,
761
+ sendResetPassword: async ({
762
+ user,
763
+ url,
764
+ token
765
+ }: {
766
+ user: { email: string };
767
+ url: string;
768
+ token: string;
769
+ }) => {
770
+ await sendResetPassword({ email: user.email, url, token });
771
+ }
772
+ }
773
+ : {})
774
+ },
775
+ hooks: {
776
+ // Accounts enter through setup or invitations only. HTTP sign-up is
777
+ // closed: ctx.request exists exactly when the call arrived over HTTP;
778
+ // server-side auth.api.signUpEmail calls (setup, invitation accept)
779
+ // carry no request and pass.
780
+ before: createAuthMiddleware(async (ctx) => {
781
+ if (ctx.path.startsWith('/sign-up') && ctx.request) {
782
+ throw new APIError('FORBIDDEN', {
783
+ message: 'Sign-up is closed on this instance — ask an admin for an invitation'
784
+ });
785
+ }
786
+ // Cloud edition (D1, hub-only login): the ENTIRE local password-reset
787
+ // surface refuses — see isPasswordResetPath for the enumerated routes
788
+ // and why. Unconditional (no ctx.request escape hatch like sign-up's):
789
+ // nothing server-side consumes these paths, so a server-side caller
790
+ // appearing would itself be a posture regression. oss is untouched
791
+ // (hubSso exists only on EDITION=cloud boots).
792
+ if (hubSso && isPasswordResetPath(ctx.path)) {
793
+ throw new APIError('FORBIDDEN', {
794
+ message:
795
+ 'Password reset is disabled on this edition — credentials are managed at the Antasphere hub'
796
+ });
797
+ }
798
+ // Cloud edition (PRDCT-2818): the email address is the hub's — see
799
+ // isEmailChangePath. Unconditional like the reset closure: nothing
800
+ // server-side calls these paths.
801
+ if (hubSso && isEmailChangePath(ctx.path)) {
802
+ throw new APIError('FORBIDDEN', {
803
+ code: 'email_change_disabled',
804
+ message: `Email changes are disabled on this edition — change your address on your Antasphere account (${hubSso.accountUrl()})`
805
+ });
806
+ }
807
+ // Cloud edition (D1, hub-only login): the emailOTP SIGN-IN surface
808
+ // refuses too — see isOtpSignInPath for the enumerated routes and
809
+ // why. Same unconditional stance as the reset closure (no
810
+ // ctx.request escape hatch): the one server-side consumer of these
811
+ // paths, /cli/auth/request's sendVerificationOTP delegate, refuses
812
+ // at its own route first on cloud (cli_otp_disabled), so a
813
+ // server-side caller reaching this would itself be a posture
814
+ // regression. oss is untouched (hubSso exists only on cloud boots).
815
+ if (hubSso && isOtpSignInPath(ctx.path)) {
816
+ throw new APIError('FORBIDDEN', {
817
+ code: 'otp_signin_disabled',
818
+ message: 'Email-code sign-in is disabled on this edition — use "Sign in with Antasphere"'
819
+ });
820
+ }
821
+ // BOTH editions: the provider-grant read/rotate routes are closed —
822
+ // see isProviderGrantPath for the enumeration and the three reasons.
823
+ // Unconditional (no `ctx.request` escape hatch): nothing server-side
824
+ // calls them either, so a server-side caller appearing would itself
825
+ // be the regression this guard exists to catch.
826
+ // Cloud edition (CLOUD-1 / AUTH-13, PRDCT-1356): unlinking the
827
+ // `antasphere` provider is refused. The /auth/* mount sits BEFORE
828
+ // authContext (isPublicApiPath), so the principal gate never saw
829
+ // this route; a hub user who unlinked kept their origin='hub'
830
+ // memberships while every later reconcile took the `no_link` branch
831
+ // — the one fail-open outcome — cached like a success. Enforcement
832
+ // must not be switchable by its subject. (The reconciler now also
833
+ // fails CLOSED on no_link for a principal holding hub-origin rows.)
834
+ if (hubSso && ctx.path === '/unlink-account') {
835
+ const providerId = (ctx.body as { providerId?: unknown } | undefined)?.providerId;
836
+ if (providerId === 'antasphere') {
837
+ throw new APIError('FORBIDDEN', {
838
+ code: 'hub_unlink_forbidden',
839
+ message: 'The Antasphere link cannot be removed on this edition — it is the identity itself'
840
+ });
841
+ }
842
+ }
843
+ // Cloud edition (EDIT-2, PRDCT-1356): /sign-in/email stays WIRED as
844
+ // the break-glass door, but it is a door for the OPERATOR and the
845
+ // SUPERADMIN_EMAILS allowlist only. Every other local credential on
846
+ // a cloud instance (pre-flip users, a leftover invitation account)
847
+ // would otherwise be a permanent non-SSO entrance that never sees
848
+ // the hub's audit log. Decided on the presented email; unknown
849
+ // addresses answer the same 403 (no account oracle).
850
+ if (hubSso && ctx.path === '/sign-in/email' && ctx.request) {
851
+ const email = String((ctx.body as { email?: unknown } | undefined)?.email ?? '')
852
+ .trim()
853
+ .toLowerCase();
854
+ if (!email || !(await isLocalSignInAllowed(email))) {
855
+ throw new APIError('FORBIDDEN', {
856
+ code: 'local_signin_disabled',
857
+ message:
858
+ 'Password sign-in is reserved for the instance operator on this edition — use "Sign in with Antasphere"'
859
+ });
860
+ }
861
+ }
862
+ if (isProviderGrantPath(ctx.path)) {
863
+ throw new APIError('FORBIDDEN', {
864
+ code: 'provider_grant_forbidden',
865
+ message: 'This endpoint is disabled — provider tokens are never handed to callers'
866
+ });
867
+ }
868
+ // Login-CSRF (session fixation) hardening: Better Auth's own origin
869
+ // check deliberately SKIPS requests carrying neither cookies nor
870
+ // Sec-Fetch metadata (progressive enhancement for non-browser
871
+ // clients), which leaves legacy browsers open to a cross-site login
872
+ // POST. Close the gap on the credential-issuing sign-in surface: a
873
+ // sign-in that DOES present an Origin must present a trusted one —
874
+ // trust evaluation is Better Auth's own isTrustedOrigin plus the
875
+ // serving origin (mirroring the trustedOrigins function below).
876
+ // CLI/SDK/MCP flows send no Origin header and are untouched; the
877
+ // OAuth token/register endpoints keep their deliberate public
878
+ // wildcard-CORS posture (middleware/oauth-public.ts).
879
+ if (ctx.path.startsWith('/sign-in') && ctx.request) {
880
+ const origin = ctx.request.headers.get('origin');
881
+ if (origin) {
882
+ let servingOrigin: string | null = null;
883
+ try {
884
+ servingOrigin = new URL(ctx.request.url).origin;
885
+ } catch {
886
+ // unparseable request URL — judge by the trusted list only
887
+ }
888
+ if (
889
+ signInOriginRefused({
890
+ origin,
891
+ servingOrigin,
892
+ untrustedOrigins,
893
+ isTrustedOrigin: (o) => ctx.context.isTrustedOrigin(o)
894
+ })
895
+ ) {
896
+ throw new APIError('FORBIDDEN', { message: 'Invalid origin' });
897
+ }
898
+ }
899
+ }
900
+ // PRDCT-1437 door 1 (the change-email consume existence oracle) is
901
+ // closed at the mount wrapper in api/index.ts, not here: it needs to
902
+ // rewrite the REQUEST before Better Auth reads the token, which a
903
+ // before-hook's `ctx.query` mutation does not reach (the endpoint reads
904
+ // its own parsed query). See `rewriteChangeEmailCollision` there.
905
+ // The plugin scheme-checks only redirect_uris — reject javascript:/data:
906
+ // metadata URIs on every client write path (unauthenticated DCR AND the
907
+ // session-gated create/update-client endpoints) so a stored javascript:
908
+ // URI can never reach a render site. The consent page also validates at
909
+ // render (defense in depth).
910
+ if (
911
+ ctx.path === '/oauth2/register' ||
912
+ ctx.path === '/oauth2/create-client' ||
913
+ ctx.path === '/oauth2/update-client'
914
+ ) {
915
+ const raw = ctx.body as Record<string, unknown> | undefined;
916
+ // register/create-client carry the fields at the top level;
917
+ // update-client nests them under `update`.
918
+ const meta = (raw?.update ?? raw) as Record<string, unknown> | undefined;
919
+ for (const field of CLIENT_METADATA_URI_FIELDS) {
920
+ const value = meta?.[field];
921
+ if (value != null && value !== '' && !isHttpUrl(value)) {
922
+ throw new APIError('BAD_REQUEST', {
923
+ error: 'invalid_client_metadata',
924
+ error_description: `${field} must be an http(s) URL`
925
+ });
926
+ }
927
+ }
928
+ }
929
+ }),
930
+ // A successful /change-password or /change-email reaches the after-hook
931
+ // (an error throws before it) — audit them here since Better Auth
932
+ // routes bypass the /api/v1 audit middleware. Reset is audited via
933
+ // onPasswordReset above; the email-change LANDING (the verify-email
934
+ // consumption) via afterEmailVerification.
935
+ after: createAuthMiddleware(async (ctx) => {
936
+ // Hub SSO per-login work (cloud only) — the callback after-hook.
937
+ // Fires for JIT AND returning users; `ctx.context.newSession` is the
938
+ // success discriminator (set by setSessionCookie before the redirect
939
+ // throw, never on the plugin's error exits — S1(b)). The verified
940
+ // assertion crosses from getUserInfo via the request-scoped login
941
+ // scope (ADR 015); everything here FAILS CLOSED: no assertion or a
942
+ // failed projection revokes the just-minted session — a cloud login
943
+ // without its hub-asserted workspace projection must not exist.
944
+ if (hubSso && ctx.path === '/oauth2/callback/:providerId') {
945
+ const providerId = (ctx as { params?: Record<string, string> }).params?.providerId;
946
+ if (providerId === HUB_SSO_PROVIDER_ID) {
947
+ const data = ctx.context.newSession;
948
+ if (!data?.user) return; // error exits mint no session — nothing to assert
949
+ const assertion = hubSso.takeAssertion();
950
+ try {
951
+ if (!assertion) {
952
+ throw new HubSsoLoginError(
953
+ 'sso_assertion_missing',
954
+ 'callback succeeded but no verified hub assertion reached the after-hook'
955
+ );
956
+ }
957
+ await hubSso.assertLogin(data.user.id, assertion);
958
+ } catch (err) {
959
+ hubSso.logLoginFailure(err, data.user.id);
960
+ // Same undo dance as the 2FA interstitial below: drop the
961
+ // session row, expire the cookie, clear newSession, then
962
+ // replace the success redirect with the error redirect.
963
+ await ctx.context.internalAdapter.deleteSession(data.session.token);
964
+ deleteSessionCookie(ctx, true);
965
+ ctx.context.setNewSession(null);
966
+ const code = err instanceof HubSsoLoginError ? err.code : 'sso_login_failed';
967
+ throw ctx.redirect(hubSso.loginErrorUrl(code));
968
+ }
969
+ return;
970
+ }
971
+ }
972
+ if (ctx.path === '/change-password') {
973
+ const userId = ctx.context.session?.user?.id;
974
+ if (userId) await onAccountEvent?.('password_change', userId);
975
+ }
976
+ if (ctx.path === '/change-email') {
977
+ const userId = ctx.context.session?.user?.id;
978
+ if (userId) await onAccountEvent?.('email_change_request', userId);
979
+ }
980
+ // 2FA credential-material events (both routes are session-gated).
981
+ // Enroll = secret + backup codes issued (activation completes on the
982
+ // first verified TOTP); disable = secret + backup codes destroyed.
983
+ if (ctx.path === '/two-factor/enable') {
984
+ const userId = ctx.context.session?.user?.id;
985
+ if (userId) await onAccountEvent?.('two_factor_enroll', userId);
986
+ }
987
+ if (ctx.path === '/two-factor/disable') {
988
+ const userId = ctx.context.session?.user?.id;
989
+ if (userId) await onAccountEvent?.('two_factor_disable', userId);
990
+ }
991
+ // Close the email-OTP second-factor bypass: the twoFactor plugin's
992
+ // own sign-in hook covers /sign-in/email (password) but NOT
993
+ // /sign-in/email-otp, so an emailed one-time code would sign a
994
+ // 2FA-enabled user straight in — collapsing 2FA to mailbox control.
995
+ // Mirror the plugin's dance (verified against 1.6.22
996
+ // plugins/two-factor/index.mjs; re-verify on ANY Better Auth bump):
997
+ // drop the just-minted session, park the pending sign-in in the
998
+ // verification table behind the signed `two_factor` cookie, park the
999
+ // per-challenge attempt counter next to it, and answer
1000
+ // { twoFactorRedirect } so /two-factor/verify-totp (or a backup code)
1001
+ // completes it. A 2FA-enabled user always has a verified TOTP secret
1002
+ // — activation is the only path that sets user.twoFactorEnabled.
1003
+ // The `2fa-attempts-<identifier>` row is NOT optional since 1.6.22:
1004
+ // verifyTwoFactor's beginAttempt CONSUMES it on every verify and
1005
+ // throws INVALID_TWO_FACTOR_COOKIE (401) when it is missing, so a
1006
+ // mirror that parks only the `2fa-` row hands the user a pending
1007
+ // state no factor can ever complete (PRDCT-1344; the hub hit the
1008
+ // same trap on its own bump).
1009
+ if (ctx.path === '/sign-in/email-otp') {
1010
+ const data = ctx.context.newSession;
1011
+ if (!data?.user || !(data.user as { twoFactorEnabled?: boolean }).twoFactorEnabled) return;
1012
+ deleteSessionCookie(ctx, true);
1013
+ await ctx.context.internalAdapter.deleteSession(data.session.token);
1014
+ ctx.context.setNewSession(null);
1015
+ const maxAge = 600; // the plugin's twoFactorCookieMaxAge default
1016
+ const twoFactorCookie = ctx.context.createAuthCookie('two_factor', { maxAge });
1017
+ const identifier = `2fa-${generateRandomString(20)}`;
1018
+ const expiresAt = new Date(Date.now() + maxAge * 1000);
1019
+ await ctx.context.internalAdapter.createVerificationValue({
1020
+ value: data.user.id,
1021
+ identifier,
1022
+ expiresAt
1023
+ });
1024
+ await ctx.context.internalAdapter.createVerificationValue({
1025
+ value: '0',
1026
+ identifier: `2fa-attempts-${identifier}`,
1027
+ expiresAt
1028
+ });
1029
+ await ctx.setSignedCookie(
1030
+ twoFactorCookie.name,
1031
+ identifier,
1032
+ ctx.context.secret,
1033
+ twoFactorCookie.attributes
1034
+ );
1035
+ return ctx.json({ twoFactorRedirect: true, twoFactorMethods: ['totp'] });
1036
+ }
1037
+ })
1038
+ },
1039
+ // Adapter-level hooks: config only, NOT schema — the drift guard's
1040
+ // generated auth-schema is unaffected (verified via drift:check).
1041
+ databaseHooks: {
1042
+ user: {
1043
+ create: {
1044
+ // Every entrance (setup's signUpEmail, invitation accept, the
1045
+ // collaborator claim endpoint, a future SSO JIT) creates the row
1046
+ // through the internal adapter, so this is the single trustworthy
1047
+ // `user.created` seam. Synchronous dispatch onto the event bus;
1048
+ // subscriber failures are isolated there and never fail account
1049
+ // creation.
1050
+ after: async (user: { id: string; email: string }) => {
1051
+ onUserCreated?.({ id: user.id, email: user.email });
1052
+ }
1053
+ }
1054
+ }
1055
+ },
1056
+ // Sessions, split by edition (SL-5, "one concept of being logged in"):
1057
+ //
1058
+ // - oss keeps the template's long-lived SLIDING sessions, byte-identical
1059
+ // (safe because the auth-context middleware re-checks workspace
1060
+ // membership on every request — instant revocation).
1061
+ // - cloud runs 30-day FIXED (non-sliding) sessions: the tool session is
1062
+ // a PROJECTION of the hub anchor session, and a sliding projection
1063
+ // would never re-dance — it would let the anchor die under an active
1064
+ // user. A fixed expiry forces a silent prompt=none re-derivation from
1065
+ // the anchor at most every 30 days, and each re-derivation's authorize
1066
+ // touch slides the HUB session forward (the anchor stays alive off
1067
+ // activity on ANY tool). The knob is `disableSessionRefresh` — pinned
1068
+ // on 1.6.15 and 1.6.22 (dist/api/routes/session.mjs is its ONLY reader:
1069
+ // `needsRefresh` goes false, so expiresAt never advances on use;
1070
+ // re-verify on ANY bump). `updateAge` is left at its default there —
1071
+ // it is dead config once refresh is disabled. CONFIG-ONLY: no schema
1072
+ // change (drift:check proves).
1073
+ session: hubSso
1074
+ ? {
1075
+ expiresIn: 60 * 60 * 24 * 30,
1076
+ disableSessionRefresh: true
1077
+ }
1078
+ : {
1079
+ expiresIn: 60 * 60 * 24 * 365,
1080
+ updateAge: 60 * 60 * 24
1081
+ },
1082
+ advanced: {
1083
+ useSecureCookies: isHttps
1084
+ },
1085
+ // The library's own limiter, on in production, keyed per client ADDRESS. Its defaults
1086
+ // (100 requests per 10 s per path, and 3 per 10 s on every /sign-in/*) let a room behind one
1087
+ // venue address sign in at about 18 people a minute and share one session-check budget
1088
+ // (proven on the cloud instances, 2026-10-01: the fourth sign-in start in 10 s answered 429).
1089
+ // Sized for an event: the general budget at ten times the default, the SSO start at 500.
1090
+ // The per-person walls (login, otp, the API quota) stay what they are.
1091
+ rateLimit: {
1092
+ window: 10,
1093
+ max: 1000,
1094
+ customRules: {
1095
+ '/sign-in/oauth2': { window: 10, max: 500 }
1096
+ }
1097
+ },
1098
+ // Trust the serving origin (works on localhost, previews, any domain)
1099
+ // plus the configured public origin behind a TLS-terminating proxy.
1100
+ // Serving origin + public origin, never the viewer origin (see trustedOriginsFor).
1101
+ trustedOrigins: trustedOriginsFor(env.PUBLIC_BASE_URL, untrustedOrigins)
1102
+ });
1103
+ }