@warlock.js/core 5.2.3 → 5.3.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 (261) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/esm/application/application.d.mts.map +1 -1
  3. package/esm/application/application.mjs.map +1 -1
  4. package/esm/benchmark/benchmark-snapshots.d.mts.map +1 -1
  5. package/esm/benchmark/benchmark.d.mts.map +1 -1
  6. package/esm/benchmark/channels/console.channel.mjs.map +1 -1
  7. package/esm/benchmark/channels/noop.channel.d.mts.map +1 -1
  8. package/esm/benchmark/profiler.d.mts.map +1 -1
  9. package/esm/benchmark/profiler.mjs.map +1 -1
  10. package/esm/bootstrap/setup.d.mts.map +1 -1
  11. package/esm/bootstrap/setup.mjs.map +1 -1
  12. package/esm/bootstrap.d.mts.map +1 -1
  13. package/esm/cache/database-cache-driver.d.mts.map +1 -1
  14. package/esm/cache/database-cache-driver.mjs.map +1 -1
  15. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  16. package/esm/cli/cli-commands.utils.mjs.map +1 -1
  17. package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -1
  18. package/esm/cli/commands/doctor/checks/connectors.check.mjs.map +1 -1
  19. package/esm/cli/commands/doctor/checks/handler-signature.check.mjs.map +1 -1
  20. package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -1
  21. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
  22. package/esm/cli/commands/doctor/checks/routes.check.mjs.map +1 -1
  23. package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
  24. package/esm/cli/commands/generate/generators/column-dsl-parser.mjs.map +1 -1
  25. package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
  26. package/esm/cli/commands/generate/generators/model.generator.mjs.map +1 -1
  27. package/esm/cli/commands/generate/templates/stubs.mjs +13 -14
  28. package/esm/cli/commands/generate/templates/stubs.mjs.map +1 -1
  29. package/esm/cli/commands/generate/utils/name-parser.mjs.map +1 -1
  30. package/esm/cli/commands/generate/utils/prompt.mjs.map +1 -1
  31. package/esm/cli/commands/routes/diff-page-routes.mjs.map +1 -1
  32. package/esm/cli/commands/routes/format-routes-table.mjs.map +1 -1
  33. package/esm/cli/commands/routes/route-row.mjs.map +1 -1
  34. package/esm/cli/commands/routes/routes-command.action.mjs.map +1 -1
  35. package/esm/cli/commands/routes/routes-diff.action.mjs.map +1 -1
  36. package/esm/cli/commands/storage-put.action.mjs.map +1 -1
  37. package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
  38. package/esm/cli/commands-loader.mjs.map +1 -1
  39. package/esm/cli/start.mjs.map +1 -1
  40. package/esm/cli/string-similarity.mjs.map +1 -1
  41. package/esm/commands/cli-command.mjs.map +1 -1
  42. package/esm/config/config-loader.mjs.map +1 -1
  43. package/esm/config/config-setter.d.mts.map +1 -1
  44. package/esm/config/load-config-files.mjs.map +1 -1
  45. package/esm/connectors/access-connector.d.mts.map +1 -1
  46. package/esm/connectors/ai-connector.d.mts.map +1 -1
  47. package/esm/connectors/assert-no-reserved-connector-names.mjs.map +1 -1
  48. package/esm/connectors/assert-unique-connector-names.mjs.map +1 -1
  49. package/esm/connectors/base-connector.d.mts.map +1 -1
  50. package/esm/connectors/base-connector.mjs.map +1 -1
  51. package/esm/connectors/cache-connector.d.mts.map +1 -1
  52. package/esm/connectors/connectors-manager.d.mts.map +1 -1
  53. package/esm/connectors/connectors-manager.mjs.map +1 -1
  54. package/esm/connectors/database-connector.d.mts.map +1 -1
  55. package/esm/connectors/describe-server-address.mjs.map +1 -1
  56. package/esm/connectors/herald-connector.d.mts.map +1 -1
  57. package/esm/connectors/http-connector.d.mts.map +1 -1
  58. package/esm/connectors/http-connector.mjs.map +1 -1
  59. package/esm/connectors/logger-connector.d.mts.map +1 -1
  60. package/esm/connectors/mail-connector.d.mts.map +1 -1
  61. package/esm/connectors/notifications-connector.d.mts.map +1 -1
  62. package/esm/connectors/register-configured-connectors.mjs.map +1 -1
  63. package/esm/connectors/socket-connector.d.mts.map +1 -1
  64. package/esm/connectors/socket-connector.mjs.map +1 -1
  65. package/esm/connectors/storage.connector.d.mts.map +1 -1
  66. package/esm/container/index.d.mts.map +1 -1
  67. package/esm/database/create-database-action.mjs.map +1 -1
  68. package/esm/database/drop-tables-action.mjs.map +1 -1
  69. package/esm/database/migrate-action.mjs.map +1 -1
  70. package/esm/database/seed-command-action.mjs.map +1 -1
  71. package/esm/database/seeds/seed-records-table-migration.mjs.map +1 -1
  72. package/esm/database/seeds/seeders.manager.mjs.map +1 -1
  73. package/esm/database/seeds/seeds-table-migration.mjs.map +1 -1
  74. package/esm/dev-server/check-for-updates.mjs.map +1 -1
  75. package/esm/dev-server/dependency-graph.d.mts.map +1 -1
  76. package/esm/dev-server/dependency-graph.mjs.map +1 -1
  77. package/esm/dev-server/dev-logger.mjs.map +1 -1
  78. package/esm/dev-server/development-server.mjs.map +1 -1
  79. package/esm/dev-server/file-event-handler.mjs.map +1 -1
  80. package/esm/dev-server/file-manager.d.mts.map +1 -1
  81. package/esm/dev-server/file-manager.mjs.map +1 -1
  82. package/esm/dev-server/file-operations.d.mts.map +1 -1
  83. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  84. package/esm/dev-server/files-watcher.mjs.map +1 -1
  85. package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs.map +1 -1
  86. package/esm/dev-server/health-checker/checkers/typescript-health-checker.mjs.map +1 -1
  87. package/esm/dev-server/health-checker/file-health-result.d.mts.map +1 -1
  88. package/esm/dev-server/health-checker/file-health-result.mjs.map +1 -1
  89. package/esm/dev-server/health-checker/files-healthcare.manager.mjs.map +1 -1
  90. package/esm/dev-server/layer-executor.mjs.map +1 -1
  91. package/esm/dev-server/loader/hook-thread.d.mts.map +1 -1
  92. package/esm/dev-server/loader/own-resolver.mjs.map +1 -1
  93. package/esm/dev-server/loader/register-loader.mjs.map +1 -1
  94. package/esm/dev-server/loader/resolve-hook.mjs.map +1 -1
  95. package/esm/dev-server/loader/source-slug.mjs.map +1 -1
  96. package/esm/dev-server/loader/transpile-cache.mjs.map +1 -1
  97. package/esm/dev-server/loader/version-registry.mjs.map +1 -1
  98. package/esm/dev-server/manifest-manager.d.mts.map +1 -1
  99. package/esm/dev-server/manifest-manager.mjs.map +1 -1
  100. package/esm/dev-server/module-loader.mjs.map +1 -1
  101. package/esm/dev-server/package-json-manager.mjs.map +1 -1
  102. package/esm/dev-server/parse-imports.mjs.map +1 -1
  103. package/esm/dev-server/ready-block.mjs.map +1 -1
  104. package/esm/dev-server/special-files-collector.d.mts.map +1 -1
  105. package/esm/dev-server/special-files-collector.mjs.map +1 -1
  106. package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
  107. package/esm/dev-server/type-generator.mjs.map +1 -1
  108. package/esm/dev-server/utils.mjs.map +1 -1
  109. package/esm/encryption/encrypt.mjs.map +1 -1
  110. package/esm/encryption/hash.mjs.map +1 -1
  111. package/esm/generations/add-command.action.mjs.map +1 -1
  112. package/esm/generations/features/shadcn.feature.mjs.map +1 -1
  113. package/esm/generations/features/shared/migration-timestamp.mjs.map +1 -1
  114. package/esm/generations/features/tailwind.feature.mjs.map +1 -1
  115. package/esm/generations/features/web.feature.mjs +2 -1
  116. package/esm/generations/features/web.feature.mjs.map +1 -1
  117. package/esm/generations/stubs.mjs +53 -54
  118. package/esm/generations/stubs.mjs.map +1 -1
  119. package/esm/http/boot-port-preflight.d.mts.map +1 -1
  120. package/esm/http/context/request-context.d.mts.map +1 -1
  121. package/esm/http/context/request-context.mjs.map +1 -1
  122. package/esm/http/context/request-memo.d.mts.map +1 -1
  123. package/esm/http/context/request-memo.mjs.map +1 -1
  124. package/esm/http/createHttpApplication.d.mts.map +1 -1
  125. package/esm/http/createHttpApplication.mjs.map +1 -1
  126. package/esm/http/health.d.mts.map +1 -1
  127. package/esm/http/index.d.mts +1 -1
  128. package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
  129. package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
  130. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  131. package/esm/http/middleware/inject-request-context.mjs +2 -21
  132. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  133. package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
  134. package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
  135. package/esm/http/ready-report.d.mts.map +1 -1
  136. package/esm/http/request-controller.d.mts.map +1 -1
  137. package/esm/http/request.d.mts +25 -6
  138. package/esm/http/request.d.mts.map +1 -1
  139. package/esm/http/request.mjs +48 -0
  140. package/esm/http/request.mjs.map +1 -1
  141. package/esm/http/response.d.mts +3 -3
  142. package/esm/http/response.d.mts.map +1 -1
  143. package/esm/http/response.mjs.map +1 -1
  144. package/esm/http/server.d.mts.map +1 -1
  145. package/esm/http/server.mjs.map +1 -1
  146. package/esm/http/types.d.mts +36 -5
  147. package/esm/http/types.d.mts.map +1 -1
  148. package/esm/http/uploaded-file.d.mts.map +1 -1
  149. package/esm/http/uploaded-file.mjs.map +1 -1
  150. package/esm/http/uploads-config.d.mts.map +1 -1
  151. package/esm/image/image.d.mts.map +1 -1
  152. package/esm/image/image.mjs.map +1 -1
  153. package/esm/index.d.mts +2 -2
  154. package/esm/logger/logger.mjs.map +1 -1
  155. package/esm/mail/config.d.mts.map +1 -1
  156. package/esm/mail/events.d.mts.map +1 -1
  157. package/esm/mail/mail.d.mts.map +1 -1
  158. package/esm/mail/mail.mjs.map +1 -1
  159. package/esm/mail/mailer-pool.d.mts.map +1 -1
  160. package/esm/mail/mailer-pool.mjs.map +1 -1
  161. package/esm/mail/send-mail.mjs.map +1 -1
  162. package/esm/mail/test-mailbox.d.mts.map +1 -1
  163. package/esm/mail/test-mailbox.mjs.map +1 -1
  164. package/esm/production/assert-generated-imports.mjs.map +1 -1
  165. package/esm/production/build-app-production.mjs.map +1 -1
  166. package/esm/production/dist-build-manifest.mjs.map +1 -1
  167. package/esm/production/production-builder.mjs.map +1 -1
  168. package/esm/production/production-supervisor.mjs.map +1 -1
  169. package/esm/production/promote-dist.mjs.map +1 -1
  170. package/esm/production/resolve-build-config.mjs.map +1 -1
  171. package/esm/repositories/adapters/cascade/cascade-adapter.d.mts.map +1 -1
  172. package/esm/repositories/adapters/cascade/cascade-adapter.mjs.map +1 -1
  173. package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts.map +1 -1
  174. package/esm/repositories/adapters/cascade/cascade-query-builder.mjs.map +1 -1
  175. package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
  176. package/esm/repositories/repository.manager.d.mts.map +1 -1
  177. package/esm/repositories/repository.manager.mjs.map +1 -1
  178. package/esm/resource/register-resource.d.mts.map +1 -1
  179. package/esm/resource/resource-field-builder.d.mts.map +1 -1
  180. package/esm/resource/resource-field-builder.mjs.map +1 -1
  181. package/esm/resource/resource.d.mts.map +1 -1
  182. package/esm/resource/resource.mjs.map +1 -1
  183. package/esm/restful/restful.d.mts.map +1 -1
  184. package/esm/restful/restful.mjs.map +1 -1
  185. package/esm/router/log-request-lifecycle.mjs.map +1 -1
  186. package/esm/router/normalize-route-path.d.mts.map +1 -1
  187. package/esm/router/positional-handler-diagnostics.d.mts.map +1 -1
  188. package/esm/router/positional-handler-diagnostics.mjs.map +1 -1
  189. package/esm/router/route-registry.d.mts.map +1 -1
  190. package/esm/router/route-registry.mjs.map +1 -1
  191. package/esm/router/router.d.mts.map +1 -1
  192. package/esm/router/router.mjs.map +1 -1
  193. package/esm/socket/utils.d.mts.map +1 -1
  194. package/esm/storage/config.d.mts.map +1 -1
  195. package/esm/storage/context/storage-driver-context.d.mts.map +1 -1
  196. package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
  197. package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
  198. package/esm/storage/drivers/do-spaces-driver.d.mts.map +1 -1
  199. package/esm/storage/drivers/local-driver.d.mts.map +1 -1
  200. package/esm/storage/drivers/local-driver.mjs.map +1 -1
  201. package/esm/storage/drivers/r2-driver.d.mts.map +1 -1
  202. package/esm/storage/scoped-storage.d.mts.map +1 -1
  203. package/esm/storage/scoped-storage.mjs.map +1 -1
  204. package/esm/storage/storage-file.d.mts.map +1 -1
  205. package/esm/storage/storage-file.mjs.map +1 -1
  206. package/esm/storage/storage.d.mts.map +1 -1
  207. package/esm/storage/utils/safe-fetch.mjs.map +1 -1
  208. package/esm/tests/start-http-development-server.d.mts.map +1 -1
  209. package/esm/tests/test-connectors-selection.mjs.map +1 -1
  210. package/esm/tests/test-helpers.d.mts.map +1 -1
  211. package/esm/tests/test-setup-timeout.mjs.map +1 -1
  212. package/esm/tests/vitest-setup.d.mts.map +1 -1
  213. package/esm/tests/vitest-setup.mjs.map +1 -1
  214. package/esm/updater/update-warlock-packages.mjs.map +1 -1
  215. package/esm/use-cases/use-case-broadcast.d.mts.map +1 -1
  216. package/esm/use-cases/use-case-events.d.mts.map +1 -1
  217. package/esm/use-cases/use-case-pipeline.d.mts.map +1 -1
  218. package/esm/use-cases/use-case.d.mts.map +1 -1
  219. package/esm/use-cases/use-cases-registry.d.mts.map +1 -1
  220. package/esm/use-cases/use-cases-registry.mjs.map +1 -1
  221. package/esm/utils/database-log.d.mts.map +1 -1
  222. package/esm/utils/database-log.mjs.map +1 -1
  223. package/esm/utils/environment.d.mts.map +1 -1
  224. package/esm/utils/framework-vesion.mjs.map +1 -1
  225. package/esm/utils/get-localized.mjs.map +1 -1
  226. package/esm/utils/load-environment.mjs.map +1 -1
  227. package/esm/utils/normalized-path.d.mts.map +1 -1
  228. package/esm/utils/paths.d.mts.map +1 -1
  229. package/esm/utils/promise-all-object.d.mts.map +1 -1
  230. package/esm/utils/promise-all-object.mjs.map +1 -1
  231. package/esm/utils/sluggable.mjs.map +1 -1
  232. package/esm/utils/version-compare.mjs.map +1 -1
  233. package/esm/validation/database/exists-except-current-id.mjs.map +1 -1
  234. package/esm/validation/database/exists-except-current-user.mjs.map +1 -1
  235. package/esm/validation/database/unique-except-current-id.mjs.map +1 -1
  236. package/esm/validation/plugins/file-plugin.mjs.map +1 -1
  237. package/esm/validation/plugins/localized-plugin.mjs +2 -2
  238. package/esm/validation/plugins/localized-plugin.mjs.map +1 -1
  239. package/esm/validation/types.d.mts +17 -4
  240. package/esm/validation/types.d.mts.map +1 -1
  241. package/esm/validation/validators/file-validator.mjs.map +1 -1
  242. package/esm/vite/lower-stage3-decorators.d.mts.map +1 -1
  243. package/esm/warlock-config/warlock-config.manager.d.mts.map +1 -1
  244. package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
  245. package/llms-full.txt +113 -87
  246. package/llms.txt +2 -2
  247. package/package.json +12 -12
  248. package/skills/README.md +2 -2
  249. package/skills/create-controller/SKILL.md +9 -9
  250. package/skills/send-response/SKILL.md +51 -37
  251. package/skills/store-file/SKILL.md +8 -3
  252. package/skills/upload-file/SKILL.md +9 -7
  253. package/skills/use-app-context/SKILL.md +2 -2
  254. package/skills/use-localization/SKILL.md +6 -2
  255. package/skills/use-repository/SKILL.md +2 -2
  256. package/skills/use-request-locals/SKILL.md +3 -3
  257. package/skills/validate-input/SKILL.md +2 -2
  258. package/skills/warlock-conventions/SKILL.md +2 -2
  259. package/skills/wire-socket/SKILL.md +2 -2
  260. package/skills/write-cli-command/SKILL.md +4 -1
  261. package/skills/write-middleware/SKILL.md +13 -15
