@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,1074 @@
1
+ import { createServer } from 'node:http';
2
+ import { entitlementProfileSchema, usageCheckRequestSchema, usageCheckSchema, usageEventSchema, usageEventsBatchSchema, usageIngestResultSchema } from '@antasphere/chassis-contract';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { exportJWK, generateKeyPair, SignJWT } from 'jose';
5
+ /** What the tool requested before PRDCT-2443 — and what the hub's H3 connect grant carries. */
6
+ export const LEGACY_GRANT_SCOPE = 'openid profile email offline_access account:read';
7
+ /** The hub's DEDICATED scope for POST /api/v1/orgs. `account:write` does NOT open it. */
8
+ export const ORG_CREATE_SCOPE = 'orgs:create';
9
+ /** The credits a new organization is granted at the hub (the sign-up grant). */
10
+ const SIGNUP_GRANT = 5_000;
11
+ /** The hub's `z.uuid()` on an account reference, as zod 4 spells it (the nil and max uuids included). */
12
+ const UUID_RE = /^([0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i;
13
+ export class FakeHub {
14
+ server;
15
+ issuer;
16
+ keys;
17
+ configuredClientId;
18
+ configuredClientSecret;
19
+ configuredToolSlug;
20
+ codes = new Map();
21
+ /** code → the scope string the grant will carry (see mintCode). */
22
+ codeScopes = new Map();
23
+ /** Body of the most recent token request — pins the `resource` passthrough. */
24
+ lastTokenRequest = null;
25
+ /** sub → orgId → entry: the caller-scoped truth GET /orgs serves. */
26
+ userOrgs = new Map();
27
+ /**
28
+ * orgId → the organization's WHOLE team list (what GET /teams serves,
29
+ * PRDCT-2813). An org with no entry here serves the union of the teams its
30
+ * members' `GET /orgs` entries carry (the real hub's list always holds
31
+ * every team a member sits in), so a suite that seats teams through
32
+ * `setUserOrg` alone never sees them swept by the team-list read.
33
+ */
34
+ orgTeamRegistry = new Map();
35
+ /** sub → the orgs that do NOT open the tool to them (GET /orgs' top-level `denied`). */
36
+ userDenied = new Map();
37
+ /** Every JWT access token this fake minted (bearer lookup for /api/v1). */
38
+ accessTokens = new Map();
39
+ /** Live + rotated-out refresh tokens (rotation families). */
40
+ refreshTokens = new Map();
41
+ /** Families torn down by reuse detection (or `revokeGrants`). */
42
+ deadFamilies = new Set();
43
+ // ── Failure injection ─────────────────────────────────────────────────
44
+ /** GET /api/v1/orgs behavior. */
45
+ orgsMode = 'ok';
46
+ /** GET /api/v1/teams behavior (`http404` = an older hub without the route). */
47
+ teamsMode = 'ok';
48
+ /** Hold every /orgs answer this long (single-flight/race tests). */
49
+ orgsDelayMs = 0;
50
+ /** Hold every GET /api/v1/teams answer this long (the stale-delete race). */
51
+ teamsDelayMs = 0;
52
+ /**
53
+ * The real hub's answer when the cursor's team was deleted between two page
54
+ * reads: an unknown keyset cursor answers an EMPTY page with no cursor. When
55
+ * true, every GET /api/v1/teams carrying a `cursor` answers exactly that;
56
+ * the first page is unaffected.
57
+ */
58
+ teamsCursorLost = false;
59
+ /**
60
+ * POST /api/v1/orgs behavior (the as-the-user org creation, PRDCT-2443).
61
+ * `limit` = the hub's per-user cap (403 org_limit_reached, the real hub's
62
+ * answer); `forbidden` = a hub that does not open creation to this grant;
63
+ * `commit_then_500` creates the org and THEN answers 500 — the ambiguity
64
+ * the tool must never resolve by re-posting.
65
+ */
66
+ orgCreateMode = 'ok';
67
+ /** Every POST /api/v1/orgs: the presented Authorization header + parsed body. */
68
+ orgCreateRequests = [];
69
+ /**
70
+ * Token endpoint behavior (both grants). `hang` never answers (the request
71
+ * is parked, nothing consumed); `commit_then_hang` consumes the presented
72
+ * refresh token — rotation committed hub-side — and THEN never answers.
73
+ */
74
+ tokenMode = 'ok';
75
+ /** Hold every token answer this long (refresh-race tests). */
76
+ tokenDelayMs = 0;
77
+ /** How long a `hang` / `commit_then_hang` request is parked before the fake gives up on it. */
78
+ tokenHangMs = 30_000;
79
+ /** Introspection endpoint behavior. */
80
+ introspectMode = 'ok';
81
+ /** Hold every introspection answer this long (lock-hold tests). */
82
+ introspectDelayMs = 0;
83
+ /** Lifetime of newly minted access tokens (seconds). */
84
+ accessTokenTtlSeconds = 900;
85
+ /** Every GET /api/v1/teams request: the presented Authorization header and the org it named. */
86
+ teamsRequests = [];
87
+ /** Every /api/v1/orgs request: the presented Authorization header. */
88
+ orgsRequests = [];
89
+ /** Every refresh-grant presentation (single-flight/rotation pins). */
90
+ refreshRequests = [];
91
+ /** Every introspection call (the grant service's probe pins). */
92
+ introspectRequests = [];
93
+ // ── The billing rail's machine channel (the spec, §5 and §6) ──────────
94
+ /**
95
+ * `grant_type=client_credentials` on a registry tool client, scope
96
+ * `usage:write`: the token carries a client and no subject, and the fake
97
+ * refuses it on every route but `/api/v1/usage/*` — exactly the hub's
98
+ * stance. Client auth is basic or post; the secret is checked when the
99
+ * fake was started with one.
100
+ */
101
+ machineTokens = new Map();
102
+ /** How many machine tokens were minted (single-flight and refresh pins). */
103
+ machineTokenMints = 0;
104
+ /** Lifetime of a machine token (seconds; the real hub mints 900). */
105
+ machineTokenTtlSeconds = 900;
106
+ /** `POST /api/v1/usage/events` behavior; `http401_once` refuses the first post with 401 (a dead token), then answers. */
107
+ usageMode = 'ok';
108
+ /** Every usage post: the presented Authorization header + the parsed body. */
109
+ usageRequests = [];
110
+ /** id → the event as ingested (the dedupe table). */
111
+ usageEvents = new Map();
112
+ /** `GET /api/v1/usage/entitlements` behavior. */
113
+ entitlementsMode = 'ok';
114
+ /** Every entitlements read: the accountRef asked and the Authorization header. */
115
+ entitlementsRequests = [];
116
+ /** accountRef → the profile the hub answers; an account not set here is `free` with no overrides. */
117
+ entitlementProfiles = new Map();
118
+ /** `POST /api/v1/usage/check` behavior; `hang` parks the request (`tokenHangMs`). */
119
+ checkMode = 'ok';
120
+ /** Hold every check answer this long. */
121
+ checkDelayMs = 0;
122
+ /** Every credit check: the presented Authorization header + the parsed body. */
123
+ checkRequests = [];
124
+ /** actionKey → its price row (the hub's price book for this tool). */
125
+ prices = new Map();
126
+ /** accountRef → credits; a known organization with no entry holds the sign-up grant. */
127
+ balances = new Map();
128
+ /** Every balance movement, in order: the sign-up grant, manual grants, ingest debits. */
129
+ ledger = [];
130
+ constructor(server, issuer, keys, configuredClientId, configuredClientSecret, configuredToolSlug) {
131
+ this.server = server;
132
+ this.issuer = issuer;
133
+ this.keys = keys;
134
+ this.configuredClientId = configuredClientId;
135
+ this.configuredClientSecret = configuredClientSecret;
136
+ this.configuredToolSlug = configuredToolSlug;
137
+ }
138
+ /**
139
+ * `clientId` is the TOOL's OAuth client id at the hub (its `HUB_CLIENT_ID`):
140
+ * the client the fake mints a connect grant for, stamps as `azp`, and keys a
141
+ * grant family on when the request names none. The fake spells no tool's id:
142
+ * a suite that reaches one of those paths passes it, one that only needs an
143
+ * issuer may omit it.
144
+ */
145
+ static async start(options = {}) {
146
+ const key = await newKey();
147
+ const server = createServer();
148
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
149
+ const address = server.address();
150
+ if (!address || typeof address === 'string')
151
+ throw new Error('fake hub failed to bind');
152
+ const hub = new FakeHub(server, `http://127.0.0.1:${address.port}`, [key], options.clientId, options.clientSecret, options.toolSlug);
153
+ server.on('request', (req, res) => void hub.handle(req, res));
154
+ return hub;
155
+ }
156
+ /** The tool's client id, for the paths that need one the request did not carry. */
157
+ get clientId() {
158
+ if (!this.configuredClientId) {
159
+ throw new Error('FakeHub: this path needs the tool client id — start it with FakeHub.start({ clientId })');
160
+ }
161
+ return this.configuredClientId;
162
+ }
163
+ async stop() {
164
+ await new Promise((resolve, reject) => this.server.close((err) => (err ? reject(err) : resolve())));
165
+ }
166
+ /** Rotate the signing key: new tokens sign with the new key; old key drops out of the JWKS. */
167
+ async rotateKey() {
168
+ this.keys = [await newKey()];
169
+ }
170
+ /** The hub's own API resource identifier — the aud /api/v1 requires. */
171
+ get apiResource() {
172
+ return `${this.issuer}/mcp`;
173
+ }
174
+ /** The tool's registry slug: what every usage row carries, from the token, never from the body. */
175
+ get toolSlug() {
176
+ return this.configuredToolSlug ?? this.clientId.replace(/^tool-/, '');
177
+ }
178
+ // ── The org registry (what GET /orgs serves per sub) ──────────────────
179
+ setUserOrg(sub, orgId, entry) {
180
+ let orgs = this.userOrgs.get(sub);
181
+ if (!orgs) {
182
+ orgs = new Map();
183
+ this.userOrgs.set(sub, orgs);
184
+ }
185
+ orgs.set(orgId, entry);
186
+ }
187
+ removeUserOrg(sub, orgId) {
188
+ this.userOrgs.get(sub)?.delete(orgId);
189
+ }
190
+ clearUserOrgs(sub) {
191
+ this.userOrgs.delete(sub);
192
+ }
193
+ /** The organization's whole team list, as GET /teams serves it. */
194
+ setOrgTeams(orgId, teams) {
195
+ this.orgTeamRegistry.set(orgId, teams);
196
+ }
197
+ /** Back to the default: the union of the members' own teams. */
198
+ clearOrgTeams(orgId) {
199
+ this.orgTeamRegistry.delete(orgId);
200
+ }
201
+ /** The orgs GET /orgs names in `denied` for this sub (default none). */
202
+ setUserDenied(sub, denied) {
203
+ this.userDenied.set(sub, denied);
204
+ }
205
+ /**
206
+ * The hub-side "revoke this tool's access" action: tears down the user's
207
+ * refresh families AND forgets their live access tokens (emulating expiry
208
+ * — real JWTs die at their 15-minute exp; tests shouldn't wait).
209
+ */
210
+ revokeGrants(sub) {
211
+ for (const [, record] of this.refreshTokens) {
212
+ if (record.sub === sub)
213
+ this.deadFamilies.add(record.family);
214
+ }
215
+ for (const [token, record] of this.accessTokens) {
216
+ if (record.sub === sub)
217
+ this.accessTokens.delete(token);
218
+ }
219
+ }
220
+ /** Number of refresh-grant presentations seen so far (single-flight pins). */
221
+ /** Whether reuse detection (or `revokeGrants`) tore this (sub, client) family down. */
222
+ isFamilyDead(sub, clientId = this.clientId) {
223
+ return this.deadFamilies.has(`${sub}:${clientId}`);
224
+ }
225
+ refreshCount() {
226
+ return this.refreshRequests.length;
227
+ }
228
+ /** Register a one-shot authorization code for the given user fixture. */
229
+ /**
230
+ * `requestedScope` is the `scope` parameter of the tool's authorize URL
231
+ * (sso-helpers reads it off the real sign-in leg): like the real hub, the
232
+ * grant carries WHAT WAS REQUESTED and nothing else. A caller that skips
233
+ * it gets the legacy grant — the fake never hands out `orgs:create`
234
+ * unasked, so a tool that stops requesting it turns the create suites red.
235
+ */
236
+ mintCode(fixture, requestedScope) {
237
+ const code = `code_${randomUUID()}`;
238
+ this.codes.set(code, fixture);
239
+ this.codeScopes.set(code, fixture.overrides?.scope ?? requestedScope ?? LEGACY_GRANT_SCOPE);
240
+ return code;
241
+ }
242
+ /** Sign an access token directly (no HTTP dance); recorded as a live bearer. */
243
+ async signAccessToken(fixture, resource) {
244
+ return this.accessToken(fixture, resource, fixture.overrides?.scope ?? LEGACY_GRANT_SCOPE);
245
+ }
246
+ /**
247
+ * Sign an H3 exchange token — what the real hub's `POST /sso/tool-token`
248
+ * mints for the tool's `/sso/cli-connect` (the P5 pinned contract): a
249
+ * 120 s RS256 JWT, `aud` = the TOOL's resource URL (single string, no
250
+ * userinfo entry — this is not an OIDC access token), the transitional
251
+ * org-claims payload (the tool must IGNORE it — suites prove nothing
252
+ * reads it), `purpose: 'sso-connect'`, and a unique `jti`. Like the real
253
+ * H3 response, a `hubRefreshToken` rides along: a RAW one-time
254
+ * offline-grant refresh token in the ordinary (sub, client) rotation
255
+ * family — redeemable at this fake's token endpoint under the TOOL
256
+ * CLIENT's credentials, rotation + reuse detection included. And like
257
+ * the real hub (whose H3 mint proves a live membership of the target
258
+ * org), the fixture's org is seeded into the sub's registry so a
259
+ * subsequent as-the-user `GET /orgs` asserts it. `opts` bends the
260
+ * connect-specific claims for negative tests; `fixture.overrides` still
261
+ * bends iss/exp/aud like everywhere else.
262
+ */
263
+ async signConnectToken(fixture, resource, opts = {}) {
264
+ const key = this.signingKey();
265
+ const o = fixture.overrides ?? {};
266
+ const now = Math.floor(Date.now() / 1000);
267
+ const jti = opts.jti ?? `jti_${randomUUID()}`;
268
+ const purpose = opts.purpose === undefined ? 'sso-connect' : opts.purpose;
269
+ const token = await new SignJWT({
270
+ role: fixture.role,
271
+ workspace_id: fixture.workspaceId,
272
+ ...(fixture.workspaceName === null ? {} : { workspace_name: fixture.workspaceName ?? 'Fake Org' }),
273
+ email: fixture.email,
274
+ ...(purpose === null ? {} : { purpose }),
275
+ ...(opts.omitJti ? {} : { jti }),
276
+ aud: o.accessAud ?? resource
277
+ })
278
+ .setProtectedHeader({ alg: 'RS256', kid: key.kid })
279
+ .setSubject(fixture.sub)
280
+ .setIssuer(o.iss ?? this.issuer)
281
+ .setIssuedAt(now)
282
+ .setExpirationTime(now + (o.expiresInSeconds ?? 120))
283
+ .sign(key.privateKey);
284
+ // The org registry mirror of the real H3 membership precondition —
285
+ // registry-level extras survive, exactly like the code-exchange seed.
286
+ const previous = this.userOrgs.get(fixture.sub)?.get(fixture.workspaceId);
287
+ this.setUserOrg(fixture.sub, fixture.workspaceId, {
288
+ name: fixture.workspaceName === null ? null : (fixture.workspaceName ?? 'Fake Org'),
289
+ role: fixture.role,
290
+ ...(previous?.status !== undefined ? { status: previous.status } : {}),
291
+ ...(previous?.isDefault !== undefined ? { isDefault: previous.isDefault } : {}),
292
+ ...(previous?.teams !== undefined ? { teams: previous.teams } : {})
293
+ });
294
+ // A fresh H3 exchange is a FRESH grant, like a fresh consent: a past
295
+ // reuse-detection teardown must not shadow a new connect.
296
+ this.deadFamilies.delete(`${fixture.sub}:${this.clientId}`);
297
+ // The hub mints the H3 grant itself: the tool requests nothing here, so
298
+ // it carries the legacy scopes — never orgs:create.
299
+ const hubRefreshToken = this.mintRefreshToken(fixture.sub, this.clientId, fixture.overrides?.scope ?? LEGACY_GRANT_SCOPE);
300
+ return { token, jti, hubRefreshToken };
301
+ }
302
+ async handle(req, res) {
303
+ const url = new URL(req.url ?? '/', this.issuer);
304
+ if (req.method === 'GET' && url.pathname === '/.well-known/openid-configuration') {
305
+ return sendJson(res, 200, {
306
+ issuer: this.issuer,
307
+ authorization_endpoint: `${this.issuer}/api/v1/auth/oauth2/authorize`,
308
+ token_endpoint: `${this.issuer}/api/v1/auth/oauth2/token`,
309
+ userinfo_endpoint: `${this.issuer}/api/v1/auth/oauth2/userinfo`,
310
+ jwks_uri: `${this.issuer}/api/v1/auth/jwks`,
311
+ // RP-initiated logout (SL-2): the real hub's oauth-provider plugin
312
+ // advertises this once end-session ships; the tool discovers it here
313
+ // and builds ?id_token_hint&post_logout_redirect_uri URLs against it.
314
+ end_session_endpoint: `${this.issuer}/api/v1/auth/oauth2/end-session`,
315
+ code_challenge_methods_supported: ['S256'],
316
+ id_token_signing_alg_values_supported: ['RS256']
317
+ });
318
+ }
319
+ if (req.method === 'GET' && url.pathname === '/api/v1/auth/jwks') {
320
+ return sendJson(res, 200, { keys: this.keys.map((k) => ({ ...k.publicJwk, kid: k.kid })) });
321
+ }
322
+ if (req.method === 'GET' && url.pathname === '/api/v1/orgs') {
323
+ return this.handleOrgs(req, res);
324
+ }
325
+ if (req.method === 'GET' && url.pathname === '/api/v1/teams') {
326
+ return this.handleTeams(req, res, url);
327
+ }
328
+ if (req.method === 'POST' && url.pathname === '/api/v1/orgs') {
329
+ return this.handleOrgCreate(req, res);
330
+ }
331
+ if (req.method === 'POST' && url.pathname === '/api/v1/auth/oauth2/token') {
332
+ return this.handleToken(req, res);
333
+ }
334
+ if (req.method === 'POST' && url.pathname === '/api/v1/auth/oauth2/introspect') {
335
+ return this.handleIntrospect(req, res);
336
+ }
337
+ if (req.method === 'POST' && url.pathname === '/api/v1/usage/events') {
338
+ return this.handleUsageEvents(req, res);
339
+ }
340
+ if (req.method === 'GET' && url.pathname === '/api/v1/usage/entitlements') {
341
+ return this.handleUsageEntitlements(req, res, url);
342
+ }
343
+ if (req.method === 'POST' && url.pathname === '/api/v1/usage/check') {
344
+ return this.handleUsageCheck(req, res);
345
+ }
346
+ sendJson(res, 404, { error: 'not_found' });
347
+ }
348
+ /** Set what `GET /usage/entitlements` answers for one account (the tier's defaults plus overrides). */
349
+ setEntitlements(accountRef, profile) {
350
+ this.entitlementProfiles.set(accountRef, profile);
351
+ }
352
+ // ── The price book and the balances (phase 2, PRDCT-2663) ─────────────
353
+ /** Set the price row of an action: `creditsPerUnit` credits per `per` units, after a monthly free allowance. */
354
+ setPrice(actionKey, row) {
355
+ this.prices.set(actionKey, row);
356
+ }
357
+ clearPrices() {
358
+ this.prices.clear();
359
+ }
360
+ /** Set an organization's balance outright (no ledger entry). */
361
+ setBalance(accountRef, credits) {
362
+ this.balances.set(accountRef, credits);
363
+ }
364
+ /** Add (or, negative, take) credits by hand: a `manual` ledger entry. */
365
+ grant(accountRef, delta) {
366
+ this.balances.set(accountRef, this.balanceOf(accountRef) + delta);
367
+ this.ledger.push({
368
+ accountRef,
369
+ kind: 'manual',
370
+ amount: delta,
371
+ sourceRef: `manual-${this.ledger.length + 1}`
372
+ });
373
+ }
374
+ /**
375
+ * An organization's balance. One the fake knows (an org registry entry)
376
+ * with no balance set holds the sign-up grant, 5,000 credits, recorded as
377
+ * a `signup` ledger entry the first time it is read — like the real hub,
378
+ * which grants it at the organization's creation. Unknown: 0.
379
+ */
380
+ balanceOf(accountRef) {
381
+ const held = this.balances.get(accountRef);
382
+ if (held !== undefined)
383
+ return held;
384
+ if (!this.knownOrgs().has(accountRef))
385
+ return 0;
386
+ this.balances.set(accountRef, SIGNUP_GRANT);
387
+ this.ledger.push({ accountRef, kind: 'signup', amount: SIGNUP_GRANT, sourceRef: 'signup' });
388
+ return SIGNUP_GRANT;
389
+ }
390
+ knownOrgs() {
391
+ const known = new Set();
392
+ for (const orgs of this.userOrgs.values())
393
+ for (const id of orgs.keys())
394
+ known.add(id);
395
+ return known;
396
+ }
397
+ /**
398
+ * The price of a quantity of an action for an account, by the hub's rule:
399
+ * `ceil(max(0, quantity − freeLeft) / per) × creditsPerUnit`, where
400
+ * `freeLeft` is the monthly allowance minus the quantities already
401
+ * ingested for that account and action. The fake has NO month rollover:
402
+ * every event it ever ingested counts against the allowance.
403
+ */
404
+ priceOf(accountRef, actionKey, quantity) {
405
+ const row = this.prices.get(actionKey);
406
+ if (!row)
407
+ return { credits: 0, priced: false, unit: null };
408
+ let used = 0;
409
+ for (const e of this.usageEvents.values()) {
410
+ if (e.accountRef === accountRef &&
411
+ (e.actionKey ?? e.meter) === actionKey &&
412
+ typeof e.quantity === 'number') {
413
+ used += e.quantity;
414
+ }
415
+ }
416
+ const freeLeft = Math.max(0, (row.freeQuantityPerMonth ?? 0) - used);
417
+ const billable = Math.max(0, quantity - freeLeft);
418
+ return {
419
+ credits: Math.ceil(billable / (row.per ?? 1)) * row.creditsPerUnit,
420
+ priced: true,
421
+ unit: row.unit
422
+ };
423
+ }
424
+ /**
425
+ * Forget every machine token: the next post answers 401 until re-minted.
426
+ * Models a REGISTRY REMOVAL or a rotated client secret, not a restart —
427
+ * the real hub persists its signing keys and never refuses a live JWT on
428
+ * restart (proven on the pair, lane C of the billing rail wave).
429
+ */
430
+ revokeMachineTokens() {
431
+ this.machineTokens.clear();
432
+ }
433
+ /** The machine-token check of `/api/v1/usage/*`: a live client_credentials token with `usage:write`. */
434
+ machineBearer(req) {
435
+ const bearer = /^Bearer\s+(.+)$/i.exec(req.headers.authorization ?? '')?.[1] ?? null;
436
+ const record = bearer ? this.machineTokens.get(bearer) : undefined;
437
+ return Boolean(record && record.expMs > Date.now() && record.scope.split(' ').includes('usage:write'));
438
+ }
439
+ // ── POST /api/v1/usage/events: at-least-once in, exactly-once by id ─────
440
+ async handleUsageEvents(req, res) {
441
+ let body = null;
442
+ try {
443
+ body = JSON.parse(await readBody(req));
444
+ }
445
+ catch {
446
+ body = null;
447
+ }
448
+ this.usageRequests.push({ auth: req.headers.authorization ?? null, body });
449
+ if (this.usageMode === 'network') {
450
+ req.destroy();
451
+ return;
452
+ }
453
+ if (this.usageMode === 'http404')
454
+ return sendJson(res, 404, { error: { code: 'not_found' } });
455
+ if (this.usageMode === 'http500')
456
+ return sendJson(res, 500, { error: { code: 'internal' } });
457
+ if (this.usageMode === 'http400') {
458
+ return sendJson(res, 400, { error: { code: 'validation_error', message: 'injected' } });
459
+ }
460
+ if (this.usageMode === 'http401_once') {
461
+ this.usageMode = 'ok';
462
+ return sendJson(res, 401, { error: { code: 'invalid_token', message: 'Unauthorized' } });
463
+ }
464
+ if (!this.machineBearer(req)) {
465
+ return sendJson(res, 401, { error: { code: 'invalid_token', message: 'Unauthorized' } });
466
+ }
467
+ const batch = usageEventsBatchSchema.safeParse(body);
468
+ if (!batch.success) {
469
+ return sendJson(res, 400, { error: { code: 'validation_error', message: '1 to 500 events' } });
470
+ }
471
+ const { events } = batch.data;
472
+ // The real hub's per-element judgement (PRDCT-2625, `classifyBatch` in
473
+ // its api/usage.ts), in its order: each element parses on its own
474
+ // against the hub's schema (the chassis-contract copy, checked against the hub's wire snapshot: the
475
+ // uuid account reference, the maximum lengths, the action key or its
476
+ // alias — PRDCT-2629), a body that names a tool other than the token's
477
+ // registry slug is `tool_mismatch`, the account must be an organization
478
+ // this hub holds, a named user must be a hub user, a duplicate id is
479
+ // never an error.
480
+ const knownOrgs = this.knownOrgs();
481
+ const results = events.map((raw) => {
482
+ const parsed = usageEventSchema.safeParse(raw);
483
+ if (!parsed.success) {
484
+ const rawId = raw?.id;
485
+ return {
486
+ id: typeof rawId === 'string' ? rawId : null,
487
+ status: 'rejected',
488
+ reason: 'invalid_event',
489
+ details: parsed.error.issues.map((issue) => ({
490
+ path: issue.path.join('.'),
491
+ message: issue.message
492
+ }))
493
+ };
494
+ }
495
+ const e = parsed.data;
496
+ const id = e.id;
497
+ if (e.toolSlug !== undefined && e.toolSlug !== this.toolSlug) {
498
+ return { id, status: 'rejected', reason: 'tool_mismatch' };
499
+ }
500
+ if (!knownOrgs.has(e.accountRef))
501
+ return { id, status: 'rejected', reason: 'unknown_account' };
502
+ if (e.userId != null && !this.userOrgs.has(e.userId)) {
503
+ return { id, status: 'rejected', reason: 'unknown_user' };
504
+ }
505
+ if (this.usageEvents.has(id))
506
+ return { id, status: 'duplicate' };
507
+ // Priced BEFORE it is recorded, so its own quantity is not counted
508
+ // against the free allowance twice; the debit lands with it.
509
+ const { credits } = this.priceOf(e.accountRef, e.actionKey ?? e.meter, e.quantity);
510
+ this.usageEvents.set(id, { ...raw, toolSlug: this.toolSlug });
511
+ if (credits > 0) {
512
+ this.balances.set(e.accountRef, this.balanceOf(e.accountRef) - credits);
513
+ this.ledger.push({ accountRef: e.accountRef, kind: 'debit', amount: -credits, sourceRef: id });
514
+ }
515
+ return { id, status: 'accepted', credits };
516
+ });
517
+ const count = (status) => results.filter((r) => r.status === status).length;
518
+ return sendJson(res, 200, usageIngestResultSchema.parse({
519
+ results,
520
+ accepted: count('accepted'),
521
+ duplicate: count('duplicate'),
522
+ rejected: count('rejected')
523
+ }));
524
+ }
525
+ // ── GET /api/v1/usage/entitlements?accountRef=: the account's plan ─────
526
+ async handleUsageEntitlements(req, res, url) {
527
+ const accountRef = url.searchParams.get('accountRef');
528
+ this.entitlementsRequests.push({ accountRef, auth: req.headers.authorization ?? null });
529
+ if (this.entitlementsMode === 'network') {
530
+ req.destroy();
531
+ return;
532
+ }
533
+ if (this.entitlementsMode === 'http404')
534
+ return sendJson(res, 404, { error: { code: 'not_found' } });
535
+ if (this.entitlementsMode === 'http500')
536
+ return sendJson(res, 500, { error: { code: 'internal' } });
537
+ if (!this.machineBearer(req)) {
538
+ return sendJson(res, 401, { error: { code: 'invalid_token', message: 'Unauthorized' } });
539
+ }
540
+ // The real hub validates the query (`accountRef` a uuid): 400, not 404.
541
+ if (!accountRef || !UUID_RE.test(accountRef))
542
+ return sendJson(res, 400, {
543
+ error: { code: 'validation_error', message: 'accountRef must be a uuid' }
544
+ });
545
+ if (!this.knownOrgs().has(accountRef) && !this.entitlementProfiles.has(accountRef)) {
546
+ return sendJson(res, 404, {
547
+ error: { code: 'unknown_account', message: 'no organization holds this id' }
548
+ });
549
+ }
550
+ const profile = this.entitlementProfiles.get(accountRef) ?? { plan: 'free' };
551
+ return sendJson(res, 200, entitlementProfileSchema.parse({
552
+ accountRef,
553
+ plan: profile.plan,
554
+ planUntil: null,
555
+ limits: profile.limits ?? {},
556
+ features: profile.features ?? [],
557
+ upgradeUrl: `${this.issuer}/billing/upgrade?org=${accountRef}&tool=${this.toolSlug}&plan=${profile.plan}`
558
+ }));
559
+ }
560
+ // ── POST /api/v1/usage/check: the price and the balance, no side effect ─
561
+ async handleUsageCheck(req, res) {
562
+ let body = null;
563
+ try {
564
+ body = JSON.parse(await readBody(req));
565
+ }
566
+ catch {
567
+ body = null;
568
+ }
569
+ this.checkRequests.push({ auth: req.headers.authorization ?? null, body });
570
+ if (this.checkDelayMs > 0)
571
+ await new Promise((r) => setTimeout(r, this.checkDelayMs));
572
+ if (this.checkMode === 'network') {
573
+ req.destroy();
574
+ return;
575
+ }
576
+ if (this.checkMode === 'hang')
577
+ return this.park(res);
578
+ if (this.checkMode === 'http404')
579
+ return sendJson(res, 404, { error: { code: 'not_found' } });
580
+ if (this.checkMode === 'http500')
581
+ return sendJson(res, 500, { error: { code: 'internal' } });
582
+ if (!this.machineBearer(req)) {
583
+ return sendJson(res, 401, { error: { code: 'invalid_token', message: 'Unauthorized' } });
584
+ }
585
+ const parsed = usageCheckRequestSchema.safeParse(body);
586
+ if (!parsed.success) {
587
+ return sendJson(res, 400, {
588
+ error: {
589
+ code: 'validation_error',
590
+ message: parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')
591
+ }
592
+ });
593
+ }
594
+ const { accountRef, actionKey, quantity } = parsed.data;
595
+ const orgKnown = this.knownOrgs().has(accountRef);
596
+ if (!orgKnown && !this.balances.has(accountRef)) {
597
+ return sendJson(res, 404, {
598
+ error: { code: 'unknown_account', message: 'no organization holds this id' }
599
+ });
600
+ }
601
+ let suspended = false;
602
+ for (const orgs of this.userOrgs.values()) {
603
+ if (orgs.get(accountRef)?.status === 'suspended')
604
+ suspended = true;
605
+ }
606
+ const priced = this.priceOf(accountRef, actionKey, quantity);
607
+ const { unit } = priced;
608
+ // The hub's rule (its api/usage.ts): a price past the exactly
609
+ // representable range is answered as `unpriceable`, the credits clamped
610
+ // to the bound (PRDCT-2677).
611
+ const unpriceableNow = priced.credits > Number.MAX_SAFE_INTEGER;
612
+ const credits = unpriceableNow ? Number.MAX_SAFE_INTEGER : priced.credits;
613
+ const balance = this.balanceOf(accountRef);
614
+ // The hub's rule since its fix round (lane A, 23 September, ce2ce1b): a
615
+ // 0-credit action is allowed whatever the balance, negative included; an
616
+ // unpriced action is never refused. Suspension is tested first.
617
+ const covered = credits === 0 || balance >= credits;
618
+ const reason = suspended
619
+ ? 'account_suspended'
620
+ : unpriceableNow
621
+ ? 'unpriceable'
622
+ : covered
623
+ ? null
624
+ : 'insufficient_credits';
625
+ return sendJson(res, 200, usageCheckSchema.parse({
626
+ accountRef,
627
+ actionKey,
628
+ quantity,
629
+ allowed: reason === null,
630
+ credits,
631
+ balance,
632
+ unit,
633
+ priced: priced.priced,
634
+ plan: this.entitlementProfiles.get(accountRef)?.plan ?? 'free',
635
+ reason,
636
+ topUpUrl: `${this.issuer}/billing/top-up?org=${accountRef}&credits=${credits}&balance=${balance}&tool=${this.toolSlug}&action=${actionKey}`
637
+ }));
638
+ }
639
+ // ── GET /api/v1/orgs: the caller-scoped org list ──────────────────────
640
+ async handleOrgs(req, res) {
641
+ this.orgsRequests.push({ auth: req.headers.authorization ?? null });
642
+ if (this.orgsDelayMs > 0)
643
+ await new Promise((r) => setTimeout(r, this.orgsDelayMs));
644
+ if (this.orgsMode === 'network') {
645
+ req.destroy(); // mid-request connection failure, no HTTP answer
646
+ return;
647
+ }
648
+ if (this.orgsMode === 'http500')
649
+ return sendJson(res, 500, { error: { code: 'internal' } });
650
+ const bearer = /^Bearer\s+(.+)$/i.exec(req.headers.authorization ?? '')?.[1] ?? null;
651
+ const record = bearer ? this.accessTokens.get(bearer) : undefined;
652
+ // The real hub's OauthJwtVerifier: JWT it minted, unexpired, aud pinned
653
+ // to ITS OWN resource. An opaque/unknown/expired/foreign-aud bearer is
654
+ // one uniform 401 — exactly what a tool-audienced token gets.
655
+ if (!record || record.expMs <= Date.now() || !record.aud.includes(this.apiResource)) {
656
+ return sendJson(res, 401, { error: { code: 'invalid_token', message: 'Unauthorized' } });
657
+ }
658
+ const orgs = [...(this.userOrgs.get(record.sub) ?? new Map())].map(([id, entry]) => ({
659
+ id,
660
+ name: entry.name,
661
+ role: entry.role,
662
+ personal: false,
663
+ status: entry.status ?? 'active',
664
+ isDefault: entry.isDefault ?? false,
665
+ createdAt: new Date(0).toISOString(),
666
+ teams: entry.teams ?? []
667
+ }));
668
+ return sendJson(res, 200, { orgs, denied: this.userDenied.get(record.sub) ?? [] });
669
+ }
670
+ // ── GET /api/v1/teams: the organization's whole team list ─────────────
671
+ // Mirrors the hub's api/teams.ts: the org is the one `X-Workspace-Id`
672
+ // names, any member of it reads the list, paged by `limit` / `cursor`.
673
+ async handleTeams(req, res, url) {
674
+ const header = req.headers['x-workspace-id'];
675
+ const orgId = (Array.isArray(header) ? header[0] : header) ?? null;
676
+ this.teamsRequests.push({ auth: req.headers.authorization ?? null, orgId });
677
+ if (this.teamsDelayMs > 0)
678
+ await new Promise((r) => setTimeout(r, this.teamsDelayMs));
679
+ if (this.teamsMode === 'network') {
680
+ req.destroy();
681
+ return;
682
+ }
683
+ if (this.teamsMode === 'http500')
684
+ return sendJson(res, 500, { error: { code: 'internal' } });
685
+ if (this.teamsMode === 'http404')
686
+ return sendJson(res, 404, { error: 'not_found' });
687
+ const bearer = /^Bearer\s+(.+)$/i.exec(req.headers.authorization ?? '')?.[1] ?? null;
688
+ const record = bearer ? this.accessTokens.get(bearer) : undefined;
689
+ if (!record || record.expMs <= Date.now() || !record.aud.includes(this.apiResource)) {
690
+ return sendJson(res, 401, { error: { code: 'invalid_token', message: 'Unauthorized' } });
691
+ }
692
+ if (!orgId || !this.userOrgs.get(record.sub)?.has(orgId)) {
693
+ return sendJson(res, 403, { error: { code: 'workspace_mismatch' } });
694
+ }
695
+ let all = this.orgTeamRegistry.get(orgId);
696
+ if (!all) {
697
+ const union = new Map();
698
+ for (const orgs of this.userOrgs.values()) {
699
+ for (const team of orgs.get(orgId)?.teams ?? [])
700
+ if (!union.has(team.id))
701
+ union.set(team.id, team);
702
+ }
703
+ all = [...union.values()];
704
+ }
705
+ if (this.teamsCursorLost && url.searchParams.has('cursor')) {
706
+ return sendJson(res, 200, { teams: [], nextCursor: null });
707
+ }
708
+ const rawLimit = Number(url.searchParams.get('limit') ?? 50);
709
+ const limit = Number.isInteger(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, 100) : 50;
710
+ const start = Number(url.searchParams.get('cursor') ?? 0) || 0;
711
+ const page = all.slice(start, start + limit);
712
+ const stamp = new Date(0).toISOString();
713
+ return sendJson(res, 200, {
714
+ teams: page.map((team) => ({
715
+ id: team.id,
716
+ slug: team.slug,
717
+ name: team.name,
718
+ membersCount: 0,
719
+ isMember: false,
720
+ createdAt: stamp,
721
+ updatedAt: stamp
722
+ })),
723
+ nextCursor: start + limit < all.length ? String(start + limit) : null
724
+ });
725
+ }
726
+ // ── POST /api/v1/orgs: create an org AS THE BEARER ────────────────────
727
+ // Mirrors the hub's api/orgs.ts: the bearer's subject becomes the owner —
728
+ // the body carries a name and nothing that could name another user.
729
+ async handleOrgCreate(req, res) {
730
+ let body = null;
731
+ try {
732
+ body = JSON.parse(await readBody(req));
733
+ }
734
+ catch {
735
+ body = null;
736
+ }
737
+ this.orgCreateRequests.push({ auth: req.headers.authorization ?? null, body });
738
+ if (this.orgCreateMode === 'network') {
739
+ req.destroy();
740
+ return;
741
+ }
742
+ if (this.orgCreateMode === 'http500')
743
+ return sendJson(res, 500, { error: { code: 'internal' } });
744
+ const bearer = /^Bearer\s+(.+)$/i.exec(req.headers.authorization ?? '')?.[1] ?? null;
745
+ const record = bearer ? this.accessTokens.get(bearer) : undefined;
746
+ if (!record || record.expMs <= Date.now() || !record.aud.includes(this.apiResource)) {
747
+ return sendJson(res, 401, { error: { code: 'invalid_token', message: 'Unauthorized' } });
748
+ }
749
+ // The real hub's entrance rule (api/orgs.ts `orgCreateRefusal`): a tool
750
+ // grant creates an organization ONLY with the dedicated scope. A legacy
751
+ // grant (account:read), and one carrying account:write, are refused the
752
+ // same way — 403 insufficient_scope, before the cap is even looked at.
753
+ if (!record.scope.split(' ').includes(ORG_CREATE_SCOPE)) {
754
+ return sendJson(res, 403, {
755
+ error: {
756
+ code: 'insufficient_scope',
757
+ message: `This credential was not granted "${ORG_CREATE_SCOPE}"`
758
+ }
759
+ });
760
+ }
761
+ if (this.orgCreateMode === 'limit') {
762
+ return sendJson(res, 403, { error: { code: 'org_limit_reached', message: 'cap' } });
763
+ }
764
+ if (this.orgCreateMode === 'forbidden') {
765
+ return sendJson(res, 403, { error: { code: 'forbidden', message: 'sessions only' } });
766
+ }
767
+ const name = body?.name;
768
+ if (typeof name !== 'string' || !name.trim()) {
769
+ return sendJson(res, 400, { error: { code: 'validation_error', message: 'name' } });
770
+ }
771
+ const id = randomUUID();
772
+ this.setUserOrg(record.sub, id, { name: name.trim(), role: 'owner' });
773
+ if (this.orgCreateMode === 'commit_then_500') {
774
+ return sendJson(res, 500, { error: { code: 'internal' } });
775
+ }
776
+ return sendJson(res, 201, {
777
+ org: {
778
+ id,
779
+ name: name.trim(),
780
+ role: 'owner',
781
+ personal: false,
782
+ status: 'active',
783
+ isDefault: false,
784
+ createdAt: new Date().toISOString()
785
+ }
786
+ });
787
+ }
788
+ /** The org ids the hub holds for a subject (what a test asserts creation against). */
789
+ orgIdsOf(sub) {
790
+ return [...(this.userOrgs.get(sub)?.keys() ?? [])];
791
+ }
792
+ // ── POST /api/v1/auth/oauth2/token: both grants ───────────────────────
793
+ async handleToken(req, res) {
794
+ const body = new URLSearchParams(await readBody(req));
795
+ this.lastTokenRequest = body;
796
+ if (this.tokenDelayMs > 0)
797
+ await new Promise((r) => setTimeout(r, this.tokenDelayMs));
798
+ if (this.tokenMode === 'network') {
799
+ req.destroy();
800
+ return;
801
+ }
802
+ if (this.tokenMode === 'http500')
803
+ return sendJson(res, 500, { error: { code: 'internal' } });
804
+ if (this.tokenMode === 'invalid_grant') {
805
+ return sendJson(res, 400, { error: 'invalid_grant', error_description: 'injected' });
806
+ }
807
+ if (this.tokenMode === 'invalid_client') {
808
+ return sendJson(res, 401, { error: 'invalid_client', error_description: 'injected' });
809
+ }
810
+ if (this.tokenMode === 'hang')
811
+ return this.park(res);
812
+ const grantType = body.get('grant_type') ?? (body.get('code') ? 'authorization_code' : '');
813
+ if (grantType === 'refresh_token')
814
+ return this.handleRefreshGrant(body, res);
815
+ if (grantType === 'client_credentials')
816
+ return this.handleClientCredentials(req, body, res);
817
+ const fixture = this.codes.get(body.get('code') ?? '');
818
+ if (!fixture) {
819
+ return sendJson(res, 400, { error: 'invalid_grant', error_description: 'unknown code' });
820
+ }
821
+ const grantScope = this.codeScopes.get(body.get('code')) ?? LEGACY_GRANT_SCOPE;
822
+ this.codes.delete(body.get('code')); // one-shot, like the real thing
823
+ this.codeScopes.delete(body.get('code'));
824
+ // The code's fixture seeds the sub's org registry (the hub knows the
825
+ // orgs its own users SSO from) — name/role from the fixture, while the
826
+ // registry-level extras (`status`, `isDefault`, `teams` — managed via
827
+ // `setUserOrg`) survive a re-login's re-seed.
828
+ const previous = this.userOrgs.get(fixture.sub)?.get(fixture.workspaceId);
829
+ this.setUserOrg(fixture.sub, fixture.workspaceId, {
830
+ name: fixture.workspaceName === null ? null : (fixture.workspaceName ?? 'Fake Org'),
831
+ role: fixture.role,
832
+ ...(previous?.status !== undefined ? { status: previous.status } : {}),
833
+ ...(previous?.isDefault !== undefined ? { isDefault: previous.isDefault } : {}),
834
+ ...(previous?.teams !== undefined ? { teams: previous.teams } : {})
835
+ });
836
+ const clientId = body.get('client_id') ?? this.clientId;
837
+ // A fresh code exchange is a FRESH grant: the real hub's new consent
838
+ // mints a new family — a past reuse-detection teardown does not shadow
839
+ // a re-login (the hub_grant_expired → browser-re-login heal path).
840
+ this.deadFamilies.delete(`${fixture.sub}:${clientId}`);
841
+ const resource = body.get('resource');
842
+ // JWT iff the token request carries `resource` (checkResource →
843
+ // isJwtAccessToken on the real hub); otherwise opaque.
844
+ const accessToken = resource && !fixture.overrides?.forceOpaque
845
+ ? await this.accessToken(fixture, resource, grantScope)
846
+ : `opaque_${randomUUID()}`;
847
+ const refreshToken = this.mintRefreshToken(fixture.sub, clientId, grantScope);
848
+ return sendJson(res, 200, {
849
+ access_token: accessToken,
850
+ id_token: await this.idToken(fixture, clientId),
851
+ refresh_token: refreshToken,
852
+ token_type: 'Bearer',
853
+ expires_in: fixture.overrides?.expiresInSeconds ?? this.accessTokenTtlSeconds,
854
+ scope: grantScope
855
+ });
856
+ }
857
+ /**
858
+ * The registry client's machine grant (the billing rail, §6): client auth
859
+ * (basic, or client_id + client_secret in the body) on THIS tool's client,
860
+ * scope `usage:write` only. Answers a bearer with no subject.
861
+ */
862
+ handleClientCredentials(req, body, res) {
863
+ const basic = /^Basic\s+(.+)$/i.exec(req.headers.authorization ?? '')?.[1] ?? null;
864
+ let clientId = body.get('client_id');
865
+ let clientSecret = body.get('client_secret');
866
+ if (basic) {
867
+ const decoded = Buffer.from(basic, 'base64').toString('utf8');
868
+ const colon = decoded.indexOf(':');
869
+ if (colon > 0) {
870
+ clientId = decodeURIComponent(decoded.slice(0, colon));
871
+ clientSecret = decodeURIComponent(decoded.slice(colon + 1));
872
+ }
873
+ }
874
+ if (!clientId || !clientSecret || clientId !== this.clientId) {
875
+ return sendJson(res, 401, { error: 'invalid_client', error_description: 'client auth required' });
876
+ }
877
+ if (this.configuredClientSecret !== undefined && clientSecret !== this.configuredClientSecret) {
878
+ return sendJson(res, 401, { error: 'invalid_client', error_description: 'bad secret' });
879
+ }
880
+ // The real hub (PRDCT-2625): the scope must be EXACTLY usage:write, and
881
+ // the RFC 8707 resource must be the hub's own API resource.
882
+ if (body.get('scope') !== 'usage:write') {
883
+ return sendJson(res, 400, { error: 'invalid_scope', error_description: 'usage:write only' });
884
+ }
885
+ if (body.get('resource') !== this.apiResource) {
886
+ return sendJson(res, 400, { error: 'invalid_target', error_description: 'resource must be the hub' });
887
+ }
888
+ this.machineTokenMints += 1;
889
+ const token = `mach_${randomUUID()}`;
890
+ this.machineTokens.set(token, {
891
+ expMs: Date.now() + this.machineTokenTtlSeconds * 1000,
892
+ scope: 'usage:write'
893
+ });
894
+ return sendJson(res, 200, {
895
+ access_token: token,
896
+ token_type: 'Bearer',
897
+ expires_in: this.machineTokenTtlSeconds,
898
+ scope: 'usage:write'
899
+ });
900
+ }
901
+ handleRefreshGrant(body, res) {
902
+ const presented = body.get('refresh_token') ?? '';
903
+ const clientId = body.get('client_id');
904
+ this.refreshRequests.push({
905
+ refreshToken: presented,
906
+ resource: body.get('resource'),
907
+ clientId
908
+ });
909
+ if (!clientId || !body.get('client_secret')) {
910
+ return sendJson(res, 401, { error: 'invalid_client', error_description: 'client auth required' });
911
+ }
912
+ const record = this.refreshTokens.get(presented);
913
+ if (!record) {
914
+ return sendJson(res, 400, { error: 'invalid_grant', error_description: 'unknown refresh token' });
915
+ }
916
+ if (this.deadFamilies.has(record.family)) {
917
+ return sendJson(res, 400, { error: 'invalid_grant', error_description: 'grant family revoked' });
918
+ }
919
+ if (record.rotatedOut) {
920
+ // RFC 9700 REUSE DETECTION: presenting a rotated-out token is treated
921
+ // as theft — the WHOLE (sub, client) family dies, current token
922
+ // included. The single most important behavior this fake models.
923
+ this.deadFamilies.add(record.family);
924
+ return sendJson(res, 400, {
925
+ error: 'invalid_grant',
926
+ error_description: 'reuse detected — family revoked'
927
+ });
928
+ }
929
+ record.rotatedOut = true;
930
+ const rotated = this.mintRefreshToken(record.sub, record.clientId, record.scope);
931
+ // The slow-but-alive hub: the rotation above is committed and the
932
+ // answer never leaves. Whoever presented `presented` now holds a
933
+ // rotated-out token without knowing it.
934
+ if (this.tokenMode === 'commit_then_hang')
935
+ return this.park(res);
936
+ const resource = body.get('resource');
937
+ const respond = async () => {
938
+ const accessToken = resource
939
+ ? await this.accessToken({ sub: record.sub, email: 'refresh@fake.hub', workspaceId: '', role: 'member' }, resource, record.scope)
940
+ : `opaque_${randomUUID()}`;
941
+ sendJson(res, 200, {
942
+ access_token: accessToken,
943
+ refresh_token: rotated,
944
+ token_type: 'Bearer',
945
+ expires_in: this.accessTokenTtlSeconds,
946
+ scope: record.scope
947
+ });
948
+ };
949
+ void respond();
950
+ }
951
+ /** Never answer; give up with a 504 long after any client timeout (nothing observes it). */
952
+ park(res) {
953
+ const t = setTimeout(() => {
954
+ if (!res.writableEnded)
955
+ sendJson(res, 504, { error: 'parked' });
956
+ }, this.tokenHangMs);
957
+ t.unref();
958
+ }
959
+ // ── POST /api/v1/auth/oauth2/introspect: RFC 7662, read-only ──────────
960
+ async handleIntrospect(req, res) {
961
+ const body = new URLSearchParams(await readBody(req));
962
+ const token = body.get('token') ?? '';
963
+ const clientId = body.get('client_id');
964
+ this.introspectRequests.push({ token, hint: body.get('token_type_hint'), clientId });
965
+ if (this.introspectDelayMs > 0)
966
+ await new Promise((r) => setTimeout(r, this.introspectDelayMs));
967
+ if (this.introspectMode === 'invalid_client') {
968
+ return sendJson(res, 401, { error: 'invalid_client', error_description: 'injected' });
969
+ }
970
+ if (this.introspectMode === 'malformed')
971
+ return sendJson(res, 200, { hello: 'world' });
972
+ if (this.introspectMode === 'network') {
973
+ req.destroy();
974
+ return;
975
+ }
976
+ if (this.introspectMode === 'http500')
977
+ return sendJson(res, 500, { error: { code: 'internal' } });
978
+ if (!clientId || !body.get('client_secret')) {
979
+ return sendJson(res, 401, { error: 'invalid_client', error_description: 'client auth required' });
980
+ }
981
+ // Refresh tokens only (the grant service's probe always hints it); an
982
+ // access token is out of scope for this fake's introspection.
983
+ const record = this.refreshTokens.get(token);
984
+ if (!record ||
985
+ record.clientId !== clientId ||
986
+ record.rotatedOut ||
987
+ this.deadFamilies.has(record.family)) {
988
+ return sendJson(res, 200, { active: false });
989
+ }
990
+ return sendJson(res, 200, {
991
+ active: true,
992
+ client_id: clientId,
993
+ sub: record.sub,
994
+ token_type: 'refresh_token'
995
+ });
996
+ }
997
+ mintRefreshToken(sub, clientId, scope) {
998
+ const token = `rt_${randomUUID()}`;
999
+ this.refreshTokens.set(token, { sub, clientId, family: `${sub}:${clientId}`, rotatedOut: false, scope });
1000
+ return token;
1001
+ }
1002
+ signingKey() {
1003
+ const key = this.keys[0];
1004
+ if (!key)
1005
+ throw new Error('fake hub has no signing key');
1006
+ return key;
1007
+ }
1008
+ async accessToken(fixture, resource, scope) {
1009
+ const key = this.signingKey();
1010
+ const o = fixture.overrides ?? {};
1011
+ const rawAud = o.accessAud ?? [resource, `${this.issuer}/api/v1/auth/oauth2/userinfo`];
1012
+ const aud = Array.isArray(rawAud) ? rawAud : [rawAud];
1013
+ const now = Math.floor(Date.now() / 1000);
1014
+ const exp = now + (o.expiresInSeconds ?? this.accessTokenTtlSeconds);
1015
+ const token = await new SignJWT({
1016
+ // TRANSITIONAL advisory org claims (compat window) — never read by
1017
+ // the new login path; suites prove it.
1018
+ role: fixture.role,
1019
+ ...(fixture.workspaceId ? { workspace_id: fixture.workspaceId } : {}),
1020
+ ...(fixture.workspaceName === null ? {} : { workspace_name: fixture.workspaceName ?? 'Fake Org' }),
1021
+ email: fixture.email,
1022
+ azp: this.clientId,
1023
+ scope,
1024
+ aud
1025
+ })
1026
+ .setProtectedHeader({ alg: 'RS256', kid: key.kid })
1027
+ .setSubject(fixture.sub)
1028
+ .setIssuer(o.iss ?? this.issuer)
1029
+ .setIssuedAt(now)
1030
+ .setExpirationTime(exp)
1031
+ .sign(key.privateKey);
1032
+ // Recorded so /api/v1/orgs can resolve the bearer to its subject —
1033
+ // with the REAL exp/aud so wrong-audience and expiry refuse honestly.
1034
+ this.accessTokens.set(token, { sub: fixture.sub, aud, expMs: exp * 1000, scope });
1035
+ return token;
1036
+ }
1037
+ async idToken(fixture, clientId) {
1038
+ const key = this.signingKey();
1039
+ const o = fixture.overrides ?? {};
1040
+ const now = Math.floor(Date.now() / 1000);
1041
+ return new SignJWT({
1042
+ name: fixture.name ?? 'Fake Hub User',
1043
+ email: fixture.email,
1044
+ email_verified: fixture.emailVerified ?? true,
1045
+ ...(o.idTokenSid ? { sid: o.idTokenSid } : {}),
1046
+ aud: clientId
1047
+ })
1048
+ .setProtectedHeader({ alg: 'RS256', kid: key.kid })
1049
+ .setSubject(o.idTokenSub ?? fixture.sub)
1050
+ .setIssuer(o.iss ?? this.issuer)
1051
+ .setIssuedAt(now)
1052
+ .setExpirationTime(now + (o.expiresInSeconds ?? 3600))
1053
+ .sign(key.privateKey);
1054
+ }
1055
+ }
1056
+ async function newKey() {
1057
+ const { privateKey, publicKey } = await generateKeyPair('RS256', { extractable: true });
1058
+ const publicJwk = await exportJWK(publicKey);
1059
+ return { kid: `kid_${randomUUID()}`, privateKey, publicJwk };
1060
+ }
1061
+ function sendJson(res, status, body) {
1062
+ const payload = JSON.stringify(body);
1063
+ res.writeHead(status, { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) });
1064
+ res.end(payload);
1065
+ }
1066
+ function readBody(req) {
1067
+ return new Promise((resolve, reject) => {
1068
+ const chunks = [];
1069
+ req.on('data', (c) => chunks.push(c));
1070
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
1071
+ req.on('error', reject);
1072
+ });
1073
+ }
1074
+ //# sourceMappingURL=fake-hub.js.map