@fedify/fedify 2.4.0-dev.2256 → 2.4.0-dev.2259

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 (105) hide show
  1. package/dist/{builder-DvSQqk2l.mjs → builder-CBNa1Uw2.mjs} +8 -2
  2. package/dist/compat/mod.d.cts +1 -1
  3. package/dist/compat/mod.d.ts +1 -1
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{compound-proof-5bg_j_ye.mjs → compound-proof-CMPv4UK0.mjs} +20 -3
  6. package/dist/{context-CtI307_A.d.ts → context-7NyiqIcg.d.ts} +111 -2
  7. package/dist/{context-B6X-t8gI.d.cts → context-tsYPJ2Ew.d.cts} +111 -2
  8. package/dist/{deno-AmcRMq5z.mjs → deno-C8XaXmPB.mjs} +1 -1
  9. package/dist/{docloader-CIP_SG39.mjs → docloader-CpdFLwAg.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 +76 -5
  14. package/dist/federation/hashlink-media.test.mjs +2 -2
  15. package/dist/federation/idempotency.test.mjs +2 -2
  16. package/dist/federation/inbox-report.test.d.mts +2 -0
  17. package/dist/federation/inbox-report.test.mjs +475 -0
  18. package/dist/federation/metrics.test.mjs +1 -1
  19. package/dist/federation/middleware.test.mjs +7 -7
  20. package/dist/federation/mod.cjs +1 -1
  21. package/dist/federation/mod.d.cts +4 -3
  22. package/dist/federation/mod.d.ts +4 -3
  23. package/dist/federation/mod.js +1 -1
  24. package/dist/federation/outgoing-proof.test.mjs +5 -5
  25. package/dist/federation/portable-actor.test.mjs +3 -3
  26. package/dist/federation/portable-chain.test.mjs +3 -3
  27. package/dist/federation/portable-collection.test.mjs +4 -4
  28. package/dist/federation/portable-delivery.test.mjs +2 -2
  29. package/dist/federation/portable-gateway-keys.test.mjs +4 -4
  30. package/dist/federation/portable-inbox.test.mjs +67 -6
  31. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  32. package/dist/federation/portable-route-activity.test.mjs +2 -2
  33. package/dist/federation/portable.test.mjs +3 -3
  34. package/dist/federation/send.test.mjs +3 -3
  35. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  36. package/dist/federation/tasks/tasks.test.mjs +2 -2
  37. package/dist/federation/temporal.test.mjs +1 -1
  38. package/dist/federation/webfinger.test.mjs +1 -1
  39. package/dist/{federation-BDzIgqOd.cjs → federation-D6giSGUT.cjs} +524 -118
  40. package/dist/{http-DkqWu-sm.mjs → http-BqSbUEiQ.mjs} +141 -98
  41. package/dist/{http-1fZ83jj7.cjs → http-Cki-npuA.cjs} +447 -110
  42. package/dist/{http-BGb3QioZ.js → http-OMtKlbx1.js} +430 -111
  43. package/dist/{http-BrwKINQn.d.cts → key-C1nv7_jD.d.ts} +638 -229
  44. package/dist/{http-DGx0KOws.d.ts → key-DYxuyd-s.d.cts} +638 -229
  45. package/dist/{key-CEsu7oaD.mjs → key-DecAtABm.mjs} +30 -3
  46. package/dist/{kv-cache-CJS4mImS.mjs → kv-cache-CWWlMv-s.mjs} +1 -1
  47. package/dist/{kv-cache-BEWuZSB4.cjs → kv-cache-D73yVcE5.cjs} +1 -1
  48. package/dist/{kv-cache-CGhEgt98.js → kv-cache-J0v3UbUn.js} +1 -1
  49. package/dist/{ld-Cf4CPWN_.mjs → ld-UsN-TIPh.mjs} +40 -5
  50. package/dist/{metrics-CocsZP0E.mjs → metrics-DWCn4VXX.mjs} +2 -1
  51. package/dist/{middleware-BfYke3YQ.js → middleware-BqzfIlUO.js} +524 -118
  52. package/dist/{middleware-Dxn0E9ES.mjs → middleware-CkjwiEBd.mjs} +512 -127
  53. package/dist/{middleware-Cne_jMZo.mjs → middleware-DHn2q0L1.mjs} +1 -1
  54. package/dist/{mod-C7_2j7VT.d.cts → mod-BojP2Bw3.d.cts} +1 -1
  55. package/dist/{mod-BlLwewYe.d.ts → mod-CavhAgLw.d.cts} +5 -255
  56. package/dist/{mod-VHYh1lTK.d.cts → mod-CtpF2BU5.d.ts} +5 -255
  57. package/dist/{mod-B8MI1mpi.d.ts → mod-DEZXwV6J.d.ts} +1 -1
  58. package/dist/{mod-DFL6IxYQ.d.cts → mod-Dw-nPxQh.d.cts} +1 -1
  59. package/dist/{mod-BhTaDR5H.d.ts → mod-Xj5P4s4d.d.ts} +1 -1
  60. package/dist/mod.cjs +4 -4
  61. package/dist/mod.d.cts +7 -7
  62. package/dist/mod.d.ts +7 -7
  63. package/dist/mod.js +4 -4
  64. package/dist/nodeinfo/handler.test.mjs +1 -1
  65. package/dist/{owner-DSJ7nFgy.mjs → owner-CEBn81LD.mjs} +3 -3
  66. package/dist/{portable-collection-r-mHg1PV.cjs → portable-collection-DqTUBKY7.cjs} +1 -1
  67. package/dist/{portable-collection-BuBV6xTo.js → portable-collection-ZkUNjtb7.js} +1 -1
  68. package/dist/{portable-collection-qskqXJrH.mjs → portable-collection-qFSYw4TG.mjs} +2 -2
  69. package/dist/{portable-key-woGH_1nn.mjs → portable-key-LlMkP-vz.mjs} +1 -1
  70. package/dist/{proof-8WiH_-Gq.mjs → proof-BHRf_jMG.mjs} +133 -16
  71. package/dist/{send-JKzZBgPU.mjs → send-DjG8DDU_.mjs} +3 -3
  72. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  73. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  74. package/dist/sig/compound-proof-verification.test.mjs +19 -2
  75. package/dist/sig/compound-proof.test.mjs +2 -2
  76. package/dist/sig/http.test.mjs +2 -2
  77. package/dist/sig/key.test.mjs +1 -1
  78. package/dist/sig/ld.test.mjs +2 -2
  79. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  80. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  81. package/dist/sig/mod.cjs +2 -2
  82. package/dist/sig/mod.d.cts +2 -2
  83. package/dist/sig/mod.d.ts +2 -2
  84. package/dist/sig/mod.js +2 -2
  85. package/dist/sig/owner.test.mjs +1 -1
  86. package/dist/sig/portable-collection.test.mjs +1 -1
  87. package/dist/sig/portable-interop.test.mjs +3 -3
  88. package/dist/sig/portable-key.test.mjs +6 -6
  89. package/dist/sig/proof.test.mjs +76 -3
  90. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  91. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  92. package/dist/sig/verification.test.d.mts +2 -0
  93. package/dist/sig/verification.test.mjs +310 -0
  94. package/dist/{temporal-CT3fRaMH.mjs → temporal-BRFH_dte.mjs} +1 -1
  95. package/dist/testing/kv.test.mjs +1 -1
  96. package/dist/testing/mod.d.mts +315 -28
  97. package/dist/utils/docloader.test.mjs +2 -2
  98. package/dist/utils/kv-cache.test.mjs +1 -1
  99. package/dist/utils/mod.cjs +1 -1
  100. package/dist/utils/mod.d.cts +1 -1
  101. package/dist/utils/mod.d.ts +1 -1
  102. package/dist/utils/mod.js +1 -1
  103. package/dist/verification-C3954hQh.mjs +5 -0
  104. package/dist/verification-Dpfcw8ml.mjs +105 -0
  105. package/package.json +7 -7
@@ -3,28 +3,29 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { d as isPortableUri, f as isSameObjectId, i as getPortableDid, l as isPortableId, n as getCanonicalPortableId, o as hasPortableActor, r as getGatewayKeyBase, s as isCompatibleKeyId, t as getAuthenticationOrigin, u as isPortableKeyId } from "./portable-key-id-Ca30-hOa.mjs";
5
5
  import { n as isPubliclyAddressedNode, t as isInAudience } from "./audience-CHQ-FlLD.mjs";
6
- import { n as version, t as name } from "./deno-AmcRMq5z.mjs";
7
- 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-CocsZP0E.mjs";
6
+ import { a as verificationObservation, r as snapshotKey, t as observeAttempt } from "./verification-Dpfcw8ml.mjs";
7
+ import { n as version, t as name } from "./deno-C8XaXmPB.mjs";
8
+ 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-DWCn4VXX.mjs";
8
9
  import { t as formatAcceptSignature } from "./accept-CPVqki78.mjs";
9
- import { c as validateCryptoKey, n as exportJwk, s as importJwk } from "./key-CEsu7oaD.mjs";
10
+ import { c as validateCryptoKey, n as exportJwk, s as importJwk } from "./key-DecAtABm.mjs";
10
11
  import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-4IJqPYos.mjs";
11
- import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-Cf4CPWN_.mjs";
12
- import { a as signObject, c as verifyPortableObjectProof, i as hasProofLike, s as verifyObject, t as InvalidPortableObjectIdError, u as verifyPortableObjectProofWithRoot } from "./proof-8WiH_-Gq.mjs";
13
- import { a as listRequestSignatures, c as selectRequestSignature, d as validateMaxSignatures, f as verifyRequest, p as verifyRequestDetailed, s as parseRfc9421SignatureInput } from "./http-DkqWu-sm.mjs";
14
- import { t as getAuthenticatedDocumentLoader } from "./docloader-CIP_SG39.mjs";
15
- import { n as kvCache } from "./kv-cache-CJS4mImS.mjs";
12
+ import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-UsN-TIPh.mjs";
13
+ import { a as signObject, c as verifyPortableObjectProof, i as hasProofLike, s as verifyObject, t as InvalidPortableObjectIdError, u as verifyPortableObjectProofWithRoot } from "./proof-BHRf_jMG.mjs";
14
+ import { a as listRequestSignatures, c as selectRequestSignature, d as validateMaxSignatures, f as verifyRequest, p as verifyRequestDetailed, s as parseRfc9421SignatureInput } from "./http-BqSbUEiQ.mjs";
15
+ import { t as getAuthenticatedDocumentLoader } from "./docloader-CpdFLwAg.mjs";
16
+ import { n as kvCache } from "./kv-cache-CWWlMv-s.mjs";
16
17
  import { d as readBoundedText, l as BodyTooLargeError, t as getNodeInfo, u as MAX_BODY_SIZE } from "./client-CEnNz8O7.mjs";
17
18
  import { t as nodeInfoToJson } from "./types-DE_LV1JB.mjs";
18
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-DSJ7nFgy.mjs";
19
- import { t as verifyPortableObject } from "./portable-collection-qskqXJrH.mjs";
20
- import { a as getOrderingDestination, i as sendActivityThroughGateways, n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-JKzZBgPU.mjs";
21
- import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-DvSQqk2l.mjs";
19
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-CEBn81LD.mjs";
20
+ import { t as verifyPortableObject } from "./portable-collection-qFSYw4TG.mjs";
21
+ import { a as getOrderingDestination, i as sendActivityThroughGateways, n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-DjG8DDU_.mjs";
22
+ import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-CBNa1Uw2.mjs";
22
23
  import { t as CircuitBreaker } from "./circuit-breaker-XJ53md4S.mjs";
23
24
  import { t as buildCollectionSynchronizationHeader } from "./collection-CnnP1UAv.mjs";
24
- import { a as verifyCompoundPortableObjectProofs, i as inspectCompoundPortableObjectApplicability, r as findUnsupportedCompoundProofShape, s as verifyServedPortableObjects, t as containsCompoundPortableObject } from "./compound-proof-5bg_j_ye.mjs";
25
+ import { a as verifyCompoundPortableObjectProofs, i as inspectCompoundPortableObjectApplicability, r as findUnsupportedCompoundProofShape, s as verifyServedPortableObjects, t as containsCompoundPortableObject } from "./compound-proof-CMPv4UK0.mjs";
25
26
  import { t as KvKeyCache } from "./keycache-Da8A8mZa.mjs";
26
27
  import { t as acceptsJsonLd } from "./negotiation-CCocWT_N.mjs";
27
- import { t as hasMalformedKnownTemporalLiteral } from "./temporal-CT3fRaMH.mjs";
28
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-BRFH_dte.mjs";
28
29
  import { t as createExponentialBackoffPolicy } from "./retry-zX5xw5e0.mjs";
29
30
  import { t as TaskCodec } from "./codec-C6rTrpIp.mjs";
30
31
  import { getLogger, withContext } from "@logtape/logtape";
@@ -39,6 +40,111 @@ import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_
39
40
  import jsonld from "@fedify/vocab-runtime/jsonld";
40
41
  import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
41
42
  import { domainToASCII } from "node:url";
43
+ //#region src/federation/inbox-observation.ts
44
+ /** Internal state owned by the ingress boundary, never serialized into queues. */
45
+ var InboxObservation = class {
46
+ inbox;
47
+ payload = { status: "unavailable" };
48
+ activity = null;
49
+ authentication = { status: "notDetermined" };
50
+ hasException = false;
51
+ stage = "prepare";
52
+ result = {
53
+ disposition: "rejected",
54
+ reason: "recipientNotFound"
55
+ };
56
+ verification = {
57
+ attempts: [],
58
+ subject: {
59
+ id: null,
60
+ pointer: ""
61
+ },
62
+ parsedObject: (object) => {
63
+ if (object instanceof Activity) {
64
+ this.activity = object;
65
+ this.verification.subject.id = object.id == null ? null : new URL(object.id.href);
66
+ }
67
+ }
68
+ };
69
+ constructor(inbox) {
70
+ this.inbox = inbox;
71
+ }
72
+ /** Run an ingress operation and project telemetry; fetch owns completion. */
73
+ async run(operation, span) {
74
+ let response;
75
+ let outcome;
76
+ let threw = false;
77
+ let originalError;
78
+ try {
79
+ response = await operation();
80
+ outcome = {
81
+ type: "response",
82
+ status: response.status,
83
+ ...this.result
84
+ };
85
+ } catch (error) {
86
+ threw = true;
87
+ this.hasException = true;
88
+ originalError = error;
89
+ outcome = {
90
+ type: "exception",
91
+ stage: this.stage,
92
+ error
93
+ };
94
+ }
95
+ if (span != null) this.project(span, outcome);
96
+ if (threw) throw originalError;
97
+ return response;
98
+ }
99
+ /** Complete standalone handler calls that have no enclosing fetch boundary. */
100
+ async runAndFinish(getContext, handler, operation) {
101
+ let response;
102
+ try {
103
+ response = await this.run(operation);
104
+ } catch (error) {
105
+ await this.finish(getContext(), handler, {
106
+ type: "exception",
107
+ stage: this.stage,
108
+ error
109
+ });
110
+ throw error;
111
+ }
112
+ await this.finish(getContext(), handler, {
113
+ type: "response",
114
+ status: response.status,
115
+ ...this.result
116
+ });
117
+ return response;
118
+ }
119
+ project(span, outcome) {
120
+ span.setAttribute("activitypub.authentication.status", this.authentication.status);
121
+ if (this.authentication.status === "rejected") span.setAttribute("activitypub.verification.failure_reason", this.authentication.reason.type);
122
+ span.setAttribute("activitypub.inbox.disposition", outcome?.type === "exception" || this.hasException ? "exception" : this.result.disposition);
123
+ }
124
+ async finish(context, handler, outcome) {
125
+ if (handler == null) return;
126
+ const report = {
127
+ inbox: this.inbox,
128
+ payload: this.payload,
129
+ activity: this.activity,
130
+ attempts: this.verification.attempts,
131
+ authentication: this.authentication,
132
+ outcome
133
+ };
134
+ try {
135
+ await handler(context, report);
136
+ } catch (error) {
137
+ try {
138
+ getLogger([
139
+ "fedify",
140
+ "federation",
141
+ "inbox"
142
+ ]).error("An unexpected error occurred in inbox request finished handler:\n{error}", { error });
143
+ } catch {}
144
+ }
145
+ }
146
+ };
147
+ //#endregion
42
148
  //#region src/compat/transformers.ts
43
149
  const logger$2 = getLogger([
44
150
  "fedify",
@@ -1620,13 +1726,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
1620
1726
  * @param parameters The parameters for handling the collection.
1621
1727
  * @returns A promise that resolves to an HTTP response.
1622
1728
  */
1623
- async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, portable, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
1624
- const spanName = name$1.trim().replace(/\s+/g, "_");
1729
+ async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, portable, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
1730
+ const spanName = name$2.trim().replace(/\s+/g, "_");
1625
1731
  tracerProvider = tracerProvider ?? trace.getTracerProvider();
1626
1732
  const tracer = tracerProvider.getTracer(name, version);
1627
1733
  const cursor = new URL(request.url).searchParams.get("cursor");
1628
1734
  const metricBase = {
1629
- kind: getCollectionMetricKind(name$1),
1735
+ kind: getCollectionMetricKind(name$2),
1630
1736
  page: cursor != null,
1631
1737
  dispatcher: "built_in"
1632
1738
  };
@@ -1672,7 +1778,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
1672
1778
  span.setStatus({ code: SpanStatusCode.ERROR });
1673
1779
  return await onNotFound(request);
1674
1780
  }
1675
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
1781
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
1676
1782
  itemCount = items.length;
1677
1783
  span.setAttribute("fedify.collection.items", itemCount);
1678
1784
  return items;
@@ -1714,7 +1820,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
1714
1820
  } else {
1715
1821
  const uri = new URL(portable == null ? baseUri : pageBase);
1716
1822
  uri.searchParams.set("cursor", cursor);
1717
- const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
1823
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
1718
1824
  kind: SpanKind.SERVER,
1719
1825
  attributes: {
1720
1826
  "activitypub.collection.id": uri.href,
@@ -1730,7 +1836,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
1730
1836
  span.setStatus({ code: SpanStatusCode.ERROR });
1731
1837
  return await onNotFound(request);
1732
1838
  }
1733
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
1839
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
1734
1840
  itemCount = items.length;
1735
1841
  span.setAttribute("fedify.collection.items", itemCount);
1736
1842
  return {
@@ -2211,6 +2317,16 @@ async function handleMediaUpload(request, { identifier, context: ctx, mediaUploa
2211
2317
  * @returns A promise that resolves to an HTTP response.
2212
2318
  */
2213
2319
  async function handleInbox(request, options) {
2320
+ if (options.observation == null) {
2321
+ const observation = new InboxObservation({
2322
+ kind: options.portableInbox != null ? "portable" : options.recipient == null ? "shared" : "personal",
2323
+ recipient: options.recipient
2324
+ });
2325
+ return await observation.runAndFinish(() => options.context, options.inboxRequestFinishedHandler, () => handleInbox(request, {
2326
+ ...options,
2327
+ observation
2328
+ }));
2329
+ }
2214
2330
  return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("activitypub.inbox", {
2215
2331
  kind: options.queue == null ? SpanKind.SERVER : SpanKind.PRODUCER,
2216
2332
  attributes: { "activitypub.shared_inbox": options.recipient == null }
@@ -2219,12 +2335,14 @@ async function handleInbox(request, options) {
2219
2335
  try {
2220
2336
  return await handleInboxInternal(request, options, span);
2221
2337
  } catch (e) {
2338
+ options.observation.hasException = true;
2222
2339
  span.setStatus({
2223
2340
  code: SpanStatusCode.ERROR,
2224
2341
  message: String(e)
2225
2342
  });
2226
2343
  throw e;
2227
2344
  } finally {
2345
+ options.observation.project(span);
2228
2346
  span.end();
2229
2347
  }
2230
2348
  });
@@ -2239,6 +2357,8 @@ async function handleInbox(request, options) {
2239
2357
  */
2240
2358
  async function handleInboxInternal(request, parameters, span) {
2241
2359
  const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, maxHttpSignatures, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider, portableInbox } = parameters;
2360
+ const observation = parameters.observation;
2361
+ const signatureObservation = { [verificationObservation]: observation.verification };
2242
2362
  const logger = getLogger([
2243
2363
  "fedify",
2244
2364
  "federation",
@@ -2263,6 +2383,10 @@ async function handleInboxInternal(request, parameters, span) {
2263
2383
  }
2264
2384
  }
2265
2385
  if (request.bodyUsed) {
2386
+ observation.result = {
2387
+ disposition: "failed",
2388
+ reason: "bodyUnavailable"
2389
+ };
2266
2390
  logger.error("Request body has already been read.", { recipient });
2267
2391
  span.setStatus({
2268
2392
  code: SpanStatusCode.ERROR,
@@ -2273,6 +2397,10 @@ async function handleInboxInternal(request, parameters, span) {
2273
2397
  headers: { "Content-Type": "text/plain; charset=utf-8" }
2274
2398
  });
2275
2399
  } else if (request.body?.locked) {
2400
+ observation.result = {
2401
+ disposition: "failed",
2402
+ reason: "bodyUnavailable"
2403
+ };
2276
2404
  logger.error("Request body is locked.", { recipient });
2277
2405
  span.setStatus({
2278
2406
  code: SpanStatusCode.ERROR,
@@ -2283,11 +2411,20 @@ async function handleInboxInternal(request, parameters, span) {
2283
2411
  headers: { "Content-Type": "text/plain; charset=utf-8" }
2284
2412
  });
2285
2413
  }
2414
+ observation.stage = "parse";
2415
+ observation.result = {
2416
+ disposition: "rejected",
2417
+ reason: "invalidJson"
2418
+ };
2286
2419
  let json;
2287
2420
  try {
2288
2421
  json = JSON.parse(await readBoundedText(request.clone(), MAX_BODY_SIZE, request.url));
2289
2422
  } catch (error) {
2290
2423
  if (error instanceof BodyTooLargeError) {
2424
+ observation.result = {
2425
+ disposition: "rejected",
2426
+ reason: "bodyTooLarge"
2427
+ };
2291
2428
  request.body?.cancel(error).catch(() => {});
2292
2429
  span.setStatus({
2293
2430
  code: SpanStatusCode.ERROR,
@@ -2317,6 +2454,15 @@ async function handleInboxInternal(request, parameters, span) {
2317
2454
  headers: { "Content-Type": "text/plain; charset=utf-8" }
2318
2455
  });
2319
2456
  }
2457
+ observation.payload = {
2458
+ status: "parsed",
2459
+ value: structuredClone(json)
2460
+ };
2461
+ observation.result = {
2462
+ disposition: "rejected",
2463
+ reason: "invalidActivity"
2464
+ };
2465
+ observation.stage = "verify";
2320
2466
  const keyCache = new KvKeyCache(kv, kvPrefixes.publicKey, {
2321
2467
  documentLoader: ctx.documentLoader,
2322
2468
  contextLoader: ctx.contextLoader,
@@ -2346,6 +2492,10 @@ async function handleInboxInternal(request, parameters, span) {
2346
2492
  code: SpanStatusCode.ERROR,
2347
2493
  message: `Failed to parse activity:\n${error}`
2348
2494
  });
2495
+ observation.result = {
2496
+ disposition: "rejected",
2497
+ reason: "invalidActivity"
2498
+ };
2349
2499
  return new Response("Invalid activity.", {
2350
2500
  status: 400,
2351
2501
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -2363,11 +2513,25 @@ async function handleInboxInternal(request, parameters, span) {
2363
2513
  recipient,
2364
2514
  error
2365
2515
  });
2516
+ observation.result = {
2517
+ disposition: "rejected",
2518
+ reason: "invalidJsonLd"
2519
+ };
2366
2520
  return new Response("Invalid JSON-LD.", {
2367
2521
  status: 400,
2368
2522
  headers: { "Content-Type": "text/plain; charset=utf-8" }
2369
2523
  });
2370
2524
  }
2525
+ observation.verification.attempts.push({
2526
+ mechanism: "linkedData",
2527
+ subject: {
2528
+ id: null,
2529
+ pointer: ""
2530
+ },
2531
+ checks: [],
2532
+ status: "error",
2533
+ error
2534
+ });
2371
2535
  if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
2372
2536
  if (!skipSignatureVerification) deferredLdSignatureError = error;
2373
2537
  logger.debug("Failed to normalize JSON-LD for Linked Data Signatures; deferring to another authentication path only if it verifies:\n{error}", {
@@ -2379,6 +2543,7 @@ async function handleInboxInternal(request, parameters, span) {
2379
2543
  compactedJsonWithoutSig = detachSignature(compactedJson);
2380
2544
  try {
2381
2545
  ldSigVerified = await verifyCompactJsonLd(compactedJson, {
2546
+ ...signatureObservation,
2382
2547
  contextLoader: ctx.contextLoader,
2383
2548
  documentLoader: ctx.documentLoader,
2384
2549
  keyCache,
@@ -2392,6 +2557,10 @@ async function handleInboxInternal(request, parameters, span) {
2392
2557
  recipient,
2393
2558
  error
2394
2559
  });
2560
+ observation.result = {
2561
+ disposition: "rejected",
2562
+ reason: "invalidJsonLd"
2563
+ };
2395
2564
  return new Response("Invalid JSON-LD.", {
2396
2565
  status: 400,
2397
2566
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -2411,6 +2580,10 @@ async function handleInboxInternal(request, parameters, span) {
2411
2580
  recipient,
2412
2581
  error: parseError
2413
2582
  });
2583
+ observation.result = {
2584
+ disposition: "rejected",
2585
+ reason: "invalidJsonLd"
2586
+ };
2414
2587
  return new Response("Invalid JSON-LD.", {
2415
2588
  status: 400,
2416
2589
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -2431,7 +2604,7 @@ async function handleInboxInternal(request, parameters, span) {
2431
2604
  json
2432
2605
  });
2433
2606
  try {
2434
- activity = await Activity.fromJsonLd(compactedJsonWithoutSig, {
2607
+ activity = observation.activity = await Activity.fromJsonLd(compactedJsonWithoutSig, {
2435
2608
  ...ctx,
2436
2609
  contextLoader: getNormalizationContextLoader(ctx.contextLoader)
2437
2610
  });
@@ -2443,6 +2616,7 @@ async function handleInboxInternal(request, parameters, span) {
2443
2616
  activityVerified = true;
2444
2617
  if (!skipSignatureVerification && isPortableActivity(activity)) try {
2445
2618
  proofVerified = await verifyObject(Activity, jsonWithoutSig, {
2619
+ ...signatureObservation,
2446
2620
  contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
2447
2621
  documentLoader: ctx.documentLoader,
2448
2622
  keyCache,
@@ -2463,6 +2637,7 @@ async function handleInboxInternal(request, parameters, span) {
2463
2637
  });
2464
2638
  try {
2465
2639
  activity = await verifyObject(Activity, jsonWithoutSig, {
2640
+ ...signatureObservation,
2466
2641
  contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
2467
2642
  documentLoader: ctx.documentLoader,
2468
2643
  keyCache,
@@ -2498,6 +2673,10 @@ async function handleInboxInternal(request, parameters, span) {
2498
2673
  code: SpanStatusCode.ERROR,
2499
2674
  message: `Failed to parse activity:\n${error}`
2500
2675
  });
2676
+ observation.result = {
2677
+ disposition: "rejected",
2678
+ reason: "invalidActivity"
2679
+ };
2501
2680
  return new Response("Invalid activity.", {
2502
2681
  status: 400,
2503
2682
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -2521,6 +2700,7 @@ async function handleInboxInternal(request, parameters, span) {
2521
2700
  if (activity == null) {
2522
2701
  if (!skipSignatureVerification) {
2523
2702
  const verification = await verifyRequestDetailed(request, {
2703
+ ...signatureObservation,
2524
2704
  contextLoader: ctx.contextLoader,
2525
2705
  documentLoader: ctx.documentLoader,
2526
2706
  timeWindow: signatureTimeWindow,
@@ -2531,6 +2711,14 @@ async function handleInboxInternal(request, parameters, span) {
2531
2711
  });
2532
2712
  if (verification.verified === false) {
2533
2713
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
2714
+ observation.authentication = {
2715
+ status: "rejected",
2716
+ reason: { type: "verificationFailed" }
2717
+ };
2718
+ observation.result = {
2719
+ disposition: "rejected",
2720
+ reason: "authentication"
2721
+ };
2534
2722
  const reason = verification.reason;
2535
2723
  const remoteHost = "keyId" in reason && reason.keyId != null ? getRemoteHost(reason.keyId) : void 0;
2536
2724
  getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure(reason.type, remoteHost);
@@ -2545,7 +2733,7 @@ async function handleInboxInternal(request, parameters, span) {
2545
2733
  });
2546
2734
  if (unverifiedActivityHandler == null) return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
2547
2735
  try {
2548
- activity = await Activity.fromJsonLd(jsonWithoutSig, ctx);
2736
+ activity = observation.activity = await Activity.fromJsonLd(jsonWithoutSig, ctx);
2549
2737
  } catch (error) {
2550
2738
  logger.error("Failed to parse activity:\n{error}", {
2551
2739
  recipient,
@@ -2561,6 +2749,10 @@ async function handleInboxInternal(request, parameters, span) {
2561
2749
  recipient
2562
2750
  });
2563
2751
  }
2752
+ observation.result = {
2753
+ disposition: "rejected",
2754
+ reason: "invalidActivity"
2755
+ };
2564
2756
  return new Response("Invalid activity.", {
2565
2757
  status: 400,
2566
2758
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -2583,6 +2775,11 @@ async function handleInboxInternal(request, parameters, span) {
2583
2775
  try {
2584
2776
  response = await unverifiedActivityHandler(ctx, activity, reason);
2585
2777
  } catch (error) {
2778
+ observation.result = {
2779
+ disposition: "failed",
2780
+ reason: "listenerError",
2781
+ error
2782
+ };
2586
2783
  logger.error("An unexpected error occurred in unverified activity handler:\n{error}", {
2587
2784
  error,
2588
2785
  activity: json,
@@ -2599,7 +2796,10 @@ async function handleInboxInternal(request, parameters, span) {
2599
2796
  }
2600
2797
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
2601
2798
  }
2602
- if (response instanceof Response) return response;
2799
+ if (response instanceof Response) {
2800
+ observation.result = { disposition: "customResponse" };
2801
+ return response;
2802
+ }
2603
2803
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
2604
2804
  } else {
2605
2805
  if (inboxChallengePolicy?.enabled && inboxChallengePolicy.requestNonce) pendingNonceLabel = verification.signatureLabel;
@@ -2609,7 +2809,7 @@ async function handleInboxInternal(request, parameters, span) {
2609
2809
  httpSigKey = verification.key;
2610
2810
  }
2611
2811
  try {
2612
- activity = await Activity.fromJsonLd(jsonWithoutSig, {
2812
+ activity = observation.activity = await Activity.fromJsonLd(jsonWithoutSig, {
2613
2813
  ...ctx,
2614
2814
  contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader)
2615
2815
  });
@@ -2618,6 +2818,12 @@ async function handleInboxInternal(request, parameters, span) {
2618
2818
  return await respondInvalidActivity(error);
2619
2819
  }
2620
2820
  }
2821
+ observation.activity = activity;
2822
+ observation.stage = "policy";
2823
+ observation.result = {
2824
+ disposition: "rejected",
2825
+ reason: "authentication"
2826
+ };
2621
2827
  if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
2622
2828
  span.setAttribute("activitypub.activity.type", getTypeId(activity).href);
2623
2829
  span.addEvent("activitypub.activity.received", {
@@ -2629,6 +2835,13 @@ async function handleInboxInternal(request, parameters, span) {
2629
2835
  });
2630
2836
  if (!skipSignatureVerification && !proofVerified && (isPortableActivity(activity) || isPortableActorKey(httpSigKey))) {
2631
2837
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
2838
+ observation.authentication = {
2839
+ status: "rejected",
2840
+ reason: {
2841
+ type: "proofPolicy",
2842
+ policy: "portableActor"
2843
+ }
2844
+ };
2632
2845
  logger.error("The activity {activityId} of the portable actor {actorId} is not authenticated by a valid Object Integrity Proof.", {
2633
2846
  activity: json,
2634
2847
  recipient,
@@ -2646,20 +2859,36 @@ async function handleInboxInternal(request, parameters, span) {
2646
2859
  });
2647
2860
  }
2648
2861
  if (!skipSignatureVerification && activity.id != null && isPortableId(activity.id)) {
2649
- const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
2650
- contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
2651
- documentLoader: ctx.documentLoader,
2652
- keyCache,
2653
- meterProvider,
2654
- tracerProvider
2655
- });
2862
+ const rejection = await observeAttempt(signatureObservation, "objectIntegrity", async (verification) => {
2863
+ const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
2864
+ [verificationObservation]: verification,
2865
+ contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
2866
+ documentLoader: ctx.documentLoader,
2867
+ keyCache,
2868
+ meterProvider,
2869
+ tracerProvider
2870
+ });
2871
+ if (rejection != null) verification.attempt.reason = {
2872
+ type: "proofPolicy",
2873
+ reason: rejection
2874
+ };
2875
+ return rejection;
2876
+ }, (value) => value == null);
2656
2877
  if (rejection != null) {
2657
2878
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
2879
+ observation.authentication = {
2880
+ status: "rejected",
2881
+ reason: {
2882
+ type: "proofPolicy",
2883
+ policy: "portableActivity",
2884
+ detail: rejection
2885
+ }
2886
+ };
2658
2887
  logger.error("The portable activity {activityId} is not authenticated by an Object Integrity Proof of its own DID: {reason}", {
2659
2888
  activity: json,
2660
2889
  recipient,
2661
2890
  activityId: activity.id.href,
2662
- reason: rejection
2891
+ reason: rejection.type
2663
2892
  });
2664
2893
  span.setStatus({
2665
2894
  code: SpanStatusCode.ERROR,
@@ -2673,6 +2902,14 @@ async function handleInboxInternal(request, parameters, span) {
2673
2902
  }
2674
2903
  if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
2675
2904
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
2905
+ observation.authentication = {
2906
+ status: "rejected",
2907
+ reason: {
2908
+ type: "actorKeyMismatch",
2909
+ key: snapshotKey(httpSigKey),
2910
+ actorIds: activity.actorIds.map((id) => new URL(id.href))
2911
+ }
2912
+ };
2676
2913
  getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : getRemoteHost(httpSigKey.id));
2677
2914
  logger.error("The signer ({keyId}) and the actor ({actorId}) do not match.", {
2678
2915
  activity: json,
@@ -2692,13 +2929,35 @@ async function handleInboxInternal(request, parameters, span) {
2692
2929
  if (!skipSignatureVerification) {
2693
2930
  const compoundJson = Array.isArray(json) ? json : jsonWithoutSig;
2694
2931
  if (inspectCompoundPortableObjectApplicability(compoundJson, INBOX_COMPOUND_PROOF_LIMITS) !== "absent") {
2695
- const compoundProof = await verifyCompoundPortableObjectProofs(compoundJson, INBOX_COMPOUND_PROOF_LIMITS, {
2696
- documentLoader: ctx.documentLoader,
2697
- keyCache,
2698
- meterProvider,
2699
- tracerProvider
2700
- });
2932
+ const compoundProof = await observeAttempt(signatureObservation, "objectIntegrity", async (verification) => {
2933
+ const result = await verifyCompoundPortableObjectProofs(compoundJson, INBOX_COMPOUND_PROOF_LIMITS, {
2934
+ [verificationObservation]: verification,
2935
+ documentLoader: ctx.documentLoader,
2936
+ keyCache,
2937
+ meterProvider,
2938
+ tracerProvider
2939
+ });
2940
+ if (result.status === "unsupported") verification.attempt.reason = {
2941
+ type: "proofPolicy",
2942
+ reason: result.reason
2943
+ };
2944
+ else if (!result.verified) {
2945
+ const failed = result.portableObjects.find((object) => !object.verified);
2946
+ verification.attempt.reason = {
2947
+ type: "proofPolicy",
2948
+ reason: failed?.verified === false ? failed.reason : { type: "invalidProof" }
2949
+ };
2950
+ }
2951
+ return result;
2952
+ }, (result) => result.status === "ok" && result.verified);
2701
2953
  if (compoundProof.status !== "ok" || !compoundProof.verified) {
2954
+ observation.authentication = {
2955
+ status: "rejected",
2956
+ reason: {
2957
+ type: "proofPolicy",
2958
+ policy: "compound"
2959
+ }
2960
+ };
2702
2961
  logger.error("Failed to verify compound portable Object Integrity Proofs.", {
2703
2962
  recipient,
2704
2963
  activity: json,
@@ -2717,11 +2976,20 @@ async function handleInboxInternal(request, parameters, span) {
2717
2976
  }
2718
2977
  if (pendingNonceLabel != null) {
2719
2978
  if (!await verifySignatureNonce(request, kv, kvPrefixes.acceptSignatureNonce, pendingNonceLabel)) {
2979
+ observation.authentication = {
2980
+ status: "rejected",
2981
+ reason: { type: "invalidNonce" }
2982
+ };
2720
2983
  getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("invalidNonce", httpSigKey?.id == null ? void 0 : getRemoteHost(httpSigKey.id));
2721
2984
  logger.error("Signature nonce verification failed (missing, expired, or replayed).", { recipient });
2722
2985
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
2723
2986
  }
2724
2987
  }
2988
+ observation.authentication = skipSignatureVerification ? { status: "skipped" } : {
2989
+ status: "verified",
2990
+ attempts: observation.verification.attempts.filter((attempt) => attempt.status === "verified")
2991
+ };
2992
+ observation.stage = "dispatch";
2725
2993
  const routeResult = await routeActivity({
2726
2994
  context: ctx,
2727
2995
  json,
@@ -2733,7 +3001,14 @@ async function handleInboxInternal(request, parameters, span) {
2733
3001
  inboxListeners,
2734
3002
  inboxContextFactory,
2735
3003
  listenerInboxContextFactory: ldSigVerified ? inboxContextFactory[rawInboxContextFactorySymbol] : void 0,
2736
- inboxErrorHandler,
3004
+ inboxErrorHandler: async (context, error) => {
3005
+ observation.result = {
3006
+ disposition: "failed",
3007
+ reason: "listenerError",
3008
+ error
3009
+ };
3010
+ await inboxErrorHandler?.(context, error);
3011
+ },
2737
3012
  kv,
2738
3013
  kvPrefixes,
2739
3014
  queue,
@@ -2769,30 +3044,44 @@ async function handleInboxInternal(request, parameters, span) {
2769
3044
  error
2770
3045
  });
2771
3046
  }
2772
- if (routeResult === "alreadyProcessed") return new Response(`Activity <${activity.id}> has already been processed.`, {
2773
- status: 202,
2774
- headers: { "Content-Type": "text/plain; charset=utf-8" }
2775
- });
2776
- else if (routeResult === "missingActor") return new Response("Missing actor.", {
2777
- status: 400,
2778
- headers: { "Content-Type": "text/plain; charset=utf-8" }
2779
- });
2780
- else if (routeResult === "enqueued") return new Response("Activity is enqueued.", {
2781
- status: 202,
2782
- headers: { "Content-Type": "text/plain; charset=utf-8" }
2783
- });
2784
- else if (routeResult === "unsupportedActivity") return new Response("", {
2785
- status: 202,
2786
- headers: { "Content-Type": "text/plain; charset=utf-8" }
2787
- });
2788
- else if (routeResult === "error") return new Response("Internal server error.", {
3047
+ if (routeResult === "alreadyProcessed") {
3048
+ observation.result = { disposition: "duplicate" };
3049
+ return new Response(`Activity <${activity.id}> has already been processed.`, {
3050
+ status: 202,
3051
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
3052
+ });
3053
+ } else if (routeResult === "missingActor") {
3054
+ observation.result = {
3055
+ disposition: "rejected",
3056
+ reason: "missingActor"
3057
+ };
3058
+ return new Response("Missing actor.", {
3059
+ status: 400,
3060
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
3061
+ });
3062
+ } else if (routeResult === "enqueued") {
3063
+ observation.result = { disposition: "enqueued" };
3064
+ return new Response("Activity is enqueued.", {
3065
+ status: 202,
3066
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
3067
+ });
3068
+ } else if (routeResult === "unsupportedActivity") {
3069
+ observation.result = { disposition: "unhandled" };
3070
+ return new Response("", {
3071
+ status: 202,
3072
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
3073
+ });
3074
+ } else if (routeResult === "error") return new Response("Internal server error.", {
2789
3075
  status: 500,
2790
3076
  headers: { "Content-Type": "text/plain; charset=utf-8" }
2791
3077
  });
2792
- else return new Response("", {
2793
- status: 202,
2794
- headers: { "Content-Type": "text/plain; charset=utf-8" }
2795
- });
3078
+ else {
3079
+ observation.result = { disposition: "processed" };
3080
+ return new Response("", {
3081
+ status: 202,
3082
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
3083
+ });
3084
+ }
2796
3085
  }
2797
3086
  const PORTABLE_INBOX_FORWARDABLE_RESULTS = /* @__PURE__ */ new Set([
2798
3087
  "success",
@@ -2921,8 +3210,8 @@ var CustomCollectionHandler = class {
2921
3210
  * @param CollectionPage The CollectionPage constructor.
2922
3211
  * @param filterPredicate Optional filter predicate for items.
2923
3212
  */
2924
- constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
2925
- this.name = name$2;
3213
+ constructor(name$1, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
3214
+ this.name = name$1;
2926
3215
  this.values = values;
2927
3216
  this.context = context;
2928
3217
  this.callbacks = callbacks;
@@ -3458,19 +3747,19 @@ function isPortableActivity(activity) {
3458
3747
  */
3459
3748
  async function verifyPortableActivityId(activityId, jsonLd, options) {
3460
3749
  const expectedId = getCanonicalPortableId(activityId);
3461
- if (expectedId == null) return "malformed portable ID";
3750
+ if (expectedId == null) return { type: "invalidObjectId" };
3462
3751
  let verification;
3463
3752
  try {
3464
3753
  verification = await verifyPortableObjectProofWithRoot(jsonLd, options);
3465
3754
  } catch (error) {
3466
- if (error instanceof InvalidPortableObjectIdError) return "malformed portable ID";
3755
+ if (error instanceof InvalidPortableObjectIdError) return { type: "invalidObjectId" };
3467
3756
  if (!isPermanentActivityParseError(error)) throw error;
3468
- return "malformed document";
3757
+ return { type: "invalidDocument" };
3469
3758
  }
3470
3759
  const { result, root } = verification;
3471
- if (!result.verified) return result.reason.type;
3760
+ if (!result.verified) return result.reason;
3472
3761
  const rootId = root?.["@id"];
3473
- if (typeof rootId !== "string" || getCanonicalPortableId(parseIri(rootId)) !== expectedId) return "the verified document has another ID";
3762
+ if (typeof rootId !== "string" || getCanonicalPortableId(parseIri(rootId)) !== expectedId) return { type: "subjectMismatch" };
3474
3763
  return null;
3475
3764
  }
3476
3765
  //#endregion
@@ -5950,7 +6239,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5950
6239
  return await withContext({
5951
6240
  traceId: spanCtx.traceId,
5952
6241
  spanId: spanCtx.spanId
5953
- }, async () => {
6242
+ }, () => this.#observeInboxFetch(metricState, async () => {
5954
6243
  const logger = getLogger([
5955
6244
  "fedify",
5956
6245
  "federation",
@@ -5965,8 +6254,14 @@ var FederationImpl = class extends FederationBuilderImpl {
5965
6254
  tracer,
5966
6255
  metricState
5967
6256
  });
6257
+ if (metricState.inboxCompletion != null) metricState.inboxCompletion.observation.stage = "respond";
5968
6258
  if (metricState.endpoint !== "hashlink_media" && acceptsJsonLd(request)) response.headers.set("Vary", "Accept");
5969
6259
  } catch (error) {
6260
+ metricState.inboxCompletion?.observation.project(span, {
6261
+ type: "exception",
6262
+ stage: metricState.inboxCompletion.observation.stage,
6263
+ error
6264
+ });
5970
6265
  this.metrics.recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), { routeTemplate: metricState.routeTemplate });
5971
6266
  span.setStatus({
5972
6267
  code: SpanStatusCode.ERROR,
@@ -6005,7 +6300,7 @@ var FederationImpl = class extends FederationBuilderImpl {
6005
6300
  else if (response.status >= 400) logger.warn(logTpl, values);
6006
6301
  else logger.info(logTpl, values);
6007
6302
  return response;
6008
- });
6303
+ }));
6009
6304
  });
6010
6305
  });
6011
6306
  }
@@ -6047,8 +6342,17 @@ var FederationImpl = class extends FederationBuilderImpl {
6047
6342
  metricState.routeTemplate = route.template;
6048
6343
  metricState.endpoint = getEndpointCategory(route.name);
6049
6344
  span.updateName(`${request.method} ${route.template}`);
6050
- let context = this.#createContext(request, contextData);
6051
6345
  const routeName = route.name.replace(/:.*$/, "");
6346
+ let context;
6347
+ try {
6348
+ context = this.#createContext(request, contextData);
6349
+ } catch (error) {
6350
+ if (request.method !== "POST" || routeName !== "inbox" && routeName !== "sharedInbox") throw error;
6351
+ return await this.#reportInboxPreparationError(request, contextData, {
6352
+ kind: routeName === "inbox" ? "personal" : "shared",
6353
+ recipient: route.values.identifier ?? null
6354
+ }, error, span, metricState);
6355
+ }
6052
6356
  switch (routeName) {
6053
6357
  case "webfinger": return await handleWebFinger(request, {
6054
6358
  context,
@@ -6107,6 +6411,28 @@ var FederationImpl = class extends FederationBuilderImpl {
6107
6411
  });
6108
6412
  return response;
6109
6413
  }
6414
+ if (request.method === "POST" && (routeName === "inbox" || routeName === "sharedInbox")) {
6415
+ const recipient = route.values.identifier ?? null;
6416
+ const observation = new InboxObservation({
6417
+ kind: routeName === "inbox" ? "personal" : "shared",
6418
+ recipient
6419
+ });
6420
+ this.#setInboxCompletion(metricState, observation, () => context);
6421
+ return await observation.run(async () => {
6422
+ if (routeName === "inbox") context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: recipient }) });
6423
+ else if (this.sharedInboxKeyDispatcher != null) {
6424
+ const identity = await this.sharedInboxKeyDispatcher(context);
6425
+ if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
6426
+ }
6427
+ return await this.#handleInbox(request, {
6428
+ recipient,
6429
+ context,
6430
+ contextData,
6431
+ onNotFound,
6432
+ observation
6433
+ });
6434
+ }, span);
6435
+ }
6110
6436
  switch (routeName) {
6111
6437
  case "actor":
6112
6438
  case "actorAlias": {
@@ -6182,9 +6508,9 @@ var FederationImpl = class extends FederationBuilderImpl {
6182
6508
  onUnauthorized,
6183
6509
  onNotFound
6184
6510
  });
6185
- context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
6511
+ throw new Error("Unreachable inbox delivery.");
6186
6512
  case "sharedInbox":
6187
- if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
6513
+ if (this.sharedInboxKeyDispatcher != null) {
6188
6514
  const identity = await this.sharedInboxKeyDispatcher(context);
6189
6515
  if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
6190
6516
  }
@@ -6477,7 +6803,49 @@ var FederationImpl = class extends FederationBuilderImpl {
6477
6803
  }
6478
6804
  return await onNotFound(request);
6479
6805
  }
6480
- async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox }) {
6806
+ #setInboxCompletion(metricState, observation, getContext) {
6807
+ const handler = this.inboxRequestFinishedHandler;
6808
+ metricState.inboxCompletion = {
6809
+ observation,
6810
+ finish: (outcome) => observation.finish(getContext(), handler, outcome)
6811
+ };
6812
+ }
6813
+ async #observeInboxFetch(metricState, operation) {
6814
+ let response;
6815
+ try {
6816
+ response = await operation();
6817
+ } catch (error) {
6818
+ const completion = metricState.inboxCompletion;
6819
+ if (completion != null) await completion.finish({
6820
+ type: "exception",
6821
+ stage: completion.observation.stage,
6822
+ error
6823
+ });
6824
+ throw error;
6825
+ }
6826
+ const completion = metricState.inboxCompletion;
6827
+ if (completion != null) await completion.finish({
6828
+ type: "response",
6829
+ status: response.status,
6830
+ ...completion.observation.result
6831
+ });
6832
+ return response;
6833
+ }
6834
+ async #reportInboxPreparationError(request, contextData, inbox, error, span, metricState) {
6835
+ const unavailableLoader = () => Promise.reject(error);
6836
+ const context = new RequestContextImpl({
6837
+ url: new URL(request.url),
6838
+ request,
6839
+ federation: this,
6840
+ data: contextData,
6841
+ documentLoader: unavailableLoader,
6842
+ contextLoader: unavailableLoader
6843
+ });
6844
+ const observation = new InboxObservation(inbox);
6845
+ this.#setInboxCompletion(metricState, observation, () => context);
6846
+ return await observation.run(() => Promise.reject(error), span);
6847
+ }
6848
+ async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox, observation }) {
6481
6849
  if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
6482
6850
  const inboxContextFactory = context.toInboxContext.bind(context);
6483
6851
  inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
@@ -6501,7 +6869,8 @@ var FederationImpl = class extends FederationBuilderImpl {
6501
6869
  meterProvider: this.meterProvider,
6502
6870
  tracerProvider: this.tracerProvider,
6503
6871
  idempotencyStrategy: this.idempotencyStrategy,
6504
- portableInbox
6872
+ portableInbox,
6873
+ observation
6505
6874
  });
6506
6875
  }
6507
6876
  async #fetchPortableInbox(request, portable, { onNotFound, contextData, span, metricState }) {
@@ -6530,63 +6899,79 @@ var FederationImpl = class extends FederationBuilderImpl {
6530
6899
  metricState.endpoint = "inbox";
6531
6900
  span.updateName(`${request.method} ${metricState.routeTemplate}`);
6532
6901
  const identifier = route.values.identifier;
6533
- let context = this.#createContext(request, contextData);
6534
- const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
6535
- const localOrigin = context.canonicalOrigin;
6536
- const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof Tombstone ? null : actor, {
6537
- authority: portable.portableRequest.authority,
6538
- canonicalInboxId: portable.canonicalId,
6539
- localOrigin
6540
- });
6541
- if (resolution.status === "rejected") {
6542
- logger.debug("Not accepting a delivery to the portable inbox {inbox} on behalf of the actor {identifier}: {reason}.", {
6543
- inbox: portable.canonicalId,
6544
- identifier,
6545
- reason: resolution.reason
6546
- });
6547
- return await onNotFound(request);
6902
+ let context;
6903
+ try {
6904
+ context = this.#createContext(request, contextData);
6905
+ } catch (error) {
6906
+ return await this.#reportInboxPreparationError(request, contextData, {
6907
+ kind: "portable",
6908
+ recipient: identifier
6909
+ }, error, span, metricState);
6548
6910
  }
6549
- const { recipient } = resolution;
6550
- if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
6551
- const excludedOrigins = [localOrigin, new URL(request.url).origin];
6552
- return await this.#handleInbox(request, {
6553
- recipient: identifier,
6554
- context,
6555
- contextData,
6556
- onNotFound,
6557
- portableInbox: {
6558
- recipient,
6559
- forward: async (activity, activityId, activityType, signatures) => {
6560
- if (this.portableInboxForwarding.maxTargets < 1) return;
6561
- if (this.kv.cas == null) {
6562
- if (!this.#portableInboxForwardingWarned) {
6563
- this.#portableInboxForwardingWarned = true;
6564
- 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.");
6911
+ const observation = new InboxObservation({
6912
+ kind: "portable",
6913
+ recipient: identifier
6914
+ });
6915
+ this.#setInboxCompletion(metricState, observation, () => context);
6916
+ return await observation.run(async () => {
6917
+ const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
6918
+ const localOrigin = context.canonicalOrigin;
6919
+ const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof Tombstone ? null : actor, {
6920
+ authority: portable.portableRequest.authority,
6921
+ canonicalInboxId: portable.canonicalId,
6922
+ localOrigin
6923
+ });
6924
+ if (resolution.status === "rejected") {
6925
+ logger.debug("Not accepting a delivery to the portable inbox {inbox} on behalf of the actor {identifier}: {reason}.", {
6926
+ inbox: portable.canonicalId,
6927
+ identifier,
6928
+ reason: resolution.reason
6929
+ });
6930
+ return await onNotFound(request);
6931
+ }
6932
+ const { recipient } = resolution;
6933
+ if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
6934
+ const excludedOrigins = [localOrigin, new URL(request.url).origin];
6935
+ return await this.#handleInbox(request, {
6936
+ recipient: identifier,
6937
+ observation,
6938
+ context,
6939
+ contextData,
6940
+ onNotFound,
6941
+ portableInbox: {
6942
+ recipient,
6943
+ forward: async (activity, activityId, activityType, signatures) => {
6944
+ if (this.portableInboxForwarding.maxTargets < 1) return;
6945
+ if (this.kv.cas == null) {
6946
+ if (!this.#portableInboxForwardingWarned) {
6947
+ this.#portableInboxForwardingWarned = true;
6948
+ 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.");
6949
+ }
6950
+ return;
6565
6951
  }
6566
- return;
6952
+ await forwardPortableInboxActivity({
6953
+ recipient,
6954
+ activity,
6955
+ activityId,
6956
+ activityType,
6957
+ excludedOrigins,
6958
+ baseUrl: context.origin,
6959
+ kv: this.kv,
6960
+ kvPrefix: this.kvPrefixes.portableInboxForwarding,
6961
+ outboxQueue: this.outboxQueue,
6962
+ startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
6963
+ allowPrivateAddress: this.allowPrivateAddress,
6964
+ getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
6965
+ specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
6966
+ signatures,
6967
+ options: this.portableInboxForwarding,
6968
+ meterProvider: this.meterProvider,
6969
+ tracerProvider: this.tracerProvider
6970
+ });
6567
6971
  }
6568
- await forwardPortableInboxActivity({
6569
- recipient,
6570
- activity,
6571
- activityId,
6572
- activityType,
6573
- excludedOrigins,
6574
- baseUrl: context.origin,
6575
- kv: this.kv,
6576
- kvPrefix: this.kvPrefixes.portableInboxForwarding,
6577
- outboxQueue: this.outboxQueue,
6578
- startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
6579
- allowPrivateAddress: this.allowPrivateAddress,
6580
- getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
6581
- specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
6582
- signatures,
6583
- options: this.portableInboxForwarding,
6584
- meterProvider: this.meterProvider,
6585
- tracerProvider: this.tracerProvider
6586
- });
6587
6972
  }
6588
- }
6589
- });
6973
+ });
6974
+ }, span);
6590
6975
  }
6591
6976
  /**
6592
6977
  * Gets this server's gateway key pairs for the portable actor that owns