@fedify/vocab-runtime 2.4.0-dev.2169 → 2.4.0-dev.2193

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/{docloader-ClQraSWr.d.cts → docloader-C63enuyr.d.cts} +53 -1
  3. package/dist/{docloader-ClQraSWr.d.ts → docloader-C63enuyr.d.ts} +53 -1
  4. package/dist/internal/jsonld-cache.cjs +1 -1
  5. package/dist/internal/jsonld-cache.d.cts +1 -1
  6. package/dist/internal/jsonld-cache.d.ts +1 -1
  7. package/dist/internal/jsonld-cache.js +1 -1
  8. package/dist/internal/portable-dereference.cjs +1 -1
  9. package/dist/internal/portable-dereference.d.cts +2 -2
  10. package/dist/internal/portable-dereference.d.ts +2 -2
  11. package/dist/internal/portable-dereference.js +1 -1
  12. package/dist/mod.cjs +206 -17
  13. package/dist/mod.d.cts +7 -3
  14. package/dist/mod.d.ts +7 -3
  15. package/dist/mod.js +205 -18
  16. package/dist/{portable-ilkFPcXa.d.cts → portable-Cux3IA20.d.cts} +1 -1
  17. package/dist/{portable-DRtvOxTb.d.ts → portable-DBA1FS_L.d.ts} +1 -1
  18. package/dist/tests/{body-CNwDKYQs.mjs → body-D1z0X88T.mjs} +55 -2
  19. package/dist/tests/{body-DHAC8qhR.cjs → body-ORlui-Nx.cjs} +60 -1
  20. package/dist/tests/body.test.cjs +1 -1
  21. package/dist/tests/body.test.mjs +1 -1
  22. package/dist/tests/decimal.test.cjs +2 -2
  23. package/dist/tests/decimal.test.mjs +2 -2
  24. package/dist/tests/{docloader-DWkl3u7X.mjs → docloader-CUdtpDuf.mjs} +153 -19
  25. package/dist/tests/{docloader-DLkb9qAS.cjs → docloader-Vc3BC9ad.cjs} +164 -18
  26. package/dist/tests/docloader.test.cjs +266 -3
  27. package/dist/tests/docloader.test.mjs +267 -4
  28. package/dist/tests/internal/portable-dereference.test.cjs +2 -2
  29. package/dist/tests/internal/portable-dereference.test.mjs +2 -2
  30. package/dist/tests/{jsonld-cache-BO-QXxIc.cjs → jsonld-cache-BQMn_ILF.cjs} +1 -1
  31. package/dist/tests/{jsonld-cache-B7PKo3Lu.mjs → jsonld-cache-DUmuWRwp.mjs} +1 -1
  32. package/dist/tests/jsonld-cache.test.cjs +2 -2
  33. package/dist/tests/jsonld-cache.test.mjs +2 -2
  34. package/dist/tests/{request-Dm3ye46t.cjs → request-C5_3S-un.cjs} +1 -1
  35. package/dist/tests/{request-Cg8MQByW.mjs → request-IIntZ5kj.mjs} +1 -1
  36. package/dist/tests/request.test.cjs +1 -1
  37. package/dist/tests/request.test.mjs +1 -1
  38. package/dist/tests/{url-gLKqAO1H.cjs → url-B8xyTwP-.cjs} +5 -1
  39. package/dist/tests/{url-D7qRF-dF.mjs → url-DfS-cvwr.mjs} +5 -1
  40. package/dist/tests/url.test.cjs +2 -2
  41. package/dist/tests/url.test.mjs +2 -2
  42. package/dist/{url-BHZYe8Vu.js → url-Be3moMor.js} +5 -1
  43. package/dist/{url-P6l9s9dN.cjs → url-DDVweCXr.cjs} +5 -1
  44. package/package.json +1 -1
  45. package/src/body.ts +57 -0
  46. package/src/docloader.test.ts +341 -3
  47. package/src/docloader.ts +205 -5
  48. package/src/mod.ts +2 -0
  49. package/src/url.test.ts +7 -1
  50. package/src/url.ts +6 -2
