@fedify/fedify 2.4.0-dev.2253 → 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 (110) hide show
  1. package/dist/{builder-DdUWFCwL.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-CcWDD5VB.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-D9uhdIuD.mjs → deno-C8XaXmPB.mjs} +1 -1
  9. package/dist/{docloader-fbmPhCdS.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-B858-Ylk.cjs → federation-D6giSGUT.cjs} +524 -118
  40. package/dist/{http-CUyBaVsQ.mjs → http-BqSbUEiQ.mjs} +141 -98
  41. package/dist/{http-Bp0MKAK3.cjs → http-Cki-npuA.cjs} +456 -116
  42. package/dist/{http-BoGE-nk7.js → http-OMtKlbx1.js} +439 -117
  43. package/dist/{http-DGx0KOws.d.ts → key-C1nv7_jD.d.ts} +638 -229
  44. package/dist/{http-BrwKINQn.d.cts → key-DYxuyd-s.d.cts} +638 -229
  45. package/dist/{key-BlTaKABr.mjs → key-DecAtABm.mjs} +30 -3
  46. package/dist/{kv-cache-Dmqd7s2a.mjs → kv-cache-CWWlMv-s.mjs} +1 -1
  47. package/dist/{kv-cache-BnWemxH2.cjs → kv-cache-D73yVcE5.cjs} +1 -1
  48. package/dist/{kv-cache-DhMzUXEE.js → kv-cache-J0v3UbUn.js} +1 -1
  49. package/dist/{ld-Dx8r_qJU.mjs → ld-UsN-TIPh.mjs} +40 -5
  50. package/dist/{metrics-DFkZkLrO.mjs → metrics-DWCn4VXX.mjs} +2 -1
  51. package/dist/{middleware-DC018B4u.js → middleware-BqzfIlUO.js} +524 -118
  52. package/dist/{middleware-B0RRI88k.mjs → middleware-CkjwiEBd.mjs} +512 -127
  53. package/dist/{middleware-BNhF-Ayx.mjs → middleware-DHn2q0L1.mjs} +1 -1
  54. package/dist/{mod-C7_2j7VT.d.cts → mod-BojP2Bw3.d.cts} +1 -1
  55. package/dist/{mod-Cy9VNHVw.d.ts → mod-CavhAgLw.d.cts} +5 -253
  56. package/dist/{mod-DoTWIc7F.d.cts → mod-CtpF2BU5.d.ts} +5 -253
  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-B61jjp5K.mjs → owner-CEBn81LD.mjs} +3 -3
  66. package/dist/{portable-collection-DhEA_nWT.cjs → portable-collection-DqTUBKY7.cjs} +1 -1
  67. package/dist/{portable-collection-BnktjQ04.js → portable-collection-ZkUNjtb7.js} +1 -1
  68. package/dist/{portable-collection-CyBJDEzs.mjs → portable-collection-qFSYw4TG.mjs} +2 -2
  69. package/dist/{portable-key-CPN0QZSA.mjs → portable-key-LlMkP-vz.mjs} +1 -1
  70. package/dist/{proof-CZrjvoJ3.mjs → proof-BHRf_jMG.mjs} +142 -22
  71. package/dist/{send-enxOnmZo.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.d.mts +2 -0
  80. package/dist/sig/mitra-receive-interop.test.mjs +260 -0
  81. package/dist/sig/mitra-send-interop.test.d.mts +2 -0
  82. package/dist/sig/mitra-send-interop.test.mjs +461 -0
  83. package/dist/sig/mod.cjs +2 -2
  84. package/dist/sig/mod.d.cts +2 -2
  85. package/dist/sig/mod.d.ts +2 -2
  86. package/dist/sig/mod.js +2 -2
  87. package/dist/sig/owner.test.mjs +1 -1
  88. package/dist/sig/portable-collection.test.mjs +1 -1
  89. package/dist/sig/portable-interop.test.d.mts +2 -0
  90. package/dist/sig/portable-interop.test.mjs +156 -0
  91. package/dist/sig/portable-key.test.mjs +6 -6
  92. package/dist/sig/proof.test.mjs +161 -3
  93. package/dist/sig/tootik-request-interop.test.d.mts +2 -0
  94. package/dist/sig/tootik-request-interop.test.mjs +190 -0
  95. package/dist/sig/tootik-send-interop.test.d.mts +2 -0
  96. package/dist/sig/tootik-send-interop.test.mjs +331 -0
  97. package/dist/sig/verification.test.d.mts +2 -0
  98. package/dist/sig/verification.test.mjs +310 -0
  99. package/dist/{temporal-CBVv0xRB.mjs → temporal-BRFH_dte.mjs} +1 -1
  100. package/dist/testing/kv.test.mjs +1 -1
  101. package/dist/testing/mod.d.mts +315 -28
  102. package/dist/utils/docloader.test.mjs +2 -2
  103. package/dist/utils/kv-cache.test.mjs +1 -1
  104. package/dist/utils/mod.cjs +1 -1
  105. package/dist/utils/mod.d.cts +1 -1
  106. package/dist/utils/mod.d.ts +1 -1
  107. package/dist/utils/mod.js +1 -1
  108. package/dist/verification-C3954hQh.mjs +5 -0
  109. package/dist/verification-Dpfcw8ml.mjs +105 -0
  110. package/package.json +6 -6
@@ -2,10 +2,10 @@ import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  import { t as __exportAll } from "./rolldown-runtime-BC-Eo5Mj.js";
4
4
  import { r as getDefaultActivityTransformers } from "./transformers-DAtXl_Kc.js";
5
- import { $ as instrumentDocumentLoader, A as hasSignature, At as name, C as InvalidContextReferenceError, Ct as hasPortableActor, Dt as isPortableUri, E as compactJsonLd, Et as isPortableKeyId, F as verifyCompactJsonLd, G as validateCryptoKey, M as isClearlyMalformedContextReference, N as isInvalidUrlTypeError, O as detachSignature, Ot as isSameObjectId, P as signJsonLd, Q as getRemoteHost, R as wrapContextLoaderForJsonLd, S as normalizeOutgoingActivityJsonLd, St as getRawCanonicalPortableId, Tt as isPortableId, W as importJwk, X as getDurationMs, Z as getFederationMetrics, _ as verifyObject, at as recordCollectionTotalItems, b as verifyPortableObjectProofWithRoot, bt as getGatewayKeyBase, c as verifyRequestDetailed, ct as recordInboxActivity, d as InvalidPortableObjectIdError, dt as recordWebFingerHandle, et as isAbortError, f as classifyFep2277CoreType, ft as registerQueueDepthGauge, g as verifyMapLocalProof, h as signObject, i as selectRequestSignature, it as recordCollectionRequest, j as hasSignatureLike, jt as version, k as getNormalizationContextLoader, lt as recordOutboxActivity, m as hasProofLike, mt as formatAcceptSignature, n as listRequestSignatures, nt as recordCollectionDispatchDuration, o as validateMaxSignatures, pt as preloadedOnlyDocumentLoader, q as MAX_PORTABLE_KEY_TTL, r as parseRfc9421SignatureInput, rt as recordCollectionPageItems, s as verifyRequest, st as recordFanoutRecipients, t as doubleKnock, tt as recordCircuitBreakerStateChange, ut as recordOutboxEnqueue, v as verifyPortableObjectProof, vt as getAuthenticationOrigin, w as assertSafeJsonLd, wt as isCompatibleKeyId, xt as getPortableDid, y as verifyPortableObjectProofPolicy, yt as getCanonicalPortableId, z as exportJwk } from "./http-BoGE-nk7.js";
5
+ import { $ as instrumentDocumentLoader, A as hasSignature, At as isPortableUri, C as InvalidContextReferenceError, Ct as getGatewayKeyBase, Dt as isCompatibleKeyId, E as compactJsonLd, Et as hasPortableActor, F as verifyCompactJsonLd, G as validateCryptoKey, M as isClearlyMalformedContextReference, N as isInvalidUrlTypeError, Nt as name, O as detachSignature, Ot as isPortableId, P as signJsonLd, Pt as version, Q as getRemoteHost, R as wrapContextLoaderForJsonLd, S as normalizeOutgoingActivityJsonLd, St as getCanonicalPortableId, Tt as getRawCanonicalPortableId, W as importJwk, X as getDurationMs, Z as getFederationMetrics, _ as verifyObject, at as recordCollectionTotalItems, b as verifyPortableObjectProofWithRoot, bt as verificationObservation, c as verifyRequestDetailed, ct as recordInboxActivity, d as InvalidPortableObjectIdError, dt as recordWebFingerHandle, et as isAbortError, f as classifyFep2277CoreType, ft as registerQueueDepthGauge, g as verifyMapLocalProof, h as signObject, i as selectRequestSignature, it as recordCollectionRequest, j as hasSignatureLike, jt as isSameObjectId, k as getNormalizationContextLoader, kt as isPortableKeyId, lt as recordOutboxActivity, m as hasProofLike, mt as formatAcceptSignature, n as listRequestSignatures, nt as recordCollectionDispatchDuration, o as validateMaxSignatures, pt as preloadedOnlyDocumentLoader, q as MAX_PORTABLE_KEY_TTL, r as parseRfc9421SignatureInput, rt as recordCollectionPageItems, s as verifyRequest, st as recordFanoutRecipients, t as doubleKnock, tt as recordCircuitBreakerStateChange, ut as recordOutboxEnqueue, v as verifyPortableObjectProof, vt as observeAttempt, w as assertSafeJsonLd, wt as getPortableDid, xt as getAuthenticationOrigin, y as verifyPortableObjectProofPolicy, yt as snapshotKey, z as exportJwk } from "./http-OMtKlbx1.js";
6
6
  import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-_S2_MeLF.js";
7
- import { n as doesActorOwnKey, r as getKeyOwner, t as verifyPortableObject } from "./portable-collection-BnktjQ04.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DhMzUXEE.js";
7
+ import { n as doesActorOwnKey, r as getKeyOwner, t as verifyPortableObject } from "./portable-collection-ZkUNjtb7.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-J0v3UbUn.js";
9
9
  import { getLogger, withContext } from "@logtape/logtape";
10
10
  import { Router, RouterError, assertPath } from "@fedify/uri-template";
11
11
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, PUBLIC_COLLECTION, Tombstone, getTypeId, isActor, lookupObject, traverseCollection } from "@fedify/vocab";
@@ -100,6 +100,7 @@ var FederationBuilderImpl = class {
100
100
  featuredTagsCallbacks;
101
101
  inboxListeners;
102
102
  outboxListeners;
103
+ inboxRequestFinishedHandler;
103
104
  inboxErrorHandler;
104
105
  outboxListenerErrorHandler;
105
106
  outboxAuthorizePredicate;
@@ -161,6 +162,7 @@ var FederationBuilderImpl = class {
161
162
  f.featuredTagsCallbacks = this.featuredTagsCallbacks == null ? void 0 : { ...this.featuredTagsCallbacks };
162
163
  f.inboxListeners = this.inboxListeners?.clone();
163
164
  f.outboxListeners = this.outboxListeners?.clone();
165
+ f.inboxRequestFinishedHandler = this.inboxRequestFinishedHandler;
164
166
  f.inboxErrorHandler = this.inboxErrorHandler;
165
167
  f.outboxListenerErrorHandler = this.outboxListenerErrorHandler;
166
168
  f.outboxAuthorizePredicate = this.outboxAuthorizePredicate;
@@ -641,6 +643,10 @@ var FederationBuilderImpl = class {
641
643
  this.inboxErrorHandler = handler;
642
644
  return setters;
643
645
  },
646
+ onRequestFinished: (handler) => {
647
+ this.inboxRequestFinishedHandler = handler;
648
+ return setters;
649
+ },
644
650
  onUnverifiedActivity: (handler) => {
645
651
  this.unverifiedActivityHandler = handler;
646
652
  return setters;
@@ -1446,6 +1452,111 @@ async function buildCollectionSynchronizationHeader(collectionId, actorIds) {
1446
1452
  return `collectionId="${collectionId}", url="${url}", digest="${encodeHex(await digest(actorIds))}"`;
1447
1453
  }
1448
1454
  //#endregion
1455
+ //#region src/federation/inbox-observation.ts
1456
+ /** Internal state owned by the ingress boundary, never serialized into queues. */
1457
+ var InboxObservation = class {
1458
+ inbox;
1459
+ payload = { status: "unavailable" };
1460
+ activity = null;
1461
+ authentication = { status: "notDetermined" };
1462
+ hasException = false;
1463
+ stage = "prepare";
1464
+ result = {
1465
+ disposition: "rejected",
1466
+ reason: "recipientNotFound"
1467
+ };
1468
+ verification = {
1469
+ attempts: [],
1470
+ subject: {
1471
+ id: null,
1472
+ pointer: ""
1473
+ },
1474
+ parsedObject: (object) => {
1475
+ if (object instanceof Activity) {
1476
+ this.activity = object;
1477
+ this.verification.subject.id = object.id == null ? null : new URL(object.id.href);
1478
+ }
1479
+ }
1480
+ };
1481
+ constructor(inbox) {
1482
+ this.inbox = inbox;
1483
+ }
1484
+ /** Run an ingress operation and project telemetry; fetch owns completion. */
1485
+ async run(operation, span) {
1486
+ let response;
1487
+ let outcome;
1488
+ let threw = false;
1489
+ let originalError;
1490
+ try {
1491
+ response = await operation();
1492
+ outcome = {
1493
+ type: "response",
1494
+ status: response.status,
1495
+ ...this.result
1496
+ };
1497
+ } catch (error) {
1498
+ threw = true;
1499
+ this.hasException = true;
1500
+ originalError = error;
1501
+ outcome = {
1502
+ type: "exception",
1503
+ stage: this.stage,
1504
+ error
1505
+ };
1506
+ }
1507
+ if (span != null) this.project(span, outcome);
1508
+ if (threw) throw originalError;
1509
+ return response;
1510
+ }
1511
+ /** Complete standalone handler calls that have no enclosing fetch boundary. */
1512
+ async runAndFinish(getContext, handler, operation) {
1513
+ let response;
1514
+ try {
1515
+ response = await this.run(operation);
1516
+ } catch (error) {
1517
+ await this.finish(getContext(), handler, {
1518
+ type: "exception",
1519
+ stage: this.stage,
1520
+ error
1521
+ });
1522
+ throw error;
1523
+ }
1524
+ await this.finish(getContext(), handler, {
1525
+ type: "response",
1526
+ status: response.status,
1527
+ ...this.result
1528
+ });
1529
+ return response;
1530
+ }
1531
+ project(span, outcome) {
1532
+ span.setAttribute("activitypub.authentication.status", this.authentication.status);
1533
+ if (this.authentication.status === "rejected") span.setAttribute("activitypub.verification.failure_reason", this.authentication.reason.type);
1534
+ span.setAttribute("activitypub.inbox.disposition", outcome?.type === "exception" || this.hasException ? "exception" : this.result.disposition);
1535
+ }
1536
+ async finish(context, handler, outcome) {
1537
+ if (handler == null) return;
1538
+ const report = {
1539
+ inbox: this.inbox,
1540
+ payload: this.payload,
1541
+ activity: this.activity,
1542
+ attempts: this.verification.attempts,
1543
+ authentication: this.authentication,
1544
+ outcome
1545
+ };
1546
+ try {
1547
+ await handler(context, report);
1548
+ } catch (error) {
1549
+ try {
1550
+ getLogger([
1551
+ "fedify",
1552
+ "federation",
1553
+ "inbox"
1554
+ ]).error("An unexpected error occurred in inbox request finished handler:\n{error}", { error });
1555
+ } catch {}
1556
+ }
1557
+ }
1558
+ };
1559
+ //#endregion
1449
1560
  //#region src/sig/compound-proof.ts
1450
1561
  const textEncoder = new TextEncoder();
1451
1562
  /**
@@ -1934,7 +2045,23 @@ async function verifyDiscoveredProofDocuments(discovered, options) {
1934
2045
  verified: false,
1935
2046
  reason: { type: "missingContext" }
1936
2047
  });
1937
- const key = await verifyMapLocalProof(document.securedDocument, options);
2048
+ const inherited = options[verificationObservation];
2049
+ let subjectId = null;
2050
+ if (inherited != null && document.id != null) try {
2051
+ subjectId = parseIri(document.id);
2052
+ } catch {}
2053
+ const key = await observeAttempt({ [verificationObservation]: inherited == null ? void 0 : {
2054
+ ...inherited,
2055
+ attempt: void 0,
2056
+ check: void 0,
2057
+ subject: {
2058
+ id: subjectId,
2059
+ pointer: document.path
2060
+ }
2061
+ } }, "objectIntegrity", (observation) => verifyMapLocalProof(document.securedDocument, {
2062
+ ...options,
2063
+ [verificationObservation]: inherited == null ? void 0 : observation
2064
+ }), (key) => key != null);
1938
2065
  return key == null ? Object.freeze({
1939
2066
  ...metadata,
1940
2067
  verified: false,
@@ -4573,13 +4700,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
4573
4700
  * @param parameters The parameters for handling the collection.
4574
4701
  * @returns A promise that resolves to an HTTP response.
4575
4702
  */
4576
- async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, portable, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
4577
- const spanName = name$1.trim().replace(/\s+/g, "_");
4703
+ async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, portable, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
4704
+ const spanName = name$2.trim().replace(/\s+/g, "_");
4578
4705
  tracerProvider = tracerProvider ?? trace.getTracerProvider();
4579
4706
  const tracer = tracerProvider.getTracer(name, version);
4580
4707
  const cursor = new URL(request.url).searchParams.get("cursor");
4581
4708
  const metricBase = {
4582
- kind: getCollectionMetricKind(name$1),
4709
+ kind: getCollectionMetricKind(name$2),
4583
4710
  page: cursor != null,
4584
4711
  dispatcher: "built_in"
4585
4712
  };
@@ -4625,7 +4752,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
4625
4752
  span.setStatus({ code: SpanStatusCode.ERROR });
4626
4753
  return await onNotFound(request);
4627
4754
  }
4628
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
4755
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
4629
4756
  itemCount = items.length;
4630
4757
  span.setAttribute("fedify.collection.items", itemCount);
4631
4758
  return items;
@@ -4667,7 +4794,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
4667
4794
  } else {
4668
4795
  const uri = new URL(portable == null ? baseUri : pageBase);
4669
4796
  uri.searchParams.set("cursor", cursor);
4670
- const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
4797
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
4671
4798
  kind: SpanKind.SERVER,
4672
4799
  attributes: {
4673
4800
  "activitypub.collection.id": uri.href,
@@ -4683,7 +4810,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
4683
4810
  span.setStatus({ code: SpanStatusCode.ERROR });
4684
4811
  return await onNotFound(request);
4685
4812
  }
4686
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
4813
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
4687
4814
  itemCount = items.length;
4688
4815
  span.setAttribute("fedify.collection.items", itemCount);
4689
4816
  return {
@@ -5164,6 +5291,16 @@ async function handleMediaUpload(request, { identifier, context: ctx, mediaUploa
5164
5291
  * @returns A promise that resolves to an HTTP response.
5165
5292
  */
5166
5293
  async function handleInbox(request, options) {
5294
+ if (options.observation == null) {
5295
+ const observation = new InboxObservation({
5296
+ kind: options.portableInbox != null ? "portable" : options.recipient == null ? "shared" : "personal",
5297
+ recipient: options.recipient
5298
+ });
5299
+ return await observation.runAndFinish(() => options.context, options.inboxRequestFinishedHandler, () => handleInbox(request, {
5300
+ ...options,
5301
+ observation
5302
+ }));
5303
+ }
5167
5304
  return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("activitypub.inbox", {
5168
5305
  kind: options.queue == null ? SpanKind.SERVER : SpanKind.PRODUCER,
5169
5306
  attributes: { "activitypub.shared_inbox": options.recipient == null }
@@ -5172,12 +5309,14 @@ async function handleInbox(request, options) {
5172
5309
  try {
5173
5310
  return await handleInboxInternal(request, options, span);
5174
5311
  } catch (e) {
5312
+ options.observation.hasException = true;
5175
5313
  span.setStatus({
5176
5314
  code: SpanStatusCode.ERROR,
5177
5315
  message: String(e)
5178
5316
  });
5179
5317
  throw e;
5180
5318
  } finally {
5319
+ options.observation.project(span);
5181
5320
  span.end();
5182
5321
  }
5183
5322
  });
@@ -5192,6 +5331,8 @@ async function handleInbox(request, options) {
5192
5331
  */
5193
5332
  async function handleInboxInternal(request, parameters, span) {
5194
5333
  const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, maxHttpSignatures, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider, portableInbox } = parameters;
5334
+ const observation = parameters.observation;
5335
+ const signatureObservation = { [verificationObservation]: observation.verification };
5195
5336
  const logger = getLogger([
5196
5337
  "fedify",
5197
5338
  "federation",
@@ -5216,6 +5357,10 @@ async function handleInboxInternal(request, parameters, span) {
5216
5357
  }
5217
5358
  }
5218
5359
  if (request.bodyUsed) {
5360
+ observation.result = {
5361
+ disposition: "failed",
5362
+ reason: "bodyUnavailable"
5363
+ };
5219
5364
  logger.error("Request body has already been read.", { recipient });
5220
5365
  span.setStatus({
5221
5366
  code: SpanStatusCode.ERROR,
@@ -5226,6 +5371,10 @@ async function handleInboxInternal(request, parameters, span) {
5226
5371
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5227
5372
  });
5228
5373
  } else if (request.body?.locked) {
5374
+ observation.result = {
5375
+ disposition: "failed",
5376
+ reason: "bodyUnavailable"
5377
+ };
5229
5378
  logger.error("Request body is locked.", { recipient });
5230
5379
  span.setStatus({
5231
5380
  code: SpanStatusCode.ERROR,
@@ -5236,11 +5385,20 @@ async function handleInboxInternal(request, parameters, span) {
5236
5385
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5237
5386
  });
5238
5387
  }
5388
+ observation.stage = "parse";
5389
+ observation.result = {
5390
+ disposition: "rejected",
5391
+ reason: "invalidJson"
5392
+ };
5239
5393
  let json;
5240
5394
  try {
5241
5395
  json = JSON.parse(await readBoundedText(request.clone(), MAX_BODY_SIZE, request.url));
5242
5396
  } catch (error) {
5243
5397
  if (error instanceof BodyTooLargeError) {
5398
+ observation.result = {
5399
+ disposition: "rejected",
5400
+ reason: "bodyTooLarge"
5401
+ };
5244
5402
  request.body?.cancel(error).catch(() => {});
5245
5403
  span.setStatus({
5246
5404
  code: SpanStatusCode.ERROR,
@@ -5270,6 +5428,15 @@ async function handleInboxInternal(request, parameters, span) {
5270
5428
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5271
5429
  });
5272
5430
  }
5431
+ observation.payload = {
5432
+ status: "parsed",
5433
+ value: structuredClone(json)
5434
+ };
5435
+ observation.result = {
5436
+ disposition: "rejected",
5437
+ reason: "invalidActivity"
5438
+ };
5439
+ observation.stage = "verify";
5273
5440
  const keyCache = new KvKeyCache(kv, kvPrefixes.publicKey, {
5274
5441
  documentLoader: ctx.documentLoader,
5275
5442
  contextLoader: ctx.contextLoader,
@@ -5299,6 +5466,10 @@ async function handleInboxInternal(request, parameters, span) {
5299
5466
  code: SpanStatusCode.ERROR,
5300
5467
  message: `Failed to parse activity:\n${error}`
5301
5468
  });
5469
+ observation.result = {
5470
+ disposition: "rejected",
5471
+ reason: "invalidActivity"
5472
+ };
5302
5473
  return new Response("Invalid activity.", {
5303
5474
  status: 400,
5304
5475
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5316,11 +5487,25 @@ async function handleInboxInternal(request, parameters, span) {
5316
5487
  recipient,
5317
5488
  error
5318
5489
  });
5490
+ observation.result = {
5491
+ disposition: "rejected",
5492
+ reason: "invalidJsonLd"
5493
+ };
5319
5494
  return new Response("Invalid JSON-LD.", {
5320
5495
  status: 400,
5321
5496
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5322
5497
  });
5323
5498
  }
5499
+ observation.verification.attempts.push({
5500
+ mechanism: "linkedData",
5501
+ subject: {
5502
+ id: null,
5503
+ pointer: ""
5504
+ },
5505
+ checks: [],
5506
+ status: "error",
5507
+ error
5508
+ });
5324
5509
  if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
5325
5510
  if (!skipSignatureVerification) deferredLdSignatureError = error;
5326
5511
  logger.debug("Failed to normalize JSON-LD for Linked Data Signatures; deferring to another authentication path only if it verifies:\n{error}", {
@@ -5332,6 +5517,7 @@ async function handleInboxInternal(request, parameters, span) {
5332
5517
  compactedJsonWithoutSig = detachSignature(compactedJson);
5333
5518
  try {
5334
5519
  ldSigVerified = await verifyCompactJsonLd(compactedJson, {
5520
+ ...signatureObservation,
5335
5521
  contextLoader: ctx.contextLoader,
5336
5522
  documentLoader: ctx.documentLoader,
5337
5523
  keyCache,
@@ -5345,6 +5531,10 @@ async function handleInboxInternal(request, parameters, span) {
5345
5531
  recipient,
5346
5532
  error
5347
5533
  });
5534
+ observation.result = {
5535
+ disposition: "rejected",
5536
+ reason: "invalidJsonLd"
5537
+ };
5348
5538
  return new Response("Invalid JSON-LD.", {
5349
5539
  status: 400,
5350
5540
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5364,6 +5554,10 @@ async function handleInboxInternal(request, parameters, span) {
5364
5554
  recipient,
5365
5555
  error: parseError
5366
5556
  });
5557
+ observation.result = {
5558
+ disposition: "rejected",
5559
+ reason: "invalidJsonLd"
5560
+ };
5367
5561
  return new Response("Invalid JSON-LD.", {
5368
5562
  status: 400,
5369
5563
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5384,7 +5578,7 @@ async function handleInboxInternal(request, parameters, span) {
5384
5578
  json
5385
5579
  });
5386
5580
  try {
5387
- activity = await Activity.fromJsonLd(compactedJsonWithoutSig, {
5581
+ activity = observation.activity = await Activity.fromJsonLd(compactedJsonWithoutSig, {
5388
5582
  ...ctx,
5389
5583
  contextLoader: getNormalizationContextLoader(ctx.contextLoader)
5390
5584
  });
@@ -5396,6 +5590,7 @@ async function handleInboxInternal(request, parameters, span) {
5396
5590
  activityVerified = true;
5397
5591
  if (!skipSignatureVerification && isPortableActivity(activity)) try {
5398
5592
  proofVerified = await verifyObject(Activity, jsonWithoutSig, {
5593
+ ...signatureObservation,
5399
5594
  contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
5400
5595
  documentLoader: ctx.documentLoader,
5401
5596
  keyCache,
@@ -5416,6 +5611,7 @@ async function handleInboxInternal(request, parameters, span) {
5416
5611
  });
5417
5612
  try {
5418
5613
  activity = await verifyObject(Activity, jsonWithoutSig, {
5614
+ ...signatureObservation,
5419
5615
  contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
5420
5616
  documentLoader: ctx.documentLoader,
5421
5617
  keyCache,
@@ -5451,6 +5647,10 @@ async function handleInboxInternal(request, parameters, span) {
5451
5647
  code: SpanStatusCode.ERROR,
5452
5648
  message: `Failed to parse activity:\n${error}`
5453
5649
  });
5650
+ observation.result = {
5651
+ disposition: "rejected",
5652
+ reason: "invalidActivity"
5653
+ };
5454
5654
  return new Response("Invalid activity.", {
5455
5655
  status: 400,
5456
5656
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5474,6 +5674,7 @@ async function handleInboxInternal(request, parameters, span) {
5474
5674
  if (activity == null) {
5475
5675
  if (!skipSignatureVerification) {
5476
5676
  const verification = await verifyRequestDetailed(request, {
5677
+ ...signatureObservation,
5477
5678
  contextLoader: ctx.contextLoader,
5478
5679
  documentLoader: ctx.documentLoader,
5479
5680
  timeWindow: signatureTimeWindow,
@@ -5484,6 +5685,14 @@ async function handleInboxInternal(request, parameters, span) {
5484
5685
  });
5485
5686
  if (verification.verified === false) {
5486
5687
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
5688
+ observation.authentication = {
5689
+ status: "rejected",
5690
+ reason: { type: "verificationFailed" }
5691
+ };
5692
+ observation.result = {
5693
+ disposition: "rejected",
5694
+ reason: "authentication"
5695
+ };
5487
5696
  const reason = verification.reason;
5488
5697
  const remoteHost = "keyId" in reason && reason.keyId != null ? getRemoteHost(reason.keyId) : void 0;
5489
5698
  getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure(reason.type, remoteHost);
@@ -5498,7 +5707,7 @@ async function handleInboxInternal(request, parameters, span) {
5498
5707
  });
5499
5708
  if (unverifiedActivityHandler == null) return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
5500
5709
  try {
5501
- activity = await Activity.fromJsonLd(jsonWithoutSig, ctx);
5710
+ activity = observation.activity = await Activity.fromJsonLd(jsonWithoutSig, ctx);
5502
5711
  } catch (error) {
5503
5712
  logger.error("Failed to parse activity:\n{error}", {
5504
5713
  recipient,
@@ -5514,6 +5723,10 @@ async function handleInboxInternal(request, parameters, span) {
5514
5723
  recipient
5515
5724
  });
5516
5725
  }
5726
+ observation.result = {
5727
+ disposition: "rejected",
5728
+ reason: "invalidActivity"
5729
+ };
5517
5730
  return new Response("Invalid activity.", {
5518
5731
  status: 400,
5519
5732
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5536,6 +5749,11 @@ async function handleInboxInternal(request, parameters, span) {
5536
5749
  try {
5537
5750
  response = await unverifiedActivityHandler(ctx, activity, reason);
5538
5751
  } catch (error) {
5752
+ observation.result = {
5753
+ disposition: "failed",
5754
+ reason: "listenerError",
5755
+ error
5756
+ };
5539
5757
  logger.error("An unexpected error occurred in unverified activity handler:\n{error}", {
5540
5758
  error,
5541
5759
  activity: json,
@@ -5552,7 +5770,10 @@ async function handleInboxInternal(request, parameters, span) {
5552
5770
  }
5553
5771
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
5554
5772
  }
5555
- if (response instanceof Response) return response;
5773
+ if (response instanceof Response) {
5774
+ observation.result = { disposition: "customResponse" };
5775
+ return response;
5776
+ }
5556
5777
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
5557
5778
  } else {
5558
5779
  if (inboxChallengePolicy?.enabled && inboxChallengePolicy.requestNonce) pendingNonceLabel = verification.signatureLabel;
@@ -5562,7 +5783,7 @@ async function handleInboxInternal(request, parameters, span) {
5562
5783
  httpSigKey = verification.key;
5563
5784
  }
5564
5785
  try {
5565
- activity = await Activity.fromJsonLd(jsonWithoutSig, {
5786
+ activity = observation.activity = await Activity.fromJsonLd(jsonWithoutSig, {
5566
5787
  ...ctx,
5567
5788
  contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader)
5568
5789
  });
@@ -5571,6 +5792,12 @@ async function handleInboxInternal(request, parameters, span) {
5571
5792
  return await respondInvalidActivity(error);
5572
5793
  }
5573
5794
  }
5795
+ observation.activity = activity;
5796
+ observation.stage = "policy";
5797
+ observation.result = {
5798
+ disposition: "rejected",
5799
+ reason: "authentication"
5800
+ };
5574
5801
  if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
5575
5802
  span.setAttribute("activitypub.activity.type", getTypeId(activity).href);
5576
5803
  span.addEvent("activitypub.activity.received", {
@@ -5582,6 +5809,13 @@ async function handleInboxInternal(request, parameters, span) {
5582
5809
  });
5583
5810
  if (!skipSignatureVerification && !proofVerified && (isPortableActivity(activity) || isPortableActorKey(httpSigKey))) {
5584
5811
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
5812
+ observation.authentication = {
5813
+ status: "rejected",
5814
+ reason: {
5815
+ type: "proofPolicy",
5816
+ policy: "portableActor"
5817
+ }
5818
+ };
5585
5819
  logger.error("The activity {activityId} of the portable actor {actorId} is not authenticated by a valid Object Integrity Proof.", {
5586
5820
  activity: json,
5587
5821
  recipient,
@@ -5599,20 +5833,36 @@ async function handleInboxInternal(request, parameters, span) {
5599
5833
  });
5600
5834
  }
5601
5835
  if (!skipSignatureVerification && activity.id != null && isPortableId(activity.id)) {
5602
- const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
5603
- contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
5604
- documentLoader: ctx.documentLoader,
5605
- keyCache,
5606
- meterProvider,
5607
- tracerProvider
5608
- });
5836
+ const rejection = await observeAttempt(signatureObservation, "objectIntegrity", async (verification) => {
5837
+ const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
5838
+ [verificationObservation]: verification,
5839
+ contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
5840
+ documentLoader: ctx.documentLoader,
5841
+ keyCache,
5842
+ meterProvider,
5843
+ tracerProvider
5844
+ });
5845
+ if (rejection != null) verification.attempt.reason = {
5846
+ type: "proofPolicy",
5847
+ reason: rejection
5848
+ };
5849
+ return rejection;
5850
+ }, (value) => value == null);
5609
5851
  if (rejection != null) {
5610
5852
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
5853
+ observation.authentication = {
5854
+ status: "rejected",
5855
+ reason: {
5856
+ type: "proofPolicy",
5857
+ policy: "portableActivity",
5858
+ detail: rejection
5859
+ }
5860
+ };
5611
5861
  logger.error("The portable activity {activityId} is not authenticated by an Object Integrity Proof of its own DID: {reason}", {
5612
5862
  activity: json,
5613
5863
  recipient,
5614
5864
  activityId: activity.id.href,
5615
- reason: rejection
5865
+ reason: rejection.type
5616
5866
  });
5617
5867
  span.setStatus({
5618
5868
  code: SpanStatusCode.ERROR,
@@ -5626,6 +5876,14 @@ async function handleInboxInternal(request, parameters, span) {
5626
5876
  }
5627
5877
  if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
5628
5878
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
5879
+ observation.authentication = {
5880
+ status: "rejected",
5881
+ reason: {
5882
+ type: "actorKeyMismatch",
5883
+ key: snapshotKey(httpSigKey),
5884
+ actorIds: activity.actorIds.map((id) => new URL(id.href))
5885
+ }
5886
+ };
5629
5887
  getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : getRemoteHost(httpSigKey.id));
5630
5888
  logger.error("The signer ({keyId}) and the actor ({actorId}) do not match.", {
5631
5889
  activity: json,
@@ -5645,13 +5903,35 @@ async function handleInboxInternal(request, parameters, span) {
5645
5903
  if (!skipSignatureVerification) {
5646
5904
  const compoundJson = Array.isArray(json) ? json : jsonWithoutSig;
5647
5905
  if (inspectCompoundPortableObjectApplicability(compoundJson, INBOX_COMPOUND_PROOF_LIMITS) !== "absent") {
5648
- const compoundProof = await verifyCompoundPortableObjectProofs(compoundJson, INBOX_COMPOUND_PROOF_LIMITS, {
5649
- documentLoader: ctx.documentLoader,
5650
- keyCache,
5651
- meterProvider,
5652
- tracerProvider
5653
- });
5906
+ const compoundProof = await observeAttempt(signatureObservation, "objectIntegrity", async (verification) => {
5907
+ const result = await verifyCompoundPortableObjectProofs(compoundJson, INBOX_COMPOUND_PROOF_LIMITS, {
5908
+ [verificationObservation]: verification,
5909
+ documentLoader: ctx.documentLoader,
5910
+ keyCache,
5911
+ meterProvider,
5912
+ tracerProvider
5913
+ });
5914
+ if (result.status === "unsupported") verification.attempt.reason = {
5915
+ type: "proofPolicy",
5916
+ reason: result.reason
5917
+ };
5918
+ else if (!result.verified) {
5919
+ const failed = result.portableObjects.find((object) => !object.verified);
5920
+ verification.attempt.reason = {
5921
+ type: "proofPolicy",
5922
+ reason: failed?.verified === false ? failed.reason : { type: "invalidProof" }
5923
+ };
5924
+ }
5925
+ return result;
5926
+ }, (result) => result.status === "ok" && result.verified);
5654
5927
  if (compoundProof.status !== "ok" || !compoundProof.verified) {
5928
+ observation.authentication = {
5929
+ status: "rejected",
5930
+ reason: {
5931
+ type: "proofPolicy",
5932
+ policy: "compound"
5933
+ }
5934
+ };
5655
5935
  logger.error("Failed to verify compound portable Object Integrity Proofs.", {
5656
5936
  recipient,
5657
5937
  activity: json,
@@ -5670,11 +5950,20 @@ async function handleInboxInternal(request, parameters, span) {
5670
5950
  }
5671
5951
  if (pendingNonceLabel != null) {
5672
5952
  if (!await verifySignatureNonce(request, kv, kvPrefixes.acceptSignatureNonce, pendingNonceLabel)) {
5953
+ observation.authentication = {
5954
+ status: "rejected",
5955
+ reason: { type: "invalidNonce" }
5956
+ };
5673
5957
  getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("invalidNonce", httpSigKey?.id == null ? void 0 : getRemoteHost(httpSigKey.id));
5674
5958
  logger.error("Signature nonce verification failed (missing, expired, or replayed).", { recipient });
5675
5959
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
5676
5960
  }
5677
5961
  }
5962
+ observation.authentication = skipSignatureVerification ? { status: "skipped" } : {
5963
+ status: "verified",
5964
+ attempts: observation.verification.attempts.filter((attempt) => attempt.status === "verified")
5965
+ };
5966
+ observation.stage = "dispatch";
5678
5967
  const routeResult = await routeActivity({
5679
5968
  context: ctx,
5680
5969
  json,
@@ -5686,7 +5975,14 @@ async function handleInboxInternal(request, parameters, span) {
5686
5975
  inboxListeners,
5687
5976
  inboxContextFactory,
5688
5977
  listenerInboxContextFactory: ldSigVerified ? inboxContextFactory[rawInboxContextFactorySymbol] : void 0,
5689
- inboxErrorHandler,
5978
+ inboxErrorHandler: async (context, error) => {
5979
+ observation.result = {
5980
+ disposition: "failed",
5981
+ reason: "listenerError",
5982
+ error
5983
+ };
5984
+ await inboxErrorHandler?.(context, error);
5985
+ },
5690
5986
  kv,
5691
5987
  kvPrefixes,
5692
5988
  queue,
@@ -5722,30 +6018,44 @@ async function handleInboxInternal(request, parameters, span) {
5722
6018
  error
5723
6019
  });
5724
6020
  }
5725
- if (routeResult === "alreadyProcessed") return new Response(`Activity <${activity.id}> has already been processed.`, {
5726
- status: 202,
5727
- headers: { "Content-Type": "text/plain; charset=utf-8" }
5728
- });
5729
- else if (routeResult === "missingActor") return new Response("Missing actor.", {
5730
- status: 400,
5731
- headers: { "Content-Type": "text/plain; charset=utf-8" }
5732
- });
5733
- else if (routeResult === "enqueued") return new Response("Activity is enqueued.", {
5734
- status: 202,
5735
- headers: { "Content-Type": "text/plain; charset=utf-8" }
5736
- });
5737
- else if (routeResult === "unsupportedActivity") return new Response("", {
5738
- status: 202,
5739
- headers: { "Content-Type": "text/plain; charset=utf-8" }
5740
- });
5741
- else if (routeResult === "error") return new Response("Internal server error.", {
6021
+ if (routeResult === "alreadyProcessed") {
6022
+ observation.result = { disposition: "duplicate" };
6023
+ return new Response(`Activity <${activity.id}> has already been processed.`, {
6024
+ status: 202,
6025
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6026
+ });
6027
+ } else if (routeResult === "missingActor") {
6028
+ observation.result = {
6029
+ disposition: "rejected",
6030
+ reason: "missingActor"
6031
+ };
6032
+ return new Response("Missing actor.", {
6033
+ status: 400,
6034
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6035
+ });
6036
+ } else if (routeResult === "enqueued") {
6037
+ observation.result = { disposition: "enqueued" };
6038
+ return new Response("Activity is enqueued.", {
6039
+ status: 202,
6040
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6041
+ });
6042
+ } else if (routeResult === "unsupportedActivity") {
6043
+ observation.result = { disposition: "unhandled" };
6044
+ return new Response("", {
6045
+ status: 202,
6046
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6047
+ });
6048
+ } else if (routeResult === "error") return new Response("Internal server error.", {
5742
6049
  status: 500,
5743
6050
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5744
6051
  });
5745
- else return new Response("", {
5746
- status: 202,
5747
- headers: { "Content-Type": "text/plain; charset=utf-8" }
5748
- });
6052
+ else {
6053
+ observation.result = { disposition: "processed" };
6054
+ return new Response("", {
6055
+ status: 202,
6056
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6057
+ });
6058
+ }
5749
6059
  }
5750
6060
  const PORTABLE_INBOX_FORWARDABLE_RESULTS = /* @__PURE__ */ new Set([
5751
6061
  "success",
@@ -5874,8 +6184,8 @@ var CustomCollectionHandler = class {
5874
6184
  * @param CollectionPage The CollectionPage constructor.
5875
6185
  * @param filterPredicate Optional filter predicate for items.
5876
6186
  */
5877
- constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
5878
- this.name = name$2;
6187
+ constructor(name$1, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
6188
+ this.name = name$1;
5879
6189
  this.values = values;
5880
6190
  this.context = context;
5881
6191
  this.callbacks = callbacks;
@@ -6411,19 +6721,19 @@ function isPortableActivity(activity) {
6411
6721
  */
6412
6722
  async function verifyPortableActivityId(activityId, jsonLd, options) {
6413
6723
  const expectedId = getCanonicalPortableId(activityId);
6414
- if (expectedId == null) return "malformed portable ID";
6724
+ if (expectedId == null) return { type: "invalidObjectId" };
6415
6725
  let verification;
6416
6726
  try {
6417
6727
  verification = await verifyPortableObjectProofWithRoot(jsonLd, options);
6418
6728
  } catch (error) {
6419
- if (error instanceof InvalidPortableObjectIdError) return "malformed portable ID";
6729
+ if (error instanceof InvalidPortableObjectIdError) return { type: "invalidObjectId" };
6420
6730
  if (!isPermanentActivityParseError(error)) throw error;
6421
- return "malformed document";
6731
+ return { type: "invalidDocument" };
6422
6732
  }
6423
6733
  const { result, root } = verification;
6424
- if (!result.verified) return result.reason.type;
6734
+ if (!result.verified) return result.reason;
6425
6735
  const rootId = root?.["@id"];
6426
- if (typeof rootId !== "string" || getCanonicalPortableId(parseIri(rootId)) !== expectedId) return "the verified document has another ID";
6736
+ if (typeof rootId !== "string" || getCanonicalPortableId(parseIri(rootId)) !== expectedId) return { type: "subjectMismatch" };
6427
6737
  return null;
6428
6738
  }
6429
6739
  //#endregion
@@ -9312,7 +9622,7 @@ var FederationImpl = class extends FederationBuilderImpl {
9312
9622
  return await withContext({
9313
9623
  traceId: spanCtx.traceId,
9314
9624
  spanId: spanCtx.spanId
9315
- }, async () => {
9625
+ }, () => this.#observeInboxFetch(metricState, async () => {
9316
9626
  const logger = getLogger([
9317
9627
  "fedify",
9318
9628
  "federation",
@@ -9327,8 +9637,14 @@ var FederationImpl = class extends FederationBuilderImpl {
9327
9637
  tracer,
9328
9638
  metricState
9329
9639
  });
9640
+ if (metricState.inboxCompletion != null) metricState.inboxCompletion.observation.stage = "respond";
9330
9641
  if (metricState.endpoint !== "hashlink_media" && acceptsJsonLd(request)) response.headers.set("Vary", "Accept");
9331
9642
  } catch (error) {
9643
+ metricState.inboxCompletion?.observation.project(span, {
9644
+ type: "exception",
9645
+ stage: metricState.inboxCompletion.observation.stage,
9646
+ error
9647
+ });
9332
9648
  this.metrics.recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), { routeTemplate: metricState.routeTemplate });
9333
9649
  span.setStatus({
9334
9650
  code: SpanStatusCode.ERROR,
@@ -9367,7 +9683,7 @@ var FederationImpl = class extends FederationBuilderImpl {
9367
9683
  else if (response.status >= 400) logger.warn(logTpl, values);
9368
9684
  else logger.info(logTpl, values);
9369
9685
  return response;
9370
- });
9686
+ }));
9371
9687
  });
9372
9688
  });
9373
9689
  }
@@ -9409,8 +9725,17 @@ var FederationImpl = class extends FederationBuilderImpl {
9409
9725
  metricState.routeTemplate = route.template;
9410
9726
  metricState.endpoint = getEndpointCategory(route.name);
9411
9727
  span.updateName(`${request.method} ${route.template}`);
9412
- let context = this.#createContext(request, contextData);
9413
9728
  const routeName = route.name.replace(/:.*$/, "");
9729
+ let context;
9730
+ try {
9731
+ context = this.#createContext(request, contextData);
9732
+ } catch (error) {
9733
+ if (request.method !== "POST" || routeName !== "inbox" && routeName !== "sharedInbox") throw error;
9734
+ return await this.#reportInboxPreparationError(request, contextData, {
9735
+ kind: routeName === "inbox" ? "personal" : "shared",
9736
+ recipient: route.values.identifier ?? null
9737
+ }, error, span, metricState);
9738
+ }
9414
9739
  switch (routeName) {
9415
9740
  case "webfinger": return await handleWebFinger(request, {
9416
9741
  context,
@@ -9469,6 +9794,28 @@ var FederationImpl = class extends FederationBuilderImpl {
9469
9794
  });
9470
9795
  return response;
9471
9796
  }
9797
+ if (request.method === "POST" && (routeName === "inbox" || routeName === "sharedInbox")) {
9798
+ const recipient = route.values.identifier ?? null;
9799
+ const observation = new InboxObservation({
9800
+ kind: routeName === "inbox" ? "personal" : "shared",
9801
+ recipient
9802
+ });
9803
+ this.#setInboxCompletion(metricState, observation, () => context);
9804
+ return await observation.run(async () => {
9805
+ if (routeName === "inbox") context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: recipient }) });
9806
+ else if (this.sharedInboxKeyDispatcher != null) {
9807
+ const identity = await this.sharedInboxKeyDispatcher(context);
9808
+ if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
9809
+ }
9810
+ return await this.#handleInbox(request, {
9811
+ recipient,
9812
+ context,
9813
+ contextData,
9814
+ onNotFound,
9815
+ observation
9816
+ });
9817
+ }, span);
9818
+ }
9472
9819
  switch (routeName) {
9473
9820
  case "actor":
9474
9821
  case "actorAlias": {
@@ -9544,9 +9891,9 @@ var FederationImpl = class extends FederationBuilderImpl {
9544
9891
  onUnauthorized,
9545
9892
  onNotFound
9546
9893
  });
9547
- context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
9894
+ throw new Error("Unreachable inbox delivery.");
9548
9895
  case "sharedInbox":
9549
- if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
9896
+ if (this.sharedInboxKeyDispatcher != null) {
9550
9897
  const identity = await this.sharedInboxKeyDispatcher(context);
9551
9898
  if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
9552
9899
  }
@@ -9839,7 +10186,49 @@ var FederationImpl = class extends FederationBuilderImpl {
9839
10186
  }
9840
10187
  return await onNotFound(request);
9841
10188
  }
9842
- async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox }) {
10189
+ #setInboxCompletion(metricState, observation, getContext) {
10190
+ const handler = this.inboxRequestFinishedHandler;
10191
+ metricState.inboxCompletion = {
10192
+ observation,
10193
+ finish: (outcome) => observation.finish(getContext(), handler, outcome)
10194
+ };
10195
+ }
10196
+ async #observeInboxFetch(metricState, operation) {
10197
+ let response;
10198
+ try {
10199
+ response = await operation();
10200
+ } catch (error) {
10201
+ const completion = metricState.inboxCompletion;
10202
+ if (completion != null) await completion.finish({
10203
+ type: "exception",
10204
+ stage: completion.observation.stage,
10205
+ error
10206
+ });
10207
+ throw error;
10208
+ }
10209
+ const completion = metricState.inboxCompletion;
10210
+ if (completion != null) await completion.finish({
10211
+ type: "response",
10212
+ status: response.status,
10213
+ ...completion.observation.result
10214
+ });
10215
+ return response;
10216
+ }
10217
+ async #reportInboxPreparationError(request, contextData, inbox, error, span, metricState) {
10218
+ const unavailableLoader = () => Promise.reject(error);
10219
+ const context = new RequestContextImpl({
10220
+ url: new URL(request.url),
10221
+ request,
10222
+ federation: this,
10223
+ data: contextData,
10224
+ documentLoader: unavailableLoader,
10225
+ contextLoader: unavailableLoader
10226
+ });
10227
+ const observation = new InboxObservation(inbox);
10228
+ this.#setInboxCompletion(metricState, observation, () => context);
10229
+ return await observation.run(() => Promise.reject(error), span);
10230
+ }
10231
+ async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox, observation }) {
9843
10232
  if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
9844
10233
  const inboxContextFactory = context.toInboxContext.bind(context);
9845
10234
  inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
@@ -9863,7 +10252,8 @@ var FederationImpl = class extends FederationBuilderImpl {
9863
10252
  meterProvider: this.meterProvider,
9864
10253
  tracerProvider: this.tracerProvider,
9865
10254
  idempotencyStrategy: this.idempotencyStrategy,
9866
- portableInbox
10255
+ portableInbox,
10256
+ observation
9867
10257
  });
9868
10258
  }
9869
10259
  async #fetchPortableInbox(request, portable, { onNotFound, contextData, span, metricState }) {
@@ -9892,63 +10282,79 @@ var FederationImpl = class extends FederationBuilderImpl {
9892
10282
  metricState.endpoint = "inbox";
9893
10283
  span.updateName(`${request.method} ${metricState.routeTemplate}`);
9894
10284
  const identifier = route.values.identifier;
9895
- let context = this.#createContext(request, contextData);
9896
- const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
9897
- const localOrigin = context.canonicalOrigin;
9898
- const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof Tombstone ? null : actor, {
9899
- authority: portable.portableRequest.authority,
9900
- canonicalInboxId: portable.canonicalId,
9901
- localOrigin
9902
- });
9903
- if (resolution.status === "rejected") {
9904
- logger.debug("Not accepting a delivery to the portable inbox {inbox} on behalf of the actor {identifier}: {reason}.", {
9905
- inbox: portable.canonicalId,
9906
- identifier,
9907
- reason: resolution.reason
9908
- });
9909
- return await onNotFound(request);
9910
- }
9911
- const { recipient } = resolution;
9912
- if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
9913
- const excludedOrigins = [localOrigin, new URL(request.url).origin];
9914
- return await this.#handleInbox(request, {
9915
- recipient: identifier,
9916
- context,
9917
- contextData,
9918
- onNotFound,
9919
- portableInbox: {
9920
- recipient,
9921
- forward: async (activity, activityId, activityType, signatures) => {
9922
- if (this.portableInboxForwarding.maxTargets < 1) return;
9923
- if (this.kv.cas == null) {
9924
- if (!this.#portableInboxForwardingWarned) {
9925
- this.#portableInboxForwardingWarned = true;
9926
- 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.");
10285
+ let context;
10286
+ try {
10287
+ context = this.#createContext(request, contextData);
10288
+ } catch (error) {
10289
+ return await this.#reportInboxPreparationError(request, contextData, {
10290
+ kind: "portable",
10291
+ recipient: identifier
10292
+ }, error, span, metricState);
10293
+ }
10294
+ const observation = new InboxObservation({
10295
+ kind: "portable",
10296
+ recipient: identifier
10297
+ });
10298
+ this.#setInboxCompletion(metricState, observation, () => context);
10299
+ return await observation.run(async () => {
10300
+ const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
10301
+ const localOrigin = context.canonicalOrigin;
10302
+ const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof Tombstone ? null : actor, {
10303
+ authority: portable.portableRequest.authority,
10304
+ canonicalInboxId: portable.canonicalId,
10305
+ localOrigin
10306
+ });
10307
+ if (resolution.status === "rejected") {
10308
+ logger.debug("Not accepting a delivery to the portable inbox {inbox} on behalf of the actor {identifier}: {reason}.", {
10309
+ inbox: portable.canonicalId,
10310
+ identifier,
10311
+ reason: resolution.reason
10312
+ });
10313
+ return await onNotFound(request);
10314
+ }
10315
+ const { recipient } = resolution;
10316
+ if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
10317
+ const excludedOrigins = [localOrigin, new URL(request.url).origin];
10318
+ return await this.#handleInbox(request, {
10319
+ recipient: identifier,
10320
+ observation,
10321
+ context,
10322
+ contextData,
10323
+ onNotFound,
10324
+ portableInbox: {
10325
+ recipient,
10326
+ forward: async (activity, activityId, activityType, signatures) => {
10327
+ if (this.portableInboxForwarding.maxTargets < 1) return;
10328
+ if (this.kv.cas == null) {
10329
+ if (!this.#portableInboxForwardingWarned) {
10330
+ this.#portableInboxForwardingWarned = true;
10331
+ 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.");
10332
+ }
10333
+ return;
9927
10334
  }
9928
- return;
10335
+ await forwardPortableInboxActivity({
10336
+ recipient,
10337
+ activity,
10338
+ activityId,
10339
+ activityType,
10340
+ excludedOrigins,
10341
+ baseUrl: context.origin,
10342
+ kv: this.kv,
10343
+ kvPrefix: this.kvPrefixes.portableInboxForwarding,
10344
+ outboxQueue: this.outboxQueue,
10345
+ startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
10346
+ allowPrivateAddress: this.allowPrivateAddress,
10347
+ getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
10348
+ specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
10349
+ signatures,
10350
+ options: this.portableInboxForwarding,
10351
+ meterProvider: this.meterProvider,
10352
+ tracerProvider: this.tracerProvider
10353
+ });
9929
10354
  }
9930
- await forwardPortableInboxActivity({
9931
- recipient,
9932
- activity,
9933
- activityId,
9934
- activityType,
9935
- excludedOrigins,
9936
- baseUrl: context.origin,
9937
- kv: this.kv,
9938
- kvPrefix: this.kvPrefixes.portableInboxForwarding,
9939
- outboxQueue: this.outboxQueue,
9940
- startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
9941
- allowPrivateAddress: this.allowPrivateAddress,
9942
- getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
9943
- specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
9944
- signatures,
9945
- options: this.portableInboxForwarding,
9946
- meterProvider: this.meterProvider,
9947
- tracerProvider: this.tracerProvider
9948
- });
9949
10355
  }
9950
- }
9951
- });
10356
+ });
10357
+ }, span);
9952
10358
  }
9953
10359
  /**
9954
10360
  * Gets this server's gateway key pairs for the portable actor that owns