unjwt 0.8.5 → 0.9.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.
Files changed (44) hide show
  1. package/README.md +34 -21
  2. package/dist/_chunks/algorithms.mjs +44 -20
  3. package/dist/_chunks/digest.d.mts +4 -4
  4. package/dist/_chunks/digest.mjs +23 -15
  5. package/dist/_chunks/error.d.mts +29 -42
  6. package/dist/_chunks/export.mjs +1 -4
  7. package/dist/_chunks/index.d.mts +270 -234
  8. package/dist/_chunks/index2.d.mts +82 -189
  9. package/dist/_chunks/index4.d.mts +223 -60
  10. package/dist/_chunks/index5.d.mts +68 -31
  11. package/dist/_chunks/index6.d.mts +45 -64
  12. package/dist/_chunks/jwe.mjs +53 -48
  13. package/dist/_chunks/jwk.mjs +9 -3
  14. package/dist/_chunks/jws.mjs +55 -50
  15. package/dist/_chunks/jwt.d.mts +13 -49
  16. package/dist/_chunks/jwt.mjs +4 -158
  17. package/dist/_chunks/jwt2.mjs +162 -0
  18. package/dist/_chunks/libs/unsecure.d.mts +0 -6
  19. package/dist/_chunks/limits.d.mts +3 -2
  20. package/dist/_chunks/payload.mjs +13 -7
  21. package/dist/_chunks/wrap.mjs +3 -1
  22. package/dist/core/internal.d.mts +50 -2
  23. package/dist/core/internal.mjs +3 -1
  24. package/dist/core/jwe/index.d.mts +1 -1
  25. package/dist/core/jwk/index.d.mts +2 -2
  26. package/dist/core/jwk/index.mjs +2 -2
  27. package/dist/dpop/index.d.mts +77 -18
  28. package/dist/index.d.mts +6 -6
  29. package/dist/index.mjs +4 -4
  30. package/dist/session/index.d.mts +568 -22
  31. package/dist/session/index.mjs +1386 -34
  32. package/dist/utils/index.d.mts +4 -4
  33. package/dist/utils/index.mjs +2 -10
  34. package/package.json +4 -32
  35. package/dist/_chunks/codec-jwe.d.mts +0 -280
  36. package/dist/_chunks/codec-jws.mjs +0 -1005
  37. package/dist/_chunks/cookie-attributes.d.mts +0 -12
  38. package/dist/_chunks/cookie.mjs +0 -50
  39. package/dist/adapters/elysia/index.d.mts +0 -123
  40. package/dist/adapters/elysia/index.mjs +0 -120
  41. package/dist/adapters/h3/index.d.mts +0 -135
  42. package/dist/adapters/h3/index.mjs +0 -117
  43. package/dist/adapters/h3v1/index.d.mts +0 -148
  44. package/dist/adapters/h3v1/index.mjs +0 -128
package/README.md CHANGED
@@ -8,13 +8,14 @@ A collection of JWT utilities based on the Web Crypto API, in a single zero-depe
8
8
 
9
9
  ## Features
10
10
 