package/dist/mod.js CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { t as preloadedContexts } from "./contexts-DPuJ4UYL.js";
3
- import { _ as validatePublicUrl, a as formatIri, c as haveSameFe34Origin, d as isValidPublicIPv4Address, f as isValidPublicIPv6Address, g as toCompatibleEf61Id, h as parseJsonLdId, i as expandIPv6Address, l as haveSameIriOrigin, m as parseIri, n as arePortableUrisEqual, o as fromCompatibleEf61Id, p as parseGatewayUrl, r as canonicalizePortableUri, s as getFe34Origin, t as UrlError, u as isGatewayUrl } from "./url-BHZYe8Vu.js";
3
+ import { _ as validatePublicUrl, a as formatIri, c as haveSameFe34Origin, d as isValidPublicIPv4Address, f as isValidPublicIPv6Address, g as toCompatibleEf61Id, h as parseJsonLdId, i as expandIPv6Address, l as haveSameIriOrigin, m as parseIri, n as arePortableUrisEqual, o as fromCompatibleEf61Id, p as parseGatewayUrl, r as canonicalizePortableUri, s as getFe34Origin, t as UrlError, u as isGatewayUrl } from "./url-Be3moMor.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
6
6
  import process from "node:process";
@@ -12,7 +12,7 @@ import { PublicKeyInfo } from "pkijs";
12
12
  import baseX from "@multiformats/base-x";
13
13
  //#region deno.json
14
14
  var name = "@fedify/vocab-runtime";
15
- var version = "2.4.0-dev.2169+0a45df07";
15
+ var version = "2.4.0-dev.2193+402149b5";
16
16
  //#endregion
17
17
  //#region src/request.ts
18
18
  /**
@@ -163,6 +163,59 @@ async function readBoundedText(message, maxBytes, url) {
163
163
  reader?.releaseLock();
164
164
  }
165
165
  }
166
+ /**
167
+ * Reads raw bytes while limiting bytes received from the body stream.
168
+ * Unlike {@link readBoundedText}, the bytes are returned as they were
169
+ * received, without decoding.
170
+ * @param message The response or request to read.
171
+ * @param maxBytes The maximum number of decoded bytes to read.
172
+ * @param url The URL to include in errors and logs.
173
+ * @returns The body bytes.
174
+ * @throws {BodyTooLargeError} If the body exceeds the limit.
175
+ * @internal
176
+ */
177
+ async function readBoundedBytes(message, maxBytes, url) {
178
+ validateBodySizeLimit(maxBytes);
179
+ const reader = message.body?.getReader();
180
+ const tooLarge = () => {
181
+ getLogger([
182
+ "fedify",
183
+ "runtime",
184
+ "body"
185
+ ]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
186
+ url: url.toString(),
187
+ maxBytes
188
+ });
189
+ throw new BodyTooLargeError(url, maxBytes);
190
+ };
191
+ try {
192
+ const length = message.headers.get("Content-Length");
193
+ const encoding = message.headers.get("Content-Encoding");
194
+ if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
195
+ if (reader == null) return /* @__PURE__ */ new Uint8Array(0);
196
+ const chunks = [];
197
+ let size = 0;
198
+ while (true) {
199
+ const { done, value } = await reader.read();
200
+ if (done) break;
201
+ size += value.byteLength;
202
+ if (size > maxBytes) tooLarge();
203
+ chunks.push(value);
204
+ }
205
+ const bytes = new Uint8Array(size);
206
+ let offset = 0;
207
+ for (const chunk of chunks) {
208
+ bytes.set(chunk, offset);
209
+ offset += chunk.byteLength;
210
+ }
211
+ return bytes;
212
+ } catch (error) {
213
+ if (reader != null) reader.cancel(error).catch(() => {});
214
+ throw error;
215
+ } finally {
216
+ reader?.releaseLock();
217
+ }
218
+ }
166
219
  //#endregion
167
220
  //#region src/link.ts
