@fedify/fedify 2.5.0-dev.2448 → 2.5.0-dev.2492

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 (126) hide show
  1. package/README.md +3 -0
  2. package/dist/{builder-DbC8bn6k.mjs → builder-DphK9v7f.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-heeXW2_A.mjs → compound-proof-cYtvlTgF.mjs} +2 -2
  10. package/dist/{context-GlHrzdSP.d.ts → context-DQJE9YfH.d.ts} +29 -3
  11. package/dist/{context-BazwQrHn.d.cts → context-DSuL3YWx.d.cts} +29 -3
  12. package/dist/{deno-Dpewt_jJ.mjs → deno-iOAA7iRN.mjs} +1 -1
  13. package/dist/docloader-DCZkiTeF.mjs +136 -0
  14. package/dist/{exporter-DThFfe7P.mjs → exporter-DVG68ozH.mjs} +36 -30
  15. package/dist/federation/builder.test.mjs +1 -1
  16. package/dist/federation/compound-fanout.test.mjs +6 -3
  17. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  18. package/dist/federation/handler.test.mjs +5 -5
  19. package/dist/federation/hashlink-media.test.mjs +2 -2
  20. package/dist/federation/idempotency.test.mjs +2 -2
  21. package/dist/federation/inbox-actorless.test.mjs +5 -5
  22. package/dist/federation/inbox-report.test.mjs +5 -5
  23. package/dist/federation/inbox-view.test.mjs +1 -1
  24. package/dist/federation/inbox.test.mjs +1 -1
  25. package/dist/federation/metrics.test.mjs +1 -1
  26. package/dist/federation/middleware.test.mjs +468 -33
  27. package/dist/federation/mod.cjs +1 -1
  28. package/dist/federation/mod.d.cts +3 -3
  29. package/dist/federation/mod.d.ts +3 -3
  30. package/dist/federation/mod.js +1 -1
  31. package/dist/federation/mq.test.mjs +1 -1
  32. package/dist/federation/outgoing-proof.test.mjs +9 -6
  33. package/dist/federation/portable-actor.test.mjs +3 -3
  34. package/dist/federation/portable-chain.test.mjs +3 -3
  35. package/dist/federation/portable-collection.test.mjs +4 -4
  36. package/dist/federation/portable-delivery.test.mjs +2 -2
  37. package/dist/federation/portable-gateway-keys.test.mjs +11 -5
  38. package/dist/federation/portable-inbox.test.mjs +4 -4
  39. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  40. package/dist/federation/portable-route-activity.test.mjs +2 -2
  41. package/dist/federation/portable.test.mjs +3 -3
  42. package/dist/federation/send.test.mjs +502 -93
  43. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  44. package/dist/federation/tasks/tasks.test.mjs +2 -2
  45. package/dist/federation/temporal.test.mjs +1 -1
  46. package/dist/federation/webfinger.test.mjs +1 -1
  47. package/dist/{federation-D8YexkP1.cjs → federation-D0AZw_aw.cjs} +246 -108
  48. package/dist/{http-BZkyQIkb.mjs → http-BoJCMnjY.mjs} +204 -106
  49. package/dist/{http-BBr-VLil.js → http-ChXUU_3c.js} +178 -109
  50. package/dist/{http-CAl7Num5.cjs → http-kv1LZw-r.cjs} +179 -110
  51. package/dist/{inbox-wDuMKaFW.mjs → inbox-BjihTKLo.mjs} +2 -2
  52. package/dist/{inbox-view-NE9wXdLo.mjs → inbox-view-D-skypg-.mjs} +1 -1
  53. package/dist/{key-nYGY4QB9.d.cts → key-DbLNT-vA.d.ts} +1 -2
  54. package/dist/{key-DhsBxxZN.d.ts → key-DlHT4R6k.d.cts} +1 -2
  55. package/dist/{kv-cache-CTQSfkmp.mjs → kv-cache-Cmsyke4d.mjs} +1 -1
  56. package/dist/{kv-cache-U7uz5nx4.js → kv-cache-DQcte7ES.js} +73 -28
  57. package/dist/{kv-cache-CjgCOz3i.cjs → kv-cache-Ddh5sMxH.cjs} +72 -27
  58. package/dist/{ld-Cel1RRZ5.d.cts → ld-CWVpo_NM.d.cts} +1 -1
  59. package/dist/{ld-CQFuiah9.mjs → ld-CdvcOEXQ.mjs} +2 -2
  60. package/dist/{ld-CcuZ4NZa.d.ts → ld-Inagww4R.d.ts} +1 -1
  61. package/dist/{metrics-B-QxRSSR.mjs → metrics-CpxvUrBz.mjs} +1 -1
  62. package/dist/{middleware-P-AZEQMn.js → middleware-BHb5toqA.js} +247 -109
  63. package/dist/{middleware-BdlNa8BD.mjs → middleware-BKs2DLUF.mjs} +1 -1
  64. package/dist/{middleware-CuVa0UuS.mjs → middleware-CoBLrTKX.mjs} +89 -49
  65. package/dist/{mod-DvWOv2Ho.d.cts → mod-6TQ4UQZb.d.cts} +1 -1
  66. package/dist/{mod-BHQE-7WQ.d.ts → mod-B4kBw1-8.d.ts} +1 -1
  67. package/dist/{mod-DkhfZC1u.d.ts → mod-B7j30IQf.d.ts} +2 -9
  68. package/dist/{mod-B2783_3v.d.cts → mod-C1nT74IG.d.cts} +1 -1
  69. package/dist/{mod-DjZfJj9R.d.ts → mod-CIeBgsOC.d.ts} +1 -1
  70. package/dist/{mod-B5-wdzmJ.d.cts → mod-YvA4Rk8J.d.cts} +2 -9
  71. package/dist/mod.cjs +5 -5
  72. package/dist/mod.d.cts +7 -7
  73. package/dist/mod.d.ts +7 -7
  74. package/dist/mod.js +5 -5
  75. package/dist/nodeinfo/client.test.mjs +337 -19
  76. package/dist/nodeinfo/handler.test.mjs +1 -1
  77. package/dist/nodeinfo/mod.cjs +1 -1
  78. package/dist/nodeinfo/mod.d.cts +1 -1
  79. package/dist/nodeinfo/mod.d.ts +1 -1
  80. package/dist/nodeinfo/mod.js +1 -1
  81. package/dist/otel/exporter.test.mjs +257 -1
  82. package/dist/otel/mod.cjs +36 -30
  83. package/dist/otel/mod.d.cts +13 -0
  84. package/dist/otel/mod.d.ts +13 -0
  85. package/dist/otel/mod.js +36 -30
  86. package/dist/{owner-ccKkQOAf.mjs → owner-DTnVpUKA.mjs} +3 -3
  87. package/dist/{portable-collection-CndnpXfo.mjs → portable-collection-CJeYTL61.mjs} +2 -2
  88. package/dist/{portable-collection-TC-kvkVI.cjs → portable-collection-DciwBIia.cjs} +1 -1
  89. package/dist/{portable-collection-DUlQ3s2M.js → portable-collection-TmtpzD4E.js} +1 -1
  90. package/dist/{portable-key-D5tFOoV-.mjs → portable-key-DcYok4ki.mjs} +1 -1
  91. package/dist/{proof-DPoF06nz.mjs → proof-GH7Poos_.mjs} +3 -3
  92. package/dist/{send-CF44O1Df.mjs → send-BgGJh6pv.mjs} +174 -76
  93. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  94. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  95. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  96. package/dist/sig/compound-proof.test.mjs +2 -2
  97. package/dist/sig/http.test.mjs +553 -2
  98. package/dist/sig/key.test.mjs +1 -1
  99. package/dist/sig/ld.test.mjs +1 -1
  100. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  101. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  102. package/dist/sig/mod.cjs +2 -2
  103. package/dist/sig/mod.d.cts +3 -3
  104. package/dist/sig/mod.d.ts +3 -3
  105. package/dist/sig/mod.js +2 -2
  106. package/dist/sig/owner.test.mjs +1 -1
  107. package/dist/sig/portable-collection.test.mjs +1 -1
  108. package/dist/sig/portable-interop.test.mjs +3 -3
  109. package/dist/sig/portable-key.test.mjs +5 -5
  110. package/dist/sig/proof.test.mjs +4 -4
  111. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  112. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  113. package/dist/sig/verification.test.mjs +3 -3
  114. package/dist/{temporal-XlZ4Wb94.mjs → temporal-gJINzcL4.mjs} +1 -1
  115. package/dist/testing/kv.test.mjs +1 -1
  116. package/dist/testing/mod.d.mts +26 -0
  117. package/dist/{types-_S2_MeLF.js → types-DUjZb4i6.js} +112 -63
  118. package/dist/{types-poIao40f.cjs → types-hKCCjii0.cjs} +111 -62
  119. package/dist/utils/docloader.test.mjs +344 -36
  120. package/dist/utils/kv-cache.test.mjs +1 -1
  121. package/dist/utils/mod.cjs +1 -1
  122. package/dist/utils/mod.d.cts +1 -1
  123. package/dist/utils/mod.d.ts +1 -1
  124. package/dist/utils/mod.js +1 -1
  125. package/package.json +8 -8
  126. package/dist/docloader-CpsDyqVN.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-Dbr21vZk.mjs";
