@fedify/vocab-runtime 2.2.19 → 2.2.20

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 (114) hide show
  1. package/deno.json +5 -2
  2. package/dist/internal.cjs +6 -0
  3. package/dist/internal.d.cts +3 -0
  4. package/dist/internal.d.ts +3 -0
  5. package/dist/internal.js +3 -0
  6. package/dist/mod.cjs +105 -253
  7. package/dist/mod.d.cts +5 -28
  8. package/dist/mod.d.ts +6 -28
  9. package/dist/mod.js +98 -245
  10. package/dist/public-fetch-BC-nWJNM.d.ts +143 -0
  11. package/dist/public-fetch-CFnW7XMn.cjs +821 -0
  12. package/dist/public-fetch-DBpt3xxf.d.cts +143 -0
  13. package/dist/public-fetch-Dfmc_Kfy.js +772 -0
  14. package/dist/tests/{body-BOnyCa1y.mjs → body-B9_hb-GO.mjs} +1 -1
  15. package/dist/tests/{body-DhbRZ6Ps.cjs → body-pOSG2RFb.cjs} +1 -1
  16. package/dist/tests/body.test.cjs +1 -1
  17. package/dist/tests/body.test.mjs +1 -1
  18. package/dist/tests/decimal.test.cjs +6 -6
  19. package/dist/tests/decimal.test.mjs +6 -6
  20. package/dist/tests/{docloader-BjyFsnAA.mjs → docloader-Bdlp7FkK.mjs} +102 -37
  21. package/dist/tests/{docloader-BhSiTajy.cjs → docloader-Ck3VfY29.cjs} +106 -35
  22. package/dist/tests/docloader.test.cjs +523 -1273
  23. package/dist/tests/docloader.test.mjs +469 -1238
  24. package/dist/tests/esm-CnLt0wAF.mjs +1225 -0
  25. package/dist/tests/esm-CxbqfkLP.cjs +1208 -0
  26. package/dist/tests/internal/multicodec.test.cjs +1 -1
  27. package/dist/tests/internal/multicodec.test.mjs +1 -1
  28. package/dist/tests/{key-BDIsC6hV.mjs → key-GEdpxA4M.mjs} +2 -2
  29. package/dist/tests/{key-BarKGLQz.cjs → key-L6ReMuUb.cjs} +2 -2
  30. package/dist/tests/key.test.cjs +2 -2
  31. package/dist/tests/key.test.mjs +2 -2
  32. package/dist/tests/{langstr-BsqFJupv.cjs → langstr-CZpWmpo5.cjs} +1 -1
  33. package/dist/tests/{langstr-ABdAOKlF.mjs → langstr-DTB4sl0a.mjs} +1 -1
  34. package/dist/tests/langstr.test.cjs +1 -1
  35. package/dist/tests/langstr.test.mjs +1 -1
  36. package/dist/tests/langtag.test.cjs +1 -1
  37. package/dist/tests/langtag.test.mjs +1 -1
  38. package/dist/tests/link.test.mjs +1 -1
  39. package/dist/tests/multibase/multibase.test.cjs +1 -1
  40. package/dist/tests/multibase/multibase.test.mjs +1 -1
  41. package/dist/tests/node-C3bjFjL1.mjs +84 -0
  42. package/dist/tests/node-DWD76vPo.cjs +90 -0
  43. package/dist/tests/pin-grant-CU-o367k.cjs +70 -0
  44. package/dist/tests/pin-grant-HW3B6SKv.mjs +53 -0
  45. package/dist/tests/pin-grant.test.cjs +88 -0
  46. package/dist/tests/pin-grant.test.d.cts +1 -0
  47. package/dist/tests/pin-grant.test.d.mts +1 -0
  48. package/dist/tests/pin-grant.test.mjs +88 -0
  49. package/dist/tests/pinners/bun.test.cjs +167 -0
  50. package/dist/tests/pinners/bun.test.d.cts +1 -0
  51. package/dist/tests/pinners/bun.test.d.mts +1 -0
  52. package/dist/tests/pinners/bun.test.mjs +167 -0
  53. package/dist/tests/pinners/node.test.cjs +269 -0
  54. package/dist/tests/pinners/node.test.d.cts +1 -0
  55. package/dist/tests/pinners/node.test.d.mts +1 -0
  56. package/dist/tests/pinners/node.test.mjs +268 -0
  57. package/dist/tests/pinners/pinners.test.cjs +197 -0
  58. package/dist/tests/pinners/pinners.test.d.cts +1 -0
  59. package/dist/tests/pinners/pinners.test.d.mts +1 -0
  60. package/dist/tests/pinners/pinners.test.mjs +197 -0
  61. package/dist/tests/pinners/testdata/node-child.cjs +135 -0
  62. package/dist/tests/pinners/testdata/node-child.d.cts +1 -0
  63. package/dist/tests/pinners/testdata/node-child.d.mts +1 -0
  64. package/dist/tests/pinners/testdata/node-child.mjs +134 -0
  65. package/dist/tests/pinners-BjE-YJLL.cjs +221 -0
  66. package/dist/tests/pinners-ZgZDx9y6.mjs +203 -0
  67. package/dist/tests/public-fetch-Ddjfxbwc.cjs +187 -0
  68. package/dist/tests/public-fetch-Yi7Vks9s.mjs +170 -0
  69. package/dist/tests/public-fetch.test.cjs +1074 -0
  70. package/dist/tests/public-fetch.test.d.cts +1 -0
  71. package/dist/tests/public-fetch.test.d.mts +1 -0
  72. package/dist/tests/public-fetch.test.mjs +1074 -0
  73. package/dist/tests/{request-DGtx5MkG.cjs → request-B3NUKbyk.cjs} +1 -1
  74. package/dist/tests/{request-yCis9VCB.mjs → request-BSg3IQzf.mjs} +1 -1
  75. package/dist/tests/request.test.cjs +1 -1
  76. package/dist/tests/request.test.mjs +1 -1
  77. package/dist/tests/support-D5-KVjWZ.cjs +363 -0
  78. package/dist/tests/support-M5ClN-no.mjs +272 -0
  79. package/dist/tests/{url-vZTgkIZg.cjs → url-DLuSj57U.cjs} +88 -12
  80. package/dist/tests/{url-3fyJuqC0.mjs → url-DQWrMGhu.mjs} +65 -13
  81. package/dist/tests/url.test.cjs +4 -1
  82. package/dist/tests/url.test.mjs +4 -1
  83. package/package.json +14 -3
  84. package/src/docloader.test.ts +700 -32
  85. package/src/docloader.ts +174 -67
  86. package/src/internal.ts +13 -0
  87. package/src/pin-grant.test.ts +118 -0
  88. package/src/pin-grant.ts +99 -0
  89. package/src/pinners/bun.test.ts +278 -0
  90. package/src/pinners/bun.ts +223 -0
  91. package/src/pinners/mod.ts +59 -0
  92. package/src/pinners/node.test.ts +415 -0
  93. package/src/pinners/node.ts +144 -0
  94. package/src/pinners/pinners.test.ts +316 -0
  95. package/src/pinners/testdata/node-child.ts +147 -0
  96. package/src/pinners/testdata/proxy-child.ts +82 -0
  97. package/src/pinners/testdata/support.ts +343 -0
  98. package/src/pinners/testdata/tls-cert.pem +12 -0
  99. package/src/pinners/testdata/tls-child.ts +88 -0
  100. package/src/pinners/testdata/tls-key.pem +5 -0
  101. package/src/pinners/types.ts +48 -0
  102. package/src/public-fetch.test.ts +1286 -0
  103. package/src/public-fetch.ts +338 -0
  104. package/src/testdata/public-fetch-deno-worker.ts +74 -0
  105. package/src/url.test.ts +11 -0
  106. package/src/url.ts +113 -22
  107. package/tsdown.config.ts +12 -3
  108. /package/dist/tests/{langtag-LDBqlxHv.mjs → langtag-D8FWxjv8.mjs} +0 -0
  109. /package/dist/tests/{langtag-DgySESmk.cjs → langtag-a2-s9uyi.cjs} +0 -0
  110. /package/dist/tests/{link-CYszIegt.mjs → link-CHMtH2-k.mjs} +0 -0
  111. /package/dist/tests/{multibase-8zhYPa7o.cjs → multibase-BmWaK56H.cjs} +0 -0
  112. /package/dist/tests/{multibase-Ds75BvXs.mjs → multibase-D642Uv3S.mjs} +0 -0
  113. /package/dist/tests/{multicodec-ByS9XLsw.cjs → multicodec-B-_CSt1M.cjs} +0 -0
  114. /package/dist/tests/{multicodec-BBPZjRfg.mjs → multicodec-caWdG98t.mjs} +0 -0
