@fedify/fedify 2.4.5-dev.2462 → 2.4.6-dev.2628

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 (119) hide show
  1. package/README.md +3 -0
  2. package/dist/{builder-BBUGd9wr.mjs → builder-kj5boG2l.mjs} +2 -2
  3. package/dist/{client-D-XP_Ccb.d.cts → client-3InDZbba.d.cts} +0 -1
  4. package/dist/{client-D-XP_Ccb.d.ts → client-3InDZbba.d.ts} +0 -1
  5. package/dist/{client-CEnNz8O7.mjs → client-5SIFzW3q.mjs} +113 -64
  6. package/dist/compat/mod.d.cts +1 -1
  7. package/dist/compat/mod.d.ts +1 -1
  8. package/dist/compat/transformers.test.mjs +1 -1
  9. package/dist/{compound-proof-CariICma.mjs → compound-proof-BoNAYSHt.mjs} +2 -2
  10. package/dist/{context-7NyiqIcg.d.ts → context-CVOOd9pD.d.ts} +28 -2
  11. package/dist/{context-tsYPJ2Ew.d.cts → context-D0gFIgev.d.cts} +28 -2
  12. package/dist/{deno-BO7-a3hz.mjs → deno-ksnVeCA7.mjs} +1 -1
  13. package/dist/docloader-BhVNi06k.mjs +136 -0
  14. package/dist/federation/builder.test.mjs +1 -1
  15. package/dist/federation/compound-fanout.test.mjs +6 -3
  16. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  17. package/dist/federation/handler.test.mjs +4 -4
  18. package/dist/federation/hashlink-media.test.mjs +2 -2
  19. package/dist/federation/idempotency.test.mjs +2 -2
  20. package/dist/federation/inbox-report.test.mjs +5 -5
  21. package/dist/federation/inbox.test.mjs +1 -1
  22. package/dist/federation/metrics.test.mjs +1 -1
  23. package/dist/federation/middleware.test.mjs +465 -32
  24. package/dist/federation/mod.cjs +1 -1
  25. package/dist/federation/mod.d.cts +3 -3
  26. package/dist/federation/mod.d.ts +3 -3
  27. package/dist/federation/mod.js +1 -1
  28. package/dist/federation/outgoing-proof.test.mjs +9 -6
  29. package/dist/federation/portable-actor.test.mjs +3 -3
  30. package/dist/federation/portable-chain.test.mjs +3 -3
  31. package/dist/federation/portable-collection.test.mjs +4 -4
  32. package/dist/federation/portable-delivery.test.mjs +2 -2
  33. package/dist/federation/portable-gateway-keys.test.mjs +11 -5
  34. package/dist/federation/portable-inbox.test.mjs +4 -4
  35. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  36. package/dist/federation/portable-route-activity.test.mjs +2 -2
  37. package/dist/federation/portable.test.mjs +3 -3
  38. package/dist/federation/send.test.mjs +498 -88
  39. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  40. package/dist/federation/tasks/tasks.test.mjs +2 -2
  41. package/dist/federation/temporal.test.mjs +1 -1
  42. package/dist/federation/webfinger.test.mjs +1 -1
  43. package/dist/{federation-Dd9IwOEi.cjs → federation-BtPz9sr3.cjs} +240 -102
  44. package/dist/{http-D7L2pqLT.mjs → http-BSARoDPh.mjs} +204 -106
  45. package/dist/{http-DLf8QYD6.cjs → http-CeVcKQMZ.cjs} +179 -110
  46. package/dist/{http-BVrOR6MG.js → http-rH230xCD.js} +178 -109
  47. package/dist/{inbox-DtEKOr3W.mjs → inbox-BdZenpZQ.mjs} +2 -2
  48. package/dist/{key-DYxuyd-s.d.cts → key-CyCcut3h.d.ts} +1 -2
  49. package/dist/{key-C1nv7_jD.d.ts → key-VEL1QqHF.d.cts} +1 -2
  50. package/dist/{kv-cache-Dkzuqs8B.cjs → kv-cache-BpGIONmd.cjs} +72 -27
  51. package/dist/{kv-cache-DoZoRS9p.js → kv-cache-CNfNI_95.js} +73 -28
  52. package/dist/{kv-cache-m3RIr811.mjs → kv-cache-Cxn7pNMx.mjs} +1 -1
  53. package/dist/{ld-PJLGA2MJ.mjs → ld-DJZrjNuN.mjs} +2 -2
  54. package/dist/{metrics-CGR9cI-Y.mjs → metrics-DtxsYIkN.mjs} +1 -1
  55. package/dist/{middleware-BxhBd7pw.js → middleware-DDvck2q9.js} +241 -103
  56. package/dist/{middleware-ByanhTvb.mjs → middleware-DblGj642.mjs} +82 -42
  57. package/dist/{middleware-DPI-Jp9w.mjs → middleware-DyEjt33n.mjs} +1 -1
  58. package/dist/{mod-DEZXwV6J.d.ts → mod-B1Qt5cQQ.d.ts} +2 -9
  59. package/dist/{mod-Xj5P4s4d.d.ts → mod-B5COx5Wd.d.ts} +1 -1
  60. package/dist/{mod-CavhAgLw.d.cts → mod-BTxJG7ZH.d.cts} +1 -1
  61. package/dist/{mod-CtpF2BU5.d.ts → mod-CpyjFA9s.d.ts} +1 -1
  62. package/dist/{mod-BojP2Bw3.d.cts → mod-makPMVMg.d.cts} +1 -1
  63. package/dist/{mod-Dw-nPxQh.d.cts → mod-ss0wBUH1.d.cts} +2 -9
  64. package/dist/mod.cjs +5 -5
  65. package/dist/mod.d.cts +6 -6
  66. package/dist/mod.d.ts +6 -6
  67. package/dist/mod.js +5 -5
  68. package/dist/nodeinfo/client.test.mjs +337 -19
  69. package/dist/nodeinfo/handler.test.mjs +1 -1
  70. package/dist/nodeinfo/mod.cjs +1 -1
  71. package/dist/nodeinfo/mod.d.cts +1 -1
  72. package/dist/nodeinfo/mod.d.ts +1 -1
  73. package/dist/nodeinfo/mod.js +1 -1
  74. package/dist/otel/exporter.test.mjs +292 -30
  75. package/dist/otel/mod.cjs +36 -30
  76. package/dist/otel/mod.d.cts +13 -0
  77. package/dist/otel/mod.d.ts +13 -0
  78. package/dist/otel/mod.js +36 -30
  79. package/dist/{owner-B5c276hH.mjs → owner-2vfqhUJw.mjs} +3 -3
  80. package/dist/{portable-collection-BMZY9qT-.js → portable-collection-B7AQ_dW7.js} +1 -1
  81. package/dist/{portable-collection-CR5uwjws.cjs → portable-collection-Cs--KD5A.cjs} +1 -1
  82. package/dist/{portable-collection-C88tBiy0.mjs → portable-collection-DKJwGO8S.mjs} +2 -2
  83. package/dist/{portable-key-DxTXUpCK.mjs → portable-key-DXGepvtD.mjs} +1 -1
  84. package/dist/{proof-DNu-qP0i.mjs → proof-DW0FK6Cp.mjs} +3 -3
  85. package/dist/{send-CdCifQs4.mjs → send-CqsocpvF.mjs} +174 -76
  86. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  87. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  88. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  89. package/dist/sig/compound-proof.test.mjs +2 -2
  90. package/dist/sig/http.test.mjs +553 -2
  91. package/dist/sig/key.test.mjs +1 -1
  92. package/dist/sig/ld.test.mjs +1 -1
  93. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  94. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  95. package/dist/sig/mod.cjs +2 -2
  96. package/dist/sig/mod.d.cts +2 -2
  97. package/dist/sig/mod.d.ts +2 -2
  98. package/dist/sig/mod.js +2 -2
  99. package/dist/sig/owner.test.mjs +1 -1
  100. package/dist/sig/portable-collection.test.mjs +1 -1
  101. package/dist/sig/portable-interop.test.mjs +3 -3
  102. package/dist/sig/portable-key.test.mjs +5 -5
  103. package/dist/sig/proof.test.mjs +4 -4
  104. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  105. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  106. package/dist/sig/verification.test.mjs +3 -3
  107. package/dist/{temporal-BuMu2bgP.mjs → temporal-Dm2E1-vs.mjs} +1 -1
  108. package/dist/testing/kv.test.mjs +1 -1
  109. package/dist/testing/mod.d.mts +26 -0
  110. package/dist/{types-_S2_MeLF.js → types-DUjZb4i6.js} +112 -63
  111. package/dist/{types-poIao40f.cjs → types-hKCCjii0.cjs} +111 -62
  112. package/dist/utils/docloader.test.mjs +344 -36
  113. package/dist/utils/kv-cache.test.mjs +1 -1
  114. package/dist/utils/mod.cjs +1 -1
  115. package/dist/utils/mod.d.cts +1 -1
  116. package/dist/utils/mod.d.ts +1 -1
  117. package/dist/utils/mod.js +1 -1
  118. package/package.json +7 -7
  119. package/dist/docloader-DQNRDYDb.mjs +0 -91