7
- import { o as createFederation } from "../../middleware-CuVa0UuS.mjs";
7
+ import { o as createFederation } from "../../middleware-CoBLrTKX.mjs";
8
8
  import { n as ParallelMessageQueue } from "../../mq-eF96QxUT.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-Dbr21vZk.mjs";
7
- import { o as createFederation } from "../../middleware-CuVa0UuS.mjs";
8
- import { r as createFederationBuilder } from "../../builder-DbC8bn6k.mjs";
7
+ import { o as createFederation } from "../../middleware-CoBLrTKX.mjs";
8
+ import { r as createFederationBuilder } from "../../builder-DphK9v7f.mjs";
9
9
  import { t as TaskCodec } from "../../codec-C6rTrpIp.mjs";
10
10
  import { t as InProcessMessageQueue } from "../../mq-eF96QxUT.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-XlZ4Wb94.mjs";
7
+ import { t as hasMalformedKnownTemporalLiteral } from "../temporal-gJINzcL4.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-CuVa0UuS.mjs";
9
+ import { o as createFederation, s as handleWebFinger } from "../middleware-CoBLrTKX.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-CAl7Num5.cjs");
5
- const require_types = require("./types-poIao40f.cjs");
6
- const require_portable_collection = require("./portable-collection-TC-kvkVI.cjs");
7
- const require_kv_cache = require("./kv-cache-CjgCOz3i.cjs");
4
+ const require_http = require("./http-kv1LZw-r.cjs");
5
+ const require_types = require("./types-hKCCjii0.cjs");
6
+ const require_portable_collection = require("./portable-collection-DciwBIia.cjs");
7
+ const require_kv_cache = require("./kv-cache-Ddh5sMxH.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");
@@ -18,6 +18,7 @@ let es_toolkit = require("es-toolkit");
18
18
  let json_canon = require("json-canon");