@@ -1,4 +1,4 @@
1
- import { t as FetchError } from "./request-yCis9VCB.mjs";
1
+ import { t as FetchError } from "./request-BSg3IQzf.mjs";
2
2
  import { getLogger } from "@logtape/logtape";
3
3
  //#region src/body.ts
4
4
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -1,5 +1,5 @@
1
1
  require("./chunk-Do9eywBl.cjs");
2
- const require_request = require("./request-DGtx5MkG.cjs");
2
+ const require_request = require("./request-B3NUKbyk.cjs");
3
3
  let _logtape_logtape = require("@logtape/logtape");
4
4
  //#region src/body.ts
5
5
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -1,5 +1,5 @@
1
1
  require("./chunk-Do9eywBl.cjs");
2
- const require_body = require("./body-DhbRZ6Ps.cjs");
2
+ const require_body = require("./body-pOSG2RFb.cjs");
3
3
  let node_assert_strict = require("node:assert/strict");
4
4
  let node_test = require("node:test");
5
5
  let node_zlib = require("node:zlib");
@@ -1,4 +1,4 @@
1
- import { r as readBoundedText, t as BodyTooLargeError } from "./body-BOnyCa1y.mjs";
1
+ import { r as readBoundedText, t as BodyTooLargeError } from "./body-B9_hb-GO.mjs";
2
2
  import { deepStrictEqual, ok, rejects } from "node:assert/strict";
