@warlock.js/core 5.30.0 → 5.31.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 (797) hide show
  1. package/CHANGELOG.md +142 -36
  2. package/README.md +28 -23
  3. package/bin/source-load-hook.mjs +63 -0
  4. package/bin/warlock.js +102 -42
  5. package/esm/cli/commands/add.command.mjs +4 -2
  6. package/esm/cli/commands/add.command.mjs.map +1 -1
  7. package/esm/cli/commands/agents-md/agents-md-command.action.mjs +69 -0
  8. package/esm/cli/commands/agents-md/agents-md-command.action.mjs.map +1 -0
  9. package/esm/cli/commands/agents-md.command.mjs +38 -0
  10. package/esm/cli/commands/agents-md.command.mjs.map +1 -0
  11. package/esm/cli/commands/board/board-command.action.mjs +279 -0
  12. package/esm/cli/commands/board/board-command.action.mjs.map +1 -0
  13. package/esm/cli/commands/board.command.mjs +78 -0
  14. package/esm/cli/commands/board.command.mjs.map +1 -0
  15. package/esm/cli/commands/build.command.mjs +6 -1
  16. package/esm/cli/commands/build.command.mjs.map +1 -1
  17. package/esm/cli/commands/check/check-command.action.mjs +57 -0
  18. package/esm/cli/commands/check/check-command.action.mjs.map +1 -0
  19. package/esm/cli/commands/check/child-typecheck.mjs +66 -0
  20. package/esm/cli/commands/check/child-typecheck.mjs.map +1 -0
  21. package/esm/cli/commands/check/format-problems.mjs +43 -0
  22. package/esm/cli/commands/check/format-problems.mjs.map +1 -0
  23. package/esm/cli/commands/check/typecheck-command.action.mjs +61 -0
  24. package/esm/cli/commands/check/typecheck-command.action.mjs.map +1 -0
  25. package/esm/cli/commands/check.command.mjs +50 -0
  26. package/esm/cli/commands/check.command.mjs.map +1 -0
  27. package/esm/cli/commands/dev-server.command.mjs +9 -1
  28. package/esm/cli/commands/dev-server.command.mjs.map +1 -1
  29. package/esm/cli/commands/doctor/boot-for-diagnostics.mjs +8 -3
  30. package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -1
  31. package/esm/cli/commands/doctor/check.types.d.mts +76 -0
  32. package/esm/cli/commands/doctor/check.types.d.mts.map +1 -0
  33. package/esm/cli/commands/doctor/checks/config.check.mjs +1 -0
  34. package/esm/cli/commands/doctor/checks/config.check.mjs.map +1 -1
  35. package/esm/cli/commands/doctor/checks/connectors.check.mjs +2 -0
  36. package/esm/cli/commands/doctor/checks/connectors.check.mjs.map +1 -1
  37. package/esm/cli/commands/doctor/checks/handler-signature.check.mjs +1 -0
  38. package/esm/cli/commands/doctor/checks/handler-signature.check.mjs.map +1 -1
  39. package/esm/cli/commands/doctor/checks/health.check.mjs +3 -0
  40. package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -1
  41. package/esm/cli/commands/doctor/checks/jwt-secret.check.mjs +1 -0
  42. package/esm/cli/commands/doctor/checks/jwt-secret.check.mjs.map +1 -1
  43. package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs +1 -0
  44. package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs.map +1 -1
  45. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs +1 -0
  46. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
  47. package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs +4 -0
  48. package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs.map +1 -1
  49. package/esm/cli/commands/doctor/checks/routes.check.mjs +3 -0
  50. package/esm/cli/commands/doctor/checks/routes.check.mjs.map +1 -1
  51. package/esm/cli/commands/doctor/doctor-command.action.mjs +14 -6
  52. package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
  53. package/esm/cli/commands/doctor/format-report.mjs +30 -2
  54. package/esm/cli/commands/doctor/format-report.mjs.map +1 -1
  55. package/esm/cli/commands/doctor.command.mjs +4 -0
  56. package/esm/cli/commands/doctor.command.mjs.map +1 -1
  57. package/esm/cli/commands/explain/explain-command.action.mjs +64 -0
  58. package/esm/cli/commands/explain/explain-command.action.mjs.map +1 -0
  59. package/esm/cli/commands/explain.command.mjs +23 -0
  60. package/esm/cli/commands/explain.command.mjs.map +1 -0
  61. package/esm/cli/commands/generate/generate.command.mjs +124 -124
  62. package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
  63. package/esm/cli/commands/generate/generators/endpoint.generator.mjs +311 -0
  64. package/esm/cli/commands/generate/generators/endpoint.generator.mjs.map +1 -0
  65. package/esm/cli/commands/generate/generators/migration.generator.mjs +1 -1
  66. package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
  67. package/esm/cli/commands/generate/generators/model.generator.mjs +1 -1
  68. package/esm/cli/commands/generate/templates/endpoint-stubs.mjs +341 -0
  69. package/esm/cli/commands/generate/templates/endpoint-stubs.mjs.map +1 -0
  70. package/esm/cli/commands/generate/templates/stubs.mjs +19 -6
  71. package/esm/cli/commands/generate/templates/stubs.mjs.map +1 -1
  72. package/esm/cli/commands/generate/utils/endpoint-schema.mjs +137 -0
  73. package/esm/cli/commands/generate/utils/endpoint-schema.mjs.map +1 -0
  74. package/esm/cli/commands/generate/utils/name-parser.mjs +1 -1
  75. package/esm/cli/commands/generate/utils/name-parser.mjs.map +1 -1
  76. package/esm/cli/commands/generate/utils/prompt.mjs.map +1 -1
  77. package/esm/cli/commands/generate/utils/writer.mjs +38 -13
  78. package/esm/cli/commands/generate/utils/writer.mjs.map +1 -1
  79. package/esm/cli/commands/inspect/inspect-command.action.mjs +72 -0
  80. package/esm/cli/commands/inspect/inspect-command.action.mjs.map +1 -0
  81. package/esm/cli/commands/inspect.command.mjs +39 -0
  82. package/esm/cli/commands/inspect.command.mjs.map +1 -0
  83. package/esm/cli/commands/mcp/mcp-command.action.mjs +103 -0
  84. package/esm/cli/commands/mcp/mcp-command.action.mjs.map +1 -0
  85. package/esm/cli/commands/mcp.command.mjs +76 -0
  86. package/esm/cli/commands/mcp.command.mjs.map +1 -0
  87. package/esm/cli/commands/openapi-generator.action.mjs.map +1 -1
  88. package/esm/cli/commands/openapi-generator.command.mjs.map +1 -1
  89. package/esm/cli/commands/postman-generator.action.mjs.map +1 -1
  90. package/esm/cli/commands/postman-generator.command.mjs.map +1 -1
  91. package/esm/cli/commands/routes/routes-diff.action.mjs +1 -1
  92. package/esm/cli/commands/test.command.mjs +131 -0
  93. package/esm/cli/commands/test.command.mjs.map +1 -0
  94. package/esm/cli/commands/trace/trace-command.action.mjs +145 -0
  95. package/esm/cli/commands/trace/trace-command.action.mjs.map +1 -0
  96. package/esm/cli/commands/trace.command.mjs +60 -0
  97. package/esm/cli/commands/trace.command.mjs.map +1 -0
  98. package/esm/cli/commands/typecheck.command.mjs +40 -0
  99. package/esm/cli/commands/typecheck.command.mjs.map +1 -0
  100. package/esm/cli/commands/upgrade.command.mjs +46 -0
  101. package/esm/cli/commands/upgrade.command.mjs.map +1 -0
  102. package/esm/cli/flush-and-exit.mjs +34 -0
  103. package/esm/cli/flush-and-exit.mjs.map +1 -0
  104. package/esm/cli/framework-cli-commands.mjs +23 -1
  105. package/esm/cli/framework-cli-commands.mjs.map +1 -1
  106. package/esm/cli/parse-cli-args.mjs +1 -0
  107. package/esm/cli/parse-cli-args.mjs.map +1 -1
  108. package/esm/config/config-loader.d.mts.map +1 -1
  109. package/esm/config/config-loader.mjs +34 -13
  110. package/esm/config/config-loader.mjs.map +1 -1
  111. package/esm/config/config-schema.d.mts +67 -0
  112. package/esm/config/config-schema.d.mts.map +1 -0
  113. package/esm/config/config-schema.mjs +77 -0
  114. package/esm/config/config-schema.mjs.map +1 -0
  115. package/esm/config/index.d.mts +6 -0
  116. package/esm/config/index.mjs +1 -0
  117. package/esm/connectors/assert-no-reserved-connector-names.d.mts.map +1 -1
  118. package/esm/connectors/assert-no-reserved-connector-names.mjs +2 -1
  119. package/esm/connectors/assert-no-reserved-connector-names.mjs.map +1 -1
  120. package/esm/connectors/assert-unique-connector-names.d.mts.map +1 -1
  121. package/esm/connectors/assert-unique-connector-names.mjs +26 -2
  122. package/esm/connectors/assert-unique-connector-names.mjs.map +1 -1
  123. package/esm/connectors/http-connector.d.mts.map +1 -1
  124. package/esm/connectors/http-connector.mjs +3 -2
  125. package/esm/connectors/http-connector.mjs.map +1 -1
  126. package/esm/connectors/single-server-warnings.mjs.map +1 -1
  127. package/esm/connectors/socket-connector.d.mts.map +1 -1
  128. package/esm/connectors/socket-connector.mjs.map +1 -1
  129. package/esm/container/index.d.mts.map +1 -1
  130. package/esm/container/index.mjs.map +1 -1
  131. package/esm/database/migrate-action.mjs.map +1 -1
  132. package/esm/database/models/database-log/database-log.d.mts +0 -28
  133. package/esm/database/models/database-log/database-log.d.mts.map +1 -1
  134. package/esm/database/seeds/seeder.errors.d.mts +4 -0
  135. package/esm/database/seeds/seeder.errors.d.mts.map +1 -1
  136. package/esm/database/seeds/seeder.errors.mjs +2 -0
  137. package/esm/database/seeds/seeder.errors.mjs.map +1 -1
  138. package/esm/dev-server/boot-precondition-error.mjs +1 -0
  139. package/esm/dev-server/boot-precondition-error.mjs.map +1 -1
  140. package/esm/dev-server/create-worker.mjs +15 -9
  141. package/esm/dev-server/create-worker.mjs.map +1 -1
  142. package/esm/dev-server/dev-agent-tooling.mjs +135 -0
  143. package/esm/dev-server/dev-agent-tooling.mjs.map +1 -0
  144. package/esm/dev-server/dev-logger.mjs.map +1 -1
  145. package/esm/dev-server/development-server.mjs +8 -0
  146. package/esm/dev-server/development-server.mjs.map +1 -1
  147. package/esm/dev-server/files-orchestrator.mjs +4 -5
  148. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  149. package/esm/dev-server/files-watcher.mjs +1 -1
  150. package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs +2 -3
  151. package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs.map +1 -1
  152. package/esm/dev-server/health-checker/checkers/oxlint-health-checker.mjs +33 -0
  153. package/esm/dev-server/health-checker/checkers/oxlint-health-checker.mjs.map +1 -0
  154. package/esm/dev-server/health-checker/default-health-checkers.mjs +31 -0
  155. package/esm/dev-server/health-checker/default-health-checkers.mjs.map +1 -0
  156. package/esm/dev-server/health-checker/file-health-checker.contract.d.mts +9 -3
  157. package/esm/dev-server/health-checker/file-health-checker.contract.d.mts.map +1 -1
  158. package/esm/dev-server/health-checker/files-healthcare.manager.mjs +15 -4
  159. package/esm/dev-server/health-checker/files-healthcare.manager.mjs.map +1 -1
  160. package/esm/dev-server/health-checker/oxlint-runner.mjs +302 -0
  161. package/esm/dev-server/health-checker/oxlint-runner.mjs.map +1 -0
  162. package/esm/dev-server/health-checker/workers/oxlint-health.worker.d.mts +1 -0
  163. package/esm/dev-server/health-checker/workers/oxlint-health.worker.mjs +42 -0
  164. package/esm/dev-server/health-checker/workers/oxlint-health.worker.mjs.map +1 -0
  165. package/esm/dev-server/loader/loader-port-acks.mjs +1 -1
  166. package/esm/dev-server/loader/loader-port-acks.mjs.map +1 -1
  167. package/esm/dev-server/loader/register-loader.mjs +1 -1
  168. package/esm/dev-server/loader/register-loader.mjs.map +1 -1
  169. package/esm/dev-server/loader/resolve-hook.mjs +1 -1
  170. package/esm/dev-server/loader/transpile-cache.mjs +1 -1
  171. package/esm/dev-server/model-resource-typings.mjs.map +1 -1
  172. package/esm/dev-server/module-loader.mjs +1 -0
  173. package/esm/dev-server/module-loader.mjs.map +1 -1
  174. package/esm/dev-server/parse-imports.mjs +1 -1
  175. package/esm/dev-server/parse-imports.mjs.map +1 -1
  176. package/esm/dev-server/route-types-publisher.mjs +1 -1
  177. package/esm/dev-server/start-development-server.mjs.map +1 -1
  178. package/esm/dev-server/translation-keys-sources.mjs +1 -1
  179. package/esm/dev-server/translation-keys-sources.mjs.map +1 -1
  180. package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
  181. package/esm/dev-server/type-generator.mjs +3 -3
  182. package/esm/dev-server/type-generator.mjs.map +1 -1
  183. package/esm/dev-server/types.d.mts.map +1 -1
  184. package/esm/errors/container-key-missing-error.mjs +1 -0
  185. package/esm/errors/container-key-missing-error.mjs.map +1 -1
  186. package/esm/errors/error-codes.d.mts +653 -0
  187. package/esm/errors/error-codes.d.mts.map +1 -0
  188. package/esm/errors/error-codes.mjs +634 -0
  189. package/esm/errors/error-codes.mjs.map +1 -0
  190. package/esm/errors/esbuild-binary-missing-error.mjs +1 -0
  191. package/esm/errors/esbuild-binary-missing-error.mjs.map +1 -1
  192. package/esm/errors/index.d.mts +3 -1
  193. package/esm/errors/index.mjs +3 -1
  194. package/esm/errors/unknown-locale-error.mjs +1 -0
  195. package/esm/errors/unknown-locale-error.mjs.map +1 -1
  196. package/esm/errors/warlock-error.d.mts +23 -0
  197. package/esm/errors/warlock-error.d.mts.map +1 -0
  198. package/esm/errors/warlock-error.mjs +22 -0
  199. package/esm/errors/warlock-error.mjs.map +1 -0
  200. package/esm/generations/add-command.action.mjs +19 -3
  201. package/esm/generations/add-command.action.mjs.map +1 -1
  202. package/esm/generations/features/access.feature.mjs +1 -1
  203. package/esm/generations/features/access.feature.mjs.map +1 -1
  204. package/esm/generations/features/bull-board.feature.mjs +1 -1
  205. package/esm/generations/features/bull-board.feature.mjs.map +1 -1
  206. package/esm/generations/features/mysql.feature.mjs.map +1 -1
  207. package/esm/generations/features/notifications.feature.mjs +5 -2
  208. package/esm/generations/features/notifications.feature.mjs.map +1 -1
  209. package/esm/generations/features/queue.feature.mjs +1 -1
  210. package/esm/generations/features/queue.feature.mjs.map +1 -1
  211. package/esm/generations/features/react-email.feature.mjs +1 -1
  212. package/esm/generations/features/react-email.feature.mjs.map +1 -1
  213. package/esm/generations/features/shadcn.feature.mjs +1 -1
  214. package/esm/generations/features/shadcn.feature.mjs.map +1 -1
  215. package/esm/generations/features/shared/link-ai-package-import.mjs +1 -1
  216. package/esm/generations/features/shared/link-ai-package-import.mjs.map +1 -1
  217. package/esm/generations/features/shared/merge-web-sitemap-config.mjs.map +1 -1
  218. package/esm/generations/features/shared/patch-tsconfig-include.mjs +1 -1
  219. package/esm/generations/features/shared/patch-tsconfig-include.mjs.map +1 -1
  220. package/esm/generations/features/shared/relocate-conflicting-home-route.mjs +1 -1
  221. package/esm/generations/features/shared/relocate-conflicting-home-route.mjs.map +1 -1
  222. package/esm/generations/features/sitemap.feature.mjs +1 -1
  223. package/esm/generations/features/sitemap.feature.mjs.map +1 -1
  224. package/esm/generations/features/tailwind.feature.mjs +1 -1
  225. package/esm/generations/features/tailwind.feature.mjs.map +1 -1
  226. package/esm/generations/features/test.feature.mjs +11 -6
  227. package/esm/generations/features/test.feature.mjs.map +1 -1
  228. package/esm/generations/features/web.feature.mjs +1 -1
  229. package/esm/generations/features/web.feature.mjs.map +1 -1
  230. package/esm/generations/generation-fs.mjs +73 -0
  231. package/esm/generations/generation-fs.mjs.map +1 -0
  232. package/esm/generations/generation-run.mjs +185 -0
  233. package/esm/generations/generation-run.mjs.map +1 -0
  234. package/esm/generations/stubs.mjs +155 -13
  235. package/esm/generations/stubs.mjs.map +1 -1
  236. package/esm/http/bounded-stream-queue.d.mts +10 -0
  237. package/esm/http/bounded-stream-queue.d.mts.map +1 -0
  238. package/esm/http/bounded-stream-queue.mjs +97 -0
  239. package/esm/http/bounded-stream-queue.mjs.map +1 -0
  240. package/esm/http/build-rate-limit-options.d.mts +13 -0
  241. package/esm/http/build-rate-limit-options.d.mts.map +1 -1
  242. package/esm/http/build-rate-limit-options.mjs.map +1 -1
  243. package/esm/http/conditional-request.d.mts +86 -0
  244. package/esm/http/conditional-request.d.mts.map +1 -0
  245. package/esm/http/conditional-request.mjs +121 -0
  246. package/esm/http/conditional-request.mjs.map +1 -0
  247. package/esm/http/csp.d.mts +2 -0
  248. package/esm/http/csp.d.mts.map +1 -1
  249. package/esm/http/csp.mjs +1 -0
  250. package/esm/http/csp.mjs.map +1 -1
  251. package/esm/http/csrf-origin-policy.mjs.map +1 -1
  252. package/esm/http/database/RequestLog.d.mts +24 -84
  253. package/esm/http/database/RequestLog.d.mts.map +1 -1
  254. package/esm/http/errors/errors.d.mts +2 -0
  255. package/esm/http/errors/errors.d.mts.map +1 -1
  256. package/esm/http/errors/errors.mjs +1 -0
  257. package/esm/http/errors/errors.mjs.map +1 -1
  258. package/esm/http/escape-header-value.d.mts +18 -0
  259. package/esm/http/escape-header-value.d.mts.map +1 -0
  260. package/esm/http/escape-header-value.mjs +27 -0
  261. package/esm/http/escape-header-value.mjs.map +1 -0
  262. package/esm/http/flush-pending-cookies.mjs.map +1 -1
  263. package/esm/http/index.d.mts +5 -2
  264. package/esm/http/index.mjs +4 -1
  265. package/esm/http/lazy-static.mjs +194 -0
  266. package/esm/http/lazy-static.mjs.map +1 -0
  267. package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
  268. package/esm/http/plugins.d.mts.map +1 -1
  269. package/esm/http/plugins.mjs +7 -4
  270. package/esm/http/plugins.mjs.map +1 -1
  271. package/esm/http/rate-limit-cache-store.mjs +2 -1
  272. package/esm/http/rate-limit-cache-store.mjs.map +1 -1
  273. package/esm/http/request-log-snapshot.d.mts +12 -5
  274. package/esm/http/request-log-snapshot.d.mts.map +1 -1
  275. package/esm/http/request-log-snapshot.mjs +83 -4
  276. package/esm/http/request-log-snapshot.mjs.map +1 -1
  277. package/esm/http/request.d.mts +2 -2
  278. package/esm/http/request.d.mts.map +1 -1
  279. package/esm/http/request.mjs +5 -4
  280. package/esm/http/request.mjs.map +1 -1
  281. package/esm/http/response.d.mts +11 -1
  282. package/esm/http/response.d.mts.map +1 -1
  283. package/esm/http/response.mjs +164 -76
  284. package/esm/http/response.mjs.map +1 -1
  285. package/esm/http/safe-redirect.d.mts +51 -0
  286. package/esm/http/safe-redirect.d.mts.map +1 -0
  287. package/esm/http/safe-redirect.mjs +80 -0
  288. package/esm/http/safe-redirect.mjs.map +1 -0
  289. package/esm/http/serialized.d.mts.map +1 -1
  290. package/esm/http/server.d.mts +3 -1
  291. package/esm/http/server.d.mts.map +1 -1
  292. package/esm/http/server.mjs +14 -4
  293. package/esm/http/server.mjs.map +1 -1
  294. package/esm/http/stream-transport.mjs +4 -2
  295. package/esm/http/stream-transport.mjs.map +1 -1
  296. package/esm/http/types.d.mts +44 -11
  297. package/esm/http/types.d.mts.map +1 -1
  298. package/esm/http/uploads/generate-image-variant.mjs +1 -1
  299. package/esm/http/uploads/generate-image-variants.d.mts +10 -7
  300. package/esm/http/uploads/generate-image-variants.d.mts.map +1 -1
  301. package/esm/http/uploads/generate-image-variants.mjs +61 -24
  302. package/esm/http/uploads/generate-image-variants.mjs.map +1 -1
  303. package/esm/http/uploads/image-variant-cache-entries.mjs +78 -0
  304. package/esm/http/uploads/image-variant-cache-entries.mjs.map +1 -0
  305. package/esm/http/uploads/image-variant-cache.mjs +72 -30
  306. package/esm/http/uploads/image-variant-cache.mjs.map +1 -1
  307. package/esm/http/uploads/image-variant-types.d.mts +2 -0
  308. package/esm/http/uploads/image-variant-types.d.mts.map +1 -1
  309. package/esm/http/uploads/image-variants-config-error.d.mts +3 -1
  310. package/esm/http/uploads/image-variants-config-error.d.mts.map +1 -1
  311. package/esm/http/uploads/image-variants-config-error.mjs +3 -2
  312. package/esm/http/uploads/image-variants-config-error.mjs.map +1 -1
  313. package/esm/http/uploads/load-variant-source.mjs +1 -1
  314. package/esm/http/uploads/load-variant-source.mjs.map +1 -1
  315. package/esm/http/uploads/purge-image-variants.d.mts +7 -2
  316. package/esm/http/uploads/purge-image-variants.d.mts.map +1 -1
  317. package/esm/http/uploads/purge-image-variants.mjs +16 -16
  318. package/esm/http/uploads/purge-image-variants.mjs.map +1 -1
  319. package/esm/http/uploads/remote-variant-source.mjs +115 -0
  320. package/esm/http/uploads/remote-variant-source.mjs.map +1 -0
  321. package/esm/http/uploads/resolve-image-variant.d.mts +3 -4
  322. package/esm/http/uploads/resolve-image-variant.d.mts.map +1 -1
  323. package/esm/http/uploads/resolve-image-variant.mjs +10 -18
  324. package/esm/http/uploads/resolve-image-variant.mjs.map +1 -1
  325. package/esm/http/uploads/resolve-image-variants-config.mjs +5 -3
  326. package/esm/http/uploads/resolve-image-variants-config.mjs.map +1 -1
  327. package/esm/http/uploads/resolve-remote-image-variant.mjs +89 -0
  328. package/esm/http/uploads/resolve-remote-image-variant.mjs.map +1 -0
  329. package/esm/http/uploads/trim-image-variant-cache.mjs +193 -0
  330. package/esm/http/uploads/trim-image-variant-cache.mjs.map +1 -0
  331. package/esm/http/uploads/uploaded-file.controller.d.mts +5 -3
  332. package/esm/http/uploads/uploaded-file.controller.d.mts.map +1 -1
  333. package/esm/http/uploads/uploaded-file.controller.mjs +16 -8
  334. package/esm/http/uploads/uploaded-file.controller.mjs.map +1 -1
  335. package/esm/http/uploads/variant-cache-key.mjs +1 -1
  336. package/esm/http/uploads-types.d.mts +6 -2
  337. package/esm/http/uploads-types.d.mts.map +1 -1
  338. package/esm/index.d.mts +17 -6
  339. package/esm/index.mjs +14 -3
  340. package/esm/localization/parse-locale-dictionary.d.mts +2 -0
  341. package/esm/localization/parse-locale-dictionary.d.mts.map +1 -1
  342. package/esm/localization/parse-locale-dictionary.mjs +1 -0
  343. package/esm/localization/parse-locale-dictionary.mjs.map +1 -1
  344. package/esm/mail/config.d.mts +12 -1
  345. package/esm/mail/config.d.mts.map +1 -1
  346. package/esm/mail/config.mjs +16 -1
  347. package/esm/mail/config.mjs.map +1 -1
  348. package/esm/mail/index.d.mts +1 -1
  349. package/esm/mail/index.mjs +1 -1
  350. package/esm/net/index.d.mts +2 -0
  351. package/esm/net/index.mjs +3 -0
  352. package/esm/openapi/build-openapi-document.d.mts.map +1 -1
  353. package/esm/openapi/build-openapi-document.mjs +8 -5
  354. package/esm/openapi/build-openapi-document.mjs.map +1 -1
  355. package/esm/openapi/error-responses.mjs +28 -2
  356. package/esm/openapi/error-responses.mjs.map +1 -1
  357. package/esm/openapi/postman/build-request-body.mjs.map +1 -1
  358. package/esm/openapi/postman/build-request-url.mjs.map +1 -1
  359. package/esm/openapi/postman/build-saved-responses.mjs.map +1 -1
  360. package/esm/openapi/postman/example-from-schema.d.mts.map +1 -1
  361. package/esm/openapi/postman/example-from-schema.mjs.map +1 -1
  362. package/esm/openapi/postman/openapi-to-postman-collection.d.mts.map +1 -1
  363. package/esm/openapi/postman/openapi-to-postman-collection.mjs.map +1 -1
  364. package/esm/openapi/postman/schema-refs.mjs.map +1 -1
  365. package/esm/openapi/postman/uuid-v5.mjs.map +1 -1
  366. package/esm/openapi/request-schemas.mjs.map +1 -1
  367. package/esm/openapi/response-schemas.mjs.map +1 -1
  368. package/esm/openapi/schema-registry.d.mts +1 -0
  369. package/esm/openapi/schema-registry.mjs.map +1 -1
  370. package/esm/openapi/security.mjs.map +1 -1
  371. package/esm/openapi/types.d.mts +3 -2
  372. package/esm/openapi/types.d.mts.map +1 -1
  373. package/esm/production/assert-generated-imports.mjs +1 -0
  374. package/esm/production/assert-generated-imports.mjs.map +1 -1
  375. package/esm/production/production-builder.mjs +12 -4
  376. package/esm/production/production-builder.mjs.map +1 -1
  377. package/esm/production/resolve-build-config.mjs +1 -0
  378. package/esm/production/resolve-build-config.mjs.map +1 -1
  379. package/esm/production/route-registration-child.mjs.map +1 -1
  380. package/esm/production/route-registration-snapshot.mjs.map +1 -1
  381. package/esm/repositories/contracts/query-builder.contract.d.mts.map +1 -1
  382. package/esm/repositories/contracts/types.d.mts.map +1 -1
  383. package/esm/repositories/repository.manager.mjs.map +1 -1
  384. package/esm/resource/resource.d.mts.map +1 -1
  385. package/esm/resource/resource.mjs.map +1 -1
  386. package/esm/resource/types.d.mts.map +1 -1
  387. package/esm/restful/restful.d.mts +5 -5
  388. package/esm/restful/restful.d.mts.map +1 -1
  389. package/esm/restful/restful.mjs.map +1 -1
  390. package/esm/router/build-route-rate-limit.mjs.map +1 -1
  391. package/esm/router/dev-dispatch.mjs +31 -12
  392. package/esm/router/dev-dispatch.mjs.map +1 -1
  393. package/esm/router/index.d.mts +4 -2
  394. package/esm/router/index.mjs +2 -0
  395. package/esm/router/log-request-lifecycle.mjs.map +1 -1
  396. package/esm/router/positional-handler-diagnostics.d.mts.map +1 -1
  397. package/esm/router/positional-handler-diagnostics.mjs.map +1 -1
  398. package/esm/router/resolve-route-rate-limit.mjs +47 -0
  399. package/esm/router/resolve-route-rate-limit.mjs.map +1 -0
  400. package/esm/router/resolve-route-validation.mjs +19 -0
  401. package/esm/router/resolve-route-validation.mjs.map +1 -0
  402. package/esm/router/route-guard-names.mjs +30 -0
  403. package/esm/router/route-guard-names.mjs.map +1 -0
  404. package/esm/router/route-name-taken-error.d.mts +14 -0
  405. package/esm/router/route-name-taken-error.d.mts.map +1 -0
  406. package/esm/router/route-name-taken-error.mjs +18 -0
  407. package/esm/router/route-name-taken-error.mjs.map +1 -0
  408. package/esm/router/router.d.mts +32 -13
  409. package/esm/router/router.d.mts.map +1 -1
  410. package/esm/router/router.mjs +71 -38
  411. package/esm/router/router.mjs.map +1 -1
  412. package/esm/router/types.d.mts +137 -17
  413. package/esm/router/types.d.mts.map +1 -1
  414. package/esm/router/unguarded-route-diagnostics.d.mts +40 -0
  415. package/esm/router/unguarded-route-diagnostics.d.mts.map +1 -0
  416. package/esm/router/unguarded-route-diagnostics.mjs +95 -0
  417. package/esm/router/unguarded-route-diagnostics.mjs.map +1 -0
  418. package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
  419. package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
  420. package/esm/storage/drivers/local-driver.d.mts.map +1 -1
  421. package/esm/storage/drivers/local-driver.mjs.map +1 -1
  422. package/esm/storage/local-disk-roots.mjs +24 -0
  423. package/esm/storage/local-disk-roots.mjs.map +1 -0
  424. package/esm/storage/storage.d.mts +14 -0
  425. package/esm/storage/storage.d.mts.map +1 -1
  426. package/esm/storage/storage.mjs +22 -0
  427. package/esm/storage/storage.mjs.map +1 -1
  428. package/esm/storage/types.d.mts.map +1 -1
  429. package/esm/storage/utils/storage-not-initialized-error.d.mts +2 -0
  430. package/esm/storage/utils/storage-not-initialized-error.d.mts.map +1 -1
  431. package/esm/storage/utils/storage-not-initialized-error.mjs +1 -0
  432. package/esm/storage/utils/storage-not-initialized-error.mjs.map +1 -1
  433. package/esm/tests/authenticated-test-client.d.mts +32 -0
  434. package/esm/tests/authenticated-test-client.d.mts.map +1 -0
  435. package/esm/tests/authenticated-test-client.mjs +40 -0
  436. package/esm/tests/authenticated-test-client.mjs.map +1 -0
  437. package/esm/tests/factories.d.mts +59 -0
  438. package/esm/tests/factories.d.mts.map +1 -0
  439. package/esm/tests/factories.mjs +105 -0
  440. package/esm/tests/factories.mjs.map +1 -0
  441. package/esm/tests/fakes/cache-fake.d.mts +29 -0
  442. package/esm/tests/fakes/cache-fake.d.mts.map +1 -0
  443. package/esm/tests/fakes/cache-fake.mjs +79 -0
  444. package/esm/tests/fakes/cache-fake.mjs.map +1 -0
  445. package/esm/tests/fakes/clock-fake.d.mts +32 -0
  446. package/esm/tests/fakes/clock-fake.d.mts.map +1 -0
  447. package/esm/tests/fakes/clock-fake.mjs +69 -0
  448. package/esm/tests/fakes/clock-fake.mjs.map +1 -0
  449. package/esm/tests/fakes/fake-assertion-error.d.mts +13 -0
  450. package/esm/tests/fakes/fake-assertion-error.d.mts.map +1 -0
  451. package/esm/tests/fakes/fake-assertion-error.mjs +20 -0
  452. package/esm/tests/fakes/fake-assertion-error.mjs.map +1 -0
  453. package/esm/tests/fakes/ids-fake.d.mts +22 -0
  454. package/esm/tests/fakes/ids-fake.d.mts.map +1 -0
  455. package/esm/tests/fakes/ids-fake.mjs +57 -0
  456. package/esm/tests/fakes/ids-fake.mjs.map +1 -0
  457. package/esm/tests/fakes/index.d.mts +8 -0
  458. package/esm/tests/fakes/index.mjs +10 -0
  459. package/esm/tests/fakes/mail-fake.d.mts +41 -0
  460. package/esm/tests/fakes/mail-fake.d.mts.map +1 -0
  461. package/esm/tests/fakes/mail-fake.mjs +111 -0
  462. package/esm/tests/fakes/mail-fake.mjs.map +1 -0
  463. package/esm/tests/fakes/queue-fake.d.mts +48 -0
  464. package/esm/tests/fakes/queue-fake.d.mts.map +1 -0
  465. package/esm/tests/fakes/queue-fake.mjs +134 -0
  466. package/esm/tests/fakes/queue-fake.mjs.map +1 -0
  467. package/esm/tests/fakes/storage-fake.d.mts +31 -0
  468. package/esm/tests/fakes/storage-fake.d.mts.map +1 -0
  469. package/esm/tests/fakes/storage-fake.mjs +122 -0
  470. package/esm/tests/fakes/storage-fake.mjs.map +1 -0
  471. package/esm/tests/fakes/test-fakes.d.mts +39 -0
  472. package/esm/tests/fakes/test-fakes.d.mts.map +1 -0
  473. package/esm/tests/fakes/test-fakes.mjs +90 -0
  474. package/esm/tests/fakes/test-fakes.mjs.map +1 -0
  475. package/esm/tests/index.d.mts +14 -2
  476. package/esm/tests/index.mjs +15 -2
  477. package/esm/tests/snapshot-response.d.mts +34 -0
  478. package/esm/tests/snapshot-response.d.mts.map +1 -0
  479. package/esm/tests/snapshot-response.mjs +42 -0
  480. package/esm/tests/snapshot-response.mjs.map +1 -0
  481. package/esm/tests/start-http-development-server.d.mts +22 -0
  482. package/esm/tests/start-http-development-server.d.mts.map +1 -1
  483. package/esm/tests/start-http-development-server.mjs +36 -3
  484. package/esm/tests/start-http-development-server.mjs.map +1 -1
  485. package/esm/tests/test-helpers.d.mts +20 -1
  486. package/esm/tests/test-helpers.d.mts.map +1 -1
  487. package/esm/tests/test-helpers.mjs +40 -27
  488. package/esm/tests/test-helpers.mjs.map +1 -1
  489. package/esm/tests/test-http-server.d.mts +62 -0
  490. package/esm/tests/test-http-server.d.mts.map +1 -0
  491. package/esm/tests/test-http-server.mjs +119 -0
  492. package/esm/tests/test-http-server.mjs.map +1 -0
  493. package/esm/tests/vitest-setup.d.mts +11 -0
  494. package/esm/tests/vitest-setup.d.mts.map +1 -1
  495. package/esm/tests/vitest-setup.mjs +12 -4
  496. package/esm/tests/vitest-setup.mjs.map +1 -1
  497. package/esm/tooling/agents-md/agents-md-markers.d.mts +27 -0
  498. package/esm/tooling/agents-md/agents-md-markers.d.mts.map +1 -0
  499. package/esm/tooling/agents-md/agents-md-markers.mjs +59 -0
  500. package/esm/tooling/agents-md/agents-md-markers.mjs.map +1 -0
  501. package/esm/tooling/agents-md/agents-md-refresher.d.mts +36 -0
  502. package/esm/tooling/agents-md/agents-md-refresher.d.mts.map +1 -0
  503. package/esm/tooling/agents-md/agents-md-refresher.mjs +72 -0
  504. package/esm/tooling/agents-md/agents-md-refresher.mjs.map +1 -0
  505. package/esm/tooling/agents-md/index.mjs +7 -0
  506. package/esm/tooling/agents-md/installed-skills.d.mts +30 -0
  507. package/esm/tooling/agents-md/installed-skills.d.mts.map +1 -0
  508. package/esm/tooling/agents-md/installed-skills.mjs +74 -0
  509. package/esm/tooling/agents-md/installed-skills.mjs.map +1 -0
  510. package/esm/tooling/agents-md/render-agents-md.d.mts +33 -0
  511. package/esm/tooling/agents-md/render-agents-md.d.mts.map +1 -0
  512. package/esm/tooling/agents-md/render-agents-md.mjs +195 -0
  513. package/esm/tooling/agents-md/render-agents-md.mjs.map +1 -0
  514. package/esm/tooling/agents-md/write-agents-md.d.mts +33 -0
  515. package/esm/tooling/agents-md/write-agents-md.d.mts.map +1 -0
  516. package/esm/tooling/agents-md/write-agents-md.mjs +49 -0
  517. package/esm/tooling/agents-md/write-agents-md.mjs.map +1 -0
  518. package/esm/tooling/board/board-message.d.mts +31 -0
  519. package/esm/tooling/board/board-message.d.mts.map +1 -0
  520. package/esm/tooling/board/board-message.mjs +86 -0
  521. package/esm/tooling/board/board-message.mjs.map +1 -0
  522. package/esm/tooling/board/board.d.mts +109 -0
  523. package/esm/tooling/board/board.d.mts.map +1 -0
  524. package/esm/tooling/board/board.mjs +406 -0
  525. package/esm/tooling/board/board.mjs.map +1 -0
  526. package/esm/tooling/board/board.types.d.mts +72 -0
  527. package/esm/tooling/board/board.types.d.mts.map +1 -0
  528. package/esm/tooling/board/board.types.mjs +54 -0
  529. package/esm/tooling/board/board.types.mjs.map +1 -0
  530. package/esm/tooling/check/changed-files.d.mts +33 -0
  531. package/esm/tooling/check/changed-files.d.mts.map +1 -0
  532. package/esm/tooling/check/changed-files.mjs +73 -0
  533. package/esm/tooling/check/changed-files.mjs.map +1 -0
  534. package/esm/tooling/check/check-report.types.d.mts +62 -0
  535. package/esm/tooling/check/check-report.types.d.mts.map +1 -0
  536. package/esm/tooling/check/check-report.types.mjs +37 -0
  537. package/esm/tooling/check/check-report.types.mjs.map +1 -0
  538. package/esm/tooling/check/code-style-conventions.mjs +106 -0
  539. package/esm/tooling/check/code-style-conventions.mjs.map +1 -0
  540. package/esm/tooling/check/conventions-step.d.mts +34 -0
  541. package/esm/tooling/check/conventions-step.d.mts.map +1 -0
  542. package/esm/tooling/check/conventions-step.mjs +184 -0
  543. package/esm/tooling/check/conventions-step.mjs.map +1 -0
  544. package/esm/tooling/check/environment-step.d.mts +18 -0
  545. package/esm/tooling/check/environment-step.d.mts.map +1 -0
  546. package/esm/tooling/check/environment-step.mjs +78 -0
  547. package/esm/tooling/check/environment-step.mjs.map +1 -0
  548. package/esm/tooling/check/index.d.mts +8 -0
  549. package/esm/tooling/check/index.mjs +10 -0
  550. package/esm/tooling/check/lint-step.d.mts +19 -0
  551. package/esm/tooling/check/lint-step.d.mts.map +1 -0
  552. package/esm/tooling/check/lint-step.mjs +111 -0
  553. package/esm/tooling/check/lint-step.mjs.map +1 -0
  554. package/esm/tooling/check/request-input.mjs +132 -0
  555. package/esm/tooling/check/request-input.mjs.map +1 -0
  556. package/esm/tooling/check/run-check.d.mts +35 -0
  557. package/esm/tooling/check/run-check.d.mts.map +1 -0
  558. package/esm/tooling/check/run-check.mjs +192 -0
  559. package/esm/tooling/check/run-check.mjs.map +1 -0
  560. package/esm/tooling/check/run-tests.d.mts +48 -0
  561. package/esm/tooling/check/run-tests.d.mts.map +1 -0
  562. package/esm/tooling/check/run-tests.mjs +93 -0
  563. package/esm/tooling/check/run-tests.mjs.map +1 -0
  564. package/esm/tooling/check/security-conventions.mjs +648 -0
  565. package/esm/tooling/check/security-conventions.mjs.map +1 -0
  566. package/esm/tooling/check/source-ast.mjs +195 -0
  567. package/esm/tooling/check/source-ast.mjs.map +1 -0
  568. package/esm/tooling/check/source-files.mjs +27 -0
  569. package/esm/tooling/check/source-files.mjs.map +1 -0
  570. package/esm/tooling/check/tests-step.d.mts +34 -0
  571. package/esm/tooling/check/tests-step.d.mts.map +1 -0
  572. package/esm/tooling/check/tests-step.mjs +177 -0
  573. package/esm/tooling/check/tests-step.mjs.map +1 -0
  574. package/esm/tooling/dev-state/dev-server-state.d.mts +39 -0
  575. package/esm/tooling/dev-state/dev-server-state.d.mts.map +1 -0
  576. package/esm/tooling/dev-state/dev-server-state.mjs +67 -0
  577. package/esm/tooling/dev-state/dev-server-state.mjs.map +1 -0
  578. package/esm/tooling/devtools/devtools-client.d.mts +91 -0
  579. package/esm/tooling/devtools/devtools-client.d.mts.map +1 -0
  580. package/esm/tooling/devtools/devtools-client.mjs +155 -0
  581. package/esm/tooling/devtools/devtools-client.mjs.map +1 -0
  582. package/esm/tooling/generate/generation.d.mts +78 -0
  583. package/esm/tooling/generate/generation.d.mts.map +1 -0
  584. package/esm/tooling/generate/generation.mjs +123 -0
  585. package/esm/tooling/generate/generation.mjs.map +1 -0
  586. package/esm/tooling/generate/generator-spec.d.mts +57 -0
  587. package/esm/tooling/generate/generator-spec.d.mts.map +1 -0
  588. package/esm/tooling/generate/generator-spec.mjs +102 -0
  589. package/esm/tooling/generate/generator-spec.mjs.map +1 -0
  590. package/esm/tooling/generate/index.mjs +6 -0
  591. package/esm/tooling/generate/project-files.mjs +121 -0
  592. package/esm/tooling/generate/project-files.mjs.map +1 -0
  593. package/esm/tooling/generate/run-cli.d.mts +40 -0
  594. package/esm/tooling/generate/run-cli.d.mts.map +1 -0
  595. package/esm/tooling/generate/run-cli.mjs +93 -0
  596. package/esm/tooling/generate/run-cli.mjs.map +1 -0
  597. package/esm/tooling/generate/unified-diff.d.mts +37 -0
  598. package/esm/tooling/generate/unified-diff.d.mts.map +1 -0
  599. package/esm/tooling/generate/unified-diff.mjs +161 -0
  600. package/esm/tooling/generate/unified-diff.mjs.map +1 -0
  601. package/esm/tooling/index.d.mts +41 -0
  602. package/esm/tooling/index.mjs +45 -0
  603. package/esm/tooling/inspect/boot-for-inspect.d.mts +23 -0
  604. package/esm/tooling/inspect/boot-for-inspect.d.mts.map +1 -0
  605. package/esm/tooling/inspect/boot-for-inspect.mjs +63 -0
  606. package/esm/tooling/inspect/boot-for-inspect.mjs.map +1 -0
  607. package/esm/tooling/inspect/collect-config.mjs +77 -0
  608. package/esm/tooling/inspect/collect-config.mjs.map +1 -0
  609. package/esm/tooling/inspect/collect-models.d.mts +9 -0
  610. package/esm/tooling/inspect/collect-models.d.mts.map +1 -0
  611. package/esm/tooling/inspect/collect-models.mjs +154 -0
  612. package/esm/tooling/inspect/collect-models.mjs.map +1 -0
  613. package/esm/tooling/inspect/collect-packages.d.mts +10 -0
  614. package/esm/tooling/inspect/collect-packages.d.mts.map +1 -0
  615. package/esm/tooling/inspect/collect-packages.mjs +157 -0
  616. package/esm/tooling/inspect/collect-packages.mjs.map +1 -0
  617. package/esm/tooling/inspect/collect-pages.mjs +108 -0
  618. package/esm/tooling/inspect/collect-pages.mjs.map +1 -0
  619. package/esm/tooling/inspect/collect-routes.mjs +168 -0
  620. package/esm/tooling/inspect/collect-routes.mjs.map +1 -0
  621. package/esm/tooling/inspect/event-recorder.d.mts +37 -0
  622. package/esm/tooling/inspect/event-recorder.d.mts.map +1 -0
  623. package/esm/tooling/inspect/event-recorder.mjs +91 -0
  624. package/esm/tooling/inspect/event-recorder.mjs.map +1 -0
  625. package/esm/tooling/inspect/index.d.mts +7 -0
  626. package/esm/tooling/inspect/index.mjs +7 -0
  627. package/esm/tooling/inspect/inspect-app.d.mts +53 -0
  628. package/esm/tooling/inspect/inspect-app.d.mts.map +1 -0
  629. package/esm/tooling/inspect/inspect-app.mjs +92 -0
  630. package/esm/tooling/inspect/inspect-app.mjs.map +1 -0
  631. package/esm/tooling/inspect/inspect-report.schema.d.mts +15 -0
  632. package/esm/tooling/inspect/inspect-report.schema.d.mts.map +1 -0
  633. package/esm/tooling/inspect/inspect-report.schema.mjs +463 -0
  634. package/esm/tooling/inspect/inspect-report.schema.mjs.map +1 -0
  635. package/esm/tooling/inspect/inspect-report.types.d.mts +196 -0
  636. package/esm/tooling/inspect/inspect-report.types.d.mts.map +1 -0
  637. package/esm/tooling/inspect/inspect-report.types.mjs +33 -0
  638. package/esm/tooling/inspect/inspect-report.types.mjs.map +1 -0
  639. package/esm/tooling/mcp/create-mcp-context.d.mts +26 -0
  640. package/esm/tooling/mcp/create-mcp-context.d.mts.map +1 -0
  641. package/esm/tooling/mcp/create-mcp-context.mjs +75 -0
  642. package/esm/tooling/mcp/create-mcp-context.mjs.map +1 -0
  643. package/esm/tooling/mcp/db-schema.d.mts +49 -0
  644. package/esm/tooling/mcp/db-schema.d.mts.map +1 -0
  645. package/esm/tooling/mcp/db-schema.mjs +56 -0
  646. package/esm/tooling/mcp/db-schema.mjs.map +1 -0
  647. package/esm/tooling/mcp/docs-search.d.mts +72 -0
  648. package/esm/tooling/mcp/docs-search.d.mts.map +1 -0
  649. package/esm/tooling/mcp/docs-search.mjs +178 -0
  650. package/esm/tooling/mcp/docs-search.mjs.map +1 -0
  651. package/esm/tooling/mcp/index.mjs +9 -0
  652. package/esm/tooling/mcp/load-ai-tools.d.mts +62 -0
  653. package/esm/tooling/mcp/load-ai-tools.d.mts.map +1 -0
  654. package/esm/tooling/mcp/load-ai-tools.mjs +44 -0
  655. package/esm/tooling/mcp/load-ai-tools.mjs.map +1 -0
  656. package/esm/tooling/mcp/mcp-tools.d.mts +60 -0
  657. package/esm/tooling/mcp/mcp-tools.d.mts.map +1 -0
  658. package/esm/tooling/mcp/mcp-tools.mjs +513 -0
  659. package/esm/tooling/mcp/mcp-tools.mjs.map +1 -0
  660. package/esm/tooling/mcp/start-mcp-server.d.mts +56 -0
  661. package/esm/tooling/mcp/start-mcp-server.d.mts.map +1 -0
  662. package/esm/tooling/mcp/start-mcp-server.mjs +126 -0
  663. package/esm/tooling/mcp/start-mcp-server.mjs.map +1 -0
  664. package/esm/tooling/mcp/tool-input.d.mts +75 -0
  665. package/esm/tooling/mcp/tool-input.d.mts.map +1 -0
  666. package/esm/tooling/mcp/tool-input.mjs +72 -0
  667. package/esm/tooling/mcp/tool-input.mjs.map +1 -0
  668. package/esm/tooling/problem.d.mts +45 -0
  669. package/esm/tooling/problem.d.mts.map +1 -0
  670. package/esm/tooling/problem.mjs +28 -0
  671. package/esm/tooling/problem.mjs.map +1 -0
  672. package/esm/tooling/trace/trace-report.types.d.mts +71 -0
  673. package/esm/tooling/trace/trace-report.types.d.mts.map +1 -0
  674. package/esm/tooling/trace/trace-report.types.mjs +7 -0
  675. package/esm/tooling/trace/trace-report.types.mjs.map +1 -0
  676. package/esm/tooling/trace/trace-request.d.mts +83 -0
  677. package/esm/tooling/trace/trace-request.d.mts.map +1 -0
  678. package/esm/tooling/trace/trace-request.mjs +241 -0
  679. package/esm/tooling/trace/trace-request.mjs.map +1 -0
  680. package/esm/tooling/typecheck/index.d.mts +1 -0
  681. package/esm/tooling/typecheck/index.mjs +3 -0
  682. package/esm/tooling/typecheck/run-typecheck.d.mts +59 -0
  683. package/esm/tooling/typecheck/run-typecheck.d.mts.map +1 -0
  684. package/esm/tooling/typecheck/run-typecheck.mjs +153 -0
  685. package/esm/tooling/typecheck/run-typecheck.mjs.map +1 -0
  686. package/esm/upgrade/codemods/head-viewport.mjs +46 -0
  687. package/esm/upgrade/codemods/head-viewport.mjs.map +1 -0
  688. package/esm/upgrade/codemods/helpers.mjs +81 -0
  689. package/esm/upgrade/codemods/helpers.mjs.map +1 -0
  690. package/esm/upgrade/codemods/index.mjs +34 -0
  691. package/esm/upgrade/codemods/index.mjs.map +1 -0
  692. package/esm/upgrade/codemods/logger-sync-calls.mjs +57 -0
  693. package/esm/upgrade/codemods/logger-sync-calls.mjs.map +1 -0
  694. package/esm/upgrade/codemods/package-json.mjs +122 -0
  695. package/esm/upgrade/codemods/package-json.mjs.map +1 -0
  696. package/esm/upgrade/codemods/request-log-snapshot.mjs +82 -0
  697. package/esm/upgrade/codemods/request-log-snapshot.mjs.map +1 -0
  698. package/esm/upgrade/codemods/review-sites.mjs +262 -0
  699. package/esm/upgrade/codemods/review-sites.mjs.map +1 -0
  700. package/esm/upgrade/diff.mjs +119 -0
  701. package/esm/upgrade/diff.mjs.map +1 -0
  702. package/esm/upgrade/format-upgrade-report.mjs +32 -0
  703. package/esm/upgrade/format-upgrade-report.mjs.map +1 -0
  704. package/esm/upgrade/load-ts-morph.mjs +48 -0
  705. package/esm/upgrade/load-ts-morph.mjs.map +1 -0
  706. package/esm/upgrade/notes.mjs +16 -0
  707. package/esm/upgrade/notes.mjs.map +1 -0
  708. package/esm/upgrade/run-upgrade.mjs +246 -0
  709. package/esm/upgrade/run-upgrade.mjs.map +1 -0
  710. package/esm/upgrade/upgrade-command.action.mjs +37 -0
  711. package/esm/upgrade/upgrade-command.action.mjs.map +1 -0
  712. package/esm/upgrade/version.mjs +39 -0
  713. package/esm/upgrade/version.mjs.map +1 -0
  714. package/esm/use-cases/use-cases-registry.mjs.map +1 -1
  715. package/esm/utils/clock.d.mts +33 -0
  716. package/esm/utils/clock.d.mts.map +1 -0
  717. package/esm/utils/clock.mjs +27 -0
  718. package/esm/utils/clock.mjs.map +1 -0
  719. package/esm/utils/framework-vesion.mjs +1 -1
  720. package/esm/utils/framework-vesion.mjs.map +1 -1
  721. package/esm/utils/ids.d.mts +32 -0
  722. package/esm/utils/ids.d.mts.map +1 -0
  723. package/esm/utils/ids.mjs +33 -0
  724. package/esm/utils/ids.mjs.map +1 -0
  725. package/esm/utils/index.d.mts +3 -0
  726. package/esm/utils/index.mjs +3 -0
  727. package/esm/utils/runtime.d.mts +59 -0
  728. package/esm/utils/runtime.d.mts.map +1 -0
  729. package/esm/utils/runtime.mjs +85 -0
  730. package/esm/utils/runtime.mjs.map +1 -0
  731. package/esm/validation/index.d.mts +1 -1
  732. package/esm/validation/types.d.mts +57 -2
  733. package/esm/validation/types.d.mts.map +1 -1
  734. package/esm/validation/validateAll.mjs +19 -2
  735. package/esm/validation/validateAll.mjs.map +1 -1
  736. package/esm/validation/validation-failure-status.mjs +15 -0
  737. package/esm/validation/validation-failure-status.mjs.map +1 -0
  738. package/esm/validation/validators/file-validator.d.mts +0 -16
  739. package/esm/validation/validators/file-validator.d.mts.map +1 -1
  740. package/esm/warlock-config/bundle-config.mjs +1 -1
  741. package/esm/warlock-config/types.d.mts +27 -3
  742. package/esm/warlock-config/types.d.mts.map +1 -1
  743. package/llms-full.txt +3166 -736
  744. package/llms.txt +16 -6
  745. package/package.json +43 -13
  746. package/schemas/inspect-report.schema.json +554 -0
  747. package/skills/README.md +9 -0
  748. package/skills/add-connector/SKILL.md +26 -38
  749. package/skills/benchmark-code/SKILL.md +30 -24
  750. package/skills/build-restful/SKILL.md +14 -14
  751. package/skills/build-url/SKILL.md +18 -18
  752. package/skills/configure-app/SKILL.md +107 -14
  753. package/skills/create-controller/SKILL.md +25 -12
  754. package/skills/create-module/SKILL.md +3 -3
  755. package/skills/define-resource/SKILL.md +32 -29
  756. package/skills/encrypt-data/SKILL.md +21 -19
  757. package/skills/generate-openapi/SKILL.md +50 -24
  758. package/skills/hash-password/SKILL.md +2 -2
  759. package/skills/health-checks/SKILL.md +24 -7
  760. package/skills/index.md +9 -4
  761. package/skills/lower-stage3-decorators/SKILL.md +4 -4
  762. package/skills/make-endpoint/SKILL.md +110 -0
  763. package/skills/model-factories/SKILL.md +92 -0
  764. package/skills/process-image/SKILL.md +32 -30
  765. package/skills/register-route/SKILL.md +109 -10
  766. package/skills/request-memo/SKILL.md +1 -1
  767. package/skills/request-tracing/SKILL.md +4 -1
  768. package/skills/resolve-path/SKILL.md +45 -44
  769. package/skills/retry-operation/SKILL.md +20 -11
  770. package/skills/run-app/SKILL.md +106 -22
  771. package/skills/send-response/SKILL.md +179 -14
  772. package/skills/store-file/SKILL.md +93 -53
  773. package/skills/test-fakes/SKILL.md +123 -0
  774. package/skills/test-http/SKILL.md +125 -21
  775. package/skills/test-service/SKILL.md +41 -1
  776. package/skills/update-packages/SKILL.md +29 -18
  777. package/skills/upgrade-from-5-29/SKILL.md +413 -0
  778. package/skills/upload-file/SKILL.md +31 -33
  779. package/skills/use-app-context/SKILL.md +38 -31
  780. package/skills/use-middleware/SKILL.md +45 -25
  781. package/skills/use-model-transformers/SKILL.md +37 -28
  782. package/skills/use-repository/SKILL.md +2 -2
  783. package/skills/validate-input/SKILL.md +72 -28
  784. package/skills/warlock-agents-md/SKILL.md +96 -0
  785. package/skills/warlock-board/SKILL.md +123 -0
  786. package/skills/warlock-check/SKILL.md +132 -0
  787. package/skills/warlock-conventions/SKILL.md +115 -0
  788. package/skills/warlock-doctor/SKILL.md +55 -22
  789. package/skills/warlock-inspect/SKILL.md +134 -0
  790. package/skills/warlock-mcp/SKILL.md +120 -0
  791. package/skills/warlock-routes/SKILL.md +18 -11
  792. package/skills/warlock-trace/SKILL.md +81 -0
  793. package/skills/wire-socket/SKILL.md +8 -8
  794. package/skills/write-seeder/SKILL.md +46 -32
  795. package/skills/write-use-case/SKILL.md +40 -27
  796. package/esm/http/uploads/matches-if-none-match.mjs +0 -13
  797. package/esm/http/uploads/matches-if-none-match.mjs.map +0 -1
