@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
@@ -2,16 +2,17 @@ import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  import { t as __exportAll } from "./rolldown-runtime-BC-Eo5Mj.js";
4
4
  import { r as getDefaultActivityTransformers } from "./transformers-DAtXl_Kc.js";
5
- import { $ as recordCollectionPageItems, B as getCanonicalPortableId, C as hasSignature, D as signJsonLd, E as isInvalidUrlTypeError, H as hasPortableActor, J as getRemoteHost, K as getDurationMs, L as importJwk, M as exportJwk, O as verifyCompactJsonLd, Q as recordCollectionDispatchDuration, R as validateCryptoKey, S as getNormalizationContextLoader, T as isClearlyMalformedContextReference, U as isCompatibleKeyId, V as getGatewayKeyBase, W as isPortableUri, X as isAbortError, Y as instrumentDocumentLoader, Z as recordCircuitBreakerStateChange, _ as assertSafeJsonLd, a as verifyRequestDetailed, at as recordOutboxActivity, c as hasProofLike, ct as registerQueueDepthGauge, d as verifyObject, et as recordCollectionRequest, f as verifyPortableObjectProof, g as InvalidContextReferenceError, ht as version, i as verifyRequest, it as recordInboxActivity, j as wrapContextLoaderForJsonLd, l as signObject, lt as normalizeOutgoingActivityJsonLd, mt as name, n as parseRfc9421SignatureInput, ot as recordOutboxEnqueue, p as verifyPortableObjectProofPolicy, q as getFederationMetrics, rt as recordFanoutRecipients, st as recordWebFingerHandle, t as doubleKnock, tt as recordCollectionTotalItems, u as verifyMapLocalProof, ut as formatAcceptSignature, w as hasSignatureLike, x as detachSignature, y as compactJsonLd } from "./http-4PCCvhsU.js";
5
+ import { $ as recordCollectionTotalItems, A as isInvalidUrlTypeError, C as compactJsonLd, D as hasSignature, E as getNormalizationContextLoader, F as wrapContextLoaderForJsonLd, G as getFederationMetrics, H as validateCryptoKey, I as exportJwk, J as isAbortError, K as getRemoteHost, M as verifyCompactJsonLd, O as hasSignatureLike, Q as recordCollectionRequest, T as detachSignature, V as importJwk, W as getDurationMs, X as recordCollectionDispatchDuration, Y as recordCircuitBreakerStateChange, Z as recordCollectionPageItems, _ as verifyPortableObjectProofWithRoot, _t as isPortableId, at as recordWebFingerHandle, b as InvalidContextReferenceError, bt as name, ct as formatAcceptSignature, d as hasProofLike, f as signObject, ft as getCanonicalPortableId, g as verifyPortableObjectProofPolicy, gt as isCompatibleKeyId, h as verifyPortableObjectProof, ht as hasPortableActor, i as selectRequestSignature, it as recordOutboxEnqueue, j as signJsonLd, k as isClearlyMalformedContextReference, l as InvalidPortableObjectIdError, m as verifyObject, mt as getPortableDid, n as listRequestSignatures, nt as recordInboxActivity, o as verifyRequest, ot as registerQueueDepthGauge, p as verifyMapLocalProof, pt as getGatewayKeyBase, q as instrumentDocumentLoader, r as parseRfc9421SignatureInput, rt as recordOutboxActivity, s as verifyRequestDetailed, st as preloadedOnlyDocumentLoader, t as doubleKnock, tt as recordFanoutRecipients, vt as isPortableUri, x as assertSafeJsonLd, xt as version, y as normalizeOutgoingActivityJsonLd } from "./http-D-rbr_Sy.js";
6
6
  import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-C_cNlNVK.js";
7
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-_pbbgLJK.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-1KUmAPH7.js";
7
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-U12fDpQW.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DIxwMeNi.js";
9
9
  import { getLogger, withContext } from "@logtape/logtape";
10
10
  import { Router, RouterError, assertPath } from "@fedify/uri-template";
11
11
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
12
12
  import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
13
+ import { FetchError, UrlError, canonicalizePortableUri, decodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, isGatewayUrl, parseDigestMultibase, parseHashlink, parseIri, toCompatibleEf61Id, validatePublicUrl } from "@fedify/vocab-runtime";
14
+ import { isCompatibleEf61Iri } from "@fedify/vocab-runtime/internal/portable-dereference";
13
15
  import { encodeHex } from "byte-encodings/hex";
14
- import { FetchError, UrlError, canonicalizePortableUri, decodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, haveSameFe34Origin, isGatewayUrl, parseDigestMultibase, parseHashlink, parseIri, toCompatibleEf61Id, validatePublicUrl } from "@fedify/vocab-runtime";
15
16
  import jsonld from "@fedify/vocab-runtime/jsonld";
16
17
  import { uniq } from "es-toolkit";
17
18
  import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
