@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,646 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql';
3
+ import {
4
+ createDatabase,
5
+ createTestApp,
6
+ extractCookie,
7
+ readJson,
8
+ startPostgres,
9
+ type TestApp,
10
+ host
11
+ } from './helpers.js';
12
+ import { FakeHub, type HubUserFixture } from '@antasphere/chassis-server/testing';
13
+ import * as sso from './sso-helpers.js';
14
+
15
+ /**
16
+ * Live org reconciliation (identity/hub-reconcile.ts): org and membership
17
+ * truth live ONLY at the hub, read AS THE USER via each user's own grant
18
+ * (never a service key), and every request into a projected workspace
19
+ * reflects it — the live gate runs the cached `reconcile()`, every SSO
20
+ * login the TTL-bypassing force pass. One pass projects missing orgs,
21
+ * syncs names/roles/hub_status/is_default, and deactivates `origin='hub'`
22
+ * memberships the hub no longer asserts.
23
+ *
24
+ * Failure posture pinned here: only a definitive 200 list mutates state;
25
+ * transient hub errors serve local state (nothing deactivated) inside the
26
+ * stale window; probes are retry-throttled. Enforcement verdicts
27
+ * (revoked/suspended/unavailable/grant-dead) are the live-gate suite's.
28
+ *
29
+ * Dials are shrunk so propagation is observable in milliseconds; the stale
30
+ * window is pinned LONG so this suite never trips hub_unavailable.
31
+ */
32
+
33
+ const OWNER = { email: 'owner@reconcile.test', name: 'Op Owner', password: 'op-owner-password-123' };
34
+
35
+ const ORG_ONE = '33333333-aaaa-4bbb-8ccc-000000000001';
36
+ const ORG_TWO = '33333333-aaaa-4bbb-8ccc-000000000002';
37
+ const ORG_THREE = '33333333-aaaa-4bbb-8ccc-000000000003';
38
+ const ORG_TINA = '33333333-aaaa-4bbb-8ccc-000000000005';
39
+ const ORG_FRESH = '33333333-aaaa-4bbb-8ccc-000000000006';
40
+ const ORG_WEIRD = '33333333-aaaa-4bbb-8ccc-000000000009';
41
+ const ORG_DANA = '33333333-aaaa-4bbb-8ccc-00000000000a';
42
+ const ORG_BG = '33333333-aaaa-4bbb-8ccc-00000000000b';
43
+ const ORG_OPX = '33333333-aaaa-4bbb-8ccc-00000000000c';
44
+
45
+ const DIALS = {
46
+ reconcileTtlMs: 120,
47
+ reconcileStaleMaxMs: 60_000,
48
+ retryMs: 250,
49
+ orgsTimeoutMs: 400,
50
+ tokenTimeoutMs: 2_000
51
+ };
52
+
53
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
54
+ /** Let the per-user reconcile cache expire so the next request runs a fresh pass. */
55
+ const expireTtl = () => sleep(DIALS.reconcileTtlMs + 40);
56
+
57
+ let container: StartedPostgreSqlContainer;
58
+ let hub: FakeHub;
59
+ let app: TestApp;
60
+
61
+ beforeAll(async () => {
62
+ [container, hub] = await Promise.all([startPostgres(), FakeHub.start({ clientId: host.hubClientId })]);
63
+ app = await createTestApp(
64
+ await createDatabase(container, 'hub_reconcile'),
65
+ {
66
+ EDITION: 'cloud',
67
+ HUB_ISSUER_URL: hub.issuer,
68
+ HUB_CLIENT_ID: host.hubClientId,
69
+ HUB_CLIENT_SECRET: 'integration-test-hub-secret-0001',
70
+ METRICS_TOKEN: 'reconcile-metrics-token',
71
+ // The break-glass lifeboat suite below acts as the setup operator.
72
+ SUPERADMIN_EMAILS: OWNER.email
73
+ },
74
+ { hubDials: DIALS }
75
+ );
76
+ const res = await app.app.request(
77
+ '/api/v1/setup',
78
+ sso.json({ setupToken: 'integration-test-setup-token', instanceName: 'Reconcile', owner: OWNER })
79
+ );
80
+ expect(res.status).toBe(201);
81
+ }, 240_000);
82
+
83
+ afterAll(async () => {
84
+ await app?.stop();
85
+ await Promise.all([container?.stop(), hub?.stop()]);
86
+ });
87
+
88
+ const me = async (cookie: string, workspaceId?: string) =>
89
+ app.app.request('/api/v1/me', {
90
+ headers: { cookie, ...(workspaceId ? { 'x-workspace-id': workspaceId } : {}) }
91
+ });
92
+
93
+ const orgFetches = () => hub.orgsRequests.length;
94
+
95
+ async function workspaceIdOf(centralAccountId: string): Promise<string> {
96
+ const { rows } = await app.db.pool.query(`SELECT id FROM workspaces WHERE central_account_id = $1`, [
97
+ centralAccountId
98
+ ]);
99
+ expect(rows).toHaveLength(1);
100
+ return rows[0].id as string;
101
+ }
102
+
103
+ async function membershipRow(centralAccountId: string, email: string) {
104
+ const { rows } = await app.db.pool.query(
105
+ `SELECT m.role, m.origin, m.is_active FROM workspace_members m
106
+ JOIN workspaces w ON w.id = m.workspace_id
107
+ JOIN "user" u ON u.id = m.user_id
108
+ WHERE w.central_account_id = $1 AND u.email = $2`,
109
+ [centralAccountId, email]
110
+ );
111
+ return rows;
112
+ }
113
+
114
+ describe('login-time reconciliation (the fail-closed assertLogin pass)', () => {
115
+ const rick: HubUserFixture = {
116
+ sub: 'hub-rick',
117
+ email: 'rick@reconcile.test',
118
+ name: 'Rick Reconciled',
119
+ workspaceId: ORG_ONE,
120
+ role: 'owner',
121
+ workspaceName: 'Rick Org'
122
+ };
123
+ let rickCookie: string;
124
+
125
+ it('ONE login materializes ALL of the user’s hub orgs, not just the one they came from', async () => {
126
+ // The hub knows rick in a SECOND org before he ever logs in here.
127
+ hub.setUserOrg('hub-rick', ORG_TWO, { name: 'Second Org', role: 'member' });
128
+ rickCookie = await sso.ssoLogin(app, hub, rick);
129
+
130
+ const body = await readJson(await me(rickCookie));
131
+ const byName = Object.fromEntries(
132
+ body.workspaces.map((w: { name: string; role: string; hubOrigin: boolean }) => [w.name, w])
133
+ );
134
+ expect(byName['Rick Org']).toMatchObject({ role: 'owner', hubOrigin: true });
135
+ expect(byName['Second Org']).toMatchObject({ role: 'member', hubOrigin: true });
136
+ expect(await membershipRow(ORG_TWO, rick.email)).toEqual([
137
+ { role: 'member', origin: 'hub', is_active: true }
138
+ ]);
139
+ });
140
+
141
+ it('a fresh hub org appears IMMEDIATELY after a re-login (force pass bypasses the TTL)', async () => {
142
+ await me(rickCookie); // warm the cache …
143
+ hub.setUserOrg('hub-rick', ORG_FRESH, { name: 'Fresh Org', role: 'admin' });
144
+ await sso.ssoLogin(app, hub, rick); // … the login must NOT wait for its expiry
145
+ const body = await readJson(await me(rickCookie));
146
+ expect(body.workspaces.map((w: { name: string }) => w.name)).toContain('Fresh Org');
147
+ hub.removeUserOrg('hub-rick', ORG_FRESH); // keep the later pins focused
148
+ await expireTtl();
149
+ await me(rickCookie); // sweep it back out
150
+ });
151
+
152
+ describe('request-time reconciliation (the live gate’s read-through)', () => {
153
+ it('an org created at the hub appears at the next reload', async () => {
154
+ hub.setUserOrg('hub-rick', ORG_THREE, { name: 'Third Org', role: 'member' });
155
+ await expireTtl();
156
+ const body = await readJson(await me(rickCookie));
157
+ expect(body.workspaces.map((w: { name: string }) => w.name)).toContain('Third Org');
158
+ expect(await membershipRow(ORG_THREE, rick.email)).toEqual([
159
+ { role: 'member', origin: 'hub', is_active: true }
160
+ ]);
161
+ });
162
+
163
+ it('rename and role change at the hub propagate at the next reload', async () => {
164
+ hub.setUserOrg('hub-rick', ORG_THREE, { name: 'Third Org GmbH', role: 'admin' });
165
+ await expireTtl();
166
+ const body = await readJson(await me(rickCookie));
167
+ const third = body.workspaces.find((w: { name: string }) => w.name === 'Third Org GmbH');
168
+ expect(third).toMatchObject({ role: 'admin', hubOrigin: true });
169
+ expect(await membershipRow(ORG_THREE, rick.email)).toEqual([
170
+ { role: 'admin', origin: 'hub', is_active: true }
171
+ ]);
172
+ });
173
+
174
+ it('a hub-side removal deactivates the projection with exactly ONE audit row; the workspace dies', async () => {
175
+ hub.removeUserOrg('hub-rick', ORG_THREE);
176
+ await expireTtl();
177
+ const body = await readJson(await me(rickCookie));
178
+ expect(body.workspaces.map((w: { name: string }) => w.name)).not.toContain('Third Org GmbH');
179
+ expect(await membershipRow(ORG_THREE, rick.email)).toEqual([
180
+ { role: 'admin', origin: 'hub', is_active: false }
181
+ ]);
182
+
183
+ // Naming the swept workspace now fails closed (no membership resolves
184
+ // — the miss-hook retries one cached reconcile and still finds none).
185
+ const wsThree = await workspaceIdOf(ORG_THREE);
186
+ expect((await me(rickCookie, wsThree)).status).toBe(401);
187
+
188
+ // Exactly ONE audit row per flip — system actor, hub_reconcile reason.
189
+ await expireTtl();
190
+ await me(rickCookie); // an extra pass must NOT audit again (row already inactive)
191
+ const { rows: audit } = await app.db.pool.query(
192
+ `SELECT a.actor_via FROM audit_log a JOIN workspaces w ON w.id = a.workspace_id
193
+ WHERE w.central_account_id = $1 AND a.action = 'member.deactivate'
194
+ AND a.metadata->>'reason' = 'hub_reconcile'`,
195
+ [ORG_THREE]
196
+ );
197
+ expect(audit).toEqual([{ actor_via: 'system' }]);
198
+ });
199
+
200
+ it('an unknown-role entry keeps the membership ALIVE but is never projected/synced', async () => {
201
+ // A brand-new org with an unknown role: never projected.
202
+ hub.setUserOrg('hub-rick', ORG_WEIRD, { name: 'Weird Org', role: 'emperor' });
203
+ await expireTtl();
204
+ const body = await readJson(await me(rickCookie));
205
+ expect(body.workspaces.map((w: { name: string }) => w.name)).not.toContain('Weird Org');
206
+ const { rows } = await app.db.pool.query(`SELECT 1 FROM workspaces WHERE central_account_id = $1`, [
207
+ ORG_WEIRD
208
+ ]);
209
+ expect(rows).toHaveLength(0);
210
+
211
+ // An EXISTING org whose entry turns unknown-role: kept alive (never
212
+ // read as absence), local role kept (no sync from a malformed entry).
213
+ hub.setUserOrg('hub-rick', ORG_ONE, { name: 'Rick Org', role: 'emperor' });
214
+ await expireTtl();
215
+ await me(rickCookie);
216
+ expect(await membershipRow(ORG_ONE, rick.email)).toEqual([
217
+ { role: 'owner', origin: 'hub', is_active: true }
218
+ ]);
219
+ hub.setUserOrg('hub-rick', ORG_ONE, { name: 'Rick Org', role: 'owner' });
220
+ hub.removeUserOrg('hub-rick', ORG_WEIRD);
221
+ });
222
+ });
223
+
224
+ describe('failure posture: hub errors fail OPEN inside the stale window, retry-throttled', () => {
225
+ it('5xx: local state served, nothing deactivated, ONE probe per retry window', async () => {
226
+ await expireTtl();
227
+ await me(rickCookie); // fresh definitive pass first
228
+ hub.orgsMode = 'http500';
229
+ try {
230
+ await expireTtl();
231
+ const before = orgFetches();
232
+ const first = await readJson(await me(rickCookie));
233
+ expect(first.workspaces.length).toBeGreaterThanOrEqual(2); // ORG_ONE + ORG_TWO intact
234
+ expect(orgFetches()).toBe(before + 1);
235
+ // Within retryMs: no second probe, still 200 on local state.
236
+ expect((await me(rickCookie)).status).toBe(200);
237
+ expect((await me(rickCookie)).status).toBe(200);
238
+ expect(orgFetches()).toBe(before + 1);
239
+ } finally {
240
+ hub.orgsMode = 'ok';
241
+ }
242
+ expect(await membershipRow(ORG_TWO, rick.email)).toEqual([
243
+ { role: 'member', origin: 'hub', is_active: true }
244
+ ]);
245
+ });
246
+
247
+ it('timeout: the awaited pass is bounded by orgsTimeoutMs and fails open', async () => {
248
+ await sleep(DIALS.retryMs + 40);
249
+ await me(rickCookie);
250
+ hub.orgsDelayMs = DIALS.orgsTimeoutMs + 400;
251
+ try {
252
+ await expireTtl();
253
+ const startedAt = Date.now();
254
+ expect((await me(rickCookie)).status).toBe(200);
255
+ expect(Date.now() - startedAt).toBeLessThan(DIALS.orgsTimeoutMs + 600);
256
+ } finally {
257
+ hub.orgsDelayMs = 0;
258
+ }
259
+ expect(await membershipRow(ORG_TWO, rick.email)).toEqual([
260
+ { role: 'member', origin: 'hub', is_active: true }
261
+ ]);
262
+ });
263
+
264
+ it('recovers after the throttle window: the next reload reconciles again', async () => {
265
+ hub.setUserOrg('hub-rick', ORG_FRESH, { name: 'Fresh Org', role: 'member' });
266
+ await sleep(DIALS.retryMs + 40);
267
+ const body = await readJson(await me(rickCookie));
268
+ expect(body.workspaces.map((w: { name: string }) => w.name)).toContain('Fresh Org');
269
+ hub.removeUserOrg('hub-rick', ORG_FRESH);
270
+ await expireTtl();
271
+ await me(rickCookie);
272
+ });
273
+ });
274
+
275
+ describe('cost discipline: cache, single-flight — and machine credentials reconcile too', () => {
276
+ it('N concurrent reloads share ONE fetch; a fresh cache costs zero', async () => {
277
+ await expireTtl();
278
+ hub.orgsDelayMs = 100; // hold all racers in flight together
279
+ let before: number;
280
+ try {
281
+ before = orgFetches();
282
+ const responses = await Promise.all(Array.from({ length: 8 }, () => me(rickCookie)));
283
+ for (const res of responses) expect(res.status).toBe(200);
284
+ } finally {
285
+ hub.orgsDelayMs = 0;
286
+ }
287
+ expect(orgFetches()).toBe(before + 1); // single-flight
288
+ expect((await me(rickCookie)).status).toBe(200); // within the TTL
289
+ expect(orgFetches()).toBe(before + 1); // cache hit, no fetch
290
+ });
291
+
292
+ it('an API key into a projected workspace reconciles like a session (a key IS its user)', async () => {
293
+ const minted = await app.app.request('/api/v1/api-keys', {
294
+ method: 'POST',
295
+ headers: { 'content-type': 'application/json', cookie: rickCookie },
296
+ body: JSON.stringify({ name: 'reconcile key', scopes: [host.scopes.read] })
297
+ });
298
+ expect(minted.status).toBe(201);
299
+ const { key } = await readJson(minted);
300
+ await expireTtl();
301
+ const before = orgFetches();
302
+ const res = await app.app.request('/api/v1/me', { headers: { authorization: `Bearer ${key}` } });
303
+ expect(res.status).toBe(200);
304
+ const body = await readJson(res);
305
+ // The user-scoped model: /me lists ALL the holder's workspaces…
306
+ expect(body.workspaces.length).toBeGreaterThanOrEqual(2);
307
+ // …and the request ran a real as-the-user reconcile pass.
308
+ expect(orgFetches()).toBe(before + 1);
309
+ });
310
+ });
311
+ });
312
+
313
+ describe('the sweep’s blast radius: ONLY hub rows in projected workspaces', () => {
314
+ it('an empty definitive list deactivates hub projections and NOTHING else', async () => {
315
+ const tina: HubUserFixture = {
316
+ sub: 'hub-tina',
317
+ email: 'tina@reconcile.test',
318
+ workspaceId: ORG_TINA,
319
+ role: 'owner',
320
+ workspaceName: 'Tina Org'
321
+ };
322
+ const cookie = await sso.ssoLogin(app, hub, tina);
323
+ expect((await me(cookie)).status).toBe(200);
324
+ const { rows: users } = await app.db.pool.query(`SELECT id FROM "user" WHERE email = $1`, [tina.email]);
325
+ const tinaId = users[0].id as string;
326
+
327
+ // Three rows the sweep must NEVER touch: a local row in an unprojected
328
+ // workspace, a guest row, and a hub-origin RESIDUE row whose workspace
329
+ // is not a projection (centralAccountId NULL).
330
+ const mkWorkspace = async (name: string) => {
331
+ const { rows } = await app.db.pool.query(`INSERT INTO workspaces (name) VALUES ($1) RETURNING id`, [
332
+ name
333
+ ]);
334
+ return rows[0].id as string;
335
+ };
336
+ const localWs = await mkWorkspace('Tina Local');
337
+ const guestWs = await mkWorkspace('Tina Guest Host');
338
+ const residueWs = await mkWorkspace('Tina Residue');
339
+ await app.db.pool.query(
340
+ `INSERT INTO workspace_members (workspace_id, user_id, role, origin, is_active) VALUES
341
+ ($1, $4, 'member', 'local', true),
342
+ ($2, $4, 'member', 'guest', true),
343
+ ($3, $4, 'member', 'hub', true)`,
344
+ [localWs, guestWs, residueWs, tinaId]
345
+ );
346
+
347
+ // The hub now asserts NO orgs for tina.
348
+ hub.clearUserOrgs('hub-tina');
349
+ await expireTtl();
350
+ // The FIRST request into the projected workspace runs the sweep and is
351
+ // refused by the gate's own verdict — enforcement and freshness are one
352
+ // pass now.
353
+ const swept = await me(cookie);
354
+ expect(swept.status).toBe(401);
355
+ expect((await readJson(swept)).error.code).toBe('membership_revoked');
356
+
357
+ const { rows: after } = await app.db.pool.query(
358
+ `SELECT w.name, m.origin, m.is_active FROM workspace_members m
359
+ JOIN workspaces w ON w.id = m.workspace_id
360
+ WHERE m.user_id = $1 ORDER BY w.name`,
361
+ [tinaId]
362
+ );
363
+ expect(after).toEqual([
364
+ { name: 'Tina Guest Host', origin: 'guest', is_active: true },
365
+ { name: 'Tina Local', origin: 'local', is_active: true },
366
+ { name: 'Tina Org', origin: 'hub', is_active: false }, // the ONE swept row
367
+ { name: 'Tina Residue', origin: 'hub', is_active: true } // unprojected: out of scope
368
+ ]);
369
+ const { rows: audit } = await app.db.pool.query(
370
+ `SELECT a.workspace_id FROM audit_log a
371
+ WHERE a.action = 'member.deactivate' AND a.metadata->>'reason' = 'hub_reconcile'
372
+ AND a.resource_id = $1`,
373
+ [tinaId]
374
+ );
375
+ expect(audit).toHaveLength(1);
376
+
377
+ // Tina still reaches her LOCAL workspace: the session survives; only
378
+ // the hub-asserted membership died.
379
+ const local = await me(cookie, localWs);
380
+ expect(local.status).toBe(200);
381
+ });
382
+ });
383
+
384
+ describe('the unknown-workspace retry: a named miss reconciles once and re-resolves — all three credential kinds', () => {
385
+ const dana: HubUserFixture = {
386
+ sub: 'hub-dana',
387
+ email: 'dana@reconcile.test',
388
+ workspaceId: ORG_DANA,
389
+ role: 'owner',
390
+ workspaceName: 'Dana Org'
391
+ };
392
+ let danaCookie: string;
393
+ let wsTwo: string;
394
+
395
+ it('“invited at the hub, clicks a deep link” resolves without a re-login (session)', async () => {
396
+ danaCookie = await sso.ssoLogin(app, hub, dana);
397
+ // Granted membership of rick's ORG_TWO at the hub AFTER her login; its
398
+ // workspace already exists locally (rick's projection) — the deep-link
399
+ // URL names it, but dana holds no local membership row yet.
400
+ hub.setUserOrg('hub-dana', ORG_TWO, { name: 'Second Org', role: 'member' });
401
+ wsTwo = await workspaceIdOf(ORG_TWO);
402
+ await expireTtl(); // the retry rides the CACHED reconcile — let it expire
403
+ const res = await me(danaCookie, wsTwo);
404
+ expect(res.status).toBe(200);
405
+ const body = await readJson(res);
406
+ expect(body.activeWorkspaceId).toBe(wsTwo);
407
+ expect(body.role).toBe('member');
408
+ expect(await membershipRow(ORG_TWO, dana.email)).toEqual([
409
+ { role: 'member', origin: 'hub', is_active: true }
410
+ ]);
411
+ });
412
+
413
+ it('an API key naming a freshly granted org resolves through the same retry', async () => {
414
+ const minted = await app.app.request('/api/v1/api-keys', {
415
+ method: 'POST',
416
+ headers: { 'content-type': 'application/json', cookie: danaCookie },
417
+ body: JSON.stringify({ name: 'dana key', scopes: [host.scopes.read] })
418
+ });
419
+ expect(minted.status).toBe(201);
420
+ const { key } = await readJson(minted);
421
+ // Deactivate the row the session retry just projected — the KEY's own
422
+ // miss must re-project it.
423
+ await app.db.pool.query(
424
+ `UPDATE workspace_members SET is_active = false
425
+ WHERE workspace_id = $1 AND user_id = (SELECT id FROM "user" WHERE email = $2)`,
426
+ [wsTwo, dana.email]
427
+ );
428
+ await expireTtl();
429
+ const res = await app.app.request('/api/v1/me', {
430
+ headers: { authorization: `Bearer ${key}`, 'x-workspace-id': wsTwo }
431
+ });
432
+ expect(res.status).toBe(200);
433
+ expect((await readJson(res)).activeWorkspaceId).toBe(wsTwo);
434
+ });
435
+
436
+ it('an OAuth bearer naming a freshly granted org resolves through the same retry', async () => {
437
+ const bearer = await sso.oauthBearer(app, danaCookie);
438
+ await app.db.pool.query(
439
+ `UPDATE workspace_members SET is_active = false
440
+ WHERE workspace_id = $1 AND user_id = (SELECT id FROM "user" WHERE email = $2)`,
441
+ [wsTwo, dana.email]
442
+ );
443
+ await expireTtl();
444
+ const res = await app.app.request('/api/v1/me', {
445
+ headers: { authorization: `Bearer ${bearer}`, 'x-workspace-id': wsTwo }
446
+ });
447
+ expect(res.status).toBe(200);
448
+ expect((await readJson(res)).activeWorkspaceId).toBe(wsTwo);
449
+ });
450
+
451
+ it('a workspace the hub does not grant stays a fail-closed 401 (one retry, no oracle)', async () => {
452
+ await expireTtl();
453
+ // A real workspace she has no grant for, and a phantom id: same answer.
454
+ const solo = await me(danaCookie, await workspaceIdOf(ORG_ONE));
455
+ expect(solo.status).toBe(401);
456
+ const phantom = await me(danaCookie, '99999999-9999-4999-8999-999999999999');
457
+ expect(phantom.status).toBe(401);
458
+ });
459
+ });
460
+
461
+ describe('observability', () => {
462
+ it('hub_reconcile_passes_total and hub_grant_refreshes_total are registered and counting', async () => {
463
+ const res = await app.app.request('/metrics', {
464
+ headers: { authorization: 'Bearer reconcile-metrics-token' }
465
+ });
466
+ expect(res.status).toBe(200);
467
+ const text = await res.text();
468
+ expect(text).toMatch(/hub_reconcile_passes_total\{outcome="ok"\} [1-9]/);
469
+ expect(text).toMatch(/hub_reconcile_passes_total\{outcome="inconclusive"\} [1-9]/);
470
+ expect(text).toMatch(/hub_grant_refreshes_total/);
471
+ });
472
+ });
473
+
474
+ describe('break-glass claim on a PROJECTED workspace: the origin=local lifeboat (pin)', () => {
475
+ // Decision 2 (user-scoped federation): cloud setup mints no workspace, so
476
+ // operator recovery targets PROJECTED workspaces — break-glass
477
+ // claim-ownership writes an `origin='local'` owner row there, and the
478
+ // reconcile sweep is scoped to `origin='hub'` rows ONLY. This pin proves
479
+ // the whole lifeboat: the zero-membership operator claims a projection,
480
+ // gains dashboard access, and KEEPS it through the maximal sweep — a
481
+ // reconcile pass whose hub org list is EMPTY.
482
+ let operatorCookie: string;
483
+ let wsBg: string;
484
+
485
+ it('a zero-membership operator claims ownership of a projected workspace', async () => {
486
+ // A hub user projects ORG_BG first (the workspace break-glass targets).
487
+ await sso.ssoLogin(app, hub, {
488
+ sub: 'hub-bg-owner',
489
+ email: 'bg-owner@reconcile.test',
490
+ workspaceId: ORG_BG,
491
+ role: 'owner',
492
+ workspaceName: 'BG Org'
493
+ });
494
+ wsBg = await workspaceIdOf(ORG_BG);
495
+
496
+ // The operator: setup ran in beforeAll — a verified user with ZERO
497
+ // memberships (cloud setup creates no workspace). The break-glass door
498
+ // is the wired-but-hidden password sign-in.
499
+ const signIn = await app.app.request(
500
+ '/api/v1/auth/sign-in/email',
501
+ sso.json({ email: OWNER.email, password: OWNER.password })
502
+ );
503
+ expect(signIn.status).toBe(200);
504
+ operatorCookie = extractCookie(signIn);
505
+ const zero = await readJson(await me(operatorCookie));
506
+ expect(zero.workspaces).toEqual([]);
507
+
508
+ const claim = await app.app.request('/api/v1/admin/break-glass/claim-ownership', {
509
+ ...sso.json({ workspaceId: wsBg }),
510
+ headers: { 'content-type': 'application/json', cookie: operatorCookie, 'x-forwarded-for': '10.88.0.1' }
511
+ });
512
+ expect(claim.status).toBe(200);
513
+ const claimed = await readJson(claim);
514
+ expect(claimed.role).toBe('owner');
515
+ expect(claimed.created).toBe(true);
516
+
517
+ // Dashboard access: the projected workspace resolves for the operator,
518
+ // via the local row (the live gate skips hub enforcement for it).
519
+ const meRes = await me(operatorCookie, wsBg);
520
+ expect(meRes.status).toBe(200);
521
+ const body = await readJson(meRes);
522
+ expect(body.activeWorkspaceId).toBe(wsBg);
523
+ expect(body.origin).toBe('local');
524
+ expect(body.role).toBe('owner');
525
+
526
+ // The row break-glass wrote is origin='local' — the sweep's blind spot,
527
+ // by design.
528
+ const { rows } = await app.db.pool.query(
529
+ `SELECT m.origin, m.role, m.is_active FROM workspace_members m
530
+ JOIN "user" u ON u.id = m.user_id
531
+ WHERE m.workspace_id = $1 AND u.email = $2`,
532
+ [wsBg, OWNER.email]
533
+ );
534
+ expect(rows).toEqual([{ origin: 'local', role: 'owner', is_active: true }]);
535
+ });
536
+
537
+ it('the claim survives an EMPTY-hub-list reconcile (the maximal sweep)', async () => {
538
+ // Give the operator a hub identity + one hub org of their own: the D9
539
+ // trusted link rides the verified operator email.
540
+ const operatorSsoCookie = await sso.ssoLogin(app, hub, {
541
+ sub: 'hub-operator-bg',
542
+ email: OWNER.email,
543
+ workspaceId: ORG_OPX,
544
+ role: 'owner',
545
+ workspaceName: 'Operator Own Org'
546
+ });
547
+ const wsOpx = await workspaceIdOf(ORG_OPX);
548
+ expect((await me(operatorSsoCookie, wsOpx)).status).toBe(200);
549
+
550
+ // The hub now asserts NOTHING for the operator: the next reconcile pass
551
+ // runs with an EMPTY org list — the maximal deactivation sweep.
552
+ hub.clearUserOrgs('hub-operator-bg');
553
+ await expireTtl();
554
+ const swept = await me(operatorSsoCookie, wsOpx);
555
+ expect(swept.status).toBe(401); // the hub-origin row went with the sweep
556
+
557
+ // …but the break-glass claim row is origin='local': untouched. The
558
+ // operator keeps full dashboard access to the projected workspace.
559
+ const kept = await me(operatorSsoCookie, wsBg);
560
+ expect(kept.status).toBe(200);
561
+ const body = await readJson(kept);
562
+ expect(body.activeWorkspaceId).toBe(wsBg);
563
+ expect(body.origin).toBe('local');
564
+ expect(body.role).toBe('owner');
565
+ const { rows } = await app.db.pool.query(
566
+ `SELECT m.is_active FROM workspace_members m
567
+ JOIN "user" u ON u.id = m.user_id
568
+ WHERE m.workspace_id = $1 AND u.email = $2`,
569
+ [wsBg, OWNER.email]
570
+ );
571
+ expect(rows).toEqual([{ is_active: true }]);
572
+ });
573
+ });
574
+
575
+ describe('a severed hub link fails CLOSED for a hub-origin principal (CLOUD-1, PRDCT-1356)', () => {
576
+ const ORG_SEV = '33333333-aaaa-4bbb-8ccc-00000000000d';
577
+ const sev: HubUserFixture = {
578
+ sub: 'hub-sev',
579
+ email: 'sev@reconcile.test',
580
+ name: 'Sev Ered',
581
+ workspaceId: ORG_SEV,
582
+ role: 'member',
583
+ workspaceName: 'Sev Org'
584
+ };
585
+ const SEV_ENV = {
586
+ EDITION: 'cloud',
587
+ HUB_CLIENT_ID: host.hubClientId,
588
+ HUB_CLIENT_SECRET: 'integration-test-hub-secret-0001'
589
+ };
590
+
591
+ it('no account row + hub-origin memberships → 401 hub_grant_expired, never the fail-open no_link', async () => {
592
+ // Own database + own boots: the grant store caches the hub access token
593
+ // in PROCESS memory, so the severed row is only observed by a fresh
594
+ // process — which is the honest shape anyway (a replica restart, the
595
+ // next deploy).
596
+ const url = await createDatabase(container, 'hub_severed');
597
+ const first = await createTestApp(url, { ...SEV_ENV, HUB_ISSUER_URL: hub.issuer }, { hubDials: DIALS });
598
+ let cookie: string;
599
+ let userId: string;
600
+ try {
601
+ const setup = await first.app.request(
602
+ '/api/v1/setup',
603
+ sso.json({ setupToken: 'integration-test-setup-token', instanceName: 'Severed', owner: OWNER })
604
+ );
605
+ expect(setup.status).toBe(201);
606
+ cookie = await sso.ssoLogin(first, hub, sev);
607
+ const ws = await first.db.pool.query(`SELECT id FROM workspaces WHERE central_account_id = $1`, [
608
+ ORG_SEV
609
+ ]);
610
+ expect(ws.rows).toHaveLength(1);
611
+ const ok = await first.app.request('/api/v1/me', {
612
+ headers: { cookie, 'x-workspace-id': ws.rows[0].id }
613
+ });
614
+ expect(ok.status).toBe(200);
615
+ const u = await first.db.pool.query(`SELECT id FROM "user" WHERE email = $1`, [sev.email]);
616
+ userId = u.rows[0].id as string;
617
+ // Sever the link the way an unlink (now refused) or a partial cleanup
618
+ // would: the account row goes, the hub-origin membership stays.
619
+ await first.db.pool.query(`DELETE FROM account WHERE user_id = $1 AND provider_id = 'antasphere'`, [
620
+ userId
621
+ ]);
622
+ } finally {
623
+ await first.stop();
624
+ }
625
+
626
+ const second = await createTestApp(url, { ...SEV_ENV, HUB_ISSUER_URL: hub.issuer }, { hubDials: DIALS });
627
+ try {
628
+ const ws = await second.db.pool.query(`SELECT id FROM workspaces WHERE central_account_id = $1`, [
629
+ ORG_SEV
630
+ ]);
631
+ const res = await second.app.request('/api/v1/me', {
632
+ headers: { cookie, 'x-workspace-id': ws.rows[0].id }
633
+ });
634
+ expect(res.status).toBe(401);
635
+ expect((await readJson(res)).error.code).toBe('hub_grant_expired');
636
+ // The membership row itself was NOT swept (no org truth was read).
637
+ const rows = await second.db.pool.query(
638
+ `SELECT origin, is_active FROM workspace_members WHERE user_id = $1`,
639
+ [userId]
640
+ );
641
+ expect(rows.rows).toEqual([{ origin: 'hub', is_active: true }]);
642
+ } finally {
643
+ await second.stop();
644
+ }
645
+ });
646
+ });