@fedify/vocab-runtime 2.4.0-dev.2233 → 2.4.0-dev.2245
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 +2 -2
- package/dist/internal/jsonld-cache.cjs +1 -1
- package/dist/internal/jsonld-cache.js +1 -1
- package/dist/internal/portable-dereference.cjs +3 -6
- package/dist/internal/portable-dereference.js +3 -6
- package/dist/mod.cjs +2 -2
- package/dist/mod.d.cts +14 -3
- package/dist/mod.d.ts +14 -3
- package/dist/mod.js +2 -2
- package/dist/tests/{body-DQKTFMsH.cjs → body-C7LfsQJ3.cjs} +1 -1
- package/dist/tests/{body-BEYUpKvX.mjs → body-D50XSh7s.mjs} +1 -1
- package/dist/tests/body.test.cjs +1 -1
- package/dist/tests/body.test.mjs +1 -1
- package/dist/tests/decimal.test.cjs +3 -3
- package/dist/tests/decimal.test.mjs +3 -3
- package/dist/tests/{docloader-ra1lbBfP.mjs → docloader-BdcRw8S9.mjs} +3 -3
- package/dist/tests/{docloader-DHCYdBXf.cjs → docloader-oUrWeeUz.cjs} +3 -3
- package/dist/tests/docloader.test.cjs +59 -65
- package/dist/tests/docloader.test.mjs +38 -44
- package/dist/tests/internal/portable-dereference.test.cjs +21 -149
- package/dist/tests/internal/portable-dereference.test.mjs +3 -131
- package/dist/tests/{jsonld-cache-BKrsWPCQ.mjs → jsonld-cache-CKfgtxPB.mjs} +1 -1
- package/dist/tests/{jsonld-cache-BTgizq9A.cjs → jsonld-cache-DzxT9IXH.cjs} +1 -1
- package/dist/tests/jsonld-cache.test.cjs +2 -2
- package/dist/tests/jsonld-cache.test.mjs +2 -2
- package/dist/tests/multibase/multibase.test.cjs +7 -7
- package/dist/tests/multibase/multibase.test.mjs +5 -5
- package/dist/tests/portable-dereference-C_cwGjz6.mjs +132 -0
- package/dist/tests/portable-dereference-CyFYTBnr.cjs +149 -0
- package/dist/tests/{portable-media-DHaAEpaQ.cjs → portable-media-Bj5FXpUr.cjs} +3 -3
- package/dist/tests/{portable-media-1NjUQari.mjs → portable-media-D4NJcqkI.mjs} +3 -3
- package/dist/tests/portable-media.test.cjs +1 -1
- package/dist/tests/portable-media.test.mjs +1 -1
- 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-BQlkB8du.cjs → request-CBCk-e0R.cjs} +1 -1
- package/dist/tests/{request-D3elVRyB.mjs → request-wKyc91L5.mjs} +1 -1
- package/dist/tests/request.test.cjs +1 -1
- package/dist/tests/request.test.mjs +1 -1
- package/dist/tests/{url-Cdu4ASgJ.mjs → url-CWC-NI3a.mjs} +63 -7
- package/dist/tests/{url-CtHyzMR4.cjs → url-DsMBGqCc.cjs} +63 -7
- package/dist/tests/url.test.cjs +74 -1
- package/dist/tests/url.test.mjs +74 -1
- package/dist/{url-BHfQuRdy.cjs → url-BJMXv2kE.cjs} +63 -7
- package/dist/{url-DqqDZY9i.js → url-CZm2-7ZO.js} +63 -7
- package/package.json +6 -3
- package/scripts/test-bun.mjs +17 -0
- package/src/docloader.test.ts +82 -74
- package/src/internal/portable-dereference.ts +2 -5
- package/src/multibase/multibase.test.ts +5 -5
- package/src/portable-workers.test.ts +81 -0
- package/src/url.test.ts +90 -0
- package/src/url.ts +115 -13
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { t as FetchError } from "./request-
|
|
1
|
+
import { t as FetchError } from "./request-wKyc91L5.mjs";
|
|
2
2
|
import { n as cid_v1_default, t as preloadedContexts } from "./contexts-CIKsin4e.mjs";
|
|
3
|
-
import { n as UrlError } from "./url-
|
|
4
|
-
import { i as withDocumentLoaderTimeout, n as getRemoteDocument, r as resolveDocumentLoaderTimeout, t as getDocumentLoader } from "./docloader-
|
|
5
|
-
import { test } from "node:test";
|
|
3
|
+
import { n as UrlError } from "./url-CWC-NI3a.mjs";
|
|
4
|
+
import { i as withDocumentLoaderTimeout, n as getRemoteDocument, r as resolveDocumentLoaderTimeout, t as getDocumentLoader } from "./docloader-BdcRw8S9.mjs";
|
|
6
5
|
import { gzipSync } from "node:zlib";
|
|
7
6
|
import { configure, reset } from "@logtape/logtape";
|
|
8
7
|
import { deepStrictEqual, ok, rejects, throws } from "node:assert";
|
|
9
8
|
import dns from "node:dns/promises";
|
|
9
|
+
import { test } from "@fedify/fixture";
|
|
10
10
|
import { createServer } from "node:http";
|
|
11
11
|
//#region \0rolldown/runtime.js
|
|
12
12
|
var __create = Object.create;
|
|
@@ -1305,7 +1305,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1305
1305
|
name: "Fetched object",
|
|
1306
1306
|
type: "Object"
|
|
1307
1307
|
} });
|
|
1308
|
-
await t.
|
|
1308
|
+
await t.step("ok", async () => {
|
|
1309
1309
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/object"), {
|
|
1310
1310
|
contextUrl: null,
|
|
1311
1311
|
documentUrl: "https://example.com/object",
|
|
@@ -1348,7 +1348,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1348
1348
|
Link: "<https://example.com/object>; rel=\"alternate\"; type=\"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"\""
|
|
1349
1349
|
}
|
|
1350
1350
|
});
|
|
1351
|
-
await t.
|
|
1351
|
+
await t.step("Link header", async () => {
|
|
1352
1352
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/link-ctx"), {
|
|
1353
1353
|
contextUrl: "https://www.w3.org/ns/activitystreams",
|
|
1354
1354
|
documentUrl: "https://example.com/link-ctx",
|
|
@@ -1369,7 +1369,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1369
1369
|
}
|
|
1370
1370
|
});
|
|
1371
1371
|
});
|
|
1372
|
-
await t.
|
|
1372
|
+
await t.step("Link header relative url", async () => {
|
|
1373
1373
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/link-ctx"), {
|
|
1374
1374
|
contextUrl: "https://www.w3.org/ns/activitystreams",
|
|
1375
1375
|
documentUrl: "https://example.com/link-ctx",
|
|
@@ -1390,7 +1390,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1390
1390
|
}
|
|
1391
1391
|
});
|
|
1392
1392
|
});
|
|
1393
|
-
await t.
|
|
1393
|
+
await t.step("wrong Link header syntax", async () => {
|
|
1394
1394
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/obj-w-wrong-link"), {
|
|
1395
1395
|
contextUrl: null,
|
|
1396
1396
|
documentUrl: "https://example.com/obj-w-wrong-link",
|
|
@@ -1414,7 +1414,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1414
1414
|
</html>`,
|
|
1415
1415
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1416
1416
|
});
|
|
1417
|
-
await t.
|
|
1417
|
+
await t.step("HTML <link>", async () => {
|
|
1418
1418
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/html-link"), {
|
|
1419
1419
|
contextUrl: null,
|
|
1420
1420
|
documentUrl: "https://example.com/object",
|
|
@@ -1438,7 +1438,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1438
1438
|
</html>`,
|
|
1439
1439
|
headers: { "Content-Type": "application/xhtml+xml; charset=utf-8" }
|
|
1440
1440
|
});
|
|
1441
|
-
await t.
|
|
1441
|
+
await t.step("XHTML <link>", async () => {
|
|
1442
1442
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/xhtml-link"), {
|
|
1443
1443
|
contextUrl: null,
|
|
1444
1444
|
documentUrl: "https://example.com/object",
|
|
@@ -1464,7 +1464,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1464
1464
|
</html>`,
|
|
1465
1465
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1466
1466
|
});
|
|
1467
|
-
await t.
|
|
1467
|
+
await t.step("HTML <a>", async () => {
|
|
1468
1468
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/html-a"), {
|
|
1469
1469
|
contextUrl: null,
|
|
1470
1470
|
documentUrl: "https://example.com/object",
|
|
@@ -1486,7 +1486,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1486
1486
|
</html>`,
|
|
1487
1487
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1488
1488
|
});
|
|
1489
|
-
await t.
|
|
1489
|
+
await t.step("HTML without ActivityPub alternate link", async () => {
|
|
1490
1490
|
await rejects(() => fetchDocumentLoader("https://example.com/html-no-alternate"), (error) => {
|
|
1491
1491
|
ok(error instanceof FetchError);
|
|
1492
1492
|
ok(error.message.includes("HTML document has no ActivityPub alternate link"));
|
|
@@ -1506,7 +1506,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1506
1506
|
},
|
|
1507
1507
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1508
1508
|
});
|
|
1509
|
-
await t.
|
|
1509
|
+
await t.step("wrong Content-Type with JSON body", async () => {
|
|
1510
1510
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/wrong-content-type"), {
|
|
1511
1511
|
contextUrl: null,
|
|
1512
1512
|
documentUrl: "https://example.com/wrong-content-type",
|
|
@@ -1525,7 +1525,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1525
1525
|
"Content-Type": "text/html; charset=utf-8"
|
|
1526
1526
|
}
|
|
1527
1527
|
});
|
|
1528
|
-
await t.
|
|
1528
|
+
await t.step("HTML Content-Length over limit", async () => {
|
|
1529
1529
|
await rejects(() => fetchDocumentLoader("https://example.com/large-html"), (error) => {
|
|
1530
1530
|
ok(error instanceof FetchError);
|
|
1531
1531
|
ok(error.message.includes("HTML document is too large to scan for an ActivityPub alternate link"));
|
|
@@ -1535,7 +1535,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1535
1535
|
return true;
|
|
1536
1536
|
});
|
|
1537
1537
|
});
|
|
1538
|
-
await t.
|
|
1538
|
+
await t.step("HTML Content-Length over limit cancels body", async () => {
|
|
1539
1539
|
let canceled = false;
|
|
1540
1540
|
const response = new Response("<!DOCTYPE html>", { headers: {
|
|
1541
1541
|
"Content-Length": String(1048577),
|
|
@@ -1550,10 +1550,10 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1550
1550
|
deepStrictEqual(canceled, true);
|
|
1551
1551
|
});
|
|
1552
1552
|
esm_default.get("https://example.com/404", { status: 404 });
|
|
1553
|
-
await t.
|
|
1553
|
+
await t.step("not ok", async () => {
|
|
1554
1554
|
await rejects(() => fetchDocumentLoader("https://example.com/404"), FetchError, "HTTP 404: https://example.com/404");
|
|
1555
1555
|
});
|
|
1556
|
-
await t.
|
|
1556
|
+
await t.step("preloaded contexts", async () => {
|
|
1557
1557
|
for (const [url, document] of Object.entries(preloadedContexts)) deepStrictEqual(await fetchDocumentLoader(url), {
|
|
1558
1558
|
contextUrl: null,
|
|
1559
1559
|
documentUrl: url,
|
|
@@ -1561,7 +1561,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1561
1561
|
});
|
|
1562
1562
|
});
|
|
1563
1563
|
esm_default.get("https://www.w3.org/ns/cid/v1", { status: 503 });
|
|
1564
|
-
await t.
|
|
1564
|
+
await t.step("preloaded CID v1 context", async () => {
|
|
1565
1565
|
const url = "https://www.w3.org/ns/cid/v1";
|
|
1566
1566
|
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
1567
1567
|
contextUrl: null,
|
|
@@ -1569,7 +1569,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1569
1569
|
document: cid_v1_default
|
|
1570
1570
|
});
|
|
1571
1571
|
});
|
|
1572
|
-
await t.
|
|
1572
|
+
await t.step("preloaded FEP-ef61 context", async () => {
|
|
1573
1573
|
const url = "https://w3id.org/fep/ef61";
|
|
1574
1574
|
ok(url in preloadedContexts);
|
|
1575
1575
|
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
@@ -1586,7 +1586,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1586
1586
|
});
|
|
1587
1587
|
});
|
|
1588
1588
|
esm_default.get("https://w3id.org/fep/7aa9", { status: 502 });
|
|
1589
|
-
await t.
|
|
1589
|
+
await t.step("preloaded FEP-7aa9 context", async () => {
|
|
1590
1590
|
const url = "https://w3id.org/fep/7aa9";
|
|
1591
1591
|
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
1592
1592
|
contextUrl: null,
|
|
@@ -1616,7 +1616,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1616
1616
|
});
|
|
1617
1617
|
deepStrictEqual(esm_default.callHistory.calls(url).length, 0);
|
|
1618
1618
|
});
|
|
1619
|
-
await t.
|
|
1619
|
+
await t.step("deny non-HTTP/HTTPS", async () => {
|
|
1620
1620
|
await rejects(() => fetchDocumentLoader("ftp://localhost"), UrlError);
|
|
1621
1621
|
});
|
|
1622
1622
|
esm_default.get("https://example.com/localhost-redirect", {
|
|
@@ -1641,13 +1641,13 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1641
1641
|
name: "Fetched object",
|
|
1642
1642
|
type: "Object"
|
|
1643
1643
|
} });
|
|
1644
|
-
await t.
|
|
1644
|
+
await t.step("allowPrivateAddress: false", async () => {
|
|
1645
1645
|
await rejects(() => fetchDocumentLoader("https://localhost/object"), UrlError);
|
|
1646
1646
|
await rejects(() => fetchDocumentLoader("https://example.com/localhost-redirect"), UrlError);
|
|
1647
1647
|
await rejects(() => fetchDocumentLoader("https://example.com/localhost-link"), UrlError);
|
|
1648
1648
|
});
|
|
1649
1649
|
const fetchDocumentLoader2 = getDocumentLoader({ allowPrivateAddress: true });
|
|
1650
|
-
await t.
|
|
1650
|
+
await t.step("allowPrivateAddress: true", async () => {
|
|
1651
1651
|
const expected = {
|
|
1652
1652
|
contextUrl: null,
|
|
1653
1653
|
documentUrl: "https://localhost/object",
|
|
@@ -1670,12 +1670,12 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1670
1670
|
headers: { Location: `https://example.com/too-many-redirects/${Number(cl.url.split("/").at(-1)) + 1}` }
|
|
1671
1671
|
};
|
|
1672
1672
|
});
|
|
1673
|
-
await t.
|
|
1673
|
+
await t.step("too many redirects", async () => {
|
|
1674
1674
|
redirectAttempts = 0;
|
|
1675
1675
|
await rejects(() => fetchDocumentLoader("https://example.com/too-many-redirects/0"), FetchError, "Too many redirections");
|
|
1676
1676
|
deepStrictEqual(redirectAttempts, 21);
|
|
1677
1677
|
});
|
|
1678
|
-
await t.
|
|
1678
|
+
await t.step("custom max redirection", async () => {
|
|
1679
1679
|
redirectAttempts = 0;
|
|
1680
1680
|
const loader = getDocumentLoader({ maxRedirection: 1 });
|
|
1681
1681
|
await rejects(() => loader("https://example.com/too-many-redirects/0"), FetchError, "Too many redirections");
|
|
@@ -1696,7 +1696,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1696
1696
|
headers: { Location: "https://example.com/redirect-loop-a" }
|
|
1697
1697
|
};
|
|
1698
1698
|
});
|
|
1699
|
-
await t.
|
|
1699
|
+
await t.step("redirect loop", async () => {
|
|
1700
1700
|
loopAttempts = 0;
|
|
1701
1701
|
await rejects(() => fetchDocumentLoader("https://example.com/redirect-loop-a"), FetchError, "Redirect loop detected");
|
|
1702
1702
|
deepStrictEqual(loopAttempts, 2);
|
|
@@ -1709,7 +1709,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1709
1709
|
headers: { Location: "/redirect-loop-relative" }
|
|
1710
1710
|
};
|
|
1711
1711
|
});
|
|
1712
|
-
await t.
|
|
1712
|
+
await t.step("redirect loop with relative location", async () => {
|
|
1713
1713
|
relativeLoopAttempts = 0;
|
|
1714
1714
|
await rejects(() => fetchDocumentLoader("https://example.com/redirect-loop-relative"), FetchError, "Redirect loop detected");
|
|
1715
1715
|
deepStrictEqual(relativeLoopAttempts, 1);
|
|
@@ -1719,7 +1719,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
1719
1719
|
body: maliciousPayload,
|
|
1720
1720
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1721
1721
|
});
|
|
1722
|
-
await t.
|
|
1722
|
+
await t.step("ReDoS resistance (CVE-2025-68475)", async () => {
|
|
1723
1723
|
const start = performance.now();
|
|
1724
1724
|
await rejects(() => fetchDocumentLoader("https://example.com/redos"), FetchError);
|
|
1725
1725
|
const elapsed = performance.now() - start;
|
|
@@ -1805,16 +1805,13 @@ test("getRemoteDocument() bounds JSON by default", async () => {
|
|
|
1805
1805
|
deepStrictEqual(pulls, 257);
|
|
1806
1806
|
});
|
|
1807
1807
|
test("getDocumentLoader() logs DNS failures as such", async (t) => {
|
|
1808
|
-
if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted")
|
|
1809
|
-
t.skip("requires the net permission");
|
|
1810
|
-
return;
|
|
1811
|
-
}
|
|
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).");
|
|
1812
1809
|
const loader = getDocumentLoader();
|
|
1813
1810
|
for (const result of [
|
|
1814
1811
|
"throws",
|
|
1815
1812
|
"empty",
|
|
1816
1813
|
"private"
|
|
1817
|
-
]) await t.
|
|
1814
|
+
]) await t.step(result, async () => {
|
|
1818
1815
|
const originalLookup = dns.lookup;
|
|
1819
1816
|
dns.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
1820
1817
|
address: "127.0.0.1",
|
|
@@ -1869,7 +1866,7 @@ test("getDocumentLoader() bounds alternate document chains", async (t) => {
|
|
|
1869
1866
|
"header",
|
|
1870
1867
|
"html",
|
|
1871
1868
|
"mixed"
|
|
1872
|
-
]) for (const hops of [20, 21]) await t.
|
|
1869
|
+
]) for (const hops of [20, 21]) await t.step(`${mode}: ${hops} hops`, async () => {
|
|
1873
1870
|
esm_default.mockGlobal();
|
|
1874
1871
|
let requests = 0;
|
|
1875
1872
|
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
@@ -1898,7 +1895,7 @@ test("getDocumentLoader() bounds alternate document chains", async (t) => {
|
|
|
1898
1895
|
"alternate-redirect",
|
|
1899
1896
|
"redirect-alternate",
|
|
1900
1897
|
"redirect-intermediate"
|
|
1901
|
-
]) await t.
|
|
1898
|
+
]) await t.step(`${mode}: cycle`, async () => {
|
|
1902
1899
|
esm_default.mockGlobal();
|
|
1903
1900
|
let requests = 0;
|
|
1904
1901
|
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
@@ -1920,7 +1917,7 @@ test("getDocumentLoader() bounds alternate document chains", async (t) => {
|
|
|
1920
1917
|
esm_default.hardReset();
|
|
1921
1918
|
}
|
|
1922
1919
|
});
|
|
1923
|
-
await t.
|
|
1920
|
+
await t.step("alternate followed by 20 redirects shares the limit", async () => {
|
|
1924
1921
|
esm_default.mockGlobal();
|
|
1925
1922
|
let requests = 0;
|
|
1926
1923
|
esm_default.get(`begin:${base}`, ({ url }) => {
|
|
@@ -1940,7 +1937,7 @@ test("getDocumentLoader() bounds alternate document chains", async (t) => {
|
|
|
1940
1937
|
esm_default.hardReset();
|
|
1941
1938
|
}
|
|
1942
1939
|
});
|
|
1943
|
-
await t.
|
|
1940
|
+
await t.step("relative alternate after redirect and isolated calls", async () => {
|
|
1944
1941
|
esm_default.mockGlobal();
|
|
1945
1942
|
esm_default.get(`${base}start`, Response.redirect(`${base}html`, 302));
|
|
1946
1943
|
esm_default.get(`${base}html`, alternate("./document", true));
|
|
@@ -1967,7 +1964,7 @@ test("getDocumentLoader() bounds alternate document chains", async (t) => {
|
|
|
1967
1964
|
test("getDocumentLoader() preserves cancellation across alternates", async (t) => {
|
|
1968
1965
|
const base = "https://example.com/alternate-abort/";
|
|
1969
1966
|
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
1970
|
-
for (const html of [false, true]) await t.
|
|
1967
|
+
for (const html of [false, true]) await t.step(html ? "HTML" : "Link header", async () => {
|
|
1971
1968
|
esm_default.mockGlobal();
|
|
1972
1969
|
const controller = new AbortController();
|
|
1973
1970
|
let requests = 0;
|
|
@@ -2097,7 +2094,7 @@ test("getDocumentLoader() shares the timeout across redirects", async () => {
|
|
|
2097
2094
|
});
|
|
2098
2095
|
});
|
|
2099
2096
|
test("getDocumentLoader() shares the timeout across alternates", async (t) => {
|
|
2100
|
-
for (const html of [false, true]) await t.
|
|
2097
|
+
for (const html of [false, true]) await t.step(html ? "HTML" : "Link header", async () => {
|
|
2101
2098
|
await withServer((request, response) => {
|
|
2102
2099
|
const index = Number(request.url?.split("/").at(-1));
|
|
2103
2100
|
setTimeout(() => {
|
|
@@ -2155,11 +2152,8 @@ test("getDocumentLoader() lets the caller's signal abort a request", async () =>
|
|
|
2155
2152
|
});
|
|
2156
2153
|
});
|
|
2157
2154
|
});
|
|
2158
|
-
test("getDocumentLoader() bounds DNS lookups that ignore the timeout", async (
|
|
2159
|
-
if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted")
|
|
2160
|
-
t.skip("requires the net permission");
|
|
2161
|
-
return;
|
|
2162
|
-
}
|
|
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).");
|
|
2163
2157
|
esm_default.mockGlobal();
|
|
2164
2158
|
let requests = 0;
|
|
2165
2159
|
const url = "https://slow-dns.example/object";
|
|
@@ -1,148 +1,20 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
1
|
+
const require_url = require("../url-DsMBGqCc.cjs");
|
|
2
|
+
const require_jsonld_cache = require("../jsonld-cache-DzxT9IXH.cjs");
|
|
3
|
+
const require_portable_dereference = require("../portable-dereference-CyFYTBnr.cjs");
|
|
4
4
|
let node_assert_strict = require("node:assert/strict");
|
|
5
5
|
let node_test = require("node:test");
|
|
6
|
-
let _logtape_logtape = require("@logtape/logtape");
|
|
7
|
-
require("@opentelemetry/api");
|
|
8
|
-
//#region src/internal/portable-dereference.ts
|
|
9
|
-
const logger = (0, _logtape_logtape.getLogger)([
|
|
10
|
-
"fedify",
|
|
11
|
-
"vocab",
|
|
12
|
-
"gateway"
|
|
13
|
-
]);
|
|
14
|
-
/**
|
|
15
|
-
* The maximum number of `@gateway` location hints to try for one reference.
|
|
16
|
-
* Hints come from possibly untrusted documents, so they are bounded to keep
|
|
17
|
-
* a single accessor call from fanning out to many servers.
|
|
18
|
-
*/
|
|
19
|
-
const MAX_GATEWAY_HINTS = 5;
|
|
20
|
-
const BASELINE_CONTEXT_URLS = /* @__PURE__ */ new Set([
|
|
21
|
-
"https://w3id.org/identity/v1",
|
|
22
|
-
"https://www.w3.org/ns/activitystreams",
|
|
23
|
-
"https://w3id.org/security/v1",
|
|
24
|
-
"https://w3id.org/security/data-integrity/v1"
|
|
25
|
-
]);
|
|
26
|
-
/**
|
|
27
|
-
* Checks whether a URL is an FEP-ef61 portable ActivityPub IRI.
|
|
28
|
-
*
|
|
29
|
-
* @internal Technically exported for generated vocabulary classes, but not
|
|
30
|
-
* part of the public API contract. This is not considered public API for
|
|
31
|
-
* Semantic Versioning decisions.
|
|
32
|
-
*/
|
|
33
|
-
function isPortableIri(url) {
|
|
34
|
-
return url.protocol === "ap+ef61:" || url.protocol === "ap:";
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Picks the ordered list of FEP-ef61 gateways to fetch a portable IRI from.
|
|
38
|
-
*
|
|
39
|
-
* If `gateways` is given, it is used as is (even when empty), and `@gateway`
|
|
40
|
-
* location hints in the IRI are ignored. Otherwise, up to
|
|
41
|
-
* {@link MAX_GATEWAY_HINTS} valid `@gateway` hints are used. Duplicate
|
|
42
|
-
* gateways are dropped in both cases.
|
|
43
|
-
*
|
|
44
|
-
* @throws {TypeError} If an explicit gateway is not an HTTP(S) origin.
|
|
45
|
-
* @internal
|
|
46
|
-
*/
|
|
47
|
-
function getPortableGatewayCandidates(url, gateways) {
|
|
48
|
-
const candidates = [];
|
|
49
|
-
const seen = /* @__PURE__ */ new Set();
|
|
50
|
-
const add = (gateway) => {
|
|
51
|
-
if (seen.has(gateway.href)) return;
|
|
52
|
-
seen.add(gateway.href);
|
|
53
|
-
candidates.push(gateway);
|
|
54
|
-
};
|
|
55
|
-
if (gateways != null) {
|
|
56
|
-
for (const gateway of gateways) {
|
|
57
|
-
const parsed = require_url.parseGatewayOrigin(gateway);
|
|
58
|
-
if (parsed == null) throw new TypeError("FEP-ef61 gateways must be HTTP(S) origins with no credentials, path, query, or fragment: " + String(gateway));
|
|
59
|
-
add(parsed);
|
|
60
|
-
}
|
|
61
|
-
return candidates;
|
|
62
|
-
}
|
|
63
|
-
for (const hint of new URLSearchParams(url.search).getAll(require_url.GATEWAY_HINT_PARAMETER)) {
|
|
64
|
-
if (candidates.length >= MAX_GATEWAY_HINTS) break;
|
|
65
|
-
const parsed = require_url.parseGatewayOrigin(hint);
|
|
66
|
-
if (parsed == null) {
|
|
67
|
-
logger.debug("Ignoring an invalid FEP-ef61 gateway hint {hint} in {url}.", {
|
|
68
|
-
hint,
|
|
69
|
-
url: require_url.formatIri(url)
|
|
70
|
-
});
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
add(parsed);
|
|
74
|
-
}
|
|
75
|
-
return candidates;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Creates a context loader that returns the same context documents for the
|
|
79
|
-
* whole dereference operation, so that the identity check, the proof
|
|
80
|
-
* verifier, and the parser interpret the fetched document identically even
|
|
81
|
-
* if the underlying loader is nondeterministic. Failed loads are not
|
|
82
|
-
* remembered, so a transient failure does not affect the next gateway.
|
|
83
|
-
*
|
|
84
|
-
* The parsed object keeps the loader for its own later dereferences, so
|
|
85
|
-
* `release()` turns it into a plain pass-through to the underlying loader
|
|
86
|
-
* once the operation is over.
|
|
87
|
-
*
|
|
88
|
-
* @internal Technically exported for generated vocabulary classes, but not
|
|
89
|
-
* part of the public API contract. This is not considered public API for
|
|
90
|
-
* Semantic Versioning decisions.
|
|
91
|
-
*/
|
|
92
|
-
function createSnapshotContextLoader(contextLoader, suppressError) {
|
|
93
|
-
contextLoader = require_jsonld_cache.unwrapReleasedDocumentLoader(contextLoader);
|
|
94
|
-
const cache = /* @__PURE__ */ new Map();
|
|
95
|
-
let released = false;
|
|
96
|
-
const release = () => {
|
|
97
|
-
released = true;
|
|
98
|
-
state.released = true;
|
|
99
|
-
cache.clear();
|
|
100
|
-
};
|
|
101
|
-
const loader = async (url, options) => {
|
|
102
|
-
if (released) return await contextLoader(url, options);
|
|
103
|
-
const key = URL.canParse(url) ? new URL(url).href : url;
|
|
104
|
-
if (BASELINE_CONTEXT_URLS.has(key)) return {
|
|
105
|
-
contextUrl: null,
|
|
106
|
-
document: structuredClone(require_contexts.preloadedContexts[key]),
|
|
107
|
-
documentUrl: key
|
|
108
|
-
};
|
|
109
|
-
let promise = cache.get(key);
|
|
110
|
-
if (promise == null) {
|
|
111
|
-
const loading = contextLoader(url, suppressError ? {
|
|
112
|
-
...options,
|
|
113
|
-
suppressError: true
|
|
114
|
-
} : options).then((document) => structuredClone(document));
|
|
115
|
-
promise = loading;
|
|
116
|
-
cache.set(key, loading);
|
|
117
|
-
loading.catch(() => {
|
|
118
|
-
if (cache.get(key) === loading) cache.delete(key);
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
return structuredClone(await promise);
|
|
122
|
-
};
|
|
123
|
-
const state = {
|
|
124
|
-
base: contextLoader,
|
|
125
|
-
released: false
|
|
126
|
-
};
|
|
127
|
-
require_jsonld_cache.registerDocumentLoaderWrapper(loader, state);
|
|
128
|
-
return {
|
|
129
|
-
loader,
|
|
130
|
-
release
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
//#endregion
|
|
134
6
|
//#region src/internal/portable-dereference.test.ts
|
|
135
7
|
const hrefs = (urls) => urls.map((url) => url.href);
|
|
136
8
|
(0, node_test.test)("isPortableIri()", () => {
|
|
137
|
-
(0, node_assert_strict.ok)(isPortableIri(require_url.parseIri("ap://did:key:z6Mkabc/actor")));
|
|
138
|
-
(0, node_assert_strict.ok)(isPortableIri(require_url.parseIri("ap+ef61://did:key:z6Mkabc/actor")));
|
|
139
|
-
(0, node_assert_strict.ok)(isPortableIri(new URL("ap://did%3Akey%3Az6Mkabc/actor")));
|
|
140
|
-
(0, node_assert_strict.ok)(!isPortableIri(new URL("https://example.com/actor")));
|
|
141
|
-
(0, node_assert_strict.ok)(!isPortableIri(new URL("did:key:z6Mkabc")));
|
|
9
|
+
(0, node_assert_strict.ok)(require_portable_dereference.isPortableIri(require_url.parseIri("ap://did:key:z6Mkabc/actor")));
|
|
10
|
+
(0, node_assert_strict.ok)(require_portable_dereference.isPortableIri(require_url.parseIri("ap+ef61://did:key:z6Mkabc/actor")));
|
|
11
|
+
(0, node_assert_strict.ok)(require_portable_dereference.isPortableIri(new URL("ap://did%3Akey%3Az6Mkabc/actor")));
|
|
12
|
+
(0, node_assert_strict.ok)(!require_portable_dereference.isPortableIri(new URL("https://example.com/actor")));
|
|
13
|
+
(0, node_assert_strict.ok)(!require_portable_dereference.isPortableIri(new URL("did:key:z6Mkabc")));
|
|
142
14
|
});
|
|
143
15
|
(0, node_test.test)("getPortableGatewayCandidates() uses explicit gateways", () => {
|
|
144
16
|
const url = require_url.parseIri("ap://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fhint.example");
|
|
145
|
-
(0, node_assert_strict.deepStrictEqual)(hrefs(getPortableGatewayCandidates(url, [
|
|
17
|
+
(0, node_assert_strict.deepStrictEqual)(hrefs(require_portable_dereference.getPortableGatewayCandidates(url, [
|
|
146
18
|
"https://b.example",
|
|
147
19
|
new URL("https://a.example/"),
|
|
148
20
|
"https://b.example/",
|
|
@@ -152,7 +24,7 @@ const hrefs = (urls) => urls.map((url) => url.href);
|
|
|
152
24
|
"https://a.example/",
|
|
153
25
|
"http://c.example:8080/"
|
|
154
26
|
]);
|
|
155
|
-
(0, node_assert_strict.deepStrictEqual)(getPortableGatewayCandidates(url, []), []);
|
|
27
|
+
(0, node_assert_strict.deepStrictEqual)(require_portable_dereference.getPortableGatewayCandidates(url, []), []);
|
|
156
28
|
for (const gateway of [
|
|
157
29
|
"https://a.example/path",
|
|
158
30
|
"https://a.example/?",
|
|
@@ -160,13 +32,13 @@ const hrefs = (urls) => urls.map((url) => url.href);
|
|
|
160
32
|
"https://user:pass@a.example",
|
|
161
33
|
"ftp://a.example",
|
|
162
34
|
"a.example"
|
|
163
|
-
]) (0, node_assert_strict.throws)(() => getPortableGatewayCandidates(url, [gateway]), TypeError);
|
|
35
|
+
]) (0, node_assert_strict.throws)(() => require_portable_dereference.getPortableGatewayCandidates(url, [gateway]), TypeError);
|
|
164
36
|
});
|
|
165
37
|
(0, node_test.test)("getPortableGatewayCandidates() reads @gateway hints", () => {
|
|
166
|
-
(0, node_assert_strict.deepStrictEqual)(hrefs(getPortableGatewayCandidates(require_url.parseIri("ap://did:key:z6Mkabc/actor?page=1&@gateway=https%3A%2F%2Fa.example&gateways=https%3A%2F%2Flegacy.example&@gateway=not%20a%20URL&@gateway=https%3A%2F%2Fb.example%2Fpath&@gateway=https%3A%2F%2Fquery.example%2F%3F&@gateway=https%3A%2F%2Ffragment.example%2F%23&%40gateway=https%3A%2F%2Fb.example&@gateway=https%3A%2F%2Fa.example%2F"))), ["https://a.example/", "https://b.example/"]);
|
|
167
|
-
(0, node_assert_strict.deepStrictEqual)(getPortableGatewayCandidates(require_url.parseIri("ap://did:key:z6Mkabc/actor")), []);
|
|
38
|
+
(0, node_assert_strict.deepStrictEqual)(hrefs(require_portable_dereference.getPortableGatewayCandidates(require_url.parseIri("ap://did:key:z6Mkabc/actor?page=1&@gateway=https%3A%2F%2Fa.example&gateways=https%3A%2F%2Flegacy.example&@gateway=not%20a%20URL&@gateway=https%3A%2F%2Fb.example%2Fpath&@gateway=https%3A%2F%2Fquery.example%2F%3F&@gateway=https%3A%2F%2Ffragment.example%2F%23&%40gateway=https%3A%2F%2Fb.example&@gateway=https%3A%2F%2Fa.example%2F"))), ["https://a.example/", "https://b.example/"]);
|
|
39
|
+
(0, node_assert_strict.deepStrictEqual)(require_portable_dereference.getPortableGatewayCandidates(require_url.parseIri("ap://did:key:z6Mkabc/actor")), []);
|
|
168
40
|
const hints = Array.from({ length: 7 }, (_, i) => `@gateway=https%3A%2F%2Fg${i}.example`);
|
|
169
|
-
(0, node_assert_strict.deepStrictEqual)(hrefs(getPortableGatewayCandidates(require_url.parseIri(`ap://did:key:z6Mkabc/actor?${hints.join("&")}`))), [
|
|
41
|
+
(0, node_assert_strict.deepStrictEqual)(hrefs(require_portable_dereference.getPortableGatewayCandidates(require_url.parseIri(`ap://did:key:z6Mkabc/actor?${hints.join("&")}`))), [
|
|
170
42
|
0,
|
|
171
43
|
1,
|
|
172
44
|
2,
|
|
@@ -176,7 +48,7 @@ const hrefs = (urls) => urls.map((url) => url.href);
|
|
|
176
48
|
});
|
|
177
49
|
(0, node_test.test)("getPortableGatewayCandidates() reads hints from withGatewayHints()", () => {
|
|
178
50
|
const gateways = Array.from({ length: 7 }, (_, i) => `https://g${i}.example`);
|
|
179
|
-
(0, node_assert_strict.deepStrictEqual)(hrefs(getPortableGatewayCandidates(require_url.withGatewayHints("ap://did:key:z6Mkabc/actor?page=1", gateways))), [
|
|
51
|
+
(0, node_assert_strict.deepStrictEqual)(hrefs(require_portable_dereference.getPortableGatewayCandidates(require_url.withGatewayHints("ap://did:key:z6Mkabc/actor?page=1", gateways))), [
|
|
180
52
|
0,
|
|
181
53
|
1,
|
|
182
54
|
2,
|
|
@@ -196,15 +68,15 @@ const hrefs = (urls) => urls.map((url) => url.href);
|
|
|
196
68
|
};
|
|
197
69
|
let loader = base;
|
|
198
70
|
for (let i = 0; i < 1e4; i++) {
|
|
199
|
-
const snapshot = createSnapshotContextLoader(loader);
|
|
71
|
+
const snapshot = require_portable_dereference.createSnapshotContextLoader(loader);
|
|
200
72
|
snapshot.release();
|
|
201
73
|
loader = snapshot.loader;
|
|
202
74
|
}
|
|
203
75
|
await loader("https://example.com/context");
|
|
204
76
|
(0, node_assert_strict.deepStrictEqual)(calls, ["https://example.com/context"]);
|
|
205
|
-
const outer = createSnapshotContextLoader(base);
|
|
77
|
+
const outer = require_portable_dereference.createSnapshotContextLoader(base);
|
|
206
78
|
await outer.loader("https://example.com/context");
|
|
207
|
-
const inner = createSnapshotContextLoader(outer.loader);
|
|
79
|
+
const inner = require_portable_dereference.createSnapshotContextLoader(outer.loader);
|
|
208
80
|
await inner.loader("https://example.com/context");
|
|
209
81
|
(0, node_assert_strict.deepStrictEqual)(calls.length, 2);
|
|
210
82
|
inner.release();
|
|
@@ -223,7 +95,7 @@ const hrefs = (urls) => urls.map((url) => url.href);
|
|
|
223
95
|
for (const snapshotFirst of [false, true]) {
|
|
224
96
|
let loader = base;
|
|
225
97
|
for (let i = 0; i < 1e4; i++) {
|
|
226
|
-
const wrapper = i % 2 === 0 === snapshotFirst ? createSnapshotContextLoader(loader, true) : require_jsonld_cache.createScopedContextLoader(loader, true);
|
|
98
|
+
const wrapper = i % 2 === 0 === snapshotFirst ? require_portable_dereference.createSnapshotContextLoader(loader, true) : require_jsonld_cache.createScopedContextLoader(loader, true);
|
|
227
99
|
wrapper.release();
|
|
228
100
|
loader = wrapper.loader;
|
|
229
101
|
}
|
|
@@ -244,10 +116,10 @@ const hrefs = (urls) => urls.map((url) => url.href);
|
|
|
244
116
|
document: { "@context": {} }
|
|
245
117
|
});
|
|
246
118
|
};
|
|
247
|
-
const active = activeSnapshot ? createSnapshotContextLoader(base, true) : require_jsonld_cache.createScopedContextLoader(base, true);
|
|
119
|
+
const active = activeSnapshot ? require_portable_dereference.createSnapshotContextLoader(base, true) : require_jsonld_cache.createScopedContextLoader(base, true);
|
|
248
120
|
let loader = active.loader;
|
|
249
121
|
for (let i = 0; i < 4; i++) {
|
|
250
|
-
const wrapper = i % 2 === 0 ? require_jsonld_cache.createScopedContextLoader(loader, true) : createSnapshotContextLoader(loader, true);
|
|
122
|
+
const wrapper = i % 2 === 0 ? require_jsonld_cache.createScopedContextLoader(loader, true) : require_portable_dereference.createSnapshotContextLoader(loader, true);
|
|
251
123
|
wrapper.release();
|
|
252
124
|
loader = wrapper.loader;
|
|
253
125
|
}
|