@@ -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 && 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 Tombstone) return actor;
213
+ if (actor.id != null && 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 = getLogger([
745
+ "fedify",
746
+ "federation",
747
+ "actor"
748
+ ]);
749
+ try {
750
+ 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 || !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 LEGACY_SWEEP_MARKER = ["__fedify_meta", "circuit_breaker_state_ttl_sweep_v2"];
@@ -1309,7 +1338,14 @@ async function buildCollectionSynchronizationHeader(collectionId, actorIds) {
1309
1338
  //#endregion
1310
1339
  //#region src/sig/compound-proof.ts
1311
1340
  const textEncoder = new TextEncoder();
1312
- const PORTABLE_OBJECT_ID_PATTERN = /^ap(?:\+ef61)?:\/\//i;
1341
+ /**
1342
+ * Checks whether a literal `id` or `@id` value makes a map a portable object:
1343
+ * an `ap:` or `ap+ef61:` URI, or anything that looks like an FEP-ef61
1344
+ * compatible identifier, even a malformed one, which then fails the policy.
1345
+ */
1346
+ function isPortableIdValue(value) {
1347
+ return typeof value === "string" && isPortableId(value);
1348
+ }
1313
1349
  function isJsonMap(value) {
1314
1350
  return typeof value === "object" && value != null && !Array.isArray(value);
1315
1351
  }
@@ -1555,18 +1591,20 @@ function collectPortableObjects(snapshot) {
1555
1591
  for (let index = current.value.length - 1; index >= 0; index--) pending.push({
1556
1592
  value: current.value[index],
1557
1593
  path: childPath(current.path, String(index)),
1558
- depth: current.depth + 1
1594
+ depth: current.depth + 1,
1595
+ keyOf: current.keyOf
1559
1596
  });
1560
1597
  continue;
1561
1598
  }
1562
1599
  if (!isCompoundProofJsonObject(current.value)) continue;
1563
1600
  const document = current.value;
1564
- const id = [document.id, document["@id"]].find((value) => typeof value === "string" && PORTABLE_OBJECT_ID_PATTERN.test(value));
1601
+ const id = [document.id, document["@id"]].find(isPortableIdValue);
1565
1602
  if (typeof id === "string") objects.push(Object.freeze({
1566
1603
  path: current.path,
1567
1604
  id,
1568
1605
  depth: current.depth,
1569
- document
1606
+ document,
1607
+ ...current.keyOf == null ? {} : { keyOf: current.keyOf }
1570
1608
  }));
1571
1609
  const entries = Object.entries(document).sort(([left], [right]) => comparePaths(left, right));
1572
1610
  for (let index = entries.length - 1; index >= 0; index--) {
@@ -1575,7 +1613,11 @@ function collectPortableObjects(snapshot) {
1575
1613
  pending.push({
1576
1614
  value,
1577
1615
  path: childPath(current.path, key),
1578
- depth: current.depth + 1
1616
+ depth: current.depth + 1,
1617
+ ...typeof id === "string" && (key === "publicKey" || key === "assertionMethod") ? { keyOf: {
1618
+ path: current.path,
1619
+ member: key
1620
+ } } : {}
1579
1621
  });
1580
1622
  }
1581
1623
  }
@@ -1583,8 +1625,9 @@ function collectPortableObjects(snapshot) {
1583
1625
  return objects;
1584
1626
  }
1585
1627
  /**
1586
- * Determines whether a received JSON tree contains a portable object ID
1587
- * outside a proof or context definition, without exceeding discovery limits.
1628
+ * Determines whether a received JSON tree contains a portable object ID, i.e.,
1629
+ * an `ap:` or `ap+ef61:` URI or an FEP-ef61 compatible identifier, outside
1630
+ * a proof or context definition, without exceeding discovery limits.
1588
1631
  *
1589
1632
  * This is only an applicability check. It does not authenticate the input;
1590
1633
  * callers must still use {@link verifyCompoundPortableObjectProofs} to obtain
@@ -1631,7 +1674,7 @@ function inspectCompoundPortableObjectApplicability(json, limits) {
1631
1674
  } else if (isJsonMap(value)) {
1632
1675
  mapCount++;
1633
1676
  if (mapCount > limits.maxMaps) return "indeterminate";
1634
- if ([value.id, value["@id"]].some((id) => typeof id === "string" && PORTABLE_OBJECT_ID_PATTERN.test(id))) return "present";
1677
+ if ([value.id, value["@id"]].some(isPortableIdValue)) return "present";
1635
1678
  if (!addByteLength(2)) return "indeterminate";
1636
1679
  const children = function* () {
1637
1680
  let first = true;
@@ -1694,7 +1737,8 @@ function findInJsonMaps(json, visit) {
1694
1737
  /**
1695
1738
  * Determines whether an outgoing JSON tree contains portable content that
1696
1739
  * requires producer-side proof-shape checks: a map, outside proof and context
1697
- * values, whose direct `id` or `@id` is a portable ActivityPub URI.
1740
+ * values, whose direct `id` or `@id` is a portable ActivityPub URI or an
1741
+ * FEP-ef61 compatible identifier.
1698
1742
  *
1699
1743
  * This deliberately differs from
1700
1744
  * {@link inspectCompoundPortableObjectApplicability}: it applies no resource
@@ -1707,7 +1751,7 @@ function findInJsonMaps(json, visit) {
1707
1751
  * @internal
1708
1752
  */
1709
1753
  function containsCompoundPortableObject(json) {
1710
- return findInJsonMaps(json, (map) => [map.id, map["@id"]].some((id) => typeof id === "string" && PORTABLE_OBJECT_ID_PATTERN.test(id)) ? true : void 0) ?? false;
1754
+ return findInJsonMaps(json, (map) => [map.id, map["@id"]].some(isPortableIdValue) ? true : void 0) ?? false;
1711
1755
  }
1712
1756
  /**
1713
1757
  * Finds the first direct `proof` member whose value the map-local
@@ -1804,7 +1848,7 @@ async function verifyCompoundPortableObjectProofs(json, limits, options = {}) {
1804
1848
  if (discovered.status !== "ok") return discovered;
1805
1849
  const proofs = await verifyDiscoveredProofDocuments(discovered, options);
1806
1850
  const proofByPath = new Map(proofs.map((proof) => [proof.path, proof]));
1807
- const portableObjects = await Promise.all(collectPortableObjects(discovered.snapshot).map(async (object) => {
1851
+ const verifyPortableObject = async (object) => {
1808
1852
  const metadata = {
1809
1853
  path: object.path,
1810
1854
  id: object.id,
@@ -1836,16 +1880,174 @@ async function verifyCompoundPortableObjectProofs(json, limits, options = {}) {
1836
1880
  reason: { type: "invalidPortableObject" }
1837
1881
  });
1838
1882
  }
1883
+ };
1884
+ const collected = collectPortableObjects(discovered.snapshot);
1885
+ const objectByPath = new Map(collected.map((object) => [object.path, object]));
1886
+ const isKeyCandidate = (object) => object.keyOf != null && !Object.hasOwn(object.document, "proof");
1887
+ const results = /* @__PURE__ */ new Map();
1888
+ for (const result of await Promise.all(collected.filter((object) => !isKeyCandidate(object)).map(verifyPortableObject))) results.set(result.path, result);
1889
+ let projections = 0;
1890
+ for (const object of collected) {
1891
+ if (!isKeyCandidate(object)) continue;
1892
+ const { path, member } = object.keyOf;
1893
+ const parent = results.get(path);
1894
+ const parentObject = objectByPath.get(path);
1895
+ if (parent?.verified === true && parentObject != null && projections++ < MAX_KEY_PROJECTIONS && await isEmbeddedPortableKey(parentObject.document, member, object.document, parent.id)) continue;
1896
+ results.set(object.path, await verifyPortableObject(object));
1897
+ }
1898
+ const portableObjects = Object.freeze(collected.flatMap((object) => {
1899
+ const result = results.get(object.path);
1900
+ return result == null ? [] : [result];
1839
1901
  }));
1840
1902
  return {
1841
1903
  status: "ok",
1842
1904
  verified: portableObjects.length > 0 && proofs.every((proof) => proof.verified) && portableObjects.every((object) => object.verified),
1843
1905
  snapshot: discovered.snapshot,
1844
1906
  proofs,
1845
- portableObjects: Object.freeze(portableObjects),
1907
+ portableObjects,
1846
1908
  statistics: discovered.statistics
1847
1909
  };
1848
1910
  }
1911
+ /**
1912
+ * The maximum number of embedded keys per compound document that are checked
1913
+ * by {@link isEmbeddedPortableKey}, each of which expands a projection of its
1914
+ * parent. Keys beyond it have to carry their own proofs.
1915
+ */
1916
+ const MAX_KEY_PROJECTIONS = 64;
1917
+ const SECURITY = "https://w3id.org/security#";
1918
+ const KEY_TYPES = /* @__PURE__ */ new Set([`${SECURITY}Key`, `${SECURITY}Multikey`]);
1919
+ const KEY_PROPERTIES = /* @__PURE__ */ new Set([
1920
+ "@id",
1921
+ "@type",
1922
+ `${SECURITY}owner`,
1923
+ `${SECURITY}controller`,
1924
+ `${SECURITY}publicKeyPem`,
1925
+ `${SECURITY}publicKeyMultibase`
1926
+ ]);
1927
+ /**
1928
+ * Tells whether an unsigned map embedded in a verified portable map is just
1929
+ * a key of the portable object the parent is, e.g., a gateway key of
1930
+ * a portable actor, which the parent's proof covers.
1931
+ *
1932
+ * The key's meaning depends on the contexts active where it sits, so it is
1933
+ * decided on a projection of the parent that keeps only what those contexts
1934
+ * depend on: the parent's `@context`, its types and ID, and the member that
1935
+ * holds the key. That holds only if nothing else in the parent can change
1936
+ * them, so both documents may only use contexts that define no keyword
1937
+ * aliases and no scoped contexts; Fedify's preloaded contexts, the only
1938
+ * remote ones the compound policy loads, alias just `id` and `type`.
1939
+ *
1940
+ * The projection must expand to the parent's verified ID with the key as the
1941
+ * single value of `publicKey` or `assertionMethod`, and the key must be
1942
+ * nothing but a `CryptographicKey` or `Multikey` whose ID is a compatible
1943
+ * identifier of the parent plus a fragment, and whose owner or controller,
1944
+ * if any, is the parent.
1945
+ */
1946
+ async function isEmbeddedPortableKey(parent, member, key, parentId) {
1947
+ const parentPortableId = getCanonicalPortableIdOf(parentId);
1948
+ if (parentPortableId == null) return false;
1949
+ if (!isSimpleContext(parent["@context"]) || Object.hasOwn(key, "@context") && !isSimpleContext(key["@context"])) return false;
1950
+ const literalIds = [key.id, key["@id"]].filter((id) => id !== void 0);
1951
+ if (literalIds.length < 1 || literalIds.some((id) => id !== literalIds[0]) || typeof literalIds[0] !== "string" || !isKeyIdOf(literalIds[0], parentPortableId)) return false;
1952
+ const projection = {};
1953
+ for (const property of [
1954
+ "@context",
1955
+ "type",
1956
+ "@type",
1957
+ "id",
1958
+ "@id"
1959
+ ]) if (Object.hasOwn(parent, property)) projection[property] = parent[property];
1960
+ projection[member] = key;
1961
+ let expanded;
1962
+ try {
1963
+ expanded = await jsonld.expand(structuredClone(projection), {
1964
+ documentLoader: getNormalizationContextLoader(preloadedOnlyDocumentLoader),
1965
+ keepFreeFloatingNodes: true
1966
+ });
1967
+ } catch {
1968
+ return false;
1969
+ }
1970
+ if (expanded.length !== 1 || hasGraphKeyword(expanded)) return false;
1971
+ const node = expanded[0];
1972
+ if (!isJsonMap(node) || typeof node["@id"] !== "string" || getCanonicalPortableIdOf(node["@id"]) !== parentPortableId) return false;
1973
+ const properties = Object.keys(node).filter((property) => property !== "@id" && property !== "@type");
1974
+ if (properties.length !== 1 || properties[0] !== `${SECURITY}${member}`) return false;
1975
+ const values = node[properties[0]];
1976
+ if (!Array.isArray(values) || values.length !== 1) return false;
1977
+ const expandedKey = values[0];
1978
+ if (!isJsonMap(expandedKey) || expandedKey["@id"] !== literalIds[0] || Object.keys(expandedKey).some((property) => !KEY_PROPERTIES.has(property))) return false;
1979
+ const types = expandedKey["@type"];
1980
+ if (types !== void 0 && (!Array.isArray(types) || types.some((type) => typeof type !== "string" || !KEY_TYPES.has(type)))) return false;
1981
+ const pem = expandedKey[`${SECURITY}publicKeyPem`];
1982
+ const multibase = expandedKey[`${SECURITY}publicKeyMultibase`];
1983
+ if (pem === void 0 && multibase === void 0) return false;
1984
+ if (pem !== void 0 && !isSingleStringValue(pem)) return false;
1985
+ if (multibase !== void 0 && !isSingleStringValue(multibase, `${SECURITY}multibase`)) return false;
1986
+ for (const property of [`${SECURITY}owner`, `${SECURITY}controller`]) {
1987
+ const owners = expandedKey[property];
1988
+ if (owners === void 0) continue;
1989
+ 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;
1990
+ }
1991
+ return true;
1992
+ }
1993
+ function getCanonicalPortableIdOf(id) {
1994
+ try {
1995
+ return getCanonicalPortableId(parseIri(id));
1996
+ } catch (error) {
1997
+ if (error instanceof TypeError) return null;
1998
+ throw error;
1999
+ }
2000
+ }
2001
+ /**
2002
+ * Checks whether an ID is a compatible identifier of the given portable
2003
+ * object plus a fragment.
2004
+ */
2005
+ function isKeyIdOf(id, portableId) {
2006
+ let url;
2007
+ try {
2008
+ if (fromCompatibleEf61Id(id) == null) return false;
2009
+ url = new URL(id);
2010
+ } catch (error) {
2011
+ if (error instanceof TypeError) return false;
2012
+ throw error;
2013
+ }
2014
+ if (url.hash.length < 2) return false;
2015
+ url.hash = "";
2016
+ return getCanonicalPortableId(url) === portableId;
2017
+ }
2018
+ /**
2019
+ * Checks whether a `@context` value defines no keyword aliases and no scoped
2020
+ * contexts: only references to contexts and term definitions that map terms
2021
+ * to IRIs, optionally with an `@id` or `@vocab` type coercion or a datatype.
2022
+ */
2023
+ function isSimpleContext(context) {
2024
+ if (typeof context === "string") return true;
2025
+ if (Array.isArray(context)) return context.every(isSimpleContext);
2026
+ if (context === void 0 || !isCompoundProofJsonObject(context)) return false;
2027
+ return Object.entries(context).every(([term, definition]) => {
2028
+ if (term.startsWith("@")) return false;
2029
+ if (typeof definition === "string") return !definition.startsWith("@");
2030
+ if (!isCompoundProofJsonObject(definition)) return false;
2031
+ const { "@id": id, "@type": type, ...rest } = definition;
2032
+ return Object.keys(rest).length < 1 && typeof id === "string" && !id.startsWith("@") && (type === void 0 || type === "@id" || type === "@vocab" || typeof type === "string" && !type.startsWith("@"));
2033
+ });
2034
+ }
2035
+ function hasGraphKeyword(value) {
2036
+ if (Array.isArray(value)) return value.some(hasGraphKeyword);
2037
+ if (!isJsonMap(value)) return false;
2038
+ return Object.entries(value).some(([key, child]) => key === "@graph" || key === "@included" || key === "@reverse" || hasGraphKeyword(child));
2039
+ }
2040
+ /**
2041
+ * Checks whether expanded values are a single plain string, optionally with
2042
+ * the given datatype.
2043
+ */
2044
+ function isSingleStringValue(values, datatype) {
2045
+ if (!Array.isArray(values) || values.length !== 1) return false;
2046
+ const value = values[0];
2047
+ if (!isJsonMap(value) || typeof value["@value"] !== "string") return false;
2048
+ const keys = Object.keys(value);
2049
+ return keys.length === 1 || datatype != null && keys.length === 2 && value["@type"] === datatype;
2050
+ }
1849
2051
  //#endregion
1850
2052
  //#region src/federation/inbox.ts
1851
2053
  async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
@@ -2316,282 +2518,975 @@ function parseHashlinkGatewayRequest(url) {
2316
2518
  };
2317
2519
  }
2318
2520
  //#endregion
2319
- //#region src/federation/temporal.ts
2320
- function isPlainObject$1(value) {
2321
- return typeof value === "object" && value != null && !Array.isArray(value);
2322
- }
2323
- function normalizeDateTimeLiteral(value) {
2324
- return value.substring(19).match(/[Z+-]/) ? value : value + "Z";
2325
- }
2326
- function isMalformedDateTimeLiteral(value) {
2327
- if (typeof value !== "string") return false;
2328
- try {
2329
- Temporal.Instant.from(normalizeDateTimeLiteral(value));
2330
- return false;
2331
- } catch {
2332
- return true;
2333
- }
2334
- }
2335
- function isMalformedDurationLiteral(value) {
2336
- if (typeof value !== "string") return false;
2337
- try {
2338
- Temporal.Duration.from(value);
2339
- return false;
2340
- } catch {
2341
- return true;
2521
+ //#region src/federation/send.ts
2522
+ /**
2523
+ * Extracts the inbox URLs from recipients.
2524
+ * @param parameters The parameters to extract the inboxes.
2525
+ * See also {@link ExtractInboxesParameters}.
2526
+ * @returns The inboxes as a map of inbox URL to actor URIs.
2527
+ */
2528
+ function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
2529
+ const inboxes = {};
2530
+ for (const recipient of recipients) {
2531
+ let inbox;
2532
+ let sharedInbox = false;
2533
+ if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) {
2534
+ inbox = recipient.endpoints.sharedInbox;
2535
+ sharedInbox = true;
2536
+ } else inbox = recipient.inboxId;
2537
+ if (inbox != null && recipient.id != null) {
2538
+ if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
2539
+ inboxes[inbox.href] ??= {
2540
+ actorIds: /* @__PURE__ */ new Set(),
2541
+ sharedInbox
2542
+ };
2543
+ inboxes[inbox.href].actorIds.add(recipient.id.href);
2544
+ }
2342
2545
  }
2546
+ return inboxes;
2343
2547
  }
2344
- const TEMPORAL_DATE_TIME_IRIS = /* @__PURE__ */ new Set([
2345
- "https://www.w3.org/ns/activitystreams#deleted",
2346
- "https://www.w3.org/ns/activitystreams#endTime",
2347
- "https://www.w3.org/ns/activitystreams#published",
2348
- "https://www.w3.org/ns/activitystreams#startTime",
2349
- "https://www.w3.org/ns/activitystreams#updated",
2350
- "http://purl.org/dc/terms/created",
2351
- "https://w3id.org/security#created"
2352
- ]);
2353
- const TEMPORAL_DURATION_IRIS = /* @__PURE__ */ new Set(["https://www.w3.org/ns/activitystreams#duration"]);
2354
- const QUESTION_CLOSED_IRI = "https://www.w3.org/ns/activitystreams#closed";
2355
- const XSD_DATE_TIME_IRI = "http://www.w3.org/2001/XMLSchema#dateTime";
2356
- function hasMalformedExpandedDateTimeLiteral(value) {
2357
- if (Array.isArray(value)) return value.some(hasMalformedExpandedDateTimeLiteral);
2358
- return isPlainObject$1(value) && "@value" in value && isMalformedDateTimeLiteral(value["@value"]);
2359
- }
2360
- function hasMalformedExpandedQuestionClosedLiteral(value) {
2361
- if (Array.isArray(value)) return value.some(hasMalformedExpandedQuestionClosedLiteral);
2362
- if (!isPlainObject$1(value) || !("@value" in value)) return false;
2363
- const literal = value["@value"];
2364
- if (typeof literal === "boolean") return false;
2365
- if (typeof literal !== "string") return false;
2366
- if (value["@type"] !== XSD_DATE_TIME_IRI) return false;
2367
- if (new Date(literal).toString() === "Invalid Date") return false;
2368
- return isMalformedDateTimeLiteral(literal);
2548
+ /**
2549
+ * Sends an {@link Activity} to an inbox.
2550
+ *
2551
+ * @param parameters The parameters for sending the activity.
2552
+ * See also {@link SendActivityParameters}.
2553
+ * @throws {Error} If the activity fails to send.
2554
+ */
2555
+ function sendActivity(options) {
2556
+ const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
2557
+ return tracerProvider.getTracer(name, version).startActiveSpan("activitypub.send_activity", {
2558
+ kind: SpanKind.CLIENT,
2559
+ attributes: { "activitypub.shared_inbox": options.sharedInbox ?? false }
2560
+ }, async (span) => {
2561
+ if (options.activityId != null) span.setAttribute("activitypub.activity.id", options.activityId);
2562
+ if (options.activityType != null) span.setAttribute("activitypub.activity.type", options.activityType);
2563
+ try {
2564
+ await sendActivityInternal({
2565
+ ...options,
2566
+ tracerProvider
2567
+ }, span);
2568
+ } catch (e) {
2569
+ span.setStatus({
2570
+ code: SpanStatusCode.ERROR,
2571
+ message: String(e)
2572
+ });
2573
+ throw e;
2574
+ } finally {
2575
+ span.end();
2576
+ }
2577
+ });
2369
2578
  }
2370
- function hasMalformedExpandedDurationLiteral(value) {
2371
- if (Array.isArray(value)) return value.some(hasMalformedExpandedDurationLiteral);
2372
- return isPlainObject$1(value) && "@value" in value && isMalformedDurationLiteral(value["@value"]);
2579
+ const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
2580
+ function getActivityActorId(activity) {
2581
+ if (!isRecord(activity)) return void 0;
2582
+ return getIdValue(activity.actor);
2373
2583
  }
2374
- function hasMalformedKnownTemporalLiteralInternal(value, visited) {
2375
- if (Array.isArray(value)) return value.some((item) => hasMalformedKnownTemporalLiteralInternal(item, visited));
2376
- if (!isPlainObject$1(value)) return false;
2377
- if (visited.has(value)) return false;
2378
- visited.add(value);
2379
- if ("@value" in value) return false;
2380
- for (const [key, child] of Object.entries(value)) {
2381
- if (TEMPORAL_DATE_TIME_IRIS.has(key)) {
2382
- if (hasMalformedExpandedDateTimeLiteral(child)) return true;
2383
- continue;
2384
- }
2385
- if (key === QUESTION_CLOSED_IRI) {
2386
- if (hasMalformedExpandedQuestionClosedLiteral(child)) return true;
2387
- continue;
2388
- }
2389
- if (TEMPORAL_DURATION_IRIS.has(key)) {
2390
- if (hasMalformedExpandedDurationLiteral(child)) return true;
2391
- continue;
2584
+ function getIdValue(value) {
2585
+ if (typeof value === "string" && value !== "") return value;
2586
+ if (value instanceof URL) return value.href;
2587
+ if (Array.isArray(value)) {
2588
+ for (const item of value) {
2589
+ const id = getIdValue(item);
2590
+ if (id != null) return id;
2392
2591
  }
2393
- if (hasMalformedKnownTemporalLiteralInternal(child, visited)) return true;
2592
+ return;
2394
2593
  }
2395
- return false;
2594
+ if (isRecord(value)) return getIdValue(value.id);
2396
2595
  }
2397
- async function hasMalformedKnownTemporalLiteral(value, contextLoader) {
2596
+ function isRecord(value) {
2597
+ return typeof value === "object" && value != null;
2598
+ }
2599
+ async function readLimitedResponseBody(response, maxBytes) {
2600
+ if (response.body == null) return "";
2601
+ const reader = response.body.getReader();
2602
+ const decoder = new TextDecoder();
2603
+ const chunks = [];
2604
+ let totalBytes = 0;
2605
+ let truncated = false;
2398
2606
  try {
2399
- return hasMalformedKnownTemporalLiteralInternal(await jsonld.expand(value, {
2400
- documentLoader: getNormalizationContextLoader(contextLoader),
2401
- keepFreeFloatingNodes: true
2402
- }), /* @__PURE__ */ new Set());
2403
- } catch {
2404
- return false;
2607
+ while (true) {
2608
+ const { done, value } = await reader.read();
2609
+ if (done) break;
2610
+ if (totalBytes + value.length > maxBytes) {
2611
+ const remaining = maxBytes - totalBytes;
2612
+ if (remaining > 0) chunks.push(decoder.decode(value.slice(0, remaining), { stream: true }));
2613
+ truncated = true;
2614
+ break;
2615
+ }
2616
+ chunks.push(decoder.decode(value, { stream: true }));
2617
+ totalBytes += value.length;
2618
+ }
2619
+ } finally {
2620
+ reader.releaseLock();
2405
2621
  }
2622
+ let result = chunks.join("");
2623
+ if (truncated) result += "… (truncated)";
2624
+ return result;
2406
2625
  }
2407
- //#endregion
2408
- //#region src/federation/handler.ts
2409
- const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
2410
- const INBOX_COMPOUND_PROOF_LIMITS = {
2411
- maxDepth: 64,
2412
- maxMaps: 1e4,
2413
- maxProofs: 32,
2414
- maxBytes: 10 * 1024 * 1024
2415
- };
2416
- function isRemoteContextLoadingFailure$1(error) {
2417
- return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
2418
- }
2419
- function isPermanentRemoteContextError$1(error) {
2420
- if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
2421
- const details = error.details;
2422
- if (details?.code === "invalid remote context") return true;
2423
- return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) && isClearlyMalformedContextReference(details.url);
2424
- }
2425
- function isInvalidJsonLdError(error) {
2426
- if (!(error instanceof Error)) return false;
2427
- const name = error.name;
2428
- return name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
2429
- }
2430
- function isValidationTypeError(error) {
2431
- return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || isInvalidUrlTypeError(error));
2432
- }
2433
- function isPermanentActivityParseError(error) {
2434
- return isInvalidJsonLdError(error) || isValidationTypeError(error);
2435
- }
2436
- function hasHttpSignatureHeaders(request) {
2437
- return request.headers.has("Signature") || request.headers.has("Signature-Input");
2438
- }
2439
- function hasObjectIntegrityProof(json) {
2440
- return typeof json === "object" && json != null && "proof" in json;
2441
- }
2442
- /**
2443
- * Handles an actor request.
2444
- * @template TContextData The context data to pass to the context.
2445
- * @param request The HTTP request.
2446
- * @param parameters The parameters for handling the actor.
2447
- * @returns A promise that resolves to an HTTP response.
2448
- */
2449
- async function handleActor(request, { identifier, context, actorDispatcher, authorizePredicate, onNotFound, onUnauthorized }) {
2626
+ async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
2450
2627
  const logger = getLogger([
2451
2628
  "fedify",
2452
2629
  "federation",
2453
- "actor"
2630
+ "outbox"
2454
2631
  ]);
2455
- if (actorDispatcher == null) {
2456
- logger.debug("Actor dispatcher is not set.", { identifier });
2457
- return await onNotFound(request);
2632
+ const federationMetrics = getFederationMetrics(meterProvider);
2633
+ const started = performance.now();
2634
+ let deliverySuccess = false;
2635
+ async function validateUrl(url) {
2636
+ if (!allowPrivateAddress) try {
2637
+ await validatePublicUrl(url);
2638
+ } catch (error) {
2639
+ if (error instanceof UrlError) logger.error("Disallowed private URL: {url}", {
2640
+ url,
2641
+ error
2642
+ });
2643
+ throw error;
2644
+ }
2458
2645
  }
2459
- const actor = await actorDispatcher(context, identifier);
2460
- if (actor == null) {
2461
- logger.debug("Actor {identifier} not found.", { identifier });
2462
- return await onNotFound(request);
2646
+ await validateUrl(inbox.href);
2647
+ headers = new Headers(headers);
2648
+ headers.set("Content-Type", "application/activity+json");
2649
+ const request = new Request(inbox, {
2650
+ method: "POST",
2651
+ headers,
2652
+ body: JSON.stringify(activity),
2653
+ redirect: "manual"
2654
+ });
2655
+ let rsaKey = null;
2656
+ for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
2657
+ rsaKey = key;
2658
+ break;
2463
2659
  }
2464
- if (authorizePredicate != null) {
2465
- if (!await authorizePredicate(context, identifier)) return await onUnauthorized(request);
2660
+ if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
2661
+ activityId,
2662
+ inbox: inbox.href
2663
+ });
2664
+ 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.", {
2665
+ inbox: inbox.href,
2666
+ keys: keys.map((pair) => ({
2667
+ keyId: pair.keyId.href,
2668
+ privateKey: pair.privateKey
2669
+ }))
2670
+ });
2671
+ let response;
2672
+ try {
2673
+ response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await doubleKnock(request, rsaKey, {
2674
+ tracerProvider,
2675
+ specDeterminer,
2676
+ validateRedirect: validateUrl
2677
+ });
2678
+ } catch (error) {
2679
+ const failure = error instanceof FetchError || error instanceof UrlError ? error : createFetchError(inbox.href, error);
2680
+ logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
2681
+ activityId,
2682
+ inbox: inbox.href,
2683
+ error: failure
2684
+ });
2685
+ federationMetrics.recordDelivery(inbox, getDurationMs(started), false, activityType);
2686
+ throw failure;
2466
2687
  }
2467
- if (actor instanceof Tombstone) {
2468
- const jsonLd = await actor.toJsonLd(context);
2469
- return new Response(JSON.stringify(jsonLd), {
2470
- status: 410,
2471
- headers: {
2472
- "Content-Type": "application/activity+json",
2473
- Vary: "Accept"
2688
+ try {
2689
+ if (!response.ok) {
2690
+ let error;
2691
+ try {
2692
+ error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
2693
+ } catch (_) {
2694
+ error = "";
2474
2695
  }
2696
+ logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
2697
+ activityId,
2698
+ inbox: inbox.href,
2699
+ status: response.status,
2700
+ statusText: response.statusText,
2701
+ error
2702
+ });
2703
+ throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error, response.headers);
2704
+ }
2705
+ deliverySuccess = true;
2706
+ const eventAttributes = {
2707
+ "activitypub.inbox.url": inbox.href,
2708
+ "activitypub.activity.id": activityId ?? ""
2709
+ };
2710
+ if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
2711
+ const actorId = getActivityActorId(activity);
2712
+ if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
2713
+ span.addEvent("activitypub.activity.sent", eventAttributes);
2714
+ } finally {
2715
+ federationMetrics.recordDelivery(inbox, getDurationMs(started), deliverySuccess, activityType);
2716
+ }
2717
+ }
2718
+ function createFetchError(url, cause) {
2719
+ const error = new FetchError(url, cause instanceof Error ? cause.message : String(cause));
2720
+ error.cause = cause;
2721
+ return error;
2722
+ }
2723
+ async function fetchWithValidatedRedirects(request, validateUrl) {
2724
+ let body = await request.clone().text();
2725
+ for (let redirects = 0;; redirects++) {
2726
+ const response = await fetch(request, { redirect: "manual" });
2727
+ const location = response.headers.get("Location");
2728
+ if (![
2729
+ 301,
2730
+ 302,
2731
+ 303,
2732
+ 307,
2733
+ 308
2734
+ ].includes(response.status) || location == null) return response;
2735
+ await response.body?.cancel();
2736
+ if (redirects >= 20) throw new TypeError("Too many redirects");
2737
+ const url = new URL(location, request.url);
2738
+ await validateUrl(url.href);
2739
+ const headers = new Headers(request.headers);
2740
+ if (url.origin !== new URL(request.url).origin) {
2741
+ headers.delete("Authorization");
2742
+ headers.delete("Proxy-Authorization");
2743
+ headers.delete("Cookie");
2744
+ headers.delete("Host");
2745
+ }
2746
+ let method = request.method;
2747
+ if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
2748
+ method = "GET";
2749
+ body = void 0;
2750
+ for (const name of [
2751
+ "Content-Encoding",
2752
+ "Content-Language",
2753
+ "Content-Length",
2754
+ "Content-Location",
2755
+ "Content-Type"
2756
+ ]) headers.delete(name);
2757
+ }
2758
+ request = new Request(url, {
2759
+ method,
2760
+ headers,
2761
+ body,
2762
+ redirect: "manual"
2475
2763
  });
2476
2764
  }
2477
- const jsonLd = await actor.toJsonLd(context);
2478
- return new Response(JSON.stringify(jsonLd), { headers: {
2479
- "Content-Type": "application/activity+json",
2480
- Vary: "Accept"
2481
- } });
2482
2765
  }
2483
2766
  /**
2484
- * Handles an object request.
2485
- * @template TContextData The context data to pass to the context.
2486
- * @param request The HTTP request.
2487
- * @param parameters The parameters for handling the object.
2488
- * @returns A promise that resolves to an HTTP response.
2767
+ * An error that is thrown when an activity fails to send to a remote inbox.
2768
+ * It contains structured information about the failure, including the HTTP
2769
+ * status code, the inbox URL, and the response body.
2770
+ * @since 2.0.0
2489
2771
  */
2490
- async function handleObject(request, { values, context, objectDispatcher, authorizePredicate, onNotFound, onUnauthorized }) {
2491
- if (objectDispatcher == null) return await onNotFound(request);
2492
- const object = await objectDispatcher(context, values);
2493
- if (object == null) return await onNotFound(request);
2494
- if (authorizePredicate != null) {
2495
- if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
2772
+ var SendActivityError = class extends Error {
2773
+ /**
2774
+ * The inbox URL that the activity was being sent to.
2775
+ */
2776
+ inbox;
2777
+ /**
2778
+ * The HTTP status code returned by the inbox.
2779
+ */
2780
+ statusCode;
2781
+ /**
2782
+ * The response body from the inbox, if any. Note that this may be
2783
+ * truncated to a maximum of 1 KiB to prevent excessive memory consumption
2784
+ * when remote servers return large error pages (e.g., Cloudflare error pages).
2785
+ * If truncated, the string will end with `"… (truncated)"`.
2786
+ */
2787
+ responseBody;
2788
+ /**
2789
+ * The response headers from the inbox.
2790
+ * @since 2.3.0
2791
+ */
2792
+ responseHeaders;
2793
+ /**
2794
+ * Creates a new {@link SendActivityError}.
2795
+ * @param inbox The inbox URL.
2796
+ * @param statusCode The HTTP status code.
2797
+ * @param message The error message.
2798
+ * @param responseBody The response body.
2799
+ * @param responseHeaders The response headers.
2800
+ */
2801
+ constructor(inbox, statusCode, message, responseBody, responseHeaders) {
2802
+ super(message);
2803
+ this.name = "SendActivityError";
2804
+ this.inbox = inbox;
2805
+ this.statusCode = statusCode;
2806
+ this.responseBody = responseBody;
2807
+ this.responseHeaders = new Headers(responseHeaders);
2496
2808
  }
2497
- const jsonLd = await object.toJsonLd(context);
2498
- return new Response(JSON.stringify(jsonLd), { headers: {
2499
- "Content-Type": "application/activity+json",
2500
- Vary: "Accept"
2501
- } });
2502
- }
2809
+ };
2810
+ //#endregion
2811
+ //#region src/federation/portable-inbox.ts
2812
+ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
2503
2813
  /**
2504
- * Handles an FEP-ef61 gateway request for a portable object through an object
2505
- * dispatcher. The object is served only if its ID canonically matches the
2506
- * requested portable ID and it satisfies the FEP-ef61 proof policy.
2507
- * @template TContextData The context data to pass to the context.
2508
- * @param request The HTTP request.
2509
- * @param parameters The parameters for handling the portable object.
2510
- * @returns A promise that resolves to an HTTP response.
2814
+ * Fills in the defaults of {@link PortableInboxForwardingOptions} and
2815
+ * validates them.
2816
+ * @param options The options.
2817
+ * @returns The resolved options.
2818
+ * @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
2819
+ * is not positive, if `deadline` is negative or longer
2820
+ * than 2,147,483,647 milliseconds (about 24.8 days), or
2821
+ * if either duration has calendar units, i.e., weeks,
2822
+ * months, or years, whose length depends on the date.
2511
2823
  */
2512
- async function handlePortableObject(request, { values, context, objectDispatcher, authorizePredicate, canonicalId, onNotFound, onUnauthorized }) {
2513
- const logger = getLogger([
2514
- "fedify",
2515
- "federation",
2516
- "object"
2517
- ]);
2518
- if (objectDispatcher == null) return await onNotFound(request);
2519
- const object = await objectDispatcher(context, values);
2520
- if (object == null) return await onNotFound(request);
2521
- if (!isRequestedPortableObject(object, canonicalId)) {
2522
- logger.debug("The object {objectId} does not match the requested portable object {portableId}.", {
2523
- objectId: object.id?.href,
2524
- portableId: canonicalId
2525
- });
2526
- return await onNotFound(request);
2824
+ function resolvePortableInboxForwardingOptions(options = {}) {
2825
+ const maxTargets = options.maxTargets ?? 10;
2826
+ if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
2827
+ const ttl = Temporal.Duration.from(options.ttl ?? { days: 30 });
2828
+ if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
2829
+ const deadline = Temporal.Duration.from(options.deadline ?? { seconds: 10 });
2830
+ const deadlineMs = deadline.total("millisecond");
2831
+ if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
2832
+ return {
2833
+ maxTargets,
2834
+ ttl,
2835
+ deadline
2836
+ };
2837
+ }
2838
+ /**
2839
+ * Decides whether this server accepts a delivery to a portable inbox on behalf
2840
+ * of the given actor, which the actor dispatcher returned for the identifier
2841
+ * in the requested inbox path.
2842
+ *
2843
+ * The actor document comes from the application, so it is trusted as is and
2844
+ * its proof is not verified here.
2845
+ * @param actor The actor the actor dispatcher returned.
2846
+ * @param options The requested DID, the canonical form of the requested inbox
2847
+ * ID, and the origin of this server.
2848
+ * @returns The resolution.
2849
+ */
2850
+ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
2851
+ if (actor == null) return {
2852
+ status: "rejected",
2853
+ reason: "notFound"
2854
+ };
2855
+ const id = actor.id;
2856
+ const did = id == null || !isPortableId(id) ? null : getPortableDid(id);
2857
+ if (id == null || did == null) return {
2858
+ status: "rejected",
2859
+ reason: "notPortable"
2860
+ };
2861
+ if (did !== authority) return {
2862
+ status: "rejected",
2863
+ reason: "authorityMismatch"
2864
+ };
2865
+ const inboxId = actor.inboxId == null ? null : toPortableUri(actor.inboxId);
2866
+ if (inboxId == null || getCanonicalPortableId(inboxId) !== canonicalInboxId) return {
2867
+ status: "rejected",
2868
+ reason: "inboxMismatch"
2869
+ };
2870
+ const gateways = actor.gateways;
2871
+ if (!gateways.some((g) => isGatewayUrl(g) && g.origin === localOrigin)) return {
2872
+ status: "rejected",
2873
+ reason: "notGateway"
2874
+ };
2875
+ return {
2876
+ status: "accepted",
2877
+ recipient: {
2878
+ actorId: id,
2879
+ inboxId,
2880
+ canonicalInboxId,
2881
+ gateways
2882
+ }
2883
+ };
2884
+ }
2885
+ /**
2886
+ * Gets the portable form of an `ap:` or `ap+ef61:` URI or a compatible
2887
+ * identifier, keeping its query.
2888
+ * @returns The portable URI, or `null` if the ID is neither, or is malformed.
2889
+ */
2890
+ function toPortableUri(id) {
2891
+ if (isPortableUri(id)) return id;
2892
+ try {
2893
+ return fromCompatibleEf61Id(id);
2894
+ } catch (error) {
2895
+ if (error instanceof TypeError) return null;
2896
+ throw error;
2527
2897
  }
2528
- if (authorizePredicate != null) {
2529
- if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
2898
+ }
2899
+ const RFC9421_DELIVERY_COMPONENTS = [
2900
+ "@method",
2901
+ "@target-uri",
2902
+ "content-digest"
2903
+ ];
2904
+ const DRAFT_DELIVERY_COMPONENTS = [
2905
+ "(request-target)",
2906
+ "host",
2907
+ "digest"
2908
+ ];
2909
+ /**
2910
+ * Checks whether an HTTP Signature covers the method, the target URI, and the
2911
+ * body of a request, which both Fedify and Mitra sign when forwarding.
2912
+ * @param signature The signature.
2913
+ * @returns `true` if the signature covers them.
2914
+ */
2915
+ function coversDelivery(signature) {
2916
+ 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));
2917
+ }
2918
+ /**
2919
+ * Forwards an activity received in a portable inbox to the inboxes of the same
2920
+ * portable actor on its other gateways, as FEP-ef61 recommends.
2921
+ *
2922
+ * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
2923
+ * each target gateway is claimed with an atomic compare-and-swap before the
2924
+ * activity is handed off, and a claim is never released, even if the hand-off
2925
+ * fails, since a failed hand-off does not prove that nothing was delivered.
2926
+ * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
2927
+ * forwarded.
2928
+ *
2929
+ * Forwarded requests are signed with HTTP Signatures by the RSA key among
2930
+ * {@link ForwardPortableInboxActivityParameters.getKeys}, if any, so that
2931
+ * servers requiring HTTP Signatures accept them, and are sent unsigned
2932
+ * otherwise. Either way, the receiving gateways authenticate the activity by
2933
+ * its own proof.
2934
+ *
2935
+ * If the delivery is signed with a gateway key of the recipient by one of its
2936
+ * other gateways, that gateway has the activity already, so it is claimed
2937
+ * without forwarding the activity to it. See {@link identifySendingGateway}.
2938
+ * @param parameters The parameters.
2939
+ * @returns The target inbox URLs that the activity was handed off to.
2940
+ */
2941
+ async function forwardPortableInboxActivity(parameters) {
2942
+ const logger = getLogger([
2943
+ "fedify",
2944
+ "federation",
2945
+ "inbox"
2946
+ ]);
2947
+ const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
2948
+ const options = parameters.options ?? resolvePortableInboxForwardingOptions();
2949
+ const { maxTargets, ttl } = options;
2950
+ if (kv.cas == null || maxTargets < 1) return [];
2951
+ const deadline = Date.now() + options.deadline.total("millisecond");
2952
+ const canonicalActivityId = getCanonicalPortableId(activityId) ?? activityId.href;
2953
+ const claimKey = (gateway) => [
2954
+ ...kvPrefix,
2955
+ recipient.canonicalInboxId,
2956
+ canonicalActivityId,
2957
+ gateway
2958
+ ];
2959
+ let targets = getForwardingTargets(recipient, excludedOrigins);
2960
+ const sender = await identifySendingGateway(parameters, targets.map((t) => t.gateway), claimKey, deadline);
2961
+ if (sender != null) {
2962
+ logger.debug("Not forwarding activity {activityId} from the portable inbox {inbox} back to the gateway {gateway}, which delivered it.", {
2963
+ activityId: activityId.href,
2964
+ inbox: recipient.canonicalInboxId,
2965
+ gateway: sender
2966
+ });
2967
+ await kv.cas(claimKey(sender), void 0, true, { ttl });
2968
+ targets = targets.filter((t) => t.gateway !== sender);
2530
2969
  }
2531
- let jsonLd;
2532
- let servedId;
2533
- let result;
2534
- try {
2535
- jsonLd = await object.toJsonLd(context);
2536
- servedId = await getJsonLdRootId(jsonLd, context.contextLoader);
2537
- result = await verifyPortableObjectProof(jsonLd, {
2538
- contextLoader: context.contextLoader,
2539
- documentLoader: context.documentLoader,
2540
- tracerProvider: context.tracerProvider,
2541
- meterProvider: context.meterProvider
2970
+ if (targets.length > maxTargets) {
2971
+ 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.", {
2972
+ inbox: recipient.canonicalInboxId,
2973
+ activityId: activityId.href,
2974
+ max: maxTargets
2542
2975
  });
2543
- } catch (error) {
2544
- logger.error("Failed to verify the Object Integrity Proofs of the portable object {portableId}:\n{error}", {
2545
- portableId: canonicalId,
2976
+ targets.length = maxTargets;
2977
+ }
2978
+ const claimed = [];
2979
+ for (const { gateway, inbox } of targets) if (await kv.cas(claimKey(gateway), void 0, true, { ttl })) claimed.push(inbox);
2980
+ if (claimed.length < 1) return [];
2981
+ logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
2982
+ activityId: activityId.href,
2983
+ inbox: recipient.canonicalInboxId,
2984
+ targets: claimed.map((t) => t.href)
2985
+ });
2986
+ if (outboxQueue == null) await forwardImmediately(parameters, claimed, deadline);
2987
+ else await enqueueForwarding(parameters, outboxQueue, claimed);
2988
+ return claimed;
2989
+ }
2990
+ /**
2991
+ * Identifies the gateway of the recipient that forwarded a delivery to its
2992
+ * portable inbox, by the delivery's HTTP Signature made with the recipient's
2993
+ * gateway key, which FEP-ef61 has each gateway use for requests on behalf of
2994
+ * an actor. Since the activity is authenticated by its own proof, the
2995
+ * signature only tells which gateway to skip, and a delivery whose signature
2996
+ * is invalid or cannot be verified is still forwarded to every gateway.
2997
+ *
2998
+ * Verifying the signature fetches the recipient's actor document from the
2999
+ * sending gateway, as gateway keys are not cached, so only the first signature
3000
+ * that may save a request is verified: one that covers the delivery and names
3001
+ * a gateway key of the recipient on one of the forwarding targets that the
3002
+ * activity has not been forwarded to yet.
3003
+ * @param parameters The forwarding parameters.
3004
+ * @param targets The origins of the gateways to forward to.
3005
+ * @param claimKey Gets the key of the claim for a gateway.
3006
+ * @param deadline The time by which identification has to finish, in
3007
+ * milliseconds since the epoch; afterwards, the sending
3008
+ * gateway is left unidentified.
3009
+ * @returns The origin of the sending gateway, or `null` if it is not
3010
+ * identified.
3011
+ */
3012
+ async function identifySendingGateway({ recipient, activityId, kv, signatures }, targets, claimKey, deadline) {
3013
+ if (signatures == null || signatures.length < 1) return null;
3014
+ const logger = getLogger([
3015
+ "fedify",
3016
+ "federation",
3017
+ "inbox"
3018
+ ]);
3019
+ const actorId = getCanonicalPortableId(recipient.actorId);
3020
+ if (actorId == null) return null;
3021
+ const isGatewayKeyOfRecipient = (keyId) => {
3022
+ if (!isCompatibleKeyId(keyId) || !targets.includes(keyId.origin)) return false;
3023
+ const base = new URL(keyId.href);
3024
+ base.hash = "";
3025
+ return getCanonicalPortableId(base) === actorId;
3026
+ };
3027
+ let signature;
3028
+ for (const candidate of signatures) if (candidate.coversDelivery && isGatewayKeyOfRecipient(candidate.keyId) && await kv.get(claimKey(candidate.keyId.origin)) === void 0) {
3029
+ signature = candidate;
3030
+ break;
3031
+ }
3032
+ if (signature == null) return null;
3033
+ const { keyId } = signature;
3034
+ const logProperties = {
3035
+ activityId: activityId.href,
3036
+ inbox: recipient.canonicalInboxId,
3037
+ keyId: keyId.href
3038
+ };
3039
+ const verification = Promise.resolve().then(() => signature.verify()).then((key) => {
3040
+ 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);
3041
+ return key;
3042
+ }, (error) => {
3043
+ 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}", {
3044
+ ...logProperties,
2546
3045
  error
2547
3046
  });
2548
- return portableObjectInternalServerError(request);
2549
- }
2550
- if (servedId == null || !isRequestedPortableId(servedId, canonicalId)) {
2551
- logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
2552
- return await onNotFound(request);
3047
+ return null;
3048
+ });
3049
+ let timer;
3050
+ const key = await Promise.race([verification, new Promise((resolve) => {
3051
+ timer = setTimeout(() => {
3052
+ 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);
3053
+ resolve(null);
3054
+ }, Math.max(0, deadline - Date.now()));
3055
+ })]);
3056
+ clearTimeout(timer);
3057
+ if (key == null) return null;
3058
+ if (key.id == null || key.id.href !== keyId.href || !isGatewayKeyOfRecipient(key.id) || key.ownerId == null || !isPortableId(key.ownerId) || getCanonicalPortableId(key.ownerId) !== actorId) {
3059
+ 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);
3060
+ return null;
2553
3061
  }