168
221
  const parametersNeedLowerCase = ["rel", "type"];
@@ -353,6 +406,9 @@ const logger = getLogger([
353
406
  ]);
354
407
  const DEFAULT_MAX_REDIRECTION = 20;
355
408
  const MAX_HTML_SIZE = 1024 * 1024;
409
+ const MAX_ERROR_BODY_SIZE = 1024 * 1024;
410
+ const DEFAULT_TIMEOUT = 1e4;
411
+ const MAX_TIMEOUT = 2147483647;
356
412
  function createResponseMetadata(response) {
357
413
  return new Response(null, {
358
414
  headers: response.headers,
@@ -360,6 +416,55 @@ function createResponseMetadata(response) {
360
416
  statusText: response.statusText
361
417
  });
362
418
  }
419
+ const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([
420
+ 204,
421
+ 205,
422
+ 304
423
+ ]);
424
+ /**
425
+ * Reads the body of an error response while the document loader is still
426
+ * running, so that its timeout and `AbortSignal` also bound the read, and
427
+ * nothing reading {@link FetchError.response} later waits on the network.
428
+ * The body is kept byte for byte, unless it is too large or cannot be read;
429
+ * then only the status and headers are kept. A response whose status
430
+ * the `Response` constructor does not accept (e.g., 999) is kept as a clone
431
+ * whose body has been read in full; if its body is too large or cannot be
432
+ * read, no response is kept at all.
433
+ */
434
+ async function bufferErrorResponse(response, url, signal) {
435
+ if (response.status < 200 || response.status > 599) {
436
+ const clone = response.clone();
437
+ try {
438
+ await readBoundedBytes(response, MAX_ERROR_BODY_SIZE, url);
439
+ } catch (error) {
440
+ await clone.body?.cancel().catch(() => {});
441
+ if (signal?.aborted) throw error;
442
+ logger.debug("Failed to read the error response body from {url}: {error}", {
443
+ url,
444
+ error
445
+ });
446
+ return;
447
+ }
448
+ return clone;
449
+ }
450
+ if (response.body == null || NULL_BODY_STATUSES.has(response.status)) return createResponseMetadata(response);
451
+ let body;
452
+ try {
453
+ body = await readBoundedBytes(response, MAX_ERROR_BODY_SIZE, url);
454
+ } catch (error) {
455
+ if (signal?.aborted) throw error;
456
+ logger.debug("Failed to read the error response body from {url}: {error}", {
457
+ url,
458
+ error
459
+ });
460
+ return createResponseMetadata(response);
461
+ }
462
+ return new Response(body, {
463
+ headers: response.headers,
464
+ status: response.status,
465
+ statusText: response.statusText
466
+ });
467
+ }
363
468
  /**
364
469
  * Gets a {@link RemoteDocument} from the given response.
365
470
  * @param url The URL of the document to load.
@@ -384,7 +489,7 @@ async function getRemoteDocument(url, response, fetch, options) {
384
489
  url: documentUrl,
385
490
  headers: Object.fromEntries(response.headers.entries())
386
491
  });
387
- throw new FetchError(documentUrl, `HTTP ${response.status}: ${documentUrl}`, response.clone());
492
+ throw new FetchError(documentUrl, `HTTP ${response.status}: ${documentUrl}`, await bufferErrorResponse(response, documentUrl, options?.signal));
388
493
  }
389
494
  const contentType = response.headers.get("Content-Type");
390
495
  const jsonLd = contentType == null || contentType === "application/activity+json" || contentType.startsWith("application/activity+json;") || contentType === "application/ld+json" || contentType.startsWith("application/ld+json;");
@@ -471,9 +576,87 @@ async function getRemoteDocument(url, response, fetch, options) {
471
576
  };
472
577
  }
473
578
  /**
579
+ * Resolves {@link DocumentLoaderFactoryOptions.timeout} into milliseconds.
580
+ * @param timeout The timeout option. `undefined` means the default timeout,
581
+ * and `null` means no timeout.
582
+ * @returns The timeout in milliseconds, or `null` if it is turned off.
583
+ * @throws {RangeError} If the timeout is invalid.
584
+ * @internal
585
+ */
586
+ function resolveDocumentLoaderTimeout(timeout) {
587
+ if (timeout === void 0) return DEFAULT_TIMEOUT;
588
+ if (timeout === null) return null;
589
+ if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) throw new RangeError(`The document loader timeout must be a positive finite number of milliseconds, but got ${String(timeout)}.`);
590
+ const ms = Math.ceil(timeout);
591
+ if (ms > MAX_TIMEOUT) throw new RangeError(`The document loader timeout must not be greater than ${MAX_TIMEOUT} milliseconds, but got ${timeout}.`);
592
+ return ms;
593
+ }
594
+ /**
595
+ * Bounds each call of the given document loader by the given timeout.
596
+ * The timeout is combined with the caller's `signal`, and the combined
597
+ * signal is passed to the loader. The call settles no later than the
598
+ * timeout even if the loader is stuck in a step that cannot be aborted,
599
+ * e.g., a DNS lookup.
600
+ *
601
+ * A timed-out call throws a {@link FetchError} without a response, whose
602
+ * `cause` is a `DOMException` named `"TimeoutError"`. If the caller's
603
+ * signal is aborted, its reason is thrown instead.
604
+ * @param loader The document loader to bound.
605
+ * @param timeout The timeout in milliseconds, or `null` for no timeout.
606
+ * It is assumed to have been resolved by
607
+ * {@link resolveDocumentLoaderTimeout}.
608
+ * @returns The bounded document loader.
609
+ * @internal
610
+ */
611
+ function withDocumentLoaderTimeout(loader, timeout) {
612
+ if (timeout == null) return loader;
613
+ return async (url, options) => {
614
+ const callerSignal = options?.signal;
615
+ callerSignal?.throwIfAborted();
616
+ const controller = new AbortController();
617
+ const timeoutReason = new DOMException(`The document loader timed out after ${timeout} ms.`, "TimeoutError");
618
+ let timedOut = false;
619
+ const timer = setTimeout(() => {
620
+ timedOut = true;
621
+ controller.abort(timeoutReason);
622
+ }, timeout);
623
+ const onCallerAbort = () => controller.abort(callerSignal?.reason);
624
+ callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
625
+ let onAbort;
626
+ const aborted = new Promise((_, reject) => {
627
+ onAbort = () => reject(controller.signal.reason);
628
+ controller.signal.addEventListener("abort", onAbort, { once: true });
629
+ });
630
+ const loading = loader(url, {
631
+ ...options,
632
+ signal: controller.signal
633
+ });
634
+ loading.catch(() => {});
635
+ try {
636
+ return await Promise.race([loading, aborted]);
637
+ } catch (error) {
638
+ if (callerSignal?.aborted) throw callerSignal.reason;
639
+ if (!timedOut) throw error;
640
+ logger[options?.suppressError ? "warn" : "error"]("Timed out after {timeout} ms while fetching document: {url}", {
641
+ timeout,
642
+ url
643
+ });
644
+ const fetchError = new FetchError(url, `Timed out after ${timeout} ms`);
645
+ fetchError.cause = timeoutReason;
646
+ throw fetchError;
647
+ } finally {
648
+ clearTimeout(timer);
649
+ callerSignal?.removeEventListener("abort", onCallerAbort);
650
+ if (onAbort != null) controller.signal.removeEventListener("abort", onAbort);
651
+ }
652
+ };
653
+ }
654
+ /**
474
655
  * Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
475
656
  * At most 20 HTTP redirects and alternate document links are followed in total
476
657
  * per call. Revisiting a URL within that chain throws a {@link FetchError}.
658
+ * Each call times out after 10 seconds by default; see
659
+ * {@link DocumentLoaderFactoryOptions.timeout}.
477
660
  *
478
661
  * The created loader preloads the below frequently used contexts by default
479
662
  * (unless `options.skipPreloadedContexts` is set to `true`):
@@ -493,7 +676,8 @@ async function getRemoteDocument(url, response, fetch, options) {
493
676
  * @returns The document loader.
494
677
  * @since 1.3.0
495
678
  */
