@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,321 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql';
3
+ import AdmZip from 'adm-zip';
4
+ import { eq } from 'drizzle-orm';
5
+ import { workspaces, type Db } from '@antasphere/chassis-db';
6
+ import { createPlatform, type ExportEntry, type ToolDefinition } from '@antasphere/chassis-server';
7
+ import {
8
+ createDatabase,
9
+ extractCookie,
10
+ makeCreateTestApp,
11
+ readJson,
12
+ SETUP_TOKEN,
13
+ startPostgres
14
+ } from '@antasphere/chassis-server/testing';
15
+ import { minimalTool } from '../host/minimal-tool.js';
16
+
17
+ /**
18
+ * The `api.exportEntries` slot (PRDCT-2543): a tool adds named entries of rows
19
+ * to `GET /workspace/export`. This file carries its OWN two tools (the minimal
20
+ * test tool with the slot filled, and the same without it), so it proves the
21
+ * same thing under either runner of the suite: it never reads `@chassis-test/host`.
22
+ *
23
+ * The export bucket is 5 per user per 10 minutes: the filled tool's owner
24
+ * exports five times here, the bare tool's owner once. A request refused at
25
+ * the membership gate resolves no principal, so it spends the ADDRESS bucket
26
+ * only: those come from addresses of their own.
27
+ *
28
+ * EVERY export request of this file carries hostile values that name ANOTHER
29
+ * workspace (a header and a query parameter): the tool function receives the
30
+ * principal's resolved workspace id, never a raw request value (F2 of the
31
+ * PRDCT-2543 verification).
32
+ */
33
+
34
+ const OWNER = {
35
+ email: 'owner@export-entries.test',
36
+ name: 'Export Owner',
37
+ password: 'export-owner-password-1'
38
+ };
39
+ const IP = '10.7.0.1';
40
+
41
+ type MinimalTool = typeof minimalTool;
42
+
43
+ /** What the filled tool contributes: each test sets it, the slot reads it per export. */
44
+ let contribution: (db: Db, workspaceId: string) => Promise<ExportEntry[]> = async () => [];
45
+ const seen: Array<{ workspaceId: string }> = [];
46
+
47
+ const filledTool: MinimalTool = {
48
+ ...minimalTool,
49
+ api: {
50
+ ...minimalTool.api,
51
+ exportEntries: (domain) => {
52
+ // Bound once, with the tool's domain (the minimal tool's is empty).
53
+ expect(domain).toEqual({});
54
+ return (db, workspaceId) => {
55
+ seen.push({ workspaceId });
56
+ return contribution(db, workspaceId);
57
+ };
58
+ }
59
+ }
60
+ } satisfies ToolDefinition<Record<never, never>, Record<string, never>>;
61
+
62
+ const json = (body: unknown, headers: Record<string, string> = {}) => ({
63
+ method: 'POST',
64
+ headers: { 'content-type': 'application/json', ...headers },
65
+ body: JSON.stringify(body)
66
+ });
67
+
68
+ const bootFilled = makeCreateTestApp((source?: NodeJS.ProcessEnv) => createPlatform(filledTool).boot(source));
69
+ const bootBare = makeCreateTestApp((source?: NodeJS.ProcessEnv) => createPlatform(minimalTool).boot(source));
70
+ type Booted = Awaited<ReturnType<typeof bootFilled>>;
71
+
72
+ let container: StartedPostgreSqlContainer;
73
+ let filled: Booted;
74
+ let bare: Booted;
75
+
76
+ async function claim(app: Booted): Promise<{ cookie: string; workspaceId: string }> {
77
+ const setup = await app.app.request(
78
+ '/api/v1/setup',
79
+ json({ setupToken: SETUP_TOKEN, instanceName: 'Export Entries', owner: OWNER })
80
+ );
81
+ expect(setup.status).toBe(201);
82
+ const signIn = await app.app.request(
83
+ '/api/v1/auth/sign-in/email',
84
+ json({ email: OWNER.email, password: OWNER.password }, { 'x-forwarded-for': IP })
85
+ );
86
+ expect(signIn.status).toBe(200);
87
+ const cookie = extractCookie(signIn);
88
+ const me = await readJson(await app.app.request('/api/v1/me', { headers: { cookie } }));
89
+ return { cookie, workspaceId: me.workspace.id as string };
90
+ }
91
+
92
+ async function upload(app: Booted, cookie: string, name: string, text: string): Promise<string> {
93
+ const res = await app.app.request(`/api/v1/files?name=${encodeURIComponent(name)}`, {
94
+ method: 'POST',
95
+ headers: { 'content-type': 'application/octet-stream', cookie },
96
+ body: text
97
+ });
98
+ expect(res.status).toBe(201);
99
+ return (await readJson(res)).file.id as string;
100
+ }
101
+
102
+ /** A well-formed workspace id that is nobody's: what the hostile request values name. */
103
+ const HOSTILE_WORKSPACE = '0b57113e-0000-4000-8000-00000000dead';
104
+
105
+ const exportAs = (app: Booted, cookie: string, headers: Record<string, string> = {}) =>
106
+ app.app.request(
107
+ `/api/v1/workspace/export?workspaceId=${HOSTILE_WORKSPACE}&workspace=${HOSTILE_WORKSPACE}`,
108
+ {
109
+ headers: { cookie, 'x-forwarded-for': IP, 'x-export-workspace': HOSTILE_WORKSPACE, ...headers }
110
+ }
111
+ );
112
+
113
+ async function exportAuditRows(app: Booted, cookie: string): Promise<number> {
114
+ const res = await app.app.request('/api/v1/audit?action=workspace.export', { headers: { cookie } });
115
+ expect(res.status).toBe(200);
116
+ const body = await readJson(res);
117
+ return (body.entries as Array<{ action: string }>).filter((e) => e.action === 'workspace.export').length;
118
+ }
119
+
120
+ let filledOwner: { cookie: string; workspaceId: string };
121
+ let filledBlobId: string;
122
+
123
+ beforeAll(async () => {
124
+ container = await startPostgres();
125
+ filled = await bootFilled(await createDatabase(container, 'export_entries_filled'));
126
+ bare = await bootBare(await createDatabase(container, 'export_entries_bare'));
127
+ filledOwner = await claim(filled);
128
+ filledBlobId = await upload(filled, filledOwner.cookie, 'kept.bin', 'a blob that stays');
129
+ }, 240_000);
130
+
131
+ afterAll(async () => {
132
+ await filled?.stop();
133
+ await bare?.stop();
134
+ await container?.stop();
135
+ });
136
+
137
+ describe('GET /workspace/export with the `api.exportEntries` slot filled', () => {
138
+ it('writes the tool entries after files.json and before the blobs, rows as JSON, hostile names sanitized', async () => {
139
+ contribution = async (db, workspaceId) => {
140
+ // The handle is a live one and the id is the caller's workspace.
141
+ const rows = await db
142
+ .select({ id: workspaces.id, name: workspaces.name })
143
+ .from(workspaces)
144
+ .where(eq(workspaces.id, workspaceId));
145
+ return [
146
+ { name: 'things', rows },
147
+ { name: '../../etc/cron.d/x', rows: [{ hostile: true }] },
148
+ { name: 'ctl\u0000\u001f\u007fname', rows: [] }
149
+ ];
150
+ };
151
+ const res = await exportAs(filled, filledOwner.cookie);
152
+ expect(res.status).toBe(200);
153
+ expect(res.headers.get('content-type')).toBe('application/zip');
154
+ const zip = new AdmZip(Buffer.from(await res.arrayBuffer()));
155
+ expect(zip.getEntries().map((e) => e.entryName)).toEqual([
156
+ 'manifest.json',
157
+ 'workspace.json',
158
+ 'members.json',
159
+ 'invitations.json',
160
+ 'api-keys.json',
161
+ 'projects.json',
162
+ 'project_members.json',
163
+ 'teams.json',
164
+ 'team_members.json',
165
+ 'project_teams.json',
166
+ 'audit-log.ndjson',
167
+ 'files.json',
168
+ 'things.json',
169
+ '.._.._etc_cron.d_x.json',
170
+ 'ctl___name.json',
171
+ `files/${filledBlobId}-kept.bin`,
172
+ 'skipped-blobs.json'
173
+ ]);
174
+ expect(zip.readAsText('things.json')).toBe(
175
+ JSON.stringify([{ id: filledOwner.workspaceId, name: 'Export Entries' }], null, 2) + '\n'
176
+ );
177
+ expect(JSON.parse(zip.readAsText('.._.._etc_cron.d_x.json'))).toEqual([{ hostile: true }]);
178
+ expect(JSON.parse(zip.readAsText('ctl___name.json'))).toEqual([]);
179
+ // The manifest does not list the tool's entries.
180
+ expect(Object.keys(JSON.parse(zip.readAsText('manifest.json')))).toEqual([
181
+ 'formatVersion',
182
+ 'exportedAt',
183
+ 'instance',
184
+ 'workspaceId'
185
+ ]);
186
+ expect(seen.at(-1)).toEqual({ workspaceId: filledOwner.workspaceId });
187
+ expect(await exportAuditRows(filled, filledOwner.cookie)).toBe(1);
188
+ });
189
+
190
+ it('an empty list writes nothing: the entry list is the bare one', async () => {
191
+ contribution = async () => [];
192
+ const res = await exportAs(filled, filledOwner.cookie);
193
+ expect(res.status).toBe(200);
194
+ const zip = new AdmZip(Buffer.from(await res.arrayBuffer()));
195
+ expect(zip.getEntries().map((e) => e.entryName)).toEqual([
196
+ 'manifest.json',
197
+ 'workspace.json',
198
+ 'members.json',
199
+ 'invitations.json',
200
+ 'api-keys.json',
201
+ 'projects.json',
202
+ 'project_members.json',
203
+ 'teams.json',
204
+ 'team_members.json',
205
+ 'project_teams.json',
206
+ 'audit-log.ndjson',
207
+ 'files.json',
208
+ `files/${filledBlobId}-kept.bin`,
209
+ 'skipped-blobs.json'
210
+ ]);
211
+ expect(await exportAuditRows(filled, filledOwner.cookie)).toBe(2);
212
+ });
213
+
214
+ it('a name that collides with a chassis entry answers 500, no zip, no audit row', async () => {
215
+ contribution = async () => [{ name: 'files', rows: [{ overwrite: true }] }];
216
+ const res = await exportAs(filled, filledOwner.cookie);
217
+ expect(res.status).toBe(500);
218
+ expect(res.headers.get('content-type')).toMatch(/json/);
219
+ expect(res.headers.get('content-disposition')).toBeNull();
220
+ const body = await readJson(res);
221
+ expect(JSON.stringify(body)).not.toContain('already an entry');
222
+ expect(await exportAuditRows(filled, filledOwner.cookie)).toBe(2);
223
+ });
224
+
225
+ it('a contribution that throws answers 500, no zip, no audit row', async () => {
226
+ contribution = async () => {
227
+ throw new Error('the tool could not read its rows');
228
+ };
229
+ const calls = seen.length;
230
+ const res = await exportAs(filled, filledOwner.cookie);
231
+ expect(seen.length).toBe(calls + 1);
232
+ expect(res.status).toBe(500);
233
+ expect(res.headers.get('content-type')).toMatch(/json/);
234
+ expect(res.headers.get('content-disposition')).toBeNull();
235
+ expect(await exportAuditRows(filled, filledOwner.cookie)).toBe(2);
236
+ });
237
+ });
238
+
239
+ describe("the workspace id the tool function receives is the principal's, never a request value", () => {
240
+ it('the hostile header and query parameters were sent on every export above, and never seen', () => {
241
+ expect(seen.length).toBeGreaterThanOrEqual(4);
242
+ expect(seen.map((s) => s.workspaceId)).toEqual(seen.map(() => filledOwner.workspaceId));
243
+ });
244
+
245
+ it('X-Workspace-Id naming a workspace the caller is NOT a member of: 401, the tool function is not called', async () => {
246
+ // A real workspace of the same instance, with no membership row for the caller.
247
+ const foreign = await filled.db.pool.query<{ id: string }>(
248
+ `INSERT INTO workspaces (name) VALUES ('Somebody else''s') RETURNING id`
249
+ );
250
+ const foreignId = foreign.rows[0]!.id;
251
+ contribution = async () => [{ name: 'things', rows: [] }];
252
+ const calls = seen.length;
253
+ for (const [selector, ip] of [
254
+ [foreignId, '10.7.0.2'],
255
+ [HOSTILE_WORKSPACE, '10.7.0.3']
256
+ ] as const) {
257
+ // Same answer for a workspace that exists and one that does not (no oracle).
258
+ const res = await exportAs(filled, filledOwner.cookie, {
259
+ 'x-workspace-id': selector,
260
+ 'x-forwarded-for': ip
261
+ });
262
+ expect(res.status).toBe(401);
263
+ expect(res.headers.get('content-type')).toMatch(/json/);
264
+ expect(res.headers.get('content-disposition')).toBeNull();
265
+ }
266
+ expect(seen.length).toBe(calls);
267
+ expect(await exportAuditRows(filled, filledOwner.cookie)).toBe(2);
268
+ });
269
+
270
+ it('X-Workspace-Id naming the OTHER workspace the caller is a member of: the tool receives that resolved id', async () => {
271
+ const created = await filled.app.request(
272
+ '/api/v1/workspaces',
273
+ json({ name: 'Second' }, { cookie: filledOwner.cookie, 'x-forwarded-for': IP })
274
+ );
275
+ expect(created.status).toBe(201);
276
+ const secondId = (await readJson(created)).workspace.id as string;
277
+ expect(secondId).not.toBe(filledOwner.workspaceId);
278
+
279
+ contribution = async (_db, workspaceId) => [{ name: 'things', rows: [{ workspaceId }] }];
280
+ const calls = seen.length;
281
+ const res = await exportAs(filled, filledOwner.cookie, {
282
+ 'x-workspace-id': secondId,
283
+ 'x-forwarded-for': '10.7.0.4'
284
+ });
285
+ expect(res.status).toBe(200);
286
+ expect(seen.length).toBe(calls + 1);
287
+ // The principal resolved to the second workspace: exactly that id, not the
288
+ // default one, and not the hostile header/query value riding the same request.
289
+ expect(seen.at(-1)).toEqual({ workspaceId: secondId });
290
+ const zip = new AdmZip(Buffer.from(await res.arrayBuffer()));
291
+ expect(JSON.parse(zip.readAsText('things.json'))).toEqual([{ workspaceId: secondId }]);
292
+ expect(JSON.parse(zip.readAsText('manifest.json')).workspaceId).toBe(secondId);
293
+ });
294
+ });
295
+
296
+ describe('GET /workspace/export with the slot ABSENT', () => {
297
+ it("the bundle's entry list is exactly the chassis sections and the blobs", async () => {
298
+ const owner = await claim(bare);
299
+ const blobId = await upload(bare, owner.cookie, 'kept.bin', 'a blob that stays');
300
+ const res = await exportAs(bare, owner.cookie);
301
+ expect(res.status).toBe(200);
302
+ const zip = new AdmZip(Buffer.from(await res.arrayBuffer()));
303
+ expect(zip.getEntries().map((e) => e.entryName)).toEqual([
304
+ 'manifest.json',
305
+ 'workspace.json',
306
+ 'members.json',
307
+ 'invitations.json',
308
+ 'api-keys.json',
309
+ 'projects.json',
310
+ 'project_members.json',
311
+ 'teams.json',
312
+ 'team_members.json',
313
+ 'project_teams.json',
314
+ 'audit-log.ndjson',
315
+ 'files.json',
316
+ `files/${blobId}-kept.bin`,
317
+ 'skipped-blobs.json'
318
+ ]);
319
+ expect(await exportAuditRows(bare, owner.cookie)).toBe(1);
320
+ });
321
+ });
@@ -0,0 +1,320 @@
1
+ import { rm } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
4
+ import type { StartedPostgreSqlContainer } from '@testcontainers/postgresql';
5
+ import { GenericContainer, type StartedTestContainer, Wait } from 'testcontainers';
6
+ import {
7
+ createDatabase,
8
+ createTestApp,
9
+ extractCookie,
10
+ readJson,
11
+ startPostgres,
12
+ type TestApp,
13
+ host
14
+ } from './helpers.js';
15
+
16
+ /**
17
+ * Files module end-to-end (exit criterion 10): streamed upload, Range/206
18
+ * download, safe-serving headers — against the local driver, then the same
19
+ * core flow against MinIO through the s3 driver.
20
+ */
21
+
22
+ const OWNER = { email: 'owner@files.test', name: 'Files Owner', password: 'files-owner-password-1' };
23
+ const PAYLOAD = 'The quick brown fox jumps over the lazy dog'; // 43 bytes
24
+
25
+ let container: StartedPostgreSqlContainer;
26
+
27
+ const json = (body: unknown) => ({
28
+ method: 'POST',
29
+ headers: { 'content-type': 'application/json' },
30
+ body: JSON.stringify(body)
31
+ });
32
+
33
+ async function setupApp(app: TestApp): Promise<string> {
34
+ await app.app.request(
35
+ '/api/v1/setup',
36
+ json({ setupToken: 'integration-test-setup-token', instanceName: 'Files', owner: OWNER })
37
+ );
38
+ const signIn = await app.app.request(
39
+ '/api/v1/auth/sign-in/email',
40
+ json({ email: OWNER.email, password: OWNER.password })
41
+ );
42
+ return extractCookie(signIn);
43
+ }
44
+
45
+ async function uploadText(app: TestApp, cookie: string, name: string, text: string, contentType: string) {
46
+ return app.app.request(`/api/v1/files?name=${encodeURIComponent(name)}`, {
47
+ method: 'POST',
48
+ headers: { 'content-type': contentType, cookie },
49
+ body: text
50
+ });
51
+ }
52
+
53
+ beforeAll(async () => {
54
+ container = await startPostgres();
55
+ });
56
+
57
+ afterAll(async () => {
58
+ await container?.stop();
59
+ });
60
+
61
+ describe('local driver', () => {
62
+ let app: TestApp;
63
+ let cookie: string;
64
+ let fileId: string;
65
+
66
+ beforeAll(async () => {
67
+ app = await createTestApp(await createDatabase(container, 'files_local'), { MAX_FILE_SIZE_MB: '1' });
68
+ cookie = await setupApp(app);
69
+ });
70
+
71
+ afterAll(async () => {
72
+ await app?.stop();
73
+ });
74
+
75
+ it('uploads streamed bytes and returns the content address', async () => {
76
+ const res = await uploadText(app, cookie, 'fox.txt', PAYLOAD, 'text/plain');
77
+ expect(res.status).toBe(201);
78
+ const body = await readJson(res);
79
+ expect(body.file.sha256).toMatch(/^[a-f0-9]{64}$/);
80
+ expect(body.file.sizeBytes).toBe(43);
81
+ expect(body.deduplicated).toBe(false);
82
+ fileId = body.file.id;
83
+ });
84
+
85
+ it('re-uploading identical bytes deduplicates to the same row', async () => {
86
+ const res = await uploadText(app, cookie, 'fox-again.txt', PAYLOAD, 'text/plain');
87
+ expect(res.status).toBe(201);
88
+ const body = await readJson(res);
89
+ expect(body.deduplicated).toBe(true);
90
+ expect(body.file.id).toBe(fileId);
91
+ });
92
+
93
+ it('downloads in full with immutable caching and nosniff', async () => {
94
+ const res = await app.app.request(`/api/v1/files/${fileId}/content`, { headers: { cookie } });
95
+ expect(res.status).toBe(200);
96
+ expect(await res.text()).toBe(PAYLOAD);
97
+ expect(res.headers.get('x-content-type-options')).toBe('nosniff');
98
+ expect(res.headers.get('cache-control')).toContain('immutable');
99
+ expect(res.headers.get('accept-ranges')).toBe('bytes');
100
+ expect(res.headers.get('content-disposition')).toMatch(/^inline/); // text/plain is passive
101
+ });
102
+
103
+ it('serves an exact 206 slice with Content-Range (exit criterion 10)', async () => {
104
+ const res = await app.app.request(`/api/v1/files/${fileId}/content`, {
105
+ headers: { cookie, range: 'bytes=4-8' }
106
+ });
107
+ expect(res.status).toBe(206);
108
+ expect(await res.text()).toBe('quick');
109
+ expect(res.headers.get('content-range')).toBe('bytes 4-8/43');
110
+ expect(res.headers.get('content-length')).toBe('5');
111
+ expect(res.headers.get('x-content-type-options')).toBe('nosniff');
112
+ });
113
+
114
+ it('suffix and open ranges work (video-seek shapes)', async () => {
115
+ const tail = await app.app.request(`/api/v1/files/${fileId}/content`, {
116
+ headers: { cookie, range: 'bytes=-3' }
117
+ });
118
+ expect(tail.status).toBe(206);
119
+ expect(await tail.text()).toBe('dog');
120
+
121
+ const open = await app.app.request(`/api/v1/files/${fileId}/content`, {
122
+ headers: { cookie, range: 'bytes=40-' }
123
+ });
124
+ expect(open.status).toBe(206);
125
+ expect(await open.text()).toBe('dog');
126
+ });
127
+
128
+ it('rejects unsatisfiable and multi-range with 416 + bytes */size', async () => {
129
+ for (const range of ['bytes=100-', 'bytes=0-1,5-9']) {
130
+ const res = await app.app.request(`/api/v1/files/${fileId}/content`, {
131
+ headers: { cookie, range }
132
+ });
133
+ expect(res.status).toBe(416);
134
+ expect(res.headers.get('content-range')).toBe('bytes */43');
135
+ }
136
+ });
137
+
138
+ it('answers HEAD with headers and no body', async () => {
139
+ const res = await app.app.request(`/api/v1/files/${fileId}/content`, {
140
+ method: 'HEAD',
141
+ headers: { cookie }
142
+ });
143
+ expect(res.status).toBe(200);
144
+ expect(res.headers.get('content-length')).toBe('43');
145
+ expect(await res.text()).toBe('');
146
+ });
147
+
148
+ it('serves 304 on a matching ETag (content-addressed: hash is the tag)', async () => {
149
+ const first = await app.app.request(`/api/v1/files/${fileId}/content`, { headers: { cookie } });
150
+ const etag = first.headers.get('etag')!;
151
+ const res = await app.app.request(`/api/v1/files/${fileId}/content`, {
152
+ headers: { cookie, 'if-none-match': etag }
153
+ });
154
+ expect(res.status).toBe(304);
155
+ });
156
+
157
+ it('forces attachment for browser-renderable uploads (html)', async () => {
158
+ const up = await uploadText(app, cookie, 'evil.html', '<script>alert(1)</script>', 'text/html');
159
+ const body = await readJson(up);
160
+ const res = await app.app.request(`/api/v1/files/${body.file.id}/content`, { headers: { cookie } });
161
+ expect(res.headers.get('content-disposition')).toMatch(/^attachment/);
162
+ expect(res.headers.get('x-content-type-options')).toBe('nosniff');
163
+ });
164
+
165
+ it('enforces the size cap mid-stream with 413', async () => {
166
+ // Test app runs with MAX_FILE_SIZE_MB=1; send ~1.5MB without content-length honesty.
167
+ const big = 'x'.repeat(1_500_000);
168
+ const res = await uploadText(app, cookie, 'big.bin', big, 'application/octet-stream');
169
+ expect(res.status).toBe(413);
170
+ });
171
+
172
+ it('scopes machine access: a read-scope key can stream, cannot upload', async () => {
173
+ const mint = await app.app.request('/api/v1/api-keys', {
174
+ ...json({ name: 'files-ro', scopes: [host.scopes.read] }),
175
+ headers: { 'content-type': 'application/json', cookie }
176
+ });
177
+ const { key } = await readJson(mint);
178
+
179
+ const read = await app.app.request(`/api/v1/files/${fileId}/content`, {
180
+ headers: { authorization: `Bearer ${key}` }
181
+ });
182
+ expect(read.status).toBe(200);
183
+
184
+ const write = await app.app.request('/api/v1/files?name=x.txt', {
185
+ method: 'POST',
186
+ headers: { 'content-type': 'text/plain', authorization: `Bearer ${key}` },
187
+ body: 'nope'
188
+ });
189
+ expect(write.status).toBe(403);
190
+ });
191
+
192
+ it('delete removes metadata and the blob', async () => {
193
+ const up = await uploadText(app, cookie, 'temp.txt', 'delete me', 'text/plain');
194
+ const { file } = await readJson(up);
195
+ const del = await app.app.request(`/api/v1/files/${file.id}`, {
196
+ method: 'DELETE',
197
+ headers: { cookie }
198
+ });
199
+ expect(del.status).toBe(200);
200
+ const gone = await app.app.request(`/api/v1/files/${file.id}/content`, { headers: { cookie } });
201
+ expect(gone.status).toBe(404);
202
+ });
203
+
204
+ it('answers a clean 404 when the metadata row exists but the blob is unreachable (never a truncated 200)', async () => {
205
+ // The cross-replica local-storage situation (scale drill, I2): the shared
206
+ // Postgres row exists, but THIS replica's private disk has no blob.
207
+ // Simulated by deleting the blob file underneath a live row.
208
+ const up = await uploadText(
209
+ app,
210
+ cookie,
211
+ 'ghost.txt',
212
+ 'bytes that vanish from this replica',
213
+ 'text/plain'
214
+ );
215
+ const { file } = await readJson(up);
216
+ const { rows } = await app.db.pool.query<{ workspace_id: string }>(
217
+ 'SELECT workspace_id FROM files WHERE id = $1',
218
+ [file.id]
219
+ );
220
+ const blobPath = join(
221
+ app.env.DATA_DIR,
222
+ 'storage',
223
+ 'blobs',
224
+ 'ws',
225
+ rows[0]!.workspace_id,
226
+ file.sha256.slice(0, 2),
227
+ file.sha256
228
+ );
229
+ await rm(blobPath);
230
+
231
+ // GET: a clean pre-body 404 with the error envelope — not 200 + headers
232
+ // followed by a dead stream (the silent truncation the drill surfaced).
233
+ const res = await app.app.request(`/api/v1/files/${file.id}/content`, { headers: { cookie } });
234
+ expect(res.status).toBe(404);
235
+ const body = await readJson(res);
236
+ expect(body.error.code).toBe('not_found');
237
+
238
+ // HEAD and Range take the same guard: no 2xx may promise unreachable bytes.
239
+ const head = await app.app.request(`/api/v1/files/${file.id}/content`, {
240
+ method: 'HEAD',
241
+ headers: { cookie }
242
+ });
243
+ expect(head.status).toBe(404);
244
+ const ranged = await app.app.request(`/api/v1/files/${file.id}/content`, {
245
+ headers: { cookie, range: 'bytes=0-4' }
246
+ });
247
+ expect(ranged.status).toBe(404);
248
+
249
+ // The metadata endpoint still serves the row — only the content is 404.
250
+ const meta = await app.app.request(`/api/v1/files/${file.id}`, { headers: { cookie } });
251
+ expect(meta.status).toBe(200);
252
+ });
253
+ });
254
+
255
+ describe('s3 driver (MinIO)', () => {
256
+ let minio: StartedTestContainer;
257
+ let app: TestApp;
258
+ let cookie: string;
259
+
260
+ beforeAll(async () => {
261
+ // MinIO no longer publishes images anonymously: Docker Hub's `minio/minio`
262
+ // answers "pull access denied" and quay.io/minio/minio answers 401 to any
263
+ // client without credentials (the GitHub runner's case; a Mac with the
264
+ // image cached never pulls). Bitnami's frozen legacy build of MinIO is
265
+ // still public; pinned on its multi-arch digest so the reference cannot
266
+ // move. Its entrypoint starts the server itself (no command), and the
267
+ // readiness probe waits past the entrypoint's own setup restart.
268
+ minio = await new GenericContainer(
269
+ 'bitnamilegacy/minio:2025.7.23-debian-12-r5@sha256:6dabb4a2088c9a79908de3bc05f4586c23ad2182c8908e7e3acbf61c1467fb20'
270
+ )
271
+ .withEnvironment({ MINIO_ROOT_USER: 'minioadmin', MINIO_ROOT_PASSWORD: 'minioadmin' })
272
+ .withExposedPorts(9000)
273
+ .withWaitStrategy(Wait.forHttp('/minio/health/ready', 9000))
274
+ .start();
275
+
276
+ const endpoint = `http://${minio.getHost()}:${minio.getMappedPort(9000)}`;
277
+ // Create the bucket via the S3 API directly.
278
+ const { S3Client, CreateBucketCommand } = await import('@aws-sdk/client-s3');
279
+ const client = new S3Client({
280
+ region: 'us-east-1',
281
+ endpoint,
282
+ forcePathStyle: true,
283
+ credentials: { accessKeyId: 'minioadmin', secretAccessKey: 'minioadmin' }
284
+ });
285
+ await client.send(new CreateBucketCommand({ Bucket: 'chassis-test' }));
286
+
287
+ app = await createTestApp(await createDatabase(container, 'files_s3'), {
288
+ STORAGE_DRIVER: 's3',
289
+ S3_BUCKET: 'chassis-test',
290
+ S3_REGION: 'us-east-1',
291
+ S3_ENDPOINT: endpoint,
292
+ S3_ACCESS_KEY_ID: 'minioadmin',
293
+ S3_SECRET_ACCESS_KEY: 'minioadmin',
294
+ S3_FORCE_PATH_STYLE: 'true'
295
+ });
296
+ cookie = await setupApp(app);
297
+ }, 240_000);
298
+
299
+ afterAll(async () => {
300
+ await app?.stop();
301
+ await minio?.stop();
302
+ });
303
+
304
+ it('uploads and Range-streams through MinIO identically', async () => {
305
+ const up = await uploadText(app, cookie, 'fox-s3.txt', PAYLOAD, 'text/plain');
306
+ expect(up.status).toBe(201);
307
+ const { file } = await readJson(up);
308
+
309
+ const res = await app.app.request(`/api/v1/files/${file.id}/content`, {
310
+ headers: { cookie, range: 'bytes=4-8' }
311
+ });
312
+ expect(res.status).toBe(206);
313
+ expect(await res.text()).toBe('quick');
314
+ expect(res.headers.get('content-range')).toBe('bytes 4-8/43');
315
+
316
+ const full = await app.app.request(`/api/v1/files/${file.id}/content`, { headers: { cookie } });
317
+ expect(full.status).toBe(200);
318
+ expect(await full.text()).toBe(PAYLOAD);
319
+ });
320
+ });