@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,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session stores used by {@link HonoBff}.
|
|
3
|
+
*
|
|
4
|
+
* Two shapes are supported:
|
|
5
|
+
*
|
|
6
|
+
* - **Stateful**: the cookie holds a random session id; tokens live in a
|
|
7
|
+
* server-side store keyed by that id ({@link MemorySessionStore}, or
|
|
8
|
+
* your own DB-backed {@link SessionStore}). Pros: instant revocation,
|
|
9
|
+
* tiny cookie, leaked session id is useless outside your server. Cons:
|
|
10
|
+
* needs a store to scale horizontally.
|
|
11
|
+
*
|
|
12
|
+
* - **Stateless** ({@link EncryptedCookieSessionStore}): the cookie value
|
|
13
|
+
* *is* the encrypted token bundle (AES-GCM with a server-held secret),
|
|
14
|
+
* no server-side storage needed. Pros: no infrastructure beyond the
|
|
15
|
+
* secret. Cons: revocation requires `/revoke` + cookie clear, cookie
|
|
16
|
+
* grows with token size, key rotation needs care.
|
|
17
|
+
*
|
|
18
|
+
* A raw "token in cookie" store is deliberately **not** shipped: a leaked
|
|
19
|
+
* log line containing the cookie would yield a working bearer token. The
|
|
20
|
+
* AES-GCM encryption in the stateless store is what makes that pattern
|
|
21
|
+
* safe.
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import type { TokenBundle, UserInfoClaims } from "../../../client/mod.js";
|
|
26
|
+
/** The data a BFF persists per session. */
|
|
27
|
+
export interface SessionData {
|
|
28
|
+
/** The current token bundle (access token + expiry + scope + id_token). */
|
|
29
|
+
tokens: TokenBundle;
|
|
30
|
+
/** The refresh token, if one was issued. */
|
|
31
|
+
refreshToken?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Cached user claims surfaced by `GET /auth/session` without exposing
|
|
34
|
+
* tokens to the browser. Populated from the id_token or the userinfo
|
|
35
|
+
* endpoint at callback time.
|
|
36
|
+
*/
|
|
37
|
+
user?: UserInfoClaims | null;
|
|
38
|
+
/**
|
|
39
|
+
* The OP session id (`sid` claim from the id_token), when present. Used to
|
|
40
|
+
* target a single session during OIDC Back-Channel Logout; matching falls
|
|
41
|
+
* back to the subject (`user.sub`) when the logout_token carries no `sid`.
|
|
42
|
+
*/
|
|
43
|
+
sid?: string;
|
|
44
|
+
/** When the session was created (epoch ms). */
|
|
45
|
+
createdAt: number;
|
|
46
|
+
/** When the session was last updated (epoch ms). */
|
|
47
|
+
updatedAt: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Optional {@link SessionStore} capability: destroy sessions by subject and/or
|
|
51
|
+
* OP session id, for an OIDC Back-Channel Logout receiver. A store opts in by
|
|
52
|
+
* implementing this method (it is duck-typed at runtime). Stateful stores
|
|
53
|
+
* (server-side records) can; stateless stores (e.g.
|
|
54
|
+
* {@link EncryptedCookieSessionStore}) cannot enumerate their sessions, so they
|
|
55
|
+
* omit it and the BFF's `/auth/backchannel` route stays unavailable.
|
|
56
|
+
*/
|
|
57
|
+
export interface BackchannelLogoutStore {
|
|
58
|
+
/**
|
|
59
|
+
* Destroy every session matching a validated logout_token. When `sid` is
|
|
60
|
+
* given, only the session(s) with that OP session id are destroyed; otherwise
|
|
61
|
+
* all sessions for `sub` are destroyed. Returns the number destroyed. Back a
|
|
62
|
+
* persistent implementation with an index on `sid`/`sub`.
|
|
63
|
+
*/
|
|
64
|
+
destroyByLogout(criteria: {
|
|
65
|
+
sub?: string;
|
|
66
|
+
sid?: string;
|
|
67
|
+
}): Promise<number>;
|
|
68
|
+
}
|
|
69
|
+
/** Whether a store implements the {@link BackchannelLogoutStore} capability. */
|
|
70
|
+
export declare function supportsBackchannelLogout(store: SessionStore): store is SessionStore & BackchannelLogoutStore;
|
|
71
|
+
/**
|
|
72
|
+
* Default session lifetime — 14 days — shared by
|
|
73
|
+
* {@link EncryptedCookieSessionStore} and `HonoBff`'s
|
|
74
|
+
* `sessionMaxAgeMs`, so a BFF built with neither configured has a cookie
|
|
75
|
+
* and a server-side session that expire together.
|
|
76
|
+
*
|
|
77
|
+
* Long enough that an app used weekly never nags; short enough that a
|
|
78
|
+
* credential left on an unattended phone dies on its own. Set both knobs if
|
|
79
|
+
* your product wants a different window.
|
|
80
|
+
*/
|
|
81
|
+
export declare const DEFAULT_SESSION_MAX_AGE_MS: number;
|
|
82
|
+
/**
|
|
83
|
+
* Optional {@link SessionStore} capability: the store enforces a maximum
|
|
84
|
+
* session age, in milliseconds, and says what it is.
|
|
85
|
+
*
|
|
86
|
+
* `HonoBff` reads it to refuse a configuration whose session cookie would die
|
|
87
|
+
* before the sessions this store keeps serving — the pairing that leaves a user
|
|
88
|
+
* signed out while their session is still listed as active. Expose it from a
|
|
89
|
+
* DB-backed store to get that check; omit it and the BFF falls back to its own
|
|
90
|
+
* `sessionMaxAgeMs` bound.
|
|
91
|
+
*/
|
|
92
|
+
export interface BoundedSessionStore {
|
|
93
|
+
/** Maximum age, in ms, past which this store stops honoring a session. */
|
|
94
|
+
readonly maxAgeMs: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The maximum session age a store enforces, or `undefined` when it does not
|
|
98
|
+
* advertise one (see {@link BoundedSessionStore}).
|
|
99
|
+
*/
|
|
100
|
+
export declare function sessionStoreMaxAgeMs(store: SessionStore): number | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* Session persistence contract for {@link HonoBff}.
|
|
103
|
+
*
|
|
104
|
+
* Implementations may be stateful (cookie holds a session id, data lives
|
|
105
|
+
* server-side) or stateless (cookie holds the encrypted payload itself).
|
|
106
|
+
* `create` / `update` return the cookie value to set; `read` takes a
|
|
107
|
+
* cookie value and returns the data (or `null` if the cookie doesn't
|
|
108
|
+
* correspond to a live session). `destroy` removes a session.
|
|
109
|
+
*/
|
|
110
|
+
export interface SessionStore {
|
|
111
|
+
/** Create a new session and return the cookie value to set. */
|
|
112
|
+
create(data: SessionData): Promise<string>;
|
|
113
|
+
/** Read an existing session's data, or `null` if not found / invalid. */
|
|
114
|
+
read(cookieValue: string): Promise<SessionData | null>;
|
|
115
|
+
/**
|
|
116
|
+
* Update an existing session's data. Returns the new cookie value to
|
|
117
|
+
* set (may be the same as `cookieValue` for stateful stores; changes
|
|
118
|
+
* with every update for stateless stores). Stateful implementations must
|
|
119
|
+
* atomically reject missing, expired or revoked sessions with
|
|
120
|
+
* `InvalidGrantError`; updating must never create or restore a session.
|
|
121
|
+
*/
|
|
122
|
+
update(cookieValue: string, data: SessionData): Promise<string>;
|
|
123
|
+
/** Destroy the session identified by `cookieValue`. */
|
|
124
|
+
destroy(cookieValue: string): Promise<void>;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* In-memory stateful session store. Data is lost when the process
|
|
128
|
+
* restarts. Suitable for single-process development or tests; use a
|
|
129
|
+
* persistent store in production.
|
|
130
|
+
*/
|
|
131
|
+
export declare class MemorySessionStore implements SessionStore, BackchannelLogoutStore {
|
|
132
|
+
#private;
|
|
133
|
+
/** Stores the session under a fresh random id and returns it as the cookie value. */
|
|
134
|
+
create(data: SessionData): Promise<string>;
|
|
135
|
+
/** Looks up the session by its id, or `null` if no live session matches. */
|
|
136
|
+
read(cookieValue: string): Promise<SessionData | null>;
|
|
137
|
+
/** Replaces the stored data in place; the cookie value (session id) is unchanged. */
|
|
138
|
+
update(cookieValue: string, data: SessionData): Promise<string>;
|
|
139
|
+
/** Removes the session record for the given id. */
|
|
140
|
+
destroy(cookieValue: string): Promise<void>;
|
|
141
|
+
/**
|
|
142
|
+
* Destroys the sessions a validated logout_token targets: the specific `sid`
|
|
143
|
+
* when given, otherwise every session for `sub`. Returns the number
|
|
144
|
+
* destroyed.
|
|
145
|
+
*/
|
|
146
|
+
destroyByLogout(criteria: {
|
|
147
|
+
sub?: string;
|
|
148
|
+
sid?: string;
|
|
149
|
+
}): Promise<number>;
|
|
150
|
+
}
|
|
151
|
+
/** Options for {@link EncryptedCookieSessionStore}. */
|
|
152
|
+
export interface EncryptedCookieSessionStoreOptions {
|
|
153
|
+
/**
|
|
154
|
+
* Server-held secret(s) used to derive the AES-256 key(s). Each must have at
|
|
155
|
+
* least 32 bytes of entropy; pass a base64 / hex string or raw bytes.
|
|
156
|
+
*
|
|
157
|
+
* Pass an **ordered list** to rotate the secret without signing everyone out:
|
|
158
|
+
* the **first** entry is current — every cookie is sealed under it — and
|
|
159
|
+
* `read` tries each entry in turn, so a cookie sealed under a still-listed
|
|
160
|
+
* older secret keeps unsealing. Rotate by deploying `[newSecret, oldSecret]`,
|
|
161
|
+
* then drop `oldSecret` once the grace window (at least {@linkcode maxAgeMs},
|
|
162
|
+
* or your longest session lifetime) has elapsed. This is the session-store
|
|
163
|
+
* analog of a multi-key JWKS grace window. A single secret behaves exactly as
|
|
164
|
+
* before — a fleet-wide forced sign-out on rotation.
|
|
165
|
+
*/
|
|
166
|
+
secret: string | Uint8Array | Array<string | Uint8Array>;
|
|
167
|
+
/**
|
|
168
|
+
* Maximum age of a sealed cookie, in milliseconds: `read` returns `null` for
|
|
169
|
+
* any cookie sealed longer ago than this — even if it decrypts — bounding how
|
|
170
|
+
* long a captured cookie stays useful without any server-side state. This
|
|
171
|
+
* does **not** make `destroy` real; it only caps the window.
|
|
172
|
+
*
|
|
173
|
+
* Defaults to {@link DEFAULT_SESSION_MAX_AGE_MS} (14 days). There is no
|
|
174
|
+
* unbounded setting: an encrypted bearer cookie nobody can revoke must expire
|
|
175
|
+
* on its own. Pass a larger value for a longer window, and set the BFF's
|
|
176
|
+
* `sessionMaxAgeMs` to match — `HonoBff` throws when its session cookie
|
|
177
|
+
* would die before the sessions this store keeps accepting.
|
|
178
|
+
*
|
|
179
|
+
* `update` re-seals with a fresh stamp, so this is an **inactivity** window
|
|
180
|
+
* for a session the BFF keeps refreshing. Must be a positive number of
|
|
181
|
+
* milliseconds.
|
|
182
|
+
*
|
|
183
|
+
* The store stamps each cookie with a seal time at `create`/`update`; a
|
|
184
|
+
* pre-rotation cookie sealed before this field existed has no stamp and falls
|
|
185
|
+
* back to its own `updatedAt`, so old cookies are bounded too rather than
|
|
186
|
+
* grandfathered as unbounded.
|
|
187
|
+
*/
|
|
188
|
+
maxAgeMs?: number;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Stateless session store. The cookie value is the AES-GCM-encrypted
|
|
192
|
+
* JSON-serialized {@link SessionData}. No server-side storage needed.
|
|
193
|
+
*
|
|
194
|
+
* Security notes:
|
|
195
|
+
* - Each secret must have at least 256 bits of entropy. A weak secret defeats
|
|
196
|
+
* the confidentiality of the cookie.
|
|
197
|
+
* - AES-GCM provides authenticated encryption; tampered cookies fail `read`
|
|
198
|
+
* (returns `null`).
|
|
199
|
+
* - `destroy` is a no-op on the server — the cookie must be cleared in the
|
|
200
|
+
* client response for the session to end, and the refresh token should be
|
|
201
|
+
* revoked at the authorization server. Genuine server-side revocation is not
|
|
202
|
+
* achievable statelessly; choose a DB-backed {@link SessionStore} for that.
|
|
203
|
+
* {@linkcode EncryptedCookieSessionStoreOptions.maxAgeMs} — 14 days unless
|
|
204
|
+
* you set it — bounds the window a captured cookie stays useful but does not
|
|
205
|
+
* make `destroy` real.
|
|
206
|
+
* - Rotate the secret without a fleet-wide sign-out by passing an ordered list;
|
|
207
|
+
* see {@linkcode EncryptedCookieSessionStoreOptions.secret}.
|
|
208
|
+
*
|
|
209
|
+
* @example Rotating the secret with a grace window
|
|
210
|
+
* ```ts
|
|
211
|
+
* // Deploy with the new secret first, the old one second: new cookies seal
|
|
212
|
+
* // under NEW, cookies still sealed under OLD keep unsealing until you drop it.
|
|
213
|
+
* const store = new EncryptedCookieSessionStore({
|
|
214
|
+
* secret: [Deno.env.get("SESSION_SECRET")!, Deno.env.get("SESSION_SECRET_OLD")!],
|
|
215
|
+
* maxAgeMs: 7 * 24 * 60 * 60 * 1000,
|
|
216
|
+
* });
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
export declare class EncryptedCookieSessionStore implements SessionStore, BoundedSessionStore {
|
|
220
|
+
#private;
|
|
221
|
+
/**
|
|
222
|
+
* Derives each AES-256 key on first use and reuses it for every later
|
|
223
|
+
* operation. The first secret is current (used to seal); all are tried on
|
|
224
|
+
* read.
|
|
225
|
+
*
|
|
226
|
+
* @throws {Error} When the secret list is empty, or when
|
|
227
|
+
* {@linkcode EncryptedCookieSessionStoreOptions.maxAgeMs} is not a positive
|
|
228
|
+
* finite number of milliseconds.
|
|
229
|
+
*/
|
|
230
|
+
constructor(options: EncryptedCookieSessionStoreOptions);
|
|
231
|
+
/**
|
|
232
|
+
* The maximum age, in ms, past which a sealed cookie stops unsealing.
|
|
233
|
+
* Defaults to {@link DEFAULT_SESSION_MAX_AGE_MS}; `HonoBff` reads it to
|
|
234
|
+
* check that its session cookie outlives the sessions this store accepts.
|
|
235
|
+
*/
|
|
236
|
+
get maxAgeMs(): number;
|
|
237
|
+
/** Encrypts the session into the cookie value under the current secret; there is no server-side record. */
|
|
238
|
+
create(data: SessionData): Promise<string>;
|
|
239
|
+
/**
|
|
240
|
+
* Decrypts the cookie value, trying each configured secret in turn. Returns
|
|
241
|
+
* `null` if it is tampered, malformed, sealed with an unknown secret, or
|
|
242
|
+
* sealed longer ago than
|
|
243
|
+
* {@linkcode EncryptedCookieSessionStoreOptions.maxAgeMs}.
|
|
244
|
+
*/
|
|
245
|
+
read(cookieValue: string): Promise<SessionData | null>;
|
|
246
|
+
/** Re-encrypts the data into a fresh cookie value under the current secret. */
|
|
247
|
+
update(_cookieValue: string, data: SessionData): Promise<string>;
|
|
248
|
+
/** Stateless — the server has nothing to destroy. The caller must clear the cookie. */
|
|
249
|
+
destroy(_cookieValue: string): Promise<void>;
|
|
250
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session stores used by {@link HonoBff}.
|
|
3
|
+
*
|
|
4
|
+
* Two shapes are supported:
|
|
5
|
+
*
|
|
6
|
+
* - **Stateful**: the cookie holds a random session id; tokens live in a
|
|
7
|
+
* server-side store keyed by that id ({@link MemorySessionStore}, or
|
|
8
|
+
* your own DB-backed {@link SessionStore}). Pros: instant revocation,
|
|
9
|
+
* tiny cookie, leaked session id is useless outside your server. Cons:
|
|
10
|
+
* needs a store to scale horizontally.
|
|
11
|
+
*
|
|
12
|
+
* - **Stateless** ({@link EncryptedCookieSessionStore}): the cookie value
|
|
13
|
+
* *is* the encrypted token bundle (AES-GCM with a server-held secret),
|
|
14
|
+
* no server-side storage needed. Pros: no infrastructure beyond the
|
|
15
|
+
* secret. Cons: revocation requires `/revoke` + cookie clear, cookie
|
|
16
|
+
* grows with token size, key rotation needs care.
|
|
17
|
+
*
|
|
18
|
+
* A raw "token in cookie" store is deliberately **not** shipped: a leaked
|
|
19
|
+
* log line containing the cookie would yield a working bearer token. The
|
|
20
|
+
* AES-GCM encryption in the stateless store is what makes that pattern
|
|
21
|
+
* safe.
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import { InvalidGrantError } from "../../../errors.js";
|
|
26
|
+
import { deriveAesKey, sealJson, unsealJson } from "../../../utils/crypto.js";
|
|
27
|
+
/** Whether a store implements the {@link BackchannelLogoutStore} capability. */
|
|
28
|
+
export function supportsBackchannelLogout(store) {
|
|
29
|
+
return typeof store.destroyByLogout ===
|
|
30
|
+
"function";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Default session lifetime — 14 days — shared by
|
|
34
|
+
* {@link EncryptedCookieSessionStore} and `HonoBff`'s
|
|
35
|
+
* `sessionMaxAgeMs`, so a BFF built with neither configured has a cookie
|
|
36
|
+
* and a server-side session that expire together.
|
|
37
|
+
*
|
|
38
|
+
* Long enough that an app used weekly never nags; short enough that a
|
|
39
|
+
* credential left on an unattended phone dies on its own. Set both knobs if
|
|
40
|
+
* your product wants a different window.
|
|
41
|
+
*/
|
|
42
|
+
export const DEFAULT_SESSION_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
43
|
+
/**
|
|
44
|
+
* The maximum session age a store enforces, or `undefined` when it does not
|
|
45
|
+
* advertise one (see {@link BoundedSessionStore}).
|
|
46
|
+
*/
|
|
47
|
+
export function sessionStoreMaxAgeMs(store) {
|
|
48
|
+
const maxAgeMs = store.maxAgeMs;
|
|
49
|
+
return typeof maxAgeMs === "number" && Number.isFinite(maxAgeMs)
|
|
50
|
+
? maxAgeMs
|
|
51
|
+
: undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* In-memory stateful session store. Data is lost when the process
|
|
55
|
+
* restarts. Suitable for single-process development or tests; use a
|
|
56
|
+
* persistent store in production.
|
|
57
|
+
*/
|
|
58
|
+
export class MemorySessionStore {
|
|
59
|
+
#sessions = new Map();
|
|
60
|
+
/** Stores the session under a fresh random id and returns it as the cookie value. */
|
|
61
|
+
create(data) {
|
|
62
|
+
const id = crypto.randomUUID();
|
|
63
|
+
this.#sessions.set(id, data);
|
|
64
|
+
return Promise.resolve(id);
|
|
65
|
+
}
|
|
66
|
+
/** Looks up the session by its id, or `null` if no live session matches. */
|
|
67
|
+
read(cookieValue) {
|
|
68
|
+
return Promise.resolve(this.#sessions.get(cookieValue) ?? null);
|
|
69
|
+
}
|
|
70
|
+
/** Replaces the stored data in place; the cookie value (session id) is unchanged. */
|
|
71
|
+
update(cookieValue, data) {
|
|
72
|
+
if (!this.#sessions.has(cookieValue)) {
|
|
73
|
+
return Promise.reject(new InvalidGrantError("session is no longer active"));
|
|
74
|
+
}
|
|
75
|
+
this.#sessions.set(cookieValue, data);
|
|
76
|
+
return Promise.resolve(cookieValue);
|
|
77
|
+
}
|
|
78
|
+
/** Removes the session record for the given id. */
|
|
79
|
+
destroy(cookieValue) {
|
|
80
|
+
this.#sessions.delete(cookieValue);
|
|
81
|
+
return Promise.resolve();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Destroys the sessions a validated logout_token targets: the specific `sid`
|
|
85
|
+
* when given, otherwise every session for `sub`. Returns the number
|
|
86
|
+
* destroyed.
|
|
87
|
+
*/
|
|
88
|
+
destroyByLogout(criteria) {
|
|
89
|
+
const { sub, sid } = criteria;
|
|
90
|
+
let destroyed = 0;
|
|
91
|
+
for (const [id, data] of this.#sessions) {
|
|
92
|
+
const match = sid !== undefined
|
|
93
|
+
? data.sid === sid
|
|
94
|
+
: sub !== undefined && data.user?.sub === sub;
|
|
95
|
+
if (match) {
|
|
96
|
+
this.#sessions.delete(id);
|
|
97
|
+
destroyed++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return Promise.resolve(destroyed);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function isSealedSession(value) {
|
|
104
|
+
return typeof value === "object" && value !== null &&
|
|
105
|
+
typeof value.issuedAt === "number" &&
|
|
106
|
+
typeof value.data === "object" &&
|
|
107
|
+
value.data !== null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Stateless session store. The cookie value is the AES-GCM-encrypted
|
|
111
|
+
* JSON-serialized {@link SessionData}. No server-side storage needed.
|
|
112
|
+
*
|
|
113
|
+
* Security notes:
|
|
114
|
+
* - Each secret must have at least 256 bits of entropy. A weak secret defeats
|
|
115
|
+
* the confidentiality of the cookie.
|
|
116
|
+
* - AES-GCM provides authenticated encryption; tampered cookies fail `read`
|
|
117
|
+
* (returns `null`).
|
|
118
|
+
* - `destroy` is a no-op on the server — the cookie must be cleared in the
|
|
119
|
+
* client response for the session to end, and the refresh token should be
|
|
120
|
+
* revoked at the authorization server. Genuine server-side revocation is not
|
|
121
|
+
* achievable statelessly; choose a DB-backed {@link SessionStore} for that.
|
|
122
|
+
* {@linkcode EncryptedCookieSessionStoreOptions.maxAgeMs} — 14 days unless
|
|
123
|
+
* you set it — bounds the window a captured cookie stays useful but does not
|
|
124
|
+
* make `destroy` real.
|
|
125
|
+
* - Rotate the secret without a fleet-wide sign-out by passing an ordered list;
|
|
126
|
+
* see {@linkcode EncryptedCookieSessionStoreOptions.secret}.
|
|
127
|
+
*
|
|
128
|
+
* @example Rotating the secret with a grace window
|
|
129
|
+
* ```ts
|
|
130
|
+
* // Deploy with the new secret first, the old one second: new cookies seal
|
|
131
|
+
* // under NEW, cookies still sealed under OLD keep unsealing until you drop it.
|
|
132
|
+
* const store = new EncryptedCookieSessionStore({
|
|
133
|
+
* secret: [Deno.env.get("SESSION_SECRET")!, Deno.env.get("SESSION_SECRET_OLD")!],
|
|
134
|
+
* maxAgeMs: 7 * 24 * 60 * 60 * 1000,
|
|
135
|
+
* });
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export class EncryptedCookieSessionStore {
|
|
139
|
+
#secrets;
|
|
140
|
+
#keyPromises;
|
|
141
|
+
#maxAgeMs;
|
|
142
|
+
/**
|
|
143
|
+
* Derives each AES-256 key on first use and reuses it for every later
|
|
144
|
+
* operation. The first secret is current (used to seal); all are tried on
|
|
145
|
+
* read.
|
|
146
|
+
*
|
|
147
|
+
* @throws {Error} When the secret list is empty, or when
|
|
148
|
+
* {@linkcode EncryptedCookieSessionStoreOptions.maxAgeMs} is not a positive
|
|
149
|
+
* finite number of milliseconds.
|
|
150
|
+
*/
|
|
151
|
+
constructor(options) {
|
|
152
|
+
const secrets = Array.isArray(options.secret)
|
|
153
|
+
? options.secret
|
|
154
|
+
: [options.secret];
|
|
155
|
+
if (secrets.length === 0) {
|
|
156
|
+
throw new Error("secret must not be an empty list.");
|
|
157
|
+
}
|
|
158
|
+
const maxAgeMs = options.maxAgeMs ?? DEFAULT_SESSION_MAX_AGE_MS;
|
|
159
|
+
if (!Number.isFinite(maxAgeMs) || maxAgeMs <= 0) {
|
|
160
|
+
throw new Error("maxAgeMs must be a positive number of milliseconds, got " +
|
|
161
|
+
`${options.maxAgeMs}. A cookie nobody can revoke has to expire on ` +
|
|
162
|
+
"its own; pass a larger value for a longer window.");
|
|
163
|
+
}
|
|
164
|
+
this.#secrets = secrets;
|
|
165
|
+
this.#keyPromises = secrets.map(() => undefined);
|
|
166
|
+
this.#maxAgeMs = maxAgeMs;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The maximum age, in ms, past which a sealed cookie stops unsealing.
|
|
170
|
+
* Defaults to {@link DEFAULT_SESSION_MAX_AGE_MS}; `HonoBff` reads it to
|
|
171
|
+
* check that its session cookie outlives the sessions this store accepts.
|
|
172
|
+
*/
|
|
173
|
+
get maxAgeMs() {
|
|
174
|
+
return this.#maxAgeMs;
|
|
175
|
+
}
|
|
176
|
+
#key(index) {
|
|
177
|
+
return this.#keyPromises[index] ??= deriveAesKey(this.#secrets[index]);
|
|
178
|
+
}
|
|
179
|
+
/** Encrypts the session into the cookie value under the current secret; there is no server-side record. */
|
|
180
|
+
async create(data) {
|
|
181
|
+
const sealed = { issuedAt: Date.now(), data };
|
|
182
|
+
return await sealJson(await this.#key(0), sealed);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Decrypts the cookie value, trying each configured secret in turn. Returns
|
|
186
|
+
* `null` if it is tampered, malformed, sealed with an unknown secret, or
|
|
187
|
+
* sealed longer ago than
|
|
188
|
+
* {@linkcode EncryptedCookieSessionStoreOptions.maxAgeMs}.
|
|
189
|
+
*/
|
|
190
|
+
async read(cookieValue) {
|
|
191
|
+
for (let index = 0; index < this.#secrets.length; index++) {
|
|
192
|
+
const unsealed = await unsealJson(await this.#key(index), cookieValue);
|
|
193
|
+
if (unsealed === null)
|
|
194
|
+
continue;
|
|
195
|
+
if (isSealedSession(unsealed)) {
|
|
196
|
+
return this.#isExpired(unsealed.issuedAt) ? null : unsealed.data;
|
|
197
|
+
}
|
|
198
|
+
const legacy = unsealed;
|
|
199
|
+
return this.#isExpired(legacy.updatedAt ?? legacy.createdAt)
|
|
200
|
+
? null
|
|
201
|
+
: legacy;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
#isExpired(issuedAt) {
|
|
206
|
+
return Date.now() - issuedAt > this.#maxAgeMs;
|
|
207
|
+
}
|
|
208
|
+
/** Re-encrypts the data into a fresh cookie value under the current secret. */
|
|
209
|
+
update(_cookieValue, data) {
|
|
210
|
+
return this.create(data);
|
|
211
|
+
}
|
|
212
|
+
/** Stateless — the server has nothing to destroy. The caller must clear the cookie. */
|
|
213
|
+
destroy(_cookieValue) {
|
|
214
|
+
return Promise.resolve();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testing helpers for the Hono BFF adapter.
|
|
3
|
+
*
|
|
4
|
+
* Lets a consumer's tests drive protected endpoints without walking
|
|
5
|
+
* through the full login → authorize → callback flow, and ages the
|
|
6
|
+
* access token in the underlying {@link SessionStore} so a follow-up
|
|
7
|
+
* request through `attachToken()` exercises the refresh path
|
|
8
|
+
* deterministically (no `setTimeout` required).
|
|
9
|
+
*
|
|
10
|
+
* The runner-style helper {@link runSessionStoreContractTests} is
|
|
11
|
+
* available for verifying custom {@link SessionStore} implementations
|
|
12
|
+
* (Postgres, Redis, DynamoDB, …) satisfy the create / read / update /
|
|
13
|
+
* destroy contract.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import "../../../_dnt.polyfills.js";
|
|
18
|
+
import type { ClientInterface } from "../../../models/client.js";
|
|
19
|
+
import type { AbstractScope } from "../../../models/scope.js";
|
|
20
|
+
import type { TokenServiceInterface } from "../../../server/services/token.js";
|
|
21
|
+
import type { TokenBundle, UserInfoClaims } from "../../../client/mod.js";
|
|
22
|
+
import type { HonoBff } from "./bff.js";
|
|
23
|
+
import { type SessionData, type SessionStore } from "./session-store.js";
|
|
24
|
+
/** Options for {@link createTestSession}. */
|
|
25
|
+
export interface CreateTestSessionOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Token bundle to install. Anything you don't supply gets a sensible
|
|
28
|
+
* default (random access token, expires in 1 hour, no refresh token).
|
|
29
|
+
*/
|
|
30
|
+
tokens?: Partial<TokenBundle>;
|
|
31
|
+
/** Refresh token to store alongside the bundle. */
|
|
32
|
+
refreshToken?: string;
|
|
33
|
+
/** Cached user claims surfaced by `GET /auth/session`. */
|
|
34
|
+
user?: UserInfoClaims | null;
|
|
35
|
+
/** Override `createdAt` (epoch ms). Defaults to now. */
|
|
36
|
+
createdAt?: number;
|
|
37
|
+
/** Override `updatedAt` (epoch ms). Defaults to now. */
|
|
38
|
+
updatedAt?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Seeds a session in `bff.sessionStore` and returns a `Cookie` header
|
|
42
|
+
* value (`<name>=<value>`) ready to attach to subsequent
|
|
43
|
+
* `app.request(...)` calls. Skips the authorize / callback round-trip
|
|
44
|
+
* entirely — useful for tests that only care about authenticated
|
|
45
|
+
* endpoint behavior.
|
|
46
|
+
*
|
|
47
|
+
* **Important — token recognition.** The seeded access token is random by
|
|
48
|
+
* default and is NOT registered with any resource server. That is enough for
|
|
49
|
+
* `GET /auth/session` (which reads the cached `user` claim), but a route
|
|
50
|
+
* guarded by {@link HonoBff.protect} validates the session's access token
|
|
51
|
+
* against the resource server's token service — so an unrecognized token
|
|
52
|
+
* yields 401. For protected API routes, either pre-register the token
|
|
53
|
+
* (`tokenService.save({ accessToken, ... })` then pass the same
|
|
54
|
+
* `tokens.accessToken` here), or use {@link createAuthenticatedTestSession},
|
|
55
|
+
* which does both in one call.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* // Probe-only (GET /auth/session) — no resource server involved:
|
|
60
|
+
* const cookie = await createTestSession(bff, {
|
|
61
|
+
* user: { sub: "u1", name: "Alice" },
|
|
62
|
+
* });
|
|
63
|
+
* const res = await app.request("/auth/session", { headers: { cookie } });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function createTestSession(bff: HonoBff, options?: CreateTestSessionOptions): Promise<string>;
|
|
67
|
+
/** Options for {@link createAuthenticatedTestSession}. */
|
|
68
|
+
export interface CreateAuthenticatedTestSessionOptions<Client extends ClientInterface, User, Scope extends AbstractScope> {
|
|
69
|
+
/**
|
|
70
|
+
* The resource server's token service. The minted access token is saved
|
|
71
|
+
* here so {@link HonoBff.protect} recognizes it — pass the SAME token
|
|
72
|
+
* service the BFF's resource server validates against.
|
|
73
|
+
*/
|
|
74
|
+
tokenService: TokenServiceInterface<Client, User, Scope>;
|
|
75
|
+
/** Client the token is issued to. */
|
|
76
|
+
client: Client;
|
|
77
|
+
/** User the token is issued for. */
|
|
78
|
+
user?: User;
|
|
79
|
+
/** Scope granted to the token (drives `protect("scope")` checks). */
|
|
80
|
+
scope?: Scope;
|
|
81
|
+
/** Access-token lifetime in seconds. Defaults to one hour. */
|
|
82
|
+
accessTokenLifetime?: number;
|
|
83
|
+
/** Cached user claims surfaced by `GET /auth/session`. Defaults to null. */
|
|
84
|
+
claims?: UserInfoClaims | null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Mints a real access token, registers it with the resource server's token
|
|
88
|
+
* service, AND seeds a matching BFF session — the one-call setup for testing a
|
|
89
|
+
* route guarded by {@link HonoBff.protect}. Returns a `Cookie` header ready to
|
|
90
|
+
* attach to `app.request(...)`.
|
|
91
|
+
*
|
|
92
|
+
* This is the combined form of the two-step pattern (save a token, then
|
|
93
|
+
* {@link createTestSession} with the same access token) that protected-route
|
|
94
|
+
* tests otherwise hand-roll. Works with any {@link SessionStore} the BFF can
|
|
95
|
+
* `create()` into (e.g. `MemorySessionStore`,
|
|
96
|
+
* `EncryptedCookieSessionStore`); apps with a DB-backed store that mints
|
|
97
|
+
* its own cookie/secret should seed via that store directly.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* const cookie = await createAuthenticatedTestSession(bff, {
|
|
102
|
+
* tokenService,
|
|
103
|
+
* client: { id: "spa" },
|
|
104
|
+
* user: { id: "u1", username: "alice" },
|
|
105
|
+
* scope: new BasicScope("read write"),
|
|
106
|
+
* claims: { sub: "u1", name: "Alice" },
|
|
107
|
+
* });
|
|
108
|
+
* const res = await app.request("/api/me", { headers: { cookie } });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export declare function createAuthenticatedTestSession<Client extends ClientInterface, User, Scope extends AbstractScope>(bff: HonoBff, options: CreateAuthenticatedTestSessionOptions<Client, User, Scope>): Promise<string>;
|
|
112
|
+
/**
|
|
113
|
+
* Ages the access token in the underlying session so the next request
|
|
114
|
+
* through {@link HonoBff.attachToken} sees it as expired and triggers
|
|
115
|
+
* the refresh path.
|
|
116
|
+
*
|
|
117
|
+
* Returns a fresh `Cookie` header. For stateful stores
|
|
118
|
+
* (`MemorySessionStore` and DB-backed variants) the cookie value
|
|
119
|
+
* is unchanged. For stateless stores
|
|
120
|
+
* (`EncryptedCookieSessionStore`) the value is re-encrypted and
|
|
121
|
+
* differs from the input — use the returned header for subsequent
|
|
122
|
+
* requests.
|
|
123
|
+
*
|
|
124
|
+
* @param expiredAt Optional time to set the access token's expiry to
|
|
125
|
+
* (epoch ms). Defaults to one second in the past.
|
|
126
|
+
*/
|
|
127
|
+
export declare function forceTokenExpiry(bff: HonoBff, cookieHeader: string, expiredAt?: number): Promise<string>;
|
|
128
|
+
/**
|
|
129
|
+
* Returns the session data currently stored under the given cookie
|
|
130
|
+
* header, or `null` if there is none. Convenience wrapper around
|
|
131
|
+
* `bff.sessionStore.read` that handles cookie parsing.
|
|
132
|
+
*/
|
|
133
|
+
export declare function readTestSession(bff: HonoBff, cookieHeader: string): Promise<SessionData | null>;
|
|
134
|
+
/** Options for {@link runSessionStoreContractTests}. */
|
|
135
|
+
export interface SessionStoreContractOptions {
|
|
136
|
+
/** Returns a fresh, empty store for each test. */
|
|
137
|
+
makeStore(): Promise<SessionStore> | SessionStore;
|
|
138
|
+
/**
|
|
139
|
+
* Some stateless stores (`EncryptedCookieSessionStore`) cannot
|
|
140
|
+
* distinguish "session destroyed" from "session never existed" —
|
|
141
|
+
* destroy is a no-op on the server, with the cookie clear happening on
|
|
142
|
+
* the client side. Set this to `false` to skip the post-destroy
|
|
143
|
+
* `read` returns null assertion.
|
|
144
|
+
*/
|
|
145
|
+
destroyClearsRead?: boolean;
|
|
146
|
+
/** Label for the generated `describe` block. Defaults to `"SessionStore contract"`. */
|
|
147
|
+
describeName?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Returns a store bound to the given maximum session age, in milliseconds.
|
|
150
|
+
* Supply it whenever your store enforces an age bound — the suite builds a
|
|
151
|
+
* store with a one-second bound, waits it out, and requires `read` to stop
|
|
152
|
+
* returning the session. One second is whole-second-expressible, so a store
|
|
153
|
+
* whose TTL column or `EXPIRE` call has second resolution can honor it
|
|
154
|
+
* exactly.
|
|
155
|
+
*
|
|
156
|
+
* `HonoBff` trusts the `maxAgeMs` a store advertises when it refuses a
|
|
157
|
+
* configuration whose session cookie would expire before the store stops
|
|
158
|
+
* honoring the session. A store that advertises a bound it does not enforce
|
|
159
|
+
* defeats that check, so the suite fails a store that advertises one without
|
|
160
|
+
* supplying this factory.
|
|
161
|
+
*/
|
|
162
|
+
makeBoundedStore?(maxAgeMs: number): Promise<SessionStore> | SessionStore;
|
|
163
|
+
}
|
|
164
|
+
/** Contract test suite for {@link SessionStore} implementations. */
|
|
165
|
+
export declare function runSessionStoreContractTests(options: SessionStoreContractOptions): void;
|