unjwt 0.7.0 → 0.7.1

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/README.md CHANGED
@@ -4,16 +4,18 @@
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
- A collection of low-level JWT ([RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519)) utilities using the Web Crypto API. Supports:
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.
8
8
 
9
- - **JWS** ([RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515)) — sign and verify tokens using HMAC, RSA, RSA-PSS, ECDSA, and EdDSA algorithms
10
- - **JWE** ([RFC 7516](https://datatracker.ietf.org/doc/html/rfc7516)) — encrypt and decrypt data using Direct (`dir`), AES Key Wrap, AES-GCM KW, RSA-OAEP, PBES2, and ECDH-ES key management with AES-GCM or AES-CBC+HMAC-SHA2 content encryption
11
- - **JWK** ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) — generate, import, export, wrap, and unwrap keys (CryptoKey, JWK, PEM)
12
- - **Framework adapters** for cookie-based JWT sessions:
13
- - [H3 v1](./skills/unjwt/references/adapters-h3.md) (Nuxt v4, Nitro v2)
14
- - [H3 v2](./skills/unjwt/references/adapters-h3.md) (Nuxt v5, Nitro v3)
9
+ > **📖 Documentation — [unjwt.s94.dev](https://unjwt.s94.dev)**
15
10
 
16
- Zero runtime dependencies for core. Optional peer deps for adapters (`h3`, `cookie-es`, `rou3`).
11
+ ## Features
12
+
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
+ - **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
+ - **JWK** ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) — generate, import, export, wrap, unwrap. `CryptoKey` ↔ JWK ↔ PEM conversion. ECDH-ES shared-secret derivation. JWKS fetching, lookup, and rotation.
16
+ - **Framework adapters** — cookie-based JWT sessions for [H3 v1](https://unjwt.s94.dev/adapters) (Nuxt v4 / Nitro v2) and [H3 v2](https://unjwt.s94.dev/adapters) (Nuxt v5 / Nitro v3).
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`, `rou3`).
17
19
 
18
20
  ## Install
19
21
 
@@ -22,376 +24,39 @@ Zero runtime dependencies for core. Optional peer deps for adapters (`h3`, `cook
22
24
  npx nypm install unjwt
23
25
  ```
24
26
 
25
- If you would like to install the agent skill you can run:
27
+ Agent skill for Claude Code (and compatible harnesses):
26
28
 
27
29
  ```sh
28
30
  npx skills add sandros94/unjwt
29
31
  ```
30
32
 
31
- ## Usage
32
-
33
- ### JWS — Sign & Verify
34
-
35
- > Full reference: [skills/unjwt/references/jws.md](./skills/unjwt/references/jws.md)
33
+ ## Quickstart
36
34
 
37
35
  ```ts
36
+ // Sign and verify a JWT (JWS)
38
37
  import { sign, verify } from "unjwt/jws";
39
38
  import { generateJWK } from "unjwt/jwk";
40
- // or import both from the root: import { sign, verify, generateJWK } from "unjwt";
41
39
 
42
- // Generate a key (HMAC, RSA, ECDSA, EdDSA, etc.)
43
40
  const key = await generateJWK("HS256");
41
+ const token = await sign({ sub: "user_1" }, key, { expiresIn: "1h" });
42
+ const { payload } = await verify(token, key);
43
+ // { sub: "user_1", iat: ..., exp: ... }
44
44
 
45
- // Sign a JWT
46
- const token = await sign(
47
- { sub: "user123", role: "admin" },
48
- key,
49
- { expiresIn: "1h" }, // supports: number (seconds), "30s", "10m", "2h", "7D", "1Y"
50
- );
51
-
52
- // Verify and extract payload
53
- const { payload, protectedHeader } = await verify(token, key);
54
- console.log(payload); // { sub: "user123", role: "admin", iat: ..., exp: ... }
55
- ```
56
-
57
- **Asymmetric keys (RS256, ES256, PS256, Ed25519, etc.):**
58
-
59
- ```ts
60
- const keys = await generateJWK("RS256");
61
- const token = await sign({ sub: "user123" }, keys.privateKey);
62
- const { payload } = await verify(token, keys.publicKey);
63
- ```
64
-
65
- **Key lookup function for dynamic key resolution:**
66
-
67
- ```ts
68
- const { payload } = await verify(
69
- token,
70
- async (header) => {
71
- // Resolve key by kid, alg, or any header field
72
- return await fetchPublicKeyByKid(header.kid);
73
- },
74
- { algorithms: ["RS256"] },
75
- );
76
- ```
77
-
78
- **JWKSet for key rotation and JWKS endpoint consumption:**
79
-
80
- When a `JWKSet` is passed (directly or returned from a lookup function), `verify` tries each matching key in order until one succeeds. If the token carries a `kid`, only keys with that exact `kid` are tried — no retry. If `kid` is absent, all keys in the set whose `alg` is compatible become candidates. This makes transparent key rotation possible without any retry logic in userland.
81
-
82
- ```ts
83
- // Fetch all public keys from a JWKS endpoint — library picks the right one
84
- const jwks = await fetch("https://auth.example.com/.well-known/jwks.json").then((r) => r.json());
85
- const { payload } = await verify(token, jwks);
86
-
87
- // Rotation window: old tokens (no kid) are tried against all keys automatically
88
- const rotatingSet = { keys: [newKey, legacyKey] };
89
- const { payload: p } = await verify(oldToken, rotatingSet);
90
- ```
91
-
92
- ### JWE — Encrypt & Decrypt
93
-
94
- > Full reference: [skills/unjwt/references/jwe.md](./skills/unjwt/references/jwe.md)
95
-
96
- ```ts
97
- import { encrypt, decrypt } from "unjwt/jwe";
98
- // or: import { encrypt, decrypt } from "unjwt";
99
-
100
- // Password-based encryption (simplest — uses PBES2)
101
- const token = await encrypt({ secret: "sensitive data" }, "my-password");
102
- const { payload } = await decrypt(token, "my-password");
103
- ```
104
-
105
- **With explicit key management:**
106
-
107
- ```ts
108
- import { generateJWK } from "unjwt/jwk";
109
-
110
- // AES Key Wrap
111
- const aesKey = await generateJWK("A256KW");
112
- const token = await encrypt({ data: "secret" }, aesKey);
113
-
114
- // RSA-OAEP
115
- const rsaKeys = await generateJWK("RSA-OAEP-256");
116
- const token2 = await encrypt({ data: "secret" }, rsaKeys.publicKey);
117
- const { payload } = await decrypt(token2, rsaKeys.privateKey);
118
-
119
- // ECDH-ES
120
- const ecKeys = await generateJWK("ECDH-ES+A256KW");
121
- const token3 = await encrypt({ data: "secret" }, ecKeys.publicKey);
122
- ```
123
-
124
- ### ECDH-ES — End-to-End Encrypted Messaging
125
-
126
- In end-to-end encryption (E2EE), each participant holds a **key pair**: the **private key** never leaves the owner's system; the **public key** is distributed freely. A sender encrypts a message using the recipient's public key, producing a token that only the recipient — holding the matching private key — can decrypt.
127
-
128
- #### One-time key setup
129
-
130
- Each participant generates a key pair once and persists it. The public key is shared out-of-band — published to a server endpoint, written to a config file, exchanged at registration, etc.:
131
-
132
- ```ts
133
- import { generateJWK } from "unjwt/jwk";
134
-
135
- const myKeys = await generateJWK("ECDH-ES+A256KW", { kid: "alice-2025" });
136
- // → { privateKey: JWK_EC_Private, publicKey: JWK_EC_Public }
137
-
138
- // Keep myKeys.privateKey in secure storage — never expose it
139
- // Share myKeys.publicKey freely (e.g., publish at /.well-known/jwks.json)
140
- ```
141
-
142
- #### Sending to one recipient
143
-
144
- For two-party communication, `encrypt` and `decrypt` handle everything. Pass the recipient's public key — a one-time ephemeral key pair is generated internally per message:
145
-
146
- ```ts
147
- import { encrypt, decrypt } from "unjwt/jwe";
148
-
149
- // Sender (Alice) ————————————————————————————————————
150
- // Fetch Bob's public key (from his public endpoint, shared config, etc.)
151
- const bobPublicKey = /* ... */;
152
-
153
- const token = await encrypt({ message: "Hello Bob!" }, bobPublicKey);
154
- // Send `token` to Bob over any channel — only Bob can decrypt it
155
-
156
- // Recipient (Bob) ————————————————————————————————————
157
- const { payload } = await decrypt(token, bobPrivateKey);
158
- console.log(payload.message); // "Hello Bob!"
159
- ```
160
-
161
- Each token carries a fresh ephemeral public key (`epk`) in its header. Bob uses that `epk` together with his private key to re-derive the same shared secret Alice used, then unwraps the Content Encryption Key. At no point does Alice have Bob's private key, and Bob cannot forge a message appearing to come from Alice.
162
-
163
- #### Sending to multiple recipients
164
-
165
- **Simple — one token per recipient.** Encrypt the payload independently for each party. Each token is decryptable only by its intended recipient:
166
-
167
- ```ts
168
- const recipients = [
169
- { name: "bob", publicKey: bobPublicKey },
170
- { name: "charlie", publicKey: charliePublicKey },
171
- ];
172
-
173
- const tokens = await Promise.all(
174
- recipients.map(({ publicKey }) => encrypt({ message: "Hello everyone!" }, publicKey)),
175
- );
176
- // Deliver tokens[0] to Bob, tokens[1] to Charlie
177
- ```
178
-
179
- This is the right choice for most use cases. The only trade-off is proportional size: N recipients produce N tokens.
180
-
181
- **Shared ciphertext — one payload, N wrapped keys.** When the payload is large or bandwidth matters, encrypt the payload once with a random Content Encryption Key (`dir`), then wrap that key separately per recipient. Everyone receives the same ciphertext, each with their own individually wrapped key:
182
-
183
- ```ts
184
- import { wrapKey, unwrapKey } from "unjwt/jwk";
185
- import { randomBytes } from "unjwt/utils";
45
+ // Encrypt and decrypt (JWE)
186
46
  import { encrypt, decrypt } from "unjwt/jwe";
187
47
 
188
- const enc = "A256GCM";
189
-
190
- // 1. Generate a random CEK once for this message (32 bytes = 256-bit for A256GCM)
191
- const cek = randomBytes(32);
192
-
193
- // 2. Encrypt the payload ONCE using the CEK directly (alg: "dir")
194
- const ciphertext = await encrypt({ message: "Hello everyone!" }, cek, { alg: "dir", enc });
195
-
196
- // 3. Wrap the CEK individually for each recipient
197
- const wrappedKeys = await Promise.all(
198
- recipients.map(async ({ name, publicKey }) => {
199
- const { encryptedKey, epk } = await wrapKey("ECDH-ES+A256KW", cek, publicKey);
200
- return { name, encryptedKey, epk };
201
- }),
202
- );
203
- // Deliver `ciphertext` (same for all) + each recipient's own { encryptedKey, epk }
204
-
205
- // Recipient side ——————————————————————————————————————————————
206
- const mine = wrappedKeys.find((w) => w.name === myName)!;
207
- const rawCek = await unwrapKey("ECDH-ES+A256KW", mine.encryptedKey, myPrivateKey, {
208
- format: "raw",
209
- epk: mine.epk,
210
- enc,
211
- });
212
- const { payload } = await decrypt(ciphertext, rawCek);
213
- console.log(payload.message); // "Hello everyone!"
214
- ```
215
-
216
- > This pattern is the manual foundation of [JWE JSON Serialization](https://www.rfc-editor.org/rfc/rfc7516#section-3.3) (RFC 7516 §3.3), which packages the ciphertext and all wrapped keys into a single `recipients[]` JSON structure. Full JWE JSON Serialization support is planned for a future release; `wrapKey` and `unwrapKey` provide the building blocks for those who need to construct or consume the JSON form today.
217
-
218
- #### `deriveSharedSecret` — raw key material
219
-
220
- `wrapKey` and `unwrapKey` already handle the full ECDH Concat KDF + AES-KW cycle internally. `deriveSharedSecret` exposes the KDF step alone, returning the raw derived bytes before any key-wrapping step:
221
-
222
- ```ts
223
- import { deriveSharedSecret } from "unjwt/jwk";
224
-
225
- // Both sides independently derive the exact same bytes
226
- const aliceView = await deriveSharedSecret(
227
- bobPublicKey,
228
- aliceEphemeralPrivateKey,
229
- "ECDH-ES+A256KW",
230
- );
231
- const bobView = await deriveSharedSecret(aliceEphemeralPublicKey, bobPrivateKey, "ECDH-ES+A256KW");
232
- // aliceView and bobView are identical Uint8Arrays
233
- ```
234
-
235
- Use `deriveSharedSecret` when you need the shared secret itself rather than a wrapped key: custom hybrid protocols that use the derived bytes directly, non-standard wrapping schemes, or verifying the key agreement step in isolation.
236
-
237
- ### JWK — Key Management
238
-
239
- > Full reference: [skills/unjwt/references/jwk.md](./skills/unjwt/references/jwk.md)
240
-
241
- ```ts
242
- import {
243
- generateKey,
244
- generateJWK,
245
- importKey,
246
- exportKey,
247
- importFromPEM,
248
- exportToPEM,
249
- wrapKey,
250
- unwrapKey,
251
- deriveKeyFromPassword,
252
- getJWKsFromSet,
253
- deriveSharedSecret,
254
- configureJWKCache,
255
- } from "unjwt/jwk";
256
- // All of the above are also importable from "unjwt" directly.
257
-
258
- // Generate keys as CryptoKey or JWK
259
- const hmacKey = await generateKey("HS256"); // CryptoKey
260
- const rsaPair = await generateKey("RS256"); // CryptoKeyPair
261
- const ecJwk = await generateJWK("ES256", { kid: "k1" }); // { privateKey: JWK, publicKey: JWK }
262
-
263
- // PEM conversion
264
- const jwk = await importFromPEM(pemString, "spki", "RS256", { jwkParams: { kid: "rsa-1" } });
265
- const pem = await exportToPEM(jwk, "spki");
266
-
267
- // Key wrapping
268
- const cek = crypto.getRandomValues(new Uint8Array(32));
269
- const { encryptedKey } = await wrapKey("A256KW", cek, aesKey);
270
- const unwrapped = await unwrapKey("A256KW", encryptedKey, aesKey, { format: "raw" });
271
-
272
- // ECDH-ES shared secret (e.g. for multi-recipient JWE)
273
- const secret = await deriveSharedSecret(
274
- recipientPublicKey,
275
- senderEphemeralPrivate,
276
- "ECDH-ES+A256KW",
277
- );
278
- ```
279
-
280
- ### Utility Functions
281
-
282
- > Full reference: [skills/unjwt/references/utils.md](./skills/unjwt/references/utils.md)
283
-
284
- ```ts
285
- import {
286
- base64UrlEncode,
287
- base64UrlDecode,
288
- randomBytes,
289
- isJWK,
290
- isJWKSet,
291
- isSymmetricJWK,
292
- isPrivateJWK,
293
- isPublicJWK,
294
- isCryptoKey,
295
- textEncoder,
296
- textDecoder,
297
- } from "unjwt/utils";
298
- // Most of the above are also importable from "unjwt" directly.
299
-
300
- const bytes = randomBytes(32);
301
- const encoded = base64UrlEncode(bytes);
302
- const decoded = base64UrlDecode(encoded, false); // Uint8Array
303
- ```
304
-
305
- ### H3 Session Adapters
306
-
307
- > Full reference: [skills/unjwt/references/adapters-h3.md](./skills/unjwt/references/adapters-h3.md)
308
-
309
- Cookie-based JWT session management for H3 applications. Sessions are stored as encrypted (JWE) or signed (JWS) tokens in chunked cookies.
310
-
311
- > **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.
312
-
313
- #### JWE Session (encrypted, recommended for sensitive data)
314
-
315
- ```ts
316
- import { useJWESession, generateJWK } from "unjwt/adapters/h3v2";
317
-
318
- app.get("/profile", async (event) => {
319
- const session = await useJWESession(event, {
320
- key: process.env.SESSION_SECRET!, // password string, symmetric JWK, or asymmetric keypair
321
- maxAge: "7D",
322
- });
323
-
324
- if (!session.id) {
325
- // No active session — create one
326
- await session.update({ userId: "123", email: "user@example.com" });
327
- }
328
-
329
- return { user: session.data };
330
- });
48
+ const jwe = await encrypt({ secret: "data" }, "my-password");
49
+ const { payload: p } = await decrypt(jwe, "my-password");
331
50
  ```
332
51
 
333
- #### JWS Session (signed, readable by client)
52
+ Full walkthroughs, recipes, and the complete API reference live at **[unjwt.s94.dev](https://unjwt.s94.dev)**:
334
53
 
335
- ```ts
336
- import { useJWSSession, generateJWK } from "unjwt/adapters/h3v2";
337
-
338
- const keys = await generateJWK("RS256"); // persist these!
339
-
340
- app.get("/preferences", async (event) => {
341
- const session = await useJWSSession(event, {
342
- key: keys,
343
- maxAge: "24h",
344
- });
345
-
346
- return { theme: session.data.theme };
347
- });
348
- ```
349
-
350
- #### Session features
351
-
352
- - **Cookie chunking** — large tokens are automatically split across multiple cookies
353
- - **Header-based tokens** — read from `Authorization: Bearer <token>` or custom headers via `sessionHeader`
354
- - **Lifecycle hooks** — `onRead`, `onUpdate`, `onClear`, `onExpire`, `onError` for logging, refresh logic, etc.
355
- - **Key lookup hooks** — `onUnsealKeyLookup` (JWE) / `onVerifyKeyLookup` (JWS) for key rotation
356
- - **TypeScript generics** — strongly-typed session data via `useJWESession<MyData>(event, config)`
357
-
358
- #### Refresh token pattern
359
-
360
- ```ts
361
- import {
362
- type SessionConfigJWE,
363
- type SessionConfigJWS,
364
- useJWESession,
365
- useJWSSession,
366
- getJWESession,
367
- updateJWSSession,
368
- generateJWK,
369
- } from "unjwt/adapters/h3v2";
370
-
371
- const atKeys = await generateJWK("RS256");
372
-
373
- const refreshConfig = {
374
- key: process.env.REFRESH_SECRET!,
375
- name: "refresh_token",
376
- } satisfies SessionConfigJWE;
377
-
378
- const accessConfig = {
379
- key: atKeys,
380
- name: "access_token",
381
- maxAge: "15m",
382
- hooks: {
383
- async onExpire({ event, config }) {
384
- const refresh = await getJWESession(event, refreshConfig);
385
- if (refresh.data.sub) {
386
- await updateJWSSession(event, config, {
387
- sub: refresh.data.sub,
388
- scope: refresh.data.scope,
389
- });
390
- }
391
- },
392
- },
393
- } satisfies SessionConfigJWS;
394
- ```
54
+ - [Getting started →](https://unjwt.s94.dev/getting-started)
55
+ - [JWS — signing and verifying →](https://unjwt.s94.dev/jwt/jws)
56
+ - [JWE — encrypting and decrypting →](https://unjwt.s94.dev/jwt/jwe)
57
+ - [JWK — key management →](https://unjwt.s94.dev/jwk)
58
+ - [H3 session adapters →](https://unjwt.s94.dev/adapters)
59
+ - [Examples →](https://unjwt.s94.dev/examples) — authentication, JWKS endpoints, refresh tokens, end-to-end encryption, signed receipts.
395
60
 
396
61
  ## Development
397
62