@warlock.js/core 5.5.0 → 5.7.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 (171) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/esm/application/app.d.mts.map +1 -1
  3. package/esm/application/app.mjs +7 -1
  4. package/esm/application/app.mjs.map +1 -1
  5. package/esm/benchmark/profiler.d.mts.map +1 -1
  6. package/esm/benchmark/profiler.mjs +4 -3
  7. package/esm/benchmark/profiler.mjs.map +1 -1
  8. package/esm/cli/cli-commands.manager.mjs +10 -7
  9. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  10. package/esm/cli/cli-commands.utils.mjs.map +1 -1
  11. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
  12. package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs.map +1 -1
  13. package/esm/cli/commands/generate/generators/column-dsl-parser.mjs.map +1 -1
  14. package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
  15. package/esm/cli/commands/routes/format-routes-table.mjs +3 -3
  16. package/esm/cli/commands/routes/format-routes-table.mjs.map +1 -1
  17. package/esm/cli/commands/routes/route-row.mjs.map +1 -1
  18. package/esm/cli/commands/routes.command.mjs.map +1 -1
  19. package/esm/cli/framework-cli-commands.mjs.map +1 -1
  20. package/esm/cli/parse-cli-args.mjs +1 -0
  21. package/esm/cli/parse-cli-args.mjs.map +1 -1
  22. package/esm/cli/string-similarity.mjs +13 -4
  23. package/esm/cli/string-similarity.mjs.map +1 -1
  24. package/esm/commands/cli-command.d.mts.map +1 -1
  25. package/esm/commands/cli-command.mjs +5 -2
  26. package/esm/commands/cli-command.mjs.map +1 -1
  27. package/esm/config/config-loader.mjs +3 -1
  28. package/esm/config/config-loader.mjs.map +1 -1
  29. package/esm/config/locale-configuration.mjs +11 -1
  30. package/esm/config/locale-configuration.mjs.map +1 -1
  31. package/esm/container/container-instance-registry.mjs +40 -0
  32. package/esm/container/container-instance-registry.mjs.map +1 -0
  33. package/esm/container/index.d.mts +11 -0
  34. package/esm/container/index.d.mts.map +1 -1
  35. package/esm/container/index.mjs +11 -0
  36. package/esm/container/index.mjs.map +1 -1
  37. package/esm/database/drop-tables-action.mjs.map +1 -1
  38. package/esm/database/migrate-action.mjs.map +1 -1
  39. package/esm/database/seed-command-action.mjs.map +1 -1
  40. package/esm/dev-server/dependency-graph.d.mts.map +1 -1
  41. package/esm/dev-server/dependency-graph.mjs +2 -1
  42. package/esm/dev-server/dependency-graph.mjs.map +1 -1
  43. package/esm/dev-server/dev-logger.mjs +7 -5
  44. package/esm/dev-server/dev-logger.mjs.map +1 -1
  45. package/esm/dev-server/file-event-handler.mjs.map +1 -1
  46. package/esm/dev-server/files-orchestrator.mjs +1 -1
  47. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  48. package/esm/dev-server/layer-executor.mjs.map +1 -1
  49. package/esm/dev-server/loader/build-transpile-init.mjs.map +1 -1
  50. package/esm/dev-server/loader/load-hook.mjs.map +1 -1
  51. package/esm/dev-server/loader/register-loader.mjs.map +1 -1
  52. package/esm/dev-server/loader/resolve-hook.mjs.map +1 -1
  53. package/esm/dev-server/loader/transpile-cache.mjs.map +1 -1
  54. package/esm/dev-server/module-loader.mjs.map +1 -1
  55. package/esm/dev-server/parse-imports.mjs +22 -8
  56. package/esm/dev-server/parse-imports.mjs.map +1 -1
  57. package/esm/dev-server/type-generator.mjs.map +1 -1
  58. package/esm/dev-server/types.d.mts.map +1 -1
  59. package/esm/dev-server/utils.mjs.map +1 -1
  60. package/esm/encryption/encrypt.mjs +1 -0
  61. package/esm/encryption/encrypt.mjs.map +1 -1
  62. package/esm/errors/container-key-missing-error.mjs +28 -0
  63. package/esm/errors/container-key-missing-error.mjs.map +1 -0
  64. package/esm/errors/unknown-locale-error.mjs +28 -0
  65. package/esm/errors/unknown-locale-error.mjs.map +1 -0
  66. package/esm/generations/add-command.action.mjs +8 -2
  67. package/esm/generations/add-command.action.mjs.map +1 -1
  68. package/esm/generations/features/access.feature.mjs.map +1 -1
  69. package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -1
  70. package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -1
  71. package/esm/generations/features/ai-google.feature.mjs.map +1 -1
  72. package/esm/generations/features/ai-ollama.feature.mjs.map +1 -1
  73. package/esm/generations/features/ai-openai.feature.mjs.map +1 -1
  74. package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -1
  75. package/esm/generations/features/ai-tools.feature.mjs.map +1 -1
  76. package/esm/generations/features/ai-workspace.feature.mjs.map +1 -1
  77. package/esm/generations/features/ai.feature.mjs.map +1 -1
  78. package/esm/generations/features/herald.feature.mjs.map +1 -1
  79. package/esm/generations/features/notifications.feature.mjs.map +1 -1
  80. package/esm/generations/features/scheduler.feature.mjs.map +1 -1
  81. package/esm/generations/features/shared/patch-tsconfig-include.mjs.map +1 -1
  82. package/esm/generations/features/shared/relocate-conflicting-home-route.mjs +6 -1
  83. package/esm/generations/features/shared/relocate-conflicting-home-route.mjs.map +1 -1
  84. package/esm/generations/features/tailwind.feature.mjs.map +1 -1
  85. package/esm/generations/features/types.mjs.map +1 -1
  86. package/esm/generations/features/web.feature.mjs.map +1 -1
  87. package/esm/http/context/request-memo.mjs.map +1 -1
  88. package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
  89. package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
  90. package/esm/http/middleware/concurrency-limit.middleware.mjs.map +1 -1
  91. package/esm/http/middleware/idempotency.middleware.d.mts.map +1 -1
  92. package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
  93. package/esm/http/middleware/maintenance.middleware.mjs +1 -2
  94. package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
  95. package/esm/http/middleware/max-body-size.middleware.mjs.map +1 -1
  96. package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
  97. package/esm/http/middleware/utils/cidr-match.d.mts.map +1 -1
  98. package/esm/http/middleware/utils/cidr-match.mjs +1 -0
  99. package/esm/http/middleware/utils/cidr-match.mjs.map +1 -1
  100. package/esm/http/port-preflight.d.mts.map +1 -1
  101. package/esm/http/port-preflight.mjs.map +1 -1
  102. package/esm/http/request.d.mts.map +1 -1
  103. package/esm/http/request.mjs +16 -9
  104. package/esm/http/request.mjs.map +1 -1
  105. package/esm/http/response.d.mts +38 -0
  106. package/esm/http/response.d.mts.map +1 -1
  107. package/esm/http/response.mjs +47 -0
  108. package/esm/http/response.mjs.map +1 -1
  109. package/esm/http/server.d.mts.map +1 -1
  110. package/esm/http/server.mjs.map +1 -1
  111. package/esm/http/types.d.mts.map +1 -1
  112. package/esm/image/image.d.mts.map +1 -1
  113. package/esm/image/image.mjs.map +1 -1
  114. package/esm/mail/send-mail.mjs.map +1 -1
  115. package/esm/production/build-contributions.mjs.map +1 -1
  116. package/esm/production/bundle-framework-dependencies.mjs +152 -0
  117. package/esm/production/bundle-framework-dependencies.mjs.map +1 -0
  118. package/esm/production/production-builder.mjs +30 -3
  119. package/esm/production/production-builder.mjs.map +1 -1
  120. package/esm/production/production-supervisor.mjs.map +1 -1
  121. package/esm/production/tsconfig-path-aliases.mjs +99 -0
  122. package/esm/production/tsconfig-path-aliases.mjs.map +1 -0
  123. package/esm/react/index.d.mts.map +1 -1
  124. package/esm/react/index.mjs.map +1 -1
  125. package/esm/repositories/adapters/cascade/filter-applicator.mjs +4 -2
  126. package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
  127. package/esm/repositories/contracts/query-builder.contract.d.mts.map +1 -1
  128. package/esm/repositories/contracts/types.d.mts.map +1 -1
  129. package/esm/resource/resource-field-builder.d.mts.map +1 -1
  130. package/esm/resource/resource-field-builder.mjs.map +1 -1
  131. package/esm/resource/types.d.mts.map +1 -1
  132. package/esm/router/positional-handler-diagnostics.d.mts.map +1 -1
  133. package/esm/router/positional-handler-diagnostics.mjs +9 -3
  134. package/esm/router/positional-handler-diagnostics.mjs.map +1 -1
  135. package/esm/router/route-registry.mjs +1 -1
  136. package/esm/router/route-registry.mjs.map +1 -1
  137. package/esm/router/router.d.mts.map +1 -1
  138. package/esm/router/router.mjs +1 -2
  139. package/esm/router/router.mjs.map +1 -1
  140. package/esm/router/types.d.mts.map +1 -1
  141. package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
  142. package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
  143. package/esm/storage/scoped-storage.d.mts.map +1 -1
  144. package/esm/storage/scoped-storage.mjs +1 -0
  145. package/esm/storage/scoped-storage.mjs.map +1 -1
  146. package/esm/storage/storage.d.mts.map +1 -1
  147. package/esm/storage/storage.mjs +6 -2
  148. package/esm/storage/storage.mjs.map +1 -1
  149. package/esm/storage/types.d.mts.map +1 -1
  150. package/esm/storage/utils/contain-path.mjs.map +1 -1
  151. package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
  152. package/esm/storage/utils/safe-fetch.mjs +6 -1
  153. package/esm/storage/utils/safe-fetch.mjs.map +1 -1
  154. package/esm/tests/start-http-development-server.d.mts.map +1 -1
  155. package/esm/tests/start-http-development-server.mjs.map +1 -1
  156. package/esm/tests/test-lifecycle-state.mjs.map +1 -1
  157. package/esm/tests/test-setup-timeout.mjs.map +1 -1
  158. package/esm/tests/vitest-setup.d.mts.map +1 -1
  159. package/esm/tests/vitest-setup.mjs.map +1 -1
  160. package/esm/use-cases/use-cases-registry.d.mts.map +1 -1
  161. package/esm/use-cases/use-cases-registry.mjs.map +1 -1
  162. package/esm/utils/app-log.mjs.map +1 -1
  163. package/esm/utils/queue.mjs.map +1 -1
  164. package/esm/utils/sleep.mjs.map +1 -1
  165. package/esm/utils/version-compare.d.mts.map +1 -1
  166. package/esm/utils/version-compare.mjs +12 -4
  167. package/esm/utils/version-compare.mjs.map +1 -1
  168. package/esm/validation/plugins/database-plugin.mjs.map +1 -1
  169. package/esm/vite/lower-stage3-decorators.mjs +1 -0
  170. package/esm/vite/lower-stage3-decorators.mjs.map +1 -1
  171. package/package.json +12 -12
