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

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 (84) hide show
  1. package/dist/{builder-BGlpUocK.mjs → builder-Bno54o_-.mjs} +34 -3
  2. package/dist/compat/mod.d.cts +1 -1
  3. package/dist/compat/mod.d.ts +1 -1
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{compound-proof-B6Aj2kGY.mjs → compound-proof-X3SxXVbc.mjs} +191 -13
  6. package/dist/{context-CZjZYXFC.d.cts → context-C43spTGI.d.cts} +5 -2
  7. package/dist/{context-BSywYspi.d.ts → context-MORIQJIh.d.ts} +5 -2
  8. package/dist/{deno-Dn-py9Zy.mjs → deno-dxdvl2XZ.mjs} +1 -1
  9. package/dist/{docloader-BUq5bYJr.mjs → docloader-eBxQDsk9.mjs} +2 -2
  10. package/dist/federation/builder.test.mjs +1 -1
  11. package/dist/federation/compound-fanout.test.mjs +3 -3
  12. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  13. package/dist/federation/handler.test.mjs +5 -5
  14. package/dist/federation/hashlink-media.test.mjs +2 -2
  15. package/dist/federation/idempotency.test.mjs +2 -2
  16. package/dist/federation/metrics.test.mjs +1 -1
  17. package/dist/federation/middleware.test.mjs +7 -7
  18. package/dist/federation/mod.cjs +1 -1
  19. package/dist/federation/mod.d.cts +2 -2
  20. package/dist/federation/mod.d.ts +2 -2
  21. package/dist/federation/mod.js +1 -1
  22. package/dist/federation/outgoing-proof.test.mjs +254 -5
  23. package/dist/federation/portable-gateway-keys.test.mjs +72 -11
  24. package/dist/federation/portable-inbox.test.mjs +487 -10
  25. package/dist/federation/portable.test.mjs +182 -4
  26. package/dist/federation/send.test.mjs +3 -3
  27. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  28. package/dist/federation/tasks/tasks.test.mjs +2 -2
  29. package/dist/federation/temporal.test.mjs +1 -1
  30. package/dist/federation/webfinger.test.mjs +89 -1
  31. package/dist/{federation-BLGx8JUH.cjs → federation-C4Ef3JKa.cjs} +1487 -1070
  32. package/dist/{http-B5q1CHtL.cjs → http-C8cZTE3Y.cjs} +1224 -985
  33. package/dist/{http-4PCCvhsU.js → http-D-rbr_Sy.js} +1663 -1460
  34. package/dist/{http-B0VRNipE.mjs → http-DsgawTd8.mjs} +82 -7
  35. package/dist/{key-BJrp524f.mjs → key-DYfmjnmp.mjs} +39 -99
  36. package/dist/{kv-cache-1KUmAPH7.js → kv-cache-DIxwMeNi.js} +1 -1
  37. package/dist/{kv-cache-Dhhhrryv.cjs → kv-cache-Dvd-Q-E2.cjs} +1 -1
  38. package/dist/{kv-cache-C4C3LyYW.mjs → kv-cache-EvOgrFsS.mjs} +1 -1
  39. package/dist/{ld-BmFz5NLA.mjs → ld-B00wWHzc.mjs} +3 -3
  40. package/dist/{metrics-Beod6tzO.mjs → metrics-BP3vdBpb.mjs} +1 -1
  41. package/dist/{middleware-BKeQYPm0.mjs → middleware-Br1_d-Ga.mjs} +606 -389
  42. package/dist/{middleware-D_9pNYe9.js → middleware-CcXMe400.js} +1602 -1185
  43. package/dist/{middleware-B7CNsyqt.mjs → middleware-CeBVBa61.mjs} +1 -1
  44. package/dist/{mod-B2h2KY0q.d.ts → mod-BL5BQHCq.d.ts} +1 -1
  45. package/dist/{mod-HDSvS-ww.d.cts → mod-C8LieBzF.d.cts} +16 -3
  46. package/dist/{mod-C9WSpZWm.d.ts → mod-Coacostr.d.ts} +16 -3
  47. package/dist/{mod-JzkaEXsi.d.cts → mod-DMI4DbOo.d.cts} +1 -1
  48. package/dist/mod.cjs +5 -5
  49. package/dist/mod.d.cts +3 -3
  50. package/dist/mod.d.ts +3 -3
  51. package/dist/mod.js +5 -5
  52. package/dist/nodeinfo/handler.test.mjs +1 -1
  53. package/dist/{owner-CsKVgMzk.mjs → owner-BJPDRDRq.mjs} +5 -4
  54. package/dist/{owner-BQFZtFV4.cjs → owner-DSzH7wro.cjs} +2 -2
  55. package/dist/{owner-_pbbgLJK.js → owner-U12fDpQW.js} +2 -2
  56. package/dist/{portable-key-B4R-CiIV.mjs → portable-key-D0mWbV9G.mjs} +60 -41
  57. package/dist/portable-key-id-Bva5iT3W.mjs +136 -0
  58. package/dist/{proof-ozG4HfcQ.mjs → proof-ChjBwnep.mjs} +59 -18
  59. package/dist/{send-ClPt8nYu.mjs → send-yFX0E-ye.mjs} +3 -3
  60. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  61. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  62. package/dist/sig/compound-proof-verification.test.mjs +160 -3
  63. package/dist/sig/compound-proof.test.mjs +2 -2
  64. package/dist/sig/http.test.mjs +72 -2
  65. package/dist/sig/key.test.mjs +1 -1
  66. package/dist/sig/ld.test.mjs +2 -2
  67. package/dist/sig/mod.cjs +3 -3
  68. package/dist/sig/mod.d.cts +1 -1
  69. package/dist/sig/mod.d.ts +1 -1
  70. package/dist/sig/mod.js +3 -3
  71. package/dist/sig/owner.test.mjs +1 -1
  72. package/dist/sig/portable-collection.test.mjs +69 -5
  73. package/dist/sig/portable-key.test.mjs +176 -10
  74. package/dist/sig/proof.test.mjs +107 -4
  75. package/dist/{sig-k3C8sF7g.cjs → sig-B689Nj7y.cjs} +19 -3
  76. package/dist/{sig-DhdomAN0.js → sig-C85KqAcx.js} +19 -3
  77. package/dist/{temporal-Ba0EAZAI.mjs → temporal-Dl9x-Qw4.mjs} +1 -1
  78. package/dist/testing/kv.test.mjs +1 -1
  79. package/dist/testing/mod.d.mts +5 -2
  80. package/dist/utils/docloader.test.mjs +2 -2
  81. package/dist/utils/kv-cache.test.mjs +1 -1
  82. package/dist/utils/mod.cjs +1 -1
  83. package/dist/utils/mod.js +1 -1
  84. 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-C8cZTE3Y.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-DSzH7wro.cjs");
7
+ const require_kv_cache = require("./kv-cache-Dvd-Q-E2.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;
@@ -2319,115 +2521,815 @@ function parseHashlinkGatewayRequest(url) {
2319
2521
  };
2320
2522
  }
2321
2523
  //#endregion
2322
- //#region src/federation/temporal.ts
2323
- const Temporal$8 = require_http.Temporal;
2324
- function isPlainObject$1(value) {
2325
- return typeof value === "object" && value != null && !Array.isArray(value);
2326
- }
2327
- function normalizeDateTimeLiteral(value) {
2328
- return value.substring(19).match(/[Z+-]/) ? value : value + "Z";
2329
- }
2330
- function isMalformedDateTimeLiteral(value) {
2331
- if (typeof value !== "string") return false;
2332
- try {
2333
- Temporal$8.Instant.from(normalizeDateTimeLiteral(value));
2334
- return false;
2335
- } catch {
2336
- return true;
2337
- }
2338
- }
2339
- function isMalformedDurationLiteral(value) {
2340
- if (typeof value !== "string") return false;
2341
- try {
2342
- Temporal$8.Duration.from(value);
2343
- return false;
2344
- } catch {
2345
- return true;
2524
+ //#region src/federation/send.ts
2525
+ /**
2526
+ * Extracts the inbox URLs from recipients.
2527
+ * @param parameters The parameters to extract the inboxes.
2528
+ * See also {@link ExtractInboxesParameters}.
2529
+ * @returns The inboxes as a map of inbox URL to actor URIs.
2530
+ */
2531
+ function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
2532
+ const inboxes = {};
2533
+ for (const recipient of recipients) {
2534
+ let inbox;
2535
+ let sharedInbox = false;
2536
+ if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) {
2537
+ inbox = recipient.endpoints.sharedInbox;
2538
+ sharedInbox = true;
2539
+ } else inbox = recipient.inboxId;
2540
+ if (inbox != null && recipient.id != null) {
2541
+ if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
2542
+ inboxes[inbox.href] ??= {
2543
+ actorIds: /* @__PURE__ */ new Set(),
2544
+ sharedInbox
2545
+ };
2546
+ inboxes[inbox.href].actorIds.add(recipient.id.href);
2547
+ }
2346
2548
  }
2549
+ return inboxes;
2347
2550
  }
