@fedify/vocab-runtime 2.0.28-dev.1979 → 2.0.28

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 (42) hide show
  1. package/deno.json +1 -1
  2. package/dist/mod.cjs +160 -95
  3. package/dist/mod.js +160 -95
  4. package/dist/tests/body-C9c6QIhB.cjs +95 -0
  5. package/dist/tests/body-z85Vik_c.mjs +77 -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/docloader.test.cjs +105 -34
  11. package/dist/tests/docloader.test.mjs +105 -34
  12. package/dist/tests/internal/multicodec.test.cjs +2 -2
  13. package/dist/tests/internal/multicodec.test.mjs +2 -2
  14. package/dist/tests/key.test.cjs +3 -3
  15. package/dist/tests/key.test.mjs +3 -3
  16. package/dist/tests/langstr.test.cjs +1 -1
  17. package/dist/tests/langstr.test.mjs +1 -1
  18. package/dist/tests/link.test.cjs +2 -2
  19. package/dist/tests/link.test.mjs +2 -2
  20. package/dist/tests/multibase/multibase.test.cjs +2 -2
  21. package/dist/tests/multibase/multibase.test.mjs +2 -2
  22. package/dist/tests/{request-D-itzzal.cjs → request-CoDfvqT9.cjs} +1 -1
  23. package/dist/tests/{request-C0jCmRWK.mjs → request-DJnNRk6i.mjs} +1 -1
  24. package/dist/tests/request.test.cjs +2 -2
  25. package/dist/tests/request.test.mjs +2 -2
  26. package/dist/tests/temporal.test.cjs +1 -1
  27. package/dist/tests/temporal.test.mjs +1 -1
  28. package/dist/tests/url.test.cjs +2 -2
  29. package/dist/tests/url.test.mjs +2 -2
  30. package/package.json +1 -1
  31. package/src/body.test.ts +125 -0
  32. package/src/body.ts +95 -0
  33. package/src/docloader.test.ts +124 -1
  34. package/src/docloader.ts +41 -43
  35. /package/dist/tests/{link-B6ZWBZhf.cjs → link-FguCydMA.cjs} +0 -0
  36. /package/dist/tests/{link-B8JGXSS2.mjs → link-NUUWCdnK.mjs} +0 -0
  37. /package/dist/tests/{multibase-CgYqpk4Z.cjs → multibase-CnmModw0.cjs} +0 -0
  38. /package/dist/tests/{multibase-jcKrOpuU.mjs → multibase-DLBufb99.mjs} +0 -0
  39. /package/dist/tests/{multicodec-DeYop8xg.cjs → multicodec-CxGVGa91.cjs} +0 -0
  40. /package/dist/tests/{multicodec-aqbZnrNi.mjs → multicodec-CyFp54fI.mjs} +0 -0
  41. /package/dist/tests/{url-CqwkmczT.cjs → url-CWU5HT3Y.cjs} +0 -0
  42. /package/dist/tests/{url-BulORwtj.mjs → url-o0e-aXQY.mjs} +0 -0
