@fedify/fedify 2.2.0-pr.695.16 → 2.2.0-pr.697.18

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 (63) hide show
  1. package/dist/activity-listener-Ck3JZ_hR.mjs +40 -0
  2. package/dist/{builder-7PVCiLiR.mjs → builder-CssIxEgK.mjs} +57 -7
  3. package/dist/compat/mod.d.cts +1 -1
  4. package/dist/compat/mod.d.ts +1 -1
  5. package/dist/compat/transformers.test.mjs +1 -1
  6. package/dist/{context-78ecvxf5.d.ts → context-BGrYMSTk.d.ts} +143 -1
  7. package/dist/{context-DYDPdoCb.d.cts → context-CMUd4wy0.d.cts} +143 -1
  8. package/dist/{context-Juj6bdHC.mjs → context-Dk_tacqz.mjs} +17 -2
  9. package/dist/{deno-vxcWcxQS.mjs → deno-DFC3hdDk.mjs} +1 -1
  10. package/dist/{docloader-D7q0-Xef.mjs → docloader-DJSGzW4N.mjs} +2 -2
  11. package/dist/federation/builder.test.mjs +25 -1
  12. package/dist/federation/handler.test.mjs +369 -8
  13. package/dist/federation/idempotency.test.mjs +2 -2
  14. package/dist/federation/inbox.test.mjs +3 -3
  15. package/dist/federation/middleware.test.mjs +510 -8
  16. package/dist/federation/mod.cjs +1 -1
  17. package/dist/federation/mod.d.cts +3 -3
  18. package/dist/federation/mod.d.ts +3 -3
  19. package/dist/federation/mod.js +1 -1
  20. package/dist/federation/send.test.mjs +3 -3
  21. package/dist/federation/webfinger.test.mjs +2 -2
  22. package/dist/{http-RZPxDWq5.mjs → http-BrF-JQov.mjs} +2 -2
  23. package/dist/{http-JxF7bG0o.cjs → http-DDRsBRY5.cjs} +1 -1
  24. package/dist/{http-D-MhhYUF.js → http-nVAbZPuI.js} +1 -1
  25. package/dist/{key-CGx_dDkX.mjs → key-2MxqHz-F.mjs} +1 -1
  26. package/dist/{kv-cache-D84Mk0fZ.js → kv-cache-5w7DZnmJ.js} +1 -1
  27. package/dist/{kv-cache-C2gdVgvb.cjs → kv-cache-CPeV5q6I.cjs} +1 -1
  28. package/dist/{ld-wup-liFO.mjs → ld-bY6topKr.mjs} +26 -3
  29. package/dist/{middleware-BjVx-_bv.mjs → middleware-BCJUFXYb.mjs} +612 -180
  30. package/dist/{middleware-Bn75dPug.cjs → middleware-BPHO6DE3.cjs} +676 -323
  31. package/dist/{middleware-RF-sUfTr.js → middleware-C10kVjEo.js} +670 -322
  32. package/dist/{middleware-wdfeWjRJ.mjs → middleware-DI82-dr3.mjs} +1 -1
  33. package/dist/{middleware-CXOVT4Ph.cjs → middleware-JAlnEFGy.cjs} +1 -1
  34. package/dist/{mod-CEohtXhV.d.cts → mod-BcJHeuv1.d.cts} +1 -1
  35. package/dist/{mod-CokIUYDr.d.ts → mod-CJXfyw7v.d.ts} +1 -1
  36. package/dist/{mod-DvxszxXC.d.ts → mod-CR8soWa9.d.ts} +18 -1
  37. package/dist/{mod-DoJBjjnO.d.cts → mod-Cr3f-ACa.d.cts} +18 -1
  38. package/dist/mod.cjs +6 -4
  39. package/dist/mod.d.cts +5 -5
  40. package/dist/mod.d.ts +5 -5
  41. package/dist/mod.js +5 -5
  42. package/dist/nodeinfo/handler.test.mjs +2 -2
  43. package/dist/{owner-q2mUMM9a.mjs → owner-D4-A8f_n.mjs} +2 -2
  44. package/dist/{proof--CpZsF_p.mjs → proof-45_MjnD1.mjs} +32 -3
  45. package/dist/{proof-_Zyfqyce.cjs → proof-COdach6j.cjs} +61 -3
  46. package/dist/{proof-CirP9OSd.js → proof-CnfkRjXL.js} +54 -2
  47. package/dist/{send-CVJfx7bF.mjs → send-BeNvLSaC.mjs} +2 -2
  48. package/dist/sig/http.test.mjs +2 -2
  49. package/dist/sig/key.test.mjs +1 -1
  50. package/dist/sig/ld.test.mjs +44 -2
  51. package/dist/sig/mod.cjs +4 -2
  52. package/dist/sig/mod.d.cts +2 -2
  53. package/dist/sig/mod.d.ts +2 -2
  54. package/dist/sig/mod.js +3 -3
  55. package/dist/sig/owner.test.mjs +1 -1
  56. package/dist/sig/proof.test.mjs +46 -2
  57. package/dist/testing/mod.d.mts +149 -1
  58. package/dist/testing/mod.mjs +2 -2
  59. package/dist/utils/docloader.test.mjs +2 -2
  60. package/dist/utils/mod.cjs +1 -1
  61. package/dist/utils/mod.js +1 -1
  62. package/package.json +5 -5
  63. package/dist/inbox-CmYvcSMM.mjs +0 -179
@@ -2,10 +2,10 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  import { t as __exportAll } from "./chunk-nlSIicah.js";
4
4
  import { r as getDefaultActivityTransformers } from "./transformers-ve6e2xcg.js";
5
- import { _ as version, a as verifyRequestDetailed, d as validateCryptoKey, f as formatAcceptSignature, g as name, i as verifyRequest, n as parseRfc9421SignatureInput, o as exportJwk, t as doubleKnock, u as importJwk } from "./http-D-MhhYUF.js";
6
- import { a as doesActorOwnKey, d as signJsonLd, f as verifyJsonLd, l as detachSignature, n as signObject, o as getKeyOwner, r as verifyObject, u as hasSignature } from "./proof-CirP9OSd.js";
5
+ import { _ as version, a as verifyRequestDetailed, d as validateCryptoKey, f as formatAcceptSignature, g as name, i as verifyRequest, n as parseRfc9421SignatureInput, o as exportJwk, t as doubleKnock, u as importJwk } from "./http-nVAbZPuI.js";
6
+ import { d as hasSignatureLike, f as signJsonLd, i as verifyObject, n as hasProofLike, o as doesActorOwnKey, p as verifyJsonLd, r as signObject, s as getKeyOwner, u as detachSignature } from "./proof-CnfkRjXL.js";
7
7
  import { n as getNodeInfo, t as nodeInfoToJson } from "./types-hvL8ElAs.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-D84Mk0fZ.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-5w7DZnmJ.js";
9
9
  import { getLogger, withContext } from "@logtape/logtape";
10
10
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
11
11
  import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
@@ -17,14 +17,15 @@ import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
17
17
  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";
18
18
  import { lookupWebFinger } from "@fedify/webfinger";
19
19
  import { domainToASCII } from "node:url";
