@fedify/fedify 2.4.0-dev.2071 → 2.4.0-dev.2077

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 (83) hide show
  1. package/dist/{builder-DVQ0yhKJ.mjs → builder-i3jxVe9m.mjs} +2 -2
  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-KmXDfWFp.mjs → compound-proof-Cayy-ECj.mjs} +1 -1
  6. package/dist/{context-DsB9kF6q.d.ts → context-BSywYspi.d.ts} +109 -1
  7. package/dist/{context-DijrgHDe.d.cts → context-CZjZYXFC.d.cts} +109 -1
  8. package/dist/{context-DG5QeQ8W.mjs → context-DlZhxdFT.mjs} +2 -1
  9. package/dist/{deno-B-NtYZVW.mjs → deno-Djkzd-G_.mjs} +1 -1
  10. package/dist/{docloader-C6yOVoaF.mjs → docloader-dA0RswUV.mjs} +2 -2
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/compound-fanout.test.mjs +3 -3
  13. package/dist/federation/handler-compound-proof.test.mjs +138 -11
  14. package/dist/federation/handler.test.mjs +6 -6
  15. package/dist/federation/hashlink-media.test.mjs +2 -2
  16. package/dist/federation/idempotency.test.mjs +2 -2
  17. package/dist/federation/metrics.test.mjs +1 -1
  18. package/dist/federation/middleware.test.mjs +8 -8
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +3 -3
  21. package/dist/federation/mod.d.ts +3 -3
  22. package/dist/federation/mod.js +1 -1
  23. package/dist/federation/outgoing-proof.test.mjs +5 -5
  24. package/dist/federation/portable-gateway-keys.test.mjs +4 -4
  25. package/dist/federation/portable-inbox.test.d.mts +2 -0
  26. package/dist/federation/portable-inbox.test.mjs +546 -0
  27. package/dist/federation/portable.test.mjs +3 -3
  28. package/dist/federation/send.test.mjs +3 -3
  29. package/dist/federation/tasks/enqueue.test.mjs +2 -2
  30. package/dist/federation/tasks/tasks.test.mjs +3 -3
  31. package/dist/federation/temporal.test.mjs +1 -1
  32. package/dist/federation/webfinger.test.mjs +2 -2
  33. package/dist/{federation-CUiHc7dP.cjs → federation-CFd0Jhhv.cjs} +463 -82
  34. package/dist/{http-DKWi8RjI.mjs → http-CCJ97Jql.mjs} +4 -4
  35. package/dist/{http-CuLWIJdf.cjs → http-Dvy_IwRa.cjs} +1 -1
  36. package/dist/{http-BoWBwC0g.js → http-aqh3yFdM.js} +1 -1
  37. package/dist/{key-lfcx_NaE.mjs → key-IxTTC8Yf.mjs} +2 -2
  38. package/dist/{kv-cache-DE8kSNQU.cjs → kv-cache-BP69NV0P.cjs} +1 -1
  39. package/dist/{kv-cache-d8EjO29d.mjs → kv-cache-Bwwbh-xR.mjs} +1 -1
  40. package/dist/{kv-cache-BdnmnAVc.js → kv-cache-Y6SDRSX6.js} +1 -1
  41. package/dist/{ld-jyjdrWjG.mjs → ld-l0B9nREb.mjs} +3 -3
  42. package/dist/{metrics-B5Rei-HC.mjs → metrics-D1p6hv4z.mjs} +1 -1
  43. package/dist/{middleware-CdVnCDXD.mjs → middleware-BDjCUHMS.mjs} +1 -1
  44. package/dist/{middleware-CPx4ZWwt.mjs → middleware-BMgzqBtr.mjs} +418 -42
  45. package/dist/{middleware-CAObohey.js → middleware-SSfryVzY.js} +412 -32
  46. package/dist/{mod-DMkflQdF.d.ts → mod-B2h2KY0q.d.ts} +1 -1
  47. package/dist/{mod-DTAaBwiY.d.cts → mod-JzkaEXsi.d.cts} +1 -1
  48. package/dist/mod.cjs +5 -5
  49. package/dist/mod.d.cts +4 -4
  50. package/dist/mod.d.ts +4 -4
  51. package/dist/mod.js +5 -5
  52. package/dist/nodeinfo/handler.test.mjs +2 -2
  53. package/dist/{owner-BXufwAzm.cjs → owner-BpOxL5PG.cjs} +1 -1
  54. package/dist/{owner-ByqNQVaa.mjs → owner-DGbYmXqU.mjs} +3 -3
  55. package/dist/{owner-1TFakcTF.js → owner-jH1z_YfD.js} +1 -1
  56. package/dist/{portable-key-Cw_ow5vn.mjs → portable-key-CCwJGSsU.mjs} +2 -2
  57. package/dist/{proof-B6gwdhN5.mjs → proof-CVL7JnZd.mjs} +4 -4
  58. package/dist/{send-DsK7xevO.mjs → send-Ejy1jOz9.mjs} +8 -4
  59. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  60. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  61. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  62. package/dist/sig/compound-proof.test.mjs +2 -2
  63. package/dist/sig/http.test.mjs +2 -2
  64. package/dist/sig/key.test.mjs +1 -1
  65. package/dist/sig/ld.test.mjs +2 -2
  66. package/dist/sig/mod.cjs +3 -3
  67. package/dist/sig/mod.js +3 -3
  68. package/dist/sig/owner.test.mjs +1 -1
  69. package/dist/sig/portable-collection.test.mjs +2 -2
  70. package/dist/sig/portable-key.test.mjs +3 -3
  71. package/dist/sig/proof.test.mjs +3 -3
  72. package/dist/{sig-BR_Z6mpY.js → sig-Dpy1B1Ad.js} +2 -2
  73. package/dist/{sig-TTn9wYrE.cjs → sig-xoqy3ATq.cjs} +2 -2
  74. package/dist/{temporal-Bw5jtrBY.mjs → temporal-DNJ1cNpr.mjs} +1 -1
  75. package/dist/testing/kv.test.mjs +1 -1
  76. package/dist/testing/mod.d.mts +108 -0
  77. package/dist/testing/mod.mjs +2 -2
  78. package/dist/{testing-By1YYsIK.mjs → testing-CO_cEvQc.mjs} +1 -1
  79. package/dist/utils/docloader.test.mjs +2 -2
  80. package/dist/utils/kv-cache.test.mjs +1 -1
  81. package/dist/utils/mod.cjs +1 -1
  82. package/dist/utils/mod.js +1 -1
  83. package/package.json +7 -7