@@ -0,0 +1,77 @@
1
+ import { t as FetchError } from "./request-DJnNRk6i.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 };
@@ -0,0 +1,90 @@
1
+ require("./chunk-Do9eywBl.cjs");
2
+ const require_body = require("./body-C9c6QIhB.cjs");
3
+ let node_assert_strict = require("node:assert/strict");
4
+ let node_test = require("node:test");
5
+ let node_zlib = require("node:zlib");
6
+ //#region src/body.test.ts
7
+ const url = "https://example.com/document";
8
+ (0, node_test.test)("readBoundedText handles empty bodies and flushes incomplete UTF-8", async () => {
9
+ (0, node_assert_strict.deepStrictEqual)(await require_body.readBoundedText(new Response(null), 1, url), "");
10
+ (0, node_assert_strict.deepStrictEqual)(await require_body.readBoundedText(new Response(""), 1, url), "");
11
+ (0, node_assert_strict.deepStrictEqual)(await require_body.readBoundedText(new Response(new Uint8Array([226, 130])), 2, url), "�");
12
+ });
13
+ (0, node_test.test)("readBoundedText counts bytes and preserves split UTF-8", async () => {
14
+ const bytes = new TextEncoder().encode("\"안녕\"");
15
+ const stream = new ReadableStream({ start(controller) {
16
+ for (const byte of bytes) controller.enqueue(new Uint8Array([byte]));
17
+ controller.close();
18
+ } });
19
+ (0, node_assert_strict.deepStrictEqual)(await require_body.readBoundedText(new Response(stream), bytes.length, url), "\"안녕\"");
20
+ await (0, node_assert_strict.rejects)(require_body.readBoundedText(new Response(bytes), bytes.length - 1, url), require_body.BodyTooLargeError);
21
+ });
22
+ (0, node_test.test)("readBoundedText rejects a declared oversized body without pulling", async () => {
23
+ let pulled = false;
24
+ let canceled = false;
25
+ const response = new Response(new ReadableStream({
26
+ pull() {
27
+ pulled = true;
28
+ },
29
+ cancel() {
30
+ canceled = true;
31
+ }
32
+ }, { highWaterMark: 0 }), { headers: { "Content-Length": "100" } });
33
+ await (0, node_assert_strict.rejects)(require_body.readBoundedText(response, 10, url), require_body.BodyTooLargeError);
34
+ (0, node_assert_strict.deepStrictEqual)(pulled, false);
35
+ (0, node_assert_strict.deepStrictEqual)(canceled, true);
36
+ (0, node_assert_strict.deepStrictEqual)(response.body?.locked, false);
37
+ });
38
+ (0, node_test.test)("readBoundedText cancels chunked bodies despite absent or false lengths", async () => {
39
+ for (const length of [
40
+ void 0,
41
+ "1",
42
+ "invalid"
43
+ ]) {
44
+ let pulls = 0;
45
+ let canceled = false;
46
+ const stream = new ReadableStream({
47
+ pull(controller) {
48
+ pulls++;
49
+ controller.enqueue(new Uint8Array(4));
50
+ },
51
+ cancel() {
52
+ canceled = true;
53
+ }
54
+ }, { highWaterMark: 0 });
55
+ const response = new Response(stream, { headers: length == null ? {} : { "Content-Length": length } });
56
+ await (0, node_assert_strict.rejects)(require_body.readBoundedText(response, 8, url), require_body.BodyTooLargeError);
57
+ (0, node_assert_strict.deepStrictEqual)(pulls, 3);
58
+ (0, node_assert_strict.deepStrictEqual)(canceled, true);
59
+ (0, node_assert_strict.deepStrictEqual)(response.body?.locked, false);
60
+ }
61
+ });
62
+ (0, node_test.test)("readBoundedText limits decoded gzip bytes, not the compressed length", async () => {
63
+ const compressed = (0, node_zlib.gzipSync)("\"" + "a".repeat(4096) + "\"");
64
+ (0, node_assert_strict.ok)(compressed.length < 128);
65
+ const decoded = (0, node_zlib.gunzipSync)(compressed);
66
+ await (0, node_assert_strict.rejects)(require_body.readBoundedText(new Response(decoded, { headers: {
67
+ "Content-Encoding": "gzip",
68
+ "Content-Length": String(compressed.length)
69
+ } }), 128, url), require_body.BodyTooLargeError);
70
+ (0, node_assert_strict.deepStrictEqual)(await require_body.readBoundedText(new Response("{}", { headers: {
71
+ "Content-Encoding": "gzip",
72
+ "Content-Length": "22"
73
+ } }), 2, url), "{}");
74
+ });
75
+ (0, node_test.test)("readBoundedText rejects invalid limits and releases failed streams", async () => {
76
+ for (const limit of [
77
+ 0,
78
+ -1,
79
+ NaN,
80
+ Infinity,
81
+ 1.5
82
+ ]) await (0, node_assert_strict.rejects)(require_body.readBoundedText(new Response("{}"), limit, url), RangeError);
83
+ const failure = /* @__PURE__ */ new Error("broken stream");
84
+ const response = new Response(new ReadableStream({ pull(controller) {
85
+ controller.error(failure);
86
+ } }));
87
+ await (0, node_assert_strict.rejects)(require_body.readBoundedText(response, 10, url), (error) => error === failure);
88
+ (0, node_assert_strict.deepStrictEqual)(response.body?.locked, false);
89
+ });
90
+ //#endregion
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,90 @@
1
+ import { r as readBoundedText, t as BodyTooLargeError } from "./body-z85Vik_c.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,10 +1,13 @@
1
1
  const require_chunk = require("./chunk-Do9eywBl.cjs");