@@ -4,7 +4,7 @@ globalThis.addEventListener = () => {};
4
4
  import { t as MemoryKvStore } from "../../kv-DCHkaJ9-.mjs";
5
5
  import { a as numberSchema, i as makeSchema, n as baseOptions, o as stringSchema, t as MockQueue } from "../../tasks-IG3pHfvZ.mjs";
6
6
  import "../../testing-t8xDeJZ5.mjs";
7
- import { o as createFederation } from "../../middleware-ByanhTvb.mjs";
7
+ import { o as createFederation } from "../../middleware-DblGj642.mjs";
8
8
  import { n as ParallelMessageQueue } from "../../mq-Dm1Wmtm-.mjs";
9
9
  import { configure, reset } from "@logtape/logtape";
10
10
  import { delay } from "es-toolkit";
@@ -4,8 +4,8 @@ globalThis.addEventListener = () => {};
4
4
  import { t as MemoryKvStore } from "../../kv-DCHkaJ9-.mjs";
5
5
  import { a as numberSchema, i as makeSchema, n as baseOptions, o as stringSchema, t as MockQueue } from "../../tasks-IG3pHfvZ.mjs";
6
6
  import "../../testing-t8xDeJZ5.mjs";
7
- import { o as createFederation } from "../../middleware-ByanhTvb.mjs";
8
- import { r as createFederationBuilder } from "../../builder-BBUGd9wr.mjs";
7
+ import { o as createFederation } from "../../middleware-DblGj642.mjs";
8
+ import { r as createFederationBuilder } from "../../builder-kj5boG2l.mjs";
9
9
  import { t as TaskCodec } from "../../codec-C6rTrpIp.mjs";
