@fedify/fedify 2.5.0-dev.2597 → 2.5.0-dev.2614

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-Da_V_LDj.mjs → builder-B2H8GsyI.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-BqqP38Sf.mjs → compound-proof-BavXW-ow.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-4cGKg59R.mjs → deno-CRXki9-m.mjs} +1 -1
  9. package/dist/{docloader-BiHD6NSO.mjs → docloader-Ca6i6ddp.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-BKJZEC0O.cjs → federation-DRl81KbJ.cjs} +2358 -405
  59. package/dist/{http-DQP4rw46.js → http-C2H4DT6t.js} +62 -32
  60. package/dist/{http-KU7_TLD9.cjs → http-DHSIMipE.cjs} +62 -32
  61. package/dist/{http-D5n4S1oL.mjs → http-DqOXkYbX.mjs} +64 -34
  62. package/dist/{inbox-CElAglrC.mjs → inbox-C4iW1g0F.mjs} +90 -3
  63. package/dist/{inbox-view-BTUuIIf6.mjs → inbox-view-Bu_aSwFJ.mjs} +1 -1
  64. package/dist/{key-DbLNT-vA.d.ts → key-dwwAg5bA.d.ts} +1 -1
  65. package/dist/{kv-cache-CFBwLUZr.js → kv-cache-B-x_OrBo.js} +1 -1
  66. package/dist/{kv-cache-DrQn-ugb.cjs → kv-cache-CHR_BVyM.cjs} +1 -1
  67. package/dist/{kv-cache-BV55jAE6.mjs → kv-cache-DJsxu9nI.mjs} +1 -1
  68. package/dist/{ld-CmAQYDWE.mjs → ld-C5JOBBFj.mjs} +2 -2
  69. package/dist/{ld-Inagww4R.d.ts → ld-o4LnhCYY.d.ts} +2 -2
  70. package/dist/{metrics-BvV60TwE.mjs → metrics-CTteRnEr.mjs} +1 -1
  71. package/dist/{metrics-DoRtI0qU.d.ts → metrics-DVKYY6K1.d.ts} +1 -1
  72. package/dist/{middleware-Di4y_M3y.js → middleware-B-tMVSUA.js} +2303 -351
  73. package/dist/{middleware-D02eC0j7.mjs → middleware-BkRqMGPU.mjs} +1573 -325
  74. package/dist/{middleware-iAJcDIM2.mjs → middleware-CPigKLnt.mjs} +1 -1
  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-C1-csjTo.mjs → owner-CKlRIkeF.mjs} +3 -3
  88. package/dist/{portable-collection-C0MNEEED.mjs → portable-collection-B7RBHVGl.mjs} +2 -2
  89. package/dist/{portable-collection-DkTLVfoM.js → portable-collection-CByyPjLc.js} +2 -2
  90. package/dist/{portable-collection-w6Qn-vJ3.cjs → portable-collection-CmLjeKsP.cjs} +2 -2
  91. package/dist/{portable-key-jtaxB7TD.mjs → portable-key-Clg0IH8C.mjs} +1 -1
  92. package/dist/{proof-CzWCRhnC.mjs → proof-tAb5_Dq4.mjs} +3 -3
  93. package/dist/{send-C9-dwpEH.mjs → send-DzfFREML.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-lDCl8Zxn.mjs → temporal-D9Nt8Fm4.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 +6 -6
  123. package/skills/fedify/SKILL.md +60 -1
@@ -5,48 +5,832 @@ import { d as isPortableUri, f as isSameObjectId, i as getPortableDid, l as isPo
5
5
  import { n as isPubliclyAddressedNode, t as isInAudience } from "./audience-CHQ-FlLD.mjs";
6
6
  import { i as getEmptyAuthenticationEvidence, n as deserializeAuthentication, s as toProofPrincipal, t as createAuthenticationEvidence } from "./inbox-authentication-CgonVRkg.mjs";
7
7
  import { a as verificationObservation, r as snapshotKey, t as observeAttempt } from "./verification-Dpfcw8ml.mjs";
8
- import { n as version, t as name } from "./deno-4cGKg59R.mjs";
9
- import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-BvV60TwE.mjs";
8
+ import { n as version, t as name } from "./deno-CRXki9-m.mjs";
9
+ import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-CTteRnEr.mjs";
10
10
  import { t as formatAcceptSignature } from "./accept-CPVqki78.mjs";
11
11
  import { r as getVerifiedKeyOwnerEvidence } from "./key-owner-evidence-APB6NpSk.mjs";
12
- import { D as validateCryptoKey, E as importJwk, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, g as verifyCompactJsonLdDetailed, l as hasSignature, m as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike, x as exportJwk, y as wrapContextLoaderForJsonLd } from "./ld-CmAQYDWE.mjs";
12
+ import { D as validateCryptoKey, E as importJwk, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, g as verifyCompactJsonLdDetailed, l as hasSignature, m as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike, x as exportJwk, y as wrapContextLoaderForJsonLd } from "./ld-C5JOBBFj.mjs";
13
13
  import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-4IJqPYos.mjs";
14
- import { a as hasProofLike, d as verifyPortableObjectProof, i as getProofSigner, l as verifyObject, p as verifyPortableObjectProofWithRoot, s as signObject, t as InvalidPortableObjectIdError, u as verifyObjectDetailed } from "./proof-CzWCRhnC.mjs";
15
- import { c as parseRfc9421SignatureInput, f as validateMaxSignatures, l as selectRequestSignature, m as verifyRequestDetailed, o as listRequestSignatures, p as verifyRequest } from "./http-D5n4S1oL.mjs";
16
- import { n as getAuthenticatedDocumentLoader } from "./docloader-BiHD6NSO.mjs";
17
- import { n as kvCache } from "./kv-cache-BV55jAE6.mjs";
14
+ import { a as hasProofLike, d as verifyPortableObjectProof, i as getProofSigner, l as verifyObject, p as verifyPortableObjectProofWithRoot, s as signObject, t as InvalidPortableObjectIdError, u as verifyObjectDetailed } from "./proof-tAb5_Dq4.mjs";
15
+ import { c as parseRfc9421SignatureInput, f as validateMaxSignatures, l as selectRequestSignature, m as verifyRequestDetailed, o as listRequestSignatures, p as verifyRequest } from "./http-DqOXkYbX.mjs";
16
+ import { n as getAuthenticatedDocumentLoader } from "./docloader-Ca6i6ddp.mjs";
17
+ import { n as kvCache } from "./kv-cache-DJsxu9nI.mjs";
18
+ import { a as isActorlessActivityAuthenticated, c as createInboxRequestSource, d as invocationProvenance, f as outboundProvenance, g as sourceLogContext, h as snapshotLocalContext, i as getInboxIdempotencyKey, l as emptyProvenance, m as serializeProvenance, n as checkActorlessActivity, o as isInboxActivityProcessed, p as restoreProvenance, s as routeActivity, t as actorsHaveSameOrigin, u as getOutboundProvenance } from "./inbox-C4iW1g0F.mjs";
19
+ import { t as projectSenderPayload } from "./outbound-document-BMgwLXTY.mjs";
20
+ import { t as captureResponseBody } from "./outbound-capture-DVgnRzlf.mjs";
21
+ import { a as snapshotHeaders, n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-B2H8GsyI.mjs";
18
22
  import { d as MAX_BODY_SIZE, f as readBoundedText, t as getNodeInfo, u as BodyTooLargeError } from "./client-5SIFzW3q.mjs";
19
23
  import { t as nodeInfoToJson } from "./types-DE_LV1JB.mjs";
20
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-C1-csjTo.mjs";
21
- import { t as verifyPortableObject } from "./portable-collection-C0MNEEED.mjs";
22
- import { a as sendActivityThroughGateways, i as sendActivity, n as extractInboxes, o as getOrderingDestination, t as SendActivityError } from "./send-C9-dwpEH.mjs";
23
- import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-Da_V_LDj.mjs";
24
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-CKlRIkeF.mjs";
25
+ import { t as verifyPortableObject } from "./portable-collection-B7RBHVGl.mjs";
26
+ import { a as sendActivityThroughGateways, i as sendActivity, n as extractInboxes, o as getOrderingDestination, t as SendActivityError } from "./send-DzfFREML.mjs";
24
27
  import { t as CircuitBreaker } from "./circuit-breaker-XJ53md4S.mjs";
25
28
  import { t as buildCollectionSynchronizationHeader } from "./collection-CnnP1UAv.mjs";
26
- import { a as inspectCompoundPortableObjectApplicability, c as verifyServedPortableObjects, i as findUnsupportedCompoundProofShape, o as verifyCompoundPortableObjectProofs, r as findEmbeddedProofWithoutContext, t as containsCompoundPortableObject } from "./compound-proof-BqqP38Sf.mjs";
27
- import { a as isActorlessActivityAuthenticated, i as getInboxIdempotencyKey, n as checkActorlessActivity, o as isInboxActivityProcessed, s as routeActivity, t as actorsHaveSameOrigin } from "./inbox-CElAglrC.mjs";
28
- import { c as expandDocument, d as parseSerializedVerifiedActivityView, f as restoreVerifiedActivityView, i as createReleasableLoader, l as getActorPresence, n as buildVerifiedActivityView, o as deepFreeze, r as createContextSnapshot, t as ContextSnapshotMissError } from "./inbox-view-BTUuIIf6.mjs";
29
+ import { a as inspectCompoundPortableObjectApplicability, c as verifyServedPortableObjects, i as findUnsupportedCompoundProofShape, o as verifyCompoundPortableObjectProofs, r as findEmbeddedProofWithoutContext, t as containsCompoundPortableObject } from "./compound-proof-BavXW-ow.mjs";
30
+ import { c as expandDocument, d as parseSerializedVerifiedActivityView, f as restoreVerifiedActivityView, i as createReleasableLoader, l as getActorPresence, n as buildVerifiedActivityView, o as deepFreeze, r as createContextSnapshot, t as ContextSnapshotMissError } from "./inbox-view-Bu_aSwFJ.mjs";
29
31
  import { t as KvKeyCache } from "./keycache-N2opu4-3.mjs";
30
32
  import { t as acceptsJsonLd } from "./negotiation-CCocWT_N.mjs";
31
- import { t as hasMalformedKnownTemporalLiteral } from "./temporal-lDCl8Zxn.mjs";
33
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-D9Nt8Fm4.mjs";
32
34
  import { t as createExponentialBackoffPolicy } from "./retry-zX5xw5e0.mjs";
33
35
  import { t as TaskCodec } from "./codec-C6rTrpIp.mjs";
34
36
  import { getLogger, withContext } from "@logtape/logtape";
35
37
  import { RouterError } from "@fedify/uri-template";
36
38
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, isActor, lookupObject, lookupObserver, traverseCollection } from "@fedify/vocab";
37
39
  import { lookupWebFinger } from "@fedify/webfinger";
38
- import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
40
+ import { SpanKind, SpanStatusCode, context, isSpanContextValid, metrics, propagation, trace } from "@opentelemetry/api";
39
41
  import { FetchError, canonicalizePortableUri, decodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, isGatewayUrl, parseDigestMultibase, parseHashlink, parseIri, resolveDocumentLoaderTimeout, toCompatibleEf61Id } from "@fedify/vocab-runtime";
40
42
  import { isCompatibleEf61Iri } from "@fedify/vocab-runtime/internal/portable-dereference";
41
43
  import { uniq } from "es-toolkit";
42
44
  import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
45
+ import { encodeHex } from "byte-encodings/hex";
43
46
  import jsonld from "@fedify/vocab-runtime/jsonld";
44
47
  import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
45
48
  import { domainToASCII } from "node:url";
49
+ //#region src/federation/outbound-observation.ts
50
+ function snapshotDeliveryIdentity(identity) {
51
+ 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.");
52
+ return Object.freeze({
53
+ broadcast: Object.freeze(identity.broadcast.type === "known" ? {
54
+ type: "known",
55
+ id: identity.broadcast.id
56
+ } : { type: "unknown" }),
57
+ deliveryId: identity.deliveryId,
58
+ destinationInbox: identity.destinationInbox,
59
+ ...identity.portableInbox == null ? {} : { portableInbox: identity.portableInbox }
60
+ });
61
+ }
62
+ async function legacyFanoutIdentity(taskId, inbox, portableInbox) {
63
+ const bytes = new TextEncoder().encode(JSON.stringify([
64
+ "fedify-legacy-fanout-delivery",
65
+ taskId,
66
+ inbox
67
+ ]));
68
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
69
+ return {
70
+ broadcast: { type: "unknown" },
71
+ deliveryId: `legacy-fanout:${encodeHex(new Uint8Array(digest))}`,
72
+ destinationInbox: inbox,
73
+ ...portableInbox == null ? {} : { portableInbox }
74
+ };
75
+ }
76
+ async function notifyObserver(context, callback, event, unit) {
77
+ if (callback == null) return;
78
+ try {
79
+ await withContext(sourceLogContext(event.provenance), () => callback(context, event));
80
+ } catch (error) {
81
+ logObserverFailure(unit, error);
82
+ }
83
+ }
84
+ function logObserverFailure(unit, error) {
85
+ try {
86
+ getLogger([
87
+ "fedify",
88
+ "federation",
89
+ "outbox"
90
+ ]).error("An unexpected error occurred in outbound {unit} observer:\n{error}", {
91
+ unit,
92
+ error
93
+ });
94
+ } catch {}
95
+ }
96
+ function warnInvalidDeliveryIdentity(messageId, error) {
97
+ try {
98
+ getLogger([
99
+ "fedify",
100
+ "federation",
101
+ "outbox"
102
+ ]).warn("Ignoring invalid outbound delivery identity in queue message {messageId}: {error}", {
103
+ messageId,
104
+ error
105
+ });
106
+ } catch {}
107
+ }
108
+ function snapshotTrace(span, parentSpanContext) {
109
+ const spanContext = span.spanContext();
110
+ if (!isSpanContextValid(spanContext) || spanContext.isRemote || spanContext.traceId === parentSpanContext?.traceId && spanContext.spanId === parentSpanContext.spanId) return void 0;
111
+ return Object.freeze({
112
+ traceId: spanContext.traceId,
113
+ spanId: spanContext.spanId
114
+ });
115
+ }
116
+ /** State owned by one concrete send, never persisted in queue metadata. */
117
+ var OutboundAttemptObservation = class {
118
+ inboxUrl;
119
+ report;
120
+ reportExchange;
121
+ headerPolicies;
122
+ bodyCapture;
123
+ senderPayload;
124
+ #attemptId = crypto.randomUUID();
125
+ #failures = [];
126
+ #response = Object.freeze({ type: "not-observed" });
127
+ #finalResponse;
128
+ #responses = /* @__PURE__ */ new WeakMap();
129
+ #trace;
130
+ #finished = false;
131
+ #exchangeCount = 0;
132
+ #senderPayload;
133
+ constructor(inboxUrl, report, reportExchange = void 0, headerPolicies = {}, bodyCapture = void 0, senderPayload = false) {
134
+ this.inboxUrl = inboxUrl;
135
+ this.report = report;
136
+ this.reportExchange = reportExchange;
137
+ this.headerPolicies = headerPolicies;
138
+ this.bodyCapture = bodyCapture;
139
+ this.senderPayload = senderPayload;
140
+ this.#senderPayload = Object.freeze({
141
+ stage: "sender-payload",
142
+ type: "unavailable",
143
+ reason: senderPayload ? "not-materialized" : "disabled"
144
+ });
145
+ }
146
+ snapshotSenderPayload(input) {
147
+ if (this.senderPayload) this.#senderPayload = projectSenderPayload(input);
148
+ }
149
+ setSpan(span, parentSpanContext) {
150
+ this.#trace = snapshotTrace(span, parentSpanContext);
151
+ }
152
+ async fetch(request, submit) {
153
+ const exchangeIndex = this.#exchangeCount++;
154
+ const snapshot = Object.freeze({
155
+ url: request.url,
156
+ method: request.method,
157
+ ...this.headerPolicies.requestHeaders == null ? {} : { headers: snapshotHeaders(request.headers, this.headerPolicies.requestHeaders) }
158
+ });
159
+ let response;
160
+ try {
161
+ response = await submit();
162
+ } catch (error) {
163
+ await this.#exchange(exchangeIndex, snapshot, {
164
+ type: "failed",
165
+ error
166
+ });
167
+ throw error;
168
+ }
169
+ const metadata = Object.freeze({
170
+ status: response.status,
171
+ statusText: response.statusText,
172
+ ...response.url === "" ? {} : { url: response.url }
173
+ });
174
+ const observed = Object.freeze({
175
+ exchangeIndex,
176
+ metadata
177
+ });
178
+ this.#responses.set(response, observed);
179
+ this.#response = Object.freeze({
180
+ type: "undetermined",
181
+ lastObserved: observed
182
+ });
183
+ const responseSnapshot = Object.freeze({
184
+ ...metadata,
185
+ ...this.headerPolicies.responseHeaders == null ? {} : { headers: snapshotHeaders(response.headers, this.headerPolicies.responseHeaders) }
186
+ });
187
+ const bodyCapture = this.bodyCapture == null ? void 0 : await captureResponseBody(response, this.bodyCapture, (body, signal) => this.bodyCapture.onBody({
188
+ attemptId: this.#attemptId,
189
+ inboxUrl: this.inboxUrl,
190
+ exchangeIndex,
191
+ observedAt: Temporal.Now.instant().epochMilliseconds,
192
+ request: snapshot,
193
+ response: responseSnapshot,
194
+ body,
195
+ signal
196
+ }));
197
+ if (bodyCapture?.type === "failed" && bodyCapture.phase === "observer") logObserverFailure("response-body", bodyCapture.error);
198
+ await this.#exchange(exchangeIndex, snapshot, {
199
+ type: "response",
200
+ response: responseSnapshot
201
+ }, bodyCapture);
202
+ return response;
203
+ }
204
+ #exchange(exchangeIndex, request, outcome, bodyCapture) {
205
+ return this.reportExchange?.({
206
+ attemptId: this.#attemptId,
207
+ inboxUrl: this.inboxUrl,
208
+ exchangeIndex,
209
+ observedAt: Temporal.Now.instant().epochMilliseconds,
210
+ request,
211
+ outcome: Object.freeze(outcome),
212
+ ...bodyCapture == null ? {} : { bodyCapture }
213
+ }) ?? Promise.resolve();
214
+ }
215
+ response(response, disposition) {
216
+ const observed = this.#responses.get(response);
217
+ if (observed == null) return;
218
+ if (disposition === "final") {
219
+ this.#finalResponse = Object.freeze({
220
+ type: "final",
221
+ ...observed
222
+ });
223
+ this.#response = this.#finalResponse;
224
+ } else this.#response = Object.freeze({
225
+ type: "intermediate-only",
226
+ lastObserved: observed
227
+ });
228
+ }
229
+ failure(failure) {
230
+ this.#failures.push(Object.freeze(failure));
231
+ }
232
+ unknown(error) {
233
+ if (!this.#failures.some((failure) => failure.error === error || failure.phase !== "cleanup")) this.failure({
234
+ kind: "unknown",
235
+ phase: "unknown",
236
+ error
237
+ });
238
+ }
239
+ succeeded() {
240
+ if (this.#finished) return Promise.resolve();
241
+ if (this.#finalResponse == null) throw new Error("A successful outbound attempt must have an observed final response.");
242
+ return this.#finish({
243
+ type: "succeeded",
244
+ response: this.#finalResponse
245
+ });
246
+ }
247
+ failed(error) {
248
+ let failure;
249
+ let diagnostics;
250
+ if (this.#response.type === "final" && (this.#response.metadata.status < 200 || this.#response.metadata.status >= 300)) {
251
+ failure = this.#failures.find((failure) => failure.kind === "http") ?? Object.freeze({
252
+ kind: "http",
253
+ phase: "response"
254
+ });
255
+ diagnostics = this.#failures.filter((failure) => failure.kind !== "http");
256
+ } else {
257
+ failure = this.#failures.find((failure) => failure.phase !== "cleanup") ?? this.#failures[0] ?? Object.freeze({
258
+ kind: "unknown",
259
+ phase: "unknown",
260
+ error
261
+ });
262
+ diagnostics = this.#failures.filter((item) => item !== failure);
263
+ }
264
+ return this.#finish({
265
+ type: "failed",
266
+ failure,
267
+ error,
268
+ response: this.#response,
269
+ ...diagnostics.length === 0 ? {} : { diagnostics: Object.freeze(diagnostics) }
270
+ });
271
+ }
272
+ #finish(outcome) {
273
+ if (this.#finished) return Promise.resolve();
274
+ this.#finished = true;
275
+ return this.report({
276
+ attemptId: this.#attemptId,
277
+ inboxUrl: this.inboxUrl,
278
+ httpExchangeCount: this.#exchangeCount,
279
+ senderPayload: this.#senderPayload,
280
+ observedAt: Temporal.Now.instant().epochMilliseconds,
281
+ ...this.#trace == null ? {} : { trace: this.#trace },
282
+ outcome: Object.freeze(outcome)
283
+ });
284
+ }
285
+ };
286
+ function snapshotOutboxIdentity(message) {
287
+ if (message.outbound != null) try {
288
+ return snapshotDeliveryIdentity(message.outbound);
289
+ } catch (error) {
290
+ warnInvalidDeliveryIdentity(message.id, error);
291
+ }
292
+ return Object.freeze({
293
+ broadcast: Object.freeze({ type: "unknown" }),
294
+ deliveryId: message.id,
295
+ ...message.portableInbox == null ? {} : { portableInbox: message.portableInbox }
296
+ });
297
+ }
298
+ /** Per-execution state, independent of concrete attempt/exchange callbacks. */
299
+ var OutboundWorkerObservation = class {
300
+ context;
301
+ callback;
302
+ provenance;
303
+ #attemptsStarted = 0;
304
+ #actions;
305
+ #trace;
306
+ #preparationFailure;
307
+ #preparationFailures = [];
308
+ #queue;
309
+ #delivery;
310
+ #role;
311
+ #currentContext;
312
+ constructor(context, callback, message, provenance) {
313
+ this.context = context;
314
+ this.callback = callback;
315
+ this.provenance = provenance;
316
+ this.#role = message.type;
317
+ this.#queue = Object.freeze({
318
+ queueMessageId: message.id,
319
+ ...message.type === "outbox" ? { queueAttempt: message.attempt } : {}
320
+ });
321
+ if (message.type === "outbox") this.#delivery = snapshotOutboxIdentity(message);
322
+ }
323
+ attemptStarted = () => {
324
+ this.#attemptsStarted++;
325
+ };
326
+ enableActions() {
327
+ this.#actions ??= [];
328
+ }
329
+ directAttemptStarted(inboxUrl, identity) {
330
+ this.attemptStarted();
331
+ this.#actions?.push(Object.freeze({
332
+ type: "handoff",
333
+ action: "direct",
334
+ inboxUrls: Object.freeze([inboxUrl]),
335
+ ...identity == null ? {} : { delivery: snapshotDeliveryIdentity(identity) }
336
+ }));
337
+ }
338
+ async enqueueChildren(messages, enqueue) {
339
+ if (this.#actions == null) return await enqueue();
340
+ const facts = messages.map((message) => ({
341
+ action: "enqueue",
342
+ queue: Object.freeze({
343
+ queueMessageId: message.id,
344
+ queueAttempt: message.attempt
345
+ }),
346
+ inboxUrls: Object.freeze(message.gatewayInboxes != null && message.gatewayInboxes.length > 1 ? [...message.gatewayInboxes] : [message.inbox]),
347
+ ...message.outbound == null ? {} : { delivery: snapshotDeliveryIdentity(message.outbound) }
348
+ }));
349
+ try {
350
+ await enqueue();
351
+ } catch (error) {
352
+ for (const fact of facts) this.#actions.push(Object.freeze({
353
+ type: "action-failed",
354
+ ...fact,
355
+ acceptance: "unknown",
356
+ error
357
+ }));
358
+ throw error;
359
+ }
360
+ for (const fact of facts) this.#actions.push(Object.freeze({
361
+ type: "scheduled",
362
+ ...fact
363
+ }));
364
+ }
365
+ async enqueue(action, message, delay, enqueue, preparationFailures) {
366
+ if (this.#actions == null) return await enqueue();
367
+ const facts = {
368
+ action,
369
+ queue: Object.freeze({
370
+ queueMessageId: message.id,
371
+ queueAttempt: message.attempt
372
+ }),
373
+ inboxUrls: Object.freeze(message.gatewayInboxes != null && message.gatewayInboxes.length > 1 ? [...message.gatewayInboxes] : [message.inbox]),
374
+ delay: delay.toString(),
375
+ ...preparationFailures == null || preparationFailures.length === 0 ? {} : { preparationFailures: Object.freeze([...preparationFailures]) },
376
+ ...message.gatewayNotBefore == null ? {} : { notBefore: Object.freeze({ ...message.gatewayNotBefore }) }
377
+ };
378
+ try {
379
+ await enqueue();
380
+ } catch (error) {
381
+ this.#actions.push(Object.freeze({
382
+ type: "action-failed",
383
+ ...facts,
384
+ acceptance: "unknown",
385
+ error
386
+ }));
387
+ throw error;
388
+ }
389
+ this.#actions.push(Object.freeze({
390
+ type: "scheduled",
391
+ ...facts
392
+ }));
393
+ }
394
+ abandon(reason, inboxUrls, preparationFailures) {
395
+ this.#actions?.push(Object.freeze({
396
+ type: "abandoned",
397
+ reason,
398
+ inboxUrls: Object.freeze([...inboxUrls]),
399
+ ...preparationFailures == null || preparationFailures.length === 0 ? {} : { preparationFailures: Object.freeze([...preparationFailures]) }
400
+ }));
401
+ }
402
+ dropFailure(inboxUrls, error) {
403
+ this.#actions?.push(Object.freeze({
404
+ type: "action-failed",
405
+ action: "drop",
406
+ inboxUrls: Object.freeze([...inboxUrls]),
407
+ error
408
+ }));
409
+ }
410
+ async drop(inboxUrls, operation) {
411
+ try {
412
+ await operation();
413
+ } catch (error) {
414
+ this.dropFailure(inboxUrls, error);
415
+ throw error;
416
+ }
417
+ }
418
+ getContext() {
419
+ return this.#currentContext ??= this.context();
420
+ }
421
+ useContext(context) {
422
+ this.#currentContext = context;
423
+ }
424
+ setSpan(span, parentSpanContext) {
425
+ this.#trace = snapshotTrace(span, parentSpanContext);
426
+ }
427
+ async prepare(phase, operation) {
428
+ try {
429
+ const result = await operation();
430
+ this.#preparationFailure = void 0;
431
+ return result;
432
+ } catch (error) {
433
+ this.#preparationFailure = Object.freeze({
434
+ kind: "local",
435
+ phase,
436
+ error
437
+ });
438
+ throw error;
439
+ }
440
+ }
441
+ recordPreparationFailure(error, inboxUrl) {
442
+ const failure = this.#preparationFailure;
443
+ if (failure == null || !Object.is(failure.error, error)) return [];
444
+ const diagnostic = Object.freeze({
445
+ inboxUrl,
446
+ failure
447
+ });
448
+ this.#preparationFailures.push(diagnostic);
449
+ return [diagnostic];
450
+ }
451
+ async run(operation) {
452
+ try {
453
+ await operation();
454
+ } catch (error) {
455
+ await this.#notify(this.#preparationFailure == null || !Object.is(this.#preparationFailure.error, error) ? {
456
+ type: "failed",
457
+ error
458
+ } : {
459
+ type: "preparation-failed",
460
+ failure: this.#preparationFailure,
461
+ error
462
+ });
463
+ throw error;
464
+ }
465
+ await this.#notify({ type: "completed" });
466
+ }
467
+ async #notify(result) {
468
+ const observedAt = Temporal.Now.instant().epochMilliseconds;
469
+ try {
470
+ const context = this.getContext();
471
+ await notifyObserver(context, this.callback, Object.freeze({
472
+ role: this.#role,
473
+ queue: this.#queue,
474
+ ...this.#delivery == null ? {} : { delivery: this.#delivery },
475
+ attemptsStarted: this.#attemptsStarted,
476
+ actions: Object.freeze([...this.#actions ?? []]),
477
+ ...this.#preparationFailures.length === 0 ? {} : { preparationFailures: Object.freeze([...this.#preparationFailures]) },
478
+ observedAt,
479
+ ...this.#trace == null ? {} : { trace: this.#trace },
480
+ provenance: this.provenance,
481
+ local: snapshotLocalContext(context),
482
+ result: Object.freeze(result)
483
+ }), "worker");
484
+ } catch (error) {
485
+ logObserverFailure("worker", error);
486
+ }
487
+ }
488
+ };
489
+ /** Selected callback and restored grouping, shared by concrete gateway attempts. */
490
+ var OutboundAttemptObserver = class OutboundAttemptObserver {
491
+ context;
492
+ callback;
493
+ identity;
494
+ provenance;
495
+ onHttpExchange;
496
+ headerPolicies;
497
+ bodyCapture;
498
+ #queue;
499
+ constructor(context, callback, identity, provenance, queue, onHttpExchange = void 0, headerPolicies = {}, bodyCapture = void 0) {
500
+ this.context = context;
501
+ this.callback = callback;
502
+ this.identity = identity;
503
+ this.provenance = provenance;
504
+ this.onHttpExchange = onHttpExchange;
505
+ this.headerPolicies = headerPolicies;
506
+ this.bodyCapture = bodyCapture;
507
+ this.#queue = queue == null ? void 0 : Object.freeze({ ...queue });
508
+ }
509
+ static resumeOutbox(context, callback, message, provenance, onHttpExchange, headerPolicies = {}, bodyCapture) {
510
+ if (callback == null && onHttpExchange == null && bodyCapture == null) return;
511
+ const identity = snapshotOutboxIdentity(message);
512
+ return new OutboundAttemptObserver(context, callback, identity, provenance, {
513
+ queueMessageId: message.id,
514
+ queueAttempt: message.attempt
515
+ }, onHttpExchange, headerPolicies, bodyCapture);
516
+ }
517
+ startAttempt(inboxUrl) {
518
+ if (this.callback == null && this.onHttpExchange == null && this.bodyCapture == null) return void 0;
519
+ const context = this.context();
520
+ return new OutboundAttemptObservation(inboxUrl, (facts) => notifyObserver(context, this.callback, Object.freeze({
521
+ ...this.identity,
522
+ ...facts,
523
+ ...this.#queue == null ? {} : { queue: this.#queue },
524
+ provenance: this.provenance,
525
+ local: snapshotLocalContext(context)
526
+ }), "attempt"), (facts) => notifyObserver(context, this.onHttpExchange, Object.freeze({
527
+ ...this.identity,
528
+ ...facts,
529
+ ...this.#queue == null ? {} : { queue: this.#queue },
530
+ provenance: this.provenance,
531
+ local: snapshotLocalContext(context)
532
+ }), "http-exchange"), this.onHttpExchange == null && this.bodyCapture == null ? {} : this.headerPolicies, this.bodyCapture == null ? void 0 : {
533
+ ...this.bodyCapture,
534
+ onBody: (event) => withContext(sourceLogContext(this.provenance), () => this.bodyCapture.onBody(context, Object.freeze({
535
+ ...this.identity,
536
+ ...event,
537
+ ...this.#queue == null ? {} : { queue: this.#queue },
538
+ provenance: this.provenance,
539
+ local: snapshotLocalContext(context)
540
+ })))
541
+ }, this.callback != null && this.headerPolicies.senderPayload === true);
542
+ }
543
+ };
544
+ /** One producer's destination, without callbacks or live inputs in queues. */
545
+ var OutboundDeliveryObservation = class {
546
+ context;
547
+ observer;
548
+ provenance;
549
+ trace;
550
+ #finished = false;
551
+ #reference;
552
+ #plan;
553
+ #attempts;
554
+ constructor(context, observer, identity, destination, provenance, trace, onAttempt, onHttpExchange, queue, headerPolicies = {}, bodyCapture) {
555
+ this.context = context;
556
+ this.observer = observer;
557
+ this.provenance = provenance;
558
+ this.trace = trace;
559
+ this.#reference = snapshotDeliveryIdentity(identity);
560
+ this.#attempts = new OutboundAttemptObserver(() => this.context, onAttempt, this.#reference, this.provenance, queue, onHttpExchange, headerPolicies, bodyCapture);
561
+ this.#plan = Object.freeze({
562
+ actorIds: Object.freeze(Array.from(destination.actorIds)),
563
+ sharedInbox: destination.sharedInbox,
564
+ mode: destination.mode ?? (destination.portableInbox == null ? "ordinary" : "portable-fallback")
565
+ });
566
+ }
567
+ startAttempt(inboxUrl) {
568
+ return this.#attempts.startAttempt(inboxUrl);
569
+ }
570
+ get identity() {
571
+ return this.#reference;
572
+ }
573
+ planned() {
574
+ return notifyObserver(this.context, this.observer, Object.freeze({
575
+ ...this.trace == null ? {} : { trace: this.trace },
576
+ observedAt: Temporal.Now.instant().epochMilliseconds,
577
+ provenance: this.provenance,
578
+ local: snapshotLocalContext(this.context),
579
+ ...this.#reference,
580
+ ...this.#plan,
581
+ stage: "planned"
582
+ }), "delivery");
583
+ }
584
+ handoff(result) {
585
+ if (this.#finished) return Promise.resolve();
586
+ this.#finished = true;
587
+ return notifyObserver(this.context, this.observer, Object.freeze({
588
+ ...this.trace == null ? {} : { trace: this.trace },
589
+ observedAt: Temporal.Now.instant().epochMilliseconds,
590
+ provenance: this.provenance,
591
+ local: snapshotLocalContext(this.context),
592
+ ...this.#reference,
593
+ stage: "handoff",
594
+ result: Object.freeze(result)
595
+ }), "delivery");
596
+ }
597
+ failed(error) {
598
+ if (this.#finished) return Promise.resolve();
599
+ this.#finished = true;
600
+ return notifyObserver(this.context, this.observer, Object.freeze({
601
+ ...this.trace == null ? {} : { trace: this.trace },
602
+ observedAt: Temporal.Now.instant().epochMilliseconds,
603
+ provenance: this.provenance,
604
+ local: snapshotLocalContext(this.context),
605
+ ...this.#reference,
606
+ stage: "preparation-failed",
607
+ failure: Object.freeze({
608
+ kind: "local",
609
+ phase: "request-preparation",
610
+ error
611
+ })
612
+ }), "delivery");
613
+ }
614
+ };
615
+ /** State owned by one outgoing invocation, never serialized into queues. */
616
+ var OutboundBroadcastObservation = class OutboundBroadcastObservation {
617
+ context;
618
+ operation;
619
+ provenance;
620
+ queue;
621
+ #broadcast;
622
+ #finished = false;
623
+ #phase = "unknown";
624
+ #planning = "not-started";
625
+ #destinationCount = 0;
626
+ #trace;
627
+ #activity = Object.freeze({});
628
+ #onBroadcast;
629
+ #onDelivery;
630
+ #onAttempt;
631
+ #onHttpExchange;
632
+ #headerPolicies;
633
+ #bodyCapture;
634
+ #deliveries = /* @__PURE__ */ new Map();
635
+ constructor(context, observers, operation, provenance = getOutboundProvenance(context), broadcast = {
636
+ type: "known",
637
+ id: crypto.randomUUID()
638
+ }, queue = void 0) {
639
+ this.context = context;
640
+ this.operation = operation;
641
+ this.provenance = provenance;
642
+ this.queue = queue;
643
+ this.#broadcast = broadcast;
644
+ this.#onBroadcast = observers.onBroadcast;
645
+ this.#onDelivery = observers.onDelivery;
646
+ this.#onAttempt = observers.onAttempt;
647
+ this.#onHttpExchange = observers.onHttpExchange;
648
+ this.#bodyCapture = observers.responseBodyCapture;
649
+ this.#headerPolicies = {
650
+ requestHeaders: observers.requestHeaders,
651
+ responseHeaders: observers.responseHeaders,
652
+ senderPayload: observers.senderPayload
653
+ };
654
+ }
655
+ /** Continue an existing fanout; do not emit a new broadcast invocation. */
656
+ static async resumeFanout(context, observers, message, span, parentSpanContext) {
657
+ 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;
658
+ const observation = new OutboundBroadcastObservation(context, {
659
+ onDelivery: observers.onDelivery,
660
+ onAttempt: observers.onAttempt,
661
+ onHttpExchange: observers.onHttpExchange,
662
+ requestHeaders: observers.requestHeaders,
663
+ responseHeaders: observers.responseHeaders,
664
+ responseBodyCapture: observers.responseBodyCapture,
665
+ senderPayload: observers.senderPayload
666
+ }, "send", getOutboundProvenance(context), { type: "unknown" }, { queueMessageId: message.id });
667
+ if (span != null) observation.setSpan(span, parentSpanContext);
668
+ observation.#finished = true;
669
+ observation.planned(Object.keys(message.inboxes).length);
670
+ for (const [inbox, destination] of Object.entries(message.inboxes)) {
671
+ let identity;
672
+ if (destination.outbound != null) try {
673
+ const snapshot = snapshotDeliveryIdentity(destination.outbound);
674
+ if (snapshot.destinationInbox !== inbox) throw new TypeError("Outbound delivery identity does not match its fanout destination.");
675
+ identity = snapshot;
676
+ } catch (error) {
677
+ warnInvalidDeliveryIdentity(message.id, error);
678
+ }
679
+ identity ??= await legacyFanoutIdentity(message.id, inbox, destination.portableInbox);
680
+ observation.#deliveries.set(inbox, observation.#createDelivery(inbox, destination, identity));
681
+ }
682
+ return observation;
683
+ }
684
+ #createDelivery(inbox, destination, identity) {
685
+ return new OutboundDeliveryObservation(this.context, this.#onDelivery, identity ?? {
686
+ broadcast: this.#broadcast,
687
+ deliveryId: crypto.randomUUID(),
688
+ destinationInbox: inbox,
689
+ ...destination.portableInbox == null ? {} : { portableInbox: destination.portableInbox }
690
+ }, destination, this.provenance, this.#trace, this.#onAttempt, this.#onHttpExchange, this.queue, this.#headerPolicies, this.#bodyCapture);
691
+ }
692
+ continuation(inbox) {
693
+ const delivery = this.delivery(inbox);
694
+ return delivery == null ? void 0 : { outbound: delivery.identity };
695
+ }
696
+ async planDestinations(inboxes) {
697
+ this.planned(Object.keys(inboxes).length);
698
+ for (const [inbox, destination] of Object.entries(inboxes)) if (!this.#deliveries.has(inbox)) this.#deliveries.set(inbox, this.#createDelivery(inbox, destination));
699
+ for (const delivery of this.#deliveries.values()) await delivery.planned();
700
+ }
701
+ delivery(inbox) {
702
+ return this.#deliveries.get(inbox);
703
+ }
704
+ async enqueue(entries, queue, enqueue) {
705
+ let result;
706
+ try {
707
+ result = await enqueue();
708
+ } catch (error) {
709
+ await Promise.all(entries.map(({ inbox }) => this.delivery(inbox)?.handoff({
710
+ type: "enqueue-failed",
711
+ acceptance: "unknown",
712
+ error
713
+ })));
714
+ throw error;
715
+ }
716
+ await Promise.all(entries.map(({ inbox, queueMessageId }) => this.delivery(inbox)?.handoff({
717
+ type: "enqueued",
718
+ queue,
719
+ queueMessageId
720
+ })));
721
+ return result;
722
+ }
723
+ /** Attach only the span supplied by this execution, never an ambient fallback. */
724
+ setSpan(span, parentSpanContext) {
725
+ this.#trace = snapshotTrace(span, parentSpanContext);
726
+ }
727
+ setPhase(phase) {
728
+ this.#phase = phase;
729
+ if (phase === "destination-planning") this.#planning = "incomplete";
730
+ }
731
+ /** Replace the Activity facts captured by the producer path. */
732
+ setActivity(reference) {
733
+ this.#activity = Object.freeze({ ...reference });
734
+ }
735
+ planned(destinationCount) {
736
+ this.#planning = "complete";
737
+ this.#destinationCount = destinationCount;
738
+ }
739
+ async planDestination(inbox, destination) {
740
+ this.#destinationCount++;
741
+ const delivery = this.#createDelivery(inbox, destination);
742
+ this.#deliveries.set(inbox, delivery);
743
+ await delivery.planned();
744
+ }
745
+ prepared(preparedFor) {
746
+ return this.#finish(Object.freeze({
747
+ type: "prepared",
748
+ preparedFor,
749
+ destinationCount: this.#destinationCount
750
+ }));
751
+ }
752
+ skipped(reason = "no-destinations") {
753
+ return this.#finish(Object.freeze({
754
+ type: "skipped",
755
+ reason
756
+ }));
757
+ }
758
+ async run(send) {
759
+ if (this.#broadcast.type === "known") await this.#notify(Object.freeze({
760
+ broadcastId: this.#broadcast.id,
761
+ operation: this.operation,
762
+ stage: "started",
763
+ activity: this.#activity
764
+ }));
765
+ try {
766
+ return await send(this, this.provenance);
767
+ } catch (error) {
768
+ await this.failed(error);
769
+ throw error;
770
+ }
771
+ }
772
+ failed(error) {
773
+ const failure = {
774
+ kind: this.#phase === "unknown" ? "unknown" : "local",
775
+ phase: this.#phase,
776
+ error
777
+ };
778
+ return this.#finish(Object.freeze({
779
+ type: "failed",
780
+ failure: Object.freeze(failure),
781
+ planning: this.#planning,
782
+ plannedDestinationCount: this.#destinationCount
783
+ }));
784
+ }
785
+ async #finish(result) {
786
+ if (this.#finished) return;
787
+ this.#finished = true;
788
+ if (this.#broadcast.type === "known") await this.#notify(Object.freeze({
789
+ broadcastId: this.#broadcast.id,
790
+ operation: this.operation,
791
+ stage: "preparation-finished",
792
+ result,
793
+ activity: this.#activity
794
+ }));
795
+ }
796
+ #notify(event) {
797
+ return notifyObserver(this.context, this.#onBroadcast, Object.freeze({
798
+ ...event,
799
+ ...this.#trace == null ? {} : { trace: this.#trace },
800
+ observedAt: Temporal.Now.instant().epochMilliseconds,
801
+ provenance: this.provenance,
802
+ local: snapshotLocalContext(this.context)
803
+ }), "broadcast");
804
+ }
805
+ };
806
+ /** Observe preparation without changing the unobserved send's execution path. */
807
+ function observeOutboundBroadcast(context, observers, send, operation = "send", dispatchId) {
808
+ let provenance;
809
+ let invalid;
810
+ try {
811
+ provenance = invocationProvenance(context, dispatchId);
812
+ } catch (error) {
813
+ provenance = invocationProvenance(context, void 0);
814
+ invalid = { error };
815
+ }
816
+ const execute = async (observation) => {
817
+ if (invalid != null) {
818
+ observation?.setPhase("validation");
819
+ throw invalid.error;
820
+ }
821
+ return await send(observation, provenance);
822
+ };
823
+ return withContext(sourceLogContext(provenance), () => {
824
+ if (observers.onBroadcast == null && observers.onDelivery == null && observers.onAttempt == null && observers.onHttpExchange == null && observers.responseBodyCapture == null) return execute();
825
+ return new OutboundBroadcastObservation(context, observers, operation, provenance).run(execute);
826
+ });
827
+ }
828
+ //#endregion
46
829
  //#region src/federation/inbox-observation.ts