2
- const require_request = require("./request-D-itzzal.cjs");
3
- const require_link = require("./link-B6ZWBZhf.cjs");
4
- const require_url = require("./url-CqwkmczT.cjs");
5
- let node_assert = require("node:assert");
2
+ const require_request = require("./request-CoDfvqT9.cjs");
3
+ const require_body = require("./body-C9c6QIhB.cjs");
4
+ const require_link = require("./link-FguCydMA.cjs");
5
+ const require_url = require("./url-CWU5HT3Y.cjs");
6
6
  let node_test = require("node:test");
7
+ let node_zlib = require("node:zlib");
7
8
  let _logtape_logtape = require("@logtape/logtape");
9
+ let node_assert = require("node:assert");
10
+ let node_http = require("node:http");
8
11
  let _opentelemetry_api = require("@opentelemetry/api");
9
12
  //#region ../../node_modules/.pnpm/glob-to-regexp@0.4.1/node_modules/glob-to-regexp/index.js
10
13
  var require_glob_to_regexp = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module) => {
@@ -5686,38 +5689,29 @@ async function getRemoteDocument(url, response, fetch) {
5686
5689
  }
5687
5690
  let document;
5688
5691
  if (!jsonLd && (contentType === "text/html" || contentType?.startsWith("text/html;") || contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;"))) {
5689
- const MAX_HTML_SIZE = 1024 * 1024;
5690
- const html = await response.text();
5691
- if (html.length > MAX_HTML_SIZE) {
5692
- logger.warn("HTML response too large, skipping alternate link discovery: {url}", {
5693
- url: documentUrl,
5694
- size: html.length
5695
- });
5696
- document = JSON.parse(html);
5697
- } else {
5698
- const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
5699
- const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
5700
- let tagMatch;
5701
- while ((tagMatch = tagPattern.exec(html)) !== null) {
5702
- const tagContent = tagMatch[2];
5703
- let attrMatch;
5704
- const attribs = {};
5705
- attrPattern.lastIndex = 0;
5706
- while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
5707
- const key = attrMatch[1].toLowerCase();
5708
- attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
5709
- }
5710
- 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) {
5711
- logger.debug("Found alternate document: {alternateUrl} from {url}", {
5712
- alternateUrl: attribs.href,
5713
- url: documentUrl
5714
- });
5715
- return await fetch(new URL(attribs.href, docUrl).href);
5716
- }
5692
+ const html = await require_body.readBoundedText(response, 1024 * 1024, documentUrl);
5693
+ const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
5694
+ const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
5695
+ let tagMatch;
5696
+ while ((tagMatch = tagPattern.exec(html)) !== null) {
5697
+ const tagContent = tagMatch[2];
5698
+ let attrMatch;
5699
+ const attribs = {};
5700
+ attrPattern.lastIndex = 0;
5701
+ while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
5702
+ const key = attrMatch[1].toLowerCase();
5703
+ attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
5704
+ }
5705
+ 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) {
5706
+ logger.debug("Found alternate document: {alternateUrl} from {url}", {
5707
+ alternateUrl: attribs.href,
5708
+ url: documentUrl
5709
+ });
5710
+ return await fetch(new URL(attribs.href, docUrl).href);
5717
5711
  }
5718
- document = JSON.parse(html);
5719
5712
  }
5720
- } else document = await response.json();
5713
+ document = JSON.parse(html);
5714
+ } else document = JSON.parse(await require_body.readBoundedText(response, require_body.MAX_BODY_SIZE, documentUrl));
5721
5715
  logger.debug("Fetched document: {status} {url} {headers}", {
5722
5716
  status: response.status,
5723
5717
  url: documentUrl,
@@ -6173,4 +6167,81 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
6173
6167
  });