2348
- const TEMPORAL_DATE_TIME_IRIS = /* @__PURE__ */ new Set([
2349
- "https://www.w3.org/ns/activitystreams#deleted",
2350
- "https://www.w3.org/ns/activitystreams#endTime",
2351
- "https://www.w3.org/ns/activitystreams#published",
2352
- "https://www.w3.org/ns/activitystreams#startTime",
2353
- "https://www.w3.org/ns/activitystreams#updated",
2354
- "http://purl.org/dc/terms/created",
2355
- "https://w3id.org/security#created"
2356
- ]);
2357
- const TEMPORAL_DURATION_IRIS = /* @__PURE__ */ new Set(["https://www.w3.org/ns/activitystreams#duration"]);
2358
- const QUESTION_CLOSED_IRI = "https://www.w3.org/ns/activitystreams#closed";
2359
- const XSD_DATE_TIME_IRI = "http://www.w3.org/2001/XMLSchema#dateTime";
2360
- function hasMalformedExpandedDateTimeLiteral(value) {
2361
- if (Array.isArray(value)) return value.some(hasMalformedExpandedDateTimeLiteral);
2362
- return isPlainObject$1(value) && "@value" in value && isMalformedDateTimeLiteral(value["@value"]);
2363
- }
2364
- function hasMalformedExpandedQuestionClosedLiteral(value) {
2365
- if (Array.isArray(value)) return value.some(hasMalformedExpandedQuestionClosedLiteral);
2366
- if (!isPlainObject$1(value) || !("@value" in value)) return false;
2367
- const literal = value["@value"];
2368
- if (typeof literal === "boolean") return false;
2369
- if (typeof literal !== "string") return false;
2370
- if (value["@type"] !== XSD_DATE_TIME_IRI) return false;
2371
- if (new Date(literal).toString() === "Invalid Date") return false;
2372
- return isMalformedDateTimeLiteral(literal);
2551
+ /**
2552
+ * Sends an {@link Activity} to an inbox.
2553
+ *
2554
+ * @param parameters The parameters for sending the activity.
2555
+ * See also {@link SendActivityParameters}.
2556
+ * @throws {Error} If the activity fails to send.
2557
+ */
2558
+ function sendActivity(options) {
2559
+ const tracerProvider = options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider();
2560
+ return tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan("activitypub.send_activity", {
2561
+ kind: _opentelemetry_api.SpanKind.CLIENT,
2562
+ attributes: { "activitypub.shared_inbox": options.sharedInbox ?? false }
2563
+ }, async (span) => {
2564
+ if (options.activityId != null) span.setAttribute("activitypub.activity.id", options.activityId);
2565
+ if (options.activityType != null) span.setAttribute("activitypub.activity.type", options.activityType);
2566
+ try {
2567
+ await sendActivityInternal({
2568
+ ...options,
2569
+ tracerProvider
2570
+ }, span);
2571
+ } catch (e) {
2572
+ span.setStatus({
2573
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
2574
+ message: String(e)
2575
+ });
2576
+ throw e;
2577
+ } finally {
2578
+ span.end();
2579
+ }
2580
+ });
2373
2581
  }
2374
- function hasMalformedExpandedDurationLiteral(value) {
2375
- if (Array.isArray(value)) return value.some(hasMalformedExpandedDurationLiteral);
2376
- return isPlainObject$1(value) && "@value" in value && isMalformedDurationLiteral(value["@value"]);
2582
+ const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
2583
+ function getActivityActorId(activity) {
2584
+ if (!isRecord(activity)) return void 0;
2585
+ return getIdValue(activity.actor);
2377
2586
  }
2378
- function hasMalformedKnownTemporalLiteralInternal(value, visited) {
2379
- if (Array.isArray(value)) return value.some((item) => hasMalformedKnownTemporalLiteralInternal(item, visited));
2380
- if (!isPlainObject$1(value)) return false;
2381
- if (visited.has(value)) return false;
2382
- visited.add(value);
2383
- if ("@value" in value) return false;
2384
- for (const [key, child] of Object.entries(value)) {
2385
- if (TEMPORAL_DATE_TIME_IRIS.has(key)) {
2386
- if (hasMalformedExpandedDateTimeLiteral(child)) return true;
2387
- continue;
2388
- }
2389
- if (key === QUESTION_CLOSED_IRI) {
2390
- if (hasMalformedExpandedQuestionClosedLiteral(child)) return true;
2391
- continue;
2392
- }
2393
- if (TEMPORAL_DURATION_IRIS.has(key)) {
2394
- if (hasMalformedExpandedDurationLiteral(child)) return true;
2395
- continue;
2587
+ function getIdValue(value) {
2588
+ if (typeof value === "string" && value !== "") return value;
2589
+ if (value instanceof URL) return value.href;
2590
+ if (Array.isArray(value)) {
2591
+ for (const item of value) {
2592
+ const id = getIdValue(item);
2593
+ if (id != null) return id;
2396
2594
  }
2397
- if (hasMalformedKnownTemporalLiteralInternal(child, visited)) return true;
2595
+ return;
2398
2596
  }
2399
- return false;
2597
+ if (isRecord(value)) return getIdValue(value.id);
2400
2598
  }
2401
- async function hasMalformedKnownTemporalLiteral(value, contextLoader) {
2599
+ function isRecord(value) {
2600
+ return typeof value === "object" && value != null;
2601
+ }
2602
+ async function readLimitedResponseBody(response, maxBytes) {
2603
+ if (response.body == null) return "";
2604
+ const reader = response.body.getReader();
2605
+ const decoder = new TextDecoder();
2606
+ const chunks = [];
2607
+ let totalBytes = 0;
2608
+ let truncated = false;
2402
2609
  try {
2403
- return hasMalformedKnownTemporalLiteralInternal(await _fedify_vocab_runtime_jsonld.default.expand(value, {
2404
- documentLoader: require_http.getNormalizationContextLoader(contextLoader),
2405
- keepFreeFloatingNodes: true
2406
- }), /* @__PURE__ */ new Set());
2407
- } catch {
2408
- return false;
2610
+ while (true) {
2611
+ const { done, value } = await reader.read();
2612
+ if (done) break;
2613
+ if (totalBytes + value.length > maxBytes) {
2614
+ const remaining = maxBytes - totalBytes;
2615
+ if (remaining > 0) chunks.push(decoder.decode(value.slice(0, remaining), { stream: true }));
2616
+ truncated = true;
2617
+ break;
2618
+ }
2619
+ chunks.push(decoder.decode(value, { stream: true }));
2620
+ totalBytes += value.length;
2621
+ }
2622
+ } finally {
2623
+ reader.releaseLock();
2409
2624
  }
2625
+ let result = chunks.join("");
2626
+ if (truncated) result += "… (truncated)";
2627
+ return result;
2410
2628
  }
2411
- //#endregion
2412
- //#region src/federation/handler.ts
2413
- const Temporal$7 = require_http.Temporal;
2414
- const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
2415
- const INBOX_COMPOUND_PROOF_LIMITS = {
2416
- maxDepth: 64,
2417
- maxMaps: 1e4,
2418
- maxProofs: 32,
2419
- maxBytes: 10 * 1024 * 1024
2420
- };
2421
- function isRemoteContextLoadingFailure$1(error) {
2422
- return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
2423
- }
2424
- function isPermanentRemoteContextError$1(error) {
2425
- if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
2426
- const details = error.details;
2427
- if (details?.code === "invalid remote context") return true;
2428
- return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) && require_http.isClearlyMalformedContextReference(details.url);
2429
- }
2430
- function isInvalidJsonLdError(error) {
2629
+ async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
2630
+ const logger = (0, _logtape_logtape.getLogger)([
2631
+ "fedify",
2632
+ "federation",
2633
+ "outbox"
2634
+ ]);
2635
+ const federationMetrics = require_http.getFederationMetrics(meterProvider);
2636
+ const started = performance.now();
2637
+ let deliverySuccess = false;
2638
+ async function validateUrl(url) {
2639
+ if (!allowPrivateAddress) try {
2640
+ await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
2641
+ } catch (error) {
2642
+ if (error instanceof _fedify_vocab_runtime.UrlError) logger.error("Disallowed private URL: {url}", {
2643
+ url,
2644
+ error
2645
+ });
2646
+ throw error;
2647
+ }
2648
+ }
2649
+ await validateUrl(inbox.href);
2650
+ headers = new Headers(headers);
2651
+ headers.set("Content-Type", "application/activity+json");
2652
+ const request = new Request(inbox, {
2653
+ method: "POST",
2654
+ headers,
2655
+ body: JSON.stringify(activity),
2656
+ redirect: "manual"
2657
+ });
2658
+ let rsaKey = null;
2659
+ for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
2660
+ rsaKey = key;
2661
+ break;
2662
+ }
2663
+ if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
2664
+ activityId,
2665
+ inbox: inbox.href
2666
+ });
2667
+ else if (rsaKey == null) logger.warn("No supported key found to sign the request to {inbox}. The request will be sent without a signature. In order to sign the request, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
2668
+ inbox: inbox.href,
2669
+ keys: keys.map((pair) => ({
2670
+ keyId: pair.keyId.href,
2671
+ privateKey: pair.privateKey
2672
+ }))
2673
+ });
2674
+ let response;
2675
+ try {
2676
+ response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await require_http.doubleKnock(request, rsaKey, {
2677
+ tracerProvider,
2678
+ specDeterminer,
2679
+ validateRedirect: validateUrl
2680
+ });
2681
+ } catch (error) {
2682
+ const failure = error instanceof _fedify_vocab_runtime.FetchError || error instanceof _fedify_vocab_runtime.UrlError ? error : createFetchError(inbox.href, error);
2683
+ logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
2684
+ activityId,
2685
+ inbox: inbox.href,
2686
+ error: failure
2687
+ });
2688
+ federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
2689
+ throw failure;
2690
+ }
2691
+ try {
2692
+ if (!response.ok) {
2693
+ let error;
2694
+ try {
2695
+ error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
2696
+ } catch (_) {
2697
+ error = "";
2698
+ }
2699
+ logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
2700
+ activityId,
2701
+ inbox: inbox.href,
2702
+ status: response.status,
2703
+ statusText: response.statusText,
2704
+ error
2705
+ });
2706
+ throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error, response.headers);
2707
+ }
2708
+ deliverySuccess = true;
2709
+ const eventAttributes = {
2710
+ "activitypub.inbox.url": inbox.href,
2711
+ "activitypub.activity.id": activityId ?? ""
2712
+ };
2713
+ if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
2714
+ const actorId = getActivityActorId(activity);
2715
+ if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
2716
+ span.addEvent("activitypub.activity.sent", eventAttributes);
2717
+ } finally {
2718
+ federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), deliverySuccess, activityType);
2719
+ }
2720
+ }
2721
+ function createFetchError(url, cause) {
2722
+ const error = new _fedify_vocab_runtime.FetchError(url, cause instanceof Error ? cause.message : String(cause));
2723
+ error.cause = cause;
2724
+ return error;
2725
+ }
2726
+ async function fetchWithValidatedRedirects(request, validateUrl) {
2727
+ let body = await request.clone().text();
2728
+ for (let redirects = 0;; redirects++) {
2729
+ const response = await fetch(request, { redirect: "manual" });
2730
+ const location = response.headers.get("Location");
2731
+ if (![
2732
+ 301,
2733
+ 302,
2734
+ 303,
2735
+ 307,
2736
+ 308
2737
+ ].includes(response.status) || location == null) return response;
2738
+ await response.body?.cancel();
2739
+ if (redirects >= 20) throw new TypeError("Too many redirects");
2740
+ const url = new URL(location, request.url);
2741
+ await validateUrl(url.href);
2742
+ const headers = new Headers(request.headers);
2743
+ if (url.origin !== new URL(request.url).origin) {
2744
+ headers.delete("Authorization");
2745
+ headers.delete("Proxy-Authorization");
2746
+ headers.delete("Cookie");
2747
+ headers.delete("Host");
2748
+ }
2749
+ let method = request.method;
2750
+ if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
2751
+ method = "GET";
2752
+ body = void 0;
2753
+ for (const name of [
2754
+ "Content-Encoding",
2755
+ "Content-Language",
2756
+ "Content-Length",
2757
+ "Content-Location",
2758
+ "Content-Type"
2759
+ ]) headers.delete(name);
2760
+ }
2761
+ request = new Request(url, {
2762
+ method,
2763
+ headers,
2764
+ body,
2765
+ redirect: "manual"
2766
+ });
2767
+ }
2768
+ }
2769
+ /**
2770
+ * An error that is thrown when an activity fails to send to a remote inbox.
2771
+ * It contains structured information about the failure, including the HTTP
2772
+ * status code, the inbox URL, and the response body.
2773
+ * @since 2.0.0
2774
+ */
2775
+ var SendActivityError = class extends Error {
2776
+ /**
2777
+ * The inbox URL that the activity was being sent to.
2778
+ */
2779
+ inbox;
2780
+ /**
2781
+ * The HTTP status code returned by the inbox.
2782
+ */
2783
+ statusCode;
2784
+ /**
2785
+ * The response body from the inbox, if any. Note that this may be
2786
+ * truncated to a maximum of 1 KiB to prevent excessive memory consumption
2787
+ * when remote servers return large error pages (e.g., Cloudflare error pages).
2788
+ * If truncated, the string will end with `"… (truncated)"`.
2789
+ */
2790
+ responseBody;
2791
+ /**
2792
+ * The response headers from the inbox.
2793
+ * @since 2.3.0
2794
+ */
2795
+ responseHeaders;
2796
+ /**
2797
+ * Creates a new {@link SendActivityError}.
2798
+ * @param inbox The inbox URL.
2799
+ * @param statusCode The HTTP status code.
2800
+ * @param message The error message.
2801
+ * @param responseBody The response body.
2802
+ * @param responseHeaders The response headers.
2803
+ */
2804
+ constructor(inbox, statusCode, message, responseBody, responseHeaders) {
2805
+ super(message);
2806
+ this.name = "SendActivityError";
2807
+ this.inbox = inbox;
2808
+ this.statusCode = statusCode;
2809
+ this.responseBody = responseBody;
2810
+ this.responseHeaders = new Headers(responseHeaders);
2811
+ }
2812
+ };
2813
+ //#endregion
2814
+ //#region src/federation/portable-inbox.ts
2815
+ const Temporal$8 = require_http.Temporal;
2816
+ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
2817
+ /**
2818
+ * Fills in the defaults of {@link PortableInboxForwardingOptions} and
2819
+ * validates them.
2820
+ * @param options The options.
2821
+ * @returns The resolved options.
2822
+ * @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
2823
+ * is not positive, if `deadline` is negative or longer
2824
+ * than 2,147,483,647 milliseconds (about 24.8 days), or
2825
+ * if either duration has calendar units, i.e., weeks,
2826
+ * months, or years, whose length depends on the date.
2827
+ */
2828
+ function resolvePortableInboxForwardingOptions(options = {}) {
2829
+ const maxTargets = options.maxTargets ?? 10;
2830
+ if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
2831
+ const ttl = Temporal$8.Duration.from(options.ttl ?? { days: 30 });
2832
+ if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
2833
+ const deadline = Temporal$8.Duration.from(options.deadline ?? { seconds: 10 });
2834
+ const deadlineMs = deadline.total("millisecond");
2835
+ if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
2836
+ return {
2837
+ maxTargets,
2838
+ ttl,
2839
+ deadline
2840
+ };
2841
+ }
2842
+ /**
2843
+ * Decides whether this server accepts a delivery to a portable inbox on behalf
2844
+ * of the given actor, which the actor dispatcher returned for the identifier
2845
+ * in the requested inbox path.
2846
+ *
2847
+ * The actor document comes from the application, so it is trusted as is and
2848
+ * its proof is not verified here.
2849
+ * @param actor The actor the actor dispatcher returned.
2850
+ * @param options The requested DID, the canonical form of the requested inbox
2851
+ * ID, and the origin of this server.
2852
+ * @returns The resolution.
2853
+ */
2854
+ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
2855
+ if (actor == null) return {
2856
+ status: "rejected",
2857
+ reason: "notFound"
2858
+ };
2859
+ const id = actor.id;
2860
+ const did = id == null || !require_http.isPortableId(id) ? null : require_http.getPortableDid(id);
2861
+ if (id == null || did == null) return {
2862
+ status: "rejected",
2863
+ reason: "notPortable"
2864
+ };
2865
+ if (did !== authority) return {
2866
+ status: "rejected",
2867
+ reason: "authorityMismatch"
2868
+ };
2869
+ const inboxId = actor.inboxId == null ? null : toPortableUri(actor.inboxId);
2870
+ if (inboxId == null || require_http.getCanonicalPortableId(inboxId) !== canonicalInboxId) return {
2871
+ status: "rejected",
2872
+ reason: "inboxMismatch"
2873
+ };
2874
+ const gateways = actor.gateways;
2875
+ if (!gateways.some((g) => (0, _fedify_vocab_runtime.isGatewayUrl)(g) && g.origin === localOrigin)) return {
2876
+ status: "rejected",
2877
+ reason: "notGateway"
2878
+ };
2879
+ return {
2880
+ status: "accepted",
2881
+ recipient: {
2882
+ actorId: id,
2883
+ inboxId,
2884
+ canonicalInboxId,
2885
+ gateways
2886
+ }
2887
+ };
2888
+ }
2889
+ /**
2890
+ * Gets the portable form of an `ap:` or `ap+ef61:` URI or a compatible
2891
+ * identifier, keeping its query.
2892
+ * @returns The portable URI, or `null` if the ID is neither, or is malformed.
2893
+ */
2894
+ function toPortableUri(id) {
2895
+ if (require_http.isPortableUri(id)) return id;
2896
+ try {
2897
+ return (0, _fedify_vocab_runtime.fromCompatibleEf61Id)(id);
2898
+ } catch (error) {
2899
+ if (error instanceof TypeError) return null;
2900
+ throw error;
2901
+ }
2902
+ }
2903
+ const RFC9421_DELIVERY_COMPONENTS = [
2904
+ "@method",
2905
+ "@target-uri",
2906
+ "content-digest"
2907
+ ];
2908
+ const DRAFT_DELIVERY_COMPONENTS = [
2909
+ "(request-target)",
2910
+ "host",
2911
+ "digest"
2912
+ ];
2913
+ /**
2914
+ * Checks whether an HTTP Signature covers the method, the target URI, and the
2915
+ * body of a request, which both Fedify and Mitra sign when forwarding.
2916
+ * @param signature The signature.
2917
+ * @returns `true` if the signature covers them.
2918
+ */
2919
+ function coversDelivery(signature) {
2920
+ return (signature.label == null ? DRAFT_DELIVERY_COMPONENTS : RFC9421_DELIVERY_COMPONENTS).every((name) => signature.components.some((c) => c.value === name && Object.keys(c.params).length < 1));
2921
+ }
2922
+ /**
2923
+ * Forwards an activity received in a portable inbox to the inboxes of the same
2924
+ * portable actor on its other gateways, as FEP-ef61 recommends.
2925
+ *
2926
+ * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
2927
+ * each target gateway is claimed with an atomic compare-and-swap before the
2928
+ * activity is handed off, and a claim is never released, even if the hand-off
2929
+ * fails, since a failed hand-off does not prove that nothing was delivered.
2930
+ * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
2931
+ * forwarded.
2932
+ *
2933
+ * Forwarded requests are signed with HTTP Signatures by the RSA key among
2934
+ * {@link ForwardPortableInboxActivityParameters.getKeys}, if any, so that
2935
+ * servers requiring HTTP Signatures accept them, and are sent unsigned
2936
+ * otherwise. Either way, the receiving gateways authenticate the activity by
2937
+ * its own proof.
2938
+ *
2939
+ * If the delivery is signed with a gateway key of the recipient by one of its
2940
+ * other gateways, that gateway has the activity already, so it is claimed
2941
+ * without forwarding the activity to it. See {@link identifySendingGateway}.
2942
+ * @param parameters The parameters.
2943
+ * @returns The target inbox URLs that the activity was handed off to.
2944
+ */
2945
+ async function forwardPortableInboxActivity(parameters) {
2946
+ const logger = (0, _logtape_logtape.getLogger)([
2947
+ "fedify",
2948
+ "federation",
2949
+ "inbox"
2950
+ ]);
2951
+ const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
2952
+ const options = parameters.options ?? resolvePortableInboxForwardingOptions();
2953
+ const { maxTargets, ttl } = options;
2954
+ if (kv.cas == null || maxTargets < 1) return [];
2955
+ const deadline = Date.now() + options.deadline.total("millisecond");
2956
+ const canonicalActivityId = require_http.getCanonicalPortableId(activityId) ?? activityId.href;
2957
+ const claimKey = (gateway) => [
2958
+ ...kvPrefix,
2959
+ recipient.canonicalInboxId,
2960
+ canonicalActivityId,
2961
+ gateway
2962
+ ];
2963
+ let targets = getForwardingTargets(recipient, excludedOrigins);
2964
+ const sender = await identifySendingGateway(parameters, targets.map((t) => t.gateway), claimKey, deadline);
2965
+ if (sender != null) {
2966
+ logger.debug("Not forwarding activity {activityId} from the portable inbox {inbox} back to the gateway {gateway}, which delivered it.", {
2967
+ activityId: activityId.href,
2968
+ inbox: recipient.canonicalInboxId,
2969
+ gateway: sender
2970
+ });
2971
+ await kv.cas(claimKey(sender), void 0, true, { ttl });
2972
+ targets = targets.filter((t) => t.gateway !== sender);
2973
+ }
2974
+ if (targets.length > maxTargets) {
2975
+ 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.", {
2976
+ inbox: recipient.canonicalInboxId,
2977
+ activityId: activityId.href,
2978
+ max: maxTargets
2979
+ });
2980
+ targets.length = maxTargets;
2981
+ }
2982
+ const claimed = [];
2983
+ for (const { gateway, inbox } of targets) if (await kv.cas(claimKey(gateway), void 0, true, { ttl })) claimed.push(inbox);
2984
+ if (claimed.length < 1) return [];
2985
+ logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
2986
+ activityId: activityId.href,
2987
+ inbox: recipient.canonicalInboxId,
2988
+ targets: claimed.map((t) => t.href)
2989
+ });
2990
+ if (outboxQueue == null) await forwardImmediately(parameters, claimed, deadline);
2991
+ else await enqueueForwarding(parameters, outboxQueue, claimed);
2992
+ return claimed;
2993
+ }
2994
+ /**
2995
+ * Identifies the gateway of the recipient that forwarded a delivery to its
2996
+ * portable inbox, by the delivery's HTTP Signature made with the recipient's
2997
+ * gateway key, which FEP-ef61 has each gateway use for requests on behalf of
2998
+ * an actor. Since the activity is authenticated by its own proof, the
2999
+ * signature only tells which gateway to skip, and a delivery whose signature
3000
+ * is invalid or cannot be verified is still forwarded to every gateway.
3001
+ *
3002
+ * Verifying the signature fetches the recipient's actor document from the
3003
+ * sending gateway, as gateway keys are not cached, so only the first signature
3004
+ * that may save a request is verified: one that covers the delivery and names
3005
+ * a gateway key of the recipient on one of the forwarding targets that the
3006
+ * activity has not been forwarded to yet.
3007
+ * @param parameters The forwarding parameters.
3008
+ * @param targets The origins of the gateways to forward to.
3009
+ * @param claimKey Gets the key of the claim for a gateway.
3010
+ * @param deadline The time by which identification has to finish, in
3011
+ * milliseconds since the epoch; afterwards, the sending
3012
+ * gateway is left unidentified.
3013
+ * @returns The origin of the sending gateway, or `null` if it is not
3014
+ * identified.
3015
+ */
3016
+ async function identifySendingGateway({ recipient, activityId, kv, signatures }, targets, claimKey, deadline) {
3017
+ if (signatures == null || signatures.length < 1) return null;
3018
+ const logger = (0, _logtape_logtape.getLogger)([
3019
+ "fedify",
3020
+ "federation",
3021
+ "inbox"
3022
+ ]);
3023
+ const actorId = require_http.getCanonicalPortableId(recipient.actorId);
3024
+ if (actorId == null) return null;
3025
+ const isGatewayKeyOfRecipient = (keyId) => {
3026
+ if (!require_http.isCompatibleKeyId(keyId) || !targets.includes(keyId.origin)) return false;
3027
+ const base = new URL(keyId.href);
3028
+ base.hash = "";
3029
+ return require_http.getCanonicalPortableId(base) === actorId;
3030
+ };
3031
+ let signature;
3032
+ for (const candidate of signatures) if (candidate.coversDelivery && isGatewayKeyOfRecipient(candidate.keyId) && await kv.get(claimKey(candidate.keyId.origin)) === void 0) {
3033
+ signature = candidate;
3034
+ break;
3035
+ }
3036
+ if (signature == null) return null;
3037
+ const { keyId } = signature;
3038
+ const logProperties = {
3039
+ activityId: activityId.href,
3040
+ inbox: recipient.canonicalInboxId,
3041
+ keyId: keyId.href
3042
+ };
3043
+ const verification = Promise.resolve().then(() => signature.verify()).then((key) => {
3044
+ if (key == null) logger.debug("The HTTP Signature of the delivery of activity {activityId} to the portable inbox {inbox} is not verified with the key {keyId}; forwarding it without identifying the sending gateway.", logProperties);
3045
+ return key;
3046
+ }, (error) => {
3047
+ logger.debug("Failed to verify the HTTP Signature of the delivery of activity {activityId} to the portable inbox {inbox} with the key {keyId}; forwarding it without identifying the sending gateway:\n{error}", {
3048
+ ...logProperties,
3049
+ error
3050
+ });
3051
+ return null;
3052
+ });
3053
+ let timer;
3054
+ const key = await Promise.race([verification, new Promise((resolve) => {
3055
+ timer = setTimeout(() => {
3056
+ logger.debug("Verifying the HTTP Signature of the delivery of activity {activityId} to the portable inbox {inbox} did not finish in time; forwarding it without identifying the sending gateway.", logProperties);
3057
+ resolve(null);
3058
+ }, Math.max(0, deadline - Date.now()));
3059
+ })]);
3060
+ clearTimeout(timer);
3061
+ if (key == null) return null;
3062
+ if (key.id == null || key.id.href !== keyId.href || !isGatewayKeyOfRecipient(key.id) || key.ownerId == null || !require_http.isPortableId(key.ownerId) || require_http.getCanonicalPortableId(key.ownerId) !== actorId) {
3063
+ logger.debug("The delivery of activity {activityId} to the portable inbox {inbox} is not signed with a gateway key of the recipient; forwarding it without identifying the sending gateway.", logProperties);
3064
+ return null;
3065
+ }
3066
+ return key.id.origin;
3067
+ }
3068
+ function getForwardingTargets(recipient, excludedOrigins) {
3069
+ const logger = (0, _logtape_logtape.getLogger)([
3070
+ "fedify",
3071
+ "federation",
3072
+ "inbox"
3073
+ ]);
3074
+ const seen = new Set(excludedOrigins);
3075
+ const targets = [];
3076
+ for (const gateway of recipient.gateways) {
3077
+ if (!(0, _fedify_vocab_runtime.isGatewayUrl)(gateway) || seen.has(gateway.origin)) continue;
3078
+ seen.add(gateway.origin);
3079
+ let inbox;
3080
+ try {
3081
+ inbox = (0, _fedify_vocab_runtime.toCompatibleEf61Id)(recipient.inboxId, gateway.origin);
3082
+ } catch (error) {
3083
+ if (!(error instanceof TypeError)) throw error;
3084
+ logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
3085
+ gateway: gateway.origin,
3086
+ inbox: recipient.canonicalInboxId
3087
+ });
3088
+ continue;
3089
+ }
3090
+ targets.push({
3091
+ gateway: gateway.origin,
3092
+ inbox
3093
+ });
3094
+ }
3095
+ return targets;
3096
+ }
3097
+ /**
3098
+ * Resolves the key that signs forwarded requests: the first RSASSA-PKCS1-v1_5
3099
+ * key, as {@link sendActivity} signs requests only with such a key. Since
3100
+ * the targets have already been claimed, the activity is forwarded unsigned
3101
+ * rather than not at all if the key cannot be resolved or used.
3102
+ */
3103
+ async function resolveForwardingKey({ getKeys, activityId }) {
3104
+ if (getKeys == null) return null;
3105
+ const logger = (0, _logtape_logtape.getLogger)([
3106
+ "fedify",
3107
+ "federation",
3108
+ "inbox"
3109
+ ]);
3110
+ let key;
3111
+ try {
3112
+ key = (await getKeys()).find((k) => k.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5");
3113
+ } catch (error) {
3114
+ logger.error("Failed to get the gateway keys to sign forwarded requests with; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
3115
+ activityId: activityId.href,
3116
+ error
3117
+ });
3118
+ return null;
3119
+ }
3120
+ if (key == null) return null;
3121
+ try {
3122
+ require_http.validateCryptoKey(key.privateKey, "private");
3123
+ } catch (error) {
3124
+ if (!(error instanceof TypeError)) throw error;
3125
+ logger.warn("The gateway key {keyId} cannot sign forwarded requests; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
3126
+ keyId: key.keyId.href,
3127
+ activityId: activityId.href,
3128
+ error
3129
+ });
3130
+ return null;
3131
+ }
3132
+ return key;
3133
+ }
3134
+ async function forwardImmediately(parameters, inboxes, deadlineAt) {
3135
+ const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, meterProvider, tracerProvider } = parameters;
3136
+ const logger = (0, _logtape_logtape.getLogger)([
3137
+ "fedify",
3138
+ "federation",
3139
+ "inbox"
3140
+ ]);
3141
+ const deadline = Math.max(0, deadlineAt - Date.now());
3142
+ const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
3143
+ activity,
3144
+ activityId: activityId.href,
3145
+ activityType,
3146
+ keys: key == null ? [] : [key],
3147
+ inbox,
3148
+ allowPrivateAddress,
3149
+ specDeterminer,
3150
+ meterProvider,
3151
+ tracerProvider
3152
+ }).catch((error) => {
3153
+ logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
3154
+ activityId: activityId.href,
3155
+ inbox: inbox.href,
3156
+ error
3157
+ });
3158
+ }))));
3159
+ let timer;
3160
+ const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
3161
+ timer = setTimeout(() => resolve(true), deadline);
3162
+ })]);
3163
+ clearTimeout(timer);
3164
+ if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
3165
+ activityId: activityId.href,
3166
+ deadline
3167
+ });
3168
+ }
3169
+ async function enqueueForwarding(parameters, outboxQueue, inboxes) {
3170
+ const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
3171
+ const logger = (0, _logtape_logtape.getLogger)([
3172
+ "fedify",
3173
+ "federation",
3174
+ "inbox"
3175
+ ]);
3176
+ const key = await resolveForwardingKey(parameters);
3177
+ const keys = [];
3178
+ if (key != null) try {
3179
+ keys.push({
3180
+ keyId: key.keyId.href,
3181
+ privateKey: await require_http.exportJwk(key.privateKey)
3182
+ });
3183
+ } catch (error) {
3184
+ logger.error("Failed to export the gateway key {keyId}; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
3185
+ keyId: key.keyId.href,
3186
+ activityId: activityId.href,
3187
+ error
3188
+ });
3189
+ }
3190
+ startQueue?.();
3191
+ const started = (/* @__PURE__ */ new Date()).toISOString();
3192
+ const traceContext = {};
3193
+ _opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), traceContext);
3194
+ await Promise.all(inboxes.map(async (inbox) => {
3195
+ const message = {
3196
+ type: "outbox",
3197
+ id: crypto.randomUUID(),
3198
+ baseUrl,
3199
+ keys,
3200
+ activity,
3201
+ activityId: activityId.href,
3202
+ activityType,
3203
+ inbox: inbox.href,
3204
+ sharedInbox: false,
3205
+ started,
3206
+ attempt: 0,
3207
+ headers: {},
3208
+ traceContext
3209
+ };
3210
+ try {
3211
+ await outboxQueue.enqueue(message);
3212
+ } catch (error) {
3213
+ logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
3214
+ activityId: activityId.href,
3215
+ inbox: inbox.href,
3216
+ error
3217
+ });
3218
+ return;
3219
+ }
3220
+ require_http.recordOutboxEnqueue(meterProvider, outboxQueue, message);
3221
+ }));
3222
+ }
3223
+ //#endregion
3224
+ //#region src/federation/temporal.ts
3225
+ const Temporal$7 = require_http.Temporal;
3226
+ function isPlainObject$1(value) {
3227
+ return typeof value === "object" && value != null && !Array.isArray(value);
3228
+ }
3229
+ function normalizeDateTimeLiteral(value) {
3230
+ return value.substring(19).match(/[Z+-]/) ? value : value + "Z";
3231
+ }
3232
+ function isMalformedDateTimeLiteral(value) {
3233
+ if (typeof value !== "string") return false;
3234
+ try {
3235
+ Temporal$7.Instant.from(normalizeDateTimeLiteral(value));
3236
+ return false;
3237
+ } catch {
3238
+ return true;
3239
+ }
3240
+ }
3241
+ function isMalformedDurationLiteral(value) {
3242
+ if (typeof value !== "string") return false;
3243
+ try {
3244
+ Temporal$7.Duration.from(value);
3245
+ return false;
3246
+ } catch {
3247
+ return true;
3248
+ }
3249
+ }
3250
+ const TEMPORAL_DATE_TIME_IRIS = /* @__PURE__ */ new Set([
3251
+ "https://www.w3.org/ns/activitystreams#deleted",
3252
+ "https://www.w3.org/ns/activitystreams#endTime",
3253
+ "https://www.w3.org/ns/activitystreams#published",
3254
+ "https://www.w3.org/ns/activitystreams#startTime",
3255
+ "https://www.w3.org/ns/activitystreams#updated",
3256
+ "http://purl.org/dc/terms/created",
3257
+ "https://w3id.org/security#created"
3258
+ ]);
3259
+ const TEMPORAL_DURATION_IRIS = /* @__PURE__ */ new Set(["https://www.w3.org/ns/activitystreams#duration"]);
3260
+ const QUESTION_CLOSED_IRI = "https://www.w3.org/ns/activitystreams#closed";
3261
+ const XSD_DATE_TIME_IRI = "http://www.w3.org/2001/XMLSchema#dateTime";
3262
+ function hasMalformedExpandedDateTimeLiteral(value) {
3263
+ if (Array.isArray(value)) return value.some(hasMalformedExpandedDateTimeLiteral);
3264
+ return isPlainObject$1(value) && "@value" in value && isMalformedDateTimeLiteral(value["@value"]);
3265
+ }
3266
+ function hasMalformedExpandedQuestionClosedLiteral(value) {
3267
+ if (Array.isArray(value)) return value.some(hasMalformedExpandedQuestionClosedLiteral);
3268
+ if (!isPlainObject$1(value) || !("@value" in value)) return false;
3269
+ const literal = value["@value"];
3270
+ if (typeof literal === "boolean") return false;
3271
+ if (typeof literal !== "string") return false;
3272
+ if (value["@type"] !== XSD_DATE_TIME_IRI) return false;
3273
+ if (new Date(literal).toString() === "Invalid Date") return false;
3274
+ return isMalformedDateTimeLiteral(literal);
3275
+ }
3276
+ function hasMalformedExpandedDurationLiteral(value) {
3277
+ if (Array.isArray(value)) return value.some(hasMalformedExpandedDurationLiteral);
3278
+ return isPlainObject$1(value) && "@value" in value && isMalformedDurationLiteral(value["@value"]);
3279
+ }
3280
+ function hasMalformedKnownTemporalLiteralInternal(value, visited) {
3281
+ if (Array.isArray(value)) return value.some((item) => hasMalformedKnownTemporalLiteralInternal(item, visited));
3282
+ if (!isPlainObject$1(value)) return false;
3283
+ if (visited.has(value)) return false;
3284
+ visited.add(value);
3285
+ if ("@value" in value) return false;
3286
+ for (const [key, child] of Object.entries(value)) {
3287
+ if (TEMPORAL_DATE_TIME_IRIS.has(key)) {
3288
+ if (hasMalformedExpandedDateTimeLiteral(child)) return true;
3289
+ continue;
3290
+ }
3291
+ if (key === QUESTION_CLOSED_IRI) {
3292
+ if (hasMalformedExpandedQuestionClosedLiteral(child)) return true;
3293
+ continue;
3294
+ }
3295
+ if (TEMPORAL_DURATION_IRIS.has(key)) {
3296
+ if (hasMalformedExpandedDurationLiteral(child)) return true;
3297
+ continue;
3298
+ }
3299
+ if (hasMalformedKnownTemporalLiteralInternal(child, visited)) return true;
3300
+ }
3301
+ return false;
3302
+ }
3303
+ async function hasMalformedKnownTemporalLiteral(value, contextLoader) {
3304
+ try {
3305
+ return hasMalformedKnownTemporalLiteralInternal(await _fedify_vocab_runtime_jsonld.default.expand(value, {
3306
+ documentLoader: require_http.getNormalizationContextLoader(contextLoader),
3307
+ keepFreeFloatingNodes: true
3308
+ }), /* @__PURE__ */ new Set());
3309
+ } catch {
3310
+ return false;
3311
+ }
3312
+ }
3313
+ //#endregion
3314
+ //#region src/federation/handler.ts
3315
+ const Temporal$6 = require_http.Temporal;
3316
+ const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
3317
+ const INBOX_COMPOUND_PROOF_LIMITS = {
3318
+ maxDepth: 64,
3319
+ maxMaps: 1e4,
3320
+ maxProofs: 32,
3321
+ maxBytes: 10 * 1024 * 1024
3322
+ };
3323
+ function isRemoteContextLoadingFailure$1(error) {
3324
+ return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
3325
+ }
3326
+ function isPermanentRemoteContextError$1(error) {
3327
+ if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
3328
+ const details = error.details;
3329
+ if (details?.code === "invalid remote context") return true;
3330
+ return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) && require_http.isClearlyMalformedContextReference(details.url);
3331
+ }
3332
+ function isInvalidJsonLdError(error) {
2431
3333
  if (!(error instanceof Error)) return false;
2432
3334
  const name = error.name;
2433
3335
  return name === "UnsafeJsonLdError" || error instanceof require_http.InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
@@ -2569,14 +3471,7 @@ async function handlePortableObject(request, { values, context, objectDispatcher
2569
3471
  } });
