@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
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,90 @@
1
+ import { r as readBoundedText, t as BodyTooLargeError } from "./body-DBqFojnW.mjs";
2
+ import { deepStrictEqual, ok, rejects } from "node:assert/strict";
3
+ import { test } from "node:test";
4
+ import { gunzipSync, gzipSync } from "node:zlib";
5
+ //#region src/body.test.ts
6
+ const url = "https://example.com/document";
7
+ test("readBoundedText handles empty bodies and flushes incomplete UTF-8", async () => {
8
+ deepStrictEqual(await readBoundedText(new Response(null), 1, url), "");
9
+ deepStrictEqual(await readBoundedText(new Response(""), 1, url), "");
10
+ deepStrictEqual(await readBoundedText(new Response(new Uint8Array([226, 130])), 2, url), "�");
11
+ });
12
+ test("readBoundedText counts bytes and preserves split UTF-8", async () => {
13
+ const bytes = new TextEncoder().encode("\"안녕\"");
14
+ const stream = new ReadableStream({ start(controller) {
15
+ for (const byte of bytes) controller.enqueue(new Uint8Array([byte]));
16
+ controller.close();
17
+ } });
18
+ deepStrictEqual(await readBoundedText(new Response(stream), bytes.length, url), "\"안녕\"");
19
+ await rejects(readBoundedText(new Response(bytes), bytes.length - 1, url), BodyTooLargeError);
20
+ });
21
+ test("readBoundedText rejects a declared oversized body without pulling", async () => {
22
+ let pulled = false;
23
+ let canceled = false;
24
+ const response = new Response(new ReadableStream({
25
+ pull() {
26
+ pulled = true;
27
+ },
28
+ cancel() {
29
+ canceled = true;
30
+ }
31
+ }, { highWaterMark: 0 }), { headers: { "Content-Length": "100" } });
32
+ await rejects(readBoundedText(response, 10, url), BodyTooLargeError);
33
+ deepStrictEqual(pulled, false);
34
+ deepStrictEqual(canceled, true);
35
+ deepStrictEqual(response.body?.locked, false);
36
+ });
37
+ test("readBoundedText cancels chunked bodies despite absent or false lengths", async () => {
38
+ for (const length of [
39
+ void 0,
40
+ "1",
41
+ "invalid"
42
+ ]) {
43
+ let pulls = 0;
44
+ let canceled = false;
45
+ const stream = new ReadableStream({
46
+ pull(controller) {
47
+ pulls++;
48
+ controller.enqueue(new Uint8Array(4));
49
+ },
50
+ cancel() {
51
+ canceled = true;
52
+ }
53
+ }, { highWaterMark: 0 });
54
+ const response = new Response(stream, { headers: length == null ? {} : { "Content-Length": length } });
55
+ await rejects(readBoundedText(response, 8, url), BodyTooLargeError);
56
+ deepStrictEqual(pulls, 3);
57
+ deepStrictEqual(canceled, true);
58
+ deepStrictEqual(response.body?.locked, false);
59
+ }
60
+ });
61
+ test("readBoundedText limits decoded gzip bytes, not the compressed length", async () => {
62
+ const compressed = gzipSync("\"" + "a".repeat(4096) + "\"");
63
+ ok(compressed.length < 128);
64
+ const decoded = gunzipSync(compressed);
65
+ await rejects(readBoundedText(new Response(decoded, { headers: {
66
+ "Content-Encoding": "gzip",
67
+ "Content-Length": String(compressed.length)
68
+ } }), 128, url), BodyTooLargeError);
69
+ deepStrictEqual(await readBoundedText(new Response("{}", { headers: {
70
+ "Content-Encoding": "gzip",
71
+ "Content-Length": "22"
72
+ } }), 2, url), "{}");
73
+ });
74
+ test("readBoundedText rejects invalid limits and releases failed streams", async () => {
75
+ for (const limit of [
76
+ 0,
77
+ -1,
78
+ NaN,
79
+ Infinity,
80
+ 1.5
81
+ ]) await rejects(readBoundedText(new Response("{}"), limit, url), RangeError);
82
+ const failure = /* @__PURE__ */ new Error("broken stream");
83
+ const response = new Response(new ReadableStream({ pull(controller) {
84
+ controller.error(failure);
85
+ } }));
86
+ await rejects(readBoundedText(response, 10, url), (error) => error === failure);
87
+ deepStrictEqual(response.body?.locked, false);
88
+ });
89
+ //#endregion
90
+ export {};
@@ -1,11 +1,11 @@
1
1
  require("./chunk-Do9eywBl.cjs");