6174
6168
  esm_default.hardReset();
6175
6169
  });
6170
+ (0, node_test.test)("getDocumentLoader() bounds JSON, HTML, and alternate documents", async () => {
6171
+ const url = "https://example.com/bounded";
6172
+ esm_default.mockGlobal();
6173
+ let oversized = true;
6174
+ try {
6175
+ esm_default.get(url, () => new Response("{\"name\":\"hello\"}", { headers: {
6176
+ "Content-Type": "application/activity+json",
6177
+ ...oversized ? { "Content-Length": String(16777217) } : {}
6178
+ } }));
6179
+ const loader = getDocumentLoader({ allowPrivateAddress: true });
6180
+ await (0, node_assert.rejects)(loader(url), require_request.FetchError);
6181
+ oversized = false;
6182
+ (0, node_assert.deepStrictEqual)((await loader(url)).document, { name: "hello" });
6183
+ oversized = true;
6184
+ esm_default.get(`${url}/html`, () => new Response(" ".repeat(1048577), { headers: { "Content-Type": "text/html" } }));
6185
+ await (0, node_assert.rejects)(getDocumentLoader({ allowPrivateAddress: true })(`${url}/html`), require_request.FetchError);
6186
+ esm_default.get(`${url}/alternate`, () => new Response("", { headers: {
6187
+ "Content-Type": "text/html",
6188
+ Link: `<${url}>; rel="alternate"; type="application/activity+json"`
6189
+ } }));
6190
+ await (0, node_assert.rejects)(loader(`${url}/alternate`), require_request.FetchError);
6191
+ esm_default.get(`${url}/redirect`, {
6192
+ status: 302,
6193
+ headers: { Location: url }
6194
+ });
6195
+ await (0, node_assert.rejects)(loader(`${url}/redirect`), require_request.FetchError);
6196
+ } finally {
6197
+ esm_default.hardReset();
6198
+ }
6199
+ });
6200
+ (0, node_test.test)("getDocumentLoader() rejects oversized gzip responses from fetch", async () => {
6201
+ const document = { name: "a".repeat(16 * 1024 * 1024) };
6202
+ const compressed = (0, node_zlib.gzipSync)(JSON.stringify(document));
6203
+ (0, node_assert.ok)(compressed.byteLength < 64 * 1024);
6204
+ const server = (0, node_http.createServer)((request, response) => {
6205
+ const body = request.url === "/small" ? (0, node_zlib.gzipSync)("{\"name\":\"hello\"}") : compressed;
6206
+ response.writeHead(200, {
6207
+ "Content-Type": "application/activity+json",
6208
+ "Content-Encoding": "gzip",
6209
+ "Content-Length": body.byteLength
6210
+ });
6211
+ response.end(body);
6212
+ });
6213
+ await new Promise((resolve, reject) => {
6214
+ server.once("error", reject);
6215
+ server.listen(0, "127.0.0.1", resolve);
6216
+ });
6217
+ try {
6218
+ const address = server.address();
6219
+ (0, node_assert.ok)(address != null && typeof address !== "string");
6220
+ const url = `http://127.0.0.1:${address.port}/document`;
6221
+ await (0, node_assert.rejects)(getDocumentLoader({ allowPrivateAddress: true })(url), require_request.FetchError);
6222
+ (0, node_assert.deepStrictEqual)((await getDocumentLoader({ allowPrivateAddress: true })(new URL("/small", url).href)).document, { name: "hello" });
6223
+ } finally {
6224
+ await new Promise((resolve, reject) => {
6225
+ server.close((error) => error == null ? resolve() : reject(error));
6226
+ server.closeAllConnections();
6227
+ });
6228
+ }
6229
+ });
6230
+ (0, node_test.test)("getRemoteDocument() bounds JSON by default", async () => {
6231
+ let canceled = false;
6232
+ let pulls = 0;
6233
+ await (0, node_assert.rejects)(getRemoteDocument("https://example.com/document", new Response(new ReadableStream({
6234
+ pull(controller) {
6235
+ pulls++;
6236
+ controller.enqueue(new Uint8Array(64 * 1024).fill(32));
6237
+ },
6238
+ cancel() {
6239
+ canceled = true;
6240
+ }
6241
+ }, { highWaterMark: 0 })), () => {
6242
+ throw new Error("Unexpected alternate document");
6243
+ }), require_request.FetchError);
6244
+ (0, node_assert.deepStrictEqual)(canceled, true);
6245
+ (0, node_assert.deepStrictEqual)(pulls, 257);
6246
+ });
6176
6247
  //#endregion