47
830
  /** Internal state owned by the ingress boundary, never serialized into queues. */
48
831
  var InboxObservation = class {
49
832
  inbox;
833
+ source;
50
834
  payload = { status: "unavailable" };
51
835
  activity = null;
52
836
  authentication = { status: "notDetermined" };
@@ -69,8 +853,9 @@ var InboxObservation = class {
69
853
  }
70
854
  }
71
855
  };
72
- constructor(inbox) {
856
+ constructor(inbox, source = void 0) {
73
857
  this.inbox = inbox;
858
+ this.source = source;
74
859
  }
75
860
  /** Run an ingress operation and project telemetry; fetch owns completion. */
76
861
  async run(operation, span) {
@@ -126,7 +911,18 @@ var InboxObservation = class {
126
911
  }
127
912
  async finish(context, handler, outcome) {
128
913
  if (handler == null) return;
914
+ const signal = context.request.signal;
915
+ const requestCancellation = signal.aborted ? Object.freeze({
916
+ type: "aborted",
917
+ reason: signal.reason
918
+ }) : Object.freeze({ type: "not-aborted" });
129
919
  const report = {
920
+ ...this.source == null ? {} : {
921
+ inboxRequestId: this.source.inboxRequestId,
922
+ requestId: this.source.requestId,
923
+ local: Object.freeze({ ...this.source.context })
924
+ },
925
+ requestCancellation,
130
926
  inbox: this.inbox,
131
927
  payload: this.payload,
132
928
  activity: this.activity,
@@ -135,7 +931,7 @@ var InboxObservation = class {
135
931
  outcome
136
932
  };
137
933
  try {
138
- await handler(context, report);
934
+ await withContext(this.source == null ? {} : { inboxRequestId: this.source.inboxRequestId }, () => handler(context, report));
139
935
  } catch (error) {
140
936
  try {
141
937
  getLogger([
@@ -949,9 +1745,10 @@ function coversDelivery(signature) {
949
1745
  * other gateways, that gateway has the activity already, so it is claimed
950
1746
  * without forwarding the activity to it. See {@link identifySendingGateway}.
951
1747
  * @param parameters The parameters.
1748
+ * @param observation The in-process producer observation, if registered.
952
1749
  * @returns The target inbox URLs that the activity was handed off to.
953
1750
  */
954
- async function forwardPortableInboxActivity(parameters) {
1751
+ async function forwardPortableInboxActivity(parameters, observation) {
955
1752
  const logger = getLogger([
956
1753
  "fedify",
957
1754
  "federation",
@@ -960,8 +1757,16 @@ async function forwardPortableInboxActivity(parameters) {
960
1757
  const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
961
1758
  const options = parameters.options ?? resolvePortableInboxForwardingOptions();
962
1759
  const { maxTargets, ttl } = options;
963
- if (kv.cas == null || maxTargets < 1) return [];
1760
+ if (kv.cas == null) {
1761
+ if (observation != null) await observation.skipped("forwarding-unavailable");
1762
+ return [];
1763
+ }
1764
+ if (maxTargets < 1) {
1765
+ if (observation != null) await observation.skipped("forwarding-disabled");
1766
+ return [];
1767
+ }
964
1768
  const deadline = Date.now() + options.deadline.total("millisecond");
1769
+ observation?.setPhase("destination-planning");
965
1770
  const canonicalActivityId = getCanonicalPortableId(activityId) ?? activityId.href;
966
1771
  const claimKey = (gateway) => [
967
1772
  ...kvPrefix,
@@ -989,15 +1794,27 @@ async function forwardPortableInboxActivity(parameters) {
989
1794
  targets.length = maxTargets;
990
1795
  }
991
1796
  const claimed = [];
992
- for (const { gateway, inbox } of targets) if (await kv.cas(claimKey(gateway), void 0, true, { ttl })) claimed.push(inbox);
993
- if (claimed.length < 1) return [];
1797
+ for (const { gateway, inbox } of targets) if (await kv.cas(claimKey(gateway), void 0, true, { ttl })) {
1798
+ claimed.push(inbox);
1799
+ if (observation != null) await observation.planDestination(inbox.href, {
1800
+ actorIds: [recipient.actorId.href],
1801
+ sharedInbox: false,
1802
+ portableInbox: recipient.canonicalInboxId,
1803
+ mode: "portable-replication"
1804
+ });
1805
+ }
1806
+ observation?.planned(claimed.length);
1807
+ if (claimed.length < 1) {
1808
+ if (observation != null) await observation.skipped("no-forwarding-targets");
1809
+ return [];
1810
+ }
994
1811
  logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
995
1812
  activityId: activityId.href,
996
1813
  inbox: recipient.canonicalInboxId,
997
1814
  targets: claimed.map((t) => t.href)
998
1815
  });
999
- if (outboxQueue == null) await forwardImmediately(parameters, claimed, deadline);
1000
- else await enqueueForwarding(parameters, outboxQueue, claimed);
1816
+ if (outboxQueue == null) await forwardImmediately(parameters, claimed, deadline, observation);
1817
+ else await enqueueForwarding(parameters, outboxQueue, claimed, observation);
1001
1818
  return claimed;
1002
1819
  }
1003
1820
  /**
@@ -1140,7 +1957,7 @@ async function resolveForwardingKey({ getKeys, activityId }) {
1140
1957
  }
1141
1958
  return key;
1142
1959
  }
1143
- async function forwardImmediately(parameters, inboxes, deadlineAt) {
1960
+ async function forwardImmediately(parameters, inboxes, deadlineAt, observation) {
1144
1961
  const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, meterProvider, tracerProvider } = parameters;
1145
1962
  const logger = getLogger([
1146
1963
  "fedify",
@@ -1148,23 +1965,50 @@ async function forwardImmediately(parameters, inboxes, deadlineAt) {
1148
1965
  "inbox"
1149
1966
  ]);
1150
1967
  const deadline = Math.max(0, deadlineAt - Date.now());
1151
- const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
1152
- activity,
1153
- activityId: activityId.href,
1154
- activityType,
1155
- keys: key == null ? [] : [key],
1156
- inbox,
1157
- allowPrivateAddress,
1158
- specDeterminer,
1159
- meterProvider,
1160
- tracerProvider
1161
- }).catch((error) => {
1162
- logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
1163
- activityId: activityId.href,
1164
- inbox: inbox.href,
1165
- error
1166
- });
1167
- }))));
1968
+ observation?.setPhase("key-resolution");
1969
+ const forwarding = (async () => {
1970
+ try {
1971
+ const key = await resolveForwardingKey(parameters);
1972
+ observation?.setPhase("request-preparation");
1973
+ const sending = [];
1974
+ for (const inbox of inboxes) {
1975
+ const delivery = observation?.delivery(inbox.href);
1976
+ let pending;
1977
+ try {
1978
+ pending = sendActivity({
1979
+ observation: delivery,
1980
+ activity,
1981
+ activityId: activityId.href,
1982
+ activityType,
1983
+ keys: key == null ? [] : [key],
1984
+ inbox,
1985
+ allowPrivateAddress,
1986
+ specDeterminer,
1987
+ meterProvider,
1988
+ tracerProvider
1989
+ });
1990
+ } catch (error) {
1991
+ if (delivery != null) await delivery.failed(error);
1992
+ throw error;
1993
+ }
1994
+ const settled = pending.catch((error) => {
1995
+ logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
1996
+ activityId: activityId.href,
1997
+ inbox: inbox.href,
1998
+ error
1999
+ });
2000
+ });
2001
+ if (observation != null) settled.catch(() => {});
2002
+ sending.push(settled);
2003
+ if (delivery != null) await delivery.handoff({ type: "direct" });
2004
+ }
2005
+ if (observation != null) await observation.prepared("delivery");
2006
+ await Promise.all(sending);
2007
+ } catch (error) {
2008
+ if (observation != null) await observation.failed(error);
2009
+ throw error;
2010
+ }
2011
+ })();
1168
2012
  let timer;
1169
2013
  const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
1170
2014
  timer = setTimeout(() => resolve(true), deadline);
@@ -1175,13 +2019,14 @@ async function forwardImmediately(parameters, inboxes, deadlineAt) {
1175
2019
  deadline
1176
2020
  });
1177
2021
  }
1178
- async function enqueueForwarding(parameters, outboxQueue, inboxes) {
2022
+ async function enqueueForwarding(parameters, outboxQueue, inboxes, observation) {
1179
2023
  const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
1180
2024
  const logger = getLogger([
1181
2025
  "fedify",
1182
2026
  "federation",
1183
2027
  "inbox"
1184
2028
  ]);
2029
+ observation?.setPhase("key-resolution");
1185
2030
  const key = await resolveForwardingKey(parameters);
1186
2031
  const keys = [];
1187
2032
  if (key != null) try {
@@ -1196,28 +2041,43 @@ async function enqueueForwarding(parameters, outboxQueue, inboxes) {
1196
2041
  error
1197
2042
  });
1198
2043
  }
2044
+ observation?.setPhase("request-preparation");
1199
2045
  startQueue?.();
1200
2046
  const started = (/* @__PURE__ */ new Date()).toISOString();
1201
2047
  const traceContext = {};
1202
2048
  propagation.inject(context.active(), traceContext);
2049
+ let preparedCount = 0;
1203
2050
  await Promise.all(inboxes.map(async (inbox) => {
1204
- const message = {
1205
- type: "outbox",
1206
- id: crypto.randomUUID(),
1207
- baseUrl,
1208
- keys,
1209
- activity,
1210
- activityId: activityId.href,
1211
- activityType,
1212
- inbox: inbox.href,
1213
- sharedInbox: false,
1214
- started,
1215
- attempt: 0,
1216
- headers: {},
1217
- traceContext
1218
- };
2051
+ const delivery = observation?.delivery(inbox.href);
2052
+ let message;
2053
+ try {
2054
+ message = {
2055
+ type: "outbox",
2056
+ ...serializeProvenance(parameters.provenance),
2057
+ id: crypto.randomUUID(),
2058
+ baseUrl,
2059
+ keys,
2060
+ activity,
2061
+ activityId: activityId.href,
2062
+ activityType,
2063
+ inbox: inbox.href,
2064
+ ...observation?.continuation(inbox.href),
2065
+ sharedInbox: false,
2066
+ started,
2067
+ attempt: 0,
2068
+ headers: {},
2069
+ traceContext
2070
+ };
2071
+ } catch (error) {
2072
+ if (delivery != null) await delivery.failed(error);
2073
+ throw error;
2074
+ }
2075
+ if (observation != null && ++preparedCount === inboxes.length) await observation.prepared("delivery");
1219
2076
  try {
1220
- await outboxQueue.enqueue(message);
2077
+ await (observation == null ? outboxQueue.enqueue(message) : observation.enqueue([{
2078
+ inbox: inbox.href,
2079
+ queueMessageId: message.id
2080
+ }], "outbox", () => outboxQueue.enqueue(message)));
1221
2081
  } catch (error) {
1222
2082
  logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
1223
2083
  activityId: activityId.href,
@@ -5016,41 +5876,70 @@ var FederationImpl = class extends FederationBuilderImpl {
5016
5876
  }
5017
5877
  await Promise.all(promises);
5018
5878
  }
5019
- processQueuedTask(contextData, message) {
5879
+ async processQueuedTask(contextData, message) {
5020
5880
  const extractedContext = propagation.extract(context.active(), message.traceContext);
5021
- return withContext({ messageId: message.id }, async () => {
5881
+ let provenance = emptyProvenance;
5882
+ if (message.type !== "task") try {
5883
+ provenance = restoreProvenance(message.provenance);
5884
+ } catch (error) {
5885
+ try {
5886
+ getLogger([
5887
+ "fedify",
5888
+ "federation",
5889
+ "queue"
5890
+ ]).warn("Ignoring invalid provenance in queue message {messageId}: {error}", {
5891
+ messageId: message.id,
5892
+ error
5893
+ });
5894
+ } catch {}
5895
+ }
5896
+ if (message.type !== "task" && message.provenance !== void 0) message = {
5897
+ ...message,
5898
+ provenance
5899
+ };
5900
+ return await withContext({
5901
+ messageId: message.id,
5902
+ inboxRequestId: void 0,
5903
+ ...sourceLogContext(provenance)
5904
+ }, async () => {
5022
5905
  if (message.type === "fanout") {
5906
+ const observers = this.outboundObservers;
5907
+ const worker = this.#createOutboundWorkerObservation(contextData, message, provenance, observers.onWorker);
5023
5908
  const common = {
5024
5909
  role: "fanout",
5025
5910
  queue: this.fanoutQueue,
5026
5911
  activityType: message.activityType
5027
5912
  };
5028
- await this.#runWorkerSpan("activitypub.fanout", { "activitypub.activity.type": message.activityType }, extractedContext, this.#instrumentWorkerBody(common, async (span) => {
5913
+ const execute = () => this.#runWorkerSpan("activitypub.fanout", { "activitypub.activity.type": message.activityType }, extractedContext, this.#instrumentWorkerBody(common, async (span) => {
5029
5914
  if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
5030
- await this.#listenFanoutMessage(contextData, message);
5915
+ await this.#listenFanoutMessage(contextData, message, provenance, span, trace.getSpanContext(extractedContext), observers, worker);
5031
5916
  return { outcome: "completed" };
5032
- }, classifyAbortableError));
5917
+ }, classifyAbortableError), worker);
5918
+ await (worker == null ? execute() : worker.run(execute));
5033
5919
  } else if (message.type === "outbox") {
5920
+ const observers = this.outboundObservers;
5921
+ const worker = this.#createOutboundWorkerObservation(contextData, message, provenance, observers.onWorker);
5034
5922
  const common = {
5035
5923
  role: "outbox",
5036
5924
  queue: this.outboxQueue,
5037
5925
  activityType: message.activityType
5038
5926
  };
5039
- await this.#runWorkerSpan("activitypub.outbox", {
5927
+ const execute = () => this.#runWorkerSpan("activitypub.outbox", {
5040
5928
  "activitypub.activity.type": message.activityType,
5041
5929
  "activitypub.activity.retries": message.attempt
5042
5930
  }, extractedContext, this.#instrumentWorkerBody(common, async (span) => {
5043
5931
  if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
5044
- await this.#listenOutboxMessage(contextData, message, span);
5932
+ await this.#listenOutboxMessage(contextData, message, span, provenance, observers, worker);
5045
5933
  return { outcome: "completed" };
5046
- }, classifyAbortableError));
5934
+ }, classifyAbortableError), worker);
5935
+ await (worker == null ? execute() : worker.run(execute));
5047
5936
  } else if (message.type === "inbox") {
5048
5937
  const common = {
5049
5938
  role: "inbox",
5050
5939
  queue: this.inboxQueue
5051
5940
  };
5052
5941
  await this.#runWorkerSpan("activitypub.inbox", { "activitypub.shared_inbox": message.identifier == null }, extractedContext, this.#instrumentWorkerBody(common, async (span) => {
5053
- await this.#listenInboxMessage(contextData, message, span, (activityType) => {
5942
+ await this.#listenInboxMessage(contextData, message, span, provenance, (activityType) => {
5054
5943
  common.activityType = activityType;
5055
5944
  });
5056
5945
  return { outcome: "completed" };
@@ -5069,6 +5958,22 @@ var FederationImpl = class extends FederationBuilderImpl {
5069
5958
  }
5070
5959
  });
5071
5960
  }
5961
+ #createOutboundWorkerObservation(contextData, message, provenance, callback) {
5962
+ if (callback == null) return void 0;
5963
+ let documentLoader;
5964
+ let contextLoader;
5965
+ return new OutboundWorkerObservation(() => {
5966
+ const loaderOptions = this.#getLoaderOptions(message.baseUrl);
5967
+ return new ContextImpl({
5968
+ [outboundProvenance]: provenance,
5969
+ federation: this,
5970
+ url: new URL("/", message.baseUrl),
5971
+ data: contextData,
5972
+ documentLoader: (...args) => (documentLoader ??= this.documentLoaderFactory(loaderOptions))(...args),
5973
+ contextLoader: (...args) => (contextLoader ??= this.contextLoaderFactory(loaderOptions))(...args)
5974
+ });
5975
+ }, callback, message, provenance);
5976
+ }
5072
5977
  /**
5073
5978
  * Opens a CONSUMER span for a queue worker and scopes the extracted trace
5074
5979
  * context around its body. The body—typically built by
@@ -5080,11 +5985,12 @@ var FederationImpl = class extends FederationBuilderImpl {
5080
5985
  * @param instrumentedBody Builds the context-scoped worker callback for the
5081
5986
  * opened span.
5082
5987
  */
5083
- async #runWorkerSpan(spanName, spanAttributes, extractedContext, instrumentedBody) {
5988
+ async #runWorkerSpan(spanName, spanAttributes, extractedContext, instrumentedBody, worker) {
5084
5989
  await this._getTracer().startActiveSpan(spanName, {
5085
5990
  kind: SpanKind.CONSUMER,
5086
5991
  attributes: spanAttributes
5087
5992
  }, extractedContext, async (span) => {
5993
+ worker?.setSpan(span, trace.getSpanContext(extractedContext));
5088
5994
  const spanCtx = span.spanContext();
5089
5995
  return await withContext({
5090
5996
  traceId: spanCtx.traceId,
@@ -5127,7 +6033,8 @@ var FederationImpl = class extends FederationBuilderImpl {
5127
6033
  span.end();
5128
6034
  }
5129
6035
  };
5130
- async #listenFanoutMessage(data, message) {
6036
+ async #listenFanoutMessage(data, message, provenance, span, parentSpanContext, observers, worker) {
6037
+ worker?.enableActions();
5131
6038
  getLogger([
5132
6039
  "fedify",
5133
6040
  "federation",
@@ -5136,11 +6043,12 @@ var FederationImpl = class extends FederationBuilderImpl {
5136
6043
  activityId: message.activityId,
5137
6044
  inboxes: globalThis.Object.keys(message.inboxes).length
5138
6045
  });
5139
- const keys = await Promise.all(message.keys.map(async ({ keyId, privateKey }) => ({
6046
+ const importKeys = () => Promise.all(message.keys.map(async ({ keyId, privateKey }) => ({
5140
6047
  keyId: new URL(keyId),
5141
6048
  privateKey: await importJwk(privateKey, "private")
5142
6049
  })));
5143
- const activity = await Activity.fromJsonLd(message.activity, {
6050
+ const keys = worker == null ? await importKeys() : await worker.prepare("key-import", importKeys);
6051
+ const parseActivity = () => Activity.fromJsonLd(message.activity, {
5144
6052
  contextLoader: this.contextLoaderFactory({
5145
6053
  allowPrivateAddress: this.allowPrivateAddress,
5146
6054
  userAgent: this.userAgent
@@ -5151,19 +6059,30 @@ var FederationImpl = class extends FederationBuilderImpl {
5151
6059
  }),
5152
6060
  tracerProvider: this.tracerProvider
5153
6061
  });
5154
- const context = this.#createContext(new URL(message.baseUrl), data, { documentLoader: this.documentLoaderFactory({
5155
- allowPrivateAddress: this.allowPrivateAddress,
5156
- userAgent: this.userAgent
5157
- }) });
6062
+ const activity = worker == null ? await parseActivity() : await worker.prepare("activity-parsing", parseActivity);
6063
+ const createContext = () => this.#createContext(new URL(message.baseUrl), data, {
6064
+ [outboundProvenance]: provenance,
6065
+ documentLoader: this.documentLoaderFactory({
6066
+ allowPrivateAddress: this.allowPrivateAddress,
6067
+ userAgent: this.userAgent
6068
+ })
6069
+ });
6070
+ const context = worker == null ? createContext() : await worker.prepare("context-preparation", createContext);
6071
+ worker?.useContext(context);
6072
+ const observation = await OutboundBroadcastObservation.resumeFanout(context, observers, message, span, parentSpanContext);
5158
6073
  await this.sendActivity(keys, message.inboxes, activity, {
5159
6074
  collectionSync: message.collectionSync,
5160
6075
  orderingKey: message.orderingKey,
5161
6076
  normalizeExistingProofs: message.normalizeExistingProofs,
5162
6077
  activityJsonLd: message.activity,
6078
+ observation,
6079
+ worker,
5163
6080
  context
5164
6081
  });
5165
6082
  }
5166
- async #listenOutboxMessage(_, message, span) {
6083
+ async #listenOutboxMessage(contextData, message, span, provenance, observers, worker) {
6084
+ const { onAttempt, onHttpExchange } = observers;
6085
+ worker?.enableActions();
5167
6086
  const logger = getLogger([
5168
6087
  "fedify",
5169
6088
  "federation",
@@ -5180,14 +6099,26 @@ var FederationImpl = class extends FederationBuilderImpl {
5180
6099
  const keys = [];
5181
6100
  let rsaKeyPair = null;
5182
6101
  for (const { keyId, privateKey } of message.keys) {
5183
- const pair = {
6102
+ const importPair = async () => ({
5184
6103
  keyId: new URL(keyId),
5185
6104
  privateKey: await importJwk(privateKey, "private")
5186
- };
6105
+ });
6106
+ const pair = worker == null ? await importPair() : await worker.prepare("key-import", importPair);
5187
6107
  if (rsaKeyPair == null && pair.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") rsaKeyPair = pair;
5188
6108
  keys.push(pair);
5189
6109
  }
5190
6110
  const loaderOptions = this.#getLoaderOptions(message.baseUrl);
6111
+ let observationContext;
6112
+ let observationDocumentLoader;
6113
+ let observationContextLoader;
6114
+ const observation = OutboundAttemptObserver.resumeOutbox(() => worker?.getContext() ?? (observationContext ??= new ContextImpl({
6115
+ [outboundProvenance]: provenance,
6116
+ federation: this,
6117
+ url: new URL("/", message.baseUrl),
6118
+ data: contextData,
6119
+ documentLoader: (...args) => (observationDocumentLoader ??= this.documentLoaderFactory(loaderOptions))(...args),
6120
+ contextLoader: (...args) => (observationContextLoader ??= this.contextLoaderFactory(loaderOptions))(...args)
6121
+ })), onAttempt, message, provenance, onHttpExchange, observers, observers.responseBodyCapture);
5191
6122
  let parsedActorIds;
5192
6123
  const getActorIds = () => {
5193
6124
  parsedActorIds ??= (message.actorIds ?? []).flatMap((id) => {
@@ -5210,7 +6141,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5210
6141
  verifyPortableObject: createPortableObjectVerifier(documentLoader, contextLoader, this)
5211
6142
  });
5212
6143
  };
5213
- const enqueueHeldOutboxMessage = async (delay, heldSince) => {
6144
+ const enqueueHeldOutboxMessage = async (delay, heldSince, preparationFailures) => {
5214
6145
  const { outboxQueue } = this;
5215
6146
  if (outboxQueue == null) return;
5216
6147
  const heldMessage = {
@@ -5218,10 +6149,11 @@ var FederationImpl = class extends FederationBuilderImpl {
5218
6149
  circuitHeld: true,
5219
6150
  circuitHeldSince: heldSince.toString()
5220
6151
  };
5221
- await outboxQueue.enqueue(heldMessage, {
6152
+ const enqueueOptions = {
5222
6153
  delay: clampNegativeDelay(delay),
5223
6154
  orderingKey: message.orderingKey
5224
- });
6155
+ };
6156
+ await (worker == null ? outboxQueue.enqueue(heldMessage, enqueueOptions) : worker.enqueue("defer", heldMessage, enqueueOptions.delay, () => outboxQueue.enqueue(heldMessage, enqueueOptions), preparationFailures));
5225
6157
  this.metrics.recordQueueTaskEnqueued({
5226
6158
  role: "outbox",
5227
6159
  queue: outboxQueue,
@@ -5238,7 +6170,10 @@ var FederationImpl = class extends FederationBuilderImpl {
5238
6170
  heldSince
5239
6171
  });
5240
6172
  if (this.outboxPermanentFailureHandler != null) {
5241
- const ctx = this.#createContext(new URL(message.baseUrl), _, { documentLoader: this.documentLoaderFactory(loaderOptions) });
6173
+ const ctx = this.#createContext(new URL(message.baseUrl), contextData, {
6174
+ [outboundProvenance]: provenance,
6175
+ documentLoader: this.documentLoaderFactory(loaderOptions)
6176
+ });
5242
6177
  try {
5243
6178
  await this.outboxPermanentFailureHandler(ctx, {
5244
6179
  reason: "circuit-breaker-ttl",
@@ -5250,6 +6185,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5250
6185
  actorIds: getActorIds()
5251
6186
  });
5252
6187
  } catch (handlerError) {
6188
+ worker?.dropFailure([inbox.href], handlerError);
5253
6189
  logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
5254
6190
  ...logData,
5255
6191
  error: handlerError
@@ -5260,10 +6196,13 @@ var FederationImpl = class extends FederationBuilderImpl {
5260
6196
  };
5261
6197
  const { gatewayInboxes } = message;
5262
6198
  if (gatewayInboxes != null && gatewayInboxes.length > 1) {
5263
- await this.#listenGatewayOutboxMessage(_, {
6199
+ await this.#listenGatewayOutboxMessage(contextData, {
5264
6200
  ...message,
5265
6201
  gatewayInboxes
5266
6202
  }, span, {
6203
+ observation,
6204
+ worker,
6205
+ provenance,
5267
6206
  keys,
5268
6207
  logData,
5269
6208
  getActorIds,
@@ -5273,7 +6212,8 @@ var FederationImpl = class extends FederationBuilderImpl {
5273
6212
  return;
5274
6213
  }
5275
6214
  try {
5276
- const inbox = new URL(message.inbox);
6215
+ const parseInbox = () => new URL(message.inbox);
6216
+ const inbox = worker == null ? parseInbox() : await worker.prepare("validation", parseInbox);
5277
6217
  const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
5278
6218
  const remoteHost = getRemoteHost(inbox);
5279
6219
  let decision;
@@ -5297,13 +6237,19 @@ var FederationImpl = class extends FederationBuilderImpl {
5297
6237
  return;
5298
6238
  }
5299
6239
  if (decision.type === "drop") {
5300
- const activity = await parseActivity();
5301
- await dropHeldOutboxMessage(circuit, remoteHost, inbox, decision.heldSince, activity);
6240
+ worker?.abandon("held-ttl-expired", [inbox.href]);
6241
+ const drop = async () => {
6242
+ const activity = await parseActivity();
6243
+ await dropHeldOutboxMessage(circuit, remoteHost, inbox, decision.heldSince, activity);
6244
+ };
6245
+ await (worker == null ? drop() : worker.drop([inbox.href], drop));
5302
6246
  return;
5303
6247
  }
5304
6248
  if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
5305
6249
  }
5306
6250
  await sendActivity({
6251
+ attemptStarted: worker?.attemptStarted,
6252
+ observation,
5307
6253
  allowPrivateAddress: this.allowPrivateAddress,
5308
6254
  keys,
5309
6255
  activity: message.activity,
@@ -5311,7 +6257,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5311
6257
  activityType: message.activityType,
5312
6258
  inbox,
5313
6259
  sharedInbox: message.sharedInbox,
5314
- headers: new Headers(message.headers),
6260
+ headers: worker == null ? new Headers(message.headers) : await worker.prepare("request-preparation", () => new Headers(message.headers)),
5315
6261
  specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
5316
6262
  meterProvider: this.meterProvider,
5317
6263
  tracerProvider: this.tracerProvider
@@ -5331,6 +6277,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5331
6277
  });
5332
6278
  }
5333
6279
  } catch (error) {
6280
+ const preparationFailures = worker?.recordPreparationFailure(error, message.inbox) ?? [];
5334
6281
  span.setStatus({
5335
6282
  code: SpanStatusCode.ERROR,
5336
6283
  message: String(error)
@@ -5339,7 +6286,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5339
6286
  if (error instanceof SendActivityError) return getRemoteHost(error.inbox);
5340
6287
  try {
5341
6288
  return getRemoteHost(new URL(message.inbox));
5342
- } catch (_) {
6289
+ } catch {
5343
6290
  logger.warn("Invalid inbox URL in queued outbox message: {inbox}", logData);
5344
6291
  return;
5345
6292
  }
@@ -5412,32 +6359,42 @@ var FederationImpl = class extends FederationBuilderImpl {
5412
6359
  });
5413
6360
  }
5414
6361
  if (circuitDrop != null) {
5415
- await dropHeldOutboxMessage(circuitDrop.circuit, circuitDrop.remoteHost, circuitDrop.inbox, circuitDrop.heldSince, activity);
6362
+ worker?.abandon("held-ttl-expired", [circuitDrop.inbox.href], preparationFailures);
6363
+ const drop = () => dropHeldOutboxMessage(circuitDrop.circuit, circuitDrop.remoteHost, circuitDrop.inbox, circuitDrop.heldSince, activity);
6364
+ await (worker == null ? drop() : worker.drop([circuitDrop.inbox.href], drop));
5416
6365
  return;
5417
6366
  }
5418
6367
  if (isPermanentFailure) {
6368
+ worker?.abandon("permanent-failure", [message.inbox], preparationFailures);
5419
6369
  this.metrics.recordPermanentFailure(error.inbox, error.statusCode);
5420
6370
  logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not retrying.", {
5421
6371
  ...logData,
5422
6372
  status: error.statusCode
5423
6373
  });
5424
6374
  if (this.outboxPermanentFailureHandler != null) {
5425
- const ctx = this.#createContext(new URL(message.baseUrl), _, { documentLoader: this.documentLoaderFactory(loaderOptions) });
5426
- try {
5427
- await this.outboxPermanentFailureHandler(ctx, {
5428
- reason: "http",
5429
- inbox: new URL(message.inbox),
5430
- activity,
5431
- error,
5432
- statusCode: error.statusCode,
5433
- actorIds: getActorIds()
5434
- });
5435
- } catch (handlerError) {
5436
- logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
5437
- ...logData,
5438
- error: handlerError
6375
+ const drop = async () => {
6376
+ const ctx = this.#createContext(new URL(message.baseUrl), contextData, {
6377
+ [outboundProvenance]: provenance,
6378
+ documentLoader: this.documentLoaderFactory(loaderOptions)
5439
6379
  });
5440
- }
6380
+ try {
6381
+ await this.outboxPermanentFailureHandler(ctx, {
6382
+ reason: "http",
6383
+ inbox: new URL(message.inbox),
6384
+ activity,
6385
+ error,
6386
+ statusCode: error.statusCode,
6387
+ actorIds: getActorIds()
6388
+ });
6389
+ } catch (handlerError) {
6390
+ worker?.dropFailure([message.inbox], handlerError);
6391
+ logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
6392
+ ...logData,
6393
+ error: handlerError
6394
+ });
6395
+ }
6396
+ };
6397
+ await (worker == null ? drop() : worker.drop([message.inbox], drop));
5441
6398
  }
5442
6399
  recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
5443
6400
  return;
@@ -5449,7 +6406,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5449
6406
  });
5450
6407
  recordCircuitBreakerHeldSpanEvent(span, circuitHold.remoteHost, circuitHold.state);
5451
6408
  const circuit = this.circuitBreaker;
5452
- await enqueueHeldOutboxMessage(retryAfterDelay == null || circuit == null ? circuitHold.delay : circuit.capHeldDelay(circuitHold.heldSince, maxDelay(circuitHold.delay, retryAfterDelay)), circuitHold.heldSince);
6409
+ await enqueueHeldOutboxMessage(retryAfterDelay == null || circuit == null ? circuitHold.delay : circuit.capHeldDelay(circuitHold.heldSince, maxDelay(circuitHold.delay, retryAfterDelay)), circuitHold.heldSince, preparationFailures);
5453
6410
  return;
5454
6411
  }
5455
6412
  if (this.outboxQueue?.nativeRetrial && retryAfterDelay == null) {
@@ -5472,18 +6429,20 @@ var FederationImpl = class extends FederationBuilderImpl {
5472
6429
  };
5473
6430
  const { outboxQueue } = this;
5474
6431
  if (outboxQueue != null) {
5475
- await outboxQueue.enqueue(retryMessage, {
6432
+ const enqueueOptions = {
5476
6433
  delay: clampNegativeDelay(delay),
5477
6434
  orderingKey: message.orderingKey
5478
- });
6435
+ };
6436
+ await (worker == null ? outboxQueue.enqueue(retryMessage, enqueueOptions) : worker.enqueue("retry", retryMessage, enqueueOptions.delay, () => outboxQueue.enqueue(retryMessage, enqueueOptions), preparationFailures));
5479
6437
  this.metrics.recordQueueTaskEnqueued({
5480
6438
  role: "outbox",
5481
6439
  queue: outboxQueue,
5482
6440
  activityType: retryMessage.activityType
5483
6441
  }, retryMessage.attempt);
5484
6442
  recordOutboxActivity(this.meterProvider, "retried", retryMessage.activityType);
5485
- }
6443
+ } else worker?.abandon("queue-unavailable", [message.inbox], preparationFailures);
5486
6444
  } else {
6445
+ worker?.abandon("retry-policy-declined", [message.inbox], preparationFailures);
5487
6446
  logger.error("Failed to send activity {activityId} to {inbox} after {attempt} attempts; giving up:\n{error}", {
5488
6447
  ...logData,
5489
6448
  error
@@ -5518,25 +6477,32 @@ var FederationImpl = class extends FederationBuilderImpl {
5518
6477
  "federation",
5519
6478
  "circuit"
5520
6479
  ]);
5521
- const { keys, logData, getActorIds, parseActivity, dropHeldOutboxMessage } = helpers;
6480
+ const { observation, worker, provenance, keys, logData, getActorIds, parseActivity, dropHeldOutboxMessage } = helpers;
5522
6481
  const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
5523
6482
  const specDeterminer = new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl });
5524
6483
  const survivors = [];
6484
+ const preparationFailuresByRoute = /* @__PURE__ */ new Map();
6485
+ const permanentRoutes = /* @__PURE__ */ new Set();
5525
6486
  let lastError;
5526
6487
  let lastPermanent;
5527
6488
  let activity;
5528
6489
  const getActivity = async () => activity ??= await parseActivity();
5529
6490
  const drop = async (circuit, remoteHost, inbox, heldSince) => {
5530
- await dropHeldOutboxMessage(circuit, remoteHost, inbox, heldSince, await getActivity());
6491
+ const routes = message.gatewayInboxes.filter((route) => !permanentRoutes.has(route));
6492
+ worker?.abandon("held-ttl-expired", routes, routes.flatMap((route) => preparationFailuresByRoute.get(route) ?? []));
6493
+ const execute = async () => dropHeldOutboxMessage(circuit, remoteHost, inbox, heldSince, await getActivity());
6494
+ await (worker == null ? execute() : worker.drop(routes, execute));
5531
6495
  };
5532
6496
  const heldSince = parseInstant(message.circuitHeldSince);
5533
6497
  if (circuit != null && heldSince != null && Temporal.Instant.compare(heldSince.add(circuit.options.heldActivityTtl), Temporal.Now.instant()) <= 0) {
5534
- const inbox = new URL(message.gatewayInboxes[0]);
6498
+ const parseInbox = () => new URL(message.gatewayInboxes[0]);
6499
+ const inbox = worker == null ? parseInbox() : await worker.prepare("validation", parseInbox);
5535
6500
  await drop(circuit, getRemoteHost(inbox), inbox, heldSince);
5536
6501
  return;
5537
6502
  }
5538
6503
  for (const inboxHref of message.gatewayInboxes) {
5539
- const inbox = new URL(inboxHref);
6504
+ const parseInbox = () => new URL(inboxHref);
6505
+ const inbox = worker == null ? parseInbox() : await worker.prepare("validation", parseInbox);
5540
6506
  const remoteHost = getRemoteHost(inbox);
5541
6507
  const now = Temporal.Now.instant();
5542
6508
  const notBefore = parseInstant(message.gatewayNotBefore?.[inboxHref]);
@@ -5574,6 +6540,8 @@ var FederationImpl = class extends FederationBuilderImpl {
5574
6540
  }
5575
6541
  try {
5576
6542
  await sendActivity({
6543
+ attemptStarted: worker?.attemptStarted,
6544
+ observation,
5577
6545
  allowPrivateAddress: this.allowPrivateAddress,
5578
6546
  keys,
5579
6547
  activity: message.activity,
@@ -5581,12 +6549,14 @@ var FederationImpl = class extends FederationBuilderImpl {
5581
6549
  activityType: message.activityType,
5582
6550
  inbox,
5583
6551
  sharedInbox: message.sharedInbox,
5584
- headers: new Headers(message.headers),
6552
+ headers: worker == null ? new Headers(message.headers) : await worker.prepare("request-preparation", () => new Headers(message.headers)),
5585
6553
  specDeterminer,
5586
6554
  meterProvider: this.meterProvider,
5587
6555
  tracerProvider: this.tracerProvider
5588
6556
  });
5589
6557
  } catch (error) {
6558
+ const preparationFailures = worker?.recordPreparationFailure(error, inboxHref) ?? [];
6559
+ if (preparationFailures.length > 0) preparationFailuresByRoute.set(inboxHref, preparationFailures);
5590
6560
  lastError = error;
5591
6561
  const isPermanentFailure = error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode);
5592
6562
  let retryAfter = !isPermanentFailure && error instanceof SendActivityError && (error.statusCode === 429 || error.statusCode === 503) ? parseRetryAfter(error.responseHeaders) : void 0;
@@ -5631,6 +6601,8 @@ var FederationImpl = class extends FederationBuilderImpl {
5631
6601
  });
5632
6602
  }
5633
6603
  if (isPermanentFailure) {
6604
+ permanentRoutes.add(inboxHref);
6605
+ worker?.abandon("permanent-failure", [inboxHref], preparationFailures);
5634
6606
  logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not trying the gateway again.", {
5635
6607
  ...logData,
5636
6608
  inbox: inboxHref,
@@ -5695,22 +6667,29 @@ var FederationImpl = class extends FederationBuilderImpl {
5695
6667
  portableInbox: message.portableInbox
5696
6668
  });
5697
6669
  if (this.outboxPermanentFailureHandler != null) {
5698
- const ctx = this.#createContext(new URL(message.baseUrl), contextData, { documentLoader: this.documentLoaderFactory(this.#getLoaderOptions(message.baseUrl)) });
5699
- try {
5700
- await this.outboxPermanentFailureHandler(ctx, {
5701
- reason: "http",
5702
- inbox,
5703
- activity: await getActivity(),
5704
- error,
5705
- statusCode: error.statusCode,
5706
- actorIds: getActorIds()
5707
- });
5708
- } catch (handlerError) {
5709
- logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
5710
- ...logData,
5711
- error: handlerError
6670
+ const drop = async () => {
6671
+ const ctx = this.#createContext(new URL(message.baseUrl), contextData, {
6672
+ [outboundProvenance]: provenance,
6673
+ documentLoader: this.documentLoaderFactory(this.#getLoaderOptions(message.baseUrl))
5712
6674
  });
5713
- }
6675
+ try {
6676
+ await this.outboxPermanentFailureHandler(ctx, {
6677
+ reason: "http",
6678
+ inbox,
6679
+ activity: await getActivity(),
6680
+ error,
6681
+ statusCode: error.statusCode,
6682
+ actorIds: getActorIds()
6683
+ });
6684
+ } catch (handlerError) {
6685
+ worker?.dropFailure(message.gatewayInboxes, handlerError);
6686
+ logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
6687
+ ...logData,
6688
+ error: handlerError
6689
+ });
6690
+ }
6691
+ };
6692
+ await (worker == null ? drop() : worker.drop(message.gatewayInboxes, drop));
5714
6693
  }
5715
6694
  recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
5716
6695
  return;
@@ -5746,10 +6725,11 @@ var FederationImpl = class extends FederationBuilderImpl {
5746
6725
  ...logData,
5747
6726
  portableInbox: message.portableInbox
5748
6727
  });
5749
- await outboxQueue.enqueue(heldMessage, {
6728
+ const enqueueOptions = {
5750
6729
  delay: clampNegativeDelay(delay),
5751
6730
  orderingKey: message.orderingKey
5752
- });
6731
+ };
6732
+ await (worker == null ? outboxQueue.enqueue(heldMessage, enqueueOptions) : worker.enqueue("defer", heldMessage, enqueueOptions.delay, () => outboxQueue.enqueue(heldMessage, enqueueOptions), survivors.flatMap((route) => preparationFailuresByRoute.get(route.inbox) ?? [])));
5753
6733
  this.metrics.recordQueueTaskEnqueued({
5754
6734
  role: "outbox",
5755
6735
  queue: outboxQueue,
@@ -5767,6 +6747,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5767
6747
  throw lastError;
5768
6748
  }
5769
6749
  if (policyDelay == null || outboxQueue == null) {
6750
+ worker?.abandon(outboxQueue == null ? "queue-unavailable" : "retry-policy-declined", survivors.map((route) => route.inbox), survivors.flatMap((route) => preparationFailuresByRoute.get(route.inbox) ?? []));
5770
6751
  logger.error("Failed to send activity {activityId} to every gateway of {portableInbox} after {attempt} attempts; giving up:\n{error}", {
5771
6752
  ...logData,
5772
6753
  portableInbox: message.portableInbox,
@@ -5785,10 +6766,11 @@ var FederationImpl = class extends FederationBuilderImpl {
5785
6766
  portableInbox: message.portableInbox,
5786
6767
  error: lastError
5787
6768
  });
5788
- await outboxQueue.enqueue(retryMessage, {
6769
+ const enqueueOptions = {
5789
6770
  delay: clampNegativeDelay(delay),
5790
6771
  orderingKey: message.orderingKey
5791
- });
6772
+ };
6773
+ await (worker == null ? outboxQueue.enqueue(retryMessage, enqueueOptions) : worker.enqueue("retry", retryMessage, enqueueOptions.delay, () => outboxQueue.enqueue(retryMessage, enqueueOptions), survivors.flatMap((route) => preparationFailuresByRoute.get(route.inbox) ?? [])));
5792
6774
  this.metrics.recordQueueTaskEnqueued({
5793
6775
  role: "outbox",
5794
6776
  queue: outboxQueue,
@@ -5796,18 +6778,24 @@ var FederationImpl = class extends FederationBuilderImpl {
5796
6778
  }, retryMessage.attempt);
5797
6779
  recordOutboxActivity(this.meterProvider, "retried", retryMessage.activityType);
5798
6780
  }
5799
- async #listenInboxMessage(ctxData, message, span, onActivityType) {
6781
+ async #listenInboxMessage(ctxData, message, span, provenance, onActivityType) {
5800
6782
  const logger = getLogger([
5801
6783
  "fedify",
5802
6784
  "federation",
5803
6785
  "inbox"
5804
6786
  ]);
5805
6787
  const baseUrl = new URL(message.baseUrl);
5806
- let context = this.#createContext(baseUrl, ctxData);
5807
- if (message.identifier != null && this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
6788
+ let context = this.#createContext(baseUrl, ctxData, { [outboundProvenance]: provenance });
6789
+ if (message.identifier != null && this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(baseUrl, ctxData, {
6790
+ [outboundProvenance]: provenance,
6791
+ documentLoader: await context.getDocumentLoader({ identifier: message.identifier })
6792
+ });
5808
6793
  else if (message.identifier == null && this.sharedInboxKeyDispatcher != null) {
5809
6794
  const identity = await this.sharedInboxKeyDispatcher(context);
5810
- if (identity != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
6795
+ if (identity != null) context = this.#createContext(baseUrl, ctxData, {
6796
+ [outboundProvenance]: provenance,
6797
+ documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity)
6798
+ });
5811
6799
  }
5812
6800
  await this._getTracer().startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (listenerSpan) => {
5813
6801
  let activity = null;
@@ -6230,6 +7218,7 @@ var FederationImpl = class extends FederationBuilderImpl {
6230
7218
  }
6231
7219
  const loaderOptions = this.#getLoaderOptions(url.origin);
6232
7220
  const ctxOptions = {
7221
+ [outboundProvenance]: opts[outboundProvenance],
6233
7222
  url,
6234
7223
  federation: this,
6235
7224
  data: contextData,
@@ -6261,22 +7250,37 @@ var FederationImpl = class extends FederationBuilderImpl {
6261
7250
  "federation",
6262
7251
  "outbox"
6263
7252
  ]);
6264
- const { immediate, collectionSync, orderingKey, context: ctx } = options;
6265
- if (activity.id == null) throw new TypeError("The activity to send must have an id.");
6266
- if (activity.actorId == null) throw new TypeError("The activity to send must have at least one actor property.");
6267
- else if (keys.length < 1) throw new TypeError("The keys must not be empty.");
6268
- const contextLoader = this.contextLoaderFactory(this.#getLoaderOptions(ctx.origin));
6269
- const activityId = activity.id.href;
6270
- let rsaKey = null;
6271
- const ldSignable = !hasPortableActor(activity);
6272
- for (const { keyId, privateKey } of keys) {
6273
- validateCryptoKey(privateKey, "private");
6274
- if (ldSignable && rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5" && !isPortableKeyId(keyId)) rsaKey = {
6275
- keyId,
6276
- privateKey
7253
+ const { immediate, collectionSync, orderingKey, context: ctx, observation, worker, provenance = getOutboundProvenance(ctx) } = options;
7254
+ observation?.setPhase("validation");
7255
+ const validateActivity = () => {
7256
+ if (activity.id == null) throw new TypeError("The activity to send must have an id.");
7257
+ if (activity.actorId == null) throw new TypeError("The activity to send must have at least one actor property.");
7258
+ else if (keys.length < 1) throw new TypeError("The keys must not be empty.");
7259
+ return activity.id.href;
7260
+ };
7261
+ const activityId = worker == null ? validateActivity() : await worker.prepare("validation", validateActivity);
7262
+ observation?.setPhase("serialization");
7263
+ const createContextLoader = () => this.contextLoaderFactory(this.#getLoaderOptions(ctx.origin));
7264
+ const contextLoader = worker == null ? createContextLoader() : await worker.prepare("serialization", createContextLoader);
7265
+ const resolveRsaKey = () => {
7266
+ const ldSignable = !hasPortableActor(activity);
7267
+ let rsaKey = null;
7268
+ for (const { keyId, privateKey } of keys) {
7269
+ validateCryptoKey(privateKey, "private");
7270
+ if (ldSignable && rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5" && !isPortableKeyId(keyId)) rsaKey = {
7271
+ keyId,
7272
+ privateKey
7273
+ };
7274
+ }
7275
+ return {
7276
+ rsaKey,
7277
+ ldSignable
6277
7278
  };
6278
- }
6279
- const signed = await signOutgoingActivity(activity, keys.map(({ keyId, privateKey }) => ({
7279
+ };
7280
+ observation?.setPhase("key-resolution");
7281
+ const { rsaKey, ldSignable } = worker == null ? resolveRsaKey() : await worker.prepare("key-resolution", resolveRsaKey);
7282
+ observation?.setPhase("signing");
7283
+ const signActivity = () => signOutgoingActivity(activity, keys.map(({ keyId, privateKey }) => ({
6280
7284
  verificationMethod: keyId,
6281
7285
  privateKey
6282
7286
  })), {
@@ -6284,14 +7288,23 @@ var FederationImpl = class extends FederationBuilderImpl {
6284
7288
  tracerProvider: this.tracerProvider,
6285
7289
  appendToExistingProofs: false
6286
7290
  });
7291
+ const signed = worker == null ? await signActivity() : await worker.prepare("signing", signActivity);
6287
7292
  activity = signed.activity;
6288
7293
  const { hasProof, proofCreated } = signed;
6289
- let jsonLd = !proofCreated && options.activityJsonLd != null ? options.activityJsonLd : await activity.toJsonLd({
6290
- format: "compact",
6291
- contextLoader
6292
- });
6293
- if (proofCreated || !hasProof || options.normalizeExistingProofs) jsonLd = await normalizeOutgoingActivityJsonLd(jsonLd, contextLoader, { preserveNestedSecuredDocuments: true });
6294
- assertSupportedCompoundProofShape(jsonLd, activityId);
7294
+ observation?.setPhase("serialization");
7295
+ const serializeActivity = async () => {
7296
+ let result = !proofCreated && options.activityJsonLd != null ? options.activityJsonLd : await activity.toJsonLd({
7297
+ format: "compact",
7298
+ contextLoader
7299
+ });
7300
+ if (proofCreated || !hasProof || options.normalizeExistingProofs) result = await normalizeOutgoingActivityJsonLd(result, contextLoader, { preserveNestedSecuredDocuments: true });
7301
+ return result;
7302
+ };
7303
+ let jsonLd = worker == null ? await serializeActivity() : await worker.prepare("serialization", serializeActivity);
7304
+ observation?.setPhase("validation");
7305
+ const validateProofShape = () => assertSupportedCompoundProofShape(jsonLd, activityId);
7306
+ if (worker == null) validateProofShape();
7307
+ else await worker.prepare("validation", validateProofShape);
6295
7308
  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 });
6296
7309
  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.", {
6297
7310
  activityId,
@@ -6300,17 +7313,24 @@ var FederationImpl = class extends FederationBuilderImpl {
6300
7313
  privateKey: pair.privateKey
6301
7314
  }))
6302
7315
  });
6303
- else try {
6304
- jsonLd = await signJsonLd(jsonLd, rsaKey.privateKey, rsaKey.keyId, {
6305
- contextLoader,
6306
- tracerProvider: this.tracerProvider
6307
- });
6308
- } catch (error) {
6309
- if (!isLinkedDataSignatureJsonLdProcessingError(error)) throw error;
6310
- logger.warn("Failed to create a Linked Data signature for the activity {activityId}. The activity will be sent without a Linked Data signature.", {
6311
- activityId,
6312
- error
6313
- });
7316
+ else {
7317
+ const signLinkedData = async () => {
7318
+ try {
7319
+ observation?.setPhase("signing");
7320
+ return await signJsonLd(jsonLd, rsaKey.privateKey, rsaKey.keyId, {
7321
+ contextLoader,
7322
+ tracerProvider: this.tracerProvider
7323
+ });
7324
+ } catch (error) {
7325
+ if (!isLinkedDataSignatureJsonLdProcessingError(error)) throw error;
7326
+ logger.warn("Failed to create a Linked Data signature for the activity {activityId}. The activity will be sent without a Linked Data signature.", {
7327
+ activityId,
7328
+ error
7329
+ });
7330
+ return jsonLd;
7331
+ }
7332
+ };
7333
+ jsonLd = worker == null ? await signLinkedData() : await worker.prepare("signing", signLinkedData);
6314
7334
  }
6315
7335
  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.", {
6316
7336
  activityId,
@@ -6319,6 +7339,7 @@ var FederationImpl = class extends FederationBuilderImpl {
6319
7339
  privateKey: pair.privateKey
6320
7340
  }))
6321
7341
  });
7342
+ observation?.setPhase("request-preparation");
6322
7343
  if (immediate || this.outboxQueue == null) {
6323
7344
  if (immediate) logger.debug("Sending activity immediately without queue since immediate option is set.", {
6324
7345
  activityId: activity.id.href,
@@ -6329,18 +7350,40 @@ var FederationImpl = class extends FederationBuilderImpl {
6329
7350
  activity: jsonLd
6330
7351
  });
6331
7352
  const promises = [];
6332
- for (const inbox in inboxes) promises.push(sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
6333
- allowPrivateAddress: this.allowPrivateAddress,
6334
- keys,
6335
- activity: jsonLd,
6336
- activityId: activity.id?.href,
6337
- activityType: getTypeId(activity).href,
6338
- sharedInbox: inboxes[inbox].sharedInbox,
6339
- headers: collectionSync == null ? void 0 : new Headers({ "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) }),
6340
- specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
6341
- meterProvider: this.meterProvider,
6342
- tracerProvider: this.tracerProvider
6343
- }));
7353
+ for (const inbox in inboxes) {
7354
+ const delivery = observation?.delivery(inbox);
7355
+ let sending;
7356
+ try {
7357
+ const createHeaders = collectionSync == null ? void 0 : async () => new Headers({ "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) });
7358
+ const sendActivityId = activity.id?.href;
7359
+ const activityType = getTypeId(activity).href;
7360
+ observation?.setActivity({
7361
+ ...sendActivityId == null ? {} : { id: sendActivityId },
7362
+ type: activityType
7363
+ });
7364
+ sending = sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
7365
+ attemptStarted: worker == null ? void 0 : (target) => worker.directAttemptStarted(target, observation?.continuation(inbox)?.outbound),
7366
+ observation: delivery,
7367
+ allowPrivateAddress: this.allowPrivateAddress,
7368
+ keys,
7369
+ activity: jsonLd,
7370
+ activityId: sendActivityId,
7371
+ activityType,
7372
+ sharedInbox: inboxes[inbox].sharedInbox,
7373
+ headers: createHeaders == null ? void 0 : await (worker == null ? createHeaders() : worker.prepare("request-preparation", createHeaders)),
7374
+ specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
7375
+ meterProvider: this.meterProvider,
7376
+ tracerProvider: this.tracerProvider
7377
+ });
7378
+ } catch (error) {
7379
+ if (delivery != null) await delivery.failed(error);
7380
+ throw error;
7381
+ }
7382
+ if (observation != null) sending.catch(() => {});
7383
+ if (delivery != null) await delivery.handoff({ type: "direct" });
7384
+ promises.push(sending);
7385
+ }
7386
+ if (observation != null) await observation.prepared("delivery");
6344
7387
  await Promise.all(promises);
6345
7388
  return;
6346
7389
  }
@@ -6348,48 +7391,71 @@ var FederationImpl = class extends FederationBuilderImpl {
6348
7391
  activityId: activity.id.href,
6349
7392
  activity: jsonLd
6350
7393
  });
7394
+ observation?.setPhase("key-resolution");
6351
7395
  const keyJwkPairs = [];
6352
7396
  for (const { keyId, privateKey } of keys) {
6353
- const privateKeyJwk = await exportJwk(privateKey);
7397
+ const exportPrivateKey = () => exportJwk(privateKey);
7398
+ const privateKeyJwk = worker == null ? await exportPrivateKey() : await worker.prepare("key-export", exportPrivateKey);
6354
7399
  keyJwkPairs.push({
6355
7400
  keyId: keyId.href,
6356
7401
  privateKey: privateKeyJwk
6357
7402
  });
6358
7403
  }
6359
7404
  if (!this.manuallyStartQueue) this._startQueueInternal(ctx.data);
7405
+ observation?.setPhase("request-preparation");
6360
7406
  const carrier = {};
6361
- propagation.inject(context.active(), carrier);
7407
+ const injectTraceContext = () => propagation.inject(context.active(), carrier);
7408
+ if (worker == null) injectTraceContext();
7409
+ else await worker.prepare("request-preparation", injectTraceContext);
6362
7410
  const messages = [];
6363
- for (const inbox in inboxes) {
6364
- const { portableInbox, gatewayInboxes } = inboxes[inbox];
6365
- const messageOrderingKey = orderingKey == null ? void 0 : `${orderingKey}\n${getOrderingDestination(inbox, portableInbox)}`;
6366
- const message = {
6367
- type: "outbox",
6368
- id: crypto.randomUUID(),
6369
- baseUrl: ctx.origin,
6370
- keys: keyJwkPairs,
6371
- activity: jsonLd,
6372
- activityId: activity.id?.href,
6373
- activityType: getTypeId(activity).href,
6374
- inbox,
6375
- sharedInbox: inboxes[inbox].sharedInbox,
6376
- actorIds: [...inboxes[inbox].actorIds],
6377
- started: (/* @__PURE__ */ new Date()).toISOString(),
6378
- attempt: 0,
6379
- headers: collectionSync == null ? {} : { "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) },
6380
- orderingKey: messageOrderingKey,
6381
- ...getPortableMessageFields(portableInbox, gatewayInboxes),
6382
- traceContext: carrier
7411
+ for (const inbox in inboxes) try {
7412
+ const prepareMessage = async () => {
7413
+ const { portableInbox, gatewayInboxes } = inboxes[inbox];
7414
+ const messageOrderingKey = orderingKey == null ? void 0 : `${orderingKey}\n${getOrderingDestination(inbox, portableInbox)}`;
7415
+ const messageActivityId = activity.id?.href;
7416
+ const messageActivityType = getTypeId(activity).href;
7417
+ observation?.setActivity({
7418
+ ...messageActivityId == null ? {} : { id: messageActivityId },
7419
+ type: messageActivityType
7420
+ });
7421
+ return {
7422
+ message: {
7423
+ type: "outbox",
7424
+ ...serializeProvenance(provenance),
7425
+ id: crypto.randomUUID(),
7426
+ baseUrl: ctx.origin,
7427
+ keys: keyJwkPairs,
7428
+ activity: jsonLd,
7429
+ activityId: messageActivityId,
7430
+ activityType: messageActivityType,
7431
+ inbox,
7432
+ ...observation?.continuation(inbox),
7433
+ sharedInbox: inboxes[inbox].sharedInbox,
7434
+ actorIds: [...inboxes[inbox].actorIds],
7435
+ started: (/* @__PURE__ */ new Date()).toISOString(),
7436
+ attempt: 0,
7437
+ headers: collectionSync == null ? {} : { "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) },
7438
+ orderingKey: messageOrderingKey,
7439
+ ...getPortableMessageFields(portableInbox, gatewayInboxes),
7440
+ traceContext: carrier
7441
+ },
7442
+ orderingKey: messageOrderingKey
7443
+ };
6383
7444
  };
6384
- messages.push({
6385
- message,
6386
- orderingKey: messageOrderingKey
6387
- });
7445
+ messages.push(await (worker == null ? prepareMessage() : worker.prepare("request-preparation", prepareMessage)));
7446
+ } catch (error) {
7447
+ if (observation != null) await observation.delivery(inbox)?.failed(error);
7448
+ throw error;
6388
7449
  }
7450
+ if (observation != null) await observation.prepared("delivery");
6389
7451
  const { outboxQueue } = this;
6390
7452
  if (outboxQueue.enqueueMany == null || orderingKey != null) {
6391
7453
  const errors = (await Promise.allSettled(messages.map(async (m) => {
6392
- await outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey });
7454
+ const enqueue = () => worker == null ? outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }) : worker.enqueueChildren([m.message], () => outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }));
7455
+ await (observation == null ? enqueue() : observation.enqueue([{
7456
+ inbox: m.message.inbox,
7457
+ queueMessageId: m.message.id
7458
+ }], "outbox", enqueue));
6393
7459
  recordOutboxEnqueue(this.meterProvider, outboxQueue, m.message);
6394
7460
  }))).filter((r) => r.status === "rejected").map((r) => r.reason);
6395
7461
  if (errors.length > 0) {
@@ -6402,7 +7468,11 @@ var FederationImpl = class extends FederationBuilderImpl {
6402
7468
  }
6403
7469
  } else {
6404
7470
  try {
6405
- await outboxQueue.enqueueMany(messages.map((m) => m.message));
7471
+ 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)));
7472
+ await (observation == null ? enqueue() : observation.enqueue(messages.map(({ message }) => ({
7473
+ inbox: message.inbox,
7474
+ queueMessageId: message.id
7475
+ })), "outbox", enqueue));
6406
7476
  } catch (error) {
6407
7477
  logger.error("Failed to enqueue activity {activityId} to send later: {error}", {
6408
7478
  activityId: activity.id.href,
@@ -6414,9 +7484,16 @@ var FederationImpl = class extends FederationBuilderImpl {
6414
7484
  }
6415
7485
  }
6416
7486
  fetch(request, options) {
6417
- return withContext({ requestId: getRequestId(request) }, async () => {
7487
+ const requestId = getRequestId(request);
7488
+ return withContext({
7489
+ requestId,
7490
+ inboxRequestId: void 0,
7491
+ dispatchId: void 0,
7492
+ sourceInboxRequestId: void 0,
7493
+ sourceRequestId: void 0
7494
+ }, async () => {
6418
7495
  const tracer = this._getTracer();
6419
- const metricState = {};
7496
+ const metricState = { requestId };
6420
7497
  const metricStart = performance.now();
6421
7498
  return await tracer.startActiveSpan(request.method, {
6422
7499
  kind: SpanKind.SERVER,
@@ -6461,7 +7538,8 @@ var FederationImpl = class extends FederationBuilderImpl {
6461
7538
  logger.error("An error occurred while serving request {method} {url}: {error}", {
6462
7539
  method: request.method,
6463
7540
  url: request.url,
6464
- error
7541
+ error,
7542
+ ...metricState.inboxRequest == null ? {} : { inboxRequestId: metricState.inboxRequest.inboxRequestId }
6465
7543
  });
6466
7544
  throw error;
6467
7545
  }
@@ -6481,6 +7559,7 @@ var FederationImpl = class extends FederationBuilderImpl {
6481
7559
  const url = new URL(request.url);
6482
7560
  const logTpl = "{method} {path}: {status}";
6483
7561
  const values = {
7562
+ ...metricState.inboxRequest == null ? {} : { inboxRequestId: metricState.inboxRequest.inboxRequestId },
6484
7563
  method: request.method,
6485
7564
  path: `${url.pathname}${url.search}`,
6486
7565
  url: request.url,
@@ -6533,9 +7612,11 @@ var FederationImpl = class extends FederationBuilderImpl {
6533
7612
  metricState.endpoint = getEndpointCategory(route.name);
6534
7613
  span.updateName(`${request.method} ${route.template}`);
6535
7614
  const routeName = route.name.replace(/:.*$/, "");
7615
+ const source = request.method === "POST" && (routeName === "inbox" || routeName === "sharedInbox") ? this.#createInboxRequestSource(request, metricState) : void 0;
7616
+ const provenance = globalThis.Object.freeze(source == null ? { originatingRequestId: metricState.requestId } : { inboxRequest: source });
6536
7617
  let context;
6537
7618
  try {
6538
- context = this.#createContext(request, contextData);
7619
+ context = withContext(source == null ? {} : { inboxRequestId: source.inboxRequestId }, () => this.#createContext(request, contextData, { [outboundProvenance]: provenance }));
6539
7620
  } catch (error) {
6540
7621
  if (request.method !== "POST" || routeName !== "inbox" && routeName !== "sharedInbox") throw error;
6541
7622
  return await this.#reportInboxPreparationError(request, contextData, {
@@ -6606,13 +7687,19 @@ var FederationImpl = class extends FederationBuilderImpl {
6606
7687
  const observation = new InboxObservation({
6607
7688
  kind: routeName === "inbox" ? "personal" : "shared",
6608
7689
  recipient
6609
- });
7690
+ }, source);
6610
7691
  this.#setInboxCompletion(metricState, observation, () => context);
6611
- return await observation.run(async () => {
6612
- if (routeName === "inbox") context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: recipient }) });
7692
+ return await withContext({ inboxRequestId: source.inboxRequestId }, () => observation.run(async () => {
7693
+ if (routeName === "inbox") context = this.#createContext(request, contextData, {
7694
+ [outboundProvenance]: getOutboundProvenance(context),
7695
+ documentLoader: await context.getDocumentLoader({ identifier: recipient })
7696
+ });
6613
7697
  else if (this.sharedInboxKeyDispatcher != null) {
6614
7698
  const identity = await this.sharedInboxKeyDispatcher(context);
6615
- if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
7699
+ if (identity != null) context = this.#createContext(request, contextData, {
7700
+ [outboundProvenance]: getOutboundProvenance(context),
7701
+ documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity)
7702
+ });
6616
7703
  }
6617
7704
  return await this.#handleInbox(request, {
6618
7705
  recipient,
@@ -6621,13 +7708,16 @@ var FederationImpl = class extends FederationBuilderImpl {
6621
7708
  onNotFound,
6622
7709
  observation
6623
7710
  });
6624
- }, span);
7711
+ }, span));
6625
7712
  }
6626
7713
  switch (routeName) {
6627
7714
  case "actor":
6628
7715
  case "actorAlias": {
6629
7716
  const identifier = route.name.startsWith("actorAlias:") ? route.name.substring(ACTOR_ALIAS_PREFIX.length) : route.values.identifier;
6630
- context = this.#createContext(request, contextData, { invokedFromActorDispatcher: { identifier } });
7717
+ context = this.#createContext(request, contextData, {
7718
+ [outboundProvenance]: getOutboundProvenance(context),
7719
+ invokedFromActorDispatcher: { identifier }
7720
+ });
6631
7721
  return await handleActor(request, {
6632
7722
  identifier,
6633
7723
  context,
@@ -6641,10 +7731,13 @@ var FederationImpl = class extends FederationBuilderImpl {
6641
7731
  const typeId = route.name.replace(/^object:/, "");
6642
7732
  const callbacks = this.objectCallbacks[typeId];
6643
7733
  const cls = this.objectTypeIds[typeId];
6644
- context = this.#createContext(request, contextData, { invokedFromObjectDispatcher: {
6645
- cls,
6646
- values: route.values
6647
- } });
7734
+ context = this.#createContext(request, contextData, {
7735
+ [outboundProvenance]: getOutboundProvenance(context),
7736
+ invokedFromObjectDispatcher: {
7737
+ cls,
7738
+ values: route.values
7739
+ }
7740
+ });
6648
7741
  return await handleObject(request, {
6649
7742
  values: route.values,
6650
7743
  context,
@@ -6702,7 +7795,10 @@ var FederationImpl = class extends FederationBuilderImpl {
6702
7795
  case "sharedInbox":
6703
7796
  if (this.sharedInboxKeyDispatcher != null) {
6704
7797
  const identity = await this.sharedInboxKeyDispatcher(context);
6705
- if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
7798
+ if (identity != null) context = this.#createContext(request, contextData, {
7799
+ [outboundProvenance]: getOutboundProvenance(context),
7800
+ documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity)
7801
+ });
6706
7802
  }
6707
7803
  return await this.#handleInbox(request, {
6708
7804
  recipient: route.values.identifier ?? null,
@@ -6846,14 +7942,17 @@ var FederationImpl = class extends FederationBuilderImpl {
6846
7942
  });