@@ -26,17 +26,19 @@ function startHttpServer(options) {
26
26
  const trustProxy = resolveTrustProxy(config.get("http.trustProxy", false));
27
27
  const bodyLimit = config.get("http.bodyLimit") ?? void 0;
28
28
  const configuredRewriteUrl = options?.rewriteUrl;
29
- const forceCloseConnections = options?.forceCloseConnections ?? config.get("http.gracefulShutdown.forceCloseConnections", "idle");
29
+ const http2 = options?.http2 === true;
30
+ const requestedForceClose = options?.forceCloseConnections ?? config.get("http.gracefulShutdown.forceCloseConnections", http2 ? false : "idle");
31
+ const forceCloseConnections = http2 && requestedForceClose === "idle" ? false : requestedForceClose;
30
32
  server = Fastify({
31
33
  trustProxy,
32
34
  ...bodyLimit !== void 0 && { bodyLimit },
33
- forceCloseConnections,
34
35
  ...options,
36
+ forceCloseConnections,
35
37
  rewriteUrl(request) {
36
38
  return normalizeRequestUrl(configuredRewriteUrl ? configuredRewriteUrl.call(this, request) : request.url ?? "/");
37
39
  }
38
40
  });
39
- if (forceCloseConnections === "idle") trackConnectionsForDrain(server.server);
41
+ if (http2 || forceCloseConnections === "idle") trackConnectionsForDrain(server.server);
40
42
  return server;
41
43
  }
