@fedify/fedify 2.3.9-dev.2048 → 2.3.10

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 (85) hide show
  1. package/dist/{builder-BfY2wZ4V.mjs → builder-Bal9bfuM.mjs} +10 -2
  2. package/dist/{client-DYDoDpMa.mjs → client-Ccbsek6u.mjs} +7 -3
  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/{context-CAKnQy1x.d.ts → context-BWbdE_MZ.d.ts} +1 -1
  7. package/dist/{context-BepjCFHN.mjs → context-CxbCIptr.mjs} +2 -1
  8. package/dist/{context-CSIggG9_.d.cts → context-MOrCC7yl.d.cts} +1 -1
  9. package/dist/{deno-ayal9buz.mjs → deno-DUgFsCmM.mjs} +1 -1
  10. package/dist/docloader-w72ipM49.mjs +79 -0
  11. package/dist/federation/builder.test.mjs +38 -2
  12. package/dist/federation/circuit-breaker.test.mjs +1 -1
  13. package/dist/federation/collection.test.mjs +1 -1
  14. package/dist/federation/handler.test.mjs +7 -7
  15. package/dist/federation/idempotency.test.mjs +3 -3
  16. package/dist/federation/kv.test.mjs +1 -1
  17. package/dist/federation/metrics.test.mjs +2 -2
  18. package/dist/federation/middleware.test.mjs +302 -8
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +3 -3
  21. package/dist/federation/mod.d.ts +3 -3
  22. package/dist/federation/mod.js +1 -1
  23. package/dist/federation/mq.test.mjs +98 -43
  24. package/dist/federation/negotiation.test.mjs +1 -1
  25. package/dist/federation/router.test.mjs +1 -1
  26. package/dist/federation/send.test.mjs +85 -4
  27. package/dist/federation/temporal.test.mjs +2 -2
  28. package/dist/federation/webfinger.test.mjs +3 -3
  29. package/dist/{http-B2u6PvNX.mjs → http-BNDHRNJP.mjs} +8 -4
  30. package/dist/{http-BPPCh0xN.js → http-BY8PpQzP.js} +6 -2
  31. package/dist/{http-BGP2QCbe.cjs → http-CTYio50W.cjs} +6 -2
  32. package/dist/{key-C9DkrWwb.mjs → key-BbuFo4CW.mjs} +2 -2
  33. package/dist/{kv-D6hNiMTK.d.ts → kv-CZKl1IUS.d.cts} +3 -2
  34. package/dist/{kv-gJ8LYbxX.d.cts → kv-CxaI4eGo.d.ts} +3 -2
  35. package/dist/{kv-cache-qJSjfLGm.mjs → kv-cache-CNZOaZYq.mjs} +1 -1
  36. package/dist/{kv-cache-C1TC_e9y.cjs → kv-cache-CrcqGkT-.cjs} +34 -8
  37. package/dist/{kv-cache-Cl1q1aA9.js → kv-cache-DNnjnc9t.js} +35 -9
  38. package/dist/{ld-DNl7KwLY.mjs → ld-BYV2Luge.mjs} +4 -4
  39. package/dist/{metrics-Ce55disp.mjs → metrics-QrVsKSG9.mjs} +1 -1
  40. package/dist/{middleware-C96wm_7x.mjs → middleware-54KSGAQA.mjs} +1 -1
  41. package/dist/{middleware-C_utyVa_.js → middleware-B4FNnN3W.js} +36 -11
  42. package/dist/{middleware-BCG2BX1n.mjs → middleware-Bj9s3J_0.mjs} +22 -19
  43. package/dist/{middleware-BZojifnF.cjs → middleware-Dv-L7Plm.cjs} +36 -11
  44. package/dist/{mod-PVR75MeU.d.cts → mod-B6PwkUMU.d.cts} +1 -1
  45. package/dist/{mod-DHgc4ygG.d.ts → mod-Br8yJNcE.d.ts} +3 -1
  46. package/dist/{mod-1TuoXkGs.d.cts → mod-Co1M3Ux0.d.cts} +3 -1
  47. package/dist/{mod-CFuyfw3c.d.ts → mod-LmD03uRA.d.ts} +1 -1
  48. package/dist/mod.cjs +5 -5
  49. package/dist/mod.d.cts +4 -4
  50. package/dist/mod.d.ts +4 -4
  51. package/dist/mod.js +5 -5
  52. package/dist/nodeinfo/client.test.mjs +50 -2
  53. package/dist/nodeinfo/handler.test.mjs +3 -3
  54. package/dist/nodeinfo/mod.cjs +1 -1
  55. package/dist/nodeinfo/mod.js +1 -1
  56. package/dist/nodeinfo/types.test.mjs +1 -1
  57. package/dist/otel/exporter.test.mjs +1 -1
  58. package/dist/otel/mod.d.cts +1 -1
  59. package/dist/otel/mod.d.ts +1 -1
  60. package/dist/{owner-DqqDBw-l.mjs → owner-BsBzJADO.mjs} +2 -2
  61. package/dist/{proof-Dz9ObvUq.js → proof-BxS2Xa8B.js} +2 -2
  62. package/dist/{proof-I6YcKRRq.cjs → proof-CGKQFT2M.cjs} +2 -2
  63. package/dist/{proof-D5vP7VIN.mjs → proof-cXVkPBIU.mjs} +3 -3
  64. package/dist/{send-CBtR8DQK.mjs → send-omt23uTA.mjs} +18 -4
  65. package/dist/sig/http.test.mjs +43 -3
  66. package/dist/sig/key.test.mjs +2 -2
  67. package/dist/sig/ld.test.mjs +5 -2
  68. package/dist/sig/mod.cjs +2 -2
  69. package/dist/sig/mod.js +2 -2
  70. package/dist/sig/owner.test.mjs +2 -2
  71. package/dist/sig/proof.test.mjs +2 -2
  72. package/dist/{std__assert-BBjXFNOb.mjs → std__assert-BvGuO4xO.mjs} +2 -27
  73. package/dist/{temporal-B9t2tY9A.mjs → temporal-ZUAQe754.mjs} +1 -1
  74. package/dist/testing/mod.mjs +1 -1
  75. package/dist/{types-DWs-qZaY.cjs → types-DRPjmHVV.cjs} +7 -3
  76. package/dist/{types-sXITXcWa.js → types-miAYfiGO.js} +7 -3
  77. package/dist/utils/docloader.test.mjs +299 -3
  78. package/dist/utils/kv-cache.test.mjs +1 -1
  79. package/dist/utils/mod.cjs +1 -1
  80. package/dist/utils/mod.d.cts +1 -1
  81. package/dist/utils/mod.d.ts +1 -1
  82. package/dist/utils/mod.js +1 -1
  83. package/dist/vocab/cjs.test.mjs +1 -1
  84. package/package.json +7 -7
  85. package/dist/docloader-C-JrNL14.mjs +0 -53
