@fedify/vocab-runtime 2.4.0-dev.2219 → 2.4.0-dev.2233

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 (45) 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 +4 -13
  5. package/dist/internal/portable-dereference.js +2 -11
  6. package/dist/mod.cjs +5 -2
  7. package/dist/mod.d.cts +95 -1
  8. package/dist/mod.d.ts +95 -1
  9. package/dist/mod.js +3 -3
  10. package/dist/tests/{body-BzhhuMI0.mjs → body-BEYUpKvX.mjs} +1 -1
  11. package/dist/tests/{body-GaHFfkaq.cjs → body-DQKTFMsH.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-sGr7kARm.cjs → docloader-DHCYdBXf.cjs} +3 -3
  17. package/dist/tests/{docloader-DiRcdiPp.mjs → docloader-ra1lbBfP.mjs} +3 -3
  18. package/dist/tests/docloader.test.cjs +3 -3
  19. package/dist/tests/docloader.test.mjs +3 -3
  20. package/dist/tests/internal/portable-dereference.test.cjs +16 -15
  21. package/dist/tests/internal/portable-dereference.test.mjs +14 -13
  22. package/dist/tests/{jsonld-cache-DUmuWRwp.mjs → jsonld-cache-BKrsWPCQ.mjs} +1 -1
  23. package/dist/tests/{jsonld-cache-BQMn_ILF.cjs → jsonld-cache-BTgizq9A.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/{portable-media-BERS_tX_.mjs → portable-media-1NjUQari.mjs} +3 -3
  27. package/dist/tests/{portable-media-C_Q9GmqU.cjs → portable-media-DHaAEpaQ.cjs} +3 -3
  28. package/dist/tests/portable-media.test.cjs +1 -1
  29. package/dist/tests/portable-media.test.mjs +1 -1
  30. package/dist/tests/{request-qq1N2cl6.cjs → request-BQlkB8du.cjs} +1 -1
  31. package/dist/tests/{request-CWTG9_Xx.mjs → request-D3elVRyB.mjs} +1 -1
  32. package/dist/tests/request.test.cjs +1 -1
  33. package/dist/tests/request.test.mjs +1 -1
  34. package/dist/tests/{url-DfS-cvwr.mjs → url-Cdu4ASgJ.mjs} +165 -3
  35. package/dist/tests/{url-B8xyTwP-.cjs → url-CtHyzMR4.cjs} +194 -2
  36. package/dist/tests/url.test.cjs +133 -3
  37. package/dist/tests/url.test.mjs +133 -3
  38. package/dist/{url-DDVweCXr.cjs → url-BHfQuRdy.cjs} +194 -2
  39. package/dist/{url-Be3moMor.js → url-DqqDZY9i.js} +165 -3
  40. package/package.json +1 -1
  41. package/src/internal/portable-dereference.test.ts +16 -1
  42. package/src/internal/portable-dereference.ts +3 -20
  43. package/src/mod.ts +3 -0
  44. package/src/url.test.ts +311 -1
  45. package/src/url.ts +236 -8
@@ -1,4 +1,4 @@
1
- const require_url = require("./url-B8xyTwP-.cjs");
1
+ const require_url = require("./url-CtHyzMR4.cjs");
2
2
  let node_test = require("node:test");
3
3
  let node_assert = require("node:assert");
4
4
  //#region src/url.test.ts
@@ -228,7 +228,11 @@ let node_assert = require("node:assert");
228
228
  (0, node_assert.deepStrictEqual)(require_url.formatIri(parsed), "ap+ef61://did:web:example.com%00/u/1");
229
229
  });
230
230
  (0, node_test.test)("parseGatewayUrl() accepts only HTTP(S) base URIs", () => {
231
- for (const url of ["https://server.example/", "http://server.example/"]) {
231
+ for (const url of [
232
+ "https://server.example",
233
+ "https://server.example/",
234
+ "http://server.example/"
235
+ ]) {
232
236
  (0, node_assert.deepStrictEqual)(require_url.parseGatewayUrl(url), new URL(url));
233
237
  (0, node_assert.ok)(require_url.isGatewayUrl(new URL(url)));
234
238
  }
@@ -238,11 +242,19 @@ let node_assert = require("node:assert");
238
242
  "https://user@server.example/",
239
243
  "https://server.example/path",
240
244
  "https://server.example/?x=1",
241
- "https://server.example/#fragment"
245
+ "https://server.example/?",
246
+ "https://server.example/#fragment",
247
+ "https://server.example/#"
242
248
  ]) {
243
249
  (0, node_assert.throws)(() => require_url.parseGatewayUrl(url), (error) => error instanceof TypeError && error.message.startsWith("Invalid FEP-ef61 gateway: "));
244
250
  (0, node_assert.ok)(!require_url.isGatewayUrl(new URL(url)));
245
251
  }
252
+ const mutated = new URL("https://server.example/");
253
+ mutated.search = "?";
254
+ (0, node_assert.ok)(!require_url.isGatewayUrl(mutated));
255
+ mutated.search = "";
256
+ mutated.hash = "#";
257
+ (0, node_assert.ok)(!require_url.isGatewayUrl(mutated));
246
258
  });
