@fedify/vocab-runtime 2.4.0-dev.2228 → 2.4.0-dev.2240
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 +6 -18
- package/dist/internal/portable-dereference.js +4 -16
- package/dist/mod.cjs +5 -2
- package/dist/mod.d.cts +107 -4
- package/dist/mod.d.ts +107 -4
- package/dist/mod.js +3 -3
- package/dist/tests/{body-BmNV5g5S.mjs → body-C00dWiYW.mjs} +1 -1
- package/dist/tests/{body-BQZL1aJ1.cjs → body-DHk-vT0I.cjs} +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-ButlYQxr.mjs → docloader-Bi982uOa.mjs} +3 -3
- package/dist/tests/{docloader-BnuGCXLg.cjs → docloader-gX-8knd3.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 +30 -157
- package/dist/tests/internal/portable-dereference.test.mjs +13 -140
- package/dist/tests/{jsonld-cache-C7mkqjiE.mjs → jsonld-cache-CKfgtxPB.mjs} +1 -1
- package/dist/tests/{jsonld-cache-CXA76Xi6.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-CFrGpz-3.cjs → portable-media-B39cvJGK.cjs} +3 -3
- package/dist/tests/{portable-media-xFDDMte7.mjs → portable-media-l0ET9Pq8.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-2GDEEJng.mjs → request-BRPiDcmG.mjs} +1 -1
- package/dist/tests/{request-Cme6ShRu.cjs → request-fo2l2yJS.cjs} +1 -1
- package/dist/tests/request.test.cjs +1 -1
- package/dist/tests/request.test.mjs +1 -1
- package/dist/tests/{url-R9TTZ67B.mjs → url-CWC-NI3a.mjs} +224 -8
- package/dist/tests/{url-B6WlNhra.cjs → url-DsMBGqCc.cjs} +253 -7
- package/dist/tests/url.test.cjs +192 -1
- package/dist/tests/url.test.mjs +192 -1
- package/dist/{url-CHAbe3hE.cjs → url-BJMXv2kE.cjs} +253 -7
- package/dist/{url-BfguNa6K.js → url-CZm2-7ZO.js} +224 -8
- package/package.json +6 -3
- package/scripts/test-bun.mjs +17 -0
- package/src/docloader.test.ts +82 -74
- package/src/internal/portable-dereference.test.ts +14 -1
- package/src/internal/portable-dereference.ts +5 -19
- package/src/mod.ts +3 -0
- package/src/multibase/multibase.test.ts +5 -5
- package/src/portable-workers.test.ts +81 -0
- package/src/url.test.ts +385 -0
- package/src/url.ts +347 -13
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { t as FetchError } from "./request-
|
|
1
|
+
import { t as FetchError } from "./request-BRPiDcmG.mjs";
|
|
2
2
|
import { n as cid_v1_default, t as preloadedContexts } from "./contexts-CIKsin4e.mjs";
|
|
3
|
-
import {
|
|
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-Bi982uOa.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,157 +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 LOCATION_HINT_PARAMETER = "@gateway";
|
|
21
|
-
const BASELINE_CONTEXT_URLS = /* @__PURE__ */ new Set([
|
|
22
|
-
"https://w3id.org/identity/v1",
|
|
23
|
-
"https://www.w3.org/ns/activitystreams",
|
|
24
|
-
"https://w3id.org/security/v1",
|
|
25
|
-
"https://w3id.org/security/data-integrity/v1"
|
|
26
|
-
]);
|
|
27
|
-
/**
|
|
28
|
-
* Checks whether a URL is an FEP-ef61 portable ActivityPub IRI.
|
|
29
|
-
*
|
|
30
|
-
* @internal Technically exported for generated vocabulary classes, but not
|
|
31
|
-
* part of the public API contract. This is not considered public API for
|
|
32
|
-
* Semantic Versioning decisions.
|
|
33
|
-
*/
|
|
34
|
-
function isPortableIri(url) {
|
|
35
|
-
return url.protocol === "ap+ef61:" || url.protocol === "ap:";
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Picks the ordered list of FEP-ef61 gateways to fetch a portable IRI from.
|
|
39
|
-
*
|
|
40
|
-
* If `gateways` is given, it is used as is (even when empty), and `@gateway`
|
|
41
|
-
* location hints in the IRI are ignored. Otherwise, up to
|
|
42
|
-
* {@link MAX_GATEWAY_HINTS} valid `@gateway` hints are used. Duplicate
|
|
43
|
-
* gateways are dropped in both cases.
|
|
44
|
-
*
|
|
45
|
-
* @throws {TypeError} If an explicit gateway is not an HTTP(S) origin.
|
|
46
|
-
* @internal
|
|
47
|
-
*/
|
|
48
|
-
function getPortableGatewayCandidates(url, gateways) {
|
|
49
|
-
const candidates = [];
|
|
50
|
-
const seen = /* @__PURE__ */ new Set();
|
|
51
|
-
const add = (gateway) => {
|
|
52
|
-
if (seen.has(gateway.href)) return;
|
|
53
|
-
seen.add(gateway.href);
|
|
54
|
-
candidates.push(gateway);
|
|
55
|
-
};
|
|
56
|
-
if (gateways != null) {
|
|
57
|
-
for (const gateway of gateways) {
|
|
58
|
-
const parsed = parseGatewayOrigin(gateway);
|
|
59
|
-
if (parsed == null) throw new TypeError("FEP-ef61 gateways must be HTTP(S) origins with no credentials, path, query, or fragment: " + String(gateway));
|
|
60
|
-
add(parsed);
|
|
61
|
-
}
|
|
62
|
-
return candidates;
|
|
63
|
-
}
|
|
64
|
-
for (const hint of new URLSearchParams(url.search).getAll(LOCATION_HINT_PARAMETER)) {
|
|
65
|
-
if (candidates.length >= MAX_GATEWAY_HINTS) break;
|
|
66
|
-
const parsed = parseGatewayOrigin(hint);
|
|
67
|
-
if (parsed == null) {
|
|
68
|
-
logger.debug("Ignoring an invalid FEP-ef61 gateway hint {hint} in {url}.", {
|
|
69
|
-
hint,
|
|
70
|
-
url: require_url.formatIri(url)
|
|
71
|
-
});
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
add(parsed);
|
|
75
|
-
}
|
|
76
|
-
return candidates;
|
|
77
|
-
}
|
|
78
|
-
function parseGatewayOrigin(gateway) {
|
|
79
|
-
let url;
|
|
80
|
-
if (gateway instanceof URL) url = new URL(gateway.href);
|
|
81
|
-
else if (typeof gateway === "string" && URL.canParse(gateway)) url = new URL(gateway);
|
|
82
|
-
else return null;
|
|
83
|
-
if (!require_url.isGatewayUrl(url)) return null;
|
|
84
|
-
return url;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Creates a context loader that returns the same context documents for the
|
|
88
|
-
* whole dereference operation, so that the identity check, the proof
|
|
89
|
-
* verifier, and the parser interpret the fetched document identically even
|
|
90
|
-
* if the underlying loader is nondeterministic. Failed loads are not
|
|
91
|
-
* remembered, so a transient failure does not affect the next gateway.
|
|
92
|
-
*
|
|
93
|
-
* The parsed object keeps the loader for its own later dereferences, so
|
|
94
|
-
* `release()` turns it into a plain pass-through to the underlying loader
|
|
95
|
-
* once the operation is over.
|
|
96
|
-
*
|
|
97
|
-
* @internal Technically exported for generated vocabulary classes, but not
|
|
98
|
-
* part of the public API contract. This is not considered public API for
|
|
99
|
-
* Semantic Versioning decisions.
|
|
100
|
-
*/
|
|
101
|
-
function createSnapshotContextLoader(contextLoader, suppressError) {
|
|
102
|
-
contextLoader = require_jsonld_cache.unwrapReleasedDocumentLoader(contextLoader);
|
|
103
|
-
const cache = /* @__PURE__ */ new Map();
|
|
104
|
-
let released = false;
|
|
105
|
-
const release = () => {
|
|
106
|
-
released = true;
|
|
107
|
-
state.released = true;
|
|
108
|
-
cache.clear();
|
|
109
|
-
};
|
|
110
|
-
const loader = async (url, options) => {
|
|
111
|
-
if (released) return await contextLoader(url, options);
|
|
112
|
-
const key = URL.canParse(url) ? new URL(url).href : url;
|
|
113
|
-
if (BASELINE_CONTEXT_URLS.has(key)) return {
|
|
114
|
-
contextUrl: null,
|
|
115
|
-
document: structuredClone(require_contexts.preloadedContexts[key]),
|
|
116
|
-
documentUrl: key
|
|
117
|
-
};
|
|
118
|
-
let promise = cache.get(key);
|
|
119
|
-
if (promise == null) {
|
|
120
|
-
const loading = contextLoader(url, suppressError ? {
|
|
121
|
-
...options,
|
|
122
|
-
suppressError: true
|
|
123
|
-
} : options).then((document) => structuredClone(document));
|
|
124
|
-
promise = loading;
|
|
125
|
-
cache.set(key, loading);
|
|
126
|
-
loading.catch(() => {
|
|
127
|
-
if (cache.get(key) === loading) cache.delete(key);
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
return structuredClone(await promise);
|
|
131
|
-
};
|
|
132
|
-
const state = {
|
|
133
|
-
base: contextLoader,
|
|
134
|
-
released: false
|
|
135
|
-
};
|
|
136
|
-
require_jsonld_cache.registerDocumentLoaderWrapper(loader, state);
|
|
137
|
-
return {
|
|
138
|
-
loader,
|
|
139
|
-
release
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
//#endregion
|
|
143
6
|
//#region src/internal/portable-dereference.test.ts
|
|
144
7
|
const hrefs = (urls) => urls.map((url) => url.href);
|
|
145
8
|
(0, node_test.test)("isPortableIri()", () => {
|
|
146
|
-
(0, node_assert_strict.ok)(isPortableIri(require_url.parseIri("ap://did:key:z6Mkabc/actor")));
|
|
147
|
-
(0, node_assert_strict.ok)(isPortableIri(require_url.parseIri("ap+ef61://did:key:z6Mkabc/actor")));
|
|
148
|
-
(0, node_assert_strict.ok)(isPortableIri(new URL("ap://did%3Akey%3Az6Mkabc/actor")));
|
|
149
|
-
(0, node_assert_strict.ok)(!isPortableIri(new URL("https://example.com/actor")));
|
|
150
|
-
(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")));
|
|
151
14
|
});
|
|
152
15
|
(0, node_test.test)("getPortableGatewayCandidates() uses explicit gateways", () => {
|
|
153
16
|
const url = require_url.parseIri("ap://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fhint.example");
|
|
154
|
-
(0, node_assert_strict.deepStrictEqual)(hrefs(getPortableGatewayCandidates(url, [
|
|
17
|
+
(0, node_assert_strict.deepStrictEqual)(hrefs(require_portable_dereference.getPortableGatewayCandidates(url, [
|
|
155
18
|
"https://b.example",
|
|
156
19
|
new URL("https://a.example/"),
|
|
157
20
|
"https://b.example/",
|
|
@@ -161,7 +24,7 @@ const hrefs = (urls) => urls.map((url) => url.href);
|
|
|
161
24
|
"https://a.example/",
|
|
162
25
|
"http://c.example:8080/"
|
|
163
26
|
]);
|
|
164
|
-
(0, node_assert_strict.deepStrictEqual)(getPortableGatewayCandidates(url, []), []);
|
|
27
|
+
(0, node_assert_strict.deepStrictEqual)(require_portable_dereference.getPortableGatewayCandidates(url, []), []);
|
|
165
28
|
for (const gateway of [
|
|
166
29
|
"https://a.example/path",
|
|
167
30
|
"https://a.example/?",
|
|
@@ -169,13 +32,23 @@ const hrefs = (urls) => urls.map((url) => url.href);
|
|
|
169
32
|
"https://user:pass@a.example",
|
|
170
33
|
"ftp://a.example",
|
|
171
34
|
"a.example"
|
|
172
|
-
]) (0, node_assert_strict.throws)(() => getPortableGatewayCandidates(url, [gateway]), TypeError);
|
|
35
|
+
]) (0, node_assert_strict.throws)(() => require_portable_dereference.getPortableGatewayCandidates(url, [gateway]), TypeError);
|
|
173
36
|
});
|
|
174
37
|
(0, node_test.test)("getPortableGatewayCandidates() reads @gateway hints", () => {
|
|
175
|
-
(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/"]);
|
|
176
|
-
(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")), []);
|
|
177
40
|
const hints = Array.from({ length: 7 }, (_, i) => `@gateway=https%3A%2F%2Fg${i}.example`);
|
|
178
|
-
(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("&")}`))), [
|
|
42
|
+
0,
|
|
43
|
+
1,
|
|
44
|
+
2,
|
|
45
|
+
3,
|
|
46
|
+
4
|
|
47
|
+
].map((i) => `https://g${i}.example/`));
|
|
48
|
+
});
|
|
49
|
+
(0, node_test.test)("getPortableGatewayCandidates() reads hints from withGatewayHints()", () => {
|
|
50
|
+
const gateways = Array.from({ length: 7 }, (_, i) => `https://g${i}.example`);
|
|
51
|
+
(0, node_assert_strict.deepStrictEqual)(hrefs(require_portable_dereference.getPortableGatewayCandidates(require_url.withGatewayHints("ap://did:key:z6Mkabc/actor?page=1", gateways))), [
|
|
179
52
|
0,
|
|
180
53
|
1,
|
|
181
54
|
2,
|
|
@@ -195,15 +68,15 @@ const hrefs = (urls) => urls.map((url) => url.href);
|
|
|
195
68
|
};
|
|
196
69
|
let loader = base;
|
|
197
70
|
for (let i = 0; i < 1e4; i++) {
|
|
198
|
-
const snapshot = createSnapshotContextLoader(loader);
|
|
71
|
+
const snapshot = require_portable_dereference.createSnapshotContextLoader(loader);
|
|
199
72
|
snapshot.release();
|
|
200
73
|
loader = snapshot.loader;
|
|
201
74
|
}
|
|
202
75
|
await loader("https://example.com/context");
|
|
203
76
|
(0, node_assert_strict.deepStrictEqual)(calls, ["https://example.com/context"]);
|
|
204
|
-
const outer = createSnapshotContextLoader(base);
|
|
77
|
+
const outer = require_portable_dereference.createSnapshotContextLoader(base);
|
|
205
78
|
await outer.loader("https://example.com/context");
|
|
206
|
-
const inner = createSnapshotContextLoader(outer.loader);
|
|
79
|
+
const inner = require_portable_dereference.createSnapshotContextLoader(outer.loader);
|
|
207
80
|
await inner.loader("https://example.com/context");
|
|
208
81
|
(0, node_assert_strict.deepStrictEqual)(calls.length, 2);
|
|
209
82
|
inner.release();
|
|
@@ -222,7 +95,7 @@ const hrefs = (urls) => urls.map((url) => url.href);
|
|
|
222
95
|
for (const snapshotFirst of [false, true]) {
|
|
223
96
|
let loader = base;
|
|
224
97
|
for (let i = 0; i < 1e4; i++) {
|
|
225
|
-
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);
|
|
226
99
|
wrapper.release();
|
|
227
100
|
loader = wrapper.loader;
|
|
228
101
|
}
|
|
@@ -243,10 +116,10 @@ const hrefs = (urls) => urls.map((url) => url.href);
|
|
|
243
116
|
document: { "@context": {} }
|
|
244
117
|
});
|
|
245
118
|
};
|
|
246
|
-
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);
|
|
247
120
|
let loader = active.loader;
|
|
248
121
|
for (let i = 0; i < 4; i++) {
|
|
249
|
-
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);
|
|
250
123
|
wrapper.release();
|
|
251
124
|
loader = wrapper.loader;
|
|
252
125
|
}
|