@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
@@ -3,7 +3,7 @@ let node_process = require("node:process");
3
3
  node_process = require_rolldown_runtime.__toESM(node_process, 1);
4
4
  //#region deno.json
5
5
  var name = "@fedify/vocab-runtime";
6
- var version = "2.4.0-dev.2233+abb4a6b2";
6
+ var version = "2.4.0-dev.2240+79613fa6";
7
7
  //#endregion
8
8
  //#region src/request.ts
9
9
  /**
@@ -1,5 +1,5 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-emK7D4bc.cjs");
2
- const require_request = require("./request-BQlkB8du.cjs");
2
+ const require_request = require("./request-fo2l2yJS.cjs");
3
3
  let node_test = require("node:test");
4
4
  let node_process = require("node:process");
5
5
  node_process = require_rolldown_runtime.__toESM(node_process, 1);
@@ -1,4 +1,4 @@
1
- import { n as createActivityPubRequest, o as version, r as getUserAgent } from "./request-D3elVRyB.mjs";
1
+ import { n as createActivityPubRequest, o as version, r as getUserAgent } from "./request-BRPiDcmG.mjs";
2
2
  import { test } from "node:test";
3
3
  import process from "node:process";
4
4
  import { deepStrictEqual } from "node:assert";
@@ -19,6 +19,26 @@ const INVALID_PERCENT_ENCODING_PATTERN = /%(?![0-9A-Fa-f]{2})/;
19
19
  const PERCENT_ENCODING_PATTERN = /%[0-9A-Fa-f]{2}/g;
20
20
  const DID_SCHEME_PATTERN = /^did:/i;
21
21
  const DID_PATTERN = /^did:[a-z0-9]+:[-A-Za-z0-9._%]+(?::[-A-Za-z0-9._%]+)*$/i;
22
+ const DOT_SEGMENT_PATTERN = /(?:^|\/)(?:\.|%2e){1,2}(?=\/|$)/i;
23
+ function prepareUrlInput(iri) {
24
+ let prepared = iri.replace(/[\t\n\r]/g, "");
25
+ while (prepared.length > 0 && prepared.charCodeAt(0) <= 32) prepared = prepared.slice(1);
26
+ while (prepared.length > 0 && prepared.charCodeAt(prepared.length - 1) <= 32) prepared = prepared.slice(0, -1);
27
+ return prepared;
28
+ }
29
+ function assertPortablePathCanBeParsed(iri) {
30
+ const match = prepareUrlInput(iri).match(PORTABLE_IRI_PATTERN);
31
+ if (match != null && DOT_SEGMENT_PATTERN.test(match[3])) throw new TypeError("Portable ActivityPub IRI paths with dot segments cannot be represented as URLs.");
32
+ }
33
+ function assertCompatiblePathCanBeParsed(raw, parsed) {
34
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
35
+ const match = prepareUrlInput(raw).match(/^https?:[\/\\]*[^/?#\\]*([^?#]*)/i);
36
+ if (match == null) return;
37
+ const rawPath = match[1].replace(/\\/g, "/");
38
+ const compatiblePath = parsed.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX) && COMPATIBLE_ID_DID_PATTERN.test(parsed.pathname.slice(COMPATIBLE_ID_PATH_PREFIX.length));
39
+ const rawCompatiblePath = rawPath.startsWith(COMPATIBLE_ID_PATH_PREFIX) && COMPATIBLE_ID_DID_PATTERN.test(rawPath.slice(COMPATIBLE_ID_PATH_PREFIX.length));
40
+ if ((compatiblePath || rawCompatiblePath) && DOT_SEGMENT_PATTERN.test(rawPath)) throw new TypeError("FEP-ef61 compatible identifier paths with dot segments cannot be represented as URLs.");
41
+ }
22
42
  /**
23
43
  * Parses a JSON-LD `@id` value as an IRI.
24
44
  */
@@ -32,22 +52,40 @@ function parseJsonLdId(id, base) {
32
52
  }
33
53
  /**
34
54
  * Parses an IRI as a URL, including FEP-ef61 portable ActivityPub IRIs.
55
+ * Portable URI and FEP-ef61 compatible identifier strings whose path contains
56
+ * a `.` or `..` segment, including percent-encoded spellings, throw a
57
+ * `TypeError`: JavaScript `URL` would otherwise identify a different object.
58
+ * This also applies to compatible identifier strings used as relative bases.
59
+ * A `URL` argument may already have lost such segments before this function
60
+ * receives it.
35
61
  */