@@ -1,9 +1,12 @@
1
- import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-C0jCmRWK.mjs";
2
- import { t as HttpHeaderLink } from "./link-B8JGXSS2.mjs";
3
- import { o as validatePublicUrl, t as UrlError } from "./url-BulORwtj.mjs";
4
- import { deepStrictEqual, ok, rejects } from "node:assert";
1
+ import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-DJnNRk6i.mjs";
2
+ import { n as MAX_BODY_SIZE, r as readBoundedText } from "./body-z85Vik_c.mjs";
3
+ import { t as HttpHeaderLink } from "./link-NUUWCdnK.mjs";
4
+ import { o as validatePublicUrl, t as UrlError } from "./url-o0e-aXQY.mjs";
5
5
  import { test } from "node:test";
6
+ import { gzipSync } from "node:zlib";
6
7
  import { getLogger } from "@logtape/logtape";
8
+ import { deepStrictEqual, ok, rejects } from "node:assert";
9
+ import { createServer } from "node:http";
7
10
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
8
11
  //#region \0rolldown/runtime.js
9
12
  var __create = Object.create;
@@ -5705,38 +5708,29 @@ async function getRemoteDocument(url, response, fetch) {
5705
5708
  }
5706
5709
  let document;
5707
5710
  if (!jsonLd && (contentType === "text/html" || contentType?.startsWith("text/html;") || contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;"))) {
5708
- const MAX_HTML_SIZE = 1024 * 1024;
5709
- const html = await response.text();
5710
- if (html.length > MAX_HTML_SIZE) {
5711
- logger.warn("HTML response too large, skipping alternate link discovery: {url}", {
5712
- url: documentUrl,
5713
- size: html.length
5714
- });
5715
- document = JSON.parse(html);
5716
- } else {
5717
- const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
5718
- const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
5719
- let tagMatch;
5720
- while ((tagMatch = tagPattern.exec(html)) !== null) {
5721
- const tagContent = tagMatch[2];
5722
- let attrMatch;
5723
- const attribs = {};
5724
- attrPattern.lastIndex = 0;
5725
- while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
5726
- const key = attrMatch[1].toLowerCase();
5727
- attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
5728
- }
5729
- 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) {
5730
- logger.debug("Found alternate document: {alternateUrl} from {url}", {
5731
- alternateUrl: attribs.href,
5732
- url: documentUrl
5733
- });
5734
- return await fetch(new URL(attribs.href, docUrl).href);
5735
- }
5711
+ const html = await readBoundedText(response, 1024 * 1024, documentUrl);
5712
+ const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
5713
+ const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
5714
+ let tagMatch;
5715
+ while ((tagMatch = tagPattern.exec(html)) !== null) {
5716
+ const tagContent = tagMatch[2];
5717
+ let attrMatch;
5718
+ const attribs = {};
5719
+ attrPattern.lastIndex = 0;
5720
+ while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
5721
+ const key = attrMatch[1].toLowerCase();
5722
+ attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
5723
+ }
5724
+ 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) {
5725
+ logger.debug("Found alternate document: {alternateUrl} from {url}", {
5726
+ alternateUrl: attribs.href,
5727
+ url: documentUrl
5728
+ });
5729
+ return await fetch(new URL(attribs.href, docUrl).href);
5736
5730
  }
5737
- document = JSON.parse(html);
5738
5731
  }
