@fedify/fedify 2.0.35-dev.2480 → 2.0.35

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 (79) hide show
  1. package/dist/{builder-cEjyBwpD.mjs → builder-CpxZPa7I.mjs} +3 -3
  2. package/dist/{client-CAM_bQXx.d.cts → client-BIBzqEIp.d.cts} +0 -1
  3. package/dist/{client-BUqbea1d.mjs → client-DGNacEx_.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-r8sjhltN.d.cts → context-CnoWxuoB.d.cts} +2 -2
  9. package/dist/{context-B2iX3heK.d.ts → context-lxjaUTAI.d.ts} +2 -2
  10. package/dist/{deno-BcnGeGr2.mjs → deno-BAtjc8bx.mjs} +1 -1
  11. package/dist/docloader-CyBB50Om.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/middleware.test.mjs +42 -23
  17. package/dist/federation/mod.cjs +1 -1
  18. package/dist/federation/mod.d.cts +2 -2
  19. package/dist/federation/mod.d.ts +2 -2
  20. package/dist/federation/mod.js +1 -1
  21. package/dist/federation/send.test.mjs +445 -70
  22. package/dist/federation/temporal.test.mjs +1 -1
  23. package/dist/federation/webfinger.test.mjs +1 -1
  24. package/dist/{http-BD9izRp9.cjs → http-5u2wDmjl.cjs} +114 -56
  25. package/dist/{http-D0cjeL8a.mjs → http-9wCo74JF.mjs} +136 -49
  26. package/dist/{http-Bs6thuCC.d.cts → http-Cd8UD1ym.d.ts} +1 -2
  27. package/dist/{http-CDupyfOc.d.ts → http-DFqAb11b.d.cts} +1 -2
  28. package/dist/{http-IYRDhhnT.js → http-Dwj7bNhv.js} +113 -55
  29. package/dist/{inbox-D2fsxjqC.mjs → inbox-De2IaCfd.mjs} +1 -1
  30. package/dist/{key-BFzQ3Hef.mjs → key-BD-PBpNB.mjs} +1 -1
  31. package/dist/{kv-cache-C-RyNMt6.js → kv-cache-5TdAMHng.js} +60 -18
  32. package/dist/{kv-cache-B4sDuLjb.cjs → kv-cache-ivTOKx4b.cjs} +59 -17
  33. package/dist/{ld-D2bkYBIz.mjs → ld-34_mZbyP.mjs} +2 -2
  34. package/dist/{middleware-3mc-kq3v.mjs → middleware-BI7Hldd0.mjs} +1 -1
  35. package/dist/{middleware-DxX8ZL-q.cjs → middleware-Co4IpMca.cjs} +118 -33
  36. package/dist/{middleware-DRZp7Odv.js → middleware-DTJ9dnYC.js} +118 -33
  37. package/dist/{middleware-B2UC8mtN.cjs → middleware-DhwZ4gNG.cjs} +1 -1
  38. package/dist/{middleware-CXIBpN4y.mjs → middleware-DzaMP9BP.mjs} +12 -12
  39. package/dist/{mod-BkPB4hPe.d.cts → mod-BPDfu4Ey.d.cts} +2 -7
  40. package/dist/{mod-E7YM72JZ.d.ts → mod-BS-jg3Wx.d.ts} +1 -1
  41. package/dist/{mod-BKJyD7nk.d.ts → mod-DGP89q0u.d.ts} +2 -7
  42. package/dist/{mod-IOWUF5Yr.d.cts → mod-DVPS4yam.d.cts} +1 -1
  43. package/dist/{mod-CM_eR8hQ.d.ts → mod-I-34OMN3.d.ts} +1 -1
  44. package/dist/{mod-DDTLbfce.d.cts → mod-w1oyHseQ.d.cts} +1 -1
  45. package/dist/mod.cjs +5 -5
  46. package/dist/mod.d.cts +6 -6
  47. package/dist/mod.d.ts +6 -6
  48. package/dist/mod.js +5 -5
  49. package/dist/nodeinfo/client.test.mjs +337 -19
  50. package/dist/nodeinfo/handler.test.mjs +1 -1
  51. package/dist/nodeinfo/mod.cjs +1 -1
  52. package/dist/nodeinfo/mod.d.cts +1 -1
  53. package/dist/nodeinfo/mod.d.ts +1 -1
  54. package/dist/nodeinfo/mod.js +1 -1
  55. package/dist/{owner-BuJXJDhl.mjs → owner-CxBLBD2K.mjs} +2 -2
  56. package/dist/{proof-BJ_6nKhb.js → proof-D5IUmaZ0.js} +1 -1
  57. package/dist/{proof-JxVhLZA3.mjs → proof-DhW1tGUz.mjs} +2 -2
  58. package/dist/{proof-DfmHAmtr.cjs → proof-yX6qSpxk.cjs} +1 -1
  59. package/dist/{send-CDaWmeqV.mjs → send-CdfEXNS7.mjs} +117 -32
  60. package/dist/sig/http.test.mjs +411 -2
  61. package/dist/sig/key.test.mjs +2 -2
  62. package/dist/sig/ld.test.mjs +2 -2
  63. package/dist/sig/mod.cjs +2 -2
  64. package/dist/sig/mod.d.cts +2 -2
  65. package/dist/sig/mod.d.ts +2 -2
  66. package/dist/sig/mod.js +2 -2
  67. package/dist/sig/owner.test.mjs +1 -1
  68. package/dist/sig/proof.test.mjs +1 -1
  69. package/dist/{temporal-Bv6Zn1C5.mjs → temporal-BbywNJ81.mjs} +1 -1
  70. package/dist/testing/mod.d.mts +0 -1
  71. package/dist/{types-miAYfiGO.js → types-B1_pRFE_.js} +112 -63
  72. package/dist/{types-DRPjmHVV.cjs → types-DOvyymM4.cjs} +111 -62
  73. package/dist/utils/docloader.test.mjs +316 -32
  74. package/dist/utils/mod.cjs +1 -1
  75. package/dist/utils/mod.d.cts +1 -1
  76. package/dist/utils/mod.d.ts +1 -1
  77. package/dist/utils/mod.js +1 -1
  78. package/package.json +5 -5
  79. package/dist/docloader-BqDq3fDF.mjs +0 -82