10
10
  import { t as InProcessMessageQueue } from "../../mq-Dm1Wmtm-.mjs";
11
11
  import { W3CTraceContextPropagator } from "@opentelemetry/core";
@@ -4,7 +4,7 @@ globalThis.addEventListener = () => {};
4
4
  import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
5
5
  import "../std__assert-DvMDRPes.mjs";
6
6
  import { t as assert } from "../assert-D1Eafmd_.mjs";
7
- import { t as hasMalformedKnownTemporalLiteral } from "../temporal-BuMu2bgP.mjs";
7
+ import { t as hasMalformedKnownTemporalLiteral } from "../temporal-Dm2E1-vs.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 () => {
@@ -6,7 +6,7 @@ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
6
6
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
7
7
  import { t as assertNotEquals } from "../assert_not_equals-CpPK2t89.mjs";
8
8
  import "../std__assert-DvMDRPes.mjs";
9
- import { o as createFederation, s as handleWebFinger } from "../middleware-ByanhTvb.mjs";
9
+ import { o as createFederation, s as handleWebFinger } from "../middleware-DblGj642.mjs";
10
10
  import { Image, Link, Person, Tombstone } from "@fedify/vocab";
11
11
  import { parseIri } from "@fedify/vocab-runtime";
12
12
  import { createTestMeterProvider, test } from "@fedify/fixture";
@@ -1,10 +1,10 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  const require_rolldown_runtime = require("./rolldown-runtime-Dmm3t92W.cjs");
3
3
  const require_transformers = require("./transformers-BalCrSsr.cjs");
