unjwt 0.8.3 → 0.8.5

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 (54) hide show
  1. package/README.md +3 -9
  2. package/dist/_chunks/algorithms.mjs +1541 -0
  3. package/dist/_chunks/{_codec-jwe.d.mts → codec-jwe.d.mts} +40 -9
  4. package/dist/_chunks/{_codec-jws.mjs → codec-jws.mjs} +148 -95
  5. package/dist/_chunks/cookie.mjs +50 -0
  6. package/dist/_chunks/digest.mjs +10 -3
  7. package/dist/_chunks/error.d.mts +103 -0
  8. package/dist/_chunks/export.mjs +97 -0
  9. package/dist/_chunks/index.d.mts +5 -103
  10. package/dist/_chunks/index2.d.mts +206 -12
  11. package/dist/_chunks/index3.d.mts +12 -109
  12. package/dist/_chunks/{jwe.d.mts → index4.d.mts} +3 -1
  13. package/dist/_chunks/{jws.d.mts → index5.d.mts} +6 -2
  14. package/dist/_chunks/index6.d.mts +113 -0
  15. package/dist/_chunks/jwe.mjs +208 -74
  16. package/dist/_chunks/jwk.mjs +214 -2237
  17. package/dist/_chunks/jws.mjs +289 -202
  18. package/dist/_chunks/jwt.d.mts +2 -1
  19. package/dist/_chunks/{_jwt-internal.mjs → jwt.mjs} +124 -68
  20. package/dist/_chunks/libs/unsecure.d.mts +35 -25
  21. package/dist/_chunks/libs/unsecure.mjs +148 -302
  22. package/dist/_chunks/{_internal.d.mts → limits.d.mts} +1 -1
  23. package/dist/_chunks/payload.mjs +23 -0
  24. package/dist/_chunks/wrap.mjs +209 -0
  25. package/dist/adapters/elysia/index.d.mts +4 -4
  26. package/dist/adapters/elysia/index.mjs +5 -3
  27. package/dist/adapters/h3/index.d.mts +13 -13
  28. package/dist/adapters/h3/index.mjs +8 -10
  29. package/dist/adapters/h3v1/index.d.mts +5 -5
  30. package/dist/adapters/h3v1/index.mjs +6 -8
  31. package/dist/core/internal.mjs +1 -1
  32. package/dist/core/{jwe.d.mts → jwe/index.d.mts} +5 -4
  33. package/dist/core/jwe/index.mjs +5 -0
  34. package/dist/core/jwk/index.d.mts +6 -0
  35. package/dist/core/jwk/index.mjs +7 -0
  36. package/dist/core/{jws.d.mts → jws/index.d.mts} +5 -4
  37. package/dist/core/jws/index.mjs +5 -0
  38. package/dist/dpop/index.d.mts +73 -0
  39. package/dist/dpop/index.mjs +296 -0
  40. package/dist/error.d.mts +3 -0
  41. package/dist/error.mjs +20 -0
  42. package/dist/index.d.mts +10 -9
  43. package/dist/index.mjs +7 -3
  44. package/dist/session/index.d.mts +4 -4
  45. package/dist/session/index.mjs +6 -46
  46. package/dist/utils/index.d.mts +7 -0
  47. package/dist/{core/utils → utils}/index.mjs +4 -3
  48. package/package.json +22 -10
  49. package/dist/core/jwe.mjs +0 -5
  50. package/dist/core/jwk.d.mts +0 -207
  51. package/dist/core/jwk.mjs +0 -4
  52. package/dist/core/jws.mjs +0 -5
  53. package/dist/core/utils/index.d.mts +0 -6
  54. /package/dist/_chunks/{_cookie-attributes.d.mts → cookie-attributes.d.mts} +0 -0
