@fedify/fedify 2.1.29 → 2.1.31

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 (86) hide show
  1. package/dist/{builder-Mg2G3qj8.mjs → builder-isMRh_I6.mjs} +3 -3
  2. package/dist/{client-CAM_bQXx.d.cts → client-BIBzqEIp.d.cts} +0 -1
  3. package/dist/{client-CjDMsn4a.mjs → client-BZr7TGyu.mjs} +113 -64
  4. package/dist/{client-CSddvgWN.d.ts → client-jE7oGvRh.d.ts} +0 -1
  5. package/dist/compat/mod.d.cts +1 -1
  6. package/dist/compat/mod.d.ts +1 -1
  7. package/dist/compat/transformers.test.mjs +1 -1
  8. package/dist/{context-DLLaDau8.d.ts → context-C0uGVzgk.d.ts} +28 -2
  9. package/dist/{context-BNwv9Q1v.d.cts → context-CLaLxi49.d.cts} +28 -2
  10. package/dist/{deno-BiYv5RT5.mjs → deno-CiaJqTcu.mjs} +1 -1
  11. package/dist/docloader-DOrwedT7.mjs +124 -0
  12. package/dist/federation/builder.test.mjs +1 -1
  13. package/dist/federation/handler.test.mjs +6 -6
  14. package/dist/federation/idempotency.test.mjs +2 -2
  15. package/dist/federation/inbox.test.mjs +1 -1
  16. package/dist/federation/keycache.test.mjs +18 -7
  17. package/dist/federation/middleware.test.mjs +380 -24
  18. package/dist/federation/mod.cjs +1 -1
  19. package/dist/federation/mod.d.cts +2 -2
  20. package/dist/federation/mod.d.ts +2 -2
  21. package/dist/federation/mod.js +1 -1
  22. package/dist/federation/send.test.mjs +445 -70
  23. package/dist/federation/temporal.test.mjs +1 -1
  24. package/dist/federation/webfinger.test.mjs +1 -1
  25. package/dist/{http-Dl00HXr4.d.ts → http-9IFUxdkp.d.cts} +1 -2
  26. package/dist/{http-C1ga9WQu.mjs → http-A9mP2uM7.mjs} +194 -96
  27. package/dist/{http-Cginz7L0.cjs → http-CRiVlZd5.cjs} +188 -105
  28. package/dist/{http-B3rSb2GQ.js → http-Crml3gei.js} +187 -104
  29. package/dist/{http-DqZhyEso.d.cts → http-SbfezERn.d.ts} +1 -2
  30. package/dist/{inbox-BWzL0xMU.mjs → inbox-KESLudq4.mjs} +1 -1
  31. package/dist/{key-O-ATwIGS.mjs → key-Dg6-DV_O.mjs} +17 -3
  32. package/dist/{keycache-RFlZObU3.mjs → keycache-DahCaG_m.mjs} +1 -1
  33. package/dist/{kv-cache-BdylAE9w.cjs → kv-cache-Cqmm-yQa.cjs} +59 -15
  34. package/dist/{kv-cache-8Kg2CHiD.js → kv-cache-DQcyZL5T.js} +60 -16
  35. package/dist/{ld-C-fCGM-z.mjs → ld-COjDN6Nb.mjs} +2 -2
  36. package/dist/{middleware-CVRFS7PZ.cjs → middleware-BbgR7xOI.cjs} +185 -60
  37. package/dist/{middleware-VvrqPwHL.cjs → middleware-BtDQUCcz.cjs} +1 -1
  38. package/dist/{middleware-C9UCWOqw.mjs → middleware-COFlKeYn.mjs} +79 -39
  39. package/dist/{middleware-CxgFiKI3.mjs → middleware-CuQvczZd.mjs} +1 -1
  40. package/dist/{middleware-41MkquTV.js → middleware-DIW5S_5L.js} +185 -60
  41. package/dist/{mod-DNEgTI8M.d.ts → mod-BRnwh2oy.d.ts} +2 -7
  42. package/dist/{mod-BLDHhuW0.d.ts → mod-C7w-QVUv.d.ts} +1 -1
  43. package/dist/{mod-CWJNbyju.d.ts → mod-CFRocFeQ.d.ts} +1 -1
  44. package/dist/{mod-BmlfRBmU.d.cts → mod-CGUH8Ujp.d.cts} +1 -1
  45. package/dist/{mod-Cn9GJ5ex.d.cts → mod-Co7LEK9X.d.cts} +2 -7
  46. package/dist/{mod-DiuFTYUt.d.cts → mod-LOEOKbYd.d.cts} +1 -1
  47. package/dist/mod.cjs +5 -5
  48. package/dist/mod.d.cts +6 -6
  49. package/dist/mod.d.ts +6 -6
  50. package/dist/mod.js +5 -5
  51. package/dist/nodeinfo/client.test.mjs +337 -19
  52. package/dist/nodeinfo/handler.test.mjs +1 -1
  53. package/dist/nodeinfo/mod.cjs +1 -1
  54. package/dist/nodeinfo/mod.d.cts +1 -1
  55. package/dist/nodeinfo/mod.d.ts +1 -1
  56. package/dist/nodeinfo/mod.js +1 -1
  57. package/dist/otel/exporter.test.mjs +292 -30
  58. package/dist/otel/mod.cjs +36 -30
  59. package/dist/otel/mod.d.cts +13 -0
  60. package/dist/otel/mod.d.ts +13 -0
  61. package/dist/otel/mod.js +36 -30
  62. package/dist/{owner-C8IVASD3.mjs → owner-CFHq61cq.mjs} +2 -2
  63. package/dist/{proof-BbHMAkPG.cjs → proof-0JkSC_pT.cjs} +1 -1
  64. package/dist/{proof-cGfRPTiV.js → proof-DzksJFK7.js} +1 -1
  65. package/dist/{proof-CUdcBgvg.mjs → proof-KSHlbPCG.mjs} +2 -2
  66. package/dist/{send-BoV4dyVO.mjs → send-DaDe2YCb.mjs} +117 -32
  67. package/dist/sig/http.test.mjs +507 -3
  68. package/dist/sig/key.test.mjs +197 -3
  69. package/dist/sig/ld.test.mjs +47 -2
  70. package/dist/sig/mod.cjs +2 -2
  71. package/dist/sig/mod.d.cts +2 -2
  72. package/dist/sig/mod.d.ts +2 -2
  73. package/dist/sig/mod.js +2 -2
  74. package/dist/sig/owner.test.mjs +46 -1
  75. package/dist/sig/proof.test.mjs +52 -1
  76. package/dist/{temporal-BJlnku_d.mjs → temporal-DNGH8o2E.mjs} +1 -1
  77. package/dist/testing/mod.d.mts +26 -1
  78. package/dist/{types-miAYfiGO.js → types-B1_pRFE_.js} +112 -63
  79. package/dist/{types-DRPjmHVV.cjs → types-DOvyymM4.cjs} +111 -62
  80. package/dist/utils/docloader.test.mjs +380 -32
  81. package/dist/utils/mod.cjs +1 -1
  82. package/dist/utils/mod.d.cts +1 -1
  83. package/dist/utils/mod.d.ts +1 -1
  84. package/dist/utils/mod.js +1 -1
  85. package/package.json +7 -7
  86. package/dist/docloader-DrxXBFL8.mjs +0 -80