247
259
  (0, node_test.test)("fromCompatibleEf61Id() converts compatible identifiers", () => {
248
260
  for (const [input, expected] of [
@@ -608,4 +620,122 @@ let node_assert = require("node:assert");
608
620
  (0, node_assert.deepStrictEqual)(require_url.expandIPv6Address("2001:db8::1"), "2001:0db8:0000:0000:0000:0000:0000:0001");
609
621
  (0, node_assert.deepStrictEqual)(require_url.expandIPv6Address("64:ff9b::8.8.8.8"), "0064:ff9b:0000:0000:0000:0000:0808:0808");
610
622
  });
623
+ (0, node_test.test)("withGatewayHints() adds @gateway location hints", () => {
624
+ const expected = "ap+ef61://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fserver1.example&@gateway=https%3A%2F%2Fserver2.example";
625
+ for (const id of [
626
+ "ap://did:key:z6Mkabc/actor",
627
+ "ap+ef61://did:key:z6Mkabc/actor",
628
+ "ap://did%3Akey%3Az6Mkabc/actor",
629
+ require_url.parseIri("ap://did:key:z6Mkabc/actor"),
630
+ new URL("ap://did%3Akey%3Az6Mkabc/actor")
631
+ ]) {
632
+ const hinted = require_url.withGatewayHints(id, ["https://server1.example", new URL("https://server2.example/")]);
633
+ (0, node_assert.ok)(hinted instanceof URL);
634
+ (0, node_assert.strictEqual)(hinted.href, "ap+ef61://did%3Akey%3Az6Mkabc/actor?@gateway=https%3A%2F%2Fserver1.example&@gateway=https%3A%2F%2Fserver2.example");
635
+ (0, node_assert.strictEqual)(require_url.formatIri(hinted), expected);
636
+ (0, node_assert.deepStrictEqual)(require_url.parseIri(require_url.formatIri(hinted)), hinted);
637
+ }
638
+ });
639
+ (0, node_test.test)("withGatewayHints() encodes and deduplicates gateway origins", () => {
640
+ function* gateways() {
641
+ yield "https://A.example:443";
642
+ yield "https://a.example/";
643
+ yield new URL("https://a.example");
644
+ yield "https://b.example:8443";
645
+ yield "http://c.example:80/";
646
+ yield "https://例え.jp";
647
+ }
648
+ (0, node_assert.strictEqual)(require_url.formatIri(require_url.withGatewayHints("ap://did:key:z6Mkabc/actor", gateways())), "ap+ef61://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fa.example&@gateway=https%3A%2F%2Fb.example%3A8443&@gateway=http%3A%2F%2Fc.example&@gateway=https%3A%2F%2Fxn--r8jz45g.jp");
649
+ (0, node_assert.strictEqual)(require_url.formatIri(require_url.withGatewayHints("ap://did:key:z6Mkabc/actor", /* @__PURE__ */ new Set(["https://b.example", "https://a.example"]))), "ap+ef61://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fb.example&@gateway=https%3A%2F%2Fa.example");
650
+ const many = Array.from({ length: 7 }, (_, i) => `https://server${i}.example`);
651
+ (0, node_assert.deepStrictEqual)(require_url.getGatewayHints(require_url.withGatewayHints("ap://did:key:z6Mkabc/actor", many)).map((url) => url.origin), many);
652
+ });
653
+ (0, node_test.test)("withGatewayHints() keeps other query parameters and fragments", () => {
654
+ (0, node_assert.strictEqual)(require_url.formatIri(require_url.withGatewayHints("ap://did:key:z6Mkabc/collection?page=3&maxItems=20&q=a+b%20c%2b", ["https://server.example"])), "ap+ef61://did:key:z6Mkabc/collection?page=3&maxItems=20&q=a+b%20c%2B&@gateway=https%3A%2F%2Fserver.example");
655
+ (0, node_assert.strictEqual)(require_url.formatIri(require_url.withGatewayHints("ap://did:key:z6Mkabc/actor?x=%26%3D#main-key", ["https://server.example"])), "ap+ef61://did:key:z6Mkabc/actor?x=%26%3D&@gateway=https%3A%2F%2Fserver.example#main-key");
656
+ (0, node_assert.strictEqual)(require_url.withGatewayHints("ap://did:key:z6Mkabc/actor#", ["https://s.example"]).href, "ap+ef61://did%3Akey%3Az6Mkabc/actor?@gateway=https%3A%2F%2Fs.example#");
657
+ });
658
+ (0, node_test.test)("withGatewayHints() replaces existing location hints", () => {
659
+ (0, node_assert.strictEqual)(require_url.formatIri(require_url.withGatewayHints("ap://did:key:z6Mkabc/collection?@gateway=https%3A%2F%2Fold.example&page=2&%40gateway=https%3A%2F%2Fold2.example&@gateway&gateways=https%3A%2F%2Flegacy.example&%2540gateway=kept", ["https://new.example"])), "ap+ef61://did:key:z6Mkabc/collection?page=2&%2540gateway=kept&@gateway=https%3A%2F%2Fnew.example");
660
+ for (const char of [
661
+ " ",
662
+ "\n",
663
+ "\r"
664
+ ]) (0, node_assert.deepStrictEqual)(require_url.getGatewayHints(require_url.withGatewayHints(`ap://did:key:z6Mkabc/actor?@gate${char}way=https%3A%2F%2Fevil.example`, ["https://new.example"])).map((url) => url.href), ["https://new.example/"]);
665
+ });
666
+ (0, node_test.test)("withGatewayHints() with no gateways removes location hints", () => {
667
+ for (const [input, expected] of [
668
+ ["ap://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fa.example", "ap+ef61://did%3Akey%3Az6Mkabc/actor"],
669
+ ["ap://did:key:z6Mkabc/actor?&@gateway=https%3A%2F%2Fa.example&&p=1&", "ap+ef61://did%3Akey%3Az6Mkabc/actor?p=1"],
670
+ ["ap://did:key:z6Mkabc/actor?", "ap+ef61://did%3Akey%3Az6Mkabc/actor"],
671
+ ["ap://did:key:z6Mkabc/actor?gateways=https%3A%2F%2Fa.example#k", "ap+ef61://did%3Akey%3Az6Mkabc/actor#k"]
672
+ ]) {
673
+ (0, node_assert.strictEqual)(require_url.withGatewayHints(input, []).href, expected, input);
674
+ (0, node_assert.strictEqual)(require_url.withoutGatewayHints(input).href, expected, input);
675
+ }
676
+ const input = require_url.parseIri("ap://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fa.example");
677
+ const href = input.href;
678
+ require_url.withoutGatewayHints(input);
679
+ (0, node_assert.strictEqual)(input.href, href);
680
+ });
681
+ (0, node_test.test)("withGatewayHints() rejects non-portable IDs", () => {
682
+ for (const id of [
683
+ "https://server.example/.well-known/apgateway/did:key:z6Mkabc/actor",
684
+ "https://example.com/actor",
685
+ "did:key:z6Mkabc#z6Mkabc",
686
+ "ap://did:key:z6Mkabc",
687
+ "ap://example.com/actor",
688
+ "ap://did:key:z6Mkabc/actor%zz",
689
+ new URL("https://example.com/actor"),
690
+ new URL("ap://did%3Akey%3Az6Mkabc:8080/actor"),
691
+ new URL("ap://user@did%3Akey%3Az6Mkabc/actor")
692
+ ]) {
693
+ (0, node_assert.throws)(() => require_url.withGatewayHints(id, ["https://server.example"]), TypeError, String(id));
694
+ (0, node_assert.throws)(() => require_url.withoutGatewayHints(id), TypeError, String(id));
695
+ (0, node_assert.throws)(() => require_url.getGatewayHints(id), TypeError, String(id));
696
+ }
697
+ });
698
+ (0, node_test.test)("withGatewayHints() rejects paths that URLs cannot represent", () => {
699
+ for (const id of [
700
+ "ap://did:key:z6Mkabc/a/../actor",
701
+ "ap://did:key:z6Mkabc/a/./actor",
702
+ "ap://did:key:z6Mkabc/a/%2e%2e/actor",
703
+ "ap://did:key:z6Mkabc/a/%2E/actor"
704
+ ]) {
705
+ (0, node_assert.throws)(() => require_url.withGatewayHints(id, ["https://server.example"]), TypeError, id);
706
+ (0, node_assert.throws)(() => require_url.withoutGatewayHints(id), TypeError, id);
707
+ }
708
+ (0, node_assert.strictEqual)(require_url.withoutGatewayHints("ap://did:key:z6Mkabc/a b").href, "ap+ef61://did%3Akey%3Az6Mkabc/a%09b");
709
+ });
710
+ (0, node_test.test)("withGatewayHints() rejects invalid gateways", () => {
711
+ for (const gateway of [
712
+ "https://server.example/path",
713
+ "https://server.example/?",
714
+ "https://server.example/#",
715
+ "https://server.example/?q=1",
716
+ "https://user:pass@server.example",
717
+ "ftp://server.example",
718
+ "ap://did:key:z6Mkabc/actor",
719
+ "server.example",
720
+ new URL("https://server.example/path")
721
+ ]) (0, node_assert.throws)(() => require_url.withGatewayHints("ap://did:key:z6Mkabc/actor", [gateway]), TypeError, String(gateway));
722
+ (0, node_assert.throws)(() => require_url.withGatewayHints("ap://did:key:z6Mkabc/actor", "https://server.example"), TypeError);
723
+ });
724
+ (0, node_test.test)("getGatewayHints() reads @gateway location hints", () => {
725
+ (0, node_assert.deepStrictEqual)(require_url.getGatewayHints("ap://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fa.example&@gateway=invalid&%40gateway=https%3A%2F%2Fb.example%2F&@gateway=https%3A%2F%2Fa.example%2F&@gateway=https%3A%2F%2Fc.example%2Fpath&gateways=https%3A%2F%2Flegacy.example&page=1#k").map((url) => url.href), ["https://a.example/", "https://b.example/"]);
726
+ (0, node_assert.deepStrictEqual)(require_url.getGatewayHints("ap://did:key:z6Mkabc/actor"), []);
727
+ const questioned = "ap://did:key:z6Mkabc/actor??@gateway=https%3A%2F%2Fa.example";
728
+ (0, node_assert.deepStrictEqual)(require_url.getGatewayHints(questioned), []);
729
+ (0, node_assert.strictEqual)(require_url.withoutGatewayHints(questioned).href, "ap+ef61://did%3Akey%3Az6Mkabc/actor??@gateway=https%3A%2F%2Fa.example");
730
+ (0, node_assert.deepStrictEqual)(require_url.getGatewayHints(require_url.withGatewayHints(questioned, ["https://b.example"])).map((url) => url.href), ["https://b.example/"]);
731
+ (0, node_assert.deepStrictEqual)(require_url.getGatewayHints("ap://did:key:z6Mkabc/actor?@gate%09way=https%3A%2F%2Fa.example"), []);
732
+ (0, node_assert.deepStrictEqual)(require_url.getGatewayHints("ap://did:key:z6Mkabc/actor?@gate way=https%3A%2F%2Fa.example"), []);
733
+ });
734
+ (0, node_test.test)("withGatewayHints() round-trips with other portable ID helpers", () => {
735
+ const hinted = require_url.withGatewayHints("ap://did:key:z6Mkabc/objects/1#frag", ["https://server1.example", "https://server2.example"]);
736
+ (0, node_assert.strictEqual)(require_url.canonicalizePortableUri(require_url.formatIri(hinted)), "ap+ef61://did:key:z6Mkabc/objects/1#frag");
737
+ (0, node_assert.ok)(require_url.arePortableUrisEqual(require_url.formatIri(hinted), "ap://did:key:z6Mkabc/objects/1#frag"));
738
+ (0, node_assert.strictEqual)(require_url.toCompatibleEf61Id(hinted, "https://server1.example").href, "https://server1.example/.well-known/apgateway/did:key:z6Mkabc/objects/1#frag");
739
+ (0, node_assert.strictEqual)(require_url.formatIri(require_url.withoutGatewayHints(hinted)), "ap+ef61://did:key:z6Mkabc/objects/1#frag");
740
+ });
611
741
  //#endregion
@@ -1,4 +1,4 @@
1
- import { _ as validateLookupAddresses, a as formatIri, c as haveSameFe34Origin, d as isValidPublicIPv4Address, f as isValidPublicIPv6Address, g as toCompatibleEf61Id, h as parseJsonLdId, i as expandIPv6Address, l as haveSameIriOrigin, m as parseIri, n as arePortableUrisEqual, o as fromCompatibleEf61Id, p as parseGatewayUrl, r as canonicalizePortableUri, s as getFe34Origin, t as UrlError, u as isGatewayUrl, v as validatePublicUrl } from "./url-DfS-cvwr.mjs";
1
+ import { C as withoutGatewayHints, S as withGatewayHints, _ as parseIri, a as expandIPv6Address, b as validateLookupAddresses, c as getFe34Origin, d as haveSameIriOrigin, f as isGatewayUrl, g as parseGatewayUrl, i as canonicalizePortableUri, l as getGatewayHints, m as isValidPublicIPv6Address, n as UrlError, o as formatIri, p as isValidPublicIPv4Address, r as arePortableUrisEqual, s as fromCompatibleEf61Id, u as haveSameFe34Origin, v as parseJsonLdId, x as validatePublicUrl, y as toCompatibleEf61Id } from "./url-Cdu4ASgJ.mjs";
2
2
  import { test } from "node:test";
3
3
  import { deepStrictEqual, ok, rejects, strictEqual, throws } from "node:assert";
4
4
  //#region src/url.test.ts
@@ -228,7 +228,11 @@ test("parseIri() preserves encoded percent signs while decoding delimiters", ()
228
228
  deepStrictEqual(formatIri(parsed), "ap+ef61://did:web:example.com%00/u/1");
229
229
  });
230
230
  test("parseGatewayUrl() accepts only HTTP(S) base URIs", () => {
231
- for (const url of ["https://server.example/", "http://server.example/"]) {
231
+ for (const url of [
232
+ "https://server.example",
233
+ "https://server.example/",
234
+ "http://server.example/"
235
+ ]) {
232
236
  deepStrictEqual(parseGatewayUrl(url), new URL(url));
233
237
  ok(isGatewayUrl(new URL(url)));
234
238
  }
@@ -238,11 +242,19 @@ test("parseGatewayUrl() accepts only HTTP(S) base URIs", () => {
238
242
  "https://user@server.example/",
239
243
  "https://server.example/path",
240
244
  "https://server.example/?x=1",
241
- "https://server.example/#fragment"
245
+ "https://server.example/?",
246
+ "https://server.example/#fragment",
247
+ "https://server.example/#"
242
248
  ]) {
243
249
  throws(() => parseGatewayUrl(url), (error) => error instanceof TypeError && error.message.startsWith("Invalid FEP-ef61 gateway: "));
244
250
  ok(!isGatewayUrl(new URL(url)));
245
251
  }
252
+ const mutated = new URL("https://server.example/");
253
+ mutated.search = "?";
254
+ ok(!isGatewayUrl(mutated));
255
+ mutated.search = "";
256
+ mutated.hash = "#";
257
+ ok(!isGatewayUrl(mutated));
246
258
  });
247
259
  test("fromCompatibleEf61Id() converts compatible identifiers", () => {
248
260
  for (const [input, expected] of [
@@ -608,5 +620,123 @@ test("expandIPv6Address()", () => {
608
620
  deepStrictEqual(expandIPv6Address("2001:db8::1"), "2001:0db8:0000:0000:0000:0000:0000:0001");
609
621
  deepStrictEqual(expandIPv6Address("64:ff9b::8.8.8.8"), "0064:ff9b:0000:0000:0000:0000:0808:0808");
610
622
  });
623
+ test("withGatewayHints() adds @gateway location hints", () => {
624
+ const expected = "ap+ef61://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fserver1.example&@gateway=https%3A%2F%2Fserver2.example";
625
+ for (const id of [
626
+ "ap://did:key:z6Mkabc/actor",
627
+ "ap+ef61://did:key:z6Mkabc/actor",
628
+ "ap://did%3Akey%3Az6Mkabc/actor",
629
+ parseIri("ap://did:key:z6Mkabc/actor"),
630
+ new URL("ap://did%3Akey%3Az6Mkabc/actor")
631
+ ]) {
632
+ const hinted = withGatewayHints(id, ["https://server1.example", new URL("https://server2.example/")]);
633
+ ok(hinted instanceof URL);
634
+ strictEqual(hinted.href, "ap+ef61://did%3Akey%3Az6Mkabc/actor?@gateway=https%3A%2F%2Fserver1.example&@gateway=https%3A%2F%2Fserver2.example");
635
+ strictEqual(formatIri(hinted), expected);
636
+ deepStrictEqual(parseIri(formatIri(hinted)), hinted);
637
+ }
638
+ });
639
+ test("withGatewayHints() encodes and deduplicates gateway origins", () => {
640
+ function* gateways() {
641
+ yield "https://A.example:443";
642
+ yield "https://a.example/";
643
+ yield new URL("https://a.example");
644
+ yield "https://b.example:8443";
645
+ yield "http://c.example:80/";
646
+ yield "https://例え.jp";
647
+ }
648
+ strictEqual(formatIri(withGatewayHints("ap://did:key:z6Mkabc/actor", gateways())), "ap+ef61://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fa.example&@gateway=https%3A%2F%2Fb.example%3A8443&@gateway=http%3A%2F%2Fc.example&@gateway=https%3A%2F%2Fxn--r8jz45g.jp");
649
+ strictEqual(formatIri(withGatewayHints("ap://did:key:z6Mkabc/actor", /* @__PURE__ */ new Set(["https://b.example", "https://a.example"]))), "ap+ef61://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fb.example&@gateway=https%3A%2F%2Fa.example");
650
+ const many = Array.from({ length: 7 }, (_, i) => `https://server${i}.example`);
651
+ deepStrictEqual(getGatewayHints(withGatewayHints("ap://did:key:z6Mkabc/actor", many)).map((url) => url.origin), many);
652
+ });
653
+ test("withGatewayHints() keeps other query parameters and fragments", () => {
654
+ strictEqual(formatIri(withGatewayHints("ap://did:key:z6Mkabc/collection?page=3&maxItems=20&q=a+b%20c%2b", ["https://server.example"])), "ap+ef61://did:key:z6Mkabc/collection?page=3&maxItems=20&q=a+b%20c%2B&@gateway=https%3A%2F%2Fserver.example");
655
+ strictEqual(formatIri(withGatewayHints("ap://did:key:z6Mkabc/actor?x=%26%3D#main-key", ["https://server.example"])), "ap+ef61://did:key:z6Mkabc/actor?x=%26%3D&@gateway=https%3A%2F%2Fserver.example#main-key");
656
+ strictEqual(withGatewayHints("ap://did:key:z6Mkabc/actor#", ["https://s.example"]).href, "ap+ef61://did%3Akey%3Az6Mkabc/actor?@gateway=https%3A%2F%2Fs.example#");
657
+ });
658
+ test("withGatewayHints() replaces existing location hints", () => {
659
+ strictEqual(formatIri(withGatewayHints("ap://did:key:z6Mkabc/collection?@gateway=https%3A%2F%2Fold.example&page=2&%40gateway=https%3A%2F%2Fold2.example&@gateway&gateways=https%3A%2F%2Flegacy.example&%2540gateway=kept", ["https://new.example"])), "ap+ef61://did:key:z6Mkabc/collection?page=2&%2540gateway=kept&@gateway=https%3A%2F%2Fnew.example");
660
+ for (const char of [
661
+ " ",
662
+ "\n",
663
+ "\r"
664
+ ]) deepStrictEqual(getGatewayHints(withGatewayHints(`ap://did:key:z6Mkabc/actor?@gate${char}way=https%3A%2F%2Fevil.example`, ["https://new.example"])).map((url) => url.href), ["https://new.example/"]);
665
+ });
666
+ test("withGatewayHints() with no gateways removes location hints", () => {
667
+ for (const [input, expected] of [
668
+ ["ap://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fa.example", "ap+ef61://did%3Akey%3Az6Mkabc/actor"],
669
+ ["ap://did:key:z6Mkabc/actor?&@gateway=https%3A%2F%2Fa.example&&p=1&", "ap+ef61://did%3Akey%3Az6Mkabc/actor?p=1"],
670
+ ["ap://did:key:z6Mkabc/actor?", "ap+ef61://did%3Akey%3Az6Mkabc/actor"],
671
+ ["ap://did:key:z6Mkabc/actor?gateways=https%3A%2F%2Fa.example#k", "ap+ef61://did%3Akey%3Az6Mkabc/actor#k"]
672
+ ]) {
673
+ strictEqual(withGatewayHints(input, []).href, expected, input);
674
+ strictEqual(withoutGatewayHints(input).href, expected, input);
675
+ }
676
+ const input = parseIri("ap://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fa.example");
677
+ const href = input.href;
678
+ withoutGatewayHints(input);
679
+ strictEqual(input.href, href);
680
+ });
681
+ test("withGatewayHints() rejects non-portable IDs", () => {
682
+ for (const id of [
683
+ "https://server.example/.well-known/apgateway/did:key:z6Mkabc/actor",
684
+ "https://example.com/actor",
685
+ "did:key:z6Mkabc#z6Mkabc",
686
+ "ap://did:key:z6Mkabc",
687
+ "ap://example.com/actor",
688
+ "ap://did:key:z6Mkabc/actor%zz",
689
+ new URL("https://example.com/actor"),
690
+ new URL("ap://did%3Akey%3Az6Mkabc:8080/actor"),
691
+ new URL("ap://user@did%3Akey%3Az6Mkabc/actor")
692
+ ]) {
693
+ throws(() => withGatewayHints(id, ["https://server.example"]), TypeError, String(id));
694
+ throws(() => withoutGatewayHints(id), TypeError, String(id));
695
+ throws(() => getGatewayHints(id), TypeError, String(id));
696
+ }
697
+ });
698
+ test("withGatewayHints() rejects paths that URLs cannot represent", () => {
699
+ for (const id of [
700
+ "ap://did:key:z6Mkabc/a/../actor",
701
+ "ap://did:key:z6Mkabc/a/./actor",
702
+ "ap://did:key:z6Mkabc/a/%2e%2e/actor",
703
+ "ap://did:key:z6Mkabc/a/%2E/actor"
704
+ ]) {
705
+ throws(() => withGatewayHints(id, ["https://server.example"]), TypeError, id);
706
+ throws(() => withoutGatewayHints(id), TypeError, id);
707
+ }
708
+ strictEqual(withoutGatewayHints("ap://did:key:z6Mkabc/a b").href, "ap+ef61://did%3Akey%3Az6Mkabc/a%09b");
709
+ });
710
+ test("withGatewayHints() rejects invalid gateways", () => {
711
+ for (const gateway of [
712
+ "https://server.example/path",
713
+ "https://server.example/?",
714
+ "https://server.example/#",
715
+ "https://server.example/?q=1",
716
+ "https://user:pass@server.example",
717
+ "ftp://server.example",
718
+ "ap://did:key:z6Mkabc/actor",
719
+ "server.example",
720
+ new URL("https://server.example/path")
721
+ ]) throws(() => withGatewayHints("ap://did:key:z6Mkabc/actor", [gateway]), TypeError, String(gateway));
722
+ throws(() => withGatewayHints("ap://did:key:z6Mkabc/actor", "https://server.example"), TypeError);
723
+ });
724
+ test("getGatewayHints() reads @gateway location hints", () => {
725
+ deepStrictEqual(getGatewayHints("ap://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fa.example&@gateway=invalid&%40gateway=https%3A%2F%2Fb.example%2F&@gateway=https%3A%2F%2Fa.example%2F&@gateway=https%3A%2F%2Fc.example%2Fpath&gateways=https%3A%2F%2Flegacy.example&page=1#k").map((url) => url.href), ["https://a.example/", "https://b.example/"]);
726
+ deepStrictEqual(getGatewayHints("ap://did:key:z6Mkabc/actor"), []);
727
+ const questioned = "ap://did:key:z6Mkabc/actor??@gateway=https%3A%2F%2Fa.example";
728
+ deepStrictEqual(getGatewayHints(questioned), []);
729
+ strictEqual(withoutGatewayHints(questioned).href, "ap+ef61://did%3Akey%3Az6Mkabc/actor??@gateway=https%3A%2F%2Fa.example");
730
+ deepStrictEqual(getGatewayHints(withGatewayHints(questioned, ["https://b.example"])).map((url) => url.href), ["https://b.example/"]);
731
+ deepStrictEqual(getGatewayHints("ap://did:key:z6Mkabc/actor?@gate%09way=https%3A%2F%2Fa.example"), []);
732
+ deepStrictEqual(getGatewayHints("ap://did:key:z6Mkabc/actor?@gate way=https%3A%2F%2Fa.example"), []);
733
+ });
734
+ test("withGatewayHints() round-trips with other portable ID helpers", () => {
735
+ const hinted = withGatewayHints("ap://did:key:z6Mkabc/objects/1#frag", ["https://server1.example", "https://server2.example"]);
736
+ strictEqual(canonicalizePortableUri(formatIri(hinted)), "ap+ef61://did:key:z6Mkabc/objects/1#frag");
737
+ ok(arePortableUrisEqual(formatIri(hinted), "ap://did:key:z6Mkabc/objects/1#frag"));
738
+ strictEqual(toCompatibleEf61Id(hinted, "https://server1.example").href, "https://server1.example/.well-known/apgateway/did:key:z6Mkabc/objects/1#frag");
739
+ strictEqual(formatIri(withoutGatewayHints(hinted)), "ap+ef61://did:key:z6Mkabc/objects/1#frag");
740
+ });
611
741
  //#endregion
612
742
  export {};
@@ -221,9 +221,10 @@ function parseAtUri(uri) {
221
221
  }
222
222
  /**
223
223
  * Checks whether the URL is an FEP-ef61 gateway base URI.
224
+ * @since 2.4.0
224
225
  */
225
226
  function isGatewayUrl(url) {
226
- return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "" && url.pathname === "/" && url.search === "" && url.hash === "";
227
+ return (url.protocol === "http:" || url.protocol === "https:") && url.href === `${url.origin}/`;
227
228
  }
228
229
  /**
229
230
  * Parses and validates an FEP-ef61 gateway base URI.
@@ -231,6 +232,7 @@ function isGatewayUrl(url) {
231
232
  * with no credentials, path, query, or fragment. In the
232
233
  * latter case, the message starts with
233
234
  * `Invalid FEP-ef61 gateway:`.
235
+ * @since 2.4.0
234
236
  */
235
237
  function parseGatewayUrl(url) {
236
238
  const parsed = parseIri(url);
@@ -358,7 +360,7 @@ function toCompatibleEf61Id(portableId, gateway) {
358
360
  }
359
361
  function parseCompatibleEf61Gateway(gateway) {
360
362
  const url = gateway instanceof URL ? gateway : typeof gateway === "string" && URL.canParse(gateway) ? new URL(gateway) : null;
361
- if (url == null || url.protocol !== "http:" && url.protocol !== "https:" || url.href !== `${url.origin}/`) throw new TypeError("FEP-ef61 gateways for compatible identifiers must be HTTP(S) origins with no credentials, path, query, or fragment.");
363
+ if (url == null || !isGatewayUrl(url)) throw new TypeError("FEP-ef61 gateways for compatible identifiers must be HTTP(S) origins with no credentials, path, query, or fragment.");
362
364
  return url;
363
365
  }
364
366
  function getRawPortableIri(portableId) {
@@ -384,6 +386,166 @@ function isLocationHint(pair) {
384
386
  }
385
387
  }
386
388
  /**
389
+ * The name of the FEP-ef61 location hint query parameter.
390
+ * @internal
391
+ */
392
+ const GATEWAY_HINT_PARAMETER = "@gateway";
393
+ /**
394
+ * Parses an FEP-ef61 gateway, which has to be an HTTP(S) origin with no
395
+ * credentials, path, query, or fragment.
396
+ * @returns The gateway, or `null` if it is not a valid gateway.
397
+ * @internal
398
+ */
399
+ function parseGatewayOrigin(gateway) {
400
+ let url;
401
+ if (gateway instanceof URL) url = new URL(gateway.href);
402
+ else if (typeof gateway === "string" && URL.canParse(gateway)) url = new URL(gateway);
403
+ else return null;
404
+ return isGatewayUrl(url) ? url : null;
405
+ }
406
+ /**
407
+ * Returns a copy of an [FEP-ef61] portable ActivityPub URI with `@gateway`
408
+ * location hints for the given gateways, which tell consumers where they can
409
+ * retrieve the object. Put hints on *references* to portable actors, e.g.,
410
+ * in `actor`, `attributedTo`, `to`, or `cc`, when constructing an object, as
411
+ * FEP-ef61 recommends:
412
+ *
413
+ * ~~~~ typescript
414
+ * withGatewayHints("ap://did:key:z6Mk.../actor", [
415
+ * "https://server1.example",
416
+ * "https://server2.example",
417
+ * ]);
418
+ * // ap+ef61://did:key:z6Mk.../actor?@gateway=https%3A%2F%2Fserver1.example&@gateway=https%3A%2F%2Fserver2.example
419
+ * ~~~~
420
+ *
421
+ * Do not put hints on an object's own `id`. Hints do not change the
422
+ * identity of a portable URI, since FEP-ef61 drops the query when comparing
423
+ * portable URIs, but implementations that do not canonicalize portable URIs
424
+ * would take a hinted ID for another object. Add hints before signing the
425
+ * object, since its Object Integrity Proof covers its references too.
426
+ *
427
+ * The hints that the URI already has, including the legacy `gateways`
428
+ * parameter, are replaced. Each gateway becomes a `@gateway` query
429
+ * parameter whose value is its URI-encoded origin, e.g.,
430
+ * `@gateway=https%3A%2F%2Fserver1.example`, in the given order after the
431
+ * other query parameters. Duplicate gateways are dropped, and an empty list
432
+ * removes the hints as {@link withoutGatewayHints} does. The other query
433
+ * parameters, their order, and the fragment are kept, but percent-encoding
434
+ * is normalized the same way as {@link canonicalizePortableUri} does it.
435
+ *
436
+ * Fedify follows at most five hints when dereferencing a portable URI
437
+ * (three for the key ID of an HTTP Signature), so list the preferred
438
+ * gateways first; there is no limit on the number of hints added here.
439
+ *
440
+ * [FEP-ef61]: https://w3id.org/fep/ef61
441
+ *
442
+ * @param portableId The `ap:` or `ap+ef61:` URI. Pass the raw string rather
443
+ * than a `URL` if its path may have `.` or `..` segments,
444
+ * because the `URL` class resolves them.
445
+ * @param gateways The gateways, e.g., the `gateways` of the actor that the
446
+ * URI refers to. Each has to be an HTTP(S) origin with no
447
+ * credentials, path, query, or fragment.
448
+ * @returns The portable URI with the hints, in the same internal `URL` form
449
+ * as {@link parseIri} returns. Use {@link formatIri} to get its
450
+ * canonical string.
451
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
452
+ * URI, e.g., it is a compatible identifier, which must
453
+ * not have location hints; if its path has `.` or `..`
454
+ * segments, which the `URL` class cannot represent; or
455
+ * if a gateway is invalid.
456
+ * @since 2.4.0
457
+ */
458
+ function withGatewayHints(portableId, gateways) {
459
+ const parts = splitPortableIri(portableId);
460
+ if (typeof gateways === "string") throw new TypeError("The gateways must be an iterable of gateways, not a string.");
461
+ const hints = [];
462
+ const seen = /* @__PURE__ */ new Set();
463
+ for (const gateway of gateways) {
464
+ const url = parseGatewayOrigin(gateway);
465
+ if (url == null) throw new TypeError("FEP-ef61 gateways must be HTTP(S) origins with no credentials, path, query, or fragment: " + String(gateway));
466
+ if (seen.has(url.href)) continue;
467
+ seen.add(url.href);
468
+ hints.push(url);
469
+ }
470
+ return replaceGatewayHints(parts, hints);
471
+ }
472
+ /**
473
+ * Returns a copy of an [FEP-ef61] portable ActivityPub URI without its
474
+ * location hints, i.e., `@gateway` query parameters and the legacy
475
+ * `gateways` parameter. The other query parameters, their order, and the
476
+ * fragment are kept, but percent-encoding is normalized the same way as
477
+ * {@link canonicalizePortableUri} does it.
478
+ *
479
+ * [FEP-ef61]: https://w3id.org/fep/ef61
480
+ *
481
+ * @param portableId The `ap:` or `ap+ef61:` URI. Pass the raw string rather
482
+ * than a `URL` if its path may have `.` or `..` segments,
483
+ * because the `URL` class resolves them.
484
+ * @returns The portable URI without the hints, in the same internal `URL`
485
+ * form as {@link parseIri} returns.
486
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
487
+ * URI, or if its path has `.` or `..` segments, which
488
+ * the `URL` class cannot represent.
489
+ * @since 2.4.0
490
+ */
491
+ function withoutGatewayHints(portableId) {
492
+ return replaceGatewayHints(splitPortableIri(portableId), []);
493
+ }
494
+ /**
495
+ * Gets the gateways in the `@gateway` location hints of an [FEP-ef61]
496
+ * portable ActivityPub URI, in order. Hints that are not valid gateways,
497
+ * i.e., HTTP(S) origins with no credentials, path, query, or fragment, are
498
+ * skipped, and so are duplicates. The legacy `gateways` parameter is not
499
+ * read.
500
+ *
501
+ * Unlike Fedify's dereferencing, which follows at most five hints, this
502
+ * returns all of them.
503
+ *
504
+ * [FEP-ef61]: https://w3id.org/fep/ef61
505
+ *
506
+ * @param portableId The `ap:` or `ap+ef61:` URI.
507
+ * @returns The gateways, e.g., `https://server1.example/`.
508
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
509
+ * URI.
510
+ * @since 2.4.0
511
+ */
512
+ function getGatewayHints(portableId) {
513
+ const { query } = splitPortableIri(portableId);
514
+ return query == null ? [] : parseGatewayHints(query);
515
+ }
516
+ function splitPortableIri(portableId) {
517
+ const raw = getRawPortableIri(portableId);
518
+ const match = raw.match(PORTABLE_IRI_PATTERN);
519
+ const parsed = parsePortableIri(raw);
520
+ if (match == null || parsed == null) throw new TypeError("Invalid portable ActivityPub IRI.");
521
+ return {
522
+ raw,
523
+ parsed,
524
+ path: normalizePortableComponent(match[3]),
525
+ query: match[4] == null ? null : normalizePortableComponent(match[4].slice(1)),
526
+ fragment: match[5] == null ? "" : normalizePortableComponent(match[5])
527
+ };
528
+ }
529
+ function parseGatewayHints(query) {
530
+ const hints = [];
531
+ const seen = /* @__PURE__ */ new Set();
532
+ for (const hint of new URLSearchParams(`?${query}`).getAll(GATEWAY_HINT_PARAMETER)) {
533
+ const url = parseGatewayOrigin(hint);
534
+ if (url == null || seen.has(url.href)) continue;
535
+ seen.add(url.href);
536
+ hints.push(url);
537
+ }
538
+ return hints;
539
+ }
540
+ function replaceGatewayHints(parts, hints) {
541
+ const pairs = parts.query == null ? [] : parts.query.split("&").filter((pair) => pair !== "" && !isLocationHint(pair));
542
+ for (const hint of hints) pairs.push(`${GATEWAY_HINT_PARAMETER}=${encodeURIComponent(hint.origin)}`);
543
+ const query = pairs.length < 1 ? "" : `?${pairs.join("&")}`;
544
+ const result = parsePortableIri(`ap+ef61://${parts.parsed.host}${parts.path}${query}${parts.fragment}`);
545
+ if (result == null || canonicalizePortableUri(result.href) !== canonicalizePortableUri(parts.raw) || parseGatewayHints(result.search.slice(1)).map((url) => url.href).join(" ") !== hints.map((url) => url.href).join(" ")) throw new TypeError("The portable ActivityPub IRI cannot be represented as a URL without changing the object it refers to.");
546
+ return result;
547
+ }
548
+ /**
387
549
  * Validates a URL to prevent SSRF attacks.
388
550
  */
389
551
  async function validatePublicUrl(url) {
@@ -579,6 +741,12 @@ function matchesIPv6Prefix(address, prefixWords, prefixLength) {
579
741
  return true;
580
742
  }
581
743
  //#endregion
744
+ Object.defineProperty(exports, "GATEWAY_HINT_PARAMETER", {
745
+ enumerable: true,
746
+ get: function() {
747
+ return GATEWAY_HINT_PARAMETER;
748
+ }
749
+ });
582
750
  Object.defineProperty(exports, "UrlError", {
583
751
  enumerable: true,
584
752
  get: function() {
@@ -621,6 +789,12 @@ Object.defineProperty(exports, "getFe34Origin", {
621
789
  return getFe34Origin;
622
790
  }
623
791
  });
792
+ Object.defineProperty(exports, "getGatewayHints", {
793
+ enumerable: true,
794
+ get: function() {
795
+ return getGatewayHints;
796
+ }
797
+ });
624
798
  Object.defineProperty(exports, "haveSameFe34Origin", {
625
799
  enumerable: true,
626
800
  get: function() {
@@ -651,6 +825,12 @@ Object.defineProperty(exports, "isValidPublicIPv6Address", {
651
825
  return isValidPublicIPv6Address;
652
826
  }
653
827
  });
828
+ Object.defineProperty(exports, "parseGatewayOrigin", {
829
+ enumerable: true,
830
+ get: function() {
831
+ return parseGatewayOrigin;
832
+ }
833
+ });
654
834
  Object.defineProperty(exports, "parseGatewayUrl", {
655
835
  enumerable: true,
656
836
  get: function() {
@@ -681,3 +861,15 @@ Object.defineProperty(exports, "validatePublicUrl", {
681
861
  return validatePublicUrl;
682
862
  }
683
863
  });
864
+ Object.defineProperty(exports, "withGatewayHints", {
865
+ enumerable: true,
866
+ get: function() {
867
+ return withGatewayHints;
868
+ }
869
+ });
870
+ Object.defineProperty(exports, "withoutGatewayHints", {
871
+ enumerable: true,
872
+ get: function() {
873
+ return withoutGatewayHints;
874
+ }
875
+ });