@fedify/fedify 2.5.0-dev.2609 → 2.5.0-dev.2618

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 (123) hide show
  1. package/dist/{builder-BLCvt0zp.mjs → builder-C-zhu1PC.mjs} +82 -3
  2. package/dist/compat/mod.d.cts +1 -1
  3. package/dist/compat/mod.d.ts +1 -1
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{compound-proof-DNhLvscb.mjs → compound-proof-B6Mz3pzt.mjs} +2 -2
  6. package/dist/{context-ByflxsY9.d.cts → context-C1NqcIct.d.cts} +1061 -443
  7. package/dist/{context-uQUY5fQT.d.ts → context-xrqq4Hrq.d.ts} +1064 -446
  8. package/dist/{deno-DYkYmKp0.mjs → deno-CaPDSXDl.mjs} +1 -1
  9. package/dist/{docloader-B9RO5nh9.mjs → docloader-kYMkiXaB.mjs} +2 -2
  10. package/dist/federation/builder.test.mjs +1 -1
  11. package/dist/federation/compound-fanout.test.mjs +3 -3
  12. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  13. package/dist/federation/handler.test.mjs +4 -4
  14. package/dist/federation/hashlink-media.test.mjs +2 -2
  15. package/dist/federation/idempotency.test.mjs +2 -2
  16. package/dist/federation/inbox-actorless.test.mjs +5 -5
  17. package/dist/federation/inbox-report.test.mjs +220 -5
  18. package/dist/federation/inbox-view.test.mjs +1 -1
  19. package/dist/federation/inbox.test.mjs +1 -1
  20. package/dist/federation/metrics.test.mjs +1 -1
  21. package/dist/federation/middleware.test.mjs +7 -7
  22. package/dist/federation/mod.cjs +1 -1
  23. package/dist/federation/mod.d.cts +3 -3
  24. package/dist/federation/mod.d.ts +4 -4
  25. package/dist/federation/mod.js +1 -1
  26. package/dist/federation/mq.test.mjs +1 -1
  27. package/dist/federation/outbound-body.test.d.mts +2 -0
  28. package/dist/federation/outbound-body.test.mjs +149 -0
  29. package/dist/federation/outbound-capture.test.d.mts +2 -0
  30. package/dist/federation/outbound-capture.test.mjs +394 -0
  31. package/dist/federation/outbound-dispatch.test.d.mts +2 -0
  32. package/dist/federation/outbound-dispatch.test.mjs +368 -0
  33. package/dist/federation/outbound-document.test.d.mts +2 -0
  34. package/dist/federation/outbound-document.test.mjs +171 -0
  35. package/dist/federation/outbound-provenance.test.d.mts +2 -0
  36. package/dist/federation/outbound-provenance.test.mjs +543 -0
  37. package/dist/federation/outbound-report.test.d.mts +2 -0
  38. package/dist/federation/outbound-report.test.mjs +5475 -0
  39. package/dist/federation/outbound-workers.test.d.mts +2 -0
  40. package/dist/federation/outbound-workers.test.mjs +64 -0
  41. package/dist/federation/outbound-workflow.test.d.mts +2 -0
  42. package/dist/federation/outbound-workflow.test.mjs +154 -0
  43. package/dist/federation/outgoing-proof.test.mjs +4 -4
  44. package/dist/federation/portable-actor.test.mjs +3 -3
  45. package/dist/federation/portable-chain.test.mjs +3 -3
  46. package/dist/federation/portable-collection.test.mjs +4 -4
  47. package/dist/federation/portable-delivery.test.mjs +106 -7
  48. package/dist/federation/portable-gateway-keys.test.mjs +5 -5
  49. package/dist/federation/portable-inbox.test.mjs +430 -5
  50. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  51. package/dist/federation/portable-route-activity.test.mjs +2 -2
  52. package/dist/federation/portable.test.mjs +3 -3
  53. package/dist/federation/send.test.mjs +44 -3
  54. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  55. package/dist/federation/tasks/tasks.test.mjs +2 -2
  56. package/dist/federation/temporal.test.mjs +1 -1
  57. package/dist/federation/webfinger.test.mjs +1 -1
  58. package/dist/{federation-DzMdu4zU.cjs → federation-c-ulWHXG.cjs} +2358 -405
  59. package/dist/{http-EoFzzh1s.js → http-C12QVNZt.js} +62 -32
  60. package/dist/{http-BdTJujR0.cjs → http-DwL_g670.cjs} +62 -32
  61. package/dist/{http-TgeyuTDM.mjs → http-NYqvoigN.mjs} +64 -34
  62. package/dist/{inbox-ChkbRxCN.mjs → inbox-CkL5YgHZ.mjs} +90 -3
  63. package/dist/{inbox-view-BosBZdlt.mjs → inbox-view-IJCZG0_b.mjs} +1 -1
  64. package/dist/{key-DbLNT-vA.d.ts → key-dwwAg5bA.d.ts} +1 -1
  65. package/dist/{kv-cache-CMgOYiLU.js → kv-cache-1BO1Deou.js} +1 -1
  66. package/dist/{kv-cache-CTA2i08a.mjs → kv-cache-BdFnnrOU.mjs} +1 -1
  67. package/dist/{kv-cache-B0o18_Ni.cjs → kv-cache-CcGUsFoZ.cjs} +1 -1
  68. package/dist/{ld-CZsMAfOw.mjs → ld-CEOS8Pho.mjs} +2 -2
  69. package/dist/{ld-Inagww4R.d.ts → ld-o4LnhCYY.d.ts} +2 -2
  70. package/dist/{metrics-Kc9mP1LD.mjs → metrics-CvuJeWaJ.mjs} +1 -1
  71. package/dist/{metrics-DoRtI0qU.d.ts → metrics-DVKYY6K1.d.ts} +1 -1
  72. package/dist/{middleware-C1coPz8l.mjs → middleware-BPZ9KhAd.mjs} +1 -1
  73. package/dist/{middleware-B-pjIvQ8.js → middleware-CzcR7pNr.js} +2303 -351
  74. package/dist/{middleware-CIRoIVSU.mjs → middleware-DMrpt2wD.mjs} +1573 -325
  75. package/dist/{mod-B7j30IQf.d.ts → mod-CQC9xlLZ.d.ts} +3 -3
  76. package/dist/{mod-B4kBw1-8.d.ts → mod-D22ZsiKi.d.ts} +2 -2
  77. package/dist/{mod-BN0TTfyQ.d.ts → mod-DO0epJGH.d.ts} +1 -1
  78. package/dist/{mod-DF2gbN_d.d.cts → mod-TwiQgEf3.d.cts} +1 -1
  79. package/dist/mod.cjs +4 -4
  80. package/dist/mod.d.cts +4 -4
  81. package/dist/mod.d.ts +8 -8
  82. package/dist/mod.js +4 -4
  83. package/dist/nodeinfo/handler.test.mjs +1 -1
  84. package/dist/outbound-body-CckPA5gc.mjs +83 -0
  85. package/dist/outbound-capture-DVgnRzlf.mjs +184 -0
  86. package/dist/outbound-document-BMgwLXTY.mjs +179 -0
  87. package/dist/{owner-Dwz4fGUW.mjs → owner-BOyHaAlU.mjs} +3 -3
  88. package/dist/{portable-collection-BikiijSk.js → portable-collection-B4HFXlgN.js} +2 -2
  89. package/dist/{portable-collection-Oxfavx8f.mjs → portable-collection-BBSMcaDY.mjs} +2 -2
  90. package/dist/{portable-collection-Ci6vIhbR.cjs → portable-collection-D_zZTK4C.cjs} +2 -2
  91. package/dist/{portable-key--fWpTNDN.mjs → portable-key-CF8xK_lj.mjs} +1 -1
  92. package/dist/{proof-D6Kb0Qq9.mjs → proof-VrE-GaGv.mjs} +3 -3
  93. package/dist/{send-BFti0L3I.mjs → send-CY6agn9T.mjs} +153 -40
  94. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  95. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  96. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  97. package/dist/sig/compound-proof.test.mjs +2 -2
  98. package/dist/sig/http.test.mjs +38 -2
  99. package/dist/sig/key.test.mjs +1 -1
  100. package/dist/sig/ld.test.mjs +1 -1
  101. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  102. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  103. package/dist/sig/mod.cjs +2 -2
  104. package/dist/sig/mod.d.ts +3 -3
  105. package/dist/sig/mod.js +2 -2
  106. package/dist/sig/owner.test.mjs +1 -1
  107. package/dist/sig/portable-collection.test.mjs +1 -1
  108. package/dist/sig/portable-interop.test.mjs +3 -3
  109. package/dist/sig/portable-key.test.mjs +5 -5
  110. package/dist/sig/proof.test.mjs +3 -3
  111. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  112. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  113. package/dist/sig/verification.test.mjs +3 -3
  114. package/dist/{temporal-DlYL_raW.mjs → temporal-QO2pe6W4.mjs} +1 -1
  115. package/dist/testing/kv.test.mjs +1 -1
  116. package/dist/testing/mod.d.mts +1030 -412
  117. package/dist/utils/docloader.test.mjs +2 -2
  118. package/dist/utils/kv-cache.test.mjs +1 -1
  119. package/dist/utils/mod.cjs +1 -1
  120. package/dist/utils/mod.d.ts +1 -1
  121. package/dist/utils/mod.js +1 -1
  122. package/package.json +7 -7
  123. package/skills/fedify/SKILL.md +60 -1
@@ -1,15 +1,15 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  const require_rolldown_runtime = require("./rolldown-runtime-Dmm3t92W.cjs");
3
3
  const require_transformers = require("./transformers-BalCrSsr.cjs");
4
- const require_http = require("./http-BdTJujR0.cjs");
4
+ const require_http = require("./http-DwL_g670.cjs");
5
5
  const require_types = require("./types-hKCCjii0.cjs");
6
- const require_portable_collection = require("./portable-collection-Ci6vIhbR.cjs");
7
- const require_kv_cache = require("./kv-cache-B0o18_Ni.cjs");
6
+ const require_portable_collection = require("./portable-collection-D_zZTK4C.cjs");
7
+ const require_kv_cache = require("./kv-cache-CcGUsFoZ.cjs");
8
8
  let _logtape_logtape = require("@logtape/logtape");
9
+ let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
9
10
  let _fedify_uri_template = require("@fedify/uri-template");
10
11
  let _fedify_vocab = require("@fedify/vocab");
11
12
  let _opentelemetry_api = require("@opentelemetry/api");
12
- let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
13
13
  let _fedify_vocab_runtime_internal_portable_dereference = require("@fedify/vocab-runtime/internal/portable-dereference");
14
14
  let byte_encodings_hex = require("byte-encodings/hex");
15
15
  let _fedify_vocab_runtime_jsonld = require("@fedify/vocab-runtime/jsonld");
@@ -23,6 +23,500 @@ let _fedify_webfinger = require("@fedify/webfinger");
23
23
  let _opentelemetry_sdk_metrics = require("@opentelemetry/sdk-metrics");
24
24
  let devalue = require("devalue");
25
25
  let node_url = require("node:url");