@@ -1,9 +1,10 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { f as validateCryptoKey, t as doubleKnock } from "./http-B3rSb2GQ.js";
3
+ import { f as validateCryptoKey, t as doubleKnockScoped } from "./http-Crml3gei.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  import { curry } from "es-toolkit";
6
- import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, validatePublicUrl } from "@fedify/vocab-runtime";
6
+ import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts } from "@fedify/vocab-runtime";
7
+ import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
7
8
  //#region src/utils/docloader.ts
8
9
  const logger$1 = getLogger([
9
10
  "fedify",
@@ -24,12 +25,34 @@ const DEFAULT_MAX_REDIRECTION = 20;
24
25
  * @throws {TypeError} If the key is invalid or unsupported.
25
26
  * @since 0.4.0
26
27
  */
27
- function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAgent, specDeterminer, tracerProvider } = {}) {
28
+ function getAuthenticatedDocumentLoader(identity, options = {}) {
29
+ return createAuthenticatedDocumentLoader(identity, options);
30
+ }
31
+ /**
32
+ * The placeholder that the alternate document callback of
33
+ * `getRemoteDocument()` returns, so that the alternate document is loaded
34
+ * after the current hop is closed.
35
+ */
36
+ const ALTERNATE_PLACEHOLDER = Object.freeze({
37
+ contextUrl: null,
38
+ document: null,
39
+ documentUrl: "about:blank"
40
+ });
41
+ /**
42
+ * The implementation of {@link getAuthenticatedDocumentLoader}. This is not
43
+ * exported from any entry point.
44
+ * @param identity The identity to get the document loader for.
45
+ * @param options The options for the document loader.
46
+ * @param deps The dependencies, for tests.
47
+ * @returns The authenticated document loader.
48
+ * @throws {TypeError} If the key is invalid or unsupported.
49
+ */
50
+ function createAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAgent, specDeterminer, tracerProvider } = {}, deps = {}) {
28
51
  validateCryptoKey(identity.privateKey);
29
52
  async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
30
53
  options?.signal?.throwIfAborted();
31
54
  let currentUrl = new URL(url).href;
32
- await validateUrl(currentUrl);
55
+ validateUrl(currentUrl);
33
56
  visited.add(currentUrl);
34
57
  const originalRequest = createActivityPubRequest(currentUrl, { userAgent });
35
58
  function follow(nextUrl) {
@@ -38,28 +61,43 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAge
38
61
  if (visited.has(nextUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${nextUrl}`);
39
62
  redirected++;
40
63
  }
41
- const response = await doubleKnock(originalRequest, identity, {
64
+ const result = await doubleKnockScoped(originalRequest, identity, {
42
65
  specDeterminer,
43
66
  log: curry(logRequest)(logger$1),
44
67
  tracerProvider,
45
68
  signal: options?.signal,
46
- validateRedirect: async (nextUrl) => {
69
+ validateRedirect: (nextUrl) => {
47
70
  follow(nextUrl);
48
- await validateUrl(nextUrl);
71
+ validateUrl(nextUrl);
49
72
  visited.add(nextUrl);
50
73
  currentUrl = nextUrl;
51
74
  }
75
+ }, (hopUrl) => openHop(hopUrl, options?.signal), async (response) => {
76
+ let alternateUrl;
77
+ const document = await getRemoteDocument(currentUrl, response, (url) => {
78
+ alternateUrl = url;
79
+ return Promise.resolve(ALTERNATE_PLACEHOLDER);
80
+ });
81
+ if (alternateUrl == null) return { document };
82
+ if (!response.bodyUsed && response.body?.locked !== true) response.body?.cancel().catch((error) => {
83
+ logger$1.debug("Failed to cancel the response body from {url}: {error}", {
84
+ url: currentUrl,
85
+ error
86
+ });
87
+ });
88
+ return { alternateUrl };
52
89
  });
53
- return getRemoteDocument(currentUrl, response, (alternateUrl) => {
54
- follow(alternateUrl);
55
- return load(alternateUrl, options, redirected, visited);
56
- });
90
+ if ("document" in result) return result.document;
91
+ follow(result.alternateUrl);
92
+ return load(result.alternateUrl, options, redirected, visited);
57
93
  }
58
- async function validateUrl(url) {
59
- const protocol = new URL(url).protocol;
60
- if (protocol !== "http:" && protocol !== "https:") throw new UrlError(`Unsupported protocol: ${protocol}`);
61
- if (!allowPrivateAddress) try {
62
- await validatePublicUrl(url);
94
+ function openUnvalidatedHop(url, signal) {
95
+ if (deps.openHop != null) return deps.openHop(url);
96
+ return allowPrivateAddress ? openDirectFetch(url) : openPublicFetch(url, { signal });
97
+ }
98
+ async function openHop(url, signal) {
99
+ try {
100
+ return await openUnvalidatedHop(url, signal);
63
101
  } catch (error) {
64
102
  if (error instanceof UrlError) if (error.reason === "dns") logger$1.debug("DNS lookup failed for {url}", {
65
103
  url,
@@ -72,6 +110,12 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAge
72
110
  throw error;
73
111
  }
74
112
  }
113
+ function validateUrl(url) {
114
+ const parsedUrl = new URL(url);
115
+ const protocol = parsedUrl.protocol;
116
+ if (protocol !== "http:" && protocol !== "https:") throw new UrlError(`Unsupported protocol: ${protocol}`);
117
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new UrlError("URLs with credentials are not allowed");
118
+ }
75
119
  return (url, options) => load(url, options);
76
120
  }
77
121
  //#endregion
@@ -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-BiYv5RT5.mjs";
5
- import { r as fetchKey, s as validateCryptoKey } from "./key-O-ATwIGS.mjs";
4
+ import { n as version, t as name } from "./deno-CiaJqTcu.mjs";
5
+ import { r as fetchKey, s as validateCryptoKey } from "./key-Dg6-DV_O.mjs";
6
6
  import { Activity, CryptographicKey, Object as Object$1, getTypeId } from "@fedify/vocab";
7
7
  import { SpanStatusCode, trace } from "@opentelemetry/api";
8
8
  import { getDocumentLoader } from "@fedify/vocab-runtime";
@@ -2,10 +2,10 @@ const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  const require_chunk = require("./chunk-DDcVe30Y.cjs");
4
4
  const require_transformers = require("./transformers-NeAONrAq.cjs");
5
- const require_http = require("./http-Cginz7L0.cjs");
6
- const require_types = require("./types-DRPjmHVV.cjs");
7
- const require_proof = require("./proof-BbHMAkPG.cjs");
8
- const require_kv_cache = require("./kv-cache-BdylAE9w.cjs");
5
+ const require_http = require("./http-CRiVlZd5.cjs");
6
+ const require_types = require("./types-DOvyymM4.cjs");
7
+ const require_proof = require("./proof-0JkSC_pT.cjs");
8
+ const require_kv_cache = require("./kv-cache-Cqmm-yQa.cjs");
9
9
  let _logtape_logtape = require("@logtape/logtape");
10
10
  let _fedify_vocab = require("@fedify/vocab");
11
11
  let _opentelemetry_api = require("@opentelemetry/api");
@@ -18,6 +18,7 @@ let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conve
18
18
  let _fedify_vocab_runtime_jsonld = require("@fedify/vocab-runtime/jsonld");
19
19
  _fedify_vocab_runtime_jsonld = require_chunk.__toESM(_fedify_vocab_runtime_jsonld);
20
20
  let _fedify_webfinger = require("@fedify/webfinger");
21
+ let _fedify_vocab_runtime_internal = require("@fedify/vocab-runtime/internal");
21
22
  let node_url = require("node:url");
22
23
  //#region src/federation/inbox.ts
23
24
  var InboxListenerSet = class InboxListenerSet {
@@ -350,7 +351,7 @@ var FederationBuilderImpl = class {
350
351
  this.collectionTypeIds = {};
351
352
  }
352
353
  async build(options) {
353
- const { FederationImpl } = await Promise.resolve().then(() => require("./middleware-VvrqPwHL.cjs"));
354
+ const { FederationImpl } = await Promise.resolve().then(() => require("./middleware-BtDQUCcz.cjs"));
354
355
  const f = new FederationImpl(options);
355
356
  const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive;
356
357
  f.router = this.router.clone();
@@ -883,7 +884,7 @@ async function buildCollectionSynchronizationHeader(collectionId, actorIds) {
883
884
  }
884
885
  //#endregion
885
886
  //#region src/federation/keycache.ts
886
- const KEY_CACHE_GENERATION = "2";
887
+ const KEY_CACHE_GENERATION = "3";
887
888
  var KvKeyCache = class {
888
889
  kv;
889
890
  prefix;
@@ -2478,9 +2479,10 @@ function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
2478
2479
  *
2479
2480
  * @param parameters The parameters for sending the activity.
2480
2481
  * See also {@link SendActivityParameters}.
2482
+ * @param deps The dependencies, for tests.
2481
2483
  * @throws {Error} If the activity fails to send.
2482
2484
  */
2483
- function sendActivity(options) {
2485
+ function sendActivity(options, deps = {}) {
2484
2486
  const tracerProvider = options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider();
2485
2487
  return tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan("activitypub.send_activity", {
2486
2488
  kind: _opentelemetry_api.SpanKind.CLIENT,
@@ -2492,7 +2494,7 @@ function sendActivity(options) {
2492
2494
  await sendActivityInternal({
2493
2495
  ...options,
2494
2496
  tracerProvider
2495
- }, span);
2497
+ }, span, deps);
2496
2498
  } catch (e) {
2497
2499
  span.setStatus({
2498
2500
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -2526,21 +2528,25 @@ async function readLimitedResponseBody(response, maxBytes) {
2526
2528
  totalBytes += value.length;
2527
2529
  }
2528
2530
  } finally {
2531
+ if (truncated) reader.cancel().catch(() => {});
2529
2532
  reader.releaseLock();
2530
2533
  }
2531
2534
  let result = chunks.join("");
2532
2535
  if (truncated) result += "… (truncated)";
2533
2536
  return result;
2534
2537
  }
2535
- async function sendActivityInternal({ activity, activityId, keys, inbox, allowPrivateAddress, headers, specDeterminer, tracerProvider }, span) {
2538
+ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPrivateAddress, headers, specDeterminer, tracerProvider }, span, deps) {
2536
2539
  const logger = (0, _logtape_logtape.getLogger)([
2537
2540
  "fedify",
2538
2541
  "federation",
2539
2542
  "outbox"
2540
2543
  ]);
2541
- async function validateUrl(url) {
2542
- if (!allowPrivateAddress) try {
2543
- await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
2544
+ const openUnvalidatedHop = deps.openHop ?? (allowPrivateAddress ? _fedify_vocab_runtime_internal.openDirectFetch : (url) => (0, _fedify_vocab_runtime_internal.openPublicFetch)(url));
2545
+ let opened = false;
2546
+ async function openHop(url) {
2547
+ let session;
2548
+ try {
2549
+ session = await openUnvalidatedHop(url);
2544
2550
  } catch (error) {
2545
2551
  if (error instanceof _fedify_vocab_runtime.UrlError && error.reason === "dns") {
2546
2552
  logger.error("DNS resolution failed for URL: {url}", {
@@ -2557,8 +2563,9 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPr
2557
2563
  });
2558
2564
  throw error;
2559
2565
  }
2566
+ opened = true;
2567
+ return session;
2560
2568
  }
2561
- await validateUrl(inbox.href);
2562
2569
  headers = new Headers(headers);
2563
2570
  headers.set("Content-Type", "application/activity+json");
2564
2571
  const request = new Request(inbox, {
@@ -2579,36 +2586,30 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPr
2579
2586
  privateKey: pair.privateKey
2580
2587
  }))
2581
2588
  });
2582
- let response;
2589
+ let outcome;
2583
2590
  try {
2584
- response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await require_http.doubleKnock(request, rsaKey, {
2591
+ outcome = rsaKey == null ? await fetchWithValidatedRedirects(request, openHop, (response) => consumeSendActivityResponse(response, inbox)) : await require_http.doubleKnockScoped(request, rsaKey, {
2585
2592
  tracerProvider,
2586
- specDeterminer,
2587
- validateRedirect: validateUrl
2588
- });
2593
+ specDeterminer
2594
+ }, openHop, (response) => consumeSendActivityResponse(response, inbox));
2589
2595
  } catch (error) {
2590
- logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
2596
+ if (opened) logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
2591
2597
  activityId,
2592
2598
  inbox: inbox.href,
2593
2599
  error
2594
2600
  });
2595
2601
  throw error;
2596
2602
  }
2597
- if (!response.ok) {
2598
- let error;
2599
- try {
2600
- error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
2601
- } catch (_) {
2602
- error = "";
2603
- }
2603
+ if (!outcome.ok) {
2604
+ const { status, statusText, error } = outcome;
2604
2605
  logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
2605
2606
  activityId,
2606
2607
  inbox: inbox.href,
2607
- status: response.status,
2608
- statusText: response.statusText,
2608
+ status,
2609
+ statusText,
2609
2610
  error
2610
2611
  });
2611
- throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error);
2612
+ throw new SendActivityError(inbox, status, `Failed to send activity ${activityId} to ${inbox.href} (${status} ${statusText}):\n${error}`, error);
2612
2613
  }
2613
2614
  span.addEvent("activitypub.activity.sent", {
2614
2615
  "activitypub.activity.json": JSON.stringify(activity),
@@ -2616,10 +2617,74 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPr
2616
2617
  "activitypub.activity.id": activityId ?? ""
2617
2618
  });
2618
2619
  }
2619
- async function fetchWithValidatedRedirects(request, validateUrl) {
2620
+ /**
2621
+ * Starts cancelling the body of a response without waiting for it to settle.
2622
+ * The cancellation of a branch of a tee (e.g., a body cloned by a fetch mock)
2623
+ * settles only after the other branch is cancelled too, per the Streams
2624
+ * standard, so awaiting it could block forever. Failures are only logged.
2625
+ */
2626
+ function cancelResponseBody(response, url) {
2627
+ response.body?.cancel().catch((error) => {
2628
+ (0, _logtape_logtape.getLogger)([
2629
+ "fedify",
2630
+ "federation",
2631
+ "outbox"
2632
+ ]).debug("Failed to cancel the response body from {url}: {error}", {
2633
+ url,
2634
+ error
2635
+ });
2636
+ });
2637
+ }
2638
+ /**
2639
+ * Reads what is needed from the final response of a delivery while its
2640
+ * session is open: the body of a successful response is cancelled, and at
2641
+ * most 1 KiB of an error response body is read.
2642
+ */
2643
+ async function consumeSendActivityResponse(response, inbox) {
2644
+ if (response.ok) {
2645
+ cancelResponseBody(response, response.url || inbox.href);
2646
+ return { ok: true };
2647
+ }
2648
+ let error;
2649
+ try {
2650
+ error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
2651
+ } catch (_) {
2652
+ error = "";
2653
+ }
2654
+ return {
2655
+ ok: false,
2656
+ status: response.status,
2657
+ statusText: response.statusText,
2658
+ error
2659
+ };
2660
+ }
2661
+ /**
2662
+ * Sends a request without signing it, following redirects manually like the
2663
+ * Fetch standard does. Every hop is opened with `openHop()`, which validates
2664
+ * the destination, after the previous hop is closed. The final response is
2665
+ * passed to `consume()`, and its session is closed after that.
2666
+ * This is exported only for tests; it is not exported from any entry point.
2667
+ * @param request The request to send.
2668
+ * @param openHop The function to open a session for each hop.
2669
+ * @param consume The function to consume the final response.
2670
+ * @returns The value returned by `consume`.
2671
+ */
2672
+ async function fetchWithValidatedRedirects(request, openHop, consume) {
2673
+ const logger = (0, _logtape_logtape.getLogger)([
2674
+ "fedify",
2675
+ "federation",
2676
+ "outbox"
2677
+ ]);
2620
2678
  let body = await request.clone().text();
2621
2679
  for (let redirects = 0;; redirects++) {
2622
- const response = await fetch(request, { redirect: "manual" });
2680
+ const session = await openHop(request.url);
2681
+ let response;
2682
+ try {
2683
+ response = await session.fetch(request, { redirect: "manual" });
2684
+ } catch (error) {
2685
+ await closeAfterError(session, logger);
2686
+ throw error;
2687
+ }
2623
2688
  const location = response.headers.get("Location");
2624
2689
  if (![
2625
2690
  301,
@@ -2627,11 +2692,21 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
2627
2692
  303,
2628
2693
  307,
2629
2694
  308
2630
- ].includes(response.status) || location == null) return response;
2631
- await response.body?.cancel();
2695
+ ].includes(response.status) || location == null) try {
2696
+ return await consume(response);
2697
+ } catch (error) {
2698
+ logger.debug("Failed to consume the response from {url}; closing the session: {error}", {
2699
+ url: request.url,
2700
+ error
2701
+ });
2702
+ throw error;
2703
+ } finally {
2704
+ await session.close();
2705
+ }
2706
+ cancelResponseBody(response, request.url);
2707
+ await session.close();
2632
2708
  if (redirects >= 20) throw new TypeError("Too many redirects");
2633
2709
  const url = new URL(location, request.url);
2634
- await validateUrl(url.href);
2635
2710
  const headers = new Headers(request.headers);
2636
2711
  if (url.origin !== new URL(request.url).origin) {
2637
2712
  headers.delete("Authorization");
@@ -2659,6 +2734,16 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
2659
2734
  });
2660
2735
  }
2661
2736
  }
2737
+ async function closeAfterError(session, logger) {
2738
+ try {
2739
+ await session.close();
2740
+ } catch (error) {
2741
+ logger.debug("Failed to close the session for {url}: {error}", {
2742
+ url: session.url.href,
2743
+ error
2744
+ });
2745
+ }
2746
+ }
2662
2747
  /**
2663
2748
  * An error that is thrown when an activity fails to send to a remote inbox.
2664
2749
  * It contains structured information about the failure, including the HTTP
@@ -4591,43 +4676,83 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
4591
4676
  }
4592
4677
  }), values);
4593
4678
  }
4594
- #signedKey = void 0;
4679
+ #signedKey = null;
4680
+ #signedKeyOwner = null;
4681
+ #signatureChecks = /* @__PURE__ */ new WeakMap();
4682
+ #getSignatureCheck(documentLoader, contextLoader) {
4683
+ let checks = this.#signatureChecks.get(documentLoader);
4684
+ if (checks == null) {
4685
+ checks = /* @__PURE__ */ new WeakMap();
4686
+ this.#signatureChecks.set(documentLoader, checks);
4687
+ }
4688
+ let check = checks.get(contextLoader);
4689
+ if (check == null) {
4690
+ check = {};
4691
+ checks.set(contextLoader, check);
4692
+ }
4693
+ return check;
4694
+ }
4595
4695
  async getSignedKey(options = {}) {
4596
4696
  if (this.#signedKey != null) return this.#signedKey;
4597
- return this.#signedKey = await require_http.verifyRequest(this.request, {
4697
+ const documentLoader = options.documentLoader ?? this.documentLoader;
4698
+ const contextLoader = options.contextLoader ?? this.contextLoader;
4699
+ const check = this.#getSignatureCheck(documentLoader, contextLoader);
4700
+ if (check.key != null) return check.key;
4701
+ const promise = require_http.verifyRequest(this.request, {
4598
4702
  ...this,
4599
- contextLoader: options.contextLoader ?? this.contextLoader,
4600
- documentLoader: options.documentLoader ?? this.documentLoader,
4703
+ contextLoader,
4704
+ documentLoader,
4601
4705
  timeWindow: this.federation.signatureTimeWindow,
4602
4706
  tracerProvider: options.tracerProvider ?? this.tracerProvider
4707
+ }).then((key) => {
4708
+ if (key != null) this.#signedKey ??= key;
4709
+ return key;
4710
+ }).catch((error) => {
4711
+ if (check.key === promise) check.key = void 0;
4712
+ throw error;
4603
4713
  });
4714
+ check.key = promise;
4715
+ return await promise;
4604
4716
  }
4605
- #signedKeyOwner = void 0;
4606
4717
  async getSignedKeyOwner(options = {}) {
4607
4718
  if (this.#signedKeyOwner != null) return this.#signedKeyOwner;
4608
- const key = await this.getSignedKey(options);
4609
- if (key == null) return this.#signedKeyOwner = null;
4610
- try {
4611
- return this.#signedKeyOwner = await require_proof.getKeyOwner(key, {
4612
- contextLoader: options.contextLoader ?? this.contextLoader,
4613
- documentLoader: options.documentLoader ?? this.documentLoader,
4614
- tracerProvider: options.tracerProvider ?? this.tracerProvider
4615
- });
4616
- } catch (error) {
4617
- if (error instanceof _fedify_vocab_runtime.FetchError) {
4618
- (0, _logtape_logtape.getLogger)([
4619
- "fedify",
4620
- "federation",
4621
- "actor"
4622
- ]).warn("Failed to fetch the key owner {keyOwner} of {keyId} while verifying the request signature; treating the request as unauthenticated: {error}", {
4623
- keyId: key.id?.href,
4624
- keyOwner: error.url.href,
4625
- error
4626
- });
4627
- return this.#signedKeyOwner = null;
4719
+ const normalizedOptions = {
4720
+ documentLoader: options.documentLoader ?? this.documentLoader,
4721
+ contextLoader: options.contextLoader ?? this.contextLoader,
4722
+ tracerProvider: options.tracerProvider ?? this.tracerProvider
4723
+ };
4724
+ const check = this.#getSignatureCheck(normalizedOptions.documentLoader, normalizedOptions.contextLoader);
4725
+ if (check.owner != null) return check.owner;
4726
+ const promise = this.getSignedKey(normalizedOptions).then(async (key) => {
4727
+ if (key == null) {
4728
+ if (check.owner === promise) check.owner = void 0;
4729
+ return null;
4628
4730
  }
4731
+ try {
4732
+ const owner = await require_proof.getKeyOwner(key, normalizedOptions);
4733
+ if (owner != null) this.#signedKeyOwner ??= owner;
4734
+ return owner;
4735
+ } catch (error) {
4736
+ if (error instanceof _fedify_vocab_runtime.FetchError) {
4737
+ (0, _logtape_logtape.getLogger)([
4738
+ "fedify",
4739
+ "federation",
4740
+ "actor"
4741
+ ]).warn("Failed to fetch the key owner {keyOwner} of {keyId} while verifying the request signature; treating the request as unauthenticated: {error}", {
4742
+ keyId: key.id?.href,
4743
+ keyOwner: error.url.href,
4744
+ error
4745
+ });
4746
+ return null;
4747
+ }
4748
+ throw error;
4749
+ }
4750
+ }).catch((error) => {
4751
+ if (check.owner === promise) check.owner = void 0;
4629
4752
  throw error;
4630
- }
4753
+ });
4754
+ check.owner = promise;
4755
+ return await promise;
4631
4756
  }
4632
4757
  };
4633
4758
  var InboxContextImpl = class InboxContextImpl extends ContextImpl {
@@ -1,4 +1,4 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
- const require_middleware = require("./middleware-CVRFS7PZ.cjs");
3
+ const require_middleware = require("./middleware-BbgR7xOI.cjs");
4
4
  exports.FederationImpl = require_middleware.FederationImpl;
@@ -2,25 +2,25 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { n as RouterError } from "./router-CrMLXoOr.mjs";
5
- import { n as version, t as name } from "./deno-BiYv5RT5.mjs";
5
+ import { n as version, t as name } from "./deno-CiaJqTcu.mjs";
6
6
  import { t as formatAcceptSignature } from "./accept-BavzedPB.mjs";
7
- import { o as importJwk, s as validateCryptoKey, t as exportJwk } from "./key-O-ATwIGS.mjs";
8
- import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-C1ga9WQu.mjs";
9
- import { t as getAuthenticatedDocumentLoader } from "./docloader-DrxXBFL8.mjs";
7
+ import { o as importJwk, s as validateCryptoKey, t as exportJwk } from "./key-Dg6-DV_O.mjs";
8
+ import { d as verifyRequestDetailed, s as parseRfc9421SignatureInput, u as verifyRequest } from "./http-A9mP2uM7.mjs";
9
+ import { n as getAuthenticatedDocumentLoader } from "./docloader-DOrwedT7.mjs";
10
10
  import { n as kvCache } from "./kv-cache-DqEKe5E0.mjs";
11
- import { t as KvKeyCache } from "./keycache-RFlZObU3.mjs";
12
- import { a as compactJsonLd, c as getNormalizationContextLoader, d as isInvalidUrlTypeError, f as signJsonLd, g as wrapContextLoaderForJsonLd, l as hasSignature, p as verifyCompactJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as isClearlyMalformedContextReference } from "./ld-C-fCGM-z.mjs";
13
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-C8IVASD3.mjs";
14
- import { n as signObject, r as verifyObject } from "./proof-CUdcBgvg.mjs";
15
- import { d as readBoundedText, l as BodyTooLargeError, t as getNodeInfo, u as MAX_BODY_SIZE } from "./client-CjDMsn4a.mjs";
11
+ import { t as KvKeyCache } from "./keycache-DahCaG_m.mjs";
12
+ import { a as compactJsonLd, c as getNormalizationContextLoader, d as isInvalidUrlTypeError, f as signJsonLd, g as wrapContextLoaderForJsonLd, l as hasSignature, p as verifyCompactJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as isClearlyMalformedContextReference } from "./ld-COjDN6Nb.mjs";
13
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-CFHq61cq.mjs";
14
+ import { n as signObject, r as verifyObject } from "./proof-KSHlbPCG.mjs";
15
+ import { d as MAX_BODY_SIZE, f as readBoundedText, t as getNodeInfo, u as BodyTooLargeError } from "./client-BZr7TGyu.mjs";
16
16
  import { t as nodeInfoToJson } from "./types-DCP0WLdt.mjs";
17
- import { n as actorsHaveSameOrigin, r as routeActivity } from "./inbox-BWzL0xMU.mjs";
18
- import { t as FederationBuilderImpl } from "./builder-Mg2G3qj8.mjs";
17
+ import { n as actorsHaveSameOrigin, r as routeActivity } from "./inbox-KESLudq4.mjs";
18
+ import { t as FederationBuilderImpl } from "./builder-isMRh_I6.mjs";
19
19
  import { t as buildCollectionSynchronizationHeader } from "./collection-BD6-SZ6O.mjs";
20
20
  import { t as acceptsJsonLd } from "./negotiation-BehA2uul.mjs";
21
- import { t as hasMalformedKnownTemporalLiteral } from "./temporal-BJlnku_d.mjs";
21
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-DNGH8o2E.mjs";
22
22
  import { t as createExponentialBackoffPolicy } from "./retry-B_E3V_Dx.mjs";
23
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-BoV4dyVO.mjs";
23
+ import { i as sendActivity, n as extractInboxes, t as SendActivityError } from "./send-DaDe2YCb.mjs";
24
24
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
25
25
  import { lookupWebFinger } from "@fedify/webfinger";
26
26
  import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
@@ -3275,43 +3275,83 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
3275
3275
  }
3276
3276
  }), values);
3277
3277
  }
3278
- #signedKey = void 0;
3278
+ #signedKey = null;
3279
+ #signedKeyOwner = null;
3280
+ #signatureChecks = /* @__PURE__ */ new WeakMap();
3281
+ #getSignatureCheck(documentLoader, contextLoader) {
3282
+ let checks = this.#signatureChecks.get(documentLoader);
3283
+ if (checks == null) {
3284
+ checks = /* @__PURE__ */ new WeakMap();
3285
+ this.#signatureChecks.set(documentLoader, checks);
3286
+ }
3287
+ let check = checks.get(contextLoader);
3288
+ if (check == null) {
3289
+ check = {};
3290
+ checks.set(contextLoader, check);
3291
+ }
3292
+ return check;
3293
+ }
3279
3294
  async getSignedKey(options = {}) {
3280
3295
  if (this.#signedKey != null) return this.#signedKey;
3281
- return this.#signedKey = await verifyRequest(this.request, {
3296
+ const documentLoader = options.documentLoader ?? this.documentLoader;
3297
+ const contextLoader = options.contextLoader ?? this.contextLoader;
3298
+ const check = this.#getSignatureCheck(documentLoader, contextLoader);
3299
+ if (check.key != null) return check.key;
3300
+ const promise = verifyRequest(this.request, {
3282
3301
  ...this,
3283
- contextLoader: options.contextLoader ?? this.contextLoader,
3284
- documentLoader: options.documentLoader ?? this.documentLoader,
3302
+ contextLoader,
3303
+ documentLoader,
3285
3304
  timeWindow: this.federation.signatureTimeWindow,
3286
3305
  tracerProvider: options.tracerProvider ?? this.tracerProvider
3306
+ }).then((key) => {
3307
+ if (key != null) this.#signedKey ??= key;
3308
+ return key;
3309
+ }).catch((error) => {
3310
+ if (check.key === promise) check.key = void 0;
3311
+ throw error;
3287
3312
  });
3313
+ check.key = promise;
3314
+ return await promise;
3288
3315
  }
3289
- #signedKeyOwner = void 0;
3290
3316
  async getSignedKeyOwner(options = {}) {
3291
3317
  if (this.#signedKeyOwner != null) return this.#signedKeyOwner;
3292
- const key = await this.getSignedKey(options);
3293
- if (key == null) return this.#signedKeyOwner = null;
3294
- try {
3295
- return this.#signedKeyOwner = await getKeyOwner(key, {
3296
- contextLoader: options.contextLoader ?? this.contextLoader,
3297
- documentLoader: options.documentLoader ?? this.documentLoader,
3298
- tracerProvider: options.tracerProvider ?? this.tracerProvider
3299
- });
3300
- } catch (error) {
3301
- if (error instanceof FetchError) {
3302
- getLogger([
3303
- "fedify",
3304
- "federation",
3305
- "actor"
3306
- ]).warn("Failed to fetch the key owner {keyOwner} of {keyId} while verifying the request signature; treating the request as unauthenticated: {error}", {
3307
- keyId: key.id?.href,
3308
- keyOwner: error.url.href,
3309
- error
3310
- });
3311
- return this.#signedKeyOwner = null;
3318
+ const normalizedOptions = {
3319
+ documentLoader: options.documentLoader ?? this.documentLoader,
3320
+ contextLoader: options.contextLoader ?? this.contextLoader,
3321
+ tracerProvider: options.tracerProvider ?? this.tracerProvider
3322
+ };
3323
+ const check = this.#getSignatureCheck(normalizedOptions.documentLoader, normalizedOptions.contextLoader);
3324
+ if (check.owner != null) return check.owner;
3325
+ const promise = this.getSignedKey(normalizedOptions).then(async (key) => {
3326
+ if (key == null) {
3327
+ if (check.owner === promise) check.owner = void 0;
3328
+ return null;
3312
3329
  }
3330
+ try {
3331
+ const owner = await getKeyOwner(key, normalizedOptions);
3332
+ if (owner != null) this.#signedKeyOwner ??= owner;
3333
+ return owner;
3334
+ } catch (error) {
3335
+ if (error instanceof FetchError) {
3336
+ getLogger([
3337
+ "fedify",
3338
+ "federation",
3339
+ "actor"
3340
+ ]).warn("Failed to fetch the key owner {keyOwner} of {keyId} while verifying the request signature; treating the request as unauthenticated: {error}", {
3341
+ keyId: key.id?.href,
3342
+ keyOwner: error.url.href,
3343
+ error
3344
+ });
3345
+ return null;
3346
+ }
3347
+ throw error;
3348
+ }
3349
+ }).catch((error) => {
3350
+ if (check.owner === promise) check.owner = void 0;
3313
3351
  throw error;
3314
- }
3352
+ });
3353
+ check.owner = promise;
3354
+ return await promise;
3315
3355
  }
3316
3356
  };
3317
3357
  var InboxContextImpl = class InboxContextImpl extends ContextImpl {