unjwt 0.5.39 → 0.6.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.
Files changed (41) hide show
  1. package/README.md +156 -13
  2. package/dist/_chunks/_crypto.mjs +1014 -0
  3. package/dist/_chunks/error.d.mts +67 -0
  4. package/dist/_chunks/index.d.mts +631 -219
  5. package/dist/_chunks/jwk.d.mts +220 -87
  6. package/dist/_chunks/jws.mjs +729 -682
  7. package/dist/_chunks/utils.mjs +74 -69
  8. package/dist/adapters/h3v1/index.d.mts +282 -0
  9. package/dist/{_chunks/h3v1.mjs → adapters/h3v1/index.mjs} +204 -91
  10. package/dist/adapters/h3v2/index.d.mts +213 -0
  11. package/dist/adapters/{h3v2.mjs → h3v2/index.mjs} +215 -113
  12. package/dist/core/_crypto/index.d.mts +71 -0
  13. package/dist/core/_crypto/index.mjs +2 -0
  14. package/dist/core/jwe.d.mts +40 -0
  15. package/dist/{_chunks → core}/jwe.mjs +61 -32
  16. package/dist/core/jwk.d.mts +231 -0
  17. package/dist/core/jwk.mjs +686 -0
  18. package/dist/core/jws.d.mts +31 -0
  19. package/dist/core/jws.mjs +104 -0
  20. package/dist/{_chunks/index2.d.mts → core/utils/index.d.mts} +13 -12
  21. package/dist/core/utils/index.mjs +2 -0
  22. package/dist/index.d.mts +7 -6
  23. package/dist/index.mjs +5 -5
  24. package/package.json +50 -41
  25. package/dist/_chunks/chunk.mjs +0 -13
  26. package/dist/_chunks/jwe.d.mts +0 -43
  27. package/dist/_chunks/jwk.mjs +0 -1592
  28. package/dist/_chunks/jws.d.mts +0 -46
  29. package/dist/adapters/h3.d.mts +0 -3
  30. package/dist/adapters/h3.mjs +0 -3
  31. package/dist/adapters/h3v1.d.mts +0 -3
  32. package/dist/adapters/h3v1.mjs +0 -3
  33. package/dist/adapters/h3v2.d.mts +0 -184
  34. package/dist/jwe.d.mts +0 -3
  35. package/dist/jwe.mjs +0 -4
  36. package/dist/jwk.d.mts +0 -3
  37. package/dist/jwk.mjs +0 -3
  38. package/dist/jws.d.mts +0 -3
  39. package/dist/jws.mjs +0 -4
  40. package/dist/utils.d.mts +0 -3
  41. package/dist/utils.mjs +0 -2
package/README.md CHANGED
@@ -1,17 +1,13 @@
1
1
  # unjwt
2
2
 