2570
3472
  }
2571
3473
  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
- }
3474
+ return object.id != null && require_http.getCanonicalPortableId(object.id) === canonicalId;
2580
3475
  }
2581
3476
  async function getJsonLdRootId(jsonLd, contextLoader) {
2582
3477
  const expanded = await _fedify_vocab_runtime_jsonld.default.expand(jsonLd, {
@@ -2588,6 +3483,7 @@ async function getJsonLdRootId(jsonLd, contextLoader) {
2588
3483
  return typeof id === "string" ? id : null;
2589
3484
  }
2590
3485
  function isRequestedPortableId(id, canonicalId) {
3486
+ if (!/^ap(?:\+ef61)?:\/\//i.test(id)) return URL.canParse(id) && require_http.getCanonicalPortableId(new URL(id)) === canonicalId;
2591
3487
  try {
2592
3488
  return (0, _fedify_vocab_runtime.canonicalizePortableUri)(id) === canonicalId;
2593
3489
  } catch (error) {
@@ -3450,7 +4346,7 @@ async function handleInboxInternal(request, parameters, span) {
3450
4346
  return await respondInvalidActivity(error);
3451
4347
  }
3452
4348
  activityVerified = true;
3453
- if (!skipSignatureVerification && require_http.hasPortableActor(activity)) try {
4349
+ if (!skipSignatureVerification && isPortableActivity(activity)) try {
3454
4350
  proofVerified = await require_http.verifyObject(_fedify_vocab.Activity, jsonWithoutSig, {
3455
4351
  contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader),
3456
4352
  documentLoader: ctx.documentLoader,
@@ -3634,7 +4530,7 @@ async function handleInboxInternal(request, parameters, span) {
3634
4530
  "http_signatures.verified": httpSigKey != null,
3635
4531
  "http_signatures.key_id": httpSigKey?.id?.href ?? ""
3636
4532
  });
3637
- if (!skipSignatureVerification && !proofVerified && (require_http.hasPortableActor(activity) || isPortableGatewayKey(httpSigKey))) {
4533
+ if (!skipSignatureVerification && !proofVerified && (isPortableActivity(activity) || isPortableGatewayKey(httpSigKey))) {
3638
4534
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
3639
4535
  logger.error("The activity {activityId} of the portable actor {actorId} is not authenticated by a valid Object Integrity Proof.", {
3640
4536
  activity: json,
@@ -3652,6 +4548,32 @@ async function handleInboxInternal(request, parameters, span) {
3652
4548
  headers: { "Content-Type": "text/plain; charset=utf-8" }
3653
4549
  });
3654
4550
  }
4551
+ if (!skipSignatureVerification && activity.id != null && require_http.isPortableId(activity.id)) {
4552
+ const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
4553
+ contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader),
4554
+ documentLoader: ctx.documentLoader,
4555
+ keyCache,
4556
+ meterProvider,
4557
+ tracerProvider
4558
+ });
4559
+ if (rejection != null) {
4560
+ if (deferredLdSignatureError != null) throw deferredLdSignatureError;
4561
+ logger.error("The portable activity {activityId} is not authenticated by an Object Integrity Proof of its own DID: {reason}", {
4562
+ activity: json,
4563
+ recipient,
4564
+ activityId: activity.id.href,
4565
+ reason: rejection
4566
+ });
4567
+ span.setStatus({
4568
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
4569
+ message: `The portable activity (${activity.id.href}) is not authenticated by an Object Integrity Proof of its own DID.`
4570
+ });
4571
+ return new Response("Portable activities must have valid Object Integrity Proofs made by their own DIDs.", {
4572
+ status: 401,
4573
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
4574
+ });
4575
+ }
4576
+ }
3655
4577
  if (httpSigKey != null && !await require_owner.doesActorOwnKey(activity, httpSigKey, ctx)) {
3656
4578
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
3657
4579
  require_http.getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : require_http.getRemoteHost(httpSigKey.id));
@@ -3728,7 +4650,21 @@ async function handleInboxInternal(request, parameters, span) {
3728
4650
  inbox: portableInbox.recipient.canonicalInboxId
3729
4651
  });
3730
4652
  else try {
3731
- await portableInbox.forward(json, activity.id, (0, _fedify_vocab.getTypeId)(activity).href);
4653
+ await portableInbox.forward(json, activity.id, (0, _fedify_vocab.getTypeId)(activity).href, require_http.listRequestSignatures(request).map((signature) => ({
4654
+ keyId: signature.keyId,
4655
+ coversDelivery: coversDelivery(signature),
4656
+ verify: async () => {
4657
+ const verification = await require_http.verifyRequestDetailed(require_http.selectRequestSignature(request, signature), {
4658
+ contextLoader: ctx.contextLoader,
4659
+ documentLoader: ctx.documentLoader,
4660
+ timeWindow: signatureTimeWindow,
4661
+ keyCache,
4662
+ meterProvider,
4663
+ tracerProvider
4664
+ });
4665
+ return verification.verified ? verification.key : null;
4666
+ }
4667
+ })));
3732
4668
  } catch (error) {
3733
4669
  logger.error("Failed to forward activity {activityId} to the other gateways of the portable inbox {inbox}:\n{error}", {
3734
4670
  activityId: activity.id.href,
@@ -4223,580 +5159,324 @@ const authIfNeeded = async (context, values, { authorizePredicate: authorize = v
4223
5159
  /**
4224
5160
  * Appends a cursor parameter to a URL if the cursor exists.
4225
5161
  * @template Cursor The type of the cursor (string, null, or undefined).
4226
- * @param {URL} url The base URL to append the cursor to.
4227
- * @param {string | null | undefined} cursor The cursor value to append.
4228
- * @returns The URL with cursor appended if cursor is a string, null otherwise.
4229
- * @since 1.8.0
4230
- */
4231
- const appendCursorIfExists = (url, cursor) => {
4232
- if (cursor === null || cursor === void 0) return null;
4233
- const copied = new URL(url);
4234
- copied.searchParams.set("cursor", cursor);
4235
- return copied;
4236
- };
4237
- /**
4238
- * Creates an HTTP response for ActivityPub data.
4239
- * @param {unknown} data The data to serialize as JSON-LD.
4240
- * @returns {Response} An HTTP response with the data as ActivityPub JSON.
4241
- * @since 1.8.0
4242
- */
4243
- const respondAsActivity = (data) => new Response(JSON.stringify(data), { headers: {
4244
- "Content-Type": "application/activity+json",
4245
- Vary: "Accept"
4246
- } });
4247
- /**
4248
- * Base class for handler errors.
4249
- * @since 1.8.0
4250
- */
4251
- var HandlerError = class extends Error {
4252
- constructor(message) {
4253
- super(message);
4254
- }
4255
- /**
4256
- * Throws this error.
4257
- * @returns Never returns, always throws.
4258
- */
4259
- throw() {
4260
- throw this;
4261
- }
4262
- };
4263
- /**
4264
- * Error thrown when items are not found in a collection.
4265
- * @since 1.8.0
4266
- */
4267
- var ItemsNotFoundError = class extends HandlerError {
4268
- constructor() {
4269
- super("Items not found in the collection.");
4270
- }
4271
- };
4272
- /**
4273
- * Error thrown when access to a collection is unauthorized.
4274
- * @since 1.8.0
4275
- */
4276
- var UnauthorizedError = class extends HandlerError {
4277
- constructor() {
4278
- super("Unauthorized access to the collection.");
4279
- }
4280
- };
4281
- /**
4282
- * Responds with the given object in JSON-LD format.
4283
- *
4284
- * @param object The object to respond with.
4285
- * @param options Options.
4286
- * @since 0.3.0
4287
- */
4288
- async function respondWithObject(object, options) {
4289
- const jsonLd = await object.toJsonLd(options);
4290
- return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json" } });
4291
- }
4292
- /**
4293
- * Responds with the given object in JSON-LD format if the request accepts
4294
- * JSON-LD.
4295
- *
4296
- * @param object The object to respond with.
4297
- * @param request The request to check for JSON-LD acceptability.
4298
- * @param options Options.
4299
- * @since 0.3.0
4300
- */
4301
- async function respondWithObjectIfAcceptable(object, request, options) {
4302
- if (!acceptsJsonLd(request)) return null;
4303
- const response = await respondWithObject(object, options);
4304
- response.headers.set("Vary", "Accept");
4305
- return response;
4306
- }
4307
- function generateNonce() {
4308
- const bytes = /* @__PURE__ */ new Uint8Array(16);
4309
- crypto.getRandomValues(bytes);
4310
- return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
4311
- }
4312
- async function verifySignatureNonce(request, kv, noncePrefix, verifiedLabel) {
4313
- const signatureInput = request.headers.get("Signature-Input");
4314
- if (signatureInput == null) return false;
4315
- const parsed = require_http.parseRfc9421SignatureInput(signatureInput);
4316
- if (verifiedLabel == null) return false;
4317
- const sig = parsed[verifiedLabel];
4318
- if (sig == null) return false;
4319
- const nonce = sig.nonce;
4320
- if (nonce == null) return false;
4321
- const key = [...noncePrefix, nonce];
4322
- if (kv.cas != null) return await kv.cas(key, true, void 0);
4323
- if (await kv.get(key) != null) {
4324
- await kv.delete(key);
4325
- return true;
4326
- }
4327
- return false;
4328
- }
4329
- const getFailedSignatureResponse = async (policy, kv, kvPrefixes) => {
4330
- const headers = await getFailedSignatureHeaders(policy, kv, kvPrefixes);
4331
- return new Response("Failed to verify the request signature.", {
4332
- status: 401,
4333
- headers
4334
- });
4335
- };
4336
- const getFailedSignatureHeaders = async (policy, kv, kvPrefixes) => ({
4337
- "Content-Type": "text/plain; charset=utf-8",
4338
- ...policy?.enabled && {
4339
- "Accept-Signature": await buildAcceptSignatureHeader(policy, kv, kvPrefixes.acceptSignatureNonce),
4340
- "Cache-Control": "no-store",
4341
- "Vary": "Accept, Signature"
4342
- }
4343
- });
4344
- async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
4345
- const parameters = { created: true };
4346
- if (policy.requestNonce) {
4347
- const nonce = generateNonce();
4348
- await setKey(kv, [...noncePrefix, nonce], policy);
4349
- parameters.nonce = nonce;
4350
- }
4351
- const baseComponents = policy.components ?? DEF_COMPONENTS;
4352
- return require_http.formatAcceptSignature([{
4353
- label: "sig1",
4354
- components: (0, es_toolkit.uniq)(MIN_COMPONENTS.concat(baseComponents)).filter((c) => c !== "@status").map((v) => ({
4355
- value: v,
4356
- params: {}
4357
- })),
4358
- parameters
4359
- }]);
4360
- }
4361
- async function setKey(kv, key, policy) {
4362
- const seconds = policy.nonceTtlSeconds ?? 300;
4363
- const ttl = Temporal$7.Duration.from({ seconds });
4364
- await kv.set(key, true, { ttl });
4365
- }
4366
- const DEF_COMPONENTS = [
4367
- "@method",
4368
- "@target-uri",
4369
- "@authority",
4370
- "content-digest"
4371
- ];
4372
- const MIN_COMPONENTS = [
4373
- "@method",
4374
- "@target-uri",
4375
- "@authority"
4376
- ];
4377
- /**
4378
- * Checks whether an HTTP Signature key is a gateway key of an FEP-ef61
4379
- * portable actor, i.e., its ID is a compatible identifier and it is owned by
4380
- * a portable actor, which only the portable actor's signed document can
4381
- * establish.
4382
- */
4383
- function isPortableGatewayKey(key) {
4384
- return key?.id != null && require_http.isCompatibleKeyId(key.id) && key.ownerId != null && require_http.isPortableUri(key.ownerId);
4385
- }
4386
- //#endregion
4387
- //#region src/federation/kv.ts
4388
- const Temporal$6 = require_http.Temporal;
4389
- /**
4390
- * A key–value store that stores values in memory.
4391
- * Do not use this in production as it does not persist values.
4392
- *
4393
- * @since 0.5.0
4394
- */
4395
- var MemoryKvStore = class {
4396
- #values = {};
4397
- #encodeKey(key) {
4398
- return JSON.stringify(key);
4399
- }
4400
- /**
4401
- * {@inheritDoc KvStore.get}
4402
- */
4403
- get(key) {
4404
- const encodedKey = this.#encodeKey(key);
4405
- const entry = this.#values[encodedKey];
4406
- if (entry == null) return Promise.resolve(void 0);
4407
- const [value, expiration] = entry;
4408
- if (expiration != null && Temporal$6.Now.instant().until(expiration).sign < 0) {
4409
- delete this.#values[encodedKey];
4410
- return Promise.resolve(void 0);
4411
- }
4412
- return Promise.resolve(value);
4413
- }
4414
- /**
4415
- * {@inheritDoc KvStore.set}
4416
- */
4417
- set(key, value, options) {
4418
- const encodedKey = this.#encodeKey(key);
4419
- const expiration = options?.ttl == null ? null : Temporal$6.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
4420
- this.#values[encodedKey] = [value, expiration];
4421
- return Promise.resolve();
4422
- }
4423
- /**
4424
- * {@inheritDoc KvStore.delete}
4425
- */
4426
- delete(key) {
4427
- const encodedKey = this.#encodeKey(key);
4428
- delete this.#values[encodedKey];
4429
- return Promise.resolve();
4430
- }
4431
- /**
4432
- * {@inheritDoc KvStore.cas}
4433
- */
4434
- cas(key, expectedValue, newValue, options) {
4435
- const encodedKey = this.#encodeKey(key);
4436
- const entry = this.#values[encodedKey];
4437
- let currentValue;
4438
- if (entry == null) currentValue = void 0;
4439
- else {
4440
- const [value, expiration] = entry;
4441
- if (expiration != null && Temporal$6.Now.instant().until(expiration).sign < 0) {
4442
- delete this.#values[encodedKey];
4443
- currentValue = void 0;
4444
- } else currentValue = value;
4445
- }
4446
- if (!(0, es_toolkit.isEqual)(currentValue, expectedValue)) return Promise.resolve(false);
4447
- const expiration = options?.ttl == null ? null : Temporal$6.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
4448
- this.#values[encodedKey] = [newValue, expiration];
4449
- return Promise.resolve(true);
5162
+ * @param {URL} url The base URL to append the cursor to.
5163
+ * @param {string | null | undefined} cursor The cursor value to append.
5164
+ * @returns The URL with cursor appended if cursor is a string, null otherwise.
5165
+ * @since 1.8.0
5166
+ */
5167
+ const appendCursorIfExists = (url, cursor) => {
5168
+ if (cursor === null || cursor === void 0) return null;
5169
+ const copied = new URL(url);
5170
+ copied.searchParams.set("cursor", cursor);
5171
+ return copied;
5172
+ };
5173
+ /**
5174
+ * Creates an HTTP response for ActivityPub data.
5175
+ * @param {unknown} data The data to serialize as JSON-LD.
5176
+ * @returns {Response} An HTTP response with the data as ActivityPub JSON.
5177
+ * @since 1.8.0
5178
+ */
5179
+ const respondAsActivity = (data) => new Response(JSON.stringify(data), { headers: {
5180
+ "Content-Type": "application/activity+json",
5181
+ Vary: "Accept"
5182
+ } });
5183
+ /**
5184
+ * Base class for handler errors.
5185
+ * @since 1.8.0
5186
+ */
5187
+ var HandlerError = class extends Error {
5188
+ constructor(message) {
5189
+ super(message);
4450
5190
  }
4451
5191
  /**
4452
- * {@inheritDoc KvStore.list}
5192
+ * Throws this error.
5193
+ * @returns Never returns, always throws.
4453
5194
  */
4454
- async *list(prefix) {
4455
- const now = Temporal$6.Now.instant();
4456
- for (const [encodedKey, entry] of Object.entries(this.#values)) {
4457
- const key = JSON.parse(encodedKey);
4458
- if (prefix != null) {
4459
- if (key.length < prefix.length) continue;
4460
- if (!prefix.every((p, i) => key[i] === p)) continue;
4461
- }
4462
- const [value, expiration] = entry;
4463
- if (expiration != null && now.until(expiration).sign < 0) {
4464
- delete this.#values[encodedKey];
4465
- continue;
4466
- }
4467
- yield {
4468
- key,
4469
- value
4470
- };
4471
- }
5195
+ throw() {
5196
+ throw this;
4472
5197
  }
4473
5198
  };
4474
- //#endregion
4475
- //#region src/nodeinfo/handler.ts
4476
5199
  /**
4477
- * Handles a NodeInfo request. You would not typically call this function
4478
- * directly, but instead use {@link Federation.handle} method.
4479
- * @param request The NodeInfo request to handle.
4480
- * @param parameters The parameters for handling the request.
4481
- * @returns The response to the request.
5200
+ * Error thrown when items are not found in a collection.
5201
+ * @since 1.8.0
4482
5202
  */
4483
- async function handleNodeInfo(_request, { context, nodeInfoDispatcher }) {
4484
- const promise = nodeInfoDispatcher(context);
4485
- const json = require_types.nodeInfoToJson(promise instanceof Promise ? await promise : promise);
4486
- return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\"" } });
4487
- }
5203
+ var ItemsNotFoundError = class extends HandlerError {
5204
+ constructor() {
5205
+ super("Items not found in the collection.");
5206
+ }
5207
+ };
4488
5208
  /**
4489
- * Handles a request to `/.well-known/nodeinfo`. You would not typically call
4490
- * this function directly, but instead use {@link Federation.handle} method.
4491
- * @param request The request to handle.
4492
- * @param context The request context.
4493
- * @returns The response to the request.
5209
+ * Error thrown when access to a collection is unauthorized.
5210
+ * @since 1.8.0
4494
5211
  */
4495
- function handleNodeInfoJrd(_request, context) {
4496
- const links = [];
4497
- try {
4498
- links.push({
4499
- rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
4500
- href: context.getNodeInfoUri().href,
4501
- type: "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\""
4502
- });
4503
- } catch (e) {
4504
- if (!(e instanceof _fedify_uri_template.RouterError)) throw e;
5212
+ var UnauthorizedError = class extends HandlerError {
5213
+ constructor() {
5214
+ super("Unauthorized access to the collection.");
4505
5215
  }
4506
- const response = new Response(JSON.stringify({ links }), { headers: { "Content-Type": "application/jrd+json" } });
4507
- return Promise.resolve(response);
4508
- }
4509
- //#endregion
4510
- //#region src/federation/send.ts
5216
+ };
4511
5217
  /**
4512
- * Extracts the inbox URLs from recipients.
4513
- * @param parameters The parameters to extract the inboxes.
4514
- * See also {@link ExtractInboxesParameters}.
4515
- * @returns The inboxes as a map of inbox URL to actor URIs.
5218
+ * Responds with the given object in JSON-LD format.
5219
+ *
5220
+ * @param object The object to respond with.
5221
+ * @param options Options.
5222
+ * @since 0.3.0
4516
5223
  */
4517
- function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
4518
- const inboxes = {};
4519
- for (const recipient of recipients) {
4520
- let inbox;
4521
- let sharedInbox = false;
4522
- if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) {
4523
- inbox = recipient.endpoints.sharedInbox;
4524
- sharedInbox = true;
4525
- } else inbox = recipient.inboxId;
4526
- if (inbox != null && recipient.id != null) {
4527
- if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
4528
- inboxes[inbox.href] ??= {
4529
- actorIds: /* @__PURE__ */ new Set(),
4530
- sharedInbox
4531
- };
4532
- inboxes[inbox.href].actorIds.add(recipient.id.href);
4533
- }
4534
- }
4535
- return inboxes;
5224
+ async function respondWithObject(object, options) {
5225
+ const jsonLd = await object.toJsonLd(options);
5226
+ return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json" } });
4536
5227
  }
4537
5228
  /**
4538
- * Sends an {@link Activity} to an inbox.
5229
+ * Responds with the given object in JSON-LD format if the request accepts
5230
+ * JSON-LD.
4539
5231
  *
4540
- * @param parameters The parameters for sending the activity.
4541
- * See also {@link SendActivityParameters}.
4542
- * @throws {Error} If the activity fails to send.
5232
+ * @param object The object to respond with.
5233
+ * @param request The request to check for JSON-LD acceptability.
5234
+ * @param options Options.
5235
+ * @since 0.3.0
4543
5236
  */
4544
- function sendActivity(options) {
4545
- const tracerProvider = options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider();
4546
- return tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan("activitypub.send_activity", {
4547
- kind: _opentelemetry_api.SpanKind.CLIENT,
4548
- attributes: { "activitypub.shared_inbox": options.sharedInbox ?? false }
4549
- }, async (span) => {
4550
- if (options.activityId != null) span.setAttribute("activitypub.activity.id", options.activityId);
4551
- if (options.activityType != null) span.setAttribute("activitypub.activity.type", options.activityType);
4552
- try {
4553
- await sendActivityInternal({
4554
- ...options,
4555
- tracerProvider
4556
- }, span);
4557
- } catch (e) {
4558
- span.setStatus({
4559
- code: _opentelemetry_api.SpanStatusCode.ERROR,
4560
- message: String(e)
4561
- });
4562
- throw e;
4563
- } finally {
4564
- span.end();
4565
- }
4566
- });
4567
- }
4568
- const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
4569
- function getActivityActorId(activity) {
4570
- if (!isRecord(activity)) return void 0;
4571
- return getIdValue(activity.actor);
4572
- }
4573
- function getIdValue(value) {
4574
- if (typeof value === "string" && value !== "") return value;
4575
- if (value instanceof URL) return value.href;
4576
- if (Array.isArray(value)) {
4577
- for (const item of value) {
4578
- const id = getIdValue(item);
4579
- if (id != null) return id;
4580
- }
4581
- return;
4582
- }
4583
- if (isRecord(value)) return getIdValue(value.id);
4584
- }
4585
- function isRecord(value) {
4586
- return typeof value === "object" && value != null;
5237
+ async function respondWithObjectIfAcceptable(object, request, options) {
5238
+ if (!acceptsJsonLd(request)) return null;
5239
+ const response = await respondWithObject(object, options);
5240
+ response.headers.set("Vary", "Accept");
5241
+ return response;
4587
5242
  }
4588
- async function readLimitedResponseBody(response, maxBytes) {
4589
- if (response.body == null) return "";
4590
- const reader = response.body.getReader();
4591
- const decoder = new TextDecoder();
4592
- const chunks = [];
4593
- let totalBytes = 0;
4594
- let truncated = false;
4595
- try {
4596
- while (true) {
4597
- const { done, value } = await reader.read();
4598
- if (done) break;
4599
- if (totalBytes + value.length > maxBytes) {
4600
- const remaining = maxBytes - totalBytes;
4601
- if (remaining > 0) chunks.push(decoder.decode(value.slice(0, remaining), { stream: true }));
4602
- truncated = true;
4603
- break;
4604
- }
4605
- chunks.push(decoder.decode(value, { stream: true }));
4606
- totalBytes += value.length;
4607
- }
4608
- } finally {
4609
- reader.releaseLock();
4610
- }
4611
- let result = chunks.join("");
4612
- if (truncated) result += "… (truncated)";
4613
- return result;
5243
+ function generateNonce() {
5244
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
5245
+ crypto.getRandomValues(bytes);
5246
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
4614
5247
  }
4615
- async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
4616
- const logger = (0, _logtape_logtape.getLogger)([
4617
- "fedify",
4618
- "federation",
4619
- "outbox"
4620
- ]);
4621
- const federationMetrics = require_http.getFederationMetrics(meterProvider);
4622
- const started = performance.now();
4623
- let deliverySuccess = false;
4624
- async function validateUrl(url) {
4625
- if (!allowPrivateAddress) try {
4626
- await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
4627
- } catch (error) {
4628
- if (error instanceof _fedify_vocab_runtime.UrlError) logger.error("Disallowed private URL: {url}", {
4629
- url,
4630
- error
4631
- });
4632
- throw error;
4633
- }
4634
- }
4635
- await validateUrl(inbox.href);
4636
- headers = new Headers(headers);
4637
- headers.set("Content-Type", "application/activity+json");
4638
- const request = new Request(inbox, {
4639
- method: "POST",
4640
- headers,
4641
- body: JSON.stringify(activity),
4642
- redirect: "manual"
4643
- });
4644
- let rsaKey = null;
4645
- for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
4646
- rsaKey = key;
4647
- break;
5248
+ async function verifySignatureNonce(request, kv, noncePrefix, verifiedLabel) {
5249
+ const signatureInput = request.headers.get("Signature-Input");
5250
+ if (signatureInput == null) return false;
5251
+ const parsed = require_http.parseRfc9421SignatureInput(signatureInput);
5252
+ if (verifiedLabel == null) return false;
5253
+ const sig = parsed[verifiedLabel];
5254
+ if (sig == null) return false;
5255
+ const nonce = sig.nonce;
5256
+ if (nonce == null) return false;
5257
+ const key = [...noncePrefix, nonce];
5258
+ if (kv.cas != null) return await kv.cas(key, true, void 0);
5259
+ if (await kv.get(key) != null) {
5260
+ await kv.delete(key);
5261
+ return true;
4648
5262
  }
4649
- if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
4650
- activityId,
4651
- inbox: inbox.href
4652
- });
4653
- else if (rsaKey == null) logger.warn("No supported key found to sign the request to {inbox}. The request will be sent without a signature. In order to sign the request, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
4654
- inbox: inbox.href,
4655
- keys: keys.map((pair) => ({
4656
- keyId: pair.keyId.href,
4657
- privateKey: pair.privateKey
4658
- }))
5263
+ return false;
5264
+ }
5265
+ const getFailedSignatureResponse = async (policy, kv, kvPrefixes) => {
5266
+ const headers = await getFailedSignatureHeaders(policy, kv, kvPrefixes);
5267
+ return new Response("Failed to verify the request signature.", {
5268
+ status: 401,
5269
+ headers
4659
5270
  });
4660
- let response;
4661
- try {
4662
- response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await require_http.doubleKnock(request, rsaKey, {
4663
- tracerProvider,
4664
- specDeterminer,
4665
- validateRedirect: validateUrl
4666
- });
4667
- } catch (error) {
4668
- const failure = error instanceof _fedify_vocab_runtime.FetchError || error instanceof _fedify_vocab_runtime.UrlError ? error : createFetchError(inbox.href, error);
4669
- logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
4670
- activityId,
4671
- inbox: inbox.href,
4672
- error: failure
4673
- });
4674
- federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
4675
- throw failure;
5271
+ };
5272
+ const getFailedSignatureHeaders = async (policy, kv, kvPrefixes) => ({
5273
+ "Content-Type": "text/plain; charset=utf-8",
5274
+ ...policy?.enabled && {
5275
+ "Accept-Signature": await buildAcceptSignatureHeader(policy, kv, kvPrefixes.acceptSignatureNonce),
5276
+ "Cache-Control": "no-store",
5277
+ "Vary": "Accept, Signature"
4676
5278
  }
4677
- try {
4678
- if (!response.ok) {
4679
- let error;
4680
- try {
4681
- error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
4682
- } catch (_) {
4683
- error = "";
4684
- }
4685
- logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
4686
- activityId,
4687
- inbox: inbox.href,
4688
- status: response.status,
4689
- statusText: response.statusText,
4690
- error
4691
- });
4692
- throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error, response.headers);
4693
- }
4694
- deliverySuccess = true;
4695
- const eventAttributes = {
4696
- "activitypub.inbox.url": inbox.href,
4697
- "activitypub.activity.id": activityId ?? ""
4698
- };
4699
- if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
4700
- const actorId = getActivityActorId(activity);
4701
- if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
4702
- span.addEvent("activitypub.activity.sent", eventAttributes);
4703
- } finally {
4704
- federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), deliverySuccess, activityType);
5279
+ });
5280
+ async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
5281
+ const parameters = { created: true };
5282
+ if (policy.requestNonce) {
5283
+ const nonce = generateNonce();
5284
+ await setKey(kv, [...noncePrefix, nonce], policy);
5285
+ parameters.nonce = nonce;
4705
5286
  }
5287
+ const baseComponents = policy.components ?? DEF_COMPONENTS;
5288
+ return require_http.formatAcceptSignature([{
5289
+ label: "sig1",
5290
+ components: (0, es_toolkit.uniq)(MIN_COMPONENTS.concat(baseComponents)).filter((c) => c !== "@status").map((v) => ({
5291
+ value: v,
5292
+ params: {}
5293
+ })),
5294
+ parameters
5295
+ }]);
4706
5296
  }
4707
- function createFetchError(url, cause) {
4708
- const error = new _fedify_vocab_runtime.FetchError(url, cause instanceof Error ? cause.message : String(cause));
4709
- error.cause = cause;
4710
- return error;
5297
+ async function setKey(kv, key, policy) {
5298
+ const seconds = policy.nonceTtlSeconds ?? 300;
5299
+ const ttl = Temporal$6.Duration.from({ seconds });
5300
+ await kv.set(key, true, { ttl });
4711
5301
  }
4712
- async function fetchWithValidatedRedirects(request, validateUrl) {
4713
- let body = await request.clone().text();
4714
- for (let redirects = 0;; redirects++) {
4715
- const response = await fetch(request, { redirect: "manual" });
4716
- const location = response.headers.get("Location");
4717
- if (![
4718
- 301,
4719
- 302,
4720
- 303,
4721
- 307,
4722
- 308
4723
- ].includes(response.status) || location == null) return response;
4724
- await response.body?.cancel();
4725
- if (redirects >= 20) throw new TypeError("Too many redirects");
4726
- const url = new URL(location, request.url);
4727
- await validateUrl(url.href);
4728
- const headers = new Headers(request.headers);
4729
- if (url.origin !== new URL(request.url).origin) {
4730
- headers.delete("Authorization");
4731
- headers.delete("Proxy-Authorization");
4732
- headers.delete("Cookie");
4733
- headers.delete("Host");
4734
- }
4735
- let method = request.method;
4736
- if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
4737
- method = "GET";
4738
- body = void 0;
4739
- for (const name of [
4740
- "Content-Encoding",
4741
- "Content-Language",
4742
- "Content-Length",
4743
- "Content-Location",
4744
- "Content-Type"
4745
- ]) headers.delete(name);
4746
- }
4747
- request = new Request(url, {
4748
- method,
4749
- headers,
4750
- body,
4751
- redirect: "manual"
4752
- });
5302
+ const DEF_COMPONENTS = [
5303
+ "@method",
5304
+ "@target-uri",
5305
+ "@authority",
5306
+ "content-digest"
5307
+ ];
5308
+ const MIN_COMPONENTS = [
5309
+ "@method",
5310
+ "@target-uri",
5311
+ "@authority"
5312
+ ];
5313
+ /**
5314
+ * Checks whether an HTTP Signature key is a gateway key of an FEP-ef61
5315
+ * portable actor, i.e., its ID is a compatible identifier and it is owned by
5316
+ * a portable actor, which only the portable actor's signed document can
5317
+ * establish.
5318
+ */
5319
+ function isPortableGatewayKey(key) {
5320
+ return key?.id != null && require_http.isCompatibleKeyId(key.id) && key.ownerId != null && require_http.isPortableId(key.ownerId);
5321
+ }
5322
+ /**
5323
+ * Checks whether an activity needs an Object Integrity Proof by its DID:
5324
+ * either it is performed by an [FEP-ef61] portable actor, or its own ID is
5325
+ * portable. Both `ap:` URIs and compatible identifiers count as portable.
5326
+ *
5327
+ * [FEP-ef61]: https://w3id.org/fep/ef61
5328
+ */
5329
+ function isPortableActivity(activity) {
5330
+ return require_http.hasPortableActor(activity) || activity.id != null && require_http.isPortableId(activity.id);
5331
+ }
5332
+ /**
5333
+ * Verifies that a portable activity's own ID belongs to the DID that signed
5334
+ * it, on the JSON-LD document received. The ID the proof policy checks has
5335
+ * to be the parsed activity's ID, so that a remote context that changes
5336
+ * between loads cannot make the two name different objects.
5337
+ * @returns `null` if the ID is verified, or the reason why it is not.
5338
+ */
5339
+ async function verifyPortableActivityId(activityId, jsonLd, options) {
5340
+ const expectedId = require_http.getCanonicalPortableId(activityId);
5341
+ if (expectedId == null) return "malformed portable ID";
5342
+ let verification;
5343
+ try {
5344
+ verification = await require_http.verifyPortableObjectProofWithRoot(jsonLd, options);
5345
+ } catch (error) {
5346
+ if (error instanceof require_http.InvalidPortableObjectIdError) return "malformed portable ID";
5347
+ if (!isPermanentActivityParseError(error)) throw error;
5348
+ return "malformed document";
4753
5349
  }
5350
+ const { result, root } = verification;
5351
+ if (!result.verified) return result.reason.type;
5352
+ const rootId = root?.["@id"];
5353
+ if (typeof rootId !== "string" || require_http.getCanonicalPortableId((0, _fedify_vocab_runtime.parseIri)(rootId)) !== expectedId) return "the verified document has another ID";
5354
+ return null;
4754
5355
  }
5356
+ //#endregion
5357
+ //#region src/federation/kv.ts
5358
+ const Temporal$5 = require_http.Temporal;
4755
5359
  /**
4756
- * An error that is thrown when an activity fails to send to a remote inbox.
4757
- * It contains structured information about the failure, including the HTTP
4758
- * status code, the inbox URL, and the response body.
4759
- * @since 2.0.0
5360
+ * A key–value store that stores values in memory.
5361
+ * Do not use this in production as it does not persist values.
5362
+ *
5363
+ * @since 0.5.0
4760
5364
  */
4761
- var SendActivityError = class extends Error {
5365
+ var MemoryKvStore = class {
5366
+ #values = {};
5367
+ #encodeKey(key) {
5368
+ return JSON.stringify(key);
5369
+ }
4762
5370
  /**
4763
- * The inbox URL that the activity was being sent to.
5371
+ * {@inheritDoc KvStore.get}
4764
5372
  */
4765
- inbox;
5373
+ get(key) {
5374
+ const encodedKey = this.#encodeKey(key);
5375
+ const entry = this.#values[encodedKey];
5376
+ if (entry == null) return Promise.resolve(void 0);
5377
+ const [value, expiration] = entry;
5378
+ if (expiration != null && Temporal$5.Now.instant().until(expiration).sign < 0) {
5379
+ delete this.#values[encodedKey];
5380
+ return Promise.resolve(void 0);
5381
+ }
5382
+ return Promise.resolve(value);
5383
+ }
4766
5384
  /**
4767
- * The HTTP status code returned by the inbox.
5385
+ * {@inheritDoc KvStore.set}
4768
5386
  */
4769
- statusCode;
5387
+ set(key, value, options) {
5388
+ const encodedKey = this.#encodeKey(key);
5389
+ const expiration = options?.ttl == null ? null : Temporal$5.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
5390
+ this.#values[encodedKey] = [value, expiration];
5391
+ return Promise.resolve();
5392
+ }
4770
5393
  /**
4771
- * The response body from the inbox, if any. Note that this may be
4772
- * truncated to a maximum of 1 KiB to prevent excessive memory consumption
4773
- * when remote servers return large error pages (e.g., Cloudflare error pages).
4774
- * If truncated, the string will end with `"… (truncated)"`.
5394
+ * {@inheritDoc KvStore.delete}
4775
5395
  */
4776
- responseBody;
5396
+ delete(key) {
5397
+ const encodedKey = this.#encodeKey(key);
5398
+ delete this.#values[encodedKey];
5399
+ return Promise.resolve();
5400
+ }
4777
5401
  /**
4778
- * The response headers from the inbox.
4779
- * @since 2.3.0
5402
+ * {@inheritDoc KvStore.cas}
4780
5403
  */
4781
- responseHeaders;
5404
+ cas(key, expectedValue, newValue, options) {
5405
+ const encodedKey = this.#encodeKey(key);
5406
+ const entry = this.#values[encodedKey];
5407
+ let currentValue;
5408
+ if (entry == null) currentValue = void 0;
5409
+ else {
5410
+ const [value, expiration] = entry;
5411
+ if (expiration != null && Temporal$5.Now.instant().until(expiration).sign < 0) {
5412
+ delete this.#values[encodedKey];
5413
+ currentValue = void 0;
5414
+ } else currentValue = value;
5415
+ }
5416
+ if (!(0, es_toolkit.isEqual)(currentValue, expectedValue)) return Promise.resolve(false);
5417
+ const expiration = options?.ttl == null ? null : Temporal$5.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
5418
+ this.#values[encodedKey] = [newValue, expiration];
5419
+ return Promise.resolve(true);
5420
+ }
4782
5421
  /**
4783
- * Creates a new {@link SendActivityError}.
4784
- * @param inbox The inbox URL.
4785
- * @param statusCode The HTTP status code.
4786
- * @param message The error message.
4787
- * @param responseBody The response body.
4788
- * @param responseHeaders The response headers.
5422
+ * {@inheritDoc KvStore.list}
4789
5423
  */
4790
- constructor(inbox, statusCode, message, responseBody, responseHeaders) {
4791
- super(message);
4792
- this.name = "SendActivityError";
4793
- this.inbox = inbox;
4794
- this.statusCode = statusCode;
4795
- this.responseBody = responseBody;
4796
- this.responseHeaders = new Headers(responseHeaders);
5424
+ async *list(prefix) {
5425
+ const now = Temporal$5.Now.instant();
5426
+ for (const [encodedKey, entry] of Object.entries(this.#values)) {
5427
+ const key = JSON.parse(encodedKey);
5428
+ if (prefix != null) {
5429
+ if (key.length < prefix.length) continue;
5430
+ if (!prefix.every((p, i) => key[i] === p)) continue;
5431
+ }
5432
+ const [value, expiration] = entry;
5433
+ if (expiration != null && now.until(expiration).sign < 0) {
5434
+ delete this.#values[encodedKey];
5435
+ continue;
5436
+ }
5437
+ yield {
5438
+ key,
5439
+ value
5440
+ };
5441
+ }
4797
5442
  }
4798
5443
  };
4799
5444
  //#endregion
5445
+ //#region src/nodeinfo/handler.ts
5446
+ /**
5447
+ * Handles a NodeInfo request. You would not typically call this function
5448
+ * directly, but instead use {@link Federation.handle} method.
5449
+ * @param request The NodeInfo request to handle.
5450
+ * @param parameters The parameters for handling the request.
5451
+ * @returns The response to the request.
5452
+ */
5453
+ async function handleNodeInfo(_request, { context, nodeInfoDispatcher }) {
5454
+ const promise = nodeInfoDispatcher(context);
5455
+ const json = require_types.nodeInfoToJson(promise instanceof Promise ? await promise : promise);
5456
+ return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\"" } });
5457
+ }
5458
+ /**
5459
+ * Handles a request to `/.well-known/nodeinfo`. You would not typically call
5460
+ * this function directly, but instead use {@link Federation.handle} method.
5461
+ * @param request The request to handle.
5462
+ * @param context The request context.
5463
+ * @returns The response to the request.
5464
+ */
5465
+ function handleNodeInfoJrd(_request, context) {
5466
+ const links = [];
5467
+ try {
5468
+ links.push({
5469
+ rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
5470
+ href: context.getNodeInfoUri().href,
5471
+ type: "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\""
5472
+ });
5473
+ } catch (e) {
5474
+ if (!(e instanceof _fedify_uri_template.RouterError)) throw e;
5475
+ }
5476
+ const response = new Response(JSON.stringify({ links }), { headers: { "Content-Type": "application/jrd+json" } });
5477
+ return Promise.resolve(response);
5478
+ }
5479
+ //#endregion
4800
5480
  //#region src/federation/bench.ts
4801
5481
  /**
4802
5482
  * Metric reader owned by `benchmarkMode`.
@@ -4892,439 +5572,151 @@ async function handleBenchmarkTrigger(request, context, options = {}) {
4892
5572
  }, 403);
4893
5573
  await context.sendActivity(sender, recipients, activity);
4894
5574
  return jsonResponse({
4895
- version: 1,
4896
- activityId,
4897
- queueCorrelationId: activityId,
4898
- recipientCount: recipients.length,
4899
- inboxCount: inboxUrls.length
4900
- }, 202);
4901
- } catch (error) {
4902
- if (error instanceof BenchmarkTriggerError) return jsonResponse({ error: error.message }, error.status);
4903
- throw error;
4904
- }
4905
- }
4906
- var BenchmarkTriggerError = class extends Error {
4907
- status;
4908
- constructor(message, status = 400) {
4909
- super(message);
4910
- this.status = status;
4911
- }
4912
- };
4913
- function parseSender(value) {
4914
- const sender = asRecord(value, "sender");
4915
- if (typeof sender.identifier === "string") return { identifier: sender.identifier };
4916
- if (typeof sender.username === "string") return { username: sender.username };
4917
- throw new BenchmarkTriggerError("sender must be { identifier } or { username }.");
4918
- }
4919
- async function parseRecipients(value, context) {
4920
- if (!Array.isArray(value)) throw new BenchmarkTriggerError("recipients must be an array.");
4921
- return await Promise.all(value.map(async (item) => {
4922
- let object;
4923
- try {
4924
- object = await _fedify_vocab.Object.fromJsonLd(item, {
4925
- documentLoader: context.documentLoader,
4926
- contextLoader: context.contextLoader
4927
- });
4928
- } catch (error) {
4929
- throw new BenchmarkTriggerError(`Invalid ActivityPub recipient: ${error}`);
4930
- }
4931
- if (!isRecipient(object)) throw new BenchmarkTriggerError("each recipient must be an ActivityPub actor.");
4932
- const recipient = object;
4933
- if (recipient.id == null || recipient.inboxId == null) throw new BenchmarkTriggerError("each recipient must have id and inbox properties.");
4934
- return recipient;
4935
- }));
4936
- }
4937
- function isRecipient(value) {
4938
- return value != null && typeof value === "object" && "id" in value && "inboxId" in value;
4939
- }
4940
- async function parseActivity(value, context) {
4941
- try {
4942
- return await _fedify_vocab.Activity.fromJsonLd(value, {
4943
- documentLoader: context.documentLoader,
4944
- contextLoader: context.contextLoader
4945
- });
4946
- } catch (error) {
4947
- throw new BenchmarkTriggerError(`Invalid ActivityPub activity: ${error}`);
4948
- }
4949
- }
4950
- function asRecord(value, name) {
4951
- if (value == null || typeof value !== "object" || Array.isArray(value)) throw new BenchmarkTriggerError(`${name} must be an object.`);
4952
- return value;
4953
- }
4954
- function jsonResponse(body, status = 200) {
4955
- return new Response(JSON.stringify(body), {
4956
- status,
4957
- headers: { "Content-Type": "application/json" }
4958
- });
4959
- }
4960
- function serializeScopeMetrics(resourceMetrics) {
4961
- return resourceMetrics.scopeMetrics.map(serializeScope);
4962
- }
4963
- function serializeScope(scopeMetrics) {
4964
- return {
4965
- scope: {
4966
- name: scopeMetrics.scope.name,
4967
- version: scopeMetrics.scope.version
4968
- },
4969
- metrics: scopeMetrics.metrics.map(serializeMetric)
4970
- };
4971
- }
4972
- function serializeMetric(metric) {
4973
- return {
4974
- name: metric.descriptor.name,
4975
- description: metric.descriptor.description,
4976
- unit: metric.descriptor.unit,
4977
- dataPointType: serializeDataPointType(metric.dataPointType),
4978
- dataPoints: metric.dataPoints.map((point) => ({
4979
- attributes: { ...point.attributes },
4980
- startTime: point.startTime,
4981
- endTime: point.endTime,
4982
- value: point.value
4983
- }))
4984
- };
4985
- }
4986
- function serializeDataPointType(dataPointType) {
4987
- switch (dataPointType) {
4988
- case _opentelemetry_sdk_metrics.DataPointType.HISTOGRAM: return "histogram";
4989
- case _opentelemetry_sdk_metrics.DataPointType.EXPONENTIAL_HISTOGRAM: return "exponential_histogram";
4990
- case _opentelemetry_sdk_metrics.DataPointType.GAUGE: return "gauge";
4991
- case _opentelemetry_sdk_metrics.DataPointType.SUM: return "sum";
4992
- }
4993
- }
4994
- //#endregion
4995
- //#region src/federation/portable-inbox.ts
4996
- const Temporal$5 = require_http.Temporal;
4997
- const MAX_TIMEOUT_MS = 2 ** 31 - 1;
4998
- /**
4999
- * Fills in the defaults of {@link PortableInboxForwardingOptions} and
5000
- * validates them.
5001
- * @param options The options.
5002
- * @returns The resolved options.
5003
- * @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
5004
- * is not positive, if `deadline` is negative or longer
5005
- * than 2,147,483,647 milliseconds (about 24.8 days), or
5006
- * if either duration has calendar units, i.e., weeks,
5007
- * months, or years, whose length depends on the date.
5008
- */
5009
- function resolvePortableInboxForwardingOptions(options = {}) {
5010
- const maxTargets = options.maxTargets ?? 10;
5011
- if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
5012
- const ttl = Temporal$5.Duration.from(options.ttl ?? { days: 30 });
5013
- if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
5014
- const deadline = Temporal$5.Duration.from(options.deadline ?? { seconds: 10 });
5015
- const deadlineMs = deadline.total("millisecond");
5016
- if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
5017
- return {
5018
- maxTargets,
5019
- ttl,
5020
- deadline
5021
- };
5022
- }
5023
- /**
5024
- * Decides whether this server accepts a delivery to a portable inbox on behalf
5025
- * of the given actor, which the actor dispatcher returned for the identifier
5026
- * in the requested inbox path.
5027
- *
5028
- * The actor document comes from the application, so it is trusted as is and
5029
- * its proof is not verified here.
5030
- * @param actor The actor the actor dispatcher returned.
5031
- * @param options The requested DID, the canonical form of the requested inbox
5032
- * ID, and the origin of this server.
5033
- * @returns The resolution.
5034
- */
5035
- function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
5036
- if (actor == null) return {
5037
- status: "rejected",
5038
- reason: "notFound"
5039
- };
5040
- const id = actor.id;
5041
- if (id == null || id.protocol !== "ap:" && id.protocol !== "ap+ef61:") return {
5042
- status: "rejected",
5043
- reason: "notPortable"
5044
- };
5045
- if (!isSameAuthority(id, authority)) return {
5046
- status: "rejected",
5047
- reason: "authorityMismatch"
5048
- };
5049
- const inboxId = actor.inboxId;
5050
- if (inboxId == null || canonicalize(inboxId) !== canonicalInboxId) return {
5051
- status: "rejected",
5052
- reason: "inboxMismatch"
5053
- };
5054
- const gateways = actor.gateways;
5055
- if (!gateways.some((g) => (0, _fedify_vocab_runtime.isGatewayUrl)(g) && g.origin === localOrigin)) return {
5056
- status: "rejected",
5057
- reason: "notGateway"
5058
- };
5059
- return {
5060
- status: "accepted",
5061
- recipient: {
5062
- actorId: id,
5063
- inboxId,
5064
- canonicalInboxId,
5065
- gateways
5066
- }
5067
- };
5068
- }
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) {
5078
- try {
5079
- return (0, _fedify_vocab_runtime.canonicalizePortableUri)(typeof iri === "string" ? iri : (0, _fedify_vocab_runtime.formatIri)(iri));
5080
- } catch (error) {
5081
- if (error instanceof TypeError) return null;
5082
- throw error;
5083
- }
5084
- }
5085
- /**
5086
- * Forwards an activity received in a portable inbox to the inboxes of the same
5087
- * portable actor on its other gateways, as FEP-ef61 recommends.
5088
- *
5089
- * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
5090
- * each target gateway is claimed with an atomic compare-and-swap before the
5091
- * activity is handed off, and a claim is never released, even if the hand-off
5092
- * fails, since a failed hand-off does not prove that nothing was delivered.
5093
- * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
5094
- * forwarded.
5095
- *
5096
- * Forwarded requests are signed with HTTP Signatures by the RSA key among
5097
- * {@link ForwardPortableInboxActivityParameters.getKeys}, if any, so that
5098
- * servers requiring HTTP Signatures accept them, and are sent unsigned
5099
- * otherwise. Either way, the receiving gateways authenticate the activity by
5100
- * its own proof.
5101
- * @param parameters The parameters.
5102
- * @returns The target inbox URLs that the activity was handed off to.
5103
- */
5104
- async function forwardPortableInboxActivity(parameters) {
5105
- const logger = (0, _logtape_logtape.getLogger)([
5106
- "fedify",
5107
- "federation",
5108
- "inbox"
5109
- ]);
5110
- const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
5111
- const { maxTargets, ttl } = parameters.options ?? resolvePortableInboxForwardingOptions();
5112
- if (kv.cas == null || maxTargets < 1) return [];
5113
- const canonicalActivityId = canonicalize(activityId) ?? activityId.href;
5114
- const targets = getForwardingTargets(recipient, excludedOrigins);
5115
- if (targets.length > maxTargets) {
5116
- 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.", {
5117
- inbox: recipient.canonicalInboxId,
5118
- activityId: activityId.href,
5119
- max: maxTargets
5120
- });
5121
- targets.length = maxTargets;
5575
+ version: 1,
5576
+ activityId,
5577
+ queueCorrelationId: activityId,
5578
+ recipientCount: recipients.length,
5579
+ inboxCount: inboxUrls.length
5580
+ }, 202);
5581
+ } catch (error) {
5582
+ if (error instanceof BenchmarkTriggerError) return jsonResponse({ error: error.message }, error.status);
5583
+ throw error;
5122
5584
  }
5123
- const claimed = [];
5124
- for (const { gateway, inbox } of targets) {
5125
- const key = [
5126
- ...kvPrefix,
5127
- recipient.canonicalInboxId,
5128
- canonicalActivityId,
5129
- gateway
5130
- ];
5131
- if (await kv.cas(key, void 0, true, { ttl })) claimed.push(inbox);
5585
+ }
5586
+ var BenchmarkTriggerError = class extends Error {
5587
+ status;
5588
+ constructor(message, status = 400) {
5589
+ super(message);
5590
+ this.status = status;
5132
5591
  }
5133
- if (claimed.length < 1) return [];
5134
- logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
5135
- activityId: activityId.href,
5136
- inbox: recipient.canonicalInboxId,
5137
- targets: claimed.map((t) => t.href)
5138
- });
5139
- if (outboxQueue == null) await forwardImmediately(parameters, claimed);
5140
- else await enqueueForwarding(parameters, outboxQueue, claimed);
5141
- return claimed;
5592
+ };
5593
+ function parseSender(value) {
5594
+ const sender = asRecord(value, "sender");
5595
+ if (typeof sender.identifier === "string") return { identifier: sender.identifier };
5596
+ if (typeof sender.username === "string") return { username: sender.username };
5597
+ throw new BenchmarkTriggerError("sender must be { identifier } or { username }.");
5142
5598
  }
