unjwt 0.5.37 → 0.5.39

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 (41) hide show
  1. package/README.md +142 -663
  2. package/dist/_chunks/chunk.mjs +13 -0
  3. package/dist/_chunks/h3v1.mjs +598 -0
  4. package/dist/_chunks/index.d.mts +250 -0
  5. package/dist/_chunks/index2.d.mts +108 -0
  6. package/dist/_chunks/jwe.d.mts +43 -0
  7. package/dist/_chunks/jwe.mjs +132 -0
  8. package/dist/_chunks/jwk.d.mts +575 -0
  9. package/dist/_chunks/jwk.mjs +1592 -0
  10. package/dist/_chunks/jws.d.mts +46 -0
  11. package/dist/_chunks/jws.mjs +2069 -0
  12. package/dist/_chunks/utils.mjs +374 -0
  13. package/dist/adapters/h3.d.mts +3 -265
  14. package/dist/adapters/h3.mjs +3 -706
  15. package/dist/adapters/h3v1.d.mts +3 -8
  16. package/dist/adapters/h3v1.mjs +3 -7
  17. package/dist/adapters/h3v2.d.mts +145 -146
  18. package/dist/adapters/h3v2.mjs +505 -596
  19. package/dist/index.d.mts +6 -8
  20. package/dist/index.mjs +5 -4
  21. package/dist/jwe.d.mts +3 -4
  22. package/dist/jwe.mjs +4 -3
  23. package/dist/jwk.d.mts +3 -2
  24. package/dist/jwk.mjs +3 -2
  25. package/dist/jws.d.mts +3 -3
  26. package/dist/jws.mjs +4 -3
  27. package/dist/utils.d.mts +3 -3
  28. package/dist/utils.mjs +2 -1
  29. package/package.json +33 -31
  30. package/dist/shared/unjwt.4JQCoID_.d.mts +0 -161
  31. package/dist/shared/unjwt.B-v62plj.mjs +0 -249
  32. package/dist/shared/unjwt.BCi3FEWr.d.mts +0 -328
  33. package/dist/shared/unjwt.BYDTEWa-.d.mts +0 -269
  34. package/dist/shared/unjwt.Be8NxwFK.mjs +0 -438
  35. package/dist/shared/unjwt.BqdNUKDr.d.mts +0 -64
  36. package/dist/shared/unjwt.CMNtw8ek.d.mts +0 -155
  37. package/dist/shared/unjwt.Cy2MlkUe.d.mts +0 -174
  38. package/dist/shared/unjwt.D0_ccT7g.d.mts +0 -166
  39. package/dist/shared/unjwt.DdLN9vwI.mjs +0 -163
  40. package/dist/shared/unjwt.Dpl__ZAR.mjs +0 -2121
  41. package/dist/shared/unjwt.JdKqVANs.d.mts +0 -5
package/README.md CHANGED
@@ -10,651 +10,211 @@
10
10
 
11
11
  A collection of low-level JWT ([RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519)) utilities using the Web Crypto API. Supports:
12
12
 
