@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.
Files changed (57) 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 +6 -18
  5. package/dist/internal/portable-dereference.js +4 -16
  6. package/dist/mod.cjs +5 -2
  7. package/dist/mod.d.cts +107 -4
  8. package/dist/mod.d.ts +107 -4
  9. package/dist/mod.js +3 -3
  10. package/dist/tests/{body-BmNV5g5S.mjs → body-C00dWiYW.mjs} +1 -1
  11. package/dist/tests/{body-BQZL1aJ1.cjs → body-DHk-vT0I.cjs} +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-ButlYQxr.mjs → docloader-Bi982uOa.mjs} +3 -3
  17. package/dist/tests/{docloader-BnuGCXLg.cjs → docloader-gX-8knd3.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 +30 -157
  21. package/dist/tests/internal/portable-dereference.test.mjs +13 -140
  22. package/dist/tests/{jsonld-cache-C7mkqjiE.mjs → jsonld-cache-CKfgtxPB.mjs} +1 -1
  23. package/dist/tests/{jsonld-cache-CXA76Xi6.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-CFrGpz-3.cjs → portable-media-B39cvJGK.cjs} +3 -3
  31. package/dist/tests/{portable-media-xFDDMte7.mjs → portable-media-l0ET9Pq8.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-2GDEEJng.mjs → request-BRPiDcmG.mjs} +1 -1
  39. package/dist/tests/{request-Cme6ShRu.cjs → request-fo2l2yJS.cjs} +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-R9TTZ67B.mjs → url-CWC-NI3a.mjs} +224 -8
  43. package/dist/tests/{url-B6WlNhra.cjs → url-DsMBGqCc.cjs} +253 -7
  44. package/dist/tests/url.test.cjs +192 -1
  45. package/dist/tests/url.test.mjs +192 -1
  46. package/dist/{url-CHAbe3hE.cjs → url-BJMXv2kE.cjs} +253 -7
  47. package/dist/{url-BfguNa6K.js → url-CZm2-7ZO.js} +224 -8
  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.test.ts +14 -1
  52. package/src/internal/portable-dereference.ts +5 -19
  53. package/src/mod.ts +3 -0
  54. package/src/multibase/multibase.test.ts +5 -5
  55. package/src/portable-workers.test.ts +81 -0
  56. package/src/url.test.ts +385 -0
  57. package/src/url.ts +347 -13
package/src/url.test.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  formatIri,
8
8
  fromCompatibleEf61Id,
9
9
  getFe34Origin,
10
+ getGatewayHints,
10
11
  haveSameFe34Origin,
11
12
  haveSameIriOrigin,
12
13
  isGatewayUrl,
@@ -19,6 +20,8 @@ import {
19
20
  UrlError,
20
21
  validateLookupAddresses,
21
22
  validatePublicUrl,
23
+ withGatewayHints,
24
+ withoutGatewayHints,
22
25
  } from "./url.ts";
23
26
 