4
- const require_http = require("./http-DLf8QYD6.cjs");
5
- const require_types = require("./types-poIao40f.cjs");
6
- const require_portable_collection = require("./portable-collection-CR5uwjws.cjs");
7
- const require_kv_cache = require("./kv-cache-Dkzuqs8B.cjs");
4
+ const require_http = require("./http-CeVcKQMZ.cjs");
5
+ const require_types = require("./types-hKCCjii0.cjs");
6
+ const require_portable_collection = require("./portable-collection-Cs--KD5A.cjs");
7
+ const require_kv_cache = require("./kv-cache-BpGIONmd.cjs");
8
8
  let _logtape_logtape = require("@logtape/logtape");
9
9
  let _fedify_uri_template = require("@fedify/uri-template");
10
10
  let _fedify_vocab = require("@fedify/vocab");
@@ -16,6 +16,7 @@ let _fedify_vocab_runtime_jsonld = require("@fedify/vocab-runtime/jsonld");
16
16
  _fedify_vocab_runtime_jsonld = require_rolldown_runtime.__toESM(_fedify_vocab_runtime_jsonld, 1);
17
17
  let es_toolkit = require("es-toolkit");
18
18
  let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
19
+ let _fedify_vocab_runtime_internal = require("@fedify/vocab-runtime/internal");
19
20
  let _fedify_webfinger = require("@fedify/webfinger");
20
21
  let _opentelemetry_sdk_metrics = require("@opentelemetry/sdk-metrics");
21
22
  let devalue = require("devalue");
@@ -3620,9 +3621,10 @@ function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
3620
3621
  *
3621
3622
  * @param parameters The parameters for sending the activity.
3622
3623
  * See also {@link SendActivityParameters}.
3624
+ * @param deps The dependencies, for tests.
3623
3625
  * @throws {Error} If the activity fails to send.
3624
3626
  */
