@slopware/sloppy-darwin-arm64 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,2142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCapabilityProvider,
|
|
3
|
+
createCapabilityRegistry,
|
|
4
|
+
} from "./internal/capabilities.js";
|
|
5
|
+
import {
|
|
6
|
+
createAuthState,
|
|
7
|
+
isAuthProviderDescriptor,
|
|
8
|
+
normalizeAuthPolicy,
|
|
9
|
+
registerAuthProvider,
|
|
10
|
+
snapshotAuthState,
|
|
11
|
+
} from "./auth.js";
|
|
12
|
+
import { createConfigBuilder, createConfigProvider } from "./internal/config.js";
|
|
13
|
+
import { createLogger, createLoggingBuilder } from "./internal/logging.js";
|
|
14
|
+
import {
|
|
15
|
+
assertRouteOnlyModule,
|
|
16
|
+
createModule,
|
|
17
|
+
createModuleDebugEntries,
|
|
18
|
+
functionModuleName,
|
|
19
|
+
getModuleState,
|
|
20
|
+
requireModuleState,
|
|
21
|
+
resolveModuleOrder,
|
|
22
|
+
runModulePhase,
|
|
23
|
+
} from "./internal/modules.js";
|
|
24
|
+
import {
|
|
25
|
+
createControllerMapper,
|
|
26
|
+
createRouteGroup,
|
|
27
|
+
createRouterGroup,
|
|
28
|
+
normalizeCorsPolicy,
|
|
29
|
+
registerRoute,
|
|
30
|
+
snapshotRoute,
|
|
31
|
+
urlForRoute,
|
|
32
|
+
} from "./internal/routes.js";
|
|
33
|
+
import { createServiceProvider, createServicesBuilder } from "./internal/services.js";
|
|
34
|
+
import { createMutationGuard, isPlainObject } from "./internal/shared.js";
|
|
35
|
+
import { validateSqliteProviderOptions } from "./internal/validation.js";
|
|
36
|
+
import { Health, createHealthHandler as createOpsHealthHandler } from "./health.js";
|
|
37
|
+
import { isCache } from "./cache.js";
|
|
38
|
+
import { Metrics } from "./metrics.js";
|
|
39
|
+
import { RateLimit, isRateLimitStore } from "./rate-limit.js";
|
|
40
|
+
import { normalizeJsonOptions, Results } from "./results.js";
|
|
41
|
+
import { createSseRouteHandler, createRealtimeRouteHandler, createWebSocketRouteHandler } from "./realtime.js";
|
|
42
|
+
import { isValidationError, validationProblem } from "./schema.js";
|
|
43
|
+
|
|
44
|
+
const DEFAULT_HEALTH_PATH = "/health";
|
|
45
|
+
const DEFAULT_LIVENESS_PATH = "/health/live";
|
|
46
|
+
const DEFAULT_READINESS_PATH = "/health/ready";
|
|
47
|
+
const DEFAULT_STARTUP_PATH = "/startup";
|
|
48
|
+
const DEFAULT_MANAGEMENT_PATH = "/_sloppy";
|
|
49
|
+
const PROMETHEUS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
|
|
50
|
+
const DEFAULT_MAX_ERROR_BODY_BYTES = 1024 * 1024;
|
|
51
|
+
const DEFAULT_CONTENT_NEGOTIATION = Object.freeze({
|
|
52
|
+
strictAccept: false,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function normalizeContentNegotiationOptions(options = undefined) {
|
|
56
|
+
if (options === undefined) {
|
|
57
|
+
return DEFAULT_CONTENT_NEGOTIATION;
|
|
58
|
+
}
|
|
59
|
+
if (!isPlainObject(options)) {
|
|
60
|
+
throw new TypeError("Sloppy content negotiation options must be a plain object.");
|
|
61
|
+
}
|
|
62
|
+
const strictAccept = options.strictAccept ?? DEFAULT_CONTENT_NEGOTIATION.strictAccept;
|
|
63
|
+
if (typeof strictAccept !== "boolean") {
|
|
64
|
+
throw new TypeError("Sloppy content negotiation strictAccept must be a boolean.");
|
|
65
|
+
}
|
|
66
|
+
return Object.freeze({ strictAccept });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeSecurityHeadersOptions(options = undefined) {
|
|
70
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
71
|
+
throw new TypeError("Sloppy securityHeaders options must be a plain object.");
|
|
72
|
+
}
|
|
73
|
+
const frameOptions = options?.frameOptions ?? "deny";
|
|
74
|
+
if (frameOptions !== false && frameOptions !== "deny" && frameOptions !== "sameorigin") {
|
|
75
|
+
throw new TypeError("Sloppy securityHeaders frameOptions must be deny, sameorigin, or false.");
|
|
76
|
+
}
|
|
77
|
+
const headers = {
|
|
78
|
+
...(options?.contentTypeOptions === false ? {} : { "X-Content-Type-Options": "nosniff" }),
|
|
79
|
+
...(frameOptions === false ? {} : { "X-Frame-Options": frameOptions === "deny" ? "DENY" : "SAMEORIGIN" }),
|
|
80
|
+
"Referrer-Policy": options?.referrerPolicy ?? "no-referrer",
|
|
81
|
+
};
|
|
82
|
+
if (options?.contentSecurityPolicy !== undefined) {
|
|
83
|
+
if (typeof options.contentSecurityPolicy !== "string" || options.contentSecurityPolicy.length === 0) {
|
|
84
|
+
throw new TypeError("Sloppy securityHeaders contentSecurityPolicy must be a non-empty string.");
|
|
85
|
+
}
|
|
86
|
+
headers["Content-Security-Policy"] = options.contentSecurityPolicy;
|
|
87
|
+
}
|
|
88
|
+
if (options?.permissionsPolicy !== undefined) {
|
|
89
|
+
if (typeof options.permissionsPolicy !== "string" || options.permissionsPolicy.length === 0) {
|
|
90
|
+
throw new TypeError("Sloppy securityHeaders permissionsPolicy must be a non-empty string.");
|
|
91
|
+
}
|
|
92
|
+
headers["Permissions-Policy"] = options.permissionsPolicy;
|
|
93
|
+
}
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
headers: Object.freeze(headers),
|
|
96
|
+
hsts: options?.hsts === true,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function securityHeadersMiddleware(policy) {
|
|
101
|
+
return async function sloppySecurityHeaders(ctx, next) {
|
|
102
|
+
const result = await next();
|
|
103
|
+
const existing = isPlainObject(result?.headers) ? result.headers : {};
|
|
104
|
+
const headers = { ...policy.headers, ...existing };
|
|
105
|
+
if (policy.hsts && ctx?.connection?.secure === true && headers["Strict-Transport-Security"] === undefined) {
|
|
106
|
+
headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains";
|
|
107
|
+
}
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
...result,
|
|
110
|
+
headers: Object.freeze(headers),
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeAppOptions(options = undefined) {
|
|
116
|
+
if (options === undefined) {
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
json: normalizeJsonOptions(),
|
|
119
|
+
contentNegotiation: DEFAULT_CONTENT_NEGOTIATION,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (!isPlainObject(options)) {
|
|
123
|
+
throw new TypeError("Sloppy.create options must be a plain object.");
|
|
124
|
+
}
|
|
125
|
+
return Object.freeze({
|
|
126
|
+
json: normalizeJsonOptions(options.json),
|
|
127
|
+
contentNegotiation: normalizeContentNegotiationOptions(options.contentNegotiation),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function validateProviderDescriptor(provider) {
|
|
132
|
+
if (!isPlainObject(provider) || provider.__sloppyProvider !== true) {
|
|
133
|
+
throw new TypeError("Sloppy app.use expects a Sloppy provider descriptor.");
|
|
134
|
+
}
|
|
135
|
+
if (provider.kind !== "sqlite") {
|
|
136
|
+
throw new TypeError("Sloppy app.use currently supports sqlite provider descriptors.");
|
|
137
|
+
}
|
|
138
|
+
if (typeof provider.name !== "string" || provider.name.length === 0) {
|
|
139
|
+
throw new TypeError("Sloppy sqlite provider name must be a non-empty string.");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isProblemDetailsDescriptor(value) {
|
|
144
|
+
return isPlainObject(value) &&
|
|
145
|
+
(value.__sloppyProblemDetails === true || value.__sloppyErrorPolicy === true);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isBooleanConfigReference(value) {
|
|
149
|
+
return isPlainObject(value) &&
|
|
150
|
+
value.__sloppyConfigReference === true &&
|
|
151
|
+
value.type === "boolean" &&
|
|
152
|
+
typeof value.key === "string";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function resolveBooleanOption(value, config, fallback, subject) {
|
|
156
|
+
if (value === undefined) {
|
|
157
|
+
return fallback;
|
|
158
|
+
}
|
|
159
|
+
if (typeof value === "boolean") {
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
if (isBooleanConfigReference(value)) {
|
|
163
|
+
const defaultValue = value.default === undefined ? fallback : value.default;
|
|
164
|
+
return config.getBool(value.key, defaultValue);
|
|
165
|
+
}
|
|
166
|
+
throw new TypeError(`Sloppy ${subject} must be a boolean or Config.boolean reference.`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function resolveDetailPolicy(options, config) {
|
|
170
|
+
if (options?.detail !== undefined) {
|
|
171
|
+
return options.detail;
|
|
172
|
+
}
|
|
173
|
+
return resolveBooleanOption(options?.includeDetails, config, false, "error includeDetails")
|
|
174
|
+
? "always"
|
|
175
|
+
: "never";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function normalizeErrorPolicyOptions(options, config) {
|
|
179
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
180
|
+
throw new TypeError("Sloppy app.useErrors options must be a plain object.");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const detail = resolveDetailPolicy(options, config);
|
|
184
|
+
if (detail !== "never" && detail !== "development" && detail !== "always") {
|
|
185
|
+
throw new TypeError("Sloppy error detail policy must be never, development, or always.");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (options?.missingRoute !== undefined && typeof options.missingRoute !== "boolean") {
|
|
189
|
+
throw new TypeError("Sloppy app.useErrors missingRoute must be a boolean when provided.");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const maxBodyBytes = options?.maxBodyBytes ?? DEFAULT_MAX_ERROR_BODY_BYTES;
|
|
193
|
+
if (!Number.isInteger(maxBodyBytes) || maxBodyBytes < 0) {
|
|
194
|
+
throw new TypeError("Sloppy app.useErrors maxBodyBytes must be a non-negative integer.");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return Object.freeze({
|
|
198
|
+
detail,
|
|
199
|
+
missingRoute: options?.missingRoute ?? true,
|
|
200
|
+
maxBodyBytes,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function normalizeLegacyProblemDetails(descriptor, config) {
|
|
205
|
+
return Object.freeze({
|
|
206
|
+
detail: descriptor.detail ?? "never",
|
|
207
|
+
missingRoute: false,
|
|
208
|
+
maxBodyBytes: DEFAULT_MAX_ERROR_BODY_BYTES,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function shouldIncludeDetails(policy, config) {
|
|
213
|
+
const environment = String(config.get("Sloppy:Environment", config.get("Environment", "")));
|
|
214
|
+
return policy.detail === "always" ||
|
|
215
|
+
(policy.detail === "development" && environment.toLowerCase() === "development");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function problemBody(status, title, code, context, error, policy, config) {
|
|
219
|
+
const problem = { status, title, code };
|
|
220
|
+
if (typeof context?.requestId === "string" && context.requestId.length !== 0) {
|
|
221
|
+
problem.requestId = context.requestId;
|
|
222
|
+
}
|
|
223
|
+
if (error !== undefined && shouldIncludeDetails(policy, config)) {
|
|
224
|
+
problem.detail = String(error?.message ?? error);
|
|
225
|
+
}
|
|
226
|
+
return Object.freeze(problem);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function problemResult(status, title, code, context, error, policy, config) {
|
|
230
|
+
return Results.problem(problemBody(status, title, code, context, error, policy, config), { status });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function validationProblemResult(error, context) {
|
|
234
|
+
const problem = {
|
|
235
|
+
...validationProblem(error.issues),
|
|
236
|
+
};
|
|
237
|
+
if (typeof context?.requestId === "string" && context.requestId.length !== 0) {
|
|
238
|
+
problem.requestId = context.requestId;
|
|
239
|
+
}
|
|
240
|
+
return Results.problem(Object.freeze(problem), { status: 400 });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function isProviderError(error) {
|
|
244
|
+
return error !== null && typeof error === "object" &&
|
|
245
|
+
(error.__sloppyProviderError === true || /provider|database|sqlite|postgres|sqlserver/iu.test(String(error.name ?? "")));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function statusTitle(status) {
|
|
249
|
+
switch (status) {
|
|
250
|
+
case 400: return "Bad Request";
|
|
251
|
+
case 401: return "Unauthorized";
|
|
252
|
+
case 403: return "Forbidden";
|
|
253
|
+
case 404: return "Not Found";
|
|
254
|
+
case 413: return "Payload Too Large";
|
|
255
|
+
case 415: return "Unsupported Media Type";
|
|
256
|
+
default: return status >= 500 ? "Internal Server Error" : "Request Failed";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function codeForStatus(status) {
|
|
261
|
+
switch (status) {
|
|
262
|
+
case 400: return "SLOPPY_E_BAD_REQUEST";
|
|
263
|
+
case 401: return "SLOPPY_E_AUTH_UNAUTHORIZED";
|
|
264
|
+
case 403: return "SLOPPY_E_AUTH_FORBIDDEN";
|
|
265
|
+
case 404: return "SLOPPY_E_NOT_FOUND";
|
|
266
|
+
case 413: return "SLOPPY_E_PAYLOAD_TOO_LARGE";
|
|
267
|
+
case 415: return "SLOPPY_E_UNSUPPORTED_MEDIA_TYPE";
|
|
268
|
+
default: return "SLOPPY_E_HANDLER_ERROR";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function logErrorOnce(context, error, status, code) {
|
|
273
|
+
if (context?.__sloppyErrorLogged === true ||
|
|
274
|
+
context?.log === undefined ||
|
|
275
|
+
typeof context.log.error !== "function")
|
|
276
|
+
{
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
Object.defineProperty(context, "__sloppyErrorLogged", {
|
|
280
|
+
value: true,
|
|
281
|
+
enumerable: false,
|
|
282
|
+
writable: true,
|
|
283
|
+
configurable: true,
|
|
284
|
+
});
|
|
285
|
+
const fields = { status, code };
|
|
286
|
+
if (typeof context.requestId === "string") {
|
|
287
|
+
fields.requestId = context.requestId;
|
|
288
|
+
}
|
|
289
|
+
if (typeof context.routePattern === "string") {
|
|
290
|
+
fields.route = context.routePattern;
|
|
291
|
+
fields.routePattern = context.routePattern;
|
|
292
|
+
}
|
|
293
|
+
if (typeof error?.name === "string") {
|
|
294
|
+
fields.errorName = error.name;
|
|
295
|
+
}
|
|
296
|
+
context.log.error("request failed", fields);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function resultFromMapper(mapped, status) {
|
|
300
|
+
if (mapped?.__sloppyResult === true) {
|
|
301
|
+
return mapped;
|
|
302
|
+
}
|
|
303
|
+
if (isPlainObject(mapped)) {
|
|
304
|
+
const resolvedStatus = Number.isInteger(mapped.status) ? mapped.status : status;
|
|
305
|
+
return Results.problem(mapped, { status: resolvedStatus });
|
|
306
|
+
}
|
|
307
|
+
throw new TypeError("Sloppy app.mapError mapper must return Results.* or a ProblemDetails object.");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function mappedErrorResult(error, context, policyState) {
|
|
311
|
+
for (const mapping of policyState.mappings) {
|
|
312
|
+
if (error instanceof mapping.type) {
|
|
313
|
+
return resultFromMapper(mapping.mapper(error, context), 500);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function errorPolicyResult(error, context, policyState, config) {
|
|
320
|
+
if (isValidationError(error)) {
|
|
321
|
+
return validationProblemResult(error, context);
|
|
322
|
+
}
|
|
323
|
+
let mapped;
|
|
324
|
+
try {
|
|
325
|
+
mapped = mappedErrorResult(error, context, policyState);
|
|
326
|
+
} catch (mapperError) {
|
|
327
|
+
logErrorOnce(context, mapperError, 500, "SLOPPY_E_HANDLER_ERROR");
|
|
328
|
+
return problemResult(
|
|
329
|
+
500,
|
|
330
|
+
"Internal Server Error",
|
|
331
|
+
"SLOPPY_E_HANDLER_ERROR",
|
|
332
|
+
context,
|
|
333
|
+
mapperError,
|
|
334
|
+
policyState.policy,
|
|
335
|
+
config,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (mapped !== undefined) {
|
|
339
|
+
const status = Number.isInteger(mapped.status) ? mapped.status : 500;
|
|
340
|
+
logErrorOnce(context, error, status, mapped.body?.code ?? codeForStatus(status));
|
|
341
|
+
return mapped;
|
|
342
|
+
}
|
|
343
|
+
if (isProviderError(error)) {
|
|
344
|
+
logErrorOnce(context, error, 500, "SLOPPY_E_PROVIDER_ERROR");
|
|
345
|
+
return problemResult(500, "Provider error", "SLOPPY_E_PROVIDER_ERROR", context, undefined, policyState.policy, config);
|
|
346
|
+
}
|
|
347
|
+
logErrorOnce(context, error, 500, "SLOPPY_E_HANDLER_ERROR");
|
|
348
|
+
return problemResult(500, "Internal Server Error", "SLOPPY_E_HANDLER_ERROR", context, error, policyState.policy, config);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function standardProblemResult(status, context, policyState, config) {
|
|
352
|
+
return problemResult(status, statusTitle(status), codeForStatus(status), context, undefined, policyState.policy, config);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function isWorkerResource(resource) {
|
|
356
|
+
return resource !== null &&
|
|
357
|
+
typeof resource === "object" &&
|
|
358
|
+
typeof resource.__sloppyPlanMetadata === "function" &&
|
|
359
|
+
typeof resource.__sloppyWorkerResource === "string";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function snapshotWorkerResource(resource) {
|
|
363
|
+
return Object.freeze({ ...resource.__sloppyPlanMetadata() });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function sqliteProviderToken(name) {
|
|
367
|
+
return name.includes(".") ? name : `data.${name}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function validateMergedProviderOptions(provider, options) {
|
|
371
|
+
if (provider.kind === "sqlite") {
|
|
372
|
+
validateSqliteProviderOptions(options, { requireDatabase: true });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function validateHealthPath(path, subject) {
|
|
377
|
+
if (typeof path !== "string" || path.length === 0 || !path.startsWith("/")) {
|
|
378
|
+
throw new TypeError(`Sloppy ${subject} path must be a non-empty string starting with '/'.`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function validateHealthCheckName(name) {
|
|
383
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
384
|
+
throw new TypeError("Sloppy health check name must be a non-empty string.");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function validateHealthCheckFunction(check) {
|
|
389
|
+
if (typeof check !== "function") {
|
|
390
|
+
throw new TypeError("Sloppy health check must be a function.");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function validateStaticRequestPath(path) {
|
|
395
|
+
if (typeof path !== "string" || path.length === 0 || !path.startsWith("/")) {
|
|
396
|
+
throw new TypeError("Sloppy static files mount path must be a non-empty string starting with '/'.");
|
|
397
|
+
}
|
|
398
|
+
if (path.length > 1 && path.endsWith("/")) {
|
|
399
|
+
throw new TypeError("Sloppy static files mount path must not end with '/'.");
|
|
400
|
+
}
|
|
401
|
+
if (path.includes("{") || path.includes("}") || path.includes("//")) {
|
|
402
|
+
throw new TypeError("Sloppy static files mount path must be a static route prefix.");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function validateStaticSafeRelativePath(value, subject) {
|
|
407
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
408
|
+
throw new TypeError(`Sloppy static files ${subject} must be a non-empty string.`);
|
|
409
|
+
}
|
|
410
|
+
if (/[\x00-\x1F\x7F]/u.test(value)) {
|
|
411
|
+
throw new TypeError(`Sloppy static files ${subject} must not contain control characters.`);
|
|
412
|
+
}
|
|
413
|
+
if (value.startsWith("/") || value.startsWith("\\") || /^[A-Za-z]:[\\/]/u.test(value) || value.startsWith("//") || value.startsWith("\\\\")) {
|
|
414
|
+
throw new TypeError(`Sloppy static files ${subject} must be project-relative.`);
|
|
415
|
+
}
|
|
416
|
+
const parts = value.split(/[\\/]/u);
|
|
417
|
+
if (parts.some((part) => part.length === 0 || part === "." || part === "..")) {
|
|
418
|
+
throw new TypeError(`Sloppy static files ${subject} must not contain empty, '.', or '..' path segments.`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function validateStaticRoot(root) {
|
|
423
|
+
validateStaticSafeRelativePath(root, "root");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function normalizeStaticCacheControl(cacheControl, subject) {
|
|
427
|
+
if (cacheControl === undefined) {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
if (typeof cacheControl !== "string" || cacheControl.length === 0 || /[\x00-\x1F\x7F]/u.test(cacheControl)) {
|
|
431
|
+
throw new TypeError(`Sloppy ${subject} cacheControl must be a safe non-empty string.`);
|
|
432
|
+
}
|
|
433
|
+
return cacheControl;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function normalizeStaticPrecompressed(value, subject) {
|
|
437
|
+
if (value === undefined) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
if (typeof value === "boolean") {
|
|
441
|
+
return value;
|
|
442
|
+
}
|
|
443
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
444
|
+
throw new TypeError(`Sloppy ${subject} precompressed must be a boolean or an array.`);
|
|
445
|
+
}
|
|
446
|
+
const values = [];
|
|
447
|
+
for (const entry of value) {
|
|
448
|
+
if (entry !== "br" && entry !== "gzip") {
|
|
449
|
+
throw new TypeError(`Sloppy ${subject} precompressed entries must be 'br' or 'gzip'.`);
|
|
450
|
+
}
|
|
451
|
+
if (!values.includes(entry)) {
|
|
452
|
+
values.push(entry);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return Object.freeze(values);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function normalizeStaticFileOptions(mount, options, legacy = false) {
|
|
459
|
+
if (!isPlainObject(options)) {
|
|
460
|
+
throw new TypeError("Sloppy app.staticFiles options must be a plain object.");
|
|
461
|
+
}
|
|
462
|
+
validateStaticRequestPath(mount);
|
|
463
|
+
validateStaticRoot(options.root);
|
|
464
|
+
if (options.index !== undefined && options.index !== false) {
|
|
465
|
+
validateStaticSafeRelativePath(options.index, "index");
|
|
466
|
+
}
|
|
467
|
+
const dotfiles = options.dotfiles ?? "deny";
|
|
468
|
+
if (dotfiles !== "deny" && dotfiles !== "ignore" && dotfiles !== "allow") {
|
|
469
|
+
throw new TypeError("Sloppy static files dotfiles must be deny, ignore, or allow.");
|
|
470
|
+
}
|
|
471
|
+
const etag = options.etag ?? true;
|
|
472
|
+
if (etag !== true && etag !== false && etag !== "weak" && etag !== "strong") {
|
|
473
|
+
throw new TypeError("Sloppy static files etag must be a boolean, weak, or strong.");
|
|
474
|
+
}
|
|
475
|
+
const lastModified = options.lastModified ?? true;
|
|
476
|
+
if (typeof lastModified !== "boolean") {
|
|
477
|
+
throw new TypeError("Sloppy static files lastModified must be a boolean.");
|
|
478
|
+
}
|
|
479
|
+
const range = options.range ?? true;
|
|
480
|
+
if (typeof range !== "boolean") {
|
|
481
|
+
throw new TypeError("Sloppy static files range must be a boolean.");
|
|
482
|
+
}
|
|
483
|
+
const maxFileBytes = options.maxFileBytes ?? 1024 * 1024;
|
|
484
|
+
if (!Number.isInteger(maxFileBytes) || maxFileBytes <= 0 || maxFileBytes > 128 * 1024 * 1024) {
|
|
485
|
+
throw new TypeError("Sloppy static files maxFileBytes must be a positive bounded integer.");
|
|
486
|
+
}
|
|
487
|
+
const cacheControl = normalizeStaticCacheControl(
|
|
488
|
+
options.cacheControl ?? (legacy && options.cache?.maxAgeSeconds !== undefined
|
|
489
|
+
? `public, max-age=${options.cache.maxAgeSeconds}`
|
|
490
|
+
: undefined),
|
|
491
|
+
"static files",
|
|
492
|
+
);
|
|
493
|
+
return Object.freeze({
|
|
494
|
+
kind: "static",
|
|
495
|
+
mount,
|
|
496
|
+
root: options.root,
|
|
497
|
+
index: options.index === undefined ? "index.html" : options.index,
|
|
498
|
+
dotfiles,
|
|
499
|
+
etag,
|
|
500
|
+
lastModified,
|
|
501
|
+
cacheControl,
|
|
502
|
+
precompressed: normalizeStaticPrecompressed(options.precompressed, "static files"),
|
|
503
|
+
range,
|
|
504
|
+
fallthrough: options.fallthrough !== false,
|
|
505
|
+
maxFileBytes,
|
|
506
|
+
allowedExtensions: Object.freeze([...(options.allowedExtensions ?? [])]),
|
|
507
|
+
deniedExtensions: Object.freeze([...(options.deniedExtensions ?? [])]),
|
|
508
|
+
contentType: Object.freeze({ ...(options.contentType ?? {}) }),
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function normalizeSpaOptions(mount, options) {
|
|
513
|
+
if (!isPlainObject(options)) {
|
|
514
|
+
throw new TypeError("Sloppy app.spa options must be a plain object.");
|
|
515
|
+
}
|
|
516
|
+
validateStaticRequestPath(mount);
|
|
517
|
+
validateStaticRoot(options.root);
|
|
518
|
+
validateStaticSafeRelativePath(options.fallback, "fallback");
|
|
519
|
+
if (options.assetsPrefix !== undefined) {
|
|
520
|
+
validateStaticRequestPath(options.assetsPrefix);
|
|
521
|
+
}
|
|
522
|
+
if (options.cacheControl !== undefined && !isPlainObject(options.cacheControl)) {
|
|
523
|
+
throw new TypeError("Sloppy app.spa cacheControl must be a plain object.");
|
|
524
|
+
}
|
|
525
|
+
const maxFileBytes = options.maxFileBytes ?? 1024 * 1024;
|
|
526
|
+
if (!Number.isInteger(maxFileBytes) || maxFileBytes <= 0 || maxFileBytes > 128 * 1024 * 1024) {
|
|
527
|
+
throw new TypeError("Sloppy SPA maxFileBytes must be a positive bounded integer.");
|
|
528
|
+
}
|
|
529
|
+
return Object.freeze({
|
|
530
|
+
kind: "spa",
|
|
531
|
+
mount,
|
|
532
|
+
root: options.root,
|
|
533
|
+
fallback: options.fallback,
|
|
534
|
+
assetsPrefix: options.assetsPrefix,
|
|
535
|
+
cacheControl: Object.freeze({
|
|
536
|
+
html: normalizeStaticCacheControl(options.cacheControl?.html, "SPA html"),
|
|
537
|
+
assets: normalizeStaticCacheControl(options.cacheControl?.assets, "SPA assets"),
|
|
538
|
+
}),
|
|
539
|
+
precompressed: normalizeStaticPrecompressed(options.precompressed, "SPA"),
|
|
540
|
+
dotfiles: "deny",
|
|
541
|
+
etag: true,
|
|
542
|
+
lastModified: true,
|
|
543
|
+
range: true,
|
|
544
|
+
index: false,
|
|
545
|
+
maxFileBytes,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function validateStaticFilesOptions(options) {
|
|
550
|
+
if (!isPlainObject(options)) {
|
|
551
|
+
throw new TypeError("Sloppy app.useStaticFiles options must be a plain object.");
|
|
552
|
+
}
|
|
553
|
+
return normalizeStaticFileOptions(options.requestPath, options, true);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function normalizeHealthCheck(check, index) {
|
|
557
|
+
if (typeof check === "function") {
|
|
558
|
+
const name = check.name.length > 0 ? check.name : `check-${index + 1}`;
|
|
559
|
+
return Object.freeze({
|
|
560
|
+
name,
|
|
561
|
+
check,
|
|
562
|
+
liveness: false,
|
|
563
|
+
readiness: true,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!isPlainObject(check)) {
|
|
568
|
+
throw new TypeError("Sloppy health checks must be functions or plain objects.");
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
validateHealthCheckName(check.name);
|
|
572
|
+
validateHealthCheckFunction(check.check);
|
|
573
|
+
|
|
574
|
+
const liveness = check.liveness === true;
|
|
575
|
+
const readiness = check.readiness !== false;
|
|
576
|
+
|
|
577
|
+
if (!liveness && !readiness) {
|
|
578
|
+
throw new TypeError("Sloppy health check must target readiness or liveness.");
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return Object.freeze({
|
|
582
|
+
name: check.name,
|
|
583
|
+
check: check.check,
|
|
584
|
+
liveness,
|
|
585
|
+
readiness,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function normalizeHealthOptions(options) {
|
|
590
|
+
if (options === undefined) {
|
|
591
|
+
return Object.freeze({
|
|
592
|
+
path: DEFAULT_HEALTH_PATH,
|
|
593
|
+
livenessPath: DEFAULT_LIVENESS_PATH,
|
|
594
|
+
readinessPath: DEFAULT_READINESS_PATH,
|
|
595
|
+
checks: Object.freeze([]),
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (typeof options === "string") {
|
|
600
|
+
validateHealthPath(options, "health");
|
|
601
|
+
if (new Set([options, DEFAULT_LIVENESS_PATH, DEFAULT_READINESS_PATH]).size !== 3) {
|
|
602
|
+
throw new TypeError("Sloppy health, liveness, and readiness paths must be distinct.");
|
|
603
|
+
}
|
|
604
|
+
return Object.freeze({
|
|
605
|
+
path: options,
|
|
606
|
+
livenessPath: DEFAULT_LIVENESS_PATH,
|
|
607
|
+
readinessPath: DEFAULT_READINESS_PATH,
|
|
608
|
+
checks: Object.freeze([]),
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (!isPlainObject(options)) {
|
|
613
|
+
throw new TypeError("Sloppy app.mapHealthChecks options must be a plain object.");
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const path = options.path ?? DEFAULT_HEALTH_PATH;
|
|
617
|
+
const livenessPath = options.livenessPath ?? DEFAULT_LIVENESS_PATH;
|
|
618
|
+
const readinessPath = options.readinessPath ?? DEFAULT_READINESS_PATH;
|
|
619
|
+
|
|
620
|
+
validateHealthPath(path, "health");
|
|
621
|
+
validateHealthPath(livenessPath, "liveness");
|
|
622
|
+
validateHealthPath(readinessPath, "readiness");
|
|
623
|
+
|
|
624
|
+
if (new Set([path, livenessPath, readinessPath]).size !== 3) {
|
|
625
|
+
throw new TypeError("Sloppy health, liveness, and readiness paths must be distinct.");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (options.checks !== undefined && !Array.isArray(options.checks)) {
|
|
629
|
+
throw new TypeError("Sloppy health checks option must be an array when provided.");
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const checks = (options.checks ?? []).map(normalizeHealthCheck);
|
|
633
|
+
|
|
634
|
+
return Object.freeze({
|
|
635
|
+
path,
|
|
636
|
+
livenessPath,
|
|
637
|
+
readinessPath,
|
|
638
|
+
checks: Object.freeze(checks),
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function checkAppliesToMode(check, mode) {
|
|
643
|
+
if (mode === "liveness") {
|
|
644
|
+
return check.liveness;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (mode === "readiness") {
|
|
648
|
+
return check.readiness;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function healthCheckNames(checks, predicate) {
|
|
655
|
+
return Object.freeze(checks.filter(predicate).map((check) => check.name));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function normalizeHealthCheckResult(result) {
|
|
659
|
+
if (result === undefined) {
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (typeof result === "boolean") {
|
|
664
|
+
return result;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (isPlainObject(result) && typeof result.ok === "boolean") {
|
|
668
|
+
return result.ok;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async function runHealthChecks(checks, mode, context) {
|
|
675
|
+
const selected = checks.filter((check) => checkAppliesToMode(check, mode));
|
|
676
|
+
const results = [];
|
|
677
|
+
let healthy = true;
|
|
678
|
+
|
|
679
|
+
for (const check of selected) {
|
|
680
|
+
try {
|
|
681
|
+
const ok = normalizeHealthCheckResult(await check.check(context));
|
|
682
|
+
healthy = healthy && ok;
|
|
683
|
+
results.push(Object.freeze({
|
|
684
|
+
name: check.name,
|
|
685
|
+
status: ok ? "healthy" : "unhealthy",
|
|
686
|
+
}));
|
|
687
|
+
} catch {
|
|
688
|
+
healthy = false;
|
|
689
|
+
results.push(Object.freeze({
|
|
690
|
+
name: check.name,
|
|
691
|
+
status: "unhealthy",
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return Object.freeze({
|
|
697
|
+
status: healthy ? "healthy" : "unhealthy",
|
|
698
|
+
checks: Object.freeze(results),
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function createHealthHandler(checks, mode) {
|
|
703
|
+
return async function healthHandler(context) {
|
|
704
|
+
const body = await runHealthChecks(checks, mode, context);
|
|
705
|
+
if (body.status === "healthy") {
|
|
706
|
+
return Results.ok(body);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return Results.status(503, body);
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function normalizeOpsPath(path, subject) {
|
|
714
|
+
validateHealthPath(path, subject);
|
|
715
|
+
if (path.length > 1 && path.endsWith("/")) {
|
|
716
|
+
throw new TypeError(`Sloppy ${subject} path must not end with '/'.`);
|
|
717
|
+
}
|
|
718
|
+
return path;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function joinOpsPath(root, child) {
|
|
722
|
+
if (root === "/") {
|
|
723
|
+
return child.startsWith("/") ? child : `/${child}`;
|
|
724
|
+
}
|
|
725
|
+
return child === "/" ? root : `${root}${child}`;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function normalizeHealthExposeOptions(options = undefined) {
|
|
729
|
+
if (options === undefined) {
|
|
730
|
+
return Object.freeze({
|
|
731
|
+
health: DEFAULT_HEALTH_PATH,
|
|
732
|
+
live: "/live",
|
|
733
|
+
ready: "/ready",
|
|
734
|
+
startup: DEFAULT_STARTUP_PATH,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
if (!isPlainObject(options)) {
|
|
738
|
+
throw new TypeError("Sloppy health expose options must be a plain object.");
|
|
739
|
+
}
|
|
740
|
+
return Object.freeze({
|
|
741
|
+
health: normalizeOpsPath(options.health ?? DEFAULT_HEALTH_PATH, "health"),
|
|
742
|
+
live: normalizeOpsPath(options.live ?? "/live", "liveness"),
|
|
743
|
+
ready: normalizeOpsPath(options.ready ?? "/ready", "readiness"),
|
|
744
|
+
startup: normalizeOpsPath(options.startup ?? DEFAULT_STARTUP_PATH, "startup"),
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function normalizeManagementOptions(options = undefined) {
|
|
749
|
+
if (options === undefined) {
|
|
750
|
+
return Object.freeze({
|
|
751
|
+
path: DEFAULT_MANAGEMENT_PATH,
|
|
752
|
+
health: true,
|
|
753
|
+
metrics: true,
|
|
754
|
+
info: true,
|
|
755
|
+
runtime: true,
|
|
756
|
+
protect: undefined,
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
if (!isPlainObject(options)) {
|
|
760
|
+
throw new TypeError("Sloppy app.management options must be a plain object.");
|
|
761
|
+
}
|
|
762
|
+
return Object.freeze({
|
|
763
|
+
path: normalizeOpsPath(options.path ?? DEFAULT_MANAGEMENT_PATH, "management"),
|
|
764
|
+
health: options.health !== false,
|
|
765
|
+
metrics: options.metrics !== false,
|
|
766
|
+
info: options.info !== false,
|
|
767
|
+
runtime: options.runtime !== false,
|
|
768
|
+
protect: options.protect,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function routeCountByKind(routes) {
|
|
773
|
+
const counts = { total: routes.length, health: 0, management: 0 };
|
|
774
|
+
for (const route of routes) {
|
|
775
|
+
if (route.metadata?.health !== undefined) {
|
|
776
|
+
counts.health += 1;
|
|
777
|
+
}
|
|
778
|
+
if (route.metadata?.management !== undefined) {
|
|
779
|
+
counts.management += 1;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return Object.freeze(counts);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function snapshotLifecycle(state) {
|
|
786
|
+
const now = Date.now();
|
|
787
|
+
return Object.freeze({
|
|
788
|
+
startupComplete: state.startupComplete,
|
|
789
|
+
shuttingDown: state.shuttingDown,
|
|
790
|
+
startedAtUtc: state.startedAtUtc,
|
|
791
|
+
uptimeSeconds: Math.max(0, (now - state.startedAtMs) / 1000),
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function normalizeDocsOptions(options = undefined) {
|
|
796
|
+
if (options === false || options?.enabled === false) {
|
|
797
|
+
return Object.freeze({ enabled: false });
|
|
798
|
+
}
|
|
799
|
+
if (options !== undefined && options !== true && !isPlainObject(options)) {
|
|
800
|
+
throw new TypeError("Sloppy app.docs options must be a plain object.");
|
|
801
|
+
}
|
|
802
|
+
const input = options === true || options === undefined ? {} : options;
|
|
803
|
+
const path = input.path ?? "/docs";
|
|
804
|
+
const openapiPath = input.openapiPath ?? "/openapi.json";
|
|
805
|
+
if (typeof path !== "string" || path.length === 0 || !path.startsWith("/") || path.endsWith("/")) {
|
|
806
|
+
throw new TypeError("Sloppy app.docs path must be an absolute path without a trailing slash.");
|
|
807
|
+
}
|
|
808
|
+
if (typeof openapiPath !== "string" || openapiPath.length === 0 || !openapiPath.startsWith("/")) {
|
|
809
|
+
throw new TypeError("Sloppy app.docs openapiPath must be an absolute path.");
|
|
810
|
+
}
|
|
811
|
+
if (typeof (input.title ?? "Sloppy API") !== "string") {
|
|
812
|
+
throw new TypeError("Sloppy app.docs title must be a string.");
|
|
813
|
+
}
|
|
814
|
+
if (input.strict !== undefined && typeof input.strict !== "boolean") {
|
|
815
|
+
throw new TypeError("Sloppy app.docs strict must be a boolean.");
|
|
816
|
+
}
|
|
817
|
+
if (input.enabled !== undefined && typeof input.enabled !== "boolean") {
|
|
818
|
+
throw new TypeError("Sloppy app.docs enabled must be a boolean.");
|
|
819
|
+
}
|
|
820
|
+
return Object.freeze({
|
|
821
|
+
enabled: true,
|
|
822
|
+
path,
|
|
823
|
+
openapiPath,
|
|
824
|
+
title: input.title ?? "Sloppy API",
|
|
825
|
+
strict: input.strict === true,
|
|
826
|
+
requireAuth: input.requireAuth,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function docsSchema(schema) {
|
|
831
|
+
if (schema === undefined) {
|
|
832
|
+
return { "x-slop-partial": "schema metadata missing" };
|
|
833
|
+
}
|
|
834
|
+
if (schema.kind === "object") {
|
|
835
|
+
const properties = {};
|
|
836
|
+
const required = [];
|
|
837
|
+
for (const [name, value] of Object.entries(schema.shape ?? {})) {
|
|
838
|
+
properties[name] = docsSchema(value);
|
|
839
|
+
if (value.optional !== true) {
|
|
840
|
+
required.push(name);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
type: "object",
|
|
845
|
+
properties,
|
|
846
|
+
...(required.length === 0 ? {} : { required }),
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
if (schema.kind === "array") {
|
|
850
|
+
return { type: "array", items: docsSchema(schema.item) };
|
|
851
|
+
}
|
|
852
|
+
if (schema.kind === "int") {
|
|
853
|
+
return { type: "integer" };
|
|
854
|
+
}
|
|
855
|
+
if (schema.kind === "number" || schema.kind === "boolean") {
|
|
856
|
+
return { type: schema.kind };
|
|
857
|
+
}
|
|
858
|
+
if (schema.kind === "null") {
|
|
859
|
+
return { nullable: true, "x-slop-partial": "null schema is not directly representable in OpenAPI 3.0.3" };
|
|
860
|
+
}
|
|
861
|
+
if (schema.kind === "enum") {
|
|
862
|
+
return { enum: schema.values ?? [] };
|
|
863
|
+
}
|
|
864
|
+
if (schema.kind === "literal") {
|
|
865
|
+
return { enum: [schema.value] };
|
|
866
|
+
}
|
|
867
|
+
const result = { type: "string" };
|
|
868
|
+
for (const rule of schema.rules ?? []) {
|
|
869
|
+
if (rule.kind === "email" || rule.kind === "uuid") {
|
|
870
|
+
result.format = rule.kind;
|
|
871
|
+
} else if (rule.kind === "min" || rule.kind === "minLength") {
|
|
872
|
+
result.minLength = rule.value;
|
|
873
|
+
} else if (rule.kind === "max" || rule.kind === "maxLength") {
|
|
874
|
+
result.maxLength = rule.value;
|
|
875
|
+
} else if (rule.kind === "pattern") {
|
|
876
|
+
result.pattern = rule.value.source;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (schema.nullable === true) {
|
|
880
|
+
result.nullable = true;
|
|
881
|
+
}
|
|
882
|
+
return result;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function docsPath(pattern) {
|
|
886
|
+
return pattern.replace(/\{([^}:]+)(?::[^}]+)?\}/gu, "{$1}");
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function docsOperation(route) {
|
|
890
|
+
const missing = [];
|
|
891
|
+
const metadata = route.metadata ?? {};
|
|
892
|
+
const responses = metadata.responses ?? (metadata.returns === undefined ? [] : [metadata.returns]);
|
|
893
|
+
if (responses.length === 0) {
|
|
894
|
+
missing.push("response.schema");
|
|
895
|
+
}
|
|
896
|
+
if (metadata.accepts !== undefined && metadata.accepts.schema === undefined) {
|
|
897
|
+
missing.push("request.schema");
|
|
898
|
+
}
|
|
899
|
+
const operation = {
|
|
900
|
+
operationId: route.name ?? undefined,
|
|
901
|
+
summary: metadata.summary,
|
|
902
|
+
description: metadata.description,
|
|
903
|
+
tags: metadata.tags,
|
|
904
|
+
deprecated: metadata.deprecated === false ? undefined : metadata.deprecated !== undefined,
|
|
905
|
+
parameters: route.params.map((param) => ({
|
|
906
|
+
name: param.name,
|
|
907
|
+
in: "path",
|
|
908
|
+
required: true,
|
|
909
|
+
schema: { type: param.kind === "int" ? "integer" : param.kind === "float" ? "number" : "string" },
|
|
910
|
+
"x-slop-constraint": param.kind,
|
|
911
|
+
})),
|
|
912
|
+
responses: Object.fromEntries((responses.length === 0 ? [{ status: 200 }] : responses).map((response) => [
|
|
913
|
+
String(response.status ?? 200),
|
|
914
|
+
{
|
|
915
|
+
description: response.description ?? "response",
|
|
916
|
+
...(response.schema === undefined ? {} : {
|
|
917
|
+
content: {
|
|
918
|
+
[response.contentType ?? "application/json"]: {
|
|
919
|
+
schema: docsSchema(response.schema),
|
|
920
|
+
},
|
|
921
|
+
},
|
|
922
|
+
}),
|
|
923
|
+
},
|
|
924
|
+
])),
|
|
925
|
+
...(metadata.realtime === undefined ? {} : {
|
|
926
|
+
"x-slop-realtime": {
|
|
927
|
+
kind: metadata.realtime.kind,
|
|
928
|
+
channel: metadata.realtime.channel?.name,
|
|
929
|
+
transport: "websocket",
|
|
930
|
+
},
|
|
931
|
+
"x-slop-transport": "websocket",
|
|
932
|
+
}),
|
|
933
|
+
"x-slop-completeness": missing.length === 0 ? "complete" : "partial",
|
|
934
|
+
...(missing.length === 0 ? {} : { "x-slop-missing": missing }),
|
|
935
|
+
};
|
|
936
|
+
if (metadata.accepts !== undefined) {
|
|
937
|
+
operation.requestBody = {
|
|
938
|
+
required: metadata.accepts.required !== false,
|
|
939
|
+
content: {
|
|
940
|
+
[metadata.accepts.contentType ?? "application/json"]: {
|
|
941
|
+
schema: docsSchema(metadata.accepts.schema),
|
|
942
|
+
},
|
|
943
|
+
},
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
if (metadata.openapi !== undefined) {
|
|
947
|
+
return metadata.openapi;
|
|
948
|
+
}
|
|
949
|
+
return operation;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function docsOperationComplete(operation, manualOverride = false) {
|
|
953
|
+
if (operation["x-slop-completeness"] === "complete") {
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
return manualOverride &&
|
|
957
|
+
operation.responses !== undefined &&
|
|
958
|
+
operation.responses !== null &&
|
|
959
|
+
typeof operation.responses === "object" &&
|
|
960
|
+
Object.keys(operation.responses).length > 0;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function buildDocsOpenApi(routes, options) {
|
|
964
|
+
const paths = {};
|
|
965
|
+
const missing = [];
|
|
966
|
+
let operationsPartial = 0;
|
|
967
|
+
let operationsComplete = 0;
|
|
968
|
+
for (const route of routes) {
|
|
969
|
+
if (route.metadata?.docsInternal === true) {
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
const path = docsPath(route.pattern);
|
|
973
|
+
const method = route.method.toLowerCase();
|
|
974
|
+
const routeSnapshot = snapshotRoute(route);
|
|
975
|
+
const operation = docsOperation(routeSnapshot);
|
|
976
|
+
if (docsOperationComplete(operation, routeSnapshot.metadata?.openapi !== undefined)) {
|
|
977
|
+
operationsComplete += 1;
|
|
978
|
+
} else {
|
|
979
|
+
operationsPartial += 1;
|
|
980
|
+
for (const reason of operation["x-slop-missing"] ?? []) {
|
|
981
|
+
missing.push({
|
|
982
|
+
method: route.method,
|
|
983
|
+
path: route.pattern,
|
|
984
|
+
reason,
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
paths[path] ??= {};
|
|
989
|
+
paths[path][method] = operation;
|
|
990
|
+
}
|
|
991
|
+
if (options.strict === true && operationsPartial !== 0) {
|
|
992
|
+
throw new TypeError("Sloppy app.docs strict mode requires complete route contracts.");
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
openapi: "3.0.3",
|
|
996
|
+
info: { title: options.title, version: "0.0.0" },
|
|
997
|
+
"x-slop-openapi-policy": {
|
|
998
|
+
mode: operationsPartial === 0 ? "complete" : "partial",
|
|
999
|
+
routesTotal: operationsComplete + operationsPartial,
|
|
1000
|
+
routesIncluded: operationsComplete + operationsPartial,
|
|
1001
|
+
routesOmitted: 0,
|
|
1002
|
+
operationsComplete,
|
|
1003
|
+
operationsPartial,
|
|
1004
|
+
missing,
|
|
1005
|
+
},
|
|
1006
|
+
paths,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function escapeHtml(value) {
|
|
1011
|
+
return String(value)
|
|
1012
|
+
.replaceAll("&", "&")
|
|
1013
|
+
.replaceAll("<", "<")
|
|
1014
|
+
.replaceAll(">", ">")
|
|
1015
|
+
.replaceAll('"', """);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function docsHtml(options) {
|
|
1019
|
+
const title = escapeHtml(options.title);
|
|
1020
|
+
return `<!doctype html>
|
|
1021
|
+
<html lang="en">
|
|
1022
|
+
<head><meta charset="utf-8"><title>${title}</title>
|
|
1023
|
+
<style>body{font:14px/1.45 system-ui,sans-serif;margin:0;color:#17202a}header{padding:16px 20px;border-bottom:1px solid #d8dee4}main{padding:20px}.warn{display:none;background:#fff4ce;border:1px solid #d29922;padding:10px;margin:0 0 16px}pre{white-space:pre-wrap;background:#f6f8fa;padding:16px;border:1px solid #d8dee4}</style></head>
|
|
1024
|
+
<body><header><h1>${title}</h1></header><main><div id="warn" class="warn">This OpenAPI spec is partial. Missing metadata is marked with x-slop-* fields.</div><pre id="spec">Loading OpenAPI...</pre></main>
|
|
1025
|
+
<script>const byId=(id)=>globalThis["doc"+"ument"]["get"+"ElementById"](id);fetch(${JSON.stringify(options.openapiPath)}).then(r=>r.json()).then(j=>{if(j["x-slop-openapi-policy"]?.mode==="partial")byId("warn").style.display="block";byId("spec").textContent=JSON.stringify(j,null,2);}).catch(e=>{byId("spec").textContent=String(e);});</script></body></html>`;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function attachCacheMetrics(value, metricsRegistry) {
|
|
1029
|
+
if (isCache(value) && typeof value.__setMetricsRegistry === "function") {
|
|
1030
|
+
value.__setMetricsRegistry(metricsRegistry);
|
|
1031
|
+
}
|
|
1032
|
+
return value;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function createMetricsAwareServices(services, metricsRegistry) {
|
|
1036
|
+
return Object.freeze({
|
|
1037
|
+
get(token) {
|
|
1038
|
+
return attachCacheMetrics(services.get(token), metricsRegistry);
|
|
1039
|
+
},
|
|
1040
|
+
tryGet(token) {
|
|
1041
|
+
return attachCacheMetrics(services.tryGet(token), metricsRegistry);
|
|
1042
|
+
},
|
|
1043
|
+
createScope() {
|
|
1044
|
+
const scope = services.createScope();
|
|
1045
|
+
return Object.freeze({
|
|
1046
|
+
...scope,
|
|
1047
|
+
get(token) {
|
|
1048
|
+
return attachCacheMetrics(scope.get(token), metricsRegistry);
|
|
1049
|
+
},
|
|
1050
|
+
tryGet(token) {
|
|
1051
|
+
return attachCacheMetrics(scope.tryGet(token), metricsRegistry);
|
|
1052
|
+
},
|
|
1053
|
+
});
|
|
1054
|
+
},
|
|
1055
|
+
addCache(cache, name = undefined) {
|
|
1056
|
+
attachCacheMetrics(cache, metricsRegistry);
|
|
1057
|
+
services.addCache(cache, name);
|
|
1058
|
+
return this;
|
|
1059
|
+
},
|
|
1060
|
+
addHttpClient(clientOrName, options = undefined) {
|
|
1061
|
+
services.addHttpClient(clientOrName, options);
|
|
1062
|
+
return this;
|
|
1063
|
+
},
|
|
1064
|
+
dispose() {
|
|
1065
|
+
return services.dispose();
|
|
1066
|
+
},
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function createApp(host) {
|
|
1071
|
+
const routes = [];
|
|
1072
|
+
const staticAssets = [];
|
|
1073
|
+
const workerResources = [];
|
|
1074
|
+
const middleware = [];
|
|
1075
|
+
const middlewareSequence = { value: 0 };
|
|
1076
|
+
function nextMiddlewareSequence() {
|
|
1077
|
+
const seq = middlewareSequence.value;
|
|
1078
|
+
middlewareSequence.value = seq + 1;
|
|
1079
|
+
return seq;
|
|
1080
|
+
}
|
|
1081
|
+
const guard = createMutationGuard("app");
|
|
1082
|
+
let corsPolicy = null;
|
|
1083
|
+
let currentModule = null;
|
|
1084
|
+
const moduleDebugRef = host.moduleDebugRef ?? { modules: Object.freeze([]) };
|
|
1085
|
+
const directModules = new Set();
|
|
1086
|
+
const errorPolicyState = {
|
|
1087
|
+
policy: null,
|
|
1088
|
+
mappings: [],
|
|
1089
|
+
};
|
|
1090
|
+
let jsonOptions = host.options.json;
|
|
1091
|
+
let contentNegotiation = host.options.contentNegotiation;
|
|
1092
|
+
const authState = createAuthState();
|
|
1093
|
+
const metricsRegistry = Metrics.createRegistry();
|
|
1094
|
+
const services = createMetricsAwareServices(host.services, metricsRegistry);
|
|
1095
|
+
const opsHealthRegistry = Health.createRegistry();
|
|
1096
|
+
const defaultRateLimitStore = RateLimit.memory({ name: "default" });
|
|
1097
|
+
const rateLimitStores = new Map([
|
|
1098
|
+
["default", defaultRateLimitStore],
|
|
1099
|
+
["memory", defaultRateLimitStore],
|
|
1100
|
+
]);
|
|
1101
|
+
const rateLimitServices = Object.freeze({
|
|
1102
|
+
...services,
|
|
1103
|
+
addRateLimitStore(nameOrStore, maybeStore = undefined) {
|
|
1104
|
+
assertAppMutable();
|
|
1105
|
+
const name = typeof nameOrStore === "string" ? nameOrStore : nameOrStore?.name;
|
|
1106
|
+
const store = typeof nameOrStore === "string" ? maybeStore : nameOrStore;
|
|
1107
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
1108
|
+
throw new TypeError("Sloppy services.addRateLimitStore name must be a non-empty string.");
|
|
1109
|
+
}
|
|
1110
|
+
if (!isRateLimitStore(store)) {
|
|
1111
|
+
throw new TypeError("Sloppy services.addRateLimitStore expects a RateLimit store.");
|
|
1112
|
+
}
|
|
1113
|
+
if (rateLimitStores.has(name)) {
|
|
1114
|
+
throw new Error(`Sloppy rate-limit store '${name}' is already registered.`);
|
|
1115
|
+
}
|
|
1116
|
+
rateLimitStores.set(name, store);
|
|
1117
|
+
if (store.kind === "redis" && !rateLimitStores.has("redis")) {
|
|
1118
|
+
rateLimitStores.set("redis", store);
|
|
1119
|
+
}
|
|
1120
|
+
return rateLimitServices;
|
|
1121
|
+
},
|
|
1122
|
+
__setRateLimitStore(name, store) {
|
|
1123
|
+
assertAppMutable();
|
|
1124
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
1125
|
+
throw new TypeError("Sloppy services.__setRateLimitStore name must be a non-empty string.");
|
|
1126
|
+
}
|
|
1127
|
+
if (!isRateLimitStore(store)) {
|
|
1128
|
+
throw new TypeError("Sloppy services.__setRateLimitStore expects a RateLimit store.");
|
|
1129
|
+
}
|
|
1130
|
+
rateLimitStores.set(name, store);
|
|
1131
|
+
if (name === "default") {
|
|
1132
|
+
rateLimitStores.set("memory", store);
|
|
1133
|
+
}
|
|
1134
|
+
if (store.kind === "redis") {
|
|
1135
|
+
rateLimitStores.set("redis", store);
|
|
1136
|
+
}
|
|
1137
|
+
return rateLimitServices;
|
|
1138
|
+
},
|
|
1139
|
+
__getRateLimitStore(name = "default") {
|
|
1140
|
+
return rateLimitStores.get(name);
|
|
1141
|
+
},
|
|
1142
|
+
__getRateLimitStores() {
|
|
1143
|
+
return new Map(rateLimitStores);
|
|
1144
|
+
},
|
|
1145
|
+
__resetRateLimitStores() {
|
|
1146
|
+
for (const store of new Set(rateLimitStores.values())) {
|
|
1147
|
+
store.reset?.();
|
|
1148
|
+
}
|
|
1149
|
+
},
|
|
1150
|
+
});
|
|
1151
|
+
const lifecycleState = {
|
|
1152
|
+
startupComplete: false,
|
|
1153
|
+
shuttingDown: false,
|
|
1154
|
+
startedAtUtc: new Date().toISOString(),
|
|
1155
|
+
startedAtMs: Date.now(),
|
|
1156
|
+
};
|
|
1157
|
+
let opsHealthExposed = false;
|
|
1158
|
+
let managementExposed = false;
|
|
1159
|
+
const routeHost = {
|
|
1160
|
+
...host,
|
|
1161
|
+
services: rateLimitServices,
|
|
1162
|
+
rateLimitStores,
|
|
1163
|
+
auth: authState,
|
|
1164
|
+
handleError(error, context) {
|
|
1165
|
+
if (errorPolicyState.policy === null) {
|
|
1166
|
+
if (isValidationError(error)) {
|
|
1167
|
+
return validationProblemResult(error, context);
|
|
1168
|
+
}
|
|
1169
|
+
throw error;
|
|
1170
|
+
}
|
|
1171
|
+
return errorPolicyResult(error, context, errorPolicyState, host.config);
|
|
1172
|
+
},
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
function assertAppMutable() {
|
|
1176
|
+
guard.assertMutable();
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function getCurrentModule() {
|
|
1180
|
+
return currentModule;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function getCorsPolicy() {
|
|
1184
|
+
return corsPolicy;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function ensureOpsRouteFree(pattern) {
|
|
1188
|
+
const conflict = routes.find((route) => route.method === "GET" && route.pattern === pattern);
|
|
1189
|
+
if (conflict !== undefined) {
|
|
1190
|
+
throw new Error(`Sloppy route 'GET ${pattern}' is already registered.`);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function registerOpsGet(pattern, metadata, handler, name) {
|
|
1195
|
+
ensureOpsRouteFree(pattern);
|
|
1196
|
+
const endpoint = registerRoute(
|
|
1197
|
+
routes,
|
|
1198
|
+
routeHost,
|
|
1199
|
+
assertAppMutable,
|
|
1200
|
+
currentModule,
|
|
1201
|
+
"GET",
|
|
1202
|
+
pattern,
|
|
1203
|
+
metadata,
|
|
1204
|
+
handler,
|
|
1205
|
+
undefined,
|
|
1206
|
+
middleware,
|
|
1207
|
+
corsPolicy,
|
|
1208
|
+
);
|
|
1209
|
+
if (name !== undefined) {
|
|
1210
|
+
endpoint.withName(name);
|
|
1211
|
+
}
|
|
1212
|
+
return endpoint;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function exposeOpsHealth(options = undefined, metadataBase = undefined, pathPrefix = undefined, wrapHandler = (handler) => handler) {
|
|
1216
|
+
const health = normalizeHealthExposeOptions(options);
|
|
1217
|
+
const paths = {
|
|
1218
|
+
health: pathPrefix === undefined ? health.health : joinOpsPath(pathPrefix, "/health"),
|
|
1219
|
+
live: pathPrefix === undefined ? health.live : joinOpsPath(pathPrefix, "/live"),
|
|
1220
|
+
ready: pathPrefix === undefined ? health.ready : joinOpsPath(pathPrefix, "/ready"),
|
|
1221
|
+
startup: pathPrefix === undefined ? health.startup : joinOpsPath(pathPrefix, "/startup"),
|
|
1222
|
+
};
|
|
1223
|
+
for (const path of Object.values(paths)) {
|
|
1224
|
+
ensureOpsRouteFree(path);
|
|
1225
|
+
}
|
|
1226
|
+
registerOpsGet(paths.health, {
|
|
1227
|
+
...metadataBase,
|
|
1228
|
+
health: "aggregate",
|
|
1229
|
+
checks: opsHealthRegistry.checks().map((check) => check.name),
|
|
1230
|
+
}, wrapHandler(createOpsHealthHandler(opsHealthRegistry, "health")), metadataBase?.management ? "Management.Health" : "Health");
|
|
1231
|
+
registerOpsGet(paths.live, {
|
|
1232
|
+
...metadataBase,
|
|
1233
|
+
health: "liveness",
|
|
1234
|
+
checks: opsHealthRegistry.checks().filter((check) => check.tags.includes("live")).map((check) => check.name),
|
|
1235
|
+
}, wrapHandler(createOpsHealthHandler(opsHealthRegistry, "live")), metadataBase?.management ? "Management.Live" : "Health.Live");
|
|
1236
|
+
registerOpsGet(paths.ready, {
|
|
1237
|
+
...metadataBase,
|
|
1238
|
+
health: "readiness",
|
|
1239
|
+
checks: opsHealthRegistry.checks().filter((check) => check.tags.includes("ready")).map((check) => check.name),
|
|
1240
|
+
}, wrapHandler(createOpsHealthHandler(opsHealthRegistry, "ready")), metadataBase?.management ? "Management.Ready" : "Health.Ready");
|
|
1241
|
+
registerOpsGet(paths.startup, {
|
|
1242
|
+
...metadataBase,
|
|
1243
|
+
health: "startup",
|
|
1244
|
+
checks: opsHealthRegistry.checks().filter((check) => check.tags.includes("startup")).map((check) => check.name),
|
|
1245
|
+
}, wrapHandler(createOpsHealthHandler(opsHealthRegistry, "startup")), metadataBase?.management ? "Management.Startup" : "Health.Startup");
|
|
1246
|
+
opsHealthExposed = true;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const healthBuilder = Object.freeze({
|
|
1250
|
+
check(name, check, options = undefined) {
|
|
1251
|
+
assertAppMutable();
|
|
1252
|
+
opsHealthRegistry.check(name, check, options);
|
|
1253
|
+
return healthBuilder;
|
|
1254
|
+
},
|
|
1255
|
+
expose(options = undefined) {
|
|
1256
|
+
assertAppMutable();
|
|
1257
|
+
exposeOpsHealth(options);
|
|
1258
|
+
return app;
|
|
1259
|
+
},
|
|
1260
|
+
registry() {
|
|
1261
|
+
return opsHealthRegistry;
|
|
1262
|
+
},
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
const app = {
|
|
1266
|
+
config: host.config,
|
|
1267
|
+
log: host.log,
|
|
1268
|
+
services: rateLimitServices,
|
|
1269
|
+
capabilities: host.capabilities,
|
|
1270
|
+
metrics: metricsRegistry,
|
|
1271
|
+
auth: Object.freeze({
|
|
1272
|
+
addPolicy(name, policy) {
|
|
1273
|
+
assertAppMutable();
|
|
1274
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
1275
|
+
throw new TypeError("Sloppy auth policy name must be a non-empty string.");
|
|
1276
|
+
}
|
|
1277
|
+
if (authState.policies.has(name)) {
|
|
1278
|
+
throw new TypeError(`Sloppy auth policy '${name}' is already registered.`);
|
|
1279
|
+
}
|
|
1280
|
+
authState.policies.set(name, normalizeAuthPolicy(policy));
|
|
1281
|
+
return app.auth;
|
|
1282
|
+
},
|
|
1283
|
+
}),
|
|
1284
|
+
|
|
1285
|
+
health() {
|
|
1286
|
+
return healthBuilder;
|
|
1287
|
+
},
|
|
1288
|
+
|
|
1289
|
+
management(options = undefined) {
|
|
1290
|
+
assertAppMutable();
|
|
1291
|
+
if (managementExposed) {
|
|
1292
|
+
throw new Error("Sloppy management endpoints are already registered.");
|
|
1293
|
+
}
|
|
1294
|
+
const management = normalizeManagementOptions(options);
|
|
1295
|
+
const protect = management.protect;
|
|
1296
|
+
if (protect !== undefined && typeof protect !== "function") {
|
|
1297
|
+
throw new TypeError("Sloppy management protect hook must be a function.");
|
|
1298
|
+
}
|
|
1299
|
+
function protectedHandler(handler) {
|
|
1300
|
+
return async (context) => {
|
|
1301
|
+
if (protect !== undefined && await protect(context) !== true) {
|
|
1302
|
+
return Results.status(403, {
|
|
1303
|
+
status: 403,
|
|
1304
|
+
title: "Forbidden",
|
|
1305
|
+
code: "SLOPPY_E_MANAGEMENT_FORBIDDEN",
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
return handler(context);
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
function refreshRuntimeMetrics() {
|
|
1312
|
+
const lifecycle = snapshotLifecycle(lifecycleState);
|
|
1313
|
+
metricsRegistry.gauge("runtime.uptime.seconds", { description: "Runtime uptime in seconds." }).set(lifecycle.uptimeSeconds);
|
|
1314
|
+
metricsRegistry.gauge("runtime.shutdown.state", { description: "Runtime shutdown state as 0 or 1." }).set(lifecycle.shuttingDown ? 1 : 0);
|
|
1315
|
+
metricsRegistry.gauge("routing.route_table.size", { description: "Registered app-host route count." }).set(routes.length);
|
|
1316
|
+
const memory = globalThis.process?.memoryUsage?.();
|
|
1317
|
+
if (memory !== undefined) {
|
|
1318
|
+
metricsRegistry.gauge("runtime.memory.rss.bytes", { description: "Resident set size in bytes." }).set(memory.rss);
|
|
1319
|
+
metricsRegistry.gauge("runtime.memory.heap.bytes", { description: "Heap bytes in use." }).set(memory.heapUsed);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (opsHealthRegistry.checks().length === 0) {
|
|
1323
|
+
opsHealthRegistry
|
|
1324
|
+
.check("self", Health.self(), { tags: ["live", "ready", "startup"], critical: true, cacheMs: 250 })
|
|
1325
|
+
.check("runtime", Health.runtime(), { tags: ["ready", "startup"], critical: true, cacheMs: 250 })
|
|
1326
|
+
.check("memory", Health.memory(), { tags: ["health"], critical: false, cacheMs: 1000 });
|
|
1327
|
+
}
|
|
1328
|
+
if (management.health) {
|
|
1329
|
+
exposeOpsHealth(undefined, { management: "health" }, management.path, protectedHandler);
|
|
1330
|
+
}
|
|
1331
|
+
if (management.metrics) {
|
|
1332
|
+
registerOpsGet(
|
|
1333
|
+
joinOpsPath(management.path, "/metrics"),
|
|
1334
|
+
{ management: "metrics", format: "prometheus" },
|
|
1335
|
+
protectedHandler(() => {
|
|
1336
|
+
refreshRuntimeMetrics();
|
|
1337
|
+
return Results.text(metricsRegistry.renderPrometheus(), { contentType: PROMETHEUS_CONTENT_TYPE });
|
|
1338
|
+
}),
|
|
1339
|
+
"Management.Metrics",
|
|
1340
|
+
);
|
|
1341
|
+
registerOpsGet(
|
|
1342
|
+
joinOpsPath(management.path, "/metrics.json"),
|
|
1343
|
+
{ management: "metrics", format: "json" },
|
|
1344
|
+
protectedHandler(() => {
|
|
1345
|
+
refreshRuntimeMetrics();
|
|
1346
|
+
return Results.json(metricsRegistry.snapshot());
|
|
1347
|
+
}),
|
|
1348
|
+
"Management.MetricsJson",
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
if (management.info) {
|
|
1352
|
+
registerOpsGet(
|
|
1353
|
+
joinOpsPath(management.path, "/info"),
|
|
1354
|
+
{ management: "info" },
|
|
1355
|
+
protectedHandler(() => Results.ok({
|
|
1356
|
+
app: {
|
|
1357
|
+
name: host.config.get("Sloppy:AppName", host.config.get("App:Name", "sloppy-app")),
|
|
1358
|
+
version: host.config.get("Sloppy:AppVersion", host.config.get("App:Version", "0.0.0")),
|
|
1359
|
+
},
|
|
1360
|
+
sloppy: {
|
|
1361
|
+
runtime: "bootstrap",
|
|
1362
|
+
operations: "health-metrics-management",
|
|
1363
|
+
},
|
|
1364
|
+
security: {
|
|
1365
|
+
protected: protect !== undefined,
|
|
1366
|
+
detailedEndpoints: true,
|
|
1367
|
+
},
|
|
1368
|
+
})),
|
|
1369
|
+
"Management.Info",
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
if (management.runtime) {
|
|
1373
|
+
registerOpsGet(
|
|
1374
|
+
joinOpsPath(management.path, "/runtime"),
|
|
1375
|
+
{ management: "runtime" },
|
|
1376
|
+
protectedHandler(() => Results.ok({
|
|
1377
|
+
routes: routeCountByKind(routes),
|
|
1378
|
+
lifecycle: snapshotLifecycle(lifecycleState),
|
|
1379
|
+
providers: routes.filter((route) => route.metadata?.provider !== undefined).length,
|
|
1380
|
+
jobs: {
|
|
1381
|
+
resources: workerResources.length,
|
|
1382
|
+
},
|
|
1383
|
+
health: {
|
|
1384
|
+
checks: opsHealthRegistry.checks(),
|
|
1385
|
+
exposed: opsHealthExposed,
|
|
1386
|
+
},
|
|
1387
|
+
metrics: metricsRegistry.snapshot(),
|
|
1388
|
+
security: {
|
|
1389
|
+
protected: protect !== undefined,
|
|
1390
|
+
},
|
|
1391
|
+
})),
|
|
1392
|
+
"Management.Runtime",
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
managementExposed = true;
|
|
1396
|
+
return app;
|
|
1397
|
+
},
|
|
1398
|
+
|
|
1399
|
+
use(provider) {
|
|
1400
|
+
assertAppMutable();
|
|
1401
|
+
if (isProblemDetailsDescriptor(provider)) {
|
|
1402
|
+
errorPolicyState.policy = provider.__sloppyProblemDetails === true
|
|
1403
|
+
? normalizeLegacyProblemDetails(provider, host.config)
|
|
1404
|
+
: normalizeErrorPolicyOptions(provider, host.config);
|
|
1405
|
+
return app;
|
|
1406
|
+
}
|
|
1407
|
+
if (typeof provider === "function") {
|
|
1408
|
+
middleware.push({ fn: provider, sequence: nextMiddlewareSequence() });
|
|
1409
|
+
return app;
|
|
1410
|
+
}
|
|
1411
|
+
if (isAuthProviderDescriptor(provider)) {
|
|
1412
|
+
middleware.push({
|
|
1413
|
+
fn: registerAuthProvider(authState, provider, host.config),
|
|
1414
|
+
sequence: nextMiddlewareSequence(),
|
|
1415
|
+
});
|
|
1416
|
+
return app;
|
|
1417
|
+
}
|
|
1418
|
+
if (isWorkerResource(provider)) {
|
|
1419
|
+
if (workerResources.includes(provider)) {
|
|
1420
|
+
return provider;
|
|
1421
|
+
}
|
|
1422
|
+
if (typeof provider.__sloppyStartForApp === "function") {
|
|
1423
|
+
provider.__sloppyStartForApp(app);
|
|
1424
|
+
}
|
|
1425
|
+
workerResources.push(provider);
|
|
1426
|
+
return provider;
|
|
1427
|
+
}
|
|
1428
|
+
validateProviderDescriptor(provider);
|
|
1429
|
+
|
|
1430
|
+
const configured = host.config.bind(`${provider.kind}:${provider.name}`);
|
|
1431
|
+
const options = Object.freeze({
|
|
1432
|
+
...configured,
|
|
1433
|
+
...(provider.options ?? {}),
|
|
1434
|
+
});
|
|
1435
|
+
validateMergedProviderOptions(provider, options);
|
|
1436
|
+
return Object.freeze({
|
|
1437
|
+
kind: provider.kind,
|
|
1438
|
+
name: provider.name,
|
|
1439
|
+
token: sqliteProviderToken(provider.name),
|
|
1440
|
+
options,
|
|
1441
|
+
});
|
|
1442
|
+
},
|
|
1443
|
+
|
|
1444
|
+
useErrors(options = undefined) {
|
|
1445
|
+
assertAppMutable();
|
|
1446
|
+
errorPolicyState.policy = normalizeErrorPolicyOptions(options, host.config);
|
|
1447
|
+
return app;
|
|
1448
|
+
},
|
|
1449
|
+
|
|
1450
|
+
mapError(type, mapper) {
|
|
1451
|
+
assertAppMutable();
|
|
1452
|
+
if (typeof type !== "function") {
|
|
1453
|
+
throw new TypeError("Sloppy app.mapError type must be an Error constructor.");
|
|
1454
|
+
}
|
|
1455
|
+
if (typeof mapper !== "function") {
|
|
1456
|
+
throw new TypeError("Sloppy app.mapError mapper must be a function.");
|
|
1457
|
+
}
|
|
1458
|
+
errorPolicyState.mappings.push(Object.freeze({ type, mapper }));
|
|
1459
|
+
return app;
|
|
1460
|
+
},
|
|
1461
|
+
|
|
1462
|
+
useCors(policy) {
|
|
1463
|
+
assertAppMutable();
|
|
1464
|
+
corsPolicy = normalizeCorsPolicy(policy);
|
|
1465
|
+
return app;
|
|
1466
|
+
},
|
|
1467
|
+
|
|
1468
|
+
cors(policy) {
|
|
1469
|
+
return app.useCors(policy);
|
|
1470
|
+
},
|
|
1471
|
+
|
|
1472
|
+
securityHeaders(options = undefined) {
|
|
1473
|
+
assertAppMutable();
|
|
1474
|
+
middleware.push({
|
|
1475
|
+
fn: securityHeadersMiddleware(normalizeSecurityHeadersOptions(options)),
|
|
1476
|
+
sequence: nextMiddlewareSequence(),
|
|
1477
|
+
});
|
|
1478
|
+
return app;
|
|
1479
|
+
},
|
|
1480
|
+
|
|
1481
|
+
useSecurityHeaders(options = undefined) {
|
|
1482
|
+
return app.securityHeaders(options);
|
|
1483
|
+
},
|
|
1484
|
+
|
|
1485
|
+
useStaticFiles(options) {
|
|
1486
|
+
assertAppMutable();
|
|
1487
|
+
staticAssets.push(validateStaticFilesOptions(options));
|
|
1488
|
+
return app;
|
|
1489
|
+
},
|
|
1490
|
+
|
|
1491
|
+
staticFiles(mount, options) {
|
|
1492
|
+
assertAppMutable();
|
|
1493
|
+
staticAssets.push(normalizeStaticFileOptions(mount, options));
|
|
1494
|
+
return app;
|
|
1495
|
+
},
|
|
1496
|
+
|
|
1497
|
+
spa(mount, options) {
|
|
1498
|
+
assertAppMutable();
|
|
1499
|
+
staticAssets.push(normalizeSpaOptions(mount, options));
|
|
1500
|
+
return app;
|
|
1501
|
+
},
|
|
1502
|
+
|
|
1503
|
+
useJson(options) {
|
|
1504
|
+
assertAppMutable();
|
|
1505
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
1506
|
+
throw new TypeError("Sloppy JSON options must be a plain object.");
|
|
1507
|
+
}
|
|
1508
|
+
jsonOptions = normalizeJsonOptions({
|
|
1509
|
+
...jsonOptions,
|
|
1510
|
+
...(options ?? {}),
|
|
1511
|
+
});
|
|
1512
|
+
return app;
|
|
1513
|
+
},
|
|
1514
|
+
|
|
1515
|
+
useContentNegotiation(options) {
|
|
1516
|
+
assertAppMutable();
|
|
1517
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
1518
|
+
throw new TypeError("Sloppy content negotiation options must be a plain object.");
|
|
1519
|
+
}
|
|
1520
|
+
contentNegotiation = normalizeContentNegotiationOptions({
|
|
1521
|
+
...contentNegotiation,
|
|
1522
|
+
...(options ?? {}),
|
|
1523
|
+
});
|
|
1524
|
+
return app;
|
|
1525
|
+
},
|
|
1526
|
+
|
|
1527
|
+
docs(options = undefined) {
|
|
1528
|
+
assertAppMutable();
|
|
1529
|
+
const docs = normalizeDocsOptions(options);
|
|
1530
|
+
if (!docs.enabled) {
|
|
1531
|
+
return app;
|
|
1532
|
+
}
|
|
1533
|
+
const metadata = {
|
|
1534
|
+
docsInternal: true,
|
|
1535
|
+
tags: ["Documentation"],
|
|
1536
|
+
};
|
|
1537
|
+
const openapiEndpoint = registerRoute(
|
|
1538
|
+
routes,
|
|
1539
|
+
routeHost,
|
|
1540
|
+
assertAppMutable,
|
|
1541
|
+
currentModule,
|
|
1542
|
+
"GET",
|
|
1543
|
+
docs.openapiPath,
|
|
1544
|
+
metadata,
|
|
1545
|
+
() => Results.json(buildDocsOpenApi(routes, docs)),
|
|
1546
|
+
undefined,
|
|
1547
|
+
middleware,
|
|
1548
|
+
corsPolicy,
|
|
1549
|
+
).withName("Docs.OpenApi");
|
|
1550
|
+
const docsEndpoint = registerRoute(
|
|
1551
|
+
routes,
|
|
1552
|
+
routeHost,
|
|
1553
|
+
assertAppMutable,
|
|
1554
|
+
currentModule,
|
|
1555
|
+
"GET",
|
|
1556
|
+
docs.path,
|
|
1557
|
+
metadata,
|
|
1558
|
+
() => Results.html(docsHtml(docs)),
|
|
1559
|
+
undefined,
|
|
1560
|
+
middleware,
|
|
1561
|
+
corsPolicy,
|
|
1562
|
+
).withName("Docs.Ui");
|
|
1563
|
+
if (docs.requireAuth !== undefined) {
|
|
1564
|
+
openapiEndpoint.requireAuth(docs.requireAuth);
|
|
1565
|
+
docsEndpoint.requireAuth(docs.requireAuth);
|
|
1566
|
+
}
|
|
1567
|
+
return app;
|
|
1568
|
+
},
|
|
1569
|
+
|
|
1570
|
+
useModule(moduleOrFactory) {
|
|
1571
|
+
assertAppMutable();
|
|
1572
|
+
|
|
1573
|
+
const moduleState = getModuleState(moduleOrFactory);
|
|
1574
|
+
if (moduleState !== undefined) {
|
|
1575
|
+
assertRouteOnlyModule(moduleState);
|
|
1576
|
+
if (directModules.has(moduleState.name)) {
|
|
1577
|
+
throw new Error(`Sloppy module '${moduleState.name}' is already registered.`);
|
|
1578
|
+
}
|
|
1579
|
+
moduleState.finalized = true;
|
|
1580
|
+
directModules.add(moduleState.name);
|
|
1581
|
+
for (const callback of moduleState.routeCallbacks) {
|
|
1582
|
+
app.__runInModule(moduleState.name, (moduleApp) => {
|
|
1583
|
+
runModulePhase(moduleState, "routes", callback, moduleApp);
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
return app;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (typeof moduleOrFactory !== "function" || functionModuleName(moduleOrFactory).length === 0) {
|
|
1590
|
+
throw new TypeError(
|
|
1591
|
+
"Sloppy app.useModule expected a named function module or route-only Sloppy.module.",
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
const moduleName = functionModuleName(moduleOrFactory);
|
|
1595
|
+
if (directModules.has(moduleName)) {
|
|
1596
|
+
throw new Error(`Sloppy module '${moduleName}' is already registered.`);
|
|
1597
|
+
}
|
|
1598
|
+
directModules.add(moduleName);
|
|
1599
|
+
app.__runInModule(moduleName, (moduleApp) => {
|
|
1600
|
+
const result = moduleOrFactory(moduleApp);
|
|
1601
|
+
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
1602
|
+
throw new TypeError("Sloppy function modules must be synchronous.");
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
return app;
|
|
1606
|
+
},
|
|
1607
|
+
|
|
1608
|
+
mapGet(pattern, optionsOrHandler, maybeHandler) {
|
|
1609
|
+
return registerRoute(
|
|
1610
|
+
routes,
|
|
1611
|
+
routeHost,
|
|
1612
|
+
assertAppMutable,
|
|
1613
|
+
currentModule,
|
|
1614
|
+
"GET",
|
|
1615
|
+
pattern,
|
|
1616
|
+
optionsOrHandler,
|
|
1617
|
+
maybeHandler,
|
|
1618
|
+
undefined,
|
|
1619
|
+
middleware,
|
|
1620
|
+
corsPolicy,
|
|
1621
|
+
);
|
|
1622
|
+
},
|
|
1623
|
+
|
|
1624
|
+
mapPost(pattern, optionsOrHandler, maybeHandler) {
|
|
1625
|
+
return registerRoute(
|
|
1626
|
+
routes,
|
|
1627
|
+
routeHost,
|
|
1628
|
+
assertAppMutable,
|
|
1629
|
+
currentModule,
|
|
1630
|
+
"POST",
|
|
1631
|
+
pattern,
|
|
1632
|
+
optionsOrHandler,
|
|
1633
|
+
maybeHandler,
|
|
1634
|
+
undefined,
|
|
1635
|
+
middleware,
|
|
1636
|
+
corsPolicy,
|
|
1637
|
+
);
|
|
1638
|
+
},
|
|
1639
|
+
|
|
1640
|
+
mapPut(pattern, optionsOrHandler, maybeHandler) {
|
|
1641
|
+
return registerRoute(
|
|
1642
|
+
routes,
|
|
1643
|
+
routeHost,
|
|
1644
|
+
assertAppMutable,
|
|
1645
|
+
currentModule,
|
|
1646
|
+
"PUT",
|
|
1647
|
+
pattern,
|
|
1648
|
+
optionsOrHandler,
|
|
1649
|
+
maybeHandler,
|
|
1650
|
+
undefined,
|
|
1651
|
+
middleware,
|
|
1652
|
+
corsPolicy,
|
|
1653
|
+
);
|
|
1654
|
+
},
|
|
1655
|
+
|
|
1656
|
+
mapPatch(pattern, optionsOrHandler, maybeHandler) {
|
|
1657
|
+
return registerRoute(
|
|
1658
|
+
routes,
|
|
1659
|
+
routeHost,
|
|
1660
|
+
assertAppMutable,
|
|
1661
|
+
currentModule,
|
|
1662
|
+
"PATCH",
|
|
1663
|
+
pattern,
|
|
1664
|
+
optionsOrHandler,
|
|
1665
|
+
maybeHandler,
|
|
1666
|
+
undefined,
|
|
1667
|
+
middleware,
|
|
1668
|
+
corsPolicy,
|
|
1669
|
+
);
|
|
1670
|
+
},
|
|
1671
|
+
|
|
1672
|
+
mapDelete(pattern, optionsOrHandler, maybeHandler) {
|
|
1673
|
+
return registerRoute(
|
|
1674
|
+
routes,
|
|
1675
|
+
routeHost,
|
|
1676
|
+
assertAppMutable,
|
|
1677
|
+
currentModule,
|
|
1678
|
+
"DELETE",
|
|
1679
|
+
pattern,
|
|
1680
|
+
optionsOrHandler,
|
|
1681
|
+
maybeHandler,
|
|
1682
|
+
undefined,
|
|
1683
|
+
middleware,
|
|
1684
|
+
corsPolicy,
|
|
1685
|
+
);
|
|
1686
|
+
},
|
|
1687
|
+
|
|
1688
|
+
mapHealthChecks(options) {
|
|
1689
|
+
assertAppMutable();
|
|
1690
|
+
const health = normalizeHealthOptions(options);
|
|
1691
|
+
|
|
1692
|
+
const targets = [health.path, health.livenessPath, health.readinessPath];
|
|
1693
|
+
for (const target of targets) {
|
|
1694
|
+
const conflict = routes.find(
|
|
1695
|
+
(route) => route.method === "GET" && route.pattern === target,
|
|
1696
|
+
);
|
|
1697
|
+
if (conflict !== undefined) {
|
|
1698
|
+
throw new Error(`Sloppy route 'GET ${target}' is already registered.`);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
registerRoute(
|
|
1703
|
+
routes,
|
|
1704
|
+
routeHost,
|
|
1705
|
+
assertAppMutable,
|
|
1706
|
+
currentModule,
|
|
1707
|
+
"GET",
|
|
1708
|
+
health.path,
|
|
1709
|
+
{
|
|
1710
|
+
health: "aggregate",
|
|
1711
|
+
checks: healthCheckNames(health.checks, () => true),
|
|
1712
|
+
},
|
|
1713
|
+
createHealthHandler(health.checks, "aggregate"),
|
|
1714
|
+
undefined,
|
|
1715
|
+
middleware,
|
|
1716
|
+
corsPolicy,
|
|
1717
|
+
).withName("Health");
|
|
1718
|
+
|
|
1719
|
+
registerRoute(
|
|
1720
|
+
routes,
|
|
1721
|
+
routeHost,
|
|
1722
|
+
assertAppMutable,
|
|
1723
|
+
currentModule,
|
|
1724
|
+
"GET",
|
|
1725
|
+
health.livenessPath,
|
|
1726
|
+
{
|
|
1727
|
+
health: "liveness",
|
|
1728
|
+
checks: healthCheckNames(health.checks, (check) => check.liveness),
|
|
1729
|
+
},
|
|
1730
|
+
createHealthHandler(health.checks, "liveness"),
|
|
1731
|
+
undefined,
|
|
1732
|
+
middleware,
|
|
1733
|
+
corsPolicy,
|
|
1734
|
+
).withName("Health.Liveness");
|
|
1735
|
+
|
|
1736
|
+
registerRoute(
|
|
1737
|
+
routes,
|
|
1738
|
+
routeHost,
|
|
1739
|
+
assertAppMutable,
|
|
1740
|
+
currentModule,
|
|
1741
|
+
"GET",
|
|
1742
|
+
health.readinessPath,
|
|
1743
|
+
{
|
|
1744
|
+
health: "readiness",
|
|
1745
|
+
checks: healthCheckNames(health.checks, (check) => check.readiness),
|
|
1746
|
+
},
|
|
1747
|
+
createHealthHandler(health.checks, "readiness"),
|
|
1748
|
+
undefined,
|
|
1749
|
+
middleware,
|
|
1750
|
+
corsPolicy,
|
|
1751
|
+
).withName("Health.Readiness");
|
|
1752
|
+
|
|
1753
|
+
return app;
|
|
1754
|
+
},
|
|
1755
|
+
|
|
1756
|
+
get(pattern, optionsOrHandler, maybeHandler) {
|
|
1757
|
+
return app.mapGet(pattern, optionsOrHandler, maybeHandler);
|
|
1758
|
+
},
|
|
1759
|
+
|
|
1760
|
+
post(pattern, optionsOrHandler, maybeHandler) {
|
|
1761
|
+
return app.mapPost(pattern, optionsOrHandler, maybeHandler);
|
|
1762
|
+
},
|
|
1763
|
+
|
|
1764
|
+
put(pattern, optionsOrHandler, maybeHandler) {
|
|
1765
|
+
return app.mapPut(pattern, optionsOrHandler, maybeHandler);
|
|
1766
|
+
},
|
|
1767
|
+
|
|
1768
|
+
patch(pattern, optionsOrHandler, maybeHandler) {
|
|
1769
|
+
return app.mapPatch(pattern, optionsOrHandler, maybeHandler);
|
|
1770
|
+
},
|
|
1771
|
+
|
|
1772
|
+
delete(pattern, optionsOrHandler, maybeHandler) {
|
|
1773
|
+
return app.mapDelete(pattern, optionsOrHandler, maybeHandler);
|
|
1774
|
+
},
|
|
1775
|
+
|
|
1776
|
+
sse(pattern, optionsOrHandler, maybeHandler) {
|
|
1777
|
+
const handler = typeof optionsOrHandler === "function" && maybeHandler === undefined
|
|
1778
|
+
? createSseRouteHandler(optionsOrHandler)
|
|
1779
|
+
: createSseRouteHandler(maybeHandler);
|
|
1780
|
+
return registerRoute(
|
|
1781
|
+
routes,
|
|
1782
|
+
routeHost,
|
|
1783
|
+
assertAppMutable,
|
|
1784
|
+
currentModule,
|
|
1785
|
+
"GET",
|
|
1786
|
+
pattern,
|
|
1787
|
+
typeof optionsOrHandler === "function" ? handler : optionsOrHandler,
|
|
1788
|
+
typeof optionsOrHandler === "function" ? undefined : handler,
|
|
1789
|
+
undefined,
|
|
1790
|
+
middleware,
|
|
1791
|
+
corsPolicy,
|
|
1792
|
+
"sse",
|
|
1793
|
+
);
|
|
1794
|
+
},
|
|
1795
|
+
|
|
1796
|
+
ws(pattern, optionsOrHandler, maybeHandler) {
|
|
1797
|
+
const handler = typeof optionsOrHandler === "function"
|
|
1798
|
+
? createWebSocketRouteHandler(optionsOrHandler, maybeHandler)
|
|
1799
|
+
: createWebSocketRouteHandler(maybeHandler, optionsOrHandler);
|
|
1800
|
+
return registerRoute(
|
|
1801
|
+
routes,
|
|
1802
|
+
routeHost,
|
|
1803
|
+
assertAppMutable,
|
|
1804
|
+
currentModule,
|
|
1805
|
+
"GET",
|
|
1806
|
+
pattern,
|
|
1807
|
+
typeof optionsOrHandler === "function" ? handler : optionsOrHandler,
|
|
1808
|
+
typeof optionsOrHandler === "function" ? undefined : handler,
|
|
1809
|
+
undefined,
|
|
1810
|
+
middleware,
|
|
1811
|
+
corsPolicy,
|
|
1812
|
+
"websocket",
|
|
1813
|
+
);
|
|
1814
|
+
},
|
|
1815
|
+
|
|
1816
|
+
websocket(pattern, handler, options = undefined) {
|
|
1817
|
+
return app.ws(pattern, handler, options);
|
|
1818
|
+
},
|
|
1819
|
+
|
|
1820
|
+
realtime(pattern, channel, handler, options = undefined) {
|
|
1821
|
+
const routeHandler = createRealtimeRouteHandler(channel, handler, options);
|
|
1822
|
+
return registerRoute(
|
|
1823
|
+
routes,
|
|
1824
|
+
routeHost,
|
|
1825
|
+
assertAppMutable,
|
|
1826
|
+
currentModule,
|
|
1827
|
+
"GET",
|
|
1828
|
+
pattern,
|
|
1829
|
+
routeHandler,
|
|
1830
|
+
undefined,
|
|
1831
|
+
undefined,
|
|
1832
|
+
middleware,
|
|
1833
|
+
corsPolicy,
|
|
1834
|
+
"websocket",
|
|
1835
|
+
);
|
|
1836
|
+
},
|
|
1837
|
+
|
|
1838
|
+
mapGroup(prefix) {
|
|
1839
|
+
assertAppMutable();
|
|
1840
|
+
return createRouteGroup(
|
|
1841
|
+
routes,
|
|
1842
|
+
routeHost,
|
|
1843
|
+
assertAppMutable,
|
|
1844
|
+
getCurrentModule,
|
|
1845
|
+
prefix,
|
|
1846
|
+
() => middleware,
|
|
1847
|
+
nextMiddlewareSequence,
|
|
1848
|
+
getCorsPolicy,
|
|
1849
|
+
);
|
|
1850
|
+
},
|
|
1851
|
+
|
|
1852
|
+
group(prefix) {
|
|
1853
|
+
return app.mapGroup(prefix);
|
|
1854
|
+
},
|
|
1855
|
+
|
|
1856
|
+
urlFor(name, params = {}, query = undefined) {
|
|
1857
|
+
return urlForRoute(routes, name, params, query);
|
|
1858
|
+
},
|
|
1859
|
+
|
|
1860
|
+
mapController(prefix, Controller, configure) {
|
|
1861
|
+
assertAppMutable();
|
|
1862
|
+
const mapper = createControllerMapper(
|
|
1863
|
+
routes,
|
|
1864
|
+
routeHost,
|
|
1865
|
+
assertAppMutable,
|
|
1866
|
+
currentModule,
|
|
1867
|
+
prefix,
|
|
1868
|
+
Controller,
|
|
1869
|
+
middleware,
|
|
1870
|
+
getCorsPolicy,
|
|
1871
|
+
);
|
|
1872
|
+
|
|
1873
|
+
if (configure === undefined) {
|
|
1874
|
+
return mapper;
|
|
1875
|
+
}
|
|
1876
|
+
if (typeof configure !== "function") {
|
|
1877
|
+
throw new TypeError("Sloppy app.mapController configure callback must be a function.");
|
|
1878
|
+
}
|
|
1879
|
+
configure(mapper);
|
|
1880
|
+
return app;
|
|
1881
|
+
},
|
|
1882
|
+
|
|
1883
|
+
controller(prefix, Controller, configure) {
|
|
1884
|
+
return app.mapController(prefix, Controller, configure);
|
|
1885
|
+
},
|
|
1886
|
+
|
|
1887
|
+
freeze() {
|
|
1888
|
+
lifecycleState.startupComplete = true;
|
|
1889
|
+
guard.freeze();
|
|
1890
|
+
return app;
|
|
1891
|
+
},
|
|
1892
|
+
|
|
1893
|
+
isFrozen() {
|
|
1894
|
+
return guard.isFrozen();
|
|
1895
|
+
},
|
|
1896
|
+
|
|
1897
|
+
__getRoutes() {
|
|
1898
|
+
return Object.freeze(routes.map(snapshotRoute));
|
|
1899
|
+
},
|
|
1900
|
+
|
|
1901
|
+
__getStaticAssets() {
|
|
1902
|
+
return Object.freeze(staticAssets.map((entry) => Object.freeze({ ...entry })));
|
|
1903
|
+
},
|
|
1904
|
+
|
|
1905
|
+
__getMetricsRegistry() {
|
|
1906
|
+
return metricsRegistry;
|
|
1907
|
+
},
|
|
1908
|
+
|
|
1909
|
+
__getHealthRegistry() {
|
|
1910
|
+
return opsHealthRegistry;
|
|
1911
|
+
},
|
|
1912
|
+
|
|
1913
|
+
__getLifecycle() {
|
|
1914
|
+
return snapshotLifecycle(lifecycleState);
|
|
1915
|
+
},
|
|
1916
|
+
|
|
1917
|
+
__beginShutdown() {
|
|
1918
|
+
lifecycleState.shuttingDown = true;
|
|
1919
|
+
},
|
|
1920
|
+
|
|
1921
|
+
__debug() {
|
|
1922
|
+
return Object.freeze({
|
|
1923
|
+
modules: moduleDebugRef.modules,
|
|
1924
|
+
workers: Object.freeze(workerResources.map(snapshotWorkerResource)),
|
|
1925
|
+
health: opsHealthRegistry.checks(),
|
|
1926
|
+
metrics: metricsRegistry.snapshot(),
|
|
1927
|
+
lifecycle: snapshotLifecycle(lifecycleState),
|
|
1928
|
+
});
|
|
1929
|
+
},
|
|
1930
|
+
|
|
1931
|
+
__getModuleGraph() {
|
|
1932
|
+
return moduleDebugRef.modules;
|
|
1933
|
+
},
|
|
1934
|
+
|
|
1935
|
+
__getPlanContributions() {
|
|
1936
|
+
return Object.freeze({
|
|
1937
|
+
modules: moduleDebugRef.modules,
|
|
1938
|
+
capabilities: host.capabilities.list(),
|
|
1939
|
+
auth: snapshotAuthState(authState),
|
|
1940
|
+
workers: Object.freeze(workerResources.map(snapshotWorkerResource)),
|
|
1941
|
+
cache: Object.freeze({
|
|
1942
|
+
enabled: routes.some((route) => route.metadata?.outputCache !== undefined),
|
|
1943
|
+
outputCacheRoutes: Object.freeze(routes
|
|
1944
|
+
.filter((route) => route.metadata?.outputCache !== undefined)
|
|
1945
|
+
.map((route) => Object.freeze({
|
|
1946
|
+
method: route.method,
|
|
1947
|
+
pattern: route.pattern,
|
|
1948
|
+
cacheName: route.metadata.outputCache.cacheName,
|
|
1949
|
+
varyByQuery: route.metadata.outputCache.varyByQuery,
|
|
1950
|
+
varyByHeader: route.metadata.outputCache.varyByHeader,
|
|
1951
|
+
varyByRouteParams: route.metadata.outputCache.varyByRouteParams,
|
|
1952
|
+
varyByUser: route.metadata.outputCache.varyByUser,
|
|
1953
|
+
}))),
|
|
1954
|
+
}),
|
|
1955
|
+
staticAssets: Object.freeze(staticAssets.map((entry) => Object.freeze({
|
|
1956
|
+
kind: entry.kind,
|
|
1957
|
+
mount: entry.mount,
|
|
1958
|
+
root: entry.root,
|
|
1959
|
+
fallback: entry.fallback,
|
|
1960
|
+
cacheControl: entry.cacheControl,
|
|
1961
|
+
precompressed: entry.precompressed,
|
|
1962
|
+
range: entry.range,
|
|
1963
|
+
}))),
|
|
1964
|
+
webhooks: host.webhooks ?? Object.freeze([]),
|
|
1965
|
+
errors: errorPolicyState.policy === null ? undefined : Object.freeze({
|
|
1966
|
+
detail: errorPolicyState.policy.detail,
|
|
1967
|
+
missingRoute: errorPolicyState.policy.missingRoute,
|
|
1968
|
+
mappings: errorPolicyState.mappings.length,
|
|
1969
|
+
}),
|
|
1970
|
+
ops: Object.freeze({
|
|
1971
|
+
healthChecks: opsHealthRegistry.checks(),
|
|
1972
|
+
healthExposed: opsHealthExposed,
|
|
1973
|
+
managementExposed,
|
|
1974
|
+
metrics: metricsRegistry.snapshot(),
|
|
1975
|
+
}),
|
|
1976
|
+
rateLimit: Object.freeze({
|
|
1977
|
+
stores: Object.freeze([...rateLimitStores.entries()].map(([name, store]) => Object.freeze({
|
|
1978
|
+
name,
|
|
1979
|
+
kind: store.kind,
|
|
1980
|
+
}))),
|
|
1981
|
+
routes: Object.freeze(routes
|
|
1982
|
+
.filter((route) => Array.isArray(route.metadata?.rateLimit) && route.metadata.rateLimit.length !== 0)
|
|
1983
|
+
.map((route) => Object.freeze({
|
|
1984
|
+
method: route.method,
|
|
1985
|
+
pattern: route.pattern,
|
|
1986
|
+
policies: route.metadata.rateLimit.map((policy) => policy.metadata),
|
|
1987
|
+
}))),
|
|
1988
|
+
}),
|
|
1989
|
+
});
|
|
1990
|
+
},
|
|
1991
|
+
|
|
1992
|
+
__getErrorPolicy() {
|
|
1993
|
+
return errorPolicyState.policy === null ? undefined : Object.freeze({
|
|
1994
|
+
detail: errorPolicyState.policy.detail,
|
|
1995
|
+
missingRoute: errorPolicyState.policy.missingRoute,
|
|
1996
|
+
maxBodyBytes: errorPolicyState.policy.maxBodyBytes,
|
|
1997
|
+
mappings: errorPolicyState.mappings.length,
|
|
1998
|
+
});
|
|
1999
|
+
},
|
|
2000
|
+
|
|
2001
|
+
__handleErrorStatus(status, context = undefined) {
|
|
2002
|
+
if (errorPolicyState.policy === null) {
|
|
2003
|
+
return undefined;
|
|
2004
|
+
}
|
|
2005
|
+
return standardProblemResult(status, context, errorPolicyState, host.config);
|
|
2006
|
+
},
|
|
2007
|
+
|
|
2008
|
+
__getSerializationOptions() {
|
|
2009
|
+
return Object.freeze({
|
|
2010
|
+
json: jsonOptions,
|
|
2011
|
+
contentNegotiation,
|
|
2012
|
+
});
|
|
2013
|
+
},
|
|
2014
|
+
|
|
2015
|
+
__runInModule(moduleName, callback) {
|
|
2016
|
+
assertAppMutable();
|
|
2017
|
+
|
|
2018
|
+
const previousModule = currentModule;
|
|
2019
|
+
currentModule = moduleName;
|
|
2020
|
+
|
|
2021
|
+
try {
|
|
2022
|
+
return callback(app);
|
|
2023
|
+
} finally {
|
|
2024
|
+
currentModule = previousModule;
|
|
2025
|
+
}
|
|
2026
|
+
},
|
|
2027
|
+
|
|
2028
|
+
};
|
|
2029
|
+
|
|
2030
|
+
return Object.freeze(app);
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
|
|
2034
|
+
function createBuilder() {
|
|
2035
|
+
const options = arguments[0];
|
|
2036
|
+
const appOptions = normalizeAppOptions(options);
|
|
2037
|
+
const guard = createMutationGuard("builder");
|
|
2038
|
+
const config = createConfigBuilder(guard);
|
|
2039
|
+
const logging = createLoggingBuilder(guard);
|
|
2040
|
+
const capabilities = createCapabilityRegistry(guard);
|
|
2041
|
+
const services = createServicesBuilder(guard);
|
|
2042
|
+
const modules = [];
|
|
2043
|
+
const moduleNames = new Set();
|
|
2044
|
+
|
|
2045
|
+
const builder = {
|
|
2046
|
+
config,
|
|
2047
|
+
logging,
|
|
2048
|
+
capabilities,
|
|
2049
|
+
services,
|
|
2050
|
+
|
|
2051
|
+
addModule(module) {
|
|
2052
|
+
guard.assertMutable();
|
|
2053
|
+
|
|
2054
|
+
const state = requireModuleState(module);
|
|
2055
|
+
|
|
2056
|
+
if (moduleNames.has(state.name)) {
|
|
2057
|
+
throw new Error(`Sloppy module '${state.name}' is already registered.`);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
state.finalized = true;
|
|
2061
|
+
modules.push(state);
|
|
2062
|
+
moduleNames.add(state.name);
|
|
2063
|
+
return builder;
|
|
2064
|
+
},
|
|
2065
|
+
|
|
2066
|
+
build() {
|
|
2067
|
+
guard.assertMutable();
|
|
2068
|
+
|
|
2069
|
+
const orderedModules = resolveModuleOrder(modules);
|
|
2070
|
+
|
|
2071
|
+
for (const state of orderedModules) {
|
|
2072
|
+
for (const callback of state.capabilityCallbacks) {
|
|
2073
|
+
capabilities.__runInModule(state.name, (capabilityRegistry) => {
|
|
2074
|
+
runModulePhase(state, "capabilities", callback, capabilityRegistry);
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
for (const state of orderedModules) {
|
|
2080
|
+
for (const callback of state.serviceCallbacks) {
|
|
2081
|
+
services.__runInModule(state.name, (servicesBuilder) => {
|
|
2082
|
+
runModulePhase(state, "services", callback, servicesBuilder);
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
const capabilitySnapshot = capabilities.__snapshot();
|
|
2088
|
+
const serviceSnapshot = services.__snapshot();
|
|
2089
|
+
|
|
2090
|
+
guard.freeze();
|
|
2091
|
+
|
|
2092
|
+
const moduleDebugRef = {
|
|
2093
|
+
modules: Object.freeze([]),
|
|
2094
|
+
};
|
|
2095
|
+
|
|
2096
|
+
const app = createApp(Object.freeze({
|
|
2097
|
+
config: createConfigProvider(config.__snapshot()),
|
|
2098
|
+
log: createLogger(logging.__snapshot()),
|
|
2099
|
+
capabilities: createCapabilityProvider(capabilitySnapshot),
|
|
2100
|
+
services: createServiceProvider(serviceSnapshot, createCapabilityProvider(capabilitySnapshot), createConfigProvider(config.__snapshot())),
|
|
2101
|
+
webhooks: Object.freeze(Array.from(serviceSnapshot.values())
|
|
2102
|
+
.map((registration) => registration.webhooks)
|
|
2103
|
+
.filter((entry) => entry !== undefined)),
|
|
2104
|
+
moduleDebugRef,
|
|
2105
|
+
options: appOptions,
|
|
2106
|
+
}));
|
|
2107
|
+
|
|
2108
|
+
for (const state of orderedModules) {
|
|
2109
|
+
for (const callback of state.routeCallbacks) {
|
|
2110
|
+
app.__runInModule(state.name, (moduleApp) => {
|
|
2111
|
+
runModulePhase(state, "routes", callback, moduleApp);
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
moduleDebugRef.modules = createModuleDebugEntries(
|
|
2117
|
+
orderedModules,
|
|
2118
|
+
capabilitySnapshot,
|
|
2119
|
+
serviceSnapshot,
|
|
2120
|
+
app.__getRoutes(),
|
|
2121
|
+
);
|
|
2122
|
+
return app;
|
|
2123
|
+
},
|
|
2124
|
+
};
|
|
2125
|
+
|
|
2126
|
+
return Object.freeze(builder);
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
function create() {
|
|
2130
|
+
const options = arguments[0];
|
|
2131
|
+
return createBuilder(options).build();
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
export const Sloppy = Object.freeze({
|
|
2135
|
+
create,
|
|
2136
|
+
createBuilder,
|
|
2137
|
+
module: createModule,
|
|
2138
|
+
});
|
|
2139
|
+
|
|
2140
|
+
export const Router = Object.freeze({
|
|
2141
|
+
group: createRouterGroup,
|
|
2142
|
+
});
|