@fedify/vocab-runtime 2.4.0-dev.1962 → 2.4.0-dev.1988
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 +204 -152
- package/dist/mod.js +204 -152
- package/dist/tests/body-Cg5W_IdS.cjs +94 -0
- package/dist/tests/body-DRVriaJO.mjs +77 -0
- package/dist/tests/body.test.cjs +89 -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 +2 -2
- package/dist/tests/decimal.test.mjs +2 -2
- package/dist/tests/digest.test.cjs +1 -1
- package/dist/tests/digest.test.mjs +1 -1
- package/dist/tests/{docloader-cQ5_cV6R.mjs → docloader-ZSduXDDg.mjs} +61 -82
- package/dist/tests/{docloader-CbeAECB4.cjs → docloader-j_N6ZEZx.cjs} +61 -82
- package/dist/tests/docloader.test.cjs +84 -5
- package/dist/tests/docloader.test.mjs +84 -5
- package/dist/tests/internal/multicodec.test.cjs +1 -1
- package/dist/tests/internal/multicodec.test.mjs +1 -1
- package/dist/tests/jsonld-cache.test.cjs +1 -1
- package/dist/tests/jsonld-cache.test.mjs +1 -1
- package/dist/tests/key.test.cjs +1 -1
- package/dist/tests/key.test.mjs +1 -1
- package/dist/tests/langstr.test.cjs +1 -1
- package/dist/tests/langstr.test.mjs +1 -1
- package/dist/tests/link.test.cjs +1 -1
- package/dist/tests/link.test.mjs +1 -1
- package/dist/tests/multibase/multibase.test.cjs +1 -1
- package/dist/tests/multibase/multibase.test.mjs +1 -1
- package/dist/tests/{request-me46Cb_a.cjs → request-B3YkikPO.cjs} +1 -1
- package/dist/tests/{request-CywSvOoa.mjs → request-Bo1iqGDW.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.test.cjs +1 -1
- package/dist/tests/url.test.mjs +1 -1
- package/package.json +1 -1
- package/src/body.test.ts +125 -0
- package/src/body.ts +95 -0
- package/src/contexts/fep-22cd.json +21 -0
- package/src/contexts.ts +2 -0
- package/src/docloader.test.ts +128 -3
- package/src/docloader.ts +52 -93
package/dist/mod.js
CHANGED
|
@@ -4245,6 +4245,34 @@ const preloadedContexts = {
|
|
|
4245
4245
|
"@type": "@id"
|
|
4246
4246
|
}
|
|
4247
4247
|
} },
|
|
4248
|
+
"https://w3id.org/fep/22cd": { "@context": {
|
|
4249
|
+
"fep-22cd": "https://w3id.org/fep/22cd#",
|
|
4250
|
+
"schema": "https://schema.org/",
|
|
4251
|
+
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
|
4252
|
+
"translations": {
|
|
4253
|
+
"@id": "fep-22cd:translations",
|
|
4254
|
+
"@container": "@set"
|
|
4255
|
+
},
|
|
4256
|
+
"Translation": "fep-22cd:Translation",
|
|
4257
|
+
"sourceUpdated": {
|
|
4258
|
+
"@id": "fep-22cd:sourceUpdated",
|
|
4259
|
+
"@type": "xsd:dateTime"
|
|
4260
|
+
},
|
|
4261
|
+
"translator": {
|
|
4262
|
+
"@id": "schema:translator",
|
|
4263
|
+
"@type": "@id",
|
|
4264
|
+
"@container": "@set"
|
|
4265
|
+
},
|
|
4266
|
+
"inLanguage": "schema:inLanguage",
|
|
4267
|
+
"translationOfWork": {
|
|
4268
|
+
"@id": "schema:translationOfWork",
|
|
4269
|
+
"@type": "@id"
|
|
4270
|
+
},
|
|
4271
|
+
"isBasedOn": {
|
|
4272
|
+
"@id": "schema:isBasedOn",
|
|
4273
|
+
"@type": "@id"
|
|
4274
|
+
}
|
|
4275
|
+
} },
|
|
4248
4276
|
"https://w3id.org/fep/5711": { "@context": {
|
|
4249
4277
|
"likesOf": {
|
|
4250
4278
|
"@id": "https://w3id.org/fep/5711#likesOf",
|
|
@@ -4489,7 +4517,151 @@ const preloadedContexts = {
|
|
|
4489
4517
|
//#endregion
|
|
4490
4518
|
//#region deno.json
|
|
4491
4519
|
var name = "@fedify/vocab-runtime";
|
|
4492
|
-
var version = "2.4.0-dev.
|
|
4520
|
+
var version = "2.4.0-dev.1988+c5c9d617";
|
|
4521
|
+
//#endregion
|
|
4522
|
+
//#region src/request.ts
|
|
4523
|
+
/**
|
|
4524
|
+
* Error thrown when fetching a JSON-LD document failed.
|
|
4525
|
+
*/
|
|
4526
|
+
var FetchError = class extends Error {
|
|
4527
|
+
/**
|
|
4528
|
+
* The URL that failed to fetch.
|
|
4529
|
+
*/
|
|
4530
|
+
url;
|
|
4531
|
+
/**
|
|
4532
|
+
* The HTTP response that failed, if available.
|
|
4533
|
+
*/
|
|
4534
|
+
response;
|
|
4535
|
+
/**
|
|
4536
|
+
* Constructs a new `FetchError`.
|
|
4537
|
+
*
|
|
4538
|
+
* @param url The URL that failed to fetch.
|
|
4539
|
+
* @param message Error message.
|
|
4540
|
+
* @param response The failed HTTP response, if available.
|
|
4541
|
+
*/
|
|
4542
|
+
constructor(url, message, response) {
|
|
4543
|
+
super(message == null ? url.toString() : `${url}: ${message}`);
|
|
4544
|
+
this.name = "FetchError";
|
|
4545
|
+
this.url = typeof url === "string" ? new URL(url) : url;
|
|
4546
|
+
this.response = response;
|
|
4547
|
+
}
|
|
4548
|
+
};
|
|
4549
|
+
/**
|
|
4550
|
+
* Creates a request for the given URL.
|
|
4551
|
+
* @param url The URL to create the request for.
|
|
4552
|
+
* @param options The options for the request.
|
|
4553
|
+
* @returns The created request.
|
|
4554
|
+
* @internal
|
|
4555
|
+
*/
|
|
4556
|
+
function createActivityPubRequest(url, options = {}) {
|
|
4557
|
+
return new Request(url, {
|
|
4558
|
+
headers: {
|
|
4559
|
+
Accept: "application/activity+json, application/ld+json",
|
|
4560
|
+
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
4561
|
+
},
|
|
4562
|
+
redirect: "manual"
|
|
4563
|
+
});
|
|
4564
|
+
}
|
|
4565
|
+
/**
|
|
4566
|
+
* Gets the user agent string for the given application and URL.
|
|
4567
|
+
* @param options The options for making the user agent string.
|
|
4568
|
+
* @returns The user agent string.
|
|
4569
|
+
* @since 1.3.0
|
|
4570
|
+
*/
|
|
4571
|
+
function getUserAgent({ software, url } = {}) {
|
|
4572
|
+
const fedify = `Fedify/${version}`;
|
|
4573
|
+
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;
|
|
4574
|
+
const userAgent = software == null ? [fedify] : [software, fedify];
|
|
4575
|
+
if (runtime != null) userAgent.push(runtime);
|
|
4576
|
+
if (url != null) userAgent.push(`+${url.toString()}`);
|
|
4577
|
+
return `${userAgent.shift()} (${userAgent.join("; ")})`;
|
|
4578
|
+
}
|
|
4579
|
+
/**
|
|
4580
|
+
* Logs the request.
|
|
4581
|
+
* @param request The request to log.
|
|
4582
|
+
* @internal
|
|
4583
|
+
*/
|
|
4584
|
+
function logRequest(logger, request) {
|
|
4585
|
+
logger.debug("Fetching document: {method} {url} {headers}", {
|
|
4586
|
+
method: request.method,
|
|
4587
|
+
url: request.url,
|
|
4588
|
+
headers: Object.fromEntries(request.headers.entries())
|
|
4589
|
+
});
|
|
4590
|
+
}
|
|
4591
|
+
//#endregion
|
|
4592
|
+
//#region src/body.ts
|
|
4593
|
+
/** Decoded JSON body limit: 16 MiB. @internal */
|
|
4594
|
+
const MAX_BODY_SIZE = 16 * 1024 * 1024;
|
|
4595
|
+
/** A body exceeded the byte limit. @internal */
|
|
4596
|
+
var BodyTooLargeError = class extends FetchError {
|
|
4597
|
+
/** Creates an error for a body exceeding the given limit. */
|
|
4598
|
+
constructor(url, maxBytes) {
|
|
4599
|
+
super(url, `Body exceeds the limit of ${maxBytes} bytes`);
|
|
4600
|
+
this.name = "BodyTooLargeError";
|
|
4601
|
+
}
|
|
4602
|
+
};
|
|
4603
|
+
/** Validates a finite, positive byte limit. @internal */
|
|
4604
|
+
function validateBodySizeLimit(maxBytes) {
|
|
4605
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
|
|
4606
|
+
}
|
|
4607
|
+
/**
|
|
4608
|
+
* Reads UTF-8 text while limiting bytes received from the body stream.
|
|
4609
|
+
* Fetch responses are already decompressed, so the stream count is authoritative.
|
|
4610
|
+
* @param message The response or request to read.
|
|
4611
|
+
* @param maxBytes The maximum number of decoded bytes to read.
|
|
4612
|
+
* @param url The URL to include in errors and logs.
|
|
4613
|
+
* @returns The decoded body text.
|
|
4614
|
+
* @throws {BodyTooLargeError} If the body exceeds the limit.
|
|
4615
|
+
* @internal
|
|
4616
|
+
*/
|
|
4617
|
+
async function readBoundedText(message, maxBytes, url) {
|
|
4618
|
+
validateBodySizeLimit(maxBytes);
|
|
4619
|
+
const reader = message.body?.getReader();
|
|
4620
|
+
const tooLarge = () => {
|
|
4621
|
+
getLogger([
|
|
4622
|
+
"fedify",
|
|
4623
|
+
"runtime",
|
|
4624
|
+
"body"
|
|
4625
|
+
]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
|
|
4626
|
+
url: url.toString(),
|
|
4627
|
+
maxBytes
|
|
4628
|
+
});
|
|
4629
|
+
throw new BodyTooLargeError(url, maxBytes);
|
|
4630
|
+
};
|
|
4631
|
+
try {
|
|
4632
|
+
const length = message.headers.get("Content-Length");
|
|
4633
|
+
const encoding = message.headers.get("Content-Encoding");
|
|
4634
|
+
if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
|
|
4635
|
+
if (reader == null) return "";
|
|
4636
|
+
let size = 0;
|
|
4637
|
+
const textReader = new ReadableStream({ async pull(controller) {
|
|
4638
|
+
const { done, value } = await reader.read();
|
|
4639
|
+
if (done) {
|
|
4640
|
+
controller.close();
|
|
4641
|
+
return;
|
|
4642
|
+
}
|
|
4643
|
+
size += value.byteLength;
|
|
4644
|
+
if (size > maxBytes) tooLarge();
|
|
4645
|
+
controller.enqueue(value);
|
|
4646
|
+
} }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
|
|
4647
|
+
try {
|
|
4648
|
+
const chunks = [];
|
|
4649
|
+
while (true) {
|
|
4650
|
+
const { done, value } = await textReader.read();
|
|
4651
|
+
if (done) break;
|
|
4652
|
+
chunks.push(value);
|
|
4653
|
+
}
|
|
4654
|
+
return chunks.join("");
|
|
4655
|
+
} finally {
|
|
4656
|
+
textReader.releaseLock();
|
|
4657
|
+
}
|
|
4658
|
+
} catch (error) {
|
|
4659
|
+
if (reader != null) reader.cancel(error).catch(() => {});
|
|
4660
|
+
throw error;
|
|
4661
|
+
} finally {
|
|
4662
|
+
reader?.releaseLock();
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4493
4665
|
//#endregion
|
|
4494
4666
|
//#region src/link.ts
|
|
4495
4667
|
const parametersNeedLowerCase = ["rel", "type"];
|
|
@@ -4672,76 +4844,6 @@ var HttpHeaderLink = class HttpHeaderLink {
|
|
|
4672
4844
|
}
|
|
4673
4845
|
};
|
|
4674
4846
|
//#endregion
|
|
4675
|
-
//#region src/request.ts
|
|
4676
|
-
/**
|
|
4677
|
-
* Error thrown when fetching a JSON-LD document failed.
|
|
4678
|
-
*/
|
|
4679
|
-
var FetchError = class extends Error {
|
|
4680
|
-
/**
|
|
4681
|
-
* The URL that failed to fetch.
|
|
4682
|
-
*/
|
|
4683
|
-
url;
|
|
4684
|
-
/**
|
|
4685
|
-
* The HTTP response that failed, if available.
|
|
4686
|
-
*/
|
|
4687
|
-
response;
|
|
4688
|
-
/**
|
|
4689
|
-
* Constructs a new `FetchError`.
|
|
4690
|
-
*
|
|
4691
|
-
* @param url The URL that failed to fetch.
|
|
4692
|
-
* @param message Error message.
|
|
4693
|
-
* @param response The failed HTTP response, if available.
|
|
4694
|
-
*/
|
|
4695
|
-
constructor(url, message, response) {
|
|
4696
|
-
super(message == null ? url.toString() : `${url}: ${message}`);
|
|
4697
|
-
this.name = "FetchError";
|
|
4698
|
-
this.url = typeof url === "string" ? new URL(url) : url;
|
|
4699
|
-
this.response = response;
|
|
4700
|
-
}
|
|
4701
|
-
};
|
|
4702
|
-
/**
|
|
4703
|
-
* Creates a request for the given URL.
|
|
4704
|
-
* @param url The URL to create the request for.
|
|
4705
|
-
* @param options The options for the request.
|
|
4706
|
-
* @returns The created request.
|
|
4707
|
-
* @internal
|
|
4708
|
-
*/
|
|
4709
|
-
function createActivityPubRequest(url, options = {}) {
|
|
4710
|
-
return new Request(url, {
|
|
4711
|
-
headers: {
|
|
4712
|
-
Accept: "application/activity+json, application/ld+json",
|
|
4713
|
-
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
4714
|
-
},
|
|
4715
|
-
redirect: "manual"
|
|
4716
|
-
});
|
|
4717
|
-
}
|
|
4718
|
-
/**
|
|
4719
|
-
* Gets the user agent string for the given application and URL.
|
|
4720
|
-
* @param options The options for making the user agent string.
|
|
4721
|
-
* @returns The user agent string.
|
|
4722
|
-
* @since 1.3.0
|
|
4723
|
-
*/
|
|
4724
|
-
function getUserAgent({ software, url } = {}) {
|
|
4725
|
-
const fedify = `Fedify/${version}`;
|
|
4726
|
-
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;
|
|
4727
|
-
const userAgent = software == null ? [fedify] : [software, fedify];
|
|
4728
|
-
if (runtime != null) userAgent.push(runtime);
|
|
4729
|
-
if (url != null) userAgent.push(`+${url.toString()}`);
|
|
4730
|
-
return `${userAgent.shift()} (${userAgent.join("; ")})`;
|
|
4731
|
-
}
|
|
4732
|
-
/**
|
|
4733
|
-
* Logs the request.
|
|
4734
|
-
* @param request The request to log.
|
|
4735
|
-
* @internal
|
|
4736
|
-
*/
|
|
4737
|
-
function logRequest(logger, request) {
|
|
4738
|
-
logger.debug("Fetching document: {method} {url} {headers}", {
|
|
4739
|
-
method: request.method,
|
|
4740
|
-
url: request.url,
|
|
4741
|
-
headers: Object.fromEntries(request.headers.entries())
|
|
4742
|
-
});
|
|
4743
|
-
}
|
|
4744
|
-
//#endregion
|
|
4745
4847
|
//#region src/docloader.ts
|
|
4746
4848
|
const logger = getLogger([
|
|
4747
4849
|
"fedify",
|
|
@@ -4757,58 +4859,6 @@ function createResponseMetadata(response) {
|
|
|
4757
4859
|
statusText: response.statusText
|
|
4758
4860
|
});
|
|
4759
4861
|
}
|
|
4760
|
-
async function cancelResponseBody(response) {
|
|
4761
|
-
if (response.body != null) await response.body.cancel();
|
|
4762
|
-
}
|
|
4763
|
-
async function readBoundedText(response, maxBytes) {
|
|
4764
|
-
const contentLength = response.headers.get("Content-Length");
|
|
4765
|
-
if (contentLength != null) {
|
|
4766
|
-
const size = Number(contentLength);
|
|
4767
|
-
if (size > maxBytes) {
|
|
4768
|
-
await cancelResponseBody(response);
|
|
4769
|
-
return {
|
|
4770
|
-
text: "",
|
|
4771
|
-
size,
|
|
4772
|
-
tooLarge: true
|
|
4773
|
-
};
|
|
4774
|
-
}
|
|
4775
|
-
}
|
|
4776
|
-
if (response.body == null) return {
|
|
4777
|
-
text: "",
|
|
4778
|
-
size: 0,
|
|
4779
|
-
tooLarge: false
|
|
4780
|
-
};
|
|
4781
|
-
const reader = response.body.getReader();
|
|
4782
|
-
const decoder = new TextDecoder();
|
|
4783
|
-
let text = "";
|
|
4784
|
-
let size = 0;
|
|
4785
|
-
try {
|
|
4786
|
-
while (true) {
|
|
4787
|
-
const result = await reader.read();
|
|
4788
|
-
if (result.done) break;
|
|
4789
|
-
const chunkSize = result.value.byteLength;
|
|
4790
|
-
if (size + chunkSize > maxBytes) {
|
|
4791
|
-
size += chunkSize;
|
|
4792
|
-
await reader.cancel();
|
|
4793
|
-
return {
|
|
4794
|
-
text: "",
|
|
4795
|
-
size,
|
|
4796
|
-
tooLarge: true
|
|
4797
|
-
};
|
|
4798
|
-
}
|
|
4799
|
-
size += chunkSize;
|
|
4800
|
-
text += decoder.decode(result.value, { stream: true });
|
|
4801
|
-
}
|
|
4802
|
-
text += decoder.decode();
|
|
4803
|
-
return {
|
|
4804
|
-
text,
|
|
4805
|
-
size,
|
|
4806
|
-
tooLarge: false
|
|
4807
|
-
};
|
|
4808
|
-
} finally {
|
|
4809
|
-
reader.releaseLock();
|
|
4810
|
-
}
|
|
4811
|
-
}
|
|
4812
4862
|
/**
|
|
4813
4863
|
* Gets a {@link RemoteDocument} from the given response.
|
|
4814
4864
|
* @param url The URL of the document to load.
|
|
@@ -4864,42 +4914,44 @@ async function getRemoteDocument(url, response, fetch) {
|
|
|
4864
4914
|
let document;
|
|
4865
4915
|
if (!jsonLd && (contentType === "text/html" || contentType?.startsWith("text/html;") || contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;"))) {
|
|
4866
4916
|
const errorResponse = createResponseMetadata(response);
|
|
4867
|
-
|
|
4868
|
-
|
|
4917
|
+
let html;
|
|
4918
|
+
try {
|
|
4919
|
+
html = await readBoundedText(response, MAX_HTML_SIZE, documentUrl);
|
|
4920
|
+
} catch (error) {
|
|
4921
|
+
if (!(error instanceof BodyTooLargeError)) throw error;
|
|
4869
4922
|
logger.warn("HTML response too large, skipping alternate link discovery: {url}", {
|
|
4870
4923
|
url: documentUrl,
|
|
4871
|
-
|
|
4924
|
+
maxBytes: MAX_HTML_SIZE
|
|
4872
4925
|
});
|
|
4873
4926
|
throw new FetchError(documentUrl, `HTML document is too large to scan for an ActivityPub alternate link (Content-Type: ${contentType})`, errorResponse);
|
|
4874
|
-
}
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
}
|
|
4887
|
-
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) {
|
|
4888
|
-
logger.debug("Found alternate document: {alternateUrl} from {url}", {
|
|
4889
|
-
alternateUrl: attribs.href,
|
|
4890
|
-
url: documentUrl
|
|
4891
|
-
});
|
|
4892
|
-
return await fetch(new URL(attribs.href, docUrl).href);
|
|
4893
|
-
}
|
|
4927
|
+
}
|
|
4928
|
+
const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
|
|
4929
|
+
const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
4930
|
+
let tagMatch;
|
|
4931
|
+
while ((tagMatch = tagPattern.exec(html)) !== null) {
|
|
4932
|
+
const tagContent = tagMatch[2];
|
|
4933
|
+
let attrMatch;
|
|
4934
|
+
const attribs = {};
|
|
4935
|
+
attrPattern.lastIndex = 0;
|
|
4936
|
+
while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
|
|
4937
|
+
const key = attrMatch[1].toLowerCase();
|
|
4938
|
+
attribs[key] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
|
|
4894
4939
|
}
|
|
4895
|
-
|
|
4896
|
-
document
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4940
|
+
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) {
|
|
4941
|
+
logger.debug("Found alternate document: {alternateUrl} from {url}", {
|
|
4942
|
+
alternateUrl: attribs.href,
|
|
4943
|
+
url: documentUrl
|
|
4944
|
+
});
|
|
4945
|
+
return await fetch(new URL(attribs.href, docUrl).href);
|
|
4900
4946
|
}
|
|
4901
4947
|
}
|
|
4902
|
-
|
|
4948
|
+
try {
|
|
4949
|
+
document = JSON.parse(html);
|
|
4950
|
+
} catch (error) {
|
|
4951
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
4952
|
+
throw new FetchError(documentUrl, `HTML document has no ActivityPub alternate link (Content-Type: ${contentType})`, errorResponse);
|
|
4953
|
+
}
|
|
4954
|
+
} else document = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, documentUrl));
|
|
4903
4955
|
logger.debug("Fetched document: {status} {url} {headers}", {
|
|
4904
4956
|
status: response.status,
|
|
4905
4957
|
url: documentUrl,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const require_request = require("./request-B3YkikPO.cjs");
|
|
2
|
+
let _logtape_logtape = require("@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 require_request.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
|
+
(0, _logtape_logtape.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
|
+
Object.defineProperty(exports, "BodyTooLargeError", {
|
|
78
|
+
enumerable: true,
|
|
79
|
+
get: function() {
|
|
80
|
+
return BodyTooLargeError;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
Object.defineProperty(exports, "MAX_BODY_SIZE", {
|
|
84
|
+
enumerable: true,
|
|
85
|
+
get: function() {
|
|
86
|
+
return MAX_BODY_SIZE;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
Object.defineProperty(exports, "readBoundedText", {
|
|
90
|
+
enumerable: true,
|
|
91
|
+
get: function() {
|
|
92
|
+
return readBoundedText;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { t as FetchError } from "./request-Bo1iqGDW.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,89 @@
|
|
|
1
|
+
const require_body = require("./body-Cg5W_IdS.cjs");
|
|
2
|
+
let node_assert_strict = require("node:assert/strict");
|
|
3
|
+
let node_test = require("node:test");
|
|
4
|
+
let node_zlib = require("node:zlib");
|
|
5
|
+
//#region src/body.test.ts
|
|
6
|
+
const url = "https://example.com/document";
|
|
7
|
+
(0, node_test.test)("readBoundedText handles empty bodies and flushes incomplete UTF-8", async () => {
|
|
8
|
+
(0, node_assert_strict.deepStrictEqual)(await require_body.readBoundedText(new Response(null), 1, url), "");
|
|
9
|
+
(0, node_assert_strict.deepStrictEqual)(await require_body.readBoundedText(new Response(""), 1, url), "");
|
|
10
|
+
(0, node_assert_strict.deepStrictEqual)(await require_body.readBoundedText(new Response(new Uint8Array([226, 130])), 2, url), "�");
|
|
11
|
+
});
|
|
12
|
+
(0, node_test.test)("readBoundedText counts bytes and preserves split UTF-8", async () => {
|
|
13
|
+
const bytes = new TextEncoder().encode("\"안녕\"");
|
|
14
|
+
const stream = new ReadableStream({ start(controller) {
|
|
15
|
+
for (const byte of bytes) controller.enqueue(new Uint8Array([byte]));
|
|
16
|
+
controller.close();
|
|
17
|
+
} });
|
|
18
|
+
(0, node_assert_strict.deepStrictEqual)(await require_body.readBoundedText(new Response(stream), bytes.length, url), "\"안녕\"");
|
|
19
|
+
await (0, node_assert_strict.rejects)(require_body.readBoundedText(new Response(bytes), bytes.length - 1, url), require_body.BodyTooLargeError);
|
|
20
|
+
});
|
|
21
|
+
(0, node_test.test)("readBoundedText rejects a declared oversized body without pulling", async () => {
|
|
22
|
+
let pulled = false;
|
|
23
|
+
let canceled = false;
|
|
24
|
+
const response = new Response(new ReadableStream({
|
|
25
|
+
pull() {
|
|
26
|
+
pulled = true;
|
|
27
|
+
},
|
|
28
|
+
cancel() {
|
|
29
|
+
canceled = true;
|
|
30
|
+
}
|
|
31
|
+
}, { highWaterMark: 0 }), { headers: { "Content-Length": "100" } });
|
|
32
|
+
await (0, node_assert_strict.rejects)(require_body.readBoundedText(response, 10, url), require_body.BodyTooLargeError);
|
|
33
|
+
(0, node_assert_strict.deepStrictEqual)(pulled, false);
|
|
34
|
+
(0, node_assert_strict.deepStrictEqual)(canceled, true);
|
|
35
|
+
(0, node_assert_strict.deepStrictEqual)(response.body?.locked, false);
|
|
36
|
+
});
|
|
37
|
+
(0, node_test.test)("readBoundedText cancels chunked bodies despite absent or false lengths", async () => {
|
|
38
|
+
for (const length of [
|
|
39
|
+
void 0,
|
|
40
|
+
"1",
|
|
41
|
+
"invalid"
|
|
42
|
+
]) {
|
|
43
|
+
let pulls = 0;
|
|
44
|
+
let canceled = false;
|
|
45
|
+
const stream = new ReadableStream({
|
|
46
|
+
pull(controller) {
|
|
47
|
+
pulls++;
|
|
48
|
+
controller.enqueue(/* @__PURE__ */ new Uint8Array(4));
|
|
49
|
+
},
|
|
50
|
+
cancel() {
|
|
51
|
+
canceled = true;
|
|
52
|
+
}
|
|
53
|
+
}, { highWaterMark: 0 });
|
|
54
|
+
const response = new Response(stream, { headers: length == null ? {} : { "Content-Length": length } });
|
|
55
|
+
await (0, node_assert_strict.rejects)(require_body.readBoundedText(response, 8, url), require_body.BodyTooLargeError);
|
|
56
|
+
(0, node_assert_strict.deepStrictEqual)(pulls, 3);
|
|
57
|
+
(0, node_assert_strict.deepStrictEqual)(canceled, true);
|
|
58
|
+
(0, node_assert_strict.deepStrictEqual)(response.body?.locked, false);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
(0, node_test.test)("readBoundedText limits decoded gzip bytes, not the compressed length", async () => {
|
|
62
|
+
const compressed = (0, node_zlib.gzipSync)("\"" + "a".repeat(4096) + "\"");
|
|
63
|
+
(0, node_assert_strict.ok)(compressed.length < 128);
|
|
64
|
+
const decoded = (0, node_zlib.gunzipSync)(compressed);
|
|
65
|
+
await (0, node_assert_strict.rejects)(require_body.readBoundedText(new Response(decoded, { headers: {
|
|
66
|
+
"Content-Encoding": "gzip",
|
|
67
|
+
"Content-Length": String(compressed.length)
|
|
68
|
+
} }), 128, url), require_body.BodyTooLargeError);
|
|
69
|
+
(0, node_assert_strict.deepStrictEqual)(await require_body.readBoundedText(new Response("{}", { headers: {
|
|
70
|
+
"Content-Encoding": "gzip",
|
|
71
|
+
"Content-Length": "22"
|
|
72
|
+
} }), 2, url), "{}");
|
|
73
|
+
});
|
|
74
|
+
(0, node_test.test)("readBoundedText rejects invalid limits and releases failed streams", async () => {
|
|
75
|
+
for (const limit of [
|
|
76
|
+
0,
|
|
77
|
+
-1,
|
|
78
|
+
NaN,
|
|
79
|
+
Infinity,
|
|
80
|
+
1.5
|
|
81
|
+
]) await (0, node_assert_strict.rejects)(require_body.readBoundedText(new Response("{}"), limit, url), RangeError);
|
|
82
|
+
const failure = /* @__PURE__ */ new Error("broken stream");
|
|
83
|
+
const response = new Response(new ReadableStream({ pull(controller) {
|
|
84
|
+
controller.error(failure);
|
|
85
|
+
} }));
|
|
86
|
+
await (0, node_assert_strict.rejects)(require_body.readBoundedText(response, 10, url), (error) => error === failure);
|
|
87
|
+
(0, node_assert_strict.deepStrictEqual)(response.body?.locked, false);
|
|
88
|
+
});
|
|
89
|
+
//#endregion
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|