@fedify/fedify 2.4.0-dev.2164 → 2.4.0-dev.2190

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 (103) hide show
  1. package/dist/{builder-BqRmnx7p.mjs → builder-CFF0YNTp.mjs} +3 -3
  2. package/dist/{circuit-breaker-C5NqoLBJ.mjs → circuit-breaker-XJ53md4S.mjs} +16 -11
  3. package/dist/compat/mod.d.cts +1 -1
  4. package/dist/compat/mod.d.ts +1 -1
  5. package/dist/compat/transformers.test.mjs +1 -1
  6. package/dist/{compound-proof-CxDBdtNz.mjs → compound-proof-F2jjkrt0.mjs} +3 -3
  7. package/dist/{context-BykNUf04.d.ts → context-9T--NSo4.d.ts} +55 -1
  8. package/dist/{context-L0Fnib9j.d.cts → context-CDcAgm4l.d.cts} +55 -1
  9. package/dist/{deno-CizGaYeG.mjs → deno-EopndaNb.mjs} +1 -1
  10. package/dist/{docloader-BBFAItNB.mjs → docloader-u6zzOExF.mjs} +25 -17
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/circuit-breaker.test.mjs +50 -1
  13. package/dist/federation/compound-fanout.test.mjs +3 -3
  14. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  15. package/dist/federation/handler.test.mjs +5 -5
  16. package/dist/federation/hashlink-media.test.mjs +2 -2
  17. package/dist/federation/idempotency.test.mjs +2 -2
  18. package/dist/federation/keycache.test.mjs +1 -1
  19. package/dist/federation/metrics.test.mjs +1 -1
  20. package/dist/federation/middleware.test.mjs +137 -13
  21. package/dist/federation/mod.cjs +1 -1
  22. package/dist/federation/mod.d.cts +2 -2
  23. package/dist/federation/mod.d.ts +2 -2
  24. package/dist/federation/mod.js +1 -1
  25. package/dist/federation/outgoing-proof.test.mjs +5 -5
  26. package/dist/federation/portable-actor.test.mjs +3 -3
  27. package/dist/federation/portable-chain.test.mjs +3 -3
  28. package/dist/federation/portable-collection.test.mjs +4 -4
  29. package/dist/federation/portable-gateway-keys.test.mjs +6 -6
  30. package/dist/federation/portable-inbox.test.mjs +35 -5
  31. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  32. package/dist/federation/portable-route-activity.test.d.mts +2 -0
  33. package/dist/federation/portable-route-activity.test.mjs +259 -0
  34. package/dist/federation/portable.test.mjs +3 -3
  35. package/dist/federation/send.test.mjs +3 -3
  36. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  37. package/dist/federation/tasks/tasks.test.mjs +2 -2
  38. package/dist/federation/temporal.test.mjs +1 -1
  39. package/dist/federation/webfinger.test.mjs +1 -1
  40. package/dist/{federation-DFCnp94y.cjs → federation-BSS2dUO7.cjs} +38 -21
  41. package/dist/{http-Baovr6xq.js → http-BqN5kyUf.js} +426 -141
  42. package/dist/{http-DC1cqAQ2.d.cts → http-BrwKINQn.d.cts} +28 -0
  43. package/dist/{http-R4g1McwE.cjs → http-CCr2_ipG.cjs} +443 -140
  44. package/dist/{http-Cok5-vIC.d.ts → http-DGx0KOws.d.ts} +28 -0
  45. package/dist/{http-DRH0n8TD.mjs → http-Y1ObT1A8.mjs} +180 -92
  46. package/dist/{key-D5awqp7u.mjs → key-DyXf88bq.mjs} +4 -4
  47. package/dist/{key-owner-evidence-Bo3yaeL5.mjs → key-owner-evidence-APB6NpSk.mjs} +1 -1
  48. package/dist/{keycache-PiOM9pM0.mjs → keycache-Da8A8mZa.mjs} +1 -1
  49. package/dist/{kv-cache-CZiBDLPi.js → kv-cache-C02XitwK.js} +24 -16
  50. package/dist/{kv-cache-C7RWL7hz.mjs → kv-cache-C8YjNAi_.mjs} +1 -1
  51. package/dist/{kv-cache-Ct-KWRta.cjs → kv-cache-DUXluAc0.cjs} +23 -15
  52. package/dist/{ld-CyliEU48.mjs → ld-oQj6j_hP.mjs} +3 -3
  53. package/dist/{metrics-CFBEehTc.mjs → metrics-zMe834ID.mjs} +1 -1
  54. package/dist/{middleware-B99JjvFn.mjs → middleware-Bdu2-OPD.mjs} +37 -25
  55. package/dist/{middleware-CdpTFsa2.js → middleware-CaR2MRWY.js} +39 -22
  56. package/dist/{middleware-BmqEWsWP.mjs → middleware-Ck9yghxs.mjs} +1 -1
  57. package/dist/{mod-DK0y9fd-.d.ts → mod-B3OBLj5g.d.ts} +1 -1
  58. package/dist/{mod-B5z_q9TH.d.ts → mod-C5Mwx5ty.d.ts} +5 -1
  59. package/dist/{mod--jv6vyrP.d.ts → mod-Ck9_lSJB.d.ts} +1 -1
  60. package/dist/{mod-BLOXGs6m.d.cts → mod-D58KCqUg.d.cts} +1 -1
  61. package/dist/{mod-CPPqkyV_.d.cts → mod-DHTuu3l8.d.cts} +1 -1
  62. package/dist/{mod-BA_F2x53.d.cts → mod-Dp3Ij7sw.d.cts} +5 -1
  63. package/dist/mod.cjs +4 -4
  64. package/dist/mod.d.cts +5 -5
  65. package/dist/mod.d.ts +5 -5
  66. package/dist/mod.js +4 -4
  67. package/dist/nodeinfo/handler.test.mjs +1 -1
  68. package/dist/{owner-DKdtsrEq.mjs → owner-Dem4fbwc.mjs} +5 -5
  69. package/dist/{portable-collection-uv4CuQXm.js → portable-collection-BEqLrMW3.js} +1 -1
  70. package/dist/{portable-collection-Cb5cdPIU.mjs → portable-collection-BL6aZlQ0.mjs} +2 -2
  71. package/dist/{portable-collection-UajqDrSL.cjs → portable-collection-DJe_bk50.cjs} +1 -1
  72. package/dist/{portable-key-Bv1RdQ0n.mjs → portable-key-Duue9Y2Y.mjs} +209 -56
  73. package/dist/{portable-key-id-Bg259c4Z.mjs → portable-key-id-Ca30-hOa.mjs} +45 -1
  74. package/dist/{proof-CRTPWbVh.mjs → proof-C9115DX_.mjs} +5 -5
  75. package/dist/{send-2dULlZc8.mjs → send-D0hxFFRB.mjs} +3 -3
  76. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  77. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  78. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  79. package/dist/sig/compound-proof.test.mjs +2 -2
  80. package/dist/sig/http.test.mjs +495 -23
  81. package/dist/sig/key-owner-evidence.test.mjs +2 -2
  82. package/dist/sig/key.test.mjs +1 -1
  83. package/dist/sig/ld.test.mjs +2 -2
  84. package/dist/sig/mod.cjs +2 -2
  85. package/dist/sig/mod.d.cts +2 -2
  86. package/dist/sig/mod.d.ts +2 -2
  87. package/dist/sig/mod.js +2 -2
  88. package/dist/sig/owner.test.mjs +1 -1
  89. package/dist/sig/portable-collection.test.mjs +1 -1
  90. package/dist/sig/portable-key-id.test.d.mts +2 -0
  91. package/dist/sig/portable-key-id.test.mjs +67 -0
  92. package/dist/sig/portable-key.test.mjs +253 -7
  93. package/dist/sig/proof.test.mjs +3 -3
  94. package/dist/{temporal-Dhsk6aUT.mjs → temporal-DKchpKyp.mjs} +1 -1
  95. package/dist/testing/kv.test.mjs +1 -1
  96. package/dist/testing/mod.d.mts +52 -0
  97. package/dist/utils/docloader.test.mjs +93 -2
  98. package/dist/utils/kv-cache.test.mjs +1 -1
  99. package/dist/utils/mod.cjs +1 -1
  100. package/dist/utils/mod.d.cts +1 -1
  101. package/dist/utils/mod.d.ts +1 -1
  102. package/dist/utils/mod.js +1 -1
  103. package/package.json +6 -6