496
- function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedContexts, userAgent } = {}) {
679
+ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedContexts, timeout, userAgent } = {}) {
680
+ const resolvedTimeout = resolveDocumentLoaderTimeout(timeout);
497
681
  const tracer = trace.getTracerProvider().getTracer(name, version);
498
682
  const maximumRedirection = maxRedirection ?? DEFAULT_MAX_REDIRECTION;
499
683
  async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
@@ -507,18 +691,21 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
507
691
  documentUrl: currentUrl
508
692
  };
509
693
  }
510
- if (!allowPrivateAddress) try {
511
- await validatePublicUrl(currentUrl);
512
- } catch (error) {
513
- if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
514
- url: currentUrl,
515
- error
516
- });
517
- else logger[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
518
- url: currentUrl,
519
- error
520
- });
521
- throw error;
694
+ if (!allowPrivateAddress) {
695
+ try {
696
+ await validatePublicUrl(currentUrl);
697
+ } catch (error) {
698
+ if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
699
+ url: currentUrl,
700
+ error
701
+ });
702
+ else logger[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
703
+ url: currentUrl,
704
+ error
705
+ });
706
+ throw error;
707
+ }
708
+ options?.signal?.throwIfAborted();
522
709
  }
523
710
  visited.add(currentUrl);
524
711
  return await tracer.startActiveSpan("activitypub.fetch_document", {
@@ -573,7 +760,7 @@ function getDocumentLoader({ allowPrivateAddress, maxRedirection, skipPreloadedC
573
760
  }
574
761
  });
575
762
  }
