@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,161 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql';
3
+ import { ulid } from 'ulid';
4
+ import type { UsageEvent } from '@antasphere/chassis-contract';
5
+ import { FakeHub } from '@antasphere/chassis-server/testing';
6
+ import { createDatabase, createTestApp, readJson, startPostgres, type TestApp, host } from './helpers.js';
7
+ import * as sso from './sso-helpers.js';
8
+
9
+ /**
10
+ * The hub's occurredAt window on the chassis (PRDCT-2644), end to end on
11
+ * the minimal host against the fake hub, cloud edition: an event whose
12
+ * `occurredAt` is eight days old is held by the poster BEFORE posting (the
13
+ * hub never sees it), sent to the held queue, counted on /metrics as a
14
+ * window hold (never as a retry-budget hold), and re-driven round after
15
+ * round, held again each time, never posted; an ordinary upload beside it
16
+ * lands as before.
17
+ *
18
+ * The stale event enters through the boot result's registry usage sink
19
+ * (`app.registry.usage`, the PgBossUsageSink the gate itself emits
20
+ * through), built from the upload's own event as the hub recorded it, so it
21
+ * is a well-formed event in every field but its date.
22
+ */
23
+
24
+ const HUB_SECRET = 'integration-test-hub-secret-window';
25
+ const METRICS_TOKEN = 'integration-metrics-token-window';
26
+ const ORG = '77777777-aaaa-4bbb-8ccc-00000000e0f1';
27
+ const OPERATOR = { email: 'operator@window.test', name: 'Operator', password: 'operator-window-pass-1' };
28
+ const DAY = 24 * 60 * 60 * 1000;
29
+
30
+ let container: StartedPostgreSqlContainer;
31
+ let hub: FakeHub;
32
+ let app: TestApp;
33
+ let cookie: string;
34
+ let workspaceId: string;
35
+
36
+ const json = (body: unknown) => ({
37
+ method: 'POST',
38
+ headers: { 'content-type': 'application/json', 'x-forwarded-for': sso.nextIp() },
39
+ body: JSON.stringify(body)
40
+ });
41
+
42
+ /** The value of one exact series (`name{labels}`), 0 when absent. */
43
+ async function metric(series: string): Promise<number> {
44
+ const res = await app.app.request('/metrics', { headers: { authorization: `Bearer ${METRICS_TOKEN}` } });
45
+ expect(res.status).toBe(200);
46
+ const line = (await res.text()).split('\n').find((l) => l.startsWith(`${series} `));
47
+ return line ? Number(line.split(' ').at(-1)) : 0;
48
+ }
49
+
50
+ async function until<T>(read: () => T | Promise<T>, ok: (v: T) => boolean, ms = 30_000): Promise<T> {
51
+ const deadline = Date.now() + ms;
52
+ for (;;) {
53
+ const v = await read();
54
+ if (ok(v) || Date.now() > deadline) return v;
55
+ await new Promise((r) => setTimeout(r, 100));
56
+ }
57
+ }
58
+
59
+ beforeAll(async () => {
60
+ container = await startPostgres();
61
+ hub = await FakeHub.start({ clientId: host.hubClientId, clientSecret: HUB_SECRET });
62
+ app = await createTestApp(
63
+ await createDatabase(container, 'usage_window'),
64
+ {
65
+ EDITION: 'cloud',
66
+ HUB_ISSUER_URL: hub.issuer,
67
+ HUB_CLIENT_ID: host.hubClientId,
68
+ HUB_CLIENT_SECRET: HUB_SECRET,
69
+ METRICS_TOKEN
70
+ },
71
+ // A held event comes back a second after its hold (the PRDCT-2635 dials).
72
+ { usageRetry: { limit: 1, delaySeconds: 1, heldDelaySeconds: 1 }, entitlementDials: { ttlMs: 60_000 } }
73
+ );
74
+ await app.app.request(
75
+ '/api/v1/setup',
76
+ json({ setupToken: 'integration-test-setup-token', instanceName: 'Window', owner: OPERATOR })
77
+ );
78
+ cookie = await sso.ssoLogin(app, hub, {
79
+ sub: 'hub-user-window',
80
+ email: 'window@window.test',
81
+ name: 'Window User',
82
+ workspaceId: ORG,
83
+ role: 'owner',
84
+ workspaceName: 'Org Window'
85
+ });
86
+ const me = await readJson(await app.app.request('/api/v1/me', { headers: { cookie } }));
87
+ workspaceId = me.activeWorkspaceId;
88
+ }, 180_000);
89
+
90
+ afterAll(async () => {
91
+ await app?.stop();
92
+ await hub?.stop();
93
+ await container?.stop();
94
+ });
95
+
96
+ describe('cloud: an event outside the hub’s occurredAt window is held, never posted (PRDCT-2644)', () => {
97
+ it('holds the stale event before the hub sees it, counts it as a window hold, and holds it again on every round', async () => {
98
+ const text = 'The window fox';
99
+ const res = await app.app.request(`/api/v1/files?name=${encodeURIComponent('fox.txt')}`, {
100
+ method: 'POST',
101
+ headers: {
102
+ cookie,
103
+ 'x-workspace-id': workspaceId,
104
+ 'content-type': 'text/plain',
105
+ 'content-length': String(Buffer.byteLength(text)),
106
+ 'x-forwarded-for': sso.nextIp()
107
+ },
108
+ body: text
109
+ });
110
+ expect(res.status).toBe(201);
111
+ const fileId = (await readJson(res)).file.id as string;
112
+ const recorded = await until(
113
+ () => [...hub.usageEvents.values()].find((e) => e.resourceId === fileId),
114
+ (e) => e !== undefined
115
+ );
116
+ expect(recorded).toBeDefined();
117
+
118
+ // The same event, a new id, eight days old.
119
+ const { toolSlug: _stamped, ...fields } = recorded as Record<string, unknown>;
120
+ const staleId = ulid();
121
+ const stale = {
122
+ ...fields,
123
+ id: staleId,
124
+ occurredAt: new Date(Date.now() - 8 * DAY).toISOString()
125
+ } as unknown as UsageEvent;
126
+ await app.registry.usage.emit(stale);
127
+
128
+ // First round: one window hold, on both counters (the poster counts
129
+ // before the worker's send, the worker after it).
130
+ const first = await until(
131
+ () => metric('usage_events_held_total{reason="occurred_at_window"}'),
132
+ (n) => n >= 1
133
+ );
134
+ expect(first).toBe(1);
135
+ expect(await metric('usage_poster_events_total{outcome="held"}')).toBe(1);
136
+ // Never counted as a retry-budget hold, never a rejection at the hub.
137
+ expect(await metric('usage_events_held_total{reason="retry_budget"}')).toBe(0);
138
+ expect(await metric('usage_poster_events_total{outcome="rejected"}')).toBe(0);
139
+
140
+ // The held queue carries it, marked as a window hold.
141
+ const rows = await app.db.pool.query<{ id: string; singleton_key: string | null }>(
142
+ `SELECT data->>'id' AS id, singleton_key FROM pgboss.job WHERE name = 'usage-events-held' AND data->>'id' = $1`,
143
+ [staleId]
144
+ );
145
+ expect(rows.rows.length).toBeGreaterThanOrEqual(1);
146
+ expect(rows.rows[0]!.singleton_key).toBe(`occurred_at_window:${staleId}`);
147
+
148
+ // The held queue re-drives it after a second; the poster holds it again.
149
+ await until(
150
+ () => metric('usage_events_held_total{reason="occurred_at_window"}'),
151
+ (n) => n >= 2
152
+ );
153
+ expect(await metric('usage_events_held_total{reason="occurred_at_window"}')).toBeGreaterThanOrEqual(2);
154
+ expect(await metric('usage_poster_events_total{outcome="held"}')).toBeGreaterThanOrEqual(2);
155
+ expect(await metric('usage_events_held_total{reason="retry_budget"}')).toBe(0);
156
+
157
+ // The hub never saw the stale event; the upload's landed exactly once.
158
+ expect(hub.usageEvents.has(staleId)).toBe(false);
159
+ expect([...hub.usageEvents.values()].filter((e) => e.resourceId === fileId)).toHaveLength(1);
160
+ }, 120_000);
161
+ });
@@ -0,0 +1,523 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
+ import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql';
3
+ import { createDatabase, createTestApp, readJson, startPostgres, type TestApp, host } from './helpers.js';
4
+ import { FakeHub, LEGACY_GRANT_SCOPE, type HubUserFixture } from '@antasphere/chassis-server/testing';
5
+ import type { HubCreateOrgResult } from '@antasphere/chassis-server/identity';
6
+ import * as sso from './sso-helpers.js';
7
+
8
+ /**
9
+ * POST /api/v1/workspaces on the CLOUD edition (PRDCT-2443): the
10
+ * organization is created AT THE HUB as the caller — their own grant, no
11
+ * service key, nothing naming a user — and projected locally before the
12
+ * answer, which carries the LOCAL workspace id.
13
+ *
14
+ * Two apps over one FakeHub:
15
+ * - `app` runs the REAL client against the fake hub's POST /api/v1/orgs
16
+ * (the wire: bearer, body, no blind retry, the projection);
17
+ * - `scripted` fakes the hub call AT THE FUNCTION BOUNDARY the route
18
+ * depends on (`BootOverrides.hubCreateOrg`), so every verdict maps to its
19
+ * code without needing a hub that can produce it.
20
+ *
21
+ * A third app, `restricted`, boots the same cloud env with TOOL_RESTRICTED
22
+ * (PRDCT-2947): organizations that open the tool are granted by Antasphere,
23
+ * so creation refuses here before any hub call.
24
+ */
25
+
26
+ const OPERATOR = { email: 'operator@wscloud.test', name: 'Op Cloud', password: 'op-cloud-password-1234' };
27
+ const ORG_HOME = '66666666-aaaa-4bbb-8ccc-000000000001';
28
+ const ORG_ZERO = '66666666-aaaa-4bbb-8ccc-000000000002';
29
+ const ORG_SCRIPTED = '66666666-aaaa-4bbb-8ccc-000000000003';
30
+
31
+ const DIALS = {
32
+ reconcileTtlMs: 120,
33
+ reconcileStaleMaxMs: 60_000,
34
+ retryMs: 50,
35
+ orgsTimeoutMs: 1_000,
36
+ tokenTimeoutMs: 2_000
37
+ };
38
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
39
+ const expireTtl = () => sleep(DIALS.reconcileTtlMs + 60);
40
+
41
+ const CLOUD_ENV = (hub: FakeHub) => ({
42
+ EDITION: 'cloud',
43
+ HUB_ISSUER_URL: hub.issuer,
44
+ HUB_CLIENT_ID: host.hubClientId,
45
+ HUB_CLIENT_SECRET: 'integration-test-hub-secret-0001'
46
+ });
47
+
48
+ let container: StartedPostgreSqlContainer;
49
+ let hub: FakeHub;
50
+ let app: TestApp;
51
+ let scripted: TestApp;
52
+ let restricted: TestApp;
53
+ let nextScripted: HubCreateOrgResult = { kind: 'inconclusive' };
54
+ const scriptedCalls: Array<{ userId: string; name: string }> = [];
55
+
56
+ const ada: HubUserFixture = {
57
+ sub: 'hub-ada',
58
+ email: 'ada@wscloud.test',
59
+ name: 'Ada Cloud',
60
+ workspaceId: ORG_HOME,
61
+ role: 'member', // a plain member of her home org — creation is not an admin act
62
+ workspaceName: 'Home Org'
63
+ };
64
+
65
+ const create = (target: TestApp, cookie: string, name: string, headers: Record<string, string> = {}) =>
66
+ target.app.request('/api/v1/workspaces', {
67
+ method: 'POST',
68
+ headers: { 'content-type': 'application/json', cookie, ...headers },
69
+ body: JSON.stringify({ name })
70
+ });
71
+
72
+ const me = async (target: TestApp, cookie: string) =>
73
+ readJson(await target.app.request('/api/v1/me', { headers: { cookie } }));
74
+
75
+ beforeAll(async () => {
76
+ [container, hub] = await Promise.all([startPostgres(), FakeHub.start({ clientId: host.hubClientId })]);
77
+ app = await createTestApp(await createDatabase(container, 'ws_create_cloud'), CLOUD_ENV(hub), {
78
+ hubDials: DIALS
79
+ });
80
+ scripted = await createTestApp(
81
+ await createDatabase(container, 'ws_create_cloud_scripted'),
82
+ CLOUD_ENV(hub),
83
+ {
84
+ hubDials: DIALS,
85
+ hubCreateOrg: async (userId, name) => {
86
+ scriptedCalls.push({ userId, name });
87
+ return nextScripted;
88
+ }
89
+ }
90
+ );
91
+ restricted = await createTestApp(
92
+ await createDatabase(container, 'ws_create_cloud_restricted'),
93
+ { ...CLOUD_ENV(hub), TOOL_RESTRICTED: 'true' },
94
+ { hubDials: DIALS }
95
+ );
96
+ for (const target of [app, scripted, restricted]) {
97
+ const res = await target.app.request(
98
+ '/api/v1/setup',
99
+ sso.json({ setupToken: 'integration-test-setup-token', instanceName: 'Cloud', owner: OPERATOR })
100
+ );
101
+ expect(res.status).toBe(201);
102
+ }
103
+ }, 300_000);
104
+
105
+ afterAll(async () => {
106
+ await Promise.all([app?.stop(), scripted?.stop(), restricted?.stop()]);
107
+ await Promise.all([container?.stop(), hub?.stop()]);
108
+ });
109
+
110
+ beforeEach(() => {
111
+ hub.orgCreateMode = 'ok';
112
+ hub.orgsMode = 'ok';
113
+ hub.orgCreateRequests.length = 0;
114
+ });
115
+
116
+ describe('cloud: the real client against the hub', () => {
117
+ let cookie = '';
118
+
119
+ it('creates the organization AT THE HUB as the user, projects it, answers the LOCAL id', async () => {
120
+ cookie = await sso.ssoLogin(app, hub, ada);
121
+ expect((await me(app, cookie)).canCreateWorkspace).toBe(true);
122
+
123
+ const res = await create(app, cookie, ' Ada’s lab ');
124
+ expect(res.status).toBe(201);
125
+ const { workspace } = await readJson(res);
126
+ expect(workspace.name).toBe('Ada’s lab');
127
+
128
+ // The hub call: ONE POST, the user's own bearer, the name and nothing else.
129
+ expect(hub.orgCreateRequests).toHaveLength(1);
130
+ const sent = hub.orgCreateRequests[0]!;
131
+ expect(sent.auth).toMatch(/^Bearer ey/); // a user access token (JWT), never a service key
132
+ expect(sent.body).toEqual({ name: 'Ada’s lab' });
133
+ const hubOrgId = hub.orgIdsOf('hub-ada').find((id) => id !== ORG_HOME)!;
134
+ expect(hubOrgId).toBeTruthy();
135
+
136
+ // The answer is the LOCAL id — never the hub org id.
137
+ expect(workspace.id).not.toBe(hubOrgId);
138
+ const ws = await app.db.pool.query(`SELECT central_account_id, name FROM workspaces WHERE id = $1`, [
139
+ workspace.id
140
+ ]);
141
+ expect(ws.rows[0]).toEqual({ central_account_id: hubOrgId, name: 'Ada’s lab' });
142
+ const members = await app.db.pool.query(
143
+ `SELECT role, origin, is_active FROM workspace_members WHERE workspace_id = $1`,
144
+ [workspace.id]
145
+ );
146
+ expect(members.rows).toEqual([{ role: 'owner', origin: 'hub', is_active: true }]);
147
+
148
+ // Usable at once: the header selects it, hub-origin, owner.
149
+ const selected = await readJson(
150
+ await app.app.request('/api/v1/me', { headers: { cookie, 'x-workspace-id': workspace.id } })
151
+ );
152
+ expect(selected.activeWorkspaceId).toBe(workspace.id);
153
+ expect(selected.role).toBe('owner');
154
+ expect(selected.workspace.hubOrigin).toBe(true);
155
+
156
+ // Audited in the NEW workspace; the home org's trail hears nothing.
157
+ const audit = await app.db.pool.query(
158
+ `SELECT workspace_id, action, actor_via FROM audit_log WHERE action ILIKE '%workspace%'`
159
+ );
160
+ expect(audit.rows).toEqual([
161
+ { workspace_id: workspace.id, action: 'workspace.create', actor_via: 'session' }
162
+ ]);
163
+ });
164
+
165
+ it('the grant that created it CARRIED orgs:create — because the sign-in asked for it', async () => {
166
+ const { scope } = await sso.ssoInitiate(app);
167
+ expect(scope?.split(' ')).toContain('orgs:create');
168
+ expect(scope?.split(' ')).not.toContain('account:write');
169
+ });
170
+
171
+ it.each([
172
+ ['a grant from before the scope existed (account:read only)', LEGACY_GRANT_SCOPE],
173
+ ['a grant with account:write but NOT orgs:create', `${LEGACY_GRANT_SCOPE} account:write`]
174
+ ])('%s answers 401 hub_reauth_required; /me stays true; a fresh sign-in heals it', async (label, scope) => {
175
+ const sub = `hub-old-${scope.length}`;
176
+ const old: HubUserFixture = {
177
+ ...ada,
178
+ sub,
179
+ email: `${sub}@wscloud.test`,
180
+ name: 'Old Grant',
181
+ overrides: { scope }
182
+ };
183
+ const oldCookie = await sso.ssoLogin(app, hub, old);
184
+ // The entry is OFFERED: the creation is one sign-in away, and false
185
+ // would hide it from every person who signed in before this shipped.
186
+ expect((await me(app, oldCookie)).canCreateWorkspace).toBe(true);
187
+
188
+ const refreshes = hub.refreshCount();
189
+ const before = await app.db.pool.query(`SELECT count(*)::int AS n FROM workspaces`);
190
+ const res = await create(app, oldCookie, 'Too early');
191
+ expect(res.status).toBe(401);
192
+ const body = await readJson(res);
193
+ expect(body.error.code).toBe('hub_reauth_required'); // NOT hub_refused, NOT hub_grant_expired
194
+ expect(body.error.message).not.toMatch(/organi[sz]ation|hub|antasphere/i);
195
+ // One POST, no refresh (a refreshed token carries the same scopes), nothing created anywhere.
196
+ expect(hub.orgCreateRequests).toHaveLength(1);
197
+ expect(hub.refreshCount()).toBe(refreshes);
198
+ expect(hub.orgIdsOf(sub)).toEqual([ORG_HOME]);
199
+ const after = await app.db.pool.query(`SELECT count(*)::int AS n FROM workspaces`);
200
+ expect(after.rows[0].n).toBe(before.rows[0].n);
201
+ // The grant is NOT dead: the rest of the product keeps working on it.
202
+ expect((await app.app.request(host.probeRoute, { headers: { cookie: oldCookie } })).status).toBe(200);
203
+
204
+ // The cure: sign in again — the fresh grant carries what the tool now asks for.
205
+ const fresh = await sso.ssoLogin(app, hub, { ...old, overrides: undefined });
206
+ const healed = await create(app, fresh, 'Now it works');
207
+ expect(healed.status).toBe(201);
208
+ });
209
+
210
+ it('a CLI connect REPLACES the stored grant with the hub’s own (no orgs:create): reauth, then healed', async () => {
211
+ const kit: HubUserFixture = { ...ada, sub: 'hub-kit', email: 'kit@wscloud.test', name: 'Kit CLI' };
212
+ const kitCookie = await sso.ssoLogin(app, hub, kit);
213
+ const { token, hubRefreshToken } = await hub.signConnectToken(kit, 'http://localhost:3000/mcp');
214
+ const connect = await app.app.request('/api/v1/sso/cli-connect', sso.json({ token, hubRefreshToken }));
215
+ expect(connect.status).toBeLessThan(300);
216
+ await app.db.pool.query(
217
+ `UPDATE account SET access_token_expires_at = now() - interval '1 hour'
218
+ WHERE provider_id = 'antasphere' AND user_id = (SELECT id FROM "user" WHERE email = $1)`,
219
+ [kit.email]
220
+ );
221
+ const res = await create(app, kitCookie, 'After the CLI');
222
+ expect(res.status).toBe(401);
223
+ expect((await readJson(res)).error.code).toBe('hub_reauth_required');
224
+ const again = await sso.ssoLogin(app, hub, kit);
225
+ expect((await create(app, again, 'After the browser')).status).toBe(201);
226
+ });
227
+
228
+ it('the hub cap answers 403 workspace_limit_reached and projects nothing', async () => {
229
+ hub.orgCreateMode = 'limit';
230
+ const before = await app.db.pool.query(`SELECT count(*)::int AS n FROM workspaces`);
231
+ const res = await create(app, cookie, 'One too many');
232
+ expect(res.status).toBe(403);
233
+ expect((await readJson(res)).error.code).toBe('workspace_limit_reached');
234
+ expect(hub.orgCreateRequests).toHaveLength(1);
235
+ const after = await app.db.pool.query(`SELECT count(*)::int AS n FROM workspaces`);
236
+ expect(after.rows[0].n).toBe(before.rows[0].n);
237
+ });
238
+
239
+ it('a hub that does not open creation to grants answers 403 hub_refused — one POST, no refresh storm', async () => {
240
+ hub.orgCreateMode = 'forbidden';
241
+ const refreshes = hub.refreshCount();
242
+ const res = await create(app, cookie, 'Refused');
243
+ expect(res.status).toBe(403);
244
+ expect((await readJson(res)).error.code).toBe('hub_refused');
245
+ expect(hub.orgCreateRequests).toHaveLength(1);
246
+ expect(hub.refreshCount()).toBe(refreshes);
247
+ });
248
+
249
+ it('a hub 5xx or a dropped connection answers 403 hub_unavailable and is NEVER re-posted', async () => {
250
+ for (const mode of ['http500', 'network'] as const) {
251
+ hub.orgCreateMode = mode;
252
+ hub.orgCreateRequests.length = 0;
253
+ const res = await create(app, cookie, `Down ${mode}`);
254
+ expect(res.status).toBe(403);
255
+ expect((await readJson(res)).error.code).toBe('hub_unavailable');
256
+ expect(hub.orgCreateRequests).toHaveLength(1);
257
+ }
258
+ });
259
+
260
+ it('an organization the hub COMMITTED before failing is not duplicated — the next pass projects it', async () => {
261
+ hub.orgCreateMode = 'commit_then_500';
262
+ const res = await create(app, cookie, 'Committed anyway');
263
+ expect(res.status).toBe(403);
264
+ expect((await readJson(res)).error.code).toBe('hub_unavailable');
265
+ expect(hub.orgCreateRequests).toHaveLength(1);
266
+ await expireTtl();
267
+ const names = ((await me(app, cookie)).workspaces as Array<{ name: string }>).map((w) => w.name);
268
+ expect(names.filter((n) => n === 'Committed anyway')).toHaveLength(1);
269
+ });
270
+
271
+ it('machine credentials are refused on cloud too (fail-closed allowlist)', async () => {
272
+ const minted = await readJson(
273
+ await app.app.request('/api/v1/api-keys', {
274
+ method: 'POST',
275
+ headers: { 'content-type': 'application/json', cookie },
276
+ body: JSON.stringify({ name: 'k', scopes: [host.scopes.read, host.scopes.write] })
277
+ })
278
+ );
279
+ const res = await create(app, '', 'By key', { authorization: `Bearer ${minted.key}` });
280
+ expect(res.status).toBe(403);
281
+ expect((await readJson(res)).error.code).toBe('endpoint_not_allowed');
282
+ expect(hub.orgCreateRequests).toHaveLength(0);
283
+ });
284
+
285
+ it('an EXPIRED grant answers the existing verdict, 401 hub_grant_expired — nothing reaches the hub', async () => {
286
+ const bea: HubUserFixture = { ...ada, sub: 'hub-bea', email: 'bea@wscloud.test', name: 'Bea' };
287
+ const beaCookie = await sso.ssoLogin(app, hub, bea);
288
+ hub.revokeGrants('hub-bea');
289
+ // Age the stored access token so the next grant read must refresh — and
290
+ // the refresh discovers the dead family (invalid_grant).
291
+ await app.db.pool.query(
292
+ `UPDATE account SET access_token_expires_at = now() - interval '1 hour'
293
+ WHERE provider_id = 'antasphere' AND user_id = (SELECT id FROM "user" WHERE email = $1)`,
294
+ [bea.email]
295
+ );
296
+ await expireTtl();
297
+ const res = await create(app, beaCookie, 'Too late');
298
+ // Whichever door answers first (the live gate's reconcile, or the route's
299
+ // own grant read), the verdict is the one existing verdict.
300
+ expect(res.status).toBe(401);
301
+ expect((await readJson(res)).error.code).toBe('hub_grant_expired');
302
+ expect(hub.orgIdsOf('hub-bea')).toEqual([ORG_HOME]);
303
+ });
304
+
305
+ it('a break-glass operator with no hub link is told so: 403 hub_link_required, /me false', async () => {
306
+ await sso.seedLocalWorkspace(app, 'Operator local', OPERATOR.email);
307
+ const signIn = await app.app.request(
308
+ '/api/v1/auth/sign-in/email',
309
+ sso.json({ email: OPERATOR.email, password: OPERATOR.password })
310
+ );
311
+ expect(signIn.status).toBe(200);
312
+ const { extractCookie } = await import('./helpers.js');
313
+ const opCookie = extractCookie(signIn);
314
+ expect((await me(app, opCookie)).canCreateWorkspace).toBe(false);
315
+ const res = await create(app, opCookie, 'Local on cloud');
316
+ expect(res.status).toBe(403);
317
+ expect((await readJson(res)).error.code).toBe('hub_link_required');
318
+ expect(hub.orgCreateRequests).toHaveLength(0);
319
+ });
320
+
321
+ it('the ZERO-membership session creates its next organization from the zero state', async () => {
322
+ const zed: HubUserFixture = {
323
+ sub: 'hub-zed',
324
+ email: 'zed@wscloud.test',
325
+ name: 'Zed',
326
+ workspaceId: ORG_ZERO,
327
+ role: 'owner',
328
+ workspaceName: 'Zed Org'
329
+ };
330
+ const zedCookie = await sso.ssoLogin(app, hub, zed);
331
+ hub.clearUserOrgs('hub-zed');
332
+ await expireTtl();
333
+ await app.app.request(host.probeRoute, { headers: { cookie: zedCookie } }); // the sweeping request
334
+ const zero = await me(app, zedCookie);
335
+ expect(zero.workspaces).toEqual([]);
336
+ expect(zero.canCreateWorkspace).toBe(true);
337
+
338
+ // Naming a workspace while holding none keeps the fail-closed 401.
339
+ const named = await create(app, zedCookie, 'Named', { 'x-workspace-id': ORG_ZERO });
340
+ expect(named.status).toBe(401);
341
+
342
+ const res = await create(app, zedCookie, 'Zed again');
343
+ expect(res.status).toBe(201);
344
+ const after = await me(app, zedCookie);
345
+ expect(after.workspaces.map((w: { name: string }) => w.name)).toEqual(['Zed again']);
346
+ expect(after.role).toBe('owner');
347
+ });
348
+ });
349
+
350
+ describe('cloud: the hub call faked at the function boundary', () => {
351
+ let cookie = '';
352
+ let userId = '';
353
+ const cy: HubUserFixture = {
354
+ sub: 'hub-cy',
355
+ email: 'cy@wscloud.test',
356
+ name: 'Cy Scripted',
357
+ workspaceId: ORG_HOME,
358
+ role: 'admin',
359
+ workspaceName: 'Home Org'
360
+ };
361
+
362
+ beforeAll(async () => {
363
+ cookie = await sso.ssoLogin(scripted, hub, cy);
364
+ userId = (await me(scripted, cookie)).user.id;
365
+ });
366
+
367
+ beforeEach(() => {
368
+ scriptedCalls.length = 0;
369
+ });
370
+
371
+ it('calls it with the CALLER and the trimmed name — there is no other parameter', async () => {
372
+ nextScripted = { kind: 'limit_reached' };
373
+ await create(scripted, cookie, ' Trim me ');
374
+ expect(scriptedCalls).toEqual([{ userId, name: 'Trim me' }]);
375
+ });
376
+
377
+ it('HOSTILE input cannot steer it: headers and body fields naming another user or another name are ignored', async () => {
378
+ const victim = await scripted.db.pool.query(`SELECT id FROM "user" WHERE email = $1`, [OPERATOR.email]);
379
+ const victimId = victim.rows[0].id as string;
380
+ expect(victimId).not.toBe(userId);
381
+ nextScripted = { kind: 'limit_reached' };
382
+ const res = await scripted.app.request('/api/v1/workspaces', {
383
+ method: 'POST',
384
+ headers: {
385
+ 'content-type': 'application/json',
386
+ cookie,
387
+ 'x-target-user': victimId,
388
+ 'x-target-name': 'Steered by header',
389
+ 'x-user-id': victimId,
390
+ 'x-owner-id': victimId
391
+ },
392
+ body: JSON.stringify({
393
+ name: ' Mine ',
394
+ ownerId: victimId,
395
+ userId: victimId,
396
+ owner: victimId,
397
+ ownerUserId: victimId,
398
+ targetUser: victimId,
399
+ org: { name: 'Steered by body', ownerId: victimId }
400
+ })
401
+ });
402
+ expect(res.status).toBe(403); // the scripted cap: the call was made
403
+ // The call's arguments: the SESSION's user and the validated, trimmed name. Nothing else exists.
404
+ expect(scriptedCalls).toEqual([{ userId, name: 'Mine' }]);
405
+ expect(JSON.stringify(scriptedCalls)).not.toContain(victimId);
406
+ expect(JSON.stringify(scriptedCalls)).not.toContain('Steered');
407
+ });
408
+
409
+ it.each([
410
+ [{ kind: 'limit_reached' }, 403, 'workspace_limit_reached'],
411
+ [{ kind: 'reauth_required' }, 401, 'hub_reauth_required'],
412
+ [{ kind: 'refused', status: 403, code: 'forbidden' }, 403, 'hub_refused'],
413
+ [{ kind: 'refused', status: 409, code: 'whatever' }, 403, 'hub_refused'],
414
+ [{ kind: 'invalid' }, 400, 'validation_error'],
415
+ [{ kind: 'inconclusive' }, 403, 'hub_unavailable'],
416
+ [{ kind: 'grant_dead' }, 401, 'hub_grant_expired'],
417
+ [{ kind: 'no_link' }, 403, 'hub_link_required']
418
+ ] as Array<[HubCreateOrgResult, number, string]>)(
419
+ '%j → %i %s, nothing projected',
420
+ async (result, status, code) => {
421
+ nextScripted = result;
422
+ const before = await scripted.db.pool.query(`SELECT count(*)::int AS n FROM workspaces`);
423
+ const res = await create(scripted, cookie, 'Scripted');
424
+ expect(res.status).toBe(status);
425
+ expect((await readJson(res)).error.code).toBe(code);
426
+ const after = await scripted.db.pool.query(`SELECT count(*)::int AS n FROM workspaces`);
427
+ expect(after.rows[0].n).toBe(before.rows[0].n);
428
+ const audit = await scripted.db.pool.query(
429
+ `SELECT count(*)::int AS n FROM audit_log WHERE action = 'workspace.create'`
430
+ );
431
+ expect(audit.rows[0].n).toBe(0);
432
+ }
433
+ );
434
+
435
+ it('201: the forced reconcile projects the org the hub now lists; the answer is the local id', async () => {
436
+ hub.setUserOrg('hub-cy', ORG_SCRIPTED, { name: 'Scripted Org', role: 'owner' });
437
+ nextScripted = { kind: 'created', org: { id: ORG_SCRIPTED, name: 'Scripted Org', role: 'owner' } };
438
+ const res = await create(scripted, cookie, 'Scripted Org');
439
+ expect(res.status).toBe(201);
440
+ const { workspace } = await readJson(res);
441
+ const row = await scripted.db.pool.query(
442
+ `SELECT w.central_account_id, m.role, m.origin FROM workspaces w
443
+ JOIN workspace_members m ON m.workspace_id = w.id WHERE w.id = $1`,
444
+ [workspace.id]
445
+ );
446
+ expect(row.rows).toEqual([{ central_account_id: ORG_SCRIPTED, role: 'owner', origin: 'hub' }]);
447
+ });
448
+
449
+ it('201 even when the pass right after the creation blips: projected from the hub’s own 201, as hub-origin', async () => {
450
+ const orgId = '66666666-aaaa-4bbb-8ccc-000000000004';
451
+ hub.setUserOrg('hub-cy', orgId, { name: 'Blip Org', role: 'owner' });
452
+ hub.orgsMode = 'http500';
453
+ nextScripted = { kind: 'created', org: { id: orgId, name: 'Blip Org', role: 'owner' } };
454
+ const res = await create(scripted, cookie, 'Blip Org');
455
+ expect(res.status).toBe(201);
456
+ const { workspace } = await readJson(res);
457
+ const row = await scripted.db.pool.query(
458
+ `SELECT w.central_account_id, w.name, m.role, m.origin, m.is_active FROM workspaces w
459
+ JOIN workspace_members m ON m.workspace_id = w.id WHERE w.id = $1`,
460
+ [workspace.id]
461
+ );
462
+ expect(row.rows).toEqual([
463
+ { central_account_id: orgId, name: 'Blip Org', role: 'owner', origin: 'hub', is_active: true }
464
+ ]);
465
+ // The hub recovers: the next pass KEEPS it (the hub lists it) — the
466
+ // fallback wrote nothing the reconcile would not have.
467
+ hub.orgsMode = 'ok';
468
+ await expireTtl();
469
+ const names = ((await me(scripted, cookie)).workspaces as Array<{ name: string }>).map((w) => w.name);
470
+ expect(names).toContain('Blip Org');
471
+ });
472
+
473
+ it('a 201 the hub does NOT back is swept by the next pass — the 201 is never a standing grant', async () => {
474
+ const ghost = '66666666-aaaa-4bbb-8ccc-000000000005';
475
+ hub.orgsMode = 'http500';
476
+ nextScripted = { kind: 'created', org: { id: ghost, name: 'Ghost', role: 'owner' } };
477
+ expect((await create(scripted, cookie, 'Ghost')).status).toBe(201);
478
+ hub.orgsMode = 'ok'; // the hub's list never carried it
479
+ await expireTtl();
480
+ const names = ((await me(scripted, cookie)).workspaces as Array<{ name: string }>).map((w) => w.name);
481
+ expect(names).not.toContain('Ghost');
482
+ });
483
+
484
+ it('a 201 without a usable role is not projected by the fallback: 403 hub_unavailable', async () => {
485
+ hub.orgsMode = 'http500';
486
+ nextScripted = {
487
+ kind: 'created',
488
+ org: { id: '66666666-aaaa-4bbb-8ccc-000000000006', name: 'Roleless', role: null }
489
+ };
490
+ const res = await create(scripted, cookie, 'Roleless');
491
+ expect(res.status).toBe(403);
492
+ expect((await readJson(res)).error.code).toBe('hub_unavailable');
493
+ });
494
+ });
495
+
496
+ describe('cloud: a restricted tool (TOOL_RESTRICTED)', () => {
497
+ let cookie = '';
498
+
499
+ it('discovery says restricted: true on the restricted instance, and carries no key at all otherwise', async () => {
500
+ const onRestricted = await readJson(await restricted.app.request('/api/v1/instance'));
501
+ expect(onRestricted.restricted).toBe(true);
502
+ const onPlain = await readJson(await app.app.request('/api/v1/instance'));
503
+ expect('restricted' in onPlain).toBe(false);
504
+ });
505
+
506
+ it('/me.canCreateWorkspace is false for a hub-linked person', async () => {
507
+ cookie = await sso.ssoLogin(restricted, hub, ada);
508
+ expect((await me(restricted, cookie)).canCreateWorkspace).toBe(false);
509
+ });
510
+
511
+ it('POST /workspaces answers 403 restricted_tool naming the tool, and never calls the hub', async () => {
512
+ const res = await create(restricted, cookie, 'Not here');
513
+ expect(res.status).toBe(403);
514
+ const body = await readJson(res);
515
+ expect(body.error.code).toBe('restricted_tool');
516
+ expect(body.error.message).toBe(
517
+ `Organizations that open ${host.identity.displayName} are granted by Antasphere; ask Antasphere to open it for yours`
518
+ );
519
+ expect(hub.orgCreateRequests).toHaveLength(0);
520
+ const { rows } = await restricted.db.pool.query(`SELECT 1 FROM workspaces WHERE name = 'Not here'`);
521
+ expect(rows).toHaveLength(0);
522
+ });
523
+ });