6847
7943
  return response;
6848
7944
  }
7945
+ const provenance = globalThis.Object.freeze({ originatingRequestId: metricState.requestId });
6849
7946
  if (collectionRoute != null) return await this.#fetchPortableCollection(request, portable, route, {
6850
7947
  onNotFound,
6851
7948
  onUnauthorized,
6852
- contextData
7949
+ contextData,
7950
+ provenance
6853
7951
  });
6854
7952
  if (isActor) {
6855
7953
  const identifier = route.name.startsWith("actorAlias:") ? route.name.substring(ACTOR_ALIAS_PREFIX.length) : route.values.identifier;
6856
7954
  const context = this.#createContext(request, contextData, {
7955
+ [outboundProvenance]: provenance,
6857
7956
  invokedFromActorDispatcher: { identifier },
6858
7957
  portableRequest: portable.portableRequest
6859
7958
  });
@@ -6874,6 +7973,7 @@ var FederationImpl = class extends FederationBuilderImpl {
6874
7973
  const callbacks = this.objectCallbacks[typeId];
6875
7974
  const cls = this.objectTypeIds[typeId];
6876
7975
  const context = this.#createContext(request, contextData, {
7976
+ [outboundProvenance]: provenance,
6877
7977
  invokedFromObjectDispatcher: {
6878
7978
  cls,
6879
7979
  values: route.values
@@ -6891,7 +7991,7 @@ var FederationImpl = class extends FederationBuilderImpl {
6891
7991
  onNotFound
6892
7992
  });
6893
7993
  }