576
- return (url, options) => load(url, options);
763
+ return withDocumentLoaderTimeout((url, options) => load(url, options), resolvedTimeout);
577
764
  }
578
765
  //#endregion
579
766
  //#region src/internal/multicodec.ts
@@ -1390,4 +1577,4 @@ LanguageString.prototype[Symbol.for("nodejs.util.inspect.custom")] = function(_d
1390
1577
  return `<${this.locale.baseName}> ${inspect(this.toString(), options)}`;
1391
1578
  };
1392
1579
  //#endregion
1393
- export { FetchError, LanguageString, UrlError, arePortableUrisEqual, canParseDecimal, canonicalizePortableUri, computeDigestMultibase, createActivityPubRequest, createHashlink, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportDidKey, exportMultibaseKey, exportSpki, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, getRemoteDocument, getUserAgent, haveSameFe34Origin, haveSameIriOrigin, importDidKey, importMultibaseKey, importPem, importPkcs1, importSpki, isDecimal, isGatewayUrl, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, parseDecimal, parseDidKeyVerificationMethod, parseDigestMultibase, parseGatewayUrl, parseHashlink, parseIri, parseJsonLdId, preloadedContexts, toCompatibleEf61Id, validatePublicUrl, verifyDigestMultibase, verifyHashlink };
1580
+ export { FetchError, LanguageString, UrlError, arePortableUrisEqual, canParseDecimal, canonicalizePortableUri, computeDigestMultibase, createActivityPubRequest, createHashlink, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportDidKey, exportMultibaseKey, exportSpki, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, getRemoteDocument, getUserAgent, haveSameFe34Origin, haveSameIriOrigin, importDidKey, importMultibaseKey, importPem, importPkcs1, importSpki, isDecimal, isGatewayUrl, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, parseDecimal, parseDidKeyVerificationMethod, parseDigestMultibase, parseGatewayUrl, parseHashlink, parseIri, parseJsonLdId, preloadedContexts, resolveDocumentLoaderTimeout, toCompatibleEf61Id, validatePublicUrl, verifyDigestMultibase, verifyHashlink, withDocumentLoaderTimeout };
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { n as DocumentLoader } from "./docloader-ClQraSWr.cjs";
2
+ import { n as DocumentLoader } from "./docloader-C63enuyr.cjs";
3
3
  import { TracerProvider } from "@opentelemetry/api";