20
- //#region src/federation/inbox.ts
21
- var InboxListenerSet = class InboxListenerSet {
20
+ //#region src/federation/activity-listener.ts
21
+ var ActivityListenerSet = class {
22
22
  #listeners;
23
23
  constructor() {
24
24
  this.#listeners = /* @__PURE__ */ new Map();
25
25
  }
26
26
  clone() {
27
- const clone = new InboxListenerSet();
27
+ const Clone = this.constructor;
28
+ const clone = new Clone();
28
29
  clone.#listeners = new Map(this.#listeners);
29
30
  return clone;
30
31
  }
@@ -34,14 +35,13 @@ var InboxListenerSet = class InboxListenerSet {
34
35
  }
35
36
  dispatchWithClass(activity) {
36
37
  let cls = activity.constructor;
37
- const inboxListeners = this.#listeners;
38
- if (inboxListeners == null) return null;
39
- while (true) {
40
- if (inboxListeners.has(cls)) break;
38
+ while (cls != null) {
39
+ if (this.#listeners.has(cls)) break;
41
40
  if (cls === Activity) return null;
42
41
  cls = globalThis.Object.getPrototypeOf(cls);
43
42
  }
44
- const listener = inboxListeners.get(cls);
43
+ if (cls == null) return null;
44
+ const listener = this.#listeners.get(cls);
45
45
  return {
46
46
  class: cls,
47
47
  listener
@@ -51,142 +51,6 @@ var InboxListenerSet = class InboxListenerSet {
51
51
  return this.dispatchWithClass(activity)?.listener ?? null;
52
52
  }
53
53
  };
54
- async function routeActivity({ context: ctx, json, activity, recipient, inboxListeners, inboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, tracerProvider, idempotencyStrategy }) {
55
- const logger = getLogger([
56
- "fedify",
57
- "federation",
58
- "inbox"
59
- ]);
60
- let cacheKey = null;
61
- if (activity.id != null) {
62
- const inboxContext = inboxContextFactory(recipient, json, activity.id?.href, getTypeId(activity).href);
63
- const strategy = idempotencyStrategy ?? "per-inbox";
64
- let keyString;
65
- if (typeof strategy === "function") keyString = await strategy(inboxContext, activity);
66
- else switch (strategy) {
67
- case "global":
68
- keyString = activity.id.href;
69
- break;
70
- case "per-origin":
71
- keyString = `${ctx.origin}\n${activity.id.href}`;
72
- break;
73
- case "per-inbox":
74
- keyString = `${ctx.origin}\n${activity.id.href}\n${recipient == null ? "sharedInbox" : `inbox\n${recipient}`}`;
75
- break;
76
- default: keyString = `${ctx.origin}\n${activity.id.href}`;
77
- }
78
- if (keyString != null) cacheKey = [...kvPrefixes.activityIdempotence, keyString];
79
- }
80
- if (cacheKey != null) {
81
- if (await kv.get(cacheKey) === true) {
82
- logger.debug("Activity {activityId} has already been processed.", {
83
- activityId: activity.id?.href,
84
- activity: json,
85
- recipient
86
- });
87
- span.setStatus({
88
- code: SpanStatusCode.UNSET,
89
- message: `Activity ${activity.id?.href} has already been processed.`
90
- });
91
- return "alreadyProcessed";
92
- }
93
- }
94
- if (activity.actorId == null) {
95
- logger.error("Missing actor.", { activity: json });
96
- span.setStatus({
97
- code: SpanStatusCode.ERROR,
98
- message: "Missing actor."
99
- });
100
- return "missingActor";
101
- }
102
- span.setAttribute("activitypub.actor.id", activity.actorId.href);
103
- if (queue != null) {
104
- const carrier = {};
105
- propagation.inject(context.active(), carrier);
106
- try {
107
- await queue.enqueue({
108
- type: "inbox",
109
- id: crypto.randomUUID(),
110
- baseUrl: ctx.origin,
111
- activity: json,
112
- identifier: recipient,
113
- attempt: 0,
114
- started: (/* @__PURE__ */ new Date()).toISOString(),
115
- traceContext: carrier
116
- });
117
- } catch (error) {
118
- logger.error("Failed to enqueue the incoming activity {activityId}:\n{error}", {
119
- error,
120
- activityId: activity.id?.href,
121
- activity: json,
122
- recipient
123
- });
124
- span.setStatus({
125
- code: SpanStatusCode.ERROR,
126
- message: `Failed to enqueue the incoming activity ${activity.id?.href}.`
127
- });
128
- throw error;
129
- }
130
- logger.info("Activity {activityId} is enqueued.", {
131
- activityId: activity.id?.href,
132
- activity: json,
133
- recipient
134
- });
135
- return "enqueued";
136
- }
137
- tracerProvider = tracerProvider ?? trace.getTracerProvider();
138
- return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (span) => {
139
- const dispatched = inboxListeners?.dispatchWithClass(activity);
140
- if (dispatched == null) {
141
- logger.error("Unsupported activity type:\n{activity}", {
142
- activity: json,
143
- recipient
144
- });
145
- span.setStatus({
146
- code: SpanStatusCode.UNSET,
147
- message: `Unsupported activity type: ${getTypeId(activity).href}`
148
- });
149
- span.end();
150
- return "unsupportedActivity";
151
- }
152
- const { class: cls, listener } = dispatched;
153
- span.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
154
- try {
155
- await listener(inboxContextFactory(recipient, json, activity?.id?.href, getTypeId(activity).href), activity);
156
- } catch (error) {
157
- try {
158
- await inboxErrorHandler?.(ctx, error);
159
- } catch (error) {
160
- logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
161
- error,
162
- activityId: activity.id?.href,
163
- activity: json,
164
- recipient
165
- });
166
- }
167
- logger.error("Failed to process the incoming activity {activityId}:\n{error}", {
168
- error,
169
- activityId: activity.id?.href,
170
- activity: json,
171
- recipient
172
- });
173
- span.setStatus({
174
- code: SpanStatusCode.ERROR,
175
- message: String(error)
176
- });
177
- span.end();
178
- return "error";
179
- }
180
- if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal.Duration.from({ days: 1 }) });
181
- logger.info("Activity {activityId} has been processed.", {
182
- activityId: activity.id?.href,
183
- activity: json,
184
- recipient
185
- });
186
- span.end();
187
- return "success";
188
- });
189
- }
190
54
  //#endregion
191
55
  //#region src/federation/router.ts
