@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,794 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql';
3
+ import { and, eq } from 'drizzle-orm';
4
+ import {
5
+ invitations,
6
+ projectMembers,
7
+ projectTeams,
8
+ user as userTable,
9
+ workspaceMembers,
10
+ workspaceTeamMembers,
11
+ workspaceTeams
12
+ } from '@antasphere/chassis-db';
13
+ import { deleteMembershipGrants, type RemovedMembership } from '../../src/members/removal.js';
14
+ import {
15
+ createDatabase,
16
+ createTestApp,
17
+ extractCookie,
18
+ readJson,
19
+ startPostgres,
20
+ type TestApp,
21
+ host
22
+ } from './helpers.js';
23
+
24
+ /**
25
+ * Removing a member (PRDCT-2816): `POST /members/{id}/remove`, the act between
26
+ * the pause and the account erasure. Pinned: the membership row is switched
27
+ * off and KEPT (is_active and is_default false), the person's project grants
28
+ * and team seats are deleted with it (their teams and the teams' project
29
+ * entries stay, and so do everybody else's), the account and the person's
30
+ * memberships of other workspaces are untouched; the removed person no longer
31
+ * reaches the workspace; invited back, they reactivate the SAME row with no
32
+ * grant and no seat; a pause keeps both (the contrast) and a paused member can
33
+ * still be removed; another workspace's membership reads as absent; who may
34
+ * remove whom (anonymous, plain member, admin on an owner, oneself, owner on
35
+ * owner, the last-owner guard under a race); machine credentials never reach
36
+ * the route; and the `member.remove` audit record with its counts.
37
+ */
38
+ const PASSWORD = 'a-long-removal-password-1';
39
+ const OWNER = { email: 'owner@removal.test', name: 'Olive Owner', password: PASSWORD };
40
+
41
+ type Actor = { email: string; cookie: string; userId: string; memberId: string };
42
+
43
+ let container: StartedPostgreSqlContainer;
44
+ let app: TestApp;
45
+ let workspaceId = '';
46
+ let otherWorkspaceId = '';
47
+ const actors: Record<string, Actor> = {};
48
+
49
+ let ipCounter = 0;
50
+ const nextIp = () => `10.81.${Math.floor(ipCounter / 250)}.${(ipCounter++ % 250) + 1}`;
51
+
52
+ const send = (
53
+ method: string,
54
+ path: string,
55
+ who: { cookie?: string; key?: string },
56
+ body?: unknown,
57
+ headers: Record<string, string> = {}
58
+ ) =>
59
+ app.app.request(`/api/v1${path}`, {
60
+ method,
61
+ headers: {
62
+ 'x-forwarded-for': nextIp(),
63
+ ...(who.cookie ? { cookie: who.cookie } : {}),
64
+ ...(who.key ? { authorization: `Bearer ${who.key}` } : {}),
65
+ ...(body !== undefined ? { 'content-type': 'application/json' } : {}),
66
+ ...headers
67
+ },
68
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {})
69
+ });
70
+
71
+ async function expectError(res: Response, status: number, code: string): Promise<void> {
72
+ const body = await readJson(res);
73
+ expect({ status: res.status, code: body?.error?.code }).toEqual({ status, code });
74
+ }
75
+
76
+ async function signIn(email: string): Promise<string> {
77
+ const res = await send('POST', '/auth/sign-in/email', {}, { email, password: PASSWORD });
78
+ expect(res.status).toBe(200);
79
+ return extractCookie(res);
80
+ }
81
+
82
+ async function addActor(
83
+ name: string,
84
+ opts: { role?: 'owner' | 'admin' | 'member'; origin?: 'local' | 'guest'; workspace?: string } = {}
85
+ ): Promise<Actor> {
86
+ const email = `${name}@removal.test`;
87
+ const created = await app.auth.api.signUpEmail({ body: { email, password: PASSWORD, name } });
88
+ const [row] = await app.db.db
89
+ .insert(workspaceMembers)
90
+ .values({
91
+ workspaceId: opts.workspace ?? workspaceId,
92
+ userId: created.user.id,
93
+ role: opts.role ?? 'member',
94
+ origin: opts.origin ?? 'local'
95
+ })
96
+ .returning({ id: workspaceMembers.id });
97
+ const actor = { email, cookie: await signIn(email), userId: created.user.id, memberId: row!.id };
98
+ actors[name] = actor;
99
+ return actor;
100
+ }
101
+
102
+ async function mintKey(cookie: string, scopes: string[]): Promise<string> {
103
+ const res = await send('POST', '/api-keys', { cookie }, { name: `key-${scopes.join('-')}`, scopes });
104
+ expect(res.status).toBe(201);
105
+ return (await readJson(res)).key;
106
+ }
107
+
108
+ async function createProject(
109
+ who: { cookie?: string },
110
+ name: string,
111
+ headers: Record<string, string> = {}
112
+ ): Promise<string> {
113
+ const res = await send('POST', '/projects', who, { name }, headers);
114
+ expect(res.status).toBe(201);
115
+ return (await readJson(res)).id;
116
+ }
117
+
118
+ async function createTeam(who: { cookie?: string }, name: string): Promise<string> {
119
+ const res = await send('POST', '/teams', who, { name });
120
+ expect(res.status).toBe(201);
121
+ return (await readJson(res)).id;
122
+ }
123
+
124
+ async function grant(
125
+ projectId: string,
126
+ who: Actor,
127
+ role: 'manager' | 'editor' | 'viewer',
128
+ headers: Record<string, string> = {}
129
+ ): Promise<void> {
130
+ const res = await send(
131
+ 'POST',
132
+ `/projects/${projectId}/members`,
133
+ actors.owner!,
134
+ { userId: who.userId, role },
135
+ headers
136
+ );
137
+ expect(res.status).toBe(201);
138
+ }
139
+
140
+ async function seatIn(teamId: string, who: Actor): Promise<void> {
141
+ const res = await send('POST', `/teams/${teamId}/members`, actors.owner!, { userId: who.userId });
142
+ expect(res.status).toBe(201);
143
+ }
144
+
145
+ const remove = (memberId: string, who: { cookie?: string; key?: string }, headers?: Record<string, string>) =>
146
+ send('POST', `/members/${memberId}/remove`, who, undefined, headers);
147
+
148
+ const memberRow = async (memberId: string) =>
149
+ (await app.db.db.select().from(workspaceMembers).where(eq(workspaceMembers.id, memberId)))[0];
150
+ const grantsOf = (memberId: string) =>
151
+ app.db.db.select().from(projectMembers).where(eq(projectMembers.memberId, memberId));
152
+ const seatsOf = (memberId: string) =>
153
+ app.db.db.select().from(workspaceTeamMembers).where(eq(workspaceTeamMembers.memberId, memberId));
154
+ const listedProjects = async (who: Actor): Promise<string[]> => {
155
+ const res = await send('GET', '/projects?limit=100', who);
156
+ expect(res.status).toBe(200);
157
+ return (await readJson(res)).projects.map((p: { id: string }) => p.id);
158
+ };
159
+
160
+ beforeAll(async () => {
161
+ container = await startPostgres();
162
+ app = await createTestApp(await createDatabase(container, 'member_removal'));
163
+ const setup = await send(
164
+ 'POST',
165
+ '/setup',
166
+ {},
167
+ { setupToken: 'integration-test-setup-token', instanceName: 'Removal', owner: OWNER }
168
+ );
169
+ expect(setup.status).toBe(201);
170
+ workspaceId = (await readJson(setup)).workspaceId;
171
+ const ownerCookie = await signIn(OWNER.email);
172
+ const me = await readJson(await send('GET', '/me', { cookie: ownerCookie }));
173
+ const [ownerRow] = await app.db.db
174
+ .select({ id: workspaceMembers.id })
175
+ .from(workspaceMembers)
176
+ .where(and(eq(workspaceMembers.workspaceId, workspaceId), eq(workspaceMembers.userId, me.user.id)));
177
+ actors.owner = { email: OWNER.email, cookie: ownerCookie, userId: me.user.id, memberId: ownerRow!.id };
178
+ otherWorkspaceId = (await app.registry.workspaces.create('Elsewhere', me.user.id)).workspaceId;
179
+
180
+ await addActor('admin', { role: 'admin' });
181
+ for (const name of ['leaver', 'stayer', 'member', 'paused', 'both', 'target', 'defaulted']) {
182
+ await addActor(name);
183
+ }
184
+ }, 180_000);
185
+
186
+ afterAll(async () => {
187
+ await app?.stop();
188
+ await container?.stop();
189
+ });
190
+
191
+ describe('a removal takes the grants and the seats, and keeps the account', () => {
192
+ let p1 = '';
193
+ let p2 = '';
194
+ let t1 = '';
195
+
196
+ beforeAll(async () => {
197
+ p1 = await createProject(actors.owner!, 'Removal P1');
198
+ p2 = await createProject(actors.owner!, 'Removal P2');
199
+ t1 = await createTeam(actors.owner!, 'Removal T1');
200
+ await grant(p1, actors.leaver!, 'editor');
201
+ await grant(p1, actors.stayer!, 'viewer');
202
+ await seatIn(t1, actors.leaver!);
203
+ await seatIn(t1, actors.stayer!);
204
+ const onP2 = await send('POST', `/projects/${p2}/members`, actors.owner!, { teamId: t1, role: 'viewer' });
205
+ expect(onP2.status).toBe(201);
206
+ });
207
+
208
+ it('removes: 200 with the member off; the row kept, the grant and the seat gone, the rest untouched', async () => {
209
+ const leaver = actors.leaver!;
210
+ const stayer = actors.stayer!;
211
+ expect(await listedProjects(leaver)).toEqual(expect.arrayContaining([p1, p2]));
212
+
213
+ const res = await remove(leaver.memberId, actors.admin!);
214
+ expect(res.status).toBe(200);
215
+ const body = await readJson(res);
216
+ expect(body).toMatchObject({
217
+ id: leaver.memberId,
218
+ userId: leaver.userId,
219
+ email: leaver.email,
220
+ role: 'member',
221
+ isActive: false
222
+ });
223
+
224
+ expect(await memberRow(leaver.memberId)).toMatchObject({
225
+ workspaceId,
226
+ isActive: false,
227
+ isDefault: false
228
+ });
229
+ expect(await grantsOf(leaver.memberId)).toHaveLength(0);
230
+ expect(await seatsOf(leaver.memberId)).toHaveLength(0);
231
+ expect(await app.db.db.select().from(userTable).where(eq(userTable.id, leaver.userId))).toHaveLength(1);
232
+
233
+ // The team and its place on P2 stay.
234
+ expect(await app.db.db.select().from(workspaceTeams).where(eq(workspaceTeams.id, t1))).toHaveLength(1);
235
+ expect(
236
+ await app.db.db
237
+ .select()
238
+ .from(projectTeams)
239
+ .where(and(eq(projectTeams.projectId, p2), eq(projectTeams.teamId, t1)))
240
+ ).toHaveLength(1);
241
+
242
+ // Everybody else's grant on P1 and seat in T1 stay.
243
+ expect(await grantsOf(stayer.memberId)).toEqual([
244
+ expect.objectContaining({ projectId: p1, role: 'viewer' })
245
+ ]);
246
+ expect(await seatsOf(stayer.memberId)).toEqual([expect.objectContaining({ teamId: t1 })]);
247
+ expect(await listedProjects(stayer)).toEqual(expect.arrayContaining([p1, p2]));
248
+ });
249
+
250
+ it('the removed person no longer reaches the workspace', async () => {
251
+ const leaver = actors.leaver!;
252
+ // /me still answers the signed-in account, without the workspace.
253
+ const me = await send('GET', '/me', leaver);
254
+ expect(me.status).toBe(200);
255
+ const workspaces = (await readJson(me)).workspaces as Array<{ id: string }>;
256
+ expect(Array.isArray(workspaces)).toBe(true);
257
+ expect(workspaces.map((w) => w.id)).not.toContain(workspaceId);
258
+ // Naming the workspace resolves no membership, so no principal: pinned as
259
+ // observed, 401 unauthenticated.
260
+ await expectError(
261
+ await send('GET', '/projects', leaver, undefined, { 'x-workspace-id': workspaceId }),
262
+ 401,
263
+ 'unauthenticated'
264
+ );
265
+ });
266
+
267
+ it('invited back, the person reactivates the SAME row and starts clean', async () => {
268
+ const leaver = actors.leaver!;
269
+ const invited = await send('POST', '/invitations', actors.owner!, {
270
+ email: leaver.email,
271
+ role: 'member'
272
+ });
273
+ expect(invited.status).toBe(201);
274
+ const token = ((await readJson(invited)).acceptUrl as string).split('/invite/')[1]!;
275
+ const accepted = await send('POST', '/invitations/accept', leaver, { token });
276
+ expect(accepted.status).toBe(200);
277
+
278
+ const rows = await app.db.db
279
+ .select()
280
+ .from(workspaceMembers)
281
+ .where(and(eq(workspaceMembers.workspaceId, workspaceId), eq(workspaceMembers.userId, leaver.userId)));
282
+ expect(rows).toHaveLength(1);
283
+ expect(rows[0]).toMatchObject({ id: leaver.memberId, isActive: true });
284
+ const listed = await listedProjects(leaver);
285
+ expect(listed).not.toContain(p1);
286
+ expect(listed).not.toContain(p2);
287
+ expect(await grantsOf(leaver.memberId)).toHaveLength(0);
288
+ expect(await seatsOf(leaver.memberId)).toHaveLength(0);
289
+ });
290
+ });
291
+
292
+ describe('a pause keeps the grants and the seats (the contrast)', () => {
293
+ it('paused: both rows stay; then removed while paused: 200 and both rows gone', async () => {
294
+ const paused = actors.paused!;
295
+ const project = await createProject(actors.owner!, 'Pause project');
296
+ const team = await createTeam(actors.owner!, 'Pause team');
297
+ await grant(project, paused, 'editor');
298
+ await seatIn(team, paused);
299
+
300
+ const pause = await send('PATCH', `/members/${paused.memberId}`, actors.admin!, { isActive: false });
301
+ expect(pause.status).toBe(200);
302
+ expect(await memberRow(paused.memberId)).toMatchObject({ isActive: false });
303
+ expect(await grantsOf(paused.memberId)).toHaveLength(1);
304
+ expect(await seatsOf(paused.memberId)).toHaveLength(1);
305
+
306
+ const res = await remove(paused.memberId, actors.admin!);
307
+ expect(res.status).toBe(200);
308
+ expect(await readJson(res)).toMatchObject({ id: paused.memberId, isActive: false });
309
+ expect(await grantsOf(paused.memberId)).toHaveLength(0);
310
+ expect(await seatsOf(paused.memberId)).toHaveLength(0);
311
+ expect(await memberRow(paused.memberId)).toMatchObject({ isActive: false });
312
+ });
313
+ });
314
+
315
+ describe('another workspace’s member is absent', () => {
316
+ it('a membership of another workspace is 404 not_found and stays as it was', async () => {
317
+ const created = await app.auth.api.signUpEmail({
318
+ body: { email: 'foreigner@removal.test', password: PASSWORD, name: 'foreigner' }
319
+ });
320
+ const [row] = await app.db.db
321
+ .insert(workspaceMembers)
322
+ .values({ workspaceId: otherWorkspaceId, userId: created.user.id, role: 'member' })
323
+ .returning({ id: workspaceMembers.id });
324
+ await expectError(await remove(row!.id, actors.owner!), 404, 'not_found');
325
+ expect(await memberRow(row!.id)).toMatchObject({ workspaceId: otherWorkspaceId, isActive: true });
326
+ });
327
+
328
+ it('a well-formed unknown id is 404, a non-uuid id is 400', async () => {
329
+ await expectError(await remove('0f0f0f0f-1111-4222-8333-444455556666', actors.owner!), 404, 'not_found');
330
+ await expectError(await remove('not-a-uuid', actors.owner!), 400, 'validation_error');
331
+ });
332
+ });
333
+
334
+ describe('the person’s other workspace is untouched', () => {
335
+ it('removed here, the membership and the grant there stay', async () => {
336
+ const both = actors.both!;
337
+ const there = { 'x-workspace-id': otherWorkspaceId };
338
+ const [thereRow] = await app.db.db
339
+ .insert(workspaceMembers)
340
+ .values({ workspaceId: otherWorkspaceId, userId: both.userId, role: 'member' })
341
+ .returning({ id: workspaceMembers.id });
342
+ // Both projects through the routes: the owner owns both workspaces.
343
+ const here = await createProject(actors.owner!, 'Both: here');
344
+ const away = await createProject(actors.owner!, 'Both: there', there);
345
+ await grant(here, both, 'viewer');
346
+ await grant(away, both, 'editor', there);
347
+ expect(await grantsOf(thereRow!.id)).toHaveLength(1);
348
+
349
+ expect((await remove(both.memberId, actors.owner!)).status).toBe(200);
350
+
351
+ expect(await grantsOf(both.memberId)).toHaveLength(0);
352
+ expect(await memberRow(thereRow!.id)).toMatchObject({ workspaceId: otherWorkspaceId, isActive: true });
353
+ expect(await grantsOf(thereRow!.id)).toEqual([
354
+ expect.objectContaining({ projectId: away, role: 'editor' })
355
+ ]);
356
+ const listed = await send('GET', '/projects?limit=100', both, undefined, there);
357
+ expect(listed.status).toBe(200);
358
+ expect((await readJson(listed)).projects.map((p: { id: string }) => p.id)).toContain(away);
359
+ });
360
+ });
361
+
362
+ describe('who may remove whom', () => {
363
+ it('anonymous is 401', async () => {
364
+ await expectError(await remove(actors.target!.memberId, {}), 401, 'unauthenticated');
365
+ });
366
+
367
+ it('a plain member is 403', async () => {
368
+ await expectError(await remove(actors.target!.memberId, actors.member!), 403, 'forbidden');
369
+ expect(await memberRow(actors.target!.memberId)).toMatchObject({ isActive: true });
370
+ });
371
+
372
+ it('the caller removing themself is 400 cannot_remove_self', async () => {
373
+ await expectError(await remove(actors.admin!.memberId, actors.admin!), 400, 'cannot_remove_self');
374
+ await expectError(await remove(actors.owner!.memberId, actors.owner!), 400, 'cannot_remove_self');
375
+ expect(await memberRow(actors.admin!.memberId)).toMatchObject({ isActive: true });
376
+ });
377
+
378
+ it('an owner removes a second owner (200); an admin is refused an owner (403 forbidden)', async () => {
379
+ const ownerA = await addActor('owner-a', { role: 'owner' });
380
+ const ownerB = await addActor('owner-b', { role: 'owner' });
381
+ // A was not the last active owner: B removes A.
382
+ const res = await remove(ownerA.memberId, ownerB);
383
+ expect(res.status).toBe(200);
384
+ // The removed owner's row is left at the member role (PRDCT-2816 F1).
385
+ expect(await readJson(res)).toMatchObject({ id: ownerA.memberId, role: 'member', isActive: false });
386
+ expect(await memberRow(ownerA.memberId)).toMatchObject({ role: 'member', isActive: false });
387
+ // An admin never removes an owner, and nothing moves.
388
+ await expectError(await remove(ownerB.memberId, actors.admin!), 403, 'forbidden');
389
+ expect(await memberRow(ownerB.memberId)).toMatchObject({ role: 'owner', isActive: true });
390
+ });
391
+
392
+ // The last-owner guard (400 `last_owner`) is structurally unreachable by ONE
393
+ // sequential request: the target must be an active owner, the caller must be
394
+ // an owner (an admin meets 403 first) who is not the target (400
395
+ // cannot_remove_self), and the caller's own membership is active (the
396
+ // credential resolves only an active one) — so two active owners always
397
+ // exist when the handler runs. The guard answers only a RACE: two owners
398
+ // removing each other at once, the case last-owner-race.test.ts pins for the
399
+ // account deletion and the demotion. That race, in a workspace of its own:
400
+ it('two owners removing each other at once: exactly one wins, the loser meets last_owner, one owner stays', async () => {
401
+ const x = await addActor('race-x', { role: 'owner' });
402
+ const raceWs = (await app.registry.workspaces.create('Race', x.userId)).workspaceId;
403
+ const created = await app.auth.api.signUpEmail({
404
+ body: { email: 'race-y@removal.test', password: PASSWORD, name: 'race-y' }
405
+ });
406
+ const [yRow] = await app.db.db
407
+ .insert(workspaceMembers)
408
+ .values({ workspaceId: raceWs, userId: created.user.id, role: 'owner' })
409
+ .returning({ id: workspaceMembers.id });
410
+ const yCookie = await signIn('race-y@removal.test');
411
+ const [xRow] = await app.db.db
412
+ .select({ id: workspaceMembers.id })
413
+ .from(workspaceMembers)
414
+ .where(and(eq(workspaceMembers.workspaceId, raceWs), eq(workspaceMembers.userId, x.userId)));
415
+ const inRace = { 'x-workspace-id': raceWs };
416
+
417
+ const results = await Promise.all([
418
+ remove(yRow!.id, x, inRace),
419
+ remove(xRow!.id, { cookie: yCookie }, inRace)
420
+ ]);
421
+ const statuses = results.map((r) => r.status);
422
+ expect(statuses.filter((s) => s === 200)).toHaveLength(1);
423
+ for (const r of results) {
424
+ if (r.status === 200) continue;
425
+ // 400 last_owner from the guard. A loser whose credential resolved
426
+ // after the winning removal landed never reaches the guard: its own
427
+ // membership is off by then (401), or its role is read as gone (403).
428
+ expect([400, 401, 403]).toContain(r.status);
429
+ if (r.status === 400) expect((await readJson(r)).error.code).toBe('last_owner');
430
+ }
431
+ const owners = await app.db.db
432
+ .select()
433
+ .from(workspaceMembers)
434
+ .where(
435
+ and(
436
+ eq(workspaceMembers.workspaceId, raceWs),
437
+ eq(workspaceMembers.role, 'owner'),
438
+ eq(workspaceMembers.isActive, true)
439
+ )
440
+ );
441
+ expect(owners).toHaveLength(1);
442
+ }, 60_000);
443
+ });
444
+
445
+ describe('machines never reach it', () => {
446
+ it('an owner’s key with both scopes is 403, and the target is unchanged', async () => {
447
+ const target = actors.target!;
448
+ const project = await createProject(actors.owner!, 'Machine project');
449
+ await grant(project, target, 'viewer');
450
+ const key = await mintKey(actors.owner!.cookie, [host.scopes.read, host.scopes.write]);
451
+ await expectError(await remove(target.memberId, { key }), 403, 'endpoint_not_allowed');
452
+ expect(await memberRow(target.memberId)).toMatchObject({ isActive: true });
453
+ expect(await grantsOf(target.memberId)).toHaveLength(1);
454
+ });
455
+ });
456
+
457
+ describe('the default flag goes with the membership', () => {
458
+ it('is_default is false on the removed row', async () => {
459
+ const defaulted = actors.defaulted!;
460
+ await app.db.db
461
+ .update(workspaceMembers)
462
+ .set({ isDefault: true })
463
+ .where(eq(workspaceMembers.id, defaulted.memberId));
464
+ expect(await memberRow(defaulted.memberId)).toMatchObject({ isDefault: true });
465
+ expect((await remove(defaulted.memberId, actors.owner!)).status).toBe(200);
466
+ expect(await memberRow(defaulted.memberId)).toMatchObject({ isActive: false, isDefault: false });
467
+ });
468
+ });
469
+
470
+ describe('a removed admin comes back as a plain member', () => {
471
+ it('removed: the row reads member and inactive; reactivated by an admin: a plain member', async () => {
472
+ const exAdmin = await addActor('ex-admin', { role: 'admin' });
473
+ const res = await remove(exAdmin.memberId, actors.owner!);
474
+ expect(res.status).toBe(200);
475
+ expect(await readJson(res)).toMatchObject({ id: exAdmin.memberId, role: 'member', isActive: false });
476
+ expect(await memberRow(exAdmin.memberId)).toMatchObject({ role: 'member', isActive: false });
477
+
478
+ const back = await send('PATCH', `/members/${exAdmin.memberId}`, actors.admin!, { isActive: true });
479
+ expect(back.status).toBe(200);
480
+ expect(await readJson(back)).toMatchObject({ id: exAdmin.memberId, role: 'member', isActive: true });
481
+ expect(await memberRow(exAdmin.memberId)).toMatchObject({ role: 'member', isActive: true });
482
+
483
+ // A plain member reads the roster, and is refused an admin's act.
484
+ expect((await send('GET', '/members', exAdmin)).status).toBe(200);
485
+ const invite = await send('POST', '/invitations', exAdmin, {
486
+ email: 'nobody-new@removal.test',
487
+ role: 'member'
488
+ });
489
+ expect(invite.status).toBe(403);
490
+ });
491
+ });
492
+
493
+ describe('deleteMembershipGrants, called directly (S06)', () => {
494
+ it('two memberships with a grant and a seat each: both counted, both gone; the hook sees both', async () => {
495
+ const d1 = await addActor('direct-1');
496
+ const d2 = await addActor('direct-2');
497
+ const project = await createProject(actors.owner!, 'Direct project');
498
+ const team = await createTeam(actors.owner!, 'Direct team');
499
+ for (const who of [d1, d2]) {
500
+ await grant(project, who, 'viewer');
501
+ await seatIn(team, who);
502
+ expect(await grantsOf(who.memberId)).toHaveLength(1);
503
+ expect(await seatsOf(who.memberId)).toHaveLength(1);
504
+ }
505
+ const removed: RemovedMembership[] = [d1, d2].map((who) => ({
506
+ memberId: who.memberId,
507
+ workspaceId,
508
+ userId: who.userId
509
+ }));
510
+
511
+ const plain = await app.db.db.transaction((tx) => deleteMembershipGrants(tx, removed));
512
+ expect(plain).toEqual({ projectGrants: 2, teamSeats: 2, invitations: 0, demoPasses: 0, tool: {} });
513
+ for (const who of [d1, d2]) {
514
+ expect(await grantsOf(who.memberId)).toHaveLength(0);
515
+ expect(await seatsOf(who.memberId)).toHaveLength(0);
516
+ }
517
+
518
+ const seen: Array<readonly RemovedMembership[]> = [];
519
+ const hooked = await app.db.db.transaction((tx) =>
520
+ deleteMembershipGrants(tx, removed, async (_tx, rows) => {
521
+ seen.push(rows);
522
+ return { things: 3 };
523
+ })
524
+ );
525
+ expect(hooked).toEqual({
526
+ projectGrants: 0,
527
+ teamSeats: 0,
528
+ invitations: 0,
529
+ demoPasses: 0,
530
+ tool: { things: 3 }
531
+ });
532
+ expect(seen).toHaveLength(1);
533
+ expect(seen[0]).toEqual([
534
+ { memberId: d1.memberId, workspaceId, userId: d1.userId },
535
+ { memberId: d2.memberId, workspaceId, userId: d2.userId }
536
+ ]);
537
+ });
538
+
539
+ it('an empty list calls no hook and counts nothing', async () => {
540
+ let calls = 0;
541
+ const result = await app.db.db.transaction((tx) =>
542
+ deleteMembershipGrants(tx, [], async () => {
543
+ calls += 1;
544
+ return { things: 3 };
545
+ })
546
+ );
547
+ expect(result).toEqual({ projectGrants: 0, teamSeats: 0, invitations: 0, demoPasses: 0, tool: {} });
548
+ expect(calls).toBe(0);
549
+ });
550
+ });
551
+
552
+ describe('the invitations open at the removal', () => {
553
+ type Invited = { id: string; token: string };
554
+
555
+ const invite = async (
556
+ who: { cookie?: string },
557
+ email: string,
558
+ role: 'owner' | 'admin' | 'member',
559
+ headers: Record<string, string> = {}
560
+ ): Promise<Invited> => {
561
+ const res = await send('POST', '/invitations', who, { email, role }, headers);
562
+ expect(res.status).toBe(201);
563
+ const body = await readJson(res);
564
+ return { id: body.invitation.id, token: (body.acceptUrl as string).split('/invite/')[1]! };
565
+ };
566
+ const invitationRow = async (id: string) =>
567
+ (await app.db.db.select().from(invitations).where(eq(invitations.id, id)))[0]!;
568
+ const pause = async (who: Actor): Promise<void> => {
569
+ const res = await send('PATCH', `/members/${who.memberId}`, actors.admin!, { isActive: false });
570
+ expect(res.status).toBe(200);
571
+ };
572
+ const removalEntry = async (memberId: string) => {
573
+ const res = await send('GET', '/audit?action=member.remove&limit=100', actors.owner!);
574
+ expect(res.status).toBe(200);
575
+ const entries = (await readJson(res)).entries as Array<{
576
+ resourceId: string;
577
+ metadata: Record<string, unknown>;
578
+ }>;
579
+ const mine = entries.filter((e) => e.resourceId === memberId);
580
+ expect(mine).toHaveLength(1);
581
+ return mine[0]!;
582
+ };
583
+
584
+ it('N1: an invitation for a paused person, open at the removal, is revoked and brings nobody back', async () => {
585
+ const n1 = await addActor('n1-paused');
586
+ await pause(n1);
587
+ const open = await invite(actors.admin!, n1.email, 'admin');
588
+ expect(await invitationRow(open.id)).toMatchObject({ revokedAt: null, acceptedAt: null });
589
+
590
+ expect((await remove(n1.memberId, actors.owner!)).status).toBe(200);
591
+ expect((await invitationRow(open.id)).revokedAt).toBeInstanceOf(Date);
592
+
593
+ // Pinned as observed: a revoked invitation is not live, so the accept
594
+ // answers the lookup's 404 not_found.
595
+ await expectError(await send('POST', '/invitations/accept', n1, { token: open.token }), 404, 'not_found');
596
+ expect(await memberRow(n1.memberId)).toMatchObject({ isActive: false, role: 'member' });
597
+ });
598
+
599
+ it('an invitation made after the removal is the workspace’s own act: accepted, at the role it names', async () => {
600
+ const n1 = actors['n1-paused']!;
601
+ const after = await invite(actors.owner!, n1.email, 'admin');
602
+ const res = await send('POST', '/invitations/accept', n1, { token: after.token });
603
+ expect(res.status).toBe(200);
604
+ expect(await memberRow(n1.memberId)).toMatchObject({ isActive: true, role: 'admin' });
605
+ });
606
+
607
+ it('N2: an invitation a removed admin issued is revoked, and accepting it creates no member', async () => {
608
+ const issuer = await addActor('n2-issuer', { role: 'admin' });
609
+ const address = 'n2-other-address@removal.test';
610
+ const issued = await invite(issuer, address, 'admin');
611
+
612
+ expect((await remove(issuer.memberId, actors.owner!)).status).toBe(200);
613
+ expect((await invitationRow(issued.id)).revokedAt).toBeInstanceOf(Date);
614
+
615
+ // Clean context (no cookie), the account-creating shape. Pinned as observed.
616
+ await expectError(
617
+ await send(
618
+ 'POST',
619
+ '/invitations/accept',
620
+ {},
621
+ { token: issued.token, name: 'Other', password: PASSWORD }
622
+ ),
623
+ 404,
624
+ 'not_found'
625
+ );
626
+ const members = await app.db.db
627
+ .select({ id: workspaceMembers.id })
628
+ .from(workspaceMembers)
629
+ .innerJoin(userTable, eq(workspaceMembers.userId, userTable.id))
630
+ .where(eq(userTable.email, address));
631
+ expect(members).toHaveLength(0);
632
+ expect(await app.db.db.select().from(userTable).where(eq(userTable.email, address))).toHaveLength(0);
633
+ });
634
+
635
+ it('boundaries: another workspace, accepted, already revoked and other people’s rows are untouched; case is ignored', async () => {
636
+ const bound = await addActor('bound-case');
637
+ const elsewhere = { 'x-workspace-id': otherWorkspaceId };
638
+ const inOther = await invite(actors.owner!, bound.email, 'member', elsewhere);
639
+
640
+ // An ACCEPTED invitation for the person: paused, invited, accepted back.
641
+ await pause(bound);
642
+ const accepted = await invite(actors.owner!, bound.email, 'member');
643
+ expect((await send('POST', '/invitations/accept', bound, { token: accepted.token })).status).toBe(200);
644
+ const acceptedAt = (await invitationRow(accepted.id)).acceptedAt;
645
+ expect(acceptedAt).toBeInstanceOf(Date);
646
+
647
+ // An already REVOKED one, its revoked_at moved to a fixed old time.
648
+ await pause(bound);
649
+ const revoked = await invite(actors.owner!, bound.email, 'member');
650
+ expect((await send('DELETE', `/invitations/${revoked.id}`, actors.owner!)).status).toBe(200);
651
+ const oldRevokedAt = new Date('2020-01-01T00:00:00.000Z');
652
+ await app.db.db
653
+ .update(invitations)
654
+ .set({ revokedAt: oldRevokedAt })
655
+ .where(eq(invitations.id, revoked.id));
656
+
657
+ // An OPEN one whose stored address is in mixed case.
658
+ const mixed = await invite(actors.owner!, bound.email, 'member');
659
+ await app.db.db
660
+ .update(invitations)
661
+ .set({ email: 'Bound-Case@Removal.TEST' })
662
+ .where(eq(invitations.id, mixed.id));
663
+
664
+ // Someone else's invitation, issued by someone else.
665
+ const bystander = await invite(actors.admin!, 'bystander@removal.test', 'member');
666
+
667
+ const res = await remove(bound.memberId, actors.owner!);
668
+ expect(res.status).toBe(200);
669
+
670
+ expect(await invitationRow(inOther.id)).toMatchObject({ revokedAt: null, acceptedAt: null });
671
+ expect(await invitationRow(accepted.id)).toMatchObject({ acceptedAt, revokedAt: null });
672
+ expect((await invitationRow(revoked.id)).revokedAt).toEqual(oldRevokedAt);
673
+ expect(await invitationRow(bystander.id)).toMatchObject({ revokedAt: null, acceptedAt: null });
674
+ const mixedRow = await invitationRow(mixed.id);
675
+ expect(mixedRow.revokedAt).toBeInstanceOf(Date);
676
+ expect(mixedRow.acceptedAt).toBeNull();
677
+
678
+ // Only the mixed-case open row was this removal's to revoke.
679
+ expect((await removalEntry(bound.memberId)).metadata).toMatchObject({ invitations: 1 });
680
+ });
681
+
682
+ it('the member.remove entries count the invitations revoked', async () => {
683
+ expect((await removalEntry(actors['n1-paused']!.memberId)).metadata).toMatchObject({ invitations: 1 });
684
+ expect((await removalEntry(actors['n2-issuer']!.memberId)).metadata).toMatchObject({ invitations: 1 });
685
+ });
686
+
687
+ it('a direct call with two memberships, each with one invitation for them: invitations 2, both revoked', async () => {
688
+ const e1 = await addActor('direct-inv-1');
689
+ const e2 = await addActor('direct-inv-2');
690
+ const opened: Invited[] = [];
691
+ for (const who of [e1, e2]) {
692
+ await pause(who);
693
+ opened.push(await invite(actors.owner!, who.email, 'member'));
694
+ }
695
+ const removed: RemovedMembership[] = [e1, e2].map((who) => ({
696
+ memberId: who.memberId,
697
+ workspaceId,
698
+ userId: who.userId
699
+ }));
700
+ const result = await app.db.db.transaction((tx) => deleteMembershipGrants(tx, removed));
701
+ expect(result).toEqual({ projectGrants: 0, teamSeats: 0, invitations: 2, demoPasses: 0, tool: {} });
702
+ for (const row of opened) {
703
+ expect((await invitationRow(row.id)).revokedAt).toBeInstanceOf(Date);
704
+ }
705
+ });
706
+ });
707
+
708
+ // Round 3, F2: the removal locked the membership row and then the invitation,
709
+ // an accept locks the invitation and then the membership row; racing, one of
710
+ // the two died of a deadlock and answered 500. The removal now takes what the
711
+ // person held first and the row second, the accept's order.
712
+ describe('a removal racing an acceptance', () => {
713
+ it('eight rounds: never a 500, and the row always matches whoever won', async () => {
714
+ const observed: Record<string, number> = {};
715
+ for (let round = 1; round <= 8; round++) {
716
+ const racer = await addActor(`racer-${round}`);
717
+ const pause = await send('PATCH', `/members/${racer.memberId}`, actors.admin!, { isActive: false });
718
+ expect(pause.status).toBe(200);
719
+ const invited = await send('POST', '/invitations', actors.admin!, {
720
+ email: racer.email,
721
+ role: 'admin'
722
+ });
723
+ expect(invited.status).toBe(201);
724
+ const invitedBody = await readJson(invited);
725
+ const invitationId = invitedBody.invitation.id as string;
726
+ const token = (invitedBody.acceptUrl as string).split('/invite/')[1]!;
727
+
728
+ const [removed, accepted] = await Promise.all([
729
+ remove(racer.memberId, actors.owner!),
730
+ send('POST', '/invitations/accept', racer, { token })
731
+ ]);
732
+ const acceptCode = accepted.status === 200 ? '' : ` ${(await readJson(accepted))?.error?.code}`;
733
+ const row = await memberRow(racer.memberId);
734
+ const [invitation] = await app.db.db.select().from(invitations).where(eq(invitations.id, invitationId));
735
+ const state = {
736
+ isActive: row!.isActive,
737
+ role: row!.role,
738
+ revoked: invitation!.revokedAt !== null,
739
+ accepted: invitation!.acceptedAt !== null
740
+ };
741
+ const pair = `remove ${removed.status} / accept ${accepted.status}${acceptCode} → ${JSON.stringify(state)}`;
742
+ observed[pair] = (observed[pair] ?? 0) + 1;
743
+ console.log(`race round ${round}: ${pair}`);
744
+
745
+ expect(removed.status).not.toBe(500);
746
+ expect(accepted.status).not.toBe(500);
747
+ expect(removed.status).toBe(200);
748
+ expect([200, 404, 410]).toContain(accepted.status);
749
+ expect([
750
+ // The removal won: the invitation went with it.
751
+ { isActive: false, role: 'member', revoked: true, accepted: false },
752
+ // The acceptance committed first and the removal came after it, the
753
+ // later act: the person is out, and the invitation reads as used.
754
+ { isActive: false, role: 'member', revoked: false, accepted: true }
755
+ ]).toContainEqual(state);
756
+ }
757
+ console.log('removal vs accept race outcomes:', JSON.stringify(observed));
758
+ }, 120_000);
759
+ });
760
+
761
+ describe('the record', () => {
762
+ it('every removal wrote member.remove, with the target and the counts', async () => {
763
+ const res = await send('GET', '/audit?action=member.remove&limit=100', actors.owner!);
764
+ expect(res.status).toBe(200);
765
+ const entries = (await readJson(res)).entries as Array<{
766
+ action: string;
767
+ resourceType: string;
768
+ resourceId: string;
769
+ metadata: Record<string, unknown>;
770
+ }>;
771
+ const leaver = actors.leaver!;
772
+ const mine = entries.filter((e) => e.resourceId === leaver.memberId);
773
+ expect(mine).toHaveLength(1);
774
+ expect(mine[0]).toMatchObject({
775
+ action: 'member.remove',
776
+ resourceType: 'member',
777
+ metadata: { targetUserId: leaver.userId, roleBefore: 'member', projectGrants: 1, teamSeats: 1 }
778
+ });
779
+ for (const e of entries) {
780
+ expect(typeof e.metadata.projectGrants).toBe('number');
781
+ expect(typeof e.metadata.teamSeats).toBe('number');
782
+ }
783
+ const paused = entries.find((e) => e.resourceId === actors.paused!.memberId);
784
+ expect(paused?.metadata).toMatchObject({ projectGrants: 1, teamSeats: 1 });
785
+ // The role the person held before the removal is on the record.
786
+ const exAdmin = entries.find((e) => e.resourceId === actors['ex-admin']!.memberId);
787
+ expect(exAdmin?.metadata).toMatchObject({
788
+ targetUserId: actors['ex-admin']!.userId,
789
+ roleBefore: 'admin'
790
+ });
791
+ const ownerA = entries.find((e) => e.resourceId === actors['owner-a']!.memberId);
792
+ expect(ownerA?.metadata).toMatchObject({ roleBefore: 'owner' });
793
+ });
794
+ });