@warlock.js/core 5.30.0 → 5.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -36
- package/README.md +28 -23
- package/bin/source-load-hook.mjs +63 -0
- package/bin/warlock.js +102 -42
- package/esm/cli/commands/add.command.mjs +4 -2
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/agents-md/agents-md-command.action.mjs +69 -0
- package/esm/cli/commands/agents-md/agents-md-command.action.mjs.map +1 -0
- package/esm/cli/commands/agents-md.command.mjs +38 -0
- package/esm/cli/commands/agents-md.command.mjs.map +1 -0
- package/esm/cli/commands/board/board-command.action.mjs +279 -0
- package/esm/cli/commands/board/board-command.action.mjs.map +1 -0
- package/esm/cli/commands/board.command.mjs +78 -0
- package/esm/cli/commands/board.command.mjs.map +1 -0
- package/esm/cli/commands/build.command.mjs +6 -1
- package/esm/cli/commands/build.command.mjs.map +1 -1
- package/esm/cli/commands/check/check-command.action.mjs +57 -0
- package/esm/cli/commands/check/check-command.action.mjs.map +1 -0
- package/esm/cli/commands/check/child-typecheck.mjs +66 -0
- package/esm/cli/commands/check/child-typecheck.mjs.map +1 -0
- package/esm/cli/commands/check/format-problems.mjs +43 -0
- package/esm/cli/commands/check/format-problems.mjs.map +1 -0
- package/esm/cli/commands/check/typecheck-command.action.mjs +61 -0
- package/esm/cli/commands/check/typecheck-command.action.mjs.map +1 -0
- package/esm/cli/commands/check.command.mjs +50 -0
- package/esm/cli/commands/check.command.mjs.map +1 -0
- package/esm/cli/commands/dev-server.command.mjs +9 -1
- package/esm/cli/commands/dev-server.command.mjs.map +1 -1
- package/esm/cli/commands/doctor/boot-for-diagnostics.mjs +8 -3
- package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -1
- package/esm/cli/commands/doctor/check.types.d.mts +76 -0
- package/esm/cli/commands/doctor/check.types.d.mts.map +1 -0
- package/esm/cli/commands/doctor/checks/config.check.mjs +1 -0
- package/esm/cli/commands/doctor/checks/config.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/connectors.check.mjs +2 -0
- package/esm/cli/commands/doctor/checks/connectors.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/handler-signature.check.mjs +1 -0
- package/esm/cli/commands/doctor/checks/handler-signature.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/health.check.mjs +3 -0
- package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/jwt-secret.check.mjs +1 -0
- package/esm/cli/commands/doctor/checks/jwt-secret.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs +1 -0
- package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs +1 -0
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs +4 -0
- package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/routes.check.mjs +3 -0
- package/esm/cli/commands/doctor/checks/routes.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/doctor-command.action.mjs +14 -6
- package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
- package/esm/cli/commands/doctor/format-report.mjs +30 -2
- package/esm/cli/commands/doctor/format-report.mjs.map +1 -1
- package/esm/cli/commands/doctor.command.mjs +4 -0
- package/esm/cli/commands/doctor.command.mjs.map +1 -1
- package/esm/cli/commands/explain/explain-command.action.mjs +64 -0
- package/esm/cli/commands/explain/explain-command.action.mjs.map +1 -0
- package/esm/cli/commands/explain.command.mjs +23 -0
- package/esm/cli/commands/explain.command.mjs.map +1 -0
- package/esm/cli/commands/generate/generate.command.mjs +124 -124
- package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/endpoint.generator.mjs +311 -0
- package/esm/cli/commands/generate/generators/endpoint.generator.mjs.map +1 -0
- package/esm/cli/commands/generate/generators/migration.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/model.generator.mjs +1 -1
- package/esm/cli/commands/generate/templates/endpoint-stubs.mjs +341 -0
- package/esm/cli/commands/generate/templates/endpoint-stubs.mjs.map +1 -0
- package/esm/cli/commands/generate/templates/stubs.mjs +19 -6
- package/esm/cli/commands/generate/templates/stubs.mjs.map +1 -1
- package/esm/cli/commands/generate/utils/endpoint-schema.mjs +137 -0
- package/esm/cli/commands/generate/utils/endpoint-schema.mjs.map +1 -0
- package/esm/cli/commands/generate/utils/name-parser.mjs +1 -1
- package/esm/cli/commands/generate/utils/name-parser.mjs.map +1 -1
- package/esm/cli/commands/generate/utils/prompt.mjs.map +1 -1
- package/esm/cli/commands/generate/utils/writer.mjs +38 -13
- package/esm/cli/commands/generate/utils/writer.mjs.map +1 -1
- package/esm/cli/commands/inspect/inspect-command.action.mjs +72 -0
- package/esm/cli/commands/inspect/inspect-command.action.mjs.map +1 -0
- package/esm/cli/commands/inspect.command.mjs +39 -0
- package/esm/cli/commands/inspect.command.mjs.map +1 -0
- package/esm/cli/commands/mcp/mcp-command.action.mjs +103 -0
- package/esm/cli/commands/mcp/mcp-command.action.mjs.map +1 -0
- package/esm/cli/commands/mcp.command.mjs +76 -0
- package/esm/cli/commands/mcp.command.mjs.map +1 -0
- package/esm/cli/commands/openapi-generator.action.mjs.map +1 -1
- package/esm/cli/commands/openapi-generator.command.mjs.map +1 -1
- package/esm/cli/commands/postman-generator.action.mjs.map +1 -1
- package/esm/cli/commands/postman-generator.command.mjs.map +1 -1
- package/esm/cli/commands/routes/routes-diff.action.mjs +1 -1
- package/esm/cli/commands/test.command.mjs +131 -0
- package/esm/cli/commands/test.command.mjs.map +1 -0
- package/esm/cli/commands/trace/trace-command.action.mjs +145 -0
- package/esm/cli/commands/trace/trace-command.action.mjs.map +1 -0
- package/esm/cli/commands/trace.command.mjs +60 -0
- package/esm/cli/commands/trace.command.mjs.map +1 -0
- package/esm/cli/commands/typecheck.command.mjs +40 -0
- package/esm/cli/commands/typecheck.command.mjs.map +1 -0
- package/esm/cli/commands/upgrade.command.mjs +46 -0
- package/esm/cli/commands/upgrade.command.mjs.map +1 -0
- package/esm/cli/flush-and-exit.mjs +34 -0
- package/esm/cli/flush-and-exit.mjs.map +1 -0
- package/esm/cli/framework-cli-commands.mjs +23 -1
- package/esm/cli/framework-cli-commands.mjs.map +1 -1
- package/esm/cli/parse-cli-args.mjs +1 -0
- package/esm/cli/parse-cli-args.mjs.map +1 -1
- package/esm/config/config-loader.d.mts.map +1 -1
- package/esm/config/config-loader.mjs +34 -13
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/config/config-schema.d.mts +67 -0
- package/esm/config/config-schema.d.mts.map +1 -0
- package/esm/config/config-schema.mjs +77 -0
- package/esm/config/config-schema.mjs.map +1 -0
- package/esm/config/index.d.mts +6 -0
- package/esm/config/index.mjs +1 -0
- package/esm/connectors/assert-no-reserved-connector-names.d.mts.map +1 -1
- package/esm/connectors/assert-no-reserved-connector-names.mjs +2 -1
- package/esm/connectors/assert-no-reserved-connector-names.mjs.map +1 -1
- package/esm/connectors/assert-unique-connector-names.d.mts.map +1 -1
- package/esm/connectors/assert-unique-connector-names.mjs +26 -2
- package/esm/connectors/assert-unique-connector-names.mjs.map +1 -1
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +3 -2
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/connectors/single-server-warnings.mjs.map +1 -1
- package/esm/connectors/socket-connector.d.mts.map +1 -1
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/container/index.d.mts.map +1 -1
- package/esm/container/index.mjs.map +1 -1
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/database/models/database-log/database-log.d.mts +0 -28
- package/esm/database/models/database-log/database-log.d.mts.map +1 -1
- package/esm/database/seeds/seeder.errors.d.mts +4 -0
- package/esm/database/seeds/seeder.errors.d.mts.map +1 -1
- package/esm/database/seeds/seeder.errors.mjs +2 -0
- package/esm/database/seeds/seeder.errors.mjs.map +1 -1
- package/esm/dev-server/boot-precondition-error.mjs +1 -0
- package/esm/dev-server/boot-precondition-error.mjs.map +1 -1
- package/esm/dev-server/create-worker.mjs +15 -9
- package/esm/dev-server/create-worker.mjs.map +1 -1
- package/esm/dev-server/dev-agent-tooling.mjs +135 -0
- package/esm/dev-server/dev-agent-tooling.mjs.map +1 -0
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +8 -0
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +4 -5
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +1 -1
- package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs +2 -3
- package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs.map +1 -1
- package/esm/dev-server/health-checker/checkers/oxlint-health-checker.mjs +33 -0
- package/esm/dev-server/health-checker/checkers/oxlint-health-checker.mjs.map +1 -0
- package/esm/dev-server/health-checker/default-health-checkers.mjs +31 -0
- package/esm/dev-server/health-checker/default-health-checkers.mjs.map +1 -0
- package/esm/dev-server/health-checker/file-health-checker.contract.d.mts +9 -3
- package/esm/dev-server/health-checker/file-health-checker.contract.d.mts.map +1 -1
- package/esm/dev-server/health-checker/files-healthcare.manager.mjs +15 -4
- package/esm/dev-server/health-checker/files-healthcare.manager.mjs.map +1 -1
- package/esm/dev-server/health-checker/oxlint-runner.mjs +302 -0
- package/esm/dev-server/health-checker/oxlint-runner.mjs.map +1 -0
- package/esm/dev-server/health-checker/workers/oxlint-health.worker.d.mts +1 -0
- package/esm/dev-server/health-checker/workers/oxlint-health.worker.mjs +42 -0
- package/esm/dev-server/health-checker/workers/oxlint-health.worker.mjs.map +1 -0
- package/esm/dev-server/loader/loader-port-acks.mjs +1 -1
- package/esm/dev-server/loader/loader-port-acks.mjs.map +1 -1
- package/esm/dev-server/loader/register-loader.mjs +1 -1
- package/esm/dev-server/loader/register-loader.mjs.map +1 -1
- package/esm/dev-server/loader/resolve-hook.mjs +1 -1
- package/esm/dev-server/loader/transpile-cache.mjs +1 -1
- package/esm/dev-server/model-resource-typings.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs +1 -0
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/parse-imports.mjs +1 -1
- package/esm/dev-server/parse-imports.mjs.map +1 -1
- package/esm/dev-server/route-types-publisher.mjs +1 -1
- package/esm/dev-server/start-development-server.mjs.map +1 -1
- package/esm/dev-server/translation-keys-sources.mjs +1 -1
- package/esm/dev-server/translation-keys-sources.mjs.map +1 -1
- package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs +3 -3
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/dev-server/types.d.mts.map +1 -1
- package/esm/errors/container-key-missing-error.mjs +1 -0
- package/esm/errors/container-key-missing-error.mjs.map +1 -1
- package/esm/errors/error-codes.d.mts +653 -0
- package/esm/errors/error-codes.d.mts.map +1 -0
- package/esm/errors/error-codes.mjs +634 -0
- package/esm/errors/error-codes.mjs.map +1 -0
- package/esm/errors/esbuild-binary-missing-error.mjs +1 -0
- package/esm/errors/esbuild-binary-missing-error.mjs.map +1 -1
- package/esm/errors/index.d.mts +3 -1
- package/esm/errors/index.mjs +3 -1
- package/esm/errors/unknown-locale-error.mjs +1 -0
- package/esm/errors/unknown-locale-error.mjs.map +1 -1
- package/esm/errors/warlock-error.d.mts +23 -0
- package/esm/errors/warlock-error.d.mts.map +1 -0
- package/esm/errors/warlock-error.mjs +22 -0
- package/esm/errors/warlock-error.mjs.map +1 -0
- package/esm/generations/add-command.action.mjs +19 -3
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/features/access.feature.mjs +1 -1
- package/esm/generations/features/access.feature.mjs.map +1 -1
- package/esm/generations/features/bull-board.feature.mjs +1 -1
- package/esm/generations/features/bull-board.feature.mjs.map +1 -1
- package/esm/generations/features/mysql.feature.mjs.map +1 -1
- package/esm/generations/features/notifications.feature.mjs +5 -2
- package/esm/generations/features/notifications.feature.mjs.map +1 -1
- package/esm/generations/features/queue.feature.mjs +1 -1
- package/esm/generations/features/queue.feature.mjs.map +1 -1
- package/esm/generations/features/react-email.feature.mjs +1 -1
- package/esm/generations/features/react-email.feature.mjs.map +1 -1
- package/esm/generations/features/shadcn.feature.mjs +1 -1
- package/esm/generations/features/shadcn.feature.mjs.map +1 -1
- package/esm/generations/features/shared/link-ai-package-import.mjs +1 -1
- package/esm/generations/features/shared/link-ai-package-import.mjs.map +1 -1
- package/esm/generations/features/shared/merge-web-sitemap-config.mjs.map +1 -1
- package/esm/generations/features/shared/patch-tsconfig-include.mjs +1 -1
- package/esm/generations/features/shared/patch-tsconfig-include.mjs.map +1 -1
- package/esm/generations/features/shared/relocate-conflicting-home-route.mjs +1 -1
- package/esm/generations/features/shared/relocate-conflicting-home-route.mjs.map +1 -1
- package/esm/generations/features/sitemap.feature.mjs +1 -1
- package/esm/generations/features/sitemap.feature.mjs.map +1 -1
- package/esm/generations/features/tailwind.feature.mjs +1 -1
- package/esm/generations/features/tailwind.feature.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +11 -6
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/features/web.feature.mjs +1 -1
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/generations/generation-fs.mjs +73 -0
- package/esm/generations/generation-fs.mjs.map +1 -0
- package/esm/generations/generation-run.mjs +185 -0
- package/esm/generations/generation-run.mjs.map +1 -0
- package/esm/generations/stubs.mjs +155 -13
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/bounded-stream-queue.d.mts +10 -0
- package/esm/http/bounded-stream-queue.d.mts.map +1 -0
- package/esm/http/bounded-stream-queue.mjs +97 -0
- package/esm/http/bounded-stream-queue.mjs.map +1 -0
- package/esm/http/build-rate-limit-options.d.mts +13 -0
- package/esm/http/build-rate-limit-options.d.mts.map +1 -1
- package/esm/http/build-rate-limit-options.mjs.map +1 -1
- package/esm/http/conditional-request.d.mts +86 -0
- package/esm/http/conditional-request.d.mts.map +1 -0
- package/esm/http/conditional-request.mjs +121 -0
- package/esm/http/conditional-request.mjs.map +1 -0
- package/esm/http/csp.d.mts +2 -0
- package/esm/http/csp.d.mts.map +1 -1
- package/esm/http/csp.mjs +1 -0
- package/esm/http/csp.mjs.map +1 -1
- package/esm/http/csrf-origin-policy.mjs.map +1 -1
- package/esm/http/database/RequestLog.d.mts +24 -84
- package/esm/http/database/RequestLog.d.mts.map +1 -1
- package/esm/http/errors/errors.d.mts +2 -0
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +1 -0
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/escape-header-value.d.mts +18 -0
- package/esm/http/escape-header-value.d.mts.map +1 -0
- package/esm/http/escape-header-value.mjs +27 -0
- package/esm/http/escape-header-value.mjs.map +1 -0
- package/esm/http/flush-pending-cookies.mjs.map +1 -1
- package/esm/http/index.d.mts +5 -2
- package/esm/http/index.mjs +4 -1
- package/esm/http/lazy-static.mjs +194 -0
- package/esm/http/lazy-static.mjs.map +1 -0
- package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
- package/esm/http/plugins.d.mts.map +1 -1
- package/esm/http/plugins.mjs +7 -4
- package/esm/http/plugins.mjs.map +1 -1
- package/esm/http/rate-limit-cache-store.mjs +2 -1
- package/esm/http/rate-limit-cache-store.mjs.map +1 -1
- package/esm/http/request-log-snapshot.d.mts +12 -5
- package/esm/http/request-log-snapshot.d.mts.map +1 -1
- package/esm/http/request-log-snapshot.mjs +83 -4
- package/esm/http/request-log-snapshot.mjs.map +1 -1
- package/esm/http/request.d.mts +2 -2
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +5 -4
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +11 -1
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +164 -76
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/safe-redirect.d.mts +51 -0
- package/esm/http/safe-redirect.d.mts.map +1 -0
- package/esm/http/safe-redirect.mjs +80 -0
- package/esm/http/safe-redirect.mjs.map +1 -0
- package/esm/http/serialized.d.mts.map +1 -1
- package/esm/http/server.d.mts +3 -1
- package/esm/http/server.d.mts.map +1 -1
- package/esm/http/server.mjs +14 -4
- package/esm/http/server.mjs.map +1 -1
- package/esm/http/stream-transport.mjs +4 -2
- package/esm/http/stream-transport.mjs.map +1 -1
- package/esm/http/types.d.mts +44 -11
- package/esm/http/types.d.mts.map +1 -1
- package/esm/http/uploads/generate-image-variant.mjs +1 -1
- package/esm/http/uploads/generate-image-variants.d.mts +10 -7
- package/esm/http/uploads/generate-image-variants.d.mts.map +1 -1
- package/esm/http/uploads/generate-image-variants.mjs +61 -24
- package/esm/http/uploads/generate-image-variants.mjs.map +1 -1
- package/esm/http/uploads/image-variant-cache-entries.mjs +78 -0
- package/esm/http/uploads/image-variant-cache-entries.mjs.map +1 -0
- package/esm/http/uploads/image-variant-cache.mjs +72 -30
- package/esm/http/uploads/image-variant-cache.mjs.map +1 -1
- package/esm/http/uploads/image-variant-types.d.mts +2 -0
- package/esm/http/uploads/image-variant-types.d.mts.map +1 -1
- package/esm/http/uploads/image-variants-config-error.d.mts +3 -1
- package/esm/http/uploads/image-variants-config-error.d.mts.map +1 -1
- package/esm/http/uploads/image-variants-config-error.mjs +3 -2
- package/esm/http/uploads/image-variants-config-error.mjs.map +1 -1
- package/esm/http/uploads/load-variant-source.mjs +1 -1
- package/esm/http/uploads/load-variant-source.mjs.map +1 -1
- package/esm/http/uploads/purge-image-variants.d.mts +7 -2
- package/esm/http/uploads/purge-image-variants.d.mts.map +1 -1
- package/esm/http/uploads/purge-image-variants.mjs +16 -16
- package/esm/http/uploads/purge-image-variants.mjs.map +1 -1
- package/esm/http/uploads/remote-variant-source.mjs +115 -0
- package/esm/http/uploads/remote-variant-source.mjs.map +1 -0
- package/esm/http/uploads/resolve-image-variant.d.mts +3 -4
- package/esm/http/uploads/resolve-image-variant.d.mts.map +1 -1
- package/esm/http/uploads/resolve-image-variant.mjs +10 -18
- package/esm/http/uploads/resolve-image-variant.mjs.map +1 -1
- package/esm/http/uploads/resolve-image-variants-config.mjs +5 -3
- package/esm/http/uploads/resolve-image-variants-config.mjs.map +1 -1
- package/esm/http/uploads/resolve-remote-image-variant.mjs +89 -0
- package/esm/http/uploads/resolve-remote-image-variant.mjs.map +1 -0
- package/esm/http/uploads/trim-image-variant-cache.mjs +193 -0
- package/esm/http/uploads/trim-image-variant-cache.mjs.map +1 -0
- package/esm/http/uploads/uploaded-file.controller.d.mts +5 -3
- package/esm/http/uploads/uploaded-file.controller.d.mts.map +1 -1
- package/esm/http/uploads/uploaded-file.controller.mjs +16 -8
- package/esm/http/uploads/uploaded-file.controller.mjs.map +1 -1
- package/esm/http/uploads/variant-cache-key.mjs +1 -1
- package/esm/http/uploads-types.d.mts +6 -2
- package/esm/http/uploads-types.d.mts.map +1 -1
- package/esm/index.d.mts +17 -6
- package/esm/index.mjs +14 -3
- package/esm/localization/parse-locale-dictionary.d.mts +2 -0
- package/esm/localization/parse-locale-dictionary.d.mts.map +1 -1
- package/esm/localization/parse-locale-dictionary.mjs +1 -0
- package/esm/localization/parse-locale-dictionary.mjs.map +1 -1
- package/esm/mail/config.d.mts +12 -1
- package/esm/mail/config.d.mts.map +1 -1
- package/esm/mail/config.mjs +16 -1
- package/esm/mail/config.mjs.map +1 -1
- package/esm/mail/index.d.mts +1 -1
- package/esm/mail/index.mjs +1 -1
- package/esm/net/index.d.mts +2 -0
- package/esm/net/index.mjs +3 -0
- package/esm/openapi/build-openapi-document.d.mts.map +1 -1
- package/esm/openapi/build-openapi-document.mjs +8 -5
- package/esm/openapi/build-openapi-document.mjs.map +1 -1
- package/esm/openapi/error-responses.mjs +28 -2
- package/esm/openapi/error-responses.mjs.map +1 -1
- package/esm/openapi/postman/build-request-body.mjs.map +1 -1
- package/esm/openapi/postman/build-request-url.mjs.map +1 -1
- package/esm/openapi/postman/build-saved-responses.mjs.map +1 -1
- package/esm/openapi/postman/example-from-schema.d.mts.map +1 -1
- package/esm/openapi/postman/example-from-schema.mjs.map +1 -1
- package/esm/openapi/postman/openapi-to-postman-collection.d.mts.map +1 -1
- package/esm/openapi/postman/openapi-to-postman-collection.mjs.map +1 -1
- package/esm/openapi/postman/schema-refs.mjs.map +1 -1
- package/esm/openapi/postman/uuid-v5.mjs.map +1 -1
- package/esm/openapi/request-schemas.mjs.map +1 -1
- package/esm/openapi/response-schemas.mjs.map +1 -1
- package/esm/openapi/schema-registry.d.mts +1 -0
- package/esm/openapi/schema-registry.mjs.map +1 -1
- package/esm/openapi/security.mjs.map +1 -1
- package/esm/openapi/types.d.mts +3 -2
- package/esm/openapi/types.d.mts.map +1 -1
- package/esm/production/assert-generated-imports.mjs +1 -0
- package/esm/production/assert-generated-imports.mjs.map +1 -1
- package/esm/production/production-builder.mjs +12 -4
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/resolve-build-config.mjs +1 -0
- package/esm/production/resolve-build-config.mjs.map +1 -1
- package/esm/production/route-registration-child.mjs.map +1 -1
- package/esm/production/route-registration-snapshot.mjs.map +1 -1
- package/esm/repositories/contracts/query-builder.contract.d.mts.map +1 -1
- package/esm/repositories/contracts/types.d.mts.map +1 -1
- package/esm/repositories/repository.manager.mjs.map +1 -1
- package/esm/resource/resource.d.mts.map +1 -1
- package/esm/resource/resource.mjs.map +1 -1
- package/esm/resource/types.d.mts.map +1 -1
- package/esm/restful/restful.d.mts +5 -5
- package/esm/restful/restful.d.mts.map +1 -1
- package/esm/restful/restful.mjs.map +1 -1
- package/esm/router/build-route-rate-limit.mjs.map +1 -1
- package/esm/router/dev-dispatch.mjs +31 -12
- package/esm/router/dev-dispatch.mjs.map +1 -1
- package/esm/router/index.d.mts +4 -2
- package/esm/router/index.mjs +2 -0
- package/esm/router/log-request-lifecycle.mjs.map +1 -1
- package/esm/router/positional-handler-diagnostics.d.mts.map +1 -1
- package/esm/router/positional-handler-diagnostics.mjs.map +1 -1
- package/esm/router/resolve-route-rate-limit.mjs +47 -0
- package/esm/router/resolve-route-rate-limit.mjs.map +1 -0
- package/esm/router/resolve-route-validation.mjs +19 -0
- package/esm/router/resolve-route-validation.mjs.map +1 -0
- package/esm/router/route-guard-names.mjs +30 -0
- package/esm/router/route-guard-names.mjs.map +1 -0
- package/esm/router/route-name-taken-error.d.mts +14 -0
- package/esm/router/route-name-taken-error.d.mts.map +1 -0
- package/esm/router/route-name-taken-error.mjs +18 -0
- package/esm/router/route-name-taken-error.mjs.map +1 -0
- package/esm/router/router.d.mts +32 -13
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +71 -38
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +137 -17
- package/esm/router/types.d.mts.map +1 -1
- package/esm/router/unguarded-route-diagnostics.d.mts +40 -0
- package/esm/router/unguarded-route-diagnostics.d.mts.map +1 -0
- package/esm/router/unguarded-route-diagnostics.mjs +95 -0
- package/esm/router/unguarded-route-diagnostics.mjs.map +1 -0
- package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
- package/esm/storage/drivers/local-driver.d.mts.map +1 -1
- package/esm/storage/drivers/local-driver.mjs.map +1 -1
- package/esm/storage/local-disk-roots.mjs +24 -0
- package/esm/storage/local-disk-roots.mjs.map +1 -0
- package/esm/storage/storage.d.mts +14 -0
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +22 -0
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/types.d.mts.map +1 -1
- package/esm/storage/utils/storage-not-initialized-error.d.mts +2 -0
- package/esm/storage/utils/storage-not-initialized-error.d.mts.map +1 -1
- package/esm/storage/utils/storage-not-initialized-error.mjs +1 -0
- package/esm/storage/utils/storage-not-initialized-error.mjs.map +1 -1
- package/esm/tests/authenticated-test-client.d.mts +32 -0
- package/esm/tests/authenticated-test-client.d.mts.map +1 -0
- package/esm/tests/authenticated-test-client.mjs +40 -0
- package/esm/tests/authenticated-test-client.mjs.map +1 -0
- package/esm/tests/factories.d.mts +59 -0
- package/esm/tests/factories.d.mts.map +1 -0
- package/esm/tests/factories.mjs +105 -0
- package/esm/tests/factories.mjs.map +1 -0
- package/esm/tests/fakes/cache-fake.d.mts +29 -0
- package/esm/tests/fakes/cache-fake.d.mts.map +1 -0
- package/esm/tests/fakes/cache-fake.mjs +79 -0
- package/esm/tests/fakes/cache-fake.mjs.map +1 -0
- package/esm/tests/fakes/clock-fake.d.mts +32 -0
- package/esm/tests/fakes/clock-fake.d.mts.map +1 -0
- package/esm/tests/fakes/clock-fake.mjs +69 -0
- package/esm/tests/fakes/clock-fake.mjs.map +1 -0
- package/esm/tests/fakes/fake-assertion-error.d.mts +13 -0
- package/esm/tests/fakes/fake-assertion-error.d.mts.map +1 -0
- package/esm/tests/fakes/fake-assertion-error.mjs +20 -0
- package/esm/tests/fakes/fake-assertion-error.mjs.map +1 -0
- package/esm/tests/fakes/ids-fake.d.mts +22 -0
- package/esm/tests/fakes/ids-fake.d.mts.map +1 -0
- package/esm/tests/fakes/ids-fake.mjs +57 -0
- package/esm/tests/fakes/ids-fake.mjs.map +1 -0
- package/esm/tests/fakes/index.d.mts +8 -0
- package/esm/tests/fakes/index.mjs +10 -0
- package/esm/tests/fakes/mail-fake.d.mts +41 -0
- package/esm/tests/fakes/mail-fake.d.mts.map +1 -0
- package/esm/tests/fakes/mail-fake.mjs +111 -0
- package/esm/tests/fakes/mail-fake.mjs.map +1 -0
- package/esm/tests/fakes/queue-fake.d.mts +48 -0
- package/esm/tests/fakes/queue-fake.d.mts.map +1 -0
- package/esm/tests/fakes/queue-fake.mjs +134 -0
- package/esm/tests/fakes/queue-fake.mjs.map +1 -0
- package/esm/tests/fakes/storage-fake.d.mts +31 -0
- package/esm/tests/fakes/storage-fake.d.mts.map +1 -0
- package/esm/tests/fakes/storage-fake.mjs +122 -0
- package/esm/tests/fakes/storage-fake.mjs.map +1 -0
- package/esm/tests/fakes/test-fakes.d.mts +39 -0
- package/esm/tests/fakes/test-fakes.d.mts.map +1 -0
- package/esm/tests/fakes/test-fakes.mjs +90 -0
- package/esm/tests/fakes/test-fakes.mjs.map +1 -0
- package/esm/tests/index.d.mts +14 -2
- package/esm/tests/index.mjs +15 -2
- package/esm/tests/snapshot-response.d.mts +34 -0
- package/esm/tests/snapshot-response.d.mts.map +1 -0
- package/esm/tests/snapshot-response.mjs +42 -0
- package/esm/tests/snapshot-response.mjs.map +1 -0
- package/esm/tests/start-http-development-server.d.mts +22 -0
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs +36 -3
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/tests/test-helpers.d.mts +20 -1
- package/esm/tests/test-helpers.d.mts.map +1 -1
- package/esm/tests/test-helpers.mjs +40 -27
- package/esm/tests/test-helpers.mjs.map +1 -1
- package/esm/tests/test-http-server.d.mts +62 -0
- package/esm/tests/test-http-server.d.mts.map +1 -0
- package/esm/tests/test-http-server.mjs +119 -0
- package/esm/tests/test-http-server.mjs.map +1 -0
- package/esm/tests/vitest-setup.d.mts +11 -0
- package/esm/tests/vitest-setup.d.mts.map +1 -1
- package/esm/tests/vitest-setup.mjs +12 -4
- package/esm/tests/vitest-setup.mjs.map +1 -1
- package/esm/tooling/agents-md/agents-md-markers.d.mts +27 -0
- package/esm/tooling/agents-md/agents-md-markers.d.mts.map +1 -0
- package/esm/tooling/agents-md/agents-md-markers.mjs +59 -0
- package/esm/tooling/agents-md/agents-md-markers.mjs.map +1 -0
- package/esm/tooling/agents-md/agents-md-refresher.d.mts +36 -0
- package/esm/tooling/agents-md/agents-md-refresher.d.mts.map +1 -0
- package/esm/tooling/agents-md/agents-md-refresher.mjs +72 -0
- package/esm/tooling/agents-md/agents-md-refresher.mjs.map +1 -0
- package/esm/tooling/agents-md/index.mjs +7 -0
- package/esm/tooling/agents-md/installed-skills.d.mts +30 -0
- package/esm/tooling/agents-md/installed-skills.d.mts.map +1 -0
- package/esm/tooling/agents-md/installed-skills.mjs +74 -0
- package/esm/tooling/agents-md/installed-skills.mjs.map +1 -0
- package/esm/tooling/agents-md/render-agents-md.d.mts +33 -0
- package/esm/tooling/agents-md/render-agents-md.d.mts.map +1 -0
- package/esm/tooling/agents-md/render-agents-md.mjs +195 -0
- package/esm/tooling/agents-md/render-agents-md.mjs.map +1 -0
- package/esm/tooling/agents-md/write-agents-md.d.mts +33 -0
- package/esm/tooling/agents-md/write-agents-md.d.mts.map +1 -0
- package/esm/tooling/agents-md/write-agents-md.mjs +49 -0
- package/esm/tooling/agents-md/write-agents-md.mjs.map +1 -0
- package/esm/tooling/board/board-message.d.mts +31 -0
- package/esm/tooling/board/board-message.d.mts.map +1 -0
- package/esm/tooling/board/board-message.mjs +86 -0
- package/esm/tooling/board/board-message.mjs.map +1 -0
- package/esm/tooling/board/board.d.mts +109 -0
- package/esm/tooling/board/board.d.mts.map +1 -0
- package/esm/tooling/board/board.mjs +406 -0
- package/esm/tooling/board/board.mjs.map +1 -0
- package/esm/tooling/board/board.types.d.mts +72 -0
- package/esm/tooling/board/board.types.d.mts.map +1 -0
- package/esm/tooling/board/board.types.mjs +54 -0
- package/esm/tooling/board/board.types.mjs.map +1 -0
- package/esm/tooling/check/changed-files.d.mts +33 -0
- package/esm/tooling/check/changed-files.d.mts.map +1 -0
- package/esm/tooling/check/changed-files.mjs +73 -0
- package/esm/tooling/check/changed-files.mjs.map +1 -0
- package/esm/tooling/check/check-report.types.d.mts +62 -0
- package/esm/tooling/check/check-report.types.d.mts.map +1 -0
- package/esm/tooling/check/check-report.types.mjs +37 -0
- package/esm/tooling/check/check-report.types.mjs.map +1 -0
- package/esm/tooling/check/code-style-conventions.mjs +106 -0
- package/esm/tooling/check/code-style-conventions.mjs.map +1 -0
- package/esm/tooling/check/conventions-step.d.mts +34 -0
- package/esm/tooling/check/conventions-step.d.mts.map +1 -0
- package/esm/tooling/check/conventions-step.mjs +184 -0
- package/esm/tooling/check/conventions-step.mjs.map +1 -0
- package/esm/tooling/check/environment-step.d.mts +18 -0
- package/esm/tooling/check/environment-step.d.mts.map +1 -0
- package/esm/tooling/check/environment-step.mjs +78 -0
- package/esm/tooling/check/environment-step.mjs.map +1 -0
- package/esm/tooling/check/index.d.mts +8 -0
- package/esm/tooling/check/index.mjs +10 -0
- package/esm/tooling/check/lint-step.d.mts +19 -0
- package/esm/tooling/check/lint-step.d.mts.map +1 -0
- package/esm/tooling/check/lint-step.mjs +111 -0
- package/esm/tooling/check/lint-step.mjs.map +1 -0
- package/esm/tooling/check/request-input.mjs +132 -0
- package/esm/tooling/check/request-input.mjs.map +1 -0
- package/esm/tooling/check/run-check.d.mts +35 -0
- package/esm/tooling/check/run-check.d.mts.map +1 -0
- package/esm/tooling/check/run-check.mjs +192 -0
- package/esm/tooling/check/run-check.mjs.map +1 -0
- package/esm/tooling/check/run-tests.d.mts +48 -0
- package/esm/tooling/check/run-tests.d.mts.map +1 -0
- package/esm/tooling/check/run-tests.mjs +93 -0
- package/esm/tooling/check/run-tests.mjs.map +1 -0
- package/esm/tooling/check/security-conventions.mjs +648 -0
- package/esm/tooling/check/security-conventions.mjs.map +1 -0
- package/esm/tooling/check/source-ast.mjs +195 -0
- package/esm/tooling/check/source-ast.mjs.map +1 -0
- package/esm/tooling/check/source-files.mjs +27 -0
- package/esm/tooling/check/source-files.mjs.map +1 -0
- package/esm/tooling/check/tests-step.d.mts +34 -0
- package/esm/tooling/check/tests-step.d.mts.map +1 -0
- package/esm/tooling/check/tests-step.mjs +177 -0
- package/esm/tooling/check/tests-step.mjs.map +1 -0
- package/esm/tooling/dev-state/dev-server-state.d.mts +39 -0
- package/esm/tooling/dev-state/dev-server-state.d.mts.map +1 -0
- package/esm/tooling/dev-state/dev-server-state.mjs +67 -0
- package/esm/tooling/dev-state/dev-server-state.mjs.map +1 -0
- package/esm/tooling/devtools/devtools-client.d.mts +91 -0
- package/esm/tooling/devtools/devtools-client.d.mts.map +1 -0
- package/esm/tooling/devtools/devtools-client.mjs +155 -0
- package/esm/tooling/devtools/devtools-client.mjs.map +1 -0
- package/esm/tooling/generate/generation.d.mts +78 -0
- package/esm/tooling/generate/generation.d.mts.map +1 -0
- package/esm/tooling/generate/generation.mjs +123 -0
- package/esm/tooling/generate/generation.mjs.map +1 -0
- package/esm/tooling/generate/generator-spec.d.mts +57 -0
- package/esm/tooling/generate/generator-spec.d.mts.map +1 -0
- package/esm/tooling/generate/generator-spec.mjs +102 -0
- package/esm/tooling/generate/generator-spec.mjs.map +1 -0
- package/esm/tooling/generate/index.mjs +6 -0
- package/esm/tooling/generate/project-files.mjs +121 -0
- package/esm/tooling/generate/project-files.mjs.map +1 -0
- package/esm/tooling/generate/run-cli.d.mts +40 -0
- package/esm/tooling/generate/run-cli.d.mts.map +1 -0
- package/esm/tooling/generate/run-cli.mjs +93 -0
- package/esm/tooling/generate/run-cli.mjs.map +1 -0
- package/esm/tooling/generate/unified-diff.d.mts +37 -0
- package/esm/tooling/generate/unified-diff.d.mts.map +1 -0
- package/esm/tooling/generate/unified-diff.mjs +161 -0
- package/esm/tooling/generate/unified-diff.mjs.map +1 -0
- package/esm/tooling/index.d.mts +41 -0
- package/esm/tooling/index.mjs +45 -0
- package/esm/tooling/inspect/boot-for-inspect.d.mts +23 -0
- package/esm/tooling/inspect/boot-for-inspect.d.mts.map +1 -0
- package/esm/tooling/inspect/boot-for-inspect.mjs +63 -0
- package/esm/tooling/inspect/boot-for-inspect.mjs.map +1 -0
- package/esm/tooling/inspect/collect-config.mjs +77 -0
- package/esm/tooling/inspect/collect-config.mjs.map +1 -0
- package/esm/tooling/inspect/collect-models.d.mts +9 -0
- package/esm/tooling/inspect/collect-models.d.mts.map +1 -0
- package/esm/tooling/inspect/collect-models.mjs +154 -0
- package/esm/tooling/inspect/collect-models.mjs.map +1 -0
- package/esm/tooling/inspect/collect-packages.d.mts +10 -0
- package/esm/tooling/inspect/collect-packages.d.mts.map +1 -0
- package/esm/tooling/inspect/collect-packages.mjs +157 -0
- package/esm/tooling/inspect/collect-packages.mjs.map +1 -0
- package/esm/tooling/inspect/collect-pages.mjs +108 -0
- package/esm/tooling/inspect/collect-pages.mjs.map +1 -0
- package/esm/tooling/inspect/collect-routes.mjs +168 -0
- package/esm/tooling/inspect/collect-routes.mjs.map +1 -0
- package/esm/tooling/inspect/event-recorder.d.mts +37 -0
- package/esm/tooling/inspect/event-recorder.d.mts.map +1 -0
- package/esm/tooling/inspect/event-recorder.mjs +91 -0
- package/esm/tooling/inspect/event-recorder.mjs.map +1 -0
- package/esm/tooling/inspect/index.d.mts +7 -0
- package/esm/tooling/inspect/index.mjs +7 -0
- package/esm/tooling/inspect/inspect-app.d.mts +53 -0
- package/esm/tooling/inspect/inspect-app.d.mts.map +1 -0
- package/esm/tooling/inspect/inspect-app.mjs +92 -0
- package/esm/tooling/inspect/inspect-app.mjs.map +1 -0
- package/esm/tooling/inspect/inspect-report.schema.d.mts +15 -0
- package/esm/tooling/inspect/inspect-report.schema.d.mts.map +1 -0
- package/esm/tooling/inspect/inspect-report.schema.mjs +463 -0
- package/esm/tooling/inspect/inspect-report.schema.mjs.map +1 -0
- package/esm/tooling/inspect/inspect-report.types.d.mts +196 -0
- package/esm/tooling/inspect/inspect-report.types.d.mts.map +1 -0
- package/esm/tooling/inspect/inspect-report.types.mjs +33 -0
- package/esm/tooling/inspect/inspect-report.types.mjs.map +1 -0
- package/esm/tooling/mcp/create-mcp-context.d.mts +26 -0
- package/esm/tooling/mcp/create-mcp-context.d.mts.map +1 -0
- package/esm/tooling/mcp/create-mcp-context.mjs +75 -0
- package/esm/tooling/mcp/create-mcp-context.mjs.map +1 -0
- package/esm/tooling/mcp/db-schema.d.mts +49 -0
- package/esm/tooling/mcp/db-schema.d.mts.map +1 -0
- package/esm/tooling/mcp/db-schema.mjs +56 -0
- package/esm/tooling/mcp/db-schema.mjs.map +1 -0
- package/esm/tooling/mcp/docs-search.d.mts +72 -0
- package/esm/tooling/mcp/docs-search.d.mts.map +1 -0
- package/esm/tooling/mcp/docs-search.mjs +178 -0
- package/esm/tooling/mcp/docs-search.mjs.map +1 -0
- package/esm/tooling/mcp/index.mjs +9 -0
- package/esm/tooling/mcp/load-ai-tools.d.mts +62 -0
- package/esm/tooling/mcp/load-ai-tools.d.mts.map +1 -0
- package/esm/tooling/mcp/load-ai-tools.mjs +44 -0
- package/esm/tooling/mcp/load-ai-tools.mjs.map +1 -0
- package/esm/tooling/mcp/mcp-tools.d.mts +60 -0
- package/esm/tooling/mcp/mcp-tools.d.mts.map +1 -0
- package/esm/tooling/mcp/mcp-tools.mjs +513 -0
- package/esm/tooling/mcp/mcp-tools.mjs.map +1 -0
- package/esm/tooling/mcp/start-mcp-server.d.mts +56 -0
- package/esm/tooling/mcp/start-mcp-server.d.mts.map +1 -0
- package/esm/tooling/mcp/start-mcp-server.mjs +126 -0
- package/esm/tooling/mcp/start-mcp-server.mjs.map +1 -0
- package/esm/tooling/mcp/tool-input.d.mts +75 -0
- package/esm/tooling/mcp/tool-input.d.mts.map +1 -0
- package/esm/tooling/mcp/tool-input.mjs +72 -0
- package/esm/tooling/mcp/tool-input.mjs.map +1 -0
- package/esm/tooling/problem.d.mts +45 -0
- package/esm/tooling/problem.d.mts.map +1 -0
- package/esm/tooling/problem.mjs +28 -0
- package/esm/tooling/problem.mjs.map +1 -0
- package/esm/tooling/trace/trace-report.types.d.mts +71 -0
- package/esm/tooling/trace/trace-report.types.d.mts.map +1 -0
- package/esm/tooling/trace/trace-report.types.mjs +7 -0
- package/esm/tooling/trace/trace-report.types.mjs.map +1 -0
- package/esm/tooling/trace/trace-request.d.mts +83 -0
- package/esm/tooling/trace/trace-request.d.mts.map +1 -0
- package/esm/tooling/trace/trace-request.mjs +241 -0
- package/esm/tooling/trace/trace-request.mjs.map +1 -0
- package/esm/tooling/typecheck/index.d.mts +1 -0
- package/esm/tooling/typecheck/index.mjs +3 -0
- package/esm/tooling/typecheck/run-typecheck.d.mts +59 -0
- package/esm/tooling/typecheck/run-typecheck.d.mts.map +1 -0
- package/esm/tooling/typecheck/run-typecheck.mjs +153 -0
- package/esm/tooling/typecheck/run-typecheck.mjs.map +1 -0
- package/esm/upgrade/codemods/head-viewport.mjs +46 -0
- package/esm/upgrade/codemods/head-viewport.mjs.map +1 -0
- package/esm/upgrade/codemods/helpers.mjs +81 -0
- package/esm/upgrade/codemods/helpers.mjs.map +1 -0
- package/esm/upgrade/codemods/index.mjs +34 -0
- package/esm/upgrade/codemods/index.mjs.map +1 -0
- package/esm/upgrade/codemods/logger-sync-calls.mjs +57 -0
- package/esm/upgrade/codemods/logger-sync-calls.mjs.map +1 -0
- package/esm/upgrade/codemods/package-json.mjs +122 -0
- package/esm/upgrade/codemods/package-json.mjs.map +1 -0
- package/esm/upgrade/codemods/request-log-snapshot.mjs +82 -0
- package/esm/upgrade/codemods/request-log-snapshot.mjs.map +1 -0
- package/esm/upgrade/codemods/review-sites.mjs +262 -0
- package/esm/upgrade/codemods/review-sites.mjs.map +1 -0
- package/esm/upgrade/diff.mjs +119 -0
- package/esm/upgrade/diff.mjs.map +1 -0
- package/esm/upgrade/format-upgrade-report.mjs +32 -0
- package/esm/upgrade/format-upgrade-report.mjs.map +1 -0
- package/esm/upgrade/load-ts-morph.mjs +48 -0
- package/esm/upgrade/load-ts-morph.mjs.map +1 -0
- package/esm/upgrade/notes.mjs +16 -0
- package/esm/upgrade/notes.mjs.map +1 -0
- package/esm/upgrade/run-upgrade.mjs +246 -0
- package/esm/upgrade/run-upgrade.mjs.map +1 -0
- package/esm/upgrade/upgrade-command.action.mjs +37 -0
- package/esm/upgrade/upgrade-command.action.mjs.map +1 -0
- package/esm/upgrade/version.mjs +39 -0
- package/esm/upgrade/version.mjs.map +1 -0
- package/esm/use-cases/use-cases-registry.mjs.map +1 -1
- package/esm/utils/clock.d.mts +33 -0
- package/esm/utils/clock.d.mts.map +1 -0
- package/esm/utils/clock.mjs +27 -0
- package/esm/utils/clock.mjs.map +1 -0
- package/esm/utils/framework-vesion.mjs +1 -1
- package/esm/utils/framework-vesion.mjs.map +1 -1
- package/esm/utils/ids.d.mts +32 -0
- package/esm/utils/ids.d.mts.map +1 -0
- package/esm/utils/ids.mjs +33 -0
- package/esm/utils/ids.mjs.map +1 -0
- package/esm/utils/index.d.mts +3 -0
- package/esm/utils/index.mjs +3 -0
- package/esm/utils/runtime.d.mts +59 -0
- package/esm/utils/runtime.d.mts.map +1 -0
- package/esm/utils/runtime.mjs +85 -0
- package/esm/utils/runtime.mjs.map +1 -0
- package/esm/validation/index.d.mts +1 -1
- package/esm/validation/types.d.mts +57 -2
- package/esm/validation/types.d.mts.map +1 -1
- package/esm/validation/validateAll.mjs +19 -2
- package/esm/validation/validateAll.mjs.map +1 -1
- package/esm/validation/validation-failure-status.mjs +15 -0
- package/esm/validation/validation-failure-status.mjs.map +1 -0
- package/esm/validation/validators/file-validator.d.mts +0 -16
- package/esm/validation/validators/file-validator.d.mts.map +1 -1
- package/esm/warlock-config/bundle-config.mjs +1 -1
- package/esm/warlock-config/types.d.mts +27 -3
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/llms-full.txt +3166 -736
- package/llms.txt +16 -6
- package/package.json +43 -13
- package/schemas/inspect-report.schema.json +554 -0
- package/skills/README.md +9 -0
- package/skills/add-connector/SKILL.md +26 -38
- package/skills/benchmark-code/SKILL.md +30 -24
- package/skills/build-restful/SKILL.md +14 -14
- package/skills/build-url/SKILL.md +18 -18
- package/skills/configure-app/SKILL.md +107 -14
- package/skills/create-controller/SKILL.md +25 -12
- package/skills/create-module/SKILL.md +3 -3
- package/skills/define-resource/SKILL.md +32 -29
- package/skills/encrypt-data/SKILL.md +21 -19
- package/skills/generate-openapi/SKILL.md +50 -24
- package/skills/hash-password/SKILL.md +2 -2
- package/skills/health-checks/SKILL.md +24 -7
- package/skills/index.md +9 -4
- package/skills/lower-stage3-decorators/SKILL.md +4 -4
- package/skills/make-endpoint/SKILL.md +110 -0
- package/skills/model-factories/SKILL.md +92 -0
- package/skills/process-image/SKILL.md +32 -30
- package/skills/register-route/SKILL.md +109 -10
- package/skills/request-memo/SKILL.md +1 -1
- package/skills/request-tracing/SKILL.md +4 -1
- package/skills/resolve-path/SKILL.md +45 -44
- package/skills/retry-operation/SKILL.md +20 -11
- package/skills/run-app/SKILL.md +106 -22
- package/skills/send-response/SKILL.md +179 -14
- package/skills/store-file/SKILL.md +93 -53
- package/skills/test-fakes/SKILL.md +123 -0
- package/skills/test-http/SKILL.md +125 -21
- package/skills/test-service/SKILL.md +41 -1
- package/skills/update-packages/SKILL.md +29 -18
- package/skills/upgrade-from-5-29/SKILL.md +413 -0
- package/skills/upload-file/SKILL.md +31 -33
- package/skills/use-app-context/SKILL.md +38 -31
- package/skills/use-middleware/SKILL.md +45 -25
- package/skills/use-model-transformers/SKILL.md +37 -28
- package/skills/use-repository/SKILL.md +2 -2
- package/skills/validate-input/SKILL.md +72 -28
- package/skills/warlock-agents-md/SKILL.md +96 -0
- package/skills/warlock-board/SKILL.md +123 -0
- package/skills/warlock-check/SKILL.md +132 -0
- package/skills/warlock-conventions/SKILL.md +115 -0
- package/skills/warlock-doctor/SKILL.md +55 -22
- package/skills/warlock-inspect/SKILL.md +134 -0
- package/skills/warlock-mcp/SKILL.md +120 -0
- package/skills/warlock-routes/SKILL.md +18 -11
- package/skills/warlock-trace/SKILL.md +81 -0
- package/skills/wire-socket/SKILL.md +8 -8
- package/skills/write-seeder/SKILL.md +46 -32
- package/skills/write-use-case/SKILL.md +40 -27
- package/esm/http/uploads/matches-if-none-match.mjs +0 -13
- package/esm/http/uploads/matches-if-none-match.mjs.map +0 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { statSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
//#region ../../packages/core/src/http/lazy-static.ts
|
|
6
|
+
function optionError(field, message, valueError = false) {
|
|
7
|
+
return Object.assign(valueError ? /* @__PURE__ */ new TypeError(`"${field}" option ${message}`) : /* @__PURE__ */ new Error(`"${field}" option ${message}`), {
|
|
8
|
+
code: valueError ? "FST_STATIC_INVALID_OPTION_VALUE" : "FST_STATIC_INVALID_OPTION",
|
|
9
|
+
statusCode: 500
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/** Validate without importing the plugin's directory-listing module. */
|
|
13
|
+
function staticOptions(server, options) {
|
|
14
|
+
const root = options.root;
|
|
15
|
+
const normalize = (entry) => entry instanceof URL && entry.protocol === "file:" ? fileURLToPath(entry) : entry;
|
|
16
|
+
const normalized = Array.isArray(root) ? root.map(normalize) : root === void 0 ? void 0 : normalize(root);
|
|
17
|
+
if (options.serve !== false || normalized !== void 0) {
|
|
18
|
+
if (normalized === void 0) throw optionError("root", "is required");
|
|
19
|
+
if (Array.isArray(normalized) && normalized.length === 0) throw optionError("root", "array requires one or more paths");
|
|
20
|
+
if (Array.isArray(normalized) && new Set(normalized).size !== normalized.length) throw optionError("root", "array contains duplicate paths");
|
|
21
|
+
for (const entry of Array.isArray(normalized) ? normalized : [normalized]) {
|
|
22
|
+
if (typeof entry !== "string") throw optionError("root", "must be a string", true);
|
|
23
|
+
if (!path.isAbsolute(entry)) throw optionError("root", "must be an absolute path");
|
|
24
|
+
try {
|
|
25
|
+
if (!statSync(entry).isDirectory()) throw optionError("root", "must be a directory");
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error.code !== "ENOENT") throw error;
|
|
28
|
+
if (!options.suppressWarning) server.log.warn(`"root" path "${entry}" must exist`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (options.setHeaders !== void 0 && typeof options.setHeaders !== "function") throw optionError("setHeaders", "must be a function", true);
|
|
33
|
+
if (options.serve !== false && options.wildcard && typeof options.wildcard !== "boolean") throw optionError("wildcard", "must be a boolean", true);
|
|
34
|
+
const list = options.list;
|
|
35
|
+
if (list) {
|
|
36
|
+
if (Array.isArray(normalized)) throw Object.assign(/* @__PURE__ */ new TypeError("multi-root with list option is not supported"), {
|
|
37
|
+
code: "FST_STATIC_MULTIROOT_LIST_CONFLICT",
|
|
38
|
+
statusCode: 500
|
|
39
|
+
});
|
|
40
|
+
if (typeof list === "object") {
|
|
41
|
+
if (list.format && list.format !== "json" && list.format !== "html") throw optionError("list.format", "must be json or html", true);
|
|
42
|
+
if (list.names && !Array.isArray(list.names)) throw optionError("list.names", "must be an array", true);
|
|
43
|
+
if (list.jsonFormat != null && list.jsonFormat !== "names" && list.jsonFormat !== "extended") throw optionError("list.jsonFormat", "must be names or extended", true);
|
|
44
|
+
if (list.format === "html" && typeof list.render !== "function") throw optionError("list.render", "must be a function and is required with html format", true);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
...options,
|
|
49
|
+
root: normalized
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Mirror the plugin's boot-time route snapshot for wildcard:false. */
|
|
53
|
+
async function fileRoutes(options, prefix) {
|
|
54
|
+
const { glob } = await import("glob");
|
|
55
|
+
const roots = Array.isArray(options.root) ? options.root : [options.root];
|
|
56
|
+
const routes = /* @__PURE__ */ new Map();
|
|
57
|
+
const indexes = new Set(options.index === void 0 ? ["index.html"] : options.index === false ? [] : [options.index].flat());
|
|
58
|
+
const indexDirectories = /* @__PURE__ */ new Map();
|
|
59
|
+
for (const root of roots) {
|
|
60
|
+
if (typeof root !== "string") continue;
|
|
61
|
+
const rootPath = root.split(path.win32.sep).join(path.posix.sep).replace(/\/?$/, "/");
|
|
62
|
+
const files = await glob("**/**", {
|
|
63
|
+
cwd: rootPath,
|
|
64
|
+
absolute: false,
|
|
65
|
+
follow: true,
|
|
66
|
+
nodir: true,
|
|
67
|
+
dot: options.serveDotFiles,
|
|
68
|
+
ignore: options.globIgnore
|
|
69
|
+
});
|
|
70
|
+
for (const fileName of files) {
|
|
71
|
+
const file = fileName.split(path.win32.sep).join(path.posix.sep);
|
|
72
|
+
const url = prefix + file;
|
|
73
|
+
if (routes.has(url)) continue;
|
|
74
|
+
routes.set(url, {
|
|
75
|
+
url,
|
|
76
|
+
file: `/${file}`,
|
|
77
|
+
rootPath
|
|
78
|
+
});
|
|
79
|
+
const key = path.posix.basename(url);
|
|
80
|
+
if (indexes.has(key) && !indexDirectories.has(key)) indexDirectories.set(path.posix.dirname(url), rootPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const [directory, rootPath] of indexDirectories) {
|
|
84
|
+
const url = directory + (directory.endsWith("/") ? "" : "/");
|
|
85
|
+
const file = "/" + url.replace(prefix, "");
|
|
86
|
+
routes.set(url, {
|
|
87
|
+
url,
|
|
88
|
+
file,
|
|
89
|
+
rootPath
|
|
90
|
+
});
|
|
91
|
+
if (options.redirect === true) routes.set(url.replace(/\/$/, ""), {
|
|
92
|
+
url: url.replace(/\/$/, ""),
|
|
93
|
+
file: file.replace(/\/$/, ""),
|
|
94
|
+
rootPath
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return [...routes.values()];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Queue public routes after earlier plugins, and capture upstream handlers on the
|
|
101
|
+
* first file request. Only public Fastify methods are replaced on the facade;
|
|
102
|
+
* the listening application's route/decorator table never changes after ready.
|
|
103
|
+
*/
|
|
104
|
+
function registerLazyStatic(server, input, mount = {}) {
|
|
105
|
+
const options = staticOptions(server, input);
|
|
106
|
+
server.register(Object.assign(async (instance) => prepareStatic(instance, options, mount), { [Symbol.for("skip-override")]: true }));
|
|
107
|
+
}
|
|
108
|
+
async function prepareStatic(server, options, mount) {
|
|
109
|
+
const originalPrefix = options.prefix ?? "/";
|
|
110
|
+
const prefix = options.prefixAvoidTrailingSlash || originalPrefix.endsWith("/") ? originalPrefix : `${originalPrefix}/`;
|
|
111
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
112
|
+
const decorators = /* @__PURE__ */ new Map();
|
|
113
|
+
let initialized;
|
|
114
|
+
function initialize() {
|
|
115
|
+
return initialized ??= (async () => {
|
|
116
|
+
const facade = Object.create(server);
|
|
117
|
+
const capture = (route) => {
|
|
118
|
+
const url = route.url ?? route.path;
|
|
119
|
+
if (!url) throw new Error("Static plugin declared a route without a path.");
|
|
120
|
+
for (const method of [route.method].flat()) handlers.set(`${method} ${url}`, route.handler);
|
|
121
|
+
return facade;
|
|
122
|
+
};
|
|
123
|
+
Object.defineProperties(facade, {
|
|
124
|
+
route: { value: capture },
|
|
125
|
+
get: { value: (url, route, handler) => capture({
|
|
126
|
+
...route,
|
|
127
|
+
url,
|
|
128
|
+
method: ["GET", "HEAD"],
|
|
129
|
+
handler
|
|
130
|
+
}) },
|
|
131
|
+
decorateReply: { value: (name, handler) => {
|
|
132
|
+
decorators.set(name, handler);
|
|
133
|
+
return facade;
|
|
134
|
+
} }
|
|
135
|
+
});
|
|
136
|
+
const { default: staticPlugin } = await import("@fastify/static");
|
|
137
|
+
await staticPlugin(facade, {
|
|
138
|
+
...options,
|
|
139
|
+
decorateReply: true,
|
|
140
|
+
suppressWarning: true
|
|
141
|
+
});
|
|
142
|
+
})();
|
|
143
|
+
}
|
|
144
|
+
if (options.decorateReply !== false) for (const name of ["sendFile", "download"]) server.decorateReply(name, function(...args) {
|
|
145
|
+
initialize().then(() => {
|
|
146
|
+
const decorator = decorators.get(name);
|
|
147
|
+
if (!decorator) throw new Error(`Static plugin did not provide ${name}.`);
|
|
148
|
+
decorator.apply(this, args);
|
|
149
|
+
}).catch((error) => this.send(error));
|
|
150
|
+
return this;
|
|
151
|
+
});
|
|
152
|
+
if (options.serve === false) return;
|
|
153
|
+
const routes = options.wildcard === false ? await fileRoutes(options, prefix) : [{ url: prefix + "*" }];
|
|
154
|
+
if (options.wildcard !== false && options.redirect === true && prefix !== originalPrefix) routes.push({ url: originalPrefix });
|
|
155
|
+
const schema = { hide: options.schemaHide ?? true };
|
|
156
|
+
for (const route of routes) server.route({
|
|
157
|
+
url: route.url,
|
|
158
|
+
method: ["HEAD", "GET"],
|
|
159
|
+
constraints: options.constraints,
|
|
160
|
+
schema,
|
|
161
|
+
logLevel: options.logLevel,
|
|
162
|
+
config: {
|
|
163
|
+
...route.file ? {
|
|
164
|
+
file: route.file,
|
|
165
|
+
rootPath: route.rootPath
|
|
166
|
+
} : {},
|
|
167
|
+
...mount.rateLimit !== void 0 ? { rateLimit: mount.rateLimit } : {}
|
|
168
|
+
},
|
|
169
|
+
errorHandler(error, request, reply) {
|
|
170
|
+
if (error?.code === "ERR_STREAM_PREMATURE_CLOSE") {
|
|
171
|
+
reply.request.raw.destroy();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
return server.errorHandler(error, request, reply);
|
|
175
|
+
},
|
|
176
|
+
async handler(request, reply) {
|
|
177
|
+
await initialize();
|
|
178
|
+
const handler = handlers.get(`${request.method} ${route.url}`);
|
|
179
|
+
if (handler) {
|
|
180
|
+
await handler.call(server, request, reply);
|
|
181
|
+
return reply;
|
|
182
|
+
}
|
|
183
|
+
if (route.file) {
|
|
184
|
+
const sendFile = decorators.get("sendFile");
|
|
185
|
+
if (sendFile) return sendFile.call(reply, route.file, route.rootPath);
|
|
186
|
+
}
|
|
187
|
+
return reply.callNotFound();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
//#endregion
|
|
193
|
+
export { registerLazyStatic };
|
|
194
|
+
//# sourceMappingURL=lazy-static.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lazy-static.mjs","names":[],"sources":["../../../../../../../../packages/core/src/http/lazy-static.ts"],"sourcesContent":["import type { FastifyStaticOptions } from \"@fastify/static\";\nimport { statSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type {\n FastifyInstance,\n FastifyReply,\n FastifyRequest,\n FastifySchema,\n RouteHandlerMethod,\n RouteOptions,\n} from \"fastify\";\n\ntype FileRoute = { url: string; file?: string; rootPath?: string };\ntype ReplyDecorator = (this: FastifyReply, ...args: unknown[]) => FastifyReply;\n\nfunction optionError(field: string, message: string, valueError = false): Error {\n return Object.assign(\n valueError\n ? new TypeError(`\"${field}\" option ${message}`)\n : new Error(`\"${field}\" option ${message}`),\n {\n code: valueError ? \"FST_STATIC_INVALID_OPTION_VALUE\" : \"FST_STATIC_INVALID_OPTION\",\n statusCode: 500,\n },\n );\n}\n\n/** Validate without importing the plugin's directory-listing module. */\nfunction staticOptions(\n server: FastifyInstance,\n options: FastifyStaticOptions,\n): FastifyStaticOptions {\n const root = options.root;\n const normalize = (entry: string | URL): string | URL =>\n entry instanceof URL && entry.protocol === \"file:\" ? fileURLToPath(entry) : entry;\n const normalized = Array.isArray(root)\n ? root.map(normalize)\n : root === undefined\n ? undefined\n : normalize(root);\n if (options.serve !== false || normalized !== undefined) {\n if (normalized === undefined) throw optionError(\"root\", \"is required\");\n if (Array.isArray(normalized) && normalized.length === 0)\n throw optionError(\"root\", \"array requires one or more paths\");\n if (Array.isArray(normalized) && new Set(normalized).size !== normalized.length)\n throw optionError(\"root\", \"array contains duplicate paths\");\n for (const entry of Array.isArray(normalized) ? normalized : [normalized]) {\n if (typeof entry !== \"string\") throw optionError(\"root\", \"must be a string\", true);\n if (!path.isAbsolute(entry)) throw optionError(\"root\", \"must be an absolute path\");\n try {\n if (!statSync(entry).isDirectory()) throw optionError(\"root\", \"must be a directory\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n if (!options.suppressWarning) server.log.warn(`\"root\" path \"${entry}\" must exist`);\n }\n }\n }\n if (options.setHeaders !== undefined && typeof options.setHeaders !== \"function\")\n throw optionError(\"setHeaders\", \"must be a function\", true);\n if (options.serve !== false && options.wildcard && typeof options.wildcard !== \"boolean\")\n throw optionError(\"wildcard\", \"must be a boolean\", true);\n const list = options.list;\n if (list) {\n if (Array.isArray(normalized))\n throw Object.assign(new TypeError(\"multi-root with list option is not supported\"), {\n code: \"FST_STATIC_MULTIROOT_LIST_CONFLICT\",\n statusCode: 500,\n });\n if (typeof list === \"object\") {\n if (list.format && list.format !== \"json\" && list.format !== \"html\")\n throw optionError(\"list.format\", \"must be json or html\", true);\n if (list.names && !Array.isArray(list.names))\n throw optionError(\"list.names\", \"must be an array\", true);\n if (list.jsonFormat != null && list.jsonFormat !== \"names\" && list.jsonFormat !== \"extended\")\n throw optionError(\"list.jsonFormat\", \"must be names or extended\", true);\n if (list.format === \"html\" && typeof list.render !== \"function\")\n throw optionError(\n \"list.render\",\n \"must be a function and is required with html format\",\n true,\n );\n }\n }\n return { ...options, root: normalized } as FastifyStaticOptions;\n}\n\n/** Mirror the plugin's boot-time route snapshot for wildcard:false. */\nasync function fileRoutes(options: FastifyStaticOptions, prefix: string): Promise<FileRoute[]> {\n const { glob } = await import(\"glob\");\n const roots = Array.isArray(options.root) ? options.root : [options.root];\n const routes = new Map<string, FileRoute>();\n const indexes = new Set(\n options.index === undefined\n ? [\"index.html\"]\n : options.index === false\n ? []\n : [options.index].flat(),\n );\n const indexDirectories = new Map<string, string>();\n for (const root of roots) {\n if (typeof root !== \"string\") continue;\n const rootPath = root.split(path.win32.sep).join(path.posix.sep).replace(/\\/?$/, \"/\");\n const files = await glob(\"**/**\", {\n cwd: rootPath,\n absolute: false,\n follow: true,\n nodir: true,\n dot: options.serveDotFiles,\n ignore: options.globIgnore,\n });\n for (const fileName of files) {\n const file = fileName.split(path.win32.sep).join(path.posix.sep);\n const url = prefix + file;\n if (routes.has(url)) continue;\n routes.set(url, { url, file: `/${file}`, rootPath });\n // Match upstream: distinct index filenames in later roots replace the\n // directory route, while duplicate file URLs keep the first root.\n const key = path.posix.basename(url);\n if (indexes.has(key) && !indexDirectories.has(key))\n indexDirectories.set(path.posix.dirname(url), rootPath);\n }\n }\n for (const [directory, rootPath] of indexDirectories) {\n const url = directory + (directory.endsWith(\"/\") ? \"\" : \"/\");\n const file = \"/\" + url.replace(prefix, \"\");\n routes.set(url, { url, file, rootPath });\n if (options.redirect === true)\n routes.set(url.replace(/\\/$/, \"\"), {\n url: url.replace(/\\/$/, \"\"),\n file: file.replace(/\\/$/, \"\"),\n rootPath,\n });\n }\n return [...routes.values()];\n}\n\n/**\n * Queue public routes after earlier plugins, and capture upstream handlers on the\n * first file request. Only public Fastify methods are replaced on the facade;\n * the listening application's route/decorator table never changes after ready.\n */\nexport function registerLazyStatic(\n server: FastifyInstance,\n input: FastifyStaticOptions,\n mount: LazyStaticMount = {},\n): void {\n const options = staticOptions(server, input);\n // Earlier plugins install onRoute hooks (including global rate limiting).\n // Keep the decorators on the calling scope, as @fastify/static does.\n server.register(\n Object.assign(async (instance: FastifyInstance) => prepareStatic(instance, options, mount), {\n [Symbol.for(\"skip-override\")]: true,\n }),\n );\n}\n\n/**\n * How the file routes of one static mount meet the rate limiter.\n */\nexport type LazyStaticMount = {\n /**\n * The `config.rateLimit` of every file route: `false` exempts the mount\n * from the global limiter, an object gives it its own limit, and\n * `undefined` leaves it under the global policy.\n */\n rateLimit?: false | Record<string, unknown>;\n};\n\nasync function prepareStatic(\n server: FastifyInstance,\n options: FastifyStaticOptions,\n mount: LazyStaticMount,\n): Promise<void> {\n const originalPrefix = options.prefix ?? \"/\";\n const prefix =\n options.prefixAvoidTrailingSlash || originalPrefix.endsWith(\"/\")\n ? originalPrefix\n : `${originalPrefix}/`;\n const handlers = new Map<string, RouteHandlerMethod>();\n const decorators = new Map<string, ReplyDecorator>();\n let initialized: Promise<void> | undefined;\n\n function initialize(): Promise<void> {\n return (initialized ??= (async () => {\n const facade = Object.create(server) as FastifyInstance;\n const capture = (route: RouteOptions): FastifyInstance => {\n const url = route.url ?? (route as RouteOptions & { path?: string }).path;\n if (!url) throw new Error(\"Static plugin declared a route without a path.\");\n for (const method of [route.method].flat()) handlers.set(`${method} ${url}`, route.handler);\n return facade;\n };\n Object.defineProperties(facade, {\n route: { value: capture },\n get: {\n value: (\n url: string,\n route: Omit<RouteOptions, \"method\" | \"url\" | \"handler\">,\n handler: RouteHandlerMethod,\n ) => capture({ ...route, url, method: [\"GET\", \"HEAD\"], handler }),\n },\n decorateReply: {\n value: (name: string, handler: ReplyDecorator) => {\n decorators.set(name, handler);\n return facade;\n },\n },\n });\n const { default: staticPlugin } = await import(\"@fastify/static\");\n await staticPlugin(facade, { ...options, decorateReply: true, suppressWarning: true });\n })());\n }\n\n if (options.decorateReply !== false) {\n for (const name of [\"sendFile\", \"download\"]) {\n server.decorateReply(name, function (this: FastifyReply, ...args: unknown[]) {\n void initialize()\n .then(() => {\n const decorator = decorators.get(name);\n if (!decorator) throw new Error(`Static plugin did not provide ${name}.`);\n decorator.apply(this, args);\n })\n .catch((error: unknown) => this.send(error));\n return this;\n });\n }\n }\n if (options.serve === false) return;\n const routes: FileRoute[] =\n options.wildcard === false ? await fileRoutes(options, prefix) : [{ url: prefix + \"*\" }];\n if (options.wildcard !== false && options.redirect === true && prefix !== originalPrefix)\n routes.push({ url: originalPrefix });\n const schema: FastifySchema & { hide: boolean } = { hide: options.schemaHide ?? true };\n for (const route of routes) {\n server.route({\n url: route.url,\n method: [\"HEAD\", \"GET\"],\n constraints: options.constraints,\n schema,\n logLevel: options.logLevel,\n config: {\n ...(route.file ? { file: route.file, rootPath: route.rootPath } : {}),\n ...(mount.rateLimit !== undefined ? { rateLimit: mount.rateLimit } : {}),\n },\n errorHandler(error, request, reply) {\n if (error?.code === \"ERR_STREAM_PREMATURE_CLOSE\") {\n reply.request.raw.destroy();\n return;\n }\n return server.errorHandler(error, request, reply);\n },\n async handler(request: FastifyRequest, reply: FastifyReply) {\n await initialize();\n const handler = handlers.get(`${request.method} ${route.url}`);\n if (handler) {\n await handler.call(server, request, reply);\n // Some upstream handlers start an async send without returning its\n // promise. Returning the real reply keeps Fastify from auto-sending.\n return reply;\n }\n // A wildcard:false file may disappear between boot and the first request.\n if (route.file) {\n const sendFile = decorators.get(\"sendFile\");\n if (sendFile) return sendFile.call(reply, route.file, route.rootPath);\n }\n return reply.callNotFound();\n },\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"idempotency.middleware.mjs","names":[],"sources":["../../../../../../../../../packages/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 * TTL in seconds of the in-flight reservation held while the handler runs.\n * Falls back to `http.idempotency.reservationTtl`, then `60`.\n */\n reservationTtl?: 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\ntype InFlightReservation = {\n state: \"in-flight\";\n startedAt: number;\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 reservationTtl =\n options.reservationTtl || config.get(\"http.idempotency.reservationTtl\", 60);\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 respondToExisting = (existing: CachedResponse | InFlightReservation | undefined | null) => {\n if (!existing || (existing as InFlightReservation).state === \"in-flight\") {\n response.header(\"Retry-After\", \"1\");\n\n return response.conflict({\n error: \"A request with this Idempotency-Key is already in progress\",\n errorCode: HttpErrorCodes.IdempotencyKeyConflict,\n });\n }\n\n const cached = existing as CachedResponse;\n\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 // Fast path: a finished (or in-flight) request is already recorded.\n let existing: CachedResponse | InFlightReservation | null = null;\n\n try {\n existing = (await cacheDriver.get(cacheKey)) as CachedResponse | InFlightReservation | null;\n } catch (error) {\n // The cache is an optimisation of idempotency, not a gate on writes:\n // when it is down, fail open (the handler runs) and log it.\n log.error(\"idempotency-middleware\", \"get\", error);\n }\n\n if (existing) return respondToExisting(existing);\n\n // Reserve the key atomically before the handler runs, so concurrent\n // requests with the same key can't both execute it.\n try {\n const reservation = (await cacheDriver.set(\n cacheKey,\n { state: \"in-flight\", startedAt: Date.now() } satisfies InFlightReservation,\n { onConflict: \"create\", ttl: reservationTtl },\n )) as { wasSet?: boolean; existing?: CachedResponse | InFlightReservation } | undefined;\n\n if (reservation && reservation.wasSet === false) {\n return respondToExisting(reservation.existing);\n }\n } catch (error) {\n log.error(\"idempotency-middleware\", \"reserve\", error);\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 // Free the reservation so a retry can run.\n if (sentResponse.statusCode >= 500) {\n // Wrapped so a synchronous throw is caught as well as a rejection.\n Promise.resolve()\n .then(() => cacheDriver.remove(cacheKey))\n .catch((error: unknown) => {\n log.error(\"idempotency-middleware\", \"remove\", error);\n });\n\n return;\n }\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":";;;;;;;;AAoDA,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,iBACJ,QAAQ,kBAAkB,OAAO,IAAI,mCAAmC,EAAE;EAC5E,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,
|
|
1
|
+
{"version":3,"file":"idempotency.middleware.mjs","names":[],"sources":["../../../../../../../../../packages/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 * TTL in seconds of the in-flight reservation held while the handler runs.\n * Falls back to `http.idempotency.reservationTtl`, then `60`.\n */\n reservationTtl?: 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\ntype InFlightReservation = {\n state: \"in-flight\";\n startedAt: number;\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 reservationTtl =\n options.reservationTtl || config.get(\"http.idempotency.reservationTtl\", 60);\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 respondToExisting = (\n existing: CachedResponse | InFlightReservation | undefined | null,\n ) => {\n if (!existing || (existing as InFlightReservation).state === \"in-flight\") {\n response.header(\"Retry-After\", \"1\");\n\n return response.conflict({\n error: \"A request with this Idempotency-Key is already in progress\",\n errorCode: HttpErrorCodes.IdempotencyKeyConflict,\n });\n }\n\n const cached = existing as CachedResponse;\n\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 // Fast path: a finished (or in-flight) request is already recorded.\n let existing: CachedResponse | InFlightReservation | null = null;\n\n try {\n existing = (await cacheDriver.get(cacheKey)) as CachedResponse | InFlightReservation | null;\n } catch (error) {\n // The cache is an optimisation of idempotency, not a gate on writes:\n // when it is down, fail open (the handler runs) and log it.\n log.error(\"idempotency-middleware\", \"get\", error);\n }\n\n if (existing) return respondToExisting(existing);\n\n // Reserve the key atomically before the handler runs, so concurrent\n // requests with the same key can't both execute it.\n try {\n const reservation = (await cacheDriver.set(\n cacheKey,\n { state: \"in-flight\", startedAt: Date.now() } satisfies InFlightReservation,\n { onConflict: \"create\", ttl: reservationTtl },\n )) as { wasSet?: boolean; existing?: CachedResponse | InFlightReservation } | undefined;\n\n if (reservation && reservation.wasSet === false) {\n return respondToExisting(reservation.existing);\n }\n } catch (error) {\n log.error(\"idempotency-middleware\", \"reserve\", error);\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 // Free the reservation so a retry can run.\n if (sentResponse.statusCode >= 500) {\n // Wrapped so a synchronous throw is caught as well as a rejection.\n Promise.resolve()\n .then(() => cacheDriver.remove(cacheKey))\n .catch((error: unknown) => {\n log.error(\"idempotency-middleware\", \"remove\", error);\n });\n\n return;\n }\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":";;;;;;;;AAoDA,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,iBACJ,QAAQ,kBAAkB,OAAO,IAAI,mCAAmC,EAAE;EAC5E,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,qBACJ,aACG;GACH,IAAI,CAAC,YAAa,SAAiC,UAAU,aAAa;IACxE,SAAS,OAAO,eAAe,GAAG;IAElC,OAAO,SAAS,SAAS;KACvB,OAAO;KACP;IACF,CAAC;GACH;GAEA,MAAM,SAAS;GAEf,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;EAGA,IAAI,WAAwD;EAE5D,IAAI;GACF,WAAY,MAAM,YAAY,IAAI,QAAQ;EAC5C,SAAS,OAAO;GAGd,IAAI,MAAM,0BAA0B,OAAO,KAAK;EAClD;EAEA,IAAI,UAAU,OAAO,kBAAkB,QAAQ;EAI/C,IAAI;GACF,MAAM,cAAe,MAAM,YAAY,IACrC,UACA;IAAE,OAAO;IAAa,WAAW,KAAK,IAAI;GAAE,GAC5C;IAAE,YAAY;IAAU,KAAK;GAAe,CAC9C;GAEA,IAAI,eAAe,YAAY,WAAW,OACxC,OAAO,kBAAkB,YAAY,QAAQ;EAEjD,SAAS,OAAO;GACd,IAAI,MAAM,0BAA0B,WAAW,KAAK;EACtD;EAEA,SAAS,QAAQ,iBAA2B;GAI1C,IAAI,aAAa,cAAc,KAAK;IAElC,QAAQ,QAAQ,CAAC,CACd,WAAW,YAAY,OAAO,QAAQ,CAAC,CAAC,CACxC,OAAO,UAAmB;KACzB,IAAI,MAAM,0BAA0B,UAAU,KAAK;IACrD,CAAC;IAEH;GACF;GAEA,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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugins.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/http/plugins.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"plugins.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/http/plugins.ts"],"mappings":";;;iBAcsB,mBAAA,CAAoB,MAAA,EAAQ,eAAA,GAAe,OAAA;;AAAjE;;;;;iBAuFgB,iBAAA,CAAkB,UAAmB"}
|
package/esm/http/plugins.mjs
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { rootPath } from "../utils/paths.mjs";
|
|
2
2
|
import "../utils/index.mjs";
|
|
3
|
+
import { registerLazyStatic } from "./lazy-static.mjs";
|
|
3
4
|
import { buildCorsOptions } from "./build-cors-options.mjs";
|
|
5
|
+
import { resolveStaticRateLimit } from "../router/resolve-route-rate-limit.mjs";
|
|
4
6
|
import { router } from "../router/router.mjs";
|
|
5
7
|
import "../router/index.mjs";
|
|
6
8
|
import { buildRateLimitOptions } from "./build-rate-limit-options.mjs";
|
|
7
9
|
import { parseUrlencodedBody } from "./parse-urlencoded-body.mjs";
|
|
8
10
|
import config from "@mongez/config";
|
|
9
11
|
import { log } from "@warlock.js/logger";
|
|
10
|
-
import fastifyMultipart from "@fastify/multipart";
|
|
11
12
|
import { existsSync } from "node:fs";
|
|
13
|
+
import fastifyMultipart from "@fastify/multipart";
|
|
12
14
|
|
|
13
15
|
//#region ../../packages/core/src/http/plugins.ts
|
|
14
16
|
async function registerHttpPlugins(server) {
|
|
15
17
|
const rateLimitOptions = buildRateLimitOptions(config.get("http.rateLimit"));
|
|
16
18
|
if (rateLimitOptions) server.register(import("@fastify/rate-limit"), rateLimitOptions);
|
|
17
|
-
else if (router.list().some((route) => route.rateLimit)) log.warn("http", "rateLimit", "Routes declare `rateLimit`, but `http.rateLimit.enabled` is false so the rate-limit plugin is not registered: route rate limits (and their errorMessage) are inactive.");
|
|
19
|
+
else if (router.list().some((route) => typeof route.rateLimit === "object")) log.warn("http", "rateLimit", "Routes declare `rateLimit`, but `http.rateLimit.enabled` is false so the rate-limit plugin is not registered: route rate limits (and their errorMessage) are inactive.");
|
|
18
20
|
server.register(import("@fastify/cors"), buildCorsOptions());
|
|
19
21
|
server.register(fastifyMultipart, {
|
|
20
22
|
attachFieldsToBody: true,
|
|
@@ -33,14 +35,15 @@ async function registerHttpPlugins(server) {
|
|
|
33
35
|
}
|
|
34
36
|
});
|
|
35
37
|
const publicRoot = resolvePublicRoot(config.get("storage.publicRoot", rootPath("public")));
|
|
36
|
-
if (publicRoot !== void 0) server
|
|
38
|
+
if (publicRoot !== void 0) registerLazyStatic(server, {
|
|
37
39
|
root: publicRoot,
|
|
38
40
|
prefix: config.get("storage.publicPrefix", "/public/")
|
|
39
|
-
});
|
|
41
|
+
}, { rateLimit: resolveStaticRateLimit(void 0) });
|
|
40
42
|
server.register(import("@fastify/cookie"), {
|
|
41
43
|
secret: config.get("http.cookies.secret"),
|
|
42
44
|
parseOptions: config.get("http.cookies.options", {})
|
|
43
45
|
});
|
|
46
|
+
await server.after();
|
|
44
47
|
}
|
|
45
48
|
/**
|
|
46
49
|
* The public folder(s) worth handing to `@fastify/static`, or `undefined`
|
package/esm/http/plugins.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugins.mjs","names":[],"sources":["../../../../../../../../packages/core/src/http/plugins.ts"],"sourcesContent":["import fastifyMultipart from \"@fastify/multipart\";\nimport config from \"@mongez/config\";\nimport { existsSync } from \"node:fs\";\nimport type { FastifyRequest } from \"fastify\";\nimport { log } from \"@warlock.js/logger\";\nimport { router } from \"../router\";\nimport { rootPath } from \"../utils\";\nimport { buildCorsOptions } from \"./build-cors-options\";\nimport { buildRateLimitOptions } from \"./build-rate-limit-options\";\nimport { parseUrlencodedBody } from \"./parse-urlencoded-body\";\nimport type { FastifyInstance } from \"./server\";\n\nexport async function registerHttpPlugins(server: FastifyInstance) {\n // 👇🏻 register rate-limit plugin\n const rateLimitOptions = buildRateLimitOptions(config.get(\"http.rateLimit\"));\n\n if (rateLimitOptions) {\n server.register(import(\"@fastify/rate-limit\"), rateLimitOptions);\n } else if (router.list().some((route) => route.rateLimit)) {\n log.warn(\n \"http\",\n \"rateLimit\",\n \"Routes declare `rateLimit`, but `http.rateLimit.enabled` is false so the rate-limit plugin is not registered: route rate limits (and their errorMessage) are inactive.\",\n );\n }\n\n // 👇🏻 register cors plugin\n server.register(import(\"@fastify/cors\"), buildCorsOptions());\n\n // 👇🏻 import multipart plugin\n server.register(fastifyMultipart, {\n attachFieldsToBody: true,\n limits: {\n // file size could be up to 10MB\n fileSize: config.get(\"http.fileUploadLimit\", 10 * 1024 * 1024),\n // every file part is buffered in memory (`attachFieldsToBody`), so cap the counts too;\n // hitting any limit makes @fastify/multipart throw an error with statusCode 413\n files: config.get(\"http.multipart.files\", 10),\n fields: config.get(\"http.multipart.fields\", 100),\n fieldSize: config.get(\"http.multipart.fieldSize\", 1024 * 1024),\n },\n });\n\n // 👇🏻 parse application/x-www-form-urlencoded bodies (Apple's OAuth\n // `form_post` callback, plain HTML forms). Fastify only parses JSON and\n // text natively, so without this every urlencoded POST is rejected with\n // FST_ERR_CTP_INVALID_MEDIA_TYPE before it reaches a route.\n //\n // No `bodyLimit` option here: Fastify falls back to the server's own\n // `bodyLimit` (`server.ts`, from `http.bodyLimit`) whenever a content-type\n // parser doesn't set one — the exact limit JSON bodies are already held to.\n // Reading `http.bodyLimit` again here would duplicate that lookup and risk\n // the two silently drifting.\n server.addContentTypeParser(\n \"application/x-www-form-urlencoded\",\n { parseAs: \"string\" },\n (_request: FastifyRequest, body: string, done: (err: Error | null, body?: any) => void) => {\n try {\n done(null, parseUrlencodedBody(body));\n } catch (error) {\n done(error as Error, undefined);\n }\n },\n );\n\n // 👇🏻 serve the public folder. Importing `@fastify/static` is not free (its\n // directory-listing module calls `os.cpus()` at load), so
|
|
1
|
+
{"version":3,"file":"plugins.mjs","names":[],"sources":["../../../../../../../../packages/core/src/http/plugins.ts"],"sourcesContent":["import fastifyMultipart from \"@fastify/multipart\";\nimport config from \"@mongez/config\";\nimport { existsSync } from \"node:fs\";\nimport type { FastifyRequest } from \"fastify\";\nimport { log } from \"@warlock.js/logger\";\nimport { router } from \"../router\";\nimport { resolveStaticRateLimit } from \"../router/resolve-route-rate-limit\";\nimport { rootPath } from \"../utils\";\nimport { buildCorsOptions } from \"./build-cors-options\";\nimport { buildRateLimitOptions } from \"./build-rate-limit-options\";\nimport { registerLazyStatic } from \"./lazy-static\";\nimport { parseUrlencodedBody } from \"./parse-urlencoded-body\";\nimport type { FastifyInstance } from \"./server\";\n\nexport async function registerHttpPlugins(server: FastifyInstance) {\n // 👇🏻 register rate-limit plugin\n const rateLimitOptions = buildRateLimitOptions(config.get(\"http.rateLimit\"));\n\n if (rateLimitOptions) {\n server.register(import(\"@fastify/rate-limit\"), rateLimitOptions);\n } else if (router.list().some((route) => typeof route.rateLimit === \"object\")) {\n log.warn(\n \"http\",\n \"rateLimit\",\n \"Routes declare `rateLimit`, but `http.rateLimit.enabled` is false so the rate-limit plugin is not registered: route rate limits (and their errorMessage) are inactive.\",\n );\n }\n\n // 👇🏻 register cors plugin\n server.register(import(\"@fastify/cors\"), buildCorsOptions());\n\n // 👇🏻 import multipart plugin\n server.register(fastifyMultipart, {\n attachFieldsToBody: true,\n limits: {\n // file size could be up to 10MB\n fileSize: config.get(\"http.fileUploadLimit\", 10 * 1024 * 1024),\n // every file part is buffered in memory (`attachFieldsToBody`), so cap the counts too;\n // hitting any limit makes @fastify/multipart throw an error with statusCode 413\n files: config.get(\"http.multipart.files\", 10),\n fields: config.get(\"http.multipart.fields\", 100),\n fieldSize: config.get(\"http.multipart.fieldSize\", 1024 * 1024),\n },\n });\n\n // 👇🏻 parse application/x-www-form-urlencoded bodies (Apple's OAuth\n // `form_post` callback, plain HTML forms). Fastify only parses JSON and\n // text natively, so without this every urlencoded POST is rejected with\n // FST_ERR_CTP_INVALID_MEDIA_TYPE before it reaches a route.\n //\n // No `bodyLimit` option here: Fastify falls back to the server's own\n // `bodyLimit` (`server.ts`, from `http.bodyLimit`) whenever a content-type\n // parser doesn't set one — the exact limit JSON bodies are already held to.\n // Reading `http.bodyLimit` again here would duplicate that lookup and risk\n // the two silently drifting.\n server.addContentTypeParser(\n \"application/x-www-form-urlencoded\",\n { parseAs: \"string\" },\n (_request: FastifyRequest, body: string, done: (err: Error | null, body?: any) => void) => {\n try {\n done(null, parseUrlencodedBody(body));\n } catch (error) {\n done(error as Error, undefined);\n }\n },\n );\n\n // 👇🏻 serve the public folder. Importing `@fastify/static` is not free (its\n // directory-listing module calls `os.cpus()` at load), so defer loading to\n // the first file request even when the public folder already exists.\n const publicRoot = resolvePublicRoot(config.get(\"storage.publicRoot\", rootPath(\"public\")));\n\n if (publicRoot !== undefined) {\n registerLazyStatic(\n server,\n {\n root: publicRoot,\n prefix: config.get(\"storage.publicPrefix\", \"/public/\"),\n },\n // Exempt from the global limiter unless `http.rateLimit.staticFiles`.\n { rateLimit: resolveStaticRateLimit(undefined) },\n );\n }\n\n // 👇🏻 register cookie plugin\n server.register(import(\"@fastify/cookie\"), {\n secret: config.get(\"http.cookies.secret\"), // Optional: allow signed cookies\n parseOptions: config.get(\"http.cookies.options\", {}),\n });\n\n // Install plugin onRoute hooks before the connector registers health and app\n // routes. after() waits for this queue without closing route registration.\n await server.after();\n}\n\n/**\n * The public folder(s) worth handing to `@fastify/static`, or `undefined`\n * when there is nothing to serve: `false`, or a single path that does not\n * exist. An array is passed through unchanged; `@fastify/static` validates\n * each entry itself.\n */\nexport function resolvePublicRoot(publicRoot: unknown): string | string[] | undefined {\n if (publicRoot === false || publicRoot === null || publicRoot === undefined) {\n return undefined;\n }\n\n if (Array.isArray(publicRoot)) {\n return publicRoot.length > 0 ? publicRoot : undefined;\n }\n\n if (typeof publicRoot === \"string\") {\n return existsSync(publicRoot) ? publicRoot : undefined;\n }\n\n return publicRoot as string;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,eAAsB,oBAAoB,QAAyB;CAEjE,MAAM,mBAAmB,sBAAsB,OAAO,IAAI,gBAAgB,CAAC;CAE3E,IAAI,kBACF,OAAO,SAAS,OAAO,wBAAwB,gBAAgB;MAC1D,IAAI,OAAO,KAAK,CAAC,CAAC,MAAM,UAAU,OAAO,MAAM,cAAc,QAAQ,GAC1E,IAAI,KACF,QACA,aACA,wKACF;CAIF,OAAO,SAAS,OAAO,kBAAkB,iBAAiB,CAAC;CAG3D,OAAO,SAAS,kBAAkB;EAChC,oBAAoB;EACpB,QAAQ;GAEN,UAAU,OAAO,IAAI,wBAAwB,KAAK,OAAO,IAAI;GAG7D,OAAO,OAAO,IAAI,wBAAwB,EAAE;GAC5C,QAAQ,OAAO,IAAI,yBAAyB,GAAG;GAC/C,WAAW,OAAO,IAAI,4BAA4B,OAAO,IAAI;EAC/D;CACF,CAAC;CAYD,OAAO,qBACL,qCACA,EAAE,SAAS,SAAS,IACnB,UAA0B,MAAc,SAAkD;EACzF,IAAI;GACF,KAAK,MAAM,oBAAoB,IAAI,CAAC;EACtC,SAAS,OAAO;GACd,KAAK,OAAgB,MAAS;EAChC;CACF,CACF;CAKA,MAAM,aAAa,kBAAkB,OAAO,IAAI,sBAAsB,SAAS,QAAQ,CAAC,CAAC;CAEzF,IAAI,eAAe,QACjB,mBACE,QACA;EACE,MAAM;EACN,QAAQ,OAAO,IAAI,wBAAwB,UAAU;CACvD,GAEA,EAAE,WAAW,uBAAuB,MAAS,EAAE,CACjD;CAIF,OAAO,SAAS,OAAO,oBAAoB;EACzC,QAAQ,OAAO,IAAI,qBAAqB;EACxC,cAAc,OAAO,IAAI,wBAAwB,CAAC,CAAC;CACrD,CAAC;CAID,MAAM,OAAO,MAAM;AACrB;;;;;;;AAQA,SAAgB,kBAAkB,YAAoD;CACpF,IAAI,eAAe,SAAS,eAAe,QAAQ,eAAe,QAChE;CAGF,IAAI,MAAM,QAAQ,UAAU,GAC1B,OAAO,WAAW,SAAS,IAAI,aAAa;CAG9C,IAAI,OAAO,eAAe,UACxB,OAAO,WAAW,UAAU,IAAI,aAAa;CAG/C,OAAO;AACT"}
|
|
@@ -33,6 +33,7 @@ const UNSUPPORTED_DRIVERS = new Set(["null", "file"]);
|
|
|
33
33
|
var UnsupportedRateLimitDriverError = class extends Error {
|
|
34
34
|
constructor(driverName) {
|
|
35
35
|
super(`Rate limiting with store: "cache" cannot use the "${driverName}" cache driver: ` + (driverName === "null" ? "it stores nothing, so the limit would never trip. " : "it never removes expired windows and its increment is not shared between processes. ") + "Use redis or pg (or a memory driver for a single process), or set store: \"memory\".");
|
|
36
|
+
this.code = "WL1003";
|
|
36
37
|
this.name = "UnsupportedRateLimitDriverError";
|
|
37
38
|
}
|
|
38
39
|
};
|
|
@@ -75,7 +76,7 @@ async function incrementSharedRateLimit(namespace, group, windowMs, now = Date.n
|
|
|
75
76
|
onConflict: "create"
|
|
76
77
|
});
|
|
77
78
|
return {
|
|
78
|
-
current: await cache.increment(key),
|
|
79
|
+
current: await cache.increment(key, 1, { ttl: ttlSeconds }),
|
|
79
80
|
resetAt: (windowIndex + 1) * windowMs
|
|
80
81
|
};
|
|
81
82
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit-cache-store.mjs","names":[],"sources":["../../../../../../../../packages/core/src/http/rate-limit-cache-store.ts"],"sourcesContent":["import type { FastifyRateLimitOptions, FastifyRateLimitStore } from \"@fastify/rate-limit\";\nimport { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport { createHash } from \"node:crypto\";\n\n/**\n * Rate-limit counting shared through `@warlock.js/cache`, so every process\n * that uses the same cache driver (Redis, Postgres) counts into the same\n * buckets. With a memory driver (`memory`, `lru`, `memoryExtended`) the\n * counts stay in the process, like the in-memory limiter.\n *\n * Windows are fixed and aligned to the clock: a 60 s window resets on the\n * minute, whichever process saw the first request. Each window has its own\n * key, created atomically with a TTL of two windows and incremented with the\n * driver's atomic increment, so a key cannot expire while its window counts.\n *\n * Two drivers are refused, with {@link UnsupportedRateLimitDriverError}:\n *\n * - `null` stores nothing, so every request would count as the first and the\n * limit would never trip.\n * - `file` has no expiry sweeper, so every past window would stay on disk\n * forever, and its increment is only serialized within one process, so\n * workers sharing the directory would lose counts.\n */\n\nconst KEY_PREFIX = \"rate-limit\";\n\n/**\n * Cache drivers that cannot back a shared limiter (see the module comment).\n */\nconst UNSUPPORTED_DRIVERS = new Set([\"null\", \"file\"]);\n\n/**\n * Thrown when `store: \"cache\"` runs on a cache driver that cannot count.\n */\nexport class UnsupportedRateLimitDriverError extends Error {\n public constructor(driverName: string) {\n super(\n `Rate limiting with store: \"cache\" cannot use the \"${driverName}\" cache driver: ` +\n (driverName === \"null\"\n ? \"it stores nothing, so the limit would never trip. \"\n : \"it never removes expired windows and its increment is not shared between processes. \") +\n 'Use redis or pg (or a memory driver for a single process), or set store: \"memory\".',\n );\n\n this.name = \"UnsupportedRateLimitDriverError\";\n }\n}\n\nfunction assertSupportedDriver(): void {\n const driverName = cache.currentDriver?.name;\n\n if (driverName && UNSUPPORTED_DRIVERS.has(driverName)) {\n throw new UnsupportedRateLimitDriverError(driverName);\n }\n}\n\n/**\n * Hash used for group keys and route namespaces before they reach the cache.\n */\nfunction shortHash(value: string): string {\n return createHash(\"sha256\").update(value).digest(\"hex\").slice(0, 32);\n}\n\n/**\n * The group key (an IP, a user id, whatever `keyGenerator` returned) is\n * hashed before it reaches the cache: cache drivers log their keys, and a raw\n * IP or email in a key is personal data in a log line.\n *\n * The namespace (it carries a route path) is hashed as well: flattening\n * `/users/:id` and `/users/id` to one safe string merged their counters, and\n * the file driver would turn `/` into directories. A readable, flattened\n * prefix stays in front of the hash for debugging.\n */\nfunction windowKey(namespace: string, group: string, windowIndex: number): string {\n const readable = namespace.replace(/[^A-Za-z0-9_-]+/g, \"_\").slice(0, 40);\n\n return `${KEY_PREFIX}.${readable}-${shortHash(namespace).slice(0, 16)}.${shortHash(group)}.${windowIndex}`;\n}\n\n/**\n * Count one request for `group` in the current window.\n *\n * @returns the count so far in this window and when the window resets (epoch ms)\n * @throws {UnsupportedRateLimitDriverError} on the `null` or `file` driver\n */\nexport async function incrementSharedRateLimit(\n namespace: string,\n group: string,\n windowMs: number,\n now: number = Date.now(),\n): Promise<{ current: number; resetAt: number }> {\n assertSupportedDriver();\n\n const windowIndex = Math.floor(now / windowMs);\n const key = windowKey(namespace, group, windowIndex);\n const ttlSeconds = Math.max(1, Math.ceil((2 * windowMs) / 1000));\n\n await cache.set(key, 0, { ttl: ttlSeconds, onConflict: \"create\" });\n\n const current = await cache.increment(key);\n\n return { current, resetAt: (windowIndex + 1) * windowMs };\n}\n\n/**\n * Read the count for `group` in the current window without counting a request\n */\nexport async function readSharedRateLimit(\n namespace: string,\n group: string,\n windowMs: number,\n now: number = Date.now(),\n): Promise<{ current: number; resetAt: number }> {\n assertSupportedDriver();\n\n const windowIndex = Math.floor(now / windowMs);\n const value = await cache.get(windowKey(namespace, group, windowIndex));\n\n return {\n current: typeof value === \"number\" ? value : 0,\n resetAt: (windowIndex + 1) * windowMs,\n };\n}\n\n/**\n * How often a failing shared store is reported. A cache outage fails every\n * request, so logging each one would flood the log.\n */\nconst ERROR_LOG_INTERVAL_MS = 60_000;\n\nlet lastErrorLogAt = 0;\n\n/**\n * Log a shared rate-limit store failure, at most once per minute.\n */\nexport function logSharedRateLimitError(error: unknown, now: number = Date.now()): void {\n if (now - lastErrorLogAt < ERROR_LOG_INTERVAL_MS) return;\n\n lastErrorLogAt = now;\n log.error(\"http\", \"rate-limit\", error);\n}\n\n/**\n * Forget when the last store error was logged (tests).\n */\nexport function resetSharedRateLimitErrorLog(): void {\n lastErrorLogAt = 0;\n}\n\n/**\n * Route data `@fastify/rate-limit` passes to `child()`: the route's options\n * under `routeInfo` (`fastify.rateLimit()` hooks pass an empty one).\n */\ntype ChildRouteOptions = {\n routeInfo?: { method?: unknown; url?: string; path?: string };\n};\n\n/**\n * `fastify.rateLimit()` hooks have no route; told apart by creation order,\n * which matches across processes running the same code.\n */\nlet nextHookNamespace = 0;\n\n/**\n * `@fastify/rate-limit` store backed by `@warlock.js/cache`. Selected with\n * `http.rateLimit.store: \"cache\"`; each route with its own `rateLimit` gets\n * its own namespace through `child()`.\n *\n * A failure (cache outage, unsupported driver) is logged at most once a\n * minute and passed to the plugin, which lets the request through because\n * Warlock registers it with `skipOnError: true` for this store.\n */\nexport class CacheRateLimitStore implements FastifyRateLimitStore {\n public constructor(\n _options?: FastifyRateLimitOptions,\n protected readonly namespace: string = \"global\",\n ) {}\n\n public incr(\n key: string,\n callback: (error: Error | null, result?: { current: number; ttl: number }) => void,\n timeWindow: number,\n ): void {\n incrementSharedRateLimit(this.namespace, key, timeWindow).then(\n ({ current, resetAt }) => callback(null, { current, ttl: Math.max(0, resetAt - Date.now()) }),\n (error: unknown) => {\n logSharedRateLimitError(error);\n callback(error instanceof Error ? error : new Error(String(error)));\n },\n );\n }\n\n public read(\n key: string,\n callback: (error: Error | null, result?: { current: number; ttl: number }) => void,\n timeWindow: number,\n ): void {\n readSharedRateLimit(this.namespace, key, timeWindow).then(\n ({ current, resetAt }) =>\n callback(null, { current, ttl: current === 0 ? 0 : Math.max(0, resetAt - Date.now()) }),\n (error: unknown) => {\n logSharedRateLimitError(error);\n callback(error instanceof Error ? error : new Error(String(error)));\n },\n );\n }\n\n public child(routeOptions: Parameters<FastifyRateLimitStore[\"child\"]>[0]): FastifyRateLimitStore {\n // The plugin's declared type names the route options directly, but at\n // runtime (11.x) it passes them merged under `routeInfo`.\n const routeInfo = (routeOptions as unknown as ChildRouteOptions).routeInfo ?? {};\n const method = Array.isArray(routeInfo.method)\n ? routeInfo.method.join(\",\")\n : String(routeInfo.method ?? \"\");\n const url = routeInfo.url ?? routeInfo.path ?? \"\";\n\n if (!method && !url) {\n return new CacheRateLimitStore(undefined, `hook.${nextHookNamespace++}`);\n }\n\n return new CacheRateLimitStore(undefined, `route.${method} ${url}`);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyBA,MAAM,aAAa;;;;AAKnB,MAAM,sBAAsB,IAAI,IAAI,CAAC,QAAQ,MAAM,CAAC;;;;AAKpD,IAAa,kCAAb,cAAqD,MAAM;CACzD,AAAO,YAAY,YAAoB;EACrC,MACE,qDAAqD,WAAW,qBAC7D,eAAe,SACZ,uDACA,0FACJ,sFACJ;EAEA,KAAK,OAAO;CACd;AACF;AAEA,SAAS,wBAA8B;CACrC,MAAM,aAAa,MAAM,eAAe;CAExC,IAAI,cAAc,oBAAoB,IAAI,UAAU,GAClD,MAAM,IAAI,gCAAgC,UAAU;AAExD;;;;AAKA,SAAS,UAAU,OAAuB;CACxC,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,EAAE;AACrE;;;;;;;;;;;AAYA,SAAS,UAAU,WAAmB,OAAe,aAA6B;CAGhF,OAAO,GAAG,WAAW,GAFJ,UAAU,QAAQ,oBAAoB,GAAG,CAAC,CAAC,MAAM,GAAG,EAEtC,EAAE,GAAG,UAAU,SAAS,CAAC,CAAC,MAAM,GAAG,EAAE,EAAE,GAAG,UAAU,KAAK,EAAE,GAAG;AAC/F;;;;;;;AAQA,eAAsB,yBACpB,WACA,OACA,UACA,MAAc,KAAK,IAAI,GACwB;CAC/C,sBAAsB;CAEtB,MAAM,cAAc,KAAK,MAAM,MAAM,QAAQ;CAC7C,MAAM,MAAM,UAAU,WAAW,OAAO,WAAW;CACnD,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,KAAM,IAAI,WAAY,GAAI,CAAC;CAE/D,MAAM,MAAM,IAAI,KAAK,GAAG;EAAE,KAAK;EAAY,YAAY;CAAS,CAAC;CAIjE,OAAO;EAAE,eAFa,MAAM,UAAU,GAAG;EAEvB,UAAU,cAAc,KAAK;CAAS;AAC1D;;;;AAKA,eAAsB,oBACpB,WACA,OACA,UACA,MAAc,KAAK,IAAI,GACwB;CAC/C,sBAAsB;CAEtB,MAAM,cAAc,KAAK,MAAM,MAAM,QAAQ;CAC7C,MAAM,QAAQ,MAAM,MAAM,IAAI,UAAU,WAAW,OAAO,WAAW,CAAC;CAEtE,OAAO;EACL,SAAS,OAAO,UAAU,WAAW,QAAQ;EAC7C,UAAU,cAAc,KAAK;CAC/B;AACF;;;;;AAMA,MAAM,wBAAwB;AAE9B,IAAI,iBAAiB;;;;AAKrB,SAAgB,wBAAwB,OAAgB,MAAc,KAAK,IAAI,GAAS;CACtF,IAAI,MAAM,iBAAiB,uBAAuB;CAElD,iBAAiB;CACjB,IAAI,MAAM,QAAQ,cAAc,KAAK;AACvC;;;;;AAqBA,IAAI,oBAAoB;;;;;;;;;;AAWxB,IAAa,sBAAb,MAAa,oBAAqD;CAChE,AAAO,YACL,UACA,AAAmB,YAAoB,UACvC;EADmB;CAClB;CAEH,AAAO,KACL,KACA,UACA,YACM;EACN,yBAAyB,KAAK,WAAW,KAAK,UAAU,CAAC,CAAC,MACvD,EAAE,SAAS,cAAc,SAAS,MAAM;GAAE;GAAS,KAAK,KAAK,IAAI,GAAG,UAAU,KAAK,IAAI,CAAC;EAAE,CAAC,IAC3F,UAAmB;GAClB,wBAAwB,KAAK;GAC7B,SAAS,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CAAC;EACpE,CACF;CACF;CAEA,AAAO,KACL,KACA,UACA,YACM;EACN,oBAAoB,KAAK,WAAW,KAAK,UAAU,CAAC,CAAC,MAClD,EAAE,SAAS,cACV,SAAS,MAAM;GAAE;GAAS,KAAK,YAAY,IAAI,IAAI,KAAK,IAAI,GAAG,UAAU,KAAK,IAAI,CAAC;EAAE,CAAC,IACvF,UAAmB;GAClB,wBAAwB,KAAK;GAC7B,SAAS,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CAAC;EACpE,CACF;CACF;CAEA,AAAO,MAAM,cAAoF;EAG/F,MAAM,YAAa,aAA8C,aAAa,CAAC;EAC/E,MAAM,SAAS,MAAM,QAAQ,UAAU,MAAM,IACzC,UAAU,OAAO,KAAK,GAAG,IACzB,OAAO,UAAU,UAAU,EAAE;EACjC,MAAM,MAAM,UAAU,OAAO,UAAU,QAAQ;EAE/C,IAAI,CAAC,UAAU,CAAC,KACd,OAAO,IAAI,oBAAoB,QAAW,QAAQ,qBAAqB;EAGzE,OAAO,IAAI,oBAAoB,QAAW,SAAS,OAAO,GAAG,KAAK;CACpE;AACF"}
|
|
1
|
+
{"version":3,"file":"rate-limit-cache-store.mjs","names":[],"sources":["../../../../../../../../packages/core/src/http/rate-limit-cache-store.ts"],"sourcesContent":["import type { FastifyRateLimitOptions, FastifyRateLimitStore } from \"@fastify/rate-limit\";\nimport { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport { createHash } from \"node:crypto\";\n\n/**\n * Rate-limit counting shared through `@warlock.js/cache`, so every process\n * that uses the same cache driver (Redis, Postgres) counts into the same\n * buckets. With a memory driver (`memory`, `lru`, `memoryExtended`) the\n * counts stay in the process, like the in-memory limiter.\n *\n * Windows are fixed and aligned to the clock: a 60 s window resets on the\n * minute, whichever process saw the first request. Each window has its own\n * key, created atomically with a TTL of two windows and incremented with the\n * driver's atomic increment, so a key cannot expire while its window counts.\n *\n * Two drivers are refused, with {@link UnsupportedRateLimitDriverError}:\n *\n * - `null` stores nothing, so every request would count as the first and the\n * limit would never trip.\n * - `file` has no expiry sweeper, so every past window would stay on disk\n * forever, and its increment is only serialized within one process, so\n * workers sharing the directory would lose counts.\n */\n\nconst KEY_PREFIX = \"rate-limit\";\n\n/**\n * Cache drivers that cannot back a shared limiter (see the module comment).\n */\nconst UNSUPPORTED_DRIVERS = new Set([\"null\", \"file\"]);\n\n/**\n * Thrown when `store: \"cache\"` runs on a cache driver that cannot count.\n */\nexport class UnsupportedRateLimitDriverError extends Error {\n /** Stable Warlock error code; `warlock explain WL1003` prints the fix. */\n public readonly code = \"WL1003\";\n\n public constructor(driverName: string) {\n super(\n `Rate limiting with store: \"cache\" cannot use the \"${driverName}\" cache driver: ` +\n (driverName === \"null\"\n ? \"it stores nothing, so the limit would never trip. \"\n : \"it never removes expired windows and its increment is not shared between processes. \") +\n 'Use redis or pg (or a memory driver for a single process), or set store: \"memory\".',\n );\n\n this.name = \"UnsupportedRateLimitDriverError\";\n }\n}\n\nfunction assertSupportedDriver(): void {\n const driverName = cache.currentDriver?.name;\n\n if (driverName && UNSUPPORTED_DRIVERS.has(driverName)) {\n throw new UnsupportedRateLimitDriverError(driverName);\n }\n}\n\n/**\n * Hash used for group keys and route namespaces before they reach the cache.\n */\nfunction shortHash(value: string): string {\n return createHash(\"sha256\").update(value).digest(\"hex\").slice(0, 32);\n}\n\n/**\n * The group key (an IP, a user id, whatever `keyGenerator` returned) is\n * hashed before it reaches the cache: cache drivers log their keys, and a raw\n * IP or email in a key is personal data in a log line.\n *\n * The namespace (it carries a route path) is hashed as well: flattening\n * `/users/:id` and `/users/id` to one safe string merged their counters, and\n * the file driver would turn `/` into directories. A readable, flattened\n * prefix stays in front of the hash for debugging.\n */\nfunction windowKey(namespace: string, group: string, windowIndex: number): string {\n const readable = namespace.replace(/[^A-Za-z0-9_-]+/g, \"_\").slice(0, 40);\n\n return `${KEY_PREFIX}.${readable}-${shortHash(namespace).slice(0, 16)}.${shortHash(group)}.${windowIndex}`;\n}\n\n/**\n * Count one request for `group` in the current window.\n *\n * @returns the count so far in this window and when the window resets (epoch ms)\n * @throws {UnsupportedRateLimitDriverError} on the `null` or `file` driver\n */\nexport async function incrementSharedRateLimit(\n namespace: string,\n group: string,\n windowMs: number,\n now: number = Date.now(),\n): Promise<{ current: number; resetAt: number }> {\n assertSupportedDriver();\n\n const windowIndex = Math.floor(now / windowMs);\n const key = windowKey(namespace, group, windowIndex);\n const ttlSeconds = Math.max(1, Math.ceil((2 * windowMs) / 1000));\n\n await cache.set(key, 0, { ttl: ttlSeconds, onConflict: \"create\" });\n\n const current = await cache.increment(key, 1, { ttl: ttlSeconds });\n\n return { current, resetAt: (windowIndex + 1) * windowMs };\n}\n\n/**\n * Read the count for `group` in the current window without counting a request\n */\nexport async function readSharedRateLimit(\n namespace: string,\n group: string,\n windowMs: number,\n now: number = Date.now(),\n): Promise<{ current: number; resetAt: number }> {\n assertSupportedDriver();\n\n const windowIndex = Math.floor(now / windowMs);\n const value = await cache.get(windowKey(namespace, group, windowIndex));\n\n return {\n current: typeof value === \"number\" ? value : 0,\n resetAt: (windowIndex + 1) * windowMs,\n };\n}\n\n/**\n * How often a failing shared store is reported. A cache outage fails every\n * request, so logging each one would flood the log.\n */\nconst ERROR_LOG_INTERVAL_MS = 60_000;\n\nlet lastErrorLogAt = 0;\n\n/**\n * Log a shared rate-limit store failure, at most once per minute.\n */\nexport function logSharedRateLimitError(error: unknown, now: number = Date.now()): void {\n if (now - lastErrorLogAt < ERROR_LOG_INTERVAL_MS) return;\n\n lastErrorLogAt = now;\n log.error(\"http\", \"rate-limit\", error);\n}\n\n/**\n * Forget when the last store error was logged (tests).\n */\nexport function resetSharedRateLimitErrorLog(): void {\n lastErrorLogAt = 0;\n}\n\n/**\n * Route data `@fastify/rate-limit` passes to `child()`: the route's options\n * under `routeInfo` (`fastify.rateLimit()` hooks pass an empty one).\n */\ntype ChildRouteOptions = {\n routeInfo?: { method?: unknown; url?: string; path?: string };\n};\n\n/**\n * `fastify.rateLimit()` hooks have no route; told apart by creation order,\n * which matches across processes running the same code.\n */\nlet nextHookNamespace = 0;\n\n/**\n * `@fastify/rate-limit` store backed by `@warlock.js/cache`. Selected with\n * `http.rateLimit.store: \"cache\"`; each route with its own `rateLimit` gets\n * its own namespace through `child()`.\n *\n * A failure (cache outage, unsupported driver) is logged at most once a\n * minute and passed to the plugin, which lets the request through because\n * Warlock registers it with `skipOnError: true` for this store.\n */\nexport class CacheRateLimitStore implements FastifyRateLimitStore {\n public constructor(\n _options?: FastifyRateLimitOptions,\n protected readonly namespace: string = \"global\",\n ) {}\n\n public incr(\n key: string,\n callback: (error: Error | null, result?: { current: number; ttl: number }) => void,\n timeWindow: number,\n ): void {\n incrementSharedRateLimit(this.namespace, key, timeWindow).then(\n ({ current, resetAt }) => callback(null, { current, ttl: Math.max(0, resetAt - Date.now()) }),\n (error: unknown) => {\n logSharedRateLimitError(error);\n callback(error instanceof Error ? error : new Error(String(error)));\n },\n );\n }\n\n public read(\n key: string,\n callback: (error: Error | null, result?: { current: number; ttl: number }) => void,\n timeWindow: number,\n ): void {\n readSharedRateLimit(this.namespace, key, timeWindow).then(\n ({ current, resetAt }) =>\n callback(null, { current, ttl: current === 0 ? 0 : Math.max(0, resetAt - Date.now()) }),\n (error: unknown) => {\n logSharedRateLimitError(error);\n callback(error instanceof Error ? error : new Error(String(error)));\n },\n );\n }\n\n public child(routeOptions: Parameters<FastifyRateLimitStore[\"child\"]>[0]): FastifyRateLimitStore {\n // The plugin's declared type names the route options directly, but at\n // runtime (11.x) it passes them merged under `routeInfo`.\n const routeInfo = (routeOptions as unknown as ChildRouteOptions).routeInfo ?? {};\n const method = Array.isArray(routeInfo.method)\n ? routeInfo.method.join(\",\")\n : String(routeInfo.method ?? \"\");\n const url = routeInfo.url ?? routeInfo.path ?? \"\";\n\n if (!method && !url) {\n return new CacheRateLimitStore(undefined, `hook.${nextHookNamespace++}`);\n }\n\n return new CacheRateLimitStore(undefined, `route.${method} ${url}`);\n }\n}\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* What a request's log entries carry of its input. Set globally under
|
|
4
4
|
* `http.logging` and per route with the route's `logging` option; the route
|
|
5
|
-
* wins key by key.
|
|
5
|
+
* wins key by key, unless a private URL policy suppresses that field.
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* // never log the body or the query string of this route, not even redacted
|
|
@@ -29,6 +29,12 @@ type RequestLoggingOptions = {
|
|
|
29
29
|
*/
|
|
30
30
|
query?: boolean;
|
|
31
31
|
};
|
|
32
|
+
/** A URL policy that can only remove input from snapshots, even on unmatched requests. */
|
|
33
|
+
type PrivateRequestLoggingRule = {
|
|
34
|
+
/** Path prefix at a segment boundary, or a regular expression over the normalized path. */path: string | RegExp;
|
|
35
|
+
body?: false;
|
|
36
|
+
query?: false;
|
|
37
|
+
};
|
|
32
38
|
/**
|
|
33
39
|
* `http.logging`: the app-wide defaults for {@link RequestLoggingOptions},
|
|
34
40
|
* plus query parameter names whose values are masked in logged URLs.
|
|
@@ -39,7 +45,8 @@ type HttpLoggingConfig = RequestLoggingOptions & {
|
|
|
39
45
|
* logger's default secret-key denylist and {@link URL_CREDENTIAL_PARAMS}.
|
|
40
46
|
* Matched like the logger's keys: case, `-` and `_` are ignored.
|
|
41
47
|
*/
|
|
42
|
-
redactQueryParams?: string[];
|
|
48
|
+
redactQueryParams?: string[]; /** URL policies applied before route logging; matching rules cannot be overridden by a route. */
|
|
49
|
+
private?: PrivateRequestLoggingRule[];
|
|
43
50
|
};
|
|
44
51
|
/**
|
|
45
52
|
* The bounded, plain-data view of a request that Core puts in log entries in
|
|
@@ -86,9 +93,9 @@ declare function redactUrlCredentials(url: string, extraParams?: readonly string
|
|
|
86
93
|
declare function redactQueryCredentials(query: Record<string, unknown>, extraParams?: readonly string[]): Record<string, unknown>;
|
|
87
94
|
/**
|
|
88
95
|
* The effective logging options for a request: `http.logging` overlaid by the
|
|
89
|
-
* route's own `logging` option.
|
|
96
|
+
* route's own `logging` option, then restricted by matching private URL policies.
|
|
90
97
|
*/
|
|
91
|
-
declare function resolveRequestLoggingOptions(routeOptions: RequestLoggingOptions | undefined): Required<RequestLoggingOptions> & {
|
|
98
|
+
declare function resolveRequestLoggingOptions(routeOptions: RequestLoggingOptions | undefined, url?: string, routePath?: string): Required<RequestLoggingOptions> & {
|
|
92
99
|
redactQueryParams: string[];
|
|
93
100
|
};
|
|
94
101
|
/**
|
|
@@ -119,5 +126,5 @@ type RequestLogSource = {
|
|
|
119
126
|
*/
|
|
120
127
|
declare function buildRequestLogSnapshot(source: RequestLogSource): RequestLogSnapshot;
|
|
121
128
|
//#endregion
|
|
122
|
-
export { HttpLoggingConfig, RequestLogSnapshot, RequestLogSource, RequestLoggingOptions, URL_CREDENTIAL_PARAMS, buildRequestLogSnapshot, redactQueryCredentials, redactUrlCredentials, resolveRequestLoggingOptions };
|
|
129
|
+
export { HttpLoggingConfig, PrivateRequestLoggingRule, RequestLogSnapshot, RequestLogSource, RequestLoggingOptions, URL_CREDENTIAL_PARAMS, buildRequestLogSnapshot, redactQueryCredentials, redactUrlCredentials, resolveRequestLoggingOptions };
|
|
123
130
|
//# sourceMappingURL=request-log-snapshot.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-log-snapshot.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/http/request-log-snapshot.ts"],"mappings":";;AAeA;;;;AAiBO;
|
|
1
|
+
{"version":3,"file":"request-log-snapshot.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/http/request-log-snapshot.ts"],"mappings":";;AAeA;;;;AAiBO;AAIP;;;;;KArBY,qBAAA;EAwBV;;;AACK;AAOP;;;;;EAtBE,IAAA;EA8BA;;;AAAmC;AAQrC;;EA/BE,KAAK;AAAA;;KAIK,yBAAA;EA+BV,2FA7BA,IAAA,WAAe,MAAM;EACrB,IAAA;EACA,KAAA;AAAA;;;;;KAOU,iBAAA,GAAoB,qBAAA;EA4BjB;AAQf;;;;EA9BE,iBAAA,aA0Dc;EAxDd,OAAA,GAAU,yBAAyB;AAAA;;AAwDgD;AA4DrF;;;KA5GY,kBAAA;EACV,EAAA;EACA,MAAA,UA4GA;EA1GA,GAAA,UA2GO;EAzGP,KAAA,WA4Ic;EA1Id,IAAA;EACA,EAAA;EACA,SAAA;EACA,KAAA,GAAQ,MAAA;EACR,IAAA,GAAO,MAAM;AAAA;;;;;;cAQF,qBAAA;;;;AAkI2C;AAuGxD;;;;;;;iBA7MgB,oBAAA,CAAqB,GAAA,UAAa,WAAmC;;;;;;;;;iBA4DrE,sBAAA,CACd,KAAA,EAAO,MAAA,mBACP,WAAA,uBACC,MAAM;;;;;iBAmCO,4BAAA,CACd,YAAA,EAAc,qBAAA,cACd,GAAA,WACA,SAAA,YACC,QAAA,CAAS,qBAAA;EAA2B,iBAAA;AAAA;AA0HvC;;;;AAAA,KAnBY,gBAAA;EACV,EAAA;EACA,MAAA;EACA,GAAA;EACA,EAAA;EACA,SAAA;EACA,KAAA,GAAQ,MAAA;EACR,IAAA,GAAO,MAAA;EACP,KAAA;IAAU,IAAA;IAAe,IAAA;IAAe,OAAA,GAAU,qBAAA;EAAA;AAAA;;;;;;;;;iBAWpC,uBAAA,CAAwB,MAAA,EAAQ,gBAAA,GAAmB,kBAAkB"}
|
|
@@ -84,15 +84,94 @@ function redactQueryValue(value, isCredential) {
|
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
86
|
* The effective logging options for a request: `http.logging` overlaid by the
|
|
87
|
-
* route's own `logging` option.
|
|
87
|
+
* route's own `logging` option, then restricted by matching private URL policies.
|
|
88
88
|
*/
|
|
89
|
-
function resolveRequestLoggingOptions(routeOptions) {
|
|
89
|
+
function resolveRequestLoggingOptions(routeOptions, url, routePath) {
|
|
90
90
|
const global = config.get("http.logging") ?? {};
|
|
91
|
-
|
|
91
|
+
const options = {
|
|
92
92
|
body: routeOptions?.body ?? global.body ?? true,
|
|
93
93
|
query: routeOptions?.query ?? global.query ?? true,
|
|
94
94
|
redactQueryParams: global.redactQueryParams ?? []
|
|
95
95
|
};
|
|
96
|
+
if (!global.private?.length || url === void 0) return options;
|
|
97
|
+
const candidates = loggingPathCandidates(url);
|
|
98
|
+
if (candidates === void 0) return {
|
|
99
|
+
...options,
|
|
100
|
+
body: false,
|
|
101
|
+
query: false
|
|
102
|
+
};
|
|
103
|
+
if (routePath) {
|
|
104
|
+
const routeCandidates = loggingPathCandidates(routePath);
|
|
105
|
+
if (!routeCandidates) return {
|
|
106
|
+
...options,
|
|
107
|
+
body: false,
|
|
108
|
+
query: false
|
|
109
|
+
};
|
|
110
|
+
candidates.push(...routeCandidates);
|
|
111
|
+
}
|
|
112
|
+
for (const rule of compiledPrivateRules(global.private)) {
|
|
113
|
+
let matches;
|
|
114
|
+
if (rule.type === "regex") {
|
|
115
|
+
const { regex, needsClone } = rule;
|
|
116
|
+
matches = candidates.some((candidate) => (needsClone ? new RegExp(regex.source, regex.flags) : regex).test(candidate));
|
|
117
|
+
} else {
|
|
118
|
+
const { prefixes } = rule;
|
|
119
|
+
matches = prefixes === void 0 || prefixes.some((prefix) => candidates.some((candidate) => prefix === "/" || candidate === prefix || candidate.startsWith(`${prefix}/`)));
|
|
120
|
+
}
|
|
121
|
+
if (!matches) continue;
|
|
122
|
+
if (rule.body === false) options.body = false;
|
|
123
|
+
if (rule.query === false) options.query = false;
|
|
124
|
+
}
|
|
125
|
+
return options;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Compiled rules per configured `http.logging.private` array. Snapshots are
|
|
129
|
+
* built several times per request, so each array's prefixes are normalised
|
|
130
|
+
* once; replacing the array (a new config value) compiles the new one.
|
|
131
|
+
*/
|
|
132
|
+
const compiledRulesCache = /* @__PURE__ */ new WeakMap();
|
|
133
|
+
function compiledPrivateRules(rules) {
|
|
134
|
+
let compiled = compiledRulesCache.get(rules);
|
|
135
|
+
if (compiled) return compiled;
|
|
136
|
+
compiled = rules.map((rule) => {
|
|
137
|
+
const { path, body, query } = rule;
|
|
138
|
+
if (path instanceof RegExp) return {
|
|
139
|
+
type: "regex",
|
|
140
|
+
regex: path,
|
|
141
|
+
needsClone: path.global || path.sticky,
|
|
142
|
+
body,
|
|
143
|
+
query
|
|
144
|
+
};
|
|
145
|
+
return {
|
|
146
|
+
type: "prefix",
|
|
147
|
+
prefixes: loggingPathCandidates(path),
|
|
148
|
+
body,
|
|
149
|
+
query
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
compiledRulesCache.set(rules, compiled);
|
|
153
|
+
return compiled;
|
|
154
|
+
}
|
|
155
|
+
/** Backslashes to slashes, repeated slashes collapsed, no trailing slash. */
|
|
156
|
+
function collapseLoggingPath(path) {
|
|
157
|
+
return path.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\/$/, "") || "/";
|
|
158
|
+
}
|
|
159
|
+
function loggingPathCandidates(url) {
|
|
160
|
+
try {
|
|
161
|
+
const pathname = url.replace(/^[a-z][a-z\d+.-]*:\/\/[^/?#]*/i, "").split(/[?#]/u)[0] || "/";
|
|
162
|
+
const raw = collapseLoggingPath(pathname);
|
|
163
|
+
const decoded = collapseLoggingPath(decodeURIComponent(pathname));
|
|
164
|
+
const segments = [];
|
|
165
|
+
for (const segment of decoded.split("/")) if (segment === "..") segments.pop();
|
|
166
|
+
else if (segment && segment !== ".") segments.push(segment);
|
|
167
|
+
return [...new Set([
|
|
168
|
+
raw,
|
|
169
|
+
decoded,
|
|
170
|
+
`/${segments.join("/")}`
|
|
171
|
+
])];
|
|
172
|
+
} catch {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
96
175
|
}
|
|
97
176
|
/**
|
|
98
177
|
* Build the {@link RequestLogSnapshot} for a request, applying the route's
|
|
@@ -103,7 +182,7 @@ function resolveRequestLoggingOptions(routeOptions) {
|
|
|
103
182
|
* Uploaded files are summarised (`{ file, mimeType }`) rather than walked.
|
|
104
183
|
*/
|
|
105
184
|
function buildRequestLogSnapshot(source) {
|
|
106
|
-
const options = resolveRequestLoggingOptions(source.route?.logging);
|
|
185
|
+
const options = resolveRequestLoggingOptions(source.route?.logging, source.url, source.route?.path);
|
|
107
186
|
const url = options.query ? source.url : stripQuery(source.url);
|
|
108
187
|
const snapshot = {
|
|
109
188
|
id: source.id,
|