@@ -1 +1 @@
1
- {"version":3,"file":"file-validator.mjs","names":[],"sources":["../../../../../../../../core/src/validation/validators/file-validator.ts"],"sourcesContent":["import type { FileSizeOption, JsonSchemaResult, JsonSchemaTarget } from \"@warlock.js/seal\";\nimport {\n BaseValidator,\n maxFileSizeRule,\n maxHeightRule,\n maxWidthRule,\n minFileSizeRule,\n minHeightRule,\n minWidthRule,\n resolveFileSize,\n v,\n} from \"@warlock.js/seal\";\nimport { UploadedFile } from \"../../http\";\nimport { fileExtensionRule, fileRule, fileTypeRule, imageRule } from \"../file\";\n\nexport const uploadedFileMetadataSchema = v.object({\n location: v.string().oneOf([\"local\", \"cloud\"]),\n width: v.int().positive(),\n height: v.int().positive(),\n size: v.int().positive(),\n mimeType: v.string(),\n extension: v.string(),\n name: v.string(),\n});\n\n/**\n * File validator class\n */\nexport class FileValidator extends BaseValidator {\n public constructor(errorMessage?: string) {\n super();\n this.addMutableRule(fileRule, errorMessage);\n }\n\n /**\n * Check if value is a File type\n */\n public matchesType(value: any): boolean {\n return value instanceof UploadedFile;\n }\n\n /** Value must be an image */\n public image(errorMessage?: string): FileValidator {\n return this.addRule(imageRule, errorMessage);\n }\n\n /** Accept specific file extensions */\n public accept(extensions: string | string[], errorMessage?: string): FileValidator {\n return this.addRule(fileExtensionRule, errorMessage, {\n extensions,\n });\n }\n\n /** Allow specific MIME types */\n public mimeType(mimeTypes: string | string[], errorMessage?: string): FileValidator {\n return this.addRule(fileTypeRule, errorMessage, {\n mimeTypes,\n });\n }\n\n /** Allow only pdf files */\n public pdf(errorMessage?: string): FileValidator {\n return this.mimeType(\"application/pdf\", errorMessage);\n }\n\n /** Allow only excel files */\n public excel(errorMessage?: string): FileValidator {\n return this.mimeType(\n [\n \"application/vnd.ms-excel\",\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n ],\n errorMessage,\n );\n }\n\n /** Allow only word files */\n public word(errorMessage?: string): FileValidator {\n return this.mimeType(\n [\n \"application/msword\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n ],\n errorMessage,\n );\n }\n\n /** Minimum file size */\n public minSize(size: number | FileSizeOption, errorMessage?: string): FileValidator {\n return this.addRule(minFileSizeRule, errorMessage, {\n minSize: resolveFileSize(size),\n });\n }\n\n /** @alias minSize */\n public min(size: number | FileSizeOption, errorMessage?: string): FileValidator {\n return this.minSize(size, errorMessage);\n }\n\n /** Maximum file size */\n public maxSize(size: number | FileSizeOption, errorMessage?: string): FileValidator {\n return this.addRule(maxFileSizeRule, errorMessage, {\n maxSize: resolveFileSize(size),\n });\n }\n\n /** @alias maxSize */\n public max(size: number, errorMessage?: string): FileValidator {\n return this.maxSize(size, errorMessage);\n }\n\n /** Minimum image width */\n public minWidth(width: number, errorMessage?: string): FileValidator {\n return this.addRule(minWidthRule, errorMessage, {\n minWidth: width,\n });\n }\n\n /** Maximum image width */\n public maxWidth(width: number, errorMessage?: string): FileValidator {\n return this.addRule(maxWidthRule, errorMessage, {\n maxWidth: width,\n });\n }\n\n /** Minimum image height */\n public minHeight(height: number, errorMessage?: string): FileValidator {\n return this.addRule(minHeightRule, errorMessage, {\n minHeight: height,\n });\n }\n\n /** Maximum image height */\n public maxHeight(height: number, errorMessage?: string): FileValidator {\n return this.addRule(maxHeightRule, errorMessage, {\n maxHeight: height,\n });\n }\n\n /**\n * Save the file and return it as a string\n */\n public saveTo(relativeDirectory: string): FileValidator {\n return this.addTransformer(async (file: UploadedFile) => {\n const output = await file.save(relativeDirectory);\n\n return output.path;\n });\n }\n\n /**\n * @inheritdoc\n *\n * File uploads are not natively representable in JSON Schema.\n * The output varies by target:\n * - `openapi-3.0` → `{ type: \"string\", format: \"binary\" }` (standard for multipart/form-data uploads)\n * - `draft-2020-12` → `{ type: \"string\", contentEncoding: \"binary\" }`\n * - `draft-07` → `{}` (no standard binary representation — permissive fallback)\n *\n * @example\n * ```ts\n * v.file().toJsonSchema(\"openapi-3.0\")\n * // → { type: \"string\", format: \"binary\" }\n * ```\n */\n public override toJsonSchema(target: JsonSchemaTarget = \"draft-2020-12\"): JsonSchemaResult {\n if (target === \"openapi-3.0\") {\n return { type: \"string\", format: \"binary\" };\n }\n\n if (target === \"draft-2020-12\") {\n return { type: \"string\", contentEncoding: \"binary\" };\n }\n\n // draft-07: no standard binary representation\n return {};\n }\n}\n"],"mappings":";;;;;;;AAeA,MAAa,6BAA6B,EAAE,OAAO;CACjD,UAAU,EAAE,OAAO,EAAE,MAAM,CAAC,SAAS,OAAO,CAAC;CAC7C,OAAO,EAAE,IAAI,EAAE,SAAS;CACxB,QAAQ,EAAE,IAAI,EAAE,SAAS;CACzB,MAAM,EAAE,IAAI,EAAE,SAAS;CACvB,UAAU,EAAE,OAAO;CACnB,WAAW,EAAE,OAAO;CACpB,MAAM,EAAE,OAAO;AACjB,CAAC;;;;AAKD,IAAa,gBAAb,cAAmC,cAAc;CAC/C,AAAO,YAAY,cAAuB;EACxC,MAAM;EACN,KAAK,eAAe,UAAU,YAAY;CAC5C;;;;CAKA,AAAO,YAAY,OAAqB;EACtC,OAAO,iBAAiB;CAC1B;;CAGA,AAAO,MAAM,cAAsC;EACjD,OAAO,KAAK,QAAQ,WAAW,YAAY;CAC7C;;CAGA,AAAO,OAAO,YAA+B,cAAsC;EACjF,OAAO,KAAK,QAAQ,mBAAmB,cAAc,EACnD,WACF,CAAC;CACH;;CAGA,AAAO,SAAS,WAA8B,cAAsC;EAClF,OAAO,KAAK,QAAQ,cAAc,cAAc,EAC9C,UACF,CAAC;CACH;;CAGA,AAAO,IAAI,cAAsC;EAC/C,OAAO,KAAK,SAAS,mBAAmB,YAAY;CACtD;;CAGA,AAAO,MAAM,cAAsC;EACjD,OAAO,KAAK,SACV,CACE,4BACA,mEACF,GACA,YACF;CACF;;CAGA,AAAO,KAAK,cAAsC;EAChD,OAAO,KAAK,SACV,CACE,sBACA,yEACF,GACA,YACF;CACF;;CAGA,AAAO,QAAQ,MAA+B,cAAsC;EAClF,OAAO,KAAK,QAAQ,iBAAiB,cAAc,EACjD,SAAS,gBAAgB,IAAI,EAC/B,CAAC;CACH;;CAGA,AAAO,IAAI,MAA+B,cAAsC;EAC9E,OAAO,KAAK,QAAQ,MAAM,YAAY;CACxC;;CAGA,AAAO,QAAQ,MAA+B,cAAsC;EAClF,OAAO,KAAK,QAAQ,iBAAiB,cAAc,EACjD,SAAS,gBAAgB,IAAI,EAC/B,CAAC;CACH;;CAGA,AAAO,IAAI,MAAc,cAAsC;EAC7D,OAAO,KAAK,QAAQ,MAAM,YAAY;CACxC;;CAGA,AAAO,SAAS,OAAe,cAAsC;EACnE,OAAO,KAAK,QAAQ,cAAc,cAAc,EAC9C,UAAU,MACZ,CAAC;CACH;;CAGA,AAAO,SAAS,OAAe,cAAsC;EACnE,OAAO,KAAK,QAAQ,cAAc,cAAc,EAC9C,UAAU,MACZ,CAAC;CACH;;CAGA,AAAO,UAAU,QAAgB,cAAsC;EACrE,OAAO,KAAK,QAAQ,eAAe,cAAc,EAC/C,WAAW,OACb,CAAC;CACH;;CAGA,AAAO,UAAU,QAAgB,cAAsC;EACrE,OAAO,KAAK,QAAQ,eAAe,cAAc,EAC/C,WAAW,OACb,CAAC;CACH;;;;CAKA,AAAO,OAAO,mBAA0C;EACtD,OAAO,KAAK,eAAe,OAAO,SAAuB;GAGvD,QAAO,MAFc,KAAK,KAAK,iBAAiB,GAElC;EAChB,CAAC;CACH;;;;;;;;;;;;;;;;CAiBA,AAAgB,aAAa,SAA2B,iBAAmC;EACzF,IAAI,WAAW,eACb,OAAO;GAAE,MAAM;GAAU,QAAQ;EAAS;EAG5C,IAAI,WAAW,iBACb,OAAO;GAAE,MAAM;GAAU,iBAAiB;EAAS;EAIrD,OAAO,CAAC;CACV;AACF"}
1
+ {"version":3,"file":"file-validator.mjs","names":[],"sources":["../../../../../../../../core/src/validation/validators/file-validator.ts"],"sourcesContent":["import type { FileSizeOption, JsonSchemaResult, JsonSchemaTarget } from \"@warlock.js/seal\";\nimport {\n BaseValidator,\n maxFileSizeRule,\n maxHeightRule,\n maxWidthRule,\n minFileSizeRule,\n minHeightRule,\n minWidthRule,\n resolveFileSize,\n v,\n} from \"@warlock.js/seal\";\nimport { UploadedFile } from \"../../http\";\nimport { fileExtensionRule, fileRule, fileTypeRule, imageRule } from \"../file\";\n\nexport const uploadedFileMetadataSchema = v.object({\n location: v.string().oneOf([\"local\", \"cloud\"]),\n width: v.int().positive(),\n height: v.int().positive(),\n size: v.int().positive(),\n mimeType: v.string(),\n extension: v.string(),\n name: v.string(),\n});\n\n/**\n * File validator class\n */\nexport class FileValidator extends BaseValidator {\n public constructor(errorMessage?: string) {\n super();\n this.addMutableRule(fileRule, errorMessage);\n }\n\n /**\n * Check if value is a File type\n */\n public matchesType(value: any): boolean {\n return value instanceof UploadedFile;\n }\n\n /** Value must be an image */\n public image(errorMessage?: string): FileValidator {\n return this.addRule(imageRule, errorMessage);\n }\n\n /** Accept specific file extensions */\n public accept(extensions: string | string[], errorMessage?: string): FileValidator {\n return this.addRule(fileExtensionRule, errorMessage, {\n extensions,\n });\n }\n\n /** Allow specific MIME types */\n public mimeType(mimeTypes: string | string[], errorMessage?: string): FileValidator {\n return this.addRule(fileTypeRule, errorMessage, {\n mimeTypes,\n });\n }\n\n /** Allow only pdf files */\n public pdf(errorMessage?: string): FileValidator {\n return this.mimeType(\"application/pdf\", errorMessage);\n }\n\n /** Allow only excel files */\n public excel(errorMessage?: string): FileValidator {\n return this.mimeType(\n [\n \"application/vnd.ms-excel\",\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n ],\n errorMessage,\n );\n }\n\n /** Allow only word files */\n public word(errorMessage?: string): FileValidator {\n return this.mimeType(\n [\n \"application/msword\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n ],\n errorMessage,\n );\n }\n\n /** Minimum file size */\n public minSize(size: number | FileSizeOption, errorMessage?: string): FileValidator {\n return this.addRule(minFileSizeRule, errorMessage, {\n minSize: resolveFileSize(size),\n });\n }\n\n /** @alias minSize */\n public min(size: number | FileSizeOption, errorMessage?: string): FileValidator {\n return this.minSize(size, errorMessage);\n }\n\n /** Maximum file size */\n public maxSize(size: number | FileSizeOption, errorMessage?: string): FileValidator {\n return this.addRule(maxFileSizeRule, errorMessage, {\n maxSize: resolveFileSize(size),\n });\n }\n\n /** @alias maxSize */\n public max(size: number, errorMessage?: string): FileValidator {\n return this.maxSize(size, errorMessage);\n }\n\n /** Minimum image width */\n public minWidth(width: number, errorMessage?: string): FileValidator {\n return this.addRule(minWidthRule, errorMessage, {\n minWidth: width,\n });\n }\n\n /** Maximum image width */\n public maxWidth(width: number, errorMessage?: string): FileValidator {\n return this.addRule(maxWidthRule, errorMessage, {\n maxWidth: width,\n });\n }\n\n /** Minimum image height */\n public minHeight(height: number, errorMessage?: string): FileValidator {\n return this.addRule(minHeightRule, errorMessage, {\n minHeight: height,\n });\n }\n\n /** Maximum image height */\n public maxHeight(height: number, errorMessage?: string): FileValidator {\n return this.addRule(maxHeightRule, errorMessage, {\n maxHeight: height,\n });\n }\n\n /**\n * Save the file and return it as a string\n */\n public saveTo(relativeDirectory: string): FileValidator {\n return this.addTransformer(async (file: UploadedFile) => {\n const output = await file.save(relativeDirectory);\n\n return output.path;\n });\n }\n\n /**\n * @inheritdoc\n *\n * File uploads are not natively representable in JSON Schema.\n * The output varies by target:\n * - `openapi-3.0` → `{ type: \"string\", format: \"binary\" }` (standard for multipart/form-data uploads)\n * - `draft-2020-12` → `{ type: \"string\", contentEncoding: \"binary\" }`\n * - `draft-07` → `{}` (no standard binary representation — permissive fallback)\n *\n * @example\n * ```ts\n * v.file().toJsonSchema(\"openapi-3.0\")\n * // → { type: \"string\", format: \"binary\" }\n * ```\n */\n public override toJsonSchema(target: JsonSchemaTarget = \"draft-2020-12\"): JsonSchemaResult {\n if (target === \"openapi-3.0\") {\n return { type: \"string\", format: \"binary\" };\n }\n\n if (target === \"draft-2020-12\") {\n return { type: \"string\", contentEncoding: \"binary\" };\n }\n\n // draft-07: no standard binary representation\n return {};\n }\n}\n"],"mappings":";;;;;;;AAeA,MAAa,6BAA6B,EAAE,OAAO;CACjD,UAAU,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,SAAS,OAAO,CAAC;CAC7C,OAAO,EAAE,IAAI,CAAC,CAAC,SAAS;CACxB,QAAQ,EAAE,IAAI,CAAC,CAAC,SAAS;CACzB,MAAM,EAAE,IAAI,CAAC,CAAC,SAAS;CACvB,UAAU,EAAE,OAAO;CACnB,WAAW,EAAE,OAAO;CACpB,MAAM,EAAE,OAAO;AACjB,CAAC;;;;AAKD,IAAa,gBAAb,cAAmC,cAAc;CAC/C,AAAO,YAAY,cAAuB;EACxC,MAAM;EACN,KAAK,eAAe,UAAU,YAAY;CAC5C;;;;CAKA,AAAO,YAAY,OAAqB;EACtC,OAAO,iBAAiB;CAC1B;;CAGA,AAAO,MAAM,cAAsC;EACjD,OAAO,KAAK,QAAQ,WAAW,YAAY;CAC7C;;CAGA,AAAO,OAAO,YAA+B,cAAsC;EACjF,OAAO,KAAK,QAAQ,mBAAmB,cAAc,EACnD,WACF,CAAC;CACH;;CAGA,AAAO,SAAS,WAA8B,cAAsC;EAClF,OAAO,KAAK,QAAQ,cAAc,cAAc,EAC9C,UACF,CAAC;CACH;;CAGA,AAAO,IAAI,cAAsC;EAC/C,OAAO,KAAK,SAAS,mBAAmB,YAAY;CACtD;;CAGA,AAAO,MAAM,cAAsC;EACjD,OAAO,KAAK,SACV,CACE,4BACA,mEACF,GACA,YACF;CACF;;CAGA,AAAO,KAAK,cAAsC;EAChD,OAAO,KAAK,SACV,CACE,sBACA,yEACF,GACA,YACF;CACF;;CAGA,AAAO,QAAQ,MAA+B,cAAsC;EAClF,OAAO,KAAK,QAAQ,iBAAiB,cAAc,EACjD,SAAS,gBAAgB,IAAI,EAC/B,CAAC;CACH;;CAGA,AAAO,IAAI,MAA+B,cAAsC;EAC9E,OAAO,KAAK,QAAQ,MAAM,YAAY;CACxC;;CAGA,AAAO,QAAQ,MAA+B,cAAsC;EAClF,OAAO,KAAK,QAAQ,iBAAiB,cAAc,EACjD,SAAS,gBAAgB,IAAI,EAC/B,CAAC;CACH;;CAGA,AAAO,IAAI,MAAc,cAAsC;EAC7D,OAAO,KAAK,QAAQ,MAAM,YAAY;CACxC;;CAGA,AAAO,SAAS,OAAe,cAAsC;EACnE,OAAO,KAAK,QAAQ,cAAc,cAAc,EAC9C,UAAU,MACZ,CAAC;CACH;;CAGA,AAAO,SAAS,OAAe,cAAsC;EACnE,OAAO,KAAK,QAAQ,cAAc,cAAc,EAC9C,UAAU,MACZ,CAAC;CACH;;CAGA,AAAO,UAAU,QAAgB,cAAsC;EACrE,OAAO,KAAK,QAAQ,eAAe,cAAc,EAC/C,WAAW,OACb,CAAC;CACH;;CAGA,AAAO,UAAU,QAAgB,cAAsC;EACrE,OAAO,KAAK,QAAQ,eAAe,cAAc,EAC/C,WAAW,OACb,CAAC;CACH;;;;CAKA,AAAO,OAAO,mBAA0C;EACtD,OAAO,KAAK,eAAe,OAAO,SAAuB;GAGvD,QAAO,MAFc,KAAK,KAAK,iBAAiB,EAEnC,CAAC;EAChB,CAAC;CACH;;;;;;;;;;;;;;;;CAiBA,AAAgB,aAAa,SAA2B,iBAAmC;EACzF,IAAI,WAAW,eACb,OAAO;GAAE,MAAM;GAAU,QAAQ;EAAS;EAG5C,IAAI,WAAW,iBACb,OAAO;GAAE,MAAM;GAAU,iBAAiB;EAAS;EAIrD,OAAO,CAAC;CACV;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"lower-stage3-decorators.d.mts","names":[],"sources":["../../../../../../../core/src/vite/lower-stage3-decorators.ts"],"mappings":";;AASA;;;;;;UAAiB,uBAAA;EACf,IAAA;EACA,OAAA;EACA,SAAA,CAAU,IAAA,UAAc,EAAA,WAAa,OAAO;IAAG,IAAA;IAAc,GAAA;EAAA;AAAA;AAoC/D;;;;AAAgE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAhE,iBAAgB,qBAAA,CAAA,GAAyB,uBAAuB"}
1
+ {"version":3,"file":"lower-stage3-decorators.d.mts","names":[],"sources":["../../../../../../../core/src/vite/lower-stage3-decorators.ts"],"mappings":";;AASA;;;;;;UAAiB,uBAAA;EACf,IAAA;EACA,OAAA;EACA,SAAA,CAAU,IAAA,UAAc,EAAA,WAAa,OAAO;IAAG,IAAA;IAAc,GAAA;EAAA;AAAA;AAoC/D;;;;AAAgE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAhE,iBAAgB,qBAAA,IAAyB,uBAAuB"}
@@ -1 +1 @@
1
- {"version":3,"file":"warlock-config.manager.d.mts","names":[],"sources":["../../../../../../../core/src/warlock-config/warlock-config.manager.ts"],"mappings":";;;;;AAeA;;;;AAAwD;AAaxD;iBAbgB,yBAAA,CAA0B,KAAc;;;;;;;cAa3C,oBAAA;EAyHR;;;EAAA,QArHK,MAAA;EAyIS;;;EAAA,QApIT,OAAA;EAqIL;;;;;EA9HU,IAAA,CAAA,GAAQ,OAAA,CAAQ,aAAA;EAPrB;;;;;;EAAA,QAgCM,MAAA;EA6EH;;;;;;;;EAAA,QAtCG,cAAA;EA2DR;;;;;;;;;;;EArBC,GAAA,mBAAsB,aAAA,CAAA,CAC3B,GAAA,EAAK,GAAA,EACL,YAAA,GAAe,aAAA,CAAc,GAAA,IAC5B,aAAA,CAAc,GAAA;EAsCV;;;;;AAWuB;AAoBhC;;;;EAnDQ,OAAA,mBAA0B,aAAA,CAAA,CAC9B,GAAA,EAAK,GAAA,EACL,YAAA,GAAe,aAAA,CAAc,GAAA,IAC5B,OAAA,CAAQ,aAAA,CAAc,GAAA;;;;MAQd,QAAA,CAAA;;;;;;EASJ,MAAA,CAAA,GAAU,aAAA;;;;EAWJ,MAAA,CAAA,GAAU,OAAA;AAAA;;;;;;;;;;;;;;cAoBZ,oBAAA,EAAoB,oBAA6B"}
1
+ {"version":3,"file":"warlock-config.manager.d.mts","names":[],"sources":["../../../../../../../core/src/warlock-config/warlock-config.manager.ts"],"mappings":";;;;;AAeA;;;;AAAwD;AAaxD;iBAbgB,yBAAA,CAA0B,KAAc;;;;;;;cAa3C,oBAAA;EAyHR;;;EAAA,QArHK,MAAA;EAyIS;;;EAAA,QApIT,OAAA;EAqIL;;;;;EA9HU,IAAA,IAAQ,OAAA,CAAQ,aAAA;EAPrB;;;;;;EAAA,QAgCM,MAAA;EA6EH;;;;;;;;EAAA,QAtCG,cAAA;EA2DR;;;;;;;;;;;EArBC,GAAA,mBAAsB,aAAA,EAC3B,GAAA,EAAK,GAAA,EACL,YAAA,GAAe,aAAA,CAAc,GAAA,IAC5B,aAAA,CAAc,GAAA;EAsCV;;;;;AAWuB;AAoBhC;;;;EAnDQ,OAAA,mBAA0B,aAAA,EAC9B,GAAA,EAAK,GAAA,EACL,YAAA,GAAe,aAAA,CAAc,GAAA,IAC5B,OAAA,CAAQ,aAAA,CAAc,GAAA;;;;MAQd,QAAA;;;;;;EASJ,MAAA,IAAU,aAAA;;;;EAWJ,MAAA,IAAU,OAAA;AAAA;;;;;;;;;;;;;;cAoBZ,oBAAA,EAAoB,oBAA6B"}
@@ -1 +1 @@
1
- {"version":3,"file":"warlock-config.manager.mjs","names":[],"sources":["../../../../../../../core/src/warlock-config/warlock-config.manager.ts"],"sourcesContent":["import { fileExistsAsync } from \"@warlock.js/fs\";\r\nimport { get } from \"@mongez/reinforcements\";\r\nimport { readFile, unlink, writeFile } from \"node:fs/promises\";\r\nimport { pathToFileURL } from \"url\";\r\nimport { rootPath } from \"../utils\";\r\nimport { type WarlockConfig } from \"./types\";\r\n\r\n/**\r\n * True when `error` is Node's \"this runtime can't import a `.ts` file\" error.\r\n *\r\n * Node only executes TypeScript natively from v22.18 / v23.6 onward (earlier\r\n * 22.x needs `--experimental-strip-types`). On older runtimes a bare\r\n * `import(\"warlock.config.ts\")` throws `ERR_UNKNOWN_FILE_EXTENSION` — which is\r\n * the signal to fall back to transpiling the config ourselves.\r\n */\r\nexport function isUnknownTsExtensionError(error: unknown): boolean {\r\n return (\r\n (error as { code?: string })?.code === \"ERR_UNKNOWN_FILE_EXTENSION\" ||\r\n /Unknown file extension \"\\.tsx?\"/.test(String((error as Error)?.message))\r\n );\r\n}\r\n\r\n/**\r\n * Warlock Config Manager\r\n *\r\n * Manages lazy loading of the pre-compiled warlock.config.js file\r\n * from the .warlock/cache directory.\r\n */\r\nexport class WarlockConfigManager {\r\n /**\r\n * Cached config instance\r\n */\r\n private config?: WarlockConfig;\r\n\r\n /**\r\n * Loading promise to prevent duplicate loads\r\n */\r\n private loading?: Promise<WarlockConfig | undefined>;\r\n\r\n /**\r\n * Load warlock.config.js (cached after first load)\r\n *\r\n * @returns The resolved Warlock configuration\r\n */\r\n public async load(): Promise<WarlockConfig | undefined> {\r\n // Already loaded\r\n if (this.config) {\r\n return this.config;\r\n }\r\n\r\n // Currently loading (prevent duplicate loads)\r\n if (this.loading) {\r\n return this.loading;\r\n }\r\n\r\n // Start loading\r\n this.loading = this.doLoad();\r\n this.config = await this.loading;\r\n this.loading = undefined;\r\n\r\n return this.config;\r\n }\r\n\r\n /**\r\n * Internal load implementation\r\n *\r\n * The ESM loader hook transpiles `warlock.config.ts` on import — no\r\n * separate compile-to-disk step is needed.\r\n */\r\n private async doLoad(): Promise<WarlockConfig | undefined> {\r\n const configPath = rootPath(\"warlock.config.ts\");\r\n\r\n if (!(await fileExistsAsync(configPath))) {\r\n // Written straight to the console, NOT through `@warlock.js/logger`.\r\n // This runs during CLI bootstrap, before `LoggerConnector.start()` has\r\n // configured a single channel, so a logger call here reaches nobody in\r\n // any application — the warning would be silently dropped exactly when\r\n // the user most needs it.\r\n console.warn(\"warlock.config.ts is missing — run `warlock init` to create it\");\r\n return;\r\n }\r\n\r\n try {\r\n const configModule = await import(pathToFileURL(configPath).href);\r\n return configModule.default;\r\n } catch (error) {\r\n // `dev` registers a TS loader hook, and Node ≥ 22.18 / 23.6 strips types\r\n // natively — in both cases the direct import above works. But `build` /\r\n // `start` register no hook, so on an older Node the import throws\r\n // ERR_UNKNOWN_FILE_EXTENSION. Transpile the config with esbuild ourselves\r\n // so config loading is Node-version-independent rather than relying on an\r\n // experimental runtime feature.\r\n if (isUnknownTsExtensionError(error)) {\r\n return await this.loadViaEsbuild(configPath);\r\n }\r\n\r\n throw new Error(`Failed to load warlock.config.ts: ${error}`);\r\n }\r\n }\r\n\r\n /**\r\n * Fallback config loader for runtimes without native TypeScript support.\r\n *\r\n * Transpiles `warlock.config.ts` with esbuild (already a core dependency),\r\n * writes the result as a sibling `.mjs` — so the config's own imports (bare\r\n * `@warlock.js/*` and any relative paths) resolve exactly as they would from\r\n * the original location — imports it, then removes the temp file.\r\n */\r\n private async loadViaEsbuild(configPath: string): Promise<WarlockConfig | undefined> {\r\n const { transformSync } = await import(\"esbuild\");\r\n const source = await readFile(configPath, \"utf8\");\r\n\r\n const { code } = transformSync(source, {\r\n loader: \"ts\",\r\n format: \"esm\",\r\n target: `node${process.versions.node.split(\".\")[0]}`,\r\n sourcefile: configPath,\r\n });\r\n\r\n // Unique sibling name so concurrent processes never clobber each other.\r\n const compiledPath = configPath.replace(\r\n /warlock\\.config\\.ts$/,\r\n `.warlock.config.${process.pid}.mjs`,\r\n );\r\n\r\n await writeFile(compiledPath, code, \"utf8\");\r\n\r\n try {\r\n const configModule = await import(pathToFileURL(compiledPath).href);\r\n return configModule.default;\r\n } finally {\r\n await unlink(compiledPath).catch(() => {});\r\n }\r\n }\r\n\r\n /**\r\n * Get config value by key (dot notation supported)\r\n *\r\n * @example\r\n * config.get(\"server.port\") // Returns 3000\r\n * config.get(\"cli.commands\") // Returns array of commands\r\n *\r\n * @param key - Config key (supports dot notation), autocompletes for first level only\r\n * @returns The config value\r\n * @throws Error if config is not loaded\r\n */\r\n public get<Key extends keyof WarlockConfig>(\r\n key: Key,\r\n defaultValue?: WarlockConfig[Key],\r\n ): WarlockConfig[Key] {\r\n if (!this.config) {\r\n throw new Error(\"WarlockConfig not loaded. Call load() first or use lazyGet().\");\r\n }\r\n\r\n return get(this.config, key as string, defaultValue);\r\n }\r\n\r\n /**\r\n * Lazy get - loads config if not already loaded\r\n *\r\n * @example\r\n * const port = await config.lazyGet(\"server\");\r\n *\r\n * @param key - Config key (supports dot notation), autocompletes for first level only\r\n * @param defaultValue - Default value if config key is undefined\r\n * @returns The config value\r\n */\r\n async lazyGet<Key extends keyof WarlockConfig>(\r\n key: Key,\r\n defaultValue?: WarlockConfig[Key],\r\n ): Promise<WarlockConfig[Key]> {\r\n await this.load();\r\n return this.get(key, defaultValue);\r\n }\r\n\r\n /**\r\n * Check if config is loaded\r\n */\r\n public get isLoaded(): boolean {\r\n return this.config !== undefined;\r\n }\r\n\r\n /**\r\n * Get the entire config object\r\n *\r\n * @throws Error if config is not loaded\r\n */\r\n public getAll(): WarlockConfig {\r\n if (!this.config) {\r\n throw new Error(\"WarlockConfig not loaded. Call load() first or use lazyGet().\");\r\n }\r\n\r\n return this.config;\r\n }\r\n\r\n /**\r\n * Reload config (useful for HMR/development)\r\n */\r\n public async reload(): Promise<void> {\r\n this.config = undefined;\r\n this.loading = undefined;\r\n await this.load();\r\n }\r\n}\r\n\r\n/**\r\n * Exported singleton instance\r\n *\r\n * @example\r\n * import { warlockConfig } from \"@warlock.js/core\";\r\n *\r\n * // Lazy load and get value\r\n * const port = await warlockConfig.lazyGet(\"server.port\");\r\n *\r\n * // Or load first, then get\r\n * await warlockConfig.load();\r\n * const commands = warlockConfig.get(\"cli.commands\");\r\n */\r\nexport const warlockConfigManager = new WarlockConfigManager();\r\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,SAAgB,0BAA0B,OAAyB;CACjE,OACG,OAA6B,SAAS,gCACvC,kCAAkC,KAAK,OAAQ,OAAiB,OAAO,CAAC;AAE5E;;;;;;;AAQA,IAAa,uBAAb,MAAkC;;;;;;CAgBhC,MAAa,OAA2C;EAEtD,IAAI,KAAK,QACP,OAAO,KAAK;EAId,IAAI,KAAK,SACP,OAAO,KAAK;EAId,KAAK,UAAU,KAAK,OAAO;EAC3B,KAAK,SAAS,MAAM,KAAK;EACzB,KAAK,UAAU;EAEf,OAAO,KAAK;CACd;;;;;;;CAQA,MAAc,SAA6C;EACzD,MAAM,aAAa,SAAS,mBAAmB;EAE/C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;GAMxC,QAAQ,KAAK,gEAAgE;GAC7E;EACF;EAEA,IAAI;GAEF,QAAO,MADoB,OAAO,cAAc,UAAU,EAAE,OACxC;EACtB,SAAS,OAAO;GAOd,IAAI,0BAA0B,KAAK,GACjC,OAAO,MAAM,KAAK,eAAe,UAAU;GAG7C,MAAM,IAAI,MAAM,qCAAqC,OAAO;EAC9D;CACF;;;;;;;;;CAUA,MAAc,eAAe,YAAwD;EACnF,MAAM,EAAE,kBAAkB,MAAM,OAAO;EAGvC,MAAM,EAAE,SAAS,cAAc,MAFV,SAAS,YAAY,MAAM,GAET;GACrC,QAAQ;GACR,QAAQ;GACR,QAAQ,OAAO,QAAQ,SAAS,KAAK,MAAM,GAAG,EAAE;GAChD,YAAY;EACd,CAAC;EAGD,MAAM,eAAe,WAAW,QAC9B,wBACA,mBAAmB,QAAQ,IAAI,KACjC;EAEA,MAAM,UAAU,cAAc,MAAM,MAAM;EAE1C,IAAI;GAEF,QAAO,MADoB,OAAO,cAAc,YAAY,EAAE,OAC1C;EACtB,UAAU;GACR,MAAM,OAAO,YAAY,EAAE,YAAY,CAAC,CAAC;EAC3C;CACF;;;;;;;;;;;;CAaA,AAAO,IACL,KACA,cACoB;EACpB,IAAI,CAAC,KAAK,QACR,MAAM,IAAI,MAAM,+DAA+D;EAGjF,OAAO,IAAI,KAAK,QAAQ,KAAe,YAAY;CACrD;;;;;;;;;;;CAYA,MAAM,QACJ,KACA,cAC6B;EAC7B,MAAM,KAAK,KAAK;EAChB,OAAO,KAAK,IAAI,KAAK,YAAY;CACnC;;;;CAKA,IAAW,WAAoB;EAC7B,OAAO,KAAK,WAAW;CACzB;;;;;;CAOA,AAAO,SAAwB;EAC7B,IAAI,CAAC,KAAK,QACR,MAAM,IAAI,MAAM,+DAA+D;EAGjF,OAAO,KAAK;CACd;;;;CAKA,MAAa,SAAwB;EACnC,KAAK,SAAS;EACd,KAAK,UAAU;EACf,MAAM,KAAK,KAAK;CAClB;AACF;;;;;;;;;;;;;;AAeA,MAAa,uBAAuB,IAAI,qBAAqB"}
1
+ {"version":3,"file":"warlock-config.manager.mjs","names":[],"sources":["../../../../../../../core/src/warlock-config/warlock-config.manager.ts"],"sourcesContent":["import { fileExistsAsync } from \"@warlock.js/fs\";\r\nimport { get } from \"@mongez/reinforcements\";\r\nimport { readFile, unlink, writeFile } from \"node:fs/promises\";\r\nimport { pathToFileURL } from \"url\";\r\nimport { rootPath } from \"../utils\";\r\nimport { type WarlockConfig } from \"./types\";\r\n\r\n/**\r\n * True when `error` is Node's \"this runtime can't import a `.ts` file\" error.\r\n *\r\n * Node only executes TypeScript natively from v22.18 / v23.6 onward (earlier\r\n * 22.x needs `--experimental-strip-types`). On older runtimes a bare\r\n * `import(\"warlock.config.ts\")` throws `ERR_UNKNOWN_FILE_EXTENSION` — which is\r\n * the signal to fall back to transpiling the config ourselves.\r\n */\r\nexport function isUnknownTsExtensionError(error: unknown): boolean {\r\n return (\r\n (error as { code?: string })?.code === \"ERR_UNKNOWN_FILE_EXTENSION\" ||\r\n /Unknown file extension \"\\.tsx?\"/.test(String((error as Error)?.message))\r\n );\r\n}\r\n\r\n/**\r\n * Warlock Config Manager\r\n *\r\n * Manages lazy loading of the pre-compiled warlock.config.js file\r\n * from the .warlock/cache directory.\r\n */\r\nexport class WarlockConfigManager {\r\n /**\r\n * Cached config instance\r\n */\r\n private config?: WarlockConfig;\r\n\r\n /**\r\n * Loading promise to prevent duplicate loads\r\n */\r\n private loading?: Promise<WarlockConfig | undefined>;\r\n\r\n /**\r\n * Load warlock.config.js (cached after first load)\r\n *\r\n * @returns The resolved Warlock configuration\r\n */\r\n public async load(): Promise<WarlockConfig | undefined> {\r\n // Already loaded\r\n if (this.config) {\r\n return this.config;\r\n }\r\n\r\n // Currently loading (prevent duplicate loads)\r\n if (this.loading) {\r\n return this.loading;\r\n }\r\n\r\n // Start loading\r\n this.loading = this.doLoad();\r\n this.config = await this.loading;\r\n this.loading = undefined;\r\n\r\n return this.config;\r\n }\r\n\r\n /**\r\n * Internal load implementation\r\n *\r\n * The ESM loader hook transpiles `warlock.config.ts` on import — no\r\n * separate compile-to-disk step is needed.\r\n */\r\n private async doLoad(): Promise<WarlockConfig | undefined> {\r\n const configPath = rootPath(\"warlock.config.ts\");\r\n\r\n if (!(await fileExistsAsync(configPath))) {\r\n // Written straight to the console, NOT through `@warlock.js/logger`.\r\n // This runs during CLI bootstrap, before `LoggerConnector.start()` has\r\n // configured a single channel, so a logger call here reaches nobody in\r\n // any application — the warning would be silently dropped exactly when\r\n // the user most needs it.\r\n console.warn(\"warlock.config.ts is missing — run `warlock init` to create it\");\r\n return;\r\n }\r\n\r\n try {\r\n const configModule = await import(pathToFileURL(configPath).href);\r\n return configModule.default;\r\n } catch (error) {\r\n // `dev` registers a TS loader hook, and Node ≥ 22.18 / 23.6 strips types\r\n // natively — in both cases the direct import above works. But `build` /\r\n // `start` register no hook, so on an older Node the import throws\r\n // ERR_UNKNOWN_FILE_EXTENSION. Transpile the config with esbuild ourselves\r\n // so config loading is Node-version-independent rather than relying on an\r\n // experimental runtime feature.\r\n if (isUnknownTsExtensionError(error)) {\r\n return await this.loadViaEsbuild(configPath);\r\n }\r\n\r\n throw new Error(`Failed to load warlock.config.ts: ${error}`);\r\n }\r\n }\r\n\r\n /**\r\n * Fallback config loader for runtimes without native TypeScript support.\r\n *\r\n * Transpiles `warlock.config.ts` with esbuild (already a core dependency),\r\n * writes the result as a sibling `.mjs` — so the config's own imports (bare\r\n * `@warlock.js/*` and any relative paths) resolve exactly as they would from\r\n * the original location — imports it, then removes the temp file.\r\n */\r\n private async loadViaEsbuild(configPath: string): Promise<WarlockConfig | undefined> {\r\n const { transformSync } = await import(\"esbuild\");\r\n const source = await readFile(configPath, \"utf8\");\r\n\r\n const { code } = transformSync(source, {\r\n loader: \"ts\",\r\n format: \"esm\",\r\n target: `node${process.versions.node.split(\".\")[0]}`,\r\n sourcefile: configPath,\r\n });\r\n\r\n // Unique sibling name so concurrent processes never clobber each other.\r\n const compiledPath = configPath.replace(\r\n /warlock\\.config\\.ts$/,\r\n `.warlock.config.${process.pid}.mjs`,\r\n );\r\n\r\n await writeFile(compiledPath, code, \"utf8\");\r\n\r\n try {\r\n const configModule = await import(pathToFileURL(compiledPath).href);\r\n return configModule.default;\r\n } finally {\r\n await unlink(compiledPath).catch(() => {});\r\n }\r\n }\r\n\r\n /**\r\n * Get config value by key (dot notation supported)\r\n *\r\n * @example\r\n * config.get(\"server.port\") // Returns 3000\r\n * config.get(\"cli.commands\") // Returns array of commands\r\n *\r\n * @param key - Config key (supports dot notation), autocompletes for first level only\r\n * @returns The config value\r\n * @throws Error if config is not loaded\r\n */\r\n public get<Key extends keyof WarlockConfig>(\r\n key: Key,\r\n defaultValue?: WarlockConfig[Key],\r\n ): WarlockConfig[Key] {\r\n if (!this.config) {\r\n throw new Error(\"WarlockConfig not loaded. Call load() first or use lazyGet().\");\r\n }\r\n\r\n return get(this.config, key as string, defaultValue);\r\n }\r\n\r\n /**\r\n * Lazy get - loads config if not already loaded\r\n *\r\n * @example\r\n * const port = await config.lazyGet(\"server\");\r\n *\r\n * @param key - Config key (supports dot notation), autocompletes for first level only\r\n * @param defaultValue - Default value if config key is undefined\r\n * @returns The config value\r\n */\r\n async lazyGet<Key extends keyof WarlockConfig>(\r\n key: Key,\r\n defaultValue?: WarlockConfig[Key],\r\n ): Promise<WarlockConfig[Key]> {\r\n await this.load();\r\n return this.get(key, defaultValue);\r\n }\r\n\r\n /**\r\n * Check if config is loaded\r\n */\r\n public get isLoaded(): boolean {\r\n return this.config !== undefined;\r\n }\r\n\r\n /**\r\n * Get the entire config object\r\n *\r\n * @throws Error if config is not loaded\r\n */\r\n public getAll(): WarlockConfig {\r\n if (!this.config) {\r\n throw new Error(\"WarlockConfig not loaded. Call load() first or use lazyGet().\");\r\n }\r\n\r\n return this.config;\r\n }\r\n\r\n /**\r\n * Reload config (useful for HMR/development)\r\n */\r\n public async reload(): Promise<void> {\r\n this.config = undefined;\r\n this.loading = undefined;\r\n await this.load();\r\n }\r\n}\r\n\r\n/**\r\n * Exported singleton instance\r\n *\r\n * @example\r\n * import { warlockConfig } from \"@warlock.js/core\";\r\n *\r\n * // Lazy load and get value\r\n * const port = await warlockConfig.lazyGet(\"server.port\");\r\n *\r\n * // Or load first, then get\r\n * await warlockConfig.load();\r\n * const commands = warlockConfig.get(\"cli.commands\");\r\n */\r\nexport const warlockConfigManager = new WarlockConfigManager();\r\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,SAAgB,0BAA0B,OAAyB;CACjE,OACG,OAA6B,SAAS,gCACvC,kCAAkC,KAAK,OAAQ,OAAiB,OAAO,CAAC;AAE5E;;;;;;;AAQA,IAAa,uBAAb,MAAkC;;;;;;CAgBhC,MAAa,OAA2C;EAEtD,IAAI,KAAK,QACP,OAAO,KAAK;EAId,IAAI,KAAK,SACP,OAAO,KAAK;EAId,KAAK,UAAU,KAAK,OAAO;EAC3B,KAAK,SAAS,MAAM,KAAK;EACzB,KAAK,UAAU;EAEf,OAAO,KAAK;CACd;;;;;;;CAQA,MAAc,SAA6C;EACzD,MAAM,aAAa,SAAS,mBAAmB;EAE/C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;GAMxC,QAAQ,KAAK,gEAAgE;GAC7E;EACF;EAEA,IAAI;GAEF,QAAO,MADoB,OAAO,cAAc,UAAU,CAAC,CAAC,MACzC,CAAC;EACtB,SAAS,OAAO;GAOd,IAAI,0BAA0B,KAAK,GACjC,OAAO,MAAM,KAAK,eAAe,UAAU;GAG7C,MAAM,IAAI,MAAM,qCAAqC,OAAO;EAC9D;CACF;;;;;;;;;CAUA,MAAc,eAAe,YAAwD;EACnF,MAAM,EAAE,kBAAkB,MAAM,OAAO;EAGvC,MAAM,EAAE,SAAS,cAAc,MAFV,SAAS,YAAY,MAAM,GAET;GACrC,QAAQ;GACR,QAAQ;GACR,QAAQ,OAAO,QAAQ,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC;GAChD,YAAY;EACd,CAAC;EAGD,MAAM,eAAe,WAAW,QAC9B,wBACA,mBAAmB,QAAQ,IAAI,KACjC;EAEA,MAAM,UAAU,cAAc,MAAM,MAAM;EAE1C,IAAI;GAEF,QAAO,MADoB,OAAO,cAAc,YAAY,CAAC,CAAC,MAC3C,CAAC;EACtB,UAAU;GACR,MAAM,OAAO,YAAY,CAAC,CAAC,YAAY,CAAC,CAAC;EAC3C;CACF;;;;;;;;;;;;CAaA,AAAO,IACL,KACA,cACoB;EACpB,IAAI,CAAC,KAAK,QACR,MAAM,IAAI,MAAM,+DAA+D;EAGjF,OAAO,IAAI,KAAK,QAAQ,KAAe,YAAY;CACrD;;;;;;;;;;;CAYA,MAAM,QACJ,KACA,cAC6B;EAC7B,MAAM,KAAK,KAAK;EAChB,OAAO,KAAK,IAAI,KAAK,YAAY;CACnC;;;;CAKA,IAAW,WAAoB;EAC7B,OAAO,KAAK,WAAW;CACzB;;;;;;CAOA,AAAO,SAAwB;EAC7B,IAAI,CAAC,KAAK,QACR,MAAM,IAAI,MAAM,+DAA+D;EAGjF,OAAO,KAAK;CACd;;;;CAKA,MAAa,SAAwB;EACnC,KAAK,SAAS;EACd,KAAK,UAAU;EACf,MAAM,KAAK,KAAK;CAClB;AACF;;;;;;;;;;;;;;AAeA,MAAa,uBAAuB,IAAI,qBAAqB"}
package/llms-full.txt CHANGED
@@ -1222,7 +1222,7 @@ A controller is a thin function: pull inputs from `request`, call work, return t
1222
1222
  ```ts title="src/app/<module>/controllers/<action>.controller.ts"
1223
1223
  import { type RequestHandler } from "@warlock.js/core";
1224
1224
 
1225
- export const listProductsController: RequestHandler = async (request, response) => {
1225
+ export const listProductsController: RequestHandler = async ({ request, response }) => {
1226
1226
  return response.success({ products: [] });
1227
1227
  };
1228
1228
  ```
@@ -1277,10 +1277,10 @@ import { type Request, type RequestHandler } from "@warlock.js/core";
1277
1277
  import { type CreateProductSchema, createProductSchema } from "../schema/create-product.schema";
1278
1278
  import { createProductService } from "../services/create-product.service";
1279
1279
 
1280
- export const createProductController: RequestHandler<Request<CreateProductSchema>> = async (
1280
+ export const createProductController: RequestHandler<Request<CreateProductSchema>> = async ({
1281
1281
  request,
1282
1282
  response,
1283
- ) => {
1283
+ }) => {
1284
1284
  const product = await createProductService(request.validated());
1285
1285
 
1286
1286
  return response.successCreate({ product });
@@ -1306,10 +1306,10 @@ Routes behind `authMiddleware` need `request.user` typed. Project conventions ad
1306
1306
  import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
1307
1307
  import { type CreateProductSchema, createProductSchema } from "../schema/create-product.schema";
1308
1308
 
1309
- export const createProductController: GuardedRequestHandler<CreateProductSchema> = async (
1309
+ export const createProductController: GuardedRequestHandler<CreateProductSchema> = async ({
1310
1310
  request,
1311
1311
  response,
1312
- ) => {
1312
+ }) => {
1313
1313
  // request.user is typed
1314
1314
  const product = await createProductService(request.validated());
1315
1315
  return response.successCreate({ product });
@@ -1356,7 +1356,7 @@ If your controller is over ~30 lines, the work probably belongs in a service.
1356
1356
  import { type RequestHandler } from "@warlock.js/core";
1357
1357
  import { listProductsService } from "../services/list-products.service";
1358
1358
 
1359
- export const listProductsController: RequestHandler = async (request, response) => {
1359
+ export const listProductsController: RequestHandler = async ({ request, response }) => {
1360
1360
  const { data: products, pagination } = await listProductsService({
1361
1361
  ...request.all(),
1362
1362
  organization_id: request.user.organizationId,
@@ -1373,10 +1373,10 @@ import { type Request, type RequestHandler } from "@warlock.js/core";
1373
1373
  import { type CreateProductSchema, createProductSchema } from "../schema/create-product.schema";
1374
1374
  import { createProductService } from "../services/create-product.service";
1375
1375
 
1376
- export const createProductController: RequestHandler<Request<CreateProductSchema>> = async (
1376
+ export const createProductController: RequestHandler<Request<CreateProductSchema>> = async ({
1377
1377
  request,
1378
1378
  response,
1379
- ) => {
1379
+ }) => {
1380
1380
  const product = await createProductService(request.validated());
1381
1381
 
1382
1382
  return response.successCreate({ product });
@@ -1410,7 +1410,7 @@ export async function getProductService(id: string) {
1410
1410
  import type { RequestHandler } from "@warlock.js/core";
1411
1411
  import { getProductService } from "../services/get-product.service";
1412
1412
 
1413
- export const getProductController: RequestHandler = async (request, response) => {
1413
+ export const getProductController: RequestHandler = async ({ request, response }) => {
1414
1414
  const product = await getProductService(request.input("id"));
1415
1415
 
1416
1416
  return response.success({ product });
@@ -4133,9 +4133,9 @@ description: 'Send HTTP responses via @warlock.js/core''s Response helpers — s
4133
4133
  ## The shape
4134
4134
 
4135
4135
  ```ts
4136
- import type { RequestHandler, Response } from "@warlock.js/core";
4136
+ import type { RequestHandler } from "@warlock.js/core";
4137
4137
 
4138
- export const myController: RequestHandler = async (request, response: Response) => {
4138
+ export const myController: RequestHandler = async ({ request, response }) => {
4139
4139
  // …choose a helper and return it
4140
4140
  return response.success({ data: "…" });
4141
4141
  };
@@ -4145,11 +4145,11 @@ Always `return response.<helper>(...)`. The return value drives Fastify's send.
4145
4145
 
4146
4146
  ## Success helpers
4147
4147
 
4148
- | Method | Status | When |
4149
- | ----------------------------------- | ------ | ----------------------------------------- |
4150
- | `response.success(data?)` | 200 | normal read / update |
4151
- | `response.successCreate(data)` | 201 | resource created (POST) |
4152
- | `response.noContent()` | 204 | delete succeeded, no body needed |
4148
+ | Method | Status | When |
4149
+ | ------------------------------ | ------ | -------------------------------- |
4150
+ | `response.success(data?)` | 200 | normal read / update |
4151
+ | `response.successCreate(data)` | 201 | resource created (POST) |
4152
+ | `response.noContent()` | 204 | delete succeeded, no body needed |
4153
4153
 
4154
4154
  ```ts
4155
4155
  return response.success({ products: [...] });
@@ -4163,13 +4163,13 @@ return response.noContent();
4163
4163
 
4164
4164
  ## Client-error helpers
4165
4165
 
4166
- | Method | Status | When |
4167
- | --------------------------------------------------- | ------ | ------------------------------------- |
4168
- | `response.badRequest(data)` | 400 | malformed or invalid input |
4169
- | `response.unauthorized(data?)` | 401 | missing/invalid auth token |
4170
- | `response.forbidden(data?)` | 403 | authenticated but not allowed |
4171
- | `response.notFound(data?)` | 404 | record missing |
4172
- | `response.conflict(data?)` | 409 | uniqueness violation, state conflict |
4166
+ | Method | Status | When |
4167
+ | ------------------------------ | ------ | ------------------------------------ |
4168
+ | `response.badRequest(data)` | 400 | malformed or invalid input |
4169
+ | `response.unauthorized(data?)` | 401 | missing/invalid auth token |
4170
+ | `response.forbidden(data?)` | 403 | authenticated but not allowed |
4171
+ | `response.notFound(data?)` | 404 | record missing |
4172
+ | `response.conflict(data?)` | 409 | uniqueness violation, state conflict |
4173
4173
 
4174
4174
  ```ts
4175
4175
  return response.badRequest({ error: t("validation.invalid") });
@@ -4188,8 +4188,8 @@ Most error helpers accept an optional payload — if you omit it, they send a de
4188
4188
  ## Redirects
4189
4189
 
4190
4190
  ```ts
4191
- return response.redirect("/login"); // 302
4192
- return response.redirect("/new-home", 301); // permanent
4191
+ return response.redirect("/login"); // 302
4192
+ return response.redirect("/new-home", 301); // permanent
4193
4193
  ```
4194
4194
 
4195
4195
  ## Files
@@ -4221,19 +4221,19 @@ stream.end();
4221
4221
 
4222
4222
  ## Throwing HTTP errors
4223
4223
 
4224
- Most of the time, controllers don't need to *choose* an error helper — they throw from the service layer instead. The request middleware (`http/middleware/inject-request-context.ts`) catches every `HttpError` subclass and produces the matching response. The error classes mirror the helpers above:
4224
+ Most of the time, controllers don't need to _choose_ an error helper — they throw from the service layer instead. The request middleware (`http/middleware/inject-request-context.ts`) catches every `HttpError` subclass and produces the matching response. The error classes mirror the helpers above:
4225
4225
 
4226
4226
  ```ts
4227
4227
  import {
4228
- ResourceNotFoundError, // 404
4229
- UnAuthorizedError, // 401
4230
- ForbiddenError, // 403
4231
- BadRequestError, // 400
4232
- ConflictError, // 409
4233
- NotAcceptableError, // 406
4234
- NotAllowedError, // 405
4235
- ServerError, // 500
4236
- HttpError, // base class — `new HttpError(status, message, payload?)` for arbitrary codes
4228
+ ResourceNotFoundError, // 404
4229
+ UnAuthorizedError, // 401
4230
+ ForbiddenError, // 403
4231
+ BadRequestError, // 400
4232
+ ConflictError, // 409
4233
+ NotAcceptableError, // 406
4234
+ NotAllowedError, // 405
4235
+ ServerError, // 500
4236
+ HttpError, // base class — `new HttpError(status, message, payload?)` for arbitrary codes
4237
4237
  } from "@warlock.js/core";
4238
4238
 
4239
4239
  throw new ResourceNotFoundError("product.notFound");
@@ -4241,13 +4241,27 @@ throw new ForbiddenError("permission.denied", { resource: "product", id });
4241
4241
  throw new ConflictError("user.duplicateEmail");
4242
4242
  ```
4243
4243
 
4244
- Each class takes `(message, payload?)`. The payload merges into the response body alongside `error`. In development mode, the stack trace is included too.
4244
+ Each class takes `(message, payload?)`. The framework keeps the optional
4245
+ detail nested under `payload`:
4246
+
4247
+ ```json
4248
+ {
4249
+ "error": "Product not found",
4250
+ "payload": { "id": 42 }
4251
+ }
4252
+ ```
4253
+
4254
+ Every unhandled error response also carries
4255
+ `Cache-Control: private, no-store`, regardless of status. The floor is
4256
+ applied once at the shared error funnel, so API-route errors cannot be stored
4257
+ and replayed across users. In development mode, the stack trace is included
4258
+ too.
4245
4259
 
4246
4260
  Pick the class, throw from the service or use-case, and forget about response shaping at the call site. The controller stays focused on the success path:
4247
4261
 
4248
4262
  ```ts
4249
- export const getProductController: RequestHandler = async (request, response) => {
4250
- const product = await getProductService(request.input("id")); // throws ResourceNotFoundError on miss
4263
+ export const getProductController: RequestHandler = async ({ request, response }) => {
4264
+ const product = await getProductService(request.input("id")); // throws ResourceNotFoundError on miss
4251
4265
  return response.success({ product });
4252
4266
  };
4253
4267
  ```
@@ -4259,9 +4273,9 @@ See [`create-controller`](../create-controller/SKILL.md) for the "throw from ser
4259
4273
  ```ts
4260
4274
  const sse = response.sse();
4261
4275
 
4262
- sse.send("tick", { count: 1 }); // event name, data, optional id
4263
- sse.send("tick", { count: 2 }, "msg-2"); // third arg is the SSE event id
4264
- sse.comment("keep-alive"); // invisible to the client, prevents timeout
4276
+ sse.send("tick", { count: 1 }); // event name, data, optional id
4277
+ sse.send("tick", { count: 2 }, "msg-2"); // third arg is the SSE event id
4278
+ sse.comment("keep-alive"); // invisible to the client, prevents timeout
4265
4279
  sse.end();
4266
4280
  ```
4267
4281
 
@@ -4301,11 +4315,11 @@ response.clearCookie("session_id");
4301
4315
 
4302
4316
  Every response cookie gets `httpOnly: true`, `sameSite: "lax"`, and `secure: true` outside development, unless you override them:
4303
4317
 
4304
- | Flag | Default | Why it's the default |
4305
- |---|---|---|
4306
- | `httpOnly` | `true` | without it, any injected script can read the cookie |
4307
- | `sameSite` | `"lax"` | without it, the cookie rides along on cross-site requests |
4308
- | `secure` | `true`, except in development | without it, the cookie travels in cleartext |
4318
+ | Flag | Default | Why it's the default |
4319
+ | ---------- | ----------------------------- | --------------------------------------------------------- |
4320
+ | `httpOnly` | `true` | without it, any injected script can read the cookie |
4321
+ | `sameSite` | `"lax"` | without it, the cookie rides along on cross-site requests |
4322
+ | `secure` | `true`, except in development | without it, the cookie travels in cleartext |
4309
4323
 
4310
4324
  `secure` is relaxed in development only — browsers drop a `Secure` cookie over plain http, which would silently break every local login. It stays on in test and staging.
4311
4325
 
@@ -4736,12 +4750,17 @@ Set `STORAGE_DRIVER=r2` in production, leave unset in dev — same code uses loc
4736
4750
  ### Uploading a request file
4737
4751
 
4738
4752
  ```ts
4739
- import type { RequestHandler, Response } from "@warlock.js/core";
4753
+ import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
4740
4754
  import { storage } from "@warlock.js/core";
4741
4755
 
4742
- export const uploadAvatarController: RequestHandler = async (request, response: Response) => {
4756
+ export const uploadAvatarController: GuardedRequestHandler = async ({ request, response }) => {
4743
4757
  const upload = request.file("avatar");
4744
- const file = await storage.put(upload, `avatars/${request.user.id}/${upload.fileName}`);
4758
+
4759
+ if (!upload) {
4760
+ return response.badRequest({ error: "missing file" });
4761
+ }
4762
+
4763
+ const file = await storage.put(upload, `avatars/${request.user.id}/${upload.name}`);
4745
4764
 
4746
4765
  return response.successCreate({ url: file.url, hash: file.hash });
4747
4766
  };
@@ -5656,8 +5675,8 @@ Multipart uploads come in as `UploadedFile` instances. The class wraps Fastify's
5656
5675
 
5657
5676
  ## The shape
5658
5677
 
5659
- ```ts title="src/app/uploads/schema/index.ts"
5660
- import { v } from "@warlock.js/seal";
5678
+ ```ts title="src/app/uploads/schema/upload-avatar.schema.ts"
5679
+ import { v, type Infer } from "@warlock.js/seal";
5661
5680
 
5662
5681
  export const uploadAvatarSchema = v.object({
5663
5682
  avatar: v
@@ -5666,16 +5685,18 @@ export const uploadAvatarSchema = v.object({
5666
5685
  .maxSize({ unit: "MB", size: 5 })
5667
5686
  .mimeType(["image/jpeg", "image/png", "image/webp"]),
5668
5687
  });
5688
+
5689
+ export type UploadAvatarSchema = Infer<typeof uploadAvatarSchema>;
5669
5690
  ```
5670
5691
 
5671
5692
  ```ts title="src/app/uploads/controllers/upload-avatar.controller.ts"
5672
- import { type GuardedRequestHandler } from "app/auth/types/guarded-request.type";
5693
+ import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
5673
5694
  import { type UploadAvatarSchema, uploadAvatarSchema } from "../schema/upload-avatar.schema";
5674
5695
 
5675
- export const uploadAvatarController: GuardedRequestHandler<UploadAvatarSchema> = async (
5696
+ export const uploadAvatarController: GuardedRequestHandler<UploadAvatarSchema> = async ({
5676
5697
  request,
5677
5698
  response,
5678
- ) => {
5699
+ }) => {
5679
5700
  const { avatar } = request.validated();
5680
5701
 
5681
5702
  const file = await avatar
@@ -5701,7 +5722,7 @@ Inside a controller:
5701
5722
  ```ts
5702
5723
  import type { RequestHandler, UploadedFile } from "@warlock.js/core";
5703
5724
 
5704
- export const uploadController: RequestHandler = async (request, response) => {
5725
+ export const uploadController: RequestHandler = async ({ request, response }) => {
5705
5726
  // option A — direct from request, no validation
5706
5727
  const file: UploadedFile | undefined = request.file("avatar");
5707
5728
 
@@ -5883,7 +5904,7 @@ export const uploadFilesSchema = v.object({
5883
5904
  ```ts title="src/app/uploads/controllers/create-upload.controller.ts"
5884
5905
  import type { RequestHandler } from "@warlock.js/core";
5885
5906
 
5886
- export const createUploadController: RequestHandler = async (request, response) => {
5907
+ export const createUploadController: RequestHandler = async ({ request, response }) => {
5887
5908
  const { files } = request.validated();
5888
5909
 
5889
5910
  const saved = await Promise.all(
@@ -6180,9 +6201,9 @@ const template = await readFile(appPath("mailers/templates/welcome.html"), "utf-
6180
6201
  ### Health endpoint
6181
6202
 
6182
6203
  ```ts title="src/app/system/controllers/health.controller.ts"
6183
- import { Application, type RequestHandler, type Response } from "@warlock.js/core";
6204
+ import { Application, type RequestHandler } from "@warlock.js/core";
6184
6205
 
6185
- export const healthController: RequestHandler = async (_request, response: Response) => {
6206
+ export const healthController: RequestHandler = async ({ response }) => {
6186
6207
  return response.success({
6187
6208
  status: "ok",
6188
6209
  environment: Application.environment,
@@ -6481,10 +6502,14 @@ The error's message is locale-aware — the `inject-request-context` middleware
6481
6502
  ### Translated response message in a controller
6482
6503
 
6483
6504
  ```ts
6484
- export const createProductController: GuardedRequestHandler<CreateProductSchema> = async (
6505
+ import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
6506
+ import { type CreateProductSchema } from "../schema/create-product.schema";
6507
+ import { createProductService } from "../services/create-product.service";
6508
+
6509
+ export const createProductController: GuardedRequestHandler<CreateProductSchema> = async ({
6485
6510
  request,
6486
6511
  response,
6487
- ) => {
6512
+ }) => {
6488
6513
  const product = await createProductService(request.validated());
6489
6514
  return response.success({
6490
6515
  message: request.t("products.created"),
@@ -7301,10 +7326,10 @@ export async function listFaqsService(filters: FaqListOptions) {
7301
7326
  ```
7302
7327
 
7303
7328
  ```ts title="src/app/faqs/controllers/list-faqs.controller.ts"
7304
- import type { RequestHandler, Response } from "@warlock.js/core";
7329
+ import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
7305
7330
  import { listFaqsService } from "../services/list-faqs.service";
7306
7331
 
7307
- export const listFaqsController: RequestHandler = async (request, response: Response) => {
7332
+ export const listFaqsController: GuardedRequestHandler = async ({ request, response }) => {
7308
7333
  const { data, pagination } = await listFaqsService({
7309
7334
  ...request.all(),
7310
7335
  organization_id: request.user.organizationId,
@@ -7489,7 +7514,7 @@ public locals: RequestLocals = {};
7489
7514
  ```ts title="src/app/observability/middleware/request-timing.middleware.ts"
7490
7515
  import type { Middleware } from "@warlock.js/core";
7491
7516
 
7492
- export const requestTimingMiddleware: Middleware = request => {
7517
+ export const requestTimingMiddleware: Middleware = (request) => {
7493
7518
  request.startedAt = Date.now();
7494
7519
  };
7495
7520
  ```
@@ -7497,7 +7522,7 @@ export const requestTimingMiddleware: Middleware = request => {
7497
7522
  ```ts title="src/app/observability/controllers/timing.controller.ts"
7498
7523
  import type { RequestHandler } from "@warlock.js/core";
7499
7524
 
7500
- export const timingController: RequestHandler = async (request, response) => {
7525
+ export const timingController: RequestHandler = async ({ request, response }) => {
7501
7526
  return response.success({ elapsedMs: Date.now() - request.startedAt });
7502
7527
  };
7503
7528
  ```
@@ -7582,7 +7607,7 @@ Feature-local files such as `src/app/organizations/request-locals.d.ts` are equa
7582
7607
 
7583
7608
  Two rules the scaffold's own comments spell out, and both bite silently:
7584
7609
 
7585
- - **Keep the trailing `export {}`.** `declare module "x"` inside a file with no top-level import or export declares an *ambient* module, which REPLACES `@warlock.js/core`'s real typings instead of merging into them — every framework export vanishes. The `export {}` is what makes the file a module and the block an augmentation. It is not an unused statement to clean up.
7610
+ - **Keep the trailing `export {}`.** `declare module "x"` inside a file with no top-level import or export declares an _ambient_ module, which REPLACES `@warlock.js/core`'s real typings instead of merging into them — every framework export vanishes. The `export {}` is what makes the file a module and the block an augmentation. It is not an unused statement to clean up.
7586
7611
  - **Keep them `interface`, not `type`.** This project otherwise prefers `type`; these are the named exception, because declaration merging is interface-only. `type RequestUser = { ... }` is a duplicate-identifier error, not an augmentation.
7587
7612
 
7588
7613
  On a project scaffolded before 5.1 there is no `src/typings.d.ts`, and `tsconfig.json` carries `"typeRoots": ["./src/typings.d.ts"]` — wrong twice, since `typeRoots` takes directories of `@types` packages rather than files, and that file did not exist. Drop the `typeRoots` entry, create the file, and list it under `include`.
@@ -7656,10 +7681,10 @@ import type { Request, RequestHandler } from "@warlock.js/core";
7656
7681
  import { type CreateProductSchema, createProductSchema } from "../schema/create-product.schema";
7657
7682
  import { createProductService } from "../services/create-product.service";
7658
7683
 
7659
- export const createProductController: RequestHandler<Request<CreateProductSchema>> = async (
7684
+ export const createProductController: RequestHandler<Request<CreateProductSchema>> = async ({
7660
7685
  request,
7661
7686
  response,
7662
- ) => {
7687
+ }) => {
7663
7688
  const product = await createProductService(request.validated());
7664
7689
 
7665
7690
  return response.successCreate({ product });
@@ -7969,11 +7994,11 @@ This skill is the foundation. Every other warlock skill (`register-route`, `crea
7969
7994
  ```
7970
7995
 
7971
7996
  ```ts title="src/app/<module>/controllers/create-<thing>.controller.ts"
7972
- import { type GuardedRequestHandler } from "app/auth/types/guarded-request.type";
7997
+ import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
7973
7998
  import { type CreateThingSchema, createThingSchema } from "../schema/create-thing.schema";
7974
7999
  import { createThingService } from "../services/create-thing.service";
7975
8000
 
7976
- export const createThingController: GuardedRequestHandler<CreateThingSchema> = async (request, response) => {
8001
+ export const createThingController: GuardedRequestHandler<CreateThingSchema> = async ({ request, response }) => {
7977
8002
  const thing = await createThingService(request.validated());
7978
8003
  return response.success({ thing });
7979
8004
  };
@@ -8459,10 +8484,10 @@ export async function notifyUserService(user: User, payload: unknown) {
8459
8484
  Then from a controller:
8460
8485
 
8461
8486
  ```ts
8462
- import type { GuardedRequestHandler } from "app/auth/types/guarded-request.type";
8487
+ import type { GuardedRequestHandler } from "app/auth/requests/guarded.request";
8463
8488
  import { notifyUserService } from "../services/notify-user.service";
8464
8489
 
8465
- export const sendNotificationController: GuardedRequestHandler = async (request, response) => {
8490
+ export const sendNotificationController: GuardedRequestHandler = async ({ request, response }) => {
8466
8491
  await notifyUserService(request.user, request.input("payload"));
8467
8492
  return response.success({ delivered: true });
8468
8493
  };
@@ -8624,7 +8649,7 @@ CLI commands, scheduled jobs, queue workers — anything running outside an HTTP
8624
8649
 
8625
8650
  ---
8626
8651
  name: write-cli-command
8627
- description: 'Author a custom `warlock <my-cmd>` command via the `command()` factory — name, description, action, options, preload, then register in `warlock.config.ts > cli.commands` or drop in `src/app/<module>/commands/`. Triggers: `command`, `CLICommand`, `CLICommandPreload`, `CLICommandOption`, `preload`, `preAction`, `persistent`, `colors`; "write a custom warlock command", "one-off maintenance task", "ship a CLI from a package", "framework built-in commands"; typical import `import { command } from "@warlock.js/core"`. Skip: framework dev/build/start — `@warlock.js/core/run-app/SKILL.md`; warlock.config.ts wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs `commander`, `yargs`, `oclif`.'
8652
+ description: 'Author a custom `warlock <my-cmd>` command via the `command()` factory — name, description, action, options, preload, then register in `warlock.config.ts > cli.commands` or drop in `src/app/<module>/commands/`. Also covers built-in `warlock add` feature scaffolding, including the Web starter and `index.register.ts`. Triggers: `command`, `CLICommand`, `CLICommandPreload`, `CLICommandOption`, `preload`, `preAction`, `persistent`, `colors`, `warlock add`, `index.register.ts`; "write a custom warlock command", "one-off maintenance task", "ship a CLI from a package", "framework built-in commands"; typical import `import { command } from "@warlock.js/core"`. Skip: framework dev/build/start — `@warlock.js/core/run-app/SKILL.md`; warlock.config.ts wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs `commander`, `yargs`, `oclif`.'
8628
8653
  ---
8629
8654
 
8630
8655
  # Warlock — write a CLI command
@@ -8829,6 +8854,7 @@ warlock generate (alias: g) — interactive picker
8829
8854
  | Feature | Installs |
8830
8855
  | ------------- | --------------------------------------------------------------------------------------------------- |
8831
8856
  | `react-email` | `react-email` + `@react-email/components` + `@react-email/render` + `@react-email/tailwind`; drops a `welcome-email.tsx` sample; patches `tsconfig.json` |
8857
+ | `web` | Warlock Web + React stack; scaffolds the application page, localization, contact endpoint, and Web configuration |
8832
8858
  | `react` | `react` + `react-dom` + types |
8833
8859
  | `image` | `sharp` (for the `Image` class) |
8834
8860
  | `mail` | `nodemailer` + types |
@@ -8846,6 +8872,8 @@ warlock generate (alias: g) — interactive picker
8846
8872
 
8847
8873
  Run `warlock add --list` to see what's currently registered. Pass `--packageManager <yarn\|pnpm\|npm>` to override auto-detection (defaults to whichever lockfile is present).
8848
8874
 
8875
+ The Web starter keeps the page's universal non-component setup in `src/web/index.register.ts`, while `src/web/index.page.tsx` re-exports `register`. Warlock still discovers and runs the hook from the page namespace in both SSR and the browser, but editing the page component remains inside React's Fast Refresh boundary. Do not move the `register()` declaration inline beside the component: React Refresh treats that non-component export as incompatible and reloads the page, losing component state.
8876
+
8849
8877
  ### Misc
8850
8878
 
8851
8879
  | Command | Purpose |
@@ -8935,7 +8963,7 @@ export default command({
8935
8963
 
8936
8964
  ---
8937
8965
  name: write-middleware
8938
- description: 'Author HTTP middleware for @warlock.js/core — the `(request, response)` signature, short-circuit by returning a response, enrich the request with extra fields, register per-route, per-group, or app-wide. Triggers: `Middleware`, `MiddlewareResponse`, `router.group`, `guarded`, `request.detectIp`, `authMiddleware`; "write a custom middleware", "short-circuit a request", "enrich the request with extra fields", "per-route vs per-group middleware"; typical import `import type { Middleware } from "@warlock.js/core"`. Skip: built-in middleware catalog — `@warlock.js/core/use-middleware/SKILL.md`; route attachment — `@warlock.js/core/register-route/SKILL.md`; response helpers — `@warlock.js/core/send-response/SKILL.md`; competing patterns: `express` `(req, res, next)` middleware, Fastify `preHandler` hooks.'
8966
+ description: 'Author HTTP middleware for @warlock.js/core — the `({ request, response })` signature, short-circuit by returning a response, enrich the request with extra fields, register per-route, per-group, or app-wide. Triggers: `Middleware`, `MiddlewareResponse`, `router.group`, `guarded`, `request.detectIp`, `authMiddleware`; "write a custom middleware", "short-circuit a request", "enrich the request with extra fields", "per-route vs per-group middleware"; typical import `import type { Middleware } from "@warlock.js/core"`. Skip: built-in middleware catalog — `@warlock.js/core/use-middleware/SKILL.md`; route attachment — `@warlock.js/core/register-route/SKILL.md`; response helpers — `@warlock.js/core/send-response/SKILL.md`; competing patterns: `express` `(req, res, next)` middleware, Fastify `preHandler` hooks.'
8939
8967
  ---
8940
8968
 
8941
8969
  # Warlock — write a middleware
@@ -8947,7 +8975,7 @@ Middleware is a plain function that runs before the controller. Two outcomes: re
8947
8975
  ```ts title="src/app/<module>/utils/<name>.middleware.ts"
8948
8976
  import type { Middleware } from "@warlock.js/core";
8949
8977
 
8950
- export const requireApiKey: Middleware = (request, response) => {
8978
+ export const requireApiKey: Middleware = ({ request, response }) => {
8951
8979
  const key = request.header("X-API-Key");
8952
8980
 
8953
8981
  if (!key || key !== process.env.API_KEY) {
@@ -8958,16 +8986,14 @@ export const requireApiKey: Middleware = (request, response) => {
8958
8986
  };
8959
8987
  ```
8960
8988
 
8961
- That's the contract: `(request: Request, response: Response) => Response | undefined | void`. Async is fine — return a `Promise<Response | undefined | void>`.
8989
+ That's the contract: `(context: HttpContext<Request>) => Response | undefined | void`, where `context` is `{ request, response }`. Async is fine — return a `Promise<Response | undefined | void>`.
8962
8990
 
8963
8991
  The real type, from `@warlock.js/core/src/router/types.ts`:
8964
8992
 
8965
8993
  ```ts
8966
8994
  export type Middleware<MiddlewareRequest extends Request = Request> = {
8967
- (request: MiddlewareRequest, response: Response): MiddlewareResponse;
8995
+ (context: HttpContext<MiddlewareRequest>): MiddlewareResponse;
8968
8996
  };
8969
-
8970
- export type MiddlewareResponse = ReturnedResponse | undefined | void;
8971
8997
  ```
8972
8998
 
8973
8999
  ## Short-circuit vs continue
@@ -8977,7 +9003,7 @@ The pattern is "return a response to stop, return nothing to continue":
8977
9003
  ```ts
8978
9004
  import type { Middleware } from "@warlock.js/core";
8979
9005
 
8980
- export const requireFeatureFlag: Middleware = async (request, response) => {
9006
+ export const requireFeatureFlag: Middleware = async ({ request, response }) => {
8981
9007
  const flag = await loadFeatureFlag(request.input("organization_id"));
8982
9008
 
8983
9009
  if (!flag.enabled) {
@@ -8995,8 +9021,8 @@ If you short-circuit, the controller never runs. The response helper you pick (`
8995
9021
  You can attach arbitrary fields to `request` from a middleware, and they survive into the controller. The cleanest pattern is to extend `Request` via module augmentation in a `.d.ts` and assign in the middleware:
8996
9022
 
8997
9023
  ```ts title="src/app/feature-flags/middleware/load-feature-flag.middleware.ts"
8998
- import type { Middleware } from "@warlock.js/core";
8999
- import type { FeatureFlag } from "../models/feature-flag";
9024
+ import type { Middleware, Request, RequestUser } from "@warlock.js/core";
9025
+ import { FeatureFlag } from "../models/feature-flag";
9000
9026
 
9001
9027
  declare module "@warlock.js/core" {
9002
9028
  interface Request {
@@ -9004,7 +9030,7 @@ declare module "@warlock.js/core" {
9004
9030
  }
9005
9031
  }
9006
9032
 
9007
- export const loadFeatureFlag: Middleware = async (request) => {
9033
+ export const loadFeatureFlag: Middleware<Request & { user: RequestUser }> = async ({ request }) => {
9008
9034
  request.featureFlag = await FeatureFlag.findBy("organization_id", request.user.organizationId);
9009
9035
  };
9010
9036
  ```
@@ -9074,7 +9100,7 @@ export function guarded(callback: () => void) {
9074
9100
  }
9075
9101
 
9076
9102
  export function guardedAdmin(callback: () => void) {
9077
- router.group({ prefix: "/admin", middleware: [authMiddleware()] }, callback);
9103
+ router.group({ prefix: "/admin", middleware: [authMiddleware("admin")] }, callback);
9078
9104
  }
9079
9105
 
9080
9106
  export function publicRoutes(callback: () => void) {
@@ -9092,7 +9118,7 @@ guarded(() => {
9092
9118
  });
9093
9119
  ```
9094
9120
 
9095
- `authMiddleware(allowedUserType?)` accepts a user-type string or array. Without an arg it just verifies the token is present; with `"user"` / `"admin"` it also checks the decoded `userType` matches.
9121
+ `authMiddleware(allowedUserType, tokenFrom?)` requires a user-type string or array `[]` accepts any authenticated user without checking `userType`; `"user"` / `"admin"` also checks the decoded `userType` matches.
9096
9122
 
9097
9123
  ## Common patterns
9098
9124
 
@@ -9119,13 +9145,13 @@ router.group(
9119
9145
  ```ts
9120
9146
  import type { Middleware } from "@warlock.js/core";
9121
9147
 
9122
- export const optionalAuth: Middleware = async (request, response) => {
9148
+ export const optionalAuth: Middleware = async ({ request, response }) => {
9123
9149
  if (!request.authorizationValue) {
9124
9150
  return; // anonymous — let it through
9125
9151
  }
9126
9152
 
9127
9153
  // token present → enforce it
9128
- return authMiddleware("user")(request, response);
9154
+ return authMiddleware("user")({ request, response });
9129
9155
  };
9130
9156
  ```
9131
9157
 
@@ -9141,7 +9167,7 @@ export const optionalAuth: Middleware = async (request, response) => {
9141
9167
  - **Group middleware runs before per-route middleware**, in array order. The full chain is `app.all → group → per-route → controller`. Mind the order if you stack auth + rate-limit + audit.
9142
9168
  - **Middleware can be async.** Returning `Promise<undefined>` continues the chain. Returning `Promise<Response>` short-circuits. The framework awaits the result.
9143
9169
  - **Don't mutate `request.payload` directly.** Use `request.setValidatedData(...)` or attach a new named field (`request.featureFlag = ...`). The internals expect `payload.all` shapes to come from the validator pipeline.
9144
- - **`Middleware` is generic over the request type.** For middleware that assumes a validated schema, narrow it: `const m: Middleware<CreateProductRequest> = (request) => { ... }`. But most middlewares run before validation, so the default `Middleware` is right.
9170
+ - **`Middleware` is generic over the request type.** For middleware that assumes a validated schema, narrow it: `const m: Middleware<CreateProductRequest> = ({ request }) => { ... }`. But most middlewares run before validation, so the default `Middleware` is right.
9145
9171
  - **No `next()` parameter.** Express-style `next()` doesn't apply here. The framework chains based on return value.
9146
9172
  - **`request.baseRequest` / `response.baseResponse` are escape hatches, not API.** They expose the underlying Fastify primitives for cases the framework hasn't covered yet (streaming was the historical precedent). Prefer framework helpers first — `response.send()`, `response.header()`, `response.replay()`, `request.input()`, `request.detectIp()`, etc. If you find yourself reaching for `baseResponse` or `baseRequest` for non-streaming work, that's a missing helper — file an issue. The cache and idempotency middlewares both shipped with a quietly-broken FastifyReply-return bug because they bypassed the helper layer; the framework now guards `Response.send()` against double-send, but the right answer is "use the helper."
9147
9173
  - **Behind any proxy, use `request.detectIp()` not `request.ip`.** `request.ip` is the immediate peer (likely your load balancer); `request.detectIp()` honors `X-Real-IP` / `X-Forwarded-For`. Either way, only trust the result as far as you trust the upstream chain — those headers are client-settable; verify the request came through your trusted edge before treating the value as authoritative.