@@ -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-BD9izRp9.cjs");
6
- const require_types = require("./types-DRPjmHVV.cjs");
7
- const require_proof = require("./proof-DfmHAmtr.cjs");
8
- const require_kv_cache = require("./kv-cache-B4sDuLjb.cjs");
5
+ const require_http = require("./http-5u2wDmjl.cjs");
6
+ const require_types = require("./types-DOvyymM4.cjs");
7
+ const require_proof = require("./proof-yX6qSpxk.cjs");
8
+ const require_kv_cache = require("./kv-cache-ivTOKx4b.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 {
@@ -349,7 +350,7 @@ var FederationBuilderImpl = class {
349
350
  this.collectionTypeIds = {};
350
351
  }
351
352
  async build(options) {
352
- const { FederationImpl } = await Promise.resolve().then(() => require("./middleware-B2UC8mtN.cjs"));
353
+ const { FederationImpl } = await Promise.resolve().then(() => require("./middleware-DhwZ4gNG.cjs"));
353
354
  const f = new FederationImpl(options);
354
355
  const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive;
355
356
  f.router = this.router.clone();
@@ -2263,9 +2264,10 @@ function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
2263
2264
  *
2264
2265
  * @param parameters The parameters for sending the activity.
2265
2266
  * See also {@link SendActivityParameters}.
2267
+ * @param deps The dependencies, for tests.
2266
2268
  * @throws {Error} If the activity fails to send.
2267
2269
  */
2268
- function sendActivity(options) {
2270
+ function sendActivity(options, deps = {}) {
2269
2271
  const tracerProvider = options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider();
2270
2272
  return tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan("activitypub.send_activity", {
2271
2273
  kind: _opentelemetry_api.SpanKind.CLIENT,
@@ -2277,7 +2279,7 @@ function sendActivity(options) {
2277
2279
  await sendActivityInternal({
2278
2280
  ...options,
2279
2281
  tracerProvider
2280
- }, span);
2282
+ }, span, deps);
2281
2283
  } catch (e) {
2282
2284
  span.setStatus({
2283
2285
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -2311,21 +2313,25 @@ async function readLimitedResponseBody(response, maxBytes) {
2311
2313
  totalBytes += value.length;
2312
2314
  }
2313
2315
  } finally {
2316
+ if (truncated) reader.cancel().catch(() => {});
2314
2317
  reader.releaseLock();
2315
2318
  }
2316
2319
  let result = chunks.join("");
2317
2320
  if (truncated) result += "… (truncated)";
2318
2321
  return result;
2319
2322
  }
2320
- async function sendActivityInternal({ activity, activityId, keys, inbox, allowPrivateAddress, headers, specDeterminer, tracerProvider }, span) {
2323
+ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPrivateAddress, headers, specDeterminer, tracerProvider }, span, deps) {
2321
2324
  const logger = (0, _logtape_logtape.getLogger)([
2322
2325
  "fedify",
2323
2326
  "federation",
2324
2327
  "outbox"
2325
2328
  ]);
2326
- async function validateUrl(url) {
2327
- if (!allowPrivateAddress) try {
2328
- await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
2329
+ const openUnvalidatedHop = deps.openHop ?? (allowPrivateAddress ? _fedify_vocab_runtime_internal.openDirectFetch : (url) => (0, _fedify_vocab_runtime_internal.openPublicFetch)(url));
2330
+ let opened = false;
2331
+ async function openHop(url) {
2332
+ let session;
2333
+ try {
2334
+ session = await openUnvalidatedHop(url);
2329
2335
  } catch (error) {
2330
2336
  if (error instanceof _fedify_vocab_runtime.UrlError && error.reason === "dns") {
2331
2337
  logger.error("DNS resolution failed for URL: {url}", {
@@ -2342,8 +2348,9 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPr
2342
2348
  });
2343
2349
  throw error;
2344
2350
  }
2351
+ opened = true;
2352
+ return session;
2345
2353
  }
2346
- await validateUrl(inbox.href);
2347
2354
  headers = new Headers(headers);
2348
2355
  headers.set("Content-Type", "application/activity+json");
2349
2356
  const request = new Request(inbox, {
@@ -2364,36 +2371,30 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPr
2364
2371
  privateKey: pair.privateKey
2365
2372
  }))
2366
2373
  });
2367
- let response;
2374
+ let outcome;
2368
2375
  try {
2369
- response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await require_http.doubleKnock(request, rsaKey, {
2376
+ outcome = rsaKey == null ? await fetchWithValidatedRedirects(request, openHop, (response) => consumeSendActivityResponse(response, inbox)) : await require_http.doubleKnockScoped(request, rsaKey, {
2370
2377
  tracerProvider,
2371
- specDeterminer,
2372
- validateRedirect: validateUrl
2373
- });
2378
+ specDeterminer
2379
+ }, openHop, (response) => consumeSendActivityResponse(response, inbox));
2374
2380
  } catch (error) {
2375
- logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
2381
+ if (opened) logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
2376
2382
  activityId,
2377
2383
  inbox: inbox.href,
2378
2384
  error
2379
2385
  });
2380
2386
  throw error;
2381
2387
  }
2382
- if (!response.ok) {
2383
- let error;
2384
- try {
2385
- error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
2386
- } catch (_) {
2387
- error = "";
2388
- }
2388
+ if (!outcome.ok) {
2389
+ const { status, statusText, error } = outcome;
2389
2390
  logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
2390
2391
  activityId,
2391
2392
  inbox: inbox.href,
2392
- status: response.status,
2393
- statusText: response.statusText,
2393
+ status,
2394
+ statusText,
2394
2395
  error
2395
2396
  });
2396
- throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error);
2397
+ throw new SendActivityError(inbox, status, `Failed to send activity ${activityId} to ${inbox.href} (${status} ${statusText}):\n${error}`, error);
2397
2398
  }
2398
2399
  span.addEvent("activitypub.activity.sent", {
2399
2400
  "activitypub.activity.json": JSON.stringify(activity),
@@ -2401,10 +2402,74 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPr
2401
2402
  "activitypub.activity.id": activityId ?? ""
2402
2403
  });
2403
2404
  }