@@ -16,7 +16,7 @@ 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.2164+db2b8571";
19
+ var version = "2.4.0-dev.2190+be3a8be0";
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;
@@ -117,6 +117,50 @@ function getCanonicalPortableId(id) {
117
117
  }
118
118
  }
119
119
  /**
120
+ * Gets the [FEP-fe34] origin of an ID for same-origin checks. The origin of
121
+ * an `ap:` or `ap+ef61:` URI, or of a compatible identifier on any gateway,
122
+ * is the DID of its canonical portable ID, as FEP-ef61 treats a compatible
123
+ * identifier as the portable ID it stands for; unlike `getFe34Origin()`,
124
+ * which gives a compatible identifier the web origin of its gateway. The
125
+ * origin of any other HTTP(S) URL is its web origin, and that of a DID URL
126
+ * is its DID.
127
+ *
128
+ * Only the parsed `URL` is validated, so dot segments that URL parsing has
129
+ * already resolved cannot be detected here.
130
+ *
131
+ * [FEP-fe34]: https://w3id.org/fep/fe34
132
+ * @returns The origin, or `null` if the ID has none that is supported, or is
133
+ * a malformed portable ID or compatible identifier.
134
+ * @internal
135
+ */
136
+ function getAuthenticationOrigin(id) {
137
+ try {
138
+ if (isPortableId(id)) {
139
+ const canonical = getCanonicalPortableId(id);
140
+ return canonical == null ? null : (0, _fedify_vocab_runtime.getFe34Origin)(canonical);
141
+ }
142
+ return (0, _fedify_vocab_runtime.getFe34Origin)(id);
143
+ } catch (error) {
144
+ if (error instanceof TypeError) return null;
145
+ throw error;
146
+ }
147
+ }
148
+ /**
149
+ * Checks whether two IDs identify the same object. Two portable IDs, i.e.,
150
+ * `ap:` or `ap+ef61:` URIs or compatible identifiers, are compared by their
151
+ * canonical portable IDs, so that the same object on different gateways and
152
+ * with different location hints matches. Other IDs have to be equal as
153
+ * they are.
154
+ * @internal
155
+ */
156
+ function isSameObjectId(a, b) {
157
+ const aPortable = isPortableId(a);
158
+ if (aPortable !== isPortableId(b)) return false;
159
+ if (!aPortable) return a.href === b.href;
160
+ const canonical = getCanonicalPortableId(a);
161
+ return canonical != null && canonical === getCanonicalPortableId(b);
162
+ }
163
+ /**
120
164
  * Gets the canonical portable ID of an `ap:` or `ap+ef61:` URI or
121
165
  * a compatible identifier as it is written, before any URL parsing.
122
166
  * Parsing an ID into a `URL` resolves dot segments in its path, which would
@@ -8102,6 +8146,21 @@ async function fetchPortableGatewayKey(keyId, options = {}) {
8102
8146
  });
8103
8147
  }
8104
8148
  /**
8149
+ * The maximum number of `@gateway` location hints followed for one `ap:` or
8150
+ * `ap+ef61:` key ID. Whoever made a signature chooses the hints of its key
8151
+ * ID, so fewer of them are followed than when dereferencing objects, which
8152
+ * bounds the gateways one signature can make Fedify ask. Further hints are
8153
+ * ignored.
8154
+ */
8155
+ const MAX_KEY_ID_GATEWAY_HINTS = 3;
8156
+ /**
8157
+ * The default of {@link PortableActorKeyOptions.lookupTimeout}, in
8158
+ * milliseconds.
8159
+ */
8160
+ const DEFAULT_KEY_ID_LOOKUP_TIMEOUT = 1e4;
8161
+ /** The longest delay that `setTimeout()` supports, in milliseconds. */
8162
+ const MAX_TIMEOUT = 2 ** 31 - 1;
8163
+ /**
8105
8164
  * Resolves a key of a portable actor at an `ap:` or `ap+ef61:` key ID, e.g.,
8106
8165
  * `ap://did:key:z6Mk…/actor#main-key`. Such a key ID names no gateway, so
8107
8166
  * the actor's document, i.e., the key ID without its fragment, is fetched
@@ -8129,12 +8188,25 @@ async function fetchPortableGatewayKey(keyId, options = {}) {
8129
8188
  * unreachable gateway would have served, so the result is an `unavailable`
8130
8189
  * that is not cacheable.
8131
8190
  *
8191
+ * Only the first {@link MAX_KEY_ID_GATEWAY_HINTS} location hints are
8192
+ * followed, and all the gateways share one timeout, see
8193
+ * {@link PortableActorKeyOptions.lookupTimeout}. A gateway that does not
8194
+ * serve the document in time failed to serve it, like an unreachable one,
8195
+ * with an error that has no HTTP status. A gateway whose document could not
8196
+ * be processed in time, and the gateways left untried, make the result an
8197
+ * `unavailable` that is not cacheable. A result that arrives after
8198
+ * the timeout is ignored, even if it vouches for the key.
8199
+ *
8132
8200
  * @param keyId The key ID, which must be an `ap:` or `ap+ef61:` URI.
8133
- * @param options Loaders and telemetry providers.
8201
+ * @param options Loaders, telemetry providers, and the timeout.
8134
8202
  * @returns The resolution, which is never `legacy`.
8203
+ * @throws {RangeError} If the timeout is not a non-negative number of
8204
+ * milliseconds that `setTimeout()` supports.
8135
8205
  * @internal
8136
8206
  */
