@fedify/fedify 2.4.0-dev.2084 → 2.4.0-dev.2093

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 (75) hide show
  1. package/dist/{builder-BGlpUocK.mjs → builder-Ld4Y9ymR.mjs} +34 -3
  2. package/dist/compat/transformers.test.mjs +1 -1
  3. package/dist/{compound-proof-B6Aj2kGY.mjs → compound-proof-CTx_lewQ.mjs} +191 -13
  4. package/dist/{deno-Dn-py9Zy.mjs → deno-8t8k_6Yv.mjs} +1 -1
  5. package/dist/{docloader-BUq5bYJr.mjs → docloader-DJKcsAiF.mjs} +2 -2
  6. package/dist/federation/builder.test.mjs +1 -1
  7. package/dist/federation/compound-fanout.test.mjs +3 -3
  8. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  9. package/dist/federation/handler.test.mjs +5 -5
  10. package/dist/federation/hashlink-media.test.mjs +2 -2
  11. package/dist/federation/idempotency.test.mjs +2 -2
  12. package/dist/federation/metrics.test.mjs +1 -1
  13. package/dist/federation/middleware.test.mjs +7 -7
  14. package/dist/federation/mod.cjs +1 -1
  15. package/dist/federation/mod.js +1 -1
  16. package/dist/federation/outgoing-proof.test.mjs +254 -5
  17. package/dist/federation/portable-gateway-keys.test.mjs +72 -11
  18. package/dist/federation/portable-inbox.test.mjs +116 -4
  19. package/dist/federation/portable.test.mjs +182 -4
  20. package/dist/federation/send.test.mjs +3 -3
  21. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  22. package/dist/federation/tasks/tasks.test.mjs +2 -2
  23. package/dist/federation/temporal.test.mjs +1 -1
  24. package/dist/federation/webfinger.test.mjs +89 -1
  25. package/dist/{federation-BLGx8JUH.cjs → federation-aSHO04ob.cjs} +360 -65
  26. package/dist/{http-B0VRNipE.mjs → http-B2ftY0Ha.mjs} +4 -4
  27. package/dist/{http-B5q1CHtL.cjs → http-DSaSAN7X.cjs} +1609 -1457
  28. package/dist/{http-4PCCvhsU.js → http-DyT7DP1Y.js} +1518 -1390
  29. package/dist/{key-BJrp524f.mjs → key-Dwm4VswT.mjs} +39 -99
  30. package/dist/{kv-cache-Dhhhrryv.cjs → kv-cache-BSJAlP4C.cjs} +1 -1
  31. package/dist/{kv-cache-C4C3LyYW.mjs → kv-cache-D0yYukqk.mjs} +1 -1
  32. package/dist/{kv-cache-1KUmAPH7.js → kv-cache-DmroAz3M.js} +1 -1
  33. package/dist/{ld-BmFz5NLA.mjs → ld-BYZOoDzZ.mjs} +3 -3
  34. package/dist/{metrics-Beod6tzO.mjs → metrics-B7YwKTFr.mjs} +1 -1
  35. package/dist/{middleware-B7CNsyqt.mjs → middleware-BNeqKSsL.mjs} +1 -1
  36. package/dist/{middleware-D_9pNYe9.js → middleware-Blq0882d.js} +368 -73
  37. package/dist/{middleware-BKeQYPm0.mjs → middleware-E_0IH72m.mjs} +166 -71
  38. package/dist/{mod-HDSvS-ww.d.cts → mod-C8LieBzF.d.cts} +16 -3
  39. package/dist/{mod-C9WSpZWm.d.ts → mod-Coacostr.d.ts} +16 -3
  40. package/dist/mod.cjs +5 -5
  41. package/dist/mod.d.cts +1 -1
  42. package/dist/mod.d.ts +1 -1
  43. package/dist/mod.js +5 -5
  44. package/dist/nodeinfo/handler.test.mjs +1 -1
  45. package/dist/{owner-CsKVgMzk.mjs → owner-Cgx2Dc7I.mjs} +5 -4
  46. package/dist/{owner-_pbbgLJK.js → owner-DAGSC5gX.js} +2 -2
  47. package/dist/{owner-BQFZtFV4.cjs → owner-szIZl4qI.cjs} +2 -2
  48. package/dist/{portable-key-B4R-CiIV.mjs → portable-key-Cn1WJSN1.mjs} +60 -41
  49. package/dist/portable-key-id-Bva5iT3W.mjs +136 -0
  50. package/dist/{proof-ozG4HfcQ.mjs → proof-DVXC6nYz.mjs} +59 -18
  51. package/dist/{send-ClPt8nYu.mjs → send-D-hOvYKy.mjs} +3 -3
  52. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  53. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  54. package/dist/sig/compound-proof-verification.test.mjs +160 -3
  55. package/dist/sig/compound-proof.test.mjs +2 -2
  56. package/dist/sig/http.test.mjs +2 -2
  57. package/dist/sig/key.test.mjs +1 -1
  58. package/dist/sig/ld.test.mjs +2 -2
  59. package/dist/sig/mod.cjs +3 -3
  60. package/dist/sig/mod.d.cts +1 -1
  61. package/dist/sig/mod.d.ts +1 -1
  62. package/dist/sig/mod.js +3 -3
  63. package/dist/sig/owner.test.mjs +1 -1
  64. package/dist/sig/portable-collection.test.mjs +69 -5
  65. package/dist/sig/portable-key.test.mjs +176 -10
  66. package/dist/sig/proof.test.mjs +107 -4
  67. package/dist/{sig-DhdomAN0.js → sig-B5Ell7pO.js} +19 -3
  68. package/dist/{sig-k3C8sF7g.cjs → sig-CXOKN4Br.cjs} +19 -3
  69. package/dist/{temporal-Ba0EAZAI.mjs → temporal-CqpKGEPk.mjs} +1 -1
  70. package/dist/testing/kv.test.mjs +1 -1
  71. package/dist/utils/docloader.test.mjs +2 -2
  72. package/dist/utils/kv-cache.test.mjs +1 -1
  73. package/dist/utils/mod.cjs +1 -1
  74. package/dist/utils/mod.js +1 -1
  75. package/package.json +7 -7
