@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
|
@@ -22,6 +22,26 @@ const INVALID_PERCENT_ENCODING_PATTERN = /%(?![0-9A-Fa-f]{2})/;
|
|
|
22
22
|
const PERCENT_ENCODING_PATTERN = /%[0-9A-Fa-f]{2}/g;
|
|
23
23
|
const DID_SCHEME_PATTERN = /^did:/i;
|
|
24
24
|
const DID_PATTERN = /^did:[a-z0-9]+:[-A-Za-z0-9._%]+(?::[-A-Za-z0-9._%]+)*$/i;
|
|
25
|
+
const DOT_SEGMENT_PATTERN = /(?:^|\/)(?:\.|%2e){1,2}(?=\/|$)/i;
|
|
26
|
+
function prepareUrlInput(iri) {
|
|
27
|
+
let prepared = iri.replace(/[\t\n\r]/g, "");
|
|
28
|
+
while (prepared.length > 0 && prepared.charCodeAt(0) <= 32) prepared = prepared.slice(1);
|
|
29
|
+
while (prepared.length > 0 && prepared.charCodeAt(prepared.length - 1) <= 32) prepared = prepared.slice(0, -1);
|
|
30
|
+
return prepared;
|
|
31
|
+
}
|
|
32
|
+
function assertPortablePathCanBeParsed(iri) {
|
|
33
|
+
const match = prepareUrlInput(iri).match(PORTABLE_IRI_PATTERN);
|
|
34
|
+
if (match != null && DOT_SEGMENT_PATTERN.test(match[3])) throw new TypeError("Portable ActivityPub IRI paths with dot segments cannot be represented as URLs.");
|
|
35
|
+
}
|
|
36
|
+
function assertCompatiblePathCanBeParsed(raw, parsed) {
|
|
37
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
|
|
38
|
+
const match = prepareUrlInput(raw).match(/^https?:[\/\\]*[^/?#\\]*([^?#]*)/i);
|
|
39
|
+
if (match == null) return;
|
|
40
|
+
const rawPath = match[1].replace(/\\/g, "/");
|
|
41
|
+
const compatiblePath = parsed.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX) && COMPATIBLE_ID_DID_PATTERN.test(parsed.pathname.slice(COMPATIBLE_ID_PATH_PREFIX.length));
|
|
42
|
+
const rawCompatiblePath = rawPath.startsWith(COMPATIBLE_ID_PATH_PREFIX) && COMPATIBLE_ID_DID_PATTERN.test(rawPath.slice(COMPATIBLE_ID_PATH_PREFIX.length));
|
|
43
|
+
if ((compatiblePath || rawCompatiblePath) && DOT_SEGMENT_PATTERN.test(rawPath)) throw new TypeError("FEP-ef61 compatible identifier paths with dot segments cannot be represented as URLs.");
|
|
44
|
+
}
|
|
25
45
|
/**
|
|
26
46
|
* Parses a JSON-LD `@id` value as an IRI.
|
|
27
47
|
*/
|
|
@@ -35,22 +55,40 @@ function parseJsonLdId(id, base) {
|
|
|
35
55
|
}
|
|
36
56
|
/**
|
|
37
57
|
* Parses an IRI as a URL, including FEP-ef61 portable ActivityPub IRIs.
|
|
58
|
+
* Portable URI and FEP-ef61 compatible identifier strings whose path contains
|
|
59
|
+
* a `.` or `..` segment, including percent-encoded spellings, throw a
|
|
60
|
+
* `TypeError`: JavaScript `URL` would otherwise identify a different object.
|
|
61
|
+
* This also applies to compatible identifier strings used as relative bases.
|
|
62
|
+
* A `URL` argument may already have lost such segments before this function
|
|
63
|
+
* receives it.
|
|
38
64
|
*/
|
|
39
65
|
function parseIri(iri, base) {
|
|
40
66
|
if (iri instanceof URL) return normalizePortableUrl(iri) ?? new URL(iri.href);
|
|
67
|
+
assertPortablePathCanBeParsed(iri);
|
|
41
68
|
const portable = parsePortableIri(iri);
|
|
42
69
|
if (portable != null) return portable;
|
|
43
70
|
base = normalizeBaseIri(base);
|
|
44
71
|
if (!URL.canParse(iri, base) && iri.startsWith("at://")) return parseAtUri(iri);
|
|
45
72
|
const parsed = new URL(iri, base);
|
|
73
|
+
assertCompatiblePathCanBeParsed(iri, parsed);
|
|
46
74
|
return normalizePortableUrl(parsed) ?? parsed;
|
|
47
75
|
}
|
|
48
76
|
/**
|
|
49
77
|
* Formats a URL as an IRI, including FEP-ef61 portable ActivityPub IRIs.
|
|
78
|
+
* Portable URI and FEP-ef61 compatible identifier strings with dot segments
|
|
79
|
+
* throw a `TypeError` because their paths cannot be represented by JavaScript
|
|
80
|
+
* `URL` without normalization.
|
|
50
81
|
*/
|
|
51
82
|
function formatIri(iri) {
|
|
83
|
+
if (typeof iri === "string") assertPortablePathCanBeParsed(iri);
|
|
52
84
|
const parsed = parsePortableIri(iri instanceof URL ? iri.href : iri);
|
|
53
|
-
if (parsed == null)
|
|
85
|
+
if (parsed == null) {
|
|
86
|
+
if (iri instanceof URL) return iri.href;
|
|
87
|
+
if (!URL.canParse(iri)) return iri;
|
|
88
|
+
const url = new URL(iri);
|
|
89
|
+
assertCompatiblePathCanBeParsed(iri, url);
|
|
90
|
+
return url.href;
|
|
91
|
+
}
|
|
54
92
|
return `ap+ef61://${decodePortableAuthority(parsed.host)}${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
55
93
|
}
|
|
56
94
|
/**
|
|
@@ -170,11 +208,15 @@ function parsePortableIri(iri) {
|
|
|
170
208
|
}
|
|
171
209
|
function normalizePortableUrl(iri) {
|
|
172
210
|
if (iri.protocol !== "ap:" && iri.protocol !== "ap+ef61:") return null;
|
|
173
|
-
|
|
211
|
+
const raw = `ap+ef61://${iri.host}${iri.pathname}${iri.search}${iri.hash}`;
|
|
212
|
+
assertPortablePathCanBeParsed(raw);
|
|
213
|
+
return parsePortableIri(raw);
|
|
174
214
|
}
|
|
175
215
|
function normalizeBaseIri(base) {
|
|
176
216
|
if (base == null) return void 0;
|
|
177
217
|
if (base instanceof URL) return normalizePortableUrl(base) ?? base;
|
|
218
|
+
assertPortablePathCanBeParsed(base);
|
|
219
|
+
if (URL.canParse(base)) assertCompatiblePathCanBeParsed(base, new URL(base));
|
|
178
220
|
return parsePortableIri(base) ?? (base.startsWith("at://") && !URL.canParse(".", base) ? parseAtUri(base) : base);
|
|
179
221
|
}
|
|
180
222
|
function decodePortableAuthority(authority) {
|
|
@@ -241,6 +283,7 @@ function parseGatewayUrl(url) {
|
|
|
241
283
|
}
|
|
242
284
|
const COMPATIBLE_ID_PATH_PREFIX = "/.well-known/apgateway/";
|
|
243
285
|
const COMPATIBLE_ID_DID_PATTERN = /^did(?::|%3A)/i;
|
|
286
|
+
const RAW_COMPATIBLE_ID_PREFIX_PATTERN = /^(?:[hH][tT][tT][pP][sS]?):\/\/[^/?#]*\/\.well-known\/apgateway\/(?=[dD][iI][dD](?::|%3[aA]))/;
|
|
244
287
|
const LOCATION_HINT_PARAMETERS = /* @__PURE__ */ new Set(["@gateway", "gateways"]);
|
|
245
288
|
/**
|
|
246
289
|
* Converts an [FEP-ef61] compatible identifier into a portable ActivityPub
|
|
@@ -276,8 +319,9 @@ const LOCATION_HINT_PARAMETERS = /* @__PURE__ */ new Set(["@gateway", "gateways"
|
|
|
276
319
|
* malformed, e.g., it has an invalid DID, no object path,
|
|
277
320
|
* invalid percent-encoding, credentials, or location
|
|
278
321
|
* hints (`@gateway` query parameters, or the legacy
|
|
279
|
-
* `gateways` parameter),
|
|
280
|
-
*
|
|
322
|
+
* `gateways` parameter), or its raw string path would
|
|
323
|
+
* change during URL parsing. Already-parsed `URL`
|
|
324
|
+
* arguments cannot reveal segments lost by their parser.
|
|
281
325
|
* @since 2.4.0
|
|
282
326
|
*/
|
|
283
327
|
function fromCompatibleEf61Id(input) {
|
|
@@ -285,18 +329,29 @@ function fromCompatibleEf61Id(input) {
|
|
|
285
329
|
}
|
|
286
330
|
function convertCompatibleEf61Id(input) {
|
|
287
331
|
let url;
|
|
332
|
+
const rawPrefix = typeof input === "string" ? input.match(RAW_COMPATIBLE_ID_PREFIX_PATTERN) : null;
|
|
288
333
|
if (input instanceof URL) url = input;
|
|
289
334
|
else if (typeof input === "string" && URL.canParse(input)) url = new URL(input);
|
|
290
335
|
else return null;
|
|
291
336
|
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
292
|
-
if (
|
|
337
|
+
if (typeof input === "string" && rawPrefix == null) {
|
|
338
|
+
if (url.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX) && COMPATIBLE_ID_DID_PATTERN.test(url.pathname.slice(23))) throw new TypeError("Invalid FEP-ef61 compatible identifier.");
|
|
339
|
+
}
|
|
340
|
+
if (!url.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX)) {
|
|
341
|
+
if (rawPrefix != null) throw new TypeError("Invalid FEP-ef61 compatible identifier.");
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
293
344
|
const tail = url.pathname.slice(23);
|
|
294
|
-
if (!COMPATIBLE_ID_DID_PATTERN.test(tail))
|
|
345
|
+
if (!COMPATIBLE_ID_DID_PATTERN.test(tail)) {
|
|
346
|
+
if (rawPrefix != null) throw new TypeError("Invalid FEP-ef61 compatible identifier.");
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
295
349
|
if (url.username !== "" || url.password !== "") throw new TypeError("Invalid FEP-ef61 compatible identifier: credentials are not allowed.");
|
|
296
350
|
const iri = "ap+ef61://" + url.href.slice(url.origin.length + 23);
|
|
297
351
|
try {
|
|
298
352
|
const parsed = parsePortableIri(iri);
|
|
299
353
|
if (parsed == null) throw new TypeError("Not a portable IRI.");
|
|
354
|
+
if (typeof input === "string" && rawPrefix != null && canonicalizePortableUri("ap://" + input.slice(rawPrefix[0].length)) !== canonicalizePortableUri(iri)) throw new TypeError("URL parsing changed the portable identifier.");
|
|
300
355
|
canonicalizePortableUri(iri);
|
|
301
356
|
if ((url.search === "" ? [] : normalizePortableComponent(url.search.slice(1)).split("&")).some(isLocationHint)) throw new TypeError("Location hints are not allowed.");
|
|
302
357
|
return {
|
|
@@ -336,7 +391,8 @@ function convertCompatibleEf61Id(input) {
|
|
|
336
391
|
* @returns The compatible identifier.
|
|
337
392
|
* @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
|
|
338
393
|
* URI, if its path has `.` or `..` segments (which
|
|
339
|
-
* HTTP(S) URLs cannot represent
|
|
394
|
+
* HTTP(S) URLs cannot represent without changing the
|
|
395
|
+
* identified object), or if the gateway is
|
|
340
396
|
* not an HTTP(S) origin with no credentials, path, query,
|
|
341
397
|
* or fragment.
|
|
342
398
|
* @since 2.4.0
|
|
@@ -20,6 +20,26 @@ const INVALID_PERCENT_ENCODING_PATTERN = /%(?![0-9A-Fa-f]{2})/;
|
|
|
20
20
|
const PERCENT_ENCODING_PATTERN = /%[0-9A-Fa-f]{2}/g;
|
|
21
21
|
const DID_SCHEME_PATTERN = /^did:/i;
|
|
22
22
|
const DID_PATTERN = /^did:[a-z0-9]+:[-A-Za-z0-9._%]+(?::[-A-Za-z0-9._%]+)*$/i;
|
|
23
|
+
const DOT_SEGMENT_PATTERN = /(?:^|\/)(?:\.|%2e){1,2}(?=\/|$)/i;
|
|
24
|
+
function prepareUrlInput(iri) {
|
|
25
|
+
let prepared = iri.replace(/[\t\n\r]/g, "");
|
|
26
|
+
while (prepared.length > 0 && prepared.charCodeAt(0) <= 32) prepared = prepared.slice(1);
|
|
27
|
+
while (prepared.length > 0 && prepared.charCodeAt(prepared.length - 1) <= 32) prepared = prepared.slice(0, -1);
|
|
28
|
+
return prepared;
|
|
29
|
+
}
|
|
30
|
+
function assertPortablePathCanBeParsed(iri) {
|
|
31
|
+
const match = prepareUrlInput(iri).match(PORTABLE_IRI_PATTERN);
|
|
32
|
+
if (match != null && DOT_SEGMENT_PATTERN.test(match[3])) throw new TypeError("Portable ActivityPub IRI paths with dot segments cannot be represented as URLs.");
|
|
33
|
+
}
|
|
34
|
+
function assertCompatiblePathCanBeParsed(raw, parsed) {
|
|
35
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
|
|
36
|
+
const match = prepareUrlInput(raw).match(/^https?:[\/\\]*[^/?#\\]*([^?#]*)/i);
|
|
37
|
+
if (match == null) return;
|
|
38
|
+
const rawPath = match[1].replace(/\\/g, "/");
|
|
39
|
+
const compatiblePath = parsed.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX) && COMPATIBLE_ID_DID_PATTERN.test(parsed.pathname.slice(COMPATIBLE_ID_PATH_PREFIX.length));
|
|
40
|
+
const rawCompatiblePath = rawPath.startsWith(COMPATIBLE_ID_PATH_PREFIX) && COMPATIBLE_ID_DID_PATTERN.test(rawPath.slice(COMPATIBLE_ID_PATH_PREFIX.length));
|
|
41
|
+
if ((compatiblePath || rawCompatiblePath) && DOT_SEGMENT_PATTERN.test(rawPath)) throw new TypeError("FEP-ef61 compatible identifier paths with dot segments cannot be represented as URLs.");
|
|
42
|
+
}
|
|
23
43
|
/**
|
|
24
44
|
* Parses a JSON-LD `@id` value as an IRI.
|
|
25
45
|
*/
|
|
@@ -33,22 +53,40 @@ function parseJsonLdId(id, base) {
|
|
|
33
53
|
}
|
|
34
54
|
/**
|
|
35
55
|
* Parses an IRI as a URL, including FEP-ef61 portable ActivityPub IRIs.
|
|
56
|
+
* Portable URI and FEP-ef61 compatible identifier strings whose path contains
|
|
57
|
+
* a `.` or `..` segment, including percent-encoded spellings, throw a
|
|
58
|
+
* `TypeError`: JavaScript `URL` would otherwise identify a different object.
|
|
59
|
+
* This also applies to compatible identifier strings used as relative bases.
|
|
60
|
+
* A `URL` argument may already have lost such segments before this function
|
|
61
|
+
* receives it.
|
|
36
62
|
*/
|
|
37
63
|
function parseIri(iri, base) {
|
|
38
64
|
if (iri instanceof URL) return normalizePortableUrl(iri) ?? new URL(iri.href);
|
|
65
|
+
assertPortablePathCanBeParsed(iri);
|
|
39
66
|
const portable = parsePortableIri(iri);
|
|
40
67
|
if (portable != null) return portable;
|
|
41
68
|
base = normalizeBaseIri(base);
|
|
42
69
|
if (!URL.canParse(iri, base) && iri.startsWith("at://")) return parseAtUri(iri);
|
|
43
70
|
const parsed = new URL(iri, base);
|
|
71
|
+
assertCompatiblePathCanBeParsed(iri, parsed);
|
|
44
72
|
return normalizePortableUrl(parsed) ?? parsed;
|
|
45
73
|
}
|
|
46
74
|
/**
|
|
47
75
|
* Formats a URL as an IRI, including FEP-ef61 portable ActivityPub IRIs.
|
|
76
|
+
* Portable URI and FEP-ef61 compatible identifier strings with dot segments
|
|
77
|
+
* throw a `TypeError` because their paths cannot be represented by JavaScript
|
|
78
|
+
* `URL` without normalization.
|
|
48
79
|
*/
|
|
49
80
|
function formatIri(iri) {
|
|
81
|
+
if (typeof iri === "string") assertPortablePathCanBeParsed(iri);
|
|
50
82
|
const parsed = parsePortableIri(iri instanceof URL ? iri.href : iri);
|
|
51
|
-
if (parsed == null)
|
|
83
|
+
if (parsed == null) {
|
|
84
|
+
if (iri instanceof URL) return iri.href;
|
|
85
|
+
if (!URL.canParse(iri)) return iri;
|
|
86
|
+
const url = new URL(iri);
|
|
87
|
+
assertCompatiblePathCanBeParsed(iri, url);
|
|
88
|
+
return url.href;
|
|
89
|
+
}
|
|
52
90
|
return `ap+ef61://${decodePortableAuthority(parsed.host)}${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
53
91
|
}
|
|
54
92
|
/**
|
|
@@ -168,11 +206,15 @@ function parsePortableIri(iri) {
|
|
|
168
206
|
}
|
|
169
207
|
function normalizePortableUrl(iri) {
|
|
170
208
|
if (iri.protocol !== "ap:" && iri.protocol !== "ap+ef61:") return null;
|
|
171
|
-
|
|
209
|
+
const raw = `ap+ef61://${iri.host}${iri.pathname}${iri.search}${iri.hash}`;
|
|
210
|
+
assertPortablePathCanBeParsed(raw);
|
|
211
|
+
return parsePortableIri(raw);
|
|
172
212
|
}
|
|
173
213
|
function normalizeBaseIri(base) {
|
|
174
214
|
if (base == null) return void 0;
|
|
175
215
|
if (base instanceof URL) return normalizePortableUrl(base) ?? base;
|
|
216
|
+
assertPortablePathCanBeParsed(base);
|
|
217
|
+
if (URL.canParse(base)) assertCompatiblePathCanBeParsed(base, new URL(base));
|
|
176
218
|
return parsePortableIri(base) ?? (base.startsWith("at://") && !URL.canParse(".", base) ? parseAtUri(base) : base);
|
|
177
219
|
}
|
|
178
220
|
function decodePortableAuthority(authority) {
|
|
@@ -239,6 +281,7 @@ function parseGatewayUrl(url) {
|
|
|
239
281
|
}
|
|
240
282
|
const COMPATIBLE_ID_PATH_PREFIX = "/.well-known/apgateway/";
|
|
241
283
|
const COMPATIBLE_ID_DID_PATTERN = /^did(?::|%3A)/i;
|
|
284
|
+
const RAW_COMPATIBLE_ID_PREFIX_PATTERN = /^(?:[hH][tT][tT][pP][sS]?):\/\/[^/?#]*\/\.well-known\/apgateway\/(?=[dD][iI][dD](?::|%3[aA]))/;
|
|
242
285
|
const LOCATION_HINT_PARAMETERS = /* @__PURE__ */ new Set(["@gateway", "gateways"]);
|
|
243
286
|
/**
|
|
244
287
|
* Converts an [FEP-ef61] compatible identifier into a portable ActivityPub
|
|
@@ -274,8 +317,9 @@ const LOCATION_HINT_PARAMETERS = /* @__PURE__ */ new Set(["@gateway", "gateways"
|
|
|
274
317
|
* malformed, e.g., it has an invalid DID, no object path,
|
|
275
318
|
* invalid percent-encoding, credentials, or location
|
|
276
319
|
* hints (`@gateway` query parameters, or the legacy
|
|
277
|
-
* `gateways` parameter),
|
|
278
|
-
*
|
|
320
|
+
* `gateways` parameter), or its raw string path would
|
|
321
|
+
* change during URL parsing. Already-parsed `URL`
|
|
322
|
+
* arguments cannot reveal segments lost by their parser.
|
|
279
323
|
* @since 2.4.0
|
|
280
324
|
*/
|
|
281
325
|
function fromCompatibleEf61Id(input) {
|
|
@@ -283,18 +327,29 @@ function fromCompatibleEf61Id(input) {
|
|
|
283
327
|
}
|
|
284
328
|
function convertCompatibleEf61Id(input) {
|
|
285
329
|
let url;
|
|
330
|
+
const rawPrefix = typeof input === "string" ? input.match(RAW_COMPATIBLE_ID_PREFIX_PATTERN) : null;
|
|
286
331
|
if (input instanceof URL) url = input;
|
|
287
332
|
else if (typeof input === "string" && URL.canParse(input)) url = new URL(input);
|
|
288
333
|
else return null;
|
|
289
334
|
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
290
|
-
if (
|
|
335
|
+
if (typeof input === "string" && rawPrefix == null) {
|
|
336
|
+
if (url.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX) && COMPATIBLE_ID_DID_PATTERN.test(url.pathname.slice(23))) throw new TypeError("Invalid FEP-ef61 compatible identifier.");
|
|
337
|
+
}
|
|
338
|
+
if (!url.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX)) {
|
|
339
|
+
if (rawPrefix != null) throw new TypeError("Invalid FEP-ef61 compatible identifier.");
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
291
342
|
const tail = url.pathname.slice(23);
|
|
292
|
-
if (!COMPATIBLE_ID_DID_PATTERN.test(tail))
|
|
343
|
+
if (!COMPATIBLE_ID_DID_PATTERN.test(tail)) {
|
|
344
|
+
if (rawPrefix != null) throw new TypeError("Invalid FEP-ef61 compatible identifier.");
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
293
347
|
if (url.username !== "" || url.password !== "") throw new TypeError("Invalid FEP-ef61 compatible identifier: credentials are not allowed.");
|
|
294
348
|
const iri = "ap+ef61://" + url.href.slice(url.origin.length + 23);
|
|
295
349
|
try {
|
|
296
350
|
const parsed = parsePortableIri(iri);
|
|
297
351
|
if (parsed == null) throw new TypeError("Not a portable IRI.");
|
|
352
|
+
if (typeof input === "string" && rawPrefix != null && canonicalizePortableUri("ap://" + input.slice(rawPrefix[0].length)) !== canonicalizePortableUri(iri)) throw new TypeError("URL parsing changed the portable identifier.");
|
|
298
353
|
canonicalizePortableUri(iri);
|
|
299
354
|
if ((url.search === "" ? [] : normalizePortableComponent(url.search.slice(1)).split("&")).some(isLocationHint)) throw new TypeError("Location hints are not allowed.");
|
|
300
355
|
return {
|
|
@@ -334,7 +389,8 @@ function convertCompatibleEf61Id(input) {
|
|
|
334
389
|
* @returns The compatible identifier.
|
|
335
390
|
* @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
|
|
336
391
|
* URI, if its path has `.` or `..` segments (which
|
|
337
|
-
* HTTP(S) URLs cannot represent
|
|
392
|
+
* HTTP(S) URLs cannot represent without changing the
|
|
393
|
+
* identified object), or if the gateway is
|
|
338
394
|
* not an HTTP(S) origin with no credentials, path, query,
|
|
339
395
|
* or fragment.
|
|
340
396
|
* @since 2.4.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/vocab-runtime",
|
|
3
|
-
"version": "2.4.0-dev.
|
|
3
|
+
"version": "2.4.0-dev.2240+79613fa6",
|
|
4
4
|
"homepage": "https://fedify.dev/",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -99,7 +99,8 @@
|
|
|
99
99
|
"@types/node": "^24.12.2",
|
|
100
100
|
"fetch-mock": "^12.5.4",
|
|
101
101
|
"tsdown": "^0.22.0",
|
|
102
|
-
"typescript": "^6.0.0"
|
|
102
|
+
"typescript": "^6.0.0",
|
|
103
|
+
"@fedify/fixture": "2.0.0"
|
|
103
104
|
},
|
|
104
105
|
"dependencies": {
|
|
105
106
|
"@js-temporal/polyfill": "^0.5.1",
|
|
@@ -116,6 +117,8 @@
|
|
|
116
117
|
"build": "pnpm --filter @fedify/vocab-runtime... run build:self",
|
|
117
118
|
"prepublish": "pnpm build",
|
|
118
119
|
"pretest": "pnpm build",
|
|
119
|
-
"test": "cd dist/tests && node --test"
|
|
120
|
+
"test": "cd dist/tests && node --test",
|
|
121
|
+
"pretest:bun": "pnpm build",
|
|
122
|
+
"test:bun": "node scripts/test-bun.mjs"
|
|
120
123
|
}
|
|
121
124
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { access, glob } from "node:fs/promises";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
const sources = await Array.fromAsync(glob("src/**/*.test.ts"));
|
|
5
|
+
if (sources.length === 0) throw new Error("No vocab-runtime tests found.");
|
|
6
|
+
|
|
7
|
+
const tests = sources.map((source) =>
|
|
8
|
+
source.replaceAll("\\", "/").replace(/^src\//, "dist/tests/")
|
|
9
|
+
.replace(/\.ts$/, ".mjs")
|
|
10
|
+
).sort();
|
|
11
|
+
for (const test of tests) await access(test);
|
|
12
|
+
|
|
13
|
+
const result = spawnSync("bun", ["test", "--timeout", "60000", ...tests], {
|
|
14
|
+
stdio: "inherit",
|
|
15
|
+
});
|
|
16
|
+
if (result.error) throw result.error;
|
|
17
|
+
process.exitCode = result.status ?? 1;
|