@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.
Files changed (55) hide show
  1. package/deno.json +2 -2
  2. package/dist/internal/jsonld-cache.cjs +1 -1
  3. package/dist/internal/jsonld-cache.js +1 -1
  4. package/dist/internal/portable-dereference.cjs +3 -6
  5. package/dist/internal/portable-dereference.js +3 -6
  6. package/dist/mod.cjs +2 -2
  7. package/dist/mod.d.cts +14 -3
  8. package/dist/mod.d.ts +14 -3
  9. package/dist/mod.js +2 -2
  10. package/dist/tests/{body-DQKTFMsH.cjs → body-C7LfsQJ3.cjs} +1 -1
  11. package/dist/tests/{body-BEYUpKvX.mjs → body-D50XSh7s.mjs} +1 -1
  12. package/dist/tests/body.test.cjs +1 -1
  13. package/dist/tests/body.test.mjs +1 -1
  14. package/dist/tests/decimal.test.cjs +3 -3
  15. package/dist/tests/decimal.test.mjs +3 -3
  16. package/dist/tests/{docloader-ra1lbBfP.mjs → docloader-BdcRw8S9.mjs} +3 -3
  17. package/dist/tests/{docloader-DHCYdBXf.cjs → docloader-oUrWeeUz.cjs} +3 -3
  18. package/dist/tests/docloader.test.cjs +59 -65
  19. package/dist/tests/docloader.test.mjs +38 -44
  20. package/dist/tests/internal/portable-dereference.test.cjs +21 -149
  21. package/dist/tests/internal/portable-dereference.test.mjs +3 -131
  22. package/dist/tests/{jsonld-cache-BKrsWPCQ.mjs → jsonld-cache-CKfgtxPB.mjs} +1 -1
  23. package/dist/tests/{jsonld-cache-BTgizq9A.cjs → jsonld-cache-DzxT9IXH.cjs} +1 -1
  24. package/dist/tests/jsonld-cache.test.cjs +2 -2
  25. package/dist/tests/jsonld-cache.test.mjs +2 -2
  26. package/dist/tests/multibase/multibase.test.cjs +7 -7
  27. package/dist/tests/multibase/multibase.test.mjs +5 -5
  28. package/dist/tests/portable-dereference-C_cwGjz6.mjs +132 -0
  29. package/dist/tests/portable-dereference-CyFYTBnr.cjs +149 -0
  30. package/dist/tests/{portable-media-DHaAEpaQ.cjs → portable-media-Bj5FXpUr.cjs} +3 -3
  31. package/dist/tests/{portable-media-1NjUQari.mjs → portable-media-D4NJcqkI.mjs} +3 -3
  32. package/dist/tests/portable-media.test.cjs +1 -1
  33. package/dist/tests/portable-media.test.mjs +1 -1
  34. package/dist/tests/portable-workers.test.cjs +36 -0
  35. package/dist/tests/portable-workers.test.d.cts +2 -0
  36. package/dist/tests/portable-workers.test.d.mts +2 -0
  37. package/dist/tests/portable-workers.test.mjs +35 -0
  38. package/dist/tests/{request-BQlkB8du.cjs → request-CBCk-e0R.cjs} +1 -1
  39. package/dist/tests/{request-D3elVRyB.mjs → request-wKyc91L5.mjs} +1 -1
  40. package/dist/tests/request.test.cjs +1 -1
  41. package/dist/tests/request.test.mjs +1 -1
  42. package/dist/tests/{url-Cdu4ASgJ.mjs → url-CWC-NI3a.mjs} +63 -7
  43. package/dist/tests/{url-CtHyzMR4.cjs → url-DsMBGqCc.cjs} +63 -7
  44. package/dist/tests/url.test.cjs +74 -1
  45. package/dist/tests/url.test.mjs +74 -1
  46. package/dist/{url-BHfQuRdy.cjs → url-BJMXv2kE.cjs} +63 -7
  47. package/dist/{url-DqqDZY9i.js → url-CZm2-7ZO.js} +63 -7
  48. package/package.json +6 -3
  49. package/scripts/test-bun.mjs +17 -0
  50. package/src/docloader.test.ts +82 -74
  51. package/src/internal/portable-dereference.ts +2 -5
  52. package/src/multibase/multibase.test.ts +5 -5
  53. package/src/portable-workers.test.ts +81 -0
  54. package/src/url.test.ts +90 -0
  55. package/src/url.ts +115 -13
@@ -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 "node:test";
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.test("ok", async () => {
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.test("Link header", async () => {
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.test("Link header relative url", async () => {
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.test("wrong Link header syntax", async () => {
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.test("HTML <link>", async () => {
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.test("XHTML <link>", async () => {
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.test("HTML <a>", async () => {
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.test("HTML without ActivityPub alternate link", async () => {
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.test("wrong Content-Type with JSON body", async () => {
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.test("HTML Content-Length over limit", async () => {
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.test("HTML Content-Length over limit cancels body", async () => {
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.test("not ok", async () => {
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.test("preloaded contexts", async () => {
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.test("preloaded CID v1 context", async () => {
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.test("preloaded FEP-ef61 context", async () => {
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.test("preloaded FEP-7aa9 context", async () => {
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.test("deny non-HTTP/HTTPS", async () => {
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.test("allowPrivateAddress: false", async () => {
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.test("allowPrivateAddress: true", async () => {
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.test("too many redirects", async () => {
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.test("custom max redirection", async () => {
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.test("redirect loop", async () => {
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.test("redirect loop with relative location", async () => {
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.test("ReDoS resistance (CVE-2025-68475)", async () => {
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
- t.skip("requires the net permission");
871
- return;
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.test(result, async () => {
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.test(`${mode}: ${hops} hops`, async () => {
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.test(`${mode}: cycle`, async () => {
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.test("alternate followed by 20 redirects shares the limit", async () => {
1011
- fetchMock.mockGlobal();
1012
- let requests = 0;
1013
- fetchMock.get(`begin:${base}`, ({ url }) => {
1014
- requests++;
1015
- const index = Number(new URL(url).pathname.split("/").at(-1));
1016
- if (index === 21) return Response.json({ done: true });
1017
- return index === 0
1018
- ? alternate(`${base}1`, false)
1019
- : Response.redirect(`${base}${index + 1}`, 302);
1020
- });
1021
- try {
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
- deepStrictEqual(requests, 21);
1028
- } finally {
1029
- fetchMock.hardReset();
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.test("relative alternate after redirect and isolated calls", async () => {
1034
- fetchMock.mockGlobal();
1035
- fetchMock.get(`${base}start`, Response.redirect(`${base}html`, 302));
1036
- fetchMock.get(`${base}html`, alternate("./document", true));
1037
- fetchMock.get(`${base}document`, Response.json({ done: true }));
1038
- try {
1039
- for (let i = 0; i < 2; i++) {
1040
- const results = await Promise.all([
1041
- loader(`${base}start`),
1042
- loader(`${base}start`),
1043
- ]);
1044
- for (const result of results) {
1045
- deepStrictEqual(result.document, { done: true });
1046
- deepStrictEqual(result.documentUrl, `${base}document`);
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
- } finally {
1050
- fetchMock.hardReset();
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.test(html ? "HTML" : "Link header", async () => {
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.test(html ? "HTML" : "Link header", async () => {
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 (t) => {
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
- t.skip("requires the net permission");
1316
- return;
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 "node:test";
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.test(`Encoding/Decoding ${name} with ${input}`, () => {
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.test("should allow base32pad full alphabet", () => {
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.test("constants indexed by name", () => {
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.test("constants indexed by code", () => {
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
  [