@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,344 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { eq } from 'drizzle-orm';
5
+ import { apiKeys, files, invitations, user as userTable, workspaceMembers } from '@antasphere/chassis-db';
6
+ import {
7
+ createDatabase,
8
+ createTestApp,
9
+ extractCookie,
10
+ readJson,
11
+ startPostgres,
12
+ type TestApp,
13
+ host
14
+ } from './helpers.js';
15
+
16
+ /**
17
+ * Cursor pagination across the list endpoints: exhaustive page walks (no
18
+ * duplicates, no gaps, strict (created_at, id) DESC), the same-timestamp
19
+ * tie-break (the microsecond regression the plain-id cursor exists for),
20
+ * cursor tolerance (malformed → page 1, unknown uuid → empty page), limit
21
+ * validation, the SQL-side soft-delete filter, and api-key scoping held
22
+ * across pages.
23
+ */
24
+
25
+ const OWNER = { email: 'owner@pagination.test', name: 'Page Owner', password: 'page-owner-password-1' };
26
+ const MEMBER = { email: 'member@pagination.test', name: 'Page Member', password: 'page-member-pass-123' };
27
+
28
+ let container: StartedPostgreSqlContainer;
29
+ let app: TestApp;
30
+ let ownerCookie: string;
31
+ let workspaceId: string;
32
+ let ownerUserId: string;
33
+
34
+ const json = (body: unknown) => ({
35
+ method: 'POST',
36
+ headers: { 'content-type': 'application/json' },
37
+ body: JSON.stringify(body)
38
+ });
39
+
40
+ /** Deterministic, distinct timestamps (UTC, second-spaced) for seeded rows. */
41
+ const ts = (i: number) => new Date(Date.UTC(2026, 0, 1, 0, 0, i));
42
+
43
+ async function listPage(path: string, query: Record<string, string>, cookie = ownerCookie) {
44
+ const qs = new URLSearchParams(query).toString();
45
+ return app.app.request(`/api/v1/${path}${qs ? `?${qs}` : ''}`, { headers: { cookie } });
46
+ }
47
+
48
+ interface WalkRow {
49
+ id: string;
50
+ createdAt: string;
51
+ }
52
+
53
+ /** Follow nextCursor to the end; the final page must answer nextCursor null. */
54
+ async function walk(path: string, key: string, limit: number, cookie = ownerCookie): Promise<WalkRow[]> {
55
+ const rows: WalkRow[] = [];
56
+ let cursor: string | undefined;
57
+ for (let guard = 0; guard < 50; guard++) {
58
+ const res = await listPage(path, { limit: String(limit), ...(cursor ? { cursor } : {}) }, cookie);
59
+ expect(res.status).toBe(200);
60
+ const body = await readJson(res);
61
+ const page = body[key] as WalkRow[];
62
+ expect(page.length).toBeLessThanOrEqual(limit);
63
+ rows.push(...page);
64
+ if (body.nextCursor === null) return rows;
65
+ expect(typeof body.nextCursor).toBe('string');
66
+ cursor = body.nextCursor;
67
+ }
68
+ throw new Error(`walk of /${path} did not terminate`);
69
+ }
70
+
71
+ /** No duplicates, no gaps: the walked ids are exactly the expected set. */
72
+ function expectExactly(rows: WalkRow[], expectedIds: string[]): void {
73
+ const ids = rows.map((r) => r.id);
74
+ expect(new Set(ids).size).toBe(ids.length);
75
+ expect(new Set(ids)).toEqual(new Set(expectedIds));
76
+ }
77
+
78
+ /** Strict (createdAt, id) DESC across page boundaries (ISO strings compare lexicographically). */
79
+ function expectStrictDesc(rows: WalkRow[]): void {
80
+ for (let i = 1; i < rows.length; i++) {
81
+ const prev = rows[i - 1]!;
82
+ const cur = rows[i]!;
83
+ const strictlyBefore =
84
+ cur.createdAt < prev.createdAt || (cur.createdAt === prev.createdAt && cur.id < prev.id);
85
+ expect(
86
+ strictlyBefore,
87
+ `row ${i} out of order: ${prev.createdAt}/${prev.id} then ${cur.createdAt}/${cur.id}`
88
+ ).toBe(true);
89
+ }
90
+ }
91
+
92
+ async function uploadFile(name: string, text: string): Promise<string> {
93
+ const res = await app.app.request(`/api/v1/files?name=${encodeURIComponent(name)}`, {
94
+ method: 'POST',
95
+ headers: { 'content-type': 'text/plain', cookie: ownerCookie },
96
+ body: text
97
+ });
98
+ expect(res.status).toBe(201);
99
+ return (await readJson(res)).file.id as string;
100
+ }
101
+
102
+ beforeAll(async () => {
103
+ container = await startPostgres();
104
+ app = await createTestApp(await createDatabase(container, 'pagination'));
105
+
106
+ await app.app.request(
107
+ '/api/v1/setup',
108
+ json({ setupToken: 'integration-test-setup-token', instanceName: 'Pagination', owner: OWNER })
109
+ );
110
+ const signIn = await app.app.request(
111
+ '/api/v1/auth/sign-in/email',
112
+ json({ email: OWNER.email, password: OWNER.password })
113
+ );
114
+ ownerCookie = extractCookie(signIn);
115
+ const me = await readJson(await app.app.request('/api/v1/me', { headers: { cookie: ownerCookie } }));
116
+ workspaceId = me.workspace.id;
117
+ ownerUserId = me.user.id;
118
+ });
119
+
120
+ afterAll(async () => {
121
+ await app?.stop();
122
+ await container?.stop();
123
+ });
124
+
125
+ describe('page walks (no dups, no gaps, strict order)', () => {
126
+ const walkFileIds: string[] = [];
127
+
128
+ it('files: uploads walk back newest-first through ?limit=2 pages', async () => {
129
+ for (let i = 0; i < 5; i++) {
130
+ const id = await uploadFile(`walk-${i}.txt`, `walk file ${i}`);
131
+ walkFileIds.push(id);
132
+ // Pin distinct timestamps so the expected order is exact; the
133
+ // identical-timestamp case gets its own test below.
134
+ await app.db.db
135
+ .update(files)
136
+ .set({ createdAt: ts(i) })
137
+ .where(eq(files.id, id));
138
+ }
139
+ const rows = await walk('files', 'files', 2);
140
+ expectExactly(rows, walkFileIds);
141
+ expectStrictDesc(rows);
142
+ });
143
+
144
+ it('members: seeded memberships walk cleanly (join included)', async () => {
145
+ const memberIds: string[] = [];
146
+ for (let i = 0; i < 5; i++) {
147
+ const userId = `pagination-user-${i}`;
148
+ await app.db.db.insert(userTable).values({
149
+ id: userId,
150
+ name: `Walk User ${i}`,
151
+ email: `walk-user-${i}@pagination.test`,
152
+ emailVerified: true
153
+ });
154
+ const [row] = await app.db.db
155
+ .insert(workspaceMembers)
156
+ .values({ workspaceId, userId, role: 'member', createdAt: ts(i) })
157
+ .returning({ id: workspaceMembers.id });
158
+ memberIds.push(row!.id);
159
+ }
160
+ const [ownerRow] = await app.db.db
161
+ .select({ id: workspaceMembers.id })
162
+ .from(workspaceMembers)
163
+ .where(eq(workspaceMembers.userId, ownerUserId));
164
+ const rows = await walk('members', 'members', 2);
165
+ expectExactly(rows, [...memberIds, ownerRow!.id]);
166
+ expectStrictDesc(rows);
167
+ });
168
+
169
+ it('invitations: seeded invitations walk cleanly', async () => {
170
+ const invitationIds: string[] = [];
171
+ for (let i = 0; i < 5; i++) {
172
+ const [row] = await app.db.db
173
+ .insert(invitations)
174
+ .values({
175
+ workspaceId,
176
+ email: `walk-invitee-${i}@pagination.test`,
177
+ role: 'member',
178
+ tokenHash: randomUUID(),
179
+ invitedBy: ownerUserId,
180
+ expiresAt: new Date(Date.now() + 86_400_000),
181
+ createdAt: ts(i)
182
+ })
183
+ .returning({ id: invitations.id });
184
+ invitationIds.push(row!.id);
185
+ }
186
+ const rows = await walk('invitations', 'invitations', 2);
187
+ expectExactly(rows, invitationIds);
188
+ expectStrictDesc(rows);
189
+ });
190
+ });
191
+
192
+ describe('same-timestamp tie-break (the microsecond regression test)', () => {
193
+ it('walks limit=1 through rows sharing one createdAt without dup or gap', async () => {
194
+ const tied = new Date(Date.UTC(2025, 5, 1, 12, 0, 0, 123));
195
+ const tiedIds: string[] = [];
196
+ for (let i = 0; i < 3; i++) {
197
+ const id = await uploadFile(`tied-${i}.txt`, `tied file ${i}`);
198
+ await app.db.db.update(files).set({ createdAt: tied }).where(eq(files.id, id));
199
+ tiedIds.push(id);
200
+ }
201
+ const rows = await walk('files', 'files', 1);
202
+ // Each tied row exactly once (id DESC breaks the tie), everything ordered.
203
+ expect(new Set(rows.map((r) => r.id)).size).toBe(rows.length);
204
+ for (const id of tiedIds) expect(rows.map((r) => r.id)).toContain(id);
205
+ expectStrictDesc(rows);
206
+ });
207
+ });
208
+
209
+ describe('cursor tolerance', () => {
210
+ const endpoints: Array<[path: string, key: string]> = [
211
+ ['members', 'members'],
212
+ ['api-keys', 'apiKeys'],
213
+ ['invitations', 'invitations'],
214
+ ['files', 'files'],
215
+ ['audit', 'entries']
216
+ ];
217
+
218
+ it('a malformed cursor is ignored: ?cursor=banana equals the uncursored first page', async () => {
219
+ for (const [path] of endpoints) {
220
+ const withBanana = await listPage(path, { cursor: 'banana' });
221
+ expect(withBanana.status).toBe(200);
222
+ const plain = await listPage(path, {});
223
+ expect(await readJson(withBanana)).toEqual(await readJson(plain));
224
+ }
225
+ });
226
+
227
+ it('an unknown well-formed uuid cursor yields an empty page + nextCursor null', async () => {
228
+ const ghost = randomUUID();
229
+ for (const [path, key] of endpoints) {
230
+ if (path === 'audit') continue; // numeric ids — a uuid is just malformed there
231
+ const res = await listPage(path, { cursor: ghost });
232
+ expect(res.status).toBe(200);
233
+ expect(await readJson(res)).toEqual({ [key]: [], nextCursor: null });
234
+ }
235
+ });
236
+
237
+ it('rejects out-of-range limits with validation_error on every list', async () => {
238
+ for (const [path] of endpoints) {
239
+ for (const limit of ['0', '101']) {
240
+ const res = await listPage(path, { limit });
241
+ expect(res.status).toBe(400);
242
+ const body = await readJson(res);
243
+ expect(body.error.code).toBe('validation_error');
244
+ }
245
+ }
246
+ });
247
+ });
248
+
249
+ describe('soft-deleted files stay out of every page', () => {
250
+ it('a deleted file appears on no page of the walk', async () => {
251
+ const doomedId = await uploadFile('doomed.txt', 'delete me before the walk');
252
+ const del = await app.app.request(`/api/v1/files/${doomedId}`, {
253
+ method: 'DELETE',
254
+ headers: { cookie: ownerCookie }
255
+ });
256
+ expect(del.status).toBe(200);
257
+ const rows = await walk('files', 'files', 1);
258
+ expect(rows.map((r) => r.id)).not.toContain(doomedId);
259
+ });
260
+
261
+ it('re-uploading identical bytes revives the soft-deleted row back into the list', async () => {
262
+ const body = 'phoenix bytes — deleted then re-uploaded';
263
+ const phoenixId = await uploadFile('phoenix.txt', body);
264
+
265
+ const del = await app.app.request(`/api/v1/files/${phoenixId}`, {
266
+ method: 'DELETE',
267
+ headers: { cookie: ownerCookie }
268
+ });
269
+ expect(del.status).toBe(200);
270
+ const gone = await readJson(await listPage('files', {}));
271
+ expect(gone.files.map((f: WalkRow) => f.id)).not.toContain(phoenixId);
272
+
273
+ // Same bytes → the dedupe path revives the existing row (deletedAt back
274
+ // to null), which the SQL-side isNull filter must show again.
275
+ const reupload = await app.app.request(`/api/v1/files?name=${encodeURIComponent('phoenix.txt')}`, {
276
+ method: 'POST',
277
+ headers: { 'content-type': 'text/plain', cookie: ownerCookie },
278
+ body
279
+ });
280
+ expect(reupload.status).toBe(201);
281
+ const reuploaded = await readJson(reupload);
282
+ expect(reuploaded.deduplicated).toBe(true);
283
+ expect(reuploaded.file.id).toBe(phoenixId);
284
+
285
+ const back = await readJson(await listPage('files', {}));
286
+ expect(back.files.map((f: WalkRow) => f.id)).toContain(phoenixId);
287
+ });
288
+ });
289
+
290
+ describe('api-key scoping holds on every page', () => {
291
+ let memberCookie: string;
292
+ const memberKeyIds: string[] = [];
293
+ const ownerKeyIds: string[] = [];
294
+
295
+ it('each user sees exactly THEIR OWN keys across a walk (keys are user credentials)', async () => {
296
+ // Bring in a real member via the invitation-accept flow.
297
+ const invite = await app.app.request('/api/v1/invitations', {
298
+ ...json({ email: MEMBER.email, role: 'member' }),
299
+ headers: { 'content-type': 'application/json', cookie: ownerCookie }
300
+ });
301
+ const { acceptUrl } = await readJson(invite);
302
+ const token = acceptUrl.split('/invite/')[1]!;
303
+ const accepted = await app.app.request(
304
+ '/api/v1/invitations/accept',
305
+ json({ token, name: MEMBER.name, password: MEMBER.password })
306
+ );
307
+ expect(accepted.status).toBe(200);
308
+ const signIn = await app.app.request(
309
+ '/api/v1/auth/sign-in/email',
310
+ json({ email: MEMBER.email, password: MEMBER.password })
311
+ );
312
+ memberCookie = extractCookie(signIn);
313
+
314
+ const mint = async (cookie: string, name: string) => {
315
+ const res = await app.app.request('/api/v1/api-keys', {
316
+ ...json({ name, scopes: [host.scopes.read] }),
317
+ headers: { 'content-type': 'application/json', cookie }
318
+ });
319
+ expect(res.status).toBe(201);
320
+ return (await readJson(res)).apiKey.id as string;
321
+ };
322
+ for (let i = 0; i < 2; i++) memberKeyIds.push(await mint(memberCookie, `member-key-${i}`));
323
+ for (let i = 0; i < 3; i++) ownerKeyIds.push(await mint(ownerCookie, `owner-key-${i}`));
324
+ // Pin distinct timestamps (rapid mints can share a millisecond) so the
325
+ // strict-order assertion is exact.
326
+ const allKeyIds = [...memberKeyIds, ...ownerKeyIds];
327
+ for (let i = 0; i < allKeyIds.length; i++) {
328
+ await app.db.db
329
+ .update(apiKeys)
330
+ .set({ createdAt: ts(10 + i) })
331
+ .where(eq(apiKeys.id, allKeyIds[i]!));
332
+ }
333
+
334
+ const memberRows = await walk('api-keys', 'apiKeys', 1, memberCookie);
335
+ expectExactly(memberRows, memberKeyIds);
336
+ expectStrictDesc(memberRows);
337
+
338
+ // The owner's walk is creator-scoped too (user-scoped credential model):
339
+ // no role sees anyone else's keys through this listing.
340
+ const ownerRows = await walk('api-keys', 'apiKeys', 2);
341
+ expectExactly(ownerRows, ownerKeyIds);
342
+ expectStrictDesc(ownerRows);
343
+ });
344
+ });
@@ -0,0 +1,305 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql';
3
+ import { desc, eq } from 'drizzle-orm';
4
+ import { auditLog } from '@antasphere/chassis-db';
5
+ import {
6
+ createDatabase,
7
+ createTestApp,
8
+ extractCookie,
9
+ readJson,
10
+ startPostgres,
11
+ type TestApp,
12
+ host
13
+ } from './helpers.js';
14
+
15
+ /**
16
+ * M2 platform core: API keys, members, invitations, audit, rate limits.
17
+ * Isolated database (per-suite isolation, decision 15); the flow inside is
18
+ * deliberately ordered — it is the exit-criteria #3/#4 story.
19
+ */
20
+
21
+ const OWNER = { email: 'owner@core.test', name: 'Core Owner', password: 'core-owner-password-12' };
22
+ const INVITEE = { email: 'teammate@core.test', name: 'Team Mate', password: 'teammate-password-123' };
23
+
24
+ let container: StartedPostgreSqlContainer;
25
+ let app: TestApp;
26
+ let ownerCookie: string;
27
+
28
+ const json = (body: unknown) => ({
29
+ method: 'POST',
30
+ headers: { 'content-type': 'application/json' },
31
+ body: JSON.stringify(body)
32
+ });
33
+
34
+ beforeAll(async () => {
35
+ container = await startPostgres();
36
+ const url = await createDatabase(container, 'platform_core');
37
+ app = await createTestApp(url);
38
+
39
+ await app.app.request(
40
+ '/api/v1/setup',
41
+ json({ setupToken: 'integration-test-setup-token', instanceName: 'Core', owner: OWNER })
42
+ );
43
+ const signIn = await app.app.request(
44
+ '/api/v1/auth/sign-in/email',
45
+ json({ email: OWNER.email, password: OWNER.password })
46
+ );
47
+ ownerCookie = extractCookie(signIn);
48
+ });
49
+
50
+ afterAll(async () => {
51
+ await app?.stop();
52
+ await container?.stop();
53
+ });
54
+
55
+ describe('API keys (exit criterion 4, API half)', () => {
56
+ let mintedKey: string;
57
+ let keyRecordId: string;
58
+ let keyId: string;
59
+
60
+ it('mints a key from a session; the secret appears exactly once', async () => {
61
+ const res = await app.app.request('/api/v1/api-keys', {
62
+ ...json({ name: 'ci key', scopes: [host.scopes.read] }),
63
+ headers: { 'content-type': 'application/json', cookie: ownerCookie }
64
+ });
65
+ expect(res.status).toBe(201);
66
+ const body = await readJson(res);
67
+ // The HOST's prefix, read literally (no pattern built from it), then the fixed shape.
68
+ const prefix = `${host.identity.apiKeyPrefix}_`;
69
+ expect(body.key.startsWith(prefix)).toBe(true);
70
+ expect(body.key.slice(prefix.length)).toMatch(/^[A-Za-z0-9_-]{8}_[A-Za-z0-9_-]{20,}$/);
71
+ expect(body.apiKey.scopes).toEqual([host.scopes.read]);
72
+ mintedKey = body.key;
73
+ keyRecordId = body.apiKey.id;
74
+ keyId = body.apiKey.keyId; // authoritative — never derive it by splitting on '_'
75
+
76
+ const list = await app.app.request('/api/v1/api-keys', { headers: { cookie: ownerCookie } });
77
+ const listBody = await readJson(list);
78
+ expect(listBody.apiKeys.map((k: { id: string }) => k.id)).toContain(keyRecordId);
79
+ // The full key never reappears, and no secret material (hash) leaks. Do
80
+ // NOT split on '_' to isolate the secret — base64url secrets contain '_',
81
+ // so a fragment can spuriously collide with a UUID/timestamp in the list.
82
+ const serialized = JSON.stringify(listBody);
83
+ expect(serialized).not.toContain(mintedKey);
84
+ expect(serialized).not.toContain('secretHash');
85
+ });
86
+
87
+ it('authenticates a curl-style Bearer request and lands it in the audit log with the key identity', async () => {
88
+ const me = await app.app.request('/api/v1/me', {
89
+ headers: { authorization: `Bearer ${mintedKey}` }
90
+ });
91
+ expect(me.status).toBe(200);
92
+ const body = await readJson(me);
93
+ expect(body.via).toBe('api_key');
94
+ expect(body.scopes).toEqual([host.scopes.read]);
95
+
96
+ // Exit criterion 4: the key-authenticated request itself appears in the
97
+ // audit log with the KEY identity (machine reads are audited).
98
+ const [keyAudit] = await app.db.db
99
+ .select()
100
+ .from(auditLog)
101
+ .where(eq(auditLog.actorVia, 'api_key'))
102
+ .orderBy(desc(auditLog.id))
103
+ .limit(1);
104
+ expect(keyAudit).toBeTruthy();
105
+ expect(keyAudit!.apiKeyId).toBe(keyRecordId);
106
+ expect(keyAudit!.action).toBe('get /api/v1/me');
107
+
108
+ // The mint was audited with the SESSION identity + the key as resource.
109
+ const [mintAudit] = await app.db.db
110
+ .select()
111
+ .from(auditLog)
112
+ .where(eq(auditLog.action, 'apikey.create'))
113
+ .orderBy(desc(auditLog.id))
114
+ .limit(1);
115
+ expect(mintAudit).toBeTruthy();
116
+ expect(mintAudit!.resourceId).toBe(keyRecordId);
117
+ expect(mintAudit!.actorVia).toBe('session');
118
+ });
119
+
120
+ it('walls machine principals off unlisted endpoints (fail-closed allowlist)', async () => {
121
+ const res = await app.app.request('/api/v1/members', {
122
+ headers: { authorization: `Bearer ${mintedKey}` }
123
+ });
124
+ expect(res.status).toBe(403);
125
+ const body = await readJson(res);
126
+ expect(body.error.code).toBe('endpoint_not_allowed');
127
+ });
128
+
129
+ it('refuses to mint a key with a key (sessions only)', async () => {
130
+ const res = await app.app.request('/api/v1/api-keys', {
131
+ ...json({ name: 'evil', scopes: [host.scopes.read] }),
132
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${mintedKey}` }
133
+ });
134
+ expect(res.status).toBe(403);
135
+ });
136
+
137
+ it('rejects a tampered key and rate-limits repeated failures per IP', async () => {
138
+ // Use the real keyId (a base64url string that may contain '_') so the
139
+ // tampered key still passes the format check and reaches the wall.
140
+ const bad = `${host.identity.apiKeyPrefix}_${keyId}_${'A'.repeat(43)}`;
141
+ // Own bucket via spoofed client IP: the wall must not bleed into the
142
+ // other tests' default bucket.
143
+ const attacker = { authorization: `Bearer ${bad}`, 'x-forwarded-for': '203.0.113.9' };
144
+ const first = await app.app.request('/api/v1/me', { headers: attacker });
145
+ expect(first.status).toBe(401);
146
+
147
+ // The wall trips within the window; the exact iteration is ±1 (the
148
+ // limiter checks state before consuming), so hammer past the threshold
149
+ // and assert a 429 appears — not on a specific iteration.
150
+ let sawRateLimit = false;
151
+ for (let i = 0; i < 30 && !sawRateLimit; i++) {
152
+ const r = await app.app.request('/api/v1/me', { headers: attacker });
153
+ if (r.status === 429) sawRateLimit = true;
154
+ else expect(r.status).toBe(401);
155
+ }
156
+ expect(sawRateLimit).toBe(true);
157
+
158
+ // The wall is per-IP: the valid key from ANOTHER address still works.
159
+ const stillFine = await app.app.request('/api/v1/me', {
160
+ headers: { authorization: `Bearer ${mintedKey}`, 'x-forwarded-for': '198.51.100.7' }
161
+ });
162
+ expect(stillFine.status).toBe(200);
163
+ });
164
+
165
+ it('revoking the key kills it immediately', async () => {
166
+ const res = await app.app.request(`/api/v1/api-keys/${keyRecordId}`, {
167
+ method: 'DELETE',
168
+ headers: { cookie: ownerCookie }
169
+ });
170
+ expect(res.status).toBe(200);
171
+ const after = await app.app.request('/api/v1/me', {
172
+ headers: { authorization: `Bearer ${mintedKey}` }
173
+ });
174
+ expect(after.status).toBe(401);
175
+ });
176
+ });
177
+
178
+ describe('invitations without SMTP (exit criterion 3, API half)', () => {
179
+ let acceptUrl: string;
180
+
181
+ it('creates an invitation and always returns a copyable link (no SMTP configured)', async () => {
182
+ const res = await app.app.request('/api/v1/invitations', {
183
+ ...json({ email: INVITEE.email, role: 'member' }),
184
+ headers: { 'content-type': 'application/json', cookie: ownerCookie }
185
+ });
186
+ expect(res.status).toBe(201);
187
+ const body = await readJson(res);
188
+ expect(body.emailSent).toBe(false);
189
+ expect(body.acceptUrl).toContain('/invite/');
190
+ acceptUrl = body.acceptUrl;
191
+ });
192
+
193
+ it('rejects a duplicate open invitation', async () => {
194
+ const res = await app.app.request('/api/v1/invitations', {
195
+ ...json({ email: INVITEE.email, role: 'member' }),
196
+ headers: { 'content-type': 'application/json', cookie: ownerCookie }
197
+ });
198
+ expect(res.status).toBe(409);
199
+ });
200
+
201
+ it('resolves the token publicly (accept-page data)', async () => {
202
+ const token = acceptUrl.split('/invite/')[1]!;
203
+ const res = await app.app.request(`/api/v1/invitations/lookup?token=${token}`);
204
+ expect(res.status).toBe(200);
205
+ const body = await readJson(res);
206
+ expect(body.email).toBe(INVITEE.email);
207
+ // No account-existence signal on the public lookup (PRDCT-1437): the
208
+ // admin holds the accept URL, so the field was an instance-global oracle.
209
+ expect(body).not.toHaveProperty('accountExists');
210
+ expect(body.workspaceName).toBe('Core');
211
+ });
212
+
213
+ it('accepts with new-account credentials, then the invitee logs in with member role', async () => {
214
+ // `user.created` fires from the identity layer's database hook — every
215
+ // entrance, by construction. Subscribe before accept.
216
+ const created: Array<{ userId: string; email: string }> = [];
217
+ const unsubscribe = app.registry.events.on('user.created', (payload) => {
218
+ created.push(payload);
219
+ });
220
+
221
+ const token = acceptUrl.split('/invite/')[1]!;
222
+ const res = await app.app.request(
223
+ '/api/v1/invitations/accept',
224
+ json({ token, name: INVITEE.name, password: INVITEE.password })
225
+ );
226
+ expect(res.status).toBe(200);
227
+ const body = await readJson(res);
228
+ expect(body.role).toBe('member');
229
+
230
+ unsubscribe();
231
+ expect(created).toHaveLength(1);
232
+ expect(created[0]!.email).toBe(INVITEE.email);
233
+ expect(created[0]!.userId).toBeTruthy();
234
+
235
+ const signIn = await app.app.request(
236
+ '/api/v1/auth/sign-in/email',
237
+ json({ email: INVITEE.email, password: INVITEE.password })
238
+ );
239
+ expect(signIn.status).toBe(200);
240
+ const cookie = extractCookie(signIn);
241
+ const me = await app.app.request('/api/v1/me', { headers: { cookie } });
242
+ const meBody = await readJson(me);
243
+ expect(meBody.role).toBe('member');
244
+ });
245
+
246
+ it('a used token is dead (single redemption)', async () => {
247
+ const token = acceptUrl.split('/invite/')[1]!;
248
+ const res = await app.app.request(
249
+ '/api/v1/invitations/accept',
250
+ json({ token, name: 'X', password: 'x'.repeat(16) })
251
+ );
252
+ expect(res.status).toBe(404); // no longer live
253
+ });
254
+
255
+ it('enforces roles: the member cannot invite, list audit, or update members', async () => {
256
+ const signIn = await app.app.request(
257
+ '/api/v1/auth/sign-in/email',
258
+ json({ email: INVITEE.email, password: INVITEE.password })
259
+ );
260
+ const cookie = extractCookie(signIn);
261
+
262
+ const invite = await app.app.request('/api/v1/invitations', {
263
+ ...json({ email: 'x@y.test', role: 'member' }),
264
+ headers: { 'content-type': 'application/json', cookie }
265
+ });
266
+ expect(invite.status).toBe(403);
267
+
268
+ const audit = await app.app.request('/api/v1/audit', { headers: { cookie } });
269
+ expect(audit.status).toBe(403);
270
+ });
271
+ });
272
+
273
+ describe('audit trail (exit criterion 4 wrap-up)', () => {
274
+ it('exposes the trail to admins with actor identities and request ids', async () => {
275
+ const res = await app.app.request('/api/v1/audit', { headers: { cookie: ownerCookie } });
276
+ expect(res.status).toBe(200);
277
+ const body = await readJson(res);
278
+ const actions = body.entries.map((e: { action: string }) => e.action);
279
+ expect(actions).toEqual(
280
+ expect.arrayContaining(['apikey.create', 'apikey.revoke', 'invitation.create', 'invitation.accept'])
281
+ );
282
+ const withRequestId = body.entries.filter((e: { requestId: string | null }) => e.requestId);
283
+ expect(withRequestId.length).toBeGreaterThan(0);
284
+ });
285
+ });
286
+
287
+ describe('jobs runtime', () => {
288
+ it('pg-boss started and accepts a send (usage sink path)', async () => {
289
+ await app.registry.usage.emit({
290
+ id: '01JZZZZZZZZZZZZZZZZZZZZZZZ',
291
+ meter: 'test.meter',
292
+ actionKey: 'test.meter',
293
+ quantity: 1,
294
+ unit: 'count',
295
+ occurredAt: new Date().toISOString(),
296
+ workspaceId: '00000000-0000-0000-0000-000000000000',
297
+ userId: null,
298
+ via: 'session',
299
+ source: { instanceId: 'test', edition: 'oss', version: 'dev' }
300
+ });
301
+ // send() throwing would have been logged + swallowed; assert the queue exists.
302
+ const size = await app.jobs.boss.getQueueSize('usage-events');
303
+ expect(size).toBeGreaterThanOrEqual(0);
304
+ });
305
+ });