2404
- async function fetchWithValidatedRedirects(request, validateUrl) {
2405
+ /**
2406
+ * Starts cancelling the body of a response without waiting for it to settle.
2407
+ * The cancellation of a branch of a tee (e.g., a body cloned by a fetch mock)
2408
+ * settles only after the other branch is cancelled too, per the Streams
2409
+ * standard, so awaiting it could block forever. Failures are only logged.
2410
+ */
2411
+ function cancelResponseBody(response, url) {
2412
+ response.body?.cancel().catch((error) => {
2413
+ (0, _logtape_logtape.getLogger)([
2414
+ "fedify",
2415
+ "federation",
2416
+ "outbox"
2417
+ ]).debug("Failed to cancel the response body from {url}: {error}", {
2418
+ url,
2419
+ error
2420
+ });
2421
+ });
2422
+ }
2423
+ /**
2424
+ * Reads what is needed from the final response of a delivery while its
2425
+ * session is open: the body of a successful response is cancelled, and at
2426
+ * most 1 KiB of an error response body is read.
2427
+ */
2428
+ async function consumeSendActivityResponse(response, inbox) {
2429
+ if (response.ok) {
2430
+ cancelResponseBody(response, response.url || inbox.href);
2431
+ return { ok: true };
2432
+ }
2433
+ let error;
2434
+ try {
2435
+ error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
2436
+ } catch (_) {
2437
+ error = "";
2438
+ }
2439
+ return {
2440
+ ok: false,
2441
+ status: response.status,
2442
+ statusText: response.statusText,
2443
+ error
2444
+ };
2445
+ }
2446
+ /**
2447
+ * Sends a request without signing it, following redirects manually like the
2448
+ * Fetch standard does. Every hop is opened with `openHop()`, which validates
2449
+ * the destination, after the previous hop is closed. The final response is
2450
+ * passed to `consume()`, and its session is closed after that.
2451
+ * This is exported only for tests; it is not exported from any entry point.
2452
+ * @param request The request to send.
2453
+ * @param openHop The function to open a session for each hop.
2454
+ * @param consume The function to consume the final response.
2455
+ * @returns The value returned by `consume`.
2456
+ */
2457
+ async function fetchWithValidatedRedirects(request, openHop, consume) {
2458
+ const logger = (0, _logtape_logtape.getLogger)([
2459
+ "fedify",
2460
+ "federation",
2461
+ "outbox"
2462
+ ]);
2405
2463
  let body = await request.clone().text();
2406
2464
  for (let redirects = 0;; redirects++) {
2407
- const response = await fetch(request, { redirect: "manual" });
2465
+ const session = await openHop(request.url);
2466
+ let response;
2467
+ try {
2468
+ response = await session.fetch(request, { redirect: "manual" });
2469
+ } catch (error) {
2470
+ await closeAfterError(session, logger);
2471
+ throw error;
2472
+ }
2408
2473
  const location = response.headers.get("Location");
2409
2474
  if (![
2410
2475
  301,
@@ -2412,11 +2477,21 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
2412
2477
  303,
2413
2478
  307,
2414
2479
  308
2415
- ].includes(response.status) || location == null) return response;
2416
- await response.body?.cancel();
2480
+ ].includes(response.status) || location == null) try {
2481
+ return await consume(response);
2482
+ } catch (error) {
2483
+ logger.debug("Failed to consume the response from {url}; closing the session: {error}", {
2484
+ url: request.url,
2485
+ error
2486
+ });
2487
+ throw error;
2488
+ } finally {
2489
+ await session.close();
2490
+ }
2491
+ cancelResponseBody(response, request.url);
2492
+ await session.close();
2417
2493
  if (redirects >= 20) throw new TypeError("Too many redirects");
2418
2494
  const url = new URL(location, request.url);
2419
- await validateUrl(url.href);
2420
2495
  const headers = new Headers(request.headers);
2421
2496
  if (url.origin !== new URL(request.url).origin) {
2422
2497
  headers.delete("Authorization");
@@ -2444,6 +2519,16 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
2444
2519
  });
