@fedify/vocab-runtime 2.1.23 → 2.1.25
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/mod.cjs +220 -107
- package/dist/mod.d.cts +10 -0
- package/dist/mod.d.ts +10 -0
- package/dist/mod.js +220 -108
- package/dist/tests/body-D-gC5vdl.cjs +95 -0
- package/dist/tests/body-DBqFojnW.mjs +77 -0
- package/dist/tests/body.test.cjs +90 -0
- package/dist/tests/body.test.d.cts +1 -0
- package/dist/tests/body.test.d.mts +1 -0
- package/dist/tests/body.test.mjs +90 -0
- package/dist/tests/decimal.test.cjs +6 -6
- package/dist/tests/decimal.test.mjs +6 -6
- package/dist/tests/{docloader-D-CBVlrJ.mjs → docloader-Beoc8qTH.mjs} +66 -37
- package/dist/tests/{docloader-BcWmxXdo.cjs → docloader-fn4ICzFS.cjs} +71 -36
- package/dist/tests/docloader.test.cjs +321 -4
- package/dist/tests/docloader.test.mjs +320 -4
- package/dist/tests/internal/multicodec.test.cjs +2 -2
- package/dist/tests/internal/multicodec.test.mjs +2 -2
- package/dist/tests/{key-BeTHFQJK.mjs → key-BDIsC6hV.mjs} +2 -2
- package/dist/tests/{key-DTTIntwb.cjs → key-BarKGLQz.cjs} +2 -2
- package/dist/tests/key.test.cjs +3 -3
- package/dist/tests/key.test.mjs +3 -3
- package/dist/tests/langstr.test.cjs +2 -2
- package/dist/tests/langstr.test.mjs +2 -2
- package/dist/tests/link.test.cjs +2 -2
- package/dist/tests/link.test.mjs +2 -2
- package/dist/tests/multibase/multibase.test.cjs +2 -2
- package/dist/tests/multibase/multibase.test.mjs +2 -2
- package/dist/tests/{request-CdMLb0ba.cjs → request-DrO5xzev.cjs} +1 -1
- package/dist/tests/{request-DqsgdBUH.mjs → request-DxeGyJVh.mjs} +1 -1
- package/dist/tests/request.test.cjs +2 -2
- package/dist/tests/request.test.mjs +2 -2
- package/dist/tests/temporal.test.cjs +1 -1
- package/dist/tests/temporal.test.mjs +1 -1
- package/dist/tests/{url-o0e-aXQY.mjs → url-3fyJuqC0.mjs} +14 -4
- package/dist/tests/{url-CWU5HT3Y.cjs → url-vZTgkIZg.cjs} +15 -4
- package/dist/tests/url.test.cjs +37 -4
- package/dist/tests/url.test.mjs +37 -4
- package/package.json +1 -1
- package/src/body.test.ts +125 -0
- package/src/body.ts +95 -0
- package/src/contexts/fep-7aa9.json +24 -0
- package/src/contexts.ts +8 -0
- package/src/docloader.test.ts +431 -1
- package/src/docloader.ts +76 -49
- package/src/url.test.ts +29 -3
- package/src/url.ts +21 -5
- /package/dist/tests/{langstr-CbAxaeEZ.cjs → langstr-BOQHfJDr.cjs} +0 -0
- /package/dist/tests/{langstr-Di5AvKpB.mjs → langstr-DDLc4833.mjs} +0 -0
- /package/dist/tests/{link-NUUWCdnK.mjs → link-CYszIegt.mjs} +0 -0
- /package/dist/tests/{link-FguCydMA.cjs → link-qDTmjPw_.cjs} +0 -0
- /package/dist/tests/{multibase-F7LtMMsK.cjs → multibase-8zhYPa7o.cjs} +0 -0
- /package/dist/tests/{multibase-BgU9XRf7.mjs → multibase-Ds75BvXs.mjs} +0 -0
- /package/dist/tests/{multicodec-CyFp54fI.mjs → multicodec-BBPZjRfg.mjs} +0 -0
- /package/dist/tests/{multicodec-CxGVGa91.cjs → multicodec-ByS9XLsw.cjs} +0 -0
package/dist/mod.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { getLogger } from "@logtape/logtape";
|
|
3
3
|
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
4
4
|
import process from "node:process";
|
|
5
|
-
import
|
|
5
|
+
import dns from "node:dns/promises";
|
|
6
6
|
import { isIP } from "node:net";
|
|
7
7
|
import { Integer, Sequence } from "asn1js";
|
|
8
8
|
import { decodeBase64, encodeBase64 } from "byte-encodings/base64";
|
|
@@ -595,6 +595,30 @@ var fep_5711_default = { "@context": {
|
|
|
595
595
|
}
|
|
596
596
|
} };
|
|
597
597
|
//#endregion
|
|
598
|
+
//#region src/contexts/fep-7aa9.json
|
|
599
|
+
var fep_7aa9_default = { "@context": {
|
|
600
|
+
"FeaturedCollection": "https://w3id.org/fep/7aa9#FeaturedCollection",
|
|
601
|
+
"FeaturedItem": "https://w3id.org/fep/7aa9#FeaturedItem",
|
|
602
|
+
"FeatureRequest": "https://w3id.org/fep/7aa9#FeatureRequest",
|
|
603
|
+
"FeatureAuthorization": "https://w3id.org/fep/7aa9#FeatureAuthorization",
|
|
604
|
+
"topic": {
|
|
605
|
+
"@id": "https://w3id.org/fep/7aa9#topic",
|
|
606
|
+
"@type": "@id"
|
|
607
|
+
},
|
|
608
|
+
"featuredObject": {
|
|
609
|
+
"@id": "https://w3id.org/fep/7aa9#featuredObject",
|
|
610
|
+
"@type": "@id"
|
|
611
|
+
},
|
|
612
|
+
"canFeature": {
|
|
613
|
+
"@id": "https://w3id.org/fep/7aa9#canFeature",
|
|
614
|
+
"@type": "@id"
|
|
615
|
+
},
|
|
616
|
+
"featureAuthorization": {
|
|
617
|
+
"@id": "https://w3id.org/fep/7aa9#featureAuthorization",
|
|
618
|
+
"@type": "@id"
|
|
619
|
+
}
|
|
620
|
+
} };
|
|
621
|
+
//#endregion
|
|
598
622
|
//#region src/contexts/fep-ef61.json
|
|
599
623
|
var fep_ef61_default = { "@context": {
|
|
600
624
|
"gateways": {
|
|
@@ -4512,6 +4536,7 @@ const preloadedContexts = {
|
|
|
4512
4536
|
"https://w3id.org/fep/5711": fep_5711_default,
|
|
4513
4537
|
"https://www.w3.org/ns/cid/v1": cid_v1_default,
|
|
4514
4538
|
"https://w3id.org/fep/ef61": fep_ef61_default,
|
|
4539
|
+
"https://w3id.org/fep/7aa9": fep_7aa9_default,
|
|
4515
4540
|
"https://join-lemmy.org/context.json": join_lemmy_default,
|
|
4516
4541
|
"http://joinmastodon.org/ns": joinmastodon_default,
|
|
4517
4542
|
"https://purl.archive.org/miscellany": miscellany_default
|
|
@@ -4519,7 +4544,151 @@ const preloadedContexts = {
|
|
|
4519
4544
|
//#endregion
|
|
4520
4545
|
//#region deno.json
|
|
4521
4546
|
var name = "@fedify/vocab-runtime";
|
|
4522
|
-
var version = "2.1.
|
|
4547
|
+
var version = "2.1.25";
|
|
4548
|
+
//#endregion
|
|
4549
|
+
//#region src/request.ts
|
|
4550
|
+
/**
|
|
4551
|
+
* Error thrown when fetching a JSON-LD document failed.
|
|
4552
|
+
*/
|
|
4553
|
+
var FetchError = class extends Error {
|
|
4554
|
+
/**
|
|
4555
|
+
* The URL that failed to fetch.
|
|
4556
|
+
*/
|
|
4557
|
+
url;
|
|
4558
|
+
/**
|
|
4559
|
+
* The HTTP response that failed, if available.
|
|
4560
|
+
*/
|
|
4561
|
+
response;
|
|
4562
|
+
/**
|
|
4563
|
+
* Constructs a new `FetchError`.
|
|
4564
|
+
*
|
|
4565
|
+
* @param url The URL that failed to fetch.
|
|
4566
|
+
* @param message Error message.
|
|
4567
|
+
* @param response The failed HTTP response, if available.
|
|
4568
|
+
*/
|
|
4569
|
+
constructor(url, message, response) {
|
|
4570
|
+
super(message == null ? url.toString() : `${url}: ${message}`);
|
|
4571
|
+
this.name = "FetchError";
|
|
4572
|
+
this.url = typeof url === "string" ? new URL(url) : url;
|
|
4573
|
+
this.response = response;
|
|
4574
|
+
}
|
|
4575
|
+
};
|
|
4576
|
+
/**
|
|
4577
|
+
* Creates a request for the given URL.
|
|
4578
|
+
* @param url The URL to create the request for.
|
|
4579
|
+
* @param options The options for the request.
|
|
4580
|
+
* @returns The created request.
|
|
4581
|
+
* @internal
|
|
4582
|
+
*/
|
|
4583
|
+
function createActivityPubRequest(url, options = {}) {
|
|
4584
|
+
return new Request(url, {
|
|
4585
|
+
headers: {
|
|
4586
|
+
Accept: "application/activity+json, application/ld+json",
|
|
4587
|
+
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
4588
|
+
},
|
|
4589
|
+
redirect: "manual"
|
|
4590
|
+
});
|
|
4591
|
+
}
|
|
4592
|
+
/**
|
|
4593
|
+
* Gets the user agent string for the given application and URL.
|
|
4594
|
+
* @param options The options for making the user agent string.
|
|
4595
|
+
* @returns The user agent string.
|
|
4596
|
+
* @since 1.3.0
|
|
4597
|
+
*/
|
|
4598
|
+
function getUserAgent({ software, url } = {}) {
|
|
4599
|
+
const fedify = `Fedify/${version}`;
|
|
4600
|
+
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;
|
|
4601
|
+
const userAgent = software == null ? [fedify] : [software, fedify];
|
|
4602
|
+
if (runtime != null) userAgent.push(runtime);
|
|
4603
|
+
if (url != null) userAgent.push(`+${url.toString()}`);
|
|
4604
|
+
return `${userAgent.shift()} (${userAgent.join("; ")})`;
|
|
4605
|
+
}
|
|
4606
|
+
/**
|
|
4607
|
+
* Logs the request.
|
|
4608
|
+
* @param request The request to log.
|
|
4609
|
+
* @internal
|
|
4610
|
+
*/
|
|
4611
|
+
function logRequest(logger, request) {
|
|
4612
|
+
logger.debug("Fetching document: {method} {url} {headers}", {
|
|
4613
|
+
method: request.method,
|
|
4614
|
+
url: request.url,
|
|
4615
|
+
headers: Object.fromEntries(request.headers.entries())
|
|
4616
|
+
});
|
|
4617
|
+
}
|
|
4618
|
+
//#endregion
|
|
4619
|
+
//#region src/body.ts
|
|
4620
|
+
/** Decoded JSON body limit: 16 MiB. @internal */
|
|
4621
|
+
const MAX_BODY_SIZE = 16 * 1024 * 1024;
|
|
4622
|
+
/** A body exceeded the byte limit. @internal */
|
|
4623
|
+
var BodyTooLargeError = class extends FetchError {
|
|
4624
|
+
/** Creates an error for a body exceeding the given limit. */
|
|
4625
|
+
constructor(url, maxBytes) {
|
|
4626
|
+
super(url, `Body exceeds the limit of ${maxBytes} bytes`);
|
|
4627
|
+
this.name = "BodyTooLargeError";
|
|
4628
|
+
}
|
|
4629
|
+
};
|
|
4630
|
+
/** Validates a finite, positive byte limit. @internal */
|
|
4631
|
+
function validateBodySizeLimit(maxBytes) {
|
|
4632
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
|
|
4633
|
+
}
|
|
4634
|
+
/**
|
|
4635
|
+
* Reads UTF-8 text while limiting bytes received from the body stream.
|
|
4636
|
+
* Fetch responses are already decompressed, so the stream count is authoritative.
|
|
4637
|
+
* @param message The response or request to read.
|
|
4638
|
+
* @param maxBytes The maximum number of decoded bytes to read.
|
|
4639
|
+
* @param url The URL to include in errors and logs.
|
|
4640
|
+
* @returns The decoded body text.
|
|
4641
|
+
* @throws {BodyTooLargeError} If the body exceeds the limit.
|
|
4642
|
+
* @internal
|
|
4643
|
+
*/
|
|
4644
|
+
async function readBoundedText(message, maxBytes, url) {
|
|
4645
|
+
validateBodySizeLimit(maxBytes);
|
|
4646
|
+
const reader = message.body?.getReader();
|
|
4647
|
+
const tooLarge = () => {
|
|
4648
|
+
getLogger([
|
|
4649
|
+
"fedify",
|
|
4650
|
+
"runtime",
|
|
4651
|
+
"body"
|
|
4652
|
+
]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
|
|
4653
|
+
url: url.toString(),
|
|
4654
|
+
maxBytes
|
|
4655
|
+
});
|
|
4656
|
+
throw new BodyTooLargeError(url, maxBytes);
|
|
4657
|
+
};
|
|
4658
|
+
try {
|
|
4659
|
+
const length = message.headers.get("Content-Length");
|
|
4660
|
+
const encoding = message.headers.get("Content-Encoding");
|
|
4661
|
+
if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
|
|
4662
|
+
if (reader == null) return "";
|
|
4663
|
+
let size = 0;
|
|
4664
|
+
const textReader = new ReadableStream({ async pull(controller) {
|
|
4665
|
+
const { done, value } = await reader.read();
|
|
4666
|
+
if (done) {
|
|
4667
|
+
controller.close();
|
|
4668
|
+
return;
|
|
4669
|
+
}
|
|
4670
|
+
size += value.byteLength;
|
|
4671
|
+
if (size > maxBytes) tooLarge();
|
|
4672
|
+
controller.enqueue(value);
|
|
4673
|
+
} }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
|
|
4674
|
+
try {
|
|
4675
|
+
const chunks = [];
|
|
4676
|
+
while (true) {
|
|
4677
|
+
const { done, value } = await textReader.read();
|
|
4678
|
+
if (done) break;
|
|
4679
|
+
chunks.push(value);
|
|
4680
|
+
}
|
|
4681
|
+
return chunks.join("");
|
|
4682
|
+
} finally {
|
|
4683
|
+
textReader.releaseLock();
|
|
4684
|
+
}
|
|
4685
|
+
} catch (error) {
|
|
4686
|
+
if (reader != null) reader.cancel(error).catch(() => {});
|
|
4687
|
+
throw error;
|
|
4688
|
+
} finally {
|
|
4689
|
+
reader?.releaseLock();
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4523
4692
|
//#endregion
|
|
4524
4693
|
//#region src/link.ts
|
|
4525
4694
|
const parametersNeedLowerCase = ["rel", "type"];
|
|
@@ -4702,81 +4871,18 @@ var HttpHeaderLink = class HttpHeaderLink {
|
|
|
4702
4871
|
}
|
|
4703
4872
|
};
|
|
4704
4873
|
//#endregion
|
|
4705
|
-
//#region src/request.ts
|
|
4706
|
-
/**
|
|
4707
|
-
* Error thrown when fetching a JSON-LD document failed.
|
|
4708
|
-
*/
|
|
4709
|
-
var FetchError = class extends Error {
|
|
4710
|
-
/**
|
|
4711
|
-
* The URL that failed to fetch.
|
|
4712
|
-
*/
|
|
4713
|
-
url;
|
|
4714
|
-
/**
|
|
4715
|
-
* The HTTP response that failed, if available.
|
|
4716
|
-
*/
|
|
4717
|
-
response;
|
|
4718
|
-
/**
|
|
4719
|
-
* Constructs a new `FetchError`.
|
|
4720
|
-
*
|
|
4721
|
-
* @param url The URL that failed to fetch.
|
|
4722
|
-
* @param message Error message.
|
|
4723
|
-
* @param response The failed HTTP response, if available.
|
|
4724
|
-
*/
|
|
4725
|
-
constructor(url, message, response) {
|
|
4726
|
-
super(message == null ? url.toString() : `${url}: ${message}`);
|
|
4727
|
-
this.name = "FetchError";
|
|
4728
|
-
this.url = typeof url === "string" ? new URL(url) : url;
|
|
4729
|
-
this.response = response;
|
|
4730
|
-
}
|
|
4731
|
-
};
|
|
4732
|
-
/**
|
|
4733
|
-
* Creates a request for the given URL.
|
|
4734
|
-
* @param url The URL to create the request for.
|
|
4735
|
-
* @param options The options for the request.
|
|
4736
|
-
* @returns The created request.
|
|
4737
|
-
* @internal
|
|
4738
|
-
*/
|
|
4739
|
-
function createActivityPubRequest(url, options = {}) {
|
|
4740
|
-
return new Request(url, {
|
|
4741
|
-
headers: {
|
|
4742
|
-
Accept: "application/activity+json, application/ld+json",
|
|
4743
|
-
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
4744
|
-
},
|
|
4745
|
-
redirect: "manual"
|
|
4746
|
-
});
|
|
4747
|
-
}
|
|
4748
|
-
/**
|
|
4749
|
-
* Gets the user agent string for the given application and URL.
|
|
4750
|
-
* @param options The options for making the user agent string.
|
|
4751
|
-
* @returns The user agent string.
|
|
4752
|
-
* @since 1.3.0
|
|
4753
|
-
*/
|
|
4754
|
-
function getUserAgent({ software, url } = {}) {
|
|
4755
|
-
const fedify = `Fedify/${version}`;
|
|
4756
|
-
const runtime = globalThis.Deno?.version?.deno != null ? `Deno/${Deno.version.deno}` : globalThis.process?.versions?.bun != null ? `Bun/${process.versions.bun}` : "navigator" in globalThis && navigator.userAgent === "Cloudflare-Workers" ? navigator.userAgent : globalThis.process?.versions?.node != null ? `Node.js/${process.versions.node}` : null;
|
|
4757
|
-
const userAgent = software == null ? [fedify] : [software, fedify];
|
|
4758
|
-
if (runtime != null) userAgent.push(runtime);
|
|
4759
|
-
if (url != null) userAgent.push(`+${url.toString()}`);
|
|
4760
|
-
return `${userAgent.shift()} (${userAgent.join("; ")})`;
|
|
4761
|
-
}
|
|
4762
|
-
/**
|
|
4763
|
-
* Logs the request.
|
|
4764
|
-
* @param request The request to log.
|
|
4765
|
-
* @internal
|
|
4766
|
-
*/
|
|
4767
|
-
function logRequest(logger, request) {
|
|
4768
|
-
logger.debug("Fetching document: {method} {url} {headers}", {
|
|
4769
|
-
method: request.method,
|
|
4770
|
-
url: request.url,
|
|
4771
|
-
headers: Object.fromEntries(request.headers.entries())
|
|
4772
|
-
});
|
|
4773
|
-
}
|
|
4774
|
-
//#endregion
|
|
4775
4874
|
//#region src/url.ts
|
|
4776
4875
|
var UrlError = class extends Error {
|
|
4876
|
+
/**
|
|
4877
|
+
* The failure category. Defaults to `"disallowed"`.
|
|
4878
|
+
* `"dns"` includes lookup errors and results with no usable IP addresses.
|
|
4879
|
+
* @since 2.0.29
|
|
4880
|
+
*/
|
|
4881
|
+
reason;
|
|
4777
4882
|
constructor(message, options) {
|
|
4778
4883
|
super(message, options);
|
|
4779
4884
|
this.name = "UrlError";
|
|
4885
|
+
this.reason = options?.reason ?? "disallowed";
|
|
4780
4886
|
}
|
|
4781
4887
|
};
|
|
4782
4888
|
/**
|
|
@@ -4802,9 +4908,12 @@ async function validatePublicUrl(url) {
|
|
|
4802
4908
|
}
|
|
4803
4909
|
let addresses;
|
|
4804
4910
|
try {
|
|
4805
|
-
addresses = await lookup(hostname, { all: true });
|
|
4911
|
+
addresses = await dns.lookup(hostname, { all: true });
|
|
4806
4912
|
} catch (error) {
|
|
4807
|
-
throw new UrlError("DNS lookup failed", {
|
|
4913
|
+
throw new UrlError("DNS lookup failed", {
|
|
4914
|
+
cause: error,
|
|
4915
|
+
reason: "dns"
|
|
4916
|
+
});
|
|
4808
4917
|
}
|
|
4809
4918
|
validateLookupAddresses(addresses);
|
|
4810
4919
|
}
|
|
@@ -4833,7 +4942,7 @@ function validateLookupAddresses(addresses) {
|
|
|
4833
4942
|
ipAddressCount++;
|
|
4834
4943
|
validatePublicIpAddress(address, family);
|
|
4835
4944
|
}
|
|
4836
|
-
if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address");
|
|
4945
|
+
if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address", { reason: "dns" });
|
|
4837
4946
|
}
|
|
4838
4947
|
function validatePublicIpAddress(address, family) {
|
|
4839
4948
|
if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return;
|
|
@@ -5033,38 +5142,29 @@ async function getRemoteDocument(url, response, fetch) {
|
|
|
5033
5142
|
}
|
|
5034
5143
|
let document;
|
|
5035
5144
|
if (!jsonLd && (contentType === "text/html" || contentType?.startsWith("text/html;") || contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;"))) {
|
|
5036
|
-
const
|
|
5037
|
-
const
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
|
|
5056
|
-
}
|
|
5057
|
-
if (attribs.rel === "alternate" && "type" in attribs && (attribs.type === "application/activity+json" || attribs.type === "application/ld+json" || attribs.type.startsWith("application/ld+json;")) && "href" in attribs && new URL(attribs.href, docUrl).href !== docUrl.href) {
|
|
5058
|
-
logger.debug("Found alternate document: {alternateUrl} from {url}", {
|
|
5059
|
-
alternateUrl: attribs.href,
|
|
5060
|
-
url: documentUrl
|
|
5061
|
-
});
|
|
5062
|
-
return await fetch(new URL(attribs.href, docUrl).href);
|
|
5063
|
-
}
|
|
5145
|
+
const html = await readBoundedText(response, 1024 * 1024, documentUrl);
|
|
5146
|
+
const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
|
|
5147
|
+
const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
5148
|
+
let tagMatch;
|
|
5149
|
+
while ((tagMatch = tagPattern.exec(html)) !== null) {
|
|
5150
|
+
const tagContent = tagMatch[2];
|
|
5151
|
+
let attrMatch;
|
|
5152
|
+
const attribs = {};
|
|
5153
|
+
attrPattern.lastIndex = 0;
|
|
5154
|
+
while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
|
|
5155
|
+
const key = attrMatch[1].toLowerCase();
|
|
5156
|
+
attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
|
|
5157
|
+
}
|
|
5158
|
+
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) {
|
|
5159
|
+
logger.debug("Found alternate document: {alternateUrl} from {url}", {
|
|
5160
|
+
alternateUrl: attribs.href,
|
|
5161
|
+
url: documentUrl
|
|
5162
|
+
});
|
|
5163
|
+
return await fetch(new URL(attribs.href, docUrl).href);
|
|
5064
5164
|
}
|
|
5065
|
-
document = JSON.parse(html);
|
|
5066
5165
|
}
|
|
5067
|
-
|
|
5166
|
+
document = JSON.parse(html);
|
|
5167
|
+
} else document = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, documentUrl));
|
|
5068
5168
|
logger.debug("Fetched document: {status} {url} {headers}", {
|
|
5069
5169
|
status: response.status,
|
|
5070
5170
|
url: documentUrl,
|
|
@@ -5078,6 +5178,8 @@ async function getRemoteDocument(url, response, fetch) {
|
|
|
5078
5178
|
}
|
|
5079
5179
|
/**
|
|
5080
5180
|
* Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
|
|
5181
|
+
* At most 20 HTTP redirects and alternate document links are followed in total
|
|
5182
|
+
* per call. Revisiting a URL within that chain throws a {@link FetchError}.
|
|
5081
5183
|
*
|
|
5082
5184
|
* The created loader preloads the below frequently used contexts by default
|
|
5083
5185
|
* (unless `options.skipPreloadedContexts` is set to `true`):
|
|
@@ -5089,6 +5191,7 @@ async function getRemoteDocument(url, response, fetch) {
|
|
|
5089
5191
|
* - <https://www.w3.org/ns/cid/v1>
|
|
5090
5192
|
* - <https://w3id.org/security/multikey/v1>
|
|
5091
5193
|
* - <https://w3id.org/fep/ef61>
|
|
5194
|
+
* - <https://w3id.org/fep/7aa9>
|
|
5092
5195
|
* - <https://purl.archive.org/socialweb/webfinger>
|
|
5093
5196
|
* - <http://schema.org/>
|
|
5094
5197
|
* @param options Options for the document loader.
|
|
@@ -5111,7 +5214,11 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
|
|
|
5111
5214
|
if (!allowPrivateAddress) try {
|
|
5112
5215
|
await validatePublicUrl(currentUrl);
|
|
5113
5216
|
} catch (error) {
|
|
5114
|
-
if (error instanceof UrlError) logger.
|
|
5217
|
+
if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
|
|
5218
|
+
url: currentUrl,
|
|
5219
|
+
error
|
|
5220
|
+
});
|
|
5221
|
+
else logger.error("Disallowed private URL: {url}", {
|
|
5115
5222
|
url: currentUrl,
|
|
5116
5223
|
error
|
|
5117
5224
|
});
|
|
@@ -5149,7 +5256,12 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
|
|
|
5149
5256
|
}
|
|
5150
5257
|
return await load(redirectUrl, options, redirected + 1, visited);
|
|
5151
5258
|
}
|
|
5152
|
-
const result = await getRemoteDocument(currentUrl, response,
|
|
5259
|
+
const result = await getRemoteDocument(currentUrl, response, async (alternateUrl) => {
|
|
5260
|
+
options?.signal?.throwIfAborted();
|
|
5261
|
+
if (redirected >= DEFAULT_MAX_REDIRECTION) throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
|
|
5262
|
+
if (visited.has(alternateUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${alternateUrl}`);
|
|
5263
|
+
return await load(alternateUrl, options, redirected + 1, visited);
|
|
5264
|
+
});
|
|
5153
5265
|
span.setAttribute("docloader.document_url", result.documentUrl);
|
|
5154
5266
|
if (result.contextUrl != null) span.setAttribute("docloader.context_url", result.contextUrl);
|
|
5155
5267
|
return result;
|
|
@@ -5165,7 +5277,7 @@ function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAge
|
|
|
5165
5277
|
}
|
|
5166
5278
|
});
|
|
5167
5279
|
}
|
|
5168
|
-
return load;
|
|
5280
|
+
return (url, options) => load(url, options);
|
|
5169
5281
|
}
|
|
5170
5282
|
//#endregion
|
|
5171
5283
|
//#region src/internal/multicodec.ts
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
require("./chunk-Do9eywBl.cjs");
|
|
2
|
+
const require_request = require("./request-DrO5xzev.cjs");
|
|
3
|
+
let _logtape_logtape = require("@logtape/logtape");
|
|
4
|
+
//#region src/body.ts
|
|
5
|
+
/** Decoded JSON body limit: 16 MiB. @internal */
|
|
6
|
+
const MAX_BODY_SIZE = 16 * 1024 * 1024;
|
|
7
|
+
/** A body exceeded the byte limit. @internal */
|
|
8
|
+
var BodyTooLargeError = class extends require_request.FetchError {
|
|
9
|
+
/** Creates an error for a body exceeding the given limit. */
|
|
10
|
+
constructor(url, maxBytes) {
|
|
11
|
+
super(url, `Body exceeds the limit of ${maxBytes} bytes`);
|
|
12
|
+
this.name = "BodyTooLargeError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/** Validates a finite, positive byte limit. @internal */
|
|
16
|
+
function validateBodySizeLimit(maxBytes) {
|
|
17
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Reads UTF-8 text while limiting bytes received from the body stream.
|
|
21
|
+
* Fetch responses are already decompressed, so the stream count is authoritative.
|
|
22
|
+
* @param message The response or request to read.
|
|
23
|
+
* @param maxBytes The maximum number of decoded bytes to read.
|
|
24
|
+
* @param url The URL to include in errors and logs.
|
|
25
|
+
* @returns The decoded body text.
|
|
26
|
+
* @throws {BodyTooLargeError} If the body exceeds the limit.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
async function readBoundedText(message, maxBytes, url) {
|
|
30
|
+
validateBodySizeLimit(maxBytes);
|
|
31
|
+
const reader = message.body?.getReader();
|
|
32
|
+
const tooLarge = () => {
|
|
33
|
+
(0, _logtape_logtape.getLogger)([
|
|
34
|
+
"fedify",
|
|
35
|
+
"runtime",
|
|
36
|
+
"body"
|
|
37
|
+
]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
|
|
38
|
+
url: url.toString(),
|
|
39
|
+
maxBytes
|
|
40
|
+
});
|
|
41
|
+
throw new BodyTooLargeError(url, maxBytes);
|
|
42
|
+
};
|
|
43
|
+
try {
|
|
44
|
+
const length = message.headers.get("Content-Length");
|
|
45
|
+
const encoding = message.headers.get("Content-Encoding");
|
|
46
|
+
if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
|
|
47
|
+
if (reader == null) return "";
|
|
48
|
+
let size = 0;
|
|
49
|
+
const textReader = new ReadableStream({ async pull(controller) {
|
|
50
|
+
const { done, value } = await reader.read();
|
|
51
|
+
if (done) {
|
|
52
|
+
controller.close();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
size += value.byteLength;
|
|
56
|
+
if (size > maxBytes) tooLarge();
|
|
57
|
+
controller.enqueue(value);
|
|
58
|
+
} }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
|
|
59
|
+
try {
|
|
60
|
+
const chunks = [];
|
|
61
|
+
while (true) {
|
|
62
|
+
const { done, value } = await textReader.read();
|
|
63
|
+
if (done) break;
|
|
64
|
+
chunks.push(value);
|
|
65
|
+
}
|
|
66
|
+
return chunks.join("");
|
|
67
|
+
} finally {
|
|
68
|
+
textReader.releaseLock();
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (reader != null) reader.cancel(error).catch(() => {});
|
|
72
|
+
throw error;
|
|
73
|
+
} finally {
|
|
74
|
+
reader?.releaseLock();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
Object.defineProperty(exports, "BodyTooLargeError", {
|
|
79
|
+
enumerable: true,
|
|
80
|
+
get: function() {
|
|
81
|
+
return BodyTooLargeError;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
Object.defineProperty(exports, "MAX_BODY_SIZE", {
|
|
85
|
+
enumerable: true,
|
|
86
|
+
get: function() {
|
|
87
|
+
return MAX_BODY_SIZE;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
Object.defineProperty(exports, "readBoundedText", {
|
|
91
|
+
enumerable: true,
|
|
92
|
+
get: function() {
|
|
93
|
+
return readBoundedText;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { t as FetchError } from "./request-DxeGyJVh.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-D-gC5vdl.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 { };
|