@fedify/vocab-runtime 2.4.0-dev.2233 → 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.
- 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 +3 -6
- package/dist/internal/portable-dereference.js +3 -6
- package/dist/mod.cjs +2 -2
- package/dist/mod.d.cts +14 -3
- package/dist/mod.d.ts +14 -3
- package/dist/mod.js +2 -2
- package/dist/tests/{body-BEYUpKvX.mjs → body-C00dWiYW.mjs} +1 -1
- package/dist/tests/{body-DQKTFMsH.cjs → body-DHk-vT0I.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-ra1lbBfP.mjs → docloader-Bi982uOa.mjs} +3 -3
- package/dist/tests/{docloader-DHCYdBXf.cjs → docloader-gX-8knd3.cjs} +3 -3
- package/dist/tests/docloader.test.cjs +59 -65
- package/dist/tests/docloader.test.mjs +38 -44
- package/dist/tests/internal/portable-dereference.test.cjs +21 -149
- package/dist/tests/internal/portable-dereference.test.mjs +3 -131
- package/dist/tests/{jsonld-cache-BKrsWPCQ.mjs → jsonld-cache-CKfgtxPB.mjs} +1 -1
- package/dist/tests/{jsonld-cache-BTgizq9A.cjs → jsonld-cache-DzxT9IXH.cjs} +1 -1
- package/dist/tests/jsonld-cache.test.cjs +2 -2
- package/dist/tests/jsonld-cache.test.mjs +2 -2
- package/dist/tests/multibase/multibase.test.cjs +7 -7
- package/dist/tests/multibase/multibase.test.mjs +5 -5
- package/dist/tests/portable-dereference-C_cwGjz6.mjs +132 -0
- package/dist/tests/portable-dereference-CyFYTBnr.cjs +149 -0
- package/dist/tests/{portable-media-DHaAEpaQ.cjs → portable-media-B39cvJGK.cjs} +3 -3
- package/dist/tests/{portable-media-1NjUQari.mjs → portable-media-l0ET9Pq8.mjs} +3 -3
- package/dist/tests/portable-media.test.cjs +1 -1
- package/dist/tests/portable-media.test.mjs +1 -1
- package/dist/tests/portable-workers.test.cjs +36 -0
- package/dist/tests/portable-workers.test.d.cts +2 -0
- package/dist/tests/portable-workers.test.d.mts +2 -0
- package/dist/tests/portable-workers.test.mjs +35 -0
- package/dist/tests/{request-D3elVRyB.mjs → request-BRPiDcmG.mjs} +1 -1
- package/dist/tests/{request-BQlkB8du.cjs → request-fo2l2yJS.cjs} +1 -1
- package/dist/tests/request.test.cjs +1 -1
- package/dist/tests/request.test.mjs +1 -1
- package/dist/tests/{url-Cdu4ASgJ.mjs → url-CWC-NI3a.mjs} +63 -7
- package/dist/tests/{url-CtHyzMR4.cjs → url-DsMBGqCc.cjs} +63 -7
- package/dist/tests/url.test.cjs +74 -1
- package/dist/tests/url.test.mjs +74 -1
- package/dist/{url-BHfQuRdy.cjs → url-BJMXv2kE.cjs} +63 -7
- package/dist/{url-DqqDZY9i.js → url-CZm2-7ZO.js} +63 -7
- package/package.json +6 -3
- package/scripts/test-bun.mjs +17 -0
- package/src/docloader.test.ts +82 -74
- package/src/internal/portable-dereference.ts +2 -5
- package/src/multibase/multibase.test.ts +5 -5
- package/src/portable-workers.test.ts +81 -0
- package/src/url.test.ts +90 -0
- package/src/url.ts +115 -13
package/src/url.ts
CHANGED
|
@@ -29,6 +29,56 @@ const INVALID_PERCENT_ENCODING_PATTERN = /%(?![0-9A-Fa-f]{2})/;
|
|
|
29
29
|
const PERCENT_ENCODING_PATTERN = /%[0-9A-Fa-f]{2}/g;
|
|
30
30
|
const DID_SCHEME_PATTERN = /^did:/i;
|
|
31
31
|
const DID_PATTERN = /^did:[a-z0-9]+:[-A-Za-z0-9._%]+(?::[-A-Za-z0-9._%]+)*$/i;
|
|
32
|
+
const DOT_SEGMENT_PATTERN = /(?:^|\/)(?:\.|%2e){1,2}(?=\/|$)/i;
|
|
33
|
+
|
|
34
|
+
function prepareUrlInput(iri: string): string {
|
|
35
|
+
// WHATWG URL removes these characters before it recognizes dot segments.
|
|
36
|
+
// Check the same spelling it will parse, while retaining the raw path for
|
|
37
|
+
// the comparison-only canonicalizer below.
|
|
38
|
+
let prepared = iri.replace(/[\t\n\r]/g, "");
|
|
39
|
+
while (prepared.length > 0 && prepared.charCodeAt(0) <= 0x20) {
|
|
40
|
+
prepared = prepared.slice(1);
|
|
41
|
+
}
|
|
42
|
+
while (
|
|
43
|
+
prepared.length > 0 && prepared.charCodeAt(prepared.length - 1) <= 0x20
|
|
44
|
+
) {
|
|
45
|
+
prepared = prepared.slice(0, -1);
|
|
46
|
+
}
|
|
47
|
+
return prepared;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function assertPortablePathCanBeParsed(iri: string): void {
|
|
51
|
+
const match = prepareUrlInput(iri).match(PORTABLE_IRI_PATTERN);
|
|
52
|
+
if (match != null && DOT_SEGMENT_PATTERN.test(match[3])) {
|
|
53
|
+
throw new TypeError(
|
|
54
|
+
"Portable ActivityPub IRI paths with dot segments cannot be represented as URLs.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function assertCompatiblePathCanBeParsed(raw: string, parsed: URL): void {
|
|
60
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
|
|
61
|
+
const prepared = prepareUrlInput(raw);
|
|
62
|
+
const match = prepared.match(/^https?:[\/\\]*[^/?#\\]*([^?#]*)/i);
|
|
63
|
+
if (match == null) return;
|
|
64
|
+
const rawPath = match[1].replace(/\\/g, "/");
|
|
65
|
+
const compatiblePath =
|
|
66
|
+
parsed.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX) &&
|
|
67
|
+
COMPATIBLE_ID_DID_PATTERN.test(
|
|
68
|
+
parsed.pathname.slice(COMPATIBLE_ID_PATH_PREFIX.length),
|
|
69
|
+
);
|
|
70
|
+
const rawCompatiblePath = rawPath.startsWith(COMPATIBLE_ID_PATH_PREFIX) &&
|
|
71
|
+
COMPATIBLE_ID_DID_PATTERN.test(
|
|
72
|
+
rawPath.slice(COMPATIBLE_ID_PATH_PREFIX.length),
|
|
73
|
+
);
|
|
74
|
+
if (
|
|
75
|
+
(compatiblePath || rawCompatiblePath) && DOT_SEGMENT_PATTERN.test(rawPath)
|
|
76
|
+
) {
|
|
77
|
+
throw new TypeError(
|
|
78
|
+
"FEP-ef61 compatible identifier paths with dot segments cannot be represented as URLs.",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
32
82
|
|
|
33
83
|
/**
|
|
34
84
|
* Parses a JSON-LD `@id` value as an IRI.
|
|
@@ -47,11 +97,18 @@ export function parseJsonLdId(
|
|
|
47
97
|
|
|
48
98
|
/**
|
|
49
99
|
* Parses an IRI as a URL, including FEP-ef61 portable ActivityPub IRIs.
|
|
100
|
+
* Portable URI and FEP-ef61 compatible identifier strings whose path contains
|
|
101
|
+
* a `.` or `..` segment, including percent-encoded spellings, throw a
|
|
102
|
+
* `TypeError`: JavaScript `URL` would otherwise identify a different object.
|
|
103
|
+
* This also applies to compatible identifier strings used as relative bases.
|
|
104
|
+
* A `URL` argument may already have lost such segments before this function
|
|
105
|
+
* receives it.
|
|
50
106
|
*/
|
|
51
107
|
export function parseIri(iri: string | URL, base?: string | URL): URL {
|
|
52
108
|
if (iri instanceof URL) {
|
|
53
109
|
return normalizePortableUrl(iri) ?? new URL(iri.href);
|
|
54
110
|
}
|
|
111
|
+
assertPortablePathCanBeParsed(iri);
|
|
55
112
|
const portable = parsePortableIri(iri);
|
|
56
113
|
if (portable != null) return portable;
|
|
57
114
|
base = normalizeBaseIri(base);
|
|
@@ -59,20 +116,25 @@ export function parseIri(iri: string | URL, base?: string | URL): URL {
|
|
|
59
116
|
return parseAtUri(iri);
|
|
60
117
|
}
|
|
61
118
|
const parsed = new URL(iri, base);
|
|
119
|
+
assertCompatiblePathCanBeParsed(iri, parsed);
|
|
62
120
|
return normalizePortableUrl(parsed) ?? parsed;
|
|
63
121
|
}
|
|
64
122
|
|
|
65
123
|
/**
|
|
66
124
|
* Formats a URL as an IRI, including FEP-ef61 portable ActivityPub IRIs.
|
|
125
|
+
* Portable URI and FEP-ef61 compatible identifier strings with dot segments
|
|
126
|
+
* throw a `TypeError` because their paths cannot be represented by JavaScript
|
|
127
|
+
* `URL` without normalization.
|
|
67
128
|
*/
|
|
68
129
|
export function formatIri(iri: string | URL): string {
|
|
130
|
+
if (typeof iri === "string") assertPortablePathCanBeParsed(iri);
|
|
69
131
|
const parsed = parsePortableIri(iri instanceof URL ? iri.href : iri);
|
|
70
132
|
if (parsed == null) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
133
|
+
if (iri instanceof URL) return iri.href;
|
|
134
|
+
if (!URL.canParse(iri)) return iri;
|
|
135
|
+
const url = new URL(iri);
|
|
136
|
+
assertCompatiblePathCanBeParsed(iri, url);
|
|
137
|
+
return url.href;
|
|
76
138
|
}
|
|
77
139
|
const authority = decodePortableAuthority(parsed.host);
|
|
78
140
|
return `ap+ef61://${authority}${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
@@ -252,14 +314,18 @@ function parsePortableIri(iri: string): URL | null {
|
|
|
252
314
|
|
|
253
315
|
function normalizePortableUrl(iri: URL): URL | null {
|
|
254
316
|
if (iri.protocol !== "ap:" && iri.protocol !== "ap+ef61:") return null;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
);
|
|
317
|
+
const raw = `ap+ef61://${iri.host}${iri.pathname}${iri.search}${iri.hash}`;
|
|
318
|
+
assertPortablePathCanBeParsed(raw);
|
|
319
|
+
return parsePortableIri(raw);
|
|
258
320
|
}
|
|
259
321
|
|
|
260
322
|
function normalizeBaseIri(base?: string | URL): string | URL | undefined {
|
|
261
323
|
if (base == null) return undefined;
|
|
262
324
|
if (base instanceof URL) return normalizePortableUrl(base) ?? base;
|
|
325
|
+
assertPortablePathCanBeParsed(base);
|
|
326
|
+
if (URL.canParse(base)) {
|
|
327
|
+
assertCompatiblePathCanBeParsed(base, new URL(base));
|
|
328
|
+
}
|
|
263
329
|
return parsePortableIri(base) ??
|
|
264
330
|
(base.startsWith("at://") && !URL.canParse(".", base)
|
|
265
331
|
? parseAtUri(base)
|
|
@@ -367,6 +433,8 @@ export function parseGatewayUrl(url: string): URL {
|
|
|
367
433
|
|
|
368
434
|
const COMPATIBLE_ID_PATH_PREFIX = "/.well-known/apgateway/";
|
|
369
435
|
const COMPATIBLE_ID_DID_PATTERN = /^did(?::|%3A)/i;
|
|
436
|
+
const RAW_COMPATIBLE_ID_PREFIX_PATTERN =
|
|
437
|
+
/^(?:[hH][tT][tT][pP][sS]?):\/\/[^/?#]*\/\.well-known\/apgateway\/(?=[dD][iI][dD](?::|%3[aA]))/;
|
|
370
438
|
// `gateways` is the location hint parameter name used by earlier FEP-ef61
|
|
371
439
|
// revisions; strip it as well for compatibility with older publishers.
|
|
372
440
|
const LOCATION_HINT_PARAMETERS: ReadonlySet<string> = new Set([
|
|
@@ -408,8 +476,9 @@ const LOCATION_HINT_PARAMETERS: ReadonlySet<string> = new Set([
|
|
|
408
476
|
* malformed, e.g., it has an invalid DID, no object path,
|
|
409
477
|
* invalid percent-encoding, credentials, or location
|
|
410
478
|
* hints (`@gateway` query parameters, or the legacy
|
|
411
|
-
* `gateways` parameter),
|
|
412
|
-
*
|
|
479
|
+
* `gateways` parameter), or its raw string path would
|
|
480
|
+
* change during URL parsing. Already-parsed `URL`
|
|
481
|
+
* arguments cannot reveal segments lost by their parser.
|
|
413
482
|
* @since 2.4.0
|
|
414
483
|
*/
|
|
415
484
|
export function fromCompatibleEf61Id(input: string | URL): URL | null {
|
|
@@ -420,14 +489,39 @@ function convertCompatibleEf61Id(
|
|
|
420
489
|
input: string | URL,
|
|
421
490
|
): { url: URL; iri: string } | null {
|
|
422
491
|
let url: URL;
|
|
492
|
+
const rawPrefix = typeof input === "string"
|
|
493
|
+
? input.match(RAW_COMPATIBLE_ID_PREFIX_PATTERN)
|
|
494
|
+
: null;
|
|
423
495
|
if (input instanceof URL) url = input;
|
|
424
496
|
else if (typeof input === "string" && URL.canParse(input)) {
|
|
425
497
|
url = new URL(input);
|
|
426
498
|
} else return null;
|
|
427
499
|
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
428
|
-
|
|
500
|
+
// A raw compatible ID may lose path segments before URL.pathname is read.
|
|
501
|
+
// Likewise, preprocessing must not turn a different raw string into one.
|
|
502
|
+
if (typeof input === "string" && rawPrefix == null) {
|
|
503
|
+
if (
|
|
504
|
+
url.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX) &&
|
|
505
|
+
COMPATIBLE_ID_DID_PATTERN.test(
|
|
506
|
+
url.pathname.slice(COMPATIBLE_ID_PATH_PREFIX.length),
|
|
507
|
+
)
|
|
508
|
+
) {
|
|
509
|
+
throw new TypeError("Invalid FEP-ef61 compatible identifier.");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (!url.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX)) {
|
|
513
|
+
if (rawPrefix != null) {
|
|
514
|
+
throw new TypeError("Invalid FEP-ef61 compatible identifier.");
|
|
515
|
+
}
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
429
518
|
const tail = url.pathname.slice(COMPATIBLE_ID_PATH_PREFIX.length);
|
|
430
|
-
if (!COMPATIBLE_ID_DID_PATTERN.test(tail))
|
|
519
|
+
if (!COMPATIBLE_ID_DID_PATTERN.test(tail)) {
|
|
520
|
+
if (rawPrefix != null) {
|
|
521
|
+
throw new TypeError("Invalid FEP-ef61 compatible identifier.");
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
431
525
|
if (url.username !== "" || url.password !== "") {
|
|
432
526
|
throw new TypeError(
|
|
433
527
|
"Invalid FEP-ef61 compatible identifier: credentials are not allowed.",
|
|
@@ -440,6 +534,13 @@ function convertCompatibleEf61Id(
|
|
|
440
534
|
try {
|
|
441
535
|
const parsed = parsePortableIri(iri);
|
|
442
536
|
if (parsed == null) throw new TypeError("Not a portable IRI.");
|
|
537
|
+
if (
|
|
538
|
+
typeof input === "string" && rawPrefix != null &&
|
|
539
|
+
canonicalizePortableUri("ap://" + input.slice(rawPrefix[0].length)) !==
|
|
540
|
+
canonicalizePortableUri(iri)
|
|
541
|
+
) {
|
|
542
|
+
throw new TypeError("URL parsing changed the portable identifier.");
|
|
543
|
+
}
|
|
443
544
|
// parsePortableIri() does not validate path and fragment
|
|
444
545
|
// percent-encoding, but canonicalizePortableUri() does:
|
|
445
546
|
canonicalizePortableUri(iri);
|
|
@@ -490,7 +591,8 @@ function convertCompatibleEf61Id(
|
|
|
490
591
|
* @returns The compatible identifier.
|
|
491
592
|
* @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
|
|
492
593
|
* URI, if its path has `.` or `..` segments (which
|
|
493
|
-
* HTTP(S) URLs cannot represent
|
|
594
|
+
* HTTP(S) URLs cannot represent without changing the
|
|
595
|
+
* identified object), or if the gateway is
|
|
494
596
|
* not an HTTP(S) origin with no credentials, path, query,
|
|
495
597
|
* or fragment.
|
|
496
598
|
* @since 2.4.0
|