@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,482 @@
1
+ import type { OpenAPIHono } from '@hono/zod-openapi';
2
+ import { Readable } from 'node:stream';
3
+ import { finished } from 'node:stream/promises';
4
+ import { and, asc, eq, gt, isNull, sql } from 'drizzle-orm';
5
+ import { ZipFile } from 'yazl';
6
+ import {
7
+ apiKeys,
8
+ auditLog,
9
+ files,
10
+ instanceSettings,
11
+ invitations,
12
+ projectMembers,
13
+ projects,
14
+ projectTeams,
15
+ user as userTable,
16
+ workspaceMembers,
17
+ workspaces,
18
+ workspaceTeamMembers,
19
+ workspaceTeams,
20
+ type Db
21
+ } from '@antasphere/chassis-db';
22
+ import type { Env } from '../env.js';
23
+ import type { ScopeRoutes } from './scope-routes.js';
24
+ import type { Logger } from '../logger.js';
25
+ import type { AuditService } from '../audit/service.js';
26
+ import { blobKey, type StorageDriver } from '../storage/driver.js';
27
+ import { requireAuth, requireNonGuest, requireRole } from '../middleware/auth-context.js';
28
+ import { rateLimit, type RateLimiters } from '../middleware/rate-limit.js';
29
+ import type { ClientIpFn } from '../middleware/rate-limit.js';
30
+
31
+ /**
32
+ * GET /workspace/export — the GDPR/portability bundle: every workspace table
33
+ * as JSON, the audit log as NDJSON, and every live blob, streamed as one zip
34
+ * (yazl; modeled on the files-content streaming precedent in files.ts).
35
+ * Secrets never leave: invitations drop tokenHash, api-keys drop secretHash.
36
+ */
37
+
38
+ const AUDIT_BATCH = 500;
39
+
40
+ export interface ExportRouteDeps {
41
+ db: Db;
42
+ env: Pick<Env, 'EDITION' | 'APP_VERSION'>;
43
+ storage: StorageDriver;
44
+ audit: AuditService;
45
+ limiters: RateLimiters;
46
+ clientIp: ClientIpFn;
47
+ logger: Logger;
48
+ /** The export route the tool instantiated: its OpenAPI summary names the tool's export scope. */
49
+ workspaceExportRoute: ScopeRoutes['workspaceExportRoute'];
50
+ /** The tool's `api.exportEntries` slot, already bound to its domain. Absent = the chassis sections only. */
51
+ exportEntries?: ExportEntriesFn;
52
+ }
53
+
54
+ /** One entry a tool adds to the bundle: written as `<name>.json`, its rows as a JSON array. */
55
+ export interface ExportEntry {
56
+ name: string;
57
+ rows: unknown[];
58
+ }
59
+
60
+ /**
61
+ * What the `api.exportEntries` slot returns: the tool's entries for ONE
62
+ * workspace. It reads with the handle the route itself reads with (`db`, no
63
+ * transaction), and it filters on `workspaceId` itself.
64
+ */
65
+ export type ExportEntriesFn = (db: Db, workspaceId: string) => Promise<ExportEntry[]>;
66
+
67
+ /**
68
+ * The entry names the chassis writes itself, without their extension. A tool
69
+ * entry that lands on one of them is refused: never a silent overwrite, never
70
+ * two zip entries of one name. (`files/…` needs no line here: a sanitized name
71
+ * holds no `/`.) `audit-log` is reserved although its file is `.ndjson`, so
72
+ * that the bundle never holds two audit logs.
73
+ */
74
+ export const RESERVED_EXPORT_ENTRY_NAMES = [
75
+ 'manifest',
76
+ 'workspace',
77
+ 'members',
78
+ 'invitations',
79
+ 'api-keys',
80
+ 'projects',
81
+ 'project_members',
82
+ 'teams',
83
+ 'team_members',
84
+ 'project_teams',
85
+ 'audit-log',
86
+ 'files',
87
+ 'skipped-blobs'
88
+ ] as const;
89
+
90
+ /** A tool entry, ready to write: its zip entry name and its bytes. */
91
+ export interface ResolvedExportEntry {
92
+ entryName: string;
93
+ buffer: Buffer;
94
+ }
95
+
96
+ /**
97
+ * Tool entries → zip entries, BEFORE the stream starts: every name goes
98
+ * through `sanitizeEntryName`, every `rows` through `jsonBuffer`, and a name
99
+ * that collides (compared without case, since the bundle is extracted on
100
+ * case-insensitive disks too) with a chassis entry or with another tool entry
101
+ * throws. Order is the tool's.
102
+ */
103
+ export function resolveExportEntries(entries: ExportEntry[]): ResolvedExportEntry[] {
104
+ if (!Array.isArray(entries)) throw new Error('export entries: the tool must return an array of entries');
105
+ const taken = new Set<string>(RESERVED_EXPORT_ENTRY_NAMES);
106
+ return entries.map((entry) => {
107
+ if (typeof entry?.name !== 'string' || !Array.isArray(entry.rows)) {
108
+ throw new Error('export entries: every entry is { name: string, rows: unknown[] }');
109
+ }
110
+ const name = sanitizeEntryName(entry.name);
111
+ const key = name.toLowerCase();
112
+ if (taken.has(key)) {
113
+ throw new Error(`export entries: the name "${name}" is already an entry of the bundle`);
114
+ }
115
+ taken.add(key);
116
+ return { entryName: `${name}.json`, buffer: jsonBuffer(entry.rows) };
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Zip-slip guard: entry names embed the user-supplied originalName, so strip
122
+ * path separators (`/`, `\`) and control chars, and cap the length — an
123
+ * uploaded name like `../../etc/cron.d/x` must never place an entry outside
124
+ * the extraction directory.
125
+ */
126
+ export function sanitizeEntryName(name: string): string {
127
+ // eslint-disable-next-line no-control-regex
128
+ const cleaned = name.replace(/[/\\\u0000-\u001f\u007f]/g, '_');
129
+ return (cleaned || 'file').slice(0, 150);
130
+ }
131
+
132
+ function jsonBuffer(value: unknown): Buffer {
133
+ return Buffer.from(JSON.stringify(value, null, 2) + '\n', 'utf8');
134
+ }
135
+
136
+ /** Keyset walk of the audit log, one NDJSON line per row (never buffers the table). */
137
+ async function* auditNdjson(db: Db, workspaceId: string): AsyncGenerator<string> {
138
+ let lastId = 0;
139
+ for (;;) {
140
+ const rows = await db
141
+ .select()
142
+ .from(auditLog)
143
+ .where(and(eq(auditLog.workspaceId, workspaceId), gt(auditLog.id, lastId)))
144
+ .orderBy(asc(auditLog.id))
145
+ .limit(AUDIT_BATCH);
146
+ if (rows.length === 0) return;
147
+ for (const row of rows) {
148
+ yield JSON.stringify({ ...row, createdAt: row.createdAt.toISOString() }) + '\n';
149
+ lastId = row.id;
150
+ }
151
+ if (rows.length < AUDIT_BATCH) return;
152
+ }
153
+ }
154
+
155
+ export function registerExportRoutes(api: OpenAPIHono, deps: ExportRouteDeps): void {
156
+ const { db, env, storage, audit, limiters, clientIp, logger } = deps;
157
+
158
+ // Gates, in order: rate limit (IP AND user — exports are expensive) →
159
+ // authenticated → admin+. Machine principals additionally passed the
160
+ // fail-closed export-scope gate in authContext before landing here.
161
+ api.use(
162
+ '/workspace/export',
163
+ rateLimit(limiters.workspaceExport, clientIp, async (c) => {
164
+ const principal = c.get('principal');
165
+ return principal ? [`u:${principal.userId}`] : [];
166
+ })
167
+ );
168
+ api.use('/workspace/export', requireAuth());
169
+ // Guest capability limit (D2): the whole-workspace export is the named
170
+ // workspace-scope surface guests must never reach. Defense in depth in
171
+ // front of the admin gate — a guest is minted role 'member' and the role
172
+ // is locked (api/members.ts), but export is exactly the surface where a
173
+ // second, origin-keyed wall is cheap and the failure catastrophic.
174
+ api.use('/workspace/export', requireNonGuest());
175
+ api.use('/workspace/export', requireRole('admin'));
176
+
177
+ api.openapi(deps.workspaceExportRoute, async (c) => {
178
+ const principal = c.get('principal')!;
179
+ const workspaceId = principal.workspaceId;
180
+
181
+ const [instance] = await db.select().from(instanceSettings).limit(1);
182
+ const [{ liveFiles }] = (await db
183
+ .select({ liveFiles: sql<number>`count(*)::int` })
184
+ .from(files)
185
+ .where(and(eq(files.workspaceId, workspaceId), isNull(files.deletedAt)))) as [{ liveFiles: number }];
186
+
187
+ // The tool's entries (the `api.exportEntries` slot), computed and
188
+ // serialized BEFORE the audit row and before the first byte: a
189
+ // contribution that throws, or whose name collides, answers the standard
190
+ // 500 with no audit row (the middleware writes none on a 4xx/5xx, and the
191
+ // session row below is not reached) and never a truncated zip. They are
192
+ // held in memory until their place in the pump: bounded tables only, the
193
+ // same stance as the chassis sections of step 2. No slot: nothing runs.
194
+ const toolEntries = deps.exportEntries
195
+ ? resolveExportEntries(await deps.exportEntries(db, workspaceId))
196
+ : [];
197
+
198
+ // Exactly one audit row either way: machine reads are audited by the
199
+ // middleware (it picks this up from c.set), session GETs are not
200
+ // (audit/service.ts) — so sessions write directly here.
201
+ const auditInfo = {
202
+ action: 'workspace.export',
203
+ resourceType: 'workspace',
204
+ resourceId: workspaceId,
205
+ metadata: { files: liveFiles }
206
+ };
207
+ c.set('audit', auditInfo);
208
+ if (principal.via === 'session') {
209
+ await audit.write({
210
+ workspaceId,
211
+ principal,
212
+ ...auditInfo,
213
+ requestId: c.get('requestId'),
214
+ ip: clientIp(c)
215
+ });
216
+ }
217
+
218
+ const zip = new ZipFile();
219
+ // @types/yazl types outputStream as the legacy NodeJS.ReadableStream; at
220
+ // runtime it is a real Readable (a PassThrough), which destroy/toWeb need.
221
+ const output = zip.outputStream as Readable;
222
+
223
+ // A client abort must tear down the source stream and the zip output or
224
+ // every cancelled download leaks a descriptor/S3 socket (files.ts rule).
225
+ let currentBlobStream: Readable | null = null;
226
+ c.req.raw.signal.addEventListener('abort', () => {
227
+ currentBlobStream?.destroy();
228
+ output.destroy(new Error('client aborted the export download'));
229
+ });
230
+
231
+ // Sequential pump, started before returning: entries are added in a
232
+ // fixed order while the output already streams. An error mid-pump
233
+ // destroys the output stream → the client sees a truncated (invalid)
234
+ // zip rather than a silently incomplete "valid" one — accepted: HTTP has
235
+ // no way to fail a 200 that already started streaming.
236
+ void (async () => {
237
+ // 1. Manifest first (no skip list here — it is not known yet; the
238
+ // skips land in skipped-blobs.json, written last).
239
+ zip.addBuffer(
240
+ jsonBuffer({
241
+ formatVersion: 1,
242
+ exportedAt: new Date().toISOString(),
243
+ instance: {
244
+ instanceId: instance?.instanceId ?? null,
245
+ name: instance?.name ?? null,
246
+ edition: env.EDITION,
247
+ version: env.APP_VERSION
248
+ },
249
+ workspaceId
250
+ }),
251
+ 'manifest.json'
252
+ );
253
+
254
+ // 2. The small bounded tables. Secrets are excluded by SELECTION, not
255
+ // by post-filtering: invitations.tokenHash and apiKeys.secretHash
256
+ // are simply never queried.
257
+ const [workspace] = await db
258
+ .select({ id: workspaces.id, name: workspaces.name, createdAt: workspaces.createdAt })
259
+ .from(workspaces)
260
+ .where(eq(workspaces.id, workspaceId))
261
+ .limit(1);
262
+ zip.addBuffer(jsonBuffer(workspace ?? null), 'workspace.json');
263
+
264
+ const memberRows = await db
265
+ .select({
266
+ id: workspaceMembers.id,
267
+ userId: workspaceMembers.userId,
268
+ email: userTable.email,
269
+ name: userTable.name,
270
+ role: workspaceMembers.role,
271
+ isActive: workspaceMembers.isActive,
272
+ createdAt: workspaceMembers.createdAt,
273
+ lastSeenAt: workspaceMembers.lastSeenAt
274
+ })
275
+ .from(workspaceMembers)
276
+ .innerJoin(userTable, eq(workspaceMembers.userId, userTable.id))
277
+ .where(eq(workspaceMembers.workspaceId, workspaceId))
278
+ .orderBy(asc(workspaceMembers.createdAt), asc(workspaceMembers.id));
279
+ zip.addBuffer(jsonBuffer(memberRows), 'members.json');
280
+
281
+ const invitationRows = await db
282
+ .select({
283
+ id: invitations.id,
284
+ email: invitations.email,
285
+ role: invitations.role,
286
+ invitedBy: invitations.invitedBy,
287
+ expiresAt: invitations.expiresAt,
288
+ acceptedAt: invitations.acceptedAt,
289
+ revokedAt: invitations.revokedAt,
290
+ createdAt: invitations.createdAt
291
+ })
292
+ .from(invitations)
293
+ .where(eq(invitations.workspaceId, workspaceId))
294
+ .orderBy(asc(invitations.createdAt), asc(invitations.id));
295
+ zip.addBuffer(jsonBuffer(invitationRows), 'invitations.json');
296
+
297
+ // PINNED keys only (workspace_id = this workspace): keys are USER
298
+ // credentials under the user-scoped model — an unpinned key belongs
299
+ // to its holder, not to any one workspace's export.
300
+ const apiKeyRows = await db
301
+ .select({
302
+ id: apiKeys.id,
303
+ keyId: apiKeys.keyId,
304
+ name: apiKeys.name,
305
+ scopes: apiKeys.scopes,
306
+ createdBy: apiKeys.createdBy,
307
+ createdAt: apiKeys.createdAt,
308
+ lastUsedAt: apiKeys.lastUsedAt,
309
+ revokedAt: apiKeys.revokedAt,
310
+ expiresAt: apiKeys.expiresAt
311
+ })
312
+ .from(apiKeys)
313
+ .where(eq(apiKeys.workspaceId, workspaceId))
314
+ .orderBy(asc(apiKeys.createdAt), asc(apiKeys.id));
315
+ zip.addBuffer(jsonBuffer(apiKeyRows), 'api-keys.json');
316
+
317
+ // Projects and their members, archived projects included (a project is
318
+ // never deleted). What a tool links to a project is the tool's entry.
319
+ const projectRows = await db
320
+ .select({
321
+ id: projects.id,
322
+ name: projects.name,
323
+ description: projects.description,
324
+ metadata: projects.metadata,
325
+ archivedAt: projects.archivedAt,
326
+ createdBy: projects.createdBy,
327
+ createdAt: projects.createdAt,
328
+ updatedAt: projects.updatedAt
329
+ })
330
+ .from(projects)
331
+ .where(eq(projects.workspaceId, workspaceId))
332
+ .orderBy(asc(projects.createdAt), asc(projects.id));
333
+ zip.addBuffer(jsonBuffer(projectRows), 'projects.json');
334
+
335
+ const projectMemberRows = await db
336
+ .select({
337
+ id: projectMembers.id,
338
+ projectId: projectMembers.projectId,
339
+ memberId: projectMembers.memberId,
340
+ userId: workspaceMembers.userId,
341
+ role: projectMembers.role,
342
+ addedBy: projectMembers.addedBy,
343
+ createdAt: projectMembers.createdAt
344
+ })
345
+ .from(projectMembers)
346
+ .innerJoin(projects, eq(projectMembers.projectId, projects.id))
347
+ .innerJoin(workspaceMembers, eq(projectMembers.memberId, workspaceMembers.id))
348
+ .where(eq(projects.workspaceId, workspaceId))
349
+ .orderBy(asc(projectMembers.createdAt), asc(projectMembers.id));
350
+ zip.addBuffer(jsonBuffer(projectMemberRows), 'project_members.json');
351
+
352
+ // The teams (PRDCT-2813): the tool's own and the hub's projections
353
+ // alike (`hubTeamId` tells them apart), their seats, and their places
354
+ // on projects (PRDCT-2794). The same three tables on both editions.
355
+ const teamRows = await db
356
+ .select({
357
+ id: workspaceTeams.id,
358
+ hubTeamId: workspaceTeams.hubTeamId,
359
+ slug: workspaceTeams.slug,
360
+ name: workspaceTeams.name,
361
+ createdBy: workspaceTeams.createdBy,
362
+ createdAt: workspaceTeams.createdAt,
363
+ updatedAt: workspaceTeams.updatedAt
364
+ })
365
+ .from(workspaceTeams)
366
+ .where(eq(workspaceTeams.workspaceId, workspaceId))
367
+ .orderBy(asc(workspaceTeams.createdAt), asc(workspaceTeams.id));
368
+ zip.addBuffer(jsonBuffer(teamRows), 'teams.json');
369
+
370
+ const teamMemberRows = await db
371
+ .select({
372
+ id: workspaceTeamMembers.id,
373
+ teamId: workspaceTeamMembers.teamId,
374
+ memberId: workspaceTeamMembers.memberId,
375
+ userId: workspaceMembers.userId,
376
+ addedBy: workspaceTeamMembers.addedBy,
377
+ createdAt: workspaceTeamMembers.createdAt
378
+ })
379
+ .from(workspaceTeamMembers)
380
+ .innerJoin(workspaceTeams, eq(workspaceTeamMembers.teamId, workspaceTeams.id))
381
+ .innerJoin(workspaceMembers, eq(workspaceTeamMembers.memberId, workspaceMembers.id))
382
+ .where(eq(workspaceTeams.workspaceId, workspaceId))
383
+ .orderBy(asc(workspaceTeamMembers.createdAt), asc(workspaceTeamMembers.id));
384
+ zip.addBuffer(jsonBuffer(teamMemberRows), 'team_members.json');
385
+
386
+ const projectTeamRows = await db
387
+ .select({
388
+ id: projectTeams.id,
389
+ projectId: projectTeams.projectId,
390
+ teamId: projectTeams.teamId,
391
+ role: projectTeams.role,
392
+ addedBy: projectTeams.addedBy,
393
+ createdAt: projectTeams.createdAt
394
+ })
395
+ .from(projectTeams)
396
+ .innerJoin(projects, eq(projectTeams.projectId, projects.id))
397
+ .where(eq(projects.workspaceId, workspaceId))
398
+ .orderBy(asc(projectTeams.createdAt), asc(projectTeams.id));
399
+ zip.addBuffer(jsonBuffer(projectTeamRows), 'project_teams.json');
400
+
401
+ // 3. Audit log as NDJSON, keyset-batched (this table is unbounded).
402
+ zip.addReadStream(Readable.from(auditNdjson(db, workspaceId)), 'audit-log.ndjson');
403
+
404
+ // 4. Files metadata — ALL rows including soft-deleted (deletedAt set;
405
+ // createdBy may be null after an account delete).
406
+ const fileRows = await db
407
+ .select()
408
+ .from(files)
409
+ .where(eq(files.workspaceId, workspaceId))
410
+ .orderBy(asc(files.createdAt), asc(files.id));
411
+ zip.addBuffer(
412
+ jsonBuffer(
413
+ fileRows.map((f) => ({
414
+ id: f.id,
415
+ sha256: f.sha256,
416
+ sizeBytes: f.sizeBytes,
417
+ contentType: f.contentType,
418
+ originalName: f.originalName,
419
+ createdBy: f.createdBy,
420
+ createdAt: f.createdAt.toISOString(),
421
+ deletedAt: f.deletedAt?.toISOString() ?? null
422
+ }))
423
+ ),
424
+ 'files.json'
425
+ );
426
+
427
+ // 4b. The tool's entries, in the tool's order (none without the slot).
428
+ for (const entry of toolEntries) zip.addBuffer(entry.buffer, entry.entryName);
429
+
430
+ // 5. Blobs, strictly one at a time: await each stream's end before
431
+ // opening the next so file descriptors / S3 sockets never pile up
432
+ // (yazl keeps accepting entries while piping until end()).
433
+ const skipped: Array<{ fileId: string; sha256: string; reason: string }> = [];
434
+ for (const f of fileRows) {
435
+ // Destroying yazl's output does NOT stop the pump: the ZipFile only
436
+ // latches errors emitted on itself, so entries added after an abort
437
+ // are still opened and fully drained. Check the signal explicitly or
438
+ // a cancelled multi-GB export keeps consuming disk/S3 bandwidth.
439
+ if (c.req.raw.signal.aborted) throw new Error('client aborted the export download');
440
+ if (f.deletedAt) {
441
+ skipped.push({ fileId: f.id, sha256: f.sha256, reason: 'soft-deleted' });
442
+ continue;
443
+ }
444
+ const key = blobKey(f.workspaceId, f.sha256);
445
+ if (!(await storage.exists(key))) {
446
+ skipped.push({ fileId: f.id, sha256: f.sha256, reason: 'blob-missing' });
447
+ continue;
448
+ }
449
+ const stream = await storage.getStream(key);
450
+ currentBlobStream = stream;
451
+ // compress: false — media/binary blobs don't deflate; JSON entries
452
+ // above keep yazl's default compression.
453
+ zip.addReadStream(stream, `files/${f.id}-${sanitizeEntryName(f.originalName)}`, {
454
+ compress: false
455
+ });
456
+ await finished(stream);
457
+ currentBlobStream = null;
458
+ }
459
+
460
+ // 6. The skip list last (this is why the manifest carries none).
461
+ zip.addBuffer(jsonBuffer(skipped), 'skipped-blobs.json');
462
+ zip.end();
463
+ })().catch((err: unknown) => {
464
+ // A client cancel is routine, not an operational error.
465
+ if (c.req.raw.signal.aborted) {
466
+ logger.info({ workspaceId }, 'workspace export cancelled by the client');
467
+ } else {
468
+ logger.error({ err, workspaceId }, 'workspace export pump failed — download truncated');
469
+ }
470
+ currentBlobStream?.destroy();
471
+ output.destroy(err instanceof Error ? err : new Error(String(err)));
472
+ });
473
+
474
+ const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
475
+ return c.body(Readable.toWeb(output) as unknown as ReadableStream, 200, {
476
+ 'content-type': 'application/zip',
477
+ 'content-disposition': `attachment; filename="export-${instance?.instanceId ?? 'instance'}-${stamp}.zip"`,
478
+ 'x-content-type-options': 'nosniff',
479
+ 'cache-control': 'no-store'
480
+ });
481
+ });
482
+ }
@@ -0,0 +1,200 @@
1
+ import type { OpenAPIHono } from '@hono/zod-openapi';
2
+ import type { Context } from 'hono';
3
+ import { Readable } from 'node:stream';
4
+ import type { ReadableStream as WebReadableStream } from 'node:stream/web';
5
+ import type { SQL } from 'drizzle-orm';
6
+ import { fileGetRoute, filesListRoute, fileUploadRoute } from '@antasphere/chassis-contract/routes';
7
+ import type { Principal } from '@antasphere/chassis-contract';
8
+ import type { DbConn, FileRow } from '@antasphere/chassis-db';
9
+ import type { Env } from '../env.js';
10
+ import type { ScopeRoutes } from './scope-routes.js';
11
+ import type { Logger } from '../logger.js';
12
+ import type { PlatformRegistry } from '../platform/registry.js';
13
+ import type { FileService } from '../files/service.js';
14
+ import { FileTooLargeError } from '../files/spool.js';
15
+ import { serveBlob } from '../files/serve.js';
16
+ import type { StorageDriver } from '../storage/driver.js';
17
+ import { isUuid } from '../pagination.js';
18
+ import { requireAuth, requireNonGuest } from '../middleware/auth-context.js';
19
+
20
+ const err = (code: string, message: string) => ({ error: { code, message } });
21
+
22
+ const toWire = (f: FileRow) => ({
23
+ id: f.id,
24
+ sha256: f.sha256,
25
+ sizeBytes: f.sizeBytes,
26
+ contentType: f.contentType,
27
+ originalName: f.originalName,
28
+ createdBy: f.createdBy,
29
+ createdAt: f.createdAt.toISOString()
30
+ });
31
+
32
+ export interface FileRouteDeps {
33
+ service: FileService;
34
+ storage: StorageDriver;
35
+ registry: PlatformRegistry;
36
+ env: Pick<Env, 'MAX_FILE_SIZE_MB'>;
37
+ logger: Logger;
38
+ /**
39
+ * ADR 011 blob-delete guard: true when the blob is referenced by a live
40
+ * version manifest of the tool's resource — DELETE answers 409 file_in_use instead
41
+ * of removing it. Runs inside the delete transaction (files module stays
42
+ * tool-agnostic; the wiring point injects the tool's check).
43
+ */
44
+ blobInUse: (tx: DbConn, workspaceId: string, sha256: string) => Promise<boolean>;
45
+ /**
46
+ * ADR 013 per-blob read scope (SL-B1): a WHERE predicate over the `files`
47
+ * row, or `undefined` for the workspace admin/owner operator view. Injected
48
+ * for the same reason as `blobInUse` — the files module stays
49
+ * tool-agnostic and the wiring point supplies the tool's policy.
50
+ */
51
+ blobReadScope: (principal: Principal) => SQL | undefined;
52
+ /**
53
+ * The delete route the tool instantiated (its OpenAPI 409 carries the tool's
54
+ * wording), and the tool's message of that 409 `file_in_use`
55
+ * (`copy.fileInUse`): what references a blob is the tool's to name.
56
+ */
57
+ fileDeleteRoute: ScopeRoutes['fileDeleteRoute'];
58
+ fileInUse: string;
59
+ }
60
+
61
+ export function registerFileRoutes(api: OpenAPIHono, deps: FileRouteDeps): void {
62
+ const { service, storage, registry, env, logger } = deps;
63
+ const maxBytes = env.MAX_FILE_SIZE_MB * 1024 * 1024;
64
+
65
+ api.use('/files', requireAuth());
66
+ api.use('/files/*', requireAuth());
67
+ // Guest capability limit (D2, both editions): the generic files surface is
68
+ // a WORKSPACE-level surface (ADR 006) — an inventory of the tenant's blobs
69
+ // with a delete on it. Its READS are now per-resource authorized like every
70
+ // other content read (SL-B1: `deps.blobReadScope`, ADR 013), but the
71
+ // surface as a whole still belongs to the workspace, and an external guest
72
+ // has no business there: they were invited to ONE resource, not to the host
73
+ // tenant's file cabinet. Guests push and pull its bytes through the ADR
74
+ // 013-gated routes of the tool instead (its asset upload and its
75
+ // per-resource asset download).
76
+ api.use('/files', requireNonGuest());
77
+ api.use('/files/*', requireNonGuest());
78
+
79
+ api.openapi(filesListRoute, async (c) => {
80
+ const principal = c.get('principal')!;
81
+ const { cursor, limit } = c.req.valid('query');
82
+ const { files: rows, nextCursor } = await service.list(principal.workspaceId, {
83
+ ...(cursor !== undefined ? { cursor } : {}),
84
+ limit,
85
+ // ADR 013 (SL-B1): a plain member pages the blobs they uploaded plus
86
+ // those referenced by resources they can read; admins/owners keep the
87
+ // whole-workspace operator view.
88
+ visibility: deps.blobReadScope(principal)
89
+ });
90
+ return c.json({ files: rows.map(toWire), nextCursor }, 200);
91
+ });
92
+
93
+ api.openapi(fileUploadRoute, async (c) => {
94
+ const principal = c.get('principal')!;
95
+ const { name } = c.req.valid('query');
96
+ const contentType = c.req.header('content-type') ?? 'application/octet-stream';
97
+
98
+ // The declared-size check and the usage event are the entitlement gate's
99
+ // (the billing rail, §7): a tool declares `files.upload` on this route in
100
+ // its contract and the chassis gate runs it before this handler; the hard
101
+ // cap is still enforced mid-stream below. Nothing is checked or emitted
102
+ // by hand here.
103
+ if (!c.req.raw.body) {
104
+ return c.json(err('empty_body', 'Request body required'), 400);
105
+ }
106
+
107
+ try {
108
+ const { file, deduplicated } = await service.upload({
109
+ workspaceId: principal.workspaceId,
110
+ createdBy: principal.userId,
111
+ originalName: name,
112
+ contentType,
113
+ body: Readable.fromWeb(c.req.raw.body as WebReadableStream),
114
+ maxBytes
115
+ });
116
+
117
+ c.set('audit', {
118
+ action: 'file.upload',
119
+ resourceType: 'file',
120
+ resourceId: file.id,
121
+ metadata: { name, sizeBytes: file.sizeBytes, deduplicated }
122
+ });
123
+ registry.events.emit('file.uploaded', {
124
+ workspaceId: principal.workspaceId,
125
+ fileId: file.id,
126
+ sizeBytes: file.sizeBytes
127
+ });
128
+
129
+ return c.json({ file: toWire(file), deduplicated }, 201);
130
+ } catch (e) {
131
+ if (e instanceof FileTooLargeError) {
132
+ return c.json(err('file_too_large', e.message), 413);
133
+ }
134
+ logger.error({ err: e }, 'file upload failed');
135
+ return c.json(err('upload_failed', 'Upload failed'), 400);
136
+ }
137
+ });
138
+
139
+ api.openapi(fileGetRoute, async (c) => {
140
+ const principal = c.get('principal')!;
141
+ const { id } = c.req.valid('param');
142
+ // 404, never 403, when the scope excludes the blob (ADR 013): a blob a
143
+ // principal cannot read must not be probeable by id either.
144
+ const file = await service.get(principal.workspaceId, id, deps.blobReadScope(principal));
145
+ if (!file) return c.json(err('not_found', 'File not found'), 404);
146
+ return c.json(toWire(file), 200);
147
+ });
148
+
149
+ api.openapi(deps.fileDeleteRoute, async (c) => {
150
+ const principal = c.get('principal')!;
151
+ const { id } = c.req.valid('param');
152
+ // Same scope on the mutation: a member must not be able to delete (or
153
+ // probe) a blob they may not read.
154
+ const file = await service.get(principal.workspaceId, id, deps.blobReadScope(principal));
155
+ if (!file) return c.json(err('not_found', 'File not found'), 404);
156
+ const outcome = await service.delete(file, (tx, row) => deps.blobInUse(tx, row.workspaceId, row.sha256));
157
+ if (outcome === 'in_use') {
158
+ return c.json(err('file_in_use', deps.fileInUse), 409);
159
+ }
160
+ c.set('audit', { action: 'file.delete', resourceType: 'file', resourceId: file.id });
161
+ return c.json(toWire(file), 200);
162
+ });
163
+
164
+ // ── Content: streamed, Range-capable. Registered as plain Hono routes (a
165
+ // byte endpoint, not part of the JSON OpenAPI surface); guarded by the
166
+ // same requireAuth + scope machinery as everything else under /files.
167
+ // The streaming machinery itself (ETag/304/Range/safe-serving) is shared
168
+ // with the tool's asset download — files/serve.ts.
169
+ const contentHandler = async (c: Context, headOnly: boolean) => {
170
+ const principal = c.get('principal')!;
171
+ const id = c.req.param('id') ?? '';
172
+ // Plain Hono route (no contract param schema): reject a non-UUID id here
173
+ // or it reaches Postgres' uuid cast and surfaces as a sanitized 500.
174
+ if (!isUuid(id)) return c.json(err('not_found', 'File not found'), 404);
175
+ // The byte route carries the ADR 013 scope too (SL-B1) — this was the
176
+ // whole-tenant read: `workspace_id` alone served ANY resource's content to
177
+ // any member and to any read-scope key.
178
+ const file = await service.get(principal.workspaceId, id, deps.blobReadScope(principal));
179
+ if (!file) return c.json(err('not_found', 'File not found'), 404);
180
+
181
+ return serveBlob(c, {
182
+ storage,
183
+ logger,
184
+ workspaceId: file.workspaceId,
185
+ sha256: file.sha256,
186
+ sizeBytes: file.sizeBytes,
187
+ contentType: file.contentType,
188
+ filename: file.originalName,
189
+ headOnly
190
+ });
191
+ };
192
+
193
+ // ONE registration for both verbs. Hono rewrites HEAD to GET before routing
194
+ // (verified against the pinned version: a HEAD request runs the GET handler
195
+ // and the HEAD registration never fires), so a separate `api.on('HEAD', …)`
196
+ // is dead code and every HEAD would silently stream the whole body. Read
197
+ // the verb off the request instead — that is the only signal that survives
198
+ // the rewrite. Never add an `api.on('HEAD', …)` route on this app.
199
+ api.get('/files/:id/content', (c) => contentHandler(c, c.req.method === 'HEAD'));
200
+ }