8137
8207
  async function resolvePortableActorKey(keyId, options = {}) {
8208
+ const lookupTimeout = options.lookupTimeout ?? DEFAULT_KEY_ID_LOOKUP_TIMEOUT;
8209
+ if (!Number.isFinite(lookupTimeout) || lookupTimeout < 0 || lookupTimeout > MAX_TIMEOUT) throw new RangeError(`The lookup timeout must be between 0 and ${MAX_TIMEOUT} ms: ${lookupTimeout}`);
8138
8210
  const documentLoader = options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
8139
8211
  const contextLoader = options.contextLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
8140
8212
  let formattedKeyId = keyId.href;
@@ -8153,75 +8225,67 @@ async function resolvePortableActorKey(keyId, options = {}) {
8153
8225
  if (keyId.hash === "" || keyId.hash === "#") return reject("The key ID has no fragment; only keys embedded in actor documents are supported.");
8154
8226
  const actorUrl = new URL(keyId.href);
8155
8227
  actorUrl.hash = "";
8228
+ const gateways = (0, _fedify_vocab_runtime_internal_portable_dereference.getPortableGatewayCandidates)(actorUrl);
8229
+ if (gateways.length > MAX_KEY_ID_GATEWAY_HINTS) logger.debug("Following only the first {max} location hints of key {keyId}.", {
8230
+ keyId: formattedKeyId,
8231
+ max: MAX_KEY_ID_GATEWAY_HINTS
8232
+ });
8156
8233
  const requestUrls = [];
8157
- for (const gateway of (0, _fedify_vocab_runtime_internal_portable_dereference.getPortableGatewayCandidates)(actorUrl)) try {
8234
+ for (const gateway of gateways.slice(0, MAX_KEY_ID_GATEWAY_HINTS)) try {
8158
8235
  requestUrls.push((0, _fedify_vocab_runtime.toCompatibleEf61Id)(actorUrl, gateway).href);
8159
8236
  } catch (error) {
8160
8237
  if (!(error instanceof TypeError)) throw error;
8161
8238
  return reject(`The key ID cannot be fetched from gateways: ${error}`);
8162
8239
  }
8163
8240
  if (requestUrls.length < 1) requestUrls.push((0, _fedify_vocab_runtime.formatIri)(actorUrl));
8241
+ const controller = new AbortController();
8242
+ const timeoutError = new DOMException(`Timed out looking up the actor of key ${formattedKeyId} after ${lookupTimeout} ms.`, "TimeoutError");
8243
+ let timer;
8244
+ if (lookupTimeout > 0) timer = setTimeout(() => controller.abort(timeoutError), lookupTimeout);
8245
+ else controller.abort(timeoutError);
8246
+ const { signal } = controller;
8247
+ const attemptOptions = {
8248
+ ...options,
8249
+ documentLoader: withDeadline(documentLoader, signal),
8250
+ contextLoader: withDeadline(contextLoader, signal),
8251
+ signal,
8252
+ formattedKeyId
8253
+ };
8164
8254
  const attempts = [];
8165
- for (const url of requestUrls) {
8166
- let remoteDocument;
8167
- try {
8168
- remoteDocument = await documentLoader(url);
8169
- } catch (error) {
8170
- logger.debug("Failed to fetch the actor of key {keyId} from {url}: {error}", {
8171
- keyId: formattedKeyId,
8172
- url,
8173
- error
8174
- });
8175
- attempts.push({
8176
- type: "retrievalFailed",
8177
- error
8178
- });
8179
- continue;
8180
- }
8181
- let resolution;
8182
- try {
8183
- const object = await _fedify_vocab.Object.fromJsonLd(remoteDocument.document, {
8184
- documentLoader,
8185
- contextLoader,
8186
- tracerProvider: options.tracerProvider
8187
- });
8188
- resolution = isPortableActorDocument(object) ? await verifyPortableKeyDocument(remoteDocument.document, object, keyId, false, {
8189
- ...options,
8190
- documentLoader,
8191
- contextLoader
8192
- }) : {
8193
- type: "rejected",
8194
- reason: "The document is not a portable actor."
8195
- };
8196
- } catch (error) {
8197
- if (error instanceof TypeError) resolution = {
8198
- type: "rejected",
8199
- reason: `The document is malformed: ${error.message}`
8200
- };
8201
- else {
8202
- logger.debug("Failed to process the actor of key {keyId} served from {url}: {error}", {
8255
+ try {
8256
+ for (const url of requestUrls) {
8257
+ if (signal.aborted) {
8258
+ logger.debug("Ran out of time looking up the actor of key {keyId}; gateways from {url} on were not tried.", {
8203
8259
  keyId: formattedKeyId,
8204
- url,
8205
- error
8260
+ url
8206
8261
  });
8207
8262
  attempts.push({
8208
8263
  type: "indeterminate",
8209
- error
8264
+ error: signal.reason
8265
+ });
8266
+ break;
8267
+ }
8268
+ const state = { processing: false };
8269
+ let attempt;
8270
+ try {
8271
+ attempt = await raceAbort(attemptGateway(url, keyId, attemptOptions, state), signal);
8272
+ } catch (error) {
8273
+ if (!signal.aborted) throw error;
8274
+ logger.debug("Ran out of time looking up the actor of key {keyId} at {url}.", {
8275
+ keyId: formattedKeyId,
8276
+ url
8277
+ });
8278
+ attempts.push({
8279
+ type: state.processing ? "indeterminate" : "retrievalFailed",
8280
+ error: signal.reason
8210
8281
  });
8211
8282
  continue;
8212
8283
  }
8284
+ if (attempt.type === "verified") return attempt;
8285
+ attempts.push(attempt);
8213
8286
  }
8214
- if (resolution.type === "verified") return resolution;
8215
- const reason = resolution.type === "rejected" ? resolution.reason : "The document is not a portable actor.";
8216
- logger.debug("The actor document served from {url} does not vouch for key {keyId}: {reason}", {
8217
- keyId: formattedKeyId,
8218
- url,
8219
- reason
8220
- });
8221
- attempts.push({
8222
- type: "rejected",
8223
- reason
8224
- });
8287
+ } finally {
8288
+ clearTimeout(timer);
8225
8289
  }
8226
8290
  const rejections = attempts.flatMap((a) => a.type === "rejected" ? [a.reason] : []);
8227
8291
  if (rejections.length === attempts.length) return reject(rejections.join(" "));
@@ -8242,6 +8306,139 @@ async function resolvePortableActorKey(keyId, options = {}) {
8242
8306
  };
8243
8307
  }
8244
8308
  /**
8309
+ * Fetches the actor's document of a key at an `ap:` or `ap+ef61:` key ID from
8310
+ * one gateway, and tells whether it vouches for the key. It never rejects;
8311
+ * the caller races it against the lookup's timeout, and ignores what it
8312
+ * resolves to after the timeout.
8313
+ */
8314
+ async function attemptGateway(url, keyId, options, state) {
8315
+ const { documentLoader, contextLoader, signal, formattedKeyId } = options;
8316
+ let remoteDocument;
8317
+ try {
8318
+ remoteDocument = await documentLoader(url);
8319
+ } catch (error) {
8320
+ logger.debug("Failed to fetch the actor of key {keyId} from {url}: {error}", {
8321
+ keyId: formattedKeyId,
8322
+ url,
8323
+ error
8324
+ });
8325
+ return {
8326
+ type: "retrievalFailed",
8327
+ error
8328
+ };
8329
+ }
8330
+ if (signal.aborted) return {
8331
+ type: "indeterminate",
8332
+ error: signal.reason
8333
+ };
8334
+ state.processing = true;
8335
+ let resolution;
8336
+ try {
8337
+ const object = await _fedify_vocab.Object.fromJsonLd(remoteDocument.document, {
8338
+ documentLoader,
8339
+ contextLoader,
8340
+ tracerProvider: options.tracerProvider
8341
+ });
8342
+ signal.throwIfAborted();
8343
+ resolution = isPortableActorDocument(object) ? await verifyPortableKeyDocument(remoteDocument.document, object, keyId, false, options) : {
8344
+ type: "rejected",
8345
+ reason: "The document is not a portable actor."
8346
+ };
8347
+ } catch (error) {
8348
+ if (error instanceof TypeError && !signal.aborted) resolution = {
8349
+ type: "rejected",
8350
+ reason: `The document is malformed: ${error.message}`
8351
+ };
8352
+ else {
8353
+ logger.debug("Failed to process the actor of key {keyId} served from {url}: {error}", {
8354
+ keyId: formattedKeyId,
8355
+ url,
8356
+ error
8357
+ });
8358
+ return {
8359
+ type: "indeterminate",
8360
+ error
8361
+ };
8362
+ }
8363
+ }
8364
+ if (resolution.type === "verified") return resolution;
8365
+ const reason = resolution.type === "rejected" ? resolution.reason : "The document is not a portable actor.";
8366
+ logger.debug("The actor document served from {url} does not vouch for key {keyId}: {reason}", {
8367
+ keyId: formattedKeyId,
8368
+ url,
8369
+ reason
8370
+ });
8371
+ return {
8372
+ type: "rejected",
8373
+ reason
8374
+ };
8375
+ }
8376
+ /**
8377
+ * Wraps a document loader so that it gives up when the given signal aborts,
8378
+ * as well as when the signal in its own options does. The wrapped loader is
8379
+ * passed a signal that aborts in either case, but in case it ignores that
8380
+ * signal, what it resolves to after the abort is ignored, and the wrapper
8381
+ * rejects with the reason of the abort at once. After the abort, the wrapped
8382
+ * loader is not called at all.
8383
+ */
8384
+ function withDeadline(loader, signal) {
8385
+ return (url, options) => {
8386
+ const combined = combineSignals(signal, options?.signal);
8387
+ return raceAbort(combined.signal.aborted ? Promise.reject(combined.signal.reason) : new Promise((resolve) => resolve(loader(url, {
8388
+ ...options,
8389
+ signal: combined.signal
8390
+ }))), combined.signal).finally(combined.dispose);
8391
+ };
8392
+ }
8393
+ /**
8394
+ * Makes a signal that aborts when either of the given signals does, with
8395
+ * the reason of the one that aborted first. `AbortSignal.any()` is not
8396
+ * available on every supported runtime, e.g., Bun before 1.1.4.
8397
+ * @returns The signal, and a function that stops listening to the given
8398
+ * signals.
8399
+ */
8400
+ function combineSignals(signal, other) {
8401
+ if (other == null || other === signal) return {
8402
+ signal,
8403
+ dispose() {}
8404
+ };
8405
+ const controller = new AbortController();
8406
+ const sources = [signal, other];
8407
+ const dispose = () => {
8408
+ for (const source of sources) source.removeEventListener("abort", onAbort);
8409
+ };
8410
+ function onAbort() {
8411
+ dispose();
8412
+ controller.abort(this.reason);
8413
+ }
8414
+ const aborted = sources.find((source) => source.aborted);
8415
+ if (aborted == null) for (const source of sources) source.addEventListener("abort", onAbort);
8416
+ else controller.abort(aborted.reason);
8417
+ return {
8418
+ signal: controller.signal,
8419
+ dispose
8420
+ };
8421
+ }
8422
+ /**
8423
+ * Waits for the given promise unless the given signal aborts first, in which
8424
+ * case it rejects with the reason of the abort. If the signal has aborted by
8425
+ * the time the promise settles, the abort wins as well, so that nothing that
8426
+ * settles after it counts.
8427
+ */
8428
+ function raceAbort(promise, signal) {
8429
+ return new Promise((resolve, reject) => {
8430
+ const onAbort = () => reject(signal.reason);
8431
+ const settle = (settled) => {
8432
+ signal.removeEventListener("abort", onAbort);
8433
+ if (signal.aborted) onAbort();
8434
+ else settled();
8435
+ };
8436
+ promise.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
8437
+ if (signal.aborted) onAbort();
8438
+ else signal.addEventListener("abort", onAbort, { once: true });
8439
+ });
8440
+ }
8441
+ /**
8245
8442
  * Finds the nodes that stand for the key with the given canonical portable
8246
8443
  * ID. A node whose ID is an `ap:` or `ap+ef61:` URI stands for the key
8247
8444
  * itself, e.g., Mitra lists the keys of portable actors that way while
@@ -8473,16 +8670,19 @@ function parseDraftSignature(signature) {
8473
8670
  * Lists the HTTP Signatures that a request claims to carry, without verifying
8474
8671
  * any of them. Signatures whose key IDs are not URLs are left out.
8475
8672
  * @param request The request.
8673
+ * @param maxSignatures The maximum number of RFC 9421 signatures to consider,
8674
+ * counted the same way as by {@link verifyRequest}, so
8675
+ * that the signatures it ignores are not listed either.
8476
8676
  * @returns The signatures; RFC 9421 ones if the request has
8477
8677
  * a `Signature-Input` header, or a draft-cavage one otherwise.
8478
8678
  * @internal
8479
8679
  */
8480
- function listRequestSignatures(request) {
8680
+ function listRequestSignatures(request, maxSignatures = 3) {
8481
8681
  const signatureInput = request.headers.get("Signature-Input");
8482
8682
  if (signatureInput != null) {
8483
8683
  const signatures = [];
8484
- const inputs = parseRfc9421SignatureInput(signatureInput);
8485
- for (const [label, input] of Object.entries(inputs)) {
8684
+ const inputs = Object.entries(parseRfc9421SignatureInput(signatureInput));
8685
+ for (const [label, input] of inputs.slice(0, maxSignatures)) {
8486
8686
  const keyId = parseKeyId(input.keyId);
8487
8687
  if (keyId == null) continue;
8488
8688
  signatures.push({
@@ -8610,6 +8810,18 @@ async function signRequestRfc9421(request, privateKey, keyId, span, currentTime,
8610
8810
  body
8611
8811
  });
8612
8812
  }
8813
+ /**
8814
+ * Checks that a maximum number of signatures to verify is a positive safe
8815
+ * integer or `Infinity`.
8816
+ * @param value The value to check.
8817
+ * @param name The name of the option, used in the error message.
8818
+ * @throws {RangeError} Thrown if the value is not valid.
8819
+ * @internal
8820
+ */
8821
+ function validateMaxSignatures(value, name) {
8822
+ if (value === Infinity || Number.isSafeInteger(value) && value >= 1) return;
8823
+ throw new RangeError(`${name} must be a positive integer or Infinity.`);
8824
+ }
8613
8825
  const supportedHashAlgorithms = {
8614
8826
  "sha": "SHA-1",
8615
8827
  "sha-256": "SHA-256",
@@ -8702,6 +8914,8 @@ function recordVerificationResult(span, result) {
8702
8914
  * @param options Options for verifying the request.
8703
8915
  * @returns The public key of the verified signature, or `null` if the signature
8704
8916
  * could not be verified.
8917
+ * @throws {RangeError} Thrown if {@link VerifyRequestOptions.maxSignatures}
8918
+ * is not a positive integer or `Infinity`.
8705
8919
  */
8706
8920
  async function verifyRequest(request, options = {}) {
8707
8921
  const result = await verifyRequestDetailed(request, options);
@@ -8714,9 +8928,12 @@ async function verifyRequest(request, options = {}) {
8714
8928
  * @param request The request to verify.
8715
8929
  * @param options Options for verifying the request.
8716
8930
  * @returns The verified public key, or a structured verification failure.
8931
+ * @throws {RangeError} Thrown if {@link VerifyRequestOptions.maxSignatures}
8932
+ * is not a positive integer or `Infinity`.
8717
8933
  * @since 2.1.0
8718
8934
  */
8719
8935
  async function verifyRequestDetailed(request, options = {}) {
8936
+ if (options.maxSignatures !== void 0) validateMaxSignatures(options.maxSignatures, "maxSignatures");
8720
8937
  return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version).startActiveSpan("http_signatures.verify", async (span) => {
8721
8938
  if (span.isRecording()) {
8722
8939
  span.setAttribute(_opentelemetry_semantic_conventions.ATTR_HTTP_REQUEST_METHOD, request.method);
@@ -9025,7 +9242,100 @@ async function verifyRfc9421ContentDigest(digestHeader, body) {
9025
9242
  }
9026
9243
  return false;
9027
9244
  }
9028
- async function verifyRequestRfc9421(request, span, metricsContext, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, meterProvider, tracerProvider } = {}) {
9245
+ /**
9246
+ * Verifies one RFC 9421 signature of a request with a key.
9247
+ * @param request The request, whose body may be consumed already.
9248
+ * @param sigInput The parsed `Signature-Input` member of the signature.
9249
+ * @param sigBytes The bytes of the signature.
9250
+ * @param key The public key named by the signature.
9251
+ * @param span The span to record the algorithm and the signature on.
9252
+ * @returns Whether the signature is verified, and the algorithm of the
9253
+ * signature if it is supported.
9254
+ */
9255
+ async function verifyRfc9421SignatureWithKey(request, sigInput, sigBytes, key, span) {
9256
+ const logger = (0, _logtape_logtape.getLogger)([
9257
+ "fedify",
9258
+ "sig",
9259
+ "http"
9260
+ ]);
9261
+ let alg = sigInput.alg?.toLowerCase();
9262
+ if (alg == null) {
9263
+ if (key.publicKey.algorithm.name === "RSASSA-PKCS1-v1_5") alg = "hash" in key.publicKey.algorithm ? key.publicKey.algorithm.hash === "SHA-512" ? "rsa-v1_5-sha512" : "rsa-v1_5-sha256" : "rsa-v1_5-sha256";
9264
+ else if (key.publicKey.algorithm.name === "RSA-PSS") alg = "rsa-pss-sha512";
9265
+ else if (key.publicKey.algorithm.name === "ECDSA") alg = "namedCurve" in key.publicKey.algorithm && key.publicKey.algorithm.namedCurve === "P-256" ? "ecdsa-p256-sha256" : "ecdsa-p384-sha384";
9266
+ else if (key.publicKey.algorithm.name === "Ed25519") alg = "ed25519";
9267
+ }
9268
+ if (alg) span?.setAttribute("http_signatures.algorithm", alg);
9269
+ const algorithm = alg && rfc9421AlgorithmMap[alg];
9270
+ if (!algorithm) {
9271
+ logger.debug("Failed to verify; unsupported algorithm: {algorithm}", {
9272
+ algorithm: sigInput.alg,
9273
+ supported: Object.keys(rfc9421AlgorithmMap)
9274
+ });
9275
+ return {
9276
+ verified: false,
9277
+ mismatched: false
9278
+ };
9279
+ }
9280
+ const metricAlgorithm = alg;
9281
+ let signatureBase;
9282
+ try {
9283
+ signatureBase = createRfc9421SignatureBase(request, sigInput.components, sigInput.parameters);
9284
+ } catch (error) {
9285
+ logger.debug("Failed to create signature base for verification: {error}", {
9286
+ error,
9287
+ signatureInput: sigInput
9288
+ });
9289
+ return {
9290
+ verified: false,
9291
+ algorithm: metricAlgorithm,
9292
+ mismatched: false
9293
+ };
9294
+ }
9295
+ const signatureBaseBytes = new TextEncoder().encode(signatureBase);
9296
+ span?.setAttribute("http_signatures.signature", (0, byte_encodings_hex.encodeHex)(sigBytes));
9297
+ const algorithmName = typeof algorithm === "string" ? algorithm : algorithm.name;
9298
+ if (key.publicKey.algorithm.name !== algorithmName) {
9299
+ logger.debug("Failed to verify; key {keyId} is not a key for algorithm {algorithm}.", {
9300
+ keyId: sigInput.keyId,
9301
+ algorithm: alg
9302
+ });
9303
+ return {
9304
+ verified: false,
9305
+ algorithm: metricAlgorithm,
9306
+ mismatched: true
9307
+ };
9308
+ }
9309
+ let verified;
9310
+ try {
9311
+ verified = await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes);
9312
+ } catch (error) {
9313
+ logger.debug("Error during signature verification: {error}", {
9314
+ error,
9315
+ keyId: sigInput.keyId,
9316
+ algorithm: sigInput.alg
9317
+ });
9318
+ return {
9319
+ verified: false,
9320
+ algorithm: metricAlgorithm,
9321
+ mismatched: false
9322
+ };
9323
+ }
9324
+ if (verified) return {
9325
+ verified: true,
9326
+ algorithm: metricAlgorithm
9327
+ };
9328
+ logger.debug("Failed to verify signature with key {keyId}; signature invalid.", {
9329
+ keyId: sigInput.keyId,
9330
+ signatureBase
9331
+ });
9332
+ return {
9333
+ verified: false,
9334
+ algorithm: metricAlgorithm,
9335
+ mismatched: true
9336
+ };
9337
+ }
9338
+ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, maxSignatures = 3, meterProvider, tracerProvider } = {}) {
9029
9339
  const logger = (0, _logtape_logtape.getLogger)([
9030
9340
  "fedify",
9031
9341
  "sig",
@@ -9038,7 +9348,6 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
9038
9348
  logger.error("Failed to verify; the request body is locked.", { url: request.url });
9039
9349
  return noSignatureResult();
9040
9350
  }
9041
- const originalRequest = request;
9042
9351
  request = request.clone();
9043
9352
  const signatureInputHeader = request.headers.get("Signature-Input");
9044
9353
  if (!signatureInputHeader) {
@@ -9058,6 +9367,29 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
9058
9367
  logger.debug("Failed to verify; no valid signatures found in Signature-Input header.", { header: signatureInputHeader });
9059
9368
  return invalidSignatureResult(null);
9060
9369
  }
9370
+ if (signatureNames.length > maxSignatures) {
9371
+ logger.debug("Verifying only the first {maxSignatures} of the {count} signatures in the Signature-Input header; the rest are ignored.", {
9372
+ maxSignatures,
9373
+ count: signatureNames.length
9374
+ });
9375
+ signatureNames.splice(maxSignatures);
9376
+ }
9377
+ const keyLookups = /* @__PURE__ */ new Map();
9378
+ const refreshedKeyIds = /* @__PURE__ */ new Set();
9379
+ const lookUpKey = (keyId, keyCache) => measureSignatureKeyFetch(meterProvider, "http", () => fetchKeyDetailed(keyId, _fedify_vocab.CryptographicKey, {
9380
+ documentLoader,
9381
+ contextLoader,
9382
+ keyCache,
9383
+ tracerProvider,
9384
+ meterProvider,
9385
+ portableKeyResolvers: createPortableKeyResolvers({
9386
+ documentLoader,
9387
+ contextLoader,
9388
+ keyCache,
9389
+ tracerProvider,
9390
+ meterProvider
9391
+ })
9392
+ }));
9061
9393
  let failure = noSignatureResult();
9062
9394
  let failureAlgorithm;
9063
9395
  const setFailure = (result, algorithm) => {
@@ -9131,94 +9463,40 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
9131
9463
  setFailure(invalidSignatureResult(null));
9132
9464
  continue;
9133
9465
  }
9134
- const { key, cached, fetchError } = await measureSignatureKeyFetch(meterProvider, "http", () => fetchKeyDetailed(keyId, _fedify_vocab.CryptographicKey, {
9135
- documentLoader,
9136
- contextLoader,
9137
- keyCache,
9138
- tracerProvider,
9139
- meterProvider,
9140
- portableKeyResolvers: createPortableKeyResolvers({
9141
- documentLoader,
9142
- contextLoader,
9143
- keyCache,
9144
- tracerProvider,
9145
- meterProvider
9146
- })
9147
- }));
9148
- if (fetchError != null) {
9149
- setFailure(keyFetchErrorResult(keyId, fetchError));
9150
- continue;
9151
- }
9152
- if (!key) {
9153
- logger.debug("Failed to fetch key: {keyId}", { keyId: sigInput.keyId });
9154
- setFailure(invalidSignatureResult(keyId));
9155
- continue;
9156
- }
9157
- let alg = sigInput.alg?.toLowerCase();
9158
- if (alg == null) {
9159
- if (key.publicKey.algorithm.name === "RSASSA-PKCS1-v1_5") alg = "hash" in key.publicKey.algorithm ? key.publicKey.algorithm.hash === "SHA-512" ? "rsa-v1_5-sha512" : "rsa-v1_5-sha256" : "rsa-v1_5-sha256";
9160
- else if (key.publicKey.algorithm.name === "RSA-PSS") alg = "rsa-pss-sha512";
9161
- else if (key.publicKey.algorithm.name === "ECDSA") alg = "namedCurve" in key.publicKey.algorithm && key.publicKey.algorithm.namedCurve === "P-256" ? "ecdsa-p256-sha256" : "ecdsa-p384-sha384";
9162
- else if (key.publicKey.algorithm.name === "Ed25519") alg = "ed25519";
9466
+ let lookup = keyLookups.get(keyId.href);
9467
+ if (lookup == null) {
9468
+ lookup = await lookUpKey(keyId, keyCache);
9469
+ keyLookups.set(keyId.href, lookup);
9163
9470
  }
9164
- if (alg) span?.setAttribute("http_signatures.algorithm", alg);
9165
- const algorithm = alg && rfc9421AlgorithmMap[alg];
9166
- const candidateAlgorithm = algorithm ? alg : void 0;
9167
- if (!algorithm) {
9168
- logger.debug("Failed to verify; unsupported algorithm: {algorithm}", {
9169
- algorithm: sigInput.alg,
9170
- supported: Object.keys(rfc9421AlgorithmMap)
9171
- });
9172
- setFailure(invalidSignatureResult(keyId));
9173
- continue;
9174
- }
9175
- let signatureBase;
9176
- try {
9177
- signatureBase = createRfc9421SignatureBase(request, sigInput.components, sigInput.parameters);
9178
- } catch (error) {
9179
- logger.debug("Failed to create signature base for verification: {error}", {
9180
- error,
9181
- signatureInput: sigInput
9182
- });
9183
- setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
9184
- continue;
9185
- }
9186
- const signatureBaseBytes = new TextEncoder().encode(signatureBase);
9187
- span?.setAttribute("http_signatures.signature", (0, byte_encodings_hex.encodeHex)(sigBytes));
9188
- try {
9189
- if (await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes)) {
9190
- metricsContext.algorithm = candidateAlgorithm;
9471
+ while (true) {
9472
+ const { key, cached, fetchError } = lookup;
9473
+ if (fetchError != null) {
9474
+ setFailure(keyFetchErrorResult(keyId, fetchError));
9475
+ break;
9476
+ }
9477
+ if (!key) {
9478
+ logger.debug("Failed to fetch key: {keyId}", { keyId: sigInput.keyId });
9479
+ setFailure(invalidSignatureResult(keyId));
9480
+ break;
9481
+ }
9482
+ const result = await verifyRfc9421SignatureWithKey(request, sigInput, sigBytes, key, span);
9483
+ if (result.verified) {
9484
+ metricsContext.algorithm = result.algorithm;
9191
9485
  return {
9192
9486
  verified: true,
9193
9487
  key,
9194
9488
  signatureLabel: sigName
9195
9489
  };
9196
- } else if (cached) {
9490
+ }
9491
+ if (result.mismatched && cached && !refreshedKeyIds.has(keyId.href)) {
9197
9492
  logger.debug("Failed to verify with cached key {keyId}; retrying with fresh key...", { keyId: sigInput.keyId });
9198
- return await verifyRequestRfc9421(originalRequest, span, metricsContext, {
9199
- documentLoader,
9200
- contextLoader,
9201
- timeWindow,
9202
- currentTime,
9203
- keyCache: bypassKeyCacheReads(keyCache),
9204
- spec: "rfc9421",
9205
- meterProvider,
9206
- tracerProvider
9207
- });
9208
- } else {
9209
- logger.debug("Failed to verify signature with fetched key {keyId}; signature invalid.", {
9210
- keyId: sigInput.keyId,
9211
- signatureBase
9212
- });
9213
- setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
9493
+ refreshedKeyIds.add(keyId.href);
9494
+ lookup = await lookUpKey(keyId, bypassKeyCacheReads(keyCache));
9495
+ keyLookups.set(keyId.href, lookup);
9496
+ continue;
9214
9497
  }
9215
- } catch (error) {
9216
- logger.debug("Error during signature verification: {error}", {
9217
- error,
9218
- keyId: sigInput.keyId,
9219
- algorithm: sigInput.alg
9220
- });
9221
- setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
9498
+ setFailure(invalidSignatureResult(keyId), result.algorithm);
9499
+ break;
9222
9500
  }
9223
9501
  }
9224
9502
  metricsContext.algorithm = failureAlgorithm;
@@ -9318,16 +9596,19 @@ async function doubleKnock(request, identity, options = {}) {
9318
9596
  }
9319
9597
  async function doubleKnockInternal(request, identity, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
9320
9598
  const { specDeterminer, log, tracerProvider, signal } = options;
9599
+ signal?.throwIfAborted();
9321
9600
  const maximumRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
9322
9601
  visited.add(request.url);
9323
9602
  const origin = new URL(request.url).origin;
9324
9603
  const firstTrySpec = specDeterminer == null ? "rfc9421" : await specDeterminer.determineSpec(origin);
9604
+ signal?.throwIfAborted();
9325
9605
  const body = options.body !== void 0 ? options.body : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null;
9326
9606
  let signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
9327
9607
  spec: firstTrySpec,
9328
9608
  tracerProvider,
9329
9609
  body
9330
9610
  });
9611
+ signal?.throwIfAborted();
9331
9612
  log?.(signedRequest);
9332
9613
  let response = await fetchDoubleKnockRequest(request, signedRequest, signal);
9333
9614
  if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
@@ -9352,6 +9633,7 @@ async function doubleKnockInternal(request, identity, options, redirected = 0, v
9352
9633
  let fulfilled = false;
9353
9634
  let challengeRequest;
9354
9635
  for (const entry of entries) {
9636
+ signal?.throwIfAborted();
9355
9637
  const rfc9421 = fulfillAcceptSignature(entry, localKeyId, localAlg);
9356
9638
  if (rfc9421 == null) continue;
9357
9639
  logger.debug("Received Accept-Signature challenge; accumulating label {label} and components {components}.", {
@@ -9374,6 +9656,7 @@ async function doubleKnockInternal(request, identity, options, redirected = 0, v
9374
9656
  }
9375
9657
  }
9376
9658
  if (fulfilled && challengeRequest != null) {
9659
+ signal?.throwIfAborted();
9377
9660
  signedRequest = challengeRequest;
9378
9661
  log?.(signedRequest);
9379
9662
  response = await fetch(signedRequest, {
@@ -9398,11 +9681,13 @@ async function doubleKnockInternal(request, identity, options, redirected = 0, v
9398
9681
  status: response.status,
9399
9682
  statusText: response.statusText
9400
9683
  });
9684
+ signal?.throwIfAborted();
9401
9685
  signedRequest = await signRequest(request, identity.privateKey, identity.keyId, {
9402
9686
  spec,
9403
9687
  tracerProvider,
9404
9688
  body
9405
9689
  });
9690
+ signal?.throwIfAborted();
9406
9691
  log?.(signedRequest);
9407
9692
  response = await fetchDoubleKnockRequest(request, signedRequest, signal);
9408
9693
  if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
@@ -9565,6 +9850,12 @@ Object.defineProperty(exports, "generateCryptoKeyPair", {
9565
9850
  return generateCryptoKeyPair;
9566
9851
  }
9567
9852
  });
9853
+ Object.defineProperty(exports, "getAuthenticationOrigin", {
9854
+ enumerable: true,
9855
+ get: function() {
9856
+ return getAuthenticationOrigin;
9857
+ }
9858
+ });
9568
9859
  Object.defineProperty(exports, "getCanonicalPortableId", {
9569
9860
  enumerable: true,
9570
9861
  get: function() {
@@ -9697,6 +9988,12 @@ Object.defineProperty(exports, "isPortableUri", {
9697
9988
  return isPortableUri;
9698
9989
  }
9699
9990
  });
9991
+ Object.defineProperty(exports, "isSameObjectId", {
9992
+ enumerable: true,
9993
+ get: function() {
9994
+ return isSameObjectId;
9995
+ }
9996
+ });
9700
9997
  Object.defineProperty(exports, "isSamePublicKey", {
9701
9998
  enumerable: true,
9702
9999
  get: function() {
@@ -9859,6 +10156,12 @@ Object.defineProperty(exports, "validateCryptoKey", {
9859
10156
  return validateCryptoKey;
9860
10157
  }
9861
10158
  });
10159
+ Object.defineProperty(exports, "validateMaxSignatures", {
10160
+ enumerable: true,
10161
+ get: function() {
10162
+ return validateMaxSignatures;
10163
+ }
10164
+ });
9862
10165
  Object.defineProperty(exports, "verifyCompactJsonLd", {
9863
10166
  enumerable: true,
9864
10167
  get: function() {