@@ -1,16 +1,17 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  const require_rolldown_runtime = require("./rolldown-runtime-Dmm3t92W.cjs");
3
3
  const require_transformers = require("./transformers-BalCrSsr.cjs");
4
- const require_http = require("./http-B5q1CHtL.cjs");
4
+ const require_http = require("./http-DSaSAN7X.cjs");
5
5
  const require_types = require("./types-Be2zbyeG.cjs");
6
- const require_owner = require("./owner-BQFZtFV4.cjs");
7
- const require_kv_cache = require("./kv-cache-Dhhhrryv.cjs");
6
+ const require_owner = require("./owner-szIZl4qI.cjs");
7
+ const require_kv_cache = require("./kv-cache-BSJAlP4C.cjs");
8
8
  let _logtape_logtape = require("@logtape/logtape");
9
9
  let _fedify_uri_template = require("@fedify/uri-template");
10
10
  let _fedify_vocab = require("@fedify/vocab");
11
11
  let _opentelemetry_api = require("@opentelemetry/api");
12
- let byte_encodings_hex = require("byte-encodings/hex");
13
12
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
13
+ let _fedify_vocab_runtime_internal_portable_dereference = require("@fedify/vocab-runtime/internal/portable-dereference");
14
+ let byte_encodings_hex = require("byte-encodings/hex");
14
15
  let _fedify_vocab_runtime_jsonld = require("@fedify/vocab-runtime/jsonld");
15
16
  _fedify_vocab_runtime_jsonld = require_rolldown_runtime.__toESM(_fedify_vocab_runtime_jsonld, 1);
16
17
  let es_toolkit = require("es-toolkit");
@@ -205,10 +206,11 @@ var FederationBuilderImpl = class {
205
206
  "federation",
206
207
  "actor"
207
208
  ]);
208
- const portable = actor.id?.protocol === "ap:" || actor.id?.protocol === "ap+ef61:";
209
+ const portable = actor.id != null && require_http.isPortableId(actor.id);
209
210
  if (actor.id == null) logger.warn("Actor dispatcher returned an actor without an id property. Set the property with Context.getActorUri(identifier).");
210
211
  else if (!portable && actor.id.href != context.getActorUri(identifier).href) logger.warn("Actor dispatcher returned an actor with an id property that does not match the actor URI. Set the property with Context.getActorUri(identifier).");
211
212
  if (actor instanceof _fedify_vocab.Tombstone) return actor;