4
4
 
5
5
  //#region src/portable.d.ts
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { n as DocumentLoader } from "./docloader-ClQraSWr.js";
2
+ import { n as DocumentLoader } from "./docloader-C63enuyr.js";
3
3
  import { TracerProvider } from "@opentelemetry/api";
4
4
 
5
5
  //#region src/portable.d.ts
@@ -1,4 +1,4 @@
1
- import { t as FetchError } from "./request-Cg8MQByW.mjs";
1
+ import { t as FetchError } from "./request-IIntZ5kj.mjs";
2
2
  import { getLogger } from "@logtape/logtape";
3
3
  //#region src/body.ts
4
4
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -73,5 +73,58 @@ async function readBoundedText(message, maxBytes, url) {
73
73
  reader?.releaseLock();
74
74
  }
75
75
  }
76
+ /**
77
+ * Reads raw bytes while limiting bytes received from the body stream.
78
+ * Unlike {@link readBoundedText}, the bytes are returned as they were
79
+ * received, without decoding.
80
+ * @param message The response or request to read.
81
+ * @param maxBytes The maximum number of decoded bytes to read.
82
+ * @param url The URL to include in errors and logs.
83
+ * @returns The body bytes.
84
+ * @throws {BodyTooLargeError} If the body exceeds the limit.
85
+ * @internal
86
+ */
87
+ async function readBoundedBytes(message, maxBytes, url) {
88
+ validateBodySizeLimit(maxBytes);
89
+ const reader = message.body?.getReader();
90
+ const tooLarge = () => {
91
+ getLogger([
92
+ "fedify",
93
+ "runtime",
94
+ "body"
95
+ ]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
96
+ url: url.toString(),
97
+ maxBytes
98
+ });
99
+ throw new BodyTooLargeError(url, maxBytes);
100
+ };
101
+ try {
102
+ const length = message.headers.get("Content-Length");
103
+ const encoding = message.headers.get("Content-Encoding");
104
+ if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
105
+ if (reader == null) return /* @__PURE__ */ new Uint8Array(0);
106
+ const chunks = [];
107
+ let size = 0;
108
+ while (true) {
109
+ const { done, value } = await reader.read();
110
+ if (done) break;
111
+ size += value.byteLength;
112
+ if (size > maxBytes) tooLarge();
113
+ chunks.push(value);
114
+ }
115
+ const bytes = new Uint8Array(size);
116
+ let offset = 0;
117
+ for (const chunk of chunks) {
118
+ bytes.set(chunk, offset);
119
+ offset += chunk.byteLength;
120
+ }
121
+ return bytes;
122
+ } catch (error) {
123
+ if (reader != null) reader.cancel(error).catch(() => {});
124
+ throw error;
125
+ } finally {
126
+ reader?.releaseLock();
127
+ }
128
+ }
76
129
  //#endregion
77
- export { MAX_BODY_SIZE as n, readBoundedText as r, BodyTooLargeError as t };
130
+ export { readBoundedText as i, MAX_BODY_SIZE as n, readBoundedBytes as r, BodyTooLargeError as t };
@@ -1,4 +1,4 @@
1
- const require_request = require("./request-Dm3ye46t.cjs");
1
+ const require_request = require("./request-C5_3S-un.cjs");
2
2
  let _logtape_logtape = require("@logtape/logtape");
3
3
  //#region src/body.ts
4
4
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -73,6 +73,59 @@ async function readBoundedText(message, maxBytes, url) {
73
73
  reader?.releaseLock();
74
74
  }
75
75
  }