5739
- } else document = await response.json();
5732
+ document = JSON.parse(html);
5733
+ } else document = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, documentUrl));
5740
5734
  logger.debug("Fetched document: {status} {url} {headers}", {
5741
5735
  status: response.status,
5742
5736
  url: documentUrl,
@@ -6192,5 +6186,82 @@ test("getDocumentLoader()", async (t) => {
6192
6186
  });
6193
6187
  esm_default.hardReset();
6194
6188
  });
6189
+ test("getDocumentLoader() bounds JSON, HTML, and alternate documents", async () => {
6190
+ const url = "https://example.com/bounded";
6191
+ esm_default.mockGlobal();
6192
+ let oversized = true;
6193
+ try {
6194
+ esm_default.get(url, () => new Response("{\"name\":\"hello\"}", { headers: {
6195
+ "Content-Type": "application/activity+json",
6196
+ ...oversized ? { "Content-Length": String(16777217) } : {}
6197
+ } }));
6198
+ const loader = getDocumentLoader({ allowPrivateAddress: true });
6199
+ await rejects(loader(url), FetchError);
6200
+ oversized = false;
6201
+ deepStrictEqual((await loader(url)).document, { name: "hello" });
6202
+ oversized = true;
6203
+ esm_default.get(`${url}/html`, () => new Response(" ".repeat(1048577), { headers: { "Content-Type": "text/html" } }));
6204
+ await rejects(getDocumentLoader({ allowPrivateAddress: true })(`${url}/html`), FetchError);
6205
+ esm_default.get(`${url}/alternate`, () => new Response("", { headers: {
6206
+ "Content-Type": "text/html",
6207
+ Link: `<${url}>; rel="alternate"; type="application/activity+json"`
6208
+ } }));
6209
+ await rejects(loader(`${url}/alternate`), FetchError);
6210
+ esm_default.get(`${url}/redirect`, {
6211
+ status: 302,
6212
+ headers: { Location: url }
6213
+ });
6214
+ await rejects(loader(`${url}/redirect`), FetchError);
6215
+ } finally {
6216
+ esm_default.hardReset();
6217
+ }
6218
+ });
6219
+ test("getDocumentLoader() rejects oversized gzip responses from fetch", async () => {
6220
+ const document = { name: "a".repeat(16 * 1024 * 1024) };
6221
+ const compressed = gzipSync(JSON.stringify(document));
6222
+ ok(compressed.byteLength < 64 * 1024);
6223
+ const server = createServer((request, response) => {
6224
+ const body = request.url === "/small" ? gzipSync("{\"name\":\"hello\"}") : compressed;
6225
+ response.writeHead(200, {
6226
+ "Content-Type": "application/activity+json",
6227
+ "Content-Encoding": "gzip",
6228
+ "Content-Length": body.byteLength
6229
+ });
6230
+ response.end(body);
6231
+ });
6232
+ await new Promise((resolve, reject) => {
6233
+ server.once("error", reject);
6234
+ server.listen(0, "127.0.0.1", resolve);
6235
+ });
6236
+ try {
6237
+ const address = server.address();
6238
+ ok(address != null && typeof address !== "string");
6239
+ const url = `http://127.0.0.1:${address.port}/document`;
6240
+ await rejects(getDocumentLoader({ allowPrivateAddress: true })(url), FetchError);
6241
+ deepStrictEqual((await getDocumentLoader({ allowPrivateAddress: true })(new URL("/small", url).href)).document, { name: "hello" });
6242
+ } finally {
6243
+ await new Promise((resolve, reject) => {
6244
+ server.close((error) => error == null ? resolve() : reject(error));
6245
+ server.closeAllConnections();
6246
+ });
6247
+ }
6248
+ });
6249
+ test("getRemoteDocument() bounds JSON by default", async () => {
6250
+ let canceled = false;
6251
+ let pulls = 0;
6252
+ await rejects(getRemoteDocument("https://example.com/document", new Response(new ReadableStream({
6253
+ pull(controller) {
6254
+ pulls++;
6255
+ controller.enqueue(new Uint8Array(64 * 1024).fill(32));
6256
+ },
6257
+ cancel() {
6258
+ canceled = true;
6259
+ }
6260
+ }, { highWaterMark: 0 })), () => {
6261
+ throw new Error("Unexpected alternate document");
6262
+ }), FetchError);
6263
+ deepStrictEqual(canceled, true);
6264
+ deepStrictEqual(pulls, 257);
6265
+ });
6195
6266
  //#endregion
