@fedify/vocab-runtime 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/deno.json +1 -1
- package/dist/mod.cjs +220 -107
- package/dist/mod.d.cts +10 -0
- package/dist/mod.d.ts +10 -0
- package/dist/mod.js +220 -108
- package/dist/tests/body-D-gC5vdl.cjs +95 -0
- package/dist/tests/body-DBqFojnW.mjs +77 -0
- package/dist/tests/body.test.cjs +90 -0
- package/dist/tests/body.test.d.cts +1 -0
- package/dist/tests/body.test.d.mts +1 -0
- package/dist/tests/body.test.mjs +90 -0
- package/dist/tests/decimal.test.cjs +6 -6
- package/dist/tests/decimal.test.mjs +6 -6
- package/dist/tests/{docloader-D-CBVlrJ.mjs → docloader-Beoc8qTH.mjs} +66 -37
- package/dist/tests/{docloader-BcWmxXdo.cjs → docloader-fn4ICzFS.cjs} +71 -36
- package/dist/tests/docloader.test.cjs +321 -4
- package/dist/tests/docloader.test.mjs +320 -4
- package/dist/tests/internal/multicodec.test.cjs +2 -2
- package/dist/tests/internal/multicodec.test.mjs +2 -2
- package/dist/tests/{key-BeTHFQJK.mjs → key-BDIsC6hV.mjs} +2 -2
- package/dist/tests/{key-DTTIntwb.cjs → key-BarKGLQz.cjs} +2 -2
- package/dist/tests/key.test.cjs +3 -3
- package/dist/tests/key.test.mjs +3 -3
- package/dist/tests/langstr.test.cjs +2 -2
- package/dist/tests/langstr.test.mjs +2 -2
- package/dist/tests/link.test.cjs +2 -2
- package/dist/tests/link.test.mjs +2 -2
- package/dist/tests/multibase/multibase.test.cjs +2 -2
- package/dist/tests/multibase/multibase.test.mjs +2 -2
- package/dist/tests/{request-CdMLb0ba.cjs → request-DrO5xzev.cjs} +1 -1
- package/dist/tests/{request-DqsgdBUH.mjs → request-DxeGyJVh.mjs} +1 -1
- package/dist/tests/request.test.cjs +2 -2
- package/dist/tests/request.test.mjs +2 -2
- package/dist/tests/temporal.test.cjs +1 -1
- package/dist/tests/temporal.test.mjs +1 -1
- package/dist/tests/{url-o0e-aXQY.mjs → url-3fyJuqC0.mjs} +14 -4
- package/dist/tests/{url-CWU5HT3Y.cjs → url-vZTgkIZg.cjs} +15 -4
- package/dist/tests/url.test.cjs +37 -4
- package/dist/tests/url.test.mjs +37 -4
- package/package.json +1 -1
- package/src/body.test.ts +125 -0
- package/src/body.ts +95 -0
- package/src/contexts/fep-7aa9.json +24 -0
- package/src/contexts.ts +8 -0
- package/src/docloader.test.ts +431 -1
- package/src/docloader.ts +76 -49
- package/src/url.test.ts +29 -3
- package/src/url.ts +21 -5
- /package/dist/tests/{langstr-CbAxaeEZ.cjs → langstr-BOQHfJDr.cjs} +0 -0
- /package/dist/tests/{langstr-Di5AvKpB.mjs → langstr-DDLc4833.mjs} +0 -0
- /package/dist/tests/{link-NUUWCdnK.mjs → link-CYszIegt.mjs} +0 -0
- /package/dist/tests/{link-FguCydMA.cjs → link-qDTmjPw_.cjs} +0 -0
- /package/dist/tests/{multibase-F7LtMMsK.cjs → multibase-8zhYPa7o.cjs} +0 -0
- /package/dist/tests/{multibase-BgU9XRf7.mjs → multibase-Ds75BvXs.mjs} +0 -0
- /package/dist/tests/{multicodec-CyFp54fI.mjs → multicodec-BBPZjRfg.mjs} +0 -0
- /package/dist/tests/{multicodec-CxGVGa91.cjs → multicodec-ByS9XLsw.cjs} +0 -0
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Do9eywBl.cjs");
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const require_url = require("./url-
|
|
5
|
-
let node_assert = require("node:assert");
|
|
2
|
+
const require_request = require("./request-DrO5xzev.cjs");
|
|
3
|
+
const require_docloader = require("./docloader-fn4ICzFS.cjs");
|
|
4
|
+
const require_url = require("./url-vZTgkIZg.cjs");
|
|
6
5
|
let node_test = require("node:test");
|
|
6
|
+
let node_zlib = require("node:zlib");
|
|
7
|
+
let _logtape_logtape = require("@logtape/logtape");
|
|
8
|
+
let node_assert = require("node:assert");
|
|
9
|
+
let node_dns_promises = require("node:dns/promises");
|
|
10
|
+
node_dns_promises = require_chunk.__toESM(node_dns_promises);
|
|
11
|
+
let node_http = require("node:http");
|
|
7
12
|
//#region ../../node_modules/.pnpm/glob-to-regexp@0.4.1/node_modules/glob-to-regexp/index.js
|
|
8
13
|
var require_glob_to_regexp = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module) => {
|
|
9
14
|
module.exports = function(glob, opts) {
|
|
@@ -1456,6 +1461,37 @@ var esm_default = new class FetchMock {
|
|
|
1456
1461
|
} }
|
|
1457
1462
|
});
|
|
1458
1463
|
});
|
|
1464
|
+
esm_default.get("https://w3id.org/fep/7aa9", { status: 502 });
|
|
1465
|
+
await t.test("preloaded FEP-7aa9 context", async () => {
|
|
1466
|
+
const url = "https://w3id.org/fep/7aa9";
|
|
1467
|
+
(0, node_assert.deepStrictEqual)(await fetchDocumentLoader(url), {
|
|
1468
|
+
contextUrl: null,
|
|
1469
|
+
documentUrl: url,
|
|
1470
|
+
document: { "@context": {
|
|
1471
|
+
"FeaturedCollection": "https://w3id.org/fep/7aa9#FeaturedCollection",
|
|
1472
|
+
"FeaturedItem": "https://w3id.org/fep/7aa9#FeaturedItem",
|
|
1473
|
+
"FeatureRequest": "https://w3id.org/fep/7aa9#FeatureRequest",
|
|
1474
|
+
"FeatureAuthorization": "https://w3id.org/fep/7aa9#FeatureAuthorization",
|
|
1475
|
+
"topic": {
|
|
1476
|
+
"@id": "https://w3id.org/fep/7aa9#topic",
|
|
1477
|
+
"@type": "@id"
|
|
1478
|
+
},
|
|
1479
|
+
"featuredObject": {
|
|
1480
|
+
"@id": "https://w3id.org/fep/7aa9#featuredObject",
|
|
1481
|
+
"@type": "@id"
|
|
1482
|
+
},
|
|
1483
|
+
"canFeature": {
|
|
1484
|
+
"@id": "https://w3id.org/fep/7aa9#canFeature",
|
|
1485
|
+
"@type": "@id"
|
|
1486
|
+
},
|
|
1487
|
+
"featureAuthorization": {
|
|
1488
|
+
"@id": "https://w3id.org/fep/7aa9#featureAuthorization",
|
|
1489
|
+
"@type": "@id"
|
|
1490
|
+
}
|
|
1491
|
+
} }
|
|
1492
|
+
});
|
|
1493
|
+
(0, node_assert.deepStrictEqual)(esm_default.callHistory.calls(url).length, 0);
|
|
1494
|
+
});
|
|
1459
1495
|
await t.test("deny non-HTTP/HTTPS", async () => {
|
|
1460
1496
|
await (0, node_assert.rejects)(() => fetchDocumentLoader("ftp://localhost"), require_url.UrlError);
|
|
1461
1497
|
});
|
|
@@ -1561,4 +1597,285 @@ var esm_default = new class FetchMock {
|
|
|
1561
1597
|
});
|
|
1562
1598
|
esm_default.hardReset();
|
|
1563
1599
|
});
|
|
1600
|
+
(0, node_test.test)("getDocumentLoader() bounds JSON, HTML, and alternate documents", async () => {
|
|
1601
|
+
const url = "https://example.com/bounded";
|
|
1602
|
+
esm_default.mockGlobal();
|
|
1603
|
+
let oversized = true;
|
|
1604
|
+
try {
|
|
1605
|
+
esm_default.get(url, () => new Response("{\"name\":\"hello\"}", { headers: {
|
|
1606
|
+
"Content-Type": "application/activity+json",
|
|
1607
|
+
...oversized ? { "Content-Length": String(16777217) } : {}
|
|
1608
|
+
} }));
|
|
1609
|
+
const loader = require_docloader.getDocumentLoader({ allowPrivateAddress: true });
|
|
1610
|
+
await (0, node_assert.rejects)(loader(url), require_request.FetchError);
|
|
1611
|
+
oversized = false;
|
|
1612
|
+
(0, node_assert.deepStrictEqual)((await loader(url)).document, { name: "hello" });
|
|
1613
|
+
oversized = true;
|
|
1614
|
+
esm_default.get(`${url}/html`, () => new Response(" ".repeat(1048577), { headers: { "Content-Type": "text/html" } }));
|
|
1615
|
+
await (0, node_assert.rejects)(require_docloader.getDocumentLoader({ allowPrivateAddress: true })(`${url}/html`), require_request.FetchError);
|
|
1616
|
+
esm_default.get(`${url}/alternate`, () => new Response("", { headers: {
|
|
1617
|
+
"Content-Type": "text/html",
|
|
1618
|
+
Link: `<${url}>; rel="alternate"; type="application/activity+json"`
|
|
1619
|
+
} }));
|
|
1620
|
+
await (0, node_assert.rejects)(loader(`${url}/alternate`), require_request.FetchError);
|
|
1621
|
+
esm_default.get(`${url}/redirect`, {
|
|
1622
|
+
status: 302,
|
|
1623
|
+
headers: { Location: url }
|
|
1624
|
+
});
|
|
1625
|
+
await (0, node_assert.rejects)(loader(`${url}/redirect`), require_request.FetchError);
|
|
1626
|
+
} finally {
|
|
1627
|
+
esm_default.hardReset();
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
(0, node_test.test)("getDocumentLoader() rejects oversized gzip responses from fetch", async () => {
|
|
1631
|
+
const document = { name: "a".repeat(16 * 1024 * 1024) };
|
|
1632
|
+
const compressed = (0, node_zlib.gzipSync)(JSON.stringify(document));
|
|
1633
|
+
(0, node_assert.ok)(compressed.byteLength < 64 * 1024);
|
|
1634
|
+
const server = (0, node_http.createServer)((request, response) => {
|
|
1635
|
+
const body = request.url === "/small" ? (0, node_zlib.gzipSync)("{\"name\":\"hello\"}") : compressed;
|
|
1636
|
+
response.writeHead(200, {
|
|
1637
|
+
"Content-Type": "application/activity+json",
|
|
1638
|
+
"Content-Encoding": "gzip",
|
|
1639
|
+
"Content-Length": body.byteLength
|
|
1640
|
+
});
|
|
1641
|
+
response.end(body);
|
|
1642
|
+
});
|
|
1643
|
+
await new Promise((resolve, reject) => {
|
|
1644
|
+
server.once("error", reject);
|
|
1645
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
1646
|
+
});
|
|
1647
|
+
try {
|
|
1648
|
+
const address = server.address();
|
|
1649
|
+
(0, node_assert.ok)(address != null && typeof address !== "string");
|
|
1650
|
+
const url = `http://127.0.0.1:${address.port}/document`;
|
|
1651
|
+
await (0, node_assert.rejects)(require_docloader.getDocumentLoader({ allowPrivateAddress: true })(url), require_request.FetchError);
|
|
1652
|
+
(0, node_assert.deepStrictEqual)((await require_docloader.getDocumentLoader({ allowPrivateAddress: true })(new URL("/small", url).href)).document, { name: "hello" });
|
|
1653
|
+
} finally {
|
|
1654
|
+
await new Promise((resolve, reject) => {
|
|
1655
|
+
server.close((error) => error == null ? resolve() : reject(error));
|
|
1656
|
+
server.closeAllConnections();
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
(0, node_test.test)("getRemoteDocument() bounds JSON by default", async () => {
|
|
1661
|
+
let canceled = false;
|
|
1662
|
+
let pulls = 0;
|
|
1663
|
+
await (0, node_assert.rejects)(require_docloader.getRemoteDocument("https://example.com/document", new Response(new ReadableStream({
|
|
1664
|
+
pull(controller) {
|
|
1665
|
+
pulls++;
|
|
1666
|
+
controller.enqueue(new Uint8Array(64 * 1024).fill(32));
|
|
1667
|
+
},
|
|
1668
|
+
cancel() {
|
|
1669
|
+
canceled = true;
|
|
1670
|
+
}
|
|
1671
|
+
}, { highWaterMark: 0 })), () => {
|
|
1672
|
+
throw new Error("Unexpected alternate document");
|
|
1673
|
+
}), require_request.FetchError);
|
|
1674
|
+
(0, node_assert.deepStrictEqual)(canceled, true);
|
|
1675
|
+
(0, node_assert.deepStrictEqual)(pulls, 257);
|
|
1676
|
+
});
|
|
1677
|
+
(0, node_test.test)("getDocumentLoader() logs DNS failures as such", async (t) => {
|
|
1678
|
+
if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") {
|
|
1679
|
+
t.skip("requires the net permission");
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
const loader = require_docloader.getDocumentLoader();
|
|
1683
|
+
for (const result of [
|
|
1684
|
+
"throws",
|
|
1685
|
+
"empty",
|
|
1686
|
+
"private"
|
|
1687
|
+
]) await t.test(result, async () => {
|
|
1688
|
+
const originalLookup = node_dns_promises.default.lookup;
|
|
1689
|
+
node_dns_promises.default.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
1690
|
+
address: "127.0.0.1",
|
|
1691
|
+
family: 4
|
|
1692
|
+
}]));
|
|
1693
|
+
const records = [];
|
|
1694
|
+
await (0, _logtape_logtape.configure)({
|
|
1695
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
1696
|
+
loggers: [{
|
|
1697
|
+
category: "fedify",
|
|
1698
|
+
sinks: ["buffer"],
|
|
1699
|
+
lowestLevel: "debug"
|
|
1700
|
+
}, {
|
|
1701
|
+
category: ["logtape", "meta"],
|
|
1702
|
+
sinks: []
|
|
1703
|
+
}],
|
|
1704
|
+
reset: true
|
|
1705
|
+
});
|
|
1706
|
+
try {
|
|
1707
|
+
const url = "https://dns-failure.invalid/object";
|
|
1708
|
+
let error;
|
|
1709
|
+
await (0, node_assert.rejects)(() => loader(url), (e) => {
|
|
1710
|
+
error = e;
|
|
1711
|
+
return true;
|
|
1712
|
+
});
|
|
1713
|
+
(0, node_assert.ok)(error instanceof require_url.UrlError);
|
|
1714
|
+
(0, node_assert.deepStrictEqual)(error.reason, result === "private" ? "disallowed" : "dns");
|
|
1715
|
+
(0, node_assert.deepStrictEqual)(records.map((r) => [
|
|
1716
|
+
r.level,
|
|
1717
|
+
r.rawMessage,
|
|
1718
|
+
r.properties.url
|
|
1719
|
+
]), [result === "private" ? [
|
|
1720
|
+
"error",
|
|
1721
|
+
"Disallowed private URL: {url}",
|
|
1722
|
+
url
|
|
1723
|
+
] : [
|
|
1724
|
+
"debug",
|
|
1725
|
+
"DNS lookup failed for {url}",
|
|
1726
|
+
url
|
|
1727
|
+
]]);
|
|
1728
|
+
(0, node_assert.ok)(records[0].properties.error === error);
|
|
1729
|
+
} finally {
|
|
1730
|
+
await (0, _logtape_logtape.reset)();
|
|
1731
|
+
node_dns_promises.default.lookup = originalLookup;
|
|
1732
|
+
}
|
|
1733
|
+
});
|
|
1734
|
+
});
|
|
1735
|
+
(0, node_test.test)("getDocumentLoader() bounds alternate document chains", async (t) => {
|
|
1736
|
+
const base = "https://example.com/alternate-chain/";
|
|
1737
|
+
const loader = require_docloader.getDocumentLoader({ allowPrivateAddress: true });
|
|
1738
|
+
for (const mode of [
|
|
1739
|
+
"header",
|
|
1740
|
+
"html",
|
|
1741
|
+
"mixed"
|
|
1742
|
+
]) for (const hops of [20, 21]) await t.test(`${mode}: ${hops} hops`, async () => {
|
|
1743
|
+
esm_default.mockGlobal();
|
|
1744
|
+
let requests = 0;
|
|
1745
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
1746
|
+
requests++;
|
|
1747
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
1748
|
+
if (index === hops || requests > 30) return Response.json({ done: true });
|
|
1749
|
+
const next = `${base}${index + 1}`;
|
|
1750
|
+
if (mode === "mixed" && index % 2 === 1) return Response.redirect(next, 302);
|
|
1751
|
+
return alternate(next, mode === "html");
|
|
1752
|
+
});
|
|
1753
|
+
try {
|
|
1754
|
+
if (hops === 20) (0, node_assert.deepStrictEqual)((await loader(`${base}0`)).document, { done: true });
|
|
1755
|
+
else await (0, node_assert.rejects)(loader(`${base}0`), (error) => {
|
|
1756
|
+
(0, node_assert.ok)(error instanceof require_request.FetchError);
|
|
1757
|
+
(0, node_assert.ok)(error.message.includes("Too many redirections (21)"));
|
|
1758
|
+
return true;
|
|
1759
|
+
});
|
|
1760
|
+
(0, node_assert.deepStrictEqual)(requests, 21);
|
|
1761
|
+
} finally {
|
|
1762
|
+
esm_default.hardReset();
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1765
|
+
for (const mode of [
|
|
1766
|
+
"header",
|
|
1767
|
+
"html",
|
|
1768
|
+
"alternate-redirect",
|
|
1769
|
+
"redirect-alternate",
|
|
1770
|
+
"redirect-intermediate"
|
|
1771
|
+
]) await t.test(`${mode}: cycle`, async () => {
|
|
1772
|
+
esm_default.mockGlobal();
|
|
1773
|
+
let requests = 0;
|
|
1774
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
1775
|
+
requests++;
|
|
1776
|
+
if (requests > 30) return Response.json({ stopped: true });
|
|
1777
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
1778
|
+
const next = `${base}${mode === "redirect-intermediate" ? index === 0 ? 1 : index === 1 ? 2 : 1 : 1 - index}`;
|
|
1779
|
+
if (mode === "alternate-redirect" && index === 1 || (mode === "redirect-alternate" || mode === "redirect-intermediate") && index === 0) return Response.redirect(next, 302);
|
|
1780
|
+
return alternate(next, mode === "html");
|
|
1781
|
+
});
|
|
1782
|
+
try {
|
|
1783
|
+
await (0, node_assert.rejects)(loader(`${base}0`), (error) => {
|
|
1784
|
+
(0, node_assert.ok)(error instanceof require_request.FetchError);
|
|
1785
|
+
(0, node_assert.ok)(error.message.includes("Redirect loop detected:"));
|
|
1786
|
+
return true;
|
|
1787
|
+
});
|
|
1788
|
+
(0, node_assert.deepStrictEqual)(requests, mode === "redirect-intermediate" ? 3 : 2);
|
|
1789
|
+
} finally {
|
|
1790
|
+
esm_default.hardReset();
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
await t.test("alternate followed by 20 redirects shares the limit", async () => {
|
|
1794
|
+
esm_default.mockGlobal();
|
|
1795
|
+
let requests = 0;
|
|
1796
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
1797
|
+
requests++;
|
|
1798
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
1799
|
+
if (index === 21) return Response.json({ done: true });
|
|
1800
|
+
return index === 0 ? alternate(`${base}1`, false) : Response.redirect(`${base}${index + 1}`, 302);
|
|
1801
|
+
});
|
|
1802
|
+
try {
|
|
1803
|
+
await (0, node_assert.rejects)(loader(`${base}0`), (error) => {
|
|
1804
|
+
(0, node_assert.ok)(error instanceof require_request.FetchError);
|
|
1805
|
+
(0, node_assert.ok)(error.message.includes("Too many redirections (21)"));
|
|
1806
|
+
return true;
|
|
1807
|
+
});
|
|
1808
|
+
(0, node_assert.deepStrictEqual)(requests, 21);
|
|
1809
|
+
} finally {
|
|
1810
|
+
esm_default.hardReset();
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
await t.test("relative alternate after redirect and isolated calls", async () => {
|
|
1814
|
+
esm_default.mockGlobal();
|
|
1815
|
+
esm_default.get(`${base}start`, Response.redirect(`${base}html`, 302));
|
|
1816
|
+
esm_default.get(`${base}html`, alternate("./document", true));
|
|
1817
|
+
esm_default.get(`${base}document`, Response.json({ done: true }));
|
|
1818
|
+
try {
|
|
1819
|
+
for (let i = 0; i < 2; i++) {
|
|
1820
|
+
const results = await Promise.all([loader(`${base}start`), loader(`${base}start`)]);
|
|
1821
|
+
for (const result of results) {
|
|
1822
|
+
(0, node_assert.deepStrictEqual)(result.document, { done: true });
|
|
1823
|
+
(0, node_assert.deepStrictEqual)(result.documentUrl, `${base}document`);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
} finally {
|
|
1827
|
+
esm_default.hardReset();
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
function alternate(next, html) {
|
|
1831
|
+
return html ? new Response(`<link rel="alternate" type="application/activity+json" href="${next}">`, { headers: { "Content-Type": "text/html" } }) : new Response("not JSON", { headers: {
|
|
1832
|
+
"Content-Type": "text/plain",
|
|
1833
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
1834
|
+
} });
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
(0, node_test.test)("getDocumentLoader() preserves cancellation across alternates", async (t) => {
|
|
1838
|
+
const base = "https://example.com/alternate-abort/";
|
|
1839
|
+
const loader = require_docloader.getDocumentLoader({ allowPrivateAddress: true });
|
|
1840
|
+
for (const html of [false, true]) await t.test(html ? "HTML" : "Link header", async () => {
|
|
1841
|
+
esm_default.mockGlobal();
|
|
1842
|
+
const controller = new AbortController();
|
|
1843
|
+
let requests = 0;
|
|
1844
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
1845
|
+
requests++;
|
|
1846
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
1847
|
+
if (index === 2) return Response.json({ done: true });
|
|
1848
|
+
if (index === 1) controller.abort();
|
|
1849
|
+
const next = `${base}${index + 1}`;
|
|
1850
|
+
return new Response(html ? `<link rel="alternate" type="application/activity+json" href="${next}">` : "not JSON", { headers: html ? { "Content-Type": "text/html" } : {
|
|
1851
|
+
"Content-Type": "text/plain",
|
|
1852
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
1853
|
+
} });
|
|
1854
|
+
});
|
|
1855
|
+
try {
|
|
1856
|
+
await (0, node_assert.rejects)(loader(`${base}0`, { signal: controller.signal }), { name: "AbortError" });
|
|
1857
|
+
(0, node_assert.deepStrictEqual)(requests, 2);
|
|
1858
|
+
} finally {
|
|
1859
|
+
esm_default.hardReset();
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
});
|
|
1863
|
+
(0, node_test.test)("getDocumentLoader() rejects cancellation before fetching", async () => {
|
|
1864
|
+
esm_default.mockGlobal();
|
|
1865
|
+
let requests = 0;
|
|
1866
|
+
const url = "https://example.com/pre-aborted-alternate";
|
|
1867
|
+
esm_default.get(url, () => {
|
|
1868
|
+
requests++;
|
|
1869
|
+
return Response.json({ done: true });
|
|
1870
|
+
});
|
|
1871
|
+
try {
|
|
1872
|
+
const loader = require_docloader.getDocumentLoader({ allowPrivateAddress: true });
|
|
1873
|
+
const controller = new AbortController();
|
|
1874
|
+
controller.abort();
|
|
1875
|
+
await (0, node_assert.rejects)(loader(url, { signal: controller.signal }), { name: "AbortError" });
|
|
1876
|
+
(0, node_assert.deepStrictEqual)(requests, 0);
|
|
1877
|
+
} finally {
|
|
1878
|
+
esm_default.hardReset();
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1564
1881
|
//#endregion
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as
|
|
3
|
-
import { t as UrlError } from "./url-
|
|
4
|
-
import { deepStrictEqual, ok, rejects } from "node:assert";
|
|
1
|
+
import { t as FetchError } from "./request-DxeGyJVh.mjs";
|
|
2
|
+
import { i as cid_v1_default, n as getRemoteDocument, r as preloadedContexts, t as getDocumentLoader } from "./docloader-Beoc8qTH.mjs";
|
|
3
|
+
import { t as UrlError } from "./url-3fyJuqC0.mjs";
|
|
5
4
|
import { test } from "node:test";
|
|
5
|
+
import { gzipSync } from "node:zlib";
|
|
6
|
+
import { configure, reset } from "@logtape/logtape";
|
|
7
|
+
import { deepStrictEqual, ok, rejects } from "node:assert";
|
|
8
|
+
import dns from "node:dns/promises";
|
|
9
|
+
import { createServer } from "node:http";
|
|
6
10
|
//#region \0rolldown/runtime.js
|
|
7
11
|
var __create = Object.create;
|
|
8
12
|
var __defProp = Object.defineProperty;
|
|
@@ -1475,6 +1479,37 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1475
1479
|
} }
|
|
1476
1480
|
});
|
|
1477
1481
|
});
|
|
1482
|
+
esm_default.get("https://w3id.org/fep/7aa9", { status: 502 });
|
|
1483
|
+
await t.test("preloaded FEP-7aa9 context", async () => {
|
|
1484
|
+
const url = "https://w3id.org/fep/7aa9";
|
|
1485
|
+
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
1486
|
+
contextUrl: null,
|
|
1487
|
+
documentUrl: url,
|
|
1488
|
+
document: { "@context": {
|
|
1489
|
+
"FeaturedCollection": "https://w3id.org/fep/7aa9#FeaturedCollection",
|
|
1490
|
+
"FeaturedItem": "https://w3id.org/fep/7aa9#FeaturedItem",
|
|
1491
|
+
"FeatureRequest": "https://w3id.org/fep/7aa9#FeatureRequest",
|
|
1492
|
+
"FeatureAuthorization": "https://w3id.org/fep/7aa9#FeatureAuthorization",
|
|
1493
|
+
"topic": {
|
|
1494
|
+
"@id": "https://w3id.org/fep/7aa9#topic",
|
|
1495
|
+
"@type": "@id"
|
|
1496
|
+
},
|
|
1497
|
+
"featuredObject": {
|
|
1498
|
+
"@id": "https://w3id.org/fep/7aa9#featuredObject",
|
|
1499
|
+
"@type": "@id"
|
|
1500
|
+
},
|
|
1501
|
+
"canFeature": {
|
|
1502
|
+
"@id": "https://w3id.org/fep/7aa9#canFeature",
|
|
1503
|
+
"@type": "@id"
|
|
1504
|
+
},
|
|
1505
|
+
"featureAuthorization": {
|
|
1506
|
+
"@id": "https://w3id.org/fep/7aa9#featureAuthorization",
|
|
1507
|
+
"@type": "@id"
|
|
1508
|
+
}
|
|
1509
|
+
} }
|
|
1510
|
+
});
|
|
1511
|
+
deepStrictEqual(esm_default.callHistory.calls(url).length, 0);
|
|
1512
|
+
});
|
|
1478
1513
|
await t.test("deny non-HTTP/HTTPS", async () => {
|
|
1479
1514
|
await rejects(() => fetchDocumentLoader("ftp://localhost"), UrlError);
|
|
1480
1515
|
});
|
|
@@ -1580,5 +1615,286 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1580
1615
|
});
|
|
1581
1616
|
esm_default.hardReset();
|
|
1582
1617
|
});
|
|
1618
|
+
test("getDocumentLoader() bounds JSON, HTML, and alternate documents", async () => {
|
|
1619
|
+
const url = "https://example.com/bounded";
|
|
1620
|
+
esm_default.mockGlobal();
|
|
1621
|
+
let oversized = true;
|
|
1622
|
+
try {
|
|
1623
|
+
esm_default.get(url, () => new Response("{\"name\":\"hello\"}", { headers: {
|
|
1624
|
+
"Content-Type": "application/activity+json",
|
|
1625
|
+
...oversized ? { "Content-Length": String(16777217) } : {}
|
|
1626
|
+
} }));
|
|
1627
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
1628
|
+
await rejects(loader(url), FetchError);
|
|
1629
|
+
oversized = false;
|
|
1630
|
+
deepStrictEqual((await loader(url)).document, { name: "hello" });
|
|
1631
|
+
oversized = true;
|
|
1632
|
+
esm_default.get(`${url}/html`, () => new Response(" ".repeat(1048577), { headers: { "Content-Type": "text/html" } }));
|
|
1633
|
+
await rejects(getDocumentLoader({ allowPrivateAddress: true })(`${url}/html`), FetchError);
|
|
1634
|
+
esm_default.get(`${url}/alternate`, () => new Response("", { headers: {
|
|
1635
|
+
"Content-Type": "text/html",
|
|
1636
|
+
Link: `<${url}>; rel="alternate"; type="application/activity+json"`
|
|
1637
|
+
} }));
|
|
1638
|
+
await rejects(loader(`${url}/alternate`), FetchError);
|
|
1639
|
+
esm_default.get(`${url}/redirect`, {
|
|
1640
|
+
status: 302,
|
|
1641
|
+
headers: { Location: url }
|
|
1642
|
+
});
|
|
1643
|
+
await rejects(loader(`${url}/redirect`), FetchError);
|
|
1644
|
+
} finally {
|
|
1645
|
+
esm_default.hardReset();
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
test("getDocumentLoader() rejects oversized gzip responses from fetch", async () => {
|
|
1649
|
+
const document = { name: "a".repeat(16 * 1024 * 1024) };
|
|
1650
|
+
const compressed = gzipSync(JSON.stringify(document));
|
|
1651
|
+
ok(compressed.byteLength < 64 * 1024);
|
|
1652
|
+
const server = createServer((request, response) => {
|
|
1653
|
+
const body = request.url === "/small" ? gzipSync("{\"name\":\"hello\"}") : compressed;
|
|
1654
|
+
response.writeHead(200, {
|
|
1655
|
+
"Content-Type": "application/activity+json",
|
|
1656
|
+
"Content-Encoding": "gzip",
|
|
1657
|
+
"Content-Length": body.byteLength
|
|
1658
|
+
});
|
|
1659
|
+
response.end(body);
|
|
1660
|
+
});
|
|
1661
|
+
await new Promise((resolve, reject) => {
|
|
1662
|
+
server.once("error", reject);
|
|
1663
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
1664
|
+
});
|
|
1665
|
+
try {
|
|
1666
|
+
const address = server.address();
|
|
1667
|
+
ok(address != null && typeof address !== "string");
|
|
1668
|
+
const url = `http://127.0.0.1:${address.port}/document`;
|
|
1669
|
+
await rejects(getDocumentLoader({ allowPrivateAddress: true })(url), FetchError);
|
|
1670
|
+
deepStrictEqual((await getDocumentLoader({ allowPrivateAddress: true })(new URL("/small", url).href)).document, { name: "hello" });
|
|
1671
|
+
} finally {
|
|
1672
|
+
await new Promise((resolve, reject) => {
|
|
1673
|
+
server.close((error) => error == null ? resolve() : reject(error));
|
|
1674
|
+
server.closeAllConnections();
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
test("getRemoteDocument() bounds JSON by default", async () => {
|
|
1679
|
+
let canceled = false;
|
|
1680
|
+
let pulls = 0;
|
|
1681
|
+
await rejects(getRemoteDocument("https://example.com/document", new Response(new ReadableStream({
|
|
1682
|
+
pull(controller) {
|
|
1683
|
+
pulls++;
|
|
1684
|
+
controller.enqueue(new Uint8Array(64 * 1024).fill(32));
|
|
1685
|
+
},
|
|
1686
|
+
cancel() {
|
|
1687
|
+
canceled = true;
|
|
1688
|
+
}
|
|
1689
|
+
}, { highWaterMark: 0 })), () => {
|
|
1690
|
+
throw new Error("Unexpected alternate document");
|
|
1691
|
+
}), FetchError);
|
|
1692
|
+
deepStrictEqual(canceled, true);
|
|
1693
|
+
deepStrictEqual(pulls, 257);
|
|
1694
|
+
});
|
|
1695
|
+
test("getDocumentLoader() logs DNS failures as such", async (t) => {
|
|
1696
|
+
if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") {
|
|
1697
|
+
t.skip("requires the net permission");
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
const loader = getDocumentLoader();
|
|
1701
|
+
for (const result of [
|
|
1702
|
+
"throws",
|
|
1703
|
+
"empty",
|
|
1704
|
+
"private"
|
|
1705
|
+
]) await t.test(result, async () => {
|
|
1706
|
+
const originalLookup = dns.lookup;
|
|
1707
|
+
dns.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
1708
|
+
address: "127.0.0.1",
|
|
1709
|
+
family: 4
|
|
1710
|
+
}]));
|
|
1711
|
+
const records = [];
|
|
1712
|
+
await configure({
|
|
1713
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
1714
|
+
loggers: [{
|
|
1715
|
+
category: "fedify",
|
|
1716
|
+
sinks: ["buffer"],
|
|
1717
|
+
lowestLevel: "debug"
|
|
1718
|
+
}, {
|
|
1719
|
+
category: ["logtape", "meta"],
|
|
1720
|
+
sinks: []
|
|
1721
|
+
}],
|
|
1722
|
+
reset: true
|
|
1723
|
+
});
|
|
1724
|
+
try {
|
|
1725
|
+
const url = "https://dns-failure.invalid/object";
|
|
1726
|
+
let error;
|
|
1727
|
+
await rejects(() => loader(url), (e) => {
|
|
1728
|
+
error = e;
|
|
1729
|
+
return true;
|
|
1730
|
+
});
|
|
1731
|
+
ok(error instanceof UrlError);
|
|
1732
|
+
deepStrictEqual(error.reason, result === "private" ? "disallowed" : "dns");
|
|
1733
|
+
deepStrictEqual(records.map((r) => [
|
|
1734
|
+
r.level,
|
|
1735
|
+
r.rawMessage,
|
|
1736
|
+
r.properties.url
|
|
1737
|
+
]), [result === "private" ? [
|
|
1738
|
+
"error",
|
|
1739
|
+
"Disallowed private URL: {url}",
|
|
1740
|
+
url
|
|
1741
|
+
] : [
|
|
1742
|
+
"debug",
|
|
1743
|
+
"DNS lookup failed for {url}",
|
|
1744
|
+
url
|
|
1745
|
+
]]);
|
|
1746
|
+
ok(records[0].properties.error === error);
|
|
1747
|
+
} finally {
|
|
1748
|
+
await reset();
|
|
1749
|
+
dns.lookup = originalLookup;
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
});
|
|
1753
|
+
test("getDocumentLoader() bounds alternate document chains", async (t) => {
|
|
1754
|
+
const base = "https://example.com/alternate-chain/";
|
|
1755
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
1756
|
+
for (const mode of [
|
|
1757
|
+
"header",
|
|
1758
|
+
"html",
|
|
1759
|
+
"mixed"
|
|
1760
|
+
]) for (const hops of [20, 21]) await t.test(`${mode}: ${hops} hops`, async () => {
|
|
1761
|
+
esm_default.mockGlobal();
|
|
1762
|
+
let requests = 0;
|
|
1763
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
1764
|
+
requests++;
|
|
1765
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
1766
|
+
if (index === hops || requests > 30) return Response.json({ done: true });
|
|
1767
|
+
const next = `${base}${index + 1}`;
|
|
1768
|
+
if (mode === "mixed" && index % 2 === 1) return Response.redirect(next, 302);
|
|
1769
|
+
return alternate(next, mode === "html");
|
|
1770
|
+
});
|
|
1771
|
+
try {
|
|
1772
|
+
if (hops === 20) deepStrictEqual((await loader(`${base}0`)).document, { done: true });
|
|
1773
|
+
else await rejects(loader(`${base}0`), (error) => {
|
|
1774
|
+
ok(error instanceof FetchError);
|
|
1775
|
+
ok(error.message.includes("Too many redirections (21)"));
|
|
1776
|
+
return true;
|
|
1777
|
+
});
|
|
1778
|
+
deepStrictEqual(requests, 21);
|
|
1779
|
+
} finally {
|
|
1780
|
+
esm_default.hardReset();
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
for (const mode of [
|
|
1784
|
+
"header",
|
|
1785
|
+
"html",
|
|
1786
|
+
"alternate-redirect",
|
|
1787
|
+
"redirect-alternate",
|
|
1788
|
+
"redirect-intermediate"
|
|
1789
|
+
]) await t.test(`${mode}: cycle`, async () => {
|
|
1790
|
+
esm_default.mockGlobal();
|
|
1791
|
+
let requests = 0;
|
|
1792
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
1793
|
+
requests++;
|
|
1794
|
+
if (requests > 30) return Response.json({ stopped: true });
|
|
1795
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
1796
|
+
const next = `${base}${mode === "redirect-intermediate" ? index === 0 ? 1 : index === 1 ? 2 : 1 : 1 - index}`;
|
|
1797
|
+
if (mode === "alternate-redirect" && index === 1 || (mode === "redirect-alternate" || mode === "redirect-intermediate") && index === 0) return Response.redirect(next, 302);
|
|
1798
|
+
return alternate(next, mode === "html");
|
|
1799
|
+
});
|
|
1800
|
+
try {
|
|
1801
|
+
await rejects(loader(`${base}0`), (error) => {
|
|
1802
|
+
ok(error instanceof FetchError);
|
|
1803
|
+
ok(error.message.includes("Redirect loop detected:"));
|
|
1804
|
+
return true;
|
|
1805
|
+
});
|
|
1806
|
+
deepStrictEqual(requests, mode === "redirect-intermediate" ? 3 : 2);
|
|
1807
|
+
} finally {
|
|
1808
|
+
esm_default.hardReset();
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
await t.test("alternate followed by 20 redirects shares the limit", async () => {
|
|
1812
|
+
esm_default.mockGlobal();
|
|
1813
|
+
let requests = 0;
|
|
1814
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
1815
|
+
requests++;
|
|
1816
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
1817
|
+
if (index === 21) return Response.json({ done: true });
|
|
1818
|
+
return index === 0 ? alternate(`${base}1`, false) : Response.redirect(`${base}${index + 1}`, 302);
|
|
1819
|
+
});
|
|
1820
|
+
try {
|
|
1821
|
+
await rejects(loader(`${base}0`), (error) => {
|
|
1822
|
+
ok(error instanceof FetchError);
|
|
1823
|
+
ok(error.message.includes("Too many redirections (21)"));
|
|
1824
|
+
return true;
|
|
1825
|
+
});
|
|
1826
|
+
deepStrictEqual(requests, 21);
|
|
1827
|
+
} finally {
|
|
1828
|
+
esm_default.hardReset();
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
await t.test("relative alternate after redirect and isolated calls", async () => {
|
|
1832
|
+
esm_default.mockGlobal();
|
|
1833
|
+
esm_default.get(`${base}start`, Response.redirect(`${base}html`, 302));
|
|
1834
|
+
esm_default.get(`${base}html`, alternate("./document", true));
|
|
1835
|
+
esm_default.get(`${base}document`, Response.json({ done: true }));
|
|
1836
|
+
try {
|
|
1837
|
+
for (let i = 0; i < 2; i++) {
|
|
1838
|
+
const results = await Promise.all([loader(`${base}start`), loader(`${base}start`)]);
|
|
1839
|
+
for (const result of results) {
|
|
1840
|
+
deepStrictEqual(result.document, { done: true });
|
|
1841
|
+
deepStrictEqual(result.documentUrl, `${base}document`);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
} finally {
|
|
1845
|
+
esm_default.hardReset();
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
function alternate(next, html) {
|
|
1849
|
+
return html ? new Response(`<link rel="alternate" type="application/activity+json" href="${next}">`, { headers: { "Content-Type": "text/html" } }) : new Response("not JSON", { headers: {
|
|
1850
|
+
"Content-Type": "text/plain",
|
|
1851
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
1852
|
+
} });
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1855
|
+
test("getDocumentLoader() preserves cancellation across alternates", async (t) => {
|
|
1856
|
+
const base = "https://example.com/alternate-abort/";
|
|
1857
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
1858
|
+
for (const html of [false, true]) await t.test(html ? "HTML" : "Link header", async () => {
|
|
1859
|
+
esm_default.mockGlobal();
|
|
1860
|
+
const controller = new AbortController();
|
|
1861
|
+
let requests = 0;
|
|
1862
|
+
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
1863
|
+
requests++;
|
|
1864
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
1865
|
+
if (index === 2) return Response.json({ done: true });
|
|
1866
|
+
if (index === 1) controller.abort();
|
|
1867
|
+
const next = `${base}${index + 1}`;
|
|
1868
|
+
return new Response(html ? `<link rel="alternate" type="application/activity+json" href="${next}">` : "not JSON", { headers: html ? { "Content-Type": "text/html" } : {
|
|
1869
|
+
"Content-Type": "text/plain",
|
|
1870
|
+
Link: `<${next}>; rel="alternate"; type="application/activity+json"`
|
|
1871
|
+
} });
|
|
1872
|
+
});
|
|
1873
|
+
try {
|
|
1874
|
+
await rejects(loader(`${base}0`, { signal: controller.signal }), { name: "AbortError" });
|
|
1875
|
+
deepStrictEqual(requests, 2);
|
|
1876
|
+
} finally {
|
|
1877
|
+
esm_default.hardReset();
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
});
|
|
1881
|
+
test("getDocumentLoader() rejects cancellation before fetching", async () => {
|
|
1882
|
+
esm_default.mockGlobal();
|
|
1883
|
+
let requests = 0;
|
|
1884
|
+
const url = "https://example.com/pre-aborted-alternate";
|
|
1885
|
+
esm_default.get(url, () => {
|
|
1886
|
+
requests++;
|
|
1887
|
+
return Response.json({ done: true });
|
|
1888
|
+
});
|
|
1889
|
+
try {
|
|
1890
|
+
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
1891
|
+
const controller = new AbortController();
|
|
1892
|
+
controller.abort();
|
|
1893
|
+
await rejects(loader(url, { signal: controller.signal }), { name: "AbortError" });
|
|
1894
|
+
deepStrictEqual(requests, 0);
|
|
1895
|
+
} finally {
|
|
1896
|
+
esm_default.hardReset();
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1583
1899
|
//#endregion
|
|
1584
1900
|
export {};
|