@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,572 @@
1
+ import { Counter } from 'prom-client';
2
+ import { and, eq, inArray, isNotNull, ne, notInArray, sql } from 'drizzle-orm';
3
+ import { workspaceMembers, workspaces, type Db } from '@antasphere/chassis-db';
4
+ import type { AuditService } from '../audit/service.js';
5
+ import type { Logger } from '../logger.js';
6
+ import { deleteMembershipGrants, type MembershipRemovalHook } from '../members/removal.js';
7
+ import { projectOrgMembership, projectOrgTeams, projectTeamSeats } from './hub-projection.js';
8
+ import type { HubDeniedOrg, HubUserClient, LoginAccessToken } from './hub-user-client.js';
9
+
10
+ /**
11
+ * Live org reconciliation (internal/federation.md, user-scoped federation): org
12
+ * and membership truth live ONLY at the hub, and the tool reflects it by
13
+ * reading the hub's caller-scoped `GET /orgs` AS THE USER (hub-user-client)
14
+ * — every SSO login runs the TTL-bypassing `forceReconcile()` (fail-CLOSED:
15
+ * a login whose pass fails is revoked — reconcile IS the projection), and
16
+ * the live gate (hub-live-gate.ts) runs the cached `reconcile()` on every
17
+ * request into a projected workspace. One pass fetches the user's full hub
18
+ * org list and makes the local projections match: project missing orgs,
19
+ * sync names/roles, sync `workspaces.hub_status` and the user's
20
+ * `workspace_members.is_default`, deactivate `origin='hub'` memberships the
21
+ * hub no longer asserts.
22
+ *
23
+ * Failure posture: a pass mutates state ONLY on a definitive 200 list.
24
+ * Transient hub errors keep local state serving (nothing deactivated) —
25
+ * the ENFORCEMENT of prolonged staleness is the gate's, via the windowed
26
+ * `reconcile()` verdict (fresh ≤ staleMaxMs → serve; beyond it, failing →
27
+ * `hub_unavailable`). A definitive `grant_dead` is its own verdict — the
28
+ * gate answers 401 `hub_grant_expired` immediately, no stale window.
29
+ *
30
+ * Cost discipline: per-user TTL cache (~10 s) + single-flight, so N
31
+ * concurrent requests cost one hub fetch; a pass on an expired cache IS
32
+ * awaited (freshness at request time is the whole point) but is bounded by
33
+ * the client's tight fetch timeout; failures re-probe at most once per
34
+ * `retryMs`. An oss boot never constructs this class.
35
+ */
36
+
37
+ /** Every dial of the live federation loop — one override surface for tests. */
38
+ export interface HubFederationDials {
39
+ /** Per-user reconcile TTL (the identity-path freshness bound). */
40
+ reconcileTtlMs: number;
41
+ /** Definitive-pass staleness bound; beyond it a failing hub gates fail-closed. */
42
+ reconcileStaleMaxMs: number;
43
+ /** Failed-pass re-probe throttle: at most one hub attempt per user per interval. */
44
+ retryMs: number;
45
+ /** GET /orgs fetch timeout (awaited in the identity path — keep tight). */
46
+ orgsTimeoutMs: number;
47
+ /** Token-endpoint fetch timeout (hub-grant.ts). */
48
+ tokenTimeoutMs: number;
49
+ /** Serve cached access tokens only to exp − this (hub-grant.ts). */
50
+ accessSkewMs: number;
51
+ /** Watchdog cutting a wedged refresh's dedicated lock connection (hub-grant.ts). */
52
+ lockWatchdogMs: number;
53
+ /**
54
+ * How old an organization's whole team list (`GET /teams`, PRDCT-2813) may
55
+ * grow before a pass reads it again, per org per replica. A failed read is
56
+ * re-tried at most once per `retryMs`.
57
+ */
58
+ orgTeamsTtlMs: number;
59
+ }
60
+
61
+ export const DEFAULT_FEDERATION_DIALS: HubFederationDials = {
62
+ reconcileTtlMs: 10_000,
63
+ reconcileStaleMaxMs: 15 * 60_000,
64
+ retryMs: 15_000,
65
+ orgsTimeoutMs: 1_500,
66
+ tokenTimeoutMs: 5_000,
67
+ accessSkewMs: 60_000,
68
+ // Must cover probe + presentation under the refresh lock (hub-grant.ts
69
+ // clamps it up if not): 5 s + 5 s + 2 s headroom.
70
+ lockWatchdogMs: 12_000,
71
+ orgTeamsTtlMs: 5 * 60_000
72
+ };
73
+
74
+ /**
75
+ * One pass's own outcome (what the metric labels count):
76
+ * - 'ok': definitive list applied;
77
+ * - 'no_link': the user holds no hub identity — nothing to reconcile,
78
+ * cached like a success;
79
+ * - 'grant_dead': the user's hub grant is definitively dead (cached like a
80
+ * success — the answer is real; a browser login heals it);
81
+ * - 'inconclusive': transient hub error, fail open;
82
+ * - 'error': unexpected local failure, fail open.
83
+ */
84
+ export type ReconcilePassOutcome = 'ok' | 'no_link' | 'grant_dead' | 'inconclusive' | 'error';
85
+
86
+ /**
87
+ * The gate's windowed verdict:
88
+ * - 'ok': definitive data within the stale window — serve on local rows;
89
+ * - 'grant_dead': the grant is dead — the gate refuses immediately;
90
+ * - 'unavailable': no definitive pass within the window and the hub keeps
91
+ * failing — the gate fails closed.
92
+ */
93
+ export type ReconcileVerdict = { kind: 'ok' } | { kind: 'grant_dead' } | { kind: 'unavailable' };
94
+
95
+ interface CacheEntry {
96
+ /** Timestamp of the last DEFINITIVE pass (the TTL + stale window anchor here). */
97
+ fetchedAt: number;
98
+ /** Timestamp of the last attempt, success or failure (outage throttle). */
99
+ lastAttemptAt: number;
100
+ /** The last DEFINITIVE outcome ('ok' | 'no_link' | 'grant_dead'). */
101
+ lastDefinitive: Extract<ReconcilePassOutcome, 'ok' | 'no_link' | 'grant_dead'> | null;
102
+ /**
103
+ * The organizations the hub's last definitive list named as NOT opening
104
+ * this tool to the person (`denied`). A transient hint for the refusal
105
+ * page, never an access input: the hub is the truth, and nothing here is
106
+ * stored. An inconclusive pass keeps the previous list; `no_link` and
107
+ * `grant_dead` clear it.
108
+ */
109
+ denied: HubDeniedOrg[];
110
+ }
111
+
112
+ /** When an organization's whole team list was last read, per replica. */
113
+ interface OrgTeamsEntry {
114
+ /** The last successful read (0 = never). */
115
+ refreshedAt: number;
116
+ /** The last failed read (0 = none): the retry throttle and the once-per-window log. */
117
+ lastAttemptAt: number;
118
+ }
119
+
120
+ /** What one team-list refresh ended on (the metric's `outcome` label). */
121
+ type OrgTeamsRefreshOutcome = 'ok' | 'inconclusive' | 'grant_dead' | 'no_link' | 'error';
122
+
123
+ /** Bound the cache — a user-id flood must never balloon memory. */
124
+ const MAX_CACHE_ENTRIES = 10_000;
125
+
126
+ export interface HubOrgReconcilerDeps {
127
+ db: Db;
128
+ client: HubUserClient;
129
+ audit: AuditService;
130
+ logger: Logger;
131
+ dials: HubFederationDials;
132
+ /** The tool's half of a removal (the `membershipRemoval` slot), run in the sweep's transaction. */
133
+ onMembershipRemoval?: MembershipRemovalHook | undefined;
134
+ /** Test seam. */
135
+ now?: () => number;
136
+ }
137
+
138
+ export class HubOrgReconciler {
139
+ /** local userId → pass freshness. */
140
+ private readonly cache = new Map<string, CacheEntry>();
141
+ private readonly inFlight = new Map<string, Promise<ReconcilePassOutcome>>();
142
+ /** hub org id → its whole team list's freshness (bounded like the user cache). */
143
+ private readonly orgTeams = new Map<string, OrgTeamsEntry>();
144
+ private readonly now: () => number;
145
+ readonly dials: HubFederationDials;
146
+
147
+ /** Registered into the Prometheus registry by boot (cloud only). */
148
+ readonly promMetrics: Counter[];
149
+ private readonly passes: Counter;
150
+ private readonly orgTeamsRefreshes: Counter;
151
+
152
+ constructor(private readonly deps: HubOrgReconcilerDeps) {
153
+ this.now = deps.now ?? Date.now;
154
+ this.dials = deps.dials;
155
+ // registers: [] — boot attaches this to the app registry on cloud; an
156
+ // oss boot never constructs this class, so the metric never exists there.
157
+ this.passes = new Counter({
158
+ name: 'hub_reconcile_passes_total',
159
+ help: 'Hub org-list reconciliation passes by outcome',
160
+ labelNames: ['outcome'] as const,
161
+ registers: []
162
+ });
163
+ this.orgTeamsRefreshes = new Counter({
164
+ name: 'hub_org_teams_refresh_total',
165
+ help: 'Reads of a hub organization’s whole team list by outcome',
166
+ labelNames: ['outcome'] as const,
167
+ registers: []
168
+ });
169
+ this.promMetrics = [this.passes, this.orgTeamsRefreshes];
170
+ }
171
+
172
+ /**
173
+ * The identity-path read-through: cached per user (~10 s), single-flight,
174
+ * retry-throttled after failures. Awaits one pass on expiry — the request
175
+ * that triggered it observes the reconciled state — and NEVER throws.
176
+ * Returns the windowed verdict the live gate enforces.
177
+ */
178
+ async reconcile(localUserId: string): Promise<ReconcileVerdict> {
179
+ const now = this.now();
180
+ const entry = this.cache.get(localUserId);
181
+ if (!entry || now - entry.fetchedAt >= this.dials.reconcileTtlMs) {
182
+ let flight = this.inFlight.get(localUserId);
183
+ if (!flight) {
184
+ // Outage throttle: after a FAILED pass (lastAttemptAt advanced past
185
+ // the last success), at most one re-probe per retryMs — a down hub
186
+ // costs each user one bounded timeout per window. A successful pass
187
+ // is never throttled beyond its own TTL.
188
+ const throttled =
189
+ entry !== undefined &&
190
+ entry.lastAttemptAt > entry.fetchedAt &&
191
+ now - entry.lastAttemptAt < this.dials.retryMs;
192
+ if (!throttled) {
193
+ flight = this.startPass(localUserId);
194
+ }
195
+ }
196
+ if (flight) await flight;
197
+ }
198
+ return this.verdict(localUserId);
199
+ }
200
+
201
+ /**
202
+ * The login-time pass (assertLogin step 3): TTL and throttle deliberately
203
+ * bypassed — a login is the freshest signal there is, and `login` carries
204
+ * the callback access token so no refresh is needed. Returns the PASS'S
205
+ * OWN outcome (not the windowed verdict): the fail-CLOSED login requires
206
+ * this very pass to have succeeded, never a recent cached one.
207
+ */
208
+ async forceReconcile(localUserId: string, login?: LoginAccessToken): Promise<ReconcilePassOutcome> {
209
+ const flight = this.runPass(localUserId, login).finally(() => {
210
+ if (this.inFlight.get(localUserId) === flight) this.inFlight.delete(localUserId);
211
+ });
212
+ // Concurrent cached reconciles join this pass instead of doubling it.
213
+ this.inFlight.set(localUserId, flight);
214
+ return flight;
215
+ }
216
+
217
+ private startPass(localUserId: string): Promise<ReconcilePassOutcome> {
218
+ const flight = this.runPass(localUserId, undefined).finally(() => {
219
+ if (this.inFlight.get(localUserId) === flight) this.inFlight.delete(localUserId);
220
+ });
221
+ this.inFlight.set(localUserId, flight);
222
+ return flight;
223
+ }
224
+
225
+ private verdict(localUserId: string): ReconcileVerdict {
226
+ const entry = this.cache.get(localUserId);
227
+ const now = this.now();
228
+ if (entry && entry.lastDefinitive && now - entry.fetchedAt < this.dials.reconcileStaleMaxMs) {
229
+ // Definitive data within the window. A definitive DEAD grant refuses
230
+ // immediately (no stale grace — Romain's decision 3); 'ok'/'no_link'
231
+ // serve on local rows.
232
+ return entry.lastDefinitive === 'grant_dead' ? { kind: 'grant_dead' } : { kind: 'ok' };
233
+ }
234
+ // No definitive pass within the window (or ever, e.g. a replica that
235
+ // restarted mid-outage) and the hub keeps failing: fail closed.
236
+ return { kind: 'unavailable' };
237
+ }
238
+
239
+ private async runPass(localUserId: string, login?: LoginAccessToken): Promise<ReconcilePassOutcome> {
240
+ try {
241
+ const result = await this.deps.client.orgs(localUserId, login);
242
+ if (result.kind === 'no_link') {
243
+ // A purely local user: the hub asserts nothing about them, and that
244
+ // IS the definitive answer. Nothing is swept.
245
+ //
246
+ // EXCEPT (CLOUD-1, PRDCT-1356) when the user HOLDS origin='hub'
247
+ // membership rows: those rows exist only because a hub link once
248
+ // projected them, so "no link" means the link was SEVERED (an
249
+ // unlinked account row, a partial cleanup) — and treating that as
250
+ // the fail-open definitive answer, cached like a success, was the
251
+ // one way to switch cloud enforcement off permanently. It fails
252
+ // CLOSED instead, as a dead grant: the gate answers 401
253
+ // hub_grant_expired, and a browser SSO re-login (which re-links)
254
+ // heals it. Local/guest rows are untouched — they never reconcile.
255
+ const [hubRow] = await this.deps.db
256
+ .select({ id: workspaceMembers.id })
257
+ .from(workspaceMembers)
258
+ .where(and(eq(workspaceMembers.userId, localUserId), eq(workspaceMembers.origin, 'hub')))
259
+ .limit(1);
260
+ if (hubRow) {
261
+ this.deps.logger.warn(
262
+ { userId: localUserId },
263
+ 'hub org reconcile: no hub link but hub-origin memberships exist — link severed; failing CLOSED'
264
+ );
265
+ this.remember(localUserId, 'grant_dead');
266
+ this.passes.inc({ outcome: 'link_severed' });
267
+ return 'grant_dead';
268
+ }
269
+ this.remember(localUserId, 'no_link');
270
+ this.passes.inc({ outcome: 'no_link' });
271
+ return 'no_link';
272
+ }
273
+ if (result.kind === 'grant_dead') {
274
+ // Definitive: the grant family is gone. Freshness-wise a real
275
+ // answer (cached); enforcement is the gate's 401 hub_grant_expired.
276
+ // Nothing is swept — org truth was not read, only the grant state.
277
+ this.remember(localUserId, 'grant_dead');
278
+ this.passes.inc({ outcome: 'grant_dead' });
279
+ return 'grant_dead';
280
+ }
281
+ if (result.kind === 'inconclusive') {
282
+ // Loudly logged by the client already; retry at the next window.
283
+ this.remember(localUserId, null);
284
+ this.passes.inc({ outcome: 'inconclusive' });
285
+ return 'inconclusive';
286
+ }
287
+
288
+ // Definitive 200 list — the ONLY thing that may ever mutate state.
289
+ // Every listed org is kept, suspended ones included (visible but
290
+ // blocked: the synced hub_status is what refuses their requests);
291
+ // role-null entries are kept but not projected/synced.
292
+ const keep = new Set(result.orgs.map((o) => o.id));
293
+
294
+ // Deactivation sweep FIRST (removal must win over projection within
295
+ // one pass): ONE conditional UPDATE covering exactly the rows the hub
296
+ // no longer asserts — origin='hub' AND active AND the workspace IS a
297
+ // projection whose hub org is off the keep-set. local/guest rows and
298
+ // rows in non-projected workspaces are structurally out of scope.
299
+ // `is_active = true` in the WHERE makes the flip observable: only the
300
+ // pass that actually flipped a row audits it — one logical removal
301
+ // writes ONE member.deactivate row.
302
+ const keepIds = [...keep];
303
+ const sweptWorkspaces = this.deps.db
304
+ .select({ id: workspaces.id })
305
+ .from(workspaces)
306
+ .where(
307
+ keepIds.length > 0
308
+ ? and(isNotNull(workspaces.centralAccountId), notInArray(workspaces.centralAccountId, keepIds))
309
+ : isNotNull(workspaces.centralAccountId)
310
+ );
311
+ // The project grants that rode on a swept membership go WITH it, in the
312
+ // same transaction. The sweep deactivates the row and never deletes it
313
+ // (a re-add at the hub reactivates the very same row), so the foreign
314
+ // key's cascade never fires here: without this delete, a person removed
315
+ // at the hub and added back later would find every old project grant
316
+ // waiting. A removal is a removal: a re-add starts with none. (A LOCAL
317
+ // deactivation by an admin is a pause, not a removal, and keeps them.)
318
+ const deactivated = await this.deps.db.transaction(async (tx) => {
319
+ const rows = await tx
320
+ .update(workspaceMembers)
321
+ .set({ isActive: false })
322
+ .where(
323
+ and(
324
+ eq(workspaceMembers.userId, localUserId),
325
+ eq(workspaceMembers.origin, 'hub'),
326
+ eq(workspaceMembers.isActive, true),
327
+ inArray(workspaceMembers.workspaceId, sweptWorkspaces)
328
+ )
329
+ )
330
+ .returning({ id: workspaceMembers.id, workspaceId: workspaceMembers.workspaceId });
331
+ // The grants and the seats go with the membership: the one statement
332
+ // of what a removal takes, shared with the tool's own removal route.
333
+ await deleteMembershipGrants(
334
+ tx,
335
+ rows.map((row) => ({ memberId: row.id, workspaceId: row.workspaceId, userId: localUserId })),
336
+ this.deps.onMembershipRemoval
337
+ );
338
+ return rows;
339
+ });
340
+ for (const row of deactivated) {
341
+ await this.deps.audit.write({
342
+ workspaceId: row.workspaceId,
343
+ principal: null,
344
+ action: 'member.deactivate',
345
+ resourceType: 'member',
346
+ resourceId: localUserId,
347
+ metadata: { reason: 'hub_reconcile' }
348
+ });
349
+ this.deps.logger.info(
350
+ { userId: localUserId, workspaceId: row.workspaceId },
351
+ 'hub reconcile: membership no longer asserted by the hub — local membership deactivated'
352
+ );
353
+ }
354
+
355
+ // Then project/upsert every well-formed entry (role upserts are
356
+ // deliberately NOT audited — the login-projection precedent), the
357
+ // person's team seats in it, and sync the org-level hub_status. One
358
+ // failing entry logs and moves on: a single bad org must never starve
359
+ // the others.
360
+ for (const org of result.orgs) {
361
+ try {
362
+ if (org.role !== null) {
363
+ const projected = await projectOrgMembership(this.deps.db, {
364
+ localUserId,
365
+ hubWorkspaceId: org.id,
366
+ hubWorkspaceName: org.name,
367
+ role: org.role
368
+ });
369
+ // The person's own seats in this org's teams: one transaction
370
+ // per org, so a seat set is never half-applied.
371
+ await this.deps.db.transaction(async (tx) => {
372
+ await projectTeamSeats(tx, {
373
+ workspaceId: projected.workspaceId,
374
+ memberId: projected.memberId,
375
+ teams: org.teams
376
+ });
377
+ });
378
+ // Then the org's whole team list, when it is due (never fails the pass).
379
+ await this.refreshOrgTeams(localUserId, org.id, projected.workspaceId, login !== undefined);
380
+ }
381
+ // Org-level suspension is LOCALLY MATERIALIZED truth: the gate
382
+ // (and guests' requests — the accepted staleness bound) read this
383
+ // column, never a hub round-trip of their own. A role-null entry
384
+ // still syncs it when the projection already exists.
385
+ await this.deps.db
386
+ .update(workspaces)
387
+ .set({ hubStatus: org.status })
388
+ .where(and(eq(workspaces.centralAccountId, org.id), ne(workspaces.hubStatus, org.status)));
389
+ } catch (err) {
390
+ this.deps.logger.error(
391
+ { err, userId: localUserId, hubOrgId: org.id },
392
+ 'hub reconcile: projecting one org failed — continuing with the rest'
393
+ );
394
+ }
395
+ }
396
+
397
+ // Materialize the hub-level default org on the user's membership rows
398
+ // (selection happens inside the resolvers, before any gate — the flag
399
+ // must live locally). Clear-then-set in ONE transaction: the partial
400
+ // unique index (user_id WHERE is_default) forbids two trues even
401
+ // transiently, so a single both-ways UPDATE could trip it on row
402
+ // order. A cross-replica race still can — the violation aborts only
403
+ // this step and self-heals next pass.
404
+ try {
405
+ const defaultOrgId = result.orgs.find((o) => o.isDefault)?.id ?? null;
406
+ let defaultWorkspaceId: string | null = null;
407
+ if (defaultOrgId) {
408
+ const [ws] = await this.deps.db
409
+ .select({ id: workspaces.id })
410
+ .from(workspaces)
411
+ .where(eq(workspaces.centralAccountId, defaultOrgId))
412
+ .limit(1);
413
+ defaultWorkspaceId = ws?.id ?? null;
414
+ }
415
+ await this.deps.db.transaction(async (tx) => {
416
+ await tx
417
+ .update(workspaceMembers)
418
+ .set({ isDefault: false })
419
+ .where(
420
+ and(
421
+ eq(workspaceMembers.userId, localUserId),
422
+ eq(workspaceMembers.isDefault, true),
423
+ ...(defaultWorkspaceId ? [ne(workspaceMembers.workspaceId, defaultWorkspaceId)] : [])
424
+ )
425
+ );
426
+ if (defaultWorkspaceId) {
427
+ await tx
428
+ .update(workspaceMembers)
429
+ .set({ isDefault: true })
430
+ .where(
431
+ and(
432
+ eq(workspaceMembers.userId, localUserId),
433
+ eq(workspaceMembers.workspaceId, defaultWorkspaceId),
434
+ eq(workspaceMembers.isDefault, false)
435
+ )
436
+ );
437
+ }
438
+ });
439
+ } catch (err) {
440
+ this.deps.logger.warn(
441
+ { err, userId: localUserId },
442
+ 'hub reconcile: default-org sync failed — selection falls back to oldest-active until the next pass'
443
+ );
444
+ }
445
+
446
+ this.remember(localUserId, 'ok', result.denied);
447
+ this.passes.inc({ outcome: 'ok' });
448
+ return 'ok';
449
+ } catch (err) {
450
+ // Fail open, always: reconciliation is freshness; enforcement lives
451
+ // in the gate's windowed verdict.
452
+ this.remember(localUserId, null);
453
+ this.passes.inc({ outcome: 'error' });
454
+ this.deps.logger.error(
455
+ { err, userId: localUserId },
456
+ 'hub org reconcile pass failed — serving local state'
457
+ );
458
+ return 'error';
459
+ }
460
+ }
461
+
462
+ /**
463
+ * Read the organization's WHOLE team list as this person and project it
464
+ * (PRDCT-2813), when the last read is older than `orgTeamsTtlMs` (or never
465
+ * happened) and no failed read happened within `retryMs`. The LOGIN pass
466
+ * reads it whatever the window says (`force`): a login is the freshest
467
+ * signal there is, and Romain's rule is "at login, then every five minutes". A refinement
468
+ * of a pass whose org list already succeeded: it never throws and never
469
+ * fails the pass; a failed read keeps the previous list and is logged once
470
+ * per throttle window.
471
+ *
472
+ * No lock: two replicas (or two passes) may both refresh the same org. The
473
+ * upsert is idempotent and the delete is by membership of the list each
474
+ * one read, so the last writer leaves the hub's list, whichever it is.
475
+ */
476
+ private async refreshOrgTeams(
477
+ localUserId: string,
478
+ hubOrgId: string,
479
+ workspaceId: string,
480
+ /** A login: the freshest signal there is, read whatever the window says (the login pass's own rule). */
481
+ force = false
482
+ ): Promise<void> {
483
+ const now = this.now();
484
+ const entry = this.orgTeams.get(hubOrgId);
485
+ if (entry) {
486
+ // A login skips the window, never the failure throttle: a hub whose
487
+ // team list hangs must not cost every sign-in one timeout per org
488
+ // (verifier round 2).
489
+ if (!force && entry.refreshedAt > 0 && now - entry.refreshedAt < this.dials.orgTeamsTtlMs) return;
490
+ if (entry.lastAttemptAt > entry.refreshedAt && now - entry.lastAttemptAt < this.dials.retryMs) return;
491
+ }
492
+ let outcome: OrgTeamsRefreshOutcome;
493
+ try {
494
+ // Stamped BEFORE the read, on the DATABASE's clock (the one the rows'
495
+ // `created_at` is written from, whatever the app host's says; never the
496
+ // test seam): a team projected here while the hub answered is newer
497
+ // than the list and is not deleted by it.
498
+ const [clock] = (await this.deps.db.execute(sql`SELECT now() AS now`)).rows as Array<{
499
+ now: Date | string;
500
+ }>;
501
+ const readAt = new Date(clock!.now);
502
+ const result = await this.deps.client.orgTeams(localUserId, hubOrgId);
503
+ if (result.kind === 'ok') {
504
+ await this.deps.db.transaction(async (tx) => {
505
+ await projectOrgTeams(tx, { workspaceId, teams: result.teams, complete: result.complete, readAt });
506
+ });
507
+ outcome = 'ok';
508
+ } else {
509
+ outcome = result.kind;
510
+ }
511
+ } catch (err) {
512
+ this.deps.logger.warn({ err, hubOrgId }, 'hub reconcile: projecting the org’s team list failed');
513
+ outcome = 'error';
514
+ }
515
+ this.orgTeamsRefreshes.inc({ outcome });
516
+ const at = this.now();
517
+ pruneOversized(this.orgTeams, (e) => at - e.refreshedAt >= this.dials.orgTeamsTtlMs);
518
+ const prev = this.orgTeams.get(hubOrgId);
519
+ if (outcome === 'ok') {
520
+ this.orgTeams.set(hubOrgId, { refreshedAt: at, lastAttemptAt: prev?.lastAttemptAt ?? 0 });
521
+ return;
522
+ }
523
+ this.orgTeams.set(hubOrgId, { refreshedAt: prev?.refreshedAt ?? 0, lastAttemptAt: at });
524
+ if (outcome !== 'error') {
525
+ this.deps.logger.warn(
526
+ { hubOrgId, outcome },
527
+ 'hub reconcile: the org’s team list could not be read — keeping the previous one'
528
+ );
529
+ }
530
+ }
531
+
532
+ /**
533
+ * The organizations the person's last definitive hub list named as not
534
+ * opening this tool to them (`GET /me`'s `hubDenied`, the refusal page's
535
+ * words). Empty when unknown: never read, evicted, or cleared by a
536
+ * `no_link` / `grant_dead` pass. Per replica, like the rest of the cache.
537
+ */
538
+ deniedOrgs(localUserId: string): HubDeniedOrg[] {
539
+ return this.cache.get(localUserId)?.denied ?? [];
540
+ }
541
+
542
+ private remember(
543
+ localUserId: string,
544
+ definitive: Extract<ReconcilePassOutcome, 'ok' | 'no_link' | 'grant_dead'> | null,
545
+ denied: HubDeniedOrg[] = []
546
+ ): void {
547
+ const now = this.now();
548
+ pruneOversized(this.cache, (entry) => now - entry.fetchedAt >= this.dials.reconcileTtlMs);
549
+ const prev = this.cache.get(localUserId);
550
+ this.cache.set(localUserId, {
551
+ fetchedAt: definitive ? now : (prev?.fetchedAt ?? 0),
552
+ lastAttemptAt: now,
553
+ lastDefinitive: definitive ?? prev?.lastDefinitive ?? null,
554
+ // 'ok' carries the fresh list; a failed pass keeps the last one; a
555
+ // definitive no_link / grant_dead has no list to show.
556
+ denied: definitive === 'ok' ? denied : definitive === null ? (prev?.denied ?? []) : []
557
+ });
558
+ }
559
+ }
560
+
561
+ /** Keep a cache under MAX_CACHE_ENTRIES: drop expired entries first, then oldest-inserted. */
562
+ export function pruneOversized<V>(cache: Map<string, V>, expired: (value: V) => boolean): void {
563
+ if (cache.size < MAX_CACHE_ENTRIES) return;
564
+ for (const [key, value] of cache) {
565
+ if (expired(value)) cache.delete(key);
566
+ }
567
+ while (cache.size >= MAX_CACHE_ENTRIES) {
568
+ const oldest = cache.keys().next();
569
+ if (oldest.done) break;
570
+ cache.delete(oldest.value);
571
+ }
572
+ }