@fedify/fedify 2.4.0-dev.2103 → 2.4.0-dev.2115

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 (93) hide show
  1. package/dist/{builder-CHht75pX.mjs → builder-CZcBWrBd.mjs} +29 -5
  2. package/dist/compat/mod.d.cts +1 -1
  3. package/dist/compat/mod.d.ts +1 -1
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{compound-proof-DKKH_Nr1.mjs → compound-proof-CDuP9MIX.mjs} +3 -3
  6. package/dist/{context-mDTxTw-S.d.cts → context-SMNOiiY6.d.cts} +93 -11
  7. package/dist/{context-CfJE_oQu.d.ts → context-tOiJfBoD.d.ts} +93 -11
  8. package/dist/{deno-CkjiYK_Z.mjs → deno-CCABdw7r.mjs} +1 -1
  9. package/dist/{docloader-C1hAJnWc.mjs → docloader-Bby5g2iu.mjs} +2 -2
  10. package/dist/federation/builder.test.mjs +1 -1
  11. package/dist/federation/compound-fanout.test.mjs +3 -3
  12. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  13. package/dist/federation/handler.test.mjs +118 -5
  14. package/dist/federation/hashlink-media.test.mjs +2 -2
  15. package/dist/federation/idempotency.test.mjs +2 -2
  16. package/dist/federation/keycache.test.mjs +67 -1
  17. package/dist/federation/metrics.test.mjs +1 -1
  18. package/dist/federation/middleware.test.mjs +184 -7
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +3 -3
  21. package/dist/federation/mod.d.ts +3 -3
  22. package/dist/federation/mod.js +1 -1
  23. package/dist/federation/outgoing-proof.test.mjs +5 -5
  24. package/dist/federation/portable-actor.test.mjs +3 -3
  25. package/dist/federation/portable-gateway-keys.test.mjs +24 -7
  26. package/dist/federation/portable-inbox.test.mjs +4 -4
  27. package/dist/federation/portable.test.mjs +31 -4
  28. package/dist/federation/send.test.mjs +3 -3
  29. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  30. package/dist/federation/tasks/tasks.test.mjs +2 -2
  31. package/dist/federation/temporal.test.mjs +1 -1
  32. package/dist/federation/webfinger.test.mjs +1 -1
  33. package/dist/{federation-C1dL6SiT.cjs → federation-DYr384xz.cjs} +116 -15
  34. package/dist/{http-Cala2Grs.cjs → http-B4mQvNcv.cjs} +140 -55
  35. package/dist/{http-BU-8fUrz.mjs → http-BcLnr2do.mjs} +6 -12
  36. package/dist/{http-xlG-jRdB.d.cts → http-BrQgkJay.d.ts} +12 -7
  37. package/dist/{http-tTlTeFa0.js → http-Ce6_Lf7k.js} +140 -55
  38. package/dist/{http-n2v6DPxu.d.ts → http-D76Py0M4.d.cts} +12 -7
  39. package/dist/{key-I8gLBkcf.mjs → key-CkNxztNV.mjs} +107 -16
  40. package/dist/{keycache-BUDHB5rY.mjs → keycache-BInaECGj.mjs} +68 -0
  41. package/dist/{kv-cache-Bl5EBFrU.cjs → kv-cache-Dqp_E-1g.cjs} +1 -1
  42. package/dist/{kv-cache-CxQu_cd4.js → kv-cache-DwzXzchj.js} +1 -1
  43. package/dist/{kv-cache-DJFoGRWQ.mjs → kv-cache-iFaRIcIL.mjs} +1 -1
  44. package/dist/{ld-wiJbqJHa.mjs → ld-CnT0f1nU.mjs} +4 -7
  45. package/dist/{metrics-BCAa5-kE.mjs → metrics-em_7lwhG.mjs} +1 -1
  46. package/dist/{middleware-FnITXhtz.mjs → middleware-BpwKvOpU.mjs} +26 -17
  47. package/dist/{middleware-B6IVTVL0.js → middleware-DhIBngkj.js} +108 -7
  48. package/dist/{middleware-6p2dYEVA.mjs → middleware-U9IuzvDO.mjs} +1 -1
  49. package/dist/{mod-BYvPz_T0.d.ts → mod-COWgSo2v.d.ts} +1 -1
  50. package/dist/{mod-Coacostr.d.ts → mod-CTh7ijx5.d.ts} +1 -1
  51. package/dist/{mod-xGFCHqq_.d.cts → mod-Cr79cQlc.d.cts} +1 -1
  52. package/dist/{mod-CVZrXSdm.d.ts → mod-Cwdt_CWU.d.ts} +1 -1
  53. package/dist/{mod-C8LieBzF.d.cts → mod-DMdGFMEl.d.cts} +1 -1
  54. package/dist/{mod-B3OzOobN.d.cts → mod-DnElMslb.d.cts} +1 -1
  55. package/dist/mod.cjs +5 -5
  56. package/dist/mod.d.cts +7 -7
  57. package/dist/mod.d.ts +7 -7
  58. package/dist/mod.js +5 -5
  59. package/dist/nodeinfo/handler.test.mjs +1 -1
  60. package/dist/{owner-BJp47LxN.mjs → owner-9LiHmOv1.mjs} +4 -4
  61. package/dist/{owner-DCt8W_Cv.cjs → owner-BtoMCv6Z.cjs} +1 -1
  62. package/dist/{owner-j5yrbXS1.js → owner-Cb3ytrlq.js} +1 -1
  63. package/dist/{portable-key-B5oKE_wZ.mjs → portable-key-9ck61lI0.mjs} +5 -4
  64. package/dist/{portable-key-id-Bva5iT3W.mjs → portable-key-id-T7lecIbt.mjs} +3 -2
  65. package/dist/{proof-BE4T5zF6.mjs → proof-xdPYGPaA.mjs} +32 -31
  66. package/dist/{send-BFITuKHw.mjs → send-Bf8rSWch.mjs} +3 -3
  67. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  68. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  69. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  70. package/dist/sig/compound-proof.test.mjs +2 -2
  71. package/dist/sig/http.test.mjs +2 -2
  72. package/dist/sig/key.test.mjs +1 -1
  73. package/dist/sig/ld.test.mjs +2 -2
  74. package/dist/sig/mod.cjs +3 -3
  75. package/dist/sig/mod.d.cts +2 -2
  76. package/dist/sig/mod.d.ts +2 -2
  77. package/dist/sig/mod.js +3 -3
  78. package/dist/sig/owner.test.mjs +1 -1
  79. package/dist/sig/portable-collection.test.mjs +2 -2
  80. package/dist/sig/portable-key.test.mjs +301 -23
  81. package/dist/sig/proof.test.mjs +3 -3
  82. package/dist/{sig-CRTRoouH.cjs → sig-Bj3fHnv0.cjs} +2 -2
  83. package/dist/{sig-CJHdGk9w.js → sig-DN9pOglI.js} +2 -2
  84. package/dist/{temporal-CyDDbE4i.mjs → temporal-tAtwv6vX.mjs} +1 -1
  85. package/dist/testing/kv.test.mjs +1 -1
  86. package/dist/testing/mod.d.mts +91 -9
  87. package/dist/utils/docloader.test.mjs +2 -2
  88. package/dist/utils/kv-cache.test.mjs +1 -1
  89. package/dist/utils/mod.cjs +1 -1
  90. package/dist/utils/mod.d.cts +1 -1
  91. package/dist/utils/mod.d.ts +1 -1
  92. package/dist/utils/mod.js +1 -1
  93. package/package.json +7 -7