3
3
  import { test } from "node:test";
4
4
  import { gunzipSync, gzipSync } from "node:zlib";
@@ -1,10 +1,10 @@
1
1
  require("./chunk-Do9eywBl.cjs");
2
- require("./docloader-BhSiTajy.cjs");
3
- require("./url-vZTgkIZg.cjs");
4
- require("./key-BarKGLQz.cjs");
5
- require("./multibase-8zhYPa7o.cjs");
6
- require("./langtag-DgySESmk.cjs");
7
- require("./langstr-BsqFJupv.cjs");
2
+ require("./docloader-Ck3VfY29.cjs");
3
+ require("./url-DLuSj57U.cjs");
4
+ require("./key-L6ReMuUb.cjs");
5
+ require("./multibase-BmWaK56H.cjs");
6
+ require("./langtag-a2-s9uyi.cjs");
7
+ require("./langstr-CZpWmpo5.cjs");
8
8
  let node_test = require("node:test");
9
9
  let node_assert = require("node:assert");
10
10
  //#region src/decimal.ts
@@ -1,9 +1,9 @@
1
- import "./docloader-BjyFsnAA.mjs";
2
- import "./url-3fyJuqC0.mjs";
3
- import "./key-BDIsC6hV.mjs";
4
- import "./multibase-Ds75BvXs.mjs";
5
- import "./langtag-LDBqlxHv.mjs";
6
- import "./langstr-ABdAOKlF.mjs";
1
+ import "./docloader-Bdlp7FkK.mjs";
2
+ import "./url-DQWrMGhu.mjs";
3
+ import "./key-GEdpxA4M.mjs";
4
+ import "./multibase-D642Uv3S.mjs";
5
+ import "./langtag-D8FWxjv8.mjs";
6
+ import "./langstr-DTB4sl0a.mjs";
7
7
  import { test } from "node:test";
8
8
  import { deepStrictEqual, throws } from "node:assert";
9
9
  //#region src/decimal.ts
@@ -1,7 +1,8 @@
1
- import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-yCis9VCB.mjs";
2
- import { n as MAX_BODY_SIZE, r as readBoundedText } from "./body-BOnyCa1y.mjs";
3
- import { t as HttpHeaderLink } from "./link-CYszIegt.mjs";
4
- import { o as validatePublicUrl, t as UrlError } from "./url-3fyJuqC0.mjs";
1
+ import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-BSg3IQzf.mjs";
2
+ import { n as MAX_BODY_SIZE, r as readBoundedText } from "./body-B9_hb-GO.mjs";
3
+ import { t as HttpHeaderLink } from "./link-CHMtH2-k.mjs";
4
+ import { t as UrlError } from "./url-DQWrMGhu.mjs";
5
+ import { n as openDirectFetch, r as openPublicFetch } from "./public-fetch-Yi7Vks9s.mjs";
5
6
  import { getLogger } from "@logtape/logtape";
