@fedify/vocab-runtime 2.1.23 → 2.1.25

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 (56) hide show
  1. package/deno.json +1 -1
  2. package/dist/mod.cjs +220 -107
  3. package/dist/mod.d.cts +10 -0
  4. package/dist/mod.d.ts +10 -0
  5. package/dist/mod.js +220 -108
  6. package/dist/tests/body-D-gC5vdl.cjs +95 -0
  7. package/dist/tests/body-DBqFojnW.mjs +77 -0
  8. package/dist/tests/body.test.cjs +90 -0
  9. package/dist/tests/body.test.d.cts +1 -0
  10. package/dist/tests/body.test.d.mts +1 -0
  11. package/dist/tests/body.test.mjs +90 -0
  12. package/dist/tests/decimal.test.cjs +6 -6
  13. package/dist/tests/decimal.test.mjs +6 -6
  14. package/dist/tests/{docloader-D-CBVlrJ.mjs → docloader-Beoc8qTH.mjs} +66 -37
  15. package/dist/tests/{docloader-BcWmxXdo.cjs → docloader-fn4ICzFS.cjs} +71 -36
  16. package/dist/tests/docloader.test.cjs +321 -4
  17. package/dist/tests/docloader.test.mjs +320 -4
  18. package/dist/tests/internal/multicodec.test.cjs +2 -2
  19. package/dist/tests/internal/multicodec.test.mjs +2 -2
  20. package/dist/tests/{key-BeTHFQJK.mjs → key-BDIsC6hV.mjs} +2 -2
  21. package/dist/tests/{key-DTTIntwb.cjs → key-BarKGLQz.cjs} +2 -2
  22. package/dist/tests/key.test.cjs +3 -3
  23. package/dist/tests/key.test.mjs +3 -3
  24. package/dist/tests/langstr.test.cjs +2 -2
  25. package/dist/tests/langstr.test.mjs +2 -2
  26. package/dist/tests/link.test.cjs +2 -2
  27. package/dist/tests/link.test.mjs +2 -2
  28. package/dist/tests/multibase/multibase.test.cjs +2 -2
  29. package/dist/tests/multibase/multibase.test.mjs +2 -2
  30. package/dist/tests/{request-CdMLb0ba.cjs → request-DrO5xzev.cjs} +1 -1
  31. package/dist/tests/{request-DqsgdBUH.mjs → request-DxeGyJVh.mjs} +1 -1
  32. package/dist/tests/request.test.cjs +2 -2
  33. package/dist/tests/request.test.mjs +2 -2
  34. package/dist/tests/temporal.test.cjs +1 -1
  35. package/dist/tests/temporal.test.mjs +1 -1
  36. package/dist/tests/{url-o0e-aXQY.mjs → url-3fyJuqC0.mjs} +14 -4
  37. package/dist/tests/{url-CWU5HT3Y.cjs → url-vZTgkIZg.cjs} +15 -4
  38. package/dist/tests/url.test.cjs +37 -4
  39. package/dist/tests/url.test.mjs +37 -4
  40. package/package.json +1 -1
  41. package/src/body.test.ts +125 -0
  42. package/src/body.ts +95 -0
  43. package/src/contexts/fep-7aa9.json +24 -0
  44. package/src/contexts.ts +8 -0
  45. package/src/docloader.test.ts +431 -1
  46. package/src/docloader.ts +76 -49
  47. package/src/url.test.ts +29 -3
  48. package/src/url.ts +21 -5
  49. /package/dist/tests/{langstr-CbAxaeEZ.cjs → langstr-BOQHfJDr.cjs} +0 -0
  50. /package/dist/tests/{langstr-Di5AvKpB.mjs → langstr-DDLc4833.mjs} +0 -0
  51. /package/dist/tests/{link-NUUWCdnK.mjs → link-CYszIegt.mjs} +0 -0
  52. /package/dist/tests/{link-FguCydMA.cjs → link-qDTmjPw_.cjs} +0 -0
  53. /package/dist/tests/{multibase-F7LtMMsK.cjs → multibase-8zhYPa7o.cjs} +0 -0
  54. /package/dist/tests/{multibase-BgU9XRf7.mjs → multibase-Ds75BvXs.mjs} +0 -0
  55. /package/dist/tests/{multicodec-CyFp54fI.mjs → multicodec-BBPZjRfg.mjs} +0 -0
  56. /package/dist/tests/{multicodec-CxGVGa91.cjs → multicodec-ByS9XLsw.cjs} +0 -0
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.1.23",
3
+ "version": "2.1.25",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./src/mod.ts",
package/dist/mod.cjs CHANGED
@@ -6,6 +6,7 @@ let _opentelemetry_api = require("@opentelemetry/api");
6
6
  let node_process = require("node:process");