42
44
  /**
@@ -52,11 +54,12 @@ const drainStates = /* @__PURE__ */ new WeakMap();
52
54
  * retired by {@link reapIdleConnections} after it finishes instead.
53
55
  */
54
56
  function retireAfterResponse(response) {
57
+ if ("stream" in response) return;
55
58
  if (response.headersSent || response.writableEnded) return;
56
59
  response.setHeader("Connection", "close");
57
60
  }
58
61
  function reapIdleConnections(rawServer) {
59
- if (typeof rawServer.closeIdleConnections === "function") rawServer.closeIdleConnections();
62
+ if ("closeIdleConnections" in rawServer && typeof rawServer.closeIdleConnections === "function") rawServer.closeIdleConnections();
60
63
  }
61
64
  /**
62
65
  * Track in-flight responses on a Node HTTP server so a graceful drain can
@@ -74,9 +77,15 @@ function trackConnectionsForDrain(rawServer) {
74
77
  if (!rawServer || typeof rawServer.on !== "function" || drainStates.has(rawServer)) return;
75
78
  const state = {
76
79
  inFlight: /* @__PURE__ */ new Set(),
80
+ sessions: /* @__PURE__ */ new Set(),
77
81
  draining: false
78
82
  };
79
83
  drainStates.set(rawServer, state);
