@fedify/vocab-runtime 2.4.0-dev.2228 → 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 (57) 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 +6 -18
  5. package/dist/internal/portable-dereference.js +4 -16
  6. package/dist/mod.cjs +5 -2
  7. package/dist/mod.d.cts +107 -4
  8. package/dist/mod.d.ts +107 -4
  9. package/dist/mod.js +3 -3
  10. package/dist/tests/{body-BmNV5g5S.mjs → body-C00dWiYW.mjs} +1 -1
  11. package/dist/tests/{body-BQZL1aJ1.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-ButlYQxr.mjs → docloader-Bi982uOa.mjs} +3 -3
  17. package/dist/tests/{docloader-BnuGCXLg.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 +30 -157
  21. package/dist/tests/internal/portable-dereference.test.mjs +13 -140
  22. package/dist/tests/{jsonld-cache-C7mkqjiE.mjs → jsonld-cache-CKfgtxPB.mjs} +1 -1
  23. package/dist/tests/{jsonld-cache-CXA76Xi6.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-CFrGpz-3.cjs → portable-media-B39cvJGK.cjs} +3 -3
  31. package/dist/tests/{portable-media-xFDDMte7.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-2GDEEJng.mjs → request-BRPiDcmG.mjs} +1 -1
  39. package/dist/tests/{request-Cme6ShRu.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-R9TTZ67B.mjs → url-CWC-NI3a.mjs} +224 -8
  43. package/dist/tests/{url-B6WlNhra.cjs → url-DsMBGqCc.cjs} +253 -7
  44. package/dist/tests/url.test.cjs +192 -1
  45. package/dist/tests/url.test.mjs +192 -1
  46. package/dist/{url-CHAbe3hE.cjs → url-BJMXv2kE.cjs} +253 -7
  47. package/dist/{url-BfguNa6K.js → url-CZm2-7ZO.js} +224 -8
  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.test.ts +14 -1
  52. package/src/internal/portable-dereference.ts +5 -19
  53. package/src/mod.ts +3 -0
  54. package/src/multibase/multibase.test.ts +5 -5
  55. package/src/portable-workers.test.ts +81 -0
  56. package/src/url.test.ts +385 -0
  57. package/src/url.ts +347 -13
@@ -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
@@ -385,6 +441,166 @@ function isLocationHint(pair) {
385
441
  }
386
442
  }