7
7
  node_process = require_chunk.__toESM(node_process);
8
8
  let node_dns_promises = require("node:dns/promises");
9
+ node_dns_promises = require_chunk.__toESM(node_dns_promises);
9
10
  let node_net = require("node:net");
10
11
  let asn1js = require("asn1js");
11
12
  let byte_encodings_base64 = require("byte-encodings/base64");
@@ -599,6 +600,30 @@ var fep_5711_default = { "@context": {
599
600
  }
600
601
  } };
601
602
  //#endregion
603
+ //#region src/contexts/fep-7aa9.json
604
+ var fep_7aa9_default = { "@context": {
605
+ "FeaturedCollection": "https://w3id.org/fep/7aa9#FeaturedCollection",
606
+ "FeaturedItem": "https://w3id.org/fep/7aa9#FeaturedItem",
607
+ "FeatureRequest": "https://w3id.org/fep/7aa9#FeatureRequest",
608
+ "FeatureAuthorization": "https://w3id.org/fep/7aa9#FeatureAuthorization",
609
+ "topic": {
610
+ "@id": "https://w3id.org/fep/7aa9#topic",
611
+ "@type": "@id"
612
+ },
613
+ "featuredObject": {
614
+ "@id": "https://w3id.org/fep/7aa9#featuredObject",
615
+ "@type": "@id"
616
+ },
617
+ "canFeature": {
618
+ "@id": "https://w3id.org/fep/7aa9#canFeature",
619
+ "@type": "@id"
620
+ },
621
+ "featureAuthorization": {
622
+ "@id": "https://w3id.org/fep/7aa9#featureAuthorization",
623
+ "@type": "@id"
624
+ }
625
+ } };
626
+ //#endregion
602
627
  //#region src/contexts/fep-ef61.json