2554
- if (!result.verified && result.reason.type !== "unsecuredCollection" && result.reason.type !== "unsupportedObjectType") {
2555
- logger.error("Refusing to serve the portable object {portableId}, as it does not satisfy the FEP-ef61 proof policy: {reason}. Portable actors, activities, and objects need an Object Integrity Proof made with a key of the DID in their ID.", {
2556
- portableId: canonicalId,
2557
- reason: result.reason.type
3062
+ return key.id.origin;
3063
+ }
3064
+ function getForwardingTargets(recipient, excludedOrigins) {
3065
+ const logger = getLogger([
3066
+ "fedify",
3067
+ "federation",
3068
+ "inbox"
3069
+ ]);
3070
+ const seen = new Set(excludedOrigins);
3071
+ const targets = [];
3072
+ for (const gateway of recipient.gateways) {
3073
+ if (!isGatewayUrl(gateway) || seen.has(gateway.origin)) continue;
3074
+ seen.add(gateway.origin);
3075
+ let inbox;
3076
+ try {
3077
+ inbox = toCompatibleEf61Id(recipient.inboxId, gateway.origin);
3078
+ } catch (error) {
3079
+ if (!(error instanceof TypeError)) throw error;
3080
+ logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
3081
+ gateway: gateway.origin,
3082
+ inbox: recipient.canonicalInboxId
3083
+ });
3084
+ continue;
3085
+ }
3086
+ targets.push({
3087
+ gateway: gateway.origin,
3088
+ inbox
2558
3089
  });
2559
- return portableObjectInternalServerError(request);
2560
3090
  }
2561
- return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), { headers: {
2562
- "Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
2563
- Vary: "Accept"
2564
- } });
3091
+ return targets;
2565
3092
  }
