@udibo/oauth2 0.1.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 +20 -0
- package/README.md +174 -0
- package/SECURITY.md +77 -0
- package/esm/_dnt.polyfills.d.ts +17 -0
- package/esm/_dnt.polyfills.js +15 -0
- package/esm/adapters/hono/_common.d.ts +66 -0
- package/esm/adapters/hono/_common.js +108 -0
- package/esm/adapters/hono/authorization-server.d.ts +211 -0
- package/esm/adapters/hono/authorization-server.js +214 -0
- package/esm/adapters/hono/bff/auth-request-store.d.ts +164 -0
- package/esm/adapters/hono/bff/auth-request-store.js +154 -0
- package/esm/adapters/hono/bff/bff.d.ts +852 -0
- package/esm/adapters/hono/bff/bff.js +1194 -0
- package/esm/adapters/hono/bff/cookie-prefix.d.ts +72 -0
- package/esm/adapters/hono/bff/cookie-prefix.js +85 -0
- package/esm/adapters/hono/bff/mod.d.ts +19 -0
- package/esm/adapters/hono/bff/mod.js +19 -0
- package/esm/adapters/hono/bff/proxy.d.ts +108 -0
- package/esm/adapters/hono/bff/proxy.js +314 -0
- package/esm/adapters/hono/bff/session-store.d.ts +250 -0
- package/esm/adapters/hono/bff/session-store.js +216 -0
- package/esm/adapters/hono/bff/testing.d.ts +165 -0
- package/esm/adapters/hono/bff/testing.js +254 -0
- package/esm/adapters/hono/identity.d.ts +117 -0
- package/esm/adapters/hono/identity.js +215 -0
- package/esm/adapters/hono/log.d.ts +49 -0
- package/esm/adapters/hono/log.js +66 -0
- package/esm/adapters/hono/resource-server.d.ts +101 -0
- package/esm/adapters/hono/resource-server.js +108 -0
- package/esm/client/_http.d.ts +91 -0
- package/esm/client/_http.js +303 -0
- package/esm/client/base.d.ts +184 -0
- package/esm/client/base.js +93 -0
- package/esm/client/bff-client.d.ts +235 -0
- package/esm/client/bff-client.js +280 -0
- package/esm/client/browser-storage.d.ts +152 -0
- package/esm/client/browser-storage.js +246 -0
- package/esm/client/check.d.ts +114 -0
- package/esm/client/check.js +97 -0
- package/esm/client/direct-client.d.ts +707 -0
- package/esm/client/direct-client.js +1237 -0
- package/esm/client/discovery-cache.d.ts +138 -0
- package/esm/client/discovery-cache.js +124 -0
- package/esm/client/events.d.ts +54 -0
- package/esm/client/events.js +37 -0
- package/esm/client/mod.d.ts +35 -0
- package/esm/client/mod.js +33 -0
- package/esm/client/storage.d.ts +127 -0
- package/esm/client/storage.js +81 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/almost_equals.d.ts +29 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/almost_equals.js +47 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/array_includes.d.ts +28 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/array_includes.js +65 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/assert.d.ts +16 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/assert.js +23 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/assertion_error.d.ts +25 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/assertion_error.js +30 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/equal.d.ts +24 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/equal.js +193 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/equals.d.ts +36 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/equals.js +61 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/exists.d.ts +17 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/exists.js +27 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/fail.d.ts +14 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/fail.js +20 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/false.d.ts +18 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/false.js +23 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater.d.ts +19 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater.js +29 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.d.ts +19 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.js +29 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/instance_of.d.ts +22 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/instance_of.js +52 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/is_error.d.ts +24 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/is_error.js +53 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/less.d.ts +18 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/less.js +28 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/less_or_equal.d.ts +19 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/less_or_equal.js +29 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/match.d.ts +17 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/match.js +26 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/mod.d.ts +43 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/mod.js +45 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_equals.d.ts +26 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_equals.js +39 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_instance_of.d.ts +19 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_instance_of.js +29 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_match.d.ts +17 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_match.js +26 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.d.ts +22 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.js +32 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/object_match.d.ts +28 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/object_match.js +177 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/rejects.d.ts +41 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/rejects.js +53 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/strict_equals.d.ts +23 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/strict_equals.js +58 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/string_includes.d.ts +17 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/string_includes.js +26 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/throws.d.ts +44 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/throws.js +44 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/unimplemented.d.ts +14 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/unimplemented.js +20 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/unreachable.d.ts +14 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/unreachable.js +20 -0
- package/esm/deps/jsr.io/@std/async/1.5.0/delay.d.ts +44 -0
- package/esm/deps/jsr.io/@std/async/1.5.0/delay.js +84 -0
- package/esm/deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.d.ts +39 -0
- package/esm/deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.js +61 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common16.d.ts +22 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common16.js +51 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common32.d.ts +34 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common32.js +192 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common64.d.ts +34 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common64.js +113 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common_detach.d.ts +3 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common_detach.js +13 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_types.d.ts +8 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_types.js +2 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_validate_binary_like.d.ts +1 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_validate_binary_like.js +26 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/ascii85.d.ts +60 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/ascii85.js +152 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base32.d.ts +39 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base32.js +87 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base58.d.ts +39 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base58.js +131 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base64.d.ts +39 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base64.js +82 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base64url.d.ts +39 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base64url.js +72 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/hex.d.ts +38 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/hex.js +87 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/mod.d.ts +97 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/mod.js +99 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/varint.d.ts +119 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/varint.js +209 -0
- package/esm/deps/jsr.io/@std/http/1.1.3/status.d.ts +343 -0
- package/esm/deps/jsr.io/@std/http/1.1.3/status.js +343 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/assertion_state.d.ts +159 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/assertion_state.js +223 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/build_message.d.ts +79 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/build_message.js +116 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/diff.d.ts +139 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/diff.js +277 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/diff_str.d.ts +98 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/diff_str.js +181 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/format.d.ts +29 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/format.js +68 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/styles.d.ts +158 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/styles.js +206 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/types.d.ts +24 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/types.js +3 -0
- package/esm/deps/jsr.io/@std/testing/1.0.20/_test_suite.d.ts +91 -0
- package/esm/deps/jsr.io/@std/testing/1.0.20/_test_suite.js +347 -0
- package/esm/deps/jsr.io/@std/testing/1.0.20/bdd.d.ts +382 -0
- package/esm/deps/jsr.io/@std/testing/1.0.20/bdd.js +1110 -0
- package/esm/deps/jsr.io/@udibo/http-error/0.11.1/mod.d.ts +363 -0
- package/esm/deps/jsr.io/@udibo/http-error/0.11.1/mod.js +616 -0
- package/esm/errors.d.ts +203 -0
- package/esm/errors.js +287 -0
- package/esm/identity/captcha.d.ts +147 -0
- package/esm/identity/captcha.js +64 -0
- package/esm/identity/delivery.d.ts +96 -0
- package/esm/identity/delivery.js +41 -0
- package/esm/identity/errors.d.ts +39 -0
- package/esm/identity/errors.js +46 -0
- package/esm/identity/events.d.ts +242 -0
- package/esm/identity/events.js +39 -0
- package/esm/identity/external/_shared.d.ts +86 -0
- package/esm/identity/external/_shared.js +107 -0
- package/esm/identity/external/_token-exchange.d.ts +61 -0
- package/esm/identity/external/_token-exchange.js +179 -0
- package/esm/identity/external/apple-client-secret.d.ts +82 -0
- package/esm/identity/external/apple-client-secret.js +133 -0
- package/esm/identity/external/apple.d.ts +64 -0
- package/esm/identity/external/apple.js +248 -0
- package/esm/identity/external/discord.d.ts +38 -0
- package/esm/identity/external/discord.js +62 -0
- package/esm/identity/external/errors.d.ts +48 -0
- package/esm/identity/external/errors.js +36 -0
- package/esm/identity/external/flow.d.ts +107 -0
- package/esm/identity/external/flow.js +158 -0
- package/esm/identity/external/github.d.ts +42 -0
- package/esm/identity/external/github.js +164 -0
- package/esm/identity/external/google.d.ts +52 -0
- package/esm/identity/external/google.js +38 -0
- package/esm/identity/external/mod.d.ts +63 -0
- package/esm/identity/external/mod.js +61 -0
- package/esm/identity/external/oauth2.d.ts +110 -0
- package/esm/identity/external/oauth2.js +158 -0
- package/esm/identity/external/oidc.d.ts +81 -0
- package/esm/identity/external/oidc.js +250 -0
- package/esm/identity/external/provider.d.ts +132 -0
- package/esm/identity/external/provider.js +12 -0
- package/esm/identity/hibp.d.ts +67 -0
- package/esm/identity/hibp.js +87 -0
- package/esm/identity/identifier.d.ts +44 -0
- package/esm/identity/identifier.js +47 -0
- package/esm/identity/lockout.d.ts +155 -0
- package/esm/identity/lockout.js +117 -0
- package/esm/identity/mfa/mod.d.ts +42 -0
- package/esm/identity/mfa/mod.js +42 -0
- package/esm/identity/mfa/recovery-codes.d.ts +96 -0
- package/esm/identity/mfa/recovery-codes.js +80 -0
- package/esm/identity/mfa/service.d.ts +338 -0
- package/esm/identity/mfa/service.js +351 -0
- package/esm/identity/mfa/totp.d.ts +186 -0
- package/esm/identity/mfa/totp.js +194 -0
- package/esm/identity/migration.d.ts +155 -0
- package/esm/identity/migration.js +243 -0
- package/esm/identity/mod.d.ts +65 -0
- package/esm/identity/mod.js +64 -0
- package/esm/identity/otp.d.ts +255 -0
- package/esm/identity/otp.js +245 -0
- package/esm/identity/password-policy.d.ts +45 -0
- package/esm/identity/password-policy.js +60 -0
- package/esm/identity/password.d.ts +171 -0
- package/esm/identity/password.js +160 -0
- package/esm/identity/rate-limit.d.ts +192 -0
- package/esm/identity/rate-limit.js +168 -0
- package/esm/identity/service.d.ts +579 -0
- package/esm/identity/service.js +835 -0
- package/esm/identity/session.d.ts +122 -0
- package/esm/identity/session.js +48 -0
- package/esm/identity/token-flow.d.ts +214 -0
- package/esm/identity/token-flow.js +173 -0
- package/esm/models/authentication.d.ts +24 -0
- package/esm/models/authentication.js +1 -0
- package/esm/models/authorization-code.d.ts +63 -0
- package/esm/models/authorization-code.js +1 -0
- package/esm/models/authorization.d.ts +165 -0
- package/esm/models/authorization.js +160 -0
- package/esm/models/client.d.ts +78 -0
- package/esm/models/client.js +1 -0
- package/esm/models/device-authorization.d.ts +30 -0
- package/esm/models/device-authorization.js +1 -0
- package/esm/models/responses.d.ts +93 -0
- package/esm/models/responses.js +1 -0
- package/esm/models/scope.d.ts +81 -0
- package/esm/models/scope.js +125 -0
- package/esm/models/token.d.ts +135 -0
- package/esm/models/token.js +1 -0
- package/esm/package.json +3 -0
- package/esm/react/callback.d.ts +52 -0
- package/esm/react/callback.js +71 -0
- package/esm/react/components/class-names.d.ts +91 -0
- package/esm/react/components/class-names.js +118 -0
- package/esm/react/components/internal.d.ts +62 -0
- package/esm/react/components/internal.js +102 -0
- package/esm/react/components/mfa-challenge-form.d.ts +66 -0
- package/esm/react/components/mfa-challenge-form.js +46 -0
- package/esm/react/components/mfa-enrollment-form.d.ts +83 -0
- package/esm/react/components/mfa-enrollment-form.js +41 -0
- package/esm/react/components/mod.d.ts +103 -0
- package/esm/react/components/mod.js +93 -0
- package/esm/react/components/request-password-reset-form.d.ts +61 -0
- package/esm/react/components/request-password-reset-form.js +41 -0
- package/esm/react/components/reset-password-form.d.ts +66 -0
- package/esm/react/components/reset-password-form.js +56 -0
- package/esm/react/components/sign-in-form.d.ts +74 -0
- package/esm/react/components/sign-in-form.js +43 -0
- package/esm/react/components/sign-up-form.d.ts +85 -0
- package/esm/react/components/sign-up-form.js +57 -0
- package/esm/react/components/types.d.ts +208 -0
- package/esm/react/components/types.js +14 -0
- package/esm/react/components/use-auth-form.d.ts +118 -0
- package/esm/react/components/use-auth-form.js +172 -0
- package/esm/react/components/user-menu.d.ts +79 -0
- package/esm/react/components/user-menu.js +73 -0
- package/esm/react/context.d.ts +51 -0
- package/esm/react/context.js +16 -0
- package/esm/react/mod.d.ts +26 -0
- package/esm/react/mod.js +26 -0
- package/esm/react/provider.d.ts +40 -0
- package/esm/react/provider.js +189 -0
- package/esm/react/require-auth.d.ts +31 -0
- package/esm/react/require-auth.js +37 -0
- package/esm/react/testing.d.ts +322 -0
- package/esm/react/testing.js +461 -0
- package/esm/react/use-authorization.d.ts +31 -0
- package/esm/react/use-authorization.js +36 -0
- package/esm/react/use-oauth2.d.ts +112 -0
- package/esm/react/use-oauth2.js +127 -0
- package/esm/server/authorization-server.d.ts +736 -0
- package/esm/server/authorization-server.js +1304 -0
- package/esm/server/client-authentication.d.ts +28 -0
- package/esm/server/client-authentication.js +51 -0
- package/esm/server/grants/authorization-code.d.ts +179 -0
- package/esm/server/grants/authorization-code.js +232 -0
- package/esm/server/grants/client-credentials.d.ts +52 -0
- package/esm/server/grants/client-credentials.js +53 -0
- package/esm/server/grants/device-authorization.d.ts +103 -0
- package/esm/server/grants/device-authorization.js +156 -0
- package/esm/server/grants/grant.d.ts +165 -0
- package/esm/server/grants/grant.js +173 -0
- package/esm/server/grants/password.d.ts +80 -0
- package/esm/server/grants/password.js +92 -0
- package/esm/server/grants/refresh-token.d.ts +80 -0
- package/esm/server/grants/refresh-token.js +136 -0
- package/esm/server/introspection-token-reader.d.ts +153 -0
- package/esm/server/introspection-token-reader.js +164 -0
- package/esm/server/jwks-token-reader.d.ts +263 -0
- package/esm/server/jwks-token-reader.js +508 -0
- package/esm/server/mod.d.ts +23 -0
- package/esm/server/mod.js +15 -0
- package/esm/server/public-suffix/mod.d.ts +62 -0
- package/esm/server/public-suffix/mod.js +103 -0
- package/esm/server/public-suffix/rules.d.ts +23 -0
- package/esm/server/public-suffix/rules.js +23 -0
- package/esm/server/redirect-uri.d.ts +187 -0
- package/esm/server/redirect-uri.js +331 -0
- package/esm/server/resource-server.d.ts +286 -0
- package/esm/server/resource-server.js +365 -0
- package/esm/server/services/authorization-code.d.ts +84 -0
- package/esm/server/services/authorization-code.js +35 -0
- package/esm/server/services/client.d.ts +35 -0
- package/esm/server/services/client.js +1 -0
- package/esm/server/services/device-authorization.d.ts +134 -0
- package/esm/server/services/device-authorization.js +7 -0
- package/esm/server/services/token.d.ts +242 -0
- package/esm/server/services/token.js +78 -0
- package/esm/server/services/user.d.ts +29 -0
- package/esm/server/services/user.js +7 -0
- package/esm/server/signing-keys.d.ts +201 -0
- package/esm/server/signing-keys.js +225 -0
- package/esm/server/utils/hash.d.ts +6 -0
- package/esm/server/utils/hash.js +11 -0
- package/esm/testing/contract/authorization-code.d.ts +37 -0
- package/esm/testing/contract/authorization-code.js +93 -0
- package/esm/testing/contract/client.d.ts +85 -0
- package/esm/testing/contract/client.js +126 -0
- package/esm/testing/contract/device-authorization.d.ts +37 -0
- package/esm/testing/contract/device-authorization.js +193 -0
- package/esm/testing/contract/lockout-store.d.ts +47 -0
- package/esm/testing/contract/lockout-store.js +122 -0
- package/esm/testing/contract/mfa-store.d.ts +50 -0
- package/esm/testing/contract/mfa-store.js +226 -0
- package/esm/testing/contract/mod.d.ts +39 -0
- package/esm/testing/contract/mod.js +39 -0
- package/esm/testing/contract/otp-store.d.ts +48 -0
- package/esm/testing/contract/otp-store.js +197 -0
- package/esm/testing/contract/rate-limit-store.d.ts +52 -0
- package/esm/testing/contract/rate-limit-store.js +105 -0
- package/esm/testing/contract/token-flow-store.d.ts +54 -0
- package/esm/testing/contract/token-flow-store.js +179 -0
- package/esm/testing/contract/token-reader.d.ts +77 -0
- package/esm/testing/contract/token-reader.js +78 -0
- package/esm/testing/contract/token.d.ts +61 -0
- package/esm/testing/contract/token.js +287 -0
- package/esm/testing/contract/user.d.ts +63 -0
- package/esm/testing/contract/user.js +75 -0
- package/esm/testing/mod.d.ts +34 -0
- package/esm/testing/mod.js +34 -0
- package/esm/testing/server.d.ts +197 -0
- package/esm/testing/server.js +193 -0
- package/esm/testing/services.d.ts +373 -0
- package/esm/testing/services.js +703 -0
- package/esm/utils/_buffer.d.ts +10 -0
- package/esm/utils/_buffer.js +12 -0
- package/esm/utils/_timing-safe.d.ts +13 -0
- package/esm/utils/_timing-safe.js +22 -0
- package/esm/utils/authentication-context.d.ts +3 -0
- package/esm/utils/authentication-context.js +14 -0
- package/esm/utils/basic-auth.d.ts +50 -0
- package/esm/utils/basic-auth.js +106 -0
- package/esm/utils/crypto.d.ts +97 -0
- package/esm/utils/crypto.js +162 -0
- package/esm/utils/pkce.d.ts +151 -0
- package/esm/utils/pkce.js +179 -0
- package/esm/utils/text.d.ts +24 -0
- package/esm/utils/text.js +28 -0
- package/esm/utils/url.d.ts +66 -0
- package/esm/utils/url.js +90 -0
- package/package.json +92 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test suites for the framework's service and store interfaces.
|
|
3
|
+
*
|
|
4
|
+
* These runners register `describe` / `it` blocks that verify a
|
|
5
|
+
* consumer's implementation of `UserServiceInterface`,
|
|
6
|
+
* `ClientServiceInterface`, `TokenServiceInterface`,
|
|
7
|
+
* `TokenReaderInterface`, `AuthorizationCodeServiceInterface`, or
|
|
8
|
+
* `DeviceAuthorizationServiceInterface` satisfies what the framework
|
|
9
|
+
* expects. The same suites run against the library's `Memory*Service`
|
|
10
|
+
* fixtures and shipped token readers internally, so consumers and the
|
|
11
|
+
* library are tested by the exact same checks.
|
|
12
|
+
*
|
|
13
|
+
* The identity layer's storage seams — `MfaStore`, `TokenFlowStore`,
|
|
14
|
+
* `OtpStore`, `RateLimitStore`, `LockoutStore` — have runners of their
|
|
15
|
+
* own. Those interfaces document methods that **must be atomic**
|
|
16
|
+
* (single-use recovery codes, single-use links, the TOTP replay guard,
|
|
17
|
+
* the guess and failure budgets), and each suite checks that claim with
|
|
18
|
+
* overlapping calls: a read-modify-write implementation passes every
|
|
19
|
+
* sequential case and fails the concurrent one, which is the whole
|
|
20
|
+
* reason the suites exist.
|
|
21
|
+
*
|
|
22
|
+
* Each runner takes a factory + seed-helpers because `add` / `register`
|
|
23
|
+
* methods aren't part of the public service interface — the consumer
|
|
24
|
+
* tells the runner how to populate test data.
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
import "../../_dnt.polyfills.js";
|
|
29
|
+
export { runUserServiceContractTests, type UserServiceContractOptions, } from "./user.js";
|
|
30
|
+
export { type ClientServiceContractOptions, runClientServiceContractTests, } from "./client.js";
|
|
31
|
+
export { runTokenServiceContractTests, type TokenServiceContractOptions, } from "./token.js";
|
|
32
|
+
export { runTokenReaderContractTests, type TokenReaderContractFixture, type TokenReaderContractOptions, } from "./token-reader.js";
|
|
33
|
+
export { type AuthorizationCodeServiceContractOptions, runAuthorizationCodeServiceContractTests, } from "./authorization-code.js";
|
|
34
|
+
export { type DeviceAuthorizationServiceContractOptions, runDeviceAuthorizationServiceContractTests, } from "./device-authorization.js";
|
|
35
|
+
export { type MfaStoreContractOptions, runMfaStoreContractTests, } from "./mfa-store.js";
|
|
36
|
+
export { runTokenFlowStoreContractTests, type TokenFlowStoreContractOptions, } from "./token-flow-store.js";
|
|
37
|
+
export { type OtpStoreContractOptions, runOtpStoreContractTests, } from "./otp-store.js";
|
|
38
|
+
export { type RateLimitStoreContractOptions, runRateLimitStoreContractTests, } from "./rate-limit-store.js";
|
|
39
|
+
export { type LockoutStoreContractOptions, runLockoutStoreContractTests, } from "./lockout-store.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test suites for the framework's service and store interfaces.
|
|
3
|
+
*
|
|
4
|
+
* These runners register `describe` / `it` blocks that verify a
|
|
5
|
+
* consumer's implementation of `UserServiceInterface`,
|
|
6
|
+
* `ClientServiceInterface`, `TokenServiceInterface`,
|
|
7
|
+
* `TokenReaderInterface`, `AuthorizationCodeServiceInterface`, or
|
|
8
|
+
* `DeviceAuthorizationServiceInterface` satisfies what the framework
|
|
9
|
+
* expects. The same suites run against the library's `Memory*Service`
|
|
10
|
+
* fixtures and shipped token readers internally, so consumers and the
|
|
11
|
+
* library are tested by the exact same checks.
|
|
12
|
+
*
|
|
13
|
+
* The identity layer's storage seams — `MfaStore`, `TokenFlowStore`,
|
|
14
|
+
* `OtpStore`, `RateLimitStore`, `LockoutStore` — have runners of their
|
|
15
|
+
* own. Those interfaces document methods that **must be atomic**
|
|
16
|
+
* (single-use recovery codes, single-use links, the TOTP replay guard,
|
|
17
|
+
* the guess and failure budgets), and each suite checks that claim with
|
|
18
|
+
* overlapping calls: a read-modify-write implementation passes every
|
|
19
|
+
* sequential case and fails the concurrent one, which is the whole
|
|
20
|
+
* reason the suites exist.
|
|
21
|
+
*
|
|
22
|
+
* Each runner takes a factory + seed-helpers because `add` / `register`
|
|
23
|
+
* methods aren't part of the public service interface — the consumer
|
|
24
|
+
* tells the runner how to populate test data.
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
import "../../_dnt.polyfills.js";
|
|
29
|
+
export { runUserServiceContractTests, } from "./user.js";
|
|
30
|
+
export { runClientServiceContractTests, } from "./client.js";
|
|
31
|
+
export { runTokenServiceContractTests, } from "./token.js";
|
|
32
|
+
export { runTokenReaderContractTests, } from "./token-reader.js";
|
|
33
|
+
export { runAuthorizationCodeServiceContractTests, } from "./authorization-code.js";
|
|
34
|
+
export { runDeviceAuthorizationServiceContractTests, } from "./device-authorization.js";
|
|
35
|
+
export { runMfaStoreContractTests, } from "./mfa-store.js";
|
|
36
|
+
export { runTokenFlowStoreContractTests, } from "./token-flow-store.js";
|
|
37
|
+
export { runOtpStoreContractTests, } from "./otp-store.js";
|
|
38
|
+
export { runRateLimitStoreContractTests, } from "./rate-limit-store.js";
|
|
39
|
+
export { runLockoutStoreContractTests, } from "./lockout-store.js";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test suite for {@link OtpStore} implementations — the storage behind
|
|
3
|
+
* emailed one-time codes.
|
|
4
|
+
*
|
|
5
|
+
* A six-digit code is guessable online, so the attempt budget is the only thing
|
|
6
|
+
* standing between an attacker and a one-in-a-million draw. `recordAttempt` is
|
|
7
|
+
* what enforces it, and it only enforces it when concurrent guesses cannot
|
|
8
|
+
* share a count — which is what this suite checks.
|
|
9
|
+
*
|
|
10
|
+
* @example Verify a database-backed store
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { runOtpStoreContractTests } from "@udibo/oauth2/testing/contract";
|
|
13
|
+
* import type { OtpStore } from "@udibo/oauth2/identity";
|
|
14
|
+
*
|
|
15
|
+
* declare function freshOtpStore(): Promise<OtpStore>;
|
|
16
|
+
*
|
|
17
|
+
* runOtpStoreContractTests({
|
|
18
|
+
* describeName: "DrizzleOtpStore satisfies OtpStore contract",
|
|
19
|
+
* makeStore: freshOtpStore,
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import type { OtpStore } from "../../identity/otp.js";
|
|
26
|
+
/** Options for {@link runOtpStoreContractTests}. */
|
|
27
|
+
export interface OtpStoreContractOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Returns a fresh, empty store for each test. Isolation is required — an
|
|
30
|
+
* active code left behind by one test makes the `findActive` checks
|
|
31
|
+
* meaningless.
|
|
32
|
+
*/
|
|
33
|
+
makeStore(): Promise<OtpStore> | OtpStore;
|
|
34
|
+
/**
|
|
35
|
+
* Overrides the name passed to the outer `describe` block. Defaults to
|
|
36
|
+
* `"OtpStore contract"`.
|
|
37
|
+
*/
|
|
38
|
+
describeName?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Runs the {@link OtpStore} contract suite. Call it from your own test file —
|
|
42
|
+
* it registers `describe` / `it` blocks the test runner picks up.
|
|
43
|
+
*
|
|
44
|
+
* The concurrency case fires overlapping `recordAttempt` calls and requires
|
|
45
|
+
* each to see a distinct count; a store that reads, awaits, then writes hands
|
|
46
|
+
* the same count to every guess and fails it.
|
|
47
|
+
*/
|
|
48
|
+
export declare function runOtpStoreContractTests(options: OtpStoreContractOptions): void;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test suite for {@link OtpStore} implementations — the storage behind
|
|
3
|
+
* emailed one-time codes.
|
|
4
|
+
*
|
|
5
|
+
* A six-digit code is guessable online, so the attempt budget is the only thing
|
|
6
|
+
* standing between an attacker and a one-in-a-million draw. `recordAttempt` is
|
|
7
|
+
* what enforces it, and it only enforces it when concurrent guesses cannot
|
|
8
|
+
* share a count — which is what this suite checks.
|
|
9
|
+
*
|
|
10
|
+
* @example Verify a database-backed store
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { runOtpStoreContractTests } from "@udibo/oauth2/testing/contract";
|
|
13
|
+
* import type { OtpStore } from "@udibo/oauth2/identity";
|
|
14
|
+
*
|
|
15
|
+
* declare function freshOtpStore(): Promise<OtpStore>;
|
|
16
|
+
*
|
|
17
|
+
* runOtpStoreContractTests({
|
|
18
|
+
* describeName: "DrizzleOtpStore satisfies OtpStore contract",
|
|
19
|
+
* makeStore: freshOtpStore,
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import { assert, assertEquals, assertStrictEquals } from "../../deps/jsr.io/@std/assert/1.0.19/mod.js";
|
|
26
|
+
import { beforeEach, describe, it } from "../../deps/jsr.io/@std/testing/1.0.20/bdd.js";
|
|
27
|
+
const EMAIL = "user@example.com";
|
|
28
|
+
const OTHER_EMAIL = "other@example.com";
|
|
29
|
+
const PURPOSE = "signin";
|
|
30
|
+
const OTHER_PURPOSE = "step-up";
|
|
31
|
+
let sequence = 0;
|
|
32
|
+
function record(overrides = {}) {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
return {
|
|
35
|
+
id: `otp-${++sequence}`,
|
|
36
|
+
email: EMAIL,
|
|
37
|
+
purpose: PURPOSE,
|
|
38
|
+
codeHash: "hash",
|
|
39
|
+
expiresAt: now + 600_000,
|
|
40
|
+
attempts: 0,
|
|
41
|
+
maxAttempts: 5,
|
|
42
|
+
createdAt: now,
|
|
43
|
+
...overrides,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Runs the {@link OtpStore} contract suite. Call it from your own test file —
|
|
48
|
+
* it registers `describe` / `it` blocks the test runner picks up.
|
|
49
|
+
*
|
|
50
|
+
* The concurrency case fires overlapping `recordAttempt` calls and requires
|
|
51
|
+
* each to see a distinct count; a store that reads, awaits, then writes hands
|
|
52
|
+
* the same count to every guess and fails it.
|
|
53
|
+
*/
|
|
54
|
+
export function runOtpStoreContractTests(options) {
|
|
55
|
+
describe(options.describeName ?? "OtpStore contract", () => {
|
|
56
|
+
let store;
|
|
57
|
+
beforeEach(async () => {
|
|
58
|
+
store = await options.makeStore();
|
|
59
|
+
});
|
|
60
|
+
describe("create / findActive", () => {
|
|
61
|
+
it("returns null when the pair has no code", async () => {
|
|
62
|
+
assertStrictEquals(await store.findActive(EMAIL, PURPOSE), null);
|
|
63
|
+
});
|
|
64
|
+
it("round-trips every field of a created record", async () => {
|
|
65
|
+
const created = record({ codeHash: "hash-1", maxAttempts: 3 });
|
|
66
|
+
await store.create(created);
|
|
67
|
+
const found = await store.findActive(EMAIL, PURPOSE);
|
|
68
|
+
assert(found !== null, "a created record must be findable");
|
|
69
|
+
assertStrictEquals(found.id, created.id);
|
|
70
|
+
assertStrictEquals(found.email, created.email);
|
|
71
|
+
assertStrictEquals(found.purpose, created.purpose);
|
|
72
|
+
assertStrictEquals(found.codeHash, created.codeHash);
|
|
73
|
+
assertStrictEquals(found.expiresAt, created.expiresAt);
|
|
74
|
+
assertStrictEquals(found.attempts, 0);
|
|
75
|
+
assertStrictEquals(found.maxAttempts, 3);
|
|
76
|
+
});
|
|
77
|
+
it("returns the most recently created active record", async () => {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const older = record({ createdAt: now - 1000, codeHash: "old" });
|
|
80
|
+
const newer = record({ createdAt: now, codeHash: "new" });
|
|
81
|
+
await store.create(older);
|
|
82
|
+
await store.create(newer);
|
|
83
|
+
assertStrictEquals((await store.findActive(EMAIL, PURPOSE))?.id, newer.id, "a re-send supersedes the code before it");
|
|
84
|
+
});
|
|
85
|
+
it("returns an expired record rather than hiding it", async () => {
|
|
86
|
+
await store.create(record({ expiresAt: Date.now() - 1000 }));
|
|
87
|
+
assert(await store.findActive(EMAIL, PURPOSE) !== null, "expiry is the service's call — it reports `expired` distinctly " +
|
|
88
|
+
"from `invalid`, which it cannot do if the store swallows the row");
|
|
89
|
+
});
|
|
90
|
+
it("scopes lookups by email and by purpose", async () => {
|
|
91
|
+
await store.create(record());
|
|
92
|
+
assertStrictEquals(await store.findActive(OTHER_EMAIL, PURPOSE), null);
|
|
93
|
+
assertStrictEquals(await store.findActive(EMAIL, OTHER_PURPOSE), null);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe("consume", () => {
|
|
97
|
+
it("claims an active code only once under concurrency", async () => {
|
|
98
|
+
const created = record();
|
|
99
|
+
await store.create(created);
|
|
100
|
+
const results = await Promise.all([
|
|
101
|
+
store.consume(created.id),
|
|
102
|
+
store.consume(created.id),
|
|
103
|
+
]);
|
|
104
|
+
assertEquals(results.sort(), [false, true]);
|
|
105
|
+
assertStrictEquals(await store.consume("missing-code"), false);
|
|
106
|
+
});
|
|
107
|
+
it("takes the record out of the active set", async () => {
|
|
108
|
+
const created = record();
|
|
109
|
+
await store.create(created);
|
|
110
|
+
await store.consume(created.id);
|
|
111
|
+
assertStrictEquals(await store.findActive(EMAIL, PURPOSE), null);
|
|
112
|
+
});
|
|
113
|
+
it("leaves an older active code findable", async () => {
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
const older = record({ createdAt: now - 1000 });
|
|
116
|
+
const newer = record({ createdAt: now });
|
|
117
|
+
await store.create(older);
|
|
118
|
+
await store.create(newer);
|
|
119
|
+
await store.consume(newer.id);
|
|
120
|
+
assertStrictEquals((await store.findActive(EMAIL, PURPOSE))?.id, older.id);
|
|
121
|
+
});
|
|
122
|
+
it("is a no-op for an unknown id", async () => {
|
|
123
|
+
const created = record();
|
|
124
|
+
await store.create(created);
|
|
125
|
+
await store.consume("no-such-id");
|
|
126
|
+
assertStrictEquals((await store.findActive(EMAIL, PURPOSE))?.id, created.id);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe("invalidateById", () => {
|
|
130
|
+
it("deactivates only the named record", async () => {
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const undelivered = record({ createdAt: now });
|
|
133
|
+
const previous = record({ createdAt: now - 1000 });
|
|
134
|
+
await store.create(previous);
|
|
135
|
+
await store.create(undelivered);
|
|
136
|
+
await store.invalidateById(undelivered.id);
|
|
137
|
+
assertStrictEquals((await store.findActive(EMAIL, PURPOSE))?.id, previous.id, "discarding an undeliverable code must not take a concurrent " +
|
|
138
|
+
"request's code with it");
|
|
139
|
+
});
|
|
140
|
+
it("is a no-op for an unknown id", async () => {
|
|
141
|
+
const created = record();
|
|
142
|
+
await store.create(created);
|
|
143
|
+
await store.invalidateById("no-such-id");
|
|
144
|
+
assertStrictEquals((await store.findActive(EMAIL, PURPOSE))?.id, created.id);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe("invalidate", () => {
|
|
148
|
+
it("deactivates every active code for the pair", async () => {
|
|
149
|
+
await store.create(record({ createdAt: Date.now() - 1000 }));
|
|
150
|
+
await store.create(record());
|
|
151
|
+
await store.invalidate(EMAIL, PURPOSE);
|
|
152
|
+
assertStrictEquals(await store.findActive(EMAIL, PURPOSE), null);
|
|
153
|
+
});
|
|
154
|
+
it("leaves other emails and other purposes alone", async () => {
|
|
155
|
+
await store.create(record());
|
|
156
|
+
await store.create(record({ email: OTHER_EMAIL }));
|
|
157
|
+
await store.create(record({ purpose: OTHER_PURPOSE }));
|
|
158
|
+
await store.invalidate(EMAIL, PURPOSE);
|
|
159
|
+
assert(await store.findActive(OTHER_EMAIL, PURPOSE) !== null);
|
|
160
|
+
assert(await store.findActive(EMAIL, OTHER_PURPOSE) !== null);
|
|
161
|
+
});
|
|
162
|
+
it("is a no-op when the pair has no active code", async () => {
|
|
163
|
+
await store.invalidate(EMAIL, PURPOSE);
|
|
164
|
+
assertStrictEquals(await store.findActive(EMAIL, PURPOSE), null);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe("recordAttempt", () => {
|
|
168
|
+
it("returns the count after the increment", async () => {
|
|
169
|
+
const created = record();
|
|
170
|
+
await store.create(created);
|
|
171
|
+
assertStrictEquals(await store.recordAttempt(created.id), 1);
|
|
172
|
+
assertStrictEquals(await store.recordAttempt(created.id), 2);
|
|
173
|
+
assertStrictEquals(await store.recordAttempt(created.id), 3);
|
|
174
|
+
});
|
|
175
|
+
it("returns 0 for an unknown id", async () => {
|
|
176
|
+
assertStrictEquals(await store.recordAttempt("no-such-id"), 0);
|
|
177
|
+
});
|
|
178
|
+
it("counts each record separately", async () => {
|
|
179
|
+
const mine = record();
|
|
180
|
+
const theirs = record({ email: OTHER_EMAIL });
|
|
181
|
+
await store.create(mine);
|
|
182
|
+
await store.create(theirs);
|
|
183
|
+
await store.recordAttempt(mine.id);
|
|
184
|
+
await store.recordAttempt(mine.id);
|
|
185
|
+
assertStrictEquals(await store.recordAttempt(theirs.id), 1);
|
|
186
|
+
});
|
|
187
|
+
it("gives every concurrent guess its own count", async () => {
|
|
188
|
+
const created = record({ maxAttempts: 5 });
|
|
189
|
+
await store.create(created);
|
|
190
|
+
const counts = await Promise.all(Array.from({ length: 5 }, () => store.recordAttempt(created.id)));
|
|
191
|
+
assertEquals([...counts].sort((a, b) => a - b), [1, 2, 3, 4, 5], "the guess budget is spent by the returned count — concurrent " +
|
|
192
|
+
"guesses sharing one count spend the budget once and let the " +
|
|
193
|
+
"rest through");
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test suite for {@link RateLimitStore} implementations — the shared
|
|
3
|
+
* counter behind `RateLimiter`.
|
|
4
|
+
*
|
|
5
|
+
* The limiter allows or denies on the count `increment` returns, so a store
|
|
6
|
+
* that lets two concurrent attempts read the same count raises the effective
|
|
7
|
+
* threshold by exactly the burst the limiter exists to catch. That is the case
|
|
8
|
+
* this suite exists for.
|
|
9
|
+
*
|
|
10
|
+
* @example Verify a Redis-backed store
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { runRateLimitStoreContractTests } from "@udibo/oauth2/testing/contract";
|
|
13
|
+
* import type { RateLimitStore } from "@udibo/oauth2/identity";
|
|
14
|
+
*
|
|
15
|
+
* declare function freshRateLimitStore(): Promise<RateLimitStore>;
|
|
16
|
+
*
|
|
17
|
+
* runRateLimitStoreContractTests({
|
|
18
|
+
* describeName: "RedisRateLimitStore satisfies RateLimitStore contract",
|
|
19
|
+
* makeStore: freshRateLimitStore,
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import type { RateLimitStore } from "../../identity/rate-limit.js";
|
|
26
|
+
/** Options for {@link runRateLimitStoreContractTests}. */
|
|
27
|
+
export interface RateLimitStoreContractOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Returns a fresh, empty store for each test. Isolation is required — a
|
|
30
|
+
* counter surviving into the next test shifts every count the suite asserts.
|
|
31
|
+
*/
|
|
32
|
+
makeStore(): Promise<RateLimitStore> | RateLimitStore;
|
|
33
|
+
/**
|
|
34
|
+
* How far `resetAt` may sit from `now + windowMs`, in ms — headroom for a
|
|
35
|
+
* store whose backend rounds a TTL (Redis rounds to whole seconds). Defaults
|
|
36
|
+
* to 1000.
|
|
37
|
+
*/
|
|
38
|
+
resetAtToleranceMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Overrides the name passed to the outer `describe` block. Defaults to
|
|
41
|
+
* `"RateLimitStore contract"`.
|
|
42
|
+
*/
|
|
43
|
+
describeName?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Runs the {@link RateLimitStore} contract suite. Call it from your own test
|
|
47
|
+
* file — it registers `describe` / `it` blocks the test runner picks up.
|
|
48
|
+
*
|
|
49
|
+
* `now` is passed explicitly on every call, so the window cases are clock-free
|
|
50
|
+
* and never need a timer.
|
|
51
|
+
*/
|
|
52
|
+
export declare function runRateLimitStoreContractTests(options: RateLimitStoreContractOptions): void;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test suite for {@link RateLimitStore} implementations — the shared
|
|
3
|
+
* counter behind `RateLimiter`.
|
|
4
|
+
*
|
|
5
|
+
* The limiter allows or denies on the count `increment` returns, so a store
|
|
6
|
+
* that lets two concurrent attempts read the same count raises the effective
|
|
7
|
+
* threshold by exactly the burst the limiter exists to catch. That is the case
|
|
8
|
+
* this suite exists for.
|
|
9
|
+
*
|
|
10
|
+
* @example Verify a Redis-backed store
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { runRateLimitStoreContractTests } from "@udibo/oauth2/testing/contract";
|
|
13
|
+
* import type { RateLimitStore } from "@udibo/oauth2/identity";
|
|
14
|
+
*
|
|
15
|
+
* declare function freshRateLimitStore(): Promise<RateLimitStore>;
|
|
16
|
+
*
|
|
17
|
+
* runRateLimitStoreContractTests({
|
|
18
|
+
* describeName: "RedisRateLimitStore satisfies RateLimitStore contract",
|
|
19
|
+
* makeStore: freshRateLimitStore,
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import { assert, assertAlmostEquals, assertEquals } from "../../deps/jsr.io/@std/assert/1.0.19/mod.js";
|
|
26
|
+
import { beforeEach, describe, it } from "../../deps/jsr.io/@std/testing/1.0.20/bdd.js";
|
|
27
|
+
const KEY = "signin:1.2.3.4";
|
|
28
|
+
const OTHER_KEY = "signin:5.6.7.8";
|
|
29
|
+
const WINDOW_MS = 60_000;
|
|
30
|
+
/**
|
|
31
|
+
* Runs the {@link RateLimitStore} contract suite. Call it from your own test
|
|
32
|
+
* file — it registers `describe` / `it` blocks the test runner picks up.
|
|
33
|
+
*
|
|
34
|
+
* `now` is passed explicitly on every call, so the window cases are clock-free
|
|
35
|
+
* and never need a timer.
|
|
36
|
+
*/
|
|
37
|
+
export function runRateLimitStoreContractTests(options) {
|
|
38
|
+
const tolerance = options.resetAtToleranceMs ?? 1000;
|
|
39
|
+
describe(options.describeName ?? "RateLimitStore contract", () => {
|
|
40
|
+
let store;
|
|
41
|
+
let now;
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
store = await options.makeStore();
|
|
44
|
+
now = Date.now();
|
|
45
|
+
});
|
|
46
|
+
describe("increment", () => {
|
|
47
|
+
it("opens a window at count 1 that ends one window from now", async () => {
|
|
48
|
+
const result = await store.increment(KEY, WINDOW_MS, now);
|
|
49
|
+
assertEquals(result.count, 1);
|
|
50
|
+
assertAlmostEquals(result.resetAt, now + WINDOW_MS, tolerance);
|
|
51
|
+
});
|
|
52
|
+
it("counts up within the window without moving its end", async () => {
|
|
53
|
+
const first = await store.increment(KEY, WINDOW_MS, now);
|
|
54
|
+
const second = await store.increment(KEY, WINDOW_MS, now + 10);
|
|
55
|
+
const third = await store.increment(KEY, WINDOW_MS, now + 20);
|
|
56
|
+
assertEquals([second.count, third.count], [2, 3]);
|
|
57
|
+
assertEquals([second.resetAt, third.resetAt], [first.resetAt, first.resetAt], "a fixed window ends when it ends — hits inside it must not push " +
|
|
58
|
+
"the reset out");
|
|
59
|
+
});
|
|
60
|
+
it("opens a fresh window once the previous one has lapsed", async () => {
|
|
61
|
+
const first = await store.increment(KEY, WINDOW_MS, now);
|
|
62
|
+
const after = first.resetAt + 1;
|
|
63
|
+
const result = await store.increment(KEY, WINDOW_MS, after);
|
|
64
|
+
assertEquals(result.count, 1);
|
|
65
|
+
assertAlmostEquals(result.resetAt, after + WINDOW_MS, tolerance);
|
|
66
|
+
});
|
|
67
|
+
it("counts each key on its own", async () => {
|
|
68
|
+
await store.increment(KEY, WINDOW_MS, now);
|
|
69
|
+
await store.increment(KEY, WINDOW_MS, now);
|
|
70
|
+
const other = await store.increment(OTHER_KEY, WINDOW_MS, now);
|
|
71
|
+
assertEquals(other.count, 1);
|
|
72
|
+
});
|
|
73
|
+
it("gives every concurrent hit its own count", async () => {
|
|
74
|
+
const results = await Promise.all(Array.from({ length: 5 }, () => store.increment(KEY, WINDOW_MS, now)));
|
|
75
|
+
assertEquals(results.map((result) => result.count).sort((a, b) => a - b), [1, 2, 3, 4, 5], "the limiter decides on the returned count — concurrent hits " +
|
|
76
|
+
"sharing a count are exactly the burst that slips past the " +
|
|
77
|
+
"threshold");
|
|
78
|
+
const next = await store.increment(KEY, WINDOW_MS, now);
|
|
79
|
+
assertEquals(next.count, 6, "no hit may be lost to a stale read");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe("reset", () => {
|
|
83
|
+
it("sends the key back to a fresh window", async () => {
|
|
84
|
+
await store.increment(KEY, WINDOW_MS, now);
|
|
85
|
+
await store.increment(KEY, WINDOW_MS, now);
|
|
86
|
+
await store.reset(KEY);
|
|
87
|
+
const result = await store.increment(KEY, WINDOW_MS, now);
|
|
88
|
+
assertEquals(result.count, 1);
|
|
89
|
+
assertAlmostEquals(result.resetAt, now + WINDOW_MS, tolerance);
|
|
90
|
+
});
|
|
91
|
+
it("clears only the key it is given", async () => {
|
|
92
|
+
await store.increment(KEY, WINDOW_MS, now);
|
|
93
|
+
await store.increment(OTHER_KEY, WINDOW_MS, now);
|
|
94
|
+
await store.reset(KEY);
|
|
95
|
+
const other = await store.increment(OTHER_KEY, WINDOW_MS, now);
|
|
96
|
+
assertEquals(other.count, 2);
|
|
97
|
+
});
|
|
98
|
+
it("is a no-op for a key with no counter", async () => {
|
|
99
|
+
await store.reset("never-seen");
|
|
100
|
+
const result = await store.increment("never-seen", WINDOW_MS, now);
|
|
101
|
+
assert(result.count === 1);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test suite for {@link TokenFlowStore} implementations — the storage
|
|
3
|
+
* behind verification links, password-reset links, and magic links.
|
|
4
|
+
*
|
|
5
|
+
* The load-bearing method is `markConsumed`: it is what makes a link
|
|
6
|
+
* single-use, and only a conditional write survives two requests redeeming one
|
|
7
|
+
* link at the same time. This suite issues those two requests.
|
|
8
|
+
*
|
|
9
|
+
* @example Verify a database-backed store
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { runTokenFlowStoreContractTests } from "@udibo/oauth2/testing/contract";
|
|
12
|
+
* import type { TokenFlowStore } from "@udibo/oauth2/identity";
|
|
13
|
+
*
|
|
14
|
+
* declare function freshTokenFlowStore(): Promise<TokenFlowStore>;
|
|
15
|
+
*
|
|
16
|
+
* runTokenFlowStoreContractTests({
|
|
17
|
+
* describeName: "DrizzleTokenFlowStore satisfies TokenFlowStore contract",
|
|
18
|
+
* makeStore: freshTokenFlowStore,
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
import type { TokenFlowStore } from "../../identity/token-flow.js";
|
|
25
|
+
/** Options for {@link runTokenFlowStoreContractTests}. */
|
|
26
|
+
export interface TokenFlowStoreContractOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Returns a fresh, empty store for each test. Isolation is required — a
|
|
29
|
+
* record surviving into the next test makes the single-use checks
|
|
30
|
+
* meaningless.
|
|
31
|
+
*/
|
|
32
|
+
makeStore(): Promise<TokenFlowStore> | TokenFlowStore;
|
|
33
|
+
/**
|
|
34
|
+
* Set to `false` when the store deliberately omits the optional
|
|
35
|
+
* {@link TokenFlowStore.deleteBySubject}. The suite then registers an ignored
|
|
36
|
+
* case naming what is not covered — outstanding links stay redeemable until
|
|
37
|
+
* they expire — instead of passing in silence. Defaults to `true`, so a store
|
|
38
|
+
* that meant to implement it and does not fails loudly.
|
|
39
|
+
*/
|
|
40
|
+
deleteBySubject?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Overrides the name passed to the outer `describe` block. Defaults to
|
|
43
|
+
* `"TokenFlowStore contract"`.
|
|
44
|
+
*/
|
|
45
|
+
describeName?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Runs the {@link TokenFlowStore} contract suite. Call it from your own test
|
|
49
|
+
* file — it registers `describe` / `it` blocks the test runner picks up.
|
|
50
|
+
*
|
|
51
|
+
* The concurrency cases redeem one token from two callers at once; a store that
|
|
52
|
+
* reads, awaits, then writes hands the token to both and fails them.
|
|
53
|
+
*/
|
|
54
|
+
export declare function runTokenFlowStoreContractTests(options: TokenFlowStoreContractOptions): void;
|