2445
2520
  }
2446
2521
  }
2522
+ async function closeAfterError(session, logger) {
2523
+ try {
2524
+ await session.close();
2525
+ } catch (error) {
2526
+ logger.debug("Failed to close the session for {url}: {error}", {
2527
+ url: session.url.href,
2528
+ error
2529
+ });
2530
+ }
2531
+ }
2447
2532
  /**
2448
2533
  * An error that is thrown when an activity fails to send to a remote inbox.
2449
2534
  * It contains structured information about the failure, including the HTTP
@@ -2,10 +2,10 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  import { t as __exportAll } from "./chunk-CRNNMoPX.js";
4
4
  import { r as getDefaultActivityTransformers } from "./transformers-BGMIq1cs.js";
5
- import { c as importJwk, d as name, f as version, i as exportJwk, l as validateCryptoKey, r as verifyRequest, t as doubleKnock } from "./http-IYRDhhnT.js";
6
- import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-miAYfiGO.js";
7
- import { _ as signJsonLd, a as doesActorOwnKey, c as assertSafeJsonLd, f as detachSignature, g as isInvalidUrlTypeError, h as isClearlyMalformedContextReference, m as hasSignature, n as signObject, o as getKeyOwner, p as getNormalizationContextLoader, r as verifyObject, s as InvalidContextReferenceError, u as compactJsonLd, v as verifyCompactJsonLd, x as wrapContextLoaderForJsonLd } from "./proof-BJ_6nKhb.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-C-RyNMt6.js";
5
+ import { c as importJwk, d as name, f as version, i as exportJwk, l as validateCryptoKey, r as verifyRequest, t as doubleKnockScoped } from "./http-Dwj7bNhv.js";
6
+ import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-B1_pRFE_.js";
7
+ import { _ as signJsonLd, a as doesActorOwnKey, c as assertSafeJsonLd, f as detachSignature, g as isInvalidUrlTypeError, h as isClearlyMalformedContextReference, m as hasSignature, n as signObject, o as getKeyOwner, p as getNormalizationContextLoader, r as verifyObject, s as InvalidContextReferenceError, u as compactJsonLd, v as verifyCompactJsonLd, x as wrapContextLoaderForJsonLd } from "./proof-D5IUmaZ0.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-5TdAMHng.js";
9
9
  import { getLogger, withContext } from "@logtape/logtape";
10
10
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
11
11
  import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
@@ -13,10 +13,11 @@ import { cloneDeep } from "es-toolkit";
13
13
  import { Router } from "uri-template-router";
14
14
  import { parseTemplate } from "url-template";
15
15
  import { encodeHex } from "byte-encodings/hex";
16
- import { FetchError, UrlError, getDocumentLoader, validatePublicUrl } from "@fedify/vocab-runtime";
16
+ import { FetchError, UrlError, getDocumentLoader } from "@fedify/vocab-runtime";
17
17
  import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
18
18
  import jsonld from "@fedify/vocab-runtime/jsonld";
19
19
  import { lookupWebFinger } from "@fedify/webfinger";
20
+ import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
20
21
  import { domainToASCII } from "node:url";
21
22
  //#region src/federation/inbox.ts
22
23
  var InboxListenerSet = class InboxListenerSet {
@@ -2262,9 +2263,10 @@ function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
2262
2263
  *
2263
2264
  * @param parameters The parameters for sending the activity.
2264
2265
  * See also {@link SendActivityParameters}.
2266
+ * @param deps The dependencies, for tests.
2265
2267
  * @throws {Error} If the activity fails to send.
2266
2268
  */