36
62
  function parseIri(iri, base) {
37
63
  if (iri instanceof URL) return normalizePortableUrl(iri) ?? new URL(iri.href);
64
+ assertPortablePathCanBeParsed(iri);
38
65
  const portable = parsePortableIri(iri);
39
66
  if (portable != null) return portable;
40
67
  base = normalizeBaseIri(base);
41
68
  if (!URL.canParse(iri, base) && iri.startsWith("at://")) return parseAtUri(iri);
42
69
  const parsed = new URL(iri, base);
70
+ assertCompatiblePathCanBeParsed(iri, parsed);
43
71
  return normalizePortableUrl(parsed) ?? parsed;
44
72
  }
45
73
  /**
46
74
  * Formats a URL as an IRI, including FEP-ef61 portable ActivityPub IRIs.
75
+ * Portable URI and FEP-ef61 compatible identifier strings with dot segments
76
+ * throw a `TypeError` because their paths cannot be represented by JavaScript
77
+ * `URL` without normalization.
47
78
  */
48
79
  function formatIri(iri) {
80
+ if (typeof iri === "string") assertPortablePathCanBeParsed(iri);
49
81
  const parsed = parsePortableIri(iri instanceof URL ? iri.href : iri);
50
- if (parsed == null) return iri instanceof URL ? iri.href : URL.canParse(iri) ? new URL(iri).href : iri;
82
+ if (parsed == null) {
83
+ if (iri instanceof URL) return iri.href;
84
+ if (!URL.canParse(iri)) return iri;
85
+ const url = new URL(iri);
86
+ assertCompatiblePathCanBeParsed(iri, url);
87
+ return url.href;
88
+ }
51
89
  return `ap+ef61://${decodePortableAuthority(parsed.host)}${parsed.pathname}${parsed.search}${parsed.hash}`;
52
90
  }
53
91
  /**
@@ -167,11 +205,15 @@ function parsePortableIri(iri) {
167
205
  }
168
206
  function normalizePortableUrl(iri) {
169
207
  if (iri.protocol !== "ap:" && iri.protocol !== "ap+ef61:") return null;
170
- return parsePortableIri(`ap+ef61://${iri.host}${iri.pathname}${iri.search}${iri.hash}`);
208
+ const raw = `ap+ef61://${iri.host}${iri.pathname}${iri.search}${iri.hash}`;
209
+ assertPortablePathCanBeParsed(raw);
210
+ return parsePortableIri(raw);
171
211
  }
172
212
  function normalizeBaseIri(base) {
173
213
  if (base == null) return void 0;
174
214
  if (base instanceof URL) return normalizePortableUrl(base) ?? base;
215
+ assertPortablePathCanBeParsed(base);
216
+ if (URL.canParse(base)) assertCompatiblePathCanBeParsed(base, new URL(base));
175
217
  return parsePortableIri(base) ?? (base.startsWith("at://") && !URL.canParse(".", base) ? parseAtUri(base) : base);
176
218
  }
177
219
  function decodePortableAuthority(authority) {
@@ -238,6 +280,7 @@ function parseGatewayUrl(url) {
238
280
  }
239
281
  const COMPATIBLE_ID_PATH_PREFIX = "/.well-known/apgateway/";
240
282
  const COMPATIBLE_ID_DID_PATTERN = /^did(?::|%3A)/i;
283
+ const RAW_COMPATIBLE_ID_PREFIX_PATTERN = /^(?:[hH][tT][tT][pP][sS]?):\/\/[^/?#]*\/\.well-known\/apgateway\/(?=[dD][iI][dD](?::|%3[aA]))/;
241
284
  const LOCATION_HINT_PARAMETERS = /* @__PURE__ */ new Set(["@gateway", "gateways"]);