213
+ if (actor.id != null && (0, _fedify_vocab_runtime_internal_portable_dereference.isCompatibleEf61Iri)(actor.id)) warnCompatibleActorId(actor.id, actor.gateway);
212
214
  if (this.followingCallbacks != null && this.followingCallbacks.dispatcher != null) {
213
215
  if (actor.followingId == null) logger.warn("You configured a following collection dispatcher, but the actor does not have a following property. Set the property with Context.getFollowingUri(identifier).");
214
216
  else if (!portable && actor.followingId.href != context.getFollowingUri(identifier).href) logger.warn("You configured a following collection dispatcher, but the actor's following property does not match the following collection URI. Set the property with Context.getFollowingUri(identifier).");
@@ -733,6 +735,33 @@ var FederationBuilderImpl = class {
733
735
  function createFederationBuilder() {
734
736
  return new FederationBuilderImpl();
735
737
  }
738
+ /**
739
+ * Warns about an FEP-ef61 portable actor identified by a compatible identifier
740
+ * that is malformed, or is not on the actor's first gateway, where FEP-ef61
741
+ * requires publishers to construct compatible identifiers.
742
+ */
743
+ function warnCompatibleActorId(actorId, gateway) {
744
+ const logger = (0, _logtape_logtape.getLogger)([
745
+ "fedify",
746
+ "federation",
747
+ "actor"
748
+ ]);
749
+ try {
750
+ (0, _fedify_vocab_runtime.fromCompatibleEf61Id)(actorId);
751
+ } catch (error) {
752
+ if (!(error instanceof TypeError)) throw error;
753
+ logger.warn("Actor dispatcher returned an actor whose id property, {actorId}, is a malformed FEP-ef61 compatible identifier: {error}", {
754
+ actorId: actorId.href,
755
+ error
756
+ });
757
+ return;
758
+ }
759
+ if (gateway == null || !(0, _fedify_vocab_runtime.isGatewayUrl)(gateway)) logger.warn("Actor dispatcher returned an actor whose id property, {actorId}, is an FEP-ef61 compatible identifier, but whose first gateway is not an HTTP(S) origin. Set the gateways property, and construct the compatible identifier with its first gateway.", { actorId: actorId.href });
760
+ else if (actorId.origin !== gateway.origin) logger.warn("Actor dispatcher returned an actor whose id property, {actorId}, is an FEP-ef61 compatible identifier on another gateway than its first gateway, {gateway}. FEP-ef61 requires publishers to construct compatible identifiers with the first gateway in the actor's gateways.", {
761
+ actorId: actorId.href,
762
+ gateway: gateway.origin
763
+ });
764
+ }
736
765
  //#endregion
737
766
  //#region src/federation/circuit-breaker.ts
738
767
  const Temporal$11 = require_http.Temporal;
@@ -1310,7 +1339,14 @@ async function buildCollectionSynchronizationHeader(collectionId, actorIds) {
1310
1339
  //#endregion
1311
1340
  //#region src/sig/compound-proof.ts
1312
1341
  const textEncoder = new TextEncoder();
1313
- const PORTABLE_OBJECT_ID_PATTERN = /^ap(?:\+ef61)?:\/\//i;
1342
+ /**
1343
+ * Checks whether a literal `id` or `@id` value makes a map a portable object:
1344
+ * an `ap:` or `ap+ef61:` URI, or anything that looks like an FEP-ef61
1345
+ * compatible identifier, even a malformed one, which then fails the policy.
1346
+ */
1347
+ function isPortableIdValue(value) {
1348
+ return typeof value === "string" && require_http.isPortableId(value);
1349
+ }
1314
1350
  function isJsonMap(value) {
1315
1351
  return typeof value === "object" && value != null && !Array.isArray(value);
1316
1352
  }
@@ -1556,18 +1592,20 @@ function collectPortableObjects(snapshot) {
1556
1592
  for (let index = current.value.length - 1; index >= 0; index--) pending.push({
1557
1593
  value: current.value[index],
1558
1594
  path: childPath(current.path, String(index)),
1559
- depth: current.depth + 1
1595
+ depth: current.depth + 1,
1596
+ keyOf: current.keyOf
1560
1597
  });
1561
1598
  continue;
1562
1599
  }
1563
1600
  if (!isCompoundProofJsonObject(current.value)) continue;
1564
1601
  const document = current.value;
1565
- const id = [document.id, document["@id"]].find((value) => typeof value === "string" && PORTABLE_OBJECT_ID_PATTERN.test(value));
1602
+ const id = [document.id, document["@id"]].find(isPortableIdValue);
1566
1603
  if (typeof id === "string") objects.push(Object.freeze({
1567
1604
  path: current.path,
1568
1605
  id,
1569
1606
  depth: current.depth,
1570
- document
1607
+ document,
1608
+ ...current.keyOf == null ? {} : { keyOf: current.keyOf }
1571
1609
  }));
1572
1610
  const entries = Object.entries(document).sort(([left], [right]) => comparePaths(left, right));
1573
1611
  for (let index = entries.length - 1; index >= 0; index--) {
@@ -1576,7 +1614,11 @@ function collectPortableObjects(snapshot) {
1576
1614
  pending.push({
1577
1615
  value,
1578
1616
  path: childPath(current.path, key),
1579
- depth: current.depth + 1
1617
+ depth: current.depth + 1,
1618
+ ...typeof id === "string" && (key === "publicKey" || key === "assertionMethod") ? { keyOf: {
1619
+ path: current.path,
1620
+ member: key
1621
+ } } : {}
1580
1622
  });
1581
1623
  }
1582
1624
  }
@@ -1584,8 +1626,9 @@ function collectPortableObjects(snapshot) {
1584
1626
  return objects;
1585
1627
  }
1586
1628
  /**
1587
- * Determines whether a received JSON tree contains a portable object ID
1588
- * outside a proof or context definition, without exceeding discovery limits.
1629
+ * Determines whether a received JSON tree contains a portable object ID, i.e.,
1630
+ * an `ap:` or `ap+ef61:` URI or an FEP-ef61 compatible identifier, outside
1631
+ * a proof or context definition, without exceeding discovery limits.
1589
1632
  *
1590
1633
  * This is only an applicability check. It does not authenticate the input;
1591
1634
  * callers must still use {@link verifyCompoundPortableObjectProofs} to obtain
@@ -1632,7 +1675,7 @@ function inspectCompoundPortableObjectApplicability(json, limits) {
1632
1675
  } else if (isJsonMap(value)) {
1633
1676
  mapCount++;
1634
1677
  if (mapCount > limits.maxMaps) return "indeterminate";
1635
- if ([value.id, value["@id"]].some((id) => typeof id === "string" && PORTABLE_OBJECT_ID_PATTERN.test(id))) return "present";
1678
+ if ([value.id, value["@id"]].some(isPortableIdValue)) return "present";
1636
1679
  if (!addByteLength(2)) return "indeterminate";
1637
1680
  const children = function* () {
1638
1681
  let first = true;
@@ -1695,7 +1738,8 @@ function findInJsonMaps(json, visit) {
1695
1738
  /**
1696
1739
  * Determines whether an outgoing JSON tree contains portable content that
1697
1740
  * requires producer-side proof-shape checks: a map, outside proof and context
1698
- * values, whose direct `id` or `@id` is a portable ActivityPub URI.
1741
+ * values, whose direct `id` or `@id` is a portable ActivityPub URI or an
1742
+ * FEP-ef61 compatible identifier.
1699
1743
  *
1700
1744
  * This deliberately differs from
1701
1745
  * {@link inspectCompoundPortableObjectApplicability}: it applies no resource
@@ -1708,7 +1752,7 @@ function findInJsonMaps(json, visit) {
1708
1752
  * @internal
1709
1753
  */
1710
1754
  function containsCompoundPortableObject(json) {
1711
- return findInJsonMaps(json, (map) => [map.id, map["@id"]].some((id) => typeof id === "string" && PORTABLE_OBJECT_ID_PATTERN.test(id)) ? true : void 0) ?? false;
1755
+ return findInJsonMaps(json, (map) => [map.id, map["@id"]].some(isPortableIdValue) ? true : void 0) ?? false;
1712
1756
  }
1713
1757
  /**
1714
1758
  * Finds the first direct `proof` member whose value the map-local
@@ -1805,7 +1849,7 @@ async function verifyCompoundPortableObjectProofs(json, limits, options = {}) {
1805
1849
  if (discovered.status !== "ok") return discovered;
1806
1850
  const proofs = await verifyDiscoveredProofDocuments(discovered, options);
1807
1851
  const proofByPath = new Map(proofs.map((proof) => [proof.path, proof]));
1808
- const portableObjects = await Promise.all(collectPortableObjects(discovered.snapshot).map(async (object) => {
1852
+ const verifyPortableObject = async (object) => {
1809
1853
  const metadata = {
1810
1854
  path: object.path,
1811
1855
  id: object.id,
@@ -1837,16 +1881,174 @@ async function verifyCompoundPortableObjectProofs(json, limits, options = {}) {
1837
1881
  reason: { type: "invalidPortableObject" }
1838
1882
  });
1839
1883
  }
1884
+ };
1885
+ const collected = collectPortableObjects(discovered.snapshot);
1886
+ const objectByPath = new Map(collected.map((object) => [object.path, object]));
1887
+ const isKeyCandidate = (object) => object.keyOf != null && !Object.hasOwn(object.document, "proof");
1888
+ const results = /* @__PURE__ */ new Map();
1889
+ for (const result of await Promise.all(collected.filter((object) => !isKeyCandidate(object)).map(verifyPortableObject))) results.set(result.path, result);
1890
+ let projections = 0;
1891
+ for (const object of collected) {
1892
+ if (!isKeyCandidate(object)) continue;
1893
+ const { path, member } = object.keyOf;
1894
+ const parent = results.get(path);
1895
+ const parentObject = objectByPath.get(path);
1896
+ if (parent?.verified === true && parentObject != null && projections++ < MAX_KEY_PROJECTIONS && await isEmbeddedPortableKey(parentObject.document, member, object.document, parent.id)) continue;
1897
+ results.set(object.path, await verifyPortableObject(object));
1898
+ }
1899
+ const portableObjects = Object.freeze(collected.flatMap((object) => {
1900
+ const result = results.get(object.path);
1901
+ return result == null ? [] : [result];
1840
1902
  }));
1841
1903
  return {
1842
1904
  status: "ok",
1843
1905
  verified: portableObjects.length > 0 && proofs.every((proof) => proof.verified) && portableObjects.every((object) => object.verified),
1844
1906
  snapshot: discovered.snapshot,
1845
1907
  proofs,
1846
- portableObjects: Object.freeze(portableObjects),
1908
+ portableObjects,
1847
1909
  statistics: discovered.statistics
1848
1910
  };
1849
1911
  }
1912
+ /**
1913
+ * The maximum number of embedded keys per compound document that are checked
1914
+ * by {@link isEmbeddedPortableKey}, each of which expands a projection of its
1915
+ * parent. Keys beyond it have to carry their own proofs.
1916
+ */
1917
+ const MAX_KEY_PROJECTIONS = 64;
1918
+ const SECURITY = "https://w3id.org/security#";
1919
+ const KEY_TYPES = /* @__PURE__ */ new Set([`${SECURITY}Key`, `${SECURITY}Multikey`]);
1920
+ const KEY_PROPERTIES = /* @__PURE__ */ new Set([
1921
+ "@id",
1922
+ "@type",
1923
+ `${SECURITY}owner`,
1924
+ `${SECURITY}controller`,
1925
+ `${SECURITY}publicKeyPem`,
1926
+ `${SECURITY}publicKeyMultibase`
1927
+ ]);
1928
+ /**
1929
+ * Tells whether an unsigned map embedded in a verified portable map is just
1930
+ * a key of the portable object the parent is, e.g., a gateway key of
1931
+ * a portable actor, which the parent's proof covers.
1932
+ *
1933
+ * The key's meaning depends on the contexts active where it sits, so it is
1934
+ * decided on a projection of the parent that keeps only what those contexts
1935
+ * depend on: the parent's `@context`, its types and ID, and the member that
1936
+ * holds the key. That holds only if nothing else in the parent can change
1937
+ * them, so both documents may only use contexts that define no keyword
1938
+ * aliases and no scoped contexts; Fedify's preloaded contexts, the only
1939
+ * remote ones the compound policy loads, alias just `id` and `type`.
1940
+ *
1941
+ * The projection must expand to the parent's verified ID with the key as the
1942
+ * single value of `publicKey` or `assertionMethod`, and the key must be
1943
+ * nothing but a `CryptographicKey` or `Multikey` whose ID is a compatible
1944
+ * identifier of the parent plus a fragment, and whose owner or controller,
1945
+ * if any, is the parent.
1946
+ */
1947
+ async function isEmbeddedPortableKey(parent, member, key, parentId) {
1948
+ const parentPortableId = getCanonicalPortableIdOf(parentId);
1949
+ if (parentPortableId == null) return false;
1950
+ if (!isSimpleContext(parent["@context"]) || Object.hasOwn(key, "@context") && !isSimpleContext(key["@context"])) return false;
1951
+ const literalIds = [key.id, key["@id"]].filter((id) => id !== void 0);
1952
+ if (literalIds.length < 1 || literalIds.some((id) => id !== literalIds[0]) || typeof literalIds[0] !== "string" || !isKeyIdOf(literalIds[0], parentPortableId)) return false;
1953
+ const projection = {};
1954
+ for (const property of [
1955
+ "@context",
1956
+ "type",
1957
+ "@type",
1958
+ "id",
1959
+ "@id"
1960
+ ]) if (Object.hasOwn(parent, property)) projection[property] = parent[property];
1961
+ projection[member] = key;
1962
+ let expanded;
1963
+ try {
1964
+ expanded = await _fedify_vocab_runtime_jsonld.default.expand(structuredClone(projection), {
1965
+ documentLoader: require_http.getNormalizationContextLoader(require_http.preloadedOnlyDocumentLoader),
1966
+ keepFreeFloatingNodes: true
1967
+ });
1968
+ } catch {
1969
+ return false;
1970
+ }
1971
+ if (expanded.length !== 1 || hasGraphKeyword(expanded)) return false;
1972
+ const node = expanded[0];
1973
+ if (!isJsonMap(node) || typeof node["@id"] !== "string" || getCanonicalPortableIdOf(node["@id"]) !== parentPortableId) return false;
1974
+ const properties = Object.keys(node).filter((property) => property !== "@id" && property !== "@type");
1975
+ if (properties.length !== 1 || properties[0] !== `${SECURITY}${member}`) return false;
1976
+ const values = node[properties[0]];
1977
+ if (!Array.isArray(values) || values.length !== 1) return false;
1978
+ const expandedKey = values[0];
1979
+ if (!isJsonMap(expandedKey) || expandedKey["@id"] !== literalIds[0] || Object.keys(expandedKey).some((property) => !KEY_PROPERTIES.has(property))) return false;
1980
+ const types = expandedKey["@type"];
1981
+ if (types !== void 0 && (!Array.isArray(types) || types.some((type) => typeof type !== "string" || !KEY_TYPES.has(type)))) return false;
1982
+ const pem = expandedKey[`${SECURITY}publicKeyPem`];
1983
+ const multibase = expandedKey[`${SECURITY}publicKeyMultibase`];
1984
+ if (pem === void 0 && multibase === void 0) return false;
1985
+ if (pem !== void 0 && !isSingleStringValue(pem)) return false;
1986
+ if (multibase !== void 0 && !isSingleStringValue(multibase, `${SECURITY}multibase`)) return false;
1987
+ for (const property of [`${SECURITY}owner`, `${SECURITY}controller`]) {
1988
+ const owners = expandedKey[property];
1989
+ if (owners === void 0) continue;
1990
+ if (!Array.isArray(owners) || owners.length !== 1 || !isJsonMap(owners[0]) || Object.keys(owners[0]).length !== 1 || typeof owners[0]["@id"] !== "string" || getCanonicalPortableIdOf(owners[0]["@id"]) !== parentPortableId) return false;
1991
+ }
1992
+ return true;
1993
+ }
1994
+ function getCanonicalPortableIdOf(id) {
1995
+ try {
1996
+ return require_http.getCanonicalPortableId((0, _fedify_vocab_runtime.parseIri)(id));
1997
+ } catch (error) {
1998
+ if (error instanceof TypeError) return null;
1999
+ throw error;
2000
+ }
2001
+ }
2002
+ /**
2003
+ * Checks whether an ID is a compatible identifier of the given portable
2004
+ * object plus a fragment.
2005
+ */
2006
+ function isKeyIdOf(id, portableId) {
2007
+ let url;
2008
+ try {
2009
+ if ((0, _fedify_vocab_runtime.fromCompatibleEf61Id)(id) == null) return false;
2010
+ url = new URL(id);
2011
+ } catch (error) {
2012
+ if (error instanceof TypeError) return false;
2013
+ throw error;
2014
+ }
2015
+ if (url.hash.length < 2) return false;
2016
+ url.hash = "";
2017
+ return require_http.getCanonicalPortableId(url) === portableId;
2018
+ }
2019
+ /**
2020
+ * Checks whether a `@context` value defines no keyword aliases and no scoped
2021
+ * contexts: only references to contexts and term definitions that map terms
2022
+ * to IRIs, optionally with an `@id` or `@vocab` type coercion or a datatype.
2023
+ */
2024
+ function isSimpleContext(context) {
2025
+ if (typeof context === "string") return true;
2026
+ if (Array.isArray(context)) return context.every(isSimpleContext);
2027
+ if (context === void 0 || !isCompoundProofJsonObject(context)) return false;
2028
+ return Object.entries(context).every(([term, definition]) => {
2029
+ if (term.startsWith("@")) return false;
2030
+ if (typeof definition === "string") return !definition.startsWith("@");
2031
+ if (!isCompoundProofJsonObject(definition)) return false;
2032
+ const { "@id": id, "@type": type, ...rest } = definition;
2033
+ return Object.keys(rest).length < 1 && typeof id === "string" && !id.startsWith("@") && (type === void 0 || type === "@id" || type === "@vocab" || typeof type === "string" && !type.startsWith("@"));
2034
+ });
2035
+ }
2036
+ function hasGraphKeyword(value) {
2037
+ if (Array.isArray(value)) return value.some(hasGraphKeyword);
2038
+ if (!isJsonMap(value)) return false;
2039
+ return Object.entries(value).some(([key, child]) => key === "@graph" || key === "@included" || key === "@reverse" || hasGraphKeyword(child));
2040
+ }
2041
+ /**
2042
+ * Checks whether expanded values are a single plain string, optionally with
2043
+ * the given datatype.
2044
+ */
2045
+ function isSingleStringValue(values, datatype) {
2046
+ if (!Array.isArray(values) || values.length !== 1) return false;
2047
+ const value = values[0];
2048
+ if (!isJsonMap(value) || typeof value["@value"] !== "string") return false;
2049
+ const keys = Object.keys(value);
2050
+ return keys.length === 1 || datatype != null && keys.length === 2 && value["@type"] === datatype;
2051
+ }
1850
2052
  //#endregion
1851
2053
  //#region src/federation/inbox.ts
1852
2054
  const Temporal$10 = require_http.Temporal;
@@ -2569,14 +2771,7 @@ async function handlePortableObject(request, { values, context, objectDispatcher
2569
2771
  } });
2570
2772
  }