6894
- async #fetchPortableCollection(request, portable, route, { onNotFound, onUnauthorized, contextData }) {
7994
+ async #fetchPortableCollection(request, portable, route, { onNotFound, onUnauthorized, contextData, provenance }) {
6895
7995
  const logger = getLogger([
6896
7996
  "fedify",
6897
7997
  "federation",
@@ -6899,8 +7999,11 @@ var FederationImpl = class extends FederationBuilderImpl {
6899
7999
  ]);
6900
8000
  const url = new URL(request.url);
6901
8001
  const { authority } = portable.portableRequest;
6902
- const ownerContext = this.#createContext(request, contextData);
6903
- const context = this.#createContext(request, contextData, { portableRequest: portable.portableRequest });
8002
+ const ownerContext = this.#createContext(request, contextData, { [outboundProvenance]: provenance });
8003
+ const context = this.#createContext(request, contextData, {
8004
+ [outboundProvenance]: provenance,
8005
+ portableRequest: portable.portableRequest
8006
+ });
6904
8007
  const reject = async (reason, identifier) => {
6905
8008
  logger.debug("Not serving the portable collection {collection} of the actor {identifier}: {reason}.", {
6906
8009
  collection: portable.canonicalId,
@@ -6993,6 +8096,11 @@ var FederationImpl = class extends FederationBuilderImpl {
6993
8096
  }
6994
8097
  return await onNotFound(request);
6995
8098
  }
8099
+ #createInboxRequestSource(request, metricState) {
8100
+ const source = createInboxRequestSource(request, metricState.requestId ?? getRequestId(request), this.origin?.webOrigin);
8101
+ metricState.inboxRequest = source;
8102
+ return source;
8103
+ }
6996
8104
  #setInboxCompletion(metricState, observation, getContext) {
6997
8105
  const handler = this.inboxRequestFinishedHandler;
6998
8106
  metricState.inboxCompletion = {
@@ -7024,6 +8132,7 @@ var FederationImpl = class extends FederationBuilderImpl {
7024
8132
  async #reportInboxPreparationError(request, contextData, inbox, error, span, metricState) {
7025
8133
  const unavailableLoader = () => Promise.reject(error);
7026
8134
  const context = new RequestContextImpl({
8135
+ [outboundProvenance]: globalThis.Object.freeze({ inboxRequest: metricState.inboxRequest }),
7027
8136
  url: new URL(request.url),
7028
8137
  request,
7029
8138
  federation: this,
@@ -7031,7 +8140,7 @@ var FederationImpl = class extends FederationBuilderImpl {
7031
8140
  documentLoader: unavailableLoader,
7032
8141
  contextLoader: unavailableLoader
7033
8142
  });
7034
- const observation = new InboxObservation(inbox);
8143
+ const observation = new InboxObservation(inbox, metricState.inboxRequest);
7035
8144
  this.#setInboxCompletion(metricState, observation, () => context);
7036
8145
  return await observation.run(() => Promise.reject(error), span);
7037
8146
  }
@@ -7089,9 +8198,10 @@ var FederationImpl = class extends FederationBuilderImpl {
7089
8198
  metricState.endpoint = "inbox";
7090
8199
  span.updateName(`${request.method} ${metricState.routeTemplate}`);
7091
8200
  const identifier = route.values.identifier;
8201
+ const source = this.#createInboxRequestSource(request, metricState);
7092
8202
  let context;
7093
8203
  try {
7094
- context = this.#createContext(request, contextData);
8204
+ context = withContext({ inboxRequestId: source.inboxRequestId }, () => this.#createContext(request, contextData, { [outboundProvenance]: globalThis.Object.freeze({ inboxRequest: source }) }));
7095
8205
  } catch (error) {
7096
8206
  return await this.#reportInboxPreparationError(request, contextData, {
7097
8207
  kind: "portable",
@@ -7101,9 +8211,9 @@ var FederationImpl = class extends FederationBuilderImpl {
7101
8211
  const observation = new InboxObservation({
7102
8212
  kind: "portable",
7103
8213
  recipient: identifier
7104
- });
8214
+ }, source);
7105
8215
  this.#setInboxCompletion(metricState, observation, () => context);
7106
- return await observation.run(async () => {
8216
+ return await withContext({ inboxRequestId: source.inboxRequestId }, () => observation.run(async () => {
7107
8217
  const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
7108
8218
  const localOrigin = context.canonicalOrigin;
7109
8219
  const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof Tombstone ? null : actor, {
@@ -7120,7 +8230,10 @@ var FederationImpl = class extends FederationBuilderImpl {
7120
8230
  return await onNotFound(request);
7121
8231
  }
7122
8232
  const { recipient } = resolution;
7123
- if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
8233
+ if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, {
8234
+ [outboundProvenance]: getOutboundProvenance(context),
8235
+ documentLoader: await context.getDocumentLoader({ identifier })
8236
+ });
7124
8237
  const excludedOrigins = [localOrigin, new URL(request.url).origin];
7125
8238
  return await this.#handleInbox(request, {
7126
8239
  recipient: identifier,
@@ -7130,16 +8243,25 @@ var FederationImpl = class extends FederationBuilderImpl {
7130
8243
  onNotFound,
7131
8244
  portableInbox: {
7132
8245
  recipient,
7133
- forward: async (activity, activityId, activityType, signatures) => {
7134
- if (this.portableInboxForwarding.maxTargets < 1) return;
8246
+ forward: (activity, activityId, activityType, signatures) => observeOutboundBroadcast(context, this.outboundObservers, async (observation) => {
8247
+ observation?.setActivity({
8248
+ ...activityId == null ? {} : { id: activityId.href },
8249
+ type: activityType
8250
+ });
8251
+ if (this.portableInboxForwarding.maxTargets < 1) {
8252
+ if (observation != null) await observation.skipped("forwarding-disabled");
8253
+ return;
8254
+ }
7135
8255
  if (this.kv.cas == null) {
7136
8256
  if (!this.#portableInboxForwardingWarned) {
7137
8257
  this.#portableInboxForwardingWarned = true;
7138
8258
  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.");
7139
8259
  }
8260
+ if (observation != null) await observation.skipped("forwarding-unavailable");
7140
8261
  return;
7141
8262
  }
7142
8263
  await forwardPortableInboxActivity({
8264
+ provenance: getOutboundProvenance(context),
7143
8265
  recipient,
7144
8266
  activity,
7145
8267
  activityId,
@@ -7157,11 +8279,11 @@ var FederationImpl = class extends FederationBuilderImpl {
7157
8279
  options: this.portableInboxForwarding,
7158
8280
  meterProvider: this.meterProvider,
7159
8281
  tracerProvider: this.tracerProvider
7160
- });
7161
- }
8282
+ }, observation);
8283
+ }, "portable-replication")
7162
8284
  }
7163
8285
  });
7164
- }, span);
8286
+ }, span));
7165
8287
  }
7166
8288
  /**
7167
8289
  * Gets this server's gateway key pairs for the portable actor that owns
@@ -7231,7 +8353,7 @@ var FederationImpl = class extends FederationBuilderImpl {
7231
8353
  headers: { "Content-Type": "text/plain; charset=utf-8" }
7232
8354
  });
7233
8355
  }
7234
- const context = this.#createContext(request, contextData);
8356
+ const context = this.#createContext(request, contextData, { [outboundProvenance]: globalThis.Object.freeze({ originatingRequestId: metricState.requestId }) });
7235
8357
  const response = await this.hashlinkMediaDispatcher(context, media.media);
7236
8358
  if (response == null) return await onNotFound(request);
7237
8359
  return await discardBodyForHead(request, response, false);
@@ -7373,8 +8495,9 @@ var ContextImpl = class ContextImpl {
7373
8495
  verifyPortableObject;
7374
8496
  invokedFromActorKeyPairsDispatcher;
7375
8497
  #codec;
7376
- constructor({ url, federation, data, documentLoader, contextLoader, invokedFromActorKeyPairsDispatcher }) {
8498
+ constructor({ url, federation, data, documentLoader, contextLoader, invokedFromActorKeyPairsDispatcher, [outboundProvenance]: provenance = emptyProvenance }) {
7377
8499
  this.url = url;
8500
+ globalThis.Object.assign(this, { [outboundProvenance]: provenance });
7378
8501
  this.federation = federation;
7379
8502
  this.data = data;
7380
8503
  this.documentLoader = documentLoader;
@@ -7396,6 +8519,7 @@ var ContextImpl = class ContextImpl {
7396
8519
  }
7397
8520
  clone(data) {
7398
8521
  return new ContextImpl({
8522
+ [outboundProvenance]: getOutboundProvenance(this),
7399
8523
  url: this.url,
7400
8524
  federation: this.federation,
7401
8525
  data,
@@ -7406,6 +8530,7 @@ var ContextImpl = class ContextImpl {
7406
8530
  }
7407
8531
  toInboxContext(recipient, activity, activityId, activityType, metadata) {
7408
8532
  return new InboxContextImpl(recipient, activity, activityId, activityType, {
8533
+ [outboundProvenance]: getOutboundProvenance(this),
7409
8534
  url: this.url,
7410
8535
  federation: this.federation,
7411
8536
  data: this.data,
@@ -7416,6 +8541,7 @@ var ContextImpl = class ContextImpl {
7416
8541
  }
7417
8542
  toOutboxContext(identifier, activity, activityId, activityType) {
7418
8543
  return new OutboxContextImpl(identifier, activity, activityId, activityType, {
8544
+ [outboundProvenance]: getOutboundProvenance(this),
7419
8545
  url: this.url,
7420
8546
  federation: this.federation,
7421
8547
  data: this.data,
@@ -7805,36 +8931,49 @@ var ContextImpl = class ContextImpl {
7805
8931
  await this.#enqueueTasks(task, payloads, options);
7806
8932
  }
7807
8933
  sendActivity(sender, recipients, activity, options = {}) {
7808
- return this.tracerProvider.getTracer(name, version).startActiveSpan(this.federation.outboxQueue == null || options.immediate ? "activitypub.outbox" : "activitypub.fanout", {
7809
- kind: this.federation.outboxQueue == null || options.immediate ? SpanKind.CLIENT : SpanKind.PRODUCER,
7810
- attributes: {
7811
- "activitypub.activity.type": getTypeId(activity).href,
7812
- "activitypub.activity.to": activity.toIds.map((to) => to.href),
7813
- "activitypub.activity.cc": activity.ccIds.map((cc) => cc.href),
7814
- "activitypub.activity.bto": activity.btoIds.map((bto) => bto.href),
7815
- "activitypub.activity.bcc": activity.bccIds.map((bcc) => bcc.href)
7816
- }
7817
- }, async (span) => {
7818
- try {
7819
- if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
7820
- await this.sendActivityInternal(sender, recipients, activity, options, span);
7821
- } catch (e) {
7822
- span.setStatus({
7823
- code: SpanStatusCode.ERROR,
7824
- message: String(e)
7825
- });
7826
- throw e;
7827
- } finally {
7828
- span.end();
7829
- }
7830
- });
8934
+ return observeOutboundBroadcast(this, this.federation.outboundObservers, (observation, provenance) => {
8935
+ const tracer = this.tracerProvider.getTracer(name, version);
8936
+ const parentSpanContext = trace.getSpanContext(context.active());
8937
+ const activityType = getTypeId(activity).href;
8938
+ observation?.setActivity({ type: activityType });
8939
+ return tracer.startActiveSpan(this.federation.outboxQueue == null || options.immediate ? "activitypub.outbox" : "activitypub.fanout", {
8940
+ kind: this.federation.outboxQueue == null || options.immediate ? SpanKind.CLIENT : SpanKind.PRODUCER,
8941
+ attributes: {
8942
+ "activitypub.activity.type": activityType,
8943
+ "activitypub.activity.to": activity.toIds.map((to) => to.href),
8944
+ "activitypub.activity.cc": activity.ccIds.map((cc) => cc.href),
8945
+ "activitypub.activity.bto": activity.btoIds.map((bto) => bto.href),
8946
+ "activitypub.activity.bcc": activity.bccIds.map((bcc) => bcc.href)
8947
+ }
8948
+ }, async (span) => {
8949
+ observation?.setSpan(span, parentSpanContext);
8950
+ try {
8951
+ const activityId = activity.id?.href;
8952
+ if (activityId != null) span.setAttribute("activitypub.activity.id", activityId);
8953
+ observation?.setActivity({
8954
+ type: activityType,
8955
+ ...activityId == null ? {} : { id: activityId }
8956
+ });
8957
+ await this.sendActivityInternal(sender, recipients, activity, options, span, observation, provenance);
8958
+ } catch (e) {
8959
+ span.setStatus({
8960
+ code: SpanStatusCode.ERROR,
8961
+ message: String(e)
8962
+ });
8963
+ throw e;
8964
+ } finally {
8965
+ span.end();
8966
+ }
8967
+ });
8968
+ }, "send", options.dispatchId);
7831
8969
  }
7832
- async sendActivityInternal(sender, recipients, activity, options, span) {
8970
+ async sendActivityInternal(sender, recipients, activity, options, span, observation, provenance = getOutboundProvenance(this)) {
7833
8971
  const logger = getLogger([
7834
8972
  "fedify",
7835
8973
  "federation",
7836
8974
  "outbox"
7837
8975
  ]);
8976
+ observation?.setPhase("sender-resolution");
7838
8977
  let keys;
7839
8978
  let identifier = null;
7840
8979
  let actorKeyPairs = null;
@@ -7850,6 +8989,7 @@ var ContextImpl = class ContextImpl {
7850
8989
  }
7851
8990
  }
7852
8991
  span.setAttribute("fedify.actor.identifier", identifier);
8992
+ observation?.setPhase("key-resolution");
7853
8993
  if (this.federation.actorCallbacks?.keyPairsDispatcher == null) throw new Error("No actor key pairs dispatcher registered.");
7854
8994
  actorKeyPairs = await this.getActorKeyPairs(identifier);
7855
8995
  if (actorKeyPairs.length < 1) throw new Error(`No key pair found for actor ${JSON.stringify(identifier)}.`);
@@ -7858,11 +8998,14 @@ var ContextImpl = class ContextImpl {
7858
8998
  privateKey: kp.privateKey
7859
8999
  }));
7860
9000
  } else if (Array.isArray(sender)) {
9001
+ observation?.setPhase("key-resolution");
7861
9002
  if (sender.length < 1) throw new Error("The sender's key pairs are empty.");
7862
9003
  keys = sender;
7863
9004
  } else keys = [sender];
7864
9005
  if (keys.length < 1) throw new TypeError("The sender's keys must not be empty.");
9006
+ observation?.setPhase("key-resolution");
7865
9007
  for (const { privateKey } of keys) validateCryptoKey(privateKey, "private");
9008
+ observation?.setPhase("recipient-resolution");
7866
9009
  let expandedRecipients;
7867
9010
  let collectionSync;
7868
9011
  if (Array.isArray(recipients)) expandedRecipients = recipients;
@@ -7878,14 +9021,34 @@ var ContextImpl = class ContextImpl {
7878
9021
  } else expandedRecipients = [recipients];
7879
9022
  const opts = {
7880
9023
  context: this,
9024
+ observation,
9025
+ provenance,
7881
9026
  orderingKey: options.orderingKey,
7882
9027
  collectionSync,
7883
9028
  immediate: options.immediate,
7884
9029
  normalizeExistingProofs: options.normalizeExistingProofs
7885
9030
  };
7886
9031
  span.setAttribute("activitypub.inboxes", expandedRecipients.length);
9032
+ observation?.setPhase("transformation");
9033
+ if (this.federation.activityTransformers.length > 0) observation?.setActivity({});
7887
9034
  for (const activityTransformer of this.federation.activityTransformers) activity = activityTransformer(activity, this);
7888
- span?.setAttribute("activitypub.activity.id", activity?.id?.href ?? "");
9035
+ let transformedActivityId;
9036
+ try {
9037
+ transformedActivityId = activity?.id?.href;
9038
+ } finally {
9039
+ if (observation != null) {
9040
+ let activityType;
9041
+ try {
9042
+ activityType = getTypeId(activity)?.href;
9043
+ } catch {}
9044
+ observation.setActivity({
9045
+ ...transformedActivityId == null ? {} : { id: transformedActivityId },
9046
+ ...activityType == null ? {} : { type: activityType }
9047
+ });
9048
+ }
9049
+ }
9050
+ span?.setAttribute("activitypub.activity.id", transformedActivityId ?? "");
9051
+ observation?.setPhase("validation");
7889
9052
  if (activity.actorId == null) {
7890
9053
  logger.error("Activity {activityId} to send does not have an actor.", {
7891
9054
  activity,
@@ -7893,6 +9056,7 @@ var ContextImpl = class ContextImpl {
7893
9056
  });
7894
9057
  throw new TypeError("The activity to send must have at least one actor property.");
7895
9058
  }
9059
+ observation?.setPhase("signing");
7896
9060
  const { activity: signedActivity, proofCreated } = await signOutgoingActivity(activity, actorKeyPairs == null ? keys.map(({ keyId, privateKey }) => ({
7897
9061
  verificationMethod: keyId,
7898
9062
  privateKey
@@ -7905,16 +9069,19 @@ var ContextImpl = class ContextImpl {
7905
9069
  appendToExistingProofs: actorKeyPairs != null
7906
9070
  });
7907
9071
  activity = signedActivity;
9072
+ observation?.setPhase("destination-planning");
7908
9073
  const inboxes = extractInboxes({
7909
9074
  recipients: expandedRecipients,
7910
9075
  preferSharedInbox: options.preferSharedInbox,
7911
9076
  excludeBaseUris: options.excludeBaseUris
7912
9077
  });
9078
+ if (observation != null) await observation.planDestinations(inboxes);
7913
9079
  if (globalThis.Object.keys(inboxes).length < 1) {
7914
9080
  logger.debug("No inboxes found for activity {activityId}.", {
7915
9081
  activityId: activity.id?.href,
7916
9082
  activity
7917
9083
  });
9084
+ if (observation != null) await observation.skipped();
7918
9085
  return false;
7919
9086
  }
7920
9087
  logger.debug("Sending activity {activityId} to inboxes:\n{inboxes}", {
@@ -7922,6 +9089,7 @@ var ContextImpl = class ContextImpl {
7922
9089
  activityId: activity.id?.href,
7923
9090
  activity
7924
9091
  });
9092
+ observation?.setPhase("serialization");
7925
9093
  const activityJsonLd = await activity.toJsonLd({
7926
9094
  format: "compact",
7927
9095
  contextLoader: this.contextLoader
@@ -7942,33 +9110,48 @@ var ContextImpl = class ContextImpl {
7942
9110
  });
7943
9111
  return true;
7944
9112
  }
9113
+ observation?.setPhase("validation");
7945
9114
  assertSupportedCompoundProofShape(activityJsonLd, activity.id?.href);
9115
+ observation?.setPhase("key-resolution");
7946
9116
  const keyJwkPairs = await Promise.all(keys.map(async ({ keyId, privateKey }) => ({
7947
9117
  keyId: keyId.href,
7948
9118
  privateKey: await exportJwk(privateKey)
7949
9119
  })));
9120
+ observation?.setPhase("request-preparation");
7950
9121
  const carrier = {};
7951
9122
  propagation.inject(context.active(), carrier);
9123
+ const activityId = activity.id?.href;
9124
+ const activityType = getTypeId(activity).href;
9125
+ observation?.setActivity({
9126
+ ...activityId == null ? {} : { id: activityId },
9127
+ type: activityType
9128
+ });
7952
9129
  const message = {
7953
9130
  type: "fanout",
9131
+ ...serializeProvenance(provenance),
7954
9132
  id: crypto.randomUUID(),
7955
9133
  baseUrl: this.origin,
7956
9134
  keys: keyJwkPairs,
7957
9135
  inboxes: globalThis.Object.fromEntries(globalThis.Object.entries(inboxes).map(([k, { actorIds, sharedInbox, portableInbox, gatewayInboxes }]) => [k, {
7958
9136
  actorIds: [...actorIds],
9137
+ ...observation?.continuation(k),
7959
9138
  sharedInbox,
7960
9139
  ...getPortableMessageFields(portableInbox, gatewayInboxes)
7961
9140
  }])),
7962
9141
  activity: activityJsonLd,
7963
- activityId: activity.id?.href,
7964
- activityType: getTypeId(activity).href,
9142
+ activityId,
9143
+ activityType,
7965
9144
  collectionSync: opts.collectionSync,
7966
9145
  orderingKey: options.orderingKey,
7967
9146
  normalizeExistingProofs: proofCreated || options.normalizeExistingProofs,
7968
9147
  traceContext: carrier
7969
9148
  };
7970
9149
  if (!this.federation.manuallyStartQueue) this.federation._startQueueInternal(this.data);
7971
- await this.federation.fanoutQueue.enqueue(message, { orderingKey: options.orderingKey });
9150
+ if (observation != null) await observation.prepared("fanout");
9151
+ await (observation == null ? this.federation.fanoutQueue.enqueue(message, { orderingKey: options.orderingKey }) : observation.enqueue(globalThis.Object.keys(inboxes).map((inbox) => ({
9152
+ inbox,
9153
+ queueMessageId: message.id
9154
+ })), "fanout", () => this.federation.fanoutQueue.enqueue(message, { orderingKey: options.orderingKey })));
7972
9155
  getFederationMetrics(this.federation.meterProvider).recordQueueTaskEnqueued({
7973
9156
  role: "fanout",
7974
9157
  queue: this.federation.fanoutQueue,
@@ -8229,6 +9412,7 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
8229
9412
  }
8230
9413
  clone(data) {
8231
9414
  return new RequestContextImpl({
9415
+ [outboundProvenance]: getOutboundProvenance(this),
8232
9416
  url: this.url,
8233
9417
  federation: this.federation,
8234
9418
  data,
@@ -8400,30 +9584,40 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
8400
9584
  }
8401
9585
  };
8402
9586
  function forwardActivity(ctx, loggerCategory, forwarder, recipients, options) {
8403
- return ctx.tracerProvider.getTracer(name, version).startActiveSpan(ctx.federation.outboxQueue == null || options?.immediate ? `activitypub.${loggerCategory}` : "activitypub.fanout", {
8404
- kind: ctx.federation.outboxQueue == null || options?.immediate ? SpanKind.CLIENT : SpanKind.PRODUCER,
8405
- attributes: { "activitypub.activity.type": ctx.activityType }
8406
- }, async (span) => {
8407
- try {
8408
- if (ctx.activityId != null) span.setAttribute("activitypub.activity.id", ctx.activityId);
8409
- return await forwardActivityInternal(ctx, loggerCategory, forwarder, recipients, options);
8410
- } catch (e) {
8411
- span.setStatus({
8412
- code: SpanStatusCode.ERROR,
8413
- message: String(e)
8414
- });
8415
- throw e;
8416
- } finally {
8417
- span.end();
8418
- }
8419
- });
9587
+ return observeOutboundBroadcast(ctx, ctx.federation.outboundObservers, (observation, provenance) => {
9588
+ observation?.setActivity({
9589
+ ...ctx.activityId == null ? {} : { id: ctx.activityId },
9590
+ type: ctx.activityType
9591
+ });
9592
+ const tracer = ctx.tracerProvider.getTracer(name, version);
9593
+ const parentSpanContext = trace.getSpanContext(context.active());
9594
+ return tracer.startActiveSpan(ctx.federation.outboxQueue == null || options?.immediate ? `activitypub.${loggerCategory}` : "activitypub.fanout", {
9595
+ kind: ctx.federation.outboxQueue == null || options?.immediate ? SpanKind.CLIENT : SpanKind.PRODUCER,
9596
+ attributes: { "activitypub.activity.type": ctx.activityType }
9597
+ }, async (span) => {
9598
+ observation?.setSpan(span, parentSpanContext);
9599
+ try {
9600
+ if (ctx.activityId != null) span.setAttribute("activitypub.activity.id", ctx.activityId);
9601
+ return await forwardActivityInternal(ctx, loggerCategory, forwarder, recipients, options, observation, provenance);
9602
+ } catch (e) {
9603
+ span.setStatus({
9604
+ code: SpanStatusCode.ERROR,
9605
+ message: String(e)
9606
+ });
9607
+ throw e;
9608
+ } finally {
9609
+ span.end();
9610
+ }
9611
+ });
9612
+ }, "forward", options?.dispatchId);
8420
9613
  }
8421
- async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipients, options) {
9614
+ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipients, options, observation, provenance = getOutboundProvenance(ctx)) {
8422
9615
  const logger = getLogger([
8423
9616
  "fedify",
8424
9617
  "federation",
8425
9618
  loggerCategory
8426
9619
  ]);
9620
+ observation?.setPhase("sender-resolution");
8427
9621
  let keys;
8428
9622
  let identifier = null;
8429
9623
  if ("identifier" in forwarder || "username" in forwarder) {
@@ -8437,6 +9631,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
8437
9631
  identifier = mapped;
8438
9632
  }
8439
9633
  }
9634
+ observation?.setPhase("key-resolution");
8440
9635
  const actorKeyPairs = await ctx.getActorKeyPairs(identifier);
8441
9636
  if (actorKeyPairs.length < 1) throw new Error(`No key pair found for actor ${JSON.stringify(identifier)}.`);
8442
9637
  keys = actorKeyPairs.map((kp) => ({
@@ -8444,12 +9639,17 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
8444
9639
  privateKey: kp.privateKey
8445
9640
  }));
8446
9641
  } else if (Array.isArray(forwarder)) {
9642
+ observation?.setPhase("key-resolution");
8447
9643
  if (forwarder.length < 1) throw new Error("The forwarder's key pairs are empty.");
8448
9644
  keys = forwarder;
8449
9645
  } else keys = [forwarder];
9646
+ observation?.setPhase("validation");
8450
9647
  if (!hasSignatureLike(ctx.activity)) {
8451
9648
  if (!hasProofLike(ctx.activity)) {
8452
- if (options?.skipIfUnsigned) return false;
9649
+ if (options?.skipIfUnsigned) {
9650
+ if (observation != null) await observation.skipped("unsigned");
9651
+ return false;
9652
+ }
8453
9653
  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.", {
8454
9654
  activityId: ctx.activityId,
8455
9655
  activityType: ctx.activityType,
@@ -8457,23 +9657,27 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
8457
9657
  });
8458
9658
  }
8459
9659
  }
9660
+ observation?.setPhase("recipient-resolution");
8460
9661
  if (recipients === "followers") {
8461
9662
  if (identifier == null) throw new Error("If recipients is \"followers\", forwarder must be an actor identifier or username.");
8462
9663
  const followers = [];
8463
9664
  for await (const recipient of ctx.getFollowers(identifier)) followers.push(recipient);
8464
9665
  recipients = followers;
8465
9666
  }
9667
+ observation?.setPhase("destination-planning");
8466
9668
  const inboxes = extractInboxes({
8467
9669
  recipients: Array.isArray(recipients) ? recipients : [recipients],
8468
9670
  preferSharedInbox: options?.preferSharedInbox,
8469
9671
  excludeBaseUris: options?.excludeBaseUris
8470
9672
  });
9673
+ if (observation != null) await observation.planDestinations(inboxes);
8471
9674
  if (globalThis.Object.keys(inboxes).length < 1) {
8472
9675
  logger.debug("No inboxes found for activity {activityId}.", {
8473
9676
  activityId: ctx.activityId,
8474
9677
  activityType: ctx.activityType,
8475
9678
  identifier: identifier ?? void 0
8476
9679
  });
9680
+ if (observation != null) await observation.skipped();
8477
9681
  return false;
8478
9682
  }
8479
9683
  logger.debug("Forwarding activity {activityId} to inboxes:\n{inboxes}", {
@@ -8484,18 +9688,33 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
8484
9688
  if (options?.immediate || ctx.federation.outboxQueue == null) {
8485
9689
  if (options?.immediate) logger.debug("Forwarding activity immediately without queue since immediate option is set.");
8486
9690
  else logger.debug("Forwarding activity immediately without queue since queue is not set.");
9691
+ observation?.setPhase("request-preparation");
8487
9692
  const promises = [];
8488
- for (const inbox in inboxes) promises.push(sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
8489
- allowPrivateAddress: ctx.federation.allowPrivateAddress,
8490
- keys,
8491
- activity: ctx.activity,
8492
- activityId: ctx.activityId,
8493
- activityType: ctx.activityType,
8494
- sharedInbox: inboxes[inbox].sharedInbox,
8495
- meterProvider: ctx.meterProvider,
8496
- tracerProvider: ctx.tracerProvider,
8497
- specDeterminer: new KvSpecDeterminer(ctx.federation.kv, ctx.federation.kvPrefixes.httpMessageSignaturesSpec, ctx.federation.firstKnock, { specTtl: ctx.federation.httpMessageSignaturesSpecTtl })
8498
- }));
9693
+ for (const inbox in inboxes) {
9694
+ const delivery = observation?.delivery(inbox);
9695
+ let sending;
9696
+ try {
9697
+ sending = sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
9698
+ observation: delivery,
9699
+ allowPrivateAddress: ctx.federation.allowPrivateAddress,
9700
+ keys,
9701
+ activity: ctx.activity,
9702
+ activityId: ctx.activityId,
9703
+ activityType: ctx.activityType,
9704
+ sharedInbox: inboxes[inbox].sharedInbox,
9705
+ meterProvider: ctx.meterProvider,
9706
+ tracerProvider: ctx.tracerProvider,
9707
+ specDeterminer: new KvSpecDeterminer(ctx.federation.kv, ctx.federation.kvPrefixes.httpMessageSignaturesSpec, ctx.federation.firstKnock, { specTtl: ctx.federation.httpMessageSignaturesSpecTtl })
9708
+ });
9709
+ } catch (error) {
9710
+ if (delivery != null) await delivery.failed(error);
9711
+ throw error;
9712
+ }
9713
+ if (observation != null) sending.catch(() => {});
9714
+ if (delivery != null) await delivery.handoff({ type: "direct" });
9715
+ promises.push(sending);
9716
+ }
9717
+ if (observation != null) await observation.prepared("delivery");
8499
9718
  await Promise.all(promises);
8500
9719
  return true;
8501
9720
  }
@@ -8503,7 +9722,9 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
8503
9722
  activityId: ctx.activityId,
8504
9723
  activity: ctx.activity
8505
9724
  });
9725
+ observation?.setPhase("request-preparation");
8506
9726
  if (!ctx.federation.manuallyStartQueue) ctx.federation._startQueueInternal(ctx.data);
9727
+ observation?.setPhase("key-resolution");
8507
9728
  const keyJwkPairs = [];
8508
9729
  for (const { keyId, privateKey } of keys) {
8509
9730
  const privateKeyJwk = await exportJwk(privateKey);
@@ -8512,15 +9733,17 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
8512
9733
  privateKey: privateKeyJwk
8513
9734
  });
8514
9735
  }
9736
+ observation?.setPhase("request-preparation");
8515
9737
  const carrier = {};
8516
9738
  propagation.inject(context.active(), carrier);
8517
9739
  const orderingKey = options?.orderingKey;
8518
9740
  const started = (/* @__PURE__ */ new Date()).toISOString();
8519
9741
  const messages = [];
8520
- for (const inbox in inboxes) {
9742
+ for (const inbox in inboxes) try {
8521
9743
  const { portableInbox, gatewayInboxes } = inboxes[inbox];
8522
9744
  const message = {
8523
9745
  type: "outbox",
9746
+ ...serializeProvenance(provenance),
8524
9747
  id: crypto.randomUUID(),
8525
9748
  baseUrl: ctx.origin,
8526
9749
  keys: keyJwkPairs,
@@ -8528,6 +9751,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
8528
9751
  activityId: ctx.activityId,
8529
9752
  activityType: ctx.activityType,
8530
9753
  inbox,
9754
+ ...observation?.continuation(inbox),
8531
9755
  sharedInbox: inboxes[inbox].sharedInbox,
8532
9756
  actorIds: [...inboxes[inbox].actorIds],
8533
9757
  started,
@@ -8541,11 +9765,18 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
8541
9765
  message,
8542
9766
  orderingKey: message.orderingKey
8543
9767
  });
9768
+ } catch (error) {
9769
+ if (observation != null) await observation.delivery(inbox)?.failed(error);
9770
+ throw error;
8544
9771
  }
9772
+ if (observation != null) await observation.prepared("delivery");
8545
9773
  const { outboxQueue } = ctx.federation;
8546
9774
  if (outboxQueue.enqueueMany == null || orderingKey != null) {
8547
9775
  const errors = (await Promise.allSettled(messages.map(async (m) => {
8548
- await outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey });
9776
+ await (observation == null ? outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }) : observation.enqueue([{
9777
+ inbox: m.message.inbox,
9778
+ queueMessageId: m.message.id
9779
+ }], "outbox", () => outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey })));
8549
9780
  recordOutboxEnqueue(ctx.federation.meterProvider, outboxQueue, m.message);
