@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,617 @@
1
+ import type { OpenAPIHono } from '@hono/zod-openapi';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { and, desc, eq, ne } from 'drizzle-orm';
4
+ import { createEmailVerificationToken } from 'better-auth/api';
5
+ import {
6
+ memberChangeEmailLinkRoute,
7
+ memberDeleteRoute,
8
+ memberRemoveRoute,
9
+ memberResetLinkRoute,
10
+ memberUpdateRoute,
11
+ membersListRoute
12
+ } from '@antasphere/chassis-contract/routes';
13
+ import { workspaceMembers, user as userTable, type Db } from '@antasphere/chassis-db';
14
+ import type { Auth } from '../identity/better-auth.js';
15
+ import { isLastOwnerDbError, LastOwnerError, type AccountDeletionService } from '../accounts/deletion.js';
16
+ import { mintRefusal as sharedMintRefusal } from '../accounts/mint-refusal.js';
17
+ import { cursorRowId, keysetBefore, pageOf } from '../pagination.js';
18
+ import { requireAuth, requireNonGuest, requireRole } from '../middleware/auth-context.js';
19
+ import { hubManagedMembershipGate } from '../middleware/hub-managed.js';
20
+ import {
21
+ deleteMembershipGrants,
22
+ mergeRemovalCounts,
23
+ type MembershipRemovalHook,
24
+ type RemovalCounts
25
+ } from '../members/removal.js';
26
+
27
+ export interface MemberRouteDeps {
28
+ db: Db;
29
+ auth: Auth;
30
+ publicBaseUrl: string;
31
+ accountDeletion: AccountDeletionService;
32
+ /**
33
+ * Cloud edition only (P7, internal/federation.md): when set, every membership
34
+ * MUTATION on a hub-origin workspace answers 403 `hub_managed` with this
35
+ * pointer. undefined on oss — zero behavior change there.
36
+ */
37
+ hubManaged?: { manageUrl: string } | undefined;
38
+ /** The tool's `guest_target` sentence (`copy.guestTarget`). */
39
+ guestTargetMessage: string;
40
+ /** The tool's half of a removal (the `membershipRemoval` slot), run in the removal's transaction. */
41
+ onMembershipRemoval?: MembershipRemovalHook | undefined;
42
+ }
43
+
44
+ const err = (code: string, message: string) => ({ error: { code, message } });
45
+
46
+ const toWire = (m: {
47
+ id: string;
48
+ userId: string;
49
+ email: string;
50
+ name: string;
51
+ role: 'owner' | 'admin' | 'member';
52
+ isActive: boolean;
53
+ createdAt: Date;
54
+ lastSeenAt: Date | null;
55
+ }) => ({
56
+ id: m.id,
57
+ userId: m.userId,
58
+ email: m.email,
59
+ name: m.name,
60
+ role: m.role,
61
+ isActive: m.isActive,
62
+ createdAt: m.createdAt.toISOString(),
63
+ lastSeenAt: m.lastSeenAt?.toISOString() ?? null
64
+ });
65
+
66
+ export function registerMemberRoutes(api: OpenAPIHono, deps: MemberRouteDeps): void {
67
+ const { db, auth, publicBaseUrl, accountDeletion, hubManaged } = deps;
68
+ // P7 (cloud only): membership of a hub-origin workspace is the HUB's to
69
+ // manage — every local mutation shape under /members answers the
70
+ // `hub_managed` pointer. Registered BEFORE the role gates so any
71
+ // authenticated caller gets the truthful refusal (an anonymous caller
72
+ // passes through — no principal — and still 401s at requireRole below).
73
+ // GET /members is untouched: the gate is method-keyed and the projected
74
+ // roster is real. ONE wildcard mount covers the whole subtree — the
75
+ // collection root, `/members/:id`, the 3-segment link paths, and any
76
+ // FUTURE mutation registered under /members — so fail-closed holds by
77
+ // construction, never by remembering a per-path registration.
78
+ if (hubManaged) {
79
+ api.use('/members/*', hubManagedMembershipGate(hubManaged.manageUrl));
80
+ }
81
+ api.use('/members', requireAuth());
82
+ // Guest capability limit (D2, both editions): the member roster (names +
83
+ // emails of the whole team) is a workspace-level surface. A guest is an
84
+ // external party invited to ONE resource — the host tenant's directory is not
85
+ // theirs to read. Per-resource surfaces their grant opens are untouched.
86
+ api.use('/members', requireNonGuest());
87
+ api.openapi(membersListRoute, async (c) => {
88
+ const principal = c.get('principal')!;
89
+ const { cursor, limit } = c.req.valid('query');
90
+ const cursorId = cursorRowId(cursor);
91
+ const rows = await db
92
+ .select({
93
+ id: workspaceMembers.id,
94
+ userId: workspaceMembers.userId,
95
+ email: userTable.email,
96
+ name: userTable.name,
97
+ role: workspaceMembers.role,
98
+ isActive: workspaceMembers.isActive,
99
+ createdAt: workspaceMembers.createdAt,
100
+ lastSeenAt: workspaceMembers.lastSeenAt
101
+ })
102
+ .from(workspaceMembers)
103
+ .innerJoin(userTable, eq(workspaceMembers.userId, userTable.id))
104
+ .where(
105
+ and(
106
+ eq(workspaceMembers.workspaceId, principal.workspaceId),
107
+ ...(cursorId
108
+ ? [
109
+ keysetBefore({
110
+ table: workspaceMembers,
111
+ id: workspaceMembers.id,
112
+ createdAt: workspaceMembers.createdAt,
113
+ workspaceId: workspaceMembers.workspaceId,
114
+ cursorId,
115
+ workspace: principal.workspaceId
116
+ })
117
+ ]
118
+ : [])
119
+ )
120
+ )
121
+ .orderBy(desc(workspaceMembers.createdAt), desc(workspaceMembers.id))
122
+ .limit(limit + 1);
123
+ const { page, nextCursor } = pageOf(rows, limit);
124
+ return c.json({ members: page.map(toWire), nextCursor }, 200);
125
+ });
126
+
127
+ api.use('/members/:id', requireRole('admin'));
128
+ api.openapi(memberUpdateRoute, async (c) => {
129
+ const principal = c.get('principal')!;
130
+ const { id } = c.req.valid('param');
131
+ const patch = c.req.valid('json');
132
+
133
+ const [target] = await db
134
+ .select({
135
+ id: workspaceMembers.id,
136
+ userId: workspaceMembers.userId,
137
+ role: workspaceMembers.role,
138
+ origin: workspaceMembers.origin,
139
+ isActive: workspaceMembers.isActive
140
+ })
141
+ .from(workspaceMembers)
142
+ .where(and(eq(workspaceMembers.id, id), eq(workspaceMembers.workspaceId, principal.workspaceId)))
143
+ .limit(1);
144
+ if (!target) return c.json(err('not_found', 'Member not found'), 404);
145
+
146
+ if (target.userId === principal.userId && patch.isActive === false) {
147
+ return c.json(err('cannot_deactivate_self', 'You cannot deactivate yourself'), 400);
148
+ }
149
+ // Guest role-lock (D2): a guest row is 'member' by construction and the
150
+ // guest capability limits key on origin, which no surface upgrades —
151
+ // promoting one would mint a workspace admin the guest gates still
152
+ // refuse resource creation to (an incoherent half-state), and on cloud an
153
+ // administrator the hub knows nothing about. To empower the person,
154
+ // invite them as a real member; the claim path preserves that row's
155
+ // origin. Deactivate/reactivate stays available — cutting a guest off
156
+ // entirely is a legitimate admin act.
157
+ if (target.origin === 'guest' && patch.role && patch.role !== target.role) {
158
+ return c.json(
159
+ err('guest_role_locked', 'A guest membership cannot change role — invite them as a member instead'),
160
+ 403
161
+ );
162
+ }
163
+ // Only owners touch owners (grant or revoke).
164
+ if ((target.role === 'owner' || patch.role === 'owner') && principal.role !== 'owner') {
165
+ return c.json(err('forbidden', 'Only an owner can change owner roles'), 403);
166
+ }
167
+ // Never leave the workspace without an active owner (shared with the
168
+ // account-deletion guards in accounts/deletion.ts). Demotions run under
169
+ // the race-free guard: workspace advisory lock + re-check, with the
170
+ // migration-0009 trigger as the DB-level backstop — a concurrent-race
171
+ // loser gets this clean 400, never a 500.
172
+ const losesOwner =
173
+ target.role === 'owner' && ((patch.role && patch.role !== 'owner') || patch.isActive === false);
174
+ const applyPatch = async () => {
175
+ await db
176
+ .update(workspaceMembers)
177
+ .set({
178
+ ...(patch.role ? { role: patch.role } : {}),
179
+ ...(patch.isActive !== undefined ? { isActive: patch.isActive } : {})
180
+ })
181
+ .where(eq(workspaceMembers.id, target.id));
182
+ };
183
+ try {
184
+ if (losesOwner) {
185
+ await accountDeletion.withLastOwnerGuard(principal.workspaceId, target.userId, applyPatch);
186
+ } else {
187
+ await applyPatch();
188
+ }
189
+ } catch (cause) {
190
+ // The guarded loser throws LastOwnerError; the unguarded branch (or a
191
+ // demotion racing invitation re-acceptance) can still trip the 0009
192
+ // trigger with P0409 — both mean the last active owner would be lost.
193
+ if (cause instanceof LastOwnerError || isLastOwnerDbError(cause)) {
194
+ const message =
195
+ cause instanceof LastOwnerError
196
+ ? cause.message
197
+ : 'The workspace must keep at least one active owner';
198
+ return c.json(err('last_owner', message), 400);
199
+ }
200
+ throw cause;
201
+ }
202
+
203
+ const [updated] = await db
204
+ .select({
205
+ id: workspaceMembers.id,
206
+ userId: workspaceMembers.userId,
207
+ email: userTable.email,
208
+ name: userTable.name,
209
+ role: workspaceMembers.role,
210
+ isActive: workspaceMembers.isActive,
211
+ createdAt: workspaceMembers.createdAt,
212
+ lastSeenAt: workspaceMembers.lastSeenAt
213
+ })
214
+ .from(workspaceMembers)
215
+ .innerJoin(userTable, eq(workspaceMembers.userId, userTable.id))
216
+ .where(eq(workspaceMembers.id, target.id))
217
+ .limit(1);
218
+
219
+ c.set('audit', {
220
+ action: 'member.update',
221
+ resourceType: 'member',
222
+ resourceId: target.id,
223
+ metadata: { ...patch, targetUserId: target.userId }
224
+ });
225
+ return c.json(toWire(updated!), 200);
226
+ });
227
+
228
+ // Admin account deletion (GDPR erasure, the second delete surface).
229
+ // Covered by the 2-segment `/members/:id` requireRole gate above; machines
230
+ // are excluded automatically because DELETE /members/{id} is deliberately
231
+ // unlisted in the fail-closed scope allowlist. Files the target uploaded
232
+ // stay with the workspace (created_by → NULL via FK); NO blob cleanup.
233
+ api.openapi(memberDeleteRoute, async (c) => {
234
+ const principal = c.get('principal')!;
235
+ const { id } = c.req.valid('param');
236
+
237
+ const [target] = await db
238
+ .select({
239
+ id: workspaceMembers.id,
240
+ userId: workspaceMembers.userId,
241
+ role: workspaceMembers.role,
242
+ isActive: workspaceMembers.isActive
243
+ })
244
+ .from(workspaceMembers)
245
+ .where(and(eq(workspaceMembers.id, id), eq(workspaceMembers.workspaceId, principal.workspaceId)))
246
+ .limit(1);
247
+ if (!target) return c.json(err('not_found', 'Member not found'), 404);
248
+ if (target.userId === principal.userId) {
249
+ return c.json(err('cannot_delete_self', 'Delete your own account from the account page'), 400);
250
+ }
251
+ if (target.role === 'owner' && principal.role !== 'owner') {
252
+ return c.json(err('forbidden', 'Only an owner can delete an owner'), 403);
253
+ }
254
+ // ADR 014: deleting the ACCOUNT erases the user from EVERY workspace, and
255
+ // an admin's authority ends at their own — refuse when the target belongs
256
+ // to any other workspace (remove the person from this one instead; the account
257
+ // holder can erase themselves). Single-workspace instances never hit this.
258
+ const [foreign] = await db
259
+ .select({ id: workspaceMembers.id })
260
+ .from(workspaceMembers)
261
+ .where(
262
+ and(
263
+ eq(workspaceMembers.userId, target.userId),
264
+ ne(workspaceMembers.workspaceId, principal.workspaceId)
265
+ )
266
+ )
267
+ .limit(1);
268
+ if (foreign) {
269
+ return c.json(
270
+ err(
271
+ 'member_of_other_workspaces',
272
+ 'This account belongs to other workspaces — remove the person from this workspace instead of deleting the account'
273
+ ),
274
+ 409
275
+ );
276
+ }
277
+
278
+ // Snapshot the wire member BEFORE the cascade removes the joined user row.
279
+ const [snapshot] = await db
280
+ .select({
281
+ id: workspaceMembers.id,
282
+ userId: workspaceMembers.userId,
283
+ email: userTable.email,
284
+ name: userTable.name,
285
+ role: workspaceMembers.role,
286
+ isActive: workspaceMembers.isActive,
287
+ createdAt: workspaceMembers.createdAt,
288
+ lastSeenAt: workspaceMembers.lastSeenAt
289
+ })
290
+ .from(workspaceMembers)
291
+ .innerJoin(userTable, eq(workspaceMembers.userId, userTable.id))
292
+ .where(eq(workspaceMembers.id, target.id))
293
+ .limit(1);
294
+
295
+ // Better Auth's own deletion path (adapter hooks included) — NOT raw
296
+ // drizzle. Membership, api keys, invitations they issued, and sessions
297
+ // cascade; audit rows and file uploader anonymize via SET NULL.
298
+ // Deleting an ACTIVE OWNER runs under the race-free last-owner guard
299
+ // (workspace advisory lock + re-check; the migration-0009 trigger
300
+ // backstops) so a concurrent-race loser gets a clean 400 last_owner.
301
+ const cascade = async () => {
302
+ const authCtx = await auth.$context;
303
+ await authCtx.internalAdapter.deleteUser(target.userId);
304
+ await authCtx.internalAdapter.deleteUserSessions(target.userId);
305
+ // OPS-3 (PRDCT-1357): this path bypasses the deleteUser hooks, so the
306
+ // erasure tombstone is written here — a restore must not undo this.
307
+ await accountDeletion.recordErasure({ id: target.userId, email: snapshot!.email });
308
+ };
309
+ try {
310
+ if (target.role === 'owner' && target.isActive) {
311
+ await accountDeletion.withLastOwnerGuard(principal.workspaceId, target.userId, cascade);
312
+ } else {
313
+ await cascade();
314
+ }
315
+ } catch (cause) {
316
+ // isLastOwnerDbError: the unguarded branch can still trip the trigger
317
+ // when the target is promoted to last owner mid-flight.
318
+ if (cause instanceof LastOwnerError || isLastOwnerDbError(cause)) {
319
+ return c.json(err('last_owner', 'The workspace must keep at least one active owner'), 400);
320
+ }
321
+ throw cause;
322
+ }
323
+
324
+ c.set('audit', {
325
+ action: 'member.delete',
326
+ resourceType: 'member',
327
+ resourceId: target.id,
328
+ metadata: { targetUserId: target.userId, email: snapshot!.email }
329
+ });
330
+ return c.json(toWire(snapshot!), 200);
331
+ });
332
+
333
+ // Removal (PRDCT-2816): the act between the pause and the account erasure,
334
+ // and the same one the hub's removal is on cloud. The membership row is
335
+ // switched off, never deleted (a new invitation reactivates the very same
336
+ // row), and the person's project grants, their team seats and what the
337
+ // tool hangs on them in this workspace go with it, in ONE transaction: a
338
+ // person brought back starts with none. The row is left at the role
339
+ // `member`: the role a person held is not kept in wait for them either, so
340
+ // whatever switches the row back on (an admin's Reactivate, an invitation,
341
+ // which names its own role) never hands an old admin or owner role back.
342
+ // The account, and the person's memberships of other workspaces, are untouched.
343
+ // An already paused member can be removed: that turns the pause into a
344
+ // removal. The 2-segment gate above does NOT cover this 3-segment path, so
345
+ // it is gated explicitly. Machines never reach it: the path is deliberately
346
+ // UNLISTED in the fail-closed scope allowlist, like the account deletion.
347
+ // A hub-origin workspace died at the subtree gate with `hub_managed`.
348
+ api.use('/members/:id/remove', requireRole('admin'));
349
+ api.openapi(memberRemoveRoute, async (c) => {
350
+ const principal = c.get('principal')!;
351
+ const { id } = c.req.valid('param');
352
+
353
+ const [target] = await db
354
+ .select({
355
+ id: workspaceMembers.id,
356
+ userId: workspaceMembers.userId,
357
+ role: workspaceMembers.role,
358
+ isActive: workspaceMembers.isActive
359
+ })
360
+ .from(workspaceMembers)
361
+ .where(and(eq(workspaceMembers.id, id), eq(workspaceMembers.workspaceId, principal.workspaceId)))
362
+ .limit(1);
363
+ if (!target) return c.json(err('not_found', 'Member not found'), 404);
364
+ if (target.userId === principal.userId) {
365
+ return c.json(err('cannot_remove_self', 'You cannot remove yourself'), 400);
366
+ }
367
+ if (target.role === 'owner' && principal.role !== 'owner') {
368
+ return c.json(err('forbidden', 'Only an owner can remove an owner'), 403);
369
+ }
370
+
371
+ // The WHERE names the workspace again: the row is the one read above,
372
+ // and the statement could not reach another workspace's even if it were not.
373
+ // What the person held goes first, the row second: the same lock order
374
+ // as an invitation's accept, which locks the invitation and then the
375
+ // membership row (members/removal.ts, ORDER).
376
+ const remove = () =>
377
+ db.transaction(async (tx) => {
378
+ const removedRow = [
379
+ { memberId: target.id, workspaceId: principal.workspaceId, userId: target.userId }
380
+ ];
381
+ const counts = await deleteMembershipGrants(tx, removedRow, deps.onMembershipRemoval);
382
+ await tx
383
+ .update(workspaceMembers)
384
+ .set({ isActive: false, isDefault: false, role: 'member' })
385
+ .where(
386
+ and(eq(workspaceMembers.id, target.id), eq(workspaceMembers.workspaceId, principal.workspaceId))
387
+ );
388
+ // The second pass, after the row is off (the end-to-end verification,
389
+ // finding 5): an add that held the membership row FOR SHARE before the
390
+ // update (`holdLiveMembership`: a project grant, a team seat, a demo
391
+ // pass) made the update wait, and committed its right between the
392
+ // first pass and the update. This pass takes it. Any add after the
393
+ // update re-reads the row off and writes nothing, so two passes end it.
394
+ const late = await deleteMembershipGrants(tx, removedRow, deps.onMembershipRemoval);
395
+ return mergeRemovalCounts(counts, late);
396
+ });
397
+ let removed: RemovalCounts;
398
+ try {
399
+ // Removing an ACTIVE owner runs under the race-free last-owner guard,
400
+ // as the pause and the account deletion do.
401
+ removed =
402
+ target.role === 'owner' && target.isActive
403
+ ? await accountDeletion.withLastOwnerGuard(principal.workspaceId, target.userId, remove)
404
+ : await remove();
405
+ } catch (cause) {
406
+ if (cause instanceof LastOwnerError || isLastOwnerDbError(cause)) {
407
+ return c.json(err('last_owner', 'The workspace must keep at least one active owner'), 400);
408
+ }
409
+ throw cause;
410
+ }
411
+
412
+ const [after] = await db
413
+ .select({
414
+ id: workspaceMembers.id,
415
+ userId: workspaceMembers.userId,
416
+ email: userTable.email,
417
+ name: userTable.name,
418
+ role: workspaceMembers.role,
419
+ isActive: workspaceMembers.isActive,
420
+ createdAt: workspaceMembers.createdAt,
421
+ lastSeenAt: workspaceMembers.lastSeenAt
422
+ })
423
+ .from(workspaceMembers)
424
+ .innerJoin(userTable, eq(workspaceMembers.userId, userTable.id))
425
+ .where(eq(workspaceMembers.id, target.id))
426
+ .limit(1);
427
+
428
+ c.set('audit', {
429
+ action: 'member.remove',
430
+ resourceType: 'member',
431
+ resourceId: target.id,
432
+ // The tool's counts first: a name the chassis writes is the chassis's.
433
+ metadata: {
434
+ ...removed.tool,
435
+ targetUserId: target.userId,
436
+ roleBefore: target.role,
437
+ projectGrants: removed.projectGrants,
438
+ teamSeats: removed.teamSeats,
439
+ invitations: removed.invitations,
440
+ demoPasses: removed.demoPasses
441
+ }
442
+ });
443
+ return c.json(toWire(after!), 200);
444
+ });
445
+
446
+ // The shared refusal of every route that mints a sign-in-equivalent
447
+ // credential for another user (PRDCT-1354): guest targets and targets
448
+ // belonging to another workspace. Its own module (accounts/mint-refusal.ts),
449
+ // shared with the demo pass mint; bound here to this router's db and copy.
450
+ const mintRefusal = (principal: { workspaceId: string }, target: { userId: string; origin: string }) =>
451
+ sharedMintRefusal(db, deps.guestTargetMessage, principal, target);
452
+
453
+ // Owner-generated password reset link — the recovery path that works with
454
+ // no email driver (invitations' copyable-link pattern). The 2-segment gate
455
+ // above does NOT cover this 3-segment path, so gate it explicitly — and at
456
+ // OWNER, not admin (AUTH-8: see mintRefusal).
457
+ api.use('/members/:id/reset-link', requireRole('owner'));
458
+ api.openapi(memberResetLinkRoute, async (c) => {
459
+ // Cloud edition (`hubManaged` is present iff EDITION=cloud): the token a
460
+ // mint would produce lands on POST /reset-password, which the D1 hub-only
461
+ // posture refuses there (identity/better-auth.ts isPasswordResetPath) —
462
+ // so minting would hand admins a dead link. Refuse at the source instead.
463
+ // Hub-origin workspaces already died at the subtree gate above with
464
+ // `hub_managed`; this covers cloud-LOCAL workspaces (the operator's,
465
+ // guest hosts). Recovery for local accounts there: hub SSO for
466
+ // humans, break-glass for the operator. oss is untouched.
467
+ if (hubManaged) {
468
+ return c.json(
469
+ err(
470
+ 'password_reset_disabled',
471
+ 'Local password reset is disabled on this edition — credentials are managed at the Antasphere hub'
472
+ ),
473
+ 403
474
+ );
475
+ }
476
+ const principal = c.get('principal')!;
477
+ const { id } = c.req.valid('param');
478
+
479
+ const [target] = await db
480
+ .select({
481
+ id: workspaceMembers.id,
482
+ userId: workspaceMembers.userId,
483
+ role: workspaceMembers.role,
484
+ origin: workspaceMembers.origin
485
+ })
486
+ .from(workspaceMembers)
487
+ .where(and(eq(workspaceMembers.id, id), eq(workspaceMembers.workspaceId, principal.workspaceId)))
488
+ .limit(1);
489
+ if (!target) return c.json(err('not_found', 'Member not found'), 404);
490
+ const refusal = await mintRefusal(principal, target);
491
+ if (refusal) return c.json(err(refusal.code, refusal.message), 403);
492
+
493
+ // Mint a single-use token in Better Auth's own verification table, in the
494
+ // exact shape POST /reset-password consumes (`reset-password:<token>`).
495
+ const token = randomBytes(24).toString('base64url');
496
+ const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1h
497
+ const authCtx = await auth.$context;
498
+ await authCtx.internalAdapter.createVerificationValue({
499
+ identifier: `reset-password:${token}`,
500
+ value: target.userId,
501
+ expiresAt
502
+ });
503
+
504
+ c.set('audit', {
505
+ action: 'member.reset_link',
506
+ resourceType: 'member',
507
+ resourceId: target.id,
508
+ metadata: { targetUserId: target.userId }
509
+ });
510
+ return c.json(
511
+ {
512
+ resetUrl: `${publicBaseUrl.replace(/\/+$/, '')}/reset-password?token=${token}`,
513
+ expiresAt: expiresAt.toISOString()
514
+ },
515
+ 200
516
+ );
517
+ });
518
+
519
+ // Owner-generated email change link — the email-change path that works
520
+ // with no email driver (same copyable-link pattern as the reset link).
521
+ // The 2-segment gate above does NOT cover this 3-segment path either, so
522
+ // gate it explicitly — at OWNER, like its sibling (AUTH-8).
523
+ api.use('/members/:id/change-email-link', requireRole('owner'));
524
+ api.openapi(memberChangeEmailLinkRoute, async (c) => {
525
+ // Cloud edition (`hubManaged` is present iff EDITION=cloud): the SAME
526
+ // closure the reset-link sibling carries, which this route was missing
527
+ // (AUTH-1). It is the stronger of the two — the minted JWT is
528
+ // sign-in-equivalent AND rewrites the address the hub identity is keyed
529
+ // on, while on cloud a user's email is the hub's to own (D10 re-syncs it
530
+ // on every login, so a local rewrite is either overwritten or a
531
+ // hijack). Hub-origin workspaces already died at the P7 subtree gate
532
+ // with `hub_managed`; this covers cloud-LOCAL workspaces (the
533
+ // operator's, guest hosts). oss is untouched.
534
+ if (hubManaged) {
535
+ return c.json(
536
+ err(
537
+ 'email_change_disabled',
538
+ 'Local email changes are disabled on this edition — identities are managed at the Antasphere hub'
539
+ ),
540
+ 403
541
+ );
542
+ }
543
+ const principal = c.get('principal')!;
544
+ const { id } = c.req.valid('param');
545
+ const body = c.req.valid('json');
546
+
547
+ const [target] = await db
548
+ .select({
549
+ id: workspaceMembers.id,
550
+ userId: workspaceMembers.userId,
551
+ role: workspaceMembers.role,
552
+ origin: workspaceMembers.origin,
553
+ email: userTable.email
554
+ })
555
+ .from(workspaceMembers)
556
+ .innerJoin(userTable, eq(workspaceMembers.userId, userTable.id))
557
+ .where(and(eq(workspaceMembers.id, id), eq(workspaceMembers.workspaceId, principal.workspaceId)))
558
+ .limit(1);
559
+ if (!target) return c.json(err('not_found', 'Member not found'), 404);
560
+ const refusal = await mintRefusal(principal, target);
561
+ if (refusal) return c.json(err(refusal.code, refusal.message), 403);
562
+
563
+ // Better Auth stores emails lowercased — compare and mint in that form.
564
+ // ⚠️ RACE-7: this lookup is ADVISORY ONLY, never the boundary. It is a
565
+ // read outside any lock, and the mint writes nothing (the token is a
566
+ // stateless JWT — LESSONS.md M6), so two concurrent mints for the same
567
+ // `newEmail` both pass it. The REAL enforcement is the `user.email`
568
+ // UNIQUE constraint at CONSUMPTION time: whichever link is consumed
569
+ // first wins the address and the loser's link fails, leaving exactly one
570
+ // account on that email (asserted in test/integration/minted-credentials
571
+ // .test.ts). Never promote this pre-check to a uniqueness guarantee.
572
+ // RACE-7 scope choice: joined on THIS workspace's membership rows — an
573
+ // unscoped user lookup made the 409-vs-200 an instance-global account
574
+ // oracle; the owner already sees these emails on the roster.
575
+ // ⚠️ The constraint at consumption is OBSERVABLE, not a silent backstop
576
+ // (PRDCT-1437): a colliding consume used to answer a raw 500 against the
577
+ // free case's 302 — a mint-then-consume account oracle. The consume path
578
+ // now answers the SAME redirect + session on both branches (the
579
+ // collision rewrite at the /auth mount — rewriteChangeEmailCollision in
580
+ // api/index.ts re-signs the token as a no-op change so Better Auth runs
581
+ // its native success path), and the honest residual — the owner can always
582
+ // read back whether the change TOOK, because email uniqueness is
583
+ // instance-global — is documented in ADR 013's amendment.
584
+ const newEmail = body.newEmail.toLowerCase();
585
+ const [existing] = await db
586
+ .select({ id: userTable.id })
587
+ .from(userTable)
588
+ .innerJoin(workspaceMembers, eq(workspaceMembers.userId, userTable.id))
589
+ .where(and(eq(userTable.email, newEmail), eq(workspaceMembers.workspaceId, principal.workspaceId)))
590
+ .limit(1);
591
+ if (existing) {
592
+ return c.json(err('email_taken', 'That email is already in use'), 409);
593
+ }
594
+
595
+ // Mint the exact stateless JWT GET /verify-email consumes for an email
596
+ // change ({email: current, updateTo: new, requestType}), signed with the
597
+ // auth secret — Better Auth's own helper, never hand-rolled jose.
598
+ // ⚠️ Consuming this link updates the email AND signs the target in (a
599
+ // session is created when none exists) — it is sign-in-equivalent. Hand
600
+ // it to the target member only, never to anyone else.
601
+ const expiresIn = 3600; // 1h
602
+ const expiresAt = new Date(Date.now() + expiresIn * 1000);
603
+ const authCtx = await auth.$context;
604
+ const token = await createEmailVerificationToken(authCtx.secret, target.email, newEmail, expiresIn, {
605
+ requestType: 'change-email-verification'
606
+ });
607
+ const verifyUrl = `${publicBaseUrl.replace(/\/+$/, '')}/api/v1/auth/verify-email?token=${token}&callbackURL=%2Faccount`;
608
+
609
+ c.set('audit', {
610
+ action: 'member.change_email_link',
611
+ resourceType: 'member',
612
+ resourceId: target.id,
613
+ metadata: { targetUserId: target.userId, newEmail }
614
+ });
615
+ return c.json({ verifyUrl, newEmail, expiresAt: expiresAt.toISOString() }, 200);
616
+ });
617
+ }
@@ -0,0 +1,50 @@
1
+ import type { OpenAPIHono } from '@hono/zod-openapi';
2
+ import { sql } from 'drizzle-orm';
3
+ import { onboardingDismissRoute } from '@antasphere/chassis-contract/routes';
4
+ import { userOnboarding, type Db } from '@antasphere/chassis-db';
5
+ import type { Auth } from '../identity/better-auth.js';
6
+
7
+ const err = (code: string, message: string) => ({ error: { code, message } });
8
+
9
+ /**
10
+ * POST /me/onboarding/dismiss — the explicit act that hides the first-run
11
+ * welcome (SL-6, cloud edition ONLY; api/index.ts registers this module iff
12
+ * the instance boots EDITION=cloud, so oss answers the JSON 404 terminator).
13
+ *
14
+ * Session-only, same construction as /sso/logout: the path is deliberately
15
+ * UNLISTED in the fail-closed scope allowlist (middleware/scopes.ts) — every
16
+ * API key / OAuth bearer 403s in authContext before this runs — and the
17
+ * session resolves ROUTE-LOCALLY (the /me zero-state pattern) so a
18
+ * zero-membership session can dismiss too.
19
+ *
20
+ * The upsert is idempotent and preserves the FIRST dismissal timestamp: a
21
+ * second dismiss (double-click, replayed request) changes nothing. Only this
22
+ * route (and the deploy backfill) ever sets dismissed_at — the login path's
23
+ * lazy insert writes NULL — so `firstRunPending` flips exactly on the user's
24
+ * explicit act, retry-safe on every other path.
25
+ */
26
+
27
+ export interface OnboardingRouteDeps {
28
+ db: Db;
29
+ auth: Auth;
30
+ }
31
+
32
+ export function registerOnboardingRoutes(api: OpenAPIHono, deps: OnboardingRouteDeps): void {
33
+ const { db, auth } = deps;
34
+
35
+ api.openapi(onboardingDismissRoute, async (c) => {
36
+ const session = await auth.api.getSession({ headers: c.req.raw.headers });
37
+ if (!session?.user) {
38
+ return c.json(err('unauthenticated', 'Authentication required'), 401);
39
+ }
40
+ await db
41
+ .insert(userOnboarding)
42
+ .values({ userId: session.user.id, dismissedAt: new Date() })
43
+ .onConflictDoUpdate({
44
+ target: userOnboarding.userId,
45
+ // Keep an earlier dismissal's timestamp; fill only a NULL.
46
+ set: { dismissedAt: sql`coalesce(${userOnboarding.dismissedAt}, excluded.dismissed_at)` }
47
+ });
48
+ return c.json({ dismissed: true as const }, 200);
49
+ });
50
+ }