@fedify/vocab-runtime 2.2.11 → 2.2.13

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 (50) hide show
  1. package/deno.json +1 -1
  2. package/dist/mod.cjs +166 -101
  3. package/dist/mod.js +166 -101
  4. package/dist/tests/body-UDpZTF6c.mjs +77 -0
  5. package/dist/tests/body-Vx0QSf6C.cjs +95 -0
  6. package/dist/tests/body.test.cjs +90 -0
  7. package/dist/tests/body.test.d.cts +1 -0
  8. package/dist/tests/body.test.d.mts +1 -0
  9. package/dist/tests/body.test.mjs +90 -0
  10. package/dist/tests/decimal.test.cjs +6 -6
  11. package/dist/tests/decimal.test.mjs +6 -6
  12. package/dist/tests/{docloader-ekzTbc9R.cjs → docloader-B5V6GD-7.cjs} +31 -33
  13. package/dist/tests/{docloader-Nug7_hIl.mjs → docloader-Cld9tcx4.mjs} +26 -34
  14. package/dist/tests/docloader.test.cjs +83 -4
  15. package/dist/tests/docloader.test.mjs +83 -4
  16. package/dist/tests/internal/multicodec.test.cjs +2 -2
  17. package/dist/tests/internal/multicodec.test.mjs +2 -2
  18. package/dist/tests/{key-BeTHFQJK.mjs → key-BDIsC6hV.mjs} +2 -2
  19. package/dist/tests/{key-DTTIntwb.cjs → key-BarKGLQz.cjs} +2 -2
  20. package/dist/tests/key.test.cjs +3 -3
  21. package/dist/tests/key.test.mjs +3 -3
  22. package/dist/tests/langstr.test.cjs +2 -2
  23. package/dist/tests/langstr.test.mjs +2 -2
  24. package/dist/tests/link.test.cjs +2 -2
  25. package/dist/tests/link.test.mjs +2 -2
  26. package/dist/tests/multibase/multibase.test.cjs +2 -2
  27. package/dist/tests/multibase/multibase.test.mjs +2 -2
  28. package/dist/tests/{request-CMtb-99N.mjs → request-B2ac0drD.mjs} +1 -1
  29. package/dist/tests/{request-HcA2nRAs.cjs → request-DqvIx6-9.cjs} +1 -1
  30. package/dist/tests/request.test.cjs +2 -2
  31. package/dist/tests/request.test.mjs +2 -2
  32. package/dist/tests/temporal.test.cjs +1 -1
  33. package/dist/tests/temporal.test.mjs +1 -1
  34. package/dist/tests/url.test.cjs +2 -2
  35. package/dist/tests/url.test.mjs +2 -2
  36. package/package.json +1 -1
  37. package/src/body.test.ts +125 -0
  38. package/src/body.ts +95 -0
  39. package/src/docloader.test.ts +124 -1
  40. package/src/docloader.ts +41 -43
  41. /package/dist/tests/{langstr-CbAxaeEZ.cjs → langstr-BOQHfJDr.cjs} +0 -0
  42. /package/dist/tests/{langstr-Di5AvKpB.mjs → langstr-DDLc4833.mjs} +0 -0
  43. /package/dist/tests/{link-NUUWCdnK.mjs → link-CYszIegt.mjs} +0 -0
  44. /package/dist/tests/{link-FguCydMA.cjs → link-qDTmjPw_.cjs} +0 -0
  45. /package/dist/tests/{multibase-F7LtMMsK.cjs → multibase-8zhYPa7o.cjs} +0 -0
  46. /package/dist/tests/{multibase-BgU9XRf7.mjs → multibase-Ds75BvXs.mjs} +0 -0
  47. /package/dist/tests/{multicodec-CyFp54fI.mjs → multicodec-BBPZjRfg.mjs} +0 -0
  48. /package/dist/tests/{multicodec-CxGVGa91.cjs → multicodec-ByS9XLsw.cjs} +0 -0
  49. /package/dist/tests/{url-o0e-aXQY.mjs → url-BBfduh6o.mjs} +0 -0
  50. /package/dist/tests/{url-CWU5HT3Y.cjs → url-CnyF489g.cjs} +0 -0
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.2.11",
3
+ "version": "2.2.13",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./src/mod.ts",
package/dist/mod.cjs CHANGED
@@ -4523,7 +4523,151 @@ const preloadedContexts = {
4523
4523
  //#endregion
4524
4524
  //#region deno.json
4525
4525
  var name = "@fedify/vocab-runtime";
4526
- var version = "2.2.11";
4526
+ var version = "2.2.13";
4527
+ //#endregion
4528
+ //#region src/request.ts
4529
+ /**
4530
+ * Error thrown when fetching a JSON-LD document failed.
4531
+ */
4532
+ var FetchError = class extends Error {
4533
+ /**
4534
+ * The URL that failed to fetch.
4535
+ */
4536
+ url;
4537
+ /**
4538
+ * The HTTP response that failed, if available.
4539
+ */
4540
+ response;
4541
+ /**
4542
+ * Constructs a new `FetchError`.
4543
+ *
4544
+ * @param url The URL that failed to fetch.
4545
+ * @param message Error message.
4546
+ * @param response The failed HTTP response, if available.
4547
+ */
4548
+ constructor(url, message, response) {
4549
+ super(message == null ? url.toString() : `${url}: ${message}`);
4550
+ this.name = "FetchError";
4551
+ this.url = typeof url === "string" ? new URL(url) : url;
4552
+ this.response = response;
4553
+ }
4554
+ };
4555
+ /**
4556
+ * Creates a request for the given URL.
4557
+ * @param url The URL to create the request for.
4558
+ * @param options The options for the request.
4559
+ * @returns The created request.
4560
+ * @internal
4561
+ */
4562
+ function createActivityPubRequest(url, options = {}) {
4563
+ return new Request(url, {
4564
+ headers: {
4565
+ Accept: "application/activity+json, application/ld+json",
4566
+ "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
4567
+ },
4568
+ redirect: "manual"
4569
+ });
4570
+ }
4571
+ /**
4572
+ * Gets the user agent string for the given application and URL.
4573
+ * @param options The options for making the user agent string.
4574
+ * @returns The user agent string.
4575
+ * @since 1.3.0
4576
+ */
4577
+ function getUserAgent({ software, url } = {}) {
4578
+ const fedify = `Fedify/${version}`;
4579
+ 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;
4580
+ const userAgent = software == null ? [fedify] : [software, fedify];
4581
+ if (runtime != null) userAgent.push(runtime);
4582
+ if (url != null) userAgent.push(`+${url.toString()}`);
4583
+ return `${userAgent.shift()} (${userAgent.join("; ")})`;
4584
+ }
4585
+ /**
4586
+ * Logs the request.
4587
+ * @param request The request to log.
4588
+ * @internal
4589
+ */
4590
+ function logRequest(logger, request) {
4591
+ logger.debug("Fetching document: {method} {url} {headers}", {
4592
+ method: request.method,
4593
+ url: request.url,
4594
+ headers: Object.fromEntries(request.headers.entries())
4595
+ });
4596
+ }
4597
+ //#endregion
4598
+ //#region src/body.ts
4599
+ /** Decoded JSON body limit: 16 MiB. @internal */
4600
+ const MAX_BODY_SIZE = 16 * 1024 * 1024;
4601
+ /** A body exceeded the byte limit. @internal */
4602
+ var BodyTooLargeError = class extends FetchError {
4603
+ /** Creates an error for a body exceeding the given limit. */
4604
+ constructor(url, maxBytes) {
4605
+ super(url, `Body exceeds the limit of ${maxBytes} bytes`);
4606
+ this.name = "BodyTooLargeError";
4607
+ }
4608
+ };
4609
+ /** Validates a finite, positive byte limit. @internal */
4610
+ function validateBodySizeLimit(maxBytes) {
4611
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
4612
+ }
4613
+ /**
4614
+ * Reads UTF-8 text while limiting bytes received from the body stream.
4615
+ * Fetch responses are already decompressed, so the stream count is authoritative.
4616
+ * @param message The response or request to read.
4617
+ * @param maxBytes The maximum number of decoded bytes to read.
4618
+ * @param url The URL to include in errors and logs.
4619
+ * @returns The decoded body text.
4620
+ * @throws {BodyTooLargeError} If the body exceeds the limit.
4621
+ * @internal
4622
+ */
4623
+ async function readBoundedText(message, maxBytes, url) {
4624
+ validateBodySizeLimit(maxBytes);
4625
+ const reader = message.body?.getReader();
4626
+ const tooLarge = () => {
4627
+ (0, _logtape_logtape.getLogger)([
4628
+ "fedify",
4629
+ "runtime",
4630
+ "body"
4631
+ ]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
4632
+ url: url.toString(),
4633
+ maxBytes
4634
+ });
4635
+ throw new BodyTooLargeError(url, maxBytes);
4636
+ };
4637
+ try {
4638
+ const length = message.headers.get("Content-Length");
4639
+ const encoding = message.headers.get("Content-Encoding");
4640
+ if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
4641
+ if (reader == null) return "";
4642
+ let size = 0;
4643
+ const textReader = new ReadableStream({ async pull(controller) {
4644
+ const { done, value } = await reader.read();
4645
+ if (done) {
4646
+ controller.close();
4647
+ return;
4648
+ }
4649
+ size += value.byteLength;
4650
+ if (size > maxBytes) tooLarge();
4651
+ controller.enqueue(value);
4652
+ } }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
4653
+ try {
4654
+ const chunks = [];
4655
+ while (true) {
4656
+ const { done, value } = await textReader.read();
4657
+ if (done) break;
4658
+ chunks.push(value);
4659
+ }
4660
+ return chunks.join("");
4661
+ } finally {
4662
+ textReader.releaseLock();
4663
+ }
4664
+ } catch (error) {
4665
+ if (reader != null) reader.cancel(error).catch(() => {});
4666
+ throw error;
4667
+ } finally {
4668
+ reader?.releaseLock();
4669
+ }
4670
+ }
4527
4671
  //#endregion
4528
4672
  //#region src/link.ts
4529
4673
  const parametersNeedLowerCase = ["rel", "type"];
@@ -4706,76 +4850,6 @@ var HttpHeaderLink = class HttpHeaderLink {
4706
4850
  }
4707
4851
  };