@@ -14,7 +14,7 @@ import serialize from "json-canon";
14
14
  import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
15
15
  //#region deno.json
16
16
  var name = "@fedify/fedify";
17
- var version = "2.4.0-dev.2103+3be829c0";
17
+ var version = "2.4.0-dev.2115+e2feaf2b";
18
18
  //#endregion
19
19
  //#region src/sig/portable-key-id.ts
20
20
  const COMPATIBLE_KEY_ID_PATH_PATTERN = /^\/\.well-known\/apgateway\/did(?::|%3A)/i;
@@ -70,8 +70,9 @@ function hasPortableActor(activity) {
70
70
  /**
71
71
  * Checks whether a key ID looks like an [FEP-ef61] compatible identifier,
72
72
  * i.e., an HTTP(S) URL under a gateway's `/.well-known/apgateway/did:` path.
73
- * Such a key ID names a key that a gateway holds for a portable actor, so it
74
- * is resolved only as a gateway key, and never cached in a key cache.
73
+ * Such a key ID may name a key that a gateway holds for a portable actor,
74
+ * which only HTTP Signatures accept, so what it resolves to is cached apart
75
+ * for each purpose, never under the key ID itself.
75
76
  *
76
77
  * [FEP-ef61]: https://w3id.org/fep/ef61
77
78
  * @internal
@@ -1315,6 +1316,32 @@ async function verifyKeyOwnership(key, options = {}) {
1315
1316
  });
1316
1317
  return null;
1317
1318
  }
1319
+ /**
1320
+ * Wraps a key cache so that lookups through the wrapper miss, while
1321
+ * whatever they resolve is still written to the cache. It is used to retry
1322
+ * a verification that failed with a cached key with a freshly fetched one.
1323
+ * @param keyCache The key cache to wrap.
1324
+ * @returns The wrapped key cache.
1325
+ * @internal
1326
+ */
1327
+ function bypassKeyCacheReads(keyCache) {
1328
+ const cache = keyCache;
1329
+ const bypassed = {
1330
+ get: () => Promise.resolve(void 0),
1331
+ set: async (keyId, key) => await cache?.set(keyId, key),
1332
+ getFetchError: () => Promise.resolve(void 0),
1333
+ setFetchError: async (keyId, error) => await cache?.setFetchError?.(keyId, error)
1334
+ };
1335
+ if (cache?.compatibleKeyScope != null) bypassed.compatibleKeyScope = (scope) => {
1336
+ const scoped = cache.compatibleKeyScope(scope);
1337
+ return {
1338
+ get: () => Promise.resolve(void 0),
1339
+ set: async (keyId, key, options) => await scoped.set(keyId, key, options),
1340
+ delete: async (keyId) => await scoped.delete(keyId)
1341
+ };
1342
+ };
1343
+ return bypassed;
1344
+ }
1318
1345
  async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
1319
1346
  tracerProvider ??= trace.getTracerProvider();
1320
1347
  return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
@@ -1351,9 +1378,9 @@ async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
1351
1378
  * Gateway keys of [FEP-ef61] portable actors, i.e., keys whose IDs are
1352
1379
  * compatible identifiers that dereference to portable actor documents, are
1353
1380
  * not resolved by this function, as they only authenticate HTTP requests;
1354
- * use `verifyRequest()` or `getKeyOwner()` for them instead. Keys
1355
- * whose IDs are compatible identifiers are never read from or written to
1356
- * the key cache.
1381
+ * use `verifyRequest()` or `getKeyOwner()` for them instead. Keys whose IDs
1382
+ * are compatible identifiers are never read from or written to the key
1383
+ * cache under their IDs, except for failures to fetch them.
1357
1384
  *
1358
1385
  * [FEP-ef61]: https://w3id.org/fep/ef61
1359
1386
  * @template T The type of the key to fetch. Either {@link CryptographicKey}
@@ -1376,9 +1403,9 @@ function fetchKey(keyId, cls, options = {}) {
1376
1403
  * Gateway keys of [FEP-ef61] portable actors, i.e., keys whose IDs are
1377
1404
  * compatible identifiers that dereference to portable actor documents, are
1378
1405
  * not resolved by this function, as they only authenticate HTTP requests;
1379
- * use `verifyRequest()` or `getKeyOwner()` for them instead. Keys
1380
- * whose IDs are compatible identifiers are never read from or written to
1381
- * the key cache.
1406
+ * use `verifyRequest()` or `getKeyOwner()` for them instead. Keys whose IDs
1407
+ * are compatible identifiers are never read from or written to the key
1408
+ * cache under their IDs, except for failures to fetch them.
1382
1409
  *
1383
1410
  * [FEP-ef61]: https://w3id.org/fep/ef61
1384
1411
  *
@@ -1561,11 +1588,13 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
1561
1588
  }
1562
1589
  }