84
+ rawServer.on("session", (session) => {
85
+ state.sessions.add(session);
86
+ session.once("close", () => state.sessions.delete(session));
87
+ if (state.draining) session.close();
88
+ });
80
89
  rawServer.on("request", (_request, response) => {
81
90
  state.inFlight.add(response);
82
91
  if (state.draining) retireAfterResponse(response);
@@ -100,6 +109,7 @@ function beginHttpDrain(rawServer) {
100
109
  if (!state) return false;
101
110
  state.draining = true;
102
111
  for (const response of state.inFlight) retireAfterResponse(response);
112
+ for (const session of state.sessions) session.close();
103
113
  reapIdleConnections(rawServer);
104
114
  return true;
105
115
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server.mjs","names":[],"sources":["../../../../../../../../packages/core/src/http/server.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport Fastify, { type FastifyServerOptions } from \"fastify\";\nimport type { Server as NodeHttpServer, ServerResponse } from \"node:http\";\nimport { normalizeRequestPath } from \"../router/normalize-request-path\";\nimport type { HttpServerOptions } from \"./types\";\n\nexport type FastifyInstance = ReturnType<typeof Fastify>;\n\n// Instantiate Fastify server\nlet server: FastifyInstance | undefined = undefined;\n\n/** Fastify's accepted, non-null `trustProxy` configuration shape. */\ntype TrustProxy = NonNullable<FastifyServerOptions[\"trustProxy\"]>;\ntype TrustProxyPredicate = (address: string, hop: number) => boolean;\n\nfunction isTrustProxyPredicate(value: unknown): value is TrustProxyPredicate {\n return typeof value === \"function\";\n}\n\nfunction isTrustProxyList(value: unknown): value is string[] {\n return (\n Array.isArray(value) &&\n value.length > 0 &&\n value.every((entry) => typeof entry === \"string\" && entry.trim().length > 0)\n );\n}\n\nfunction resolveTrustProxy(value: unknown): TrustProxy {\n if (value === undefined || value === null) return false;\n\n if (typeof value === \"boolean\" || isTrustProxyPredicate(value)) return value;\n\n if (typeof value === \"string\" && value.trim().length > 0) return value;\n\n if (isTrustProxyList(value)) return value;\n\n const received = Array.isArray(value) ? \"array\" : typeof value;\n\n throw new TypeError(\n \"Invalid http.trustProxy configuration: expected a boolean, a non-empty IP/CIDR string, \" +\n `a non-empty string array, or a predicate function; received ${received}.`,\n );\n}\n\nfunction normalizeRequestUrl(url: string): string {\n const queryIndex = url.indexOf(\"?\");\n\n if (queryIndex === -1) return normalizeRequestPath(url);\n\n return normalizeRequestPath(url.slice(0, queryIndex)) + url.slice(queryIndex);\n}\n\nexport function startHttpServer(options?: HttpServerOptions): FastifyInstance {\n // `config.set(key, undefined)` stores null rather than unsetting, so an app\n // that clears a key is treated the same as an app that never configured it.\n const trustProxy = resolveTrustProxy(config.get(\"http.trustProxy\", false));\n\n const bodyLimit = config.get(\"http.bodyLimit\") ?? undefined;\n const configuredRewriteUrl = options?.rewriteUrl;\n const forceCloseConnections =\n options?.forceCloseConnections ??\n config.get(\"http.gracefulShutdown.forceCloseConnections\", \"idle\");\n\n server = Fastify({\n // `X-Forwarded-For` is client-settable and spoofable, and `request.ip` is\n // what @fastify/rate-limit keys its buckets on — so trusting it by default\n // makes rate limiting bypassable on any deployment NOT behind a proxy that\n // strips the header. Apps behind such a proxy opt in explicitly.\n //\n // Validated shapes resolve exactly as Fastify does: `true`, a CIDR/IP list\n // (string, comma-separated string or array), or a predicate.\n trustProxy,\n // No default: an app that configures nothing keeps Fastify's own 1MB limit\n // rather than the historical 200GB, which silently removed the protection\n // Fastify provides. Per-route caps go through `serverOptions.bodyLimit`;\n // the `maxBodySize()` middleware runs AFTER parsing and cannot reject\n // before the bytes are resident.\n ...(bodyLimit !== undefined && { bodyLimit }),\n // Close idle keep-alive connections on shutdown while letting in-flight\n // requests finish — the basis for graceful draining. Override via\n // `http.gracefulShutdown.forceCloseConnections`.\n forceCloseConnections,\n ...options,\n // Fastify calls this before route matching in both production `scan()` and\n // the development wildcard dispatcher. Compose an app-supplied rewrite\n // first, then apply the one framework request-path normalizer so the two\n // modes cannot disagree about a terminal slash.\n rewriteUrl(request) {\n const rewritten = configuredRewriteUrl\n ? configuredRewriteUrl.call(this, request)\n : (request.url ?? \"/\");\n\n return normalizeRequestUrl(rewritten);\n },\n });\n\n // `\"idle\"` promises that idle keep-alive connections close on shutdown. Fastify\n // only reaps the ones idle at the moment close starts; a connection whose\n // request finishes DURING the drain stays open and holds `close()` until\n // the shutdown budget expires (WR-014). Track responses so the drain can\n // retire those connections as they go idle.\n if (forceCloseConnections === \"idle\") {\n trackConnectionsForDrain(server.server as NodeHttpServer);\n }\n\n return server;\n}\n\n/**\n * Expose the server to be publicly accessible\n */\nexport function getHttpServer(): FastifyInstance {\n return server;\n}\n\n/**\n * Minimal shape needed to close a server — lets {@link closeServerWithTimeout}\n * be unit-tested with a fake instead of a real Fastify instance.\n */\nexport type ClosableServer = {\n close: () => Promise<unknown>;\n /** The underlying Node server, when there is one (a Fastify instance's `.server`). */\n server?: unknown;\n};\n\n/**\n * Per-server drain state: the responses currently in flight, and whether a\n * graceful shutdown has begun.\n */\ntype DrainState = {\n inFlight: Set<ServerResponse>;\n draining: boolean;\n};\n\nconst drainStates = new WeakMap<object, DrainState>();\n\n/**\n * Mark a response so its connection closes once it is sent. Only possible\n * before the headers go out; a response that already streams its headers is\n * retired by {@link reapIdleConnections} after it finishes instead.\n */\nfunction retireAfterResponse(response: ServerResponse): void {\n if (response.headersSent || response.writableEnded) return;\n\n response.setHeader(\"Connection\", \"close\");\n}\n\nfunction reapIdleConnections(rawServer: NodeHttpServer): void {\n if (typeof rawServer.closeIdleConnections === \"function\") {\n rawServer.closeIdleConnections();\n }\n}\n\n/**\n * Track in-flight responses on a Node HTTP server so a graceful drain can\n * close keep-alive connections that become idle AFTER `close()` started.\n *\n * Node's `server.close()` (and Fastify's `forceCloseConnections: \"idle\"`)\n * closes only the connections idle at that moment. A request in flight when\n * shutdown begins finishes its response, the client keeps the connection\n * alive, and `close()` waits on it until the shutdown budget runs out. Active\n * responses are never cut short: each one is told to close its connection\n * when it is done (`Connection: close`), and every connection is reaped as\n * soon as its response has finished.\n */\nexport function trackConnectionsForDrain(rawServer: NodeHttpServer): void {\n if (!rawServer || typeof rawServer.on !== \"function\" || drainStates.has(rawServer)) return;\n\n const state: DrainState = { inFlight: new Set(), draining: false };\n\n drainStates.set(rawServer, state);\n\n rawServer.on(\"request\", (_request, response: ServerResponse) => {\n state.inFlight.add(response);\n\n if (state.draining) retireAfterResponse(response);\n\n response.once(\"close\", () => {\n state.inFlight.delete(response);\n\n // The socket becomes idle once Node finishes handling this response,\n // which happens after the response's own `close` event.\n if (state.draining) setImmediate(() => reapIdleConnections(rawServer));\n });\n });\n}\n\n/**\n * Begin a graceful drain on a server tracked by {@link trackConnectionsForDrain}:\n * every in-flight response closes its connection when it is done, and idle\n * keep-alive connections are closed now and whenever another one goes idle.\n * A server that is not tracked is left alone.\n *\n * @returns Whether the server was tracked (and is now draining).\n */\nexport function beginHttpDrain(rawServer: unknown): boolean {\n if (!rawServer || typeof rawServer !== \"object\") return false;\n\n const state = drainStates.get(rawServer);\n\n if (!state) return false;\n\n state.draining = true;\n\n for (const response of state.inFlight) {\n retireAfterResponse(response);\n }\n\n reapIdleConnections(rawServer as NodeHttpServer);\n\n return true;\n}\n\n/**\n * Close a server, bounded by a timeout. Fastify's `close()` stops accepting new\n * requests (it answers 503 while closing) and drains the in-flight ones; this\n * wraps it so a single stuck request can't hang shutdown forever. When the\n * server's connections are tracked (the default `\"idle\"` mode), keep-alive\n * connections that go idle during the drain are closed too, so a finished\n * request does not hold the shutdown open.\n *\n * @returns `true` if the server drained cleanly, `false` if the timeout fired first.\n */\nexport async function closeServerWithTimeout(\n server: ClosableServer,\n timeoutMs: number,\n): Promise<boolean> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n beginHttpDrain(server.server);\n\n const drained = server.close().then(() => true);\n\n const timedOut = new Promise<boolean>((resolve) => {\n timer = setTimeout(() => resolve(false), timeoutMs);\n });\n\n try {\n return await Promise.race([drained, timedOut]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n"],"mappings":";;;;;AASA,IAAI,SAAsC;AAM1C,SAAS,sBAAsB,OAA8C;CAC3E,OAAO,OAAO,UAAU;AAC1B;AAEA,SAAS,iBAAiB,OAAmC;CAC3D,OACE,MAAM,QAAQ,KAAK,KACnB,MAAM,SAAS,KACf,MAAM,OAAO,UAAU,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,SAAS,CAAC;AAE/E;AAEA,SAAS,kBAAkB,OAA4B;CACrD,IAAI,UAAU,UAAa,UAAU,MAAM,OAAO;CAElD,IAAI,OAAO,UAAU,aAAa,sBAAsB,KAAK,GAAG,OAAO;CAEvE,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,SAAS,GAAG,OAAO;CAEjE,IAAI,iBAAiB,KAAK,GAAG,OAAO;CAIpC,MAAM,IAAI,UACR,sJAHe,MAAM,QAAQ,KAAK,IAAI,UAAU,OAAO,MAImB,EAC5E;AACF;AAEA,SAAS,oBAAoB,KAAqB;CAChD,MAAM,aAAa,IAAI,QAAQ,GAAG;CAElC,IAAI,eAAe,IAAI,OAAO,qBAAqB,GAAG;CAEtD,OAAO,qBAAqB,IAAI,MAAM,GAAG,UAAU,CAAC,IAAI,IAAI,MAAM,UAAU;AAC9E;AAEA,SAAgB,gBAAgB,SAA8C;CAG5E,MAAM,aAAa,kBAAkB,OAAO,IAAI,mBAAmB,KAAK,CAAC;CAEzE,MAAM,YAAY,OAAO,IAAI,gBAAgB,KAAK;CAClD,MAAM,uBAAuB,SAAS;CACtC,MAAM,wBACJ,SAAS,yBACT,OAAO,IAAI,+CAA+C,MAAM;CAElE,SAAS,QAAQ;EAQf;EAMA,GAAI,cAAc,UAAa,EAAE,UAAU;EAI3C;EACA,GAAG;EAKH,WAAW,SAAS;GAKlB,OAAO,oBAJW,uBACd,qBAAqB,KAAK,MAAM,OAAO,IACtC,QAAQ,OAAO,GAEgB;EACtC;CACF,CAAC;CAOD,IAAI,0BAA0B,QAC5B,yBAAyB,OAAO,MAAwB;CAG1D,OAAO;AACT;;;;AAKA,SAAgB,gBAAiC;CAC/C,OAAO;AACT;AAqBA,MAAM,8BAAc,IAAI,QAA4B;;;;;;AAOpD,SAAS,oBAAoB,UAAgC;CAC3D,IAAI,SAAS,eAAe,SAAS,eAAe;CAEpD,SAAS,UAAU,cAAc,OAAO;AAC1C;AAEA,SAAS,oBAAoB,WAAiC;CAC5D,IAAI,OAAO,UAAU,yBAAyB,YAC5C,UAAU,qBAAqB;AAEnC;;;;;;;;;;;;;AAcA,SAAgB,yBAAyB,WAAiC;CACxE,IAAI,CAAC,aAAa,OAAO,UAAU,OAAO,cAAc,YAAY,IAAI,SAAS,GAAG;CAEpF,MAAM,QAAoB;EAAE,0BAAU,IAAI,IAAI;EAAG,UAAU;CAAM;CAEjE,YAAY,IAAI,WAAW,KAAK;CAEhC,UAAU,GAAG,YAAY,UAAU,aAA6B;EAC9D,MAAM,SAAS,IAAI,QAAQ;EAE3B,IAAI,MAAM,UAAU,oBAAoB,QAAQ;EAEhD,SAAS,KAAK,eAAe;GAC3B,MAAM,SAAS,OAAO,QAAQ;GAI9B,IAAI,MAAM,UAAU,mBAAmB,oBAAoB,SAAS,CAAC;EACvE,CAAC;CACH,CAAC;AACH;;;;;;;;;AAUA,SAAgB,eAAe,WAA6B;CAC1D,IAAI,CAAC,aAAa,OAAO,cAAc,UAAU,OAAO;CAExD,MAAM,QAAQ,YAAY,IAAI,SAAS;CAEvC,IAAI,CAAC,OAAO,OAAO;CAEnB,MAAM,WAAW;CAEjB,KAAK,MAAM,YAAY,MAAM,UAC3B,oBAAoB,QAAQ;CAG9B,oBAAoB,SAA2B;CAE/C,OAAO;AACT;;;;;;;;;;;AAYA,eAAsB,uBACpB,QACA,WACkB;CAClB,IAAI;CAEJ,eAAe,OAAO,MAAM;CAE5B,MAAM,UAAU,OAAO,MAAM,CAAC,CAAC,WAAW,IAAI;CAE9C,MAAM,WAAW,IAAI,SAAkB,YAAY;EACjD,QAAQ,iBAAiB,QAAQ,KAAK,GAAG,SAAS;CACpD,CAAC;CAED,IAAI;EACF,OAAO,MAAM,QAAQ,KAAK,CAAC,SAAS,QAAQ,CAAC;CAC/C,UAAU;EACR,IAAI,OACF,aAAa,KAAK;CAEtB;AACF"}
1
+ {"version":3,"file":"server.mjs","names":["createServer"],"sources":["../../../../../../../../packages/core/src/http/server.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport Fastify, { type FastifyServerOptions } from \"fastify\";\nimport type { Server as NodeHttpServer, ServerResponse } from \"node:http\";\nimport type {\n Http2Server,\n Http2SecureServer,\n Http2ServerResponse,\n ServerHttp2Session,\n} from \"node:http2\";\n\ntype DrainServer = NodeHttpServer | Http2Server | Http2SecureServer;\ntype DrainResponse = ServerResponse | Http2ServerResponse;\nimport { normalizeRequestPath } from \"../router/normalize-request-path\";\nimport type { HttpServerOptions } from \"./types\";\n\nexport type FastifyInstance = ReturnType<typeof Fastify>;\n\n// Instantiate Fastify server\nlet server: FastifyInstance | undefined = undefined;\n\n/** Fastify's accepted, non-null `trustProxy` configuration shape. */\ntype TrustProxy = NonNullable<FastifyServerOptions[\"trustProxy\"]>;\ntype TrustProxyPredicate = (address: string, hop: number) => boolean;\n\nfunction isTrustProxyPredicate(value: unknown): value is TrustProxyPredicate {\n return typeof value === \"function\";\n}\n\nfunction isTrustProxyList(value: unknown): value is string[] {\n return (\n Array.isArray(value) &&\n value.length > 0 &&\n value.every((entry) => typeof entry === \"string\" && entry.trim().length > 0)\n );\n}\n\nfunction resolveTrustProxy(value: unknown): TrustProxy {\n if (value === undefined || value === null) return false;\n\n if (typeof value === \"boolean\" || isTrustProxyPredicate(value)) return value;\n\n if (typeof value === \"string\" && value.trim().length > 0) return value;\n\n if (isTrustProxyList(value)) return value;\n\n const received = Array.isArray(value) ? \"array\" : typeof value;\n\n throw new TypeError(\n \"Invalid http.trustProxy configuration: expected a boolean, a non-empty IP/CIDR string, \" +\n `a non-empty string array, or a predicate function; received ${received}.`,\n );\n}\n\nfunction normalizeRequestUrl(url: string): string {\n const queryIndex = url.indexOf(\"?\");\n\n if (queryIndex === -1) return normalizeRequestPath(url);\n\n return normalizeRequestPath(url.slice(0, queryIndex)) + url.slice(queryIndex);\n}\n\nexport function startHttpServer(options?: HttpServerOptions): FastifyInstance {\n // `config.set(key, undefined)` stores null rather than unsetting, so an app\n // that clears a key is treated the same as an app that never configured it.\n const trustProxy = resolveTrustProxy(config.get(\"http.trustProxy\", false));\n\n const bodyLimit = config.get(\"http.bodyLimit\") ?? undefined;\n const configuredRewriteUrl = options?.rewriteUrl;\n const http2 = options?.http2 === true;\n const requestedForceClose =\n options?.forceCloseConnections ??\n config.get(\"http.gracefulShutdown.forceCloseConnections\", http2 ? false : \"idle\");\n // HTTP/2 has sessions rather than closeIdleConnections(); session tracking\n // below supplies graceful draining even when Fastify cannot use idle mode.\n const forceCloseConnections =\n http2 && requestedForceClose === \"idle\" ? false : requestedForceClose;\n\n // Keep the framework's established HTTP-compatible reply facade while\n // accepting Fastify's HTTP/1 and HTTP/2 option overloads at this boundary.\n const createServer = Fastify as (options: HttpServerOptions) => FastifyInstance;\n server = createServer({\n // `X-Forwarded-For` is client-settable and spoofable, and `request.ip` is\n // what @fastify/rate-limit keys its buckets on — so trusting it by default\n // makes rate limiting bypassable on any deployment NOT behind a proxy that\n // strips the header. Apps behind such a proxy opt in explicitly.\n //\n // Validated shapes resolve exactly as Fastify does: `true`, a CIDR/IP list\n // (string, comma-separated string or array), or a predicate.\n trustProxy,\n // No default: an app that configures nothing keeps Fastify's own 1MB limit\n // rather than the historical 200GB, which silently removed the protection\n // Fastify provides. Per-route caps go through `serverOptions.bodyLimit`;\n // the `maxBodySize()` middleware runs AFTER parsing and cannot reject\n // before the bytes are resident.\n ...(bodyLimit !== undefined && { bodyLimit }),\n // Close idle keep-alive connections on shutdown while letting in-flight\n // requests finish — the basis for graceful draining. Override via\n // `http.gracefulShutdown.forceCloseConnections`.\n ...options,\n forceCloseConnections,\n // Fastify calls this before route matching in both production `scan()` and\n // the development wildcard dispatcher. Compose an app-supplied rewrite\n // first, then apply the one framework request-path normalizer so the two\n // modes cannot disagree about a terminal slash.\n rewriteUrl(request) {\n const rewritten = configuredRewriteUrl\n ? configuredRewriteUrl.call(this, request)\n : (request.url ?? \"/\");\n\n return normalizeRequestUrl(rewritten);\n },\n });\n\n // `\"idle\"` promises that idle keep-alive connections close on shutdown. Fastify\n // only reaps the ones idle at the moment close starts; a connection whose\n // request finishes DURING the drain stays open and holds `close()` until\n // the shutdown budget expires. Track responses so the drain can\n // retire those connections as they go idle.\n if (http2 || forceCloseConnections === \"idle\") {\n trackConnectionsForDrain(server.server as DrainServer);\n }\n\n return server;\n}\n\n/**\n * Expose the server to be publicly accessible\n */\nexport function getHttpServer(): FastifyInstance {\n return server;\n}\n\n/**\n * Minimal shape needed to close a server — lets {@link closeServerWithTimeout}\n * be unit-tested with a fake instead of a real Fastify instance.\n */\nexport type ClosableServer = {\n close: () => Promise<unknown>;\n /** The underlying Node server, when there is one (a Fastify instance's `.server`). */\n server?: unknown;\n};\n\n/**\n * Per-server drain state: the responses currently in flight, and whether a\n * graceful shutdown has begun.\n */\ntype DrainState = {\n inFlight: Set<DrainResponse>;\n sessions: Set<ServerHttp2Session>;\n draining: boolean;\n};\n\nconst drainStates = new WeakMap<object, DrainState>();\n\n/**\n * Mark a response so its connection closes once it is sent. Only possible\n * before the headers go out; a response that already streams its headers is\n * retired by {@link reapIdleConnections} after it finishes instead.\n */\nfunction retireAfterResponse(response: DrainResponse): void {\n // HTTP/2 drains the session with GOAWAY; connection headers are forbidden.\n if (\"stream\" in response) return;\n if (response.headersSent || response.writableEnded) return;\n\n response.setHeader(\"Connection\", \"close\");\n}\n\nfunction reapIdleConnections(rawServer: DrainServer): void {\n if (\"closeIdleConnections\" in rawServer && typeof rawServer.closeIdleConnections === \"function\") {\n rawServer.closeIdleConnections();\n }\n}\n\n/**\n * Track in-flight responses on a Node HTTP server so a graceful drain can\n * close keep-alive connections that become idle AFTER `close()` started.\n *\n * Node's `server.close()` (and Fastify's `forceCloseConnections: \"idle\"`)\n * closes only the connections idle at that moment. A request in flight when\n * shutdown begins finishes its response, the client keeps the connection\n * alive, and `close()` waits on it until the shutdown budget runs out. Active\n * responses are never cut short: each one is told to close its connection\n * when it is done (`Connection: close`), and every connection is reaped as\n * soon as its response has finished.\n */\nexport function trackConnectionsForDrain(rawServer: DrainServer): void {\n if (!rawServer || typeof rawServer.on !== \"function\" || drainStates.has(rawServer)) return;\n\n const state: DrainState = { inFlight: new Set(), sessions: new Set(), draining: false };\n\n drainStates.set(rawServer, state);\n\n rawServer.on(\"session\", (session: ServerHttp2Session) => {\n state.sessions.add(session);\n session.once(\"close\", () => state.sessions.delete(session));\n if (state.draining) session.close();\n });\n\n rawServer.on(\"request\", (_request: unknown, response: DrainResponse) => {\n state.inFlight.add(response);\n\n if (state.draining) retireAfterResponse(response);\n\n response.once(\"close\", () => {\n state.inFlight.delete(response);\n\n // The socket becomes idle once Node finishes handling this response,\n // which happens after the response's own `close` event.\n if (state.draining) setImmediate(() => reapIdleConnections(rawServer));\n });\n });\n}\n\n/**\n * Begin a graceful drain on a server tracked by {@link trackConnectionsForDrain}:\n * every in-flight response closes its connection when it is done, and idle\n * keep-alive connections are closed now and whenever another one goes idle.\n * A server that is not tracked is left alone.\n *\n * @returns Whether the server was tracked (and is now draining).\n */\nexport function beginHttpDrain(rawServer: unknown): boolean {\n if (!rawServer || typeof rawServer !== \"object\") return false;\n\n const state = drainStates.get(rawServer);\n\n if (!state) return false;\n\n state.draining = true;\n\n for (const response of state.inFlight) {\n retireAfterResponse(response);\n }\n\n // close() sends GOAWAY and waits for active streams before closing the socket.\n for (const session of state.sessions) session.close();\n\n reapIdleConnections(rawServer as DrainServer);\n\n return true;\n}\n\n/**\n * Close a server, bounded by a timeout. Fastify's `close()` stops accepting new\n * requests (it answers 503 while closing) and drains the in-flight ones; this\n * wraps it so a single stuck request can't hang shutdown forever. When the\n * server's connections are tracked (the default `\"idle\"` mode), keep-alive\n * connections that go idle during the drain are closed too, so a finished\n * request does not hold the shutdown open.\n *\n * @returns `true` if the server drained cleanly, `false` if the timeout fired first.\n */\nexport async function closeServerWithTimeout(\n server: ClosableServer,\n timeoutMs: number,\n): Promise<boolean> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n beginHttpDrain(server.server);\n\n const drained = server.close().then(() => true);\n\n const timedOut = new Promise<boolean>((resolve) => {\n timer = setTimeout(() => resolve(false), timeoutMs);\n });\n\n try {\n return await Promise.race([drained, timedOut]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ function createStreamTransport(raw, options) {
6
6
  let disconnected = false;
7
7
  const disconnectHandlers = [];
8
8
  const disconnectController = new AbortController();
9
- raw.on("close", () => {
9
+ const disconnect = () => {
10
10
  if (closed) return;
11
11
  closed = true;
12
12
  disconnected = true;
@@ -17,7 +17,8 @@ function createStreamTransport(raw, options) {
17
17
  } catch (error) {
18
18
  log.error("http", "stream", error, { source: "onDisconnect handler" });
19
19
  }
20
- });
20
+ };
21
+ raw.on("close", disconnect);
21
22
  return {
22
23
  signal: disconnectController.signal,
23
24
  get closed() {
@@ -32,6 +33,7 @@ function createStreamTransport(raw, options) {
32
33
  finish() {
33
34
  closed = true;
34
35
  },
36
+ disconnect,
35
37
  onDisconnect(handler) {
36
38
  disconnectHandlers.push(handler);
37
39
  },
@@ -1 +1 @@
1
- {"version":3,"file":"stream-transport.mjs","names":[],"sources":["../../../../../../../../packages/core/src/http/stream-transport.ts"],"sourcesContent":["import type { ServerResponse } from \"node:http\";\nimport { log } from \"@warlock.js/logger\";\n\n/**\n * Shared transport bookkeeping for the long-lived responses\n * (`response.sse()` and `response.stream()`): client-disconnect detection,\n * an `AbortSignal` for the work feeding the stream, and backpressure (WR-006).\n */\nexport type StreamTransport = {\n /** Aborted when the client disconnects (not when the stream is ended). */\n readonly signal: AbortSignal;\n /** `true` once the stream was ended or the client disconnected. */\n readonly closed: boolean;\n /** `true` once the client disconnected before the stream was ended. */\n readonly disconnected: boolean;\n /** Whether the transport holds more than its high-water mark. */\n readonly backpressured: boolean;\n /** Mark the stream as ended by the server. */\n finish(): void;\n /** Call `handler` when the client disconnects. */\n onDisconnect(handler: () => void): void;\n /**\n * Resolve once the transport has flushed what it buffered: `true` when the\n * client can take more, `false` when the stream ended or the client left.\n */\n waitForDrain(): Promise<boolean>;\n};\n\nexport function createStreamTransport(\n raw: ServerResponse,\n options: { disconnectMessage: string; onClientDisconnected?: () => void },\n): StreamTransport {\n let closed = false;\n let disconnected = false;\n const disconnectHandlers: Array<() => void> = [];\n const disconnectController = new AbortController();\n\n // Without this, background jobs keep writing to a dead socket after the\n // client drops. `close` also fires after a normal `end()`; `closed` is\n // already set by then.\n raw.on(\"close\", () => {\n if (closed) return;\n\n closed = true;\n disconnected = true;\n options.onClientDisconnected?.();\n disconnectController.abort(new DOMException(options.disconnectMessage, \"AbortError\"));\n\n // Each handler is isolated: this runs inside the socket's `close`\n // listener, so a throwing handler would otherwise become an uncaught\n // exception (taking the process down) and skip the handlers after it.\n for (const handler of disconnectHandlers) {\n try {\n handler();\n } catch (error) {\n log.error(\"http\", \"stream\", error as Error, { source: \"onDisconnect handler\" });\n }\n }\n });\n\n return {\n signal: disconnectController.signal,\n\n get closed() {\n return closed;\n },\n\n get disconnected() {\n return disconnected;\n },\n\n get backpressured() {\n return !closed && raw.writableNeedDrain;\n },\n\n finish() {\n closed = true;\n },\n\n onDisconnect(handler) {\n disconnectHandlers.push(handler);\n },\n\n waitForDrain() {\n if (closed) return Promise.resolve(false);\n\n if (!raw.writableNeedDrain) return Promise.resolve(true);\n\n return new Promise<boolean>((resolve) => {\n const settle = (drained: boolean) => {\n raw.off(\"drain\", onDrain);\n raw.off(\"close\", onClose);\n resolve(drained);\n };\n const onDrain = () => settle(!closed);\n const onClose = () => settle(false);\n\n raw.once(\"drain\", onDrain);\n raw.once(\"close\", onClose);\n });\n },\n };\n}\n\n/**\n * The most recent `limit` items, for logging/debugging a stream after it\n * ended. `limit` 0 (or missing/NaN) keeps nothing, so a long-lived stream\n * does not grow with its own history.\n */\nexport function createRetainedTail<T>(limit: number | undefined): {\n push(item: T): void;\n items: T[];\n} {\n const max = limit === undefined || Number.isNaN(limit) ? 0 : Math.max(0, Math.floor(limit));\n const items: T[] = [];\n\n return {\n items,\n push(item) {\n if (max === 0) return;\n\n items.push(item);\n\n if (items.length > max) items.shift();\n },\n };\n}\n"],"mappings":";;;AA4BA,SAAgB,sBACd,KACA,SACiB;CACjB,IAAI,SAAS;CACb,IAAI,eAAe;CACnB,MAAM,qBAAwC,CAAC;CAC/C,MAAM,uBAAuB,IAAI,gBAAgB;CAKjD,IAAI,GAAG,eAAe;EACpB,IAAI,QAAQ;EAEZ,SAAS;EACT,eAAe;EACf,QAAQ,uBAAuB;EAC/B,qBAAqB,MAAM,IAAI,aAAa,QAAQ,mBAAmB,YAAY,CAAC;EAKpF,KAAK,MAAM,WAAW,oBACpB,IAAI;GACF,QAAQ;EACV,SAAS,OAAO;GACd,IAAI,MAAM,QAAQ,UAAU,OAAgB,EAAE,QAAQ,uBAAuB,CAAC;EAChF;CAEJ,CAAC;CAED,OAAO;EACL,QAAQ,qBAAqB;EAE7B,IAAI,SAAS;GACX,OAAO;EACT;EAEA,IAAI,eAAe;GACjB,OAAO;EACT;EAEA,IAAI,gBAAgB;GAClB,OAAO,CAAC,UAAU,IAAI;EACxB;EAEA,SAAS;GACP,SAAS;EACX;EAEA,aAAa,SAAS;GACpB,mBAAmB,KAAK,OAAO;EACjC;EAEA,eAAe;GACb,IAAI,QAAQ,OAAO,QAAQ,QAAQ,KAAK;GAExC,IAAI,CAAC,IAAI,mBAAmB,OAAO,QAAQ,QAAQ,IAAI;GAEvD,OAAO,IAAI,SAAkB,YAAY;IACvC,MAAM,UAAU,YAAqB;KACnC,IAAI,IAAI,SAAS,OAAO;KACxB,IAAI,IAAI,SAAS,OAAO;KACxB,QAAQ,OAAO;IACjB;IACA,MAAM,gBAAgB,OAAO,CAAC,MAAM;IACpC,MAAM,gBAAgB,OAAO,KAAK;IAElC,IAAI,KAAK,SAAS,OAAO;IACzB,IAAI,KAAK,SAAS,OAAO;GAC3B,CAAC;EACH;CACF;AACF;;;;;;AAOA,SAAgB,mBAAsB,OAGpC;CACA,MAAM,MAAM,UAAU,UAAa,OAAO,MAAM,KAAK,IAAI,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC;CAC1F,MAAM,QAAa,CAAC;CAEpB,OAAO;EACL;EACA,KAAK,MAAM;GACT,IAAI,QAAQ,GAAG;GAEf,MAAM,KAAK,IAAI;GAEf,IAAI,MAAM,SAAS,KAAK,MAAM,MAAM;EACtC;CACF;AACF"}
1
+ {"version":3,"file":"stream-transport.mjs","names":[],"sources":["../../../../../../../../packages/core/src/http/stream-transport.ts"],"sourcesContent":["import type { ServerResponse } from \"node:http\";\nimport { log } from \"@warlock.js/logger\";\n\n/**\n * Shared transport bookkeeping for the long-lived responses\n * (`response.sse()` and `response.stream()`): client-disconnect detection,\n * an `AbortSignal` for the work feeding the stream, and backpressure.\n */\nexport type StreamTransport = {\n /** Aborted when the client disconnects (not when the stream is ended). */\n readonly signal: AbortSignal;\n /** `true` once the stream was ended or the client disconnected. */\n readonly closed: boolean;\n /** `true` once the client disconnected before the stream was ended. */\n readonly disconnected: boolean;\n /** Whether the transport holds more than its high-water mark. */\n readonly backpressured: boolean;\n /** Mark the stream as ended by the server. */\n finish(): void;\n /** Abort the producer and notify disconnect handlers when a slow client is disconnected. */\n disconnect(): void;\n /** Call `handler` when the client disconnects. */\n onDisconnect(handler: () => void): void;\n /**\n * Resolve once the transport has flushed what it buffered: `true` when the\n * client can take more, `false` when the stream ended or the client left.\n */\n waitForDrain(): Promise<boolean>;\n};\n\nexport function createStreamTransport(\n raw: ServerResponse,\n options: { disconnectMessage: string; onClientDisconnected?: () => void },\n): StreamTransport {\n let closed = false;\n let disconnected = false;\n const disconnectHandlers: Array<() => void> = [];\n const disconnectController = new AbortController();\n\n // Without this, background jobs keep writing to a dead socket after the\n // client drops. `close` also fires after a normal `end()`; `closed` is\n // already set by then.\n const disconnect = () => {\n if (closed) return;\n\n closed = true;\n disconnected = true;\n options.onClientDisconnected?.();\n disconnectController.abort(new DOMException(options.disconnectMessage, \"AbortError\"));\n\n // Each handler is isolated: this runs inside the socket's `close`\n // listener, so a throwing handler would otherwise become an uncaught\n // exception (taking the process down) and skip the handlers after it.\n for (const handler of disconnectHandlers) {\n try {\n handler();\n } catch (error) {\n log.error(\"http\", \"stream\", error as Error, { source: \"onDisconnect handler\" });\n }\n }\n };\n raw.on(\"close\", disconnect);\n\n return {\n signal: disconnectController.signal,\n\n get closed() {\n return closed;\n },\n\n get disconnected() {\n return disconnected;\n },\n\n get backpressured() {\n return !closed && raw.writableNeedDrain;\n },\n\n finish() {\n closed = true;\n },\n\n disconnect,\n\n onDisconnect(handler) {\n disconnectHandlers.push(handler);\n },\n\n waitForDrain() {\n if (closed) return Promise.resolve(false);\n\n if (!raw.writableNeedDrain) return Promise.resolve(true);\n\n return new Promise<boolean>((resolve) => {\n const settle = (drained: boolean) => {\n raw.off(\"drain\", onDrain);\n raw.off(\"close\", onClose);\n resolve(drained);\n };\n const onDrain = () => settle(!closed);\n const onClose = () => settle(false);\n\n raw.once(\"drain\", onDrain);\n raw.once(\"close\", onClose);\n });\n },\n };\n}\n\n/**\n * The most recent `limit` items, for logging/debugging a stream after it\n * ended. `limit` 0 (or missing/NaN) keeps nothing, so a long-lived stream\n * does not grow with its own history.\n */\nexport function createRetainedTail<T>(limit: number | undefined): {\n push(item: T): void;\n items: T[];\n} {\n const max = limit === undefined || Number.isNaN(limit) ? 0 : Math.max(0, Math.floor(limit));\n const items: T[] = [];\n\n return {\n items,\n push(item) {\n if (max === 0) return;\n\n items.push(item);\n\n if (items.length > max) items.shift();\n },\n };\n}\n"],"mappings":";;;AA8BA,SAAgB,sBACd,KACA,SACiB;CACjB,IAAI,SAAS;CACb,IAAI,eAAe;CACnB,MAAM,qBAAwC,CAAC;CAC/C,MAAM,uBAAuB,IAAI,gBAAgB;CAKjD,MAAM,mBAAmB;EACvB,IAAI,QAAQ;EAEZ,SAAS;EACT,eAAe;EACf,QAAQ,uBAAuB;EAC/B,qBAAqB,MAAM,IAAI,aAAa,QAAQ,mBAAmB,YAAY,CAAC;EAKpF,KAAK,MAAM,WAAW,oBACpB,IAAI;GACF,QAAQ;EACV,SAAS,OAAO;GACd,IAAI,MAAM,QAAQ,UAAU,OAAgB,EAAE,QAAQ,uBAAuB,CAAC;EAChF;CAEJ;CACA,IAAI,GAAG,SAAS,UAAU;CAE1B,OAAO;EACL,QAAQ,qBAAqB;EAE7B,IAAI,SAAS;GACX,OAAO;EACT;EAEA,IAAI,eAAe;GACjB,OAAO;EACT;EAEA,IAAI,gBAAgB;GAClB,OAAO,CAAC,UAAU,IAAI;EACxB;EAEA,SAAS;GACP,SAAS;EACX;EAEA;EAEA,aAAa,SAAS;GACpB,mBAAmB,KAAK,OAAO;EACjC;EAEA,eAAe;GACb,IAAI,QAAQ,OAAO,QAAQ,QAAQ,KAAK;GAExC,IAAI,CAAC,IAAI,mBAAmB,OAAO,QAAQ,QAAQ,IAAI;GAEvD,OAAO,IAAI,SAAkB,YAAY;IACvC,MAAM,UAAU,YAAqB;KACnC,IAAI,IAAI,SAAS,OAAO;KACxB,IAAI,IAAI,SAAS,OAAO;KACxB,QAAQ,OAAO;IACjB;IACA,MAAM,gBAAgB,OAAO,CAAC,MAAM;IACpC,MAAM,gBAAgB,OAAO,KAAK;IAElC,IAAI,KAAK,SAAS,OAAO;IACzB,IAAI,KAAK,SAAS,OAAO;GAC3B,CAAC;EACH;CACF;AACF;;;;;;AAOA,SAAgB,mBAAsB,OAGpC;CACA,MAAM,MAAM,UAAU,UAAa,OAAO,MAAM,KAAK,IAAI,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC;CAC1F,MAAM,QAAa,CAAC;CAEpB,OAAO;EACL;EACA,KAAK,MAAM;GACT,IAAI,QAAQ,GAAG;GAEf,MAAM,KAAK,IAAI;GAEf,IAAI,MAAM,SAAS,KAAK,MAAM,MAAM;EACtC;CACF;AACF"}
@@ -1,4 +1,5 @@
1
1
  import { HttpRateLimitConfig } from "./build-rate-limit-options.mjs";
2
+ import { StreamQueueOptions } from "./bounded-stream-queue.mjs";
2
3
  import { HttpLoggingConfig } from "./request-log-snapshot.mjs";
3
4
  import { Response } from "./response.mjs";
4
5
  import { CspConfig } from "./csp.mjs";
@@ -9,6 +10,7 @@ import { FastifyHttpOptions } from "fastify";
9
10
  import { CookieSerializeOptions } from "@fastify/cookie";
10
11
  import { FastifyCorsOptions } from "@fastify/cors";
11
12
  import { Server } from "node:http";
13
+ import { SecureServerOptions } from "node:http2";
12
14
  import React from "react";
13
15
 
14
16
  //#region ../../packages/core/src/http/types.d.ts
@@ -188,7 +190,11 @@ interface PartialMiddleware {
188
190
  * `gracefulShutdown.forceCloseConnections`) have their own `http.*` keys;
189
191
  * setting them here as well overrides those keys.
190
192
  */
191
- type HttpServerOptions = FastifyHttpOptions<Server>;
193
+ type HttpServerOptions = Omit<FastifyHttpOptions<Server>, "http2"> & {
194
+ /** Enable HTTP/2 while retaining the framework's HTTP-compatible reply API. */http2?: boolean;
195
+ https?: SecureServerOptions;
196
+ http2SessionTimeout?: number;
197
+ };
192
198
  /**
193
199
  * Http Configurations list
194
200
  */
@@ -228,6 +234,14 @@ interface HttpConfigurations {
228
234
  * handling. See {@link HttpErrorMapper}.
229
235
  */
230
236
  errorMapper?: HttpErrorMapper;
237
+ /**
238
+ * In `warlock dev`, warn once per route about POST/PUT/PATCH/DELETE routes
239
+ * that have no auth middleware and no `public: true`. Development only;
240
+ * production never checks. Set `false` to silence the warning entirely.
241
+ *
242
+ * @default true
243
+ */
244
+ warnUnguardedRoutes?: boolean;
231
245
  /**
232
246
  * Server port
233
247
  */
@@ -453,6 +467,13 @@ interface HttpConfigurations {
453
467
  * @default "/ready"
454
468
  */
455
469
  readinessPath?: string;
470
+ /**
471
+ * Include built-in liveness/readiness probes in the global rate-limit policy.
472
+ * Leave false so client traffic cannot exhaust infrastructure probe quotas.
473
+ *
474
+ * @default false
475
+ */
476
+ rateLimit?: boolean;
456
477
  };
457
478
  /**
458
479
  * Host
@@ -485,7 +506,8 @@ interface HttpConfigurations {
485
506
  /**
486
507
  * Options for `response.stream()`.
487
508
  */
488
- type ResponseStreamOptions = {
509
+ type ResponseStreamOptions = StreamQueueOptions & {
510
+ /** Application/cache headers override defaults before flushing; framing and connection headers are ignored. */headers?: Record<string, string>;
489
511
  /**
490
512
  * How many of the most recent chunks (from `send()`, `write()` and
491
513
  * `render()`) to keep on `response.body` after `end()`, for logging and
@@ -503,11 +525,13 @@ type ResponseStreamController = {
503
525
  * down to the client's pace instead of buffering. A silent no-op after the
504
526
  * client disconnected; throws after `end()`.
505
527
  */
506
- send: (data: string | Uint8Array) => void;
528
+ send: (data: string | Uint8Array) => void; /** Bounded synchronous write; false when closed or overflow rejects this chunk. */
529
+ enqueue: (data: string | Uint8Array) => boolean;
507
530
  /**
508
531
  * Send data and wait until the transport can take more. Resolves `true`
509
532
  * when it can, `false` when the stream ended or the client disconnected —
510
- * stop producing then.
533
+ * stop producing then. In drop mode, true means the stream is ready to continue;
534
+ * inspect dropped to detect rejected output and schedule a resync.
511
535
  */
512
536
  write: (data: string | Uint8Array) => Promise<boolean>;
513
537
  /**
@@ -536,7 +560,8 @@ type ResponseStreamController = {
536
560
  /**
537
561
  * Whether the transport is currently holding more than its high-water mark.
538
562
  */
539
- backpressured: boolean;
563
+ backpressured: boolean; /** Chunks/events/comments rejected by onOverflow: "drop", including send/write/render paths. */
564
+ readonly dropped: number;
540
565
  /**
541
566
  * Detect whether stream is ended (by `end()` or a client disconnect)
542
567
  */
@@ -545,7 +570,8 @@ type ResponseStreamController = {
545
570
  /**
546
571
  * Options for `response.sse()`.
547
572
  */
548
- type ResponseSSEOptions = {
573
+ type ResponseSSEOptions = StreamQueueOptions & {
574
+ /** Application/cache headers override defaults before flushing; framing and connection headers are ignored. */headers?: Record<string, string>;
549
575
  /**
550
576
  * How many of the most recent events to keep on `response.body` after
551
577
  * `end()`, for logging and debugging. Sent events are otherwise not kept,
@@ -555,6 +581,10 @@ type ResponseSSEOptions = {
555
581
  */
556
582
  retainEvents?: number;
557
583
  };
584
+ /** Wire serialization for one SSE event. Raw strings are split into safe data lines. */
585
+ type ResponseSSEMessageOptions = {
586
+ raw?: boolean;
587
+ };
558
588
  type ResponseSSEController = {
559
589
  /**
560
590
  * Send an SSE event to the client. Does not wait for the client; for
@@ -564,13 +594,15 @@ type ResponseSSEController = {
564
594
  * @param data - Event data (will be JSON stringified)
565
595
  * @param id - Optional event ID for client-side Last-Event-ID tracking
566
596
  */
567
- send: (event: string, data: any, id?: string) => ResponseSSEController;
597
+ send: (event: string | undefined, data: any, id?: string, options?: ResponseSSEMessageOptions) => ResponseSSEController; /** Bounded synchronous event; undefined event omits the event field. */
598
+ enqueue: (event: string | undefined, data: unknown, id?: string, options?: ResponseSSEMessageOptions) => boolean;
568
599
  /**
569
600
  * Send an SSE event and wait until the transport can take more.
570
601
  * Resolves `true` when it can, `false` when the stream ended or the client
571
- * disconnected — stop producing then.
602
+ * disconnected — stop producing then. In drop mode, true means ready to continue;
603
+ * inspect dropped to detect rejected output and schedule a resync.
572
604
  */
573
- write: (event: string, data: any, id?: string) => Promise<boolean>;
605
+ write: (event: string | undefined, data: any, id?: string, options?: ResponseSSEMessageOptions) => Promise<boolean>;
574
606
  /**
575
607
  * Wait until output buffered for the client has been flushed. Resolves
576
608
  * `true` immediately when nothing is backed up, `false` when the stream
@@ -585,7 +617,8 @@ type ResponseSSEController = {
585
617
  /**
586
618
  * Whether the transport is currently holding more than its high-water mark.
587
619
  */
588
- backpressured: boolean;
620
+ backpressured: boolean; /** Chunks/events/comments rejected by onOverflow: "drop", including send/write/render paths. */
621
+ readonly dropped: number;
589
622
  /**
590
623
  * Send a comment to keep the connection alive (invisible to client)
591
624
  */
@@ -605,5 +638,5 @@ type ResponseSSEController = {
605
638
  ended: boolean;
606
639
  };
607
640
  //#endregion
608
- export { DecodedAccessToken, HttpConfigurations, HttpServerOptions, PartialMiddleware, RequestEvent, RequestLocals, ResponseEvent, ResponseSSEController, ResponseSSEOptions, ResponseStreamController, ResponseStreamOptions, ReturnedResponse };
641
+ export { DecodedAccessToken, HttpConfigurations, HttpServerOptions, PartialMiddleware, RequestEvent, RequestLocals, ResponseEvent, ResponseSSEController, ResponseSSEMessageOptions, ResponseSSEOptions, ResponseStreamController, ResponseStreamOptions, ReturnedResponse };
609
642
  //# sourceMappingURL=types.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/http/types.ts"],"mappings":"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"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/http/types.ts"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import path from "node:path";
2
1
  import { randomBytes } from "node:crypto";
2
+ import path from "node:path";
3
3
  import fs from "node:fs/promises";
4
4
 
5
5
  //#region ../../packages/core/src/http/uploads/generate-image-variant.ts
@@ -1,3 +1,4 @@
1
+ import { StorageDriverName } from "../../storage/types.mjs";
1
2
  import { GeneratedImageDescriptor } from "./image-variant-types.mjs";
2
3
 
3
4
  //#region ../../packages/core/src/http/uploads/generate-image-variants.d.ts
@@ -9,7 +10,9 @@ type GenerateImageVariantsOptions = {
9
10
  * Limit generation to these variant names, instead of every variant in
10
11
  * `uploads.images.variants`.
11
12
  */
12
- variants?: readonly string[];
13
+ variants?: readonly string[]; /** Disk holding the source; defaults to the active disk. */
14
+ disk?: StorageDriverName; /** Route prefix serving variants of this disk (and originals for local disks). Default /uploads. */
15
+ urlPrefix?: string;
13
16
  };
14
17
  /**
15
18
  * Render every configured variant of one uploaded image, right after it is
@@ -25,15 +28,15 @@ type GenerateImageVariantsOptions = {
25
28
  * The returned descriptor is plain JSON, safe to store next to the record
26
29
  * that owns the image, or to carry through loader data. On local storage the
27
30
  * `url`/`urls` already point at `uploadedFileController`; an app serving
28
- * uploads from object storage or a CDN should not proxy the remote files
29
- * through this route at all — store the descriptor once generation runs
30
- * against the same bucket the CDN serves from, or resolve its URLs through a
31
- * custom `ImageLoader` on the client instead.
31
+ * uploads from object storage use the driver URL for the original, and
32
+ * `options.urlPrefix` for the route serving locally cached derivatives.
33
+ * Ingest downloads a remote source once for all variants and formats.
34
+ * On-demand cache hits use ETag metadata; sources without ETags are hashed.
32
35
  *
33
36
  * @param relativePath - path of the saved upload, relative to the storage root
34
37
  * @param options.variants - limit generation to these variant names
35
- * @throws ImageVariantsConfigError when there is no local storage root, or
36
- * `uploads.images` is not configured — both are server misconfiguration, not
38
+ * @throws ImageVariantsConfigError when `uploads.images` is not
39
+ * configured — this is server misconfiguration, not
37
40
  * a bad request
38
41
  * @throws HttpError(400) when a name in `options.variants` is not declared in
39
42
  * `uploads.images.variants`
@@ -1 +1 @@
1
- {"version":3,"file":"generate-image-variants.d.mts","names":[],"sources":["../../../../../../../../../packages/core/src/http/uploads/generate-image-variants.ts"],"mappings":";;;;;AAiCA;KAAY,4BAAA;;;AAKF;AA8EV;EA9EE,QAAQ;AAAA;;;;;;;;;;;;AAiFyB;;;;;;;;;;;;;;;;;;;;;iBAHb,qBAAA,CACpB,YAAA,UACA,OAAA,GAAS,4BAAA,GACR,OAAA,CAAQ,wBAAA"}
1
+ {"version":3,"file":"generate-image-variants.d.mts","names":[],"sources":["../../../../../../../../../packages/core/src/http/uploads/generate-image-variants.ts"],"mappings":";;;;;;AAqCA;KAAY,4BAAA;;;;;EAKV,QAAA,sBAMA;EAHA,IAAA,GAAO,iBAAiB,EAGf;EAAT,SAAA;AAAA;;;;;;;;;;;;;AAuEiC;;;;;;;;;;;;;;;;;;;;iBAHb,qBAAA,CACpB,YAAA,UACA,OAAA,GAAS,4BAAA,GACR,OAAA,CAAQ,wBAAA"}
@@ -1,28 +1,22 @@
1
1
  import { HttpError } from "../errors/errors.mjs";
2
+ import { ImageVariantsConfigError } from "./image-variants-config-error.mjs";
2
3
  import { uploadsConfig } from "../uploads-config.mjs";
3
4
  import { variantCacheKey, variantCachePath, variantGenerationTag, variantSourceDirectory, variantSourceKey } from "./variant-cache-key.mjs";
4
5
  import { pruneStaleVariantGenerationsIfCurrent, retryOnVanishedDirectory, warnIfVariantCacheIsPublic } from "./image-variant-cache.mjs";
5
- import { storage } from "../../storage/storage.mjs";
6
- import "../../storage/index.mjs";
7
6
  import { Image } from "../../image/image.mjs";
8
7
  import "../../image/index.mjs";
9
- import { generateImageVariant } from "./generate-image-variant.mjs";
10
- import { ImageVariantsConfigError } from "./image-variants-config-error.mjs";
8
+ import { localDiskRoot } from "./local-disk-root.mjs";
11
9
  import { loadVariantSource } from "./load-variant-source.mjs";
10
+ import { remoteVariantDisk, remoteVariantSource } from "./remote-variant-source.mjs";
11
+ import { generateImageVariant } from "./generate-image-variant.mjs";
12
12
  import { resolveImageVariantsConfig } from "./resolve-image-variants-config.mjs";
13
- import { resolveVariantCandidate } from "./resolve-variant-candidate.mjs";
13
+ import { freshImageVariant, scheduleImageVariantCacheTrim } from "./trim-image-variant-cache.mjs";
14
14
  import { variantGenerations } from "./variant-generations.mjs";
15
+ import { renderRemoteImageVariant } from "./resolve-remote-image-variant.mjs";
16
+ import { resolveVariantCandidate } from "./resolve-variant-candidate.mjs";
15
17
  import fs from "node:fs/promises";
16
18
 
17
19
  //#region ../../packages/core/src/http/uploads/generate-image-variants.ts
18
- async function exists(target) {
19
- try {
20
- await fs.access(target);
21
- return true;
22
- } catch {
23
- return false;
24
- }
25
- }
26
20
  function encodeUploadsPath(relativePath) {
27
21
  return relativePath.split("/").map(encodeURIComponent).join("/");
28
22
  }
@@ -58,15 +52,15 @@ function outputFormatsFor(sourceFormat, configuredFormats) {
58
52
  * The returned descriptor is plain JSON, safe to store next to the record
59
53
  * that owns the image, or to carry through loader data. On local storage the
60
54
  * `url`/`urls` already point at `uploadedFileController`; an app serving
61
- * uploads from object storage or a CDN should not proxy the remote files
62
- * through this route at all — store the descriptor once generation runs
63
- * against the same bucket the CDN serves from, or resolve its URLs through a
64
- * custom `ImageLoader` on the client instead.
55
+ * uploads from object storage use the driver URL for the original, and
56
+ * `options.urlPrefix` for the route serving locally cached derivatives.
57
+ * Ingest downloads a remote source once for all variants and formats.
58
+ * On-demand cache hits use ETag metadata; sources without ETags are hashed.
65
59
  *
66
60
  * @param relativePath - path of the saved upload, relative to the storage root
67
61
  * @param options.variants - limit generation to these variant names
68
- * @throws ImageVariantsConfigError when there is no local storage root, or
69
- * `uploads.images` is not configured — both are server misconfiguration, not
62
+ * @throws ImageVariantsConfigError when `uploads.images` is not
63
+ * configured — this is server misconfiguration, not
70
64
  * a bad request
71
65
  * @throws HttpError(400) when a name in `options.variants` is not declared in
72
66
  * `uploads.images.variants`
@@ -77,8 +71,8 @@ function outputFormatsFor(sourceFormat, configuredFormats) {
77
71
  * @throws HttpError(415) when the source is not jpeg, png, webp or avif
78
72
  */
79
73
  async function generateImageVariants(relativePath, options = {}) {
80
- const storageRoot = storage.root();
81
- if (!storageRoot || storageRoot === ".") throw new ImageVariantsConfigError("generateImageVariants needs a local storage driver with a root.");
74
+ const storageRoot = localDiskRoot(options.disk);
75
+ if (!storageRoot) return generateRemoteImageVariants(relativePath, options);
82
76
  const images = resolveImageVariantsConfig(uploadsConfig("images"), storageRoot);
83
77
  if (!images) throw new ImageVariantsConfigError("uploads.images is not configured.");
84
78
  const names = options.variants ?? Object.keys(images.variants);
@@ -93,7 +87,7 @@ async function generateImageVariants(relativePath, options = {}) {
93
87
  });
94
88
  const { width: sourceWidth, height: sourceHeight } = await image.metadata();
95
89
  warnIfVariantCacheIsPublic(images.cacheDirectory);
96
- const src = `/uploads/${encodeUploadsPath(source.relativePath)}`;
90
+ const src = `${(options.urlPrefix ?? "/uploads").replace(/\/$/, "")}/${encodeUploadsPath(source.relativePath)}`;
97
91
  const variants = {};
98
92
  for (const name of names) {
99
93
  const variant = images.variants[name];
@@ -114,14 +108,15 @@ async function generateImageVariants(relativePath, options = {}) {
114
108
  sourceKey,
115
109
  generation
116
110
  }, hash, format);
117
- if (!await exists(target)) await variantGenerations.run(target, async () => {
118
- if (await exists(target)) return;
111
+ if (!await freshImageVariant(target, images.maxCacheAge)) await variantGenerations.run(target, async () => {
112
+ if (await freshImageVariant(target, images.maxCacheAge)) return;
119
113
  await retryOnVanishedDirectory(() => generateImageVariant({
120
114
  image: image.clone(),
121
115
  variant,
122
116
  format,
123
117
  targetPath: target
124
118
  }));
119
+ scheduleImageVariantCacheTrim(images, target);
125
120
  await pruneStaleVariantGenerationsIfCurrent(source.absolutePath, variantSourceDirectory(images.cacheDirectory, sourceKey), generation);
126
121
  });
127
122
  if (dimensions === void 0) dimensions = await readDerivativeDimensions(target);
@@ -143,6 +138,48 @@ async function generateImageVariants(relativePath, options = {}) {
143
138
  ...images.formats.length > 0 ? { formats: images.formats } : {}
144
139
  };
145
140
  }
141
+ async function generateRemoteImageVariants(relativePath, options) {
142
+ const disk = remoteVariantDisk(options.disk);
143
+ const images = resolveImageVariantsConfig(uploadsConfig("images"), disk.cacheRoot);
144
+ if (!images) throw new ImageVariantsConfigError("uploads.images is not configured.");
145
+ const names = options.variants ?? Object.keys(images.variants);
146
+ for (const name of names) if (!Object.prototype.hasOwnProperty.call(images.variants, name)) throw new HttpError(400, `Unknown variant "${name}".`);
147
+ const source = await remoteVariantSource(relativePath, disk, images);
148
+ if (!source) throw new HttpError(404, "File Not Found");
149
+ try {
150
+ const { image, sourceFormat } = await source.load();
151
+ const { width, height } = await image.metadata();
152
+ const variantSrc = `${(options.urlPrefix ?? "/uploads").replace(/\/$/, "")}/${encodeUploadsPath(source.relativePath)}`;
153
+ const variants = {};
154
+ for (const name of names) {
155
+ let dimensions;
156
+ for (const format of outputFormatsFor(sourceFormat, images.formats)) {
157
+ const resolved = await renderRemoteImageVariant({
158
+ path: relativePath,
159
+ variant: name,
160
+ ...format === sourceFormat ? {} : { format }
161
+ }, disk, images, source);
162
+ if (!resolved) throw new HttpError(404, "File Not Found");
163
+ dimensions ??= await readDerivativeDimensions(resolved.absolutePath);
164
+ }
165
+ variants[name] = {
166
+ width: dimensions?.width ?? 0,
167
+ height: dimensions?.height ?? 0,
168
+ url: variantUrl(variantSrc, name),
169
+ ...images.formats.length ? { urls: Object.fromEntries(images.formats.map((format) => [format, variantUrl(variantSrc, name, format)])) } : {}
170
+ };
171
+ }
172
+ return {
173
+ src: disk.driver.url(source.relativePath),
174
+ width: width ?? 0,
175
+ height: height ?? 0,
176
+ variants,
177
+ ...images.formats.length ? { formats: images.formats } : {}
178
+ };
179
+ } finally {
180
+ source.release();
181
+ }
182
+ }
146
183
 
147
184
  //#endregion
148
185
  export { generateImageVariants };
@@ -1 +1 @@
1
- {"version":3,"file":"generate-image-variants.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/http/uploads/generate-image-variants.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport { Image } from \"../../image\";\nimport { storage } from \"../../storage\";\nimport { HttpError } from \"../errors\";\nimport { uploadsConfig } from \"../uploads-config\";\nimport type { ImageVariantOutputFormat } from \"../uploads-types\";\nimport { generateImageVariant } from \"./generate-image-variant\";\nimport {\n pruneStaleVariantGenerationsIfCurrent,\n retryOnVanishedDirectory,\n warnIfVariantCacheIsPublic,\n} from \"./image-variant-cache\";\nimport { ImageVariantsConfigError } from \"./image-variants-config-error\";\nimport type {\n GeneratedImageDescriptor,\n GeneratedImageVariant,\n VariantOutputFormat,\n} from \"./image-variant-types\";\nimport { loadVariantSource } from \"./load-variant-source\";\nimport { resolveImageVariantsConfig } from \"./resolve-image-variants-config\";\nimport { resolveVariantCandidate } from \"./resolve-variant-candidate\";\nimport {\n variantCacheKey,\n variantCachePath,\n variantGenerationTag,\n variantSourceDirectory,\n variantSourceKey,\n} from \"./variant-cache-key\";\nimport { variantGenerations } from \"./variant-generations\";\n\n/**\n * Options for `generateImageVariants`\n */\nexport type GenerateImageVariantsOptions = {\n /**\n * Limit generation to these variant names, instead of every variant in\n * `uploads.images.variants`.\n */\n variants?: readonly string[];\n};\n\nasync function exists(target: string): Promise<boolean> {\n try {\n await fs.access(target);\n\n return true;\n } catch {\n return false;\n }\n}\n\nfunction encodeUploadsPath(relativePath: string): string {\n return relativePath.split(\"/\").map(encodeURIComponent).join(\"/\");\n}\n\nfunction variantUrl(src: string, name: string, format?: VariantOutputFormat): string {\n const query = format\n ? `variant=${encodeURIComponent(name)}&format=${format}`\n : `variant=${encodeURIComponent(name)}`;\n\n return `${src}?${query}`;\n}\n\nasync function readDerivativeDimensions(\n target: string,\n): Promise<{ width: number; height: number }> {\n const derivative = new Image(await fs.readFile(target));\n const { width, height } = await derivative.metadata();\n\n return { width: width ?? 0, height: height ?? 0 };\n}\n\n/**\n * Every distinct format a variant is rendered in: the source format plus\n * whatever `uploads.images.formats` adds, with duplicates dropped so a format\n * that is both the source format and an allowed format is generated once.\n */\nfunction outputFormatsFor(\n sourceFormat: VariantOutputFormat,\n configuredFormats: readonly ImageVariantOutputFormat[],\n): VariantOutputFormat[] {\n return [...new Set<VariantOutputFormat>([sourceFormat, ...configuredFormats])];\n}\n\n/**\n * Render every configured variant of one uploaded image, right after it is\n * saved.\n *\n * Uses the exact same normalized config (`uploads.images`), cache key and\n * derivative path as `uploadedFileController`, so a variant generated here is\n * already on disk the first time `GET /uploads/<path>?variant=<name>` is\n * requested — no second render. `variantGenerations` (the single-flight used\n * by the route) is reused, so a concurrent request for a variant this call is\n * still generating joins the same run instead of starting a second one.\n *\n * The returned descriptor is plain JSON, safe to store next to the record\n * that owns the image, or to carry through loader data. On local storage the\n * `url`/`urls` already point at `uploadedFileController`; an app serving\n * uploads from object storage or a CDN should not proxy the remote files\n * through this route at all — store the descriptor once generation runs\n * against the same bucket the CDN serves from, or resolve its URLs through a\n * custom `ImageLoader` on the client instead.\n *\n * @param relativePath - path of the saved upload, relative to the storage root\n * @param options.variants - limit generation to these variant names\n * @throws ImageVariantsConfigError when there is no local storage root, or\n * `uploads.images` is not configured — both are server misconfiguration, not\n * a bad request\n * @throws HttpError(400) when a name in `options.variants` is not declared in\n * `uploads.images.variants`\n * @throws HttpError(404) when `relativePath` does not resolve to a file\n * inside the storage root\n * @throws HttpError(413) when the source is over `maxSourceBytes` or\n * `maxSourcePixels`\n * @throws HttpError(415) when the source is not jpeg, png, webp or avif\n */\nexport async function generateImageVariants(\n relativePath: string,\n options: GenerateImageVariantsOptions = {},\n): Promise<GeneratedImageDescriptor> {\n const storageRoot = storage.root();\n\n if (!storageRoot || storageRoot === \".\") {\n throw new ImageVariantsConfigError(\n \"generateImageVariants needs a local storage driver with a root.\",\n );\n }\n\n const images = resolveImageVariantsConfig(uploadsConfig(\"images\"), storageRoot);\n\n if (!images) {\n throw new ImageVariantsConfigError(\"uploads.images is not configured.\");\n }\n\n const names = options.variants ?? Object.keys(images.variants);\n\n for (const name of names) {\n if (!Object.prototype.hasOwnProperty.call(images.variants, name)) {\n throw new HttpError(400, `Unknown variant \"${name}\".`);\n }\n }\n\n const candidate = await resolveVariantCandidate(relativePath, storageRoot, images);\n\n if (!candidate) {\n throw new HttpError(404, \"File Not Found\");\n }\n\n const { source, stats, sourceFormat } = candidate;\n\n const image = await loadVariantSource(source.absolutePath, {\n maxSourceBytes: images.maxSourceBytes,\n maxSourcePixels: images.maxSourcePixels,\n expectedFormat: sourceFormat,\n });\n\n const { width: sourceWidth, height: sourceHeight } = await image.metadata();\n warnIfVariantCacheIsPublic(images.cacheDirectory);\n\n const src = `/uploads/${encodeUploadsPath(source.relativePath)}`;\n const variants: Record<string, GeneratedImageVariant> = {};\n\n for (const name of names) {\n const variant = images.variants[name];\n if (variant === undefined) {\n throw new HttpError(400, `Unknown variant \"${name}\".`);\n }\n const formats = outputFormatsFor(sourceFormat, images.formats);\n let dimensions: { width: number; height: number } | undefined;\n\n for (const format of formats) {\n const hash = variantCacheKey({\n relativePath: source.relativePath,\n sourceSize: stats.size,\n sourceMtimeMs: stats.mtimeMs,\n variant,\n format,\n });\n const sourceKey = variantSourceKey(source.root, source.relativePath);\n const generation = variantGenerationTag(stats.size, stats.mtimeMs);\n const target = variantCachePath(\n images.cacheDirectory,\n { sourceKey, generation },\n hash,\n format,\n );\n\n if (!(await exists(target))) {\n // keyed on the target, which includes the disk root (see resolveImageVariant)\n await variantGenerations.run(target, async () => {\n if (await exists(target)) return;\n\n await retryOnVanishedDirectory(() =>\n generateImageVariant({ image: image.clone(), variant, format, targetPath: target }),\n );\n await pruneStaleVariantGenerationsIfCurrent(\n source.absolutePath,\n variantSourceDirectory(images.cacheDirectory, sourceKey),\n generation,\n );\n });\n }\n\n if (dimensions === undefined) {\n dimensions = await readDerivativeDimensions(target);\n }\n }\n\n const urls: Partial<Record<ImageVariantOutputFormat, string>> = {};\n\n for (const format of images.formats) {\n urls[format] = variantUrl(src, name, format);\n }\n\n variants[name] = {\n width: dimensions?.width ?? variant.width,\n height:\n dimensions?.height ??\n variant.height ??\n Math.round(variant.width * ((sourceHeight ?? 1) / (sourceWidth ?? 1))),\n url: variantUrl(src, name),\n ...(Object.keys(urls).length > 0 ? { urls } : {}),\n };\n }\n\n return {\n src,\n width: sourceWidth ?? 0,\n height: sourceHeight ?? 0,\n variants,\n ...(images.formats.length > 0 ? { formats: images.formats } : {}),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"generate-image-variants.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/http/uploads/generate-image-variants.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport { Image } from \"../../image\";\nimport type { StorageDriverName } from \"../../storage/types\";\nimport { localDiskRoot } from \"./local-disk-root\";\nimport { remoteVariantDisk, remoteVariantSource } from \"./remote-variant-source\";\nimport { renderRemoteImageVariant } from \"./resolve-remote-image-variant\";\nimport { HttpError } from \"../errors\";\nimport { uploadsConfig } from \"../uploads-config\";\nimport type { ImageVariantOutputFormat } from \"../uploads-types\";\nimport { generateImageVariant } from \"./generate-image-variant\";\nimport {\n pruneStaleVariantGenerationsIfCurrent,\n retryOnVanishedDirectory,\n warnIfVariantCacheIsPublic,\n} from \"./image-variant-cache\";\nimport { ImageVariantsConfigError } from \"./image-variants-config-error\";\nimport type {\n GeneratedImageDescriptor,\n GeneratedImageVariant,\n VariantOutputFormat,\n} from \"./image-variant-types\";\nimport { loadVariantSource } from \"./load-variant-source\";\nimport { resolveImageVariantsConfig } from \"./resolve-image-variants-config\";\nimport { resolveVariantCandidate } from \"./resolve-variant-candidate\";\nimport {\n variantCacheKey,\n variantCachePath,\n variantGenerationTag,\n variantSourceDirectory,\n variantSourceKey,\n} from \"./variant-cache-key\";\nimport { freshImageVariant, scheduleImageVariantCacheTrim } from \"./trim-image-variant-cache\";\nimport { variantGenerations } from \"./variant-generations\";\n\n/**\n * Options for `generateImageVariants`\n */\nexport type GenerateImageVariantsOptions = {\n /**\n * Limit generation to these variant names, instead of every variant in\n * `uploads.images.variants`.\n */\n variants?: readonly string[];\n\n /** Disk holding the source; defaults to the active disk. */\n disk?: StorageDriverName;\n\n /** Route prefix serving variants of this disk (and originals for local disks). Default /uploads. */\n urlPrefix?: string;\n};\n\nfunction encodeUploadsPath(relativePath: string): string {\n return relativePath.split(\"/\").map(encodeURIComponent).join(\"/\");\n}\n\nfunction variantUrl(src: string, name: string, format?: VariantOutputFormat): string {\n const query = format\n ? `variant=${encodeURIComponent(name)}&format=${format}`\n : `variant=${encodeURIComponent(name)}`;\n\n return `${src}?${query}`;\n}\n\nasync function readDerivativeDimensions(\n target: string,\n): Promise<{ width: number; height: number }> {\n const derivative = new Image(await fs.readFile(target));\n const { width, height } = await derivative.metadata();\n\n return { width: width ?? 0, height: height ?? 0 };\n}\n\n/**\n * Every distinct format a variant is rendered in: the source format plus\n * whatever `uploads.images.formats` adds, with duplicates dropped so a format\n * that is both the source format and an allowed format is generated once.\n */\nfunction outputFormatsFor(\n sourceFormat: VariantOutputFormat,\n configuredFormats: readonly ImageVariantOutputFormat[],\n): VariantOutputFormat[] {\n return [...new Set<VariantOutputFormat>([sourceFormat, ...configuredFormats])];\n}\n\n/**\n * Render every configured variant of one uploaded image, right after it is\n * saved.\n *\n * Uses the exact same normalized config (`uploads.images`), cache key and\n * derivative path as `uploadedFileController`, so a variant generated here is\n * already on disk the first time `GET /uploads/<path>?variant=<name>` is\n * requested — no second render. `variantGenerations` (the single-flight used\n * by the route) is reused, so a concurrent request for a variant this call is\n * still generating joins the same run instead of starting a second one.\n *\n * The returned descriptor is plain JSON, safe to store next to the record\n * that owns the image, or to carry through loader data. On local storage the\n * `url`/`urls` already point at `uploadedFileController`; an app serving\n * uploads from object storage use the driver URL for the original, and\n * `options.urlPrefix` for the route serving locally cached derivatives.\n * Ingest downloads a remote source once for all variants and formats.\n * On-demand cache hits use ETag metadata; sources without ETags are hashed.\n *\n * @param relativePath - path of the saved upload, relative to the storage root\n * @param options.variants - limit generation to these variant names\n * @throws ImageVariantsConfigError when `uploads.images` is not\n * configured — this is server misconfiguration, not\n * a bad request\n * @throws HttpError(400) when a name in `options.variants` is not declared in\n * `uploads.images.variants`\n * @throws HttpError(404) when `relativePath` does not resolve to a file\n * inside the storage root\n * @throws HttpError(413) when the source is over `maxSourceBytes` or\n * `maxSourcePixels`\n * @throws HttpError(415) when the source is not jpeg, png, webp or avif\n */\nexport async function generateImageVariants(\n relativePath: string,\n options: GenerateImageVariantsOptions = {},\n): Promise<GeneratedImageDescriptor> {\n const storageRoot = localDiskRoot(options.disk);\n\n if (!storageRoot) return generateRemoteImageVariants(relativePath, options);\n\n const images = resolveImageVariantsConfig(uploadsConfig(\"images\"), storageRoot);\n\n if (!images) {\n throw new ImageVariantsConfigError(\"uploads.images is not configured.\");\n }\n\n const names = options.variants ?? Object.keys(images.variants);\n\n for (const name of names) {\n if (!Object.prototype.hasOwnProperty.call(images.variants, name)) {\n throw new HttpError(400, `Unknown variant \"${name}\".`);\n }\n }\n\n const candidate = await resolveVariantCandidate(relativePath, storageRoot, images);\n\n if (!candidate) {\n throw new HttpError(404, \"File Not Found\");\n }\n\n const { source, stats, sourceFormat } = candidate;\n\n const image = await loadVariantSource(source.absolutePath, {\n maxSourceBytes: images.maxSourceBytes,\n maxSourcePixels: images.maxSourcePixels,\n expectedFormat: sourceFormat,\n });\n\n const { width: sourceWidth, height: sourceHeight } = await image.metadata();\n warnIfVariantCacheIsPublic(images.cacheDirectory);\n\n const src = `${(options.urlPrefix ?? \"/uploads\").replace(/\\/$/, \"\")}/${encodeUploadsPath(source.relativePath)}`;\n const variants: Record<string, GeneratedImageVariant> = {};\n\n for (const name of names) {\n const variant = images.variants[name];\n if (variant === undefined) {\n throw new HttpError(400, `Unknown variant \"${name}\".`);\n }\n const formats = outputFormatsFor(sourceFormat, images.formats);\n let dimensions: { width: number; height: number } | undefined;\n\n for (const format of formats) {\n const hash = variantCacheKey({\n relativePath: source.relativePath,\n sourceSize: stats.size,\n sourceMtimeMs: stats.mtimeMs,\n variant,\n format,\n });\n const sourceKey = variantSourceKey(source.root, source.relativePath);\n const generation = variantGenerationTag(stats.size, stats.mtimeMs);\n const target = variantCachePath(\n images.cacheDirectory,\n { sourceKey, generation },\n hash,\n format,\n );\n\n if (!(await freshImageVariant(target, images.maxCacheAge))) {\n // keyed on the target, which includes the disk root (see resolveImageVariant)\n await variantGenerations.run(target, async () => {\n if (await freshImageVariant(target, images.maxCacheAge)) return;\n\n await retryOnVanishedDirectory(() =>\n generateImageVariant({ image: image.clone(), variant, format, targetPath: target }),\n );\n scheduleImageVariantCacheTrim(images, target);\n await pruneStaleVariantGenerationsIfCurrent(\n source.absolutePath,\n variantSourceDirectory(images.cacheDirectory, sourceKey),\n generation,\n );\n });\n }\n\n if (dimensions === undefined) {\n dimensions = await readDerivativeDimensions(target);\n }\n }\n\n const urls: Partial<Record<ImageVariantOutputFormat, string>> = {};\n\n for (const format of images.formats) {\n urls[format] = variantUrl(src, name, format);\n }\n\n variants[name] = {\n width: dimensions?.width ?? variant.width,\n height:\n dimensions?.height ??\n variant.height ??\n Math.round(variant.width * ((sourceHeight ?? 1) / (sourceWidth ?? 1))),\n url: variantUrl(src, name),\n ...(Object.keys(urls).length > 0 ? { urls } : {}),\n };\n }\n\n return {\n src,\n width: sourceWidth ?? 0,\n height: sourceHeight ?? 0,\n variants,\n ...(images.formats.length > 0 ? { formats: images.formats } : {}),\n };\n}\n\nasync function generateRemoteImageVariants(\n relativePath: string,\n options: GenerateImageVariantsOptions,\n): Promise<GeneratedImageDescriptor> {\n const disk = remoteVariantDisk(options.disk);\n const images = resolveImageVariantsConfig(uploadsConfig(\"images\"), disk.cacheRoot);\n if (!images) throw new ImageVariantsConfigError(\"uploads.images is not configured.\");\n const names = options.variants ?? Object.keys(images.variants);\n for (const name of names) {\n if (!Object.prototype.hasOwnProperty.call(images.variants, name))\n throw new HttpError(400, `Unknown variant \"${name}\".`);\n }\n const source = await remoteVariantSource(relativePath, disk, images);\n if (!source) throw new HttpError(404, \"File Not Found\");\n try {\n const { image, sourceFormat } = await source.load();\n const { width, height } = await image.metadata();\n const variantSrc = `${(options.urlPrefix ?? \"/uploads\").replace(/\\/$/, \"\")}/${encodeUploadsPath(source.relativePath)}`;\n const variants: Record<string, GeneratedImageVariant> = {};\n for (const name of names) {\n let dimensions: { width: number; height: number } | undefined;\n for (const format of outputFormatsFor(sourceFormat, images.formats)) {\n const resolved = await renderRemoteImageVariant(\n {\n path: relativePath,\n variant: name,\n ...(format === sourceFormat ? {} : { format }),\n },\n disk,\n images,\n source,\n );\n if (!resolved) throw new HttpError(404, \"File Not Found\");\n dimensions ??= await readDerivativeDimensions(resolved.absolutePath);\n }\n variants[name] = {\n width: dimensions?.width ?? 0,\n height: dimensions?.height ?? 0,\n url: variantUrl(variantSrc, name),\n ...(images.formats.length\n ? {\n urls: Object.fromEntries(\n images.formats.map((format) => [format, variantUrl(variantSrc, name, format)]),\n ),\n }\n : {}),\n };\n }\n return {\n src: disk.driver.url(source.relativePath),\n width: width ?? 0,\n height: height ?? 0,\n variants,\n ...(images.formats.length ? { formats: images.formats } : {}),\n };\n } finally {\n source.release();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmDA,SAAS,kBAAkB,cAA8B;CACvD,OAAO,aAAa,MAAM,GAAG,CAAC,CAAC,IAAI,kBAAkB,CAAC,CAAC,KAAK,GAAG;AACjE;AAEA,SAAS,WAAW,KAAa,MAAc,QAAsC;CAKnF,OAAO,GAAG,IAAI,GAJA,SACV,WAAW,mBAAmB,IAAI,EAAE,UAAU,WAC9C,WAAW,mBAAmB,IAAI;AAGxC;AAEA,eAAe,yBACb,QAC4C;CAE5C,MAAM,EAAE,OAAO,WAAW,MAAM,IADT,MAAM,MAAM,GAAG,SAAS,MAAM,CACZ,CAAC,CAAC,SAAS;CAEpD,OAAO;EAAE,OAAO,SAAS;EAAG,QAAQ,UAAU;CAAE;AAClD;;;;;;AAOA,SAAS,iBACP,cACA,mBACuB;CACvB,OAAO,CAAC,GAAG,IAAI,IAAyB,CAAC,cAAc,GAAG,iBAAiB,CAAC,CAAC;AAC/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,eAAsB,sBACpB,cACA,UAAwC,CAAC,GACN;CACnC,MAAM,cAAc,cAAc,QAAQ,IAAI;CAE9C,IAAI,CAAC,aAAa,OAAO,4BAA4B,cAAc,OAAO;CAE1E,MAAM,SAAS,2BAA2B,cAAc,QAAQ,GAAG,WAAW;CAE9E,IAAI,CAAC,QACH,MAAM,IAAI,yBAAyB,mCAAmC;CAGxE,MAAM,QAAQ,QAAQ,YAAY,OAAO,KAAK,OAAO,QAAQ;CAE7D,KAAK,MAAM,QAAQ,OACjB,IAAI,CAAC,OAAO,UAAU,eAAe,KAAK,OAAO,UAAU,IAAI,GAC7D,MAAM,IAAI,UAAU,KAAK,oBAAoB,KAAK,GAAG;CAIzD,MAAM,YAAY,MAAM,wBAAwB,cAAc,aAAa,MAAM;CAEjF,IAAI,CAAC,WACH,MAAM,IAAI,UAAU,KAAK,gBAAgB;CAG3C,MAAM,EAAE,QAAQ,OAAO,iBAAiB;CAExC,MAAM,QAAQ,MAAM,kBAAkB,OAAO,cAAc;EACzD,gBAAgB,OAAO;EACvB,iBAAiB,OAAO;EACxB,gBAAgB;CAClB,CAAC;CAED,MAAM,EAAE,OAAO,aAAa,QAAQ,iBAAiB,MAAM,MAAM,SAAS;CAC1E,2BAA2B,OAAO,cAAc;CAEhD,MAAM,MAAM,IAAI,QAAQ,aAAa,WAAU,CAAE,QAAQ,OAAO,EAAE,EAAE,GAAG,kBAAkB,OAAO,YAAY;CAC5G,MAAM,WAAkD,CAAC;CAEzD,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,OAAO,SAAS;EAChC,IAAI,YAAY,QACd,MAAM,IAAI,UAAU,KAAK,oBAAoB,KAAK,GAAG;EAEvD,MAAM,UAAU,iBAAiB,cAAc,OAAO,OAAO;EAC7D,IAAI;EAEJ,KAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,OAAO,gBAAgB;IAC3B,cAAc,OAAO;IACrB,YAAY,MAAM;IAClB,eAAe,MAAM;IACrB;IACA;GACF,CAAC;GACD,MAAM,YAAY,iBAAiB,OAAO,MAAM,OAAO,YAAY;GACnE,MAAM,aAAa,qBAAqB,MAAM,MAAM,MAAM,OAAO;GACjE,MAAM,SAAS,iBACb,OAAO,gBACP;IAAE;IAAW;GAAW,GACxB,MACA,MACF;GAEA,IAAI,CAAE,MAAM,kBAAkB,QAAQ,OAAO,WAAW,GAEtD,MAAM,mBAAmB,IAAI,QAAQ,YAAY;IAC/C,IAAI,MAAM,kBAAkB,QAAQ,OAAO,WAAW,GAAG;IAEzD,MAAM,+BACJ,qBAAqB;KAAE,OAAO,MAAM,MAAM;KAAG;KAAS;KAAQ,YAAY;IAAO,CAAC,CACpF;IACA,8BAA8B,QAAQ,MAAM;IAC5C,MAAM,sCACJ,OAAO,cACP,uBAAuB,OAAO,gBAAgB,SAAS,GACvD,UACF;GACF,CAAC;GAGH,IAAI,eAAe,QACjB,aAAa,MAAM,yBAAyB,MAAM;EAEtD;EAEA,MAAM,OAA0D,CAAC;EAEjE,KAAK,MAAM,UAAU,OAAO,SAC1B,KAAK,UAAU,WAAW,KAAK,MAAM,MAAM;EAG7C,SAAS,QAAQ;GACf,OAAO,YAAY,SAAS,QAAQ;GACpC,QACE,YAAY,UACZ,QAAQ,UACR,KAAK,MAAM,QAAQ,UAAU,gBAAgB,MAAM,eAAe,GAAG;GACvE,KAAK,WAAW,KAAK,IAAI;GACzB,GAAI,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,IAAI,EAAE,KAAK,IAAI,CAAC;EACjD;CACF;CAEA,OAAO;EACL;EACA,OAAO,eAAe;EACtB,QAAQ,gBAAgB;EACxB;EACA,GAAI,OAAO,QAAQ,SAAS,IAAI,EAAE,SAAS,OAAO,QAAQ,IAAI,CAAC;CACjE;AACF;AAEA,eAAe,4BACb,cACA,SACmC;CACnC,MAAM,OAAO,kBAAkB,QAAQ,IAAI;CAC3C,MAAM,SAAS,2BAA2B,cAAc,QAAQ,GAAG,KAAK,SAAS;CACjF,IAAI,CAAC,QAAQ,MAAM,IAAI,yBAAyB,mCAAmC;CACnF,MAAM,QAAQ,QAAQ,YAAY,OAAO,KAAK,OAAO,QAAQ;CAC7D,KAAK,MAAM,QAAQ,OACjB,IAAI,CAAC,OAAO,UAAU,eAAe,KAAK,OAAO,UAAU,IAAI,GAC7D,MAAM,IAAI,UAAU,KAAK,oBAAoB,KAAK,GAAG;CAEzD,MAAM,SAAS,MAAM,oBAAoB,cAAc,MAAM,MAAM;CACnE,IAAI,CAAC,QAAQ,MAAM,IAAI,UAAU,KAAK,gBAAgB;CACtD,IAAI;EACF,MAAM,EAAE,OAAO,iBAAiB,MAAM,OAAO,KAAK;EAClD,MAAM,EAAE,OAAO,WAAW,MAAM,MAAM,SAAS;EAC/C,MAAM,aAAa,IAAI,QAAQ,aAAa,WAAU,CAAE,QAAQ,OAAO,EAAE,EAAE,GAAG,kBAAkB,OAAO,YAAY;EACnH,MAAM,WAAkD,CAAC;EACzD,KAAK,MAAM,QAAQ,OAAO;GACxB,IAAI;GACJ,KAAK,MAAM,UAAU,iBAAiB,cAAc,OAAO,OAAO,GAAG;IACnE,MAAM,WAAW,MAAM,yBACrB;KACE,MAAM;KACN,SAAS;KACT,GAAI,WAAW,eAAe,CAAC,IAAI,EAAE,OAAO;IAC9C,GACA,MACA,QACA,MACF;IACA,IAAI,CAAC,UAAU,MAAM,IAAI,UAAU,KAAK,gBAAgB;IACxD,eAAe,MAAM,yBAAyB,SAAS,YAAY;GACrE;GACA,SAAS,QAAQ;IACf,OAAO,YAAY,SAAS;IAC5B,QAAQ,YAAY,UAAU;IAC9B,KAAK,WAAW,YAAY,IAAI;IAChC,GAAI,OAAO,QAAQ,SACf,EACE,MAAM,OAAO,YACX,OAAO,QAAQ,KAAK,WAAW,CAAC,QAAQ,WAAW,YAAY,MAAM,MAAM,CAAC,CAAC,CAC/E,EACF,IACA,CAAC;GACP;EACF;EACA,OAAO;GACL,KAAK,KAAK,OAAO,IAAI,OAAO,YAAY;GACxC,OAAO,SAAS;GAChB,QAAQ,UAAU;GAClB;GACA,GAAI,OAAO,QAAQ,SAAS,EAAE,SAAS,OAAO,QAAQ,IAAI,CAAC;EAC7D;CACF,UAAU;EACR,OAAO,QAAQ;CACjB;AACF"}