387
443
  /**
444
+ * The name of the FEP-ef61 location hint query parameter.
445
+ * @internal
446
+ */
447
+ const GATEWAY_HINT_PARAMETER = "@gateway";
448
+ /**
449
+ * Parses an FEP-ef61 gateway, which has to be an HTTP(S) origin with no
450
+ * credentials, path, query, or fragment.
451
+ * @returns The gateway, or `null` if it is not a valid gateway.
452
+ * @internal
453
+ */
454
+ function parseGatewayOrigin(gateway) {
455
+ let url;
456
+ if (gateway instanceof URL) url = new URL(gateway.href);
457
+ else if (typeof gateway === "string" && URL.canParse(gateway)) url = new URL(gateway);
458
+ else return null;
459
+ return isGatewayUrl(url) ? url : null;
460
+ }
461
+ /**
462
+ * Returns a copy of an [FEP-ef61] portable ActivityPub URI with `@gateway`
463
+ * location hints for the given gateways, which tell consumers where they can
464
+ * retrieve the object. Put hints on *references* to portable actors, e.g.,
465
+ * in `actor`, `attributedTo`, `to`, or `cc`, when constructing an object, as
466
+ * FEP-ef61 recommends:
467
+ *
468
+ * ~~~~ typescript
469
+ * withGatewayHints("ap://did:key:z6Mk.../actor", [
470
+ * "https://server1.example",
471
+ * "https://server2.example",
472
+ * ]);
473
+ * // ap+ef61://did:key:z6Mk.../actor?@gateway=https%3A%2F%2Fserver1.example&@gateway=https%3A%2F%2Fserver2.example
474
+ * ~~~~
475
+ *
476
+ * Do not put hints on an object's own `id`. Hints do not change the
477
+ * identity of a portable URI, since FEP-ef61 drops the query when comparing
478
+ * portable URIs, but implementations that do not canonicalize portable URIs
479
+ * would take a hinted ID for another object. Add hints before signing the
480
+ * object, since its Object Integrity Proof covers its references too.
481
+ *
482
+ * The hints that the URI already has, including the legacy `gateways`
483
+ * parameter, are replaced. Each gateway becomes a `@gateway` query
484
+ * parameter whose value is its URI-encoded origin, e.g.,
485
+ * `@gateway=https%3A%2F%2Fserver1.example`, in the given order after the
486
+ * other query parameters. Duplicate gateways are dropped, and an empty list
487
+ * removes the hints as {@link withoutGatewayHints} does. The other query
488
+ * parameters, their order, and the fragment are kept, but percent-encoding
489
+ * is normalized the same way as {@link canonicalizePortableUri} does it.
490
+ *
491
+ * Fedify follows at most five hints when dereferencing a portable URI
492
+ * (three for the key ID of an HTTP Signature), so list the preferred
493
+ * gateways first; there is no limit on the number of hints added here.
494
+ *
495
+ * [FEP-ef61]: https://w3id.org/fep/ef61
496
+ *
497
+ * @param portableId The `ap:` or `ap+ef61:` URI. Pass the raw string rather
498
+ * than a `URL` if its path may have `.` or `..` segments,
499
+ * because the `URL` class resolves them.
500
+ * @param gateways The gateways, e.g., the `gateways` of the actor that the
501
+ * URI refers to. Each has to be an HTTP(S) origin with no
502
+ * credentials, path, query, or fragment.
503
+ * @returns The portable URI with the hints, in the same internal `URL` form
504
+ * as {@link parseIri} returns. Use {@link formatIri} to get its
505
+ * canonical string.
506
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
507
+ * URI, e.g., it is a compatible identifier, which must
508
+ * not have location hints; if its path has `.` or `..`
509
+ * segments, which the `URL` class cannot represent; or
510
+ * if a gateway is invalid.
511
+ * @since 2.4.0
512
+ */
513
+ function withGatewayHints(portableId, gateways) {
514
+ const parts = splitPortableIri(portableId);
515
+ if (typeof gateways === "string") throw new TypeError("The gateways must be an iterable of gateways, not a string.");
516
+ const hints = [];
517
+ const seen = /* @__PURE__ */ new Set();
518
+ for (const gateway of gateways) {
519
+ const url = parseGatewayOrigin(gateway);
520
+ if (url == null) throw new TypeError("FEP-ef61 gateways must be HTTP(S) origins with no credentials, path, query, or fragment: " + String(gateway));
521
+ if (seen.has(url.href)) continue;
522
+ seen.add(url.href);
523
+ hints.push(url);
524
+ }
525
+ return replaceGatewayHints(parts, hints);
526
+ }
527
+ /**
528
+ * Returns a copy of an [FEP-ef61] portable ActivityPub URI without its
529
+ * location hints, i.e., `@gateway` query parameters and the legacy
530
+ * `gateways` parameter. The other query parameters, their order, and the
531
+ * fragment are kept, but percent-encoding is normalized the same way as
532
+ * {@link canonicalizePortableUri} does it.
533
+ *
534
+ * [FEP-ef61]: https://w3id.org/fep/ef61
535
+ *
536
+ * @param portableId The `ap:` or `ap+ef61:` URI. Pass the raw string rather
537
+ * than a `URL` if its path may have `.` or `..` segments,
538
+ * because the `URL` class resolves them.
539
+ * @returns The portable URI without the hints, in the same internal `URL`
540
+ * form as {@link parseIri} returns.
541
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
542
+ * URI, or if its path has `.` or `..` segments, which
543
+ * the `URL` class cannot represent.
544
+ * @since 2.4.0
545
+ */
546
+ function withoutGatewayHints(portableId) {
547
+ return replaceGatewayHints(splitPortableIri(portableId), []);
548
+ }
549
+ /**
550
+ * Gets the gateways in the `@gateway` location hints of an [FEP-ef61]
551
+ * portable ActivityPub URI, in order. Hints that are not valid gateways,
552
+ * i.e., HTTP(S) origins with no credentials, path, query, or fragment, are
553
+ * skipped, and so are duplicates. The legacy `gateways` parameter is not
554
+ * read.
555
+ *
556
+ * Unlike Fedify's dereferencing, which follows at most five hints, this
557
+ * returns all of them.
558
+ *
559
+ * [FEP-ef61]: https://w3id.org/fep/ef61
560
+ *
561
+ * @param portableId The `ap:` or `ap+ef61:` URI.
562
+ * @returns The gateways, e.g., `https://server1.example/`.
563
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
564
+ * URI.
565
+ * @since 2.4.0
566
+ */
567
+ function getGatewayHints(portableId) {
568
+ const { query } = splitPortableIri(portableId);
569
+ return query == null ? [] : parseGatewayHints(query);
570
+ }
571
+ function splitPortableIri(portableId) {
572
+ const raw = getRawPortableIri(portableId);
573
+ const match = raw.match(PORTABLE_IRI_PATTERN);
574
+ const parsed = parsePortableIri(raw);
575
+ if (match == null || parsed == null) throw new TypeError("Invalid portable ActivityPub IRI.");
576
+ return {
577
+ raw,
578
+ parsed,
579
+ path: normalizePortableComponent(match[3]),
580
+ query: match[4] == null ? null : normalizePortableComponent(match[4].slice(1)),
581
+ fragment: match[5] == null ? "" : normalizePortableComponent(match[5])
582
+ };
583
+ }
584
+ function parseGatewayHints(query) {
585
+ const hints = [];
586
+ const seen = /* @__PURE__ */ new Set();
587
+ for (const hint of new URLSearchParams(`?${query}`).getAll(GATEWAY_HINT_PARAMETER)) {
588
+ const url = parseGatewayOrigin(hint);
589
+ if (url == null || seen.has(url.href)) continue;
590
+ seen.add(url.href);
591
+ hints.push(url);
592
+ }
593
+ return hints;
594
+ }
595
+ function replaceGatewayHints(parts, hints) {
596
+ const pairs = parts.query == null ? [] : parts.query.split("&").filter((pair) => pair !== "" && !isLocationHint(pair));
597
+ for (const hint of hints) pairs.push(`${GATEWAY_HINT_PARAMETER}=${encodeURIComponent(hint.origin)}`);
598
+ const query = pairs.length < 1 ? "" : `?${pairs.join("&")}`;
599
+ const result = parsePortableIri(`ap+ef61://${parts.parsed.host}${parts.path}${query}${parts.fragment}`);
600
+ if (result == null || canonicalizePortableUri(result.href) !== canonicalizePortableUri(parts.raw) || parseGatewayHints(result.search.slice(1)).map((url) => url.href).join(" ") !== hints.map((url) => url.href).join(" ")) throw new TypeError("The portable ActivityPub IRI cannot be represented as a URL without changing the object it refers to.");
601
+ return result;
602
+ }
603
+ /**
388
604
  * Validates a URL to prevent SSRF attacks.
389
605
  */