1563
1590
  if (isCompatibleKeyId(cacheKey) && isPortableActorDocument(object)) {
1591
+ const scopedCache = keyCache;
1564
1592
  if (portableGatewayKeyResolver == null || cls !== CryptographicKey) {
1565
1593
  logger.debug("Failed to verify; key {keyId} is a gateway key of the portable actor {actorId}, which is only accepted for HTTP Signatures.", {
1566
1594
  keyId,
1567
1595
  actorId: object.id?.href
1568
1596
  });
1597
+ await scopedCache?.set(cacheKey, null);
1569
1598
  return {
1570
1599
  key: null,
1571
1600
  cached: false
@@ -1577,11 +1606,13 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
1577
1606
  keyId,
1578
1607
  actorId: object.id?.href
1579
1608
  });
1609
+ await scopedCache?.set(cacheKey, null);
1580
1610
  return {
1581
1611
  key: null,
1582
1612
  cached: false
1583
1613
  };
1584
1614
  }
1615
+ await scopedCache?.set(cacheKey, resolution.key, { expires: resolution.expires });
1585
1616
  return {
1586
1617
  key: resolution.key,
1587
1618
  cached: false
@@ -1710,8 +1741,9 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
1710
1741
  }
1711
1742
  /**
1712
1743
  * Tells whether a key claims a portable actor as its owner. Such a key can
1713
- * only be a gateway key, which is never cached, so a cached one was cached
1714
- * by an older version that trusted web origins, and is fetched again.
1744
+ * only be a gateway key, which is never cached under its ID, so a cached one
1745
+ * was cached by an older version that trusted web origins, and is fetched
1746
+ * again.
1715
1747
  */
1716
1748
  function claimsPortableOwner(key) {
1717
1749
  const ownerId = key instanceof CryptographicKey ? key.ownerId : key?.controllerId;
@@ -1727,11 +1759,12 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
1727
1759
  "key"
1728
1760
  ]);
1729
1761
  const keyId = cacheKey.href;
1730
- if (isCompatibleKeyId(cacheKey)) options = {
1731
- ...options,
1732
- keyCache: void 0
1733
- };
1734
1762
  const keyCache = options.keyCache;
1763
+ if (isCompatibleKeyId(cacheKey)) {
1764
+ const result = await fetchCompatibleKey(cacheKey, cls, options, onCachedUnavailable, onFetchError, logger);
1765
+ outcome = result.outcome;
1766
+ return result.result;
1767
+ }
1735
1768
  const didKey = await resolveDidKey(cacheKey, cls, keyCache, logger);
1736
1769
  if (didKey != null) {
1737
1770
  outcome = { result: didKey.key == null ? "invalid" : didKey.cached ? "hit" : "fetched" };
@@ -1773,6 +1806,65 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
1773
1806
  });
1774
1807
  }
1775
1808
  }
