@fedify/vocab-runtime 2.3.6 → 2.3.8

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-B82RILk5.mjs +77 -0
  5. package/dist/tests/body-CKFs5kO9.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-CWKdF29U.mjs → docloader-BetS1A0I.mjs} +26 -34
  13. package/dist/tests/{docloader-DprR2N-C.cjs → docloader-DfLdNMgM.cjs} +31 -33
  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-pMmqUKuo.cjs → key-CWDxavIR.cjs} +2 -2
  19. package/dist/tests/{key-CrrK9mYh.mjs → key-Qj7u1qmH.mjs} +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-D4Rca6WD.mjs → request-Borhn2tu.mjs} +1 -1
  29. package/dist/tests/{request-CfJdvXUH.cjs → request-FmFI0z5J.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-Bz_UUDtL.cjs → multibase-BL45eko3.cjs} +0 -0
  46. /package/dist/tests/{multibase-B4bvakyA.mjs → multibase-Da9VJCw8.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-BF_Nb_Cu.cjs → url-BbEC9H_R.cjs} +0 -0
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.3.6",
3
+ "version": "2.3.8",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./src/mod.ts",
package/dist/mod.cjs CHANGED
@@ -4476,7 +4476,151 @@ const preloadedContexts = {
4476
4476
  //#endregion
4477
4477
  //#region deno.json
4478
4478
  var name = "@fedify/vocab-runtime";
4479
- var version = "2.3.6";
4479
+ var version = "2.3.8";
4480
+ //#endregion
4481
+ //#region src/request.ts
4482
+ /**
4483
+ * Error thrown when fetching a JSON-LD document failed.
4484
+ */
4485
+ var FetchError = class extends Error {
4486
+ /**
4487
+ * The URL that failed to fetch.
4488
+ */
4489
+ url;
4490
+ /**
4491
+ * The HTTP response that failed, if available.
4492
+ */
4493
+ response;
4494
+ /**
4495
+ * Constructs a new `FetchError`.
4496
+ *
4497
+ * @param url The URL that failed to fetch.
4498
+ * @param message Error message.
4499
+ * @param response The failed HTTP response, if available.
4500
+ */
4501
+ constructor(url, message, response) {
4502
+ super(message == null ? url.toString() : `${url}: ${message}`);
4503
+ this.name = "FetchError";
4504
+ this.url = typeof url === "string" ? new URL(url) : url;
4505
+ this.response = response;
4506
+ }
4507
+ };
4508
+ /**
4509
+ * Creates a request for the given URL.
4510
+ * @param url The URL to create the request for.
4511
+ * @param options The options for the request.
4512
+ * @returns The created request.
4513
+ * @internal
4514
+ */
4515
+ function createActivityPubRequest(url, options = {}) {
4516
+ return new Request(url, {
4517
+ headers: {
4518
+ Accept: "application/activity+json, application/ld+json",
4519
+ "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
4520
+ },
4521
+ redirect: "manual"
4522
+ });
4523
+ }
4524
+ /**
4525
+ * Gets the user agent string for the given application and URL.
4526
+ * @param options The options for making the user agent string.
4527
+ * @returns The user agent string.
4528
+ * @since 1.3.0
4529
+ */
4530
+ function getUserAgent({ software, url } = {}) {
4531
+ const fedify = `Fedify/${version}`;
4532
+ 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;
4533
+ const userAgent = software == null ? [fedify] : [software, fedify];
4534
+ if (runtime != null) userAgent.push(runtime);
4535
+ if (url != null) userAgent.push(`+${url.toString()}`);
4536
+ return `${userAgent.shift()} (${userAgent.join("; ")})`;
4537
+ }
4538
+ /**
4539
+ * Logs the request.
4540
+ * @param request The request to log.
4541
+ * @internal
4542
+ */
4543
+ function logRequest(logger, request) {
4544
+ logger.debug("Fetching document: {method} {url} {headers}", {
4545
+ method: request.method,
4546
+ url: request.url,
4547
+ headers: Object.fromEntries(request.headers.entries())
4548
+ });
4549
+ }
4550
+ //#endregion
4551
+ //#region src/body.ts
4552
+ /** Decoded JSON body limit: 16 MiB. @internal */
4553
+ const MAX_BODY_SIZE = 16 * 1024 * 1024;
4554
+ /** A body exceeded the byte limit. @internal */
4555
+ var BodyTooLargeError = class extends FetchError {
4556
+ /** Creates an error for a body exceeding the given limit. */
4557
+ constructor(url, maxBytes) {
4558
+ super(url, `Body exceeds the limit of ${maxBytes} bytes`);
4559
+ this.name = "BodyTooLargeError";
4560
+ }
4561
+ };
4562
+ /** Validates a finite, positive byte limit. @internal */
4563
+ function validateBodySizeLimit(maxBytes) {
4564
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
4565
+ }
4566
+ /**
4567
+ * Reads UTF-8 text while limiting bytes received from the body stream.
4568
+ * Fetch responses are already decompressed, so the stream count is authoritative.
4569
+ * @param message The response or request to read.
4570
+ * @param maxBytes The maximum number of decoded bytes to read.
4571
+ * @param url The URL to include in errors and logs.
4572
+ * @returns The decoded body text.
4573
+ * @throws {BodyTooLargeError} If the body exceeds the limit.
4574
+ * @internal
4575
+ */
4576
+ async function readBoundedText(message, maxBytes, url) {
4577
+ validateBodySizeLimit(maxBytes);
4578
+ const reader = message.body?.getReader();
4579
+ const tooLarge = () => {
4580
+ (0, _logtape_logtape.getLogger)([
4581
+ "fedify",
4582
+ "runtime",
4583
+ "body"
4584
+ ]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
4585
+ url: url.toString(),
4586
+ maxBytes
4587
+ });
4588
+ throw new BodyTooLargeError(url, maxBytes);
4589
+ };
4590
+ try {
4591
+ const length = message.headers.get("Content-Length");
4592
+ const encoding = message.headers.get("Content-Encoding");
4593
+ if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
4594
+ if (reader == null) return "";
4595
+ let size = 0;
4596
+ const textReader = new ReadableStream({ async pull(controller) {
4597
+ const { done, value } = await reader.read();
4598
+ if (done) {
4599
+ controller.close();
4600
+ return;
4601
+ }
4602
+ size += value.byteLength;
4603
+ if (size > maxBytes) tooLarge();
4604
+ controller.enqueue(value);
4605
+ } }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
4606
+ try {
4607
+ const chunks = [];
4608
+ while (true) {
4609
+ const { done, value } = await textReader.read();
4610
+ if (done) break;
4611
+ chunks.push(value);
4612
+ }
4613
+ return chunks.join("");
4614
+ } finally {
4615
+ textReader.releaseLock();
4616
+ }
4617
+ } catch (error) {
4618
+ if (reader != null) reader.cancel(error).catch(() => {});
4619
+ throw error;
4620
+ } finally {
4621
+ reader?.releaseLock();
4622
+ }
4623
+ }
4480
4624
  //#endregion
4481
4625
  //#region src/link.ts
4482
4626
  const parametersNeedLowerCase = ["rel", "type"];
@@ -4659,76 +4803,6 @@ var HttpHeaderLink = class HttpHeaderLink {
4659
4803
  }
4660
4804
  };