2566
- function isRequestedPortableObject(object, canonicalId) {
2567
- if (object.id == null) return false;
2568
- if (object.id.protocol !== "ap:" && object.id.protocol !== "ap+ef61:") return false;
3093
+ /**
3094
+ * Resolves the key that signs forwarded requests: the first RSASSA-PKCS1-v1_5
3095
+ * key, as {@link sendActivity} signs requests only with such a key. Since
3096
+ * the targets have already been claimed, the activity is forwarded unsigned
3097
+ * rather than not at all if the key cannot be resolved or used.
3098
+ */
3099
+ async function resolveForwardingKey({ getKeys, activityId }) {
3100
+ if (getKeys == null) return null;
3101
+ const logger = getLogger([
3102
+ "fedify",
3103
+ "federation",
3104
+ "inbox"
3105
+ ]);
3106
+ let key;
2569
3107
  try {
2570
- return canonicalizePortableUri(formatIri(object.id)) === canonicalId;
3108
+ key = (await getKeys()).find((k) => k.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5");
2571
3109
  } catch (error) {
2572
- if (error instanceof TypeError) return false;
2573
- throw error;
3110
+ logger.error("Failed to get the gateway keys to sign forwarded requests with; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
3111
+ activityId: activityId.href,
3112
+ error
3113
+ });
3114
+ return null;
2574
3115
  }
2575
- }
2576
- async function getJsonLdRootId(jsonLd, contextLoader) {
2577
- const expanded = await jsonld.expand(jsonLd, {
2578
- documentLoader: getNormalizationContextLoader(contextLoader),
2579
- keepFreeFloatingNodes: true
2580
- });
2581
- if (expanded.length !== 1) return null;
2582
- const id = expanded[0]["@id"];
2583
- return typeof id === "string" ? id : null;
2584
- }
2585
- function isRequestedPortableId(id, canonicalId) {
3116
+ if (key == null) return null;
2586
3117
  try {
2587
- return canonicalizePortableUri(id) === canonicalId;
3118
+ validateCryptoKey(key.privateKey, "private");
2588
3119
  } catch (error) {
2589
- if (error instanceof TypeError) return false;
2590
- throw error;
3120
+ if (!(error instanceof TypeError)) throw error;
3121
+ logger.warn("The gateway key {keyId} cannot sign forwarded requests; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
3122
+ keyId: key.keyId.href,
3123
+ activityId: activityId.href,
3124
+ error
3125
+ });
3126
+ return null;
2591
3127
  }
3128
+ return key;
2592
3129
  }
