@slopware/sloppy-linux-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 +34 -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,856 @@
|
|
|
1
|
+
import { isPlainObject } from "./internal/validation.js";
|
|
2
|
+
import { Results } from "./results.js";
|
|
3
|
+
|
|
4
|
+
const POLICY_MARKER = Symbol.for("sloppy.rateLimit.policy");
|
|
5
|
+
const STORE_MARKER = Symbol.for("sloppy.rateLimit.store");
|
|
6
|
+
const PARTITION_MARKER = Symbol.for("sloppy.rateLimit.partition");
|
|
7
|
+
const HEADER_TOKEN_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/u;
|
|
8
|
+
const DEFAULT_MAX_KEYS = 10000;
|
|
9
|
+
const DEFAULT_MAX_COST = 1000000;
|
|
10
|
+
const DEFAULT_PROBLEM_TYPE = "https://sloppy.dev/problems/rate-limit";
|
|
11
|
+
const ERROR_CODE = "SLOPPY_E_RATE_LIMIT_EXCEEDED";
|
|
12
|
+
|
|
13
|
+
function nowMs(clock = undefined) {
|
|
14
|
+
if (clock !== undefined && typeof clock.monotonicNowMs === "function") {
|
|
15
|
+
return clock.monotonicNowMs();
|
|
16
|
+
}
|
|
17
|
+
if (clock !== undefined && typeof clock.now === "function") {
|
|
18
|
+
const value = clock.now();
|
|
19
|
+
return value instanceof Date ? value.getTime() : Number(value);
|
|
20
|
+
}
|
|
21
|
+
return Date.now();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stableHash(value) {
|
|
25
|
+
const text = String(value);
|
|
26
|
+
let hash = 0xcbf29ce484222325n;
|
|
27
|
+
const prime = 0x100000001b3n;
|
|
28
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
29
|
+
hash ^= BigInt(text.charCodeAt(index));
|
|
30
|
+
hash = BigInt.asUintN(64, hash * prime);
|
|
31
|
+
}
|
|
32
|
+
return hash.toString(16).padStart(16, "0");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function stableStringify(value) {
|
|
36
|
+
if (value === null || typeof value !== "object") {
|
|
37
|
+
return JSON.stringify(value);
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
|
|
41
|
+
}
|
|
42
|
+
const keys = Object.keys(value).sort();
|
|
43
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function positiveInteger(value, subject, max = Number.MAX_SAFE_INTEGER) {
|
|
47
|
+
if (!Number.isInteger(value) || value <= 0 || value > max) {
|
|
48
|
+
throw new TypeError(`Sloppy RateLimit ${subject} must be a positive integer no greater than ${max}.`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function positiveNumber(value, subject, max = Number.MAX_SAFE_INTEGER) {
|
|
54
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > max) {
|
|
55
|
+
throw new TypeError(`Sloppy RateLimit ${subject} must be a positive finite number no greater than ${max}.`);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function optionalName(value, subject = "name") {
|
|
61
|
+
if (value === undefined) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
if (typeof value !== "string" || value.length === 0 || /[\x00-\x1F\x7F]/u.test(value)) {
|
|
65
|
+
throw new TypeError(`Sloppy RateLimit ${subject} must be a non-empty string without control characters.`);
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateHeaderName(name) {
|
|
71
|
+
if (typeof name !== "string" || !HEADER_TOKEN_PATTERN.test(name)) {
|
|
72
|
+
throw new TypeError("Sloppy RateLimit header partition name must be a safe HTTP token.");
|
|
73
|
+
}
|
|
74
|
+
return name.toLowerCase();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function freezeJson(value, subject) {
|
|
78
|
+
if (value === undefined || value === null || typeof value === "string" ||
|
|
79
|
+
typeof value === "boolean" || Number.isFinite(value)) {
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
return Object.freeze(value.map((entry) => freezeJson(entry, subject)));
|
|
84
|
+
}
|
|
85
|
+
if (isPlainObject(value)) {
|
|
86
|
+
const output = {};
|
|
87
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
88
|
+
output[key] = freezeJson(nested, subject);
|
|
89
|
+
}
|
|
90
|
+
return Object.freeze(output);
|
|
91
|
+
}
|
|
92
|
+
throw new TypeError(`Sloppy RateLimit ${subject} must be JSON-compatible.`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function requestHeader(ctx, name) {
|
|
96
|
+
const headers = ctx?.request?.headers;
|
|
97
|
+
if (headers === undefined || headers === null) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
if (typeof headers.get === "function") {
|
|
101
|
+
return headers.get(name) ?? headers.get(name.toLowerCase()) ?? undefined;
|
|
102
|
+
}
|
|
103
|
+
if (isPlainObject(headers)) {
|
|
104
|
+
const lower = name.toLowerCase();
|
|
105
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
106
|
+
if (key.toLowerCase() === lower) {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function firstHeaderValue(value) {
|
|
115
|
+
if (Array.isArray(value)) {
|
|
116
|
+
return value.length === 0 ? undefined : firstHeaderValue(value[0]);
|
|
117
|
+
}
|
|
118
|
+
if (value === undefined || value === null) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
return String(value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function validateTrustProxyOptions(options = undefined, subject = "ip partition") {
|
|
125
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
126
|
+
throw new TypeError(`Sloppy RateLimit ${subject} options must be a plain object.`);
|
|
127
|
+
}
|
|
128
|
+
if (options?.trustProxy !== undefined && typeof options.trustProxy !== "boolean") {
|
|
129
|
+
throw new TypeError(`Sloppy RateLimit ${subject} trustProxy must be a boolean.`);
|
|
130
|
+
}
|
|
131
|
+
return Object.freeze({ trustProxy: options?.trustProxy === true });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function rawRemoteAddress(ctx, options = undefined) {
|
|
135
|
+
const proxy = validateTrustProxyOptions(options);
|
|
136
|
+
if (proxy.trustProxy) {
|
|
137
|
+
const forwarded = firstHeaderValue(requestHeader(ctx, "x-forwarded-for"))
|
|
138
|
+
?.split(",")[0]
|
|
139
|
+
?.trim();
|
|
140
|
+
if (forwarded !== undefined && forwarded.length !== 0) {
|
|
141
|
+
return forwarded;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return firstHeaderValue(ctx?.connection?.remoteAddress) ??
|
|
145
|
+
firstHeaderValue(ctx?.request?.remoteAddress) ??
|
|
146
|
+
"unknown";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function createPartition(kind, metadata, resolver, options = {}) {
|
|
150
|
+
const partition = {
|
|
151
|
+
[PARTITION_MARKER]: true,
|
|
152
|
+
kind,
|
|
153
|
+
metadata: Object.freeze({ kind, ...metadata }),
|
|
154
|
+
needsAuth: options.needsAuth === true,
|
|
155
|
+
resolve: resolver,
|
|
156
|
+
orIp(ipOptions = undefined) {
|
|
157
|
+
const fallbackOptions = validateTrustProxyOptions(ipOptions, "orIp fallback");
|
|
158
|
+
const primary = partition;
|
|
159
|
+
return createPartition(
|
|
160
|
+
`${kind}.orIp`,
|
|
161
|
+
{ ...primary.metadata, fallback: "ip", fallbackTrustProxy: fallbackOptions.trustProxy },
|
|
162
|
+
(ctx) => {
|
|
163
|
+
const value = primary.resolve(ctx);
|
|
164
|
+
return value === undefined || value === null || value === ""
|
|
165
|
+
? `ip:${rawRemoteAddress(ctx, fallbackOptions)}`
|
|
166
|
+
: value;
|
|
167
|
+
},
|
|
168
|
+
{ needsAuth: false },
|
|
169
|
+
);
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
return Object.freeze(partition);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function ensurePartition(partitionBy) {
|
|
176
|
+
if (partitionBy === "global") {
|
|
177
|
+
return createPartition("global", { kind: "global" }, () => "global");
|
|
178
|
+
}
|
|
179
|
+
if (partitionBy?.[PARTITION_MARKER] === true && typeof partitionBy.resolve === "function") {
|
|
180
|
+
return partitionBy;
|
|
181
|
+
}
|
|
182
|
+
if (typeof partitionBy === "function") {
|
|
183
|
+
return createPartition(
|
|
184
|
+
"custom",
|
|
185
|
+
{ kind: "custom", marker: partitionBy.name || "anonymous", partial: true },
|
|
186
|
+
partitionBy,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
throw new TypeError("Sloppy RateLimit partitionBy is required; use RateLimit.partition.*() or 'global'.");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizePolicyOptions(algorithm, options, required) {
|
|
193
|
+
if (!isPlainObject(options)) {
|
|
194
|
+
throw new TypeError(`Sloppy RateLimit.${algorithm} options must be a plain object.`);
|
|
195
|
+
}
|
|
196
|
+
const partition = ensurePartition(options.partitionBy);
|
|
197
|
+
const statusCode = options.statusCode ?? 429;
|
|
198
|
+
if (statusCode !== 429) {
|
|
199
|
+
throw new TypeError("Sloppy RateLimit statusCode currently supports only 429.");
|
|
200
|
+
}
|
|
201
|
+
const cost = options.cost ?? 1;
|
|
202
|
+
if (typeof cost !== "function") {
|
|
203
|
+
positiveNumber(cost, "cost", DEFAULT_MAX_COST);
|
|
204
|
+
}
|
|
205
|
+
if (options.skip !== undefined && typeof options.skip !== "function") {
|
|
206
|
+
throw new TypeError("Sloppy RateLimit skip must be a function.");
|
|
207
|
+
}
|
|
208
|
+
if (options.problem !== undefined && typeof options.problem !== "function" && !isPlainObject(options.problem)) {
|
|
209
|
+
throw new TypeError("Sloppy RateLimit problem must be an object or function.");
|
|
210
|
+
}
|
|
211
|
+
const store = options.store;
|
|
212
|
+
if (store !== undefined && typeof store !== "string" && store?.[STORE_MARKER] !== true) {
|
|
213
|
+
throw new TypeError("Sloppy RateLimit store must be a store name or RateLimit store.");
|
|
214
|
+
}
|
|
215
|
+
const nameExplicit = options.name !== undefined;
|
|
216
|
+
const name = optionalName(options.name) ?? `${algorithm}:${partition.metadata.kind}`;
|
|
217
|
+
return Object.freeze({
|
|
218
|
+
algorithm,
|
|
219
|
+
name,
|
|
220
|
+
nameExplicit,
|
|
221
|
+
partition,
|
|
222
|
+
store,
|
|
223
|
+
cost,
|
|
224
|
+
skip: options.skip,
|
|
225
|
+
statusCode,
|
|
226
|
+
problem: options.problem,
|
|
227
|
+
required: Object.freeze(required),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function createPolicy(algorithm, options, required) {
|
|
232
|
+
const normalized = normalizePolicyOptions(algorithm, options, required);
|
|
233
|
+
const policy = {
|
|
234
|
+
[POLICY_MARKER]: true,
|
|
235
|
+
algorithm,
|
|
236
|
+
name: normalized.name,
|
|
237
|
+
nameExplicit: normalized.nameExplicit,
|
|
238
|
+
partition: normalized.partition,
|
|
239
|
+
store: normalized.store,
|
|
240
|
+
options: normalized,
|
|
241
|
+
metadata: Object.freeze({
|
|
242
|
+
name: normalized.name,
|
|
243
|
+
algorithm,
|
|
244
|
+
store: typeof normalized.store === "string"
|
|
245
|
+
? normalized.store
|
|
246
|
+
: normalized.store?.kind ?? "default",
|
|
247
|
+
partition: normalized.partition.metadata,
|
|
248
|
+
requiresAuth: normalized.partition.needsAuth,
|
|
249
|
+
...required,
|
|
250
|
+
}),
|
|
251
|
+
};
|
|
252
|
+
return Object.freeze(policy);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function fixedWindow(options) {
|
|
256
|
+
return createPolicy("fixedWindow", options, {
|
|
257
|
+
limit: positiveInteger(options?.limit, "limit"),
|
|
258
|
+
windowMs: positiveInteger(options?.windowMs, "windowMs"),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function slidingWindow(options) {
|
|
263
|
+
return createPolicy("slidingWindow", options, {
|
|
264
|
+
limit: positiveInteger(options?.limit, "limit"),
|
|
265
|
+
windowMs: positiveInteger(options?.windowMs, "windowMs"),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function tokenBucket(options) {
|
|
270
|
+
return createPolicy("tokenBucket", options, {
|
|
271
|
+
capacity: positiveNumber(options?.capacity, "capacity"),
|
|
272
|
+
refillPerSecond: positiveNumber(options?.refillPerSecond, "refillPerSecond"),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function concurrency(options) {
|
|
277
|
+
return createPolicy("concurrency", options, {
|
|
278
|
+
limit: positiveInteger(options?.limit, "limit"),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
class SloppyRateLimitError extends Error {
|
|
283
|
+
constructor(message, options = undefined) {
|
|
284
|
+
super(message);
|
|
285
|
+
this.name = "SloppyRateLimitError";
|
|
286
|
+
this.code = options?.code ?? "SLOPPY_E_RATE_LIMIT";
|
|
287
|
+
this.policy = options?.policy;
|
|
288
|
+
this.store = options?.store;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function normalizeStoreOptions(options = undefined) {
|
|
293
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
294
|
+
throw new TypeError("Sloppy RateLimit.memory options must be a plain object.");
|
|
295
|
+
}
|
|
296
|
+
const maxKeys = positiveInteger(options?.maxKeys ?? DEFAULT_MAX_KEYS, "maxKeys", 1000000);
|
|
297
|
+
const cleanupIntervalMs = positiveInteger(options?.cleanupIntervalMs ?? 60000, "cleanupIntervalMs");
|
|
298
|
+
return Object.freeze({
|
|
299
|
+
name: optionalName(options?.name) ?? "memory",
|
|
300
|
+
maxKeys,
|
|
301
|
+
cleanupIntervalMs,
|
|
302
|
+
rejectOnMaxKeys: options?.rejectOnMaxKeys === true,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function entryExpired(entry, current) {
|
|
307
|
+
return entry.expiresAt !== undefined && entry.expiresAt <= current;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function retrySeconds(retryAfterMs) {
|
|
311
|
+
return Math.max(1, Math.ceil(retryAfterMs / 1000));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function createMemoryStore(options = undefined) {
|
|
315
|
+
const config = normalizeStoreOptions(options);
|
|
316
|
+
const entries = new Map();
|
|
317
|
+
let disposed = false;
|
|
318
|
+
let lastCleanup = 0;
|
|
319
|
+
let evictions = 0;
|
|
320
|
+
let rejectedKeys = 0;
|
|
321
|
+
|
|
322
|
+
function assertActive() {
|
|
323
|
+
if (disposed) {
|
|
324
|
+
throw new SloppyRateLimitError("SLOPPY_E_RATE_LIMIT_STORE_DISPOSED: memory rate-limit store is disposed.", {
|
|
325
|
+
code: "SLOPPY_E_RATE_LIMIT_STORE_DISPOSED",
|
|
326
|
+
store: config.name,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function cleanup(current, force = false) {
|
|
332
|
+
if (!force && current - lastCleanup < config.cleanupIntervalMs) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
lastCleanup = current;
|
|
336
|
+
for (const [key, entry] of entries) {
|
|
337
|
+
if (entryExpired(entry, current)) {
|
|
338
|
+
entries.delete(key);
|
|
339
|
+
evictions += 1;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function ensureKey(key, createEntry, current) {
|
|
345
|
+
cleanup(current);
|
|
346
|
+
let entry = entries.get(key);
|
|
347
|
+
if (entry !== undefined) {
|
|
348
|
+
return entry;
|
|
349
|
+
}
|
|
350
|
+
if (entries.size >= config.maxKeys) {
|
|
351
|
+
cleanup(current, true);
|
|
352
|
+
}
|
|
353
|
+
if (entries.size >= config.maxKeys) {
|
|
354
|
+
if (config.rejectOnMaxKeys) {
|
|
355
|
+
rejectedKeys += 1;
|
|
356
|
+
throw new SloppyRateLimitError("SLOPPY_E_RATE_LIMIT_STORE_FULL: memory rate-limit store reached maxKeys.", {
|
|
357
|
+
code: "SLOPPY_E_RATE_LIMIT_STORE_FULL",
|
|
358
|
+
store: config.name,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const oldest = entries.keys().next().value;
|
|
362
|
+
if (oldest !== undefined) {
|
|
363
|
+
entries.delete(oldest);
|
|
364
|
+
evictions += 1;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
entry = createEntry();
|
|
368
|
+
entries.set(key, entry);
|
|
369
|
+
return entry;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function checkFixedWindow(key, policy, cost, current) {
|
|
373
|
+
const windowMs = policy.metadata.windowMs;
|
|
374
|
+
const limit = policy.metadata.limit;
|
|
375
|
+
const entry = ensureKey(key, () => ({ count: 0, resetAt: current + windowMs, expiresAt: current + windowMs }), current);
|
|
376
|
+
if (current >= entry.resetAt) {
|
|
377
|
+
entry.count = 0;
|
|
378
|
+
entry.resetAt = current + windowMs;
|
|
379
|
+
entry.expiresAt = entry.resetAt;
|
|
380
|
+
}
|
|
381
|
+
const allowed = entry.count + cost <= limit;
|
|
382
|
+
if (allowed) {
|
|
383
|
+
entry.count += cost;
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
allowed,
|
|
387
|
+
limit,
|
|
388
|
+
remaining: Math.max(0, limit - entry.count),
|
|
389
|
+
resetAtMs: entry.resetAt,
|
|
390
|
+
retryAfterMs: allowed ? 0 : Math.max(1, entry.resetAt - current),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function checkSlidingWindow(key, policy, cost, current) {
|
|
395
|
+
const windowMs = policy.metadata.windowMs;
|
|
396
|
+
const limit = policy.metadata.limit;
|
|
397
|
+
const cutoff = current - windowMs;
|
|
398
|
+
const entry = ensureKey(key, () => ({ hits: [], expiresAt: current + windowMs }), current);
|
|
399
|
+
entry.hits = entry.hits.filter((hit) => hit.at > cutoff);
|
|
400
|
+
const used = entry.hits.reduce((sum, hit) => sum + hit.cost, 0);
|
|
401
|
+
const allowed = used + cost <= limit;
|
|
402
|
+
if (allowed) {
|
|
403
|
+
entry.hits.push({ at: current, cost });
|
|
404
|
+
}
|
|
405
|
+
const nextUsed = allowed ? used + cost : used;
|
|
406
|
+
const oldest = entry.hits[0]?.at ?? current;
|
|
407
|
+
entry.expiresAt = current + windowMs;
|
|
408
|
+
return {
|
|
409
|
+
allowed,
|
|
410
|
+
limit,
|
|
411
|
+
remaining: Math.max(0, limit - nextUsed),
|
|
412
|
+
resetAtMs: oldest + windowMs,
|
|
413
|
+
retryAfterMs: allowed ? 0 : Math.max(1, oldest + windowMs - current),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function checkTokenBucket(key, policy, cost, current) {
|
|
418
|
+
const capacity = policy.metadata.capacity;
|
|
419
|
+
const refillPerSecond = policy.metadata.refillPerSecond;
|
|
420
|
+
const entry = ensureKey(key, () => ({
|
|
421
|
+
tokens: capacity,
|
|
422
|
+
updatedAt: current,
|
|
423
|
+
expiresAt: current + Math.ceil((capacity / refillPerSecond) * 1000) * 2,
|
|
424
|
+
}), current);
|
|
425
|
+
const elapsedSeconds = Math.max(0, (current - entry.updatedAt) / 1000);
|
|
426
|
+
entry.tokens = Math.min(capacity, entry.tokens + elapsedSeconds * refillPerSecond);
|
|
427
|
+
entry.updatedAt = current;
|
|
428
|
+
const allowed = entry.tokens >= cost;
|
|
429
|
+
if (allowed) {
|
|
430
|
+
entry.tokens -= cost;
|
|
431
|
+
}
|
|
432
|
+
const missing = Math.max(0, cost - entry.tokens);
|
|
433
|
+
const retryAfterMs = allowed ? 0 : Math.ceil((missing / refillPerSecond) * 1000);
|
|
434
|
+
entry.expiresAt = current + Math.ceil((capacity / refillPerSecond) * 1000) * 2;
|
|
435
|
+
return {
|
|
436
|
+
allowed,
|
|
437
|
+
limit: capacity,
|
|
438
|
+
remaining: Math.max(0, Math.floor(entry.tokens)),
|
|
439
|
+
resetAtMs: current + retryAfterMs,
|
|
440
|
+
retryAfterMs,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function checkConcurrency(key, policy, cost, current) {
|
|
445
|
+
const limit = policy.metadata.limit;
|
|
446
|
+
const entry = ensureKey(key, () => ({ active: 0, expiresAt: current + 60000 }), current);
|
|
447
|
+
const allowed = entry.active + cost <= limit;
|
|
448
|
+
if (!allowed) {
|
|
449
|
+
return {
|
|
450
|
+
allowed: false,
|
|
451
|
+
limit,
|
|
452
|
+
remaining: Math.max(0, limit - entry.active),
|
|
453
|
+
resetAtMs: current + 1000,
|
|
454
|
+
retryAfterMs: 1000,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
entry.active += cost;
|
|
458
|
+
entry.expiresAt = undefined;
|
|
459
|
+
let released = false;
|
|
460
|
+
return {
|
|
461
|
+
allowed: true,
|
|
462
|
+
limit,
|
|
463
|
+
remaining: Math.max(0, limit - entry.active),
|
|
464
|
+
resetAtMs: current,
|
|
465
|
+
retryAfterMs: 0,
|
|
466
|
+
release() {
|
|
467
|
+
if (released) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
released = true;
|
|
471
|
+
entry.active = Math.max(0, entry.active - cost);
|
|
472
|
+
if (entry.active === 0) {
|
|
473
|
+
entries.delete(key);
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const store = {
|
|
480
|
+
[STORE_MARKER]: true,
|
|
481
|
+
__sloppyRateLimitStore: true,
|
|
482
|
+
kind: "memory",
|
|
483
|
+
name: config.name,
|
|
484
|
+
async check(input) {
|
|
485
|
+
assertActive();
|
|
486
|
+
const current = input.nowMs ?? nowMs();
|
|
487
|
+
const policyKey = input.policyKey ?? input.policy.name;
|
|
488
|
+
const key = `${policyKey}:${input.policy.algorithm}:${input.partitionHash}`;
|
|
489
|
+
if (input.policy.algorithm === "fixedWindow") {
|
|
490
|
+
return checkFixedWindow(key, input.policy, input.cost, current);
|
|
491
|
+
}
|
|
492
|
+
if (input.policy.algorithm === "slidingWindow") {
|
|
493
|
+
return checkSlidingWindow(key, input.policy, input.cost, current);
|
|
494
|
+
}
|
|
495
|
+
if (input.policy.algorithm === "tokenBucket") {
|
|
496
|
+
return checkTokenBucket(key, input.policy, input.cost, current);
|
|
497
|
+
}
|
|
498
|
+
if (input.policy.algorithm === "concurrency") {
|
|
499
|
+
return checkConcurrency(key, input.policy, input.cost, current);
|
|
500
|
+
}
|
|
501
|
+
throw new SloppyRateLimitError(`SLOPPY_E_RATE_LIMIT_ALGORITHM: unsupported algorithm '${input.policy.algorithm}'.`, {
|
|
502
|
+
code: "SLOPPY_E_RATE_LIMIT_ALGORITHM",
|
|
503
|
+
store: config.name,
|
|
504
|
+
});
|
|
505
|
+
},
|
|
506
|
+
stats() {
|
|
507
|
+
cleanup(nowMs(), true);
|
|
508
|
+
return Object.freeze({
|
|
509
|
+
kind: "memory",
|
|
510
|
+
name: config.name,
|
|
511
|
+
keys: entries.size,
|
|
512
|
+
maxKeys: config.maxKeys,
|
|
513
|
+
evictions,
|
|
514
|
+
rejectedKeys,
|
|
515
|
+
disposed,
|
|
516
|
+
});
|
|
517
|
+
},
|
|
518
|
+
reset() {
|
|
519
|
+
entries.clear();
|
|
520
|
+
evictions = 0;
|
|
521
|
+
rejectedKeys = 0;
|
|
522
|
+
},
|
|
523
|
+
dispose() {
|
|
524
|
+
disposed = true;
|
|
525
|
+
entries.clear();
|
|
526
|
+
},
|
|
527
|
+
async health() {
|
|
528
|
+
return disposed
|
|
529
|
+
? { status: "unhealthy", message: "memory rate-limit store is disposed" }
|
|
530
|
+
: { status: "healthy", data: store.stats() };
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
return Object.freeze(store);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function redis(redisClient, options = undefined) {
|
|
537
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
538
|
+
throw new TypeError("Sloppy RateLimit.redis options must be a plain object.");
|
|
539
|
+
}
|
|
540
|
+
const prefix = options?.prefix ?? "sloppy:rl:";
|
|
541
|
+
if (typeof prefix !== "string" || prefix.length === 0 || /[\x00-\x1F\x7F]/u.test(prefix)) {
|
|
542
|
+
throw new TypeError("Sloppy RateLimit.redis prefix must be a non-empty string without control characters.");
|
|
543
|
+
}
|
|
544
|
+
const name = optionalName(options?.name) ?? "redis";
|
|
545
|
+
return Object.freeze({
|
|
546
|
+
[STORE_MARKER]: true,
|
|
547
|
+
__sloppyRateLimitStore: true,
|
|
548
|
+
kind: "redis",
|
|
549
|
+
name,
|
|
550
|
+
prefix,
|
|
551
|
+
async check() {
|
|
552
|
+
throw new SloppyRateLimitError(
|
|
553
|
+
"SLOPPY_E_RATE_LIMIT_REDIS_UNAVAILABLE: Redis rate-limit store requires the Sloppy Redis provider, which is not present in this build.",
|
|
554
|
+
{ code: "SLOPPY_E_RATE_LIMIT_REDIS_UNAVAILABLE", store: name },
|
|
555
|
+
);
|
|
556
|
+
},
|
|
557
|
+
async health() {
|
|
558
|
+
if (redisClient !== undefined && typeof redisClient.ping === "function") {
|
|
559
|
+
try {
|
|
560
|
+
await redisClient.ping();
|
|
561
|
+
return {
|
|
562
|
+
status: "degraded",
|
|
563
|
+
message: "Redis connection responded, but the Sloppy Redis rate-limit provider is not available in this build.",
|
|
564
|
+
errorCode: "SLOPPY_E_RATE_LIMIT_REDIS_UNAVAILABLE",
|
|
565
|
+
data: { kind: "redis", prefixHash: stableHash(prefix) },
|
|
566
|
+
};
|
|
567
|
+
} catch (error) {
|
|
568
|
+
return { status: "unhealthy", message: String(error?.message ?? error) };
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
status: "degraded",
|
|
573
|
+
message: "Redis provider is not available in this Sloppy build.",
|
|
574
|
+
errorCode: "SLOPPY_E_RATE_LIMIT_REDIS_UNAVAILABLE",
|
|
575
|
+
};
|
|
576
|
+
},
|
|
577
|
+
stats() {
|
|
578
|
+
return Object.freeze({ kind: "redis", name, prefixHash: stableHash(prefix), available: false });
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function isRateLimitPolicy(value) {
|
|
584
|
+
return value?.[POLICY_MARKER] === true;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function isRateLimitStore(value) {
|
|
588
|
+
return value?.[STORE_MARKER] === true;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function snapshotRateLimitPolicy(policy) {
|
|
592
|
+
if (!isRateLimitPolicy(policy)) {
|
|
593
|
+
throw new TypeError("Sloppy endpoint rateLimit expects a RateLimit policy.");
|
|
594
|
+
}
|
|
595
|
+
return Object.freeze({
|
|
596
|
+
...policy.metadata,
|
|
597
|
+
partition: Object.freeze({ ...policy.metadata.partition }),
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function resolveStore(ctx, policy) {
|
|
602
|
+
if (isRateLimitStore(policy.store)) {
|
|
603
|
+
return policy.store;
|
|
604
|
+
}
|
|
605
|
+
const stores = ctx?.__sloppyHost?.rateLimitStores;
|
|
606
|
+
const name = typeof policy.store === "string" ? policy.store : "default";
|
|
607
|
+
const store = stores?.get?.(name);
|
|
608
|
+
if (store === undefined) {
|
|
609
|
+
throw new SloppyRateLimitError(`SLOPPY_E_RATE_LIMIT_STORE_NOT_FOUND: rate-limit store '${name}' is not registered.`, {
|
|
610
|
+
code: "SLOPPY_E_RATE_LIMIT_STORE_NOT_FOUND",
|
|
611
|
+
policy: policy.name,
|
|
612
|
+
store: name,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
return store;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function normalizeCost(policy, ctx) {
|
|
619
|
+
const value = typeof policy.options.cost === "function" ? policy.options.cost(ctx) : policy.options.cost;
|
|
620
|
+
return positiveNumber(value, "cost", DEFAULT_MAX_COST);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function routeLabel(ctx) {
|
|
624
|
+
return ctx?.routePattern ?? ctx?.route?.pattern ?? "unknown";
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function effectivePolicyKey(policy, ctx) {
|
|
628
|
+
if (policy.nameExplicit === true) {
|
|
629
|
+
return `named:${policy.name}`;
|
|
630
|
+
}
|
|
631
|
+
const method = ctx?.request?.method ?? ctx?.method ?? "UNKNOWN";
|
|
632
|
+
return `route:${String(method).toUpperCase()}:${routeLabel(ctx)}:${policy.name}`;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function metricLabels(policy, store, outcome, ctx) {
|
|
636
|
+
return Object.freeze({
|
|
637
|
+
policy: policy.name,
|
|
638
|
+
route: routeLabel(ctx),
|
|
639
|
+
algorithm: policy.algorithm,
|
|
640
|
+
store: store.kind,
|
|
641
|
+
outcome,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function recordMetric(ctx, name, labels, value = undefined) {
|
|
646
|
+
try {
|
|
647
|
+
const registry = ctx?.metrics;
|
|
648
|
+
if (registry?.counter !== undefined && name.endsWith(".total")) {
|
|
649
|
+
registry.counter(name).inc(labels, value ?? 1);
|
|
650
|
+
} else if (registry?.gauge !== undefined) {
|
|
651
|
+
registry.gauge(name).set(labels, value ?? 0);
|
|
652
|
+
} else if (typeof registry?.increment === "function") {
|
|
653
|
+
registry.increment(name, labels, value ?? 1);
|
|
654
|
+
}
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function recordDiagnostic(ctx, policy, store, partitionHash, result) {
|
|
660
|
+
try {
|
|
661
|
+
ctx?.diagnostics?.record?.({
|
|
662
|
+
code: result.allowed ? "SLOPPY_RATE_LIMIT_ALLOWED" : ERROR_CODE,
|
|
663
|
+
subsystem: "rate-limit",
|
|
664
|
+
severity: result.allowed ? "debug" : "warn",
|
|
665
|
+
message: result.allowed ? "Rate limit allowed request." : "Rate limit denied request.",
|
|
666
|
+
fields: {
|
|
667
|
+
policy: policy.name,
|
|
668
|
+
route: routeLabel(ctx),
|
|
669
|
+
algorithm: policy.algorithm,
|
|
670
|
+
store: store.kind,
|
|
671
|
+
partitionHash,
|
|
672
|
+
reason: result.allowed ? "allowed" : "limit-exceeded",
|
|
673
|
+
retryAfterMs: result.retryAfterMs,
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
} catch {
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function denialProblem(ctx, policy, result) {
|
|
681
|
+
const base = Object.freeze({
|
|
682
|
+
type: DEFAULT_PROBLEM_TYPE,
|
|
683
|
+
title: "Too Many Requests",
|
|
684
|
+
status: 429,
|
|
685
|
+
code: ERROR_CODE,
|
|
686
|
+
});
|
|
687
|
+
const custom = typeof policy.options.problem === "function"
|
|
688
|
+
? policy.options.problem(ctx, result)
|
|
689
|
+
: policy.options.problem;
|
|
690
|
+
return freezeJson({ ...base, ...(custom ?? {}) }, "problem");
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function denialResult(ctx, policy, result) {
|
|
694
|
+
const retryAfter = retrySeconds(result.retryAfterMs);
|
|
695
|
+
return Results.problem(denialProblem(ctx, policy, result), {
|
|
696
|
+
status: 429,
|
|
697
|
+
headers: {
|
|
698
|
+
"Retry-After": String(retryAfter),
|
|
699
|
+
"RateLimit-Limit": String(Math.trunc(result.limit)),
|
|
700
|
+
"RateLimit-Remaining": String(Math.trunc(result.remaining)),
|
|
701
|
+
"RateLimit-Reset": String(retryAfter),
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function partitionHash(policy, partitionValue) {
|
|
707
|
+
const metadata = policy.partition.metadata;
|
|
708
|
+
return stableHash(stableStringify({
|
|
709
|
+
customMarker: metadata.marker,
|
|
710
|
+
fallback: metadata.fallback,
|
|
711
|
+
fallbackTrustProxy: metadata.fallbackTrustProxy === true,
|
|
712
|
+
kind: metadata.kind,
|
|
713
|
+
name: metadata.name,
|
|
714
|
+
partial: metadata.partial === true,
|
|
715
|
+
trustProxy: metadata.trustProxy === true,
|
|
716
|
+
value: String(partitionValue),
|
|
717
|
+
}));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async function enforceRateLimit(ctx, policy) {
|
|
721
|
+
if (!isRateLimitPolicy(policy)) {
|
|
722
|
+
throw new TypeError("Sloppy rate-limit enforcement expects a RateLimit policy.");
|
|
723
|
+
}
|
|
724
|
+
if (policy.options.skip !== undefined && await policy.options.skip(ctx)) {
|
|
725
|
+
return Object.freeze({ allowed: true, skipped: true });
|
|
726
|
+
}
|
|
727
|
+
if (policy.partition.needsAuth && ctx?.user?.authenticated !== true) {
|
|
728
|
+
throw new SloppyRateLimitError("SLOPPY_E_RATE_LIMIT_AUTH_REQUIRED: authenticated partition requires an authenticated route.", {
|
|
729
|
+
code: "SLOPPY_E_RATE_LIMIT_AUTH_REQUIRED",
|
|
730
|
+
policy: policy.name,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
const partitionValue = await policy.partition.resolve(ctx);
|
|
734
|
+
if (partitionValue === undefined || partitionValue === null || partitionValue === "") {
|
|
735
|
+
throw new SloppyRateLimitError("SLOPPY_E_RATE_LIMIT_PARTITION_EMPTY: partition resolved to an empty value.", {
|
|
736
|
+
code: "SLOPPY_E_RATE_LIMIT_PARTITION_EMPTY",
|
|
737
|
+
policy: policy.name,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
const partitionHashValue = partitionHash(policy, partitionValue);
|
|
741
|
+
const store = resolveStore(ctx, policy);
|
|
742
|
+
const cost = normalizeCost(policy, ctx);
|
|
743
|
+
const current = nowMs(ctx?.clock);
|
|
744
|
+
let result;
|
|
745
|
+
try {
|
|
746
|
+
result = await store.check({
|
|
747
|
+
policy,
|
|
748
|
+
policyKey: effectivePolicyKey(policy, ctx),
|
|
749
|
+
cost,
|
|
750
|
+
partitionHash: partitionHashValue,
|
|
751
|
+
nowMs: current,
|
|
752
|
+
});
|
|
753
|
+
} catch (error) {
|
|
754
|
+
recordMetric(ctx, "rate_limit.store.errors.total", metricLabels(policy, store, "error", ctx));
|
|
755
|
+
throw error;
|
|
756
|
+
}
|
|
757
|
+
const labels = metricLabels(policy, store, result.allowed ? "allowed" : "denied", ctx);
|
|
758
|
+
recordMetric(ctx, "rate_limit.requests.total", labels);
|
|
759
|
+
recordMetric(ctx, result.allowed ? "rate_limit.allowed.total" : "rate_limit.denied.total", labels);
|
|
760
|
+
recordMetric(ctx, "rate_limit.tokens.remaining", labels, result.remaining);
|
|
761
|
+
if (policy.algorithm === "concurrency") {
|
|
762
|
+
recordMetric(ctx, "rate_limit.concurrency.active", labels, Math.max(0, result.limit - result.remaining));
|
|
763
|
+
}
|
|
764
|
+
recordDiagnostic(ctx, policy, store, partitionHashValue, result);
|
|
765
|
+
if (result.allowed) {
|
|
766
|
+
return Object.freeze({ ...result, partitionHash: partitionHashValue, store, release: result.release });
|
|
767
|
+
}
|
|
768
|
+
return Object.freeze({
|
|
769
|
+
...result,
|
|
770
|
+
partitionHash: partitionHashValue,
|
|
771
|
+
store,
|
|
772
|
+
response: denialResult(ctx, policy, result),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function rateLimitHealth(store) {
|
|
777
|
+
if (!isRateLimitStore(store)) {
|
|
778
|
+
throw new TypeError("Health.rateLimit expects a RateLimit store.");
|
|
779
|
+
}
|
|
780
|
+
return async () => {
|
|
781
|
+
if (typeof store.health === "function") {
|
|
782
|
+
return store.health();
|
|
783
|
+
}
|
|
784
|
+
return { status: "healthy", data: { kind: store.kind } };
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const partition = Object.freeze({
|
|
789
|
+
ip(options = undefined) {
|
|
790
|
+
const proxy = validateTrustProxyOptions(options);
|
|
791
|
+
return createPartition(
|
|
792
|
+
"ip",
|
|
793
|
+
{ kind: "ip", trustProxy: proxy.trustProxy },
|
|
794
|
+
(ctx) => `ip:${rawRemoteAddress(ctx, proxy)}`,
|
|
795
|
+
);
|
|
796
|
+
},
|
|
797
|
+
user() {
|
|
798
|
+
return createPartition("user", { kind: "user" }, (ctx) => ctx?.user?.sub, { needsAuth: true });
|
|
799
|
+
},
|
|
800
|
+
apiKey() {
|
|
801
|
+
return createPartition("apiKey", { kind: "apiKey" }, (ctx) => {
|
|
802
|
+
if (ctx?.user?.authenticated === true && /api.?key/iu.test(ctx.user.scheme ?? ctx.user.authScheme ?? "")) {
|
|
803
|
+
return ctx.user.sub;
|
|
804
|
+
}
|
|
805
|
+
return requestHeader(ctx, "x-api-key");
|
|
806
|
+
});
|
|
807
|
+
},
|
|
808
|
+
header(name) {
|
|
809
|
+
const header = validateHeaderName(name);
|
|
810
|
+
return createPartition("header", { kind: "header", name: header }, (ctx) => requestHeader(ctx, header));
|
|
811
|
+
},
|
|
812
|
+
claim(name) {
|
|
813
|
+
const claim = optionalName(name, "claim name");
|
|
814
|
+
return createPartition("claim", { kind: "claim", name: claim }, (ctx) => ctx?.user?.claims?.[claim], { needsAuth: true });
|
|
815
|
+
},
|
|
816
|
+
routeParam(name) {
|
|
817
|
+
const routeParam = optionalName(name, "route parameter name");
|
|
818
|
+
return createPartition("routeParam", { kind: "routeParam", name: routeParam }, (ctx) => ctx?.route?.[routeParam]);
|
|
819
|
+
},
|
|
820
|
+
custom(fn, options = undefined) {
|
|
821
|
+
if (typeof fn !== "function") {
|
|
822
|
+
throw new TypeError("Sloppy RateLimit.partition.custom expects a function.");
|
|
823
|
+
}
|
|
824
|
+
if (options !== undefined && !isPlainObject(options)) {
|
|
825
|
+
throw new TypeError("Sloppy RateLimit.partition.custom options must be a plain object.");
|
|
826
|
+
}
|
|
827
|
+
const marker = optionalName(options?.marker ?? (fn.name || "anonymous"), "custom partition marker");
|
|
828
|
+
return createPartition("custom", { kind: "custom", marker, partial: true }, fn);
|
|
829
|
+
},
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
function token(name = undefined) {
|
|
833
|
+
return optionalName(name, "token") ?? "default";
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const RateLimit = Object.freeze({
|
|
837
|
+
fixedWindow,
|
|
838
|
+
slidingWindow,
|
|
839
|
+
tokenBucket,
|
|
840
|
+
concurrency,
|
|
841
|
+
memory: createMemoryStore,
|
|
842
|
+
redis,
|
|
843
|
+
partition,
|
|
844
|
+
token,
|
|
845
|
+
health: rateLimitHealth,
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
export {
|
|
849
|
+
RateLimit,
|
|
850
|
+
SloppyRateLimitError,
|
|
851
|
+
enforceRateLimit,
|
|
852
|
+
isRateLimitPolicy,
|
|
853
|
+
isRateLimitStore,
|
|
854
|
+
rateLimitHealth,
|
|
855
|
+
snapshotRateLimitPolicy,
|
|
856
|
+
};
|