@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
package/src/docloader.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { configure, type LogRecord, reset } from "@logtape/logtape";
|
|
|
2
2
|
import fetchMock from "fetch-mock";
|
|
3
3
|
import { deepStrictEqual, ok, rejects, throws } from "node:assert";
|
|
4
4
|
import dns from "node:dns/promises";
|
|
5
|
-
import { test } from "
|
|
5
|
+
import { test } from "@fedify/fixture";
|
|
6
6
|
import {
|
|
7
7
|
createServer,
|
|
8
8
|
type IncomingMessage,
|
|
@@ -125,7 +125,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
125
125
|
},
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
-
await t.
|
|
128
|
+
await t.step("ok", async () => {
|
|
129
129
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/object"), {
|
|
130
130
|
contextUrl: null,
|
|
131
131
|
documentUrl: "https://example.com/object",
|
|
@@ -182,7 +182,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
182
182
|
},
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
-
await t.
|
|
185
|
+
await t.step("Link header", async () => {
|
|
186
186
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/link-ctx"), {
|
|
187
187
|
contextUrl: "https://www.w3.org/ns/activitystreams",
|
|
188
188
|
documentUrl: "https://example.com/link-ctx",
|
|
@@ -205,7 +205,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
205
205
|
});
|
|
206
206
|
});
|
|
207
207
|
|
|
208
|
-
await t.
|
|
208
|
+
await t.step("Link header relative url", async () => {
|
|
209
209
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/link-ctx"), {
|
|
210
210
|
contextUrl: "https://www.w3.org/ns/activitystreams",
|
|
211
211
|
documentUrl: "https://example.com/link-ctx",
|
|
@@ -231,7 +231,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
231
231
|
);
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
-
await t.
|
|
234
|
+
await t.step("wrong Link header syntax", async () => {
|
|
235
235
|
deepStrictEqual(
|
|
236
236
|
await fetchDocumentLoader("https://example.com/obj-w-wrong-link"),
|
|
237
237
|
{
|
|
@@ -260,7 +260,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
260
260
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
261
261
|
});
|
|
262
262
|
|
|
263
|
-
await t.
|
|
263
|
+
await t.step("HTML <link>", async () => {
|
|
264
264
|
deepStrictEqual(
|
|
265
265
|
await fetchDocumentLoader("https://example.com/html-link"),
|
|
266
266
|
{
|
|
@@ -289,7 +289,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
289
289
|
headers: { "Content-Type": "application/xhtml+xml; charset=utf-8" },
|
|
290
290
|
});
|
|
291
291
|
|
|
292
|
-
await t.
|
|
292
|
+
await t.step("XHTML <link>", async () => {
|
|
293
293
|
deepStrictEqual(
|
|
294
294
|
await fetchDocumentLoader("https://example.com/xhtml-link"),
|
|
295
295
|
{
|
|
@@ -320,7 +320,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
320
320
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
321
321
|
});
|
|
322
322
|
|
|
323
|
-
await t.
|
|
323
|
+
await t.step("HTML <a>", async () => {
|
|
324
324
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/html-a"), {
|
|
325
325
|
contextUrl: null,
|
|
326
326
|
documentUrl: "https://example.com/object",
|
|
@@ -344,7 +344,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
344
344
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
345
345
|
});
|
|
346
346
|
|
|
347
|
-
await t.
|
|
347
|
+
await t.step("HTML without ActivityPub alternate link", async () => {
|
|
348
348
|
await rejects(
|
|
349
349
|
() => fetchDocumentLoader("https://example.com/html-no-alternate"),
|
|
350
350
|
(error) => {
|
|
@@ -381,7 +381,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
381
381
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
382
382
|
});
|
|
383
383
|
|
|
384
|
-
await t.
|
|
384
|
+
await t.step("wrong Content-Type with JSON body", async () => {
|
|
385
385
|
deepStrictEqual(
|
|
386
386
|
await fetchDocumentLoader("https://example.com/wrong-content-type"),
|
|
387
387
|
{
|
|
@@ -405,7 +405,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
405
405
|
},
|
|
406
406
|
});
|
|
407
407
|
|
|
408
|
-
await t.
|
|
408
|
+
await t.step("HTML Content-Length over limit", async () => {
|
|
409
409
|
await rejects(
|
|
410
410
|
() => fetchDocumentLoader("https://example.com/large-html"),
|
|
411
411
|
(error) => {
|
|
@@ -426,7 +426,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
426
426
|
);
|
|
427
427
|
});
|
|
428
428
|
|
|
429
|
-
await t.
|
|
429
|
+
await t.step("HTML Content-Length over limit cancels body", async () => {
|
|
430
430
|
let canceled = false;
|
|
431
431
|
const response = new Response("<!DOCTYPE html>", {
|
|
432
432
|
headers: {
|
|
@@ -459,7 +459,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
459
459
|
|
|
460
460
|
fetchMock.get("https://example.com/404", { status: 404 });
|
|
461
461
|
|
|
462
|
-
await t.
|
|
462
|
+
await t.step("not ok", async () => {
|
|
463
463
|
await rejects(
|
|
464
464
|
() => fetchDocumentLoader("https://example.com/404"),
|
|
465
465
|
FetchError,
|
|
@@ -467,7 +467,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
467
467
|
);
|
|
468
468
|
});
|
|
469
469
|
|
|
470
|
-
await t.
|
|
470
|
+
await t.step("preloaded contexts", async () => {
|
|
471
471
|
for (const [url, document] of Object.entries(preloadedContexts)) {
|
|
472
472
|
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
473
473
|
contextUrl: null,
|
|
@@ -483,7 +483,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
483
483
|
// See: https://www.w3.org/TR/cid-1.0/#json-ld-context
|
|
484
484
|
// https://github.com/fedify-dev/fedify/issues/932
|
|
485
485
|
fetchMock.get("https://www.w3.org/ns/cid/v1", { status: 503 });
|
|
486
|
-
await t.
|
|
486
|
+
await t.step("preloaded CID v1 context", async () => {
|
|
487
487
|
const url = "https://www.w3.org/ns/cid/v1";
|
|
488
488
|
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
489
489
|
contextUrl: null,
|
|
@@ -497,7 +497,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
497
497
|
// document referencing it fails before application handlers run. It has to
|
|
498
498
|
// be resolved from the built-in copy rather than over the network.
|
|
499
499
|
// See: https://github.com/fedify-dev/fedify/issues/982
|
|
500
|
-
await t.
|
|
500
|
+
await t.step("preloaded FEP-ef61 context", async () => {
|
|
501
501
|
const url = "https://w3id.org/fep/ef61";
|
|
502
502
|
ok(url in preloadedContexts);
|
|
503
503
|
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
@@ -520,7 +520,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
520
520
|
// A Codeberg Pages outage must not prevent loading the FEP-7aa9 context.
|
|
521
521
|
// See: https://github.com/fedify-dev/fedify/issues/1078
|
|
522
522
|
fetchMock.get("https://w3id.org/fep/7aa9", { status: 502 });
|
|
523
|
-
await t.
|
|
523
|
+
await t.step("preloaded FEP-7aa9 context", async () => {
|
|
524
524
|
const url = "https://w3id.org/fep/7aa9";
|
|
525
525
|
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
526
526
|
contextUrl: null,
|
|
@@ -554,7 +554,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
554
554
|
deepStrictEqual(fetchMock.callHistory.calls(url).length, 0);
|
|
555
555
|
});
|
|
556
556
|
|
|
557
|
-
await t.
|
|
557
|
+
await t.step("deny non-HTTP/HTTPS", async () => {
|
|
558
558
|
await rejects(
|
|
559
559
|
() => fetchDocumentLoader("ftp://localhost"),
|
|
560
560
|
UrlError,
|
|
@@ -588,7 +588,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
588
588
|
},
|
|
589
589
|
});
|
|
590
590
|
|
|
591
|
-
await t.
|
|
591
|
+
await t.step("allowPrivateAddress: false", async () => {
|
|
592
592
|
await rejects(
|
|
593
593
|
() => fetchDocumentLoader("https://localhost/object"),
|
|
594
594
|
UrlError,
|
|
@@ -605,7 +605,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
605
605
|
|
|
606
606
|
const fetchDocumentLoader2 = getDocumentLoader({ allowPrivateAddress: true });
|
|
607
607
|
|
|
608
|
-
await t.
|
|
608
|
+
await t.step("allowPrivateAddress: true", async () => {
|
|
609
609
|
const expected = {
|
|
610
610
|
contextUrl: null,
|
|
611
611
|
documentUrl: "https://localhost/object",
|
|
@@ -642,7 +642,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
642
642
|
};
|
|
643
643
|
});
|
|
644
644
|
|
|
645
|
-
await t.
|
|
645
|
+
await t.step("too many redirects", async () => {
|
|
646
646
|
redirectAttempts = 0;
|
|
647
647
|
await rejects(
|
|
648
648
|
() => fetchDocumentLoader("https://example.com/too-many-redirects/0"),
|
|
@@ -652,7 +652,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
652
652
|
deepStrictEqual(redirectAttempts, 21);
|
|
653
653
|
});
|
|
654
654
|
|
|
655
|
-
await t.
|
|
655
|
+
await t.step("custom max redirection", async () => {
|
|
656
656
|
redirectAttempts = 0;
|
|
657
657
|
const loader = getDocumentLoader({ maxRedirection: 1 });
|
|
658
658
|
await rejects(
|
|
@@ -679,7 +679,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
679
679
|
};
|
|
680
680
|
});
|
|
681
681
|
|
|
682
|
-
await t.
|
|
682
|
+
await t.step("redirect loop", async () => {
|
|
683
683
|
loopAttempts = 0;
|
|
684
684
|
await rejects(
|
|
685
685
|
() => fetchDocumentLoader("https://example.com/redirect-loop-a"),
|
|
@@ -698,7 +698,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
698
698
|
};
|
|
699
699
|
});
|
|
700
700
|
|
|
701
|
-
await t.
|
|
701
|
+
await t.step("redirect loop with relative location", async () => {
|
|
702
702
|
relativeLoopAttempts = 0;
|
|
703
703
|
await rejects(
|
|
704
704
|
() => fetchDocumentLoader("https://example.com/redirect-loop-relative"),
|
|
@@ -718,7 +718,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
718
718
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
719
719
|
});
|
|
720
720
|
|
|
721
|
-
await t.
|
|
721
|
+
await t.step("ReDoS resistance (CVE-2025-68475)", async () => {
|
|
722
722
|
const start = performance.now();
|
|
723
723
|
// The malicious HTML will fail alternate discovery, but the important
|
|
724
724
|
// thing is that it should complete quickly (not hang due to ReDoS).
|
|
@@ -867,12 +867,13 @@ test("getDocumentLoader() logs DNS failures as such", async (t) => {
|
|
|
867
867
|
"Deno" in globalThis &&
|
|
868
868
|
(await Deno.permissions.query({ name: "net" })).state !== "granted"
|
|
869
869
|
) {
|
|
870
|
-
|
|
871
|
-
|
|
870
|
+
throw new Error(
|
|
871
|
+
"This DNS test requires Deno network permission (--allow-net).",
|
|
872
|
+
);
|
|
872
873
|
}
|
|
873
874
|
const loader = getDocumentLoader();
|
|
874
875
|
for (const result of ["throws", "empty", "private"] as const) {
|
|
875
|
-
await t.
|
|
876
|
+
await t.step(result, async () => {
|
|
876
877
|
// Stubbing works only because url.ts uses the default node:dns/promises
|
|
877
878
|
// import; see the FIXME there.
|
|
878
879
|
const originalLookup = dns.lookup;
|
|
@@ -928,7 +929,7 @@ test("getDocumentLoader() bounds alternate document chains", async (t) => {
|
|
|
928
929
|
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
929
930
|
for (const mode of ["header", "html", "mixed"] as const) {
|
|
930
931
|
for (const hops of [20, 21]) {
|
|
931
|
-
await t.
|
|
932
|
+
await t.step(`${mode}: ${hops} hops`, async () => {
|
|
932
933
|
fetchMock.mockGlobal();
|
|
933
934
|
let requests = 0;
|
|
934
935
|
fetchMock.get(`begin:${base}`, ({ url }) => {
|
|
@@ -973,7 +974,7 @@ test("getDocumentLoader() bounds alternate document chains", async (t) => {
|
|
|
973
974
|
"redirect-intermediate",
|
|
974
975
|
]
|
|
975
976
|
) {
|
|
976
|
-
await t.
|
|
977
|
+
await t.step(`${mode}: cycle`, async () => {
|
|
977
978
|
fetchMock.mockGlobal();
|
|
978
979
|
let requests = 0;
|
|
979
980
|
fetchMock.get(`begin:${base}`, ({ url }) => {
|
|
@@ -1007,49 +1008,55 @@ test("getDocumentLoader() bounds alternate document chains", async (t) => {
|
|
|
1007
1008
|
});
|
|
1008
1009
|
}
|
|
1009
1010
|
|
|
1010
|
-
await t.
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
requests
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
await rejects(loader(`${base}0`), (error: unknown) => {
|
|
1023
|
-
ok(error instanceof FetchError);
|
|
1024
|
-
ok(error.message.includes("Too many redirections (21)"));
|
|
1025
|
-
return true;
|
|
1011
|
+
await t.step(
|
|
1012
|
+
"alternate followed by 20 redirects shares the limit",
|
|
1013
|
+
async () => {
|
|
1014
|
+
fetchMock.mockGlobal();
|
|
1015
|
+
let requests = 0;
|
|
1016
|
+
fetchMock.get(`begin:${base}`, ({ url }) => {
|
|
1017
|
+
requests++;
|
|
1018
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
1019
|
+
if (index === 21) return Response.json({ done: true });
|
|
1020
|
+
return index === 0
|
|
1021
|
+
? alternate(`${base}1`, false)
|
|
1022
|
+
: Response.redirect(`${base}${index + 1}`, 302);
|
|
1026
1023
|
});
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1024
|
+
try {
|
|
1025
|
+
await rejects(loader(`${base}0`), (error: unknown) => {
|
|
1026
|
+
ok(error instanceof FetchError);
|
|
1027
|
+
ok(error.message.includes("Too many redirections (21)"));
|
|
1028
|
+
return true;
|
|
1029
|
+
});
|
|
1030
|
+
deepStrictEqual(requests, 21);
|
|
1031
|
+
} finally {
|
|
1032
|
+
fetchMock.hardReset();
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
);
|
|
1032
1036
|
|
|
1033
|
-
await t.
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1037
|
+
await t.step(
|
|
1038
|
+
"relative alternate after redirect and isolated calls",
|
|
1039
|
+
async () => {
|
|
1040
|
+
fetchMock.mockGlobal();
|
|
1041
|
+
fetchMock.get(`${base}start`, Response.redirect(`${base}html`, 302));
|
|
1042
|
+
fetchMock.get(`${base}html`, alternate("./document", true));
|
|
1043
|
+
fetchMock.get(`${base}document`, Response.json({ done: true }));
|
|
1044
|
+
try {
|
|
1045
|
+
for (let i = 0; i < 2; i++) {
|
|
1046
|
+
const results = await Promise.all([
|
|
1047
|
+
loader(`${base}start`),
|
|
1048
|
+
loader(`${base}start`),
|
|
1049
|
+
]);
|
|
1050
|
+
for (const result of results) {
|
|
1051
|
+
deepStrictEqual(result.document, { done: true });
|
|
1052
|
+
deepStrictEqual(result.documentUrl, `${base}document`);
|
|
1053
|
+
}
|
|
1047
1054
|
}
|
|
1055
|
+
} finally {
|
|
1056
|
+
fetchMock.hardReset();
|
|
1048
1057
|
}
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
}
|
|
1052
|
-
});
|
|
1058
|
+
},
|
|
1059
|
+
);
|
|
1053
1060
|
|
|
1054
1061
|
function alternate(next: string, html: boolean): Response {
|
|
1055
1062
|
return html
|
|
@@ -1072,7 +1079,7 @@ test("getDocumentLoader() preserves cancellation across alternates", async (t) =
|
|
|
1072
1079
|
const base = "https://example.com/alternate-abort/";
|
|
1073
1080
|
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
1074
1081
|
for (const html of [false, true]) {
|
|
1075
|
-
await t.
|
|
1082
|
+
await t.step(html ? "HTML" : "Link header", async () => {
|
|
1076
1083
|
fetchMock.mockGlobal();
|
|
1077
1084
|
const controller = new AbortController();
|
|
1078
1085
|
let requests = 0;
|
|
@@ -1238,7 +1245,7 @@ test("getDocumentLoader() shares the timeout across redirects", async () => {
|
|
|
1238
1245
|
|
|
1239
1246
|
test("getDocumentLoader() shares the timeout across alternates", async (t) => {
|
|
1240
1247
|
for (const html of [false, true]) {
|
|
1241
|
-
await t.
|
|
1248
|
+
await t.step(html ? "HTML" : "Link header", async () => {
|
|
1242
1249
|
await withServer((request, response) => {
|
|
1243
1250
|
const index = Number(request.url?.split("/").at(-1));
|
|
1244
1251
|
setTimeout(() => {
|
|
@@ -1306,14 +1313,15 @@ test("getDocumentLoader() lets the caller's signal abort a request", async () =>
|
|
|
1306
1313
|
});
|
|
1307
1314
|
});
|
|
1308
1315
|
|
|
1309
|
-
test("getDocumentLoader() bounds DNS lookups that ignore the timeout", async (
|
|
1316
|
+
test("getDocumentLoader() bounds DNS lookups that ignore the timeout", async () => {
|
|
1310
1317
|
// The validator skips DNS when Deno has no network permission.
|
|
1311
1318
|
if (
|
|
1312
1319
|
"Deno" in globalThis &&
|
|
1313
1320
|
(await Deno.permissions.query({ name: "net" })).state !== "granted"
|
|
1314
1321
|
) {
|
|
1315
|
-
|
|
1316
|
-
|
|
1322
|
+
throw new Error(
|
|
1323
|
+
"This DNS test requires Deno network permission (--allow-net).",
|
|
1324
|
+
);
|
|
1317
1325
|
}
|
|
1318
1326
|
fetchMock.mockGlobal();
|
|
1319
1327
|
let requests = 0;
|
|
@@ -212,19 +212,16 @@ export function copyPortableProvenance(from: object, to: object): void {
|
|
|
212
212
|
export function parseCompatibleEf61Reference(
|
|
213
213
|
url: URL | string,
|
|
214
214
|
): { readonly id: URL; readonly gateway: URL } | null | undefined {
|
|
215
|
-
if (typeof url === "string") {
|
|
216
|
-
if (!URL.canParse(url)) return undefined;
|
|
217
|
-
url = new URL(url);
|
|
218
|
-
}
|
|
219
215
|
try {
|
|
220
216
|
const id = fromCompatibleEf61Id(url);
|
|
221
217
|
if (id == null) return undefined;
|
|
218
|
+
if (typeof url === "string") url = new URL(url);
|
|
222
219
|
return { id, gateway: new URL(url.origin) };
|
|
223
220
|
} catch (error) {
|
|
224
221
|
if (error instanceof TypeError) {
|
|
225
222
|
logger.debug(
|
|
226
223
|
"Invalid FEP-ef61 compatible identifier {url}: {error}",
|
|
227
|
-
{ url: url.href, error },
|
|
224
|
+
{ url: typeof url === "string" ? url : url.href, error },
|
|
228
225
|
);
|
|
229
226
|
return null;
|
|
230
227
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { deepStrictEqual } from "node:assert";
|
|
2
|
-
import { test } from "
|
|
2
|
+
import { test } from "@fedify/fixture";
|
|
3
3
|
import * as constants from "./constants.ts";
|
|
4
4
|
import { decodeMultibase, encodeMultibase } from "./mod.ts";
|
|
5
5
|
import type { BaseName } from "./types.d.ts";
|
|
@@ -76,7 +76,7 @@ test("multibase.encode and decode", async (t) => {
|
|
|
76
76
|
];
|
|
77
77
|
|
|
78
78
|
for (const [name, input, expectedOutput] of testCases) {
|
|
79
|
-
await t.
|
|
79
|
+
await t.step(`Encoding/Decoding ${name} with ${input}`, () => {
|
|
80
80
|
const encoded = encodeMultibase(name, encodeText(input));
|
|
81
81
|
deepStrictEqual(
|
|
82
82
|
decodeText(encoded),
|
|
@@ -96,7 +96,7 @@ test("multibase.encode and decode", async (t) => {
|
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
await t.
|
|
99
|
+
await t.step("should allow base32pad full alphabet", () => {
|
|
100
100
|
const encodedStr = "ctimaq4ygg2iegci7";
|
|
101
101
|
const decoded = decodeMultibase(encodedStr);
|
|
102
102
|
const encoded = encodeMultibase("c", decoded);
|
|
@@ -105,12 +105,12 @@ test("multibase.encode and decode", async (t) => {
|
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
test("constants", async (t) => {
|
|
108
|
-
await t.
|
|
108
|
+
await t.step("constants indexed by name", () => {
|
|
109
109
|
const names = constants.names;
|
|
110
110
|
deepStrictEqual(Object.keys(names).length, 23);
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
await t.
|
|
113
|
+
await t.step("constants indexed by code", () => {
|
|
114
114
|
const codes = constants.codes;
|
|
115
115
|
deepStrictEqual(Object.keys(codes).length, 23);
|
|
116
116
|
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { test, testDefinitions } from "@fedify/fixture";
|
|
2
|
+
import { deepStrictEqual, equal } from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
computeDigestMultibase,
|
|
5
|
+
createHashlink,
|
|
6
|
+
verifyHashlink,
|
|
7
|
+
} from "./digest.ts";
|
|
8
|
+
import { exportDidKey, importDidKey } from "./key.ts";
|
|
9
|
+
import { getPortableGatewayCandidates } from "./internal/portable-dereference.ts";
|
|
10
|
+
import {
|
|
11
|
+
canonicalizePortableUri,
|
|
12
|
+
formatIri,
|
|
13
|
+
fromCompatibleEf61Id,
|
|
14
|
+
parseIri,
|
|
15
|
+
toCompatibleEf61Id,
|
|
16
|
+
} from "./url.ts";
|
|
17
|
+
|
|
18
|
+
const portableId = "ap://did:key:z6Mkabc/actor";
|
|
19
|
+
|
|
20
|
+
export { testDefinitions };
|
|
21
|
+
|
|
22
|
+
test("vocab-runtime portable URI formats on Workers", () => {
|
|
23
|
+
equal(formatIri(parseIri(portableId)), "ap+ef61://did:key:z6Mkabc/actor");
|
|
24
|
+
equal(
|
|
25
|
+
canonicalizePortableUri(`${portableId}?@gateway=https%3A%2F%2Fa.example`),
|
|
26
|
+
"ap+ef61://did:key:z6Mkabc/actor",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("vocab-runtime compatible IDs on Workers", () => {
|
|
31
|
+
const compatible = toCompatibleEf61Id(portableId, "https://gw.example");
|
|
32
|
+
equal(
|
|
33
|
+
compatible.href,
|
|
34
|
+
"https://gw.example/.well-known/apgateway/did:key:z6Mkabc/actor",
|
|
35
|
+
);
|
|
36
|
+
equal(
|
|
37
|
+
formatIri(fromCompatibleEf61Id(compatible)!),
|
|
38
|
+
"ap+ef61://did:key:z6Mkabc/actor",
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("vocab-runtime did:key on Workers", async () => {
|
|
43
|
+
const keys = await crypto.subtle.generateKey("Ed25519", true, [
|
|
44
|
+
"sign",
|
|
45
|
+
"verify",
|
|
46
|
+
]) as CryptoKeyPair;
|
|
47
|
+
const did = await exportDidKey(keys.publicKey);
|
|
48
|
+
const imported = await importDidKey(did);
|
|
49
|
+
deepStrictEqual(
|
|
50
|
+
new Uint8Array(await crypto.subtle.exportKey("raw", imported)),
|
|
51
|
+
new Uint8Array(await crypto.subtle.exportKey("raw", keys.publicKey)),
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("vocab-runtime hashlink on Workers", async () => {
|
|
56
|
+
const bytes = new TextEncoder().encode("portable media");
|
|
57
|
+
const digest = await computeDigestMultibase(bytes);
|
|
58
|
+
equal(await verifyHashlink(bytes, createHashlink(digest)), true);
|
|
59
|
+
equal(
|
|
60
|
+
await verifyHashlink(
|
|
61
|
+
new TextEncoder().encode("tampered"),
|
|
62
|
+
createHashlink(digest),
|
|
63
|
+
),
|
|
64
|
+
false,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("vocab-runtime gateway candidates on Workers", () => {
|
|
69
|
+
const hinted = parseIri(
|
|
70
|
+
`${portableId}?@gateway=https%3A%2F%2Fhint.example`,
|
|
71
|
+
);
|
|
72
|
+
deepStrictEqual(
|
|
73
|
+
getPortableGatewayCandidates(hinted, ["https://first.example"])
|
|
74
|
+
.map((url) => url.href),
|
|
75
|
+
["https://first.example/"],
|
|
76
|
+
);
|
|
77
|
+
deepStrictEqual(
|
|
78
|
+
getPortableGatewayCandidates(hinted).map((url) => url.href),
|
|
79
|
+
["https://hint.example/"],
|
|
80
|
+
);
|
|
81
|
+
});
|
package/src/url.test.ts
CHANGED
|
@@ -40,6 +40,35 @@ test("parseIri() accepts portable ActivityPub URI schemes", () => {
|
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
test("portable URI parsing refuses dot segments before URL normalization", () => {
|
|
44
|
+
const base = "ap://did:key:z6Mkabc";
|
|
45
|
+
for (const segment of [".", "..", "%2e", ".%2E", "%2e.", "%2E%2e"]) {
|
|
46
|
+
for (const path of [`/${segment}`, `/a/${segment}/b`, `/a/${segment}`]) {
|
|
47
|
+
const iri = base + path;
|
|
48
|
+
throws(() => parseIri(iri), TypeError, iri);
|
|
49
|
+
throws(() => parseJsonLdId(iri), TypeError, iri);
|
|
50
|
+
throws(() => formatIri(iri), TypeError, iri);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (
|
|
54
|
+
const iri of [
|
|
55
|
+
`${base}/a/.\t./b`,
|
|
56
|
+
`${base}/a/..\n/b`,
|
|
57
|
+
` ${base}/a/../b`,
|
|
58
|
+
`${base}/a/.. `,
|
|
59
|
+
]
|
|
60
|
+
) {
|
|
61
|
+
throws(() => parseIri(iri), TypeError, iri);
|
|
62
|
+
}
|
|
63
|
+
for (const path of ["/a/.../b", "/a/.x/b", "/a/x.y/b", "/a/%252e/b"]) {
|
|
64
|
+
deepStrictEqual(
|
|
65
|
+
formatIri(parseIri(base + path)),
|
|
66
|
+
`ap+ef61://did:key:z6Mkabc${path}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
throws(() => parseIri("child", `${base}/a/../b`), TypeError);
|
|
70
|
+
});
|
|
71
|
+
|
|
43
72
|
test("parseIri() normalizes DID scheme and method casing", () => {
|
|
44
73
|
const cases = [
|
|
45
74
|
"ap://DID:key:z6Mkabc/actor",
|
|
@@ -112,6 +141,19 @@ test("parseIri() resolves relative IRIs against portable string bases", () => {
|
|
|
112
141
|
);
|
|
113
142
|
});
|
|
114
143
|
|
|
144
|
+
test("parseIri() rejects lossy compatible ID string bases", () => {
|
|
145
|
+
const base = "https://server.example/.well-known/apgateway/did:key:z6MkAlice";
|
|
146
|
+
for (const segment of [".", "..", "%2e", "%2e%2e"]) {
|
|
147
|
+
const rawBase = `${base}/a/${segment}/b`;
|
|
148
|
+
throws(() => parseIri("child", rawBase), TypeError, rawBase);
|
|
149
|
+
throws(() => parseJsonLdId("child", rawBase), TypeError, rawBase);
|
|
150
|
+
}
|
|
151
|
+
deepStrictEqual(
|
|
152
|
+
parseIri("child", "https://server.example/a/../b"),
|
|
153
|
+
new URL("https://server.example/child"),
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
115
157
|
test("parseIri() resolves relative IRIs against at:// string bases", () => {
|
|
116
158
|
deepStrictEqual(
|
|
117
159
|
parseIri("/record", "at://did:plc:example/collection/item"),
|
|
@@ -817,6 +859,54 @@ test("fromCompatibleEf61Id() rejects malformed compatible identifiers", () => {
|
|
|
817
859
|
}
|
|
818
860
|
});
|
|
819
861
|
|
|
862
|
+
test("fromCompatibleEf61Id() refuses raw paths changed by URL parsing", () => {
|
|
863
|
+
const base = "https://server.example/.well-known/apgateway/did:key:z6MkAlice";
|
|
864
|
+
for (
|
|
865
|
+
const path of [
|
|
866
|
+
"/a/../b",
|
|
867
|
+
"/a/%2e%2E/b",
|
|
868
|
+
"/a/.\t./b",
|
|
869
|
+
"/a/.. ",
|
|
870
|
+
]
|
|
871
|
+
) {
|
|
872
|
+
throws(() => fromCompatibleEf61Id(base + path), TypeError, path);
|
|
873
|
+
throws(() => parseIri(base + path), TypeError, path);
|
|
874
|
+
throws(() => formatIri(base + path), TypeError, path);
|
|
875
|
+
}
|
|
876
|
+
for (
|
|
877
|
+
const input of [
|
|
878
|
+
"https:/server.example/.well-known/apgateway/did:key:z6MkAlice/a/../b",
|
|
879
|
+
"https:server.example/.well-known/apgateway/did:key:z6MkAlice/a/../b",
|
|
880
|
+
"https:\\\\server.example\\.well-known\\apgateway\\did:key:z6MkAlice\\a\\..\\b",
|
|
881
|
+
` ${base}/../../other`,
|
|
882
|
+
]
|
|
883
|
+
) {
|
|
884
|
+
throws(() => parseIri(input), TypeError, input);
|
|
885
|
+
throws(() => formatIri(input), TypeError, input);
|
|
886
|
+
}
|
|
887
|
+
throws(
|
|
888
|
+
() => fromCompatibleEf61Id(` ${base}/actor`),
|
|
889
|
+
TypeError,
|
|
890
|
+
);
|
|
891
|
+
throws(
|
|
892
|
+
() =>
|
|
893
|
+
fromCompatibleEf61Id(
|
|
894
|
+
`https://server.example/foo/../.well-known/apgateway/did:key:z6MkAlice/actor`,
|
|
895
|
+
),
|
|
896
|
+
TypeError,
|
|
897
|
+
);
|
|
898
|
+
deepStrictEqual(
|
|
899
|
+
fromCompatibleEf61Id(`${base}/a/%252e/b`)?.pathname,
|
|
900
|
+
"/a/%252e/b",
|
|
901
|
+
);
|
|
902
|
+
deepStrictEqual(
|
|
903
|
+
fromCompatibleEf61Id(
|
|
904
|
+
"https://server.example/.WELL-KNOWN/apgateway/did:key:z6MkAlice/actor",
|
|
905
|
+
),
|
|
906
|
+
null,
|
|
907
|
+
);
|
|
908
|
+
});
|
|
909
|
+
|
|
820
910
|
test("toCompatibleEf61Id() converts portable URIs", () => {
|
|
821
911
|
const cases: [string | URL, string | URL, string][] = [
|
|
822
912
|
[
|