2571
2773
  function isRequestedPortableObject(object, canonicalId) {
2572
- if (object.id == null) return false;
2573
- if (object.id.protocol !== "ap:" && object.id.protocol !== "ap+ef61:") return false;
2574
- try {
2575
- return (0, _fedify_vocab_runtime.canonicalizePortableUri)((0, _fedify_vocab_runtime.formatIri)(object.id)) === canonicalId;
2576
- } catch (error) {
2577
- if (error instanceof TypeError) return false;
2578
- throw error;
2579
- }
2774
+ return object.id != null && require_http.getCanonicalPortableId(object.id) === canonicalId;
2580
2775
  }
2581
2776
  async function getJsonLdRootId(jsonLd, contextLoader) {
2582
2777
  const expanded = await _fedify_vocab_runtime_jsonld.default.expand(jsonLd, {
@@ -2588,6 +2783,7 @@ async function getJsonLdRootId(jsonLd, contextLoader) {
2588
2783
  return typeof id === "string" ? id : null;
2589
2784
  }
2590
2785
  function isRequestedPortableId(id, canonicalId) {
2786
+ if (!/^ap(?:\+ef61)?:\/\//i.test(id)) return URL.canParse(id) && require_http.getCanonicalPortableId(new URL(id)) === canonicalId;
2591
2787
  try {
2592
2788
  return (0, _fedify_vocab_runtime.canonicalizePortableUri)(id) === canonicalId;
2593
2789
  } catch (error) {
@@ -3450,7 +3646,7 @@ async function handleInboxInternal(request, parameters, span) {
3450
3646
  return await respondInvalidActivity(error);
3451
3647
  }
3452
3648
  activityVerified = true;
3453
- if (!skipSignatureVerification && require_http.hasPortableActor(activity)) try {
3649
+ if (!skipSignatureVerification && isPortableActivity(activity)) try {
3454
3650
  proofVerified = await require_http.verifyObject(_fedify_vocab.Activity, jsonWithoutSig, {
3455
3651
  contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader),
3456
3652
  documentLoader: ctx.documentLoader,
@@ -3634,7 +3830,7 @@ async function handleInboxInternal(request, parameters, span) {
3634
3830
  "http_signatures.verified": httpSigKey != null,
3635
3831
  "http_signatures.key_id": httpSigKey?.id?.href ?? ""
3636
3832
  });
3637
- if (!skipSignatureVerification && !proofVerified && (require_http.hasPortableActor(activity) || isPortableGatewayKey(httpSigKey))) {
3833
+ if (!skipSignatureVerification && !proofVerified && (isPortableActivity(activity) || isPortableGatewayKey(httpSigKey))) {
3638
3834
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
3639
3835
  logger.error("The activity {activityId} of the portable actor {actorId} is not authenticated by a valid Object Integrity Proof.", {
3640
3836
  activity: json,
@@ -3652,6 +3848,32 @@ async function handleInboxInternal(request, parameters, span) {
3652
3848
  headers: { "Content-Type": "text/plain; charset=utf-8" }
3653
3849
  });
3654
3850
  }
3851
+ if (!skipSignatureVerification && activity.id != null && require_http.isPortableId(activity.id)) {
3852
+ const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
3853
+ contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader),
3854
+ documentLoader: ctx.documentLoader,
3855
+ keyCache,
3856
+ meterProvider,
3857
+ tracerProvider
3858
+ });
3859
+ if (rejection != null) {
3860
+ if (deferredLdSignatureError != null) throw deferredLdSignatureError;
3861
+ logger.error("The portable activity {activityId} is not authenticated by an Object Integrity Proof of its own DID: {reason}", {
3862
+ activity: json,
3863
+ recipient,
3864
+ activityId: activity.id.href,
3865
+ reason: rejection
3866
+ });
3867
+ span.setStatus({
3868
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
3869
+ message: `The portable activity (${activity.id.href}) is not authenticated by an Object Integrity Proof of its own DID.`
3870
+ });
3871
+ return new Response("Portable activities must have valid Object Integrity Proofs made by their own DIDs.", {
3872
+ status: 401,
3873
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
3874
+ });
3875
+ }
3876
+ }
3655
3877
  if (httpSigKey != null && !await require_owner.doesActorOwnKey(activity, httpSigKey, ctx)) {
3656
3878
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
3657
3879
  require_http.getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : require_http.getRemoteHost(httpSigKey.id));
@@ -4381,7 +4603,41 @@ const MIN_COMPONENTS = [
4381
4603
  * establish.
4382
4604
  */
4383
4605
  function isPortableGatewayKey(key) {
4384
- return key?.id != null && require_http.isCompatibleKeyId(key.id) && key.ownerId != null && require_http.isPortableUri(key.ownerId);
4606
+ return key?.id != null && require_http.isCompatibleKeyId(key.id) && key.ownerId != null && require_http.isPortableId(key.ownerId);
4607
+ }
4608
+ /**
4609
+ * Checks whether an activity needs an Object Integrity Proof by its DID:
4610
+ * either it is performed by an [FEP-ef61] portable actor, or its own ID is
4611
+ * portable. Both `ap:` URIs and compatible identifiers count as portable.
4612
+ *
4613
+ * [FEP-ef61]: https://w3id.org/fep/ef61
4614
+ */
4615
+ function isPortableActivity(activity) {
4616
+ return require_http.hasPortableActor(activity) || activity.id != null && require_http.isPortableId(activity.id);
4617
+ }
4618
+ /**
4619
+ * Verifies that a portable activity's own ID belongs to the DID that signed
4620
+ * it, on the JSON-LD document received. The ID the proof policy checks has
4621
+ * to be the parsed activity's ID, so that a remote context that changes
4622
+ * between loads cannot make the two name different objects.
4623
+ * @returns `null` if the ID is verified, or the reason why it is not.
4624
+ */
4625
+ async function verifyPortableActivityId(activityId, jsonLd, options) {
4626
+ const expectedId = require_http.getCanonicalPortableId(activityId);
4627
+ if (expectedId == null) return "malformed portable ID";
4628
+ let verification;
4629
+ try {
4630
+ verification = await require_http.verifyPortableObjectProofWithRoot(jsonLd, options);
4631
+ } catch (error) {
4632
+ if (error instanceof require_http.InvalidPortableObjectIdError) return "malformed portable ID";
4633
+ if (!isPermanentActivityParseError(error)) throw error;
4634
+ return "malformed document";
4635
+ }
4636
+ const { result, root } = verification;
4637
+ if (!result.verified) return result.reason.type;
4638
+ const rootId = root?.["@id"];
4639
+ if (typeof rootId !== "string" || require_http.getCanonicalPortableId((0, _fedify_vocab_runtime.parseIri)(rootId)) !== expectedId) return "the verified document has another ID";
4640
+ return null;
4385
4641
  }
4386
4642
  //#endregion
4387
4643
  //#region src/federation/kv.ts
@@ -5038,16 +5294,17 @@ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, loc
5038
5294
  reason: "notFound"
5039
5295
  };
5040
5296
  const id = actor.id;
5041
- if (id == null || id.protocol !== "ap:" && id.protocol !== "ap+ef61:") return {
5297
+ const did = id == null || !require_http.isPortableId(id) ? null : require_http.getPortableDid(id);
5298
+ if (id == null || did == null) return {
5042
5299
  status: "rejected",
5043
5300
  reason: "notPortable"
5044
5301
  };
5045
- if (!isSameAuthority(id, authority)) return {
5302
+ if (did !== authority) return {
5046
5303
  status: "rejected",
5047
5304
  reason: "authorityMismatch"
5048
5305
  };
5049
- const inboxId = actor.inboxId;
5050
- if (inboxId == null || canonicalize(inboxId) !== canonicalInboxId) return {
5306
+ const inboxId = actor.inboxId == null ? null : toPortableUri(actor.inboxId);
5307
+ if (inboxId == null || require_http.getCanonicalPortableId(inboxId) !== canonicalInboxId) return {
5051
5308
  status: "rejected",
5052
5309
  reason: "inboxMismatch"
5053
5310
  };
@@ -5066,17 +5323,15 @@ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, loc
5066
5323
  }
5067
5324
  };
5068
5325
  }
5069
- function isSameAuthority(id, authority) {
5070
- try {
5071
- return (0, _fedify_vocab_runtime.getFe34Origin)(id) === authority;
5072
- } catch (error) {
5073
- if (error instanceof TypeError) return false;
5074
- throw error;
5075
- }
5076
- }
5077
- function canonicalize(iri) {
5326
+ /**
5327
+ * Gets the portable form of an `ap:` or `ap+ef61:` URI or a compatible
5328
+ * identifier, keeping its query.
5329
+ * @returns The portable URI, or `null` if the ID is neither, or is malformed.
5330
+ */
5331
+ function toPortableUri(id) {
5332
+ if (require_http.isPortableUri(id)) return id;
5078
5333
  try {
5079
- return (0, _fedify_vocab_runtime.canonicalizePortableUri)(typeof iri === "string" ? iri : (0, _fedify_vocab_runtime.formatIri)(iri));
5334
+ return (0, _fedify_vocab_runtime.fromCompatibleEf61Id)(id);
5080
5335
  } catch (error) {
5081
5336
  if (error instanceof TypeError) return null;
5082
5337
  throw error;
@@ -5110,7 +5365,7 @@ async function forwardPortableInboxActivity(parameters) {
5110
5365
  const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
5111
5366
  const { maxTargets, ttl } = parameters.options ?? resolvePortableInboxForwardingOptions();
5112
5367
  if (kv.cas == null || maxTargets < 1) return [];
5113
- const canonicalActivityId = canonicalize(activityId) ?? activityId.href;
5368
+ const canonicalActivityId = require_http.getCanonicalPortableId(activityId) ?? activityId.href;
5114
5369
  const targets = getForwardingTargets(recipient, excludedOrigins);
5115
5370
  if (targets.length > maxTargets) {
5116
5371
  logger.warn("The portable actor that owns the inbox {inbox} has more than {max} other gateways; the activity {activityId} is forwarded only to the first {max} of them.", {
@@ -5297,34 +5552,50 @@ async function enqueueForwarding(parameters, outboxQueue, inboxes) {
5297
5552
  }
5298
5553
  //#endregion
5299
5554
  //#region src/federation/outgoing-proof.ts
5300
- function isPortableUrl(url) {
5301
- return url.protocol === "ap:" || url.protocol === "ap+ef61:";
5302
- }
5303
5555
  function formatKeyIds(candidates) {
5304
5556
  return candidates.map((c) => c.verificationMethod.href).join(", ");
5305
5557
  }
5306
5558
  /**
5307
5559
  * Rejects an activity of a portable actor whose ID cannot be authenticated by
5308
5560
  * the actor's DID: FEP-ef61 requires the activity to be a portable object
5309
- * signed by that DID, so its ID has to be a portable ID with the same DID.
5561
+ * signed by that DID, so its ID has to be a portable ID, i.e., an `ap:` or
5562
+ * `ap+ef61:` URI or a compatible identifier, with the same DID. A portable
5563
+ * actor is one whose ID is such an ID as well.
5310
5564
  * @throws {TypeError} If the activity is performed by a portable actor, but
5311
- * its ID is not a portable ID of the actor's DID, or its
5312
- * portable actors do not share a DID.
5565
+ * its ID is not a portable ID of the actor's DID, its
5566
+ * portable actors do not share a DID, or the ID of one of
5567
+ * them is a malformed compatible identifier.
5313
5568
  * @internal
5314
5569
  */
5315
5570
  function assertPortableActorActivity(activity) {
5316
- const portableActors = activity.actorIds.filter(isPortableUrl);
5571
+ const portableActors = activity.actorIds.filter((id) => require_http.isPortableId(id));
5317
5572
  if (portableActors.length < 1) return;
5318
- const did = (0, _fedify_vocab_runtime.getFe34Origin)(portableActors[0]);
5319
- if (portableActors.some((actorId) => (0, _fedify_vocab_runtime.getFe34Origin)(actorId) !== did)) throw new TypeError(`The activity ${activity.id?.href} has portable actors with different DIDs (${portableActors.map((a) => a.href).join(", ")}); a portable activity is signed by a single DID.`);
5320
- if (activity.id == null || !isPortableUrl(activity.id) || (0, _fedify_vocab_runtime.getFe34Origin)(activity.id) !== did) throw new TypeError(`The activity ${activity.id?.href} is performed by the portable actor ${portableActors[0].href}, so its ID has to be an ap: or ap+ef61: URI with the same DID (${did}), as FEP-ef61 requires it to carry an Object Integrity Proof made by that DID.`);
5573
+ const dids = portableActors.map((actorId) => {
5574
+ const did = require_http.getPortableDid(actorId);
5575
+ if (did == null) throw new TypeError(`The activity ${activity.id?.href} is performed by the actor ${actorId.href}, whose ID is a malformed FEP-ef61 portable ID. A compatible identifier must have a valid DID and an object path, and must not have credentials or @gateway location hints.`);
5576
+ return did;
5577
+ });
5578
+ const did = dids[0];
5579
+ if (dids.some((d) => d !== did)) throw new TypeError(`The activity ${activity.id?.href} has portable actors with different DIDs (${portableActors.map((a) => a.href).join(", ")}); a portable activity is signed by a single DID.`);
5580
+ if (activity.id == null || !require_http.isPortableId(activity.id) || require_http.getPortableDid(activity.id) !== did) throw new TypeError(`The activity ${activity.id?.href} is performed by the portable actor ${portableActors[0].href}, so its ID has to be an ap: or ap+ef61: URI, or an FEP-ef61 compatible identifier, with the same DID (${did}), as FEP-ef61 requires it to carry an Object Integrity Proof made by that DID.`);
5581
+ if (!(0, _fedify_vocab_runtime_internal_portable_dereference.isCompatibleEf61Iri)(activity.id)) return;
5582
+ const mismatched = portableActors.find((actorId) => (0, _fedify_vocab_runtime_internal_portable_dereference.isCompatibleEf61Iri)(actorId) && actorId.origin !== activity.id?.origin);
5583
+ if (mismatched == null) return;
5584
+ (0, _logtape_logtape.getLogger)([
5585
+ "fedify",
5586
+ "federation",
5587
+ "outbox"
5588
+ ]).warn("The activity {activityId} and its actor {actorId} are identified by FEP-ef61 compatible identifiers on different gateways. FEP-ef61 requires publishers to use the first gateway in the actor's gateways when constructing compatible identifiers.", {
5589
+ activityId: activity.id.href,
5590
+ actorId: mismatched.href
5591
+ });
5321
5592
  }
5322
5593
  /**
5323
5594
  * Creates the Object Integrity Proofs Fedify attaches to an outgoing activity.
5324
5595
  *
5325
5596
  * Keys whose IDs are FEP-ef61 compatible identifiers are gateway keys, which
5326
5597
  * never sign proofs. A portable activity, i.e., one with an `ap:` or
5327
- * `ap+ef61:` ID, that carries no proof is signed only by the one Ed25519 key
5598
+ * `ap+ef61:` ID or a compatible identifier, that carries no proof is signed only by the one Ed25519 key
5328
5599
  * whose verification method is a DID URL for its DID, even if it is the only
5329
5600
  * key; if no key or more than one key qualifies, this function throws.
5330
5601
  *
@@ -5363,7 +5634,7 @@ async function signOutgoingActivity(activity, candidates, options) {
5363
5634
  break;
5364
5635
  }
5365
5636
  const keys = candidates.filter((c) => c.privateKey.algorithm.name === "Ed25519" && !require_http.isCompatibleKeyId(c.verificationMethod));
5366
- if (!hasProof && activity.id != null && isPortableUrl(activity.id)) return await sign(selectPortableActivityKey(activity, activity.id, keys));
5637
+ if (!hasProof && activity.id != null && require_http.isPortableId(activity.id)) return await sign(selectPortableActivityKey(activity, activity.id, keys));
5367
5638
  if (keys.length < 1 || hasProof && !options.appendToExistingProofs) return {
5368
5639
  activity,
5369
5640
  hasProof,
@@ -5396,8 +5667,9 @@ async function signOutgoingActivity(activity, candidates, options) {
5396
5667
  * @throws {TypeError} If no key or more than one key qualifies.
5397
5668
  */
5398
5669
  function selectPortableActivityKey(activity, activityId, keys) {
5399
- const activityDid = (0, _fedify_vocab_runtime.getFe34Origin)(activityId);
5400
- const eligible = keys.filter((c) => c.verificationMethod.protocol === "did:" && (0, _fedify_vocab_runtime.haveSameFe34Origin)(activityId, c.verificationMethod));
5670
+ const activityDid = require_http.getPortableDid(activityId);
5671
+ if (activityDid == null) throw new TypeError(`Cannot sign the portable activity ${activityId.href}, as its ID is a malformed FEP-ef61 portable ID. A compatible identifier must have a valid DID and an object path, and must not have credentials or @gateway location hints.`);
5672
+ const eligible = keys.filter((c) => c.verificationMethod.protocol === "did:" && require_http.getPortableDid(c.verificationMethod) === activityDid);
5401
5673
  if (eligible.length !== 1) throw new TypeError(`Cannot choose an Object Integrity Proof key for the portable activity ${activityId.href}: ` + (eligible.length < 1 ? `none of its ${keys.length} Ed25519 keys (${formatKeyIds(keys)}) ` : `${eligible.length} of its Ed25519 keys (${formatKeyIds(eligible)}) `) + `${eligible.length < 1 ? "is" : "are"} a DID URL verification method for ${activityDid}. FEP-ef61 requires a portable activity's proof to be made by its DID, and the map-local compound-proof profile accepts exactly one direct proof per map. Gateway keys, whose IDs are compatible identifiers, never sign proofs. Supply exactly one Ed25519 key whose key ID is a DID URL for ${activityDid}, or sign the activity with signObject() before sending it.`);
5402
5674
  (0, _logtape_logtape.getLogger)([
5403
5675
  "fedify",
@@ -5817,6 +6089,27 @@ function getWebFingerSubjectAndAliases({ resourceUrl, actorUri, preferredUsernam
5817
6089
  };
5818
6090
  }
5819
6091
  /**
6092
+ * Gets the compatible identifier that a WebFinger response links to for
6093
+ * an FEP-ef61 portable actor. An actor identified by an `ap:` or `ap+ef61:`
6094
+ * URI is linked through its compatible identifier on the given gateway, while
6095
+ * an actor identified by a compatible identifier is linked through its ID as
6096
+ * is, so that software without FEP-ef61 support finds the same ID in the actor
6097
+ * document.
6098
+ * @param actorId The portable actor's ID.
6099
+ * @param gateway The actor's first gateway.
6100
+ * @returns The compatible identifier, or `null` if the actor ID is malformed
6101
+ * or cannot be represented as a compatible identifier.
6102
+ */
6103
+ function getCompatibleActorId(actorId, gateway) {
6104
+ try {
6105
+ if (require_http.isPortableUri(actorId)) return (0, _fedify_vocab_runtime.toCompatibleEf61Id)(actorId, gateway);
6106
+ return (0, _fedify_vocab_runtime.fromCompatibleEf61Id)(actorId) == null ? null : actorId;
6107
+ } catch (error) {
6108
+ if (error instanceof TypeError) return null;
6109
+ throw error;
6110
+ }
6111
+ }
6112
+ /**
5820
6113
  * Computes the subject and aliases for an FEP-ef61 portable actor, whose
5821
6114
  * WebFinger address takes its domain from the first gateway of the actor
5822
6115
  * rather than from the actor ID. The canonical `ap:` ID is not emitted,
@@ -5980,21 +6273,23 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
5980
6273
  });
5981
6274
  let actorUri = context.getActorUri(identifier);
5982
6275
  let portable;
5983
- if (actor.id != null && (actor.id.protocol === "ap:" || actor.id.protocol === "ap+ef61:")) {
6276
+ if (actor.id != null && require_http.isPortableId(actor.id)) {
5984
6277
  const gateway = actor.gateway;
5985
- let compatibleId;
5986
- if (gateway != null && (0, _fedify_vocab_runtime.isGatewayUrl)(gateway)) try {
5987
- compatibleId = (0, _fedify_vocab_runtime.toCompatibleEf61Id)(actor.id, gateway);
5988
- } catch (error) {
5989
- if (!(error instanceof TypeError)) throw error;
5990
- }
5991
- if (gateway == null || compatibleId == null) {
6278
+ if (gateway == null || !(0, _fedify_vocab_runtime.isGatewayUrl)(gateway)) {
5992
6279
  logger$1.error("The portable actor {actorId} (identifier: {identifier}) needs an HTTP(S) origin as the first item of its gateways property to be discovered through WebFinger.", {
5993
6280
  actorId: (0, _fedify_vocab_runtime.formatIri)(actor.id),
5994
6281
  identifier
5995
6282
  });
5996
6283
  return await onNotFound(request);
5997
6284
  }
6285
+ const compatibleId = getCompatibleActorId(actor.id, gateway);
6286
+ if (compatibleId == null) {
6287
+ logger$1.error("The portable actor {actorId} (identifier: {identifier}) cannot be discovered through WebFinger, as its ID is malformed or cannot be represented as an FEP-ef61 compatible identifier.", {
6288
+ actorId: (0, _fedify_vocab_runtime.formatIri)(actor.id),
6289
+ identifier
6290
+ });
6291
+ return await onNotFound(request);
6292
+ }
5998
6293
  actorUri = compatibleId;
5999
6294
  portable = {
6000
6295
  compatibleId,