@peac/protocol 0.16.4 → 0.16.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.
package/dist/index.mjs CHANGED
@@ -1529,6 +1529,138 @@ async function fetchDiscovery(issuerUrl) {
1529
1529
  );
1530
1530
  }
1531
1531
  }
1532
+
1533
+ // src/_internal/address-policy.ts
1534
+ var IPV4_OCTET = /^(?:0|[1-9]\d{0,2})$/;
1535
+ var IPV6_GROUP = /^[0-9a-fA-F]{1,4}$/;
1536
+ function parseIPv4(text) {
1537
+ const parts = text.split(".");
1538
+ if (parts.length !== 4) return null;
1539
+ let value = 0;
1540
+ for (const part of parts) {
1541
+ if (!IPV4_OCTET.test(part)) return null;
1542
+ const octet = Number(part);
1543
+ if (octet > 255) return null;
1544
+ value = value * 256 + octet;
1545
+ }
1546
+ return value;
1547
+ }
1548
+ function parseGroups(text) {
1549
+ if (text === "") return [];
1550
+ const groups = [];
1551
+ for (const group of text.split(":")) {
1552
+ if (!IPV6_GROUP.test(group)) return null;
1553
+ groups.push(parseInt(group, 16));
1554
+ }
1555
+ return groups;
1556
+ }
1557
+ function parseIPv6(text) {
1558
+ if (text.length === 0 || text.length > 45) return null;
1559
+ let body = text;
1560
+ const lastColon = body.lastIndexOf(":");
1561
+ if (lastColon === -1) return null;
1562
+ const last = body.slice(lastColon + 1);
1563
+ if (last.includes(".")) {
1564
+ const v4 = parseIPv4(last);
1565
+ if (v4 === null) return null;
1566
+ body = `${body.slice(0, lastColon + 1)}${Math.floor(v4 / 65536).toString(16)}:${(v4 % 65536).toString(16)}`;
1567
+ }
1568
+ const halves = body.split("::");
1569
+ if (halves.length > 2) return null;
1570
+ const head = parseGroups(halves[0]);
1571
+ if (head === null) return null;
1572
+ if (halves.length === 1) return head.length === 8 ? head : null;
1573
+ const tail = parseGroups(halves[1]);
1574
+ if (tail === null) return null;
1575
+ const fill = 8 - head.length - tail.length;
1576
+ if (fill < 1) return null;
1577
+ return [...head, ...new Array(fill).fill(0), ...tail];
1578
+ }
1579
+ var IPV4_BLOCKS = [
1580
+ ["0.0.0.0", 8, "private_ip"],
1581
+ ["10.0.0.0", 8, "private_ip"],
1582
+ ["100.64.0.0", 10, "private_ip"],
1583
+ ["127.0.0.0", 8, "loopback"],
1584
+ ["169.254.0.0", 16, "link_local"],
1585
+ ["172.16.0.0", 12, "private_ip"],
1586
+ ["192.0.0.0", 24, "private_ip"],
1587
+ ["192.0.2.0", 24, "private_ip"],
1588
+ ["192.31.196.0", 24, "private_ip"],
1589
+ ["192.52.193.0", 24, "private_ip"],
1590
+ ["192.88.99.0", 24, "private_ip"],
1591
+ ["192.168.0.0", 16, "private_ip"],
1592
+ ["192.175.48.0", 24, "private_ip"],
1593
+ ["198.18.0.0", 15, "private_ip"],
1594
+ ["198.51.100.0", 24, "private_ip"],
1595
+ ["203.0.113.0", 24, "private_ip"],
1596
+ ["224.0.0.0", 4, "private_ip"],
1597
+ ["240.0.0.0", 4, "private_ip"]
1598
+ ];
1599
+ var IPV4_BLOCK_TABLE = IPV4_BLOCKS.map(([network, prefix, reason]) => {
1600
+ const base = parseIPv4(network);
1601
+ const size = 2 ** (32 - prefix);
1602
+ return { start: base, end: base + size - 1, reason };
1603
+ });
1604
+ function classifyIPv4(value) {
1605
+ for (const block of IPV4_BLOCK_TABLE) {
1606
+ if (value >= block.start && value <= block.end) {
1607
+ return { allowed: false, reason: block.reason };
1608
+ }
1609
+ }
1610
+ return { allowed: true };
1611
+ }
1612
+ function matchesPrefix(groups, network, bits) {
1613
+ for (let i = 0; i < 8; i++) {
1614
+ const bitsHere = Math.min(16, Math.max(0, bits - 16 * i));
1615
+ if (bitsHere === 0) return true;
1616
+ const mask = 65535 << 16 - bitsHere & 65535;
1617
+ if ((groups[i] & mask) !== (network[i] & mask)) return false;
1618
+ }
1619
+ return true;
1620
+ }
1621
+ function v6(text) {
1622
+ return parseIPv6(text);
1623
+ }
1624
+ var GLOBAL_UNICAST_V6 = v6("2000::");
1625
+ var IPV6_BLOCKS_IN_GLOBAL = [
1626
+ [v6("2001::"), 23],
1627
+ [v6("2001:db8::"), 32],
1628
+ [v6("2002::"), 16],
1629
+ [v6("2620:4f:8000::"), 48],
1630
+ [v6("3fff::"), 20]
1631
+ ];
1632
+ var IPV6_LOOPBACK = v6("::1");
1633
+ var IPV6_LINK_LOCAL = v6("fe80::");
1634
+ var IPV4_MAPPED = v6("::ffff:0:0");
1635
+ var NAT64_WELL_KNOWN = v6("64:ff9b::");
1636
+ function classifyIPv6(groups) {
1637
+ if (matchesPrefix(groups, IPV4_MAPPED, 96) || matchesPrefix(groups, NAT64_WELL_KNOWN, 96)) {
1638
+ return classifyIPv4(groups[6] * 65536 + groups[7]);
1639
+ }
1640
+ if (!matchesPrefix(groups, GLOBAL_UNICAST_V6, 3)) {
1641
+ if (matchesPrefix(groups, IPV6_LOOPBACK, 128)) return { allowed: false, reason: "loopback" };
1642
+ if (matchesPrefix(groups, IPV6_LINK_LOCAL, 10)) return { allowed: false, reason: "link_local" };
1643
+ return { allowed: false, reason: "private_ip" };
1644
+ }
1645
+ for (const [network, bits] of IPV6_BLOCKS_IN_GLOBAL) {
1646
+ if (matchesPrefix(groups, network, bits)) return { allowed: false, reason: "private_ip" };
1647
+ }
1648
+ return { allowed: true };
1649
+ }
1650
+ function classifyAddress(ip) {
1651
+ const v4 = parseIPv4(ip);
1652
+ if (v4 !== null) return classifyIPv4(v4);
1653
+ const groups = parseIPv6(ip);
1654
+ if (groups !== null) return classifyIPv6(groups);
1655
+ return { allowed: false, reason: "private_ip" };
1656
+ }
1657
+ function hostIpLiteral(hostname) {
1658
+ if (hostname.startsWith("[") && hostname.endsWith("]")) return hostname.slice(1, -1);
1659
+ if (/^[0-9.]+$/.test(hostname)) return hostname;
1660
+ return null;
1661
+ }
1662
+
1663
+ // src/ssrf-safe-fetch.ts
1532
1664
  var cachedCapabilities = null;