2593
- function portableObjectInternalServerError(request) {
2594
- return new Response(request.method === "HEAD" ? null : "Internal server error.", {
3130
+ async function forwardImmediately(parameters, inboxes, deadlineAt) {
3131
+ const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, meterProvider, tracerProvider } = parameters;
3132
+ const logger = getLogger([
3133
+ "fedify",
3134
+ "federation",
3135
+ "inbox"
3136
+ ]);
3137
+ const deadline = Math.max(0, deadlineAt - Date.now());
3138
+ const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
3139
+ activity,
3140
+ activityId: activityId.href,
3141
+ activityType,
3142
+ keys: key == null ? [] : [key],
3143
+ inbox,
3144
+ allowPrivateAddress,
3145
+ specDeterminer,
3146
+ meterProvider,
3147
+ tracerProvider
3148
+ }).catch((error) => {
3149
+ logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
3150
+ activityId: activityId.href,
3151
+ inbox: inbox.href,
3152
+ error
3153
+ });
3154
+ }))));
3155
+ let timer;
3156
+ const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
3157
+ timer = setTimeout(() => resolve(true), deadline);
3158
+ })]);
3159
+ clearTimeout(timer);
3160
+ if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
3161
+ activityId: activityId.href,
3162
+ deadline
3163
+ });
3164
+ }
3165
+ async function enqueueForwarding(parameters, outboxQueue, inboxes) {
3166
+ const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
3167
+ const logger = getLogger([
3168
+ "fedify",
3169
+ "federation",
3170
+ "inbox"
3171
+ ]);
3172
+ const key = await resolveForwardingKey(parameters);
3173
+ const keys = [];
3174
+ if (key != null) try {
3175
+ keys.push({
3176
+ keyId: key.keyId.href,
3177
+ privateKey: await exportJwk(key.privateKey)
3178
+ });
3179
+ } catch (error) {
3180
+ logger.error("Failed to export the gateway key {keyId}; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
3181
+ keyId: key.keyId.href,
3182
+ activityId: activityId.href,
3183
+ error
3184
+ });
3185
+ }
3186
+ startQueue?.();
3187
+ const started = (/* @__PURE__ */ new Date()).toISOString();
3188
+ const traceContext = {};
3189
+ propagation.inject(context.active(), traceContext);
3190
+ await Promise.all(inboxes.map(async (inbox) => {
3191
+ const message = {
3192
+ type: "outbox",
3193
+ id: crypto.randomUUID(),
3194
+ baseUrl,
3195
+ keys,
3196
+ activity,
3197
+ activityId: activityId.href,
3198
+ activityType,
3199
+ inbox: inbox.href,
3200
+ sharedInbox: false,
3201
+ started,
3202
+ attempt: 0,
3203
+ headers: {},
3204
+ traceContext
3205
+ };
3206
+ try {
3207
+ await outboxQueue.enqueue(message);
3208
+ } catch (error) {
3209
+ logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
3210
+ activityId: activityId.href,
3211
+ inbox: inbox.href,
3212
+ error
3213
+ });
3214
+ return;
3215
+ }
3216
+ recordOutboxEnqueue(meterProvider, outboxQueue, message);
3217
+ }));
3218
+ }
3219
+ //#endregion
3220
+ //#region src/federation/temporal.ts
3221
+ function isPlainObject$1(value) {
3222
+ return typeof value === "object" && value != null && !Array.isArray(value);
3223
+ }
3224
+ function normalizeDateTimeLiteral(value) {
3225
+ return value.substring(19).match(/[Z+-]/) ? value : value + "Z";
3226
+ }
3227
+ function isMalformedDateTimeLiteral(value) {
3228
+ if (typeof value !== "string") return false;
3229
+ try {
3230
+ Temporal.Instant.from(normalizeDateTimeLiteral(value));
3231
+ return false;
3232
+ } catch {
3233
+ return true;
3234
+ }
3235
+ }
3236
+ function isMalformedDurationLiteral(value) {
3237
+ if (typeof value !== "string") return false;
3238
+ try {
3239
+ Temporal.Duration.from(value);
3240
+ return false;
3241
+ } catch {
3242
+ return true;
3243
+ }
3244
+ }
3245
+ const TEMPORAL_DATE_TIME_IRIS = /* @__PURE__ */ new Set([
3246
+ "https://www.w3.org/ns/activitystreams#deleted",
3247
+ "https://www.w3.org/ns/activitystreams#endTime",
3248
+ "https://www.w3.org/ns/activitystreams#published",
3249
+ "https://www.w3.org/ns/activitystreams#startTime",
3250
+ "https://www.w3.org/ns/activitystreams#updated",
3251
+ "http://purl.org/dc/terms/created",
3252
+ "https://w3id.org/security#created"
3253
+ ]);
3254
+ const TEMPORAL_DURATION_IRIS = /* @__PURE__ */ new Set(["https://www.w3.org/ns/activitystreams#duration"]);
3255
+ const QUESTION_CLOSED_IRI = "https://www.w3.org/ns/activitystreams#closed";
3256
+ const XSD_DATE_TIME_IRI = "http://www.w3.org/2001/XMLSchema#dateTime";
3257
+ function hasMalformedExpandedDateTimeLiteral(value) {
3258
+ if (Array.isArray(value)) return value.some(hasMalformedExpandedDateTimeLiteral);
3259
+ return isPlainObject$1(value) && "@value" in value && isMalformedDateTimeLiteral(value["@value"]);
3260
+ }
3261
+ function hasMalformedExpandedQuestionClosedLiteral(value) {
3262
+ if (Array.isArray(value)) return value.some(hasMalformedExpandedQuestionClosedLiteral);
3263
+ if (!isPlainObject$1(value) || !("@value" in value)) return false;
3264
+ const literal = value["@value"];
3265
+ if (typeof literal === "boolean") return false;
3266
+ if (typeof literal !== "string") return false;
3267
+ if (value["@type"] !== XSD_DATE_TIME_IRI) return false;
3268
+ if (new Date(literal).toString() === "Invalid Date") return false;
3269
+ return isMalformedDateTimeLiteral(literal);
3270
+ }
3271
+ function hasMalformedExpandedDurationLiteral(value) {
3272
+ if (Array.isArray(value)) return value.some(hasMalformedExpandedDurationLiteral);
3273
+ return isPlainObject$1(value) && "@value" in value && isMalformedDurationLiteral(value["@value"]);
3274
+ }
3275
+ function hasMalformedKnownTemporalLiteralInternal(value, visited) {
3276
+ if (Array.isArray(value)) return value.some((item) => hasMalformedKnownTemporalLiteralInternal(item, visited));
3277
+ if (!isPlainObject$1(value)) return false;
3278
+ if (visited.has(value)) return false;
3279
+ visited.add(value);
3280
+ if ("@value" in value) return false;
3281
+ for (const [key, child] of Object.entries(value)) {
3282
+ if (TEMPORAL_DATE_TIME_IRIS.has(key)) {
3283
+ if (hasMalformedExpandedDateTimeLiteral(child)) return true;
3284
+ continue;
3285
+ }
3286
+ if (key === QUESTION_CLOSED_IRI) {
3287
+ if (hasMalformedExpandedQuestionClosedLiteral(child)) return true;
3288
+ continue;
3289
+ }
3290
+ if (TEMPORAL_DURATION_IRIS.has(key)) {
3291
+ if (hasMalformedExpandedDurationLiteral(child)) return true;
3292
+ continue;
3293
+ }
3294
+ if (hasMalformedKnownTemporalLiteralInternal(child, visited)) return true;
3295
+ }
3296
+ return false;
3297
+ }
3298
+ async function hasMalformedKnownTemporalLiteral(value, contextLoader) {
3299
+ try {
3300
+ return hasMalformedKnownTemporalLiteralInternal(await jsonld.expand(value, {
3301
+ documentLoader: getNormalizationContextLoader(contextLoader),
3302
+ keepFreeFloatingNodes: true
3303
+ }), /* @__PURE__ */ new Set());
3304
+ } catch {
3305
+ return false;
3306
+ }
3307
+ }
3308
+ //#endregion
3309
+ //#region src/federation/handler.ts
3310
+ const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
3311
+ const INBOX_COMPOUND_PROOF_LIMITS = {
3312
+ maxDepth: 64,
3313
+ maxMaps: 1e4,
3314
+ maxProofs: 32,
3315
+ maxBytes: 10 * 1024 * 1024
3316
+ };
3317
+ function isRemoteContextLoadingFailure$1(error) {
3318
+ return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
3319
+ }
3320
+ function isPermanentRemoteContextError$1(error) {
3321
+ if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
3322
+ const details = error.details;
3323
+ if (details?.code === "invalid remote context") return true;
3324
+ return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) && isClearlyMalformedContextReference(details.url);
3325
+ }
3326
+ function isInvalidJsonLdError(error) {
3327
+ if (!(error instanceof Error)) return false;
3328
+ const name = error.name;
3329
+ return name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
3330
+ }
3331
+ function isValidationTypeError(error) {
3332
+ return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || isInvalidUrlTypeError(error));
3333
+ }
3334
+ function isPermanentActivityParseError(error) {
3335
+ return isInvalidJsonLdError(error) || isValidationTypeError(error);
3336
+ }
3337
+ function hasHttpSignatureHeaders(request) {
3338
+ return request.headers.has("Signature") || request.headers.has("Signature-Input");
3339
+ }
3340
+ function hasObjectIntegrityProof(json) {
3341
+ return typeof json === "object" && json != null && "proof" in json;
3342
+ }
3343
+ /**
3344
+ * Handles an actor request.
3345
+ * @template TContextData The context data to pass to the context.
3346
+ * @param request The HTTP request.
3347
+ * @param parameters The parameters for handling the actor.
3348
+ * @returns A promise that resolves to an HTTP response.
3349
+ */
3350
+ async function handleActor(request, { identifier, context, actorDispatcher, authorizePredicate, onNotFound, onUnauthorized }) {
3351
+ const logger = getLogger([
3352
+ "fedify",
3353
+ "federation",
3354
+ "actor"
3355
+ ]);
3356
+ if (actorDispatcher == null) {
3357
+ logger.debug("Actor dispatcher is not set.", { identifier });
3358
+ return await onNotFound(request);
3359
+ }
3360
+ const actor = await actorDispatcher(context, identifier);
3361
+ if (actor == null) {
3362
+ logger.debug("Actor {identifier} not found.", { identifier });
3363
+ return await onNotFound(request);
3364
+ }
3365
+ if (authorizePredicate != null) {
3366
+ if (!await authorizePredicate(context, identifier)) return await onUnauthorized(request);
3367
+ }
3368
+ if (actor instanceof Tombstone) {
3369
+ const jsonLd = await actor.toJsonLd(context);
3370
+ return new Response(JSON.stringify(jsonLd), {
3371
+ status: 410,
3372
+ headers: {
3373
+ "Content-Type": "application/activity+json",
3374
+ Vary: "Accept"
3375
+ }
3376
+ });
3377
+ }
3378
+ const jsonLd = await actor.toJsonLd(context);
3379
+ return new Response(JSON.stringify(jsonLd), { headers: {
3380
+ "Content-Type": "application/activity+json",
3381
+ Vary: "Accept"
3382
+ } });
3383
+ }
3384
+ /**
3385
+ * Handles an object request.
3386
+ * @template TContextData The context data to pass to the context.
3387
+ * @param request The HTTP request.
3388
+ * @param parameters The parameters for handling the object.
3389
+ * @returns A promise that resolves to an HTTP response.
3390
+ */
3391
+ async function handleObject(request, { values, context, objectDispatcher, authorizePredicate, onNotFound, onUnauthorized }) {
3392
+ if (objectDispatcher == null) return await onNotFound(request);
3393
+ const object = await objectDispatcher(context, values);
3394
+ if (object == null) return await onNotFound(request);
3395
+ if (authorizePredicate != null) {
3396
+ if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
3397
+ }
3398
+ const jsonLd = await object.toJsonLd(context);
3399
+ return new Response(JSON.stringify(jsonLd), { headers: {
3400
+ "Content-Type": "application/activity+json",
3401
+ Vary: "Accept"
3402
+ } });
3403
+ }
3404
+ /**
3405
+ * Handles an FEP-ef61 gateway request for a portable object through an object
3406
+ * dispatcher. The object is served only if its ID canonically matches the
3407
+ * requested portable ID and it satisfies the FEP-ef61 proof policy.
3408
+ * @template TContextData The context data to pass to the context.
3409
+ * @param request The HTTP request.
3410
+ * @param parameters The parameters for handling the portable object.
3411
+ * @returns A promise that resolves to an HTTP response.
3412
+ */
3413
+ async function handlePortableObject(request, { values, context, objectDispatcher, authorizePredicate, canonicalId, onNotFound, onUnauthorized }) {
3414
+ const logger = getLogger([
3415
+ "fedify",
3416
+ "federation",
3417
+ "object"
3418
+ ]);
3419
+ if (objectDispatcher == null) return await onNotFound(request);
3420
+ const object = await objectDispatcher(context, values);
3421
+ if (object == null) return await onNotFound(request);
3422
+ if (!isRequestedPortableObject(object, canonicalId)) {
3423
+ logger.debug("The object {objectId} does not match the requested portable object {portableId}.", {
3424
+ objectId: object.id?.href,
3425
+ portableId: canonicalId
3426
+ });
3427
+ return await onNotFound(request);
3428
+ }
3429
+ if (authorizePredicate != null) {
3430
+ if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
3431
+ }
3432
+ let jsonLd;
3433
+ let servedId;
3434
+ let result;
3435
+ try {
3436
+ jsonLd = await object.toJsonLd(context);
3437
+ servedId = await getJsonLdRootId(jsonLd, context.contextLoader);
3438
+ result = await verifyPortableObjectProof(jsonLd, {
3439
+ contextLoader: context.contextLoader,
3440
+ documentLoader: context.documentLoader,
3441
+ tracerProvider: context.tracerProvider,
3442
+ meterProvider: context.meterProvider
3443
+ });
3444
+ } catch (error) {
3445
+ logger.error("Failed to verify the Object Integrity Proofs of the portable object {portableId}:\n{error}", {
3446
+ portableId: canonicalId,
3447
+ error
3448
+ });
3449
+ return portableObjectInternalServerError(request);
3450
+ }
3451
+ if (servedId == null || !isRequestedPortableId(servedId, canonicalId)) {
3452
+ logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
3453
+ return await onNotFound(request);
3454
+ }
3455
+ if (!result.verified && result.reason.type !== "unsecuredCollection" && result.reason.type !== "unsupportedObjectType") {
3456
+ logger.error("Refusing to serve the portable object {portableId}, as it does not satisfy the FEP-ef61 proof policy: {reason}. Portable actors, activities, and objects need an Object Integrity Proof made with a key of the DID in their ID.", {
3457
+ portableId: canonicalId,
3458
+ reason: result.reason.type
3459
+ });
3460
+ return portableObjectInternalServerError(request);
3461
+ }
3462
+ return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), { headers: {
3463
+ "Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
3464
+ Vary: "Accept"
3465
+ } });
3466
+ }
3467
+ function isRequestedPortableObject(object, canonicalId) {
3468
+ return object.id != null && getCanonicalPortableId(object.id) === canonicalId;
3469
+ }
3470
+ async function getJsonLdRootId(jsonLd, contextLoader) {
3471
+ const expanded = await jsonld.expand(jsonLd, {
3472
+ documentLoader: getNormalizationContextLoader(contextLoader),
3473
+ keepFreeFloatingNodes: true
3474
+ });
3475
+ if (expanded.length !== 1) return null;
3476
+ const id = expanded[0]["@id"];
3477
+ return typeof id === "string" ? id : null;
3478
+ }
3479
+ function isRequestedPortableId(id, canonicalId) {
3480
+ if (!/^ap(?:\+ef61)?:\/\//i.test(id)) return URL.canParse(id) && getCanonicalPortableId(new URL(id)) === canonicalId;
3481
+ try {
3482
+ return canonicalizePortableUri(id) === canonicalId;
3483
+ } catch (error) {
3484
+ if (error instanceof TypeError) return false;
3485
+ throw error;
3486
+ }
3487
+ }
3488
+ function portableObjectInternalServerError(request) {
3489
+ return new Response(request.method === "HEAD" ? null : "Internal server error.", {
2595
3490
  status: 500,
2596
3491
  headers: { "Content-Type": "text/plain; charset=utf-8" }
2597
3492
  });
@@ -2633,13 +3528,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
2633
3528
  * @param parameters The parameters for handling the collection.
2634
3529
  * @returns A promise that resolves to an HTTP response.
2635
3530
  */
2636
- async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
2637
- const spanName = name$1.trim().replace(/\s+/g, "_");
3531
+ async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
3532
+ const spanName = name$2.trim().replace(/\s+/g, "_");
2638
3533
  tracerProvider = tracerProvider ?? trace.getTracerProvider();
2639
3534
  const tracer = tracerProvider.getTracer(name, version);
2640
3535
  const cursor = new URL(request.url).searchParams.get("cursor");
2641
3536
  const metricBase = {
2642
- kind: getCollectionMetricKind(name$1),
3537
+ kind: getCollectionMetricKind(name$2),
2643
3538
  page: cursor != null,
2644
3539
  dispatcher: "built_in"
2645
3540
  };
@@ -2680,7 +3575,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
2680
3575
  span.setStatus({ code: SpanStatusCode.ERROR });
2681
3576
  return await onNotFound(request);
2682
3577
  }
2683
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
3578
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
2684
3579
  itemCount = items.length;
2685
3580
  span.setAttribute("fedify.collection.items", itemCount);
2686
3581
  return items;
@@ -2720,7 +3615,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
2720
3615
  } else {
2721
3616
  const uri = new URL(baseUri);
2722
3617
  uri.searchParams.set("cursor", cursor);
2723
- const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
3618
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
2724
3619
  kind: SpanKind.SERVER,
2725
3620
  attributes: {
2726
3621
  "activitypub.collection.id": uri.href,
@@ -2736,7 +3631,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
2736
3631
  span.setStatus({ code: SpanStatusCode.ERROR });
2737
3632
  return await onNotFound(request);
2738
3633
  }
2739
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
3634
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
2740
3635
  itemCount = items.length;
2741
3636
  span.setAttribute("fedify.collection.items", itemCount);
2742
3637
  return {
@@ -3445,7 +4340,7 @@ async function handleInboxInternal(request, parameters, span) {
3445
4340
  return await respondInvalidActivity(error);
3446
4341
  }
3447
4342
  activityVerified = true;
3448
- if (!skipSignatureVerification && hasPortableActor(activity)) try {
4343
+ if (!skipSignatureVerification && isPortableActivity(activity)) try {
3449
4344
  proofVerified = await verifyObject(Activity, jsonWithoutSig, {
3450
4345
  contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
3451
4346
  documentLoader: ctx.documentLoader,
@@ -3629,7 +4524,7 @@ async function handleInboxInternal(request, parameters, span) {
3629
4524
  "http_signatures.verified": httpSigKey != null,
3630
4525
  "http_signatures.key_id": httpSigKey?.id?.href ?? ""
3631
4526
  });
3632
- if (!skipSignatureVerification && !proofVerified && (hasPortableActor(activity) || isPortableGatewayKey(httpSigKey))) {
4527
+ if (!skipSignatureVerification && !proofVerified && (isPortableActivity(activity) || isPortableGatewayKey(httpSigKey))) {
3633
4528
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
3634
4529
  logger.error("The activity {activityId} of the portable actor {actorId} is not authenticated by a valid Object Integrity Proof.", {
3635
4530
  activity: json,
@@ -3647,6 +4542,32 @@ async function handleInboxInternal(request, parameters, span) {
3647
4542
  headers: { "Content-Type": "text/plain; charset=utf-8" }
3648
4543
  });
3649
4544
  }
4545
+ if (!skipSignatureVerification && activity.id != null && isPortableId(activity.id)) {
4546
+ const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
4547
+ contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
4548
+ documentLoader: ctx.documentLoader,
4549
+ keyCache,
4550
+ meterProvider,
4551
+ tracerProvider
4552
+ });
4553
+ if (rejection != null) {
4554
+ if (deferredLdSignatureError != null) throw deferredLdSignatureError;
4555
+ logger.error("The portable activity {activityId} is not authenticated by an Object Integrity Proof of its own DID: {reason}", {
4556
+ activity: json,
4557
+ recipient,
4558
+ activityId: activity.id.href,
4559
+ reason: rejection
4560
+ });
4561
+ span.setStatus({
4562
+ code: SpanStatusCode.ERROR,
4563
+ message: `The portable activity (${activity.id.href}) is not authenticated by an Object Integrity Proof of its own DID.`
4564
+ });
4565
+ return new Response("Portable activities must have valid Object Integrity Proofs made by their own DIDs.", {
4566
+ status: 401,
4567
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
4568
+ });
4569
+ }
4570
+ }
3650
4571
  if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
3651
4572
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
3652
4573
  getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : getRemoteHost(httpSigKey.id));
@@ -3723,7 +4644,21 @@ async function handleInboxInternal(request, parameters, span) {
3723
4644
  inbox: portableInbox.recipient.canonicalInboxId
3724
4645
  });
3725
4646
  else try {
3726
- await portableInbox.forward(json, activity.id, getTypeId(activity).href);
4647
+ await portableInbox.forward(json, activity.id, getTypeId(activity).href, listRequestSignatures(request).map((signature) => ({
4648
+ keyId: signature.keyId,
4649
+ coversDelivery: coversDelivery(signature),
4650
+ verify: async () => {
4651
+ const verification = await verifyRequestDetailed(selectRequestSignature(request, signature), {
4652
+ contextLoader: ctx.contextLoader,
4653
+ documentLoader: ctx.documentLoader,
4654
+ timeWindow: signatureTimeWindow,
4655
+ keyCache,
4656
+ meterProvider,
4657
+ tracerProvider
4658
+ });
4659
+ return verification.verified ? verification.key : null;
4660
+ }
4661
+ })));
3727
4662
  } catch (error) {
3728
4663
  logger.error("Failed to forward activity {activityId} to the other gateways of the portable inbox {inbox}:\n{error}", {
3729
4664
  activityId: activity.id.href,
@@ -3884,8 +4819,8 @@ var CustomCollectionHandler = class {
3884
4819
  * @param CollectionPage The CollectionPage constructor.
3885
4820
  * @param filterPredicate Optional filter predicate for items.
3886
4821
  */
3887
- constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
3888
- this.name = name$2;
4822
+ constructor(name$1, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
4823
+ this.name = name$1;
3889
4824
  this.values = values;
3890
4825
  this.context = context;
3891
4826
  this.callbacks = callbacks;
@@ -4166,543 +5101,287 @@ function exceptWrapper(handler) {
4166
5101
  dispatcher: "custom"
4167
5102
  };
4168
5103
  try {
4169
- const response = await handler(request, handlerParams);
4170
- recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "served", response));
4171
- return response;
4172
- } catch (error) {
4173
- const { onNotFound, onUnauthorized } = handlerParams;
4174
- switch (error?.constructor) {
4175
- case ItemsNotFoundError: {
4176
- const response = await onNotFound(request);
4177
- recordDeferredPendingCollectionMetrics(error, "not_found", response);
4178
- recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "not_found", response));
4179
- return response;
4180
- }
4181
- case UnauthorizedError: {
4182
- const response = await onUnauthorized(request);
4183
- recordDeferredPendingCollectionMetrics(error, "unauthorized", response);
4184
- recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "unauthorized", response));
4185
- return response;
4186
- }
4187
- default:
4188
- recordDeferredPendingCollectionMetrics(error, "error");
4189
- recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "error"));
4190
- throw error;
4191
- }
4192
- }
4193
- };
4194
- }
4195
- /**
4196
- * Verifies that a value is defined (not undefined).
4197
- * @template T The type of the value, excluding undefined.
4198
- * @param callbacks The value to verify.
4199
- * @throws {ItemsNotFoundError} If the value is undefined.
4200
- * @since 1.8.0
4201
- */
4202
- const verifyDefined = (callbacks) => {
4203
- if (callbacks === void 0) throw new ItemsNotFoundError();
4204
- };
4205
- /**
4206
- * Performs authorization if needed based on the authorization predicate.
4207
- * @template TContextData The context data type.
4208
- * @param {RequestContext<TContextData>} context The request context.
4209
- * @param {Record<string, string>} values The parameter values.
4210
- * @param options Options containing the authorization predicate.
4211
- * @throws {UnauthorizedError} If authorization fails.
4212
- * @since 1.8.0
4213
- */
4214
- const authIfNeeded = async (context, values, { authorizePredicate: authorize = void 0 }) => {
4215
- if (authorize === void 0) return;
4216
- if (!await authorize(context, values)) throw new UnauthorizedError();
4217
- };
4218
- /**
4219
- * Appends a cursor parameter to a URL if the cursor exists.
4220
- * @template Cursor The type of the cursor (string, null, or undefined).
4221
- * @param {URL} url The base URL to append the cursor to.
4222
- * @param {string | null | undefined} cursor The cursor value to append.
4223
- * @returns The URL with cursor appended if cursor is a string, null otherwise.
4224
- * @since 1.8.0
4225
- */
4226
- const appendCursorIfExists = (url, cursor) => {
4227
- if (cursor === null || cursor === void 0) return null;
4228
- const copied = new URL(url);
4229
- copied.searchParams.set("cursor", cursor);
4230
- return copied;
4231
- };
4232
- /**
4233
- * Creates an HTTP response for ActivityPub data.
4234
- * @param {unknown} data The data to serialize as JSON-LD.
4235
- * @returns {Response} An HTTP response with the data as ActivityPub JSON.
4236
- * @since 1.8.0
4237
- */
4238
- const respondAsActivity = (data) => new Response(JSON.stringify(data), { headers: {
4239
- "Content-Type": "application/activity+json",
4240
- Vary: "Accept"
4241
- } });
4242
- /**
4243
- * Base class for handler errors.
4244
- * @since 1.8.0
4245
- */
4246
- var HandlerError = class extends Error {
4247
- constructor(message) {
4248
- super(message);
4249
- }
4250
- /**
4251
- * Throws this error.
4252
- * @returns Never returns, always throws.
4253
- */
4254
- throw() {
4255
- throw this;
4256
- }
4257
- };
4258
- /**
4259
- * Error thrown when items are not found in a collection.
4260
- * @since 1.8.0
4261
- */
4262
- var ItemsNotFoundError = class extends HandlerError {
4263
- constructor() {
4264
- super("Items not found in the collection.");
4265
- }
4266
- };
4267
- /**
4268
- * Error thrown when access to a collection is unauthorized.
4269
- * @since 1.8.0
4270
- */
4271
- var UnauthorizedError = class extends HandlerError {
4272
- constructor() {
4273
- super("Unauthorized access to the collection.");
4274
- }
4275
- };
4276
- /**
4277
- * Responds with the given object in JSON-LD format.
4278
- *
4279
- * @param object The object to respond with.
4280
- * @param options Options.
4281
- * @since 0.3.0
4282
- */
4283
- async function respondWithObject(object, options) {
4284
- const jsonLd = await object.toJsonLd(options);
4285
- return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json" } });
4286
- }
4287
- /**
4288
- * Responds with the given object in JSON-LD format if the request accepts
4289
- * JSON-LD.
4290
- *
4291
- * @param object The object to respond with.
4292
- * @param request The request to check for JSON-LD acceptability.
4293
- * @param options Options.
4294
- * @since 0.3.0
4295
- */
4296
- async function respondWithObjectIfAcceptable(object, request, options) {
4297
- if (!acceptsJsonLd(request)) return null;
4298
- const response = await respondWithObject(object, options);
4299
- response.headers.set("Vary", "Accept");
4300
- return response;
4301
- }
4302
- function generateNonce() {
4303
- const bytes = /* @__PURE__ */ new Uint8Array(16);
4304
- crypto.getRandomValues(bytes);
4305
- return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
4306
- }
4307
- async function verifySignatureNonce(request, kv, noncePrefix, verifiedLabel) {
4308
- const signatureInput = request.headers.get("Signature-Input");
4309
- if (signatureInput == null) return false;
4310
- const parsed = parseRfc9421SignatureInput(signatureInput);
4311
- if (verifiedLabel == null) return false;
4312
- const sig = parsed[verifiedLabel];
4313
- if (sig == null) return false;
4314
- const nonce = sig.nonce;
4315
- if (nonce == null) return false;
4316
- const key = [...noncePrefix, nonce];
4317
- if (kv.cas != null) return await kv.cas(key, true, void 0);
4318
- if (await kv.get(key) != null) {
4319
- await kv.delete(key);
4320
- return true;
4321
- }
4322
- return false;
4323
- }
4324
- const getFailedSignatureResponse = async (policy, kv, kvPrefixes) => {
4325
- const headers = await getFailedSignatureHeaders(policy, kv, kvPrefixes);
4326
- return new Response("Failed to verify the request signature.", {
4327
- status: 401,
4328
- headers
4329
- });
4330
- };
4331
- const getFailedSignatureHeaders = async (policy, kv, kvPrefixes) => ({
4332
- "Content-Type": "text/plain; charset=utf-8",
4333
- ...policy?.enabled && {
4334
- "Accept-Signature": await buildAcceptSignatureHeader(policy, kv, kvPrefixes.acceptSignatureNonce),
4335
- "Cache-Control": "no-store",
4336
- "Vary": "Accept, Signature"
4337
- }
4338
- });
4339
- async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
4340
- const parameters = { created: true };
4341
- if (policy.requestNonce) {
4342
- const nonce = generateNonce();
4343
- await setKey(kv, [...noncePrefix, nonce], policy);
4344
- parameters.nonce = nonce;
4345
- }
4346
- const baseComponents = policy.components ?? DEF_COMPONENTS;
4347
- return formatAcceptSignature([{
4348
- label: "sig1",
4349
- components: uniq(MIN_COMPONENTS.concat(baseComponents)).filter((c) => c !== "@status").map((v) => ({
4350
- value: v,
4351
- params: {}
4352
- })),
4353
- parameters
4354
- }]);
4355
- }
4356
- async function setKey(kv, key, policy) {
4357
- const seconds = policy.nonceTtlSeconds ?? 300;
4358
- const ttl = Temporal.Duration.from({ seconds });
4359
- await kv.set(key, true, { ttl });
5104
+ const response = await handler(request, handlerParams);
5105
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "served", response));
5106
+ return response;
5107
+ } catch (error) {
5108
+ const { onNotFound, onUnauthorized } = handlerParams;
5109
+ switch (error?.constructor) {
5110
+ case ItemsNotFoundError: {
5111
+ const response = await onNotFound(request);
5112
+ recordDeferredPendingCollectionMetrics(error, "not_found", response);
5113
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "not_found", response));
5114
+ return response;
5115
+ }
5116
+ case UnauthorizedError: {
5117
+ const response = await onUnauthorized(request);
5118
+ recordDeferredPendingCollectionMetrics(error, "unauthorized", response);
5119
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "unauthorized", response));
5120
+ return response;
5121
+ }
5122
+ default:
5123
+ recordDeferredPendingCollectionMetrics(error, "error");
5124
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "error"));
5125
+ throw error;
5126
+ }
5127
+ }
5128
+ };
4360
5129
  }
