@fedify/fedify 2.3.8 → 2.3.9

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 (73) hide show
  1. package/dist/{builder-CDForRSL.mjs → builder-Dm72xfCy.mjs} +2 -2
  2. package/dist/{client-DYDoDpMa.mjs → client-Ccbsek6u.mjs} +7 -3
  3. package/dist/compat/transformers.test.mjs +1 -1
  4. package/dist/{context-BepjCFHN.mjs → context-CxbCIptr.mjs} +2 -1
  5. package/dist/{deno--DCBC7WG.mjs → deno-Deo5dZf_.mjs} +1 -1
  6. package/dist/docloader-BAVVKXPl.mjs +79 -0
  7. package/dist/federation/builder.test.mjs +2 -2
  8. package/dist/federation/circuit-breaker.test.mjs +1 -1
  9. package/dist/federation/collection.test.mjs +1 -1
  10. package/dist/federation/handler.test.mjs +7 -7
  11. package/dist/federation/idempotency.test.mjs +3 -3
  12. package/dist/federation/kv.test.mjs +1 -1
  13. package/dist/federation/metrics.test.mjs +2 -2
  14. package/dist/federation/middleware.test.mjs +187 -8
  15. package/dist/federation/mod.cjs +1 -1
  16. package/dist/federation/mod.js +1 -1
  17. package/dist/federation/mq.test.mjs +98 -43
  18. package/dist/federation/negotiation.test.mjs +1 -1
  19. package/dist/federation/router.test.mjs +1 -1
  20. package/dist/federation/send.test.mjs +85 -4
  21. package/dist/federation/temporal.test.mjs +2 -2
  22. package/dist/federation/webfinger.test.mjs +3 -3
  23. package/dist/{http-ClEDLGpr.mjs → http-BqObaL32.mjs} +8 -4
  24. package/dist/{http-ChVf88XZ.cjs → http-CjvY7wBM.cjs} +6 -2
  25. package/dist/{http-BKJYj9oz.js → http-DuWPL6ic.js} +6 -2
  26. package/dist/{key-C-7sqo8D.mjs → key-D1TEfV4d.mjs} +2 -2
  27. package/dist/{kv-cache-DFBOAyNj.cjs → kv-cache-C_4ufNbl.cjs} +34 -8
  28. package/dist/{kv-cache-C5FK4PLG.mjs → kv-cache-DGnB9ZX_.mjs} +1 -1
  29. package/dist/{kv-cache-C_w70bFh.js → kv-cache-w0De7ODp.js} +35 -9
  30. package/dist/{ld-CtFHB6rU.mjs → ld-DnGqAKWi.mjs} +4 -4
  31. package/dist/{metrics-FDcYXRP5.mjs → metrics-jUJmtE2u.mjs} +1 -1
  32. package/dist/{middleware-DunPXFJL.js → middleware-A-SjayRO.js} +19 -5
  33. package/dist/{middleware-BdDtzhXb.cjs → middleware-BW5W0CTK.cjs} +19 -5
  34. package/dist/{middleware-K7mJeIgT.mjs → middleware-C70IPcBw.mjs} +1 -1
  35. package/dist/{middleware-COzI8bCL.mjs → middleware-XCr66VRe.mjs} +13 -13
  36. package/dist/{mod-DHgc4ygG.d.ts → mod-CQ8EjnN5.d.ts} +2 -0
  37. package/dist/{mod-1TuoXkGs.d.cts → mod-EbRFjVlY.d.cts} +2 -0
  38. package/dist/mod.cjs +5 -5
  39. package/dist/mod.d.cts +1 -1
  40. package/dist/mod.d.ts +1 -1
  41. package/dist/mod.js +5 -5
  42. package/dist/nodeinfo/client.test.mjs +50 -2
  43. package/dist/nodeinfo/handler.test.mjs +3 -3
  44. package/dist/nodeinfo/mod.cjs +1 -1
  45. package/dist/nodeinfo/mod.js +1 -1
  46. package/dist/nodeinfo/types.test.mjs +1 -1
  47. package/dist/otel/exporter.test.mjs +1 -1
  48. package/dist/{owner-CGuS9lE7.mjs → owner-C4iDnghj.mjs} +2 -2
  49. package/dist/{proof--56drUyt.mjs → proof-CpU6r25U.mjs} +3 -3
  50. package/dist/{proof-tjbzynMa.js → proof-DqO9zYcc.js} +2 -2
  51. package/dist/{proof-Dgozne0w.cjs → proof-DvVA2Bmk.cjs} +2 -2
  52. package/dist/{send-CC1A7ATC.mjs → send-YVatAj-a.mjs} +18 -4
  53. package/dist/sig/http.test.mjs +43 -3
  54. package/dist/sig/key.test.mjs +2 -2
  55. package/dist/sig/ld.test.mjs +5 -2
  56. package/dist/sig/mod.cjs +2 -2
  57. package/dist/sig/mod.js +2 -2
  58. package/dist/sig/owner.test.mjs +2 -2
  59. package/dist/sig/proof.test.mjs +2 -2
  60. package/dist/{std__assert-BBjXFNOb.mjs → std__assert-BvGuO4xO.mjs} +2 -27
  61. package/dist/{temporal-DvptXZ7V.mjs → temporal-BJ-5wxWU.mjs} +1 -1
  62. package/dist/testing/mod.mjs +1 -1
  63. package/dist/{types-DWs-qZaY.cjs → types-DRPjmHVV.cjs} +7 -3
  64. package/dist/{types-sXITXcWa.js → types-miAYfiGO.js} +7 -3
  65. package/dist/utils/docloader.test.mjs +299 -3
  66. package/dist/utils/kv-cache.test.mjs +1 -1
  67. package/dist/utils/mod.cjs +1 -1
  68. package/dist/utils/mod.d.cts +1 -1
  69. package/dist/utils/mod.d.ts +1 -1
  70. package/dist/utils/mod.js +1 -1
  71. package/dist/vocab/cjs.test.mjs +1 -1
  72. package/package.json +8 -8
  73. package/dist/docloader-BAdp-ZeK.mjs +0 -53
