@fedify/fedify 2.4.0-pr.936.41 → 2.5.0-dev.2271
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/README.md +50 -39
- package/dist/{assert-CJ1KXcvv.mjs → assert-D1Eafmd_.mjs} +1 -1
- package/dist/{assert_equals-BV7oLMxm.mjs → assert_equals-Z6IcEbWU.mjs} +2 -28
- package/dist/assert_exists-cffSpJ7X.mjs +28 -0
- package/dist/assert_false-CPdRnc3E.mjs +23 -0
- package/dist/assert_greater_or_equal-BlafOF4-.mjs +31 -0
- package/dist/{assert_instance_of-Q9KTBnrf.mjs → assert_instance_of-mrXWWNY-.mjs} +1 -1
- package/dist/{assert_not_equals-EPJPoxuf.mjs → assert_not_equals-CpPK2t89.mjs} +2 -1
- package/dist/assert_rejects-CTptdYck.mjs +39 -0
- package/dist/{assert_strict_equals-Ec20AUwa.mjs → assert_strict_equals-CYvBmkpC.mjs} +2 -1
- package/dist/{assert_throws-NiFfqbnH.mjs → assert_throws-BYITOvPO.mjs} +2 -1
- package/dist/assertion_error-B-cx0fFA.mjs +31 -0
- package/dist/audience-CHQ-FlLD.mjs +95 -0
- package/dist/{builder-DonuxRl_.mjs → builder-Bda6LK7A.mjs} +169 -14
- package/dist/{circuit-breaker-CBGyz5F7.mjs → circuit-breaker-XJ53md4S.mjs} +224 -17
- package/dist/circuit-breaker-test-utils-DW6YC_Pe.mjs +14 -0
- package/dist/{client-CjF5IYQS.mjs → client-CEnNz8O7.mjs} +144 -8
- package/dist/{codec-BBga-FWI.mjs → codec-C6rTrpIp.mjs} +26 -3
- package/dist/{collection-BIhlldqK.mjs → collection-CnnP1UAv.mjs} +1 -1
- package/dist/compat/mod.cjs +1 -1
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/mod.js +1 -1
- package/dist/compat/outgoing-jsonld.test.mjs +71 -3
- package/dist/compat/public-audience.test.mjs +14 -5
- package/dist/compat/secured-document.test.d.mts +2 -0
- package/dist/compat/secured-document.test.mjs +58 -0
- package/dist/compat/transformers.test.mjs +5 -5
- package/dist/compound-proof-B-9mXxFW.mjs +989 -0
- package/dist/{context-Zii1f5xS.d.ts → context-7NyiqIcg.d.ts} +1600 -25
- package/dist/{context-BJXxPElm.mjs → context-8Tu9kea0.mjs} +21 -63
- package/dist/{context-lxX6I7as.d.cts → context-tsYPJ2Ew.d.cts} +1600 -25
- package/dist/{deno-BHzKn9Ig.mjs → deno-oR5aJKpU.mjs} +1 -1
- package/dist/dist-DJQM4u7i.mjs +189 -0
- package/dist/docloader-DBoYbvYW.mjs +87 -0
- package/dist/{esm-yYQfPhIY.mjs → esm-BHXXx4Wx.mjs} +13 -6
- package/dist/federation/audience.test.d.mts +2 -0
- package/dist/federation/audience.test.mjs +130 -0
- package/dist/federation/builder.test.mjs +54 -5
- package/dist/federation/circuit-breaker.test.mjs +838 -7
- package/dist/federation/collection.test.mjs +3 -3
- package/dist/federation/compound-fanout.test.d.mts +2 -0
- package/dist/federation/compound-fanout.test.mjs +228 -0
- package/dist/federation/handler-compound-proof.test.d.mts +2 -0
- package/dist/federation/handler-compound-proof.test.mjs +384 -0
- package/dist/federation/handler.test.mjs +385 -15
- package/dist/federation/hashlink-media.test.d.mts +2 -0
- package/dist/federation/hashlink-media.test.mjs +422 -0
- package/dist/federation/idempotency.test.mjs +6 -6
- package/dist/federation/inbox-report.test.d.mts +2 -0
- package/dist/federation/inbox-report.test.mjs +475 -0
- package/dist/federation/inbox.test.mjs +3 -3
- package/dist/federation/keycache.test.mjs +215 -14
- package/dist/federation/kv.test.mjs +3 -3
- package/dist/federation/metrics.test.mjs +5 -5
- package/dist/federation/middleware.test.mjs +967 -35
- package/dist/federation/mod.cjs +18 -261
- package/dist/federation/mod.d.cts +5 -5
- package/dist/federation/mod.d.ts +5 -5
- package/dist/federation/mod.js +279 -1
- package/dist/federation/mq.test.mjs +123 -47
- package/dist/federation/negotiation.test.mjs +4 -4
- package/dist/federation/outgoing-proof.test.d.mts +2 -0
- package/dist/federation/outgoing-proof.test.mjs +931 -0
- package/dist/federation/portable-actor.test.d.mts +2 -0
- package/dist/federation/portable-actor.test.mjs +496 -0
- package/dist/federation/portable-chain.test.d.mts +2 -0
- package/dist/federation/portable-chain.test.mjs +370 -0
- package/dist/federation/portable-collection.test.d.mts +2 -0
- package/dist/federation/portable-collection.test.mjs +832 -0
- package/dist/federation/portable-delivery.test.d.mts +2 -0
- package/dist/federation/portable-delivery.test.mjs +746 -0
- package/dist/federation/portable-gateway-keys.test.d.mts +2 -0
- package/dist/federation/portable-gateway-keys.test.mjs +686 -0
- package/dist/federation/portable-inbox.test.d.mts +2 -0
- package/dist/federation/portable-inbox.test.mjs +1535 -0
- package/dist/federation/portable-parse-uri.test.d.mts +2 -0
- package/dist/federation/portable-parse-uri.test.mjs +348 -0
- package/dist/federation/portable-route-activity.test.d.mts +2 -0
- package/dist/federation/portable-route-activity.test.mjs +259 -0
- package/dist/federation/portable.test.d.mts +2 -0
- package/dist/federation/portable.test.mjs +1072 -0
- package/dist/federation/retry.test.mjs +4 -3
- package/dist/federation/router.test.mjs +9 -159
- package/dist/federation/send.test.mjs +245 -27
- package/dist/federation/tasks/codec.test.mjs +4 -4
- package/dist/federation/tasks/enqueue.test.mjs +42 -4
- package/dist/federation/tasks/tasks.test.mjs +7 -6
- package/dist/federation/temporal.test.mjs +4 -4
- package/dist/federation/webfinger.test.mjs +275 -12
- package/dist/{middleware-FrSMAYEx.cjs → federation-CkZiAcWY.cjs} +8640 -3722
- package/dist/http-B5JT115n.cjs +10630 -0
- package/dist/http-DydWCB7R.js +6527 -0
- package/dist/{http-CPVMUgsW.mjs → http-siciWkmx.mjs} +397 -168
- package/dist/key-BspPiXqn.mjs +951 -0
- package/dist/key-C1nv7_jD.d.ts +1125 -0
- package/dist/key-DYxuyd-s.d.cts +1125 -0
- package/dist/key-owner-evidence-APB6NpSk.mjs +108 -0
- package/dist/keycache-Da8A8mZa.mjs +200 -0
- package/dist/{kv-D6hNiMTK.d.ts → kv-B2mzK5tl.d.cts} +3 -2
- package/dist/{kv-gJ8LYbxX.d.cts → kv-B2mzK5tl.d.ts} +3 -2
- package/dist/kv-DBRWVx3Q.mjs +104 -0
- package/dist/{kv-cache-KyGqJqSp.mjs → kv-cache-BF1hnW65.mjs} +1 -1
- package/dist/{kv-cache-5bnX3-Jq.js → kv-cache-CTQr1tbn.js} +54 -16
- package/dist/{kv-cache-DQ29C4XL.cjs → kv-cache-DCac1nEU.cjs} +55 -19
- package/dist/{ld-Cj9F_5AP.mjs → ld-CzaP-hJl.mjs} +44 -12
- package/dist/map-local-context-conflict-DOYEEJ3u.mjs +204 -0
- package/dist/map-local-create-note-B0SeXJms.mjs +214 -0
- package/dist/{metrics-DMk-nTxI.mjs → metrics-CYg9yxBL.mjs} +3 -2
- package/dist/metrics-DVKYY6K1.d.cts +36 -0
- package/dist/metrics-DoRtI0qU.d.ts +36 -0
- package/dist/{middleware-Y-kpsp6S.js → middleware-CsDDXKuL.js} +5493 -1103
- package/dist/{middleware-CvwTUgnP.mjs → middleware-D7_LO-Vs.mjs} +3157 -272
- package/dist/{middleware-CEctxBN6.mjs → middleware-DWjrfpcz.mjs} +1 -1
- package/dist/{mod-gOlPDuT8.d.cts → mod-BojP2Bw3.d.cts} +1 -1
- package/dist/mod-CavhAgLw.d.cts +320 -0
- package/dist/mod-CtpF2BU5.d.ts +320 -0
- package/dist/{mod-BtEaIEcD.d.cts → mod-DEZXwV6J.d.ts} +10 -4
- package/dist/{mod-UMmveiO0.d.ts → mod-Dw-nPxQh.d.cts} +10 -4
- package/dist/{mod-DLc2Ot6f.d.ts → mod-Xj5P4s4d.d.ts} +1 -1
- package/dist/mod.cjs +40 -39
- package/dist/mod.d.cts +10 -11
- package/dist/mod.d.ts +10 -11
- package/dist/mod.js +9 -9
- package/dist/{mq-llHKivST.mjs → mq-Dm1Wmtm-.mjs} +6 -0
- package/dist/nodeinfo/client.test.mjs +139 -4
- package/dist/nodeinfo/handler.test.mjs +5 -5
- package/dist/nodeinfo/mod.cjs +1 -1
- package/dist/nodeinfo/mod.d.cts +1 -1
- package/dist/nodeinfo/mod.d.ts +1 -1
- package/dist/nodeinfo/mod.js +1 -1
- package/dist/nodeinfo/types.test.mjs +4 -4
- package/dist/otel/exporter.test.mjs +3 -3
- package/dist/otel/mod.cjs +0 -1
- package/dist/otel/mod.d.cts +1 -1
- package/dist/otel/mod.d.ts +1 -1
- package/dist/{outgoing-jsonld-URuqsICz.mjs → outgoing-jsonld-4IJqPYos.mjs} +12 -10
- package/dist/{owner-DEvZuyOE.d.cts → owner-BF0L9cMt.d.cts} +11 -0
- package/dist/{owner-CnngXDNJ.d.ts → owner-BSzr5-I8.d.ts} +11 -0
- package/dist/owner-DYgfTFHy.mjs +267 -0
- package/dist/portable-collection-BpsVsRov.js +670 -0
- package/dist/portable-collection-C74P1vtC.mjs +418 -0
- package/dist/portable-collection-DZd2bm7M.cjs +686 -0
- package/dist/portable-key-BIMLrAP4.mjs +549 -0
- package/dist/portable-key-id-Ca30-hOa.mjs +248 -0
- package/dist/proof-Dxy6ByqI.mjs +1299 -0
- package/dist/{public-audience-Cr5MsnMq.mjs → public-audience--klBIOk1.mjs} +15 -10
- package/dist/router-u5O5KCQr.mjs +156 -0
- package/dist/runtime/mod.cjs +1 -1
- package/dist/runtime/mod.js +1 -1
- package/dist/secured-document-CxzZZ9nL.mjs +35 -0
- package/dist/send-C4dDpAxm.mjs +495 -0
- package/dist/sig/accept.test.mjs +1 -1
- package/dist/sig/compound-proof-discovery.test.d.mts +2 -0
- package/dist/sig/compound-proof-discovery.test.mjs +331 -0
- package/dist/sig/compound-proof-producer.test.d.mts +2 -0
- package/dist/sig/compound-proof-producer.test.mjs +107 -0
- package/dist/sig/compound-proof-verification.test.d.mts +2 -0
- package/dist/sig/compound-proof-verification.test.mjs +833 -0
- package/dist/sig/compound-proof.test.d.mts +2 -0
- package/dist/sig/compound-proof.test.mjs +411 -0
- package/dist/sig/http.test.mjs +702 -31
- package/dist/sig/key-owner-evidence.test.d.mts +2 -0
- package/dist/sig/key-owner-evidence.test.mjs +100 -0
- package/dist/sig/key.test.mjs +220 -8
- package/dist/sig/ld.test.mjs +65 -7
- package/dist/sig/mitra-receive-interop.test.d.mts +2 -0
- package/dist/sig/mitra-receive-interop.test.mjs +260 -0
- package/dist/sig/mitra-send-interop.test.d.mts +2 -0
- package/dist/sig/mitra-send-interop.test.mjs +461 -0
- package/dist/sig/mod.cjs +18 -16
- package/dist/sig/mod.d.cts +4 -4
- package/dist/sig/mod.d.ts +4 -4
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +108 -9
- package/dist/sig/portable-collection.test.d.mts +2 -0
- package/dist/sig/portable-collection.test.mjs +697 -0
- package/dist/sig/portable-interop.test.d.mts +2 -0
- package/dist/sig/portable-interop.test.mjs +156 -0
- package/dist/sig/portable-key-id.test.d.mts +2 -0
- package/dist/sig/portable-key-id.test.mjs +67 -0
- package/dist/sig/portable-key.test.d.mts +2 -0
- package/dist/sig/portable-key.test.mjs +1439 -0
- package/dist/sig/proof.test.mjs +1753 -16
- package/dist/sig/tootik-request-interop.test.d.mts +2 -0
- package/dist/sig/tootik-request-interop.test.mjs +190 -0
- package/dist/sig/tootik-send-interop.test.d.mts +2 -0
- package/dist/sig/tootik-send-interop.test.mjs +331 -0
- package/dist/sig/verification.test.d.mts +2 -0
- package/dist/sig/verification.test.mjs +310 -0
- package/dist/std__assert-DvMDRPes.mjs +8 -0
- package/dist/{testing-CO7ftO8A.mjs → tasks-IG3pHfvZ.mjs} +2 -2
- package/dist/{temporal-fsXO5IvD.mjs → temporal-D0KJI4NF.mjs} +3 -3
- package/dist/testing/context.test.d.mts +2 -0
- package/dist/testing/context.test.mjs +19 -0
- package/dist/testing/kv.test.d.mts +2 -0
- package/dist/testing/kv.test.mjs +101 -0
- package/dist/testing/mod.d.mts +1617 -48
- package/dist/testing/mod.mjs +6 -3
- package/dist/testing-t8xDeJZ5.mjs +8 -0
- package/dist/{transformers-DGwl-hD1.cjs → transformers-BalCrSsr.cjs} +0 -1
- package/dist/{types-DC1pNgJA.js → types-_S2_MeLF.js} +144 -8
- package/dist/{types-CPym83zc.cjs → types-poIao40f.cjs} +160 -7
- package/dist/utils/docloader.test.mjs +530 -8
- package/dist/utils/kv-cache.test.mjs +2 -2
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/dist/verification-C3954hQh.mjs +5 -0
- package/dist/verification-Dpfcw8ml.mjs +105 -0
- package/dist/vocab/cjs.test.mjs +1 -1
- package/package.json +8 -8
- package/dist/assert_rejects-EgstcQy5.mjs +0 -82
- package/dist/docloader-e1khNUKy.mjs +0 -49
- package/dist/http-CsmQ-BQ4.js +0 -2483
- package/dist/http-VyDTd4G3.d.cts +0 -553
- package/dist/http-hlIjmewS.cjs +0 -2694
- package/dist/http-lf8Hsd91.d.ts +0 -553
- package/dist/key-DfPsX8hv.mjs +0 -453
- package/dist/keycache-_DHqz5z_.mjs +0 -97
- package/dist/metrics-CoUJLQhZ.d.cts +0 -276
- package/dist/metrics-Dc4jR-IC.d.ts +0 -276
- package/dist/mod-B0hW12_O.d.cts +0 -307
- package/dist/mod-COIAjwRS.d.ts +0 -307
- package/dist/owner-Br9YYzn_.mjs +0 -120
- package/dist/proof-B4ngoaNQ.mjs +0 -316
- package/dist/proof-B6E0XUXJ.js +0 -1415
- package/dist/proof-NN4xCQGL.cjs +0 -1562
- package/dist/send-CxHKUNzv.mjs +0 -239
- package/dist/std__assert-B2NnY9bq.mjs +0 -79
- package/dist/temporal-polyfill-BCdTHexd.cjs +0 -3565
- /package/dist/{accept-8PiolDP7.mjs → accept-CPVqki78.mjs} +0 -0
- /package/dist/{activity-listener-BtTSxZWA.mjs → activity-listener-C-BSbKI7.mjs} +0 -0
- /package/dist/{client-CAM_bQXx.d.cts → client-D-XP_Ccb.d.cts} +0 -0
- /package/dist/{client-CSddvgWN.d.ts → client-D-XP_Ccb.d.ts} +0 -0
- /package/dist/{keys-Dbyd7bhl.mjs → keys-DIJNHW-Y.mjs} +0 -0
- /package/dist/{kv-CXBAok3k.mjs → kv-DCHkaJ9-.mjs} +0 -0
- /package/dist/{negotiation-RuwrPFqi.mjs → negotiation-CCocWT_N.mjs} +0 -0
- /package/dist/{retry-DgFqeS5k.mjs → retry-zX5xw5e0.mjs} +0 -0
- /package/dist/{chunk-D5IdezsZ.js → rolldown-runtime-BC-Eo5Mj.js} +0 -0
- /package/dist/{chunk-Czpeue4M.cjs → rolldown-runtime-Dmm3t92W.cjs} +0 -0
- /package/dist/{transformers-DXze6FUB.js → transformers-DAtXl_Kc.js} +0 -0
- /package/dist/{types-CrtdVuDP.mjs → types-DE_LV1JB.mjs} +0 -0
package/dist/testing/mod.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import "temporal-polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
4
|
+
import { r as testDefinitions } from "../dist-DJQM4u7i.mjs";
|
|
5
|
+
import { n as createOutboxContext, r as createRequestContext, t as createInboxContext } from "../context-8Tu9kea0.mjs";
|
|
6
|
+
import { t as ManualClockKvStore } from "../kv-DBRWVx3Q.mjs";
|
|
7
|
+
import { a as numberSchema, i as makeSchema, n as baseOptions, o as stringSchema, r as envelopeSchema, t as MockQueue } from "../tasks-IG3pHfvZ.mjs";
|
|
8
|
+
import "../testing-t8xDeJZ5.mjs";
|
|
9
|
+
export { ManualClockKvStore, MockQueue, baseOptions, createInboxContext, createOutboxContext, createRequestContext, envelopeSchema, makeSchema, numberSchema, stringSchema, testDefinitions };
|
|
@@ -1,22 +1,157 @@
|
|
|
1
1
|
import { Temporal } from "temporal-polyfill";
|
|
2
2
|
import { URLPattern } from "urlpattern-polyfill";
|
|
3
3
|
import { getLogger } from "@logtape/logtape";
|
|
4
|
-
import { getUserAgent } from "@fedify/vocab-runtime";
|
|
4
|
+
import { FetchError, UrlError, getUserAgent, validatePublicUrl } from "@fedify/vocab-runtime";
|
|
5
|
+
//#region src/utils/body.ts
|
|
6
|
+
/** Decoded JSON body limit: 16 MiB. @internal */
|
|
7
|
+
const MAX_BODY_SIZE = 16 * 1024 * 1024;
|
|
8
|
+
/** A body exceeded the byte limit. @internal */
|
|
9
|
+
var BodyTooLargeError = class extends FetchError {
|
|
10
|
+
/** Creates an error for a body exceeding the given limit. */
|
|
11
|
+
constructor(url, maxBytes) {
|
|
12
|
+
super(url, `Body exceeds the limit of ${maxBytes} bytes`);
|
|
13
|
+
this.name = "BodyTooLargeError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/** Validates a finite, positive byte limit. @internal */
|
|
17
|
+
function validateBodySizeLimit(maxBytes) {
|
|
18
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Reads UTF-8 text while limiting bytes received from the body stream.
|
|
22
|
+
* Fetch responses are already decompressed, so the stream count is authoritative.
|
|
23
|
+
* @param message The response or request to read.
|
|
24
|
+
* @param maxBytes The maximum number of decoded bytes to read.
|
|
25
|
+
* @param url The URL to include in errors and logs.
|
|
26
|
+
* @returns The decoded body text.
|
|
27
|
+
* @throws {BodyTooLargeError} If the body exceeds the limit.
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
async function readBoundedText(message, maxBytes, url) {
|
|
31
|
+
validateBodySizeLimit(maxBytes);
|
|
32
|
+
const reader = message.body?.getReader();
|
|
33
|
+
const tooLarge = () => {
|
|
34
|
+
getLogger([
|
|
35
|
+
"fedify",
|
|
36
|
+
"runtime",
|
|
37
|
+
"body"
|
|
38
|
+
]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
|
|
39
|
+
url: url.toString(),
|
|
40
|
+
maxBytes
|
|
41
|
+
});
|
|
42
|
+
throw new BodyTooLargeError(url, maxBytes);
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
const length = message.headers.get("Content-Length");
|
|
46
|
+
const encoding = message.headers.get("Content-Encoding");
|
|
47
|
+
if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
|
|
48
|
+
if (reader == null) return "";
|
|
49
|
+
let size = 0;
|
|
50
|
+
const textReader = new ReadableStream({ async pull(controller) {
|
|
51
|
+
const { done, value } = await reader.read();
|
|
52
|
+
if (done) {
|
|
53
|
+
controller.close();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
size += value.byteLength;
|
|
57
|
+
if (size > maxBytes) tooLarge();
|
|
58
|
+
controller.enqueue(value);
|
|
59
|
+
} }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
|
|
60
|
+
try {
|
|
61
|
+
const chunks = [];
|
|
62
|
+
while (true) {
|
|
63
|
+
const { done, value } = await textReader.read();
|
|
64
|
+
if (done) break;
|
|
65
|
+
chunks.push(value);
|
|
66
|
+
}
|
|
67
|
+
return chunks.join("");
|
|
68
|
+
} finally {
|
|
69
|
+
textReader.releaseLock();
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (reader != null) reader.cancel(error).catch(() => {});
|
|
73
|
+
throw error;
|
|
74
|
+
} finally {
|
|
75
|
+
reader?.releaseLock();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
5
79
|
//#region src/nodeinfo/client.ts
|
|
6
80
|
const logger = getLogger([
|
|
7
81
|
"fedify",
|
|
8
82
|
"nodeinfo",
|
|
9
83
|
"client"
|
|
10
84
|
]);
|
|
85
|
+
const DEFAULT_MAX_REDIRECTION = 5;
|
|
86
|
+
/**
|
|
87
|
+
* Fetches a URL while guarding against SSRF. Before every request—including
|
|
88
|
+
* each redirect hop—the destination is validated with
|
|
89
|
+
* {@link validatePublicUrl}, so that loopback, link-local, and private
|
|
90
|
+
* addresses, as well as non-HTTP(S) schemes such as `data:`, are rejected.
|
|
91
|
+
* Redirects are followed manually with a cap, so that a public URL cannot
|
|
92
|
+
* bounce the request to an internal address.
|
|
93
|
+
* @param url The URL to fetch.
|
|
94
|
+
* @param headers The request headers.
|
|
95
|
+
* @returns The final response, or `null` if the URL is disallowed, its
|
|
96
|
+
* hostname cannot be resolved, or there are too many redirects.
|
|
97
|
+
*/
|
|
98
|
+
async function fetchPublicUrl(url, headers) {
|
|
99
|
+
let current = url instanceof URL ? url : new URL(url);
|
|
100
|
+
let redirected = 0;
|
|
101
|
+
while (true) {
|
|
102
|
+
try {
|
|
103
|
+
await validatePublicUrl(current.href);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
if (e instanceof UrlError) {
|
|
106
|
+
if (e.reason === "dns") logger.error("DNS lookup failed for {url}: {error}", {
|
|
107
|
+
url: current.href,
|
|
108
|
+
error: e
|
|
109
|
+
});
|
|
110
|
+
else logger.error("Refused to fetch a private or invalid URL {url}: {error}", {
|
|
111
|
+
url: current.href,
|
|
112
|
+
error: e
|
|
113
|
+
});
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
throw e;
|
|
117
|
+
}
|
|
118
|
+
const response = await fetch(current, {
|
|
119
|
+
headers,
|
|
120
|
+
redirect: "manual"
|
|
121
|
+
});
|
|
122
|
+
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
123
|
+
redirected++;
|
|
124
|
+
if (redirected >= DEFAULT_MAX_REDIRECTION) {
|
|
125
|
+
logger.error("Too many redirections ({redirections}) while fetching {url}.", {
|
|
126
|
+
redirections: redirected,
|
|
127
|
+
url: current.href
|
|
128
|
+
});
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const next = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? current : response.url);
|
|
132
|
+
if (next.protocol !== current.protocol) {
|
|
133
|
+
logger.error("Refused a redirect to a different protocol ({protocol} to {redirectedProtocol}) while fetching NodeInfo.", {
|
|
134
|
+
protocol: current.protocol,
|
|
135
|
+
redirectedProtocol: next.protocol
|
|
136
|
+
});
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
current = next;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
return response;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
11
145
|
async function getNodeInfo(url, options = {}) {
|
|
12
146
|
try {
|
|
13
147
|
let nodeInfoUrl = url;
|
|
14
148
|
if (!options.direct) {
|
|
15
149
|
const wellKnownUrl = new URL("/.well-known/nodeinfo", url);
|
|
16
|
-
const wellKnownResponse = await
|
|
150
|
+
const wellKnownResponse = await fetchPublicUrl(wellKnownUrl, {
|
|
17
151
|
Accept: "application/json",
|
|
18
152
|
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
19
|
-
}
|
|
153
|
+
});
|
|
154
|
+
if (wellKnownResponse == null) return void 0;
|
|
20
155
|
if (!wellKnownResponse.ok) {
|
|
21
156
|
logger.error("Failed to fetch {url}: {status} {statusText}", {
|
|
22
157
|
url: wellKnownUrl.href,
|
|
@@ -25,7 +160,7 @@ async function getNodeInfo(url, options = {}) {
|
|
|
25
160
|
});
|
|
26
161
|
return;
|
|
27
162
|
}
|
|
28
|
-
const wellKnownRd = await wellKnownResponse
|
|
163
|
+
const wellKnownRd = JSON.parse(await readBoundedText(wellKnownResponse, MAX_BODY_SIZE, wellKnownUrl));
|
|
29
164
|
const link = wellKnownRd?.links?.find((link) => link != null && "rel" in link && (link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.0" || link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.1") && "href" in link && link.href != null);
|
|
30
165
|
if (link == null || link.href == null) {
|
|
31
166
|
logger.error("Failed to find a NodeInfo document link from {url}: {resourceDescriptor}", {
|
|
@@ -36,10 +171,11 @@ async function getNodeInfo(url, options = {}) {
|
|
|
36
171
|
}
|
|
37
172
|
nodeInfoUrl = link.href;
|
|
38
173
|
}
|
|
39
|
-
const response = await
|
|
174
|
+
const response = await fetchPublicUrl(nodeInfoUrl, {
|
|
40
175
|
Accept: "application/json",
|
|
41
176
|
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
42
|
-
}
|
|
177
|
+
});
|
|
178
|
+
if (response == null) return void 0;
|
|
43
179
|
if (!response.ok) {
|
|
44
180
|
logger.error("Failed to fetch NodeInfo document from {url}: {status} {statusText}", {
|
|
45
181
|
url: nodeInfoUrl.toString(),
|
|
@@ -48,7 +184,7 @@ async function getNodeInfo(url, options = {}) {
|
|
|
48
184
|
});
|
|
49
185
|
return;
|
|
50
186
|
}
|
|
51
|
-
const data = await response
|
|
187
|
+
const data = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, nodeInfoUrl));
|
|
52
188
|
if (options.parse === "none") return data;
|
|
53
189
|
return parseNodeInfo(data, { tryBestEffort: options.parse === "best-effort" }) ?? void 0;
|
|
54
190
|
} catch (error) {
|
|
@@ -294,4 +430,4 @@ function nodeInfoToJson(nodeInfo) {
|
|
|
294
430
|
};
|
|
295
431
|
}
|
|
296
432
|
//#endregion
|
|
297
|
-
export { getNodeInfo as n, parseNodeInfo as r, nodeInfoToJson as t };
|
|
433
|
+
export { MAX_BODY_SIZE as a, BodyTooLargeError as i, getNodeInfo as n, readBoundedText as o, parseNodeInfo as r, nodeInfoToJson as t };
|
|
@@ -1,22 +1,156 @@
|
|
|
1
1
|
const { URLPattern } = require("urlpattern-polyfill");
|
|
2
|
-
require("./chunk-Czpeue4M.cjs");
|
|
3
2
|
let _logtape_logtape = require("@logtape/logtape");
|
|
4
3
|
let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
|
|
4
|
+
//#region src/utils/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 _fedify_vocab_runtime.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
|
|
5
78
|
//#region src/nodeinfo/client.ts
|
|
6
79
|
const logger = (0, _logtape_logtape.getLogger)([
|
|
7
80
|
"fedify",
|
|
8
81
|
"nodeinfo",
|
|
9
82
|
"client"
|
|
10
83
|
]);
|
|
84
|
+
const DEFAULT_MAX_REDIRECTION = 5;
|
|
85
|
+
/**
|
|
86
|
+
* Fetches a URL while guarding against SSRF. Before every request—including
|
|
87
|
+
* each redirect hop—the destination is validated with
|
|
88
|
+
* {@link validatePublicUrl}, so that loopback, link-local, and private
|
|
89
|
+
* addresses, as well as non-HTTP(S) schemes such as `data:`, are rejected.
|
|
90
|
+
* Redirects are followed manually with a cap, so that a public URL cannot
|
|
91
|
+
* bounce the request to an internal address.
|
|
92
|
+
* @param url The URL to fetch.
|
|
93
|
+
* @param headers The request headers.
|
|
94
|
+
* @returns The final response, or `null` if the URL is disallowed, its
|
|
95
|
+
* hostname cannot be resolved, or there are too many redirects.
|
|
96
|
+
*/
|
|
97
|
+
async function fetchPublicUrl(url, headers) {
|
|
98
|
+
let current = url instanceof URL ? url : new URL(url);
|
|
99
|
+
let redirected = 0;
|
|
100
|
+
while (true) {
|
|
101
|
+
try {
|
|
102
|
+
await (0, _fedify_vocab_runtime.validatePublicUrl)(current.href);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
if (e instanceof _fedify_vocab_runtime.UrlError) {
|
|
105
|
+
if (e.reason === "dns") logger.error("DNS lookup failed for {url}: {error}", {
|
|
106
|
+
url: current.href,
|
|
107
|
+
error: e
|
|
108
|
+
});
|
|
109
|
+
else logger.error("Refused to fetch a private or invalid URL {url}: {error}", {
|
|
110
|
+
url: current.href,
|
|
111
|
+
error: e
|
|
112
|
+
});
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
117
|
+
const response = await fetch(current, {
|
|
118
|
+
headers,
|
|
119
|
+
redirect: "manual"
|
|
120
|
+
});
|
|
121
|
+
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
122
|
+
redirected++;
|
|
123
|
+
if (redirected >= DEFAULT_MAX_REDIRECTION) {
|
|
124
|
+
logger.error("Too many redirections ({redirections}) while fetching {url}.", {
|
|
125
|
+
redirections: redirected,
|
|
126
|
+
url: current.href
|
|
127
|
+
});
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const next = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? current : response.url);
|
|
131
|
+
if (next.protocol !== current.protocol) {
|
|
132
|
+
logger.error("Refused a redirect to a different protocol ({protocol} to {redirectedProtocol}) while fetching NodeInfo.", {
|
|
133
|
+
protocol: current.protocol,
|
|
134
|
+
redirectedProtocol: next.protocol
|
|
135
|
+
});
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
current = next;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
return response;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
11
144
|
async function getNodeInfo(url, options = {}) {
|
|
12
145
|
try {
|
|
13
146
|
let nodeInfoUrl = url;
|
|
14
147
|
if (!options.direct) {
|
|
15
148
|
const wellKnownUrl = new URL("/.well-known/nodeinfo", url);
|
|
16
|
-
const wellKnownResponse = await
|
|
149
|
+
const wellKnownResponse = await fetchPublicUrl(wellKnownUrl, {
|
|
17
150
|
Accept: "application/json",
|
|
18
151
|
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : (0, _fedify_vocab_runtime.getUserAgent)(options.userAgent)
|
|
19
|
-
}
|
|
152
|
+
});
|
|
153
|
+
if (wellKnownResponse == null) return void 0;
|
|
20
154
|
if (!wellKnownResponse.ok) {
|
|
21
155
|
logger.error("Failed to fetch {url}: {status} {statusText}", {
|
|
22
156
|
url: wellKnownUrl.href,
|
|
@@ -25,7 +159,7 @@ async function getNodeInfo(url, options = {}) {
|
|
|
25
159
|
});
|
|
26
160
|
return;
|
|
27
161
|
}
|
|
28
|
-
const wellKnownRd = await wellKnownResponse
|
|
162
|
+
const wellKnownRd = JSON.parse(await readBoundedText(wellKnownResponse, MAX_BODY_SIZE, wellKnownUrl));
|
|
29
163
|
const link = wellKnownRd?.links?.find((link) => link != null && "rel" in link && (link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.0" || link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.1") && "href" in link && link.href != null);
|
|
30
164
|
if (link == null || link.href == null) {
|
|
31
165
|
logger.error("Failed to find a NodeInfo document link from {url}: {resourceDescriptor}", {
|
|
@@ -36,10 +170,11 @@ async function getNodeInfo(url, options = {}) {
|
|
|
36
170
|
}
|
|
37
171
|
nodeInfoUrl = link.href;
|
|
38
172
|
}
|
|
39
|
-
const response = await
|
|
173
|
+
const response = await fetchPublicUrl(nodeInfoUrl, {
|
|
40
174
|
Accept: "application/json",
|
|
41
175
|
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : (0, _fedify_vocab_runtime.getUserAgent)(options.userAgent)
|
|
42
|
-
}
|
|
176
|
+
});
|
|
177
|
+
if (response == null) return void 0;
|
|
43
178
|
if (!response.ok) {
|
|
44
179
|
logger.error("Failed to fetch NodeInfo document from {url}: {status} {statusText}", {
|
|
45
180
|
url: nodeInfoUrl.toString(),
|
|
@@ -48,7 +183,7 @@ async function getNodeInfo(url, options = {}) {
|
|
|
48
183
|
});
|
|
49
184
|
return;
|
|
50
185
|
}
|
|
51
|
-
const data = await response
|
|
186
|
+
const data = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, nodeInfoUrl));
|
|
52
187
|
if (options.parse === "none") return data;
|
|
53
188
|
return parseNodeInfo(data, { tryBestEffort: options.parse === "best-effort" }) ?? void 0;
|
|
54
189
|
} catch (error) {
|
|
@@ -294,6 +429,18 @@ function nodeInfoToJson(nodeInfo) {
|
|
|
294
429
|
};
|
|
295
430
|
}
|
|
296
431
|
//#endregion
|
|
432
|
+
Object.defineProperty(exports, "BodyTooLargeError", {
|
|
433
|
+
enumerable: true,
|
|
434
|
+
get: function() {
|
|
435
|
+
return BodyTooLargeError;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
Object.defineProperty(exports, "MAX_BODY_SIZE", {
|
|
439
|
+
enumerable: true,
|
|
440
|
+
get: function() {
|
|
441
|
+
return MAX_BODY_SIZE;
|
|
442
|
+
}
|
|
443
|
+
});
|
|
297
444
|
Object.defineProperty(exports, "getNodeInfo", {
|
|
298
445
|
enumerable: true,
|
|
299
446
|
get: function() {
|
|
@@ -312,3 +459,9 @@ Object.defineProperty(exports, "parseNodeInfo", {
|
|
|
312
459
|
return parseNodeInfo;
|
|
313
460
|
}
|
|
314
461
|
});
|
|
462
|
+
Object.defineProperty(exports, "readBoundedText", {
|
|
463
|
+
enumerable: true,
|
|
464
|
+
get: function() {
|
|
465
|
+
return readBoundedText;
|
|
466
|
+
}
|
|
467
|
+
});
|