@@ -1 +1 @@
1
- {"version":3,"file":"web.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/web.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\r\nimport type { CommandActionData } from \"../../commands/types\";\r\nimport { rootPath, srcPath } from \"../../utils\";\r\nimport { relocateConflictingHomeRoute } from \"./shared/relocate-conflicting-home-route\";\r\nimport { resolveContactScaffold } from \"./shared/resolve-contact-scaffold\";\r\nimport {\r\n webContactControllerStub,\r\n webContactRoutesStub,\r\n webHomePageStub,\r\n webHomeRegisterStub,\r\n webRootStub,\r\n} from \"../stubs\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\r\n\r\n/**\r\n * Register the WebConnector in `warlock.config.ts`, and ONLY there.\r\n *\r\n * It belongs to the config array or to app code, never both. Both halves are\r\n * registered before app code loads — the CLI preloader in dev, the generated\r\n * entry in production — so also calling `connectorsManager.register(...)` in\r\n * `src/app/main.ts` boots the connector twice and installs every page route\r\n * twice. That surfaces at PRODUCTION boot as `Route name \"...\" is already\r\n * taken`, because pages and API routes share one route-name namespace.\r\n *\r\n * The config array is the half to prefer: `warlock build` reads the same array\r\n * to drain each connector's build contribution, so \"built for\" and \"boots with\"\r\n * cannot drift.\r\n *\r\n * String surgery rather than a TypeScript parse: `warlock.config.ts` is an\r\n * app-owned file that may carry any formatting, and a parse-and-print would\r\n * reformat the parts we did not come to change.\r\n */\r\nasync function registerWebConnector(): Promise<void> {\r\n const configPath = rootPath(\"warlock.config.ts\");\r\n\r\n if (!(await fileExistsAsync(configPath))) {\r\n console.log(\r\n `${colors.yellowBright(\"warlock.config.ts\")} not found — add this yourself:\\n` +\r\n ` import { webConnector } from \"@warlock.js/web/connector\";\\n` +\r\n ` export default defineConfig({ connectors: [webConnector()] });`,\r\n );\r\n\r\n return;\r\n }\r\n\r\n const current = await getFileAsync(configPath);\r\n\r\n if (current.includes(\"webConnector\")) {\r\n console.log(`${colors.yellowBright(\"webConnector\")} already registered, skipping...`);\r\n\r\n return;\r\n }\r\n\r\n const importLine = 'import { webConnector } from \"@warlock.js/web/connector\";';\r\n let next = current.includes(importLine) ? current : `${importLine}\\n${current}`;\r\n\r\n // An existing `connectors: [` gains one entry; otherwise the key is added to\r\n // the object `defineConfig` receives.\r\n if (/connectors:\\s*\\[/.test(next)) {\r\n next = next.replace(/connectors:\\s*\\[/, \"connectors: [webConnector(),\");\r\n } else if (next.includes(\"defineConfig({\")) {\r\n next = next.replace(\"defineConfig({\", \"defineConfig({\\n connectors: [webConnector()],\");\r\n } else {\r\n console.log(\r\n `${colors.yellowBright(\"warlock.config.ts\")} has no recognisable defineConfig({...}) — ` +\r\n \"add `connectors: [webConnector()]` yourself.\",\r\n );\r\n\r\n return;\r\n }\r\n\r\n await putFileAsync(configPath, next);\r\n console.log(`${colors.green(\"✓\")} Registered webConnector in warlock.config.ts`);\r\n}\r\n\r\n/**\r\n * Scaffold the smallest page layer that renders, and register the connector.\r\n *\r\n * `src/web/root.tsx` is the sentinel for \"already scaffolded\" — the framework\r\n * ships a default root, so its presence means a human has been here.\r\n */\r\nasync function completeWebInstallation(_options: CommandActionData) {\r\n const rootFile = srcPath(\"web/root.tsx\");\r\n\r\n if (await fileExistsAsync(rootFile)) {\r\n console.log(`${colors.yellowBright(\"src/web\")} already scaffolded, skipping...`);\r\n } else {\r\n await ensureDirectoryAsync(srcPath(\"web\"));\r\n await putFileAsync(rootFile, webRootStub);\r\n console.log(`${colors.green(\"✓\")} Created src/web/root.tsx`);\r\n\r\n const collision = await relocateConflictingHomeRoute();\r\n\r\n if (collision.outcome === \"relocated\") {\r\n console.log(\r\n `${colors.green(\"✓\")} Moved the existing ${colors.yellowBright('GET \"/\"')} route to ` +\r\n `${colors.yellowBright('\"/welcome\"')} in ${colors.yellowBright(`src/${collision.relativePath}`)} — ` +\r\n \"the new page owns `/` now, and the JSON welcome route still answers at /welcome.\",\r\n );\r\n }\r\n\r\n // The page is written ONLY when `/` is provably free. Writing it while\r\n // another handler holds `/` produces a homepage that 500s on first request,\r\n // which is precisely the outcome a scaffolder must never hand back.\r\n if (collision.outcome === \"conflict\" || collision.outcome === \"failed\") {\r\n const verb = collision.outcome === \"failed\" ? colors.redBright(\"✗\") : colors.yellowBright(\"!\");\r\n\r\n console.log(\r\n `${verb} Did not create src/web/index.page.tsx: ${collision.reason}.\\n` +\r\n ` The page stub declares ${colors.yellowBright('route.path = \"/\"')}, and two handlers on one ` +\r\n \"path is a 500 at request time, not a startup error.\\n\" +\r\n ` Free up ${colors.yellowBright('GET \"/\"')} under src/app — move it to a path of its own, ` +\r\n \"or remove it — then create src/web/index.page.tsx yourself. Giving the page a `route` other \" +\r\n \"than `/` works too.\",\r\n );\r\n\r\n // Non-zero on BOTH branches. The page layer this command exists to\r\n // scaffold was not scaffolded, and a 0 here is the exact \"looked like it\r\n // worked\" signal that put `/` in this state to begin with — a conflict we\r\n // declined to guess at is still an incomplete install, not a success.\r\n //\r\n // `exitCode` rather than `exit(1)`: the connector below still has to be\r\n // registered, and any other feature in the same `warlock add` invocation\r\n // still has to install, or the project is left half-wired on top of this.\r\n process.exitCode = 1;\r\n } else {\r\n await putFileAsync(srcPath(\"web/index.page.tsx\"), webHomePageStub);\r\n await putFileAsync(srcPath(\"web/index.register.ts\"), webHomeRegisterStub);\r\n\r\n // Unlike root.tsx above, the contact controller and its routes file are\r\n // ordinary application files a project can already have — independently\r\n // of ever having run `warlock add web`. Each is guarded on its OWN\r\n // existence, not on the (already-consumed) root.tsx sentinel, so an\r\n // existing `contact` module is skipped rather than clobbered. See\r\n // resolveContactScaffold's doc comment for why.\r\n const contactPlan = resolveContactScaffold({\r\n controllerExists: await fileExistsAsync(\r\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\r\n ),\r\n routesExists: await fileExistsAsync(srcPath(\"app/contact/routes.ts\")),\r\n });\r\n\r\n if (contactPlan.writeController) {\r\n await ensureDirectoryAsync(srcPath(\"app/contact/controllers\"));\r\n await putFileAsync(\r\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\r\n webContactControllerStub,\r\n );\r\n }\r\n\r\n if (contactPlan.writeRoutes) {\r\n await ensureDirectoryAsync(srcPath(\"app/contact\"));\r\n await putFileAsync(srcPath(\"app/contact/routes.ts\"), webContactRoutesStub);\r\n }\r\n\r\n console.log(`${colors.green(\"✓\")} Created src/web/index.page.tsx`);\r\n\r\n for (const message of contactPlan.messages) {\r\n console.log(message);\r\n }\r\n }\r\n }\r\n\r\n await registerWebConnector();\r\n}\r\n\r\nexport const webFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/web — SSR React pages served by the Warlock HTTP server. Scaffolds src/web (root.tsx + a home page) and registers the WebConnector in warlock.config.ts. Pages are opt-in: a Warlock app is an API until you add this.\",\r\n dependencies: {\r\n \"@warlock.js/web\": INSTALLED_WARLOCK_VERSION,\r\n \"@mongez/http\": \"^3.5.0\",\r\n \"@mongez/react-form\": \"^4.0.0\",\r\n \"@mongez/react-localization\": \"^3.4.7\",\r\n react: \"^19.2.3\",\r\n \"react-dom\": \"^19.2.3\",\r\n },\r\n devDependencies: {\r\n \"@types/react\": \"^19.2.7\",\r\n \"@types/react-dom\": \"^19.2.3\",\r\n // Loaded through `await import()` by the dev server only, so both are\r\n // optional peers of `web` rather than hard dependencies.\r\n vite: \"^7.3.5\",\r\n \"@vitejs/plugin-react\": \"^5.2.0\",\r\n },\r\n onExecuting: completeWebInstallation,\r\n};\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"web.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/web.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\r\nimport type { CommandActionData } from \"../../commands/types\";\r\nimport { rootPath, srcPath } from \"../../utils\";\r\nimport { relocateConflictingHomeRoute } from \"./shared/relocate-conflicting-home-route\";\r\nimport { resolveContactScaffold } from \"./shared/resolve-contact-scaffold\";\r\nimport {\r\n webContactControllerStub,\r\n webContactRoutesStub,\r\n webHomePageStub,\r\n webHomeRegisterStub,\r\n webRootStub,\r\n} from \"../stubs\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\r\n\r\n/**\r\n * Register the WebConnector in `warlock.config.ts`, and ONLY there.\r\n *\r\n * It belongs to the config array or to app code, never both. Both halves are\r\n * registered before app code loads — the CLI preloader in dev, the generated\r\n * entry in production — so also calling `connectorsManager.register(...)` in\r\n * `src/app/main.ts` boots the connector twice and installs every page route\r\n * twice. That surfaces at PRODUCTION boot as `Route name \"...\" is already\r\n * taken`, because pages and API routes share one route-name namespace.\r\n *\r\n * The config array is the half to prefer: `warlock build` reads the same array\r\n * to drain each connector's build contribution, so \"built for\" and \"boots with\"\r\n * cannot drift.\r\n *\r\n * String surgery rather than a TypeScript parse: `warlock.config.ts` is an\r\n * app-owned file that may carry any formatting, and a parse-and-print would\r\n * reformat the parts we did not come to change.\r\n */\r\nasync function registerWebConnector(): Promise<void> {\r\n const configPath = rootPath(\"warlock.config.ts\");\r\n\r\n if (!(await fileExistsAsync(configPath))) {\r\n console.log(\r\n `${colors.yellowBright(\"warlock.config.ts\")} not found — add this yourself:\\n` +\r\n ` import { webConnector } from \"@warlock.js/web/connector\";\\n` +\r\n ` export default defineConfig({ connectors: [webConnector()] });`,\r\n );\r\n\r\n return;\r\n }\r\n\r\n const current = await getFileAsync(configPath);\r\n\r\n if (current.includes(\"webConnector\")) {\r\n console.log(`${colors.yellowBright(\"webConnector\")} already registered, skipping...`);\r\n\r\n return;\r\n }\r\n\r\n const importLine = 'import { webConnector } from \"@warlock.js/web/connector\";';\r\n let next = current.includes(importLine) ? current : `${importLine}\\n${current}`;\r\n\r\n // An existing `connectors: [` gains one entry; otherwise the key is added to\r\n // the object `defineConfig` receives.\r\n if (/connectors:\\s*\\[/.test(next)) {\r\n next = next.replace(/connectors:\\s*\\[/, \"connectors: [webConnector(),\");\r\n } else if (next.includes(\"defineConfig({\")) {\r\n next = next.replace(\"defineConfig({\", \"defineConfig({\\n connectors: [webConnector()],\");\r\n } else {\r\n console.log(\r\n `${colors.yellowBright(\"warlock.config.ts\")} has no recognisable defineConfig({...}) — ` +\r\n \"add `connectors: [webConnector()]` yourself.\",\r\n );\r\n\r\n return;\r\n }\r\n\r\n await putFileAsync(configPath, next);\r\n console.log(`${colors.green(\"✓\")} Registered webConnector in warlock.config.ts`);\r\n}\r\n\r\n/**\r\n * Scaffold the smallest page layer that renders, and register the connector.\r\n *\r\n * `src/web/root.tsx` is the sentinel for \"already scaffolded\" — the framework\r\n * ships a default root, so its presence means a human has been here.\r\n */\r\nasync function completeWebInstallation(_options: CommandActionData) {\r\n const rootFile = srcPath(\"web/root.tsx\");\r\n\r\n if (await fileExistsAsync(rootFile)) {\r\n console.log(`${colors.yellowBright(\"src/web\")} already scaffolded, skipping...`);\r\n } else {\r\n await ensureDirectoryAsync(srcPath(\"web\"));\r\n await putFileAsync(rootFile, webRootStub);\r\n console.log(`${colors.green(\"✓\")} Created src/web/root.tsx`);\r\n\r\n const collision = await relocateConflictingHomeRoute();\r\n\r\n if (collision.outcome === \"relocated\") {\r\n console.log(\r\n `${colors.green(\"✓\")} Moved the existing ${colors.yellowBright('GET \"/\"')} route to ` +\r\n `${colors.yellowBright('\"/welcome\"')} in ${colors.yellowBright(`src/${collision.relativePath}`)} — ` +\r\n \"the new page owns `/` now, and the JSON welcome route still answers at /welcome.\",\r\n );\r\n }\r\n\r\n // The page is written ONLY when `/` is provably free. Writing it while\r\n // another handler holds `/` produces a homepage that 500s on first request,\r\n // which is precisely the outcome a scaffolder must never hand back.\r\n if (collision.outcome === \"conflict\" || collision.outcome === \"failed\") {\r\n const verb =\r\n collision.outcome === \"failed\" ? colors.redBright(\"✗\") : colors.yellowBright(\"!\");\r\n\r\n console.log(\r\n `${verb} Did not create src/web/index.page.tsx: ${collision.reason}.\\n` +\r\n ` The page stub declares ${colors.yellowBright('route.path = \"/\"')}, and two handlers on one ` +\r\n \"path is a 500 at request time, not a startup error.\\n\" +\r\n ` Free up ${colors.yellowBright('GET \"/\"')} under src/app — move it to a path of its own, ` +\r\n \"or remove it — then create src/web/index.page.tsx yourself. Giving the page a `route` other \" +\r\n \"than `/` works too.\",\r\n );\r\n\r\n // Non-zero on BOTH branches. The page layer this command exists to\r\n // scaffold was not scaffolded, and a 0 here is the exact \"looked like it\r\n // worked\" signal that put `/` in this state to begin with — a conflict we\r\n // declined to guess at is still an incomplete install, not a success.\r\n //\r\n // `exitCode` rather than `exit(1)`: the connector below still has to be\r\n // registered, and any other feature in the same `warlock add` invocation\r\n // still has to install, or the project is left half-wired on top of this.\r\n process.exitCode = 1;\r\n } else {\r\n await putFileAsync(srcPath(\"web/index.page.tsx\"), webHomePageStub);\r\n await putFileAsync(srcPath(\"web/index.register.ts\"), webHomeRegisterStub);\r\n\r\n // Unlike root.tsx above, the contact controller and its routes file are\r\n // ordinary application files a project can already have — independently\r\n // of ever having run `warlock add web`. Each is guarded on its OWN\r\n // existence, not on the (already-consumed) root.tsx sentinel, so an\r\n // existing `contact` module is skipped rather than clobbered. See\r\n // resolveContactScaffold's doc comment for why.\r\n const contactPlan = resolveContactScaffold({\r\n controllerExists: await fileExistsAsync(\r\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\r\n ),\r\n routesExists: await fileExistsAsync(srcPath(\"app/contact/routes.ts\")),\r\n });\r\n\r\n if (contactPlan.writeController) {\r\n await ensureDirectoryAsync(srcPath(\"app/contact/controllers\"));\r\n await putFileAsync(\r\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\r\n webContactControllerStub,\r\n );\r\n }\r\n\r\n if (contactPlan.writeRoutes) {\r\n await ensureDirectoryAsync(srcPath(\"app/contact\"));\r\n await putFileAsync(srcPath(\"app/contact/routes.ts\"), webContactRoutesStub);\r\n }\r\n\r\n console.log(`${colors.green(\"✓\")} Created src/web/index.page.tsx`);\r\n\r\n for (const message of contactPlan.messages) {\r\n console.log(message);\r\n }\r\n }\r\n }\r\n\r\n await registerWebConnector();\r\n}\r\n\r\nexport const webFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/web — SSR React pages served by the Warlock HTTP server. Scaffolds src/web (root.tsx + a home page) and registers the WebConnector in warlock.config.ts. Pages are opt-in: a Warlock app is an API until you add this.\",\r\n dependencies: {\r\n \"@warlock.js/web\": INSTALLED_WARLOCK_VERSION,\r\n \"@mongez/http\": \"^3.5.0\",\r\n \"@mongez/react-form\": \"^4.0.0\",\r\n \"@mongez/react-localization\": \"^3.4.7\",\r\n react: \"^19.2.3\",\r\n \"react-dom\": \"^19.2.3\",\r\n },\r\n devDependencies: {\r\n \"@types/react\": \"^19.2.7\",\r\n \"@types/react-dom\": \"^19.2.3\",\r\n // Loaded through `await import()` by the dev server only, so both are\r\n // optional peers of `web` rather than hard dependencies.\r\n vite: \"^7.3.5\",\r\n \"@vitejs/plugin-react\": \"^5.2.0\",\r\n },\r\n onExecuting: completeWebInstallation,\r\n};\r\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"request-memo.mjs","names":[],"sources":["../../../../../../../../core/src/http/context/request-memo.ts"],"sourcesContent":["import { requestContext, type RequestContextStore } from \"./request-context\";\n\n/**\n * In-flight/settled promises for the current request, keyed by the caller's\n * key. Never a process-global map: the outer key is the request's own store\n * object (fresh per request, see `RequestContext.buildStore`), so two\n * concurrent requests can never share — or even see — each other's entries,\n * and everything is GC-eligible once the request's ALS frame ends.\n */\nconst requestMemoStores = new WeakMap<RequestContextStore, Map<string, Promise<any>>>();\n\n/**\n * Single-flight, request-scoped memoization.\n *\n * The first call for a given `key` within the current request runs `fn` once\n * and caches the resulting promise; any call for the same `key` issued while\n * that promise is still pending — or after it has resolved — gets that exact\n * promise back, so parallel loaders that all need the same value never issue\n * duplicate work. A rejection is evicted immediately, so the next call for\n * that `key` retries `fn` from scratch instead of caching the failure.\n *\n * Must run inside a request context (established by\n * `createRequestStore`/`contextManager.runAll` for every HTTP request). There\n * is deliberately no process-global fallback — that is the exact cross-request\n * leak this helper exists to prevent — so calling it outside a request\n * context throws instead of silently memoizing across requests.\n */\nexport function requestMemo<T>(key: string, fn: () => Promise<T>): Promise<T> {\n const store = requestContext.getStore();\n\n if (!store) {\n throw new Error(\n \"requestMemo() was called outside a request context. \" +\n \"It caches work for the lifetime of a single request and must run inside \" +\n \"code executed by the HTTP request pipeline (middleware, a loader, or a \" +\n \"controller) — call it from there. If you need to share a value across \" +\n \"requests, use a process-level cache instead; requestMemo will not fall \" +\n \"back to one.\",\n );\n }\n\n let entries = requestMemoStores.get(store);\n\n if (!entries) {\n entries = new Map();\n requestMemoStores.set(store, entries);\n }\n\n const cached = entries.get(key);\n\n if (cached) return cached;\n\n const promise = (async () => fn())().catch(error => {\n entries!.delete(key);\n throw error;\n });\n\n entries.set(key, promise);\n\n return promise;\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,oCAAoB,IAAI,QAAwD;;;;;;;;;;;;;;;;;AAkBtF,SAAgB,YAAe,KAAa,IAAkC;CAC5E,MAAM,QAAQ,eAAe,SAAS;CAEtC,IAAI,CAAC,OACH,MAAM,IAAI,MACR,8VAMF;CAGF,IAAI,UAAU,kBAAkB,IAAI,KAAK;CAEzC,IAAI,CAAC,SAAS;EACZ,0BAAU,IAAI,IAAI;EAClB,kBAAkB,IAAI,OAAO,OAAO;CACtC;CAEA,MAAM,SAAS,QAAQ,IAAI,GAAG;CAE9B,IAAI,QAAQ,OAAO;CAEnB,MAAM,WAAW,YAAY,GAAG,EAAC,CAAE,CAAC,CAAC,OAAM,UAAS;EAClD,QAAS,OAAO,GAAG;EACnB,MAAM;CACR,CAAC;CAED,QAAQ,IAAI,KAAK,OAAO;CAExB,OAAO;AACT"}
1
+ {"version":3,"file":"request-memo.mjs","names":[],"sources":["../../../../../../../../core/src/http/context/request-memo.ts"],"sourcesContent":["import { requestContext, type RequestContextStore } from \"./request-context\";\n\n/**\n * In-flight/settled promises for the current request, keyed by the caller's\n * key. Never a process-global map: the outer key is the request's own store\n * object (fresh per request, see `RequestContext.buildStore`), so two\n * concurrent requests can never share — or even see — each other's entries,\n * and everything is GC-eligible once the request's ALS frame ends.\n */\nconst requestMemoStores = new WeakMap<RequestContextStore, Map<string, Promise<any>>>();\n\n/**\n * Single-flight, request-scoped memoization.\n *\n * The first call for a given `key` within the current request runs `fn` once\n * and caches the resulting promise; any call for the same `key` issued while\n * that promise is still pending — or after it has resolved — gets that exact\n * promise back, so parallel loaders that all need the same value never issue\n * duplicate work. A rejection is evicted immediately, so the next call for\n * that `key` retries `fn` from scratch instead of caching the failure.\n *\n * Must run inside a request context (established by\n * `createRequestStore`/`contextManager.runAll` for every HTTP request). There\n * is deliberately no process-global fallback — that is the exact cross-request\n * leak this helper exists to prevent — so calling it outside a request\n * context throws instead of silently memoizing across requests.\n */\nexport function requestMemo<T>(key: string, fn: () => Promise<T>): Promise<T> {\n const store = requestContext.getStore();\n\n if (!store) {\n throw new Error(\n \"requestMemo() was called outside a request context. \" +\n \"It caches work for the lifetime of a single request and must run inside \" +\n \"code executed by the HTTP request pipeline (middleware, a loader, or a \" +\n \"controller) — call it from there. If you need to share a value across \" +\n \"requests, use a process-level cache instead; requestMemo will not fall \" +\n \"back to one.\",\n );\n }\n\n let entries = requestMemoStores.get(store);\n\n if (!entries) {\n entries = new Map();\n requestMemoStores.set(store, entries);\n }\n\n const cached = entries.get(key);\n\n if (cached) return cached;\n\n const promise = (async () => fn())().catch((error) => {\n entries!.delete(key);\n throw error;\n });\n\n entries.set(key, promise);\n\n return promise;\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,oCAAoB,IAAI,QAAwD;;;;;;;;;;;;;;;;;AAkBtF,SAAgB,YAAe,KAAa,IAAkC;CAC5E,MAAM,QAAQ,eAAe,SAAS;CAEtC,IAAI,CAAC,OACH,MAAM,IAAI,MACR,8VAMF;CAGF,IAAI,UAAU,kBAAkB,IAAI,KAAK;CAEzC,IAAI,CAAC,SAAS;EACZ,0BAAU,IAAI,IAAI;EAClB,kBAAkB,IAAI,OAAO,OAAO;CACtC;CAEA,MAAM,SAAS,QAAQ,IAAI,GAAG;CAE9B,IAAI,QAAQ,OAAO;CAEnB,MAAM,WAAW,YAAY,GAAG,EAAC,CAAE,CAAC,CAAC,OAAO,UAAU;EACpD,QAAS,OAAO,GAAG;EACnB,MAAM;CACR,CAAC;CAED,QAAQ,IAAI,KAAK,OAAO;CAExB,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"cache-response-middleware.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/cache-response-middleware.ts"],"mappings":";;;;KAsBY,sBAAA;;AAAZ;;EAIE,QAAA,aAEM,OAAA,EAAS,OAAA,iBACT,OAAA,EAAS,OAAA,KAAY,OAAA;EADZ;;;;;;EAQf,UAAA;EARM;;;;;EAcN,IAAA;EAIA;;;EAAA,GAAA;EAkDc;;;;;;EA3Cd,MAAA;AAAA;AAAA,iBA2Cc,eAAA,CACd,oBAAA,EAAsB,sBAAA,YACrB,UAAU"}
1
+ {"version":3,"file":"cache-response-middleware.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/cache-response-middleware.ts"],"mappings":";;;;KAsBY,sBAAA;;AAAZ;;EAIE,QAAA,aAAqB,OAAA,EAAS,OAAA,iBAAwB,OAAA,EAAS,OAAA,KAAY,OAAA;EAA7C;;;;;;EAO9B,UAAA;EAPqB;;;;;EAarB,IAAA;EAIA;;;EAAA,GAAA;EA+Cc;;;;;;EAxCd,MAAA;AAAA;AAAA,iBAwCc,eAAA,CAAgB,oBAAA,EAAsB,sBAAA,YAAkC,UAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"cache-response-middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/cache-response-middleware.ts"],"sourcesContent":["import { except } from \"@mongez/reinforcements\";\nimport { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport type { Middleware } from \"../../router/types\";\nimport type { Request } from \"./../request\";\nimport type { Response } from \"./../response\";\n\n/**\n * Shape persisted to the cache for a cached response. Stores the status and\n * content-type alongside the body so the HIT path can replay the response\n * faithfully via {@link Response.replay} instead of re-entering `send()`.\n */\ntype CachedResponsePayload = {\n status: number;\n data: unknown;\n contentType?: string;\n};\n\n// TODO: Add option to determine whether to cache the response or not\n// TODO: add option to determine what to be cached from the response\n// TODO: add cache middleware config options for example to set the default driver, ttl, etc\n\nexport type CacheMiddlewareOptions = {\n /**\n * Cache key\n */\n cacheKey:\n | string\n | ((request: Request) => string)\n | ((request: Request) => Promise<string>);\n /**\n * If true, then the response will be cached based on the current locale code\n * This is useful when you have a multi-language website, and you want to cache the response based on the current locale\n *\n * @default true\n */\n withLocale?: boolean;\n /**\n * List of keys from the response object to omit from the cached response\n *\n * @default ['user']\n */\n omit?: string[];\n /**\n * Expires after number of seconds\n */\n ttl?: number;\n /**\n * Cache driver\n *\n * @see config/cache.ts: drivers object\n * @default cache manager\n */\n driver?: string;\n};\n\nconst defaultCacheOptions: Partial<CacheMiddlewareOptions> = {\n withLocale: true,\n};\n\ntype ParsedCacheOptions = Required<CacheMiddlewareOptions> & {\n cacheKey: string;\n};\n\nasync function parseCacheOptions(\n cacheOptions: CacheMiddlewareOptions | string,\n request: Request,\n) {\n if (typeof cacheOptions === \"string\") {\n cacheOptions = {\n cacheKey: cacheOptions,\n };\n }\n\n if (typeof cacheOptions.cacheKey === \"function\") {\n cacheOptions.cacheKey = await cacheOptions.cacheKey(request);\n }\n\n const finalCacheOptions = {\n ...defaultCacheOptions,\n ...cacheOptions,\n } as ParsedCacheOptions;\n\n if (finalCacheOptions.withLocale) {\n const locale = request.getLocaleCode();\n\n finalCacheOptions.cacheKey = `${finalCacheOptions.cacheKey}:${locale}`;\n }\n\n if (!finalCacheOptions.omit) {\n finalCacheOptions.omit = [\"user\", \"settings\"];\n }\n\n return finalCacheOptions;\n}\n\nexport function cacheMiddleware(\n responseCacheOptions: CacheMiddlewareOptions | string,\n): Middleware {\n // The `Middleware` return annotation is load-bearing: without it, tsc never\n // checks this factory's calling convention, which is how the positional v4\n // shape survived an earlier refactor unnoticed.\n return async function ({ request, response }) {\n const { ttl, omit, cacheKey, driver } = await parseCacheOptions(\n responseCacheOptions,\n request,\n );\n const cacheDriver = driver ? await cache.use(driver) : cache;\n\n const content = (await cacheDriver.get(cacheKey)) as CachedResponsePayload | null;\n\n if (content) {\n // Replay through the standard pipeline (status + content-type preserved)\n // instead of `baseResponse.send()`, which would re-enter Response.send()\n // on an already-sent reply, trip the double-send guard, and drop the\n // status / content-type.\n return response.replay({\n status: content.status ?? 200,\n body: content.data,\n contentType: content.contentType,\n });\n }\n\n response.onSent((response: Response) => {\n if (!response.isOk || response.request.path !== request.path) {\n return;\n }\n\n const sentContentType = response.contentType;\n\n const content: CachedResponsePayload = {\n status: response.statusCode,\n data: except(response.parsedBody, omit),\n contentType: typeof sentContentType === \"string\" ? sentContentType : undefined,\n };\n\n // `set` is fire-and-forget inside `onSent`; without a `.catch` a rejected\n // write (e.g. Redis down) would surface as an unhandledRejection.\n cacheDriver.set(cacheKey, content, ttl).catch((error: unknown) => {\n log.error(\"cache-middleware\", \"set\", error);\n });\n });\n };\n}\n"],"mappings":";;;;;AAwDA,MAAM,sBAAuD,EAC3D,YAAY,KACd;AAMA,eAAe,kBACb,cACA,SACA;CACA,IAAI,OAAO,iBAAiB,UAC1B,eAAe,EACb,UAAU,aACZ;CAGF,IAAI,OAAO,aAAa,aAAa,YACnC,aAAa,WAAW,MAAM,aAAa,SAAS,OAAO;CAG7D,MAAM,oBAAoB;EACxB,GAAG;EACH,GAAG;CACL;CAEA,IAAI,kBAAkB,YAAY;EAChC,MAAM,SAAS,QAAQ,cAAc;EAErC,kBAAkB,WAAW,GAAG,kBAAkB,SAAS,GAAG;CAChE;CAEA,IAAI,CAAC,kBAAkB,MACrB,kBAAkB,OAAO,CAAC,QAAQ,UAAU;CAG9C,OAAO;AACT;AAEA,SAAgB,gBACd,sBACY;CAIZ,OAAO,eAAgB,EAAE,SAAS,YAAY;EAC5C,MAAM,EAAE,KAAK,MAAM,UAAU,WAAW,MAAM,kBAC5C,sBACA,OACF;EACA,MAAM,cAAc,SAAS,MAAM,MAAM,IAAI,MAAM,IAAI;EAEvD,MAAM,UAAW,MAAM,YAAY,IAAI,QAAQ;EAE/C,IAAI,SAKF,OAAO,SAAS,OAAO;GACrB,QAAQ,QAAQ,UAAU;GAC1B,MAAM,QAAQ;GACd,aAAa,QAAQ;EACvB,CAAC;EAGH,SAAS,QAAQ,aAAuB;GACtC,IAAI,CAAC,SAAS,QAAQ,SAAS,QAAQ,SAAS,QAAQ,MACtD;GAGF,MAAM,kBAAkB,SAAS;GAEjC,MAAM,UAAiC;IACrC,QAAQ,SAAS;IACjB,MAAM,OAAO,SAAS,YAAY,IAAI;IACtC,aAAa,OAAO,oBAAoB,WAAW,kBAAkB;GACvE;GAIA,YAAY,IAAI,UAAU,SAAS,GAAG,CAAC,CAAC,OAAO,UAAmB;IAChE,IAAI,MAAM,oBAAoB,OAAO,KAAK;GAC5C,CAAC;EACH,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"cache-response-middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/cache-response-middleware.ts"],"sourcesContent":["import { except } from \"@mongez/reinforcements\";\nimport { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport type { Middleware } from \"../../router/types\";\nimport type { Request } from \"./../request\";\nimport type { Response } from \"./../response\";\n\n/**\n * Shape persisted to the cache for a cached response. Stores the status and\n * content-type alongside the body so the HIT path can replay the response\n * faithfully via {@link Response.replay} instead of re-entering `send()`.\n */\ntype CachedResponsePayload = {\n status: number;\n data: unknown;\n contentType?: string;\n};\n\n// TODO: Add option to determine whether to cache the response or not\n// TODO: add option to determine what to be cached from the response\n// TODO: add cache middleware config options for example to set the default driver, ttl, etc\n\nexport type CacheMiddlewareOptions = {\n /**\n * Cache key\n */\n cacheKey: string | ((request: Request) => string) | ((request: Request) => Promise<string>);\n /**\n * If true, then the response will be cached based on the current locale code\n * This is useful when you have a multi-language website, and you want to cache the response based on the current locale\n *\n * @default true\n */\n withLocale?: boolean;\n /**\n * List of keys from the response object to omit from the cached response\n *\n * @default ['user']\n */\n omit?: string[];\n /**\n * Expires after number of seconds\n */\n ttl?: number;\n /**\n * Cache driver\n *\n * @see config/cache.ts: drivers object\n * @default cache manager\n */\n driver?: string;\n};\n\nconst defaultCacheOptions: Partial<CacheMiddlewareOptions> = {\n withLocale: true,\n};\n\ntype ParsedCacheOptions = Required<CacheMiddlewareOptions> & {\n cacheKey: string;\n};\n\nasync function parseCacheOptions(cacheOptions: CacheMiddlewareOptions | string, request: Request) {\n if (typeof cacheOptions === \"string\") {\n cacheOptions = {\n cacheKey: cacheOptions,\n };\n }\n\n if (typeof cacheOptions.cacheKey === \"function\") {\n cacheOptions.cacheKey = await cacheOptions.cacheKey(request);\n }\n\n const finalCacheOptions = {\n ...defaultCacheOptions,\n ...cacheOptions,\n } as ParsedCacheOptions;\n\n if (finalCacheOptions.withLocale) {\n const locale = request.getLocaleCode();\n\n finalCacheOptions.cacheKey = `${finalCacheOptions.cacheKey}:${locale}`;\n }\n\n if (!finalCacheOptions.omit) {\n finalCacheOptions.omit = [\"user\", \"settings\"];\n }\n\n return finalCacheOptions;\n}\n\nexport function cacheMiddleware(responseCacheOptions: CacheMiddlewareOptions | string): Middleware {\n // The `Middleware` return annotation is load-bearing: without it, tsc never\n // checks this factory's calling convention, which is how the positional v4\n // shape survived an earlier refactor unnoticed.\n return async function ({ request, response }) {\n const { ttl, omit, cacheKey, driver } = await parseCacheOptions(responseCacheOptions, request);\n const cacheDriver = driver ? await cache.use(driver) : cache;\n\n const content = (await cacheDriver.get(cacheKey)) as CachedResponsePayload | null;\n\n if (content) {\n // Replay through the standard pipeline (status + content-type preserved)\n // instead of `baseResponse.send()`, which would re-enter Response.send()\n // on an already-sent reply, trip the double-send guard, and drop the\n // status / content-type.\n return response.replay({\n status: content.status ?? 200,\n body: content.data,\n contentType: content.contentType,\n });\n }\n\n response.onSent((response: Response) => {\n if (!response.isOk || response.request.path !== request.path) {\n return;\n }\n\n const sentContentType = response.contentType;\n\n const content: CachedResponsePayload = {\n status: response.statusCode,\n data: except(response.parsedBody, omit),\n contentType: typeof sentContentType === \"string\" ? sentContentType : undefined,\n };\n\n // `set` is fire-and-forget inside `onSent`; without a `.catch` a rejected\n // write (e.g. Redis down) would surface as an unhandledRejection.\n cacheDriver.set(cacheKey, content, ttl).catch((error: unknown) => {\n log.error(\"cache-middleware\", \"set\", error);\n });\n });\n };\n}\n"],"mappings":";;;;;AAqDA,MAAM,sBAAuD,EAC3D,YAAY,KACd;AAMA,eAAe,kBAAkB,cAA+C,SAAkB;CAChG,IAAI,OAAO,iBAAiB,UAC1B,eAAe,EACb,UAAU,aACZ;CAGF,IAAI,OAAO,aAAa,aAAa,YACnC,aAAa,WAAW,MAAM,aAAa,SAAS,OAAO;CAG7D,MAAM,oBAAoB;EACxB,GAAG;EACH,GAAG;CACL;CAEA,IAAI,kBAAkB,YAAY;EAChC,MAAM,SAAS,QAAQ,cAAc;EAErC,kBAAkB,WAAW,GAAG,kBAAkB,SAAS,GAAG;CAChE;CAEA,IAAI,CAAC,kBAAkB,MACrB,kBAAkB,OAAO,CAAC,QAAQ,UAAU;CAG9C,OAAO;AACT;AAEA,SAAgB,gBAAgB,sBAAmE;CAIjG,OAAO,eAAgB,EAAE,SAAS,YAAY;EAC5C,MAAM,EAAE,KAAK,MAAM,UAAU,WAAW,MAAM,kBAAkB,sBAAsB,OAAO;EAC7F,MAAM,cAAc,SAAS,MAAM,MAAM,IAAI,MAAM,IAAI;EAEvD,MAAM,UAAW,MAAM,YAAY,IAAI,QAAQ;EAE/C,IAAI,SAKF,OAAO,SAAS,OAAO;GACrB,QAAQ,QAAQ,UAAU;GAC1B,MAAM,QAAQ;GACd,aAAa,QAAQ;EACvB,CAAC;EAGH,SAAS,QAAQ,aAAuB;GACtC,IAAI,CAAC,SAAS,QAAQ,SAAS,QAAQ,SAAS,QAAQ,MACtD;GAGF,MAAM,kBAAkB,SAAS;GAEjC,MAAM,UAAiC;IACrC,QAAQ,SAAS;IACjB,MAAM,OAAO,SAAS,YAAY,IAAI;IACtC,aAAa,OAAO,oBAAoB,WAAW,kBAAkB;GACvE;GAIA,YAAY,IAAI,UAAU,SAAS,GAAG,CAAC,CAAC,OAAO,UAAmB;IAChE,IAAI,MAAM,oBAAoB,OAAO,KAAK;GAC5C,CAAC;EACH,CAAC;CACH;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"concurrency-limit.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/concurrency-limit.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport type { Request } from \"../request\";\nimport type { Response } from \"../response\";\nimport { t } from \"./inject-request-context\";\n\n/**\n * Options for the concurrency limit middleware.\n */\nexport type ConcurrencyLimitOptions = {\n /**\n * Group key generator. Defaults to `request.route.path` — i.e. the cap\n * applies across all callers of that route. Override to scope per-user\n * or per-tenant.\n *\n * @example\n * keyGenerator: (request) => `${request.route.path}:${request.user?.id ?? request.ip}`,\n */\n keyGenerator?: (request: Request) => string;\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\nconst counters = new Map<string, number>();\n\nfunction release(key: string) {\n const after = (counters.get(key) || 1) - 1;\n\n if (after <= 0) {\n counters.delete(key);\n\n return;\n }\n\n counters.set(key, after);\n}\n\n/**\n * Cap the number of in-flight requests against a route. Above the cap, new\n * requests get a fast 429 + `Retry-After: 1` — no queue, no timeout.\n *\n * Use for endpoints whose cost is unbounded per-request: report generation,\n * AI completions, image processing, expensive aggregations. Different from\n * `rateLimit()` — rate-limit caps requests-per-time, concurrency caps\n * in-flight requests at any instant.\n *\n * The counter is process-local. With `N` replicas the effective cap is\n * `N × max`. Document this in the route's behavior; if you need shared\n * concurrency across replicas, reach for a `@warlock.js/cache` lock instead.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n *\n * router.post(\"/reports/generate\", reportController, {\n * middleware: [middleware.concurrencyLimit(3)],\n * });\n *\n * router.post(\"/ai/summarize\", summarizeController, {\n * middleware: [\n * middleware.concurrencyLimit(10, {\n * keyGenerator: (request) => `ai:${request.user?.id ?? request.ip}`,\n * }),\n * ],\n * });\n */\nexport function concurrencyLimitMiddleware(\n max: number,\n options: ConcurrencyLimitOptions = {},\n): Middleware {\n return ({ request, response }) => {\n const key = options.keyGenerator?.(request) || request.route.path;\n const current = counters.get(key) || 0;\n\n if (current >= max) {\n response.header(\"Retry-After\", 1);\n\n return response.tooManyRequests({\n error: options.errorMessage || t(\"http.concurrencyLimitReached\"),\n errorCode: HttpErrorCodes.ConcurrencyLimitReached,\n limit: max,\n },\n );\n }\n\n counters.set(key, current + 1);\n\n let released = false;\n\n const releaseOnce = () => {\n if (released) {\n return;\n }\n\n released = true;\n release(key);\n };\n\n // `onSent` only fires from Response.send()/stream().end()/sse().end().\n // Bare-reply paths (noContent, redirect, sendFile, download, sendBuffer,\n // raw send) never trigger it, so the slot would leak and the route would\n // permanently 429 after `max` such requests. Bind to the raw socket\n // lifecycle as well so the slot frees regardless of response path —\n // `releaseOnce` is idempotent, so double-firing is harmless.\n response.onSent((_sentResponse: Response) => releaseOnce());\n response.baseResponse.raw.once(\"finish\", releaseOnce);\n response.baseResponse.raw.once(\"close\", releaseOnce);\n };\n}\n"],"mappings":";;;;AAyBA,MAAM,2BAAW,IAAI,IAAoB;AAEzC,SAAS,QAAQ,KAAa;CAC5B,MAAM,SAAS,SAAS,IAAI,GAAG,KAAK,KAAK;CAEzC,IAAI,SAAS,GAAG;EACd,SAAS,OAAO,GAAG;EAEnB;CACF;CAEA,SAAS,IAAI,KAAK,KAAK;AACzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,2BACd,KACA,UAAmC,CAAC,GACxB;CACZ,QAAQ,EAAE,SAAS,eAAe;EAChC,MAAM,MAAM,QAAQ,eAAe,OAAO,KAAK,QAAQ,MAAM;EAC7D,MAAM,UAAU,SAAS,IAAI,GAAG,KAAK;EAErC,IAAI,WAAW,KAAK;GAClB,SAAS,OAAO,eAAe,CAAC;GAEhC,OAAO,SAAS,gBAAgB;IAC5B,OAAO,QAAQ,gBAAgB,EAAE,8BAA8B;IAC/D;IACA,OAAO;GACT,CACF;EACF;EAEA,SAAS,IAAI,KAAK,UAAU,CAAC;EAE7B,IAAI,WAAW;EAEf,MAAM,oBAAoB;GACxB,IAAI,UACF;GAGF,WAAW;GACX,QAAQ,GAAG;EACb;EAQA,SAAS,QAAQ,kBAA4B,YAAY,CAAC;EAC1D,SAAS,aAAa,IAAI,KAAK,UAAU,WAAW;EACpD,SAAS,aAAa,IAAI,KAAK,SAAS,WAAW;CACrD;AACF"}
1
+ {"version":3,"file":"concurrency-limit.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/concurrency-limit.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport type { Request } from \"../request\";\nimport type { Response } from \"../response\";\nimport { t } from \"./inject-request-context\";\n\n/**\n * Options for the concurrency limit middleware.\n */\nexport type ConcurrencyLimitOptions = {\n /**\n * Group key generator. Defaults to `request.route.path` — i.e. the cap\n * applies across all callers of that route. Override to scope per-user\n * or per-tenant.\n *\n * @example\n * keyGenerator: (request) => `${request.route.path}:${request.user?.id ?? request.ip}`,\n */\n keyGenerator?: (request: Request) => string;\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\nconst counters = new Map<string, number>();\n\nfunction release(key: string) {\n const after = (counters.get(key) || 1) - 1;\n\n if (after <= 0) {\n counters.delete(key);\n\n return;\n }\n\n counters.set(key, after);\n}\n\n/**\n * Cap the number of in-flight requests against a route. Above the cap, new\n * requests get a fast 429 + `Retry-After: 1` — no queue, no timeout.\n *\n * Use for endpoints whose cost is unbounded per-request: report generation,\n * AI completions, image processing, expensive aggregations. Different from\n * `rateLimit()` — rate-limit caps requests-per-time, concurrency caps\n * in-flight requests at any instant.\n *\n * The counter is process-local. With `N` replicas the effective cap is\n * `N × max`. Document this in the route's behavior; if you need shared\n * concurrency across replicas, reach for a `@warlock.js/cache` lock instead.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n *\n * router.post(\"/reports/generate\", reportController, {\n * middleware: [middleware.concurrencyLimit(3)],\n * });\n *\n * router.post(\"/ai/summarize\", summarizeController, {\n * middleware: [\n * middleware.concurrencyLimit(10, {\n * keyGenerator: (request) => `ai:${request.user?.id ?? request.ip}`,\n * }),\n * ],\n * });\n */\nexport function concurrencyLimitMiddleware(\n max: number,\n options: ConcurrencyLimitOptions = {},\n): Middleware {\n return ({ request, response }) => {\n const key = options.keyGenerator?.(request) || request.route.path;\n const current = counters.get(key) || 0;\n\n if (current >= max) {\n response.header(\"Retry-After\", 1);\n\n return response.tooManyRequests({\n error: options.errorMessage || t(\"http.concurrencyLimitReached\"),\n errorCode: HttpErrorCodes.ConcurrencyLimitReached,\n limit: max,\n });\n }\n\n counters.set(key, current + 1);\n\n let released = false;\n\n const releaseOnce = () => {\n if (released) {\n return;\n }\n\n released = true;\n release(key);\n };\n\n // `onSent` only fires from Response.send()/stream().end()/sse().end().\n // Bare-reply paths (noContent, redirect, sendFile, download, sendBuffer,\n // raw send) never trigger it, so the slot would leak and the route would\n // permanently 429 after `max` such requests. Bind to the raw socket\n // lifecycle as well so the slot frees regardless of response path —\n // `releaseOnce` is idempotent, so double-firing is harmless.\n response.onSent((_sentResponse: Response) => releaseOnce());\n response.baseResponse.raw.once(\"finish\", releaseOnce);\n response.baseResponse.raw.once(\"close\", releaseOnce);\n };\n}\n"],"mappings":";;;;AAyBA,MAAM,2BAAW,IAAI,IAAoB;AAEzC,SAAS,QAAQ,KAAa;CAC5B,MAAM,SAAS,SAAS,IAAI,GAAG,KAAK,KAAK;CAEzC,IAAI,SAAS,GAAG;EACd,SAAS,OAAO,GAAG;EAEnB;CACF;CAEA,SAAS,IAAI,KAAK,KAAK;AACzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,2BACd,KACA,UAAmC,CAAC,GACxB;CACZ,QAAQ,EAAE,SAAS,eAAe;EAChC,MAAM,MAAM,QAAQ,eAAe,OAAO,KAAK,QAAQ,MAAM;EAC7D,MAAM,UAAU,SAAS,IAAI,GAAG,KAAK;EAErC,IAAI,WAAW,KAAK;GAClB,SAAS,OAAO,eAAe,CAAC;GAEhC,OAAO,SAAS,gBAAgB;IAC9B,OAAO,QAAQ,gBAAgB,EAAE,8BAA8B;IAC/D;IACA,OAAO;GACT,CAAC;EACH;EAEA,SAAS,IAAI,KAAK,UAAU,CAAC;EAE7B,IAAI,WAAW;EAEf,MAAM,oBAAoB;GACxB,IAAI,UACF;GAGF,WAAW;GACX,QAAQ,GAAG;EACb;EAQA,SAAS,QAAQ,kBAA4B,YAAY,CAAC;EAC1D,SAAS,aAAa,IAAI,KAAK,UAAU,WAAW;EACpD,SAAS,aAAa,IAAI,KAAK,SAAS,WAAW;CACrD;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency.middleware.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/idempotency.middleware.ts"],"mappings":";;;;;KAgBY,kBAAA;EAAkB;;;EAI5B,GAAA;EAKA;;;;EAAA,UAAA;EAqDc;;;;;EA/Cd,OAAA;EA+CuE;;AAAU;;EA1CjF,MAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0Cc,qBAAA,CAAsB,OAAA,GAAS,kBAAA,GAA0B,UAAU"}
1
+ {"version":3,"file":"idempotency.middleware.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/idempotency.middleware.ts"],"mappings":";;;;;KAYY,kBAAA;EAAkB;;;EAI5B,GAAA;EAKA;;;;EAAA,UAAA;EAqDc;;;;;EA/Cd,OAAA;EA+CuE;;AAAU;;EA1CjF,MAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0Cc,qBAAA,CAAsB,OAAA,GAAS,kBAAA,GAA0B,UAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/idempotency.middleware.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport type { Response } from \"../response\";\nimport { t } from \"./inject-request-context\";\nimport {\n buildIdempotencyCacheKey,\n hashBody,\n isValidIdempotencyKey,\n} from \"./utils/idempotency-key\";\n\n/**\n * Options for the idempotency middleware.\n */\nexport type IdempotencyOptions = {\n /**\n * Cache TTL in seconds. Falls back to `http.idempotency.ttl`, then `86400` (24h).\n */\n ttl?: number;\n /**\n * Header name carrying the client's key. Falls back to\n * `http.idempotency.headerName`, then `\"Idempotency-Key\"`.\n */\n headerName?: string;\n /**\n * HTTP methods eligible for idempotency. Falls back to\n * `http.idempotency.methods`, then `[\"POST\",\"PUT\",\"PATCH\",\"DELETE\"]`.\n * Safe methods (GET/HEAD) are skipped regardless.\n */\n methods?: string[];\n /**\n * Cache driver name. Falls back to `http.idempotency.driver`, then the\n * default driver of the cache manager.\n */\n driver?: string;\n};\n\ntype CachedResponse = {\n status: number;\n body: unknown;\n bodyHash: string;\n contentType?: string;\n};\n\nconst DEFAULT_METHODS = [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"];\n\n/**\n * Dedupe non-idempotent writes by an `Idempotency-Key` header — same key,\n * same body, within TTL → cached replay; same key, different body → 422\n * `IdempotencyKeyConflict`.\n *\n * **Must run after `authMiddleware`** — the cache key is scoped per-user\n * (`idem:{userType}:{userId}:{key}`) so user A can't replay user B's key.\n * Anonymous requests fall back to IP scope.\n *\n * The replay sets `Idempotent-Replay: true` on the response for easy\n * client-side / observability detection.\n *\n * Eligible methods default to POST/PUT/PATCH/DELETE. GET/HEAD pass through\n * even with the header set (RFC: safe methods are already idempotent).\n *\n * @example\n * import { authMiddleware } from \"@warlock.js/auth\";\n * import { middleware } from \"@warlock.js/core\";\n *\n * router.post(\"/orders\", createOrderController, {\n * middleware: [authMiddleware(\"client\"), middleware.idempotency()],\n * });\n *\n * router.post(\"/ai/summarize\", summarizeController, {\n * middleware: [\n * authMiddleware(\"client\"),\n * middleware.idempotency({ ttl: 60 * 60 }), // 1h is enough for client retries\n * ],\n * });\n */\nexport function idempotencyMiddleware(options: IdempotencyOptions = {}): Middleware {\n return async ({ request, response }) => {\n const headerName =\n options.headerName ||\n config.get(\"http.idempotency.headerName\", \"Idempotency-Key\");\n const methods =\n options.methods || config.get(\"http.idempotency.methods\", DEFAULT_METHODS);\n const ttl = options.ttl || config.get(\"http.idempotency.ttl\", 86400);\n const driverName = options.driver || config.get(\"http.idempotency.driver\");\n\n if (!methods.includes(request.method.toUpperCase())) return;\n\n const idempotencyKey = request.header(headerName.toLowerCase());\n\n if (!idempotencyKey) return;\n\n if (!isValidIdempotencyKey(idempotencyKey)) {\n return response.badRequest({\n error: t(\"http.idempotencyKeyInvalid\"),\n errorCode: HttpErrorCodes.IdempotencyKeyInvalid,\n });\n }\n\n const cacheDriver = driverName ? await cache.use(driverName) : cache;\n const cacheKey = buildIdempotencyCacheKey(request, idempotencyKey);\n const bodyHash = hashBody(request.body);\n\n const cached = (await cacheDriver.get(cacheKey)) as CachedResponse | null;\n\n if (cached) {\n if (cached.bodyHash !== bodyHash) {\n return response.unprocessableEntity({\n error: t(\"http.idempotencyKeyConflict\"),\n errorCode: HttpErrorCodes.IdempotencyKeyConflict,\n });\n }\n\n response.header(\"Idempotent-Replay\", \"true\");\n\n return response.replay({\n status: cached.status,\n body: cached.body,\n contentType: cached.contentType,\n });\n }\n\n response.onSent((sentResponse: Response) => {\n // Don't cache server errors — clients should be able to retry past a 5xx.\n // 4xx are deterministic outcomes of the request, so caching is fine.\n if (sentResponse.statusCode >= 500) return;\n\n const sentContentType = sentResponse.contentType;\n\n // `set` is fire-and-forget inside `onSent`; without a `.catch` a rejected\n // write (e.g. Redis down) would surface as an unhandledRejection.\n cacheDriver\n .set(\n cacheKey,\n {\n status: sentResponse.statusCode,\n body: sentResponse.parsedBody,\n bodyHash,\n contentType: typeof sentContentType === \"string\" ? sentContentType : undefined,\n },\n ttl,\n )\n .catch((error: unknown) => {\n log.error(\"idempotency-middleware\", \"set\", error);\n });\n });\n };\n}\n"],"mappings":";;;;;;;;AA8CA,MAAM,kBAAkB;CAAC;CAAQ;CAAO;CAAS;AAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCzD,SAAgB,sBAAsB,UAA8B,CAAC,GAAe;CAClF,OAAO,OAAO,EAAE,SAAS,eAAe;EACtC,MAAM,aACJ,QAAQ,cACR,OAAO,IAAI,+BAA+B,iBAAiB;EAC7D,MAAM,UACJ,QAAQ,WAAW,OAAO,IAAI,4BAA4B,eAAe;EAC3E,MAAM,MAAM,QAAQ,OAAO,OAAO,IAAI,wBAAwB,KAAK;EACnE,MAAM,aAAa,QAAQ,UAAU,OAAO,IAAI,yBAAyB;EAEzE,IAAI,CAAC,QAAQ,SAAS,QAAQ,OAAO,YAAY,CAAC,GAAG;EAErD,MAAM,iBAAiB,QAAQ,OAAO,WAAW,YAAY,CAAC;EAE9D,IAAI,CAAC,gBAAgB;EAErB,IAAI,CAAC,sBAAsB,cAAc,GACvC,OAAO,SAAS,WAAW;GACzB,OAAO,EAAE,4BAA4B;GACrC;EACF,CAAC;EAGH,MAAM,cAAc,aAAa,MAAM,MAAM,IAAI,UAAU,IAAI;EAC/D,MAAM,WAAW,yBAAyB,SAAS,cAAc;EACjE,MAAM,WAAW,SAAS,QAAQ,IAAI;EAEtC,MAAM,SAAU,MAAM,YAAY,IAAI,QAAQ;EAE9C,IAAI,QAAQ;GACV,IAAI,OAAO,aAAa,UACtB,OAAO,SAAS,oBAAoB;IAClC,OAAO,EAAE,6BAA6B;IACtC;GACF,CAAC;GAGH,SAAS,OAAO,qBAAqB,MAAM;GAE3C,OAAO,SAAS,OAAO;IACrB,QAAQ,OAAO;IACf,MAAM,OAAO;IACb,aAAa,OAAO;GACtB,CAAC;EACH;EAEA,SAAS,QAAQ,iBAA2B;GAG1C,IAAI,aAAa,cAAc,KAAK;GAEpC,MAAM,kBAAkB,aAAa;GAIrC,YACG,IACC,UACA;IACE,QAAQ,aAAa;IACrB,MAAM,aAAa;IACnB;IACA,aAAa,OAAO,oBAAoB,WAAW,kBAAkB;GACvE,GACA,GACF,CAAC,CACA,OAAO,UAAmB;IACzB,IAAI,MAAM,0BAA0B,OAAO,KAAK;GAClD,CAAC;EACL,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"idempotency.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/idempotency.middleware.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport type { Response } from \"../response\";\nimport { t } from \"./inject-request-context\";\nimport { buildIdempotencyCacheKey, hashBody, isValidIdempotencyKey } from \"./utils/idempotency-key\";\n\n/**\n * Options for the idempotency middleware.\n */\nexport type IdempotencyOptions = {\n /**\n * Cache TTL in seconds. Falls back to `http.idempotency.ttl`, then `86400` (24h).\n */\n ttl?: number;\n /**\n * Header name carrying the client's key. Falls back to\n * `http.idempotency.headerName`, then `\"Idempotency-Key\"`.\n */\n headerName?: string;\n /**\n * HTTP methods eligible for idempotency. Falls back to\n * `http.idempotency.methods`, then `[\"POST\",\"PUT\",\"PATCH\",\"DELETE\"]`.\n * Safe methods (GET/HEAD) are skipped regardless.\n */\n methods?: string[];\n /**\n * Cache driver name. Falls back to `http.idempotency.driver`, then the\n * default driver of the cache manager.\n */\n driver?: string;\n};\n\ntype CachedResponse = {\n status: number;\n body: unknown;\n bodyHash: string;\n contentType?: string;\n};\n\nconst DEFAULT_METHODS = [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"];\n\n/**\n * Dedupe non-idempotent writes by an `Idempotency-Key` header — same key,\n * same body, within TTL → cached replay; same key, different body → 422\n * `IdempotencyKeyConflict`.\n *\n * **Must run after `authMiddleware`** — the cache key is scoped per-user\n * (`idem:{userType}:{userId}:{key}`) so user A can't replay user B's key.\n * Anonymous requests fall back to IP scope.\n *\n * The replay sets `Idempotent-Replay: true` on the response for easy\n * client-side / observability detection.\n *\n * Eligible methods default to POST/PUT/PATCH/DELETE. GET/HEAD pass through\n * even with the header set (RFC: safe methods are already idempotent).\n *\n * @example\n * import { authMiddleware } from \"@warlock.js/auth\";\n * import { middleware } from \"@warlock.js/core\";\n *\n * router.post(\"/orders\", createOrderController, {\n * middleware: [authMiddleware(\"client\"), middleware.idempotency()],\n * });\n *\n * router.post(\"/ai/summarize\", summarizeController, {\n * middleware: [\n * authMiddleware(\"client\"),\n * middleware.idempotency({ ttl: 60 * 60 }), // 1h is enough for client retries\n * ],\n * });\n */\nexport function idempotencyMiddleware(options: IdempotencyOptions = {}): Middleware {\n return async ({ request, response }) => {\n const headerName =\n options.headerName || config.get(\"http.idempotency.headerName\", \"Idempotency-Key\");\n const methods = options.methods || config.get(\"http.idempotency.methods\", DEFAULT_METHODS);\n const ttl = options.ttl || config.get(\"http.idempotency.ttl\", 86400);\n const driverName = options.driver || config.get(\"http.idempotency.driver\");\n\n if (!methods.includes(request.method.toUpperCase())) return;\n\n const idempotencyKey = request.header(headerName.toLowerCase());\n\n if (!idempotencyKey) return;\n\n if (!isValidIdempotencyKey(idempotencyKey)) {\n return response.badRequest({\n error: t(\"http.idempotencyKeyInvalid\"),\n errorCode: HttpErrorCodes.IdempotencyKeyInvalid,\n });\n }\n\n const cacheDriver = driverName ? await cache.use(driverName) : cache;\n const cacheKey = buildIdempotencyCacheKey(request, idempotencyKey);\n const bodyHash = hashBody(request.body);\n\n const cached = (await cacheDriver.get(cacheKey)) as CachedResponse | null;\n\n if (cached) {\n if (cached.bodyHash !== bodyHash) {\n return response.unprocessableEntity({\n error: t(\"http.idempotencyKeyConflict\"),\n errorCode: HttpErrorCodes.IdempotencyKeyConflict,\n });\n }\n\n response.header(\"Idempotent-Replay\", \"true\");\n\n return response.replay({\n status: cached.status,\n body: cached.body,\n contentType: cached.contentType,\n });\n }\n\n response.onSent((sentResponse: Response) => {\n // Don't cache server errors — clients should be able to retry past a 5xx.\n // 4xx are deterministic outcomes of the request, so caching is fine.\n if (sentResponse.statusCode >= 500) return;\n\n const sentContentType = sentResponse.contentType;\n\n // `set` is fire-and-forget inside `onSent`; without a `.catch` a rejected\n // write (e.g. Redis down) would surface as an unhandledRejection.\n cacheDriver\n .set(\n cacheKey,\n {\n status: sentResponse.statusCode,\n body: sentResponse.parsedBody,\n bodyHash,\n contentType: typeof sentContentType === \"string\" ? sentContentType : undefined,\n },\n ttl,\n )\n .catch((error: unknown) => {\n log.error(\"idempotency-middleware\", \"set\", error);\n });\n });\n };\n}\n"],"mappings":";;;;;;;;AA0CA,MAAM,kBAAkB;CAAC;CAAQ;CAAO;CAAS;AAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCzD,SAAgB,sBAAsB,UAA8B,CAAC,GAAe;CAClF,OAAO,OAAO,EAAE,SAAS,eAAe;EACtC,MAAM,aACJ,QAAQ,cAAc,OAAO,IAAI,+BAA+B,iBAAiB;EACnF,MAAM,UAAU,QAAQ,WAAW,OAAO,IAAI,4BAA4B,eAAe;EACzF,MAAM,MAAM,QAAQ,OAAO,OAAO,IAAI,wBAAwB,KAAK;EACnE,MAAM,aAAa,QAAQ,UAAU,OAAO,IAAI,yBAAyB;EAEzE,IAAI,CAAC,QAAQ,SAAS,QAAQ,OAAO,YAAY,CAAC,GAAG;EAErD,MAAM,iBAAiB,QAAQ,OAAO,WAAW,YAAY,CAAC;EAE9D,IAAI,CAAC,gBAAgB;EAErB,IAAI,CAAC,sBAAsB,cAAc,GACvC,OAAO,SAAS,WAAW;GACzB,OAAO,EAAE,4BAA4B;GACrC;EACF,CAAC;EAGH,MAAM,cAAc,aAAa,MAAM,MAAM,IAAI,UAAU,IAAI;EAC/D,MAAM,WAAW,yBAAyB,SAAS,cAAc;EACjE,MAAM,WAAW,SAAS,QAAQ,IAAI;EAEtC,MAAM,SAAU,MAAM,YAAY,IAAI,QAAQ;EAE9C,IAAI,QAAQ;GACV,IAAI,OAAO,aAAa,UACtB,OAAO,SAAS,oBAAoB;IAClC,OAAO,EAAE,6BAA6B;IACtC;GACF,CAAC;GAGH,SAAS,OAAO,qBAAqB,MAAM;GAE3C,OAAO,SAAS,OAAO;IACrB,QAAQ,OAAO;IACf,MAAM,OAAO;IACb,aAAa,OAAO;GACtB,CAAC;EACH;EAEA,SAAS,QAAQ,iBAA2B;GAG1C,IAAI,aAAa,cAAc,KAAK;GAEpC,MAAM,kBAAkB,aAAa;GAIrC,YACG,IACC,UACA;IACE,QAAQ,aAAa;IACrB,MAAM,aAAa;IACnB;IACA,aAAa,OAAO,oBAAoB,WAAW,kBAAkB;GACvE,GACA,GACF,CAAC,CACA,OAAO,UAAmB;IACzB,IAAI,MAAM,0BAA0B,OAAO,KAAK;GAClD,CAAC;EACL,CAAC;CACH;AACF"}
@@ -33,8 +33,7 @@ function maintenanceMiddleware(options = {}) {
33
33
  return ({ request, response }) => {
34
34
  if (!config.get("http.maintenance.enabled", false)) return;
35
35
  const allowlist = options.allowlist || config.get("http.maintenance.allowlist", ["/health"]);
36
- const pathname = request.path.split("?")[0];
37
- if (isAllowlisted(pathname, allowlist)) return;
36
+ if (isAllowlisted(request.path.split("?")[0] ?? "/", allowlist)) return;
38
37
  const retryAfter = options.retryAfter || config.get("http.maintenance.retryAfter", 60);
39
38
  response.header("Retry-After", retryAfter);
40
39
  return response.serviceUnavailable({
@@ -1 +1 @@
1
- {"version":3,"file":"maintenance.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/maintenance.middleware.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport { t } from \"./inject-request-context\";\n\n/**\n * Options for the maintenance middleware.\n */\nexport type MaintenanceOptions = {\n /**\n * Path prefixes (ending in `*`) or exact paths to bypass even when\n * maintenance is on. Falls back to `http.maintenance.allowlist`, then\n * `[\"/health\"]`.\n *\n * @example\n * allowlist: [\"/health\", \"/admin/*\", \"/webhooks/stripe\"]\n */\n allowlist?: string[];\n /**\n * Seconds advertised in the `Retry-After` header. Falls back to\n * `http.maintenance.retryAfter`, then `60`.\n */\n retryAfter?: number;\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\nfunction isAllowlisted(path: string, patterns: string[]) {\n return patterns.some((pattern) => {\n if (pattern.endsWith(\"*\")) {\n return path.startsWith(pattern.slice(0, -1));\n }\n\n return path === pattern;\n });\n}\n\n/**\n * Return 503 + `Retry-After` for every request when `http.maintenance.enabled`\n * is true, except for paths matching the allowlist.\n *\n * Designed for app-wide registration via `http.middleware.all`. Toggled via\n * config — flipping the flag requires a restart (no runtime hot-flip yet).\n * Allowlist defaults to `[\"/health\"]` so health checks still pass during\n * planned downtime.\n *\n * @example\n * // src/config/http.ts\n * import { middleware } from \"@warlock.js/core\";\n *\n * export default {\n * maintenance: { enabled: env(\"MAINTENANCE_MODE\") === \"true\" },\n * middleware: {\n * all: [middleware.maintenance({ allowlist: [\"/health\", \"/admin/*\"] })],\n * },\n * };\n */\nexport function maintenanceMiddleware(options: MaintenanceOptions = {}): Middleware {\n return ({ request, response }) => {\n const enabled = config.get(\"http.maintenance.enabled\", false);\n\n if (!enabled) return;\n\n const allowlist =\n options.allowlist || config.get(\"http.maintenance.allowlist\", [\"/health\"]);\n\n // `request.path` includes the query string, but allowlist entries are\n // path-only (\"/webhooks/stripe\"), so strip the query before matching —\n // otherwise \"/webhooks/stripe?sig=...\" never matches its exact entry.\n const pathname = request.path.split(\"?\")[0];\n\n if (isAllowlisted(pathname, allowlist)) return;\n\n const retryAfter =\n options.retryAfter || config.get(\"http.maintenance.retryAfter\", 60);\n\n response.header(\"Retry-After\", retryAfter);\n\n return response.serviceUnavailable({\n error: options.errorMessage || t(\"http.maintenance\"),\n errorCode: HttpErrorCodes.Maintenance,\n });\n };\n}\n"],"mappings":";;;;;AA6BA,SAAS,cAAc,MAAc,UAAoB;CACvD,OAAO,SAAS,MAAM,YAAY;EAChC,IAAI,QAAQ,SAAS,GAAG,GACtB,OAAO,KAAK,WAAW,QAAQ,MAAM,GAAG,EAAE,CAAC;EAG7C,OAAO,SAAS;CAClB,CAAC;AACH;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,sBAAsB,UAA8B,CAAC,GAAe;CAClF,QAAQ,EAAE,SAAS,eAAe;EAGhC,IAAI,CAFY,OAAO,IAAI,4BAA4B,KAE5C,GAAG;EAEd,MAAM,YACJ,QAAQ,aAAa,OAAO,IAAI,8BAA8B,CAAC,SAAS,CAAC;EAK3E,MAAM,WAAW,QAAQ,KAAK,MAAM,GAAG,CAAC,CAAC;EAEzC,IAAI,cAAc,UAAU,SAAS,GAAG;EAExC,MAAM,aACJ,QAAQ,cAAc,OAAO,IAAI,+BAA+B,EAAE;EAEpE,SAAS,OAAO,eAAe,UAAU;EAEzC,OAAO,SAAS,mBAAmB;GACjC,OAAO,QAAQ,gBAAgB,EAAE,kBAAkB;GACnD;EACF,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"maintenance.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/maintenance.middleware.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport { t } from \"./inject-request-context\";\n\n/**\n * Options for the maintenance middleware.\n */\nexport type MaintenanceOptions = {\n /**\n * Path prefixes (ending in `*`) or exact paths to bypass even when\n * maintenance is on. Falls back to `http.maintenance.allowlist`, then\n * `[\"/health\"]`.\n *\n * @example\n * allowlist: [\"/health\", \"/admin/*\", \"/webhooks/stripe\"]\n */\n allowlist?: string[];\n /**\n * Seconds advertised in the `Retry-After` header. Falls back to\n * `http.maintenance.retryAfter`, then `60`.\n */\n retryAfter?: number;\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\nfunction isAllowlisted(path: string, patterns: string[]) {\n return patterns.some((pattern) => {\n if (pattern.endsWith(\"*\")) {\n return path.startsWith(pattern.slice(0, -1));\n }\n\n return path === pattern;\n });\n}\n\n/**\n * Return 503 + `Retry-After` for every request when `http.maintenance.enabled`\n * is true, except for paths matching the allowlist.\n *\n * Designed for app-wide registration via `http.middleware.all`. Toggled via\n * config — flipping the flag requires a restart (no runtime hot-flip yet).\n * Allowlist defaults to `[\"/health\"]` so health checks still pass during\n * planned downtime.\n *\n * @example\n * // src/config/http.ts\n * import { middleware } from \"@warlock.js/core\";\n *\n * export default {\n * maintenance: { enabled: env(\"MAINTENANCE_MODE\") === \"true\" },\n * middleware: {\n * all: [middleware.maintenance({ allowlist: [\"/health\", \"/admin/*\"] })],\n * },\n * };\n */\nexport function maintenanceMiddleware(options: MaintenanceOptions = {}): Middleware {\n return ({ request, response }) => {\n const enabled = config.get(\"http.maintenance.enabled\", false);\n\n if (!enabled) return;\n\n const allowlist = options.allowlist || config.get(\"http.maintenance.allowlist\", [\"/health\"]);\n\n // `request.path` includes the query string, but allowlist entries are\n // path-only (\"/webhooks/stripe\"), so strip the query before matching —\n // otherwise \"/webhooks/stripe?sig=...\" never matches its exact entry.\n const pathname = request.path.split(\"?\")[0] ?? \"/\";\n\n if (isAllowlisted(pathname, allowlist)) return;\n\n const retryAfter = options.retryAfter || config.get(\"http.maintenance.retryAfter\", 60);\n\n response.header(\"Retry-After\", retryAfter);\n\n return response.serviceUnavailable({\n error: options.errorMessage || t(\"http.maintenance\"),\n errorCode: HttpErrorCodes.Maintenance,\n });\n };\n}\n"],"mappings":";;;;;AA6BA,SAAS,cAAc,MAAc,UAAoB;CACvD,OAAO,SAAS,MAAM,YAAY;EAChC,IAAI,QAAQ,SAAS,GAAG,GACtB,OAAO,KAAK,WAAW,QAAQ,MAAM,GAAG,EAAE,CAAC;EAG7C,OAAO,SAAS;CAClB,CAAC;AACH;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,sBAAsB,UAA8B,CAAC,GAAe;CAClF,QAAQ,EAAE,SAAS,eAAe;EAGhC,IAAI,CAFY,OAAO,IAAI,4BAA4B,KAE5C,GAAG;EAEd,MAAM,YAAY,QAAQ,aAAa,OAAO,IAAI,8BAA8B,CAAC,SAAS,CAAC;EAO3F,IAAI,cAFa,QAAQ,KAAK,MAAM,GAAG,CAAC,CAAC,MAAM,KAEnB,SAAS,GAAG;EAExC,MAAM,aAAa,QAAQ,cAAc,OAAO,IAAI,+BAA+B,EAAE;EAErF,SAAS,OAAO,eAAe,UAAU;EAEzC,OAAO,SAAS,mBAAmB;GACjC,OAAO,QAAQ,gBAAgB,EAAE,kBAAkB;GACnD;EACF,CAAC;CACH;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"max-body-size.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/max-body-size.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport { t } from \"./inject-request-context\";\nimport { parseSize } from \"./utils/parse-size\";\n\n/**\n * Reject requests whose `Content-Length` exceeds the configured cap.\n *\n * ⚠️ **This runs AFTER the body has been parsed, not before it.** Framework\n * middleware executes inside the Fastify route handler, and `@fastify/multipart`\n * is registered with `attachFieldsToBody: true` — so by the time this sees the\n * request, the bytes are already resident. It inspects `Content-Length` and\n * answers 413; it does not and cannot prevent the read.\n *\n * **So this is not a resource-exhaustion control.** For that, lower\n * `http.bodyLimit`, or set `bodyLimit` in the route's own `serverOptions` —\n * Fastify enforces those while reading. This middleware is a per-route\n * convenience for returning a consistent 413, not a replacement for either.\n *\n * The same caveat applies to `concurrencyLimit`: it bounds how many requests\n * run at once, not how many bytes are resident.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n *\n * router.post(\"/comments\", createCommentController, {\n * middleware: [middleware.maxBodySize(\"8kb\")],\n * });\n *\n * router.post(\"/uploads\", uploadController, {\n * middleware: [middleware.maxBodySize(\"10mb\")],\n * });\n */\nexport function maxBodySizeMiddleware(limit: string | number): Middleware {\n const limitBytes = parseSize(limit);\n\n return ({ request, response }) => {\n const contentLengthHeader = request.header(\"content-length\");\n\n if (!contentLengthHeader) return;\n\n const contentLength = Number(contentLengthHeader);\n\n if (!Number.isFinite(contentLength)) return;\n\n if (contentLength <= limitBytes) return;\n\n response.header(\"Connection\", \"close\");\n\n return response.contentTooLarge({\n error: t(\"http.bodyTooLarge\"),\n errorCode: HttpErrorCodes.BodyTooLarge,\n limit: limitBytes,\n received: contentLength,\n },\n );\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,sBAAsB,OAAoC;CACxE,MAAM,aAAa,UAAU,KAAK;CAElC,QAAQ,EAAE,SAAS,eAAe;EAChC,MAAM,sBAAsB,QAAQ,OAAO,gBAAgB;EAE3D,IAAI,CAAC,qBAAqB;EAE1B,MAAM,gBAAgB,OAAO,mBAAmB;EAEhD,IAAI,CAAC,OAAO,SAAS,aAAa,GAAG;EAErC,IAAI,iBAAiB,YAAY;EAEjC,SAAS,OAAO,cAAc,OAAO;EAErC,OAAO,SAAS,gBAAgB;GAC5B,OAAO,EAAE,mBAAmB;GAC5B;GACA,OAAO;GACP,UAAU;EACZ,CACF;CACF;AACF"}
1
+ {"version":3,"file":"max-body-size.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/max-body-size.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport { t } from \"./inject-request-context\";\nimport { parseSize } from \"./utils/parse-size\";\n\n/**\n * Reject requests whose `Content-Length` exceeds the configured cap.\n *\n * ⚠️ **This runs AFTER the body has been parsed, not before it.** Framework\n * middleware executes inside the Fastify route handler, and `@fastify/multipart`\n * is registered with `attachFieldsToBody: true` — so by the time this sees the\n * request, the bytes are already resident. It inspects `Content-Length` and\n * answers 413; it does not and cannot prevent the read.\n *\n * **So this is not a resource-exhaustion control.** For that, lower\n * `http.bodyLimit`, or set `bodyLimit` in the route's own `serverOptions` —\n * Fastify enforces those while reading. This middleware is a per-route\n * convenience for returning a consistent 413, not a replacement for either.\n *\n * The same caveat applies to `concurrencyLimit`: it bounds how many requests\n * run at once, not how many bytes are resident.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n *\n * router.post(\"/comments\", createCommentController, {\n * middleware: [middleware.maxBodySize(\"8kb\")],\n * });\n *\n * router.post(\"/uploads\", uploadController, {\n * middleware: [middleware.maxBodySize(\"10mb\")],\n * });\n */\nexport function maxBodySizeMiddleware(limit: string | number): Middleware {\n const limitBytes = parseSize(limit);\n\n return ({ request, response }) => {\n const contentLengthHeader = request.header(\"content-length\");\n\n if (!contentLengthHeader) return;\n\n const contentLength = Number(contentLengthHeader);\n\n if (!Number.isFinite(contentLength)) return;\n\n if (contentLength <= limitBytes) return;\n\n response.header(\"Connection\", \"close\");\n\n return response.contentTooLarge({\n error: t(\"http.bodyTooLarge\"),\n errorCode: HttpErrorCodes.BodyTooLarge,\n limit: limitBytes,\n received: contentLength,\n });\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,sBAAsB,OAAoC;CACxE,MAAM,aAAa,UAAU,KAAK;CAElC,QAAQ,EAAE,SAAS,eAAe;EAChC,MAAM,sBAAsB,QAAQ,OAAO,gBAAgB;EAE3D,IAAI,CAAC,qBAAqB;EAE1B,MAAM,gBAAgB,OAAO,mBAAmB;EAEhD,IAAI,CAAC,OAAO,SAAS,aAAa,GAAG;EAErC,IAAI,iBAAiB,YAAY;EAEjC,SAAS,OAAO,cAAc,OAAO;EAErC,OAAO,SAAS,gBAAgB;GAC9B,OAAO,EAAE,mBAAmB;GAC5B;GACA,OAAO;GACP,UAAU;EACZ,CAAC;CACH;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/rate-limit.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport type { Request } from \"../request\";\nimport { t } from \"./inject-request-context\";\n\n/**\n * Options for the per-route rate limit middleware.\n */\nexport type RateLimitOptions = {\n /**\n * Max requests allowed within the time window.\n */\n max: number;\n /**\n * Time window in milliseconds.\n */\n duration: number;\n /**\n * Group key generator. Defaults to the client IP. Override to scope per-user,\n * per-organization, per-tenant, etc.\n *\n * @example\n * keyGenerator: (request) => request.user?.id ?? request.ip,\n */\n keyGenerator?: (request: Request) => string;\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\ntype Bucket = {\n count: number;\n resetAt: number;\n};\n\nconst buckets = new Map<string, Bucket>();\n\n/**\n * Sweep expired buckets so the Map doesn't grow unbounded with one-shot keys\n * (rare per-user keys, dynamic path parameters in `keyGenerator`, etc.).\n * Called opportunistically, at most once per minute, not on a timer — keeps\n * the module side-effect-free for testing.\n */\nlet lastPruneAt = 0;\n\nfunction pruneExpired(now: number) {\n for (const [key, bucket] of buckets) {\n if (bucket.resetAt <= now) {\n buckets.delete(key);\n }\n }\n}\n\n/**\n * Per-route / per-group rate limit. Layers on top of `@fastify/rate-limit`'s\n * global cap (configured via `http.rateLimit`) — both run, and either can\n * 429. Use this for endpoints that need a tighter cap than the global default:\n * login, OTP request, password reset, expensive search, AI completions.\n *\n * The counter lives in-process. With `N` replicas the effective cap is\n * `N × max`. For genuinely shared limits, configure `@fastify/rate-limit`'s\n * Redis store via `http.rateLimit` instead.\n *\n * Sets `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` on\n * every response; adds `Retry-After` (seconds) on a 429.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n * router.post(\"/auth/login\", loginController, {\n * middleware: [middleware.rateLimit({ max: 5, duration: 60_000 })],\n * });\n */\nexport function rateLimitMiddleware(options: RateLimitOptions): Middleware {\n return ({ request, response }) => {\n const now = Date.now();\n\n if (now - lastPruneAt > 60_000) {\n pruneExpired(now);\n lastPruneAt = now;\n }\n\n const groupKey = options.keyGenerator?.(request) || request.detectIp() || \"unknown\";\n const cacheKey = `${request.route.path}:${groupKey}`;\n\n let bucket = buckets.get(cacheKey);\n\n if (!bucket || bucket.resetAt <= now) {\n bucket = { count: 0, resetAt: now + options.duration };\n buckets.set(cacheKey, bucket);\n }\n\n bucket.count++;\n\n const remaining = Math.max(0, options.max - bucket.count);\n const retryAfter = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000));\n\n response.header(\"X-RateLimit-Limit\", options.max);\n response.header(\"X-RateLimit-Remaining\", remaining);\n response.header(\"X-RateLimit-Reset\", Math.ceil(bucket.resetAt / 1000));\n\n if (bucket.count > options.max) {\n response.header(\"Retry-After\", retryAfter);\n\n return response.tooManyRequests({\n error: options.errorMessage || t(\"http.rateLimitExceeded\"),\n errorCode: HttpErrorCodes.RateLimitExceeded,\n },\n );\n }\n };\n}\n"],"mappings":";;;;AAoCA,MAAM,0BAAU,IAAI,IAAoB;;;;;;;AAQxC,IAAI,cAAc;AAElB,SAAS,aAAa,KAAa;CACjC,KAAK,MAAM,CAAC,KAAK,WAAW,SAC1B,IAAI,OAAO,WAAW,KACpB,QAAQ,OAAO,GAAG;AAGxB;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,oBAAoB,SAAuC;CACzE,QAAQ,EAAE,SAAS,eAAe;EAChC,MAAM,MAAM,KAAK,IAAI;EAErB,IAAI,MAAM,cAAc,KAAQ;GAC9B,aAAa,GAAG;GAChB,cAAc;EAChB;EAEA,MAAM,WAAW,QAAQ,eAAe,OAAO,KAAK,QAAQ,SAAS,KAAK;EAC1E,MAAM,WAAW,GAAG,QAAQ,MAAM,KAAK,GAAG;EAE1C,IAAI,SAAS,QAAQ,IAAI,QAAQ;EAEjC,IAAI,CAAC,UAAU,OAAO,WAAW,KAAK;GACpC,SAAS;IAAE,OAAO;IAAG,SAAS,MAAM,QAAQ;GAAS;GACrD,QAAQ,IAAI,UAAU,MAAM;EAC9B;EAEA,OAAO;EAEP,MAAM,YAAY,KAAK,IAAI,GAAG,QAAQ,MAAM,OAAO,KAAK;EACxD,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,UAAU,OAAO,GAAI,CAAC;EAEvE,SAAS,OAAO,qBAAqB,QAAQ,GAAG;EAChD,SAAS,OAAO,yBAAyB,SAAS;EAClD,SAAS,OAAO,qBAAqB,KAAK,KAAK,OAAO,UAAU,GAAI,CAAC;EAErE,IAAI,OAAO,QAAQ,QAAQ,KAAK;GAC9B,SAAS,OAAO,eAAe,UAAU;GAEzC,OAAO,SAAS,gBAAgB;IAC5B,OAAO,QAAQ,gBAAgB,EAAE,wBAAwB;IACzD;GACF,CACF;EACF;CACF;AACF"}
1
+ {"version":3,"file":"rate-limit.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/rate-limit.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport type { Request } from \"../request\";\nimport { t } from \"./inject-request-context\";\n\n/**\n * Options for the per-route rate limit middleware.\n */\nexport type RateLimitOptions = {\n /**\n * Max requests allowed within the time window.\n */\n max: number;\n /**\n * Time window in milliseconds.\n */\n duration: number;\n /**\n * Group key generator. Defaults to the client IP. Override to scope per-user,\n * per-organization, per-tenant, etc.\n *\n * @example\n * keyGenerator: (request) => request.user?.id ?? request.ip,\n */\n keyGenerator?: (request: Request) => string;\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\ntype Bucket = {\n count: number;\n resetAt: number;\n};\n\nconst buckets = new Map<string, Bucket>();\n\n/**\n * Sweep expired buckets so the Map doesn't grow unbounded with one-shot keys\n * (rare per-user keys, dynamic path parameters in `keyGenerator`, etc.).\n * Called opportunistically, at most once per minute, not on a timer — keeps\n * the module side-effect-free for testing.\n */\nlet lastPruneAt = 0;\n\nfunction pruneExpired(now: number) {\n for (const [key, bucket] of buckets) {\n if (bucket.resetAt <= now) {\n buckets.delete(key);\n }\n }\n}\n\n/**\n * Per-route / per-group rate limit. Layers on top of `@fastify/rate-limit`'s\n * global cap (configured via `http.rateLimit`) — both run, and either can\n * 429. Use this for endpoints that need a tighter cap than the global default:\n * login, OTP request, password reset, expensive search, AI completions.\n *\n * The counter lives in-process. With `N` replicas the effective cap is\n * `N × max`. For genuinely shared limits, configure `@fastify/rate-limit`'s\n * Redis store via `http.rateLimit` instead.\n *\n * Sets `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` on\n * every response; adds `Retry-After` (seconds) on a 429.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n * router.post(\"/auth/login\", loginController, {\n * middleware: [middleware.rateLimit({ max: 5, duration: 60_000 })],\n * });\n */\nexport function rateLimitMiddleware(options: RateLimitOptions): Middleware {\n return ({ request, response }) => {\n const now = Date.now();\n\n if (now - lastPruneAt > 60_000) {\n pruneExpired(now);\n lastPruneAt = now;\n }\n\n const groupKey = options.keyGenerator?.(request) || request.detectIp() || \"unknown\";\n const cacheKey = `${request.route.path}:${groupKey}`;\n\n let bucket = buckets.get(cacheKey);\n\n if (!bucket || bucket.resetAt <= now) {\n bucket = { count: 0, resetAt: now + options.duration };\n buckets.set(cacheKey, bucket);\n }\n\n bucket.count++;\n\n const remaining = Math.max(0, options.max - bucket.count);\n const retryAfter = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000));\n\n response.header(\"X-RateLimit-Limit\", options.max);\n response.header(\"X-RateLimit-Remaining\", remaining);\n response.header(\"X-RateLimit-Reset\", Math.ceil(bucket.resetAt / 1000));\n\n if (bucket.count > options.max) {\n response.header(\"Retry-After\", retryAfter);\n\n return response.tooManyRequests({\n error: options.errorMessage || t(\"http.rateLimitExceeded\"),\n errorCode: HttpErrorCodes.RateLimitExceeded,\n });\n }\n };\n}\n"],"mappings":";;;;AAoCA,MAAM,0BAAU,IAAI,IAAoB;;;;;;;AAQxC,IAAI,cAAc;AAElB,SAAS,aAAa,KAAa;CACjC,KAAK,MAAM,CAAC,KAAK,WAAW,SAC1B,IAAI,OAAO,WAAW,KACpB,QAAQ,OAAO,GAAG;AAGxB;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,oBAAoB,SAAuC;CACzE,QAAQ,EAAE,SAAS,eAAe;EAChC,MAAM,MAAM,KAAK,IAAI;EAErB,IAAI,MAAM,cAAc,KAAQ;GAC9B,aAAa,GAAG;GAChB,cAAc;EAChB;EAEA,MAAM,WAAW,QAAQ,eAAe,OAAO,KAAK,QAAQ,SAAS,KAAK;EAC1E,MAAM,WAAW,GAAG,QAAQ,MAAM,KAAK,GAAG;EAE1C,IAAI,SAAS,QAAQ,IAAI,QAAQ;EAEjC,IAAI,CAAC,UAAU,OAAO,WAAW,KAAK;GACpC,SAAS;IAAE,OAAO;IAAG,SAAS,MAAM,QAAQ;GAAS;GACrD,QAAQ,IAAI,UAAU,MAAM;EAC9B;EAEA,OAAO;EAEP,MAAM,YAAY,KAAK,IAAI,GAAG,QAAQ,MAAM,OAAO,KAAK;EACxD,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,UAAU,OAAO,GAAI,CAAC;EAEvE,SAAS,OAAO,qBAAqB,QAAQ,GAAG;EAChD,SAAS,OAAO,yBAAyB,SAAS;EAClD,SAAS,OAAO,qBAAqB,KAAK,KAAK,OAAO,UAAU,GAAI,CAAC;EAErE,IAAI,OAAO,QAAQ,QAAQ,KAAK;GAC9B,SAAS,OAAO,eAAe,UAAU;GAEzC,OAAO,SAAS,gBAAgB;IAC9B,OAAO,QAAQ,gBAAgB,EAAE,wBAAwB;IACzD;GACF,CAAC;EACH;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"cidr-match.d.mts","names":[],"sources":["../../../../../../../../../core/src/http/middleware/utils/cidr-match.ts"],"mappings":";iBAkCgB,SAAA,CAAU,EAAA,UAAY,OAAe;AAArD;;;;AAAqD;AA0CrD;AA1CA,iBA0CgB,QAAA,CAAS,EAAA,UAAY,QAAkB"}
1
+ {"version":3,"file":"cidr-match.d.mts","names":[],"sources":["../../../../../../../../../core/src/http/middleware/utils/cidr-match.ts"],"mappings":";iBAkCgB,SAAA,CAAU,EAAA,UAAY,OAAe;AAArD;;;;AAAqD;AA+CrD;AA/CA,iBA+CgB,QAAA,CAAS,EAAA,UAAY,QAAkB"}
@@ -27,6 +27,7 @@ function ipMatches(ip, pattern) {
27
27
  if (pattern === ip) return true;
28
28
  if (!pattern.includes("/")) return false;
29
29
  const [base, bitsRaw] = pattern.split("/");
30
+ if (base === void 0 || bitsRaw === void 0) return false;
30
31
  const bits = Number(bitsRaw);
31
32
  if (!Number.isInteger(bits) || bits < 0 || bits > 32) return false;
32
33
  const ipInt = ipv4ToInt(ip);
@@ -1 +1 @@
1
- {"version":3,"file":"cidr-match.mjs","names":[],"sources":["../../../../../../../../../core/src/http/middleware/utils/cidr-match.ts"],"sourcesContent":["/**\n * Match an IPv4 address against an exact IP or a CIDR block.\n *\n * IPv6 patterns are matched as exact-string only — full IPv6 CIDR support is\n * out of scope; ACLs in production almost always use IPv4 ranges. Add IPv6 CIDR\n * later if a real use-case appears.\n *\n * @example\n * ipMatches(\"10.0.0.5\", \"10.0.0.0/8\"); // true\n * ipMatches(\"10.0.0.5\", \"10.0.0.5\"); // true\n * ipMatches(\"192.168.1.1\", \"10.0.0.0/8\"); // false\n */\nfunction ipv4ToInt(ip: string): number | null {\n const parts = ip.split(\".\");\n\n if (parts.length !== 4) {\n return null;\n }\n\n let result = 0;\n\n for (const part of parts) {\n const num = Number(part);\n\n if (!Number.isInteger(num) || num < 0 || num > 255) {\n return null;\n }\n\n result = (result << 8) + num;\n }\n\n return result >>> 0;\n}\n\nexport function ipMatches(ip: string, pattern: string): boolean {\n if (!ip || !pattern) {\n return false;\n }\n\n if (pattern === ip) {\n return true;\n }\n\n if (!pattern.includes(\"/\")) {\n return false;\n }\n\n const [base, bitsRaw] = pattern.split(\"/\");\n const bits = Number(bitsRaw);\n\n if (!Number.isInteger(bits) || bits < 0 || bits > 32) {\n return false;\n }\n\n const ipInt = ipv4ToInt(ip);\n const baseInt = ipv4ToInt(base);\n\n if (ipInt === null || baseInt === null) {\n return false;\n }\n\n if (bits === 0) {\n return true;\n }\n\n const mask = (~0 << (32 - bits)) >>> 0;\n\n return (ipInt & mask) === (baseInt & mask);\n}\n\n/**\n * Returns true if `ip` matches any pattern in the list.\n *\n * @example\n * anyMatch(\"10.0.0.5\", [\"192.168.0.0/16\", \"10.0.0.0/8\"]); // true\n */\nexport function anyMatch(ip: string, patterns: string[]): boolean {\n return patterns.some((pattern) => ipMatches(ip, pattern));\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAS,UAAU,IAA2B;CAC5C,MAAM,QAAQ,GAAG,MAAM,GAAG;CAE1B,IAAI,MAAM,WAAW,GACnB,OAAO;CAGT,IAAI,SAAS;CAEb,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,MAAM,OAAO,IAAI;EAEvB,IAAI,CAAC,OAAO,UAAU,GAAG,KAAK,MAAM,KAAK,MAAM,KAC7C,OAAO;EAGT,UAAU,UAAU,KAAK;CAC3B;CAEA,OAAO,WAAW;AACpB;AAEA,SAAgB,UAAU,IAAY,SAA0B;CAC9D,IAAI,CAAC,MAAM,CAAC,SACV,OAAO;CAGT,IAAI,YAAY,IACd,OAAO;CAGT,IAAI,CAAC,QAAQ,SAAS,GAAG,GACvB,OAAO;CAGT,MAAM,CAAC,MAAM,WAAW,QAAQ,MAAM,GAAG;CACzC,MAAM,OAAO,OAAO,OAAO;CAE3B,IAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,IAChD,OAAO;CAGT,MAAM,QAAQ,UAAU,EAAE;CAC1B,MAAM,UAAU,UAAU,IAAI;CAE9B,IAAI,UAAU,QAAQ,YAAY,MAChC,OAAO;CAGT,IAAI,SAAS,GACX,OAAO;CAGT,MAAM,OAAQ,MAAO,KAAK,SAAW;CAErC,QAAQ,QAAQ,WAAW,UAAU;AACvC;;;;;;;AAQA,SAAgB,SAAS,IAAY,UAA6B;CAChE,OAAO,SAAS,MAAM,YAAY,UAAU,IAAI,OAAO,CAAC;AAC1D"}
1
+ {"version":3,"file":"cidr-match.mjs","names":[],"sources":["../../../../../../../../../core/src/http/middleware/utils/cidr-match.ts"],"sourcesContent":["/**\n * Match an IPv4 address against an exact IP or a CIDR block.\n *\n * IPv6 patterns are matched as exact-string only — full IPv6 CIDR support is\n * out of scope; ACLs in production almost always use IPv4 ranges. Add IPv6 CIDR\n * later if a real use-case appears.\n *\n * @example\n * ipMatches(\"10.0.0.5\", \"10.0.0.0/8\"); // true\n * ipMatches(\"10.0.0.5\", \"10.0.0.5\"); // true\n * ipMatches(\"192.168.1.1\", \"10.0.0.0/8\"); // false\n */\nfunction ipv4ToInt(ip: string): number | null {\n const parts = ip.split(\".\");\n\n if (parts.length !== 4) {\n return null;\n }\n\n let result = 0;\n\n for (const part of parts) {\n const num = Number(part);\n\n if (!Number.isInteger(num) || num < 0 || num > 255) {\n return null;\n }\n\n result = (result << 8) + num;\n }\n\n return result >>> 0;\n}\n\nexport function ipMatches(ip: string, pattern: string): boolean {\n if (!ip || !pattern) {\n return false;\n }\n\n if (pattern === ip) {\n return true;\n }\n\n if (!pattern.includes(\"/\")) {\n return false;\n }\n\n const [base, bitsRaw] = pattern.split(\"/\");\n\n if (base === undefined || bitsRaw === undefined) {\n return false;\n }\n\n const bits = Number(bitsRaw);\n\n if (!Number.isInteger(bits) || bits < 0 || bits > 32) {\n return false;\n }\n\n const ipInt = ipv4ToInt(ip);\n const baseInt = ipv4ToInt(base);\n\n if (ipInt === null || baseInt === null) {\n return false;\n }\n\n if (bits === 0) {\n return true;\n }\n\n const mask = (~0 << (32 - bits)) >>> 0;\n\n return (ipInt & mask) === (baseInt & mask);\n}\n\n/**\n * Returns true if `ip` matches any pattern in the list.\n *\n * @example\n * anyMatch(\"10.0.0.5\", [\"192.168.0.0/16\", \"10.0.0.0/8\"]); // true\n */\nexport function anyMatch(ip: string, patterns: string[]): boolean {\n return patterns.some((pattern) => ipMatches(ip, pattern));\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAS,UAAU,IAA2B;CAC5C,MAAM,QAAQ,GAAG,MAAM,GAAG;CAE1B,IAAI,MAAM,WAAW,GACnB,OAAO;CAGT,IAAI,SAAS;CAEb,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,MAAM,OAAO,IAAI;EAEvB,IAAI,CAAC,OAAO,UAAU,GAAG,KAAK,MAAM,KAAK,MAAM,KAC7C,OAAO;EAGT,UAAU,UAAU,KAAK;CAC3B;CAEA,OAAO,WAAW;AACpB;AAEA,SAAgB,UAAU,IAAY,SAA0B;CAC9D,IAAI,CAAC,MAAM,CAAC,SACV,OAAO;CAGT,IAAI,YAAY,IACd,OAAO;CAGT,IAAI,CAAC,QAAQ,SAAS,GAAG,GACvB,OAAO;CAGT,MAAM,CAAC,MAAM,WAAW,QAAQ,MAAM,GAAG;CAEzC,IAAI,SAAS,UAAa,YAAY,QACpC,OAAO;CAGT,MAAM,OAAO,OAAO,OAAO;CAE3B,IAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,IAChD,OAAO;CAGT,MAAM,QAAQ,UAAU,EAAE;CAC1B,MAAM,UAAU,UAAU,IAAI;CAE9B,IAAI,UAAU,QAAQ,YAAY,MAChC,OAAO;CAGT,IAAI,SAAS,GACX,OAAO;CAGT,MAAM,OAAQ,MAAO,KAAK,SAAW;CAErC,QAAQ,QAAQ,WAAW,UAAU;AACvC;;;;;;;AAQA,SAAgB,SAAS,IAAY,UAA6B;CAChE,OAAO,SAAS,MAAM,YAAY,UAAU,IAAI,OAAO,CAAC;AAC1D"}
@@ -1 +1 @@
1
- {"version":3,"file":"port-preflight.d.mts","names":[],"sources":["../../../../../../../core/src/http/port-preflight.ts"],"mappings":";;AAoBA;;;;;;;;;;;;cAAa,cAAA,SAAuB,KAAK;EAAA,SAKrB,IAAA;EAAA,SACA,IAAA;;WAJF,IAAA;cAGE,IAAA,UACA,IAAA;AAAA;;;AAoBV;AAmCV;;;;iBAtCgB,eAAA,CACd,IAAA,UACA,IAAA,YACC,OAAO;;;;AAsCA;;;;;;iBAHY,qBAAA,CACpB,IAAA,UACA,IAAA,YACC,OAAO"}
1
+ {"version":3,"file":"port-preflight.d.mts","names":[],"sources":["../../../../../../../core/src/http/port-preflight.ts"],"mappings":";;AAoBA;;;;;;;;;;;;cAAa,cAAA,SAAuB,KAAK;EAAA,SAKrB,IAAA;EAAA,SACA,IAAA;;WAJF,IAAA;cAGE,IAAA,UACA,IAAA;AAAA;;;AAiBqE;AAmCzF;;;;iBAnCgB,eAAA,CAAgB,IAAA,UAAc,IAAA,YAAoC,OAAO;;;;AAsC/E;;;;;;iBAHY,qBAAA,CACpB,IAAA,UACA,IAAA,YACC,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"port-preflight.mjs","names":[],"sources":["../../../../../../../core/src/http/port-preflight.ts"],"sourcesContent":["import { createServer } from \"node:net\";\n\n/**\n * Host used when the caller has no configured bind host.\n */\nconst DEFAULT_PROBE_HOST = \"localhost\";\n\n/**\n * Raised when a port the framework is about to bind is already taken.\n *\n * Carries the port and host so a caller can report them without parsing the\n * message, and its message is the developer-facing instruction — the whole\n * point of preflighting is that nobody has to decode a raw `EADDRINUSE`.\n *\n * The message leads with `EADDRINUSE` on purpose: it is the string a\n * developer greps their terminal for, and the one every other Node tool\n * already trained them to recognize as \"something else is on this port\".\n * `code` mirrors it as a property so a caller can branch on it without\n * parsing text, the same way `NodeJS.ErrnoException.code` works.\n */\nexport class PortInUseError extends Error {\n /** Mirrors Node's own `ErrnoException.code` for this failure. */\n public readonly code = \"EADDRINUSE\";\n\n public constructor(\n public readonly port: number,\n public readonly host: string,\n ) {\n super(\n `EADDRINUSE: Port ${port} is already in use on ${host}. Stop the dev server (or whatever else is listening on port ${port}) and run again, or start on a free port — e.g. startHttpTestServer({ port: ${port + 1} }).`,\n );\n\n this.name = \"PortInUseError\";\n }\n}\n\n/**\n * Probe a TCP port by attempting a bind and immediately releasing it.\n *\n * @returns `true` when the port is free, `false` when it is taken or the\n * process is not allowed to bind it. Any other bind failure is re-thrown —\n * an unreadable network error must not be reported as \"port in use\".\n */\nexport function isPortAvailable(\n port: number,\n host: string = DEFAULT_PROBE_HOST,\n): Promise<boolean> {\n return new Promise((resolve, reject) => {\n const probe = createServer();\n\n probe.unref();\n\n probe.once(\"error\", (error: NodeJS.ErrnoException) => {\n if (error.code === \"EADDRINUSE\" || error.code === \"EACCES\") {\n resolve(false);\n\n return;\n }\n\n reject(error);\n });\n\n probe.once(\"listening\", () => {\n probe.close(() => resolve(true));\n });\n\n // `exclusive` refuses a shared bind, which is what makes the probe honest:\n // with load balancing enabled a second bind on a served port can succeed.\n probe.listen({ port, host, exclusive: true });\n });\n}\n\n/**\n * Assert a port is free before something binds it.\n *\n * Run this ahead of the actual `listen()` so a collision surfaces as an\n * instruction the developer can act on, instead of a raw `EADDRINUSE` thrown\n * from deep inside the HTTP stack.\n *\n * @throws {PortInUseError} when the port is taken.\n */\nexport async function assertPortIsAvailable(\n port: number,\n host: string = DEFAULT_PROBE_HOST,\n): Promise<void> {\n const available = await isPortAvailable(port, host);\n\n if (!available) {\n throw new PortInUseError(port, host);\n }\n}\n"],"mappings":";;;;;;AAKA,MAAM,qBAAqB;;;;;;;;;;;;;;AAe3B,IAAa,iBAAb,cAAoC,MAAM;CAIxC,AAAO,YACL,AAAgB,MAChB,AAAgB,MAChB;EACA,MACE,oBAAoB,KAAK,wBAAwB,KAAK,+DAA+D,KAAK,8EAA8E,OAAO,EAAE,KACnN;EALgB;EACA;cAJK;EAUrB,KAAK,OAAO;CACd;AACF;;;;;;;;AASA,SAAgB,gBACd,MACA,OAAe,oBACG;CAClB,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,QAAQ,aAAa;EAE3B,MAAM,MAAM;EAEZ,MAAM,KAAK,UAAU,UAAiC;GACpD,IAAI,MAAM,SAAS,gBAAgB,MAAM,SAAS,UAAU;IAC1D,QAAQ,KAAK;IAEb;GACF;GAEA,OAAO,KAAK;EACd,CAAC;EAED,MAAM,KAAK,mBAAmB;GAC5B,MAAM,YAAY,QAAQ,IAAI,CAAC;EACjC,CAAC;EAID,MAAM,OAAO;GAAE;GAAM;GAAM,WAAW;EAAK,CAAC;CAC9C,CAAC;AACH;;;;;;;;;;AAWA,eAAsB,sBACpB,MACA,OAAe,oBACA;CAGf,IAAI,CAAC,MAFmB,gBAAgB,MAAM,IAAI,GAGhD,MAAM,IAAI,eAAe,MAAM,IAAI;AAEvC"}
1
+ {"version":3,"file":"port-preflight.mjs","names":[],"sources":["../../../../../../../core/src/http/port-preflight.ts"],"sourcesContent":["import { createServer } from \"node:net\";\n\n/**\n * Host used when the caller has no configured bind host.\n */\nconst DEFAULT_PROBE_HOST = \"localhost\";\n\n/**\n * Raised when a port the framework is about to bind is already taken.\n *\n * Carries the port and host so a caller can report them without parsing the\n * message, and its message is the developer-facing instruction — the whole\n * point of preflighting is that nobody has to decode a raw `EADDRINUSE`.\n *\n * The message leads with `EADDRINUSE` on purpose: it is the string a\n * developer greps their terminal for, and the one every other Node tool\n * already trained them to recognize as \"something else is on this port\".\n * `code` mirrors it as a property so a caller can branch on it without\n * parsing text, the same way `NodeJS.ErrnoException.code` works.\n */\nexport class PortInUseError extends Error {\n /** Mirrors Node's own `ErrnoException.code` for this failure. */\n public readonly code = \"EADDRINUSE\";\n\n public constructor(\n public readonly port: number,\n public readonly host: string,\n ) {\n super(\n `EADDRINUSE: Port ${port} is already in use on ${host}. Stop the dev server (or whatever else is listening on port ${port}) and run again, or start on a free port — e.g. startHttpTestServer({ port: ${port + 1} }).`,\n );\n\n this.name = \"PortInUseError\";\n }\n}\n\n/**\n * Probe a TCP port by attempting a bind and immediately releasing it.\n *\n * @returns `true` when the port is free, `false` when it is taken or the\n * process is not allowed to bind it. Any other bind failure is re-thrown —\n * an unreadable network error must not be reported as \"port in use\".\n */\nexport function isPortAvailable(port: number, host: string = DEFAULT_PROBE_HOST): Promise<boolean> {\n return new Promise((resolve, reject) => {\n const probe = createServer();\n\n probe.unref();\n\n probe.once(\"error\", (error: NodeJS.ErrnoException) => {\n if (error.code === \"EADDRINUSE\" || error.code === \"EACCES\") {\n resolve(false);\n\n return;\n }\n\n reject(error);\n });\n\n probe.once(\"listening\", () => {\n probe.close(() => resolve(true));\n });\n\n // `exclusive` refuses a shared bind, which is what makes the probe honest:\n // with load balancing enabled a second bind on a served port can succeed.\n probe.listen({ port, host, exclusive: true });\n });\n}\n\n/**\n * Assert a port is free before something binds it.\n *\n * Run this ahead of the actual `listen()` so a collision surfaces as an\n * instruction the developer can act on, instead of a raw `EADDRINUSE` thrown\n * from deep inside the HTTP stack.\n *\n * @throws {PortInUseError} when the port is taken.\n */\nexport async function assertPortIsAvailable(\n port: number,\n host: string = DEFAULT_PROBE_HOST,\n): Promise<void> {\n const available = await isPortAvailable(port, host);\n\n if (!available) {\n throw new PortInUseError(port, host);\n }\n}\n"],"mappings":";;;;;;AAKA,MAAM,qBAAqB;;;;;;;;;;;;;;AAe3B,IAAa,iBAAb,cAAoC,MAAM;CAIxC,AAAO,YACL,AAAgB,MAChB,AAAgB,MAChB;EACA,MACE,oBAAoB,KAAK,wBAAwB,KAAK,+DAA+D,KAAK,8EAA8E,OAAO,EAAE,KACnN;EALgB;EACA;cAJK;EAUrB,KAAK,OAAO;CACd;AACF;;;;;;;;AASA,SAAgB,gBAAgB,MAAc,OAAe,oBAAsC;CACjG,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,QAAQ,aAAa;EAE3B,MAAM,MAAM;EAEZ,MAAM,KAAK,UAAU,UAAiC;GACpD,IAAI,MAAM,SAAS,gBAAgB,MAAM,SAAS,UAAU;IAC1D,QAAQ,KAAK;IAEb;GACF;GAEA,OAAO,KAAK;EACd,CAAC;EAED,MAAM,KAAK,mBAAmB;GAC5B,MAAM,YAAY,QAAQ,IAAI,CAAC;EACjC,CAAC;EAID,MAAM,OAAO;GAAE;GAAM;GAAM,WAAW;EAAK,CAAC;CAC9C,CAAC;AACH;;;;;;;;;;AAWA,eAAsB,sBACpB,MACA,OAAe,oBACA;CAGf,IAAI,CAAC,MAFmB,gBAAgB,MAAM,IAAI,GAGhD,MAAM,IAAI,eAAe,MAAM,IAAI;AAEvC"}
@@ -1 +1 @@
1
- {"version":3,"file":"request.d.mts","names":[],"sources":["../../../../../../../core/src/http/request.ts"],"mappings":"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"}
1
+ {"version":3,"file":"request.d.mts","names":[],"sources":["../../../../../../../core/src/http/request.ts"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { config } from "../config/config-getter.mjs";
2
- import { resolveLocaleConfiguration } from "../config/locale-configuration.mjs";
2
+ import { LOCALE_COOKIE_NAME, resolveLocaleConfiguration } from "../config/locale-configuration.mjs";
3
3
  import { createRequestStore } from "./middleware/inject-request-context.mjs";
4
4
  import { validateAll } from "../validation/validateAll.mjs";
5
5
  import { Response } from "./response.mjs";
@@ -177,7 +177,7 @@ var Request = class Request {
177
177
  resolveLocale() {
178
178
  const candidate = [
179
179
  this.query["locale"],
180
- this.cookies["locale"],
180
+ this.cookies[LOCALE_COOKIE_NAME],
181
181
  this.header("locale")
182
182
  ].find((value) => typeof value === "string" && value.length > 0);
183
183
  return this.cacheLocale(candidate);
@@ -364,21 +364,28 @@ var Request = class Request {
364
364
  if (key.includes("][")) {
365
365
  const keyParts = key.split("[");
366
366
  const keyName = keyParts[0];
367
- const keyNameParts = keyParts[1].split("]");
367
+ const firstBracket = keyParts[1];
368
+ const secondBracket = keyParts[2];
369
+ if (keyName === void 0 || firstBracket === void 0 || secondBracket === void 0) {
370
+ set(body, this.bracketKeyToPath(key), this.arrayValueFor(value, isArrayKey, this.parseValue.bind(this)));
371
+ continue;
372
+ }
373
+ const keyNameParts = firstBracket.split("]");
368
374
  const index = Number(keyNameParts[0]);
369
375
  if (Number.isNaN(index)) {
370
376
  set(body, this.bracketKeyToPath(key), this.arrayValueFor(value, isArrayKey, this.parseValue.bind(this)));
371
377
  continue;
372
378
  }
373
- if (!arrayOfObjectValues[keyName]) arrayOfObjectValues[keyName] = [];
374
- if (!arrayOfObjectValues[keyName][index]) arrayOfObjectValues[keyName][index] = {};
375
- const keyName2 = keyParts[2].split("]")[0];
376
- arrayOfObjectValues[keyName][index][keyName2] = this.parseValue(value);
379
+ const bucket = arrayOfObjectValues[keyName] ??= [];
380
+ const entry = bucket[index] ??= {};
381
+ const keyName2 = secondBracket.split("]")[0];
382
+ if (keyName2 === void 0) continue;
383
+ entry[keyName2] = this.parseValue(value);
377
384
  continue;
378
385
  }
379
386
  const keyParts = key.split("[");
380
387
  const keyName = keyParts[0];
381
- const keyNameParts = keyParts[1].split("]");
388
+ const keyNameParts = (keyParts[1] ?? key).split("]");
382
389
  set(body, keyName + "." + keyNameParts[0], this.arrayValueFor(value, isArrayKey, this.parseValue.bind(this)));
383
390
  continue;
384
391
  }
@@ -827,7 +834,7 @@ var Request = class Request {
827
834
  if (config.get("http.trustProxy", false) === true) {
828
835
  const realIp = this.header("x-real-ip");
829
836
  if (realIp) {
830
- const address = String(realIp).split(",")[0].trim();
837
+ const address = (String(realIp).split(",")[0] ?? "").trim();
831
838
  if (address) return address;
832
839
  }
833
840
  }