@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.
- 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 +95 -1
- package/dist/mod.d.ts +95 -1
- package/dist/mod.js +3 -3
- package/dist/tests/{body-BzhhuMI0.mjs → body-BEYUpKvX.mjs} +1 -1
- package/dist/tests/{body-GaHFfkaq.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-sGr7kARm.cjs → docloader-DHCYdBXf.cjs} +3 -3
- package/dist/tests/{docloader-DiRcdiPp.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 +16 -15
- package/dist/tests/internal/portable-dereference.test.mjs +14 -13
- package/dist/tests/{jsonld-cache-DUmuWRwp.mjs → jsonld-cache-BKrsWPCQ.mjs} +1 -1
- package/dist/tests/{jsonld-cache-BQMn_ILF.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-BERS_tX_.mjs → portable-media-1NjUQari.mjs} +3 -3
- package/dist/tests/{portable-media-C_Q9GmqU.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-qq1N2cl6.cjs → request-BQlkB8du.cjs} +1 -1
- package/dist/tests/{request-CWTG9_Xx.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-DfS-cvwr.mjs → url-Cdu4ASgJ.mjs} +165 -3
- package/dist/tests/{url-B8xyTwP-.cjs → url-CtHyzMR4.cjs} +194 -2
- package/dist/tests/url.test.cjs +133 -3
- package/dist/tests/url.test.mjs +133 -3
- package/dist/{url-DDVweCXr.cjs → url-BHfQuRdy.cjs} +194 -2
- package/dist/{url-Be3moMor.js → url-DqqDZY9i.js} +165 -3
- package/package.json +1 -1
- package/src/internal/portable-dereference.test.ts +16 -1
- package/src/internal/portable-dereference.ts +3 -20
- package/src/mod.ts +3 -0
- package/src/url.test.ts +311 -1
- package/src/url.ts +236 -8
package/src/url.ts
CHANGED
|
@@ -339,11 +339,11 @@ function parseAtUri(uri: string): URL {
|
|
|
339
339
|
|
|
340
340
|
/**
|
|
341
341
|
* Checks whether the URL is an FEP-ef61 gateway base URI.
|
|
342
|
+
* @since 2.4.0
|
|
342
343
|
*/
|
|
343
344
|
export function isGatewayUrl(url: URL): boolean {
|
|
344
345
|
return (url.protocol === "http:" || url.protocol === "https:") &&
|
|
345
|
-
url.
|
|
346
|
-
url.pathname === "/" && url.search === "" && url.hash === "";
|
|
346
|
+
url.href === `${url.origin}/`;
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
/**
|
|
@@ -352,6 +352,7 @@ export function isGatewayUrl(url: URL): boolean {
|
|
|
352
352
|
* with no credentials, path, query, or fragment. In the
|
|
353
353
|
* latter case, the message starts with
|
|
354
354
|
* `Invalid FEP-ef61 gateway:`.
|
|
355
|
+
* @since 2.4.0
|
|
355
356
|
*/
|
|
356
357
|
export function parseGatewayUrl(url: string): URL {
|
|
357
358
|
const parsed = parseIri(url);
|
|
@@ -550,12 +551,7 @@ function parseCompatibleEf61Gateway(gateway: string | URL): URL {
|
|
|
550
551
|
: typeof gateway === "string" && URL.canParse(gateway)
|
|
551
552
|
? new URL(gateway)
|
|
552
553
|
: null;
|
|
553
|
-
|
|
554
|
-
// query and fragment components, including empty ? and # delimiters.
|
|
555
|
-
if (
|
|
556
|
-
url == null || (url.protocol !== "http:" && url.protocol !== "https:") ||
|
|
557
|
-
url.href !== `${url.origin}/`
|
|
558
|
-
) {
|
|
554
|
+
if (url == null || !isGatewayUrl(url)) {
|
|
559
555
|
throw new TypeError(
|
|
560
556
|
"FEP-ef61 gateways for compatible identifiers must be HTTP(S) origins " +
|
|
561
557
|
"with no credentials, path, query, or fragment.",
|
|
@@ -599,6 +595,238 @@ function isLocationHint(pair: string): boolean {
|
|
|
599
595
|
}
|
|
600
596
|
}
|
|
601
597
|
|
|
598
|
+
/**
|
|
599
|
+
* The name of the FEP-ef61 location hint query parameter.
|
|
600
|
+
* @internal
|
|
601
|
+
*/
|
|
602
|
+
export const GATEWAY_HINT_PARAMETER = "@gateway";
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Parses an FEP-ef61 gateway, which has to be an HTTP(S) origin with no
|
|
606
|
+
* credentials, path, query, or fragment.
|
|
607
|
+
* @returns The gateway, or `null` if it is not a valid gateway.
|
|
608
|
+
* @internal
|
|
609
|
+
*/
|
|
610
|
+
export function parseGatewayOrigin(gateway: string | URL): URL | null {
|
|
611
|
+
let url: URL;
|
|
612
|
+
if (gateway instanceof URL) url = new URL(gateway.href);
|
|
613
|
+
else if (typeof gateway === "string" && URL.canParse(gateway)) {
|
|
614
|
+
url = new URL(gateway);
|
|
615
|
+
} else return null;
|
|
616
|
+
return isGatewayUrl(url) ? url : null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Returns a copy of an [FEP-ef61] portable ActivityPub URI with `@gateway`
|
|
621
|
+
* location hints for the given gateways, which tell consumers where they can
|
|
622
|
+
* retrieve the object. Put hints on *references* to portable actors, e.g.,
|
|
623
|
+
* in `actor`, `attributedTo`, `to`, or `cc`, when constructing an object, as
|
|
624
|
+
* FEP-ef61 recommends:
|
|
625
|
+
*
|
|
626
|
+
* ~~~~ typescript
|
|
627
|
+
* withGatewayHints("ap://did:key:z6Mk.../actor", [
|
|
628
|
+
* "https://server1.example",
|
|
629
|
+
* "https://server2.example",
|
|
630
|
+
* ]);
|
|
631
|
+
* // ap+ef61://did:key:z6Mk.../actor?@gateway=https%3A%2F%2Fserver1.example&@gateway=https%3A%2F%2Fserver2.example
|
|
632
|
+
* ~~~~
|
|
633
|
+
*
|
|
634
|
+
* Do not put hints on an object's own `id`. Hints do not change the
|
|
635
|
+
* identity of a portable URI, since FEP-ef61 drops the query when comparing
|
|
636
|
+
* portable URIs, but implementations that do not canonicalize portable URIs
|
|
637
|
+
* would take a hinted ID for another object. Add hints before signing the
|
|
638
|
+
* object, since its Object Integrity Proof covers its references too.
|
|
639
|
+
*
|
|
640
|
+
* The hints that the URI already has, including the legacy `gateways`
|
|
641
|
+
* parameter, are replaced. Each gateway becomes a `@gateway` query
|
|
642
|
+
* parameter whose value is its URI-encoded origin, e.g.,
|
|
643
|
+
* `@gateway=https%3A%2F%2Fserver1.example`, in the given order after the
|
|
644
|
+
* other query parameters. Duplicate gateways are dropped, and an empty list
|
|
645
|
+
* removes the hints as {@link withoutGatewayHints} does. The other query
|
|
646
|
+
* parameters, their order, and the fragment are kept, but percent-encoding
|
|
647
|
+
* is normalized the same way as {@link canonicalizePortableUri} does it.
|
|
648
|
+
*
|
|
649
|
+
* Fedify follows at most five hints when dereferencing a portable URI
|
|
650
|
+
* (three for the key ID of an HTTP Signature), so list the preferred
|
|
651
|
+
* gateways first; there is no limit on the number of hints added here.
|
|
652
|
+
*
|
|
653
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
654
|
+
*
|
|
655
|
+
* @param portableId The `ap:` or `ap+ef61:` URI. Pass the raw string rather
|
|
656
|
+
* than a `URL` if its path may have `.` or `..` segments,
|
|
657
|
+
* because the `URL` class resolves them.
|
|
658
|
+
* @param gateways The gateways, e.g., the `gateways` of the actor that the
|
|
659
|
+
* URI refers to. Each has to be an HTTP(S) origin with no
|
|
660
|
+
* credentials, path, query, or fragment.
|
|
661
|
+
* @returns The portable URI with the hints, in the same internal `URL` form
|
|
662
|
+
* as {@link parseIri} returns. Use {@link formatIri} to get its
|
|
663
|
+
* canonical string.
|
|
664
|
+
* @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
|
|
665
|
+
* URI, e.g., it is a compatible identifier, which must
|
|
666
|
+
* not have location hints; if its path has `.` or `..`
|
|
667
|
+
* segments, which the `URL` class cannot represent; or
|
|
668
|
+
* if a gateway is invalid.
|
|
669
|
+
* @since 2.4.0
|
|
670
|
+
*/
|
|
671
|
+
export function withGatewayHints(
|
|
672
|
+
portableId: string | URL,
|
|
673
|
+
gateways: Iterable<string | URL>,
|
|
674
|
+
): URL {
|
|
675
|
+
const parts = splitPortableIri(portableId);
|
|
676
|
+
if (typeof gateways === "string") {
|
|
677
|
+
throw new TypeError(
|
|
678
|
+
"The gateways must be an iterable of gateways, not a string.",
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
const hints: URL[] = [];
|
|
682
|
+
const seen = new Set<string>();
|
|
683
|
+
for (const gateway of gateways) {
|
|
684
|
+
const url = parseGatewayOrigin(gateway);
|
|
685
|
+
if (url == null) {
|
|
686
|
+
throw new TypeError(
|
|
687
|
+
"FEP-ef61 gateways must be HTTP(S) origins with no credentials, " +
|
|
688
|
+
"path, query, or fragment: " + String(gateway),
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
if (seen.has(url.href)) continue;
|
|
692
|
+
seen.add(url.href);
|
|
693
|
+
hints.push(url);
|
|
694
|
+
}
|
|
695
|
+
return replaceGatewayHints(parts, hints);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Returns a copy of an [FEP-ef61] portable ActivityPub URI without its
|
|
700
|
+
* location hints, i.e., `@gateway` query parameters and the legacy
|
|
701
|
+
* `gateways` parameter. The other query parameters, their order, and the
|
|
702
|
+
* fragment are kept, but percent-encoding is normalized the same way as
|
|
703
|
+
* {@link canonicalizePortableUri} does it.
|
|
704
|
+
*
|
|
705
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
706
|
+
*
|
|
707
|
+
* @param portableId The `ap:` or `ap+ef61:` URI. Pass the raw string rather
|
|
708
|
+
* than a `URL` if its path may have `.` or `..` segments,
|
|
709
|
+
* because the `URL` class resolves them.
|
|
710
|
+
* @returns The portable URI without the hints, in the same internal `URL`
|
|
711
|
+
* form as {@link parseIri} returns.
|
|
712
|
+
* @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
|
|
713
|
+
* URI, or if its path has `.` or `..` segments, which
|
|
714
|
+
* the `URL` class cannot represent.
|
|
715
|
+
* @since 2.4.0
|
|
716
|
+
*/
|
|
717
|
+
export function withoutGatewayHints(portableId: string | URL): URL {
|
|
718
|
+
return replaceGatewayHints(splitPortableIri(portableId), []);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Gets the gateways in the `@gateway` location hints of an [FEP-ef61]
|
|
723
|
+
* portable ActivityPub URI, in order. Hints that are not valid gateways,
|
|
724
|
+
* i.e., HTTP(S) origins with no credentials, path, query, or fragment, are
|
|
725
|
+
* skipped, and so are duplicates. The legacy `gateways` parameter is not
|
|
726
|
+
* read.
|
|
727
|
+
*
|
|
728
|
+
* Unlike Fedify's dereferencing, which follows at most five hints, this
|
|
729
|
+
* returns all of them.
|
|
730
|
+
*
|
|
731
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
732
|
+
*
|
|
733
|
+
* @param portableId The `ap:` or `ap+ef61:` URI.
|
|
734
|
+
* @returns The gateways, e.g., `https://server1.example/`.
|
|
735
|
+
* @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
|
|
736
|
+
* URI.
|
|
737
|
+
* @since 2.4.0
|
|
738
|
+
*/
|
|
739
|
+
export function getGatewayHints(portableId: string | URL): URL[] {
|
|
740
|
+
const { query } = splitPortableIri(portableId);
|
|
741
|
+
return query == null ? [] : parseGatewayHints(query);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
interface PortableIriParts {
|
|
745
|
+
/** The portable ID as it was given, or the `href` of a `URL`. */
|
|
746
|
+
readonly raw: string;
|
|
747
|
+
/** The parsed portable ID. */
|
|
748
|
+
readonly parsed: URL;
|
|
749
|
+
/** The normalized path. */
|
|
750
|
+
readonly path: string;
|
|
751
|
+
/** The normalized query without `?`, or `null` if there is none. */
|
|
752
|
+
readonly query: string | null;
|
|
753
|
+
/** The normalized fragment with `#`, or an empty string if there is none. */
|
|
754
|
+
readonly fragment: string;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function splitPortableIri(portableId: string | URL): PortableIriParts {
|
|
758
|
+
const raw = getRawPortableIri(portableId);
|
|
759
|
+
const match = raw.match(PORTABLE_IRI_PATTERN);
|
|
760
|
+
const parsed = parsePortableIri(raw);
|
|
761
|
+
if (match == null || parsed == null) {
|
|
762
|
+
throw new TypeError("Invalid portable ActivityPub IRI.");
|
|
763
|
+
}
|
|
764
|
+
// Normalize the components before looking at them, as the URL parser would
|
|
765
|
+
// otherwise strip characters such as tabs later, which could turn
|
|
766
|
+
// an unrelated query parameter into a location hint:
|
|
767
|
+
return {
|
|
768
|
+
raw,
|
|
769
|
+
parsed,
|
|
770
|
+
path: normalizePortableComponent(match[3]),
|
|
771
|
+
query: match[4] == null
|
|
772
|
+
? null
|
|
773
|
+
: normalizePortableComponent(match[4].slice(1)),
|
|
774
|
+
fragment: match[5] == null ? "" : normalizePortableComponent(match[5]),
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function parseGatewayHints(query: string): URL[] {
|
|
779
|
+
const hints: URL[] = [];
|
|
780
|
+
const seen = new Set<string>();
|
|
781
|
+
// Keep the delimiter, as URLSearchParams would otherwise strip a leading
|
|
782
|
+
// question mark that is part of the first parameter's name:
|
|
783
|
+
for (
|
|
784
|
+
const hint of new URLSearchParams(`?${query}`).getAll(
|
|
785
|
+
GATEWAY_HINT_PARAMETER,
|
|
786
|
+
)
|
|
787
|
+
) {
|
|
788
|
+
const url = parseGatewayOrigin(hint);
|
|
789
|
+
if (url == null || seen.has(url.href)) continue;
|
|
790
|
+
seen.add(url.href);
|
|
791
|
+
hints.push(url);
|
|
792
|
+
}
|
|
793
|
+
return hints;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function replaceGatewayHints(
|
|
797
|
+
parts: PortableIriParts,
|
|
798
|
+
hints: readonly URL[],
|
|
799
|
+
): URL {
|
|
800
|
+
const pairs = parts.query == null
|
|
801
|
+
? []
|
|
802
|
+
: parts.query.split("&").filter((pair) =>
|
|
803
|
+
pair !== "" && !isLocationHint(pair)
|
|
804
|
+
);
|
|
805
|
+
for (const hint of hints) {
|
|
806
|
+
pairs.push(`${GATEWAY_HINT_PARAMETER}=${encodeURIComponent(hint.origin)}`);
|
|
807
|
+
}
|
|
808
|
+
const query = pairs.length < 1 ? "" : `?${pairs.join("&")}`;
|
|
809
|
+
const result = parsePortableIri(
|
|
810
|
+
`ap+ef61://${parts.parsed.host}${parts.path}${query}${parts.fragment}`,
|
|
811
|
+
);
|
|
812
|
+
// Guard against URL parser normalization that would silently change
|
|
813
|
+
// the referenced object (e.g., dot segments) or its hints:
|
|
814
|
+
if (
|
|
815
|
+
result == null ||
|
|
816
|
+
canonicalizePortableUri(result.href) !==
|
|
817
|
+
canonicalizePortableUri(parts.raw) ||
|
|
818
|
+
parseGatewayHints(result.search.slice(1)).map((url) => url.href).join(
|
|
819
|
+
" ",
|
|
820
|
+
) !== hints.map((url) => url.href).join(" ")
|
|
821
|
+
) {
|
|
822
|
+
throw new TypeError(
|
|
823
|
+
"The portable ActivityPub IRI cannot be represented as a URL without " +
|
|
824
|
+
"changing the object it refers to.",
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
return result;
|
|
828
|
+
}
|
|
829
|
+
|
|
602
830
|
/**
|
|
603
831
|
* Validates a URL to prevent SSRF attacks.
|
|
604
832
|
*/
|