192
56
  function cloneInnerRouter(router) {
@@ -296,6 +160,17 @@ var RouterError = class extends Error {
296
160
  };
297
161
  //#endregion
298
162
  //#region src/federation/builder.ts
163
+ function validateSingleIdentifierVariablePath(path, errorMessage) {
164
+ const operatorMatches = globalThis.Array.from(path.matchAll(/{([+#./;?&]?)([A-Za-z_][A-Za-z0-9_]*)}/g));
165
+ if (operatorMatches.length !== 1 || operatorMatches[0]?.[2] !== "identifier") throw new RouterError(errorMessage);
166
+ if (operatorMatches.some((match) => [
167
+ "?",
168
+ "&",
169
+ "#"
170
+ ].includes(match[1]) && match[2] === "identifier")) throw new RouterError(errorMessage);
171
+ const variables = new Router$1().add(path, "outbox");
172
+ if (variables.size !== 1 || !variables.has("identifier")) throw new RouterError(errorMessage);
173
+ }
299
174
  var FederationBuilderImpl = class {
300
175
  router;
301
176
  actorCallbacks;
@@ -304,6 +179,7 @@ var FederationBuilderImpl = class {
304
179
  objectCallbacks;
305
180
  objectTypeIds;
306
181
  inboxPath;
182
+ outboxPath;
307
183
  inboxCallbacks;
308
184
  outboxCallbacks;
309
185
  followingCallbacks;
@@ -312,7 +188,10 @@ var FederationBuilderImpl = class {
312
188
  featuredCallbacks;
313
189
  featuredTagsCallbacks;
314
190
  inboxListeners;
191
+ outboxListeners;
315
192
  inboxErrorHandler;
193
+ outboxListenerErrorHandler;
194
+ outboxAuthorizePredicate;
316
195
  sharedInboxKeyDispatcher;
317
196
  unverifiedActivityHandler;
318
197
  outboxPermanentFailureHandler;
@@ -343,6 +222,7 @@ var FederationBuilderImpl = class {
343
222
  f.objectCallbacks = { ...this.objectCallbacks };
344
223
  f.objectTypeIds = { ...this.objectTypeIds };
345
224
  f.inboxPath = this.inboxPath;
225
+ f.outboxPath = this.outboxPath;
346
226
  f.inboxCallbacks = this.inboxCallbacks == null ? void 0 : { ...this.inboxCallbacks };
347
227
  f.outboxCallbacks = this.outboxCallbacks == null ? void 0 : { ...this.outboxCallbacks };
348
228
  f.followingCallbacks = this.followingCallbacks == null ? void 0 : { ...this.followingCallbacks };
@@ -351,7 +231,10 @@ var FederationBuilderImpl = class {
351
231
  f.featuredCallbacks = this.featuredCallbacks == null ? void 0 : { ...this.featuredCallbacks };
352
232
  f.featuredTagsCallbacks = this.featuredTagsCallbacks == null ? void 0 : { ...this.featuredTagsCallbacks };
353
233
  f.inboxListeners = this.inboxListeners?.clone();
234
+ f.outboxListeners = this.outboxListeners?.clone();
354
235
  f.inboxErrorHandler = this.inboxErrorHandler;
236
+ f.outboxListenerErrorHandler = this.outboxListenerErrorHandler;
237
+ f.outboxAuthorizePredicate = this.outboxAuthorizePredicate;
355
238
  f.sharedInboxKeyDispatcher = this.sharedInboxKeyDispatcher;
356
239
  f.unverifiedActivityHandler = this.unverifiedActivityHandler;
357
240
  f.outboxPermanentFailureHandler = this.outboxPermanentFailureHandler;
@@ -551,9 +434,14 @@ var FederationBuilderImpl = class {
551
434
  return setters;
552
435
  }
553
436
  setOutboxDispatcher(path, dispatcher) {
554
- if (this.router.has("outbox")) throw new RouterError("Outbox dispatcher already set.");
555
- const variables = this.router.add(path, "outbox");
556
- if (variables.size !== 1 || !variables.has("identifier")) throw new RouterError("Path for outbox dispatcher must have one variable: {identifier}");
437
+ if (this.outboxCallbacks != null) throw new RouterError("Outbox dispatcher already set.");
438
+ if (this.router.has("outbox")) {
439
+ if (this.outboxPath !== path) throw new RouterError("Outbox dispatcher path must match outbox listener path.");
440
+ } else {
441
+ validateSingleIdentifierVariablePath(path, "Path for outbox dispatcher must have one variable: {identifier}");
442
+ this.router.add(path, "outbox");
443
+ this.outboxPath = path;
444
+ }
557
445
  const callbacks = { dispatcher };
558
446
  this.outboxCallbacks = callbacks;
559
447
  const setters = {
@@ -576,6 +464,32 @@ var FederationBuilderImpl = class {
576
464
  };
577
465
  return setters;
578
466
  }
467
+ setOutboxListeners(outboxPath) {
468
+ if (this.outboxListeners != null) throw new RouterError("Outbox listeners already set.");
469
+ if (this.router.has("outbox")) {
470
+ if (this.outboxPath !== outboxPath) throw new RouterError("Outbox listener path must match outbox dispatcher path.");
471
+ } else {
472
+ validateSingleIdentifierVariablePath(outboxPath, "Path for outbox must have one variable: {identifier}");
473
+ this.router.add(outboxPath, "outbox");
474
+ this.outboxPath = outboxPath;
475
+ }
476
+ const listeners = this.outboxListeners = new ActivityListenerSet();
477
+ const setters = {
478
+ on(type, listener) {
479
+ listeners.add(type, listener);
480
+ return setters;
481
+ },
482
+ onError: (handler) => {
483
+ this.outboxListenerErrorHandler = handler;
484
+ return setters;
485
+ },
486
+ authorize: (predicate) => {
487
+ this.outboxAuthorizePredicate = predicate;
488
+ return setters;
489
+ }
490
+ };
491
+ return setters;
492
+ }
579
493
  setFollowingDispatcher(path, dispatcher) {
580
494
  if (this.router.has("following")) throw new RouterError("Following collection dispatcher already set.");
581
495
  const variables = this.router.add(path, "following");
@@ -718,7 +632,7 @@ var FederationBuilderImpl = class {
718
632
  if (sharedInboxPath != null) {
719
633
  if (this.router.add(sharedInboxPath, "sharedInbox").size !== 0) throw new RouterError("Path for shared inbox must have no variables.");
720
634
  }
721
- const listeners = this.inboxListeners = new InboxListenerSet();
635
+ const listeners = this.inboxListeners = new ActivityListenerSet();
722
636
  const setters = {
723
637
  on(type, listener) {
724
638
  listeners.add(type, listener);
@@ -855,6 +769,144 @@ async function buildCollectionSynchronizationHeader(collectionId, actorIds) {
855
769
  return `collectionId="${collectionId}", url="${url}", digest="${encodeHex(await digest(actorIds))}"`;
856
770
  }
857
771
  //#endregion
772
+ //#region src/federation/inbox.ts
773
+ async function routeActivity({ context: ctx, json, activity, recipient, inboxListeners, inboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, tracerProvider, idempotencyStrategy }) {
774
+ const logger = getLogger([
775
+ "fedify",
776
+ "federation",
777
+ "inbox"
778
+ ]);
779
+ let cacheKey = null;
780
+ if (activity.id != null) {
781
+ const inboxContext = inboxContextFactory(recipient, json, activity.id?.href, getTypeId(activity).href);
782
+ const strategy = idempotencyStrategy ?? "per-inbox";
783
+ let keyString;
784
+ if (typeof strategy === "function") keyString = await strategy(inboxContext, activity);
785
+ else switch (strategy) {
786
+ case "global":
787
+ keyString = activity.id.href;
788
+ break;
789
+ case "per-origin":
790
+ keyString = `${ctx.origin}\n${activity.id.href}`;
791
+ break;
792
+ case "per-inbox":
793
+ keyString = `${ctx.origin}\n${activity.id.href}\n${recipient == null ? "sharedInbox" : `inbox\n${recipient}`}`;
794
+ break;
795
+ default: keyString = `${ctx.origin}\n${activity.id.href}`;
796
+ }
797
+ if (keyString != null) cacheKey = [...kvPrefixes.activityIdempotence, keyString];
798
+ }
799
+ if (cacheKey != null) {
800
+ if (await kv.get(cacheKey) === true) {
801
+ logger.debug("Activity {activityId} has already been processed.", {
802
+ activityId: activity.id?.href,
803
+ activity: json,
804
+ recipient
805
+ });
806
+ span.setStatus({
807
+ code: SpanStatusCode.UNSET,
808
+ message: `Activity ${activity.id?.href} has already been processed.`
809
+ });
810
+ return "alreadyProcessed";
811
+ }
812
+ }
813
+ if (activity.actorId == null) {
814
+ logger.error("Missing actor.", { activity: json });
815
+ span.setStatus({
816
+ code: SpanStatusCode.ERROR,
817
+ message: "Missing actor."
818
+ });
819
+ return "missingActor";
820
+ }
821
+ span.setAttribute("activitypub.actor.id", activity.actorId.href);
822
+ if (queue != null) {
823
+ const carrier = {};
824
+ propagation.inject(context.active(), carrier);
825
+ try {
826
+ await queue.enqueue({
827
+ type: "inbox",
828
+ id: crypto.randomUUID(),
829
+ baseUrl: ctx.origin,
830
+ activity: json,
831
+ identifier: recipient,
832
+ attempt: 0,
833
+ started: (/* @__PURE__ */ new Date()).toISOString(),
834
+ traceContext: carrier
835
+ });
836
+ } catch (error) {
837
+ logger.error("Failed to enqueue the incoming activity {activityId}:\n{error}", {
838
+ error,
839
+ activityId: activity.id?.href,
840
+ activity: json,
841
+ recipient
842
+ });
843
+ span.setStatus({
844
+ code: SpanStatusCode.ERROR,
845
+ message: `Failed to enqueue the incoming activity ${activity.id?.href}.`
846
+ });
847
+ throw error;
848
+ }
849
+ logger.info("Activity {activityId} is enqueued.", {
850
+ activityId: activity.id?.href,
851
+ activity: json,
852
+ recipient
853
+ });
854
+ return "enqueued";
855
+ }
856
+ tracerProvider = tracerProvider ?? trace.getTracerProvider();
857
+ return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (span) => {
858
+ const dispatched = inboxListeners?.dispatchWithClass(activity);
859
+ if (dispatched == null) {
860
+ logger.error("Unsupported activity type:\n{activity}", {
861
+ activity: json,
862
+ recipient
863
+ });
864
+ span.setStatus({
865
+ code: SpanStatusCode.UNSET,
866
+ message: `Unsupported activity type: ${getTypeId(activity).href}`
867
+ });
868
+ span.end();
869
+ return "unsupportedActivity";
870
+ }
871
+ const { class: cls, listener } = dispatched;
872
+ span.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
873
+ try {
874
+ await listener(inboxContextFactory(recipient, json, activity?.id?.href, getTypeId(activity).href), activity);
875
+ } catch (error) {
876
+ try {
877
+ await inboxErrorHandler?.(ctx, error);
878
+ } catch (error) {
879
+ logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
880
+ error,
881
+ activityId: activity.id?.href,
882
+ activity: json,
883
+ recipient
884
+ });
885
+ }
886
+ logger.error("Failed to process the incoming activity {activityId}:\n{error}", {
887
+ error,
888
+ activityId: activity.id?.href,
889
+ activity: json,
890
+ recipient
891
+ });
892
+ span.setStatus({
893
+ code: SpanStatusCode.ERROR,
894
+ message: String(error)
895
+ });
896
+ span.end();
897
+ return "error";
898
+ }
899
+ if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal.Duration.from({ days: 1 }) });
900
+ logger.info("Activity {activityId} has been processed.", {
901
+ activityId: activity.id?.href,
902
+ activity: json,
903
+ recipient
904
+ });
905
+ span.end();
906
+ return "success";
907
+ });
908
+ }
909
+ //#endregion
858
910
  //#region src/federation/keycache.ts
859
911
  var KvKeyCache = class {
860
912
  kv;
@@ -1082,8 +1134,8 @@ async function handleObject(request, { values, context, objectDispatcher, author
1082
1134
  * @param parameters The parameters for handling the collection.
1083
1135
  * @returns A promise that resolves to an HTTP response.
1084
1136
  */
1085
- async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, onUnauthorized, onNotFound }) {
1086
- const spanName = name$2.trim().replace(/\s+/g, "_");
1137
+ async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, onUnauthorized, onNotFound }) {
1138
+ const spanName = name$1.trim().replace(/\s+/g, "_");
1087
1139
  tracerProvider = tracerProvider ?? trace.getTracerProvider();
1088
1140
  const tracer = tracerProvider.getTracer(name, version);
1089
1141
  const cursor = new URL(request.url).searchParams.get("cursor");
@@ -1125,7 +1177,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
1125
1177
  collection = new OrderedCollection({
1126
1178
  id: baseUri,
1127
1179
  totalItems: totalItems == null ? null : Number(totalItems),
1128
- items: filterCollectionItems(itemsOrResponse, name$2, filterPredicate)
1180
+ items: filterCollectionItems(itemsOrResponse, name$1, filterPredicate)
1129
1181
  });
1130
1182
  } else {
1131
1183
  const lastCursor = await collectionCallbacks.lastCursor?.(context, identifier);
@@ -1146,7 +1198,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
1146
1198
  } else {
1147
1199
  const uri = new URL(baseUri);
1148
1200
  uri.searchParams.set("cursor", cursor);
1149
- const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
1201
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
1150
1202
  kind: SpanKind.SERVER,
1151
1203
  attributes: {
1152
1204
  "activitypub.collection.id": uri.href,
@@ -1190,7 +1242,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
1190
1242
  id: uri,
1191
1243
  prev,
1192
1244
  next,
1193
- items: filterCollectionItems(items, name$2, filterPredicate),
1245
+ items: filterCollectionItems(items, name$1, filterPredicate),
1194
1246
  partOf
1195
1247
  });
1196
1248
  }
@@ -1230,9 +1282,205 @@ function filterCollectionItems(items, collectionName, filterPredicate) {
1230
1282
  }
1231
1283
  continue;
1232
1284
  }
1233
- result.push(mappedItem);
1285
+ result.push(mappedItem);
1286
+ }
1287
+ return result;
1288
+ }
1289
+ function summarizeJsonActivity(json) {
1290
+ if (json == null || typeof json !== "object") return {};
1291
+ const activity = json;
1292
+ return {
1293
+ activityId: typeof activity.id === "string" ? activity.id : void 0,
1294
+ activityType: typeof activity.type === "string" ? activity.type : void 0
1295
+ };
1296
+ }
1297
+ /**
1298
+ * Handles an outbox POST request.
1299
+ * @template TContextData The context data to pass to the context.
1300
+ * @param request The HTTP request.
1301
+ * @param parameters The parameters for handling the request.
1302
+ * @returns A promise that resolves to an HTTP response.
1303
+ * @since 2.2.0
1304
+ */
1305
+ async function handleOutbox(request, { identifier, context: ctx, outboxContextFactory, actorDispatcher, authorizePredicate, outboxListeners, outboxErrorHandler, onUnauthorized, onNotFound }) {
1306
+ const logger = getLogger([
1307
+ "fedify",
1308
+ "federation",
1309
+ "outbox"
1310
+ ]);
1311
+ if (request.bodyUsed) {
1312
+ logger.error("Request body has already been read.", { identifier });
1313
+ return new Response("Internal server error.", {
1314
+ status: 500,
1315
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1316
+ });
1317
+ } else if (request.body?.locked) {
1318
+ logger.error("Request body is locked.", { identifier });
1319
+ return new Response("Internal server error.", {
1320
+ status: 500,
1321
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1322
+ });
1323
+ }
1324
+ if (actorDispatcher == null) {
1325
+ logger.error("Actor dispatcher is not set.", { identifier });
1326
+ return await onNotFound(request);
1327
+ }
1328
+ if (authorizePredicate != null) {
1329
+ const authorizeContext = ctx.clone(ctx.data);
1330
+ authorizeContext.request = request.clone();
1331
+ const requestForUnauthorized = authorizeContext.request.clone();
1332
+ if (!await authorizePredicate(authorizeContext, identifier)) return await onUnauthorized(requestForUnauthorized);
1333
+ }
1334
+ const actor = await actorDispatcher(ctx, identifier);
1335
+ if (actor == null || actor instanceof Tombstone) {
1336
+ logger.error("Actor {identifier} not found.", { identifier });
1337
+ return await onNotFound(request);
1338
+ }
1339
+ const requestForParsing = request.clone();
1340
+ let json;
1341
+ try {
1342
+ json = await requestForParsing.json();
1343
+ } catch (error) {
1344
+ logger.error("Failed to parse JSON:\n{error}", {
1345
+ identifier,
1346
+ error
1347
+ });
1348
+ const outboxContext = outboxContextFactory(identifier, null, void 0, "");
1349
+ try {
1350
+ await outboxErrorHandler?.(outboxContext, error);
1351
+ } catch (error) {
1352
+ logger.error("An unexpected error occurred in outbox error handler:\n{error}", {
1353
+ error,
1354
+ identifier
1355
+ });
1356
+ }
1357
+ return new Response("Invalid JSON.", {
1358
+ status: 400,
1359
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1360
+ });
1361
+ }
1362
+ let activity;
1363
+ try {
1364
+ activity = await Activity.fromJsonLd(json, ctx);
1365
+ } catch (error) {
1366
+ const summary = summarizeJsonActivity(json);
1367
+ logger.error("Failed to parse activity:\n{error}", {
1368
+ identifier,
1369
+ ...summary,
1370
+ error
1371
+ });
1372
+ const outboxContext = outboxContextFactory(identifier, json, summary.activityId, summary.activityType ?? "");
1373
+ try {
1374
+ await outboxErrorHandler?.(outboxContext, error);
1375
+ } catch (error) {
1376
+ logger.error("An unexpected error occurred in outbox error handler:\n{error}", {
1377
+ error,
1378
+ identifier,
1379
+ ...summary
1380
+ });
1381
+ }
1382
+ return new Response("Invalid activity.", {
1383
+ status: 400,
1384
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1385
+ });
1386
+ }
1387
+ const outboxContext = outboxContextFactory(identifier, json, activity.id?.href, getTypeId(activity).href);
1388
+ const expectedActorId = actor.id ?? ctx.getActorUri(identifier);
1389
+ if (activity.actorIds.length < 1) {
1390
+ const error = /* @__PURE__ */ new Error("The posted activity has no actor.");
1391
+ logger.error("The posted activity has no actor for outbox {identifier}.", {
1392
+ identifier,
1393
+ activityId: activity.id?.href,
1394
+ expectedActorId: expectedActorId.href
1395
+ });
1396
+ try {
1397
+ await outboxErrorHandler?.(outboxContext, error);
1398
+ } catch (error) {
1399
+ logger.error("An unexpected error occurred in outbox error handler:\n{error}", {
1400
+ error,
1401
+ activityId: activity.id?.href,
1402
+ activityType: getTypeId(activity).href,
1403
+ identifier
1404
+ });
1405
+ }
1406
+ return new Response(error.message, {
1407
+ status: 400,
1408
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1409
+ });
1234
1410
  }
1235
- return result;
1411
+ if (!activity.actorIds.every((actorId) => actorId.href === expectedActorId.href)) {
1412
+ const error = /* @__PURE__ */ new Error("The activity actor does not match the outbox owner.");
1413
+ logger.error("The posted activity actor does not match outbox owner {identifier}.", {
1414
+ identifier,
1415
+ activityId: activity.id?.href,
1416
+ expectedActorId: expectedActorId.href,
1417
+ actorIds: activity.actorIds.map((actorId) => actorId.href)
1418
+ });
1419
+ try {
1420
+ await outboxErrorHandler?.(outboxContext, error);
1421
+ } catch (error) {
1422
+ logger.error("An unexpected error occurred in outbox error handler:\n{error}", {
1423
+ error,
1424
+ activityId: activity.id?.href,
1425
+ activityType: getTypeId(activity).href,
1426
+ identifier
1427
+ });
1428
+ }
1429
+ return new Response(error.message, {
1430
+ status: 400,
1431
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1432
+ });
1433
+ }
1434
+ const dispatched = outboxListeners?.dispatchWithClass(activity);
1435
+ if (dispatched == null) {
1436
+ logger.debug("Unsupported activity type {activityType}.", {
1437
+ identifier,
1438
+ activityId: activity.id?.href,
1439
+ activityType: getTypeId(activity).href
1440
+ });
1441
+ return new Response("", {
1442
+ status: 202,
1443
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1444
+ });
1445
+ }
1446
+ try {
1447
+ await dispatched.listener(outboxContext, activity);
1448
+ } catch (error) {
1449
+ try {
1450
+ await outboxErrorHandler?.(outboxContext, error);
1451
+ } catch (error) {
1452
+ logger.error("An unexpected error occurred in outbox error handler:\n{error}", {
1453
+ error,
1454
+ activityId: activity.id?.href,
1455
+ activityType: getTypeId(activity).href,
1456
+ identifier
1457
+ });
1458
+ }
1459
+ logger.error("Failed to process the incoming activity {activityId}:\n{error}", {
1460
+ error,
1461
+ activityId: activity.id?.href,
1462
+ activityType: getTypeId(activity).href,
1463
+ identifier
1464
+ });
1465
+ return new Response("Internal server error.", {
1466
+ status: 500,
1467
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1468
+ });
1469
+ }
1470
+ if (!outboxContext.hasDeliveredActivity()) logger.warn("Outbox listener for {identifier} returned without delivering the posted activity; ctx.sendActivity() or ctx.forwardActivity() may have been skipped or resulted in no delivery.", {
1471
+ identifier,
1472
+ activityId: activity.id?.href,
1473
+ activityType: getTypeId(activity).href
1474
+ });
1475
+ logger.info("Activity {activityId} has been processed in outbox listener.", {
1476
+ activityId: activity.id?.href,
1477
+ activityType: getTypeId(activity).href,
1478
+ identifier
1479
+ });
1480
+ return new Response("", {
1481
+ status: 202,
1482
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1483
+ });
1236
1484
  }
1237
1485
  /**
1238
1486
  * Handles an inbox request for ActivityPub activities.
@@ -1664,8 +1912,8 @@ var CustomCollectionHandler = class {
1664
1912
  * @param CollectionPage The CollectionPage constructor.
1665
1913
  * @param filterPredicate Optional filter predicate for items.
1666
1914
  */
1667
- constructor(name$1, values, context, callbacks, tracerProvider = trace.getTracerProvider(), Collection, CollectionPage, filterPredicate) {
1668
- this.name = name$1;
1915
+ constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), Collection, CollectionPage, filterPredicate) {
1916
+ this.name = name$2;
1669
1917
  this.values = values;
1670
1918
  this.context = context;
1671
1919
  this.callbacks = callbacks;
@@ -2467,6 +2715,7 @@ var middleware_exports = /* @__PURE__ */ __exportAll({
2467
2715
  FederationImpl: () => FederationImpl,
2468
2716
  InboxContextImpl: () => InboxContextImpl,
2469
2717
  KvSpecDeterminer: () => KvSpecDeterminer,
2718
+ OutboxContextImpl: () => OutboxContextImpl,
2470
2719
  createFederation: () => createFederation
2471
2720
  });
2472
2721
  /**
@@ -3281,16 +3530,37 @@ var FederationImpl = class extends FederationBuilderImpl {
3281
3530
  onNotFound
3282
3531
  });
3283
3532
  }
3284
- case "outbox": return await handleCollection(request, {
3285
- name: "outbox",
3286
- identifier: route.values.identifier,
3287
- uriGetter: context.getOutboxUri.bind(context),
3288
- context,
3289
- collectionCallbacks: this.outboxCallbacks,
3290
- tracerProvider: this.tracerProvider,
3291
- onUnauthorized,
3292
- onNotFound
3293
- });
3533
+ case "outbox":
3534
+ if (request.method === "POST") {
3535
+ if (this.outboxListeners == null) return new Response("Method not allowed.", {
3536
+ status: 405,
3537
+ headers: {
3538
+ Allow: "GET, HEAD",
3539
+ "Content-Type": "text/plain; charset=utf-8"
3540
+ }
3541
+ });
3542
+ return await handleOutbox(request, {
3543
+ identifier: route.values.identifier,
3544
+ context,
3545
+ outboxContextFactory: context.toOutboxContext.bind(context),
3546
+ actorDispatcher: this.actorCallbacks?.dispatcher,
3547
+ authorizePredicate: this.outboxAuthorizePredicate ?? this.outboxCallbacks?.authorizePredicate,
3548
+ outboxListeners: this.outboxListeners,
3549
+ outboxErrorHandler: this.outboxListenerErrorHandler,
3550
+ onUnauthorized,
3551
+ onNotFound
3552
+ });
3553
+ }
3554
+ return await handleCollection(request, {
3555
+ name: "outbox",
3556
+ identifier: route.values.identifier,
3557
+ uriGetter: context.getOutboxUri.bind(context),
3558
+ context,
3559
+ collectionCallbacks: this.outboxCallbacks,
3560
+ tracerProvider: this.tracerProvider,
3561
+ onUnauthorized,
3562
+ onNotFound
3563
+ });
3294
3564
  case "inbox":
3295
3565
  if (request.method !== "POST") return await handleCollection(request, {
3296
3566
  name: "inbox",
@@ -3460,6 +3730,16 @@ var ContextImpl = class ContextImpl {
3460
3730
  invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
3461
3731
  });
3462
3732
  }
3733
+ toOutboxContext(identifier, activity, activityId, activityType) {
3734
+ return new OutboxContextImpl(identifier, activity, activityId, activityType, {
3735
+ url: this.url,
3736
+ federation: this.federation,
3737
+ data: this.data,
3738
+ documentLoader: this.documentLoader,
3739
+ contextLoader: this.contextLoader,
3740
+ invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
3741
+ });
3742
+ }
3463
3743
  get hostname() {
3464
3744
  return this.url.hostname;
3465
3745
  }
@@ -3749,9 +4029,9 @@ var ContextImpl = class ContextImpl {
3749
4029
  attributes: {
3750
4030
  "activitypub.activity.type": getTypeId(activity).href,
3751
4031
  "activitypub.activity.to": activity.toIds.map((to) => to.href),
3752
- "activitypub.activity.cc": activity.toIds.map((cc) => cc.href),
4032
+ "activitypub.activity.cc": activity.ccIds.map((cc) => cc.href),
3753
4033
  "activitypub.activity.bto": activity.btoIds.map((bto) => bto.href),
3754
- "activitypub.activity.bcc": activity.toIds.map((bcc) => bcc.href)
4034
+ "activitypub.activity.bcc": activity.bccIds.map((bcc) => bcc.href)
3755
4035
  }
3756
4036
  }, async (span) => {
3757
4037
  try {
@@ -3846,6 +4126,13 @@ var ContextImpl = class ContextImpl {
3846
4126
  preferSharedInbox: options.preferSharedInbox,
3847
4127
  excludeBaseUris: options.excludeBaseUris
3848
4128
  });
4129
+ if (globalThis.Object.keys(inboxes).length < 1) {
4130
+ logger.debug("No inboxes found for activity {activityId}.", {
4131
+ activityId: activity.id?.href,
4132
+ activity
4133
+ });
4134
+ return false;
4135
+ }
3849
4136
  logger.debug("Sending activity {activityId} to inboxes:\n{inboxes}", {
3850
4137
  inboxes: globalThis.Object.keys(inboxes),
3851
4138
  activityId: activity.id?.href,
@@ -3853,7 +4140,7 @@ var ContextImpl = class ContextImpl {
3853
4140
  });
3854
4141
  if (this.federation.fanoutQueue == null || options.immediate || options.fanout === "skip" || (options.fanout ?? "auto") === "auto" && globalThis.Object.keys(inboxes).length < FANOUT_THRESHOLD) {
3855
4142
  await this.federation.sendActivity(keys, inboxes, activity, opts);
3856
- return;
4143
+ return true;
3857
4144
  }
3858
4145
  const keyJwkPairs = await Promise.all(keys.map(async ({ keyId, privateKey }) => ({
3859
4146
  keyId: keyId.href,
@@ -3882,6 +4169,7 @@ var ContextImpl = class ContextImpl {
3882
4169
  };
3883
4170
  if (!this.federation.manuallyStartQueue) this.federation._startQueueInternal(this.data);
3884
4171
  await this.federation.fanoutQueue.enqueue(message, { orderingKey: options.orderingKey });
4172
+ return true;
3885
4173
  }
3886
4174
  async *getFollowers(identifier) {
3887
4175
  if (this.federation.followersCallbacks == null) throw new Error("No followers collection dispatcher registered.");
@@ -4116,6 +4404,170 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
4116
4404
  }
4117
4405
  }
4118
4406
  };
4407
+ function forwardActivity(ctx, loggerCategory, forwarder, recipients, options) {
4408
+ return ctx.tracerProvider.getTracer(name, version).startActiveSpan(ctx.federation.outboxQueue == null || options?.immediate ? `activitypub.${loggerCategory}` : "activitypub.fanout", {
4409
+ kind: ctx.federation.outboxQueue == null || options?.immediate ? SpanKind.CLIENT : SpanKind.PRODUCER,
4410
+ attributes: { "activitypub.activity.type": ctx.activityType }
4411
+ }, async (span) => {
4412
+ try {
4413
+ if (ctx.activityId != null) span.setAttribute("activitypub.activity.id", ctx.activityId);
4414
+ return await forwardActivityInternal(ctx, loggerCategory, forwarder, recipients, options);
4415
+ } catch (e) {
4416
+ span.setStatus({
4417
+ code: SpanStatusCode.ERROR,
4418
+ message: String(e)
4419
+ });
4420
+ throw e;
4421
+ } finally {
4422
+ span.end();
4423
+ }
4424
+ });
4425
+ }
4426
+ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipients, options) {
4427
+ const logger = getLogger([
4428
+ "fedify",
4429
+ "federation",
4430
+ loggerCategory
4431
+ ]);
4432
+ let keys;
4433
+ let identifier = null;
4434
+ if ("identifier" in forwarder || "username" in forwarder) {
4435
+ if ("identifier" in forwarder) identifier = forwarder.identifier;
4436
+ else {
4437
+ const username = forwarder.username;
4438
+ if (ctx.federation.actorCallbacks?.handleMapper == null) identifier = username;
4439
+ else {
4440
+ const mapped = await ctx.federation.actorCallbacks.handleMapper(ctx, username);
4441
+ if (mapped == null) throw new Error(`No actor found for the given username ${JSON.stringify(username)}.`);
4442
+ identifier = mapped;
4443
+ }
4444
+ }
4445
+ const actorKeyPairs = await ctx.getActorKeyPairs(identifier);
4446
+ if (actorKeyPairs.length < 1) throw new Error(`No key pair found for actor ${JSON.stringify(identifier)}.`);
4447
+ keys = actorKeyPairs.map((kp) => ({
4448
+ keyId: kp.keyId,
4449
+ privateKey: kp.privateKey
4450
+ }));
4451
+ } else if (Array.isArray(forwarder)) {
4452
+ if (forwarder.length < 1) throw new Error("The forwarder's key pairs are empty.");
4453
+ keys = forwarder;
4454
+ } else keys = [forwarder];
4455
+ if (!hasSignatureLike(ctx.activity)) {
4456
+ if (!hasProofLike(ctx.activity)) {
4457
+ if (options?.skipIfUnsigned) return false;
4458
+ logger.warn("The activity {activityId} is not signed; even if it is forwarded to other servers as is, it may not be accepted by them due to the lack of a signature/proof.", {
4459
+ activityId: ctx.activityId,
4460
+ activityType: ctx.activityType,
4461
+ identifier: identifier ?? void 0
4462
+ });
4463
+ }
4464
+ }
4465
+ if (recipients === "followers") {
4466
+ if (identifier == null) throw new Error("If recipients is \"followers\", forwarder must be an actor identifier or username.");
4467
+ const followers = [];
4468
+ for await (const recipient of ctx.getFollowers(identifier)) followers.push(recipient);
4469
+ recipients = followers;
4470
+ }
4471
+ const inboxes = extractInboxes({
4472
+ recipients: Array.isArray(recipients) ? recipients : [recipients],
4473
+ preferSharedInbox: options?.preferSharedInbox,
4474
+ excludeBaseUris: options?.excludeBaseUris
4475
+ });
4476
+ if (globalThis.Object.keys(inboxes).length < 1) {
4477
+ logger.debug("No inboxes found for activity {activityId}.", {
4478
+ activityId: ctx.activityId,
4479
+ activityType: ctx.activityType,
4480
+ identifier: identifier ?? void 0
4481
+ });
4482
+ return false;
4483
+ }
4484
+ logger.debug("Forwarding activity {activityId} to inboxes:\n{inboxes}", {
4485
+ inboxes: globalThis.Object.keys(inboxes),
4486
+ activityId: ctx.activityId,
4487
+ activity: ctx.activity
4488
+ });
4489
+ if (options?.immediate || ctx.federation.outboxQueue == null) {
4490
+ if (options?.immediate) logger.debug("Forwarding activity immediately without queue since immediate option is set.");
4491
+ else logger.debug("Forwarding activity immediately without queue since queue is not set.");
4492
+ const promises = [];
4493
+ for (const inbox in inboxes) promises.push(sendActivity({
4494
+ keys,
4495
+ activity: ctx.activity,
4496
+ activityId: ctx.activityId,
4497
+ activityType: ctx.activityType,
4498
+ inbox: new URL(inbox),
4499
+ sharedInbox: inboxes[inbox].sharedInbox,
4500
+ tracerProvider: ctx.tracerProvider,
4501
+ specDeterminer: new KvSpecDeterminer(ctx.federation.kv, ctx.federation.kvPrefixes.httpMessageSignaturesSpec, ctx.federation.firstKnock)
4502
+ }));
4503
+ await Promise.all(promises);
4504
+ return true;
4505
+ }
4506
+ logger.debug("Enqueuing activity {activityId} to forward later.", {
4507
+ activityId: ctx.activityId,
4508
+ activity: ctx.activity
4509
+ });
4510
+ if (!ctx.federation.manuallyStartQueue) ctx.federation._startQueueInternal(ctx.data);
4511
+ const keyJwkPairs = [];
4512
+ for (const { keyId, privateKey } of keys) {
4513
+ const privateKeyJwk = await exportJwk(privateKey);
4514
+ keyJwkPairs.push({
4515
+ keyId: keyId.href,
4516
+ privateKey: privateKeyJwk
4517
+ });
4518
+ }
4519
+ const carrier = {};
4520
+ propagation.inject(context.active(), carrier);
4521
+ const orderingKey = options?.orderingKey;
4522
+ const started = (/* @__PURE__ */ new Date()).toISOString();
4523
+ const messages = [];
4524
+ for (const inbox in inboxes) {
4525
+ const inboxUrl = new URL(inbox);
4526
+ const message = {
4527
+ type: "outbox",
4528
+ id: crypto.randomUUID(),
4529
+ baseUrl: ctx.origin,
4530
+ keys: keyJwkPairs,
4531
+ activity: ctx.activity,
4532
+ activityId: ctx.activityId,
4533
+ activityType: ctx.activityType,
4534
+ inbox,
4535
+ sharedInbox: inboxes[inbox].sharedInbox,
4536
+ actorIds: [...inboxes[inbox].actorIds],
4537
+ started,
4538
+ attempt: 0,
4539
+ headers: {},
4540
+ orderingKey: orderingKey == null ? void 0 : `${orderingKey}\n${inboxUrl.origin}`,
4541
+ traceContext: carrier
4542
+ };
4543
+ messages.push({
4544
+ message,
4545
+ orderingKey: message.orderingKey
4546
+ });
4547
+ }
4548
+ const { outboxQueue } = ctx.federation;
4549
+ if (outboxQueue.enqueueMany == null || orderingKey != null) {
4550
+ const promises = messages.map((m) => outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }));
4551
+ const errors = (await Promise.allSettled(promises)).filter((r) => r.status === "rejected").map((r) => r.reason);
4552
+ if (errors.length > 0) {
4553
+ logger.error("Failed to enqueue activity {activityId} to forward later:\n{errors}", {
4554
+ activityId: ctx.activityId,
4555
+ errors
4556
+ });
4557
+ if (errors.length > 1) throw new AggregateError(errors, `Failed to enqueue activity ${ctx.activityId} to forward later.`);
4558
+ throw errors[0];
4559
+ }
4560
+ } else try {
4561
+ await outboxQueue.enqueueMany(messages.map((m) => m.message));
4562
+ } catch (error) {
4563
+ logger.error("Failed to enqueue activity {activityId} to forward later:\n{error}", {
4564
+ activityId: ctx.activityId,
4565
+ error
4566
+ });
4567
+ throw error;
4568
+ }
4569
+ return true;
4570
+ }
4119
4571
  var InboxContextImpl = class InboxContextImpl extends ContextImpl {
4120
4572
  recipient;
4121
4573
  activity;
@@ -4139,13 +4591,40 @@ var InboxContextImpl = class InboxContextImpl extends ContextImpl {
4139
4591
  });
4140
4592
  }
4141
4593
  forwardActivity(forwarder, recipients, options) {
4142
- return this.tracerProvider.getTracer(name, version).startActiveSpan("activitypub.outbox", {
4143
- kind: this.federation.outboxQueue == null || options?.immediate ? SpanKind.CLIENT : SpanKind.PRODUCER,
4144
- attributes: { "activitypub.activity.type": this.activityType }
4594
+ return forwardActivity(this, "inbox", forwarder, recipients, options).then(() => void 0);
4595
+ }
4596
+ };
4597
+ var OutboxContextImpl = class OutboxContextImpl extends ContextImpl {
4598
+ #deliveryState;
4599
+ identifier;
4600
+ activity;
4601
+ activityId;
4602
+ activityType;
4603
+ constructor(identifier, activity, activityId, activityType, options, deliveryState = { delivered: false }) {
4604
+ super(options);
4605
+ this.#deliveryState = deliveryState;
4606
+ this.identifier = identifier;
4607
+ this.activity = activity;
4608
+ this.activityId = activityId;
4609
+ this.activityType = activityType;
4610
+ }
4611
+ hasDeliveredActivity() {
4612
+ return this.#deliveryState.delivered;
4613
+ }
4614
+ sendActivity(sender, recipients, activity, options = {}) {
4615
+ return this.tracerProvider.getTracer(name, version).startActiveSpan(this.federation.outboxQueue == null || options.immediate ? "activitypub.outbox" : "activitypub.fanout", {
4616
+ kind: this.federation.outboxQueue == null || options.immediate ? SpanKind.CLIENT : SpanKind.PRODUCER,
4617
+ attributes: {
4618
+ "activitypub.activity.type": getTypeId(activity).href,
4619
+ "activitypub.activity.to": activity.toIds.map((to) => to.href),
4620
+ "activitypub.activity.cc": activity.ccIds.map((cc) => cc.href),
4621
+ "activitypub.activity.bto": activity.btoIds.map((bto) => bto.href),
4622
+ "activitypub.activity.bcc": activity.bccIds.map((bcc) => bcc.href)
4623
+ }
4145
4624
  }, async (span) => {
4146
4625
  try {
4147
- if (this.activityId != null) span.setAttribute("activitypub.activity.id", this.activityId);
4148
- await this.forwardActivityInternal(forwarder, recipients, options);
4626
+ if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
4627
+ if (await this.sendActivityInternal(sender, recipients, activity, options, span)) this.#deliveryState.delivered = true;
4149
4628
  } catch (e) {
4150
4629
  span.setStatus({
4151
4630
  code: SpanStatusCode.ERROR,
@@ -4157,151 +4636,20 @@ var InboxContextImpl = class InboxContextImpl extends ContextImpl {
4157
4636
  }
4158
4637
  });
4159
4638
  }
4160
- async forwardActivityInternal(forwarder, recipients, options) {
4161
- const logger = getLogger([
4162
- "fedify",
4163
- "federation",
4164
- "inbox"
4165
- ]);
4166
- let keys;
4167
- let identifier = null;
4168
- if ("identifier" in forwarder || "username" in forwarder) {
4169
- if ("identifier" in forwarder) identifier = forwarder.identifier;
4170
- else {
4171
- const username = forwarder.username;
4172
- if (this.federation.actorCallbacks?.handleMapper == null) identifier = username;
4173
- else {
4174
- const mapped = await this.federation.actorCallbacks.handleMapper(this, username);
4175
- if (mapped == null) throw new Error(`No actor found for the given username ${JSON.stringify(username)}.`);
4176
- identifier = mapped;
4177
- }
4178
- }
4179
- const actorKeyPairs = await this.getActorKeyPairs(identifier);
4180
- if (actorKeyPairs.length < 1) throw new Error(`No key pair found for actor ${JSON.stringify(identifier)}.`);
4181
- keys = actorKeyPairs.map((kp) => ({
4182
- keyId: kp.keyId,
4183
- privateKey: kp.privateKey
4184
- }));
4185
- } else if (Array.isArray(forwarder)) {
4186
- if (forwarder.length < 1) throw new Error("The forwarder's key pairs are empty.");
4187
- keys = forwarder;
4188
- } else keys = [forwarder];
4189
- if (!hasSignature(this.activity)) {
4190
- let hasProof;
4191
- try {
4192
- hasProof = await (await Activity.fromJsonLd(this.activity, this)).getProof() != null;
4193
- } catch {
4194
- hasProof = false;
4195
- }
4196
- if (!hasProof) {
4197
- if (options?.skipIfUnsigned) return;
4198
- logger.warn("The received activity {activityId} is not signed; even if it is forwarded to other servers as is, it may not be accepted by them due to the lack of a signature/proof.");
4199
- }
4200
- }
4201
- if (recipients === "followers") {
4202
- if (identifier == null) throw new Error("If recipients is \"followers\", forwarder must be an actor identifier or username.");
4203
- const followers = [];
4204
- for await (const recipient of this.getFollowers(identifier)) followers.push(recipient);
4205
- recipients = followers;
4206
- }
4207
- const inboxes = extractInboxes({
4208
- recipients: Array.isArray(recipients) ? recipients : [recipients],
4209
- preferSharedInbox: options?.preferSharedInbox,
4210
- excludeBaseUris: options?.excludeBaseUris
4211
- });
4212
- logger.debug("Forwarding activity {activityId} to inboxes:\n{inboxes}", {
4213
- inboxes: globalThis.Object.keys(inboxes),
4214
- activityId: this.activityId,
4215
- activity: this.activity
4216
- });
4217
- if (options?.immediate || this.federation.outboxQueue == null) {
4218
- if (options?.immediate) logger.debug("Forwarding activity immediately without queue since immediate option is set.");
4219
- else logger.debug("Forwarding activity immediately without queue since queue is not set.");
4220
- const promises = [];
4221
- for (const inbox in inboxes) promises.push(sendActivity({
4222
- keys,
4223
- activity: this.activity,
4224
- activityId: this.activityId,
4225
- activityType: this.activityType,
4226
- inbox: new URL(inbox),
4227
- sharedInbox: inboxes[inbox].sharedInbox,
4228
- tracerProvider: this.tracerProvider,
4229
- specDeterminer: new KvSpecDeterminer(this.federation.kv, this.federation.kvPrefixes.httpMessageSignaturesSpec, this.federation.firstKnock)
4230
- }));
4231
- await Promise.all(promises);
4232
- return;
4233
- }
4234
- logger.debug("Enqueuing activity {activityId} to forward later.", {
4235
- activityId: this.activityId,
4236
- activity: this.activity
4639
+ forwardActivity(forwarder, recipients, options) {
4640
+ return forwardActivity(this, "outbox", forwarder, recipients, options).then((delivered) => {
4641
+ if (delivered) this.#deliveryState.delivered = true;
4237
4642
  });
4238
- const keyJwkPairs = [];
4239
- for (const { keyId, privateKey } of keys) {
4240
- const privateKeyJwk = await exportJwk(privateKey);
4241
- keyJwkPairs.push({
4242
- keyId: keyId.href,
4243
- privateKey: privateKeyJwk
4244
- });
4245
- }
4246
- const carrier = {};
4247
- propagation.inject(context.active(), carrier);
4248
- const orderingKey = options?.orderingKey;
4249
- const messages = [];
4250
- for (const inbox in inboxes) {
4251
- const inboxUrl = new URL(inbox);
4252
- const message = {
4253
- type: "outbox",
4254
- id: crypto.randomUUID(),
4255
- baseUrl: this.origin,
4256
- keys: keyJwkPairs,
4257
- activity: this.activity,
4258
- activityId: this.activityId,
4259
- activityType: this.activityType,
4260
- inbox,
4261
- sharedInbox: inboxes[inbox].sharedInbox,
4262
- started: (/* @__PURE__ */ new Date()).toISOString(),
4263
- attempt: 0,
4264
- headers: {},
4265
- orderingKey: orderingKey == null ? void 0 : `${orderingKey}\n${inboxUrl.origin}`,
4266
- traceContext: carrier
4267
- };
4268
- messages.push({
4269
- message,
4270
- orderingKey: message.orderingKey
4271
- });
4272
- }
4273
- const { outboxQueue } = this.federation;
4274
- if (outboxQueue.enqueueMany == null) {
4275
- const promises = messages.map((m) => outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }));
4276
- const errors = (await Promise.allSettled(promises)).filter((r) => r.status === "rejected").map((r) => r.reason);
4277
- if (errors.length > 0) {
4278
- logger.error("Failed to enqueue activity {activityId} to forward later:\n{errors}", {
4279
- activityId: this.activityId,
4280
- errors
4281
- });
4282
- if (errors.length > 1) throw new AggregateError(errors, `Failed to enqueue activity ${this.activityId} to forward later.`);
4283
- throw errors[0];
4284
- }
4285
- } else if (orderingKey != null) {
4286
- const promises = messages.map((m) => outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }));
4287
- const errors = (await Promise.allSettled(promises)).filter((r) => r.status === "rejected").map((r) => r.reason);
4288
- if (errors.length > 0) {
4289
- logger.error("Failed to enqueue activity {activityId} to forward later:\n{errors}", {
4290
- activityId: this.activityId,
4291
- errors
4292
- });
4293
- if (errors.length > 1) throw new AggregateError(errors, `Failed to enqueue activity ${this.activityId} to forward later.`);
4294
- throw errors[0];
4295
- }
4296
- } else try {
4297
- await outboxQueue.enqueueMany(messages.map((m) => m.message));
4298
- } catch (error) {
4299
- logger.error("Failed to enqueue activity {activityId} to forward later:\n{error}", {
4300
- activityId: this.activityId,
4301
- error
4302
- });
4303
- throw error;
4304
- }
4643
+ }
4644
+ clone(data) {
4645
+ return new OutboxContextImpl(this.identifier, this.activity, this.activityId, this.activityType, {
4646
+ url: this.url,
4647
+ federation: this.federation,
4648
+ data,
4649
+ documentLoader: this.documentLoader,
4650
+ contextLoader: this.contextLoader,
4651
+ invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
4652
+ }, this.#deliveryState);
4305
4653
  }
4306
4654
  };
4307
4655
  var KvSpecDeterminer = class {