@@ -2,7 +2,7 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
5
- import { n as assertGreater } from "../std__assert-BBjXFNOb.mjs";
5
+ import "../std__assert-BvGuO4xO.mjs";
6
6
  import { n as assertGreaterOrEqual, r as assertFalse } from "../assert_rejects-DN60FHPX.mjs";
7
7
  import { t as assert } from "../assert-jSdZMYam.mjs";
8
8
  import { test } from "@fedify/fixture";
@@ -294,50 +294,105 @@ test("InProcessMessageQueue", async (t) => {
294
294
  const listening = mq.listen((message) => {
295
295
  messages.push(message);
296
296
  }, controller);
297
- await t.step("enqueue()", async () => {
298
- await mq.enqueue("Hello, world!");
299
- });
300
- await waitFor(() => messages.length > 0, 15e3);
301
- await t.step("listen()", () => {
302
- assertEquals(messages, ["Hello, world!"]);
303
- });
304
- let started = 0;
305
- await t.step("enqueue() with delay", async () => {
306
- started = Date.now();
307
- await mq.enqueue("Delayed message", { delay: Temporal.Duration.from({ seconds: 3 }) });
308
- assertEquals(messages, ["Hello, world!"]);
309
- });
310
- await waitFor(() => messages.length > 1, 15e3);
311
- await t.step("listen() with delay", () => {
312
- assertEquals(messages, ["Hello, world!", "Delayed message"]);
313
- assertGreater(Date.now() - started, 3e3);
314
- });
315
- while (messages.length > 0) messages.pop();
316
- await t.step("enqueueMany()", async () => {
317
- const testMessages = Array.from({ length: 5 }, (_, i) => `Batch message ${i}!`);
318
- await mq.enqueueMany(testMessages);
319
- });
320
- await waitFor(() => messages.length >= 5, 15e3);
321
- await t.step("listen() [multiple]", () => {
322
- assertEquals(messages.length, 5);
323
- for (let i = 0; i < 5; i++) assertEquals(messages[i], `Batch message ${i}!`);
324
- });
325
- while (messages.length > 0) messages.pop();
326
- started = 0;
327
- await t.step("enqueueMany() with delay", async () => {
328
- started = Date.now();
329
- const testMessages = Array.from({ length: 3 }, (_, i) => `Delayed batch ${i}!`);
330
- await mq.enqueueMany(testMessages, { delay: Temporal.Duration.from({ seconds: 2 }) });
331
- assertEquals(messages.length, 0);
297
+ try {
298
+ await t.step("enqueue()", async () => {
299
+ await mq.enqueue("Hello, world!");
300
+ });
301
+ await waitFor(() => messages.length > 0, 15e3);
302
+ await t.step("listen()", () => {
303
+ assertEquals(messages, ["Hello, world!"]);
304
+ });
305
+ await t.step("enqueue() with delay", async () => {
306
+ const { timers, enqueued } = captureTimers(() => mq.enqueue("Delayed message", { delay: Temporal.Duration.from({ seconds: 3 }) }));
307
+ await enqueued;
308
+ assertEquals(timers.length, 1);
309
+ assertEquals(timers[0].delay, 3e3);
310
+ await mq.enqueue("Single delay marker");
311
+ await waitFor(() => messages.includes("Single delay marker"), 15e3);
312
+ assertEquals(messages, ["Hello, world!", "Single delay marker"]);
313
+ timers[0].callback();
314
+ });
315
+ await waitFor(() => messages.length >= 3, 15e3);
316
+ await t.step("listen() with delay", () => {
317
+ assertEquals(messages, [
318
+ "Hello, world!",
319
+ "Single delay marker",
320
+ "Delayed message"
321
+ ]);
322
+ });
323
+ while (messages.length > 0) messages.pop();
324
+ await t.step("enqueueMany()", async () => {
325
+ const testMessages = Array.from({ length: 5 }, (_, i) => `Batch message ${i}!`);
326
+ await mq.enqueueMany(testMessages);
327
+ });
328
+ await waitFor(() => messages.length >= 5, 15e3);
329
+ await t.step("listen() [multiple]", () => {
330
+ assertEquals(messages.length, 5);
331
+ for (let i = 0; i < 5; i++) assertEquals(messages[i], `Batch message ${i}!`);
332
+ });
333
+ while (messages.length > 0) messages.pop();
334
+ await t.step("enqueueMany() with delay", async () => {
335
+ const testMessages = Array.from({ length: 3 }, (_, i) => `Delayed batch ${i}!`);
336
+ const { timers, enqueued } = captureTimers(() => mq.enqueueMany(testMessages, { delay: Temporal.Duration.from({ seconds: 2 }) }));
337
+ await enqueued;
338
+ assertEquals(timers.length, 1);
339
+ assertEquals(timers[0].delay, 2e3);
340
+ await mq.enqueue("Batch delay marker");
341
+ await waitFor(() => messages.includes("Batch delay marker"), 15e3);
342
+ assertEquals(messages, ["Batch delay marker"]);
343
+ timers[0].callback();
344
+ });
345
+ await waitFor(() => messages.length >= 4, 15e3);
346
+ await t.step("listen() [delayed multiple]", () => {
347
+ assertEquals(messages.length, 4);
348
+ assertEquals(messages[0], "Batch delay marker");
349
+ for (let i = 0; i < 3; i++) assertEquals(messages[i + 1], `Delayed batch ${i}!`);
350
+ });
351
+ } finally {
352
+ controller.abort();
353
+ await listening;
354
+ }
355
+ });
356
+ function captureTimers(enqueue) {
357
+ const timers = [];
358
+ const original = globalThis.setTimeout;
359
+ globalThis.setTimeout = ((callback, delay) => {
360
+ timers.push({
361
+ callback,
362
+ delay
363
+ });
364
+ return 0;
332
365
  });
333
- await waitFor(() => messages.length >= 3, 15e3);
334
- await t.step("listen() [delayed multiple]", () => {
335
- assertEquals(messages.length, 3);
336
- assertGreater(Date.now() - started, 2e3);
337
- for (let i = 0; i < 3; i++) assertEquals(messages[i], `Delayed batch ${i}!`);
366
+ try {
367
+ return {
368
+ timers,
369
+ enqueued: enqueue()
370
+ };
371
+ } finally {
372
+ globalThis.setTimeout = original;
373
+ }
374
+ }
375
+ test("InProcessMessageQueue real delay", async (t) => {
376
+ for (const batch of [false, true]) await t.step(batch ? "enqueueMany()" : "enqueue()", async () => {
377
+ const mq = new InProcessMessageQueue();
378
+ const messages = [];
379
+ const controller = new AbortController();
380
+ const listening = mq.listen((message) => {
381
+ messages.push(message);
382
+ }, controller);
383
+ try {
384
+ const delayed = batch ? ["Delayed 1", "Delayed 2"] : ["Delayed"];
385
+ const options = { delay: Temporal.Duration.from({ milliseconds: 100 }) };
386
+ if (batch) await mq.enqueueMany(delayed, options);
387
+ else await mq.enqueue(delayed[0], options);
388
+ await mq.enqueue("Immediate");
389
+ await waitFor(() => messages.length >= delayed.length + 1, 15e3);
390
+ assertEquals(messages, ["Immediate", ...delayed]);
391
+ } finally {
392
+ controller.abort();
393
+ await listening;
394
+ }
338
395
  });
339
- controller.abort();
340
- await listening;
341
396
  });
342
397
  test("InProcessMessageQueue.getDepth()", async () => {
343
398
  const mq = new InProcessMessageQueue();
@@ -1,7 +1,7 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import "../std__assert-BBjXFNOb.mjs";
4
+ import "../std__assert-BvGuO4xO.mjs";
5
5
  import { r as assertFalse } from "../assert_rejects-DN60FHPX.mjs";
6
6
  import { t as assert } from "../assert-jSdZMYam.mjs";
7
7
  import { t as acceptsJsonLd } from "../negotiation-CDW-_gUU.mjs";
@@ -2,7 +2,7 @@ import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
5
- import "../std__assert-BBjXFNOb.mjs";
5
+ import "../std__assert-BvGuO4xO.mjs";
6
6
  import { r as assertFalse } from "../assert_rejects-DN60FHPX.mjs";
7
7
  import { t as assert } from "../assert-jSdZMYam.mjs";
8
8
  import { t as assertThrows } from "../assert_throws-BOkhLGYc.mjs";
@@ -3,17 +3,18 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as esm_default } from "../esm-vrlUxr60.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
6
- import "../std__assert-BBjXFNOb.mjs";
6
+ import "../std__assert-BvGuO4xO.mjs";
7
7
  import { n as assertGreaterOrEqual, r as assertFalse, t as assertRejects } from "../assert_rejects-DN60FHPX.mjs";
8
8
  import { n as assertInstanceOf, t as assert } from "../assert-jSdZMYam.mjs";
9
9
  import { t as assertNotEquals } from "../assert_not_equals-DkVK8oqV.mjs";
10
- import { l as verifyRequest } from "../http-ClEDLGpr.mjs";
10
+ import { l as verifyRequest } from "../http-BqObaL32.mjs";
11
11
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-BAK-tUlf.mjs";
12
- import { t as doesActorOwnKey } from "../owner-CGuS9lE7.mjs";
13
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "../send-CC1A7ATC.mjs";
12
+ import { t as doesActorOwnKey } from "../owner-C4iDnghj.mjs";
13
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "../send-YVatAj-a.mjs";
14
14
  import { Activity, Application, Endpoints, Group, Person, Service } from "@fedify/vocab";
15
15
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
16
16
  import { FetchError, UrlError } from "@fedify/vocab-runtime";
17
+ import dns from "node:dns/promises";
17
18
  import { AggregationTemporality, InMemoryMetricExporter, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
18
19
  //#region src/federation/send.test.ts
19
20
  test("extractInboxes()", () => {
@@ -687,5 +688,85 @@ for (const signed of [false, true]) {
687
688
  });
688
689
  });
689
690
  }
691
+ for (const signed of [false, true]) test({
692
+ name: `sendActivity() classifies DNS failures (signed: ${signed})`,
693
+ ignore: "Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted",
694
+ async fn(t) {
695
+ const activity = {
696
+ type: "Create",
697
+ id: "https://example.com/activity"
698
+ };
699
+ const destination = "https://delivery.invalid/inbox";
700
+ const publicInbox = "https://8.8.8.8/inbox";
701
+ const keys = signed ? [{
702
+ privateKey: rsaPrivateKey2,
703
+ keyId: rsaPublicKey2.id
704
+ }] : [];
705
+ for (const result of [
706
+ "throws",
707
+ "empty",
708
+ "cname",
709
+ "private"
710
+ ]) for (const redirected of [false, true]) await t.step(`${result}, redirected: ${redirected}`, async () => {
711
+ const [meterProvider, recorder] = createTestMeterProvider();
712
+ const originalLookup = dns.lookup;
713
+ const resolverError = /* @__PURE__ */ new Error("Resolver unavailable");
714
+ const lookups = [];
715
+ dns.lookup = ((hostname, options) => {
716
+ lookups.push(hostname);
717
+ assertEquals(hostname, "delivery.invalid");
718
+ assertEquals(options, { all: true });
719
+ if (result === "throws") return Promise.reject(resolverError);
720
+ return Promise.resolve(result === "empty" ? [] : [{
721
+ address: result === "private" ? "127.0.0.1" : "alias.invalid.",
722
+ family: 4
723
+ }]);
724
+ });
725
+ try {
726
+ esm_default.mockGlobal().catch(202);
727
+ if (redirected) esm_default.route(publicInbox, {
728
+ status: 307,
729
+ headers: { Location: destination }
730
+ });
731
+ const send = () => sendActivity({
732
+ activity,
733
+ activityType: "https://www.w3.org/ns/activitystreams#Create",
734
+ meterProvider,
735
+ keys,
736
+ inbox: new URL(redirected ? publicInbox : destination)
737
+ });
738
+ if (result === "private") assertEquals((await assertRejects(send, UrlError)).reason, "disallowed");
739
+ else {
740
+ const error = await assertRejects(send, FetchError);
741
+ assertEquals(error.url.href, destination);
742
+ assertInstanceOf(error.cause, UrlError);
743
+ assertEquals(error.cause.reason, "dns");
744
+ assertEquals(error.cause.cause, result === "throws" ? resolverError : void 0);
745
+ }
746
+ const expectedCount = result === "private" && !redirected ? 0 : 1;
747
+ const sent = recorder.getMeasurements("activitypub.delivery.sent");
748
+ const durations = recorder.getMeasurements("activitypub.delivery.duration");
749
+ assertEquals(sent.length, expectedCount);
750
+ assertEquals(durations.length, expectedCount);
751
+ if (expectedCount === 1) {
752
+ assertEquals(sent[0].value, 1);
753
+ assertEquals(sent[0].attributes, {
754
+ "activitypub.remote.host": redirected ? "8.8.8.8" : "delivery.invalid",
755
+ "activitypub.activity.type": "https://www.w3.org/ns/activitystreams#Create",
756
+ "activitypub.delivery.success": false
757
+ });
758
+ assertEquals(durations[0].attributes, sent[0].attributes);
759
+ assertGreaterOrEqual(durations[0].value, 0);
760
+ }
761
+ assertEquals(lookups, ["delivery.invalid"]);
762
+ assertEquals(esm_default.callHistory.calls(destination).length, 0);
763
+ assertEquals(esm_default.callHistory.calls().length, redirected ? 1 : 0);
764
+ } finally {
765
+ dns.lookup = originalLookup;
766
+ esm_default.hardReset();
767
+ }
768
+ });
769
+ }
770
+ });
690
771
  //#endregion
691
772
  export {};
@@ -1,10 +1,10 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import "../std__assert-BBjXFNOb.mjs";
4
+ import "../std__assert-BvGuO4xO.mjs";
5
5
  import { r as assertFalse } from "../assert_rejects-DN60FHPX.mjs";
6
6
  import { t as assert } from "../assert-jSdZMYam.mjs";
7
- import { t as hasMalformedKnownTemporalLiteral } from "../temporal-DvptXZ7V.mjs";
7
+ import { t as hasMalformedKnownTemporalLiteral } from "../temporal-BJ-5wxWU.mjs";
8
8
  import { test } from "@fedify/fixture";
9
9
  //#region src/federation/temporal.test.ts
10
10
  test("hasMalformedKnownTemporalLiteral() detects expanded proof timestamps", async () => {
@@ -1,12 +1,12 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { r as createRequestContext } from "../context-BepjCFHN.mjs";
4
+ import { r as createRequestContext } from "../context-CxbCIptr.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
6
- import "../std__assert-BBjXFNOb.mjs";
6
+ import "../std__assert-BvGuO4xO.mjs";
7
7
  import { t as assertNotEquals } from "../assert_not_equals-DkVK8oqV.mjs";
8
8
  import { t as MemoryKvStore } from "../kv-CX8qfaad.mjs";
9
- import { o as createFederation, s as handleWebFinger } from "../middleware-COzI8bCL.mjs";
9
+ import { o as createFederation, s as handleWebFinger } from "../middleware-XCr66VRe.mjs";
10
10
  import { Image, Link, Person, Tombstone } from "@fedify/vocab";
11
11
  import { createTestMeterProvider, test } from "@fedify/fixture";
12
12
  //#region src/federation/webfinger.test.ts
@@ -1,10 +1,10 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno--DCBC7WG.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-FDcYXRP5.mjs";
4
+ import { n as version, t as name } from "./deno-Deo5dZf_.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-jUJmtE2u.mjs";
6
6
  import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-Dd__NiUL.mjs";
7
- import { i as fetchKeyDetailed, s as validateCryptoKey } from "./key-C-7sqo8D.mjs";
7
+ import { i as fetchKeyDetailed, s as validateCryptoKey } from "./key-D1TEfV4d.mjs";
8
8
  import { getLogger } from "@logtape/logtape";
9
9
  import { CryptographicKey } from "@fedify/vocab";
10
10
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -718,6 +718,8 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
718
718
  failure = result;
719
719
  failureAlgorithm = algorithm;
720
720
  };
721
+ let body = null;
722
+ let digestValid = null;
721
723
  for (const sigName of signatureNames) {
722
724
  if (!signatures[sigName]) {
723
725
  setFailure(invalidSignatureResult(parseKeyId(signatureInputs[sigName]?.keyId)));
@@ -769,7 +771,9 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
769
771
  setFailure(invalidSignatureResult(keyId));
770
772
  continue;
771
773
  }
772
- if (!await verifyRfc9421ContentDigest(contentDigestHeader, await request.arrayBuffer())) {
774
+ body ??= await request.arrayBuffer();
775
+ digestValid ??= await verifyRfc9421ContentDigest(contentDigestHeader, body);
776
+ if (!digestValid) {
773
777
  logger.debug("Failed to verify; Content-Digest verification failed.", { contentDigest: contentDigestHeader });
774
778
  setFailure(invalidSignatureResult(keyId));
775
779
  continue;
@@ -11,7 +11,7 @@ let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conve
11
11
  let byte_encodings_base64 = require("byte-encodings/base64");
12
12
  //#region deno.json
13
13
  var name = "@fedify/fedify";
14
- var version = "2.3.8";
14
+ var version = "2.3.9";
15
15
  //#endregion
16
16
  //#region src/sig/accept.ts
17
17
  /**
@@ -2190,6 +2190,8 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
2190
2190
  failure = result;
2191
2191
  failureAlgorithm = algorithm;
2192
2192
  };
2193
+ let body = null;
2194
+ let digestValid = null;
2193
2195
  for (const sigName of signatureNames) {
2194
2196
  if (!signatures[sigName]) {
2195
2197
  setFailure(invalidSignatureResult(parseKeyId(signatureInputs[sigName]?.keyId)));
@@ -2241,7 +2243,9 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
2241
2243
  setFailure(invalidSignatureResult(keyId));
2242
2244
  continue;
2243
2245
  }
2244
- if (!await verifyRfc9421ContentDigest(contentDigestHeader, await request.arrayBuffer())) {
2246
+ body ??= await request.arrayBuffer();
2247
+ digestValid ??= await verifyRfc9421ContentDigest(contentDigestHeader, body);
2248
+ if (!digestValid) {
2245
2249
  logger.debug("Failed to verify; Content-Digest verification failed.", { contentDigest: contentDigestHeader });
2246
2250
  setFailure(invalidSignatureResult(keyId));
2247
2251
  continue;
@@ -10,7 +10,7 @@ import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_URL_FULL } fro
10
10
  import { decodeBase64, encodeBase64 } from "byte-encodings/base64";
11
11
  //#region deno.json
12
12
  var name = "@fedify/fedify";
13
- var version = "2.3.8";
13
+ var version = "2.3.9";
14
14
  //#endregion
15
15
  //#region src/sig/accept.ts
16
16
  /**
@@ -2189,6 +2189,8 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
2189
2189
  failure = result;
2190
2190
  failureAlgorithm = algorithm;
2191
2191
  };
2192
+ let body = null;
2193
+ let digestValid = null;
2192
2194
  for (const sigName of signatureNames) {
2193
2195
  if (!signatures[sigName]) {
2194
2196
  setFailure(invalidSignatureResult(parseKeyId(signatureInputs[sigName]?.keyId)));
@@ -2240,7 +2242,9 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
2240
2242
  setFailure(invalidSignatureResult(keyId));
2241
2243
  continue;
2242
2244
  }
2243
- if (!await verifyRfc9421ContentDigest(contentDigestHeader, await request.arrayBuffer())) {
2245
+ body ??= await request.arrayBuffer();
2246
+ digestValid ??= await verifyRfc9421ContentDigest(contentDigestHeader, body);
2247
+ if (!digestValid) {
2244
2248
  logger.debug("Failed to verify; Content-Digest verification failed.", { contentDigest: contentDigestHeader });
2245
2249
  setFailure(invalidSignatureResult(keyId));
2246
2250
  continue;
@@ -1,8 +1,8 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno--DCBC7WG.mjs";
5
- import { _ as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-FDcYXRP5.mjs";
4
+ import { n as version, t as name } from "./deno-Deo5dZf_.mjs";
5
+ import { _ as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-jUJmtE2u.mjs";
6
6
  import { getLogger } from "@logtape/logtape";
7
7
  import { CryptographicKey, Object as Object$1, isActor } from "@fedify/vocab";
8
8
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
@@ -1,7 +1,7 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  require("./chunk-DDcVe30Y.cjs");
4
- const require_http = require("./http-ChVf88XZ.cjs");
4
+ const require_http = require("./http-CjvY7wBM.cjs");
5
5
  let _logtape_logtape = require("@logtape/logtape");
6
6
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
7
7
  let es_toolkit = require("es-toolkit");
@@ -11,10 +11,13 @@ const logger$1 = (0, _logtape_logtape.getLogger)([
11
11
  "utils",
12
12
  "docloader"
13
13
  ]);
14
+ const DEFAULT_MAX_REDIRECTION = 20;
14
15
  /**
15
16
  * Gets an authenticated {@link DocumentLoader} for the given identity.
16
17
  * Note that an authenticated document loader intentionally does not cache
17
18
  * the fetched documents.
19
+ * At most 20 HTTP redirects and alternate document links are followed in total
20
+ * per call. Revisiting a URL within that chain throws a {@link FetchError}.
18
21
  * @param identity The identity to get the document loader for.
19
22
  * The actor's key pair.
20
23
  * @param options The options for the document loader.
@@ -24,29 +27,52 @@ const logger$1 = (0, _logtape_logtape.getLogger)([
24
27
  */
25
28
  function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, tracerProvider } = {}) {
26
29
  require_http.validateCryptoKey(identity.privateKey);
27
- async function load(url, options) {
28
- await validateUrl(url);
29
- return (0, _fedify_vocab_runtime.getRemoteDocument)(url, await require_http.doubleKnock((0, _fedify_vocab_runtime.createActivityPubRequest)(url, { userAgent }), identity, {
30
+ async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
31
+ options?.signal?.throwIfAborted();
32
+ let currentUrl = new URL(url).href;
33
+ await validateUrl(currentUrl);
34
+ visited.add(currentUrl);
35
+ const originalRequest = (0, _fedify_vocab_runtime.createActivityPubRequest)(currentUrl, { userAgent });
36
+ function follow(nextUrl) {
37
+ options?.signal?.throwIfAborted();
38
+ if (redirected >= DEFAULT_MAX_REDIRECTION) throw new _fedify_vocab_runtime.FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
39
+ if (visited.has(nextUrl)) throw new _fedify_vocab_runtime.FetchError(currentUrl, `Redirect loop detected: ${nextUrl}`);
40
+ redirected++;
41
+ }
42
+ const response = await require_http.doubleKnock(originalRequest, identity, {
30
43
  maxRedirection,
31
44
  specDeterminer,
32
45
  log: (0, es_toolkit.curry)(_fedify_vocab_runtime.logRequest)(logger$1),
33
46
  tracerProvider,
34
47
  signal: options?.signal,
35
- validateRedirect: validateUrl
36
- }), load);
48
+ validateRedirect: async (nextUrl) => {
49
+ follow(nextUrl);
50
+ await validateUrl(nextUrl);
51
+ visited.add(nextUrl);
52
+ currentUrl = nextUrl;
53
+ }
54
+ });
55
+ return (0, _fedify_vocab_runtime.getRemoteDocument)(currentUrl, response, (alternateUrl) => {
56
+ follow(alternateUrl);
57
+ return load(alternateUrl, options, redirected, visited);
58
+ });
37
59
  }
38
60
  async function validateUrl(url) {
39
61
  if (!allowPrivateAddress) try {
40
62
  await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
41
63
  } catch (error) {
42
- if (error instanceof _fedify_vocab_runtime.UrlError) logger$1.error("Disallowed private URL: {url}", {
64
+ if (error instanceof _fedify_vocab_runtime.UrlError) if (error.reason === "dns") logger$1.debug("DNS lookup failed for {url}", {
65
+ url,
66
+ error
67
+ });
68
+ else logger$1.error("Disallowed private URL: {url}", {
43
69
  url,
44
70
  error
45
71
  });
46
72
  throw error;
47
73
  }
48
74
  }
49
- return load;
75
+ return (url, options) => load(url, options);
50
76
  }
51
77
  //#endregion
52
78
  //#region src/utils/kv-cache.ts
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { p as recordDocumentCache } from "./metrics-FDcYXRP5.mjs";
4
+ import { p as recordDocumentCache } from "./metrics-jUJmtE2u.mjs";
5
5
  import { getLogger } from "@logtape/logtape";
6
6
  import { preloadedContexts } from "@fedify/vocab-runtime";
7
7
  //#region src/utils/kv-cache.ts
@@ -1,8 +1,8 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { T as recordDocumentCache, f as validateCryptoKey, t as doubleKnock } from "./http-BKJYj9oz.js";
3
+ import { T as recordDocumentCache, f as validateCryptoKey, t as doubleKnock } from "./http-DuWPL6ic.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
- import { UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, validatePublicUrl } from "@fedify/vocab-runtime";
5
+ import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, validatePublicUrl } from "@fedify/vocab-runtime";
6
6
  import { curry } from "es-toolkit";
7
7
  //#region src/utils/docloader.ts
8
8
  const logger$1 = getLogger([
@@ -10,10 +10,13 @@ const logger$1 = getLogger([
10
10
  "utils",
11
11
  "docloader"
12
12
  ]);
13
+ const DEFAULT_MAX_REDIRECTION = 20;
13
14
  /**
14
15
  * Gets an authenticated {@link DocumentLoader} for the given identity.
15
16
  * Note that an authenticated document loader intentionally does not cache
16
17
  * the fetched documents.
18
+ * At most 20 HTTP redirects and alternate document links are followed in total
19
+ * per call. Revisiting a URL within that chain throws a {@link FetchError}.
17
20
  * @param identity The identity to get the document loader for.
18
21
  * The actor's key pair.
19
22
  * @param options The options for the document loader.
@@ -23,29 +26,52 @@ const logger$1 = getLogger([
23
26
  */
24
27
  function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, tracerProvider } = {}) {
25
28
  validateCryptoKey(identity.privateKey);
26
- async function load(url, options) {
27
- await validateUrl(url);
28
- return getRemoteDocument(url, await doubleKnock(createActivityPubRequest(url, { userAgent }), identity, {
29
+ async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
30
+ options?.signal?.throwIfAborted();
31
+ let currentUrl = new URL(url).href;
32
+ await validateUrl(currentUrl);
33
+ visited.add(currentUrl);
34
+ const originalRequest = createActivityPubRequest(currentUrl, { userAgent });
35
+ function follow(nextUrl) {
36
+ options?.signal?.throwIfAborted();
37
+ if (redirected >= DEFAULT_MAX_REDIRECTION) throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
38
+ if (visited.has(nextUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${nextUrl}`);
39
+ redirected++;
40
+ }
41
+ const response = await doubleKnock(originalRequest, identity, {
29
42
  maxRedirection,
30
43
  specDeterminer,
31
44
  log: curry(logRequest)(logger$1),
32
45
  tracerProvider,
33
46
  signal: options?.signal,
34
- validateRedirect: validateUrl
35
- }), load);
47
+ validateRedirect: async (nextUrl) => {
48
+ follow(nextUrl);
49
+ await validateUrl(nextUrl);
50
+ visited.add(nextUrl);
51
+ currentUrl = nextUrl;
52
+ }
53
+ });
54
+ return getRemoteDocument(currentUrl, response, (alternateUrl) => {
55
+ follow(alternateUrl);
56
+ return load(alternateUrl, options, redirected, visited);
57
+ });
36
58
  }
37
59
  async function validateUrl(url) {
38
60
  if (!allowPrivateAddress) try {
39
61
  await validatePublicUrl(url);
40
62
  } catch (error) {
41
- if (error instanceof UrlError) logger$1.error("Disallowed private URL: {url}", {
63
+ if (error instanceof UrlError) if (error.reason === "dns") logger$1.debug("DNS lookup failed for {url}", {
64
+ url,
65
+ error
66
+ });
67
+ else logger$1.error("Disallowed private URL: {url}", {
42
68
  url,
43
69
  error
44
70
  });
45
71
  throw error;
46
72
  }
47
73
  }
48
- return load;
74
+ return (url, options) => load(url, options);
49
75
  }
50
76
  //#endregion
51
77
  //#region src/utils/kv-cache.ts
@@ -1,9 +1,9 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno--DCBC7WG.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-FDcYXRP5.mjs";
6
- import { r as fetchKey, s as validateCryptoKey } from "./key-C-7sqo8D.mjs";
4
+ import { n as version, t as name } from "./deno-Deo5dZf_.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-jUJmtE2u.mjs";
6
+ import { r as fetchKey, s as validateCryptoKey } from "./key-D1TEfV4d.mjs";
7
7
  import { getLogger } from "@logtape/logtape";
8
8
  import { Activity, CryptographicKey, Object as Object$1, getTypeId } from "@fedify/vocab";
9
9
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -206,7 +206,7 @@ function normalizeContextReference(reference, baseUrl) {
206
206
  /** @internal */
207
207
  function isInvalidUrlTypeError(error) {
208
208
  const code = error.code;
209
- return error instanceof TypeError && (code === "ERR_INVALID_URL" || /^Invalid URL(?::|$)/.test(error.message) || / cannot be parsed as a URL\.?$/.test(error.message));
209
+ return error instanceof TypeError && (code === "ERR_INVALID_URL" || error.message === "Invalid URL string." || /^Invalid URL(?::|$)/.test(error.message) || / cannot be parsed as a URL\.?$/.test(error.message));
210
210
  }
211
211
  async function applyGraphAliasContext(state, context, documentLoader, remoteContextCache, baseUrl = null, processingContexts = /* @__PURE__ */ new Set()) {
212
212
  if (context === null) return createGraphAliasContextState();
@@ -1,7 +1,7 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno--DCBC7WG.mjs";
4
+ import { n as version, t as name } from "./deno-Deo5dZf_.mjs";
5
5
  import { metrics } from "@opentelemetry/api";
6
6
  import { FetchError } from "@fedify/vocab-runtime";
7
7
  //#region src/federation/metrics.ts