26
+ //#region src/federation/outbound-document.ts
27
+ const asContext = "https://www.w3.org/ns/activitystreams";
28
+ const asNamespace = `${asContext}#`;
29
+ const blind = /* @__PURE__ */ new Set([
30
+ "bto",
31
+ "bcc",
32
+ "as:bto",
33
+ "as:bcc",
34
+ `${asNamespace}bto`,
35
+ `${asNamespace}bcc`
36
+ ]);
37
+ const contextCandidates = [
38
+ asContext,
39
+ "https://w3id.org/security/v1",
40
+ "https://w3id.org/identity/v1",
41
+ "https://gotosocial.org/ns",
42
+ "https://w3id.org/security/data-integrity/v1",
43
+ "https://w3id.org/security/data-integrity/v2"
44
+ ];
45
+ function safeContext(context) {
46
+ if (Array.isArray(context)) return context.every(safeContext);
47
+ if (context == null || typeof context !== "object") return false;
48
+ for (const [name, definition] of Object.entries(context)) {
49
+ if (name === "@import") return false;
50
+ const mapping = typeof definition === "string" ? definition : definition != null && typeof definition === "object" ? definition["@id"] : void 0;
51
+ if (name === "as" && mapping !== asNamespace) return false;
52
+ if (name !== "as" && typeof mapping === "string" && mapping.startsWith(asNamespace) && (mapping.endsWith("#") || mapping.endsWith("/") || typeof definition === "object" && definition["@prefix"] === true)) return false;
53
+ if (typeof mapping === "string" && blind.has(mapping) && name !== "bto" && name !== "bcc") return false;
54
+ if ((name === "bto" || name === "bcc") && mapping !== `as:${name}` && mapping !== `${asNamespace}${name}`) return false;
55
+ if (name === "@vocab" && typeof mapping === "string" && mapping.startsWith(asNamespace)) return false;
56
+ if (definition != null && typeof definition === "object" && "@context" in definition && !safeContext(definition["@context"])) return false;
57
+ }
58
+ return true;
59
+ }
60
+ const contexts = new Set(contextCandidates.filter((url) => {
61
+ const document = _fedify_vocab_runtime.preloadedContexts[url];
62
+ return document != null && typeof document === "object" && safeContext(document["@context"]);
63
+ }));
64
+ const generatedTerms = {
65
+ toot: "http://joinmastodon.org/ns#",
66
+ misskey: "https://misskey-hub.net/ns#",
67
+ fedibird: "http://fedibird.com/ns#",
68
+ sensitive: "as:sensitive",
69
+ votersCount: {
70
+ "@id": "toot:votersCount",
71
+ "@type": "http://www.w3.org/2001/XMLSchema#nonNegativeInteger"
72
+ },
73
+ Emoji: "toot:Emoji",
74
+ Hashtag: "as:Hashtag",
75
+ ChatMessage: "http://litepub.social/ns#ChatMessage",
76
+ quote: {
77
+ "@id": "https://w3id.org/fep/044f#quote",
78
+ "@type": "@id"
79
+ },
80
+ quoteUrl: "as:quoteUrl",
81
+ _misskey_quote: "misskey:_misskey_quote",
82
+ quoteUri: "fedibird:quoteUri",
83
+ QuoteAuthorization: "https://w3id.org/fep/044f#QuoteAuthorization",
84
+ QuoteRequest: "https://w3id.org/fep/044f#QuoteRequest",
85
+ quoteAuthorization: {
86
+ "@id": "https://w3id.org/fep/044f#quoteAuthorization",
87
+ "@type": "@id"
88
+ },
89
+ emojiReactions: {
90
+ "@id": "fedibird:emojiReactions",
91
+ "@type": "@id"
92
+ }
93
+ };
94
+ function sameDefinition(value, expected) {
95
+ if (value === expected) return true;
96
+ if (value == null || expected == null || typeof value !== "object" || typeof expected !== "object" || Array.isArray(value)) return false;
97
+ const actual = value;
98
+ const reference = expected;
99
+ return Object.keys(actual).length === Object.keys(reference).length && Object.keys(actual).every((name) => Object.hasOwn(reference, name) && sameDefinition(actual[name], reference[name]));
100
+ }
101
+ function generatedContext(value) {
102
+ if (value == null || typeof value !== "object" || Array.isArray(value)) return false;
103
+ return Object.entries(value).every(([name, definition]) => Object.hasOwn(generatedTerms, name) && sameDefinition(definition, generatedTerms[name]));
104
+ }
105
+ const keywords = /* @__PURE__ */ new Set([
106
+ "@id",
107
+ "@type",
108
+ "@value",
109
+ "@language",
110
+ "@direction",
111
+ "@index",
112
+ "@list",
113
+ "@set",
114
+ "@graph",
115
+ "@reverse",
116
+ "@included",
117
+ "@nest"
118
+ ]);
119
+ /** Validate the explicit opt-in before replacing observer registration. */
120
+ function snapshotSenderPayloadOption(value) {
121
+ if (value === void 0) return false;
122
+ if (typeof value !== "boolean") throw new TypeError("Outbound senderPayload must be a boolean.");
123
+ return value;
124
+ }
125
+ /** Project existing plain JSON only; no getters, JSON-LD processing or serializers. */
126
+ function projectSenderPayload(input) {
127
+ try {
128
+ let compact = false;
129
+ let redacted = false;
130
+ const seen = /* @__PURE__ */ new WeakSet();
131
+ const copy = (value, depth, contextData = false) => {
132
+ if (depth >= 64) throw new TypeError("Unsafe document depth.");
133
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
134
+ if (typeof value === "number" && Number.isFinite(value)) return value;
135
+ if (typeof value !== "object" || value == null || seen.has(value)) throw new TypeError("Not independent JSON data.");
136
+ const array = Array.isArray(value);
137
+ const prototype = Object.getPrototypeOf(value);
138
+ if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) throw new TypeError("Unsupported document object.");
139
+ seen.add(value);
140
+ try {
141
+ const descriptors = Object.getOwnPropertyDescriptors(value);
142
+ if (Object.hasOwn(descriptors, "toJSON")) throw new TypeError("Custom JSON conversion is not projected.");
143
+ const names = Reflect.ownKeys(descriptors).filter((name) => descriptors[name].enumerable);
144
+ if (names.some((name) => typeof name !== "string" || !("value" in descriptors[name]))) throw new TypeError("Not plain JSON properties.");
145
+ let contextValue;
146
+ if (!contextData && !array && descriptors["@context"]?.enumerable) {
147
+ const context = copy(descriptors["@context"].value, depth + 1, true);
148
+ contextValue = context;
149
+ const urls = typeof context === "string" ? [context] : context;
150
+ if (!Array.isArray(urls) || urls.length === 0 || (depth === 0 ? !urls.includes(asContext) : !compact) || !urls.every((url) => typeof url === "string" ? contexts.has(url) : depth === 0 && generatedContext(url))) throw new TypeError("Unsupported document context.");
151
+ compact = true;
152
+ }
153
+ if (array) {
154
+ const length = descriptors.length.value;
155
+ if (names.length !== length || names.some((name, index) => name !== String(index))) throw new TypeError("Not a JSON array.");
156
+ return Object.freeze(names.map((name) => copy(descriptors[name].value, depth + 1, contextData)));
157
+ }
158
+ const result = Object.create(null);
159
+ for (const name of names) {
160
+ if (contextData) {
161
+ result[name] = copy(descriptors[name].value, depth + 1, true);
162
+ continue;
163
+ }
164
+ if (name === "@context") {
165
+ if (contextValue === void 0 || !compact) throw new TypeError("Scoped context not supported.");
166
+ result[name] = contextValue;
167
+ continue;
168
+ }
169
+ if (!compact && !keywords.has(name) && !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(name)) throw new TypeError("Unresolved document property.");
170
+ const next = copy(descriptors[name].value, depth + 1);
171
+ if (blind.has(name)) {
172
+ redacted = true;
173
+ continue;
174
+ }
175
+ result[name] = next;
176
+ }
177
+ return Object.freeze(result);
178
+ } finally {
179
+ seen.delete(value);
180
+ }
181
+ };
182
+ if (input == null || typeof input !== "object") throw new TypeError("Not an outgoing document.");
183
+ const value = copy(input, 0);
184
+ return Object.freeze({
185
+ stage: "sender-payload",
186
+ type: "available",
187
+ representation: "privacy-projection",
188
+ redacted,
189
+ value
190
+ });
191
+ } catch {
192
+ return Object.freeze({
193
+ stage: "sender-payload",
194
+ type: "unavailable",
195
+ reason: "privacy-filter-unavailable"
196
+ });
197
+ }
198
+ }
199
+ //#endregion
200
+ //#region src/federation/outbound-body.ts
201
+ /** One pull-driven response reader with an absolute lifetime and no extra queue. */
202
+ var ResponseBodyPump = class {
203
+ #reader;
204
+ #expiresAt;
205
+ #deadline;
206
+ #timer;
207
+ #expireDeadline;
208
+ #pending;
209
+ #timedOut = false;
210
+ #closed = false;
211
+ #ended = false;
212
+ constructor(body, timeoutMs) {
213
+ this.#reader = body.getReader();
214
+ this.#expiresAt = performance.now() + timeoutMs;
215
+ this.#deadline = new Promise((resolve) => {
216
+ this.#expireDeadline = () => resolve(void 0);
217
+ });
218
+ this.#timer = setTimeout(() => this.#expire(), timeoutMs);
219
+ }
220
+ /** Undefined means the absolute lifetime expired, independently of EOF. */
221
+ read() {
222
+ if (this.#closed) return Promise.resolve(this.#timedOut ? void 0 : {
223
+ done: true,
224
+ value: void 0
225
+ });
226
+ if (performance.now() >= this.#expiresAt) {
227
+ this.#expire();
228
+ return Promise.resolve(void 0);
229
+ }
230
+ this.#pending ??= Promise.race([this.#reader.read(), this.#deadline]).then((result) => {
231
+ this.#pending = void 0;
232
+ if (this.#timedOut || performance.now() >= this.#expiresAt) {
233
+ this.#expire();
234
+ return;
235
+ }
236
+ if (this.#closed) return {
237
+ done: true,
238
+ value: void 0
239
+ };
240
+ if (result?.done) {
241
+ this.#ended = true;
242
+ this.close();
243
+ }
244
+ return result;
245
+ }, (error) => {
246
+ this.#pending = void 0;
247
+ this.close();
248
+ throw error;
249
+ });
250
+ return this.#pending;
251
+ }
252
+ /** Shorten the remaining lifetime when ownership passes to diagnostic cleanup. */
253
+ limitLifetime(timeoutMs) {
254
+ if (this.#closed) return;
255
+ this.#expiresAt = Math.min(this.#expiresAt, performance.now() + Math.max(0, timeoutMs));
256
+ clearTimeout(this.#timer);
257
+ if (performance.now() >= this.#expiresAt) this.#expire();
258
+ else this.#timer = setTimeout(() => this.#expire(), this.#expiresAt - performance.now());
259
+ }
260
+ /** Stop pulling and release ownership without awaiting arbitrary cancellation. */
261
+ close() {
262
+ if (this.#closed) return;
263
+ this.#closed = true;
264
+ clearTimeout(this.#timer);
265
+ try {
266
+ if (!this.#ended) this.#reader.cancel().catch(() => {});
267
+ } finally {
268
+ this.#reader.releaseLock();
269
+ }
270
+ }
271
+ #expire() {
272
+ if (this.#timedOut) return;
273
+ this.#timedOut = true;
274
+ this.#expireDeadline();
275
+ this.close();
276
+ }
277
+ };
278
+ //#endregion
279
+ //#region src/federation/outbound-capture.ts
280
+ const diagnosticTimeoutMs = 1e3;
281
+ const diagnosticMaxBytes = 1024;
282
+ const diagnostics = /* @__PURE__ */ new WeakMap();
283
+ /** Reuse a selected capture's diagnostic preview instead of acquiring a second reader. */
284
+ function takeCapturedDiagnostic(response) {
285
+ const value = diagnostics.get(response);
286
+ diagnostics.delete(response);
287
+ return value;
288
+ }
289
+ /** Validate and copy before replacing the current observer registration. */
290
+ function snapshotBodyCapture(value) {
291
+ if (value === void 0) return void 0;
292
+ if (value == null || typeof value !== "object" || typeof value.onBody !== "function") throw new TypeError("Response body capture requires an onBody callback.");
293
+ const { maxBytes, timeoutMs, responses = "all", onBody } = value;
294
+ if (typeof maxBytes !== "number" || typeof timeoutMs !== "number") throw new TypeError("Response body capture requires numeric maxBytes and timeoutMs.");
295
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("Response body capture maxBytes must be a positive safe integer.");
296
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || Math.ceil(timeoutMs) > 2147483647) throw new RangeError("Response body capture timeoutMs is outside the timer range.");
297
+ if (responses !== "all" && responses !== "non-success-http-responses") throw new TypeError("Invalid response body capture selector.");
298
+ return Object.freeze({
299
+ maxBytes,
300
+ timeoutMs: Math.ceil(timeoutMs),
301
+ responses,
302
+ onBody
303
+ });
304
+ }
305
+ /** One source reader feeds both the revocable application view and bounded diagnostics. */
306
+ async function captureResponseBody(response, options, onBody) {
307
+ if (options.responses === "non-success-http-responses" && response.ok) return Object.freeze({
308
+ type: "filtered",
309
+ bytesExposed: 0
310
+ });
311
+ if (response.body == null) return Object.freeze({
312
+ type: "no-body",
313
+ bytesExposed: 0
314
+ });
315
+ const start = performance.now();
316
+ const expires = start + options.timeoutMs;
317
+ let pump;
318
+ try {
319
+ pump = new ResponseBodyPump(response.body, Math.max(options.timeoutMs, response.ok ? 0 : diagnosticTimeoutMs));
320
+ } catch (error) {
321
+ return Object.freeze({
322
+ type: "failed",
323
+ phase: "body",
324
+ error,
325
+ bytesExposed: 0
326
+ });
327
+ }
328
+ const signal = new AbortController();
329
+ let controller;
330
+ let active = true;
331
+ let bytesExposed = 0;
332
+ let result;
333
+ let eof = false;
334
+ let diagnosticBytes = 0;
335
+ let diagnosticTruncated = false;
336
+ let diagnosticFailed = false;
337
+ const preview = [];
338
+ const decoder = new TextDecoder();
339
+ let pending;
340
+ const read = () => pending ??= pump.read().then((value) => {
341
+ pending = void 0;
342
+ if (value?.done) eof = true;
343
+ if (!response.ok && value != null && !value.done && performance.now() - start < diagnosticTimeoutMs) {
344
+ const remaining = diagnosticMaxBytes - diagnosticBytes;
345
+ const chunk = value.value;
346
+ if (chunk.length > remaining) diagnosticTruncated = true;
347
+ if (remaining > 0 && chunk.length > 0) {
348
+ const selected = chunk.subarray(0, remaining);
349
+ preview.push(decoder.decode(selected, { stream: true }));
350
+ diagnosticBytes += selected.length;
351
+ }
352
+ }
353
+ return value;
354
+ }, (error) => {
355
+ pending = void 0;
356
+ diagnosticFailed = true;
357
+ throw error;
358
+ });
359
+ const revoke = (reason) => {
360
+ active = false;
361
+ if (!signal.signal.aborted) signal.abort(reason);
362
+ try {
363
+ controller.error(reason);
364
+ } catch {}
365
+ };
366
+ const body = new ReadableStream({
367
+ start(value) {
368
+ controller = value;
369
+ },
370
+ async pull() {
371
+ try {
372
+ while (active) {
373
+ if (performance.now() >= expires) {
374
+ result = { type: "deadline" };
375
+ revoke(new DOMException("Response body capture deadline exceeded.", "TimeoutError"));
376
+ return;
377
+ }
378
+ const value = await read();
379
+ if (!active) return;
380
+ if (performance.now() >= expires || value == null) {
381
+ result = { type: "deadline" };
382
+ revoke(new DOMException("Response body capture deadline exceeded.", "TimeoutError"));
383
+ return;
384
+ }
385
+ if (value.done) {
386
+ result = { type: "complete" };
387
+ active = false;
388
+ controller.close();
389
+ return;
390
+ }
391
+ if (value.value.length === 0) continue;
392
+ const chunk = value.value.slice(0, options.maxBytes - bytesExposed);
393
+ bytesExposed += chunk.length;
394
+ controller.enqueue(chunk);
395
+ if (bytesExposed === options.maxBytes) {
396
+ result = { type: "limit-reached" };
397
+ revoke(new DOMException("Response body capture byte limit reached.", "QuotaExceededError"));
398
+ }
399
+ return;
400
+ }
401
+ } catch (error) {
402
+ if (active) {
403
+ result = {
404
+ type: "failed",
405
+ phase: "body",
406
+ error
407
+ };
408
+ revoke(error);
409
+ }
410
+ }
411
+ },
412
+ cancel(reason) {
413
+ result = {
414
+ type: "consumer-stopped",
415
+ reason
416
+ };
417
+ revoke(reason);
418
+ }
419
+ }, { highWaterMark: 0 });
420
+ let timer;
421
+ const deadline = new Promise((resolve) => {
422
+ timer = setTimeout(() => {
423
+ result = { type: "deadline" };
424
+ revoke(new DOMException("Response body capture deadline exceeded.", "TimeoutError"));
425
+ resolve();
426
+ }, Math.max(0, expires - performance.now()));
427
+ });
428
+ const callback = Promise.resolve().then(() => onBody(body, signal.signal)).then(() => {}, (error) => {
429
+ if (result == null || result.type === "complete") result = {
430
+ type: "failed",
431
+ phase: "observer",
432
+ error
433
+ };
434
+ });
435
+ try {
436
+ await Promise.race([callback, deadline]);
437
+ clearTimeout(timer);
438
+ if (performance.now() >= expires) result = { type: "deadline" };
439
+ result ??= { type: "callback-returned" };
440
+ revoke(new DOMException("Response body capture finished.", "AbortError"));
441
+ if (!response.ok) {
442
+ pump.limitLifetime(diagnosticTimeoutMs - (performance.now() - start));
443
+ try {
444
+ while (!eof && !diagnosticTruncated && performance.now() - start < diagnosticTimeoutMs) if (await read() == null) break;
445
+ } catch {}
446
+ diagnostics.set(response, preview.join("") + (diagnosticTruncated ? "… (truncated)" : !eof && !diagnosticFailed ? "… (timed out)" : ""));
447
+ }
448
+ return Object.freeze({
449
+ ...result,
450
+ bytesExposed
451
+ });
452
+ } finally {
453
+ clearTimeout(timer);
454
+ pump.close();
455
+ }
456
+ }
457
+ //#endregion
458
+ //#region src/federation/outbound-headers.ts
459
+ const protectedHeaders = /* @__PURE__ */ new Set([
460
+ "authorization",
461
+ "proxy-authorization",
462
+ "cookie",
463
+ "set-cookie",
464
+ "signature",
465
+ "signature-input",
466
+ "authentication-info",
467
+ "proxy-authentication-info",
468
+ "x-api-key",
469
+ "x-auth-token"
470
+ ]);
471
+ function snapshotNames(names) {
472
+ if (!Array.isArray(names)) throw new TypeError("Outbound header names must be an array.");
473
+ const normalized = /* @__PURE__ */ new Set();
474
+ for (const name of names) {
475
+ if (typeof name !== "string" || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name) || name.includes("*")) throw new TypeError("Outbound header names must be literal HTTP tokens.");
476
+ normalized.add(name.toLowerCase());
477
+ }
478
+ return Object.freeze([...normalized].sort());
479
+ }
480
+ /** Validate/copy policy before registration replaces the selected observers. */
481
+ function snapshotHeaderPolicy(policy) {
482
+ if (policy === void 0) return void 0;
483
+ if (policy == null || typeof policy !== "object") throw new TypeError("Outbound header policy must be an object.");
484
+ const { allow, redact, maxBytes } = policy;
485
+ if (typeof maxBytes !== "number") throw new TypeError("Outbound header maxBytes must be a number.");
486
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("Outbound header maxBytes must be a positive safe integer.");
487
+ return Object.freeze({
488
+ allow: snapshotNames(allow),
489
+ ...redact === void 0 ? {} : { redact: snapshotNames(redact) },
490
+ maxBytes
491
+ });
492
+ }
493
+ /** Copy only selected/redacted entries; budgets never truncate a header value. */
494
+ function snapshotHeaders(headers, policy) {
495
+ const redact = /* @__PURE__ */ new Set([...protectedHeaders, ...policy.redact ?? []]);
496
+ const entries = [];
497
+ const encoder = new TextEncoder();
498
+ let bytes = 0;
499
+ let limitReached = false;
500
+ for (const name of policy.allow) {
501
+ if (!headers.has(name)) continue;
502
+ for (const [key, rawValue] of headers) {
503
+ if (key !== name) continue;
504
+ const value = redact.has(name) ? "[redacted]" : rawValue;
505
+ const size = encoder.encode(`${name}: ${value}\r\n`).byteLength;
506
+ if (size > policy.maxBytes - bytes) {
507
+ limitReached = true;
508
+ continue;
509
+ }
510
+ entries.push(Object.freeze([name, value]));
511
+ bytes += size;
512
+ }
513
+ }
514
+ return Object.freeze({
515
+ entries: Object.freeze(entries),
516
+ limitReached
517
+ });
518
+ }
519
+ //#endregion
26
520
  //#region src/federation/activity-listener.ts
27
521
  var ActivityListenerSet = class {
28
522
  #listeners;
@@ -108,6 +602,7 @@ var FederationBuilderImpl = class {
108
602
  inboxListeners;
109
603
  outboxListeners;
110
604
  inboxRequestFinishedHandler;
605
+ outboundObservers = globalThis.Object.freeze({});
111
606
  inboxErrorHandler;
112
607
  outboxListenerErrorHandler;
113
608
  outboxAuthorizePredicate;
@@ -170,6 +665,7 @@ var FederationBuilderImpl = class {
170
665
  f.inboxListeners = this.inboxListeners?.clone();
171
666
  f.outboxListeners = this.outboxListeners?.clone();
172
667
  f.inboxRequestFinishedHandler = this.inboxRequestFinishedHandler;
668
+ f.setOutboundObservers(this.outboundObservers);
173
669
  f.inboxErrorHandler = this.inboxErrorHandler;
174
670
  f.outboxListenerErrorHandler = this.outboxListenerErrorHandler;
175
671
  f.outboxAuthorizePredicate = this.outboxAuthorizePredicate;
@@ -317,6 +813,19 @@ var FederationBuilderImpl = class {
317
813
  };
318
814
  return setters;
319
815
  }
816
+ setOutboundObservers(observers) {
817
+ this.outboundObservers = globalThis.Object.freeze({
818
+ senderPayload: snapshotSenderPayloadOption(observers.senderPayload),
819
+ responseBodyCapture: snapshotBodyCapture(observers.responseBodyCapture),
820
+ requestHeaders: snapshotHeaderPolicy(observers.requestHeaders),
821
+ responseHeaders: snapshotHeaderPolicy(observers.responseHeaders),
822
+ onBroadcast: observers.onBroadcast,
823
+ onDelivery: observers.onDelivery,
824
+ onAttempt: observers.onAttempt,
825
+ onHttpExchange: observers.onHttpExchange,
826
+ onWorker: observers.onWorker
827
+ });
828
+ }
320
829
  setNodeInfoDispatcher(path, dispatcher) {
321
830
  if (this.router.has("nodeInfo")) throw new _fedify_uri_template.RouterError("NodeInfo dispatcher already set.");
322
831
  if (_fedify_uri_template.Router.variables(path).size !== 0) throw new _fedify_uri_template.RouterError("Path for NodeInfo dispatcher must have no variables.");
@@ -882,13 +1391,13 @@ function warnCompatibleActorId(actorId, gateway) {
882
1391
  }
883
1392
  //#endregion
884
1393
  //#region src/federation/circuit-breaker.ts
885
- const Temporal$11 = require_http.Temporal;
1394
+ const Temporal$12 = require_http.Temporal;
886
1395
  const LEGACY_SWEEP_MARKER = ["__fedify_meta", "circuit_breaker_state_ttl_sweep_v2"];
887
1396
  const LEGACY_SWEEP_DELETING_MARKER = { __fedifyDeletingCircuitBreakerLegacyState: true };
888
1397
  const CIRCUIT_BREAKER_STATE_VERSION = 1;
889
- const LEGACY_SWEEP_LOCK_TTL = Temporal$11.Duration.from({ minutes: 5 });
890
- const LEGACY_SWEEP_RENEW_INTERVAL = Temporal$11.Duration.from({ minutes: 1 });
891
- const LEGACY_SWEEP_RETRY_WINDOW = Temporal$11.Duration.from({ hours: 168 });
1398
+ const LEGACY_SWEEP_LOCK_TTL = Temporal$12.Duration.from({ minutes: 5 });
1399
+ const LEGACY_SWEEP_RENEW_INTERVAL = Temporal$12.Duration.from({ minutes: 1 });
1400
+ const LEGACY_SWEEP_RETRY_WINDOW = Temporal$12.Duration.from({ hours: 168 });
892
1401
  const LEGACY_SWEEP_WAIT_INTERVAL = 10;
893
1402
  const LEGACY_SWEEP_CAS_ATTEMPTS = 3;
894
1403
  /**
@@ -907,7 +1416,7 @@ var CircuitBreaker = class {
907
1416
  this.#kv = options.kv;
908
1417
  this.#prefix = options.prefix;
909
1418
  this.#options = normalizeCircuitBreakerOptions(options.options ?? {});
910
- this.#now = options.now ?? (() => Temporal$11.Now.instant());
1419
+ this.#now = options.now ?? (() => Temporal$12.Now.instant());
911
1420
  this.#stateChangeObserver = options.stateChangeObserver;
912
1421
  }
913
1422
  get options() {
@@ -921,7 +1430,7 @@ var CircuitBreaker = class {
921
1430
  this.#sweepLegacyStates();
922
1431
  const heldSince = parseHeldSince(message.circuitHeldSince);
923
1432
  const now = this.#now();
924
- if (heldSince != null && Temporal$11.Instant.compare(heldSince.add(this.#options.heldActivityTtl), now) <= 0) return {
1433
+ if (heldSince != null && Temporal$12.Instant.compare(heldSince.add(this.#options.heldActivityTtl), now) <= 0) return {
925
1434
  type: "drop",
926
1435
  heldSince
927
1436
  };
@@ -933,12 +1442,12 @@ var CircuitBreaker = class {
933
1442
  probe: false
934
1443
  };
935
1444
  if (oldState.state === "half-open") {
936
- const halfOpened = oldState.halfOpened == null ? void 0 : Temporal$11.Instant.from(oldState.halfOpened);
1445
+ const halfOpened = oldState.halfOpened == null ? void 0 : Temporal$12.Instant.from(oldState.halfOpened);
937
1446
  if (halfOpened != null) {
938
1447
  const staleAt = halfOpened.add(this.#options.recoveryDelay);
939
- if (Temporal$11.Instant.compare(now, staleAt) < 0) {
1448
+ if (Temporal$12.Instant.compare(now, staleAt) < 0) {
940
1449
  const releaseAt = now.add(this.#options.releaseInterval);
941
- const retryAt = Temporal$11.Instant.compare(releaseAt, staleAt) < 0 ? releaseAt : staleAt;
1450
+ const retryAt = Temporal$12.Instant.compare(releaseAt, staleAt) < 0 ? releaseAt : staleAt;
942
1451
  const cappedRetryAt = this.#capHeldRetryAt(now, heldSince, retryAt);
943
1452
  return {
944
1453
  type: "hold",
@@ -960,8 +1469,8 @@ var CircuitBreaker = class {
960
1469
  lastConflictingState = "half-open";
961
1470
  continue;
962
1471
  }
963
- const probeAt = (oldState.opened == null ? now : Temporal$11.Instant.from(oldState.opened)).add(this.#options.recoveryDelay);
964
- if (Temporal$11.Instant.compare(now, probeAt) < 0) {
1472
+ const probeAt = (oldState.opened == null ? now : Temporal$12.Instant.from(oldState.opened)).add(this.#options.recoveryDelay);
1473
+ if (Temporal$12.Instant.compare(now, probeAt) < 0) {
965
1474
  const retryAt = this.#capHeldRetryAt(now, heldSince, probeAt);
966
1475
  return {
967
1476
  type: "hold",
@@ -1026,7 +1535,7 @@ var CircuitBreaker = class {
1026
1535
  for (let attempt = 0; attempt < 10; attempt++) {
1027
1536
  const { state: oldState, raw: oldValue } = await this.#get(remoteHost);
1028
1537
  if (oldState?.state === "open") return void 0;
1029
- const oldFailures = oldState?.failures.map(Temporal$11.Instant.from) ?? [];
1538
+ const oldFailures = oldState?.failures.map(Temporal$12.Instant.from) ?? [];
1030
1539
  const failures = this.#options.pruneFailures([...oldFailures, now], now);
1031
1540
  let newState;
1032
1541
  let transition;
@@ -1110,7 +1619,7 @@ var CircuitBreaker = class {
1110
1619
  for await (const { key, value } of this.#kv.list(this.#prefix)) {
1111
1620
  if (key.length !== this.#prefix.length + 1) continue;
1112
1621
  await this.#migrateLegacyState(key, value);
1113
- if (Temporal$11.Instant.compare(this.#now(), leaseRenewedAt.add(LEGACY_SWEEP_RENEW_INTERVAL)) >= 0) {
1622
+ if (Temporal$12.Instant.compare(this.#now(), leaseRenewedAt.add(LEGACY_SWEEP_RENEW_INTERVAL)) >= 0) {
1114
1623
  const renewed = await this.#renewLegacySweep(markerKey, marker);
1115
1624
  if (renewed == null) return;
1116
1625
  marker = renewed;
@@ -1134,7 +1643,7 @@ var CircuitBreaker = class {
1134
1643
  }
1135
1644
  #finishLegacySweepMarker(marker) {
1136
1645
  const retryUntil = getLegacySweepRetryUntil(marker);
1137
- if (retryUntil != null && Temporal$11.Instant.compare(this.#now(), Temporal$11.Instant.from(retryUntil)) < 0) return {
1646
+ if (retryUntil != null && Temporal$12.Instant.compare(this.#now(), Temporal$12.Instant.from(retryUntil)) < 0) return {
1138
1647
  state: "done",
1139
1648
  retryUntil
1140
1649
  };
@@ -1177,7 +1686,7 @@ var CircuitBreaker = class {
1177
1686
  #isLegacySweepDone() {
1178
1687
  if (this.#legacySweepDone === "final") return true;
1179
1688
  if (this.#legacySweepDone == null) return false;
1180
- if (Temporal$11.Instant.compare(this.#now(), this.#legacySweepDone) < 0) return true;
1689
+ if (Temporal$12.Instant.compare(this.#now(), this.#legacySweepDone) < 0) return true;
1181
1690
  this.#legacySweepDone = void 0;
1182
1691
  return false;
1183
1692
  }
@@ -1187,7 +1696,7 @@ var CircuitBreaker = class {
1187
1696
  }
1188
1697
  #capHeldRetryAt(now, heldSince, retryAt) {
1189
1698
  const expiresAt = (heldSince ?? now).add(this.#options.heldActivityTtl);
1190
- return Temporal$11.Instant.compare(expiresAt, retryAt) < 0 ? expiresAt : retryAt;
1699
+ return Temporal$12.Instant.compare(expiresAt, retryAt) < 0 ? expiresAt : retryAt;
1191
1700
  }
1192
1701
  async #get(remoteHost) {
1193
1702
  const value = await this.#kv.get(this.#key(remoteHost));
@@ -1262,7 +1771,7 @@ function normalizeCircuitBreakerOptions(options) {
1262
1771
  assertPositiveDuration(releaseInterval, "releaseInterval");
1263
1772
  if (configuredStateTtl != null) {
1264
1773
  assertPositiveDuration(configuredStateTtl, "stateTtl");
1265
- if (Temporal$11.Duration.compare(configuredStateTtl, recoveryDelay) < 0) throw new RangeError("stateTtl must be at least recoveryDelay.");
1774
+ if (Temporal$12.Duration.compare(configuredStateTtl, recoveryDelay) < 0) throw new RangeError("stateTtl must be at least recoveryDelay.");
1266
1775
  }
1267
1776
  let failure;
1268
1777
  let pruneFailures;
@@ -1274,13 +1783,13 @@ function normalizeCircuitBreakerOptions(options) {
1274
1783
  assertPositiveDuration(failureWindow, "failureWindow");
1275
1784
  pruneFailures = (timestamps, now) => {
1276
1785
  const earliest = now.subtract(failureWindow);
1277
- return timestamps.filter((timestamp) => Temporal$11.Instant.compare(timestamp, earliest) >= 0).slice(-failureThreshold);
1786
+ return timestamps.filter((timestamp) => Temporal$12.Instant.compare(timestamp, earliest) >= 0).slice(-failureThreshold);
1278
1787
  };
1279
1788
  failure = (timestamps) => {
1280
1789
  if (timestamps.length < failureThreshold) return false;
1281
1790
  const first = timestamps[timestamps.length - failureThreshold];
1282
1791
  const last = timestamps[timestamps.length - 1];
1283
- return Temporal$11.Duration.compare(first.until(last), failureWindow) <= 0;
1792
+ return Temporal$12.Duration.compare(first.until(last), failureWindow) <= 0;
1284
1793
  };
1285
1794
  stateTtl = configuredStateTtl ?? maxDuration(recoveryDelay.add(failureWindow), recoveryDelay.add(heldActivityTtl));
1286
1795
  } else {
@@ -1300,7 +1809,7 @@ function normalizeCircuitBreakerOptions(options) {
1300
1809
  };
1301
1810
  }
1302
1811
  function maxDuration(duration, ...durations) {
1303
- return durations.reduce((max, candidate) => Temporal$11.Duration.compare(candidate, max) > 0 ? candidate : max, duration);
1812
+ return durations.reduce((max, candidate) => Temporal$12.Duration.compare(candidate, max) > 0 ? candidate : max, duration);
1304
1813
  }
1305
1814
  function isLegacySweepDone(value, now) {
1306
1815
  return getLegacySweepDoneUntil(value, now) != null;
@@ -1309,8 +1818,8 @@ function getLegacySweepDoneUntil(value, now) {
1309
1818
  if (typeof value !== "object" || value == null || !("state" in value)) return;
1310
1819
  if (value.state === "final") return "final";
1311
1820
  if (value.state === "done" && "retryUntil" in value && typeof value.retryUntil === "string") try {
1312
- const retryUntil = Temporal$11.Instant.from(value.retryUntil);
1313
- return Temporal$11.Instant.compare(now, retryUntil) < 0 ? retryUntil : void 0;
1821
+ const retryUntil = Temporal$12.Instant.from(value.retryUntil);
1822
+ return Temporal$12.Instant.compare(now, retryUntil) < 0 ? retryUntil : void 0;
1314
1823
  } catch {
1315
1824
  return;
1316
1825
  }
@@ -1318,7 +1827,7 @@ function getLegacySweepDoneUntil(value, now) {
1318
1827
  function isLegacySweepRetryDue(value, now) {
1319
1828
  if (typeof value !== "object" || value == null || !("state" in value) || value.state !== "done" || !("retryUntil" in value) || typeof value.retryUntil !== "string") return false;
1320
1829
  try {
1321
- return Temporal$11.Instant.compare(now, Temporal$11.Instant.from(value.retryUntil)) >= 0;
1830
+ return Temporal$12.Instant.compare(now, Temporal$12.Instant.from(value.retryUntil)) >= 0;
1322
1831
  } catch {
1323
1832
  return false;
1324
1833
  }
@@ -1329,7 +1838,7 @@ function isLegacySweepActive(value, now) {
1329
1838
  function isLegacySweepInProgress(value, now) {
1330
1839
  if (typeof value !== "object" || value == null || !("state" in value) || value.state !== "sweeping" || !("started" in value) || typeof value.started !== "string") return false;
1331
1840
  try {
1332
- return Temporal$11.Instant.compare(now, Temporal$11.Instant.from(value.started).add(LEGACY_SWEEP_LOCK_TTL)) < 0;
1841
+ return Temporal$12.Instant.compare(now, Temporal$12.Instant.from(value.started).add(LEGACY_SWEEP_LOCK_TTL)) < 0;
1333
1842
  } catch {
1334
1843
  return false;
1335
1844
  }
@@ -1337,7 +1846,7 @@ function isLegacySweepInProgress(value, now) {
1337
1846
  function getLegacySweepRetryUntil(value) {
1338
1847
  if (typeof value !== "object" || value == null || !("retryUntil" in value) || typeof value.retryUntil !== "string") return;
1339
1848
  try {
1340
- Temporal$11.Instant.from(value.retryUntil);
1849
+ Temporal$12.Instant.from(value.retryUntil);
1341
1850
  return value.retryUntil;
1342
1851
  } catch {
1343
1852
  return;
@@ -1361,19 +1870,19 @@ function delay(milliseconds) {
1361
1870
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
1362
1871
  }
1363
1872
  function toInstantDuration(duration) {
1364
- const parsed = Temporal$11.Duration.from(duration);
1365
- return Temporal$11.Duration.from({ milliseconds: Math.trunc(parsed.total({
1873
+ const parsed = Temporal$12.Duration.from(duration);
1874
+ return Temporal$12.Duration.from({ milliseconds: Math.trunc(parsed.total({
1366
1875
  unit: "millisecond",
1367
- relativeTo: Temporal$11.PlainDateTime.from("2026-01-01T00:00:00")
1876
+ relativeTo: Temporal$12.PlainDateTime.from("2026-01-01T00:00:00")
1368
1877
  })) });
1369
1878
  }
1370
1879
  function assertPositiveDuration(duration, name) {
1371
- if (Temporal$11.Duration.compare(duration, { seconds: 0 }) <= 0) throw new RangeError(`${name} must be a positive duration.`);
1880
+ if (Temporal$12.Duration.compare(duration, { seconds: 0 }) <= 0) throw new RangeError(`${name} must be a positive duration.`);
1372
1881
  }
1373
1882
  function parseHeldSince(value) {
1374
1883
  if (value == null) return void 0;
1375
1884
  try {
1376
- return Temporal$11.Instant.from(value);
1885
+ return Temporal$12.Instant.from(value);
1377
1886
  } catch (error) {
1378
1887
  (0, _logtape_logtape.getLogger)([
1379
1888
  "fedify",
@@ -1400,7 +1909,7 @@ function parseStoredCircuitBreakerKvState(value) {
1400
1909
  const isInstantString = (v) => {
1401
1910
  if (typeof v !== "string") return false;
1402
1911
  try {
1403
- Temporal$11.Instant.from(v);
1912
+ Temporal$12.Instant.from(v);
1404
1913
  return true;
1405
1914
  } catch {
1406
1915
  return false;
@@ -1464,6 +1973,7 @@ async function buildCollectionSynchronizationHeader(collectionId, actorIds) {
1464
1973
  /** Internal state owned by the ingress boundary, never serialized into queues. */
1465
1974
  var InboxObservation = class {
1466
1975
  inbox;
1976
+ source;
1467
1977
  payload = { status: "unavailable" };
1468
1978
  activity = null;
1469
1979
  authentication = { status: "notDetermined" };
@@ -1486,8 +1996,9 @@ var InboxObservation = class {
1486
1996
  }
1487
1997
  }
1488
1998
  };
1489
- constructor(inbox) {
1999
+ constructor(inbox, source = void 0) {
1490
2000
  this.inbox = inbox;
2001
+ this.source = source;
1491
2002
  }
1492
2003
  /** Run an ingress operation and project telemetry; fetch owns completion. */
1493
2004
  async run(operation, span) {
@@ -1543,7 +2054,18 @@ var InboxObservation = class {
1543
2054
  }
1544
2055
  async finish(context, handler, outcome) {
1545
2056
  if (handler == null) return;
2057
+ const signal = context.request.signal;
2058
+ const requestCancellation = signal.aborted ? Object.freeze({
2059
+ type: "aborted",
2060
+ reason: signal.reason
2061
+ }) : Object.freeze({ type: "not-aborted" });
1546
2062
  const report = {
2063
+ ...this.source == null ? {} : {
2064
+ inboxRequestId: this.source.inboxRequestId,
2065
+ requestId: this.source.requestId,
2066
+ local: Object.freeze({ ...this.source.context })
2067
+ },
2068
+ requestCancellation,
1547
2069
  inbox: this.inbox,
1548
2070
  payload: this.payload,
1549
2071
  activity: this.activity,
@@ -1552,7 +2074,7 @@ var InboxObservation = class {
1552
2074
  outcome
1553
2075
  };
1554
2076
  try {
1555
- await handler(context, report);
2077
+ await (0, _logtape_logtape.withContext)(this.source == null ? {} : { inboxRequestId: this.source.inboxRequestId }, () => handler(context, report));
1556
2078
  } catch (error) {
1557
2079
  try {
1558
2080
  (0, _logtape_logtape.getLogger)([
@@ -2814,6 +3336,92 @@ async function isInAudience(object, actor, options = {}) {
2814
3336
  return false;
2815
3337
  }
2816
3338
  //#endregion
3339
+ //#region src/federation/outbound-provenance.ts
3340
+ const outboundProvenance = Symbol("outboundProvenance");
3341
+ const emptyProvenance = Object.freeze({});
3342
+ function getOutboundProvenance(context) {
3343
+ return context[outboundProvenance] ?? emptyProvenance;
3344
+ }
3345
+ function snapshotLocalContext(context) {
3346
+ return Object.freeze({
3347
+ origin: context.origin,
3348
+ host: context.host,
3349
+ hostname: context.hostname,
3350
+ canonicalOrigin: context.canonicalOrigin
3351
+ });
3352
+ }
3353
+ function createInboxRequestSource(request, requestId, canonicalOrigin) {
3354
+ const url = new URL(request.url);
3355
+ return Object.freeze({
3356
+ inboxRequestId: crypto.randomUUID(),
3357
+ requestId,
3358
+ context: snapshotLocalContext({
3359
+ origin: url.origin,
3360
+ host: url.host,
3361
+ hostname: url.hostname,
3362
+ canonicalOrigin: canonicalOrigin ?? url.origin
3363
+ })
3364
+ });
3365
+ }
3366
+ /** The same exact-string budget applies to public input and queued metadata. */
3367
+ function validateDispatchId(value) {
3368
+ if (value === void 0) return;
3369
+ if (typeof value !== "string") throw new TypeError("dispatchId must be a string.");
3370
+ if (value.length > 1024 || new TextEncoder().encode(value).byteLength > 1024) throw new RangeError("dispatchId must not exceed 1,024 UTF-8 bytes.");
3371
+ }
3372
+ /** A new public invocation keeps its HTTP source, with its own dispatch ID. */
3373
+ function invocationProvenance(context, dispatchId) {
3374
+ validateDispatchId(dispatchId);
3375
+ const { dispatchId: _previous, ...source } = getOutboundProvenance(context);
3376
+ return Object.freeze({
3377
+ ...source,
3378
+ ...dispatchId === void 0 ? {} : { dispatchId }
3379
+ });
3380
+ }
3381
+ /** Restore only explicit source facts; never infer them from queue/trace IDs. */
3382
+ function restoreProvenance(value) {
3383
+ if (value === void 0) return emptyProvenance;
3384
+ const record = (v) => v != null && typeof v === "object" && !Array.isArray(v);
3385
+ const invalid = () => /* @__PURE__ */ new TypeError("Invalid provenance in queue metadata.");
3386
+ if (!record(value)) throw invalid();
3387
+ validateDispatchId(value.dispatchId);
3388
+ if (value.originatingRequestId !== void 0 && typeof value.originatingRequestId !== "string") throw invalid();
3389
+ let inboxRequest;
3390
+ if (value.inboxRequest !== void 0) {
3391
+ const source = value.inboxRequest;
3392
+ if (!record(source) || typeof source.inboxRequestId !== "string" || source.inboxRequestId.length < 1 || typeof source.requestId !== "string" || !record(source.context)) throw invalid();
3393
+ const facts = source.context;
3394
+ if (typeof facts.origin !== "string" || typeof facts.host !== "string" || typeof facts.hostname !== "string" || typeof facts.canonicalOrigin !== "string") throw invalid();
3395
+ inboxRequest = Object.freeze({
3396
+ inboxRequestId: source.inboxRequestId,
3397
+ requestId: source.requestId,
3398
+ context: Object.freeze({
3399
+ origin: facts.origin,
3400
+ host: facts.host,
3401
+ hostname: facts.hostname,
3402
+ canonicalOrigin: facts.canonicalOrigin
3403
+ })
3404
+ });
3405
+ }
3406
+ return Object.freeze({
3407
+ ...value.dispatchId === void 0 ? {} : { dispatchId: value.dispatchId },
3408
+ ...inboxRequest == null ? {} : { inboxRequest },
3409
+ ...value.originatingRequestId === void 0 ? {} : { originatingRequestId: value.originatingRequestId }
3410
+ });
3411
+ }
3412
+ /** Omit absent provenance on legacy and independent producers. */
3413
+ function serializeProvenance(value) {
3414
+ const provenance = restoreProvenance(value);
3415
+ return provenance.dispatchId === void 0 && provenance.inboxRequest == null && provenance.originatingRequestId == null ? {} : { provenance };
3416
+ }
3417
+ function sourceLogContext(source) {
3418
+ return {
3419
+ dispatchId: source.dispatchId,
3420
+ sourceInboxRequestId: source.inboxRequest?.inboxRequestId,
3421
+ sourceRequestId: source.inboxRequest?.requestId ?? source.originatingRequestId
3422
+ };
3423
+ }
3424
+ //#endregion
2817
3425
  //#region src/federation/inbox-authentication.ts
2818
3426
  /**
2819
3427
  * The evidence of an activity that has not been authenticated by Fedify.
@@ -3008,7 +3616,7 @@ function createAuthenticationEvidence(evidence) {
3008
3616
  }
3009
3617
  //#endregion
3010
3618
  //#region src/federation/inbox.ts
3011
- const Temporal$10 = require_http.Temporal;
3619
+ const Temporal$11 = require_http.Temporal;
3012
3620
  /**
3013
3621
  * Checks whether an activity's ID and all of its actor IDs have the same
3014
3622
  * [FEP-fe34] origin. Activities without an ID have no origin to authenticate.
@@ -3188,6 +3796,7 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
3188
3796
  _opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), carrier);
3189
3797
  message = {
3190
3798
  type: "inbox",
3799
+ ...serializeProvenance(getOutboundProvenance(ctx)),
3191
3800
  id: crypto.randomUUID(),
3192
3801
  baseUrl: ctx.origin,
3193
3802
  activity: originalJson ?? json,
@@ -3298,7 +3907,7 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
3298
3907
  return "error";
3299
3908
  }
3300
3909
  require_http.recordInboxActivity(meterProvider, "processed", (0, _fedify_vocab.getTypeId)(activity).href);
3301
- if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal$10.Duration.from({ days: 1 }) });
3910
+ if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal$11.Duration.from({ days: 1 }) });
3302
3911
  logger.info("Activity {activityId} has been processed.", {
3303
3912
  activityId: activity.id?.href,
3304
3913
  activity: json,
@@ -3686,7 +4295,7 @@ async function restoreVerifiedActivityView(view, message) {
3686
4295
  }
3687
4296
  //#endregion
3688
4297
  //#region src/federation/keycache.ts
3689
- const Temporal$9 = require_http.Temporal;
4298
+ const Temporal$10 = require_http.Temporal;
3690
4299
  const KEY_CACHE_GENERATION = "3";
3691
4300
  const COMPATIBLE_KEY_SEGMENT = "__compatible";
3692
4301
  const MAX_OWNER_BYTES = 32 * 1024;
@@ -3707,8 +4316,8 @@ var KvKeyCache = class {
3707
4316
  this.kv = kv;
3708
4317
  this.prefix = prefix;
3709
4318
  this.options = options;
3710
- this.unavailableKeyTtl = options.unavailableKeyTtl ?? Temporal$9.Duration.from({ minutes: 10 });
3711
- this.keyTtl = options.keyTtl ?? Temporal$9.Duration.from({ days: 30 });
4319
+ this.unavailableKeyTtl = options.unavailableKeyTtl ?? Temporal$10.Duration.from({ minutes: 10 });
4320
+ this.keyTtl = options.keyTtl ?? Temporal$10.Duration.from({ days: 30 });
3712
4321
  this.nullKeys = /* @__PURE__ */ new Map();
3713
4322
  }
3714
4323
  #getFetchErrorKey(keyId) {
@@ -3728,13 +4337,13 @@ var KvKeyCache = class {
3728
4337
  async get(keyId) {
3729
4338
  const negativeExpiration = this.nullKeys.get(keyId.href);
3730
4339
  if (negativeExpiration != null) {
3731
- if (Temporal$9.Now.instant().until(negativeExpiration).sign >= 0) return null;
4340
+ if (Temporal$10.Now.instant().until(negativeExpiration).sign >= 0) return null;
3732
4341
  this.nullKeys.delete(keyId.href);
3733
4342
  }
3734
4343
  const serialized = await this.kv.get(this.#entryKey(keyId));
3735
4344
  if (serialized === void 0) return void 0;
3736
4345
  if (serialized === null) {
3737
- this.nullKeys.set(keyId.href, Temporal$9.Now.instant().add(this.unavailableKeyTtl));
4346
+ this.nullKeys.set(keyId.href, Temporal$10.Now.instant().add(this.unavailableKeyTtl));
3738
4347
  return null;
3739
4348
  }
3740
4349
  try {
@@ -3750,7 +4359,7 @@ var KvKeyCache = class {
3750
4359
  }
3751
4360
  async set(keyId, key) {
3752
4361
  if (key == null) {
3753
- this.nullKeys.set(keyId.href, Temporal$9.Now.instant().add(this.unavailableKeyTtl));
4362
+ this.nullKeys.set(keyId.href, Temporal$10.Now.instant().add(this.unavailableKeyTtl));
3754
4363
  await this.kv.set(this.#entryKey(keyId), null, { ttl: this.unavailableKeyTtl });
3755
4364
  return;
3756
4365
  }
@@ -3769,7 +4378,7 @@ var KvKeyCache = class {
3769
4378
  * @internal
3770
4379
  */
3771
4380
  compatibleKeyScope(scope) {
3772
- const now = this.options.now ?? (() => Temporal$9.Now.instant());
4381
+ const now = this.options.now ?? (() => Temporal$10.Now.instant());
3773
4382
  const entryKey = (keyId) => [
3774
4383
  ...this.prefix,
3775
4384
  COMPATIBLE_KEY_SEGMENT,
@@ -3789,7 +4398,7 @@ var KvKeyCache = class {
3789
4398
  if (entry.key === null) return null;
3790
4399
  let expires;
3791
4400
  try {
3792
- expires = Temporal$9.Instant.fromEpochMilliseconds(entry.expires);
4401
+ expires = Temporal$10.Instant.fromEpochMilliseconds(entry.expires);
3793
4402
  } catch (error) {
3794
4403
  if (!(error instanceof RangeError)) throw error;
3795
4404
  await this.kv.delete(entryKey(keyId));
@@ -3834,8 +4443,8 @@ var KvKeyCache = class {
3834
4443
  key: serialized,
3835
4444
  expires,
3836
4445
  ...owner == null ? {} : { owner }
3837
- }, { ttl: Temporal$9.Duration.from({ milliseconds: ttl }) });
3838
- return key == null ? void 0 : Temporal$9.Instant.fromEpochMilliseconds(expires);
4446
+ }, { ttl: Temporal$10.Duration.from({ milliseconds: ttl }) });
4447
+ return key == null ? void 0 : Temporal$10.Instant.fromEpochMilliseconds(expires);
3839
4448
  },
3840
4449
  delete: async (keyId) => {
3841
4450
  await this.kv.delete(entryKey(keyId));
@@ -4353,11 +4962,15 @@ function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
4353
4962
  * @throws {Error} If the activity fails to send.
4354
4963
  */
4355
4964
  function sendActivity(options, deps = {}) {
4965
+ options.attemptStarted?.(options.inbox.href);
4966
+ const attempt = options.observation?.startAttempt(options.inbox.href);
4967
+ const parentSpanContext = _opentelemetry_api.trace.getSpanContext(_opentelemetry_api.context.active());
4356
4968
  const tracerProvider = options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider();
4357
4969
  return tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan("activitypub.send_activity", {
4358
4970
  kind: _opentelemetry_api.SpanKind.CLIENT,
4359
4971
  attributes: { "activitypub.shared_inbox": options.sharedInbox ?? false }
4360
4972
  }, async (span) => {
4973
+ attempt?.setSpan(span, parentSpanContext);
4361
4974
  if (options.activityId != null) span.setAttribute("activitypub.activity.id", options.activityId);
4362
4975
  if (options.activityType != null) span.setAttribute("activitypub.activity.type", options.activityType);
4363
4976
  const spanContext = span.spanContext();
@@ -4368,12 +4981,14 @@ function sendActivity(options, deps = {}) {
4368
4981
  }, () => sendActivityInternal({
4369
4982
  ...options,
4370
4983
  tracerProvider
4371
- }, span, deps));
4984
+ }, span, deps, attempt));
4985
+ await attempt?.succeeded();
4372
4986
  } catch (e) {
4373
4987
  span.setStatus({
4374
4988
  code: _opentelemetry_api.SpanStatusCode.ERROR,
4375
4989
  message: String(e)
4376
4990
  });
4991
+ await attempt?.failed(e);
4377
4992
  throw e;
4378
4993
  } finally {
4379
4994
  span.end();
@@ -4413,6 +5028,7 @@ async function sendActivityThroughGateways(parameters, inboxes) {
4413
5028
  throw lastError;
4414
5029
  }
4415
5030
  const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
5031
+ const ERROR_RESPONSE_BODY_TIMEOUT_MS = 1e3;
4416
5032
  function getActivityActorId(activity) {
4417
5033
  if (!isRecord(activity)) return void 0;
4418
5034
  return getIdValue(activity.actor);
@@ -4433,16 +5049,25 @@ function isRecord(value) {
4433
5049
  return typeof value === "object" && value != null;
4434
5050
  }
4435
5051
  async function readLimitedResponseBody(response, maxBytes) {
5052
+ const captured = takeCapturedDiagnostic(response);
5053
+ if (captured !== void 0) return captured;
4436
5054
  if (response.body == null) return "";
4437
- const reader = response.body.getReader();
5055
+ const pump = new ResponseBodyPump(response.body, ERROR_RESPONSE_BODY_TIMEOUT_MS);
4438
5056
  const decoder = new TextDecoder();
4439
5057
  const chunks = [];
4440
5058
  let totalBytes = 0;
4441
5059
  let truncated = false;
5060
+ let timedOut = false;
4442
5061
  try {
4443
5062
  while (true) {
4444
- const { done, value } = await reader.read();
5063
+ const result = await pump.read();
5064
+ if (result == null) {
5065
+ timedOut = true;
5066
+ break;
5067
+ }
5068
+ const { done, value } = result;
4445
5069
  if (done) break;
5070
+ if (value.length === 0) continue;
4446
5071
  if (totalBytes + value.length > maxBytes) {
4447
5072
  const remaining = maxBytes - totalBytes;
4448
5073
  if (remaining > 0) chunks.push(decoder.decode(value.slice(0, remaining), { stream: true }));
@@ -4453,14 +5078,14 @@ async function readLimitedResponseBody(response, maxBytes) {
4453
5078
  totalBytes += value.length;
4454
5079
  }
4455
5080
  } finally {
4456
- if (truncated) reader.cancel().catch(() => {});
4457
- reader.releaseLock();
5081
+ pump.close();
4458
5082
  }
4459
5083
  let result = chunks.join("");
4460
5084
  if (truncated) result += "… (truncated)";
5085
+ else if (timedOut) result += "… (timed out)";
4461
5086
  return result;
4462
5087
  }
4463
- async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span, deps) {
5088
+ async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span, deps, attempt) {
4464
5089
  const logger = (0, _logtape_logtape.getLogger)([
4465
5090
  "fedify",
4466
5091
  "federation",
@@ -4476,6 +5101,11 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
4476
5101
  session = await openUnvalidatedHop(url);
4477
5102
  } catch (error) {
4478
5103
  if (error instanceof _fedify_vocab_runtime.UrlError && error.reason === "dns") {
5104
+ attempt?.failure({
5105
+ kind: "transport",
5106
+ phase: "validation",
5107
+ error
5108
+ });
4479
5109
  logger.error("DNS resolution failed for URL: {url}", {
4480
5110
  url,
4481
5111
  error
@@ -4484,13 +5114,47 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
4484
5114
  failure.cause = error;
4485
5115
  throw failure;
4486
5116
  }
4487
- if (error instanceof _fedify_vocab_runtime.UrlError) logger.error("Disallowed private URL: {url}", {
4488
- url,
4489
- error
4490
- });
5117
+ if (error instanceof _fedify_vocab_runtime.UrlError) {
5118
+ if (error.reason === "disallowed") attempt?.failure({
5119
+ kind: "policy",
5120
+ phase: "validation",
5121
+ error
5122
+ });
5123
+ logger.error("Disallowed private URL: {url}", {
5124
+ url,
5125
+ error
5126
+ });
5127
+ }
4491
5128
  throw error;
4492
5129
  }
4493
- return session;
5130
+ if (attempt == null) return session;
5131
+ return {
5132
+ url: session.url,
5133
+ async fetch(input, init) {
5134
+ try {
5135
+ return await session.fetch(input, init);
5136
+ } catch (error) {
5137
+ attempt.failure({
5138
+ kind: "transport",
5139
+ phase: "request",
5140
+ error
5141
+ });
5142
+ throw error;
5143
+ }
5144
+ },
5145
+ async close() {
5146
+ try {
5147
+ await session.close();
5148
+ } catch (error) {
5149
+ attempt.failure({
5150
+ kind: "local",
5151
+ phase: "cleanup",
5152
+ error
5153
+ });
5154
+ throw error;
5155
+ }
5156
+ }
5157
+ };
4494
5158
  }
4495
5159
  let initialSession;
4496
5160
  try {
@@ -4508,14 +5172,45 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
4508
5172
  return openHop(url);
4509
5173
  };
4510
5174
  try {
4511
- headers = new Headers(headers);
4512
- headers.set("Content-Type", "application/activity+json");
4513
- const request = new Request(inbox, {
4514
- method: "POST",
4515
- headers,
4516
- body: JSON.stringify(activity),
4517
- redirect: "manual"
4518
- });
5175
+ let request;
5176
+ let body;
5177
+ try {
5178
+ headers = new Headers(headers);
5179
+ headers.set("Content-Type", "application/activity+json");
5180
+ } catch (error) {
5181
+ attempt?.failure({
5182
+ kind: "local",
5183
+ phase: "request-preparation",
5184
+ error
5185
+ });
5186
+ throw error;
5187
+ }
5188
+ try {
5189
+ attempt?.snapshotSenderPayload(activity);
5190
+ body = JSON.stringify(activity);
5191
+ } catch (error) {
5192
+ attempt?.failure({
5193
+ kind: "local",
5194
+ phase: "serialization",
5195
+ error
5196
+ });
5197
+ throw error;
5198
+ }
5199
+ try {
5200
+ request = new Request(inbox, {
5201
+ method: "POST",
5202
+ headers,
5203
+ body,
5204
+ redirect: "manual"
5205
+ });
5206
+ } catch (error) {
5207
+ attempt?.failure({
5208
+ kind: "local",
5209
+ phase: "request-preparation",
5210
+ error
5211
+ });
5212
+ throw error;
5213
+ }
4519
5214
  let rsaKey = null;
4520
5215
  for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
4521
5216
  rsaKey = key;
@@ -4532,13 +5227,25 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
4532
5227
  privateKey: pair.privateKey
4533
5228
  }))
4534
5229
  });
5230
+ const consume = (response) => {
5231
+ return consumeSendActivityResponse(response, inbox);
5232
+ };
4535
5233
  let outcome;
4536
5234
  try {
4537
- outcome = rsaKey == null ? await fetchWithValidatedRedirects(request, openDeliveryHop, (response) => consumeSendActivityResponse(response, inbox)) : await require_http.doubleKnockScoped(request, rsaKey, {
5235
+ outcome = rsaKey == null ? await fetchWithValidatedRedirects(request, openDeliveryHop, consume, attempt) : await require_http.doubleKnockScoped(request, rsaKey, {
4538
5236
  tracerProvider,
4539
5237
  specDeterminer
4540
- }, openDeliveryHop, (response) => consumeSendActivityResponse(response, inbox));
5238
+ }, openDeliveryHop, consume, attempt == null ? void 0 : {
5239
+ fetch: (request, submit) => attempt.fetch(request, submit),
5240
+ response: (response, disposition) => attempt.response(response, disposition),
5241
+ error: (phase, error) => attempt.failure({
5242
+ kind: "local",
5243
+ phase,
5244
+ error
5245
+ })
5246
+ });
4541
5247
  } catch (error) {
5248
+ attempt?.unknown(error);
4542
5249
  const failure = error instanceof _fedify_vocab_runtime.FetchError || error instanceof _fedify_vocab_runtime.UrlError ? error : createFetchError(inbox.href, error);
4543
5250
  logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
4544
5251
  activityId,
@@ -4558,7 +5265,13 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
4558
5265
  statusText,
4559
5266
  error
4560
5267
  });
4561
- throw new SendActivityError(inbox, status, `Failed to send activity ${activityId} to ${inbox.href} (${status} ${statusText}):\n${error}`, error, responseHeaders);
5268
+ const failure = new SendActivityError(inbox, status, `Failed to send activity ${activityId} to ${inbox.href} (${status} ${statusText}):\n${error}`, error, responseHeaders);
5269
+ attempt?.failure({
5270
+ kind: "http",
5271
+ phase: "response",
5272
+ error: failure
5273
+ });
5274
+ throw failure;
4562
5275
  }
4563
5276
  deliverySuccess = true;
4564
5277
  const eventAttributes = {
@@ -4602,7 +5315,7 @@ function cancelResponseBody(response, url) {
4602
5315
  /**
4603
5316
  * Reads what is needed from the final response of a delivery while its
4604
5317
  * session is open: the body of a successful response is cancelled, and at
4605
- * most 1 KiB of an error response body is read.
5318
+ * most 1 KiB of an error response body is read within its diagnostic budget.
4606
5319
  */
4607
5320
  async function consumeSendActivityResponse(response, inbox) {
4608
5321
  if (response.ok) {
@@ -4632,9 +5345,10 @@ async function consumeSendActivityResponse(response, inbox) {
4632
5345
  * @param request The request to send.
4633
5346
  * @param openHop The function to open a session for each hop.
4634
5347
  * @param consume The function to consume the final response.
5348
+ * @param attempt The in-process attempt observation, if registered.
4635
5349
  * @returns The value returned by `consume`.
4636
5350
  */
4637
- async function fetchWithValidatedRedirects(request, openHop, consume) {
5351
+ async function fetchWithValidatedRedirects(request, openHop, consume, attempt) {
4638
5352
  const logger = (0, _logtape_logtape.getLogger)([
4639
5353
  "fedify",
4640
5354
  "federation",
@@ -4645,7 +5359,7 @@ async function fetchWithValidatedRedirects(request, openHop, consume) {
4645
5359
  const session = await openHop(request.url);
4646
5360
  let response;
4647
5361
  try {
4648
- response = await session.fetch(request, { redirect: "manual" });
5362
+ response = await (attempt == null ? session.fetch(request, { redirect: "manual" }) : attempt.fetch(request, () => session.fetch(request, { redirect: "manual" })));
4649
5363
  } catch (error) {
4650
5364
  await closeAfterError(session, logger);
4651
5365
  throw error;
@@ -4657,17 +5371,21 @@ async function fetchWithValidatedRedirects(request, openHop, consume) {
4657
5371
  303,
4658
5372
  307,
4659
5373
  308
4660
- ].includes(response.status) || location == null) try {
4661
- return await consume(response);
4662
- } catch (error) {
4663
- logger.debug("Failed to consume the response from {url}; closing the session: {error}", {
4664
- url: request.url,
4665
- error
4666
- });
4667
- throw error;
4668
- } finally {
4669
- await session.close();
5374
+ ].includes(response.status) || location == null) {
5375
+ attempt?.response(response, "final");
5376
+ try {
5377
+ return await consume(response);
5378
+ } catch (error) {
5379
+ logger.debug("Failed to consume the response from {url}; closing the session: {error}", {
5380
+ url: request.url,
5381
+ error
5382
+ });
5383
+ throw error;
5384
+ } finally {
5385
+ await session.close();
5386
+ }
4670
5387
  }
5388
+ attempt?.response(response, "intermediate");
4671
5389
  cancelResponseBody(response, request.url);
4672
5390
  await session.close();
4673
5391
  if (redirects >= 20) throw new TypeError("Too many redirects");
@@ -4728,7 +5446,9 @@ var SendActivityError = class extends Error {
4728
5446
  * The response body from the inbox, if any. Note that this may be
4729
5447
  * truncated to a maximum of 1 KiB to prevent excessive memory consumption
4730
5448
  * when remote servers return large error pages (e.g., Cloudflare error pages).
4731
- * If truncated, the string will end with `"… (truncated)"`.
5449
+ * If truncated, the string will end with `"… (truncated)"`. Diagnostic reading
5450
+ * has a separate one-second budget; a deadline retains any partial preview and
5451
+ * appends `"… (timed out)"`. This is independent of application body capture.
4732
5452
  */
4733
5453
  responseBody;
4734
5454
  /**
@@ -4755,7 +5475,7 @@ var SendActivityError = class extends Error {
4755
5475
  };
4756
5476
  //#endregion
4757
5477
  //#region src/federation/portable-inbox.ts
4758
- const Temporal$8 = require_http.Temporal;
5478
+ const Temporal$9 = require_http.Temporal;
4759
5479
  const MAX_TIMEOUT_MS = 2 ** 31 - 1;
4760
5480
  /**
4761
5481
  * Fills in the defaults of {@link PortableInboxForwardingOptions} and
@@ -4771,9 +5491,9 @@ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
4771
5491
  function resolvePortableInboxForwardingOptions(options = {}) {
4772
5492
  const maxTargets = options.maxTargets ?? 10;
4773
5493
  if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
4774
- const ttl = Temporal$8.Duration.from(options.ttl ?? { days: 30 });
5494
+ const ttl = Temporal$9.Duration.from(options.ttl ?? { days: 30 });
4775
5495
  if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
4776
- const deadline = Temporal$8.Duration.from(options.deadline ?? { seconds: 10 });
5496
+ const deadline = Temporal$9.Duration.from(options.deadline ?? { seconds: 10 });
4777
5497
  const deadlineMs = deadline.total("millisecond");
4778
5498
  if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
4779
5499
  return {
@@ -4883,9 +5603,10 @@ function coversDelivery(signature) {
4883
5603
  * other gateways, that gateway has the activity already, so it is claimed
4884
5604
  * without forwarding the activity to it. See {@link identifySendingGateway}.
4885
5605
  * @param parameters The parameters.
5606
+ * @param observation The in-process producer observation, if registered.
4886
5607
  * @returns The target inbox URLs that the activity was handed off to.
4887
5608
  */
4888
- async function forwardPortableInboxActivity(parameters) {
5609
+ async function forwardPortableInboxActivity(parameters, observation) {
4889
5610
  const logger = (0, _logtape_logtape.getLogger)([
4890
5611
  "fedify",
4891
5612
  "federation",
@@ -4894,8 +5615,16 @@ async function forwardPortableInboxActivity(parameters) {
4894
5615
  const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
4895
5616
  const options = parameters.options ?? resolvePortableInboxForwardingOptions();
4896
5617
  const { maxTargets, ttl } = options;
4897
- if (kv.cas == null || maxTargets < 1) return [];
5618
+ if (kv.cas == null) {
5619
+ if (observation != null) await observation.skipped("forwarding-unavailable");
5620
+ return [];
5621
+ }
5622
+ if (maxTargets < 1) {
5623
+ if (observation != null) await observation.skipped("forwarding-disabled");
5624
+ return [];
5625
+ }
4898
5626
  const deadline = Date.now() + options.deadline.total("millisecond");
5627
+ observation?.setPhase("destination-planning");
4899
5628
  const canonicalActivityId = require_http.getCanonicalPortableId(activityId) ?? activityId.href;
4900
5629
  const claimKey = (gateway) => [
4901
5630
  ...kvPrefix,
@@ -4923,15 +5652,27 @@ async function forwardPortableInboxActivity(parameters) {
4923
5652
  targets.length = maxTargets;
4924
5653
  }
4925
5654
  const claimed = [];
4926
- for (const { gateway, inbox } of targets) if (await kv.cas(claimKey(gateway), void 0, true, { ttl })) claimed.push(inbox);
4927
- if (claimed.length < 1) return [];
5655
+ for (const { gateway, inbox } of targets) if (await kv.cas(claimKey(gateway), void 0, true, { ttl })) {
5656
+ claimed.push(inbox);
5657
+ if (observation != null) await observation.planDestination(inbox.href, {
5658
+ actorIds: [recipient.actorId.href],
5659
+ sharedInbox: false,
5660
+ portableInbox: recipient.canonicalInboxId,
5661
+ mode: "portable-replication"
5662
+ });
5663
+ }
5664
+ observation?.planned(claimed.length);
5665
+ if (claimed.length < 1) {
5666
+ if (observation != null) await observation.skipped("no-forwarding-targets");
5667
+ return [];
5668
+ }
4928
5669
  logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
4929
5670
  activityId: activityId.href,
4930
5671
  inbox: recipient.canonicalInboxId,
4931
5672
  targets: claimed.map((t) => t.href)
4932
5673
  });
4933
- if (outboxQueue == null) await forwardImmediately(parameters, claimed, deadline);
4934
- else await enqueueForwarding(parameters, outboxQueue, claimed);
5674
+ if (outboxQueue == null) await forwardImmediately(parameters, claimed, deadline, observation);
5675
+ else await enqueueForwarding(parameters, outboxQueue, claimed, observation);
4935
5676
  return claimed;
4936
5677
  }
4937
5678
  /**
@@ -5074,7 +5815,7 @@ async function resolveForwardingKey({ getKeys, activityId }) {
5074
5815
  }
5075
5816
  return key;
5076
5817
  }
5077
- async function forwardImmediately(parameters, inboxes, deadlineAt) {
5818
+ async function forwardImmediately(parameters, inboxes, deadlineAt, observation) {
5078
5819
  const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, meterProvider, tracerProvider } = parameters;
5079
5820
  const logger = (0, _logtape_logtape.getLogger)([
5080
5821
  "fedify",
@@ -5082,23 +5823,50 @@ async function forwardImmediately(parameters, inboxes, deadlineAt) {
5082
5823
  "inbox"
5083
5824
  ]);
5084
5825
  const deadline = Math.max(0, deadlineAt - Date.now());
5085
- const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
5086
- activity,
5087
- activityId: activityId.href,
5088
- activityType,
5089
- keys: key == null ? [] : [key],
5090
- inbox,
5091
- allowPrivateAddress,
5092
- specDeterminer,
5093
- meterProvider,
5094
- tracerProvider
5095
- }).catch((error) => {
5096
- logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5097
- activityId: activityId.href,
5098
- inbox: inbox.href,
5099
- error
5100
- });
5101
- }))));
5826
+ observation?.setPhase("key-resolution");
5827
+ const forwarding = (async () => {
5828
+ try {
5829
+ const key = await resolveForwardingKey(parameters);
5830
+ observation?.setPhase("request-preparation");
5831
+ const sending = [];
5832
+ for (const inbox of inboxes) {
5833
+ const delivery = observation?.delivery(inbox.href);
5834
+ let pending;
5835
+ try {
5836
+ pending = sendActivity({
5837
+ observation: delivery,
5838
+ activity,
5839
+ activityId: activityId.href,
5840
+ activityType,
5841
+ keys: key == null ? [] : [key],
5842
+ inbox,
5843
+ allowPrivateAddress,
5844
+ specDeterminer,
5845
+ meterProvider,
5846
+ tracerProvider
5847
+ });
5848
+ } catch (error) {
5849
+ if (delivery != null) await delivery.failed(error);
5850
+ throw error;
5851
+ }
5852
+ const settled = pending.catch((error) => {
5853
+ logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5854
+ activityId: activityId.href,
5855
+ inbox: inbox.href,
5856
+ error
5857
+ });
5858
+ });
5859
+ if (observation != null) settled.catch(() => {});
5860
+ sending.push(settled);
5861
+ if (delivery != null) await delivery.handoff({ type: "direct" });
5862
+ }
5863
+ if (observation != null) await observation.prepared("delivery");
5864
+ await Promise.all(sending);
5865
+ } catch (error) {
5866
+ if (observation != null) await observation.failed(error);
5867
+ throw error;
5868
+ }
5869
+ })();
5102
5870
  let timer;
5103
5871
  const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
5104
5872
  timer = setTimeout(() => resolve(true), deadline);
@@ -5109,13 +5877,14 @@ async function forwardImmediately(parameters, inboxes, deadlineAt) {
5109
5877
  deadline
5110
5878
  });
5111
5879
  }
5112
- async function enqueueForwarding(parameters, outboxQueue, inboxes) {
5880
+ async function enqueueForwarding(parameters, outboxQueue, inboxes, observation) {
5113
5881
  const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
5114
5882
  const logger = (0, _logtape_logtape.getLogger)([
5115
5883
  "fedify",
5116
5884
  "federation",
5117
5885
  "inbox"
5118
5886
  ]);
5887
+ observation?.setPhase("key-resolution");
5119
5888
  const key = await resolveForwardingKey(parameters);
5120
5889
  const keys = [];
5121
5890
  if (key != null) try {
@@ -5130,28 +5899,43 @@ async function enqueueForwarding(parameters, outboxQueue, inboxes) {
5130
5899
  error
5131
5900
  });
5132
5901
  }
5902
+ observation?.setPhase("request-preparation");
5133
5903
  startQueue?.();
5134
5904
  const started = (/* @__PURE__ */ new Date()).toISOString();
5135
5905
  const traceContext = {};
5136
5906
  _opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), traceContext);
5907
+ let preparedCount = 0;
5137
5908
  await Promise.all(inboxes.map(async (inbox) => {
5138
- const message = {
5139
- type: "outbox",
5140
- id: crypto.randomUUID(),
5141
- baseUrl,
5142
- keys,
5143
- activity,
5144
- activityId: activityId.href,
5145
- activityType,
5146
- inbox: inbox.href,
5147
- sharedInbox: false,
5148
- started,
5149
- attempt: 0,
5150
- headers: {},
5151
- traceContext
5152
- };
5909
+ const delivery = observation?.delivery(inbox.href);
5910
+ let message;
5153
5911
  try {
5154
- await outboxQueue.enqueue(message);
5912
+ message = {
5913
+ type: "outbox",
5914
+ ...serializeProvenance(parameters.provenance),
5915
+ id: crypto.randomUUID(),
5916
+ baseUrl,
5917
+ keys,
5918
+ activity,
5919
+ activityId: activityId.href,
5920
+ activityType,
5921
+ inbox: inbox.href,
5922
+ ...observation?.continuation(inbox.href),
5923
+ sharedInbox: false,
5924
+ started,
5925
+ attempt: 0,
5926
+ headers: {},
5927
+ traceContext
5928
+ };
5929
+ } catch (error) {
5930
+ if (delivery != null) await delivery.failed(error);
5931
+ throw error;
5932
+ }
5933
+ if (observation != null && ++preparedCount === inboxes.length) await observation.prepared("delivery");
5934
+ try {
5935
+ await (observation == null ? outboxQueue.enqueue(message) : observation.enqueue([{
5936
+ inbox: inbox.href,
5937
+ queueMessageId: message.id
5938
+ }], "outbox", () => outboxQueue.enqueue(message)));
5155
5939
  } catch (error) {
5156
5940
  logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5157
5941
  activityId: activityId.href,
@@ -5165,7 +5949,7 @@ async function enqueueForwarding(parameters, outboxQueue, inboxes) {
5165
5949
  }
5166
5950
  //#endregion
5167
5951
  //#region src/federation/temporal.ts
5168
- const Temporal$7 = require_http.Temporal;
5952
+ const Temporal$8 = require_http.Temporal;
5169
5953
  function isPlainObject$1(value) {
5170
5954
  return typeof value === "object" && value != null && !Array.isArray(value);
5171
5955
  }
@@ -5175,7 +5959,7 @@ function normalizeDateTimeLiteral(value) {
5175
5959
  function isMalformedDateTimeLiteral(value) {
5176
5960
  if (typeof value !== "string") return false;
5177
5961
  try {
5178
- Temporal$7.Instant.from(normalizeDateTimeLiteral(value));
5962
+ Temporal$8.Instant.from(normalizeDateTimeLiteral(value));
5179
5963
  return false;
5180
5964
  } catch {
5181
5965
  return true;
@@ -5184,7 +5968,7 @@ function isMalformedDateTimeLiteral(value) {
5184
5968
  function isMalformedDurationLiteral(value) {
5185
5969
  if (typeof value !== "string") return false;
5186
5970
  try {
5187
- Temporal$7.Duration.from(value);
5971
+ Temporal$8.Duration.from(value);
5188
5972
  return false;
5189
5973
  } catch {
5190
5974
  return true;
@@ -5255,7 +6039,7 @@ async function hasMalformedKnownTemporalLiteral(value, contextLoader) {
5255
6039
  }
5256
6040
  //#endregion
5257
6041
  //#region src/federation/handler.ts
5258
- const Temporal$6 = require_http.Temporal;
6042
+ const Temporal$7 = require_http.Temporal;
5259
6043
  const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
5260
6044
  const INBOX_COMPOUND_PROOF_LIMITS = {
5261
6045
  maxDepth: 64,
@@ -7781,7 +8565,7 @@ async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
7781
8565
  }
7782
8566
  async function setKey(kv, key, policy) {
7783
8567
  const seconds = policy.nonceTtlSeconds ?? 300;
7784
- const ttl = Temporal$6.Duration.from({ seconds });
8568
+ const ttl = Temporal$7.Duration.from({ seconds });
7785
8569
  await kv.set(key, true, { ttl });
7786
8570
  }
7787
8571
  const DEF_COMPONENTS = [
@@ -7871,7 +8655,7 @@ async function verifyPortableActivityId(activityId, jsonLd, options) {
7871
8655
  }
7872
8656
  //#endregion
7873
8657
  //#region src/federation/kv.ts
7874
- const Temporal$5 = require_http.Temporal;
8658
+ const Temporal$6 = require_http.Temporal;
7875
8659
  /**
7876
8660
  * A key–value store that stores values in memory.
7877
8661
  * Do not use this in production as it does not persist values.
@@ -7891,7 +8675,7 @@ var MemoryKvStore = class {
7891
8675
  const entry = this.#values[encodedKey];
7892
8676
  if (entry == null) return Promise.resolve(void 0);
7893
8677
  const [value, expiration] = entry;
7894
- if (expiration != null && Temporal$5.Now.instant().until(expiration).sign < 0) {
8678
+ if (expiration != null && Temporal$6.Now.instant().until(expiration).sign < 0) {
7895
8679
  delete this.#values[encodedKey];
7896
8680
  return Promise.resolve(void 0);
7897
8681
  }
@@ -7902,7 +8686,7 @@ var MemoryKvStore = class {
7902
8686
  */
7903
8687
  set(key, value, options) {
7904
8688
  const encodedKey = this.#encodeKey(key);
7905
- const expiration = options?.ttl == null ? null : Temporal$5.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
8689
+ const expiration = options?.ttl == null ? null : Temporal$6.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
7906
8690
  this.#values[encodedKey] = [value, expiration];
7907
8691
  return Promise.resolve();
7908
8692
  }
@@ -7924,13 +8708,13 @@ var MemoryKvStore = class {
7924
8708
  if (entry == null) currentValue = void 0;
7925
8709
  else {
7926
8710
  const [value, expiration] = entry;
7927
- if (expiration != null && Temporal$5.Now.instant().until(expiration).sign < 0) {
8711
+ if (expiration != null && Temporal$6.Now.instant().until(expiration).sign < 0) {
7928
8712
  delete this.#values[encodedKey];
7929
8713
  currentValue = void 0;
7930
8714
  } else currentValue = value;
7931
8715
  }
7932
8716
  if (!(0, es_toolkit.isEqual)(currentValue, expectedValue)) return Promise.resolve(false);
7933
- const expiration = options?.ttl == null ? null : Temporal$5.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
8717
+ const expiration = options?.ttl == null ? null : Temporal$6.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
7934
8718
  this.#values[encodedKey] = [newValue, expiration];
7935
8719
  return Promise.resolve(true);
7936
8720
  }
@@ -7938,7 +8722,7 @@ var MemoryKvStore = class {
7938
8722
  * {@inheritDoc KvStore.list}
7939
8723
  */
7940
8724
  async *list(prefix) {
7941
- const now = Temporal$5.Now.instant();
8725
+ const now = Temporal$6.Now.instant();
7942
8726
  for (const [encodedKey, entry] of Object.entries(this.#values)) {
7943
8727
  const key = JSON.parse(encodedKey);
7944
8728
  if (prefix != null) {
@@ -7958,6 +8742,787 @@ var MemoryKvStore = class {
7958
8742
  }
7959
8743
  };
7960
8744
  //#endregion
8745
+ //#region src/federation/outbound-observation.ts
8746
+ const Temporal$5 = require_http.Temporal;
8747
+ function snapshotDeliveryIdentity(identity) {
8748
+ if (identity == null || typeof identity.deliveryId !== "string" || identity.deliveryId.length < 1 || typeof identity.destinationInbox !== "string" || identity.destinationInbox.length < 1 || identity.broadcast == null || !(identity.broadcast.type === "unknown" || identity.broadcast.type === "known" && typeof identity.broadcast.id === "string" && identity.broadcast.id.length > 0) || !(identity.portableInbox == null || typeof identity.portableInbox === "string")) throw new TypeError("Invalid outbound delivery identity in queue metadata.");
8749
+ return Object.freeze({
8750
+ broadcast: Object.freeze(identity.broadcast.type === "known" ? {
8751
+ type: "known",
8752
+ id: identity.broadcast.id
8753
+ } : { type: "unknown" }),
8754
+ deliveryId: identity.deliveryId,
8755
+ destinationInbox: identity.destinationInbox,
8756
+ ...identity.portableInbox == null ? {} : { portableInbox: identity.portableInbox }
8757
+ });
8758
+ }
8759
+ async function legacyFanoutIdentity(taskId, inbox, portableInbox) {
8760
+ const bytes = new TextEncoder().encode(JSON.stringify([
8761
+ "fedify-legacy-fanout-delivery",
8762
+ taskId,
8763
+ inbox
8764
+ ]));
8765
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
8766
+ return {
8767
+ broadcast: { type: "unknown" },
8768
+ deliveryId: `legacy-fanout:${(0, byte_encodings_hex.encodeHex)(new Uint8Array(digest))}`,
8769
+ destinationInbox: inbox,
8770
+ ...portableInbox == null ? {} : { portableInbox }
8771
+ };
8772
+ }
8773
+ async function notifyObserver(context, callback, event, unit) {
8774
+ if (callback == null) return;
8775
+ try {
8776
+ await (0, _logtape_logtape.withContext)(sourceLogContext(event.provenance), () => callback(context, event));
8777
+ } catch (error) {
8778
+ logObserverFailure(unit, error);
8779
+ }
8780
+ }
8781
+ function logObserverFailure(unit, error) {
8782
+ try {
8783
+ (0, _logtape_logtape.getLogger)([
8784
+ "fedify",
8785
+ "federation",
8786
+ "outbox"
8787
+ ]).error("An unexpected error occurred in outbound {unit} observer:\n{error}", {
8788
+ unit,
8789
+ error
8790
+ });
8791
+ } catch {}
8792
+ }
8793
+ function warnInvalidDeliveryIdentity(messageId, error) {
8794
+ try {
8795
+ (0, _logtape_logtape.getLogger)([
8796
+ "fedify",
8797
+ "federation",
8798
+ "outbox"
8799
+ ]).warn("Ignoring invalid outbound delivery identity in queue message {messageId}: {error}", {
8800
+ messageId,
8801
+ error
8802
+ });
8803
+ } catch {}
8804
+ }
8805
+ function snapshotTrace(span, parentSpanContext) {
8806
+ const spanContext = span.spanContext();
8807
+ if (!(0, _opentelemetry_api.isSpanContextValid)(spanContext) || spanContext.isRemote || spanContext.traceId === parentSpanContext?.traceId && spanContext.spanId === parentSpanContext.spanId) return void 0;
8808
+ return Object.freeze({
8809
+ traceId: spanContext.traceId,
8810
+ spanId: spanContext.spanId
8811
+ });
8812
+ }
8813
+ /** State owned by one concrete send, never persisted in queue metadata. */
8814
+ var OutboundAttemptObservation = class {
8815
+ inboxUrl;
8816
+ report;
8817
+ reportExchange;
8818
+ headerPolicies;
8819
+ bodyCapture;
8820
+ senderPayload;
8821
+ #attemptId = crypto.randomUUID();
8822
+ #failures = [];
8823
+ #response = Object.freeze({ type: "not-observed" });
8824
+ #finalResponse;
8825
+ #responses = /* @__PURE__ */ new WeakMap();
8826
+ #trace;
8827
+ #finished = false;
8828
+ #exchangeCount = 0;
8829
+ #senderPayload;
8830
+ constructor(inboxUrl, report, reportExchange = void 0, headerPolicies = {}, bodyCapture = void 0, senderPayload = false) {
8831
+ this.inboxUrl = inboxUrl;
8832
+ this.report = report;
8833
+ this.reportExchange = reportExchange;
8834
+ this.headerPolicies = headerPolicies;
8835
+ this.bodyCapture = bodyCapture;
8836
+ this.senderPayload = senderPayload;
8837
+ this.#senderPayload = Object.freeze({
8838
+ stage: "sender-payload",
8839
+ type: "unavailable",
8840
+ reason: senderPayload ? "not-materialized" : "disabled"
8841
+ });
8842
+ }
8843
+ snapshotSenderPayload(input) {
8844
+ if (this.senderPayload) this.#senderPayload = projectSenderPayload(input);
8845
+ }
8846
+ setSpan(span, parentSpanContext) {
8847
+ this.#trace = snapshotTrace(span, parentSpanContext);
8848
+ }
8849
+ async fetch(request, submit) {
8850
+ const exchangeIndex = this.#exchangeCount++;
8851
+ const snapshot = Object.freeze({
8852
+ url: request.url,
8853
+ method: request.method,
8854
+ ...this.headerPolicies.requestHeaders == null ? {} : { headers: snapshotHeaders(request.headers, this.headerPolicies.requestHeaders) }
8855
+ });
8856
+ let response;
8857
+ try {
8858
+ response = await submit();
8859
+ } catch (error) {
8860
+ await this.#exchange(exchangeIndex, snapshot, {
8861
+ type: "failed",
8862
+ error
8863
+ });
8864
+ throw error;
8865
+ }
8866
+ const metadata = Object.freeze({
8867
+ status: response.status,
8868
+ statusText: response.statusText,
8869
+ ...response.url === "" ? {} : { url: response.url }
8870
+ });
8871
+ const observed = Object.freeze({
8872
+ exchangeIndex,
8873
+ metadata
8874
+ });
8875
+ this.#responses.set(response, observed);
8876
+ this.#response = Object.freeze({
8877
+ type: "undetermined",
8878
+ lastObserved: observed
8879
+ });
8880
+ const responseSnapshot = Object.freeze({
8881
+ ...metadata,
8882
+ ...this.headerPolicies.responseHeaders == null ? {} : { headers: snapshotHeaders(response.headers, this.headerPolicies.responseHeaders) }
8883
+ });
8884
+ const bodyCapture = this.bodyCapture == null ? void 0 : await captureResponseBody(response, this.bodyCapture, (body, signal) => this.bodyCapture.onBody({
8885
+ attemptId: this.#attemptId,
8886
+ inboxUrl: this.inboxUrl,
8887
+ exchangeIndex,
8888
+ observedAt: Temporal$5.Now.instant().epochMilliseconds,
8889
+ request: snapshot,
8890
+ response: responseSnapshot,
8891
+ body,
8892
+ signal
8893
+ }));
8894
+ if (bodyCapture?.type === "failed" && bodyCapture.phase === "observer") logObserverFailure("response-body", bodyCapture.error);
8895
+ await this.#exchange(exchangeIndex, snapshot, {
8896
+ type: "response",
8897
+ response: responseSnapshot
8898
+ }, bodyCapture);
8899
+ return response;
8900
+ }
8901
+ #exchange(exchangeIndex, request, outcome, bodyCapture) {
8902
+ return this.reportExchange?.({
8903
+ attemptId: this.#attemptId,
8904
+ inboxUrl: this.inboxUrl,
8905
+ exchangeIndex,
8906
+ observedAt: Temporal$5.Now.instant().epochMilliseconds,
8907
+ request,
8908
+ outcome: Object.freeze(outcome),
8909
+ ...bodyCapture == null ? {} : { bodyCapture }
8910
+ }) ?? Promise.resolve();
8911
+ }
8912
+ response(response, disposition) {
8913
+ const observed = this.#responses.get(response);
8914
+ if (observed == null) return;
8915
+ if (disposition === "final") {
8916
+ this.#finalResponse = Object.freeze({
8917
+ type: "final",
8918
+ ...observed
8919
+ });
8920
+ this.#response = this.#finalResponse;
8921
+ } else this.#response = Object.freeze({
8922
+ type: "intermediate-only",
8923
+ lastObserved: observed
8924
+ });
8925
+ }
8926
+ failure(failure) {
8927
+ this.#failures.push(Object.freeze(failure));
8928
+ }
8929
+ unknown(error) {
8930
+ if (!this.#failures.some((failure) => failure.error === error || failure.phase !== "cleanup")) this.failure({
8931
+ kind: "unknown",
8932
+ phase: "unknown",
8933
+ error
8934
+ });
8935
+ }
8936
+ succeeded() {
8937
+ if (this.#finished) return Promise.resolve();
8938
+ if (this.#finalResponse == null) throw new Error("A successful outbound attempt must have an observed final response.");
8939
+ return this.#finish({
8940
+ type: "succeeded",
8941
+ response: this.#finalResponse
8942
+ });
8943
+ }
8944
+ failed(error) {
8945
+ let failure;
8946
+ let diagnostics;
8947
+ if (this.#response.type === "final" && (this.#response.metadata.status < 200 || this.#response.metadata.status >= 300)) {
8948
+ failure = this.#failures.find((failure) => failure.kind === "http") ?? Object.freeze({
8949
+ kind: "http",
8950
+ phase: "response"
8951
+ });
8952
+ diagnostics = this.#failures.filter((failure) => failure.kind !== "http");
8953
+ } else {
8954
+ failure = this.#failures.find((failure) => failure.phase !== "cleanup") ?? this.#failures[0] ?? Object.freeze({
8955
+ kind: "unknown",
8956
+ phase: "unknown",
8957
+ error
8958
+ });
8959
+ diagnostics = this.#failures.filter((item) => item !== failure);
8960
+ }
8961
+ return this.#finish({
8962
+ type: "failed",
8963
+ failure,
8964
+ error,
8965
+ response: this.#response,
8966
+ ...diagnostics.length === 0 ? {} : { diagnostics: Object.freeze(diagnostics) }
8967
+ });
8968
+ }
8969
+ #finish(outcome) {
8970
+ if (this.#finished) return Promise.resolve();
8971
+ this.#finished = true;
8972
+ return this.report({
8973
+ attemptId: this.#attemptId,
8974
+ inboxUrl: this.inboxUrl,
8975
+ httpExchangeCount: this.#exchangeCount,
8976
+ senderPayload: this.#senderPayload,
8977
+ observedAt: Temporal$5.Now.instant().epochMilliseconds,
8978
+ ...this.#trace == null ? {} : { trace: this.#trace },
8979
+ outcome: Object.freeze(outcome)
8980
+ });
8981
+ }
8982
+ };
8983
+ function snapshotOutboxIdentity(message) {
8984
+ if (message.outbound != null) try {
8985
+ return snapshotDeliveryIdentity(message.outbound);
8986
+ } catch (error) {
8987
+ warnInvalidDeliveryIdentity(message.id, error);
8988
+ }
8989
+ return Object.freeze({
8990
+ broadcast: Object.freeze({ type: "unknown" }),
8991
+ deliveryId: message.id,
8992
+ ...message.portableInbox == null ? {} : { portableInbox: message.portableInbox }
8993
+ });
8994
+ }
8995
+ /** Per-execution state, independent of concrete attempt/exchange callbacks. */
8996
+ var OutboundWorkerObservation = class {
8997
+ context;
8998
+ callback;
8999
+ provenance;
9000
+ #attemptsStarted = 0;
9001
+ #actions;
9002
+ #trace;
9003
+ #preparationFailure;
9004
+ #preparationFailures = [];
9005
+ #queue;
9006
+ #delivery;
9007
+ #role;
9008
+ #currentContext;
9009
+ constructor(context, callback, message, provenance) {
9010
+ this.context = context;
9011
+ this.callback = callback;
9012
+ this.provenance = provenance;
9013
+ this.#role = message.type;
9014
+ this.#queue = Object.freeze({
9015
+ queueMessageId: message.id,
9016
+ ...message.type === "outbox" ? { queueAttempt: message.attempt } : {}
9017
+ });
9018
+ if (message.type === "outbox") this.#delivery = snapshotOutboxIdentity(message);
9019
+ }
9020
+ attemptStarted = () => {
9021
+ this.#attemptsStarted++;
9022
+ };
9023
+ enableActions() {
9024
+ this.#actions ??= [];
9025
+ }
9026
+ directAttemptStarted(inboxUrl, identity) {
9027
+ this.attemptStarted();
9028
+ this.#actions?.push(Object.freeze({
9029
+ type: "handoff",
9030
+ action: "direct",
9031
+ inboxUrls: Object.freeze([inboxUrl]),
9032
+ ...identity == null ? {} : { delivery: snapshotDeliveryIdentity(identity) }
9033
+ }));
9034
+ }
9035
+ async enqueueChildren(messages, enqueue) {
9036
+ if (this.#actions == null) return await enqueue();
9037
+ const facts = messages.map((message) => ({
9038
+ action: "enqueue",
9039
+ queue: Object.freeze({
9040
+ queueMessageId: message.id,
9041
+ queueAttempt: message.attempt
9042
+ }),
9043
+ inboxUrls: Object.freeze(message.gatewayInboxes != null && message.gatewayInboxes.length > 1 ? [...message.gatewayInboxes] : [message.inbox]),
9044
+ ...message.outbound == null ? {} : { delivery: snapshotDeliveryIdentity(message.outbound) }
9045
+ }));
9046
+ try {
9047
+ await enqueue();
9048
+ } catch (error) {
9049
+ for (const fact of facts) this.#actions.push(Object.freeze({
9050
+ type: "action-failed",
9051
+ ...fact,
9052
+ acceptance: "unknown",
9053
+ error
9054
+ }));
9055
+ throw error;
9056
+ }
9057
+ for (const fact of facts) this.#actions.push(Object.freeze({
9058
+ type: "scheduled",
9059
+ ...fact
9060
+ }));
9061
+ }
9062
+ async enqueue(action, message, delay, enqueue, preparationFailures) {
9063
+ if (this.#actions == null) return await enqueue();
9064
+ const facts = {
9065
+ action,
9066
+ queue: Object.freeze({
9067
+ queueMessageId: message.id,
9068
+ queueAttempt: message.attempt
9069
+ }),
9070
+ inboxUrls: Object.freeze(message.gatewayInboxes != null && message.gatewayInboxes.length > 1 ? [...message.gatewayInboxes] : [message.inbox]),
9071
+ delay: delay.toString(),
9072
+ ...preparationFailures == null || preparationFailures.length === 0 ? {} : { preparationFailures: Object.freeze([...preparationFailures]) },
9073
+ ...message.gatewayNotBefore == null ? {} : { notBefore: Object.freeze({ ...message.gatewayNotBefore }) }
9074
+ };
9075
+ try {
9076
+ await enqueue();
9077
+ } catch (error) {
9078
+ this.#actions.push(Object.freeze({
9079
+ type: "action-failed",
9080
+ ...facts,
9081
+ acceptance: "unknown",
9082
+ error
9083
+ }));
9084
+ throw error;
9085
+ }
9086
+ this.#actions.push(Object.freeze({
9087
+ type: "scheduled",
9088
+ ...facts
9089
+ }));
9090
+ }
9091
+ abandon(reason, inboxUrls, preparationFailures) {
9092
+ this.#actions?.push(Object.freeze({
9093
+ type: "abandoned",
9094
+ reason,
9095
+ inboxUrls: Object.freeze([...inboxUrls]),
9096
+ ...preparationFailures == null || preparationFailures.length === 0 ? {} : { preparationFailures: Object.freeze([...preparationFailures]) }
9097
+ }));
9098
+ }
9099
+ dropFailure(inboxUrls, error) {
9100
+ this.#actions?.push(Object.freeze({
9101
+ type: "action-failed",
9102
+ action: "drop",
9103
+ inboxUrls: Object.freeze([...inboxUrls]),
9104
+ error
9105
+ }));
9106
+ }
9107
+ async drop(inboxUrls, operation) {
9108
+ try {
9109
+ await operation();
9110
+ } catch (error) {
9111
+ this.dropFailure(inboxUrls, error);
9112
+ throw error;
9113
+ }
9114
+ }
9115
+ getContext() {
9116
+ return this.#currentContext ??= this.context();
9117
+ }
9118
+ useContext(context) {
9119
+ this.#currentContext = context;
9120
+ }
9121
+ setSpan(span, parentSpanContext) {
9122
+ this.#trace = snapshotTrace(span, parentSpanContext);
9123
+ }
9124
+ async prepare(phase, operation) {
9125
+ try {
9126
+ const result = await operation();
9127
+ this.#preparationFailure = void 0;
9128
+ return result;
9129
+ } catch (error) {
9130
+ this.#preparationFailure = Object.freeze({
9131
+ kind: "local",
9132
+ phase,
9133
+ error
9134
+ });
9135
+ throw error;
9136
+ }
9137
+ }
9138
+ recordPreparationFailure(error, inboxUrl) {
9139
+ const failure = this.#preparationFailure;
9140
+ if (failure == null || !Object.is(failure.error, error)) return [];
9141
+ const diagnostic = Object.freeze({
9142
+ inboxUrl,
9143
+ failure
9144
+ });
9145
+ this.#preparationFailures.push(diagnostic);
9146
+ return [diagnostic];
9147
+ }
9148
+ async run(operation) {
9149
+ try {
9150
+ await operation();
9151
+ } catch (error) {
9152
+ await this.#notify(this.#preparationFailure == null || !Object.is(this.#preparationFailure.error, error) ? {
9153
+ type: "failed",
9154
+ error
9155
+ } : {
9156
+ type: "preparation-failed",
9157
+ failure: this.#preparationFailure,
9158
+ error
9159
+ });
9160
+ throw error;
9161
+ }
9162
+ await this.#notify({ type: "completed" });
9163
+ }
9164
+ async #notify(result) {
9165
+ const observedAt = Temporal$5.Now.instant().epochMilliseconds;
9166
+ try {
9167
+ const context = this.getContext();
9168
+ await notifyObserver(context, this.callback, Object.freeze({
9169
+ role: this.#role,
9170
+ queue: this.#queue,
9171
+ ...this.#delivery == null ? {} : { delivery: this.#delivery },
9172
+ attemptsStarted: this.#attemptsStarted,
9173
+ actions: Object.freeze([...this.#actions ?? []]),
9174
+ ...this.#preparationFailures.length === 0 ? {} : { preparationFailures: Object.freeze([...this.#preparationFailures]) },
9175
+ observedAt,
9176
+ ...this.#trace == null ? {} : { trace: this.#trace },
9177
+ provenance: this.provenance,
9178
+ local: snapshotLocalContext(context),
9179
+ result: Object.freeze(result)
9180
+ }), "worker");
9181
+ } catch (error) {
9182
+ logObserverFailure("worker", error);
9183
+ }
9184
+ }
9185
+ };
9186
+ /** Selected callback and restored grouping, shared by concrete gateway attempts. */
9187
+ var OutboundAttemptObserver = class OutboundAttemptObserver {
9188
+ context;
9189
+ callback;
9190
+ identity;
9191
+ provenance;
9192
+ onHttpExchange;
9193
+ headerPolicies;
9194
+ bodyCapture;
9195
+ #queue;
9196
+ constructor(context, callback, identity, provenance, queue, onHttpExchange = void 0, headerPolicies = {}, bodyCapture = void 0) {
9197
+ this.context = context;
9198
+ this.callback = callback;
9199
+ this.identity = identity;
9200
+ this.provenance = provenance;
9201
+ this.onHttpExchange = onHttpExchange;
9202
+ this.headerPolicies = headerPolicies;
9203
+ this.bodyCapture = bodyCapture;
9204
+ this.#queue = queue == null ? void 0 : Object.freeze({ ...queue });
9205
+ }
9206
+ static resumeOutbox(context, callback, message, provenance, onHttpExchange, headerPolicies = {}, bodyCapture) {
9207
+ if (callback == null && onHttpExchange == null && bodyCapture == null) return;
9208
+ const identity = snapshotOutboxIdentity(message);
9209
+ return new OutboundAttemptObserver(context, callback, identity, provenance, {
9210
+ queueMessageId: message.id,
9211
+ queueAttempt: message.attempt
9212
+ }, onHttpExchange, headerPolicies, bodyCapture);
9213
+ }
9214
+ startAttempt(inboxUrl) {
9215
+ if (this.callback == null && this.onHttpExchange == null && this.bodyCapture == null) return void 0;
9216
+ const context = this.context();
9217
+ return new OutboundAttemptObservation(inboxUrl, (facts) => notifyObserver(context, this.callback, Object.freeze({
9218
+ ...this.identity,
9219
+ ...facts,
9220
+ ...this.#queue == null ? {} : { queue: this.#queue },
9221
+ provenance: this.provenance,
9222
+ local: snapshotLocalContext(context)
9223
+ }), "attempt"), (facts) => notifyObserver(context, this.onHttpExchange, Object.freeze({
9224
+ ...this.identity,
9225
+ ...facts,
9226
+ ...this.#queue == null ? {} : { queue: this.#queue },
9227
+ provenance: this.provenance,
9228
+ local: snapshotLocalContext(context)
9229
+ }), "http-exchange"), this.onHttpExchange == null && this.bodyCapture == null ? {} : this.headerPolicies, this.bodyCapture == null ? void 0 : {
9230
+ ...this.bodyCapture,
9231
+ onBody: (event) => (0, _logtape_logtape.withContext)(sourceLogContext(this.provenance), () => this.bodyCapture.onBody(context, Object.freeze({
9232
+ ...this.identity,
9233
+ ...event,
9234
+ ...this.#queue == null ? {} : { queue: this.#queue },
9235
+ provenance: this.provenance,
9236
+ local: snapshotLocalContext(context)
9237
+ })))
9238
+ }, this.callback != null && this.headerPolicies.senderPayload === true);
9239
+ }
9240
+ };
9241
+ /** One producer's destination, without callbacks or live inputs in queues. */
9242
+ var OutboundDeliveryObservation = class {
9243
+ context;
9244
+ observer;
9245
+ provenance;
9246
+ trace;
9247
+ #finished = false;
9248
+ #reference;
9249
+ #plan;
9250
+ #attempts;
9251
+ constructor(context, observer, identity, destination, provenance, trace, onAttempt, onHttpExchange, queue, headerPolicies = {}, bodyCapture) {
9252
+ this.context = context;
9253
+ this.observer = observer;
9254
+ this.provenance = provenance;
9255
+ this.trace = trace;
9256
+ this.#reference = snapshotDeliveryIdentity(identity);
9257
+ this.#attempts = new OutboundAttemptObserver(() => this.context, onAttempt, this.#reference, this.provenance, queue, onHttpExchange, headerPolicies, bodyCapture);
9258
+ this.#plan = Object.freeze({
9259
+ actorIds: Object.freeze(Array.from(destination.actorIds)),
9260
+ sharedInbox: destination.sharedInbox,
9261
+ mode: destination.mode ?? (destination.portableInbox == null ? "ordinary" : "portable-fallback")
9262
+ });
9263
+ }
9264
+ startAttempt(inboxUrl) {
9265
+ return this.#attempts.startAttempt(inboxUrl);
9266
+ }
9267
+ get identity() {
9268
+ return this.#reference;
9269
+ }
9270
+ planned() {
9271
+ return notifyObserver(this.context, this.observer, Object.freeze({
9272
+ ...this.trace == null ? {} : { trace: this.trace },
9273
+ observedAt: Temporal$5.Now.instant().epochMilliseconds,
9274
+ provenance: this.provenance,
9275
+ local: snapshotLocalContext(this.context),
9276
+ ...this.#reference,
9277
+ ...this.#plan,
9278
+ stage: "planned"
9279
+ }), "delivery");
9280
+ }
9281
+ handoff(result) {
9282
+ if (this.#finished) return Promise.resolve();
9283
+ this.#finished = true;
9284
+ return notifyObserver(this.context, this.observer, Object.freeze({
9285
+ ...this.trace == null ? {} : { trace: this.trace },
9286
+ observedAt: Temporal$5.Now.instant().epochMilliseconds,
9287
+ provenance: this.provenance,
9288
+ local: snapshotLocalContext(this.context),
9289
+ ...this.#reference,
9290
+ stage: "handoff",
9291
+ result: Object.freeze(result)
9292
+ }), "delivery");
9293
+ }
9294
+ failed(error) {
9295
+ if (this.#finished) return Promise.resolve();
9296
+ this.#finished = true;
9297
+ return notifyObserver(this.context, this.observer, Object.freeze({
9298
+ ...this.trace == null ? {} : { trace: this.trace },
9299
+ observedAt: Temporal$5.Now.instant().epochMilliseconds,
9300
+ provenance: this.provenance,
9301
+ local: snapshotLocalContext(this.context),
9302
+ ...this.#reference,
9303
+ stage: "preparation-failed",
9304
+ failure: Object.freeze({
9305
+ kind: "local",
9306
+ phase: "request-preparation",
9307
+ error
9308
+ })
9309
+ }), "delivery");
9310
+ }
9311
+ };
9312
+ /** State owned by one outgoing invocation, never serialized into queues. */
9313
+ var OutboundBroadcastObservation = class OutboundBroadcastObservation {
9314
+ context;
9315
+ operation;
9316
+ provenance;
9317
+ queue;
9318
+ #broadcast;
9319
+ #finished = false;
9320
+ #phase = "unknown";
9321
+ #planning = "not-started";
9322
+ #destinationCount = 0;
9323
+ #trace;
9324
+ #activity = Object.freeze({});
9325
+ #onBroadcast;
9326
+ #onDelivery;
9327
+ #onAttempt;
9328
+ #onHttpExchange;
9329
+ #headerPolicies;
9330
+ #bodyCapture;
9331
+ #deliveries = /* @__PURE__ */ new Map();
9332
+ constructor(context, observers, operation, provenance = getOutboundProvenance(context), broadcast = {
9333
+ type: "known",
9334
+ id: crypto.randomUUID()
9335
+ }, queue = void 0) {
9336
+ this.context = context;
9337
+ this.operation = operation;
9338
+ this.provenance = provenance;
9339
+ this.queue = queue;
9340
+ this.#broadcast = broadcast;
9341
+ this.#onBroadcast = observers.onBroadcast;
9342
+ this.#onDelivery = observers.onDelivery;
9343
+ this.#onAttempt = observers.onAttempt;
9344
+ this.#onHttpExchange = observers.onHttpExchange;
9345
+ this.#bodyCapture = observers.responseBodyCapture;
9346
+ this.#headerPolicies = {
9347
+ requestHeaders: observers.requestHeaders,
9348
+ responseHeaders: observers.responseHeaders,
9349
+ senderPayload: observers.senderPayload
9350
+ };
9351
+ }
9352
+ /** Continue an existing fanout; do not emit a new broadcast invocation. */
9353
+ static async resumeFanout(context, observers, message, span, parentSpanContext) {
9354
+ if (observers.onDelivery == null && observers.onAttempt == null && observers.onHttpExchange == null && observers.responseBodyCapture == null && observers.onWorker == null && !Object.values(message.inboxes).some((entry) => entry.outbound != null)) return;
9355
+ const observation = new OutboundBroadcastObservation(context, {
9356
+ onDelivery: observers.onDelivery,
9357
+ onAttempt: observers.onAttempt,
9358
+ onHttpExchange: observers.onHttpExchange,
9359
+ requestHeaders: observers.requestHeaders,
9360
+ responseHeaders: observers.responseHeaders,
9361
+ responseBodyCapture: observers.responseBodyCapture,
9362
+ senderPayload: observers.senderPayload
9363
+ }, "send", getOutboundProvenance(context), { type: "unknown" }, { queueMessageId: message.id });
9364
+ if (span != null) observation.setSpan(span, parentSpanContext);
9365
+ observation.#finished = true;
9366
+ observation.planned(Object.keys(message.inboxes).length);
9367
+ for (const [inbox, destination] of Object.entries(message.inboxes)) {
9368
+ let identity;
9369
+ if (destination.outbound != null) try {
9370
+ const snapshot = snapshotDeliveryIdentity(destination.outbound);
9371
+ if (snapshot.destinationInbox !== inbox) throw new TypeError("Outbound delivery identity does not match its fanout destination.");
9372
+ identity = snapshot;
9373
+ } catch (error) {
9374
+ warnInvalidDeliveryIdentity(message.id, error);
9375
+ }
9376
+ identity ??= await legacyFanoutIdentity(message.id, inbox, destination.portableInbox);
9377
+ observation.#deliveries.set(inbox, observation.#createDelivery(inbox, destination, identity));
9378
+ }
9379
+ return observation;
9380
+ }
9381
+ #createDelivery(inbox, destination, identity) {
9382
+ return new OutboundDeliveryObservation(this.context, this.#onDelivery, identity ?? {
9383
+ broadcast: this.#broadcast,
9384
+ deliveryId: crypto.randomUUID(),
9385
+ destinationInbox: inbox,
9386
+ ...destination.portableInbox == null ? {} : { portableInbox: destination.portableInbox }
9387
+ }, destination, this.provenance, this.#trace, this.#onAttempt, this.#onHttpExchange, this.queue, this.#headerPolicies, this.#bodyCapture);
9388
+ }
9389
+ continuation(inbox) {
9390
+ const delivery = this.delivery(inbox);
9391
+ return delivery == null ? void 0 : { outbound: delivery.identity };
9392
+ }
9393
+ async planDestinations(inboxes) {
9394
+ this.planned(Object.keys(inboxes).length);
9395
+ for (const [inbox, destination] of Object.entries(inboxes)) if (!this.#deliveries.has(inbox)) this.#deliveries.set(inbox, this.#createDelivery(inbox, destination));
9396
+ for (const delivery of this.#deliveries.values()) await delivery.planned();
9397
+ }
9398
+ delivery(inbox) {
9399
+ return this.#deliveries.get(inbox);
9400
+ }
9401
+ async enqueue(entries, queue, enqueue) {
9402
+ let result;
9403
+ try {
9404
+ result = await enqueue();
9405
+ } catch (error) {
9406
+ await Promise.all(entries.map(({ inbox }) => this.delivery(inbox)?.handoff({
9407
+ type: "enqueue-failed",
9408
+ acceptance: "unknown",
9409
+ error
9410
+ })));
9411
+ throw error;
9412
+ }
9413
+ await Promise.all(entries.map(({ inbox, queueMessageId }) => this.delivery(inbox)?.handoff({
9414
+ type: "enqueued",
9415
+ queue,
9416
+ queueMessageId
9417
+ })));
9418
+ return result;
9419
+ }
9420
+ /** Attach only the span supplied by this execution, never an ambient fallback. */
9421
+ setSpan(span, parentSpanContext) {
9422
+ this.#trace = snapshotTrace(span, parentSpanContext);
9423
+ }
9424
+ setPhase(phase) {
9425
+ this.#phase = phase;
9426
+ if (phase === "destination-planning") this.#planning = "incomplete";
9427
+ }
9428
+ /** Replace the Activity facts captured by the producer path. */
9429
+ setActivity(reference) {
9430
+ this.#activity = Object.freeze({ ...reference });
9431
+ }
9432
+ planned(destinationCount) {
9433
+ this.#planning = "complete";
9434
+ this.#destinationCount = destinationCount;
9435
+ }
9436
+ async planDestination(inbox, destination) {
9437
+ this.#destinationCount++;
9438
+ const delivery = this.#createDelivery(inbox, destination);
9439
+ this.#deliveries.set(inbox, delivery);
9440
+ await delivery.planned();
9441
+ }
9442
+ prepared(preparedFor) {
9443
+ return this.#finish(Object.freeze({
9444
+ type: "prepared",
9445
+ preparedFor,
9446
+ destinationCount: this.#destinationCount
9447
+ }));
9448
+ }
9449
+ skipped(reason = "no-destinations") {
9450
+ return this.#finish(Object.freeze({
9451
+ type: "skipped",
9452
+ reason
9453
+ }));
9454
+ }
9455
+ async run(send) {
9456
+ if (this.#broadcast.type === "known") await this.#notify(Object.freeze({
9457
+ broadcastId: this.#broadcast.id,
9458
+ operation: this.operation,
9459
+ stage: "started",
9460
+ activity: this.#activity
9461
+ }));
9462
+ try {
9463
+ return await send(this, this.provenance);
9464
+ } catch (error) {
9465
+ await this.failed(error);
9466
+ throw error;
9467
+ }
9468
+ }
9469
+ failed(error) {
9470
+ const failure = {
9471
+ kind: this.#phase === "unknown" ? "unknown" : "local",
9472
+ phase: this.#phase,
9473
+ error
9474
+ };
9475
+ return this.#finish(Object.freeze({
9476
+ type: "failed",
9477
+ failure: Object.freeze(failure),
9478
+ planning: this.#planning,
9479
+ plannedDestinationCount: this.#destinationCount
9480
+ }));
9481
+ }
9482
+ async #finish(result) {
9483
+ if (this.#finished) return;
9484
+ this.#finished = true;
9485
+ if (this.#broadcast.type === "known") await this.#notify(Object.freeze({
9486
+ broadcastId: this.#broadcast.id,
9487
+ operation: this.operation,
9488
+ stage: "preparation-finished",
9489
+ result,
9490
+ activity: this.#activity
9491
+ }));
9492
+ }
9493
+ #notify(event) {
9494
+ return notifyObserver(this.context, this.#onBroadcast, Object.freeze({
9495
+ ...event,
9496
+ ...this.#trace == null ? {} : { trace: this.#trace },
9497
+ observedAt: Temporal$5.Now.instant().epochMilliseconds,
9498
+ provenance: this.provenance,
9499
+ local: snapshotLocalContext(this.context)
9500
+ }), "broadcast");
9501
+ }
9502
+ };
9503
+ /** Observe preparation without changing the unobserved send's execution path. */
9504
+ function observeOutboundBroadcast(context, observers, send, operation = "send", dispatchId) {
9505
+ let provenance;
9506
+ let invalid;
9507
+ try {
9508
+ provenance = invocationProvenance(context, dispatchId);
9509
+ } catch (error) {
9510
+ provenance = invocationProvenance(context, void 0);
9511
+ invalid = { error };
9512
+ }
9513
+ const execute = async (observation) => {
9514
+ if (invalid != null) {
9515
+ observation?.setPhase("validation");
9516
+ throw invalid.error;
9517
+ }
9518
+ return await send(observation, provenance);
9519
+ };
9520
+ return (0, _logtape_logtape.withContext)(sourceLogContext(provenance), () => {
9521
+ if (observers.onBroadcast == null && observers.onDelivery == null && observers.onAttempt == null && observers.onHttpExchange == null && observers.responseBodyCapture == null) return execute();
9522
+ return new OutboundBroadcastObservation(context, observers, operation, provenance).run(execute);
9523
+ });
9524
+ }
9525
+ //#endregion
7961
9526
  //#region src/nodeinfo/handler.ts