13
- - **JWS (JSON Web Signature, [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515))**: sign and verify tokens using HMAC, RSA (RSASSA-PKCS1-v1_5 & RSA-PSS), and ECDSA algorithms.
14
- - **JWE (JSON Web Encryption, [RFC 7516](https://datatracker.ietf.org/doc/html/rfc7516))**: encrypt and decrypt data using various key management algorithms (AES Key Wrap, AES-GCM Key Wrap, RSA-OAEP, PBES2, ECDH-ES) and content encryption algorithms (AES-GCM, AES-CBC HMAC-SHA2).
15
- - **JWK (JSON Web Key, [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517))**: generate, import, export, wrap, and unwrap keys in JWK format or as `CryptoKey` objects.
16
-
17
- - **Adapters** for popular frameworks (PRs welcome for more!):
18
- - [H3 v1 (Nuxt v4, Nitro v2)](./src/adapters/h3v1/): `useJWSSession()`, `useJWESession()`
19
- - [H3 v2 (Nuxt v5, Nitro v3)](./src/adapters/h3v2/): `useJWSSession()`, `useJWESession()`
20
-
21
- ## Table of Contents
22
-
23
- - [Usage](#usage)
24
- - [JWS](#jws-json-web-signature-rfc-7515)
25
- - [JWE](#jwe-json-web-encryption-rfc-7516)
26
- - [JWK](#jwk-json-web-key-rfc-7517)
27
- - [Utility Functions](#utility-functions)
28
- - [Adapters](#adapters)
29
- - [H3 v1 (Nuxt up to v4, Nitro up to v2)](#h3-v1-nuxt-v4-nitro-v2)
30
- - [H3 v2 (Nuxt v5, Nitro v3)](#h3-v2-nuxt-v5-nitro-v3)
31
- - [Development](#development)
32
- - [Credits](#credits)
33
- - [License](#license)
13
+ - **JWS** ([RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515)) — sign and verify tokens using HMAC, RSA, RSA-PSS, ECDSA, and EdDSA algorithms
14
+ - **JWE** ([RFC 7516](https://datatracker.ietf.org/doc/html/rfc7516)) — encrypt and decrypt data using AES Key Wrap, AES-GCM KW, RSA-OAEP, PBES2, and ECDH-ES key management with AES-GCM or AES-CBC+HMAC-SHA2 content encryption
15
+ - **JWK** ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) — generate, import, export, wrap, and unwrap keys (CryptoKey, JWK, PEM)
16
+ - **Framework adapters** for cookie-based JWT sessions:
17
+ - [H3 v1](./skills/unjwt/references/adapters-h3.md) (Nuxt v4, Nitro v2)
18
+ - [H3 v2](./skills/unjwt/references/adapters-h3.md) (Nuxt v5, Nitro v3)
34
19
 
35
- ## Usage
20
+ Zero runtime dependencies for core. Optional peer deps for adapters (`h3`, `cookie-es`, `rou3`).
36
21
 
37
- Install the package:
22
+ ## Install
38
23
 
39
24
  ```sh
40
- # ✨ Auto-detect (supports npm, yarn, pnpm, deno and bun)
25
+ # Auto-detect package manager (npm, yarn, pnpm, deno, bun)
41
26
  npx nypm install unjwt
42
27
  ```
43
28
 
44
- Import:
45
-
46
- **ESM** (Node.js, Bun, Deno)
47
-
48
- ```js
49
- import { jws, jwe, jwk } from "unjwt";
50
- // JWS functions
51
- import { sign, verify } from "unjwt/jws";
52
- // JWE functions
53
- import { encrypt, decrypt } from "unjwt/jwe";
54
- // JWK functions
55
- import {
56
- generateKey,
57
- generateJWK,
58
- importJWKFromPEM,
59
- exportJWKToPEM,
60
- deriveKeyFromPassword,
61
- deriveJWKFromPassword,
62
- } from "unjwt/jwk";
63
- // Utility functions
64
- import {
65
- isJWK,
66
- isJWKSet,
67
- isSymmetricJWK,
68
- isAsymmetricJWK,
69
- isPrivateJWK,
70
- isPublicJWK,
71
- } from "unjwt/utils";
72
- ```
73
-
74
- **CDN** (Deno, Bun and Browsers)
75
-
76
- ```js
77
- import { jws, jwe, jwk } from "https://esm.sh/unjwt";
78
- // JWS functions
79
- import { sign, verify } from "https://esm.sh/unjwt/jws";
80
- // JWE functions
81
- import { encrypt, decrypt } from "https://esm.sh/unjwt/jwe";
82
- // JWK functions
83
- import {
84
- generateKey,
85
- generateJWK,
86
- importJWKFromPEM,
87
- exportJWKToPEM,
88
- deriveKeyFromPassword,
89
- deriveJWKFromPassword,
90
- } from "https://esm.sh/unjwt/jwk";
91
- // Utility functions
92
- import {
93
- isJWK,
94
- isJWKSet,
95
- isSymmetricJWK,
96
- isAsymmetricJWK,
97
- isPrivateJWK,
98
- isPublicJWK,
99
- } from "https://esm.sh/unjwt/utils";
100
- ```
101
-
102
- ---
103
-
104
- ### JWS (JSON Web Signature, [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515))
105
-
106
- Functions to sign and verify data according to the JWS specification.
107
-
108
- #### `sign(payload, key, options)`
109
-
110
- Creates a JWS token.
111
-
112
- - `payload`: The data to sign (`string`, `Uint8Array`, or a JSON-serializable `object`).
113
- - `key`: The signing key (`CryptoKey`, `JWK`, or `Uint8Array` for symmetric keys).
114
- - `options`:
115
- - `alg`: (Required) The JWS algorithm (e.g., `"HS256"`, `"RS256"`, `"ES256"`, `"PS256"`).
116
- - `protectedHeader`: An object for additional JWS Protected Header parameters (e.g., `kid`, `typ`, `cty`, `crit`, `b64`). If `payload` is an object and `typ` is not set, it defaults to `"JWT"`. The `b64` parameter ([RFC7797 section-3](https://datatracker.ietf.org/doc/html/rfc7797#section-3)) controls payload encoding (defaults to `true`, meaning Base64URL encoded).
117
- - `expiresIn`: Sets an expiration time in seconds (e.g., `3600` for 1 hour). If `iat` is missing it will be set to the current time. If `exp` is missing it will be set to `iat + expiresIn`. This is only applied if `payload` is a JWT.
118
- - `currentDate`: The current date for computing `expiresIn` option. Defaults to `new Date()`.
119
-
120
- Returns a `Promise<string>` resolving to the JWS token in Compact Serialization format.
121
-
122
- **Example (HS256 with string secret):**
123
-
124
- ```ts
125
- import { sign } from "unjwt/jws";
126
-
127
- const payload = { message: "My important data" }; // Object payload
128
- const key = await generateKey("HS256", { toJWK: true }); // Generates a JWK_oct
129
-
130
- const token = await sign(payload, key);
131
-
132
- console.log(token);
133
- // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
134
- ```
135
-
136
- **Example (RS256 with CryptoKey):**
137
-
138
- ```ts
139
- import { sign } from "unjwt/jws";
140
- import { generateKey } from "unjwt/jwk";
141
-
142
- const payload = { userId: 123, permissions: ["read"] };
143
- const { privateKey } = await generateKey("RS256"); // Generates a CryptoKeyPair
144
-
145
- const token = await sign(payload, privateKey, { alg: "RS256" });
146
-
147
- console.log(token);
148
- // eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
149
- ```
150
-
151
- #### `verify(jws, key, options?)`
152
-
153
- Verifies a JWS token.
154
-
155
- - `jws`: The JWS token string.
156
- - `key`: The verification key (`CryptoKey`, `JWK`, `JWKSet`, `Uint8Array`, or a `KeyLookupFunction`).
157
- A `KeyLookupFunction` has the signature `(header: JWSProtectedHeader, jws: string) => Promise<CryptoKey | JWK | JWKSet | Uint8Array> | CryptoKey | JWK | JWKSet | Uint8Array`.
158
- - `options` (optional):
159
- - `algorithms`: An array of allowed JWS `alg` values. If not provided, the `alg` from the JWS header is used.
160
- - `validateJWT`: Unless false, will parse payload as JWT and validate claims if applicable (typ includes "jwt", case insensitive). Default `undefined`.
161
- - `critical`: An array of JWS header parameter names that the application understands and processes.
162
-
163
- Returns a `Promise<JWSVerifyResult<T>>` which is an object `{ payload: T, protectedHeader: JWSProtectedHeader }`.
164
- The `payload` type `T` can be `JWTClaims` (object), `string`, or `Uint8Array` depending on the JWS content and headers.
165
-
166
- **Example (HS256):**
167
-
168
- ```ts
169
- import { verify } from "unjwt/jws";
170
-
171
- const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // From HS256 sign example
172
- const secret = "supersecretkey";
173
-
174
- const { payload, protectedHeader } = await verify(token, secret);
175
- console.log(payload); // { message: "My important data" }
176
- console.log(protectedHeader); // { alg: "HS256", typ: "JWT" }
177
- ```
178
-
179
- **Example (RS256 with key lookup):**
180
-
181
- ```ts
182
- import { verify } from "unjwt/jws";
183
- import { generateKey } from "unjwt/jwk";
184
-
185
- const token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."; // From RS256 sign example
186
-
187
- // Example: using a key lookup function
188
- const { publicKey: rsaPublicKey } = await generateKey("RS256"); // For example purposes, assume publicKey is stored/fetched during lookup
189
-
190
- const keyLookup = async (header) => {
191
- if (header.alg === "RS256" /* && header.kid === 'expected-kid' */) {
192
- return rsaPublicKey;
193
- }
194
- throw new Error("Unsupported algorithm or key not found");
195
- };
196
-
197
- const { payload } = await verify(token, keyLookup, { algorithms: ["RS256"] });
198
- console.log(payload); // { userId: 123, permissions: ["read"] }
199
- ```
200
-
201
- ---
202
-
203
- ### JWE (JSON Web Encryption, [RFC 7516](https://datatracker.ietf.org/doc/html/rfc7516))
204
-
205
- Functions to encrypt and decrypt data according to the JWE specification.
206
-
207
- #### `encrypt(payload, key, options)`
208
-
209
- Encrypts data to produce a JWE token.
210
-
211
- - `payload`: The data to encrypt (`string`, `Uint8Array`, or a JSON-serializable `object`).
212
- - `key`: The Key Encryption Key (KEK) or password (`CryptoKey`, `JWK`, `string`, or `Uint8Array`).
213
- - `options`:
214
- - `alg`: (Required) The JWE Key Management algorithm (e.g., `"A128KW"`, `"RSA-OAEP-256"`, `"PBES2-HS256+A128KW"`, `"ECDH-ES+A128KW"`), defaults depends on the key provided.
215
- - `enc`: (Required) The JWE Content Encryption algorithm (e.g., `"A128GCM"`, `"A256CBC-HS512"`), defaults depends on the key provided.
216
- - `protectedHeader`: An object for JWE Protected Header parameters (e.g., `kid`, `typ`, `cty`, `crit`, `apu`, `apv`, `p2s`, `p2c`). If `payload` is an object and `typ` is not set, it defaults to `"JWT"`.
217
- - `cek`: (Optional) Provide your own Content Encryption Key (`CryptoKey` or `Uint8Array`).
218
- - `contentEncryptionIV`: (Optional) Provide your own Initialization Vector for content encryption (`Uint8Array`).
219
- - Other algorithm-specific options like `p2s`, `p2c` (for PBES2), `keyManagementIV`, `ecdhPartyUInfo`, `ecdhPartyVInfo`.
220
-
221
- Returns a `Promise<string>` resolving to the JWE token in Compact Serialization format.
222
-
223
- **Example (PBES2 password-based encryption):**
224
-
225
- ```ts
226
- import { encrypt } from "unjwt/jwe";
227
-
228
- const plaintext = "Secret message for password protection";
229
- const password = "myVeryStrongPassword123!";
29
+ ## Usage
230
30
 
231
- // Fallback to PBES2-HS256+A128KW and A128GCM if no `alg` and `end` are provided
232
- const jweToken = await encrypt(plaintext, password);
233
- console.log(jweToken);
234
- // JWE token string...
235
- ```
31
+ ### JWS — Sign & Verify
236
32
 
237
- **Example (A128KW with A128GCM):**
33
+ > Full reference: [skills/unjwt/references/jws.md](./skills/unjwt/references/jws.md)
238
34
 
239
35
  ```ts
240
- import { encrypt } from "unjwt/jwe";
241
- import { generateKey } from "unjwt/jwk";
36
+ import { sign, verify } from "unjwt/jws";
37
+ import { generateJWK } from "unjwt/jwk";
242
38
 
243
- const payload = { data: "sensitive information" };
244
- const kek = await generateKey("A128KW"); // AES Key Wrap key
39
+ // Generate a key (HMAC, RSA, ECDSA, EdDSA, etc.)
40
+ const key = await generateJWK("HS256");
245
41
 
246
- const jweToken = await encrypt(payload, kek, {
247
- alg: "A128KW",
248
- enc: "A128GCM",
249
- protectedHeader: { kid: "aes-key-1" },
250
- });
42
+ // Sign a JWT
43
+ const token = await sign(
44
+ { sub: "user123", role: "admin" },
45
+ key,
46
+ { expiresIn: "1h" }, // supports: number (seconds), "30s", "10m", "2h", "7D", "1Y"
47
+ );
251
48
 
252
- console.log(jweToken);
253
- // eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwia2lkIjoiYWVzLWtleS0xIn0...
49
+ // Verify and extract payload
50
+ const { payload, protectedHeader } = await verify(token, key);
51
+ console.log(payload); // { sub: "user123", role: "admin", iat: ..., exp: ... }
254
52
  ```
255
53
 
256
- #### `decrypt(jwe, key, options?)`
257
-
258
- Decrypts a JWE token.
259
-
260
- - `jwe`: The JWE token string.
261
- - `key`: The Key Decryption Key (KDK) or password (`CryptoKey`, `JWK`, `string`, `Uint8Array`, or a `JWEKeyLookupFunction`).
262
- A `JWEKeyLookupFunction` has the signature `(header: JWEHeaderParameters) => Promise<CryptoKey | JWK | string | Uint8Array> | CryptoKey | JWK | string | Uint8Array`.
263
- - `options` (optional):
264
- - `algorithms`: Array of allowed JWE Key Management `alg` values.
265
- - `validateJWT`: Unless false, will parse payload as JWT and validate claims if applicable (typ includes "jwt", case insensitive). Default `undefined`.
266
- - `encryptionAlgorithms`: Array of allowed JWE Content Encryption `enc` values.
267
- - `critical`: Array of JWE header parameter names that the application understands.
268
- - `unwrappedKeyAlgorithm`: (For `unwrapKey` internally) Algorithm details for the CEK after unwrapping.
269
- - `keyUsage`: (For `unwrapKey` internally) Intended usages for the unwrapped CEK.
270
-
271
- Returns a `Promise<JWEDecryptResult<T>>` which is an object `{ payload: T, protectedHeader: JWEHeaderParameters, cek: Uint8Array, aad: Uint8Array }`.
272
- The `payload` type `T` can be `JWTClaims` (object) or `string`.
273
-
274
- **Example (PBES2 password-based decryption):**
54
+ **Asymmetric keys (RS256, ES256, PS256, Ed25519, etc.):**
275
55
 
276
56
  ```ts
277
- import { decrypt } from "unjwt/jwe";
278
- // const jweToken = ...; // From PBES2 encrypt example
279
- // const password = "myVeryStrongPassword123!";
280
-
281
- const { payload } = await decrypt(jweToken, password);
57
+ const keys = await generateJWK("RS256");
58
+ const token = await sign({ sub: "user123" }, keys.privateKey);
59
+ const { payload } = await verify(token, keys.publicKey);
282
60
  ```
283
61
 
284
- **Example (A128KW with A128GCM):**
62
+ **Key lookup function for dynamic key resolution:**
285
63
 
286
64
  ```ts
287
- import { decrypt } from "unjwt/jwe";
288
- // const jweToken = "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwia2lkIjoiYWVzLWtleS0xIn0...";
289
- // const kek = ...; // The same AES Key Wrap key used for encryption
290
-
291
- async function decryptData(jweToken: string, kek: CryptoKey) {
292
- try {
293
- const { payload, protectedHeader, cek } = await decrypt(jweToken, kek, {
294
- algorithms: ["A128KW"],
295
- encryptionAlgorithms: ["A128GCM"],
296
- });
297
- console.log("Decrypted Plaintext:", payload);
298
- console.log("Protected Header:", protectedHeader);
299
- // console.log("CEK (Content Encryption Key):", cek);
300
- } catch (error) {
301
- console.error("Decryption failed:", error);
302
- }
303
- }
65
+ const { payload } = await verify(
66
+ token,
67
+ async (header) => {
68
+ // Resolve key by kid, alg, or any header field
69
+ return await fetchPublicKeyByKid(header.kid);
70
+ },
71
+ { algorithms: ["RS256"] },
72
+ );
304
73
  ```
305
74
 
306
- ---
307
-
308
- ### JWK (JSON Web Key, [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517))
309
-
310
- Utilities for working with JSON Web Keys.
311
-
312
- #### `generateKey(alg, options?)`
313
-
314
- Generates a cryptographic key.
75
+ ### JWE — Encrypt & Decrypt
315
76
 
316
- - `alg`: The JWA algorithm identifier for the key to be generated (e.g., `"HS256"`, `"RS256"`, `"ES256"`, `"A128KW"`, `"A128GCM"`, `"A128CBC-HS256"`).
317
- - `options` (optional):
318
- - `toJWK`: If `true`, returns the key(s) in JWK format. Otherwise, returns `CryptoKey`(s) or `Uint8Array` (for composite keys like AES-CBC-HS\*). Default `false`.
319
- - `extractable`: Boolean, whether the generated `CryptoKey` can be exported. Default `true`.
320
- - `keyUsage`: Array of `KeyUsage` strings. Defaults are algorithm-specific.
321
- - `modulusLength`: For RSA keys (e.g., `2048`, `4096`). Default `2048`.
322
- - `publicExponent`: For RSA keys. Default `new Uint8Array([0x01, 0x00, 0x01])`.
323
-
324
- Returns a `Promise` resolving to `CryptoKey`, `CryptoKeyPair`, `Uint8Array` (for composite keys), `JWK`, or `{ privateKey: JWK, publicKey: JWK }` depending on `alg` and `options.toJWK`.
325
-
326
- **Examples:**
77
+ > Full reference: [skills/unjwt/references/jwe.md](./skills/unjwt/references/jwe.md)
327
78
 
328
79
  ```ts
329
- import { generateKey } from "unjwt/jwk";
330
-
331
- // Generate an HS256 CryptoKey
332
- const hmacKey = await generateKey("HS256");
333
- console.log(hmacKey); // CryptoKey
334
-
335
- // Generate an RS256 CryptoKeyPair
336
- const rsaKeyPair = await generateKey("RS256", { modulusLength: 2048 });
337
- console.log(rsaKeyPair.publicKey); // CryptoKey
338
- console.log(rsaKeyPair.privateKey); // CryptoKey
339
-
340
- // Generate an ES384 key pair as JWKs
341
- const ecJwks = await generateKey("ES384", { toJWK: true });
342
- console.log(ecJwks.publicKey); // JWK
343
- console.log(ecJwks.privateKey); // JWK
344
-
345
- // Generate a composite key for A128CBC-HS256 as Uint8Array
346
- const aesCbcHsKeyBytes = await generateKey("A128CBC-HS256");
347
- console.log(aesCbcHsKeyBytes); // Uint8Array (32 bytes: 16 for AES, 16 for HMAC)
80
+ import { encrypt, decrypt } from "unjwt/jwe";
348
81
 
349
- // Generate an A256GCM key as a JWK
350
- const aesGcmJwk = await generateKey("A256GCM", { toJWK: true });
351
- console.log(aesGcmJwk); // JWK
82
+ // Password-based encryption (simplest — uses PBES2)
83
+ const token = await encrypt({ secret: "sensitive data" }, "my-password");
84
+ const { payload } = await decrypt(token, "my-password");
352
85
  ```
353
86
 
354
- #### `deriveKeyFromPassword(password, alg, options)`
355
-
356
- Derives a key from a password using PBKDF2 for PBES2 algorithms.
357
-
358
- - `password`: The password (`string` or `Uint8Array`).
359
- - `alg`: The PBES2 algorithm (e.g., `"PBES2-HS256+A128KW"`).
360
- - `options`:
361
- - `salt`: The salt (`Uint8Array`, at least 8 octets).
362
- - `iterations`: The iteration count (positive integer).
363
- - `toJWK`: If `true`, returns a `JWK_oct`. Otherwise, `CryptoKey`. Default `false`.
364
- - `extractable`: Boolean for `CryptoKey`. Default `false` unless `toJWK` is true.
365
- - `keyUsage`: For `CryptoKey`. Default `["wrapKey", "unwrapKey"]`.
366
-
367
- Returns a `Promise` resolving to `CryptoKey` or `JWK_oct`.
368
-
369
- **Example:**
87
+ **With explicit key management:**
370
88
 
371
89
  ```ts
372
- import { deriveKeyFromPassword } from "unjwt/jwk";
373
- import { randomBytes, textEncoder } from "unjwt/utils";
90
+ import { generateJWK } from "unjwt/jwk";
374
91
 
375
- const password = "mySecretPassword";
376
- const salt = randomBytes(16);
377
- const iterations = 4096;
92
+ // AES Key Wrap
93
+ const aesKey = await generateJWK("A256KW");
94
+ const token = await encrypt({ data: "secret" }, aesKey);
378
95
 
379
- const derivedKey = await deriveKeyFromPassword(password, "PBES2-HS384+A192KW", {
380
- salt,
381
- iterations,
382
- });
383
- console.log(derivedKey); // CryptoKey for AES-KW (192-bit)
96
+ // RSA-OAEP
97
+ const rsaKeys = await generateJWK("RSA-OAEP-256");
98
+ const token2 = await encrypt({ data: "secret" }, rsaKeys.publicKey);
99
+ const { payload } = await decrypt(token2, rsaKeys.privateKey);
384
100
 
385
- const derivedJwk = await deriveKeyFromPassword(password, "PBES2-HS512+A256KW", {
386
- salt,
387
- iterations,
388
- toJWK: true,
389
- });
390
- console.log(derivedJwk); // JWK_oct { kty: "oct", k: "...", alg: "A256KW" }
101
+ // ECDH-ES
102
+ const ecKeys = await generateJWK("ECDH-ES+A256KW");
103
+ const token3 = await encrypt({ data: "secret" }, ecKeys.publicKey);
391
104
  ```
392
105
 
393
- #### `importKey(keyMaterial, alg?)`
394
-
395
- Imports a key from various formats. This is a flexible wrapper.
396
-
397
- - `keyMaterial`: The key to import. Can be:
398
- - `CryptoKey`: Returned directly.
399
- - `Uint8Array`: Returned directly (treated as raw symmetric key bytes).
400
- - `string`: Encoded to `Uint8Array` and returned.
401
- - `JWK_oct` (symmetric JWK with `k` property): The `k` value is Base64URL decoded and returned as `Uint8Array`.
402
- - Other `JWK` types (asymmetric): Imported into a `CryptoKey`.
403
- - `alg` (optional): The JWA algorithm string. **Required** when importing asymmetric JWKs (e.g., RSA, EC) to provide context for `crypto.subtle.importKey`.
106
+ ### JWK — Key Management
404
107
 
405
- Returns a `Promise` resolving to `CryptoKey` or `Uint8Array`.
406
-
407
- **Examples:**
108
+ > Full reference: [skills/unjwt/references/jwk.md](./skills/unjwt/references/jwk.md)
408
109
 
409
110
  ```ts
410
- import { importKey } from "unjwt/jwk";
411
- import { textEncoder, base64UrlDecode } from "unjwt/utils";
412
-
413
- // Import raw symmetric key bytes
414
- const rawBytes = textEncoder.encode("a-32-byte-long-secret-key-123"); // 32 bytes for AES-256 or HS256
415
- const symmetricKeyBytes = await importKey(rawBytes);
416
- console.log(symmetricKeyBytes); // Uint8Array
417
-
418
- // Import a symmetric JWK (kty: "oct")
419
- const octJwk = {
420
- kty: "oct",
421
- k: "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr0", // Example key
422
- };
423
- const importedOctBytes = await importKey(octJwk); // Returns Uint8Array
424
- console.log(importedOctBytes); // Uint8Array (decoded from k)
425
-
426
- // Import an RSA Public Key JWK
427
- const rsaPublicJwk = {
428
- kty: "RSA",
429
- n: "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3ok92YEnjsADC4Ue87zwRzH2J-TCwlcQrY3E9gGZJZL2g_2_5QjLhL0gR0xYj04_N4M",
430
- e: "AQAB",
431
- alg: "RS256",
432
- kid: "rsa-pub-1",
433
- };
434
- const rsaPublicKey = await importKey(rsaPublicJwk, "RS256"); // 'alg' is crucial here
435
- console.log(rsaPublicKey); // CryptoKey
436
- ```
437
-
438
- #### `exportKey(key, jwk?)`
439
-
440
- Exports a `CryptoKey` to JWK format.
441
-
442
- - `key`: The `CryptoKey` to export (must be `extractable`).
443
- - `jwk` (optional): A partial `JWK` object to merge with the exported properties (e.g., to add `kid`, `use`, or override `alg`).
111
+ import {
112
+ generateKey,
113
+ generateJWK,
114
+ importKey,
115
+ exportKey,
116
+ importJWKFromPEM,
117
+ exportJWKToPEM,
118
+ wrapKey,
119
+ unwrapKey,
120
+ deriveKeyFromPassword,
121
+ } from "unjwt/jwk";
444
122
 
445
- Returns a `Promise<JWK>`.
123
+ // Generate keys as CryptoKey or JWK
124
+ const hmacKey = await generateKey("HS256"); // CryptoKey
125
+ const rsaPair = await generateKey("RS256"); // CryptoKeyPair
126
+ const ecJwk = await generateJWK("ES256", { kid: "k1" }); // { privateKey: JWK, publicKey: JWK }
446
127
 
447
- **Example:**
128
+ // PEM conversion
129
+ const jwk = await importJWKFromPEM(pemString, "spki", "RS256", undefined, { kid: "rsa-1" });
130
+ const pem = await exportJWKToPEM(jwk, "spki");
448
131
 
449
- ```ts
450
- import { generateKey, exportKey } from "unjwt/jwk";
451
-
452
- const { publicKey } = await generateKey("ES256"); // Generates an extractable CryptoKey
453
-
454
- const jwk = await exportKey(publicKey, { kid: "ec-key-001", use: "sig" });
455
- console.log(jwk);
456
- // {
457
- // kty: 'EC',
458
- // crv: 'P-256',
459
- // x: '...',
460
- // y: '...',
461
- // ext: true,
462
- // key_ops: [ 'verify' ], // or as per generation
463
- // kid: 'ec-key-001',
464
- // use: 'sig'
465
- // }
132
+ // Key wrapping
133
+ const cek = crypto.getRandomValues(new Uint8Array(32));
134
+ const { encryptedKey } = await wrapKey("A256KW", cek, aesKey);
135
+ const unwrapped = await unwrapKey("A256KW", encryptedKey, aesKey, { returnAs: false });
466
136
  ```
467
137
 
468
- #### `wrapKey(alg, keyToWrap, wrappingKey, options?)`
469
-
470
- Wraps a Content Encryption Key (CEK).
471
-
472
- - `alg`: The JWA key management algorithm (e.g., `"A128KW"`, `"RSA-OAEP"`).
473
- - `keyToWrap`: The CEK to wrap (`CryptoKey` or `Uint8Array`).
474
- - `wrappingKey`: The Key Encryption Key (KEK) (`CryptoKey`, `JWK`, or password `string`/`Uint8Array` for PBES2).
475
- - `options` (optional): Algorithm-specific options (e.g., `p2s`, `p2c` for PBES2; `iv` for AES-GCMKW).
476
-
477
- Returns a `Promise<WrapKeyResult>` containing `encryptedKey` and other parameters like `iv`, `tag`, `epk`, `p2s`, `p2c` as needed by the algorithm.
138
+ ### Utility Functions
478
139
 
479
- **Example (AES Key Wrap):**
140
+ > Full reference: [skills/unjwt/references/utils.md](./skills/unjwt/references/utils.md)
480
141
 
481
142
  ```ts
482
- import { wrapKey, generateKey } from "unjwt/jwk";
483
- import { randomBytes } from "unjwt/utils";
484
-
485
- const cekToWrap = randomBytes(32); // e.g., a 256-bit AES key as Uint8Array
486
- const kek = await generateKey("A128KW"); // 128-bit AES Key Wrap key
143
+ import {
144
+ base64UrlEncode,
145
+ base64UrlDecode,
146
+ randomBytes,
147
+ isJWK,
148
+ isJWKSet,
149
+ isSymmetricJWK,
150
+ isPrivateJWK,
151
+ isPublicJWK,
152
+ isCryptoKey,
153
+ textEncoder,
154
+ textDecoder,
155
+ } from "unjwt/utils";
487
156
 
488
- const { encryptedKey } = await wrapKey("A128KW", cekToWrap, kek);
489
- console.log("Wrapped CEK:", encryptedKey); // Uint8Array
157
+ const bytes = randomBytes(32);
158
+ const encoded = base64UrlEncode(bytes);
159
+ const decoded = base64UrlDecode(encoded, false); // Uint8Array
490
160
  ```
491
161
 
492
- #### `unwrapKey(alg, wrappedKey, unwrappingKey, options?)`
162
+ ### H3 Session Adapters
493
163
 
494
- Unwraps a Content Encryption Key (CEK).
164
+ > Full reference: [skills/unjwt/references/adapters-h3.md](./skills/unjwt/references/adapters-h3.md)
495
165
 
496
- - `alg`: The JWA key management algorithm.
497
- - `wrappedKey`: The encrypted CEK (`Uint8Array`).
498
- - `unwrappingKey`: The Key Decryption Key (KDK).
499
- - `options` (optional):
500
- - `returnAs`: If `false`, returns `Uint8Array`. If `true` (default) or undefined, returns `CryptoKey`.
501
- - `unwrappedKeyAlgorithm`: `AlgorithmIdentifier` for the imported CEK if `returnAs` is `true`.
502
- - `keyUsage`: `KeyUsage[]` for the imported CEK if `returnAs` is `true`.
503
- - `extractable`: Boolean for the imported CEK.
504
- - Other algorithm-specific options (e.g., `p2s`, `p2c`, `iv`, `tag`, `epk`).
166
+ Cookie-based JWT session management for H3 applications. Sessions are stored as encrypted (JWE) or signed (JWS) tokens in chunked cookies.
505
167
 
506
- Returns a `Promise` resolving to the unwrapped CEK as `CryptoKey` or `Uint8Array`.
168
+ > **Note:** Sessions are lazy — `session.id` is `undefined` until `session.update()` is called. This is intentional for OAuth/spec-compliant flows where sessions should only be created upon valid operations.
507
169
 
508
- **Example (AES Key Unwrap):**
170
+ #### JWE Session (encrypted, recommended for sensitive data)
509
171
 
510
172
  ```ts
511
- import { unwrapKey, generateKey } from "unjwt/jwk";
512
- // const encryptedKey = ...; // From wrapKey example
513
- // const kdk = ...; // Same KEK used for wrapping
514
-
515
- async function unwrapMyKey(encryptedKey: Uint8Array, kdk: CryptoKey) {
516
- const unwrappedCekBytes = await unwrapKey("A128KW", encryptedKey, kdk, {
517
- returnAs: false, // Get raw bytes
518
- });
519
- console.log("Unwrapped CEK (bytes):", unwrappedCekBytes); // Uint8Array
173
+ import { useJWESession, generateJWK } from "unjwt/adapters/h3v2";
520
174
 
521
- const unwrappedCekCryptoKey = await unwrapKey("A128KW", encryptedKey, kdk, {
522
- returnAs: true, // Get CryptoKey
523
- unwrappedKeyAlgorithm: { name: "AES-GCM", length: 256 }, // Specify CEK's intended alg
524
- keyUsage: ["encrypt", "decrypt"],
175
+ app.get("/profile", async (event) => {
176
+ const session = await useJWESession(event, {
177
+ key: process.env.SESSION_SECRET!, // password string, symmetric JWK, or asymmetric keypair
178
+ maxAge: "7D",
525
179
  });
526
- console.log("Unwrapped CEK (CryptoKey):", unwrappedCekCryptoKey); // CryptoKey
527
- }
528
- ```
529
-
530
- #### `importJWKFromPEM(pem, pemType, alg, importOptions?, jwkExtras?)`
531
-
532
- Imports a key from a PEM-encoded string and converts it to a JWK.
533
-
534
- - `pem`: The PEM-encoded string (including `-----BEGIN ...-----` and `-----END ...-----` markers).
535
- - `pemType`: The type of PEM encoding:
536
- - `"pkcs8"`: For private keys in PKCS#8 format.
537
- - `"spki"`: For public keys in SPKI format.
538
- - `"x509"`: For X.509 certificates (extracts the public key).
539
- - `alg`: The JWA algorithm identifier (e.g., `"RS256"`, `"ES256"`). This is crucial for `crypto.subtle.importKey` to understand the key's intended algorithm and for setting the `'alg'` field in the resulting JWK.
540
- - `importOptions` (optional): Options for the underlying `crypto.subtle.importKey` call:
541
- - `extractable`: Boolean, whether the imported `CryptoKey` should be extractable. Defaults to `true`.
542
- - `keyUsage`: Array of `KeyUsage` strings for the imported `CryptoKey`.
543
- - `jwkExtras` (optional): An object containing additional properties to merge into the resulting JWK (e.g., `"kid"`, `"use"`).
544
-
545
- Returns a `Promise<JWK>` resolving to the imported key as a JWK.
546
-
547
- **Example:**
548
-
549
- ```ts
550
- import { importJWKFromPEM } from "unjwt/jwk";
551
-
552
- const rsaPublicJwk = await importJWKFromPEM(
553
- provess.env.RSA_PEM_SPKI, // PEM string
554
- "spki",
555
- "RS256",
556
- { extractable: false },
557
- { kid: "my-rsa-key" }, // Additional properties to add to the JWK
558
- );
559
- console.log(rsaPublicJwk);
560
- // {
561
- // kty: 'RSA',
562
- // alg: 'RS256',
563
- // kid: 'my-rsa-key',
564
- // n: '...',
565
- // e: 'AQAB',
566
- // ext: false,
567
- // key_ops: [ 'verify' ]
568
- // }
569
- ```
570
-
571
- #### exportJWKToPEM(jwk, pemFormat, algForCryptoKeyImport?)
572
-
573
- Exports a JWK to a PEM-encoded string.
574
-
575
- - jwk: The JWK to export.
576
- - pemFormat: The desired PEM format:
577
- - "pkcs8": For private keys in PKCS#8 format.
578
- - "spki": For public keys in SPKI format.
579
- - algForCryptoKeyImport (optional): If the JWK does not have an 'alg' property, this algorithm hint is required to correctly convert it to a CryptoKey first. This is only needed if the JWK lacks an alg property.
580
180
 
581
- Returns a `Promise<string>` resolving to the PEM-encoded key string.
582
-
583
- Example:
181
+ if (!session.id) {
182
+ // No active session — create one
183
+ await session.update({ userId: "123", email: "user@example.com" });
184
+ }
584
185
 
585
- ```ts
586
- import { exportJWKToPEM } from "unjwt/jwk";
587
- import { rsaJWK } from "./keys"; // Assuming you have JWKs in keys.ts
588
-
589
- const rsaPrivatePem = await exportJWKToPEM(rsaJWK.private, "pkcs8");
590
- console.log(rsaPrivatePem);
591
- // -----BEGIN PRIVATE KEY-----
592
- // MII...
593
- // -----END PRIVATE KEY-----
594
-
595
- const rsaPublicSpki = await exportJWKToPEM(
596
- rsaJWK.public,
597
- "spki",
598
- "RS256", // this is required if `rsaJWK.public.alg` is undefined
599
- );
600
- console.log(rsaPublicSpki);
601
- // -----BEGIN PUBLIC KEY-----
602
- // MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQE...
603
- // -----END PUBLIC KEY-----
186
+ return { user: session.data };
187
+ });
604
188
  ```
605
189
 
606
- ### Adapters
607
-
608
- #### H3 v1 (Nuxt v4, Nitro v2)
609
-
610
- The `h3v1` adapter bundles session helpers that store data inside signed or encrypted JWTs.
611
-
612
- - `useJWESession(event, config)` encrypts the session payload with the provided `secret` (password string or private/symmetric JWK). Use this when session data must remain confidential. (cookie's `httpOnly: true` by default)
613
- - `useJWSSession(event, config)` signs, but does not encrypt, the session payload with `config.key`. Use this when clients may read the session content but you still need tamper protection. (cookie's `httpOnly: false` by default)
614
-
615
- Both helpers expose the same API: read `session.id` / `session.data`, call `session.update()` to patch values, and `session.clear()` to invalidate the cookie. When setting `maxAge`, `exp` claim is automatically managed and validated. They also support a number of `hooks` in the config for custom logic (e.g. logging, refreshing, etc).
190
+ #### JWS Session (signed, readable by client)
616
191
 
617
192
  ```ts
618
- import { defineEventHandler } from "h3";
619
- import { useJWESession, useJWSSession, generateJWK } from "unjwt/adapters/h3v1";
620
-
621
- const keys = await generateJWK("RS256"); // Make sure to persist these keys somewhere!
622
-
623
- export default defineEventHandler(async (event) => {
624
- const privateSession = await useJWESession(event, {
625
- name: "app-session",
626
- secret: process.env.SESSION_SECRET!, // or symmetric or asymmetric keypair
627
- });
193
+ import { useJWSSession, generateJWK } from "unjwt/adapters/h3v2";
628
194
 
629
- await privateSession.update((data) => ({
630
- visits: (data.visits ?? 0) + 1,
631
- }));
195
+ const keys = await generateJWK("RS256"); // persist these!
632
196
 
633
- const publicSession = await useJWSSession(event, {
634
- name: "app-session-public",
635
- key: keys, // you can directly pass symmetric or asymmetric keypairs
636
- maxAge: 60 * 60, // seconds
197
+ app.get("/preferences", async (event) => {
198
+ const session = await useJWSSession(event, {
199
+ key: keys,
200
+ maxAge: "24h",
637
201
  });
638
202
 
639
- return {
640
- encryptedSession: privateSession.data,
641
- signedSession: publicSession.data,
642
- };
203
+ return { theme: session.data.theme };
643
204
  });
644
205
  ```
645
206
 
646
- #### H3 v2 (Nuxt v5, Nitro v3)
207
+ #### Session features
647
208
 
648
- The `h3v2` adapter bundles session helpers that store data inside signed or encrypted JWTs.
209
+ - **Cookie chunking** — large tokens are automatically split across multiple cookies
210
+ - **Header-based tokens** — read from `Authorization: Bearer <token>` or custom headers via `sessionHeader`
211
+ - **Lifecycle hooks** — `onRead`, `onUpdate`, `onClear`, `onExpire`, `onError` for logging, refresh logic, etc.
212
+ - **Key lookup hooks** — `onUnsealKeyLookup` (JWE) / `onVerifyKeyLookup` (JWS) for key rotation
213
+ - **TypeScript generics** — strongly-typed session data via `useJWESession<MyData>(event, config)`
649
214
 
650
- - `useJWESession(event, config)` encrypts the session payload with the provided `secret` (password string or private/symmetric JWK). Use this when session data must remain confidential. (cookie's `httpOnly: true` by default)
651
- - `useJWSSession(event, config)` signs, but does not encrypt, the session payload with `config.key`. Use this when clients may read the session content but you still need tamper protection. (cookie's `httpOnly: false` by default)
652
-
653
- Both helpers expose the same API: read `session.id` / `session.data`, call `session.update()` to patch values, and `session.clear()` to invalidate the cookie. When setting `maxAge`, `exp` claim is automatically managed and validated. They also support a number of `hooks` in the config for custom logic (e.g. logging, refreshing, etc).
215
+ #### Refresh token pattern
654
216
 
655
217
  ```ts
656
- import { H3, HTTPError, serve } from "h3v2";
657
-
658
218
  import {
659
219
  type SessionConfigJWE,
660
220
  type SessionConfigJWS,
@@ -665,112 +225,31 @@ import {
665
225
  generateJWK,
666
226
  } from "unjwt/adapters/h3v2";
667
227
 
668
- const atJwk = await generateJWK("RS256"); // Make sure to persist these keys somewhere!
228
+ const atKeys = await generateJWK("RS256");
669
229
 
670
- const jweOptions = {
671
- key: "refresh_token_secret",
230
+ const refreshConfig = {
231
+ key: process.env.REFRESH_SECRET!,
672
232
  name: "refresh_token",
673
233
  } satisfies SessionConfigJWE;
674
234
 
675
- const jwsOptions = {
676
- key: atJwk,
235
+ const accessConfig = {
236
+ key: atKeys,
677
237
  name: "access_token",
678
- maxAge: 15 * 60, // 15 minutes
238
+ maxAge: "15m",
679
239
  hooks: {
680
240
  async onExpire({ event, config }) {
681
- const refreshSession = await getJWESession(event, jweOptions);
682
- if (!refreshSession.data.sub) {
683
- // no valid refresh session, nothing to do
684
- return;
241
+ const refresh = await getJWESession(event, refreshConfig);
242
+ if (refresh.data.sub) {
243
+ await updateJWSSession(event, config, {
244
+ sub: refresh.data.sub,
245
+ scope: refresh.data.scope,
246
+ });
685
247
  }
686
-
687
- console.log("Access token expired, refreshing...");
688
-
689
- // refresh the access token
690
- await updateJWSSession(event, config, {
691
- sub: refreshSession.data.sub,
692
- scope: refreshSession.data.scope,
693
- });
694
248
  },
695
249
  },
696
250
  } satisfies SessionConfigJWS;
697
-
698
- const app = new H3();
699
-
700
- app.post("/login", async (event) => {
701
- const refreshSession = await useJWESession(event, jweOptions);
702
- const accessSession = await useJWSSession(event, jwsOptions);
703
-
704
- if (accessSession.data.sub) {
705
- // user already logged in, return existing info
706
- return {
707
- accessToken: {
708
- id: accessSession.id,
709
- createdAt: accessSession.createdAt,
710
- expiresAt: accessSession.expiresAt,
711
- data: accessSession.data,
712
- },
713
- refreshSession: {
714
- id: refreshSession.id,
715
- createdAt: refreshSession.createdAt,
716
- expiresAt: refreshSession.expiresAt,
717
- data: refreshSession.data,
718
- },
719
- };
720
- }
721
-
722
- const data = (await event.req.json()) as {
723
- username?: string;
724
- password?: string;
725
- };
726
-
727
- if (!data.username || !data.password) {
728
- throw new HTTPError("Username and password are required", { status: 400 });
729
- }
730
-
731
- // validate user credentials here
732
-
733
- const claims = {
734
- sub: data.username,
735
- scope: ["read:profile"],
736
- };
737
-
738
- await accessSession.update(claims);
739
-
740
- await refreshSession.update(claims);
741
-
742
- return {
743
- accessToken: {
744
- id: accessSession.id,
745
- createdAt: accessSession.createdAt,
746
- expiresAt: accessSession.expiresAt,
747
- data: accessSession.data,
748
- },
749
- refreshSession: {
750
- id: refreshSession.id,
751
- createdAt: refreshSession.createdAt,
752
- expiresAt: refreshSession.expiresAt,
753
- data: refreshSession.data,
754
- },
755
- };
756
- });
757
-
758
- serve(app);
759
251
  ```
760
252
 
761
- ---
762
-
763
- ### Utility Functions
764
-
765
- `unjwt/utils` exports several helpful functions:
766
-
767
- - `base64UrlEncode(data: Uint8Array | string): string`
768
- - `base64UrlDecode(str?: string, toString?: boolean): Uint8Array | string` (Decodes to string by default, or `Uint8Array` if `toString` is `false`)
769
- - `randomBytes(length: number): Uint8Array`
770
- - `textEncoder: TextEncoder`
771
- - `textDecoder: TextDecoder`
772
- - Type guards: `isJWK(key)`, `isCryptoKey(key)`, `isCryptoKeyPair(keyPair)`
773
-
774
253
  ## Development
775
254
 
776
255
  <details>
@@ -781,7 +260,7 @@ serve(app);
781
260
  - Install latest LTS version of [Node.js](https://nodejs.org/en/)
782
261
  - Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
783
262
  - Install dependencies using `pnpm install`
784
- - Run interactive tests using `pnpm dev`
263
+ - Run tests using `pnpm test`
785
264
 
786
265
  </details>
787
266