@fedify/fedify 2.4.0-dev.2159 → 2.4.0-dev.2169

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 (96) hide show
  1. package/dist/{builder-D89oOfHC.mjs → builder-B2d_Pt0j.mjs} +2 -2
  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-B02Os8h2.mjs → compound-proof-D28gMFqP.mjs} +2 -2
  6. package/dist/{context-JDNkUtnu.d.ts → context-BykNUf04.d.ts} +1 -1
  7. package/dist/{context-DpsaHkk1.d.cts → context-L0Fnib9j.d.cts} +1 -1
  8. package/dist/{deno-CE_s0yQb.mjs → deno-DU8dvnWk.mjs} +1 -1
  9. package/dist/{docloader-C5p26tpa.mjs → docloader-CghLH7Zi.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 +5 -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 +7 -7
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +2 -2
  21. package/dist/federation/mod.d.ts +2 -2
  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-chain.test.mjs +3 -3
  26. package/dist/federation/portable-collection.test.mjs +4 -4
  27. package/dist/federation/portable-gateway-keys.test.mjs +180 -6
  28. package/dist/federation/portable-inbox.test.mjs +4 -4
  29. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  30. package/dist/federation/portable.test.mjs +3 -3
  31. package/dist/federation/send.test.mjs +3 -3
  32. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  33. package/dist/federation/tasks/tasks.test.mjs +2 -2
  34. package/dist/federation/temporal.test.mjs +1 -1
  35. package/dist/federation/webfinger.test.mjs +1 -1
  36. package/dist/{federation-dBOqmJcA.cjs → federation-DrNgkLfJ.cjs} +48 -24
  37. package/dist/{http-wdsia0-O.cjs → http-5CJBdU8n.cjs} +392 -65
  38. package/dist/{http-nTZuTacw.js → http-BGpexPO7.js} +368 -61
  39. package/dist/{http-DV_yW4dv.mjs → http-Bd6rZlJX.mjs} +7 -5
  40. package/dist/{http-C-x_OLSp.d.ts → http-Cok5-vIC.d.ts} +5 -0
  41. package/dist/{http-CjezPjFX.d.cts → http-DC1cqAQ2.d.cts} +5 -0
  42. package/dist/{key-C6jnekFI.mjs → key-gIhVZO3p.mjs} +58 -7
  43. package/dist/key-owner-evidence-Bo3yaeL5.mjs +108 -0
  44. package/dist/{keycache-BInaECGj.mjs → keycache-PiOM9pM0.mjs} +46 -21
  45. package/dist/{kv-cache-Bxm0a0Ww.mjs → kv-cache-D0qjSeWY.mjs} +1 -1
  46. package/dist/{kv-cache-BHNtQi9e.cjs → kv-cache-YRi_g8GB.cjs} +1 -1
  47. package/dist/{kv-cache-DhIl9YlV.js → kv-cache-iR55fqlk.js} +1 -1
  48. package/dist/{ld-ya2C1Xip.mjs → ld-BsTkRrwF.mjs} +3 -3
  49. package/dist/{metrics-ByOQOHHS.mjs → metrics-jUDFfVgi.mjs} +1 -1
  50. package/dist/{middleware-Bdwd_Gg8.mjs → middleware-Coqgp-K0.mjs} +15 -15
  51. package/dist/{middleware-WiaN4RS3.js → middleware-DlxMxVp5.js} +48 -24
  52. package/dist/{middleware-DoVWq8mE.mjs → middleware-Dq-OCmb6.mjs} +1 -1
  53. package/dist/{mod-Bz9tFXI2.d.ts → mod--jv6vyrP.d.ts} +1 -1
  54. package/dist/{mod-DaiBrWDW.d.ts → mod-B5z_q9TH.d.ts} +1 -1
  55. package/dist/{mod-Mt4aO3DX.d.cts → mod-BA_F2x53.d.cts} +1 -1
  56. package/dist/{mod-DSKkTwH2.d.cts → mod-BLOXGs6m.d.cts} +1 -1
  57. package/dist/{mod-S--JxYjo.d.cts → mod-CPPqkyV_.d.cts} +1 -1
  58. package/dist/{mod-_4K05HZF.d.ts → mod-DK0y9fd-.d.ts} +1 -1
  59. package/dist/mod.cjs +4 -4
  60. package/dist/mod.d.cts +5 -5
  61. package/dist/mod.d.ts +5 -5
  62. package/dist/mod.js +4 -4
  63. package/dist/nodeinfo/handler.test.mjs +1 -1
  64. package/dist/{owner-C6Nv5yyw.mjs → owner-D3GoetXS.mjs} +18 -3
  65. package/dist/{portable-collection-B-mU1F1-.js → portable-collection-Bkdh_WpL.js} +15 -1
  66. package/dist/{portable-collection-uWj3hfzv.mjs → portable-collection-C60_acmx.mjs} +2 -2
  67. package/dist/{portable-collection-C65SwL2l.cjs → portable-collection-CnlCamup.cjs} +15 -1
  68. package/dist/{portable-key-BXsVLSMa.mjs → portable-key-DyF5AnZo.mjs} +209 -55
  69. package/dist/{proof-BdO3BbLh.mjs → proof-CQAYqWdj.mjs} +4 -4
  70. package/dist/{send-BwANxryy.mjs → send-DjkQrxt5.mjs} +3 -3
  71. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  72. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  73. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  74. package/dist/sig/compound-proof.test.mjs +2 -2
  75. package/dist/sig/http.test.mjs +2 -2
  76. package/dist/sig/key-owner-evidence.test.d.mts +2 -0
  77. package/dist/sig/key-owner-evidence.test.mjs +100 -0
  78. package/dist/sig/key.test.mjs +1 -1
  79. package/dist/sig/ld.test.mjs +2 -2
  80. package/dist/sig/mod.cjs +2 -2
  81. package/dist/sig/mod.d.cts +2 -2
  82. package/dist/sig/mod.d.ts +2 -2
  83. package/dist/sig/mod.js +2 -2
  84. package/dist/sig/owner.test.mjs +1 -1
  85. package/dist/sig/portable-collection.test.mjs +1 -1
  86. package/dist/sig/portable-key.test.mjs +253 -7
  87. package/dist/sig/proof.test.mjs +3 -3
  88. package/dist/{temporal-CbPfRGZq.mjs → temporal-oRKP803M.mjs} +1 -1
  89. package/dist/testing/kv.test.mjs +1 -1
  90. package/dist/utils/docloader.test.mjs +2 -2
  91. package/dist/utils/kv-cache.test.mjs +1 -1
  92. package/dist/utils/mod.cjs +1 -1
  93. package/dist/utils/mod.d.cts +1 -1
  94. package/dist/utils/mod.d.ts +1 -1
  95. package/dist/utils/mod.js +1 -1
  96. 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.2159+bb072eda";
17
+ var version = "2.4.0-dev.2169+0a45df07";
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;
@@ -1181,6 +1181,108 @@ function getDurationMs(start) {
1181
1181
  return Math.max(0, performance.now() - start);
1182
1182
  }
1183
1183
  //#endregion
1184
+ //#region src/sig/key-owner-evidence.ts
1185
+ /**
1186
+ * How long a key of an [FEP-ef61] portable actor is trusted after its actor's
1187
+ * signed document vouched for it, however long the proof of the document is
1188
+ * valid. The actor can drop the key or the gateway from its document at any
1189
+ * time, so the key is looked up again at least this often.
1190
+ *
1191
+ * [FEP-ef61]: https://w3id.org/fep/ef61
1192
+ * @internal
1193
+ */
1194
+ const MAX_PORTABLE_KEY_TTL = Temporal.Duration.from({ hours: 1 });
1195
+ const candidates = /* @__PURE__ */ new WeakMap();
1196
+ const verified = /* @__PURE__ */ new WeakMap();
1197
+ /**
1198
+ * Attaches to a key that was just resolved the evidence of the portable actor
1199
+ * that owns it. The evidence is not used until the key verifies
1200
+ * a signature; see {@link promoteKeyOwnerEvidence}.
1201
+ *
1202
+ * Nothing is attached unless the document, the key's owner, and the actor
1203
+ * the key ID belongs to agree on the actor's canonical portable ID.
1204
+ * @param key The key, whose owner the document vouched for.
1205
+ * @param document The expanded root node of the actor's verified document.
1206
+ * @param expires When the evidence must not be used anymore.
1207
+ * @returns Whether the evidence was attached.
1208
+ * @internal
1209
+ */
1210
+ function attachKeyOwnerEvidence(key, document, expires) {
1211
+ if (!(key instanceof CryptographicKey) || key.id == null || key.ownerId == null || key.publicKey == null || document == null || typeof document !== "object" || Array.isArray(document)) return false;
1212
+ const rawId = document["@id"];
1213
+ if (typeof rawId !== "string") return false;
1214
+ const keyBase = new URL(key.id.href);
1215
+ keyBase.hash = "";
1216
+ const ownerId = getCanonicalPortableId(key.ownerId);
1217
+ if (ownerId == null || getRawCanonicalPortableId(rawId) !== ownerId || getCanonicalPortableId(keyBase) !== ownerId) return false;
1218
+ candidates.set(key, {
1219
+ keyId: key.id.href,
1220
+ keyOwner: key.ownerId.href,
1221
+ publicKey: key.publicKey,
1222
+ ownerId,
1223
+ document: structuredClone(document),
1224
+ expires
1225
+ });
1226
+ return true;
1227
+ }
1228
+ /**
1229
+ * Makes the evidence attached to a key usable, as the key has just verified
1230
+ * an HTTP Signature. Nothing happens if the key has no evidence.
1231
+ * @param key The key that verified a signature.
1232
+ * @internal
1233
+ */
1234
+ function promoteKeyOwnerEvidence(key) {
1235
+ const evidence = candidates.get(key);
1236
+ if (evidence == null) return;
1237
+ candidates.delete(key);
1238
+ verified.set(key, evidence);
1239
+ }
1240
+ /**
1241
+ * Gets the evidence of the portable actor that owns a key HTTP Signature
1242
+ * verification returned.
1243
+ * @param key The key.
1244
+ * @param now The current time.
1245
+ * @returns The evidence, or `undefined` if the key was not returned by HTTP
1246
+ * Signature verification, if it has no evidence, if the evidence
1247
+ * expired, or if the key has been changed since.
1248
+ * @internal
1249
+ */
1250
+ function getVerifiedKeyOwnerEvidence(key, now = Temporal.Now.instant()) {
1251
+ const evidence = verified.get(key);
1252
+ if (evidence == null) return void 0;
1253
+ if (Temporal.Instant.compare(now, evidence.expires) >= 0) {
1254
+ verified.delete(key);
1255
+ return;
1256
+ }
1257
+ if (key.id?.href !== evidence.keyId || key.ownerId?.href !== evidence.keyOwner || key.publicKey !== evidence.publicKey) return;
1258
+ return evidence;
1259
+ }
1260
+ /**
1261
+ * Parses the actor from the evidence, i.e., from the expansion of its
1262
+ * document whose proof was verified. Every call returns a new object.
1263
+ * @param evidence The evidence.
1264
+ * @param options Options for parsing the document.
1265
+ * @returns The actor, or `null` if the document is not the actor that
1266
+ * the evidence is about, or the evidence expired meanwhile.
1267
+ * @internal
1268
+ */
1269
+ async function parseKeyOwnerEvidence(evidence, options = {}) {
1270
+ const refuse = (url) => Promise.reject(/* @__PURE__ */ new Error(`Refusing to fetch ${url}.`));
1271
+ let actor;
1272
+ try {
1273
+ actor = await Object$1.fromJsonLd(structuredClone(evidence.document), {
1274
+ documentLoader: refuse,
1275
+ contextLoader: options.contextLoader,
1276
+ tracerProvider: options.tracerProvider
1277
+ });
1278
+ } catch (error) {
1279
+ if (!(error instanceof TypeError)) throw error;
1280
+ return null;
1281
+ }
1282
+ if (!isPortableActorDocument(actor) || getCanonicalPortableId(actor.id) !== evidence.ownerId || Temporal.Instant.compare(Temporal.Now.instant(), evidence.expires) >= 0) return null;
1283
+ return actor;
1284
+ }
1285
+ //#endregion
1184
1286
  //#region src/sig/key.ts
1185
1287
  /**
1186
1288
  * Checks if the given key is valid and supported. No-op if the key is valid,
@@ -1403,6 +1505,7 @@ function bypassKeyCacheReads(keyCache) {
1403
1505
  const scoped = cache.compatibleKeyScope(scope);
1404
1506
  return {
1405
1507
  get: () => Promise.resolve(void 0),
1508
+ getEntry: () => Promise.resolve(void 0),
1406
1509
  set: async (keyId, key, options) => await scoped.set(keyId, key, options),
1407
1510
  delete: async (keyId) => await scoped.delete(keyId)
1408
1511
  };
@@ -1570,6 +1673,49 @@ function checkCachedKeyHit(cachedKey, keyId, cls, logger) {
1570
1673
  }
1571
1674
  return null;
1572
1675
  }
1676
+ /**
1677
+ * Looks up a key at a compatible identifier, or at an `ap:` or `ap+ef61:`
1678
+ * URI, in the cache namespace of a purpose, like {@link getCachedFetchKey}.
1679
+ * A key that the verified document of its portable actor was cached with
1680
+ * gets the actor's evidence attached from the same entry; see
1681
+ * {@link attachKeyOwnerEvidence}.
1682
+ */
1683
+ async function getCachedCompatibleKey(cacheKey, keyId, cls, scopedCache, logger) {
1684
+ if (scopedCache?.getEntry == null) return await getCachedFetchKey(cacheKey, keyId, cls, scopedCache, logger);
1685
+ const entry = await scopedCache.getEntry(cacheKey);
1686
+ if (entry === void 0) return null;
1687
+ if (entry === null) {
1688
+ logger.debug("Entry {keyId} found in cache, but it is unavailable.", { keyId });
1689
+ return {
1690
+ key: null,
1691
+ cached: true
1692
+ };
1693
+ }
1694
+ const hit = checkCachedKeyHit(entry.key, keyId, cls, logger);
1695
+ if (hit?.key instanceof CryptographicKey && entry.owner != null) attachKeyOwnerEvidence(hit.key, entry.owner, entry.expires);
1696
+ return hit;
1697
+ }
1698
+ /**
1699
+ * Attaches the evidence of the portable actor whose document has just
1700
+ * vouched for a key to the key. The evidence expires with the cache entry
1701
+ * of the key, so that it is never trusted longer than the key is cached.
1702
+ * Without a cache, it expires with the proof of the document, but no later
1703
+ * than {@link MAX_PORTABLE_KEY_TTL}, as a cache entry would.
1704
+ * @param resolution The resolution of the key.
1705
+ * @param scopedCache The cache namespace the key was stored in, if any.
1706
+ * @param cached When the cache entry expires, if the cache stored one.
1707
+ */
1708
+ function attachResolvedKeyOwnerEvidence(resolution, scopedCache, cached) {
1709
+ let expires;
1710
+ if (scopedCache != null) {
1711
+ if (cached == null) return;
1712
+ expires = cached;
1713
+ } else {
1714
+ expires = Temporal.Now.instant().add(MAX_PORTABLE_KEY_TTL);
1715
+ if (resolution.expires != null && Temporal.Instant.compare(resolution.expires, expires) < 0) expires = resolution.expires;
1716
+ }
1717
+ attachKeyOwnerEvidence(resolution.key, resolution.document, expires);
1718
+ }
1573
1719
  async function clearFetchErrorMetadata(keyId, keyCache) {
1574
1720
  await keyCache?.setFetchError?.(keyId, null);
1575
1721
  }
@@ -1694,7 +1840,10 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
1694
1840
  cached: false
1695
1841
  };