3625
- function sendActivity(options) {
3627
+ function sendActivity(options, deps = {}) {
3626
3628
  const tracerProvider = options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider();
3627
3629
  return tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan("activitypub.send_activity", {
3628
3630
  kind: _opentelemetry_api.SpanKind.CLIENT,
@@ -3634,7 +3636,7 @@ function sendActivity(options) {
3634
3636
  await sendActivityInternal({
3635
3637
  ...options,
3636
3638
  tracerProvider
3637
- }, span);
3639
+ }, span, deps);
3638
3640
  } catch (e) {
3639
3641
  span.setStatus({
3640
3642
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -3719,13 +3721,14 @@ async function readLimitedResponseBody(response, maxBytes) {
3719
3721
  totalBytes += value.length;
3720
3722
  }
3721
3723
  } finally {
3724
+ if (truncated) reader.cancel().catch(() => {});
3722
3725
  reader.releaseLock();
3723
3726
  }
3724
3727
  let result = chunks.join("");
3725
3728
  if (truncated) result += "… (truncated)";
3726
3729
  return result;
3727
3730
  }
3728
- async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
3731
+ async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span, deps) {
3729
3732
  const logger = (0, _logtape_logtape.getLogger)([
3730
3733
  "fedify",
3731
3734
  "federation",
@@ -3734,9 +3737,11 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
3734
3737
  const federationMetrics = require_http.getFederationMetrics(meterProvider);
3735
3738
  const started = performance.now();
3736
3739
  let deliverySuccess = false;
3737
- async function validateUrl(url) {
3738
- if (!allowPrivateAddress) try {
3739
- await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
3740
+ const openUnvalidatedHop = deps.openHop ?? (allowPrivateAddress ? _fedify_vocab_runtime_internal.openDirectFetch : (url) => (0, _fedify_vocab_runtime_internal.openPublicFetch)(url));
3741
+ async function openHop(url) {
3742
+ let session;
3743
+ try {
3744
+ session = await openUnvalidatedHop(url);
3740
3745
  } catch (error) {
3741
3746
  if (error instanceof _fedify_vocab_runtime.UrlError && error.reason === "dns") {
3742
3747
  logger.error("DNS resolution failed for URL: {url}", {
@@ -3753,82 +3758,90 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
3753
3758
  });
3754
3759
  throw error;
3755
3760
  }
3761
+ return session;
3756
3762
  }
3763
+ let initialSession;
3757
3764
  try {
3758
- await validateUrl(inbox.href);
3765
+ initialSession = await openHop(inbox.href);
3759
3766
  } catch (error) {
3760
3767
  if (error instanceof _fedify_vocab_runtime.FetchError) federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
3761
3768
  throw error;
3762
3769
  }
3763
- headers = new Headers(headers);
3764
- headers.set("Content-Type", "application/activity+json");
3765
- const request = new Request(inbox, {
3766
- method: "POST",
3767
- headers,
3768
- body: JSON.stringify(activity),
3769
- redirect: "manual"
3770
- });
3771
- let rsaKey = null;
3772
- for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
3773
- rsaKey = key;
3774
- break;
3775
- }
3776
- if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
3777
- activityId,
3778
- inbox: inbox.href
3779
- });
3780
- else if (rsaKey == null) logger.warn("No supported key found to sign the request to {inbox}. The request will be sent without a signature. In order to sign the request, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
3781
- inbox: inbox.href,
3782
- keys: keys.map((pair) => ({
3783
- keyId: pair.keyId.href,
3784
- privateKey: pair.privateKey
3785
- }))
3786
- });
3787
- let response;
3770
+ let initialPending = true;
3771
+ const openDeliveryHop = (url) => {
3772
+ if (initialPending) {
3773
+ initialPending = false;
3774
+ return Promise.resolve(initialSession);
3775
+ }
3776
+ return openHop(url);
3777
+ };
3788
3778
  try {
3789
- response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await require_http.doubleKnock(request, rsaKey, {
3790
- tracerProvider,
3791
- specDeterminer,
3792
- validateRedirect: validateUrl
3779
+ headers = new Headers(headers);
3780
+ headers.set("Content-Type", "application/activity+json");
3781
+ const request = new Request(inbox, {
3782
+ method: "POST",
3783
+ headers,
3784
+ body: JSON.stringify(activity),
3785
+ redirect: "manual"
3793
3786
  });
3794
- } catch (error) {
3795
- const failure = error instanceof _fedify_vocab_runtime.FetchError || error instanceof _fedify_vocab_runtime.UrlError ? error : createFetchError(inbox.href, error);
3796
- logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
3787
+ let rsaKey = null;
3788
+ for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
3789
+ rsaKey = key;
3790
+ break;
3791
+ }
3792
+ if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
3797
3793
  activityId,
3794
+ inbox: inbox.href
3795
+ });
3796
+ else if (rsaKey == null) logger.warn("No supported key found to sign the request to {inbox}. The request will be sent without a signature. In order to sign the request, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
3798
3797
  inbox: inbox.href,
3799
- error: failure
3798
+ keys: keys.map((pair) => ({
3799
+ keyId: pair.keyId.href,
3800
+ privateKey: pair.privateKey
3801
+ }))
3800
3802
  });
3801
- federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
3802
- throw failure;
3803
- }
3804
- try {
3805
- if (!response.ok) {
3806
- let error;
3807
- try {
3808
- error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
3809
- } catch (_) {
3810
- error = "";
3811
- }
3812
- logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
3803
+ let outcome;
3804
+ try {
3805
+ outcome = rsaKey == null ? await fetchWithValidatedRedirects(request, openDeliveryHop, (response) => consumeSendActivityResponse(response, inbox)) : await require_http.doubleKnockScoped(request, rsaKey, {
3806
+ tracerProvider,
3807
+ specDeterminer
3808
+ }, openDeliveryHop, (response) => consumeSendActivityResponse(response, inbox));
3809
+ } catch (error) {
3810
+ const failure = error instanceof _fedify_vocab_runtime.FetchError || error instanceof _fedify_vocab_runtime.UrlError ? error : createFetchError(inbox.href, error);
3811
+ logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
3813
3812
  activityId,
3814
3813
  inbox: inbox.href,
3815
- status: response.status,
3816
- statusText: response.statusText,
3817
- error
3814
+ error: failure
3818
3815
  });
3819
- throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error, response.headers);
3816
+ federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
3817
+ throw failure;
3818
+ }
3819
+ try {
3820
+ if (!outcome.ok) {
3821
+ const { status, statusText, error, responseHeaders } = outcome;
3822
+ logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
3823
+ activityId,
3824
+ inbox: inbox.href,
3825
+ status,
3826
+ statusText,
3827
+ error
3828
+ });
3829
+ throw new SendActivityError(inbox, status, `Failed to send activity ${activityId} to ${inbox.href} (${status} ${statusText}):\n${error}`, error, responseHeaders);
3830
+ }
3831
+ deliverySuccess = true;
3832
+ const eventAttributes = {
3833
+ "activitypub.inbox.url": inbox.href,
3834
+ "activitypub.activity.id": activityId ?? ""
3835
+ };
3836
+ if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
3837
+ const actorId = getActivityActorId(activity);
3838
+ if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
3839
+ span.addEvent("activitypub.activity.sent", eventAttributes);
3840
+ } finally {
3841
+ federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), deliverySuccess, activityType);
3820
3842
  }