2267
- function sendActivity(options) {
2269
+ function sendActivity(options, deps = {}) {
2268
2270
  const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
2269
2271
  return tracerProvider.getTracer(name, version).startActiveSpan("activitypub.send_activity", {
2270
2272
  kind: SpanKind.CLIENT,
@@ -2276,7 +2278,7 @@ function sendActivity(options) {
2276
2278
  await sendActivityInternal({
2277
2279
  ...options,
2278
2280
  tracerProvider
2279
- }, span);
2281
+ }, span, deps);
2280
2282
  } catch (e) {
2281
2283
  span.setStatus({
2282
2284
  code: SpanStatusCode.ERROR,
@@ -2310,21 +2312,25 @@ async function readLimitedResponseBody(response, maxBytes) {
2310
2312
  totalBytes += value.length;
2311
2313
  }
2312
2314
  } finally {
2315
+ if (truncated) reader.cancel().catch(() => {});
2313
2316
  reader.releaseLock();
2314
2317
  }
2315
2318
  let result = chunks.join("");
2316
2319
  if (truncated) result += "… (truncated)";
2317
2320
  return result;
2318
2321
  }
2319
- async function sendActivityInternal({ activity, activityId, keys, inbox, allowPrivateAddress, headers, specDeterminer, tracerProvider }, span) {
2322
+ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPrivateAddress, headers, specDeterminer, tracerProvider }, span, deps) {
2320
2323
  const logger = getLogger([
2321
2324
  "fedify",
2322
2325
  "federation",
2323
2326
  "outbox"
2324
2327
  ]);
2325
- async function validateUrl(url) {
2326
- if (!allowPrivateAddress) try {
2327
- await validatePublicUrl(url);
2328
+ const openUnvalidatedHop = deps.openHop ?? (allowPrivateAddress ? openDirectFetch : (url) => openPublicFetch(url));
2329
+ let opened = false;
2330
+ async function openHop(url) {
2331
+ let session;
2332
+ try {
2333
+ session = await openUnvalidatedHop(url);
2328
2334
  } catch (error) {
2329
2335
  if (error instanceof UrlError && error.reason === "dns") {
2330
2336
  logger.error("DNS resolution failed for URL: {url}", {
@@ -2341,8 +2347,9 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPr
2341
2347
  });
2342
2348
  throw error;
2343
2349
  }
2350
+ opened = true;
2351
+ return session;
2344
2352
  }
2345
- await validateUrl(inbox.href);
2346
2353
  headers = new Headers(headers);
2347
2354
  headers.set("Content-Type", "application/activity+json");
2348
2355
  const request = new Request(inbox, {
@@ -2363,36 +2370,30 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPr
2363
2370
  privateKey: pair.privateKey
2364
2371
  }))
2365
2372
  });
2366
- let response;
2373
+ let outcome;
2367
2374
  try {
2368
- response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await doubleKnock(request, rsaKey, {
2375
+ outcome = rsaKey == null ? await fetchWithValidatedRedirects(request, openHop, (response) => consumeSendActivityResponse(response, inbox)) : await doubleKnockScoped(request, rsaKey, {
2369
2376
  tracerProvider,
2370
- specDeterminer,
2371
- validateRedirect: validateUrl
2372
- });
2377
+ specDeterminer
2378
+ }, openHop, (response) => consumeSendActivityResponse(response, inbox));
2373
2379
  } catch (error) {
2374
- logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
2380
+ if (opened) logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
2375
2381
  activityId,
2376
2382
  inbox: inbox.href,
2377
2383
  error
2378
2384
  });
2379
2385
  throw error;
2380
2386
  }
2381
- if (!response.ok) {
2382
- let error;
2383
- try {
2384
- error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
2385
- } catch (_) {
2386
- error = "";
2387
- }
2387
+ if (!outcome.ok) {
2388
+ const { status, statusText, error } = outcome;
2388
2389
  logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
2389
2390
  activityId,
2390
2391
  inbox: inbox.href,
2391
- status: response.status,
2392
- statusText: response.statusText,
2392
+ status,
2393
+ statusText,
2393
2394
  error
2394
2395
  });
2395
- throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error);
2396
+ throw new SendActivityError(inbox, status, `Failed to send activity ${activityId} to ${inbox.href} (${status} ${statusText}):\n${error}`, error);
2396
2397
  }