11
- - **JWS** ([RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515)) — HMAC, RSA, RSA-PSS, ECDSA, EdDSA. Compact + General/Flattened JSON Serialization (multi-signature).
12
- - **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).
13
- - **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.
14
- - **DPoP** ([RFC 9449](https://www.rfc-editor.org/rfc/rfc9449)) — create and verify proofs through the optional [`unjwt/dpop`](https://unjwt.s94.dev/dpop) module. Your application owns HTTP and replay storage.
15
- - **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.
16
- - **Runtime-agnostic** — Node 22+, Deno, Bun, Cloudflare Workers, browsers. Anywhere Web Crypto runs.
17
- - **Zero runtime dependencies** for core; optional peer deps for adapters (`h3`, `elysia`).
11
+ - **JWS** ([RFC 7515](https://www.rfc-editor.org/rfc/rfc7515)): HMAC, RSA, RSA-PSS, ECDSA, EdDSA. Compact and General/Flattened JSON serialization (several signatures over one payload).
12
+ - **JWE** ([RFC 7516](https://www.rfc-editor.org/rfc/rfc7516)): `dir`, AES-KW, AES-GCM-KW, RSA-OAEP, PBES2, ECDH-ES, with AES-GCM and AES-CBC-HMAC-SHA2 content encryption. Compact and General/Flattened JSON serialization (several recipients for one ciphertext).
13
+ - **JWK** ([RFC 7517](https://www.rfc-editor.org/rfc/rfc7517)): generate, import, export, wrap and unwrap keys; `CryptoKey`, JWK and PEM conversion; thumbprints as key IDs; JWK Set lookup and rotation.
14
+ - **DPoP** ([RFC 9449](https://www.rfc-editor.org/rfc/rfc9449)): create and verify proofs with [`unjwt/dpop`](https://unjwt.s94.dev/dpop). Your application owns HTTP and replay storage.
15
+ - **Sessions**: signed or encrypted cookie sessions with [`unjwt/session`](https://unjwt.s94.dev/sessions) over a Web `Request`, on [h3](https://unjwt.s94.dev/examples/h3), [Nitro](https://unjwt.s94.dev/examples/nitro), [Nuxt](https://unjwt.s94.dev/examples/nuxt), [Hono and Workers](https://unjwt.s94.dev/examples/hono), Bun and Deno, or on a token in hand; a [driver](https://unjwt.s94.dev/sessions/drivers) for any other framework.
16
+ - **One typed error**: every failure is a `JWTError` with a `code` to branch on.
17
+ - **Runtime-agnostic**: Node.js 22+, Bun, Deno, Cloudflare Workers and browsers, anywhere Web Crypto runs.
18
+ - **Zero runtime dependencies**, no peer dependencies.
18
19
 
19
20
  ## Install
20
21
 
@@ -32,30 +33,42 @@ npx skills add sandros94/unjwt
32
33
  ## Quickstart
33
34
 
34
35
  ```ts
35
- // Sign and verify a JWT (JWS)
36
+ // Sign with the private half, verify with the public one (JWS)
36
37
  import { sign, verify } from "unjwt/jws";
37
38
  import { generateJWK } from "unjwt/jwk";
38
39
 
39
- const key = await generateJWK("HS256");
40
- const token = await sign({ sub: "user_1" }, key, { expiresIn: "1h" });
41
- const { payload } = await verify(token, key);
42
- // { sub: "user_1", iat: ..., exp: ... }
40
+ const { privateKey, publicKey } = await generateJWK("ES256");
41
+ const token = await sign({ sub: "user_1a2b" }, privateKey, { expiresIn: "15m" });
42
+ const { payload } = await verify(token, publicKey);
43
+ // { sub: "user_1a2b", iat: …, exp: … }
43
44
 
44
- // Encrypt and decrypt (JWE)
45
+ // Encrypt for a recipient's public key, decrypt with its private key (JWE)
45
46
  import { encrypt, decrypt } from "unjwt/jwe";
46
47
 
47
- const jwe = await encrypt({ secret: "data" }, "my-password");
48
- const { payload: p } = await decrypt(jwe, "my-password");
48
+ const recipient = await generateJWK("ECDH-ES+A256KW");
49
+ const jwe = await encrypt({ invoice: "inv_2026_0042" }, recipient.publicKey);
50
+ const opened = await decrypt(jwe, recipient.privateKey);
51
+
52
+ // Keep a session in a cookie on any Web Request
53
+ import { defineJWESession } from "unjwt/session";
54
+
55
+ const appSession = defineJWESession({ key: await generateJWK("A256GCM"), maxAge: "7D" });
56
+ const headers = new Headers();
57
+ const session = await appSession.use({ req: request, res: { headers } });
58
+ await session.update({ user: "user_1a2b" }); // Set-Cookie lands on `headers`
49
59
  ```
50
60
 
51
- Full walkthroughs, recipes, and the complete API reference live at **[unjwt.s94.dev](https://unjwt.s94.dev)**:
61
+ Walkthroughs, recipes and the full API live at **[unjwt.s94.dev](https://unjwt.s94.dev)**:
52
62
 
53
63
  - [Getting started →](https://unjwt.s94.dev/getting-started)
54
- - [JWS — signing and verifying →](https://unjwt.s94.dev/jwt/jws)
55
- - [JWE — encrypting and decrypting →](https://unjwt.s94.dev/jwt/jwe)
56
- - [JWK — key management →](https://unjwt.s94.dev/jwk)
57
- - [Sessions on your framework →](https://unjwt.s94.dev/frameworks)
58
- - [Examples →](https://unjwt.s94.dev/examples) — authentication, JWKS endpoints, refresh tokens, end-to-end encryption, signed receipts.
64
+ - [Sign and verify (JWS) →](https://unjwt.s94.dev/jws)
65
+ - [Encrypt and decrypt (JWE) →](https://unjwt.s94.dev/jwe)
66
+ - [Keys (JWK) →](https://unjwt.s94.dev/jwk)
67
+ - [Sessions →](https://unjwt.s94.dev/sessions)
68
+ - [Safety: errors, claims and limits →](https://unjwt.s94.dev/safety)
69
+ - [DPoP proofs →](https://unjwt.s94.dev/dpop)
70
+ - [Recipes →](https://unjwt.s94.dev/examples): sessions on h3, Nitro, Nuxt, Hono and Workers, CDN imports, custom drivers.
71
+ - [Migration guides →](https://unjwt.s94.dev/getting-started/migration)
59
72
 
60
73
  ## Development
61
74
 
@@ -1,7 +1,8 @@
1
1
  import { safeJsonParse, sanitizeObjectCopy, textEncoder } from "./libs/unsecure.mjs";
2
2
  import { JWTError } from "../error.mjs";
3
- import { assertClaimValidationOptions, assertCritShape, assertCryptoKey, assertPlainObject, assertStringList, base64Parse, base64Stringify, base64UrlParse, base64UrlStringify, concatUint8Arrays, hasSymmetricSecret, isCryptoKey, isCryptoKeyPair, isJWK, isJWKSet, isPlainObject, isPublicJWK, quoteTokenValue, uint32be } from "./jwt.mjs";
4
- import { ECDSA_CURVES, aesBitsOf, checkKeyUsage, cryptoKeyCurve, digestUrl, hashBitsOf, inferJWSAllowedAlgorithms, innerKeyWrapAlg, isAlgorithm, isContentEncryptionAlgorithm, isECDHESAlgorithm, isPBES2Algorithm, isSameAlgorithm, jwkAlgServes, jwsHashOf, lengthOf, modulusLengthOf, namedCurveOf, pbes2HashOf, rsaOaepHashOf, shaBitsOf, unusableKey } from "./digest.mjs";
3
+ import { assertClaimValidationOptions, assertCryptoKey, assertPlainObject, assertStringList, base64Parse, base64Stringify, base64UrlParse, base64UrlStringify, concatUint8Arrays, hasMisplacedSecret, isCryptoKey, isCryptoKeyPair, isJWK, isJWKSet, isPlainObject, isPublicJWK, uint32be } from "./jwt.mjs";
4
+ import { assertCritShape, quoteTokenValue } from "./jwt2.mjs";
5
+ import { ECDSA_CURVES, aesBitsOf, checkKeyUsage, cryptoKeyCurve, digestUrl, hashBitsOf, inferJWSAllowedAlgorithms, innerKeyWrapAlg, isAlgorithm, isContentEncryptionAlgorithm, isECDHESAlgorithm, isPBES2Algorithm, isSameAlgorithm, jwkAlgServes, jwkKtyServes, jwsHashOf, lengthOf, modulusLengthOf, namedCurveOf, pbes2HashOf, rsaOaepHashOf, shaBitsOf, unusableKey } from "./digest.mjs";
5
6
  function serializeProtectedHeader(header) {
6
7
  try {
7
8
  return JSON.stringify(header);
@@ -1242,6 +1243,7 @@ const DEFAULT_JOSE_LIMITS = /* @__PURE__ */ Object.freeze({
1242
1243
  signatures: 16,
1243
1244
  recipients: 16,
1244
1245
  keyCandidates: 8,
1246
+ keyAttempts: 16,
1245
1247
  pbes2Iterations: 4 * DEFAULT_PBES2_ITERATIONS
1246
1248
  });
1247
1249
  function resolveLimit(limits, name) {
@@ -1258,6 +1260,23 @@ function assertCandidateLimit(count, limit) {
1258
1260
  if (count <= limit) return;
1259
1261
  throw new JWTError(`${count} candidate keys exceed the limit of ${limit}, narrow them with a "kid".`, "ERR_JWK_KEY_NOT_FOUND");
1260
1262
  }
1263
+ function createKeyAttemptBudget(limit) {
1264
+ let remaining = limit;
1265
+ let exhausted;
1266
+ return {
1267
+ get exhausted() {
1268
+ return exhausted;
1269
+ },
1270
+ spend() {
1271
+ if (remaining > 0) {
1272
+ remaining--;
1273
+ return;
1274
+ }
1275
+ exhausted ??= new JWTError(`Key attempt budget of ${limit} ("limits.keyAttempts") exhausted, narrow the keys with a "kid".`, "ERR_JWK_KEY_NOT_FOUND");
1276
+ throw exhausted;
1277
+ }
1278
+ };
1279
+ }
1261
1280
  function assertJWKSetCandidates(candidates, options) {
1262
1281
  if (candidates.length === 0) {
1263
1282
  const { kid } = options;
@@ -1304,12 +1323,11 @@ const JWK_SET_KEY_OPS = /* @__PURE__ */ Object.freeze({
1304
1323
  function buildJWKSetFilter(header, options) {
1305
1324
  const kid = header?.kid;
1306
1325
  const alg = header?.alg;
1307
- const { op, allowSymmetric } = options;
1326
+ const { op } = options;
1308
1327
  const use = op;
1309
1328
  const keyOps = JWK_SET_KEY_OPS[op];
1310
1329
  return (k) => {
1311
- if (!isJWK(k)) return false;
1312
- if (!allowSymmetric && hasSymmetricSecret(k)) return false;
1330
+ if (!isJWK(k) || hasMisplacedSecret(k)) return false;
1313
1331
  if (kid && k.kid !== kid) return false;
1314
1332
  if (k.alg && !(alg && jwkAlgServes({
1315
1333
  kty: k.kty,
@@ -1381,7 +1399,10 @@ async function resolveKeyInput(key, options) {
1381
1399
  const { header, token, what } = options;
1382
1400
  let resolved;
1383
1401
  try {
1384
- resolved = await key(header, token);
1402
+ resolved = await key({
1403
+ header,
1404
+ token
1405
+ });
1385
1406
  } catch (error) {
1386
1407
  throw asKeyLookupError(error, what);
1387
1408
  }
@@ -1422,7 +1443,7 @@ async function importKey(key, algOrOptions) {
1422
1443
  return key;
1423
1444
  }
1424
1445
  if (isJWK(key)) {
1425
- if (key.kty !== "oct" && Object.hasOwn(key, "k")) throw new JWTError(`JWK "k" is defined only for "oct" keys, not "${key.kty}".`, "ERR_JWK_INVALID");
1446
+ if (hasMisplacedSecret(key)) throw new JWTError(`JWK "k" is defined only for "oct" keys, not "${key.kty}".`, "ERR_JWK_INVALID");
1426
1447
  assertJWKAlgAuthority(key, alg);
1427
1448
  if (key.kty === "oct" && typeof key.k === "string") {
1428
1449
  const rawBytes = decodeSegment(key.k, {
@@ -1462,11 +1483,16 @@ async function importKey(key, algOrOptions) {
1462
1483
  throw new JWTError("Invalid key type provided to importKey", "ERR_JWK_INVALID");
1463
1484
  }
1464
1485
  function assertJWKAlgAuthority(jwk, alg) {
1465
- if (!alg || !jwk.alg || jwkAlgServes({
1466
- kty: jwk.kty,
1467
- alg: jwk.alg
1468
- }, alg)) return;
1469
- throw new JWTError(`JWK "alg" is "${jwk.alg}" but the operation requires "${alg}".`, "ERR_JWK_INVALID");
1486
+ if (!alg) return;
1487
+ if (jwk.alg) {
1488
+ if (jwkAlgServes({
1489
+ kty: jwk.kty,
1490
+ alg: jwk.alg
1491
+ }, alg)) return;
1492
+ throw new JWTError(`JWK "alg" is "${jwk.alg}" but the operation requires "${alg}".`, "ERR_JWK_INVALID");
1493
+ }
1494
+ if (jwkKtyServes(jwk.kty, alg)) return;
1495
+ throw new JWTError(`JWK "kty" is "${jwk.kty}", which cannot serve "${alg}".`, "ERR_JWK_INVALID");
1470
1496
  }
1471
1497
  function assertIntentOnJWK(jwk, expect) {
1472
1498
  if (expect === void 0 || jwk.kty === "oct") return;
@@ -1492,10 +1518,11 @@ const JWS_ALG_CTX = /* @__PURE__ */ Object.freeze({
1492
1518
  op: "sig"
1493
1519
  });
1494
1520
  function checkAlgAllowed(alg, key, options) {
1495
- const { ctx, algorithms, allowSymmetric } = options;
1521
+ const { ctx, algorithms } = options;
1496
1522
  if (algorithms !== void 0) return algorithms.includes(alg) ? void 0 : notAllowed(alg, ctx);
1497
1523
  if (isJWK(key) && typeof key.alg === "string" && (!ctx.octRoles || key.kty !== "oct")) return isSameAlgorithm(key.alg, alg) ? void 0 : notAllowed(alg, ctx);
1498
- const inferred = ctx.infer(key, { allowSymmetric });
1524
+ if (key === void 0 || key === null) return new JWTError("A key is required.", "ERR_JWK_INVALID");
1525
+ const inferred = ctx.infer(key);
1499
1526
  if (!inferred) return new JWTError("Cannot infer allowed algorithms from this key, pass \"algorithms\".", ctx.errorCode);
1500
1527
  return inferred.includes(alg) ? void 0 : notAllowed(alg, ctx);
1501
1528
  }
@@ -1503,12 +1530,9 @@ function notAllowed(alg, ctx) {
1503
1530
  return new JWTError(`${ctx.label} not allowed: ${alg}`, ctx.errorCode);
1504
1531
  }
1505
1532
  function keyMatchesHeader(header, keyMaterial, options) {
1506
- const { ctx, allowSymmetric } = options;
1533
+ const { ctx } = options;
1507
1534
  if (isJWKSet(keyMaterial)) {
1508
- const candidates = keyMaterial.keys.filter(buildJWKSetFilter(header, {
1509
- op: ctx.op,
1510
- allowSymmetric
1511
- }));
1535
+ const candidates = keyMaterial.keys.filter(buildJWKSetFilter(header, { op: ctx.op }));
1512
1536
  return candidates.length === 0 || candidates.some((k) => keyServesHeaderAlg(header, k, ctx));
1513
1537
  }
1514
1538
  if (isJWK(keyMaterial)) return keyServesHeaderAlg(header, keyMaterial, ctx);
@@ -1538,4 +1562,4 @@ function keyServesHeaderAlg(header, key, ctx) {
1538
1562
  const keyCurve = isCryptoKey(key) ? cryptoKeyCurve(key) : "crv" in key ? key.crv : void 0;
1539
1563
  return !epkCurve || !keyCurve || epkCurve === keyCurve;
1540
1564
  }
1541
- export { DEFAULT_JOSE_LIMITS, DEFAULT_PBES2_ITERATIONS, JWS_ALG_CTX, MAX_PARTY_INFO_BYTES, WeakMapJWKCache, aesKwUnwrap, aesKwWrap, asDecryptionError, asJWTError, asKeyLookupError, assertCompactToken, assertConsumerOptions, assertDirectKeyRole, assertECDHKeyAllowed, assertEntryLimit, assertJWEEncSupported, assertJWKSetCandidates, assertOctKeyLength, assertOptionEntry, assertPBES2Count, assertPBES2Iterations, assertPBES2Params, assertPBES2Salt, assertProducerConfirmation, assertProducerCrit, assertProducerKeyHeaders, assertProtectedOnlyOption, assertSerializationObject, bitLengthCEK, bitLengthIV, bitLengthTag, buildECDHAgreementParams, buildJWKSetFilter, checkAlgAllowed, checkAlgSupported, checkJWEEncSupported, checkProtectedTyp, checkSerializationEntry, clearJWKCache, configureJWKCache, copyJsonObject, decodeProtectedHeader, decodeSegment, decrypt, decryptRSAES, deriveECDHESKey, deriveKey, ecdhDerivedBits, ecdhesWrap, encrypt, encryptKey, encryptRSAES, fromPKCS8, fromSPKI, fromX509, gcmkwDecrypt, gcmkwEncrypt, generateCEK, generateIV, getJWKsFromSet, importKey, jwkTokey, keyMatchesHeader, keyToJWK, normalizeEpk, pbes2Unwrap, pbes2Wrap, resolveKeyInput, resolveLimit, resolvePasswordKey, serializeProtectedHeader, sign, stringKeyError, thumbprint, toPKCS8, toSPKI, verify };
1565
+ export { DEFAULT_JOSE_LIMITS, DEFAULT_PBES2_ITERATIONS, JWS_ALG_CTX, MAX_PARTY_INFO_BYTES, WeakMapJWKCache, aesKwUnwrap, aesKwWrap, asDecryptionError, asJWTError, asKeyLookupError, assertCompactToken, assertConsumerOptions, assertDirectKeyRole, assertECDHKeyAllowed, assertEntryLimit, assertJWEEncSupported, assertJWKSetCandidates, assertOctKeyLength, assertOptionEntry, assertPBES2Count, assertPBES2Iterations, assertPBES2Params, assertPBES2Salt, assertProducerConfirmation, assertProducerCrit, assertProducerKeyHeaders, assertProtectedOnlyOption, assertSerializationObject, bitLengthCEK, bitLengthIV, bitLengthTag, buildECDHAgreementParams, buildJWKSetFilter, checkAlgAllowed, checkAlgSupported, checkJWEEncSupported, checkProtectedTyp, checkSerializationEntry, clearJWKCache, configureJWKCache, copyJsonObject, createKeyAttemptBudget, decodeProtectedHeader, decodeSegment, decrypt, decryptRSAES, deriveECDHESKey, deriveKey, ecdhDerivedBits, ecdhesWrap, encrypt, encryptKey, encryptRSAES, fromPKCS8, fromSPKI, fromX509, gcmkwDecrypt, gcmkwEncrypt, generateCEK, generateIV, getJWKsFromSet, importKey, jwkTokey, keyMatchesHeader, keyToJWK, normalizeEpk, pbes2Unwrap, pbes2Wrap, resolveKeyInput, resolveLimit, resolvePasswordKey, serializeProtectedHeader, sign, stringKeyError, thumbprint, toPKCS8, toSPKI, verify };
@@ -1,8 +1,8 @@
1
- /** Hash functions the JOSE specs name for digests carried in tokens. */
1
+ /** The hash functions the JOSE specs name for digests in token members. */
2
2
  export type DigestHash = "SHA-256" | "SHA-384" | "SHA-512";
3
3
  /**
4
- * The base64url digest of text or bytes, SHA-256 unless `hash` says otherwise: a DPoP `ath`
5
- * (RFC 9449 §4.2) over the access token, an `x5t#S256` over a certificate, the last step of a
6
- * JWK thumbprint. Text is digested as UTF-8.
4
+ * Returns the unpadded base64url digest of `data` under `hash`, `SHA-256` when omitted.
5
+ * A string is digested as its UTF-8 bytes.
6
+ * Examples: a DPoP `ath` (RFC 9449 §4.2), an `x5t#S256`, a JWK thumbprint.
7
7
  */
8
8
  export declare function digestUrl(data: string | Uint8Array<ArrayBuffer>, hash?: DigestHash): Promise<string>;
@@ -1,6 +1,6 @@
1
1
  import { textEncoder } from "./libs/unsecure.mjs";
2
2
  import { JWTError } from "../error.mjs";
3
- import { base64UrlStringify, hasSymmetricSecret, isCryptoKey, isCryptoKeyPair, isJWK, isJWKSet } from "./jwt.mjs";
3
+ import { base64UrlStringify, hasMisplacedSecret, isCryptoKey, isCryptoKeyPair, isJWK, isJWKSet } from "./jwt.mjs";
4
4
  const ECDSA_CURVES = /* @__PURE__ */ Object.freeze({
5
5
  ES256: "P-256",
6
6
  ES384: "P-384",
@@ -127,30 +127,29 @@ const ECDH_ES_ALGORITHM_SET = /* @__PURE__ */ new Set(ECDH_ES_ALGORITHMS);
127
127
  function isECDHESAlgorithm(alg) {
128
128
  return ECDH_ES_ALGORITHM_SET.has(alg);
129
129
  }
130
- function inferJWSAllowedAlgorithms(key, options) {
130
+ function inferJWSAllowedAlgorithms(key) {
131
131
  if (!key || typeof key === "function") return void 0;
132
132
  if (typeof key === "string" || key instanceof Uint8Array) return void 0;
133
133
  if (typeof key !== "object") return void 0;
134
134
  if (isCryptoKeyPair(key)) return inferJWSAllowedAlgorithms(key.publicKey);
135
- if (isJWKSet(key)) return unionFromJWKSet(key.keys, jwsAlgsFromJWK, options);
135
+ if (isJWKSet(key)) return unionFromJWKSet(key.keys, jwsAlgsFromJWK);
136
136
  if (isCryptoKey(key)) return jwsAlgsFromCryptoKey(key);
137
137
  if (isJWK(key)) return jwsAlgsFromJWK(key);
138
138
  }
139
- function inferJWEAllowedAlgorithms(key, options) {
139
+ function inferJWEAllowedAlgorithms(key) {
140
140
  if (!key || typeof key === "function") return void 0;
141
141
  if (typeof key === "string") return [...PBES2_ALGORITHMS];
142
142
  if (key instanceof Uint8Array) return void 0;
143
143
  if (typeof key !== "object") return void 0;
144
144
  if (isCryptoKeyPair(key)) return inferJWEAllowedAlgorithms(key.privateKey);
145
- if (isJWKSet(key)) return unionFromJWKSet(key.keys, jweAlgsFromJWK, options);
145
+ if (isJWKSet(key)) return unionFromJWKSet(key.keys, jweAlgsFromJWK);
146
146
  if (isCryptoKey(key)) return jweAlgsFromCryptoKey(key);
147
147
  if (isJWK(key)) return jweAlgsFromJWK(key);
148
148
  }
149
- function unionFromJWKSet(keys, mapper, options) {
149
+ function unionFromJWKSet(keys, mapper) {
150
150
  const algs = /* @__PURE__ */ new Set();
151
151
  for (const k of keys) {
152
- if (!isJWK(k)) continue;
153
- if (!options?.allowSymmetric && hasSymmetricSecret(k)) continue;
152
+ if (!isJWK(k) || hasMisplacedSecret(k)) continue;
154
153
  const mapped = mapper(k);
155
154
  if (!mapped) continue;
156
155
  for (const a of mapped) algs.add(a);
@@ -163,6 +162,15 @@ function isEd25519Signature(alg) {
163
162
  function isSameAlgorithm(a, b) {
164
163
  return a === b || isEd25519Signature(a) && isEd25519Signature(b);
165
164
  }
165
+ function jwkKtyServes(kty, alg) {
166
+ switch (kty) {
167
+ case "oct": return !/^(RS|PS|ES|Ed|EC)/.test(alg);
168
+ case "RSA": return /^(RS|PS)/.test(alg);
169
+ case "EC": return /^E(S|C)/.test(alg);
170
+ case "OKP": return /^E(d|C)/.test(alg);
171
+ default: return false;
172
+ }
173
+ }
166
174
  function jwkAlgServes(jwk, alg) {
167
175
  return isSameAlgorithm(jwk.alg, alg) || jwk.kty === "oct" && alg === "dir" && isContentEncryptionAlgorithm(jwk.alg);
168
176
  }
@@ -231,14 +239,14 @@ function jweAlgsFromCryptoKey(key) {
231
239
  const length = lengthOf(key.algorithm);
232
240
  switch (name) {
233
241
  case "AES-KW":
234
- if (length === 128) return ["A128KW", "dir"];
235
- if (length === 192) return ["A192KW", "dir"];
236
- if (length === 256) return ["A256KW", "dir"];
242
+ if (length === 128) return ["A128KW"];
243
+ if (length === 192) return ["A192KW"];
244
+ if (length === 256) return ["A256KW"];
237
245
  return;
238
246
  case "AES-GCM":
239
- if (length === 128) return ["A128GCMKW", "dir"];
240
- if (length === 192) return ["A192GCMKW", "dir"];
241
- if (length === 256) return ["A256GCMKW", "dir"];
247
+ if (length === 128) return ["A128GCMKW"];
248
+ if (length === 192) return ["A192GCMKW"];
249
+ if (length === 256) return ["A256GCMKW"];
242
250
  return;
243
251
  case "RSA-OAEP":
244
252
  if (hashName === "SHA-1") return ["RSA-OAEP"];
@@ -260,4 +268,4 @@ async function digestUrl(data, hash = "SHA-256") {
260
268
  const bytes = typeof data === "string" ? textEncoder.encode(data) : data;
261
269
  return base64UrlStringify(new Uint8Array(await crypto.subtle.digest(hash, bytes)));
262
270
  }
263
- export { ECDSA_CURVES, PBES2_ALGORITHMS, aesBitsOf, checkKeyUsage, cryptoKeyCurve, digestUrl, hashBitsOf, inferJWEAllowedAlgorithms, inferJWSAllowedAlgorithms, innerKeyWrapAlg, isAlgorithm, isContentEncryptionAlgorithm, isECDHESAlgorithm, isJWSAlgorithm, isKeyManagementAlgorithm, isPBES2Algorithm, isSameAlgorithm, jwkAlgServes, jwsHashOf, lengthOf, modulusLengthOf, namedCurveOf, pbes2HashOf, rsaOaepHashOf, shaBitsOf, unusableKey };
271
+ export { ECDSA_CURVES, PBES2_ALGORITHMS, aesBitsOf, checkKeyUsage, cryptoKeyCurve, digestUrl, hashBitsOf, inferJWEAllowedAlgorithms, inferJWSAllowedAlgorithms, innerKeyWrapAlg, isAlgorithm, isContentEncryptionAlgorithm, isECDHESAlgorithm, isJWSAlgorithm, isKeyManagementAlgorithm, isPBES2Algorithm, isSameAlgorithm, jwkAlgServes, jwkKtyServes, jwsAlgsFromJWK, jwsHashOf, lengthOf, modulusLengthOf, namedCurveOf, pbes2HashOf, rsaOaepHashOf, shaBitsOf, unusableKey };
@@ -1,34 +1,30 @@
1
1
  import { UnsecureError, UnsecureErrorCode } from "./libs/unsecure.mjs";
2
2
  /**
3
- * The closed union of error codes unjwt itself throws.
3
+ * Every error code unjwt throws, as a closed union.
4
4
  *
5
- * Code naming follows the JOSE spec hierarchy:
6
- * ERR_JWT_* — RFC 7519 JSON Web Token claim validation
7
- * ERR_JWS_* — RFC 7515 JSON Web Signature
8
- * ERR_JWE_* — RFC 7516 JSON Web Encryption
9
- * ERR_JWK_* — RFC 7517 JSON Web Key
10
- * ERR_DPOP_* — RFC 9449 proof validation
11
- * ERR_SESSION_* — the framework session adapters
5
+ * Prefixes follow the specs:
6
+ * ERR_JWT_*: RFC 7519 claim validation
7
+ * ERR_JWS_*: RFC 7515 signatures
8
+ * ERR_JWE_*: RFC 7516 encryption
9
+ * ERR_JWK_*: RFC 7517 keys
10
+ * ERR_DPOP_*: RFC 9449 proofs
11
+ * ERR_SESSION_*: sessions
12
12
  *
13
- * Use this type where a typo must not compile — {@link isJWTError} takes it,
14
- * so `isJWTError(err, "ERR_JWS_INVALD")` is a type error rather than a check
15
- * that silently never matches. Use {@link JWTErrorCode} for values that may
16
- * legitimately carry a code this version does not know about.
13
+ * {@link isJWTError} takes this type, so a misspelled code such as `"ERR_JWS_INVALD"` is a type error.
14
+ * Use {@link JWTErrorCode} for a value that may hold a code outside this union.
17
15
  */
18
- export type KnownJWTErrorCode = "ERR_JWT_EXPIRED" | "ERR_JWT_NBF" | "ERR_JWT_CLAIM_INVALID" | "ERR_JWT_CLAIM_MISSING" | "ERR_JWS_INVALID" | "ERR_JWS_INVALID_SERIALIZATION" | "ERR_JWS_SIGNATURE_INVALID" | "ERR_JWS_ALG_NOT_ALLOWED" | "ERR_JWS_ALG_MISSING" | "ERR_JWS_SIGNER_ALG_INFERENCE" | "ERR_JWS_NO_MATCHING_SIGNER" | "ERR_JWS_HEADER_PARAMS_NOT_DISJOINT" | "ERR_JWS_B64_INCONSISTENT" | "ERR_JWE_INVALID" | "ERR_JWE_INVALID_SERIALIZATION" | "ERR_JWE_DECRYPTION_FAILED" | "ERR_JWE_ALG_NOT_ALLOWED" | "ERR_JWE_ALG_MISSING" | "ERR_JWE_ALG_FORBIDDEN_IN_MULTI" | "ERR_JWE_ENC_MISSING" | "ERR_JWE_RECIPIENT_ALG_INFERENCE" | "ERR_JWE_NO_MATCHING_RECIPIENT" | "ERR_JWE_HEADER_PARAMS_NOT_DISJOINT" | "ERR_JWK_INVALID" | "ERR_JWK_KEY_NOT_FOUND" | "ERR_JWK_UNSUPPORTED" | "ERR_DPOP_INVALID" | "ERR_DPOP_CLAIM_INVALID" | "ERR_DPOP_NONCE_INVALID" | "ERR_DPOP_REPLAY" | "ERR_SESSION_TOKEN_INVALID" | "ERR_SESSION_READONLY_EVENT" | "ERR_SESSION_KEY_INVALID" | "ERR_SESSION_CONFIG_INVALID" | "ERR_INVALID_OPTION";
16
+ export type KnownJWTErrorCode = "ERR_JWT_EXPIRED" | "ERR_JWT_NBF" | "ERR_JWT_CLAIM_INVALID" | "ERR_JWT_CLAIM_MISSING" | "ERR_JWS_INVALID" | "ERR_JWS_INVALID_SERIALIZATION" | "ERR_JWS_SIGNATURE_INVALID" | "ERR_JWS_ALG_NOT_ALLOWED" | "ERR_JWS_ALG_MISSING" | "ERR_JWS_SIGNER_ALG_INFERENCE" | "ERR_JWS_NO_MATCHING_SIGNER" | "ERR_JWS_HEADER_PARAMS_NOT_DISJOINT" | "ERR_JWS_B64_INCONSISTENT" | "ERR_JWE_INVALID" | "ERR_JWE_INVALID_SERIALIZATION" | "ERR_JWE_DECRYPTION_FAILED" | "ERR_JWE_ALG_NOT_ALLOWED" | "ERR_JWE_ALG_MISSING" | "ERR_JWE_ALG_FORBIDDEN_IN_MULTI" | "ERR_JWE_ENC_MISSING" | "ERR_JWE_RECIPIENT_ALG_INFERENCE" | "ERR_JWE_NO_MATCHING_RECIPIENT" | "ERR_JWE_HEADER_PARAMS_NOT_DISJOINT" | "ERR_JWK_INVALID" | "ERR_JWK_KEY_NOT_FOUND" | "ERR_JWK_UNSUPPORTED" | "ERR_DPOP_INVALID" | "ERR_DPOP_CLAIM_INVALID" | "ERR_DPOP_NONCE_INVALID" | "ERR_DPOP_REPLAY" | "ERR_SESSION_TOKEN_INVALID" | "ERR_SESSION_READ_FAILED" | "ERR_SESSION_READONLY_EVENT" | "ERR_SESSION_KEY_INVALID" | "ERR_SESSION_CONFIG_INVALID" | "ERR_INVALID_OPTION";
19
17
  /**
20
- * A {@link JWTError} `code`: every {@link KnownJWTErrorCode}, plus any other
21
- * string for forward compatibility — a newer unjwt (or a caller constructing
22
- * its own `JWTError`) may carry a code this version's union does not list.
18
+ * A {@link JWTError} `code`: any {@link KnownJWTErrorCode} or any other string.
19
+ * A `JWTError` built outside unjwt can hold a code outside the union.
23
20
  */
24
21
  export type JWTErrorCode = KnownJWTErrorCode | (string & {});
25
22
  /**
26
- * Maps known error codes to their structured `cause` shapes.
27
- * Only codes that carry decoded context beyond the message appear here.
28
- * Extend this interface when new structured causes are introduced.
23
+ * The structured `cause` of each code that has one.
24
+ * {@link isJWTError} narrows `cause` to this shape for the listed codes.
29
25
  */
30
26
  export interface JWTErrorCauseMap {
31
- /** Decoded claims available at the point the lifetime check failed. */
27
+ /** The token's `jti` (when a string), `iat` and `exp` claims. */
32
28
  ERR_JWT_EXPIRED: {
33
29
  jti?: string;
34
30
  iat?: number;
@@ -36,13 +32,13 @@ export interface JWTErrorCauseMap {
36
32
  };
37
33
  }
38
34
  /**
39
- * Base error class for all unjwt errors. Every error thrown by the library is
40
- * an instance of `JWTError`, so a single `instanceof` check is sufficient to
41
- * distinguish library errors from other thrown values. Use the `code` property
42
- * (or the `isJWTError` type guard) to narrow to a specific error condition.
35
+ * The error class of the JWS, JWE, JWK, DPoP and session operations.
36
+ * `code` names the condition. Narrow on it with {@link isJWTError}.
37
+ *
38
+ * The `unjwt/utils` primitives throw {@link UnsecureError} instead.
43
39
  *
44
40
  * @example
45
- * import { JWTError, isJWTError } from "unjwt/jws";
41
+ * import { verify, isJWTError } from "unjwt/jws";
46
42
  *
47
43
  * try {
48
44
  * await verify(token, key);
@@ -58,16 +54,11 @@ export declare class JWTError<TCode extends JWTErrorCode = JWTErrorCode> extends
58
54
  constructor(message: string, code: TCode, cause?: unknown);
59
55
  }
60
56
  /**
61
- * Type guard that narrows `error` to `JWTError`.
62
- *
63
- * When called with a code that is a key of `JWTErrorCauseMap`, the return
64
- * type is additionally narrowed to include the structured `cause` shape for
65
- * that code. When called with any other `KnownJWTErrorCode`, `code` is narrowed
66
- * but `cause` remains `unknown`. When called without a code, narrows only to
67
- * `JWTError`.
57
+ * Returns `true` when `error` is a {@link JWTError}, and with `code`, only when its `code` is that one.
68
58
  *
69
- * `code` is a {@link KnownJWTErrorCode}, so a misspelled code fails to
70
- * type-check instead of silently never matching.
59
+ * With a code listed in {@link JWTErrorCauseMap}, `cause` narrows to that shape.
60
+ * With any other code, `code` narrows and `cause` stays `unknown`.
61
+ * `code` is a {@link KnownJWTErrorCode}, so a misspelled code is a type error.
71
62
  *
72
63
  * @example
73
64
  * if (isJWTError(err, "ERR_JWT_EXPIRED")) {
@@ -80,17 +71,13 @@ export declare function isJWTError<T extends keyof JWTErrorCauseMap>(error: unkn
80
71
  export declare function isJWTError<T extends KnownJWTErrorCode>(error: unknown, code: T): error is JWTError<T>;
81
72
  export declare function isJWTError(error: unknown): error is JWTError;
82
73
  /**
83
- * Type guard that narrows `error` to the bundled {@link UnsecureError}, the
84
- * class thrown by unjwt's codec / JSON / random primitives and carried as the
85
- * `cause` of a {@link JWTError} when a wire segment or key field fails to
86
- * decode. Pass a `code` to narrow further.
74
+ * Returns `true` when `error` is the {@link UnsecureError} unjwt throws, and with `code`, only when its `code` is that one.
75
+ * Use it on the `cause` of a {@link JWTError} whose token segment or key field failed to decode.
87
76
  *
88
- * Check against this export rather than a `UnsecureError` imported from a
89
- * consumer-installed `unsecure`: unjwt bundles its own copy, so the two classes
90
- * are distinct and `instanceof` against the latter is always `false`.
77
+ * A separately installed copy of the class never matches.
91
78
  *
92
79
  * @example
93
- * import { isJWTError, isUnsecureError } from "unjwt/jws";
80
+ * import { verify, isJWTError, isUnsecureError } from "unjwt/jws";
94
81
  *
95
82
  * const error = await verify(token, key).catch((e) => e);
96
83
  * if (isJWTError(error, "ERR_JWS_INVALID") && isUnsecureError(error.cause, "MALFORMED")) {
@@ -61,9 +61,6 @@ async function exportJWK(key, jwkParams) {
61
61
  }
62
62
  return exportedJwk;
63
63
  }
64
- async function exportKey(key, jwkParams) {
65
- return exportJWK(key, jwkParams);
66
- }
67
64
  function toPublicJWK(jwk) {
68
65
  if (!isAsymmetricJWK(jwk)) throw new JWTError(isJWK(jwk) ? "An oct JWK has no public half." : "toPublicJWK expects an asymmetric JWK.", "ERR_JWK_INVALID");
69
66
  const { d: _d, p: _p, q: _q, dp: _dp, dq: _dq, qi: _qi, oth: _oth, k: _k, key_ops, ...publicMembers } = copyJsonObject({ ...jwk }, {
@@ -94,4 +91,4 @@ function publicKeyOps(keyOps) {
94
91
  }
95
92
  return ops;
96
93
  }
97
- export { assertKidOption, assertNoReservedJWKParams, exportJWK, exportKey, requestedKeyOps, toPublicJWK, withGeneratedKid, withKeyOps };
94
+ export { assertKidOption, assertNoReservedJWKParams, exportJWK, requestedKeyOps, toPublicJWK, withGeneratedKid, withKeyOps };