3821
- deliverySuccess = true;
3822
- const eventAttributes = {
3823
- "activitypub.inbox.url": inbox.href,
3824
- "activitypub.activity.id": activityId ?? ""
3825
- };
3826
- if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
3827
- const actorId = getActivityActorId(activity);
3828
- if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
3829
- span.addEvent("activitypub.activity.sent", eventAttributes);
3830
3843
  } finally {
3831
- federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), deliverySuccess, activityType);
3844
+ if (initialPending) await initialSession.close();
3832
3845
  }
3833
3846
  }
3834
3847
  function createFetchError(url, cause) {
@@ -3836,10 +3849,75 @@ function createFetchError(url, cause) {
3836
3849
  error.cause = cause;
3837
3850
  return error;
3838
3851
  }
3839
- async function fetchWithValidatedRedirects(request, validateUrl) {
3852
+ /**
3853
+ * Starts cancelling the body of a response without waiting for it to settle.
3854
+ * The cancellation of a branch of a tee (e.g., a body cloned by a fetch mock)
3855
+ * settles only after the other branch is cancelled too, per the Streams
3856
+ * standard, so awaiting it could block forever. Failures are only logged.
3857
+ */
3858
+ function cancelResponseBody(response, url) {
3859
+ response.body?.cancel().catch((error) => {
3860
+ (0, _logtape_logtape.getLogger)([
3861
+ "fedify",
3862
+ "federation",
3863
+ "outbox"
3864
+ ]).debug("Failed to cancel the response body from {url}: {error}", {
3865
+ url,
3866
+ error
3867
+ });
3868
+ });
3869
+ }
3870
+ /**
3871
+ * Reads what is needed from the final response of a delivery while its
3872
+ * session is open: the body of a successful response is cancelled, and at
3873
+ * most 1 KiB of an error response body is read.
3874
+ */
3875
+ async function consumeSendActivityResponse(response, inbox) {
3876
+ if (response.ok) {
3877
+ cancelResponseBody(response, response.url || inbox.href);
3878
+ return { ok: true };
3879
+ }
3880
+ let error;
3881
+ try {
3882
+ error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
3883
+ } catch (_) {
3884
+ error = "";
3885
+ }
3886
+ return {
3887
+ ok: false,
3888
+ status: response.status,
3889
+ statusText: response.statusText,
3890
+ error,
3891
+ responseHeaders: new Headers(response.headers)
3892
+ };
3893
+ }
3894
+ /**
3895
+ * Sends a request without signing it, following redirects manually like the
3896
+ * Fetch standard does. Every hop is opened with `openHop()`, which validates
3897
+ * the destination, after the previous hop is closed. The final response is
3898
+ * passed to `consume()`, and its session is closed after that.
3899
+ * This is exported only for tests; it is not exported from any entry point.
3900
+ * @param request The request to send.
3901
+ * @param openHop The function to open a session for each hop.
3902
+ * @param consume The function to consume the final response.
3903
+ * @returns The value returned by `consume`.
3904
+ */
3905
+ async function fetchWithValidatedRedirects(request, openHop, consume) {
3906
+ const logger = (0, _logtape_logtape.getLogger)([
3907
+ "fedify",
3908
+ "federation",
3909
+ "outbox"
3910
+ ]);
3840
3911
  let body = await request.clone().text();
3841
3912
  for (let redirects = 0;; redirects++) {
3842
- const response = await fetch(request, { redirect: "manual" });
3913
+ const session = await openHop(request.url);
3914
+ let response;
3915
+ try {
3916
+ response = await session.fetch(request, { redirect: "manual" });
3917
+ } catch (error) {
3918
+ await closeAfterError(session, logger);
3919
+ throw error;
3920
+ }
3843
3921
  const location = response.headers.get("Location");
3844
3922
  if (![
3845
3923
  301,
@@ -3847,11 +3925,21 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
3847
3925
  303,
3848
3926
  307,
3849
3927
  308
3850
- ].includes(response.status) || location == null) return response;
3851
- await response.body?.cancel();
3928
+ ].includes(response.status) || location == null) try {
3929
+ return await consume(response);
3930
+ } catch (error) {
3931
+ logger.debug("Failed to consume the response from {url}; closing the session: {error}", {
3932
+ url: request.url,
3933
+ error
3934
+ });
3935
+ throw error;
3936
+ } finally {
3937
+ await session.close();
3938
+ }
3939
+ cancelResponseBody(response, request.url);
3940
+ await session.close();
3852
3941
  if (redirects >= 20) throw new TypeError("Too many redirects");
3853
3942
  const url = new URL(location, request.url);
3854
- await validateUrl(url.href);
3855
3943
  const headers = new Headers(request.headers);
3856
3944
  if (url.origin !== new URL(request.url).origin) {
3857
3945
  headers.delete("Authorization");
@@ -3879,6 +3967,16 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
3879
3967
  });
3880
3968
  }
