@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,835 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `IdentityService` — composable functions for an app that owns its login.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the identity primitives ({@link PasswordIdentityService},
|
|
5
|
+
* {@link TokenFlowService}, delivery hooks, {@link RevocableSessionService})
|
|
6
|
+
* over an **app-owned** {@link IdentityUserStore} into the flows a single app
|
|
7
|
+
* needs: sign-up, sign-in, password reset, email verification, and passwordless
|
|
8
|
+
* sign-in (magic links + one-time codes). These are plain
|
|
9
|
+
* methods you call from your own routes/loaders/actions — there is no opinionated
|
|
10
|
+
* mounted framework here. A thin, optional Hono route factory lives in
|
|
11
|
+
* `@udibo/oauth2/hono/identity` for apps that want the endpoints mounted.
|
|
12
|
+
*
|
|
13
|
+
* Storage-agnostic by design: every flow addresses users by opaque id and the
|
|
14
|
+
* {@link IdentityUserStore} is yours — back it with whatever database, ORM, and
|
|
15
|
+
* schema your app already uses. The library composes the flows; it never owns
|
|
16
|
+
* your tables. It stays below the turnkey-identity-service line.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
import { delay } from "../deps/jsr.io/@std/async/1.5.0/delay.js";
|
|
21
|
+
import { buildResetUrl, buildSignInUrl, buildUnlockUrl, buildVerificationUrl, } from "./delivery.js";
|
|
22
|
+
import { PasswordIdentityService, } from "./password.js";
|
|
23
|
+
import { TokenPurpose, } from "./token-flow.js";
|
|
24
|
+
import { assertPasswordPolicy, } from "./password-policy.js";
|
|
25
|
+
import { enforceRateLimit } from "./rate-limit.js";
|
|
26
|
+
import { EmailOtpService, otpRateKey } from "./otp.js";
|
|
27
|
+
import { dispatchIdentityEvent, } from "./events.js";
|
|
28
|
+
import { findLegacyVerifier, } from "./migration.js";
|
|
29
|
+
const DEFAULT_RESET_TTL = 60 * 60 * 1000;
|
|
30
|
+
const DEFAULT_VERIFY_TTL = 24 * 60 * 60 * 1000;
|
|
31
|
+
const DEFAULT_UNLOCK_TTL = 60 * 60 * 1000;
|
|
32
|
+
const DEFAULT_SIGNIN_LINK_TTL = 15 * 60 * 1000;
|
|
33
|
+
const DEFAULT_FAILED_SIGN_IN_FLOOR_MS = 250;
|
|
34
|
+
function foldEmail(email) {
|
|
35
|
+
return email.trim().toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The rate-limit key for each throttled flow, one entry per
|
|
39
|
+
* {@link IdentityRateLimiters} key. Every check and every reset derives its key
|
|
40
|
+
* here, so the window a flow fills is always the window its reset clears.
|
|
41
|
+
*
|
|
42
|
+
* Case-folding is a decision per key type. Email-addressed keys fold, so casing
|
|
43
|
+
* variants of one mailbox share a window instead of each earning a fresh send
|
|
44
|
+
* budget (`signInCode` folds inside {@link otpRateKey}, so the window it fills
|
|
45
|
+
* is the one a standalone {@link EmailOtpService} would). Opaque user ids pass
|
|
46
|
+
* through untouched — only the app knows whether they are case-sensitive.
|
|
47
|
+
* `signin:` is trimmed but not folded: an identifier
|
|
48
|
+
* may be a username or phone whose equality only the app's `findByIdentifier`
|
|
49
|
+
* defines, and folding would merge two distinct accounts (`Bob`, `bob`) into
|
|
50
|
+
* one window, letting failures against either deny sign-in to the other.
|
|
51
|
+
*/
|
|
52
|
+
const RATE_KEYS = {
|
|
53
|
+
signIn: (identifier) => `signin:${identifier.trim()}`,
|
|
54
|
+
passwordReset: (email) => `pwreset:${foldEmail(email)}`,
|
|
55
|
+
emailVerification: (userId) => `verifyemail:${userId}`,
|
|
56
|
+
accountUnlock: (userId) => `unlock:${userId}`,
|
|
57
|
+
signInLink: (email) => `pwless:${foldEmail(email)}`,
|
|
58
|
+
signInCode: (email) => otpRateKey(TokenPurpose.SignIn, email),
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Composable own-auth flows. Construct once with your dependencies and call the
|
|
62
|
+
* methods from your routes; the credential form, copy, and session creation stay
|
|
63
|
+
* in your app.
|
|
64
|
+
*/
|
|
65
|
+
export class IdentityService {
|
|
66
|
+
#users;
|
|
67
|
+
#tokens;
|
|
68
|
+
#passwords;
|
|
69
|
+
#sessions;
|
|
70
|
+
#delivery;
|
|
71
|
+
#baseUrl;
|
|
72
|
+
#ttl;
|
|
73
|
+
#passwordPolicy;
|
|
74
|
+
#rateLimiters;
|
|
75
|
+
#otp;
|
|
76
|
+
#lockout;
|
|
77
|
+
#enforce;
|
|
78
|
+
#onEvent;
|
|
79
|
+
#legacyVerifiers;
|
|
80
|
+
#failedSignInFloorMs;
|
|
81
|
+
/** Wires the user store and optional collaborators; applies the TTL and hasher defaults. */
|
|
82
|
+
constructor(options) {
|
|
83
|
+
this.#users = options.users;
|
|
84
|
+
this.#tokens = options.tokens;
|
|
85
|
+
this.#passwords = options.passwords ?? new PasswordIdentityService();
|
|
86
|
+
this.#sessions = options.sessions;
|
|
87
|
+
this.#delivery = options.delivery;
|
|
88
|
+
this.#baseUrl = options.baseUrl;
|
|
89
|
+
this.#ttl = {
|
|
90
|
+
passwordReset: options.ttl?.passwordReset ?? DEFAULT_RESET_TTL,
|
|
91
|
+
emailVerification: options.ttl?.emailVerification ?? DEFAULT_VERIFY_TTL,
|
|
92
|
+
accountUnlock: options.ttl?.accountUnlock ?? DEFAULT_UNLOCK_TTL,
|
|
93
|
+
signInLink: options.ttl?.signInLink ?? DEFAULT_SIGNIN_LINK_TTL,
|
|
94
|
+
};
|
|
95
|
+
this.#passwordPolicy = options.passwordPolicy;
|
|
96
|
+
this.#rateLimiters = {
|
|
97
|
+
signIn: options.rateLimiters?.signIn ?? options.rateLimiter,
|
|
98
|
+
passwordReset: options.rateLimiters?.passwordReset ?? options.rateLimiter,
|
|
99
|
+
emailVerification: options.rateLimiters?.emailVerification ??
|
|
100
|
+
options.rateLimiter,
|
|
101
|
+
accountUnlock: options.rateLimiters?.accountUnlock ?? options.rateLimiter,
|
|
102
|
+
signInLink: options.rateLimiters?.signInLink ?? options.rateLimiter,
|
|
103
|
+
signInCode: options.rateLimiters?.signInCode ?? options.rateLimiter,
|
|
104
|
+
};
|
|
105
|
+
this.#otp = options.otp
|
|
106
|
+
? new EmailOtpService({
|
|
107
|
+
store: options.otp.store,
|
|
108
|
+
digits: options.otp.digits,
|
|
109
|
+
ttlMs: options.otp.ttlMs,
|
|
110
|
+
maxAttempts: options.otp.maxAttempts,
|
|
111
|
+
})
|
|
112
|
+
: undefined;
|
|
113
|
+
this.#lockout = options.lockout;
|
|
114
|
+
this.#enforce = (options.protectionMode ?? "enforce") === "enforce";
|
|
115
|
+
this.#onEvent = options.onEvent;
|
|
116
|
+
this.#legacyVerifiers = options.legacyVerifiers?.length
|
|
117
|
+
? options.legacyVerifiers
|
|
118
|
+
: undefined;
|
|
119
|
+
const floor = options.failedSignInFloorMs ??
|
|
120
|
+
DEFAULT_FAILED_SIGN_IN_FLOOR_MS;
|
|
121
|
+
this.#failedSignInFloorMs = Number.isFinite(floor) && floor >= 0
|
|
122
|
+
? floor
|
|
123
|
+
: DEFAULT_FAILED_SIGN_IN_FLOOR_MS;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Create a user with a hashed password. `profile` is your app's own fields.
|
|
127
|
+
*
|
|
128
|
+
* @throws {IdentityError} `weak_password` when `password` fails the password
|
|
129
|
+
* policy — including the 8–256 character defaults that apply when
|
|
130
|
+
* {@link IdentityServiceOptions.passwordPolicy} is omitted, and a non-string
|
|
131
|
+
* password (the hash would `String`-coerce it).
|
|
132
|
+
*/
|
|
133
|
+
async signUp(input) {
|
|
134
|
+
await assertPasswordPolicy(input.password, this.#passwordPolicy);
|
|
135
|
+
const credential = await this.#passwords.hash(input.password);
|
|
136
|
+
const user = await this.#users.create(input.profile, credential);
|
|
137
|
+
await this.#emit({ type: "sign_up", userId: user.id });
|
|
138
|
+
return user;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Verify credentials; returns the user, or `null` for an unknown identifier or
|
|
142
|
+
* wrong password (don't reveal which). Establishing a session is your app's
|
|
143
|
+
* job — do it on a non-null result.
|
|
144
|
+
*
|
|
145
|
+
* The throttle window is keyed on the identifier as submitted (trimmed, not
|
|
146
|
+
* case-folded), so pass the same normalized form you resolve accounts by —
|
|
147
|
+
* otherwise casing variants of one account each get their own window, and
|
|
148
|
+
* the identifiers you later hand {@link IdentityService.resetSignInThrottle}
|
|
149
|
+
* won't match the ones that filled it.
|
|
150
|
+
*
|
|
151
|
+
* Every rejection is held to
|
|
152
|
+
* {@link IdentityServiceOptions.failedSignInFloorMs} (250 ms by default),
|
|
153
|
+
* measured from entry, so an unknown identifier, a locked account, a wrong
|
|
154
|
+
* native password, and a wrong password against an imported hash all take the
|
|
155
|
+
* same time — the branch that runs a foreign KDF is not visible from
|
|
156
|
+
* outside. A success returns as soon as it is done.
|
|
157
|
+
*
|
|
158
|
+
* @throws {IdentityError} `rate_limited` (with `retryAfterMs`) when a
|
|
159
|
+
* `rateLimiter` is configured, the identifier's limit is hit, and
|
|
160
|
+
* `protectionMode` is `"enforce"`. Rate-limit rejections throw before the
|
|
161
|
+
* account lookup and are not held to the floor.
|
|
162
|
+
*/
|
|
163
|
+
async signIn(input) {
|
|
164
|
+
const startedAt = performance.now();
|
|
165
|
+
const identifier = input.identifier.trim();
|
|
166
|
+
// Throttle before lookup so 429s don't reveal whether the account exists.
|
|
167
|
+
await this.#throttle("signIn", identifier, "too many sign-in attempts", {
|
|
168
|
+
type: "sign_in.rate_limited",
|
|
169
|
+
identifier,
|
|
170
|
+
});
|
|
171
|
+
const user = await this.#users.findByIdentifier(identifier);
|
|
172
|
+
if (!user) {
|
|
173
|
+
await this.#equalizeTiming(input.password);
|
|
174
|
+
await this.#emit({
|
|
175
|
+
type: "sign_in.failed",
|
|
176
|
+
identifier,
|
|
177
|
+
reason: "unknown_identifier",
|
|
178
|
+
});
|
|
179
|
+
return await this.#rejectSignIn(startedAt);
|
|
180
|
+
}
|
|
181
|
+
if (await this.#isLockedOut(user.id, identifier)) {
|
|
182
|
+
await this.#equalizeTiming(input.password);
|
|
183
|
+
return await this.#rejectSignIn(startedAt);
|
|
184
|
+
}
|
|
185
|
+
const outcome = await this.#authenticate(user.id, input.password);
|
|
186
|
+
if (outcome !== "authenticated") {
|
|
187
|
+
await this.#emit({
|
|
188
|
+
type: "sign_in.failed",
|
|
189
|
+
identifier,
|
|
190
|
+
reason: outcome,
|
|
191
|
+
userId: user.id,
|
|
192
|
+
});
|
|
193
|
+
if (outcome === "wrong_password") {
|
|
194
|
+
await this.#recordSignInFailure(user.id, identifier);
|
|
195
|
+
}
|
|
196
|
+
return await this.#rejectSignIn(startedAt);
|
|
197
|
+
}
|
|
198
|
+
await this.#resetThrottle("signIn", identifier);
|
|
199
|
+
if (this.#lockout)
|
|
200
|
+
await this.#lockout.reset(user.id);
|
|
201
|
+
await this.#emit({
|
|
202
|
+
type: "sign_in.succeeded",
|
|
203
|
+
userId: user.id,
|
|
204
|
+
identifier,
|
|
205
|
+
});
|
|
206
|
+
return user;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Clear the sign-in rate-limit window for one identifier. Call it after a
|
|
210
|
+
* flow that proves account ownership (account unlock, password reset) with
|
|
211
|
+
* each identifier the user signs in with — otherwise the failed attempts
|
|
212
|
+
* that caused the lockout still fill the window and the user's next sign-in
|
|
213
|
+
* is rejected as rate-limited despite being unlocked. No-op without a
|
|
214
|
+
* `rateLimiter`.
|
|
215
|
+
*/
|
|
216
|
+
async resetSignInThrottle(identifier) {
|
|
217
|
+
await this.#resetThrottle("signIn", identifier);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Enumeration-safe: always resolves or throws identically whether or not the
|
|
221
|
+
* email maps to a user. If it does, mints a single-use reset token and calls
|
|
222
|
+
* `delivery.sendPasswordReset`. With a `rateLimiter`, requests throttle per
|
|
223
|
+
* target email, case-folded so casing variants share one window — the check
|
|
224
|
+
* runs before the lookup, so a 429 reveals nothing —
|
|
225
|
+
* and throw {@link IdentityError} `rate_limited` when exceeded; catch it and
|
|
226
|
+
* return your uniform success response. Give it a tighter threshold than
|
|
227
|
+
* sign-in via `rateLimiters.passwordReset`.
|
|
228
|
+
*/
|
|
229
|
+
async requestPasswordReset(email) {
|
|
230
|
+
const tokens = this.#requireTokens("requestPasswordReset");
|
|
231
|
+
const normalized = email.trim();
|
|
232
|
+
await this.#throttle("passwordReset", normalized, "too many password reset requests", { type: "password_reset.rate_limited", email: normalized });
|
|
233
|
+
const user = await this.#users.findByEmail(normalized);
|
|
234
|
+
if (!user) {
|
|
235
|
+
await this.#emit({
|
|
236
|
+
type: "password_reset.requested",
|
|
237
|
+
email: normalized,
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
await this.#issueAndDeliver(tokens, {
|
|
242
|
+
hook: "sendPasswordReset",
|
|
243
|
+
to: normalized,
|
|
244
|
+
subject: user.id,
|
|
245
|
+
data: { email: normalized },
|
|
246
|
+
ttlMs: this.#ttl.passwordReset,
|
|
247
|
+
requested: {
|
|
248
|
+
type: "password_reset.requested",
|
|
249
|
+
email: normalized,
|
|
250
|
+
userId: user.id,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Consume a reset token, set the new password, and (if a `sessions` service
|
|
256
|
+
* was configured) revoke the user's other sessions. Clears any failed-attempt
|
|
257
|
+
* lockout — the emailed token proves the same account ownership the unlock
|
|
258
|
+
* flow does. Returns `{ userId }`, or `null` for an invalid/expired/used
|
|
259
|
+
* token.
|
|
260
|
+
*
|
|
261
|
+
* A reset is the user's "lock everyone else out" action, so the outstanding
|
|
262
|
+
* passwordless credentials for the same account are voided too: sign-in links
|
|
263
|
+
* (needs a token store with `deleteBySubject` — without it they stay
|
|
264
|
+
* redeemable until they expire) and, when an `otp` service is configured, the
|
|
265
|
+
* pending sign-in code. Neither can fail the reset: a store that throws is
|
|
266
|
+
* logged and the reset still completes, since the password has already
|
|
267
|
+
* changed by then.
|
|
268
|
+
*
|
|
269
|
+
* Sessions are revoked **after** the password is changed. If that revocation
|
|
270
|
+
* throws, the reset is reported failed — a `password_reset.failed`
|
|
271
|
+
* (`session_revocation_failed`) event fires, no `password_reset.completed`
|
|
272
|
+
* fires, and the error rethrows — rather than silently returning success with
|
|
273
|
+
* the user's old sessions still live. Retry the reset to clear them.
|
|
274
|
+
*
|
|
275
|
+
* @throws {IdentityError} `weak_password` when `password` fails the password
|
|
276
|
+
* policy — including the 8–256 character defaults that apply when
|
|
277
|
+
* {@link IdentityServiceOptions.passwordPolicy} is omitted. The token is left
|
|
278
|
+
* unconsumed, so the user's link still works.
|
|
279
|
+
*/
|
|
280
|
+
async resetPassword(input) {
|
|
281
|
+
const tokens = this.#requireTokens("resetPassword");
|
|
282
|
+
// Check the policy before consuming the single-use token so a weak password
|
|
283
|
+
// doesn't burn the user's reset link.
|
|
284
|
+
await assertPasswordPolicy(input.password, this.#passwordPolicy);
|
|
285
|
+
const consumed = await this.#consumeToken({
|
|
286
|
+
tokens,
|
|
287
|
+
purpose: TokenPurpose.PasswordReset,
|
|
288
|
+
token: input.token,
|
|
289
|
+
failed: () => ({
|
|
290
|
+
type: "password_reset.failed",
|
|
291
|
+
reason: "invalid_token",
|
|
292
|
+
}),
|
|
293
|
+
});
|
|
294
|
+
if (consumed.status !== "success")
|
|
295
|
+
return null;
|
|
296
|
+
const { resolved } = consumed;
|
|
297
|
+
await this.#users.setCredential(resolved.subject, await this.#passwords.hash(input.password));
|
|
298
|
+
try {
|
|
299
|
+
await this.#users.clearLegacyCredential?.(resolved.subject);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
// A throwing hook must not skip session revocation after the password
|
|
303
|
+
// already changed.
|
|
304
|
+
console.error("[@udibo/oauth2] resetPassword legacy-credential clear failed:", error instanceof Error ? error.message : error);
|
|
305
|
+
}
|
|
306
|
+
await this.#voidPasswordlessCredentials(tokens, resolved.subject, emailFromData(resolved.data));
|
|
307
|
+
if (this.#sessions) {
|
|
308
|
+
try {
|
|
309
|
+
await this.#sessions.revokeAllByUser(resolved.subject);
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
await this.#emit({
|
|
313
|
+
type: "password_reset.failed",
|
|
314
|
+
reason: "session_revocation_failed",
|
|
315
|
+
});
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
await this.#lockout?.reset(resolved.subject);
|
|
320
|
+
await this.#emit({
|
|
321
|
+
type: "password_reset.completed",
|
|
322
|
+
userId: resolved.subject,
|
|
323
|
+
});
|
|
324
|
+
return { userId: resolved.subject };
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Mint + deliver an email-verification link for a user. Typically called
|
|
328
|
+
* right after sign-up, or from an authenticated "resend" action — not exposed
|
|
329
|
+
* as an unauthenticated route (you'd be emailing arbitrary user ids). With a
|
|
330
|
+
* `rateLimiter` (or a tighter `rateLimiters.emailVerification`), requests
|
|
331
|
+
* throttle per user and throw {@link IdentityError} `rate_limited` when
|
|
332
|
+
* exceeded — surface it as a friendly cooldown.
|
|
333
|
+
*/
|
|
334
|
+
async requestEmailVerification(input) {
|
|
335
|
+
const tokens = this.#requireTokens("requestEmailVerification");
|
|
336
|
+
await this.#throttle("emailVerification", input.userId, "too many verification email requests", {
|
|
337
|
+
type: "email_verification.rate_limited",
|
|
338
|
+
userId: input.userId,
|
|
339
|
+
email: input.email,
|
|
340
|
+
});
|
|
341
|
+
await this.#issueAndDeliver(tokens, {
|
|
342
|
+
hook: "sendEmailVerification",
|
|
343
|
+
to: input.email,
|
|
344
|
+
subject: input.userId,
|
|
345
|
+
ttlMs: this.#ttl.emailVerification,
|
|
346
|
+
data: { email: input.email },
|
|
347
|
+
requested: {
|
|
348
|
+
type: "email_verification.requested",
|
|
349
|
+
userId: input.userId,
|
|
350
|
+
email: input.email,
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Consume a verification token and mark the user's email verified.
|
|
356
|
+
*
|
|
357
|
+
* Returns a discriminated {@link VerifyEmailResult} so the UI can tell an
|
|
358
|
+
* **expired** link (offer to resend) from a generically **invalid** one — the
|
|
359
|
+
* `inspect()` distinction, surfaced through the flow. A `valid` token is
|
|
360
|
+
* consumed; a lost race (consumed between inspect and consume) reports
|
|
361
|
+
* `invalid`.
|
|
362
|
+
*
|
|
363
|
+
* The email is marked verified **before** the token is consumed, so a throw
|
|
364
|
+
* from `markEmailVerified` leaves the single-use link unspent and the user
|
|
365
|
+
* can retry with the same link (`markEmailVerified` must be idempotent).
|
|
366
|
+
*
|
|
367
|
+
* The address the token was issued for is passed to
|
|
368
|
+
* {@link IdentityUserStore.markEmailVerified} alongside the user id, so a
|
|
369
|
+
* store that predicates on both never verifies an address the link was not
|
|
370
|
+
* minted for.
|
|
371
|
+
*/
|
|
372
|
+
async verifyEmail(token) {
|
|
373
|
+
const tokens = this.#requireTokens("verifyEmail");
|
|
374
|
+
const result = await this.#consumeToken({
|
|
375
|
+
tokens,
|
|
376
|
+
purpose: TokenPurpose.EmailVerification,
|
|
377
|
+
token,
|
|
378
|
+
failed: (reason) => ({ type: "email_verification.failed", reason }),
|
|
379
|
+
beforeConsume: (subject, data) => this.#users.markEmailVerified?.(subject, emailFromData(data)),
|
|
380
|
+
});
|
|
381
|
+
if (result.status !== "success")
|
|
382
|
+
return result;
|
|
383
|
+
const userId = result.resolved.subject;
|
|
384
|
+
const email = emailFromData(result.resolved.data);
|
|
385
|
+
await this.#emit({
|
|
386
|
+
type: "email_verification.completed",
|
|
387
|
+
userId,
|
|
388
|
+
email,
|
|
389
|
+
});
|
|
390
|
+
return { status: "success", userId, email };
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Mint + deliver a single-use account-unlock link after a lockout. Call it
|
|
394
|
+
* with the locked account's email (typically from your `lockout` event
|
|
395
|
+
* handler — the service only knows opaque user ids). Requires `tokens` and
|
|
396
|
+
* a `delivery.sendAccountUnlock` hook to actually send. With a `rateLimiter`
|
|
397
|
+
* (or a tighter `rateLimiters.accountUnlock`), requests throttle per user and
|
|
398
|
+
* throw {@link IdentityError} `rate_limited` when exceeded.
|
|
399
|
+
*/
|
|
400
|
+
async requestAccountUnlock(input) {
|
|
401
|
+
const tokens = this.#requireTokens("requestAccountUnlock");
|
|
402
|
+
await this.#throttle("accountUnlock", input.userId, "too many account unlock requests", {
|
|
403
|
+
type: "account_unlock.rate_limited",
|
|
404
|
+
userId: input.userId,
|
|
405
|
+
email: input.email,
|
|
406
|
+
});
|
|
407
|
+
await this.#issueAndDeliver(tokens, {
|
|
408
|
+
hook: "sendAccountUnlock",
|
|
409
|
+
to: input.email,
|
|
410
|
+
subject: input.userId,
|
|
411
|
+
ttlMs: this.#ttl.accountUnlock,
|
|
412
|
+
requested: {
|
|
413
|
+
type: "account_unlock.requested",
|
|
414
|
+
userId: input.userId,
|
|
415
|
+
email: input.email,
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Consume an unlock token and clear the account's lockout state (failures
|
|
421
|
+
* and lock). The self-service unlock path: the emailed link lands here.
|
|
422
|
+
*/
|
|
423
|
+
async unlockAccount(token) {
|
|
424
|
+
const tokens = this.#requireTokens("unlockAccount");
|
|
425
|
+
const result = await this.#consumeToken({
|
|
426
|
+
tokens,
|
|
427
|
+
purpose: TokenPurpose.AccountUnlock,
|
|
428
|
+
token,
|
|
429
|
+
failed: (reason) => ({ type: "account_unlock.failed", reason }),
|
|
430
|
+
});
|
|
431
|
+
if (result.status !== "success")
|
|
432
|
+
return result;
|
|
433
|
+
const userId = result.resolved.subject;
|
|
434
|
+
await this.#lockout?.reset(userId);
|
|
435
|
+
await this.#emit({ type: "account_unlock.completed", userId });
|
|
436
|
+
return { status: "success", userId };
|
|
437
|
+
}
|
|
438
|
+
async #getLegacyCredential(userId) {
|
|
439
|
+
if (!this.#legacyVerifiers)
|
|
440
|
+
return null;
|
|
441
|
+
const get = this.#users.getLegacyCredential;
|
|
442
|
+
if (!get)
|
|
443
|
+
return null;
|
|
444
|
+
return (await get.call(this.#users, userId)) ?? null;
|
|
445
|
+
}
|
|
446
|
+
async #tryLegacy(verifier, password, phc) {
|
|
447
|
+
try {
|
|
448
|
+
return await verifier.verify(password, phc);
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
// Never log the error object: a BYO verifier may echo the hash or the
|
|
452
|
+
// submitted password in its message. Fail closed.
|
|
453
|
+
console.error(`[@udibo/oauth2] legacy password verifier ${verifier.id} threw`);
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Passwordless request: always resolves with the same `void` result. If the
|
|
459
|
+
* email maps to a user, mints a single-use sign-in token (invalidating any
|
|
460
|
+
* outstanding one) and calls `delivery.sendSignInLink`; an unknown email does
|
|
461
|
+
* neither, so the **return value** never reveals whether an account exists.
|
|
462
|
+
* The synchronous path is not fully timing-uniform, though — only the
|
|
463
|
+
* known-email branch awaits token minting and delivery — so send email from a
|
|
464
|
+
* background queue and apply IP-level throttling at the route to close the
|
|
465
|
+
* residual latency/side-channel oracle. Throttled per email (case-folded,
|
|
466
|
+
* key `pwless:<email>`) when a `rateLimiter` — or a tighter
|
|
467
|
+
* `rateLimiters.signInLink` — is configured; throws {@link IdentityError}
|
|
468
|
+
* `rate_limited` in enforce mode. Default link lifetime is 15 minutes.
|
|
469
|
+
*/
|
|
470
|
+
async requestSignInLink(email, options) {
|
|
471
|
+
const tokens = this.#requireTokens("requestSignInLink");
|
|
472
|
+
const normalized = email.trim();
|
|
473
|
+
await this.#throttle("signInLink", normalized, "too many sign-in link requests", { type: "signin_link.rate_limited", email: normalized });
|
|
474
|
+
const user = await this.#users.findByEmail(normalized);
|
|
475
|
+
if (!user) {
|
|
476
|
+
await this.#emit({ type: "signin_link.requested", email: normalized });
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
await this.#issueAndDeliver(tokens, {
|
|
480
|
+
hook: "sendSignInLink",
|
|
481
|
+
to: normalized,
|
|
482
|
+
subject: user.id,
|
|
483
|
+
ttlMs: options?.ttlMs ?? this.#ttl.signInLink,
|
|
484
|
+
requested: {
|
|
485
|
+
type: "signin_link.requested",
|
|
486
|
+
email: normalized,
|
|
487
|
+
userId: user.id,
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Consume a sign-in link token (single-use). `success` carries the `userId`
|
|
493
|
+
* to sign in; establishing the session — and running your MFA gate first —
|
|
494
|
+
* stays your app's job. So does re-resolving the user: a user deleted or
|
|
495
|
+
* disabled since the link was minted must be treated as `invalid` by your
|
|
496
|
+
* user store, not signed in. `expired` and `invalid` are distinguished so
|
|
497
|
+
* the UI can offer a fresh link.
|
|
498
|
+
*/
|
|
499
|
+
async consumeSignInLink(token) {
|
|
500
|
+
const tokens = this.#requireTokens("consumeSignInLink");
|
|
501
|
+
const result = await this.#consumeToken({
|
|
502
|
+
tokens,
|
|
503
|
+
purpose: TokenPurpose.SignIn,
|
|
504
|
+
token,
|
|
505
|
+
failed: (reason) => ({ type: "signin_link.failed", reason }),
|
|
506
|
+
});
|
|
507
|
+
if (result.status !== "success")
|
|
508
|
+
return result;
|
|
509
|
+
const userId = result.resolved.subject;
|
|
510
|
+
await this.#emit({ type: "signin_link.consumed", userId });
|
|
511
|
+
return { status: "success", userId };
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Passwordless request: always resolves with the same `void` result. If the
|
|
515
|
+
* email maps to a user, mints a one-time numeric code (invalidating any
|
|
516
|
+
* outstanding one) and calls `delivery.sendSignInCode`; an unknown email does
|
|
517
|
+
* neither, so the **return value** never reveals whether an account exists.
|
|
518
|
+
* As with {@link IdentityService.requestSignInLink}, only the known-email
|
|
519
|
+
* branch awaits code minting and delivery, so send from a background queue
|
|
520
|
+
* and throttle by IP at the route to close the residual timing oracle.
|
|
521
|
+
* Requires the `otp` option. Throttled per email (case-folded, key
|
|
522
|
+
* `otp:signin:<email>`, shared with {@link IdentityService.verifySignInCode})
|
|
523
|
+
* when a `rateLimiter` — or a tighter `rateLimiters.signInCode` — is
|
|
524
|
+
* configured; throws {@link IdentityError} `rate_limited` in enforce mode.
|
|
525
|
+
* That is the flow's only throttle, and it runs before the lookup, so a
|
|
526
|
+
* known email can never be rate-limited on a request an unknown one survives.
|
|
527
|
+
* A throwing `sendSignInCode` is trapped like the link hooks (see
|
|
528
|
+
* {@link DeliveryHooks}): that exact code is invalidated — never a newer one a
|
|
529
|
+
* retry may have delivered meanwhile — a `delivery.failed` event is emitted,
|
|
530
|
+
* and the call still resolves `void`, so a mailer outage is not an enumeration
|
|
531
|
+
* oracle either.
|
|
532
|
+
*/
|
|
533
|
+
async requestSignInCode(email) {
|
|
534
|
+
const otp = this.#requireOtp("requestSignInCode");
|
|
535
|
+
const normalized = email.trim();
|
|
536
|
+
await this.#throttle("signInCode", normalized, "too many sign-in code requests", { type: "signin_code.rate_limited", email: normalized });
|
|
537
|
+
const user = await this.#users.findByEmail(normalized);
|
|
538
|
+
if (!user) {
|
|
539
|
+
await this.#emit({ type: "signin_code.requested", email: normalized });
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
await otp.request({
|
|
543
|
+
email: normalized,
|
|
544
|
+
purpose: TokenPurpose.SignIn,
|
|
545
|
+
onDeliver: (code, expiresAt, codeId) => this.#deliverCode(normalized, code, user.id, expiresAt, codeId),
|
|
546
|
+
});
|
|
547
|
+
await this.#emit({
|
|
548
|
+
type: "signin_code.requested",
|
|
549
|
+
email: normalized,
|
|
550
|
+
userId: user.id,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Verify a one-time sign-in code. An unknown email performs the same hash
|
|
555
|
+
* work as a known one and reports plain `invalid` — the result is not an
|
|
556
|
+
* enumeration oracle. Wrong tries spend the code's attempt budget (`locked`
|
|
557
|
+
* once it runs out); success consumes the code (single-use), resets the
|
|
558
|
+
* flow's throttle window, and carries the `userId` to sign in — session
|
|
559
|
+
* creation and your MFA gate stay your app's job, as does treating a
|
|
560
|
+
* since-deleted/disabled user as a failed sign-in.
|
|
561
|
+
*/
|
|
562
|
+
async verifySignInCode(input) {
|
|
563
|
+
const otp = this.#requireOtp("verifySignInCode");
|
|
564
|
+
const email = input.email.trim();
|
|
565
|
+
await this.#throttle("signInCode", email, "too many sign-in code attempts", { type: "signin_code.rate_limited", email });
|
|
566
|
+
const user = await this.#users.findByEmail(email);
|
|
567
|
+
const result = await otp.verify({
|
|
568
|
+
email,
|
|
569
|
+
purpose: TokenPurpose.SignIn,
|
|
570
|
+
code: input.code,
|
|
571
|
+
});
|
|
572
|
+
if (!user) {
|
|
573
|
+
await this.#emit({
|
|
574
|
+
type: "signin_code.failed",
|
|
575
|
+
email,
|
|
576
|
+
reason: "unknown_email",
|
|
577
|
+
});
|
|
578
|
+
return { status: "invalid" };
|
|
579
|
+
}
|
|
580
|
+
if (result.status !== "success") {
|
|
581
|
+
await this.#emit({
|
|
582
|
+
type: "signin_code.failed",
|
|
583
|
+
email,
|
|
584
|
+
reason: result.status,
|
|
585
|
+
});
|
|
586
|
+
return { status: "invalid" };
|
|
587
|
+
}
|
|
588
|
+
await this.#resetThrottle("signInCode", email);
|
|
589
|
+
await this.#emit({ type: "signin_code.verified", userId: user.id });
|
|
590
|
+
return { status: "success", userId: user.id };
|
|
591
|
+
}
|
|
592
|
+
#throttle(purpose, value, message, event) {
|
|
593
|
+
return enforceRateLimit({
|
|
594
|
+
rateLimiter: this.#rateLimiters[purpose],
|
|
595
|
+
key: RATE_KEYS[purpose](value),
|
|
596
|
+
message,
|
|
597
|
+
enforce: this.#enforce,
|
|
598
|
+
event,
|
|
599
|
+
emit: (e) => this.#emit(e),
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
async #resetThrottle(purpose, value) {
|
|
603
|
+
await this.#rateLimiters[purpose]?.reset(RATE_KEYS[purpose](value));
|
|
604
|
+
}
|
|
605
|
+
async #equalizeTiming(password) {
|
|
606
|
+
await this.#passwords.hash(password);
|
|
607
|
+
}
|
|
608
|
+
async #rejectSignIn(startedAt) {
|
|
609
|
+
const remaining = this.#failedSignInFloorMs -
|
|
610
|
+
(performance.now() - startedAt);
|
|
611
|
+
if (remaining > 0)
|
|
612
|
+
await delay(remaining);
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
async #isLockedOut(userId, identifier) {
|
|
616
|
+
if (!this.#lockout)
|
|
617
|
+
return false;
|
|
618
|
+
const status = await this.#lockout.status(userId);
|
|
619
|
+
if (!status.locked)
|
|
620
|
+
return false;
|
|
621
|
+
await this.#emit({
|
|
622
|
+
type: "sign_in.locked",
|
|
623
|
+
userId,
|
|
624
|
+
identifier,
|
|
625
|
+
enforced: this.#enforce,
|
|
626
|
+
});
|
|
627
|
+
return this.#enforce;
|
|
628
|
+
}
|
|
629
|
+
async #authenticate(userId, password) {
|
|
630
|
+
const credential = await this.#users.getCredential(userId);
|
|
631
|
+
// Native credential is authoritative: only try an imported hash when there
|
|
632
|
+
// is none, so a wrong password can't resurrect a stale one (no downgrade).
|
|
633
|
+
const legacyHash = credential ? null : await this.#getLegacyCredential(userId);
|
|
634
|
+
if (!credential && !legacyHash) {
|
|
635
|
+
await this.#equalizeTiming(password);
|
|
636
|
+
return "no_password";
|
|
637
|
+
}
|
|
638
|
+
if (credential && await this.#passwords.verify(password, credential)) {
|
|
639
|
+
return await this.#rehashCredential(userId, password, credential)
|
|
640
|
+
? "authenticated"
|
|
641
|
+
: "wrong_password";
|
|
642
|
+
}
|
|
643
|
+
if (!legacyHash)
|
|
644
|
+
return "wrong_password";
|
|
645
|
+
const verifier = findLegacyVerifier(this.#legacyVerifiers, legacyHash);
|
|
646
|
+
if (!verifier || !await this.#tryLegacy(verifier, password, legacyHash)) {
|
|
647
|
+
await this.#equalizeTiming(password);
|
|
648
|
+
return "wrong_password";
|
|
649
|
+
}
|
|
650
|
+
return await this.#upgradeCredential(userId, password, verifier.id)
|
|
651
|
+
? "authenticated"
|
|
652
|
+
: "wrong_password";
|
|
653
|
+
}
|
|
654
|
+
async #rehashCredential(userId, password, credential) {
|
|
655
|
+
if (!this.#users.replaceCredential ||
|
|
656
|
+
!this.#passwords.needsRehash?.(credential))
|
|
657
|
+
return true;
|
|
658
|
+
let replaced;
|
|
659
|
+
try {
|
|
660
|
+
replaced = await this.#users.replaceCredential(userId, credential, await this.#passwords.hash(password));
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
this.#logPersistFailure("password rehash", error);
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
return replaced || await this.#verifyCurrentCredential(userId, password);
|
|
667
|
+
}
|
|
668
|
+
async #upgradeCredential(userId, password, verifierId) {
|
|
669
|
+
if (!this.#users.replaceCredential)
|
|
670
|
+
return true;
|
|
671
|
+
let upgraded;
|
|
672
|
+
try {
|
|
673
|
+
upgraded = await this.#users.replaceCredential(userId, undefined, await this.#passwords.hash(password));
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
this.#logPersistFailure("upgrade-on-login", error);
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
if (!upgraded)
|
|
680
|
+
return await this.#verifyCurrentCredential(userId, password);
|
|
681
|
+
try {
|
|
682
|
+
await this.#users.clearLegacyCredential?.(userId);
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
this.#logPersistFailure("upgrade-on-login", error);
|
|
686
|
+
}
|
|
687
|
+
await this.#emit({ type: "password.upgraded", userId, verifierId });
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
async #verifyCurrentCredential(userId, password) {
|
|
691
|
+
const current = await this.#users.getCredential(userId);
|
|
692
|
+
return current !== undefined &&
|
|
693
|
+
await this.#passwords.verify(password, current);
|
|
694
|
+
}
|
|
695
|
+
#logPersistFailure(operation, error) {
|
|
696
|
+
console.error(`[@udibo/oauth2] ${operation} persist failed:`, error instanceof Error ? error.message : error);
|
|
697
|
+
}
|
|
698
|
+
async #recordSignInFailure(userId, identifier) {
|
|
699
|
+
if (!this.#lockout)
|
|
700
|
+
return;
|
|
701
|
+
const result = await this.#lockout.recordFailure(userId);
|
|
702
|
+
if (!result.justLocked)
|
|
703
|
+
return;
|
|
704
|
+
await this.#emit({
|
|
705
|
+
type: "lockout",
|
|
706
|
+
userId,
|
|
707
|
+
identifier,
|
|
708
|
+
failures: result.failures,
|
|
709
|
+
lockedUntil: result.lockedUntil,
|
|
710
|
+
enforced: this.#enforce,
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
async #voidPasswordlessCredentials(tokens, subject, email) {
|
|
714
|
+
try {
|
|
715
|
+
await tokens.invalidate(TokenPurpose.SignIn, subject);
|
|
716
|
+
if (email)
|
|
717
|
+
await this.#otp?.invalidate(email, TokenPurpose.SignIn);
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
console.error("[@udibo/oauth2] resetPassword could not void the outstanding " +
|
|
721
|
+
"passwordless credentials; they stay usable until they expire:", error instanceof Error ? error.message : error);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
async #issueAndDeliver(tokens, request) {
|
|
725
|
+
const { token, expiresAt } = await tokens.create({
|
|
726
|
+
purpose: DELIVERY_PURPOSES[request.hook],
|
|
727
|
+
subject: request.subject,
|
|
728
|
+
data: request.data,
|
|
729
|
+
ttlMs: request.ttlMs,
|
|
730
|
+
invalidateExisting: true,
|
|
731
|
+
});
|
|
732
|
+
await this.#deliver(request.hook, request.to, token, request.subject, expiresAt);
|
|
733
|
+
await this.#emit(request.requested);
|
|
734
|
+
}
|
|
735
|
+
async #consumeToken(input) {
|
|
736
|
+
const { tokens, purpose, token } = input;
|
|
737
|
+
const status = await tokens.inspect(purpose, token);
|
|
738
|
+
if (status.status === "expired") {
|
|
739
|
+
await this.#emit(input.failed("expired"));
|
|
740
|
+
return { status: "expired" };
|
|
741
|
+
}
|
|
742
|
+
if (status.status !== "valid") {
|
|
743
|
+
await this.#emit(input.failed("invalid"));
|
|
744
|
+
return { status: "invalid" };
|
|
745
|
+
}
|
|
746
|
+
await input.beforeConsume?.(status.subject, status.data);
|
|
747
|
+
const resolved = await tokens.consume(purpose, token);
|
|
748
|
+
if (!resolved) {
|
|
749
|
+
await this.#emit(input.failed("invalid"));
|
|
750
|
+
return { status: "invalid" };
|
|
751
|
+
}
|
|
752
|
+
return { status: "success", resolved };
|
|
753
|
+
}
|
|
754
|
+
#emit(event) {
|
|
755
|
+
return dispatchIdentityEvent(this.#onEvent, "identity", event);
|
|
756
|
+
}
|
|
757
|
+
#requireTokens(method) {
|
|
758
|
+
if (!this.#tokens) {
|
|
759
|
+
throw new Error(`IdentityService.${method}() requires a \`tokens\` option`);
|
|
760
|
+
}
|
|
761
|
+
return this.#tokens;
|
|
762
|
+
}
|
|
763
|
+
#requireOtp(method) {
|
|
764
|
+
if (!this.#otp) {
|
|
765
|
+
throw new Error(`IdentityService.${method}() requires an \`otp\` option`);
|
|
766
|
+
}
|
|
767
|
+
return this.#otp;
|
|
768
|
+
}
|
|
769
|
+
async #deliver(hook, to, token, subject, expiresAt) {
|
|
770
|
+
const delivery = this.#delivery;
|
|
771
|
+
const send = delivery?.[hook];
|
|
772
|
+
if (!send)
|
|
773
|
+
return;
|
|
774
|
+
const url = this.#baseUrl
|
|
775
|
+
? URL_BUILDERS[hook](this.#baseUrl, token)
|
|
776
|
+
: undefined;
|
|
777
|
+
await this.#trapDeliveryFailure(hook, () => send.call(delivery, { to, url, token, subject, expiresAt }), async () => {
|
|
778
|
+
await this.#tokens?.consume(DELIVERY_PURPOSES[hook], token);
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
async #deliverCode(to, code, subject, expiresAt, codeId) {
|
|
782
|
+
const delivery = this.#delivery;
|
|
783
|
+
const send = delivery?.sendSignInCode;
|
|
784
|
+
if (!send)
|
|
785
|
+
return;
|
|
786
|
+
await this.#trapDeliveryFailure("sendSignInCode", () => send.call(delivery, { to, code, subject, expiresAt }), async () => {
|
|
787
|
+
await this.#otp?.invalidateCode(codeId);
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
async #trapDeliveryFailure(hook, send, invalidate) {
|
|
791
|
+
try {
|
|
792
|
+
await send();
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
797
|
+
const invalidated = await this.#invalidateAfterFailedDelivery(hook, invalidate, message);
|
|
798
|
+
await this.#emit({
|
|
799
|
+
type: "delivery.failed",
|
|
800
|
+
hook,
|
|
801
|
+
invalidated,
|
|
802
|
+
error: message,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
async #invalidateAfterFailedDelivery(hook, invalidate, message) {
|
|
807
|
+
try {
|
|
808
|
+
await invalidate();
|
|
809
|
+
console.error(`[@udibo/oauth2] delivery hook ${hook} failed; minted credential invalidated:`, message);
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
catch (error) {
|
|
813
|
+
console.error(`[@udibo/oauth2] delivery hook ${hook} failed AND the minted ` +
|
|
814
|
+
`credential could not be invalidated — it stays live until it ` +
|
|
815
|
+
`expires. Send failure:`, message, "Invalidation failure:", error instanceof Error ? error.message : error);
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const URL_BUILDERS = {
|
|
821
|
+
sendPasswordReset: buildResetUrl,
|
|
822
|
+
sendAccountUnlock: buildUnlockUrl,
|
|
823
|
+
sendEmailVerification: buildVerificationUrl,
|
|
824
|
+
sendSignInLink: buildSignInUrl,
|
|
825
|
+
};
|
|
826
|
+
const DELIVERY_PURPOSES = {
|
|
827
|
+
sendPasswordReset: TokenPurpose.PasswordReset,
|
|
828
|
+
sendAccountUnlock: TokenPurpose.AccountUnlock,
|
|
829
|
+
sendEmailVerification: TokenPurpose.EmailVerification,
|
|
830
|
+
sendSignInLink: TokenPurpose.SignIn,
|
|
831
|
+
};
|
|
832
|
+
function emailFromData(data) {
|
|
833
|
+
const email = data?.email;
|
|
834
|
+
return typeof email === "string" ? email : undefined;
|
|
835
|
+
}
|