@slopware/sloppy-darwin-x64 0.1.0-alpha.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/LICENSE +201 -0
- package/README.md +5 -0
- package/bin/sloppy +0 -0
- package/bin/sloppyc +0 -0
- package/docs/KNOWN_LIMITATIONS.md +16 -0
- package/docs/LICENSES.md +6 -0
- package/docs/NOTICE.md +8 -0
- package/examples/README.md +140 -0
- package/examples/auth-api/README.md +20 -0
- package/examples/auth-api/app.js +61 -0
- package/examples/auth-api/appsettings.json +7 -0
- package/examples/auth-api/sloppy.json +5 -0
- package/examples/cache-basic/README.md +9 -0
- package/examples/cache-basic/app.js +32 -0
- package/examples/cache-hybrid-postgres/README.md +10 -0
- package/examples/cache-hybrid-postgres/app.js +27 -0
- package/examples/cache-output-api/README.md +10 -0
- package/examples/cache-output-api/app.js +35 -0
- package/examples/codec-base64-hex/README.md +14 -0
- package/examples/codec-base64-hex/app.js +15 -0
- package/examples/codec-checksums/README.md +15 -0
- package/examples/codec-checksums/app.js +8 -0
- package/examples/codec-compression/README.md +13 -0
- package/examples/codec-compression/app.js +9 -0
- package/examples/codec-streaming-compression/README.md +19 -0
- package/examples/codec-streaming-compression/app.js +16 -0
- package/examples/codec-text-binary/README.md +16 -0
- package/examples/codec-text-binary/app.js +17 -0
- package/examples/compiler-hello/README.md +71 -0
- package/examples/compiler-hello/app.js +7 -0
- package/examples/compiler-hello/expected/app.js +8 -0
- package/examples/compiler-hello/expected/app.js.map +53 -0
- package/examples/compiler-hello/expected/app.plan.json +229 -0
- package/examples/compiler-hello/expected/routes.slrt +0 -0
- package/examples/config-basic/README.md +13 -0
- package/examples/config-basic/app.js +13 -0
- package/examples/config-basic/appsettings.json +7 -0
- package/examples/config-secrets-redaction/README.md +9 -0
- package/examples/config-secrets-redaction/app.js +9 -0
- package/examples/config-secrets-redaction/appsettings.json +5 -0
- package/examples/config-strict-mode/README.md +7 -0
- package/examples/config-strict-mode/app.js +10 -0
- package/examples/config-strict-mode/appsettings.json +7 -0
- package/examples/configured-api/README.md +38 -0
- package/examples/configured-api/app.js +12 -0
- package/examples/configured-api/appsettings.Development.json +5 -0
- package/examples/configured-api/appsettings.json +6 -0
- package/examples/configured-api/sloppy.json +5 -0
- package/examples/core-config-secrets/README.md +10 -0
- package/examples/core-config-secrets/app.js +15 -0
- package/examples/core-fs-time-codec/README.md +9 -0
- package/examples/core-fs-time-codec/app.js +8 -0
- package/examples/core-network-time-codec/README.md +11 -0
- package/examples/core-network-time-codec/app.js +20 -0
- package/examples/core-policy-audit/README.md +7 -0
- package/examples/core-policy-audit/app.js +22 -0
- package/examples/core-process-time-codec/README.md +8 -0
- package/examples/core-process-time-codec/app.js +28 -0
- package/examples/core-worker-time/README.md +8 -0
- package/examples/core-worker-time/app.js +17 -0
- package/examples/crypto-hash-hmac/README.md +17 -0
- package/examples/crypto-hash-hmac/app.js +29 -0
- package/examples/crypto-password/README.md +21 -0
- package/examples/crypto-password/app.js +12 -0
- package/examples/crypto-random-token/README.md +16 -0
- package/examples/crypto-random-token/app.js +12 -0
- package/examples/crypto-secret-constant-time/README.md +21 -0
- package/examples/crypto-secret-constant-time/app.js +15 -0
- package/examples/data-foundation/README.md +39 -0
- package/examples/data-foundation/app.js +63 -0
- package/examples/dependency-graph/README.md +19 -0
- package/examples/dependency-graph/fixtures/graph-helper/index.js +3 -0
- package/examples/dependency-graph/fixtures/graph-helper/package.json +6 -0
- package/examples/dependency-graph/package.json +7 -0
- package/examples/dependency-graph/public/message.txt +1 -0
- package/examples/dependency-graph/sloppy.json +9 -0
- package/examples/dependency-graph/src/main.ts +8 -0
- package/examples/dogfood/README.md +23 -0
- package/examples/dogfood/dogfood.json +136 -0
- package/examples/dynamic-module-include/README.md +20 -0
- package/examples/dynamic-module-include/public/readme.txt +1 -0
- package/examples/dynamic-module-include/sloppy.json +12 -0
- package/examples/dynamic-module-include/src/main.ts +6 -0
- package/examples/dynamic-module-include/src/plugins/alpha.js +3 -0
- package/examples/dynamic-module-include/src/plugins/beta.js +3 -0
- package/examples/ergonomics/README.md +42 -0
- package/examples/ergonomics/app.js +38 -0
- package/examples/framework-controller/README.md +12 -0
- package/examples/framework-controller/app.js +31 -0
- package/examples/framework-di-services/README.md +17 -0
- package/examples/framework-di-services/app.ts +40 -0
- package/examples/framework-explicit-binding/README.md +12 -0
- package/examples/framework-explicit-binding/app.ts +34 -0
- package/examples/framework-hello/README.md +16 -0
- package/examples/framework-hello/app.ts +16 -0
- package/examples/framework-postgres-crud/README.md +73 -0
- package/examples/framework-postgres-crud/app.ts +64 -0
- package/examples/framework-sqlite-crud/README.md +52 -0
- package/examples/framework-sqlite-crud/app.ts +90 -0
- package/examples/framework-sqlite-crud/appsettings.json +11 -0
- package/examples/framework-sqlserver-crud/README.md +73 -0
- package/examples/framework-sqlserver-crud/app.ts +64 -0
- package/examples/framework-validation-errors/README.md +12 -0
- package/examples/framework-validation-errors/app.ts +16 -0
- package/examples/fs-basic/README.md +24 -0
- package/examples/fs-basic/app.js +12 -0
- package/examples/fs-roots-policy/README.md +14 -0
- package/examples/fs-roots-policy/app.js +4 -0
- package/examples/fs-streams/README.md +18 -0
- package/examples/fs-streams/app.js +11 -0
- package/examples/fs-watch/README.md +19 -0
- package/examples/fs-watch/app.js +11 -0
- package/examples/hello/README.md +63 -0
- package/examples/hello/app.js +19 -0
- package/examples/hello-minimal/README.md +51 -0
- package/examples/hello-minimal/sloppy.json +5 -0
- package/examples/hello-minimal/src/main.ts +9 -0
- package/examples/http-client-basic/README.md +11 -0
- package/examples/http-client-basic/app.js +46 -0
- package/examples/http-client-generated/README.md +22 -0
- package/examples/http-client-generated/openapi.json +45 -0
- package/examples/http-client-resilience/README.md +4 -0
- package/examples/http-client-resilience/app.js +38 -0
- package/examples/http-client-runtime-loopback/README.md +24 -0
- package/examples/http-client-testhost/README.md +4 -0
- package/examples/http-client-testhost/app.js +27 -0
- package/examples/http-client-testhost-package-mock/README.md +26 -0
- package/examples/http-client-typed/README.md +5 -0
- package/examples/http-client-typed/app.js +33 -0
- package/examples/modules-api/README.md +30 -0
- package/examples/modules-api/app.js +9 -0
- package/examples/modules-api/modules/routes.js +16 -0
- package/examples/modules-api/sloppy.json +5 -0
- package/examples/modules-basic/README.md +32 -0
- package/examples/modules-basic/app.js +41 -0
- package/examples/net-deadline-cancel/README.md +13 -0
- package/examples/net-deadline-cancel/app.js +34 -0
- package/examples/net-local-ipc/README.md +12 -0
- package/examples/net-local-ipc/app.js +46 -0
- package/examples/net-policy-strict/README.md +12 -0
- package/examples/net-policy-strict/app.js +34 -0
- package/examples/net-tcp-client/README.md +10 -0
- package/examples/net-tcp-client/app.js +23 -0
- package/examples/net-tcp-echo/README.md +11 -0
- package/examples/net-tcp-echo/app.js +45 -0
- package/examples/net-tcp-server/README.md +10 -0
- package/examples/net-tcp-server/app.js +28 -0
- package/examples/node-compat-path-events/README.md +15 -0
- package/examples/node-compat-path-events/sloppy.json +6 -0
- package/examples/node-compat-path-events/src/main.ts +15 -0
- package/examples/ops-compiler/README.md +9 -0
- package/examples/ops-compiler/app.js +26 -0
- package/examples/ops-health-metrics-management/README.md +14 -0
- package/examples/ops-health-metrics-management/app.js +24 -0
- package/examples/orm-basic/README.md +17 -0
- package/examples/orm-basic/app.js +82 -0
- package/examples/orm-cursor-export/README.md +16 -0
- package/examples/orm-cursor-export/app.js +28 -0
- package/examples/orm-migrations/README.md +14 -0
- package/examples/orm-migrations/migrations/.gitkeep +1 -0
- package/examples/orm-migrations/sloppy.json +9 -0
- package/examples/orm-migrations/src/app.ts +34 -0
- package/examples/orm-relations-includes/README.md +10 -0
- package/examples/orm-relations-includes/app.js +47 -0
- package/examples/orm-testservices/README.md +37 -0
- package/examples/orm-testservices/test.mjs +32 -0
- package/examples/os-runtime-api/README.md +11 -0
- package/examples/os-runtime-api/app.js +44 -0
- package/examples/package-zod-like/README.md +28 -0
- package/examples/package-zod-like/fixtures/zod-like/index.js +48 -0
- package/examples/package-zod-like/fixtures/zod-like/package.json +12 -0
- package/examples/package-zod-like/package.json +7 -0
- package/examples/package-zod-like/sloppy.json +6 -0
- package/examples/package-zod-like/src/main.ts +16 -0
- package/examples/postgres-basic/README.md +31 -0
- package/examples/postgres-basic/app.js +50 -0
- package/examples/prealpha-control-plane/README.md +50 -0
- package/examples/prealpha-control-plane/appsettings.Development.json +11 -0
- package/examples/prealpha-control-plane/appsettings.json +15 -0
- package/examples/prealpha-control-plane/sloppy.json +5 -0
- package/examples/prealpha-control-plane/src/db/schema.js +7 -0
- package/examples/prealpha-control-plane/src/db/seed.js +6 -0
- package/examples/prealpha-control-plane/src/main.js +21 -0
- package/examples/prealpha-control-plane/src/routes/apps.js +34 -0
- package/examples/prealpha-control-plane/src/routes/builds.js +25 -0
- package/examples/prealpha-control-plane/src/routes/deployments.js +19 -0
- package/examples/prealpha-control-plane/src/routes/diagnostics.js +11 -0
- package/examples/prealpha-control-plane/src/routes/health.js +27 -0
- package/examples/prealpha-control-plane/src/routes/projects.js +38 -0
- package/examples/prealpha-control-plane/src/services/diagnosticsSink.js +11 -0
- package/examples/prealpha-control-plane/src/services/repositories.js +9 -0
- package/examples/prealpha-control-plane/src/validation/schemas.js +6 -0
- package/examples/program-fs-process/README.md +31 -0
- package/examples/program-fs-process/sloppy.json +9 -0
- package/examples/program-fs-process/src/main.ts +27 -0
- package/examples/program-hello/README.md +32 -0
- package/examples/program-hello/main.ts +8 -0
- package/examples/program-hello/message.ts +1 -0
- package/examples/program-hello/sloppy.json +5 -0
- package/examples/rate-limit-auth/README.md +3 -0
- package/examples/rate-limit-auth/app.js +14 -0
- package/examples/rate-limit-basic/README.md +3 -0
- package/examples/rate-limit-basic/app.js +13 -0
- package/examples/rate-limit-redis/README.md +5 -0
- package/examples/rate-limit-redis/app.js +20 -0
- package/examples/rate-limit-testhost/README.md +4 -0
- package/examples/rate-limit-testhost/app.js +13 -0
- package/examples/rate-limit-websocket/README.md +3 -0
- package/examples/rate-limit-websocket/app.js +16 -0
- package/examples/realtime-auth/README.md +8 -0
- package/examples/realtime-auth/app.js +25 -0
- package/examples/realtime-auth/test.mjs +43 -0
- package/examples/realtime-chat/README.md +8 -0
- package/examples/realtime-chat/app.js +32 -0
- package/examples/realtime-chat/test.mjs +52 -0
- package/examples/realtime-dashboard/README.md +20 -0
- package/examples/realtime-dashboard/app.js +37 -0
- package/examples/realtime-presence/README.md +8 -0
- package/examples/realtime-presence/app.js +32 -0
- package/examples/realtime-presence/test.mjs +50 -0
- package/examples/realtime-testhost/README.md +8 -0
- package/examples/realtime-testhost/test.mjs +31 -0
- package/examples/redis-basic/README.md +17 -0
- package/examples/redis-basic/app.js +39 -0
- package/examples/redis-cache/README.md +14 -0
- package/examples/redis-cache/app.js +36 -0
- package/examples/redis-locks/README.md +13 -0
- package/examples/redis-locks/app.js +49 -0
- package/examples/request-context/README.md +32 -0
- package/examples/request-context/app.js +15 -0
- package/examples/sqlite-basic/README.md +52 -0
- package/examples/sqlite-basic/app.js +56 -0
- package/examples/sqlserver-basic/README.md +36 -0
- package/examples/sqlserver-basic/app.js +59 -0
- package/examples/static-files-basic/README.md +11 -0
- package/examples/static-files-basic/app.js +12 -0
- package/examples/static-files-basic/public/app.js +1 -0
- package/examples/static-files-basic/public/site.css +3 -0
- package/examples/static-files-package/README.md +12 -0
- package/examples/static-files-package/app.js +10 -0
- package/examples/static-files-package/public/index.html +2 -0
- package/examples/static-files-precompressed/README.md +12 -0
- package/examples/static-files-precompressed/app.js +11 -0
- package/examples/static-files-precompressed/public/app.js +1 -0
- package/examples/static-files-precompressed/public/app.js.br +0 -0
- package/examples/static-files-precompressed/public/app.js.gz +0 -0
- package/examples/static-files-spa/README.md +12 -0
- package/examples/static-files-spa/app.js +16 -0
- package/examples/static-files-spa/dist/assets/app.js +1 -0
- package/examples/static-files-spa/dist/index.html +4 -0
- package/examples/static-files-testhost/README.md +8 -0
- package/examples/static-files-testhost/app.js +13 -0
- package/examples/static-files-testhost/public/app.js +1 -0
- package/examples/static-files-testhost/public/app.js.gz +0 -0
- package/examples/static-files-testhost/test.mjs +38 -0
- package/examples/testhost-basic/README.md +26 -0
- package/examples/testhost-db/README.md +31 -0
- package/examples/testservices-postgres/README.md +68 -0
- package/examples/testservices-redis/README.md +71 -0
- package/examples/testservices-sqlserver/README.md +75 -0
- package/examples/time-basic/README.md +18 -0
- package/examples/time-basic/app.js +12 -0
- package/examples/time-deadline-cancellation/README.md +11 -0
- package/examples/time-deadline-cancellation/app.js +27 -0
- package/examples/time-fake-clock/README.md +14 -0
- package/examples/time-fake-clock/app.js +25 -0
- package/examples/time-interval-schedule/README.md +13 -0
- package/examples/time-interval-schedule/app.js +60 -0
- package/examples/users-api-sqlite/README.md +74 -0
- package/examples/users-api-sqlite/app.js +11 -0
- package/examples/users-api-sqlite/appsettings.Development.json +11 -0
- package/examples/users-api-sqlite/appsettings.json +11 -0
- package/examples/users-api-sqlite/modules/users.js +40 -0
- package/examples/users-api-sqlite/sloppy.json +5 -0
- package/examples/validation-errors/README.md +36 -0
- package/examples/validation-errors/app.js +14 -0
- package/examples/validation-errors/invalid-user.http +6 -0
- package/examples/validation-errors/sloppy.json +5 -0
- package/examples/web-dynamic-routes/README.md +17 -0
- package/examples/web-dynamic-routes/app.ts +27 -0
- package/examples/webhooks-basic/README.md +11 -0
- package/examples/webhooks-basic/app.js +48 -0
- package/examples/websocket-auth/README.md +8 -0
- package/examples/websocket-auth/app.js +16 -0
- package/examples/websocket-echo/README.md +9 -0
- package/examples/websocket-echo/app.js +36 -0
- package/examples/websocket-json-schema/README.md +5 -0
- package/examples/websocket-json-schema/app.js +25 -0
- package/examples/websocket-testhost/README.md +11 -0
- package/examples/websocket-testhost/test.mjs +49 -0
- package/examples/workers-background-service/README.md +7 -0
- package/examples/workers-background-service/app.js +16 -0
- package/examples/workers-js-isolate/README.md +8 -0
- package/examples/workers-js-isolate/app.js +19 -0
- package/examples/workers-js-isolate/workers/parser.ts +11 -0
- package/examples/workers-shutdown/README.md +6 -0
- package/examples/workers-shutdown/app.js +26 -0
- package/examples/workers-workerpool/README.md +6 -0
- package/examples/workers-workerpool/app.js +23 -0
- package/examples/workers-workqueue/README.md +8 -0
- package/examples/workers-workqueue/app.js +24 -0
- package/manifest.json +59 -0
- package/package.json +31 -0
- package/stdlib/sloppy/README.md +177 -0
- package/stdlib/sloppy/app.js +2142 -0
- package/stdlib/sloppy/auth.js +1813 -0
- package/stdlib/sloppy/bootstrap.manifest.json +83 -0
- package/stdlib/sloppy/cache.js +1542 -0
- package/stdlib/sloppy/codec.js +1153 -0
- package/stdlib/sloppy/config.js +61 -0
- package/stdlib/sloppy/crypto.js +312 -0
- package/stdlib/sloppy/data.js +2945 -0
- package/stdlib/sloppy/ffi.js +185 -0
- package/stdlib/sloppy/fs.js +795 -0
- package/stdlib/sloppy/health.js +603 -0
- package/stdlib/sloppy/http.js +1595 -0
- package/stdlib/sloppy/index.js +59 -0
- package/stdlib/sloppy/internal/bytes.js +31 -0
- package/stdlib/sloppy/internal/capabilities.js +155 -0
- package/stdlib/sloppy/internal/config.js +640 -0
- package/stdlib/sloppy/internal/disposable.js +31 -0
- package/stdlib/sloppy/internal/headers.js +63 -0
- package/stdlib/sloppy/internal/intrinsics.js +2 -0
- package/stdlib/sloppy/internal/json.js +20 -0
- package/stdlib/sloppy/internal/logging.js +278 -0
- package/stdlib/sloppy/internal/modules.js +405 -0
- package/stdlib/sloppy/internal/redaction.js +87 -0
- package/stdlib/sloppy/internal/routes.js +2279 -0
- package/stdlib/sloppy/internal/runtime-classic.js +19837 -0
- package/stdlib/sloppy/internal/services.js +690 -0
- package/stdlib/sloppy/internal/shared.js +32 -0
- package/stdlib/sloppy/internal/testhost-diagnostics.js +88 -0
- package/stdlib/sloppy/internal/testhost-http-server.js +238 -0
- package/stdlib/sloppy/internal/testhost-http.js +118 -0
- package/stdlib/sloppy/internal/testhost-loopback.js +50 -0
- package/stdlib/sloppy/internal/testservices-docker.js +154 -0
- package/stdlib/sloppy/internal/validation.js +117 -0
- package/stdlib/sloppy/metrics.js +427 -0
- package/stdlib/sloppy/net.js +5208 -0
- package/stdlib/sloppy/node/assert/strict.js +39 -0
- package/stdlib/sloppy/node/assert.js +228 -0
- package/stdlib/sloppy/node/buffer.js +247 -0
- package/stdlib/sloppy/node/console.js +33 -0
- package/stdlib/sloppy/node/constants.js +9 -0
- package/stdlib/sloppy/node/crypto.js +89 -0
- package/stdlib/sloppy/node/diagnostics_channel.js +41 -0
- package/stdlib/sloppy/node/events.js +113 -0
- package/stdlib/sloppy/node/fs/promises.js +27 -0
- package/stdlib/sloppy/node/fs.js +280 -0
- package/stdlib/sloppy/node/http.js +11 -0
- package/stdlib/sloppy/node/https.js +11 -0
- package/stdlib/sloppy/node/module.js +40 -0
- package/stdlib/sloppy/node/os.js +22 -0
- package/stdlib/sloppy/node/path.js +78 -0
- package/stdlib/sloppy/node/perf_hooks.js +12 -0
- package/stdlib/sloppy/node/process.js +129 -0
- package/stdlib/sloppy/node/querystring.js +21 -0
- package/stdlib/sloppy/node/stream/promises.js +3 -0
- package/stdlib/sloppy/node/stream.js +132 -0
- package/stdlib/sloppy/node/string_decoder.js +23 -0
- package/stdlib/sloppy/node/timers.js +26 -0
- package/stdlib/sloppy/node/tty.js +18 -0
- package/stdlib/sloppy/node/url.js +17 -0
- package/stdlib/sloppy/node/util.js +95 -0
- package/stdlib/sloppy/node/zlib.js +72 -0
- package/stdlib/sloppy/orm.js +2188 -0
- package/stdlib/sloppy/os.js +580 -0
- package/stdlib/sloppy/problem-details.js +29 -0
- package/stdlib/sloppy/providers/sqlite.js +26 -0
- package/stdlib/sloppy/rate-limit.js +856 -0
- package/stdlib/sloppy/realtime.js +1508 -0
- package/stdlib/sloppy/redis.js +1272 -0
- package/stdlib/sloppy/request-id.js +184 -0
- package/stdlib/sloppy/request-logging.js +101 -0
- package/stdlib/sloppy/results.js +933 -0
- package/stdlib/sloppy/schema.js +546 -0
- package/stdlib/sloppy/testing.js +4081 -0
- package/stdlib/sloppy/testservices.js +1041 -0
- package/stdlib/sloppy/time.js +894 -0
- package/stdlib/sloppy/webhooks.js +1330 -0
- package/stdlib/sloppy/workers.js +986 -0
- package/templates/api/README.md +82 -0
- package/templates/api/appsettings.Development.json +14 -0
- package/templates/api/appsettings.json +13 -0
- package/templates/api/data/.gitkeep +1 -0
- package/templates/api/gitignore +4 -0
- package/templates/api/migrations/0001_create_users.sql +1 -0
- package/templates/api/package.json +16 -0
- package/templates/api/public/hello.txt +1 -0
- package/templates/api/sloppy.json +14 -0
- package/templates/api/src/config.ts +1 -0
- package/templates/api/src/db/migrate.ts +14 -0
- package/templates/api/src/db/schema.ts +4 -0
- package/templates/api/src/db/usersRepository.ts +23 -0
- package/templates/api/src/main.ts +18 -0
- package/templates/api/src/models/user.ts +7 -0
- package/templates/api/src/routes/health.ts +20 -0
- package/templates/api/src/routes/users.ts +40 -0
- package/templates/api/src/services/usersService.ts +21 -0
- package/templates/api/tsconfig.json +15 -0
- package/templates/cli/README.md +16 -0
- package/templates/cli/gitignore +2 -0
- package/templates/cli/package.json +13 -0
- package/templates/cli/sloppy.json +6 -0
- package/templates/cli/src/commands/echo.ts +9 -0
- package/templates/cli/src/commands/inspect.ts +20 -0
- package/templates/cli/src/main.ts +50 -0
- package/templates/cli/tsconfig.json +15 -0
- package/templates/minimal-api/README.md +14 -0
- package/templates/minimal-api/gitignore +3 -0
- package/templates/minimal-api/package.json +14 -0
- package/templates/minimal-api/sloppy.json +5 -0
- package/templates/minimal-api/src/main.ts +9 -0
- package/templates/minimal-api/tsconfig.json +15 -0
- package/templates/node-compat/README.md +40 -0
- package/templates/node-compat/gitignore +2 -0
- package/templates/node-compat/package.json +11 -0
- package/templates/node-compat/sloppy.json +6 -0
- package/templates/node-compat/src/main.ts +40 -0
- package/templates/package-api/README.md +44 -0
- package/templates/package-api/fixtures/validator-lite/index.js +7 -0
- package/templates/package-api/fixtures/validator-lite/package.json +6 -0
- package/templates/package-api/gitignore +3 -0
- package/templates/package-api/package.json +17 -0
- package/templates/package-api/sloppy.json +5 -0
- package/templates/package-api/src/main.ts +10 -0
- package/templates/package-api/src/routes/health.ts +5 -0
- package/templates/package-api/src/routes/users.ts +12 -0
- package/templates/package-api/tsconfig.json +15 -0
- package/templates/program/README.md +12 -0
- package/templates/program/gitignore +1 -0
- package/templates/program/package.json +10 -0
- package/templates/program/sloppy.json +6 -0
- package/templates/program/src/main.ts +9 -0
|
@@ -0,0 +1,2279 @@
|
|
|
1
|
+
import { defineFunctionModuleName } from "./modules.js";
|
|
2
|
+
import {
|
|
3
|
+
anonymousUser,
|
|
4
|
+
authorizePolicy,
|
|
5
|
+
authorizeRoute,
|
|
6
|
+
normalizeAuthRequirement,
|
|
7
|
+
snapshotAuthRequirement,
|
|
8
|
+
} from "../auth.js";
|
|
9
|
+
import { Cache, isCache, stableHash } from "../cache.js";
|
|
10
|
+
import { Text } from "../codec.js";
|
|
11
|
+
import {
|
|
12
|
+
createSseRouteHandler,
|
|
13
|
+
createRealtimeRouteHandler,
|
|
14
|
+
createWebSocketRouteHandler,
|
|
15
|
+
normalizeWebSocketRouteOptions,
|
|
16
|
+
realtimeRouteMetadata,
|
|
17
|
+
webSocketRouteOptions,
|
|
18
|
+
} from "../realtime.js";
|
|
19
|
+
import {
|
|
20
|
+
enforceRateLimit,
|
|
21
|
+
isRateLimitPolicy,
|
|
22
|
+
snapshotRateLimitPolicy,
|
|
23
|
+
} from "../rate-limit.js";
|
|
24
|
+
import { Results } from "../results.js";
|
|
25
|
+
import { isSchema, schema as Schema } from "../schema.js";
|
|
26
|
+
import { cleanupAfterFailure, finishWithCleanup, validateServiceToken } from "./services.js";
|
|
27
|
+
import { isPlainObject } from "./shared.js";
|
|
28
|
+
|
|
29
|
+
const ROUTE_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
30
|
+
const ROUTE_KINDS = new Set(["http", "sse", "websocket"]);
|
|
31
|
+
const PREFLIGHT_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
32
|
+
const HEADER_TOKEN_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/u;
|
|
33
|
+
const ROUTE_PARAM_PATTERN = /^\{([A-Za-z_][0-9A-Za-z_]*)(?::(str|int|uuid|alpha|float))?\}$/u;
|
|
34
|
+
const MEDIA_TYPE_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+\/[!#$%&'*+\-.^_`|~0-9A-Za-z]+(?:\s*;\s*[!#$%&'*+\-.^_`|~0-9A-Za-z]+=[!#$%&'*+\-.^_`|~0-9A-Za-z]+)*$/u;
|
|
35
|
+
const DEFAULT_OUTPUT_CACHE_STATUS_CODES = Object.freeze([200, 203, 204]);
|
|
36
|
+
const DEFAULT_OUTPUT_CACHE_MAX_BODY_BYTES = 1024 * 1024;
|
|
37
|
+
|
|
38
|
+
function validatePattern(pattern) {
|
|
39
|
+
if (typeof pattern !== "string" || pattern.length === 0 || !pattern.startsWith("/")) {
|
|
40
|
+
throw new TypeError("Sloppy app.mapGet pattern must be a non-empty string starting with '/'.");
|
|
41
|
+
}
|
|
42
|
+
if (pattern === "/") {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (pattern !== "/" && (pattern.endsWith("/") || pattern.includes("//"))) {
|
|
46
|
+
throw new TypeError("Sloppy route patterns use strict slashes and must not end with '/' or contain '//'.");
|
|
47
|
+
}
|
|
48
|
+
for (const segment of pattern.split("/").slice(1)) {
|
|
49
|
+
if (segment.length === 0) {
|
|
50
|
+
throw new TypeError("Sloppy route patterns must not contain empty segments.");
|
|
51
|
+
}
|
|
52
|
+
if ((segment.includes("{") || segment.includes("}")) && !ROUTE_PARAM_PATTERN.test(segment)) {
|
|
53
|
+
throw new TypeError("Sloppy route parameters must be whole segments like {id}, {id:int}, {id:uuid}, {slug:alpha}, or {value:float}.");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function validateGroupPrefix(prefix) {
|
|
59
|
+
if (typeof prefix !== "string" || prefix.length === 0 || !prefix.startsWith("/")) {
|
|
60
|
+
throw new TypeError("Sloppy app.mapGroup prefix must be a non-empty string starting with '/'.");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateGroupChildPattern(pattern) {
|
|
65
|
+
if (typeof pattern !== "string" || pattern.length === 0) {
|
|
66
|
+
throw new TypeError("Sloppy route group child pattern must be a non-empty string.");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateHandler(handler) {
|
|
71
|
+
if (typeof handler !== "function") {
|
|
72
|
+
throw new TypeError("Sloppy route handler must be a function.");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateMiddleware(middleware) {
|
|
77
|
+
if (typeof middleware !== "function") {
|
|
78
|
+
throw new TypeError("Sloppy middleware must be a function.");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function validateMiddlewareEntry(entry) {
|
|
83
|
+
if (entry === null || typeof entry !== "object") {
|
|
84
|
+
throw new TypeError("Sloppy middleware entries must carry { fn, sequence }.");
|
|
85
|
+
}
|
|
86
|
+
validateMiddleware(entry.fn);
|
|
87
|
+
if (typeof entry.sequence !== "number") {
|
|
88
|
+
throw new TypeError("Sloppy middleware entries must carry a numeric sequence.");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function orderedMiddlewareFunctions(entries) {
|
|
93
|
+
return [...entries]
|
|
94
|
+
.sort((a, b) => a.sequence - b.sequence)
|
|
95
|
+
.map((entry) => entry.fn);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function middlewareMetadata(middleware) {
|
|
99
|
+
return Object.freeze({
|
|
100
|
+
count: middleware.length,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function invokeMiddlewarePipeline(context, middleware, terminal) {
|
|
105
|
+
let index = -1;
|
|
106
|
+
|
|
107
|
+
function dispatch(nextIndex) {
|
|
108
|
+
if (nextIndex <= index) {
|
|
109
|
+
throw new Error("Sloppy middleware next() must not be called more than once.");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
index = nextIndex;
|
|
113
|
+
const current = middleware[nextIndex];
|
|
114
|
+
if (current === undefined) {
|
|
115
|
+
return terminal();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let nextCalled = false;
|
|
119
|
+
let downstreamPromise;
|
|
120
|
+
function next() {
|
|
121
|
+
if (nextCalled) {
|
|
122
|
+
throw new Error("Sloppy middleware next() must not be called more than once.");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
nextCalled = true;
|
|
126
|
+
const downstream = dispatch(nextIndex + 1);
|
|
127
|
+
downstreamPromise = Promise.resolve(downstream);
|
|
128
|
+
return downstream;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const middlewareReturn = current(context, next);
|
|
132
|
+
if (!nextCalled) {
|
|
133
|
+
return middlewareReturn;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return Promise.resolve(middlewareReturn).then(
|
|
137
|
+
(value) => downstreamPromise.then(() => value),
|
|
138
|
+
(error) => {
|
|
139
|
+
if (downstreamPromise === undefined) {
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
return downstreamPromise.then(
|
|
143
|
+
() => {
|
|
144
|
+
throw error;
|
|
145
|
+
},
|
|
146
|
+
() => {
|
|
147
|
+
throw error;
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return dispatch(0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function handleRouteError(host, error, context) {
|
|
158
|
+
if (typeof host.handleError !== "function") {
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
return host.handleError(error, context);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function appendContextResponseHeaders(result, context) {
|
|
165
|
+
const responseHeaders = context?.__sloppyResponseHeaders;
|
|
166
|
+
if (!isPlainObject(responseHeaders) || result === null || typeof result !== "object") {
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return Object.freeze({
|
|
171
|
+
...result,
|
|
172
|
+
headers: Object.freeze({
|
|
173
|
+
...(isPlainObject(result.headers) ? result.headers : {}),
|
|
174
|
+
...responseHeaders,
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function finishRouteResult(result, policy, context) {
|
|
180
|
+
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
181
|
+
return Promise.resolve(result).then((value) => finishRouteResult(value, policy, context));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return finishWithCors(appendContextResponseHeaders(result, context), policy, context);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function finishHandledRouteError(host, error, policy, context) {
|
|
188
|
+
return finishRouteResult(handleRouteError(host, error, context), policy, context);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function finishRouteError(host, error, policy, context, cleanup) {
|
|
192
|
+
try {
|
|
193
|
+
return finishWithCleanup(finishHandledRouteError(host, error, policy, context), cleanup);
|
|
194
|
+
} catch (handledError) {
|
|
195
|
+
return cleanupAfterFailure(handledError, cleanup);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function validateController(controller) {
|
|
200
|
+
if (typeof controller !== "function") {
|
|
201
|
+
throw new TypeError("Sloppy controller must be a constructor function.");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function validateControllerAction(action) {
|
|
206
|
+
if (typeof action !== "string" || action.length === 0) {
|
|
207
|
+
throw new TypeError("Sloppy controller action must be a non-empty string.");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function validateMetadataOptions(options) {
|
|
212
|
+
if (options === undefined) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!isPlainObject(options)) {
|
|
217
|
+
throw new TypeError("Sloppy route metadata options must be a plain object.");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return Object.freeze({ ...options });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function validateTag(tag) {
|
|
224
|
+
if (typeof tag !== "string" || tag.length === 0) {
|
|
225
|
+
throw new TypeError("Sloppy route group tags must be non-empty strings.");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function validateName(name, subject) {
|
|
230
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
231
|
+
throw new TypeError(`Sloppy ${subject} name must be a non-empty string.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function validateMetadataText(value, subject) {
|
|
236
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
237
|
+
throw new TypeError(`Sloppy ${subject} must be a non-empty string.`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function cloneFrozenJson(value, subject) {
|
|
242
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
if (typeof value === "number") {
|
|
246
|
+
if (!Number.isFinite(value)) {
|
|
247
|
+
throw new TypeError(`Sloppy ${subject} must be JSON-compatible.`);
|
|
248
|
+
}
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
if (Array.isArray(value)) {
|
|
252
|
+
return Object.freeze(value.map((item) => cloneFrozenJson(item, subject)));
|
|
253
|
+
}
|
|
254
|
+
if (isPlainObject(value)) {
|
|
255
|
+
const out = {};
|
|
256
|
+
for (const [key, current] of Object.entries(value)) {
|
|
257
|
+
out[key] = cloneFrozenJson(current, subject);
|
|
258
|
+
}
|
|
259
|
+
return Object.freeze(out);
|
|
260
|
+
}
|
|
261
|
+
throw new TypeError(`Sloppy ${subject} must be JSON-compatible.`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function validateStatusCode(status) {
|
|
265
|
+
if (!Number.isInteger(status) || status < 100 || status > 599) {
|
|
266
|
+
throw new TypeError("Sloppy route response status must be an integer from 100 to 599.");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function validateMediaType(value, subject) {
|
|
271
|
+
if (typeof value !== "string" || !MEDIA_TYPE_PATTERN.test(value)) {
|
|
272
|
+
throw new TypeError(`Sloppy ${subject} must be an HTTP media type.`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function schemaMetadata(schema, subject) {
|
|
277
|
+
validateSchema(schema, subject);
|
|
278
|
+
return schema.metadata;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function routeParamEntries(pattern) {
|
|
282
|
+
if (pattern === "/") {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
return pattern.split("/").slice(1)
|
|
286
|
+
.map((segment) => ROUTE_PARAM_PATTERN.exec(segment))
|
|
287
|
+
.filter((match) => match !== null)
|
|
288
|
+
.map((match) => Object.freeze({ name: match[1], kind: match[2] ?? "str" }));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function encodeQuery(query) {
|
|
292
|
+
if (query === undefined || query === null) {
|
|
293
|
+
return "";
|
|
294
|
+
}
|
|
295
|
+
if (!isPlainObject(query)) {
|
|
296
|
+
throw new TypeError("Sloppy urlFor query must be a plain object when provided.");
|
|
297
|
+
}
|
|
298
|
+
const pairs = [];
|
|
299
|
+
for (const [key, value] of Object.entries(query)) {
|
|
300
|
+
if (value === undefined || value === null) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const values = Array.isArray(value) ? value : [value];
|
|
304
|
+
for (const item of values) {
|
|
305
|
+
if (item !== undefined && item !== null) {
|
|
306
|
+
pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(item))}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return pairs.length === 0 ? "" : `?${pairs.join("&")}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function routeParamValueSatisfies(kind, value) {
|
|
314
|
+
if (kind === "str") {
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
if (kind === "int") {
|
|
318
|
+
return /^[0-9]+$/u.test(value);
|
|
319
|
+
}
|
|
320
|
+
if (kind === "uuid") {
|
|
321
|
+
return /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/u.test(value);
|
|
322
|
+
}
|
|
323
|
+
if (kind === "alpha") {
|
|
324
|
+
return /^[A-Za-z]+$/u.test(value);
|
|
325
|
+
}
|
|
326
|
+
if (kind === "float") {
|
|
327
|
+
return /^(?:[0-9]+\.[0-9]*|\.[0-9]+)$/u.test(value);
|
|
328
|
+
}
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function buildRouteUrl(pattern, params = {}, query = undefined) {
|
|
333
|
+
if (!isPlainObject(params)) {
|
|
334
|
+
throw new TypeError("Sloppy urlFor params must be a plain object.");
|
|
335
|
+
}
|
|
336
|
+
const used = new Set();
|
|
337
|
+
const path = pattern === "/" ? "/" : pattern.split("/").map((segment, index) => {
|
|
338
|
+
if (index === 0) {
|
|
339
|
+
return "";
|
|
340
|
+
}
|
|
341
|
+
const match = ROUTE_PARAM_PATTERN.exec(segment);
|
|
342
|
+
if (match === null) {
|
|
343
|
+
return segment;
|
|
344
|
+
}
|
|
345
|
+
const name = match[1];
|
|
346
|
+
if (params[name] === undefined || params[name] === null) {
|
|
347
|
+
throw new TypeError(`Sloppy urlFor route parameter '${name}' is required.`);
|
|
348
|
+
}
|
|
349
|
+
const kind = match[2] ?? "str";
|
|
350
|
+
const value = String(params[name]);
|
|
351
|
+
if (!routeParamValueSatisfies(kind, value)) {
|
|
352
|
+
throw new TypeError(`Sloppy urlFor route parameter '${name}' must satisfy '${kind}'.`);
|
|
353
|
+
}
|
|
354
|
+
used.add(name);
|
|
355
|
+
return encodeURIComponent(value);
|
|
356
|
+
}).join("/");
|
|
357
|
+
const extra = Object.keys(params).filter((key) => !used.has(key));
|
|
358
|
+
if (extra.length !== 0) {
|
|
359
|
+
throw new TypeError(`Sloppy urlFor received extra route parameter '${extra[0]}'.`);
|
|
360
|
+
}
|
|
361
|
+
return `${path}${encodeQuery(query)}`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function routeSegments(pattern) {
|
|
365
|
+
return pattern === "/" ? [] : pattern.split("/").slice(1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function routeSegmentRank(segment) {
|
|
369
|
+
const match = ROUTE_PARAM_PATTERN.exec(segment);
|
|
370
|
+
if (match === null) {
|
|
371
|
+
return 3;
|
|
372
|
+
}
|
|
373
|
+
return (match[2] ?? "str") === "str" ? 1 : 2;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function compareRouteSpecificity(left, right) {
|
|
377
|
+
const leftSegments = routeSegments(left.route.pattern);
|
|
378
|
+
const rightSegments = routeSegments(right.route.pattern);
|
|
379
|
+
const shared = Math.min(leftSegments.length, rightSegments.length);
|
|
380
|
+
for (let index = 0; index < shared; index += 1) {
|
|
381
|
+
const leftRank = routeSegmentRank(leftSegments[index]);
|
|
382
|
+
const rightRank = routeSegmentRank(rightSegments[index]);
|
|
383
|
+
if (leftRank !== rightRank) {
|
|
384
|
+
return rightRank - leftRank;
|
|
385
|
+
}
|
|
386
|
+
if (leftRank === 3 && leftSegments[index] !== rightSegments[index]) {
|
|
387
|
+
return leftSegments[index] < rightSegments[index] ? -1 : 1;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (leftSegments.length !== rightSegments.length) {
|
|
391
|
+
return rightSegments.length - leftSegments.length;
|
|
392
|
+
}
|
|
393
|
+
return left.sourceOrder - right.sourceOrder;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function validateSchema(schema, subject) {
|
|
397
|
+
if (!isSchema(schema)) {
|
|
398
|
+
throw new TypeError(`Sloppy ${subject} schema must be a Schema value.`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function validateHeaderToken(value, subject) {
|
|
403
|
+
if (typeof value !== "string" || !HEADER_TOKEN_PATTERN.test(value)) {
|
|
404
|
+
throw new TypeError(`Sloppy CORS ${subject} must be an HTTP token string.`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function normalizeStringList(value, subject, { lower = false } = {}) {
|
|
409
|
+
if (value === undefined) {
|
|
410
|
+
return Object.freeze([]);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const values = Array.isArray(value) ? value : [value];
|
|
414
|
+
const normalized = [];
|
|
415
|
+
|
|
416
|
+
for (const current of values) {
|
|
417
|
+
if (typeof current !== "string" || current.length === 0 || /[\x00-\x1F\x7F]/u.test(current)) {
|
|
418
|
+
throw new TypeError(`Sloppy CORS ${subject} entries must be non-empty strings without control characters.`);
|
|
419
|
+
}
|
|
420
|
+
normalized.push(lower ? current.toLowerCase() : current);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return Object.freeze([...new Set(normalized)]);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function normalizeTokenList(value, subject) {
|
|
427
|
+
const values = normalizeStringList(value, subject);
|
|
428
|
+
for (const current of values) {
|
|
429
|
+
validateHeaderToken(current, subject);
|
|
430
|
+
}
|
|
431
|
+
return values;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function normalizeCorsMethods(value) {
|
|
435
|
+
const methods = normalizeStringList(value, "methods").map((method) => method.toUpperCase());
|
|
436
|
+
|
|
437
|
+
for (const method of methods) {
|
|
438
|
+
if (!PREFLIGHT_METHODS.has(method)) {
|
|
439
|
+
throw new TypeError("Sloppy CORS methods must be supported HTTP methods.");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return Object.freeze([...new Set(methods)]);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function normalizeCorsPolicy(policy) {
|
|
447
|
+
if (!isPlainObject(policy)) {
|
|
448
|
+
throw new TypeError("Sloppy app.useCors policy must be a plain object.");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const origins = normalizeStringList(policy.origins ?? policy.origin, "origins");
|
|
452
|
+
if (origins.length === 0) {
|
|
453
|
+
throw new TypeError("Sloppy CORS origins must include at least one origin or '*'.");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const allowAnyOrigin = origins.includes("*");
|
|
457
|
+
if (allowAnyOrigin && origins.length !== 1) {
|
|
458
|
+
throw new TypeError("Sloppy CORS '*' origin cannot be combined with other origins.");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const credentials = policy.credentials === true;
|
|
462
|
+
if (allowAnyOrigin && credentials) {
|
|
463
|
+
throw new TypeError("Sloppy CORS credentials require explicit origins.");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const maxAgeSeconds = policy.maxAgeSeconds ?? policy.maxAge;
|
|
467
|
+
if (maxAgeSeconds !== undefined && (!Number.isInteger(maxAgeSeconds) || maxAgeSeconds < 0)) {
|
|
468
|
+
throw new TypeError("Sloppy CORS maxAgeSeconds must be a non-negative integer.");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const headers = normalizeTokenList(policy.headers ?? policy.allowHeaders, "headers")
|
|
472
|
+
.map((header) => header.toLowerCase());
|
|
473
|
+
const exposedHeaders = normalizeTokenList(policy.exposedHeaders ?? policy.exposeHeaders, "exposedHeaders");
|
|
474
|
+
|
|
475
|
+
return Object.freeze({
|
|
476
|
+
origins,
|
|
477
|
+
allowAnyOrigin,
|
|
478
|
+
methods: normalizeCorsMethods(policy.methods),
|
|
479
|
+
headers: Object.freeze([...new Set(headers)]),
|
|
480
|
+
exposedHeaders,
|
|
481
|
+
credentials,
|
|
482
|
+
maxAgeSeconds,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function snapshotCorsPolicy(policy) {
|
|
487
|
+
if (policy === null) {
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return Object.freeze({
|
|
492
|
+
origins: policy.origins,
|
|
493
|
+
methods: policy.methods,
|
|
494
|
+
headers: policy.headers,
|
|
495
|
+
exposedHeaders: policy.exposedHeaders,
|
|
496
|
+
credentials: policy.credentials,
|
|
497
|
+
maxAgeSeconds: policy.maxAgeSeconds,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function getRequestHeader(context, name) {
|
|
502
|
+
const headers = context?.request?.headers;
|
|
503
|
+
if (headers === undefined || headers === null) {
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (typeof headers.get === "function") {
|
|
508
|
+
return headers.get(name) ?? headers.get(name.toLowerCase()) ?? undefined;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (isPlainObject(headers)) {
|
|
512
|
+
const lower = name.toLowerCase();
|
|
513
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
514
|
+
if (key.toLowerCase() === lower) {
|
|
515
|
+
return value;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return undefined;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function allowedOrigin(policy, origin) {
|
|
524
|
+
if (typeof origin !== "string" || origin.length === 0) {
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (policy.allowAnyOrigin) {
|
|
529
|
+
return "*";
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return policy.origins.includes(origin) ? origin : undefined;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function mergeVary(existing, value) {
|
|
536
|
+
if (existing === undefined || existing.length === 0) {
|
|
537
|
+
return value;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const tokens = existing.split(",").map((token) => token.trim().toLowerCase());
|
|
541
|
+
return tokens.includes(value.toLowerCase()) ? existing : `${existing}, ${value}`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function normalizeRouteStringList(value, subject, { lower = false, allowAll = false } = {}) {
|
|
545
|
+
if (value === undefined) {
|
|
546
|
+
return Object.freeze([]);
|
|
547
|
+
}
|
|
548
|
+
if (allowAll && value === "all") {
|
|
549
|
+
return "all";
|
|
550
|
+
}
|
|
551
|
+
if (!Array.isArray(value)) {
|
|
552
|
+
throw new TypeError(`Sloppy ${subject} must be an array${allowAll ? " or 'all'" : ""}.`);
|
|
553
|
+
}
|
|
554
|
+
const output = [];
|
|
555
|
+
for (const item of value) {
|
|
556
|
+
if (typeof item !== "string" || item.length === 0 || /[\x00-\x1F\x7F]/u.test(item)) {
|
|
557
|
+
throw new TypeError(`Sloppy ${subject} entries must be non-empty strings without control characters.`);
|
|
558
|
+
}
|
|
559
|
+
output.push(lower ? item.toLowerCase() : item);
|
|
560
|
+
}
|
|
561
|
+
return Object.freeze([...new Set(output)]);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function normalizeOutputCacheOptions(options) {
|
|
565
|
+
if (!isPlainObject(options)) {
|
|
566
|
+
throw new TypeError("Sloppy outputCache options must be a plain object.");
|
|
567
|
+
}
|
|
568
|
+
if (!Number.isInteger(options.ttlMs) || options.ttlMs < 1 || options.ttlMs > 0x7fffffff) {
|
|
569
|
+
throw new TypeError("Sloppy outputCache ttlMs must be an integer from 1 to 2147483647.");
|
|
570
|
+
}
|
|
571
|
+
const statusCodes = options.statusCodes === undefined ? DEFAULT_OUTPUT_CACHE_STATUS_CODES : options.statusCodes;
|
|
572
|
+
if (!Array.isArray(statusCodes) || statusCodes.length === 0 ||
|
|
573
|
+
!statusCodes.every((status) => Number.isInteger(status) && status >= 100 && status <= 599))
|
|
574
|
+
{
|
|
575
|
+
throw new TypeError("Sloppy outputCache statusCodes must be a non-empty array of HTTP status codes.");
|
|
576
|
+
}
|
|
577
|
+
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_OUTPUT_CACHE_MAX_BODY_BYTES;
|
|
578
|
+
if (!Number.isInteger(maxBodyBytes) || maxBodyBytes < 0) {
|
|
579
|
+
throw new TypeError("Sloppy outputCache maxBodyBytes must be a non-negative integer.");
|
|
580
|
+
}
|
|
581
|
+
const varyByClaim = normalizeRouteStringList(options.varyByClaim, "outputCache varyByClaim");
|
|
582
|
+
const sharedAuthenticated = options.allowSharedAuthenticated === true;
|
|
583
|
+
if (options.allowAuthenticated === true && options.varyByUser !== true) {
|
|
584
|
+
if (!sharedAuthenticated || (options.varyByRole !== true && varyByClaim.length === 0)) {
|
|
585
|
+
throw new TypeError("Sloppy outputCache allowAuthenticated requires varyByUser; shared role/claim caching requires allowSharedAuthenticated.");
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (options.tags !== undefined && typeof options.tags !== "function" && !Array.isArray(options.tags)) {
|
|
589
|
+
throw new TypeError("Sloppy outputCache tags must be an array or function.");
|
|
590
|
+
}
|
|
591
|
+
return Object.freeze({
|
|
592
|
+
ttlMs: options.ttlMs,
|
|
593
|
+
cacheName: options.cacheName ?? "default",
|
|
594
|
+
varyByQuery: normalizeRouteStringList(options.varyByQuery, "outputCache varyByQuery", { allowAll: true }),
|
|
595
|
+
varyByHeader: normalizeRouteStringList(options.varyByHeader, "outputCache varyByHeader", { lower: true }),
|
|
596
|
+
varyByRouteParams: normalizeRouteStringList(options.varyByRouteParams, "outputCache varyByRouteParams"),
|
|
597
|
+
varyByUser: options.varyByUser === true,
|
|
598
|
+
varyByClaim,
|
|
599
|
+
varyByRole: options.varyByRole === true,
|
|
600
|
+
tags: options.tags,
|
|
601
|
+
statusCodes: Object.freeze([...new Set(statusCodes)]),
|
|
602
|
+
maxBodyBytes,
|
|
603
|
+
allowSetCookie: options.allowSetCookie === true,
|
|
604
|
+
allowSharedAuthenticated: sharedAuthenticated,
|
|
605
|
+
allowAuthenticated: options.varyByUser === true || (sharedAuthenticated && (options.varyByRole === true || varyByClaim.length !== 0)),
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function normalizeCacheHeaderOptions(options) {
|
|
610
|
+
if (!isPlainObject(options)) {
|
|
611
|
+
throw new TypeError("Sloppy cacheHeaders options must be a plain object.");
|
|
612
|
+
}
|
|
613
|
+
if (options.cacheControl !== undefined && (typeof options.cacheControl !== "string" || options.cacheControl.length === 0)) {
|
|
614
|
+
throw new TypeError("Sloppy cacheHeaders cacheControl must be a non-empty string.");
|
|
615
|
+
}
|
|
616
|
+
if (options.vary !== undefined && !Array.isArray(options.vary)) {
|
|
617
|
+
throw new TypeError("Sloppy cacheHeaders vary must be an array.");
|
|
618
|
+
}
|
|
619
|
+
return Object.freeze({
|
|
620
|
+
cacheControl: options.cacheControl,
|
|
621
|
+
vary: options.vary === undefined ? Object.freeze([]) : normalizeRouteStringList(options.vary, "cacheHeaders vary"),
|
|
622
|
+
etag: options.etag === true,
|
|
623
|
+
lastModified: options.lastModified,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function appendResponseHeaders(result, headers) {
|
|
628
|
+
if (result === null || typeof result !== "object") {
|
|
629
|
+
return result;
|
|
630
|
+
}
|
|
631
|
+
return Object.freeze({
|
|
632
|
+
...result,
|
|
633
|
+
headers: Object.freeze({
|
|
634
|
+
...(isPlainObject(result.headers) ? result.headers : {}),
|
|
635
|
+
...headers,
|
|
636
|
+
}),
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function resultBodySize(result) {
|
|
641
|
+
if (result?.body === undefined) {
|
|
642
|
+
return 0;
|
|
643
|
+
}
|
|
644
|
+
if (typeof result.body === "string") {
|
|
645
|
+
return Text.utf8.encode(result.body).byteLength;
|
|
646
|
+
}
|
|
647
|
+
if (result.body instanceof Uint8Array) {
|
|
648
|
+
return result.body.byteLength;
|
|
649
|
+
}
|
|
650
|
+
const serialized = JSON.stringify(result.body);
|
|
651
|
+
return serialized === undefined ? 0 : Text.utf8.encode(serialized).byteLength;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function containsNonJsonValue(value, seen = new Set()) {
|
|
655
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
if (typeof value === "function" || typeof value === "symbol" || typeof value === "undefined" || typeof value === "bigint") {
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
if (typeof value !== "object") {
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
if (seen.has(value)) {
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
seen.add(value);
|
|
668
|
+
if (Array.isArray(value)) {
|
|
669
|
+
return value.some((item) => containsNonJsonValue(item, seen));
|
|
670
|
+
}
|
|
671
|
+
if (!isPlainObject(value)) {
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
return Object.values(value).some((item) => containsNonJsonValue(item, seen));
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function outputCacheUnsupportedResultReason(result) {
|
|
678
|
+
if (result?.__sloppyResult !== true) {
|
|
679
|
+
return "unsupported-result";
|
|
680
|
+
}
|
|
681
|
+
if (result.kind === "stream") {
|
|
682
|
+
return "streaming";
|
|
683
|
+
}
|
|
684
|
+
if (result.kind === "json") {
|
|
685
|
+
return containsNonJsonValue(result.body) ? "unsupported-body" : undefined;
|
|
686
|
+
}
|
|
687
|
+
if (result.kind === "text") {
|
|
688
|
+
return typeof result.body === "string" ? undefined : "unsupported-body";
|
|
689
|
+
}
|
|
690
|
+
if (result.kind === "bytes") {
|
|
691
|
+
return result.body instanceof Uint8Array ? undefined : "unsupported-body";
|
|
692
|
+
}
|
|
693
|
+
if (result.kind === "empty") {
|
|
694
|
+
return result.body === undefined ? undefined : "unsupported-body";
|
|
695
|
+
}
|
|
696
|
+
return "unsupported-result-kind";
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function outputCacheBypassReason(result, options, context, routeInfo) {
|
|
700
|
+
const method = context.request?.method ?? routeInfo.method;
|
|
701
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
702
|
+
return "method";
|
|
703
|
+
}
|
|
704
|
+
if (routeInfo.auth !== undefined && options.allowAuthenticated !== true) {
|
|
705
|
+
return "auth-unsafe";
|
|
706
|
+
}
|
|
707
|
+
const unsupported = outputCacheUnsupportedResultReason(result);
|
|
708
|
+
if (unsupported !== undefined) {
|
|
709
|
+
return unsupported;
|
|
710
|
+
}
|
|
711
|
+
if (!options.statusCodes.includes(result.status)) {
|
|
712
|
+
return "status";
|
|
713
|
+
}
|
|
714
|
+
if (Array.isArray(result.setCookies) && result.setCookies.length !== 0 && options.allowSetCookie !== true) {
|
|
715
|
+
return "set-cookie";
|
|
716
|
+
}
|
|
717
|
+
if (resultBodySize(result) > options.maxBodyBytes) {
|
|
718
|
+
return "body-too-large";
|
|
719
|
+
}
|
|
720
|
+
return undefined;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function outputCachePartition(options, context) {
|
|
724
|
+
const parts = [];
|
|
725
|
+
if (options.varyByUser) {
|
|
726
|
+
const sub = context.user?.sub ?? context.user?.id;
|
|
727
|
+
if (typeof sub !== "string" || sub.length === 0) {
|
|
728
|
+
throw new Error("Sloppy outputCache varyByUser requires an authenticated user subject.");
|
|
729
|
+
}
|
|
730
|
+
parts.push(["user", stableHash(sub)]);
|
|
731
|
+
}
|
|
732
|
+
if (options.varyByRole) {
|
|
733
|
+
const roles = Array.isArray(context.user?.roles) ? context.user.roles : [];
|
|
734
|
+
parts.push(["roles", roles.map(String).sort().join(",")]);
|
|
735
|
+
}
|
|
736
|
+
for (const claim of options.varyByClaim) {
|
|
737
|
+
const value = typeof context.user?.claim === "function" ? context.user.claim(claim) : context.user?.claims?.[claim];
|
|
738
|
+
parts.push([`claim:${claim}`, stableHash(String(value ?? ""))]);
|
|
739
|
+
}
|
|
740
|
+
return parts;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function outputCacheKey(options, context, routeInfo) {
|
|
744
|
+
const query = context.query ?? {};
|
|
745
|
+
const queryParts = options.varyByQuery === "all"
|
|
746
|
+
? Object.entries(query).sort(([left], [right]) => left.localeCompare(right))
|
|
747
|
+
: options.varyByQuery.map((name) => [name, query[name] ?? ""]);
|
|
748
|
+
const headerParts = options.varyByHeader.map((name) => [name, getRequestHeader(context, name) ?? ""]);
|
|
749
|
+
const routeParts = options.varyByRouteParams.map((name) => [name, context.route?.[name] ?? ""]);
|
|
750
|
+
return `output:${stableHash(JSON.stringify({
|
|
751
|
+
method: context.request?.method ?? routeInfo.method,
|
|
752
|
+
route: routeInfo.pattern,
|
|
753
|
+
query: queryParts,
|
|
754
|
+
headers: headerParts,
|
|
755
|
+
params: routeParts,
|
|
756
|
+
partition: outputCachePartition(options, context),
|
|
757
|
+
}))}`;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function outputCacheTags(options, context, routeInfo) {
|
|
761
|
+
const source = typeof options.tags === "function" ? options.tags(context) : options.tags;
|
|
762
|
+
const tags = Array.isArray(source) ? source : [];
|
|
763
|
+
return Object.freeze([`route:${routeInfo.pattern}`, ...tags]);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function recordOutputCacheDiagnostic(context, code, fields = {}) {
|
|
767
|
+
context.diagnostics?.record?.({
|
|
768
|
+
code,
|
|
769
|
+
subsystem: "output-cache",
|
|
770
|
+
severity: code.endsWith("BYPASS") ? "debug" : "info",
|
|
771
|
+
fields,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function recordOutputCacheMetric(context, labels) {
|
|
776
|
+
const metrics = context.metrics;
|
|
777
|
+
if (metrics === undefined || metrics === null) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const safeLabels = Object.freeze({
|
|
781
|
+
route: labels.route,
|
|
782
|
+
outcome: labels.outcome,
|
|
783
|
+
reason: labels.reason ?? "",
|
|
784
|
+
statusClass: labels.statusClass ?? "",
|
|
785
|
+
});
|
|
786
|
+
try {
|
|
787
|
+
if (typeof metrics.increment === "function") {
|
|
788
|
+
metrics.increment("output_cache.requests.total", safeLabels);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
metrics.counter?.("output_cache.requests.total", {
|
|
792
|
+
description: "Output cache requests by route pattern and outcome.",
|
|
793
|
+
})?.inc(safeLabels);
|
|
794
|
+
} catch {
|
|
795
|
+
// Metrics must not change route behavior.
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async function invokeWithOutputCache(context, routeInfo, outputCache, handler) {
|
|
800
|
+
const cache = context.services?.tryGet?.(Cache.token(outputCache.cacheName));
|
|
801
|
+
if (!isCache(cache)) {
|
|
802
|
+
throw new Error(`Sloppy outputCache cache '${outputCache.cacheName}' is not registered.`);
|
|
803
|
+
}
|
|
804
|
+
if (routeInfo.auth !== undefined && outputCache.allowAuthenticated !== true) {
|
|
805
|
+
recordOutputCacheDiagnostic(context, "SLOPPY_OUTPUT_CACHE_BYPASS", { reason: "auth-unsafe", route: routeInfo.pattern });
|
|
806
|
+
return appendResponseHeaders(await handler(), { "X-Sloppy-Output-Cache": "BYPASS" });
|
|
807
|
+
}
|
|
808
|
+
const key = outputCacheKey(outputCache, context, routeInfo);
|
|
809
|
+
const cached = await cache.get(key);
|
|
810
|
+
if (cached !== undefined) {
|
|
811
|
+
recordOutputCacheMetric(context, { route: routeInfo.pattern, outcome: "hit", statusClass: `${Math.trunc(cached.status / 100)}xx` });
|
|
812
|
+
recordOutputCacheDiagnostic(context, "SLOPPY_OUTPUT_CACHE_HIT", { route: routeInfo.pattern, keyHash: stableHash(key) });
|
|
813
|
+
return appendResponseHeaders(cached, { "X-Sloppy-Output-Cache": "HIT" });
|
|
814
|
+
}
|
|
815
|
+
const result = await handler();
|
|
816
|
+
const reason = outputCacheBypassReason(result, outputCache, context, routeInfo);
|
|
817
|
+
if (reason !== undefined) {
|
|
818
|
+
recordOutputCacheMetric(context, { route: routeInfo.pattern, outcome: "bypass", reason, statusClass: result?.status === undefined ? "" : `${Math.trunc(result.status / 100)}xx` });
|
|
819
|
+
recordOutputCacheDiagnostic(context, "SLOPPY_OUTPUT_CACHE_BYPASS", { route: routeInfo.pattern, reason });
|
|
820
|
+
return appendResponseHeaders(result, { "X-Sloppy-Output-Cache": "BYPASS" });
|
|
821
|
+
}
|
|
822
|
+
await cache.set(key, result, {
|
|
823
|
+
ttlMs: outputCache.ttlMs,
|
|
824
|
+
tags: outputCacheTags(outputCache, context, routeInfo),
|
|
825
|
+
});
|
|
826
|
+
recordOutputCacheMetric(context, { route: routeInfo.pattern, outcome: "miss", statusClass: `${Math.trunc(result.status / 100)}xx` });
|
|
827
|
+
recordOutputCacheDiagnostic(context, "SLOPPY_OUTPUT_CACHE_MISS", { route: routeInfo.pattern, keyHash: stableHash(key) });
|
|
828
|
+
return appendResponseHeaders(result, { "X-Sloppy-Output-Cache": "MISS" });
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function applyCacheHeaders(result, options) {
|
|
832
|
+
const headers = {};
|
|
833
|
+
if (options.cacheControl !== undefined) {
|
|
834
|
+
headers["Cache-Control"] = options.cacheControl;
|
|
835
|
+
}
|
|
836
|
+
for (const value of options.vary) {
|
|
837
|
+
const current = headers.Vary ?? result?.headers?.Vary ?? result?.headers?.vary;
|
|
838
|
+
headers.Vary = mergeVary(current, value);
|
|
839
|
+
}
|
|
840
|
+
if (options.lastModified !== undefined) {
|
|
841
|
+
const date = options.lastModified instanceof Date ? options.lastModified : new Date(options.lastModified);
|
|
842
|
+
if (Number.isFinite(date.getTime())) {
|
|
843
|
+
headers["Last-Modified"] = date.toUTCString();
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (options.etag) {
|
|
847
|
+
headers.ETag = `"${stableHash(JSON.stringify(result?.body ?? ""))}"`;
|
|
848
|
+
}
|
|
849
|
+
return appendResponseHeaders(result, headers);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function appendCorsHeaders(result, policy, context) {
|
|
853
|
+
if (policy === null) {
|
|
854
|
+
return result;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const origin = getRequestHeader(context, "origin");
|
|
858
|
+
const allowed = allowedOrigin(policy, origin);
|
|
859
|
+
if (allowed === undefined) {
|
|
860
|
+
return result;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const headers = {
|
|
864
|
+
...(isPlainObject(result?.headers) ? result.headers : {}),
|
|
865
|
+
"Access-Control-Allow-Origin": allowed,
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
if (!policy.allowAnyOrigin) {
|
|
869
|
+
headers.Vary = mergeVary(headers.Vary, "Origin");
|
|
870
|
+
}
|
|
871
|
+
if (policy.credentials) {
|
|
872
|
+
headers["Access-Control-Allow-Credentials"] = "true";
|
|
873
|
+
}
|
|
874
|
+
if (policy.exposedHeaders.length !== 0) {
|
|
875
|
+
headers["Access-Control-Expose-Headers"] = policy.exposedHeaders.join(", ");
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return Object.freeze({
|
|
879
|
+
...result,
|
|
880
|
+
headers: Object.freeze(headers),
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function finishWithCors(result, policy, context) {
|
|
885
|
+
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
886
|
+
return Promise.resolve(result).then((value) => appendCorsHeaders(value, policy, context));
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return appendCorsHeaders(result, policy, context);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function requestedHeadersAllowed(policy, requestedHeaders) {
|
|
893
|
+
if (typeof requestedHeaders !== "string" || requestedHeaders.length === 0) {
|
|
894
|
+
return true;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const requested = requestedHeaders
|
|
898
|
+
.split(",")
|
|
899
|
+
.map((header) => header.trim().toLowerCase())
|
|
900
|
+
.filter((header) => header.length !== 0);
|
|
901
|
+
|
|
902
|
+
return requested.every((header) => policy.headers.includes(header));
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function createCorsPreflightHandler(state) {
|
|
906
|
+
return function corsPreflightHandler(context) {
|
|
907
|
+
const origin = getRequestHeader(context, "origin");
|
|
908
|
+
const allowed = allowedOrigin(state.policy, origin);
|
|
909
|
+
const requestedMethod = getRequestHeader(context, "access-control-request-method")?.toUpperCase();
|
|
910
|
+
const requestedHeaders = getRequestHeader(context, "access-control-request-headers");
|
|
911
|
+
const methods = state.policy.methods.length === 0 ? Array.from(state.methods) : state.policy.methods;
|
|
912
|
+
|
|
913
|
+
if (
|
|
914
|
+
allowed === undefined ||
|
|
915
|
+
!methods.includes(requestedMethod) ||
|
|
916
|
+
!requestedHeadersAllowed(state.policy, requestedHeaders)
|
|
917
|
+
) {
|
|
918
|
+
return Results.status(403);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const headers = {
|
|
922
|
+
"Access-Control-Allow-Origin": allowed,
|
|
923
|
+
"Access-Control-Allow-Methods": methods.join(", "),
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
if (!state.policy.allowAnyOrigin) {
|
|
927
|
+
headers.Vary = "Origin, Access-Control-Request-Method, Access-Control-Request-Headers";
|
|
928
|
+
}
|
|
929
|
+
if (state.policy.credentials) {
|
|
930
|
+
headers["Access-Control-Allow-Credentials"] = "true";
|
|
931
|
+
}
|
|
932
|
+
if (state.policy.headers.length !== 0) {
|
|
933
|
+
headers["Access-Control-Allow-Headers"] = state.policy.headers.join(", ");
|
|
934
|
+
}
|
|
935
|
+
if (state.policy.maxAgeSeconds !== undefined) {
|
|
936
|
+
headers["Access-Control-Max-Age"] = String(state.policy.maxAgeSeconds);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return Results.status(204, undefined, { headers });
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
const EMPTY_HEADERS = Object.freeze({
|
|
947
|
+
get() {
|
|
948
|
+
return undefined;
|
|
949
|
+
},
|
|
950
|
+
entries() {
|
|
951
|
+
return Object.freeze([]);
|
|
952
|
+
},
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
function createDefaultRequest(routeInfo) {
|
|
956
|
+
return Object.freeze({
|
|
957
|
+
method: routeInfo.method,
|
|
958
|
+
path: routeInfo.pattern,
|
|
959
|
+
rawTarget: routeInfo.pattern,
|
|
960
|
+
headers: EMPTY_HEADERS,
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function attachHostMarker(context, host) {
|
|
965
|
+
Object.defineProperty(context, "__sloppyHost", {
|
|
966
|
+
value: host,
|
|
967
|
+
});
|
|
968
|
+
return context;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function stringPolicyName(policy) {
|
|
972
|
+
if (typeof policy !== "string" || policy.length === 0) {
|
|
973
|
+
throw new TypeError("Sloppy ctx.authorize policy must be a non-empty string.");
|
|
974
|
+
}
|
|
975
|
+
return policy;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function attachAuthHelpers(context) {
|
|
979
|
+
if (context.auth !== undefined) {
|
|
980
|
+
return context;
|
|
981
|
+
}
|
|
982
|
+
const helpers = Object.freeze({
|
|
983
|
+
get user() {
|
|
984
|
+
return context.user;
|
|
985
|
+
},
|
|
986
|
+
requireUser() {
|
|
987
|
+
if (context.user?.authenticated !== true) {
|
|
988
|
+
throw new Error("SLOPPY_E_AUTH_MISSING_CREDENTIALS: authenticated user is required.");
|
|
989
|
+
}
|
|
990
|
+
return context.user;
|
|
991
|
+
},
|
|
992
|
+
hasScope(scope) {
|
|
993
|
+
return context.user?.hasScope(scope) === true;
|
|
994
|
+
},
|
|
995
|
+
hasRole(role) {
|
|
996
|
+
return context.user?.hasRole(role) === true;
|
|
997
|
+
},
|
|
998
|
+
hasClaim(name, value = undefined) {
|
|
999
|
+
return context.user?.hasClaim(name, value) === true;
|
|
1000
|
+
},
|
|
1001
|
+
async authorize(policy, resource = undefined) {
|
|
1002
|
+
const name = stringPolicyName(policy);
|
|
1003
|
+
const denied = authorizePolicy(context.__sloppyHost?.auth, name, context.user, context, resource);
|
|
1004
|
+
const result = denied !== null && typeof denied === "object" && typeof denied.then === "function"
|
|
1005
|
+
? await denied
|
|
1006
|
+
: denied;
|
|
1007
|
+
return result === undefined;
|
|
1008
|
+
},
|
|
1009
|
+
});
|
|
1010
|
+
Object.defineProperties(context, {
|
|
1011
|
+
auth: {
|
|
1012
|
+
value: helpers,
|
|
1013
|
+
enumerable: true,
|
|
1014
|
+
configurable: true,
|
|
1015
|
+
},
|
|
1016
|
+
claims: {
|
|
1017
|
+
get() {
|
|
1018
|
+
return context.user?.claims ?? {};
|
|
1019
|
+
},
|
|
1020
|
+
enumerable: true,
|
|
1021
|
+
configurable: true,
|
|
1022
|
+
},
|
|
1023
|
+
requireUser: {
|
|
1024
|
+
value: helpers.requireUser,
|
|
1025
|
+
enumerable: true,
|
|
1026
|
+
configurable: true,
|
|
1027
|
+
},
|
|
1028
|
+
hasScope: {
|
|
1029
|
+
value: helpers.hasScope,
|
|
1030
|
+
enumerable: true,
|
|
1031
|
+
configurable: true,
|
|
1032
|
+
},
|
|
1033
|
+
hasRole: {
|
|
1034
|
+
value: helpers.hasRole,
|
|
1035
|
+
enumerable: true,
|
|
1036
|
+
configurable: true,
|
|
1037
|
+
},
|
|
1038
|
+
hasClaim: {
|
|
1039
|
+
value: helpers.hasClaim,
|
|
1040
|
+
enumerable: true,
|
|
1041
|
+
configurable: true,
|
|
1042
|
+
},
|
|
1043
|
+
authorize: {
|
|
1044
|
+
value: helpers.authorize,
|
|
1045
|
+
enumerable: true,
|
|
1046
|
+
configurable: true,
|
|
1047
|
+
},
|
|
1048
|
+
});
|
|
1049
|
+
return context;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function createHandlerContext(host, routeInfo) {
|
|
1053
|
+
return attachAuthHelpers(attachHostMarker({
|
|
1054
|
+
services: host.services.createScope(),
|
|
1055
|
+
capabilities: host.capabilities,
|
|
1056
|
+
config: host.config,
|
|
1057
|
+
log: host.log,
|
|
1058
|
+
get webhooks() {
|
|
1059
|
+
const services = this.services;
|
|
1060
|
+
return services !== undefined && services !== null && typeof services.tryGet === "function"
|
|
1061
|
+
? services.tryGet("webhooks")
|
|
1062
|
+
: undefined;
|
|
1063
|
+
},
|
|
1064
|
+
user: anonymousUser(),
|
|
1065
|
+
route: {},
|
|
1066
|
+
routeName: routeInfo.name ?? "",
|
|
1067
|
+
routePattern: routeInfo.pattern,
|
|
1068
|
+
urlFor: routeInfo.urlFor,
|
|
1069
|
+
request: createDefaultRequest(routeInfo),
|
|
1070
|
+
}, host));
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function decorateProvidedContext(host, context, routeInfo) {
|
|
1074
|
+
const nextContext = {
|
|
1075
|
+
...context,
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
nextContext.config ??= host.config;
|
|
1079
|
+
nextContext.log ??= host.log;
|
|
1080
|
+
nextContext.capabilities ??= host.capabilities;
|
|
1081
|
+
nextContext.user ??= anonymousUser();
|
|
1082
|
+
attachAuthHelpers(attachHostMarker(nextContext, host));
|
|
1083
|
+
|
|
1084
|
+
if (nextContext.webhooks === undefined) {
|
|
1085
|
+
Object.defineProperty(nextContext, "webhooks", {
|
|
1086
|
+
enumerable: true,
|
|
1087
|
+
configurable: true,
|
|
1088
|
+
get() {
|
|
1089
|
+
const services = nextContext.services;
|
|
1090
|
+
return services !== undefined && services !== null && typeof services.tryGet === "function"
|
|
1091
|
+
? services.tryGet("webhooks")
|
|
1092
|
+
: undefined;
|
|
1093
|
+
},
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (nextContext.route === undefined || nextContext.route === null) {
|
|
1098
|
+
nextContext.route = {};
|
|
1099
|
+
}
|
|
1100
|
+
if (nextContext.routeName === undefined) {
|
|
1101
|
+
nextContext.routeName = routeInfo.name ?? "";
|
|
1102
|
+
}
|
|
1103
|
+
if (nextContext.routePattern === undefined) {
|
|
1104
|
+
nextContext.routePattern = routeInfo.pattern;
|
|
1105
|
+
}
|
|
1106
|
+
if (nextContext.urlFor === undefined) {
|
|
1107
|
+
nextContext.urlFor = routeInfo.urlFor;
|
|
1108
|
+
}
|
|
1109
|
+
if (nextContext.request === undefined || nextContext.request === null) {
|
|
1110
|
+
nextContext.request = createDefaultRequest(routeInfo);
|
|
1111
|
+
} else {
|
|
1112
|
+
nextContext.request = Object.freeze({
|
|
1113
|
+
...nextContext.request,
|
|
1114
|
+
method: nextContext.request.method ?? routeInfo.method,
|
|
1115
|
+
path: nextContext.request.path ?? routeInfo.pattern,
|
|
1116
|
+
rawTarget: nextContext.request.rawTarget ?? nextContext.request.target ??
|
|
1117
|
+
nextContext.request.path ?? routeInfo.pattern,
|
|
1118
|
+
headers: nextContext.request.headers ?? EMPTY_HEADERS,
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
return nextContext;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function createRouteHandler(host, handler, middleware = [], corsPolicy = null, routeInfo) {
|
|
1126
|
+
async function applyRateLimitPolicies(ctx) {
|
|
1127
|
+
const policies = routeInfo.rateLimits ?? Object.freeze([]);
|
|
1128
|
+
const leases = [];
|
|
1129
|
+
try {
|
|
1130
|
+
for (const policy of policies) {
|
|
1131
|
+
const result = await enforceRateLimit(ctx, policy);
|
|
1132
|
+
if (result.release !== undefined) {
|
|
1133
|
+
leases.push(result.release);
|
|
1134
|
+
}
|
|
1135
|
+
if (result.allowed !== true) {
|
|
1136
|
+
for (const release of leases.reverse()) {
|
|
1137
|
+
release();
|
|
1138
|
+
}
|
|
1139
|
+
return result.response;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
for (const release of leases.reverse()) {
|
|
1144
|
+
release();
|
|
1145
|
+
}
|
|
1146
|
+
throw error;
|
|
1147
|
+
}
|
|
1148
|
+
if (leases.length === 0) {
|
|
1149
|
+
return undefined;
|
|
1150
|
+
}
|
|
1151
|
+
return () => {
|
|
1152
|
+
for (const release of leases.reverse()) {
|
|
1153
|
+
release();
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function invokeHandler(ctx) {
|
|
1159
|
+
const denied = authorizeRoute(ctx, routeInfo.auth, host.auth);
|
|
1160
|
+
if (denied !== null && typeof denied === "object" && typeof denied.then === "function") {
|
|
1161
|
+
return Promise.resolve(denied).then((resolved) => {
|
|
1162
|
+
if (resolved !== undefined) {
|
|
1163
|
+
return resolved;
|
|
1164
|
+
}
|
|
1165
|
+
return invokeRateLimitedHandler(ctx);
|
|
1166
|
+
});
|
|
1167
|
+
} else if (denied !== undefined) {
|
|
1168
|
+
return denied;
|
|
1169
|
+
}
|
|
1170
|
+
return invokeRateLimitedHandler(ctx);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function invokeRouteOutput(ctx) {
|
|
1174
|
+
const output = routeInfo.outputCache === undefined
|
|
1175
|
+
? handler(ctx)
|
|
1176
|
+
: invokeWithOutputCache(ctx, routeInfo, routeInfo.outputCache, () => handler(ctx));
|
|
1177
|
+
if (routeInfo.cacheHeaders === undefined) {
|
|
1178
|
+
return output;
|
|
1179
|
+
}
|
|
1180
|
+
return Promise.resolve(output).then((result) => applyCacheHeaders(result, routeInfo.cacheHeaders));
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function invokeRateLimitedHandler(ctx) {
|
|
1184
|
+
if ((routeInfo.rateLimits ?? Object.freeze([])).length === 0) {
|
|
1185
|
+
return invokeRouteOutput(ctx);
|
|
1186
|
+
}
|
|
1187
|
+
return invokeRateLimitedHandlerAsync(ctx);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
async function invokeRateLimitedHandlerAsync(ctx) {
|
|
1191
|
+
const rateLimitRelease = await applyRateLimitPolicies(ctx);
|
|
1192
|
+
if (typeof rateLimitRelease !== "function") {
|
|
1193
|
+
if (rateLimitRelease !== undefined) {
|
|
1194
|
+
return rateLimitRelease;
|
|
1195
|
+
}
|
|
1196
|
+
return invokeRouteOutput(ctx);
|
|
1197
|
+
}
|
|
1198
|
+
try {
|
|
1199
|
+
const result = invokeRouteOutput(ctx);
|
|
1200
|
+
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
1201
|
+
return Promise.resolve(result).finally(rateLimitRelease);
|
|
1202
|
+
}
|
|
1203
|
+
rateLimitRelease();
|
|
1204
|
+
return result;
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
rateLimitRelease();
|
|
1207
|
+
throw error;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function routeHandler(context) {
|
|
1212
|
+
if (context !== undefined && context !== null) {
|
|
1213
|
+
const providedContext = decorateProvidedContext(host, context, routeInfo);
|
|
1214
|
+
try {
|
|
1215
|
+
const result = invokeMiddlewarePipeline(
|
|
1216
|
+
providedContext,
|
|
1217
|
+
middleware,
|
|
1218
|
+
() => invokeHandler(providedContext),
|
|
1219
|
+
);
|
|
1220
|
+
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
1221
|
+
return Promise.resolve(result).then(
|
|
1222
|
+
(value) => finishRouteResult(value, corsPolicy, providedContext),
|
|
1223
|
+
(error) => finishHandledRouteError(host, error, corsPolicy, providedContext),
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
return finishRouteResult(result, corsPolicy, providedContext);
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
return finishHandledRouteError(host, error, corsPolicy, providedContext);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const ownedContext = createHandlerContext(host, routeInfo);
|
|
1233
|
+
try {
|
|
1234
|
+
const result = invokeMiddlewarePipeline(
|
|
1235
|
+
ownedContext,
|
|
1236
|
+
middleware,
|
|
1237
|
+
() => invokeHandler(ownedContext),
|
|
1238
|
+
);
|
|
1239
|
+
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
1240
|
+
return Promise.resolve(result).then(
|
|
1241
|
+
(value) => finishWithCleanup(
|
|
1242
|
+
finishRouteResult(value, corsPolicy, ownedContext),
|
|
1243
|
+
() => ownedContext.services.dispose(),
|
|
1244
|
+
),
|
|
1245
|
+
(error) => finishRouteError(
|
|
1246
|
+
host,
|
|
1247
|
+
error,
|
|
1248
|
+
corsPolicy,
|
|
1249
|
+
ownedContext,
|
|
1250
|
+
() => ownedContext.services.dispose(),
|
|
1251
|
+
),
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
return finishWithCleanup(
|
|
1255
|
+
finishRouteResult(result, corsPolicy, ownedContext),
|
|
1256
|
+
() => ownedContext.services.dispose(),
|
|
1257
|
+
);
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
return finishRouteError(
|
|
1260
|
+
host,
|
|
1261
|
+
error,
|
|
1262
|
+
corsPolicy,
|
|
1263
|
+
ownedContext,
|
|
1264
|
+
() => ownedContext.services.dispose(),
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const websocketOptions = webSocketRouteOptions(handler);
|
|
1269
|
+
if (websocketOptions !== undefined) {
|
|
1270
|
+
Object.defineProperty(routeHandler, Symbol.for("sloppy.websocket.routeOptions"), {
|
|
1271
|
+
value: websocketOptions,
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
return routeHandler;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function snapshotRoute(route) {
|
|
1278
|
+
return Object.freeze({
|
|
1279
|
+
method: route.method,
|
|
1280
|
+
kind: route.kind,
|
|
1281
|
+
pattern: route.pattern,
|
|
1282
|
+
handler: route.handler,
|
|
1283
|
+
name: route.name,
|
|
1284
|
+
params: Object.freeze(routeParamEntries(route.pattern)),
|
|
1285
|
+
metadata: snapshotMetadata(route.metadata),
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function routeSnapshotOrder(routes) {
|
|
1290
|
+
return routes.map((route, index) => Object.freeze({
|
|
1291
|
+
route,
|
|
1292
|
+
sourceOrder: index,
|
|
1293
|
+
})).sort(compareRouteSpecificity).map((entry) => entry.route);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function urlForRoute(routes, name, params = {}, query = undefined) {
|
|
1297
|
+
validateName(name, "route");
|
|
1298
|
+
const route = routes.find((current) => current.name === name);
|
|
1299
|
+
if (route === undefined) {
|
|
1300
|
+
throw new Error(`Sloppy route name '${name}' is not registered.`);
|
|
1301
|
+
}
|
|
1302
|
+
return buildRouteUrl(route.pattern, params, query);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function snapshotMetadata(metadata) {
|
|
1306
|
+
const snapshot = { ...metadata };
|
|
1307
|
+
|
|
1308
|
+
if (Array.isArray(snapshot.tags)) {
|
|
1309
|
+
snapshot.tags = Object.freeze([...snapshot.tags]);
|
|
1310
|
+
}
|
|
1311
|
+
if (snapshot.cors !== undefined) {
|
|
1312
|
+
const { state, ...cors } = snapshot.cors;
|
|
1313
|
+
snapshot.cors = Object.freeze({
|
|
1314
|
+
...cors,
|
|
1315
|
+
origins: Object.freeze([...(cors.origins ?? [])]),
|
|
1316
|
+
methods: Object.freeze([...(cors.methods ?? [])]),
|
|
1317
|
+
headers: Object.freeze([...(cors.headers ?? [])]),
|
|
1318
|
+
exposedHeaders: Object.freeze([...(cors.exposedHeaders ?? [])]),
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
if (snapshot.auth !== undefined) {
|
|
1322
|
+
snapshot.auth = snapshotAuthRequirement(snapshot.auth);
|
|
1323
|
+
}
|
|
1324
|
+
if (Array.isArray(snapshot.responses)) {
|
|
1325
|
+
snapshot.responses = Object.freeze(snapshot.responses.map((response) => Object.freeze({ ...response })));
|
|
1326
|
+
}
|
|
1327
|
+
if (Array.isArray(snapshot.rateLimit)) {
|
|
1328
|
+
snapshot.rateLimit = Object.freeze(snapshot.rateLimit.map(snapshotRateLimitPolicy));
|
|
1329
|
+
}
|
|
1330
|
+
if (Array.isArray(snapshot.consumes)) {
|
|
1331
|
+
snapshot.consumes = Object.freeze([...snapshot.consumes]);
|
|
1332
|
+
}
|
|
1333
|
+
if (Array.isArray(snapshot.produces)) {
|
|
1334
|
+
snapshot.produces = Object.freeze([...snapshot.produces]);
|
|
1335
|
+
}
|
|
1336
|
+
if (Array.isArray(snapshot.headers)) {
|
|
1337
|
+
snapshot.headers = Object.freeze(snapshot.headers.map((header) => Object.freeze({ ...header })));
|
|
1338
|
+
}
|
|
1339
|
+
if (snapshot.realtime?.websocket !== undefined) {
|
|
1340
|
+
snapshot.realtime = Object.freeze({
|
|
1341
|
+
...snapshot.realtime,
|
|
1342
|
+
websocket: Object.freeze({
|
|
1343
|
+
...snapshot.realtime.websocket,
|
|
1344
|
+
protocols: Object.freeze([...(snapshot.realtime.websocket.protocols ?? [])]),
|
|
1345
|
+
origins: snapshot.realtime.websocket.origins === "*"
|
|
1346
|
+
? "*"
|
|
1347
|
+
: snapshot.realtime.websocket.origins === undefined
|
|
1348
|
+
? undefined
|
|
1349
|
+
: Object.freeze([...snapshot.realtime.websocket.origins]),
|
|
1350
|
+
}),
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
return Object.freeze(snapshot);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function createEndpointBuilder(route, assertAppMutable) {
|
|
1358
|
+
function setName(name) {
|
|
1359
|
+
assertAppMutable();
|
|
1360
|
+
validateName(name, "endpoint");
|
|
1361
|
+
if (route.name !== name && route.routeSet.some((current) => current.name === name)) {
|
|
1362
|
+
throw new Error(`Sloppy route name '${name}' is already registered.`);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
route.name = name;
|
|
1366
|
+
if (route.routeInfo !== undefined) {
|
|
1367
|
+
route.routeInfo.name = name;
|
|
1368
|
+
}
|
|
1369
|
+
return endpoint;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function addResponse(status, schemaOrResult = undefined, options = undefined) {
|
|
1373
|
+
assertAppMutable();
|
|
1374
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
1375
|
+
throw new TypeError("Sloppy endpoint returns options must be a plain object.");
|
|
1376
|
+
}
|
|
1377
|
+
validateStatusCode(status);
|
|
1378
|
+
|
|
1379
|
+
const response = {
|
|
1380
|
+
status,
|
|
1381
|
+
description: options?.description,
|
|
1382
|
+
contentType: options?.contentType ?? route.metadata.produces?.[0] ?? "application/json",
|
|
1383
|
+
schema: schemaOrResult === undefined ? undefined : schemaMetadata(schemaOrResult, "endpoint returns"),
|
|
1384
|
+
};
|
|
1385
|
+
if (response.description !== undefined) {
|
|
1386
|
+
validateMetadataText(response.description, "endpoint response description");
|
|
1387
|
+
}
|
|
1388
|
+
validateMediaType(response.contentType, "endpoint response content type");
|
|
1389
|
+
|
|
1390
|
+
const responses = [...(route.metadata.responses ?? [])];
|
|
1391
|
+
const existing = responses.findIndex((current) => current.status === status);
|
|
1392
|
+
if (existing >= 0) {
|
|
1393
|
+
responses[existing] = Object.freeze(response);
|
|
1394
|
+
} else {
|
|
1395
|
+
responses.push(Object.freeze(response));
|
|
1396
|
+
}
|
|
1397
|
+
responses.sort((left, right) => left.status - right.status);
|
|
1398
|
+
route.metadata.responses = Object.freeze(responses);
|
|
1399
|
+
route.metadata.returns = Object.freeze(response);
|
|
1400
|
+
return endpoint;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function setAuthRequirement(requirement) {
|
|
1404
|
+
route.metadata.auth = requirement;
|
|
1405
|
+
if (route.routeInfo !== undefined) {
|
|
1406
|
+
route.routeInfo.auth = requirement;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function mergeAuthRequirement(extra) {
|
|
1411
|
+
const current = route.metadata.auth?.required === true ? route.metadata.auth : { required: true };
|
|
1412
|
+
setAuthRequirement(Object.freeze({
|
|
1413
|
+
...current,
|
|
1414
|
+
...extra,
|
|
1415
|
+
schemes: Object.freeze([
|
|
1416
|
+
...new Set([
|
|
1417
|
+
...((current.schemes === undefined) ? [] : current.schemes),
|
|
1418
|
+
...((extra.schemes === undefined) ? [] : extra.schemes),
|
|
1419
|
+
]),
|
|
1420
|
+
]),
|
|
1421
|
+
scopes: Object.freeze([
|
|
1422
|
+
...new Set([
|
|
1423
|
+
...((current.scopes === undefined) ? [] : current.scopes),
|
|
1424
|
+
...((extra.scopes === undefined) ? [] : extra.scopes),
|
|
1425
|
+
]),
|
|
1426
|
+
]),
|
|
1427
|
+
roles: Object.freeze([
|
|
1428
|
+
...new Set([
|
|
1429
|
+
...((current.roles === undefined) ? [] : current.roles),
|
|
1430
|
+
...((extra.roles === undefined) ? [] : extra.roles),
|
|
1431
|
+
]),
|
|
1432
|
+
]),
|
|
1433
|
+
claims: Object.freeze([
|
|
1434
|
+
...new Set([
|
|
1435
|
+
...((current.claims === undefined) ? [] : current.claims),
|
|
1436
|
+
...((extra.claims === undefined) ? [] : extra.claims),
|
|
1437
|
+
]),
|
|
1438
|
+
]),
|
|
1439
|
+
}));
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
const endpoint = {
|
|
1443
|
+
withName(name) {
|
|
1444
|
+
return setName(name);
|
|
1445
|
+
},
|
|
1446
|
+
name(name) {
|
|
1447
|
+
return setName(name);
|
|
1448
|
+
},
|
|
1449
|
+
summary(text) {
|
|
1450
|
+
assertAppMutable();
|
|
1451
|
+
validateMetadataText(text, "endpoint summary");
|
|
1452
|
+
route.metadata.summary = text;
|
|
1453
|
+
return endpoint;
|
|
1454
|
+
},
|
|
1455
|
+
description(text) {
|
|
1456
|
+
assertAppMutable();
|
|
1457
|
+
validateMetadataText(text, "endpoint description");
|
|
1458
|
+
route.metadata.description = text;
|
|
1459
|
+
return endpoint;
|
|
1460
|
+
},
|
|
1461
|
+
tags(...tags) {
|
|
1462
|
+
assertAppMutable();
|
|
1463
|
+
for (const tag of tags) {
|
|
1464
|
+
validateTag(tag);
|
|
1465
|
+
}
|
|
1466
|
+
route.metadata.tags = Object.freeze([...new Set([...(route.metadata.tags ?? []), ...tags])]);
|
|
1467
|
+
return endpoint;
|
|
1468
|
+
},
|
|
1469
|
+
deprecated(reasonOrBool = true) {
|
|
1470
|
+
assertAppMutable();
|
|
1471
|
+
if (typeof reasonOrBool !== "boolean" && typeof reasonOrBool !== "string") {
|
|
1472
|
+
throw new TypeError("Sloppy endpoint deprecated metadata must be a boolean or reason string.");
|
|
1473
|
+
}
|
|
1474
|
+
route.metadata.deprecated = reasonOrBool === false
|
|
1475
|
+
? false
|
|
1476
|
+
: Object.freeze({ value: true, reason: typeof reasonOrBool === "string" ? reasonOrBool : undefined });
|
|
1477
|
+
return endpoint;
|
|
1478
|
+
},
|
|
1479
|
+
requireAuth(options = undefined) {
|
|
1480
|
+
assertAppMutable();
|
|
1481
|
+
const requirement = normalizeAuthRequirement(options);
|
|
1482
|
+
setAuthRequirement(requirement);
|
|
1483
|
+
return endpoint;
|
|
1484
|
+
},
|
|
1485
|
+
requiresAuth(options = undefined) {
|
|
1486
|
+
return endpoint.requireAuth(options);
|
|
1487
|
+
},
|
|
1488
|
+
allowAnonymous() {
|
|
1489
|
+
assertAppMutable();
|
|
1490
|
+
setAuthRequirement(Object.freeze({ required: false, allowAnonymous: true }));
|
|
1491
|
+
return endpoint;
|
|
1492
|
+
},
|
|
1493
|
+
rateLimit(policy) {
|
|
1494
|
+
assertAppMutable();
|
|
1495
|
+
if (!isRateLimitPolicy(policy)) {
|
|
1496
|
+
throw new TypeError("Sloppy endpoint rateLimit expects a RateLimit policy.");
|
|
1497
|
+
}
|
|
1498
|
+
route.metadata.rateLimit = Object.freeze([...(route.metadata.rateLimit ?? []), policy]);
|
|
1499
|
+
route.metadata.responses = Object.freeze([
|
|
1500
|
+
...(route.metadata.responses ?? []).filter((response) => response.status !== 429),
|
|
1501
|
+
Object.freeze({
|
|
1502
|
+
status: 429,
|
|
1503
|
+
description: "Too Many Requests",
|
|
1504
|
+
contentType: "application/problem+json",
|
|
1505
|
+
}),
|
|
1506
|
+
].sort((left, right) => left.status - right.status));
|
|
1507
|
+
if (route.routeInfo !== undefined) {
|
|
1508
|
+
route.routeInfo.rateLimits = route.metadata.rateLimit;
|
|
1509
|
+
}
|
|
1510
|
+
return endpoint;
|
|
1511
|
+
},
|
|
1512
|
+
authorize(policy) {
|
|
1513
|
+
assertAppMutable();
|
|
1514
|
+
mergeAuthRequirement({ policy });
|
|
1515
|
+
return endpoint;
|
|
1516
|
+
},
|
|
1517
|
+
requiresScope(...scopes) {
|
|
1518
|
+
assertAppMutable();
|
|
1519
|
+
if (scopes.length === 0) {
|
|
1520
|
+
throw new TypeError("Sloppy endpoint authorization scope must be a non-empty string.");
|
|
1521
|
+
}
|
|
1522
|
+
for (const scope of scopes) {
|
|
1523
|
+
validateMetadataText(scope, "authorization scope");
|
|
1524
|
+
}
|
|
1525
|
+
mergeAuthRequirement({ scopes: Object.freeze(scopes) });
|
|
1526
|
+
return endpoint;
|
|
1527
|
+
},
|
|
1528
|
+
requiresRole(...roles) {
|
|
1529
|
+
assertAppMutable();
|
|
1530
|
+
mergeAuthRequirement({ roles: Object.freeze(roles) });
|
|
1531
|
+
return endpoint;
|
|
1532
|
+
},
|
|
1533
|
+
security(options = undefined) {
|
|
1534
|
+
return endpoint.requireAuth(options);
|
|
1535
|
+
},
|
|
1536
|
+
allowedOrigins(origins) {
|
|
1537
|
+
assertAppMutable();
|
|
1538
|
+
if (route.kind !== "websocket") {
|
|
1539
|
+
throw new TypeError("Sloppy endpoint allowedOrigins is only supported on WebSocket routes.");
|
|
1540
|
+
}
|
|
1541
|
+
const current = route.metadata.realtime?.websocket ?? {};
|
|
1542
|
+
route.metadata.realtime = Object.freeze({
|
|
1543
|
+
...(route.metadata.realtime ?? {}),
|
|
1544
|
+
kind: route.metadata.realtime?.kind ?? "websocket",
|
|
1545
|
+
websocket: normalizeWebSocketRouteOptions({
|
|
1546
|
+
...current,
|
|
1547
|
+
origins,
|
|
1548
|
+
}),
|
|
1549
|
+
});
|
|
1550
|
+
return endpoint;
|
|
1551
|
+
},
|
|
1552
|
+
accepts(schema, options = undefined) {
|
|
1553
|
+
assertAppMutable();
|
|
1554
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
1555
|
+
throw new TypeError("Sloppy endpoint accepts options must be a plain object.");
|
|
1556
|
+
}
|
|
1557
|
+
if (options?.description !== undefined) {
|
|
1558
|
+
validateMetadataText(options.description, "endpoint request description");
|
|
1559
|
+
}
|
|
1560
|
+
const contentType = options?.contentType ?? route.metadata.consumes?.[0] ?? "application/json";
|
|
1561
|
+
validateMediaType(contentType, "endpoint request content type");
|
|
1562
|
+
|
|
1563
|
+
route.metadata.accepts = Object.freeze({
|
|
1564
|
+
contentType,
|
|
1565
|
+
required: options?.required !== false,
|
|
1566
|
+
description: options?.description,
|
|
1567
|
+
schema: schemaMetadata(schema, "endpoint accepts"),
|
|
1568
|
+
});
|
|
1569
|
+
return endpoint;
|
|
1570
|
+
},
|
|
1571
|
+
returns(statusOrSchema, schemaOrOptions = undefined, maybeOptions = undefined) {
|
|
1572
|
+
assertAppMutable();
|
|
1573
|
+
if (typeof statusOrSchema === "number") {
|
|
1574
|
+
return addResponse(statusOrSchema, schemaOrOptions, maybeOptions);
|
|
1575
|
+
}
|
|
1576
|
+
const options = schemaOrOptions;
|
|
1577
|
+
return addResponse(options?.status ?? 200, statusOrSchema, options);
|
|
1578
|
+
},
|
|
1579
|
+
produces(mediaType) {
|
|
1580
|
+
assertAppMutable();
|
|
1581
|
+
validateMediaType(mediaType, "endpoint produces media type");
|
|
1582
|
+
route.metadata.produces = Object.freeze([...new Set([...(route.metadata.produces ?? []), mediaType])]);
|
|
1583
|
+
return endpoint;
|
|
1584
|
+
},
|
|
1585
|
+
consumes(mediaType) {
|
|
1586
|
+
assertAppMutable();
|
|
1587
|
+
validateMediaType(mediaType, "endpoint consumes media type");
|
|
1588
|
+
route.metadata.consumes = Object.freeze([...new Set([...(route.metadata.consumes ?? []), mediaType])]);
|
|
1589
|
+
return endpoint;
|
|
1590
|
+
},
|
|
1591
|
+
header(name, schema, options = undefined) {
|
|
1592
|
+
assertAppMutable();
|
|
1593
|
+
validateHeaderToken(name, "endpoint header");
|
|
1594
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
1595
|
+
throw new TypeError("Sloppy endpoint header options must be a plain object.");
|
|
1596
|
+
}
|
|
1597
|
+
if (options?.description !== undefined) {
|
|
1598
|
+
validateMetadataText(options.description, "endpoint header description");
|
|
1599
|
+
}
|
|
1600
|
+
route.metadata.headers = Object.freeze([
|
|
1601
|
+
...(route.metadata.headers ?? []).filter((header) => header.name.toLowerCase() !== name.toLowerCase()),
|
|
1602
|
+
Object.freeze({
|
|
1603
|
+
name,
|
|
1604
|
+
schema: schemaMetadata(schema, "endpoint header"),
|
|
1605
|
+
required: options?.required === true,
|
|
1606
|
+
description: options?.description,
|
|
1607
|
+
}),
|
|
1608
|
+
]);
|
|
1609
|
+
return endpoint;
|
|
1610
|
+
},
|
|
1611
|
+
query(schemaOrObject, options = undefined) {
|
|
1612
|
+
assertAppMutable();
|
|
1613
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
1614
|
+
throw new TypeError("Sloppy endpoint query options must be a plain object.");
|
|
1615
|
+
}
|
|
1616
|
+
const schemaValue = isSchema(schemaOrObject) ? schemaOrObject : Schema.object(schemaOrObject);
|
|
1617
|
+
route.metadata.query = Object.freeze({
|
|
1618
|
+
schema: schemaMetadata(schemaValue, "endpoint query"),
|
|
1619
|
+
required: options?.required === true,
|
|
1620
|
+
});
|
|
1621
|
+
return endpoint;
|
|
1622
|
+
},
|
|
1623
|
+
params(schemaOrObject, options = undefined) {
|
|
1624
|
+
assertAppMutable();
|
|
1625
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
1626
|
+
throw new TypeError("Sloppy endpoint params options must be a plain object.");
|
|
1627
|
+
}
|
|
1628
|
+
const schemaValue = isSchema(schemaOrObject) ? schemaOrObject : Schema.object(schemaOrObject);
|
|
1629
|
+
route.metadata.params = Object.freeze({
|
|
1630
|
+
schema: schemaMetadata(schemaValue, "endpoint params"),
|
|
1631
|
+
required: options?.required !== false,
|
|
1632
|
+
});
|
|
1633
|
+
return endpoint;
|
|
1634
|
+
},
|
|
1635
|
+
openapi(override) {
|
|
1636
|
+
assertAppMutable();
|
|
1637
|
+
if (!isPlainObject(override)) {
|
|
1638
|
+
throw new TypeError("Sloppy endpoint OpenAPI override must be a plain object.");
|
|
1639
|
+
}
|
|
1640
|
+
route.metadata.openapi = cloneFrozenJson(override, "endpoint OpenAPI override");
|
|
1641
|
+
return endpoint;
|
|
1642
|
+
},
|
|
1643
|
+
outputCache(options) {
|
|
1644
|
+
assertAppMutable();
|
|
1645
|
+
const normalized = normalizeOutputCacheOptions(options);
|
|
1646
|
+
route.metadata.outputCache = normalized;
|
|
1647
|
+
route.routeInfo.outputCache = normalized;
|
|
1648
|
+
return endpoint;
|
|
1649
|
+
},
|
|
1650
|
+
noOutputCache() {
|
|
1651
|
+
assertAppMutable();
|
|
1652
|
+
route.metadata.outputCache = undefined;
|
|
1653
|
+
route.routeInfo.outputCache = undefined;
|
|
1654
|
+
return endpoint;
|
|
1655
|
+
},
|
|
1656
|
+
cacheHeaders(options) {
|
|
1657
|
+
assertAppMutable();
|
|
1658
|
+
const normalized = normalizeCacheHeaderOptions(options);
|
|
1659
|
+
route.metadata.cacheHeaders = normalized;
|
|
1660
|
+
route.routeInfo.cacheHeaders = normalized;
|
|
1661
|
+
return endpoint;
|
|
1662
|
+
},
|
|
1663
|
+
};
|
|
1664
|
+
|
|
1665
|
+
return Object.freeze(endpoint);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function normalizeGroupPrefix(prefix) {
|
|
1669
|
+
validateGroupPrefix(prefix);
|
|
1670
|
+
|
|
1671
|
+
if (prefix === "/") {
|
|
1672
|
+
return "/";
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
return prefix.replace(/\/+$/u, "");
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
function composeRoutePattern(prefix, childPattern) {
|
|
1679
|
+
validateGroupChildPattern(childPattern);
|
|
1680
|
+
|
|
1681
|
+
if (prefix === "/") {
|
|
1682
|
+
return childPattern.startsWith("/") ? childPattern : `/${childPattern}`;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
if (childPattern === "/") {
|
|
1686
|
+
return prefix;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
return childPattern.startsWith("/") ? `${prefix}${childPattern}` : `${prefix}/${childPattern}`;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function normalizeMapArguments(pattern, optionsOrHandler, maybeHandler) {
|
|
1693
|
+
if (typeof optionsOrHandler === "function" && maybeHandler === undefined) {
|
|
1694
|
+
return {
|
|
1695
|
+
pattern,
|
|
1696
|
+
metadata: undefined,
|
|
1697
|
+
handler: optionsOrHandler,
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
return {
|
|
1702
|
+
pattern,
|
|
1703
|
+
metadata: validateMetadataOptions(optionsOrHandler),
|
|
1704
|
+
handler: maybeHandler,
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
function mergeRouteMetadata(groupMetadata, routeMetadata) {
|
|
1709
|
+
if (routeMetadata?.tags !== undefined && !Array.isArray(routeMetadata.tags)) {
|
|
1710
|
+
throw new TypeError("Sloppy route metadata tags must be an array when provided.");
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
const tags = [
|
|
1714
|
+
...groupMetadata.tags,
|
|
1715
|
+
...((routeMetadata?.tags !== undefined) ? routeMetadata.tags : []),
|
|
1716
|
+
];
|
|
1717
|
+
|
|
1718
|
+
for (const tag of tags) {
|
|
1719
|
+
validateTag(tag);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
return {
|
|
1723
|
+
...routeMetadata,
|
|
1724
|
+
tags,
|
|
1725
|
+
groupName: groupMetadata.name,
|
|
1726
|
+
groupPrefix: groupMetadata.prefix,
|
|
1727
|
+
rateLimit: Object.freeze([
|
|
1728
|
+
...(groupMetadata.rateLimit ?? []),
|
|
1729
|
+
...(routeMetadata?.rateLimit ?? []),
|
|
1730
|
+
]),
|
|
1731
|
+
...(routeMetadata?.auth !== undefined
|
|
1732
|
+
? { auth: routeMetadata.auth }
|
|
1733
|
+
: groupMetadata.auth === undefined ? {} : { auth: groupMetadata.auth }),
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function createRouteMetadata(routeMetadata) {
|
|
1738
|
+
if (routeMetadata?.tags !== undefined) {
|
|
1739
|
+
if (!Array.isArray(routeMetadata.tags)) {
|
|
1740
|
+
throw new TypeError("Sloppy route metadata tags must be an array when provided.");
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
for (const tag of routeMetadata.tags) {
|
|
1744
|
+
validateTag(tag);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
return {
|
|
1748
|
+
...routeMetadata,
|
|
1749
|
+
tags: [...routeMetadata.tags],
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
return routeMetadata ?? {};
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function registerRoute(
|
|
1757
|
+
routes,
|
|
1758
|
+
host,
|
|
1759
|
+
assertAppMutable,
|
|
1760
|
+
currentModule,
|
|
1761
|
+
method,
|
|
1762
|
+
pattern,
|
|
1763
|
+
optionsOrHandler,
|
|
1764
|
+
maybeHandler,
|
|
1765
|
+
metadataBase,
|
|
1766
|
+
middleware = [],
|
|
1767
|
+
corsPolicy = null,
|
|
1768
|
+
kind = "http",
|
|
1769
|
+
) {
|
|
1770
|
+
const args = normalizeMapArguments(pattern, optionsOrHandler, maybeHandler);
|
|
1771
|
+
|
|
1772
|
+
assertAppMutable();
|
|
1773
|
+
validatePattern(args.pattern);
|
|
1774
|
+
validateHandler(args.handler);
|
|
1775
|
+
if (!ROUTE_KINDS.has(kind)) {
|
|
1776
|
+
throw new TypeError("Sloppy route kind is not supported by bootstrap registration.");
|
|
1777
|
+
}
|
|
1778
|
+
if (!ROUTE_METHODS.has(method)) {
|
|
1779
|
+
throw new TypeError("Sloppy route method is not supported by bootstrap registration.");
|
|
1780
|
+
}
|
|
1781
|
+
for (const current of middleware) {
|
|
1782
|
+
validateMiddlewareEntry(current);
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
if (routes.some((route) => route.method === method && route.pattern === args.pattern)) {
|
|
1786
|
+
throw new Error(`Sloppy route '${method} ${args.pattern}' is already registered.`);
|
|
1787
|
+
}
|
|
1788
|
+
if (args.metadata?.name !== undefined &&
|
|
1789
|
+
(typeof args.metadata.name !== "string" || args.metadata.name.length === 0))
|
|
1790
|
+
{
|
|
1791
|
+
throw new TypeError("Sloppy route name must be a non-empty string.");
|
|
1792
|
+
}
|
|
1793
|
+
if (args.metadata?.name !== undefined &&
|
|
1794
|
+
routes.some((route) => route.name === args.metadata.name))
|
|
1795
|
+
{
|
|
1796
|
+
throw new Error(`Sloppy route name '${args.metadata.name}' is already registered.`);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
const orderedMiddleware = orderedMiddlewareFunctions(middleware);
|
|
1800
|
+
const highLevelRealtimeMetadata = kind === "websocket" ? realtimeRouteMetadata(args.handler) : undefined;
|
|
1801
|
+
const realtimeMetadata = highLevelRealtimeMetadata !== undefined
|
|
1802
|
+
? highLevelRealtimeMetadata
|
|
1803
|
+
: kind === "websocket"
|
|
1804
|
+
? { kind, websocket: webSocketRouteOptions(args.handler) ?? normalizeWebSocketRouteOptions() }
|
|
1805
|
+
: { kind };
|
|
1806
|
+
let metadata = {
|
|
1807
|
+
...(metadataBase ? mergeRouteMetadata(metadataBase, args.metadata) : createRouteMetadata(args.metadata)),
|
|
1808
|
+
...((kind === "http") ? {} : { realtime: realtimeMetadata }),
|
|
1809
|
+
...((currentModule !== null) ? { module: currentModule } : {}),
|
|
1810
|
+
middleware: middlewareMetadata(orderedMiddleware),
|
|
1811
|
+
...((corsPolicy !== null) ? { cors: snapshotCorsPolicy(corsPolicy) } : {}),
|
|
1812
|
+
};
|
|
1813
|
+
const outputCache = metadata.outputCache === undefined ? undefined : normalizeOutputCacheOptions(metadata.outputCache);
|
|
1814
|
+
const cacheHeaders = metadata.cacheHeaders === undefined ? undefined : normalizeCacheHeaderOptions(metadata.cacheHeaders);
|
|
1815
|
+
metadata = {
|
|
1816
|
+
...metadata,
|
|
1817
|
+
outputCache,
|
|
1818
|
+
cacheHeaders,
|
|
1819
|
+
};
|
|
1820
|
+
const routeInfo = {
|
|
1821
|
+
method,
|
|
1822
|
+
pattern: args.pattern,
|
|
1823
|
+
name: typeof args.metadata?.name === "string" ? args.metadata.name : null,
|
|
1824
|
+
auth: metadata.auth,
|
|
1825
|
+
outputCache,
|
|
1826
|
+
cacheHeaders,
|
|
1827
|
+
kind,
|
|
1828
|
+
rateLimits: metadata.rateLimit ?? Object.freeze([]),
|
|
1829
|
+
urlFor(name, params = {}, query = undefined) {
|
|
1830
|
+
return urlForRoute(routes, name, params, query);
|
|
1831
|
+
},
|
|
1832
|
+
};
|
|
1833
|
+
const route = {
|
|
1834
|
+
method,
|
|
1835
|
+
kind,
|
|
1836
|
+
pattern: args.pattern,
|
|
1837
|
+
handler: createRouteHandler(
|
|
1838
|
+
host,
|
|
1839
|
+
args.handler,
|
|
1840
|
+
Object.freeze(orderedMiddleware),
|
|
1841
|
+
corsPolicy,
|
|
1842
|
+
routeInfo,
|
|
1843
|
+
),
|
|
1844
|
+
name: routeInfo.name,
|
|
1845
|
+
routeInfo,
|
|
1846
|
+
routeSet: routes,
|
|
1847
|
+
metadata,
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1850
|
+
routes.push(route);
|
|
1851
|
+
if (corsPolicy !== null) {
|
|
1852
|
+
registerCorsPreflightRoute(
|
|
1853
|
+
routes,
|
|
1854
|
+
host,
|
|
1855
|
+
assertAppMutable,
|
|
1856
|
+
args.pattern,
|
|
1857
|
+
method,
|
|
1858
|
+
corsPolicy,
|
|
1859
|
+
Object.freeze(orderedMiddleware),
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
return createEndpointBuilder(route, assertAppMutable);
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
function corsPoliciesEqual(a, b) {
|
|
1866
|
+
if (a === b) {
|
|
1867
|
+
return true;
|
|
1868
|
+
}
|
|
1869
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
|
|
1870
|
+
return false;
|
|
1871
|
+
}
|
|
1872
|
+
if (
|
|
1873
|
+
a.allowAnyOrigin !== b.allowAnyOrigin ||
|
|
1874
|
+
a.credentials !== b.credentials ||
|
|
1875
|
+
a.maxAgeSeconds !== b.maxAgeSeconds
|
|
1876
|
+
) {
|
|
1877
|
+
return false;
|
|
1878
|
+
}
|
|
1879
|
+
const arraysEqual = (left, right) => {
|
|
1880
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
|
|
1881
|
+
return false;
|
|
1882
|
+
}
|
|
1883
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
1884
|
+
if (left[index] !== right[index]) {
|
|
1885
|
+
return false;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
return true;
|
|
1889
|
+
};
|
|
1890
|
+
return (
|
|
1891
|
+
arraysEqual(a.origins, b.origins) &&
|
|
1892
|
+
arraysEqual(a.methods, b.methods) &&
|
|
1893
|
+
arraysEqual(a.headers, b.headers) &&
|
|
1894
|
+
arraysEqual(a.exposedHeaders, b.exposedHeaders)
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
function registerCorsPreflightRoute(
|
|
1899
|
+
routes,
|
|
1900
|
+
host,
|
|
1901
|
+
assertAppMutable,
|
|
1902
|
+
pattern,
|
|
1903
|
+
method,
|
|
1904
|
+
corsPolicy,
|
|
1905
|
+
middleware,
|
|
1906
|
+
) {
|
|
1907
|
+
const existing = routes.find((route) => route.method === "OPTIONS" &&
|
|
1908
|
+
route.pattern === pattern &&
|
|
1909
|
+
route.metadata?.cors?.preflight === true);
|
|
1910
|
+
|
|
1911
|
+
if (existing !== undefined) {
|
|
1912
|
+
if (!corsPoliciesEqual(existing.metadata.cors.state.policy, corsPolicy)) {
|
|
1913
|
+
throw new Error(`Sloppy CORS preflight route '${pattern}' already has a different policy.`);
|
|
1914
|
+
}
|
|
1915
|
+
existing.metadata.cors.state.methods.add(method);
|
|
1916
|
+
existing.kind = "http";
|
|
1917
|
+
existing.handler = createRouteHandler(
|
|
1918
|
+
host,
|
|
1919
|
+
createCorsPreflightHandler(existing.metadata.cors.state),
|
|
1920
|
+
middleware,
|
|
1921
|
+
null,
|
|
1922
|
+
existing.routeInfo ?? { method: "OPTIONS", pattern, name: existing.name ?? null, kind: "http" },
|
|
1923
|
+
);
|
|
1924
|
+
existing.metadata.middleware = middlewareMetadata(middleware);
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
const state = {
|
|
1929
|
+
policy: corsPolicy,
|
|
1930
|
+
methods: new Set([method]),
|
|
1931
|
+
};
|
|
1932
|
+
const routeInfo = {
|
|
1933
|
+
method: "OPTIONS",
|
|
1934
|
+
pattern,
|
|
1935
|
+
name: null,
|
|
1936
|
+
kind: "http",
|
|
1937
|
+
urlFor(name, params = {}, query = undefined) {
|
|
1938
|
+
return urlForRoute(routes, name, params, query);
|
|
1939
|
+
},
|
|
1940
|
+
};
|
|
1941
|
+
routes.push({
|
|
1942
|
+
method: "OPTIONS",
|
|
1943
|
+
kind: "http",
|
|
1944
|
+
pattern,
|
|
1945
|
+
handler: createRouteHandler(
|
|
1946
|
+
host,
|
|
1947
|
+
createCorsPreflightHandler(state),
|
|
1948
|
+
middleware,
|
|
1949
|
+
null,
|
|
1950
|
+
routeInfo,
|
|
1951
|
+
),
|
|
1952
|
+
name: null,
|
|
1953
|
+
routeInfo,
|
|
1954
|
+
routeSet: routes,
|
|
1955
|
+
metadata: {
|
|
1956
|
+
cors: {
|
|
1957
|
+
...snapshotCorsPolicy(corsPolicy),
|
|
1958
|
+
preflight: true,
|
|
1959
|
+
state,
|
|
1960
|
+
},
|
|
1961
|
+
middleware: middlewareMetadata(middleware),
|
|
1962
|
+
},
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
function controllerInjectionTokens(controller) {
|
|
1967
|
+
const tokens = controller.inject ?? controller.dependencies ?? [];
|
|
1968
|
+
|
|
1969
|
+
if (!Array.isArray(tokens)) {
|
|
1970
|
+
throw new TypeError("Sloppy controller inject metadata must be an array when provided.");
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
for (const token of tokens) {
|
|
1974
|
+
validateServiceToken(token);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
return Object.freeze([...tokens]);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
function createControllerHandler(host, Controller, action, routeInfo) {
|
|
1981
|
+
const inject = controllerInjectionTokens(Controller);
|
|
1982
|
+
const prototypeMethod = Controller.prototype?.[action];
|
|
1983
|
+
|
|
1984
|
+
if (typeof prototypeMethod !== "function") {
|
|
1985
|
+
throw new TypeError(`Sloppy controller action '${action}' must name a prototype method.`);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
return function controllerHandler(context) {
|
|
1989
|
+
let ctx = context === undefined || context === null
|
|
1990
|
+
? createHandlerContext(host, routeInfo)
|
|
1991
|
+
: decorateProvidedContext(host, context, routeInfo);
|
|
1992
|
+
let ownsServices = context === undefined || context === null;
|
|
1993
|
+
if (ctx.services === undefined || ctx.services === null) {
|
|
1994
|
+
const nextContext = {
|
|
1995
|
+
...ctx,
|
|
1996
|
+
services: host.services.createScope(),
|
|
1997
|
+
};
|
|
1998
|
+
attachHostMarker(nextContext, host);
|
|
1999
|
+
ctx = Object.freeze(nextContext);
|
|
2000
|
+
ownsServices = true;
|
|
2001
|
+
}
|
|
2002
|
+
const services = ctx.services;
|
|
2003
|
+
try {
|
|
2004
|
+
const dependencies = inject.map((token) => services.get(token));
|
|
2005
|
+
const instance = new Controller(...dependencies);
|
|
2006
|
+
const result = instance[action](ctx);
|
|
2007
|
+
if (ownsServices) {
|
|
2008
|
+
return finishWithCleanup(result, () => services.dispose());
|
|
2009
|
+
}
|
|
2010
|
+
return result;
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
if (ownsServices) {
|
|
2013
|
+
return cleanupAfterFailure(error, () => services.dispose());
|
|
2014
|
+
}
|
|
2015
|
+
throw error;
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
function createControllerMapper(
|
|
2021
|
+
routes,
|
|
2022
|
+
host,
|
|
2023
|
+
assertAppMutable,
|
|
2024
|
+
currentModule,
|
|
2025
|
+
prefix,
|
|
2026
|
+
Controller,
|
|
2027
|
+
middleware = [],
|
|
2028
|
+
getCorsPolicy = () => null,
|
|
2029
|
+
) {
|
|
2030
|
+
const normalizedPrefix = normalizeGroupPrefix(prefix);
|
|
2031
|
+
validateController(Controller);
|
|
2032
|
+
|
|
2033
|
+
function map(method, pattern, action, options) {
|
|
2034
|
+
validateControllerAction(action);
|
|
2035
|
+
return registerRoute(
|
|
2036
|
+
routes,
|
|
2037
|
+
host,
|
|
2038
|
+
assertAppMutable,
|
|
2039
|
+
currentModule,
|
|
2040
|
+
method,
|
|
2041
|
+
composeRoutePattern(normalizedPrefix, pattern),
|
|
2042
|
+
{
|
|
2043
|
+
...(options ?? {}),
|
|
2044
|
+
controller: Controller.name || "AnonymousController",
|
|
2045
|
+
action,
|
|
2046
|
+
},
|
|
2047
|
+
createControllerHandler(
|
|
2048
|
+
host,
|
|
2049
|
+
Controller,
|
|
2050
|
+
action,
|
|
2051
|
+
Object.freeze({ method, pattern: composeRoutePattern(normalizedPrefix, pattern) }),
|
|
2052
|
+
),
|
|
2053
|
+
undefined,
|
|
2054
|
+
middleware,
|
|
2055
|
+
getCorsPolicy(),
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
return Object.freeze({
|
|
2060
|
+
get(pattern, action, options) {
|
|
2061
|
+
return map("GET", pattern, action, options);
|
|
2062
|
+
},
|
|
2063
|
+
post(pattern, action, options) {
|
|
2064
|
+
return map("POST", pattern, action, options);
|
|
2065
|
+
},
|
|
2066
|
+
put(pattern, action, options) {
|
|
2067
|
+
return map("PUT", pattern, action, options);
|
|
2068
|
+
},
|
|
2069
|
+
patch(pattern, action, options) {
|
|
2070
|
+
return map("PATCH", pattern, action, options);
|
|
2071
|
+
},
|
|
2072
|
+
delete(pattern, action, options) {
|
|
2073
|
+
return map("DELETE", pattern, action, options);
|
|
2074
|
+
},
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
function createRouteGroup(
|
|
2079
|
+
routes,
|
|
2080
|
+
host,
|
|
2081
|
+
assertAppMutable,
|
|
2082
|
+
getCurrentModule,
|
|
2083
|
+
prefix,
|
|
2084
|
+
getInheritedMiddleware = () => [],
|
|
2085
|
+
nextMiddlewareSequence = () => 0,
|
|
2086
|
+
getCorsPolicy = () => null,
|
|
2087
|
+
) {
|
|
2088
|
+
const groupMetadata = {
|
|
2089
|
+
prefix: normalizeGroupPrefix(prefix),
|
|
2090
|
+
tags: [],
|
|
2091
|
+
name: null,
|
|
2092
|
+
auth: undefined,
|
|
2093
|
+
rateLimit: Object.freeze([]),
|
|
2094
|
+
};
|
|
2095
|
+
const groupMiddleware = [];
|
|
2096
|
+
|
|
2097
|
+
function createMapMethod(method, kind = "http") {
|
|
2098
|
+
return function mapRoute(pattern, optionsOrHandler, maybeHandler) {
|
|
2099
|
+
const fullPattern = composeRoutePattern(groupMetadata.prefix, pattern);
|
|
2100
|
+
const mappedOptionsOrHandler = kind === "sse" && typeof optionsOrHandler === "function"
|
|
2101
|
+
? createSseRouteHandler(optionsOrHandler)
|
|
2102
|
+
: kind === "websocket" && typeof optionsOrHandler === "function"
|
|
2103
|
+
? createWebSocketRouteHandler(optionsOrHandler, maybeHandler)
|
|
2104
|
+
: optionsOrHandler;
|
|
2105
|
+
const mappedMaybeHandler = kind === "sse" && typeof optionsOrHandler !== "function"
|
|
2106
|
+
? createSseRouteHandler(maybeHandler)
|
|
2107
|
+
: kind === "websocket" && typeof optionsOrHandler !== "function"
|
|
2108
|
+
? createWebSocketRouteHandler(maybeHandler, optionsOrHandler)
|
|
2109
|
+
: kind === "websocket"
|
|
2110
|
+
? undefined
|
|
2111
|
+
: maybeHandler;
|
|
2112
|
+
return registerRoute(
|
|
2113
|
+
routes,
|
|
2114
|
+
host,
|
|
2115
|
+
assertAppMutable,
|
|
2116
|
+
getCurrentModule(),
|
|
2117
|
+
method,
|
|
2118
|
+
fullPattern,
|
|
2119
|
+
mappedOptionsOrHandler,
|
|
2120
|
+
mappedMaybeHandler,
|
|
2121
|
+
{
|
|
2122
|
+
prefix: groupMetadata.prefix,
|
|
2123
|
+
tags: groupMetadata.tags,
|
|
2124
|
+
name: groupMetadata.name,
|
|
2125
|
+
auth: groupMetadata.auth,
|
|
2126
|
+
rateLimit: groupMetadata.rateLimit,
|
|
2127
|
+
},
|
|
2128
|
+
[...getInheritedMiddleware(), ...groupMiddleware],
|
|
2129
|
+
getCorsPolicy(),
|
|
2130
|
+
kind,
|
|
2131
|
+
);
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
const group = {
|
|
2136
|
+
get prefix() {
|
|
2137
|
+
return groupMetadata.prefix;
|
|
2138
|
+
},
|
|
2139
|
+
|
|
2140
|
+
withTags(...tags) {
|
|
2141
|
+
assertAppMutable();
|
|
2142
|
+
|
|
2143
|
+
for (const tag of tags) {
|
|
2144
|
+
validateTag(tag);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
groupMetadata.tags.push(...tags);
|
|
2148
|
+
return group;
|
|
2149
|
+
},
|
|
2150
|
+
|
|
2151
|
+
withName(name) {
|
|
2152
|
+
assertAppMutable();
|
|
2153
|
+
validateName(name, "route group");
|
|
2154
|
+
|
|
2155
|
+
groupMetadata.name = name;
|
|
2156
|
+
return group;
|
|
2157
|
+
},
|
|
2158
|
+
|
|
2159
|
+
use(middleware) {
|
|
2160
|
+
assertAppMutable();
|
|
2161
|
+
validateMiddleware(middleware);
|
|
2162
|
+
|
|
2163
|
+
groupMiddleware.push({ fn: middleware, sequence: nextMiddlewareSequence() });
|
|
2164
|
+
return group;
|
|
2165
|
+
},
|
|
2166
|
+
|
|
2167
|
+
requireAuth(options = undefined) {
|
|
2168
|
+
assertAppMutable();
|
|
2169
|
+
groupMetadata.auth = normalizeAuthRequirement(options);
|
|
2170
|
+
return group;
|
|
2171
|
+
},
|
|
2172
|
+
requiresAuth(options = undefined) {
|
|
2173
|
+
return group.requireAuth(options);
|
|
2174
|
+
},
|
|
2175
|
+
allowAnonymous() {
|
|
2176
|
+
assertAppMutable();
|
|
2177
|
+
groupMetadata.auth = Object.freeze({ required: false, allowAnonymous: true });
|
|
2178
|
+
return group;
|
|
2179
|
+
},
|
|
2180
|
+
rateLimit(policy) {
|
|
2181
|
+
assertAppMutable();
|
|
2182
|
+
if (!isRateLimitPolicy(policy)) {
|
|
2183
|
+
throw new TypeError("Sloppy route group rateLimit expects a RateLimit policy.");
|
|
2184
|
+
}
|
|
2185
|
+
groupMetadata.rateLimit = Object.freeze([...groupMetadata.rateLimit, policy]);
|
|
2186
|
+
return group;
|
|
2187
|
+
},
|
|
2188
|
+
|
|
2189
|
+
mapGet: createMapMethod("GET"),
|
|
2190
|
+
mapPost: createMapMethod("POST"),
|
|
2191
|
+
mapPut: createMapMethod("PUT"),
|
|
2192
|
+
mapPatch: createMapMethod("PATCH"),
|
|
2193
|
+
mapDelete: createMapMethod("DELETE"),
|
|
2194
|
+
get: createMapMethod("GET"),
|
|
2195
|
+
post: createMapMethod("POST"),
|
|
2196
|
+
put: createMapMethod("PUT"),
|
|
2197
|
+
patch: createMapMethod("PATCH"),
|
|
2198
|
+
delete: createMapMethod("DELETE"),
|
|
2199
|
+
sse: createMapMethod("GET", "sse"),
|
|
2200
|
+
ws: createMapMethod("GET", "websocket"),
|
|
2201
|
+
websocket: createMapMethod("GET", "websocket"),
|
|
2202
|
+
realtime(pattern, channel, handler, options = undefined) {
|
|
2203
|
+
const fullPattern = composeRoutePattern(groupMetadata.prefix, pattern);
|
|
2204
|
+
const routeHandler = createRealtimeRouteHandler(channel, handler, options);
|
|
2205
|
+
return registerRoute(
|
|
2206
|
+
routes,
|
|
2207
|
+
host,
|
|
2208
|
+
assertAppMutable,
|
|
2209
|
+
getCurrentModule(),
|
|
2210
|
+
"GET",
|
|
2211
|
+
fullPattern,
|
|
2212
|
+
routeHandler,
|
|
2213
|
+
undefined,
|
|
2214
|
+
{
|
|
2215
|
+
prefix: groupMetadata.prefix,
|
|
2216
|
+
tags: groupMetadata.tags,
|
|
2217
|
+
name: groupMetadata.name,
|
|
2218
|
+
auth: groupMetadata.auth,
|
|
2219
|
+
},
|
|
2220
|
+
[...getInheritedMiddleware(), ...groupMiddleware],
|
|
2221
|
+
getCorsPolicy(),
|
|
2222
|
+
"websocket",
|
|
2223
|
+
);
|
|
2224
|
+
},
|
|
2225
|
+
group(childPrefix) {
|
|
2226
|
+
assertAppMutable();
|
|
2227
|
+
const child = createRouteGroup(
|
|
2228
|
+
routes,
|
|
2229
|
+
host,
|
|
2230
|
+
assertAppMutable,
|
|
2231
|
+
getCurrentModule,
|
|
2232
|
+
composeRoutePattern(groupMetadata.prefix, childPrefix),
|
|
2233
|
+
() => [...getInheritedMiddleware(), ...groupMiddleware],
|
|
2234
|
+
nextMiddlewareSequence,
|
|
2235
|
+
getCorsPolicy,
|
|
2236
|
+
);
|
|
2237
|
+
if (groupMetadata.auth !== undefined) {
|
|
2238
|
+
child.requireAuth(groupMetadata.auth);
|
|
2239
|
+
}
|
|
2240
|
+
for (const policy of groupMetadata.rateLimit) {
|
|
2241
|
+
child.rateLimit(policy);
|
|
2242
|
+
}
|
|
2243
|
+
return child;
|
|
2244
|
+
},
|
|
2245
|
+
};
|
|
2246
|
+
|
|
2247
|
+
return Object.freeze(group);
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
|
|
2251
|
+
function createRouterGroup(prefix, configure) {
|
|
2252
|
+
validateGroupPrefix(prefix);
|
|
2253
|
+
|
|
2254
|
+
if (configure !== undefined && typeof configure !== "function") {
|
|
2255
|
+
throw new TypeError("Sloppy Router.group configure callback must be a function.");
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
function routerGroup(app) {
|
|
2259
|
+
const group = app.group(prefix);
|
|
2260
|
+
if (configure !== undefined) {
|
|
2261
|
+
configure(group);
|
|
2262
|
+
}
|
|
2263
|
+
return group;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
defineFunctionModuleName(routerGroup, `router:${normalizeGroupPrefix(prefix)}`);
|
|
2267
|
+
return Object.freeze(routerGroup);
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
export {
|
|
2271
|
+
createControllerMapper,
|
|
2272
|
+
createRouteGroup,
|
|
2273
|
+
createRouterGroup,
|
|
2274
|
+
normalizeCorsPolicy,
|
|
2275
|
+
registerRoute,
|
|
2276
|
+
routeSnapshotOrder,
|
|
2277
|
+
snapshotRoute,
|
|
2278
|
+
urlForRoute,
|
|
2279
|
+
};
|