24
27
  test("parseIri() accepts portable ActivityPub URI schemes", () => {
@@ -37,6 +40,35 @@ test("parseIri() accepts portable ActivityPub URI schemes", () => {
37
40
  }
38
41
  });
39
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
+
40
72
  test("parseIri() normalizes DID scheme and method casing", () => {
41
73
  const cases = [
42
74
  "ap://DID:key:z6Mkabc/actor",
@@ -109,6 +141,19 @@ test("parseIri() resolves relative IRIs against portable string bases", () => {
109
141
  );
110
142
  });
111
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
+
112
157
  test("parseIri() resolves relative IRIs against at:// string bases", () => {
113
158
  deepStrictEqual(
114
159
  parseIri("/record", "at://did:plc:example/collection/item"),
@@ -814,6 +859,54 @@ test("fromCompatibleEf61Id() rejects malformed compatible identifiers", () => {
814
859
  }
815
860
  });
816
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
+
817
910
  test("toCompatibleEf61Id() converts portable URIs", () => {
818
911
  const cases: [string | URL, string | URL, string][] = [
819
912
  [
@@ -1227,3 +1320,295 @@ test("expandIPv6Address()", () => {
1227
1320
  "0064:ff9b:0000:0000:0000:0000:0808:0808",
1228
1321
  );
1229
1322
  });
1323
+
1324
+ test("withGatewayHints() adds @gateway location hints", () => {
1325
+ const expected = "ap+ef61://did:key:z6Mkabc/actor" +
1326
+ "?@gateway=https%3A%2F%2Fserver1.example" +
1327
+ "&@gateway=https%3A%2F%2Fserver2.example";
1328
+ for (
1329
+ const id of [
1330
+ "ap://did:key:z6Mkabc/actor",
1331
+ "ap+ef61://did:key:z6Mkabc/actor",
1332
+ "ap://did%3Akey%3Az6Mkabc/actor",
1333
+ parseIri("ap://did:key:z6Mkabc/actor"),
1334
+ new URL("ap://did%3Akey%3Az6Mkabc/actor"),
1335
+ ]
1336
+ ) {
1337
+ const hinted = withGatewayHints(id, [
1338
+ "https://server1.example",
1339
+ new URL("https://server2.example/"),
1340
+ ]);
1341
+ ok(hinted instanceof URL);
1342
+ strictEqual(
1343
+ hinted.href,
1344
+ "ap+ef61://did%3Akey%3Az6Mkabc/actor" +
1345
+ "?@gateway=https%3A%2F%2Fserver1.example" +
1346
+ "&@gateway=https%3A%2F%2Fserver2.example",
1347
+ );
1348
+ strictEqual(formatIri(hinted), expected);
1349
+ deepStrictEqual(parseIri(formatIri(hinted)), hinted);
1350
+ }
1351
+ });
1352
+
1353
+ test("withGatewayHints() encodes and deduplicates gateway origins", () => {
1354
+ function* gateways(): Generator<string | URL> {
1355
+ yield "https://A.example:443";
1356
+ yield "https://a.example/";
1357
+ yield new URL("https://a.example");
1358
+ yield "https://b.example:8443";
1359
+ yield "http://c.example:80/";
1360
+ yield "https://例え.jp";
1361
+ }
1362
+ strictEqual(
1363
+ formatIri(withGatewayHints("ap://did:key:z6Mkabc/actor", gateways())),
1364
+ "ap+ef61://did:key:z6Mkabc/actor" +
1365
+ "?@gateway=https%3A%2F%2Fa.example" +
1366
+ "&@gateway=https%3A%2F%2Fb.example%3A8443" +
1367
+ "&@gateway=http%3A%2F%2Fc.example" +
1368
+ "&@gateway=https%3A%2F%2Fxn--r8jz45g.jp",
1369
+ );
1370
+ strictEqual(
1371
+ formatIri(
1372
+ withGatewayHints(
1373
+ "ap://did:key:z6Mkabc/actor",
1374
+ new Set(["https://b.example", "https://a.example"]),
1375
+ ),
1376
+ ),
1377
+ "ap+ef61://did:key:z6Mkabc/actor" +
1378
+ "?@gateway=https%3A%2F%2Fb.example&@gateway=https%3A%2F%2Fa.example",
1379
+ );
1380
+ // There is no limit on the number of hints:
1381
+ const many = Array.from(
1382
+ { length: 7 },
1383
+ (_, i) => `https://server${i}.example`,
1384
+ );
1385
+ deepStrictEqual(
1386
+ getGatewayHints(withGatewayHints("ap://did:key:z6Mkabc/actor", many))
1387
+ .map((url) => url.origin),
1388
+ many,
1389
+ );
1390
+ });
1391
+
1392
+ test("withGatewayHints() keeps other query parameters and fragments", () => {
1393
+ strictEqual(
1394
+ formatIri(
1395
+ withGatewayHints(
1396
+ "ap://did:key:z6Mkabc/collection?page=3&maxItems=20&q=a+b%20c%2b",
1397
+ ["https://server.example"],
1398
+ ),
1399
+ ),
1400
+ "ap+ef61://did:key:z6Mkabc/collection?page=3&maxItems=20&q=a+b%20c%2B" +
1401
+ "&@gateway=https%3A%2F%2Fserver.example",
1402
+ );
1403
+ strictEqual(
1404
+ formatIri(
1405
+ withGatewayHints(
1406
+ "ap://did:key:z6Mkabc/actor?x=%26%3D#main-key",
1407
+ ["https://server.example"],
1408
+ ),
1409
+ ),
1410
+ "ap+ef61://did:key:z6Mkabc/actor?x=%26%3D" +
1411
+ "&@gateway=https%3A%2F%2Fserver.example#main-key",
1412
+ );
1413
+ strictEqual(
1414
+ withGatewayHints("ap://did:key:z6Mkabc/actor#", ["https://s.example"])
1415
+ .href,
1416
+ "ap+ef61://did%3Akey%3Az6Mkabc/actor" +
1417
+ "?@gateway=https%3A%2F%2Fs.example#",
1418
+ );
1419
+ });
1420
+
1421
+ test("withGatewayHints() replaces existing location hints", () => {
1422
+ strictEqual(
1423
+ formatIri(
1424
+ withGatewayHints(
1425
+ "ap://did:key:z6Mkabc/collection?@gateway=https%3A%2F%2Fold.example" +
1426
+ "&page=2&%40gateway=https%3A%2F%2Fold2.example&@gateway" +
1427
+ "&gateways=https%3A%2F%2Flegacy.example&%2540gateway=kept",
1428
+ ["https://new.example"],
1429
+ ),
1430
+ ),
1431
+ "ap+ef61://did:key:z6Mkabc/collection?page=2&%2540gateway=kept" +
1432
+ "&@gateway=https%3A%2F%2Fnew.example",
1433
+ );
1434
+ // Characters that the URL parser strips cannot turn into a hint name:
1435
+ for (const char of ["\t", "\n", "\r"]) {
1436
+ const hinted = withGatewayHints(
1437
+ `ap://did:key:z6Mkabc/actor?@gate${char}way=https%3A%2F%2Fevil.example`,
1438
+ ["https://new.example"],
1439
+ );
1440
+ deepStrictEqual(getGatewayHints(hinted).map((url) => url.href), [
1441
+ "https://new.example/",
1442
+ ]);
1443
+ }
1444
+ });
1445
+
1446
+ test("withGatewayHints() with no gateways removes location hints", () => {
1447
+ for (
1448
+ const [input, expected] of [
1449
+ [
1450
+ "ap://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fa.example",
1451
+ "ap+ef61://did%3Akey%3Az6Mkabc/actor",
1452
+ ],
1453
+ [
1454
+ "ap://did:key:z6Mkabc/actor?&@gateway=https%3A%2F%2Fa.example&&p=1&",
1455
+ "ap+ef61://did%3Akey%3Az6Mkabc/actor?p=1",
1456
+ ],
1457
+ ["ap://did:key:z6Mkabc/actor?", "ap+ef61://did%3Akey%3Az6Mkabc/actor"],
1458
+ [
1459
+ "ap://did:key:z6Mkabc/actor?gateways=https%3A%2F%2Fa.example#k",
1460
+ "ap+ef61://did%3Akey%3Az6Mkabc/actor#k",
1461
+ ],
1462
+ ]
1463
+ ) {
1464
+ strictEqual(withGatewayHints(input, []).href, expected, input);
1465
+ strictEqual(withoutGatewayHints(input).href, expected, input);
1466
+ }
1467
+ const input = parseIri(
1468
+ "ap://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fa.example",
1469
+ );
1470
+ const href = input.href;
1471
+ withoutGatewayHints(input);
1472
+ strictEqual(input.href, href);
1473
+ });
1474
+
1475
+ test("withGatewayHints() rejects non-portable IDs", () => {
1476
+ for (
1477
+ const id of [
1478
+ "https://server.example/.well-known/apgateway/did:key:z6Mkabc/actor",
1479
+ "https://example.com/actor",
1480
+ "did:key:z6Mkabc#z6Mkabc",
1481
+ "ap://did:key:z6Mkabc",
1482
+ "ap://example.com/actor",
1483
+ "ap://did:key:z6Mkabc/actor%zz",
1484
+ new URL("https://example.com/actor"),
1485
+ new URL("ap://did%3Akey%3Az6Mkabc:8080/actor"),
1486
+ new URL("ap://user@did%3Akey%3Az6Mkabc/actor"),
1487
+ ]
1488
+ ) {
1489
+ throws(
1490
+ () => withGatewayHints(id, ["https://server.example"]),
1491
+ TypeError,
1492
+ String(id),
1493
+ );
1494
+ throws(() => withoutGatewayHints(id), TypeError, String(id));
1495
+ throws(() => getGatewayHints(id), TypeError, String(id));
1496
+ }
1497
+ });
1498
+
1499
+ test("withGatewayHints() rejects paths that URLs cannot represent", () => {
1500
+ for (
1501
+ const id of [
1502
+ "ap://did:key:z6Mkabc/a/../actor",
1503
+ "ap://did:key:z6Mkabc/a/./actor",
1504
+ "ap://did:key:z6Mkabc/a/%2e%2e/actor",
1505
+ "ap://did:key:z6Mkabc/a/%2E/actor",
1506
+ ]
1507
+ ) {
1508
+ throws(
1509
+ () => withGatewayHints(id, ["https://server.example"]),
1510
+ TypeError,
1511
+ id,
1512
+ );
1513
+ throws(() => withoutGatewayHints(id), TypeError, id);
1514
+ }
1515
+ // Characters that the URL parser would strip are percent-encoded instead:
1516
+ strictEqual(
1517
+ withoutGatewayHints("ap://did:key:z6Mkabc/a\tb").href,
1518
+ "ap+ef61://did%3Akey%3Az6Mkabc/a%09b",
1519
+ );
1520
+ });
1521
+
1522
+ test("withGatewayHints() rejects invalid gateways", () => {
1523
+ for (
1524
+ const gateway of [
1525
+ "https://server.example/path",
1526
+ "https://server.example/?",
1527
+ "https://server.example/#",
1528
+ "https://server.example/?q=1",
1529
+ "https://user:pass@server.example",
1530
+ "ftp://server.example",
1531
+ "ap://did:key:z6Mkabc/actor",
1532
+ "server.example",
1533
+ new URL("https://server.example/path"),
1534
+ ]
1535
+ ) {
1536
+ throws(
1537
+ () => withGatewayHints("ap://did:key:z6Mkabc/actor", [gateway]),
1538
+ TypeError,
1539
+ String(gateway),
1540
+ );
1541
+ }
1542
+ throws(
1543
+ () =>
1544
+ withGatewayHints(
1545
+ "ap://did:key:z6Mkabc/actor",
1546
+ "https://server.example" as unknown as string[],
1547
+ ),
1548
+ TypeError,
1549
+ );
1550
+ });
1551
+
1552
+ test("getGatewayHints() reads @gateway location hints", () => {
1553
+ deepStrictEqual(
1554
+ getGatewayHints(
1555
+ "ap://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fa.example" +
1556
+ "&@gateway=invalid&%40gateway=https%3A%2F%2Fb.example%2F" +
1557
+ "&@gateway=https%3A%2F%2Fa.example%2F" +
1558
+ "&@gateway=https%3A%2F%2Fc.example%2Fpath" +
1559
+ "&gateways=https%3A%2F%2Flegacy.example&page=1#k",
1560
+ ).map((url) => url.href),
1561
+ ["https://a.example/", "https://b.example/"],
1562
+ );
1563
+ deepStrictEqual(getGatewayHints("ap://did:key:z6Mkabc/actor"), []);
1564
+ // A literal question mark is a part of the parameter name:
1565
+ const questioned =
1566
+ "ap://did:key:z6Mkabc/actor??@gateway=https%3A%2F%2Fa.example";
1567
+ deepStrictEqual(getGatewayHints(questioned), []);
1568
+ strictEqual(
1569
+ withoutGatewayHints(questioned).href,
1570
+ "ap+ef61://did%3Akey%3Az6Mkabc/actor??@gateway=https%3A%2F%2Fa.example",
1571
+ );
1572
+ deepStrictEqual(
1573
+ getGatewayHints(withGatewayHints(questioned, ["https://b.example"]))
1574
+ .map((url) => url.href),
1575
+ ["https://b.example/"],
1576
+ );
1577
+ deepStrictEqual(
1578
+ getGatewayHints(
1579
+ "ap://did:key:z6Mkabc/actor?@gate%09way=https%3A%2F%2Fa.example",
1580
+ ),
1581
+ [],
1582
+ );
1583
+ deepStrictEqual(
1584
+ getGatewayHints(
1585
+ "ap://did:key:z6Mkabc/actor?@gate\tway=https%3A%2F%2Fa.example",
1586
+ ),
1587
+ [],
1588
+ );
1589
+ });
1590
+
1591
+ test("withGatewayHints() round-trips with other portable ID helpers", () => {
1592
+ const hinted = withGatewayHints("ap://did:key:z6Mkabc/objects/1#frag", [
1593
+ "https://server1.example",
1594
+ "https://server2.example",
1595
+ ]);
1596
+ strictEqual(
1597
+ canonicalizePortableUri(formatIri(hinted)),
1598
+ "ap+ef61://did:key:z6Mkabc/objects/1#frag",
1599
+ );
1600
+ ok(
1601
+ arePortableUrisEqual(
1602
+ formatIri(hinted),
1603
+ "ap://did:key:z6Mkabc/objects/1#frag",
1604
+ ),
1605
+ );
1606
+ strictEqual(
1607
+ toCompatibleEf61Id(hinted, "https://server1.example").href,
1608
+ "https://server1.example/.well-known/apgateway/did:key:z6Mkabc/objects/1#frag",
1609
+ );
1610
+ strictEqual(
1611
+ formatIri(withoutGatewayHints(hinted)),
1612
+ "ap+ef61://did:key:z6Mkabc/objects/1#frag",
1613
+ );
1614
+ });