@fedify/vocab-runtime 2.4.0-dev.2228 → 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.
- 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 +4 -13
- package/dist/internal/portable-dereference.js +2 -11
- package/dist/mod.cjs +5 -2
- package/dist/mod.d.cts +93 -1
- package/dist/mod.d.ts +93 -1
- package/dist/mod.js +3 -3
- package/dist/tests/{body-BmNV5g5S.mjs → body-BEYUpKvX.mjs} +1 -1
- package/dist/tests/{body-BQZL1aJ1.cjs → body-DQKTFMsH.cjs} +1 -1
- package/dist/tests/body.test.cjs +1 -1
- package/dist/tests/body.test.mjs +1 -1
- package/dist/tests/decimal.test.cjs +3 -3
- package/dist/tests/decimal.test.mjs +3 -3
- package/dist/tests/{docloader-BnuGCXLg.cjs → docloader-DHCYdBXf.cjs} +3 -3
- package/dist/tests/{docloader-ButlYQxr.mjs → docloader-ra1lbBfP.mjs} +3 -3
- package/dist/tests/docloader.test.cjs +3 -3
- package/dist/tests/docloader.test.mjs +3 -3
- package/dist/tests/internal/portable-dereference.test.cjs +15 -14
- package/dist/tests/internal/portable-dereference.test.mjs +13 -12
- package/dist/tests/{jsonld-cache-C7mkqjiE.mjs → jsonld-cache-BKrsWPCQ.mjs} +1 -1
- package/dist/tests/{jsonld-cache-CXA76Xi6.cjs → jsonld-cache-BTgizq9A.cjs} +1 -1
- package/dist/tests/jsonld-cache.test.cjs +2 -2
- package/dist/tests/jsonld-cache.test.mjs +2 -2
- package/dist/tests/{portable-media-xFDDMte7.mjs → portable-media-1NjUQari.mjs} +3 -3
- package/dist/tests/{portable-media-CFrGpz-3.cjs → portable-media-DHaAEpaQ.cjs} +3 -3
- package/dist/tests/portable-media.test.cjs +1 -1
- package/dist/tests/portable-media.test.mjs +1 -1
- package/dist/tests/{request-Cme6ShRu.cjs → request-BQlkB8du.cjs} +1 -1
- package/dist/tests/{request-2GDEEJng.mjs → request-D3elVRyB.mjs} +1 -1
- package/dist/tests/request.test.cjs +1 -1
- package/dist/tests/request.test.mjs +1 -1
- package/dist/tests/{url-R9TTZ67B.mjs → url-Cdu4ASgJ.mjs} +161 -1
- package/dist/tests/{url-B6WlNhra.cjs → url-CtHyzMR4.cjs} +190 -0
- package/dist/tests/url.test.cjs +119 -1
- package/dist/tests/url.test.mjs +119 -1
- package/dist/{url-CHAbe3hE.cjs → url-BHfQuRdy.cjs} +190 -0
- package/dist/{url-BfguNa6K.js → url-DqqDZY9i.js} +161 -1
- package/package.json +1 -1
- package/src/internal/portable-dereference.test.ts +14 -1
- package/src/internal/portable-dereference.ts +3 -14
- package/src/mod.ts +3 -0
- package/src/url.test.ts +295 -0
- package/src/url.ts +232 -0
package/dist/tests/url.test.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as
|
|
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
|
|
@@ -620,5 +620,123 @@ test("expandIPv6Address()", () => {
|
|
|
620
620
|
deepStrictEqual(expandIPv6Address("2001:db8::1"), "2001:0db8:0000:0000:0000:0000:0000:0001");
|
|
621
621
|
deepStrictEqual(expandIPv6Address("64:ff9b::8.8.8.8"), "0064:ff9b:0000:0000:0000:0000:0808:0808");
|
|
622
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
|
+
});
|
|
623
741
|
//#endregion
|
|
624
742
|
export {};
|
|
@@ -386,6 +386,166 @@ function isLocationHint(pair) {
|
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
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
|
+
/**
|
|
389
549
|
* Validates a URL to prevent SSRF attacks.
|
|
390
550
|
*/
|
|
391
551
|
async function validatePublicUrl(url) {
|
|
@@ -581,6 +741,12 @@ function matchesIPv6Prefix(address, prefixWords, prefixLength) {
|
|
|
581
741
|
return true;
|
|
582
742
|
}
|
|
583
743
|
//#endregion
|
|
744
|
+
Object.defineProperty(exports, "GATEWAY_HINT_PARAMETER", {
|
|
745
|
+
enumerable: true,
|
|
746
|
+
get: function() {
|
|
747
|
+
return GATEWAY_HINT_PARAMETER;
|
|
748
|
+
}
|
|
749
|
+
});
|
|
584
750
|
Object.defineProperty(exports, "UrlError", {
|
|
585
751
|
enumerable: true,
|
|
586
752
|
get: function() {
|
|
@@ -623,6 +789,12 @@ Object.defineProperty(exports, "getFe34Origin", {
|
|
|
623
789
|
return getFe34Origin;
|
|
624
790
|
}
|
|
625
791
|
});
|
|
792
|
+
Object.defineProperty(exports, "getGatewayHints", {
|
|
793
|
+
enumerable: true,
|
|
794
|
+
get: function() {
|
|
795
|
+
return getGatewayHints;
|
|
796
|
+
}
|
|
797
|
+
});
|
|
626
798
|
Object.defineProperty(exports, "haveSameFe34Origin", {
|
|
627
799
|
enumerable: true,
|
|
628
800
|
get: function() {
|
|
@@ -653,6 +825,12 @@ Object.defineProperty(exports, "isValidPublicIPv6Address", {
|
|
|
653
825
|
return isValidPublicIPv6Address;
|
|
654
826
|
}
|
|
655
827
|
});
|
|
828
|
+
Object.defineProperty(exports, "parseGatewayOrigin", {
|
|
829
|
+
enumerable: true,
|
|
830
|
+
get: function() {
|
|
831
|
+
return parseGatewayOrigin;
|
|
832
|
+
}
|
|
833
|
+
});
|
|
656
834
|
Object.defineProperty(exports, "parseGatewayUrl", {
|
|
657
835
|
enumerable: true,
|
|
658
836
|
get: function() {
|
|
@@ -683,3 +861,15 @@ Object.defineProperty(exports, "validatePublicUrl", {
|
|
|
683
861
|
return validatePublicUrl;
|
|
684
862
|
}
|
|
685
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
|
+
});
|
|
@@ -384,6 +384,166 @@ function isLocationHint(pair) {
|
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
386
|
/**
|
|
387
|
+
* The name of the FEP-ef61 location hint query parameter.
|
|
388
|
+
* @internal
|
|
389
|
+
*/
|
|
390
|
+
const GATEWAY_HINT_PARAMETER = "@gateway";
|
|
391
|
+
/**
|
|
392
|
+
* Parses an FEP-ef61 gateway, which has to be an HTTP(S) origin with no
|
|
393
|
+
* credentials, path, query, or fragment.
|
|
394
|
+
* @returns The gateway, or `null` if it is not a valid gateway.
|
|
395
|
+
* @internal
|
|
396
|
+
*/
|
|
397
|
+
function parseGatewayOrigin(gateway) {
|
|
398
|
+
let url;
|
|
399
|
+
if (gateway instanceof URL) url = new URL(gateway.href);
|
|
400
|
+
else if (typeof gateway === "string" && URL.canParse(gateway)) url = new URL(gateway);
|
|
401
|
+
else return null;
|
|
402
|
+
return isGatewayUrl(url) ? url : null;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Returns a copy of an [FEP-ef61] portable ActivityPub URI with `@gateway`
|
|
406
|
+
* location hints for the given gateways, which tell consumers where they can
|
|
407
|
+
* retrieve the object. Put hints on *references* to portable actors, e.g.,
|
|
408
|
+
* in `actor`, `attributedTo`, `to`, or `cc`, when constructing an object, as
|
|
409
|
+
* FEP-ef61 recommends:
|
|
410
|
+
*
|
|
411
|
+
* ~~~~ typescript
|
|
412
|
+
* withGatewayHints("ap://did:key:z6Mk.../actor", [
|
|
413
|
+
* "https://server1.example",
|
|
414
|
+
* "https://server2.example",
|
|
415
|
+
* ]);
|
|
416
|
+
* // ap+ef61://did:key:z6Mk.../actor?@gateway=https%3A%2F%2Fserver1.example&@gateway=https%3A%2F%2Fserver2.example
|
|
417
|
+
* ~~~~
|
|
418
|
+
*
|
|
419
|
+
* Do not put hints on an object's own `id`. Hints do not change the
|
|
420
|
+
* identity of a portable URI, since FEP-ef61 drops the query when comparing
|
|
421
|
+
* portable URIs, but implementations that do not canonicalize portable URIs
|
|
422
|
+
* would take a hinted ID for another object. Add hints before signing the
|
|
423
|
+
* object, since its Object Integrity Proof covers its references too.
|
|
424
|
+
*
|
|
425
|
+
* The hints that the URI already has, including the legacy `gateways`
|
|
426
|
+
* parameter, are replaced. Each gateway becomes a `@gateway` query
|
|
427
|
+
* parameter whose value is its URI-encoded origin, e.g.,
|
|
428
|
+
* `@gateway=https%3A%2F%2Fserver1.example`, in the given order after the
|
|
429
|
+
* other query parameters. Duplicate gateways are dropped, and an empty list
|
|
430
|
+
* removes the hints as {@link withoutGatewayHints} does. The other query
|
|
431
|
+
* parameters, their order, and the fragment are kept, but percent-encoding
|
|
432
|
+
* is normalized the same way as {@link canonicalizePortableUri} does it.
|
|
433
|
+
*
|
|
434
|
+
* Fedify follows at most five hints when dereferencing a portable URI
|
|
435
|
+
* (three for the key ID of an HTTP Signature), so list the preferred
|
|
436
|
+
* gateways first; there is no limit on the number of hints added here.
|
|
437
|
+
*
|
|
438
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
439
|
+
*
|
|
440
|
+
* @param portableId The `ap:` or `ap+ef61:` URI. Pass the raw string rather
|
|
441
|
+
* than a `URL` if its path may have `.` or `..` segments,
|
|
442
|
+
* because the `URL` class resolves them.
|
|
443
|
+
* @param gateways The gateways, e.g., the `gateways` of the actor that the
|
|
444
|
+
* URI refers to. Each has to be an HTTP(S) origin with no
|
|
445
|
+
* credentials, path, query, or fragment.
|
|
446
|
+
* @returns The portable URI with the hints, in the same internal `URL` form
|
|
447
|
+
* as {@link parseIri} returns. Use {@link formatIri} to get its
|
|
448
|
+
* canonical string.
|
|
449
|
+
* @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
|
|
450
|
+
* URI, e.g., it is a compatible identifier, which must
|
|
451
|
+
* not have location hints; if its path has `.` or `..`
|
|
452
|
+
* segments, which the `URL` class cannot represent; or
|
|
453
|
+
* if a gateway is invalid.
|
|
454
|
+
* @since 2.4.0
|
|
455
|
+
*/
|
|
456
|
+
function withGatewayHints(portableId, gateways) {
|
|
457
|
+
const parts = splitPortableIri(portableId);
|
|
458
|
+
if (typeof gateways === "string") throw new TypeError("The gateways must be an iterable of gateways, not a string.");
|
|
459
|
+
const hints = [];
|
|
460
|
+
const seen = /* @__PURE__ */ new Set();
|
|
461
|
+
for (const gateway of gateways) {
|
|
462
|
+
const url = parseGatewayOrigin(gateway);
|
|
463
|
+
if (url == null) throw new TypeError("FEP-ef61 gateways must be HTTP(S) origins with no credentials, path, query, or fragment: " + String(gateway));
|
|
464
|
+
if (seen.has(url.href)) continue;
|
|
465
|
+
seen.add(url.href);
|
|
466
|
+
hints.push(url);
|
|
467
|
+
}
|
|
468
|
+
return replaceGatewayHints(parts, hints);
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Returns a copy of an [FEP-ef61] portable ActivityPub URI without its
|
|
472
|
+
* location hints, i.e., `@gateway` query parameters and the legacy
|
|
473
|
+
* `gateways` parameter. The other query parameters, their order, and the
|
|
474
|
+
* fragment are kept, but percent-encoding is normalized the same way as
|
|
475
|
+
* {@link canonicalizePortableUri} does it.
|
|
476
|
+
*
|
|
477
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
478
|
+
*
|
|
479
|
+
* @param portableId The `ap:` or `ap+ef61:` URI. Pass the raw string rather
|
|
480
|
+
* than a `URL` if its path may have `.` or `..` segments,
|
|
481
|
+
* because the `URL` class resolves them.
|
|
482
|
+
* @returns The portable URI without the hints, in the same internal `URL`
|
|
483
|
+
* form as {@link parseIri} returns.
|
|
484
|
+
* @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
|
|
485
|
+
* URI, or if its path has `.` or `..` segments, which
|
|
486
|
+
* the `URL` class cannot represent.
|
|
487
|
+
* @since 2.4.0
|
|
488
|
+
*/
|
|
489
|
+
function withoutGatewayHints(portableId) {
|
|
490
|
+
return replaceGatewayHints(splitPortableIri(portableId), []);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Gets the gateways in the `@gateway` location hints of an [FEP-ef61]
|
|
494
|
+
* portable ActivityPub URI, in order. Hints that are not valid gateways,
|
|
495
|
+
* i.e., HTTP(S) origins with no credentials, path, query, or fragment, are
|
|
496
|
+
* skipped, and so are duplicates. The legacy `gateways` parameter is not
|
|
497
|
+
* read.
|
|
498
|
+
*
|
|
499
|
+
* Unlike Fedify's dereferencing, which follows at most five hints, this
|
|
500
|
+
* returns all of them.
|
|
501
|
+
*
|
|
502
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
503
|
+
*
|
|
504
|
+
* @param portableId The `ap:` or `ap+ef61:` URI.
|
|
505
|
+
* @returns The gateways, e.g., `https://server1.example/`.
|
|
506
|
+
* @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
|
|
507
|
+
* URI.
|
|
508
|
+
* @since 2.4.0
|
|
509
|
+
*/
|
|
510
|
+
function getGatewayHints(portableId) {
|
|
511
|
+
const { query } = splitPortableIri(portableId);
|
|
512
|
+
return query == null ? [] : parseGatewayHints(query);
|
|
513
|
+
}
|
|
514
|
+
function splitPortableIri(portableId) {
|
|
515
|
+
const raw = getRawPortableIri(portableId);
|
|
516
|
+
const match = raw.match(PORTABLE_IRI_PATTERN);
|
|
517
|
+
const parsed = parsePortableIri(raw);
|
|
518
|
+
if (match == null || parsed == null) throw new TypeError("Invalid portable ActivityPub IRI.");
|
|
519
|
+
return {
|
|
520
|
+
raw,
|
|
521
|
+
parsed,
|
|
522
|
+
path: normalizePortableComponent(match[3]),
|
|
523
|
+
query: match[4] == null ? null : normalizePortableComponent(match[4].slice(1)),
|
|
524
|
+
fragment: match[5] == null ? "" : normalizePortableComponent(match[5])
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function parseGatewayHints(query) {
|
|
528
|
+
const hints = [];
|
|
529
|
+
const seen = /* @__PURE__ */ new Set();
|
|
530
|
+
for (const hint of new URLSearchParams(`?${query}`).getAll(GATEWAY_HINT_PARAMETER)) {
|
|
531
|
+
const url = parseGatewayOrigin(hint);
|
|
532
|
+
if (url == null || seen.has(url.href)) continue;
|
|
533
|
+
seen.add(url.href);
|
|
534
|
+
hints.push(url);
|
|
535
|
+
}
|
|
536
|
+
return hints;
|
|
537
|
+
}
|
|
538
|
+
function replaceGatewayHints(parts, hints) {
|
|
539
|
+
const pairs = parts.query == null ? [] : parts.query.split("&").filter((pair) => pair !== "" && !isLocationHint(pair));
|
|
540
|
+
for (const hint of hints) pairs.push(`${GATEWAY_HINT_PARAMETER}=${encodeURIComponent(hint.origin)}`);
|
|
541
|
+
const query = pairs.length < 1 ? "" : `?${pairs.join("&")}`;
|
|
542
|
+
const result = parsePortableIri(`ap+ef61://${parts.parsed.host}${parts.path}${query}${parts.fragment}`);
|
|
543
|
+
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.");
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
387
547
|
* Validates a URL to prevent SSRF attacks.
|
|
388
548
|
*/
|
|
389
549
|
async function validatePublicUrl(url) {
|
|
@@ -579,4 +739,4 @@ function matchesIPv6Prefix(address, prefixWords, prefixLength) {
|
|
|
579
739
|
return true;
|
|
580
740
|
}
|
|
581
741
|
//#endregion
|
|
582
|
-
export {
|
|
742
|
+
export { withoutGatewayHints as S, parseIri as _, expandIPv6Address as a, validatePublicUrl as b, getFe34Origin as c, haveSameIriOrigin as d, isGatewayUrl as f, parseGatewayUrl as g, parseGatewayOrigin as h, canonicalizePortableUri as i, getGatewayHints as l, isValidPublicIPv6Address as m, UrlError as n, formatIri as o, isValidPublicIPv4Address as p, arePortableUrisEqual as r, fromCompatibleEf61Id as s, GATEWAY_HINT_PARAMETER as t, haveSameFe34Origin as u, parseJsonLdId as v, withGatewayHints as x, toCompatibleEf61Id as y };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { deepStrictEqual, ok, throws } from "node:assert/strict";
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
import type { DocumentLoader } from "../docloader.ts";
|
|
4
|
-
import { parseIri } from "../url.ts";
|
|
4
|
+
import { parseIri, withGatewayHints } from "../url.ts";
|
|
5
5
|
import { createScopedContextLoader } from "./jsonld-cache.ts";
|
|
6
6
|
import {
|
|
7
7
|
createSnapshotContextLoader,
|
|
@@ -78,6 +78,19 @@ test("getPortableGatewayCandidates() reads @gateway hints", () => {
|
|
|
78
78
|
);
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
test("getPortableGatewayCandidates() reads hints from withGatewayHints()", () => {
|
|
82
|
+
const gateways = Array.from(
|
|
83
|
+
{ length: 7 },
|
|
84
|
+
(_, i) => `https://g${i}.example`,
|
|
85
|
+
);
|
|
86
|
+
deepStrictEqual(
|
|
87
|
+
hrefs(getPortableGatewayCandidates(
|
|
88
|
+
withGatewayHints("ap://did:key:z6Mkabc/actor?page=1", gateways),
|
|
89
|
+
)),
|
|
90
|
+
[0, 1, 2, 3, 4].map((i) => `https://g${i}.example/`),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
81
94
|
test("createSnapshotContextLoader() does not nest released snapshots", async () => {
|
|
82
95
|
const calls: string[] = [];
|
|
83
96
|
const base: DocumentLoader = (url) => {
|
|
@@ -21,8 +21,9 @@ import {
|
|
|
21
21
|
canonicalizePortableUri,
|
|
22
22
|
formatIri,
|
|
23
23
|
fromCompatibleEf61Id,
|
|
24
|
+
GATEWAY_HINT_PARAMETER,
|
|
24
25
|
haveSameFe34Origin,
|
|
25
|
-
|
|
26
|
+
parseGatewayOrigin,
|
|
26
27
|
parseIri,
|
|
27
28
|
toCompatibleEf61Id,
|
|
28
29
|
} from "../url.ts";
|
|
@@ -36,8 +37,6 @@ const logger = getLogger(["fedify", "vocab", "gateway"]);
|
|
|
36
37
|
*/
|
|
37
38
|
const MAX_GATEWAY_HINTS = 5;
|
|
38
39
|
|
|
39
|
-
const LOCATION_HINT_PARAMETER = "@gateway";
|
|
40
|
-
|
|
41
40
|
// The same baseline contexts that Fedify's proof verifier always resolves
|
|
42
41
|
// from its built-in copies (see getNormalizationContextLoader() in
|
|
43
42
|
// @fedify/fedify). Serving them identically here keeps the identity check,
|
|
@@ -525,7 +524,7 @@ export function getPortableGatewayCandidates(
|
|
|
525
524
|
}
|
|
526
525
|
for (
|
|
527
526
|
const hint of new URLSearchParams(url.search).getAll(
|
|
528
|
-
|
|
527
|
+
GATEWAY_HINT_PARAMETER,
|
|
529
528
|
)
|
|
530
529
|
) {
|
|
531
530
|
if (candidates.length >= MAX_GATEWAY_HINTS) break;
|
|
@@ -542,16 +541,6 @@ export function getPortableGatewayCandidates(
|
|
|
542
541
|
return candidates;
|
|
543
542
|
}
|
|
544
543
|
|
|
545
|
-
function parseGatewayOrigin(gateway: string | URL): URL | null {
|
|
546
|
-
let url: URL;
|
|
547
|
-
if (gateway instanceof URL) url = new URL(gateway.href);
|
|
548
|
-
else if (typeof gateway === "string" && URL.canParse(gateway)) {
|
|
549
|
-
url = new URL(gateway);
|
|
550
|
-
} else return null;
|
|
551
|
-
if (!isGatewayUrl(url)) return null;
|
|
552
|
-
return url;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
544
|
/**
|
|
556
545
|
* Creates a context loader that returns the same context documents for the
|
|
557
546
|
* whole dereference operation, so that the identity check, the proof
|
package/src/mod.ts
CHANGED
|
@@ -83,6 +83,7 @@ export {
|
|
|
83
83
|
formatIri,
|
|
84
84
|
fromCompatibleEf61Id,
|
|
85
85
|
getFe34Origin,
|
|
86
|
+
getGatewayHints,
|
|
86
87
|
haveSameFe34Origin,
|
|
87
88
|
haveSameIriOrigin,
|
|
88
89
|
isGatewayUrl,
|
|
@@ -94,4 +95,6 @@ export {
|
|
|
94
95
|
toCompatibleEf61Id,
|
|
95
96
|
UrlError,
|
|
96
97
|
validatePublicUrl,
|
|
98
|
+
withGatewayHints,
|
|
99
|
+
withoutGatewayHints,
|
|
97
100
|
} from "./url.ts";
|