5143
- function getForwardingTargets(recipient, excludedOrigins) {
5144
- const logger = (0, _logtape_logtape.getLogger)([
5145
- "fedify",
5146
- "federation",
5147
- "inbox"
5148
- ]);
5149
- const seen = new Set(excludedOrigins);
5150
- const targets = [];
5151
- for (const gateway of recipient.gateways) {
5152
- if (!(0, _fedify_vocab_runtime.isGatewayUrl)(gateway) || seen.has(gateway.origin)) continue;
5153
- seen.add(gateway.origin);
5154
- let inbox;
5599
+ async function parseRecipients(value, context) {
5600
+ if (!Array.isArray(value)) throw new BenchmarkTriggerError("recipients must be an array.");
5601
+ return await Promise.all(value.map(async (item) => {
5602
+ let object;
5155
5603
  try {
5156
- inbox = (0, _fedify_vocab_runtime.toCompatibleEf61Id)(recipient.inboxId, gateway.origin);
5157
- } catch (error) {
5158
- if (!(error instanceof TypeError)) throw error;
5159
- logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
5160
- gateway: gateway.origin,
5161
- inbox: recipient.canonicalInboxId
5604
+ object = await _fedify_vocab.Object.fromJsonLd(item, {
5605
+ documentLoader: context.documentLoader,
5606
+ contextLoader: context.contextLoader
5162
5607
  });
5163
- continue;
5608
+ } catch (error) {
5609
+ throw new BenchmarkTriggerError(`Invalid ActivityPub recipient: ${error}`);
5164
5610
  }
5165
- targets.push({
5166
- gateway: gateway.origin,
5167
- inbox
5168
- });
5169
- }
5170
- return targets;
5611
+ if (!isRecipient(object)) throw new BenchmarkTriggerError("each recipient must be an ActivityPub actor.");
5612
+ const recipient = object;
5613
+ if (recipient.id == null || recipient.inboxId == null) throw new BenchmarkTriggerError("each recipient must have id and inbox properties.");
5614
+ return recipient;
5615
+ }));
5171
5616
  }
5172
- /**
5173
- * Resolves the key that signs forwarded requests: the first RSASSA-PKCS1-v1_5
5174
- * key, as {@link sendActivity} signs requests only with such a key. Since
5175
- * the targets have already been claimed, the activity is forwarded unsigned
5176
- * rather than not at all if the key cannot be resolved or used.
5177
- */
5178
- async function resolveForwardingKey({ getKeys, activityId }) {
5179
- if (getKeys == null) return null;
5180
- const logger = (0, _logtape_logtape.getLogger)([
5181
- "fedify",
5182
- "federation",
5183
- "inbox"
5184
- ]);
5185
- let key;
5617
+ function isRecipient(value) {
5618
+ return value != null && typeof value === "object" && "id" in value && "inboxId" in value;
5619
+ }
5620
+ async function parseActivity(value, context) {
5186
5621
  try {
5187
- key = (await getKeys()).find((k) => k.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5");
5188
- } catch (error) {
5189
- logger.error("Failed to get the gateway keys to sign forwarded requests with; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
5190
- activityId: activityId.href,
5191
- error
5622
+ return await _fedify_vocab.Activity.fromJsonLd(value, {
5623
+ documentLoader: context.documentLoader,
5624
+ contextLoader: context.contextLoader
5192
5625
  });
5193
- return null;
5194
- }
5195
- if (key == null) return null;
5196
- try {
5197
- require_http.validateCryptoKey(key.privateKey, "private");
5198
5626
  } catch (error) {
5199
- if (!(error instanceof TypeError)) throw error;
5200
- logger.warn("The gateway key {keyId} cannot sign forwarded requests; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
5201
- keyId: key.keyId.href,
5202
- activityId: activityId.href,
5203
- error
5204
- });
5205
- return null;
5627
+ throw new BenchmarkTriggerError(`Invalid ActivityPub activity: ${error}`);
5206
5628
  }
5207
- return key;
5208
5629
  }
5209
- async function forwardImmediately(parameters, inboxes) {
5210
- const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, options, meterProvider, tracerProvider } = parameters;
5211
- const logger = (0, _logtape_logtape.getLogger)([
5212
- "fedify",
5213
- "federation",
5214
- "inbox"
5215
- ]);
5216
- const deadline = (options ?? resolvePortableInboxForwardingOptions()).deadline.total("millisecond");
5217
- const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
5218
- activity,
5219
- activityId: activityId.href,
5220
- activityType,
5221
- keys: key == null ? [] : [key],
5222
- inbox,
5223
- allowPrivateAddress,
5224
- specDeterminer,
5225
- meterProvider,
5226
- tracerProvider
5227
- }).catch((error) => {
5228
- logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5229
- activityId: activityId.href,
5230
- inbox: inbox.href,
5231
- error
5232
- });
5233
- }))));
5234
- let timer;
5235
- const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
5236
- timer = setTimeout(() => resolve(true), deadline);
5237
- })]);
5238
- clearTimeout(timer);
5239
- if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
5240
- activityId: activityId.href,
5241
- deadline
5630
+ function asRecord(value, name) {
5631
+ if (value == null || typeof value !== "object" || Array.isArray(value)) throw new BenchmarkTriggerError(`${name} must be an object.`);
5632
+ return value;
5633
+ }
5634
+ function jsonResponse(body, status = 200) {
5635
+ return new Response(JSON.stringify(body), {
5636
+ status,
5637
+ headers: { "Content-Type": "application/json" }
5242
5638
  });
5243
5639
  }
