@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.
- package/deno.json +1 -1
- package/dist/{docloader-ClQraSWr.d.cts → docloader-C63enuyr.d.cts} +53 -1
- package/dist/{docloader-ClQraSWr.d.ts → docloader-C63enuyr.d.ts} +53 -1
- package/dist/internal/jsonld-cache.cjs +1 -1
- package/dist/internal/jsonld-cache.d.cts +1 -1
- package/dist/internal/jsonld-cache.d.ts +1 -1
- package/dist/internal/jsonld-cache.js +1 -1
- package/dist/internal/portable-dereference.cjs +1 -1
- package/dist/internal/portable-dereference.d.cts +2 -2
- package/dist/internal/portable-dereference.d.ts +2 -2
- package/dist/internal/portable-dereference.js +1 -1
- package/dist/mod.cjs +206 -17
- package/dist/mod.d.cts +7 -3
- package/dist/mod.d.ts +7 -3
- package/dist/mod.js +205 -18
- package/dist/{portable-ilkFPcXa.d.cts → portable-Cux3IA20.d.cts} +1 -1
- package/dist/{portable-DRtvOxTb.d.ts → portable-DBA1FS_L.d.ts} +1 -1
- package/dist/tests/{body-CNwDKYQs.mjs → body-D1z0X88T.mjs} +55 -2
- package/dist/tests/{body-DHAC8qhR.cjs → body-ORlui-Nx.cjs} +60 -1
- package/dist/tests/body.test.cjs +1 -1
- package/dist/tests/body.test.mjs +1 -1
- package/dist/tests/decimal.test.cjs +2 -2
- package/dist/tests/decimal.test.mjs +2 -2
- package/dist/tests/{docloader-DWkl3u7X.mjs → docloader-CUdtpDuf.mjs} +153 -19
- package/dist/tests/{docloader-DLkb9qAS.cjs → docloader-Vc3BC9ad.cjs} +164 -18
- package/dist/tests/docloader.test.cjs +266 -3
- package/dist/tests/docloader.test.mjs +267 -4
- package/dist/tests/internal/portable-dereference.test.cjs +2 -2
- package/dist/tests/internal/portable-dereference.test.mjs +2 -2
- package/dist/tests/{jsonld-cache-BO-QXxIc.cjs → jsonld-cache-BQMn_ILF.cjs} +1 -1
- package/dist/tests/{jsonld-cache-B7PKo3Lu.mjs → jsonld-cache-DUmuWRwp.mjs} +1 -1
- package/dist/tests/jsonld-cache.test.cjs +2 -2
- package/dist/tests/jsonld-cache.test.mjs +2 -2
- package/dist/tests/{request-Dm3ye46t.cjs → request-C5_3S-un.cjs} +1 -1
- package/dist/tests/{request-Cg8MQByW.mjs → request-IIntZ5kj.mjs} +1 -1
- package/dist/tests/request.test.cjs +1 -1
- package/dist/tests/request.test.mjs +1 -1
- package/dist/tests/{url-gLKqAO1H.cjs → url-B8xyTwP-.cjs} +5 -1
- package/dist/tests/{url-D7qRF-dF.mjs → url-DfS-cvwr.mjs} +5 -1
- package/dist/tests/url.test.cjs +2 -2
- package/dist/tests/url.test.mjs +2 -2
- package/dist/{url-BHZYe8Vu.js → url-Be3moMor.js} +5 -1
- package/dist/{url-P6l9s9dN.cjs → url-DDVweCXr.cjs} +5 -1
- package/package.json +1 -1
- package/src/body.ts +57 -0
- package/src/docloader.test.ts +341 -3
- package/src/docloader.ts +205 -5
- package/src/mod.ts +2 -0
- package/src/url.test.ts +7 -1
- 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-
|
|
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.
|
|
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
|
|
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)
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
url
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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,4 +1,4 @@
|
|
|
1
|
-
import { t as FetchError } from "./request-
|
|
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,
|
|
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-
|
|
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() {
|
package/dist/tests/body.test.cjs
CHANGED
package/dist/tests/body.test.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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";
|