6
7
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
7
8
  //#region src/contexts/activitystreams.json
@@ -4591,13 +4592,47 @@ const DEFAULT_MAX_REDIRECTION = 20;
4591
4592
  * Gets a {@link RemoteDocument} from the given response.
4592
4593
  * @param url The URL of the document to load.
4593
4594
  * @param response The response to get the document from.
4594
- * @param fetch The function to fetch the document.
4595
+ * @param fetch The function to fetch the document. It is called with the URL
4596
+ * of an alternate document, if any, after the response body has
4597
+ * been either read or its cancellation has been started.
4595
4598
  * @returns The loaded remote document.
4596
4599
  * @throws {FetchError} If the response is not OK.
4597
4600
  * @internal
4598
4601
  */
4599
4602
  async function getRemoteDocument(url, response, fetch) {
4600
- const documentUrl = response.url === "" ? url : response.url;
4603
+ const result = await parseRemoteDocument(url, response);
4604
+ if ("alternate" in result) return await fetch(result.alternate);
4605
+ return result.document;
4606
+ }
4607
+ /**
4608
+ * Starts cancelling the body of a response without waiting for it to settle.
4609
+ * The cancellation of a branch of a tee (e.g., a body cloned by a fetch mock)
4610
+ * settles only after the other branch is cancelled too, per the Streams
4611
+ * standard, so awaiting it could block forever. Failures are only logged.
4612
+ * @param response The response whose body is cancelled.
4613
+ * @param url The URL of the response, for logging.
4614
+ */
4615
+ function cancelResponseBody(response, url) {
4616
+ response.body?.cancel().catch((error) => {
4617
+ logger.debug("Failed to cancel the response body from {url}: {error}", {
4618
+ url,
4619
+ error
4620
+ });
4621
+ });
4622
+ }
4623
+ /**
4624
+ * Gets a {@link RemoteDocument} from the given response, or the URL of an
4625
+ * alternate document to load instead. When an alternate document is found,
4626
+ * the response body has been either read or its cancellation has been started
4627
+ * (without waiting for it), so that the alternate document can be loaded after
4628
+ * the response's session is closed.
4629
+ * @param url The URL of the document to load.
4630
+ * @param response The response to get the document from.
4631
+ * @returns The loaded remote document, or the alternate document URL.
4632
+ * @throws {FetchError} If the response is not OK.
4633
+ */
4634
+ async function parseRemoteDocument(url, response) {
4635
+ const documentUrl = url;
4601
4636
  const docUrl = new URL(documentUrl);
4602
4637
  if (!response.ok) {
4603
4638
  logger.error("Failed to fetch document: {status} {url} {headers}", {
@@ -4634,7 +4669,8 @@ async function getRemoteDocument(url, response, fetch) {
4634
4669
  alternateUrl: altUri.href,
4635
4670
  url: documentUrl
4636
4671
  });
4637
- return await fetch(altUri.href);
4672
+ cancelResponseBody(response, documentUrl);
4673
+ return { alternate: altUri.href };
4638
4674
  }
4639
4675
  }
4640
4676
  }
@@ -4659,7 +4695,7 @@ async function getRemoteDocument(url, response, fetch) {
4659
4695
  alternateUrl: attribs.href,
4660
4696
  url: documentUrl
4661
4697
  });
4662
- return await fetch(new URL(attribs.href, docUrl).href);
4698
+ return { alternate: new URL(attribs.href, docUrl).href };
4663
4699
  }
4664
4700
  }
4665
4701
  document = JSON.parse(html);
@@ -4669,11 +4705,11 @@ async function getRemoteDocument(url, response, fetch) {
4669
4705
  url: documentUrl,
4670
4706
  headers: Object.fromEntries(response.headers.entries())
4671
4707
  });
4672
- return {
4708
+ return { document: {
4673
4709
  contextUrl,
4674
4710
  document,
4675
4711
  documentUrl
4676
- };
4712
+ } };
4677
4713
  }
