@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,788 @@
1
+ import type { OpenAPIHono } from '@hono/zod-openapi';
2
+ import type { Context } from 'hono';
3
+ import { and, desc, eq, isNotNull, isNull, sql } from 'drizzle-orm';
4
+ import { projectRoleAtLeast, type Principal, type ProjectRole } from '@antasphere/chassis-contract';
5
+ import {
6
+ projectArchiveRoute,
7
+ projectCreateRoute,
8
+ projectGetRoute,
9
+ projectMemberAddRoute,
10
+ projectMemberRemoveRoute,
11
+ projectMemberRoleRoute,
12
+ projectMembersListRoute,
13
+ projectsListRoute,
14
+ projectTeamRemoveRoute,
15
+ projectTeamRoleRoute,
16
+ projectUnarchiveRoute,
17
+ projectUpdateRoute
18
+ } from '@antasphere/chassis-contract/routes';
19
+ import {
20
+ projectMembers,
21
+ projects,
22
+ projectTeams,
23
+ user as userTable,
24
+ workspaceMembers,
25
+ workspaceTeams,
26
+ type Db,
27
+ type DbConn
28
+ } from '@antasphere/chassis-db';
29
+ import { requireAuth, requireNonGuest } from '../middleware/auth-context.js';
30
+ import { holdLiveMembership } from '../members/removal.js';
31
+ import {
32
+ createdAtText,
33
+ cursorRowId,
34
+ decodeKeysetCursor,
35
+ encodeKeysetCursor,
36
+ keysetBefore,
37
+ pageOf
38
+ } from '../pagination.js';
39
+ import {
40
+ PROJECTS_ID,
41
+ projectGrantPredicate,
42
+ projectRole,
43
+ projectRoleExpression
44
+ } from '../projects/access.js';
45
+
46
+ /**
47
+ * The project routes. NOT under the hub-managed gate, on purpose: project
48
+ * membership is the tool's own on both editions (the gate is mounted on
49
+ * `/members/*` and `/invitations/*`, and this subtree is neither). Members are
50
+ * picked among the workspace's own ACTIVE non-guest members, and its TEAMS
51
+ * (PRDCT-2794: a team holds a role like a person, on both editions; on cloud
52
+ * the team is the hub's projection, on self-hosted the tool's own): no
53
+ * invitation, no claim token and no account is ever minted here, so nothing
54
+ * in this file can diverge from the hub.
55
+ *
56
+ * The tiered answer, on every route (the ADR 013 posture):
57
+ * 404 whoever cannot read the project (`projectRole` = null);
58
+ * 403 a proven reader whose role does not allow the act;
59
+ * 409 `project_archived` on every mutation of an archived project but unarchive.
60
+ * The order is fixed: read, then role, then archive. A viewer of an archived
61
+ * project who tries to rename it learns 403, the fact about THEM, before 409.
62
+ */
63
+ export interface ProjectRouteDeps {
64
+ db: Db;
65
+ }
66
+
67
+ const err = (code: string, message: string) => ({ error: { code, message } });
68
+ const notFound = () => err('not_found', 'Project not found');
69
+
70
+ /** A Postgres foreign-key violation (SQLSTATE 23503) on the named constraint, down drizzle's `cause` chain. */
71
+ function isForeignKeyViolation(e: unknown, constraint: string): boolean {
72
+ for (let cur: unknown = e, depth = 0; cur instanceof Error && depth < 10; cur = cur.cause, depth++) {
73
+ const link = cur as { code?: unknown; constraint?: unknown };
74
+ if (link.code === '23503') return link.constraint === constraint;
75
+ }
76
+ return false;
77
+ }
78
+
79
+ /** The foreign key a team's place on a project rides on: the one violation that means "the team went". */
80
+ const PROJECT_TEAM_FK = 'project_teams_team_id_workspace_teams_id_fk';
81
+
82
+ type ProjectWireRow = {
83
+ id: string;
84
+ name: string;
85
+ description: string | null;
86
+ metadata: Record<string, unknown>;
87
+ archivedAt: Date | null;
88
+ createdBy: string | null;
89
+ createdAt: Date;
90
+ updatedAt: Date;
91
+ myRole: ProjectRole | null;
92
+ memberCount: number;
93
+ };
94
+
95
+ const toWire = (p: ProjectWireRow & { myRole: ProjectRole }) => ({
96
+ id: p.id,
97
+ name: p.name,
98
+ description: p.description,
99
+ metadata: p.metadata,
100
+ archivedAt: p.archivedAt?.toISOString() ?? null,
101
+ createdBy: p.createdBy,
102
+ createdAt: p.createdAt.toISOString(),
103
+ updatedAt: p.updatedAt.toISOString(),
104
+ myRole: p.myRole,
105
+ memberCount: p.memberCount
106
+ });
107
+
108
+ const memberToWire = (m: {
109
+ userId: string;
110
+ email: string;
111
+ name: string;
112
+ role: ProjectRole;
113
+ addedBy: string | null;
114
+ createdAt: Date;
115
+ }) => ({
116
+ kind: 'person' as const,
117
+ userId: m.userId,
118
+ email: m.email,
119
+ name: m.name,
120
+ role: m.role,
121
+ addedBy: m.addedBy,
122
+ createdAt: m.createdAt.toISOString()
123
+ });
124
+
125
+ const teamToWire = (t: {
126
+ teamId: string;
127
+ slug: string;
128
+ name: string;
129
+ membersCount: number;
130
+ hubTeamId: string | null;
131
+ role: ProjectRole;
132
+ addedBy: string | null;
133
+ createdAt: Date;
134
+ }) => ({
135
+ kind: 'team' as const,
136
+ teamId: t.teamId,
137
+ slug: t.slug,
138
+ name: t.name,
139
+ membersCount: Number(t.membersCount),
140
+ hubTeamId: t.hubTeamId,
141
+ role: t.role,
142
+ addedBy: t.addedBy,
143
+ createdAt: t.createdAt.toISOString()
144
+ });
145
+
146
+ /** How many people a team seats: the people who hold the project role through it. */
147
+ const TEAM_MEMBERS_COUNT = sql<number>`(SELECT count(*)::int FROM workspace_team_members prj_tmc WHERE prj_tmc.team_id = ${sql.raw('"workspace_teams"."id"')})`;
148
+
149
+ /** The wire columns of a project, the caller's role and the member count computed in the same read. */
150
+ const projectSelection = (principal: Principal) => ({
151
+ id: projects.id,
152
+ name: projects.name,
153
+ description: projects.description,
154
+ metadata: projects.metadata,
155
+ archivedAt: projects.archivedAt,
156
+ createdBy: projects.createdBy,
157
+ createdAt: projects.createdAt,
158
+ updatedAt: projects.updatedAt,
159
+ myRole: projectRoleExpression(principal, PROJECTS_ID),
160
+ // People and teams alike: what the members list shows.
161
+ memberCount: sql<number>`(SELECT count(*)::int FROM project_members prj_count WHERE prj_count.project_id = ${PROJECTS_ID})
162
+ + (SELECT count(*)::int FROM project_teams prj_tcount WHERE prj_tcount.project_id = ${PROJECTS_ID})`
163
+ });
164
+
165
+ // The members list pages with the value-carrying cursor (`keysetBeforeValue`):
166
+ // project_members HARD-deletes rows, so a cursor naming a member removed
167
+ // between two pages would compare against NULL under `keysetBefore` and end
168
+ // the roster early. The timestamp rides as Postgres rendered it, never as a
169
+ // JS Date, so a millisecond shared by two rows loses nobody.
170
+ const memberSelection = {
171
+ id: projectMembers.id,
172
+ createdAtText: createdAtText(projectMembers.createdAt),
173
+ userId: workspaceMembers.userId,
174
+ email: userTable.email,
175
+ name: userTable.name,
176
+ role: projectMembers.role,
177
+ addedBy: projectMembers.addedBy,
178
+ createdAt: projectMembers.createdAt
179
+ };
180
+
181
+ /** A team's entry on a project, joined to the team; the same cursor columns as a person's. */
182
+ const teamSelection = {
183
+ id: projectTeams.id,
184
+ createdAtText: createdAtText(projectTeams.createdAt),
185
+ teamId: workspaceTeams.id,
186
+ slug: workspaceTeams.slug,
187
+ name: workspaceTeams.name,
188
+ membersCount: TEAM_MEMBERS_COUNT,
189
+ hubTeamId: workspaceTeams.hubTeamId,
190
+ role: projectTeams.role,
191
+ addedBy: projectTeams.addedBy,
192
+ createdAt: projectTeams.createdAt
193
+ };
194
+
195
+ /** One row of the members list's union: a person's or a team's entry, the unused columns null. */
196
+ interface MemberUnionRow {
197
+ kind: 'person' | 'team';
198
+ id: string;
199
+ created_at_text: string;
200
+ created_at: Date;
201
+ role: ProjectRole;
202
+ added_by: string | null;
203
+ user_id: string | null;
204
+ email: string | null;
205
+ name: string;
206
+ team_id: string | null;
207
+ slug: string | null;
208
+ members_count: number | null;
209
+ hub_team_id: string | null;
210
+ }
211
+
212
+ export function registerProjectRoutes(api: OpenAPIHono, deps: ProjectRouteDeps): void {
213
+ const { db } = deps;
214
+
215
+ // Projects are a workspace-level surface: a guest membership exists for
216
+ // principal resolution only (D2) and is refused the whole subtree, flat, on
217
+ // both editions and for every credential. The predicates refuse a guest a
218
+ // second time (`projects/access.ts`): this gate is the truthful answer, that
219
+ // clause is what a tool's own rule inherits.
220
+ for (const path of ['/projects', '/projects/*']) {
221
+ api.use(path, requireAuth());
222
+ api.use(path, requireNonGuest());
223
+ }
224
+
225
+ /** One project of the request's workspace AS THE CALLER reads it, or null (= 404). */
226
+ async function readProject(
227
+ conn: DbConn,
228
+ principal: Principal,
229
+ id: string
230
+ ): Promise<(ProjectWireRow & { myRole: ProjectRole }) | null> {
231
+ const [row] = await conn
232
+ .select(projectSelection(principal))
233
+ .from(projects)
234
+ .where(and(eq(projects.id, id), eq(projects.workspaceId, principal.workspaceId)))
235
+ .limit(1);
236
+ if (!row || row.myRole === null) return null;
237
+ return { ...row, myRole: row.myRole };
238
+ }
239
+
240
+ /**
241
+ * The gate of every mutation, in its fixed order: 404, then 403, then 409.
242
+ * Returns the project as the caller reads it, or the response to send.
243
+ */
244
+ async function gateMutation(
245
+ c: Context,
246
+ id: string,
247
+ min: ProjectRole,
248
+ opts: { allowArchived?: boolean } = {}
249
+ ): Promise<{ project: ProjectWireRow & { myRole: ProjectRole } } | { response: Response }> {
250
+ const principal = c.get('principal')!;
251
+ const project = await readProject(db, principal, id);
252
+ if (!project) return { response: c.json(notFound(), 404) };
253
+ if (!projectRoleAtLeast(project.myRole, min)) {
254
+ return {
255
+ response: c.json(err('insufficient_project_role', `This needs the ${min} role on the project`), 403)
256
+ };
257
+ }
258
+ if (project.archivedAt !== null && !opts.allowArchived) {
259
+ return { response: c.json(archived(), 409) };
260
+ }
261
+ return { project };
262
+ }
263
+
264
+ const archived = () => err('project_archived', 'The project is archived: unarchive it to change it');
265
+
266
+ /**
267
+ * Runs a membership write while holding the project row FOR SHARE, after
268
+ * re-reading its archive state under that lock. Archiving UPDATEs the row,
269
+ * so it waits for the write (or the write sees it): an archive landing
270
+ * between the gate and the write can never be overtaken. `null` = archived.
271
+ */
272
+ async function whileLive<T>(
273
+ principal: Principal,
274
+ id: string,
275
+ write: (tx: DbConn) => Promise<T>
276
+ ): Promise<T | null> {
277
+ return db.transaction(async (tx) => {
278
+ const [row] = await tx
279
+ .select({ archivedAt: projects.archivedAt })
280
+ .from(projects)
281
+ .where(and(eq(projects.id, id), eq(projects.workspaceId, principal.workspaceId)))
282
+ .for('share')
283
+ .limit(1);
284
+ if (!row || row.archivedAt !== null) return null;
285
+ return write(tx);
286
+ });
287
+ }
288
+
289
+ // ── List ─────────────────────────────────────────────────────────────────
290
+ api.openapi(projectsListRoute, async (c) => {
291
+ const principal = c.get('principal')!;
292
+ const { cursor, limit, archived: archivedFilter } = c.req.valid('query');
293
+ const cursorId = cursorRowId(cursor);
294
+ const rows = await db
295
+ .select(projectSelection(principal))
296
+ .from(projects)
297
+ .where(
298
+ and(
299
+ eq(projects.workspaceId, principal.workspaceId),
300
+ // The read rule, the same builder a tool uses: a member's page is
301
+ // the projects they hold a grant on, an owner's or admin's is all.
302
+ projectGrantPredicate(principal, PROJECTS_ID, { atLeast: 'viewer', access: 'read' }),
303
+ ...(archivedFilter === 'false' ? [isNull(projects.archivedAt)] : []),
304
+ ...(archivedFilter === 'true' ? [isNotNull(projects.archivedAt)] : []),
305
+ ...(cursorId
306
+ ? [
307
+ keysetBefore({
308
+ table: projects,
309
+ id: projects.id,
310
+ createdAt: projects.createdAt,
311
+ workspaceId: projects.workspaceId,
312
+ cursorId,
313
+ workspace: principal.workspaceId
314
+ })
315
+ ]
316
+ : [])
317
+ )
318
+ )
319
+ .orderBy(desc(projects.createdAt), desc(projects.id))
320
+ .limit(limit + 1);
321
+ const { page, nextCursor } = pageOf(rows, limit);
322
+ // The WHERE is the rule: every selected row carries a role.
323
+ return c.json({ projects: page.map((p) => toWire({ ...p, myRole: p.myRole! })), nextCursor }, 200);
324
+ });
325
+
326
+ // ── Create ───────────────────────────────────────────────────────────────
327
+ // Any non-guest member creates a project and becomes its first manager in
328
+ // the SAME transaction: a project is never born without someone to manage it
329
+ // (an owner or admin could anyway, but the creator should not need one).
330
+ api.openapi(projectCreateRoute, async (c) => {
331
+ const principal = c.get('principal')!;
332
+ const body = c.req.valid('json');
333
+ const created = await db.transaction(async (tx) => {
334
+ const [membership] = await tx
335
+ .select({ id: workspaceMembers.id })
336
+ .from(workspaceMembers)
337
+ .where(
338
+ and(
339
+ eq(workspaceMembers.workspaceId, principal.workspaceId),
340
+ eq(workspaceMembers.userId, principal.userId),
341
+ eq(workspaceMembers.isActive, true)
342
+ )
343
+ )
344
+ // Held until the creator's grant commits, like every add's
345
+ // (`holdLiveMembership`): a removal of the creator waits for it.
346
+ .for('share')
347
+ .limit(1);
348
+ if (!membership) return null;
349
+ const [project] = await tx
350
+ .insert(projects)
351
+ .values({
352
+ workspaceId: principal.workspaceId,
353
+ name: body.name,
354
+ description: body.description ?? null,
355
+ metadata: body.metadata ?? {},
356
+ createdBy: principal.userId
357
+ })
358
+ .returning({ id: projects.id });
359
+ await tx.insert(projectMembers).values({
360
+ projectId: project!.id,
361
+ memberId: membership.id,
362
+ role: 'manager',
363
+ addedBy: principal.userId
364
+ });
365
+ return readProject(tx, principal, project!.id);
366
+ });
367
+ // The principal resolved from an active membership a moment ago; losing
368
+ // it mid-request is a revoked caller, answered like any other.
369
+ if (!created) return c.json(err('unauthenticated', 'Authentication required'), 401);
370
+ c.set('audit', {
371
+ action: 'project.create',
372
+ resourceType: 'project',
373
+ resourceId: created.id,
374
+ metadata: { name: created.name }
375
+ });
376
+ return c.json(toWire(created), 201);
377
+ });
378
+
379
+ // ── Get ──────────────────────────────────────────────────────────────────
380
+ api.openapi(projectGetRoute, async (c) => {
381
+ const principal = c.get('principal')!;
382
+ const { id } = c.req.valid('param');
383
+ const project = await readProject(db, principal, id);
384
+ if (!project) return c.json(notFound(), 404);
385
+ return c.json(toWire(project), 200);
386
+ });
387
+
388
+ // ── Update ───────────────────────────────────────────────────────────────
389
+ api.openapi(projectUpdateRoute, async (c) => {
390
+ const principal = c.get('principal')!;
391
+ const { id } = c.req.valid('param');
392
+ const patch = c.req.valid('json');
393
+ const gate = await gateMutation(c, id, 'manager');
394
+ if ('response' in gate) return gate.response as never;
395
+ // `archived_at IS NULL` again in the WHERE: an archive landing between
396
+ // the gate and this write must win.
397
+ const updated = await db
398
+ .update(projects)
399
+ .set({
400
+ ...(patch.name !== undefined ? { name: patch.name } : {}),
401
+ ...(patch.description !== undefined ? { description: patch.description } : {}),
402
+ ...(patch.metadata !== undefined ? { metadata: patch.metadata } : {}),
403
+ updatedAt: new Date()
404
+ })
405
+ .where(
406
+ and(eq(projects.id, id), eq(projects.workspaceId, principal.workspaceId), isNull(projects.archivedAt))
407
+ )
408
+ .returning({ id: projects.id });
409
+ if (updated.length === 0) return c.json(archived(), 409);
410
+ const project = (await readProject(db, principal, id))!;
411
+ c.set('audit', {
412
+ action: 'project.update',
413
+ resourceType: 'project',
414
+ resourceId: id,
415
+ metadata: { fields: Object.keys(patch) }
416
+ });
417
+ return c.json(toWire(project), 200);
418
+ });
419
+
420
+ // ── Archive / unarchive ──────────────────────────────────────────────────
421
+ // A project is never deleted. Archiving cascades onto NOTHING: what a tool
422
+ // linked to the project keeps working; only writes THROUGH a project grant
423
+ // stop (the archived-write rule of the predicate).
424
+ api.openapi(projectArchiveRoute, async (c) => {
425
+ const principal = c.get('principal')!;
426
+ const { id } = c.req.valid('param');
427
+ const gate = await gateMutation(c, id, 'manager');
428
+ if ('response' in gate) return gate.response as never;
429
+ const updated = await db
430
+ .update(projects)
431
+ .set({ archivedAt: new Date(), updatedAt: new Date() })
432
+ .where(
433
+ and(eq(projects.id, id), eq(projects.workspaceId, principal.workspaceId), isNull(projects.archivedAt))
434
+ )
435
+ .returning({ id: projects.id });
436
+ if (updated.length === 0) return c.json(archived(), 409);
437
+ const project = (await readProject(db, principal, id))!;
438
+ c.set('audit', { action: 'project.archive', resourceType: 'project', resourceId: id });
439
+ return c.json(toWire(project), 200);
440
+ });
441
+
442
+ api.openapi(projectUnarchiveRoute, async (c) => {
443
+ const principal = c.get('principal')!;
444
+ const { id } = c.req.valid('param');
445
+ const gate = await gateMutation(c, id, 'manager', { allowArchived: true });
446
+ if ('response' in gate) return gate.response as never;
447
+ const updated = await db
448
+ .update(projects)
449
+ .set({ archivedAt: null, updatedAt: new Date() })
450
+ .where(
451
+ and(
452
+ eq(projects.id, id),
453
+ eq(projects.workspaceId, principal.workspaceId),
454
+ isNotNull(projects.archivedAt)
455
+ )
456
+ )
457
+ .returning({ id: projects.id });
458
+ if (updated.length === 0) {
459
+ return c.json(err('project_not_archived', 'The project is not archived'), 409);
460
+ }
461
+ const project = (await readProject(db, principal, id))!;
462
+ c.set('audit', { action: 'project.unarchive', resourceType: 'project', resourceId: id });
463
+ return c.json(toWire(project), 200);
464
+ });
465
+
466
+ // ── Members ──────────────────────────────────────────────────────────────
467
+ // People and teams in ONE list, newest first, on one value-carrying cursor
468
+ // (`created_at`, `id`) that both tables answer: the cursor is applied inside
469
+ // each branch, so a page is the same page whichever kind the boundary row
470
+ // is. Raw SQL because a set operation is where drizzle's builder stops.
471
+ api.openapi(projectMembersListRoute, async (c) => {
472
+ const principal = c.get('principal')!;
473
+ const { id } = c.req.valid('param');
474
+ const { cursor, limit } = c.req.valid('query');
475
+ if ((await projectRole(db, principal, id)) === null) return c.json(notFound(), 404);
476
+ const after = decodeKeysetCursor(cursor);
477
+ const before = (createdAt: string, rowId: string) =>
478
+ after
479
+ ? sql`AND (${sql.raw(createdAt)}, ${sql.raw(rowId)}) < (${after.createdAt}::timestamptz, ${after.id}::uuid)`
480
+ : sql``;
481
+ const result = await db.execute(sql`
482
+ SELECT 'person' AS kind, pm.id, pm.created_at::text AS created_at_text, pm.created_at, pm.role, pm.added_by,
483
+ wm.user_id, u.email, u.name,
484
+ NULL::uuid AS team_id, NULL::text AS slug, NULL::int AS members_count, NULL::text AS hub_team_id
485
+ FROM project_members pm
486
+ JOIN workspace_members wm ON wm.id = pm.member_id
487
+ JOIN "user" u ON u.id = wm.user_id
488
+ WHERE pm.project_id = ${id} ${before('pm.created_at', 'pm.id')}
489
+ UNION ALL
490
+ SELECT 'team' AS kind, pt.id, pt.created_at::text AS created_at_text, pt.created_at, pt.role, pt.added_by,
491
+ NULL::text AS user_id, NULL::text AS email, t.name,
492
+ t.id AS team_id, t.slug,
493
+ (SELECT count(*)::int FROM workspace_team_members tm WHERE tm.team_id = t.id) AS members_count,
494
+ t.hub_team_id
495
+ FROM project_teams pt
496
+ JOIN workspace_teams t ON t.id = pt.team_id
497
+ WHERE pt.project_id = ${id} ${before('pt.created_at', 'pt.id')}
498
+ ORDER BY created_at DESC, id DESC
499
+ LIMIT ${limit + 1}
500
+ `);
501
+ const rows = result.rows as unknown as MemberUnionRow[];
502
+ const page = rows.slice(0, limit);
503
+ const last = rows.length > limit ? page[page.length - 1] : undefined;
504
+ const members = page.map((row) =>
505
+ row.kind === 'person'
506
+ ? memberToWire({
507
+ userId: row.user_id!,
508
+ email: row.email!,
509
+ name: row.name,
510
+ role: row.role,
511
+ addedBy: row.added_by,
512
+ createdAt: new Date(row.created_at)
513
+ })
514
+ : teamToWire({
515
+ teamId: row.team_id!,
516
+ slug: row.slug!,
517
+ name: row.name,
518
+ membersCount: row.members_count ?? 0,
519
+ hubTeamId: row.hub_team_id,
520
+ role: row.role,
521
+ addedBy: row.added_by,
522
+ createdAt: new Date(row.created_at)
523
+ })
524
+ );
525
+ return c.json(
526
+ {
527
+ members,
528
+ nextCursor: last ? encodeKeysetCursor({ id: last.id, createdAtText: last.created_at_text }) : null
529
+ },
530
+ 200
531
+ );
532
+ });
533
+
534
+ /** One member of one project, by the person's user id, or undefined. */
535
+ async function findProjectMember(conn: DbConn, projectId: string, userId: string) {
536
+ const [row] = await conn
537
+ .select(memberSelection)
538
+ .from(projectMembers)
539
+ .innerJoin(workspaceMembers, eq(projectMembers.memberId, workspaceMembers.id))
540
+ .innerJoin(userTable, eq(workspaceMembers.userId, userTable.id))
541
+ .where(and(eq(projectMembers.projectId, projectId), eq(workspaceMembers.userId, userId)))
542
+ .limit(1);
543
+ return row;
544
+ }
545
+
546
+ /** One team's entry on one project, by the team id, or undefined. */
547
+ async function findProjectTeam(conn: DbConn, projectId: string, teamId: string) {
548
+ const [row] = await conn
549
+ .select(teamSelection)
550
+ .from(projectTeams)
551
+ .innerJoin(workspaceTeams, eq(projectTeams.teamId, workspaceTeams.id))
552
+ .where(and(eq(projectTeams.projectId, projectId), eq(workspaceTeams.id, teamId)))
553
+ .limit(1);
554
+ return row;
555
+ }
556
+
557
+ /**
558
+ * Adds a team of the request's workspace to the project (PRDCT-2794): the
559
+ * team side of the members POST. A team of another workspace, or none, is
560
+ * 404 `team_not_found`; a repeat is 409 `already_member`.
561
+ */
562
+ async function addTeam(c: Context, id: string, teamId: string, role: ProjectRole): Promise<Response> {
563
+ const principal = c.get('principal')!;
564
+ const [team] = await db
565
+ .select({
566
+ teamId: workspaceTeams.id,
567
+ slug: workspaceTeams.slug,
568
+ name: workspaceTeams.name,
569
+ membersCount: TEAM_MEMBERS_COUNT,
570
+ hubTeamId: workspaceTeams.hubTeamId
571
+ })
572
+ .from(workspaceTeams)
573
+ .where(and(eq(workspaceTeams.id, teamId), eq(workspaceTeams.workspaceId, principal.workspaceId)))
574
+ .limit(1);
575
+ if (!team) return c.json(err('team_not_found', 'No team of this workspace matches'), 404);
576
+ let inserted: { createdAt: Date }[] | null;
577
+ try {
578
+ inserted = await whileLive(principal, id, (tx) =>
579
+ tx
580
+ .insert(projectTeams)
581
+ .values({ projectId: id, teamId: team.teamId, role, addedBy: principal.userId })
582
+ .onConflictDoNothing({ target: [projectTeams.projectId, projectTeams.teamId] })
583
+ .returning({ createdAt: projectTeams.createdAt })
584
+ );
585
+ } catch (cause) {
586
+ // The team went between the read above and the insert (a delete at the
587
+ // hub's next pass, an admin's): the foreign key says so, and the answer
588
+ // is the same 404 as a team that never was (verifier round 1, F2).
589
+ if (isForeignKeyViolation(cause, PROJECT_TEAM_FK)) {
590
+ return c.json(err('team_not_found', 'No team of this workspace matches'), 404);
591
+ }
592
+ throw cause;
593
+ }
594
+ if (inserted === null) return c.json(archived(), 409);
595
+ const [row] = inserted;
596
+ if (!row) return c.json(err('already_member', 'This team is already a member of the project'), 409);
597
+ c.set('audit', {
598
+ action: 'project.team_add',
599
+ resourceType: 'project',
600
+ resourceId: id,
601
+ metadata: { teamId: team.teamId, role }
602
+ });
603
+ return c.json(teamToWire({ ...team, role, addedBy: principal.userId, createdAt: row.createdAt }), 201);
604
+ }
605
+
606
+ api.openapi(projectMemberAddRoute, async (c) => {
607
+ const principal = c.get('principal')!;
608
+ const { id } = c.req.valid('param');
609
+ const body = c.req.valid('json');
610
+ const gate = await gateMutation(c, id, 'manager');
611
+ if ('response' in gate) return gate.response as never;
612
+ if (body.teamId !== undefined) return (await addTeam(c, id, body.teamId, body.role)) as never;
613
+
614
+ // The target comes from THIS workspace's own roster, by user id or by
615
+ // email, and nowhere else: nothing is invited, claimed or minted here.
616
+ const [target] = await db
617
+ .select({
618
+ id: workspaceMembers.id,
619
+ userId: workspaceMembers.userId,
620
+ email: userTable.email,
621
+ name: userTable.name,
622
+ origin: workspaceMembers.origin,
623
+ isActive: workspaceMembers.isActive
624
+ })
625
+ .from(workspaceMembers)
626
+ .innerJoin(userTable, eq(workspaceMembers.userId, userTable.id))
627
+ .where(
628
+ and(
629
+ eq(workspaceMembers.workspaceId, principal.workspaceId),
630
+ body.userId !== undefined
631
+ ? eq(workspaceMembers.userId, body.userId)
632
+ : sql`lower(${userTable.email}) = ${body.email!.toLowerCase()}`
633
+ )
634
+ )
635
+ .limit(1);
636
+ if (!target || !target.isActive) {
637
+ return c.json(err('member_not_found', 'No active member of this workspace matches'), 404);
638
+ }
639
+ // A guest membership exists for principal resolution only (D2): it can
640
+ // never hold a project grant. The predicates would ignore the row anyway;
641
+ // refusing it here keeps the table honest.
642
+ if (target.origin === 'guest') {
643
+ return c.json(
644
+ err(
645
+ 'guest_target',
646
+ 'A guest cannot be a project member: invite them to the workspace as a member first'
647
+ ),
648
+ 403
649
+ );
650
+ }
651
+ // The read above builds the answer; the one that counts is the locked
652
+ // re-read inside the insert's transaction (`holdLiveMembership`): a
653
+ // removal that landed since is seen here, one still running waits for
654
+ // this insert and takes it in its second pass.
655
+ const inserted = await whileLive(principal, id, async (tx) => {
656
+ if (!(await holdLiveMembership(tx, principal.workspaceId, { memberId: target.id }))) {
657
+ return 'member_gone' as const;
658
+ }
659
+ // A grant already there answers `already_member` WITHOUT an insert (the
660
+ // verifier's F4: a duplicate add beside a removal deadlocked, 14 of 40
661
+ // removals answered 500). Under the membership share lock, a removal's
662
+ // uncommitted delete of this grant keeps the row visible to a plain
663
+ // read, so the add answers 409 and never waits on that delete, while
664
+ // the removal's update waits on the add's share lock: the insert's
665
+ // conflict check would wait on the delete, a cycle.
666
+ const [existing] = await tx
667
+ .select({ id: projectMembers.id })
668
+ .from(projectMembers)
669
+ .where(and(eq(projectMembers.projectId, id), eq(projectMembers.memberId, target.id)))
670
+ .limit(1);
671
+ if (existing) return [];
672
+ return tx
673
+ .insert(projectMembers)
674
+ .values({ projectId: id, memberId: target.id, role: body.role, addedBy: principal.userId })
675
+ .onConflictDoNothing({ target: [projectMembers.projectId, projectMembers.memberId] })
676
+ .returning({ createdAt: projectMembers.createdAt });
677
+ });
678
+ if (inserted === null) return c.json(archived(), 409);
679
+ if (inserted === 'member_gone') {
680
+ return c.json(err('member_not_found', 'No active member of this workspace matches'), 404);
681
+ }
682
+ const [row] = inserted;
683
+ if (!row) {
684
+ return c.json(err('already_member', 'This person is already a member of the project'), 409);
685
+ }
686
+ // The answer is built from what the insert returned and the target row
687
+ // read above, never from a re-read outside the transaction: a removal
688
+ // landing in between would turn a succeeded add into a 500.
689
+ const member = { ...target, role: body.role, addedBy: principal.userId, createdAt: row.createdAt };
690
+ c.set('audit', {
691
+ action: 'project.member_add',
692
+ resourceType: 'project',
693
+ resourceId: id,
694
+ metadata: { targetUserId: target.userId, role: body.role }
695
+ });
696
+ return c.json(memberToWire(member), 201);
697
+ });
698
+
699
+ api.openapi(projectMemberRoleRoute, async (c) => {
700
+ const principal = c.get('principal')!;
701
+ const { id, userId } = c.req.valid('param');
702
+ const { role } = c.req.valid('json');
703
+ const gate = await gateMutation(c, id, 'manager');
704
+ if ('response' in gate) return gate.response as never;
705
+ const target = await findProjectMember(db, id, userId);
706
+ if (!target) return c.json(err('member_not_found', 'This person is not a member of the project'), 404);
707
+ const done = await whileLive(principal, id, (tx) =>
708
+ tx.update(projectMembers).set({ role }).where(eq(projectMembers.id, target.id))
709
+ );
710
+ if (done === null) return c.json(archived(), 409);
711
+ c.set('audit', {
712
+ action: 'project.member_role',
713
+ resourceType: 'project',
714
+ resourceId: id,
715
+ metadata: { targetUserId: userId, from: target.role, to: role }
716
+ });
717
+ return c.json(memberToWire({ ...target, role }), 200);
718
+ });
719
+
720
+ // A manager removes anyone; any member removes THEMSELVES, whatever their
721
+ // role. There is no last-manager guard: a workspace owner or admin manages
722
+ // every project, so none is ever locked.
723
+ api.openapi(projectMemberRemoveRoute, async (c) => {
724
+ const principal = c.get('principal')!;
725
+ const { id, userId } = c.req.valid('param');
726
+ const self = userId === principal.userId;
727
+ const gate = await gateMutation(c, id, self ? 'viewer' : 'manager');
728
+ if ('response' in gate) return gate.response as never;
729
+ const target = await findProjectMember(db, id, userId);
730
+ if (!target) return c.json(err('member_not_found', 'This person is not a member of the project'), 404);
731
+ const done = await whileLive(principal, id, (tx) =>
732
+ tx.delete(projectMembers).where(eq(projectMembers.id, target.id))
733
+ );
734
+ if (done === null) return c.json(archived(), 409);
735
+ c.set('audit', {
736
+ action: 'project.member_remove',
737
+ resourceType: 'project',
738
+ resourceId: id,
739
+ metadata: { targetUserId: userId, role: target.role, self }
740
+ });
741
+ return c.json(memberToWire(target), 200);
742
+ });
743
+
744
+ // ── Teams on a project (PRDCT-2794) ──────────────────────────────────────
745
+ // The same gate and order as a person's entry: 404, 403, 409. A team is
746
+ // never "itself" (no self-removal branch): a manager acts, or an owner or
747
+ // admin of the workspace.
748
+ api.openapi(projectTeamRoleRoute, async (c) => {
749
+ const principal = c.get('principal')!;
750
+ const { id, teamId } = c.req.valid('param');
751
+ const { role } = c.req.valid('json');
752
+ const gate = await gateMutation(c, id, 'manager');
753
+ if ('response' in gate) return gate.response as never;
754
+ const target = await findProjectTeam(db, id, teamId);
755
+ if (!target) return c.json(err('team_not_found', 'This team is not a member of the project'), 404);
756
+ const done = await whileLive(principal, id, (tx) =>
757
+ tx.update(projectTeams).set({ role }).where(eq(projectTeams.id, target.id))
758
+ );
759
+ if (done === null) return c.json(archived(), 409);
760
+ c.set('audit', {
761
+ action: 'project.team_role',
762
+ resourceType: 'project',
763
+ resourceId: id,
764
+ metadata: { teamId, from: target.role, to: role }
765
+ });
766
+ return c.json(teamToWire({ ...target, role }), 200);
767
+ });
768
+
769
+ api.openapi(projectTeamRemoveRoute, async (c) => {
770
+ const principal = c.get('principal')!;
771
+ const { id, teamId } = c.req.valid('param');
772
+ const gate = await gateMutation(c, id, 'manager');
773
+ if ('response' in gate) return gate.response as never;
774
+ const target = await findProjectTeam(db, id, teamId);
775
+ if (!target) return c.json(err('team_not_found', 'This team is not a member of the project'), 404);
776
+ const done = await whileLive(principal, id, (tx) =>
777
+ tx.delete(projectTeams).where(eq(projectTeams.id, target.id))
778
+ );
779
+ if (done === null) return c.json(archived(), 409);
780
+ c.set('audit', {
781
+ action: 'project.team_remove',
782
+ resourceType: 'project',
783
+ resourceId: id,
784
+ metadata: { teamId, role: target.role }
785
+ });
786
+ return c.json(teamToWire(target), 200);
787
+ });
788
+ }