5244
- async function enqueueForwarding(parameters, outboxQueue, inboxes) {
5245
- const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
5246
- const logger = (0, _logtape_logtape.getLogger)([
5247
- "fedify",
5248
- "federation",
5249
- "inbox"
5250
- ]);
5251
- const key = await resolveForwardingKey(parameters);
5252
- const keys = [];
5253
- if (key != null) try {
5254
- keys.push({
5255
- keyId: key.keyId.href,
5256
- privateKey: await require_http.exportJwk(key.privateKey)
5257
- });
5258
- } catch (error) {
5259
- logger.error("Failed to export the gateway key {keyId}; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
5260
- keyId: key.keyId.href,
5261
- activityId: activityId.href,
5262
- error
5263
- });
5640
+ function serializeScopeMetrics(resourceMetrics) {
5641
+ return resourceMetrics.scopeMetrics.map(serializeScope);
5642
+ }
5643
+ function serializeScope(scopeMetrics) {
5644
+ return {
5645
+ scope: {
5646
+ name: scopeMetrics.scope.name,
5647
+ version: scopeMetrics.scope.version
5648
+ },
5649
+ metrics: scopeMetrics.metrics.map(serializeMetric)
5650
+ };
5651
+ }
5652
+ function serializeMetric(metric) {
5653
+ return {
5654
+ name: metric.descriptor.name,
5655
+ description: metric.descriptor.description,
5656
+ unit: metric.descriptor.unit,
5657
+ dataPointType: serializeDataPointType(metric.dataPointType),
5658
+ dataPoints: metric.dataPoints.map((point) => ({
5659
+ attributes: { ...point.attributes },
5660
+ startTime: point.startTime,
5661
+ endTime: point.endTime,
5662
+ value: point.value
5663
+ }))
5664
+ };
5665
+ }
5666
+ function serializeDataPointType(dataPointType) {
5667
+ switch (dataPointType) {
5668
+ case _opentelemetry_sdk_metrics.DataPointType.HISTOGRAM: return "histogram";
5669
+ case _opentelemetry_sdk_metrics.DataPointType.EXPONENTIAL_HISTOGRAM: return "exponential_histogram";
5670
+ case _opentelemetry_sdk_metrics.DataPointType.GAUGE: return "gauge";
5671
+ case _opentelemetry_sdk_metrics.DataPointType.SUM: return "sum";
5264
5672
  }
5265
- startQueue?.();
5266
- const started = (/* @__PURE__ */ new Date()).toISOString();
5267
- const traceContext = {};
5268
- _opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), traceContext);
5269
- await Promise.all(inboxes.map(async (inbox) => {
5270
- const message = {
5271
- type: "outbox",
5272
- id: crypto.randomUUID(),
5273
- baseUrl,
5274
- keys,
5275
- activity,
5276
- activityId: activityId.href,
5277
- activityType,
5278
- inbox: inbox.href,
5279
- sharedInbox: false,
5280
- started,
5281
- attempt: 0,
5282
- headers: {},
5283
- traceContext
5284
- };
5285
- try {
5286
- await outboxQueue.enqueue(message);
5287
- } catch (error) {
5288
- logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5289
- activityId: activityId.href,
5290
- inbox: inbox.href,
5291
- error
5292
- });
5293
- return;
5294
- }
5295
- require_http.recordOutboxEnqueue(meterProvider, outboxQueue, message);
5296
- }));
5297
5673
  }