3
- <!-- automd:badges bundlephobia style="flat" color="FFDC3B" -->
4
-
5
- [![npm version](https://img.shields.io/npm/v/unjwt?color=FFDC3B)](https://npmjs.com/package/unjwt)
6
- [![npm downloads](https://img.shields.io/npm/dm/unjwt?color=FFDC3B)](https://npm.chart.dev/unjwt)
7
- [![bundle size](https://img.shields.io/bundlephobia/minzip/unjwt?color=FFDC3B)](https://bundlephobia.com/package/unjwt)
8
-
9
- <!-- /automd -->
3
+ [![npm version](https://npmx.dev/api/registry/badge/version/unjwt?name=true)](https://npmx.dev/package/unjwt)
4
+ [![npm downloads](https://npmx.dev/api/registry/badge/downloads/unjwt)](https://npmx.dev/package/unjwt)
5
+ [![bundle size](https://npmx.dev/api/registry/badge/size/unjwt)](https://npmx.dev/package/unjwt)
10
6
 
11
7
  A collection of low-level JWT ([RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519)) utilities using the Web Crypto API. Supports:
12
8
 
13
9
  - **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
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
15
11
  - **JWK** ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) — generate, import, export, wrap, and unwrap keys (CryptoKey, JWK, PEM)
16
12
  - **Framework adapters** for cookie-based JWT sessions:
17
13
  - [H3 v1](./skills/unjwt/references/adapters-h3.md) (Nuxt v4, Nitro v2)
@@ -26,6 +22,12 @@ Zero runtime dependencies for core. Optional peer deps for adapters (`h3`, `cook
26
22
  npx nypm install unjwt
27
23
  ```
28
24
 
25
+ If you would like to install the agent skill you can run:
26
+
27
+ ```sh
28
+ npx skills add sandros94/unjwt
29
+ ```
30
+
29
31
  ## Usage
30
32
 
31
33
  ### JWS — Sign & Verify
@@ -35,6 +37,7 @@ npx nypm install unjwt
35
37
  ```ts
36
38
  import { sign, verify } from "unjwt/jws";
37
39
  import { generateJWK } from "unjwt/jwk";
40
+ // or import both from the root: import { sign, verify, generateJWK } from "unjwt";
38
41
 
39
42
  // Generate a key (HMAC, RSA, ECDSA, EdDSA, etc.)
40
43
  const key = await generateJWK("HS256");
@@ -72,12 +75,27 @@ const { payload } = await verify(
72
75
  );
73
76
  ```
74
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
+
75
92
  ### JWE — Encrypt & Decrypt
76
93
 
77
94
  > Full reference: [skills/unjwt/references/jwe.md](./skills/unjwt/references/jwe.md)
78
95
 
79
96
  ```ts
80
97
  import { encrypt, decrypt } from "unjwt/jwe";
98
+ // or: import { encrypt, decrypt } from "unjwt";
81
99
 
82
100
  // Password-based encryption (simplest — uses PBES2)
83
101
  const token = await encrypt({ secret: "sensitive data" }, "my-password");
@@ -103,6 +121,119 @@ const ecKeys = await generateJWK("ECDH-ES+A256KW");
103
121
  const token3 = await encrypt({ data: "secret" }, ecKeys.publicKey);
104
122
  ```
105
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";
186
+ import { encrypt, decrypt } from "unjwt/jwe";
187
+
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
+
106
237
  ### JWK — Key Management
107
238
 
108
239
  > Full reference: [skills/unjwt/references/jwk.md](./skills/unjwt/references/jwk.md)
@@ -113,12 +244,16 @@ import {
113
244
  generateJWK,
114
245
  importKey,
115
246
  exportKey,
116
- importJWKFromPEM,
117
- exportJWKToPEM,
247
+ importFromPEM,
248
+ exportToPEM,
118
249
  wrapKey,
119
250
  unwrapKey,
120
251
  deriveKeyFromPassword,
252
+ getJWKsFromSet,
253
+ deriveSharedSecret,
254
+ configureJWKCache,
121
255
  } from "unjwt/jwk";
256
+ // All of the above are also importable from "unjwt" directly.
122
257
 
123
258
  // Generate keys as CryptoKey or JWK
124
259
  const hmacKey = await generateKey("HS256"); // CryptoKey
@@ -126,13 +261,20 @@ const rsaPair = await generateKey("RS256"); // CryptoKeyPair
126
261
  const ecJwk = await generateJWK("ES256", { kid: "k1" }); // { privateKey: JWK, publicKey: JWK }
127
262
 
128
263
  // PEM conversion
129
- const jwk = await importJWKFromPEM(pemString, "spki", "RS256", undefined, { kid: "rsa-1" });
130
- const pem = await exportJWKToPEM(jwk, "spki");
264
+ const jwk = await importFromPEM(pemString, "spki", "RS256", { jwkParams: { kid: "rsa-1" } });
265
+ const pem = await exportToPEM(jwk, "spki");
131
266
 
132
267
  // Key wrapping
133
268
  const cek = crypto.getRandomValues(new Uint8Array(32));
134
269
  const { encryptedKey } = await wrapKey("A256KW", cek, aesKey);
135
- const unwrapped = await unwrapKey("A256KW", encryptedKey, aesKey, { returnAs: false });
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
+ );
136
278
  ```
137
279
 
138
280
  ### Utility Functions
@@ -153,6 +295,7 @@ import {
153
295
  textEncoder,
154
296
  textDecoder,
155
297
  } from "unjwt/utils";
298
+ // Most of the above are also importable from "unjwt" directly.
156
299
 
157
300
  const bytes = randomBytes(32);
158
301
  const encoded = base64UrlEncode(bytes);