2397
2398
  span.addEvent("activitypub.activity.sent", {
2398
2399
  "activitypub.activity.json": JSON.stringify(activity),
@@ -2400,10 +2401,74 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPr
2400
2401
  "activitypub.activity.id": activityId ?? ""
2401
2402
  });
2402
2403
  }
2403
- async function fetchWithValidatedRedirects(request, validateUrl) {
2404
+ /**
2405
+ * Starts cancelling the body of a response without waiting for it to settle.
2406
+ * The cancellation of a branch of a tee (e.g., a body cloned by a fetch mock)
2407
+ * settles only after the other branch is cancelled too, per the Streams
2408
+ * standard, so awaiting it could block forever. Failures are only logged.
2409
+ */
2410
+ function cancelResponseBody(response, url) {
2411
+ response.body?.cancel().catch((error) => {
2412
+ getLogger([
2413
+ "fedify",
2414
+ "federation",
2415
+ "outbox"
2416
+ ]).debug("Failed to cancel the response body from {url}: {error}", {
2417
+ url,
2418
+ error
2419
+ });
2420
+ });
2421
+ }
2422
+ /**
2423
+ * Reads what is needed from the final response of a delivery while its
2424
+ * session is open: the body of a successful response is cancelled, and at
2425
+ * most 1 KiB of an error response body is read.
2426
+ */
2427
+ async function consumeSendActivityResponse(response, inbox) {
2428
+ if (response.ok) {
2429
+ cancelResponseBody(response, response.url || inbox.href);
2430
+ return { ok: true };
2431
+ }
2432
+ let error;
2433
+ try {
2434
+ error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
2435
+ } catch (_) {
2436
+ error = "";
2437
+ }
2438
+ return {
2439
+ ok: false,
2440
+ status: response.status,
2441
+ statusText: response.statusText,
2442
+ error
2443
+ };
2444
+ }
2445
+ /**
2446
+ * Sends a request without signing it, following redirects manually like the
2447
+ * Fetch standard does. Every hop is opened with `openHop()`, which validates
2448
+ * the destination, after the previous hop is closed. The final response is
2449
+ * passed to `consume()`, and its session is closed after that.
2450
+ * This is exported only for tests; it is not exported from any entry point.
2451
+ * @param request The request to send.
2452
+ * @param openHop The function to open a session for each hop.
2453
+ * @param consume The function to consume the final response.
2454
+ * @returns The value returned by `consume`.
2455
+ */
2456
+ async function fetchWithValidatedRedirects(request, openHop, consume) {
2457
+ const logger = getLogger([
2458
+ "fedify",
2459
+ "federation",
2460
+ "outbox"
2461
+ ]);
2404
2462
  let body = await request.clone().text();
2405
2463
  for (let redirects = 0;; redirects++) {
2406
- const response = await fetch(request, { redirect: "manual" });
2464
+ const session = await openHop(request.url);
2465
+ let response;
2466
+ try {
2467
+ response = await session.fetch(request, { redirect: "manual" });
2468
+ } catch (error) {
2469
+ await closeAfterError(session, logger);
2470
+ throw error;
2471
+ }
2407
2472
  const location = response.headers.get("Location");
2408
2473
  if (![
2409
2474
  301,
@@ -2411,11 +2476,21 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
2411
2476
  303,
2412
2477
  307,
2413
2478
  308
2414
- ].includes(response.status) || location == null) return response;
2415
- await response.body?.cancel();
2479
+ ].includes(response.status) || location == null) try {
2480
+ return await consume(response);
2481
+ } catch (error) {
2482
+ logger.debug("Failed to consume the response from {url}; closing the session: {error}", {
2483
+ url: request.url,
2484
+ error
2485
+ });
2486
+ throw error;
2487
+ } finally {
2488
+ await session.close();
2489
+ }
2490
+ cancelResponseBody(response, request.url);
2491
+ await session.close();
2416
2492
  if (redirects >= 20) throw new TypeError("Too many redirects");
2417
2493
  const url = new URL(location, request.url);
2418
- await validateUrl(url.href);
2419
2494
  const headers = new Headers(request.headers);
2420
2495
  if (url.origin !== new URL(request.url).origin) {
2421
2496
  headers.delete("Authorization");
@@ -2443,6 +2518,16 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
2443
2518
  });
