@fedify/fedify 2.4.0-dev.2122 → 2.4.0-dev.2129

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 (98) hide show
  1. package/dist/{builder-BCXywsZF.mjs → builder-C0bxZDL0.mjs} +3 -3
  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-Fxd45Oy5.mjs → compound-proof-D9Kljk_7.mjs} +3 -3
  6. package/dist/{context-tOiJfBoD.d.ts → context-Cz1w5Krc.d.ts} +31 -5
  7. package/dist/{context-CoNRxGYp.mjs → context-DfSVPcJ8.mjs} +6 -3
  8. package/dist/{context-SMNOiiY6.d.cts → context-Dvec_D_h.d.cts} +31 -5
  9. package/dist/{deno-BZrnW0Qz.mjs → deno-_o3lkDlG.mjs} +1 -1
  10. package/dist/{docloader-3BHaS20Z.mjs → docloader-BbS0rO-Y.mjs} +7 -7
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/compound-fanout.test.mjs +3 -3
  13. package/dist/federation/handler-compound-proof.test.mjs +4 -4
  14. package/dist/federation/handler.test.mjs +6 -6
  15. package/dist/federation/hashlink-media.test.mjs +2 -2
  16. package/dist/federation/idempotency.test.mjs +2 -2
  17. package/dist/federation/metrics.test.mjs +1 -1
  18. package/dist/federation/middleware.test.mjs +8 -8
  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 +6 -6
  24. package/dist/federation/portable-actor.test.mjs +3 -3
  25. package/dist/federation/portable-chain.test.d.mts +2 -0
  26. package/dist/federation/portable-chain.test.mjs +358 -0
  27. package/dist/federation/portable-gateway-keys.test.mjs +77 -6
  28. package/dist/federation/portable-inbox.test.mjs +9 -4
  29. package/dist/federation/portable.test.mjs +3 -3
  30. package/dist/federation/send.test.mjs +3 -3
  31. package/dist/federation/tasks/enqueue.test.mjs +2 -2
  32. package/dist/federation/tasks/tasks.test.mjs +3 -3
  33. package/dist/federation/temporal.test.mjs +1 -1
  34. package/dist/federation/webfinger.test.mjs +2 -2
  35. package/dist/{federation-Ml6NrgRV.cjs → federation-DRY8KrkB.cjs} +46 -22
  36. package/dist/{http-4bmgw-mA.cjs → http-B1J6E-bT.cjs} +384 -33
  37. package/dist/{http-B9BQDNIy.mjs → http-BpOiZE09.mjs} +18 -12
  38. package/dist/{http-D76Py0M4.d.cts → http-C-x_OLSp.d.ts} +57 -14
  39. package/dist/{http-BETASGl0.js → http-C2M6wcs8.js} +374 -35
  40. package/dist/{http-BrQgkJay.d.ts → http-CjezPjFX.d.cts} +57 -14
  41. package/dist/{key-PyYvun0F.mjs → key-DolJjJab.mjs} +103 -16
  42. package/dist/{kv-cache-CPqUxRL3.mjs → kv-cache-Bq2cah0c.mjs} +1 -1
  43. package/dist/{kv-cache-C4mv2ZFQ.js → kv-cache-D7TiyMxv.js} +6 -6
  44. package/dist/{kv-cache-D2Bz6t_c.cjs → kv-cache-D9nRbG2Q.cjs} +6 -6
  45. package/dist/{ld-u4JOD4iO.mjs → ld-Dwc176V4.mjs} +3 -3
  46. package/dist/{metrics-DCLoLvLz.mjs → metrics-CDaHfBXY.mjs} +1 -1
  47. package/dist/{middleware-B7bmtONC.mjs → middleware-CmB1FD8A.mjs} +1 -1
  48. package/dist/{middleware-DWlAGlyP.js → middleware-CylX-pvI.js} +52 -28
  49. package/dist/{middleware-CgOqRkK-.mjs → middleware-DN3MdTDg.mjs} +64 -39
  50. package/dist/{mod-CTh7ijx5.d.ts → mod-Bz9tFXI2.d.ts} +14 -3
  51. package/dist/{mod-Cwdt_CWU.d.ts → mod-CZetqT5h.d.ts} +1 -1
  52. package/dist/{mod-Cr79cQlc.d.cts → mod-D4g9ffEn.d.cts} +1 -1
  53. package/dist/{mod-DnElMslb.d.cts → mod-DGOIxEfb.d.cts} +1 -1
  54. package/dist/{mod-DMdGFMEl.d.cts → mod-S--JxYjo.d.cts} +14 -3
  55. package/dist/{mod-COWgSo2v.d.ts → mod-owwElvpw.d.ts} +1 -1
  56. package/dist/mod.cjs +8 -8
  57. package/dist/mod.d.cts +5 -5
  58. package/dist/mod.d.ts +5 -5
  59. package/dist/mod.js +5 -5
  60. package/dist/nodeinfo/handler.test.mjs +2 -2
  61. package/dist/{owner-CPFM-sqo.mjs → owner-BNHhwjNu.mjs} +37 -4
  62. package/dist/{sig-C1xOA4ml.js → portable-collection-CRpTUHCY.mjs} +5 -4
  63. package/dist/portable-collection-CgglAI9B.js +656 -0
  64. package/dist/{sig-Cb9VSeiF.cjs → portable-collection-PTGD0q70.cjs} +253 -2
  65. package/dist/portable-key-DqX2emHQ.mjs +395 -0
  66. package/dist/{portable-key-id-T7lecIbt.mjs → portable-key-id-Bg259c4Z.mjs} +69 -2
  67. package/dist/{proof-BCOg7Njv.mjs → proof-B4_0E-KU.mjs} +5 -5
  68. package/dist/{send-C8eOPat3.mjs → send-Dpu5qbP5.mjs} +3 -3
  69. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  70. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  71. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  72. package/dist/sig/compound-proof.test.mjs +2 -2
  73. package/dist/sig/http.test.mjs +2 -2
  74. package/dist/sig/key.test.mjs +1 -1
  75. package/dist/sig/ld.test.mjs +2 -2
  76. package/dist/sig/mod.cjs +5 -6
  77. package/dist/sig/mod.d.cts +2 -2
  78. package/dist/sig/mod.d.ts +2 -2
  79. package/dist/sig/mod.js +2 -3
  80. package/dist/sig/owner.test.mjs +1 -1
  81. package/dist/sig/portable-collection.test.mjs +20 -431
  82. package/dist/sig/portable-key.test.mjs +479 -5
  83. package/dist/sig/proof.test.mjs +3 -3
  84. package/dist/{temporal-vjHOhBpy.mjs → temporal-78JVR3GK.mjs} +1 -1
  85. package/dist/testing/kv.test.mjs +1 -1
  86. package/dist/testing/mod.d.mts +30 -4
  87. package/dist/testing/mod.mjs +2 -2
  88. package/dist/{testing-BWq1dCzx.mjs → testing-otQhu4aD.mjs} +1 -1
  89. package/dist/utils/docloader.test.mjs +81 -2
  90. package/dist/utils/kv-cache.test.mjs +1 -1
  91. package/dist/utils/mod.cjs +1 -1
  92. package/dist/utils/mod.d.cts +1 -1
  93. package/dist/utils/mod.d.ts +1 -1
  94. package/dist/utils/mod.js +1 -1
  95. package/package.json +7 -7
  96. package/dist/owner-DkA0Lz7v.cjs +0 -225
  97. package/dist/owner-wNinn7RK.js +0 -215
  98. package/dist/portable-key-DyylakVV.mjs +0 -214