1809
+ /**
1810
+ * Tells which purpose a lookup of a key at a compatible identifier serves.
1811
+ */
1812
+ function getCompatibleKeyScope(cls, options) {
1813
+ if (cls !== CryptographicKey) return "multikey";
1814
+ return options.portableGatewayKeyResolver == null ? "cryptographicKey" : "httpSignature";
1815
+ }
1816
+ /**
1817
+ * Looks up a key at an [FEP-ef61] compatible identifier. Such a key may be
1818
+ * a gateway key of a portable actor, which is accepted for some purposes but
1819
+ * not for others, so everything resolved from the document at the key ID is
1820
+ * cached in a namespace of the lookup's purpose, and one purpose never reuses
1821
+ * what another resolved, nor what another rejected. A failure to fetch
1822
+ * the document fails every purpose alike, so it is cached in the shared
1823
+ * namespace, as for any other key.
1824
+ *
1825
+ * [FEP-ef61]: https://w3id.org/fep/ef61
1826
+ */
1827
+ async function fetchCompatibleKey(cacheKey, cls, options, onCachedUnavailable, onFetchError, logger) {
1828
+ const keyId = cacheKey.href;
1829
+ const sharedCache = options.keyCache;
1830
+ const scopedCache = sharedCache?.compatibleKeyScope?.(getCompatibleKeyScope(cls, options));
1831
+ const cached = await getCachedFetchKey(cacheKey, keyId, cls, scopedCache, logger);
1832
+ if (cached != null) return {
1833
+ result: cached,
1834
+ outcome: { result: "hit" }
1835
+ };
1836
+ if (await sharedCache?.get(cacheKey) === null) {
1837
+ logger.debug("Entry {keyId} found in cache, but it could not be fetched.", { keyId });
1838
+ return {
1839
+ result: await onCachedUnavailable(cacheKey, keyId, sharedCache, logger),
1840
+ outcome: { result: "hit" }
1841
+ };
1842
+ }
1843
+ logger.debug("Fetching key {keyId} to verify signature...", { keyId });
1844
+ let document;
1845
+ let documentUrl;
1846
+ try {
1847
+ const remoteDocument = await (options.documentLoader ?? getDocumentLoader())(keyId);
1848
+ document = remoteDocument.document;
1849
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
1850
+ } catch (error) {
1851
+ const outcome = classifyFetchError(error);
1852
+ const result = await onFetchError(error, cacheKey, keyId, sharedCache, logger);
1853
+ await scopedCache?.delete(cacheKey);
1854
+ return {
1855
+ result,
1856
+ outcome
1857
+ };
1858
+ }
1859
+ const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
1860
+ ...options,
1861
+ keyCache: scopedCache
1862
+ }, logger);
1863
+ return {
1864
+ result: resolved,
1865
+ outcome: { result: resolved.key != null ? "fetched" : "invalid" }
1866
+ };
1867
+ }
1776
1868
  async function fetchKeyInternal(keyId, cls, options = {}) {
1777
1869
  return await fetchKeyWithResult(typeof keyId === "string" ? new URL(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
1778
1870
  return {
@@ -2289,10 +2381,7 @@ async function verifySignature(jsonLd, options = {}) {
2289
2381
  });
2290
2382
  const { key } = await measureSignatureKeyFetch(options.meterProvider, "linked_data", () => fetchKey(new URL(sig.creator), CryptographicKey, {
2291
2383
  ...options,
2292
- keyCache: {
2293
- get: () => Promise.resolve(void 0),
2294
- set: async (keyId, key) => await options.keyCache?.set(keyId, key)
2295
- }
2384
+ keyCache: bypassKeyCacheReads(options.keyCache)
2296
2385
  }));
2297
2386
  if (key == null) return null;
2298
2387
  return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, signature.slice(), messageBytes) ? key : null;
@@ -3338,38 +3427,42 @@ function equalStringSets(left, right) {
3338
3427
  }
3339
3428
  async function hasValidProofOptions(proofConfig, options, documentLoader) {
3340
3429
  const expires = await getProofOption(proofConfig, SECURITY_EXPIRATION, ["expires", SECURITY_EXPIRATION], documentLoader);
3341
- if (expires == null) return false;
3430
+ if (expires == null) return null;
3431
+ let expiration;
3342
3432
  if (expires.present) {
3343
- if (typeof expires.value !== "string") return false;
3344
- let expiration;
3433
+ if (typeof expires.value !== "string") return null;
3345
3434
  try {
3346
3435
  expiration = Temporal.Instant.from(expires.value);
3347
3436
  } catch {
3348
- return false;
3437
+ return null;
3349
3438
  }
3350
- if (Temporal.Instant.compare(Temporal.Now.instant(), expiration) >= 0) return false;
3439
+ if (Temporal.Instant.compare(Temporal.Now.instant(), expiration) >= 0) return null;
3351
3440
  }
3352
3441
  const domain = await getProofOption(proofConfig, SECURITY_DOMAIN, ["domain", SECURITY_DOMAIN], documentLoader);
3353
- if (domain == null) return false;
3442
+ if (domain == null) return null;
3354
3443
  const proofDomains = domain.present ? parseStringSet(domain.value) : null;
3355
- if (domain.present && proofDomains == null) return false;
3444
+ if (domain.present && proofDomains == null) return null;
3356
3445
  if (options.domain != null) {
3357
3446
  const expectedDomains = parseStringSet(options.domain);
3358
- if (expectedDomains == null || proofDomains == null || !equalStringSets(proofDomains, expectedDomains)) return false;
3447
+ if (expectedDomains == null || proofDomains == null || !equalStringSets(proofDomains, expectedDomains)) return null;
3359
3448
  }
3360
3449
  const challenge = await getProofOption(proofConfig, SECURITY_CHALLENGE, ["challenge", SECURITY_CHALLENGE], documentLoader);
3361
- if (challenge == null || challenge.present && typeof challenge.value !== "string" || options.challenge != null && (!challenge.present || challenge.value !== options.challenge)) return false;
3450
+ if (challenge == null || challenge.present && typeof challenge.value !== "string" || options.challenge != null && (!challenge.present || challenge.value !== options.challenge)) return null;
3362
3451
  const nonce = await getProofOption(proofConfig, SECURITY_NONCE, ["nonce", SECURITY_NONCE], documentLoader);
3363
- if (nonce == null || nonce.present && typeof nonce.value !== "string") return false;
3452
+ if (nonce == null || nonce.present && typeof nonce.value !== "string") return null;
3364
3453
  const previousProof = await getProofOption(proofConfig, SECURITY_PREVIOUS_PROOF, ["previousProof", SECURITY_PREVIOUS_PROOF], documentLoader);
3365
- if (previousProof == null || previousProof.present && typeof previousProof.value !== "string" && (!Array.isArray(previousProof.value) || previousProof.value.some((item) => typeof item !== "string"))) return false;
3366
- return true;
3454
+ if (previousProof == null || previousProof.present && typeof previousProof.value !== "string" && (!Array.isArray(previousProof.value) || previousProof.value.some((item) => typeof item !== "string"))) return null;
3455
+ return { expires: expiration };
3367
3456
  }
3368
3457
  async function verifyProofInternal(jsonLd, proof, options, messageDigestCache, rawProofCandidate, keyIdBoundByCaller = false) {
3369
3458
  if (!isJsonLdNode(jsonLd) || proof.cryptosuite !== "eddsa-jcs-2022" || proof.verificationMethodId == null || proof.proofPurpose !== "assertionMethod" || proof.proofValue == null || proof.created == null) return null;
3370
3459
  const proofContextLoader = messageDigestCache.proofContextLoader ?? preloadedOnlyDocumentLoader;
3371
3460
  const proofConfiguration = await createProofConfiguration(jsonLd, proof, options, proofContextLoader, rawProofCandidate);
3372
- if (proofConfiguration == null || !await hasValidProofOptions(proofConfiguration.value, options, proofContextLoader)) return null;
3461
+ if (proofConfiguration == null) return null;
3462
+ const validProofOptions = await hasValidProofOptions(proofConfiguration.value, options, proofContextLoader);
3463
+ if (validProofOptions == null) return null;
3464
+ const { expires } = validProofOptions;
3465
+ if (expires != null && (messageDigestCache.expires == null || Temporal.Instant.compare(expires, messageDigestCache.expires) < 0)) messageDigestCache.expires = expires;
3373
3466
  const publicKeyPromise = measureSignatureKeyFetch(options.meterProvider, "object_integrity", () => fetchKey(proof.verificationMethodId, Multikey, {
3374
3467
  ...options,
3375
3468
  keyIdBoundByCaller
@@ -3403,10 +3496,7 @@ async function verifyProofInternal(jsonLd, proof, options, messageDigestCache, r
3403
3496
  });
3404
3497
  return await verifyProofInternal(jsonLd, proof, {
3405
3498
  ...options,
3406
- keyCache: {
3407
- get: () => Promise.resolve(void 0),
3408
- set: async (keyId, key) => await options.keyCache?.set(keyId, key)
3409
- }
3499
+ keyCache: bypassKeyCacheReads(options.keyCache)
3410
3500
  }, messageDigestCache, rawProofCandidate, keyIdBoundByCaller);
3411
3501
  }
3412
3502
  logger$1.debug("The fetched key (verificationMethod) for the proof is not a valid Ed25519 key:\n{keyId}", {
@@ -3442,10 +3532,7 @@ async function verifyProofInternal(jsonLd, proof, options, messageDigestCache, r
3442
3532
  });
3443
3533
  return await verifyProofInternal(jsonLd, proof, {
3444
3534
  ...options,
3445
- keyCache: {
3446
- get: () => Promise.resolve(void 0),
3447
- set: async (keyId, key) => await options.keyCache?.set(keyId, key)
3448
- }
3535
+ keyCache: bypassKeyCacheReads(options.keyCache)
3449
3536
  }, messageDigestCache, rawProofCandidate, keyIdBoundByCaller);
3450
3537
  }
3451
3538
  logger$1.debug("Failed to verify the proof with the fetched key {keyId}:\n{proof}", {
@@ -3774,21 +3861,24 @@ async function verifyPortableObjectProofWithRoot(jsonLd, options = {}) {
3774
3861
  ...prepared.root == null ? {} : { root: prepared.root },
3775
3862
  ...prepared.objectType == null ? {} : { objectType: prepared.objectType }
3776
3863
  };
3777
- const { root, objectType } = prepared;
3864
+ const { root, objectType, proofContextLoader } = prepared;
3865
+ const messageDigestCache = { proofContextLoader };
3866
+ const result = await verifyPreparedPortableObjectProof(jsonLd, prepared, options, messageDigestCache);
3867
+ const { expires } = messageDigestCache;
3778
3868
  return {
3779
- result: await verifyPreparedPortableObjectProof(jsonLd, prepared, options),
3869
+ result,
3780
3870
  root,
3781
- objectType
3871
+ objectType,
3872
+ ...result.verified && expires != null ? { expires } : {}
3782
3873
  };
3783
3874
  }
3784
- async function verifyPreparedPortableObjectProof(jsonLd, prepared, options) {
3875
+ async function verifyPreparedPortableObjectProof(jsonLd, prepared, options, messageDigestCache) {
3785
3876
  const { proofs, rawProofValues, proofContextLoader } = prepared;
3786
3877
  const keys = [];
3787
3878
  const rawProofCandidatePool = {
3788
3879
  candidates: await parseRawProofCandidates(jsonLd, rawProofValues, options, proofContextLoader),
3789
3880
  used: /* @__PURE__ */ new Set()
3790
3881
  };
3791
- const messageDigestCache = { proofContextLoader };
3792
3882
  for (let proofIndex = 0; proofIndex < proofs.length; proofIndex++) {
3793
3883
  const rawProofCandidate = takeRawProofCandidate(rawProofCandidatePool, proofs[proofIndex]);
3794
3884
  if (rawProofCandidate == null) return {
@@ -3986,7 +4076,7 @@ async function verifyPortableGatewayKeyDocument(document, actor, keyId, options
3986
4076
  if (!(error instanceof TypeError)) throw error;
3987
4077
  return reject(`The actor document is malformed: ${error.message}`);
3988
4078
  }
3989
- const { result, root } = verification;
4079
+ const { result, root, expires } = verification;
3990
4080
  if (!result.verified) return reject("The actor document does not satisfy the FEP-ef61 proof policy: " + result.reason.type);
3991
4081
  if (root == null) return reject("The actor document could not be expanded.");
3992
4082
  const refuse = (url) => Promise.reject(/* @__PURE__ */ new Error(`Refusing to fetch ${url}.`));
@@ -4058,7 +4148,8 @@ async function verifyPortableGatewayKeyDocument(document, actor, keyId, options
4058
4148
  return {
4059
4149
  type: "verified",
4060
4150
  actor,
4061
- key
4151
+ key,
4152
+ ...expires == null ? {} : { expires }
4062
4153
  };
4063
4154
  }
4064
4155
  /**
@@ -4787,10 +4878,7 @@ async function verifyRequestDraft(request, span, metricsContext, { documentLoade
4787
4878
  contextLoader,
4788
4879
  timeWindow,
4789
4880
  currentTime,
4790
- keyCache: {
4791
- get: () => Promise.resolve(void 0),
4792
- set: async (keyId, key) => await keyCache?.set(keyId, key)
4793
- },
4881
+ keyCache: bypassKeyCacheReads(keyCache),
4794
4882
  meterProvider,
4795
4883
  tracerProvider
4796
4884
  });
@@ -5039,10 +5127,7 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
5039
5127
  contextLoader,
5040
5128
  timeWindow,
5041
5129
  currentTime,
5042
- keyCache: {
5043
- get: () => Promise.resolve(void 0),
5044
- set: async (keyId, key) => await keyCache?.set(keyId, key)
5045
- },
5130
+ keyCache: bypassKeyCacheReads(keyCache),
5046
5131
  spec: "rfc9421",
5047
5132
  meterProvider,
5048
5133
  tracerProvider
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { Actor, CryptographicKey, Multikey } from "@fedify/vocab";
3
- import { MeterProvider, TracerProvider } from "@opentelemetry/api";
4
3
  import { DocumentLoader } from "@fedify/vocab-runtime";
4
+ import { MeterProvider, TracerProvider } from "@opentelemetry/api";
5
5
 
6
6
  //#region src/sig/portable-key.d.ts
7
7
  /**
@@ -23,6 +23,11 @@ type PortableGatewayKeyResolution = {
23
23
  readonly key: CryptographicKey & {
24
24
  publicKey: CryptoKey;
25
25
  };
26
+ /**
27
+ * When the proof of the actor's document expires, if it does. The
28
+ * document does not vouch for the key after that.
29
+ */
30
+ readonly expires?: Temporal.Instant;
26
31
  };
27
32
  //#endregion
28
33
  //#region src/sig/key.d.ts
@@ -177,9 +182,9 @@ type FetchableKeyClass<T extends CryptographicKey | Multikey> = (new (...args: a
177
182
  * Gateway keys of [FEP-ef61] portable actors, i.e., keys whose IDs are
178
183
  * compatible identifiers that dereference to portable actor documents, are
179
184
  * not resolved by this function, as they only authenticate HTTP requests;
180
- * use `verifyRequest()` or `getKeyOwner()` for them instead. Keys
181
- * whose IDs are compatible identifiers are never read from or written to
182
- * the key cache.
185
+ * use `verifyRequest()` or `getKeyOwner()` for them instead. Keys whose IDs
186
+ * are compatible identifiers are never read from or written to the key
187
+ * cache under their IDs, except for failures to fetch them.
183
188
  *
184
189
  * [FEP-ef61]: https://w3id.org/fep/ef61
185
190
  * @template T The type of the key to fetch. Either {@link CryptographicKey}
@@ -199,9 +204,9 @@ declare function fetchKey<T extends CryptographicKey | Multikey>(keyId: URL | st
199
204
  * Gateway keys of [FEP-ef61] portable actors, i.e., keys whose IDs are
200
205
  * compatible identifiers that dereference to portable actor documents, are
201
206
  * not resolved by this function, as they only authenticate HTTP requests;
202
- * use `verifyRequest()` or `getKeyOwner()` for them instead. Keys
203
- * whose IDs are compatible identifiers are never read from or written to
204
- * the key cache.
207
+ * use `verifyRequest()` or `getKeyOwner()` for them instead. Keys whose IDs
208
+ * are compatible identifiers are never read from or written to the key
209
+ * cache under their IDs, except for failures to fetch them.
205
210
  *
206
211
  * [FEP-ef61]: https://w3id.org/fep/ef61
207
212
  *
@@ -1,9 +1,9 @@
1
1
  import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-CkjiYK_Z.mjs";
5
- import { _ as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-BCAa5-kE.mjs";
6
- import { a as isCompatibleKeyId, o as isPortableActorDocument, s as isPortableId } from "./portable-key-id-Bva5iT3W.mjs";
4
+ import { n as version, t as name } from "./deno-CCABdw7r.mjs";
5
+ import { _ as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-em_7lwhG.mjs";
6
+ import { a as isCompatibleKeyId, o as isPortableActorDocument, s as isPortableId } from "./portable-key-id-T7lecIbt.mjs";
7
7
  import { getLogger } from "@logtape/logtape";
8
8
  import { CryptographicKey, Multikey, Object as Object$1, isActor } from "@fedify/vocab";
9
9
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
@@ -210,6 +210,32 @@ async function verifyKeyOwnership(key, options = {}) {
210
210
  });
211
211
  return null;
212
212
  }
213
+ /**
214
+ * Wraps a key cache so that lookups through the wrapper miss, while
215
+ * whatever they resolve is still written to the cache. It is used to retry
216
+ * a verification that failed with a cached key with a freshly fetched one.
217
+ * @param keyCache The key cache to wrap.
218
+ * @returns The wrapped key cache.
219
+ * @internal
220
+ */
221
+ function bypassKeyCacheReads(keyCache) {
222
+ const cache = keyCache;
223
+ const bypassed = {
224
+ get: () => Promise.resolve(void 0),
225
+ set: async (keyId, key) => await cache?.set(keyId, key),
226
+ getFetchError: () => Promise.resolve(void 0),
227
+ setFetchError: async (keyId, error) => await cache?.setFetchError?.(keyId, error)
228
+ };
229
+ if (cache?.compatibleKeyScope != null) bypassed.compatibleKeyScope = (scope) => {
230
+ const scoped = cache.compatibleKeyScope(scope);
231
+ return {
232
+ get: () => Promise.resolve(void 0),
233
+ set: async (keyId, key, options) => await scoped.set(keyId, key, options),
234
+ delete: async (keyId) => await scoped.delete(keyId)
235
+ };
236
+ };
237
+ return bypassed;
238
+ }
213
239
  async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
214
240
  tracerProvider ??= trace.getTracerProvider();
215
241
  return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
@@ -246,9 +272,9 @@ async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
246
272
  * Gateway keys of [FEP-ef61] portable actors, i.e., keys whose IDs are
247
273
  * compatible identifiers that dereference to portable actor documents, are
248
274
  * not resolved by this function, as they only authenticate HTTP requests;
249
- * use `verifyRequest()` or `getKeyOwner()` for them instead. Keys
250
- * whose IDs are compatible identifiers are never read from or written to
251
- * the key cache.
275
+ * use `verifyRequest()` or `getKeyOwner()` for them instead. Keys whose IDs
276
+ * are compatible identifiers are never read from or written to the key
277
+ * cache under their IDs, except for failures to fetch them.
252
278
  *
253
279
  * [FEP-ef61]: https://w3id.org/fep/ef61
254
280
  * @template T The type of the key to fetch. Either {@link CryptographicKey}
@@ -271,9 +297,9 @@ function fetchKey(keyId, cls, options = {}) {
271
297
  * Gateway keys of [FEP-ef61] portable actors, i.e., keys whose IDs are
272
298
  * compatible identifiers that dereference to portable actor documents, are
273
299
  * not resolved by this function, as they only authenticate HTTP requests;
274
- * use `verifyRequest()` or `getKeyOwner()` for them instead. Keys
275
- * whose IDs are compatible identifiers are never read from or written to
276
- * the key cache.
300
+ * use `verifyRequest()` or `getKeyOwner()` for them instead. Keys whose IDs
301
+ * are compatible identifiers are never read from or written to the key
302
+ * cache under their IDs, except for failures to fetch them.
277
303
  *
278
304
  * [FEP-ef61]: https://w3id.org/fep/ef61
279
305
  *
@@ -456,11 +482,13 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
456
482
  }
457
483
  }
458
484
  if (isCompatibleKeyId(cacheKey) && isPortableActorDocument(object)) {
485
+ const scopedCache = keyCache;
459
486
  if (portableGatewayKeyResolver == null || cls !== CryptographicKey) {
460
487
  logger.debug("Failed to verify; key {keyId} is a gateway key of the portable actor {actorId}, which is only accepted for HTTP Signatures.", {
461
488
  keyId,
462
489
  actorId: object.id?.href
463
490
  });
491
+ await scopedCache?.set(cacheKey, null);
464
492
  return {
465
493
  key: null,
466
494
  cached: false
@@ -472,11 +500,13 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
472
500
  keyId,
473
501
  actorId: object.id?.href
474
502
  });
503
+ await scopedCache?.set(cacheKey, null);
475
504
  return {
476
505
  key: null,
477
506
  cached: false
478
507
  };
479
508
  }
509
+ await scopedCache?.set(cacheKey, resolution.key, { expires: resolution.expires });
480
510
  return {
481
511
  key: resolution.key,
482
512
  cached: false
@@ -605,8 +635,9 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
605
635
  }
606
636
  /**
607
637
  * Tells whether a key claims a portable actor as its owner. Such a key can
608
- * only be a gateway key, which is never cached, so a cached one was cached
609
- * by an older version that trusted web origins, and is fetched again.
638
+ * only be a gateway key, which is never cached under its ID, so a cached one
639
+ * was cached by an older version that trusted web origins, and is fetched
640
+ * again.
610
641
  */
611
642
  function claimsPortableOwner(key) {
612
643
  const ownerId = key instanceof CryptographicKey ? key.ownerId : key?.controllerId;
@@ -622,11 +653,12 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
622
653
  "key"
623
654
  ]);
624
655
  const keyId = cacheKey.href;
625
- if (isCompatibleKeyId(cacheKey)) options = {
626
- ...options,
627
- keyCache: void 0
628
- };
629
656
  const keyCache = options.keyCache;
657
+ if (isCompatibleKeyId(cacheKey)) {
658
+ const result = await fetchCompatibleKey(cacheKey, cls, options, onCachedUnavailable, onFetchError, logger);
659
+ outcome = result.outcome;
660
+ return result.result;
661
+ }
630
662
  const didKey = await resolveDidKey(cacheKey, cls, keyCache, logger);
631
663
  if (didKey != null) {
632
664
  outcome = { result: didKey.key == null ? "invalid" : didKey.cached ? "hit" : "fetched" };
@@ -668,6 +700,65 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
668
700
  });
669
701
  }
670
702
  }
703
+ /**
704
+ * Tells which purpose a lookup of a key at a compatible identifier serves.
705
+ */
706
+ function getCompatibleKeyScope(cls, options) {
707
+ if (cls !== CryptographicKey) return "multikey";
708
+ return options.portableGatewayKeyResolver == null ? "cryptographicKey" : "httpSignature";
709
+ }
710
+ /**
711
+ * Looks up a key at an [FEP-ef61] compatible identifier. Such a key may be
712
+ * a gateway key of a portable actor, which is accepted for some purposes but
713
+ * not for others, so everything resolved from the document at the key ID is
714
+ * cached in a namespace of the lookup's purpose, and one purpose never reuses
715
+ * what another resolved, nor what another rejected. A failure to fetch
716
+ * the document fails every purpose alike, so it is cached in the shared
717
+ * namespace, as for any other key.
718
+ *
719
+ * [FEP-ef61]: https://w3id.org/fep/ef61
720
+ */
721
+ async function fetchCompatibleKey(cacheKey, cls, options, onCachedUnavailable, onFetchError, logger) {
722
+ const keyId = cacheKey.href;
723
+ const sharedCache = options.keyCache;
724
+ const scopedCache = sharedCache?.compatibleKeyScope?.(getCompatibleKeyScope(cls, options));
725
+ const cached = await getCachedFetchKey(cacheKey, keyId, cls, scopedCache, logger);
726
+ if (cached != null) return {
727
+ result: cached,
728
+ outcome: { result: "hit" }
729
+ };
730
+ if (await sharedCache?.get(cacheKey) === null) {
731
+ logger.debug("Entry {keyId} found in cache, but it could not be fetched.", { keyId });
732
+ return {
733
+ result: await onCachedUnavailable(cacheKey, keyId, sharedCache, logger),
734
+ outcome: { result: "hit" }
735
+ };
736
+ }
737
+ logger.debug("Fetching key {keyId} to verify signature...", { keyId });
738
+ let document;
739
+ let documentUrl;
740
+ try {
741
+ const remoteDocument = await (options.documentLoader ?? getDocumentLoader())(keyId);
742
+ document = remoteDocument.document;
743
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
744
+ } catch (error) {
745
+ const outcome = classifyFetchError(error);
746
+ const result = await onFetchError(error, cacheKey, keyId, sharedCache, logger);
747
+ await scopedCache?.delete(cacheKey);
748
+ return {
749
+ result,
750
+ outcome
751
+ };
752
+ }
753
+ const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
754
+ ...options,
755
+ keyCache: scopedCache
756
+ }, logger);
757
+ return {
758
+ result: resolved,
759
+ outcome: { result: resolved.key != null ? "fetched" : "invalid" }
760
+ };
761
+ }
671
762
  async function fetchKeyInternal(keyId, cls, options = {}) {
672
763
  return await fetchKeyWithResult(typeof keyId === "string" ? new URL(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
673
764
  return {
@@ -692,4 +783,4 @@ async function fetchKeyInternal(keyId, cls, options = {}) {
692
783
  });
693
784
  }
694
785
  //#endregion
695
- export { generateCryptoKeyPair as a, verifyKeyOwnership as c, fetchKeyDetailed as i, fetchActorDocument as n, importJwk as o, fetchKey as r, validateCryptoKey as s, exportJwk as t };
786
+ export { fetchKeyDetailed as a, validateCryptoKey as c, fetchKey as i, verifyKeyOwnership as l, exportJwk as n, generateCryptoKeyPair as o, fetchActorDocument as r, importJwk as s, bypassKeyCacheReads as t };