3881
3969
  }
3970
+ async function closeAfterError(session, logger) {
3971
+ try {
3972
+ await session.close();
3973
+ } catch (error) {
3974
+ logger.debug("Failed to close the session for {url}: {error}", {
3975
+ url: session.url.href,
3976
+ error
3977
+ });
3978
+ }
3979
+ }
3882
3980
  /**
3883
3981
  * An error that is thrown when an activity fails to send to a remote inbox.
3884
3982
  * It contains structured information about the failure, including the HTTP
@@ -9920,7 +10018,7 @@ var FederationImpl = class extends FederationBuilderImpl {
9920
10018
  onNotAcceptable ??= notAcceptable;
9921
10019
  onUnauthorized ??= unauthorized;
9922
10020
  const url = new URL(request.url);
9923
- const route = this.router.route(url.pathname);
10021
+ const route = this.router.route(url.pathname, { allowUnencodedColon: true });
9924
10022
  if (route == null) {
9925
10023
  const media = this.hashlinkMediaDispatcher == null ? null : parseHashlinkGatewayRequest(url);
9926
10024
  if (media != null) return await this.#fetchHashlinkMedia(request, media, {
@@ -11653,45 +11751,85 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
11653
11751
  if (object instanceof _fedify_vocab.Tombstone && !(object instanceof cls) && options?.tombstone !== "passthrough") return null;
11654
11752
  return object;
11655
11753
  }
11656
- #signedKey = void 0;
11754
+ #signedKey = null;
11755
+ #signedKeyOwner = null;
11756
+ #signatureChecks = /* @__PURE__ */ new WeakMap();
11757
+ #getSignatureCheck(documentLoader, contextLoader) {
11758
+ let checks = this.#signatureChecks.get(documentLoader);
11759
+ if (checks == null) {
11760
+ checks = /* @__PURE__ */ new WeakMap();
11761
+ this.#signatureChecks.set(documentLoader, checks);
11762
+ }
11763
+ let check = checks.get(contextLoader);
11764
+ if (check == null) {
11765
+ check = {};
11766
+ checks.set(contextLoader, check);
11767
+ }
11768
+ return check;
11769
+ }
11657
11770
  async getSignedKey(options = {}) {
11658
11771
  if (this.#signedKey != null) return this.#signedKey;
11659
- return this.#signedKey = await require_http.verifyRequest(this.request, {
11772
+ const documentLoader = options.documentLoader ?? this.documentLoader;
11773
+ const contextLoader = options.contextLoader ?? this.contextLoader;
11774
+ const check = this.#getSignatureCheck(documentLoader, contextLoader);
11775
+ if (check.key != null) return check.key;
11776
+ const promise = require_http.verifyRequest(this.request, {
11660
11777
  ...this,
11661
- contextLoader: options.contextLoader ?? this.contextLoader,
11662
- documentLoader: options.documentLoader ?? this.documentLoader,
11778
+ contextLoader,
11779
+ documentLoader,
11663
11780
  timeWindow: this.federation.signatureTimeWindow,
11664
11781
  maxSignatures: this.federation.maxHttpSignatures,
11665
11782
  meterProvider: this.meterProvider,
11666
11783
  tracerProvider: options.tracerProvider ?? this.tracerProvider
11784
+ }).then((key) => {
11785
+ if (key != null) this.#signedKey ??= key;
11786
+ return key;
11787
+ }).catch((error) => {
11788
+ if (check.key === promise) check.key = void 0;
11789
+ throw error;
11667
11790
  });
11791
+ check.key = promise;
11792
+ return await promise;
11668
11793
  }
11669
- #signedKeyOwner = void 0;
11670
11794
  async getSignedKeyOwner(options = {}) {
11671
11795
  if (this.#signedKeyOwner != null) return this.#signedKeyOwner;
11672
- const key = await this.getSignedKey(options);
11673
- if (key == null) return this.#signedKeyOwner = null;
11674
- try {
11675
- return this.#signedKeyOwner = await require_portable_collection.getKeyOwner(key, {
11676
- contextLoader: options.contextLoader ?? this.contextLoader,
11677
- documentLoader: options.documentLoader ?? this.documentLoader,
11678
- tracerProvider: options.tracerProvider ?? this.tracerProvider
11679
- });
11680
- } catch (error) {
11681
- if (error instanceof _fedify_vocab_runtime.FetchError) {
11682
- (0, _logtape_logtape.getLogger)([
11683
- "fedify",
11684
- "federation",
11685
- "actor"
11686
- ]).warn("Failed to fetch the key owner {keyOwner} of {keyId} while verifying the request signature; treating the request as unauthenticated: {error}", {
11687
- keyId: key.id?.href,
11688
- keyOwner: error.url.href,
11689
- error
11690
- });
11691
- return this.#signedKeyOwner = null;
11796
+ const normalizedOptions = {
11797
+ documentLoader: options.documentLoader ?? this.documentLoader,
11798
+ contextLoader: options.contextLoader ?? this.contextLoader,
11799
+ tracerProvider: options.tracerProvider ?? this.tracerProvider
11800
+ };
11801
+ const check = this.#getSignatureCheck(normalizedOptions.documentLoader, normalizedOptions.contextLoader);
11802
+ if (check.owner != null) return check.owner;
11803
+ const promise = this.getSignedKey(normalizedOptions).then(async (key) => {
11804
+ if (key == null) {
11805
+ if (check.owner === promise) check.owner = void 0;
11806
+ return null;
11807
+ }
11808
+ try {
11809
+ const owner = await require_portable_collection.getKeyOwner(key, normalizedOptions);
11810
+ if (owner != null) this.#signedKeyOwner ??= owner;
11811
+ return owner;
11812
+ } catch (error) {
11813
+ if (error instanceof _fedify_vocab_runtime.FetchError) {
11814
+ (0, _logtape_logtape.getLogger)([
11815
+ "fedify",
11816
+ "federation",
11817
+ "actor"
11818
+ ]).warn("Failed to fetch the key owner {keyOwner} of {keyId} while verifying the request signature; treating the request as unauthenticated: {error}", {
11819
+ keyId: key.id?.href,
11820
+ keyOwner: error.url.href,
11821
+ error
11822
+ });
11823
+ return null;
11824
+ }
11825
+ throw error;
11692
11826
  }
11827
+ }).catch((error) => {
11828
+ if (check.owner === promise) check.owner = void 0;
11693
11829
  throw error;
11694
- }
11830
+ });
11831
+ check.owner = promise;
11832
+ return await promise;
11695
11833
  }
11696
11834
  async isSignedByAudience(object, options = {}) {
11697
11835
  return await isInAudience(object, null) || await isInAudience(object, await this.getSignedKeyOwner(options), options);