@@ -2,10 +2,10 @@ 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, 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-BoWBwC0g.js";
5
+ import { $ as recordCollectionPageItems, 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-aqh3yFdM.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-1TFakcTF.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-BdnmnAVc.js";
7
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-jH1z_YfD.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-Y6SDRSX6.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";
@@ -3242,7 +3242,7 @@ async function handleInbox(request, options) {
3242
3242
  * @returns A promise that resolves to an HTTP response.
3243
3243
  */
3244
3244
  async function handleInboxInternal(request, parameters, span) {
3245
- const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider } = parameters;
3245
+ const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider, portableInbox } = parameters;
3246
3246
  const logger = getLogger([
3247
3247
  "fedify",
3248
3248
  "federation",
@@ -3255,7 +3255,7 @@ async function handleInboxInternal(request, parameters, span) {
3255
3255
  message: "Actor dispatcher is not set."
3256
3256
  });
3257
3257
  return await onNotFound(request);
3258
- } else if (recipient != null) {
3258
+ } else if (recipient != null && portableInbox == null) {
3259
3259
  const actor = await actorDispatcher(ctx, recipient);
3260
3260
  if (actor == null || actor instanceof Tombstone) {
3261
3261
  logger.error("Actor {recipient} not found.", { recipient });
@@ -3666,8 +3666,9 @@ async function handleInboxInternal(request, parameters, span) {
3666
3666
  });
3667
3667
  }
3668
3668
  if (!skipSignatureVerification) {
3669
- if (inspectCompoundPortableObjectApplicability(json, INBOX_COMPOUND_PROOF_LIMITS) !== "absent") {
3670
- const compoundProof = await verifyCompoundPortableObjectProofs(json, INBOX_COMPOUND_PROOF_LIMITS, {
3669
+ const compoundJson = Array.isArray(json) ? json : jsonWithoutSig;
3670
+ if (inspectCompoundPortableObjectApplicability(compoundJson, INBOX_COMPOUND_PROOF_LIMITS) !== "absent") {
3671
+ const compoundProof = await verifyCompoundPortableObjectProofs(compoundJson, INBOX_COMPOUND_PROOF_LIMITS, {
3671
3672
  documentLoader: ctx.documentLoader,
3672
3673
  keyCache,
3673
3674
  meterProvider,
@@ -3717,6 +3718,19 @@ async function handleInboxInternal(request, parameters, span) {
3717
3718
  tracerProvider,
3718
3719
  idempotencyStrategy: parameters.idempotencyStrategy
3719
3720
  });
3721
+ if (portableInbox != null && activity.id != null && PORTABLE_INBOX_FORWARDABLE_RESULTS.has(routeResult)) if (skipSignatureVerification || !(ldSigVerified || proofVerified)) logger.debug("Not forwarding activity {activityId} to the other gateways of the portable inbox {inbox}, as it is not authenticated by its own proof.", {
3722
+ activityId: activity.id.href,
3723
+ inbox: portableInbox.recipient.canonicalInboxId
3724
+ });
3725
+ else try {
3726
+ await portableInbox.forward(json, activity.id, getTypeId(activity).href);
3727
+ } catch (error) {
3728
+ logger.error("Failed to forward activity {activityId} to the other gateways of the portable inbox {inbox}:\n{error}", {
3729
+ activityId: activity.id.href,
3730
+ inbox: portableInbox.recipient.canonicalInboxId,
3731
+ error
3732
+ });
3733
+ }
3720
3734
  if (routeResult === "alreadyProcessed") return new Response(`Activity <${activity.id}> has already been processed.`, {
3721
3735
  status: 202,
3722
3736
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -3742,6 +3756,12 @@ async function handleInboxInternal(request, parameters, span) {
3742
3756
  headers: { "Content-Type": "text/plain; charset=utf-8" }
3743
3757
  });
3744
3758
  }
3759
+ const PORTABLE_INBOX_FORWARDABLE_RESULTS = /* @__PURE__ */ new Set([
3760
+ "success",
3761
+ "enqueued",
3762
+ "alreadyProcessed",
3763
+ "unsupportedActivity"
3764
+ ]);
3745
3765
  /**
3746
3766
  * Handles a custom collection request.
3747
3767
  * @template TItem The type of items in the collection.
@@ -4533,7 +4553,11 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
4533
4553
  rsaKey = key;
4534
4554
  break;
4535
4555
  }
4536
- 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.", {
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.", {
4537
4561
  inbox: inbox.href,
4538
4562
  keys: keys.map((pair) => ({
4539
4563
  keyId: pair.keyId.href,
@@ -4875,6 +4899,251 @@ function serializeDataPointType(dataPointType) {
4875
4899
  }
4876
4900
  }
4877
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
+ inboxId,
4969
+ canonicalInboxId,
4970
+ gateways
4971
+ }
4972
+ };
4973
+ }
4974
+ function isSameAuthority(id, authority) {
4975
+ try {
4976
+ return getFe34Origin(id) === authority;
4977
+ } catch (error) {
4978
+ if (error instanceof TypeError) return false;
4979
+ throw error;
4980
+ }
4981
+ }
4982
+ function canonicalize(iri) {
4983
+ try {
4984
+ return canonicalizePortableUri(typeof iri === "string" ? iri : formatIri(iri));
4985
+ } catch (error) {
4986
+ if (error instanceof TypeError) return null;
4987
+ throw error;
4988
+ }
4989
+ }
4990
+ /**
4991
+ * Forwards an activity received in a portable inbox to the inboxes of the same
4992
+ * portable actor on its other gateways, as FEP-ef61 recommends.
4993
+ *
4994
+ * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
4995
+ * each target gateway is claimed with an atomic compare-and-swap before the
4996
+ * activity is handed off, and a claim is never released, even if the hand-off
4997
+ * fails, since a failed hand-off does not prove that nothing was delivered.
4998
+ * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
4999
+ * forwarded.
5000
+ *
5001
+ * Forwarded requests are not signed with HTTP Signatures; the receiving
5002
+ * gateways authenticate the activity by its own proof.
5003
+ * @param parameters The parameters.
5004
+ * @returns The target inbox URLs that the activity was handed off to.
5005
+ */
5006
+ async function forwardPortableInboxActivity(parameters) {
5007
+ const logger = getLogger([
5008
+ "fedify",
5009
+ "federation",
5010
+ "inbox"
5011
+ ]);
5012
+ const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
5013
+ const { maxTargets, ttl } = parameters.options ?? resolvePortableInboxForwardingOptions();
5014
+ if (kv.cas == null || maxTargets < 1) return [];
5015
+ const canonicalActivityId = canonicalize(activityId) ?? activityId.href;
5016
+ const targets = getForwardingTargets(recipient, excludedOrigins);
5017
+ if (targets.length > maxTargets) {
5018
+ 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.", {
5019
+ inbox: recipient.canonicalInboxId,
5020
+ activityId: activityId.href,
5021
+ max: maxTargets
5022
+ });
5023
+ targets.length = maxTargets;
5024
+ }
5025
+ const claimed = [];
5026
+ for (const { gateway, inbox } of targets) {
5027
+ const key = [
5028
+ ...kvPrefix,
5029
+ recipient.canonicalInboxId,
5030
+ canonicalActivityId,
5031
+ gateway
5032
+ ];
5033
+ if (await kv.cas(key, void 0, true, { ttl })) claimed.push(inbox);
5034
+ }
5035
+ if (claimed.length < 1) return [];
5036
+ logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
5037
+ activityId: activityId.href,
5038
+ inbox: recipient.canonicalInboxId,
5039
+ targets: claimed.map((t) => t.href)
5040
+ });
5041
+ if (outboxQueue == null) await forwardImmediately(parameters, claimed);
5042
+ else await enqueueForwarding(parameters, outboxQueue, claimed);
5043
+ return claimed;
5044
+ }
5045
+ function getForwardingTargets(recipient, excludedOrigins) {
5046
+ const logger = getLogger([
5047
+ "fedify",
5048
+ "federation",
5049
+ "inbox"
5050
+ ]);
5051
+ const seen = new Set(excludedOrigins);
5052
+ const targets = [];
5053
+ for (const gateway of recipient.gateways) {
5054
+ if (!isGatewayUrl(gateway) || seen.has(gateway.origin)) continue;
5055
+ seen.add(gateway.origin);
5056
+ let inbox;
5057
+ try {
5058
+ inbox = toCompatibleEf61Id(recipient.inboxId, gateway.origin);
5059
+ } catch (error) {
5060
+ if (!(error instanceof TypeError)) throw error;
5061
+ logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
5062
+ gateway: gateway.origin,
5063
+ inbox: recipient.canonicalInboxId
5064
+ });
5065
+ continue;
5066
+ }
5067
+ targets.push({
5068
+ gateway: gateway.origin,
5069
+ inbox
5070
+ });
5071
+ }
5072
+ return targets;
5073
+ }
5074
+ async function forwardImmediately({ activity, activityId, activityType, allowPrivateAddress, options, meterProvider, tracerProvider }, inboxes) {
5075
+ const logger = getLogger([
5076
+ "fedify",
5077
+ "federation",
5078
+ "inbox"
5079
+ ]);
5080
+ const deadline = (options ?? resolvePortableInboxForwardingOptions()).deadline.total("millisecond");
5081
+ const sends = inboxes.map((inbox) => sendActivity({
5082
+ activity,
5083
+ activityId: activityId.href,
5084
+ activityType,
5085
+ keys: [],
5086
+ inbox,
5087
+ allowPrivateAddress,
5088
+ meterProvider,
5089
+ tracerProvider
5090
+ }).catch((error) => {
5091
+ logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5092
+ activityId: activityId.href,
5093
+ inbox: inbox.href,
5094
+ error
5095
+ });
5096
+ }));
5097
+ let timer;
5098
+ const timedOut = await Promise.race([Promise.all(sends).then(() => false), new Promise((resolve) => {
5099
+ timer = setTimeout(() => resolve(true), deadline);
5100
+ })]);
5101
+ clearTimeout(timer);
5102
+ if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
5103
+ activityId: activityId.href,
5104
+ deadline
5105
+ });
5106
+ }
5107
+ async function enqueueForwarding({ activity, activityId, activityType, baseUrl, startQueue, meterProvider }, outboxQueue, inboxes) {
5108
+ const logger = getLogger([
5109
+ "fedify",
5110
+ "federation",
5111
+ "inbox"
5112
+ ]);
5113
+ startQueue?.();
5114
+ const started = (/* @__PURE__ */ new Date()).toISOString();
5115
+ const traceContext = {};
5116
+ propagation.inject(context.active(), traceContext);
5117
+ await Promise.all(inboxes.map(async (inbox) => {
5118
+ const message = {
5119
+ type: "outbox",
5120
+ id: crypto.randomUUID(),
5121
+ baseUrl,
5122
+ keys: [],
5123
+ activity,
5124
+ activityId: activityId.href,
5125
+ activityType,
5126
+ inbox: inbox.href,
5127
+ sharedInbox: false,
5128
+ started,
5129
+ attempt: 0,
5130
+ headers: {},
5131
+ traceContext
5132
+ };
5133
+ try {
5134
+ await outboxQueue.enqueue(message);
5135
+ } catch (error) {
5136
+ logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5137
+ activityId: activityId.href,
5138
+ inbox: inbox.href,
5139
+ error
5140
+ });
5141
+ return;
5142
+ }
5143
+ recordOutboxEnqueue(meterProvider, outboxQueue, message);
5144
+ }));
5145
+ }
5146
+ //#endregion
4878
5147
  //#region src/federation/outgoing-proof.ts