76
+ /**
77
+ * Reads raw bytes while limiting bytes received from the body stream.
78
+ * Unlike {@link readBoundedText}, the bytes are returned as they were
79
+ * received, without decoding.
80
+ * @param message The response or request to read.
81
+ * @param maxBytes The maximum number of decoded bytes to read.
82
+ * @param url The URL to include in errors and logs.
83
+ * @returns The body bytes.
84
+ * @throws {BodyTooLargeError} If the body exceeds the limit.
85
+ * @internal
86
+ */
87
+ async function readBoundedBytes(message, maxBytes, url) {
88
+ validateBodySizeLimit(maxBytes);
89
+ const reader = message.body?.getReader();
90
+ const tooLarge = () => {
91
+ (0, _logtape_logtape.getLogger)([
92
+ "fedify",
93
+ "runtime",
94
+ "body"
95
+ ]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
96
+ url: url.toString(),
97
+ maxBytes
98
+ });
99
+ throw new BodyTooLargeError(url, maxBytes);
100
+ };
101
+ try {
102
+ const length = message.headers.get("Content-Length");
103
+ const encoding = message.headers.get("Content-Encoding");
104
+ if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
105
+ if (reader == null) return /* @__PURE__ */ new Uint8Array(0);
106
+ const chunks = [];
107
+ let size = 0;
108
+ while (true) {
109
+ const { done, value } = await reader.read();
110
+ if (done) break;
111
+ size += value.byteLength;
112
+ if (size > maxBytes) tooLarge();
113
+ chunks.push(value);
114
+ }
115
+ const bytes = new Uint8Array(size);
116
+ let offset = 0;
117
+ for (const chunk of chunks) {
118
+ bytes.set(chunk, offset);
119
+ offset += chunk.byteLength;
120
+ }
121
+ return bytes;
122
+ } catch (error) {
123
+ if (reader != null) reader.cancel(error).catch(() => {});
124
+ throw error;
125
+ } finally {
126
+ reader?.releaseLock();
127
+ }
128
+ }
76
129
  //#endregion
77
130
  Object.defineProperty(exports, "BodyTooLargeError", {
78
131
  enumerable: true,
@@ -86,6 +139,12 @@ Object.defineProperty(exports, "MAX_BODY_SIZE", {
86
139
  return MAX_BODY_SIZE;
87
140
  }
88
141
  });
142
+ Object.defineProperty(exports, "readBoundedBytes", {
143
+ enumerable: true,
144
+ get: function() {
145
+ return readBoundedBytes;
146
+ }
147
+ });
89
148
  Object.defineProperty(exports, "readBoundedText", {
90
149
  enumerable: true,
91
150
  get: function() {
@@ -1,4 +1,4 @@
1
- const require_body = require("./body-DHAC8qhR.cjs");
1
+ const require_body = require("./body-ORlui-Nx.cjs");
2
2
  let node_assert_strict = require("node:assert/strict");
3
3
  let node_test = require("node:test");
4
4
  let node_zlib = require("node:zlib");
@@ -1,4 +1,4 @@
1
- import { r as readBoundedText, t as BodyTooLargeError } from "./body-CNwDKYQs.mjs";
1
+ import { i as readBoundedText, t as BodyTooLargeError } from "./body-D1z0X88T.mjs";
2
2
  import { deepStrictEqual, ok, rejects } from "node:assert/strict";
3
3
  import { test } from "node:test";
4
4
  import { gunzipSync, gzipSync } from "node:zlib";
@@ -1,5 +1,5 @@
1
- require("./url-gLKqAO1H.cjs");
2
- require("./docloader-DLkb9qAS.cjs");
1
+ require("./url-B8xyTwP-.cjs");
2
+ require("./docloader-Vc3BC9ad.cjs");
3
3
  require("./key-C-AYkdJJ.cjs");
4
4
  require("./multibase-B5Mea7Ip.cjs");
5
5
  require("./digest-3FeH2Y-Q.cjs");
@@ -1,5 +1,5 @@
1
- import "./url-D7qRF-dF.mjs";
2
- import "./docloader-DWkl3u7X.mjs";
1
+ import "./url-DfS-cvwr.mjs";
2
+ import "./docloader-CUdtpDuf.mjs";
3
3
  import "./key-C2Db_TAJ.mjs";
4
4
  import "./multibase-BPnF_L4e.mjs";
5
5
  import "./digest-COC7xDiQ.mjs";