@@ -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-B2u6PvNX.mjs";
10
+ import { l as verifyRequest } from "../http-BNDHRNJP.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-DqqDBw-l.mjs";
13
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "../send-CBtR8DQK.mjs";
12
+ import { t as doesActorOwnKey } from "../owner-BsBzJADO.mjs";
13
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "../send-omt23uTA.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-B9t2tY9A.mjs";
7
+ import { t as hasMalformedKnownTemporalLiteral } from "../temporal-ZUAQe754.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-BCG2BX1n.mjs";
9
+ import { o as createFederation, s as handleWebFinger } from "../middleware-Bj9s3J_0.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-ayal9buz.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-Ce55disp.mjs";
4
+ import { n as version, t as name } from "./deno-DUgFsCmM.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-QrVsKSG9.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-C9DkrWwb.mjs";
7
+ import { i as fetchKeyDetailed, s as validateCryptoKey } from "./key-BbuFo4CW.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;
@@ -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.9-dev.2048+c88fcc8c";
13
+ var version = "2.3.10";
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;
@@ -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.9-dev.2048+c88fcc8c";
14
+ var version = "2.3.10";
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;
@@ -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-ayal9buz.mjs";
5
- import { _ as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-Ce55disp.mjs";
4
+ import { n as version, t as name } from "./deno-DUgFsCmM.mjs";
5
+ import { _ as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-QrVsKSG9.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";
@@ -41,14 +41,15 @@ interface KvStore {
41
41
  /**
42
42
  * Gets the value for the given key.
43
43
  * @param key The key to get the value for.
44
- * @returns The value for the key, or `undefined` if the key does not exist.
44
+ * @returns The value for the key, including `null` if stored, or `undefined`
45
+ * if the key does not exist.
45
46
  * @template T The type of the value to get.
46
47
  */
47
48
  get<T = unknown>(key: KvKey): Promise<T | undefined>;
48
49
  /**
49
50
  * Sets the value for the given key.
50
51
  * @param key The key to set the value for.
51
- * @param value The value to set.
52
+ * @param value The value to set. `null` is a storable value.
52
53
  * @param options Additional options for setting the value.
53
54
  */
54
55
  set(key: KvKey, value: unknown, options?: KvStoreSetOptions): Promise<void>;
@@ -41,14 +41,15 @@ interface KvStore {
41
41
  /**
42
42
  * Gets the value for the given key.
43
43
  * @param key The key to get the value for.
44
- * @returns The value for the key, or `undefined` if the key does not exist.
44
+ * @returns The value for the key, including `null` if stored, or `undefined`
45
+ * if the key does not exist.
45
46
  * @template T The type of the value to get.
46
47
  */
47
48
  get<T = unknown>(key: KvKey): Promise<T | undefined>;
48
49
  /**
49
50
  * Sets the value for the given key.
50
51
  * @param key The key to set the value for.
51
- * @param value The value to set.
52
+ * @param value The value to set. `null` is a storable value.
52
53
  * @param options Additional options for setting the value.
53
54
  */
54
55
  set(key: KvKey, value: unknown, options?: KvStoreSetOptions): Promise<void>;
@@ -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-Ce55disp.mjs";
4
+ import { p as recordDocumentCache } from "./metrics-QrVsKSG9.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,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-BGP2QCbe.cjs");
4
+ const require_http = require("./http-CTYio50W.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,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-BPPCh0xN.js";
3
+ import { T as recordDocumentCache, f as validateCryptoKey, t as doubleKnock } from "./http-BY8PpQzP.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-ayal9buz.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-Ce55disp.mjs";
6
- import { r as fetchKey, s as validateCryptoKey } from "./key-C9DkrWwb.mjs";
4
+ import { n as version, t as name } from "./deno-DUgFsCmM.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-QrVsKSG9.mjs";
6
+ import { r as fetchKey, s as validateCryptoKey } from "./key-BbuFo4CW.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-ayal9buz.mjs";
4
+ import { n as version, t as name } from "./deno-DUgFsCmM.mjs";
5
5
  import { metrics } from "@opentelemetry/api";
6
6
  import { FetchError } from "@fedify/vocab-runtime";
7
7
  //#region src/federation/metrics.ts
@@ -1,5 +1,5 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as FederationImpl } from "./middleware-BCG2BX1n.mjs";
4
+ import { n as FederationImpl } from "./middleware-Bj9s3J_0.mjs";
5
5
  export { FederationImpl };
@@ -2,10 +2,10 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  import { t as __exportAll } from "./chunk-CRNNMoPX.js";
4
4
  import { r as getDefaultActivityTransformers } from "./transformers-BGMIq1cs.js";
5
- import { A as recordWebFingerHandle, C as recordCollectionRequest, D as recordInboxActivity, E as recordFanoutRecipients, I as name, L as version, M as formatAcceptSignature, O as recordOutboxActivity, S as recordCollectionPageItems, _ as instrumentDocumentLoader, a as verifyRequestDetailed, b as recordCircuitBreakerStateChange, d as importJwk, f as validateCryptoKey, g as getRemoteHost, h as getFederationMetrics, i as verifyRequest, j as registerQueueDepthGauge, k as recordOutboxEnqueue, m as getDurationMs, n as parseRfc9421SignatureInput, o as exportJwk, t as doubleKnock, v as isAbortError, w as recordCollectionTotalItems, x as recordCollectionDispatchDuration } from "./http-BPPCh0xN.js";
6
- import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-sXITXcWa.js";
7
- import { _ as hasSignatureLike, b as signJsonLd, c as getKeyOwner, f as compactJsonLd, g as hasSignature, h as getNormalizationContextLoader, i as verifyObject, l as InvalidContextReferenceError, m as detachSignature, n as hasProofLike, o as normalizeOutgoingActivityJsonLd, r as signObject, s as doesActorOwnKey, u as assertSafeJsonLd, v as isClearlyMalformedContextReference, w as wrapContextLoaderForJsonLd, x as verifyCompactJsonLd, y as isInvalidUrlTypeError } from "./proof-Dz9ObvUq.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-Cl1q1aA9.js";
5
+ import { A as recordWebFingerHandle, C as recordCollectionRequest, D as recordInboxActivity, E as recordFanoutRecipients, I as name, L as version, M as formatAcceptSignature, O as recordOutboxActivity, S as recordCollectionPageItems, _ as instrumentDocumentLoader, a as verifyRequestDetailed, b as recordCircuitBreakerStateChange, d as importJwk, f as validateCryptoKey, g as getRemoteHost, h as getFederationMetrics, i as verifyRequest, j as registerQueueDepthGauge, k as recordOutboxEnqueue, m as getDurationMs, n as parseRfc9421SignatureInput, o as exportJwk, t as doubleKnock, v as isAbortError, w as recordCollectionTotalItems, x as recordCollectionDispatchDuration } from "./http-BY8PpQzP.js";
6
+ import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-miAYfiGO.js";
7
+ import { _ as hasSignatureLike, b as signJsonLd, c as getKeyOwner, f as compactJsonLd, g as hasSignature, h as getNormalizationContextLoader, i as verifyObject, l as InvalidContextReferenceError, m as detachSignature, n as hasProofLike, o as normalizeOutgoingActivityJsonLd, r as signObject, s as doesActorOwnKey, u as assertSafeJsonLd, v as isClearlyMalformedContextReference, w as wrapContextLoaderForJsonLd, x as verifyCompactJsonLd, y as isInvalidUrlTypeError } from "./proof-BxS2Xa8B.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DNnjnc9t.js";
9
9
  import { getLogger, withContext } from "@logtape/logtape";
10
10
  import { Router, RouterError, assertPath } from "@fedify/uri-template";
11
11
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
@@ -111,6 +111,7 @@ var FederationBuilderImpl = class {
111
111
  * Symbol registry for unique identification of unnamed symbols.
112
112
  */
113
113
  #symbolRegistry = /* @__PURE__ */ new Map();
114
+ #collectionNames = /* @__PURE__ */ new Map();
114
115
  constructor() {
115
116
  this.router = new Router();
116
117
  this.objectCallbacks = {};
@@ -139,6 +140,8 @@ var FederationBuilderImpl = class {
139
140
  f.objectTypeIds = { ...this.objectTypeIds };
140
141
  f.collectionCallbacks = { ...this.collectionCallbacks };
141
142
  f.collectionTypeIds = { ...this.collectionTypeIds };
143
+ f.#symbolRegistry = new Map(this.#symbolRegistry);
144
+ f.#collectionNames = new Map(this.#collectionNames);
142
145
  f.inboxPath = this.inboxPath;
143
146
  f.outboxPath = this.outboxPath;
144
147
  f.inboxCallbacks = this.inboxCallbacks == null ? void 0 : { ...this.inboxCallbacks };
@@ -618,6 +621,7 @@ var FederationBuilderImpl = class {
618
621
  if (this.collectionCallbacks[name] != null) throw new RouterError(`Collection dispatcher for ${strName} already set.`);
619
622
  assertPath(path);
620
623
  if (Router.variables(path).size < 1) throw new RouterError("Path for collection dispatcher must have at least one variable.");
624
+ this.#collectionNames.set(routeName, name);
621
625
  this.router.add(path, routeName);
622
626
  const callbacks = { dispatcher };
623
627
  this.collectionCallbacks[name] = callbacks;
@@ -655,6 +659,10 @@ var FederationBuilderImpl = class {
655
659
  const routeName = this.#uniqueCollectionId(name);
656
660
  return this.router.build(`collection:${routeName}`, values) ?? this.router.build(`orderedCollection:${routeName}`, values);
657
661
  }
662
+ /** Resolves a custom collection route to its original name. */
663
+ getCollectionName(routeName) {
664
+ return this.#collectionNames.get(routeName) ?? routeName.replace(/^(collection|orderedCollection):/, "");
665
+ }
658
666
  setOutboxPermanentFailureHandler(handler) {
659
667
  this.outboxPermanentFailureHandler = handler;
660
668
  }
@@ -3437,6 +3445,15 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
3437
3445
  if (!allowPrivateAddress) try {
3438
3446
  await validatePublicUrl(url);
3439
3447
  } catch (error) {
3448
+ if (error instanceof UrlError && error.reason === "dns") {
3449
+ logger.error("DNS resolution failed for URL: {url}", {
3450
+ url,
3451
+ error
3452
+ });
3453
+ const failure = new FetchError(url, error.message);
3454
+ failure.cause = error;
3455
+ throw failure;
3456
+ }
3440
3457
  if (error instanceof UrlError) logger.error("Disallowed private URL: {url}", {
3441
3458
  url,
3442
3459
  error
@@ -3444,7 +3461,12 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
3444
3461
  throw error;
3445
3462
  }
3446
3463
  }
3447
- await validateUrl(inbox.href);
3464
+ try {
3465
+ await validateUrl(inbox.href);
3466
+ } catch (error) {
3467
+ if (error instanceof FetchError) federationMetrics.recordDelivery(inbox, getDurationMs(started), false, activityType);
3468
+ throw error;
3469
+ }
3448
3470
  headers = new Headers(headers);
3449
3471
  headers.set("Content-Type", "application/activity+json");
3450
3472
  const request = new Request(inbox, {
@@ -5607,10 +5629,10 @@ var FederationImpl = class extends FederationBuilderImpl {
5607
5629
  onNotFound
5608
5630
  });
5609
5631
  case "collection": {
5610
- const name = route.name.replace(/^collection:/, "");
5632
+ const name = this.getCollectionName(route.name);
5611
5633
  const callbacks = this.collectionCallbacks[name];
5612
5634
  return await handleCustomCollection(request, {
5613
- name,
5635
+ name: String(name),
5614
5636
  context,
5615
5637
  values: route.values,
5616
5638
  collectionCallbacks: callbacks,
@@ -5621,10 +5643,10 @@ var FederationImpl = class extends FederationBuilderImpl {
5621
5643
  });
5622
5644
  }
5623
5645
  case "orderedCollection": {
5624
- const name = route.name.replace(/^orderedCollection:/, "");
5646
+ const name = this.getCollectionName(route.name);
5625
5647
  const callbacks = this.collectionCallbacks[name];
5626
5648
  return await handleOrderedCollection(request, {
5627
- name,
5649
+ name: String(name),
5628
5650
  context,
5629
5651
  values: route.values,
5630
5652
  collectionCallbacks: callbacks,
@@ -5867,8 +5889,10 @@ var ContextImpl = class ContextImpl {
5867
5889
  const collectionRegex = new RegExp(`^(${["collection", "orderedCollection"].join("|")}):(.*)$`);
5868
5890
  const match = route.name.match(collectionRegex);
5869
5891
  if (match !== null) {
5870
- const [, type, name] = match;
5892
+ const [, type] = match;
5893
+ const name = this.federation.getCollectionName(route.name);
5871
5894
  const cls = this.federation.collectionTypeIds[name];
5895
+ if (cls == null) return null;
5872
5896
  return {
5873
5897
  type,
5874
5898
  name,
@@ -6238,7 +6262,7 @@ var ContextImpl = class ContextImpl {
6238
6262
  "inbox"
6239
6263
  ]);
6240
6264
  const contextLoader = options.contextLoader ?? this.contextLoader;
6241
- const json = await activity.toJsonLd({ contextLoader });
6265
+ let json = await activity.toJsonLd({ contextLoader });
6242
6266
  const keyCache = new KvKeyCache(this.federation.kv, this.federation.kvPrefixes.publicKey, this);
6243
6267
  if (await verifyObject(Activity, json, {
6244
6268
  contextLoader,
@@ -6295,6 +6319,7 @@ var ContextImpl = class ContextImpl {
6295
6319
  }
6296
6320
  logger.debug("Successfully fetched the remote activity object {activityId}; ignore the original activity and use the fetched one, which is trustworthy.");
6297
6321
  activity = fetched;
6322
+ json = await fetched.toJsonLd({ contextLoader });
6298
6323
  } else logger.debug("Object Integrity Proofs are verified.", {
6299
6324
  recipient,
6300
6325
  activity: json