@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,693 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql';
3
+ import type { UsageEvent } from '@antasphere/chassis-contract';
4
+ import { HubCreditCheck, HubMachineToken } from '@antasphere/chassis-server';
5
+ import type { Logger } from '@antasphere/chassis-server/logger';
6
+ import { FakeHub, type HubUserFixture } from '@antasphere/chassis-server/testing';
7
+ import {
8
+ createDatabase,
9
+ createTestApp,
10
+ extractCookie,
11
+ readJson,
12
+ startPostgres,
13
+ type TestApp,
14
+ host
15
+ } from './helpers.js';
16
+ import * as sso from './sso-helpers.js';
17
+
18
+ /**
19
+ * The billing rail's phase 1 on the chassis (PRDCT-2626), end to end on the
20
+ * host's one metered route, the generic files upload, against the stub hub
21
+ * that speaks lane A's wire contract (PRDCT-2625):
22
+ *
23
+ * - oss: discovery shows the declared slot with the operator's cap as the
24
+ * oss and free values; an upload under the cap answers as before; one
25
+ * over the declared size answers 413 entitlement_denied with today's
26
+ * message, byte for byte; no event leaves the instance (the downstream
27
+ * is asked nothing).
28
+ * - cloud: the operator's cloud-local workspace is unmetered; a hub user's
29
+ * upload lands in the hub's usage_events per user and per organization,
30
+ * with the hub user's sub, the via of each credential, the file it
31
+ * created, and the machine token minted once for the whole run; a plan
32
+ * refusal answers 403 plan_required with the upgrade link and posts
33
+ * nothing; a 404 from an older hub is retried until it lands, never
34
+ * lost; a dead machine token is minted again once.
35
+ * - cloud, phase 2 (PRDCT-2664): a priced action is checked at the hub
36
+ * with the machine token and debited at ingest; a short balance is 402
37
+ * entitlement_denied with the hub's top-up link and posts nothing; the
38
+ * plan limit, not the instance cap, is a metered account's ceiling
39
+ * (PRDCT-2653) and its refusal links the hub's upgrade page with the key
40
+ * and the required plan; an upload with no declared size is 411
41
+ * (PRDCT-2652); a hub that does not answer the check fails open, on
42
+ * /metrics; a price past `Number.MAX_SAFE_INTEGER` reaches the check as
43
+ * the hub's `unpriceable` refusal, never an outage (PRDCT-2677).
44
+ *
45
+ * The suite runs twice (the minimal host, then the tool's own composition):
46
+ * what the two declare differently (the free upload value) is read from
47
+ * discovery, never assumed.
48
+ */
49
+
50
+ const OWNER = { email: 'owner@ent.test', name: 'Ent Owner', password: 'ent-owner-password-1' };
51
+ const OPERATOR = { email: 'operator@ent.test', name: 'Operator', password: 'operator-ent-pass-1' };
52
+ const ORG_A = '77777777-aaaa-4bbb-8ccc-00000000e001';
53
+ const ORG_B = '77777777-aaaa-4bbb-8ccc-00000000e002';
54
+ const HUB_SECRET = 'integration-test-hub-secret-ent';
55
+ const PAYLOAD = 'The quick brown fox jumps over the lazy dog'; // 43 bytes
56
+ const MB = 1024 * 1024;
57
+
58
+ let container: StartedPostgreSqlContainer;
59
+
60
+ const json = (body: unknown, headers: Record<string, string> = {}) => ({
61
+ method: 'POST',
62
+ headers: { 'content-type': 'application/json', 'x-forwarded-for': sso.nextIp(), ...headers },
63
+ body: JSON.stringify(body)
64
+ });
65
+
66
+ /** An upload as a client sends it on the wire: the bytes with their Content-Length (a header passed in wins). */
67
+ function upload(app: TestApp, headers: Record<string, string>, text: string) {
68
+ return app.app.request(`/api/v1/files?name=${encodeURIComponent('fox.txt')}`, {
69
+ method: 'POST',
70
+ headers: {
71
+ 'content-type': 'text/plain',
72
+ 'content-length': String(Buffer.byteLength(text)),
73
+ 'x-forwarded-for': sso.nextIp(),
74
+ ...headers
75
+ },
76
+ body: text
77
+ });
78
+ }
79
+
80
+ const landed = (hub: FakeHub, fileId: string) =>
81
+ [...hub.usageEvents.values()].some((e) => e.resourceId === fileId);
82
+
83
+ async function until<T>(read: () => T | Promise<T>, ok: (v: T) => boolean, ms = 20_000): Promise<T> {
84
+ const deadline = Date.now() + ms;
85
+ for (;;) {
86
+ const v = await read();
87
+ if (ok(v) || Date.now() > deadline) return v;
88
+ await new Promise((r) => setTimeout(r, 100));
89
+ }
90
+ }
91
+
92
+ beforeAll(async () => {
93
+ container = await startPostgres();
94
+ });
95
+
96
+ afterAll(async () => {
97
+ await container?.stop();
98
+ });
99
+
100
+ describe('oss: unmetered by construction, the cap byte for byte', () => {
101
+ let app: TestApp;
102
+ let cookie: string;
103
+ const downstream: UsageEvent[] = [];
104
+
105
+ beforeAll(async () => {
106
+ app = await createTestApp(
107
+ await createDatabase(container, 'ent_oss'),
108
+ { MAX_FILE_SIZE_MB: '1' },
109
+ { usageDownstream: { emit: async (e) => void downstream.push(e) } }
110
+ );
111
+ await app.app.request(
112
+ '/api/v1/setup',
113
+ json({ setupToken: 'integration-test-setup-token', instanceName: 'Ent', owner: OWNER })
114
+ );
115
+ cookie = extractCookie(
116
+ await app.app.request(
117
+ '/api/v1/auth/sign-in/email',
118
+ json({ email: OWNER.email, password: OWNER.password })
119
+ )
120
+ );
121
+ });
122
+
123
+ afterAll(async () => {
124
+ await app?.stop();
125
+ });
126
+
127
+ it('discovery shows the declared slot: the upload action, the cap as the oss and pro values, the free value at or below it', async () => {
128
+ const info = await readJson(await app.app.request('/api/v1/instance'));
129
+ expect(info.entitlements.actions).toEqual(
130
+ expect.arrayContaining([expect.objectContaining({ key: 'files.upload', unit: 'bytes' })])
131
+ );
132
+ // What discovery advertises is what the instance serves (PRDCT-2653):
133
+ // the paid tier's value is the operator's cap, like the oss value.
134
+ expect(info.entitlements.limits['files.maxBytes']).toMatchObject({ oss: MB, pro: MB });
135
+ // The minimal host declares free as a fifth of the cap; a tool may
136
+ // declare 100 MB or the cap when smaller (the cap, at 1 MiB).
137
+ expect([Math.floor(MB / 5), MB]).toContain(info.entitlements.limits['files.maxBytes'].free);
138
+ });
139
+
140
+ it('an upload under the cap answers 201 and no event leaves the instance', async () => {
141
+ const res = await upload(app, { cookie }, PAYLOAD);
142
+ expect(res.status).toBe(201);
143
+ expect((await readJson(res)).file.sizeBytes).toBe(43);
144
+ await new Promise((r) => setTimeout(r, 2_500)); // longer than the queue's poll
145
+ expect(downstream).toEqual([]);
146
+ });
147
+
148
+ it('an upload over the declared size answers 413 entitlement_denied with today’s message', async () => {
149
+ const res = upload(app, { cookie, 'content-length': String(MB + 1) }, 'x'.repeat(MB + 1));
150
+ const out = await res;
151
+ expect(out.status).toBe(413);
152
+ expect(await readJson(out)).toEqual({
153
+ error: { code: 'entitlement_denied', message: 'file exceeds MAX_FILE_SIZE_MB (1MB)' }
154
+ });
155
+ });
156
+ });
157
+
158
+ describe('cloud: every metered action of a hub organization lands in the hub', () => {
159
+ let hub: FakeHub;
160
+ let app: TestApp;
161
+ let operatorCookie: string;
162
+ let hubUserCookie: string;
163
+ let hubUserId: string;
164
+ let projectedWorkspaceId: string;
165
+ let projectedKey: string;
166
+ const METRICS_TOKEN = 'integration-metrics-token-ent';
167
+
168
+ const metric = async (name: string): Promise<number> => {
169
+ const res = await app.app.request('/metrics', { headers: { authorization: `Bearer ${METRICS_TOKEN}` } });
170
+ expect(res.status).toBe(200);
171
+ const line = (await res.text())
172
+ .split('\n')
173
+ .find((l) => l.startsWith(`${name} `) || l.startsWith(`${name}{`));
174
+ return line ? Number(line.split(' ').at(-1)) : 0;
175
+ };
176
+
177
+ const hubUser: HubUserFixture = {
178
+ sub: 'hub-user-ent',
179
+ email: 'hub-user@ent.test',
180
+ name: 'Hub User',
181
+ workspaceId: ORG_A,
182
+ role: 'owner',
183
+ workspaceName: 'Org A'
184
+ };
185
+ const hubUserB: HubUserFixture = {
186
+ sub: 'hub-user-ent-b',
187
+ email: 'hub-user-b@ent.test',
188
+ name: 'Hub User B',
189
+ workspaceId: ORG_B,
190
+ role: 'owner',
191
+ workspaceName: 'Org B'
192
+ };
193
+
194
+ beforeAll(async () => {
195
+ hub = await FakeHub.start({ clientId: host.hubClientId, clientSecret: HUB_SECRET });
196
+ app = await createTestApp(
197
+ await createDatabase(container, 'ent_cloud'),
198
+ {
199
+ EDITION: 'cloud',
200
+ HUB_ISSUER_URL: hub.issuer,
201
+ HUB_CLIENT_ID: host.hubClientId,
202
+ HUB_CLIENT_SECRET: HUB_SECRET,
203
+ MAX_FILE_SIZE_MB: '1',
204
+ METRICS_TOKEN
205
+ },
206
+ {
207
+ usageRetry: { limit: 4, delaySeconds: 1 },
208
+ entitlementDials: { ttlMs: 60_000 },
209
+ // Every metered request asks the hub's check (no reuse), so each
210
+ // test below sees its own check at the fake.
211
+ // No cache and no hold after a failed call: every upload reaches the
212
+ // fake's check, and the outage case heals on the very next request.
213
+ entitlementCheckDials: { allowTtlMs: 0, denyTtlMs: 0, outageHoldMs: 0 }
214
+ }
215
+ );
216
+ await app.app.request(
217
+ '/api/v1/setup',
218
+ json({ setupToken: 'integration-test-setup-token', instanceName: 'Ent Cloud', owner: OPERATOR })
219
+ );
220
+ await sso.seedLocalWorkspace(app, 'Ent Cloud', OPERATOR.email);
221
+ operatorCookie = extractCookie(
222
+ await app.app.request(
223
+ '/api/v1/auth/sign-in/email',
224
+ json({ email: OPERATOR.email, password: OPERATOR.password })
225
+ )
226
+ );
227
+ hubUserCookie = await sso.ssoLogin(app, hub, hubUser);
228
+ const me = await readJson(await app.app.request('/api/v1/me', { headers: { cookie: hubUserCookie } }));
229
+ projectedWorkspaceId = me.activeWorkspaceId;
230
+ hubUserId = me.user.id;
231
+ expect(me.workspace.hubOrigin).toBe(true);
232
+ const minted = await readJson(
233
+ await app.app.request(
234
+ '/api/v1/api-keys',
235
+ json(
236
+ {
237
+ name: 'ent-key',
238
+ scopes: [host.scopes.read, host.scopes.write],
239
+ workspaceId: projectedWorkspaceId
240
+ },
241
+ { cookie: hubUserCookie, 'x-workspace-id': projectedWorkspaceId }
242
+ )
243
+ )
244
+ );
245
+ projectedKey = minted.key;
246
+ }, 180_000);
247
+
248
+ afterAll(async () => {
249
+ await app?.stop();
250
+ await hub?.stop();
251
+ });
252
+
253
+ it('the operator’s cloud-local workspace is unmetered: no hub read, no event', async () => {
254
+ const res = await upload(app, { cookie: operatorCookie }, PAYLOAD);
255
+ expect(res.status).toBe(201);
256
+ expect(hub.entitlementsRequests).toEqual([]);
257
+ await new Promise((r) => setTimeout(r, 2_500));
258
+ expect(hub.usageEvents.size).toBe(0);
259
+ expect(hub.usageRequests).toEqual([]);
260
+ });
261
+
262
+ it('a hub user’s upload lands in the hub: the hub sub, via session, the file it created, one machine token', async () => {
263
+ const res = await upload(app, { cookie: hubUserCookie, 'x-workspace-id': projectedWorkspaceId }, PAYLOAD);
264
+ expect(res.status).toBe(201);
265
+ const fileId = (await readJson(res)).file.id as string;
266
+ await until(
267
+ () => hub.usageEvents.size,
268
+ (n) => n >= 1
269
+ );
270
+ expect(hub.usageEvents.size).toBe(1);
271
+ const [event] = [...hub.usageEvents.values()];
272
+ expect(event).toMatchObject({
273
+ actionKey: 'files.upload',
274
+ meter: 'files.upload',
275
+ quantity: 43,
276
+ unit: 'bytes',
277
+ workspaceId: projectedWorkspaceId,
278
+ accountRef: ORG_A,
279
+ userId: hubUser.sub,
280
+ via: 'session',
281
+ resourceType: 'file',
282
+ resourceId: fileId,
283
+ // The hub records the token's registry slug; the body never named the tool (PRDCT-2629).
284
+ toolSlug: hub.toolSlug,
285
+ source: { edition: 'cloud' }
286
+ });
287
+ expect(hub.toolSlug).not.toBe(host.identity.slug);
288
+ const posted = (hub.usageRequests[0]!.body as { events: Array<Record<string, unknown>> }).events;
289
+ expect(posted.every((e) => !('toolSlug' in e))).toBe(true);
290
+ expect(event!.userId).not.toBe(hubUserId); // the hub's sub, never the tool's local id
291
+ // The machine channel: one client_credentials token, minted once, on every hub call.
292
+ expect(hub.machineTokenMints).toBe(1);
293
+ expect(hub.usageRequests[0]!.auth).toMatch(/^Bearer mach_/);
294
+ expect(hub.entitlementsRequests).toEqual([
295
+ { accountRef: ORG_A, auth: expect.stringMatching(/^Bearer mach_/) }
296
+ ]);
297
+ });
298
+
299
+ it('the same action through an API key reports via api_key, and the plan is read once per TTL', async () => {
300
+ const res = await upload(app, { authorization: `Bearer ${projectedKey}` }, PAYLOAD + '!');
301
+ expect(res.status).toBe(201);
302
+ await until(
303
+ () => hub.usageEvents.size,
304
+ (n) => n >= 2
305
+ );
306
+ const events = [...hub.usageEvents.values()];
307
+ expect(events.map((e) => e.via).sort()).toEqual(['api_key', 'session']);
308
+ expect(events.every((e) => e.userId === hubUser.sub && e.accountRef === ORG_A)).toBe(true);
309
+ expect(hub.entitlementsRequests).toHaveLength(1);
310
+ expect(hub.machineTokenMints).toBe(1);
311
+ });
312
+
313
+ it('a plan refusal answers 403 plan_required with the upgrade link, the hub’s organization page, and posts nothing', async () => {
314
+ hub.setEntitlements(ORG_B, { plan: 'free', limits: { 'files.maxBytes': 10 } });
315
+ const cookieB = await sso.ssoLogin(app, hub, hubUserB);
316
+ const meB = await readJson(await app.app.request('/api/v1/me', { headers: { cookie: cookieB } }));
317
+ const before = hub.usageRequests.length;
318
+ const res = await upload(app, { cookie: cookieB, 'x-workspace-id': meB.activeWorkspaceId }, PAYLOAD);
319
+ expect(res.status).toBe(403);
320
+ const body = await readJson(res);
321
+ expect(body.error.code).toBe('plan_required');
322
+ // The hub's upgrade page for the organization, the key and the required plan appended.
323
+ expect(body.error.details).toEqual({
324
+ key: 'files.maxBytes',
325
+ plan: 'free',
326
+ requiredPlan: 'pro',
327
+ upgradeUrl: `${hub.issuer}/billing/upgrade?org=${ORG_B}&tool=${hub.toolSlug}&plan=free&key=files.maxBytes&requiredPlan=pro`
328
+ });
329
+ await new Promise((r) => setTimeout(r, 2_500));
330
+ expect(hub.usageRequests).toHaveLength(before);
331
+ expect(hub.usageEvents.size).toBe(2);
332
+ });
333
+
334
+ it('an older hub (404) is retried until the ingest exists; nothing is lost', async () => {
335
+ hub.usageMode = 'http404';
336
+ const posts = hub.usageRequests.length;
337
+ const res = await upload(
338
+ app,
339
+ { cookie: hubUserCookie, 'x-workspace-id': projectedWorkspaceId },
340
+ PAYLOAD + '??'
341
+ );
342
+ expect(res.status).toBe(201);
343
+ const fileId = (await readJson(res)).file.id as string;
344
+ await until(
345
+ () => hub.usageRequests.length,
346
+ (n) => n > posts
347
+ );
348
+ expect(hub.usageEvents.size).toBe(2);
349
+ hub.usageMode = 'ok';
350
+ await until(
351
+ () => landed(hub, fileId),
352
+ (ok) => ok,
353
+ 30_000
354
+ );
355
+ const event = [...hub.usageEvents.values()].find((e) => e.resourceId === fileId);
356
+ expect(event).toMatchObject({ actionKey: 'files.upload', quantity: 45 });
357
+ }, 60_000);
358
+
359
+ it('a dead machine token is minted again once and the batch lands', async () => {
360
+ hub.usageMode = 'http401_once';
361
+ const mints = hub.machineTokenMints;
362
+ const res = await upload(app, { authorization: `Bearer ${projectedKey}` }, PAYLOAD + '???');
363
+ expect(res.status).toBe(201);
364
+ const fileId = (await readJson(res)).file.id as string;
365
+ await until(
366
+ () => landed(hub, fileId),
367
+ (ok) => ok,
368
+ 30_000
369
+ );
370
+ const seen = hub.usageRequests.map((r) => ({
371
+ auth: r.auth?.slice(0, 12),
372
+ files: (r.body as { events?: Array<{ resourceId?: string }> } | null)?.events?.map((e) => e.resourceId)
373
+ }));
374
+ expect(landed(hub, fileId), JSON.stringify({ fileId, seen, mints: hub.machineTokenMints })).toBe(true);
375
+ expect(hub.machineTokenMints).toBe(mints + 1);
376
+ }, 60_000);
377
+
378
+ it('the fake hub judges a body as the real hub does (PRDCT-2629): a stamped slug is tool_mismatch, a non-uuid account is invalid_event, a long unit is invalid_event', async () => {
379
+ const mint = await fetch(`${hub.issuer}/api/v1/auth/oauth2/token`, {
380
+ method: 'POST',
381
+ headers: {
382
+ 'content-type': 'application/x-www-form-urlencoded',
383
+ authorization: `Basic ${Buffer.from(`${host.hubClientId}:${HUB_SECRET}`).toString('base64')}`
384
+ },
385
+ body: new URLSearchParams({
386
+ grant_type: 'client_credentials',
387
+ scope: 'usage:write',
388
+ resource: hub.apiResource
389
+ })
390
+ });
391
+ const { access_token } = (await mint.json()) as { access_token: string };
392
+ const base = [...hub.usageEvents.values()][0]!;
393
+ const post = async (events: unknown[]) => {
394
+ const res = await fetch(`${hub.issuer}/api/v1/usage/events`, {
395
+ method: 'POST',
396
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${access_token}` },
397
+ body: JSON.stringify({ events })
398
+ });
399
+ expect(res.status).toBe(200);
400
+ return (
401
+ (await res.json()) as { results: Array<{ id: string | null; status: string; reason?: string }> }
402
+ ).results;
403
+ };
404
+ // The registry slug is the client id without its prefix — under either host.
405
+ expect(hub.toolSlug).toBe(host.hubClientId.replace(/^tool-/, ''));
406
+ const [stamped, own, notUuid, longUnit] = await post([
407
+ { ...base, id: '01JZZZZZZZZZZZZZZZZZZZZZZ1', toolSlug: 'not-the-registry-slug' },
408
+ { ...base, id: '01JZZZZZZZZZZZZZZZZZZZZZZ2', toolSlug: hub.toolSlug },
409
+ { ...base, id: '01JZZZZZZZZZZZZZZZZZZZZZZ3', accountRef: 'acct-1' },
410
+ { ...base, id: '01JZZZZZZZZZZZZZZZZZZZZZZ4', unit: 'x'.repeat(41) }
411
+ ]);
412
+ expect(stamped).toMatchObject({ status: 'rejected', reason: 'tool_mismatch' });
413
+ expect(own).toMatchObject({ status: 'accepted' });
414
+ expect(notUuid).toMatchObject({ status: 'rejected', reason: 'invalid_event' });
415
+ expect(longUnit).toMatchObject({ status: 'rejected', reason: 'invalid_event' });
416
+ // And the plan read of a non-uuid account is 400, as the real hub's query validator answers.
417
+ const bad = await fetch(`${hub.issuer}/api/v1/usage/entitlements?accountRef=acct-1`, {
418
+ headers: { authorization: `Bearer ${access_token}` }
419
+ });
420
+ expect(bad.status).toBe(400);
421
+ });
422
+
423
+ it('the upload over the operator’s cap on a free cloud account meets the plan refusal, not the cap’s 413', async () => {
424
+ // The free value is at or below the cap (1 MiB here): the limit refuses
425
+ // first, as the spec orders — with the plan refusal and its upgrade link.
426
+ const res = await upload(
427
+ app,
428
+ { cookie: hubUserCookie, 'x-workspace-id': projectedWorkspaceId, 'content-length': String(MB + 1) },
429
+ 'x'.repeat(MB + 1)
430
+ );
431
+ expect(res.status).toBe(403);
432
+ expect((await readJson(res)).error.code).toBe('plan_required');
433
+ });
434
+
435
+ it('a priced action is checked at the hub with the machine token, and debited at ingest by the event’s credits', async () => {
436
+ hub.setPrice('files.upload', { creditsPerUnit: 5, unit: 'bytes', per: 1048576 });
437
+ const balance = hub.balanceOf(ORG_A);
438
+ const checks = hub.checkRequests.length;
439
+ const res = await upload(
440
+ app,
441
+ { cookie: hubUserCookie, 'x-workspace-id': projectedWorkspaceId },
442
+ PAYLOAD + '#'
443
+ );
444
+ expect(res.status).toBe(201);
445
+ const fileId = (await readJson(res)).file.id as string;
446
+ expect(hub.checkRequests).toHaveLength(checks + 1);
447
+ expect(hub.checkRequests.at(-1)).toEqual({
448
+ auth: expect.stringMatching(/^Bearer mach_/),
449
+ body: { accountRef: ORG_A, actionKey: 'files.upload', quantity: 44, unit: 'bytes' }
450
+ });
451
+ await until(
452
+ () => landed(hub, fileId),
453
+ (ok) => ok,
454
+ 30_000
455
+ );
456
+ const [eventId] = [...hub.usageEvents.entries()].find(([, e]) => e.resourceId === fileId)!;
457
+ // 44 bytes at 5 credits per MiB, rounded up to the MiB: 5 credits.
458
+ expect(hub.balanceOf(ORG_A)).toBe(balance - 5);
459
+ const debits = hub.ledger.filter((l) => l.kind === 'debit');
460
+ expect(debits).toEqual([{ accountRef: ORG_A, kind: 'debit', amount: -5, sourceRef: eventId }]);
461
+ }, 60_000);
462
+
463
+ it('a balance below the price is 402 entitlement_denied with the hub’s top-up link, and nothing is posted', async () => {
464
+ hub.setBalance(ORG_A, 2);
465
+ const posts = hub.usageRequests.length;
466
+ const res = await upload(app, { cookie: hubUserCookie, 'x-workspace-id': projectedWorkspaceId }, PAYLOAD);
467
+ expect(res.status).toBe(402);
468
+ const body = await readJson(res);
469
+ expect(body.error.code).toBe('entitlement_denied');
470
+ expect(body.error.details).toEqual({
471
+ credits: 5,
472
+ balance: 2,
473
+ topUpUrl: `${hub.issuer}/billing/top-up?org=${ORG_A}&credits=5&balance=2&tool=${hub.toolSlug}&action=files.upload`
474
+ });
475
+ await new Promise((r) => setTimeout(r, 2_500));
476
+ expect(hub.usageRequests).toHaveLength(posts);
477
+ expect(hub.balanceOf(ORG_A)).toBe(2);
478
+ hub.setBalance(ORG_A, 5_000);
479
+ });
480
+
481
+ it('the plan limit is a metered account’s ceiling (PRDCT-2653): pro uploads above the free value, free meets plan_required with the hub’s upgrade page', async (ctx) => {
482
+ const info = await readJson(await app.app.request('/api/v1/instance'));
483
+ const { free, pro } = info.entitlements.limits['files.maxBytes'] as { free: number; pro: number };
484
+ // A host whose free value IS the cap at this size (the cap, at 1 MiB)
485
+ // has no size between the two to upload: its own metering suite pins it.
486
+ if (!(free < pro)) return ctx.skip();
487
+ const size = free + Math.floor((pro - free) / 2);
488
+ const ORG_PRO = '77777777-aaaa-4bbb-8ccc-00000000e004';
489
+ hub.setEntitlements(ORG_PRO, { plan: 'pro' });
490
+ const proCookie = await sso.ssoLogin(app, hub, {
491
+ sub: 'hub-user-ent-pro',
492
+ email: 'hub-user-pro@ent.test',
493
+ name: 'Hub User Pro',
494
+ workspaceId: ORG_PRO,
495
+ role: 'owner',
496
+ workspaceName: 'Org Pro'
497
+ });
498
+ const mePro = await readJson(await app.app.request('/api/v1/me', { headers: { cookie: proCookie } }));
499
+ const ok = await upload(
500
+ app,
501
+ { cookie: proCookie, 'x-workspace-id': mePro.activeWorkspaceId },
502
+ 'p'.repeat(size)
503
+ );
504
+ expect(ok.status).toBe(201);
505
+ // Its event lands before the next test counts the posts.
506
+ const proFileId = (await readJson(ok)).file.id as string;
507
+ await until(
508
+ () => landed(hub, proFileId),
509
+ (landedYet) => landedYet,
510
+ 30_000
511
+ );
512
+ const refused = await upload(
513
+ app,
514
+ { cookie: hubUserCookie, 'x-workspace-id': projectedWorkspaceId },
515
+ 'f'.repeat(size)
516
+ );
517
+ expect(refused.status).toBe(403);
518
+ const body = await readJson(refused);
519
+ expect(body.error.code).toBe('plan_required');
520
+ expect(body.error.details.upgradeUrl.startsWith(`${hub.issuer}/billing/upgrade?`)).toBe(true);
521
+ expect(body.error.details.upgradeUrl).toContain('key=files.maxBytes&requiredPlan=pro');
522
+ }, 60_000);
523
+
524
+ it('a metered upload that declares no size is 411 length_required: no file, no check, no event (PRDCT-2652)', async () => {
525
+ const headers = { cookie: hubUserCookie, 'x-workspace-id': projectedWorkspaceId };
526
+ const count = async () =>
527
+ ((await readJson(await app.app.request('/api/v1/files', { headers }))).files as unknown[]).length;
528
+ const files = await count();
529
+ const checks = hub.checkRequests.length;
530
+ const posts = hub.usageRequests.length;
531
+ const res = await app.app.request(`/api/v1/files?name=${encodeURIComponent('silent.txt')}`, {
532
+ method: 'POST',
533
+ headers: { ...headers, 'content-type': 'text/plain', 'x-forwarded-for': sso.nextIp() },
534
+ body: PAYLOAD
535
+ });
536
+ expect(res.status).toBe(411);
537
+ expect((await readJson(res)).error.code).toBe('length_required');
538
+ expect(await count()).toBe(files);
539
+ expect(hub.checkRequests).toHaveLength(checks);
540
+ await new Promise((r) => setTimeout(r, 2_500));
541
+ expect(hub.usageRequests).toHaveLength(posts);
542
+ });
543
+
544
+ it('a price past Number.MAX_SAFE_INTEGER is the hub’s unpriceable refusal, credits clamped, never an outage (PRDCT-2677)', async () => {
545
+ // No upload can carry a quantity this large (the instance cap refuses the
546
+ // declared size first), so the check is driven straight against the fake,
547
+ // with the machine token the instance itself would use.
548
+ hub.setPrice('big.action', { creditsPerUnit: 10_000_000, unit: 'call', per: 1 });
549
+ const logs: Array<{ level: string; msg: string }> = [];
550
+ const at = (level: string) => (ctx: unknown, msg?: string) =>
551
+ logs.push({ level, msg: msg ?? (typeof ctx === 'string' ? ctx : '') });
552
+ const logger = {
553
+ error: at('error'),
554
+ warn: at('warn'),
555
+ info: at('info'),
556
+ debug: at('debug')
557
+ } as unknown as Logger;
558
+ const token = new HubMachineToken({
559
+ issuerUrl: hub.issuer,
560
+ clientId: host.hubClientId,
561
+ clientSecret: HUB_SECRET,
562
+ resource: `${hub.issuer}/mcp`,
563
+ logger
564
+ });
565
+ const check = new HubCreditCheck({ issuerUrl: hub.issuer, token, logger });
566
+ // 10,000,000 × 1,000,000,000 = 10^16 credits, past 2^53 − 1.
567
+ const verdict = await check.check({
568
+ accountRef: ORG_A,
569
+ actionKey: 'big.action',
570
+ quantity: 1_000_000_000,
571
+ unit: 'call'
572
+ });
573
+ expect(verdict).toMatchObject({ allowed: false, reason: 'unpriceable', source: 'hub' });
574
+ expect(verdict.check).toMatchObject({
575
+ credits: Number.MAX_SAFE_INTEGER,
576
+ priced: true,
577
+ reason: 'unpriceable'
578
+ });
579
+ expect((await check.posture.get()).values[0]?.value ?? 0).toBe(0);
580
+ expect(logs.filter((l) => l.level === 'warn' || l.level === 'error')).toEqual([]);
581
+ // A quantity just inside the bound is priced and judged on the balance, as before.
582
+ const inside = await check.check({
583
+ accountRef: ORG_A,
584
+ actionKey: 'big.action',
585
+ quantity: 1,
586
+ unit: 'call'
587
+ });
588
+ expect(inside).toMatchObject({ allowed: false, reason: 'insufficient_credits', source: 'hub' });
589
+ });
590
+
591
+ it('a hub that does not answer the check fails open, on /metrics; its return clears the posture', async () => {
592
+ hub.checkMode = 'network';
593
+ const down = await upload(
594
+ app,
595
+ { cookie: hubUserCookie, 'x-workspace-id': projectedWorkspaceId },
596
+ PAYLOAD + '%'
597
+ );
598
+ expect(down.status).toBe(201);
599
+ expect(await metric('usage_check_posture')).toBe(1);
600
+ expect(await metric('usage_check_total{outcome="fail_open"}')).toBeGreaterThanOrEqual(1);
601
+ hub.checkMode = 'ok';
602
+ const up = await upload(
603
+ app,
604
+ { cookie: hubUserCookie, 'x-workspace-id': projectedWorkspaceId },
605
+ PAYLOAD + '%%'
606
+ );
607
+ expect(up.status).toBe(201);
608
+ expect(await metric('usage_check_posture')).toBe(0);
609
+ expect(await metric('usage_check_failing_since_seconds')).toBe(0);
610
+ });
611
+ });
612
+
613
+ describe('cloud: the retry budget’s end is a hold, never a loss (PRDCT-2635)', () => {
614
+ let hub: FakeHub;
615
+ let app: TestApp;
616
+ let cookie: string;
617
+ let workspaceId: string;
618
+ const METRICS_TOKEN = 'integration-metrics-token-1';
619
+ const ORG_HELD = '77777777-aaaa-4bbb-8ccc-00000000e003';
620
+
621
+ const metric = async (name: string): Promise<number> => {
622
+ const res = await app.app.request('/metrics', { headers: { authorization: `Bearer ${METRICS_TOKEN}` } });
623
+ expect(res.status).toBe(200);
624
+ const line = (await res.text())
625
+ .split('\n')
626
+ .find((l) => l.startsWith(`${name} `) || l.startsWith(`${name}{`));
627
+ return line ? Number(line.split(' ').at(-1)) : 0;
628
+ };
629
+
630
+ beforeAll(async () => {
631
+ hub = await FakeHub.start({ clientId: host.hubClientId, clientSecret: HUB_SECRET });
632
+ app = await createTestApp(
633
+ await createDatabase(container, 'ent_held'),
634
+ {
635
+ EDITION: 'cloud',
636
+ HUB_ISSUER_URL: hub.issuer,
637
+ HUB_CLIENT_ID: host.hubClientId,
638
+ HUB_CLIENT_SECRET: HUB_SECRET,
639
+ METRICS_TOKEN
640
+ },
641
+ // One retry a second later, then held; a held event comes back a second after.
642
+ { usageRetry: { limit: 1, delaySeconds: 1, heldDelaySeconds: 1 }, entitlementDials: { ttlMs: 60_000 } }
643
+ );
644
+ await app.app.request(
645
+ '/api/v1/setup',
646
+ json({ setupToken: 'integration-test-setup-token', instanceName: 'Ent Held', owner: OPERATOR })
647
+ );
648
+ cookie = await sso.ssoLogin(app, hub, {
649
+ sub: 'hub-user-held',
650
+ email: 'held@ent.test',
651
+ name: 'Held User',
652
+ workspaceId: ORG_HELD,
653
+ role: 'owner',
654
+ workspaceName: 'Org Held'
655
+ });
656
+ const me = await readJson(await app.app.request('/api/v1/me', { headers: { cookie } }));
657
+ workspaceId = me.activeWorkspaceId;
658
+ }, 180_000);
659
+
660
+ afterAll(async () => {
661
+ await app?.stop();
662
+ await hub?.stop();
663
+ });
664
+
665
+ it('an outage that outlasts the budget holds the event, counts it on /metrics, and re-drives it until the hub takes it', async () => {
666
+ hub.usageMode = 'network';
667
+ const res = await upload(app, { cookie, 'x-workspace-id': workspaceId }, PAYLOAD + '!');
668
+ expect(res.status).toBe(201);
669
+ const fileId = (await readJson(res)).file.id as string;
670
+ // The budget runs out (one attempt, one retry), the batch is held and counted.
671
+ await until(
672
+ () => metric('usage_events_held_total'),
673
+ (n) => n >= 1,
674
+ 60_000
675
+ );
676
+ expect(await metric('usage_events_held_total')).toBe(1);
677
+ expect(hub.usageEvents.size).toBe(0);
678
+ // The hub comes back: the held event comes round again and lands, exactly once.
679
+ hub.usageMode = 'ok';
680
+ await until(
681
+ () => landed(hub, fileId),
682
+ (ok) => ok,
683
+ 60_000
684
+ );
685
+ expect([...hub.usageEvents.values()].filter((e) => e.resourceId === fileId)).toHaveLength(1);
686
+ // The poster's outcomes are on /metrics too: the retried batches, then the accepted event.
687
+ expect(await metric('usage_poster_batches_total{outcome="retried"}')).toBeGreaterThanOrEqual(2);
688
+ await until(
689
+ () => metric('usage_poster_events_total{outcome="accepted"}'),
690
+ (n) => n >= 1
691
+ );
692
+ }, 120_000);
693
+ });