4678
4714
  /**
4679
4715
  * Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
@@ -4699,9 +4735,54 @@ async function getRemoteDocument(url, response, fetch) {
4699
4735
  * @returns The document loader.
4700
4736
  * @since 1.3.0
4701
4737
  */
4702
- function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedContexts, userAgent } = {}) {
4738
+ function getDocumentLoader(options = {}) {
4739
+ return createDocumentLoader(options);
4740
+ }
4741
+ /**
4742
+ * The implementation of {@link getDocumentLoader}, with injectable
4743
+ * dependencies. This is not exported from any entry point.
4744
+ * @param options Options for the document loader.
4745
+ * @param deps The dependencies.
4746
+ * @returns The document loader.
4747
+ */
4748
+ function createDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedContexts, userAgent } = {}, deps = {}) {
4703
4749
  const tracer = trace.getTracerProvider().getTracer(name, version);
4704
4750
  const maximumRedirection = maxRedirection ?? DEFAULT_MAX_REDIRECTION;
4751
+ async function openHop(url, signal) {
4752
+ try {
4753
+ if (deps.openHop != null) return await deps.openHop(url);
4754
+ return allowPrivateAddress ? await openDirectFetch(url) : await openPublicFetch(url, { signal });
4755
+ } catch (error) {
4756
+ if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
4757
+ url,
4758
+ error
4759
+ });
4760
+ else logger.error("Disallowed private URL: {url}", {
4761
+ url,
4762
+ error
4763
+ });
4764
+ throw error;
4765
+ }
4766
+ }
4767
+ async function fetchHop(session, url, span, options) {
4768
+ try {
4769
+ const request = createActivityPubRequest(url, { userAgent });
4770
+ logRequest(logger, request);
4771
+ const response = await session.fetch(request, {
4772
+ redirect: "manual",
4773
+ signal: options?.signal
4774
+ });
4775
+ span.setAttribute("http.response.status_code", response.status);
4776
+ if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
4777
+ const location = response.headers.get("Location");
4778
+ cancelResponseBody(response, url);
4779
+ return { location };
4780
+ }
4781
+ return await parseRemoteDocument(url, response);
4782
+ } finally {
4783
+ await session.close();
4784
+ }
4785
+ }
4705
4786
  async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
4706
4787
  options?.signal?.throwIfAborted();
4707
4788
  const parsedUrl = new URL(url);
@@ -4716,33 +4797,15 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
4716
4797
  documentUrl: currentUrl
4717
4798
  };
4718
4799
  }
4719
- if (!allowPrivateAddress) try {
4720
- await validatePublicUrl(currentUrl);
4721
- } catch (error) {
4722
- if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
4723
- url: currentUrl,
4724
- error
4725
- });
4726
- else logger.error("Disallowed private URL: {url}", {
4727
- url: currentUrl,
4728
- error
4729
- });
4730
- throw error;
4731
- }
4800
+ const session = await openHop(currentUrl, options?.signal);
4732
4801
  visited.add(currentUrl);
