@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,78 @@
1
+ import { createRemoteJWKSet, jwtVerify, type JWTPayload, type JWTVerifyOptions } from 'jose';
2
+
3
+ /** Discovery-document cache lifetime; the JWKS itself is cached by jose. */
4
+ const DISCOVERY_TTL_MS = 10 * 60 * 1000;
5
+
6
+ /**
7
+ * Verifies hub-minted RS256 JWTs against the hub's REMOTE JWKS (discovered
8
+ * via OIDC discovery at the issuer root) — the cloud edition's trust anchor
9
+ * for "Sign in with Antasphere" (internal/federation.md). Same discipline as
10
+ * the local OauthJwtVerifier: hard issuer pinning, per-call audience
11
+ * pinning, RS256 allowlist (implicitly rejects none/HS256), small clock
12
+ * tolerance. jose's remote key set self-refreshes on an unknown `kid`; a
13
+ * signature failure on a KNOWN kid (rotation racing the cache) gets exactly
14
+ * one retry against a freshly created key set.
15
+ */
16
+ export class HubJwtVerifier {
17
+ private discoveryCache: { jwksUri: string; fetchedAt: number } | null = null;
18
+ private keySet: ReturnType<typeof createRemoteJWKSet> | null = null;
19
+
20
+ constructor(private readonly issuerUrl: string) {}
21
+
22
+ private async jwksUri(): Promise<string> {
23
+ if (this.discoveryCache && Date.now() - this.discoveryCache.fetchedAt < DISCOVERY_TTL_MS) {
24
+ return this.discoveryCache.jwksUri;
25
+ }
26
+ const url = this.issuerUrl.replace(/\/+$/, '') + '/.well-known/openid-configuration';
27
+ const res = await fetch(url);
28
+ if (!res.ok) throw new Error(`hub discovery failed: ${res.status} ${url}`);
29
+ const doc = (await res.json()) as { issuer?: string; jwks_uri?: string };
30
+ // The discovery document must claim the issuer we were configured to
31
+ // trust — a mismatch means HUB_ISSUER_URL points at the wrong deployment
32
+ // (or something impersonating it); fail closed before trusting its keys.
33
+ if (doc.issuer !== this.issuerUrl) {
34
+ throw new Error(`hub discovery issuer mismatch: expected ${this.issuerUrl}, got ${doc.issuer}`);
35
+ }
36
+ if (typeof doc.jwks_uri !== 'string' || !doc.jwks_uri) {
37
+ throw new Error('hub discovery document carries no jwks_uri');
38
+ }
39
+ this.discoveryCache = { jwksUri: doc.jwks_uri, fetchedAt: Date.now() };
40
+ return this.discoveryCache.jwksUri;
41
+ }
42
+
43
+ private async keys(forceRefresh = false) {
44
+ const jwksUri = await this.jwksUri();
45
+ if (!forceRefresh && this.keySet) return this.keySet;
46
+ this.keySet = createRemoteJWKSet(new URL(jwksUri));
47
+ return this.keySet;
48
+ }
49
+
50
+ /** Verify a hub JWT with the given audience pin; throws on any failure. */
51
+ async verify(token: string, audience: string): Promise<JWTPayload> {
52
+ const options: JWTVerifyOptions = {
53
+ issuer: this.issuerUrl,
54
+ // The hub access token's `aud` is an ARRAY when openid is scoped
55
+ // ([<our resource>, <hub userinfo>]); jose accepts any element
56
+ // matching, so pinning our own value stays exact.
57
+ audience,
58
+ algorithms: ['RS256'], // hard allowlist — implicitly rejects none/HS256
59
+ clockTolerance: 5,
60
+ requiredClaims: ['sub']
61
+ };
62
+ try {
63
+ return (await jwtVerify(token, await this.keys(), options)).payload;
64
+ } catch (err) {
65
+ // One retry with fresh keys covers signing-key rotation mid-cache.
66
+ // Two shapes mean "the cached keys may be stale": an unknown kid
67
+ // (JWKSNoMatchingKey — jose's own refetch is cooldown-throttled and
68
+ // may have been skipped) and a signature failure on a known kid.
69
+ if (
70
+ err instanceof Error &&
71
+ (err.name === 'JWKSNoMatchingKey' || err.name === 'JWSSignatureVerificationFailed')
72
+ ) {
73
+ return (await jwtVerify(token, await this.keys(true), options)).payload;
74
+ }
75
+ throw err;
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,167 @@
1
+ import { and, eq } from 'drizzle-orm';
2
+ import { workspaceMembers, workspaces, type Db } from '@antasphere/chassis-db';
3
+ import type { Logger } from '../logger.js';
4
+ import type { GateWorkspace, PrincipalGate, PrincipalGateResult } from '../middleware/auth-context.js';
5
+ import type { HubOrgReconciler } from './hub-reconcile.js';
6
+ import { resolveMembership } from './resolve-membership.js';
7
+
8
+ /**
9
+ * The cloud edition's post-resolution principal gate (user-scoped
10
+ * federation — the replacement for the retired master-key P4 gates): on
11
+ * EVERY authenticated request into a PROJECTED workspace
12
+ * (`principal.accountRef` set), hub truth is re-read AS THE USER via the
13
+ * TTL'd reconciler, and the request is judged on the freshly reconciled
14
+ * LOCAL rows. No service key, no cross-tenant surface — a user's requests
15
+ * can only ever be gated by their OWN grant's view of the hub.
16
+ *
17
+ * Verdicts, in order:
18
+ * - grant dead → 401 `hub_grant_expired` (immediate, no stale window —
19
+ * Romain's decision 3; a browser SSO re-login heals the grant);
20
+ * - reconcile stale beyond the window AND the hub failing → 403
21
+ * `hub_unavailable` (fail closed after ~15 min of outage);
22
+ * - hub-origin membership row now inactive (this very pass may have swept
23
+ * it) → 401 `membership_revoked`;
24
+ * - workspace `hub_status='suspended'` → 403 `account_suspended`, EXCEPT
25
+ * `GET /api/v1/me` — suspended is VISIBLE-BUT-BLOCKED, and /me is how
26
+ * the dashboard shows the suspended badge instead of stranding the user
27
+ * (the reason the PrincipalGate seam carries the request);
28
+ * - a role delta from the reconciled row applies to THIS request (D11);
29
+ * - a request that SELECTED no workspace, on a credential that pins none,
30
+ * runs in the person's default AS THE PASS LEFT IT: the resolver chose
31
+ * the workspace before the pass ran, so a default moved at the hub
32
+ * (`antasphere org use`) would land the very command that read it in the
33
+ * old organization and only the next one in the new (PRDCT-2958, F2).
34
+ * The membership is resolved again here and handed back as `workspace`.
35
+ *
36
+ * Guest and local-origin rows in a projected workspace skip hub enforcement
37
+ * entirely EXCEPT the org-level suspension read — and that reads the
38
+ * LOCALLY MATERIALIZED `workspaces.hub_status` column only, never a hub
39
+ * round-trip on a borrowed grant. ACCEPTED BOUND (documented): a guest's
40
+ * own `/orgs` never includes the resource's org, so that column refreshes only
41
+ * when a MEMBER's reconcile runs — a guest may keep resource access in a
42
+ * hub-suspended org until a member next touches the tool. Every cheap fix
43
+ * leaks (a public status probe = suspension oracle) or borrows credentials
44
+ * (acting on a grant its holder didn't present). Same accepted-bound class
45
+ * as ungated share-link viewing; suspension still cuts all MEMBERS within
46
+ * seconds.
47
+ */
48
+
49
+ const SUSPENDED_MESSAGE = 'This organization is suspended on Antasphere';
50
+ const UNAVAILABLE_MESSAGE =
51
+ 'The Antasphere hub has been unreachable for too long; requests are refused until it recovers';
52
+ const GRANT_EXPIRED_MESSAGE =
53
+ 'Your Antasphere grant on this instance has expired — sign in with Antasphere again to renew it';
54
+
55
+ /** The one suspension exemption: the dashboard's whoami read. */
56
+ function isMeRead(request: { path: string; method: string }): boolean {
57
+ return request.method === 'GET' && request.path === '/api/v1/me';
58
+ }
59
+
60
+ export interface HubLiveGateDeps {
61
+ db: Db;
62
+ reconciler: HubOrgReconciler;
63
+ logger: Logger;
64
+ }
65
+
66
+ export class HubLiveGate {
67
+ constructor(private readonly deps: HubLiveGateDeps) {}
68
+
69
+ readonly assert: PrincipalGate = async (principal, request) => {
70
+ // Unprojected workspace: zero hub surface on this request.
71
+ if (!principal.accountRef) return { ok: true };
72
+
73
+ // Guest/local rows are the tool's own business (never hub truth): no
74
+ // reconcile, no membership re-assertion, no grant requirement. Org-level
75
+ // suspension still applies, read from the local column.
76
+ if (principal.origin !== 'hub') {
77
+ return this.assertOrgActive(principal.workspaceId, request);
78
+ }
79
+
80
+ const verdict = await this.deps.reconciler.reconcile(principal.userId);
81
+ if (verdict.kind === 'grant_dead') {
82
+ return { ok: false, status: 401, code: 'hub_grant_expired', message: GRANT_EXPIRED_MESSAGE };
83
+ }
84
+ if (verdict.kind === 'unavailable') {
85
+ return { ok: false, status: 403, code: 'hub_unavailable', message: UNAVAILABLE_MESSAGE };
86
+ }
87
+
88
+ // Judge the FRESHLY RECONCILED local rows (the pass above may have
89
+ // swept the membership, synced the role, or flipped hub_status).
90
+ const row = await this.memberRow(principal.userId, principal.workspaceId);
91
+ if (!row || !row.isActive) {
92
+ // Resolution saw an active row moments ago; the reconcile swept it —
93
+ // the hub no longer asserts this membership. Every later request dies
94
+ // at resolution itself (plain 401); THIS one carries the reason. A
95
+ // swept membership is never papered over by a move to another
96
+ // workspace below: the person learns what happened.
97
+ return {
98
+ ok: false,
99
+ status: 401,
100
+ code: 'membership_revoked',
101
+ message: 'Your membership of this organization was removed on Antasphere'
102
+ };
103
+ }
104
+
105
+ // A selector-less request on an unpinned credential runs in the default
106
+ // membership as the pass above left it (the resolver read it before, so
107
+ // a default moved at the hub would land THIS request in the old one).
108
+ // The moved-to workspace is judged exactly as the resolved one is.
109
+ if (request.selector === null && !principal.pinned) {
110
+ const fresh = await resolveMembership(this.deps.db, principal.userId, null);
111
+ if (fresh && fresh.workspaceId !== principal.workspaceId) {
112
+ const target = await this.memberRow(principal.userId, fresh.workspaceId);
113
+ if (target?.isActive) {
114
+ if (target.hubStatus === 'suspended' && !isMeRead(request)) {
115
+ return { ok: false, status: 403, code: 'account_suspended', message: SUSPENDED_MESSAGE };
116
+ }
117
+ const moved: GateWorkspace = {
118
+ workspaceId: fresh.workspaceId,
119
+ role: target.role,
120
+ origin: fresh.origin,
121
+ accountRef: fresh.accountRef
122
+ };
123
+ return { ok: true, workspace: moved };
124
+ }
125
+ }
126
+ }
127
+
128
+ if (row.hubStatus === 'suspended' && !isMeRead(request)) {
129
+ return { ok: false, status: 403, code: 'account_suspended', message: SUSPENDED_MESSAGE };
130
+ }
131
+ // A role delta from the reconciled row applies to THIS request (D11):
132
+ // a demoted admin loses admin surfaces now, a promotion lands now.
133
+ if (row.role !== principal.role) {
134
+ return { ok: true, role: row.role };
135
+ }
136
+ return { ok: true };
137
+ };
138
+
139
+ private async memberRow(userId: string, workspaceId: string) {
140
+ const [row] = await this.deps.db
141
+ .select({
142
+ role: workspaceMembers.role,
143
+ isActive: workspaceMembers.isActive,
144
+ hubStatus: workspaces.hubStatus
145
+ })
146
+ .from(workspaceMembers)
147
+ .innerJoin(workspaces, eq(workspaceMembers.workspaceId, workspaces.id))
148
+ .where(and(eq(workspaceMembers.userId, userId), eq(workspaceMembers.workspaceId, workspaceId)))
149
+ .limit(1);
150
+ return row;
151
+ }
152
+
153
+ private async assertOrgActive(
154
+ workspaceId: string,
155
+ request: { path: string; method: string }
156
+ ): Promise<PrincipalGateResult> {
157
+ const [ws] = await this.deps.db
158
+ .select({ hubStatus: workspaces.hubStatus })
159
+ .from(workspaces)
160
+ .where(eq(workspaces.id, workspaceId))
161
+ .limit(1);
162
+ if (ws?.hubStatus === 'suspended' && !isMeRead(request)) {
163
+ return { ok: false, status: 403, code: 'account_suspended', message: SUSPENDED_MESSAGE };
164
+ }
165
+ return { ok: true };
166
+ }
167
+ }
@@ -0,0 +1,174 @@
1
+ import { and, eq } from 'drizzle-orm';
2
+ import { decodeJwt } from 'jose';
3
+ import { symmetricDecrypt } from 'better-auth/crypto';
4
+ import { account, type Db } from '@antasphere/chassis-db';
5
+ import type { Logger } from '../logger.js';
6
+ import { isLikelyEncrypted } from './hub-grant.js';
7
+ import { HUB_SSO_PROVIDER_ID } from './hub-sso.js';
8
+
9
+ /**
10
+ * RP-initiated logout against the hub (SL-2, internal/federation.md): "one
11
+ * concept of being logged in" needs logout-anywhere to end the HUB session,
12
+ * not just the local one. The hub's oauth-provider plugin exposes
13
+ * `GET /oauth2/end-session` (RP-initiated logout); this module builds the
14
+ * URL the browser must VISIT to end the anchor session — the local revoke
15
+ * itself happens in api/sso-logout.ts regardless of whether a URL could be
16
+ * built.
17
+ *
18
+ * Everything here FAILS SOFT to `null` ("no hub leg — local signout only"):
19
+ * a logout must never be blocked by a missing id_token, a hub outage, or a
20
+ * malformed discovery document. Pinned 1.6.15 plugin facts this leans on
21
+ * (re-verify on ANY Better Auth / oauth-provider bump):
22
+ *
23
+ * - the id_token is stored PLAINTEXT on the `account` row —
24
+ * `encryptOAuthTokens` wraps only access/refresh tokens (setTokenUtil,
25
+ * dist/oauth2/utils.mjs; idToken is written raw in
26
+ * generic-oauth/routes.mjs + oauth2/link-account.mjs). The read below is
27
+ * still encrypted-TOLERANT (the hub-grant isLikelyEncrypted mirror) so a
28
+ * future bump that starts encrypting it cannot break logout;
29
+ * - the end-session endpoint HARD-FAILS on an id_token_hint without a
30
+ * `sid` claim ("id token missing session") — pre-flip id_tokens (minted
31
+ * before the hub client's enableEndSession flip) carry none, so a
32
+ * sid-less token degrades HERE to null instead of bouncing the user
33
+ * through a hub error;
34
+ * - verification there is signature-only (jose compactVerify against the
35
+ * hub JWKS) — an EXPIRED id_token is still a usable hint, so no expiry
36
+ * check happens here either;
37
+ * - `post_logout_redirect_uri` must EXACT-match the client's registered
38
+ * postLogoutRedirectUris — the cross-repo contract pins
39
+ * `<PUBLIC_BASE_URL>/login?signed_out=1`, seeded in the hub registry.
40
+ */
41
+
42
+ /** The pinned post-logout landing (cross-repo contract — match EXACTLY). */
43
+ export function postLogoutRedirectUri(publicBaseUrl: string): string {
44
+ return publicBaseUrl.replace(/\/+$/, '') + '/login?signed_out=1';
45
+ }
46
+
47
+ export interface HubLogoutOptions {
48
+ db: Db;
49
+ /** Hub OIDC issuer (HUB_ISSUER_URL) — discovery derives from it. */
50
+ issuerUrl: string;
51
+ /** This instance's public origin — the post-logout landing derives from it. */
52
+ publicBaseUrl: string;
53
+ /**
54
+ * Better Auth's token-encryption key material (`(await
55
+ * auth.$context).secretConfig`) — only ever consulted when a stored
56
+ * id_token LOOKS encrypted (future-bump insurance; today's are plaintext).
57
+ */
58
+ key: () => Promise<Parameters<typeof symmetricDecrypt>[0]['key']>;
59
+ logger: Logger;
60
+ /** Discovery fetch timeout — a slow hub must never hang a logout. */
61
+ discoveryTimeoutMs?: number;
62
+ /** Test seam. */
63
+ fetchImpl?: typeof fetch;
64
+ }
65
+
66
+ export class HubLogoutService {
67
+ private readonly fetchImpl: typeof fetch;
68
+ private readonly timeoutMs: number;
69
+ /** Discovery-cached end-session endpoint; failures are retried per call. */
70
+ private endpointCache: string | null = null;
71
+
72
+ constructor(private readonly opts: HubLogoutOptions) {
73
+ this.fetchImpl = opts.fetchImpl ?? fetch;
74
+ this.timeoutMs = opts.discoveryTimeoutMs ?? 3_000;
75
+ }
76
+
77
+ /**
78
+ * The hub end-session URL for this user's CURRENT hub session, or null
79
+ * when no usable hub leg exists (no `antasphere` account row, no stored
80
+ * id_token, a sid-less pre-flip id_token, or an unreachable/endpoint-less
81
+ * discovery document). The caller revokes the local session and clears
82
+ * the hint EITHER WAY — null only means the browser skips the hub bounce.
83
+ */
84
+ async endSessionUrl(userId: string): Promise<string | null> {
85
+ try {
86
+ const idToken = await this.readIdToken(userId);
87
+ if (!idToken) return null;
88
+ // decode (NOT verify) — we only need the sid presence check; the hub
89
+ // verifies the hint's signature itself on presentation.
90
+ let sid: unknown;
91
+ try {
92
+ sid = decodeJwt(idToken).sid;
93
+ } catch {
94
+ return null; // not a JWT — nothing presentable
95
+ }
96
+ if (typeof sid !== 'string' || !sid) {
97
+ // Pre-flip id_token (rollout caveat): the hub would hard-fail the
98
+ // hint, so degrade to a local-only signout until the next login
99
+ // stores a sid-carrying token.
100
+ this.opts.logger.info({ userId }, 'hub logout: stored id_token carries no sid — local signout only');
101
+ return null;
102
+ }
103
+ const endpoint = await this.endSessionEndpoint();
104
+ if (!endpoint) return null;
105
+ const url = new URL(endpoint);
106
+ url.searchParams.set('id_token_hint', idToken);
107
+ url.searchParams.set('post_logout_redirect_uri', postLogoutRedirectUri(this.opts.publicBaseUrl));
108
+ return url.toString();
109
+ } catch (err) {
110
+ this.opts.logger.warn({ err, userId }, 'hub logout: end-session URL build failed — local signout only');
111
+ return null;
112
+ }
113
+ }
114
+
115
+ /** The user's stored hub id_token, plaintext/encrypted-tolerant. */
116
+ private async readIdToken(userId: string): Promise<string | null> {
117
+ const [row] = await this.opts.db
118
+ .select({ idToken: account.idToken })
119
+ .from(account)
120
+ .where(and(eq(account.userId, userId), eq(account.providerId, HUB_SSO_PROVIDER_ID)))
121
+ .limit(1);
122
+ const stored = row?.idToken;
123
+ if (!stored) return null;
124
+ // A JWT (dots, base64url) never matches isLikelyEncrypted, so today's
125
+ // plaintext tokens pass straight through; a future encrypted value gets
126
+ // one decrypt attempt and fails soft.
127
+ if (!isLikelyEncrypted(stored)) return stored;
128
+ try {
129
+ return await symmetricDecrypt({ key: await this.opts.key(), data: stored });
130
+ } catch (err) {
131
+ this.opts.logger.warn({ err, userId }, 'hub logout: stored id_token failed to decrypt');
132
+ return null;
133
+ }
134
+ }
135
+
136
+ /** Discovery read, cached on success; a failed fetch retries next call. */
137
+ private async endSessionEndpoint(): Promise<string | null> {
138
+ if (this.endpointCache) return this.endpointCache;
139
+ const discoveryUrl = this.opts.issuerUrl.replace(/\/+$/, '') + '/.well-known/openid-configuration';
140
+ try {
141
+ const res = await this.fetchImpl(discoveryUrl, {
142
+ headers: { accept: 'application/json' },
143
+ signal: AbortSignal.timeout(this.timeoutMs)
144
+ });
145
+ if (!res.ok) {
146
+ this.opts.logger.warn({ status: res.status }, 'hub logout: discovery answered non-2xx');
147
+ return null;
148
+ }
149
+ const doc = (await res.json()) as { end_session_endpoint?: unknown };
150
+ const endpoint = doc.end_session_endpoint;
151
+ if (typeof endpoint !== 'string' || !endpoint) {
152
+ // A hub predating the end-session flip advertises none — degrade.
153
+ this.opts.logger.info('hub logout: discovery carries no end_session_endpoint — local signout only');
154
+ return null;
155
+ }
156
+ this.endpointCache = endpoint;
157
+ return endpoint;
158
+ } catch (err) {
159
+ this.opts.logger.warn({ err }, 'hub logout: discovery unreachable — local signout only');
160
+ return null;
161
+ }
162
+ }
163
+ }
164
+
165
+ /**
166
+ * The Set-Cookie header value that CLEARS the shared SSO hint cookie —
167
+ * mirroring the hub-set attributes exactly (cross-repo contract: Domain on
168
+ * the shared parent, Path=/, SameSite=Lax, Secure on https, NOT HttpOnly)
169
+ * so the browser matches and expires the hub's cookie rather than minting a
170
+ * host-only shadow. Max-Age=0 is the deletion.
171
+ */
172
+ export function hintCookieClearHeader(opts: { name: string; domain: string; secure: boolean }): string {
173
+ return `${opts.name}=; Max-Age=0; Domain=${opts.domain}; Path=/; SameSite=Lax${opts.secure ? '; Secure' : ''}`;
174
+ }
@@ -0,0 +1,251 @@
1
+ import { and, eq, inArray, isNotNull, lt, notInArray, sql } from 'drizzle-orm';
2
+ import {
3
+ workspaceMembers,
4
+ workspaceTeamMembers,
5
+ workspaceTeams,
6
+ workspaces,
7
+ type Db,
8
+ type DbConn,
9
+ type WorkspaceRole
10
+ } from '@antasphere/chassis-db';
11
+
12
+ /**
13
+ * The ONE hub-org projection primitive (internal/federation.md, ADR 015/018):
14
+ * extracted verbatim from `HubSsoService.project()` so the SSO login path
15
+ * and the H4 reconciler (`hub-reconcile.ts`) write byte-identical rows.
16
+ *
17
+ * Lazy projection: the hub org becomes a local workspace on first use
18
+ * (`centralAccountId` = the hub org id), then a membership upsert
19
+ * re-asserts (role, origin='hub', active) on EVERY call — reactivating a
20
+ * deactivated row and following hub role changes (D11). Deliberately NOT
21
+ * WorkspaceService.create: the projection needs ON CONFLICT semantics on
22
+ * the 0021 unique partial index, the hub's role (not owner), and
23
+ * origin='hub'.
24
+ *
25
+ * Throws raw on any database failure — each caller owns its error posture
26
+ * (the login path maps to the fail-closed `sso_projection_failed`; the
27
+ * reconciler logs and continues with the next entry).
28
+ */
29
+ export interface OrgMembershipProjection {
30
+ localUserId: string;
31
+ /** The hub org id (uuid, validated upstream) — never a local workspace id. */
32
+ hubWorkspaceId: string;
33
+ /** Display name (H1); null keeps the current name (or mints the placeholder). */
34
+ hubWorkspaceName: string | null;
35
+ /** The hub org role, verbatim (D11) — validated upstream, never invented here. */
36
+ role: WorkspaceRole;
37
+ }
38
+
39
+ /**
40
+ * The name a hub org goes by when the hub sends none (an older hub, a blank
41
+ * name): recognizable, and self-healing wherever the next hub read carries
42
+ * the real one. The projection's H1 fallback and the refusal page's hint.
43
+ */
44
+ export function placeholderWorkspaceName(hubOrgId: string): string {
45
+ return `Antasphere workspace ${hubOrgId.slice(0, 8)}`;
46
+ }
47
+
48
+ export async function projectOrgMembership(
49
+ db: Db,
50
+ projection: OrgMembershipProjection
51
+ ): Promise<{ workspaceId: string; memberId: string }> {
52
+ const { localUserId, hubWorkspaceId, hubWorkspaceName, role } = projection;
53
+ let [ws] = await db
54
+ .select({ id: workspaces.id, name: workspaces.name })
55
+ .from(workspaces)
56
+ .where(eq(workspaces.centralAccountId, hubWorkspaceId))
57
+ .limit(1);
58
+ if (!ws) {
59
+ // H1 fallback: without workspace_name (older hub), project under a
60
+ // recognizable placeholder — self-healing, the re-sync below renames
61
+ // it at the first call whose hub data carries the name.
62
+ const name = hubWorkspaceName ?? placeholderWorkspaceName(hubWorkspaceId);
63
+ const inserted = await db
64
+ .insert(workspaces)
65
+ .values({ name, centralAccountId: hubWorkspaceId })
66
+ .onConflictDoNothing({
67
+ target: workspaces.centralAccountId,
68
+ where: sql`${workspaces.centralAccountId} IS NOT NULL`
69
+ })
70
+ .returning({ id: workspaces.id, name: workspaces.name });
71
+ // Conflict = a concurrent first projection won the insert; land on its row.
72
+ ws =
73
+ inserted[0] ??
74
+ (
75
+ await db
76
+ .select({ id: workspaces.id, name: workspaces.name })
77
+ .from(workspaces)
78
+ .where(eq(workspaces.centralAccountId, hubWorkspaceId))
79
+ .limit(1)
80
+ )[0];
81
+ if (!ws) throw new Error('projection insert and re-select both returned nothing');
82
+ }
83
+ if (hubWorkspaceName && ws.name !== hubWorkspaceName) {
84
+ await db.update(workspaces).set({ name: hubWorkspaceName }).where(eq(workspaces.id, ws.id));
85
+ }
86
+ const [member] = await db
87
+ .insert(workspaceMembers)
88
+ .values({
89
+ workspaceId: ws.id,
90
+ userId: localUserId,
91
+ role,
92
+ origin: 'hub',
93
+ isActive: true
94
+ })
95
+ .onConflictDoUpdate({
96
+ target: [workspaceMembers.workspaceId, workspaceMembers.userId],
97
+ set: { role, origin: 'hub', isActive: true }
98
+ })
99
+ .returning({ id: workspaceMembers.id });
100
+ if (!member) throw new Error('membership upsert returned nothing');
101
+ return { workspaceId: ws.id, memberId: member.id };
102
+ }
103
+
104
+ /**
105
+ * The person's team seats in ONE projected workspace, as the hub asserted
106
+ * them in this pass (`GET /orgs`' per-org `teams`: the caller's own, never
107
+ * the team's roster). Upserts each team by its hub id (slug and name follow
108
+ * the hub; `updated_at` moves only when one of them changed), inserts the
109
+ * missing seats, and deletes this membership's seats whose team the hub no
110
+ * longer lists. Both the login pass and the periodic pass come through here,
111
+ * so they write byte-identical rows.
112
+ *
113
+ * Run inside one transaction per org by the caller. Throws raw, like
114
+ * `projectOrgMembership`. A team id already projected into ANOTHER workspace
115
+ * (a hub id cannot move between organizations; only a corrupt answer could
116
+ * say so) is left where it is and not seated here.
117
+ */
118
+ export interface TeamSeatsProjection {
119
+ /** The projected workspace (from `projectOrgMembership`). */
120
+ workspaceId: string;
121
+ /** The person's membership row in it — the seat rides on it. */
122
+ memberId: string;
123
+ teams: ReadonlyArray<{ id: string; slug: string; name: string }>;
124
+ }
125
+
126
+ export async function projectTeamSeats(db: DbConn, projection: TeamSeatsProjection): Promise<void> {
127
+ const { workspaceId, memberId, teams } = projection;
128
+ const seatTeamIds: string[] = [];
129
+ if (teams.length > 0) {
130
+ await db
131
+ .insert(workspaceTeams)
132
+ .values(teams.map((team) => ({ workspaceId, hubTeamId: team.id, slug: team.slug, name: team.name })))
133
+ .onConflictDoUpdate({
134
+ target: workspaceTeams.hubTeamId,
135
+ set: {
136
+ slug: sql`excluded.slug`,
137
+ name: sql`excluded.name`,
138
+ updatedAt: sql`now()`
139
+ },
140
+ // Renamed on drift only, and never across organizations.
141
+ setWhere: sql`${workspaceTeams.workspaceId} = ${workspaceId}
142
+ AND (${workspaceTeams.slug} IS DISTINCT FROM excluded.slug
143
+ OR ${workspaceTeams.name} IS DISTINCT FROM excluded.name)`
144
+ });
145
+ const rows = await db
146
+ .select({ id: workspaceTeams.id })
147
+ .from(workspaceTeams)
148
+ .where(
149
+ and(
150
+ eq(workspaceTeams.workspaceId, workspaceId),
151
+ inArray(
152
+ workspaceTeams.hubTeamId,
153
+ teams.map((team) => team.id)
154
+ )
155
+ )
156
+ );
157
+ seatTeamIds.push(...rows.map((row) => row.id));
158
+ if (seatTeamIds.length > 0) {
159
+ await db
160
+ .insert(workspaceTeamMembers)
161
+ .values(seatTeamIds.map((teamId) => ({ teamId, memberId })))
162
+ .onConflictDoNothing({ target: [workspaceTeamMembers.teamId, workspaceTeamMembers.memberId] });
163
+ }
164
+ }
165
+ // The seats the hub no longer asserts, in THIS workspace only.
166
+ const inWorkspace = db
167
+ .select({ id: workspaceTeams.id })
168
+ .from(workspaceTeams)
169
+ .where(eq(workspaceTeams.workspaceId, workspaceId));
170
+ await db
171
+ .delete(workspaceTeamMembers)
172
+ .where(
173
+ and(
174
+ eq(workspaceTeamMembers.memberId, memberId),
175
+ inArray(workspaceTeamMembers.teamId, inWorkspace),
176
+ ...(seatTeamIds.length > 0 ? [notInArray(workspaceTeamMembers.teamId, seatTeamIds)] : [])
177
+ )
178
+ );
179
+ }
180
+
181
+ /**
182
+ * The organization's teams in ONE projected workspace, as the hub's `GET
183
+ * /teams` lists them (PRDCT-2813). This is the organization's DEFINITIVE
184
+ * list as one member reads it: every team of the org, whether anyone who
185
+ * signed in here sits in it or not, so a team can be added to a project
186
+ * before any of its members arrived. The rosters stay the seats of the
187
+ * people who signed in (`projectTeamSeats`); this touches no seat itself.
188
+ *
189
+ * Upserts every listed team by its hub id (slug and name follow the hub,
190
+ * `updated_at` moves on drift only, never across organizations: the same
191
+ * rule as `projectTeamSeats`), then, when `complete`, DELETES this
192
+ * workspace's projected teams (`hub_team_id IS NOT NULL`) the list does not
193
+ * name: a team deleted at the hub goes, and the foreign keys' cascade takes
194
+ * its seats and its project entries with it. A local team (`hub_team_id IS
195
+ * NULL`) is never touched. An incomplete list (the drain's page cap) only
196
+ * upserts.
197
+ *
198
+ * Run inside one transaction by the caller. Throws raw.
199
+ */
200
+ export interface OrgTeamsProjection {
201
+ /** The projected workspace (from `projectOrgMembership`). */
202
+ workspaceId: string;
203
+ teams: ReadonlyArray<{ id: string; slug: string; name: string }>;
204
+ /** False when the list is a prefix: nothing is deleted then. Default true. */
205
+ complete?: boolean | undefined;
206
+ /**
207
+ * When the read of the list BEGAN. A projected team created here after that
208
+ * moment (another replica's pass, another person's seat) is not in this
209
+ * list because it is newer than the read, not because the hub dropped it:
210
+ * the delete leaves it alone (verifier round 1, F5). Default: now, which
211
+ * deletes nothing newer than the call.
212
+ */
213
+ readAt?: Date | undefined;
214
+ }
215
+
216
+ export async function projectOrgTeams(db: DbConn, projection: OrgTeamsProjection): Promise<void> {
217
+ const { workspaceId, teams } = projection;
218
+ if (teams.length > 0) {
219
+ await db
220
+ .insert(workspaceTeams)
221
+ .values(teams.map((team) => ({ workspaceId, hubTeamId: team.id, slug: team.slug, name: team.name })))
222
+ .onConflictDoUpdate({
223
+ target: workspaceTeams.hubTeamId,
224
+ set: {
225
+ slug: sql`excluded.slug`,
226
+ name: sql`excluded.name`,
227
+ updatedAt: sql`now()`
228
+ },
229
+ setWhere: sql`${workspaceTeams.workspaceId} = ${workspaceId}
230
+ AND (${workspaceTeams.slug} IS DISTINCT FROM excluded.slug
231
+ OR ${workspaceTeams.name} IS DISTINCT FROM excluded.name)`
232
+ });
233
+ }
234
+ if (projection.complete === false) return;
235
+ const readAt = projection.readAt ?? new Date();
236
+ await db.delete(workspaceTeams).where(
237
+ and(
238
+ eq(workspaceTeams.workspaceId, workspaceId),
239
+ isNotNull(workspaceTeams.hubTeamId),
240
+ lt(workspaceTeams.createdAt, readAt),
241
+ ...(teams.length > 0
242
+ ? [
243
+ notInArray(
244
+ workspaceTeams.hubTeamId,
245
+ teams.map((team) => team.id)
246
+ )
247
+ ]
248
+ : [])
249
+ )
250
+ );
251
+ }