1696
1842
  }
1697
- await scopedCache?.set(cacheKey, resolution.key, { expires: resolution.expires });
1843
+ attachResolvedKeyOwnerEvidence(resolution, scopedCache, await scopedCache?.set(cacheKey, resolution.key, {
1844
+ expires: resolution.expires,
1845
+ owner: resolution.document
1846
+ }));
1698
1847
  return {
1699
1848
  key: resolution.key,
1700
1849
  cached: false
@@ -1915,7 +2064,7 @@ async function fetchCompatibleKey(cacheKey, cls, options, onCachedUnavailable, o
1915
2064
  const keyId = cacheKey.href;
1916
2065
  const sharedCache = options.keyCache;
1917
2066
  const scopedCache = sharedCache?.compatibleKeyScope?.(getCompatibleKeyScope(cls, options));
1918
- const cached = await getCachedFetchKey(cacheKey, keyId, cls, scopedCache, logger);
2067
+ const cached = await getCachedCompatibleKey(cacheKey, keyId, cls, scopedCache, logger);
1919
2068
  if (cached != null) return {
1920
2069
  result: cached,
1921
2070
  outcome: { result: "hit" }
@@ -1979,7 +2128,7 @@ async function fetchPortableActorKey(cacheKey, cls, options, onCachedUnavailable
1979
2128
  }
1980
2129
  const sharedCache = options.keyCache;
1981
2130
  const scopedCache = sharedCache?.compatibleKeyScope?.("httpSignature");
1982
- const cached = await getCachedFetchKey(cacheKey, keyId, cls, scopedCache, logger);
2131
+ const cached = await getCachedCompatibleKey(cacheKey, keyId, cls, scopedCache, logger);
1983
2132
  if (cached != null) return {
1984
2133
  result: cached,
1985
2134
  outcome: { result: "hit" }
@@ -1994,7 +2143,10 @@ async function fetchPortableActorKey(cacheKey, cls, options, onCachedUnavailable
1994
2143
  logger.debug("Resolving key {keyId} to verify signature...", { keyId });
1995
2144
  const resolution = await resolvers.actorKey(cacheKey);
1996
2145
  if (resolution.type === "verified") {
1997
- await scopedCache?.set(cacheKey, resolution.key, { expires: resolution.expires });
2146
+ attachResolvedKeyOwnerEvidence(resolution, scopedCache, await scopedCache?.set(cacheKey, resolution.key, {
2147
+ expires: resolution.expires,
2148
+ owner: resolution.document
2149
+ }));
1998
2150
  return {
1999
2151
  result: {
2000
2152
  key: resolution.key,
@@ -4334,6 +4486,7 @@ async function verifyPortableKeyDocument(document, actor, keyId, isGatewayKey, o
4334
4486
  type: "verified",
4335
4487
  actor,
4336
4488
  key,
4489
+ document: root,
4337
4490
  ...expires == null ? {} : { expires }
4338
4491
  };
4339
4492
  }
@@ -4380,6 +4533,21 @@ async function fetchPortableGatewayKey(keyId, options = {}) {
4380
4533
  });
4381
4534
  }
4382
4535
  /**
4536
+ * The maximum number of `@gateway` location hints followed for one `ap:` or
4537
+ * `ap+ef61:` key ID. Whoever made a signature chooses the hints of its key
4538
+ * ID, so fewer of them are followed than when dereferencing objects, which
4539
+ * bounds the gateways one signature can make Fedify ask. Further hints are
4540
+ * ignored.
4541
+ */
4542
+ const MAX_KEY_ID_GATEWAY_HINTS = 3;
4543
+ /**
4544
+ * The default of {@link PortableActorKeyOptions.lookupTimeout}, in
4545
+ * milliseconds.
4546
+ */
4547
+ const DEFAULT_KEY_ID_LOOKUP_TIMEOUT = 1e4;
4548
+ /** The longest delay that `setTimeout()` supports, in milliseconds. */
4549
+ const MAX_TIMEOUT = 2 ** 31 - 1;
4550
+ /**
4383
4551
  * Resolves a key of a portable actor at an `ap:` or `ap+ef61:` key ID, e.g.,
4384
4552
  * `ap://did:key:z6Mk…/actor#main-key`. Such a key ID names no gateway, so
4385
4553
  * the actor's document, i.e., the key ID without its fragment, is fetched
@@ -4407,12 +4575,25 @@ async function fetchPortableGatewayKey(keyId, options = {}) {
4407
4575
  * unreachable gateway would have served, so the result is an `unavailable`
4408
4576
  * that is not cacheable.
4409
4577
  *
4578
+ * Only the first {@link MAX_KEY_ID_GATEWAY_HINTS} location hints are
4579
+ * followed, and all the gateways share one timeout, see
4580
+ * {@link PortableActorKeyOptions.lookupTimeout}. A gateway that does not
4581
+ * serve the document in time failed to serve it, like an unreachable one,
4582
+ * with an error that has no HTTP status. A gateway whose document could not
4583
+ * be processed in time, and the gateways left untried, make the result an
4584
+ * `unavailable` that is not cacheable. A result that arrives after
4585
+ * the timeout is ignored, even if it vouches for the key.
4586
+ *
4410
4587
  * @param keyId The key ID, which must be an `ap:` or `ap+ef61:` URI.
4411
- * @param options Loaders and telemetry providers.
4588
+ * @param options Loaders, telemetry providers, and the timeout.
4412
4589
  * @returns The resolution, which is never `legacy`.
4590
+ * @throws {RangeError} If the timeout is not a non-negative number of
4591
+ * milliseconds that `setTimeout()` supports.
4413
4592
  * @internal
4414
4593
  */
4415
4594
  async function resolvePortableActorKey(keyId, options = {}) {
4595
+ const lookupTimeout = options.lookupTimeout ?? DEFAULT_KEY_ID_LOOKUP_TIMEOUT;
4596
+ if (!Number.isFinite(lookupTimeout) || lookupTimeout < 0 || lookupTimeout > MAX_TIMEOUT) throw new RangeError(`The lookup timeout must be between 0 and ${MAX_TIMEOUT} ms: ${lookupTimeout}`);
4416
4597
  const documentLoader = options.documentLoader ?? getDocumentLoader();
4417
4598
  const contextLoader = options.contextLoader ?? getDocumentLoader();
4418
4599
  let formattedKeyId = keyId.href;
@@ -4431,75 +4612,67 @@ async function resolvePortableActorKey(keyId, options = {}) {
4431
4612
  if (keyId.hash === "" || keyId.hash === "#") return reject("The key ID has no fragment; only keys embedded in actor documents are supported.");
4432
4613
  const actorUrl = new URL(keyId.href);
4433
4614
  actorUrl.hash = "";
4615
+ const gateways = getPortableGatewayCandidates(actorUrl);
4616
+ if (gateways.length > MAX_KEY_ID_GATEWAY_HINTS) logger.debug("Following only the first {max} location hints of key {keyId}.", {
4617
+ keyId: formattedKeyId,
4618
+ max: MAX_KEY_ID_GATEWAY_HINTS
4619
+ });
4434
4620
  const requestUrls = [];
4435
- for (const gateway of getPortableGatewayCandidates(actorUrl)) try {
4621
+ for (const gateway of gateways.slice(0, MAX_KEY_ID_GATEWAY_HINTS)) try {
4436
4622
  requestUrls.push(toCompatibleEf61Id(actorUrl, gateway).href);
4437
4623
  } catch (error) {
4438
4624
  if (!(error instanceof TypeError)) throw error;
4439
4625
  return reject(`The key ID cannot be fetched from gateways: ${error}`);
4440
4626
  }
4441
4627
  if (requestUrls.length < 1) requestUrls.push(formatIri(actorUrl));
4628
+ const controller = new AbortController();
4629
+ const timeoutError = new DOMException(`Timed out looking up the actor of key ${formattedKeyId} after ${lookupTimeout} ms.`, "TimeoutError");
4630
+ let timer;
4631
+ if (lookupTimeout > 0) timer = setTimeout(() => controller.abort(timeoutError), lookupTimeout);
4632
+ else controller.abort(timeoutError);
4633
+ const { signal } = controller;
4634
+ const attemptOptions = {
4635
+ ...options,
4636
+ documentLoader: withDeadline(documentLoader, signal),
4637
+ contextLoader: withDeadline(contextLoader, signal),
4638
+ signal,
4639
+ formattedKeyId
4640
+ };
4442
4641
  const attempts = [];
4443
- for (const url of requestUrls) {
4444
- let remoteDocument;
4445
- try {
4446
- remoteDocument = await documentLoader(url);
4447
- } catch (error) {
4448
- logger.debug("Failed to fetch the actor of key {keyId} from {url}: {error}", {
4449
- keyId: formattedKeyId,
4450
- url,
4451
- error
4452
- });
4453
- attempts.push({
4454
- type: "retrievalFailed",
4455
- error
4456
- });
4457
- continue;
4458
- }
4459
- let resolution;
4460
- try {
4461
- const object = await Object$1.fromJsonLd(remoteDocument.document, {
4462
- documentLoader,
4463
- contextLoader,
4464
- tracerProvider: options.tracerProvider
4465
- });
4466
- resolution = isPortableActorDocument(object) ? await verifyPortableKeyDocument(remoteDocument.document, object, keyId, false, {
4467
- ...options,
4468
- documentLoader,
4469
- contextLoader
4470
- }) : {
4471
- type: "rejected",
4472
- reason: "The document is not a portable actor."
4473
- };
4474
- } catch (error) {
4475
- if (error instanceof TypeError) resolution = {
4476
- type: "rejected",
4477
- reason: `The document is malformed: ${error.message}`
4478
- };
4479
- else {
4480
- logger.debug("Failed to process the actor of key {keyId} served from {url}: {error}", {
4642
+ try {
4643
+ for (const url of requestUrls) {
4644
+ if (signal.aborted) {
4645
+ logger.debug("Ran out of time looking up the actor of key {keyId}; gateways from {url} on were not tried.", {
4481
4646
  keyId: formattedKeyId,
4482
- url,
4483
- error
4647
+ url
4484
4648
  });
4485
4649
  attempts.push({
4486
4650
  type: "indeterminate",
4487
- error
4651
+ error: signal.reason
4652
+ });
4653
+ break;
4654
+ }
4655
+ const state = { processing: false };
4656
+ let attempt;
4657
+ try {
4658
+ attempt = await raceAbort(attemptGateway(url, keyId, attemptOptions, state), signal);
4659
+ } catch (error) {
4660
+ if (!signal.aborted) throw error;
4661
+ logger.debug("Ran out of time looking up the actor of key {keyId} at {url}.", {
4662
+ keyId: formattedKeyId,
4663
+ url
4664
+ });
4665
+ attempts.push({
4666
+ type: state.processing ? "indeterminate" : "retrievalFailed",
4667
+ error: signal.reason
4488
4668
  });
4489
4669
  continue;
4490
4670
  }
4671
+ if (attempt.type === "verified") return attempt;
4672
+ attempts.push(attempt);
4491
4673
  }
4492
- if (resolution.type === "verified") return resolution;
4493
- const reason = resolution.type === "rejected" ? resolution.reason : "The document is not a portable actor.";
4494
- logger.debug("The actor document served from {url} does not vouch for key {keyId}: {reason}", {
4495
- keyId: formattedKeyId,
4496
- url,
4497
- reason
4498
- });
4499
- attempts.push({
4500
- type: "rejected",
4501
- reason
4502
- });
4674
+ } finally {
4675
+ clearTimeout(timer);
4503
4676
  }
4504
4677
  const rejections = attempts.flatMap((a) => a.type === "rejected" ? [a.reason] : []);
4505
4678
  if (rejections.length === attempts.length) return reject(rejections.join(" "));
@@ -4520,6 +4693,139 @@ async function resolvePortableActorKey(keyId, options = {}) {
4520
4693
  };
4521
4694
  }
4522
4695
  /**
4696
+ * Fetches the actor's document of a key at an `ap:` or `ap+ef61:` key ID from
4697
+ * one gateway, and tells whether it vouches for the key. It never rejects;
4698
+ * the caller races it against the lookup's timeout, and ignores what it
4699
+ * resolves to after the timeout.
4700
+ */
4701
+ async function attemptGateway(url, keyId, options, state) {
4702
+ const { documentLoader, contextLoader, signal, formattedKeyId } = options;
4703
+ let remoteDocument;
4704
+ try {
4705
+ remoteDocument = await documentLoader(url);
4706
+ } catch (error) {
4707
+ logger.debug("Failed to fetch the actor of key {keyId} from {url}: {error}", {
4708
+ keyId: formattedKeyId,
4709
+ url,
4710
+ error
4711
+ });
4712
+ return {
4713
+ type: "retrievalFailed",
4714
+ error
4715
+ };
4716
+ }
4717
+ if (signal.aborted) return {
4718
+ type: "indeterminate",
4719
+ error: signal.reason
4720
+ };
4721
+ state.processing = true;
4722
+ let resolution;
4723
+ try {
4724
+ const object = await Object$1.fromJsonLd(remoteDocument.document, {
4725
+ documentLoader,
4726
+ contextLoader,
4727
+ tracerProvider: options.tracerProvider
4728
+ });
4729
+ signal.throwIfAborted();
4730
+ resolution = isPortableActorDocument(object) ? await verifyPortableKeyDocument(remoteDocument.document, object, keyId, false, options) : {
4731
+ type: "rejected",
4732
+ reason: "The document is not a portable actor."
4733
+ };
4734
+ } catch (error) {
4735
+ if (error instanceof TypeError && !signal.aborted) resolution = {
4736
+ type: "rejected",
4737
+ reason: `The document is malformed: ${error.message}`
4738
+ };
4739
+ else {
4740
+ logger.debug("Failed to process the actor of key {keyId} served from {url}: {error}", {
4741
+ keyId: formattedKeyId,
4742
+ url,
4743
+ error
4744
+ });
4745
+ return {
4746
+ type: "indeterminate",
4747
+ error
4748
+ };
4749
+ }
4750
+ }
4751
+ if (resolution.type === "verified") return resolution;
4752
+ const reason = resolution.type === "rejected" ? resolution.reason : "The document is not a portable actor.";
4753
+ logger.debug("The actor document served from {url} does not vouch for key {keyId}: {reason}", {
4754
+ keyId: formattedKeyId,
4755
+ url,
4756
+ reason
4757
+ });
4758
+ return {
4759
+ type: "rejected",
4760
+ reason
4761
+ };
4762
+ }
4763
+ /**
4764
+ * Wraps a document loader so that it gives up when the given signal aborts,
4765
+ * as well as when the signal in its own options does. The wrapped loader is
4766
+ * passed a signal that aborts in either case, but in case it ignores that
4767
+ * signal, what it resolves to after the abort is ignored, and the wrapper
4768
+ * rejects with the reason of the abort at once. After the abort, the wrapped
4769
+ * loader is not called at all.
4770
+ */
4771
+ function withDeadline(loader, signal) {
4772
+ return (url, options) => {
4773
+ const combined = combineSignals(signal, options?.signal);
4774
+ return raceAbort(combined.signal.aborted ? Promise.reject(combined.signal.reason) : new Promise((resolve) => resolve(loader(url, {
4775
+ ...options,
4776
+ signal: combined.signal
4777
+ }))), combined.signal).finally(combined.dispose);
4778
+ };
4779
+ }
4780
+ /**
4781
+ * Makes a signal that aborts when either of the given signals does, with
4782
+ * the reason of the one that aborted first. `AbortSignal.any()` is not
4783
+ * available on every supported runtime, e.g., Bun before 1.1.4.
4784
+ * @returns The signal, and a function that stops listening to the given
4785
+ * signals.
4786
+ */
4787
+ function combineSignals(signal, other) {
4788
+ if (other == null || other === signal) return {
4789
+ signal,
4790
+ dispose() {}
4791
+ };
4792
+ const controller = new AbortController();
4793
+ const sources = [signal, other];
4794
+ const dispose = () => {
4795
+ for (const source of sources) source.removeEventListener("abort", onAbort);
4796
+ };
4797
+ function onAbort() {
4798
+ dispose();
4799
+ controller.abort(this.reason);
4800
+ }
4801
+ const aborted = sources.find((source) => source.aborted);
4802
+ if (aborted == null) for (const source of sources) source.addEventListener("abort", onAbort);
4803
+ else controller.abort(aborted.reason);
4804
+ return {
4805
+ signal: controller.signal,
4806
+ dispose
4807
+ };
4808
+ }
4809
+ /**
4810
+ * Waits for the given promise unless the given signal aborts first, in which
4811
+ * case it rejects with the reason of the abort. If the signal has aborted by
4812
+ * the time the promise settles, the abort wins as well, so that nothing that
4813
+ * settles after it counts.
4814
+ */
4815
+ function raceAbort(promise, signal) {
4816
+ return new Promise((resolve, reject) => {
4817
+ const onAbort = () => reject(signal.reason);
4818
+ const settle = (settled) => {
4819
+ signal.removeEventListener("abort", onAbort);
4820
+ if (signal.aborted) onAbort();
4821
+ else settled();
4822
+ };
4823
+ promise.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
4824
+ if (signal.aborted) onAbort();
4825
+ else signal.addEventListener("abort", onAbort, { once: true });
4826
+ });
4827
+ }
4828
+ /**
4523
4829
  * Finds the nodes that stand for the key with the given canonical portable
4524
4830
  * ID. A node whose ID is an `ap:` or `ap+ef61:` URI stands for the key
4525
4831
  * itself, e.g., Mitra lists the keys of portable actors that way while
@@ -5010,7 +5316,8 @@ async function verifyRequestDetailed(request, options = {}) {
5010
5316
  if (spec === "rfc9421") result = await verifyRequestRfc9421(request, span, metricsContext, options);
5011
5317
  else result = await verifyRequestDraft(request, span, metricsContext, options);
5012
5318
  recordVerificationResult(span, result);
5013
- if (!result.verified) span.setStatus({ code: SpanStatusCode.ERROR });
5319
+ if (result.verified) promoteKeyOwnerEvidence(result.key);
5320
+ else span.setStatus({ code: SpanStatusCode.ERROR });
5014
5321
  return result;
5015
5322
  } catch (error) {
5016
5323
  threw = true;
@@ -5721,4 +6028,4 @@ function timingSafeEqual(a, b) {
5721
6028
  return result === 0;
5722
6029
  }
5723
6030
  //#endregion
5724
- export { recordCollectionPageItems as $, hasSignatureLike as A, fetchKey as B, assertSafeJsonLd as C, isSamePublicKey as Ct, detachSignature as D, createSignature as E, verifyJsonLd as F, verifyKeyOwnership as G, generateCryptoKeyPair as H, verifySignature as I, getRemoteHost as J, getDurationMs as K, wrapContextLoaderForJsonLd as L, isInvalidUrlTypeError as M, signJsonLd as N, getNormalizationContextLoader as O, verifyCompactJsonLd as P, recordCollectionDispatchDuration as Q, exportJwk as R, InvalidContextReferenceError as S, isPortableUri as St, compactJsonLd as T, version as Tt, importJwk as U, fetchKeyDetailed as V, validateCryptoKey as W, isAbortError$1 as X, instrumentDocumentLoader as Y, recordCircuitBreakerStateChange as Z, verifyPortableObjectProof as _, getRawCanonicalPortableId as _t, signRequest as a, recordOutboxActivity as at, verifyProof as b, isPortableId as bt, fetchPortableGatewayKey as c, registerQueueDepthGauge as ct, classifyFep2277CoreType as d, fulfillAcceptSignature as dt, recordCollectionRequest as et, createProof as f, parseAcceptSignature as ft, verifyObject as g, getPortableDid as gt, verifyMapLocalProof as h, getGatewayKeyBase as ht, selectRequestSignature as i, recordInboxActivity as it, isClearlyMalformedContextReference as j, hasSignature as k, resolvePortableActorKey as l, preloadedOnlyDocumentLoader as lt, signObject as m, getCanonicalPortableId as mt, listRequestSignatures as n, recordDocumentCache as nt, verifyRequest as o, recordOutboxEnqueue as ot, hasProofLike as p, validateAcceptSignature as pt, getFederationMetrics as q, parseRfc9421SignatureInput as r, recordFanoutRecipients as rt, verifyRequestDetailed as s, recordWebFingerHandle as st, doubleKnock as t, recordCollectionTotalItems as tt, InvalidPortableObjectIdError as u, formatAcceptSignature as ut, verifyPortableObjectProofPolicy as v, hasPortableActor as vt, attachSignature as w, name as wt, normalizeOutgoingActivityJsonLd as x, isPortableKeyId as xt, verifyPortableObjectProofWithRoot as y, isCompatibleKeyId as yt, fetchActorDocument as z };
6031
+ export { isAbortError$1 as $, hasSignatureLike as A, fetchKey as B, assertSafeJsonLd as C, isPortableId as Ct, detachSignature as D, name as Dt, createSignature as E, isSamePublicKey as Et, verifyJsonLd as F, verifyKeyOwnership as G, generateCryptoKeyPair as H, verifySignature as I, parseKeyOwnerEvidence as J, MAX_PORTABLE_KEY_TTL as K, wrapContextLoaderForJsonLd as L, isInvalidUrlTypeError as M, signJsonLd as N, getNormalizationContextLoader as O, version as Ot, verifyCompactJsonLd as P, instrumentDocumentLoader as Q, exportJwk as R, InvalidContextReferenceError as S, isCompatibleKeyId as St, compactJsonLd as T, isPortableUri as Tt, importJwk as U, fetchKeyDetailed as V, validateCryptoKey as W, getFederationMetrics as X, getDurationMs as Y, getRemoteHost as Z, verifyPortableObjectProof as _, getCanonicalPortableId as _t, signRequest as a, recordDocumentCache as at, verifyProof as b, getRawCanonicalPortableId as bt, fetchPortableGatewayKey as c, recordOutboxActivity as ct, classifyFep2277CoreType as d, registerQueueDepthGauge as dt, recordCircuitBreakerStateChange as et, createProof as f, preloadedOnlyDocumentLoader as ft, verifyObject as g, validateAcceptSignature as gt, verifyMapLocalProof as h, parseAcceptSignature as ht, selectRequestSignature as i, recordCollectionTotalItems as it, isClearlyMalformedContextReference as j, hasSignature as k, resolvePortableActorKey as l, recordOutboxEnqueue as lt, signObject as m, fulfillAcceptSignature as mt, listRequestSignatures as n, recordCollectionPageItems as nt, verifyRequest as o, recordFanoutRecipients as ot, hasProofLike as p, formatAcceptSignature as pt, getVerifiedKeyOwnerEvidence as q, parseRfc9421SignatureInput as r, recordCollectionRequest as rt, verifyRequestDetailed as s, recordInboxActivity as st, doubleKnock as t, recordCollectionDispatchDuration as tt, InvalidPortableObjectIdError as u, recordWebFingerHandle as ut, verifyPortableObjectProofPolicy as v, getGatewayKeyBase as vt, attachSignature as w, isPortableKeyId as wt, normalizeOutgoingActivityJsonLd as x, hasPortableActor as xt, verifyPortableObjectProofWithRoot as y, getPortableDid as yt, fetchActorDocument as z };
@@ -1,12 +1,13 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-CE_s0yQb.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-ByOQOHHS.mjs";
4
+ import { n as version, t as name } from "./deno-DU8dvnWk.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-jUDFfVgi.mjs";
6
6
  import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-CPVqki78.mjs";
7
7
  import { f as parseKeyIdString } from "./portable-key-id-Bg259c4Z.mjs";
8
- import { a as fetchKeyDetailed, c as validateCryptoKey, t as bypassKeyCacheReads } from "./key-C6jnekFI.mjs";
9
- import { n as resolvePortableActorKey, r as verifyPortableGatewayKeyDocument } from "./portable-key-BXsVLSMa.mjs";
8
+ import { a as promoteKeyOwnerEvidence } from "./key-owner-evidence-Bo3yaeL5.mjs";
9
+ import { a as fetchKeyDetailed, c as validateCryptoKey, t as bypassKeyCacheReads } from "./key-gIhVZO3p.mjs";
10
+ import { n as resolvePortableActorKey, r as verifyPortableGatewayKeyDocument } from "./portable-key-DyF5AnZo.mjs";
10
11
  import { getLogger } from "@logtape/logtape";
11
12
  import { CryptographicKey } from "@fedify/vocab";
12
13
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -474,7 +475,8 @@ async function verifyRequestDetailed(request, options = {}) {
474
475
  if (spec === "rfc9421") result = await verifyRequestRfc9421(request, span, metricsContext, options);
475
476
  else result = await verifyRequestDraft(request, span, metricsContext, options);
476
477
  recordVerificationResult(span, result);
477
- if (!result.verified) span.setStatus({ code: SpanStatusCode.ERROR });
478
+ if (result.verified) promoteKeyOwnerEvidence(result.key);
479
+ else span.setStatus({ code: SpanStatusCode.ERROR });
478
480
  return result;
479
481
  } catch (error) {
480
482
  threw = true;
@@ -24,6 +24,11 @@ type PortableGatewayKeyResolution = {
24
24
  publicKey: CryptoKey;
25
25
  };
26
26
  /**
27
+ * The expanded root node of the actor's document whose proof was
28
+ * verified, from which {@link actor} was parsed.
29
+ */
30
+ readonly document: Record<string, unknown>;
31
+ /**
27
32
  * When the proof of the actor's document expires, if it does. The
28
33
  * document does not vouch for the key after that.
29
34
  */
@@ -24,6 +24,11 @@ type PortableGatewayKeyResolution = {
24
24
  publicKey: CryptoKey;
25
25
  };
26
26
  /**
27
+ * The expanded root node of the actor's document whose proof was
28
+ * verified, from which {@link actor} was parsed.
29
+ */
30
+ readonly document: Record<string, unknown>;
31
+ /**
27
32
  * When the proof of the actor's document expires, if it does. The
28
33
  * document does not vouch for the key after that.
29
34
  */