4361
- const DEF_COMPONENTS = [
4362
- "@method",
4363
- "@target-uri",
4364
- "@authority",
4365
- "content-digest"
4366
- ];
4367
- const MIN_COMPONENTS = [
4368
- "@method",
4369
- "@target-uri",
4370
- "@authority"
4371
- ];
4372
5130
  /**
4373
- * Checks whether an HTTP Signature key is a gateway key of an FEP-ef61
4374
- * portable actor, i.e., its ID is a compatible identifier and it is owned by
4375
- * a portable actor, which only the portable actor's signed document can
4376
- * establish.
5131
+ * Verifies that a value is defined (not undefined).
5132
+ * @template T The type of the value, excluding undefined.
5133
+ * @param callbacks The value to verify.
5134
+ * @throws {ItemsNotFoundError} If the value is undefined.
5135
+ * @since 1.8.0
4377
5136
  */
4378
- function isPortableGatewayKey(key) {
4379
- return key?.id != null && isCompatibleKeyId(key.id) && key.ownerId != null && isPortableUri(key.ownerId);
4380
- }
4381
- //#endregion
4382
- //#region src/nodeinfo/handler.ts
5137
+ const verifyDefined = (callbacks) => {
5138
+ if (callbacks === void 0) throw new ItemsNotFoundError();
5139
+ };
4383
5140
  /**
4384
- * Handles a NodeInfo request. You would not typically call this function
4385
- * directly, but instead use {@link Federation.handle} method.
4386
- * @param request The NodeInfo request to handle.
4387
- * @param parameters The parameters for handling the request.
4388
- * @returns The response to the request.
5141
+ * Performs authorization if needed based on the authorization predicate.
5142
+ * @template TContextData The context data type.
5143
+ * @param {RequestContext<TContextData>} context The request context.
5144
+ * @param {Record<string, string>} values The parameter values.
5145
+ * @param options Options containing the authorization predicate.
5146
+ * @throws {UnauthorizedError} If authorization fails.
5147
+ * @since 1.8.0
4389
5148
  */
4390
- async function handleNodeInfo(_request, { context, nodeInfoDispatcher }) {
4391
- const promise = nodeInfoDispatcher(context);
4392
- const json = nodeInfoToJson(promise instanceof Promise ? await promise : promise);
4393
- return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\"" } });
4394
- }
5149
+ const authIfNeeded = async (context, values, { authorizePredicate: authorize = void 0 }) => {
5150
+ if (authorize === void 0) return;
5151
+ if (!await authorize(context, values)) throw new UnauthorizedError();
5152
+ };
4395
5153
  /**
4396
- * Handles a request to `/.well-known/nodeinfo`. You would not typically call
4397
- * this function directly, but instead use {@link Federation.handle} method.
4398
- * @param request The request to handle.
4399
- * @param context The request context.
4400
- * @returns The response to the request.
5154
+ * Appends a cursor parameter to a URL if the cursor exists.
5155
+ * @template Cursor The type of the cursor (string, null, or undefined).
5156
+ * @param {URL} url The base URL to append the cursor to.
5157
+ * @param {string | null | undefined} cursor The cursor value to append.
5158
+ * @returns The URL with cursor appended if cursor is a string, null otherwise.
5159
+ * @since 1.8.0
4401
5160
  */
4402
- function handleNodeInfoJrd(_request, context) {
4403
- const links = [];
4404
- try {
4405
- links.push({
4406
- rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
4407
- href: context.getNodeInfoUri().href,
4408
- type: "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\""
4409
- });
4410
- } catch (e) {
4411
- if (!(e instanceof RouterError)) throw e;
4412
- }
4413
- const response = new Response(JSON.stringify({ links }), { headers: { "Content-Type": "application/jrd+json" } });
4414
- return Promise.resolve(response);
4415
- }
4416
- //#endregion
4417
- //#region src/federation/send.ts
5161
+ const appendCursorIfExists = (url, cursor) => {
5162
+ if (cursor === null || cursor === void 0) return null;
5163
+ const copied = new URL(url);
5164
+ copied.searchParams.set("cursor", cursor);
5165
+ return copied;
5166
+ };
4418
5167
  /**
4419
- * Extracts the inbox URLs from recipients.
4420
- * @param parameters The parameters to extract the inboxes.
4421
- * See also {@link ExtractInboxesParameters}.
4422
- * @returns The inboxes as a map of inbox URL to actor URIs.
5168
+ * Creates an HTTP response for ActivityPub data.
5169
+ * @param {unknown} data The data to serialize as JSON-LD.
5170
+ * @returns {Response} An HTTP response with the data as ActivityPub JSON.
5171
+ * @since 1.8.0
4423
5172
  */
4424
- function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
4425
- const inboxes = {};
4426
- for (const recipient of recipients) {
4427
- let inbox;
4428
- let sharedInbox = false;
4429
- if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) {
4430
- inbox = recipient.endpoints.sharedInbox;
4431
- sharedInbox = true;
4432
- } else inbox = recipient.inboxId;
4433
- if (inbox != null && recipient.id != null) {
4434
- if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
4435
- inboxes[inbox.href] ??= {
4436
- actorIds: /* @__PURE__ */ new Set(),
4437
- sharedInbox
4438
- };
4439
- inboxes[inbox.href].actorIds.add(recipient.id.href);
4440
- }
4441
- }
4442
- return inboxes;
4443
- }
5173
+ const respondAsActivity = (data) => new Response(JSON.stringify(data), { headers: {
5174
+ "Content-Type": "application/activity+json",
5175
+ Vary: "Accept"
5176
+ } });
4444
5177
  /**
4445
- * Sends an {@link Activity} to an inbox.
4446
- *
4447
- * @param parameters The parameters for sending the activity.
4448
- * See also {@link SendActivityParameters}.
4449
- * @throws {Error} If the activity fails to send.
5178
+ * Base class for handler errors.
5179
+ * @since 1.8.0
4450
5180
  */
4451
- function sendActivity(options) {
4452
- const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
4453
- return tracerProvider.getTracer(name, version).startActiveSpan("activitypub.send_activity", {
4454
- kind: SpanKind.CLIENT,
4455
- attributes: { "activitypub.shared_inbox": options.sharedInbox ?? false }
4456
- }, async (span) => {
4457
- if (options.activityId != null) span.setAttribute("activitypub.activity.id", options.activityId);
4458
- if (options.activityType != null) span.setAttribute("activitypub.activity.type", options.activityType);
4459
- try {
4460
- await sendActivityInternal({
4461
- ...options,
4462
- tracerProvider
4463
- }, span);
4464
- } catch (e) {
4465
- span.setStatus({
4466
- code: SpanStatusCode.ERROR,
4467
- message: String(e)
4468
- });
4469
- throw e;
4470
- } finally {
4471
- span.end();
4472
- }
4473
- });
4474
- }
4475
- const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
4476
- function getActivityActorId(activity) {
4477
- if (!isRecord(activity)) return void 0;
4478
- return getIdValue(activity.actor);
4479
- }
4480
- function getIdValue(value) {
4481
- if (typeof value === "string" && value !== "") return value;
4482
- if (value instanceof URL) return value.href;
4483
- if (Array.isArray(value)) {
4484
- for (const item of value) {
4485
- const id = getIdValue(item);
4486
- if (id != null) return id;
4487
- }
4488
- return;
5181
+ var HandlerError = class extends Error {
5182
+ constructor(message) {
5183
+ super(message);
4489
5184
  }
4490
- if (isRecord(value)) return getIdValue(value.id);
4491
- }
4492
- function isRecord(value) {
4493
- return typeof value === "object" && value != null;
4494
- }
4495
- async function readLimitedResponseBody(response, maxBytes) {
4496
- if (response.body == null) return "";
4497
- const reader = response.body.getReader();
4498
- const decoder = new TextDecoder();
4499
- const chunks = [];
4500
- let totalBytes = 0;
4501
- let truncated = false;
4502
- try {
4503
- while (true) {
4504
- const { done, value } = await reader.read();
4505
- if (done) break;
4506
- if (totalBytes + value.length > maxBytes) {
4507
- const remaining = maxBytes - totalBytes;
4508
- if (remaining > 0) chunks.push(decoder.decode(value.slice(0, remaining), { stream: true }));
4509
- truncated = true;
4510
- break;
4511
- }
4512
- chunks.push(decoder.decode(value, { stream: true }));
4513
- totalBytes += value.length;
4514
- }
4515
- } finally {
4516
- reader.releaseLock();
5185
+ /**
5186
+ * Throws this error.
5187
+ * @returns Never returns, always throws.
5188
+ */
5189
+ throw() {
5190
+ throw this;
4517
5191
  }
4518
- let result = chunks.join("");
4519
- if (truncated) result += "… (truncated)";
4520
- return result;
4521
- }
4522
- async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
4523
- const logger = getLogger([
4524
- "fedify",
4525
- "federation",
4526
- "outbox"
4527
- ]);
4528
- const federationMetrics = getFederationMetrics(meterProvider);
4529
- const started = performance.now();
4530
- let deliverySuccess = false;
4531
- async function validateUrl(url) {
4532
- if (!allowPrivateAddress) try {
4533
- await validatePublicUrl(url);
4534
- } catch (error) {
4535
- if (error instanceof UrlError) logger.error("Disallowed private URL: {url}", {
4536
- url,
4537
- error
4538
- });
4539
- throw error;
4540
- }
5192
+ };
5193
+ /**
5194
+ * Error thrown when items are not found in a collection.
5195
+ * @since 1.8.0
5196
+ */
5197
+ var ItemsNotFoundError = class extends HandlerError {
5198
+ constructor() {
5199
+ super("Items not found in the collection.");
5200
+ }
5201
+ };
5202
+ /**
5203
+ * Error thrown when access to a collection is unauthorized.
5204
+ * @since 1.8.0
5205
+ */
5206
+ var UnauthorizedError = class extends HandlerError {
5207
+ constructor() {
5208
+ super("Unauthorized access to the collection.");
4541
5209
  }
4542
- await validateUrl(inbox.href);
4543
- headers = new Headers(headers);
4544
- headers.set("Content-Type", "application/activity+json");
4545
- const request = new Request(inbox, {
4546
- method: "POST",
4547
- headers,
4548
- body: JSON.stringify(activity),
4549
- redirect: "manual"
4550
- });
4551
- let rsaKey = null;
4552
- for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
4553
- rsaKey = key;
4554
- break;
5210
+ };
5211
+ /**
5212
+ * Responds with the given object in JSON-LD format.
5213
+ *
5214
+ * @param object The object to respond with.
5215
+ * @param options Options.
5216
+ * @since 0.3.0
5217
+ */
5218
+ async function respondWithObject(object, options) {
5219
+ const jsonLd = await object.toJsonLd(options);
5220
+ return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json" } });
5221
+ }
5222
+ /**
5223
+ * Responds with the given object in JSON-LD format if the request accepts
5224
+ * JSON-LD.
5225
+ *
5226
+ * @param object The object to respond with.
5227
+ * @param request The request to check for JSON-LD acceptability.
5228
+ * @param options Options.
5229
+ * @since 0.3.0
5230
+ */
5231
+ async function respondWithObjectIfAcceptable(object, request, options) {
5232
+ if (!acceptsJsonLd(request)) return null;
5233
+ const response = await respondWithObject(object, options);
5234
+ response.headers.set("Vary", "Accept");
5235
+ return response;
5236
+ }
5237
+ function generateNonce() {
5238
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
5239
+ crypto.getRandomValues(bytes);
5240
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
5241
+ }
5242
+ async function verifySignatureNonce(request, kv, noncePrefix, verifiedLabel) {
5243
+ const signatureInput = request.headers.get("Signature-Input");
5244
+ if (signatureInput == null) return false;
5245
+ const parsed = parseRfc9421SignatureInput(signatureInput);
5246
+ if (verifiedLabel == null) return false;
5247
+ const sig = parsed[verifiedLabel];
5248
+ if (sig == null) return false;
5249
+ const nonce = sig.nonce;
5250
+ if (nonce == null) return false;
5251
+ const key = [...noncePrefix, nonce];
5252
+ if (kv.cas != null) return await kv.cas(key, true, void 0);
5253
+ if (await kv.get(key) != null) {
5254
+ await kv.delete(key);
5255
+ return true;
4555
5256
  }
4556
- if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
4557
- activityId,
4558
- inbox: inbox.href
4559
- });
4560
- 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.", {
4561
- inbox: inbox.href,
4562
- keys: keys.map((pair) => ({
4563
- keyId: pair.keyId.href,
4564
- privateKey: pair.privateKey
4565
- }))
5257
+ return false;
5258
+ }
5259
+ const getFailedSignatureResponse = async (policy, kv, kvPrefixes) => {
5260
+ const headers = await getFailedSignatureHeaders(policy, kv, kvPrefixes);
5261
+ return new Response("Failed to verify the request signature.", {
5262
+ status: 401,
5263
+ headers
4566
5264
  });
4567
- let response;
4568
- try {
4569
- response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await doubleKnock(request, rsaKey, {
4570
- tracerProvider,
4571
- specDeterminer,
4572
- validateRedirect: validateUrl
4573
- });
4574
- } catch (error) {
4575
- const failure = error instanceof FetchError || error instanceof UrlError ? error : createFetchError(inbox.href, error);
4576
- logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
4577
- activityId,
4578
- inbox: inbox.href,
4579
- error: failure
4580
- });
4581
- federationMetrics.recordDelivery(inbox, getDurationMs(started), false, activityType);
4582
- throw failure;
5265
+ };
5266
+ const getFailedSignatureHeaders = async (policy, kv, kvPrefixes) => ({
5267
+ "Content-Type": "text/plain; charset=utf-8",
5268
+ ...policy?.enabled && {
5269
+ "Accept-Signature": await buildAcceptSignatureHeader(policy, kv, kvPrefixes.acceptSignatureNonce),
5270
+ "Cache-Control": "no-store",
5271
+ "Vary": "Accept, Signature"
4583
5272
  }
4584
- try {
4585
- if (!response.ok) {
4586
- let error;
4587
- try {
4588
- error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
4589
- } catch (_) {
4590
- error = "";
4591
- }
4592
- logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
4593
- activityId,
4594
- inbox: inbox.href,
4595
- status: response.status,
4596
- statusText: response.statusText,
4597
- error
4598
- });
4599
- throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error, response.headers);
4600
- }
4601
- deliverySuccess = true;
4602
- const eventAttributes = {
4603
- "activitypub.inbox.url": inbox.href,
4604
- "activitypub.activity.id": activityId ?? ""
4605
- };
4606
- if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
4607
- const actorId = getActivityActorId(activity);
4608
- if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
4609
- span.addEvent("activitypub.activity.sent", eventAttributes);
4610
- } finally {
4611
- federationMetrics.recordDelivery(inbox, getDurationMs(started), deliverySuccess, activityType);
5273
+ });
5274
+ async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
5275
+ const parameters = { created: true };
5276
+ if (policy.requestNonce) {
5277
+ const nonce = generateNonce();
5278
+ await setKey(kv, [...noncePrefix, nonce], policy);
5279
+ parameters.nonce = nonce;
4612
5280
  }
5281
+ const baseComponents = policy.components ?? DEF_COMPONENTS;
5282
+ return formatAcceptSignature([{
5283
+ label: "sig1",
5284
+ components: uniq(MIN_COMPONENTS.concat(baseComponents)).filter((c) => c !== "@status").map((v) => ({
5285
+ value: v,
5286
+ params: {}
5287
+ })),
5288
+ parameters
5289
+ }]);
4613
5290
  }