8550
9781
  }))).filter((r) => r.status === "rejected").map((r) => r.reason);
8551
9782
  if (errors.length > 0) {
@@ -8558,7 +9789,10 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
8558
9789
  }
8559
9790
  } else {
8560
9791
  try {
8561
- await outboxQueue.enqueueMany(messages.map((m) => m.message));
9792
+ await (observation == null ? outboxQueue.enqueueMany(messages.map((m) => m.message)) : observation.enqueue(messages.map(({ message }) => ({
9793
+ inbox: message.inbox,
9794
+ queueMessageId: message.id
9795
+ })), "outbox", () => outboxQueue.enqueueMany(messages.map((m) => m.message))));
8562
9796
  } catch (error) {
8563
9797
  logger.error("Failed to enqueue activity {activityId} to forward later:\n{error}", {
8564
9798
  activityId: ctx.activityId,
@@ -8594,6 +9828,7 @@ var InboxContextImpl = class InboxContextImpl extends ContextImpl {
8594
9828
  }
8595
9829
  clone(data) {
8596
9830
  return new InboxContextImpl(this.recipient, this.activity, this.activityId, this.activityType, {
9831
+ [outboundProvenance]: getOutboundProvenance(this),
8597
9832
  url: this.url,
8598
9833
  federation: this.federation,
8599
9834
  data,
@@ -8624,29 +9859,41 @@ var OutboxContextImpl = class OutboxContextImpl extends ContextImpl {
8624
9859
  return this.#deliveryState.delivered;
8625
9860
  }
8626
9861
  sendActivity(sender, recipients, activity, options = {}) {
8627
- return this.tracerProvider.getTracer(name, version).startActiveSpan(this.federation.outboxQueue == null || options.immediate ? "activitypub.outbox" : "activitypub.fanout", {
8628
- kind: this.federation.outboxQueue == null || options.immediate ? SpanKind.CLIENT : SpanKind.PRODUCER,
8629
- attributes: {
8630
- "activitypub.activity.type": getTypeId(activity).href,
8631
- "activitypub.activity.to": activity.toIds.map((to) => to.href),
8632
- "activitypub.activity.cc": activity.ccIds.map((cc) => cc.href),
8633
- "activitypub.activity.bto": activity.btoIds.map((bto) => bto.href),
8634
- "activitypub.activity.bcc": activity.bccIds.map((bcc) => bcc.href)
8635
- }
8636
- }, async (span) => {
8637
- try {
8638
- if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
8639
- if (await this.sendActivityInternal(sender, recipients, activity, options, span)) this.#deliveryState.delivered = true;
8640
- } catch (e) {
8641
- span.setStatus({
8642
- code: SpanStatusCode.ERROR,
8643
- message: String(e)
8644
- });
8645
- throw e;
8646
- } finally {
8647
- span.end();
8648
- }
8649
- });
9862
+ return observeOutboundBroadcast(this, this.federation.outboundObservers, (observation, provenance) => {
9863
+ const tracer = this.tracerProvider.getTracer(name, version);
9864
+ const parentSpanContext = trace.getSpanContext(context.active());
9865
+ const activityType = getTypeId(activity).href;
9866
+ observation?.setActivity({ type: activityType });
9867
+ return tracer.startActiveSpan(this.federation.outboxQueue == null || options.immediate ? "activitypub.outbox" : "activitypub.fanout", {
9868
+ kind: this.federation.outboxQueue == null || options.immediate ? SpanKind.CLIENT : SpanKind.PRODUCER,
9869
+ attributes: {
9870
+ "activitypub.activity.type": activityType,
9871
+ "activitypub.activity.to": activity.toIds.map((to) => to.href),
9872
+ "activitypub.activity.cc": activity.ccIds.map((cc) => cc.href),
9873
+ "activitypub.activity.bto": activity.btoIds.map((bto) => bto.href),
9874
+ "activitypub.activity.bcc": activity.bccIds.map((bcc) => bcc.href)
9875
+ }
9876
+ }, async (span) => {
9877
+ observation?.setSpan(span, parentSpanContext);
9878
+ try {
9879
+ const activityId = activity.id?.href;
9880
+ if (activityId != null) span.setAttribute("activitypub.activity.id", activityId);
9881
+ observation?.setActivity({
9882
+ type: activityType,
9883
+ ...activityId == null ? {} : { id: activityId }
9884
+ });
9885
+ if (await this.sendActivityInternal(sender, recipients, activity, options, span, observation, provenance)) this.#deliveryState.delivered = true;
9886
+ } catch (e) {
9887
+ span.setStatus({
9888
+ code: SpanStatusCode.ERROR,
9889
+ message: String(e)
9890
+ });
9891
+ throw e;
9892
+ } finally {
9893
+ span.end();
9894
+ }
9895
+ });
9896
+ }, "send", options.dispatchId);
8650
9897
  }
8651
9898
  forwardActivity(forwarder, recipients, options) {
8652
9899
  return forwardActivity(this, "outbox", forwarder, recipients, options).then((delivered) => {
@@ -8655,6 +9902,7 @@ var OutboxContextImpl = class OutboxContextImpl extends ContextImpl {
8655
9902
  }
8656
9903
  clone(data) {
8657
9904
  return new OutboxContextImpl(this.identifier, this.activity, this.activityId, this.activityType, {
9905
+ [outboundProvenance]: getOutboundProvenance(this),
8658
9906
  url: this.url,
8659
9907
  federation: this.federation,
8660
9908
  data,
@@ -8716,4 +9964,4 @@ function getRequestId(request) {
8716
9964
  return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
8717
9965
  }
8718
9966
  //#endregion
8719
- export { getEmbeddedFirstGateway as C, handleNodeInfoJrd as D, handleNodeInfo as E, actorDehydrator as O, getCompactRootId as S, handleBenchmarkTrigger as T, coversDelivery as _, OutboxContextImpl as a, PORTABLE_OBJECT_CONTENT_TYPE as b, assertPortableActorActivity as c, handleCustomCollection as d, handleInbox as f, respondWithObjectIfAcceptable as g, respondWithObject as h, KvSpecDeterminer as i, autoIdAssigner as k, handleActor as l, handleOutbox as m, FederationImpl as n, createFederation as o, handleObject as p, InboxContextImpl as r, handleWebFinger as s, ContextImpl as t, handleCollection as u, forwardPortableInboxActivity as v, warnCompatibleIdsInJson as w, parseHashlinkGatewayRequest as x, resolvePortableInboxForwardingOptions as y };
9967
+ export { OutboundAttemptObservation as A, getEmbeddedFirstGateway as C, handleNodeInfoJrd as D, handleNodeInfo as E, observeOutboundBroadcast as M, actorDehydrator as O, getCompactRootId as S, handleBenchmarkTrigger as T, coversDelivery as _, OutboxContextImpl as a, PORTABLE_OBJECT_CONTENT_TYPE as b, assertPortableActorActivity as c, handleCustomCollection as d, handleInbox as f, respondWithObjectIfAcceptable as g, respondWithObject as h, KvSpecDeterminer as i, OutboundBroadcastObservation as j, autoIdAssigner as k, handleActor as l, handleOutbox as m, FederationImpl as n, createFederation as o, handleObject as p, InboxContextImpl as r, handleWebFinger as s, ContextImpl as t, handleCollection as u, forwardPortableInboxActivity as v, warnCompatibleIdsInJson as w, parseHashlinkGatewayRequest as x, resolvePortableInboxForwardingOptions as y };