5298
5674
  //#endregion
5299
5675
  //#region src/federation/outgoing-proof.ts
5300
- function isPortableUrl(url) {
5301
- return url.protocol === "ap:" || url.protocol === "ap+ef61:";
5302
- }
5303
5676
  function formatKeyIds(candidates) {
5304
5677
  return candidates.map((c) => c.verificationMethod.href).join(", ");
5305
5678
  }
5306
5679
  /**
5307
5680
  * Rejects an activity of a portable actor whose ID cannot be authenticated by
5308
5681
  * 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.
5682
+ * signed by that DID, so its ID has to be a portable ID, i.e., an `ap:` or
5683
+ * `ap+ef61:` URI or a compatible identifier, with the same DID. A portable
5684
+ * actor is one whose ID is such an ID as well.
5310
5685
  * @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.
5686
+ * its ID is not a portable ID of the actor's DID, its
5687
+ * portable actors do not share a DID, or the ID of one of
5688
+ * them is a malformed compatible identifier.
5313
5689
  * @internal
5314
5690
  */
5315
5691
  function assertPortableActorActivity(activity) {
5316
- const portableActors = activity.actorIds.filter(isPortableUrl);
5692
+ const portableActors = activity.actorIds.filter((id) => require_http.isPortableId(id));
5317
5693
  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.`);
5694
+ const dids = portableActors.map((actorId) => {
5695
+ const did = require_http.getPortableDid(actorId);
5696
+ 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.`);
5697
+ return did;
5698
+ });
5699
+ const did = dids[0];
5700
+ 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.`);
5701
+ 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.`);
5702
+ if (!(0, _fedify_vocab_runtime_internal_portable_dereference.isCompatibleEf61Iri)(activity.id)) return;
5703
+ const mismatched = portableActors.find((actorId) => (0, _fedify_vocab_runtime_internal_portable_dereference.isCompatibleEf61Iri)(actorId) && actorId.origin !== activity.id?.origin);
5704
+ if (mismatched == null) return;
5705
+ (0, _logtape_logtape.getLogger)([
5706
+ "fedify",
5707
+ "federation",
5708
+ "outbox"
5709
+ ]).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.", {
5710
+ activityId: activity.id.href,
5711
+ actorId: mismatched.href
5712
+ });
5321
5713
  }