4614
- function createFetchError(url, cause) {
4615
- const error = new FetchError(url, cause instanceof Error ? cause.message : String(cause));
4616
- error.cause = cause;
4617
- return error;
5291
+ async function setKey(kv, key, policy) {
5292
+ const seconds = policy.nonceTtlSeconds ?? 300;
5293
+ const ttl = Temporal.Duration.from({ seconds });
5294
+ await kv.set(key, true, { ttl });
4618
5295
  }
4619
- async function fetchWithValidatedRedirects(request, validateUrl) {
4620
- let body = await request.clone().text();
4621
- for (let redirects = 0;; redirects++) {
4622
- const response = await fetch(request, { redirect: "manual" });
4623
- const location = response.headers.get("Location");
4624
- if (![
4625
- 301,
4626
- 302,
4627
- 303,
4628
- 307,
4629
- 308
4630
- ].includes(response.status) || location == null) return response;
4631
- await response.body?.cancel();
4632
- if (redirects >= 20) throw new TypeError("Too many redirects");
4633
- const url = new URL(location, request.url);
4634
- await validateUrl(url.href);
4635
- const headers = new Headers(request.headers);
4636
- if (url.origin !== new URL(request.url).origin) {
4637
- headers.delete("Authorization");
4638
- headers.delete("Proxy-Authorization");
4639
- headers.delete("Cookie");
4640
- headers.delete("Host");
4641
- }
4642
- let method = request.method;
4643
- if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
4644
- method = "GET";
4645
- body = void 0;
4646
- for (const name of [
4647
- "Content-Encoding",
4648
- "Content-Language",
4649
- "Content-Length",
4650
- "Content-Location",
4651
- "Content-Type"
4652
- ]) headers.delete(name);
4653
- }
4654
- request = new Request(url, {
4655
- method,
4656
- headers,
4657
- body,
4658
- redirect: "manual"
4659
- });
4660
- }
5296
+ const DEF_COMPONENTS = [
5297
+ "@method",
5298
+ "@target-uri",
5299
+ "@authority",
5300
+ "content-digest"
5301
+ ];
5302
+ const MIN_COMPONENTS = [
5303
+ "@method",
5304
+ "@target-uri",
5305
+ "@authority"
5306
+ ];
5307
+ /**
5308
+ * Checks whether an HTTP Signature key is a gateway key of an FEP-ef61
5309
+ * portable actor, i.e., its ID is a compatible identifier and it is owned by
5310
+ * a portable actor, which only the portable actor's signed document can
5311
+ * establish.
5312
+ */
5313
+ function isPortableGatewayKey(key) {
5314
+ return key?.id != null && isCompatibleKeyId(key.id) && key.ownerId != null && isPortableId(key.ownerId);
4661
5315
  }
4662
5316
  /**
4663
- * An error that is thrown when an activity fails to send to a remote inbox.
4664
- * It contains structured information about the failure, including the HTTP
4665
- * status code, the inbox URL, and the response body.
4666
- * @since 2.0.0
5317
+ * Checks whether an activity needs an Object Integrity Proof by its DID:
5318
+ * either it is performed by an [FEP-ef61] portable actor, or its own ID is
5319
+ * portable. Both `ap:` URIs and compatible identifiers count as portable.
5320
+ *
5321
+ * [FEP-ef61]: https://w3id.org/fep/ef61
4667
5322
  */
4668
- var SendActivityError = class extends Error {
4669
- /**
4670
- * The inbox URL that the activity was being sent to.
4671
- */
4672
- inbox;
4673
- /**
4674
- * The HTTP status code returned by the inbox.
4675
- */
4676
- statusCode;
4677
- /**
4678
- * The response body from the inbox, if any. Note that this may be
4679
- * truncated to a maximum of 1 KiB to prevent excessive memory consumption
4680
- * when remote servers return large error pages (e.g., Cloudflare error pages).
4681
- * If truncated, the string will end with `"… (truncated)"`.
4682
- */
4683
- responseBody;
4684
- /**
4685
- * The response headers from the inbox.
4686
- * @since 2.3.0
4687
- */
4688
- responseHeaders;
4689
- /**
4690
- * Creates a new {@link SendActivityError}.
4691
- * @param inbox The inbox URL.
4692
- * @param statusCode The HTTP status code.
4693
- * @param message The error message.
4694
- * @param responseBody The response body.
4695
- * @param responseHeaders The response headers.
4696
- */
4697
- constructor(inbox, statusCode, message, responseBody, responseHeaders) {
4698
- super(message);
4699
- this.name = "SendActivityError";
4700
- this.inbox = inbox;
4701
- this.statusCode = statusCode;
4702
- this.responseBody = responseBody;
4703
- this.responseHeaders = new Headers(responseHeaders);
5323
+ function isPortableActivity(activity) {
5324
+ return hasPortableActor(activity) || activity.id != null && isPortableId(activity.id);
5325
+ }
5326
+ /**
5327
+ * Verifies that a portable activity's own ID belongs to the DID that signed
5328
+ * it, on the JSON-LD document received. The ID the proof policy checks has
5329
+ * to be the parsed activity's ID, so that a remote context that changes
5330
+ * between loads cannot make the two name different objects.
5331
+ * @returns `null` if the ID is verified, or the reason why it is not.
5332
+ */
5333
+ async function verifyPortableActivityId(activityId, jsonLd, options) {
5334
+ const expectedId = getCanonicalPortableId(activityId);
5335
+ if (expectedId == null) return "malformed portable ID";
5336
+ let verification;
5337
+ try {
5338
+ verification = await verifyPortableObjectProofWithRoot(jsonLd, options);
5339
+ } catch (error) {
5340
+ if (error instanceof InvalidPortableObjectIdError) return "malformed portable ID";
5341
+ if (!isPermanentActivityParseError(error)) throw error;
5342
+ return "malformed document";
4704
5343
  }
4705
- };
5344
+ const { result, root } = verification;
5345
+ if (!result.verified) return result.reason.type;
5346
+ const rootId = root?.["@id"];
5347
+ if (typeof rootId !== "string" || getCanonicalPortableId(parseIri(rootId)) !== expectedId) return "the verified document has another ID";
5348
+ return null;
5349
+ }
5350
+ //#endregion
5351
+ //#region src/nodeinfo/handler.ts
5352
+ /**
5353
+ * Handles a NodeInfo request. You would not typically call this function
5354
+ * directly, but instead use {@link Federation.handle} method.
5355
+ * @param request The NodeInfo request to handle.
5356
+ * @param parameters The parameters for handling the request.
5357
+ * @returns The response to the request.
5358
+ */
5359
+ async function handleNodeInfo(_request, { context, nodeInfoDispatcher }) {
5360
+ const promise = nodeInfoDispatcher(context);
5361
+ const json = nodeInfoToJson(promise instanceof Promise ? await promise : promise);
5362
+ return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\"" } });
5363
+ }
5364
+ /**
5365
+ * Handles a request to `/.well-known/nodeinfo`. You would not typically call
5366
+ * this function directly, but instead use {@link Federation.handle} method.
5367
+ * @param request The request to handle.
5368
+ * @param context The request context.
5369
+ * @returns The response to the request.
5370
+ */
5371
+ function handleNodeInfoJrd(_request, context) {
5372
+ const links = [];
5373
+ try {
5374
+ links.push({
5375
+ rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
5376
+ href: context.getNodeInfoUri().href,
5377
+ type: "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\""
5378
+ });
5379
+ } catch (e) {
5380
+ if (!(e instanceof RouterError)) throw e;
5381
+ }
5382
+ const response = new Response(JSON.stringify({ links }), { headers: { "Content-Type": "application/jrd+json" } });
5383
+ return Promise.resolve(response);
5384
+ }
4706
5385
  //#endregion
4707
5386
  //#region src/federation/bench.ts
4708
5387
  /**
@@ -4799,438 +5478,151 @@ async function handleBenchmarkTrigger(request, context, options = {}) {
4799
5478
  }, 403);
4800
5479
  await context.sendActivity(sender, recipients, activity);
4801
5480
  return jsonResponse({
4802
- version: 1,
4803
- activityId,
4804
- queueCorrelationId: activityId,
4805
- recipientCount: recipients.length,
4806
- inboxCount: inboxUrls.length
4807
- }, 202);
4808
- } catch (error) {
4809
- if (error instanceof BenchmarkTriggerError) return jsonResponse({ error: error.message }, error.status);
4810
- throw error;
4811
- }
4812
- }
4813
- var BenchmarkTriggerError = class extends Error {
4814
- status;
4815
- constructor(message, status = 400) {
4816
- super(message);
4817
- this.status = status;
4818
- }
4819
- };
4820
- function parseSender(value) {
4821
- const sender = asRecord(value, "sender");
4822
- if (typeof sender.identifier === "string") return { identifier: sender.identifier };
4823
- if (typeof sender.username === "string") return { username: sender.username };
4824
- throw new BenchmarkTriggerError("sender must be { identifier } or { username }.");
4825
- }
4826
- async function parseRecipients(value, context) {
4827
- if (!Array.isArray(value)) throw new BenchmarkTriggerError("recipients must be an array.");
4828
- return await Promise.all(value.map(async (item) => {
4829
- let object;
4830
- try {
4831
- object = await Object$1.fromJsonLd(item, {
4832
- documentLoader: context.documentLoader,
4833
- contextLoader: context.contextLoader
4834
- });
4835
- } catch (error) {
4836
- throw new BenchmarkTriggerError(`Invalid ActivityPub recipient: ${error}`);
4837
- }
4838
- if (!isRecipient(object)) throw new BenchmarkTriggerError("each recipient must be an ActivityPub actor.");
4839
- const recipient = object;
4840
- if (recipient.id == null || recipient.inboxId == null) throw new BenchmarkTriggerError("each recipient must have id and inbox properties.");
4841
- return recipient;
4842
- }));
4843
- }
4844
- function isRecipient(value) {
4845
- return value != null && typeof value === "object" && "id" in value && "inboxId" in value;
4846
- }
4847
- async function parseActivity(value, context) {
4848
- try {
4849
- return await Activity.fromJsonLd(value, {
4850
- documentLoader: context.documentLoader,
4851
- contextLoader: context.contextLoader
4852
- });
4853
- } catch (error) {
4854
- throw new BenchmarkTriggerError(`Invalid ActivityPub activity: ${error}`);
4855
- }
4856
- }
4857
- function asRecord(value, name) {
4858
- if (value == null || typeof value !== "object" || Array.isArray(value)) throw new BenchmarkTriggerError(`${name} must be an object.`);
4859
- return value;
4860
- }
4861
- function jsonResponse(body, status = 200) {
4862
- return new Response(JSON.stringify(body), {
4863
- status,
4864
- headers: { "Content-Type": "application/json" }
4865
- });
4866
- }
4867
- function serializeScopeMetrics(resourceMetrics) {
4868
- return resourceMetrics.scopeMetrics.map(serializeScope);
4869
- }
4870
- function serializeScope(scopeMetrics) {
4871
- return {
4872
- scope: {
4873
- name: scopeMetrics.scope.name,
4874
- version: scopeMetrics.scope.version
4875
- },
4876
- metrics: scopeMetrics.metrics.map(serializeMetric)
4877
- };
4878
- }
4879
- function serializeMetric(metric) {
4880
- return {
4881
- name: metric.descriptor.name,
4882
- description: metric.descriptor.description,
4883
- unit: metric.descriptor.unit,
4884
- dataPointType: serializeDataPointType(metric.dataPointType),
4885
- dataPoints: metric.dataPoints.map((point) => ({
4886
- attributes: { ...point.attributes },
4887
- startTime: point.startTime,
4888
- endTime: point.endTime,
4889
- value: point.value
4890
- }))
4891
- };
4892
- }
4893
- function serializeDataPointType(dataPointType) {
4894
- switch (dataPointType) {
4895
- case DataPointType.HISTOGRAM: return "histogram";
4896
- case DataPointType.EXPONENTIAL_HISTOGRAM: return "exponential_histogram";
4897
- case DataPointType.GAUGE: return "gauge";
4898
- case DataPointType.SUM: return "sum";
4899
- }
4900
- }
4901
- //#endregion
4902
- //#region src/federation/portable-inbox.ts
4903
- const MAX_TIMEOUT_MS = 2 ** 31 - 1;
4904
- /**
4905
- * Fills in the defaults of {@link PortableInboxForwardingOptions} and
4906
- * validates them.
4907
- * @param options The options.
4908
- * @returns The resolved options.
4909
- * @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
4910
- * is not positive, if `deadline` is negative or longer
4911
- * than 2,147,483,647 milliseconds (about 24.8 days), or
4912
- * if either duration has calendar units, i.e., weeks,
4913
- * months, or years, whose length depends on the date.
4914
- */
4915
- function resolvePortableInboxForwardingOptions(options = {}) {
4916
- const maxTargets = options.maxTargets ?? 10;
4917
- if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
4918
- const ttl = Temporal.Duration.from(options.ttl ?? { days: 30 });
4919
- if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
4920
- const deadline = Temporal.Duration.from(options.deadline ?? { seconds: 10 });
4921
- const deadlineMs = deadline.total("millisecond");
4922
- if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
4923
- return {
4924
- maxTargets,
4925
- ttl,
4926
- deadline
4927
- };
4928
- }
4929
- /**
4930
- * Decides whether this server accepts a delivery to a portable inbox on behalf
4931
- * of the given actor, which the actor dispatcher returned for the identifier
4932
- * in the requested inbox path.
4933
- *
4934
- * The actor document comes from the application, so it is trusted as is and
4935
- * its proof is not verified here.
4936
- * @param actor The actor the actor dispatcher returned.
4937
- * @param options The requested DID, the canonical form of the requested inbox
4938
- * ID, and the origin of this server.
4939
- * @returns The resolution.
4940
- */
4941
- function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
4942
- if (actor == null) return {
4943
- status: "rejected",
4944
- reason: "notFound"
4945
- };
4946
- const id = actor.id;
4947
- if (id == null || id.protocol !== "ap:" && id.protocol !== "ap+ef61:") return {
4948
- status: "rejected",
4949
- reason: "notPortable"
4950
- };
4951
- if (!isSameAuthority(id, authority)) return {
4952
- status: "rejected",
4953
- reason: "authorityMismatch"
4954
- };
4955
- const inboxId = actor.inboxId;
4956
- if (inboxId == null || canonicalize(inboxId) !== canonicalInboxId) return {
4957
- status: "rejected",
4958
- reason: "inboxMismatch"
4959
- };
4960
- const gateways = actor.gateways;
4961
- if (!gateways.some((g) => isGatewayUrl(g) && g.origin === localOrigin)) return {
4962
- status: "rejected",
4963
- reason: "notGateway"
4964
- };
4965
- return {
4966
- status: "accepted",
4967
- recipient: {
4968
- actorId: id,
4969
- inboxId,
4970
- canonicalInboxId,
4971
- gateways
4972
- }
4973
- };
4974
- }
4975
- function isSameAuthority(id, authority) {
4976
- try {
4977
- return getFe34Origin(id) === authority;
4978
- } catch (error) {
4979
- if (error instanceof TypeError) return false;
4980
- throw error;
4981
- }
4982
- }
4983
- function canonicalize(iri) {
4984
- try {
4985
- return canonicalizePortableUri(typeof iri === "string" ? iri : formatIri(iri));
4986
- } catch (error) {
4987
- if (error instanceof TypeError) return null;
4988
- throw error;
4989
- }
4990
- }
4991
- /**
4992
- * Forwards an activity received in a portable inbox to the inboxes of the same
4993
- * portable actor on its other gateways, as FEP-ef61 recommends.
4994
- *
4995
- * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
4996
- * each target gateway is claimed with an atomic compare-and-swap before the
4997
- * activity is handed off, and a claim is never released, even if the hand-off
4998
- * fails, since a failed hand-off does not prove that nothing was delivered.
4999
- * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
5000
- * forwarded.
5001
- *
5002
- * Forwarded requests are signed with HTTP Signatures by the RSA key among
5003
- * {@link ForwardPortableInboxActivityParameters.getKeys}, if any, so that
5004
- * servers requiring HTTP Signatures accept them, and are sent unsigned
5005
- * otherwise. Either way, the receiving gateways authenticate the activity by
5006
- * its own proof.
5007
- * @param parameters The parameters.
5008
- * @returns The target inbox URLs that the activity was handed off to.
5009
- */
5010
- async function forwardPortableInboxActivity(parameters) {
5011
- const logger = getLogger([
5012
- "fedify",
5013
- "federation",
5014
- "inbox"
5015
- ]);
5016
- const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
5017
- const { maxTargets, ttl } = parameters.options ?? resolvePortableInboxForwardingOptions();
5018
- if (kv.cas == null || maxTargets < 1) return [];
5019
- const canonicalActivityId = canonicalize(activityId) ?? activityId.href;
5020
- const targets = getForwardingTargets(recipient, excludedOrigins);
5021
- if (targets.length > maxTargets) {
5022
- 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.", {
5023
- inbox: recipient.canonicalInboxId,
5024
- activityId: activityId.href,
5025
- max: maxTargets
5026
- });
5027
- targets.length = maxTargets;
5481
+ version: 1,
5482
+ activityId,
5483
+ queueCorrelationId: activityId,
5484
+ recipientCount: recipients.length,
5485
+ inboxCount: inboxUrls.length
5486
+ }, 202);
5487
+ } catch (error) {
5488
+ if (error instanceof BenchmarkTriggerError) return jsonResponse({ error: error.message }, error.status);
5489
+ throw error;
5028
5490
  }
5029
- const claimed = [];
5030
- for (const { gateway, inbox } of targets) {
5031
- const key = [
5032
- ...kvPrefix,
5033
- recipient.canonicalInboxId,
5034
- canonicalActivityId,
5035
- gateway
5036
- ];
5037
- if (await kv.cas(key, void 0, true, { ttl })) claimed.push(inbox);
5491
+ }
5492
+ var BenchmarkTriggerError = class extends Error {
5493
+ status;
5494
+ constructor(message, status = 400) {
5495
+ super(message);
5496
+ this.status = status;
5038
5497
  }
5039
- if (claimed.length < 1) return [];
5040
- logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
5041
- activityId: activityId.href,
5042
- inbox: recipient.canonicalInboxId,
5043
- targets: claimed.map((t) => t.href)
5044
- });
5045
- if (outboxQueue == null) await forwardImmediately(parameters, claimed);
5046
- else await enqueueForwarding(parameters, outboxQueue, claimed);
5047
- return claimed;
5498
+ };
5499
+ function parseSender(value) {
5500
+ const sender = asRecord(value, "sender");
5501
+ if (typeof sender.identifier === "string") return { identifier: sender.identifier };
5502
+ if (typeof sender.username === "string") return { username: sender.username };
5503
+ throw new BenchmarkTriggerError("sender must be { identifier } or { username }.");
5048
5504
  }