19
19
  json_canon = require_rolldown_runtime.__toESM(json_canon, 1);
20
20
  let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
21
+ let _fedify_vocab_runtime_internal = require("@fedify/vocab-runtime/internal");
21
22
  let _fedify_webfinger = require("@fedify/webfinger");
22
23
  let _opentelemetry_sdk_metrics = require("@opentelemetry/sdk-metrics");
23
24
  let devalue = require("devalue");
@@ -4319,9 +4320,10 @@ function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
4319
4320
  *
4320
4321
  * @param parameters The parameters for sending the activity.
4321
4322
  * See also {@link SendActivityParameters}.
4323
+ * @param deps The dependencies, for tests.
4322
4324
  * @throws {Error} If the activity fails to send.
4323
4325
  */
4324
- function sendActivity(options) {
4326
+ function sendActivity(options, deps = {}) {
4325
4327
  const tracerProvider = options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider();
4326
4328
  return tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan("activitypub.send_activity", {
4327
4329
  kind: _opentelemetry_api.SpanKind.CLIENT,
@@ -4337,7 +4339,7 @@ function sendActivity(options) {
4337
4339
  }, () => sendActivityInternal({
4338
4340
  ...options,
4339
4341
  tracerProvider
4340
- }, span));
4342
+ }, span, deps));
4341
4343
  } catch (e) {
4342
4344
  span.setStatus({
4343
4345
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -4422,13 +4424,14 @@ async function readLimitedResponseBody(response, maxBytes) {
4422
4424
  totalBytes += value.length;
4423
4425
  }
4424
4426
  } finally {
4427
+ if (truncated) reader.cancel().catch(() => {});
4425
4428
  reader.releaseLock();
4426
4429
  }
4427
4430
  let result = chunks.join("");
4428
4431
  if (truncated) result += "… (truncated)";
4429
4432
  return result;
4430
4433
  }
4431
- async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
4434
+ async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span, deps) {
4432
4435
  const logger = (0, _logtape_logtape.getLogger)([
4433
4436
  "fedify",
4434
4437
  "federation",
@@ -4437,9 +4440,11 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
4437
4440
  const federationMetrics = require_http.getFederationMetrics(meterProvider);
4438
4441
  const started = performance.now();
4439
4442
  let deliverySuccess = false;
4440
- async function validateUrl(url) {
4441
- if (!allowPrivateAddress) try {
4442
- await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
4443
+ const openUnvalidatedHop = deps.openHop ?? (allowPrivateAddress ? _fedify_vocab_runtime_internal.openDirectFetch : (url) => (0, _fedify_vocab_runtime_internal.openPublicFetch)(url));
4444
+ async function openHop(url) {
4445
+ let session;
4446
+ try {
4447
+ session = await openUnvalidatedHop(url);
4443
4448
  } catch (error) {
4444
4449
  if (error instanceof _fedify_vocab_runtime.UrlError && error.reason === "dns") {
4445
4450
  logger.error("DNS resolution failed for URL: {url}", {
@@ -4456,82 +4461,90 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
4456
4461
  });
4457
4462
  throw error;
4458
4463
  }
4464
+ return session;
4459
4465
  }
4466
+ let initialSession;
4460
4467
  try {
4461
- await validateUrl(inbox.href);
4468
+ initialSession = await openHop(inbox.href);
4462
4469
  } catch (error) {
4463
4470
  if (error instanceof _fedify_vocab_runtime.FetchError) federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
4464
4471
  throw error;
4465
4472
  }
4466
- headers = new Headers(headers);
4467
- headers.set("Content-Type", "application/activity+json");
4468
- const request = new Request(inbox, {
4469
- method: "POST",
4470
- headers,
4471
- body: JSON.stringify(activity),
4472
- redirect: "manual"
4473
- });
4474
- let rsaKey = null;
4475
- for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
4476
- rsaKey = key;
4477
- break;
4478
- }
4479
- if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
4480
- activityId,
4481
- inbox: inbox.href
4482
- });
4483
- 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.", {
4484
- inbox: inbox.href,
4485
- keys: keys.map((pair) => ({
4486
- keyId: pair.keyId.href,
4487
- privateKey: pair.privateKey
4488
- }))
4489
- });
4490
- let response;
4473
+ let initialPending = true;
4474
+ const openDeliveryHop = (url) => {
4475
+ if (initialPending) {
4476
+ initialPending = false;
4477
+ return Promise.resolve(initialSession);
4478
+ }
4479
+ return openHop(url);
4480
+ };
4491
4481
  try {
4492
- response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await require_http.doubleKnock(request, rsaKey, {
4493
- tracerProvider,
4494
- specDeterminer,
4495
- validateRedirect: validateUrl
4482
+ headers = new Headers(headers);
4483
+ headers.set("Content-Type", "application/activity+json");
4484
+ const request = new Request(inbox, {
4485
+ method: "POST",
4486
+ headers,
4487
+ body: JSON.stringify(activity),
4488
+ redirect: "manual"
4496
4489
  });
4497
- } catch (error) {
4498
- const failure = error instanceof _fedify_vocab_runtime.FetchError || error instanceof _fedify_vocab_runtime.UrlError ? error : createFetchError(inbox.href, error);
4499
- logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
4490
+ let rsaKey = null;
4491
+ for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
4492
+ rsaKey = key;
4493
+ break;
4494
+ }
4495
+ if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
4500
4496
  activityId,
4497
+ inbox: inbox.href
4498
+ });
4499
+ 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.", {
4501
4500
  inbox: inbox.href,
4502
- error: failure
4501
+ keys: keys.map((pair) => ({
4502
+ keyId: pair.keyId.href,
4503
+ privateKey: pair.privateKey
4504
+ }))
4503
4505
  });