7962
9527
  /**
7963
9528
  * Handles a NodeInfo request. You would not typically call this function
@@ -9541,41 +11106,70 @@ var FederationImpl = class extends FederationBuilderImpl {
9541
11106
  }
9542
11107
  await Promise.all(promises);
9543
11108
  }
9544
- processQueuedTask(contextData, message) {
11109
+ async processQueuedTask(contextData, message) {
9545
11110
  const extractedContext = _opentelemetry_api.propagation.extract(_opentelemetry_api.context.active(), message.traceContext);
9546
- return (0, _logtape_logtape.withContext)({ messageId: message.id }, async () => {
11111
+ let provenance = emptyProvenance;
11112
+ if (message.type !== "task") try {
11113
+ provenance = restoreProvenance(message.provenance);
11114
+ } catch (error) {
11115
+ try {
11116
+ (0, _logtape_logtape.getLogger)([
11117
+ "fedify",
11118
+ "federation",
11119
+ "queue"
11120
+ ]).warn("Ignoring invalid provenance in queue message {messageId}: {error}", {
11121
+ messageId: message.id,
11122
+ error
11123
+ });
11124
+ } catch {}
11125
+ }
11126
+ if (message.type !== "task" && message.provenance !== void 0) message = {
11127
+ ...message,
11128
+ provenance
11129
+ };
11130
+ return await (0, _logtape_logtape.withContext)({
11131
+ messageId: message.id,
11132
+ inboxRequestId: void 0,
11133
+ ...sourceLogContext(provenance)
11134
+ }, async () => {
9547
11135
  if (message.type === "fanout") {
11136
+ const observers = this.outboundObservers;
11137
+ const worker = this.#createOutboundWorkerObservation(contextData, message, provenance, observers.onWorker);
9548
11138
  const common = {
9549
11139
  role: "fanout",
9550
11140
  queue: this.fanoutQueue,
9551
11141
  activityType: message.activityType
9552
11142
  };
9553
- await this.#runWorkerSpan("activitypub.fanout", { "activitypub.activity.type": message.activityType }, extractedContext, this.#instrumentWorkerBody(common, async (span) => {
11143
+ const execute = () => this.#runWorkerSpan("activitypub.fanout", { "activitypub.activity.type": message.activityType }, extractedContext, this.#instrumentWorkerBody(common, async (span) => {
9554
11144
  if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
9555
- await this.#listenFanoutMessage(contextData, message);
11145
+ await this.#listenFanoutMessage(contextData, message, provenance, span, _opentelemetry_api.trace.getSpanContext(extractedContext), observers, worker);
9556
11146
  return { outcome: "completed" };
9557
- }, classifyAbortableError));
11147
+ }, classifyAbortableError), worker);
11148
+ await (worker == null ? execute() : worker.run(execute));
9558
11149
  } else if (message.type === "outbox") {
11150
+ const observers = this.outboundObservers;
11151
+ const worker = this.#createOutboundWorkerObservation(contextData, message, provenance, observers.onWorker);
9559
11152
  const common = {
9560
11153
  role: "outbox",
9561
11154
  queue: this.outboxQueue,
9562
11155
  activityType: message.activityType
9563
11156
  };
9564
- await this.#runWorkerSpan("activitypub.outbox", {
11157
+ const execute = () => this.#runWorkerSpan("activitypub.outbox", {
9565
11158
  "activitypub.activity.type": message.activityType,
9566
11159
  "activitypub.activity.retries": message.attempt
9567
11160
  }, extractedContext, this.#instrumentWorkerBody(common, async (span) => {
9568
11161
  if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
9569
- await this.#listenOutboxMessage(contextData, message, span);
11162
+ await this.#listenOutboxMessage(contextData, message, span, provenance, observers, worker);
9570
11163
  return { outcome: "completed" };
9571
- }, classifyAbortableError));
11164
+ }, classifyAbortableError), worker);
11165
+ await (worker == null ? execute() : worker.run(execute));
9572
11166
  } else if (message.type === "inbox") {
9573
11167
  const common = {
9574
11168
  role: "inbox",
9575
11169
  queue: this.inboxQueue
9576
11170
  };
9577
11171
  await this.#runWorkerSpan("activitypub.inbox", { "activitypub.shared_inbox": message.identifier == null }, extractedContext, this.#instrumentWorkerBody(common, async (span) => {
9578
- await this.#listenInboxMessage(contextData, message, span, (activityType) => {
11172
+ await this.#listenInboxMessage(contextData, message, span, provenance, (activityType) => {
9579
11173
  common.activityType = activityType;
9580
11174
  });
9581
11175
  return { outcome: "completed" };
@@ -9594,6 +11188,22 @@ var FederationImpl = class extends FederationBuilderImpl {
9594
11188
  }
9595
11189
  });
9596
11190
  }
11191
+ #createOutboundWorkerObservation(contextData, message, provenance, callback) {
11192
+ if (callback == null) return void 0;
11193
+ let documentLoader;
11194
+ let contextLoader;
11195
+ return new OutboundWorkerObservation(() => {
11196
+ const loaderOptions = this.#getLoaderOptions(message.baseUrl);
11197
+ return new ContextImpl({
11198
+ [outboundProvenance]: provenance,
11199
+ federation: this,
11200
+ url: new URL("/", message.baseUrl),
11201
+ data: contextData,
11202
+ documentLoader: (...args) => (documentLoader ??= this.documentLoaderFactory(loaderOptions))(...args),
11203
+ contextLoader: (...args) => (contextLoader ??= this.contextLoaderFactory(loaderOptions))(...args)
11204
+ });
11205
+ }, callback, message, provenance);
11206
+ }
9597
11207
  /**
9598
11208
  * Opens a CONSUMER span for a queue worker and scopes the extracted trace
9599
11209
  * context around its body. The body—typically built by
@@ -9605,11 +11215,12 @@ var FederationImpl = class extends FederationBuilderImpl {
9605
11215
  * @param instrumentedBody Builds the context-scoped worker callback for the
9606
11216
  * opened span.
9607
11217
  */
