@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.
Files changed (55) 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 +3 -6
  5. package/dist/internal/portable-dereference.js +3 -6
  6. package/dist/mod.cjs +2 -2
  7. package/dist/mod.d.cts +14 -3
  8. package/dist/mod.d.ts +14 -3
  9. package/dist/mod.js +2 -2
  10. package/dist/tests/{body-BEYUpKvX.mjs → body-C00dWiYW.mjs} +1 -1
  11. package/dist/tests/{body-DQKTFMsH.cjs → body-DHk-vT0I.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-ra1lbBfP.mjs → docloader-Bi982uOa.mjs} +3 -3
  17. package/dist/tests/{docloader-DHCYdBXf.cjs → docloader-gX-8knd3.cjs} +3 -3
  18. package/dist/tests/docloader.test.cjs +59 -65
  19. package/dist/tests/docloader.test.mjs +38 -44
  20. package/dist/tests/internal/portable-dereference.test.cjs +21 -149
  21. package/dist/tests/internal/portable-dereference.test.mjs +3 -131
  22. package/dist/tests/{jsonld-cache-BKrsWPCQ.mjs → jsonld-cache-CKfgtxPB.mjs} +1 -1
  23. package/dist/tests/{jsonld-cache-BTgizq9A.cjs → jsonld-cache-DzxT9IXH.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/multibase/multibase.test.cjs +7 -7
  27. package/dist/tests/multibase/multibase.test.mjs +5 -5
  28. package/dist/tests/portable-dereference-C_cwGjz6.mjs +132 -0
  29. package/dist/tests/portable-dereference-CyFYTBnr.cjs +149 -0
  30. package/dist/tests/{portable-media-DHaAEpaQ.cjs → portable-media-B39cvJGK.cjs} +3 -3
  31. package/dist/tests/{portable-media-1NjUQari.mjs → portable-media-l0ET9Pq8.mjs} +3 -3
  32. package/dist/tests/portable-media.test.cjs +1 -1
  33. package/dist/tests/portable-media.test.mjs +1 -1
  34. package/dist/tests/portable-workers.test.cjs +36 -0
  35. package/dist/tests/portable-workers.test.d.cts +2 -0
  36. package/dist/tests/portable-workers.test.d.mts +2 -0
  37. package/dist/tests/portable-workers.test.mjs +35 -0
  38. package/dist/tests/{request-D3elVRyB.mjs → request-BRPiDcmG.mjs} +1 -1
  39. package/dist/tests/{request-BQlkB8du.cjs → request-fo2l2yJS.cjs} +1 -1
  40. package/dist/tests/request.test.cjs +1 -1
  41. package/dist/tests/request.test.mjs +1 -1
  42. package/dist/tests/{url-Cdu4ASgJ.mjs → url-CWC-NI3a.mjs} +63 -7
  43. package/dist/tests/{url-CtHyzMR4.cjs → url-DsMBGqCc.cjs} +63 -7
  44. package/dist/tests/url.test.cjs +74 -1
  45. package/dist/tests/url.test.mjs +74 -1
  46. package/dist/{url-BHfQuRdy.cjs → url-BJMXv2kE.cjs} +63 -7
  47. package/dist/{url-DqqDZY9i.js → url-CZm2-7ZO.js} +63 -7
  48. package/package.json +6 -3
  49. package/scripts/test-bun.mjs +17 -0
  50. package/src/docloader.test.ts +82 -74
  51. package/src/internal/portable-dereference.ts +2 -5
  52. package/src/multibase/multibase.test.ts +5 -5
  53. package/src/portable-workers.test.ts +81 -0
  54. package/src/url.test.ts +90 -0
  55. 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) return iri instanceof URL ? iri.href : URL.canParse(iri) ? new URL(iri).href : iri;
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
- return parsePortableIri(`ap+ef61://${iri.host}${iri.pathname}${iri.search}${iri.hash}`);
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), which FEP-ef61 forbids in
280
- * compatible identifiers.
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 (!url.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX)) return null;
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)) return null;
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), or if the gateway is
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) return iri instanceof URL ? iri.href : URL.canParse(iri) ? new URL(iri).href : iri;
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
- return parsePortableIri(`ap+ef61://${iri.host}${iri.pathname}${iri.search}${iri.hash}`);
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), which FEP-ef61 forbids in
278
- * compatible identifiers.
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 (!url.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX)) return null;
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)) return null;
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), or if the gateway is
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.2233+abb4a6b2",
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;