@slopware/sloppy-darwin-x64 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +5 -0
- package/bin/sloppy +0 -0
- package/bin/sloppyc +0 -0
- package/docs/KNOWN_LIMITATIONS.md +16 -0
- package/docs/LICENSES.md +6 -0
- package/docs/NOTICE.md +8 -0
- package/examples/README.md +140 -0
- package/examples/auth-api/README.md +20 -0
- package/examples/auth-api/app.js +61 -0
- package/examples/auth-api/appsettings.json +7 -0
- package/examples/auth-api/sloppy.json +5 -0
- package/examples/cache-basic/README.md +9 -0
- package/examples/cache-basic/app.js +32 -0
- package/examples/cache-hybrid-postgres/README.md +10 -0
- package/examples/cache-hybrid-postgres/app.js +27 -0
- package/examples/cache-output-api/README.md +10 -0
- package/examples/cache-output-api/app.js +35 -0
- package/examples/codec-base64-hex/README.md +14 -0
- package/examples/codec-base64-hex/app.js +15 -0
- package/examples/codec-checksums/README.md +15 -0
- package/examples/codec-checksums/app.js +8 -0
- package/examples/codec-compression/README.md +13 -0
- package/examples/codec-compression/app.js +9 -0
- package/examples/codec-streaming-compression/README.md +19 -0
- package/examples/codec-streaming-compression/app.js +16 -0
- package/examples/codec-text-binary/README.md +16 -0
- package/examples/codec-text-binary/app.js +17 -0
- package/examples/compiler-hello/README.md +71 -0
- package/examples/compiler-hello/app.js +7 -0
- package/examples/compiler-hello/expected/app.js +8 -0
- package/examples/compiler-hello/expected/app.js.map +53 -0
- package/examples/compiler-hello/expected/app.plan.json +229 -0
- package/examples/compiler-hello/expected/routes.slrt +0 -0
- package/examples/config-basic/README.md +13 -0
- package/examples/config-basic/app.js +13 -0
- package/examples/config-basic/appsettings.json +7 -0
- package/examples/config-secrets-redaction/README.md +9 -0
- package/examples/config-secrets-redaction/app.js +9 -0
- package/examples/config-secrets-redaction/appsettings.json +5 -0
- package/examples/config-strict-mode/README.md +7 -0
- package/examples/config-strict-mode/app.js +10 -0
- package/examples/config-strict-mode/appsettings.json +7 -0
- package/examples/configured-api/README.md +38 -0
- package/examples/configured-api/app.js +12 -0
- package/examples/configured-api/appsettings.Development.json +5 -0
- package/examples/configured-api/appsettings.json +6 -0
- package/examples/configured-api/sloppy.json +5 -0
- package/examples/core-config-secrets/README.md +10 -0
- package/examples/core-config-secrets/app.js +15 -0
- package/examples/core-fs-time-codec/README.md +9 -0
- package/examples/core-fs-time-codec/app.js +8 -0
- package/examples/core-network-time-codec/README.md +11 -0
- package/examples/core-network-time-codec/app.js +20 -0
- package/examples/core-policy-audit/README.md +7 -0
- package/examples/core-policy-audit/app.js +22 -0
- package/examples/core-process-time-codec/README.md +8 -0
- package/examples/core-process-time-codec/app.js +28 -0
- package/examples/core-worker-time/README.md +8 -0
- package/examples/core-worker-time/app.js +17 -0
- package/examples/crypto-hash-hmac/README.md +17 -0
- package/examples/crypto-hash-hmac/app.js +29 -0
- package/examples/crypto-password/README.md +21 -0
- package/examples/crypto-password/app.js +12 -0
- package/examples/crypto-random-token/README.md +16 -0
- package/examples/crypto-random-token/app.js +12 -0
- package/examples/crypto-secret-constant-time/README.md +21 -0
- package/examples/crypto-secret-constant-time/app.js +15 -0
- package/examples/data-foundation/README.md +39 -0
- package/examples/data-foundation/app.js +63 -0
- package/examples/dependency-graph/README.md +19 -0
- package/examples/dependency-graph/fixtures/graph-helper/index.js +3 -0
- package/examples/dependency-graph/fixtures/graph-helper/package.json +6 -0
- package/examples/dependency-graph/package.json +7 -0
- package/examples/dependency-graph/public/message.txt +1 -0
- package/examples/dependency-graph/sloppy.json +9 -0
- package/examples/dependency-graph/src/main.ts +8 -0
- package/examples/dogfood/README.md +23 -0
- package/examples/dogfood/dogfood.json +136 -0
- package/examples/dynamic-module-include/README.md +20 -0
- package/examples/dynamic-module-include/public/readme.txt +1 -0
- package/examples/dynamic-module-include/sloppy.json +12 -0
- package/examples/dynamic-module-include/src/main.ts +6 -0
- package/examples/dynamic-module-include/src/plugins/alpha.js +3 -0
- package/examples/dynamic-module-include/src/plugins/beta.js +3 -0
- package/examples/ergonomics/README.md +42 -0
- package/examples/ergonomics/app.js +38 -0
- package/examples/framework-controller/README.md +12 -0
- package/examples/framework-controller/app.js +31 -0
- package/examples/framework-di-services/README.md +17 -0
- package/examples/framework-di-services/app.ts +40 -0
- package/examples/framework-explicit-binding/README.md +12 -0
- package/examples/framework-explicit-binding/app.ts +34 -0
- package/examples/framework-hello/README.md +16 -0
- package/examples/framework-hello/app.ts +16 -0
- package/examples/framework-postgres-crud/README.md +73 -0
- package/examples/framework-postgres-crud/app.ts +64 -0
- package/examples/framework-sqlite-crud/README.md +52 -0
- package/examples/framework-sqlite-crud/app.ts +90 -0
- package/examples/framework-sqlite-crud/appsettings.json +11 -0
- package/examples/framework-sqlserver-crud/README.md +73 -0
- package/examples/framework-sqlserver-crud/app.ts +64 -0
- package/examples/framework-validation-errors/README.md +12 -0
- package/examples/framework-validation-errors/app.ts +16 -0
- package/examples/fs-basic/README.md +24 -0
- package/examples/fs-basic/app.js +12 -0
- package/examples/fs-roots-policy/README.md +14 -0
- package/examples/fs-roots-policy/app.js +4 -0
- package/examples/fs-streams/README.md +18 -0
- package/examples/fs-streams/app.js +11 -0
- package/examples/fs-watch/README.md +19 -0
- package/examples/fs-watch/app.js +11 -0
- package/examples/hello/README.md +63 -0
- package/examples/hello/app.js +19 -0
- package/examples/hello-minimal/README.md +51 -0
- package/examples/hello-minimal/sloppy.json +5 -0
- package/examples/hello-minimal/src/main.ts +9 -0
- package/examples/http-client-basic/README.md +11 -0
- package/examples/http-client-basic/app.js +46 -0
- package/examples/http-client-generated/README.md +22 -0
- package/examples/http-client-generated/openapi.json +45 -0
- package/examples/http-client-resilience/README.md +4 -0
- package/examples/http-client-resilience/app.js +38 -0
- package/examples/http-client-runtime-loopback/README.md +24 -0
- package/examples/http-client-testhost/README.md +4 -0
- package/examples/http-client-testhost/app.js +27 -0
- package/examples/http-client-testhost-package-mock/README.md +26 -0
- package/examples/http-client-typed/README.md +5 -0
- package/examples/http-client-typed/app.js +33 -0
- package/examples/modules-api/README.md +30 -0
- package/examples/modules-api/app.js +9 -0
- package/examples/modules-api/modules/routes.js +16 -0
- package/examples/modules-api/sloppy.json +5 -0
- package/examples/modules-basic/README.md +32 -0
- package/examples/modules-basic/app.js +41 -0
- package/examples/net-deadline-cancel/README.md +13 -0
- package/examples/net-deadline-cancel/app.js +34 -0
- package/examples/net-local-ipc/README.md +12 -0
- package/examples/net-local-ipc/app.js +46 -0
- package/examples/net-policy-strict/README.md +12 -0
- package/examples/net-policy-strict/app.js +34 -0
- package/examples/net-tcp-client/README.md +10 -0
- package/examples/net-tcp-client/app.js +23 -0
- package/examples/net-tcp-echo/README.md +11 -0
- package/examples/net-tcp-echo/app.js +45 -0
- package/examples/net-tcp-server/README.md +10 -0
- package/examples/net-tcp-server/app.js +28 -0
- package/examples/node-compat-path-events/README.md +15 -0
- package/examples/node-compat-path-events/sloppy.json +6 -0
- package/examples/node-compat-path-events/src/main.ts +15 -0
- package/examples/ops-compiler/README.md +9 -0
- package/examples/ops-compiler/app.js +26 -0
- package/examples/ops-health-metrics-management/README.md +14 -0
- package/examples/ops-health-metrics-management/app.js +24 -0
- package/examples/orm-basic/README.md +17 -0
- package/examples/orm-basic/app.js +82 -0
- package/examples/orm-cursor-export/README.md +16 -0
- package/examples/orm-cursor-export/app.js +28 -0
- package/examples/orm-migrations/README.md +14 -0
- package/examples/orm-migrations/migrations/.gitkeep +1 -0
- package/examples/orm-migrations/sloppy.json +9 -0
- package/examples/orm-migrations/src/app.ts +34 -0
- package/examples/orm-relations-includes/README.md +10 -0
- package/examples/orm-relations-includes/app.js +47 -0
- package/examples/orm-testservices/README.md +37 -0
- package/examples/orm-testservices/test.mjs +32 -0
- package/examples/os-runtime-api/README.md +11 -0
- package/examples/os-runtime-api/app.js +44 -0
- package/examples/package-zod-like/README.md +28 -0
- package/examples/package-zod-like/fixtures/zod-like/index.js +48 -0
- package/examples/package-zod-like/fixtures/zod-like/package.json +12 -0
- package/examples/package-zod-like/package.json +7 -0
- package/examples/package-zod-like/sloppy.json +6 -0
- package/examples/package-zod-like/src/main.ts +16 -0
- package/examples/postgres-basic/README.md +31 -0
- package/examples/postgres-basic/app.js +50 -0
- package/examples/prealpha-control-plane/README.md +50 -0
- package/examples/prealpha-control-plane/appsettings.Development.json +11 -0
- package/examples/prealpha-control-plane/appsettings.json +15 -0
- package/examples/prealpha-control-plane/sloppy.json +5 -0
- package/examples/prealpha-control-plane/src/db/schema.js +7 -0
- package/examples/prealpha-control-plane/src/db/seed.js +6 -0
- package/examples/prealpha-control-plane/src/main.js +21 -0
- package/examples/prealpha-control-plane/src/routes/apps.js +34 -0
- package/examples/prealpha-control-plane/src/routes/builds.js +25 -0
- package/examples/prealpha-control-plane/src/routes/deployments.js +19 -0
- package/examples/prealpha-control-plane/src/routes/diagnostics.js +11 -0
- package/examples/prealpha-control-plane/src/routes/health.js +27 -0
- package/examples/prealpha-control-plane/src/routes/projects.js +38 -0
- package/examples/prealpha-control-plane/src/services/diagnosticsSink.js +11 -0
- package/examples/prealpha-control-plane/src/services/repositories.js +9 -0
- package/examples/prealpha-control-plane/src/validation/schemas.js +6 -0
- package/examples/program-fs-process/README.md +31 -0
- package/examples/program-fs-process/sloppy.json +9 -0
- package/examples/program-fs-process/src/main.ts +27 -0
- package/examples/program-hello/README.md +32 -0
- package/examples/program-hello/main.ts +8 -0
- package/examples/program-hello/message.ts +1 -0
- package/examples/program-hello/sloppy.json +5 -0
- package/examples/rate-limit-auth/README.md +3 -0
- package/examples/rate-limit-auth/app.js +14 -0
- package/examples/rate-limit-basic/README.md +3 -0
- package/examples/rate-limit-basic/app.js +13 -0
- package/examples/rate-limit-redis/README.md +5 -0
- package/examples/rate-limit-redis/app.js +20 -0
- package/examples/rate-limit-testhost/README.md +4 -0
- package/examples/rate-limit-testhost/app.js +13 -0
- package/examples/rate-limit-websocket/README.md +3 -0
- package/examples/rate-limit-websocket/app.js +16 -0
- package/examples/realtime-auth/README.md +8 -0
- package/examples/realtime-auth/app.js +25 -0
- package/examples/realtime-auth/test.mjs +43 -0
- package/examples/realtime-chat/README.md +8 -0
- package/examples/realtime-chat/app.js +32 -0
- package/examples/realtime-chat/test.mjs +52 -0
- package/examples/realtime-dashboard/README.md +20 -0
- package/examples/realtime-dashboard/app.js +37 -0
- package/examples/realtime-presence/README.md +8 -0
- package/examples/realtime-presence/app.js +32 -0
- package/examples/realtime-presence/test.mjs +50 -0
- package/examples/realtime-testhost/README.md +8 -0
- package/examples/realtime-testhost/test.mjs +31 -0
- package/examples/redis-basic/README.md +17 -0
- package/examples/redis-basic/app.js +39 -0
- package/examples/redis-cache/README.md +14 -0
- package/examples/redis-cache/app.js +36 -0
- package/examples/redis-locks/README.md +13 -0
- package/examples/redis-locks/app.js +49 -0
- package/examples/request-context/README.md +32 -0
- package/examples/request-context/app.js +15 -0
- package/examples/sqlite-basic/README.md +52 -0
- package/examples/sqlite-basic/app.js +56 -0
- package/examples/sqlserver-basic/README.md +36 -0
- package/examples/sqlserver-basic/app.js +59 -0
- package/examples/static-files-basic/README.md +11 -0
- package/examples/static-files-basic/app.js +12 -0
- package/examples/static-files-basic/public/app.js +1 -0
- package/examples/static-files-basic/public/site.css +3 -0
- package/examples/static-files-package/README.md +12 -0
- package/examples/static-files-package/app.js +10 -0
- package/examples/static-files-package/public/index.html +2 -0
- package/examples/static-files-precompressed/README.md +12 -0
- package/examples/static-files-precompressed/app.js +11 -0
- package/examples/static-files-precompressed/public/app.js +1 -0
- package/examples/static-files-precompressed/public/app.js.br +0 -0
- package/examples/static-files-precompressed/public/app.js.gz +0 -0
- package/examples/static-files-spa/README.md +12 -0
- package/examples/static-files-spa/app.js +16 -0
- package/examples/static-files-spa/dist/assets/app.js +1 -0
- package/examples/static-files-spa/dist/index.html +4 -0
- package/examples/static-files-testhost/README.md +8 -0
- package/examples/static-files-testhost/app.js +13 -0
- package/examples/static-files-testhost/public/app.js +1 -0
- package/examples/static-files-testhost/public/app.js.gz +0 -0
- package/examples/static-files-testhost/test.mjs +38 -0
- package/examples/testhost-basic/README.md +26 -0
- package/examples/testhost-db/README.md +31 -0
- package/examples/testservices-postgres/README.md +68 -0
- package/examples/testservices-redis/README.md +71 -0
- package/examples/testservices-sqlserver/README.md +75 -0
- package/examples/time-basic/README.md +18 -0
- package/examples/time-basic/app.js +12 -0
- package/examples/time-deadline-cancellation/README.md +11 -0
- package/examples/time-deadline-cancellation/app.js +27 -0
- package/examples/time-fake-clock/README.md +14 -0
- package/examples/time-fake-clock/app.js +25 -0
- package/examples/time-interval-schedule/README.md +13 -0
- package/examples/time-interval-schedule/app.js +60 -0
- package/examples/users-api-sqlite/README.md +74 -0
- package/examples/users-api-sqlite/app.js +11 -0
- package/examples/users-api-sqlite/appsettings.Development.json +11 -0
- package/examples/users-api-sqlite/appsettings.json +11 -0
- package/examples/users-api-sqlite/modules/users.js +40 -0
- package/examples/users-api-sqlite/sloppy.json +5 -0
- package/examples/validation-errors/README.md +36 -0
- package/examples/validation-errors/app.js +14 -0
- package/examples/validation-errors/invalid-user.http +6 -0
- package/examples/validation-errors/sloppy.json +5 -0
- package/examples/web-dynamic-routes/README.md +17 -0
- package/examples/web-dynamic-routes/app.ts +27 -0
- package/examples/webhooks-basic/README.md +11 -0
- package/examples/webhooks-basic/app.js +48 -0
- package/examples/websocket-auth/README.md +8 -0
- package/examples/websocket-auth/app.js +16 -0
- package/examples/websocket-echo/README.md +9 -0
- package/examples/websocket-echo/app.js +36 -0
- package/examples/websocket-json-schema/README.md +5 -0
- package/examples/websocket-json-schema/app.js +25 -0
- package/examples/websocket-testhost/README.md +11 -0
- package/examples/websocket-testhost/test.mjs +49 -0
- package/examples/workers-background-service/README.md +7 -0
- package/examples/workers-background-service/app.js +16 -0
- package/examples/workers-js-isolate/README.md +8 -0
- package/examples/workers-js-isolate/app.js +19 -0
- package/examples/workers-js-isolate/workers/parser.ts +11 -0
- package/examples/workers-shutdown/README.md +6 -0
- package/examples/workers-shutdown/app.js +26 -0
- package/examples/workers-workerpool/README.md +6 -0
- package/examples/workers-workerpool/app.js +23 -0
- package/examples/workers-workqueue/README.md +8 -0
- package/examples/workers-workqueue/app.js +24 -0
- package/manifest.json +59 -0
- package/package.json +31 -0
- package/stdlib/sloppy/README.md +177 -0
- package/stdlib/sloppy/app.js +2142 -0
- package/stdlib/sloppy/auth.js +1813 -0
- package/stdlib/sloppy/bootstrap.manifest.json +83 -0
- package/stdlib/sloppy/cache.js +1542 -0
- package/stdlib/sloppy/codec.js +1153 -0
- package/stdlib/sloppy/config.js +61 -0
- package/stdlib/sloppy/crypto.js +312 -0
- package/stdlib/sloppy/data.js +2945 -0
- package/stdlib/sloppy/ffi.js +185 -0
- package/stdlib/sloppy/fs.js +795 -0
- package/stdlib/sloppy/health.js +603 -0
- package/stdlib/sloppy/http.js +1595 -0
- package/stdlib/sloppy/index.js +59 -0
- package/stdlib/sloppy/internal/bytes.js +31 -0
- package/stdlib/sloppy/internal/capabilities.js +155 -0
- package/stdlib/sloppy/internal/config.js +640 -0
- package/stdlib/sloppy/internal/disposable.js +31 -0
- package/stdlib/sloppy/internal/headers.js +63 -0
- package/stdlib/sloppy/internal/intrinsics.js +2 -0
- package/stdlib/sloppy/internal/json.js +20 -0
- package/stdlib/sloppy/internal/logging.js +278 -0
- package/stdlib/sloppy/internal/modules.js +405 -0
- package/stdlib/sloppy/internal/redaction.js +87 -0
- package/stdlib/sloppy/internal/routes.js +2279 -0
- package/stdlib/sloppy/internal/runtime-classic.js +19837 -0
- package/stdlib/sloppy/internal/services.js +690 -0
- package/stdlib/sloppy/internal/shared.js +32 -0
- package/stdlib/sloppy/internal/testhost-diagnostics.js +88 -0
- package/stdlib/sloppy/internal/testhost-http-server.js +238 -0
- package/stdlib/sloppy/internal/testhost-http.js +118 -0
- package/stdlib/sloppy/internal/testhost-loopback.js +50 -0
- package/stdlib/sloppy/internal/testservices-docker.js +154 -0
- package/stdlib/sloppy/internal/validation.js +117 -0
- package/stdlib/sloppy/metrics.js +427 -0
- package/stdlib/sloppy/net.js +5208 -0
- package/stdlib/sloppy/node/assert/strict.js +39 -0
- package/stdlib/sloppy/node/assert.js +228 -0
- package/stdlib/sloppy/node/buffer.js +247 -0
- package/stdlib/sloppy/node/console.js +33 -0
- package/stdlib/sloppy/node/constants.js +9 -0
- package/stdlib/sloppy/node/crypto.js +89 -0
- package/stdlib/sloppy/node/diagnostics_channel.js +41 -0
- package/stdlib/sloppy/node/events.js +113 -0
- package/stdlib/sloppy/node/fs/promises.js +27 -0
- package/stdlib/sloppy/node/fs.js +280 -0
- package/stdlib/sloppy/node/http.js +11 -0
- package/stdlib/sloppy/node/https.js +11 -0
- package/stdlib/sloppy/node/module.js +40 -0
- package/stdlib/sloppy/node/os.js +22 -0
- package/stdlib/sloppy/node/path.js +78 -0
- package/stdlib/sloppy/node/perf_hooks.js +12 -0
- package/stdlib/sloppy/node/process.js +129 -0
- package/stdlib/sloppy/node/querystring.js +21 -0
- package/stdlib/sloppy/node/stream/promises.js +3 -0
- package/stdlib/sloppy/node/stream.js +132 -0
- package/stdlib/sloppy/node/string_decoder.js +23 -0
- package/stdlib/sloppy/node/timers.js +26 -0
- package/stdlib/sloppy/node/tty.js +18 -0
- package/stdlib/sloppy/node/url.js +17 -0
- package/stdlib/sloppy/node/util.js +95 -0
- package/stdlib/sloppy/node/zlib.js +72 -0
- package/stdlib/sloppy/orm.js +2188 -0
- package/stdlib/sloppy/os.js +580 -0
- package/stdlib/sloppy/problem-details.js +29 -0
- package/stdlib/sloppy/providers/sqlite.js +26 -0
- package/stdlib/sloppy/rate-limit.js +856 -0
- package/stdlib/sloppy/realtime.js +1508 -0
- package/stdlib/sloppy/redis.js +1272 -0
- package/stdlib/sloppy/request-id.js +184 -0
- package/stdlib/sloppy/request-logging.js +101 -0
- package/stdlib/sloppy/results.js +933 -0
- package/stdlib/sloppy/schema.js +546 -0
- package/stdlib/sloppy/testing.js +4081 -0
- package/stdlib/sloppy/testservices.js +1041 -0
- package/stdlib/sloppy/time.js +894 -0
- package/stdlib/sloppy/webhooks.js +1330 -0
- package/stdlib/sloppy/workers.js +986 -0
- package/templates/api/README.md +82 -0
- package/templates/api/appsettings.Development.json +14 -0
- package/templates/api/appsettings.json +13 -0
- package/templates/api/data/.gitkeep +1 -0
- package/templates/api/gitignore +4 -0
- package/templates/api/migrations/0001_create_users.sql +1 -0
- package/templates/api/package.json +16 -0
- package/templates/api/public/hello.txt +1 -0
- package/templates/api/sloppy.json +14 -0
- package/templates/api/src/config.ts +1 -0
- package/templates/api/src/db/migrate.ts +14 -0
- package/templates/api/src/db/schema.ts +4 -0
- package/templates/api/src/db/usersRepository.ts +23 -0
- package/templates/api/src/main.ts +18 -0
- package/templates/api/src/models/user.ts +7 -0
- package/templates/api/src/routes/health.ts +20 -0
- package/templates/api/src/routes/users.ts +40 -0
- package/templates/api/src/services/usersService.ts +21 -0
- package/templates/api/tsconfig.json +15 -0
- package/templates/cli/README.md +16 -0
- package/templates/cli/gitignore +2 -0
- package/templates/cli/package.json +13 -0
- package/templates/cli/sloppy.json +6 -0
- package/templates/cli/src/commands/echo.ts +9 -0
- package/templates/cli/src/commands/inspect.ts +20 -0
- package/templates/cli/src/main.ts +50 -0
- package/templates/cli/tsconfig.json +15 -0
- package/templates/minimal-api/README.md +14 -0
- package/templates/minimal-api/gitignore +3 -0
- package/templates/minimal-api/package.json +14 -0
- package/templates/minimal-api/sloppy.json +5 -0
- package/templates/minimal-api/src/main.ts +9 -0
- package/templates/minimal-api/tsconfig.json +15 -0
- package/templates/node-compat/README.md +40 -0
- package/templates/node-compat/gitignore +2 -0
- package/templates/node-compat/package.json +11 -0
- package/templates/node-compat/sloppy.json +6 -0
- package/templates/node-compat/src/main.ts +40 -0
- package/templates/package-api/README.md +44 -0
- package/templates/package-api/fixtures/validator-lite/index.js +7 -0
- package/templates/package-api/fixtures/validator-lite/package.json +6 -0
- package/templates/package-api/gitignore +3 -0
- package/templates/package-api/package.json +17 -0
- package/templates/package-api/sloppy.json +5 -0
- package/templates/package-api/src/main.ts +10 -0
- package/templates/package-api/src/routes/health.ts +5 -0
- package/templates/package-api/src/routes/users.ts +12 -0
- package/templates/package-api/tsconfig.json +15 -0
- package/templates/program/README.md +12 -0
- package/templates/program/gitignore +1 -0
- package/templates/program/package.json +10 -0
- package/templates/program/sloppy.json +6 -0
- package/templates/program/src/main.ts +9 -0
|
@@ -0,0 +1,1813 @@
|
|
|
1
|
+
import { Base64Url, Hex, Text } from "./codec.js";
|
|
2
|
+
import { Hash, Hmac, Password, Random } from "./crypto.js";
|
|
3
|
+
import {
|
|
4
|
+
isPlainObject,
|
|
5
|
+
optionalBoolean,
|
|
6
|
+
optionalInteger,
|
|
7
|
+
optionalPositiveInteger,
|
|
8
|
+
requireHttpToken,
|
|
9
|
+
requireNonEmptyString,
|
|
10
|
+
} from "./internal/validation.js";
|
|
11
|
+
import { Results } from "./results.js";
|
|
12
|
+
|
|
13
|
+
const AUTH_HEADER = "authorization";
|
|
14
|
+
const JWT_SCHEME = "bearerAuth";
|
|
15
|
+
const API_KEY_SCHEME = "apiKeyAuth";
|
|
16
|
+
const COOKIE_SESSION_SCHEME = "cookieSessionAuth";
|
|
17
|
+
const DEFAULT_SESSION_COOKIE = "sloppy.session";
|
|
18
|
+
const DEFAULT_CSRF_COOKIE = "__Host-sloppy_csrf";
|
|
19
|
+
const DEFAULT_CSRF_HEADER = "x-csrf-token";
|
|
20
|
+
const SAFE_CSRF_METHODS = new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
|
|
21
|
+
const STANDARD_JWT_CLAIMS = new Set(["iss", "sub", "aud", "exp", "nbf", "iat", "jti", "name", "role", "roles", "scope", "scp", "scopes"]);
|
|
22
|
+
const DEFAULT_SESSION_IDLE_TIMEOUT_MS = 30 * 60_000;
|
|
23
|
+
const DEFAULT_SESSION_ABSOLUTE_TIMEOUT_MS = 24 * 60 * 60_000;
|
|
24
|
+
const DEFAULT_SIGNED_SESSION_MAX_AGE_SECONDS = 24 * 60 * 60;
|
|
25
|
+
|
|
26
|
+
function validateHeaderName(name, subject) {
|
|
27
|
+
requireHttpToken(name, `Sloppy ${subject} header must be an HTTP token string.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isConfigReference(value) {
|
|
31
|
+
return isPlainObject(value) && value.__sloppyConfigReference === true && typeof value.key === "string";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function secretString(value, config, subject) {
|
|
35
|
+
let resolved = value;
|
|
36
|
+
if (isConfigReference(value)) {
|
|
37
|
+
resolved = config.require(value.key);
|
|
38
|
+
}
|
|
39
|
+
if (resolved !== null && typeof resolved === "object" && typeof resolved.value === "function") {
|
|
40
|
+
resolved = resolved.value();
|
|
41
|
+
}
|
|
42
|
+
if (typeof resolved !== "string" || resolved.length === 0) {
|
|
43
|
+
throw new TypeError(`Sloppy ${subject} secret must resolve to a non-empty string.`);
|
|
44
|
+
}
|
|
45
|
+
return resolved;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stringOption(value, subject, required = true) {
|
|
49
|
+
if (value === undefined && !required) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return requireNonEmptyString(value, `Sloppy ${subject} must be a non-empty string.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stringArrayOption(value, subject, { lower = false } = {}) {
|
|
56
|
+
if (value === undefined) {
|
|
57
|
+
return Object.freeze([]);
|
|
58
|
+
}
|
|
59
|
+
const values = Array.isArray(value) ? value : [value];
|
|
60
|
+
const output = values.map((entry) => {
|
|
61
|
+
const normalized = stringOption(entry, subject);
|
|
62
|
+
return lower ? normalized.toLowerCase() : normalized;
|
|
63
|
+
});
|
|
64
|
+
return Object.freeze([...new Set(output)]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function booleanOption(value, subject, defaultValue) {
|
|
68
|
+
return optionalBoolean(value, `Sloppy ${subject} must be a boolean.`, defaultValue);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function integerOption(value, subject, defaultValue = undefined) {
|
|
72
|
+
return optionalInteger(value, `Sloppy ${subject} must be an integer.`, defaultValue);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function authProblem(status, title, code, headers = undefined) {
|
|
76
|
+
return Results.problem(
|
|
77
|
+
Object.freeze({
|
|
78
|
+
status,
|
|
79
|
+
title,
|
|
80
|
+
code,
|
|
81
|
+
}),
|
|
82
|
+
Object.freeze({
|
|
83
|
+
status,
|
|
84
|
+
...(headers === undefined ? {} : { headers }),
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function unauthorized(challenge = undefined) {
|
|
90
|
+
return authProblem(
|
|
91
|
+
401,
|
|
92
|
+
"Unauthorized",
|
|
93
|
+
"SLOPPY_E_AUTH_UNAUTHORIZED",
|
|
94
|
+
challenge === undefined ? undefined : Object.freeze({ "WWW-Authenticate": challenge }),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function forbidden() {
|
|
99
|
+
return authProblem(403, "Forbidden", "SLOPPY_E_AUTH_FORBIDDEN");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function csrfForbidden() {
|
|
103
|
+
return authProblem(403, "Forbidden", "SLOPPY_E_AUTH_CSRF_FAILED");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function constantTimeStringEquals(left, right) {
|
|
107
|
+
left = String(left);
|
|
108
|
+
right = String(right);
|
|
109
|
+
if (left.length === 0 || right.length === 0) {
|
|
110
|
+
return left.length === right.length;
|
|
111
|
+
}
|
|
112
|
+
let diff = left.length ^ right.length;
|
|
113
|
+
const length = Math.max(left.length, right.length);
|
|
114
|
+
for (let index = 0; index < length; index += 1) {
|
|
115
|
+
diff |= left.charCodeAt(index % left.length) ^ right.charCodeAt(index % right.length);
|
|
116
|
+
}
|
|
117
|
+
return diff === 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function userFromClaims(claims, scheme, authScheme = scheme) {
|
|
121
|
+
const roles = Array.isArray(claims.roles)
|
|
122
|
+
? claims.roles.filter((role) => typeof role === "string")
|
|
123
|
+
: (typeof claims.role === "string" ? [claims.role] : []);
|
|
124
|
+
const scopes = scopesFromClaims(claims);
|
|
125
|
+
const allClaims = Object.freeze({ ...claims });
|
|
126
|
+
const user = {
|
|
127
|
+
authenticated: true,
|
|
128
|
+
sub: typeof claims.sub === "string" ? claims.sub : "",
|
|
129
|
+
name: typeof claims.name === "string" ? claims.name : "",
|
|
130
|
+
roles: Object.freeze([...new Set(roles)]),
|
|
131
|
+
scopes: Object.freeze([...new Set(scopes)]),
|
|
132
|
+
claims: allClaims,
|
|
133
|
+
scheme,
|
|
134
|
+
authScheme,
|
|
135
|
+
hasRole(role) {
|
|
136
|
+
return typeof role === "string" && user.roles.includes(role);
|
|
137
|
+
},
|
|
138
|
+
hasScope(scope) {
|
|
139
|
+
return typeof scope === "string" && user.scopes.includes(scope);
|
|
140
|
+
},
|
|
141
|
+
hasClaim(name, value = undefined) {
|
|
142
|
+
if (typeof name !== "string" || !Object.prototype.hasOwnProperty.call(user.claims, name)) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return value === undefined ? true : Object.is(user.claims[name], value);
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
return Object.freeze(user);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function scopesFromClaims(claims) {
|
|
152
|
+
const scopes = [];
|
|
153
|
+
if (typeof claims.scope === "string") {
|
|
154
|
+
scopes.push(...claims.scope.split(/\s+/u).filter((scope) => scope.length !== 0));
|
|
155
|
+
}
|
|
156
|
+
if (typeof claims.scp === "string") {
|
|
157
|
+
scopes.push(...claims.scp.split(/\s+/u).filter((scope) => scope.length !== 0));
|
|
158
|
+
}
|
|
159
|
+
for (const key of ["scopes", "scp"]) {
|
|
160
|
+
if (Array.isArray(claims[key])) {
|
|
161
|
+
scopes.push(...claims[key].filter((scope) => typeof scope === "string" && scope.length !== 0));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return scopes;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function anonymousUser() {
|
|
168
|
+
const user = {
|
|
169
|
+
authenticated: false,
|
|
170
|
+
sub: "",
|
|
171
|
+
name: "",
|
|
172
|
+
roles: Object.freeze([]),
|
|
173
|
+
scopes: Object.freeze([]),
|
|
174
|
+
claims: Object.freeze({}),
|
|
175
|
+
scheme: "",
|
|
176
|
+
authScheme: "",
|
|
177
|
+
hasRole() {
|
|
178
|
+
return false;
|
|
179
|
+
},
|
|
180
|
+
hasScope() {
|
|
181
|
+
return false;
|
|
182
|
+
},
|
|
183
|
+
hasClaim() {
|
|
184
|
+
return false;
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
return Object.freeze(user);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function headerValue(ctx, name) {
|
|
191
|
+
const headers = ctx?.request?.headers;
|
|
192
|
+
if (headers === undefined || headers === null || typeof headers.get !== "function") {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
return headers.get(name);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function bearerToken(ctx) {
|
|
199
|
+
const value = headerValue(ctx, AUTH_HEADER);
|
|
200
|
+
if (typeof value !== "string") {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
if (/[\r\n,]/u.test(value)) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const match = value.match(/^Bearer ([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+){2})$/iu);
|
|
207
|
+
return match === null ? null : match[1];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function jsonFromBase64Url(value, subject) {
|
|
211
|
+
try {
|
|
212
|
+
return JSON.parse(Text.utf8.decode(Base64Url.decode(value, { padding: "optional" })));
|
|
213
|
+
} catch {
|
|
214
|
+
throw new Error(`SLOPPY_E_AUTH_INVALID_TOKEN: ${subject} is not valid JWT JSON.`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function nowSeconds(clock = Date.now) {
|
|
219
|
+
return Math.floor(clock() / 1000);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function nowMilliseconds(clock = Date.now) {
|
|
223
|
+
return clock();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function audienceMatches(expected, actual) {
|
|
227
|
+
if (expected === undefined) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
if (Array.isArray(actual)) {
|
|
231
|
+
return actual.includes(expected);
|
|
232
|
+
}
|
|
233
|
+
return actual === expected;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function selectJwtKey(header, scheme) {
|
|
237
|
+
if (scheme.keys.length === 0) {
|
|
238
|
+
return Object.freeze({ algorithm: "HS256", secret: scheme.secret });
|
|
239
|
+
}
|
|
240
|
+
const kid = typeof header.kid === "string" ? header.kid : undefined;
|
|
241
|
+
const key = scheme.keys.find((entry) => {
|
|
242
|
+
if (kid !== undefined) {
|
|
243
|
+
return entry.kid === kid;
|
|
244
|
+
}
|
|
245
|
+
return entry.default === true || entry.kid === undefined;
|
|
246
|
+
});
|
|
247
|
+
if (key === undefined) {
|
|
248
|
+
throw new Error("SLOPPY_E_AUTH_INVALID_TOKEN: JWT kid is unknown.");
|
|
249
|
+
}
|
|
250
|
+
return key;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function verifyJwtSignature(header, signingInput, signaturePart, scheme) {
|
|
254
|
+
if (!scheme.algorithms.includes(header.alg)) {
|
|
255
|
+
throw new Error("SLOPPY_E_AUTH_UNSUPPORTED_ALGORITHM: JWT algorithm is not allowed.");
|
|
256
|
+
}
|
|
257
|
+
const key = selectJwtKey(header, scheme);
|
|
258
|
+
if (key.algorithm !== header.alg) {
|
|
259
|
+
throw new Error("SLOPPY_E_AUTH_UNSUPPORTED_ALGORITHM: JWT key algorithm does not match token alg.");
|
|
260
|
+
}
|
|
261
|
+
if (header.alg === "HS256") {
|
|
262
|
+
if (key.secret === undefined && scheme.secret === undefined) {
|
|
263
|
+
throw new Error("SLOPPY_E_AUTH_INVALID_SIGNATURE: JWT HMAC key is not configured.");
|
|
264
|
+
}
|
|
265
|
+
const expected = await Hmac.sha256(key.secret ?? scheme.secret, signingInput);
|
|
266
|
+
const actual = Base64Url.decode(signaturePart, { padding: "optional" });
|
|
267
|
+
if (!constantTimeBytesEquals(expected, actual)) {
|
|
268
|
+
throw new Error("SLOPPY_E_AUTH_INVALID_SIGNATURE: JWT signature is invalid.");
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (header.alg === "RS256") {
|
|
273
|
+
const subtle = globalThis.crypto?.subtle;
|
|
274
|
+
if (subtle === undefined || key.jwk === undefined) {
|
|
275
|
+
throw new Error("SLOPPY_E_AUTH_UNSUPPORTED_ALGORITHM: RS256 requires WebCrypto static JWK support.");
|
|
276
|
+
}
|
|
277
|
+
const cryptoKey = await subtle.importKey(
|
|
278
|
+
"jwk",
|
|
279
|
+
key.jwk,
|
|
280
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
281
|
+
false,
|
|
282
|
+
["verify"],
|
|
283
|
+
);
|
|
284
|
+
const ok = await subtle.verify(
|
|
285
|
+
"RSASSA-PKCS1-v1_5",
|
|
286
|
+
cryptoKey,
|
|
287
|
+
Base64Url.decode(signaturePart, { padding: "optional" }),
|
|
288
|
+
Text.utf8.encode(signingInput),
|
|
289
|
+
);
|
|
290
|
+
if (!ok) {
|
|
291
|
+
throw new Error("SLOPPY_E_AUTH_INVALID_SIGNATURE: JWT signature is invalid.");
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
throw new Error("SLOPPY_E_AUTH_UNSUPPORTED_ALGORITHM: JWT algorithm is not supported.");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function verifyJwt(token, scheme) {
|
|
299
|
+
const parts = String(token).split(".");
|
|
300
|
+
if (parts.length !== 3 || parts.some((part) => part.length === 0)) {
|
|
301
|
+
throw new Error("SLOPPY_E_AUTH_INVALID_TOKEN: JWT must have header, payload, and signature.");
|
|
302
|
+
}
|
|
303
|
+
const header = jsonFromBase64Url(parts[0], "JWT header");
|
|
304
|
+
if (!isPlainObject(header) || typeof header.alg !== "string") {
|
|
305
|
+
throw new Error("SLOPPY_E_AUTH_INVALID_TOKEN: JWT header must include alg.");
|
|
306
|
+
}
|
|
307
|
+
if (header.alg === "none") {
|
|
308
|
+
throw new Error("SLOPPY_E_AUTH_INVALID_TOKEN: JWT alg none is not supported.");
|
|
309
|
+
}
|
|
310
|
+
await verifyJwtSignature(header, `${parts[0]}.${parts[1]}`, parts[2], scheme);
|
|
311
|
+
const claims = jsonFromBase64Url(parts[1], "JWT payload");
|
|
312
|
+
if (!isPlainObject(claims)) {
|
|
313
|
+
throw new Error("SLOPPY_E_AUTH_INVALID_TOKEN: JWT payload must be a JSON object.");
|
|
314
|
+
}
|
|
315
|
+
const current = nowSeconds(scheme.clock);
|
|
316
|
+
if (scheme.issuer !== undefined && claims.iss !== scheme.issuer) {
|
|
317
|
+
throw new Error("SLOPPY_E_AUTH_INVALID_TOKEN: JWT issuer is invalid.");
|
|
318
|
+
}
|
|
319
|
+
if (!audienceMatches(scheme.audience, claims.aud)) {
|
|
320
|
+
throw new Error("SLOPPY_E_AUTH_INVALID_TOKEN: JWT audience is invalid.");
|
|
321
|
+
}
|
|
322
|
+
if (claims.exp !== undefined && (!Number.isFinite(claims.exp) ||
|
|
323
|
+
claims.exp <= current - scheme.clockSkewSeconds)) {
|
|
324
|
+
throw new Error("SLOPPY_E_AUTH_EXPIRED_TOKEN: JWT is expired.");
|
|
325
|
+
}
|
|
326
|
+
if (claims.nbf !== undefined && (!Number.isFinite(claims.nbf) ||
|
|
327
|
+
claims.nbf > current + scheme.clockSkewSeconds)) {
|
|
328
|
+
throw new Error("SLOPPY_E_AUTH_INVALID_TOKEN: JWT is not active yet.");
|
|
329
|
+
}
|
|
330
|
+
if (claims.iat !== undefined && (!Number.isFinite(claims.iat) ||
|
|
331
|
+
claims.iat > current + scheme.clockSkewSeconds)) {
|
|
332
|
+
throw new Error("SLOPPY_E_AUTH_INVALID_TOKEN: JWT iat must be numeric seconds not in the future.");
|
|
333
|
+
}
|
|
334
|
+
if (claims.sub !== undefined && typeof claims.sub !== "string") {
|
|
335
|
+
throw new Error("SLOPPY_E_AUTH_INVALID_TOKEN: JWT sub must be a string when present.");
|
|
336
|
+
}
|
|
337
|
+
return userFromClaims(claims, scheme.principalScheme, scheme.name);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function constantTimeBytesEquals(left, right) {
|
|
341
|
+
if (!(left instanceof Uint8Array) || !(right instanceof Uint8Array)) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
if (left.byteLength === 0 || right.byteLength === 0) {
|
|
345
|
+
return left.byteLength === right.byteLength;
|
|
346
|
+
}
|
|
347
|
+
let diff = left.byteLength ^ right.byteLength;
|
|
348
|
+
const length = Math.max(left.byteLength, right.byteLength);
|
|
349
|
+
for (let index = 0; index < length; index += 1) {
|
|
350
|
+
diff |= left[index % left.byteLength] ^ right[index % right.byteLength];
|
|
351
|
+
}
|
|
352
|
+
return diff === 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function jwtMiddleware(ctx, next, scheme) {
|
|
356
|
+
const token = bearerToken(ctx);
|
|
357
|
+
if (token === undefined) {
|
|
358
|
+
return next();
|
|
359
|
+
}
|
|
360
|
+
if (token === null) {
|
|
361
|
+
return unauthorized("Bearer");
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
ctx.user = await verifyJwt(token, scheme);
|
|
365
|
+
return next();
|
|
366
|
+
} catch {
|
|
367
|
+
return unauthorized("Bearer error=\"invalid_token\"");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function normalizeApiKeyUser(result, scheme) {
|
|
372
|
+
if (result === true) {
|
|
373
|
+
return userFromClaims({ sub: "api-key" }, scheme.principalScheme, scheme.name);
|
|
374
|
+
}
|
|
375
|
+
if (isPlainObject(result)) {
|
|
376
|
+
return userFromClaims(result, scheme.principalScheme, scheme.name);
|
|
377
|
+
}
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function staticApiKeyUser(key, scheme) {
|
|
382
|
+
let keySha256 = undefined;
|
|
383
|
+
for (const entry of scheme.keys) {
|
|
384
|
+
let matched = false;
|
|
385
|
+
if (entry.hash !== undefined) {
|
|
386
|
+
keySha256 ??= `sha256:${Hex.encode(await Hash.sha256(key))}`;
|
|
387
|
+
matched = constantTimeStringEquals(keySha256, entry.hash);
|
|
388
|
+
} else if (entry.key !== undefined) {
|
|
389
|
+
matched = constantTimeStringEquals(key, entry.key);
|
|
390
|
+
}
|
|
391
|
+
if (matched) {
|
|
392
|
+
return userFromClaims({
|
|
393
|
+
sub: entry.id,
|
|
394
|
+
roles: entry.roles,
|
|
395
|
+
scopes: entry.scopes,
|
|
396
|
+
...(entry.claims ?? {}),
|
|
397
|
+
}, scheme.principalScheme, scheme.name);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function apiKeyMiddleware(ctx, next, scheme) {
|
|
404
|
+
let key = headerValue(ctx, scheme.header);
|
|
405
|
+
if (key === undefined && scheme.authorizationScheme !== undefined) {
|
|
406
|
+
const authorization = headerValue(ctx, AUTH_HEADER);
|
|
407
|
+
if (typeof authorization === "string" && !/[\r\n,]/u.test(authorization)) {
|
|
408
|
+
const prefix = `${scheme.authorizationScheme} `;
|
|
409
|
+
key = authorization.toLowerCase().startsWith(prefix.toLowerCase())
|
|
410
|
+
? authorization.slice(prefix.length)
|
|
411
|
+
: undefined;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (key === undefined) {
|
|
415
|
+
return next();
|
|
416
|
+
}
|
|
417
|
+
if (typeof key !== "string" || key.length === 0 || key.length > scheme.maxLength || /[\r\n]/u.test(key)) {
|
|
418
|
+
return unauthorized(`${scheme.header}`);
|
|
419
|
+
}
|
|
420
|
+
if (scheme.keys.length !== 0) {
|
|
421
|
+
const user = await staticApiKeyUser(key, scheme);
|
|
422
|
+
if (user === undefined) {
|
|
423
|
+
return unauthorized(`${scheme.header}`);
|
|
424
|
+
}
|
|
425
|
+
ctx.user = user;
|
|
426
|
+
return next();
|
|
427
|
+
}
|
|
428
|
+
if (scheme.expectedKey !== undefined) {
|
|
429
|
+
if (!constantTimeStringEquals(key, scheme.expectedKey)) {
|
|
430
|
+
return unauthorized(`${scheme.header}`);
|
|
431
|
+
}
|
|
432
|
+
ctx.user = userFromClaims({ sub: "api-key" }, scheme.principalScheme, scheme.name);
|
|
433
|
+
return next();
|
|
434
|
+
}
|
|
435
|
+
let result;
|
|
436
|
+
try {
|
|
437
|
+
result = await scheme.validate(key, {
|
|
438
|
+
constantTimeEquals: constantTimeStringEquals,
|
|
439
|
+
expectedKey: scheme.configKey === undefined ? undefined : secretString(
|
|
440
|
+
{ __sloppyConfigReference: true, key: scheme.configKey },
|
|
441
|
+
scheme.config,
|
|
442
|
+
"API key",
|
|
443
|
+
),
|
|
444
|
+
});
|
|
445
|
+
} catch {
|
|
446
|
+
result = false;
|
|
447
|
+
}
|
|
448
|
+
const user = normalizeApiKeyUser(result, scheme);
|
|
449
|
+
if (user === undefined) {
|
|
450
|
+
return unauthorized(`${scheme.header}`);
|
|
451
|
+
}
|
|
452
|
+
ctx.user = user;
|
|
453
|
+
return next();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function sessionCookieValue(ctx, name) {
|
|
457
|
+
const cookies = ctx?.cookies;
|
|
458
|
+
if (cookies === undefined || cookies === null || typeof cookies.get !== "function") {
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
return cookies.get(name) ?? undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function normalizeSessionClaims(claims) {
|
|
465
|
+
if (!isPlainObject(claims)) {
|
|
466
|
+
throw new TypeError("Sloppy Auth.signIn claims must be a plain object.");
|
|
467
|
+
}
|
|
468
|
+
const copied = { ...claims };
|
|
469
|
+
if (copied.sub !== undefined && typeof copied.sub !== "string") {
|
|
470
|
+
throw new TypeError("Sloppy Auth.signIn sub must be a string when provided.");
|
|
471
|
+
}
|
|
472
|
+
if (copied.roles !== undefined && (!Array.isArray(copied.roles) ||
|
|
473
|
+
!copied.roles.every((role) => typeof role === "string"))) {
|
|
474
|
+
throw new TypeError("Sloppy Auth.signIn roles must be an array of strings when provided.");
|
|
475
|
+
}
|
|
476
|
+
if (copied.claims !== undefined) {
|
|
477
|
+
if (!isPlainObject(copied.claims)) {
|
|
478
|
+
throw new TypeError("Sloppy Auth.signIn claims.claims must be a plain object when provided.");
|
|
479
|
+
}
|
|
480
|
+
Object.assign(copied, copied.claims);
|
|
481
|
+
delete copied.claims;
|
|
482
|
+
}
|
|
483
|
+
return copied;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function sessionCookieOptions(scheme, overrides = undefined) {
|
|
487
|
+
if (overrides !== undefined && !isPlainObject(overrides)) {
|
|
488
|
+
throw new TypeError("Sloppy Auth session cookie options must be a plain object.");
|
|
489
|
+
}
|
|
490
|
+
return Object.freeze({
|
|
491
|
+
path: overrides?.path ?? scheme.path,
|
|
492
|
+
secure: overrides?.secure ?? scheme.secure,
|
|
493
|
+
httpOnly: overrides?.httpOnly ?? scheme.httpOnly,
|
|
494
|
+
sameSite: overrides?.sameSite ?? scheme.sameSite,
|
|
495
|
+
...(overrides?.maxAgeSeconds !== undefined || scheme.maxAgeSeconds !== undefined
|
|
496
|
+
? { maxAgeSeconds: overrides?.maxAgeSeconds ?? scheme.maxAgeSeconds }
|
|
497
|
+
: {}),
|
|
498
|
+
...(overrides?.expires !== undefined ? { expires: overrides.expires } : {}),
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function sessionStoreCookieOptions(scheme, overrides = undefined, remainingAbsoluteLifetimeMs = undefined) {
|
|
503
|
+
const options = sessionCookieOptions(scheme, overrides);
|
|
504
|
+
let maxAgeSeconds = options.maxAgeSeconds;
|
|
505
|
+
if (overrides?.maxAgeSeconds === undefined && scheme.absoluteTimeoutMs !== undefined) {
|
|
506
|
+
maxAgeSeconds = Math.ceil(scheme.absoluteTimeoutMs / 1000);
|
|
507
|
+
}
|
|
508
|
+
if (remainingAbsoluteLifetimeMs !== undefined) {
|
|
509
|
+
const remainingSeconds = Math.max(0, Math.ceil(remainingAbsoluteLifetimeMs / 1000));
|
|
510
|
+
maxAgeSeconds = maxAgeSeconds === undefined
|
|
511
|
+
? remainingSeconds
|
|
512
|
+
: Math.min(maxAgeSeconds, remainingSeconds);
|
|
513
|
+
}
|
|
514
|
+
return Object.freeze({
|
|
515
|
+
...options,
|
|
516
|
+
...(maxAgeSeconds !== undefined ? { maxAgeSeconds } : {}),
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function signSessionPayload(scheme, payload) {
|
|
521
|
+
const body = Base64Url.encode(Text.utf8.encode(JSON.stringify(payload)));
|
|
522
|
+
const signature = await Hmac.sha256(scheme.secret, body);
|
|
523
|
+
return `${body}.${Base64Url.encode(signature)}`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function verifySessionCookie(value, scheme) {
|
|
527
|
+
const parts = String(value).split(".");
|
|
528
|
+
if (parts.length !== 2 || parts.some((part) => part.length === 0)) {
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
const expected = await Hmac.sha256(scheme.secret, parts[0]);
|
|
532
|
+
let actual;
|
|
533
|
+
try {
|
|
534
|
+
actual = Base64Url.decode(parts[1], { padding: "optional" });
|
|
535
|
+
} catch {
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
if (!constantTimeBytesEquals(expected, actual)) {
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
let payload;
|
|
542
|
+
try {
|
|
543
|
+
payload = JSON.parse(Text.utf8.decode(Base64Url.decode(parts[0], { padding: "optional" })));
|
|
544
|
+
} catch {
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
if (!isPlainObject(payload) || !isPlainObject(payload.claims)) {
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
550
|
+
const current = nowSeconds(scheme.clock);
|
|
551
|
+
if (payload.exp !== undefined && (!Number.isFinite(payload.exp) || payload.exp <= current)) {
|
|
552
|
+
return undefined;
|
|
553
|
+
}
|
|
554
|
+
const user = userFromClaims(payload.claims, scheme.principalScheme, scheme.name);
|
|
555
|
+
return Object.freeze({ user, payload });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function signSessionId(scheme, sessionId) {
|
|
559
|
+
return signSessionPayload(scheme, { sid: sessionId });
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function verifySessionIdCookie(value, scheme) {
|
|
563
|
+
const parts = String(value).split(".");
|
|
564
|
+
if (parts.length !== 2 || parts.some((part) => part.length === 0)) {
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
const expected = await Hmac.sha256(scheme.secret, parts[0]);
|
|
568
|
+
let actual;
|
|
569
|
+
try {
|
|
570
|
+
actual = Base64Url.decode(parts[1], { padding: "optional" });
|
|
571
|
+
} catch {
|
|
572
|
+
return undefined;
|
|
573
|
+
}
|
|
574
|
+
if (!constantTimeBytesEquals(expected, actual)) {
|
|
575
|
+
return undefined;
|
|
576
|
+
}
|
|
577
|
+
let payload;
|
|
578
|
+
try {
|
|
579
|
+
payload = JSON.parse(Text.utf8.decode(Base64Url.decode(parts[0], { padding: "optional" })));
|
|
580
|
+
} catch {
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
return typeof payload.sid === "string" && payload.sid.length !== 0 ? payload.sid : undefined;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function sessionRecordExpired(record, current) {
|
|
587
|
+
return record.revokedAt !== undefined ||
|
|
588
|
+
(record.expiresAt !== undefined && record.expiresAt <= current) ||
|
|
589
|
+
(record.idleExpiresAt !== undefined && record.idleExpiresAt <= current);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function verifyStoredSessionCookie(value, scheme) {
|
|
593
|
+
const sessionId = await verifySessionIdCookie(value, scheme);
|
|
594
|
+
if (sessionId === undefined) {
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
const current = nowMilliseconds(scheme.clock);
|
|
598
|
+
const record = await scheme.store.load(sessionId);
|
|
599
|
+
if (record === undefined || sessionRecordExpired(record, current)) {
|
|
600
|
+
if (record !== undefined) {
|
|
601
|
+
await scheme.store.revoke(sessionId, current).catch(() => {});
|
|
602
|
+
}
|
|
603
|
+
return undefined;
|
|
604
|
+
}
|
|
605
|
+
const nextIdleExpiresAt = scheme.idleTimeoutMs === undefined ? undefined : current + scheme.idleTimeoutMs;
|
|
606
|
+
const touched = await scheme.store.touch(sessionId, current, nextIdleExpiresAt);
|
|
607
|
+
const active = touched ?? await scheme.store.load(sessionId);
|
|
608
|
+
if (active === undefined || sessionRecordExpired(active, current)) {
|
|
609
|
+
return undefined;
|
|
610
|
+
}
|
|
611
|
+
const user = userFromClaims(active.claims, scheme.principalScheme, scheme.name);
|
|
612
|
+
return Object.freeze({
|
|
613
|
+
user,
|
|
614
|
+
payload: Object.freeze({
|
|
615
|
+
sid: sessionId,
|
|
616
|
+
iat: Math.floor(active.createdAt / 1000),
|
|
617
|
+
exp: active.expiresAt === undefined ? undefined : Math.floor(active.expiresAt / 1000),
|
|
618
|
+
csrf: active.csrf,
|
|
619
|
+
}),
|
|
620
|
+
record: active,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function cookieSessionMiddleware(ctx, next, scheme) {
|
|
625
|
+
const value = sessionCookieValue(ctx, scheme.cookieName);
|
|
626
|
+
if (value === undefined) {
|
|
627
|
+
return next();
|
|
628
|
+
}
|
|
629
|
+
const verified = scheme.store === undefined
|
|
630
|
+
? await verifySessionCookie(value, scheme)
|
|
631
|
+
: await verifyStoredSessionCookie(value, scheme);
|
|
632
|
+
if (verified === undefined) {
|
|
633
|
+
return unauthorized();
|
|
634
|
+
}
|
|
635
|
+
ctx.user = verified.user;
|
|
636
|
+
ctx.session = Object.freeze({
|
|
637
|
+
scheme: scheme.name,
|
|
638
|
+
id: verified.payload.sid,
|
|
639
|
+
issuedAt: verified.payload.iat,
|
|
640
|
+
expiresAt: verified.payload.exp,
|
|
641
|
+
csrfToken: verified.payload.csrf,
|
|
642
|
+
revoke: verified.payload.sid === undefined
|
|
643
|
+
? undefined
|
|
644
|
+
: () => scheme.store.revoke(verified.payload.sid, nowMilliseconds(scheme.clock)),
|
|
645
|
+
});
|
|
646
|
+
if (scheme.csrf.enabled && !SAFE_CSRF_METHODS.has(String(ctx.request?.method ?? "").toUpperCase())) {
|
|
647
|
+
const header = headerValue(ctx, scheme.csrf.header);
|
|
648
|
+
const cookie = sessionCookieValue(ctx, scheme.csrf.cookieName);
|
|
649
|
+
if (
|
|
650
|
+
typeof verified.payload.csrf !== "string" ||
|
|
651
|
+
typeof header !== "string" ||
|
|
652
|
+
typeof cookie !== "string" ||
|
|
653
|
+
!constantTimeStringEquals(header, verified.payload.csrf) ||
|
|
654
|
+
!constantTimeStringEquals(cookie, verified.payload.csrf)
|
|
655
|
+
) {
|
|
656
|
+
return csrfForbidden();
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (scheme.store === undefined || scheme.rotation !== true || verified.payload.sid === undefined) {
|
|
660
|
+
return next();
|
|
661
|
+
}
|
|
662
|
+
const result = await next();
|
|
663
|
+
if (result === null || typeof result !== "object" || typeof result.cookie !== "function") {
|
|
664
|
+
return result;
|
|
665
|
+
}
|
|
666
|
+
const currentMs = nowMilliseconds(scheme.clock);
|
|
667
|
+
const remainingAbsoluteLifetimeMs = verified.record.expiresAt === undefined
|
|
668
|
+
? undefined
|
|
669
|
+
: verified.record.expiresAt - currentMs;
|
|
670
|
+
if (remainingAbsoluteLifetimeMs !== undefined && remainingAbsoluteLifetimeMs <= 0) {
|
|
671
|
+
await scheme.store.revoke(verified.payload.sid, currentMs);
|
|
672
|
+
return unauthorized();
|
|
673
|
+
}
|
|
674
|
+
const sessionId = Random.token(32);
|
|
675
|
+
await scheme.store.revoke(verified.payload.sid, currentMs);
|
|
676
|
+
const csrf = scheme.csrf.enabled ? Random.token(32) : verified.payload.csrf;
|
|
677
|
+
const record = {
|
|
678
|
+
id: sessionId,
|
|
679
|
+
claims: verified.record.claims,
|
|
680
|
+
createdAt: verified.record.createdAt,
|
|
681
|
+
lastSeenAt: currentMs,
|
|
682
|
+
expiresAt: verified.record.expiresAt,
|
|
683
|
+
idleExpiresAt: scheme.idleTimeoutMs === undefined ? undefined : currentMs + scheme.idleTimeoutMs,
|
|
684
|
+
csrf,
|
|
685
|
+
metadata: verified.record.metadata,
|
|
686
|
+
};
|
|
687
|
+
await scheme.store.create(record);
|
|
688
|
+
const rotatedValue = await signSessionId(scheme, sessionId);
|
|
689
|
+
ctx.session = Object.freeze({
|
|
690
|
+
scheme: scheme.name,
|
|
691
|
+
id: sessionId,
|
|
692
|
+
issuedAt: Math.floor(record.createdAt / 1000),
|
|
693
|
+
expiresAt: Math.floor(record.expiresAt / 1000),
|
|
694
|
+
csrfToken: csrf,
|
|
695
|
+
revoke: () => scheme.store.revoke(sessionId, nowMilliseconds(scheme.clock)),
|
|
696
|
+
});
|
|
697
|
+
let rotatedResult = result.cookie(
|
|
698
|
+
scheme.cookieName,
|
|
699
|
+
rotatedValue,
|
|
700
|
+
sessionStoreCookieOptions(scheme, undefined, remainingAbsoluteLifetimeMs),
|
|
701
|
+
);
|
|
702
|
+
if (scheme.csrf.enabled) {
|
|
703
|
+
rotatedResult = rotatedResult.cookie(scheme.csrf.cookieName, csrf, {
|
|
704
|
+
path: scheme.path,
|
|
705
|
+
secure: scheme.secure,
|
|
706
|
+
httpOnly: false,
|
|
707
|
+
sameSite: scheme.sameSite,
|
|
708
|
+
...(remainingAbsoluteLifetimeMs !== undefined
|
|
709
|
+
? { maxAgeSeconds: Math.max(0, Math.ceil(remainingAbsoluteLifetimeMs / 1000)) }
|
|
710
|
+
: {}),
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
return rotatedResult;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export function createAuthState() {
|
|
717
|
+
return {
|
|
718
|
+
schemes: [],
|
|
719
|
+
schemesByName: new Map(),
|
|
720
|
+
policies: new Map(),
|
|
721
|
+
defaultScheme: undefined,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function snapshotScheme(scheme) {
|
|
726
|
+
if (scheme.kind === "jwtBearer") {
|
|
727
|
+
return Object.freeze({
|
|
728
|
+
kind: scheme.kind,
|
|
729
|
+
name: scheme.name,
|
|
730
|
+
algorithms: Object.freeze([...scheme.algorithms]),
|
|
731
|
+
issuer: scheme.issuer,
|
|
732
|
+
audience: scheme.audience,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
if (scheme.kind === "cookieSession") {
|
|
736
|
+
return Object.freeze({
|
|
737
|
+
kind: scheme.kind,
|
|
738
|
+
name: scheme.name,
|
|
739
|
+
cookie: scheme.cookieName,
|
|
740
|
+
csrf: scheme.csrf.enabled,
|
|
741
|
+
store: scheme.store === undefined ? "signed-cookie" : scheme.store.kind,
|
|
742
|
+
idleTimeoutMs: scheme.idleTimeoutMs,
|
|
743
|
+
absoluteTimeoutMs: scheme.absoluteTimeoutMs,
|
|
744
|
+
rotation: scheme.rotation,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
return Object.freeze({
|
|
748
|
+
kind: scheme.kind,
|
|
749
|
+
name: scheme.name,
|
|
750
|
+
header: scheme.header,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
export function snapshotAuthState(state) {
|
|
755
|
+
return Object.freeze({
|
|
756
|
+
schemes: Object.freeze(state.schemes.map(snapshotScheme)),
|
|
757
|
+
defaultScheme: state.defaultScheme,
|
|
758
|
+
policies: Object.freeze([...state.policies.keys()]),
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export function isAuthProviderDescriptor(value) {
|
|
763
|
+
return isPlainObject(value) && value.__sloppyAuth === true;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function registerScheme(state, scheme) {
|
|
767
|
+
if (state.schemesByName.has(scheme.name)) {
|
|
768
|
+
throw new TypeError(`Sloppy auth scheme '${scheme.name}' is already registered.`);
|
|
769
|
+
}
|
|
770
|
+
state.schemes.push(scheme);
|
|
771
|
+
state.schemesByName.set(scheme.name, scheme);
|
|
772
|
+
state.defaultScheme ??= scheme.name;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export function registerAuthProvider(state, provider, config) {
|
|
776
|
+
if (provider.kind === "configure") {
|
|
777
|
+
if (provider.defaultScheme !== undefined) {
|
|
778
|
+
state.defaultScheme = provider.defaultScheme;
|
|
779
|
+
}
|
|
780
|
+
const middleware = provider.providers.map((entry) => registerAuthProvider(state, entry, config));
|
|
781
|
+
for (const name of provider.schemeNames) {
|
|
782
|
+
if (!state.schemesByName.has(name)) {
|
|
783
|
+
throw new TypeError(`Sloppy auth default or route scheme '${name}' is not configured.`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return (ctx, next) => invokeAuthMiddlewareList(ctx, next, middleware);
|
|
787
|
+
}
|
|
788
|
+
if (provider.kind === "jwtBearer") {
|
|
789
|
+
const scheme = Object.freeze({
|
|
790
|
+
kind: "jwtBearer",
|
|
791
|
+
name: provider.name,
|
|
792
|
+
principalScheme: provider.principalScheme,
|
|
793
|
+
algorithms: provider.algorithms,
|
|
794
|
+
keys: provider.keys.map((key) => Object.freeze({
|
|
795
|
+
...key,
|
|
796
|
+
secret: key.secret === undefined ? undefined : secretString(key.secret, config, "JWT bearer key"),
|
|
797
|
+
})),
|
|
798
|
+
issuer: provider.issuer,
|
|
799
|
+
audience: provider.audience,
|
|
800
|
+
secret: provider.secret === undefined ? undefined : secretString(provider.secret, config, "JWT bearer"),
|
|
801
|
+
clock: provider.clock,
|
|
802
|
+
clockSkewSeconds: provider.clockSkewSeconds,
|
|
803
|
+
});
|
|
804
|
+
registerScheme(state, scheme);
|
|
805
|
+
return (ctx, next) => jwtMiddleware(ctx, next, scheme);
|
|
806
|
+
}
|
|
807
|
+
if (provider.kind === "cookieSession") {
|
|
808
|
+
const store = materializeSessionStore(provider.store);
|
|
809
|
+
const scheme = Object.freeze({
|
|
810
|
+
kind: "cookieSession",
|
|
811
|
+
name: provider.name,
|
|
812
|
+
principalScheme: provider.principalScheme,
|
|
813
|
+
cookieName: provider.cookieName,
|
|
814
|
+
secret: secretString(provider.secret, config, "cookie session"),
|
|
815
|
+
secure: provider.secure,
|
|
816
|
+
httpOnly: provider.httpOnly,
|
|
817
|
+
sameSite: provider.sameSite,
|
|
818
|
+
path: provider.path,
|
|
819
|
+
maxAgeSeconds: provider.maxAgeSeconds,
|
|
820
|
+
idleTimeoutMs: provider.idleTimeoutMs,
|
|
821
|
+
absoluteTimeoutMs: provider.absoluteTimeoutMs,
|
|
822
|
+
rotation: provider.rotation,
|
|
823
|
+
clock: provider.clock,
|
|
824
|
+
csrf: provider.csrf,
|
|
825
|
+
store,
|
|
826
|
+
});
|
|
827
|
+
registerScheme(state, scheme);
|
|
828
|
+
state.defaultSession = scheme;
|
|
829
|
+
return (ctx, next) => cookieSessionMiddleware(ctx, next, scheme);
|
|
830
|
+
}
|
|
831
|
+
if (provider.kind === "apiKey") {
|
|
832
|
+
const scheme = Object.freeze({
|
|
833
|
+
kind: "apiKey",
|
|
834
|
+
name: provider.name,
|
|
835
|
+
principalScheme: provider.principalScheme,
|
|
836
|
+
header: provider.header,
|
|
837
|
+
authorizationScheme: provider.authorizationScheme,
|
|
838
|
+
maxLength: provider.maxLength,
|
|
839
|
+
keys: provider.keys,
|
|
840
|
+
validate: provider.validate,
|
|
841
|
+
config: config,
|
|
842
|
+
configKey: provider.configKey,
|
|
843
|
+
expectedKey: provider.configKey === undefined || provider.usesStaticConfigEquality !== true ? undefined : secretString(
|
|
844
|
+
{ __sloppyConfigReference: true, key: provider.configKey },
|
|
845
|
+
config,
|
|
846
|
+
"API key",
|
|
847
|
+
),
|
|
848
|
+
});
|
|
849
|
+
registerScheme(state, scheme);
|
|
850
|
+
return (ctx, next) => apiKeyMiddleware(ctx, next, scheme);
|
|
851
|
+
}
|
|
852
|
+
throw new TypeError(`Sloppy Auth provider kind '${provider.kind}' is not supported.`);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function invokeAuthMiddlewareList(ctx, next, middleware) {
|
|
856
|
+
let index = -1;
|
|
857
|
+
function dispatch(nextIndex) {
|
|
858
|
+
if (nextIndex <= index) {
|
|
859
|
+
throw new Error("Sloppy auth middleware next() must not be called more than once.");
|
|
860
|
+
}
|
|
861
|
+
index = nextIndex;
|
|
862
|
+
const current = middleware[nextIndex];
|
|
863
|
+
return current === undefined ? next() : current(ctx, () => dispatch(nextIndex + 1));
|
|
864
|
+
}
|
|
865
|
+
return dispatch(0);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
export function normalizeAuthRequirement(options = undefined) {
|
|
869
|
+
if (options === undefined) {
|
|
870
|
+
return Object.freeze({ required: true });
|
|
871
|
+
}
|
|
872
|
+
if (typeof options === "string" || Array.isArray(options)) {
|
|
873
|
+
return Object.freeze({ required: true, schemes: stringArrayOption(options, "auth scheme") });
|
|
874
|
+
}
|
|
875
|
+
if (!isPlainObject(options)) {
|
|
876
|
+
throw new TypeError("Sloppy requireAuth options must be a plain object when provided.");
|
|
877
|
+
}
|
|
878
|
+
if (options.allowAnonymous === true) {
|
|
879
|
+
return Object.freeze({ required: false, allowAnonymous: true });
|
|
880
|
+
}
|
|
881
|
+
const requirement = { required: true };
|
|
882
|
+
if (options.scheme !== undefined) {
|
|
883
|
+
requirement.schemes = stringArrayOption(options.scheme, "auth scheme");
|
|
884
|
+
}
|
|
885
|
+
if (options.schemes !== undefined) {
|
|
886
|
+
requirement.schemes = stringArrayOption(options.schemes, "auth scheme");
|
|
887
|
+
}
|
|
888
|
+
if (options.role !== undefined) {
|
|
889
|
+
requirement.roles = Object.freeze([stringOption(options.role, "auth role")]);
|
|
890
|
+
}
|
|
891
|
+
if (options.roles !== undefined) {
|
|
892
|
+
if (!Array.isArray(options.roles)) {
|
|
893
|
+
throw new TypeError("Sloppy requireAuth roles must be an array when provided.");
|
|
894
|
+
}
|
|
895
|
+
requirement.roles = Object.freeze(options.roles.map((role) => stringOption(role, "auth role")));
|
|
896
|
+
}
|
|
897
|
+
if (options.policy !== undefined) {
|
|
898
|
+
requirement.policy = stringOption(options.policy, "auth policy");
|
|
899
|
+
}
|
|
900
|
+
if (options.claim !== undefined) {
|
|
901
|
+
requirement.claims = Object.freeze([stringOption(options.claim, "auth claim")]);
|
|
902
|
+
}
|
|
903
|
+
if (options.scope !== undefined) {
|
|
904
|
+
requirement.scopes = stringArrayOption(options.scope, "auth scope");
|
|
905
|
+
}
|
|
906
|
+
if (options.scopes !== undefined) {
|
|
907
|
+
requirement.scopes = stringArrayOption(options.scopes, "auth scope");
|
|
908
|
+
}
|
|
909
|
+
return Object.freeze(requirement);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
export function snapshotAuthRequirement(requirement) {
|
|
913
|
+
if (requirement === undefined || requirement === null) {
|
|
914
|
+
return undefined;
|
|
915
|
+
}
|
|
916
|
+
return Object.freeze({
|
|
917
|
+
required: requirement.required === true,
|
|
918
|
+
...(requirement.allowAnonymous === true ? { allowAnonymous: true } : {}),
|
|
919
|
+
...(requirement.schemes === undefined ? {} : { schemes: Object.freeze([...requirement.schemes]) }),
|
|
920
|
+
...(requirement.scopes === undefined ? {} : { scopes: Object.freeze([...requirement.scopes]) }),
|
|
921
|
+
...(requirement.roles === undefined ? {} : { roles: Object.freeze([...requirement.roles]) }),
|
|
922
|
+
...(requirement.policy === undefined ? {} : { policy: requirement.policy }),
|
|
923
|
+
...(requirement.claims === undefined ? {} : { claims: Object.freeze([...requirement.claims]) }),
|
|
924
|
+
...(requirement.scopes === undefined ? {} : { scopes: Object.freeze([...requirement.scopes]) }),
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function policyFailed(user, kind, values, ctx, resource) {
|
|
929
|
+
if (kind === "authenticated") {
|
|
930
|
+
return user?.authenticated !== true;
|
|
931
|
+
}
|
|
932
|
+
if (kind === "scope") {
|
|
933
|
+
return !values.every((scope) => user?.hasScope(scope) === true);
|
|
934
|
+
}
|
|
935
|
+
if (kind === "role") {
|
|
936
|
+
return !values.some((role) => user?.hasRole(role) === true);
|
|
937
|
+
}
|
|
938
|
+
if (kind === "claim") {
|
|
939
|
+
return !user?.hasClaim(values.name, values.value);
|
|
940
|
+
}
|
|
941
|
+
if (kind === "custom") {
|
|
942
|
+
return values(user, ctx, resource) !== true;
|
|
943
|
+
}
|
|
944
|
+
return true;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function authPolicy(configure) {
|
|
948
|
+
if (typeof configure !== "function") {
|
|
949
|
+
throw new TypeError("Sloppy Auth.policy requires a builder callback.");
|
|
950
|
+
}
|
|
951
|
+
const requirements = [];
|
|
952
|
+
const builder = Object.freeze({
|
|
953
|
+
requireAuthenticated() {
|
|
954
|
+
requirements.push(Object.freeze({ kind: "authenticated" }));
|
|
955
|
+
return builder;
|
|
956
|
+
},
|
|
957
|
+
requireScope(...scopes) {
|
|
958
|
+
requirements.push(Object.freeze({ kind: "scope", values: stringArrayOption(scopes, "auth policy scope") }));
|
|
959
|
+
return builder;
|
|
960
|
+
},
|
|
961
|
+
requireRole(...roles) {
|
|
962
|
+
requirements.push(Object.freeze({ kind: "role", values: stringArrayOption(roles, "auth policy role") }));
|
|
963
|
+
return builder;
|
|
964
|
+
},
|
|
965
|
+
requireClaim(name, value = undefined) {
|
|
966
|
+
requirements.push(Object.freeze({
|
|
967
|
+
kind: "claim",
|
|
968
|
+
values: Object.freeze({ name: stringOption(name, "auth policy claim"), value }),
|
|
969
|
+
}));
|
|
970
|
+
return builder;
|
|
971
|
+
},
|
|
972
|
+
custom(predicate) {
|
|
973
|
+
if (typeof predicate !== "function") {
|
|
974
|
+
throw new TypeError("Sloppy Auth.policy custom predicate must be a function.");
|
|
975
|
+
}
|
|
976
|
+
requirements.push(Object.freeze({ kind: "custom", values: predicate }));
|
|
977
|
+
return builder;
|
|
978
|
+
},
|
|
979
|
+
});
|
|
980
|
+
configure(builder);
|
|
981
|
+
return Object.freeze({
|
|
982
|
+
__sloppyPolicy: true,
|
|
983
|
+
requirements: Object.freeze([...requirements]),
|
|
984
|
+
async evaluate(user, ctx, resource = undefined) {
|
|
985
|
+
for (const requirement of requirements) {
|
|
986
|
+
if (requirement.kind === "custom") {
|
|
987
|
+
if (await requirement.values(user, ctx, resource) !== true) {
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (policyFailed(user, requirement.kind, requirement.values, ctx, resource)) {
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return true;
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
export function normalizeAuthPolicy(policy) {
|
|
1002
|
+
if (typeof policy === "function") {
|
|
1003
|
+
return policy;
|
|
1004
|
+
}
|
|
1005
|
+
if (isPlainObject(policy) && policy.__sloppyPolicy === true && typeof policy.evaluate === "function") {
|
|
1006
|
+
return (user, ctx, resource = undefined) => policy.evaluate(user, ctx, resource);
|
|
1007
|
+
}
|
|
1008
|
+
throw new TypeError("Sloppy auth policy must be a function or Auth.policy descriptor.");
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
export function authorizePolicy(state, name, user, ctx, resource = undefined) {
|
|
1012
|
+
const policy = state?.policies?.get(name);
|
|
1013
|
+
if (typeof policy !== "function") {
|
|
1014
|
+
return forbidden();
|
|
1015
|
+
}
|
|
1016
|
+
const result = policy(user, ctx, resource);
|
|
1017
|
+
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
1018
|
+
return Promise.resolve(result).then((ok) => ok === true ? undefined : forbidden());
|
|
1019
|
+
}
|
|
1020
|
+
return result === true ? undefined : forbidden();
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
export function authorizeRoute(ctx, requirement, state) {
|
|
1024
|
+
if (ctx.user === undefined || ctx.user === null) {
|
|
1025
|
+
ctx.user = anonymousUser();
|
|
1026
|
+
}
|
|
1027
|
+
if (requirement === undefined || requirement === null || requirement.required !== true) {
|
|
1028
|
+
return undefined;
|
|
1029
|
+
}
|
|
1030
|
+
if (ctx.user.authenticated !== true) {
|
|
1031
|
+
return unauthorized();
|
|
1032
|
+
}
|
|
1033
|
+
if (Array.isArray(requirement.schemes) && requirement.schemes.length > 0 &&
|
|
1034
|
+
!requirement.schemes.includes(ctx.user.scheme) &&
|
|
1035
|
+
!requirement.schemes.includes(ctx.user.authScheme))
|
|
1036
|
+
{
|
|
1037
|
+
return unauthorized();
|
|
1038
|
+
}
|
|
1039
|
+
if ((!Array.isArray(requirement.schemes) || requirement.schemes.length === 0) &&
|
|
1040
|
+
typeof state?.defaultScheme === "string" &&
|
|
1041
|
+
ctx.user.scheme !== state.defaultScheme &&
|
|
1042
|
+
ctx.user.authScheme !== state.defaultScheme)
|
|
1043
|
+
{
|
|
1044
|
+
return unauthorized();
|
|
1045
|
+
}
|
|
1046
|
+
if (Array.isArray(requirement.scopes) && requirement.scopes.length > 0) {
|
|
1047
|
+
const matched = requirement.scopes.every((scope) => ctx.user.hasScope(scope));
|
|
1048
|
+
if (!matched) {
|
|
1049
|
+
return forbidden();
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
if (Array.isArray(requirement.roles) && requirement.roles.length > 0) {
|
|
1053
|
+
const matched = requirement.roles.some((role) => ctx.user.hasRole(role));
|
|
1054
|
+
if (!matched) {
|
|
1055
|
+
return forbidden();
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (Array.isArray(requirement.claims) && requirement.claims.length > 0) {
|
|
1059
|
+
const matched = requirement.claims.every((claim) => ctx.user.hasClaim(claim));
|
|
1060
|
+
if (!matched) {
|
|
1061
|
+
return forbidden();
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
if (Array.isArray(requirement.scopes) && requirement.scopes.length > 0) {
|
|
1065
|
+
const matched = requirement.scopes.every((scope) => ctx.user.hasScope(scope));
|
|
1066
|
+
if (!matched) {
|
|
1067
|
+
return forbidden();
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (requirement.policy !== undefined) {
|
|
1071
|
+
return authorizePolicy(state, requirement.policy, ctx.user, ctx);
|
|
1072
|
+
}
|
|
1073
|
+
return undefined;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function jwtBearer(options) {
|
|
1077
|
+
if (!isPlainObject(options)) {
|
|
1078
|
+
throw new TypeError("Sloppy Auth.jwtBearer options must be a plain object.");
|
|
1079
|
+
}
|
|
1080
|
+
if (options.jwksUri !== undefined || options.jwksUrl !== undefined || options.authority !== undefined) {
|
|
1081
|
+
throw new TypeError(
|
|
1082
|
+
"Sloppy Auth.jwtBearer remote JWKS discovery requires runtime HTTP client integration; use static jwks.keys or keys.",
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
const algorithms = stringArrayOption(options.algorithms ?? options.algorithm ?? "HS256", "JWT algorithm");
|
|
1086
|
+
const keys = normalizeJwtKeys(options.keys ?? options.jwks?.keys);
|
|
1087
|
+
if (options.secret === undefined && keys.length === 0) {
|
|
1088
|
+
throw new TypeError("Sloppy Auth.jwtBearer requires secret or static keys.");
|
|
1089
|
+
}
|
|
1090
|
+
return Object.freeze({
|
|
1091
|
+
__sloppyAuth: true,
|
|
1092
|
+
kind: "jwtBearer",
|
|
1093
|
+
name: stringOption(options.name ?? JWT_SCHEME, "JWT auth scheme name"),
|
|
1094
|
+
principalScheme: stringOption(options.name ?? "jwtBearer", "JWT auth principal scheme"),
|
|
1095
|
+
algorithms,
|
|
1096
|
+
keys,
|
|
1097
|
+
issuer: stringOption(options.issuer, "JWT issuer", false),
|
|
1098
|
+
audience: stringOption(options.audience, "JWT audience", false),
|
|
1099
|
+
secret: options.secret,
|
|
1100
|
+
clock: options.clock,
|
|
1101
|
+
clockSkewSeconds: integerOption(options.clockSkewSeconds ?? options.clockSkew, "JWT clockSkewSeconds", 0),
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function normalizeJwtKeys(keys) {
|
|
1106
|
+
if (keys === undefined) {
|
|
1107
|
+
return Object.freeze([]);
|
|
1108
|
+
}
|
|
1109
|
+
if (!Array.isArray(keys)) {
|
|
1110
|
+
throw new TypeError("Sloppy Auth.jwtBearer keys must be an array when provided.");
|
|
1111
|
+
}
|
|
1112
|
+
return Object.freeze(keys.map((key) => {
|
|
1113
|
+
if (!isPlainObject(key)) {
|
|
1114
|
+
throw new TypeError("Sloppy Auth.jwtBearer key entries must be plain objects.");
|
|
1115
|
+
}
|
|
1116
|
+
const algorithm = stringOption(key.alg ?? key.algorithm ?? "HS256", "JWT key algorithm");
|
|
1117
|
+
return Object.freeze({
|
|
1118
|
+
kid: stringOption(key.kid, "JWT key kid", false),
|
|
1119
|
+
algorithm,
|
|
1120
|
+
secret: key.secret,
|
|
1121
|
+
jwk: key.kty === undefined ? key.jwk : { ...key },
|
|
1122
|
+
default: key.default === true,
|
|
1123
|
+
});
|
|
1124
|
+
}));
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function normalizeSameSiteOption(value) {
|
|
1128
|
+
if (value === undefined) {
|
|
1129
|
+
return "lax";
|
|
1130
|
+
}
|
|
1131
|
+
if (typeof value !== "string") {
|
|
1132
|
+
throw new TypeError("Sloppy Auth.cookieSession sameSite must be lax, strict, or none.");
|
|
1133
|
+
}
|
|
1134
|
+
const lowered = value.toLowerCase();
|
|
1135
|
+
if (lowered !== "lax" && lowered !== "strict" && lowered !== "none") {
|
|
1136
|
+
throw new TypeError("Sloppy Auth.cookieSession sameSite must be lax, strict, or none.");
|
|
1137
|
+
}
|
|
1138
|
+
return lowered;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function normalizeTimeoutMs(value, subject, defaultValue = undefined) {
|
|
1142
|
+
return optionalPositiveInteger(value, `Sloppy ${subject} must be a positive integer number of milliseconds.`, defaultValue);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function normalizeSessionStoreDescriptor(value) {
|
|
1146
|
+
if (value === undefined) {
|
|
1147
|
+
return undefined;
|
|
1148
|
+
}
|
|
1149
|
+
if (isPlainObject(value) && value.__sloppySessionStore === true) {
|
|
1150
|
+
return value;
|
|
1151
|
+
}
|
|
1152
|
+
if (typeof value === "object" && value !== null) {
|
|
1153
|
+
for (const method of ["create", "load", "touch", "revoke", "cleanup"]) {
|
|
1154
|
+
if (typeof value[method] !== "function") {
|
|
1155
|
+
throw new TypeError(`Sloppy Auth session store object must provide ${method}().`);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return Object.freeze({
|
|
1159
|
+
__sloppySessionStore: true,
|
|
1160
|
+
kind: "custom",
|
|
1161
|
+
store: value,
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
throw new TypeError("Sloppy Auth.cookieSession store must be a session store descriptor or store object.");
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function cloneSessionRecord(record) {
|
|
1168
|
+
if (record === undefined) {
|
|
1169
|
+
return undefined;
|
|
1170
|
+
}
|
|
1171
|
+
return Object.freeze({
|
|
1172
|
+
id: record.id,
|
|
1173
|
+
claims: Object.freeze({ ...record.claims }),
|
|
1174
|
+
createdAt: record.createdAt,
|
|
1175
|
+
lastSeenAt: record.lastSeenAt,
|
|
1176
|
+
expiresAt: record.expiresAt,
|
|
1177
|
+
idleExpiresAt: record.idleExpiresAt,
|
|
1178
|
+
revokedAt: record.revokedAt,
|
|
1179
|
+
csrf: record.csrf,
|
|
1180
|
+
metadata: record.metadata === undefined ? undefined : Object.freeze({ ...record.metadata }),
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function createMemorySessionStore(options = undefined) {
|
|
1185
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
1186
|
+
throw new TypeError("Sloppy Auth.sessionStore.memory options must be a plain object.");
|
|
1187
|
+
}
|
|
1188
|
+
const maxEntries = integerOption(options?.maxEntries, "memory session store maxEntries", 4096);
|
|
1189
|
+
if (maxEntries <= 0) {
|
|
1190
|
+
throw new TypeError("Sloppy memory session store maxEntries must be positive.");
|
|
1191
|
+
}
|
|
1192
|
+
const sessions = new Map();
|
|
1193
|
+
function trim() {
|
|
1194
|
+
while (sessions.size > maxEntries) {
|
|
1195
|
+
const oldest = sessions.keys().next().value;
|
|
1196
|
+
sessions.delete(oldest);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return Object.freeze({
|
|
1200
|
+
__sloppySessionStore: true,
|
|
1201
|
+
kind: "memory",
|
|
1202
|
+
async create(record) {
|
|
1203
|
+
sessions.set(record.id, cloneSessionRecord(record));
|
|
1204
|
+
trim();
|
|
1205
|
+
return cloneSessionRecord(sessions.get(record.id));
|
|
1206
|
+
},
|
|
1207
|
+
async load(id) {
|
|
1208
|
+
return cloneSessionRecord(sessions.get(id));
|
|
1209
|
+
},
|
|
1210
|
+
async touch(id, lastSeenAt, idleExpiresAt) {
|
|
1211
|
+
const record = sessions.get(id);
|
|
1212
|
+
if (record === undefined || record.revokedAt !== undefined) {
|
|
1213
|
+
return undefined;
|
|
1214
|
+
}
|
|
1215
|
+
const next = cloneSessionRecord({ ...record, lastSeenAt, idleExpiresAt });
|
|
1216
|
+
sessions.set(id, next);
|
|
1217
|
+
return next;
|
|
1218
|
+
},
|
|
1219
|
+
async revoke(id, revokedAt = Date.now()) {
|
|
1220
|
+
const record = sessions.get(id);
|
|
1221
|
+
if (record === undefined) {
|
|
1222
|
+
return false;
|
|
1223
|
+
}
|
|
1224
|
+
sessions.set(id, cloneSessionRecord({ ...record, revokedAt }));
|
|
1225
|
+
return true;
|
|
1226
|
+
},
|
|
1227
|
+
async cleanup(now = Date.now()) {
|
|
1228
|
+
let count = 0;
|
|
1229
|
+
for (const [id, record] of sessions) {
|
|
1230
|
+
if (sessionRecordExpired(record, now)) {
|
|
1231
|
+
sessions.delete(id);
|
|
1232
|
+
count += 1;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return count;
|
|
1236
|
+
},
|
|
1237
|
+
__debug() {
|
|
1238
|
+
return Object.freeze({ kind: "memory", count: sessions.size, maxEntries });
|
|
1239
|
+
},
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function providerKindFromDb(db, fallback) {
|
|
1244
|
+
const kind = typeof db?.__debug === "function" ? db.__debug().kind : undefined;
|
|
1245
|
+
if (kind === "sqlite-connection") {
|
|
1246
|
+
return "sqlite";
|
|
1247
|
+
}
|
|
1248
|
+
if (kind === "postgres-connection") {
|
|
1249
|
+
return "postgres";
|
|
1250
|
+
}
|
|
1251
|
+
if (kind === "sqlserver-connection") {
|
|
1252
|
+
return "sqlserver";
|
|
1253
|
+
}
|
|
1254
|
+
return fallback;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function placeholders(kind, count) {
|
|
1258
|
+
return Array.from({ length: count }, (_entry, index) => {
|
|
1259
|
+
if (kind === "postgres") {
|
|
1260
|
+
return `$${index + 1}`;
|
|
1261
|
+
}
|
|
1262
|
+
if (kind === "sqlserver") {
|
|
1263
|
+
return `@p${index + 1}`;
|
|
1264
|
+
}
|
|
1265
|
+
return "?";
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function normalizeSessionRow(row) {
|
|
1270
|
+
if (row === undefined || row === null) {
|
|
1271
|
+
return undefined;
|
|
1272
|
+
}
|
|
1273
|
+
function finiteNumber(value) {
|
|
1274
|
+
if (typeof value === "string" && value.trim().length === 0) {
|
|
1275
|
+
return undefined;
|
|
1276
|
+
}
|
|
1277
|
+
const number = Number(value);
|
|
1278
|
+
return Number.isFinite(number) ? number : undefined;
|
|
1279
|
+
}
|
|
1280
|
+
function optionalFiniteNumber(...values) {
|
|
1281
|
+
const value = values.find((entry) => entry !== undefined && entry !== null);
|
|
1282
|
+
if (value === undefined) {
|
|
1283
|
+
return { valid: true, value: undefined };
|
|
1284
|
+
}
|
|
1285
|
+
const number = finiteNumber(value);
|
|
1286
|
+
return number === undefined
|
|
1287
|
+
? { valid: false, value: undefined }
|
|
1288
|
+
: { valid: true, value: number };
|
|
1289
|
+
}
|
|
1290
|
+
const claimsText = row.claims_json ?? row.claimsJson ?? row.claims;
|
|
1291
|
+
const metadataText = row.metadata_json ?? row.metadataJson ?? row.metadata;
|
|
1292
|
+
let claimsValue = claimsText;
|
|
1293
|
+
let metadataValue = metadataText;
|
|
1294
|
+
try {
|
|
1295
|
+
if (typeof claimsText === "string") {
|
|
1296
|
+
claimsValue = JSON.parse(claimsText);
|
|
1297
|
+
}
|
|
1298
|
+
if (typeof metadataText === "string") {
|
|
1299
|
+
metadataValue = JSON.parse(metadataText);
|
|
1300
|
+
}
|
|
1301
|
+
} catch {
|
|
1302
|
+
return undefined;
|
|
1303
|
+
}
|
|
1304
|
+
const createdAt = finiteNumber(row.created_at_ms ?? row.createdAt ?? row.created_at);
|
|
1305
|
+
const lastSeenAt = finiteNumber(row.last_seen_at_ms ?? row.lastSeenAt ?? row.last_seen_at);
|
|
1306
|
+
const expiresAt = optionalFiniteNumber(row.expires_at_ms, row.expiresAt, row.expires_at);
|
|
1307
|
+
const idleExpiresAt = optionalFiniteNumber(row.idle_expires_at_ms, row.idleExpiresAt, row.idle_expires_at);
|
|
1308
|
+
const revokedAt = optionalFiniteNumber(row.revoked_at_ms, row.revokedAt, row.revoked_at);
|
|
1309
|
+
const csrf = row.csrf === null || row.csrf === undefined ? undefined : row.csrf;
|
|
1310
|
+
if (
|
|
1311
|
+
typeof row.id !== "string" ||
|
|
1312
|
+
row.id.length === 0 ||
|
|
1313
|
+
!isPlainObject(claimsValue) ||
|
|
1314
|
+
createdAt === undefined ||
|
|
1315
|
+
lastSeenAt === undefined ||
|
|
1316
|
+
expiresAt.valid !== true ||
|
|
1317
|
+
idleExpiresAt.valid !== true ||
|
|
1318
|
+
revokedAt.valid !== true ||
|
|
1319
|
+
(csrf !== undefined && typeof csrf !== "string") ||
|
|
1320
|
+
(metadataValue !== null && metadataValue !== undefined && !isPlainObject(metadataValue))
|
|
1321
|
+
) {
|
|
1322
|
+
return undefined;
|
|
1323
|
+
}
|
|
1324
|
+
return cloneSessionRecord({
|
|
1325
|
+
id: row.id,
|
|
1326
|
+
claims: claimsValue,
|
|
1327
|
+
createdAt,
|
|
1328
|
+
lastSeenAt,
|
|
1329
|
+
expiresAt: expiresAt.value,
|
|
1330
|
+
idleExpiresAt: idleExpiresAt.value,
|
|
1331
|
+
revokedAt: revokedAt.value,
|
|
1332
|
+
csrf,
|
|
1333
|
+
metadata: metadataValue === null || metadataValue === undefined
|
|
1334
|
+
? undefined
|
|
1335
|
+
: metadataValue,
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function createDataProviderSessionStore(options) {
|
|
1340
|
+
if (!isPlainObject(options)) {
|
|
1341
|
+
throw new TypeError("Sloppy Auth.sessionStore.dataProvider options must be a plain object.");
|
|
1342
|
+
}
|
|
1343
|
+
const db = options.db ?? options.connection;
|
|
1344
|
+
if (db === undefined || typeof db.exec !== "function" || typeof db.queryOne !== "function") {
|
|
1345
|
+
throw new TypeError("Sloppy Auth.sessionStore.dataProvider requires an opened data connection with exec() and queryOne().");
|
|
1346
|
+
}
|
|
1347
|
+
const kind = providerKindFromDb(db, stringOption(options.provider, "session store provider", false) ?? "sqlite");
|
|
1348
|
+
if (!["sqlite", "postgres", "sqlserver"].includes(kind)) {
|
|
1349
|
+
throw new TypeError("Sloppy Auth.sessionStore.dataProvider provider must be sqlite, postgres, or sqlserver.");
|
|
1350
|
+
}
|
|
1351
|
+
let ensured = false;
|
|
1352
|
+
async function ensure() {
|
|
1353
|
+
if (ensured) {
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (kind === "sqlserver") {
|
|
1357
|
+
await db.exec(`IF OBJECT_ID(N'dbo.sloppy_auth_sessions', N'U') IS NULL
|
|
1358
|
+
BEGIN
|
|
1359
|
+
CREATE TABLE sloppy_auth_sessions (
|
|
1360
|
+
id NVARCHAR(255) NOT NULL PRIMARY KEY,
|
|
1361
|
+
subject NVARCHAR(255) NOT NULL,
|
|
1362
|
+
claims_json NVARCHAR(MAX) NOT NULL,
|
|
1363
|
+
created_at_ms BIGINT NOT NULL,
|
|
1364
|
+
last_seen_at_ms BIGINT NOT NULL,
|
|
1365
|
+
expires_at_ms BIGINT NULL,
|
|
1366
|
+
idle_expires_at_ms BIGINT NULL,
|
|
1367
|
+
revoked_at_ms BIGINT NULL,
|
|
1368
|
+
csrf NVARCHAR(255) NULL,
|
|
1369
|
+
metadata_json NVARCHAR(MAX) NULL
|
|
1370
|
+
)
|
|
1371
|
+
END`, []);
|
|
1372
|
+
} else {
|
|
1373
|
+
await db.exec(`CREATE TABLE IF NOT EXISTS sloppy_auth_sessions (
|
|
1374
|
+
id TEXT PRIMARY KEY,
|
|
1375
|
+
subject TEXT NOT NULL,
|
|
1376
|
+
claims_json TEXT NOT NULL,
|
|
1377
|
+
created_at_ms INTEGER NOT NULL,
|
|
1378
|
+
last_seen_at_ms INTEGER NOT NULL,
|
|
1379
|
+
expires_at_ms INTEGER NULL,
|
|
1380
|
+
idle_expires_at_ms INTEGER NULL,
|
|
1381
|
+
revoked_at_ms INTEGER NULL,
|
|
1382
|
+
csrf TEXT NULL,
|
|
1383
|
+
metadata_json TEXT NULL
|
|
1384
|
+
)`, []);
|
|
1385
|
+
}
|
|
1386
|
+
ensured = true;
|
|
1387
|
+
}
|
|
1388
|
+
const store = Object.freeze({
|
|
1389
|
+
__sloppySessionStore: true,
|
|
1390
|
+
kind: `dataProvider:${kind}`,
|
|
1391
|
+
async create(record) {
|
|
1392
|
+
await ensure();
|
|
1393
|
+
const p = placeholders(kind, 10);
|
|
1394
|
+
await db.exec(
|
|
1395
|
+
`INSERT INTO sloppy_auth_sessions (id, subject, claims_json, created_at_ms, last_seen_at_ms, expires_at_ms, idle_expires_at_ms, revoked_at_ms, csrf, metadata_json) VALUES (${p.join(", ")})`,
|
|
1396
|
+
[
|
|
1397
|
+
record.id,
|
|
1398
|
+
record.claims.sub ?? "",
|
|
1399
|
+
JSON.stringify(record.claims),
|
|
1400
|
+
record.createdAt,
|
|
1401
|
+
record.lastSeenAt,
|
|
1402
|
+
record.expiresAt ?? null,
|
|
1403
|
+
record.idleExpiresAt ?? null,
|
|
1404
|
+
record.revokedAt ?? null,
|
|
1405
|
+
record.csrf ?? null,
|
|
1406
|
+
record.metadata === undefined ? null : JSON.stringify(record.metadata),
|
|
1407
|
+
],
|
|
1408
|
+
);
|
|
1409
|
+
return cloneSessionRecord(record);
|
|
1410
|
+
},
|
|
1411
|
+
async load(id) {
|
|
1412
|
+
await ensure();
|
|
1413
|
+
const p = placeholders(kind, 1);
|
|
1414
|
+
return normalizeSessionRow(await db.queryOne(`SELECT * FROM sloppy_auth_sessions WHERE id = ${p[0]}`, [id]));
|
|
1415
|
+
},
|
|
1416
|
+
async touch(id, lastSeenAt, idleExpiresAt) {
|
|
1417
|
+
await ensure();
|
|
1418
|
+
const p = placeholders(kind, 3);
|
|
1419
|
+
await db.exec(
|
|
1420
|
+
`UPDATE sloppy_auth_sessions SET last_seen_at_ms = ${p[0]}, idle_expires_at_ms = ${p[1]} WHERE id = ${p[2]} AND revoked_at_ms IS NULL`,
|
|
1421
|
+
[lastSeenAt, idleExpiresAt ?? null, id],
|
|
1422
|
+
);
|
|
1423
|
+
return store.load(id);
|
|
1424
|
+
},
|
|
1425
|
+
async revoke(id, revokedAt = Date.now()) {
|
|
1426
|
+
await ensure();
|
|
1427
|
+
const p = placeholders(kind, 2);
|
|
1428
|
+
await db.exec(`UPDATE sloppy_auth_sessions SET revoked_at_ms = ${p[0]} WHERE id = ${p[1]}`, [revokedAt, id]);
|
|
1429
|
+
return true;
|
|
1430
|
+
},
|
|
1431
|
+
async cleanup(now = Date.now()) {
|
|
1432
|
+
await ensure();
|
|
1433
|
+
const p = placeholders(kind, 1);
|
|
1434
|
+
await db.exec(
|
|
1435
|
+
`DELETE FROM sloppy_auth_sessions WHERE revoked_at_ms IS NOT NULL OR expires_at_ms <= ${p[0]} OR idle_expires_at_ms <= ${p[0]}`,
|
|
1436
|
+
[now],
|
|
1437
|
+
);
|
|
1438
|
+
return undefined;
|
|
1439
|
+
},
|
|
1440
|
+
});
|
|
1441
|
+
return store;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function materializeSessionStore(descriptor) {
|
|
1445
|
+
if (descriptor === undefined) {
|
|
1446
|
+
return undefined;
|
|
1447
|
+
}
|
|
1448
|
+
if (descriptor.kind === "memory") {
|
|
1449
|
+
return createMemorySessionStore(descriptor.options);
|
|
1450
|
+
}
|
|
1451
|
+
if (descriptor.kind === "dataProvider") {
|
|
1452
|
+
return createDataProviderSessionStore(descriptor.options);
|
|
1453
|
+
}
|
|
1454
|
+
if (descriptor.kind === "custom") {
|
|
1455
|
+
return descriptor.store;
|
|
1456
|
+
}
|
|
1457
|
+
throw new TypeError(`Sloppy Auth session store kind '${descriptor.kind}' is not supported.`);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function cookieSession(options) {
|
|
1461
|
+
if (!isPlainObject(options)) {
|
|
1462
|
+
throw new TypeError("Sloppy Auth.cookieSession options must be a plain object.");
|
|
1463
|
+
}
|
|
1464
|
+
const store = normalizeSessionStoreDescriptor(options.store);
|
|
1465
|
+
const idleTimeoutMs = normalizeTimeoutMs(
|
|
1466
|
+
options.idleTimeoutMs ?? options.idleTimeout,
|
|
1467
|
+
"Auth.cookieSession idleTimeoutMs",
|
|
1468
|
+
store === undefined ? undefined : DEFAULT_SESSION_IDLE_TIMEOUT_MS,
|
|
1469
|
+
);
|
|
1470
|
+
const absoluteTimeoutMs = normalizeTimeoutMs(
|
|
1471
|
+
options.absoluteTimeoutMs ?? options.absoluteTimeout,
|
|
1472
|
+
"Auth.cookieSession absoluteTimeoutMs",
|
|
1473
|
+
store === undefined ? undefined : DEFAULT_SESSION_ABSOLUTE_TIMEOUT_MS,
|
|
1474
|
+
);
|
|
1475
|
+
const name = options.name ?? DEFAULT_SESSION_COOKIE;
|
|
1476
|
+
requireHttpToken(name, "Sloppy Auth.cookieSession name must be a safe HTTP token.");
|
|
1477
|
+
const secure = booleanOption(options.secure, "Auth.cookieSession secure", true);
|
|
1478
|
+
const sameSite = normalizeSameSiteOption(options.sameSite);
|
|
1479
|
+
if (sameSite === "none" && secure !== true) {
|
|
1480
|
+
throw new TypeError("Sloppy Auth.cookieSession sameSite none requires secure cookies.");
|
|
1481
|
+
}
|
|
1482
|
+
const path = stringOption(options.path ?? "/", "Auth.cookieSession path");
|
|
1483
|
+
const maxAgeSeconds = integerOption(
|
|
1484
|
+
options.maxAgeSeconds ?? options.maxAge ?? (store === undefined ? DEFAULT_SIGNED_SESSION_MAX_AGE_SECONDS : undefined),
|
|
1485
|
+
"Auth.cookieSession maxAgeSeconds",
|
|
1486
|
+
);
|
|
1487
|
+
const csrf = normalizeCsrfOptions(options.csrf);
|
|
1488
|
+
if (csrf.enabled && csrf.cookieName.startsWith("__Host-") && (secure !== true || path !== "/")) {
|
|
1489
|
+
throw new TypeError("Sloppy Auth.cookieSession __Host- CSRF cookies require secure true and path '/'.");
|
|
1490
|
+
}
|
|
1491
|
+
return Object.freeze({
|
|
1492
|
+
__sloppyAuth: true,
|
|
1493
|
+
kind: "cookieSession",
|
|
1494
|
+
name: stringOption(options.scheme ?? options.schemeName ?? COOKIE_SESSION_SCHEME, "cookie session auth scheme name"),
|
|
1495
|
+
principalScheme: stringOption(options.scheme ?? options.schemeName ?? "cookieSession", "cookie session auth principal scheme"),
|
|
1496
|
+
cookieName: name,
|
|
1497
|
+
secret: options.secret,
|
|
1498
|
+
secure,
|
|
1499
|
+
httpOnly: booleanOption(options.httpOnly, "Auth.cookieSession httpOnly", true),
|
|
1500
|
+
sameSite,
|
|
1501
|
+
path,
|
|
1502
|
+
maxAgeSeconds,
|
|
1503
|
+
idleTimeoutMs,
|
|
1504
|
+
absoluteTimeoutMs,
|
|
1505
|
+
rotation: store === undefined ? false : booleanOption(options.rotation ?? options.rotate, "Auth.cookieSession rotation", false),
|
|
1506
|
+
clock: options.clock,
|
|
1507
|
+
csrf,
|
|
1508
|
+
store,
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function normalizeCsrfOptions(options) {
|
|
1513
|
+
if (options === undefined || options === false) {
|
|
1514
|
+
return Object.freeze({ enabled: false, header: DEFAULT_CSRF_HEADER, cookieName: DEFAULT_CSRF_COOKIE });
|
|
1515
|
+
}
|
|
1516
|
+
if (options === true) {
|
|
1517
|
+
return Object.freeze({ enabled: true, header: DEFAULT_CSRF_HEADER, cookieName: DEFAULT_CSRF_COOKIE });
|
|
1518
|
+
}
|
|
1519
|
+
if (!isPlainObject(options)) {
|
|
1520
|
+
throw new TypeError("Sloppy Auth CSRF options must be a plain object, true, or false.");
|
|
1521
|
+
}
|
|
1522
|
+
const header = (options.header ?? DEFAULT_CSRF_HEADER).toLowerCase();
|
|
1523
|
+
validateHeaderName(header, "CSRF");
|
|
1524
|
+
const cookieName = stringOption(options.cookieName ?? DEFAULT_CSRF_COOKIE, "CSRF cookie name");
|
|
1525
|
+
requireHttpToken(cookieName, "Sloppy Auth CSRF cookie name must be a safe HTTP token.");
|
|
1526
|
+
return Object.freeze({
|
|
1527
|
+
enabled: true,
|
|
1528
|
+
header,
|
|
1529
|
+
cookieName,
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function findSessionScheme(ctx) {
|
|
1534
|
+
const auth = ctx?.__sloppyHost?.auth;
|
|
1535
|
+
const scheme = auth?.state?.defaultSession ?? auth?.defaultSession ??
|
|
1536
|
+
(Array.isArray(auth?.schemes)
|
|
1537
|
+
? auth.schemes.find((entry) => entry.kind === "cookieSession")
|
|
1538
|
+
: undefined);
|
|
1539
|
+
if (scheme === undefined) {
|
|
1540
|
+
throw new TypeError("Sloppy Auth.signIn/signOut requires Auth.cookieSession middleware.");
|
|
1541
|
+
}
|
|
1542
|
+
return scheme;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
async function signIn(ctx, claims, options = undefined) {
|
|
1546
|
+
const scheme = findSessionScheme(ctx);
|
|
1547
|
+
const sessionClaims = normalizeSessionClaims(claims);
|
|
1548
|
+
const currentMs = nowMilliseconds(scheme.clock);
|
|
1549
|
+
const current = Math.floor(currentMs / 1000);
|
|
1550
|
+
const csrf = scheme.csrf.enabled ? Random.token(32) : undefined;
|
|
1551
|
+
if (scheme.store !== undefined) {
|
|
1552
|
+
const cookieOptions = sessionStoreCookieOptions(scheme, options, scheme.absoluteTimeoutMs);
|
|
1553
|
+
const absoluteLifetimeMs = cookieOptions.maxAgeSeconds === undefined
|
|
1554
|
+
? scheme.absoluteTimeoutMs
|
|
1555
|
+
: Math.min(scheme.absoluteTimeoutMs, Math.max(0, cookieOptions.maxAgeSeconds * 1000));
|
|
1556
|
+
const sessionId = Random.token(32);
|
|
1557
|
+
const record = {
|
|
1558
|
+
id: sessionId,
|
|
1559
|
+
claims: sessionClaims,
|
|
1560
|
+
createdAt: currentMs,
|
|
1561
|
+
lastSeenAt: currentMs,
|
|
1562
|
+
expiresAt: currentMs + absoluteLifetimeMs,
|
|
1563
|
+
idleExpiresAt: scheme.idleTimeoutMs === undefined ? undefined : currentMs + scheme.idleTimeoutMs,
|
|
1564
|
+
csrf,
|
|
1565
|
+
metadata: isPlainObject(options?.metadata) ? { ...options.metadata } : undefined,
|
|
1566
|
+
};
|
|
1567
|
+
await scheme.store.create(record);
|
|
1568
|
+
const value = await signSessionId(scheme, sessionId);
|
|
1569
|
+
ctx.user = userFromClaims(sessionClaims, scheme.principalScheme, scheme.name);
|
|
1570
|
+
ctx.session = Object.freeze({
|
|
1571
|
+
scheme: scheme.name,
|
|
1572
|
+
id: sessionId,
|
|
1573
|
+
issuedAt: current,
|
|
1574
|
+
expiresAt: Math.floor(record.expiresAt / 1000),
|
|
1575
|
+
csrfToken: csrf,
|
|
1576
|
+
revoke: () => scheme.store.revoke(sessionId, nowMilliseconds(scheme.clock)),
|
|
1577
|
+
});
|
|
1578
|
+
let result = Results.ok({ ok: true }).cookie(
|
|
1579
|
+
scheme.cookieName,
|
|
1580
|
+
value,
|
|
1581
|
+
cookieOptions,
|
|
1582
|
+
);
|
|
1583
|
+
if (scheme.csrf.enabled) {
|
|
1584
|
+
result = result.cookie(scheme.csrf.cookieName, csrf, {
|
|
1585
|
+
path: scheme.path,
|
|
1586
|
+
secure: scheme.secure,
|
|
1587
|
+
sameSite: scheme.sameSite,
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
return result;
|
|
1591
|
+
}
|
|
1592
|
+
const cookieOptions = sessionCookieOptions(scheme, options);
|
|
1593
|
+
const payload = {
|
|
1594
|
+
iat: current,
|
|
1595
|
+
...(cookieOptions.maxAgeSeconds === undefined ? {} : { exp: current + cookieOptions.maxAgeSeconds }),
|
|
1596
|
+
claims: sessionClaims,
|
|
1597
|
+
};
|
|
1598
|
+
if (csrf !== undefined) {
|
|
1599
|
+
payload.csrf = csrf;
|
|
1600
|
+
}
|
|
1601
|
+
const value = await signSessionPayload(scheme, payload);
|
|
1602
|
+
ctx.user = userFromClaims(sessionClaims, scheme.principalScheme, scheme.name);
|
|
1603
|
+
let result = Results.ok({ ok: true }).cookie(scheme.cookieName, value, cookieOptions);
|
|
1604
|
+
if (scheme.csrf.enabled) {
|
|
1605
|
+
result = result.cookie(scheme.csrf.cookieName, payload.csrf, {
|
|
1606
|
+
path: scheme.path,
|
|
1607
|
+
secure: scheme.secure,
|
|
1608
|
+
sameSite: scheme.sameSite,
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
return result;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
async function signOut(ctx, options = undefined) {
|
|
1615
|
+
const scheme = findSessionScheme(ctx);
|
|
1616
|
+
const sessionId = typeof ctx?.session?.id === "string" ? ctx.session.id : undefined;
|
|
1617
|
+
if (sessionId !== undefined && scheme.store !== undefined) {
|
|
1618
|
+
await scheme.store.revoke(sessionId, nowMilliseconds(scheme.clock));
|
|
1619
|
+
}
|
|
1620
|
+
ctx.user = anonymousUser();
|
|
1621
|
+
let result = Results.status(204).cookie(
|
|
1622
|
+
scheme.cookieName,
|
|
1623
|
+
"",
|
|
1624
|
+
sessionStoreCookieOptions(scheme, { ...options, maxAgeSeconds: 0, expires: new Date(0) }),
|
|
1625
|
+
);
|
|
1626
|
+
if (scheme.csrf.enabled) {
|
|
1627
|
+
result = result.cookie(scheme.csrf.cookieName, "", {
|
|
1628
|
+
path: scheme.path,
|
|
1629
|
+
secure: scheme.secure,
|
|
1630
|
+
sameSite: scheme.sameSite,
|
|
1631
|
+
maxAgeSeconds: 0,
|
|
1632
|
+
expires: new Date(0),
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
return result;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function apiKey(options) {
|
|
1639
|
+
if (!isPlainObject(options)) {
|
|
1640
|
+
throw new TypeError("Sloppy Auth.apiKey options must be a plain object.");
|
|
1641
|
+
}
|
|
1642
|
+
const header = options.header ?? "x-api-key";
|
|
1643
|
+
validateHeaderName(header, "API key");
|
|
1644
|
+
const keys = normalizeApiKeys(options.keys);
|
|
1645
|
+
const configKey = stringOption(options.configKey, "API key configKey", false)
|
|
1646
|
+
?? configRequiredKeysFromFunction(options.validate)?.[0];
|
|
1647
|
+
if (typeof options.validate !== "function" && configKey === undefined && keys.length === 0) {
|
|
1648
|
+
throw new TypeError("Sloppy Auth.apiKey requires validate, configKey, or keys.");
|
|
1649
|
+
}
|
|
1650
|
+
if (options.validate !== undefined && typeof options.validate !== "function") {
|
|
1651
|
+
throw new TypeError("Sloppy Auth.apiKey validate must be a function.");
|
|
1652
|
+
}
|
|
1653
|
+
return Object.freeze({
|
|
1654
|
+
__sloppyAuth: true,
|
|
1655
|
+
kind: "apiKey",
|
|
1656
|
+
name: stringOption(options.name ?? API_KEY_SCHEME, "API key auth scheme name"),
|
|
1657
|
+
principalScheme: stringOption(options.name ?? "apiKey", "API key auth principal scheme"),
|
|
1658
|
+
header: header.toLowerCase(),
|
|
1659
|
+
authorizationScheme: stringOption(options.authorizationScheme, "API key Authorization scheme", false),
|
|
1660
|
+
maxLength: integerOption(options.maxLength, "API key maxLength", 4096),
|
|
1661
|
+
keys,
|
|
1662
|
+
validate: options.validate,
|
|
1663
|
+
configKey,
|
|
1664
|
+
usesStaticConfigEquality: typeof options.validate !== "function" ||
|
|
1665
|
+
apiKeyValidatorUsesOnlyConfigKey(options.validate, configKey),
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function normalizeApiKeys(keys) {
|
|
1670
|
+
if (keys === undefined) {
|
|
1671
|
+
return Object.freeze([]);
|
|
1672
|
+
}
|
|
1673
|
+
if (!Array.isArray(keys)) {
|
|
1674
|
+
throw new TypeError("Sloppy Auth.apiKey keys must be an array.");
|
|
1675
|
+
}
|
|
1676
|
+
return Object.freeze(keys.map((entry) => {
|
|
1677
|
+
if (!isPlainObject(entry)) {
|
|
1678
|
+
throw new TypeError("Sloppy Auth.apiKey key entries must be plain objects.");
|
|
1679
|
+
}
|
|
1680
|
+
const id = stringOption(entry.id, "API key id");
|
|
1681
|
+
const key = stringOption(entry.key, "API key value", false);
|
|
1682
|
+
const hash = stringOption(entry.hash, "API key hash", false);
|
|
1683
|
+
if ((key === undefined) === (hash === undefined)) {
|
|
1684
|
+
throw new TypeError("Sloppy Auth.apiKey key entries require exactly one of key or hash.");
|
|
1685
|
+
}
|
|
1686
|
+
return Object.freeze({
|
|
1687
|
+
id,
|
|
1688
|
+
key,
|
|
1689
|
+
hash,
|
|
1690
|
+
scopes: stringArrayOption(entry.scopes, "API key scope"),
|
|
1691
|
+
roles: stringArrayOption(entry.roles, "API key role"),
|
|
1692
|
+
claims: entry.claims === undefined ? undefined : Object.freeze({ ...entry.claims }),
|
|
1693
|
+
});
|
|
1694
|
+
}));
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
function configure(options) {
|
|
1698
|
+
if (!isPlainObject(options)) {
|
|
1699
|
+
throw new TypeError("Sloppy Auth.configure options must be a plain object.");
|
|
1700
|
+
}
|
|
1701
|
+
if (!isPlainObject(options.schemes)) {
|
|
1702
|
+
throw new TypeError("Sloppy Auth.configure schemes must be an object.");
|
|
1703
|
+
}
|
|
1704
|
+
const providers = [];
|
|
1705
|
+
const schemeNames = [];
|
|
1706
|
+
for (const [name, provider] of Object.entries(options.schemes)) {
|
|
1707
|
+
if (!isAuthProviderDescriptor(provider) || provider.kind === "configure") {
|
|
1708
|
+
throw new TypeError("Sloppy Auth.configure schemes must contain Auth provider descriptors.");
|
|
1709
|
+
}
|
|
1710
|
+
providers.push(Object.freeze({ ...provider, name, principalScheme: name }));
|
|
1711
|
+
schemeNames.push(name);
|
|
1712
|
+
}
|
|
1713
|
+
const defaultScheme = stringOption(options.defaultScheme, "default auth scheme", false);
|
|
1714
|
+
if (defaultScheme !== undefined && !schemeNames.includes(defaultScheme)) {
|
|
1715
|
+
throw new TypeError(`Sloppy Auth.configure default scheme '${defaultScheme}' is not configured.`);
|
|
1716
|
+
}
|
|
1717
|
+
return Object.freeze({
|
|
1718
|
+
__sloppyAuth: true,
|
|
1719
|
+
kind: "configure",
|
|
1720
|
+
defaultScheme,
|
|
1721
|
+
schemeNames: Object.freeze(schemeNames),
|
|
1722
|
+
providers: Object.freeze(providers),
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
const password = Object.freeze({
|
|
1727
|
+
hash(passwordValue, options = undefined) {
|
|
1728
|
+
return Password.hash(passwordValue, options);
|
|
1729
|
+
},
|
|
1730
|
+
verify(encodedHash, passwordValue) {
|
|
1731
|
+
return Password.verify(passwordValue, encodedHash);
|
|
1732
|
+
},
|
|
1733
|
+
needsRehash(encodedHash, options = undefined) {
|
|
1734
|
+
return Password.needsRehash(encodedHash, options);
|
|
1735
|
+
},
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
const sessionStore = Object.freeze({
|
|
1739
|
+
memory(options = undefined) {
|
|
1740
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
1741
|
+
throw new TypeError("Sloppy Auth.sessionStore.memory options must be a plain object.");
|
|
1742
|
+
}
|
|
1743
|
+
return Object.freeze({
|
|
1744
|
+
__sloppySessionStore: true,
|
|
1745
|
+
kind: "memory",
|
|
1746
|
+
options: options === undefined ? undefined : Object.freeze({ ...options }),
|
|
1747
|
+
});
|
|
1748
|
+
},
|
|
1749
|
+
dataProvider(options) {
|
|
1750
|
+
return Object.freeze({
|
|
1751
|
+
__sloppySessionStore: true,
|
|
1752
|
+
kind: "dataProvider",
|
|
1753
|
+
options: Object.freeze({ ...options }),
|
|
1754
|
+
});
|
|
1755
|
+
},
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
function configRequiredKeysFromFunction(fn) {
|
|
1759
|
+
if (typeof fn !== "function") {
|
|
1760
|
+
return undefined;
|
|
1761
|
+
}
|
|
1762
|
+
// This only recognizes direct literal Config.required("...") calls. When the
|
|
1763
|
+
// function source cannot be parsed confidently, leave metadata undefined.
|
|
1764
|
+
const keys = [];
|
|
1765
|
+
let source = Function.prototype.toString.call(fn);
|
|
1766
|
+
while (source.length !== 0) {
|
|
1767
|
+
const index = source.indexOf("Config.required");
|
|
1768
|
+
if (index < 0) {
|
|
1769
|
+
break;
|
|
1770
|
+
}
|
|
1771
|
+
source = source.slice(index + "Config.required".length);
|
|
1772
|
+
const open = source.indexOf("(");
|
|
1773
|
+
if (open < 0) {
|
|
1774
|
+
return undefined;
|
|
1775
|
+
}
|
|
1776
|
+
source = source.slice(open + 1).trimStart();
|
|
1777
|
+
const quote = source[0];
|
|
1778
|
+
if (quote !== "\"" && quote !== "'") {
|
|
1779
|
+
return undefined;
|
|
1780
|
+
}
|
|
1781
|
+
const end = source.slice(1).indexOf(quote);
|
|
1782
|
+
if (end < 0) {
|
|
1783
|
+
return undefined;
|
|
1784
|
+
}
|
|
1785
|
+
keys.push(source.slice(1, end + 1));
|
|
1786
|
+
source = source.slice(end + 2);
|
|
1787
|
+
}
|
|
1788
|
+
return keys;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function apiKeyValidatorUsesOnlyConfigKey(validate, configKey) {
|
|
1792
|
+
if (configKey === undefined || typeof validate !== "function") {
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1795
|
+
const source = Function.prototype.toString.call(validate).trim();
|
|
1796
|
+
const escaped = configKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1797
|
+
const parameter = "([A-Za-z_$][0-9A-Za-z_$]*)";
|
|
1798
|
+
return new RegExp(`^\\(?\\s*${parameter}\\s*\\)?\\s*=>\\s*\\1\\s*===\\s*Config\\.required\\(["']${escaped}["']\\)\\s*$`, "u").test(source) ||
|
|
1799
|
+
new RegExp(`^\\(?\\s*${parameter}\\s*\\)?\\s*=>\\s*Config\\.required\\(["']${escaped}["']\\)\\s*===\\s*\\1\\s*$`, "u").test(source);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
export const Auth = Object.freeze({
|
|
1803
|
+
configure,
|
|
1804
|
+
policy: authPolicy,
|
|
1805
|
+
jwtBearer,
|
|
1806
|
+
apiKey,
|
|
1807
|
+
cookieSession,
|
|
1808
|
+
signIn,
|
|
1809
|
+
signOut,
|
|
1810
|
+
sessionStore,
|
|
1811
|
+
password,
|
|
1812
|
+
constantTimeEquals: constantTimeStringEquals,
|
|
1813
|
+
});
|