9608
- async #runWorkerSpan(spanName, spanAttributes, extractedContext, instrumentedBody) {
11218
+ async #runWorkerSpan(spanName, spanAttributes, extractedContext, instrumentedBody, worker) {
9609
11219
  await this._getTracer().startActiveSpan(spanName, {
9610
11220
  kind: _opentelemetry_api.SpanKind.CONSUMER,
9611
11221
  attributes: spanAttributes
9612
11222
  }, extractedContext, async (span) => {
11223
+ worker?.setSpan(span, _opentelemetry_api.trace.getSpanContext(extractedContext));
9613
11224
  const spanCtx = span.spanContext();
9614
11225
  return await (0, _logtape_logtape.withContext)({
9615
11226
  traceId: spanCtx.traceId,
@@ -9652,7 +11263,8 @@ var FederationImpl = class extends FederationBuilderImpl {
9652
11263
  span.end();
9653
11264
  }
9654
11265
  };
9655
- async #listenFanoutMessage(data, message) {
11266
+ async #listenFanoutMessage(data, message, provenance, span, parentSpanContext, observers, worker) {
11267
+ worker?.enableActions();
9656
11268
  (0, _logtape_logtape.getLogger)([
9657
11269
  "fedify",
9658
11270
  "federation",
@@ -9661,11 +11273,12 @@ var FederationImpl = class extends FederationBuilderImpl {
9661
11273
  activityId: message.activityId,
9662
11274
  inboxes: globalThis.Object.keys(message.inboxes).length
9663
11275
  });
9664
- const keys = await Promise.all(message.keys.map(async ({ keyId, privateKey }) => ({
11276
+ const importKeys = () => Promise.all(message.keys.map(async ({ keyId, privateKey }) => ({
9665
11277
  keyId: new URL(keyId),
9666
11278
  privateKey: await require_http.importJwk(privateKey, "private")
9667
11279
  })));
9668
- const activity = await _fedify_vocab.Activity.fromJsonLd(message.activity, {
11280
+ const keys = worker == null ? await importKeys() : await worker.prepare("key-import", importKeys);
11281
+ const parseActivity = () => _fedify_vocab.Activity.fromJsonLd(message.activity, {
9669
11282
  contextLoader: this.contextLoaderFactory({
9670
11283
  allowPrivateAddress: this.allowPrivateAddress,
9671
11284
  userAgent: this.userAgent
@@ -9676,19 +11289,30 @@ var FederationImpl = class extends FederationBuilderImpl {
9676
11289
  }),
9677
11290
  tracerProvider: this.tracerProvider
9678
11291
  });
9679
- const context = this.#createContext(new URL(message.baseUrl), data, { documentLoader: this.documentLoaderFactory({
9680
- allowPrivateAddress: this.allowPrivateAddress,
9681
- userAgent: this.userAgent
9682
- }) });
11292
+ const activity = worker == null ? await parseActivity() : await worker.prepare("activity-parsing", parseActivity);
11293
+ const createContext = () => this.#createContext(new URL(message.baseUrl), data, {
11294
+ [outboundProvenance]: provenance,
11295
+ documentLoader: this.documentLoaderFactory({
11296
+ allowPrivateAddress: this.allowPrivateAddress,
11297
+ userAgent: this.userAgent
11298
+ })
11299
+ });
11300
+ const context = worker == null ? createContext() : await worker.prepare("context-preparation", createContext);
11301
+ worker?.useContext(context);
11302
+ const observation = await OutboundBroadcastObservation.resumeFanout(context, observers, message, span, parentSpanContext);
9683
11303
  await this.sendActivity(keys, message.inboxes, activity, {
9684
11304
  collectionSync: message.collectionSync,
9685
11305
  orderingKey: message.orderingKey,
9686
11306
  normalizeExistingProofs: message.normalizeExistingProofs,
9687
11307
  activityJsonLd: message.activity,
11308
+ observation,
11309
+ worker,
9688
11310
  context
9689
11311
  });
9690
11312
  }
9691
- async #listenOutboxMessage(_, message, span) {
11313
+ async #listenOutboxMessage(contextData, message, span, provenance, observers, worker) {
11314
+ const { onAttempt, onHttpExchange } = observers;
11315
+ worker?.enableActions();
9692
11316
  const logger = (0, _logtape_logtape.getLogger)([
9693
11317
  "fedify",
9694
11318
  "federation",
@@ -9705,14 +11329,26 @@ var FederationImpl = class extends FederationBuilderImpl {
9705
11329
  const keys = [];
9706
11330
  let rsaKeyPair = null;
9707
11331
  for (const { keyId, privateKey } of message.keys) {
9708
- const pair = {
11332
+ const importPair = async () => ({
9709
11333
  keyId: new URL(keyId),
9710
11334
  privateKey: await require_http.importJwk(privateKey, "private")
9711
- };
11335
+ });
11336
+ const pair = worker == null ? await importPair() : await worker.prepare("key-import", importPair);
9712
11337
  if (rsaKeyPair == null && pair.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") rsaKeyPair = pair;
9713
11338
  keys.push(pair);
9714
11339
  }
9715
11340
  const loaderOptions = this.#getLoaderOptions(message.baseUrl);
11341
+ let observationContext;
11342
+ let observationDocumentLoader;
11343
+ let observationContextLoader;
11344
+ const observation = OutboundAttemptObserver.resumeOutbox(() => worker?.getContext() ?? (observationContext ??= new ContextImpl({
11345
+ [outboundProvenance]: provenance,
11346
+ federation: this,
11347
+ url: new URL("/", message.baseUrl),
11348
+ data: contextData,
11349
+ documentLoader: (...args) => (observationDocumentLoader ??= this.documentLoaderFactory(loaderOptions))(...args),
11350
+ contextLoader: (...args) => (observationContextLoader ??= this.contextLoaderFactory(loaderOptions))(...args)
11351
+ })), onAttempt, message, provenance, onHttpExchange, observers, observers.responseBodyCapture);
9716
11352
  let parsedActorIds;
9717
11353
  const getActorIds = () => {
9718
11354
  parsedActorIds ??= (message.actorIds ?? []).flatMap((id) => {
@@ -9735,7 +11371,7 @@ var FederationImpl = class extends FederationBuilderImpl {
9735
11371
  verifyPortableObject: createPortableObjectVerifier(documentLoader, contextLoader, this)
9736
11372
  });
9737
11373
  };
9738
- const enqueueHeldOutboxMessage = async (delay, heldSince) => {
11374
+ const enqueueHeldOutboxMessage = async (delay, heldSince, preparationFailures) => {
9739
11375
  const { outboxQueue } = this;
9740
11376
  if (outboxQueue == null) return;
9741
11377
  const heldMessage = {
@@ -9743,10 +11379,11 @@ var FederationImpl = class extends FederationBuilderImpl {
9743
11379
  circuitHeld: true,
9744
11380
  circuitHeldSince: heldSince.toString()
9745
11381
  };
9746
- await outboxQueue.enqueue(heldMessage, {
11382
+ const enqueueOptions = {
9747
11383
  delay: clampNegativeDelay(delay),
9748
11384
  orderingKey: message.orderingKey
9749
- });
11385
+ };
11386
+ await (worker == null ? outboxQueue.enqueue(heldMessage, enqueueOptions) : worker.enqueue("defer", heldMessage, enqueueOptions.delay, () => outboxQueue.enqueue(heldMessage, enqueueOptions), preparationFailures));
9750
11387
  this.metrics.recordQueueTaskEnqueued({
9751
11388
  role: "outbox",
9752
11389
  queue: outboxQueue,
@@ -9763,7 +11400,10 @@ var FederationImpl = class extends FederationBuilderImpl {
9763
11400
  heldSince
9764
11401
  });
9765
11402
  if (this.outboxPermanentFailureHandler != null) {
9766
- const ctx = this.#createContext(new URL(message.baseUrl), _, { documentLoader: this.documentLoaderFactory(loaderOptions) });
11403
+ const ctx = this.#createContext(new URL(message.baseUrl), contextData, {
11404
+ [outboundProvenance]: provenance,
11405
+ documentLoader: this.documentLoaderFactory(loaderOptions)
11406
+ });
9767
11407
  try {
9768
11408
  await this.outboxPermanentFailureHandler(ctx, {
9769
11409
  reason: "circuit-breaker-ttl",
@@ -9775,6 +11415,7 @@ var FederationImpl = class extends FederationBuilderImpl {
9775
11415
  actorIds: getActorIds()
9776
11416
  });
9777
11417
  } catch (handlerError) {
11418
+ worker?.dropFailure([inbox.href], handlerError);
9778
11419
  logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
9779
11420
  ...logData,
9780
11421
  error: handlerError
@@ -9785,10 +11426,13 @@ var FederationImpl = class extends FederationBuilderImpl {
9785
11426
  };
9786
11427
  const { gatewayInboxes } = message;
9787
11428
  if (gatewayInboxes != null && gatewayInboxes.length > 1) {
9788
- await this.#listenGatewayOutboxMessage(_, {
11429
+ await this.#listenGatewayOutboxMessage(contextData, {
9789
11430
  ...message,
9790
11431
  gatewayInboxes
9791
11432
  }, span, {
11433
+ observation,
11434
+ worker,
11435
+ provenance,
9792
11436
  keys,
9793
11437
  logData,
9794
11438
  getActorIds,
@@ -9798,7 +11442,8 @@ var FederationImpl = class extends FederationBuilderImpl {
9798
11442
  return;
9799
11443
  }
9800
11444
  try {
9801
- const inbox = new URL(message.inbox);
11445
+ const parseInbox = () => new URL(message.inbox);
11446
+ const inbox = worker == null ? parseInbox() : await worker.prepare("validation", parseInbox);
9802
11447
  const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
9803
11448
  const remoteHost = require_http.getRemoteHost(inbox);
9804
11449
  let decision;
@@ -9822,13 +11467,19 @@ var FederationImpl = class extends FederationBuilderImpl {
9822
11467
  return;
9823
11468
  }
9824
11469
  if (decision.type === "drop") {
9825
- const activity = await parseActivity();
9826
- await dropHeldOutboxMessage(circuit, remoteHost, inbox, decision.heldSince, activity);
11470
+ worker?.abandon("held-ttl-expired", [inbox.href]);
11471
+ const drop = async () => {
11472
+ const activity = await parseActivity();
11473
+ await dropHeldOutboxMessage(circuit, remoteHost, inbox, decision.heldSince, activity);
11474
+ };
11475
+ await (worker == null ? drop() : worker.drop([inbox.href], drop));
9827
11476
  return;
9828
11477
  }
9829
11478
  if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
9830
11479
  }
9831
11480
  await sendActivity({
11481
+ attemptStarted: worker?.attemptStarted,
11482
+ observation,
9832
11483
  allowPrivateAddress: this.allowPrivateAddress,
9833
11484
  keys,
9834
11485
  activity: message.activity,
@@ -9836,7 +11487,7 @@ var FederationImpl = class extends FederationBuilderImpl {
9836
11487
  activityType: message.activityType,
9837
11488
  inbox,
9838
11489
  sharedInbox: message.sharedInbox,
9839
- headers: new Headers(message.headers),
11490
+ headers: worker == null ? new Headers(message.headers) : await worker.prepare("request-preparation", () => new Headers(message.headers)),
9840
11491
  specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
9841
11492
  meterProvider: this.meterProvider,
9842
11493
  tracerProvider: this.tracerProvider
@@ -9856,6 +11507,7 @@ var FederationImpl = class extends FederationBuilderImpl {
9856
11507
  });
9857
11508
  }
9858
11509
  } catch (error) {
11510
+ const preparationFailures = worker?.recordPreparationFailure(error, message.inbox) ?? [];
9859
11511
  span.setStatus({
9860
11512
  code: _opentelemetry_api.SpanStatusCode.ERROR,
9861
11513
  message: String(error)
@@ -9864,7 +11516,7 @@ var FederationImpl = class extends FederationBuilderImpl {
9864
11516
  if (error instanceof SendActivityError) return require_http.getRemoteHost(error.inbox);
9865
11517
  try {
9866
11518
  return require_http.getRemoteHost(new URL(message.inbox));
9867
- } catch (_) {
11519
+ } catch {
9868
11520
  logger.warn("Invalid inbox URL in queued outbox message: {inbox}", logData);
9869
11521
  return;
9870
11522
  }
@@ -9937,32 +11589,42 @@ var FederationImpl = class extends FederationBuilderImpl {
9937
11589
  });
9938
11590
  }
9939
11591
  if (circuitDrop != null) {
9940
- await dropHeldOutboxMessage(circuitDrop.circuit, circuitDrop.remoteHost, circuitDrop.inbox, circuitDrop.heldSince, activity);
11592
+ worker?.abandon("held-ttl-expired", [circuitDrop.inbox.href], preparationFailures);
11593
+ const drop = () => dropHeldOutboxMessage(circuitDrop.circuit, circuitDrop.remoteHost, circuitDrop.inbox, circuitDrop.heldSince, activity);
11594
+ await (worker == null ? drop() : worker.drop([circuitDrop.inbox.href], drop));
9941
11595
  return;
9942
11596
  }
9943
11597
  if (isPermanentFailure) {
11598
+ worker?.abandon("permanent-failure", [message.inbox], preparationFailures);
9944
11599
  this.metrics.recordPermanentFailure(error.inbox, error.statusCode);
9945
11600
  logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not retrying.", {
9946
11601
  ...logData,
9947
11602
  status: error.statusCode
9948
11603
  });
9949
11604
  if (this.outboxPermanentFailureHandler != null) {
9950
- const ctx = this.#createContext(new URL(message.baseUrl), _, { documentLoader: this.documentLoaderFactory(loaderOptions) });
9951
- try {
9952
- await this.outboxPermanentFailureHandler(ctx, {
9953
- reason: "http",
9954
- inbox: new URL(message.inbox),
9955
- activity,
9956
- error,
9957
- statusCode: error.statusCode,
9958
- actorIds: getActorIds()
11605
+ const drop = async () => {
11606
+ const ctx = this.#createContext(new URL(message.baseUrl), contextData, {
11607
+ [outboundProvenance]: provenance,
11608
+ documentLoader: this.documentLoaderFactory(loaderOptions)
9959
11609
  });
9960
- } catch (handlerError) {
9961
- logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
9962
- ...logData,
9963
- error: handlerError
9964
- });
9965
- }
11610
+ try {
11611
+ await this.outboxPermanentFailureHandler(ctx, {
11612
+ reason: "http",
11613
+ inbox: new URL(message.inbox),
11614
+ activity,
11615
+ error,
11616
+ statusCode: error.statusCode,
11617
+ actorIds: getActorIds()
11618
+ });
11619
+ } catch (handlerError) {
11620
+ worker?.dropFailure([message.inbox], handlerError);
11621
+ logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
11622
+ ...logData,
11623
+ error: handlerError
11624
+ });
11625
+ }
11626
+ };
11627
+ await (worker == null ? drop() : worker.drop([message.inbox], drop));
9966
11628
  }
9967
11629
  require_http.recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
9968
11630
  return;
@@ -9974,7 +11636,7 @@ var FederationImpl = class extends FederationBuilderImpl {
9974
11636
  });
9975
11637
  recordCircuitBreakerHeldSpanEvent(span, circuitHold.remoteHost, circuitHold.state);
9976
11638
  const circuit = this.circuitBreaker;
9977
- await enqueueHeldOutboxMessage(retryAfterDelay == null || circuit == null ? circuitHold.delay : circuit.capHeldDelay(circuitHold.heldSince, maxDelay(circuitHold.delay, retryAfterDelay)), circuitHold.heldSince);
11639
+ await enqueueHeldOutboxMessage(retryAfterDelay == null || circuit == null ? circuitHold.delay : circuit.capHeldDelay(circuitHold.heldSince, maxDelay(circuitHold.delay, retryAfterDelay)), circuitHold.heldSince, preparationFailures);
9978
11640
  return;
9979
11641
  }
9980
11642
  if (this.outboxQueue?.nativeRetrial && retryAfterDelay == null) {
@@ -9997,18 +11659,20 @@ var FederationImpl = class extends FederationBuilderImpl {
9997
11659
  };
9998
11660
  const { outboxQueue } = this;
9999
11661
  if (outboxQueue != null) {
10000
- await outboxQueue.enqueue(retryMessage, {
11662
+ const enqueueOptions = {
10001
11663
  delay: clampNegativeDelay(delay),
10002
11664
  orderingKey: message.orderingKey
10003
- });
11665
+ };
11666
+ await (worker == null ? outboxQueue.enqueue(retryMessage, enqueueOptions) : worker.enqueue("retry", retryMessage, enqueueOptions.delay, () => outboxQueue.enqueue(retryMessage, enqueueOptions), preparationFailures));
10004
11667
  this.metrics.recordQueueTaskEnqueued({
10005
11668
  role: "outbox",
10006
11669
  queue: outboxQueue,
10007
11670
  activityType: retryMessage.activityType
10008
11671
  }, retryMessage.attempt);
10009
11672
  require_http.recordOutboxActivity(this.meterProvider, "retried", retryMessage.activityType);
10010
- }
11673
+ } else worker?.abandon("queue-unavailable", [message.inbox], preparationFailures);
10011
11674
  } else {
11675
+ worker?.abandon("retry-policy-declined", [message.inbox], preparationFailures);
10012
11676
  logger.error("Failed to send activity {activityId} to {inbox} after {attempt} attempts; giving up:\n{error}", {
10013
11677
  ...logData,
10014
11678
  error
@@ -10043,25 +11707,32 @@ var FederationImpl = class extends FederationBuilderImpl {
10043
11707
  "federation",
10044
11708
  "circuit"
10045
11709
  ]);
10046
- const { keys, logData, getActorIds, parseActivity, dropHeldOutboxMessage } = helpers;
11710
+ const { observation, worker, provenance, keys, logData, getActorIds, parseActivity, dropHeldOutboxMessage } = helpers;
10047
11711
  const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
10048
11712
  const specDeterminer = new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl });
10049
11713
  const survivors = [];
11714
+ const preparationFailuresByRoute = /* @__PURE__ */ new Map();
11715
+ const permanentRoutes = /* @__PURE__ */ new Set();
10050
11716
  let lastError;
10051
11717
  let lastPermanent;
10052
11718
  let activity;
10053
11719
  const getActivity = async () => activity ??= await parseActivity();
10054
11720
  const drop = async (circuit, remoteHost, inbox, heldSince) => {
10055
- await dropHeldOutboxMessage(circuit, remoteHost, inbox, heldSince, await getActivity());
11721
+ const routes = message.gatewayInboxes.filter((route) => !permanentRoutes.has(route));
11722
+ worker?.abandon("held-ttl-expired", routes, routes.flatMap((route) => preparationFailuresByRoute.get(route) ?? []));
11723
+ const execute = async () => dropHeldOutboxMessage(circuit, remoteHost, inbox, heldSince, await getActivity());
11724
+ await (worker == null ? execute() : worker.drop(routes, execute));
10056
11725
  };
10057
11726
  const heldSince = parseInstant(message.circuitHeldSince);
10058
11727
  if (circuit != null && heldSince != null && Temporal$2.Instant.compare(heldSince.add(circuit.options.heldActivityTtl), Temporal$2.Now.instant()) <= 0) {
10059
- const inbox = new URL(message.gatewayInboxes[0]);
11728
+ const parseInbox = () => new URL(message.gatewayInboxes[0]);
11729
+ const inbox = worker == null ? parseInbox() : await worker.prepare("validation", parseInbox);
10060
11730
  await drop(circuit, require_http.getRemoteHost(inbox), inbox, heldSince);
10061
11731
  return;
10062
11732
  }
10063
11733
  for (const inboxHref of message.gatewayInboxes) {
10064
- const inbox = new URL(inboxHref);
11734
+ const parseInbox = () => new URL(inboxHref);
11735
+ const inbox = worker == null ? parseInbox() : await worker.prepare("validation", parseInbox);
10065
11736
  const remoteHost = require_http.getRemoteHost(inbox);
10066
11737
  const now = Temporal$2.Now.instant();
10067
11738
  const notBefore = parseInstant(message.gatewayNotBefore?.[inboxHref]);
@@ -10099,6 +11770,8 @@ var FederationImpl = class extends FederationBuilderImpl {
10099
11770
  }
10100
11771
  try {
10101
11772
  await sendActivity({
11773
+ attemptStarted: worker?.attemptStarted,
11774
+ observation,
10102
11775
  allowPrivateAddress: this.allowPrivateAddress,
10103
11776
  keys,
10104
11777
  activity: message.activity,
@@ -10106,12 +11779,14 @@ var FederationImpl = class extends FederationBuilderImpl {
10106
11779
  activityType: message.activityType,
10107
11780
  inbox,
10108
11781
  sharedInbox: message.sharedInbox,
10109
- headers: new Headers(message.headers),
11782
+ headers: worker == null ? new Headers(message.headers) : await worker.prepare("request-preparation", () => new Headers(message.headers)),
10110
11783
  specDeterminer,
10111
11784
  meterProvider: this.meterProvider,
10112
11785
  tracerProvider: this.tracerProvider
10113
11786
  });
10114
11787
  } catch (error) {
11788
+ const preparationFailures = worker?.recordPreparationFailure(error, inboxHref) ?? [];
11789
+ if (preparationFailures.length > 0) preparationFailuresByRoute.set(inboxHref, preparationFailures);
10115
11790
  lastError = error;
10116
11791
  const isPermanentFailure = error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode);
10117
11792
  let retryAfter = !isPermanentFailure && error instanceof SendActivityError && (error.statusCode === 429 || error.statusCode === 503) ? parseRetryAfter(error.responseHeaders) : void 0;
@@ -10156,6 +11831,8 @@ var FederationImpl = class extends FederationBuilderImpl {
10156
11831
  });
10157
11832
  }
10158
11833
  if (isPermanentFailure) {
11834
+ permanentRoutes.add(inboxHref);
11835
+ worker?.abandon("permanent-failure", [inboxHref], preparationFailures);
10159
11836
  logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not trying the gateway again.", {
10160
11837
  ...logData,
10161
11838
  inbox: inboxHref,
@@ -10220,22 +11897,29 @@ var FederationImpl = class extends FederationBuilderImpl {
10220
11897
  portableInbox: message.portableInbox
10221
11898
  });
10222
11899
  if (this.outboxPermanentFailureHandler != null) {
10223
- const ctx = this.#createContext(new URL(message.baseUrl), contextData, { documentLoader: this.documentLoaderFactory(this.#getLoaderOptions(message.baseUrl)) });
10224
- try {
10225
- await this.outboxPermanentFailureHandler(ctx, {
10226
- reason: "http",
10227
- inbox,
10228
- activity: await getActivity(),
10229
- error,
10230
- statusCode: error.statusCode,
10231
- actorIds: getActorIds()
10232
- });
10233
- } catch (handlerError) {
10234
- logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
10235
- ...logData,
10236
- error: handlerError
11900
+ const drop = async () => {
11901
+ const ctx = this.#createContext(new URL(message.baseUrl), contextData, {
11902
+ [outboundProvenance]: provenance,
11903
+ documentLoader: this.documentLoaderFactory(this.#getLoaderOptions(message.baseUrl))
10237
11904
  });
10238
- }
11905
+ try {
11906
+ await this.outboxPermanentFailureHandler(ctx, {
11907
+ reason: "http",
11908
+ inbox,
11909
+ activity: await getActivity(),
11910
+ error,
11911
+ statusCode: error.statusCode,
11912
+ actorIds: getActorIds()
11913
+ });
11914
+ } catch (handlerError) {
11915
+ worker?.dropFailure(message.gatewayInboxes, handlerError);
11916
+ logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
11917
+ ...logData,
11918
+ error: handlerError
11919
+ });
11920
+ }
11921
+ };
11922
+ await (worker == null ? drop() : worker.drop(message.gatewayInboxes, drop));
10239
11923
  }
10240
11924
  require_http.recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
10241
11925
  return;
@@ -10271,10 +11955,11 @@ var FederationImpl = class extends FederationBuilderImpl {
10271
11955
  ...logData,
10272
11956
  portableInbox: message.portableInbox
10273
11957
  });
10274
- await outboxQueue.enqueue(heldMessage, {
11958
+ const enqueueOptions = {
10275
11959
  delay: clampNegativeDelay(delay),
10276
11960
  orderingKey: message.orderingKey
10277
- });
11961
+ };
11962
+ await (worker == null ? outboxQueue.enqueue(heldMessage, enqueueOptions) : worker.enqueue("defer", heldMessage, enqueueOptions.delay, () => outboxQueue.enqueue(heldMessage, enqueueOptions), survivors.flatMap((route) => preparationFailuresByRoute.get(route.inbox) ?? [])));
10278
11963
  this.metrics.recordQueueTaskEnqueued({
10279
11964
  role: "outbox",
10280
11965
  queue: outboxQueue,
@@ -10292,6 +11977,7 @@ var FederationImpl = class extends FederationBuilderImpl {
10292
11977
  throw lastError;
10293
11978
  }
10294
11979
  if (policyDelay == null || outboxQueue == null) {
11980
+ worker?.abandon(outboxQueue == null ? "queue-unavailable" : "retry-policy-declined", survivors.map((route) => route.inbox), survivors.flatMap((route) => preparationFailuresByRoute.get(route.inbox) ?? []));
10295
11981
  logger.error("Failed to send activity {activityId} to every gateway of {portableInbox} after {attempt} attempts; giving up:\n{error}", {
10296
11982
  ...logData,
10297
11983
  portableInbox: message.portableInbox,
@@ -10310,10 +11996,11 @@ var FederationImpl = class extends FederationBuilderImpl {
10310
11996
  portableInbox: message.portableInbox,
10311
11997
  error: lastError
10312
11998
  });
10313
- await outboxQueue.enqueue(retryMessage, {
11999
+ const enqueueOptions = {
10314
12000
  delay: clampNegativeDelay(delay),
10315
12001
  orderingKey: message.orderingKey
10316
- });
12002
+ };
12003
+ await (worker == null ? outboxQueue.enqueue(retryMessage, enqueueOptions) : worker.enqueue("retry", retryMessage, enqueueOptions.delay, () => outboxQueue.enqueue(retryMessage, enqueueOptions), survivors.flatMap((route) => preparationFailuresByRoute.get(route.inbox) ?? [])));
10317
12004
  this.metrics.recordQueueTaskEnqueued({
10318
12005
  role: "outbox",
10319
12006
  queue: outboxQueue,
@@ -10321,18 +12008,24 @@ var FederationImpl = class extends FederationBuilderImpl {
10321
12008
  }, retryMessage.attempt);
10322
12009
  require_http.recordOutboxActivity(this.meterProvider, "retried", retryMessage.activityType);
10323
12010
  }
10324
- async #listenInboxMessage(ctxData, message, span, onActivityType) {
12011
+ async #listenInboxMessage(ctxData, message, span, provenance, onActivityType) {
10325
12012
  const logger = (0, _logtape_logtape.getLogger)([
10326
12013
  "fedify",
10327
12014
  "federation",
10328
12015
  "inbox"
10329
12016
  ]);
10330
12017
  const baseUrl = new URL(message.baseUrl);
10331
- let context = this.#createContext(baseUrl, ctxData);
10332
- if (message.identifier != null && this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
12018
+ let context = this.#createContext(baseUrl, ctxData, { [outboundProvenance]: provenance });
12019
+ if (message.identifier != null && this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(baseUrl, ctxData, {
12020
+ [outboundProvenance]: provenance,
12021
+ documentLoader: await context.getDocumentLoader({ identifier: message.identifier })
12022
+ });
10333
12023
  else if (message.identifier == null && this.sharedInboxKeyDispatcher != null) {
10334
12024
  const identity = await this.sharedInboxKeyDispatcher(context);
10335
- if (identity != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
12025
+ if (identity != null) context = this.#createContext(baseUrl, ctxData, {
12026
+ [outboundProvenance]: provenance,
12027
+ documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity)
12028
+ });
10336
12029
  }
10337
12030
  await this._getTracer().startActiveSpan("activitypub.dispatch_inbox_listener", { kind: _opentelemetry_api.SpanKind.INTERNAL }, async (listenerSpan) => {
10338
12031
  let activity = null;
@@ -10755,6 +12448,7 @@ var FederationImpl = class extends FederationBuilderImpl {
10755
12448
  }
10756
12449
  const loaderOptions = this.#getLoaderOptions(url.origin);
10757
12450
  const ctxOptions = {
12451
+ [outboundProvenance]: opts[outboundProvenance],
10758
12452
  url,
10759
12453
  federation: this,
10760
12454
  data: contextData,
@@ -10786,22 +12480,37 @@ var FederationImpl = class extends FederationBuilderImpl {
10786
12480
  "federation",
10787
12481
  "outbox"
10788
12482
  ]);
10789
- const { immediate, collectionSync, orderingKey, context: ctx } = options;
10790
- if (activity.id == null) throw new TypeError("The activity to send must have an id.");
10791
- if (activity.actorId == null) throw new TypeError("The activity to send must have at least one actor property.");
10792
- else if (keys.length < 1) throw new TypeError("The keys must not be empty.");
10793
- const contextLoader = this.contextLoaderFactory(this.#getLoaderOptions(ctx.origin));
10794
- const activityId = activity.id.href;
10795
- let rsaKey = null;
10796
- const ldSignable = !require_http.hasPortableActor(activity);
10797
- for (const { keyId, privateKey } of keys) {
10798
- require_http.validateCryptoKey(privateKey, "private");
10799
- if (ldSignable && rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5" && !require_http.isPortableKeyId(keyId)) rsaKey = {
10800
- keyId,
10801
- privateKey
12483
+ const { immediate, collectionSync, orderingKey, context: ctx, observation, worker, provenance = getOutboundProvenance(ctx) } = options;
12484
+ observation?.setPhase("validation");
12485
+ const validateActivity = () => {
12486
+ if (activity.id == null) throw new TypeError("The activity to send must have an id.");
12487
+ if (activity.actorId == null) throw new TypeError("The activity to send must have at least one actor property.");
12488
+ else if (keys.length < 1) throw new TypeError("The keys must not be empty.");
12489
+ return activity.id.href;
12490
+ };
12491
+ const activityId = worker == null ? validateActivity() : await worker.prepare("validation", validateActivity);
12492
+ observation?.setPhase("serialization");
12493
+ const createContextLoader = () => this.contextLoaderFactory(this.#getLoaderOptions(ctx.origin));
12494
+ const contextLoader = worker == null ? createContextLoader() : await worker.prepare("serialization", createContextLoader);
12495
+ const resolveRsaKey = () => {
12496
+ const ldSignable = !require_http.hasPortableActor(activity);
12497
+ let rsaKey = null;
12498
+ for (const { keyId, privateKey } of keys) {
12499
+ require_http.validateCryptoKey(privateKey, "private");
12500
+ if (ldSignable && rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5" && !require_http.isPortableKeyId(keyId)) rsaKey = {
12501
+ keyId,
12502
+ privateKey
12503
+ };
12504
+ }
12505
+ return {
12506
+ rsaKey,
12507
+ ldSignable
10802
12508
  };
10803
- }
10804
- const signed = await signOutgoingActivity(activity, keys.map(({ keyId, privateKey }) => ({
12509
+ };
12510
+ observation?.setPhase("key-resolution");
12511
+ const { rsaKey, ldSignable } = worker == null ? resolveRsaKey() : await worker.prepare("key-resolution", resolveRsaKey);
12512
+ observation?.setPhase("signing");
12513
+ const signActivity = () => signOutgoingActivity(activity, keys.map(({ keyId, privateKey }) => ({
10805
12514
  verificationMethod: keyId,
10806
12515
  privateKey
10807
12516
  })), {
@@ -10809,14 +12518,23 @@ var FederationImpl = class extends FederationBuilderImpl {
10809
12518
  tracerProvider: this.tracerProvider,
10810
12519
  appendToExistingProofs: false
10811
12520
  });
12521
+ const signed = worker == null ? await signActivity() : await worker.prepare("signing", signActivity);
10812
12522
  activity = signed.activity;
10813
12523
  const { hasProof, proofCreated } = signed;
10814
- let jsonLd = !proofCreated && options.activityJsonLd != null ? options.activityJsonLd : await activity.toJsonLd({
10815
- format: "compact",
10816
- contextLoader
10817
- });
10818
- if (proofCreated || !hasProof || options.normalizeExistingProofs) jsonLd = await require_http.normalizeOutgoingActivityJsonLd(jsonLd, contextLoader, { preserveNestedSecuredDocuments: true });
10819
- assertSupportedCompoundProofShape(jsonLd, activityId);
12524
+ observation?.setPhase("serialization");
12525
+ const serializeActivity = async () => {
12526
+ let result = !proofCreated && options.activityJsonLd != null ? options.activityJsonLd : await activity.toJsonLd({
12527
+ format: "compact",
12528
+ contextLoader
12529
+ });
12530
+ if (proofCreated || !hasProof || options.normalizeExistingProofs) result = await require_http.normalizeOutgoingActivityJsonLd(result, contextLoader, { preserveNestedSecuredDocuments: true });
12531
+ return result;
12532
+ };
12533
+ let jsonLd = worker == null ? await serializeActivity() : await worker.prepare("serialization", serializeActivity);
12534
+ observation?.setPhase("validation");
12535
+ const validateProofShape = () => assertSupportedCompoundProofShape(jsonLd, activityId);
12536
+ if (worker == null) validateProofShape();
12537
+ else await worker.prepare("validation", validateProofShape);
10820
12538
  if (rsaKey == null && !ldSignable) logger.debug("The activity {activityId} is performed by a portable actor, so it is sent without a Linked Data signature.", { activityId });
10821
12539
  else if (rsaKey == null) logger.warn("No supported key found to create a Linked Data signature for the activity {activityId}. The activity will be sent without a Linked Data signature. In order to create a Linked Data signature, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
10822
12540
  activityId,
@@ -10825,17 +12543,24 @@ var FederationImpl = class extends FederationBuilderImpl {
10825
12543
  privateKey: pair.privateKey
10826
12544
  }))
10827
12545
  });
10828
- else try {
10829
- jsonLd = await require_http.signJsonLd(jsonLd, rsaKey.privateKey, rsaKey.keyId, {
10830
- contextLoader,
10831
- tracerProvider: this.tracerProvider
10832
- });
10833
- } catch (error) {
10834
- if (!isLinkedDataSignatureJsonLdProcessingError(error)) throw error;
10835
- logger.warn("Failed to create a Linked Data signature for the activity {activityId}. The activity will be sent without a Linked Data signature.", {
10836
- activityId,
10837
- error
10838
- });
12546
+ else {
12547
+ const signLinkedData = async () => {
12548
+ try {
12549
+ observation?.setPhase("signing");
12550
+ return await require_http.signJsonLd(jsonLd, rsaKey.privateKey, rsaKey.keyId, {
12551
+ contextLoader,
12552
+ tracerProvider: this.tracerProvider
12553
+ });
12554
+ } catch (error) {
12555
+ if (!isLinkedDataSignatureJsonLdProcessingError(error)) throw error;
12556
+ logger.warn("Failed to create a Linked Data signature for the activity {activityId}. The activity will be sent without a Linked Data signature.", {
12557
+ activityId,
12558
+ error
12559
+ });
12560
+ return jsonLd;
12561
+ }
12562
+ };
12563
+ jsonLd = worker == null ? await signLinkedData() : await worker.prepare("signing", signLinkedData);
10839
12564
  }
10840
12565
  if (!hasProof) logger.warn("No supported key found to create a proof for the activity {activityId}. The activity will be sent without a proof. In order to create a proof, at least one Ed25519 key must be provided.", {
10841
12566
  activityId,
@@ -10844,6 +12569,7 @@ var FederationImpl = class extends FederationBuilderImpl {
10844
12569
  privateKey: pair.privateKey
10845
12570
  }))
10846
12571
  });
12572
+ observation?.setPhase("request-preparation");
10847
12573
  if (immediate || this.outboxQueue == null) {
10848
12574
  if (immediate) logger.debug("Sending activity immediately without queue since immediate option is set.", {
10849
12575
  activityId: activity.id.href,
@@ -10854,18 +12580,40 @@ var FederationImpl = class extends FederationBuilderImpl {
10854
12580
  activity: jsonLd
10855
12581
  });
10856
12582
  const promises = [];
10857
- for (const inbox in inboxes) promises.push(sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
10858
- allowPrivateAddress: this.allowPrivateAddress,
10859
- keys,
10860
- activity: jsonLd,
10861
- activityId: activity.id?.href,
10862
- activityType: (0, _fedify_vocab.getTypeId)(activity).href,
10863
- sharedInbox: inboxes[inbox].sharedInbox,
10864
- headers: collectionSync == null ? void 0 : new Headers({ "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) }),
10865
- specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
10866
- meterProvider: this.meterProvider,
10867
- tracerProvider: this.tracerProvider
10868
- }));
12583
+ for (const inbox in inboxes) {
12584
+ const delivery = observation?.delivery(inbox);
12585
+ let sending;
12586
+ try {
12587
+ const createHeaders = collectionSync == null ? void 0 : async () => new Headers({ "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) });
12588
+ const sendActivityId = activity.id?.href;
12589
+ const activityType = (0, _fedify_vocab.getTypeId)(activity).href;
12590
+ observation?.setActivity({
12591
+ ...sendActivityId == null ? {} : { id: sendActivityId },
12592
+ type: activityType
12593
+ });
12594
+ sending = sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
12595
+ attemptStarted: worker == null ? void 0 : (target) => worker.directAttemptStarted(target, observation?.continuation(inbox)?.outbound),
12596
+ observation: delivery,
12597
+ allowPrivateAddress: this.allowPrivateAddress,
12598
+ keys,
12599
+ activity: jsonLd,
12600
+ activityId: sendActivityId,
12601
+ activityType,
12602
+ sharedInbox: inboxes[inbox].sharedInbox,
12603
+ headers: createHeaders == null ? void 0 : await (worker == null ? createHeaders() : worker.prepare("request-preparation", createHeaders)),
12604
+ specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
12605
+ meterProvider: this.meterProvider,
12606
+ tracerProvider: this.tracerProvider
12607
+ });
12608
+ } catch (error) {
12609
+ if (delivery != null) await delivery.failed(error);
12610
+ throw error;
12611
+ }
12612
+ if (observation != null) sending.catch(() => {});
12613
+ if (delivery != null) await delivery.handoff({ type: "direct" });
12614
+ promises.push(sending);
12615
+ }
12616
+ if (observation != null) await observation.prepared("delivery");
10869
12617
  await Promise.all(promises);
10870
12618
  return;
10871
12619
  }
@@ -10873,48 +12621,71 @@ var FederationImpl = class extends FederationBuilderImpl {
10873
12621
  activityId: activity.id.href,
10874
12622
  activity: jsonLd
10875
12623
  });
12624
+ observation?.setPhase("key-resolution");
10876
12625
  const keyJwkPairs = [];
10877
12626
  for (const { keyId, privateKey } of keys) {
10878
- const privateKeyJwk = await require_http.exportJwk(privateKey);
12627
+ const exportPrivateKey = () => require_http.exportJwk(privateKey);
12628
+ const privateKeyJwk = worker == null ? await exportPrivateKey() : await worker.prepare("key-export", exportPrivateKey);
10879
12629
  keyJwkPairs.push({
10880
12630
  keyId: keyId.href,
10881
12631
  privateKey: privateKeyJwk
10882
12632
  });
10883
12633
  }
10884
12634
  if (!this.manuallyStartQueue) this._startQueueInternal(ctx.data);
12635
+ observation?.setPhase("request-preparation");
10885
12636
  const carrier = {};
10886
- _opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), carrier);
12637
+ const injectTraceContext = () => _opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), carrier);
12638
+ if (worker == null) injectTraceContext();
12639
+ else await worker.prepare("request-preparation", injectTraceContext);
10887
12640
  const messages = [];
10888
- for (const inbox in inboxes) {
10889
- const { portableInbox, gatewayInboxes } = inboxes[inbox];
10890
- const messageOrderingKey = orderingKey == null ? void 0 : `${orderingKey}\n${getOrderingDestination(inbox, portableInbox)}`;
10891
- const message = {
10892
- type: "outbox",
10893
- id: crypto.randomUUID(),
10894
- baseUrl: ctx.origin,
10895
- keys: keyJwkPairs,
10896
- activity: jsonLd,
10897
- activityId: activity.id?.href,
10898
- activityType: (0, _fedify_vocab.getTypeId)(activity).href,
10899
- inbox,
10900
- sharedInbox: inboxes[inbox].sharedInbox,
10901
- actorIds: [...inboxes[inbox].actorIds],
10902
- started: (/* @__PURE__ */ new Date()).toISOString(),
10903
- attempt: 0,
10904
- headers: collectionSync == null ? {} : { "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) },
10905
- orderingKey: messageOrderingKey,
10906
- ...getPortableMessageFields(portableInbox, gatewayInboxes),
10907
- traceContext: carrier
12641
+ for (const inbox in inboxes) try {
12642
+ const prepareMessage = async () => {
12643
+ const { portableInbox, gatewayInboxes } = inboxes[inbox];
12644
+ const messageOrderingKey = orderingKey == null ? void 0 : `${orderingKey}\n${getOrderingDestination(inbox, portableInbox)}`;
12645
+ const messageActivityId = activity.id?.href;
12646
+ const messageActivityType = (0, _fedify_vocab.getTypeId)(activity).href;
12647
+ observation?.setActivity({
12648
+ ...messageActivityId == null ? {} : { id: messageActivityId },
12649
+ type: messageActivityType
12650
+ });
12651
+ return {
12652
+ message: {
12653
+ type: "outbox",
12654
+ ...serializeProvenance(provenance),
12655
+ id: crypto.randomUUID(),
12656
+ baseUrl: ctx.origin,
12657
+ keys: keyJwkPairs,
12658
+ activity: jsonLd,
12659
+ activityId: messageActivityId,
12660
+ activityType: messageActivityType,
12661
+ inbox,
12662
+ ...observation?.continuation(inbox),
12663
+ sharedInbox: inboxes[inbox].sharedInbox,
12664
+ actorIds: [...inboxes[inbox].actorIds],
12665
+ started: (/* @__PURE__ */ new Date()).toISOString(),
12666
+ attempt: 0,
12667
+ headers: collectionSync == null ? {} : { "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) },
12668
+ orderingKey: messageOrderingKey,
12669
+ ...getPortableMessageFields(portableInbox, gatewayInboxes),
12670
+ traceContext: carrier
12671
+ },
12672
+ orderingKey: messageOrderingKey
12673
+ };
10908
12674
  };
10909
- messages.push({
10910
- message,
10911
- orderingKey: messageOrderingKey
10912
- });
12675
+ messages.push(await (worker == null ? prepareMessage() : worker.prepare("request-preparation", prepareMessage)));
12676
+ } catch (error) {
12677
+ if (observation != null) await observation.delivery(inbox)?.failed(error);
12678
+ throw error;
10913
12679
  }
12680
+ if (observation != null) await observation.prepared("delivery");
10914
12681
  const { outboxQueue } = this;
10915
12682
  if (outboxQueue.enqueueMany == null || orderingKey != null) {
10916
12683
  const errors = (await Promise.allSettled(messages.map(async (m) => {
10917
- await outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey });
12684
+ const enqueue = () => worker == null ? outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }) : worker.enqueueChildren([m.message], () => outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }));
12685
+ await (observation == null ? enqueue() : observation.enqueue([{
12686
+ inbox: m.message.inbox,
12687
+ queueMessageId: m.message.id
12688
+ }], "outbox", enqueue));
10918
12689
  require_http.recordOutboxEnqueue(this.meterProvider, outboxQueue, m.message);
10919
12690
  }))).filter((r) => r.status === "rejected").map((r) => r.reason);
10920
12691
  if (errors.length > 0) {
@@ -10927,7 +12698,11 @@ var FederationImpl = class extends FederationBuilderImpl {
10927
12698
  }
10928
12699
  } else {
10929
12700
  try {
10930
- await outboxQueue.enqueueMany(messages.map((m) => m.message));
12701
+ const enqueue = () => worker == null ? outboxQueue.enqueueMany(messages.map((m) => m.message)) : worker.enqueueChildren(messages.map((m) => m.message), () => outboxQueue.enqueueMany(messages.map((m) => m.message)));
12702
+ await (observation == null ? enqueue() : observation.enqueue(messages.map(({ message }) => ({
12703
+ inbox: message.inbox,
12704
+ queueMessageId: message.id
12705
+ })), "outbox", enqueue));
10931
12706
  } catch (error) {
10932
12707
  logger.error("Failed to enqueue activity {activityId} to send later: {error}", {
10933
12708
  activityId: activity.id.href,
@@ -10939,9 +12714,16 @@ var FederationImpl = class extends FederationBuilderImpl {
10939
12714
  }
10940
12715
  }
10941
12716
  fetch(request, options) {
10942
- return (0, _logtape_logtape.withContext)({ requestId: getRequestId(request) }, async () => {
12717
+ const requestId = getRequestId(request);
12718
+ return (0, _logtape_logtape.withContext)({
12719
+ requestId,
12720
+ inboxRequestId: void 0,
12721
+ dispatchId: void 0,
12722
+ sourceInboxRequestId: void 0,
12723
+ sourceRequestId: void 0
12724
+ }, async () => {
10943
12725
  const tracer = this._getTracer();
10944
- const metricState = {};
12726
+ const metricState = { requestId };
10945
12727
  const metricStart = performance.now();
10946
12728
  return await tracer.startActiveSpan(request.method, {
10947
12729
  kind: _opentelemetry_api.SpanKind.SERVER,
@@ -10986,7 +12768,8 @@ var FederationImpl = class extends FederationBuilderImpl {
10986
12768
  logger.error("An error occurred while serving request {method} {url}: {error}", {
10987
12769
  method: request.method,
10988
12770
  url: request.url,
10989
- error
12771
+ error,
12772
+ ...metricState.inboxRequest == null ? {} : { inboxRequestId: metricState.inboxRequest.inboxRequestId }
10990
12773
  });
10991
12774
  throw error;
10992
12775
  }
@@ -11006,6 +12789,7 @@ var FederationImpl = class extends FederationBuilderImpl {
11006
12789
  const url = new URL(request.url);
11007
12790
  const logTpl = "{method} {path}: {status}";
11008
12791
  const values = {
12792
+ ...metricState.inboxRequest == null ? {} : { inboxRequestId: metricState.inboxRequest.inboxRequestId },
11009
12793
  method: request.method,
11010
12794
  path: `${url.pathname}${url.search}`,
11011
12795
  url: request.url,
@@ -11058,9 +12842,11 @@ var FederationImpl = class extends FederationBuilderImpl {
11058
12842
  metricState.endpoint = getEndpointCategory(route.name);
11059
12843
  span.updateName(`${request.method} ${route.template}`);
11060
12844
  const routeName = route.name.replace(/:.*$/, "");
12845
+ const source = request.method === "POST" && (routeName === "inbox" || routeName === "sharedInbox") ? this.#createInboxRequestSource(request, metricState) : void 0;
12846
+ const provenance = globalThis.Object.freeze(source == null ? { originatingRequestId: metricState.requestId } : { inboxRequest: source });
11061
12847
  let context;
11062
12848
  try {
11063
- context = this.#createContext(request, contextData);
12849
+ context = (0, _logtape_logtape.withContext)(source == null ? {} : { inboxRequestId: source.inboxRequestId }, () => this.#createContext(request, contextData, { [outboundProvenance]: provenance }));
11064
12850
  } catch (error) {
11065
12851
  if (request.method !== "POST" || routeName !== "inbox" && routeName !== "sharedInbox") throw error;
11066
12852
  return await this.#reportInboxPreparationError(request, contextData, {
@@ -11131,13 +12917,19 @@ var FederationImpl = class extends FederationBuilderImpl {
11131
12917
  const observation = new InboxObservation({
11132
12918
  kind: routeName === "inbox" ? "personal" : "shared",
11133
12919
  recipient
11134
- });
12920
+ }, source);
11135
12921
  this.#setInboxCompletion(metricState, observation, () => context);
11136
- return await observation.run(async () => {
11137
- if (routeName === "inbox") context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: recipient }) });
12922
+ return await (0, _logtape_logtape.withContext)({ inboxRequestId: source.inboxRequestId }, () => observation.run(async () => {
12923
+ if (routeName === "inbox") context = this.#createContext(request, contextData, {
12924
+ [outboundProvenance]: getOutboundProvenance(context),
12925
+ documentLoader: await context.getDocumentLoader({ identifier: recipient })
12926
+ });
11138
12927
  else if (this.sharedInboxKeyDispatcher != null) {
11139
12928
  const identity = await this.sharedInboxKeyDispatcher(context);
11140
- if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
12929
+ if (identity != null) context = this.#createContext(request, contextData, {
12930
+ [outboundProvenance]: getOutboundProvenance(context),
12931
+ documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity)
12932
+ });
11141
12933
  }
11142
12934
  return await this.#handleInbox(request, {
11143
12935
  recipient,
@@ -11146,13 +12938,16 @@ var FederationImpl = class extends FederationBuilderImpl {
11146
12938
  onNotFound,
11147
12939
  observation
11148
12940
  });
11149
- }, span);
12941
+ }, span));
11150
12942
  }
11151
12943
  switch (routeName) {
11152
12944
  case "actor":
11153
12945
  case "actorAlias": {
11154
12946
  const identifier = route.name.startsWith("actorAlias:") ? route.name.substring(11) : route.values.identifier;
11155
- context = this.#createContext(request, contextData, { invokedFromActorDispatcher: { identifier } });
12947
+ context = this.#createContext(request, contextData, {
12948
+ [outboundProvenance]: getOutboundProvenance(context),
12949
+ invokedFromActorDispatcher: { identifier }
12950
+ });
11156
12951
  return await handleActor(request, {
11157
12952
  identifier,
11158
12953
  context,
@@ -11166,10 +12961,13 @@ var FederationImpl = class extends FederationBuilderImpl {
11166
12961
  const typeId = route.name.replace(/^object:/, "");
11167
12962
  const callbacks = this.objectCallbacks[typeId];
11168
12963
  const cls = this.objectTypeIds[typeId];
11169
- context = this.#createContext(request, contextData, { invokedFromObjectDispatcher: {
11170
- cls,
11171
- values: route.values
11172
- } });
12964
+ context = this.#createContext(request, contextData, {
12965
+ [outboundProvenance]: getOutboundProvenance(context),
12966
+ invokedFromObjectDispatcher: {
12967
+ cls,
12968
+ values: route.values
12969
+ }
12970
+ });
11173
12971
  return await handleObject(request, {
11174
12972
  values: route.values,
11175
12973
  context,
@@ -11227,7 +13025,10 @@ var FederationImpl = class extends FederationBuilderImpl {
11227
13025
  case "sharedInbox":
11228
13026
  if (this.sharedInboxKeyDispatcher != null) {
11229
13027
  const identity = await this.sharedInboxKeyDispatcher(context);
11230
- if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
13028
+ if (identity != null) context = this.#createContext(request, contextData, {
13029
+ [outboundProvenance]: getOutboundProvenance(context),
13030
+ documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity)
13031
+ });
11231
13032
  }
11232
13033
  return await this.#handleInbox(request, {
11233
13034
  recipient: route.values.identifier ?? null,
@@ -11371,14 +13172,17 @@ var FederationImpl = class extends FederationBuilderImpl {
11371
13172
  });
11372
13173
  return response;
11373
13174
  }
13175
+ const provenance = globalThis.Object.freeze({ originatingRequestId: metricState.requestId });
11374
13176
  if (collectionRoute != null) return await this.#fetchPortableCollection(request, portable, route, {
11375
13177
  onNotFound,
11376
13178
  onUnauthorized,
11377
- contextData
13179
+ contextData,
13180
+ provenance
11378
13181
  });
11379
13182
  if (isActor) {
11380
13183
  const identifier = route.name.startsWith("actorAlias:") ? route.name.substring(11) : route.values.identifier;
11381
13184
  const context = this.#createContext(request, contextData, {
13185
+ [outboundProvenance]: provenance,
11382
13186
  invokedFromActorDispatcher: { identifier },
11383
13187
  portableRequest: portable.portableRequest
11384
13188
  });
@@ -11399,6 +13203,7 @@ var FederationImpl = class extends FederationBuilderImpl {
11399
13203
  const callbacks = this.objectCallbacks[typeId];
11400
13204
  const cls = this.objectTypeIds[typeId];
11401
13205
  const context = this.#createContext(request, contextData, {
13206
+ [outboundProvenance]: provenance,
11402
13207
  invokedFromObjectDispatcher: {
11403
13208
  cls,
11404
13209
  values: route.values
@@ -11416,7 +13221,7 @@ var FederationImpl = class extends FederationBuilderImpl {
11416
13221
  onNotFound
11417
13222
  });
11418
13223
  }
11419
- async #fetchPortableCollection(request, portable, route, { onNotFound, onUnauthorized, contextData }) {
13224
+ async #fetchPortableCollection(request, portable, route, { onNotFound, onUnauthorized, contextData, provenance }) {
11420
13225
  const logger = (0, _logtape_logtape.getLogger)([
11421
13226
  "fedify",
11422
13227
  "federation",
@@ -11424,8 +13229,11 @@ var FederationImpl = class extends FederationBuilderImpl {
11424
13229
  ]);
11425
13230
  const url = new URL(request.url);
11426
13231
  const { authority } = portable.portableRequest;
11427
- const ownerContext = this.#createContext(request, contextData);
11428
- const context = this.#createContext(request, contextData, { portableRequest: portable.portableRequest });
13232
+ const ownerContext = this.#createContext(request, contextData, { [outboundProvenance]: provenance });
13233
+ const context = this.#createContext(request, contextData, {
13234
+ [outboundProvenance]: provenance,
13235
+ portableRequest: portable.portableRequest
13236
+ });
11429
13237
  const reject = async (reason, identifier) => {
11430
13238
  logger.debug("Not serving the portable collection {collection} of the actor {identifier}: {reason}.", {
11431
13239
  collection: portable.canonicalId,
@@ -11518,6 +13326,11 @@ var FederationImpl = class extends FederationBuilderImpl {
11518
13326
  }
11519
13327
  return await onNotFound(request);
11520
13328
  }
13329
+ #createInboxRequestSource(request, metricState) {
13330
+ const source = createInboxRequestSource(request, metricState.requestId ?? getRequestId(request), this.origin?.webOrigin);
13331
+ metricState.inboxRequest = source;
13332
+ return source;
13333
+ }
11521
13334
  #setInboxCompletion(metricState, observation, getContext) {
11522
13335
  const handler = this.inboxRequestFinishedHandler;
11523
13336
  metricState.inboxCompletion = {
@@ -11549,6 +13362,7 @@ var FederationImpl = class extends FederationBuilderImpl {
11549
13362
  async #reportInboxPreparationError(request, contextData, inbox, error, span, metricState) {
11550
13363
  const unavailableLoader = () => Promise.reject(error);
11551
13364
  const context = new RequestContextImpl({
13365
+ [outboundProvenance]: globalThis.Object.freeze({ inboxRequest: metricState.inboxRequest }),
11552
13366
  url: new URL(request.url),
11553
13367
  request,
11554
13368
  federation: this,
@@ -11556,7 +13370,7 @@ var FederationImpl = class extends FederationBuilderImpl {
11556
13370
  documentLoader: unavailableLoader,
11557
13371
  contextLoader: unavailableLoader
11558
13372
  });
11559
- const observation = new InboxObservation(inbox);
13373
+ const observation = new InboxObservation(inbox, metricState.inboxRequest);
11560
13374
  this.#setInboxCompletion(metricState, observation, () => context);
11561
13375
  return await observation.run(() => Promise.reject(error), span);
11562
13376
  }
@@ -11614,9 +13428,10 @@ var FederationImpl = class extends FederationBuilderImpl {
11614
13428
  metricState.endpoint = "inbox";
11615
13429
  span.updateName(`${request.method} ${metricState.routeTemplate}`);
11616
13430
  const identifier = route.values.identifier;
13431
+ const source = this.#createInboxRequestSource(request, metricState);
11617
13432
  let context;
11618
13433
  try {
11619
- context = this.#createContext(request, contextData);
13434
+ context = (0, _logtape_logtape.withContext)({ inboxRequestId: source.inboxRequestId }, () => this.#createContext(request, contextData, { [outboundProvenance]: globalThis.Object.freeze({ inboxRequest: source }) }));
11620
13435
  } catch (error) {
11621
13436
  return await this.#reportInboxPreparationError(request, contextData, {
11622
13437
  kind: "portable",
@@ -11626,9 +13441,9 @@ var FederationImpl = class extends FederationBuilderImpl {
11626
13441
  const observation = new InboxObservation({
11627
13442
  kind: "portable",
11628
13443
  recipient: identifier
11629
- });
13444
+ }, source);
11630
13445
  this.#setInboxCompletion(metricState, observation, () => context);
11631
- return await observation.run(async () => {
13446
+ return await (0, _logtape_logtape.withContext)({ inboxRequestId: source.inboxRequestId }, () => observation.run(async () => {
11632
13447
  const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
11633
13448
  const localOrigin = context.canonicalOrigin;
11634
13449
  const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof _fedify_vocab.Tombstone ? null : actor, {
@@ -11645,7 +13460,10 @@ var FederationImpl = class extends FederationBuilderImpl {
11645
13460
  return await onNotFound(request);
11646
13461
  }
11647
13462
  const { recipient } = resolution;
11648
- if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
13463
+ if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, {
13464
+ [outboundProvenance]: getOutboundProvenance(context),
13465
+ documentLoader: await context.getDocumentLoader({ identifier })
13466
+ });
11649
13467
  const excludedOrigins = [localOrigin, new URL(request.url).origin];
11650
13468
  return await this.#handleInbox(request, {
11651
13469
  recipient: identifier,
@@ -11655,16 +13473,25 @@ var FederationImpl = class extends FederationBuilderImpl {
11655
13473
  onNotFound,
11656
13474
  portableInbox: {
11657
13475
  recipient,
11658
- forward: async (activity, activityId, activityType, signatures) => {
11659
- if (this.portableInboxForwarding.maxTargets < 1) return;
13476
+ forward: (activity, activityId, activityType, signatures) => observeOutboundBroadcast(context, this.outboundObservers, async (observation) => {
13477
+ observation?.setActivity({
13478
+ ...activityId == null ? {} : { id: activityId.href },
13479
+ type: activityType
13480
+ });
13481
+ if (this.portableInboxForwarding.maxTargets < 1) {
13482
+ if (observation != null) await observation.skipped("forwarding-disabled");
13483
+ return;
13484
+ }
11660
13485
  if (this.kv.cas == null) {
11661
13486
  if (!this.#portableInboxForwardingWarned) {
11662
13487
  this.#portableInboxForwardingWarned = true;
11663
13488
  logger.warn("Activities delivered to FEP-ef61 portable inboxes are not forwarded to the other gateways, as the key–value store does not support compare-and-swap (KvStore.cas()), which is needed to forward each activity at most once.");
11664
13489
  }
13490
+ if (observation != null) await observation.skipped("forwarding-unavailable");
11665
13491
  return;
11666
13492
  }
11667
13493
  await forwardPortableInboxActivity({
13494
+ provenance: getOutboundProvenance(context),
11668
13495
  recipient,
11669
13496
  activity,
11670
13497
  activityId,
@@ -11682,11 +13509,11 @@ var FederationImpl = class extends FederationBuilderImpl {
11682
13509
  options: this.portableInboxForwarding,
11683
13510
  meterProvider: this.meterProvider,
11684
13511
  tracerProvider: this.tracerProvider
11685
- });
11686
- }
13512
+ }, observation);
13513
+ }, "portable-replication")
11687
13514
  }
11688
13515
  });
11689
- }, span);
13516
+ }, span));
11690
13517
  }
11691
13518
  /**
11692
13519
  * Gets this server's gateway key pairs for the portable actor that owns
@@ -11756,7 +13583,7 @@ var FederationImpl = class extends FederationBuilderImpl {
11756
13583
  headers: { "Content-Type": "text/plain; charset=utf-8" }
11757
13584
  });
11758
13585
  }
11759
- const context = this.#createContext(request, contextData);
13586
+ const context = this.#createContext(request, contextData, { [outboundProvenance]: globalThis.Object.freeze({ originatingRequestId: metricState.requestId }) });
11760
13587
  const response = await this.hashlinkMediaDispatcher(context, media.media);
11761
13588
  if (response == null) return await onNotFound(request);
11762
13589
  return await discardBodyForHead(request, response, false);
@@ -11898,8 +13725,9 @@ var ContextImpl = class ContextImpl {
11898
13725
  verifyPortableObject;
11899
13726
  invokedFromActorKeyPairsDispatcher;
11900
13727
  #codec;
11901
- constructor({ url, federation, data, documentLoader, contextLoader, invokedFromActorKeyPairsDispatcher }) {
13728
+ constructor({ url, federation, data, documentLoader, contextLoader, invokedFromActorKeyPairsDispatcher, [outboundProvenance]: provenance = emptyProvenance }) {
11902
13729
  this.url = url;
13730
+ globalThis.Object.assign(this, { [outboundProvenance]: provenance });
11903
13731
  this.federation = federation;
11904
13732
  this.data = data;
11905
13733
  this.documentLoader = documentLoader;
@@ -11921,6 +13749,7 @@ var ContextImpl = class ContextImpl {
11921
13749
  }
11922
13750
  clone(data) {
11923
13751
  return new ContextImpl({
13752
+ [outboundProvenance]: getOutboundProvenance(this),
11924
13753
  url: this.url,
11925
13754
  federation: this.federation,
11926
13755
  data,
@@ -11931,6 +13760,7 @@ var ContextImpl = class ContextImpl {
11931
13760
  }
11932
13761
  toInboxContext(recipient, activity, activityId, activityType, metadata) {
11933
13762
  return new InboxContextImpl(recipient, activity, activityId, activityType, {
13763
+ [outboundProvenance]: getOutboundProvenance(this),
11934
13764
  url: this.url,
11935
13765
  federation: this.federation,
11936
13766
  data: this.data,
@@ -11941,6 +13771,7 @@ var ContextImpl = class ContextImpl {
11941
13771
  }
11942
13772
  toOutboxContext(identifier, activity, activityId, activityType) {
11943
13773
  return new OutboxContextImpl(identifier, activity, activityId, activityType, {
13774
+ [outboundProvenance]: getOutboundProvenance(this),
11944
13775
  url: this.url,
11945
13776
  federation: this.federation,
11946
13777
  data: this.data,
@@ -12330,36 +14161,49 @@ var ContextImpl = class ContextImpl {
12330
14161
  await this.#enqueueTasks(task, payloads, options);
12331
14162
  }
12332
14163
  sendActivity(sender, recipients, activity, options = {}) {
12333
- return this.tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan(this.federation.outboxQueue == null || options.immediate ? "activitypub.outbox" : "activitypub.fanout", {
12334
- kind: this.federation.outboxQueue == null || options.immediate ? _opentelemetry_api.SpanKind.CLIENT : _opentelemetry_api.SpanKind.PRODUCER,
12335
- attributes: {
12336
- "activitypub.activity.type": (0, _fedify_vocab.getTypeId)(activity).href,
12337
- "activitypub.activity.to": activity.toIds.map((to) => to.href),
12338
- "activitypub.activity.cc": activity.ccIds.map((cc) => cc.href),
12339
- "activitypub.activity.bto": activity.btoIds.map((bto) => bto.href),
12340
- "activitypub.activity.bcc": activity.bccIds.map((bcc) => bcc.href)
12341
- }
12342
- }, async (span) => {
12343
- try {
12344
- if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
12345
- await this.sendActivityInternal(sender, recipients, activity, options, span);
12346
- } catch (e) {
12347
- span.setStatus({
12348
- code: _opentelemetry_api.SpanStatusCode.ERROR,
12349
- message: String(e)
12350
- });
12351
- throw e;
12352
- } finally {
12353
- span.end();
12354
- }
12355
- });
14164
+ return observeOutboundBroadcast(this, this.federation.outboundObservers, (observation, provenance) => {
14165
+ const tracer = this.tracerProvider.getTracer(require_http.name, require_http.version);
14166
+ const parentSpanContext = _opentelemetry_api.trace.getSpanContext(_opentelemetry_api.context.active());
14167
+ const activityType = (0, _fedify_vocab.getTypeId)(activity).href;
14168
+ observation?.setActivity({ type: activityType });
14169
+ return tracer.startActiveSpan(this.federation.outboxQueue == null || options.immediate ? "activitypub.outbox" : "activitypub.fanout", {
14170
+ kind: this.federation.outboxQueue == null || options.immediate ? _opentelemetry_api.SpanKind.CLIENT : _opentelemetry_api.SpanKind.PRODUCER,
14171
+ attributes: {
14172
+ "activitypub.activity.type": activityType,
14173
+ "activitypub.activity.to": activity.toIds.map((to) => to.href),
14174
+ "activitypub.activity.cc": activity.ccIds.map((cc) => cc.href),
14175
+ "activitypub.activity.bto": activity.btoIds.map((bto) => bto.href),
14176
+ "activitypub.activity.bcc": activity.bccIds.map((bcc) => bcc.href)
14177
+ }
14178
+ }, async (span) => {
14179
+ observation?.setSpan(span, parentSpanContext);
14180
+ try {
14181
+ const activityId = activity.id?.href;
14182
+ if (activityId != null) span.setAttribute("activitypub.activity.id", activityId);
14183
+ observation?.setActivity({
14184
+ type: activityType,
14185
+ ...activityId == null ? {} : { id: activityId }
14186
+ });
14187
+ await this.sendActivityInternal(sender, recipients, activity, options, span, observation, provenance);
14188
+ } catch (e) {
14189
+ span.setStatus({
14190
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
14191
+ message: String(e)
14192
+ });
14193
+ throw e;
14194
+ } finally {
14195
+ span.end();
14196
+ }
14197
+ });
14198
+ }, "send", options.dispatchId);
12356
14199
  }
12357
- async sendActivityInternal(sender, recipients, activity, options, span) {
14200
+ async sendActivityInternal(sender, recipients, activity, options, span, observation, provenance = getOutboundProvenance(this)) {
12358
14201
  const logger = (0, _logtape_logtape.getLogger)([
12359
14202
  "fedify",
12360
14203
  "federation",
12361
14204
  "outbox"
12362
14205
  ]);
14206
+ observation?.setPhase("sender-resolution");
12363
14207
  let keys;
12364
14208
  let identifier = null;
12365
14209
  let actorKeyPairs = null;
@@ -12375,6 +14219,7 @@ var ContextImpl = class ContextImpl {
12375
14219
  }
12376
14220
  }
12377
14221
  span.setAttribute("fedify.actor.identifier", identifier);
14222
+ observation?.setPhase("key-resolution");
12378
14223
  if (this.federation.actorCallbacks?.keyPairsDispatcher == null) throw new Error("No actor key pairs dispatcher registered.");
12379
14224
  actorKeyPairs = await this.getActorKeyPairs(identifier);
12380
14225
  if (actorKeyPairs.length < 1) throw new Error(`No key pair found for actor ${JSON.stringify(identifier)}.`);
@@ -12383,11 +14228,14 @@ var ContextImpl = class ContextImpl {
12383
14228
  privateKey: kp.privateKey
12384
14229
  }));
12385
14230
  } else if (Array.isArray(sender)) {
14231
+ observation?.setPhase("key-resolution");
12386
14232
  if (sender.length < 1) throw new Error("The sender's key pairs are empty.");
12387
14233
  keys = sender;
12388
14234
  } else keys = [sender];
12389
14235
  if (keys.length < 1) throw new TypeError("The sender's keys must not be empty.");
14236
+ observation?.setPhase("key-resolution");
12390
14237
  for (const { privateKey } of keys) require_http.validateCryptoKey(privateKey, "private");
14238
+ observation?.setPhase("recipient-resolution");
12391
14239
  let expandedRecipients;
12392
14240
  let collectionSync;
12393
14241
  if (Array.isArray(recipients)) expandedRecipients = recipients;
@@ -12403,14 +14251,34 @@ var ContextImpl = class ContextImpl {
12403
14251
  } else expandedRecipients = [recipients];
12404
14252
  const opts = {
12405
14253
  context: this,
14254
+ observation,
14255
+ provenance,
12406
14256
  orderingKey: options.orderingKey,
12407
14257
  collectionSync,
12408
14258
  immediate: options.immediate,
12409
14259
  normalizeExistingProofs: options.normalizeExistingProofs
12410
14260
  };
12411
14261
  span.setAttribute("activitypub.inboxes", expandedRecipients.length);
14262
+ observation?.setPhase("transformation");
14263
+ if (this.federation.activityTransformers.length > 0) observation?.setActivity({});
12412
14264
  for (const activityTransformer of this.federation.activityTransformers) activity = activityTransformer(activity, this);
12413
- span?.setAttribute("activitypub.activity.id", activity?.id?.href ?? "");
14265
+ let transformedActivityId;
14266
+ try {
14267
+ transformedActivityId = activity?.id?.href;
14268
+ } finally {
14269
+ if (observation != null) {
14270
+ let activityType;
14271
+ try {
14272
+ activityType = (0, _fedify_vocab.getTypeId)(activity)?.href;
14273
+ } catch {}
14274
+ observation.setActivity({
14275
+ ...transformedActivityId == null ? {} : { id: transformedActivityId },
14276
+ ...activityType == null ? {} : { type: activityType }
14277
+ });
14278
+ }
14279
+ }
14280
+ span?.setAttribute("activitypub.activity.id", transformedActivityId ?? "");
14281
+ observation?.setPhase("validation");
12414
14282
  if (activity.actorId == null) {
12415
14283
  logger.error("Activity {activityId} to send does not have an actor.", {
12416
14284
  activity,
@@ -12418,6 +14286,7 @@ var ContextImpl = class ContextImpl {
12418
14286
  });
12419
14287
  throw new TypeError("The activity to send must have at least one actor property.");
12420
14288
  }
14289
+ observation?.setPhase("signing");
12421
14290
  const { activity: signedActivity, proofCreated } = await signOutgoingActivity(activity, actorKeyPairs == null ? keys.map(({ keyId, privateKey }) => ({
12422
14291
  verificationMethod: keyId,
12423
14292
  privateKey
@@ -12430,16 +14299,19 @@ var ContextImpl = class ContextImpl {
12430
14299
  appendToExistingProofs: actorKeyPairs != null
12431
14300
  });
12432
14301
  activity = signedActivity;
14302
+ observation?.setPhase("destination-planning");
12433
14303
  const inboxes = extractInboxes({
12434
14304
  recipients: expandedRecipients,
12435
14305
  preferSharedInbox: options.preferSharedInbox,
12436
14306
  excludeBaseUris: options.excludeBaseUris
12437
14307
  });
14308
+ if (observation != null) await observation.planDestinations(inboxes);
12438
14309
  if (globalThis.Object.keys(inboxes).length < 1) {
12439
14310
  logger.debug("No inboxes found for activity {activityId}.", {
12440
14311
  activityId: activity.id?.href,
12441
14312
  activity
12442
14313
  });
14314
+ if (observation != null) await observation.skipped();
12443
14315
  return false;
12444
14316
  }
12445
14317
  logger.debug("Sending activity {activityId} to inboxes:\n{inboxes}", {
@@ -12447,6 +14319,7 @@ var ContextImpl = class ContextImpl {
12447
14319
  activityId: activity.id?.href,
12448
14320
  activity
12449
14321
  });
14322
+ observation?.setPhase("serialization");
12450
14323
  const activityJsonLd = await activity.toJsonLd({
12451
14324
  format: "compact",
12452
14325
  contextLoader: this.contextLoader
@@ -12467,33 +14340,48 @@ var ContextImpl = class ContextImpl {
12467
14340
  });
12468
14341
  return true;
12469
14342
  }
14343
+ observation?.setPhase("validation");
12470
14344
  assertSupportedCompoundProofShape(activityJsonLd, activity.id?.href);
14345
+ observation?.setPhase("key-resolution");
12471
14346
  const keyJwkPairs = await Promise.all(keys.map(async ({ keyId, privateKey }) => ({
12472
14347
  keyId: keyId.href,
12473
14348
  privateKey: await require_http.exportJwk(privateKey)
12474
14349
  })));
14350
+ observation?.setPhase("request-preparation");
12475
14351
  const carrier = {};
12476
14352
  _opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), carrier);
14353
+ const activityId = activity.id?.href;
14354
+ const activityType = (0, _fedify_vocab.getTypeId)(activity).href;
14355
+ observation?.setActivity({
14356
+ ...activityId == null ? {} : { id: activityId },
14357
+ type: activityType
14358
+ });
12477
14359
  const message = {
12478
14360
  type: "fanout",
14361
+ ...serializeProvenance(provenance),
12479
14362
  id: crypto.randomUUID(),
12480
14363
  baseUrl: this.origin,
12481
14364
  keys: keyJwkPairs,
12482
14365
  inboxes: globalThis.Object.fromEntries(globalThis.Object.entries(inboxes).map(([k, { actorIds, sharedInbox, portableInbox, gatewayInboxes }]) => [k, {
12483
14366
  actorIds: [...actorIds],
14367
+ ...observation?.continuation(k),
12484
14368
  sharedInbox,
12485
14369
  ...getPortableMessageFields(portableInbox, gatewayInboxes)
12486
14370
  }])),
12487
14371
  activity: activityJsonLd,
12488
- activityId: activity.id?.href,
12489
- activityType: (0, _fedify_vocab.getTypeId)(activity).href,
14372
+ activityId,
14373
+ activityType,
12490
14374
  collectionSync: opts.collectionSync,
12491
14375
  orderingKey: options.orderingKey,
12492
14376
  normalizeExistingProofs: proofCreated || options.normalizeExistingProofs,
12493
14377
  traceContext: carrier
12494
14378
  };
12495
14379
  if (!this.federation.manuallyStartQueue) this.federation._startQueueInternal(this.data);
12496
- await this.federation.fanoutQueue.enqueue(message, { orderingKey: options.orderingKey });
14380
+ if (observation != null) await observation.prepared("fanout");
14381
+ await (observation == null ? this.federation.fanoutQueue.enqueue(message, { orderingKey: options.orderingKey }) : observation.enqueue(globalThis.Object.keys(inboxes).map((inbox) => ({
14382
+ inbox,
14383
+ queueMessageId: message.id
14384
+ })), "fanout", () => this.federation.fanoutQueue.enqueue(message, { orderingKey: options.orderingKey })));
12497
14385
  require_http.getFederationMetrics(this.federation.meterProvider).recordQueueTaskEnqueued({
12498
14386
  role: "fanout",
12499
14387
  queue: this.federation.fanoutQueue,
@@ -12754,6 +14642,7 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
12754
14642
  }
12755
14643
  clone(data) {
12756
14644
  return new RequestContextImpl({
14645
+ [outboundProvenance]: getOutboundProvenance(this),
12757
14646
  url: this.url,
12758
14647
  federation: this.federation,
12759
14648
  data,
@@ -12925,30 +14814,40 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
12925
14814
  }
12926
14815
  };
12927
14816
  function forwardActivity(ctx, loggerCategory, forwarder, recipients, options) {
12928
- return ctx.tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan(ctx.federation.outboxQueue == null || options?.immediate ? `activitypub.${loggerCategory}` : "activitypub.fanout", {
12929
- kind: ctx.federation.outboxQueue == null || options?.immediate ? _opentelemetry_api.SpanKind.CLIENT : _opentelemetry_api.SpanKind.PRODUCER,
12930
- attributes: { "activitypub.activity.type": ctx.activityType }
12931
- }, async (span) => {
12932
- try {
12933
- if (ctx.activityId != null) span.setAttribute("activitypub.activity.id", ctx.activityId);
12934
- return await forwardActivityInternal(ctx, loggerCategory, forwarder, recipients, options);
12935
- } catch (e) {
12936
- span.setStatus({
12937
- code: _opentelemetry_api.SpanStatusCode.ERROR,
12938
- message: String(e)
12939
- });
12940
- throw e;
12941
- } finally {
12942
- span.end();
12943
- }
12944
- });
14817
+ return observeOutboundBroadcast(ctx, ctx.federation.outboundObservers, (observation, provenance) => {
14818
+ observation?.setActivity({
14819
+ ...ctx.activityId == null ? {} : { id: ctx.activityId },
14820
+ type: ctx.activityType
14821
+ });
14822
+ const tracer = ctx.tracerProvider.getTracer(require_http.name, require_http.version);
14823
+ const parentSpanContext = _opentelemetry_api.trace.getSpanContext(_opentelemetry_api.context.active());
14824
+ return tracer.startActiveSpan(ctx.federation.outboxQueue == null || options?.immediate ? `activitypub.${loggerCategory}` : "activitypub.fanout", {
14825
+ kind: ctx.federation.outboxQueue == null || options?.immediate ? _opentelemetry_api.SpanKind.CLIENT : _opentelemetry_api.SpanKind.PRODUCER,
14826
+ attributes: { "activitypub.activity.type": ctx.activityType }
14827
+ }, async (span) => {
14828
+ observation?.setSpan(span, parentSpanContext);
14829
+ try {
14830
+ if (ctx.activityId != null) span.setAttribute("activitypub.activity.id", ctx.activityId);
14831
+ return await forwardActivityInternal(ctx, loggerCategory, forwarder, recipients, options, observation, provenance);
14832
+ } catch (e) {
14833
+ span.setStatus({
14834
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
14835
+ message: String(e)
14836
+ });
14837
+ throw e;
14838
+ } finally {
14839
+ span.end();
14840
+ }
14841
+ });
14842
+ }, "forward", options?.dispatchId);
12945
14843
  }
12946
- async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipients, options) {
14844
+ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipients, options, observation, provenance = getOutboundProvenance(ctx)) {
12947
14845
  const logger = (0, _logtape_logtape.getLogger)([
12948
14846
  "fedify",
12949
14847
  "federation",
12950
14848
  loggerCategory
12951
14849
  ]);
14850
+ observation?.setPhase("sender-resolution");
12952
14851
  let keys;
12953
14852
  let identifier = null;
12954
14853
  if ("identifier" in forwarder || "username" in forwarder) {
@@ -12962,6 +14861,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
12962
14861
  identifier = mapped;
12963
14862
  }
12964
14863
  }
14864
+ observation?.setPhase("key-resolution");
12965
14865
  const actorKeyPairs = await ctx.getActorKeyPairs(identifier);
12966
14866
  if (actorKeyPairs.length < 1) throw new Error(`No key pair found for actor ${JSON.stringify(identifier)}.`);
12967
14867
  keys = actorKeyPairs.map((kp) => ({
@@ -12969,12 +14869,17 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
12969
14869
  privateKey: kp.privateKey
12970
14870
  }));
12971
14871
  } else if (Array.isArray(forwarder)) {
14872
+ observation?.setPhase("key-resolution");
12972
14873
  if (forwarder.length < 1) throw new Error("The forwarder's key pairs are empty.");
12973
14874
  keys = forwarder;
12974
14875
  } else keys = [forwarder];
14876
+ observation?.setPhase("validation");
12975
14877
  if (!require_http.hasSignatureLike(ctx.activity)) {
12976
14878
  if (!require_http.hasProofLike(ctx.activity)) {
12977
- if (options?.skipIfUnsigned) return false;
14879
+ if (options?.skipIfUnsigned) {
14880
+ if (observation != null) await observation.skipped("unsigned");
14881
+ return false;
14882
+ }
12978
14883
  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.", {
12979
14884
  activityId: ctx.activityId,
12980
14885
  activityType: ctx.activityType,
@@ -12982,23 +14887,27 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
12982
14887
  });
12983
14888
  }
12984
14889
  }
14890
+ observation?.setPhase("recipient-resolution");
12985
14891
  if (recipients === "followers") {
12986
14892
  if (identifier == null) throw new Error("If recipients is \"followers\", forwarder must be an actor identifier or username.");
12987
14893
  const followers = [];
12988
14894
  for await (const recipient of ctx.getFollowers(identifier)) followers.push(recipient);
12989
14895
  recipients = followers;
12990
14896
  }
14897
+ observation?.setPhase("destination-planning");
12991
14898
  const inboxes = extractInboxes({
12992
14899
  recipients: Array.isArray(recipients) ? recipients : [recipients],
12993
14900
  preferSharedInbox: options?.preferSharedInbox,
12994
14901
  excludeBaseUris: options?.excludeBaseUris
12995
14902
  });
14903
+ if (observation != null) await observation.planDestinations(inboxes);
12996
14904
  if (globalThis.Object.keys(inboxes).length < 1) {
12997
14905
  logger.debug("No inboxes found for activity {activityId}.", {
12998
14906
  activityId: ctx.activityId,
12999
14907
  activityType: ctx.activityType,
13000
14908
  identifier: identifier ?? void 0
13001
14909
  });
14910
+ if (observation != null) await observation.skipped();
13002
14911
  return false;
13003
14912
  }
13004
14913
  logger.debug("Forwarding activity {activityId} to inboxes:\n{inboxes}", {
@@ -13009,18 +14918,33 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
13009
14918
  if (options?.immediate || ctx.federation.outboxQueue == null) {
13010
14919
  if (options?.immediate) logger.debug("Forwarding activity immediately without queue since immediate option is set.");
13011
14920
  else logger.debug("Forwarding activity immediately without queue since queue is not set.");
14921
+ observation?.setPhase("request-preparation");
13012
14922
  const promises = [];
13013
- for (const inbox in inboxes) promises.push(sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
13014
- allowPrivateAddress: ctx.federation.allowPrivateAddress,
13015
- keys,
13016
- activity: ctx.activity,
13017
- activityId: ctx.activityId,
13018
- activityType: ctx.activityType,
13019
- sharedInbox: inboxes[inbox].sharedInbox,
13020
- meterProvider: ctx.meterProvider,
13021
- tracerProvider: ctx.tracerProvider,
13022
- specDeterminer: new KvSpecDeterminer(ctx.federation.kv, ctx.federation.kvPrefixes.httpMessageSignaturesSpec, ctx.federation.firstKnock, { specTtl: ctx.federation.httpMessageSignaturesSpecTtl })
13023
- }));
14923
+ for (const inbox in inboxes) {
14924
+ const delivery = observation?.delivery(inbox);
14925
+ let sending;
14926
+ try {
14927
+ sending = sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
14928
+ observation: delivery,
14929
+ allowPrivateAddress: ctx.federation.allowPrivateAddress,
14930
+ keys,
14931
+ activity: ctx.activity,
14932
+ activityId: ctx.activityId,
14933
+ activityType: ctx.activityType,
14934
+ sharedInbox: inboxes[inbox].sharedInbox,
14935
+ meterProvider: ctx.meterProvider,
14936
+ tracerProvider: ctx.tracerProvider,
14937
+ specDeterminer: new KvSpecDeterminer(ctx.federation.kv, ctx.federation.kvPrefixes.httpMessageSignaturesSpec, ctx.federation.firstKnock, { specTtl: ctx.federation.httpMessageSignaturesSpecTtl })
14938
+ });
14939
+ } catch (error) {
14940
+ if (delivery != null) await delivery.failed(error);
14941
+ throw error;
14942
+ }
14943
+ if (observation != null) sending.catch(() => {});
14944
+ if (delivery != null) await delivery.handoff({ type: "direct" });
14945
+ promises.push(sending);
14946
+ }
14947
+ if (observation != null) await observation.prepared("delivery");
13024
14948
  await Promise.all(promises);
13025
14949
  return true;
13026
14950
  }
@@ -13028,7 +14952,9 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
13028
14952
  activityId: ctx.activityId,
13029
14953
  activity: ctx.activity
13030
14954
  });
14955
+ observation?.setPhase("request-preparation");
13031
14956
  if (!ctx.federation.manuallyStartQueue) ctx.federation._startQueueInternal(ctx.data);
14957
+ observation?.setPhase("key-resolution");
13032
14958
  const keyJwkPairs = [];
13033
14959
  for (const { keyId, privateKey } of keys) {
13034
14960
  const privateKeyJwk = await require_http.exportJwk(privateKey);
@@ -13037,15 +14963,17 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
13037
14963
  privateKey: privateKeyJwk
13038
14964
  });
13039
14965
  }
14966
+ observation?.setPhase("request-preparation");
13040
14967
  const carrier = {};
13041
14968
  _opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), carrier);
13042
14969
  const orderingKey = options?.orderingKey;
13043
14970
  const started = (/* @__PURE__ */ new Date()).toISOString();
13044
14971
  const messages = [];
13045
- for (const inbox in inboxes) {
14972
+ for (const inbox in inboxes) try {
13046
14973
  const { portableInbox, gatewayInboxes } = inboxes[inbox];
13047
14974
  const message = {
13048
14975
  type: "outbox",
14976
+ ...serializeProvenance(provenance),
13049
14977
  id: crypto.randomUUID(),
13050
14978
  baseUrl: ctx.origin,
13051
14979
  keys: keyJwkPairs,
@@ -13053,6 +14981,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
13053
14981
  activityId: ctx.activityId,
13054
14982
  activityType: ctx.activityType,
13055
14983
  inbox,
14984
+ ...observation?.continuation(inbox),
13056
14985
  sharedInbox: inboxes[inbox].sharedInbox,
13057
14986
  actorIds: [...inboxes[inbox].actorIds],
13058
14987
  started,
@@ -13066,11 +14995,18 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
13066
14995
  message,
13067
14996
  orderingKey: message.orderingKey
13068
14997
  });
14998
+ } catch (error) {
14999
+ if (observation != null) await observation.delivery(inbox)?.failed(error);
15000
+ throw error;
13069
15001
  }
15002
+ if (observation != null) await observation.prepared("delivery");
13070
15003
  const { outboxQueue } = ctx.federation;
13071
15004
  if (outboxQueue.enqueueMany == null || orderingKey != null) {
13072
15005
  const errors = (await Promise.allSettled(messages.map(async (m) => {
13073
- await outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey });
15006
+ await (observation == null ? outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }) : observation.enqueue([{
15007
+ inbox: m.message.inbox,
15008
+ queueMessageId: m.message.id
15009
+ }], "outbox", () => outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey })));
13074
15010
  require_http.recordOutboxEnqueue(ctx.federation.meterProvider, outboxQueue, m.message);
13075
15011
  }))).filter((r) => r.status === "rejected").map((r) => r.reason);
13076
15012
  if (errors.length > 0) {
@@ -13083,7 +15019,10 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
13083
15019
  }
13084
15020
  } else {
13085
15021
  try {
13086
- await outboxQueue.enqueueMany(messages.map((m) => m.message));
15022
+ await (observation == null ? outboxQueue.enqueueMany(messages.map((m) => m.message)) : observation.enqueue(messages.map(({ message }) => ({
15023
+ inbox: message.inbox,
15024
+ queueMessageId: message.id
15025
+ })), "outbox", () => outboxQueue.enqueueMany(messages.map((m) => m.message))));
13087
15026
  } catch (error) {
13088
15027
  logger.error("Failed to enqueue activity {activityId} to forward later:\n{error}", {
13089
15028
  activityId: ctx.activityId,
@@ -13119,6 +15058,7 @@ var InboxContextImpl = class InboxContextImpl extends ContextImpl {
13119
15058
  }
13120
15059
  clone(data) {
13121
15060
  return new InboxContextImpl(this.recipient, this.activity, this.activityId, this.activityType, {
15061
+ [outboundProvenance]: getOutboundProvenance(this),
13122
15062
  url: this.url,
13123
15063
  federation: this.federation,
13124
15064
  data,
@@ -13149,29 +15089,41 @@ var OutboxContextImpl = class OutboxContextImpl extends ContextImpl {
13149
15089
  return this.#deliveryState.delivered;
13150
15090
  }
13151
15091
  sendActivity(sender, recipients, activity, options = {}) {
13152
- return this.tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan(this.federation.outboxQueue == null || options.immediate ? "activitypub.outbox" : "activitypub.fanout", {
13153
- kind: this.federation.outboxQueue == null || options.immediate ? _opentelemetry_api.SpanKind.CLIENT : _opentelemetry_api.SpanKind.PRODUCER,
13154
- attributes: {
13155
- "activitypub.activity.type": (0, _fedify_vocab.getTypeId)(activity).href,
13156
- "activitypub.activity.to": activity.toIds.map((to) => to.href),
13157
- "activitypub.activity.cc": activity.ccIds.map((cc) => cc.href),
13158
- "activitypub.activity.bto": activity.btoIds.map((bto) => bto.href),
13159
- "activitypub.activity.bcc": activity.bccIds.map((bcc) => bcc.href)
13160
- }
13161
- }, async (span) => {
13162
- try {
13163
- if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
13164
- if (await this.sendActivityInternal(sender, recipients, activity, options, span)) this.#deliveryState.delivered = true;
13165
- } catch (e) {
13166
- span.setStatus({
13167
- code: _opentelemetry_api.SpanStatusCode.ERROR,
13168
- message: String(e)
13169
- });
13170
- throw e;
13171
- } finally {
13172
- span.end();
13173
- }
13174
- });
15092
+ return observeOutboundBroadcast(this, this.federation.outboundObservers, (observation, provenance) => {
15093
+ const tracer = this.tracerProvider.getTracer(require_http.name, require_http.version);
15094
+ const parentSpanContext = _opentelemetry_api.trace.getSpanContext(_opentelemetry_api.context.active());
15095
+ const activityType = (0, _fedify_vocab.getTypeId)(activity).href;
15096
+ observation?.setActivity({ type: activityType });
15097
+ return tracer.startActiveSpan(this.federation.outboxQueue == null || options.immediate ? "activitypub.outbox" : "activitypub.fanout", {
15098
+ kind: this.federation.outboxQueue == null || options.immediate ? _opentelemetry_api.SpanKind.CLIENT : _opentelemetry_api.SpanKind.PRODUCER,
15099
+ attributes: {
15100
+ "activitypub.activity.type": activityType,
15101
+ "activitypub.activity.to": activity.toIds.map((to) => to.href),
15102
+ "activitypub.activity.cc": activity.ccIds.map((cc) => cc.href),
15103
+ "activitypub.activity.bto": activity.btoIds.map((bto) => bto.href),
15104
+ "activitypub.activity.bcc": activity.bccIds.map((bcc) => bcc.href)
15105
+ }
15106
+ }, async (span) => {
15107
+ observation?.setSpan(span, parentSpanContext);
15108
+ try {
15109
+ const activityId = activity.id?.href;
15110
+ if (activityId != null) span.setAttribute("activitypub.activity.id", activityId);
15111
+ observation?.setActivity({
15112
+ type: activityType,
15113
+ ...activityId == null ? {} : { id: activityId }
15114
+ });
15115
+ if (await this.sendActivityInternal(sender, recipients, activity, options, span, observation, provenance)) this.#deliveryState.delivered = true;
15116
+ } catch (e) {
15117
+ span.setStatus({
15118
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
15119
+ message: String(e)
15120
+ });
15121
+ throw e;
15122
+ } finally {
15123
+ span.end();
15124
+ }
15125
+ });
15126
+ }, "send", options.dispatchId);
13175
15127
  }
13176
15128
  forwardActivity(forwarder, recipients, options) {
13177
15129
  return forwardActivity(this, "outbox", forwarder, recipients, options).then((delivered) => {
@@ -13180,6 +15132,7 @@ var OutboxContextImpl = class OutboxContextImpl extends ContextImpl {
13180
15132
  }
13181
15133
  clone(data) {
13182
15134
  return new OutboxContextImpl(this.identifier, this.activity, this.activityId, this.activityType, {
15135
+ [outboundProvenance]: getOutboundProvenance(this),
13183
15136
  url: this.url,
13184
15137
  federation: this.federation,
13185
15138
  data,