4504
- federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
4505
- throw failure;
4506
- }
4507
- try {
4508
- if (!response.ok) {
4509
- let error;
4510
- try {
4511
- error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
4512
- } catch (_) {
4513
- error = "";
4514
- }
4515
- logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
4506
+ let outcome;
4507
+ try {
4508
+ outcome = rsaKey == null ? await fetchWithValidatedRedirects(request, openDeliveryHop, (response) => consumeSendActivityResponse(response, inbox)) : await require_http.doubleKnockScoped(request, rsaKey, {
4509
+ tracerProvider,
4510
+ specDeterminer
4511
+ }, openDeliveryHop, (response) => consumeSendActivityResponse(response, inbox));
4512
+ } catch (error) {
4513
+ const failure = error instanceof _fedify_vocab_runtime.FetchError || error instanceof _fedify_vocab_runtime.UrlError ? error : createFetchError(inbox.href, error);
4514
+ logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
4516
4515
  activityId,
4517
4516
  inbox: inbox.href,
4518
- status: response.status,
4519
- statusText: response.statusText,
4520
- error
4517
+ error: failure
4521
4518
  });
4522
- throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error, response.headers);
4519
+ federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
4520
+ throw failure;
4521
+ }
4522
+ try {
4523
+ if (!outcome.ok) {
4524
+ const { status, statusText, error, responseHeaders } = outcome;
4525
+ logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
4526
+ activityId,
4527
+ inbox: inbox.href,
4528
+ status,
4529
+ statusText,
4530
+ error
4531
+ });
4532
+ throw new SendActivityError(inbox, status, `Failed to send activity ${activityId} to ${inbox.href} (${status} ${statusText}):\n${error}`, error, responseHeaders);
4533
+ }
4534
+ deliverySuccess = true;
4535
+ const eventAttributes = {
4536
+ "activitypub.inbox.url": inbox.href,
4537
+ "activitypub.activity.id": activityId ?? ""
4538
+ };
4539
+ if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
4540
+ const actorId = getActivityActorId(activity);
4541
+ if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
4542
+ span.addEvent("activitypub.activity.sent", eventAttributes);
4543
+ } finally {
4544
+ federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), deliverySuccess, activityType);
4523
4545
  }