4879
5148
  function isPortableUrl(url) {
4880
5149
  return url.protocol === "ap:" || url.protocol === "ap+ef61:";
@@ -5774,6 +6043,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5774
6043
  kvPrefixes;
5775
6044
  publicKeyTtl;
5776
6045
  httpMessageSignaturesSpecTtl;
6046
+ portableInboxForwarding;
5777
6047
  inboxQueue;
5778
6048
  outboxQueue;
5779
6049
  fanoutQueue;
@@ -5806,6 +6076,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5806
6076
  benchmarkMetricReader;
5807
6077
  benchmarkTriggerOptions;
5808
6078
  #mediaUploaderNoAuthWarned = false;
6079
+ #portableInboxForwardingWarned = false;
5809
6080
  #queueDepthGaugeSourceId = `fedify-${(++nextQueueDepthGaugeSourceId).toString(36)}`;
5810
6081
  #queueDepthGaugeEntries = [];
5811
6082
  #queueDepthGaugeMeterProvider;
@@ -5837,10 +6108,12 @@ var FederationImpl = class extends FederationBuilderImpl {
5837
6108
  acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
5838
6109
  circuitBreaker: ["_fedify", "circuit"],
5839
6110
  taskDeduplication: ["_fedify", "taskDeduplication"],
6111
+ portableInboxForwarding: ["_fedify", "portableInboxForwarding"],
5840
6112
  ...options.kvPrefixes ?? {}
5841
6113
  };
5842
6114
  this.publicKeyTtl = Temporal.Duration.from(options.publicKeyTtl ?? { days: 30 });
5843
6115
  this.httpMessageSignaturesSpecTtl = Temporal.Duration.from(options.httpMessageSignaturesSpecTtl ?? { days: 90 });
6116
+ this.portableInboxForwarding = resolvePortableInboxForwardingOptions(options.portableInboxForwarding);
5844
6117
  if (options.queue == null) {
5845
6118
  this.inboxQueue = void 0;
5846
6119
  this.outboxQueue = void 0;
@@ -6510,8 +6783,8 @@ var FederationImpl = class extends FederationBuilderImpl {
6510
6783
  ]);
6511
6784
  const baseUrl = new URL(message.baseUrl);
6512
6785
  let context = this.#createContext(baseUrl, ctxData);
6513
- if (message.identifier != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
6514
- else if (this.sharedInboxKeyDispatcher != null) {
6786
+ if (message.identifier != null && this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
6787
+ else if (message.identifier == null && this.sharedInboxKeyDispatcher != null) {
6515
6788
  const identity = await this.sharedInboxKeyDispatcher(context);
6516
6789
  if (identity != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
6517
6790
  }
@@ -7104,6 +7377,15 @@ var FederationImpl = class extends FederationBuilderImpl {
7104
7377
  span,
7105
7378
  metricState
7106
7379
  });
7380
+ if (request.method === "POST") {
7381
+ const portable = parsePortableGatewayRequest(url);
7382
+ if (portable != null) return await this.#fetchPortableInbox(request, portable, {
7383
+ onNotFound,
7384
+ contextData,
7385
+ span,
7386
+ metricState
7387
+ });
7388
+ }
7107
7389
  const portable = request.method === "GET" || request.method === "HEAD" ? parsePortableGatewayRequest(url) : null;
7108
7390
  if (portable != null) return await discardBodyForHead(request, await this.#fetchPortableObject(request, portable, {
7109
7391
  onNotFound,
@@ -7255,35 +7537,17 @@ var FederationImpl = class extends FederationBuilderImpl {
7255
7537
  onNotFound
7256
7538
  });
7257
7539
  context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
7258
- case "sharedInbox": {
7540
+ case "sharedInbox":
7259
7541
  if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
7260
7542
  const identity = await this.sharedInboxKeyDispatcher(context);
7261
7543
  if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
7262
7544
  }
7263
- if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
7264
- const inboxContextFactory = context.toInboxContext.bind(context);
7265
- inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
7266
- return await handleInbox(request, {
7545
+ return await this.#handleInbox(request, {
7267
7546
  recipient: route.values.identifier ?? null,
7268
7547
  context,
7269
- inboxContextFactory,
7270
- kv: this.kv,
7271
- kvPrefixes: this.kvPrefixes,
7272
- publicKeyTtl: this.publicKeyTtl,
7273
- queue: this.inboxQueue,
7274
- actorDispatcher: this.actorCallbacks?.dispatcher,
7275
- inboxListeners: this.inboxListeners,
7276
- inboxErrorHandler: this.inboxErrorHandler,
7277
- unverifiedActivityHandler: this.unverifiedActivityHandler,
7278
- onNotFound,
7279
- signatureTimeWindow: this.signatureTimeWindow,
7280
- skipSignatureVerification: this.skipSignatureVerification,
7281
- inboxChallengePolicy: this.inboxChallengePolicy,
7282
- meterProvider: this.meterProvider,
7283
- tracerProvider: this.tracerProvider,
7284
- idempotencyStrategy: this.idempotencyStrategy
7548
+ contextData,
7549
+ onNotFound
7285
7550
  });
7286
- }
7287
7551
  case "following": return await handleCollection(request, {
7288
7552
  name: "following",
7289
7553
  identifier: route.values.identifier,
@@ -7436,6 +7700,113 @@ var FederationImpl = class extends FederationBuilderImpl {
7436
7700
  onNotFound
7437
7701
  });
7438
7702
  }
7703
+ async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox }) {
7704
+ if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
7705
+ const inboxContextFactory = context.toInboxContext.bind(context);
7706
+ inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
7707
+ return await handleInbox(request, {
7708
+ recipient,
7709
+ context,
7710
+ inboxContextFactory,
7711
+ kv: this.kv,
7712
+ kvPrefixes: this.kvPrefixes,
7713
+ publicKeyTtl: this.publicKeyTtl,
7714
+ queue: this.inboxQueue,
7715
+ actorDispatcher: this.actorCallbacks?.dispatcher,
7716
+ inboxListeners: this.inboxListeners,
7717
+ inboxErrorHandler: this.inboxErrorHandler,
7718
+ unverifiedActivityHandler: this.unverifiedActivityHandler,
7719
+ onNotFound,
7720
+ signatureTimeWindow: this.signatureTimeWindow,
7721
+ skipSignatureVerification: this.skipSignatureVerification,
7722
+ inboxChallengePolicy: this.inboxChallengePolicy,
7723
+ meterProvider: this.meterProvider,
7724
+ tracerProvider: this.tracerProvider,
7725
+ idempotencyStrategy: this.idempotencyStrategy,
7726
+ portableInbox
7727
+ });
7728
+ }
7729
+ async #fetchPortableInbox(request, portable, { onNotFound, contextData, span, metricState }) {
7730
+ const logger = getLogger([
7731
+ "fedify",
7732
+ "federation",
7733
+ "inbox"
7734
+ ]);
7735
+ if (portable.type === "malformed") {
7736
+ metricState.endpoint = "not_found";
7737
+ logger.debug("Malformed FEP-ef61 gateway inbox request {url}: {error}", {
7738
+ url: request.url,
7739
+ error: portable.error
7740
+ });
7741
+ return new Response("Malformed portable inbox ID.", {
7742
+ status: 400,
7743
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
7744
+ });
7745
+ }
7746
+ const route = this.router.route(portable.path);
7747
+ if (route == null || route.name !== "inbox") {
7748
+ metricState.endpoint = "not_found";
7749
+ return await onNotFound(request);
7750
+ }
7751
+ metricState.routeTemplate = PORTABLE_GATEWAY_ROUTE_PREFIX + route.template;
7752
+ metricState.endpoint = "inbox";
7753
+ span.updateName(`${request.method} ${metricState.routeTemplate}`);
7754
+ const identifier = route.values.identifier;
7755
+ let context = this.#createContext(request, contextData);
7756
+ const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
7757
+ const localOrigin = context.canonicalOrigin;
7758
+ const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof Tombstone ? null : actor, {
7759
+ authority: portable.portableRequest.authority,
7760
+ canonicalInboxId: portable.canonicalId,
7761
+ localOrigin
7762
+ });
7763
+ if (resolution.status === "rejected") {
7764
+ logger.debug("Not accepting a delivery to the portable inbox {inbox} on behalf of the actor {identifier}: {reason}.", {
7765
+ inbox: portable.canonicalId,
7766
+ identifier,
7767
+ reason: resolution.reason
7768
+ });
7769
+ return await onNotFound(request);
7770
+ }
7771
+ const { recipient } = resolution;
7772
+ if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
7773
+ const excludedOrigins = [localOrigin, new URL(request.url).origin];
7774
+ return await this.#handleInbox(request, {
7775
+ recipient: identifier,
7776
+ context,
7777
+ contextData,
7778
+ onNotFound,
7779
+ portableInbox: {
7780
+ recipient,
7781
+ forward: async (activity, activityId, activityType) => {
7782
+ if (this.portableInboxForwarding.maxTargets < 1) return;
7783
+ if (this.kv.cas == null) {
7784
+ if (!this.#portableInboxForwardingWarned) {
7785
+ this.#portableInboxForwardingWarned = true;
7786
+ logger.warn("Activities delivered to FEP-ef61 portable inboxes are not forwarded to the other gateways, as the key–value store does not support compare-and-swap (KvStore.cas()), which is needed to forward each activity at most once.");
7787
+ }
7788
+ return;
7789
+ }
7790
+ await forwardPortableInboxActivity({
7791
+ recipient,
7792
+ activity,
7793
+ activityId,
7794
+ activityType,
7795
+ excludedOrigins,
7796
+ baseUrl: context.origin,
7797
+ kv: this.kv,
7798
+ kvPrefix: this.kvPrefixes.portableInboxForwarding,
7799
+ outboxQueue: this.outboxQueue,
7800
+ startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
7801
+ allowPrivateAddress: this.allowPrivateAddress,
7802
+ options: this.portableInboxForwarding,
7803
+ meterProvider: this.meterProvider,
7804
+ tracerProvider: this.tracerProvider
7805
+ });
7806
+ }
7807
+ }
7808
+ });
7809
+ }
7439
7810
  async #fetchHashlinkMedia(request, media, { onNotFound, contextData, span, metricState }) {