5322
5714
  /**
5323
5715
  * Creates the Object Integrity Proofs Fedify attaches to an outgoing activity.
5324
5716
  *
5325
5717
  * Keys whose IDs are FEP-ef61 compatible identifiers are gateway keys, which
5326
5718
  * 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
5719
+ * `ap+ef61:` ID or a compatible identifier, that carries no proof is signed only by the one Ed25519 key
5328
5720
  * whose verification method is a DID URL for its DID, even if it is the only
5329
5721
  * key; if no key or more than one key qualifies, this function throws.
5330
5722
  *
@@ -5363,7 +5755,7 @@ async function signOutgoingActivity(activity, candidates, options) {
5363
5755
  break;
5364
5756
  }
5365
5757
  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));
5758
+ if (!hasProof && activity.id != null && require_http.isPortableId(activity.id)) return await sign(selectPortableActivityKey(activity, activity.id, keys));
5367
5759
  if (keys.length < 1 || hasProof && !options.appendToExistingProofs) return {
5368
5760
  activity,
5369
5761
  hasProof,
@@ -5396,8 +5788,9 @@ async function signOutgoingActivity(activity, candidates, options) {
5396
5788
  * @throws {TypeError} If no key or more than one key qualifies.
5397
5789
  */
5398
5790
  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));