4524
- deliverySuccess = true;
4525
- const eventAttributes = {
4526
- "activitypub.inbox.url": inbox.href,
4527
- "activitypub.activity.id": activityId ?? ""
4528
- };
4529
- if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
4530
- const actorId = getActivityActorId(activity);
4531
- if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
4532
- span.addEvent("activitypub.activity.sent", eventAttributes);
4533
4546
  } finally {
4534
- federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), deliverySuccess, activityType);
4547
+ if (initialPending) await initialSession.close();
4535
4548
  }
4536
4549
  }
4537
4550
  function createFetchError(url, cause) {
@@ -4539,10 +4552,75 @@ function createFetchError(url, cause) {
4539
4552
  error.cause = cause;
4540
4553
  return error;
4541
4554
  }
4542
- async function fetchWithValidatedRedirects(request, validateUrl) {
4555
+ /**
4556
+ * Starts cancelling the body of a response without waiting for it to settle.
4557
+ * The cancellation of a branch of a tee (e.g., a body cloned by a fetch mock)
4558
+ * settles only after the other branch is cancelled too, per the Streams
4559
+ * standard, so awaiting it could block forever. Failures are only logged.
4560
+ */
4561
+ function cancelResponseBody(response, url) {
4562
+ response.body?.cancel().catch((error) => {
4563
+ (0, _logtape_logtape.getLogger)([
4564
+ "fedify",
4565
+ "federation",
4566
+ "outbox"
4567
+ ]).debug("Failed to cancel the response body from {url}: {error}", {
4568
+ url,
4569
+ error
4570
+ });
4571
+ });
4572
+ }
4573
+ /**
4574
+ * Reads what is needed from the final response of a delivery while its
4575
+ * session is open: the body of a successful response is cancelled, and at
4576
+ * most 1 KiB of an error response body is read.
4577
+ */
4578
+ async function consumeSendActivityResponse(response, inbox) {
4579
+ if (response.ok) {
4580
+ cancelResponseBody(response, response.url || inbox.href);
4581
+ return { ok: true };
4582
+ }
4583
+ let error;
4584
+ try {
4585
+ error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
4586
+ } catch (_) {
4587
+ error = "";
4588
+ }
4589
+ return {
4590
+ ok: false,
4591
+ status: response.status,
4592
+ statusText: response.statusText,
4593
+ error,
4594
+ responseHeaders: new Headers(response.headers)
4595
+ };
4596
+ }
4597
+ /**
4598
+ * Sends a request without signing it, following redirects manually like the
4599
+ * Fetch standard does. Every hop is opened with `openHop()`, which validates
4600
+ * the destination, after the previous hop is closed. The final response is
4601
+ * passed to `consume()`, and its session is closed after that.
4602
+ * This is exported only for tests; it is not exported from any entry point.
4603
+ * @param request The request to send.
4604
+ * @param openHop The function to open a session for each hop.
4605
+ * @param consume The function to consume the final response.
4606
+ * @returns The value returned by `consume`.
4607
+ */
4608
+ async function fetchWithValidatedRedirects(request, openHop, consume) {
4609
+ const logger = (0, _logtape_logtape.getLogger)([
4610
+ "fedify",
4611
+ "federation",
4612
+ "outbox"
4613
+ ]);
4543
4614
  let body = await request.clone().text();
4544
4615
  for (let redirects = 0;; redirects++) {
4545
- const response = await fetch(request, { redirect: "manual" });
4616
+ const session = await openHop(request.url);
4617
+ let response;
4618
+ try {
4619
+ response = await session.fetch(request, { redirect: "manual" });
4620
+ } catch (error) {
4621
+ await closeAfterError(session, logger);
4622
+ throw error;
4623
+ }
4546
4624
  const location = response.headers.get("Location");
4547
4625
  if (![
4548
4626
  301,
@@ -4550,11 +4628,21 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
4550
4628
  303,
4551
4629
  307,
4552
4630
  308
4553
- ].includes(response.status) || location == null) return response;
4554
- await response.body?.cancel();
4631
+ ].includes(response.status) || location == null) try {
4632
+ return await consume(response);
4633
+ } catch (error) {
4634
+ logger.debug("Failed to consume the response from {url}; closing the session: {error}", {
4635
+ url: request.url,
4636
+ error
4637
+ });
4638
+ throw error;
4639
+ } finally {
4640
+ await session.close();
4641
+ }
4642
+ cancelResponseBody(response, request.url);
4643
+ await session.close();
4555
4644
  if (redirects >= 20) throw new TypeError("Too many redirects");
4556
4645
  const url = new URL(location, request.url);
4557
- await validateUrl(url.href);
4558
4646
  const headers = new Headers(request.headers);
4559
4647
  if (url.origin !== new URL(request.url).origin) {
4560
4648
  headers.delete("Authorization");
@@ -4582,6 +4670,16 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
4582
4670
  });
4583
4671
  }