@@ -4,7 +4,7 @@ import { getLogger } from "@logtape/logtape";
4
4
  import { Activity, CryptographicKey, DataIntegrityProof, Multikey, Object as Object$1, PUBLIC_COLLECTION, getTypeId, isActor } from "@fedify/vocab";
5
5
  import { SpanKind, SpanStatusCode, metrics, trace } from "@opentelemetry/api";
6
6
  import { FetchError, canonicalizePortableUri, encodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, isGatewayUrl, parseDidKeyVerificationMethod, parseIri, preloadedContexts, toCompatibleEf61Id } from "@fedify/vocab-runtime";
7
- import { isCompatibleEf61Iri } from "@fedify/vocab-runtime/internal/portable-dereference";
7
+ import { getPortableGatewayCandidates, isCompatibleEf61Iri } from "@fedify/vocab-runtime/internal/portable-dereference";
8
8
  import { encodeHex } from "byte-encodings/hex";
9
9
  import jsonld from "@fedify/vocab-runtime/jsonld";
10
10
  import { Item, decodeDict, encodeDict, encodeItem } from "structured-field-values";
@@ -14,10 +14,12 @@ 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.2122+bdc0ce8d";
17
+ var version = "2.4.0-dev.2129+c0ddb433";
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;
21
+ const RAW_PORTABLE_URI_PATTERN = /^ap(?:\+ef61)?:\/\//i;
22
+ const RAW_COMPATIBLE_ID_PREFIX_PATTERN = /^https?:\/\/[^/?#]*\/\.well-known\/apgateway\/(?=did(?::|%3A))/i;
21
23
  /**
22
24
  * Checks whether the URL is an `ap:` or `ap+ef61:` URI.
23
25
  * @internal
@@ -81,6 +83,20 @@ function isCompatibleKeyId(keyId) {
81
83
  return (keyId.protocol === "http:" || keyId.protocol === "https:") && COMPATIBLE_KEY_ID_PATH_PATTERN.test(keyId.pathname);
82
84
  }
83
85
  /**
86
+ * Checks whether a key ID may name a key that is valid only for HTTP
87
+ * Signatures: a gateway key at an [FEP-ef61] compatible identifier, or a key
88
+ * of a portable actor at an `ap:` or `ap+ef61:` URI. Neither ever makes or
89
+ * verifies an Object Integrity Proof or a Linked Data Signature: a portable
90
+ * object's proof is made by its DID, and a proof made with a gateway key
91
+ * would claim that the gateway authored the object.
92
+ *
93
+ * [FEP-ef61]: https://w3id.org/fep/ef61
94
+ * @internal
95
+ */
96
+ function isPortableKeyId(keyId) {
97
+ return isPortableUri(keyId) || isCompatibleKeyId(keyId);
98
+ }
99
+ /**
84
100
  * Gets the canonical portable ID of an actor ID that is either an `ap:` or
85
101
  * `ap+ef61:` URI or a compatible identifier.
86
102
  * @returns The canonical portable ID, or `null` if the ID is neither, or is
@@ -99,6 +115,57 @@ function getCanonicalPortableId(id) {
99
115
  }
100
116
  }
101
117
  /**
118
+ * Gets the canonical portable ID of an `ap:` or `ap+ef61:` URI or
119
+ * a compatible identifier as it is written, before any URL parsing.
120
+ * Parsing an ID into a `URL` resolves dot segments in its path, which would
121
+ * make it identify another portable object, e.g.,
122
+ * `ap://did:key:z6Mk…/x/../actor` would become `ap://did:key:z6Mk…/actor`.
123
+ * Such an ID has no canonical portable ID here.
124
+ * @param raw The ID as it is written.
125
+ * @returns The canonical portable ID, fragment included, or `null` if the ID
126
+ * is neither an `ap:` or `ap+ef61:` URI nor a compatible identifier,
127
+ * is malformed, or would identify another object once parsed.
128
+ * @internal
129
+ */
130
+ function getRawCanonicalPortableId(raw) {
131
+ let canonical;
132
+ let parsed;
133
+ try {
134
+ if (RAW_PORTABLE_URI_PATTERN.test(raw)) {
135
+ canonical = canonicalizePortableUri(raw);
136
+ parsed = canonicalizePortableUri(formatIri(parseIri(raw)));
137
+ } else {
138
+ const prefix = raw.match(RAW_COMPATIBLE_ID_PREFIX_PATTERN);
139
+ if (prefix == null || !URL.canParse(raw)) return null;
140
+ canonical = canonicalizePortableUri("ap://" + raw.slice(prefix[0].length));
141
+ parsed = getCanonicalPortableId(new URL(raw));
142
+ }
143
+ } catch (error) {
144
+ if (error instanceof TypeError) return null;
145
+ throw error;
146
+ }
147
+ return canonical === parsed ? canonical : null;
148
+ }
149
+ /**
150
+ * Parses a key ID, e.g., the `keyId` of an HTTP Signature, into a `URL`.
151
+ * An `ap:` or `ap+ef61:` key ID is parsed into the internal `URL` form of
152
+ * portable URIs. A portable or compatible key ID that is malformed, or
153
+ * whose identity URL parsing would change, e.g., by resolving dot segments in
154
+ * its path, is refused, so that it can never be taken for, or looked up in
155
+ * the key cache as, the key of another object.
156
+ * @param raw The key ID as it is written.
157
+ * @returns The parsed key ID, or `null` if it is invalid.
158
+ * @internal
159
+ */
160
+ function parseKeyIdString(raw) {
161
+ if (RAW_PORTABLE_URI_PATTERN.test(raw)) return getRawCanonicalPortableId(raw) == null ? null : parseIri(raw);
162
+ if (!URL.canParse(raw)) return null;
163
+ const url = new URL(raw);
164
+ if (isPortableUri(url)) return null;
165
+ if (!RAW_COMPATIBLE_ID_PREFIX_PATTERN.test(raw)) return isCompatibleKeyId(url) ? null : url;
166
+ return getRawCanonicalPortableId(raw) == null ? null : url;
167
+ }
168
+ /**
102
169
  * Builds the base of the key IDs of a gateway's keys for a portable actor:
103
170
  * the actor's compatible identifier on the gateway.
104
171
  * @param actorId The portable actor's ID, either an `ap:` or `ap+ef61:` URI
@@ -1342,6 +1409,17 @@ function bypassKeyCacheReads(keyCache) {
1342
1409
  };
1343
1410
  return bypassed;
1344
1411
  }
1412
+ /**
1413
+ * Parses a key ID given as a string. Unlike `new URL()`, it accepts `ap:`
1414
+ * and `ap+ef61:` URIs, and refuses a portable or compatible key ID whose
1415
+ * identity URL parsing would change, e.g., by resolving dot segments.
1416
+ * @throws {TypeError} If the key ID is invalid.
1417
+ */
1418
+ function parseKeyIdOrThrow(keyId) {
1419
+ const parsed = parseKeyIdString(keyId);
1420
+ if (parsed == null) throw new TypeError(`Invalid key ID: ${keyId}`);
1421
+ return parsed;
1422
+ }
1345
1423
  async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
1346
1424
  tracerProvider ??= trace.getTracerProvider();
1347
1425
  return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
@@ -1378,9 +1456,11 @@ async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
1378
1456
  * Gateway keys of [FEP-ef61] portable actors, i.e., keys whose IDs are
1379
1457
  * compatible identifiers that dereference to portable actor documents, are
1380
1458
  * not resolved by this function, as they only authenticate HTTP requests;
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.
1459
+ * use `verifyRequest()` or `getKeyOwner()` for them instead. Nor are keys
1460
+ * of portable actors whose IDs are `ap:` or `ap+ef61:` URIs, for the same
1461
+ * reason. Keys whose IDs are compatible identifiers or `ap:` or `ap+ef61:`
1462
+ * URIs are never read from or written to the key cache under their IDs,
1463
+ * except for failures to fetch them.
1384
1464
  *
1385
1465
  * [FEP-ef61]: https://w3id.org/fep/ef61
1386
1466
  * @template T The type of the key to fetch. Either {@link CryptographicKey}
@@ -1393,7 +1473,7 @@ async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
1393
1473
  * @since 1.3.0
1394
1474
  */
1395
1475
  function fetchKey(keyId, cls, options = {}) {
1396
- keyId = typeof keyId === "string" ? new URL(keyId) : keyId;
1476
+ keyId = typeof keyId === "string" ? parseKeyIdOrThrow(keyId) : keyId;
1397
1477
  return withFetchKeySpan(keyId, options.tracerProvider, () => fetchKeyInternal(keyId, cls, options));
1398
1478
  }
1399
1479
  /**
@@ -1403,9 +1483,11 @@ function fetchKey(keyId, cls, options = {}) {
1403
1483
  * Gateway keys of [FEP-ef61] portable actors, i.e., keys whose IDs are
1404
1484
  * compatible identifiers that dereference to portable actor documents, are
1405
1485
  * not resolved by this function, as they only authenticate HTTP requests;
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.
1486
+ * use `verifyRequest()` or `getKeyOwner()` for them instead. Nor are keys
1487
+ * of portable actors whose IDs are `ap:` or `ap+ef61:` URIs, for the same
1488
+ * reason. Keys whose IDs are compatible identifiers or `ap:` or `ap+ef61:`
1489
+ * URIs are never read from or written to the key cache under their IDs,
1490
+ * except for failures to fetch them.
1409
1491
  *
1410
1492
  * [FEP-ef61]: https://w3id.org/fep/ef61
1411
1493
  *
@@ -1419,7 +1501,7 @@ function fetchKey(keyId, cls, options = {}) {
1419
1501
  * @since 2.1.0
1420
1502
  */
1421
1503
  async function fetchKeyDetailed(keyId, cls, options = {}) {
1422
- const cacheKey = typeof keyId === "string" ? new URL(keyId) : keyId;
1504
+ const cacheKey = typeof keyId === "string" ? parseKeyIdOrThrow(keyId) : keyId;
1423
1505
  return await withFetchKeySpan(cacheKey, options.tracerProvider, async () => {
1424
1506
  return await fetchKeyWithResult(cacheKey, cls, options, async (cacheKey, keyId, keyCache, logger) => {
1425
1507
  const fetchError = await keyCache?.getFetchError?.(cacheKey);
@@ -1558,7 +1640,7 @@ async function resolveDidKey(cacheKey, cls, keyCache, logger) {
1558
1640
  cached: false
1559
1641
  };
1560
1642
  }
1561
- async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider, keyIdBoundByCaller, portableGatewayKeyResolver }, logger) {
1643
+ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider, keyIdBoundByCaller, portableKeyResolvers }, logger) {
1562
1644
  let object;
1563
1645
  try {
1564
1646
  object = await Object$1.fromJsonLd(document, {
@@ -1589,7 +1671,7 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
1589
1671
  }
1590
1672
  if (isCompatibleKeyId(cacheKey) && isPortableActorDocument(object)) {
1591
1673
  const scopedCache = keyCache;
1592
- if (portableGatewayKeyResolver == null || cls !== CryptographicKey) {
1674
+ if (portableKeyResolvers == null || cls !== CryptographicKey) {
1593
1675
  logger.debug("Failed to verify; key {keyId} is a gateway key of the portable actor {actorId}, which is only accepted for HTTP Signatures.", {
1594
1676
  keyId,
1595
1677
  actorId: object.id?.href
@@ -1600,7 +1682,7 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
1600
1682
  cached: false
1601
1683
  };
1602
1684
  }
1603
- const resolution = await portableGatewayKeyResolver(document, object, cacheKey);
1685
+ const resolution = await portableKeyResolvers.gatewayKey(document, object, cacheKey);
1604
1686
  if (resolution.type !== "verified") {
1605
1687
  logger.debug("Failed to verify; the portable actor {actorId} does not vouch for its gateway key {keyId}.", {
1606
1688
  keyId,
@@ -1760,6 +1842,11 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
1760
1842
  ]);
1761
1843
  const keyId = cacheKey.href;
1762
1844
  const keyCache = options.keyCache;
1845
+ if (isPortableUri(cacheKey)) {
1846
+ const result = await fetchPortableActorKey(cacheKey, cls, options, onCachedUnavailable, onFetchError, logger);
1847
+ outcome = result.outcome;
1848
+ return result.result;
1849
+ }
1763
1850
  if (isCompatibleKeyId(cacheKey)) {
1764
1851
  const result = await fetchCompatibleKey(cacheKey, cls, options, onCachedUnavailable, onFetchError, logger);
1765
1852
  outcome = result.outcome;
@@ -1811,7 +1898,7 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
1811
1898
  */
1812
1899
  function getCompatibleKeyScope(cls, options) {
1813
1900
  if (cls !== CryptographicKey) return "multikey";
1814
- return options.portableGatewayKeyResolver == null ? "cryptographicKey" : "httpSignature";
1901
+ return options.portableKeyResolvers == null ? "cryptographicKey" : "httpSignature";
1815
1902
  }
1816
1903
  /**
1817
1904
  * Looks up a key at an [FEP-ef61] compatible identifier. Such a key may be
@@ -1865,8 +1952,75 @@ async function fetchCompatibleKey(cacheKey, cls, options, onCachedUnavailable, o
1865
1952
  outcome: { result: resolved.key != null ? "fetched" : "invalid" }
1866
1953
  };
1867
1954
  }
1955
+ /**
1956
+ * Looks up a key of an [FEP-ef61] portable actor at an `ap:` or `ap+ef61:`
1957
+ * key ID. Such a key is accepted only for HTTP Signatures, and is rejected
1958
+ * for any other purpose without being fetched. Like keys at compatible
1959
+ * identifiers, what it resolves to is cached in the namespace of the lookup's
1960
+ * purpose, and a failure to fetch the actor's document from every gateway is
1961
+ * cached in the shared namespace. A failure that is neither, e.g., one
1962
+ * gateway being unreachable while another served an invalid document, is not
1963
+ * cached at all.
1964
+ *
1965
+ * [FEP-ef61]: https://w3id.org/fep/ef61
1966
+ */
1967
+ async function fetchPortableActorKey(cacheKey, cls, options, onCachedUnavailable, onFetchError, logger) {
1968
+ const keyId = cacheKey.href;
1969
+ const resolvers = options.portableKeyResolvers;
1970
+ if (resolvers == null || cls !== CryptographicKey) {
1971
+ logger.debug("Failed to verify; key {keyId} is a key of a portable actor, which is only accepted for HTTP Signatures.", { keyId });
1972
+ return {
1973
+ result: {
1974
+ key: null,
1975
+ cached: false
1976
+ },
1977
+ outcome: { result: "invalid" }
1978
+ };
1979
+ }
1980
+ const sharedCache = options.keyCache;
1981
+ const scopedCache = sharedCache?.compatibleKeyScope?.("httpSignature");
1982
+ const cached = await getCachedFetchKey(cacheKey, keyId, cls, scopedCache, logger);
1983
+ if (cached != null) return {
1984
+ result: cached,
1985
+ outcome: { result: "hit" }
1986
+ };
1987
+ if (await sharedCache?.get(cacheKey) === null) {
1988
+ logger.debug("Entry {keyId} found in cache, but it could not be fetched.", { keyId });
1989
+ return {
1990
+ result: await onCachedUnavailable(cacheKey, keyId, sharedCache, logger),
1991
+ outcome: { result: "hit" }
1992
+ };
1993
+ }
1994
+ logger.debug("Resolving key {keyId} to verify signature...", { keyId });
1995
+ const resolution = await resolvers.actorKey(cacheKey);
1996
+ if (resolution.type === "verified") {
1997
+ await scopedCache?.set(cacheKey, resolution.key, { expires: resolution.expires });
1998
+ return {
1999
+ result: {
2000
+ key: resolution.key,
2001
+ cached: false
2002
+ },
2003
+ outcome: { result: "fetched" }
2004
+ };
2005
+ }
2006
+ if (resolution.type === "rejected") {
2007
+ await scopedCache?.set(cacheKey, null);
2008
+ return {
2009
+ result: {
2010
+ key: null,
2011
+ cached: false
2012
+ },
2013
+ outcome: { result: "invalid" }
2014
+ };
2015
+ }
2016
+ await scopedCache?.delete(cacheKey);
2017
+ return {
2018
+ result: await onFetchError(resolution.error, cacheKey, keyId, resolution.cacheable ? sharedCache : void 0, logger),
2019
+ outcome: resolution.cacheable ? classifyFetchError(resolution.error) : { result: "error" }
2020
+ };
2021
+ }
1868
2022
  async function fetchKeyInternal(keyId, cls, options = {}) {
1869
- return await fetchKeyWithResult(typeof keyId === "string" ? new URL(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
2023
+ return await fetchKeyWithResult(typeof keyId === "string" ? parseKeyIdOrThrow(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
1870
2024
  return {
1871
2025
  key: null,
1872
2026
  cached: true
@@ -4034,6 +4188,14 @@ const SEC = "https://w3id.org/security#";
4034
4188
  * - no more than one entry of either property has that ID; and
4035
4189
  * - the key ID's origin is one of the actor's `gateways`.
4036
4190
  *
4191
+ * An entry has the key's ID if its ID is the key ID, or any compatible
4192
+ * identifier on the same gateway, or the `ap:` or `ap+ef61:` URI, with the
4193
+ * same canonical portable ID. Some publishers, e.g., Mitra, list the keys
4194
+ * of portable actors under `ap:` URIs, but sign requests with compatible key
4195
+ * IDs on their own gateways. IDs are compared as they are written in
4196
+ * the signed document, so an ID that URL parsing would turn into another,
4197
+ * e.g., by resolving dot segments, has no match.
4198
+ *
4037
4199
  * [FEP-8b32]: https://w3id.org/fep/8b32
4038
4200
  * [FEP-ef61]: https://w3id.org/fep/ef61
4039
4201
  * [FEP-521a]: https://w3id.org/fep/521a
@@ -4045,10 +4207,22 @@ const SEC = "https://w3id.org/security#";
4045
4207
  * @returns The verification result, which is never `legacy`.
4046
4208
  * @internal
4047
4209
  */
4048
- async function verifyPortableGatewayKeyDocument(document, actor, keyId, options = {}) {
4210
+ function verifyPortableGatewayKeyDocument(document, actor, keyId, options = {}) {
4211
+ return verifyPortableKeyDocument(document, actor, keyId, true, options);
4212
+ }
4213
+ /**
4214
+ * Verifies that the signed document of a portable actor vouches for the key
4215
+ * with the given ID, either a gateway key at a compatible key ID, or a key of
4216
+ * the actor itself at an `ap:` or `ap+ef61:` key ID. See
4217
+ * {@link verifyPortableGatewayKeyDocument} for the rules; a key of the actor
4218
+ * itself names no gateway, so instead of the key ID's origin being one of
4219
+ * the actor's `gateways`, the actor only has to have a valid gateway, as
4220
+ * FEP-ef61 requires of every portable actor.
4221
+ */
4222
+ async function verifyPortableKeyDocument(document, actor, keyId, isGatewayKey, options) {
4049
4223
  const reject = (reason) => {
4050
- logger.debug("Failed to verify gateway key {keyId} of the portable actor {actorId}: {reason}", {
4051
- keyId: keyId.href,
4224
+ logger.debug("Failed to verify key {keyId} of the portable actor {actorId}: {reason}", {
4225
+ keyId: formatIri(keyId),
4052
4226
  actorId: actor.id?.href,
4053
4227
  reason
4054
4228
  });
@@ -4060,8 +4234,9 @@ async function verifyPortableGatewayKeyDocument(document, actor, keyId, options
4060
4234
  const keyBase = new URL(keyId.href);
4061
4235
  keyBase.hash = "";
4062
4236
  const claimedActorId = getCanonicalPortableId(keyBase);
4237
+ const claimedKeyId = getCanonicalPortableId(keyId);
4063
4238
  const actorId = actor.id == null ? null : getCanonicalPortableId(actor.id);
4064
- if (claimedActorId == null || actorId == null) return reject("The key ID or the actor ID is not a valid portable ID.");
4239
+ if (claimedActorId == null || claimedKeyId == null || actorId == null) return reject("The key ID or the actor ID is not a valid portable ID.");
4065
4240
  if (claimedActorId !== actorId) return reject("The key ID is not a compatible identifier of the actor it dereferences to.");
4066
4241
  let verification;
4067
4242
  try {
@@ -4106,10 +4281,12 @@ async function verifyPortableGatewayKeyDocument(document, actor, keyId, options
4106
4281
  if (!(error instanceof TypeError)) throw error;
4107
4282
  return reject("The verified actor document is malformed.");
4108
4283
  }
4109
- if (!isPortableActorDocument(verifiedActor) || getCanonicalPortableId(verifiedActor.id) !== claimedActorId) return reject("The key ID is not a compatible identifier of the actor whose document is signed.");
4284
+ const rootId = root["@id"];
4285
+ if (!isPortableActorDocument(verifiedActor) || getCanonicalPortableId(verifiedActor.id) !== claimedActorId || typeof rootId !== "string" || getRawCanonicalPortableId(rootId) !== claimedActorId) return reject("The key ID is not a compatible identifier of the actor whose document is signed.");
4110
4286
  actor = verifiedActor;
4111
- const assertionNodes = findNodes(root[`${SEC}assertionMethod`], keyId);
4112
- const publicKeyNodes = findNodes(root[`${SEC}publicKey`], keyId);
4287
+ const findKeyNodes = (values) => findNodes(values, claimedKeyId, isGatewayKey ? keyId.origin : null);
4288
+ const assertionNodes = findKeyNodes(root[`${SEC}assertionMethod`]);
4289
+ const publicKeyNodes = findKeyNodes(root[`${SEC}publicKey`]);
4113
4290
  if (assertionNodes.length > 1 || publicKeyNodes.length > 1) return reject("The actor document has more than one key with the ID.");
4114
4291
  let publicKey;
4115
4292
  if (assertionNodes.length > 0) {
@@ -4139,7 +4316,9 @@ async function verifyPortableGatewayKeyDocument(document, actor, keyId, options
4139
4316
  if (embedded.ownerId == null || getCanonicalPortableId(embedded.ownerId) !== actorId) return reject("The owner of the key is not the actor.");
4140
4317
  publicKey = embedded.publicKey;
4141
4318
  }
4142
- if (!actor.gateways.some((gateway) => isGatewayUrl(gateway) && gateway.origin === keyId.origin)) return reject(`The gateway ${keyId.origin} is not listed in the actor's gateways.`);
4319
+ if (isGatewayKey) {
4320
+ if (!actor.gateways.some((gateway) => isGatewayUrl(gateway) && gateway.origin === keyId.origin)) return reject(`The gateway ${keyId.origin} is not listed in the actor's gateways.`);
4321
+ } else if (!actor.gateways.some(isGatewayUrl)) return reject("The actor has no valid gateways.");
4143
4322
  const key = new CryptographicKey({
4144
4323
  id: keyId,
4145
4324
  owner: actor.id,
@@ -4194,15 +4373,170 @@ async function fetchPortableGatewayKey(keyId, options = {}) {
4194
4373
  contextLoader
4195
4374
  });
4196
4375
  }
4197
- function findNodes(values, id) {
4376
+ /**
4377
+ * Resolves a key of a portable actor at an `ap:` or `ap+ef61:` key ID, e.g.,
4378
+ * `ap://did:key:z6Mk…/actor#main-key`. Such a key ID names no gateway, so
4379
+ * the actor's document, i.e., the key ID without its fragment, is fetched
4380
+ * through the gateways in its `@gateway` location hints, one after another,
4381
+ * until one of them serves a document that vouches for the key. Without
4382
+ * location hints, the document loader is asked for the `ap:` URI itself,
4383
+ * which only a custom document loader can resolve. The hints are chosen by
4384
+ * whoever made the signature, but that does not matter: only a document with
4385
+ * a valid proof by the DID of the key ID vouches for the key.
4386
+ *
4387
+ * The key is taken as a key of the actor itself, not of a gateway, so it is
4388
+ * accepted by the rules of {@link verifyPortableGatewayKeyDocument}, except
4389
+ * that it is listed under the `ap:` or `ap+ef61:` URI with the same canonical
4390
+ * ID as the key ID, and that the actor only has to have a valid gateway.
4391
+ * The key ID must have a fragment; a key served as a standalone document is
4392
+ * not supported.
4393
+ *
4394
+ * If no gateway serves a document that vouches for the key, the result is
4395
+ * `rejected` only if every gateway served a document that definitely does not
4396
+ * vouch for it. If every gateway failed to serve a document at all, the
4397
+ * result is a cacheable `unavailable`, with the error of the first gateway if
4398
+ * every gateway responded with the same HTTP status, so that the status
4399
+ * describes the whole lookup. Otherwise, e.g., if one gateway was
4400
+ * unreachable and another served an invalid document, nothing tells what the
4401
+ * unreachable gateway would have served, so the result is an `unavailable`
4402
+ * that is not cacheable.
4403
+ *
4404
+ * @param keyId The key ID, which must be an `ap:` or `ap+ef61:` URI.
4405
+ * @param options Loaders and telemetry providers.
4406
+ * @returns The resolution, which is never `legacy`.
4407
+ * @internal
4408
+ */
4409
+ async function resolvePortableActorKey(keyId, options = {}) {
4410
+ const documentLoader = options.documentLoader ?? getDocumentLoader();
4411
+ const contextLoader = options.contextLoader ?? getDocumentLoader();
4412
+ let formattedKeyId = keyId.href;
4413
+ const reject = (reason) => {
4414
+ logger.debug("Failed to verify key {keyId} of a portable actor: {reason}", {
4415
+ keyId: formattedKeyId,
4416
+ reason
4417
+ });
4418
+ return {
4419
+ type: "rejected",
4420
+ reason
4421
+ };
4422
+ };
4423
+ if (!isPortableUri(keyId) || getCanonicalPortableId(keyId) == null) return reject("The key ID is not a valid ap: or ap+ef61: URI.");
4424
+ formattedKeyId = formatIri(keyId);
4425
+ if (keyId.hash === "" || keyId.hash === "#") return reject("The key ID has no fragment; only keys embedded in actor documents are supported.");
4426
+ const actorUrl = new URL(keyId.href);
4427
+ actorUrl.hash = "";
4428
+ const requestUrls = [];
4429
+ for (const gateway of getPortableGatewayCandidates(actorUrl)) try {
4430
+ requestUrls.push(toCompatibleEf61Id(actorUrl, gateway).href);
4431
+ } catch (error) {
4432
+ if (!(error instanceof TypeError)) throw error;
4433
+ return reject(`The key ID cannot be fetched from gateways: ${error}`);
4434
+ }
4435
+ if (requestUrls.length < 1) requestUrls.push(formatIri(actorUrl));
4436
+ const attempts = [];
4437
+ for (const url of requestUrls) {
4438
+ let remoteDocument;
4439
+ try {
4440
+ remoteDocument = await documentLoader(url);
4441
+ } catch (error) {
4442
+ logger.debug("Failed to fetch the actor of key {keyId} from {url}: {error}", {
4443
+ keyId: formattedKeyId,
4444
+ url,
4445
+ error
4446
+ });
4447
+ attempts.push({
4448
+ type: "retrievalFailed",
4449
+ error
4450
+ });
4451
+ continue;
4452
+ }
4453
+ let resolution;
4454
+ try {
4455
+ const object = await Object$1.fromJsonLd(remoteDocument.document, {
4456
+ documentLoader,
4457
+ contextLoader,
4458
+ tracerProvider: options.tracerProvider
4459
+ });
4460
+ resolution = isPortableActorDocument(object) ? await verifyPortableKeyDocument(remoteDocument.document, object, keyId, false, {
4461
+ ...options,
4462
+ documentLoader,
4463
+ contextLoader
4464
+ }) : {
4465
+ type: "rejected",
4466
+ reason: "The document is not a portable actor."
4467
+ };
4468
+ } catch (error) {
4469
+ if (error instanceof TypeError) resolution = {
4470
+ type: "rejected",
4471
+ reason: `The document is malformed: ${error.message}`
4472
+ };
4473
+ else {
4474
+ logger.debug("Failed to process the actor of key {keyId} served from {url}: {error}", {
4475
+ keyId: formattedKeyId,
4476
+ url,
4477
+ error
4478
+ });
4479
+ attempts.push({
4480
+ type: "indeterminate",
4481
+ error
4482
+ });
4483
+ continue;
4484
+ }
4485
+ }
4486
+ if (resolution.type === "verified") return resolution;
4487
+ const reason = resolution.type === "rejected" ? resolution.reason : "The document is not a portable actor.";
4488
+ logger.debug("The actor document served from {url} does not vouch for key {keyId}: {reason}", {
4489
+ keyId: formattedKeyId,
4490
+ url,
4491
+ reason
4492
+ });
4493
+ attempts.push({
4494
+ type: "rejected",
4495
+ reason
4496
+ });
4497
+ }
4498
+ const rejections = attempts.flatMap((a) => a.type === "rejected" ? [a.reason] : []);
4499
+ if (rejections.length === attempts.length) return reject(rejections.join(" "));
4500
+ const errors = attempts.flatMap((a) => a.type === "rejected" ? [] : [a.error]);
4501
+ const aggregate = errors.length === 1 ? errors[0] : new AggregateError(errors, `Failed to fetch the actor of key ${formattedKeyId} from any of its gateways: ${requestUrls.join(", ")}`);
4502
+ if (attempts.every((a) => a.type === "retrievalFailed")) {
4503
+ const statuses = errors.map((error) => error instanceof FetchError ? error.response?.status : void 0);
4504
+ return {
4505
+ type: "unavailable",
4506
+ error: statuses[0] != null && statuses.every((status) => status === statuses[0]) ? errors[0] : aggregate,
4507
+ cacheable: true
4508
+ };
4509
+ }
4510
+ return {
4511
+ type: "unavailable",
4512
+ error: new Error(`Could not determine whether the actor of key ${formattedKeyId} vouches for it.`, { cause: aggregate }),
4513
+ cacheable: false
4514
+ };
4515
+ }
4516
+ /**
4517
+ * Finds the nodes that stand for the key with the given canonical portable
4518
+ * ID. A node whose ID is an `ap:` or `ap+ef61:` URI stands for the key
4519
+ * itself, e.g., Mitra lists the keys of portable actors that way while
4520
+ * signing requests with compatible key IDs. A node whose ID is a compatible
4521
+ * identifier stands for a gateway's key, so it matches only a compatible key
4522
+ * ID on the same gateway; a key ID that names no gateway never selects
4523
+ * a gateway's key, even with the same fragment.
4524
+ * @param values The expanded values of a key property.
4525
+ * @param canonicalKeyId The canonical portable ID of the key ID.
4526
+ * @param gateway The origin of the gateway whose key the key ID names, or
4527
+ * `null` if the key ID names no gateway.
4528
+ */
4529
+ function findNodes(values, canonicalKeyId, gateway) {
4198
4530
  if (!Array.isArray(values)) return [];
4199
4531
  const nodes = [];
4200
4532
  for (const value of values) {
4201
4533
  if (value == null || typeof value !== "object" || Array.isArray(value)) continue;
4202
4534
  const node = value;
4203
4535
  const nodeId = node["@id"];
4204
- if (typeof nodeId !== "string" || !URL.canParse(nodeId)) continue;
4205
- if (new URL(nodeId).href === id.href) nodes.push(node);
4536
+ if (typeof nodeId !== "string") continue;
4537
+ if (getRawCanonicalPortableId(nodeId) !== canonicalKeyId) continue;
4538
+ const parsed = parseIri(nodeId);
4539
+ if (isPortableUri(parsed) || parsed.origin === gateway) nodes.push(node);
4206
4540
  }
4207
4541
  return nodes;
4208
4542
  }
@@ -4580,13 +4914,18 @@ function keyFetchErrorResult(keyId, result) {
4580
4914
  }
4581
4915
  };
4582
4916
  }
4917
+ /**
4918
+ * Creates the resolvers of keys of FEP-ef61 portable actors, which HTTP
4919
+ * Signature verification alone passes to key lookups.
4920
+ */
4921
+ function createPortableKeyResolvers(options) {
4922
+ return {
4923
+ gatewayKey: (document, actor, keyId) => verifyPortableGatewayKeyDocument(document, actor, keyId, options),
4924
+ actorKey: (keyId) => resolvePortableActorKey(keyId, options)
4925
+ };
4926
+ }
4583
4927
  function parseKeyId(value) {
4584
- if (value == null) return null;
4585
- try {
4586
- return new URL(value);
4587
- } catch {
4588
- return null;
4589
- }
4928
+ return value == null ? null : parseKeyIdString(value);
4590
4929
  }
4591
4930
  function getKeyFetchErrorName(error) {
4592
4931
  return error.name || error.constructor.name || "Error";
@@ -4844,7 +5183,7 @@ async function verifyRequestDraft(request, span, metricsContext, { documentLoade
4844
5183
  keyCache,
4845
5184
  tracerProvider,
4846
5185
  meterProvider,
4847
- portableGatewayKeyResolver: (document, actor, keyId) => verifyPortableGatewayKeyDocument(document, actor, keyId, {
5186
+ portableKeyResolvers: createPortableKeyResolvers({
4848
5187
  documentLoader,
4849
5188
  contextLoader,
4850
5189
  keyCache,
@@ -5064,7 +5403,7 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
5064
5403
  keyCache,
5065
5404
  tracerProvider,
5066
5405
  meterProvider,
5067
- portableGatewayKeyResolver: (document, actor, gatewayKeyId) => verifyPortableGatewayKeyDocument(document, actor, gatewayKeyId, {
5406
+ portableKeyResolvers: createPortableKeyResolvers({
5068
5407
  documentLoader,
5069
5408
  contextLoader,
5070
5409
  keyCache,
@@ -5372,4 +5711,4 @@ function timingSafeEqual(a, b) {
5372
5711
  return result === 0;
5373
5712
  }
5374
5713
  //#endregion
5375
- export { recordCollectionTotalItems as $, isInvalidUrlTypeError as A, generateCryptoKeyPair as B, compactJsonLd as C, hasSignature as D, getNormalizationContextLoader as E, wrapContextLoaderForJsonLd as F, getFederationMetrics as G, validateCryptoKey as H, exportJwk as I, isAbortError$1 as J, getRemoteHost as K, fetchActorDocument as L, verifyCompactJsonLd as M, verifyJsonLd as N, hasSignatureLike as O, verifySignature as P, recordCollectionRequest as Q, fetchKey as R, attachSignature as S, detachSignature as T, verifyKeyOwnership as U, importJwk as V, getDurationMs as W, recordCollectionDispatchDuration as X, recordCircuitBreakerStateChange as Y, recordCollectionPageItems as Z, verifyPortableObjectProofWithRoot as _, isPortableId as _t, signRequest as a, recordWebFingerHandle as at, InvalidContextReferenceError as b, name as bt, fetchPortableGatewayKey as c, formatAcceptSignature as ct, hasProofLike as d, validateAcceptSignature as dt, recordDocumentCache as et, signObject as f, getCanonicalPortableId as ft, verifyPortableObjectProofPolicy as g, isCompatibleKeyId as gt, verifyPortableObjectProof as h, hasPortableActor as ht, selectRequestSignature as i, recordOutboxEnqueue as it, signJsonLd as j, isClearlyMalformedContextReference as k, InvalidPortableObjectIdError as l, fulfillAcceptSignature as lt, verifyObject as m, getPortableDid as mt, listRequestSignatures as n, recordInboxActivity as nt, verifyRequest as o, registerQueueDepthGauge as ot, verifyMapLocalProof as p, getGatewayKeyBase as pt, instrumentDocumentLoader as q, parseRfc9421SignatureInput as r, recordOutboxActivity as rt, verifyRequestDetailed as s, preloadedOnlyDocumentLoader as st, doubleKnock as t, recordFanoutRecipients as tt, createProof as u, parseAcceptSignature as ut, verifyProof as v, isPortableUri as vt, createSignature as w, assertSafeJsonLd as x, version as xt, normalizeOutgoingActivityJsonLd as y, isSamePublicKey as yt, fetchKeyDetailed as z };
5714
+ export { recordCollectionRequest as $, isClearlyMalformedContextReference as A, fetchKeyDetailed as B, attachSignature as C, version as Ct, getNormalizationContextLoader as D, detachSignature as E, verifySignature as F, getDurationMs as G, importJwk as H, wrapContextLoaderForJsonLd as I, instrumentDocumentLoader as J, getFederationMetrics as K, exportJwk as L, signJsonLd as M, verifyCompactJsonLd as N, hasSignature as O, verifyJsonLd as P, recordCollectionPageItems as Q, fetchActorDocument as R, assertSafeJsonLd as S, name as St, createSignature as T, validateCryptoKey as U, generateCryptoKeyPair as V, verifyKeyOwnership as W, recordCircuitBreakerStateChange as X, isAbortError$1 as Y, recordCollectionDispatchDuration as Z, verifyPortableObjectProofPolicy as _, isCompatibleKeyId as _t, signRequest as a, recordOutboxEnqueue as at, normalizeOutgoingActivityJsonLd as b, isPortableUri as bt, fetchPortableGatewayKey as c, preloadedOnlyDocumentLoader as ct, createProof as d, parseAcceptSignature as dt, recordCollectionTotalItems as et, hasProofLike as f, validateAcceptSignature as ft, verifyPortableObjectProof as g, hasPortableActor as gt, verifyObject as h, getPortableDid as ht, selectRequestSignature as i, recordOutboxActivity as it, isInvalidUrlTypeError as j, hasSignatureLike as k, resolvePortableActorKey as l, formatAcceptSignature as lt, verifyMapLocalProof as m, getGatewayKeyBase as mt, listRequestSignatures as n, recordFanoutRecipients as nt, verifyRequest as o, recordWebFingerHandle as ot, signObject as p, getCanonicalPortableId as pt, getRemoteHost as q, parseRfc9421SignatureInput as r, recordInboxActivity as rt, verifyRequestDetailed as s, registerQueueDepthGauge as st, doubleKnock as t, recordDocumentCache as tt, InvalidPortableObjectIdError as u, fulfillAcceptSignature as ut, verifyPortableObjectProofWithRoot as v, isPortableId as vt, compactJsonLd as w, InvalidContextReferenceError as x, isSamePublicKey as xt, verifyProof as y, isPortableKeyId as yt, fetchKey as z };