@fedify/vocab-runtime 2.4.0-pr.934.40 → 2.4.0
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 +4 -2
- package/dist/{tests/docloader-Xi61Le8w.mjs → contexts-BK1CqBR5.cjs} +164 -269
- package/dist/{tests/docloader-BFedvemb.cjs → contexts-DPuJ4UYL.js} +160 -288
- package/dist/{docloader-DnUMWHaJ.d.cts → docloader-CYwqh5Df.d.cts} +68 -2
- package/dist/{docloader-xRGn1azD.d.ts → docloader-CYwqh5Df.d.ts} +68 -2
- package/dist/internal/jsonld-cache.cjs +70 -1
- package/dist/internal/jsonld-cache.d.cts +35 -2
- package/dist/internal/jsonld-cache.d.ts +35 -2
- package/dist/internal/jsonld-cache.js +67 -2
- package/dist/internal/portable-dereference.cjs +721 -0
- package/dist/internal/portable-dereference.d.cts +349 -0
- package/dist/internal/portable-dereference.d.ts +349 -0
- package/dist/internal/portable-dereference.js +702 -0
- package/dist/internal/signed-representation.cjs +378 -0
- package/dist/internal/signed-representation.d.cts +144 -0
- package/dist/internal/signed-representation.d.ts +144 -0
- package/dist/internal/signed-representation.js +371 -0
- package/dist/jsonld.cjs +2 -2
- package/dist/mod.cjs +715 -4523
- package/dist/mod.d.cts +406 -6
- package/dist/mod.d.ts +406 -7
- package/dist/mod.js +694 -4518
- package/dist/portable-DqtfLy_1.d.cts +160 -0
- package/dist/portable-DtWsu2yU.d.ts +160 -0
- package/dist/tests/body-CYiu9CO-.mjs +130 -0
- package/dist/tests/body-CkEwROsn.cjs +153 -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/contexts-CIKsin4e.mjs +4512 -0
- package/dist/tests/contexts-DizzBjz4.cjs +4523 -0
- package/dist/tests/decimal.test.cjs +8 -7
- package/dist/tests/decimal.test.mjs +8 -6
- package/dist/tests/digest-3FeH2Y-Q.cjs +176 -0
- package/dist/tests/digest-COC7xDiQ.mjs +141 -0
- package/dist/tests/digest.test.cjs +102 -0
- package/dist/tests/digest.test.d.cts +1 -0
- package/dist/tests/digest.test.d.mts +1 -0
- package/dist/tests/digest.test.mjs +103 -0
- package/dist/tests/docloader-C3YFl6y4.cjs +396 -0
- package/dist/tests/docloader-CvmS4sVq.mjs +373 -0
- package/dist/tests/docloader.test.cjs +692 -39
- package/dist/tests/docloader.test.mjs +686 -34
- package/dist/tests/internal/multicodec.test.cjs +2 -3
- package/dist/tests/internal/multicodec.test.mjs +2 -2
- package/dist/tests/internal/portable-dereference.test.cjs +172 -0
- package/dist/tests/internal/portable-dereference.test.d.cts +1 -0
- package/dist/tests/internal/portable-dereference.test.d.mts +1 -0
- package/dist/tests/internal/portable-dereference.test.mjs +173 -0
- package/dist/tests/jsonld-cache-BPQmOZWD.mjs +342 -0
- package/dist/tests/jsonld-cache-C07AyNOY.cjs +397 -0
- package/dist/tests/jsonld-cache.test.cjs +101 -298
- package/dist/tests/jsonld-cache.test.mjs +94 -289
- package/dist/tests/{key-_wXwomh_.cjs → key-C-AYkdJJ.cjs} +11 -4
- package/dist/tests/{key-CDGDH_vC.mjs → key-C2Db_TAJ.mjs} +11 -3
- package/dist/tests/key.test.cjs +6 -5
- package/dist/tests/key.test.mjs +6 -4
- package/dist/tests/langstr.test.cjs +4 -4
- package/dist/tests/langstr.test.mjs +2 -2
- package/dist/tests/link.test.cjs +2 -3
- package/dist/tests/link.test.mjs +2 -2
- package/dist/tests/multibase/multibase.test.cjs +8 -9
- package/dist/tests/multibase/multibase.test.mjs +6 -6
- package/dist/tests/{multibase-Bz_UUDtL.cjs → multibase-B5Mea7Ip.cjs} +19 -2
- package/dist/tests/{multibase-B4bvakyA.mjs → multibase-BPnF_L4e.mjs} +12 -1
- package/dist/tests/portable-dereference-BNXtgg5U.cjs +278 -0
- package/dist/tests/portable-dereference-DRE5bz-l.mjs +249 -0
- package/dist/tests/portable-media-BLKllU56.cjs +153 -0
- package/dist/tests/portable-media-DRSGBORB.mjs +148 -0
- package/dist/tests/portable-media.test.cjs +222 -0
- package/dist/tests/portable-media.test.d.cts +1 -0
- package/dist/tests/portable-media.test.d.mts +1 -0
- package/dist/tests/portable-media.test.mjs +223 -0
- package/dist/tests/portable-workers.test.cjs +36 -0
- package/dist/tests/portable-workers.test.d.cts +2 -0
- package/dist/tests/portable-workers.test.d.mts +2 -0
- package/dist/tests/portable-workers.test.mjs +35 -0
- package/dist/tests/{request-cCPgOxYG.cjs → request-8vPWtV1-.cjs} +10 -4
- package/dist/tests/{request-BOS-hNaf.mjs → request-DRaOaTqD.mjs} +8 -2
- package/dist/tests/request.test.cjs +7 -4
- package/dist/tests/request.test.mjs +5 -2
- package/dist/tests/signed-representation.test.cjs +588 -0
- package/dist/tests/signed-representation.test.d.cts +1 -0
- package/dist/tests/signed-representation.test.d.mts +1 -0
- package/dist/tests/signed-representation.test.mjs +589 -0
- package/dist/tests/temporal.test.cjs +1 -2
- package/dist/tests/temporal.test.mjs +1 -1
- package/dist/tests/url-BNakuZ8k.cjs +998 -0
- package/dist/tests/url-DMxmp7ZG.mjs +859 -0
- package/dist/tests/url.test.cjs +503 -3
- package/dist/tests/url.test.mjs +503 -2
- package/dist/url-DrGTR8yv.cjs +993 -0
- package/dist/url-Dzyp-NsC.js +860 -0
- package/package.json +27 -4
- package/scripts/test-bun.mjs +17 -0
- package/src/body.test.ts +125 -0
- package/src/body.ts +152 -0
- package/src/contexts/cid-v1.json +114 -0
- package/src/contexts/fep-22cd.json +21 -0
- package/src/contexts/fep-ef61.json +10 -0
- package/src/contexts/miscellany.json +17 -0
- package/src/contexts/security-data-integrity-v1.json +0 -4
- package/src/contexts.ts +40 -0
- package/src/digest.test.ts +220 -0
- package/src/digest.ts +229 -0
- package/src/docloader.test.ts +918 -27
- package/src/docloader.ts +322 -105
- package/src/internal/jsonld-cache.ts +97 -1
- package/src/internal/portable-dereference.test.ts +254 -0
- package/src/internal/portable-dereference.ts +1000 -0
- package/src/internal/signed-representation.ts +565 -0
- package/src/jsonld-cache.test.ts +89 -0
- package/src/key.test.ts +9 -0
- package/src/key.ts +11 -1
- package/src/mod.ts +30 -0
- package/src/multibase/multibase.test.ts +5 -5
- package/src/portable-media.test.ts +293 -0
- package/src/portable-media.ts +264 -0
- package/src/portable-workers.test.ts +81 -0
- package/src/portable.ts +180 -0
- package/src/preprocessor.ts +7 -0
- package/src/request.test.ts +10 -1
- package/src/request.ts +9 -1
- package/src/signed-representation.test.ts +338 -0
- package/src/url.test.ts +844 -1
- package/src/url.ts +717 -17
- package/tsdown.config.ts +2 -0
- package/dist/tests/url-BvjYQdxL.cjs +0 -456
- package/dist/tests/url-a2D8NAgh.mjs +0 -378
- package/dist/url-Ck3dGEwH.cjs +0 -457
- package/dist/url-m1YxGNZ0.js +0 -379
- /package/dist/{chunk-M78iaK0I.cjs → rolldown-runtime-B7lfambq.cjs} +0 -0
- /package/dist/tests/{langstr-CbAxaeEZ.cjs → langstr-C4Fl80ae.cjs} +0 -0
- /package/dist/tests/{langstr-Di5AvKpB.mjs → langstr-CQ26J_L7.mjs} +0 -0
- /package/dist/tests/{link-NUUWCdnK.mjs → link-Cevmc87v.mjs} +0 -0
- /package/dist/tests/{link-FguCydMA.cjs → link-DlKm8bEr.cjs} +0 -0
- /package/dist/tests/{multicodec-CxGVGa91.cjs → multicodec-CLRPeW4N.cjs} +0 -0
- /package/dist/tests/{multicodec-CyFp54fI.mjs → multicodec-CRIj_05H.mjs} +0 -0
- /package/dist/tests/{chunk-C2EiDwsr.cjs → rolldown-runtime-emK7D4bc.cjs} +0 -0
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { t as FetchError } from "./request-DRaOaTqD.mjs";
|
|
2
|
+
import { n as cid_v1_default, t as preloadedContexts } from "./contexts-CIKsin4e.mjs";
|
|
3
|
+
import { n as UrlError } from "./url-DMxmp7ZG.mjs";
|
|
4
|
+
import { i as withDocumentLoaderTimeout, n as getRemoteDocument, r as resolveDocumentLoaderTimeout, t as getDocumentLoader } from "./docloader-CvmS4sVq.mjs";
|
|
5
|
+
import { gzipSync } from "node:zlib";
|
|
6
|
+
import { configure, reset } from "@logtape/logtape";
|
|
7
|
+
import { deepStrictEqual, ok, rejects, throws } from "node:assert";
|
|
8
|
+
import dns from "node:dns/promises";
|
|
9
|
+
import { test } from "@fedify/fixture";
|
|
10
|
+
import { createServer } from "node:http";
|
|
6
11
|
//#region \0rolldown/runtime.js
|
|
7
12
|
var __create = Object.create;
|
|
8
13
|
var __defProp = Object.defineProperty;
|
|
@@ -130,27 +135,27 @@ function parse(input, loose) {
|
|
|
130
135
|
}
|
|
131
136
|
//#endregion
|
|
132
137
|
//#region ../../node_modules/.pnpm/fetch-mock@12.6.0/node_modules/fetch-mock/dist/esm/TypeDescriptor.js
|
|
133
|
-
const valueTypes = new Set([
|
|
138
|
+
const valueTypes = /* @__PURE__ */ new Set([
|
|
134
139
|
"boolean",
|
|
135
140
|
"number",
|
|
136
141
|
"null",
|
|
137
142
|
"string",
|
|
138
143
|
"undefined"
|
|
139
144
|
]);
|
|
140
|
-
const referenceTypes = new Set([
|
|
145
|
+
const referenceTypes = /* @__PURE__ */ new Set([
|
|
141
146
|
"array",
|
|
142
147
|
"function",
|
|
143
148
|
"object",
|
|
144
149
|
"symbol"
|
|
145
150
|
]);
|
|
146
|
-
const detectableTypes = new Set([
|
|
151
|
+
const detectableTypes = /* @__PURE__ */ new Set([
|
|
147
152
|
"boolean",
|
|
148
153
|
"function",
|
|
149
154
|
"number",
|
|
150
155
|
"string",
|
|
151
156
|
"symbol"
|
|
152
157
|
]);
|
|
153
|
-
const typeConstructors = new Set([
|
|
158
|
+
const typeConstructors = /* @__PURE__ */ new Set([
|
|
154
159
|
Boolean,
|
|
155
160
|
Number,
|
|
156
161
|
String
|
|
@@ -221,7 +226,7 @@ var TypeDescriptor = class TypeDescriptor {
|
|
|
221
226
|
};
|
|
222
227
|
//#endregion
|
|
223
228
|
//#region ../../node_modules/.pnpm/fetch-mock@12.6.0/node_modules/fetch-mock/dist/esm/IsSubsetOf.js
|
|
224
|
-
const allowedTypes = new Set([
|
|
229
|
+
const allowedTypes = /* @__PURE__ */ new Set([
|
|
225
230
|
"array",
|
|
226
231
|
"object",
|
|
227
232
|
"function",
|
|
@@ -713,7 +718,14 @@ var __classPrivateFieldGet = function(receiver, state, kind, f) {
|
|
|
713
718
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
714
719
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
715
720
|
};
|
|
716
|
-
var _Route_instances
|
|
721
|
+
var _Route_instances;
|
|
722
|
+
var _a;
|
|
723
|
+
var _Route_responseSubscriptions;
|
|
724
|
+
var _Route_validate;
|
|
725
|
+
var _Route_sanitize;
|
|
726
|
+
var _Route_generateMatcher;
|
|
727
|
+
var _Route_limit;
|
|
728
|
+
var _Route_delayResponse;
|
|
717
729
|
var RouteConfigWrapper = class {
|
|
718
730
|
constructor(config) {
|
|
719
731
|
Object.assign(this, config);
|
|
@@ -1225,6 +1237,52 @@ var esm_default = new class FetchMock {
|
|
|
1225
1237
|
}({ ...defaultFetchMockConfig });
|
|
1226
1238
|
//#endregion
|
|
1227
1239
|
//#region src/docloader.test.ts
|
|
1240
|
+
test("getRemoteDocument() forwards options to alternate documents", async () => {
|
|
1241
|
+
const records = [];
|
|
1242
|
+
await reset();
|
|
1243
|
+
await configure({
|
|
1244
|
+
sinks: { capture: (record) => records.push(record) },
|
|
1245
|
+
loggers: [{
|
|
1246
|
+
category: ["fedify"],
|
|
1247
|
+
lowestLevel: "debug",
|
|
1248
|
+
sinks: ["capture"]
|
|
1249
|
+
}]
|
|
1250
|
+
});
|
|
1251
|
+
try {
|
|
1252
|
+
for (const linkHeader of [true, false]) for (const suppressError of [
|
|
1253
|
+
true,
|
|
1254
|
+
false,
|
|
1255
|
+
void 0
|
|
1256
|
+
]) {
|
|
1257
|
+
records.length = 0;
|
|
1258
|
+
const url = "https://example.com/alternate-source";
|
|
1259
|
+
const alternateUrl = "https://example.com/missing-alternate";
|
|
1260
|
+
const options = {
|
|
1261
|
+
suppressError,
|
|
1262
|
+
signal: new AbortController().signal
|
|
1263
|
+
};
|
|
1264
|
+
const response = new Response(`<link rel="alternate" type="application/activity+json" href="${alternateUrl}">`, { headers: {
|
|
1265
|
+
"Content-Type": "text/html",
|
|
1266
|
+
...linkHeader ? { Link: `<${alternateUrl}>; rel="alternate"; type="application/activity+json"` } : {}
|
|
1267
|
+
} });
|
|
1268
|
+
let alternateRequests = 0;
|
|
1269
|
+
await rejects(() => getRemoteDocument(url, response, async (requestedUrl, forwarded) => {
|
|
1270
|
+
alternateRequests++;
|
|
1271
|
+
deepStrictEqual(requestedUrl, alternateUrl);
|
|
1272
|
+
deepStrictEqual(forwarded, options);
|
|
1273
|
+
return await getRemoteDocument(requestedUrl, new Response(null, { status: 404 }), () => {
|
|
1274
|
+
throw new Error("Unexpected additional alternate");
|
|
1275
|
+
}, forwarded);
|
|
1276
|
+
}, options), FetchError);
|
|
1277
|
+
deepStrictEqual(alternateRequests, 1);
|
|
1278
|
+
const failures = records.filter((record) => record.rawMessage === "Failed to fetch document: {status} {url} {headers}");
|
|
1279
|
+
deepStrictEqual(failures.length, 1);
|
|
1280
|
+
deepStrictEqual(failures[0].level, suppressError ? "warning" : "error");
|
|
1281
|
+
}
|
|
1282
|
+
} finally {
|
|
1283
|
+
await reset();
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1228
1286
|
test("new FetchError()", () => {
|
|
1229
1287
|
const e = new FetchError("https://example.com/", "An error message.");
|
|
1230
1288
|
deepStrictEqual(e.name, "FetchError");
|
|
@@ -1247,7 +1305,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1247
1305
|
name: "Fetched object",
|
|
1248
1306
|
type: "Object"
|
|
1249
1307
|
} });
|
|
1250
|
-
await t.
|
|
1308
|
+
await t.step("ok", async () => {
|
|
1251
1309
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/object"), {
|
|
1252
1310
|
contextUrl: null,
|
|
1253
1311
|
documentUrl: "https://example.com/object",
|
|
@@ -1290,7 +1348,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1290
1348
|
Link: "<https://example.com/object>; rel=\"alternate\"; type=\"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"\""
|
|
1291
1349
|
}
|
|
1292
1350
|
});
|
|
1293
|
-
await t.
|
|
1351
|
+
await t.step("Link header", async () => {
|
|
1294
1352
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/link-ctx"), {
|
|
1295
1353
|
contextUrl: "https://www.w3.org/ns/activitystreams",
|
|
1296
1354
|
documentUrl: "https://example.com/link-ctx",
|
|
@@ -1311,7 +1369,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1311
1369
|
}
|
|
1312
1370
|
});
|
|
1313
1371
|
});
|
|
1314
|
-
await t.
|
|
1372
|
+
await t.step("Link header relative url", async () => {
|
|
1315
1373
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/link-ctx"), {
|
|
1316
1374
|
contextUrl: "https://www.w3.org/ns/activitystreams",
|
|
1317
1375
|
documentUrl: "https://example.com/link-ctx",
|
|
@@ -1332,7 +1390,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1332
1390
|
}
|
|
1333
1391
|
});
|
|
1334
1392
|
});
|
|
1335
|
-
await t.
|
|
1393
|
+
await t.step("wrong Link header syntax", async () => {
|
|
1336
1394
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/obj-w-wrong-link"), {
|
|
1337
1395
|
contextUrl: null,
|
|
1338
1396
|
documentUrl: "https://example.com/obj-w-wrong-link",
|
|
@@ -1356,7 +1414,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1356
1414
|
</html>`,
|
|
1357
1415
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1358
1416
|
});
|
|
1359
|
-
await t.
|
|
1417
|
+
await t.step("HTML <link>", async () => {
|
|
1360
1418
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/html-link"), {
|
|
1361
1419
|
contextUrl: null,
|
|
1362
1420
|
documentUrl: "https://example.com/object",
|
|
@@ -1380,7 +1438,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1380
1438
|
</html>`,
|
|
1381
1439
|
headers: { "Content-Type": "application/xhtml+xml; charset=utf-8" }
|
|
1382
1440
|
});
|
|
1383
|
-
await t.
|
|
1441
|
+
await t.step("XHTML <link>", async () => {
|
|
1384
1442
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/xhtml-link"), {
|
|
1385
1443
|
contextUrl: null,
|
|
1386
1444
|
documentUrl: "https://example.com/object",
|
|
@@ -1406,7 +1464,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1406
1464
|
</html>`,
|
|
1407
1465
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1408
1466
|
});
|
|
1409
|
-
await t.
|
|
1467
|
+
await t.step("HTML <a>", async () => {
|
|
1410
1468
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/html-a"), {
|
|
1411
1469
|
contextUrl: null,
|
|
1412
1470
|
documentUrl: "https://example.com/object",
|
|
@@ -1428,7 +1486,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1428
1486
|
</html>`,
|
|
1429
1487
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1430
1488
|
});
|
|
1431
|
-
await t.
|
|
1489
|
+
await t.step("HTML without ActivityPub alternate link", async () => {
|
|
1432
1490
|
await rejects(() => fetchDocumentLoader("https://example.com/html-no-alternate"), (error) => {
|
|
1433
1491
|
ok(error instanceof FetchError);
|
|
1434
1492
|
ok(error.message.includes("HTML document has no ActivityPub alternate link"));
|
|
@@ -1448,7 +1506,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1448
1506
|
},
|
|
1449
1507
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1450
1508
|
});
|
|
1451
|
-
await t.
|
|
1509
|
+
await t.step("wrong Content-Type with JSON body", async () => {
|
|
1452
1510
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/wrong-content-type"), {
|
|
1453
1511
|
contextUrl: null,
|
|
1454
1512
|
documentUrl: "https://example.com/wrong-content-type",
|
|
@@ -1467,7 +1525,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1467
1525
|
"Content-Type": "text/html; charset=utf-8"
|
|
1468
1526
|
}
|
|
1469
1527
|
});
|
|
1470
|
-
await t.
|
|
1528
|
+
await t.step("HTML Content-Length over limit", async () => {
|
|
1471
1529
|
await rejects(() => fetchDocumentLoader("https://example.com/large-html"), (error) => {
|
|
1472
1530
|
ok(error instanceof FetchError);
|
|
1473
1531
|
ok(error.message.includes("HTML document is too large to scan for an ActivityPub alternate link"));
|
|
@@ -1477,32 +1535,88 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1477
1535
|
return true;
|
|
1478
1536
|
});
|
|
1479
1537
|
});
|
|
1480
|
-
await t.
|
|
1538
|
+
await t.step("HTML Content-Length over limit cancels body", async () => {
|
|
1481
1539
|
let canceled = false;
|
|
1482
1540
|
const response = new Response("<!DOCTYPE html>", { headers: {
|
|
1483
1541
|
"Content-Length": String(1048577),
|
|
1484
1542
|
"Content-Type": "text/html; charset=utf-8"
|
|
1485
1543
|
} });
|
|
1486
|
-
Object.defineProperty(response, "body", { value: { cancel
|
|
1544
|
+
Object.defineProperty(response, "body", { value: new ReadableStream({ cancel() {
|
|
1487
1545
|
canceled = true;
|
|
1488
|
-
} } });
|
|
1546
|
+
} }) });
|
|
1489
1547
|
await rejects(() => getRemoteDocument("https://example.com/large-html-cancel", response, () => {
|
|
1490
1548
|
throw new Error("unexpected alternate fetch");
|
|
1491
1549
|
}), FetchError);
|
|
1492
1550
|
deepStrictEqual(canceled, true);
|
|
1493
1551
|
});
|
|
1494
1552
|
esm_default.get("https://example.com/404", { status: 404 });
|
|
1495
|
-
await t.
|
|
1553
|
+
await t.step("not ok", async () => {
|
|
1496
1554
|
await rejects(() => fetchDocumentLoader("https://example.com/404"), FetchError, "HTTP 404: https://example.com/404");
|
|
1497
1555
|
});
|
|
1498
|
-
await t.
|
|
1556
|
+
await t.step("preloaded contexts", async () => {
|
|
1499
1557
|
for (const [url, document] of Object.entries(preloadedContexts)) deepStrictEqual(await fetchDocumentLoader(url), {
|
|
1500
1558
|
contextUrl: null,
|
|
1501
1559
|
documentUrl: url,
|
|
1502
1560
|
document
|
|
1503
1561
|
});
|
|
1504
1562
|
});
|
|
1505
|
-
|
|
1563
|
+
esm_default.get("https://www.w3.org/ns/cid/v1", { status: 503 });
|
|
1564
|
+
await t.step("preloaded CID v1 context", async () => {
|
|
1565
|
+
const url = "https://www.w3.org/ns/cid/v1";
|
|
1566
|
+
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
1567
|
+
contextUrl: null,
|
|
1568
|
+
documentUrl: url,
|
|
1569
|
+
document: cid_v1_default
|
|
1570
|
+
});
|
|
1571
|
+
});
|
|
1572
|
+
await t.step("preloaded FEP-ef61 context", async () => {
|
|
1573
|
+
const url = "https://w3id.org/fep/ef61";
|
|
1574
|
+
ok(url in preloadedContexts);
|
|
1575
|
+
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
1576
|
+
contextUrl: null,
|
|
1577
|
+
documentUrl: url,
|
|
1578
|
+
document: { "@context": {
|
|
1579
|
+
gateways: {
|
|
1580
|
+
"@id": "https://w3id.org/fep/ef61/gateways",
|
|
1581
|
+
"@type": "@id",
|
|
1582
|
+
"@container": "@list"
|
|
1583
|
+
},
|
|
1584
|
+
digestMultibase: "https://www.w3.org/ns/credentials/v2#digestMultibase"
|
|
1585
|
+
} }
|
|
1586
|
+
});
|
|
1587
|
+
});
|
|
1588
|
+
esm_default.get("https://w3id.org/fep/7aa9", { status: 502 });
|
|
1589
|
+
await t.step("preloaded FEP-7aa9 context", async () => {
|
|
1590
|
+
const url = "https://w3id.org/fep/7aa9";
|
|
1591
|
+
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
1592
|
+
contextUrl: null,
|
|
1593
|
+
documentUrl: url,
|
|
1594
|
+
document: { "@context": {
|
|
1595
|
+
"FeaturedCollection": "https://w3id.org/fep/7aa9#FeaturedCollection",
|
|
1596
|
+
"FeaturedItem": "https://w3id.org/fep/7aa9#FeaturedItem",
|
|
1597
|
+
"FeatureRequest": "https://w3id.org/fep/7aa9#FeatureRequest",
|
|
1598
|
+
"FeatureAuthorization": "https://w3id.org/fep/7aa9#FeatureAuthorization",
|
|
1599
|
+
"topic": {
|
|
1600
|
+
"@id": "https://w3id.org/fep/7aa9#topic",
|
|
1601
|
+
"@type": "@id"
|
|
1602
|
+
},
|
|
1603
|
+
"featuredObject": {
|
|
1604
|
+
"@id": "https://w3id.org/fep/7aa9#featuredObject",
|
|
1605
|
+
"@type": "@id"
|
|
1606
|
+
},
|
|
1607
|
+
"canFeature": {
|
|
1608
|
+
"@id": "https://w3id.org/fep/7aa9#canFeature",
|
|
1609
|
+
"@type": "@id"
|
|
1610
|
+
},
|
|
1611
|
+
"featureAuthorization": {
|
|
1612
|
+
"@id": "https://w3id.org/fep/7aa9#featureAuthorization",
|
|
1613
|
+
"@type": "@id"
|
|
1614
|
+
}
|
|
1615
|
+
} }
|
|
1616
|
+
});
|
|
1617
|
+
deepStrictEqual(esm_default.callHistory.calls(url).length, 0);
|
|
1618
|
+
});
|
|
1619
|
+
await t.step("deny non-HTTP/HTTPS", async () => {
|
|
1506
1620
|
await rejects(() => fetchDocumentLoader("ftp://localhost"), UrlError);
|
|
1507
1621
|
});
|
|
1508
1622
|
esm_default.get("https://example.com/localhost-redirect", {
|
|
@@ -1527,13 +1641,13 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1527
1641
|
name: "Fetched object",
|
|
1528
1642
|
type: "Object"
|
|
1529
1643
|
} });
|
|
1530
|
-
await t.
|
|
1644
|
+
await t.step("allowPrivateAddress: false", async () => {
|
|
1531
1645
|
await rejects(() => fetchDocumentLoader("https://localhost/object"), UrlError);
|
|
1532
1646
|
await rejects(() => fetchDocumentLoader("https://example.com/localhost-redirect"), UrlError);
|
|
1533
1647
|
await rejects(() => fetchDocumentLoader("https://example.com/localhost-link"), UrlError);
|
|
1534
1648
|
});
|
|
1535
1649
|
const fetchDocumentLoader2 = getDocumentLoader({ allowPrivateAddress: true });
|
|
1536
|
-
await t.
|
|
1650
|
+
await t.step("allowPrivateAddress: true", async () => {
|
|
1537
1651
|
const expected = {
|
|
1538
1652
|
contextUrl: null,
|
|
1539
1653
|
documentUrl: "https://localhost/object",
|
|
@@ -1556,12 +1670,12 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1556
1670
|
headers: { Location: `https://example.com/too-many-redirects/${Number(cl.url.split("/").at(-1)) + 1}` }
|
|
1557
1671
|
};
|
|
1558
1672
|
});
|
|
1559
|
-
await t.
|
|
1673
|
+
await t.step("too many redirects", async () => {
|
|
1560
1674
|
redirectAttempts = 0;
|
|
1561
1675
|
await rejects(() => fetchDocumentLoader("https://example.com/too-many-redirects/0"), FetchError, "Too many redirections");
|
|
1562
1676
|
deepStrictEqual(redirectAttempts, 21);
|
|
1563
1677
|
});
|
|
1564
|
-
await t.
|
|
1678
|
+
await t.step("custom max redirection", async () => {
|
|
1565
1679
|
redirectAttempts = 0;
|
|
1566
1680
|
const loader = getDocumentLoader({ maxRedirection: 1 });
|
|
1567
1681
|
await rejects(() => loader("https://example.com/too-many-redirects/0"), FetchError, "Too many redirections");
|
|
@@ -1582,7 +1696,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1582
1696
|
headers: { Location: "https://example.com/redirect-loop-a" }
|
|
1583
1697
|
};
|
|
1584
1698
|
});
|
|
1585
|
-
await t.
|
|
1699
|
+
await t.step("redirect loop", async () => {
|
|
1586
1700
|
loopAttempts = 0;
|
|
1587
1701
|
await rejects(() => fetchDocumentLoader("https://example.com/redirect-loop-a"), FetchError, "Redirect loop detected");
|
|
1588
1702
|
deepStrictEqual(loopAttempts, 2);
|
|
@@ -1595,7 +1709,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1595
1709
|
headers: { Location: "/redirect-loop-relative" }
|
|
1596
1710
|
};
|
|
1597
1711
|
});
|
|
1598
|
-
await t.
|
|
1712
|
+
await t.step("redirect loop with relative location", async () => {
|
|
1599
1713
|
relativeLoopAttempts = 0;
|
|
1600
1714
|
await rejects(() => fetchDocumentLoader("https://example.com/redirect-loop-relative"), FetchError, "Redirect loop detected");
|
|
1601
1715
|
deepStrictEqual(relativeLoopAttempts, 1);
|
|
@@ -1605,7 +1719,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1605
1719
|
body: maliciousPayload,
|
|
1606
1720
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1607
1721
|
});
|
|
1608
|
-
await t.
|
|
1722
|
+
await t.step("ReDoS resistance (CVE-2025-68475)", async () => {
|
|
1609
1723
|
const start = performance.now();
|
|
1610
1724
|
await rejects(() => fetchDocumentLoader("https://example.com/redos"), FetchError);
|
|
1611
1725
|
const elapsed = performance.now() - start;
|
|
@@ -1613,5 +1727,543 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1613
1727
|
});
|
|
1614
1728
|
esm_default.hardReset();
|
|
1615
1729
|
});
|
|
1730
|
+
test("getDocumentLoader() bounds JSON, HTML, and alternate documents", async () => {
|
|
1731
|
+
const url = "https://example.com/bounded";
|
|
1732
|
+
esm_default.mockGlobal();
|
|
1733
|
+
let oversized = true;
|
|
1734
|
+
try {
|
|
1735
|
+
esm_default.get(url, () => new Response("{\"name\":\"hello\"}", { headers: {
|
|
1736
|
+
"Content-Type": "application/activity+json",
|
|
1737
|
+
...oversized ? { "Content-Length": String(16777217) } : {}
|
|
1738
|
+
} }));
|
|
1739
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
1740
|
+
await rejects(loader(url), FetchError);
|
|
1741
|
+
oversized = false;
|
|
1742
|
+
deepStrictEqual((await loader(url)).document, { name: "hello" });
|
|
1743
|
+
oversized = true;
|
|
1744
|
+
esm_default.get(`${url}/html`, () => new Response(" ".repeat(1048577), { headers: { "Content-Type": "text/html" } }));
|
|
1745
|
+
await rejects(getDocumentLoader({ allowPrivateAddress: true })(`${url}/html`), FetchError);
|
|
1746
|
+
esm_default.get(`${url}/alternate`, () => new Response("", { headers: {
|
|
1747
|
+
"Content-Type": "text/html",
|
|
1748
|
+
Link: `<${url}>; rel="alternate"; type="application/activity+json"`
|
|
1749
|
+
} }));
|
|
1750
|
+
await rejects(loader(`${url}/alternate`), FetchError);
|
|
1751
|
+
esm_default.get(`${url}/redirect`, {
|
|
1752
|
+
status: 302,
|
|
1753
|
+
headers: { Location: url }
|
|
1754
|
+
});
|
|
1755
|
+
await rejects(loader(`${url}/redirect`), FetchError);
|
|
1756
|
+
} finally {
|
|
1757
|
+
esm_default.hardReset();
|
|
1758
|
+
}
|
|
1759
|
+
});
|
|
1760
|
+
test("getDocumentLoader() rejects oversized gzip responses from fetch", async () => {
|
|
1761
|
+
const document = { name: "a".repeat(16 * 1024 * 1024) };
|
|
1762
|
+
const compressed = gzipSync(JSON.stringify(document));
|
|
1763
|
+
ok(compressed.byteLength < 64 * 1024);
|
|
1764
|
+
const server = createServer((request, response) => {
|
|
1765
|
+
const body = request.url === "/small" ? gzipSync("{\"name\":\"hello\"}") : compressed;
|
|
1766
|
+
response.writeHead(200, {
|
|
1767
|
+
"Content-Type": "application/activity+json",
|
|
1768
|
+
"Content-Encoding": "gzip",
|
|
1769
|
+
"Content-Length": body.byteLength
|
|
1770
|
+
});
|
|
1771
|
+
response.end(body);
|
|
1772
|
+
});
|
|
1773
|
+
await new Promise((resolve, reject) => {
|
|
1774
|
+
server.once("error", reject);
|
|
1775
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
1776
|
+
});
|
|
1777
|
+
try {
|
|
1778
|
+
const address = server.address();
|
|
1779
|
+
ok(address != null && typeof address !== "string");
|
|
1780
|
+
const url = `http://127.0.0.1:${address.port}/document`;
|
|
1781
|
+
await rejects(getDocumentLoader({ allowPrivateAddress: true })(url), FetchError);
|
|
1782
|
+
deepStrictEqual((await getDocumentLoader({ allowPrivateAddress: true })(new URL("/small", url).href)).document, { name: "hello" });
|
|
1783
|
+
} finally {
|
|
1784
|
+
await new Promise((resolve, reject) => {
|
|
1785
|
+
server.close((error) => error == null ? resolve() : reject(error));
|
|
1786
|
+
server.closeAllConnections();
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
test("getRemoteDocument() bounds JSON by default", async () => {
|
|
1791
|
+
let canceled = false;
|
|
1792
|
+
let pulls = 0;
|
|
1793
|
+
await rejects(getRemoteDocument("https://example.com/document", new Response(new ReadableStream({
|
|
1794
|
+
pull(controller) {
|
|
1795
|
+
pulls++;
|
|
1796
|
+
controller.enqueue(new Uint8Array(64 * 1024).fill(32));
|
|
1797
|
+
},
|
|
1798
|
+
cancel() {
|
|
1799
|
+
canceled = true;
|
|
1800
|
+
}
|
|
1801
|
+
}, { highWaterMark: 0 })), () => {
|
|
1802
|
+
throw new Error("Unexpected alternate document");
|
|
1803
|
+
}), FetchError);
|
|
1804
|
+
deepStrictEqual(canceled, true);
|
|
1805
|
+
deepStrictEqual(pulls, 257);
|
|
1806
|
+
});
|
|
1807
|
+
test("getDocumentLoader() logs DNS failures as such", async (t) => {
|
|
1808
|
+
if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") throw new Error("This DNS test requires Deno network permission (--allow-net).");
|
|
1809
|
+
const loader = getDocumentLoader();
|
|
1810
|
+
for (const result of [
|
|
1811
|
+
"throws",
|
|
1812
|
+
"empty",
|
|
1813
|
+
"private"
|
|
1814
|
+
]) await t.step(result, async () => {
|
|
1815
|
+
const originalLookup = dns.lookup;
|
|
1816
|
+
dns.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
1817
|
+
address: "127.0.0.1",
|
|
1818
|
+
family: 4
|
|
1819
|
+
}]));
|
|
1820
|
+
const records = [];
|
|
1821
|
+
await configure({
|
|
1822
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
1823
|
+
loggers: [{
|
|
1824
|
+
category: "fedify",
|
|
1825
|
+
sinks: ["buffer"],
|
|
1826
|
+
lowestLevel: "debug"
|
|
1827
|
+
}, {
|
|
1828
|
+
category: ["logtape", "meta"],
|
|
1829
|
+
sinks: []
|
|
1830
|
+
}],
|
|
1831
|
+
reset: true
|
|
1832
|
+
});
|
|
1833
|
+
try {
|
|
1834
|
+
const url = "https://dns-failure.invalid/object";
|
|
1835
|
+
let error;
|
|
1836
|
+
await rejects(() => loader(url), (e) => {
|
|
1837
|
+
error = e;
|
|
1838
|
+
return true;
|
|
1839
|
+
});
|
|
1840
|
+
ok(error instanceof UrlError);
|
|
1841
|
+
deepStrictEqual(error.reason, result === "private" ? "disallowed" : "dns");
|
|
1842
|
+
deepStrictEqual(records.map((r) => [
|
|
1843
|
+
r.level,
|
|
1844
|
+
r.rawMessage,
|
|
1845
|
+
r.properties.url
|
|
1846
|
+
]), [result === "private" ? [
|
|
1847
|
+
"error",
|
|
1848
|
+
"Disallowed private URL: {url}",
|
|
1849
|
+
url
|
|
1850
|
+
] : [
|
|
1851
|
+
"debug",
|
|
1852
|
+
"DNS lookup failed for {url}",
|
|
1853
|
+
url
|
|
1854
|
+
]]);
|
|
1855
|
+
ok(records[0].properties.error === error);
|
|
1856
|
+
} finally {
|
|
1857
|
+
await reset();
|
|
1858
|
+
dns.lookup = originalLookup;
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1861
|
+
});
|
|
1862
|
+
test("getDocumentLoader() bounds alternate document chains", async (t) => {
|
|
1863
|
+
const base = "https://example.com/alternate-chain/";
|
|
1864
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
1865
|
+
for (const mode of [
|
|
1866
|
+
"header",
|
|
1867
|
+
"html",
|
|
1868
|
+
"mixed"
|
|
1869
|
+
]) for (const hops of [20, 21]) await t.step(`${mode}: ${hops} hops`, async () => {
|
|
1870
|
+
esm_default.mockGlobal();
|
|
1871
|
+
let requests = 0;
|
|
1872
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
1873
|
+
requests++;
|
|
1874
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
1875
|
+
if (index === hops || requests > 30) return Response.json({ done: true });
|
|
1876
|
+
const next = `${base}${index + 1}`;
|
|
1877
|
+
if (mode === "mixed" && index % 2 === 1) return Response.redirect(next, 302);
|
|
1878
|
+
return alternate(next, mode === "html");
|
|
1879
|
+
});
|
|
1880
|
+
try {
|
|
1881
|
+
if (hops === 20) deepStrictEqual((await loader(`${base}0`)).document, { done: true });
|
|
1882
|
+
else await rejects(loader(`${base}0`), (error) => {
|
|
1883
|
+
ok(error instanceof FetchError);
|
|
1884
|
+
ok(error.message.includes("Too many redirections (21)"));
|
|
1885
|
+
return true;
|
|
1886
|
+
});
|
|
1887
|
+
deepStrictEqual(requests, 21);
|
|
1888
|
+
} finally {
|
|
1889
|
+
esm_default.hardReset();
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
for (const mode of [
|
|
1893
|
+
"header",
|
|
1894
|
+
"html",
|
|
1895
|
+
"alternate-redirect",
|
|
1896
|
+
"redirect-alternate",
|
|
1897
|
+
"redirect-intermediate"
|
|
1898
|
+
]) await t.step(`${mode}: cycle`, async () => {
|
|
1899
|
+
esm_default.mockGlobal();
|
|
1900
|
+
let requests = 0;
|
|
1901
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
1902
|
+
requests++;
|
|
1903
|
+
if (requests > 30) return Response.json({ stopped: true });
|
|
1904
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
1905
|
+
const next = `${base}${mode === "redirect-intermediate" ? index === 0 ? 1 : index === 1 ? 2 : 1 : 1 - index}`;
|
|
1906
|
+
if (mode === "alternate-redirect" && index === 1 || (mode === "redirect-alternate" || mode === "redirect-intermediate") && index === 0) return Response.redirect(next, 302);
|
|
1907
|
+
return alternate(next, mode === "html");
|
|
1908
|
+
});
|
|
1909
|
+
try {
|
|
1910
|
+
await rejects(loader(`${base}0`), (error) => {
|
|
1911
|
+
ok(error instanceof FetchError);
|
|
1912
|
+
ok(error.message.includes("Redirect loop detected:"));
|
|
1913
|
+
return true;
|
|
1914
|
+
});
|
|
1915
|
+
deepStrictEqual(requests, mode === "redirect-intermediate" ? 3 : 2);
|
|
1916
|
+
} finally {
|
|
1917
|
+
esm_default.hardReset();
|
|
1918
|
+
}
|
|
1919
|
+
});
|
|
1920
|
+
await t.step("alternate followed by 20 redirects shares the limit", async () => {
|
|
1921
|
+
esm_default.mockGlobal();
|
|
1922
|
+
let requests = 0;
|
|
1923
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
1924
|
+
requests++;
|
|
1925
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
1926
|
+
if (index === 21) return Response.json({ done: true });
|
|
1927
|
+
return index === 0 ? alternate(`${base}1`, false) : Response.redirect(`${base}${index + 1}`, 302);
|
|
1928
|
+
});
|
|
1929
|
+
try {
|
|
1930
|
+
await rejects(loader(`${base}0`), (error) => {
|
|
1931
|
+
ok(error instanceof FetchError);
|
|
1932
|
+
ok(error.message.includes("Too many redirections (21)"));
|
|
1933
|
+
return true;
|
|
1934
|
+
});
|
|
1935
|
+
deepStrictEqual(requests, 21);
|
|
1936
|
+
} finally {
|
|
1937
|
+
esm_default.hardReset();
|
|
1938
|
+
}
|
|
1939
|
+
});
|
|
1940
|
+
await t.step("relative alternate after redirect and isolated calls", async () => {
|
|
1941
|
+
esm_default.mockGlobal();
|
|
1942
|
+
esm_default.get(`${base}start`, Response.redirect(`${base}html`, 302));
|
|
1943
|
+
esm_default.get(`${base}html`, alternate("./document", true));
|
|
1944
|
+
esm_default.get(`${base}document`, Response.json({ done: true }));
|
|
1945
|
+
try {
|
|
1946
|
+
for (let i = 0; i < 2; i++) {
|
|
1947
|
+
const results = await Promise.all([loader(`${base}start`), loader(`${base}start`)]);
|
|
1948
|
+
for (const result of results) {
|
|
1949
|
+
deepStrictEqual(result.document, { done: true });
|
|
1950
|
+
deepStrictEqual(result.documentUrl, `${base}document`);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
} finally {
|
|
1954
|
+
esm_default.hardReset();
|
|
1955
|
+
}
|
|
1956
|
+
});
|
|
1957
|
+
function alternate(next, html) {
|
|
1958
|
+
return html ? new Response(`<link rel="alternate" type="application/activity+json" href="${next}">`, { headers: { "Content-Type": "text/html" } }) : new Response("not JSON", { headers: {
|
|
1959
|
+
"Content-Type": "text/plain",
|
|
1960
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
1961
|
+
} });
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1964
|
+
test("getDocumentLoader() preserves cancellation across alternates", async (t) => {
|
|
1965
|
+
const base = "https://example.com/alternate-abort/";
|
|
1966
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
1967
|
+
for (const html of [false, true]) await t.step(html ? "HTML" : "Link header", async () => {
|
|
1968
|
+
esm_default.mockGlobal();
|
|
1969
|
+
const controller = new AbortController();
|
|
1970
|
+
let requests = 0;
|
|
1971
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
1972
|
+
requests++;
|
|
1973
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
1974
|
+
if (index === 2) return Response.json({ done: true });
|
|
1975
|
+
if (index === 1) controller.abort();
|
|
1976
|
+
const next = `${base}${index + 1}`;
|
|
1977
|
+
return new Response(html ? `<link rel="alternate" type="application/activity+json" href="${next}">` : "not JSON", { headers: html ? { "Content-Type": "text/html" } : {
|
|
1978
|
+
"Content-Type": "text/plain",
|
|
1979
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
1980
|
+
} });
|
|
1981
|
+
});
|
|
1982
|
+
try {
|
|
1983
|
+
await rejects(loader(`${base}0`, { signal: controller.signal }), { name: "AbortError" });
|
|
1984
|
+
deepStrictEqual(requests, 2);
|
|
1985
|
+
} finally {
|
|
1986
|
+
esm_default.hardReset();
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
});
|
|
1990
|
+
test("getDocumentLoader() rejects cancellation before fetching", async () => {
|
|
1991
|
+
esm_default.mockGlobal();
|
|
1992
|
+
let requests = 0;
|
|
1993
|
+
const url = "https://example.com/pre-aborted-alternate";
|
|
1994
|
+
esm_default.get(url, () => {
|
|
1995
|
+
requests++;
|
|
1996
|
+
return Response.json({ done: true });
|
|
1997
|
+
});
|
|
1998
|
+
try {
|
|
1999
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
2000
|
+
const controller = new AbortController();
|
|
2001
|
+
controller.abort();
|
|
2002
|
+
await rejects(loader(url, { signal: controller.signal }), { name: "AbortError" });
|
|
2003
|
+
deepStrictEqual(requests, 0);
|
|
2004
|
+
} finally {
|
|
2005
|
+
esm_default.hardReset();
|
|
2006
|
+
}
|
|
2007
|
+
});
|
|
2008
|
+
async function withServer(handler, callback) {
|
|
2009
|
+
const server = createServer(handler);
|
|
2010
|
+
await new Promise((resolve, reject) => {
|
|
2011
|
+
server.once("error", reject);
|
|
2012
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
2013
|
+
});
|
|
2014
|
+
try {
|
|
2015
|
+
const address = server.address();
|
|
2016
|
+
ok(address != null && typeof address !== "string");
|
|
2017
|
+
return await callback(`http://127.0.0.1:${address.port}`);
|
|
2018
|
+
} finally {
|
|
2019
|
+
await new Promise((resolve, reject) => {
|
|
2020
|
+
server.close((error) => error == null ? resolve() : reject(error));
|
|
2021
|
+
server.closeAllConnections();
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
function isTimeoutFetchError(error, url) {
|
|
2026
|
+
ok(error instanceof FetchError, String(error));
|
|
2027
|
+
deepStrictEqual(error.url.href, url);
|
|
2028
|
+
deepStrictEqual(error.response, void 0);
|
|
2029
|
+
ok(error.cause instanceof DOMException);
|
|
2030
|
+
deepStrictEqual(error.cause.name, "TimeoutError");
|
|
2031
|
+
return true;
|
|
2032
|
+
}
|
|
2033
|
+
test("resolveDocumentLoaderTimeout() validates timeouts", () => {
|
|
2034
|
+
deepStrictEqual(resolveDocumentLoaderTimeout(void 0), 1e4);
|
|
2035
|
+
deepStrictEqual(resolveDocumentLoaderTimeout(null), null);
|
|
2036
|
+
deepStrictEqual(resolveDocumentLoaderTimeout(1500), 1500);
|
|
2037
|
+
deepStrictEqual(resolveDocumentLoaderTimeout(.5), 1);
|
|
2038
|
+
deepStrictEqual(resolveDocumentLoaderTimeout(2147483647), 2147483647);
|
|
2039
|
+
for (const invalid of [
|
|
2040
|
+
0,
|
|
2041
|
+
-1,
|
|
2042
|
+
NaN,
|
|
2043
|
+
Infinity,
|
|
2044
|
+
2147483648
|
|
2045
|
+
]) throws(() => resolveDocumentLoaderTimeout(invalid), RangeError);
|
|
2046
|
+
throws(() => getDocumentLoader({ timeout: 0 }), RangeError);
|
|
2047
|
+
});
|
|
2048
|
+
test("getDocumentLoader() times out a request without a response", async () => {
|
|
2049
|
+
await withServer(() => {}, async (baseUrl) => {
|
|
2050
|
+
const url = `${baseUrl}/hang`;
|
|
2051
|
+
await rejects(getDocumentLoader({
|
|
2052
|
+
allowPrivateAddress: true,
|
|
2053
|
+
timeout: 100
|
|
2054
|
+
})(url), (e) => isTimeoutFetchError(e, url));
|
|
2055
|
+
});
|
|
2056
|
+
});
|
|
2057
|
+
test("getDocumentLoader() times out a stalled response body", async () => {
|
|
2058
|
+
for (const status of [200, 404]) await withServer((_, response) => {
|
|
2059
|
+
response.writeHead(status, { "Content-Type": "application/activity+json" });
|
|
2060
|
+
response.write("{\"id\":");
|
|
2061
|
+
}, async (baseUrl) => {
|
|
2062
|
+
const url = `${baseUrl}/stalled`;
|
|
2063
|
+
await rejects(getDocumentLoader({
|
|
2064
|
+
allowPrivateAddress: true,
|
|
2065
|
+
timeout: 200
|
|
2066
|
+
})(url), (e) => isTimeoutFetchError(e, url));
|
|
2067
|
+
});
|
|
2068
|
+
});
|
|
2069
|
+
test("getDocumentLoader() shares the timeout across redirects", async () => {
|
|
2070
|
+
let requests = 0;
|
|
2071
|
+
await withServer((request, response) => {
|
|
2072
|
+
requests++;
|
|
2073
|
+
const index = Number(request.url?.split("/").at(-1));
|
|
2074
|
+
setTimeout(() => {
|
|
2075
|
+
if (index >= 15) {
|
|
2076
|
+
response.writeHead(200, { "Content-Type": "application/activity+json" });
|
|
2077
|
+
response.end("{\"done\":true}");
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
response.writeHead(302, { Location: `/redirect/${index + 1}` });
|
|
2081
|
+
response.end();
|
|
2082
|
+
}, 50);
|
|
2083
|
+
}, async (baseUrl) => {
|
|
2084
|
+
const url = `${baseUrl}/redirect/0`;
|
|
2085
|
+
await rejects(getDocumentLoader({
|
|
2086
|
+
allowPrivateAddress: true,
|
|
2087
|
+
timeout: 300
|
|
2088
|
+
})(url), (e) => isTimeoutFetchError(e, url));
|
|
2089
|
+
ok(requests < 16, `requests: ${requests}`);
|
|
2090
|
+
deepStrictEqual((await getDocumentLoader({
|
|
2091
|
+
allowPrivateAddress: true,
|
|
2092
|
+
timeout: null
|
|
2093
|
+
})(url)).document, { done: true });
|
|
2094
|
+
});
|
|
2095
|
+
});
|
|
2096
|
+
test("getDocumentLoader() shares the timeout across alternates", async (t) => {
|
|
2097
|
+
for (const html of [false, true]) await t.step(html ? "HTML" : "Link header", async () => {
|
|
2098
|
+
await withServer((request, response) => {
|
|
2099
|
+
const index = Number(request.url?.split("/").at(-1));
|
|
2100
|
+
setTimeout(() => {
|
|
2101
|
+
if (index >= 15) {
|
|
2102
|
+
response.writeHead(200, { "Content-Type": "application/activity+json" });
|
|
2103
|
+
response.end("{\"done\":true}");
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
const next = `/alternate/${index + 1}`;
|
|
2107
|
+
response.writeHead(200, html ? { "Content-Type": "text/html" } : {
|
|
2108
|
+
"Content-Type": "text/plain",
|
|
2109
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
2110
|
+
});
|
|
2111
|
+
response.end(html ? `<link rel="alternate" type="application/activity+json" href="${next}">` : "not JSON");
|
|
2112
|
+
}, 50);
|
|
2113
|
+
}, async (baseUrl) => {
|
|
2114
|
+
const url = `${baseUrl}/alternate/0`;
|
|
2115
|
+
await rejects(getDocumentLoader({
|
|
2116
|
+
allowPrivateAddress: true,
|
|
2117
|
+
timeout: 300
|
|
2118
|
+
})(url), (e) => isTimeoutFetchError(e, url));
|
|
2119
|
+
});
|
|
2120
|
+
});
|
|
2121
|
+
});
|
|
2122
|
+
test("getDocumentLoader() lets the caller's signal abort a request", async () => {
|
|
2123
|
+
await withServer(() => {}, async (baseUrl) => {
|
|
2124
|
+
const url = `${baseUrl}/hang`;
|
|
2125
|
+
for (const timeout of [
|
|
2126
|
+
void 0,
|
|
2127
|
+
6e4,
|
|
2128
|
+
null
|
|
2129
|
+
]) {
|
|
2130
|
+
const loader = getDocumentLoader({
|
|
2131
|
+
allowPrivateAddress: true,
|
|
2132
|
+
timeout
|
|
2133
|
+
});
|
|
2134
|
+
const controller = new AbortController();
|
|
2135
|
+
const reason = /* @__PURE__ */ new Error("Canceled by the caller");
|
|
2136
|
+
setTimeout(() => controller.abort(reason), 50);
|
|
2137
|
+
await rejects(loader(url, { signal: controller.signal }), (e) => {
|
|
2138
|
+
deepStrictEqual(e, reason);
|
|
2139
|
+
return true;
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
const loader = getDocumentLoader({
|
|
2143
|
+
allowPrivateAddress: true,
|
|
2144
|
+
timeout: 50
|
|
2145
|
+
});
|
|
2146
|
+
const controller = new AbortController();
|
|
2147
|
+
const reason = /* @__PURE__ */ new Error("Canceled by the caller");
|
|
2148
|
+
controller.abort(reason);
|
|
2149
|
+
await rejects(loader(url, { signal: controller.signal }), (e) => {
|
|
2150
|
+
deepStrictEqual(e, reason);
|
|
2151
|
+
return true;
|
|
2152
|
+
});
|
|
2153
|
+
});
|
|
2154
|
+
});
|
|
2155
|
+
test("getDocumentLoader() bounds DNS lookups that ignore the timeout", async () => {
|
|
2156
|
+
if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") throw new Error("This DNS test requires Deno network permission (--allow-net).");
|
|
2157
|
+
esm_default.mockGlobal();
|
|
2158
|
+
let requests = 0;
|
|
2159
|
+
const url = "https://slow-dns.example/object";
|
|
2160
|
+
esm_default.get(url, () => {
|
|
2161
|
+
requests++;
|
|
2162
|
+
return Response.json({ id: url });
|
|
2163
|
+
});
|
|
2164
|
+
const originalLookup = dns.lookup;
|
|
2165
|
+
let resolveLookup = () => {};
|
|
2166
|
+
const lookedUp = new Promise((resolve) => resolveLookup = resolve);
|
|
2167
|
+
dns.lookup = (() => new Promise((resolve) => setTimeout(() => {
|
|
2168
|
+
resolve([{
|
|
2169
|
+
address: "93.184.215.14",
|
|
2170
|
+
family: 4
|
|
2171
|
+
}]);
|
|
2172
|
+
resolveLookup();
|
|
2173
|
+
}, 200)));
|
|
2174
|
+
try {
|
|
2175
|
+
await rejects(getDocumentLoader({ timeout: 50 })(url), (e) => isTimeoutFetchError(e, url));
|
|
2176
|
+
await lookedUp;
|
|
2177
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2178
|
+
deepStrictEqual(requests, 0);
|
|
2179
|
+
} finally {
|
|
2180
|
+
dns.lookup = originalLookup;
|
|
2181
|
+
esm_default.hardReset();
|
|
2182
|
+
}
|
|
2183
|
+
});
|
|
2184
|
+
test("withDocumentLoaderTimeout() cleans up after each call", async () => {
|
|
2185
|
+
const signals = [];
|
|
2186
|
+
const loader = withDocumentLoaderTimeout((url, options) => {
|
|
2187
|
+
signals.push(options.signal);
|
|
2188
|
+
return Promise.resolve({
|
|
2189
|
+
contextUrl: null,
|
|
2190
|
+
document: {},
|
|
2191
|
+
documentUrl: url
|
|
2192
|
+
});
|
|
2193
|
+
}, 50);
|
|
2194
|
+
deepStrictEqual((await Promise.all([loader("https://example.com/a"), loader("https://example.com/b")])).map((r) => r.documentUrl), ["https://example.com/a", "https://example.com/b"]);
|
|
2195
|
+
ok(signals[0] !== signals[1]);
|
|
2196
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2197
|
+
ok(signals.every((signal) => !signal.aborted));
|
|
2198
|
+
const inner = () => Promise.reject(/* @__PURE__ */ new Error("unused"));
|
|
2199
|
+
ok(withDocumentLoaderTimeout(inner, null) === inner);
|
|
2200
|
+
});
|
|
2201
|
+
test("getRemoteDocument() keeps error bodies byte for byte", async () => {
|
|
2202
|
+
const bytes = new Uint8Array([
|
|
2203
|
+
239,
|
|
2204
|
+
187,
|
|
2205
|
+
191,
|
|
2206
|
+
78,
|
|
2207
|
+
111,
|
|
2208
|
+
255,
|
|
2209
|
+
254
|
|
2210
|
+
]);
|
|
2211
|
+
const url = "https://example.com/error";
|
|
2212
|
+
let error;
|
|
2213
|
+
await rejects(getRemoteDocument(url, new Response(bytes, { status: 404 }), () => Promise.reject(/* @__PURE__ */ new Error("unused"))), (e) => {
|
|
2214
|
+
error = e;
|
|
2215
|
+
return true;
|
|
2216
|
+
});
|
|
2217
|
+
ok(error instanceof FetchError);
|
|
2218
|
+
ok(error.response != null);
|
|
2219
|
+
deepStrictEqual(error.response.status, 404);
|
|
2220
|
+
deepStrictEqual(new Uint8Array(await error.response.arrayBuffer()), bytes);
|
|
2221
|
+
});
|
|
2222
|
+
test("getRemoteDocument() keeps metadata of bodiless or large errors", async () => {
|
|
2223
|
+
const url = "https://example.com/error";
|
|
2224
|
+
for (const response of [new Response(null, {
|
|
2225
|
+
status: 304,
|
|
2226
|
+
headers: { ETag: "\"a\"" }
|
|
2227
|
+
}), new Response(/* @__PURE__ */ new Uint8Array(1048577), {
|
|
2228
|
+
status: 500,
|
|
2229
|
+
headers: { ETag: "\"a\"" }
|
|
2230
|
+
})]) await rejects(getRemoteDocument(url, response, () => Promise.reject(/* @__PURE__ */ new Error("unused"))), (e) => {
|
|
2231
|
+
ok(e instanceof FetchError);
|
|
2232
|
+
ok(e.response != null);
|
|
2233
|
+
deepStrictEqual(e.response.status, response.status);
|
|
2234
|
+
deepStrictEqual(e.response.headers.get("ETag"), "\"a\"");
|
|
2235
|
+
deepStrictEqual(e.response.body, null);
|
|
2236
|
+
return true;
|
|
2237
|
+
});
|
|
2238
|
+
});
|
|
2239
|
+
test("getDocumentLoader() reports statuses that Response cannot hold", async () => {
|
|
2240
|
+
await withServer((_, response) => {
|
|
2241
|
+
response.writeHead(999);
|
|
2242
|
+
response.end("Request denied");
|
|
2243
|
+
}, async (baseUrl) => {
|
|
2244
|
+
const url = `${baseUrl}/denied`;
|
|
2245
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
2246
|
+
let error;
|
|
2247
|
+
await rejects(loader(url), (e) => {
|
|
2248
|
+
error = e;
|
|
2249
|
+
return true;
|
|
2250
|
+
});
|
|
2251
|
+
ok(error instanceof FetchError, String(error));
|
|
2252
|
+
deepStrictEqual(error.response?.status, 999);
|
|
2253
|
+
deepStrictEqual(await error.response.text(), "Request denied");
|
|
2254
|
+
});
|
|
2255
|
+
});
|
|
2256
|
+
test("getDocumentLoader() times out a stalled body of such a status", async () => {
|
|
2257
|
+
await withServer((_, response) => {
|
|
2258
|
+
response.writeHead(999);
|
|
2259
|
+
response.write("Request");
|
|
2260
|
+
}, async (baseUrl) => {
|
|
2261
|
+
const url = `${baseUrl}/stalled-denied`;
|
|
2262
|
+
await rejects(getDocumentLoader({
|
|
2263
|
+
allowPrivateAddress: true,
|
|
2264
|
+
timeout: 200
|
|
2265
|
+
})(url), (e) => isTimeoutFetchError(e, url));
|
|
2266
|
+
});
|
|
2267
|
+
});
|
|
1616
2268
|
//#endregion
|
|
1617
2269
|
export {};
|