@fedify/vocab-runtime 2.0.28-dev.1974 → 2.0.28-dev.1982
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 +160 -95
- package/dist/mod.js +160 -95
- package/dist/tests/body-CGaJrKL7.mjs +77 -0
- package/dist/tests/body-CtbBbd67.cjs +95 -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/docloader.test.cjs +105 -34
- package/dist/tests/docloader.test.mjs +105 -34
- package/dist/tests/internal/multicodec.test.cjs +2 -2
- package/dist/tests/internal/multicodec.test.mjs +2 -2
- package/dist/tests/key.test.cjs +3 -3
- package/dist/tests/key.test.mjs +3 -3
- package/dist/tests/langstr.test.cjs +1 -1
- package/dist/tests/langstr.test.mjs +1 -1
- 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-C4ZUiyFP.mjs → request-ByxplZyY.mjs} +1 -1
- package/dist/tests/{request-DekYA6RV.cjs → request-Ir4zMwN8.cjs} +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.test.cjs +2 -2
- package/dist/tests/url.test.mjs +2 -2
- package/package.json +1 -1
- package/src/body.test.ts +125 -0
- package/src/body.ts +95 -0
- package/src/docloader.test.ts +124 -1
- package/src/docloader.ts +41 -43
- /package/dist/tests/{link-B6ZWBZhf.cjs → link-FguCydMA.cjs} +0 -0
- /package/dist/tests/{link-B8JGXSS2.mjs → link-NUUWCdnK.mjs} +0 -0
- /package/dist/tests/{multibase-CgYqpk4Z.cjs → multibase-CnmModw0.cjs} +0 -0
- /package/dist/tests/{multibase-jcKrOpuU.mjs → multibase-DLBufb99.mjs} +0 -0
- /package/dist/tests/{multicodec-DeYop8xg.cjs → multicodec-CxGVGa91.cjs} +0 -0
- /package/dist/tests/{multicodec-aqbZnrNi.mjs → multicodec-CyFp54fI.mjs} +0 -0
- /package/dist/tests/{url-CqwkmczT.cjs → url-CWU5HT3Y.cjs} +0 -0
- /package/dist/tests/{url-BulORwtj.mjs → url-o0e-aXQY.mjs} +0 -0
package/deno.json
CHANGED
package/dist/mod.cjs
CHANGED
|
@@ -4434,7 +4434,145 @@ const preloadedContexts = {
|
|
|
4434
4434
|
//#endregion
|
|
4435
4435
|
//#region deno.json
|
|
4436
4436
|
var name = "@fedify/vocab-runtime";
|
|
4437
|
-
var version = "2.0.28-dev.
|
|
4437
|
+
var version = "2.0.28-dev.1982+a57dec3f";
|
|
4438
|
+
//#endregion
|
|
4439
|
+
//#region src/request.ts
|
|
4440
|
+
/**
|
|
4441
|
+
* Error thrown when fetching a JSON-LD document failed.
|
|
4442
|
+
*/
|
|
4443
|
+
var FetchError = class extends Error {
|
|
4444
|
+
/**
|
|
4445
|
+
* The URL that failed to fetch.
|
|
4446
|
+
*/
|
|
4447
|
+
url;
|
|
4448
|
+
/**
|
|
4449
|
+
* Constructs a new `FetchError`.
|
|
4450
|
+
*
|
|
4451
|
+
* @param url The URL that failed to fetch.
|
|
4452
|
+
* @param message Error message.
|
|
4453
|
+
*/
|
|
4454
|
+
constructor(url, message) {
|
|
4455
|
+
super(message == null ? url.toString() : `${url}: ${message}`);
|
|
4456
|
+
this.name = "FetchError";
|
|
4457
|
+
this.url = typeof url === "string" ? new URL(url) : url;
|
|
4458
|
+
}
|
|
4459
|
+
};
|
|
4460
|
+
/**
|
|
4461
|
+
* Creates a request for the given URL.
|
|
4462
|
+
* @param url The URL to create the request for.
|
|
4463
|
+
* @param options The options for the request.
|
|
4464
|
+
* @returns The created request.
|
|
4465
|
+
* @internal
|
|
4466
|
+
*/
|
|
4467
|
+
function createActivityPubRequest(url, options = {}) {
|
|
4468
|
+
return new Request(url, {
|
|
4469
|
+
headers: {
|
|
4470
|
+
Accept: "application/activity+json, application/ld+json",
|
|
4471
|
+
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
4472
|
+
},
|
|
4473
|
+
redirect: "manual"
|
|
4474
|
+
});
|
|
4475
|
+
}
|
|
4476
|
+
/**
|
|
4477
|
+
* Gets the user agent string for the given application and URL.
|
|
4478
|
+
* @param options The options for making the user agent string.
|
|
4479
|
+
* @returns The user agent string.
|
|
4480
|
+
* @since 1.3.0
|
|
4481
|
+
*/
|
|
4482
|
+
function getUserAgent({ software, url } = {}) {
|
|
4483
|
+
const fedify = `Fedify/${version}`;
|
|
4484
|
+
const runtime = globalThis.Deno?.version?.deno != null ? `Deno/${Deno.version.deno}` : globalThis.process?.versions?.bun != null ? `Bun/${node_process.default.versions.bun}` : "navigator" in globalThis && navigator.userAgent === "Cloudflare-Workers" ? navigator.userAgent : globalThis.process?.versions?.node != null ? `Node.js/${node_process.default.versions.node}` : null;
|
|
4485
|
+
const userAgent = software == null ? [fedify] : [software, fedify];
|
|
4486
|
+
if (runtime != null) userAgent.push(runtime);
|
|
4487
|
+
if (url != null) userAgent.push(`+${url.toString()}`);
|
|
4488
|
+
return `${userAgent.shift()} (${userAgent.join("; ")})`;
|
|
4489
|
+
}
|
|
4490
|
+
/**
|
|
4491
|
+
* Logs the request.
|
|
4492
|
+
* @param request The request to log.
|
|
4493
|
+
* @internal
|
|
4494
|
+
*/
|
|
4495
|
+
function logRequest(logger, request) {
|
|
4496
|
+
logger.debug("Fetching document: {method} {url} {headers}", {
|
|
4497
|
+
method: request.method,
|
|
4498
|
+
url: request.url,
|
|
4499
|
+
headers: Object.fromEntries(request.headers.entries())
|
|
4500
|
+
});
|
|
4501
|
+
}
|
|
4502
|
+
//#endregion
|
|
4503
|
+
//#region src/body.ts
|
|
4504
|
+
/** Decoded JSON body limit: 16 MiB. @internal */
|
|
4505
|
+
const MAX_BODY_SIZE = 16 * 1024 * 1024;
|
|
4506
|
+
/** A body exceeded the byte limit. @internal */
|
|
4507
|
+
var BodyTooLargeError = class extends FetchError {
|
|
4508
|
+
/** Creates an error for a body exceeding the given limit. */
|
|
4509
|
+
constructor(url, maxBytes) {
|
|
4510
|
+
super(url, `Body exceeds the limit of ${maxBytes} bytes`);
|
|
4511
|
+
this.name = "BodyTooLargeError";
|
|
4512
|
+
}
|
|
4513
|
+
};
|
|
4514
|
+
/** Validates a finite, positive byte limit. @internal */
|
|
4515
|
+
function validateBodySizeLimit(maxBytes) {
|
|
4516
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
|
|
4517
|
+
}
|
|
4518
|
+
/**
|
|
4519
|
+
* Reads UTF-8 text while limiting bytes received from the body stream.
|
|
4520
|
+
* Fetch responses are already decompressed, so the stream count is authoritative.
|
|
4521
|
+
* @param message The response or request to read.
|
|
4522
|
+
* @param maxBytes The maximum number of decoded bytes to read.
|
|
4523
|
+
* @param url The URL to include in errors and logs.
|
|
4524
|
+
* @returns The decoded body text.
|
|
4525
|
+
* @throws {BodyTooLargeError} If the body exceeds the limit.
|
|
4526
|
+
* @internal
|
|
4527
|
+
*/
|
|
4528
|
+
async function readBoundedText(message, maxBytes, url) {
|
|
4529
|
+
validateBodySizeLimit(maxBytes);
|
|
4530
|
+
const reader = message.body?.getReader();
|
|
4531
|
+
const tooLarge = () => {
|
|
4532
|
+
(0, _logtape_logtape.getLogger)([
|
|
4533
|
+
"fedify",
|
|
4534
|
+
"runtime",
|
|
4535
|
+
"body"
|
|
4536
|
+
]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
|
|
4537
|
+
url: url.toString(),
|
|
4538
|
+
maxBytes
|
|
4539
|
+
});
|
|
4540
|
+
throw new BodyTooLargeError(url, maxBytes);
|
|
4541
|
+
};
|
|
4542
|
+
try {
|
|
4543
|
+
const length = message.headers.get("Content-Length");
|
|
4544
|
+
const encoding = message.headers.get("Content-Encoding");
|
|
4545
|
+
if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
|
|
4546
|
+
if (reader == null) return "";
|
|
4547
|
+
let size = 0;
|
|
4548
|
+
const textReader = new ReadableStream({ async pull(controller) {
|
|
4549
|
+
const { done, value } = await reader.read();
|
|
4550
|
+
if (done) {
|
|
4551
|
+
controller.close();
|
|
4552
|
+
return;
|
|
4553
|
+
}
|
|
4554
|
+
size += value.byteLength;
|
|
4555
|
+
if (size > maxBytes) tooLarge();
|
|
4556
|
+
controller.enqueue(value);
|
|
4557
|
+
} }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
|
|
4558
|
+
try {
|
|
4559
|
+
const chunks = [];
|
|
4560
|
+
while (true) {
|
|
4561
|
+
const { done, value } = await textReader.read();
|
|
4562
|
+
if (done) break;
|
|
4563
|
+
chunks.push(value);
|
|
4564
|
+
}
|
|
4565
|
+
return chunks.join("");
|
|
4566
|
+
} finally {
|
|
4567
|
+
textReader.releaseLock();
|
|
4568
|
+
}
|
|
4569
|
+
} catch (error) {
|
|
4570
|
+
if (reader != null) reader.cancel(error).catch(() => {});
|
|
4571
|
+
throw error;
|
|
4572
|
+
} finally {
|
|
4573
|
+
reader?.releaseLock();
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4438
4576
|
//#endregion
|
|
4439
4577
|
//#region src/link.ts
|
|
4440
4578
|
const parametersNeedLowerCase = ["rel", "type"];
|
|
@@ -4617,70 +4755,6 @@ var HttpHeaderLink = class HttpHeaderLink {
|
|
|
4617
4755
|
}
|
|
4618
4756
|
};
|
|
4619
4757
|
//#endregion
|
|
4620
|
-
//#region src/request.ts
|
|
4621
|
-
/**
|
|
4622
|
-
* Error thrown when fetching a JSON-LD document failed.
|
|
4623
|
-
*/
|
|
4624
|
-
var FetchError = class extends Error {
|
|
4625
|
-
/**
|
|
4626
|
-
* The URL that failed to fetch.
|
|
4627
|
-
*/
|
|
4628
|
-
url;
|
|
4629
|
-
/**
|
|
4630
|
-
* Constructs a new `FetchError`.
|
|
4631
|
-
*
|
|
4632
|
-
* @param url The URL that failed to fetch.
|
|
4633
|
-
* @param message Error message.
|
|
4634
|
-
*/
|
|
4635
|
-
constructor(url, message) {
|
|
4636
|
-
super(message == null ? url.toString() : `${url}: ${message}`);
|
|
4637
|
-
this.name = "FetchError";
|
|
4638
|
-
this.url = typeof url === "string" ? new URL(url) : url;
|
|
4639
|
-
}
|
|
4640
|
-
};
|
|
4641
|
-
/**
|
|
4642
|
-
* Creates a request for the given URL.
|
|
4643
|
-
* @param url The URL to create the request for.
|
|
4644
|
-
* @param options The options for the request.
|
|
4645
|
-
* @returns The created request.
|
|
4646
|
-
* @internal
|
|
4647
|
-
*/
|
|
4648
|
-
function createActivityPubRequest(url, options = {}) {
|
|
4649
|
-
return new Request(url, {
|
|
4650
|
-
headers: {
|
|
4651
|
-
Accept: "application/activity+json, application/ld+json",
|
|
4652
|
-
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
4653
|
-
},
|
|
4654
|
-
redirect: "manual"
|
|
4655
|
-
});
|
|
4656
|
-
}
|
|
4657
|
-
/**
|
|
4658
|
-
* Gets the user agent string for the given application and URL.
|
|
4659
|
-
* @param options The options for making the user agent string.
|
|
4660
|
-
* @returns The user agent string.
|
|
4661
|
-
* @since 1.3.0
|
|
4662
|
-
*/
|
|
4663
|
-
function getUserAgent({ software, url } = {}) {
|
|
4664
|
-
const fedify = `Fedify/${version}`;
|
|
4665
|
-
const runtime = globalThis.Deno?.version?.deno != null ? `Deno/${Deno.version.deno}` : globalThis.process?.versions?.bun != null ? `Bun/${node_process.default.versions.bun}` : "navigator" in globalThis && navigator.userAgent === "Cloudflare-Workers" ? navigator.userAgent : globalThis.process?.versions?.node != null ? `Node.js/${node_process.default.versions.node}` : null;
|
|
4666
|
-
const userAgent = software == null ? [fedify] : [software, fedify];
|
|
4667
|
-
if (runtime != null) userAgent.push(runtime);
|
|
4668
|
-
if (url != null) userAgent.push(`+${url.toString()}`);
|
|
4669
|
-
return `${userAgent.shift()} (${userAgent.join("; ")})`;
|
|
4670
|
-
}
|
|
4671
|
-
/**
|
|
4672
|
-
* Logs the request.
|
|
4673
|
-
* @param request The request to log.
|
|
4674
|
-
* @internal
|
|
4675
|
-
*/
|
|
4676
|
-
function logRequest(logger, request) {
|
|
4677
|
-
logger.debug("Fetching document: {method} {url} {headers}", {
|
|
4678
|
-
method: request.method,
|
|
4679
|
-
url: request.url,
|
|
4680
|
-
headers: Object.fromEntries(request.headers.entries())
|
|
4681
|
-
});
|
|
4682
|
-
}
|
|
4683
|
-
//#endregion
|
|
4684
4758
|
//#region src/url.ts
|
|
4685
4759
|
var UrlError = class extends Error {
|
|
4686
4760
|
constructor(message, options) {
|
|
@@ -4942,38 +5016,29 @@ async function getRemoteDocument(url, response, fetch) {
|
|
|
4942
5016
|
}
|
|
4943
5017
|
let document;
|
|
4944
5018
|
if (!jsonLd && (contentType === "text/html" || contentType?.startsWith("text/html;") || contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;"))) {
|
|
4945
|
-
const
|
|
4946
|
-
const
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
|
|
4965
|
-
}
|
|
4966
|
-
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) {
|
|
4967
|
-
logger.debug("Found alternate document: {alternateUrl} from {url}", {
|
|
4968
|
-
alternateUrl: attribs.href,
|
|
4969
|
-
url: documentUrl
|
|
4970
|
-
});
|
|
4971
|
-
return await fetch(new URL(attribs.href, docUrl).href);
|
|
4972
|
-
}
|
|
5019
|
+
const html = await readBoundedText(response, 1024 * 1024, documentUrl);
|
|
5020
|
+
const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
|
|
5021
|
+
const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
5022
|
+
let tagMatch;
|
|
5023
|
+
while ((tagMatch = tagPattern.exec(html)) !== null) {
|
|
5024
|
+
const tagContent = tagMatch[2];
|
|
5025
|
+
let attrMatch;
|
|
5026
|
+
const attribs = {};
|
|
5027
|
+
attrPattern.lastIndex = 0;
|
|
5028
|
+
while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
|
|
5029
|
+
const key = attrMatch[1].toLowerCase();
|
|
5030
|
+
attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
|
|
5031
|
+
}
|
|
5032
|
+
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) {
|
|
5033
|
+
logger.debug("Found alternate document: {alternateUrl} from {url}", {
|
|
5034
|
+
alternateUrl: attribs.href,
|
|
5035
|
+
url: documentUrl
|
|
5036
|
+
});
|
|
5037
|
+
return await fetch(new URL(attribs.href, docUrl).href);
|
|
4973
5038
|
}
|
|
4974
|
-
document = JSON.parse(html);
|
|
4975
5039
|
}
|
|
4976
|
-
|
|
5040
|
+
document = JSON.parse(html);
|
|
5041
|
+
} else document = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, documentUrl));
|
|
4977
5042
|
logger.debug("Fetched document: {status} {url} {headers}", {
|
|
4978
5043
|
status: response.status,
|
|
4979
5044
|
url: documentUrl,
|
package/dist/mod.js
CHANGED
|
@@ -4430,7 +4430,145 @@ const preloadedContexts = {
|
|
|
4430
4430
|
//#endregion
|
|
4431
4431
|
//#region deno.json
|
|
4432
4432
|
var name = "@fedify/vocab-runtime";
|
|
4433
|
-
var version = "2.0.28-dev.
|
|
4433
|
+
var version = "2.0.28-dev.1982+a57dec3f";
|
|
4434
|
+
//#endregion
|
|
4435
|
+
//#region src/request.ts
|
|
4436
|
+
/**
|
|
4437
|
+
* Error thrown when fetching a JSON-LD document failed.
|
|
4438
|
+
*/
|
|
4439
|
+
var FetchError = class extends Error {
|
|
4440
|
+
/**
|
|
4441
|
+
* The URL that failed to fetch.
|
|
4442
|
+
*/
|
|
4443
|
+
url;
|
|
4444
|
+
/**
|
|
4445
|
+
* Constructs a new `FetchError`.
|
|
4446
|
+
*
|
|
4447
|
+
* @param url The URL that failed to fetch.
|
|
4448
|
+
* @param message Error message.
|
|
4449
|
+
*/
|
|
4450
|
+
constructor(url, message) {
|
|
4451
|
+
super(message == null ? url.toString() : `${url}: ${message}`);
|
|
4452
|
+
this.name = "FetchError";
|
|
4453
|
+
this.url = typeof url === "string" ? new URL(url) : url;
|
|
4454
|
+
}
|
|
4455
|
+
};
|
|
4456
|
+
/**
|
|
4457
|
+
* Creates a request for the given URL.
|
|
4458
|
+
* @param url The URL to create the request for.
|
|
4459
|
+
* @param options The options for the request.
|
|
4460
|
+
* @returns The created request.
|
|
4461
|
+
* @internal
|
|
4462
|
+
*/
|
|
4463
|
+
function createActivityPubRequest(url, options = {}) {
|
|
4464
|
+
return new Request(url, {
|
|
4465
|
+
headers: {
|
|
4466
|
+
Accept: "application/activity+json, application/ld+json",
|
|
4467
|
+
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
4468
|
+
},
|
|
4469
|
+
redirect: "manual"
|
|
4470
|
+
});
|
|
4471
|
+
}
|
|
4472
|
+
/**
|
|
4473
|
+
* Gets the user agent string for the given application and URL.
|
|
4474
|
+
* @param options The options for making the user agent string.
|
|
4475
|
+
* @returns The user agent string.
|
|
4476
|
+
* @since 1.3.0
|
|
4477
|
+
*/
|
|
4478
|
+
function getUserAgent({ software, url } = {}) {
|
|
4479
|
+
const fedify = `Fedify/${version}`;
|
|
4480
|
+
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;
|
|
4481
|
+
const userAgent = software == null ? [fedify] : [software, fedify];
|
|
4482
|
+
if (runtime != null) userAgent.push(runtime);
|
|
4483
|
+
if (url != null) userAgent.push(`+${url.toString()}`);
|
|
4484
|
+
return `${userAgent.shift()} (${userAgent.join("; ")})`;
|
|
4485
|
+
}
|
|
4486
|
+
/**
|
|
4487
|
+
* Logs the request.
|
|
4488
|
+
* @param request The request to log.
|
|
4489
|
+
* @internal
|
|
4490
|
+
*/
|
|
4491
|
+
function logRequest(logger, request) {
|
|
4492
|
+
logger.debug("Fetching document: {method} {url} {headers}", {
|
|
4493
|
+
method: request.method,
|
|
4494
|
+
url: request.url,
|
|
4495
|
+
headers: Object.fromEntries(request.headers.entries())
|
|
4496
|
+
});
|
|
4497
|
+
}
|
|
4498
|
+
//#endregion
|
|
4499
|
+
//#region src/body.ts
|
|
4500
|
+
/** Decoded JSON body limit: 16 MiB. @internal */
|
|
4501
|
+
const MAX_BODY_SIZE = 16 * 1024 * 1024;
|
|
4502
|
+
/** A body exceeded the byte limit. @internal */
|
|
4503
|
+
var BodyTooLargeError = class extends FetchError {
|
|
4504
|
+
/** Creates an error for a body exceeding the given limit. */
|
|
4505
|
+
constructor(url, maxBytes) {
|
|
4506
|
+
super(url, `Body exceeds the limit of ${maxBytes} bytes`);
|
|
4507
|
+
this.name = "BodyTooLargeError";
|
|
4508
|
+
}
|
|
4509
|
+
};
|
|
4510
|
+
/** Validates a finite, positive byte limit. @internal */
|
|
4511
|
+
function validateBodySizeLimit(maxBytes) {
|
|
4512
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
|
|
4513
|
+
}
|
|
4514
|
+
/**
|
|
4515
|
+
* Reads UTF-8 text while limiting bytes received from the body stream.
|
|
4516
|
+
* Fetch responses are already decompressed, so the stream count is authoritative.
|
|
4517
|
+
* @param message The response or request to read.
|
|
4518
|
+
* @param maxBytes The maximum number of decoded bytes to read.
|
|
4519
|
+
* @param url The URL to include in errors and logs.
|
|
4520
|
+
* @returns The decoded body text.
|
|
4521
|
+
* @throws {BodyTooLargeError} If the body exceeds the limit.
|
|
4522
|
+
* @internal
|
|
4523
|
+
*/
|
|
4524
|
+
async function readBoundedText(message, maxBytes, url) {
|
|
4525
|
+
validateBodySizeLimit(maxBytes);
|
|
4526
|
+
const reader = message.body?.getReader();
|
|
4527
|
+
const tooLarge = () => {
|
|
4528
|
+
getLogger([
|
|
4529
|
+
"fedify",
|
|
4530
|
+
"runtime",
|
|
4531
|
+
"body"
|
|
4532
|
+
]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
|
|
4533
|
+
url: url.toString(),
|
|
4534
|
+
maxBytes
|
|
4535
|
+
});
|
|
4536
|
+
throw new BodyTooLargeError(url, maxBytes);
|
|
4537
|
+
};
|
|
4538
|
+
try {
|
|
4539
|
+
const length = message.headers.get("Content-Length");
|
|
4540
|
+
const encoding = message.headers.get("Content-Encoding");
|
|
4541
|
+
if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
|
|
4542
|
+
if (reader == null) return "";
|
|
4543
|
+
let size = 0;
|
|
4544
|
+
const textReader = new ReadableStream({ async pull(controller) {
|
|
4545
|
+
const { done, value } = await reader.read();
|
|
4546
|
+
if (done) {
|
|
4547
|
+
controller.close();
|
|
4548
|
+
return;
|
|
4549
|
+
}
|
|
4550
|
+
size += value.byteLength;
|
|
4551
|
+
if (size > maxBytes) tooLarge();
|
|
4552
|
+
controller.enqueue(value);
|
|
4553
|
+
} }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
|
|
4554
|
+
try {
|
|
4555
|
+
const chunks = [];
|
|
4556
|
+
while (true) {
|
|
4557
|
+
const { done, value } = await textReader.read();
|
|
4558
|
+
if (done) break;
|
|
4559
|
+
chunks.push(value);
|
|
4560
|
+
}
|
|
4561
|
+
return chunks.join("");
|
|
4562
|
+
} finally {
|
|
4563
|
+
textReader.releaseLock();
|
|
4564
|
+
}
|
|
4565
|
+
} catch (error) {
|
|
4566
|
+
if (reader != null) reader.cancel(error).catch(() => {});
|
|
4567
|
+
throw error;
|
|
4568
|
+
} finally {
|
|
4569
|
+
reader?.releaseLock();
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4434
4572
|
//#endregion
|
|
4435
4573
|
//#region src/link.ts
|
|
4436
4574
|
const parametersNeedLowerCase = ["rel", "type"];
|
|
@@ -4613,70 +4751,6 @@ var HttpHeaderLink = class HttpHeaderLink {
|
|
|
4613
4751
|
}
|
|
4614
4752
|
};
|
|
4615
4753
|
//#endregion
|
|
4616
|
-
//#region src/request.ts
|
|
4617
|
-
/**
|
|
4618
|
-
* Error thrown when fetching a JSON-LD document failed.
|
|
4619
|
-
*/
|
|
4620
|
-
var FetchError = class extends Error {
|
|
4621
|
-
/**
|
|
4622
|
-
* The URL that failed to fetch.
|
|
4623
|
-
*/
|
|
4624
|
-
url;
|
|
4625
|
-
/**
|
|
4626
|
-
* Constructs a new `FetchError`.
|
|
4627
|
-
*
|
|
4628
|
-
* @param url The URL that failed to fetch.
|
|
4629
|
-
* @param message Error message.
|
|
4630
|
-
*/
|
|
4631
|
-
constructor(url, message) {
|
|
4632
|
-
super(message == null ? url.toString() : `${url}: ${message}`);
|
|
4633
|
-
this.name = "FetchError";
|
|
4634
|
-
this.url = typeof url === "string" ? new URL(url) : url;
|
|
4635
|
-
}
|
|
4636
|
-
};
|
|
4637
|
-
/**
|
|
4638
|
-
* Creates a request for the given URL.
|
|
4639
|
-
* @param url The URL to create the request for.
|
|
4640
|
-
* @param options The options for the request.
|
|
4641
|
-
* @returns The created request.
|
|
4642
|
-
* @internal
|
|
4643
|
-
*/
|
|
4644
|
-
function createActivityPubRequest(url, options = {}) {
|
|
4645
|
-
return new Request(url, {
|
|
4646
|
-
headers: {
|
|
4647
|
-
Accept: "application/activity+json, application/ld+json",
|
|
4648
|
-
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
4649
|
-
},
|
|
4650
|
-
redirect: "manual"
|
|
4651
|
-
});
|
|
4652
|
-
}
|
|
4653
|
-
/**
|
|
4654
|
-
* Gets the user agent string for the given application and URL.
|
|
4655
|
-
* @param options The options for making the user agent string.
|
|
4656
|
-
* @returns The user agent string.
|
|
4657
|
-
* @since 1.3.0
|
|
4658
|
-
*/
|
|
4659
|
-
function getUserAgent({ software, url } = {}) {
|
|
4660
|
-
const fedify = `Fedify/${version}`;
|
|
4661
|
-
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;
|
|
4662
|
-
const userAgent = software == null ? [fedify] : [software, fedify];
|
|
4663
|
-
if (runtime != null) userAgent.push(runtime);
|
|
4664
|
-
if (url != null) userAgent.push(`+${url.toString()}`);
|
|
4665
|
-
return `${userAgent.shift()} (${userAgent.join("; ")})`;
|
|
4666
|
-
}
|
|
4667
|
-
/**
|
|
4668
|
-
* Logs the request.
|
|
4669
|
-
* @param request The request to log.
|
|
4670
|
-
* @internal
|
|
4671
|
-
*/
|
|
4672
|
-
function logRequest(logger, request) {
|
|
4673
|
-
logger.debug("Fetching document: {method} {url} {headers}", {
|
|
4674
|
-
method: request.method,
|
|
4675
|
-
url: request.url,
|
|
4676
|
-
headers: Object.fromEntries(request.headers.entries())
|
|
4677
|
-
});
|
|
4678
|
-
}
|
|
4679
|
-
//#endregion
|
|
4680
4754
|
//#region src/url.ts
|
|
4681
4755
|
var UrlError = class extends Error {
|
|
4682
4756
|
constructor(message, options) {
|
|
@@ -4938,38 +5012,29 @@ async function getRemoteDocument(url, response, fetch) {
|
|
|
4938
5012
|
}
|
|
4939
5013
|
let document;
|
|
4940
5014
|
if (!jsonLd && (contentType === "text/html" || contentType?.startsWith("text/html;") || contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;"))) {
|
|
4941
|
-
const
|
|
4942
|
-
const
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
|
|
4961
|
-
}
|
|
4962
|
-
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) {
|
|
4963
|
-
logger.debug("Found alternate document: {alternateUrl} from {url}", {
|
|
4964
|
-
alternateUrl: attribs.href,
|
|
4965
|
-
url: documentUrl
|
|
4966
|
-
});
|
|
4967
|
-
return await fetch(new URL(attribs.href, docUrl).href);
|
|
4968
|
-
}
|
|
5015
|
+
const html = await readBoundedText(response, 1024 * 1024, documentUrl);
|
|
5016
|
+
const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
|
|
5017
|
+
const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
5018
|
+
let tagMatch;
|
|
5019
|
+
while ((tagMatch = tagPattern.exec(html)) !== null) {
|
|
5020
|
+
const tagContent = tagMatch[2];
|
|
5021
|
+
let attrMatch;
|
|
5022
|
+
const attribs = {};
|
|
5023
|
+
attrPattern.lastIndex = 0;
|
|
5024
|
+
while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
|
|
5025
|
+
const key = attrMatch[1].toLowerCase();
|
|
5026
|
+
attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
|
|
5027
|
+
}
|
|
5028
|
+
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) {
|
|
5029
|
+
logger.debug("Found alternate document: {alternateUrl} from {url}", {
|
|
5030
|
+
alternateUrl: attribs.href,
|
|
5031
|
+
url: documentUrl
|
|
5032
|
+
});
|
|
5033
|
+
return await fetch(new URL(attribs.href, docUrl).href);
|
|
4969
5034
|
}
|
|
4970
|
-
document = JSON.parse(html);
|
|
4971
5035
|
}
|
|
4972
|
-
|
|
5036
|
+
document = JSON.parse(html);
|
|
5037
|
+
} else document = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, documentUrl));
|
|
4973
5038
|
logger.debug("Fetched document: {status} {url} {headers}", {
|
|
4974
5039
|
status: response.status,
|
|
4975
5040
|
url: documentUrl,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { t as FetchError } from "./request-ByxplZyY.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 };
|