2
- require("./docloader-BcWmxXdo.cjs");
3
- require("./url-CWU5HT3Y.cjs");
4
- require("./key-DTTIntwb.cjs");
5
- require("./multibase-F7LtMMsK.cjs");
6
- require("./langstr-CbAxaeEZ.cjs");
7
- let node_assert = require("node:assert");
2
+ require("./docloader-fn4ICzFS.cjs");
3
+ require("./url-vZTgkIZg.cjs");
4
+ require("./key-BarKGLQz.cjs");
5
+ require("./multibase-8zhYPa7o.cjs");
6
+ require("./langstr-BOQHfJDr.cjs");
8
7
  let node_test = require("node:test");
8
+ let node_assert = require("node:assert");
9
9
  //#region src/decimal.ts
10
10
  const DECIMAL_PATTERN = /^(\+|-)?([0-9]+(\.[0-9]*)?|\.[0-9]+)$/;
11
11
  const XML_SCHEMA_WHITESPACE_PATTERN = /[\t\n\r ]+/g;
@@ -1,10 +1,10 @@
1
- import "./docloader-D-CBVlrJ.mjs";
2
- import "./url-o0e-aXQY.mjs";
3
- import "./key-BeTHFQJK.mjs";
4
- import "./multibase-BgU9XRf7.mjs";
5
- import "./langstr-Di5AvKpB.mjs";
6
- import { deepStrictEqual, throws } from "node:assert";
1
+ import "./docloader-Beoc8qTH.mjs";
2
+ import "./url-3fyJuqC0.mjs";
3
+ import "./key-BDIsC6hV.mjs";
4
+ import "./multibase-Ds75BvXs.mjs";
5
+ import "./langstr-DDLc4833.mjs";
7
6
  import { test } from "node:test";
7
+ import { deepStrictEqual, throws } from "node:assert";
8
8
  //#region src/decimal.ts
9
9
  const DECIMAL_PATTERN = /^(\+|-)?([0-9]+(\.[0-9]*)?|\.[0-9]+)$/;
10
10
  const XML_SCHEMA_WHITESPACE_PATTERN = /[\t\n\r ]+/g;
@@ -1,6 +1,7 @@
1
- import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-DqsgdBUH.mjs";
2
- import { t as HttpHeaderLink } from "./link-NUUWCdnK.mjs";
3
- import { o as validatePublicUrl, t as UrlError } from "./url-o0e-aXQY.mjs";
1
+ import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-DxeGyJVh.mjs";
2
+ import { n as MAX_BODY_SIZE, r as readBoundedText } from "./body-DBqFojnW.mjs";
3
+ import { t as HttpHeaderLink } from "./link-CYszIegt.mjs";
4
+ import { o as validatePublicUrl, t as UrlError } from "./url-3fyJuqC0.mjs";
4
5
  import { getLogger } from "@logtape/logtape";
5
6
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
6
7
  //#region src/contexts/activitystreams.json
@@ -588,6 +589,30 @@ var fep_5711_default = { "@context": {
588
589
  }
589
590
  } };
590
591
  //#endregion
