@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,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session revocation + step-up contracts for the identity layer.
|
|
3
|
+
*
|
|
4
|
+
* Three security primitives the own-auth flows need, kept as **contracts + a
|
|
5
|
+
* pure helper** — storage stays app-owned (the `SessionStore` philosophy, and
|
|
6
|
+
* the Lucia-deprecation lesson: don't ship a DB-adapter zoo):
|
|
7
|
+
*
|
|
8
|
+
* - {@link RevocableSessionService} — revoke a user's sessions on a credential
|
|
9
|
+
* change. After a password reset the app must end the *other* sessions, or a
|
|
10
|
+
* compromised account's other devices stay logged in. The reset flow calls
|
|
11
|
+
* this; your app implements it over its session store.
|
|
12
|
+
* - {@link ListableSessionService} — the revocation seam's optional listing
|
|
13
|
+
* half: a user's live sessions as {@link SessionSummary} display rows, so a
|
|
14
|
+
* "where you're signed in" screen has one typed shape to render and wire the
|
|
15
|
+
* revoke buttons against. Only a stateful store can honestly implement it.
|
|
16
|
+
* - {@link isRecentlyAuthenticated} — a recent-auth / step-up gate for sensitive
|
|
17
|
+
* changes (change password/email, delete account, add MFA).
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Revocation operations a session store exposes so credential-change flows can
|
|
23
|
+
* end sessions. Implement over your own session storage (DB / Redis / KV).
|
|
24
|
+
*/
|
|
25
|
+
export interface RevocableSessionService {
|
|
26
|
+
/**
|
|
27
|
+
* Revoke **every** session belonging to `userId`. Call after a password reset
|
|
28
|
+
* or other full-account credential change. Returns the number revoked.
|
|
29
|
+
*/
|
|
30
|
+
revokeAllByUser(userId: string): Promise<number>;
|
|
31
|
+
/**
|
|
32
|
+
* Revoke a user's sessions **except** `keepSessionId` — "sign out everywhere
|
|
33
|
+
* else" after a password change in the current session. Returns the number
|
|
34
|
+
* revoked.
|
|
35
|
+
*/
|
|
36
|
+
revokeOthers(userId: string, keepSessionId: string): Promise<number>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* One live session as a "where you're signed in" screen renders it — a display
|
|
40
|
+
* projection, never the session's secret/hash or token material. The
|
|
41
|
+
* current-session marker is the caller's concern: compare
|
|
42
|
+
* {@link SessionSummary.id} against the id of the session backing the request
|
|
43
|
+
* being rendered, and label / disable "revoke" on the match ("revoke" on the
|
|
44
|
+
* current row is "sign out").
|
|
45
|
+
*
|
|
46
|
+
* Keep the optional client fields **coarse** — the contract omits raw address
|
|
47
|
+
* fields on purpose. `userAgent` is the raw header for a component to parse into
|
|
48
|
+
* a device label; `location` is a human-readable place (city / region /
|
|
49
|
+
* country) resolved from the IP at creation, not the raw address. Anything more
|
|
50
|
+
* precise turns a security screen into a tracking log; a store with nothing
|
|
51
|
+
* coarse to report leaves the field unset rather than reaching for the raw value.
|
|
52
|
+
*/
|
|
53
|
+
export interface SessionSummary {
|
|
54
|
+
/**
|
|
55
|
+
* Stable session id — the revoke action's target and the value a caller
|
|
56
|
+
* compares against the current session to mark "this device". Use a separate
|
|
57
|
+
* non-secret record identifier, never the cookie value or its hash.
|
|
58
|
+
*/
|
|
59
|
+
id: string;
|
|
60
|
+
/** When the session was created. */
|
|
61
|
+
createdAt: Date;
|
|
62
|
+
/**
|
|
63
|
+
* Last-activity timestamp. Bump it on a throttle (once a minute, say), not on
|
|
64
|
+
* every request, or the session table becomes the hottest write path.
|
|
65
|
+
*/
|
|
66
|
+
lastSeenAt: Date;
|
|
67
|
+
/** Raw user-agent header, for the UI to summarize into a device label. */
|
|
68
|
+
userAgent?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Coarse, human-readable location resolved from the IP at creation (e.g.
|
|
71
|
+
* `"San Francisco, US"`) — never the raw address.
|
|
72
|
+
*/
|
|
73
|
+
location?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Optional {@link RevocableSessionService} capability: list a user's live
|
|
77
|
+
* sessions as {@link SessionSummary} rows so the "where you're signed in"
|
|
78
|
+
* screen you build renders one typed shape and wires its revoke buttons against
|
|
79
|
+
* it, instead of every app inventing a different list shape. The screen is
|
|
80
|
+
* app-owned — the package ships this seam and the row type, not a list
|
|
81
|
+
* component. A store opts in by implementing this method — it is duck-typed at
|
|
82
|
+
* runtime via
|
|
83
|
+
* {@link supportsSessionListing}, mirroring how the BFF's `BackchannelLogoutStore`
|
|
84
|
+
* is an optional capability a stateful `SessionStore` opts into.
|
|
85
|
+
*
|
|
86
|
+
* Only a store that keeps a server-side record can implement it: a listing seam
|
|
87
|
+
* implies the rows are authoritative, and the paired revoke buttons promise
|
|
88
|
+
* **immediate, not advisory** revocation. A stateless self-contained-token store
|
|
89
|
+
* the server never looks up can neither enumerate its sessions nor honor a
|
|
90
|
+
* revoke, so it omits this and the screen simply isn't offered.
|
|
91
|
+
*/
|
|
92
|
+
export interface ListableSessionService {
|
|
93
|
+
/**
|
|
94
|
+
* A user's live sessions as display summaries, most-recently-active first.
|
|
95
|
+
* Return only what a session screen renders — never the session secret/hash
|
|
96
|
+
* or token material. Excludes ended sessions (revoked, expired, idle-timed-out).
|
|
97
|
+
*/
|
|
98
|
+
listByUser(userId: string): Promise<SessionSummary[]>;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Whether a session service also implements the optional
|
|
102
|
+
* {@link ListableSessionService} listing capability. Use it to decide whether to
|
|
103
|
+
* offer the "where you're signed in" screen: a stateful store returns `true`, a
|
|
104
|
+
* stateless one that only revokes returns `false`.
|
|
105
|
+
*/
|
|
106
|
+
export declare function supportsSessionListing(service: RevocableSessionService): service is RevocableSessionService & ListableSessionService;
|
|
107
|
+
/**
|
|
108
|
+
* Whether a session authenticated within `maxAgeMs` — a recent-auth ("step-up")
|
|
109
|
+
* check to gate sensitive actions. Re-prompt for credentials when this is false.
|
|
110
|
+
*
|
|
111
|
+
* @param authenticatedAt When the session last actively authenticated (epoch ms).
|
|
112
|
+
* @param maxAgeMs How recent counts as "fresh" (e.g. 5 minutes).
|
|
113
|
+
* @param now Override the clock (for tests). Defaults to `Date.now()`.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* if (!isRecentlyAuthenticated(session.authenticatedAt, 5 * 60_000)) {
|
|
118
|
+
* throw redirect("/reauthenticate?return_to=/settings/password");
|
|
119
|
+
* }
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export declare function isRecentlyAuthenticated(authenticatedAt: number, maxAgeMs: number, now?: number): boolean;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session revocation + step-up contracts for the identity layer.
|
|
3
|
+
*
|
|
4
|
+
* Three security primitives the own-auth flows need, kept as **contracts + a
|
|
5
|
+
* pure helper** — storage stays app-owned (the `SessionStore` philosophy, and
|
|
6
|
+
* the Lucia-deprecation lesson: don't ship a DB-adapter zoo):
|
|
7
|
+
*
|
|
8
|
+
* - {@link RevocableSessionService} — revoke a user's sessions on a credential
|
|
9
|
+
* change. After a password reset the app must end the *other* sessions, or a
|
|
10
|
+
* compromised account's other devices stay logged in. The reset flow calls
|
|
11
|
+
* this; your app implements it over its session store.
|
|
12
|
+
* - {@link ListableSessionService} — the revocation seam's optional listing
|
|
13
|
+
* half: a user's live sessions as {@link SessionSummary} display rows, so a
|
|
14
|
+
* "where you're signed in" screen has one typed shape to render and wire the
|
|
15
|
+
* revoke buttons against. Only a stateful store can honestly implement it.
|
|
16
|
+
* - {@link isRecentlyAuthenticated} — a recent-auth / step-up gate for sensitive
|
|
17
|
+
* changes (change password/email, delete account, add MFA).
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Whether a session service also implements the optional
|
|
23
|
+
* {@link ListableSessionService} listing capability. Use it to decide whether to
|
|
24
|
+
* offer the "where you're signed in" screen: a stateful store returns `true`, a
|
|
25
|
+
* stateless one that only revokes returns `false`.
|
|
26
|
+
*/
|
|
27
|
+
export function supportsSessionListing(service) {
|
|
28
|
+
return typeof service.listByUser ===
|
|
29
|
+
"function";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether a session authenticated within `maxAgeMs` — a recent-auth ("step-up")
|
|
33
|
+
* check to gate sensitive actions. Re-prompt for credentials when this is false.
|
|
34
|
+
*
|
|
35
|
+
* @param authenticatedAt When the session last actively authenticated (epoch ms).
|
|
36
|
+
* @param maxAgeMs How recent counts as "fresh" (e.g. 5 minutes).
|
|
37
|
+
* @param now Override the clock (for tests). Defaults to `Date.now()`.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* if (!isRecentlyAuthenticated(session.authenticatedAt, 5 * 60_000)) {
|
|
42
|
+
* throw redirect("/reauthenticate?return_to=/settings/password");
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function isRecentlyAuthenticated(authenticatedAt, maxAgeMs, now = Date.now()) {
|
|
47
|
+
return authenticatedAt > 0 && now - authenticatedAt <= maxAgeMs;
|
|
48
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One token-flow primitive for the single-use, expiring links an identity layer
|
|
3
|
+
* needs — **email verification** and **password reset** are the same mechanics:
|
|
4
|
+
* mint a high-entropy token, store only its hash, hand the raw token to the app
|
|
5
|
+
* to deliver, then validate / single-use-consume it later.
|
|
6
|
+
*
|
|
7
|
+
* The app reimplements this once per flow today (random bytes → sha256 → store
|
|
8
|
+
* with expiry → consume). This collapses it into one tested, enumeration-safe
|
|
9
|
+
* building block. Storage is an app-owned {@link TokenFlowStore} (a
|
|
10
|
+
* {@link MemoryTokenFlowStore} ships for dev/tests); the package owns the
|
|
11
|
+
* lifecycle, not the database.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
/** Common token purposes. `purpose` is a free string; these are conventions. */
|
|
16
|
+
export declare const TokenPurpose: {
|
|
17
|
+
readonly EmailVerification: "email-verification";
|
|
18
|
+
readonly PasswordReset: "password-reset";
|
|
19
|
+
readonly AccountUnlock: "account-unlock";
|
|
20
|
+
readonly SignIn: "signin";
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* A stored token record. Holds the token's **hash**, never the raw token — a
|
|
24
|
+
* leaked record (or log line) can't be replayed as a working link.
|
|
25
|
+
*/
|
|
26
|
+
export interface TokenFlowRecord {
|
|
27
|
+
/** sha256-hex of the raw token; the storage key and at-rest form. */
|
|
28
|
+
tokenHash: string;
|
|
29
|
+
/** What the token authorizes (e.g. {@link TokenPurpose.PasswordReset}). */
|
|
30
|
+
purpose: string;
|
|
31
|
+
/** The subject the token acts on (typically a user id). */
|
|
32
|
+
subject: string;
|
|
33
|
+
/** Optional extra payload (e.g. the email being verified). */
|
|
34
|
+
data?: Record<string, unknown>;
|
|
35
|
+
/** Expiry, epoch ms. */
|
|
36
|
+
expiresAt: number;
|
|
37
|
+
/** When consumed (epoch ms), or `undefined` while still usable. */
|
|
38
|
+
consumedAt?: number;
|
|
39
|
+
/** Created, epoch ms. */
|
|
40
|
+
createdAt: number;
|
|
41
|
+
}
|
|
42
|
+
/** App-owned persistence for {@link TokenFlowService}, keyed by `tokenHash`. */
|
|
43
|
+
export interface TokenFlowStore {
|
|
44
|
+
/** Persist a new record. */
|
|
45
|
+
save(record: TokenFlowRecord): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Look up a record by token hash, or `null` when no record was ever stored
|
|
48
|
+
* under it.
|
|
49
|
+
*
|
|
50
|
+
* Return the record **whatever its state** — expired and already-consumed
|
|
51
|
+
* records included. {@link TokenFlowService.inspect} classifies them
|
|
52
|
+
* (`expired` vs `consumed` vs `invalid`) after this read, so a store that
|
|
53
|
+
* filters them out in SQL (`... AND consumed_at IS NULL AND expires_at >
|
|
54
|
+
* now()`) collapses both into `invalid` and costs the caller the message it
|
|
55
|
+
* would otherwise show.
|
|
56
|
+
*/
|
|
57
|
+
get(tokenHash: string): Promise<TokenFlowRecord | null>;
|
|
58
|
+
/**
|
|
59
|
+
* Atomically claim a record as consumed (single-use): stamp `consumedAt` and
|
|
60
|
+
* return `true` only when the record existed and was still unconsumed
|
|
61
|
+
* (`UPDATE … SET consumed_at = $now WHERE token_hash = $hash AND consumed_at
|
|
62
|
+
* IS NULL`, checking the row count). Return `false` without changing anything
|
|
63
|
+
* otherwise.
|
|
64
|
+
*
|
|
65
|
+
* This **must** be a conditional write, not a read-then-write.
|
|
66
|
+
* {@link TokenFlowService.consume} reads the record before calling this and
|
|
67
|
+
* treats a `false` as "someone else got it", which is the only thing making a
|
|
68
|
+
* link single-use under concurrent redemption — an unconditional overwrite
|
|
69
|
+
* that always reports success lets two requests racing one link both proceed.
|
|
70
|
+
*/
|
|
71
|
+
markConsumed(tokenHash: string, consumedAt: number): Promise<boolean>;
|
|
72
|
+
/**
|
|
73
|
+
* Optional: delete every still-pending token for a `(purpose, subject)` — e.g.
|
|
74
|
+
* invalidate outstanding reset links once one succeeds, or when a new one is
|
|
75
|
+
* issued. Implement it to harden the flow; the service calls it only if present.
|
|
76
|
+
*
|
|
77
|
+
* Delete the **pending** records only. Leaving the consumed ones keeps
|
|
78
|
+
* {@link TokenFlowService.inspect} able to answer `consumed` for a token that
|
|
79
|
+
* really was used, instead of the generic `invalid`.
|
|
80
|
+
*/
|
|
81
|
+
deleteBySubject?(purpose: string, subject: string): Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
/** Options for {@link TokenFlowService.create}. */
|
|
84
|
+
export interface CreateTokenOptions {
|
|
85
|
+
/** What the token authorizes (e.g. {@link TokenPurpose.PasswordReset}). */
|
|
86
|
+
purpose: string;
|
|
87
|
+
/** The subject the token acts on (typically a user id). */
|
|
88
|
+
subject: string;
|
|
89
|
+
/** Optional extra payload to carry on the record (e.g. the email being verified). */
|
|
90
|
+
data?: Record<string, unknown>;
|
|
91
|
+
/** Lifetime in milliseconds. */
|
|
92
|
+
ttlMs: number;
|
|
93
|
+
/**
|
|
94
|
+
* Invalidate any existing pending tokens for this `(purpose, subject)` first
|
|
95
|
+
* (requires {@link TokenFlowStore.deleteBySubject}). Defaults to `false`.
|
|
96
|
+
*/
|
|
97
|
+
invalidateExisting?: boolean;
|
|
98
|
+
}
|
|
99
|
+
/** The raw token to deliver, available only at creation time. */
|
|
100
|
+
export interface CreatedToken {
|
|
101
|
+
/** The raw token — send it to the user; it is never stored or recoverable. */
|
|
102
|
+
token: string;
|
|
103
|
+
/** When the token expires, epoch ms. */
|
|
104
|
+
expiresAt: number;
|
|
105
|
+
}
|
|
106
|
+
/** The result of validating/consuming a token. */
|
|
107
|
+
export interface ResolvedToken {
|
|
108
|
+
/** The subject the token acts on (typically a user id). */
|
|
109
|
+
subject: string;
|
|
110
|
+
/** The extra payload supplied at creation, if any. */
|
|
111
|
+
data?: Record<string, unknown>;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The detailed outcome of {@link TokenFlowService.inspect}. Unlike the
|
|
115
|
+
* `null`-returning {@link TokenFlowService.validate}/{@link TokenFlowService.consume},
|
|
116
|
+
* this distinguishes **why** a token failed — so a verify/reset UI can say
|
|
117
|
+
* "this link has expired, request a new one" vs. a generic error. (A 32-byte
|
|
118
|
+
* random token isn't enumerable, so reporting `expired` vs `invalid` for the
|
|
119
|
+
* token is not a meaningful leak — distinct from email enumeration.)
|
|
120
|
+
*/
|
|
121
|
+
export type TokenStatus = {
|
|
122
|
+
status: "valid";
|
|
123
|
+
subject: string;
|
|
124
|
+
data?: Record<string, unknown>;
|
|
125
|
+
} | {
|
|
126
|
+
status: "invalid";
|
|
127
|
+
} | {
|
|
128
|
+
status: "expired";
|
|
129
|
+
} | {
|
|
130
|
+
status: "consumed";
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Issues, validates, and single-use-consumes purpose-scoped tokens.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* const tokens = new TokenFlowService(store);
|
|
138
|
+
* const { token } = await tokens.create({
|
|
139
|
+
* purpose: TokenPurpose.PasswordReset, subject: user.id, ttlMs: 3_600_000,
|
|
140
|
+
* });
|
|
141
|
+
* // …email a link containing `token`…
|
|
142
|
+
* const resolved = await tokens.consume(TokenPurpose.PasswordReset, token);
|
|
143
|
+
* if (resolved) await resetPassword(resolved.subject, newPassword);
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export declare class TokenFlowService {
|
|
147
|
+
#private;
|
|
148
|
+
/** Wraps an app-owned {@linkcode TokenFlowStore} that persists the token records. */
|
|
149
|
+
constructor(store: TokenFlowStore);
|
|
150
|
+
/**
|
|
151
|
+
* Drop every pending token for a `(purpose, subject)` without redeeming one —
|
|
152
|
+
* e.g. voiding outstanding sign-in links when the user resets their password,
|
|
153
|
+
* so a link an attacker already has stops working.
|
|
154
|
+
*
|
|
155
|
+
* Best-effort by design: {@link TokenFlowStore.deleteBySubject} is optional.
|
|
156
|
+
* Returns `true` when the store implements it and the deletion ran, and
|
|
157
|
+
* `false` when the store does not, in which case those tokens stay redeemable
|
|
158
|
+
* until they expire — check the result if that gap matters to you. A store
|
|
159
|
+
* that throws propagates the error.
|
|
160
|
+
*/
|
|
161
|
+
invalidate(purpose: string, subject: string): Promise<boolean>;
|
|
162
|
+
/** Mint a token; stores its hash and returns the raw token (once). */
|
|
163
|
+
create(opts: CreateTokenOptions): Promise<CreatedToken>;
|
|
164
|
+
/**
|
|
165
|
+
* Validate a token **without** consuming it (e.g. to render a reset form
|
|
166
|
+
* before the user submits). Returns the subject/data, or `null` if the token
|
|
167
|
+
* is unknown, wrong-purpose, expired, or already consumed — never reveals which.
|
|
168
|
+
*/
|
|
169
|
+
validate(purpose: string, token: string): Promise<ResolvedToken | null>;
|
|
170
|
+
/**
|
|
171
|
+
* Validate **and** consume a token (single-use). Returns the subject/data, or
|
|
172
|
+
* `null` (see {@link validate} for the failure cases). A second `consume` of
|
|
173
|
+
* the same token returns `null` — including when the second one races the
|
|
174
|
+
* first, since only the caller whose {@link TokenFlowStore.markConsumed}
|
|
175
|
+
* claimed the record is handed the token.
|
|
176
|
+
*/
|
|
177
|
+
consume(purpose: string, token: string): Promise<ResolvedToken | null>;
|
|
178
|
+
/**
|
|
179
|
+
* Inspect a token **without** consuming it, reporting the detailed
|
|
180
|
+
* {@link TokenStatus} — `valid` (with subject/data), `invalid` (unknown or
|
|
181
|
+
* wrong-purpose), `expired`, or `consumed`. Use this when the UI needs to tell
|
|
182
|
+
* "expired" apart from "invalid" (e.g. an email-verification page that offers
|
|
183
|
+
* to resend). {@link validate} is the `null`-returning shorthand.
|
|
184
|
+
*/
|
|
185
|
+
inspect(purpose: string, token: string): Promise<TokenStatus>;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* In-memory {@link TokenFlowStore} for development and tests. Records are lost
|
|
189
|
+
* on restart; back the contract with a database for production.
|
|
190
|
+
*
|
|
191
|
+
* Nothing is reclaimed: a consumed record is kept so
|
|
192
|
+
* {@link TokenFlowService.inspect} can still answer `consumed`, and no sweep
|
|
193
|
+
* ever removes it, so the map grows by one entry per token minted. That is
|
|
194
|
+
* fine for a fixture and wrong in a database-backed port — give the rows a TTL
|
|
195
|
+
* or a purge job that drops records already past `expiresAt`. Purge only
|
|
196
|
+
* records that are *also* consumed: dropping an expired-but-unconsumed record
|
|
197
|
+
* collapses `expired` into `invalid` and loses the distinction `inspect`
|
|
198
|
+
* reports.
|
|
199
|
+
*/
|
|
200
|
+
export declare class MemoryTokenFlowStore implements TokenFlowStore {
|
|
201
|
+
#private;
|
|
202
|
+
/** Persist a record, keyed by its token hash. */
|
|
203
|
+
save(record: TokenFlowRecord): Promise<void>;
|
|
204
|
+
/** Return the record for a token hash, or `null` if none is stored. */
|
|
205
|
+
get(tokenHash: string): Promise<TokenFlowRecord | null>;
|
|
206
|
+
/** Claim the record if still unconsumed; `false` when unknown or already spent. */
|
|
207
|
+
markConsumed(tokenHash: string, consumedAt: number): Promise<boolean>;
|
|
208
|
+
/**
|
|
209
|
+
* Drop every still-pending record matching the given `(purpose, subject)`.
|
|
210
|
+
* Already-consumed records are kept so {@link TokenFlowService.inspect} can
|
|
211
|
+
* still report `consumed` rather than the generic `invalid`.
|
|
212
|
+
*/
|
|
213
|
+
deleteBySubject(purpose: string, subject: string): Promise<void>;
|
|
214
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One token-flow primitive for the single-use, expiring links an identity layer
|
|
3
|
+
* needs — **email verification** and **password reset** are the same mechanics:
|
|
4
|
+
* mint a high-entropy token, store only its hash, hand the raw token to the app
|
|
5
|
+
* to deliver, then validate / single-use-consume it later.
|
|
6
|
+
*
|
|
7
|
+
* The app reimplements this once per flow today (random bytes → sha256 → store
|
|
8
|
+
* with expiry → consume). This collapses it into one tested, enumeration-safe
|
|
9
|
+
* building block. Storage is an app-owned {@link TokenFlowStore} (a
|
|
10
|
+
* {@link MemoryTokenFlowStore} ships for dev/tests); the package owns the
|
|
11
|
+
* lifecycle, not the database.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { sha256Hash } from "../server/utils/hash.js";
|
|
16
|
+
import { randomToken } from "../utils/crypto.js";
|
|
17
|
+
/** Common token purposes. `purpose` is a free string; these are conventions. */
|
|
18
|
+
export const TokenPurpose = {
|
|
19
|
+
EmailVerification: "email-verification",
|
|
20
|
+
PasswordReset: "password-reset",
|
|
21
|
+
AccountUnlock: "account-unlock",
|
|
22
|
+
SignIn: "signin",
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Issues, validates, and single-use-consumes purpose-scoped tokens.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const tokens = new TokenFlowService(store);
|
|
30
|
+
* const { token } = await tokens.create({
|
|
31
|
+
* purpose: TokenPurpose.PasswordReset, subject: user.id, ttlMs: 3_600_000,
|
|
32
|
+
* });
|
|
33
|
+
* // …email a link containing `token`…
|
|
34
|
+
* const resolved = await tokens.consume(TokenPurpose.PasswordReset, token);
|
|
35
|
+
* if (resolved) await resetPassword(resolved.subject, newPassword);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export class TokenFlowService {
|
|
39
|
+
#store;
|
|
40
|
+
/** Wraps an app-owned {@linkcode TokenFlowStore} that persists the token records. */
|
|
41
|
+
constructor(store) {
|
|
42
|
+
this.#store = store;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Drop every pending token for a `(purpose, subject)` without redeeming one —
|
|
46
|
+
* e.g. voiding outstanding sign-in links when the user resets their password,
|
|
47
|
+
* so a link an attacker already has stops working.
|
|
48
|
+
*
|
|
49
|
+
* Best-effort by design: {@link TokenFlowStore.deleteBySubject} is optional.
|
|
50
|
+
* Returns `true` when the store implements it and the deletion ran, and
|
|
51
|
+
* `false` when the store does not, in which case those tokens stay redeemable
|
|
52
|
+
* until they expire — check the result if that gap matters to you. A store
|
|
53
|
+
* that throws propagates the error.
|
|
54
|
+
*/
|
|
55
|
+
async invalidate(purpose, subject) {
|
|
56
|
+
if (!this.#store.deleteBySubject)
|
|
57
|
+
return false;
|
|
58
|
+
await this.#store.deleteBySubject(purpose, subject);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
/** Mint a token; stores its hash and returns the raw token (once). */
|
|
62
|
+
async create(opts) {
|
|
63
|
+
if (opts.invalidateExisting) {
|
|
64
|
+
await this.invalidate(opts.purpose, opts.subject);
|
|
65
|
+
}
|
|
66
|
+
const token = randomToken();
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const expiresAt = now + opts.ttlMs;
|
|
69
|
+
await this.#store.save({
|
|
70
|
+
tokenHash: await sha256Hash(token),
|
|
71
|
+
purpose: opts.purpose,
|
|
72
|
+
subject: opts.subject,
|
|
73
|
+
data: opts.data,
|
|
74
|
+
expiresAt,
|
|
75
|
+
createdAt: now,
|
|
76
|
+
});
|
|
77
|
+
return { token, expiresAt };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Validate a token **without** consuming it (e.g. to render a reset form
|
|
81
|
+
* before the user submits). Returns the subject/data, or `null` if the token
|
|
82
|
+
* is unknown, wrong-purpose, expired, or already consumed — never reveals which.
|
|
83
|
+
*/
|
|
84
|
+
async validate(purpose, token) {
|
|
85
|
+
const status = await this.inspect(purpose, token);
|
|
86
|
+
return status.status === "valid"
|
|
87
|
+
? { subject: status.subject, data: status.data }
|
|
88
|
+
: null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Validate **and** consume a token (single-use). Returns the subject/data, or
|
|
92
|
+
* `null` (see {@link validate} for the failure cases). A second `consume` of
|
|
93
|
+
* the same token returns `null` — including when the second one races the
|
|
94
|
+
* first, since only the caller whose {@link TokenFlowStore.markConsumed}
|
|
95
|
+
* claimed the record is handed the token.
|
|
96
|
+
*/
|
|
97
|
+
async consume(purpose, token) {
|
|
98
|
+
const tokenHash = await sha256Hash(token);
|
|
99
|
+
const status = this.#statusOf(await this.#store.get(tokenHash), purpose);
|
|
100
|
+
if (status.status !== "valid")
|
|
101
|
+
return null;
|
|
102
|
+
if (!await this.#store.markConsumed(tokenHash, Date.now()))
|
|
103
|
+
return null;
|
|
104
|
+
return { subject: status.subject, data: status.data };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Inspect a token **without** consuming it, reporting the detailed
|
|
108
|
+
* {@link TokenStatus} — `valid` (with subject/data), `invalid` (unknown or
|
|
109
|
+
* wrong-purpose), `expired`, or `consumed`. Use this when the UI needs to tell
|
|
110
|
+
* "expired" apart from "invalid" (e.g. an email-verification page that offers
|
|
111
|
+
* to resend). {@link validate} is the `null`-returning shorthand.
|
|
112
|
+
*/
|
|
113
|
+
async inspect(purpose, token) {
|
|
114
|
+
return this.#statusOf(await this.#store.get(await sha256Hash(token)), purpose);
|
|
115
|
+
}
|
|
116
|
+
#statusOf(record, purpose) {
|
|
117
|
+
if (!record || record.purpose !== purpose)
|
|
118
|
+
return { status: "invalid" };
|
|
119
|
+
if (record.consumedAt !== undefined)
|
|
120
|
+
return { status: "consumed" };
|
|
121
|
+
if (record.expiresAt <= Date.now())
|
|
122
|
+
return { status: "expired" };
|
|
123
|
+
return { status: "valid", subject: record.subject, data: record.data };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* In-memory {@link TokenFlowStore} for development and tests. Records are lost
|
|
128
|
+
* on restart; back the contract with a database for production.
|
|
129
|
+
*
|
|
130
|
+
* Nothing is reclaimed: a consumed record is kept so
|
|
131
|
+
* {@link TokenFlowService.inspect} can still answer `consumed`, and no sweep
|
|
132
|
+
* ever removes it, so the map grows by one entry per token minted. That is
|
|
133
|
+
* fine for a fixture and wrong in a database-backed port — give the rows a TTL
|
|
134
|
+
* or a purge job that drops records already past `expiresAt`. Purge only
|
|
135
|
+
* records that are *also* consumed: dropping an expired-but-unconsumed record
|
|
136
|
+
* collapses `expired` into `invalid` and loses the distinction `inspect`
|
|
137
|
+
* reports.
|
|
138
|
+
*/
|
|
139
|
+
export class MemoryTokenFlowStore {
|
|
140
|
+
#byHash = new Map();
|
|
141
|
+
/** Persist a record, keyed by its token hash. */
|
|
142
|
+
save(record) {
|
|
143
|
+
this.#byHash.set(record.tokenHash, record);
|
|
144
|
+
return Promise.resolve();
|
|
145
|
+
}
|
|
146
|
+
/** Return the record for a token hash, or `null` if none is stored. */
|
|
147
|
+
get(tokenHash) {
|
|
148
|
+
return Promise.resolve(this.#byHash.get(tokenHash) ?? null);
|
|
149
|
+
}
|
|
150
|
+
/** Claim the record if still unconsumed; `false` when unknown or already spent. */
|
|
151
|
+
markConsumed(tokenHash, consumedAt) {
|
|
152
|
+
const record = this.#byHash.get(tokenHash);
|
|
153
|
+
if (!record || record.consumedAt !== undefined) {
|
|
154
|
+
return Promise.resolve(false);
|
|
155
|
+
}
|
|
156
|
+
record.consumedAt = consumedAt;
|
|
157
|
+
return Promise.resolve(true);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Drop every still-pending record matching the given `(purpose, subject)`.
|
|
161
|
+
* Already-consumed records are kept so {@link TokenFlowService.inspect} can
|
|
162
|
+
* still report `consumed` rather than the generic `invalid`.
|
|
163
|
+
*/
|
|
164
|
+
deleteBySubject(purpose, subject) {
|
|
165
|
+
for (const [hash, record] of this.#byHash) {
|
|
166
|
+
if (record.purpose === purpose && record.subject === subject &&
|
|
167
|
+
record.consumedAt === undefined) {
|
|
168
|
+
this.#byHash.delete(hash);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return Promise.resolve();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verified authentication claims for one event, supplied by application policy.
|
|
3
|
+
*
|
|
4
|
+
* Capture this context at authorization-code issuance and persist it on codes
|
|
5
|
+
* and tokens. Refresh rotations retain the original event. Omit unknown claims;
|
|
6
|
+
* neither a requested assurance level nor token issuance time is evidence.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const event: AuthenticationContext = {
|
|
11
|
+
* auth_time: 1_700_000_000,
|
|
12
|
+
* acr: "urn:example:password",
|
|
13
|
+
* amr: ["pwd"],
|
|
14
|
+
* };
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export interface AuthenticationContext {
|
|
18
|
+
/** Verified authentication time in Unix seconds, not token issuance time. */
|
|
19
|
+
readonly auth_time?: number;
|
|
20
|
+
/** Achieved authentication context, never the requested preference. */
|
|
21
|
+
readonly acr?: string;
|
|
22
|
+
/** Truthful authentication methods; omitted when the complete set is unknown. */
|
|
23
|
+
readonly amr?: readonly string[];
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AbstractScope, BasicScope } from "./scope.js";
|
|
2
|
+
import type { ClientInterface } from "./client.js";
|
|
3
|
+
import type { AuthenticationContext } from "./authentication.js";
|
|
4
|
+
/**
|
|
5
|
+
* Authorization code model.
|
|
6
|
+
*
|
|
7
|
+
* Represents an authorization code issued by the authorization server
|
|
8
|
+
* after the resource owner grants authorization. The code is exchanged
|
|
9
|
+
* for tokens at the token endpoint.
|
|
10
|
+
*/
|
|
11
|
+
export interface AuthorizationCode<Client extends ClientInterface, User, Scope extends AbstractScope = BasicScope> {
|
|
12
|
+
/** The authorization code string. */
|
|
13
|
+
code: string;
|
|
14
|
+
/** When the code expires. */
|
|
15
|
+
expiresAt: Date;
|
|
16
|
+
/** The client that requested authorization. */
|
|
17
|
+
client: Client;
|
|
18
|
+
/** The user who authorized the request. */
|
|
19
|
+
user: User;
|
|
20
|
+
/** The scope that was authorized. */
|
|
21
|
+
scope?: Scope | null;
|
|
22
|
+
/** The redirect URI used in the authorization request. */
|
|
23
|
+
redirectUri?: string;
|
|
24
|
+
/** PKCE code challenge. */
|
|
25
|
+
challenge?: string;
|
|
26
|
+
/** PKCE challenge method (S256 or plain). */
|
|
27
|
+
challengeMethod?: string;
|
|
28
|
+
/** OIDC nonce from the authorization request, echoed into the id_token. */
|
|
29
|
+
nonce?: string;
|
|
30
|
+
/** Verified event captured at issuance; code stores must persist and restore it. */
|
|
31
|
+
authenticationContext?: AuthenticationContext;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parameters from an authorization request.
|
|
35
|
+
*
|
|
36
|
+
* Parsed from the query string of an authorization endpoint request.
|
|
37
|
+
*
|
|
38
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
|
|
39
|
+
*/
|
|
40
|
+
export interface AuthorizeParameters {
|
|
41
|
+
/** Raw space-delimited ACR preferences; application policy must validate and honor them. */
|
|
42
|
+
acrValues?: string;
|
|
43
|
+
/** Raw maximum authentication age in seconds; application policy must validate and enforce it. */
|
|
44
|
+
maxAge?: string;
|
|
45
|
+
/** Raw prompt values; application policy must honor or refuse every supported control. */
|
|
46
|
+
prompt?: string;
|
|
47
|
+
/** Must be "code" for authorization code flow. */
|
|
48
|
+
responseType?: string;
|
|
49
|
+
/** Client identifier issued to the client. */
|
|
50
|
+
clientId?: string;
|
|
51
|
+
/** URI to redirect to after authorization. */
|
|
52
|
+
redirectUri?: string;
|
|
53
|
+
/** Opaque state value for CSRF protection. */
|
|
54
|
+
state?: string;
|
|
55
|
+
/** Space-delimited list of requested scopes. */
|
|
56
|
+
scope?: string;
|
|
57
|
+
/** PKCE code challenge (RFC 7636). */
|
|
58
|
+
challenge?: string;
|
|
59
|
+
/** PKCE code challenge method ("S256" or "plain"). */
|
|
60
|
+
challengeMethod?: string;
|
|
61
|
+
/** OIDC nonce (echoed into the id_token when `openid` is granted). */
|
|
62
|
+
nonce?: string;
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|