4708
4852
  //#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
4853
  //#region src/url.ts
4780
4854
  var UrlError = class extends Error {
4781
4855
  constructor(message, options) {
@@ -5037,38 +5111,29 @@ async function getRemoteDocument(url, response, fetch) {
5037
5111
  }
5038
5112
  let document;
5039
5113
  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
- }
5114
+ const html = await readBoundedText(response, 1024 * 1024, documentUrl);
5115
+ const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
5116
+ const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
5117
+ let tagMatch;
5118
+ while ((tagMatch = tagPattern.exec(html)) !== null) {
5119
+ const tagContent = tagMatch[2];
5120
+ let attrMatch;
5121
+ const attribs = {};
5122
+ attrPattern.lastIndex = 0;
5123
+ while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
5124
+ const key = attrMatch[1].toLowerCase();
5125
+ attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
5126
+ }
5127
+ 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) {
5128
+ logger.debug("Found alternate document: {alternateUrl} from {url}", {
5129
+ alternateUrl: attribs.href,
5130
+ url: documentUrl
5131
+ });
5132
+ return await fetch(new URL(attribs.href, docUrl).href);
5068
5133
  }
5069
- document = JSON.parse(html);
5070
5134
  }
5071
- } else document = await response.json();
5135
+ document = JSON.parse(html);
5136
+ } else document = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, documentUrl));
5072
5137
  logger.debug("Fetched document: {status} {url} {headers}", {
5073
5138
  status: response.status,
5074
5139
  url: documentUrl,
package/dist/mod.js CHANGED
@@ -4519,7 +4519,151 @@ const preloadedContexts = {
4519
4519
  //#endregion
4520
4520
  //#region deno.json
4521
4521
  var name = "@fedify/vocab-runtime";
4522
- var version = "2.2.11";
4522
+ var version = "2.2.13";
4523
+ //#endregion
4524
+ //#region src/request.ts
4525
+ /**
4526
+ * Error thrown when fetching a JSON-LD document failed.
4527
+ */
4528
+ var FetchError = class extends Error {
4529
+ /**
4530
+ * The URL that failed to fetch.
4531
+ */
4532
+ url;
4533
+ /**
4534
+ * The HTTP response that failed, if available.
4535
+ */
4536
+ response;
4537
+ /**
4538
+ * Constructs a new `FetchError`.
4539
+ *
4540
+ * @param url The URL that failed to fetch.
4541
+ * @param message Error message.
4542
+ * @param response The failed HTTP response, if available.
4543
+ */
4544
+ constructor(url, message, response) {
4545
+ super(message == null ? url.toString() : `${url}: ${message}`);
4546
+ this.name = "FetchError";
4547
+ this.url = typeof url === "string" ? new URL(url) : url;
4548
+ this.response = response;
4549
+ }
4550
+ };
4551
+ /**
4552
+ * Creates a request for the given URL.
4553
+ * @param url The URL to create the request for.
4554
+ * @param options The options for the request.
4555
+ * @returns The created request.
4556
+ * @internal
4557
+ */
4558
+ function createActivityPubRequest(url, options = {}) {
4559
+ return new Request(url, {
4560
+ headers: {
4561
+ Accept: "application/activity+json, application/ld+json",
4562
+ "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
4563
+ },
4564
+ redirect: "manual"
4565
+ });
4566
+ }
4567
+ /**
4568
+ * Gets the user agent string for the given application and URL.
4569
+ * @param options The options for making the user agent string.
4570
+ * @returns The user agent string.
4571
+ * @since 1.3.0
4572
+ */
4573
+ function getUserAgent({ software, url } = {}) {
4574
+ const fedify = `Fedify/${version}`;
4575
+ const runtime = globalThis.Deno?.version?.deno != null ? `Deno/${Deno.version.deno}` : globalThis.process?.versions?.bun != null ? `Bun/${process.versions.bun}` : "navigator" in globalThis && navigator.userAgent === "Cloudflare-Workers" ? navigator.userAgent : globalThis.process?.versions?.node != null ? `Node.js/${process.versions.node}` : null;
4576
+ const userAgent = software == null ? [fedify] : [software, fedify];
4577
+ if (runtime != null) userAgent.push(runtime);
4578
+ if (url != null) userAgent.push(`+${url.toString()}`);
4579
+ return `${userAgent.shift()} (${userAgent.join("; ")})`;
4580
+ }
4581
+ /**
4582
+ * Logs the request.
4583
+ * @param request The request to log.
4584
+ * @internal
4585
+ */
4586
+ function logRequest(logger, request) {
4587
+ logger.debug("Fetching document: {method} {url} {headers}", {
4588
+ method: request.method,
4589
+ url: request.url,
4590
+ headers: Object.fromEntries(request.headers.entries())
4591
+ });
4592
+ }
4593
+ //#endregion
4594
+ //#region src/body.ts
4595
+ /** Decoded JSON body limit: 16 MiB. @internal */
4596
+ const MAX_BODY_SIZE = 16 * 1024 * 1024;
4597
+ /** A body exceeded the byte limit. @internal */
4598
+ var BodyTooLargeError = class extends FetchError {
4599
+ /** Creates an error for a body exceeding the given limit. */
4600
+ constructor(url, maxBytes) {
4601
+ super(url, `Body exceeds the limit of ${maxBytes} bytes`);
4602
+ this.name = "BodyTooLargeError";
4603
+ }
4604
+ };
4605
+ /** Validates a finite, positive byte limit. @internal */
4606
+ function validateBodySizeLimit(maxBytes) {
4607
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
4608
+ }
4609
+ /**
4610
+ * Reads UTF-8 text while limiting bytes received from the body stream.
4611
+ * Fetch responses are already decompressed, so the stream count is authoritative.
4612
+ * @param message The response or request to read.
4613
+ * @param maxBytes The maximum number of decoded bytes to read.
4614
+ * @param url The URL to include in errors and logs.
4615
+ * @returns The decoded body text.
4616
+ * @throws {BodyTooLargeError} If the body exceeds the limit.
4617
+ * @internal
4618
+ */
4619
+ async function readBoundedText(message, maxBytes, url) {
4620
+ validateBodySizeLimit(maxBytes);
4621
+ const reader = message.body?.getReader();
4622
+ const tooLarge = () => {
4623
+ getLogger([
4624
+ "fedify",
4625
+ "runtime",
4626
+ "body"
4627
+ ]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
4628
+ url: url.toString(),
4629
+ maxBytes
4630
+ });
4631
+ throw new BodyTooLargeError(url, maxBytes);
4632
+ };
4633
+ try {
4634
+ const length = message.headers.get("Content-Length");
4635
+ const encoding = message.headers.get("Content-Encoding");
4636
+ if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
4637
+ if (reader == null) return "";
4638
+ let size = 0;
4639
+ const textReader = new ReadableStream({ async pull(controller) {
4640
+ const { done, value } = await reader.read();
4641
+ if (done) {
4642
+ controller.close();
4643
+ return;
4644
+ }
4645
+ size += value.byteLength;
4646
+ if (size > maxBytes) tooLarge();
4647
+ controller.enqueue(value);
4648
+ } }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
4649
+ try {
4650
+ const chunks = [];
4651
+ while (true) {
4652
+ const { done, value } = await textReader.read();
4653
+ if (done) break;
4654
+ chunks.push(value);
4655
+ }
4656
+ return chunks.join("");
4657
+ } finally {
4658
+ textReader.releaseLock();
4659
+ }
4660
+ } catch (error) {
4661
+ if (reader != null) reader.cancel(error).catch(() => {});
4662
+ throw error;
4663
+ } finally {
4664
+ reader?.releaseLock();
4665
+ }
4666
+ }
4523
4667
  //#endregion
4524
4668
  //#region src/link.ts
4525
4669
  const parametersNeedLowerCase = ["rel", "type"];
@@ -4702,76 +4846,6 @@ var HttpHeaderLink = class HttpHeaderLink {
4702
4846
  }
4703
4847
  };
4704
4848
  //#endregion
4705
- //#region src/request.ts
4706
- /**
4707
- * Error thrown when fetching a JSON-LD document failed.
4708
- */
4709
- var FetchError = class extends Error {
4710
- /**
4711
- * The URL that failed to fetch.
4712
- */
4713
- url;
4714
- /**
4715
- * The HTTP response that failed, if available.
4716
- */
4717
- response;
4718
- /**
4719
- * Constructs a new `FetchError`.
4720
- *
4721
- * @param url The URL that failed to fetch.
4722
- * @param message Error message.
4723
- * @param response The failed HTTP response, if available.
4724
- */
4725
- constructor(url, message, response) {
4726
- super(message == null ? url.toString() : `${url}: ${message}`);
4727
- this.name = "FetchError";
4728
- this.url = typeof url === "string" ? new URL(url) : url;
4729
- this.response = response;
4730
- }
4731
- };
4732
- /**
4733
- * Creates a request for the given URL.
4734
- * @param url The URL to create the request for.
4735
- * @param options The options for the request.
4736
- * @returns The created request.
4737
- * @internal
4738
- */
4739
- function createActivityPubRequest(url, options = {}) {
4740
- return new Request(url, {
4741
- headers: {
4742
- Accept: "application/activity+json, application/ld+json",
4743
- "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
4744
- },
4745
- redirect: "manual"
4746
- });
4747
- }
4748
- /**
4749
- * Gets the user agent string for the given application and URL.
4750
- * @param options The options for making the user agent string.
4751
- * @returns The user agent string.
4752
- * @since 1.3.0
4753
- */
4754
- function getUserAgent({ software, url } = {}) {
4755
- const fedify = `Fedify/${version}`;
4756
- const runtime = globalThis.Deno?.version?.deno != null ? `Deno/${Deno.version.deno}` : globalThis.process?.versions?.bun != null ? `Bun/${process.versions.bun}` : "navigator" in globalThis && navigator.userAgent === "Cloudflare-Workers" ? navigator.userAgent : globalThis.process?.versions?.node != null ? `Node.js/${process.versions.node}` : null;
4757
- const userAgent = software == null ? [fedify] : [software, fedify];
4758
- if (runtime != null) userAgent.push(runtime);
4759
- if (url != null) userAgent.push(`+${url.toString()}`);
4760
- return `${userAgent.shift()} (${userAgent.join("; ")})`;
4761
- }
4762
- /**
4763
- * Logs the request.
4764
- * @param request The request to log.
4765
- * @internal
4766
- */
4767
- function logRequest(logger, request) {
4768
- logger.debug("Fetching document: {method} {url} {headers}", {
4769
- method: request.method,
4770
- url: request.url,
4771
- headers: Object.fromEntries(request.headers.entries())
4772
- });
4773
- }
4774
- //#endregion
4775
4849
  //#region src/url.ts
4776
4850
  var UrlError = class extends Error {
4777
4851
  constructor(message, options) {
@@ -5033,38 +5107,29 @@ async function getRemoteDocument(url, response, fetch) {
5033
5107
  }
5034
5108
  let document;
5035
5109
  if (!jsonLd && (contentType === "text/html" || contentType?.startsWith("text/html;") || contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;"))) {
5036
- const MAX_HTML_SIZE = 1024 * 1024;
5037
- const html = await response.text();
5038
- if (html.length > MAX_HTML_SIZE) {
5039
- logger.warn("HTML response too large, skipping alternate link discovery: {url}", {
5040
- url: documentUrl,
5041
- size: html.length
5042
- });
5043
- document = JSON.parse(html);
5044
- } else {
5045
- const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
5046
- const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
5047
- let tagMatch;
5048
- while ((tagMatch = tagPattern.exec(html)) !== null) {
5049
- const tagContent = tagMatch[2];
5050
- let attrMatch;
5051
- const attribs = {};
5052
- attrPattern.lastIndex = 0;
5053
- while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
5054
- const key = attrMatch[1].toLowerCase();
5055
- attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
5056
- }
5057
- 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) {
5058
- logger.debug("Found alternate document: {alternateUrl} from {url}", {
5059
- alternateUrl: attribs.href,
5060
- url: documentUrl
5061
- });
5062
- return await fetch(new URL(attribs.href, docUrl).href);
5063
- }
5110
+ const html = await readBoundedText(response, 1024 * 1024, documentUrl);
5111
+ const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
5112
+ const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
5113
+ let tagMatch;
5114
+ while ((tagMatch = tagPattern.exec(html)) !== null) {
5115
+ const tagContent = tagMatch[2];
5116
+ let attrMatch;
5117
+ const attribs = {};
5118
+ attrPattern.lastIndex = 0;
5119
+ while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
5120
+ const key = attrMatch[1].toLowerCase();
5121
+ attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
5122
+ }
5123
+ 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) {
5124
+ logger.debug("Found alternate document: {alternateUrl} from {url}", {
5125
+ alternateUrl: attribs.href,
5126
+ url: documentUrl
5127
+ });
5128
+ return await fetch(new URL(attribs.href, docUrl).href);
5064
5129
  }
5065
- document = JSON.parse(html);
5066
5130
  }
5067
- } else document = await response.json();
5131
+ document = JSON.parse(html);
5132
+ } else document = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, documentUrl));
5068
5133
  logger.debug("Fetched document: {status} {url} {headers}", {
5069
5134
  status: response.status,
5070
5135
  url: documentUrl,
@@ -0,0 +1,77 @@
1
+ import { t as FetchError } from "./request-B2ac0drD.mjs";
2
+ import { getLogger } from "@logtape/logtape";
3
+ //#region src/body.ts
4
+ /** Decoded JSON body limit: 16 MiB. @internal */
5
+ const MAX_BODY_SIZE = 16 * 1024 * 1024;
6
+ /** A body exceeded the byte limit. @internal */
7
+ var BodyTooLargeError = class extends FetchError {
8
+ /** Creates an error for a body exceeding the given limit. */
9
+ constructor(url, maxBytes) {
10
+ super(url, `Body exceeds the limit of ${maxBytes} bytes`);
11
+ this.name = "BodyTooLargeError";
12
+ }
13
+ };
14
+ /** Validates a finite, positive byte limit. @internal */
15
+ function validateBodySizeLimit(maxBytes) {
16
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
17
+ }
18
+ /**
19
+ * Reads UTF-8 text while limiting bytes received from the body stream.
20
+ * Fetch responses are already decompressed, so the stream count is authoritative.
21
+ * @param message The response or request to read.
22
+ * @param maxBytes The maximum number of decoded bytes to read.
23
+ * @param url The URL to include in errors and logs.
24
+ * @returns The decoded body text.
25
+ * @throws {BodyTooLargeError} If the body exceeds the limit.
26
+ * @internal
27
+ */
28
+ async function readBoundedText(message, maxBytes, url) {
29
+ validateBodySizeLimit(maxBytes);
30
+ const reader = message.body?.getReader();
31
+ const tooLarge = () => {
32
+ getLogger([
33
+ "fedify",
34
+ "runtime",
35
+ "body"
36
+ ]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
37
+ url: url.toString(),
38
+ maxBytes
39
+ });
40
+ throw new BodyTooLargeError(url, maxBytes);
41
+ };
42
+ try {
43
+ const length = message.headers.get("Content-Length");
44
+ const encoding = message.headers.get("Content-Encoding");
45
+ if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
46
+ if (reader == null) return "";
47
+ let size = 0;
48
+ const textReader = new ReadableStream({ async pull(controller) {
49
+ const { done, value } = await reader.read();
50
+ if (done) {
51
+ controller.close();
52
+ return;
53
+ }
54
+ size += value.byteLength;
55
+ if (size > maxBytes) tooLarge();
56
+ controller.enqueue(value);
57
+ } }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
58
+ try {
59
+ const chunks = [];
60
+ while (true) {
61
+ const { done, value } = await textReader.read();
62
+ if (done) break;
63
+ chunks.push(value);
64
+ }
65
+ return chunks.join("");
66
+ } finally {
67
+ textReader.releaseLock();
68
+ }
69
+ } catch (error) {
70
+ if (reader != null) reader.cancel(error).catch(() => {});
71
+ throw error;
72
+ } finally {
73
+ reader?.releaseLock();
74
+ }
75
+ }
76
+ //#endregion
77
+ export { MAX_BODY_SIZE as n, readBoundedText as r, BodyTooLargeError as t };