7440
7811
  metricState.endpoint = "hashlink_media";
7441
7812
  metricState.routeTemplate = HASHLINK_MEDIA_ROUTE_TEMPLATE;
@@ -7666,6 +8037,12 @@ var ContextImpl = class ContextImpl {
7666
8037
  if (path == null) throw new RouterError("No inbox path registered.");
7667
8038
  return new URL(path, this.canonicalOrigin);
7668
8039
  }
8040
+ getPortableInboxUri(identifier, authority) {
8041
+ const path = this.federation.router.build("inbox", { identifier });
8042
+ if (path == null) throw new RouterError("No inbox path registered.");
8043
+ if (authority == null) throw new TypeError("The authority of a portable ID is required outside an FEP-ef61 gateway request.");
8044
+ return buildPortableUri(authority, path);
8045
+ }
7669
8046
  getFollowingUri(identifier) {
7670
8047
  const path = this.federation.router.build("following", { identifier });
7671
8048
  if (path == null) throw new RouterError("No following collection path registered.");
@@ -8268,6 +8645,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
8268
8645
  getPortableObjectUri(cls, values, authority) {
8269
8646
  return super.getPortableObjectUri(cls, values, authority ?? this.portableRequest?.authority);
8270
8647
  }
8648
+ getPortableInboxUri(identifier, authority) {
8649
+ return super.getPortableInboxUri(identifier, authority ?? this.portableRequest?.authority);
8650
+ }
8271
8651
  async getActor(identifier, options) {
8272
8652
  if (this.federation.actorCallbacks == null || this.federation.actorCallbacks.dispatcher == null) throw new Error("No actor dispatcher registered.");
8273
8653
  if (this.#invokedFromActorDispatcher != null) getLogger([
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { Ut as WebFingerLinksDispatcher, _t as ActorDispatcher, gt as ActorAliasMapper, u as RequestContext, vt as ActorHandleMapper } from "./context-DsB9kF6q.js";
2
+ import { Wt as WebFingerLinksDispatcher, _t as ActorAliasMapper, u as RequestContext, vt as ActorDispatcher, yt as ActorHandleMapper } from "./context-BSywYspi.js";
3
3
  import { RouterError, RouterOptions, RouterRouteResult } from "@fedify/uri-template";
4
4
  import { MeterProvider, Span, Tracer } from "@opentelemetry/api";
5
5
 
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { Ut as WebFingerLinksDispatcher, _t as ActorDispatcher, gt as ActorAliasMapper, u as RequestContext, vt as ActorHandleMapper } from "./context-DijrgHDe.cjs";
2
+ import { Wt as WebFingerLinksDispatcher, _t as ActorAliasMapper, u as RequestContext, vt as ActorDispatcher, yt as ActorHandleMapper } from "./context-CZjZYXFC.cjs";
3
3
  import { MeterProvider, Span, Tracer } from "@opentelemetry/api";
4
4
  import { RouterError, RouterOptions, RouterRouteResult } from "@fedify/uri-template";
5
5
 
package/dist/mod.cjs CHANGED
@@ -3,14 +3,14 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  require("./rolldown-runtime-Dmm3t92W.cjs");
4
4
  const require_transformers = require("./transformers-BalCrSsr.cjs");
5
5
  require("./compat/mod.cjs");
6
- const require_http = require("./http-CuLWIJdf.cjs");
7
- const require_federation = require("./federation-CUiHc7dP.cjs");
6
+ const require_http = require("./http-Dvy_IwRa.cjs");
7
+ const require_federation = require("./federation-CFd0Jhhv.cjs");
8
8
  const require_types = require("./types-Be2zbyeG.cjs");
9
- const require_owner = require("./owner-BXufwAzm.cjs");
10
- const require_kv_cache = require("./kv-cache-DE8kSNQU.cjs");
9
+ const require_owner = require("./owner-BpOxL5PG.cjs");
10
+ const require_kv_cache = require("./kv-cache-BP69NV0P.cjs");
11
11
  require("./nodeinfo/mod.cjs");
12
12
  require("./runtime/mod.cjs");
13
- const require_sig = require("./sig-TTn9wYrE.cjs");
13
+ const require_sig = require("./sig-xoqy3ATq.cjs");
14
14
  require("./utils/mod.cjs");
15
15
  //#endregion
16
16
  exports.CircuitBreaker = require_federation.CircuitBreaker;