@fedify/fedify 2.1.23 → 2.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{assert_instance_of-C4Ri6VuN.mjs → assert-BhQd8QHP.mjs} +19 -1
- package/dist/{builder-BlCsjkrX.mjs → builder-DsBAef6b.mjs} +3 -3
- package/dist/{client-CZWLyLmp.mjs → client-DM72NZuk.mjs} +85 -7
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +3 -4
- package/dist/{context-_l9t359k.d.ts → context-BgU5UWTx.d.ts} +4 -3
- package/dist/{context-o22vjzS2.d.cts → context-D4s0Wk3N.d.cts} +4 -3
- package/dist/{deno-B_Gwkjh7.mjs → deno-9AaebKOA.mjs} +1 -1
- package/dist/docloader-DX96uPUj.mjs +78 -0
- package/dist/federation/builder.test.mjs +3 -3
- package/dist/federation/collection.test.mjs +2 -2
- package/dist/federation/handler.test.mjs +128 -10
- package/dist/federation/idempotency.test.mjs +5 -5
- package/dist/federation/inbox.test.mjs +1 -1
- package/dist/federation/keycache.test.mjs +44 -10
- package/dist/federation/kv.test.mjs +2 -2
- package/dist/federation/middleware.test.mjs +227 -12
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +2 -2
- package/dist/federation/mod.d.ts +2 -2
- package/dist/federation/mod.js +1 -1
- package/dist/federation/mq.test.mjs +99 -44
- package/dist/federation/negotiation.test.mjs +3 -3
- package/dist/federation/retry.test.mjs +1 -1
- package/dist/federation/router.test.mjs +2 -2
- package/dist/federation/send.test.mjs +202 -7
- package/dist/federation/temporal.test.mjs +3 -3
- package/dist/federation/webfinger.test.mjs +3 -3
- package/dist/{http-DIh9_s_-.mjs → http-B5qAeoJd.mjs} +8 -4
- package/dist/{http-Yoq0YGz7.cjs → http-BJEMpnnY.cjs} +171 -7
- package/dist/{http-88deUx3Y.js → http-CdJ6n4rA.js} +160 -8
- package/dist/{http-BDZeS5om.d.ts → http-Dl00HXr4.d.ts} +1 -1
- package/dist/{http-C87EWkO0.d.cts → http-DqZhyEso.d.cts} +1 -1
- package/dist/{inbox-BxCHIWPD.mjs → inbox-BSyjaFbo.mjs} +1 -1
- package/dist/{key-DywPPmb0.mjs → key-x9f-H7eO.mjs} +154 -6
- package/dist/{keycache-CCSwkQcY.mjs → keycache-CaO7wyQA.mjs} +12 -4
- package/dist/{kv-cache-HA011rBG.cjs → kv-cache-BDp2c_QE.cjs} +34 -8
- package/dist/{kv-cache-rHQOc5rE.js → kv-cache-Dx3QwNBI.js} +35 -9
- package/dist/{ld-Ch6FVq7X.mjs → ld-cfUwoi0C.mjs} +3 -3
- package/dist/{middleware-itcr4fAv.cjs → middleware-BXJpAXhO.cjs} +1 -1
- package/dist/{middleware-CYsQG25-.js → middleware-CP35ElZv.js} +99 -14
- package/dist/{middleware-DABFg_Xy.mjs → middleware-DDT9ljTQ.mjs} +1 -1
- package/dist/{middleware-D2O8b03q.mjs → middleware-DHHRwSZH.mjs} +31 -20
- package/dist/{middleware-CUNIlYzF.cjs → middleware-XXuSt_6b.cjs} +99 -14
- package/dist/{mod-CjiK2zrW.d.ts → mod-BLDHhuW0.d.ts} +1 -1
- package/dist/{mod-0dF0OeGw.d.cts → mod-Bx80LUTA.d.cts} +1 -1
- package/dist/{mod-Bv1elC-b.d.ts → mod-DcAiY74j.d.ts} +1 -1
- package/dist/{mod-D5YlzzP3.d.cts → mod-DiuFTYUt.d.cts} +1 -1
- package/dist/{mod-dxOnKp2Y.d.cts → mod-DkE98l_7.d.cts} +3 -1
- package/dist/{mod-BTWFtZwa.d.ts → mod-DqkY4FDr.d.ts} +3 -1
- package/dist/mod.cjs +5 -5
- package/dist/mod.d.cts +6 -6
- package/dist/mod.d.ts +6 -6
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/client.test.mjs +77 -2
- package/dist/nodeinfo/handler.test.mjs +3 -3
- package/dist/nodeinfo/mod.cjs +1 -1
- package/dist/nodeinfo/mod.js +1 -1
- package/dist/nodeinfo/types.test.mjs +2 -2
- package/dist/otel/exporter.test.mjs +2 -2
- package/dist/{owner-DEvZuyOE.d.cts → owner-B3hNQCG8.d.cts} +11 -0
- package/dist/{owner-CnngXDNJ.d.ts → owner-CiBPcC6F.d.ts} +11 -0
- package/dist/owner-DklgvZXn.mjs +175 -0
- package/dist/{proof-Dt5GV-xi.mjs → proof-CI4rsVLu.mjs} +2 -2
- package/dist/{proof-K3inAqnp.js → proof-Dsa_ZC-M.js} +113 -59
- package/dist/{proof-CEn2R6W9.cjs → proof-DzFzyfIc.cjs} +113 -59
- package/dist/{send-CqaJDT2z.mjs → send-DJmW6Z-I.mjs} +73 -6
- package/dist/sig/accept.test.mjs +1 -1
- package/dist/sig/http.test.mjs +45 -5
- package/dist/sig/key.test.mjs +171 -4
- package/dist/sig/ld.test.mjs +59 -4
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.d.cts +3 -3
- package/dist/sig/mod.d.ts +3 -3
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +106 -7
- package/dist/sig/proof.test.mjs +73 -4
- package/dist/{std__assert-Duiq_YC9.mjs → std__assert-BUOJXi-Z.mjs} +1 -26
- package/dist/{temporal-G_GDghC-.mjs → temporal-BanFifxu.mjs} +1 -1
- package/dist/{types-m8u9L86X.cjs → types-DRPjmHVV.cjs} +101 -5
- package/dist/{types-DTONwNqL.js → types-miAYfiGO.js} +85 -7
- package/dist/utils/docloader.test.mjs +325 -5
- package/dist/utils/kv-cache.test.mjs +1 -1
- 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/vocab/cjs.test.mjs +1 -1
- package/package.json +7 -7
- package/dist/assert-ddO5KLpe.mjs +0 -23
- package/dist/docloader-B-IG1dVk.mjs +0 -52
- package/dist/owner-Cygg-fHW.mjs +0 -120
- /package/dist/{accept-Dd__NiUL.mjs → accept-BavzedPB.mjs} +0 -0
- /package/dist/{collection-BD6-SZ6O.mjs → collection-ChgDTHLz.mjs} +0 -0
- /package/dist/{keys-BAK-tUlf.mjs → keys-CtZLJq76.mjs} +0 -0
- /package/dist/{kv-tL2TOE9X.mjs → kv-BrZHNugx.mjs} +0 -0
- /package/dist/{kv-cache-FsT1ljhs.mjs → kv-cache-DqEKe5E0.mjs} +0 -0
- /package/dist/{negotiation-DnsfFF8I.mjs → negotiation-BehA2uul.mjs} +0 -0
- /package/dist/{retry-CJL0poaU.mjs → retry-B_E3V_Dx.mjs} +0 -0
- /package/dist/{types-DCP0WLdt.mjs → types-C37hquWI.mjs} +0 -0
|
@@ -52,31 +52,6 @@ import "./assert_throws-4NwKEy2q.mjs";
|
|
|
52
52
|
throw new AssertionError(msg ?? `Expect ${actualString} >= ${expectedString}`);
|
|
53
53
|
}
|
|
54
54
|
//#endregion
|
|
55
|
-
//#region ../../node_modules/.pnpm/@jsr+std__assert@0.226.0/node_modules/@jsr/std__assert/assert_greater.js
|
|
56
|
-
/**
|
|
57
|
-
* Make an assertion that `actual` is greater than `expected`.
|
|
58
|
-
* If not then throw.
|
|
59
|
-
*
|
|
60
|
-
* @example Usage
|
|
61
|
-
* ```ts no-eval
|
|
62
|
-
* import { assertGreater } from "@std/assert/assert-greater";
|
|
63
|
-
*
|
|
64
|
-
* assertGreater(2, 1); // Doesn't throw
|
|
65
|
-
* assertGreater(1, 1); // Throws
|
|
66
|
-
* assertGreater(0, 1); // Throws
|
|
67
|
-
* ```
|
|
68
|
-
*
|
|
69
|
-
* @typeParam T The type of the values to compare.
|
|
70
|
-
* @param actual The actual value to compare.
|
|
71
|
-
* @param expected The expected value to compare.
|
|
72
|
-
* @param msg The optional message to display if the assertion fails.
|
|
73
|
-
*/ function assertGreater(actual, expected, msg) {
|
|
74
|
-
if (actual > expected) return;
|
|
75
|
-
const actualString = format(actual);
|
|
76
|
-
const expectedString = format(expected);
|
|
77
|
-
throw new AssertionError(msg ?? `Expect ${actualString} > ${expectedString}`);
|
|
78
|
-
}
|
|
79
|
-
//#endregion
|
|
80
55
|
//#region ../../node_modules/.pnpm/@jsr+std__assert@0.226.0/node_modules/@jsr/std__assert/assert_strict_equals.js
|
|
81
56
|
/**
|
|
82
57
|
* Make an assertion that `actual` and `expected` are equal using
|
|
@@ -136,4 +111,4 @@ import "./assert_throws-4NwKEy2q.mjs";
|
|
|
136
111
|
}
|
|
137
112
|
}
|
|
138
113
|
//#endregion
|
|
139
|
-
export { assertExists as
|
|
114
|
+
export { assertExists as i, assertStrictEquals as n, assertGreaterOrEqual as r, assertStringIncludes as t };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { c as getNormalizationContextLoader } from "./ld-
|
|
4
|
+
import { c as getNormalizationContextLoader } from "./ld-cfUwoi0C.mjs";
|
|
5
5
|
import jsonld from "@fedify/vocab-runtime/jsonld";
|
|
6
6
|
//#region src/federation/temporal.ts
|
|
7
7
|
function isPlainObject(value) {
|
|
@@ -3,6 +3,80 @@ const { URLPattern } = require("urlpattern-polyfill");
|
|
|
3
3
|
require("./chunk-DDcVe30Y.cjs");
|
|
4
4
|
let _logtape_logtape = require("@logtape/logtape");
|
|
5
5
|
let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
|
|
6
|
+
//#region src/utils/body.ts
|
|
7
|
+
/** Decoded JSON body limit: 16 MiB. @internal */
|
|
8
|
+
const MAX_BODY_SIZE = 16 * 1024 * 1024;
|
|
9
|
+
/** A body exceeded the byte limit. @internal */
|
|
10
|
+
var BodyTooLargeError = class extends _fedify_vocab_runtime.FetchError {
|
|
11
|
+
/** Creates an error for a body exceeding the given limit. */
|
|
12
|
+
constructor(url, maxBytes) {
|
|
13
|
+
super(url, `Body exceeds the limit of ${maxBytes} bytes`);
|
|
14
|
+
this.name = "BodyTooLargeError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/** Validates a finite, positive byte limit. @internal */
|
|
18
|
+
function validateBodySizeLimit(maxBytes) {
|
|
19
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Reads UTF-8 text while limiting bytes received from the body stream.
|
|
23
|
+
* Fetch responses are already decompressed, so the stream count is authoritative.
|
|
24
|
+
* @param message The response or request to read.
|
|
25
|
+
* @param maxBytes The maximum number of decoded bytes to read.
|
|
26
|
+
* @param url The URL to include in errors and logs.
|
|
27
|
+
* @returns The decoded body text.
|
|
28
|
+
* @throws {BodyTooLargeError} If the body exceeds the limit.
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
async function readBoundedText(message, maxBytes, url) {
|
|
32
|
+
validateBodySizeLimit(maxBytes);
|
|
33
|
+
const reader = message.body?.getReader();
|
|
34
|
+
const tooLarge = () => {
|
|
35
|
+
(0, _logtape_logtape.getLogger)([
|
|
36
|
+
"fedify",
|
|
37
|
+
"runtime",
|
|
38
|
+
"body"
|
|
39
|
+
]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
|
|
40
|
+
url: url.toString(),
|
|
41
|
+
maxBytes
|
|
42
|
+
});
|
|
43
|
+
throw new BodyTooLargeError(url, maxBytes);
|
|
44
|
+
};
|
|
45
|
+
try {
|
|
46
|
+
const length = message.headers.get("Content-Length");
|
|
47
|
+
const encoding = message.headers.get("Content-Encoding");
|
|
48
|
+
if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
|
|
49
|
+
if (reader == null) return "";
|
|
50
|
+
let size = 0;
|
|
51
|
+
const textReader = new ReadableStream({ async pull(controller) {
|
|
52
|
+
const { done, value } = await reader.read();
|
|
53
|
+
if (done) {
|
|
54
|
+
controller.close();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
size += value.byteLength;
|
|
58
|
+
if (size > maxBytes) tooLarge();
|
|
59
|
+
controller.enqueue(value);
|
|
60
|
+
} }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
|
|
61
|
+
try {
|
|
62
|
+
const chunks = [];
|
|
63
|
+
while (true) {
|
|
64
|
+
const { done, value } = await textReader.read();
|
|
65
|
+
if (done) break;
|
|
66
|
+
chunks.push(value);
|
|
67
|
+
}
|
|
68
|
+
return chunks.join("");
|
|
69
|
+
} finally {
|
|
70
|
+
textReader.releaseLock();
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (reader != null) reader.cancel(error).catch(() => {});
|
|
74
|
+
throw error;
|
|
75
|
+
} finally {
|
|
76
|
+
reader?.releaseLock();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
6
80
|
//#region src/nodeinfo/client.ts
|
|
7
81
|
const logger = (0, _logtape_logtape.getLogger)([
|
|
8
82
|
"fedify",
|
|
@@ -19,8 +93,8 @@ const DEFAULT_MAX_REDIRECTION = 5;
|
|
|
19
93
|
* bounce the request to an internal address.
|
|
20
94
|
* @param url The URL to fetch.
|
|
21
95
|
* @param headers The request headers.
|
|
22
|
-
* @returns The final response, or `null` if the URL is disallowed
|
|
23
|
-
* are too many redirects.
|
|
96
|
+
* @returns The final response, or `null` if the URL is disallowed, its
|
|
97
|
+
* hostname cannot be resolved, or there are too many redirects.
|
|
24
98
|
*/
|
|
25
99
|
async function fetchPublicUrl(url, headers) {
|
|
26
100
|
let current = url instanceof URL ? url : new URL(url);
|
|
@@ -30,7 +104,11 @@ async function fetchPublicUrl(url, headers) {
|
|
|
30
104
|
await (0, _fedify_vocab_runtime.validatePublicUrl)(current.href);
|
|
31
105
|
} catch (e) {
|
|
32
106
|
if (e instanceof _fedify_vocab_runtime.UrlError) {
|
|
33
|
-
logger.error("
|
|
107
|
+
if (e.reason === "dns") logger.error("DNS lookup failed for {url}: {error}", {
|
|
108
|
+
url: current.href,
|
|
109
|
+
error: e
|
|
110
|
+
});
|
|
111
|
+
else logger.error("Refused to fetch a private or invalid URL {url}: {error}", {
|
|
34
112
|
url: current.href,
|
|
35
113
|
error: e
|
|
36
114
|
});
|
|
@@ -83,7 +161,7 @@ async function getNodeInfo(url, options = {}) {
|
|
|
83
161
|
});
|
|
84
162
|
return;
|
|
85
163
|
}
|
|
86
|
-
const wellKnownRd = await wellKnownResponse
|
|
164
|
+
const wellKnownRd = JSON.parse(await readBoundedText(wellKnownResponse, MAX_BODY_SIZE, wellKnownUrl));
|
|
87
165
|
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);
|
|
88
166
|
if (link == null || link.href == null) {
|
|
89
167
|
logger.error("Failed to find a NodeInfo document link from {url}: {resourceDescriptor}", {
|
|
@@ -107,7 +185,7 @@ async function getNodeInfo(url, options = {}) {
|
|
|
107
185
|
});
|
|
108
186
|
return;
|
|
109
187
|
}
|
|
110
|
-
const data = await response
|
|
188
|
+
const data = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, nodeInfoUrl));
|
|
111
189
|
if (options.parse === "none") return data;
|
|
112
190
|
return parseNodeInfo(data, { tryBestEffort: options.parse === "best-effort" }) ?? void 0;
|
|
113
191
|
} catch (error) {
|
|
@@ -353,6 +431,18 @@ function nodeInfoToJson(nodeInfo) {
|
|
|
353
431
|
};
|
|
354
432
|
}
|
|
355
433
|
//#endregion
|
|
434
|
+
Object.defineProperty(exports, "BodyTooLargeError", {
|
|
435
|
+
enumerable: true,
|
|
436
|
+
get: function() {
|
|
437
|
+
return BodyTooLargeError;
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
Object.defineProperty(exports, "MAX_BODY_SIZE", {
|
|
441
|
+
enumerable: true,
|
|
442
|
+
get: function() {
|
|
443
|
+
return MAX_BODY_SIZE;
|
|
444
|
+
}
|
|
445
|
+
});
|
|
356
446
|
Object.defineProperty(exports, "getNodeInfo", {
|
|
357
447
|
enumerable: true,
|
|
358
448
|
get: function() {
|
|
@@ -371,3 +461,9 @@ Object.defineProperty(exports, "parseNodeInfo", {
|
|
|
371
461
|
return parseNodeInfo;
|
|
372
462
|
}
|
|
373
463
|
});
|
|
464
|
+
Object.defineProperty(exports, "readBoundedText", {
|
|
465
|
+
enumerable: true,
|
|
466
|
+
get: function() {
|
|
467
|
+
return readBoundedText;
|
|
468
|
+
}
|
|
469
|
+
});
|
|
@@ -1,7 +1,81 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
2
|
import { URLPattern } from "urlpattern-polyfill";
|
|
3
3
|
import { getLogger } from "@logtape/logtape";
|
|
4
|
-
import { UrlError, getUserAgent, validatePublicUrl } 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",
|
|
@@ -18,8 +92,8 @@ const DEFAULT_MAX_REDIRECTION = 5;
|
|
|
18
92
|
* bounce the request to an internal address.
|
|
19
93
|
* @param url The URL to fetch.
|
|
20
94
|
* @param headers The request headers.
|
|
21
|
-
* @returns The final response, or `null` if the URL is disallowed
|
|
22
|
-
* are too many redirects.
|
|
95
|
+
* @returns The final response, or `null` if the URL is disallowed, its
|
|
96
|
+
* hostname cannot be resolved, or there are too many redirects.
|
|
23
97
|
*/
|
|
24
98
|
async function fetchPublicUrl(url, headers) {
|
|
25
99
|
let current = url instanceof URL ? url : new URL(url);
|
|
@@ -29,7 +103,11 @@ async function fetchPublicUrl(url, headers) {
|
|
|
29
103
|
await validatePublicUrl(current.href);
|
|
30
104
|
} catch (e) {
|
|
31
105
|
if (e instanceof UrlError) {
|
|
32
|
-
logger.error("
|
|
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}", {
|
|
33
111
|
url: current.href,
|
|
34
112
|
error: e
|
|
35
113
|
});
|
|
@@ -82,7 +160,7 @@ async function getNodeInfo(url, options = {}) {
|
|
|
82
160
|
});
|
|
83
161
|
return;
|
|
84
162
|
}
|
|
85
|
-
const wellKnownRd = await wellKnownResponse
|
|
163
|
+
const wellKnownRd = JSON.parse(await readBoundedText(wellKnownResponse, MAX_BODY_SIZE, wellKnownUrl));
|
|
86
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);
|
|
87
165
|
if (link == null || link.href == null) {
|
|
88
166
|
logger.error("Failed to find a NodeInfo document link from {url}: {resourceDescriptor}", {
|
|
@@ -106,7 +184,7 @@ async function getNodeInfo(url, options = {}) {
|
|
|
106
184
|
});
|
|
107
185
|
return;
|
|
108
186
|
}
|
|
109
|
-
const data = await response
|
|
187
|
+
const data = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, nodeInfoUrl));
|
|
110
188
|
if (options.parse === "none") return data;
|
|
111
189
|
return parseNodeInfo(data, { tryBestEffort: options.parse === "best-effort" }) ?? void 0;
|
|
112
190
|
} catch (error) {
|
|
@@ -352,4 +430,4 @@ function nodeInfoToJson(nodeInfo) {
|
|
|
352
430
|
};
|
|
353
431
|
}
|
|
354
432
|
//#endregion
|
|
355
|
-
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 };
|
|
@@ -3,13 +3,16 @@ import "urlpattern-polyfill";
|
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { t as esm_default } from "../esm-DVILvP5e.mjs";
|
|
5
5
|
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
|
|
6
|
-
import "../std__assert-
|
|
6
|
+
import "../std__assert-BUOJXi-Z.mjs";
|
|
7
7
|
import { t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
|
|
8
|
-
import { l as verifyRequest } from "../http-
|
|
9
|
-
import { i as rsaPrivateKey2 } from "../keys-
|
|
10
|
-
import { t as getAuthenticatedDocumentLoader } from "../docloader-
|
|
8
|
+
import { l as verifyRequest } from "../http-B5qAeoJd.mjs";
|
|
9
|
+
import { i as rsaPrivateKey2 } from "../keys-CtZLJq76.mjs";
|
|
10
|
+
import { t as getAuthenticatedDocumentLoader } from "../docloader-DX96uPUj.mjs";
|
|
11
11
|
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
12
|
-
import { UrlError } from "@fedify/vocab-runtime";
|
|
12
|
+
import { FetchError, UrlError } from "@fedify/vocab-runtime";
|
|
13
|
+
import { configure, reset } from "@logtape/logtape";
|
|
14
|
+
import dns from "node:dns/promises";
|
|
15
|
+
import { deepStrictEqual, ok, rejects } from "node:assert/strict";
|
|
13
16
|
//#region src/utils/docloader.test.ts
|
|
14
17
|
test("getAuthenticatedDocumentLoader()", async (t) => {
|
|
15
18
|
esm_default.spyGlobal();
|
|
@@ -120,5 +123,322 @@ test("getAuthenticatedDocumentLoader() cancellation", {
|
|
|
120
123
|
});
|
|
121
124
|
esm_default.hardReset();
|
|
122
125
|
});
|
|
126
|
+
test("getAuthenticatedDocumentLoader() bounds JSON after redirects", async () => {
|
|
127
|
+
esm_default.mockGlobal();
|
|
128
|
+
let oversized = true;
|
|
129
|
+
try {
|
|
130
|
+
const url = "https://example.com/bounded";
|
|
131
|
+
esm_default.get(url, {
|
|
132
|
+
status: 302,
|
|
133
|
+
headers: { Location: `${url}/document` }
|
|
134
|
+
});
|
|
135
|
+
esm_default.get(`${url}/document`, () => new Response("{\"name\":\"hello\"}", { headers: {
|
|
136
|
+
"Content-Type": "application/activity+json",
|
|
137
|
+
...oversized ? { "Content-Length": String(16777217) } : {}
|
|
138
|
+
} }));
|
|
139
|
+
const loader = getAuthenticatedDocumentLoader({
|
|
140
|
+
privateKey: rsaPrivateKey2,
|
|
141
|
+
keyId: new URL("https://example.com/key2")
|
|
142
|
+
}, { allowPrivateAddress: true });
|
|
143
|
+
await assertRejects(() => loader(url), FetchError);
|
|
144
|
+
oversized = false;
|
|
145
|
+
assertEquals((await loader(url)).document, { name: "hello" });
|
|
146
|
+
} finally {
|
|
147
|
+
esm_default.hardReset();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
test("getAuthenticatedDocumentLoader() logs DNS failures as such", { ignore: "Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted" }, async (t) => {
|
|
151
|
+
const loader = getAuthenticatedDocumentLoader({
|
|
152
|
+
keyId: new URL("https://example.com/key2"),
|
|
153
|
+
privateKey: rsaPrivateKey2
|
|
154
|
+
});
|
|
155
|
+
for (const result of [
|
|
156
|
+
"throws",
|
|
157
|
+
"empty",
|
|
158
|
+
"private"
|
|
159
|
+
]) await t.step(result, async () => {
|
|
160
|
+
const originalLookup = dns.lookup;
|
|
161
|
+
dns.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
162
|
+
address: "127.0.0.1",
|
|
163
|
+
family: 4
|
|
164
|
+
}]));
|
|
165
|
+
const records = [];
|
|
166
|
+
await configure({
|
|
167
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
168
|
+
loggers: [{
|
|
169
|
+
category: "fedify",
|
|
170
|
+
sinks: ["buffer"],
|
|
171
|
+
lowestLevel: "debug"
|
|
172
|
+
}, {
|
|
173
|
+
category: ["logtape", "meta"],
|
|
174
|
+
sinks: []
|
|
175
|
+
}],
|
|
176
|
+
reset: true
|
|
177
|
+
});
|
|
178
|
+
try {
|
|
179
|
+
const url = "https://dns-failure.invalid/object";
|
|
180
|
+
const error = await assertRejects(() => loader(url), UrlError);
|
|
181
|
+
assertEquals(error.reason, result === "private" ? "disallowed" : "dns");
|
|
182
|
+
assertEquals(records.map((r) => [
|
|
183
|
+
r.level,
|
|
184
|
+
r.rawMessage,
|
|
185
|
+
r.properties.url
|
|
186
|
+
]), [result === "private" ? [
|
|
187
|
+
"error",
|
|
188
|
+
"Disallowed private URL: {url}",
|
|
189
|
+
url
|
|
190
|
+
] : [
|
|
191
|
+
"debug",
|
|
192
|
+
"DNS lookup failed for {url}",
|
|
193
|
+
url
|
|
194
|
+
]]);
|
|
195
|
+
assertEquals(records[0].properties.error, error);
|
|
196
|
+
} finally {
|
|
197
|
+
await reset();
|
|
198
|
+
dns.lookup = originalLookup;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
test("getAuthenticatedDocumentLoader() bounds alternate document chains", async (t) => {
|
|
203
|
+
const base = "https://example.com/alternate-chain/";
|
|
204
|
+
const loader = getAuthenticatedDocumentLoader({
|
|
205
|
+
keyId: new URL("https://example.com/key2"),
|
|
206
|
+
privateKey: rsaPrivateKey2
|
|
207
|
+
}, { allowPrivateAddress: true });
|
|
208
|
+
for (const mode of [
|
|
209
|
+
"header",
|
|
210
|
+
"html",
|
|
211
|
+
"mixed"
|
|
212
|
+
]) for (const hops of [20, 21]) await t.step(`${mode}: ${hops} hops`, async () => {
|
|
213
|
+
esm_default.mockGlobal();
|
|
214
|
+
let requests = 0;
|
|
215
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
216
|
+
requests++;
|
|
217
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
218
|
+
if (index === hops || requests > 30) return Response.json({ done: true });
|
|
219
|
+
const next = `${base}${index + 1}`;
|
|
220
|
+
if (mode === "mixed" && index % 2 === 1) return Response.redirect(next, 302);
|
|
221
|
+
return alternate(next, mode === "html");
|
|
222
|
+
});
|
|
223
|
+
try {
|
|
224
|
+
if (hops === 20) deepStrictEqual((await loader(`${base}0`)).document, { done: true });
|
|
225
|
+
else await rejects(loader(`${base}0`), (error) => {
|
|
226
|
+
ok(error instanceof FetchError);
|
|
227
|
+
ok(error.message.includes("Too many redirections (21)"));
|
|
228
|
+
return true;
|
|
229
|
+
});
|
|
230
|
+
deepStrictEqual(requests, 21);
|
|
231
|
+
} finally {
|
|
232
|
+
esm_default.hardReset();
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
for (const mode of [
|
|
236
|
+
"header",
|
|
237
|
+
"html",
|
|
238
|
+
"alternate-redirect",
|
|
239
|
+
"redirect-alternate",
|
|
240
|
+
"redirect-intermediate"
|
|
241
|
+
]) await t.step(`${mode}: cycle`, async () => {
|
|
242
|
+
esm_default.mockGlobal();
|
|
243
|
+
let requests = 0;
|
|
244
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
245
|
+
requests++;
|
|
246
|
+
if (requests > 30) return Response.json({ stopped: true });
|
|
247
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
248
|
+
const next = `${base}${mode === "redirect-intermediate" ? index === 0 ? 1 : index === 1 ? 2 : 1 : 1 - index}`;
|
|
249
|
+
if (mode === "alternate-redirect" && index === 1 || (mode === "redirect-alternate" || mode === "redirect-intermediate") && index === 0) return Response.redirect(next, 302);
|
|
250
|
+
return alternate(next, mode === "html");
|
|
251
|
+
});
|
|
252
|
+
try {
|
|
253
|
+
await rejects(loader(`${base}0`), (error) => {
|
|
254
|
+
ok(error instanceof FetchError);
|
|
255
|
+
ok(error.message.includes("Redirect loop detected:"));
|
|
256
|
+
return true;
|
|
257
|
+
});
|
|
258
|
+
deepStrictEqual(requests, mode === "redirect-intermediate" ? 3 : 2);
|
|
259
|
+
} finally {
|
|
260
|
+
esm_default.hardReset();
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
await t.step("alternate followed by 20 redirects shares the limit", async () => {
|
|
264
|
+
esm_default.mockGlobal();
|
|
265
|
+
let requests = 0;
|
|
266
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
267
|
+
requests++;
|
|
268
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
269
|
+
if (index === 21) return Response.json({ done: true });
|
|
270
|
+
return index === 0 ? alternate(`${base}1`, false) : Response.redirect(`${base}${index + 1}`, 302);
|
|
271
|
+
});
|
|
272
|
+
try {
|
|
273
|
+
await rejects(loader(`${base}0`), (error) => {
|
|
274
|
+
ok(error instanceof FetchError);
|
|
275
|
+
ok(error.message.includes("Too many redirections (21)"));
|
|
276
|
+
return true;
|
|
277
|
+
});
|
|
278
|
+
deepStrictEqual(requests, 21);
|
|
279
|
+
} finally {
|
|
280
|
+
esm_default.hardReset();
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
await t.step("relative alternate after redirect and isolated calls", async () => {
|
|
284
|
+
esm_default.mockGlobal();
|
|
285
|
+
esm_default.get(`${base}start`, Response.redirect(`${base}html`, 302));
|
|
286
|
+
esm_default.get(`${base}html`, alternate("./document", true));
|
|
287
|
+
esm_default.get(`${base}document`, Response.json({ done: true }));
|
|
288
|
+
try {
|
|
289
|
+
for (let i = 0; i < 2; i++) {
|
|
290
|
+
const results = await Promise.all([loader(`${base}start`), loader(`${base}start`)]);
|
|
291
|
+
for (const result of results) {
|
|
292
|
+
deepStrictEqual(result.document, { done: true });
|
|
293
|
+
deepStrictEqual(result.documentUrl, `${base}document`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} finally {
|
|
297
|
+
esm_default.hardReset();
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
function alternate(next, html) {
|
|
301
|
+
return html ? new Response(`<link rel="alternate" type="application/activity+json" href="${next}">`, { headers: { "Content-Type": "text/html" } }) : new Response("not JSON", { headers: {
|
|
302
|
+
"Content-Type": "text/plain",
|
|
303
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
304
|
+
} });
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
test("getAuthenticatedDocumentLoader() preserves cancellation across alternates", async (t) => {
|
|
308
|
+
const base = "https://example.com/alternate-abort/";
|
|
309
|
+
const loader = getAuthenticatedDocumentLoader({
|
|
310
|
+
keyId: new URL("https://example.com/key2"),
|
|
311
|
+
privateKey: rsaPrivateKey2
|
|
312
|
+
}, { allowPrivateAddress: true });
|
|
313
|
+
for (const html of [false, true]) await t.step(html ? "HTML" : "Link header", async () => {
|
|
314
|
+
esm_default.mockGlobal();
|
|
315
|
+
const controller = new AbortController();
|
|
316
|
+
const reason = /* @__PURE__ */ new Error("Alternate document loading cancelled");
|
|
317
|
+
let requests = 0;
|
|
318
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
319
|
+
requests++;
|
|
320
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
321
|
+
if (index === 2) return Response.json({ done: true });
|
|
322
|
+
if (index === 1) controller.abort(reason);
|
|
323
|
+
const next = `${base}${index + 1}`;
|
|
324
|
+
return new Response(html ? `<link rel="alternate" type="application/activity+json" href="${next}">` : "not JSON", { headers: html ? { "Content-Type": "text/html" } : {
|
|
325
|
+
"Content-Type": "text/plain",
|
|
326
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
327
|
+
} });
|
|
328
|
+
});
|
|
329
|
+
try {
|
|
330
|
+
await rejects(loader(`${base}0`, { signal: controller.signal }), (error) => {
|
|
331
|
+
ok(error === reason);
|
|
332
|
+
return true;
|
|
333
|
+
});
|
|
334
|
+
deepStrictEqual(requests, 2);
|
|
335
|
+
} finally {
|
|
336
|
+
esm_default.hardReset();
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
test("getAuthenticatedDocumentLoader() tracks fallback redirects after alternates", async () => {
|
|
341
|
+
esm_default.mockGlobal();
|
|
342
|
+
let requests = 0;
|
|
343
|
+
let knocks = 0;
|
|
344
|
+
const first = "https://example.com/fallback-alternate/a";
|
|
345
|
+
const second = "https://example.com/fallback-alternate/b";
|
|
346
|
+
esm_default.get(first, () => {
|
|
347
|
+
requests++;
|
|
348
|
+
if (requests > 30) return Response.json({ stopped: true });
|
|
349
|
+
return new Response("", { headers: {
|
|
350
|
+
"Content-Type": "text/plain",
|
|
351
|
+
Link: `<${second}>; rel="alternate"; type="application/activity+json"`
|
|
352
|
+
} });
|
|
353
|
+
});
|
|
354
|
+
esm_default.get(second, () => {
|
|
355
|
+
requests++;
|
|
356
|
+
return ++knocks % 2 === 1 ? new Response("", { status: 401 }) : Response.redirect(first, 302);
|
|
357
|
+
});
|
|
358
|
+
try {
|
|
359
|
+
const loader = getAuthenticatedDocumentLoader({
|
|
360
|
+
keyId: new URL("https://example.com/key2"),
|
|
361
|
+
privateKey: rsaPrivateKey2
|
|
362
|
+
}, { allowPrivateAddress: true });
|
|
363
|
+
await assertRejects(() => loader(first), FetchError, "Redirect loop detected:");
|
|
364
|
+
assertEquals(requests, 3);
|
|
365
|
+
} finally {
|
|
366
|
+
esm_default.hardReset();
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
test("getAuthenticatedDocumentLoader() resolves relative alternates without response URLs", async () => {
|
|
370
|
+
const originalFetch = globalThis.fetch;
|
|
371
|
+
const base = "https://example.com/empty-response-url/";
|
|
372
|
+
const urls = [];
|
|
373
|
+
globalThis.fetch = (input) => {
|
|
374
|
+
const url = input instanceof Request ? input.url : String(input);
|
|
375
|
+
urls.push(url);
|
|
376
|
+
if (url === `${base}start`) return Promise.resolve(Response.redirect(`${base}nested/page`, 302));
|
|
377
|
+
if (url === `${base}nested/page`) return Promise.resolve(new Response("<link rel=\"alternate\" type=\"application/activity+json\" href=\"document\">", { headers: { "Content-Type": "text/html" } }));
|
|
378
|
+
assertEquals(url, `${base}nested/document`);
|
|
379
|
+
return Promise.resolve(Response.json({ done: true }));
|
|
380
|
+
};
|
|
381
|
+
try {
|
|
382
|
+
assertEquals(await getAuthenticatedDocumentLoader({
|
|
383
|
+
keyId: new URL("https://example.com/key2"),
|
|
384
|
+
privateKey: rsaPrivateKey2
|
|
385
|
+
}, { allowPrivateAddress: true })(`${base}start`), {
|
|
386
|
+
contextUrl: null,
|
|
387
|
+
document: { done: true },
|
|
388
|
+
documentUrl: `${base}nested/document`
|
|
389
|
+
});
|
|
390
|
+
assertEquals(urls, [
|
|
391
|
+
`${base}start`,
|
|
392
|
+
`${base}nested/page`,
|
|
393
|
+
`${base}nested/document`
|
|
394
|
+
]);
|
|
395
|
+
} finally {
|
|
396
|
+
globalThis.fetch = originalFetch;
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
test("getAuthenticatedDocumentLoader() validates alternate addresses", async () => {
|
|
400
|
+
esm_default.mockGlobal();
|
|
401
|
+
let privateRequests = 0;
|
|
402
|
+
const url = "https://8.8.8.8/alternate";
|
|
403
|
+
esm_default.get(url, new Response("", { headers: {
|
|
404
|
+
"Content-Type": "text/plain",
|
|
405
|
+
Link: "<http://127.0.0.1/private>; rel=\"alternate\"; type=\"application/activity+json\""
|
|
406
|
+
} }));
|
|
407
|
+
esm_default.get("http://127.0.0.1/private", () => {
|
|
408
|
+
privateRequests++;
|
|
409
|
+
return Response.json({ private: true });
|
|
410
|
+
});
|
|
411
|
+
try {
|
|
412
|
+
const loader = getAuthenticatedDocumentLoader({
|
|
413
|
+
keyId: new URL("https://example.com/key2"),
|
|
414
|
+
privateKey: rsaPrivateKey2
|
|
415
|
+
});
|
|
416
|
+
await assertRejects(() => loader(url), UrlError);
|
|
417
|
+
assertEquals(privateRequests, 0);
|
|
418
|
+
} finally {
|
|
419
|
+
esm_default.hardReset();
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
test("getAuthenticatedDocumentLoader() rejects cancellation before fetching", async () => {
|
|
423
|
+
esm_default.mockGlobal();
|
|
424
|
+
let requests = 0;
|
|
425
|
+
const url = "https://example.com/pre-aborted-alternate";
|
|
426
|
+
esm_default.get(url, () => {
|
|
427
|
+
requests++;
|
|
428
|
+
return Response.json({ done: true });
|
|
429
|
+
});
|
|
430
|
+
try {
|
|
431
|
+
const loader = getAuthenticatedDocumentLoader({
|
|
432
|
+
keyId: new URL("https://example.com/key2"),
|
|
433
|
+
privateKey: rsaPrivateKey2
|
|
434
|
+
}, { allowPrivateAddress: true });
|
|
435
|
+
const controller = new AbortController();
|
|
436
|
+
controller.abort();
|
|
437
|
+
await rejects(loader(url, { signal: controller.signal }), { name: "AbortError" });
|
|
438
|
+
deepStrictEqual(requests, 0);
|
|
439
|
+
} finally {
|
|
440
|
+
esm_default.hardReset();
|
|
441
|
+
}
|
|
442
|
+
});
|
|
123
443
|
//#endregion
|
|
124
444
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
2
|
import { URLPattern } from "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { n as kvCache, t as MockKvStore } from "../kv-cache-
|
|
4
|
+
import { n as kvCache, t as MockKvStore } from "../kv-cache-DqEKe5E0.mjs";
|
|
5
5
|
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
6
6
|
import { deepStrictEqual, throws } from "node:assert";
|
|
7
7
|
import { preloadedContexts } from "@fedify/vocab-runtime";
|