4733
4802
  return await tracer.startActiveSpan("activitypub.fetch_document", {
4734
4803
  kind: SpanKind.CLIENT,
4735
4804
  attributes: { "url.full": currentUrl }
4736
4805
  }, async (span) => {
4737
4806
  try {
4738
- const request = createActivityPubRequest(currentUrl, { userAgent });
4739
- logRequest(logger, request);
4740
- const response = await fetch(request, {
4741
- redirect: "manual",
4742
- signal: options?.signal
4743
- });
4744
- span.setAttribute("http.response.status_code", response.status);
4745
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
4807
+ const outcome = await fetchHop(session, currentUrl, span, options);
4808
+ if ("location" in outcome) {
4746
4809
  if (redirected >= maximumRedirection) {
4747
4810
  logger.error("Too many redirections ({redirections}) while fetching document.", {
4748
4811
  redirections: redirected + 1,
@@ -4750,7 +4813,7 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
4750
4813
  });
4751
4814
  throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
4752
4815
  }
4753
- const redirectUrl = new URL(response.headers.get("Location"), response.url === "" ? currentUrl : response.url).href;
4816
+ const redirectUrl = new URL(outcome.location, currentUrl).href;
4754
4817
  span.setAttribute("http.redirect.url", redirectUrl);
4755
4818
  if (visited.has(redirectUrl)) {
4756
4819
  logger.error("Detected a redirect loop while fetching document: {url} -> {redirectUrl}", {
@@ -4761,12 +4824,14 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
4761
4824
  }
4762
4825
  return await load(redirectUrl, options, redirected + 1, visited);
4763
4826
  }
4764
- const result = await getRemoteDocument(currentUrl, response, async (alternateUrl) => {
4827
+ let result;
4828
+ if ("alternate" in outcome) {
4829
+ const alternateUrl = outcome.alternate;
4765
4830
  options?.signal?.throwIfAborted();
4766
4831
  if (redirected >= DEFAULT_MAX_REDIRECTION) throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
4767
4832
  if (visited.has(alternateUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${alternateUrl}`);
4768
- return await load(alternateUrl, options, redirected + 1, visited);
4769
- });
4833
+ result = await load(alternateUrl, options, redirected + 1, visited);
4834
+ } else result = outcome.document;
4770
4835
  span.setAttribute("docloader.document_url", result.documentUrl);
4771
4836
  if (result.contextUrl != null) span.setAttribute("docloader.context_url", result.contextUrl);
4772
4837
  return result;
@@ -4785,4 +4850,4 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
4785
4850
  return (url, options) => load(url, options);
4786
4851
  }
4787
4852
  //#endregion
4788
- export { cid_v1_default as i, getRemoteDocument as n, preloadedContexts as r, getDocumentLoader as t };
4853
+ export { cid_v1_default as a, preloadedContexts as i, getDocumentLoader as n, getRemoteDocument as r, createDocumentLoader as t };
@@ -1,8 +1,9 @@
1
1
  require("./chunk-Do9eywBl.cjs");
2
- const require_request = require("./request-DGtx5MkG.cjs");
3
- const require_body = require("./body-DhbRZ6Ps.cjs");
2
+ const require_request = require("./request-B3NUKbyk.cjs");
3
+ const require_body = require("./body-pOSG2RFb.cjs");
4
4
  const require_link = require("./link-qDTmjPw_.cjs");
5
- const require_url = require("./url-vZTgkIZg.cjs");
5
+ const require_url = require("./url-DLuSj57U.cjs");
6
+ const require_public_fetch = require("./public-fetch-Ddjfxbwc.cjs");
6
7
  let _logtape_logtape = require("@logtape/logtape");
7
8
  let _opentelemetry_api = require("@opentelemetry/api");
8
9
  //#region src/contexts/activitystreams.json
@@ -4592,13 +4593,47 @@ const DEFAULT_MAX_REDIRECTION = 20;
4592
4593
  * Gets a {@link RemoteDocument} from the given response.
4593
4594
  * @param url The URL of the document to load.
4594
4595
  * @param response The response to get the document from.
4595
- * @param fetch The function to fetch the document.
4596
+ * @param fetch The function to fetch the document. It is called with the URL
4597
+ * of an alternate document, if any, after the response body has
4598
+ * been either read or its cancellation has been started.
4596
4599
  * @returns The loaded remote document.
4597
4600
  * @throws {FetchError} If the response is not OK.
4598
4601
  * @internal
4599
4602
  */
4600
4603
  async function getRemoteDocument(url, response, fetch) {
4601
- const documentUrl = response.url === "" ? url : response.url;
4604
+ const result = await parseRemoteDocument(url, response);
4605
+ if ("alternate" in result) return await fetch(result.alternate);
4606
+ return result.document;
4607
+ }
4608
+ /**
4609
+ * Starts cancelling the body of a response without waiting for it to settle.
4610
+ * The cancellation of a branch of a tee (e.g., a body cloned by a fetch mock)
4611
+ * settles only after the other branch is cancelled too, per the Streams
4612
+ * standard, so awaiting it could block forever. Failures are only logged.
4613
+ * @param response The response whose body is cancelled.
4614
+ * @param url The URL of the response, for logging.
4615
+ */
4616
+ function cancelResponseBody(response, url) {
4617
+ response.body?.cancel().catch((error) => {
4618
+ logger.debug("Failed to cancel the response body from {url}: {error}", {
4619
+ url,
4620
+ error
4621
+ });
4622
+ });
4623
+ }
4624
+ /**
4625
+ * Gets a {@link RemoteDocument} from the given response, or the URL of an
4626
+ * alternate document to load instead. When an alternate document is found,
4627
+ * the response body has been either read or its cancellation has been started
4628
+ * (without waiting for it), so that the alternate document can be loaded after
4629
+ * the response's session is closed.
4630
+ * @param url The URL of the document to load.
4631
+ * @param response The response to get the document from.
4632
+ * @returns The loaded remote document, or the alternate document URL.
4633
+ * @throws {FetchError} If the response is not OK.
4634
+ */
4635
+ async function parseRemoteDocument(url, response) {
4636
+ const documentUrl = url;
4602
4637
  const docUrl = new URL(documentUrl);
4603
4638
  if (!response.ok) {
4604
4639
  logger.error("Failed to fetch document: {status} {url} {headers}", {
@@ -4635,7 +4670,8 @@ async function getRemoteDocument(url, response, fetch) {
4635
4670
  alternateUrl: altUri.href,
4636
4671
  url: documentUrl
4637
4672
  });
4638
- return await fetch(altUri.href);
4673
+ cancelResponseBody(response, documentUrl);
4674
+ return { alternate: altUri.href };
4639
4675
  }
4640
4676
  }
4641
4677
  }
@@ -4660,7 +4696,7 @@ async function getRemoteDocument(url, response, fetch) {
4660
4696
  alternateUrl: attribs.href,
4661
4697
  url: documentUrl
4662
4698
  });
4663
- return await fetch(new URL(attribs.href, docUrl).href);
4699
+ return { alternate: new URL(attribs.href, docUrl).href };
4664
4700
  }
4665
4701
  }
4666
4702
  document = JSON.parse(html);
@@ -4670,11 +4706,11 @@ async function getRemoteDocument(url, response, fetch) {
4670
4706
  url: documentUrl,
4671
4707
  headers: Object.fromEntries(response.headers.entries())
4672
4708
  });
4673
- return {
4709
+ return { document: {
4674
4710
  contextUrl,
4675
4711
  document,
4676
4712
  documentUrl
4677
- };
4713
+ } };
4678
4714
  }
4679
4715
  /**
4680
4716
  * Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
@@ -4700,9 +4736,54 @@ async function getRemoteDocument(url, response, fetch) {
4700
4736
  * @returns The document loader.
4701
4737
  * @since 1.3.0
4702
4738
  */
4703
- function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedContexts, userAgent } = {}) {
4739
+ function getDocumentLoader(options = {}) {
4740
+ return createDocumentLoader(options);
4741
+ }
4742
+ /**
4743
+ * The implementation of {@link getDocumentLoader}, with injectable
4744
+ * dependencies. This is not exported from any entry point.
4745
+ * @param options Options for the document loader.
4746
+ * @param deps The dependencies.
4747
+ * @returns The document loader.
4748
+ */
4749
+ function createDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedContexts, userAgent } = {}, deps = {}) {
4704
4750
  const tracer = _opentelemetry_api.trace.getTracerProvider().getTracer(require_request.name, require_request.version);
4705
4751
  const maximumRedirection = maxRedirection ?? DEFAULT_MAX_REDIRECTION;
4752
+ async function openHop(url, signal) {
4753
+ try {
4754
+ if (deps.openHop != null) return await deps.openHop(url);
4755
+ return allowPrivateAddress ? await require_public_fetch.openDirectFetch(url) : await require_public_fetch.openPublicFetch(url, { signal });
4756
+ } catch (error) {
4757
+ if (error instanceof require_url.UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
4758
+ url,
4759
+ error
4760
+ });
4761
+ else logger.error("Disallowed private URL: {url}", {
4762
+ url,
4763
+ error
4764
+ });
4765
+ throw error;
4766
+ }
4767
+ }
4768
+ async function fetchHop(session, url, span, options) {
4769
+ try {
4770
+ const request = require_request.createActivityPubRequest(url, { userAgent });
4771
+ require_request.logRequest(logger, request);
4772
+ const response = await session.fetch(request, {
4773
+ redirect: "manual",
4774
+ signal: options?.signal
4775
+ });
4776
+ span.setAttribute("http.response.status_code", response.status);
4777
+ if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
4778
+ const location = response.headers.get("Location");
4779
+ cancelResponseBody(response, url);
4780
+ return { location };
4781
+ }
4782
+ return await parseRemoteDocument(url, response);
4783
+ } finally {
4784
+ await session.close();
4785
+ }
4786
+ }
4706
4787
  async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
4707
4788
  options?.signal?.throwIfAborted();
4708
4789
  const parsedUrl = new URL(url);
@@ -4717,33 +4798,15 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
4717
4798
  documentUrl: currentUrl
4718
4799
  };
4719
4800
  }
4720
- if (!allowPrivateAddress) try {
4721
- await require_url.validatePublicUrl(currentUrl);
4722
- } catch (error) {
4723
- if (error instanceof require_url.UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
4724
- url: currentUrl,
4725
- error
4726
- });
4727
- else logger.error("Disallowed private URL: {url}", {
4728
- url: currentUrl,
4729
- error
4730
- });
4731
- throw error;
4732
- }
4801
+ const session = await openHop(currentUrl, options?.signal);
4733
4802
  visited.add(currentUrl);
4734
4803
  return await tracer.startActiveSpan("activitypub.fetch_document", {
4735
4804
  kind: _opentelemetry_api.SpanKind.CLIENT,
4736
4805
  attributes: { "url.full": currentUrl }
4737
4806
  }, async (span) => {
4738
4807
  try {
4739
- const request = require_request.createActivityPubRequest(currentUrl, { userAgent });
4740
- require_request.logRequest(logger, request);
4741
- const response = await fetch(request, {
4742
- redirect: "manual",
4743
- signal: options?.signal
4744
- });
4745
- span.setAttribute("http.response.status_code", response.status);
4746
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
4808
+ const outcome = await fetchHop(session, currentUrl, span, options);
4809
+ if ("location" in outcome) {
4747
4810
  if (redirected >= maximumRedirection) {
4748
4811
  logger.error("Too many redirections ({redirections}) while fetching document.", {
4749
4812
  redirections: redirected + 1,
@@ -4751,7 +4814,7 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
4751
4814
  });
4752
4815
  throw new require_request.FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
4753
4816
  }
4754
- const redirectUrl = new URL(response.headers.get("Location"), response.url === "" ? currentUrl : response.url).href;
4817
+ const redirectUrl = new URL(outcome.location, currentUrl).href;
4755
4818
  span.setAttribute("http.redirect.url", redirectUrl);
4756
4819
  if (visited.has(redirectUrl)) {
4757
4820
  logger.error("Detected a redirect loop while fetching document: {url} -> {redirectUrl}", {
@@ -4762,12 +4825,14 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
4762
4825
  }
4763
4826
  return await load(redirectUrl, options, redirected + 1, visited);
4764
4827
  }
4765
- const result = await getRemoteDocument(currentUrl, response, async (alternateUrl) => {
4828
+ let result;
4829
+ if ("alternate" in outcome) {
4830
+ const alternateUrl = outcome.alternate;
4766
4831
  options?.signal?.throwIfAborted();
4767
4832
  if (redirected >= DEFAULT_MAX_REDIRECTION) throw new require_request.FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
4768
4833
  if (visited.has(alternateUrl)) throw new require_request.FetchError(currentUrl, `Redirect loop detected: ${alternateUrl}`);
4769
- return await load(alternateUrl, options, redirected + 1, visited);
4770
- });
4834
+ result = await load(alternateUrl, options, redirected + 1, visited);
4835
+ } else result = outcome.document;
4771
4836
  span.setAttribute("docloader.document_url", result.documentUrl);
4772
4837
  if (result.contextUrl != null) span.setAttribute("docloader.context_url", result.contextUrl);
4773
4838
  return result;
@@ -4792,6 +4857,12 @@ Object.defineProperty(exports, "cid_v1_default", {
4792
4857
  return cid_v1_default;
4793
4858
  }
4794
4859
  });
4860
+ Object.defineProperty(exports, "createDocumentLoader", {
4861
+ enumerable: true,
4862
+ get: function() {
4863
+ return createDocumentLoader;
4864
+ }
4865
+ });
4795
4866
  Object.defineProperty(exports, "getDocumentLoader", {
4796
4867
  enumerable: true,
4797
4868
  get: function() {