unjwt 0.8.1 → 0.8.3

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.
Files changed (37) hide show
  1. package/README.md +4 -4
  2. package/dist/THIRD-PARTY-LICENSES.md.gz +0 -0
  3. package/dist/_chunks/_codec-jwe.d.mts +101 -22
  4. package/dist/_chunks/_codec-jws.mjs +306 -232
  5. package/dist/_chunks/_cookie-attributes.d.mts +12 -0
  6. package/dist/_chunks/_jwt-internal.mjs +9 -6
  7. package/dist/_chunks/digest.d.mts +8 -0
  8. package/dist/_chunks/{algorithms.mjs → digest.mjs} +78 -8
  9. package/dist/_chunks/index.d.mts +313 -268
  10. package/dist/_chunks/index2.d.mts +2 -1
  11. package/dist/_chunks/index3.d.mts +1 -0
  12. package/dist/_chunks/jwe.d.mts +4 -4
  13. package/dist/_chunks/jwe.mjs +21 -3
  14. package/dist/_chunks/jwk.mjs +219 -173
  15. package/dist/_chunks/jws.d.mts +10 -10
  16. package/dist/_chunks/jws.mjs +18 -6
  17. package/dist/_chunks/jwt.d.mts +2 -1
  18. package/dist/_chunks/libs/cookie-es.mjs +255 -0
  19. package/dist/adapters/elysia/index.d.mts +32 -15
  20. package/dist/adapters/elysia/index.mjs +46 -67
  21. package/dist/adapters/h3/index.d.mts +135 -0
  22. package/dist/adapters/h3/index.mjs +119 -0
  23. package/dist/adapters/h3v1/index.d.mts +100 -27
  24. package/dist/adapters/h3v1/index.mjs +59 -76
  25. package/dist/core/jwe.d.mts +2 -2
  26. package/dist/core/jwk.d.mts +22 -14
  27. package/dist/core/jwk.mjs +2 -2
  28. package/dist/core/jws.d.mts +2 -2
  29. package/dist/core/utils/index.d.mts +2 -1
  30. package/dist/core/utils/index.mjs +2 -2
  31. package/dist/index.d.mts +4 -3
  32. package/dist/index.mjs +3 -3
  33. package/dist/session/index.d.mts +44 -0
  34. package/dist/session/index.mjs +113 -0
  35. package/package.json +37 -18
  36. package/dist/adapters/h3v2/index.d.mts +0 -65
  37. package/dist/adapters/h3v2/index.mjs +0 -139
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm downloads](https://npmx.dev/api/registry/badge/downloads/unjwt)](https://npmx.dev/package/unjwt)
5
5
  [![bundle size](https://npmx.dev/api/registry/badge/size/unjwt)](https://npmx.dev/package/unjwt)
6
6
 
7
- Low-level JWT toolkit built on the Web Crypto API. Sign, verify, encrypt, decrypt, and manage keys — JWS, JWE, and JWK in a single zero-dependency package.
7
+ A collection of JWT utilities based on the Web Crypto API, in a single zero-dependency package.
8
8
 
9
9
  > **📖 Documentation — [unjwt.s94.dev](https://unjwt.s94.dev)**
10
10
 
@@ -13,9 +13,9 @@ Low-level JWT toolkit built on the Web Crypto API. Sign, verify, encrypt, decryp
13
13
  - **JWS** ([RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515)) — HMAC, RSA, RSA-PSS, ECDSA, EdDSA. Compact + General/Flattened JSON Serialization (multi-signature).
14
14
  - **JWE** ([RFC 7516](https://datatracker.ietf.org/doc/html/rfc7516)) — `dir`, AES-KW, AES-GCM-KW, RSA-OAEP, PBES2, ECDH-ES. AES-GCM and AES-CBC-HMAC-SHA2 content encryption. Compact + General/Flattened JSON Serialization (multi-recipient).
15
15
  - **JWK** ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) — generate, import, export, wrap, unwrap. `CryptoKey` ↔ JWK ↔ PEM conversion. ECDH-ES shared-secret derivation. JWK Set lookup and rotation over a set your application fetches.
16
- - **Framework adapters** — cookie-based JWT sessions for [H3 v1](https://unjwt.s94.dev/adapters) (Nuxt v4 / Nitro v2), [H3 v2](https://unjwt.s94.dev/adapters) (Nuxt v5 / Nitro v3) and [Elysia](https://unjwt.s94.dev/adapters/elysia).
16
+ - **Sessions** — cookie-based JWT sessions through [`unjwt/session`](https://unjwt.s94.dev/frameworks/session) on [h3 v2](https://unjwt.s94.dev/frameworks/h3), [Nitro 3](https://unjwt.s94.dev/frameworks/nitro), [Nuxt 4.6+](https://unjwt.s94.dev/frameworks/nuxt), Workers, Bun, Deno and Hono, or on a token in hand; an [Elysia](https://unjwt.s94.dev/frameworks/elysia) plugin; a [driver](https://unjwt.s94.dev/frameworks/drivers) for any other framework.
17
17
  - **Runtime-agnostic** — Node 22+, Deno, Bun, Cloudflare Workers, browsers. Anywhere Web Crypto runs.
18
- - **Zero runtime dependencies** for core; optional peer deps for adapters (`h3`, `cookie-es`, `elysia`).
18
+ - **Zero runtime dependencies** for core; optional peer deps for adapters (`h3`, `elysia`).
19
19
 
20
20
  ## Install
21
21
 
@@ -55,7 +55,7 @@ Full walkthroughs, recipes, and the complete API reference live at **[unjwt.s94.
55
55
  - [JWS — signing and verifying →](https://unjwt.s94.dev/jwt/jws)
56
56
  - [JWE — encrypting and decrypting →](https://unjwt.s94.dev/jwt/jwe)
57
57
  - [JWK — key management →](https://unjwt.s94.dev/jwk)
58
- - [Session adapters →](https://unjwt.s94.dev/adapters)
58
+ - [Sessions on your framework →](https://unjwt.s94.dev/frameworks)
59
59
  - [Examples →](https://unjwt.s94.dev/examples) — authentication, JWKS endpoints, refresh tokens, end-to-end encryption, signed receipts.
60
60
 
61
61
  ## Development
Binary file
@@ -1,8 +1,7 @@
1
- import { ExpiresIn, JWEAsymmetricPrivateJWK, JWEAsymmetricPublicJWK, JWEDecryptJWK, JWEDecryptOptions, JWEEncryptJWK, JWEEncryptOptions, JWEHeaderParameters, JWKLookupFunctionHeader, JWKSet, JWK_HMAC, JWK_oct, JWSAsymmetricPrivateJWK, JWSAsymmetricPublicJWK, JWSSignOptions, JWSVerifyJWK, JWSVerifyOptions, JWTClaims, StrictOmit } from "./index.mjs";
2
- /** Per-issue overrides for a single `update()` call. */
3
- export interface SessionIssueOptions {
4
- /** Lifetime for this issue only, overriding the config's `maxAge`. */
5
- maxAge?: ExpiresIn;
1
+ import { ExpiresIn, JWEAsymmetricPrivateJWK, JWEAsymmetricPublicJWK, JWEDecryptJWK, JWEDecryptOptions, JWEEncryptJWK, JWEEncryptOptions, JWEHeaderParameters, JWKSet, JWK_HMAC, JWK_oct, JWSAsymmetricPrivateJWK, JWSAsymmetricPublicJWK, JWSHeaderParameters, JWSSignOptions, JWSVerifyJWK, JWSVerifyOptions, JWTClaims, StrictOmit } from "./index.mjs";
2
+ interface ExpirableCookieAttributes {
3
+ expires?: Date;
4
+ maxAge?: number;
6
5
  }
7
6
  /** Session data shape; `any` values let an `interface` (no index signature) satisfy it. */
8
7
  export type SessionRecord = Record<string, any>;
@@ -31,12 +30,28 @@ export interface SessionManager<T extends SessionRecord = SessionClaims, ConfigM
31
30
  interface SessionBaseConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined> {
32
31
  /** Sets `exp = iat + maxAge`, requires `exp` on incoming tokens, drives cookie expiry; unset never expires. */
33
32
  maxAge?: MaxAge;
34
- /** Reissues a session on read, so `exp` slides: `true` on every read, a duration once less than that remains. Needs `maxAge`. */
33
+ /**
34
+ * Reissue the session when it is read, needs `maxAge`.
35
+ *
36
+ * - `true`: every read reissues.
37
+ * - a duration: a read reissues once the token is at least that old, so an active session is
38
+ * re-signed at most once per duration. With `maxAge: "1h"` and `rolling: "15m"`, reads in the
39
+ * first 15 minutes change nothing; the first read after that issues a fresh hour. Must be
40
+ * shorter than `maxAge`.
41
+ *
42
+ * A read that cannot write (a read-only event, a response already sent) does not reissue;
43
+ * the next request, carrying the same token, does.
44
+ *
45
+ * @default undefined — the session expires at `iat + maxAge` however often it is read.
46
+ */
35
47
  rolling?: true | ExpiresIn;
36
48
  /** Drives the cookie name and default header; prefer `__Host-`; a trailing `.<digits>` is refused. */
37
49
  name?: string;
38
- /** Header carrying a bare or `Bearer` token, or `false`. Default `x-<name>-session` (cookie prefix stripped). */
39
- sessionHeader?: false | string;
50
+ /**
51
+ * Header carrying a bare or `Bearer` token: a header name, `true` for `x-<name>-session` (cookie prefix stripped), or `false` for none.
52
+ * Left unset, a session with cookies still reads `x-<name>-session`, and so does one with `cookie: false`.
53
+ */
54
+ sessionHeader?: boolean | string;
40
55
  /** Session ID (`jti`) generator. Default `crypto.randomUUID`. */
41
56
  generateId?: () => string;
42
57
  }
@@ -44,9 +59,9 @@ interface SessionBaseConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | u
44
59
  * Lifecycle hooks; `onRead`, `onExpire` and `onError` are mutually exclusive per incoming token.
45
60
  * `session` is the live manager, so a hook can `update()` or `clear()` in place; `oldSession` is a snapshot.
46
61
  */
47
- interface SessionHooks<TSession, TManager, TCarrier extends Record<string, unknown>, TConfig> {
62
+ interface SessionHooks<TSession, TManager, TArgs extends Record<string, unknown>, TConfig> {
48
63
  /** Fires once per request, when the session is read from its token. */
49
- onRead?: (args: TCarrier & {
64
+ onRead?: (args: TArgs & {
50
65
  session: TManager & {
51
66
  id: string;
52
67
  token: string;
@@ -54,7 +69,7 @@ interface SessionHooks<TSession, TManager, TCarrier extends Record<string, unkno
54
69
  config: TConfig;
55
70
  }) => void | Promise<void>;
56
71
  /** Fires after a token is issued and its cookie written, by `update()` or by `rolling`; a throw rejects without rollback. */
57
- onUpdate?: (args: TCarrier & {
72
+ onUpdate?: (args: TArgs & {
58
73
  session: TManager & {
59
74
  id: string;
60
75
  token: string;
@@ -65,12 +80,12 @@ interface SessionHooks<TSession, TManager, TCarrier extends Record<string, unkno
65
80
  config: TConfig;
66
81
  }) => void | Promise<void>;
67
82
  /** Fires when the session is cleared. */
68
- onClear?: (args: TCarrier & {
83
+ onClear?: (args: TArgs & {
69
84
  oldSession: TSession | undefined;
70
85
  config: TConfig;
71
86
  }) => void | Promise<void>;
72
87
  /** Fires when the incoming token is expired. Receives the expired token's decoded claims. */
73
- onExpire?: (args: TCarrier & {
88
+ onExpire?: (args: TArgs & {
74
89
  session: {
75
90
  id: string | undefined;
76
91
  createdAt: number | undefined;
@@ -81,14 +96,17 @@ interface SessionHooks<TSession, TManager, TCarrier extends Record<string, unkno
81
96
  config: TConfig;
82
97
  }) => void | Promise<void>;
83
98
  /** Fires on read-path failures other than expiry, and on write-path issue failures. */
84
- onError?: (args: TCarrier & {
99
+ onError?: (args: TArgs & {
85
100
  session: TManager;
86
101
  error: any;
87
102
  config: TConfig;
88
103
  }) => void | Promise<void>;
89
104
  }
90
- /** In-flight read of a session, awaited by a second reader on the same request. */
91
- declare const kSessionPending: unique symbol;
105
+ /** Per-issue overrides for a single `update()` call. */
106
+ export interface SessionIssueOptions {
107
+ /** Lifetime for this issue only, overriding the config's `maxAge`. */
108
+ maxAge?: ExpiresIn;
109
+ }
92
110
  /** In-request session state, mirroring the claims of the underlying token. */
93
111
  export interface SessionState<T extends SessionRecord, MaxAge extends ExpiresIn | undefined> {
94
112
  /** JWT `jti`; `undefined` until the session is persisted. */
@@ -99,7 +117,68 @@ export interface SessionState<T extends SessionRecord, MaxAge extends ExpiresIn
99
117
  expiresAt: MaxAge extends ExpiresIn ? number : number | undefined;
100
118
  data: SessionData<T>;
101
119
  token: string | undefined;
102
- [kSessionPending]?: Promise<SessionState<T, MaxAge>>;
120
+ }
121
+ /** Session state read back from a token: always carries its `id` and `createdAt`. */
122
+ export type OpenedSessionState<T extends SessionRecord, MaxAge extends ExpiresIn | undefined> = Pick<SessionState<T, MaxAge>, "id" | "createdAt" | "expiresAt" | "data"> & {
123
+ id: string;
124
+ createdAt: number;
125
+ };
126
+ /** One session, defined once from its config and used on any request. */
127
+ export interface SessionDefinition<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent = unknown> {
128
+ /** Reads the request's session once and returns its manager; later calls on the same request share that state. */
129
+ use: (event: TEvent) => Promise<SessionManager<T, MaxAge>>;
130
+ /** The token the request carries (this response's own write first), unopened. */
131
+ token: (event: TEvent) => string | undefined;
132
+ /** Mints a token for `data` with a fresh `jti` and `iat`, and `exp` from `maxAge` (`options.maxAge` overrides it); reads and writes nothing. */
133
+ issue: (data?: Partial<SessionData<T>>, options?: SessionIssueOptions) => Promise<string>;
134
+ /** Opens a token into session state, checking `typ`, pinned algorithms, `jti`/`iat` and `exp` when `maxAge` is set; the key-lookup hook receives no request. */
135
+ open: (token: string) => Promise<OpenedSessionState<T, MaxAge>>;
136
+ }
137
+ interface SessionRequestBase {
138
+ /** The response being prepared; without it the session is read-only. */
139
+ readonly res?: {
140
+ readonly headers: Headers;
141
+ };
142
+ /** Per-request state shared with the framework, where `sessions` is kept; without it state is keyed on `req`, else on this object. */
143
+ readonly context?: Record<string, unknown>;
144
+ }
145
+ /** A request event: by its `Request`, or by its `Headers` (a `Request` itself, a WebSocket upgrade's request). */
146
+ export type SessionRequestEvent = SessionRequestBase & ({
147
+ readonly req: Request;
148
+ readonly headers?: Headers;
149
+ } | {
150
+ readonly headers: Headers;
151
+ readonly req?: Request;
152
+ });
153
+ /** A token handed in directly; sending a re-issued one is the caller's job. */
154
+ export interface SessionHandedToken {
155
+ token: string | undefined;
156
+ }
157
+ /** What the Web session driver takes: a request event, or a token in hand. */
158
+ export type WebSessionEvent = SessionRequestEvent | SessionHandedToken;
159
+ /** The cookies of one request and its response, as a framework exposes them. */
160
+ export interface CookieJar<A extends ExpirableCookieAttributes = ExpirableCookieAttributes> {
161
+ /** The request's cookie. */
162
+ read(name: string): string | undefined;
163
+ /** The value this response already assigned to `name` (`""` once deleted), when the framework exposes it. */
164
+ written?(name: string): string | undefined;
165
+ /** Absent when this event cannot take a `Set-Cookie` (a request without a response). */
166
+ write?(name: string, value: string, attrs: A): void;
167
+ }
168
+ /** The primitives a framework supplies; the session core builds the cookie and header transports over them. */
169
+ export interface SessionDriver<TEvent> {
170
+ /** Message prefix for every error the adapter emits. */
171
+ prefix: string;
172
+ /** The per-request record session state and the session-name registry live in. */
173
+ state(event: TEvent): Record<string, unknown>;
174
+ header(event: TEvent, name: string): string | null | undefined;
175
+ cookies(event: TEvent): CookieJar;
176
+ /** Platform signal that the response can still change (Node `headersSent`); absent = trust the jar. */
177
+ writable?(event: TEvent): boolean;
178
+ /** A token the event carries directly, when it does: no cookie or header is consulted. */
179
+ handed?(event: TEvent): {
180
+ token: string | undefined;
181
+ } | undefined;
103
182
  }
104
183
  /** HS* `oct` JWK, `CryptoKey` (requires pinned `alg`), or a pair: private signs, public JWK/list/set verifies. */
105
184
  type JWSSessionKey = CryptoKey | JWK_oct<JWK_HMAC> | {
@@ -127,10 +206,10 @@ export interface JWSSessionConfig<MaxAge extends ExpiresIn | undefined = Expires
127
206
  jws?: JWSSessionOptions;
128
207
  }
129
208
  /** JWS session hooks, plus the verify key lookup. */
130
- export interface JWSSessionHooks<TSession, TManager, TCarrier extends Record<string, unknown>, TConfig> extends SessionHooks<TSession, TManager, TCarrier, TConfig> {
209
+ export interface JWSSessionHooks<TSession, TManager, TArgs extends Record<string, unknown>, TConfig> extends SessionHooks<TSession, TManager, TArgs, TConfig> {
131
210
  /** Resolve the verification key(s) for the incoming token from its protected header (e.g. by `kid`). */
132
- onVerifyKeyLookup?: (args: TCarrier & {
133
- header: JWKLookupFunctionHeader;
211
+ onVerifyKeyLookup?: (args: Partial<TArgs> & {
212
+ header: JWSHeaderParameters;
134
213
  config: TConfig;
135
214
  }) => JWKSet | JWSVerifyJWK | Promise<JWKSet | JWSVerifyJWK>;
136
215
  }
@@ -160,9 +239,9 @@ export interface JWESessionConfig<MaxAge extends ExpiresIn | undefined = Expires
160
239
  jwe?: JWESessionOptions;
161
240
  }
162
241
  /** JWE session hooks, plus the unseal key lookup. */
163
- export interface JWESessionHooks<TSession, TManager, TCarrier extends Record<string, unknown>, TConfig> extends SessionHooks<TSession, TManager, TCarrier, TConfig> {
242
+ export interface JWESessionHooks<TSession, TManager, TArgs extends Record<string, unknown>, TConfig> extends SessionHooks<TSession, TManager, TArgs, TConfig> {
164
243
  /** Resolve the decryption key for the incoming token from its protected header (e.g. by `kid`). */
165
- onUnsealKeyLookup?: (args: TCarrier & {
244
+ onUnsealKeyLookup?: (args: Partial<TArgs> & {
166
245
  header: JWEHeaderParameters;
167
246
  config: TConfig;
168
247
  }) => JWEDecryptJWK | Promise<JWEDecryptJWK>;