4661
4805
  //#endregion
4662
- //#region src/request.ts
4663
- /**
4664
- * Error thrown when fetching a JSON-LD document failed.
4665
- */
4666
- var FetchError = class extends Error {
4667
- /**
4668
- * The URL that failed to fetch.
4669
- */
4670
- url;
4671
- /**
4672
- * The HTTP response that failed, if available.
4673
- */
4674
- response;
4675
- /**
4676
- * Constructs a new `FetchError`.
4677
- *
4678
- * @param url The URL that failed to fetch.
4679
- * @param message Error message.
4680
- * @param response The failed HTTP response, if available.
4681
- */
4682
- constructor(url, message, response) {
4683
- super(message == null ? url.toString() : `${url}: ${message}`);
4684
- this.name = "FetchError";
4685
- this.url = typeof url === "string" ? new URL(url) : url;
4686
- this.response = response;
4687
- }
4688
- };
4689
- /**
4690
- * Creates a request for the given URL.
4691
- * @param url The URL to create the request for.
4692
- * @param options The options for the request.
4693
- * @returns The created request.
4694
- * @internal
4695
- */
4696
- function createActivityPubRequest(url, options = {}) {
4697
- return new Request(url, {
4698
- headers: {
4699
- Accept: "application/activity+json, application/ld+json",
4700
- "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
4701
- },
4702
- redirect: "manual"
4703
- });
4704
- }
4705
- /**
4706
- * Gets the user agent string for the given application and URL.
4707
- * @param options The options for making the user agent string.
4708
- * @returns The user agent string.
4709
- * @since 1.3.0
4710
- */
4711
- function getUserAgent({ software, url } = {}) {
4712
- const fedify = `Fedify/${version}`;
4713
- 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;
4714
- const userAgent = software == null ? [fedify] : [software, fedify];
4715
- if (runtime != null) userAgent.push(runtime);
4716
- if (url != null) userAgent.push(`+${url.toString()}`);
4717
- return `${userAgent.shift()} (${userAgent.join("; ")})`;
4718
- }
4719
- /**
4720
- * Logs the request.
4721
- * @param request The request to log.
4722
- * @internal
4723
- */
4724
- function logRequest(logger, request) {
4725
- logger.debug("Fetching document: {method} {url} {headers}", {
4726
- method: request.method,
4727
- url: request.url,
4728
- headers: Object.fromEntries(request.headers.entries())
4729
- });
4730
- }
4731
- //#endregion
4732
4806
  //#region src/url.ts