@@ -1,43 +1,86 @@
1
- import { UnsecureError, base64Parse as base64Parse$1, base64Stringify as base64Stringify$1, safeJsonParse, textEncoder } from "./libs/unsecure.mjs";
2
- var JWTError = class extends Error {
3
- code;
4
- cause;
5
- constructor(message, code, cause) {
6
- super(message);
7
- this.name = "JWTError";
8
- this.code = code;
9
- this.cause = cause;
10
- }
11
- };
12
- function isJWTError(error, code) {
13
- if (!(error instanceof JWTError)) return false;
14
- return code === void 0 || error.code === code;
1
+ import { UnsecureError, safeJsonParse, textEncoder } from "./libs/unsecure.mjs";
2
+ import { JWTError } from "../error.mjs";
3
+ function base64UrlStringify(data) {
4
+ return encode(bytesOf(data, "Base64Url"), "base64url");
15
5
  }
16
- function isUnsecureError(error, code) {
17
- if (!(error instanceof UnsecureError)) return false;
18
- return code === void 0 || error.code === code;
6
+ function base64Stringify(data) {
7
+ return encode(bytesOf(data, "Base64"), "base64");
19
8
  }
20
- function base64UrlStringify(data) {
21
- return base64Stringify$1(data, { alphabet: "base64url" });
9
+ function base64UrlParse(text, options) {
10
+ return parse(text, URL, false, "Base64Url", options);
22
11
  }
23
- function base64UrlParse(input, options) {
24
- const parseOptions = {
25
- ...options,
26
- alphabet: "base64url",
27
- padding: false
28
- };
29
- return typeof input === "string" ? base64Parse$1(input, parseOptions) : base64Parse$1(input, parseOptions);
12
+ function base64Parse(text, options) {
13
+ return parse(text, STD, true, "Base64", options);
30
14
  }
31
- function base64Stringify(data) {
32
- return base64Stringify$1(data);
15
+ const URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
16
+ const STD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
17
+ const fatalDecoder = /* @__PURE__ */ new TextDecoder("utf-8", {
18
+ ignoreBOM: true,
19
+ fatal: true
20
+ });
21
+ function bytesOf(data, label) {
22
+ if (typeof data === "string") return textEncoder.encode(data);
23
+ if (data instanceof Uint8Array) return data;
24
+ throw new UnsecureError("INVALID_TYPE", `${label}.stringify: expected a string or Uint8Array.`);
25
+ }
26
+ function encode(bytes, name) {
27
+ const buffer = globalThis.Buffer;
28
+ if (typeof buffer?.from === "function") return buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString(name);
29
+ if ("toBase64" in Uint8Array.prototype) return (bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength ? bytes : bytes.slice()).toBase64({
30
+ alphabet: name,
31
+ omitPadding: name === "base64url"
32
+ });
33
+ const alphabet = name === "base64url" ? URL : STD;
34
+ const padded = name === "base64";
35
+ let out = "";
36
+ let i = 0;
37
+ for (; i + 2 < bytes.length; i += 3) {
38
+ const n = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
39
+ out += alphabet[n >>> 18] + alphabet[n >>> 12 & 63] + alphabet[n >>> 6 & 63] + alphabet[n & 63];
40
+ }
41
+ const rem = bytes.length - i;
42
+ if (rem === 1) {
43
+ const n = bytes[i] << 16;
44
+ out += alphabet[n >>> 18] + alphabet[n >>> 12 & 63] + (padded ? "==" : "");
45
+ } else if (rem === 2) {
46
+ const n = bytes[i] << 16 | bytes[i + 1] << 8;
47
+ out += alphabet[n >>> 18] + alphabet[n >>> 12 & 63] + alphabet[n >>> 6 & 63] + (padded ? "=" : "");
48
+ }
49
+ return out;
50
+ }
51
+ function parse(text, alphabet, padded, label, options) {
52
+ if (typeof text !== "string") throw new UnsecureError("INVALID_TYPE", `${label}.parse: expected a string, got ${typeof text}.`);
53
+ const wantString = options?.returnAs === void 0 || options.returnAs === "string";
54
+ let end = text.length;
55
+ while (end > 0 && text.charCodeAt(end - 1) === 61) end--;
56
+ const rem = end % 4;
57
+ if (rem === 1) throw malformed(label, `${end} symbols cannot encode whole bytes.`);
58
+ const padNeeded = padded && rem !== 0 ? 4 - rem : 0;
59
+ if (text.length - end !== padNeeded) throw malformed(label, `expected ${padNeeded} "=" padding characters, found ${text.length - end}.`);
60
+ const out = new Uint8Array(end * 6 >>> 3);
61
+ let acc = 0;
62
+ let held = 0;
63
+ let j = 0;
64
+ for (let i = 0; i < end; i++) {
65
+ const value = alphabet.indexOf(text[i]);
66
+ if (value < 0) throw malformed(label, `invalid character ${JSON.stringify(text[i])} at index ${i}.`);
67
+ acc = acc << 6 | value;
68
+ held += 6;
69
+ if (held >= 8) {
70
+ held -= 8;
71
+ out[j++] = acc >>> held & 255;
72
+ }
73
+ }
74
+ if (held !== 0 && (acc & (1 << held) - 1) !== 0) throw malformed(label, "the last symbol sets bits past the final byte.");
75
+ if (!wantString) return out;
76
+ try {
77
+ return fatalDecoder.decode(out);
78
+ } catch {
79
+ throw malformed(label, "decoded bytes are not valid UTF-8.");
80
+ }
33
81
  }
34
- function base64Parse(input, options) {
35
- const parseOptions = {
36
- ...options,
37
- alphabet: "base64",
38
- padding: true
39
- };
40
- return typeof input === "string" ? base64Parse$1(input, parseOptions) : base64Parse$1(input, parseOptions);
82
+ function malformed(label, detail) {
83
+ return new UnsecureError("MALFORMED", `${label}.parse: ${detail}`);
41
84
  }
42
85
  const base64UrlEncode = base64UrlStringify;
43
86
  const base64UrlDecode = base64UrlParse;
@@ -75,11 +118,20 @@ function isPrivateJWK(key) {
75
118
  return "d" in key && typeof key.d === "string";
76
119
  }
77
120
  function isPublicJWK(key) {
78
- if (!isJWK(key) || key.kty === "oct" || "d" in key && typeof key.d === "string") return false;
121
+ if (!isJWK(key) || key.kty === "oct" || Object.hasOwn(key, "k")) return false;
122
+ if ("d" in key && typeof key.d === "string") return false;
79
123
  if (key.kty === "EC") return typeof key.x === "string" && typeof key.y === "string";
80
124
  if (key.kty === "OKP") return typeof key.x === "string";
81
125
  return typeof key.n === "string" && typeof key.e === "string";
82
126
  }
127
+ function hasSymmetricSecret(key) {
128
+ return key.kty === "oct" || Object.hasOwn(key, "k");
129
+ }
130
+ const MAX_QUOTED_VALUE = 64;
131
+ function quoteTokenValue(value) {
132
+ const text = String(value);
133
+ return JSON.stringify(text.length > MAX_QUOTED_VALUE ? `${text.slice(0, MAX_QUOTED_VALUE)}…` : text);
134
+ }
83
135
  function concatUint8Arrays(...arrays) {
84
136
  const totalLength = arrays.reduce((length, arr) => length + arr.length, 0);
85
137
  const result = new Uint8Array(totalLength);
@@ -91,7 +143,7 @@ function concatUint8Arrays(...arrays) {
91
143
  return result;
92
144
  }
93
145
  function uint32be(value) {
94
- if (value < 0 || value >= 2 ** 32) throw new JWTError(`value must be >= 0 and <= ${2 ** 32 - 1}. Received ${value}`, "ERR_INVALID_OPTION");
146
+ if (value < 0 || value >= 2 ** 32) throw new JWTError(`value must be >= 0 and <= ${2 ** 32 - 1} (got ${value})`, "ERR_INVALID_OPTION");
95
147
  return new Uint8Array([
96
148
  value >>> 24,
97
149
  value >>> 16,
@@ -142,7 +194,7 @@ function parseDurationSeconds(value) {
142
194
  }
143
195
  if (typeof value === "string") {
144
196
  const match = value.match(DURATION_REGEX);
145
- if (!match || !match[1]) throw new JWTError("Invalid duration format. Must be an integer or a string like '10m', '2h', '7D', etc.", "ERR_INVALID_OPTION");
197
+ if (!match || !match[1]) throw new JWTError("Duration must be an integer or a string like '10m' or '7D'.", "ERR_INVALID_OPTION");
146
198
  const n = Number.parseInt(match[1], 10);
147
199
  const unit = match[2] || "s";
148
200
  return n * TIME_CONSTANTS[unit];
@@ -155,12 +207,12 @@ function computeDurationInSeconds(duration) {
155
207
  return seconds;
156
208
  }
157
209
  function expiredClaimMessage(exp) {
158
- return `JWT "exp" (Expiration Time) Claim validation failed: Token has expired (exp: ${(/* @__PURE__ */ new Date(exp * 1e3)).toISOString()})`;
210
+ return `JWT "exp" is in the past (${_formatNumericDate(exp)}).`;
159
211
  }
160
212
  function validateJwtClaims(jwtClaims, options = {}) {
161
213
  assertClaimValidationOptions(options);
162
214
  const clockTolerance = _normalizeClockTolerance(options.clockTolerance);
163
- const currentTime = Math.round((options.currentDate ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
215
+ const currentTime = (options.currentDate ?? /* @__PURE__ */ new Date()).getTime() / 1e3;
164
216
  if (options.requiredClaims !== void 0 || options.issuer !== void 0 || options.audience !== void 0 || options.subject !== void 0 || options.maxTokenAge !== void 0) {
165
217
  const allRequiredClaims = new Set(options.requiredClaims);
166
218
  if (options.issuer !== void 0) allRequiredClaims.add("iss");
@@ -171,33 +223,25 @@ function validateJwtClaims(jwtClaims, options = {}) {
171
223
  for (const claimName of allRequiredClaims) if (!Object.hasOwn(jwtClaims, claimName)) missingClaims.push(claimName);
172
224
  if (missingClaims.length > 0) throw new JWTError(`Missing required JWT Claims: ${missingClaims.join(", ")}`, "ERR_JWT_CLAIM_MISSING");
173
225
  }
174
- _assertNumericDate(jwtClaims.exp, "exp", "Expiration Time");
175
- _assertNumericDate(jwtClaims.nbf, "nbf", "Not Before");
176
- _assertNumericDate(jwtClaims.iat, "iat", "Issued At");
226
+ _assertNumericDate(jwtClaims.exp, "exp");
227
+ _assertNumericDate(jwtClaims.nbf, "nbf");
228
+ _assertNumericDate(jwtClaims.iat, "iat");
177
229
  if (options.issuer !== void 0) {
178
230
  const expectedIssuers = maybeArray(options.issuer);
179
- if (!jwtClaims.iss || !expectedIssuers.includes(jwtClaims.iss)) throw new JWTError(`Invalid JWT "iss" (Issuer) Claim: Expected ${expectedIssuers.join(" or ")}, got ${jwtClaims.iss}`, "ERR_JWT_CLAIM_INVALID");
231
+ if (!jwtClaims.iss || !expectedIssuers.includes(jwtClaims.iss)) throw new JWTError(`Invalid JWT "iss": expected ${expectedIssuers.join(" or ")}, got ${jwtClaims.iss}`, "ERR_JWT_CLAIM_INVALID");
180
232
  }
181
- if (options.subject !== void 0 && jwtClaims.sub !== options.subject) throw new JWTError(`Invalid JWT "sub" (Subject) Claim: Expected ${options.subject}, got ${jwtClaims.sub}`, "ERR_JWT_CLAIM_INVALID");
233
+ if (options.subject !== void 0 && jwtClaims.sub !== options.subject) throw new JWTError(`Invalid JWT "sub": expected ${options.subject}, got ${jwtClaims.sub}`, "ERR_JWT_CLAIM_INVALID");
182
234
  if (options.audience !== void 0) {
183
235
  const expectedAudiences = maybeArray(options.audience);
184
236
  const claimAudience = maybeArray(jwtClaims.aud || []);
185
- if (!claimAudience.some((aud) => expectedAudiences.includes(aud))) throw new JWTError(`Invalid JWT "aud" (Audience) Claim: Expected ${expectedAudiences.join(" or ")}, got ${claimAudience.join(", ")}`, "ERR_JWT_CLAIM_INVALID");
237
+ if (!claimAudience.some((aud) => expectedAudiences.includes(aud))) throw new JWTError(`Invalid JWT "aud": expected ${expectedAudiences.join(" or ")}, got ${claimAudience.join(", ")}`, "ERR_JWT_CLAIM_INVALID");
186
238
  }
187
- if (typeof jwtClaims.nbf === "number" && jwtClaims.nbf > currentTime + clockTolerance) throw new JWTError(`JWT "nbf" (Not Before) Claim validation failed: Token is not yet valid (nbf: ${(/* @__PURE__ */ new Date(jwtClaims.nbf * 1e3)).toISOString()})`, "ERR_JWT_NBF");
188
- if (typeof jwtClaims.exp === "number" && jwtClaims.exp <= currentTime - clockTolerance) throw new JWTError(expiredClaimMessage(jwtClaims.exp), "ERR_JWT_EXPIRED", {
189
- jti: jwtClaims.jti,
190
- iat: jwtClaims.iat,
191
- exp: jwtClaims.exp
192
- });
239
+ if (typeof jwtClaims.nbf === "number" && jwtClaims.nbf > currentTime + clockTolerance) throw new JWTError(`JWT "nbf" is in the future (${_formatNumericDate(jwtClaims.nbf)}).`, "ERR_JWT_NBF");
240
+ if (typeof jwtClaims.exp === "number" && jwtClaims.exp <= currentTime - clockTolerance) throw new JWTError(expiredClaimMessage(jwtClaims.exp), "ERR_JWT_EXPIRED", _expiredCause(jwtClaims));
193
241
  if (options.maxTokenAge !== void 0) {
194
- if (typeof jwtClaims.iat !== "number") throw new JWTError("JWT \"iat\" (Issued At) Claim must be a number when maxTokenAge is set.", "ERR_JWT_CLAIM_INVALID");
195
- if (jwtClaims.iat > currentTime + clockTolerance) throw new JWTError(`JWT "iat" (Issued At) Claim validation failed: Token was issued in the future (iat: ${(/* @__PURE__ */ new Date(jwtClaims.iat * 1e3)).toISOString()})`, "ERR_JWT_CLAIM_INVALID");
196
- if (jwtClaims.iat < currentTime - computeDurationInSeconds(options.maxTokenAge) - clockTolerance) throw new JWTError(`JWT "iat" (Issued At) Claim validation failed: Token is too old (maxTokenAge: ${options.maxTokenAge}, iat: ${(/* @__PURE__ */ new Date(jwtClaims.iat * 1e3)).toISOString()})`, "ERR_JWT_EXPIRED", {
197
- jti: jwtClaims.jti,
198
- iat: jwtClaims.iat,
199
- exp: jwtClaims.exp
200
- });
242
+ if (typeof jwtClaims.iat !== "number") throw new JWTError("JWT \"iat\" must be a number when maxTokenAge is set.", "ERR_JWT_CLAIM_INVALID");
243
+ if (jwtClaims.iat > currentTime + clockTolerance) throw new JWTError(`JWT "iat" is in the future (${_formatNumericDate(jwtClaims.iat)}).`, "ERR_JWT_CLAIM_INVALID");
244
+ if (jwtClaims.iat < currentTime - computeDurationInSeconds(options.maxTokenAge) - clockTolerance) throw new JWTError(`JWT "iat" is older than maxTokenAge ${options.maxTokenAge}.`, "ERR_JWT_EXPIRED", _expiredCause(jwtClaims));
201
245
  }
202
246
  }
203
247
  function assertClaimValidationOptions(options) {
@@ -238,10 +282,22 @@ function _assertExpectedValues(value, name) {
238
282
  function _isNonEmptyString(value) {
239
283
  return typeof value === "string" && value !== "";
240
284
  }
241
- function _assertNumericDate(value, claim, label) {
285
+ function _assertNumericDate(value, claim) {
242
286
  if (value === void 0) return;
243
- if (typeof value !== "number" || !Number.isFinite(value)) throw new JWTError(`JWT "${claim}" (${label}) Claim must be a number.`, "ERR_JWT_CLAIM_INVALID");
244
- if (value < 0) throw new JWTError(`JWT "${claim}" (${label}) Claim must be a non-negative NumericDate.`, "ERR_JWT_CLAIM_INVALID");
287
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new JWTError(`JWT "${claim}" must be a number.`, "ERR_JWT_CLAIM_INVALID");
288
+ if (value < 0) throw new JWTError(`JWT "${claim}" must be a non-negative NumericDate.`, "ERR_JWT_CLAIM_INVALID");
289
+ }
290
+ function _formatNumericDate(seconds) {
291
+ const date = /* @__PURE__ */ new Date(seconds * 1e3);
292
+ return Number.isFinite(date.getTime()) ? date.toISOString() : String(seconds);
293
+ }
294
+ function _expiredCause(claims) {
295
+ const { jti, iat, exp } = claims;
296
+ return {
297
+ jti: typeof jti === "string" ? jti : void 0,
298
+ iat,
299
+ exp
300
+ };
245
301
  }
246
302
  function applyTypDefault(header, payload) {
247
303
  const declared = header.typ;
@@ -285,8 +341,8 @@ function getPlaintextBytes(payload) {
285
341
  function computeJwtTimeClaims(payload, options) {
286
342
  const { currentDate, issuedAt, expiresIn, expiresAt, notBeforeIn, notBeforeAt } = options ?? {};
287
343
  if (issuedAt !== void 0 && typeof issuedAt !== "boolean" && !(issuedAt instanceof Date)) throw new JWTError("\"issuedAt\" must be a boolean or a Date.", "ERR_INVALID_OPTION");
288
- if (expiresIn !== void 0 && expiresAt !== void 0) throw new JWTError("\"expiresIn\" and \"expiresAt\" are mutually exclusive; pass only one.", "ERR_JWT_CLAIM_INVALID");
289
- if (notBeforeIn !== void 0 && notBeforeAt !== void 0) throw new JWTError("\"notBeforeIn\" and \"notBeforeAt\" are mutually exclusive; pass only one.", "ERR_JWT_CLAIM_INVALID");
344
+ if (expiresIn !== void 0 && expiresAt !== void 0) throw new JWTError("\"expiresIn\" and \"expiresAt\" are mutually exclusive.", "ERR_JWT_CLAIM_INVALID");
345
+ if (notBeforeIn !== void 0 && notBeforeAt !== void 0) throw new JWTError("\"notBeforeIn\" and \"notBeforeAt\" are mutually exclusive.", "ERR_JWT_CLAIM_INVALID");
290
346
  const hasExpHint = expiresIn !== void 0 || expiresAt !== void 0;
291
347
  const hasNbfHint = notBeforeIn !== void 0 || notBeforeAt !== void 0;
292
348
  const hasIatHint = issuedAt === true || issuedAt instanceof Date;
@@ -296,7 +352,7 @@ function computeJwtTimeClaims(payload, options) {
296
352
  const wantsNbf = hasNbfHint && (!("nbf" in payload) || payload.nbf === void 0);
297
353
  const wantsIat = hasIatHint && (!("iat" in payload) || payload.iat === void 0);
298
354
  if (!wantsExp && !wantsNbf && !wantsIat) return void 0;
299
- const now = Math.round((currentDate ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
355
+ const now = Math.floor((currentDate ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
300
356
  const claims = { ...payload };
301
357
  claims.iat ||= issuedAt instanceof Date ? Math.floor(_instantOf(issuedAt, "issuedAt") / 1e3) : now;
302
358
  if (wantsExp) {
@@ -384,8 +440,8 @@ function validateCriticalHeaders(protectedHeader, options) {
384
440
  const { family, recognizedHeaders = [] } = options;
385
441
  assertCritShape(crit, { family });
386
442
  for (const param of crit) {
387
- if (!Object.hasOwn(protectedHeader, param)) throw new JWTError(`Critical header parameter "${param}" listed in "crit" but not present in the protected header.`, `ERR_${family}_INVALID`);
388
- if (!recognizedHeaders.includes(param) && !BASE_PROCESSED[family].includes(param)) throw new JWTError(`Unprocessed critical header parameters: ${param}`, `ERR_${family}_INVALID`);
443
+ if (!Object.hasOwn(protectedHeader, param)) throw new JWTError(`Critical header parameter ${quoteTokenValue(param)} is missing from the protected header.`, `ERR_${family}_INVALID`);
444
+ if (!recognizedHeaders.includes(param) && !BASE_PROCESSED[family].includes(param)) throw new JWTError(`Unprocessed critical header parameters: ${quoteTokenValue(param)}`, `ERR_${family}_INVALID`);
389
445
  }
390
446
  }
391
- export { JWTError, applyTypDefault, assertClaimValidationOptions, assertCritShape, assertCryptoKey, assertPlainObject, assertStringList, base64Decode, base64Encode, base64Parse, base64Stringify, base64UrlDecode, base64UrlEncode, base64UrlParse, base64UrlStringify, computeDurationInSeconds, computeJwtTimeClaims, concatUint8Arrays, expiredClaimMessage, getPlaintextBytes, isAsymmetricJWK, isCryptoKey, isCryptoKeyPair, isJWK, isJWKSet, isJWTContent, isJWTError, isPlainObject, isPrivateJWK, isPublicJWK, isSymmetricJWK, isUnsecureError, maybeArray, parsePayloadBytes, parsePayloadSegment, parsePayloadText, uint32be, validateCriticalHeadersJWE, validateCriticalHeadersJWS, validateJwtClaims };
447
+ export { applyTypDefault, assertClaimValidationOptions, assertCritShape, assertCryptoKey, assertPlainObject, assertStringList, base64Decode, base64Encode, base64Parse, base64Stringify, base64UrlDecode, base64UrlEncode, base64UrlParse, base64UrlStringify, computeDurationInSeconds, computeJwtTimeClaims, concatUint8Arrays, expiredClaimMessage, getPlaintextBytes, hasSymmetricSecret, isAsymmetricJWK, isCryptoKey, isCryptoKeyPair, isJWK, isJWKSet, isJWTContent, isPlainObject, isPrivateJWK, isPublicJWK, isSymmetricJWK, maybeArray, parsePayloadBytes, parsePayloadSegment, parsePayloadText, quoteTokenValue, uint32be, validateCriticalHeadersJWE, validateCriticalHeadersJWS, validateJwtClaims };
@@ -92,7 +92,8 @@ export declare function safeJsonParse<T = any>(json: string): T;
92
92
  * Remove prototype-pollution vectors from a plain record in-place.
93
93
  *
94
94
  * Strips own properties named `__proto__`, `prototype`, and `constructor`
95
- * through nested objects and arrays, enumerable or not. Cycle-safe.
95
+ * through nested objects and arrays — a named property hung on an array
96
+ * included — enumerable or not. Cycle-safe.
96
97
  *
97
98
  * Values are read from their property descriptors — array elements included —
98
99
  * so a getter is never invoked: an accessor is neither traversed nor removed
@@ -114,17 +115,36 @@ export declare function safeJsonParse<T = any>(json: string): T;
114
115
  * one.
115
116
  */
116
117
  export declare function sanitizeObject<T extends Record<string, unknown> | undefined>(obj: T): T;
118
+ /** Options for {@link sanitizeObjectCopy}. */
119
+ interface SanitizeCopyOptions {
120
+ /**
121
+ * Constructors whose instances the copy carries by reference, untouched.
122
+ * Everything the copy meets must be JSON-shaped — a plain object or an
123
+ * array — unless it is an instance (`instanceof`, so subclasses count) of
124
+ * one of these. By reference means unchanged *and* unsanitized: an instance
125
+ * listed here is trusted as the caller holds it.
126
+ *
127
+ * `instanceof` is bound to the realm the constructor came from, which is
128
+ * the realm the caller's own `Date` or `Map` belong to.
129
+ */
130
+ allowlist?: Iterable<abstract new (...args: never[]) => object>;
131
+ }
117
132
  /**
118
133
  * Return a sanitized deep copy of `obj`. The input is never mutated.
119
134
  *
120
135
  * Dangerous keys (`__proto__`, `prototype`, `constructor`) are stripped from
121
136
  * every array and plain object reached. A plain object is one rooted on
122
- * `Object.prototype` or on `null`; copies of both are rooted on
123
- * `Object.prototype`. Every other value — `Date`, `Map`, `Set`, typed
124
- * arrays, `RegExp`, class instances, functions — is carried into the copy by
125
- * reference, exactly as {@link sanitizeObject} leaves it in place. By
126
- * reference means unchanged *and* unsanitized: what a `Map` or `Set` holds is
127
- * never read by either function.
137
+ * `null` or on an `Object.prototype` — this realm's or another's, so a value
138
+ * parsed inside a `vm` context or a frame is rebuilt like one parsed here;
139
+ * copies of both are rooted on this realm's `Object.prototype`.
140
+ *
141
+ * Anything else — a `Date`, `Map`, `Set`, typed array, `RegExp` or class
142
+ * instance — is refused: such a value is not the JSON shape this copy exists
143
+ * for, and carrying it through silently would report a sanitized copy that
144
+ * holds an unexamined object. Name the constructors you trust in
145
+ * `allowlist`, and their instances are carried into the copy by reference,
146
+ * or use {@link sanitizeObject} to walk a live object graph in place.
147
+ * Functions are values, not containers, and are carried by reference.
128
148
  *
129
149
  * Only own enumerable data properties are copied — the JSON shape. Accessors
130
150
  * are skipped rather than invoked, so the copy never runs caller code; an
@@ -135,10 +155,14 @@ export declare function sanitizeObject<T extends Record<string, unknown> | undef
135
155
  * Cycle-safe: circular references in the input are preserved in the output
136
156
  * (pointing at the copied node, not the original reference).
137
157
  *
138
- * Non-object / undefined inputs — and objects that are not arrays or plain
139
- * objects — are returned unchanged.
158
+ * Primitives and `undefined` are returned unchanged.
159
+ *
160
+ * @throws {UnsecureError} `INVALID_TYPE` if an object that is neither plain,
161
+ * an array nor an instance of an `allowlist` entry is
162
+ * met at any depth, or if `allowlist` holds something
163
+ * that is not a constructor.
140
164
  */
141
- export declare function sanitizeObjectCopy<T extends Record<string, unknown> | undefined>(obj: T): T;
165
+ export declare function sanitizeObjectCopy<T>(obj: T, options?: SanitizeCopyOptions): T;
142
166
  /**
143
167
  * Shared UTF-8 `TextEncoder`. Encoding holds no state between calls, so a
144
168
  * single instance serves every caller.
@@ -156,18 +180,4 @@ export type BytesSource = ArrayBufferLike | ArrayBufferView<ArrayBufferLike>;
156
180
  * payload, not a signature to swallow.
157
181
  */
158
182
  export declare const textDecoder: TextDecoder;
159
- /**
160
- * Decoder output shape. Omit to mirror the input (`string` in → `string` out,
161
- * `Uint8Array` in → `Uint8Array` out). `"bytes"` aliases `"uint8array"`.
162
- */
163
- export type DecodeReturnAs = "string" | "uint8array" | "bytes";
164
- /** Shared `parse` options. */
165
- export interface DecodeOptions {
166
- /** Output shape. Mirrors the input type when omitted. */
167
- returnAs?: DecodeReturnAs;
168
- /**
169
- * Tolerate malformed input (skip/normalize) instead of throwing.
170
- * @default false
171
- */
172
- loose?: boolean;
173
- }
183
+ export {};