4584
4672
  }
4673
+ async function closeAfterError(session, logger) {
4674
+ try {
4675
+ await session.close();
4676
+ } catch (error) {
4677
+ logger.debug("Failed to close the session for {url}: {error}", {
4678
+ url: session.url.href,
4679
+ error
4680
+ });
4681
+ }
4682
+ }
4585
4683
  /**
4586
4684
  * An error that is thrown when an activity fails to send to a remote inbox.
4587
4685
  * It contains structured information about the failure, including the HTTP
@@ -10902,7 +11000,7 @@ var FederationImpl = class extends FederationBuilderImpl {
10902
11000
  onNotAcceptable ??= notAcceptable;
10903
11001
  onUnauthorized ??= unauthorized;
10904
11002
  const url = new URL(request.url);
10905
- const route = this.router.route(url.pathname);
11003
+ const route = this.router.route(url.pathname, { allowUnencodedColon: true });
10906
11004
  if (route == null) {
10907
11005
  const media = this.hashlinkMediaDispatcher == null ? null : parseHashlinkGatewayRequest(url);
10908
11006
  if (media != null) return await this.#fetchHashlinkMedia(request, media, {
@@ -12721,52 +12819,92 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
12721
12819
  if (object instanceof _fedify_vocab.Tombstone && !(object instanceof cls) && options?.tombstone !== "passthrough") return null;
12722
12820
  return object;
12723
12821
  }
12724
- #signedKey = void 0;
12822
+ #signedKey = null;
12823
+ #signedKeyOwner = null;
12824
+ #signatureChecks = /* @__PURE__ */ new WeakMap();
12825
+ #getSignatureCheck(documentLoader, contextLoader) {
12826
+ let checks = this.#signatureChecks.get(documentLoader);
12827
+ if (checks == null) {
12828
+ checks = /* @__PURE__ */ new WeakMap();
12829
+ this.#signatureChecks.set(documentLoader, checks);
12830
+ }
12831
+ let check = checks.get(contextLoader);
12832
+ if (check == null) {
12833
+ check = {};
12834
+ checks.set(contextLoader, check);
12835
+ }
12836
+ return check;
12837
+ }
12725
12838
  async getSignedKey(options = {}) {
12726
12839
  if (this.#signedKey != null) return this.#signedKey;
12727
- const keyCache = new KvKeyCache(this.federation.kv, this.federation.kvPrefixes.publicKey, {
12728
- documentLoader: options.documentLoader ?? this.documentLoader,
12729
- contextLoader: options.contextLoader ?? this.contextLoader,
12840
+ const documentLoader = options.documentLoader ?? this.documentLoader;
12841
+ const contextLoader = options.contextLoader ?? this.contextLoader;
12842
+ const check = this.#getSignatureCheck(documentLoader, contextLoader);
12843
+ if (check.key != null) return check.key;
12844
+ const keyCache = documentLoader === this.documentLoader && contextLoader === this.contextLoader ? new KvKeyCache(this.federation.kv, this.federation.kvPrefixes.publicKey, {
12845
+ documentLoader,
12846
+ contextLoader,
12730
12847
  tracerProvider: options.tracerProvider ?? this.tracerProvider,
12731
12848
  keyTtl: this.federation.publicKeyTtl
12732
- });
12733
- return this.#signedKey = await require_http.verifyRequest(this.request, {
12849
+ }) : void 0;
12850
+ const promise = require_http.verifyRequest(this.request, {
12734
12851
  ...this,
12735
- contextLoader: options.contextLoader ?? this.contextLoader,
12736
- documentLoader: options.documentLoader ?? this.documentLoader,
12852
+ contextLoader,
12853
+ documentLoader,
12737
12854
  timeWindow: this.federation.signatureTimeWindow,
12738
12855
  maxSignatures: this.federation.maxHttpSignatures,
12856
+ keyCache,
12739
12857
  meterProvider: this.meterProvider,
12740
- tracerProvider: options.tracerProvider ?? this.tracerProvider,
12741
- keyCache
12858
+ tracerProvider: options.tracerProvider ?? this.tracerProvider
12859
+ }).then((key) => {
12860
+ if (key != null) this.#signedKey ??= key;
12861
+ return key;
12862
+ }).catch((error) => {
12863
+ if (check.key === promise) check.key = void 0;
12864
+ throw error;
12742
12865
  });
12866
+ check.key = promise;
12867
+ return await promise;
12743
12868
  }
12744
- #signedKeyOwner = void 0;
12745
12869
  async getSignedKeyOwner(options = {}) {
12746
12870
  if (this.#signedKeyOwner != null) return this.#signedKeyOwner;
12747
- const key = await this.getSignedKey(options);
12748
- if (key == null) return this.#signedKeyOwner = null;
12749
- try {
12750
- return this.#signedKeyOwner = await require_portable_collection.getKeyOwner(key, {
12751
- contextLoader: options.contextLoader ?? this.contextLoader,
12752
- documentLoader: options.documentLoader ?? this.documentLoader,
12753
- tracerProvider: options.tracerProvider ?? this.tracerProvider
12754
- });
12755
- } catch (error) {
12756
- if (error instanceof _fedify_vocab_runtime.FetchError) {
12757
- (0, _logtape_logtape.getLogger)([
12758
- "fedify",
12759
- "federation",
12760
- "actor"
12761
- ]).warn("Failed to fetch the key owner {keyOwner} of {keyId} while verifying the request signature; treating the request as unauthenticated: {error}", {
12762
- keyId: key.id?.href,
12763
- keyOwner: error.url.href,
12764
- error
12765
- });
12766
- return this.#signedKeyOwner = null;
12871
+ const normalizedOptions = {
12872
+ documentLoader: options.documentLoader ?? this.documentLoader,
12873
+ contextLoader: options.contextLoader ?? this.contextLoader,
12874
+ tracerProvider: options.tracerProvider ?? this.tracerProvider
12875
+ };
12876
+ const check = this.#getSignatureCheck(normalizedOptions.documentLoader, normalizedOptions.contextLoader);
12877
+ if (check.owner != null) return check.owner;
12878
+ const promise = this.getSignedKey(normalizedOptions).then(async (key) => {
12879
+ if (key == null) {
12880
+ if (check.owner === promise) check.owner = void 0;
12881
+ return null;
12767
12882
  }
12883
+ try {
12884
+ const owner = await require_portable_collection.getKeyOwner(key, normalizedOptions);
12885
+ if (owner != null) this.#signedKeyOwner ??= owner;
12886
+ return owner;
12887
+ } catch (error) {
12888
+ if (error instanceof _fedify_vocab_runtime.FetchError) {
12889
+ (0, _logtape_logtape.getLogger)([
12890
+ "fedify",
12891
+ "federation",
12892
+ "actor"
12893
+ ]).warn("Failed to fetch the key owner {keyOwner} of {keyId} while verifying the request signature; treating the request as unauthenticated: {error}", {
12894
+ keyId: key.id?.href,
12895
+ keyOwner: error.url.href,
12896
+ error
12897
+ });
12898
+ return null;
12899
+ }
12900
+ throw error;
12901
+ }
12902
+ }).catch((error) => {
12903
+ if (check.owner === promise) check.owner = void 0;
12768
12904
  throw error;
12769
- }
12905
+ });
12906
+ check.owner = promise;
12907
+ return await promise;
12770
12908
  }
12771
12909
  async isSignedByAudience(object, options = {}) {
12772
12910
  return await isInAudience(object, null) || await isInAudience(object, await this.getSignedKeyOwner(options), options);