603
628
  var fep_ef61_default = { "@context": {
604
629
  "gateways": {
@@ -4516,6 +4541,7 @@ const preloadedContexts = {
4516
4541
  "https://w3id.org/fep/5711": fep_5711_default,
4517
4542
  "https://www.w3.org/ns/cid/v1": cid_v1_default,
4518
4543
  "https://w3id.org/fep/ef61": fep_ef61_default,
4544
+ "https://w3id.org/fep/7aa9": fep_7aa9_default,
4519
4545
  "https://join-lemmy.org/context.json": join_lemmy_default,
4520
4546
  "http://joinmastodon.org/ns": joinmastodon_default,
4521
4547
  "https://purl.archive.org/miscellany": miscellany_default
@@ -4523,7 +4549,151 @@ const preloadedContexts = {
4523
4549
  //#endregion
4524
4550
  //#region deno.json
4525
4551
  var name = "@fedify/vocab-runtime";
4526
- var version = "2.1.23";
4552
+ var version = "2.1.25";
4553
+ //#endregion
4554
+ //#region src/request.ts
4555
+ /**
4556
+ * Error thrown when fetching a JSON-LD document failed.
4557
+ */
4558
+ var FetchError = class extends Error {
4559
+ /**
4560
+ * The URL that failed to fetch.
4561
+ */
4562
+ url;
4563
+ /**
4564
+ * The HTTP response that failed, if available.
4565
+ */
4566
+ response;
4567
+ /**
4568
+ * Constructs a new `FetchError`.
4569
+ *
4570
+ * @param url The URL that failed to fetch.
4571
+ * @param message Error message.
4572
+ * @param response The failed HTTP response, if available.
4573
+ */
4574
+ constructor(url, message, response) {
4575
+ super(message == null ? url.toString() : `${url}: ${message}`);
4576
+ this.name = "FetchError";
4577
+ this.url = typeof url === "string" ? new URL(url) : url;
4578
+ this.response = response;
4579
+ }
4580
+ };
4581
+ /**
4582
+ * Creates a request for the given URL.
4583
+ * @param url The URL to create the request for.
4584
+ * @param options The options for the request.
4585
+ * @returns The created request.
4586
+ * @internal
4587
+ */
4588
+ function createActivityPubRequest(url, options = {}) {
4589
+ return new Request(url, {
4590
+ headers: {
4591
+ Accept: "application/activity+json, application/ld+json",
4592
+ "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
4593
+ },
4594
+ redirect: "manual"
4595
+ });
4596
+ }
4597
+ /**
4598
+ * Gets the user agent string for the given application and URL.
4599
+ * @param options The options for making the user agent string.
4600
+ * @returns The user agent string.
4601
+ * @since 1.3.0
4602
+ */
4603
+ function getUserAgent({ software, url } = {}) {
4604
+ const fedify = `Fedify/${version}`;
4605
+ const runtime = globalThis.Deno?.version?.deno != null ? `Deno/${Deno.version.deno}` : globalThis.process?.versions?.bun != null ? `Bun/${node_process.default.versions.bun}` : "navigator" in globalThis && navigator.userAgent === "Cloudflare-Workers" ? navigator.userAgent : globalThis.process?.versions?.node != null ? `Node.js/${node_process.default.versions.node}` : null;
4606
+ const userAgent = software == null ? [fedify] : [software, fedify];
4607
+ if (runtime != null) userAgent.push(runtime);
4608
+ if (url != null) userAgent.push(`+${url.toString()}`);
4609
+ return `${userAgent.shift()} (${userAgent.join("; ")})`;
4610
+ }
4611
+ /**
4612
+ * Logs the request.
4613
+ * @param request The request to log.
4614
+ * @internal
4615
+ */
4616
+ function logRequest(logger, request) {
4617
+ logger.debug("Fetching document: {method} {url} {headers}", {
4618
+ method: request.method,
4619
+ url: request.url,
4620
+ headers: Object.fromEntries(request.headers.entries())
4621
+ });
4622
+ }
4623
+ //#endregion
4624
+ //#region src/body.ts
4625
+ /** Decoded JSON body limit: 16 MiB. @internal */
4626
+ const MAX_BODY_SIZE = 16 * 1024 * 1024;
4627
+ /** A body exceeded the byte limit. @internal */
4628
+ var BodyTooLargeError = class extends FetchError {
4629
+ /** Creates an error for a body exceeding the given limit. */
4630
+ constructor(url, maxBytes) {
4631
+ super(url, `Body exceeds the limit of ${maxBytes} bytes`);
4632
+ this.name = "BodyTooLargeError";
4633
+ }
4634
+ };
4635
+ /** Validates a finite, positive byte limit. @internal */
4636
+ function validateBodySizeLimit(maxBytes) {
4637
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
4638
+ }
4639
+ /**
4640
+ * Reads UTF-8 text while limiting bytes received from the body stream.
4641
+ * Fetch responses are already decompressed, so the stream count is authoritative.
4642
+ * @param message The response or request to read.
4643
+ * @param maxBytes The maximum number of decoded bytes to read.
4644
+ * @param url The URL to include in errors and logs.
4645
+ * @returns The decoded body text.
4646
+ * @throws {BodyTooLargeError} If the body exceeds the limit.
4647
+ * @internal
4648
+ */
4649
+ async function readBoundedText(message, maxBytes, url) {
4650
+ validateBodySizeLimit(maxBytes);
4651
+ const reader = message.body?.getReader();
4652
+ const tooLarge = () => {
4653
+ (0, _logtape_logtape.getLogger)([
4654
+ "fedify",
4655
+ "runtime",
4656
+ "body"
4657
+ ]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
4658
+ url: url.toString(),
4659
+ maxBytes
4660
+ });
4661
+ throw new BodyTooLargeError(url, maxBytes);
4662
+ };
4663
+ try {
4664
+ const length = message.headers.get("Content-Length");
4665
+ const encoding = message.headers.get("Content-Encoding");
4666
+ if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
4667
+ if (reader == null) return "";
4668
+ let size = 0;
4669
+ const textReader = new ReadableStream({ async pull(controller) {
4670
+ const { done, value } = await reader.read();
4671
+ if (done) {
4672
+ controller.close();
4673
+ return;
4674
+ }
4675
+ size += value.byteLength;
4676
+ if (size > maxBytes) tooLarge();
4677
+ controller.enqueue(value);
4678
+ } }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
4679
+ try {
4680
+ const chunks = [];
4681
+ while (true) {
4682
+ const { done, value } = await textReader.read();
4683
+ if (done) break;
4684
+ chunks.push(value);
4685
+ }
4686
+ return chunks.join("");
4687
+ } finally {
4688
+ textReader.releaseLock();
4689
+ }
4690
+ } catch (error) {
4691
+ if (reader != null) reader.cancel(error).catch(() => {});
4692
+ throw error;
4693
+ } finally {
4694
+ reader?.releaseLock();
4695
+ }
4696
+ }
4527
4697
  //#endregion
4528
4698
  //#region src/link.ts
4529
4699
  const parametersNeedLowerCase = ["rel", "type"];
@@ -4706,81 +4876,18 @@ var HttpHeaderLink = class HttpHeaderLink {
4706
4876
  }
4707
4877
  };
4708
4878
  //#endregion
4709
- //#region src/request.ts
4710
- /**
4711
- * Error thrown when fetching a JSON-LD document failed.
4712
- */
4713
- var FetchError = class extends Error {
4714
- /**
4715
- * The URL that failed to fetch.
4716
- */
4717
- url;
4718
- /**
4719
- * The HTTP response that failed, if available.
4720
- */
4721
- response;
4722
- /**
4723
- * Constructs a new `FetchError`.
4724
- *
4725
- * @param url The URL that failed to fetch.
4726
- * @param message Error message.
4727
- * @param response The failed HTTP response, if available.
4728
- */
4729
- constructor(url, message, response) {
4730
- super(message == null ? url.toString() : `${url}: ${message}`);
4731
- this.name = "FetchError";
4732
- this.url = typeof url === "string" ? new URL(url) : url;
4733
- this.response = response;
4734
- }
4735
- };
4736
- /**
4737
- * Creates a request for the given URL.
4738
- * @param url The URL to create the request for.
4739
- * @param options The options for the request.
4740
- * @returns The created request.
4741
- * @internal
4742
- */
4743
- function createActivityPubRequest(url, options = {}) {
4744
- return new Request(url, {
4745
- headers: {
4746
- Accept: "application/activity+json, application/ld+json",
4747
- "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
4748
- },
4749
- redirect: "manual"
4750
- });
4751
- }
4752
- /**
4753
- * Gets the user agent string for the given application and URL.
4754
- * @param options The options for making the user agent string.
4755
- * @returns The user agent string.
4756
- * @since 1.3.0
4757
- */
4758
- function getUserAgent({ software, url } = {}) {
4759
- const fedify = `Fedify/${version}`;
4760
- const runtime = globalThis.Deno?.version?.deno != null ? `Deno/${Deno.version.deno}` : globalThis.process?.versions?.bun != null ? `Bun/${node_process.default.versions.bun}` : "navigator" in globalThis && navigator.userAgent === "Cloudflare-Workers" ? navigator.userAgent : globalThis.process?.versions?.node != null ? `Node.js/${node_process.default.versions.node}` : null;
4761
- const userAgent = software == null ? [fedify] : [software, fedify];
4762
- if (runtime != null) userAgent.push(runtime);
4763
- if (url != null) userAgent.push(`+${url.toString()}`);
4764
- return `${userAgent.shift()} (${userAgent.join("; ")})`;
4765
- }
4766
- /**
4767
- * Logs the request.
4768
- * @param request The request to log.
4769
- * @internal
4770
- */
4771
- function logRequest(logger, request) {
4772
- logger.debug("Fetching document: {method} {url} {headers}", {
4773
- method: request.method,
4774
- url: request.url,
4775
- headers: Object.fromEntries(request.headers.entries())
4776
- });
4777
- }
4778
- //#endregion
4779
4879
  //#region src/url.ts
4780
4880
  var UrlError = class extends Error {
4881
+ /**
4882
+ * The failure category. Defaults to `"disallowed"`.
4883
+ * `"dns"` includes lookup errors and results with no usable IP addresses.
4884
+ * @since 2.0.29
4885
+ */
4886
+ reason;
4781
4887
  constructor(message, options) {
4782
4888
  super(message, options);
4783
4889
  this.name = "UrlError";
4890
+ this.reason = options?.reason ?? "disallowed";
4784
4891
  }
4785
4892
  };
4786
4893
  /**
@@ -4806,9 +4913,12 @@ async function validatePublicUrl(url) {
4806
4913
  }
4807
4914
  let addresses;
4808
4915
  try {
4809
- addresses = await (0, node_dns_promises.lookup)(hostname, { all: true });
4916
+ addresses = await node_dns_promises.default.lookup(hostname, { all: true });
4810
4917
  } catch (error) {
4811
- throw new UrlError("DNS lookup failed", { cause: error });
4918
+ throw new UrlError("DNS lookup failed", {
4919
+ cause: error,
4920
+ reason: "dns"
4921
+ });
4812
4922
  }
4813
4923
  validateLookupAddresses(addresses);
4814
4924
  }
@@ -4837,7 +4947,7 @@ function validateLookupAddresses(addresses) {
4837
4947
  ipAddressCount++;
4838
4948
  validatePublicIpAddress(address, family);
4839
4949
  }
4840
- if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
4950
+ if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address", { reason: "dns" });
4841
4951
  }
4842
4952
  function validatePublicIpAddress(address, family) {
4843
4953
  if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
@@ -5037,38 +5147,29 @@ async function getRemoteDocument(url, response, fetch) {
5037
5147
  }
5038
5148
  let document;
5039
5149
  if (!jsonLd && (contentType === "text/html" || contentType?.startsWith("text/html;") || contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;"))) {
5040
- const MAX_HTML_SIZE = 1024 * 1024;
5041
- const html = await response.text();
5042
- if (html.length > MAX_HTML_SIZE) {
5043
- logger.warn("HTML response too large, skipping alternate link discovery: {url}", {
5044
- url: documentUrl,
5045
- size: html.length
5046
- });
5047
- document = JSON.parse(html);
5048
- } else {
5049
- const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
5050
- const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
5051
- let tagMatch;
5052
- while ((tagMatch = tagPattern.exec(html)) !== null) {
5053
- const tagContent = tagMatch[2];
5054
- let attrMatch;
5055
- const attribs = {};
5056
- attrPattern.lastIndex = 0;
5057
- while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
5058
- const key = attrMatch[1].toLowerCase();
5059
- attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
5060
- }
5061
- if (attribs.rel === "alternate" && "type" in attribs && (attribs.type === "application/activity+json" || attribs.type === "application/ld+json" || attribs.type.startsWith("application/ld+json;")) && "href" in attribs && new URL(attribs.href, docUrl).href !== docUrl.href) {
5062
- logger.debug("Found alternate document: {alternateUrl} from {url}", {
5063
- alternateUrl: attribs.href,
5064
- url: documentUrl
5065
- });
5066
- return await fetch(new URL(attribs.href, docUrl).href);
5067
- }
5150
+ const html = await readBoundedText(response, 1024 * 1024, documentUrl);
5151
+ const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
5152
+ const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
5153
+ let tagMatch;
5154
+ while ((tagMatch = tagPattern.exec(html)) !== null) {
5155
+ const tagContent = tagMatch[2];
5156
+ let attrMatch;
5157
+ const attribs = {};
5158
+ attrPattern.lastIndex = 0;
5159
+ while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
5160
+ const key = attrMatch[1].toLowerCase();
5161
+ attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
5162
+ }
5163
+ if (attribs.rel === "alternate" && "type" in attribs && (attribs.type === "application/activity+json" || attribs.type === "application/ld+json" || attribs.type.startsWith("application/ld+json;")) && "href" in attribs && new URL(attribs.href, docUrl).href !== docUrl.href) {
5164
+ logger.debug("Found alternate document: {alternateUrl} from {url}", {
5165
+ alternateUrl: attribs.href,
5166
+ url: documentUrl
5167
+ });
5168
+ return await fetch(new URL(attribs.href, docUrl).href);
5068
5169
  }
5069
- document = JSON.parse(html);
5070
5170
  }
5071
- } else document = await response.json();
5171
+ document = JSON.parse(html);
5172
+ } else document = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, documentUrl));
5072
5173
  logger.debug("Fetched document: {status} {url} {headers}", {
5073
5174
  status: response.status,
5074
5175
  url: documentUrl,
@@ -5082,6 +5183,8 @@ async function getRemoteDocument(url, response, fetch) {
5082
5183
  }
5083
5184
  /**
5084
5185
  * Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
5186
+ * At most 20 HTTP redirects and alternate document links are followed in total
5187
+ * per call. Revisiting a URL within that chain throws a {@link FetchError}.
5085
5188
  *
5086
5189
  * The created loader preloads the below frequently used contexts by default
5087
5190
  * (unless `options.skipPreloadedContexts` is set to `true`):
@@ -5093,6 +5196,7 @@ async function getRemoteDocument(url, response, fetch) {
5093
5196
  * - <https://www.w3.org/ns/cid/v1>
5094
5197
  * - <https://w3id.org/security/multikey/v1>
5095
5198
  * - <https://w3id.org/fep/ef61>
5199
+ * - <https://w3id.org/fep/7aa9>
5096
5200
  * - <https://purl.archive.org/socialweb/webfinger>
5097
5201
  * - <http://schema.org/>
5098
5202
  * @param options Options for the document loader.
@@ -5115,7 +5219,11 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
5115
5219
  if (!allowPrivateAddress) try {
5116
5220
  await validatePublicUrl(currentUrl);
5117
5221
  } catch (error) {
5118
- if (error instanceof UrlError) logger.error("Disallowed private URL: {url}", {
5222
+ if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
5223
+ url: currentUrl,
5224
+ error
5225
+ });
5226
+ else logger.error("Disallowed private URL: {url}", {
5119
5227
  url: currentUrl,
5120
5228
  error
5121
5229
  });
@@ -5153,7 +5261,12 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
5153
5261
  }
5154
5262
  return await load(redirectUrl, options, redirected + 1, visited);
5155
5263
  }
5156
- const result = await getRemoteDocument(currentUrl, response, load);
5264
+ const result = await getRemoteDocument(currentUrl, response, async (alternateUrl) => {
5265
+ options?.signal?.throwIfAborted();
5266
+ if (redirected >= DEFAULT_MAX_REDIRECTION) throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
5267
+ if (visited.has(alternateUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${alternateUrl}`);
5268
+ return await load(alternateUrl, options, redirected + 1, visited);
5269
+ });
5157
5270
  span.setAttribute("docloader.document_url", result.documentUrl);
5158
5271
  if (result.contextUrl != null) span.setAttribute("docloader.context_url", result.contextUrl);
5159
5272
  return result;
@@ -5169,7 +5282,7 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
5169
5282
  }
5170
5283
  });
5171
5284
  }
5172
- return load;
5285
+ return (url, options) => load(url, options);
5173
5286
  }
5174
5287
  //#endregion
5175
5288
  //#region src/internal/multicodec.ts
package/dist/mod.d.cts CHANGED
@@ -174,6 +174,8 @@ interface GetDocumentLoaderOptions extends DocumentLoaderFactoryOptions {
174
174
  }
175
175
  /**
176
176
  * Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
177
+ * At most 20 HTTP redirects and alternate document links are followed in total
178
+ * per call. Revisiting a URL within that chain throws a {@link FetchError}.
177
179
  *
178
180
  * The created loader preloads the below frequently used contexts by default
179
181
  * (unless `options.skipPreloadedContexts` is set to `true`):
@@ -185,6 +187,7 @@ interface GetDocumentLoaderOptions extends DocumentLoaderFactoryOptions {
185
187
  * - <https://www.w3.org/ns/cid/v1>
186
188
  * - <https://w3id.org/security/multikey/v1>
187
189
  * - <https://w3id.org/fep/ef61>
190
+ * - <https://w3id.org/fep/7aa9>
188
191
  * - <https://purl.archive.org/socialweb/webfinger>
189
192
  * - <http://schema.org/>
190
193
  * @param options Options for the document loader.
@@ -419,8 +422,15 @@ declare function encodingFromBaseData(data: string | Uint8Array): Base;
419
422
  //#endregion
420
423
  //#region src/url.d.ts
421
424
  declare class UrlError extends Error {
425
+ /**
426
+ * The failure category. Defaults to `"disallowed"`.
427
+ * `"dns"` includes lookup errors and results with no usable IP addresses.
428
+ * @since 2.0.29
429
+ */
430
+ readonly reason: "dns" | "disallowed";
422
431
  constructor(message: string, options?: {
423
432
  cause?: unknown;
433
+ reason?: "dns" | "disallowed";
424
434
  });
425
435
  }
426
436
  /**
package/dist/mod.d.ts CHANGED
@@ -173,6 +173,8 @@ interface GetDocumentLoaderOptions extends DocumentLoaderFactoryOptions {
173
173
  }
174
174
  /**
175
175
  * Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
176
+ * At most 20 HTTP redirects and alternate document links are followed in total
177
+ * per call. Revisiting a URL within that chain throws a {@link FetchError}.
176
178
  *
177
179
  * The created loader preloads the below frequently used contexts by default
178
180
  * (unless `options.skipPreloadedContexts` is set to `true`):
@@ -184,6 +186,7 @@ interface GetDocumentLoaderOptions extends DocumentLoaderFactoryOptions {
184
186
  * - <https://www.w3.org/ns/cid/v1>
185
187
  * - <https://w3id.org/security/multikey/v1>
186
188
  * - <https://w3id.org/fep/ef61>
189
+ * - <https://w3id.org/fep/7aa9>
187
190
  * - <https://purl.archive.org/socialweb/webfinger>
188
191
  * - <http://schema.org/>
189
192
  * @param options Options for the document loader.
@@ -418,8 +421,15 @@ declare function encodingFromBaseData(data: string | Uint8Array): Base;
418
421
  //#endregion
419
422
  //#region src/url.d.ts
420
423
  declare class UrlError extends Error {
424
+ /**
425
+ * The failure category. Defaults to `"disallowed"`.
426
+ * `"dns"` includes lookup errors and results with no usable IP addresses.
427
+ * @since 2.0.29
428
+ */
429
+ readonly reason: "dns" | "disallowed";
421
430
  constructor(message: string, options?: {
422
431
  cause?: unknown;
432
+ reason?: "dns" | "disallowed";
423
433
  });
424
434
  }
425
435
  /**