@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
@@ -16,10 +16,12 @@ json_canon = require_rolldown_runtime.__toESM(json_canon, 1);
16
16
  let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
17
17
  //#region deno.json
18
18
  var name = "@fedify/fedify";
19
- var version = "2.4.0-dev.2122+bdc0ce8d";
19
+ var version = "2.4.0-dev.2129+c0ddb433";
20
20
  //#endregion
21
21
  //#region src/sig/portable-key-id.ts
22
22
  const COMPATIBLE_KEY_ID_PATH_PATTERN = /^\/\.well-known\/apgateway\/did(?::|%3A)/i;
23
+ const RAW_PORTABLE_URI_PATTERN = /^ap(?:\+ef61)?:\/\//i;
24
+ const RAW_COMPATIBLE_ID_PREFIX_PATTERN = /^https?:\/\/[^/?#]*\/\.well-known\/apgateway\/(?=did(?::|%3A))/i;
23
25
  /**
24
26
  * Checks whether the URL is an `ap:` or `ap+ef61:` URI.
25
27
  * @internal
@@ -83,6 +85,20 @@ function isCompatibleKeyId(keyId) {
83
85
  return (keyId.protocol === "http:" || keyId.protocol === "https:") && COMPATIBLE_KEY_ID_PATH_PATTERN.test(keyId.pathname);
84
86
  }
85
87
  /**
88
+ * Checks whether a key ID may name a key that is valid only for HTTP
89
+ * Signatures: a gateway key at an [FEP-ef61] compatible identifier, or a key
90
+ * of a portable actor at an `ap:` or `ap+ef61:` URI. Neither ever makes or
91
+ * verifies an Object Integrity Proof or a Linked Data Signature: a portable
92
+ * object's proof is made by its DID, and a proof made with a gateway key
93
+ * would claim that the gateway authored the object.
94
+ *
95
+ * [FEP-ef61]: https://w3id.org/fep/ef61
96
+ * @internal
97
+ */
98
+ function isPortableKeyId(keyId) {
99
+ return isPortableUri(keyId) || isCompatibleKeyId(keyId);
100
+ }
101
+ /**
86
102
  * Gets the canonical portable ID of an actor ID that is either an `ap:` or
87
103
  * `ap+ef61:` URI or a compatible identifier.
88
104
  * @returns The canonical portable ID, or `null` if the ID is neither, or is
@@ -101,6 +117,57 @@ function getCanonicalPortableId(id) {
101
117
  }
102
118
  }
103
119
  /**
120
+ * Gets the canonical portable ID of an `ap:` or `ap+ef61:` URI or
121
+ * a compatible identifier as it is written, before any URL parsing.
122
+ * Parsing an ID into a `URL` resolves dot segments in its path, which would
123
+ * make it identify another portable object, e.g.,
124
+ * `ap://did:key:z6Mk…/x/../actor` would become `ap://did:key:z6Mk…/actor`.
125
+ * Such an ID has no canonical portable ID here.
126
+ * @param raw The ID as it is written.
127
+ * @returns The canonical portable ID, fragment included, or `null` if the ID
128
+ * is neither an `ap:` or `ap+ef61:` URI nor a compatible identifier,
129
+ * is malformed, or would identify another object once parsed.
130
+ * @internal
131
+ */
132
+ function getRawCanonicalPortableId(raw) {
133
+ let canonical;
134
+ let parsed;
135
+ try {
136
+ if (RAW_PORTABLE_URI_PATTERN.test(raw)) {
137
+ canonical = (0, _fedify_vocab_runtime.canonicalizePortableUri)(raw);
138
+ parsed = (0, _fedify_vocab_runtime.canonicalizePortableUri)((0, _fedify_vocab_runtime.formatIri)((0, _fedify_vocab_runtime.parseIri)(raw)));
139
+ } else {
140
+ const prefix = raw.match(RAW_COMPATIBLE_ID_PREFIX_PATTERN);
141
+ if (prefix == null || !URL.canParse(raw)) return null;
142
+ canonical = (0, _fedify_vocab_runtime.canonicalizePortableUri)("ap://" + raw.slice(prefix[0].length));
143
+ parsed = getCanonicalPortableId(new URL(raw));
144
+ }
145
+ } catch (error) {
146
+ if (error instanceof TypeError) return null;
147
+ throw error;
148
+ }
149
+ return canonical === parsed ? canonical : null;
150
+ }
151
+ /**
152
+ * Parses a key ID, e.g., the `keyId` of an HTTP Signature, into a `URL`.
153
+ * An `ap:` or `ap+ef61:` key ID is parsed into the internal `URL` form of
154
+ * portable URIs. A portable or compatible key ID that is malformed, or
155
+ * whose identity URL parsing would change, e.g., by resolving dot segments in
156
+ * its path, is refused, so that it can never be taken for, or looked up in
157
+ * the key cache as, the key of another object.
158
+ * @param raw The key ID as it is written.
159
+ * @returns The parsed key ID, or `null` if it is invalid.
160
+ * @internal
161
+ */
162
+ function parseKeyIdString(raw) {
163
+ if (RAW_PORTABLE_URI_PATTERN.test(raw)) return getRawCanonicalPortableId(raw) == null ? null : (0, _fedify_vocab_runtime.parseIri)(raw);
164
+ if (!URL.canParse(raw)) return null;
165
+ const url = new URL(raw);
166
+ if (isPortableUri(url)) return null;
167
+ if (!RAW_COMPATIBLE_ID_PREFIX_PATTERN.test(raw)) return isCompatibleKeyId(url) ? null : url;
168
+ return getRawCanonicalPortableId(raw) == null ? null : url;
169
+ }
170
+ /**
104
171
  * Builds the base of the key IDs of a gateway's keys for a portable actor:
105
172
  * the actor's compatible identifier on the gateway.
106
173
  * @param actorId The portable actor's ID, either an `ap:` or `ap+ef61:` URI
@@ -4908,6 +4975,17 @@ function bypassKeyCacheReads(keyCache) {
4908
4975
  };
4909
4976
  return bypassed;
4910
4977
  }
4978
+ /**
4979
+ * Parses a key ID given as a string. Unlike `new URL()`, it accepts `ap:`
4980
+ * and `ap+ef61:` URIs, and refuses a portable or compatible key ID whose
4981
+ * identity URL parsing would change, e.g., by resolving dot segments.
4982
+ * @throws {TypeError} If the key ID is invalid.
4983
+ */
4984
+ function parseKeyIdOrThrow(keyId) {
4985
+ const parsed = parseKeyIdString(keyId);
4986
+ if (parsed == null) throw new TypeError(`Invalid key ID: ${keyId}`);
4987
+ return parsed;
4988
+ }
4911
4989
  async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
4912
4990
  tracerProvider ??= _opentelemetry_api.trace.getTracerProvider();
4913
4991
  return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
@@ -4944,9 +5022,11 @@ async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
4944
5022
  * Gateway keys of [FEP-ef61] portable actors, i.e., keys whose IDs are
4945
5023
  * compatible identifiers that dereference to portable actor documents, are
4946
5024
  * not resolved by this function, as they only authenticate HTTP requests;
4947
- * use `verifyRequest()` or `getKeyOwner()` for them instead. Keys whose IDs
4948
- * are compatible identifiers are never read from or written to the key
4949
- * cache under their IDs, except for failures to fetch them.
5025
+ * use `verifyRequest()` or `getKeyOwner()` for them instead. Nor are keys
5026
+ * of portable actors whose IDs are `ap:` or `ap+ef61:` URIs, for the same
5027
+ * reason. Keys whose IDs are compatible identifiers or `ap:` or `ap+ef61:`
5028
+ * URIs are never read from or written to the key cache under their IDs,
5029
+ * except for failures to fetch them.
4950
5030
  *
4951
5031
  * [FEP-ef61]: https://w3id.org/fep/ef61
4952
5032
  * @template T The type of the key to fetch. Either {@link CryptographicKey}
@@ -4959,7 +5039,7 @@ async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
4959
5039
  * @since 1.3.0
4960
5040
  */
4961
5041
  function fetchKey(keyId, cls, options = {}) {
4962
- keyId = typeof keyId === "string" ? new URL(keyId) : keyId;
5042
+ keyId = typeof keyId === "string" ? parseKeyIdOrThrow(keyId) : keyId;
4963
5043
  return withFetchKeySpan(keyId, options.tracerProvider, () => fetchKeyInternal(keyId, cls, options));
4964
5044
  }
4965
5045
  /**
@@ -4969,9 +5049,11 @@ function fetchKey(keyId, cls, options = {}) {
4969
5049
  * Gateway keys of [FEP-ef61] portable actors, i.e., keys whose IDs are
4970
5050
  * compatible identifiers that dereference to portable actor documents, are
4971
5051
  * not resolved by this function, as they only authenticate HTTP requests;
4972
- * use `verifyRequest()` or `getKeyOwner()` for them instead. Keys whose IDs
4973
- * are compatible identifiers are never read from or written to the key
4974
- * cache under their IDs, except for failures to fetch them.
5052
+ * use `verifyRequest()` or `getKeyOwner()` for them instead. Nor are keys
5053
+ * of portable actors whose IDs are `ap:` or `ap+ef61:` URIs, for the same
5054
+ * reason. Keys whose IDs are compatible identifiers or `ap:` or `ap+ef61:`
5055
+ * URIs are never read from or written to the key cache under their IDs,
5056
+ * except for failures to fetch them.
4975
5057
  *
4976
5058
  * [FEP-ef61]: https://w3id.org/fep/ef61
4977
5059
  *
@@ -4985,7 +5067,7 @@ function fetchKey(keyId, cls, options = {}) {
4985
5067
  * @since 2.1.0
4986
5068
  */
4987
5069
  async function fetchKeyDetailed(keyId, cls, options = {}) {
4988
- const cacheKey = typeof keyId === "string" ? new URL(keyId) : keyId;
5070
+ const cacheKey = typeof keyId === "string" ? parseKeyIdOrThrow(keyId) : keyId;
4989
5071
  return await withFetchKeySpan(cacheKey, options.tracerProvider, async () => {
4990
5072
  return await fetchKeyWithResult(cacheKey, cls, options, async (cacheKey, keyId, keyCache, logger) => {
4991
5073
  const fetchError = await keyCache?.getFetchError?.(cacheKey);
@@ -5124,7 +5206,7 @@ async function resolveDidKey(cacheKey, cls, keyCache, logger) {
5124
5206
  cached: false
5125
5207
  };
5126
5208
  }
5127
- async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider, keyIdBoundByCaller, portableGatewayKeyResolver }, logger) {
5209
+ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider, keyIdBoundByCaller, portableKeyResolvers }, logger) {
5128
5210
  let object;
5129
5211
  try {
5130
5212
  object = await _fedify_vocab.Object.fromJsonLd(document, {
@@ -5155,7 +5237,7 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
5155
5237
  }
5156
5238
  if (isCompatibleKeyId(cacheKey) && isPortableActorDocument(object)) {
5157
5239
  const scopedCache = keyCache;
5158
- if (portableGatewayKeyResolver == null || cls !== _fedify_vocab.CryptographicKey) {
5240
+ if (portableKeyResolvers == null || cls !== _fedify_vocab.CryptographicKey) {
5159
5241
  logger.debug("Failed to verify; key {keyId} is a gateway key of the portable actor {actorId}, which is only accepted for HTTP Signatures.", {
5160
5242
  keyId,
5161
5243
  actorId: object.id?.href
@@ -5166,7 +5248,7 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
5166
5248
  cached: false
5167
5249
  };
5168
5250
  }
5169
- const resolution = await portableGatewayKeyResolver(document, object, cacheKey);
5251
+ const resolution = await portableKeyResolvers.gatewayKey(document, object, cacheKey);
5170
5252
  if (resolution.type !== "verified") {
5171
5253
  logger.debug("Failed to verify; the portable actor {actorId} does not vouch for its gateway key {keyId}.", {
5172
5254
  keyId,
@@ -5326,6 +5408,11 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
5326
5408
  ]);
5327
5409
  const keyId = cacheKey.href;
5328
5410
  const keyCache = options.keyCache;
5411
+ if (isPortableUri(cacheKey)) {
5412
+ const result = await fetchPortableActorKey(cacheKey, cls, options, onCachedUnavailable, onFetchError, logger);
5413
+ outcome = result.outcome;
5414
+ return result.result;
5415
+ }
5329
5416
  if (isCompatibleKeyId(cacheKey)) {
5330
5417
  const result = await fetchCompatibleKey(cacheKey, cls, options, onCachedUnavailable, onFetchError, logger);
5331
5418
  outcome = result.outcome;
@@ -5377,7 +5464,7 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
5377
5464
  */
5378
5465
  function getCompatibleKeyScope(cls, options) {
5379
5466
  if (cls !== _fedify_vocab.CryptographicKey) return "multikey";
5380
- return options.portableGatewayKeyResolver == null ? "cryptographicKey" : "httpSignature";
5467
+ return options.portableKeyResolvers == null ? "cryptographicKey" : "httpSignature";
5381
5468
  }
5382
5469
  /**
5383
5470
  * Looks up a key at an [FEP-ef61] compatible identifier. Such a key may be
@@ -5431,8 +5518,75 @@ async function fetchCompatibleKey(cacheKey, cls, options, onCachedUnavailable, o
5431
5518
  outcome: { result: resolved.key != null ? "fetched" : "invalid" }
5432
5519
  };
5433
5520
  }
5521
+ /**
5522
+ * Looks up a key of an [FEP-ef61] portable actor at an `ap:` or `ap+ef61:`
5523
+ * key ID. Such a key is accepted only for HTTP Signatures, and is rejected
5524
+ * for any other purpose without being fetched. Like keys at compatible
5525
+ * identifiers, what it resolves to is cached in the namespace of the lookup's
5526
+ * purpose, and a failure to fetch the actor's document from every gateway is
5527
+ * cached in the shared namespace. A failure that is neither, e.g., one
5528
+ * gateway being unreachable while another served an invalid document, is not
5529
+ * cached at all.
5530
+ *
5531
+ * [FEP-ef61]: https://w3id.org/fep/ef61
5532
+ */
5533
+ async function fetchPortableActorKey(cacheKey, cls, options, onCachedUnavailable, onFetchError, logger) {
5534
+ const keyId = cacheKey.href;
5535
+ const resolvers = options.portableKeyResolvers;
5536
+ if (resolvers == null || cls !== _fedify_vocab.CryptographicKey) {
5537
+ logger.debug("Failed to verify; key {keyId} is a key of a portable actor, which is only accepted for HTTP Signatures.", { keyId });
5538
+ return {
5539
+ result: {
5540
+ key: null,
5541
+ cached: false
5542
+ },
5543
+ outcome: { result: "invalid" }
5544
+ };
5545
+ }
5546
+ const sharedCache = options.keyCache;
5547
+ const scopedCache = sharedCache?.compatibleKeyScope?.("httpSignature");
5548
+ const cached = await getCachedFetchKey(cacheKey, keyId, cls, scopedCache, logger);
5549
+ if (cached != null) return {
5550
+ result: cached,
5551
+ outcome: { result: "hit" }
5552
+ };
5553
+ if (await sharedCache?.get(cacheKey) === null) {
5554
+ logger.debug("Entry {keyId} found in cache, but it could not be fetched.", { keyId });
5555
+ return {
5556
+ result: await onCachedUnavailable(cacheKey, keyId, sharedCache, logger),
5557
+ outcome: { result: "hit" }
5558
+ };
5559
+ }
5560
+ logger.debug("Resolving key {keyId} to verify signature...", { keyId });
5561
+ const resolution = await resolvers.actorKey(cacheKey);
5562
+ if (resolution.type === "verified") {
5563
+ await scopedCache?.set(cacheKey, resolution.key, { expires: resolution.expires });
5564
+ return {
5565
+ result: {
5566
+ key: resolution.key,
5567
+ cached: false
5568
+ },
5569
+ outcome: { result: "fetched" }
5570
+ };
5571
+ }
5572
+ if (resolution.type === "rejected") {
5573
+ await scopedCache?.set(cacheKey, null);
5574
+ return {
5575
+ result: {
5576
+ key: null,
5577
+ cached: false
5578
+ },
5579
+ outcome: { result: "invalid" }
5580
+ };
5581
+ }
5582
+ await scopedCache?.delete(cacheKey);
5583
+ return {
5584
+ result: await onFetchError(resolution.error, cacheKey, keyId, resolution.cacheable ? sharedCache : void 0, logger),
5585
+ outcome: resolution.cacheable ? classifyFetchError(resolution.error) : { result: "error" }
5586
+ };
5587
+ }
5434
5588
  async function fetchKeyInternal(keyId, cls, options = {}) {
5435
- return await fetchKeyWithResult(typeof keyId === "string" ? new URL(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
5589
+ return await fetchKeyWithResult(typeof keyId === "string" ? parseKeyIdOrThrow(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
5436
5590
  return {
5437
5591
  key: null,
5438
5592
  cached: true
@@ -7601,6 +7755,14 @@ const SEC = "https://w3id.org/security#";
7601
7755
  * - no more than one entry of either property has that ID; and
7602
7756
  * - the key ID's origin is one of the actor's `gateways`.
7603
7757
  *
7758
+ * An entry has the key's ID if its ID is the key ID, or any compatible
7759
+ * identifier on the same gateway, or the `ap:` or `ap+ef61:` URI, with the
7760
+ * same canonical portable ID. Some publishers, e.g., Mitra, list the keys
7761
+ * of portable actors under `ap:` URIs, but sign requests with compatible key
7762
+ * IDs on their own gateways. IDs are compared as they are written in
7763
+ * the signed document, so an ID that URL parsing would turn into another,
7764
+ * e.g., by resolving dot segments, has no match.
7765
+ *
7604
7766
  * [FEP-8b32]: https://w3id.org/fep/8b32
7605
7767
  * [FEP-ef61]: https://w3id.org/fep/ef61
7606
7768
  * [FEP-521a]: https://w3id.org/fep/521a
@@ -7612,10 +7774,22 @@ const SEC = "https://w3id.org/security#";
7612
7774
  * @returns The verification result, which is never `legacy`.
7613
7775
  * @internal
7614
7776
  */
7615
- async function verifyPortableGatewayKeyDocument(document, actor, keyId, options = {}) {
7777
+ function verifyPortableGatewayKeyDocument(document, actor, keyId, options = {}) {
7778
+ return verifyPortableKeyDocument(document, actor, keyId, true, options);
7779
+ }
7780
+ /**
7781
+ * Verifies that the signed document of a portable actor vouches for the key
7782
+ * with the given ID, either a gateway key at a compatible key ID, or a key of
7783
+ * the actor itself at an `ap:` or `ap+ef61:` key ID. See
7784
+ * {@link verifyPortableGatewayKeyDocument} for the rules; a key of the actor
7785
+ * itself names no gateway, so instead of the key ID's origin being one of
7786
+ * the actor's `gateways`, the actor only has to have a valid gateway, as
7787
+ * FEP-ef61 requires of every portable actor.
7788
+ */
7789
+ async function verifyPortableKeyDocument(document, actor, keyId, isGatewayKey, options) {
7616
7790
  const reject = (reason) => {
7617
- logger.debug("Failed to verify gateway key {keyId} of the portable actor {actorId}: {reason}", {
7618
- keyId: keyId.href,
7791
+ logger.debug("Failed to verify key {keyId} of the portable actor {actorId}: {reason}", {
7792
+ keyId: (0, _fedify_vocab_runtime.formatIri)(keyId),
7619
7793
  actorId: actor.id?.href,
7620
7794
  reason
7621
7795
  });
@@ -7627,8 +7801,9 @@ async function verifyPortableGatewayKeyDocument(document, actor, keyId, options
7627
7801
  const keyBase = new URL(keyId.href);
7628
7802
  keyBase.hash = "";
7629
7803
  const claimedActorId = getCanonicalPortableId(keyBase);
7804
+ const claimedKeyId = getCanonicalPortableId(keyId);
7630
7805
  const actorId = actor.id == null ? null : getCanonicalPortableId(actor.id);
7631
- if (claimedActorId == null || actorId == null) return reject("The key ID or the actor ID is not a valid portable ID.");
7806
+ if (claimedActorId == null || claimedKeyId == null || actorId == null) return reject("The key ID or the actor ID is not a valid portable ID.");
7632
7807
  if (claimedActorId !== actorId) return reject("The key ID is not a compatible identifier of the actor it dereferences to.");
7633
7808
  let verification;
7634
7809
  try {
@@ -7673,10 +7848,12 @@ async function verifyPortableGatewayKeyDocument(document, actor, keyId, options
7673
7848
  if (!(error instanceof TypeError)) throw error;
7674
7849
  return reject("The verified actor document is malformed.");
7675
7850
  }
7676
- if (!isPortableActorDocument(verifiedActor) || getCanonicalPortableId(verifiedActor.id) !== claimedActorId) return reject("The key ID is not a compatible identifier of the actor whose document is signed.");
7851
+ const rootId = root["@id"];
7852
+ 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.");
7677
7853
  actor = verifiedActor;
7678
- const assertionNodes = findNodes(root[`${SEC}assertionMethod`], keyId);
7679
- const publicKeyNodes = findNodes(root[`${SEC}publicKey`], keyId);
7854
+ const findKeyNodes = (values) => findNodes(values, claimedKeyId, isGatewayKey ? keyId.origin : null);
7855
+ const assertionNodes = findKeyNodes(root[`${SEC}assertionMethod`]);
7856
+ const publicKeyNodes = findKeyNodes(root[`${SEC}publicKey`]);
7680
7857
  if (assertionNodes.length > 1 || publicKeyNodes.length > 1) return reject("The actor document has more than one key with the ID.");
7681
7858
  let publicKey;
7682
7859
  if (assertionNodes.length > 0) {
@@ -7706,7 +7883,9 @@ async function verifyPortableGatewayKeyDocument(document, actor, keyId, options
7706
7883
  if (embedded.ownerId == null || getCanonicalPortableId(embedded.ownerId) !== actorId) return reject("The owner of the key is not the actor.");
7707
7884
  publicKey = embedded.publicKey;
7708
7885
  }
7709
- if (!actor.gateways.some((gateway) => (0, _fedify_vocab_runtime.isGatewayUrl)(gateway) && gateway.origin === keyId.origin)) return reject(`The gateway ${keyId.origin} is not listed in the actor's gateways.`);
7886
+ if (isGatewayKey) {
7887
+ if (!actor.gateways.some((gateway) => (0, _fedify_vocab_runtime.isGatewayUrl)(gateway) && gateway.origin === keyId.origin)) return reject(`The gateway ${keyId.origin} is not listed in the actor's gateways.`);
7888
+ } else if (!actor.gateways.some(_fedify_vocab_runtime.isGatewayUrl)) return reject("The actor has no valid gateways.");
7710
7889
  const key = new _fedify_vocab.CryptographicKey({
7711
7890
  id: keyId,
7712
7891
  owner: actor.id,
@@ -7761,15 +7940,170 @@ async function fetchPortableGatewayKey(keyId, options = {}) {
7761
7940
  contextLoader
7762
7941
  });
7763
7942
  }
7764
- function findNodes(values, id) {
7943
+ /**
7944
+ * Resolves a key of a portable actor at an `ap:` or `ap+ef61:` key ID, e.g.,
7945
+ * `ap://did:key:z6Mk…/actor#main-key`. Such a key ID names no gateway, so
7946
+ * the actor's document, i.e., the key ID without its fragment, is fetched
7947
+ * through the gateways in its `@gateway` location hints, one after another,
7948
+ * until one of them serves a document that vouches for the key. Without
7949
+ * location hints, the document loader is asked for the `ap:` URI itself,
7950
+ * which only a custom document loader can resolve. The hints are chosen by
7951
+ * whoever made the signature, but that does not matter: only a document with
7952
+ * a valid proof by the DID of the key ID vouches for the key.
7953
+ *
7954
+ * The key is taken as a key of the actor itself, not of a gateway, so it is
7955
+ * accepted by the rules of {@link verifyPortableGatewayKeyDocument}, except
7956
+ * that it is listed under the `ap:` or `ap+ef61:` URI with the same canonical
7957
+ * ID as the key ID, and that the actor only has to have a valid gateway.
7958
+ * The key ID must have a fragment; a key served as a standalone document is
7959
+ * not supported.
7960
+ *
7961
+ * If no gateway serves a document that vouches for the key, the result is
7962
+ * `rejected` only if every gateway served a document that definitely does not
7963
+ * vouch for it. If every gateway failed to serve a document at all, the
7964
+ * result is a cacheable `unavailable`, with the error of the first gateway if
7965
+ * every gateway responded with the same HTTP status, so that the status
7966
+ * describes the whole lookup. Otherwise, e.g., if one gateway was
7967
+ * unreachable and another served an invalid document, nothing tells what the
7968
+ * unreachable gateway would have served, so the result is an `unavailable`
7969
+ * that is not cacheable.
7970
+ *
7971
+ * @param keyId The key ID, which must be an `ap:` or `ap+ef61:` URI.
7972
+ * @param options Loaders and telemetry providers.
7973
+ * @returns The resolution, which is never `legacy`.
7974
+ * @internal
7975
+ */
7976
+ async function resolvePortableActorKey(keyId, options = {}) {
7977
+ const documentLoader = options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
7978
+ const contextLoader = options.contextLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
7979
+ let formattedKeyId = keyId.href;
7980
+ const reject = (reason) => {
7981
+ logger.debug("Failed to verify key {keyId} of a portable actor: {reason}", {
7982
+ keyId: formattedKeyId,
7983
+ reason
7984
+ });
7985
+ return {
7986
+ type: "rejected",
7987
+ reason
7988
+ };
7989
+ };
7990
+ if (!isPortableUri(keyId) || getCanonicalPortableId(keyId) == null) return reject("The key ID is not a valid ap: or ap+ef61: URI.");
7991
+ formattedKeyId = (0, _fedify_vocab_runtime.formatIri)(keyId);
7992
+ if (keyId.hash === "" || keyId.hash === "#") return reject("The key ID has no fragment; only keys embedded in actor documents are supported.");
7993
+ const actorUrl = new URL(keyId.href);
7994
+ actorUrl.hash = "";
7995
+ const requestUrls = [];
7996
+ for (const gateway of (0, _fedify_vocab_runtime_internal_portable_dereference.getPortableGatewayCandidates)(actorUrl)) try {
7997
+ requestUrls.push((0, _fedify_vocab_runtime.toCompatibleEf61Id)(actorUrl, gateway).href);
7998
+ } catch (error) {
7999
+ if (!(error instanceof TypeError)) throw error;
8000
+ return reject(`The key ID cannot be fetched from gateways: ${error}`);
8001
+ }
8002
+ if (requestUrls.length < 1) requestUrls.push((0, _fedify_vocab_runtime.formatIri)(actorUrl));
8003
+ const attempts = [];
8004
+ for (const url of requestUrls) {
8005
+ let remoteDocument;
8006
+ try {
8007
+ remoteDocument = await documentLoader(url);
8008
+ } catch (error) {
8009
+ logger.debug("Failed to fetch the actor of key {keyId} from {url}: {error}", {
8010
+ keyId: formattedKeyId,
8011
+ url,
8012
+ error
8013
+ });
8014
+ attempts.push({
8015
+ type: "retrievalFailed",
8016
+ error
8017
+ });
8018
+ continue;
8019
+ }
8020
+ let resolution;
8021
+ try {
8022
+ const object = await _fedify_vocab.Object.fromJsonLd(remoteDocument.document, {
8023
+ documentLoader,
8024
+ contextLoader,
8025
+ tracerProvider: options.tracerProvider
8026
+ });
8027
+ resolution = isPortableActorDocument(object) ? await verifyPortableKeyDocument(remoteDocument.document, object, keyId, false, {
8028
+ ...options,
8029
+ documentLoader,
8030
+ contextLoader
8031
+ }) : {
8032
+ type: "rejected",
8033
+ reason: "The document is not a portable actor."
8034
+ };
8035
+ } catch (error) {
8036
+ if (error instanceof TypeError) resolution = {
8037
+ type: "rejected",
8038
+ reason: `The document is malformed: ${error.message}`
8039
+ };
8040
+ else {
8041
+ logger.debug("Failed to process the actor of key {keyId} served from {url}: {error}", {
8042
+ keyId: formattedKeyId,
8043
+ url,
8044
+ error
8045
+ });
8046
+ attempts.push({
8047
+ type: "indeterminate",
8048
+ error
8049
+ });
8050
+ continue;
8051
+ }
8052
+ }
8053
+ if (resolution.type === "verified") return resolution;
8054
+ const reason = resolution.type === "rejected" ? resolution.reason : "The document is not a portable actor.";
8055
+ logger.debug("The actor document served from {url} does not vouch for key {keyId}: {reason}", {
8056
+ keyId: formattedKeyId,
8057
+ url,
8058
+ reason
8059
+ });
8060
+ attempts.push({
8061
+ type: "rejected",
8062
+ reason
8063
+ });
8064
+ }
8065
+ const rejections = attempts.flatMap((a) => a.type === "rejected" ? [a.reason] : []);
8066
+ if (rejections.length === attempts.length) return reject(rejections.join(" "));
8067
+ const errors = attempts.flatMap((a) => a.type === "rejected" ? [] : [a.error]);
8068
+ 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(", ")}`);
8069
+ if (attempts.every((a) => a.type === "retrievalFailed")) {
8070
+ const statuses = errors.map((error) => error instanceof _fedify_vocab_runtime.FetchError ? error.response?.status : void 0);
8071
+ return {
8072
+ type: "unavailable",
8073
+ error: statuses[0] != null && statuses.every((status) => status === statuses[0]) ? errors[0] : aggregate,
8074
+ cacheable: true
8075
+ };
8076
+ }
8077
+ return {
8078
+ type: "unavailable",
8079
+ error: new Error(`Could not determine whether the actor of key ${formattedKeyId} vouches for it.`, { cause: aggregate }),
8080
+ cacheable: false
8081
+ };
8082
+ }
8083
+ /**
8084
+ * Finds the nodes that stand for the key with the given canonical portable
8085
+ * ID. A node whose ID is an `ap:` or `ap+ef61:` URI stands for the key
8086
+ * itself, e.g., Mitra lists the keys of portable actors that way while
8087
+ * signing requests with compatible key IDs. A node whose ID is a compatible
8088
+ * identifier stands for a gateway's key, so it matches only a compatible key
8089
+ * ID on the same gateway; a key ID that names no gateway never selects
8090
+ * a gateway's key, even with the same fragment.
8091
+ * @param values The expanded values of a key property.
8092
+ * @param canonicalKeyId The canonical portable ID of the key ID.
8093
+ * @param gateway The origin of the gateway whose key the key ID names, or
8094
+ * `null` if the key ID names no gateway.
8095
+ */
8096
+ function findNodes(values, canonicalKeyId, gateway) {
7765
8097
  if (!Array.isArray(values)) return [];
7766
8098
  const nodes = [];
7767
8099
  for (const value of values) {
7768
8100
  if (value == null || typeof value !== "object" || Array.isArray(value)) continue;
7769
8101
  const node = value;
7770
8102
  const nodeId = node["@id"];
7771
- if (typeof nodeId !== "string" || !URL.canParse(nodeId)) continue;
7772
- if (new URL(nodeId).href === id.href) nodes.push(node);
8103
+ if (typeof nodeId !== "string") continue;
8104
+ if (getRawCanonicalPortableId(nodeId) !== canonicalKeyId) continue;
8105
+ const parsed = (0, _fedify_vocab_runtime.parseIri)(nodeId);
8106
+ if (isPortableUri(parsed) || parsed.origin === gateway) nodes.push(node);
7773
8107
  }
7774
8108
  return nodes;
7775
8109
  }
@@ -8148,13 +8482,18 @@ function keyFetchErrorResult(keyId, result) {
8148
8482
  }
8149
8483
  };
8150
8484
  }
8485
+ /**
8486
+ * Creates the resolvers of keys of FEP-ef61 portable actors, which HTTP
8487
+ * Signature verification alone passes to key lookups.
8488
+ */
8489
+ function createPortableKeyResolvers(options) {
8490
+ return {
8491
+ gatewayKey: (document, actor, keyId) => verifyPortableGatewayKeyDocument(document, actor, keyId, options),
8492
+ actorKey: (keyId) => resolvePortableActorKey(keyId, options)
8493
+ };
8494
+ }
8151
8495
  function parseKeyId(value) {
8152
- if (value == null) return null;
8153
- try {
8154
- return new URL(value);
8155
- } catch {
8156
- return null;
8157
- }
8496
+ return value == null ? null : parseKeyIdString(value);
8158
8497
  }
8159
8498
  function getKeyFetchErrorName(error) {
8160
8499
  return error.name || error.constructor.name || "Error";
@@ -8412,7 +8751,7 @@ async function verifyRequestDraft(request, span, metricsContext, { documentLoade
8412
8751
  keyCache,
8413
8752
  tracerProvider,
8414
8753
  meterProvider,
8415
- portableGatewayKeyResolver: (document, actor, keyId) => verifyPortableGatewayKeyDocument(document, actor, keyId, {
8754
+ portableKeyResolvers: createPortableKeyResolvers({
8416
8755
  documentLoader,
8417
8756
  contextLoader,
8418
8757
  keyCache,
@@ -8632,7 +8971,7 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
8632
8971
  keyCache,
8633
8972
  tracerProvider,
8634
8973
  meterProvider,
8635
- portableGatewayKeyResolver: (document, actor, gatewayKeyId) => verifyPortableGatewayKeyDocument(document, actor, gatewayKeyId, {
8974
+ portableKeyResolvers: createPortableKeyResolvers({
8636
8975
  documentLoader,
8637
8976
  contextLoader,
8638
8977
  keyCache,
@@ -9156,6 +9495,12 @@ Object.defineProperty(exports, "isPortableId", {
9156
9495
  return isPortableId;
9157
9496
  }
9158
9497
  });
9498
+ Object.defineProperty(exports, "isPortableKeyId", {
9499
+ enumerable: true,
9500
+ get: function() {
9501
+ return isPortableKeyId;
9502
+ }
9503
+ });
9159
9504
  Object.defineProperty(exports, "isPortableUri", {
9160
9505
  enumerable: true,
9161
9506
  get: function() {
@@ -9276,6 +9621,12 @@ Object.defineProperty(exports, "registerQueueDepthGauge", {
9276
9621
  return registerQueueDepthGauge;
9277
9622
  }
9278
9623
  });
9624
+ Object.defineProperty(exports, "resolvePortableActorKey", {
9625
+ enumerable: true,
9626
+ get: function() {
9627
+ return resolvePortableActorKey;
9628
+ }
9629
+ });
9279
9630
  Object.defineProperty(exports, "selectRequestSignature", {
9280
9631
  enumerable: true,
9281
9632
  get: function() {
@@ -1,11 +1,12 @@
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-BZrnW0Qz.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-DCLoLvLz.mjs";
4
+ import { n as version, t as name } from "./deno-_o3lkDlG.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-CDaHfBXY.mjs";
6
6
  import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-CPVqki78.mjs";
7
- import { a as fetchKeyDetailed, c as validateCryptoKey, t as bypassKeyCacheReads } from "./key-PyYvun0F.mjs";
8
- import { n as verifyPortableGatewayKeyDocument } from "./portable-key-DyylakVV.mjs";
7
+ import { f as parseKeyIdString } from "./portable-key-id-Bg259c4Z.mjs";
8
+ import { a as fetchKeyDetailed, c as validateCryptoKey, t as bypassKeyCacheReads } from "./key-DolJjJab.mjs";
9
+ import { n as resolvePortableActorKey, r as verifyPortableGatewayKeyDocument } from "./portable-key-DqX2emHQ.mjs";
9
10
  import { getLogger } from "@logtape/logtape";
10
11
  import { CryptographicKey } from "@fedify/vocab";
11
12
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -383,13 +384,18 @@ function keyFetchErrorResult(keyId, result) {
383
384
  }
384
385
  };
385
386
  }
387
+ /**
388
+ * Creates the resolvers of keys of FEP-ef61 portable actors, which HTTP
389
+ * Signature verification alone passes to key lookups.
390
+ */
391
+ function createPortableKeyResolvers(options) {
392
+ return {
393
+ gatewayKey: (document, actor, keyId) => verifyPortableGatewayKeyDocument(document, actor, keyId, options),
394
+ actorKey: (keyId) => resolvePortableActorKey(keyId, options)
395
+ };
396
+ }
386
397
  function parseKeyId(value) {
387
- if (value == null) return null;
388
- try {
389
- return new URL(value);
390
- } catch {
391
- return null;
392
- }
398
+ return value == null ? null : parseKeyIdString(value);
393
399
  }
394
400
  function getKeyFetchErrorName(error) {
395
401
  return error.name || error.constructor.name || "Error";
@@ -647,7 +653,7 @@ async function verifyRequestDraft(request, span, metricsContext, { documentLoade
647
653
  keyCache,
648
654
  tracerProvider,
649
655
  meterProvider,
650
- portableGatewayKeyResolver: (document, actor, keyId) => verifyPortableGatewayKeyDocument(document, actor, keyId, {
656
+ portableKeyResolvers: createPortableKeyResolvers({
651
657
  documentLoader,
652
658
  contextLoader,
653
659
  keyCache,
@@ -867,7 +873,7 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
867
873
  keyCache,
868
874
  tracerProvider,
869
875
  meterProvider,
870
- portableGatewayKeyResolver: (document, actor, gatewayKeyId) => verifyPortableGatewayKeyDocument(document, actor, gatewayKeyId, {
876
+ portableKeyResolvers: createPortableKeyResolvers({
871
877
  documentLoader,
872
878
  contextLoader,
873
879
  keyCache,