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

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-0VkYL-Be.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-ZTLy21O_.mjs} +1 -1
  10. package/dist/{docloader-D7q0-Xef.mjs → docloader-qLB9fFVV.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-JxF7bG0o.cjs → http-BLuuf-Rt.cjs} +1 -1
  23. package/dist/{http-RZPxDWq5.mjs → http-CFmrJbuT.mjs} +2 -2
  24. package/dist/{http-D-MhhYUF.js → http-CqkZgsp_.js} +1 -1
  25. package/dist/{key-CGx_dDkX.mjs → key-DVFCI5om.mjs} +1 -1
  26. package/dist/{kv-cache-C2gdVgvb.cjs → kv-cache-BlXew67e.cjs} +1 -1
  27. package/dist/{kv-cache-D84Mk0fZ.js → kv-cache-Dgsvz3PC.js} +1 -1
  28. package/dist/{ld-wup-liFO.mjs → ld-N69KBAlI.mjs} +26 -3
  29. package/dist/{middleware-Bn75dPug.cjs → middleware-B0BqhA1u.cjs} +676 -323
  30. package/dist/{middleware-RF-sUfTr.js → middleware-BpSfJf2S.js} +670 -322
  31. package/dist/{middleware-CXOVT4Ph.cjs → middleware-CUe96Oxe.cjs} +1 -1
  32. package/dist/{middleware-wdfeWjRJ.mjs → middleware-DDXN5eBU.mjs} +1 -1
  33. package/dist/{middleware-BjVx-_bv.mjs → middleware-DbCHS-GH.mjs} +612 -180
  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-DGdkXeF5.mjs} +2 -2
  44. package/dist/{proof-CirP9OSd.js → proof-BtFhWZuV.js} +54 -2
  45. package/dist/{proof--CpZsF_p.mjs → proof-C50gFNxj.mjs} +32 -3
  46. package/dist/{proof-_Zyfqyce.cjs → proof-DwRcU3OF.cjs} +61 -3
  47. package/dist/{send-CVJfx7bF.mjs → send-BlHiNsZI.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 @@ const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  require("./chunk-DDcVe30Y.cjs");
4
4
  const require_transformers = require("./transformers-NeAONrAq.cjs");
5
- const require_http = require("./http-JxF7bG0o.cjs");
6
- const require_proof = require("./proof-_Zyfqyce.cjs");
5
+ const require_http = require("./http-BLuuf-Rt.cjs");
6
+ const require_proof = require("./proof-DwRcU3OF.cjs");
7
7
  const require_types = require("./types-KC4QAoxe.cjs");
8
- const require_kv_cache = require("./kv-cache-C2gdVgvb.cjs");
8
+ const require_kv_cache = require("./kv-cache-BlXew67e.cjs");
9
9
  let _logtape_logtape = require("@logtape/logtape");
10
10
  let _fedify_vocab = require("@fedify/vocab");
11
11
  let _opentelemetry_api = require("@opentelemetry/api");
@@ -17,14 +17,15 @@ let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
17
17
  let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
18
18
  let _fedify_webfinger = require("@fedify/webfinger");
19
19
  let node_url = require("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 === _fedify_vocab.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 = (0, _logtape_logtape.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, (0, _fedify_vocab.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: _opentelemetry_api.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: _opentelemetry_api.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
- _opentelemetry_api.propagation.inject(_opentelemetry_api.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: _opentelemetry_api.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 ?? _opentelemetry_api.trace.getTracerProvider();
138
- return await tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan("activitypub.dispatch_inbox_listener", { kind: _opentelemetry_api.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: _opentelemetry_api.SpanStatusCode.UNSET,
147
- message: `Unsupported activity type: ${(0, _fedify_vocab.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, (0, _fedify_vocab.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: _opentelemetry_api.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().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;
@@ -331,7 +210,7 @@ var FederationBuilderImpl = class {
331
210
  this.collectionTypeIds = {};
332
211
  }
333
212
  async build(options) {
334
- const { FederationImpl } = await Promise.resolve().then(() => require("./middleware-CXOVT4Ph.cjs"));
213
+ const { FederationImpl } = await Promise.resolve().then(() => require("./middleware-CUe96Oxe.cjs"));
335
214
  const f = new FederationImpl(options);
336
215
  const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive;
337
216
  f.router = this.router.clone();
@@ -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="${(0, byte_encodings_hex.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 = (0, _logtape_logtape.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, (0, _fedify_vocab.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: _opentelemetry_api.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: _opentelemetry_api.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
+ _opentelemetry_api.propagation.inject(_opentelemetry_api.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: _opentelemetry_api.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 ?? _opentelemetry_api.trace.getTracerProvider();
857
+ return await tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan("activitypub.dispatch_inbox_listener", { kind: _opentelemetry_api.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: _opentelemetry_api.SpanStatusCode.UNSET,
866
+ message: `Unsupported activity type: ${(0, _fedify_vocab.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, (0, _fedify_vocab.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: _opentelemetry_api.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 ?? _opentelemetry_api.trace.getTracerProvider();
1088
1140
  const tracer = tracerProvider.getTracer(require_http.name, require_http.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 _fedify_vocab.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: _opentelemetry_api.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 = (0, _logtape_logtape.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 _fedify_vocab.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 _fedify_vocab.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, (0, _fedify_vocab.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: (0, _fedify_vocab.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: (0, _fedify_vocab.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: (0, _fedify_vocab.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: (0, _fedify_vocab.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: (0, _fedify_vocab.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: (0, _fedify_vocab.getTypeId)(activity).href
1474
+ });
1475
+ logger.info("Activity {activityId} has been processed in outbox listener.", {
1476
+ activityId: activity.id?.href,
1477
+ activityType: (0, _fedify_vocab.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 = _opentelemetry_api.trace.getTracerProvider(), Collection, CollectionPage, filterPredicate) {
1668
- this.name = name$1;
1915
+ constructor(name$2, values, context, callbacks, tracerProvider = _opentelemetry_api.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;
@@ -3274,16 +3522,37 @@ var FederationImpl = class extends FederationBuilderImpl {
3274
3522
  onNotFound
3275
3523
  });
3276
3524
  }
3277
- case "outbox": return await handleCollection(request, {
3278
- name: "outbox",
3279
- identifier: route.values.identifier,
3280
- uriGetter: context.getOutboxUri.bind(context),
3281
- context,
3282
- collectionCallbacks: this.outboxCallbacks,
3283
- tracerProvider: this.tracerProvider,
3284
- onUnauthorized,
3285
- onNotFound
3286
- });
3525
+ case "outbox":
3526
+ if (request.method === "POST") {
3527
+ if (this.outboxListeners == null) return new Response("Method not allowed.", {
3528
+ status: 405,
3529
+ headers: {
3530
+ Allow: "GET, HEAD",
3531
+ "Content-Type": "text/plain; charset=utf-8"
3532
+ }
3533
+ });
3534
+ return await handleOutbox(request, {
3535
+ identifier: route.values.identifier,
3536
+ context,
3537
+ outboxContextFactory: context.toOutboxContext.bind(context),
3538
+ actorDispatcher: this.actorCallbacks?.dispatcher,
3539
+ authorizePredicate: this.outboxAuthorizePredicate ?? this.outboxCallbacks?.authorizePredicate,
3540
+ outboxListeners: this.outboxListeners,
3541
+ outboxErrorHandler: this.outboxListenerErrorHandler,
3542
+ onUnauthorized,
3543
+ onNotFound
3544
+ });
3545
+ }
3546
+ return await handleCollection(request, {
3547
+ name: "outbox",
3548
+ identifier: route.values.identifier,
3549
+ uriGetter: context.getOutboxUri.bind(context),
3550
+ context,
3551
+ collectionCallbacks: this.outboxCallbacks,
3552
+ tracerProvider: this.tracerProvider,
3553
+ onUnauthorized,
3554
+ onNotFound
3555
+ });
3287
3556
  case "inbox":
3288
3557
  if (request.method !== "POST") return await handleCollection(request, {
3289
3558
  name: "inbox",
@@ -3453,6 +3722,16 @@ var ContextImpl = class ContextImpl {
3453
3722
  invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
3454
3723
  });
3455
3724
  }
3725
+ toOutboxContext(identifier, activity, activityId, activityType) {
3726
+ return new OutboxContextImpl(identifier, activity, activityId, activityType, {
3727
+ url: this.url,
3728
+ federation: this.federation,
3729
+ data: this.data,
3730
+ documentLoader: this.documentLoader,
3731
+ contextLoader: this.contextLoader,
3732
+ invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
3733
+ });
3734
+ }
3456
3735
  get hostname() {
3457
3736
  return this.url.hostname;
3458
3737
  }
@@ -3742,9 +4021,9 @@ var ContextImpl = class ContextImpl {
3742
4021
  attributes: {
3743
4022
  "activitypub.activity.type": (0, _fedify_vocab.getTypeId)(activity).href,
3744
4023
  "activitypub.activity.to": activity.toIds.map((to) => to.href),
3745
- "activitypub.activity.cc": activity.toIds.map((cc) => cc.href),
4024
+ "activitypub.activity.cc": activity.ccIds.map((cc) => cc.href),
3746
4025
  "activitypub.activity.bto": activity.btoIds.map((bto) => bto.href),
3747
- "activitypub.activity.bcc": activity.toIds.map((bcc) => bcc.href)
4026
+ "activitypub.activity.bcc": activity.bccIds.map((bcc) => bcc.href)
3748
4027
  }
3749
4028
  }, async (span) => {
3750
4029
  try {
@@ -3839,6 +4118,13 @@ var ContextImpl = class ContextImpl {
3839
4118
  preferSharedInbox: options.preferSharedInbox,
3840
4119
  excludeBaseUris: options.excludeBaseUris
3841
4120
  });
4121
+ if (globalThis.Object.keys(inboxes).length < 1) {
4122
+ logger.debug("No inboxes found for activity {activityId}.", {
4123
+ activityId: activity.id?.href,
4124
+ activity
4125
+ });
4126
+ return false;
4127
+ }
3842
4128
  logger.debug("Sending activity {activityId} to inboxes:\n{inboxes}", {
3843
4129
  inboxes: globalThis.Object.keys(inboxes),
3844
4130
  activityId: activity.id?.href,
@@ -3846,7 +4132,7 @@ var ContextImpl = class ContextImpl {
3846
4132
  });
3847
4133
  if (this.federation.fanoutQueue == null || options.immediate || options.fanout === "skip" || (options.fanout ?? "auto") === "auto" && globalThis.Object.keys(inboxes).length < FANOUT_THRESHOLD) {
3848
4134
  await this.federation.sendActivity(keys, inboxes, activity, opts);
3849
- return;
4135
+ return true;
3850
4136
  }
3851
4137
  const keyJwkPairs = await Promise.all(keys.map(async ({ keyId, privateKey }) => ({
3852
4138
  keyId: keyId.href,
@@ -3875,6 +4161,7 @@ var ContextImpl = class ContextImpl {
3875
4161
  };
3876
4162
  if (!this.federation.manuallyStartQueue) this.federation._startQueueInternal(this.data);
3877
4163
  await this.federation.fanoutQueue.enqueue(message, { orderingKey: options.orderingKey });
4164
+ return true;
3878
4165
  }
3879
4166
  async *getFollowers(identifier) {
3880
4167
  if (this.federation.followersCallbacks == null) throw new Error("No followers collection dispatcher registered.");
@@ -4109,6 +4396,170 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
4109
4396
  }
4110
4397
  }
4111
4398
  };
4399
+ function forwardActivity(ctx, loggerCategory, forwarder, recipients, options) {
4400
+ return ctx.tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan(ctx.federation.outboxQueue == null || options?.immediate ? `activitypub.${loggerCategory}` : "activitypub.fanout", {
4401
+ kind: ctx.federation.outboxQueue == null || options?.immediate ? _opentelemetry_api.SpanKind.CLIENT : _opentelemetry_api.SpanKind.PRODUCER,
4402
+ attributes: { "activitypub.activity.type": ctx.activityType }
4403
+ }, async (span) => {
4404
+ try {
4405
+ if (ctx.activityId != null) span.setAttribute("activitypub.activity.id", ctx.activityId);
4406
+ return await forwardActivityInternal(ctx, loggerCategory, forwarder, recipients, options);
4407
+ } catch (e) {
4408
+ span.setStatus({
4409
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
4410
+ message: String(e)
4411
+ });
4412
+ throw e;
4413
+ } finally {
4414
+ span.end();
4415
+ }
4416
+ });
4417
+ }
4418
+ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipients, options) {
4419
+ const logger = (0, _logtape_logtape.getLogger)([
4420
+ "fedify",
4421
+ "federation",
4422
+ loggerCategory
4423
+ ]);
4424
+ let keys;
4425
+ let identifier = null;
4426
+ if ("identifier" in forwarder || "username" in forwarder) {
4427
+ if ("identifier" in forwarder) identifier = forwarder.identifier;
4428
+ else {
4429
+ const username = forwarder.username;
4430
+ if (ctx.federation.actorCallbacks?.handleMapper == null) identifier = username;
4431
+ else {
4432
+ const mapped = await ctx.federation.actorCallbacks.handleMapper(ctx, username);
4433
+ if (mapped == null) throw new Error(`No actor found for the given username ${JSON.stringify(username)}.`);
4434
+ identifier = mapped;
4435
+ }
4436
+ }
4437
+ const actorKeyPairs = await ctx.getActorKeyPairs(identifier);
4438
+ if (actorKeyPairs.length < 1) throw new Error(`No key pair found for actor ${JSON.stringify(identifier)}.`);
4439
+ keys = actorKeyPairs.map((kp) => ({
4440
+ keyId: kp.keyId,
4441
+ privateKey: kp.privateKey
4442
+ }));
4443
+ } else if (Array.isArray(forwarder)) {
4444
+ if (forwarder.length < 1) throw new Error("The forwarder's key pairs are empty.");
4445
+ keys = forwarder;
4446
+ } else keys = [forwarder];
4447
+ if (!require_proof.hasSignatureLike(ctx.activity)) {
4448
+ if (!require_proof.hasProofLike(ctx.activity)) {
4449
+ if (options?.skipIfUnsigned) return false;
4450
+ 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.", {
4451
+ activityId: ctx.activityId,
4452
+ activityType: ctx.activityType,
4453
+ identifier: identifier ?? void 0
4454
+ });
4455
+ }
4456
+ }
4457
+ if (recipients === "followers") {
4458
+ if (identifier == null) throw new Error("If recipients is \"followers\", forwarder must be an actor identifier or username.");
4459
+ const followers = [];
4460
+ for await (const recipient of ctx.getFollowers(identifier)) followers.push(recipient);
4461
+ recipients = followers;
4462
+ }
4463
+ const inboxes = extractInboxes({
4464
+ recipients: Array.isArray(recipients) ? recipients : [recipients],
4465
+ preferSharedInbox: options?.preferSharedInbox,
4466
+ excludeBaseUris: options?.excludeBaseUris
4467
+ });
4468
+ if (globalThis.Object.keys(inboxes).length < 1) {
4469
+ logger.debug("No inboxes found for activity {activityId}.", {
4470
+ activityId: ctx.activityId,
4471
+ activityType: ctx.activityType,
4472
+ identifier: identifier ?? void 0
4473
+ });
4474
+ return false;
4475
+ }
4476
+ logger.debug("Forwarding activity {activityId} to inboxes:\n{inboxes}", {
4477
+ inboxes: globalThis.Object.keys(inboxes),
4478
+ activityId: ctx.activityId,
4479
+ activity: ctx.activity
4480
+ });
4481
+ if (options?.immediate || ctx.federation.outboxQueue == null) {
4482
+ if (options?.immediate) logger.debug("Forwarding activity immediately without queue since immediate option is set.");
4483
+ else logger.debug("Forwarding activity immediately without queue since queue is not set.");
4484
+ const promises = [];
4485
+ for (const inbox in inboxes) promises.push(sendActivity({
4486
+ keys,
4487
+ activity: ctx.activity,
4488
+ activityId: ctx.activityId,
4489
+ activityType: ctx.activityType,
4490
+ inbox: new URL(inbox),
4491
+ sharedInbox: inboxes[inbox].sharedInbox,
4492
+ tracerProvider: ctx.tracerProvider,
4493
+ specDeterminer: new KvSpecDeterminer(ctx.federation.kv, ctx.federation.kvPrefixes.httpMessageSignaturesSpec, ctx.federation.firstKnock)
4494
+ }));
4495
+ await Promise.all(promises);
4496
+ return true;
4497
+ }
4498
+ logger.debug("Enqueuing activity {activityId} to forward later.", {
4499
+ activityId: ctx.activityId,
4500
+ activity: ctx.activity
4501
+ });
4502
+ if (!ctx.federation.manuallyStartQueue) ctx.federation._startQueueInternal(ctx.data);
4503
+ const keyJwkPairs = [];
4504
+ for (const { keyId, privateKey } of keys) {
4505
+ const privateKeyJwk = await require_http.exportJwk(privateKey);
4506
+ keyJwkPairs.push({
4507
+ keyId: keyId.href,
4508
+ privateKey: privateKeyJwk
4509
+ });
4510
+ }
4511
+ const carrier = {};
4512
+ _opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), carrier);
4513
+ const orderingKey = options?.orderingKey;
4514
+ const started = (/* @__PURE__ */ new Date()).toISOString();
4515
+ const messages = [];
4516
+ for (const inbox in inboxes) {
4517
+ const inboxUrl = new URL(inbox);
4518
+ const message = {
4519
+ type: "outbox",
4520
+ id: crypto.randomUUID(),
4521
+ baseUrl: ctx.origin,
4522
+ keys: keyJwkPairs,
4523
+ activity: ctx.activity,
4524
+ activityId: ctx.activityId,
4525
+ activityType: ctx.activityType,
4526
+ inbox,
4527
+ sharedInbox: inboxes[inbox].sharedInbox,
4528
+ actorIds: [...inboxes[inbox].actorIds],
4529
+ started,
4530
+ attempt: 0,
4531
+ headers: {},
4532
+ orderingKey: orderingKey == null ? void 0 : `${orderingKey}\n${inboxUrl.origin}`,
4533
+ traceContext: carrier
4534
+ };
4535
+ messages.push({
4536
+ message,
4537
+ orderingKey: message.orderingKey
4538
+ });
4539
+ }
4540
+ const { outboxQueue } = ctx.federation;
4541
+ if (outboxQueue.enqueueMany == null || orderingKey != null) {
4542
+ const promises = messages.map((m) => outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }));
4543
+ const errors = (await Promise.allSettled(promises)).filter((r) => r.status === "rejected").map((r) => r.reason);
4544
+ if (errors.length > 0) {
4545
+ logger.error("Failed to enqueue activity {activityId} to forward later:\n{errors}", {
4546
+ activityId: ctx.activityId,
4547
+ errors
4548
+ });
4549
+ if (errors.length > 1) throw new AggregateError(errors, `Failed to enqueue activity ${ctx.activityId} to forward later.`);
4550
+ throw errors[0];
4551
+ }
4552
+ } else try {
4553
+ await outboxQueue.enqueueMany(messages.map((m) => m.message));
4554
+ } catch (error) {
4555
+ logger.error("Failed to enqueue activity {activityId} to forward later:\n{error}", {
4556
+ activityId: ctx.activityId,
4557
+ error
4558
+ });
4559
+ throw error;
4560
+ }
4561
+ return true;
4562
+ }
4112
4563
  var InboxContextImpl = class InboxContextImpl extends ContextImpl {
4113
4564
  recipient;
4114
4565
  activity;
@@ -4132,13 +4583,40 @@ var InboxContextImpl = class InboxContextImpl extends ContextImpl {
4132
4583
  });
4133
4584
  }
4134
4585
  forwardActivity(forwarder, recipients, options) {
4135
- return this.tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan("activitypub.outbox", {
4136
- kind: this.federation.outboxQueue == null || options?.immediate ? _opentelemetry_api.SpanKind.CLIENT : _opentelemetry_api.SpanKind.PRODUCER,
4137
- attributes: { "activitypub.activity.type": this.activityType }
4586
+ return forwardActivity(this, "inbox", forwarder, recipients, options).then(() => void 0);
4587
+ }
4588
+ };
4589
+ var OutboxContextImpl = class OutboxContextImpl extends ContextImpl {
4590
+ #deliveryState;
4591
+ identifier;
4592
+ activity;
4593
+ activityId;
4594
+ activityType;
4595
+ constructor(identifier, activity, activityId, activityType, options, deliveryState = { delivered: false }) {
4596
+ super(options);
4597
+ this.#deliveryState = deliveryState;
4598
+ this.identifier = identifier;
4599
+ this.activity = activity;
4600
+ this.activityId = activityId;
4601
+ this.activityType = activityType;
4602
+ }
4603
+ hasDeliveredActivity() {
4604
+ return this.#deliveryState.delivered;
4605
+ }
4606
+ sendActivity(sender, recipients, activity, options = {}) {
4607
+ return this.tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan(this.federation.outboxQueue == null || options.immediate ? "activitypub.outbox" : "activitypub.fanout", {
4608
+ kind: this.federation.outboxQueue == null || options.immediate ? _opentelemetry_api.SpanKind.CLIENT : _opentelemetry_api.SpanKind.PRODUCER,
4609
+ attributes: {
4610
+ "activitypub.activity.type": (0, _fedify_vocab.getTypeId)(activity).href,
4611
+ "activitypub.activity.to": activity.toIds.map((to) => to.href),
4612
+ "activitypub.activity.cc": activity.ccIds.map((cc) => cc.href),
4613
+ "activitypub.activity.bto": activity.btoIds.map((bto) => bto.href),
4614
+ "activitypub.activity.bcc": activity.bccIds.map((bcc) => bcc.href)
4615
+ }
4138
4616
  }, async (span) => {
4139
4617
  try {
4140
- if (this.activityId != null) span.setAttribute("activitypub.activity.id", this.activityId);
4141
- await this.forwardActivityInternal(forwarder, recipients, options);
4618
+ if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
4619
+ if (await this.sendActivityInternal(sender, recipients, activity, options, span)) this.#deliveryState.delivered = true;
4142
4620
  } catch (e) {
4143
4621
  span.setStatus({
4144
4622
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -4150,151 +4628,20 @@ var InboxContextImpl = class InboxContextImpl extends ContextImpl {
4150
4628
  }
4151
4629
  });
4152
4630
  }
4153
- async forwardActivityInternal(forwarder, recipients, options) {
4154
- const logger = (0, _logtape_logtape.getLogger)([
4155
- "fedify",
4156
- "federation",
4157
- "inbox"
4158
- ]);
4159
- let keys;
4160
- let identifier = null;
4161
- if ("identifier" in forwarder || "username" in forwarder) {
4162
- if ("identifier" in forwarder) identifier = forwarder.identifier;
4163
- else {
4164
- const username = forwarder.username;
4165
- if (this.federation.actorCallbacks?.handleMapper == null) identifier = username;
4166
- else {
4167
- const mapped = await this.federation.actorCallbacks.handleMapper(this, username);
4168
- if (mapped == null) throw new Error(`No actor found for the given username ${JSON.stringify(username)}.`);
4169
- identifier = mapped;
4170
- }
4171
- }
4172
- const actorKeyPairs = await this.getActorKeyPairs(identifier);
4173
- if (actorKeyPairs.length < 1) throw new Error(`No key pair found for actor ${JSON.stringify(identifier)}.`);
4174
- keys = actorKeyPairs.map((kp) => ({
4175
- keyId: kp.keyId,
4176
- privateKey: kp.privateKey
4177
- }));
4178
- } else if (Array.isArray(forwarder)) {
4179
- if (forwarder.length < 1) throw new Error("The forwarder's key pairs are empty.");
4180
- keys = forwarder;
4181
- } else keys = [forwarder];
4182
- if (!require_proof.hasSignature(this.activity)) {
4183
- let hasProof;
4184
- try {
4185
- hasProof = await (await _fedify_vocab.Activity.fromJsonLd(this.activity, this)).getProof() != null;
4186
- } catch {
4187
- hasProof = false;
4188
- }
4189
- if (!hasProof) {
4190
- if (options?.skipIfUnsigned) return;
4191
- 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.");
4192
- }
4193
- }
4194
- if (recipients === "followers") {
4195
- if (identifier == null) throw new Error("If recipients is \"followers\", forwarder must be an actor identifier or username.");
4196
- const followers = [];
4197
- for await (const recipient of this.getFollowers(identifier)) followers.push(recipient);
4198
- recipients = followers;
4199
- }
4200
- const inboxes = extractInboxes({
4201
- recipients: Array.isArray(recipients) ? recipients : [recipients],
4202
- preferSharedInbox: options?.preferSharedInbox,
4203
- excludeBaseUris: options?.excludeBaseUris
4204
- });
4205
- logger.debug("Forwarding activity {activityId} to inboxes:\n{inboxes}", {
4206
- inboxes: globalThis.Object.keys(inboxes),
4207
- activityId: this.activityId,
4208
- activity: this.activity
4209
- });
4210
- if (options?.immediate || this.federation.outboxQueue == null) {
4211
- if (options?.immediate) logger.debug("Forwarding activity immediately without queue since immediate option is set.");
4212
- else logger.debug("Forwarding activity immediately without queue since queue is not set.");
4213
- const promises = [];
4214
- for (const inbox in inboxes) promises.push(sendActivity({
4215
- keys,
4216
- activity: this.activity,
4217
- activityId: this.activityId,
4218
- activityType: this.activityType,
4219
- inbox: new URL(inbox),
4220
- sharedInbox: inboxes[inbox].sharedInbox,
4221
- tracerProvider: this.tracerProvider,
4222
- specDeterminer: new KvSpecDeterminer(this.federation.kv, this.federation.kvPrefixes.httpMessageSignaturesSpec, this.federation.firstKnock)
4223
- }));
4224
- await Promise.all(promises);
4225
- return;
4226
- }
4227
- logger.debug("Enqueuing activity {activityId} to forward later.", {
4228
- activityId: this.activityId,
4229
- activity: this.activity
4631
+ forwardActivity(forwarder, recipients, options) {
4632
+ return forwardActivity(this, "outbox", forwarder, recipients, options).then((delivered) => {
4633
+ if (delivered) this.#deliveryState.delivered = true;
4230
4634
  });
4231
- const keyJwkPairs = [];
4232
- for (const { keyId, privateKey } of keys) {
4233
- const privateKeyJwk = await require_http.exportJwk(privateKey);
4234
- keyJwkPairs.push({
4235
- keyId: keyId.href,
4236
- privateKey: privateKeyJwk
4237
- });
4238
- }
4239
- const carrier = {};
4240
- _opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), carrier);
4241
- const orderingKey = options?.orderingKey;
4242
- const messages = [];
4243
- for (const inbox in inboxes) {
4244
- const inboxUrl = new URL(inbox);
4245
- const message = {
4246
- type: "outbox",
4247
- id: crypto.randomUUID(),
4248
- baseUrl: this.origin,
4249
- keys: keyJwkPairs,
4250
- activity: this.activity,
4251
- activityId: this.activityId,
4252
- activityType: this.activityType,
4253
- inbox,
4254
- sharedInbox: inboxes[inbox].sharedInbox,
4255
- started: (/* @__PURE__ */ new Date()).toISOString(),
4256
- attempt: 0,
4257
- headers: {},
4258
- orderingKey: orderingKey == null ? void 0 : `${orderingKey}\n${inboxUrl.origin}`,
4259
- traceContext: carrier
4260
- };
4261
- messages.push({
4262
- message,
4263
- orderingKey: message.orderingKey
4264
- });
4265
- }
4266
- const { outboxQueue } = this.federation;
4267
- if (outboxQueue.enqueueMany == null) {
4268
- const promises = messages.map((m) => outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }));
4269
- const errors = (await Promise.allSettled(promises)).filter((r) => r.status === "rejected").map((r) => r.reason);
4270
- if (errors.length > 0) {
4271
- logger.error("Failed to enqueue activity {activityId} to forward later:\n{errors}", {
4272
- activityId: this.activityId,
4273
- errors
4274
- });
4275
- if (errors.length > 1) throw new AggregateError(errors, `Failed to enqueue activity ${this.activityId} to forward later.`);
4276
- throw errors[0];
4277
- }
4278
- } else if (orderingKey != null) {
4279
- const promises = messages.map((m) => outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }));
4280
- const errors = (await Promise.allSettled(promises)).filter((r) => r.status === "rejected").map((r) => r.reason);
4281
- if (errors.length > 0) {
4282
- logger.error("Failed to enqueue activity {activityId} to forward later:\n{errors}", {
4283
- activityId: this.activityId,
4284
- errors
4285
- });
4286
- if (errors.length > 1) throw new AggregateError(errors, `Failed to enqueue activity ${this.activityId} to forward later.`);
4287
- throw errors[0];
4288
- }
4289
- } else try {
4290
- await outboxQueue.enqueueMany(messages.map((m) => m.message));
4291
- } catch (error) {
4292
- logger.error("Failed to enqueue activity {activityId} to forward later:\n{error}", {
4293
- activityId: this.activityId,
4294
- error
4295
- });
4296
- throw error;
4297
- }
4635
+ }
4636
+ clone(data) {
4637
+ return new OutboxContextImpl(this.identifier, this.activity, this.activityId, this.activityType, {
4638
+ url: this.url,
4639
+ federation: this.federation,
4640
+ data,
4641
+ documentLoader: this.documentLoader,
4642
+ contextLoader: this.contextLoader,
4643
+ invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
4644
+ }, this.#deliveryState);
4298
4645
  }
4299
4646
  };
4300
4647
  var KvSpecDeterminer = class {
@@ -4371,6 +4718,12 @@ Object.defineProperty(exports, "KvSpecDeterminer", {
4371
4718
  return KvSpecDeterminer;
4372
4719
  }
4373
4720
  });
4721
+ Object.defineProperty(exports, "OutboxContextImpl", {
4722
+ enumerable: true,
4723
+ get: function() {
4724
+ return OutboxContextImpl;
4725
+ }
4726
+ });
4374
4727
  Object.defineProperty(exports, "Router", {
4375
4728
  enumerable: true,
4376
4729
  get: function() {