5049
- function getForwardingTargets(recipient, excludedOrigins) {
5050
- const logger = getLogger([
5051
- "fedify",
5052
- "federation",
5053
- "inbox"
5054
- ]);
5055
- const seen = new Set(excludedOrigins);
5056
- const targets = [];
5057
- for (const gateway of recipient.gateways) {
5058
- if (!isGatewayUrl(gateway) || seen.has(gateway.origin)) continue;
5059
- seen.add(gateway.origin);
5060
- let inbox;
5505
+ async function parseRecipients(value, context) {
5506
+ if (!Array.isArray(value)) throw new BenchmarkTriggerError("recipients must be an array.");
5507
+ return await Promise.all(value.map(async (item) => {
5508
+ let object;
5061
5509
  try {
5062
- inbox = toCompatibleEf61Id(recipient.inboxId, gateway.origin);
5063
- } catch (error) {
5064
- if (!(error instanceof TypeError)) throw error;
5065
- logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
5066
- gateway: gateway.origin,
5067
- inbox: recipient.canonicalInboxId
5510
+ object = await Object$1.fromJsonLd(item, {
5511
+ documentLoader: context.documentLoader,
5512
+ contextLoader: context.contextLoader
5068
5513
  });
5069
- continue;
5514
+ } catch (error) {
5515
+ throw new BenchmarkTriggerError(`Invalid ActivityPub recipient: ${error}`);
5070
5516
  }
5071
- targets.push({
5072
- gateway: gateway.origin,
5073
- inbox
5074
- });
5075
- }
5076
- return targets;
5517
+ if (!isRecipient(object)) throw new BenchmarkTriggerError("each recipient must be an ActivityPub actor.");
5518
+ const recipient = object;
5519
+ if (recipient.id == null || recipient.inboxId == null) throw new BenchmarkTriggerError("each recipient must have id and inbox properties.");
5520
+ return recipient;
5521
+ }));
5077
5522
  }
5078
- /**
5079
- * Resolves the key that signs forwarded requests: the first RSASSA-PKCS1-v1_5
5080
- * key, as {@link sendActivity} signs requests only with such a key. Since
5081
- * the targets have already been claimed, the activity is forwarded unsigned
5082
- * rather than not at all if the key cannot be resolved or used.
5083
- */
5084
- async function resolveForwardingKey({ getKeys, activityId }) {
5085
- if (getKeys == null) return null;
5086
- const logger = getLogger([
5087
- "fedify",
5088
- "federation",
5089
- "inbox"
5090
- ]);
5091
- let key;
5523
+ function isRecipient(value) {
5524
+ return value != null && typeof value === "object" && "id" in value && "inboxId" in value;
5525
+ }
5526
+ async function parseActivity(value, context) {
5092
5527
  try {
5093
- key = (await getKeys()).find((k) => k.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5");
5094
- } catch (error) {
5095
- logger.error("Failed to get the gateway keys to sign forwarded requests with; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
5096
- activityId: activityId.href,
5097
- error
5528
+ return await Activity.fromJsonLd(value, {
5529
+ documentLoader: context.documentLoader,
5530
+ contextLoader: context.contextLoader
5098
5531
  });
5099
- return null;
5100
- }
5101
- if (key == null) return null;
5102
- try {
5103
- validateCryptoKey(key.privateKey, "private");
5104
5532
  } catch (error) {
5105
- if (!(error instanceof TypeError)) throw error;
5106
- logger.warn("The gateway key {keyId} cannot sign forwarded requests; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
5107
- keyId: key.keyId.href,
5108
- activityId: activityId.href,
5109
- error
5110
- });
5111
- return null;
5533
+ throw new BenchmarkTriggerError(`Invalid ActivityPub activity: ${error}`);
5112
5534
  }
5113
- return key;
5114
5535
  }
5115
- async function forwardImmediately(parameters, inboxes) {
5116
- const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, options, meterProvider, tracerProvider } = parameters;
5117
- const logger = getLogger([
5118
- "fedify",
5119
- "federation",
5120
- "inbox"
5121
- ]);
5122
- const deadline = (options ?? resolvePortableInboxForwardingOptions()).deadline.total("millisecond");
5123
- const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
5124
- activity,
5125
- activityId: activityId.href,
5126
- activityType,
5127
- keys: key == null ? [] : [key],
5128
- inbox,
5129
- allowPrivateAddress,
5130
- specDeterminer,
5131
- meterProvider,
5132
- tracerProvider
5133
- }).catch((error) => {
5134
- logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5135
- activityId: activityId.href,
5136
- inbox: inbox.href,
5137
- error
5138
- });
5139
- }))));
5140
- let timer;
5141
- const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
5142
- timer = setTimeout(() => resolve(true), deadline);
5143
- })]);
5144
- clearTimeout(timer);
5145
- if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
5146
- activityId: activityId.href,
5147
- deadline
5536
+ function asRecord(value, name) {
5537
+ if (value == null || typeof value !== "object" || Array.isArray(value)) throw new BenchmarkTriggerError(`${name} must be an object.`);
5538
+ return value;
5539
+ }
5540
+ function jsonResponse(body, status = 200) {
5541
+ return new Response(JSON.stringify(body), {
5542
+ status,
5543
+ headers: { "Content-Type": "application/json" }
5148
5544
  });
5149
5545
  }
5150
- async function enqueueForwarding(parameters, outboxQueue, inboxes) {
5151
- const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
5152
- const logger = getLogger([
5153
- "fedify",
5154
- "federation",
5155
- "inbox"
5156
- ]);
5157
- const key = await resolveForwardingKey(parameters);
5158
- const keys = [];
5159
- if (key != null) try {
5160
- keys.push({
5161
- keyId: key.keyId.href,
5162
- privateKey: await exportJwk(key.privateKey)
5163
- });
5164
- } catch (error) {
5165
- logger.error("Failed to export the gateway key {keyId}; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
5166
- keyId: key.keyId.href,
5167
- activityId: activityId.href,
5168
- error
5169
- });
5546
+ function serializeScopeMetrics(resourceMetrics) {
5547
+ return resourceMetrics.scopeMetrics.map(serializeScope);
5548
+ }
5549
+ function serializeScope(scopeMetrics) {
5550
+ return {
5551
+ scope: {
5552
+ name: scopeMetrics.scope.name,
5553
+ version: scopeMetrics.scope.version
5554
+ },
5555
+ metrics: scopeMetrics.metrics.map(serializeMetric)
5556
+ };
5557
+ }
5558
+ function serializeMetric(metric) {
5559
+ return {
5560
+ name: metric.descriptor.name,
5561
+ description: metric.descriptor.description,
5562
+ unit: metric.descriptor.unit,
5563
+ dataPointType: serializeDataPointType(metric.dataPointType),
5564
+ dataPoints: metric.dataPoints.map((point) => ({
5565
+ attributes: { ...point.attributes },
5566
+ startTime: point.startTime,
5567
+ endTime: point.endTime,
5568
+ value: point.value
5569
+ }))
5570
+ };
5571
+ }
5572
+ function serializeDataPointType(dataPointType) {
5573
+ switch (dataPointType) {
5574
+ case DataPointType.HISTOGRAM: return "histogram";
5575
+ case DataPointType.EXPONENTIAL_HISTOGRAM: return "exponential_histogram";
5576
+ case DataPointType.GAUGE: return "gauge";
5577
+ case DataPointType.SUM: return "sum";
5170
5578
  }
5171
- startQueue?.();
5172
- const started = (/* @__PURE__ */ new Date()).toISOString();
5173
- const traceContext = {};
5174
- propagation.inject(context.active(), traceContext);
5175
- await Promise.all(inboxes.map(async (inbox) => {
5176
- const message = {
5177
- type: "outbox",
5178
- id: crypto.randomUUID(),
5179
- baseUrl,
5180
- keys,
5181
- activity,
5182
- activityId: activityId.href,
5183
- activityType,
5184
- inbox: inbox.href,
5185
- sharedInbox: false,
5186
- started,
5187
- attempt: 0,
5188
- headers: {},
5189
- traceContext
5190
- };
5191
- try {
5192
- await outboxQueue.enqueue(message);
5193
- } catch (error) {
5194
- logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5195
- activityId: activityId.href,
5196
- inbox: inbox.href,
5197
- error
5198
- });
5199
- return;
5200
- }
5201
- recordOutboxEnqueue(meterProvider, outboxQueue, message);
5202
- }));
5203
5579
  }
5204
5580
  //#endregion
5205
5581
  //#region src/federation/outgoing-proof.ts
5206
- function isPortableUrl(url) {
5207
- return url.protocol === "ap:" || url.protocol === "ap+ef61:";
5208
- }
5209
5582
  function formatKeyIds(candidates) {
5210
5583
  return candidates.map((c) => c.verificationMethod.href).join(", ");
5211
5584
  }
5212
5585
  /**
5213
5586
  * Rejects an activity of a portable actor whose ID cannot be authenticated by
5214
5587
  * the actor's DID: FEP-ef61 requires the activity to be a portable object
5215
- * signed by that DID, so its ID has to be a portable ID with the same DID.
5588
+ * signed by that DID, so its ID has to be a portable ID, i.e., an `ap:` or
5589
+ * `ap+ef61:` URI or a compatible identifier, with the same DID. A portable
5590
+ * actor is one whose ID is such an ID as well.
5216
5591
  * @throws {TypeError} If the activity is performed by a portable actor, but
5217
- * its ID is not a portable ID of the actor's DID, or its
5218
- * portable actors do not share a DID.
5592
+ * its ID is not a portable ID of the actor's DID, its
5593
+ * portable actors do not share a DID, or the ID of one of
5594
+ * them is a malformed compatible identifier.
5219
5595
  * @internal
5220
5596
  */
5221
5597
  function assertPortableActorActivity(activity) {
5222
- const portableActors = activity.actorIds.filter(isPortableUrl);
5598
+ const portableActors = activity.actorIds.filter((id) => isPortableId(id));
5223
5599
  if (portableActors.length < 1) return;
5224
- const did = getFe34Origin(portableActors[0]);
5225
- if (portableActors.some((actorId) => 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.`);
5226
- if (activity.id == null || !isPortableUrl(activity.id) || 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.`);
5600
+ const dids = portableActors.map((actorId) => {
5601
+ const did = getPortableDid(actorId);
5602
+ 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.`);
5603
+ return did;
5604
+ });
5605
+ const did = dids[0];
5606
+ 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.`);
5607
+ if (activity.id == null || !isPortableId(activity.id) || 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.`);
5608
+ if (!isCompatibleEf61Iri(activity.id)) return;
5609
+ const mismatched = portableActors.find((actorId) => isCompatibleEf61Iri(actorId) && actorId.origin !== activity.id?.origin);
5610
+ if (mismatched == null) return;
5611
+ getLogger([
5612
+ "fedify",
5613
+ "federation",
5614
+ "outbox"
5615
+ ]).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.", {
5616
+ activityId: activity.id.href,
5617
+ actorId: mismatched.href
5618
+ });
5227
5619
  }
5228
5620
  /**
5229
5621
  * Creates the Object Integrity Proofs Fedify attaches to an outgoing activity.
5230
5622
  *
5231
5623
  * Keys whose IDs are FEP-ef61 compatible identifiers are gateway keys, which
5232
5624
  * never sign proofs. A portable activity, i.e., one with an `ap:` or
5233
- * `ap+ef61:` ID, that carries no proof is signed only by the one Ed25519 key
5625
+ * `ap+ef61:` ID or a compatible identifier, that carries no proof is signed only by the one Ed25519 key
5234
5626
  * whose verification method is a DID URL for its DID, even if it is the only
5235
5627
  * key; if no key or more than one key qualifies, this function throws.
5236
5628
  *
@@ -5269,7 +5661,7 @@ async function signOutgoingActivity(activity, candidates, options) {
5269
5661
  break;
5270
5662
  }
5271
5663
  const keys = candidates.filter((c) => c.privateKey.algorithm.name === "Ed25519" && !isCompatibleKeyId(c.verificationMethod));
5272
- if (!hasProof && activity.id != null && isPortableUrl(activity.id)) return await sign(selectPortableActivityKey(activity, activity.id, keys));
5664
+ if (!hasProof && activity.id != null && isPortableId(activity.id)) return await sign(selectPortableActivityKey(activity, activity.id, keys));
5273
5665
  if (keys.length < 1 || hasProof && !options.appendToExistingProofs) return {
5274
5666
  activity,
5275
5667
  hasProof,
@@ -5302,8 +5694,9 @@ async function signOutgoingActivity(activity, candidates, options) {
5302
5694
  * @throws {TypeError} If no key or more than one key qualifies.
5303
5695
  */
5304
5696
  function selectPortableActivityKey(activity, activityId, keys) {
5305
- const activityDid = getFe34Origin(activityId);
5306
- const eligible = keys.filter((c) => c.verificationMethod.protocol === "did:" && haveSameFe34Origin(activityId, c.verificationMethod));
5697
+ const activityDid = getPortableDid(activityId);
5698
+ 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.`);
5699
+ const eligible = keys.filter((c) => c.verificationMethod.protocol === "did:" && getPortableDid(c.verificationMethod) === activityDid);
5307
5700
  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.`);
5308
5701
  getLogger([
5309
5702
  "fedify",
@@ -5721,6 +6114,27 @@ function getWebFingerSubjectAndAliases({ resourceUrl, actorUri, preferredUsernam
5721
6114
  };
5722
6115
  }
5723
6116
  /**
6117
+ * Gets the compatible identifier that a WebFinger response links to for
6118
+ * an FEP-ef61 portable actor. An actor identified by an `ap:` or `ap+ef61:`
6119
+ * URI is linked through its compatible identifier on the given gateway, while
6120
+ * an actor identified by a compatible identifier is linked through its ID as
6121
+ * is, so that software without FEP-ef61 support finds the same ID in the actor
6122
+ * document.
6123
+ * @param actorId The portable actor's ID.
6124
+ * @param gateway The actor's first gateway.
6125
+ * @returns The compatible identifier, or `null` if the actor ID is malformed
6126
+ * or cannot be represented as a compatible identifier.
6127
+ */
6128
+ function getCompatibleActorId(actorId, gateway) {
6129
+ try {
6130
+ if (isPortableUri(actorId)) return toCompatibleEf61Id(actorId, gateway);
6131
+ return fromCompatibleEf61Id(actorId) == null ? null : actorId;
6132
+ } catch (error) {
6133
+ if (error instanceof TypeError) return null;
6134
+ throw error;
6135
+ }
6136
+ }
6137
+ /**
5724
6138
  * Computes the subject and aliases for an FEP-ef61 portable actor, whose
5725
6139
  * WebFinger address takes its domain from the first gateway of the actor
5726
6140
  * rather than from the actor ID. The canonical `ap:` ID is not emitted,
@@ -5884,21 +6298,23 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
5884
6298
  });
5885
6299
  let actorUri = context.getActorUri(identifier);
5886
6300
  let portable;
5887
- if (actor.id != null && (actor.id.protocol === "ap:" || actor.id.protocol === "ap+ef61:")) {
6301
+ if (actor.id != null && isPortableId(actor.id)) {
5888
6302
  const gateway = actor.gateway;
5889
- let compatibleId;
5890
- if (gateway != null && isGatewayUrl(gateway)) try {
5891
- compatibleId = toCompatibleEf61Id(actor.id, gateway);
5892
- } catch (error) {
5893
- if (!(error instanceof TypeError)) throw error;
5894
- }
5895
- if (gateway == null || compatibleId == null) {
6303
+ if (gateway == null || !isGatewayUrl(gateway)) {
5896
6304
  logger.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.", {
5897
6305
  actorId: formatIri(actor.id),
5898
6306
  identifier
5899
6307
  });
5900
6308
  return await onNotFound(request);
5901
6309
  }
6310
+ const compatibleId = getCompatibleActorId(actor.id, gateway);
6311
+ if (compatibleId == null) {
6312
+ logger.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.", {
6313
+ actorId: formatIri(actor.id),
6314
+ identifier
6315
+ });
6316
+ return await onNotFound(request);
6317
+ }
5902
6318
  actorUri = compatibleId;
5903
6319
  portable = {
5904
6320
  compatibleId,
@@ -7836,7 +8252,7 @@ var FederationImpl = class extends FederationBuilderImpl {
7836
8252
  onNotFound,
7837
8253
  portableInbox: {
7838
8254
  recipient,
7839
- forward: async (activity, activityId, activityType) => {
8255
+ forward: async (activity, activityId, activityType, signatures) => {
7840
8256
  if (this.portableInboxForwarding.maxTargets < 1) return;
7841
8257
  if (this.kv.cas == null) {
7842
8258
  if (!this.#portableInboxForwardingWarned) {
@@ -7859,6 +8275,7 @@ var FederationImpl = class extends FederationBuilderImpl {
7859
8275
  allowPrivateAddress: this.allowPrivateAddress,
7860
8276
  getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
7861
8277
  specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
8278
+ signatures,
7862
8279
  options: this.portableInboxForwarding,
7863
8280
  meterProvider: this.meterProvider,
7864
8281
  tracerProvider: this.tracerProvider
@@ -9141,4 +9558,4 @@ function getRequestId(request) {
9141
9558
  return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
9142
9559
  }
9143
9560
  //#endregion
9144
- export { respondWithObject as a, digest as c, parseCircuitBreakerKvState as d, createFederationBuilder as f, SendActivityError as i, CircuitBreaker as l, handleWebFinger as n, respondWithObjectIfAcceptable as o, createExponentialBackoffPolicy as r, buildCollectionSynchronizationHeader as s, createFederation as t, normalizeCircuitBreakerOptions as u };
9561
+ export { respondWithObjectIfAcceptable as a, digest as c, parseCircuitBreakerKvState as d, createFederationBuilder as f, respondWithObject as i, CircuitBreaker as l, handleWebFinger as n, SendActivityError as o, createExponentialBackoffPolicy as r, buildCollectionSynchronizationHeader as s, createFederation as t, normalizeCircuitBreakerOptions as u };