592
+ //#region src/contexts/fep-7aa9.json
593
+ var fep_7aa9_default = { "@context": {
594
+ "FeaturedCollection": "https://w3id.org/fep/7aa9#FeaturedCollection",
595
+ "FeaturedItem": "https://w3id.org/fep/7aa9#FeaturedItem",
596
+ "FeatureRequest": "https://w3id.org/fep/7aa9#FeatureRequest",
597
+ "FeatureAuthorization": "https://w3id.org/fep/7aa9#FeatureAuthorization",
598
+ "topic": {
599
+ "@id": "https://w3id.org/fep/7aa9#topic",
600
+ "@type": "@id"
601
+ },
602
+ "featuredObject": {
603
+ "@id": "https://w3id.org/fep/7aa9#featuredObject",
604
+ "@type": "@id"
605
+ },
606
+ "canFeature": {
607
+ "@id": "https://w3id.org/fep/7aa9#canFeature",
608
+ "@type": "@id"
609
+ },
610
+ "featureAuthorization": {
611
+ "@id": "https://w3id.org/fep/7aa9#featureAuthorization",
612
+ "@type": "@id"
613
+ }
614
+ } };
615
+ //#endregion
591
616
  //#region src/contexts/fep-ef61.json
592
617
  var fep_ef61_default = { "@context": {
593
618
  "gateways": {
@@ -4505,6 +4530,7 @@ const preloadedContexts = {
4505
4530
  "https://w3id.org/fep/5711": fep_5711_default,
4506
4531
  "https://www.w3.org/ns/cid/v1": cid_v1_default,
4507
4532
  "https://w3id.org/fep/ef61": fep_ef61_default,
4533
+ "https://w3id.org/fep/7aa9": fep_7aa9_default,
4508
4534
  "https://join-lemmy.org/context.json": join_lemmy_default,
4509
4535
  "http://joinmastodon.org/ns": joinmastodon_default,
4510
4536
  "https://purl.archive.org/miscellany": miscellany_default
@@ -4571,38 +4597,29 @@ async function getRemoteDocument(url, response, fetch) {
4571
4597
  }
4572
4598
  let document;
4573
4599
  if (!jsonLd && (contentType === "text/html" || contentType?.startsWith("text/html;") || contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;"))) {
4574
- const MAX_HTML_SIZE = 1024 * 1024;
4575
- const html = await response.text();
4576
- if (html.length > MAX_HTML_SIZE) {
4577
- logger.warn("HTML response too large, skipping alternate link discovery: {url}", {
4578
- url: documentUrl,
4579
- size: html.length
4580
- });
4581
- document = JSON.parse(html);
4582
- } else {
4583
- const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
4584
- const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
4585
- let tagMatch;
4586
- while ((tagMatch = tagPattern.exec(html)) !== null) {
4587
- const tagContent = tagMatch[2];
4588
- let attrMatch;
4589
- const attribs = {};
4590
- attrPattern.lastIndex = 0;
4591
- while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
4592
- const key = attrMatch[1].toLowerCase();
4593
- attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
4594
- }
4595
- 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) {
4596
- logger.debug("Found alternate document: {alternateUrl} from {url}", {
4597
- alternateUrl: attribs.href,
4598
- url: documentUrl
4599
- });
4600
- return await fetch(new URL(attribs.href, docUrl).href);
4601
- }
4600
+ const html = await readBoundedText(response, 1024 * 1024, documentUrl);
4601
+ const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
4602
+ const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
4603
+ let tagMatch;
4604
+ while ((tagMatch = tagPattern.exec(html)) !== null) {
4605
+ const tagContent = tagMatch[2];
4606
+ let attrMatch;
4607
+ const attribs = {};
4608
+ attrPattern.lastIndex = 0;
4609
+ while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
4610
+ const key = attrMatch[1].toLowerCase();
4611
+ attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
4612
+ }
4613
+ 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) {
4614
+ logger.debug("Found alternate document: {alternateUrl} from {url}", {
4615
+ alternateUrl: attribs.href,
4616
+ url: documentUrl
4617
+ });
4618
+ return await fetch(new URL(attribs.href, docUrl).href);
4602
4619
  }
4603
- document = JSON.parse(html);
4604
4620
  }
4605
- } else document = await response.json();
4621
+ document = JSON.parse(html);
4622
+ } else document = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, documentUrl));
4606
4623
  logger.debug("Fetched document: {status} {url} {headers}", {
4607
4624
  status: response.status,
4608
4625
  url: documentUrl,
@@ -4616,6 +4633,8 @@ async function getRemoteDocument(url, response, fetch) {
4616
4633
  }
4617
4634
  /**
4618
4635
  * Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
4636
+ * At most 20 HTTP redirects and alternate document links are followed in total
4637
+ * per call. Revisiting a URL within that chain throws a {@link FetchError}.
4619
4638
  *
4620
4639
  * The created loader preloads the below frequently used contexts by default
4621
4640
  * (unless `options.skipPreloadedContexts` is set to `true`):
@@ -4627,6 +4646,7 @@ async function getRemoteDocument(url, response, fetch) {
4627
4646
  * - <https://www.w3.org/ns/cid/v1>
4628
4647
  * - <https://w3id.org/security/multikey/v1>
4629
4648
  * - <https://w3id.org/fep/ef61>
4649
+ * - <https://w3id.org/fep/7aa9>
4630
4650
  * - <https://purl.archive.org/socialweb/webfinger>
4631
4651
  * - <http://schema.org/>
4632
4652
  * @param options Options for the document loader.
@@ -4649,7 +4669,11 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
4649
4669
  if (!allowPrivateAddress) try {
4650
4670
  await validatePublicUrl(currentUrl);
4651
4671
  } catch (error) {
4652
- if (error instanceof UrlError) logger.error("Disallowed private URL: {url}", {
4672
+ if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
4673
+ url: currentUrl,
4674
+ error
4675
+ });
4676
+ else logger.error("Disallowed private URL: {url}", {
4653
4677
  url: currentUrl,
4654
4678
  error
4655
4679
  });
@@ -4687,7 +4711,12 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
4687
4711
  }
4688
4712
  return await load(redirectUrl, options, redirected + 1, visited);
4689
4713
  }
4690
- const result = await getRemoteDocument(currentUrl, response, load);
4714
+ const result = await getRemoteDocument(currentUrl, response, async (alternateUrl) => {
4715
+ options?.signal?.throwIfAborted();
4716
+ if (redirected >= DEFAULT_MAX_REDIRECTION) throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
4717
+ if (visited.has(alternateUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${alternateUrl}`);
4718
+ return await load(alternateUrl, options, redirected + 1, visited);
4719
+ });
4691
4720
  span.setAttribute("docloader.document_url", result.documentUrl);
4692
4721
  if (result.contextUrl != null) span.setAttribute("docloader.context_url", result.contextUrl);
4693
4722
  return result;
@@ -4703,7 +4732,7 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
4703
4732
  }
4704
4733
  });
4705
4734
  }
4706
- return load;
4735
+ return (url, options) => load(url, options);
4707
4736
  }
4708
4737
  //#endregion
4709
- export { preloadedContexts as n, cid_v1_default as r, getDocumentLoader as t };
4738
+ export { cid_v1_default as i, getRemoteDocument as n, preloadedContexts as r, getDocumentLoader as t };
@@ -1,7 +1,8 @@
1
1
  require("./chunk-Do9eywBl.cjs");
2
- const require_request = require("./request-CdMLb0ba.cjs");
3
- const require_link = require("./link-FguCydMA.cjs");
4
- const require_url = require("./url-CWU5HT3Y.cjs");
2
+ const require_request = require("./request-DrO5xzev.cjs");
3
+ const require_body = require("./body-D-gC5vdl.cjs");
4
+ const require_link = require("./link-qDTmjPw_.cjs");
5
+ const require_url = require("./url-vZTgkIZg.cjs");
5
6
  let _logtape_logtape = require("@logtape/logtape");
6
7
  let _opentelemetry_api = require("@opentelemetry/api");
7
8
  //#region src/contexts/activitystreams.json
@@ -589,6 +590,30 @@ var fep_5711_default = { "@context": {
589
590
  }
590
591
  } };
591
592
  //#endregion
593
+ //#region src/contexts/fep-7aa9.json
594
+ var fep_7aa9_default = { "@context": {
595
+ "FeaturedCollection": "https://w3id.org/fep/7aa9#FeaturedCollection",
596
+ "FeaturedItem": "https://w3id.org/fep/7aa9#FeaturedItem",
597
+ "FeatureRequest": "https://w3id.org/fep/7aa9#FeatureRequest",
598
+ "FeatureAuthorization": "https://w3id.org/fep/7aa9#FeatureAuthorization",
599
+ "topic": {
600
+ "@id": "https://w3id.org/fep/7aa9#topic",
601
+ "@type": "@id"
602
+ },
603
+ "featuredObject": {
604
+ "@id": "https://w3id.org/fep/7aa9#featuredObject",
605
+ "@type": "@id"
606
+ },
607
+ "canFeature": {
608
+ "@id": "https://w3id.org/fep/7aa9#canFeature",
609
+ "@type": "@id"
610
+ },
611
+ "featureAuthorization": {
612
+ "@id": "https://w3id.org/fep/7aa9#featureAuthorization",
613
+ "@type": "@id"
614
+ }
615
+ } };
616
+ //#endregion
592
617
  //#region src/contexts/fep-ef61.json
593
618
  var fep_ef61_default = { "@context": {
594
619
  "gateways": {
@@ -4506,6 +4531,7 @@ const preloadedContexts = {
4506
4531
  "https://w3id.org/fep/5711": fep_5711_default,
4507
4532
  "https://www.w3.org/ns/cid/v1": cid_v1_default,
4508
4533
  "https://w3id.org/fep/ef61": fep_ef61_default,
4534
+ "https://w3id.org/fep/7aa9": fep_7aa9_default,
4509
4535
  "https://join-lemmy.org/context.json": join_lemmy_default,
4510
4536
  "http://joinmastodon.org/ns": joinmastodon_default,
4511
4537
  "https://purl.archive.org/miscellany": miscellany_default
@@ -4572,38 +4598,29 @@ async function getRemoteDocument(url, response, fetch) {
4572
4598
  }
4573
4599
  let document;
4574
4600
  if (!jsonLd && (contentType === "text/html" || contentType?.startsWith("text/html;") || contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;"))) {
4575
- const MAX_HTML_SIZE = 1024 * 1024;
4576
- const html = await response.text();
4577
- if (html.length > MAX_HTML_SIZE) {
4578
- logger.warn("HTML response too large, skipping alternate link discovery: {url}", {
4579
- url: documentUrl,
4580
- size: html.length
4581
- });
4582
- document = JSON.parse(html);
4583
- } else {
4584
- const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
4585
- const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
4586
- let tagMatch;
4587
- while ((tagMatch = tagPattern.exec(html)) !== null) {
4588
- const tagContent = tagMatch[2];
4589
- let attrMatch;
4590
- const attribs = {};
4591
- attrPattern.lastIndex = 0;
4592
- while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
4593
- const key = attrMatch[1].toLowerCase();
4594
- attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
4595
- }
4596
- 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) {
4597
- logger.debug("Found alternate document: {alternateUrl} from {url}", {
4598
- alternateUrl: attribs.href,
4599
- url: documentUrl
4600
- });
4601
- return await fetch(new URL(attribs.href, docUrl).href);
4602
- }
4601
+ const html = await require_body.readBoundedText(response, 1024 * 1024, documentUrl);
4602
+ const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
4603
+ const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
4604
+ let tagMatch;
4605
+ while ((tagMatch = tagPattern.exec(html)) !== null) {
4606
+ const tagContent = tagMatch[2];
4607
+ let attrMatch;
4608
+ const attribs = {};
4609
+ attrPattern.lastIndex = 0;
4610
+ while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
4611
+ const key = attrMatch[1].toLowerCase();
4612
+ attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
4613
+ }
4614
+ 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) {
4615
+ logger.debug("Found alternate document: {alternateUrl} from {url}", {
4616
+ alternateUrl: attribs.href,
4617
+ url: documentUrl
4618
+ });
4619
+ return await fetch(new URL(attribs.href, docUrl).href);
4603
4620
  }
4604
- document = JSON.parse(html);
4605
4621
  }
4606
- } else document = await response.json();
4622
+ document = JSON.parse(html);
4623
+ } else document = JSON.parse(await require_body.readBoundedText(response, require_body.MAX_BODY_SIZE, documentUrl));
4607
4624
  logger.debug("Fetched document: {status} {url} {headers}", {
4608
4625
  status: response.status,
4609
4626
  url: documentUrl,
@@ -4617,6 +4634,8 @@ async function getRemoteDocument(url, response, fetch) {
4617
4634
  }
4618
4635
  /**
4619
4636
  * Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
4637
+ * At most 20 HTTP redirects and alternate document links are followed in total
4638
+ * per call. Revisiting a URL within that chain throws a {@link FetchError}.
4620
4639
  *
4621
4640
  * The created loader preloads the below frequently used contexts by default
4622
4641
  * (unless `options.skipPreloadedContexts` is set to `true`):
@@ -4628,6 +4647,7 @@ async function getRemoteDocument(url, response, fetch) {
4628
4647
  * - <https://www.w3.org/ns/cid/v1>
4629
4648
  * - <https://w3id.org/security/multikey/v1>
4630
4649
  * - <https://w3id.org/fep/ef61>
4650
+ * - <https://w3id.org/fep/7aa9>
4631
4651
  * - <https://purl.archive.org/socialweb/webfinger>
4632
4652
  * - <http://schema.org/>
4633
4653
  * @param options Options for the document loader.
@@ -4650,7 +4670,11 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
4650
4670
  if (!allowPrivateAddress) try {
4651
4671
  await require_url.validatePublicUrl(currentUrl);
4652
4672
  } catch (error) {
4653
- if (error instanceof require_url.UrlError) logger.error("Disallowed private URL: {url}", {
4673
+ if (error instanceof require_url.UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
4674
+ url: currentUrl,
4675
+ error
4676
+ });
4677
+ else logger.error("Disallowed private URL: {url}", {
4654
4678
  url: currentUrl,
4655
4679
  error
4656
4680
  });
@@ -4688,7 +4712,12 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
4688
4712
  }
4689
4713
  return await load(redirectUrl, options, redirected + 1, visited);
4690
4714
  }
4691
- const result = await getRemoteDocument(currentUrl, response, load);
4715
+ const result = await getRemoteDocument(currentUrl, response, async (alternateUrl) => {
4716
+ options?.signal?.throwIfAborted();
4717
+ if (redirected >= DEFAULT_MAX_REDIRECTION) throw new require_request.FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
4718
+ if (visited.has(alternateUrl)) throw new require_request.FetchError(currentUrl, `Redirect loop detected: ${alternateUrl}`);
4719
+ return await load(alternateUrl, options, redirected + 1, visited);
4720
+ });
4692
4721
  span.setAttribute("docloader.document_url", result.documentUrl);
4693
4722
  if (result.contextUrl != null) span.setAttribute("docloader.context_url", result.contextUrl);
4694
4723
  return result;
@@ -4704,7 +4733,7 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
4704
4733
  }
4705
4734
  });
4706
4735
  }
4707
- return load;
4736
+ return (url, options) => load(url, options);
4708
4737
  }
4709
4738
  //#endregion
4710
4739
  Object.defineProperty(exports, "cid_v1_default", {
@@ -4719,6 +4748,12 @@ Object.defineProperty(exports, "getDocumentLoader", {
4719
4748
  return getDocumentLoader;
4720
4749
  }
4721
4750
  });
4751
+ Object.defineProperty(exports, "getRemoteDocument", {
4752
+ enumerable: true,
4753
+ get: function() {
4754
+ return getRemoteDocument;
4755
+ }
4756
+ });
4722
4757
  Object.defineProperty(exports, "preloadedContexts", {
4723
4758
  enumerable: true,
4724
4759
  get: function() {