1533
1665
  function getSSRFCapabilities() {
1534
1666
  if (cachedCapabilities) {
@@ -1546,9 +1678,9 @@ function detectCapabilities() {
1546
1678
  networkIsolation: false,
1547
1679
  protectionLevel: "full",
1548
1680
  notes: [
1549
- "Full SSRF protection available via Node.js dns module",
1550
- "DNS resolution checked before HTTP connection",
1551
- "All RFC 1918 private ranges blocked"
1681
+ "Hostnames are resolved once via the Node.js dns module",
1682
+ "Every resolved address must be a global unicast address",
1683
+ "The connection is made only to a vetted address"
1552
1684
  ]
1553
1685
  };
1554
1686
  }
@@ -1626,61 +1758,16 @@ function detectCapabilities() {
1626
1758
  function resetSSRFCapabilitiesCache() {
1627
1759
  cachedCapabilities = null;
1628
1760
  }
1629
- function parseIPv4(ip) {
1630
- const parts = ip.split(".");
1631
- if (parts.length !== 4) return null;
1632
- const octets = [];
1633
- for (const part of parts) {
1634
- const num = parseInt(part, 10);
1635
- if (isNaN(num) || num < 0 || num > 255) return null;
1636
- octets.push(num);
1637
- }
1638
- return { octets };
1639
- }
1640
- function isInCIDR(ip, cidr) {
1641
- const [rangeStr, maskStr] = cidr.split("/");
1642
- const range = parseIPv4(rangeStr);
1643
- if (!range) return false;
1644
- const maskBits = parseInt(maskStr, 10);
1645
- if (isNaN(maskBits) || maskBits < 0 || maskBits > 32) return false;
1646
- const ipNum = ip.octets[0] << 24 | ip.octets[1] << 16 | ip.octets[2] << 8 | ip.octets[3];
1647
- const rangeNum = range.octets[0] << 24 | range.octets[1] << 16 | range.octets[2] << 8 | range.octets[3];
1648
- const mask = maskBits === 0 ? 0 : ~((1 << 32 - maskBits) - 1);
1649
- return (ipNum & mask) === (rangeNum & mask);
1650
- }
1651
- function isIPv6Loopback(ip) {
1652
- const normalized = ip.toLowerCase().replace(/^::ffff:/, "");
1653
- return normalized === "::1" || normalized === "0:0:0:0:0:0:0:1";
1654
- }
1655
- function isIPv6LinkLocal(ip) {
1656
- const normalized = ip.toLowerCase();
1657
- return normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb");
1658
- }
1659
1761
  function isBlockedIP(ip) {
1660
- const ipv4Match = ip.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
1661
- const effectiveIP = ipv4Match ? ipv4Match[1] : ip;
1662
- const ipv4 = parseIPv4(effectiveIP);
1663
- if (ipv4) {
1664
- if (isInCIDR(ipv4, "10.0.0.0/8") || isInCIDR(ipv4, "172.16.0.0/12") || isInCIDR(ipv4, "192.168.0.0/16")) {
1665
- return { blocked: true, reason: "private_ip" };
1666
- }
1667
- if (isInCIDR(ipv4, "127.0.0.0/8")) {
1668
- return { blocked: true, reason: "loopback" };
1669
- }
1670
- if (isInCIDR(ipv4, "169.254.0.0/16")) {
1671
- return { blocked: true, reason: "link_local" };
1672
- }
1673
- return { blocked: false };
1674
- }
1675
- if (isIPv6Loopback(ip)) {
1676
- return { blocked: true, reason: "loopback" };
1677
- }
1678
- if (isIPv6LinkLocal(ip)) {
1679
- return { blocked: true, reason: "link_local" };
1680
- }
1681
- return { blocked: false };
1762
+ const verdict = classifyAddress(ip);
1763
+ return verdict.allowed ? { blocked: false } : { blocked: true, reason: verdict.reason };
1682
1764
  }
1683
1765
  async function resolveHostname(hostname) {
1766
+ const literal = hostIpLiteral(hostname);
1767
+ if (literal !== null) {
1768
+ const nodeRuntime = typeof process !== "undefined" && Boolean(process.versions?.node);
1769
+ return { ok: true, ips: [literal], browser: !nodeRuntime };
1770
+ }
1684
1771
  if (typeof process !== "undefined" && process.versions?.node) {
1685
1772
  try {
1686
1773
  const dns = await import('dns');
@@ -1711,7 +1798,10 @@ async function resolveHostname(hostname) {
1711
1798
  message: `DNS resolution failed for ${hostname}: ${ipv4Error.message}`
1712
1799
  };
1713
1800
  }
1714
- return { ok: true, ips: [], browser: false };
1801
+ return {
1802
+ ok: false,
1803
+ message: `DNS resolution returned no addresses for ${hostname}`
1804
+ };
1715
1805
  } catch (err) {
1716
1806
  return {
1717
1807
  ok: false,
@@ -1721,6 +1811,88 @@ async function resolveHostname(hostname) {
1721
1811
  }
1722
1812
  return { ok: true, ips: [], browser: true };
1723
1813
  }
1814
+ var NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
1815
+ async function discardBody(response) {
1816
+ await response.body?.cancel();
1817
+ }
1818
+ async function fetchFromVettedAddresses(url, addresses, headers, signal) {
1819
+ const https = await import('https');
1820
+ const { isIP } = await import('net');
1821
+ const target = new URL(url);
1822
+ const host = hostIpLiteral(target.hostname) ?? target.hostname;
1823
+ if (addresses.length === 0 || isIP(host) !== 0 && !addresses.includes(host)) {
1824
+ throw new Error(`Refusing to connect to an address that was not vetted: ${host}`);
1825
+ }
1826
+ const entries = addresses.map((address) => ({
1827
+ address,
1828
+ family: address.includes(":") ? 6 : 4
1829
+ }));
1830
+ const lookup = (_hostname, options, callback) => {
1831
+ if (options.all) {
1832
+ callback(null, entries);
1833
+ } else {
1834
+ callback(null, entries[0].address, entries[0].family);
1835
+ }
1836
+ };
1837
+ return new Promise((resolve, reject) => {
1838
+ const req = https.request(
1839
+ target,
1840
+ { method: "GET", headers, lookup, agent: false, signal },
1841
+ (res) => {
1842
+ try {
1843
+ const status = res.statusCode ?? 0;
1844
+ const responseHeaders = new Headers();
1845
+ for (const [name, value] of Object.entries(res.headers)) {
1846
+ if (value === void 0) continue;
1847
+ if (Array.isArray(value)) {
1848
+ for (const item of value) responseHeaders.append(name, item);
1849
+ } else {
1850
+ responseHeaders.set(name, value);
1851
+ }
1852
+ }
1853
+ if (NULL_BODY_STATUSES.has(status)) {
1854
+ res.resume();
1855
+ resolve(new Response(null, { status, headers: responseHeaders }));
1856
+ return;
1857
+ }
1858
+ let settled = false;
1859
+ const body = new ReadableStream({
1860
+ start(controller) {
1861
+ res.on("data", (chunk) => {
1862
+ if (settled) return;
1863
+ controller.enqueue(new Uint8Array(chunk));
1864
+ if ((controller.desiredSize ?? 0) <= 0) res.pause();
1865
+ });
1866
+ res.on("end", () => {
1867
+ if (settled) return;
1868
+ settled = true;
1869
+ controller.close();
1870
+ });
1871
+ res.on("error", (err) => {
1872
+ if (settled) return;
1873
+ settled = true;
1874
+ controller.error(err);
1875
+ });
1876
+ },
1877
+ pull() {
1878
+ res.resume();
1879
+ },
1880
+ cancel() {
1881
+ settled = true;
1882
+ res.destroy();
1883
+ }
1884
+ });
1885
+ resolve(new Response(body, { status, headers: responseHeaders }));
1886
+ } catch (err) {
1887
+ res.destroy();
1888
+ reject(err);
1889
+ }
1890
+ }
1891
+ );
1892
+ req.on("error", reject);
1893
+ req.end();
1894
+ });
1895
+ }
1724
1896
  async function ssrfSafeFetch(url, options = {}) {
1725
1897
  const {
1726
1898
  timeoutMs = VERIFIER_LIMITS.fetchTimeoutMs,
@@ -1769,38 +1941,44 @@ async function ssrfSafeFetch(url, options = {}) {
1769
1941
  blockedUrl: url
1770
1942
  };
1771
1943
  }
1772
- if (!dnsResult.browser) {
1773
- for (const ip of dnsResult.ips) {
1774
- const blockResult = isBlockedIP(ip);
1775
- if (blockResult.blocked) {
1776
- return {
1777
- ok: false,
1778
- reason: blockResult.reason,
1779
- message: `Blocked ${blockResult.reason} address: ${ip} for ${url}`,
1780
- blockedUrl: url
1781
- };
1782
- }
1944
+ for (const ip of dnsResult.ips) {
1945
+ const blockResult = isBlockedIP(ip);
1946
+ if (blockResult.blocked) {
1947
+ return {
1948
+ ok: false,
1949
+ reason: blockResult.reason,
1950
+ message: `Blocked ${blockResult.reason} address: ${ip} for ${url}`,
1951
+ blockedUrl: url
1952
+ };
1783
1953
  }
1784
1954
  }
1785
1955
  let redirectCount = 0;
1786
1956
  let currentUrl = url;
1957
+ let vettedAddresses = dnsResult.browser ? null : dnsResult.ips;
1787
1958
  const originalOrigin = parsedUrl.origin;
1788
1959
  while (true) {
1789
1960
  try {
1790
1961
  const controller = new AbortController();
1791
1962
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1792
- const response = await fetch(currentUrl, {
1793
- headers: {
1794
- Accept: "application/json, text/plain",
1795
- ...headers
1796
- },
1963
+ const requestHeaders = {
1964
+ Accept: "application/json, text/plain",
1965
+ ...headers
1966
+ };
1967
+ const response = vettedAddresses === null ? await fetch(currentUrl, {
1968
+ headers: requestHeaders,
1797
1969
  signal: controller.signal,
1798
1970
  redirect: "manual"
1799
1971
  // Handle redirects manually for security
1800
- });
1972
+ }) : await fetchFromVettedAddresses(
1973
+ currentUrl,
1974
+ vettedAddresses,
1975
+ requestHeaders,
1976
+ controller.signal
1977
+ );
1801
1978
  clearTimeout(timeoutId);
1802
1979
  if (response.status >= 300 && response.status < 400) {
1803
1980
  const location = response.headers.get("location");
1981
+ await discardBody(response);
1804
1982
  if (!location) {
1805
1983
  return {
1806
1984
  ok: false,
@@ -1869,24 +2047,24 @@ async function ssrfSafeFetch(url, options = {}) {
1869
2047
  blockedUrl: redirectUrl.href
1870
2048
  };
1871
2049
  }
1872
- if (!redirectDnsResult.browser) {
1873
- for (const ip of redirectDnsResult.ips) {
1874
- const blockResult = isBlockedIP(ip);
1875
- if (blockResult.blocked) {
1876
- return {
1877
- ok: false,
1878
- reason: blockResult.reason,
1879
- message: `Redirect to blocked ${blockResult.reason} address: ${ip}`,
1880
- blockedUrl: redirectUrl.href
1881
- };
1882
- }
2050
+ for (const ip of redirectDnsResult.ips) {
2051
+ const blockResult = isBlockedIP(ip);
2052
+ if (blockResult.blocked) {
2053
+ return {
2054
+ ok: false,
2055
+ reason: blockResult.reason,
2056
+ message: `Redirect to blocked ${blockResult.reason} address: ${ip}`,
2057
+ blockedUrl: redirectUrl.href
2058
+ };
1883
2059
  }
1884
2060
  }
1885
2061
  currentUrl = redirectUrl.href;
2062
+ vettedAddresses = redirectDnsResult.browser ? null : redirectDnsResult.ips;
1886
2063
  continue;
1887
2064
  }
1888
2065
  const contentLength = response.headers.get("content-length");
1889
2066
  if (contentLength && parseInt(contentLength, 10) > maxBytes) {
2067
+ await discardBody(response);
1890
2068
  return {
1891
2069
  ok: false,
1892
2070
  reason: "response_too_large",
@@ -2117,6 +2295,25 @@ function mapSSRFError(reason, context) {
2117
2295
  }
2118
2296
  return { code, reason };
2119
2297
  }
2298
+ var CANONICAL_ED25519_X = /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
2299
+ function isPlainObject(value) {
2300
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2301
+ }
2302
+ function jwkEntryProblem(entry) {
2303
+ if (!isPlainObject(entry)) return "not an object";
2304
+ if (entry.kty !== "OKP") return "kty must be OKP";
2305
+ if (entry.crv !== "Ed25519") return "crv must be Ed25519";
2306
+ if (typeof entry.x !== "string" || !CANONICAL_ED25519_X.test(entry.x)) {
2307
+ return "x must be a canonical base64url 32-byte value";
2308
+ }
2309
+ if (Object.prototype.hasOwnProperty.call(entry, "d")) return "private key member d present";
2310
+ if (entry.kid !== void 0 && typeof entry.kid !== "string") return "kid must be a string";
2311
+ if (entry.use !== void 0 && entry.use !== "sig") return "use must be sig";
2312
+ if (entry.alg !== void 0 && entry.alg !== "EdDSA" && entry.alg !== "Ed25519") {
2313
+ return "alg must be EdDSA or Ed25519";
2314
+ }
2315
+ return null;
2316
+ }
2120
2317
  async function resolveJWKS(issuerUrl, options) {
2121
2318
  const cacheTtlMs = options?.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
2122
2319
  const maxCacheEntries = options?.maxCacheEntries ?? DEFAULT_MAX_CACHE_ENTRIES;
@@ -2206,9 +2403,9 @@ async function resolveJWKS(issuerUrl, options) {
2206
2403
  blockedUrl: jwksResult.blockedUrl
2207
2404
  };
2208
2405
  }
2209
- let jwks;
2406
+ let parsed;
2210
2407
  try {
2211
- jwks = JSON.parse(jwksResult.body);
2408
+ parsed = JSON.parse(jwksResult.body);
2212
2409
  } catch {
2213
2410
  return {
2214
2411
  ok: false,
@@ -2216,20 +2413,37 @@ async function resolveJWKS(issuerUrl, options) {
2216
2413
  message: "JWKS response is not valid JSON"
2217
2414
  };
2218
2415
  }
2219
- if (!jwks.keys || !Array.isArray(jwks.keys)) {
2416
+ const rawKeys = isPlainObject(parsed) ? parsed.keys : void 0;
2417
+ if (!Array.isArray(rawKeys)) {
2220
2418
  return {
2221
2419
  ok: false,
2222
2420
  code: "E_VERIFY_JWKS_INVALID",
2223
2421
  message: "JWKS missing required keys array"
2224
2422
  };
2225
2423
  }
2226
- if (jwks.keys.length > VERIFIER_LIMITS.maxJwksKeys) {
2424
+ if (rawKeys.length > VERIFIER_LIMITS.maxJwksKeys) {
2227
2425
  return {
2228
2426
  ok: false,
2229
2427
  code: "E_VERIFY_JWKS_TOO_MANY_KEYS",
2230
- message: `JWKS has too many keys: ${jwks.keys.length} > ${VERIFIER_LIMITS.maxJwksKeys}`
2428
+ message: `JWKS has too many keys: ${rawKeys.length} > ${VERIFIER_LIMITS.maxJwksKeys}`
2231
2429
  };
2232
2430
  }
2431
+ const validKeys = [];
2432
+ const skipped = [];
2433
+ rawKeys.forEach((entry, index) => {
2434
+ const problem = jwkEntryProblem(entry);
2435
+ if (problem === null) {
2436
+ validKeys.push(entry);
2437
+ } else {
2438
+ skipped.push(`#${index}: ${problem}`);
2439
+ }
2440
+ });
2441
+ if (skipped.length > 0) {
2442
+ console.warn(
2443
+ `[peac:jwks] Skipped ${skipped.length} invalid JWKS entr${skipped.length === 1 ? "y" : "ies"} for ${normalizedIssuer} (${skipped.join("; ")})`
2444
+ );
2445
+ }
2446
+ const jwks = { keys: validKeys };
2233
2447
  const kidReuseError = checkKidReuse(normalizedIssuer, jwks, now);
2234
2448
  if (kidReuseError) {
2235
2449
  return {
@@ -3268,9 +3482,13 @@ async function fetchIssuerJWKS(issuerOrigin) {
3268
3482
  return {
3269
3483
  jwks: result.jwks,
3270
3484
  fromCache: result.fromCache,
3271
- rawBytes: result.rawBytes
3485
+ rawBytes: result.rawBytes,
3486
+ revokedKeys: result.revokedKeys
3272
3487
  };
3273
3488
  }
3489
+ function findRevokedKey(kid, revokedKeys) {
3490
+ return revokedKeys?.find((rk) => rk.kid === kid);
3491
+ }
3274
3492
  async function verifyReceiptCore(options) {
3275
3493
  const {
3276
3494
  receipt,
@@ -3440,6 +3658,19 @@ async function verifyReceiptCore(options) {
3440
3658
  from_cache: jwksResult.fromCache,
3441
3659
  keys_count: jwksResult.jwks.keys.length
3442
3660
  });
3661
+ const revokedPinned = findRevokedKey(kid, jwksResult.revokedKeys);
3662
+ if (revokedPinned) {
3663
+ builder.fail("key.resolve", "E_REVOKED_KEY_USED", {
3664
+ kid,
3665
+ revoked_at: revokedPinned.revoked_at,
3666
+ ...revokedPinned.reason && { reason: revokedPinned.reason }
3667
+ });
3668
+ builder.failure("key_not_found", normalizedIssuer, kid);
3669
+ return {
3670
+ valid: false,
3671
+ report: includeMeta ? builder.addTimestamp().build() : builder.build()
3672
+ };
3673
+ }
3443
3674
  const jwk = jwksResult.jwks.keys.find((k) => k.kid === kid);
3444
3675
  if (!jwk) {
3445
3676
  builder.fail("key.resolve", "E_VERIFY_KEY_NOT_FOUND", {
@@ -3501,6 +3732,19 @@ async function verifyReceiptCore(options) {
3501
3732
  from_cache: jwksResult.fromCache,
3502
3733
  keys_count: jwksResult.jwks.keys.length
3503
3734
  });
3735
+ const revoked = findRevokedKey(kid, jwksResult.revokedKeys);
3736
+ if (revoked) {
3737
+ builder.fail("key.resolve", "E_REVOKED_KEY_USED", {
3738
+ kid,
3739
+ revoked_at: revoked.revoked_at,
3740
+ ...revoked.reason && { reason: revoked.reason }
3741
+ });
3742
+ builder.failure("key_not_found", normalizedIssuer, kid);
3743
+ return {
3744
+ valid: false,
3745
+ report: includeMeta ? builder.addTimestamp().build() : builder.build()
3746
+ };
3747
+ }
3504
3748
  const jwk = jwksResult.jwks.keys.find((k) => k.kid === kid);
3505
3749
  if (!jwk) {
3506
3750
  builder.fail("key.resolve", "E_VERIFY_KEY_NOT_FOUND", {