5791
+ const activityDid = require_http.getPortableDid(activityId);
5792
+ 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.`);
5793
+ const eligible = keys.filter((c) => c.verificationMethod.protocol === "did:" && require_http.getPortableDid(c.verificationMethod) === activityDid);
5401
5794
  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
5795
  (0, _logtape_logtape.getLogger)([
5403
5796
  "fedify",
@@ -5817,6 +6210,27 @@ function getWebFingerSubjectAndAliases({ resourceUrl, actorUri, preferredUsernam
5817
6210
  };
5818
6211
  }
5819
6212
  /**
6213
+ * Gets the compatible identifier that a WebFinger response links to for
6214
+ * an FEP-ef61 portable actor. An actor identified by an `ap:` or `ap+ef61:`
6215
+ * URI is linked through its compatible identifier on the given gateway, while
6216
+ * an actor identified by a compatible identifier is linked through its ID as
6217
+ * is, so that software without FEP-ef61 support finds the same ID in the actor
6218
+ * document.
6219
+ * @param actorId The portable actor's ID.
6220
+ * @param gateway The actor's first gateway.
6221
+ * @returns The compatible identifier, or `null` if the actor ID is malformed
6222
+ * or cannot be represented as a compatible identifier.
6223
+ */
6224
+ function getCompatibleActorId(actorId, gateway) {
6225
+ try {
6226
+ if (require_http.isPortableUri(actorId)) return (0, _fedify_vocab_runtime.toCompatibleEf61Id)(actorId, gateway);
6227
+ return (0, _fedify_vocab_runtime.fromCompatibleEf61Id)(actorId) == null ? null : actorId;
6228
+ } catch (error) {
6229
+ if (error instanceof TypeError) return null;
6230
+ throw error;
6231
+ }
6232
+ }
6233
+ /**
5820
6234
  * Computes the subject and aliases for an FEP-ef61 portable actor, whose
5821
6235
  * WebFinger address takes its domain from the first gateway of the actor
5822
6236
  * rather than from the actor ID. The canonical `ap:` ID is not emitted,
@@ -5980,21 +6394,23 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
5980
6394
  });
5981
6395
  let actorUri = context.getActorUri(identifier);
5982
6396
  let portable;
5983
- if (actor.id != null && (actor.id.protocol === "ap:" || actor.id.protocol === "ap+ef61:")) {
6397
+ if (actor.id != null && require_http.isPortableId(actor.id)) {
5984
6398
  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) {
6399
+ if (gateway == null || !(0, _fedify_vocab_runtime.isGatewayUrl)(gateway)) {
5992
6400
  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
6401
  actorId: (0, _fedify_vocab_runtime.formatIri)(actor.id),
5994
6402
  identifier
5995
6403
  });
5996
6404
  return await onNotFound(request);
5997
6405
  }
6406
+ const compatibleId = getCompatibleActorId(actor.id, gateway);
6407
+ if (compatibleId == null) {
6408
+ 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.", {
6409
+ actorId: (0, _fedify_vocab_runtime.formatIri)(actor.id),
6410
+ identifier
6411
+ });
6412
+ return await onNotFound(request);
6413
+ }
5998
6414
  actorUri = compatibleId;
5999
6415
  portable = {
6000
6416
  compatibleId,
@@ -7933,7 +8349,7 @@ var FederationImpl = class extends FederationBuilderImpl {
7933
8349
  onNotFound,
7934
8350
  portableInbox: {
7935
8351
  recipient,
7936
- forward: async (activity, activityId, activityType) => {
8352
+ forward: async (activity, activityId, activityType, signatures) => {
7937
8353
  if (this.portableInboxForwarding.maxTargets < 1) return;
7938
8354
  if (this.kv.cas == null) {
7939
8355
  if (!this.#portableInboxForwardingWarned) {
@@ -7956,6 +8372,7 @@ var FederationImpl = class extends FederationBuilderImpl {
7956
8372
  allowPrivateAddress: this.allowPrivateAddress,
7957
8373
  getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
7958
8374
  specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
8375
+ signatures,
7959
8376
  options: this.portableInboxForwarding,
7960
8377
  meterProvider: this.meterProvider,
7961
8378
  tracerProvider: this.tracerProvider