@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
@@ -1,8 +1,8 @@
1
1
  import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-CizGaYeG.mjs";
5
- import { c as isPortableId, r as getPortableDid, t as getCanonicalPortableId } from "./portable-key-id-Bg259c4Z.mjs";
4
+ import { n as version, t as name } from "./deno-EopndaNb.mjs";
5
+ import { i as getPortableDid, l as isPortableId, n as getCanonicalPortableId } from "./portable-key-id-Ca30-hOa.mjs";
6
6
  import { t as ActivityListenerSet } from "./activity-listener-C-BSbKI7.mjs";
7
7
  import { getLogger } from "@logtape/logtape";
8
8
  import { Router, RouterError, assertPath } from "@fedify/uri-template";
@@ -89,7 +89,7 @@ var FederationBuilderImpl = class {
89
89
  * specified.
90
90
  */
91
91
  async build(options) {
92
- const { FederationImpl } = await import("./middleware-BmqEWsWP.mjs");
92
+ const { FederationImpl } = await import("./middleware-Ck9yghxs.mjs");
93
93
  const f = new FederationImpl(options);
94
94
  const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive;
95
95
  f.router = this.router.clone();
@@ -47,7 +47,7 @@ var CircuitBreaker = class {
47
47
  };
48
48
  let lastConflictingState;
49
49
  for (let attempt = 0; attempt < 10; attempt++) {
50
- const oldState = await this.#get(remoteHost);
50
+ const { state: oldState, raw: oldValue } = await this.#get(remoteHost);
51
51
  if (oldState == null || oldState.state === "closed") return {
52
52
  type: "send",
53
53
  probe: false
@@ -73,7 +73,7 @@ var CircuitBreaker = class {
73
73
  state: "half-open",
74
74
  halfOpened: now.toString()
75
75
  };
76
- if (await this.#replace(remoteHost, oldState, newState)) return {
76
+ if (await this.#replace(remoteHost, oldValue, newState)) return {
77
77
  type: "send",
78
78
  probe: true
79
79
  };
@@ -95,7 +95,7 @@ var CircuitBreaker = class {
95
95
  state: "half-open",
96
96
  halfOpened: now.toString()
97
97
  };
98
- if (await this.#replace(remoteHost, oldState, newState)) {
98
+ if (await this.#replace(remoteHost, oldValue, newState)) {
99
99
  await this.#notifyStateChange(remoteHost, "open", "half-open");
100
100
  return {
101
101
  type: "send",
@@ -122,9 +122,9 @@ var CircuitBreaker = class {
122
122
  async recordSuccess(remoteHost) {
123
123
  this.#sweepLegacyStates();
124
124
  for (let attempt = 0; attempt < 10; attempt++) {
125
- const oldState = await this.#get(remoteHost);
125
+ const { state: oldState, raw: oldValue } = await this.#get(remoteHost);
126
126
  if (oldState == null) return void 0;
127
- if (await this.#replace(remoteHost, oldState, void 0)) {
127
+ if (await this.#replace(remoteHost, oldValue, void 0)) {
128
128
  if (oldState.state !== "closed") {
129
129
  await this.#notifyStateChange(remoteHost, oldState.state, "closed");
130
130
  return {
@@ -144,7 +144,7 @@ var CircuitBreaker = class {
144
144
  this.#sweepLegacyStates();
145
145
  const now = this.#now();
146
146
  for (let attempt = 0; attempt < 10; attempt++) {
147
- const oldState = await this.#get(remoteHost);
147
+ const { state: oldState, raw: oldValue } = await this.#get(remoteHost);
148
148
  if (oldState?.state === "open") return void 0;
149
149
  const oldFailures = oldState?.failures.map(Temporal.Instant.from) ?? [];
150
150
  const failures = this.#options.pruneFailures([...oldFailures, now], now);
@@ -161,7 +161,7 @@ var CircuitBreaker = class {
161
161
  state: "closed",
162
162
  failures: failures.map((t) => t.toString())
163
163
  };
164
- if (await this.#replace(remoteHost, oldState, newState)) {
164
+ if (await this.#replace(remoteHost, oldValue, newState)) {
165
165
  if (transition != null) {
166
166
  await this.#notifyStateChange(remoteHost, transition[0], transition[1]);
167
167
  return {
@@ -190,7 +190,7 @@ var CircuitBreaker = class {
190
190
  }
191
191
  async getState(remoteHost) {
192
192
  this.#sweepLegacyStates();
193
- return stripStoredCircuitBreakerState(await this.#get(remoteHost));
193
+ return stripStoredCircuitBreakerState((await this.#get(remoteHost)).state);
194
194
  }
195
195
  /**
196
196
  * The currently running background legacy sweep, if any.
@@ -310,9 +310,14 @@ var CircuitBreaker = class {
310
310
  return Temporal.Instant.compare(expiresAt, retryAt) < 0 ? expiresAt : retryAt;
311
311
  }
312
312
  async #get(remoteHost) {
313
- return parseStoredCircuitBreakerKvState(await this.#kv.get(this.#key(remoteHost)));
313
+ const value = await this.#kv.get(this.#key(remoteHost));
314
+ const state = parseStoredCircuitBreakerKvState(value);
315
+ return {
316
+ state,
317
+ raw: state == null ? void 0 : value
318
+ };
314
319
  }
315
- async #replace(remoteHost, oldState, newState) {
320
+ async #replace(remoteHost, oldValue, newState) {
316
321
  const key = this.#key(remoteHost);
317
322
  const storedState = newState == null ? void 0 : markCircuitBreakerState(newState);
318
323
  if (this.#kv.cas == null) {
@@ -320,7 +325,7 @@ var CircuitBreaker = class {
320
325
  else await this.#kv.set(key, storedState, this.#setOptions());
321
326
  return true;
322
327
  }
323
- return await this.#kv.cas(key, oldState, storedState, storedState == null ? void 0 : this.#setOptions());
328
+ return await this.#kv.cas(key, oldValue, storedState, storedState == null ? void 0 : this.#setOptions());
324
329
  }
325
330
  #setOptions() {
326
331
  return this.#options.stateTtl == null ? void 0 : { ttl: this.#options.stateTtl };
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { $t as ActivityTransformer, n as Context } from "../context-L0Fnib9j.cjs";
2
+ import { $t as ActivityTransformer, n as Context } from "../context-CDcAgm4l.cjs";
3
3
  import { Activity } from "@fedify/vocab";
4
4
 
5
5
  //#region src/compat/transformers.d.ts
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { $t as ActivityTransformer, n as Context } from "../context-BykNUf04.js";
2
+ import { $t as ActivityTransformer, n as Context } from "../context-9T--NSo4.js";
3
3
  import { Activity } from "@fedify/vocab";
4
4
 
5
5
  //#region src/compat/transformers.d.ts
@@ -5,7 +5,7 @@ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
6
6
  import { t as assertInstanceOf } from "../assert_instance_of-mrXWWNY-.mjs";
7
7
  import { t as assert } from "../assert-D1Eafmd_.mjs";
8
- import { E as autoIdAssigner, T as actorDehydrator, n as FederationImpl } from "../middleware-B99JjvFn.mjs";
8
+ import { E as autoIdAssigner, T as actorDehydrator, n as FederationImpl } from "../middleware-Bdu2-OPD.mjs";
9
9
  import { Follow, Person } from "@fedify/vocab";
10
10
  import { test } from "@fedify/fixture";
11
11
  //#region src/compat/transformers.test.ts
@@ -1,10 +1,10 @@
1
1
  import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { c as isPortableId, i as getRawCanonicalPortableId, t as getCanonicalPortableId } from "./portable-key-id-Bg259c4Z.mjs";
4
+ import { a as getRawCanonicalPortableId, l as isPortableId, n as getCanonicalPortableId } from "./portable-key-id-Ca30-hOa.mjs";
5
5
  import { n as preloadedOnlyDocumentLoader } from "./public-audience--klBIOk1.mjs";
6
- import { c as getNormalizationContextLoader } from "./ld-CyliEU48.mjs";
7
- import { l as verifyPortableObjectProofPolicy, n as classifyFep2277CoreType, o as verifyMapLocalProof } from "./proof-CRTPWbVh.mjs";
6
+ import { c as getNormalizationContextLoader } from "./ld-oQj6j_hP.mjs";
7
+ import { l as verifyPortableObjectProofPolicy, n as classifyFep2277CoreType, o as verifyMapLocalProof } from "./proof-C9115DX_.mjs";
8
8
  import { parseIri, preloadedContexts } from "@fedify/vocab-runtime";
9
9
  import jsonld from "@fedify/vocab-runtime/jsonld";
10
10
  //#region src/sig/compound-proof.ts
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { o as JsonValue, s as NodeInfo, t as GetNodeInfoOptions } from "./client-D-XP_Ccb.js";
3
- import { o as VerifyRequestFailureReason, t as HttpMessageSignaturesSpec } from "./http-Cok5-vIC.js";
3
+ import { o as VerifyRequestFailureReason, t as HttpMessageSignaturesSpec } from "./http-DGx0KOws.js";
4
4
  import { n as GetKeyOwnerOptions } from "./owner-BSzr5-I8.js";
5
5
  import { n as KvStore, t as KvKey } from "./kv-BhNbN8Ed.js";
6
6
  import { Router } from "@fedify/uri-template";
@@ -1485,6 +1485,8 @@ interface FederationOrigin {
1485
1485
  * @returns A new {@link Federation} instance.
1486
1486
  * @throws {TypeError} If benchmark mode and `meterProvider` are both
1487
1487
  * specified.
1488
+ * @throws {RangeError} If {@link FederationOptions.maxHttpSignatures} is not
1489
+ * a positive integer or `Infinity`.
1488
1490
  * @since 0.10.0
1489
1491
  */
1490
1492
  declare function createFederation<TContextData>(options: FederationOptions<TContextData>): Federation<TContextData>;
@@ -2309,6 +2311,35 @@ interface FederationOptions<TContextData> {
2309
2311
  */
2310
2312
  userAgent?: GetUserAgentOptions | string;
2311
2313
  /**
2314
+ * The timeout for each call of the built-in document loader, context
2315
+ * loader, and authenticated document loader, e.g., when Fedify fetches
2316
+ * a key to verify a signature. The timeout is shared by all the steps of
2317
+ * a call, including every redirect it follows, retries, and reading the
2318
+ * response body. It does not cover the time spent reading from or
2319
+ * writing to the cache.
2320
+ *
2321
+ * A timed-out call throws a `FetchError` without a response, whose
2322
+ * `cause` is a `DOMException` named `"TimeoutError"`, so that, e.g.,
2323
+ * a key fetch that times out is reported as a `keyFetchError` and cached
2324
+ * like other failures to fetch a key.
2325
+ *
2326
+ * It does not affect loaders made by
2327
+ * {@link FederationOptions.documentLoaderFactory},
2328
+ * {@link FederationOptions.contextLoaderFactory}, or
2329
+ * {@link FederationOptions.authenticatedDocumentLoaderFactory}.
2330
+ * Note that if only {@link FederationOptions.documentLoaderFactory} is
2331
+ * set, it also makes context loaders.
2332
+ *
2333
+ * Set it to `null` to turn off the timeout.
2334
+ *
2335
+ * 10 seconds by default.
2336
+ * @throws {RangeError} If the duration is not positive, is longer than
2337
+ * about 24.8 days, or is given in calendar units
2338
+ * such as months.
2339
+ * @since 2.4.0
2340
+ */
2341
+ documentLoaderTimeout?: Temporal.Duration | Temporal.DurationLike | null;
2342
+ /**
2312
2343
  * A callback that handles errors during outbox processing. Note that this
2313
2344
  * callback can be called multiple times for the same activity, because
2314
2345
  * the delivery is retried according to the backoff schedule until it
@@ -2337,6 +2368,29 @@ interface FederationOptions<TContextData> {
2337
2368
  */
2338
2369
  signatureTimeWindow?: Temporal.Duration | Temporal.DurationLike | false;
2339
2370
  /**
2371
+ * The maximum number of [RFC 9421] signatures of an incoming request to
2372
+ * verify, e.g., in the inbox and in
2373
+ * {@link RequestContext.getSignedKey}. A request can carry several
2374
+ * signatures, each of which may make Fedify fetch the key that it names,
2375
+ * so only the first ones in the order of the `Signature-Input` header are
2376
+ * verified, and the rest are ignored as if they were absent. Every
2377
+ * signature among the first ones counts, even if it fails before its key
2378
+ * is fetched. See also the `maxSignatures` option of `verifyRequest()`.
2379
+ *
2380
+ * It has to be a positive integer, or `Infinity` to verify every
2381
+ * signature, which lets a single request make Fedify fetch any number of
2382
+ * keys. Draft-cavage HTTP Signatures carry a single signature, so this
2383
+ * option does not affect them.
2384
+ *
2385
+ * Three by default.
2386
+ *
2387
+ * [RFC 9421]: https://www.rfc-editor.org/rfc/rfc9421
2388
+ * @throws {RangeError} Thrown when the federation is created if the value
2389
+ * is not a positive integer or `Infinity`.
2390
+ * @since 2.4.0
2391
+ */
2392
+ maxHttpSignatures?: number;
2393
+ /**
2340
2394
  * Whether to skip HTTP Signatures verification for incoming activities.
2341
2395
  * This is useful for testing purposes, but should not be used in production.
2342
2396
  *
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { o as JsonValue, s as NodeInfo, t as GetNodeInfoOptions } from "./client-D-XP_Ccb.cjs";
3
- import { o as VerifyRequestFailureReason, t as HttpMessageSignaturesSpec } from "./http-DC1cqAQ2.cjs";
3
+ import { o as VerifyRequestFailureReason, t as HttpMessageSignaturesSpec } from "./http-BrwKINQn.cjs";
4
4
  import { n as GetKeyOwnerOptions } from "./owner-BF0L9cMt.cjs";
5
5
  import { n as KvStore, t as KvKey } from "./kv-BhNbN8Ed.cjs";
6
6
  import { Activity, Actor, Collection, CryptographicKey, Hashtag, Link, LookupObjectOptions, Multikey, Object as Object$1, Recipient, Tombstone, TraverseCollectionOptions } from "@fedify/vocab";
@@ -1483,6 +1483,8 @@ interface FederationOrigin {
1483
1483
  * @returns A new {@link Federation} instance.
1484
1484
  * @throws {TypeError} If benchmark mode and `meterProvider` are both
1485
1485
  * specified.
1486
+ * @throws {RangeError} If {@link FederationOptions.maxHttpSignatures} is not
1487
+ * a positive integer or `Infinity`.
1486
1488
  * @since 0.10.0
1487
1489
  */
1488
1490
  declare function createFederation<TContextData>(options: FederationOptions<TContextData>): Federation<TContextData>;
@@ -2307,6 +2309,35 @@ interface FederationOptions<TContextData> {
2307
2309
  */
2308
2310
  userAgent?: GetUserAgentOptions | string;
2309
2311
  /**
2312
+ * The timeout for each call of the built-in document loader, context
2313
+ * loader, and authenticated document loader, e.g., when Fedify fetches
2314
+ * a key to verify a signature. The timeout is shared by all the steps of
2315
+ * a call, including every redirect it follows, retries, and reading the
2316
+ * response body. It does not cover the time spent reading from or
2317
+ * writing to the cache.
2318
+ *
2319
+ * A timed-out call throws a `FetchError` without a response, whose
2320
+ * `cause` is a `DOMException` named `"TimeoutError"`, so that, e.g.,
2321
+ * a key fetch that times out is reported as a `keyFetchError` and cached
2322
+ * like other failures to fetch a key.
2323
+ *
2324
+ * It does not affect loaders made by
2325
+ * {@link FederationOptions.documentLoaderFactory},
2326
+ * {@link FederationOptions.contextLoaderFactory}, or
2327
+ * {@link FederationOptions.authenticatedDocumentLoaderFactory}.
2328
+ * Note that if only {@link FederationOptions.documentLoaderFactory} is
2329
+ * set, it also makes context loaders.
2330
+ *
2331
+ * Set it to `null` to turn off the timeout.
2332
+ *
2333
+ * 10 seconds by default.
2334
+ * @throws {RangeError} If the duration is not positive, is longer than
2335
+ * about 24.8 days, or is given in calendar units
2336
+ * such as months.
2337
+ * @since 2.4.0
2338
+ */
2339
+ documentLoaderTimeout?: Temporal.Duration | Temporal.DurationLike | null;
2340
+ /**
2310
2341
  * A callback that handles errors during outbox processing. Note that this
2311
2342
  * callback can be called multiple times for the same activity, because
2312
2343
  * the delivery is retried according to the backoff schedule until it
@@ -2335,6 +2366,29 @@ interface FederationOptions<TContextData> {
2335
2366
  */
2336
2367
  signatureTimeWindow?: Temporal.Duration | Temporal.DurationLike | false;
2337
2368
  /**
2369
+ * The maximum number of [RFC 9421] signatures of an incoming request to
2370
+ * verify, e.g., in the inbox and in
2371
+ * {@link RequestContext.getSignedKey}. A request can carry several
2372
+ * signatures, each of which may make Fedify fetch the key that it names,
2373
+ * so only the first ones in the order of the `Signature-Input` header are
2374
+ * verified, and the rest are ignored as if they were absent. Every
2375
+ * signature among the first ones counts, even if it fails before its key
2376
+ * is fetched. See also the `maxSignatures` option of `verifyRequest()`.
2377
+ *
2378
+ * It has to be a positive integer, or `Infinity` to verify every
2379
+ * signature, which lets a single request make Fedify fetch any number of
2380
+ * keys. Draft-cavage HTTP Signatures carry a single signature, so this
2381
+ * option does not affect them.
2382
+ *
2383
+ * Three by default.
2384
+ *
2385
+ * [RFC 9421]: https://www.rfc-editor.org/rfc/rfc9421
2386
+ * @throws {RangeError} Thrown when the federation is created if the value
2387
+ * is not a positive integer or `Infinity`.
2388
+ * @since 2.4.0
2389
+ */
2390
+ maxHttpSignatures?: number;
2391
+ /**
2338
2392
  * Whether to skip HTTP Signatures verification for incoming activities.
2339
2393
  * This is useful for testing purposes, but should not be used in production.
2340
2394
  *
@@ -3,6 +3,6 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  //#region deno.json
5
5
  var name = "@fedify/fedify";
6
- var version = "2.4.0-dev.2164+db2b8571";
6
+ var version = "2.4.0-dev.2190+be3a8be0";
7
7
  //#endregion
8
8
  export { version as n, name as t };
@@ -1,11 +1,11 @@
1
1
  import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { c as validateCryptoKey } from "./key-D5awqp7u.mjs";
5
- import { n as doubleKnock } from "./http-DRH0n8TD.mjs";
4
+ import { c as validateCryptoKey } from "./key-DyXf88bq.mjs";
5
+ import { n as doubleKnock } from "./http-Y1ObT1A8.mjs";
6
6
  import { getLogger } from "@logtape/logtape";
7
7
  import { curry } from "es-toolkit";
8
- import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, validatePublicUrl } from "@fedify/vocab-runtime";
8
+ import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, resolveDocumentLoaderTimeout, validatePublicUrl, withDocumentLoaderTimeout } from "@fedify/vocab-runtime";
9
9
  //#region src/utils/docloader.ts
10
10
  const logger = getLogger([
11
11
  "fedify",
@@ -19,15 +19,19 @@ const DEFAULT_MAX_REDIRECTION = 20;
19
19
  * the fetched documents.
20
20
  * At most 20 HTTP redirects and alternate document links are followed in total
21
21
  * per call. Revisiting a URL within that chain throws a {@link FetchError}.
22
+ * Each call, including its double-knocking retries, times out after
23
+ * 10 seconds by default; see {@link DocumentLoaderFactoryOptions.timeout}.
22
24
  * @param identity The identity to get the document loader for.
23
25
  * The actor's key pair.
24
26
  * @param options The options for the document loader.
25
27
  * @returns The authenticated document loader.
26
28
  * @throws {TypeError} If the key is invalid or unsupported.
29
+ * @throws {RangeError} If the `timeout` option is invalid.
27
30
  * @since 0.4.0
28
31
  */
29
- function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, tracerProvider } = {}) {
32
+ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, timeout, tracerProvider } = {}) {
30
33
  validateCryptoKey(identity.privateKey);
34
+ const resolvedTimeout = resolveDocumentLoaderTimeout(timeout);
31
35
  async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
32
36
  options?.signal?.throwIfAborted();
33
37
  let currentUrl = new URL(url).href;
@@ -53,27 +57,31 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedi
53
57
  currentUrl = nextUrl;
54
58
  }
55
59
  });
60
+ options?.signal?.throwIfAborted();
56
61
  return getRemoteDocument(currentUrl, response, (alternateUrl) => {
57
62
  follow(alternateUrl);
58
63
  return load(alternateUrl, options, redirected, visited);
59
64
  }, options);
60
65
  }
61
66
  async function validateUrl(url, options) {
62
- if (!allowPrivateAddress) try {
63
- await validatePublicUrl(url);
64
- } catch (error) {
65
- if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
66
- url,
67
- error
68
- });
69
- else logger[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
70
- url,
71
- error
72
- });
73
- throw error;
67
+ if (!allowPrivateAddress) {
68
+ try {
69
+ await validatePublicUrl(url);
70
+ } catch (error) {
71
+ if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
72
+ url,
73
+ error
74
+ });
75
+ else logger[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
76
+ url,
77
+ error
78
+ });
79
+ throw error;
80
+ }
81
+ options?.signal?.throwIfAborted();
74
82
  }
75
83
  }
76
- return (url, options) => load(url, options);
84
+ return withDocumentLoaderTimeout((url, options) => load(url, options), resolvedTimeout);
77
85
  }
78
86
  //#endregion
79
87
  export { getAuthenticatedDocumentLoader as t };
@@ -6,7 +6,7 @@ import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
6
6
  import { t as assertExists } from "../assert_exists-cffSpJ7X.mjs";
7
7
  import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
8
8
  import "../std__assert-DvMDRPes.mjs";
9
- import { r as createFederationBuilder } from "../builder-BqRmnx7p.mjs";
9
+ import { r as createFederationBuilder } from "../builder-CFF0YNTp.mjs";
10
10
  import { DisallowedOperatorError, DisallowedVarSpecModifierError, DuplicateRouteVariableError, RouteTemplateOptionsNotMatchedError, RouterError } from "@fedify/uri-template";
11
11
  import { Activity, Note, Person } from "@fedify/vocab";
12
12
  import { test } from "@fedify/fixture";
@@ -5,7 +5,8 @@ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
6
6
  import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
7
7
  import "../std__assert-DvMDRPes.mjs";
8
- import { n as normalizeCircuitBreakerOptions, r as parseCircuitBreakerKvState, t as CircuitBreaker } from "../circuit-breaker-C5NqoLBJ.mjs";
8
+ import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
9
+ import { n as normalizeCircuitBreakerOptions, r as parseCircuitBreakerKvState, t as CircuitBreaker } from "../circuit-breaker-XJ53md4S.mjs";
9
10
  import { t as markCircuitBreakerLegacySweepDone } from "../circuit-breaker-test-utils-DW6YC_Pe.mjs";
10
11
  import { test } from "@fedify/fixture";
11
12
  //#region src/federation/circuit-breaker.test.ts
@@ -26,6 +27,54 @@ var CountingCasKvStore = class extends MemoryKvStore {
26
27
  return super.cas(key, expectedValue, newValue, options);
27
28
  }
28
29
  };
30
+ var EncodedCasKvStore = class extends MemoryKvStore {
31
+ async cas(key, expectedValue, newValue, options) {
32
+ const current = await this.get(key);
33
+ if (JSON.stringify(current) !== JSON.stringify(expectedValue)) return false;
34
+ if (newValue === void 0) await this.delete(key);
35
+ else await this.set(key, newValue, options);
36
+ return true;
37
+ }
38
+ };
39
+ test("CircuitBreaker uses raw encoded CAS expectations", async () => {
40
+ const kv = new EncodedCasKvStore();
41
+ await markCircuitBreakerLegacySweepDone(kv);
42
+ let now = Temporal.Instant.from("2026-05-25T00:00:00Z");
43
+ const circuit = new CircuitBreaker({
44
+ kv,
45
+ prefix: ["_fedify", "circuit"],
46
+ now: () => now,
47
+ options: {
48
+ failureThreshold: 1,
49
+ recoveryDelay: { seconds: 30 }
50
+ }
51
+ });
52
+ const key = [
53
+ "_fedify",
54
+ "circuit",
55
+ "remote.example"
56
+ ];
57
+ await circuit.recordFailure("remote.example");
58
+ now = now.add({ seconds: 30 });
59
+ assertEquals((await circuit.beforeSend("remote.example", {})).type, "send");
60
+ assertEquals((await circuit.recordSuccess("remote.example"))?.newState, "closed");
61
+ const oldOrder = {
62
+ state: "half-open",
63
+ failures: ["2026-05-25T00:00:00Z"],
64
+ opened: "2026-05-25T00:00:00Z",
65
+ __fedifyCircuitBreakerStateVersion: 1,
66
+ halfOpened: "2026-05-25T00:00:00Z"
67
+ };
68
+ await kv.set(key, oldOrder);
69
+ assertEquals((await circuit.recordFailure("remote.example"))?.newState, "open");
70
+ await kv.set(key, oldOrder);
71
+ assertEquals((await circuit.recordSuccess("remote.example"))?.newState, "closed");
72
+ await kv.set(key, oldOrder);
73
+ assertEquals((await circuit.beforeSend("remote.example", {})).type, "send");
74
+ await kv.set(key, { __fedifyDeletingCircuitBreakerLegacyState: true });
75
+ await assertRejects(() => circuit.recordFailure("remote.example"));
76
+ assertEquals(await kv.get(key), { __fedifyDeletingCircuitBreakerLegacyState: true });
77
+ });
29
78
  var CountingSetKvStore = class {
30
79
  #store = new MemoryKvStore();
31
80
  keys = [];
@@ -6,11 +6,11 @@ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
6
6
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
7
7
  import "../std__assert-DvMDRPes.mjs";
8
8
  import { t as assert } from "../assert-D1Eafmd_.mjs";
9
- import { n as exportJwk } from "../key-D5awqp7u.mjs";
9
+ import { n as exportJwk } from "../key-DyXf88bq.mjs";
10
10
  import { r as normalizeOutgoingActivityJsonLd } from "../outgoing-jsonld-4IJqPYos.mjs";
11
- import { a as signObject, d as verifyProof } from "../proof-CRTPWbVh.mjs";
11
+ import { a as signObject, d as verifyProof } from "../proof-C9115DX_.mjs";
12
12
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
13
- import { n as FederationImpl } from "../middleware-B99JjvFn.mjs";
13
+ import { n as FederationImpl } from "../middleware-Bdu2-OPD.mjs";
14
14
  import { Create, DataIntegrityProof, Note } from "@fedify/vocab";
15
15
  import { mockDocumentLoader, test } from "@fedify/fixture";
16
16
  import { exportDidKey, parseIri } from "@fedify/vocab-runtime";
@@ -5,10 +5,10 @@ import { r as createRequestContext, t as createInboxContext } from "../context-B
5
5
  import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
6
6
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
7
7
  import "../std__assert-DvMDRPes.mjs";
8
- import { a as compactJsonLd, m as verifyCompactJsonLd, p as signJsonLd } from "../ld-CyliEU48.mjs";
9
- import { l as signRequest } from "../http-DRH0n8TD.mjs";
8
+ import { a as compactJsonLd, m as verifyCompactJsonLd, p as signJsonLd } from "../ld-oQj6j_hP.mjs";
9
+ import { l as signRequest } from "../http-Y1ObT1A8.mjs";
10
10
  import { a as rsaPrivateKey3, c as rsaPublicKey3, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
11
- import { f as handleInbox, o as createFederation } from "../middleware-B99JjvFn.mjs";
11
+ import { f as handleInbox, o as createFederation } from "../middleware-Bdu2-OPD.mjs";
12
12
  import { t as ActivityListenerSet } from "../activity-listener-C-BSbKI7.mjs";
13
13
  import { t as map_local_create_note_default } from "../map-local-create-note-B0SeXJms.mjs";
14
14
  import { Create, CryptographicKey, Multikey, Person, Update } from "@fedify/vocab";
@@ -10,12 +10,12 @@ import "../std__assert-DvMDRPes.mjs";
10
10
  import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
11
11
  import { t as assert } from "../assert-D1Eafmd_.mjs";
12
12
  import { r as parseAcceptSignature } from "../accept-CPVqki78.mjs";
13
- import { o as generateCryptoKeyPair } from "../key-D5awqp7u.mjs";
14
- import { a as compactJsonLd, p as signJsonLd } from "../ld-CyliEU48.mjs";
15
- import { a as signObject } from "../proof-CRTPWbVh.mjs";
16
- import { l as signRequest } from "../http-DRH0n8TD.mjs";
13
+ import { o as generateCryptoKeyPair } from "../key-DyXf88bq.mjs";
14
+ import { a as compactJsonLd, p as signJsonLd } from "../ld-oQj6j_hP.mjs";
15
+ import { a as signObject } from "../proof-C9115DX_.mjs";
16
+ import { l as signRequest } from "../http-Y1ObT1A8.mjs";
17
17
  import { a as rsaPrivateKey3, c as rsaPublicKey3, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
18
- import { d as handleCustomCollection, f as handleInbox, g as respondWithObjectIfAcceptable, h as respondWithObject, l as handleActor, m as handleOutbox, o as createFederation, p as handleObject, u as handleCollection } from "../middleware-B99JjvFn.mjs";
18
+ import { d as handleCustomCollection, f as handleInbox, g as respondWithObjectIfAcceptable, h as respondWithObject, l as handleActor, m as handleOutbox, o as createFederation, p as handleObject, u as handleCollection } from "../middleware-Bdu2-OPD.mjs";
19
19
  import { t as ActivityListenerSet } from "../activity-listener-C-BSbKI7.mjs";
20
20
  import { Activity, Create, CryptographicKey, Multikey, Note, Person, Tombstone } from "@fedify/vocab";
21
21
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
@@ -10,8 +10,8 @@ import "../std__assert-DvMDRPes.mjs";
10
10
  import { t as assertStrictEquals } from "../assert_strict_equals-CYvBmkpC.mjs";
11
11
  import { t as assert } from "../assert-D1Eafmd_.mjs";
12
12
  import { r as ed25519PublicKey } from "../keys-DIJNHW-Y.mjs";
13
- import { o as createFederation, x as parseHashlinkGatewayRequest } from "../middleware-B99JjvFn.mjs";
14
- import { r as createFederationBuilder } from "../builder-BqRmnx7p.mjs";
13
+ import { o as createFederation, x as parseHashlinkGatewayRequest } from "../middleware-Bdu2-OPD.mjs";
14
+ import { r as createFederationBuilder } from "../builder-CFF0YNTp.mjs";
15
15
  import { n as RouterError } from "../router-u5O5KCQr.mjs";
16
16
  import { Note } from "@fedify/vocab";
17
17
  import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
@@ -4,9 +4,9 @@ globalThis.addEventListener = () => {};
4
4
  import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
6
6
  import "../std__assert-DvMDRPes.mjs";
7
- import { a as signObject } from "../proof-CRTPWbVh.mjs";
7
+ import { a as signObject } from "../proof-C9115DX_.mjs";
8
8
  import { n as ed25519PrivateKey, r as ed25519PublicKey, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
9
- import { o as createFederation } from "../middleware-B99JjvFn.mjs";
9
+ import { o as createFederation } from "../middleware-Bdu2-OPD.mjs";
10
10
  import { Create, Follow, Person } from "@fedify/vocab";
11
11
  import { mockDocumentLoader, test } from "@fedify/fixture";
12
12
  //#region src/federation/idempotency.test.ts
@@ -6,7 +6,7 @@ import { t as ManualClockKvStore } from "../kv-DBRWVx3Q.mjs";
6
6
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
7
7
  import { t as assertInstanceOf } from "../assert_instance_of-mrXWWNY-.mjs";
8
8
  import { t as assert } from "../assert-D1Eafmd_.mjs";
9
- import { t as KvKeyCache } from "../keycache-PiOM9pM0.mjs";
9
+ import { t as KvKeyCache } from "../keycache-Da8A8mZa.mjs";
10
10
  import { CryptographicKey, Multikey } from "@fedify/vocab";
11
11
  import { test } from "@fedify/fixture";
12
12
  //#region src/federation/keycache.test.ts
@@ -4,7 +4,7 @@ globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
5
5
  import "../std__assert-DvMDRPes.mjs";
6
6
  import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
7
- import { _ as recordKeyLookup, a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, m as recordDocumentFetch, p as recordDocumentCache, r as getFederationMetrics, t as classifyFetchError, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "../metrics-CFBEehTc.mjs";
7
+ import { _ as recordKeyLookup, a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, m as recordDocumentFetch, p as recordDocumentCache, r as getFederationMetrics, t as classifyFetchError, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "../metrics-zMe834ID.mjs";
8
8
  import { createTestMeterProvider, test } from "@fedify/fixture";
9
9
  import { FetchError } from "@fedify/vocab-runtime";
10
10
  import { DataPointType, MeterProvider, MetricReader } from "@opentelemetry/sdk-metrics";