@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,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity self-service primitives for apps that own their login.
|
|
3
|
+
*
|
|
4
|
+
* The OAuth2/BFF machinery handles tokens and sessions; this layer covers the
|
|
5
|
+
* "own-auth" flows an app otherwise hand-rolls. It is **optional** and
|
|
6
|
+
* composable — own the flows and the token lifecycle, keep storage as thin,
|
|
7
|
+
* app-owned seams (the `SessionStore` philosophy), never a DB-adapter zoo.
|
|
8
|
+
*
|
|
9
|
+
* - {@link IdentityService} — orchestrates the primitives below into sign-up,
|
|
10
|
+
* sign-in, password-reset, and email-verification flows you call from your
|
|
11
|
+
* own routes (storage stays an app-owned {@link IdentityUserStore}).
|
|
12
|
+
* - {@link PasswordIdentityService} — PBKDF2 password hashing + constant-time
|
|
13
|
+
* verify (no more hand-rolled crypto); implement {@link PasswordHasherLike}
|
|
14
|
+
* instead to plug in argon2 or scrypt.
|
|
15
|
+
* - {@link TokenFlowService} — single-use, hashed-at-rest verify/reset/sign-in
|
|
16
|
+
* tokens over an app-owned {@link TokenFlowStore} (magic links reuse it via
|
|
17
|
+
* {@link IdentityService.requestSignInLink}).
|
|
18
|
+
* - {@link EmailOtpService} — short numeric one-time codes over an app-owned
|
|
19
|
+
* {@link OtpStore}: hashed at rest, attempt-capped (guessable-online threat
|
|
20
|
+
* model), purpose-generic so it doubles as a step-up factor. Wire it into
|
|
21
|
+
* {@link IdentityService} via the `otp` option for "email me a code"
|
|
22
|
+
* sign-in.
|
|
23
|
+
* - {@link createIdentifierResolver} / {@link classifyIdentifier} — accept
|
|
24
|
+
* "email or username (or phone)" on a sign-in form without the bespoke branch.
|
|
25
|
+
* - {@link DeliveryHooks} + URL builders — the seam your mailer/SMS plugs into.
|
|
26
|
+
* - {@link RevocableSessionService} / {@link ListableSessionService} /
|
|
27
|
+
* {@link isRecentlyAuthenticated} — session revocation on credential change,
|
|
28
|
+
* the optional listing half ({@link SessionSummary} rows for a "where you're
|
|
29
|
+
* signed in" screen), and step-up checks.
|
|
30
|
+
* - {@link RateLimiter} — opt-in throttling (wire it into
|
|
31
|
+
* {@link IdentityService}); {@link checkPasswordPolicy} — the password rules
|
|
32
|
+
* {@link IdentityService} already applies to `signUp`/`resetPassword`, at
|
|
33
|
+
* their 8–256 character defaults unless you pass a tighter `passwordPolicy`.
|
|
34
|
+
* - {@link CaptchaProvider} + {@link verifyCaptcha} — the bring-your-own
|
|
35
|
+
* bot-challenge seam (Turnstile/hCaptcha/reCAPTCHA), with fail-open/closed
|
|
36
|
+
* handling; no provider means no challenge.
|
|
37
|
+
* - {@link AccountLockout} — failed-attempt lockout with a self-service unlock
|
|
38
|
+
* flow ({@link IdentityService.unlockAccount}) and a log-only rollout mode
|
|
39
|
+
* via `protectionMode`.
|
|
40
|
+
* - {@link IdentityEvent} + {@link IdentityEventHook} — auth-outcome events for
|
|
41
|
+
* audit capture (wire via {@link IdentityService}'s `onEvent`).
|
|
42
|
+
* - {@link IdentityError} — a stable error contract a front-end can switch on.
|
|
43
|
+
* - Importing users from another auth system without a password reset
|
|
44
|
+
* (upgrade-on-login) lives on its own subpath,
|
|
45
|
+
* `@udibo/oauth2/identity/migration`: the `LegacyPasswordVerifier` seam, the
|
|
46
|
+
* built-in `pbkdf2Verifier`, and `parsePhc`. Wire them through this module's
|
|
47
|
+
* {@link IdentityServiceOptions.legacyVerifiers}.
|
|
48
|
+
*
|
|
49
|
+
* @module
|
|
50
|
+
*/
|
|
51
|
+
import "../_dnt.polyfills.js";
|
|
52
|
+
export { DEFAULT_PBKDF2_ITERATIONS, generateSalt, hashPassword, LEGACY_PBKDF2_ITERATIONS, PasswordIdentityService, PBKDF2_SHA256, verifyPassword, } from "./password.js";
|
|
53
|
+
export { classifyIdentifier, createIdentifierResolver, } from "./identifier.js";
|
|
54
|
+
export { MemoryTokenFlowStore, TokenFlowService, TokenPurpose, } from "./token-flow.js";
|
|
55
|
+
export { buildResetUrl, buildSignInUrl, buildTokenUrl, buildUnlockUrl, buildVerificationUrl, } from "./delivery.js";
|
|
56
|
+
export { isRecentlyAuthenticated, supportsSessionListing, } from "./session.js";
|
|
57
|
+
export { IdentityService, } from "./service.js";
|
|
58
|
+
export { EmailOtpService, MemoryOtpStore, } from "./otp.js";
|
|
59
|
+
export { AccountLockout, MemoryLockoutStore, } from "./lockout.js";
|
|
60
|
+
export { IdentityError, identityErrorStatus, isIdentityError, } from "./errors.js";
|
|
61
|
+
export { MemoryRateLimitStore, RateLimiter, } from "./rate-limit.js";
|
|
62
|
+
export { verifyCaptcha, } from "./captcha.js";
|
|
63
|
+
export { assertPasswordPolicy, checkPasswordPolicy, } from "./password-policy.js";
|
|
64
|
+
export { breachedPasswordValidator, } from "./hibp.js";
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email one-time-code primitives — the "email me a code" half of passwordless
|
|
3
|
+
* sign-in, and a reusable step-up factor.
|
|
4
|
+
*
|
|
5
|
+
* A short numeric code has a different threat model than a URL token: it is
|
|
6
|
+
* **guessable online** (a 6-digit code is one in a million), so unlike
|
|
7
|
+
* {@link TokenFlowService} tokens every code carries an attempt budget — wrong
|
|
8
|
+
* tries are counted and the code is invalidated (`locked`) when the budget is
|
|
9
|
+
* spent. Codes are stored **hashed** (SHA-256, domain-bound to the email +
|
|
10
|
+
* purpose) and verified with a constant-time compare; the raw code exists only
|
|
11
|
+
* in the delivery callback.
|
|
12
|
+
*
|
|
13
|
+
* Storage is an app-owned {@link OtpStore} (a {@link MemoryOtpStore} ships for
|
|
14
|
+
* dev/tests) and delivery is a caller-supplied callback — the package owns the
|
|
15
|
+
* lifecycle, never your tables or your mailer. `purpose` is a free string, so
|
|
16
|
+
* the same service covers sign-in codes today and step-up / verification codes
|
|
17
|
+
* later.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
import { type RateLimiterLike } from "./rate-limit.js";
|
|
22
|
+
/**
|
|
23
|
+
* The rate-limit key a one-time-code request is throttled under. Exported so
|
|
24
|
+
* `IdentityService` throttles its sign-in-code flow under the very key
|
|
25
|
+
* {@link EmailOtpService} would, instead of re-typing the format and drifting
|
|
26
|
+
* into two windows for one operation.
|
|
27
|
+
*
|
|
28
|
+
* The email is case-folded, matching how {@link EmailOtpService} folds at its
|
|
29
|
+
* entry points, so casing variants of one mailbox share a window rather than
|
|
30
|
+
* each earning a fresh send budget.
|
|
31
|
+
*/
|
|
32
|
+
export declare function otpRateKey(purpose: string, email: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* A stored one-time-code record. Holds the code's **hash**, never the raw
|
|
35
|
+
* code — a leaked record can't be replayed, and a 6-digit hash space is only
|
|
36
|
+
* brute-forceable while the record is live and has attempts left.
|
|
37
|
+
*/
|
|
38
|
+
export interface OtpRecord {
|
|
39
|
+
/** Opaque unique id (the store's key for attempt/consume updates). */
|
|
40
|
+
id: string;
|
|
41
|
+
/** The email the code was issued to, case-folded by the service. */
|
|
42
|
+
email: string;
|
|
43
|
+
/** What the code authorizes (e.g. `"signin"`). */
|
|
44
|
+
purpose: string;
|
|
45
|
+
/** Hash of the raw code (opaque to the store; the service computes it). */
|
|
46
|
+
codeHash: string;
|
|
47
|
+
/** Expiry, epoch ms. */
|
|
48
|
+
expiresAt: number;
|
|
49
|
+
/** Wrong verification attempts so far. */
|
|
50
|
+
attempts: number;
|
|
51
|
+
/** Attempt budget; reaching it invalidates the code. */
|
|
52
|
+
maxAttempts: number;
|
|
53
|
+
/** Created, epoch ms. */
|
|
54
|
+
createdAt: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* App-owned persistence for {@link EmailOtpService}. "Active" means neither
|
|
58
|
+
* consumed nor invalidated; expiry stays the service's concern so it can report
|
|
59
|
+
* `expired` distinctly. Back it with your database in production.
|
|
60
|
+
*/
|
|
61
|
+
export interface OtpStore {
|
|
62
|
+
/** Persist a new active record. */
|
|
63
|
+
create(record: OtpRecord): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* The most recently created active record for `(email, purpose)`, or `null`.
|
|
66
|
+
* Must still return an expired record — the service reports expiry itself.
|
|
67
|
+
*/
|
|
68
|
+
findActive(email: string, purpose: string): Promise<OtpRecord | null>;
|
|
69
|
+
/**
|
|
70
|
+
* Atomically increment a record's attempt count and return the new count
|
|
71
|
+
* (0 if unknown). Must be a single atomic operation (e.g.
|
|
72
|
+
* `UPDATE … SET attempts = attempts + 1 RETURNING attempts`), never a
|
|
73
|
+
* read-then-write: {@link EmailOtpService.verify} gates the guess budget on
|
|
74
|
+
* the returned value, so a non-atomic implementation lets concurrent
|
|
75
|
+
* verifies bypass `maxAttempts`.
|
|
76
|
+
*/
|
|
77
|
+
recordAttempt(id: string): Promise<number>;
|
|
78
|
+
/**
|
|
79
|
+
* Atomically consume an active code. Return `true` only to the caller that
|
|
80
|
+
* removed it from the active set; concurrent or repeated claims return
|
|
81
|
+
* `false`. Use a conditional delete/update with RETURNING in persistent stores.
|
|
82
|
+
*/
|
|
83
|
+
consume(id: string): Promise<boolean>;
|
|
84
|
+
/**
|
|
85
|
+
* Deactivate the one record with this id, for a code that was never usable —
|
|
86
|
+
* the send carrying it failed. Separate from {@link OtpStore.consume} so a
|
|
87
|
+
* store that records *why* a code left the active set can tell a redeemed
|
|
88
|
+
* code from a discarded one, and from {@link OtpStore.invalidate} so a
|
|
89
|
+
* concurrent request's newer code survives. A no-op for an unknown id.
|
|
90
|
+
*/
|
|
91
|
+
invalidateById(id: string): Promise<void>;
|
|
92
|
+
/** Deactivate every active record for `(email, purpose)`. */
|
|
93
|
+
invalidate(email: string, purpose: string): Promise<void>;
|
|
94
|
+
}
|
|
95
|
+
/** Options for {@link EmailOtpService}. */
|
|
96
|
+
export interface EmailOtpServiceOptions {
|
|
97
|
+
/** App-owned code storage. */
|
|
98
|
+
store: OtpStore;
|
|
99
|
+
/** Code length in digits. Defaults to `6`. */
|
|
100
|
+
digits?: number;
|
|
101
|
+
/** Code lifetime in ms. Defaults to 10 minutes. */
|
|
102
|
+
ttlMs?: number;
|
|
103
|
+
/** Wrong-attempt budget before the code is invalidated. Defaults to `5`. */
|
|
104
|
+
maxAttempts?: number;
|
|
105
|
+
/**
|
|
106
|
+
* Throttles {@link EmailOtpService.request} per `(purpose, email)` (key
|
|
107
|
+
* {@link otpRateKey}, which case-folds the email so casing variants of one
|
|
108
|
+
* mailbox cannot each earn a fresh send budget). When exceeded and
|
|
109
|
+
* `protectionMode` is `"enforce"`, `request` throws
|
|
110
|
+
* {@link IdentityError} `rate_limited`. A limiter whose
|
|
111
|
+
* `check` itself throws is trapped the way every identity flow traps it:
|
|
112
|
+
* under `"enforce"` it fails closed as the same `rate_limited` error, and
|
|
113
|
+
* under `"log-only"` the attempt proceeds. For direct use of this service;
|
|
114
|
+
* `IdentityService` never sets it, because it throttles the sign-in-code flow
|
|
115
|
+
* itself and a second limiter reachable only on the known-email path would be
|
|
116
|
+
* an enumeration oracle.
|
|
117
|
+
*/
|
|
118
|
+
rateLimiter?: RateLimiterLike;
|
|
119
|
+
/**
|
|
120
|
+
* `"enforce"` (default) blocks a rate-limited request; `"log-only"` lets it
|
|
121
|
+
* proceed so adopters can observe thresholds before turning enforcement on.
|
|
122
|
+
*/
|
|
123
|
+
protectionMode?: "enforce" | "log-only";
|
|
124
|
+
}
|
|
125
|
+
/** Options for {@link EmailOtpService.request}. */
|
|
126
|
+
export interface RequestOtpOptions {
|
|
127
|
+
/** The email the code is issued to. Case-folded, so casing need not match later. */
|
|
128
|
+
email: string;
|
|
129
|
+
/** What the code authorizes (e.g. `"signin"`); free string. */
|
|
130
|
+
purpose: string;
|
|
131
|
+
/**
|
|
132
|
+
* Receives the raw code for delivery — the only place it ever exists. Send
|
|
133
|
+
* it via your transport; it is not stored or recoverable. `codeId` identifies
|
|
134
|
+
* the stored record, for {@link EmailOtpService.invalidateCode} when a send
|
|
135
|
+
* fails.
|
|
136
|
+
*/
|
|
137
|
+
onDeliver: (code: string, expiresAt: number, codeId: string) => Promise<void> | void;
|
|
138
|
+
}
|
|
139
|
+
/** What {@link EmailOtpService.request} returns after storing + delivering. */
|
|
140
|
+
export interface RequestedOtp {
|
|
141
|
+
/** When the code expires, epoch ms. */
|
|
142
|
+
expiresAt: number;
|
|
143
|
+
/**
|
|
144
|
+
* The stored record's id — pass it to
|
|
145
|
+
* {@link EmailOtpService.invalidateCode} to drop exactly this code.
|
|
146
|
+
*/
|
|
147
|
+
id: string;
|
|
148
|
+
}
|
|
149
|
+
/** Options for {@link EmailOtpService.verify}. */
|
|
150
|
+
export interface VerifyOtpOptions {
|
|
151
|
+
/** The email the code was issued to. Case-folded, so any casing matches. */
|
|
152
|
+
email: string;
|
|
153
|
+
/** The purpose the code was issued for. */
|
|
154
|
+
purpose: string;
|
|
155
|
+
/** The submitted code; whitespace is tolerated. */
|
|
156
|
+
code: string;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The outcome of {@link EmailOtpService.verify}. `invalid` covers both a wrong
|
|
160
|
+
* code and no active code — indistinguishable on purpose. `locked` is reported
|
|
161
|
+
* once, on the attempt that spends the budget; afterwards the code is gone and
|
|
162
|
+
* further tries report `invalid`.
|
|
163
|
+
*/
|
|
164
|
+
export type VerifyOtpResult = {
|
|
165
|
+
status: "success";
|
|
166
|
+
} | {
|
|
167
|
+
status: "invalid";
|
|
168
|
+
} | {
|
|
169
|
+
status: "expired";
|
|
170
|
+
} | {
|
|
171
|
+
status: "locked";
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Issues and verifies short-lived numeric one-time codes over an app-owned
|
|
175
|
+
* {@link OtpStore}. Purpose-generic: `"signin"` today, step-up or verification
|
|
176
|
+
* codes tomorrow — one instance can serve them all.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```ts
|
|
180
|
+
* const otp = new EmailOtpService({ store: new MemoryOtpStore() });
|
|
181
|
+
* await otp.request({
|
|
182
|
+
* email: user.email,
|
|
183
|
+
* purpose: "signin",
|
|
184
|
+
* onDeliver: (code) => mailer.send(user.email, `Your code: ${code}`),
|
|
185
|
+
* });
|
|
186
|
+
* // …the user types the code back in…
|
|
187
|
+
* const result = await otp.verify({ email: user.email, purpose: "signin", code });
|
|
188
|
+
* if (result.status === "success") await establishSession(user);
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
export declare class EmailOtpService {
|
|
192
|
+
#private;
|
|
193
|
+
/** Wires the app-owned store and applies the digit/TTL/attempt defaults. */
|
|
194
|
+
constructor(options: EmailOtpServiceOptions);
|
|
195
|
+
/**
|
|
196
|
+
* Mint a code for `(email, purpose)`: invalidates any prior active code,
|
|
197
|
+
* stores the new code's hash, and hands the raw code to `onDeliver`. Throws
|
|
198
|
+
* {@link IdentityError} `rate_limited` when a configured `rateLimiter` is
|
|
199
|
+
* exceeded — or is itself broken — in enforce mode; nothing is stored or
|
|
200
|
+
* delivered on that path.
|
|
201
|
+
*
|
|
202
|
+
* A throw from `onDeliver` propagates and the stored code stays active — so a
|
|
203
|
+
* failed send would otherwise leave a live, unsent code behind. Trap it and
|
|
204
|
+
* call {@link EmailOtpService.invalidateCode} with the `codeId` you were
|
|
205
|
+
* handed if that matters (which is what `IdentityService` does for its
|
|
206
|
+
* `sendSignInCode` hook).
|
|
207
|
+
*/
|
|
208
|
+
request(options: RequestOtpOptions): Promise<RequestedOtp>;
|
|
209
|
+
/**
|
|
210
|
+
* Drop **every** active code for `(email, purpose)` without verifying one —
|
|
211
|
+
* for an abandoned flow, or an email that no longer belongs to the account.
|
|
212
|
+
* A no-op when none is active.
|
|
213
|
+
*
|
|
214
|
+
* Not for a failed send: a slow failure would drop whatever code is active
|
|
215
|
+
* *now*, which may be a newer one a retry already delivered. Use
|
|
216
|
+
* {@link EmailOtpService.invalidateCode} there.
|
|
217
|
+
*/
|
|
218
|
+
invalidate(email: string, purpose: string): Promise<void>;
|
|
219
|
+
/**
|
|
220
|
+
* Drop exactly the code with this id — the `codeId` handed to `onDeliver`, or
|
|
221
|
+
* {@link RequestedOtp.id}. Scoped to the one record, so a code another
|
|
222
|
+
* request has since minted and delivered survives. A no-op for an unknown id.
|
|
223
|
+
*/
|
|
224
|
+
invalidateCode(id: string): Promise<void>;
|
|
225
|
+
/**
|
|
226
|
+
* Check a submitted code against the active record for `(email, purpose)`.
|
|
227
|
+
* The hash compare is constant-time and runs even when no active record
|
|
228
|
+
* exists, so a missing account or code is not distinguishable by timing. The
|
|
229
|
+
* attempt is reserved atomically — via the store's `recordAttempt` return
|
|
230
|
+
* value — **before** the compare, so concurrent verifies of one live code
|
|
231
|
+
* can't each test a guess against a stale pre-increment count: at most
|
|
232
|
+
* `maxAttempts` guesses ever reach the comparison. Spending the budget
|
|
233
|
+
* invalidates the code (`locked`); success consumes it (single-use).
|
|
234
|
+
*/
|
|
235
|
+
verify(options: VerifyOtpOptions): Promise<VerifyOtpResult>;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* In-memory {@link OtpStore} for development and tests. Records are lost on
|
|
239
|
+
* restart; back the contract with a database for production.
|
|
240
|
+
*/
|
|
241
|
+
export declare class MemoryOtpStore implements OtpStore {
|
|
242
|
+
#private;
|
|
243
|
+
/** Persist a record and mark it active. */
|
|
244
|
+
create(record: OtpRecord): Promise<void>;
|
|
245
|
+
/** The newest active record for `(email, purpose)`, expired or not; else `null`. */
|
|
246
|
+
findActive(email: string, purpose: string): Promise<OtpRecord | null>;
|
|
247
|
+
/** Bump a record's attempt count; returns the new count, or 0 if unknown. */
|
|
248
|
+
recordAttempt(id: string): Promise<number>;
|
|
249
|
+
/** Atomically deactivate one active record; return whether it was claimed. */
|
|
250
|
+
consume(id: string): Promise<boolean>;
|
|
251
|
+
/** Deactivate one undelivered record; a no-op if the id is unknown. */
|
|
252
|
+
invalidateById(id: string): Promise<void>;
|
|
253
|
+
/** Deactivate every active record for `(email, purpose)`. */
|
|
254
|
+
invalidate(email: string, purpose: string): Promise<void>;
|
|
255
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email one-time-code primitives — the "email me a code" half of passwordless
|
|
3
|
+
* sign-in, and a reusable step-up factor.
|
|
4
|
+
*
|
|
5
|
+
* A short numeric code has a different threat model than a URL token: it is
|
|
6
|
+
* **guessable online** (a 6-digit code is one in a million), so unlike
|
|
7
|
+
* {@link TokenFlowService} tokens every code carries an attempt budget — wrong
|
|
8
|
+
* tries are counted and the code is invalidated (`locked`) when the budget is
|
|
9
|
+
* spent. Codes are stored **hashed** (SHA-256, domain-bound to the email +
|
|
10
|
+
* purpose) and verified with a constant-time compare; the raw code exists only
|
|
11
|
+
* in the delivery callback.
|
|
12
|
+
*
|
|
13
|
+
* Storage is an app-owned {@link OtpStore} (a {@link MemoryOtpStore} ships for
|
|
14
|
+
* dev/tests) and delivery is a caller-supplied callback — the package owns the
|
|
15
|
+
* lifecycle, never your tables or your mailer. `purpose` is a free string, so
|
|
16
|
+
* the same service covers sign-in codes today and step-up / verification codes
|
|
17
|
+
* later.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
import { sha256Hash } from "../server/utils/hash.js";
|
|
22
|
+
import { timingSafeEqualString } from "../utils/crypto.js";
|
|
23
|
+
import { enforceRateLimit } from "./rate-limit.js";
|
|
24
|
+
function foldEmail(email) {
|
|
25
|
+
return email.trim().toLowerCase();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The rate-limit key a one-time-code request is throttled under. Exported so
|
|
29
|
+
* `IdentityService` throttles its sign-in-code flow under the very key
|
|
30
|
+
* {@link EmailOtpService} would, instead of re-typing the format and drifting
|
|
31
|
+
* into two windows for one operation.
|
|
32
|
+
*
|
|
33
|
+
* The email is case-folded, matching how {@link EmailOtpService} folds at its
|
|
34
|
+
* entry points, so casing variants of one mailbox share a window rather than
|
|
35
|
+
* each earning a fresh send budget.
|
|
36
|
+
*/
|
|
37
|
+
export function otpRateKey(purpose, email) {
|
|
38
|
+
return `otp:${purpose}:${foldEmail(email)}`;
|
|
39
|
+
}
|
|
40
|
+
function generateNumericCode(digits) {
|
|
41
|
+
if (!Number.isInteger(digits) || digits < 1) {
|
|
42
|
+
throw new RangeError("digits must be a positive integer");
|
|
43
|
+
}
|
|
44
|
+
let code = "";
|
|
45
|
+
while (code.length < digits) {
|
|
46
|
+
const bytes = crypto.getRandomValues(new Uint8Array(digits * 2));
|
|
47
|
+
for (const byte of bytes) {
|
|
48
|
+
if (byte < 250 && code.length < digits)
|
|
49
|
+
code += String(byte % 10);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return code;
|
|
53
|
+
}
|
|
54
|
+
function hashOtpCode(email, purpose, code) {
|
|
55
|
+
return sha256Hash(`${purpose}\n${email}\n${code}`);
|
|
56
|
+
}
|
|
57
|
+
const DEFAULT_DIGITS = 6;
|
|
58
|
+
const DEFAULT_TTL_MS = 10 * 60 * 1000;
|
|
59
|
+
const DEFAULT_MAX_ATTEMPTS = 5;
|
|
60
|
+
/**
|
|
61
|
+
* Issues and verifies short-lived numeric one-time codes over an app-owned
|
|
62
|
+
* {@link OtpStore}. Purpose-generic: `"signin"` today, step-up or verification
|
|
63
|
+
* codes tomorrow — one instance can serve them all.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* const otp = new EmailOtpService({ store: new MemoryOtpStore() });
|
|
68
|
+
* await otp.request({
|
|
69
|
+
* email: user.email,
|
|
70
|
+
* purpose: "signin",
|
|
71
|
+
* onDeliver: (code) => mailer.send(user.email, `Your code: ${code}`),
|
|
72
|
+
* });
|
|
73
|
+
* // …the user types the code back in…
|
|
74
|
+
* const result = await otp.verify({ email: user.email, purpose: "signin", code });
|
|
75
|
+
* if (result.status === "success") await establishSession(user);
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export class EmailOtpService {
|
|
79
|
+
#store;
|
|
80
|
+
#digits;
|
|
81
|
+
#ttlMs;
|
|
82
|
+
#maxAttempts;
|
|
83
|
+
#rateLimiter;
|
|
84
|
+
#enforce;
|
|
85
|
+
/** Wires the app-owned store and applies the digit/TTL/attempt defaults. */
|
|
86
|
+
constructor(options) {
|
|
87
|
+
this.#store = options.store;
|
|
88
|
+
this.#digits = options.digits ?? DEFAULT_DIGITS;
|
|
89
|
+
this.#ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
90
|
+
this.#maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
91
|
+
this.#rateLimiter = options.rateLimiter;
|
|
92
|
+
this.#enforce = (options.protectionMode ?? "enforce") === "enforce";
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Mint a code for `(email, purpose)`: invalidates any prior active code,
|
|
96
|
+
* stores the new code's hash, and hands the raw code to `onDeliver`. Throws
|
|
97
|
+
* {@link IdentityError} `rate_limited` when a configured `rateLimiter` is
|
|
98
|
+
* exceeded — or is itself broken — in enforce mode; nothing is stored or
|
|
99
|
+
* delivered on that path.
|
|
100
|
+
*
|
|
101
|
+
* A throw from `onDeliver` propagates and the stored code stays active — so a
|
|
102
|
+
* failed send would otherwise leave a live, unsent code behind. Trap it and
|
|
103
|
+
* call {@link EmailOtpService.invalidateCode} with the `codeId` you were
|
|
104
|
+
* handed if that matters (which is what `IdentityService` does for its
|
|
105
|
+
* `sendSignInCode` hook).
|
|
106
|
+
*/
|
|
107
|
+
async request(options) {
|
|
108
|
+
const { purpose } = options;
|
|
109
|
+
const email = foldEmail(options.email);
|
|
110
|
+
await enforceRateLimit({
|
|
111
|
+
rateLimiter: this.#rateLimiter,
|
|
112
|
+
key: otpRateKey(purpose, email),
|
|
113
|
+
message: "too many code requests",
|
|
114
|
+
enforce: this.#enforce,
|
|
115
|
+
});
|
|
116
|
+
await this.#store.invalidate(email, purpose);
|
|
117
|
+
const code = generateNumericCode(this.#digits);
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
const expiresAt = now + this.#ttlMs;
|
|
120
|
+
const id = crypto.randomUUID();
|
|
121
|
+
await this.#store.create({
|
|
122
|
+
id,
|
|
123
|
+
email,
|
|
124
|
+
purpose,
|
|
125
|
+
codeHash: await hashOtpCode(email, purpose, code),
|
|
126
|
+
expiresAt,
|
|
127
|
+
attempts: 0,
|
|
128
|
+
maxAttempts: this.#maxAttempts,
|
|
129
|
+
createdAt: now,
|
|
130
|
+
});
|
|
131
|
+
await options.onDeliver(code, expiresAt, id);
|
|
132
|
+
return { expiresAt, id };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Drop **every** active code for `(email, purpose)` without verifying one —
|
|
136
|
+
* for an abandoned flow, or an email that no longer belongs to the account.
|
|
137
|
+
* A no-op when none is active.
|
|
138
|
+
*
|
|
139
|
+
* Not for a failed send: a slow failure would drop whatever code is active
|
|
140
|
+
* *now*, which may be a newer one a retry already delivered. Use
|
|
141
|
+
* {@link EmailOtpService.invalidateCode} there.
|
|
142
|
+
*/
|
|
143
|
+
invalidate(email, purpose) {
|
|
144
|
+
return this.#store.invalidate(foldEmail(email), purpose);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Drop exactly the code with this id — the `codeId` handed to `onDeliver`, or
|
|
148
|
+
* {@link RequestedOtp.id}. Scoped to the one record, so a code another
|
|
149
|
+
* request has since minted and delivered survives. A no-op for an unknown id.
|
|
150
|
+
*/
|
|
151
|
+
invalidateCode(id) {
|
|
152
|
+
return this.#store.invalidateById(id);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Check a submitted code against the active record for `(email, purpose)`.
|
|
156
|
+
* The hash compare is constant-time and runs even when no active record
|
|
157
|
+
* exists, so a missing account or code is not distinguishable by timing. The
|
|
158
|
+
* attempt is reserved atomically — via the store's `recordAttempt` return
|
|
159
|
+
* value — **before** the compare, so concurrent verifies of one live code
|
|
160
|
+
* can't each test a guess against a stale pre-increment count: at most
|
|
161
|
+
* `maxAttempts` guesses ever reach the comparison. Spending the budget
|
|
162
|
+
* invalidates the code (`locked`); success consumes it (single-use).
|
|
163
|
+
*/
|
|
164
|
+
async verify(options) {
|
|
165
|
+
const { purpose } = options;
|
|
166
|
+
const email = foldEmail(options.email);
|
|
167
|
+
const code = options.code.replaceAll(/\s+/g, "");
|
|
168
|
+
const submittedHash = await hashOtpCode(email, purpose, code);
|
|
169
|
+
const record = await this.#store.findActive(email, purpose);
|
|
170
|
+
if (!record) {
|
|
171
|
+
timingSafeEqualString(submittedHash, submittedHash);
|
|
172
|
+
return { status: "invalid" };
|
|
173
|
+
}
|
|
174
|
+
if (record.expiresAt <= Date.now())
|
|
175
|
+
return { status: "expired" };
|
|
176
|
+
const attempts = await this.#store.recordAttempt(record.id);
|
|
177
|
+
if (attempts > record.maxAttempts) {
|
|
178
|
+
await this.#store.invalidate(email, purpose);
|
|
179
|
+
return { status: "locked" };
|
|
180
|
+
}
|
|
181
|
+
if (timingSafeEqualString(record.codeHash, submittedHash)) {
|
|
182
|
+
return {
|
|
183
|
+
status: await this.#store.consume(record.id) ? "success" : "invalid",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (attempts === record.maxAttempts) {
|
|
187
|
+
await this.#store.invalidate(email, purpose);
|
|
188
|
+
return { status: "locked" };
|
|
189
|
+
}
|
|
190
|
+
return { status: "invalid" };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* In-memory {@link OtpStore} for development and tests. Records are lost on
|
|
195
|
+
* restart; back the contract with a database for production.
|
|
196
|
+
*/
|
|
197
|
+
export class MemoryOtpStore {
|
|
198
|
+
#records = new Map();
|
|
199
|
+
#active = new Set();
|
|
200
|
+
/** Persist a record and mark it active. */
|
|
201
|
+
create(record) {
|
|
202
|
+
this.#records.set(record.id, record);
|
|
203
|
+
this.#active.add(record.id);
|
|
204
|
+
return Promise.resolve();
|
|
205
|
+
}
|
|
206
|
+
/** The newest active record for `(email, purpose)`, expired or not; else `null`. */
|
|
207
|
+
findActive(email, purpose) {
|
|
208
|
+
let latest = null;
|
|
209
|
+
for (const id of this.#active) {
|
|
210
|
+
const record = this.#records.get(id);
|
|
211
|
+
if (record && record.email === email && record.purpose === purpose &&
|
|
212
|
+
(!latest || record.createdAt >= latest.createdAt)) {
|
|
213
|
+
latest = record;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return Promise.resolve(latest);
|
|
217
|
+
}
|
|
218
|
+
/** Bump a record's attempt count; returns the new count, or 0 if unknown. */
|
|
219
|
+
recordAttempt(id) {
|
|
220
|
+
const record = this.#records.get(id);
|
|
221
|
+
if (!record)
|
|
222
|
+
return Promise.resolve(0);
|
|
223
|
+
record.attempts++;
|
|
224
|
+
return Promise.resolve(record.attempts);
|
|
225
|
+
}
|
|
226
|
+
/** Atomically deactivate one active record; return whether it was claimed. */
|
|
227
|
+
consume(id) {
|
|
228
|
+
return Promise.resolve(this.#active.delete(id));
|
|
229
|
+
}
|
|
230
|
+
/** Deactivate one undelivered record; a no-op if the id is unknown. */
|
|
231
|
+
invalidateById(id) {
|
|
232
|
+
this.#active.delete(id);
|
|
233
|
+
return Promise.resolve();
|
|
234
|
+
}
|
|
235
|
+
/** Deactivate every active record for `(email, purpose)`. */
|
|
236
|
+
invalidate(email, purpose) {
|
|
237
|
+
for (const id of [...this.#active]) {
|
|
238
|
+
const record = this.#records.get(id);
|
|
239
|
+
if (record && record.email === email && record.purpose === purpose) {
|
|
240
|
+
this.#active.delete(id);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return Promise.resolve();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small, configurable password policy. Defaults to a length floor (the NIST
|
|
3
|
+
* guidance is "length over composition rules"); add your own `validators` for
|
|
4
|
+
* extra checks (e.g. a breached-password list). Tenant-agnostic.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
/** Password policy configuration. */
|
|
9
|
+
export interface PasswordPolicy {
|
|
10
|
+
/** Minimum length. Defaults to 8. */
|
|
11
|
+
minLength?: number;
|
|
12
|
+
/** Maximum length (a DoS guard on the hash input). Defaults to 256. */
|
|
13
|
+
maxLength?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Extra checks. Each returns an issue string to reject, or `undefined` to
|
|
16
|
+
* pass, and may be async — e.g. {@link breachedPasswordValidator}, whose
|
|
17
|
+
* k-anonymity range lookup needs the network.
|
|
18
|
+
*/
|
|
19
|
+
validators?: Array<(password: string) => string | undefined | Promise<string | undefined>>;
|
|
20
|
+
}
|
|
21
|
+
/** The result of {@link checkPasswordPolicy}. */
|
|
22
|
+
export interface PasswordPolicyResult {
|
|
23
|
+
/** Whether the password satisfied every policy check. */
|
|
24
|
+
ok: boolean;
|
|
25
|
+
/** Human-readable issues; empty when `ok`. */
|
|
26
|
+
issues: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check a password against a policy, collecting all issues.
|
|
30
|
+
*
|
|
31
|
+
* Fails closed on a non-string `password` — an untyped value off a JSON body
|
|
32
|
+
* has no `length`, so the length checks would silently pass it through to a
|
|
33
|
+
* hash function that then `String`-coerces it.
|
|
34
|
+
*/
|
|
35
|
+
export declare function checkPasswordPolicy(password: string, policy?: PasswordPolicy): Promise<PasswordPolicyResult>;
|
|
36
|
+
/**
|
|
37
|
+
* Throw {@link IdentityError} `weak_password` if the policy fails (the issues
|
|
38
|
+
* are joined into the message). Convenient before {@link PasswordIdentityService.hash}.
|
|
39
|
+
*
|
|
40
|
+
* A custom `validators` entry that **throws** (rather than returning an issue
|
|
41
|
+
* string) is trapped and surfaced as the same `weak_password` error, so a
|
|
42
|
+
* misbehaving validator fails closed instead of leaking a raw error to the
|
|
43
|
+
* caller. A validator that itself throws an {@link IdentityError} is preserved.
|
|
44
|
+
*/
|
|
45
|
+
export declare function assertPasswordPolicy(password: string, policy?: PasswordPolicy): Promise<void>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small, configurable password policy. Defaults to a length floor (the NIST
|
|
3
|
+
* guidance is "length over composition rules"); add your own `validators` for
|
|
4
|
+
* extra checks (e.g. a breached-password list). Tenant-agnostic.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import { IdentityError } from "./errors.js";
|
|
9
|
+
/**
|
|
10
|
+
* Check a password against a policy, collecting all issues.
|
|
11
|
+
*
|
|
12
|
+
* Fails closed on a non-string `password` — an untyped value off a JSON body
|
|
13
|
+
* has no `length`, so the length checks would silently pass it through to a
|
|
14
|
+
* hash function that then `String`-coerces it.
|
|
15
|
+
*/
|
|
16
|
+
export async function checkPasswordPolicy(password, policy = {}) {
|
|
17
|
+
if (typeof password !== "string") {
|
|
18
|
+
return { ok: false, issues: ["Password must be a string."] };
|
|
19
|
+
}
|
|
20
|
+
const minLength = policy.minLength ?? 8;
|
|
21
|
+
const maxLength = policy.maxLength ?? 256;
|
|
22
|
+
const issues = [];
|
|
23
|
+
if (password.length < minLength) {
|
|
24
|
+
issues.push(`Password must be at least ${minLength} characters.`);
|
|
25
|
+
}
|
|
26
|
+
if (password.length > maxLength) {
|
|
27
|
+
issues.push(`Password must be at most ${maxLength} characters.`);
|
|
28
|
+
}
|
|
29
|
+
for (const validate of policy.validators ?? []) {
|
|
30
|
+
const issue = await validate(password);
|
|
31
|
+
if (issue)
|
|
32
|
+
issues.push(issue);
|
|
33
|
+
}
|
|
34
|
+
return { ok: issues.length === 0, issues };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Throw {@link IdentityError} `weak_password` if the policy fails (the issues
|
|
38
|
+
* are joined into the message). Convenient before {@link PasswordIdentityService.hash}.
|
|
39
|
+
*
|
|
40
|
+
* A custom `validators` entry that **throws** (rather than returning an issue
|
|
41
|
+
* string) is trapped and surfaced as the same `weak_password` error, so a
|
|
42
|
+
* misbehaving validator fails closed instead of leaking a raw error to the
|
|
43
|
+
* caller. A validator that itself throws an {@link IdentityError} is preserved.
|
|
44
|
+
*/
|
|
45
|
+
export async function assertPasswordPolicy(password, policy) {
|
|
46
|
+
let result;
|
|
47
|
+
try {
|
|
48
|
+
result = await checkPasswordPolicy(password, policy);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error instanceof IdentityError)
|
|
52
|
+
throw error;
|
|
53
|
+
throw new IdentityError("weak_password", "Password could not be validated.", {
|
|
54
|
+
cause: error,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (!result.ok) {
|
|
58
|
+
throw new IdentityError("weak_password", result.issues.join(" "));
|
|
59
|
+
}
|
|
60
|
+
}
|