6196
6267
  export {};
@@ -1,7 +1,7 @@
1
1
  require("../chunk-Do9eywBl.cjs");
2
- const require_multicodec = require("../multicodec-DeYop8xg.cjs");
3
- let node_assert = require("node:assert");
2
+ const require_multicodec = require("../multicodec-CxGVGa91.cjs");
4
3
  let node_test = require("node:test");
4
+ let node_assert = require("node:assert");
5
5
  //#region src/internal/multicodec.test.ts
6
6
  (0, node_test.test)("getMulticodecPrefix() decodes supported multicodec prefixes", () => {
7
7
  (0, node_assert.deepStrictEqual)(require_multicodec.getMulticodecPrefix(Uint8Array.from([
@@ -1,6 +1,6 @@
1
- import { n as getMulticodecPrefix, r as removeMulticodecPrefix, t as addMulticodecPrefix } from "../multicodec-aqbZnrNi.mjs";
2
- import { deepStrictEqual, throws } from "node:assert";
1
+ import { n as getMulticodecPrefix, r as removeMulticodecPrefix, t as addMulticodecPrefix } from "../multicodec-CyFp54fI.mjs";
3
2
  import { test } from "node:test";
3
+ import { deepStrictEqual, throws } from "node:assert";
4
4
  //#region src/internal/multicodec.test.ts
5
5
  test("getMulticodecPrefix() decodes supported multicodec prefixes", () => {
6
6
  deepStrictEqual(getMulticodecPrefix(Uint8Array.from([
@@ -1,8 +1,8 @@
1
1
  require("./chunk-Do9eywBl.cjs");
2
- const require_multicodec = require("./multicodec-DeYop8xg.cjs");
3
- const require_multibase = require("./multibase-CgYqpk4Z.cjs");
4
- let node_assert = require("node:assert");
2
+ const require_multicodec = require("./multicodec-CxGVGa91.cjs");
3
+ const require_multibase = require("./multibase-CnmModw0.cjs");
5
4
  let node_test = require("node:test");
5
+ let node_assert = require("node:assert");
6
6
  let asn1js = require("asn1js");
7
7
  let byte_encodings_base64 = require("byte-encodings/base64");
8
8
  let byte_encodings_base64url = require("byte-encodings/base64url");
@@ -1,7 +1,7 @@
1
- import { n as getMulticodecPrefix, r as removeMulticodecPrefix, t as addMulticodecPrefix } from "./multicodec-aqbZnrNi.mjs";
2
- import { n as encodeMultibase, t as decodeMultibase } from "./multibase-jcKrOpuU.mjs";
3
- import { deepStrictEqual, rejects } from "node:assert";
1
+ import { n as getMulticodecPrefix, r as removeMulticodecPrefix, t as addMulticodecPrefix } from "./multicodec-CyFp54fI.mjs";
2
+ import { n as encodeMultibase, t as decodeMultibase } from "./multibase-DLBufb99.mjs";
4
3
  import { test } from "node:test";
4
+ import { deepStrictEqual, rejects } from "node:assert";
5
5
  import { Integer, Sequence } from "asn1js";
6
6
  import { decodeBase64, encodeBase64 } from "byte-encodings/base64";
7
7
  import { decodeBase64Url } from "byte-encodings/base64url";
@@ -1,6 +1,6 @@
1
1
  const require_chunk = require("./chunk-Do9eywBl.cjs");
2
- let node_assert = require("node:assert");
3
2
  let node_test = require("node:test");
3
+ let node_assert = require("node:assert");
4
4
  let node_util = require("node:util");
5
5
  node_util = require_chunk.__toESM(node_util);
6
6
  //#region src/langstr.ts
@@ -1,5 +1,5 @@
1
- import { deepStrictEqual } from "node:assert";
2
1
  import { test } from "node:test";
2
+ import { deepStrictEqual } from "node:assert";
3
3
  import util from "node:util";
4
4
  //#region src/langstr.ts
5
5
  /**