390
606
  async function validatePublicUrl(url) {
@@ -580,6 +796,12 @@ function matchesIPv6Prefix(address, prefixWords, prefixLength) {
580
796
  return true;
581
797
  }
582
798
  //#endregion
799
+ Object.defineProperty(exports, "GATEWAY_HINT_PARAMETER", {
800
+ enumerable: true,
801
+ get: function() {
802
+ return GATEWAY_HINT_PARAMETER;
803
+ }
804
+ });
583
805
  Object.defineProperty(exports, "UrlError", {
584
806
  enumerable: true,
585
807
  get: function() {
@@ -622,6 +844,12 @@ Object.defineProperty(exports, "getFe34Origin", {
622
844
  return getFe34Origin;
623
845
  }
624
846
  });
847
+ Object.defineProperty(exports, "getGatewayHints", {
848
+ enumerable: true,
849
+ get: function() {
850
+ return getGatewayHints;
851
+ }
852
+ });
625
853
  Object.defineProperty(exports, "haveSameFe34Origin", {
626
854
  enumerable: true,
627
855
  get: function() {
@@ -652,6 +880,12 @@ Object.defineProperty(exports, "isValidPublicIPv6Address", {
652
880
  return isValidPublicIPv6Address;
653
881
  }
654
882
  });
883
+ Object.defineProperty(exports, "parseGatewayOrigin", {
884
+ enumerable: true,
885
+ get: function() {
886
+ return parseGatewayOrigin;
887
+ }
888
+ });
655
889
  Object.defineProperty(exports, "parseGatewayUrl", {
656
890
  enumerable: true,
657
891
  get: function() {
@@ -688,3 +922,15 @@ Object.defineProperty(exports, "validatePublicUrl", {
688
922
  return validatePublicUrl;
689
923
  }
690
924
  });
925
+ Object.defineProperty(exports, "withGatewayHints", {
926
+ enumerable: true,
927
+ get: function() {
928
+ return withGatewayHints;
929
+ }
930
+ });
931
+ Object.defineProperty(exports, "withoutGatewayHints", {
932
+ enumerable: true,
933
+ get: function() {
934
+ return withoutGatewayHints;
935
+ }
936
+ });
@@ -1,4 +1,4 @@
1
- const require_url = require("./url-B6WlNhra.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
  [
@@ -620,4 +693,122 @@ let node_assert = require("node:assert");
620
693
  (0, node_assert.deepStrictEqual)(require_url.expandIPv6Address("2001:db8::1"), "2001:0db8:0000:0000:0000:0000:0000:0001");
621
694
  (0, node_assert.deepStrictEqual)(require_url.expandIPv6Address("64:ff9b::8.8.8.8"), "0064:ff9b:0000:0000:0000:0000:0808:0808");
622
695
  });
696
+ (0, node_test.test)("withGatewayHints() adds @gateway location hints", () => {
697
+ const expected = "ap+ef61://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fserver1.example&@gateway=https%3A%2F%2Fserver2.example";
698
+ for (const id of [
699
+ "ap://did:key:z6Mkabc/actor",
700
+ "ap+ef61://did:key:z6Mkabc/actor",
701
+ "ap://did%3Akey%3Az6Mkabc/actor",
702
+ require_url.parseIri("ap://did:key:z6Mkabc/actor"),
703
+ new URL("ap://did%3Akey%3Az6Mkabc/actor")
704
+ ]) {
705
+ const hinted = require_url.withGatewayHints(id, ["https://server1.example", new URL("https://server2.example/")]);
706
+ (0, node_assert.ok)(hinted instanceof URL);
707
+ (0, node_assert.strictEqual)(hinted.href, "ap+ef61://did%3Akey%3Az6Mkabc/actor?@gateway=https%3A%2F%2Fserver1.example&@gateway=https%3A%2F%2Fserver2.example");
708
+ (0, node_assert.strictEqual)(require_url.formatIri(hinted), expected);
709
+ (0, node_assert.deepStrictEqual)(require_url.parseIri(require_url.formatIri(hinted)), hinted);
710
+ }
711
+ });
712
+ (0, node_test.test)("withGatewayHints() encodes and deduplicates gateway origins", () => {
713
+ function* gateways() {
714
+ yield "https://A.example:443";
715
+ yield "https://a.example/";
716
+ yield new URL("https://a.example");
717
+ yield "https://b.example:8443";
718
+ yield "http://c.example:80/";
719
+ yield "https://例え.jp";
720
+ }
721
+ (0, node_assert.strictEqual)(require_url.formatIri(require_url.withGatewayHints("ap://did:key:z6Mkabc/actor", gateways())), "ap+ef61://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fa.example&@gateway=https%3A%2F%2Fb.example%3A8443&@gateway=http%3A%2F%2Fc.example&@gateway=https%3A%2F%2Fxn--r8jz45g.jp");
722
+ (0, node_assert.strictEqual)(require_url.formatIri(require_url.withGatewayHints("ap://did:key:z6Mkabc/actor", /* @__PURE__ */ new Set(["https://b.example", "https://a.example"]))), "ap+ef61://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fb.example&@gateway=https%3A%2F%2Fa.example");
723
+ const many = Array.from({ length: 7 }, (_, i) => `https://server${i}.example`);
724
+ (0, node_assert.deepStrictEqual)(require_url.getGatewayHints(require_url.withGatewayHints("ap://did:key:z6Mkabc/actor", many)).map((url) => url.origin), many);
725
+ });
726
+ (0, node_test.test)("withGatewayHints() keeps other query parameters and fragments", () => {
727
+ (0, node_assert.strictEqual)(require_url.formatIri(require_url.withGatewayHints("ap://did:key:z6Mkabc/collection?page=3&maxItems=20&q=a+b%20c%2b", ["https://server.example"])), "ap+ef61://did:key:z6Mkabc/collection?page=3&maxItems=20&q=a+b%20c%2B&@gateway=https%3A%2F%2Fserver.example");
728
+ (0, node_assert.strictEqual)(require_url.formatIri(require_url.withGatewayHints("ap://did:key:z6Mkabc/actor?x=%26%3D#main-key", ["https://server.example"])), "ap+ef61://did:key:z6Mkabc/actor?x=%26%3D&@gateway=https%3A%2F%2Fserver.example#main-key");
729
+ (0, node_assert.strictEqual)(require_url.withGatewayHints("ap://did:key:z6Mkabc/actor#", ["https://s.example"]).href, "ap+ef61://did%3Akey%3Az6Mkabc/actor?@gateway=https%3A%2F%2Fs.example#");
730
+ });
731
+ (0, node_test.test)("withGatewayHints() replaces existing location hints", () => {
732
+ (0, node_assert.strictEqual)(require_url.formatIri(require_url.withGatewayHints("ap://did:key:z6Mkabc/collection?@gateway=https%3A%2F%2Fold.example&page=2&%40gateway=https%3A%2F%2Fold2.example&@gateway&gateways=https%3A%2F%2Flegacy.example&%2540gateway=kept", ["https://new.example"])), "ap+ef61://did:key:z6Mkabc/collection?page=2&%2540gateway=kept&@gateway=https%3A%2F%2Fnew.example");
733
+ for (const char of [
734
+ " ",
735
+ "\n",
736
+ "\r"
737
+ ]) (0, node_assert.deepStrictEqual)(require_url.getGatewayHints(require_url.withGatewayHints(`ap://did:key:z6Mkabc/actor?@gate${char}way=https%3A%2F%2Fevil.example`, ["https://new.example"])).map((url) => url.href), ["https://new.example/"]);
738
+ });
739
+ (0, node_test.test)("withGatewayHints() with no gateways removes location hints", () => {
740
+ for (const [input, expected] of [
741
+ ["ap://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fa.example", "ap+ef61://did%3Akey%3Az6Mkabc/actor"],
742
+ ["ap://did:key:z6Mkabc/actor?&@gateway=https%3A%2F%2Fa.example&&p=1&", "ap+ef61://did%3Akey%3Az6Mkabc/actor?p=1"],
743
+ ["ap://did:key:z6Mkabc/actor?", "ap+ef61://did%3Akey%3Az6Mkabc/actor"],
744
+ ["ap://did:key:z6Mkabc/actor?gateways=https%3A%2F%2Fa.example#k", "ap+ef61://did%3Akey%3Az6Mkabc/actor#k"]
745
+ ]) {
746
+ (0, node_assert.strictEqual)(require_url.withGatewayHints(input, []).href, expected, input);
747
+ (0, node_assert.strictEqual)(require_url.withoutGatewayHints(input).href, expected, input);
748
+ }
749
+ const input = require_url.parseIri("ap://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fa.example");
750
+ const href = input.href;
751
+ require_url.withoutGatewayHints(input);
752
+ (0, node_assert.strictEqual)(input.href, href);
753
+ });
754
+ (0, node_test.test)("withGatewayHints() rejects non-portable IDs", () => {
755
+ for (const id of [
756
+ "https://server.example/.well-known/apgateway/did:key:z6Mkabc/actor",
757
+ "https://example.com/actor",
758
+ "did:key:z6Mkabc#z6Mkabc",
759
+ "ap://did:key:z6Mkabc",
760
+ "ap://example.com/actor",
761
+ "ap://did:key:z6Mkabc/actor%zz",
762
+ new URL("https://example.com/actor"),
763
+ new URL("ap://did%3Akey%3Az6Mkabc:8080/actor"),
764
+ new URL("ap://user@did%3Akey%3Az6Mkabc/actor")
765
+ ]) {
766
+ (0, node_assert.throws)(() => require_url.withGatewayHints(id, ["https://server.example"]), TypeError, String(id));
767
+ (0, node_assert.throws)(() => require_url.withoutGatewayHints(id), TypeError, String(id));
768
+ (0, node_assert.throws)(() => require_url.getGatewayHints(id), TypeError, String(id));
769
+ }
770
+ });
771
+ (0, node_test.test)("withGatewayHints() rejects paths that URLs cannot represent", () => {
772
+ for (const id of [
773
+ "ap://did:key:z6Mkabc/a/../actor",
774
+ "ap://did:key:z6Mkabc/a/./actor",
775
+ "ap://did:key:z6Mkabc/a/%2e%2e/actor",
776
+ "ap://did:key:z6Mkabc/a/%2E/actor"
777
+ ]) {
778
+ (0, node_assert.throws)(() => require_url.withGatewayHints(id, ["https://server.example"]), TypeError, id);
779
+ (0, node_assert.throws)(() => require_url.withoutGatewayHints(id), TypeError, id);
780
+ }
781
+ (0, node_assert.strictEqual)(require_url.withoutGatewayHints("ap://did:key:z6Mkabc/a b").href, "ap+ef61://did%3Akey%3Az6Mkabc/a%09b");
782
+ });
783
+ (0, node_test.test)("withGatewayHints() rejects invalid gateways", () => {
784
+ for (const gateway of [
785
+ "https://server.example/path",
786
+ "https://server.example/?",
787
+ "https://server.example/#",
788
+ "https://server.example/?q=1",
789
+ "https://user:pass@server.example",
790
+ "ftp://server.example",
791
+ "ap://did:key:z6Mkabc/actor",
792
+ "server.example",
793
+ new URL("https://server.example/path")
794
+ ]) (0, node_assert.throws)(() => require_url.withGatewayHints("ap://did:key:z6Mkabc/actor", [gateway]), TypeError, String(gateway));
795
+ (0, node_assert.throws)(() => require_url.withGatewayHints("ap://did:key:z6Mkabc/actor", "https://server.example"), TypeError);
796
+ });
797
+ (0, node_test.test)("getGatewayHints() reads @gateway location hints", () => {
798
+ (0, node_assert.deepStrictEqual)(require_url.getGatewayHints("ap://did:key:z6Mkabc/actor?@gateway=https%3A%2F%2Fa.example&@gateway=invalid&%40gateway=https%3A%2F%2Fb.example%2F&@gateway=https%3A%2F%2Fa.example%2F&@gateway=https%3A%2F%2Fc.example%2Fpath&gateways=https%3A%2F%2Flegacy.example&page=1#k").map((url) => url.href), ["https://a.example/", "https://b.example/"]);
799
+ (0, node_assert.deepStrictEqual)(require_url.getGatewayHints("ap://did:key:z6Mkabc/actor"), []);
800
+ const questioned = "ap://did:key:z6Mkabc/actor??@gateway=https%3A%2F%2Fa.example";
801
+ (0, node_assert.deepStrictEqual)(require_url.getGatewayHints(questioned), []);
802
+ (0, node_assert.strictEqual)(require_url.withoutGatewayHints(questioned).href, "ap+ef61://did%3Akey%3Az6Mkabc/actor??@gateway=https%3A%2F%2Fa.example");
803
+ (0, node_assert.deepStrictEqual)(require_url.getGatewayHints(require_url.withGatewayHints(questioned, ["https://b.example"])).map((url) => url.href), ["https://b.example/"]);
804
+ (0, node_assert.deepStrictEqual)(require_url.getGatewayHints("ap://did:key:z6Mkabc/actor?@gate%09way=https%3A%2F%2Fa.example"), []);
805
+ (0, node_assert.deepStrictEqual)(require_url.getGatewayHints("ap://did:key:z6Mkabc/actor?@gate way=https%3A%2F%2Fa.example"), []);
806
+ });
807
+ (0, node_test.test)("withGatewayHints() round-trips with other portable ID helpers", () => {
808
+ const hinted = require_url.withGatewayHints("ap://did:key:z6Mkabc/objects/1#frag", ["https://server1.example", "https://server2.example"]);
809
+ (0, node_assert.strictEqual)(require_url.canonicalizePortableUri(require_url.formatIri(hinted)), "ap+ef61://did:key:z6Mkabc/objects/1#frag");
810
+ (0, node_assert.ok)(require_url.arePortableUrisEqual(require_url.formatIri(hinted), "ap://did:key:z6Mkabc/objects/1#frag"));
811
+ (0, node_assert.strictEqual)(require_url.toCompatibleEf61Id(hinted, "https://server1.example").href, "https://server1.example/.well-known/apgateway/did:key:z6Mkabc/objects/1#frag");
812
+ (0, node_assert.strictEqual)(require_url.formatIri(require_url.withoutGatewayHints(hinted)), "ap+ef61://did:key:z6Mkabc/objects/1#frag");
813
+ });
623
814
  //#endregion