@fedify/vocab-runtime 2.4.0-dev.2219 → 2.4.0-dev.2233

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 (45) 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 +4 -13
  5. package/dist/internal/portable-dereference.js +2 -11
  6. package/dist/mod.cjs +5 -2
  7. package/dist/mod.d.cts +95 -1
  8. package/dist/mod.d.ts +95 -1
  9. package/dist/mod.js +3 -3
  10. package/dist/tests/{body-BzhhuMI0.mjs → body-BEYUpKvX.mjs} +1 -1
  11. package/dist/tests/{body-GaHFfkaq.cjs → body-DQKTFMsH.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-sGr7kARm.cjs → docloader-DHCYdBXf.cjs} +3 -3
  17. package/dist/tests/{docloader-DiRcdiPp.mjs → docloader-ra1lbBfP.mjs} +3 -3
  18. package/dist/tests/docloader.test.cjs +3 -3
  19. package/dist/tests/docloader.test.mjs +3 -3
  20. package/dist/tests/internal/portable-dereference.test.cjs +16 -15
  21. package/dist/tests/internal/portable-dereference.test.mjs +14 -13
  22. package/dist/tests/{jsonld-cache-DUmuWRwp.mjs → jsonld-cache-BKrsWPCQ.mjs} +1 -1
  23. package/dist/tests/{jsonld-cache-BQMn_ILF.cjs → jsonld-cache-BTgizq9A.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/{portable-media-BERS_tX_.mjs → portable-media-1NjUQari.mjs} +3 -3
  27. package/dist/tests/{portable-media-C_Q9GmqU.cjs → portable-media-DHaAEpaQ.cjs} +3 -3
  28. package/dist/tests/portable-media.test.cjs +1 -1
  29. package/dist/tests/portable-media.test.mjs +1 -1
  30. package/dist/tests/{request-qq1N2cl6.cjs → request-BQlkB8du.cjs} +1 -1
  31. package/dist/tests/{request-CWTG9_Xx.mjs → request-D3elVRyB.mjs} +1 -1
  32. package/dist/tests/request.test.cjs +1 -1
  33. package/dist/tests/request.test.mjs +1 -1
  34. package/dist/tests/{url-DfS-cvwr.mjs → url-Cdu4ASgJ.mjs} +165 -3
  35. package/dist/tests/{url-B8xyTwP-.cjs → url-CtHyzMR4.cjs} +194 -2
  36. package/dist/tests/url.test.cjs +133 -3
  37. package/dist/tests/url.test.mjs +133 -3
  38. package/dist/{url-DDVweCXr.cjs → url-BHfQuRdy.cjs} +194 -2
  39. package/dist/{url-Be3moMor.js → url-DqqDZY9i.js} +165 -3
  40. package/package.json +1 -1
  41. package/src/internal/portable-dereference.test.ts +16 -1
  42. package/src/internal/portable-dereference.ts +3 -20
  43. package/src/mod.ts +3 -0
  44. package/src/url.test.ts +311 -1
  45. package/src/url.ts +236 -8
@@ -1,6 +1,6 @@
1
1
  import { t as preloadedContexts } from "../contexts-CIKsin4e.mjs";
2
- import { a as formatIri, m as parseIri } from "../url-DfS-cvwr.mjs";
3
- import { n as createScopedContextLoader, o as registerDocumentLoaderWrapper, s as unwrapReleasedDocumentLoader } from "../jsonld-cache-DUmuWRwp.mjs";
2
+ import { S as withGatewayHints, _ as parseIri, h as parseGatewayOrigin, o as formatIri, t as GATEWAY_HINT_PARAMETER } from "../url-Cdu4ASgJ.mjs";
3
+ import { n as createScopedContextLoader, o as registerDocumentLoaderWrapper, s as unwrapReleasedDocumentLoader } from "../jsonld-cache-BKrsWPCQ.mjs";
4
4
  import { deepStrictEqual, ok, throws } from "node:assert/strict";
5
5
  import { test } from "node:test";
6
6
  import { getLogger } from "@logtape/logtape";
@@ -17,7 +17,6 @@ const logger = getLogger([
17
17
  * a single accessor call from fanning out to many servers.
18
18
  */
19
19
  const MAX_GATEWAY_HINTS = 5;
20
- const LOCATION_HINT_PARAMETER = "@gateway";
21
20
  const BASELINE_CONTEXT_URLS = /* @__PURE__ */ new Set([
22
21
  "https://w3id.org/identity/v1",
23
22
  "https://www.w3.org/ns/activitystreams",
@@ -61,7 +60,7 @@ function getPortableGatewayCandidates(url, gateways) {
61
60
  }
62
61
  return candidates;
63
62
  }
64
- for (const hint of new URLSearchParams(url.search).getAll(LOCATION_HINT_PARAMETER)) {
63
+ for (const hint of new URLSearchParams(url.search).getAll(GATEWAY_HINT_PARAMETER)) {
65
64
  if (candidates.length >= MAX_GATEWAY_HINTS) break;
66
65
  const parsed = parseGatewayOrigin(hint);
67
66
  if (parsed == null) {
@@ -75,14 +74,6 @@ function getPortableGatewayCandidates(url, gateways) {
75
74
  }
76
75
  return candidates;
77
76
  }
78
- function parseGatewayOrigin(gateway) {
79
- let url;
80
- if (gateway instanceof URL) url = new URL(gateway.href);
81
- else if (typeof gateway === "string" && URL.canParse(gateway)) url = new URL(gateway);
82
- else return null;
83
- if (url.protocol !== "http:" && url.protocol !== "https:" || url.href !== `${url.origin}/`) return null;
84
- return url;
85
- }
86
77
  /**
87
78
  * Creates a context loader that returns the same context documents for the
88
79
  * whole dereference operation, so that the identity check, the proof
@@ -172,7 +163,7 @@ test("getPortableGatewayCandidates() uses explicit gateways", () => {
172
163
  ]) throws(() => getPortableGatewayCandidates(url, [gateway]), TypeError);
173
164
  });
174
165
  test("getPortableGatewayCandidates() reads @gateway hints", () => {
175
- deepStrictEqual(hrefs(getPortableGatewayCandidates(parseIri("ap://did:key:z6Mkabc/actor?page=1&@gateway=https%3A%2F%2Fa.example&gateways=https%3A%2F%2Flegacy.example&@gateway=not%20a%20URL&@gateway=https%3A%2F%2Fb.example%2Fpath&%40gateway=https%3A%2F%2Fb.example&@gateway=https%3A%2F%2Fa.example%2F"))), ["https://a.example/", "https://b.example/"]);
166
+ deepStrictEqual(hrefs(getPortableGatewayCandidates(parseIri("ap://did:key:z6Mkabc/actor?page=1&@gateway=https%3A%2F%2Fa.example&gateways=https%3A%2F%2Flegacy.example&@gateway=not%20a%20URL&@gateway=https%3A%2F%2Fb.example%2Fpath&@gateway=https%3A%2F%2Fquery.example%2F%3F&@gateway=https%3A%2F%2Ffragment.example%2F%23&%40gateway=https%3A%2F%2Fb.example&@gateway=https%3A%2F%2Fa.example%2F"))), ["https://a.example/", "https://b.example/"]);
176
167
  deepStrictEqual(getPortableGatewayCandidates(parseIri("ap://did:key:z6Mkabc/actor")), []);
177
168
  const hints = Array.from({ length: 7 }, (_, i) => `@gateway=https%3A%2F%2Fg${i}.example`);
178
169
  deepStrictEqual(hrefs(getPortableGatewayCandidates(parseIri(`ap://did:key:z6Mkabc/actor?${hints.join("&")}`))), [
@@ -183,6 +174,16 @@ test("getPortableGatewayCandidates() reads @gateway hints", () => {
183
174
  4
184
175
  ].map((i) => `https://g${i}.example/`));
185
176
  });
177
+ test("getPortableGatewayCandidates() reads hints from withGatewayHints()", () => {
178
+ const gateways = Array.from({ length: 7 }, (_, i) => `https://g${i}.example`);
179
+ deepStrictEqual(hrefs(getPortableGatewayCandidates(withGatewayHints("ap://did:key:z6Mkabc/actor?page=1", gateways))), [
180
+ 0,
181
+ 1,
182
+ 2,
183
+ 3,
184
+ 4
185
+ ].map((i) => `https://g${i}.example/`));
186
+ });
186
187
  test("createSnapshotContextLoader() does not nest released snapshots", async () => {
187
188
  const calls = [];
188
189
  const base = (url) => {
@@ -1,4 +1,4 @@
1
- import { a as formatIri, c as haveSameFe34Origin, l as haveSameIriOrigin } from "./url-DfS-cvwr.mjs";
1
+ import { d as haveSameIriOrigin, o as formatIri, u as haveSameFe34Origin } from "./url-Cdu4ASgJ.mjs";
2
2
  import jsonld from "jsonld/dist/jsonld.esm.js";
3
3
  //#region src/jsonld.ts
4
4
  var jsonld_default = jsonld;
@@ -1,5 +1,5 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-emK7D4bc.cjs");
2
- const require_url = require("./url-B8xyTwP-.cjs");
2
+ const require_url = require("./url-CtHyzMR4.cjs");
3
3
  let jsonld_dist_jsonld_esm_js = require("jsonld/dist/jsonld.esm.js");
4
4
  jsonld_dist_jsonld_esm_js = require_rolldown_runtime.__toESM(jsonld_dist_jsonld_esm_js, 1);
5
5
  //#region src/jsonld.ts
@@ -1,5 +1,5 @@
1
- const require_url = require("./url-B8xyTwP-.cjs");
2
- const require_jsonld_cache = require("./jsonld-cache-BQMn_ILF.cjs");
1
+ const require_url = require("./url-CtHyzMR4.cjs");
2
+ const require_jsonld_cache = require("./jsonld-cache-BTgizq9A.cjs");
3
3
  let node_test = require("node:test");
4
4
  let node_assert = require("node:assert");
5
5
  //#region src/jsonld-cache.test.ts
@@ -1,5 +1,5 @@
1
- import { m as parseIri } from "./url-DfS-cvwr.mjs";
2
- import { a as normalizeJsonLdIris, c as jsonld_default, i as isTrustedIriOrigin, r as getJsonLdContext, t as compactJsonLdCache } from "./jsonld-cache-DUmuWRwp.mjs";
1
+ import { _ as parseIri } from "./url-Cdu4ASgJ.mjs";
2
+ import { a as normalizeJsonLdIris, c as jsonld_default, i as isTrustedIriOrigin, r as getJsonLdContext, t as compactJsonLdCache } from "./jsonld-cache-BKrsWPCQ.mjs";
3
3
  import { test } from "node:test";
4
4
  import { deepStrictEqual, ok, strictEqual } from "node:assert";
5
5
  //#region src/jsonld-cache.test.ts
@@ -1,6 +1,6 @@
1
- import { r as getUserAgent } from "./request-CWTG9_Xx.mjs";
2
- import { r as readBoundedBytes } from "./body-BzhhuMI0.mjs";
3
- import { p as parseGatewayUrl, v as validatePublicUrl } from "./url-DfS-cvwr.mjs";
1
+ import { r as getUserAgent } from "./request-D3elVRyB.mjs";
2
+ import { r as readBoundedBytes } from "./body-BEYUpKvX.mjs";
3
+ import { g as parseGatewayUrl, x as validatePublicUrl } from "./url-Cdu4ASgJ.mjs";
4
4
  import { a as verifyDigestMultibase, i as parseHashlink, r as parseDigestMultibase } from "./digest-COC7xDiQ.mjs";
5
5
  //#region src/portable-media.ts
6
6
  function resolveTimeout(value, fallback) {
@@ -1,6 +1,6 @@
1
- const require_request = require("./request-qq1N2cl6.cjs");
2
- const require_body = require("./body-GaHFfkaq.cjs");
3
- const require_url = require("./url-B8xyTwP-.cjs");
1
+ const require_request = require("./request-BQlkB8du.cjs");
2
+ const require_body = require("./body-DQKTFMsH.cjs");
3
+ const require_url = require("./url-CtHyzMR4.cjs");
4
4
  const require_digest = require("./digest-3FeH2Y-Q.cjs");
5
5
  //#region src/portable-media.ts
6
6
  function resolveTimeout(value, fallback) {
@@ -1,5 +1,5 @@
1
1
  const require_digest = require("./digest-3FeH2Y-Q.cjs");
2
- const require_portable_media = require("./portable-media-C_Q9GmqU.cjs");
2
+ const require_portable_media = require("./portable-media-DHaAEpaQ.cjs");
3
3
  let node_assert_strict = require("node:assert/strict");
4
4
  let node_test = require("node:test");
5
5
  let node_http = require("node:http");
@@ -1,5 +1,5 @@
1
1
  import { n as createHashlink, t as computeDigestMultibase } from "./digest-COC7xDiQ.mjs";
2
- import { t as fetchPortableMedia } from "./portable-media-BERS_tX_.mjs";
2
+ import { t as fetchPortableMedia } from "./portable-media-1NjUQari.mjs";
3
3
  import { deepStrictEqual, equal, rejects } from "node:assert/strict";
4
4
  import { test } from "node:test";
5
5
  import { createServer } from "node:http";
@@ -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.2219+eb70f060";
6
+ var version = "2.4.0-dev.2233+abb4a6b2";
7
7
  //#endregion
8
8
  //#region src/request.ts
9
9
  /**
@@ -1,7 +1,7 @@
1
1
  import process from "node:process";
2
2
  //#region deno.json
3
3
  var name = "@fedify/vocab-runtime";
4
- var version = "2.4.0-dev.2219+eb70f060";
4
+ var version = "2.4.0-dev.2233+abb4a6b2";
5
5
  //#endregion
6
6
  //#region src/request.ts
7
7
  /**
@@ -1,5 +1,5 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-emK7D4bc.cjs");
2
- const require_request = require("./request-qq1N2cl6.cjs");
2
+ const require_request = require("./request-BQlkB8du.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-CWTG9_Xx.mjs";
1
+ import { n as createActivityPubRequest, o as version, r as getUserAgent } from "./request-D3elVRyB.mjs";
2
2
  import { test } from "node:test";
3
3
  import process from "node:process";
4
4
  import { deepStrictEqual } from "node:assert";
@@ -218,9 +218,10 @@ function parseAtUri(uri) {
218
218
  }
219
219
  /**
220
220
  * Checks whether the URL is an FEP-ef61 gateway base URI.
221
+ * @since 2.4.0
221
222
  */
222
223
  function isGatewayUrl(url) {
223
- return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "" && url.pathname === "/" && url.search === "" && url.hash === "";
224
+ return (url.protocol === "http:" || url.protocol === "https:") && url.href === `${url.origin}/`;
224
225
  }
225
226
  /**
226
227
  * Parses and validates an FEP-ef61 gateway base URI.
@@ -228,6 +229,7 @@ function isGatewayUrl(url) {
228
229
  * with no credentials, path, query, or fragment. In the
229
230
  * latter case, the message starts with
230
231
  * `Invalid FEP-ef61 gateway:`.
232
+ * @since 2.4.0
231
233
  */
232
234
  function parseGatewayUrl(url) {
233
235
  const parsed = parseIri(url);
@@ -355,7 +357,7 @@ function toCompatibleEf61Id(portableId, gateway) {
355
357
  }
356
358
  function parseCompatibleEf61Gateway(gateway) {
357
359
  const url = gateway instanceof URL ? gateway : typeof gateway === "string" && URL.canParse(gateway) ? new URL(gateway) : null;
358
- if (url == null || url.protocol !== "http:" && url.protocol !== "https:" || url.href !== `${url.origin}/`) throw new TypeError("FEP-ef61 gateways for compatible identifiers must be HTTP(S) origins with no credentials, path, query, or fragment.");
360
+ if (url == null || !isGatewayUrl(url)) throw new TypeError("FEP-ef61 gateways for compatible identifiers must be HTTP(S) origins with no credentials, path, query, or fragment.");
359
361
  return url;
360
362
  }
361
363
  function getRawPortableIri(portableId) {
@@ -381,6 +383,166 @@ function isLocationHint(pair) {
381
383
  }
382
384
  }
383
385
  /**
386
+ * The name of the FEP-ef61 location hint query parameter.
387
+ * @internal
388
+ */
389
+ const GATEWAY_HINT_PARAMETER = "@gateway";
390
+ /**
391
+ * Parses an FEP-ef61 gateway, which has to be an HTTP(S) origin with no
392
+ * credentials, path, query, or fragment.
393
+ * @returns The gateway, or `null` if it is not a valid gateway.
394
+ * @internal
395
+ */
396
+ function parseGatewayOrigin(gateway) {
397
+ let url;
398
+ if (gateway instanceof URL) url = new URL(gateway.href);
399
+ else if (typeof gateway === "string" && URL.canParse(gateway)) url = new URL(gateway);
400
+ else return null;
401
+ return isGatewayUrl(url) ? url : null;
402
+ }
403
+ /**
404
+ * Returns a copy of an [FEP-ef61] portable ActivityPub URI with `@gateway`
405
+ * location hints for the given gateways, which tell consumers where they can
406
+ * retrieve the object. Put hints on *references* to portable actors, e.g.,
407
+ * in `actor`, `attributedTo`, `to`, or `cc`, when constructing an object, as
408
+ * FEP-ef61 recommends:
409
+ *
410
+ * ~~~~ typescript
411
+ * withGatewayHints("ap://did:key:z6Mk.../actor", [
412
+ * "https://server1.example",
413
+ * "https://server2.example",
414
+ * ]);
415
+ * // ap+ef61://did:key:z6Mk.../actor?@gateway=https%3A%2F%2Fserver1.example&@gateway=https%3A%2F%2Fserver2.example
416
+ * ~~~~
417
+ *
418
+ * Do not put hints on an object's own `id`. Hints do not change the
419
+ * identity of a portable URI, since FEP-ef61 drops the query when comparing
420
+ * portable URIs, but implementations that do not canonicalize portable URIs
421
+ * would take a hinted ID for another object. Add hints before signing the
422
+ * object, since its Object Integrity Proof covers its references too.
423
+ *
424
+ * The hints that the URI already has, including the legacy `gateways`
425
+ * parameter, are replaced. Each gateway becomes a `@gateway` query
426
+ * parameter whose value is its URI-encoded origin, e.g.,
427
+ * `@gateway=https%3A%2F%2Fserver1.example`, in the given order after the
428
+ * other query parameters. Duplicate gateways are dropped, and an empty list
429
+ * removes the hints as {@link withoutGatewayHints} does. The other query
430
+ * parameters, their order, and the fragment are kept, but percent-encoding
431
+ * is normalized the same way as {@link canonicalizePortableUri} does it.
432
+ *
433
+ * Fedify follows at most five hints when dereferencing a portable URI
434
+ * (three for the key ID of an HTTP Signature), so list the preferred
435
+ * gateways first; there is no limit on the number of hints added here.
436
+ *
437
+ * [FEP-ef61]: https://w3id.org/fep/ef61
438
+ *
439
+ * @param portableId The `ap:` or `ap+ef61:` URI. Pass the raw string rather
440
+ * than a `URL` if its path may have `.` or `..` segments,
441
+ * because the `URL` class resolves them.
442
+ * @param gateways The gateways, e.g., the `gateways` of the actor that the
443
+ * URI refers to. Each has to be an HTTP(S) origin with no
444
+ * credentials, path, query, or fragment.
445
+ * @returns The portable URI with the hints, in the same internal `URL` form
446
+ * as {@link parseIri} returns. Use {@link formatIri} to get its
447
+ * canonical string.
448
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
449
+ * URI, e.g., it is a compatible identifier, which must
450
+ * not have location hints; if its path has `.` or `..`
451
+ * segments, which the `URL` class cannot represent; or
452
+ * if a gateway is invalid.
453
+ * @since 2.4.0
454
+ */
455
+ function withGatewayHints(portableId, gateways) {
456
+ const parts = splitPortableIri(portableId);
457
+ if (typeof gateways === "string") throw new TypeError("The gateways must be an iterable of gateways, not a string.");
458
+ const hints = [];
459
+ const seen = /* @__PURE__ */ new Set();
460
+ for (const gateway of gateways) {
461
+ const url = parseGatewayOrigin(gateway);
462
+ if (url == null) throw new TypeError("FEP-ef61 gateways must be HTTP(S) origins with no credentials, path, query, or fragment: " + String(gateway));
463
+ if (seen.has(url.href)) continue;
464
+ seen.add(url.href);
465
+ hints.push(url);
466
+ }
467
+ return replaceGatewayHints(parts, hints);
468
+ }
469
+ /**
470
+ * Returns a copy of an [FEP-ef61] portable ActivityPub URI without its
471
+ * location hints, i.e., `@gateway` query parameters and the legacy
472
+ * `gateways` parameter. The other query parameters, their order, and the
473
+ * fragment are kept, but percent-encoding is normalized the same way as
474
+ * {@link canonicalizePortableUri} does it.
475
+ *
476
+ * [FEP-ef61]: https://w3id.org/fep/ef61
477
+ *
478
+ * @param portableId The `ap:` or `ap+ef61:` URI. Pass the raw string rather
479
+ * than a `URL` if its path may have `.` or `..` segments,
480
+ * because the `URL` class resolves them.
481
+ * @returns The portable URI without the hints, in the same internal `URL`
482
+ * form as {@link parseIri} returns.
483
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
484
+ * URI, or if its path has `.` or `..` segments, which
485
+ * the `URL` class cannot represent.
486
+ * @since 2.4.0
487
+ */
488
+ function withoutGatewayHints(portableId) {
489
+ return replaceGatewayHints(splitPortableIri(portableId), []);
490
+ }
491
+ /**
492
+ * Gets the gateways in the `@gateway` location hints of an [FEP-ef61]
493
+ * portable ActivityPub URI, in order. Hints that are not valid gateways,
494
+ * i.e., HTTP(S) origins with no credentials, path, query, or fragment, are
495
+ * skipped, and so are duplicates. The legacy `gateways` parameter is not
496
+ * read.
497
+ *
498
+ * Unlike Fedify's dereferencing, which follows at most five hints, this
499
+ * returns all of them.
500
+ *
501
+ * [FEP-ef61]: https://w3id.org/fep/ef61
502
+ *
503
+ * @param portableId The `ap:` or `ap+ef61:` URI.
504
+ * @returns The gateways, e.g., `https://server1.example/`.
505
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
506
+ * URI.
507
+ * @since 2.4.0
508
+ */
509
+ function getGatewayHints(portableId) {
510
+ const { query } = splitPortableIri(portableId);
511
+ return query == null ? [] : parseGatewayHints(query);
512
+ }
513
+ function splitPortableIri(portableId) {
514
+ const raw = getRawPortableIri(portableId);
515
+ const match = raw.match(PORTABLE_IRI_PATTERN);
516
+ const parsed = parsePortableIri(raw);
517
+ if (match == null || parsed == null) throw new TypeError("Invalid portable ActivityPub IRI.");
518
+ return {
519
+ raw,
520
+ parsed,
521
+ path: normalizePortableComponent(match[3]),
522
+ query: match[4] == null ? null : normalizePortableComponent(match[4].slice(1)),
523
+ fragment: match[5] == null ? "" : normalizePortableComponent(match[5])
524
+ };
525
+ }
526
+ function parseGatewayHints(query) {
527
+ const hints = [];
528
+ const seen = /* @__PURE__ */ new Set();
529
+ for (const hint of new URLSearchParams(`?${query}`).getAll(GATEWAY_HINT_PARAMETER)) {
530
+ const url = parseGatewayOrigin(hint);
531
+ if (url == null || seen.has(url.href)) continue;
532
+ seen.add(url.href);
533
+ hints.push(url);
534
+ }
535
+ return hints;
536
+ }
537
+ function replaceGatewayHints(parts, hints) {
538
+ const pairs = parts.query == null ? [] : parts.query.split("&").filter((pair) => pair !== "" && !isLocationHint(pair));
539
+ for (const hint of hints) pairs.push(`${GATEWAY_HINT_PARAMETER}=${encodeURIComponent(hint.origin)}`);
540
+ const query = pairs.length < 1 ? "" : `?${pairs.join("&")}`;
541
+ const result = parsePortableIri(`ap+ef61://${parts.parsed.host}${parts.path}${query}${parts.fragment}`);
542
+ 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.");
543
+ return result;
544
+ }
545
+ /**
384
546
  * Validates a URL to prevent SSRF attacks.
385
547
  */
386
548
  async function validatePublicUrl(url) {
@@ -576,4 +738,4 @@ function matchesIPv6Prefix(address, prefixWords, prefixLength) {
576
738
  return true;
577
739
  }
578
740
  //#endregion
579
- export { validateLookupAddresses as _, formatIri as a, haveSameFe34Origin as c, isValidPublicIPv4Address as d, isValidPublicIPv6Address as f, toCompatibleEf61Id as g, parseJsonLdId as h, expandIPv6Address as i, haveSameIriOrigin as l, parseIri as m, arePortableUrisEqual as n, fromCompatibleEf61Id as o, parseGatewayUrl as p, canonicalizePortableUri as r, getFe34Origin as s, UrlError as t, isGatewayUrl as u, validatePublicUrl as v };
741
+ export { withoutGatewayHints as C, withGatewayHints as S, parseIri as _, expandIPv6Address as a, validateLookupAddresses as b, getFe34Origin as c, haveSameIriOrigin as d, isGatewayUrl as f, parseGatewayUrl as g, parseGatewayOrigin as h, canonicalizePortableUri as i, getGatewayHints as l, isValidPublicIPv6Address as m, UrlError as n, formatIri as o, isValidPublicIPv4Address as p, arePortableUrisEqual as r, fromCompatibleEf61Id as s, GATEWAY_HINT_PARAMETER as t, haveSameFe34Origin as u, parseJsonLdId as v, validatePublicUrl as x, toCompatibleEf61Id as y };
@@ -220,9 +220,10 @@ function parseAtUri(uri) {
220
220
  }
221
221
  /**
222
222
  * Checks whether the URL is an FEP-ef61 gateway base URI.
223
+ * @since 2.4.0
223
224
  */
224
225
  function isGatewayUrl(url) {
225
- return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "" && url.pathname === "/" && url.search === "" && url.hash === "";
226
+ return (url.protocol === "http:" || url.protocol === "https:") && url.href === `${url.origin}/`;
226
227
  }
227
228
  /**
228
229
  * Parses and validates an FEP-ef61 gateway base URI.
@@ -230,6 +231,7 @@ function isGatewayUrl(url) {
230
231
  * with no credentials, path, query, or fragment. In the
231
232
  * latter case, the message starts with
232
233
  * `Invalid FEP-ef61 gateway:`.
234
+ * @since 2.4.0
233
235
  */
234
236
  function parseGatewayUrl(url) {
235
237
  const parsed = parseIri(url);
@@ -357,7 +359,7 @@ function toCompatibleEf61Id(portableId, gateway) {
357
359
  }
358
360
  function parseCompatibleEf61Gateway(gateway) {
359
361
  const url = gateway instanceof URL ? gateway : typeof gateway === "string" && URL.canParse(gateway) ? new URL(gateway) : null;
360
- if (url == null || url.protocol !== "http:" && url.protocol !== "https:" || url.href !== `${url.origin}/`) throw new TypeError("FEP-ef61 gateways for compatible identifiers must be HTTP(S) origins with no credentials, path, query, or fragment.");
362
+ if (url == null || !isGatewayUrl(url)) throw new TypeError("FEP-ef61 gateways for compatible identifiers must be HTTP(S) origins with no credentials, path, query, or fragment.");
361
363
  return url;
362
364
  }
363
365
  function getRawPortableIri(portableId) {
@@ -383,6 +385,166 @@ function isLocationHint(pair) {
383
385
  }
384
386
  }
385
387
  /**
388
+ * The name of the FEP-ef61 location hint query parameter.
389
+ * @internal
390
+ */
391
+ const GATEWAY_HINT_PARAMETER = "@gateway";
392
+ /**
393
+ * Parses an FEP-ef61 gateway, which has to be an HTTP(S) origin with no
394
+ * credentials, path, query, or fragment.
395
+ * @returns The gateway, or `null` if it is not a valid gateway.
396
+ * @internal
397
+ */
398
+ function parseGatewayOrigin(gateway) {
399
+ let url;
400
+ if (gateway instanceof URL) url = new URL(gateway.href);
401
+ else if (typeof gateway === "string" && URL.canParse(gateway)) url = new URL(gateway);
402
+ else return null;
403
+ return isGatewayUrl(url) ? url : null;
404
+ }
405
+ /**
406
+ * Returns a copy of an [FEP-ef61] portable ActivityPub URI with `@gateway`
407
+ * location hints for the given gateways, which tell consumers where they can
408
+ * retrieve the object. Put hints on *references* to portable actors, e.g.,
409
+ * in `actor`, `attributedTo`, `to`, or `cc`, when constructing an object, as
410
+ * FEP-ef61 recommends:
411
+ *
412
+ * ~~~~ typescript
413
+ * withGatewayHints("ap://did:key:z6Mk.../actor", [
414
+ * "https://server1.example",
415
+ * "https://server2.example",
416
+ * ]);
417
+ * // ap+ef61://did:key:z6Mk.../actor?@gateway=https%3A%2F%2Fserver1.example&@gateway=https%3A%2F%2Fserver2.example
418
+ * ~~~~
419
+ *
420
+ * Do not put hints on an object's own `id`. Hints do not change the
421
+ * identity of a portable URI, since FEP-ef61 drops the query when comparing
422
+ * portable URIs, but implementations that do not canonicalize portable URIs
423
+ * would take a hinted ID for another object. Add hints before signing the
424
+ * object, since its Object Integrity Proof covers its references too.
425
+ *
426
+ * The hints that the URI already has, including the legacy `gateways`
427
+ * parameter, are replaced. Each gateway becomes a `@gateway` query
428
+ * parameter whose value is its URI-encoded origin, e.g.,
429
+ * `@gateway=https%3A%2F%2Fserver1.example`, in the given order after the
430
+ * other query parameters. Duplicate gateways are dropped, and an empty list
431
+ * removes the hints as {@link withoutGatewayHints} does. The other query
432
+ * parameters, their order, and the fragment are kept, but percent-encoding
433
+ * is normalized the same way as {@link canonicalizePortableUri} does it.
434
+ *
435
+ * Fedify follows at most five hints when dereferencing a portable URI
436
+ * (three for the key ID of an HTTP Signature), so list the preferred
437
+ * gateways first; there is no limit on the number of hints added here.
438
+ *
439
+ * [FEP-ef61]: https://w3id.org/fep/ef61
440
+ *
441
+ * @param portableId The `ap:` or `ap+ef61:` URI. Pass the raw string rather
442
+ * than a `URL` if its path may have `.` or `..` segments,
443
+ * because the `URL` class resolves them.
444
+ * @param gateways The gateways, e.g., the `gateways` of the actor that the
445
+ * URI refers to. Each has to be an HTTP(S) origin with no
446
+ * credentials, path, query, or fragment.
447
+ * @returns The portable URI with the hints, in the same internal `URL` form
448
+ * as {@link parseIri} returns. Use {@link formatIri} to get its
449
+ * canonical string.
450
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
451
+ * URI, e.g., it is a compatible identifier, which must
452
+ * not have location hints; if its path has `.` or `..`
453
+ * segments, which the `URL` class cannot represent; or
454
+ * if a gateway is invalid.
455
+ * @since 2.4.0
456
+ */
457
+ function withGatewayHints(portableId, gateways) {
458
+ const parts = splitPortableIri(portableId);
459
+ if (typeof gateways === "string") throw new TypeError("The gateways must be an iterable of gateways, not a string.");
460
+ const hints = [];
461
+ const seen = /* @__PURE__ */ new Set();
462
+ for (const gateway of gateways) {
463
+ const url = parseGatewayOrigin(gateway);
464
+ if (url == null) throw new TypeError("FEP-ef61 gateways must be HTTP(S) origins with no credentials, path, query, or fragment: " + String(gateway));
465
+ if (seen.has(url.href)) continue;
466
+ seen.add(url.href);
467
+ hints.push(url);
468
+ }
469
+ return replaceGatewayHints(parts, hints);
470
+ }
471
+ /**
472
+ * Returns a copy of an [FEP-ef61] portable ActivityPub URI without its
473
+ * location hints, i.e., `@gateway` query parameters and the legacy
474
+ * `gateways` parameter. The other query parameters, their order, and the
475
+ * fragment are kept, but percent-encoding is normalized the same way as
476
+ * {@link canonicalizePortableUri} does it.
477
+ *
478
+ * [FEP-ef61]: https://w3id.org/fep/ef61
479
+ *
480
+ * @param portableId The `ap:` or `ap+ef61:` URI. Pass the raw string rather
481
+ * than a `URL` if its path may have `.` or `..` segments,
482
+ * because the `URL` class resolves them.
483
+ * @returns The portable URI without the hints, in the same internal `URL`
484
+ * form as {@link parseIri} returns.
485
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
486
+ * URI, or if its path has `.` or `..` segments, which
487
+ * the `URL` class cannot represent.
488
+ * @since 2.4.0
489
+ */
490
+ function withoutGatewayHints(portableId) {
491
+ return replaceGatewayHints(splitPortableIri(portableId), []);
492
+ }
493
+ /**
494
+ * Gets the gateways in the `@gateway` location hints of an [FEP-ef61]
495
+ * portable ActivityPub URI, in order. Hints that are not valid gateways,
496
+ * i.e., HTTP(S) origins with no credentials, path, query, or fragment, are
497
+ * skipped, and so are duplicates. The legacy `gateways` parameter is not
498
+ * read.
499
+ *
500
+ * Unlike Fedify's dereferencing, which follows at most five hints, this
501
+ * returns all of them.
502
+ *
503
+ * [FEP-ef61]: https://w3id.org/fep/ef61
504
+ *
505
+ * @param portableId The `ap:` or `ap+ef61:` URI.
506
+ * @returns The gateways, e.g., `https://server1.example/`.
507
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
508
+ * URI.
509
+ * @since 2.4.0
510
+ */
511
+ function getGatewayHints(portableId) {
512
+ const { query } = splitPortableIri(portableId);
513
+ return query == null ? [] : parseGatewayHints(query);
514
+ }
515
+ function splitPortableIri(portableId) {
516
+ const raw = getRawPortableIri(portableId);
517
+ const match = raw.match(PORTABLE_IRI_PATTERN);
518
+ const parsed = parsePortableIri(raw);
519
+ if (match == null || parsed == null) throw new TypeError("Invalid portable ActivityPub IRI.");
520
+ return {
521
+ raw,
522
+ parsed,
523
+ path: normalizePortableComponent(match[3]),
524
+ query: match[4] == null ? null : normalizePortableComponent(match[4].slice(1)),
525
+ fragment: match[5] == null ? "" : normalizePortableComponent(match[5])
526
+ };
527
+ }
528
+ function parseGatewayHints(query) {
529
+ const hints = [];
530
+ const seen = /* @__PURE__ */ new Set();
531
+ for (const hint of new URLSearchParams(`?${query}`).getAll(GATEWAY_HINT_PARAMETER)) {
532
+ const url = parseGatewayOrigin(hint);
533
+ if (url == null || seen.has(url.href)) continue;
534
+ seen.add(url.href);
535
+ hints.push(url);
536
+ }
537
+ return hints;
538
+ }
539
+ function replaceGatewayHints(parts, hints) {
540
+ const pairs = parts.query == null ? [] : parts.query.split("&").filter((pair) => pair !== "" && !isLocationHint(pair));
541
+ for (const hint of hints) pairs.push(`${GATEWAY_HINT_PARAMETER}=${encodeURIComponent(hint.origin)}`);
542
+ const query = pairs.length < 1 ? "" : `?${pairs.join("&")}`;
543
+ const result = parsePortableIri(`ap+ef61://${parts.parsed.host}${parts.path}${query}${parts.fragment}`);
544
+ 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.");
545
+ return result;
546
+ }
547
+ /**
386
548
  * Validates a URL to prevent SSRF attacks.
387
549
  */
388
550
  async function validatePublicUrl(url) {
@@ -578,6 +740,12 @@ function matchesIPv6Prefix(address, prefixWords, prefixLength) {
578
740
  return true;
579
741
  }
580
742
  //#endregion
743
+ Object.defineProperty(exports, "GATEWAY_HINT_PARAMETER", {
744
+ enumerable: true,
745
+ get: function() {
746
+ return GATEWAY_HINT_PARAMETER;
747
+ }
748
+ });
581
749
  Object.defineProperty(exports, "UrlError", {
582
750
  enumerable: true,
583
751
  get: function() {
@@ -620,6 +788,12 @@ Object.defineProperty(exports, "getFe34Origin", {
620
788
  return getFe34Origin;
621
789
  }
622
790
  });
791
+ Object.defineProperty(exports, "getGatewayHints", {
792
+ enumerable: true,
793
+ get: function() {
794
+ return getGatewayHints;
795
+ }
796
+ });
623
797
  Object.defineProperty(exports, "haveSameFe34Origin", {
624
798
  enumerable: true,
625
799
  get: function() {
@@ -650,6 +824,12 @@ Object.defineProperty(exports, "isValidPublicIPv6Address", {
650
824
  return isValidPublicIPv6Address;
651
825
  }
652
826
  });
827
+ Object.defineProperty(exports, "parseGatewayOrigin", {
828
+ enumerable: true,
829
+ get: function() {
830
+ return parseGatewayOrigin;
831
+ }
832
+ });
653
833
  Object.defineProperty(exports, "parseGatewayUrl", {
654
834
  enumerable: true,
655
835
  get: function() {
@@ -686,3 +866,15 @@ Object.defineProperty(exports, "validatePublicUrl", {
686
866
  return validatePublicUrl;
687
867
  }
688
868
  });
869
+ Object.defineProperty(exports, "withGatewayHints", {
870
+ enumerable: true,
871
+ get: function() {
872
+ return withGatewayHints;
873
+ }
874
+ });
875
+ Object.defineProperty(exports, "withoutGatewayHints", {
876
+ enumerable: true,
877
+ get: function() {
878
+ return withoutGatewayHints;
879
+ }
880
+ });