2444
2519
  }
2445
2520
  }
2521
+ async function closeAfterError(session, logger) {
2522
+ try {
2523
+ await session.close();
2524
+ } catch (error) {
2525
+ logger.debug("Failed to close the session for {url}: {error}", {
2526
+ url: session.url.href,
2527
+ error
2528
+ });
2529
+ }
2530
+ }
2446
2531
  /**
2447
2532
  * An error that is thrown when an activity fails to send to a remote inbox.
2448
2533
  * It contains structured information about the failure, including the HTTP
@@ -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-DxX8ZL-q.cjs");
3
+ const require_middleware = require("./middleware-Co4IpMca.cjs");
4
4
  exports.FederationImpl = require_middleware.FederationImpl;
@@ -2,24 +2,24 @@ 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-BcnGeGr2.mjs";
6
- import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-BFzQ3Hef.mjs";
7
- import { l as verifyRequest } from "./http-D0cjeL8a.mjs";
8
- import { t as getAuthenticatedDocumentLoader } from "./docloader-BqDq3fDF.mjs";
5
+ import { n as version, t as name } from "./deno-BAtjc8bx.mjs";
6
+ import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-BD-PBpNB.mjs";
7
+ import { u as verifyRequest } from "./http-9wCo74JF.mjs";
8
+ import { n as getAuthenticatedDocumentLoader } from "./docloader-CyBB50Om.mjs";
9
9
  import { n as kvCache } from "./kv-cache-BrjuJue8.mjs";
10
- 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-D2bkYBIz.mjs";
11
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-BuJXJDhl.mjs";
12
- import { n as signObject, r as verifyObject } from "./proof-JxVhLZA3.mjs";
13
- import { d as readBoundedText, l as BodyTooLargeError, t as getNodeInfo, u as MAX_BODY_SIZE } from "./client-BUqbea1d.mjs";
10
+ 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-34_mZbyP.mjs";
11
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-CxBLBD2K.mjs";
12
+ import { n as signObject, r as verifyObject } from "./proof-DhW1tGUz.mjs";
13
+ import { d as MAX_BODY_SIZE, f as readBoundedText, t as getNodeInfo, u as BodyTooLargeError } from "./client-DGNacEx_.mjs";
14
14
  import { t as nodeInfoToJson } from "./types-MNlFfxjv.mjs";
15
- import { n as actorsHaveSameOrigin, r as routeActivity } from "./inbox-D2fsxjqC.mjs";
16
- import { t as FederationBuilderImpl } from "./builder-cEjyBwpD.mjs";
15
+ import { n as actorsHaveSameOrigin, r as routeActivity } from "./inbox-De2IaCfd.mjs";
16
+ import { t as FederationBuilderImpl } from "./builder-CpxZPa7I.mjs";
17
17
  import { t as buildCollectionSynchronizationHeader } from "./collection-DI6YzYpd.mjs";
18
18
  import { t as KvKeyCache } from "./keycache-DBD6fzl1.mjs";
19
19
  import { t as acceptsJsonLd } from "./negotiation-TWEs6Cl8.mjs";
20
- import { t as hasMalformedKnownTemporalLiteral } from "./temporal-Bv6Zn1C5.mjs";
20
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-BbywNJ81.mjs";
21
21
  import { t as createExponentialBackoffPolicy } from "./retry-Ddbq3AcK.mjs";
22
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-CDaWmeqV.mjs";
22
+ import { i as sendActivity, n as extractInboxes, t as SendActivityError } from "./send-CdfEXNS7.mjs";
23
23
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
24
24
  import { lookupWebFinger } from "@fedify/webfinger";
25
25
  import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { n as HttpMessageSignaturesSpecDeterminer } from "./http-Bs6thuCC.cjs";
2
+ import { n as HttpMessageSignaturesSpecDeterminer } from "./http-DFqAb11b.cjs";
3
3
  import { n as KvStore, t as KvKey } from "./kv-CZKl1IUS.cjs";
4
4
  import { DocumentLoader, DocumentLoaderFactoryOptions } from "@fedify/vocab-runtime";
5
5
  import { TracerProvider } from "@opentelemetry/api";
@@ -42,12 +42,7 @@ interface GetAuthenticatedDocumentLoaderOptions extends DocumentLoaderFactoryOpt
42
42
  declare function getAuthenticatedDocumentLoader(identity: {
43
43
  keyId: URL;
44
44
  privateKey: CryptoKey;
45
- }, {
46
- allowPrivateAddress,
47
- userAgent,
48
- specDeterminer,
49
- tracerProvider
50
- }?: GetAuthenticatedDocumentLoaderOptions): DocumentLoader;
45
+ }, options?: GetAuthenticatedDocumentLoaderOptions): DocumentLoader;
51
46
  //#endregion
52
47
  //#region src/utils/kv-cache.d.ts
53
48
  /**
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { l as KeyCache } from "./http-CDupyfOc.js";
2
+ import { l as KeyCache } from "./http-Cd8UD1ym.js";
3
3
  import { CryptographicKey, DataIntegrityProof, Multikey, Object as Object$1 } from "@fedify/vocab";
4
4
  import { TracerProvider } from "@opentelemetry/api";
5
5
  import { DocumentLoader } from "@fedify/vocab-runtime";
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { n as HttpMessageSignaturesSpecDeterminer } from "./http-CDupyfOc.js";
2
+ import { n as HttpMessageSignaturesSpecDeterminer } from "./http-Cd8UD1ym.js";
3
3
  import { n as KvStore, t as KvKey } from "./kv-CxaI4eGo.js";
4
4
  import { TracerProvider } from "@opentelemetry/api";
5
5
  import { DocumentLoader, DocumentLoaderFactoryOptions } from "@fedify/vocab-runtime";
@@ -42,12 +42,7 @@ interface GetAuthenticatedDocumentLoaderOptions extends DocumentLoaderFactoryOpt
42
42
  declare function getAuthenticatedDocumentLoader(identity: {
43
43
  keyId: URL;
44
44
  privateKey: CryptoKey;
45
- }, {
46
- allowPrivateAddress,
47
- userAgent,
48
- specDeterminer,
49
- tracerProvider
50
- }?: GetAuthenticatedDocumentLoaderOptions): DocumentLoader;
45
+ }, options?: GetAuthenticatedDocumentLoaderOptions): DocumentLoader;
51
46
  //#endregion
52
47
  //#region src/utils/kv-cache.d.ts
53
48
  /**
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { l as KeyCache } from "./http-Bs6thuCC.cjs";
2
+ import { l as KeyCache } from "./http-DFqAb11b.cjs";
3
3
  import { CryptographicKey, DataIntegrityProof, Multikey, Object as Object$1 } from "@fedify/vocab";
4
4
  import { DocumentLoader } from "@fedify/vocab-runtime";
5
5
  import { TracerProvider } from "@opentelemetry/api";
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { X as ActorDispatcher, Y as ActorAliasMapper, Z as ActorHandleMapper, mt as WebFingerLinksDispatcher, s as RequestContext } from "./context-B2iX3heK.js";
2
+ import { X as ActorDispatcher, Y as ActorAliasMapper, Z as ActorHandleMapper, mt as WebFingerLinksDispatcher, s as RequestContext } from "./context-lxjaUTAI.js";
3
3
  import { Span, Tracer } from "@opentelemetry/api";
4
4
 
5
5
  //#region src/federation/webfinger.d.ts
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { X as ActorDispatcher, Y as ActorAliasMapper, Z as ActorHandleMapper, mt as WebFingerLinksDispatcher, s as RequestContext } from "./context-r8sjhltN.cjs";
2
+ import { X as ActorDispatcher, Y as ActorAliasMapper, Z as ActorHandleMapper, mt as WebFingerLinksDispatcher, s as RequestContext } from "./context-CnoWxuoB.cjs";
3
3
  import { Span, Tracer } from "@opentelemetry/api";
4
4
 
5
5
  //#region src/federation/webfinger.d.ts
package/dist/mod.cjs CHANGED
@@ -4,11 +4,11 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
4
  require("./chunk-DDcVe30Y.cjs");
5
5
  const require_transformers = require("./transformers-NeAONrAq.cjs");
6
6
  require("./compat/mod.cjs");
7
- const require_http = require("./http-BD9izRp9.cjs");
8
- const require_middleware = require("./middleware-DxX8ZL-q.cjs");
9
- const require_types = require("./types-DRPjmHVV.cjs");
10
- const require_proof = require("./proof-DfmHAmtr.cjs");
11
- const require_kv_cache = require("./kv-cache-B4sDuLjb.cjs");
7
+ const require_http = require("./http-5u2wDmjl.cjs");
8
+ const require_middleware = require("./middleware-Co4IpMca.cjs");
9
+ const require_types = require("./types-DOvyymM4.cjs");
10
+ const require_proof = require("./proof-yX6qSpxk.cjs");
11
+ const require_kv_cache = require("./kv-cache-ivTOKx4b.cjs");
12
12
  const require_federation_mod = require("./federation/mod.cjs");
13
13
  require("./nodeinfo/mod.cjs");
14
14
  require("./runtime/mod.cjs");