242
285
  /**
243
286
  * Converts an [FEP-ef61] compatible identifier into a portable ActivityPub
@@ -273,8 +316,9 @@ const LOCATION_HINT_PARAMETERS = /* @__PURE__ */ new Set(["@gateway", "gateways"
273
316
  * malformed, e.g., it has an invalid DID, no object path,
274
317
  * invalid percent-encoding, credentials, or location
275
318
  * hints (`@gateway` query parameters, or the legacy
276
- * `gateways` parameter), which FEP-ef61 forbids in
277
- * compatible identifiers.
319
+ * `gateways` parameter), or its raw string path would
320
+ * change during URL parsing. Already-parsed `URL`
321
+ * arguments cannot reveal segments lost by their parser.
278
322
  * @since 2.4.0
279
323
  */
280
324
  function fromCompatibleEf61Id(input) {
@@ -282,18 +326,29 @@ function fromCompatibleEf61Id(input) {
282
326
  }
283
327
  function convertCompatibleEf61Id(input) {
284
328
  let url;
329
+ const rawPrefix = typeof input === "string" ? input.match(RAW_COMPATIBLE_ID_PREFIX_PATTERN) : null;
285
330
  if (input instanceof URL) url = input;
286
331
  else if (typeof input === "string" && URL.canParse(input)) url = new URL(input);
287
332
  else return null;
288
333
  if (url.protocol !== "http:" && url.protocol !== "https:") return null;
289
- if (!url.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX)) return null;
334
+ if (typeof input === "string" && rawPrefix == null) {
335
+ 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.");
336
+ }
337
+ if (!url.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX)) {
338
+ if (rawPrefix != null) throw new TypeError("Invalid FEP-ef61 compatible identifier.");
339
+ return null;
340
+ }
290
341
  const tail = url.pathname.slice(23);
291
- if (!COMPATIBLE_ID_DID_PATTERN.test(tail)) return null;
342
+ if (!COMPATIBLE_ID_DID_PATTERN.test(tail)) {
343
+ if (rawPrefix != null) throw new TypeError("Invalid FEP-ef61 compatible identifier.");
344
+ return null;
345
+ }
292
346
  if (url.username !== "" || url.password !== "") throw new TypeError("Invalid FEP-ef61 compatible identifier: credentials are not allowed.");
293
347
  const iri = "ap+ef61://" + url.href.slice(url.origin.length + 23);
294
348
  try {
295
349
  const parsed = parsePortableIri(iri);
296
350
  if (parsed == null) throw new TypeError("Not a portable IRI.");
351
+ if (typeof input === "string" && rawPrefix != null && canonicalizePortableUri("ap://" + input.slice(rawPrefix[0].length)) !== canonicalizePortableUri(iri)) throw new TypeError("URL parsing changed the portable identifier.");
297
352
  canonicalizePortableUri(iri);
298
353
  if ((url.search === "" ? [] : normalizePortableComponent(url.search.slice(1)).split("&")).some(isLocationHint)) throw new TypeError("Location hints are not allowed.");
299
354
  return {
@@ -333,7 +388,8 @@ function convertCompatibleEf61Id(input) {
333
388
  * @returns The compatible identifier.
334
389
  * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
335
390
  * URI, if its path has `.` or `..` segments (which
336
- * HTTP(S) URLs cannot represent), or if the gateway is
391
+ * HTTP(S) URLs cannot represent without changing the
392
+ * identified object), or if the gateway is
337
393
  * not an HTTP(S) origin with no credentials, path, query,
338
394
  * or fragment.
339
395
  * @since 2.4.0
@@ -21,6 +21,26 @@ const INVALID_PERCENT_ENCODING_PATTERN = /%(?![0-9A-Fa-f]{2})/;
21
21
  const PERCENT_ENCODING_PATTERN = /%[0-9A-Fa-f]{2}/g;
22
22
  const DID_SCHEME_PATTERN = /^did:/i;
23
23
  const DID_PATTERN = /^did:[a-z0-9]+:[-A-Za-z0-9._%]+(?::[-A-Za-z0-9._%]+)*$/i;
24
+ const DOT_SEGMENT_PATTERN = /(?:^|\/)(?:\.|%2e){1,2}(?=\/|$)/i;
25
+ function prepareUrlInput(iri) {
26
+ let prepared = iri.replace(/[\t\n\r]/g, "");
27
+ while (prepared.length > 0 && prepared.charCodeAt(0) <= 32) prepared = prepared.slice(1);
28
+ while (prepared.length > 0 && prepared.charCodeAt(prepared.length - 1) <= 32) prepared = prepared.slice(0, -1);
29
+ return prepared;
30
+ }
31
+ function assertPortablePathCanBeParsed(iri) {
32
+ const match = prepareUrlInput(iri).match(PORTABLE_IRI_PATTERN);
33
+ if (match != null && DOT_SEGMENT_PATTERN.test(match[3])) throw new TypeError("Portable ActivityPub IRI paths with dot segments cannot be represented as URLs.");
34
+ }
35
+ function assertCompatiblePathCanBeParsed(raw, parsed) {
36
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
37
+ const match = prepareUrlInput(raw).match(/^https?:[\/\\]*[^/?#\\]*([^?#]*)/i);
38
+ if (match == null) return;
39
+ const rawPath = match[1].replace(/\\/g, "/");
40
+ const compatiblePath = parsed.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX) && COMPATIBLE_ID_DID_PATTERN.test(parsed.pathname.slice(COMPATIBLE_ID_PATH_PREFIX.length));
41
+ const rawCompatiblePath = rawPath.startsWith(COMPATIBLE_ID_PATH_PREFIX) && COMPATIBLE_ID_DID_PATTERN.test(rawPath.slice(COMPATIBLE_ID_PATH_PREFIX.length));
42
+ if ((compatiblePath || rawCompatiblePath) && DOT_SEGMENT_PATTERN.test(rawPath)) throw new TypeError("FEP-ef61 compatible identifier paths with dot segments cannot be represented as URLs.");
43
+ }
24
44
  /**
25
45
  * Parses a JSON-LD `@id` value as an IRI.
26
46
  */
@@ -34,22 +54,40 @@ function parseJsonLdId(id, base) {
34
54
  }
35
55
  /**
36
56
  * Parses an IRI as a URL, including FEP-ef61 portable ActivityPub IRIs.
57
+ * Portable URI and FEP-ef61 compatible identifier strings whose path contains
58
+ * a `.` or `..` segment, including percent-encoded spellings, throw a
59
+ * `TypeError`: JavaScript `URL` would otherwise identify a different object.
60
+ * This also applies to compatible identifier strings used as relative bases.
61
+ * A `URL` argument may already have lost such segments before this function
62
+ * receives it.
37
63
  */
38
64
  function parseIri(iri, base) {
39
65
  if (iri instanceof URL) return normalizePortableUrl(iri) ?? new URL(iri.href);
66
+ assertPortablePathCanBeParsed(iri);
40
67
  const portable = parsePortableIri(iri);
41
68
  if (portable != null) return portable;
42
69
  base = normalizeBaseIri(base);
43
70
  if (!URL.canParse(iri, base) && iri.startsWith("at://")) return parseAtUri(iri);
44
71
  const parsed = new URL(iri, base);
72
+ assertCompatiblePathCanBeParsed(iri, parsed);
45
73
  return normalizePortableUrl(parsed) ?? parsed;
46
74
  }
47
75
  /**
48
76
  * Formats a URL as an IRI, including FEP-ef61 portable ActivityPub IRIs.
77
+ * Portable URI and FEP-ef61 compatible identifier strings with dot segments
78
+ * throw a `TypeError` because their paths cannot be represented by JavaScript
79
+ * `URL` without normalization.
49
80
  */
50
81
  function formatIri(iri) {
82
+ if (typeof iri === "string") assertPortablePathCanBeParsed(iri);
51
83
  const parsed = parsePortableIri(iri instanceof URL ? iri.href : iri);
52
- if (parsed == null) return iri instanceof URL ? iri.href : URL.canParse(iri) ? new URL(iri).href : iri;
84
+ if (parsed == null) {
85
+ if (iri instanceof URL) return iri.href;
86
+ if (!URL.canParse(iri)) return iri;
87
+ const url = new URL(iri);
88
+ assertCompatiblePathCanBeParsed(iri, url);
89
+ return url.href;
90
+ }
53
91
  return `ap+ef61://${decodePortableAuthority(parsed.host)}${parsed.pathname}${parsed.search}${parsed.hash}`;
54
92
  }
55
93
  /**
@@ -169,11 +207,15 @@ function parsePortableIri(iri) {
169
207
  }
170
208
  function normalizePortableUrl(iri) {
171
209
  if (iri.protocol !== "ap:" && iri.protocol !== "ap+ef61:") return null;
172
- return parsePortableIri(`ap+ef61://${iri.host}${iri.pathname}${iri.search}${iri.hash}`);
210
+ const raw = `ap+ef61://${iri.host}${iri.pathname}${iri.search}${iri.hash}`;
211
+ assertPortablePathCanBeParsed(raw);
212
+ return parsePortableIri(raw);
173
213
  }
174
214
  function normalizeBaseIri(base) {
175
215
  if (base == null) return void 0;
176
216
  if (base instanceof URL) return normalizePortableUrl(base) ?? base;
217
+ assertPortablePathCanBeParsed(base);
218
+ if (URL.canParse(base)) assertCompatiblePathCanBeParsed(base, new URL(base));
177
219
  return parsePortableIri(base) ?? (base.startsWith("at://") && !URL.canParse(".", base) ? parseAtUri(base) : base);
178
220
  }
179
221
  function decodePortableAuthority(authority) {
@@ -240,6 +282,7 @@ function parseGatewayUrl(url) {
240
282
  }
241
283
  const COMPATIBLE_ID_PATH_PREFIX = "/.well-known/apgateway/";
242
284
  const COMPATIBLE_ID_DID_PATTERN = /^did(?::|%3A)/i;
285
+ const RAW_COMPATIBLE_ID_PREFIX_PATTERN = /^(?:[hH][tT][tT][pP][sS]?):\/\/[^/?#]*\/\.well-known\/apgateway\/(?=[dD][iI][dD](?::|%3[aA]))/;
243
286
  const LOCATION_HINT_PARAMETERS = /* @__PURE__ */ new Set(["@gateway", "gateways"]);
244
287
  /**
245
288
  * Converts an [FEP-ef61] compatible identifier into a portable ActivityPub
@@ -275,8 +318,9 @@ const LOCATION_HINT_PARAMETERS = /* @__PURE__ */ new Set(["@gateway", "gateways"
275
318
  * malformed, e.g., it has an invalid DID, no object path,
276
319
  * invalid percent-encoding, credentials, or location
277
320
  * hints (`@gateway` query parameters, or the legacy
278
- * `gateways` parameter), which FEP-ef61 forbids in
279
- * compatible identifiers.
321
+ * `gateways` parameter), or its raw string path would
322
+ * change during URL parsing. Already-parsed `URL`
323
+ * arguments cannot reveal segments lost by their parser.
280
324
  * @since 2.4.0
281
325
  */
282
326
  function fromCompatibleEf61Id(input) {
@@ -284,18 +328,29 @@ function fromCompatibleEf61Id(input) {
284
328
  }
285
329
  function convertCompatibleEf61Id(input) {
286
330
  let url;
331
+ const rawPrefix = typeof input === "string" ? input.match(RAW_COMPATIBLE_ID_PREFIX_PATTERN) : null;
287
332
  if (input instanceof URL) url = input;
288
333
  else if (typeof input === "string" && URL.canParse(input)) url = new URL(input);
289
334
  else return null;
290
335
  if (url.protocol !== "http:" && url.protocol !== "https:") return null;
291
- if (!url.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX)) return null;
336
+ if (typeof input === "string" && rawPrefix == null) {
337
+ 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.");
338
+ }
339
+ if (!url.pathname.startsWith(COMPATIBLE_ID_PATH_PREFIX)) {
340
+ if (rawPrefix != null) throw new TypeError("Invalid FEP-ef61 compatible identifier.");
341
+ return null;
342
+ }
292
343
  const tail = url.pathname.slice(23);
293
- if (!COMPATIBLE_ID_DID_PATTERN.test(tail)) return null;
344
+ if (!COMPATIBLE_ID_DID_PATTERN.test(tail)) {
345
+ if (rawPrefix != null) throw new TypeError("Invalid FEP-ef61 compatible identifier.");
346
+ return null;
347
+ }
294
348
  if (url.username !== "" || url.password !== "") throw new TypeError("Invalid FEP-ef61 compatible identifier: credentials are not allowed.");
295
349
  const iri = "ap+ef61://" + url.href.slice(url.origin.length + 23);
296
350
  try {
297
351
  const parsed = parsePortableIri(iri);
298
352
  if (parsed == null) throw new TypeError("Not a portable IRI.");
353
+ if (typeof input === "string" && rawPrefix != null && canonicalizePortableUri("ap://" + input.slice(rawPrefix[0].length)) !== canonicalizePortableUri(iri)) throw new TypeError("URL parsing changed the portable identifier.");
299
354
  canonicalizePortableUri(iri);
300
355
  if ((url.search === "" ? [] : normalizePortableComponent(url.search.slice(1)).split("&")).some(isLocationHint)) throw new TypeError("Location hints are not allowed.");
301
356
  return {
@@ -335,7 +390,8 @@ function convertCompatibleEf61Id(input) {
335
390
  * @returns The compatible identifier.
336
391
  * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
337
392
  * URI, if its path has `.` or `..` segments (which
338
- * HTTP(S) URLs cannot represent), or if the gateway is
393
+ * HTTP(S) URLs cannot represent without changing the
394
+ * identified object), or if the gateway is
339
395
  * not an HTTP(S) origin with no credentials, path, query,
340
396
  * or fragment.
341
397
  * @since 2.4.0
@@ -1,4 +1,4 @@
1
- const require_url = require("./url-CtHyzMR4.cjs");
1
+ const require_url = require("./url-DsMBGqCc.cjs");
2
2
  let node_test = require("node:test");
3
3
  let node_assert = require("node:assert");
4
4
  //#region src/url.test.ts
@@ -11,6 +11,39 @@ let node_assert = require("node:assert");
11
11
  "AP+EF61://did:key:z6Mkabc/actor"
12
12
  ]) (0, node_assert.deepStrictEqual)(require_url.parseIri(iri), new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"));
13
13
  });
14
+ (0, node_test.test)("portable URI parsing refuses dot segments before URL normalization", () => {
15
+ const base = "ap://did:key:z6Mkabc";
16
+ for (const segment of [
17
+ ".",
18
+ "..",
19
+ "%2e",
20
+ ".%2E",
21
+ "%2e.",
22
+ "%2E%2e"
23
+ ]) for (const path of [
24
+ `/${segment}`,
25
+ `/a/${segment}/b`,
26
+ `/a/${segment}`
27
+ ]) {
28
+ const iri = base + path;
29
+ (0, node_assert.throws)(() => require_url.parseIri(iri), TypeError, iri);
30
+ (0, node_assert.throws)(() => require_url.parseJsonLdId(iri), TypeError, iri);
31
+ (0, node_assert.throws)(() => require_url.formatIri(iri), TypeError, iri);
32
+ }
33
+ for (const iri of [
34
+ `${base}/a/.\t./b`,
35
+ `${base}/a/..\n/b`,
36
+ ` ${base}/a/../b`,
37
+ `${base}/a/.. `
38
+ ]) (0, node_assert.throws)(() => require_url.parseIri(iri), TypeError, iri);
39
+ for (const path of [
40
+ "/a/.../b",
41
+ "/a/.x/b",
42
+ "/a/x.y/b",
43
+ "/a/%252e/b"
44
+ ]) (0, node_assert.deepStrictEqual)(require_url.formatIri(require_url.parseIri(base + path)), `ap+ef61://did:key:z6Mkabc${path}`);
45
+ (0, node_assert.throws)(() => require_url.parseIri("child", `${base}/a/../b`), TypeError);
46
+ });
14
47
  (0, node_test.test)("parseIri() normalizes DID scheme and method casing", () => {
15
48
  for (const iri of [
16
49
  "ap://DID:key:z6Mkabc/actor",
@@ -44,6 +77,20 @@ let node_assert = require("node:assert");
44
77
  (0, node_assert.deepStrictEqual)(require_url.parseIri("attachments/1", "ap://did:key:z6Mkabc/objects/1"), new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/attachments/1"));
45
78
  (0, node_assert.throws)(() => require_url.parseIri("//example.com/outbox", "ap://did:key:z6Mkabc/objects/1"), TypeError);
46
79
  });
80
+ (0, node_test.test)("parseIri() rejects lossy compatible ID string bases", () => {
81
+ const base = "https://server.example/.well-known/apgateway/did:key:z6MkAlice";
82
+ for (const segment of [
83
+ ".",
84
+ "..",
85
+ "%2e",
86
+ "%2e%2e"
87
+ ]) {
88
+ const rawBase = `${base}/a/${segment}/b`;
89
+ (0, node_assert.throws)(() => require_url.parseIri("child", rawBase), TypeError, rawBase);
90
+ (0, node_assert.throws)(() => require_url.parseJsonLdId("child", rawBase), TypeError, rawBase);
91
+ }
92
+ (0, node_assert.deepStrictEqual)(require_url.parseIri("child", "https://server.example/a/../b"), new URL("https://server.example/child"));
93
+ });
47
94
  (0, node_test.test)("parseIri() resolves relative IRIs against at:// string bases", () => {
48
95
  (0, node_assert.deepStrictEqual)(require_url.parseIri("/record", "at://did:plc:example/collection/item"), new URL("at://did%3Aplc%3Aexample/record"));
49
96
  (0, node_assert.deepStrictEqual)(require_url.parseIri("reply", "at://did:plc:example/collection/item"), new URL("at://did%3Aplc%3Aexample/collection/reply"));
@@ -316,6 +363,32 @@ let node_assert = require("node:assert");
316
363
  "https://user@server.example/.well-known/apgateway/did:key:z6MkAlice/a"
317
364
  ]) (0, node_assert.throws)(() => require_url.fromCompatibleEf61Id(input), TypeError, input);
318
365
  });
366
+ (0, node_test.test)("fromCompatibleEf61Id() refuses raw paths changed by URL parsing", () => {
367
+ const base = "https://server.example/.well-known/apgateway/did:key:z6MkAlice";
368
+ for (const path of [
369
+ "/a/../b",
370
+ "/a/%2e%2E/b",
371
+ "/a/. ./b",
372
+ "/a/.. "
373
+ ]) {
374
+ (0, node_assert.throws)(() => require_url.fromCompatibleEf61Id(base + path), TypeError, path);
375
+ (0, node_assert.throws)(() => require_url.parseIri(base + path), TypeError, path);
376
+ (0, node_assert.throws)(() => require_url.formatIri(base + path), TypeError, path);
377
+ }
378
+ for (const input of [
379
+ "https:/server.example/.well-known/apgateway/did:key:z6MkAlice/a/../b",
380
+ "https:server.example/.well-known/apgateway/did:key:z6MkAlice/a/../b",
381
+ "https:\\\\server.example\\.well-known\\apgateway\\did:key:z6MkAlice\\a\\..\\b",
382
+ ` ${base}/../../other`
383
+ ]) {
384
+ (0, node_assert.throws)(() => require_url.parseIri(input), TypeError, input);
385
+ (0, node_assert.throws)(() => require_url.formatIri(input), TypeError, input);
386
+ }
387
+ (0, node_assert.throws)(() => require_url.fromCompatibleEf61Id(` ${base}/actor`), TypeError);
388
+ (0, node_assert.throws)(() => require_url.fromCompatibleEf61Id(`https://server.example/foo/../.well-known/apgateway/did:key:z6MkAlice/actor`), TypeError);
389
+ (0, node_assert.deepStrictEqual)(require_url.fromCompatibleEf61Id(`${base}/a/%252e/b`)?.pathname, "/a/%252e/b");
390
+ (0, node_assert.deepStrictEqual)(require_url.fromCompatibleEf61Id("https://server.example/.WELL-KNOWN/apgateway/did:key:z6MkAlice/actor"), null);
391
+ });
319
392
  (0, node_test.test)("toCompatibleEf61Id() converts portable URIs", () => {
320
393
  const cases = [
321
394
  [
@@ -1,4 +1,4 @@
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";
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-CWC-NI3a.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
@@ -11,6 +11,39 @@ test("parseIri() accepts portable ActivityPub URI schemes", () => {
11
11
  "AP+EF61://did:key:z6Mkabc/actor"
12
12
  ]) deepStrictEqual(parseIri(iri), new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"));
13
13
  });
14
+ test("portable URI parsing refuses dot segments before URL normalization", () => {
15
+ const base = "ap://did:key:z6Mkabc";
16
+ for (const segment of [
17
+ ".",
18
+ "..",
19
+ "%2e",
20
+ ".%2E",
21
+ "%2e.",
22
+ "%2E%2e"
23
+ ]) for (const path of [
24
+ `/${segment}`,
25
+ `/a/${segment}/b`,
26
+ `/a/${segment}`
27
+ ]) {
28
+ const iri = base + path;
29
+ throws(() => parseIri(iri), TypeError, iri);
30
+ throws(() => parseJsonLdId(iri), TypeError, iri);
31
+ throws(() => formatIri(iri), TypeError, iri);
32
+ }
33
+ for (const iri of [
34
+ `${base}/a/.\t./b`,
35
+ `${base}/a/..\n/b`,
36
+ ` ${base}/a/../b`,
37
+ `${base}/a/.. `
38
+ ]) throws(() => parseIri(iri), TypeError, iri);
39
+ for (const path of [
40
+ "/a/.../b",
41
+ "/a/.x/b",
42
+ "/a/x.y/b",
43
+ "/a/%252e/b"
44
+ ]) deepStrictEqual(formatIri(parseIri(base + path)), `ap+ef61://did:key:z6Mkabc${path}`);
45
+ throws(() => parseIri("child", `${base}/a/../b`), TypeError);
46
+ });
14
47
  test("parseIri() normalizes DID scheme and method casing", () => {
15
48
  for (const iri of [
16
49
  "ap://DID:key:z6Mkabc/actor",
@@ -44,6 +77,20 @@ test("parseIri() resolves relative IRIs against portable string bases", () => {
44
77
  deepStrictEqual(parseIri("attachments/1", "ap://did:key:z6Mkabc/objects/1"), new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/attachments/1"));
45
78
  throws(() => parseIri("//example.com/outbox", "ap://did:key:z6Mkabc/objects/1"), TypeError);
46
79
  });
80
+ test("parseIri() rejects lossy compatible ID string bases", () => {
81
+ const base = "https://server.example/.well-known/apgateway/did:key:z6MkAlice";
82
+ for (const segment of [
83
+ ".",
84
+ "..",
85
+ "%2e",
86
+ "%2e%2e"
87
+ ]) {
88
+ const rawBase = `${base}/a/${segment}/b`;
89
+ throws(() => parseIri("child", rawBase), TypeError, rawBase);
90
+ throws(() => parseJsonLdId("child", rawBase), TypeError, rawBase);
91
+ }
92
+ deepStrictEqual(parseIri("child", "https://server.example/a/../b"), new URL("https://server.example/child"));
93
+ });
47
94
  test("parseIri() resolves relative IRIs against at:// string bases", () => {
48
95
  deepStrictEqual(parseIri("/record", "at://did:plc:example/collection/item"), new URL("at://did%3Aplc%3Aexample/record"));
49
96
  deepStrictEqual(parseIri("reply", "at://did:plc:example/collection/item"), new URL("at://did%3Aplc%3Aexample/collection/reply"));
@@ -316,6 +363,32 @@ test("fromCompatibleEf61Id() rejects malformed compatible identifiers", () => {
316
363
  "https://user@server.example/.well-known/apgateway/did:key:z6MkAlice/a"
317
364
  ]) throws(() => fromCompatibleEf61Id(input), TypeError, input);
318
365
  });
366
+ test("fromCompatibleEf61Id() refuses raw paths changed by URL parsing", () => {
367
+ const base = "https://server.example/.well-known/apgateway/did:key:z6MkAlice";
368
+ for (const path of [
369
+ "/a/../b",
370
+ "/a/%2e%2E/b",
371
+ "/a/. ./b",
372
+ "/a/.. "
373
+ ]) {
374
+ throws(() => fromCompatibleEf61Id(base + path), TypeError, path);
375
+ throws(() => parseIri(base + path), TypeError, path);
376
+ throws(() => formatIri(base + path), TypeError, path);
377
+ }
378
+ for (const input of [
379
+ "https:/server.example/.well-known/apgateway/did:key:z6MkAlice/a/../b",
380
+ "https:server.example/.well-known/apgateway/did:key:z6MkAlice/a/../b",
381
+ "https:\\\\server.example\\.well-known\\apgateway\\did:key:z6MkAlice\\a\\..\\b",
382
+ ` ${base}/../../other`
383
+ ]) {
384
+ throws(() => parseIri(input), TypeError, input);
385
+ throws(() => formatIri(input), TypeError, input);
386
+ }
387
+ throws(() => fromCompatibleEf61Id(` ${base}/actor`), TypeError);
388
+ throws(() => fromCompatibleEf61Id(`https://server.example/foo/../.well-known/apgateway/did:key:z6MkAlice/actor`), TypeError);
389
+ deepStrictEqual(fromCompatibleEf61Id(`${base}/a/%252e/b`)?.pathname, "/a/%252e/b");
390
+ deepStrictEqual(fromCompatibleEf61Id("https://server.example/.WELL-KNOWN/apgateway/did:key:z6MkAlice/actor"), null);
391
+ });
319
392
  test("toCompatibleEf61Id() converts portable URIs", () => {
320
393
  const cases = [
321
394
  [