4733
4807
  var UrlError = class extends Error {
4734
4808
  constructor(message, options) {
@@ -4990,38 +5064,29 @@ async function getRemoteDocument(url, response, fetch) {
4990
5064
  }
4991
5065
  let document;
4992
5066
  if (!jsonLd && (contentType === "text/html" || contentType?.startsWith("text/html;") || contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;"))) {
4993
- const MAX_HTML_SIZE = 1024 * 1024;
4994
- const html = await response.text();
4995
- if (html.length > MAX_HTML_SIZE) {
4996
- logger.warn("HTML response too large, skipping alternate link discovery: {url}", {
4997
- url: documentUrl,
4998
- size: html.length
4999
- });
5000
- document = JSON.parse(html);
5001
- } else {
5002
- const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
5003
- const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
5004
- let tagMatch;
5005
- while ((tagMatch = tagPattern.exec(html)) !== null) {
5006
- const tagContent = tagMatch[2];
5007
- let attrMatch;
5008
- const attribs = {};
5009
- attrPattern.lastIndex = 0;
5010
- while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
5011
- const key = attrMatch[1].toLowerCase();
5012
- attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
5013
- }
5014
- 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) {
5015
- logger.debug("Found alternate document: {alternateUrl} from {url}", {
5016
- alternateUrl: attribs.href,
5017
- url: documentUrl
5018
- });
5019
- return await fetch(new URL(attribs.href, docUrl).href);
5020
- }
5067
+ const html = await readBoundedText(response, 1024 * 1024, documentUrl);
5068
+ const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
5069
+ const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
5070
+ let tagMatch;
5071
+ while ((tagMatch = tagPattern.exec(html)) !== null) {
5072
+ const tagContent = tagMatch[2];
5073
+ let attrMatch;
5074
+ const attribs = {};
5075
+ attrPattern.lastIndex = 0;
5076
+ while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
5077
+ const key = attrMatch[1].toLowerCase();
5078
+ attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
5079
+ }
5080
+ 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) {
5081
+ logger.debug("Found alternate document: {alternateUrl} from {url}", {
5082
+ alternateUrl: attribs.href,
5083
+ url: documentUrl
5084
+ });
5085
+ return await fetch(new URL(attribs.href, docUrl).href);
5021
5086
  }
5022
- document = JSON.parse(html);
5023
5087
  }
5024
- } else document = await response.json();
5088
+ document = JSON.parse(html);
5089
+ } else document = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, documentUrl));
5025
5090
  logger.debug("Fetched document: {status} {url} {headers}", {
5026
5091
  status: response.status,
5027
5092
  url: documentUrl,
package/dist/mod.js CHANGED
@@ -4472,7 +4472,151 @@ const preloadedContexts = {
4472
4472
  //#endregion
4473
4473
  //#region deno.json
4474
4474
  var name = "@fedify/vocab-runtime";
4475
- var version = "2.3.6";
4475
+ var version = "2.3.8";
4476
+ //#endregion
4477
+ //#region src/request.ts
4478
+ /**
4479
+ * Error thrown when fetching a JSON-LD document failed.
4480
+ */
4481
+ var FetchError = class extends Error {
4482
+ /**
4483
+ * The URL that failed to fetch.
4484
+ */
4485
+ url;
4486
+ /**
4487
+ * The HTTP response that failed, if available.
4488
+ */
4489
+ response;
4490
+ /**
4491
+ * Constructs a new `FetchError`.
4492
+ *
4493
+ * @param url The URL that failed to fetch.
4494
+ * @param message Error message.
4495
+ * @param response The failed HTTP response, if available.
4496
+ */
4497
+ constructor(url, message, response) {
4498
+ super(message == null ? url.toString() : `${url}: ${message}`);
4499
+ this.name = "FetchError";
4500
+ this.url = typeof url === "string" ? new URL(url) : url;
4501
+ this.response = response;
4502
+ }
4503
+ };
4504
+ /**
4505
+ * Creates a request for the given URL.
4506
+ * @param url The URL to create the request for.
4507
+ * @param options The options for the request.
4508
+ * @returns The created request.
4509
+ * @internal
4510
+ */
4511
+ function createActivityPubRequest(url, options = {}) {
4512
+ return new Request(url, {
4513
+ headers: {
4514
+ Accept: "application/activity+json, application/ld+json",
4515
+ "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
4516
+ },
4517
+ redirect: "manual"
4518
+ });
4519
+ }
4520
+ /**
4521
+ * Gets the user agent string for the given application and URL.
4522
+ * @param options The options for making the user agent string.
4523
+ * @returns The user agent string.
4524
+ * @since 1.3.0
4525
+ */
4526
+ function getUserAgent({ software, url } = {}) {
4527
+ const fedify = `Fedify/${version}`;
4528
+ 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;
4529
+ const userAgent = software == null ? [fedify] : [software, fedify];
4530
+ if (runtime != null) userAgent.push(runtime);
4531
+ if (url != null) userAgent.push(`+${url.toString()}`);
4532
+ return `${userAgent.shift()} (${userAgent.join("; ")})`;
4533
+ }
4534
+ /**
4535
+ * Logs the request.
4536
+ * @param request The request to log.
4537
+ * @internal
4538
+ */
4539
+ function logRequest(logger, request) {
4540
+ logger.debug("Fetching document: {method} {url} {headers}", {
4541
+ method: request.method,
4542
+ url: request.url,
4543
+ headers: Object.fromEntries(request.headers.entries())
4544
+ });
4545
+ }
4546
+ //#endregion
4547
+ //#region src/body.ts
4548
+ /** Decoded JSON body limit: 16 MiB. @internal */
4549
+ const MAX_BODY_SIZE = 16 * 1024 * 1024;
4550
+ /** A body exceeded the byte limit. @internal */
4551
+ var BodyTooLargeError = class extends FetchError {
4552
+ /** Creates an error for a body exceeding the given limit. */
4553
+ constructor(url, maxBytes) {
4554
+ super(url, `Body exceeds the limit of ${maxBytes} bytes`);
4555
+ this.name = "BodyTooLargeError";
4556
+ }
4557
+ };
4558
+ /** Validates a finite, positive byte limit. @internal */
4559
+ function validateBodySizeLimit(maxBytes) {
4560
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
4561
+ }
4562
+ /**
4563
+ * Reads UTF-8 text while limiting bytes received from the body stream.
4564
+ * Fetch responses are already decompressed, so the stream count is authoritative.
4565
+ * @param message The response or request to read.
4566
+ * @param maxBytes The maximum number of decoded bytes to read.
4567
+ * @param url The URL to include in errors and logs.
4568
+ * @returns The decoded body text.
4569
+ * @throws {BodyTooLargeError} If the body exceeds the limit.
4570
+ * @internal
4571
+ */
4572
+ async function readBoundedText(message, maxBytes, url) {
4573
+ validateBodySizeLimit(maxBytes);
4574
+ const reader = message.body?.getReader();
4575
+ const tooLarge = () => {
4576
+ getLogger([
4577
+ "fedify",
4578
+ "runtime",
4579
+ "body"
4580
+ ]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
4581
+ url: url.toString(),
4582
+ maxBytes
4583
+ });
4584
+ throw new BodyTooLargeError(url, maxBytes);
4585
+ };
4586
+ try {
4587
+ const length = message.headers.get("Content-Length");
4588
+ const encoding = message.headers.get("Content-Encoding");
4589
+ if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
4590
+ if (reader == null) return "";
4591
+ let size = 0;
4592
+ const textReader = new ReadableStream({ async pull(controller) {
4593
+ const { done, value } = await reader.read();
4594
+ if (done) {
4595
+ controller.close();
4596
+ return;
4597
+ }
4598
+ size += value.byteLength;
4599
+ if (size > maxBytes) tooLarge();
4600
+ controller.enqueue(value);
4601
+ } }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
4602
+ try {
4603
+ const chunks = [];
4604
+ while (true) {
4605
+ const { done, value } = await textReader.read();
4606
+ if (done) break;
4607
+ chunks.push(value);
4608
+ }
4609
+ return chunks.join("");
4610
+ } finally {
4611
+ textReader.releaseLock();
4612
+ }
4613
+ } catch (error) {
4614
+ if (reader != null) reader.cancel(error).catch(() => {});
4615
+ throw error;
4616
+ } finally {
4617
+ reader?.releaseLock();
4618
+ }
4619
+ }
4476
4620
  //#endregion
4477
4621
  //#region src/link.ts
4478
4622
  const parametersNeedLowerCase = ["rel", "type"];
@@ -4655,76 +4799,6 @@ var HttpHeaderLink = class HttpHeaderLink {
4655
4799
  }
4656
4800
  };
4657
4801
  //#endregion
4658
- //#region src/request.ts
4659
- /**
4660
- * Error thrown when fetching a JSON-LD document failed.
4661
- */
4662
- var FetchError = class extends Error {
4663
- /**
4664
- * The URL that failed to fetch.
4665
- */
4666
- url;
4667
- /**
4668
- * The HTTP response that failed, if available.
4669
- */
4670
- response;
4671
- /**
4672
- * Constructs a new `FetchError`.
4673
- *
4674
- * @param url The URL that failed to fetch.
4675
- * @param message Error message.
4676
- * @param response The failed HTTP response, if available.
4677
- */
4678
- constructor(url, message, response) {
4679
- super(message == null ? url.toString() : `${url}: ${message}`);
4680
- this.name = "FetchError";
4681
- this.url = typeof url === "string" ? new URL(url) : url;
4682
- this.response = response;
4683
- }
4684
- };
4685
- /**
4686
- * Creates a request for the given URL.
4687
- * @param url The URL to create the request for.
4688
- * @param options The options for the request.
4689
- * @returns The created request.
4690
- * @internal
4691
- */
4692
- function createActivityPubRequest(url, options = {}) {
4693
- return new Request(url, {
4694
- headers: {
4695
- Accept: "application/activity+json, application/ld+json",
4696
- "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
4697
- },
4698
- redirect: "manual"
4699
- });
4700
- }
4701
- /**
4702
- * Gets the user agent string for the given application and URL.
4703
- * @param options The options for making the user agent string.
4704
- * @returns The user agent string.
4705
- * @since 1.3.0
4706
- */
4707
- function getUserAgent({ software, url } = {}) {
4708
- const fedify = `Fedify/${version}`;
4709
- 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;
4710
- const userAgent = software == null ? [fedify] : [software, fedify];
4711
- if (runtime != null) userAgent.push(runtime);
4712
- if (url != null) userAgent.push(`+${url.toString()}`);
4713
- return `${userAgent.shift()} (${userAgent.join("; ")})`;
4714
- }
4715
- /**
4716
- * Logs the request.
4717
- * @param request The request to log.
4718
- * @internal
4719
- */
4720
- function logRequest(logger, request) {
4721
- logger.debug("Fetching document: {method} {url} {headers}", {
4722
- method: request.method,
4723
- url: request.url,
4724
- headers: Object.fromEntries(request.headers.entries())
4725
- });
4726
- }
4727
- //#endregion
4728
4802
  //#region src/url.ts
4729
4803
  var UrlError = class extends Error {
4730
4804
  constructor(message, options) {
@@ -4986,38 +5060,29 @@ async function getRemoteDocument(url, response, fetch) {
4986
5060
  }
4987
5061
  let document;
4988
5062
  if (!jsonLd && (contentType === "text/html" || contentType?.startsWith("text/html;") || contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;"))) {
4989
- const MAX_HTML_SIZE = 1024 * 1024;
4990
- const html = await response.text();
4991
- if (html.length > MAX_HTML_SIZE) {
4992
- logger.warn("HTML response too large, skipping alternate link discovery: {url}", {
4993
- url: documentUrl,
4994
- size: html.length
4995
- });
4996
- document = JSON.parse(html);
4997
- } else {
4998
- const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
4999
- const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
5000
- let tagMatch;
5001
- while ((tagMatch = tagPattern.exec(html)) !== null) {
5002
- const tagContent = tagMatch[2];
5003
- let attrMatch;
5004
- const attribs = {};
5005
- attrPattern.lastIndex = 0;
5006
- while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
5007
- const key = attrMatch[1].toLowerCase();
5008
- attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
5009
- }
5010
- 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) {
5011
- logger.debug("Found alternate document: {alternateUrl} from {url}", {
5012
- alternateUrl: attribs.href,
5013
- url: documentUrl
5014
- });
5015
- return await fetch(new URL(attribs.href, docUrl).href);
5016
- }
5063
+ const html = await readBoundedText(response, 1024 * 1024, documentUrl);
5064
+ const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
5065
+ const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
5066
+ let tagMatch;
5067
+ while ((tagMatch = tagPattern.exec(html)) !== null) {
5068
+ const tagContent = tagMatch[2];
5069
+ let attrMatch;
5070
+ const attribs = {};
5071
+ attrPattern.lastIndex = 0;
5072
+ while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
5073
+ const key = attrMatch[1].toLowerCase();
5074
+ attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
5075
+ }
5076
+ 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) {
5077
+ logger.debug("Found alternate document: {alternateUrl} from {url}", {
5078
+ alternateUrl: attribs.href,
5079
+ url: documentUrl
5080
+ });
5081
+ return await fetch(new URL(attribs.href, docUrl).href);
5017
5082
  }
5018
- document = JSON.parse(html);
5019
5083
  }
5020
- } else document = await response.json();
5084
+ document = JSON.parse(html);
5085
+ } else document = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, documentUrl));
5021
5086
  logger.debug("Fetched document: {status} {url} {headers}", {
5022
5087
  status: response.status,
5023
5088
  url: documentUrl,
@@ -0,0 +1,77 @@
1
+ import { t as FetchError } from "./request-Borhn2tu.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 };