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

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-BK8HfSUm.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-D--HeIbV.mjs} +41 -4
  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-DVP7DGJC.mjs} +1 -1
  9. package/dist/{docloader-CIP_SG39.mjs → docloader-Dv1JJl37.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 +54 -10
  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 +66 -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-Bo5z395X.cjs} +552 -122
  40. package/dist/{http-1fZ83jj7.cjs → http-BIhWdDf5.cjs} +447 -110
  41. package/dist/{http-BGb3QioZ.js → http-DRMu1C2T.js} +430 -111
  42. package/dist/{http-DkqWu-sm.mjs → http-DvpuyDJB.mjs} +141 -98
  43. package/dist/{key-CEsu7oaD.mjs → key-B7N8mNoY.mjs} +30 -3
  44. package/dist/{http-BrwKINQn.d.cts → key-C1nv7_jD.d.ts} +638 -229
  45. package/dist/{http-DGx0KOws.d.ts → key-DYxuyd-s.d.cts} +638 -229
  46. package/dist/{kv-cache-CJS4mImS.mjs → kv-cache-D0kA0rml.mjs} +1 -1
  47. package/dist/{kv-cache-CGhEgt98.js → kv-cache-DZLpqhW9.js} +1 -1
  48. package/dist/{kv-cache-BEWuZSB4.cjs → kv-cache-mF8Zmjix.cjs} +1 -1
  49. package/dist/{ld-Cf4CPWN_.mjs → ld-BDMNyM2M.mjs} +40 -5
  50. package/dist/{metrics-CocsZP0E.mjs → metrics-BbBj-VZN.mjs} +2 -1
  51. package/dist/{middleware-Cne_jMZo.mjs → middleware-CE1X7-UM.mjs} +1 -1
  52. package/dist/{middleware-Dxn0E9ES.mjs → middleware-DAA7wDdS.mjs} +520 -131
  53. package/dist/{middleware-BfYke3YQ.js → middleware-DfUWPAa-.js} +552 -122
  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-CzUycuhd.mjs} +3 -3
  66. package/dist/{portable-collection-BuBV6xTo.js → portable-collection-COcJxZg_.js} +1 -1
  67. package/dist/{portable-collection-qskqXJrH.mjs → portable-collection-CTcNJiAa.mjs} +2 -2
  68. package/dist/{portable-collection-r-mHg1PV.cjs → portable-collection-cPMffcHJ.cjs} +1 -1
  69. package/dist/{portable-key-woGH_1nn.mjs → portable-key-B4QN3OzR.mjs} +1 -1
  70. package/dist/{proof-8WiH_-Gq.mjs → proof-nce4pf5S.mjs} +133 -16
  71. package/dist/{send-JKzZBgPU.mjs → send-4Idpe2z0.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-BbjmPHYn.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
@@ -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-BGb3QioZ.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-DRMu1C2T.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-BuBV6xTo.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-CGhEgt98.js";
7
+ import { n as doesActorOwnKey, r as getKeyOwner, t as verifyPortableObject } from "./portable-collection-COcJxZg_.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DZLpqhW9.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
  /**
@@ -1880,6 +1991,26 @@ function findUnsupportedCompoundProofShape(json) {
1880
1991
  return findInJsonMaps(json, (map, path) => Object.hasOwn(map, "proof") && !isJsonMap$1(map.proof) ? childPath(path, "proof") : void 0) ?? null;
1881
1992
  }
1882
1993
  /**
1994
+ * Finds the first embedded map that carries a direct `proof` but not its own
1995
+ * `@context`. The map-local compound-proof profile verifies each proof-bearing
1996
+ * map as a self-contained document, so the inbox rejects such a map with
1997
+ * `missingContext` whatever its proof. An object embedded with its captured
1998
+ * signed representation, e.g., by {@link signObject}, always keeps its own
1999
+ * `@context`; one rebuilt under its parent's context, e.g., an object parsed
2000
+ * from a received document, never does, and its proof no longer covers what
2001
+ * it has become anyway.
2002
+ *
2003
+ * The input must be a finite, already-materialized JSON tree; see
2004
+ * {@link containsCompoundPortableObject}.
2005
+ *
2006
+ * @returns The RFC 6901 JSON Pointer to the map, or `null` if every embedded
2007
+ * proof-bearing map has its own `@context`.
2008
+ * @internal
2009
+ */
2010
+ function findEmbeddedProofWithoutContext(json) {
2011
+ return findInJsonMaps(json, (map, path) => path !== "" && Object.hasOwn(map, "proof") && !Object.hasOwn(map, "@context") ? path : void 0) ?? null;
2012
+ }
2013
+ /**
1883
2014
  * Discovers direct literal proof-bearing maps in an immutable JSON snapshot.
1884
2015
  *
1885
2016
  * Discovery is bounded and atomic. Unsupported proof shapes, non-JSON input,
@@ -1934,7 +2065,23 @@ async function verifyDiscoveredProofDocuments(discovered, options) {
1934
2065
  verified: false,
1935
2066
  reason: { type: "missingContext" }
1936
2067
  });
1937
- const key = await verifyMapLocalProof(document.securedDocument, options);
2068
+ const inherited = options[verificationObservation];
2069
+ let subjectId = null;
2070
+ if (inherited != null && document.id != null) try {
2071
+ subjectId = parseIri(document.id);
2072
+ } catch {}
2073
+ const key = await observeAttempt({ [verificationObservation]: inherited == null ? void 0 : {
2074
+ ...inherited,
2075
+ attempt: void 0,
2076
+ check: void 0,
2077
+ subject: {
2078
+ id: subjectId,
2079
+ pointer: document.path
2080
+ }
2081
+ } }, "objectIntegrity", (observation) => verifyMapLocalProof(document.securedDocument, {
2082
+ ...options,
2083
+ [verificationObservation]: inherited == null ? void 0 : observation
2084
+ }), (key) => key != null);
1938
2085
  return key == null ? Object.freeze({
1939
2086
  ...metadata,
1940
2087
  verified: false,
@@ -4573,13 +4720,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
4573
4720
  * @param parameters The parameters for handling the collection.
4574
4721
  * @returns A promise that resolves to an HTTP response.
4575
4722
  */
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, "_");
4723
+ async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, portable, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
4724
+ const spanName = name$2.trim().replace(/\s+/g, "_");
4578
4725
  tracerProvider = tracerProvider ?? trace.getTracerProvider();
4579
4726
  const tracer = tracerProvider.getTracer(name, version);
4580
4727
  const cursor = new URL(request.url).searchParams.get("cursor");
4581
4728
  const metricBase = {
4582
- kind: getCollectionMetricKind(name$1),
4729
+ kind: getCollectionMetricKind(name$2),
4583
4730
  page: cursor != null,
4584
4731
  dispatcher: "built_in"
4585
4732
  };
@@ -4625,7 +4772,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
4625
4772
  span.setStatus({ code: SpanStatusCode.ERROR });
4626
4773
  return await onNotFound(request);
4627
4774
  }
4628
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
4775
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
4629
4776
  itemCount = items.length;
4630
4777
  span.setAttribute("fedify.collection.items", itemCount);
4631
4778
  return items;
@@ -4644,7 +4791,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
4644
4791
  collection = new OrderedCollection({
4645
4792
  id: baseUri,
4646
4793
  attribution,
4647
- totalItems: totalItemCount ?? null,
4794
+ totalItems: totalItemCount ?? (portable == null ? null : itemsOrResponse.length),
4648
4795
  items: itemsOrResponse
4649
4796
  });
4650
4797
  } else {
@@ -4667,7 +4814,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
4667
4814
  } else {
4668
4815
  const uri = new URL(portable == null ? baseUri : pageBase);
4669
4816
  uri.searchParams.set("cursor", cursor);
4670
- const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
4817
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
4671
4818
  kind: SpanKind.SERVER,
4672
4819
  attributes: {
4673
4820
  "activitypub.collection.id": uri.href,
@@ -4683,7 +4830,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
4683
4830
  span.setStatus({ code: SpanStatusCode.ERROR });
4684
4831
  return await onNotFound(request);
4685
4832
  }
4686
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
4833
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
4687
4834
  itemCount = items.length;
4688
4835
  span.setAttribute("fedify.collection.items", itemCount);
4689
4836
  return {
@@ -5164,6 +5311,16 @@ async function handleMediaUpload(request, { identifier, context: ctx, mediaUploa
5164
5311
  * @returns A promise that resolves to an HTTP response.
5165
5312
  */
5166
5313
  async function handleInbox(request, options) {
5314
+ if (options.observation == null) {
5315
+ const observation = new InboxObservation({
5316
+ kind: options.portableInbox != null ? "portable" : options.recipient == null ? "shared" : "personal",
5317
+ recipient: options.recipient
5318
+ });
5319
+ return await observation.runAndFinish(() => options.context, options.inboxRequestFinishedHandler, () => handleInbox(request, {
5320
+ ...options,
5321
+ observation
5322
+ }));
5323
+ }
5167
5324
  return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("activitypub.inbox", {
5168
5325
  kind: options.queue == null ? SpanKind.SERVER : SpanKind.PRODUCER,
5169
5326
  attributes: { "activitypub.shared_inbox": options.recipient == null }
@@ -5172,12 +5329,14 @@ async function handleInbox(request, options) {
5172
5329
  try {
5173
5330
  return await handleInboxInternal(request, options, span);
5174
5331
  } catch (e) {
5332
+ options.observation.hasException = true;
5175
5333
  span.setStatus({
5176
5334
  code: SpanStatusCode.ERROR,
5177
5335
  message: String(e)
5178
5336
  });
5179
5337
  throw e;
5180
5338
  } finally {
5339
+ options.observation.project(span);
5181
5340
  span.end();
5182
5341
  }
5183
5342
  });
@@ -5192,6 +5351,8 @@ async function handleInbox(request, options) {
5192
5351
  */
5193
5352
  async function handleInboxInternal(request, parameters, span) {
5194
5353
  const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, maxHttpSignatures, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider, portableInbox } = parameters;
5354
+ const observation = parameters.observation;
5355
+ const signatureObservation = { [verificationObservation]: observation.verification };
5195
5356
  const logger = getLogger([
5196
5357
  "fedify",
5197
5358
  "federation",
@@ -5216,6 +5377,10 @@ async function handleInboxInternal(request, parameters, span) {
5216
5377
  }
5217
5378
  }
5218
5379
  if (request.bodyUsed) {
5380
+ observation.result = {
5381
+ disposition: "failed",
5382
+ reason: "bodyUnavailable"
5383
+ };
5219
5384
  logger.error("Request body has already been read.", { recipient });
5220
5385
  span.setStatus({
5221
5386
  code: SpanStatusCode.ERROR,
@@ -5226,6 +5391,10 @@ async function handleInboxInternal(request, parameters, span) {
5226
5391
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5227
5392
  });
5228
5393
  } else if (request.body?.locked) {
5394
+ observation.result = {
5395
+ disposition: "failed",
5396
+ reason: "bodyUnavailable"
5397
+ };
5229
5398
  logger.error("Request body is locked.", { recipient });
5230
5399
  span.setStatus({
5231
5400
  code: SpanStatusCode.ERROR,
@@ -5236,11 +5405,20 @@ async function handleInboxInternal(request, parameters, span) {
5236
5405
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5237
5406
  });
5238
5407
  }
5408
+ observation.stage = "parse";
5409
+ observation.result = {
5410
+ disposition: "rejected",
5411
+ reason: "invalidJson"
5412
+ };
5239
5413
  let json;
5240
5414
  try {
5241
5415
  json = JSON.parse(await readBoundedText(request.clone(), MAX_BODY_SIZE, request.url));
5242
5416
  } catch (error) {
5243
5417
  if (error instanceof BodyTooLargeError) {
5418
+ observation.result = {
5419
+ disposition: "rejected",
5420
+ reason: "bodyTooLarge"
5421
+ };
5244
5422
  request.body?.cancel(error).catch(() => {});
5245
5423
  span.setStatus({
5246
5424
  code: SpanStatusCode.ERROR,
@@ -5270,6 +5448,15 @@ async function handleInboxInternal(request, parameters, span) {
5270
5448
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5271
5449
  });
5272
5450
  }
5451
+ observation.payload = {
5452
+ status: "parsed",
5453
+ value: structuredClone(json)
5454
+ };
5455
+ observation.result = {
5456
+ disposition: "rejected",
5457
+ reason: "invalidActivity"
5458
+ };
5459
+ observation.stage = "verify";
5273
5460
  const keyCache = new KvKeyCache(kv, kvPrefixes.publicKey, {
5274
5461
  documentLoader: ctx.documentLoader,
5275
5462
  contextLoader: ctx.contextLoader,
@@ -5299,6 +5486,10 @@ async function handleInboxInternal(request, parameters, span) {
5299
5486
  code: SpanStatusCode.ERROR,
5300
5487
  message: `Failed to parse activity:\n${error}`
5301
5488
  });
5489
+ observation.result = {
5490
+ disposition: "rejected",
5491
+ reason: "invalidActivity"
5492
+ };
5302
5493
  return new Response("Invalid activity.", {
5303
5494
  status: 400,
5304
5495
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5316,11 +5507,25 @@ async function handleInboxInternal(request, parameters, span) {
5316
5507
  recipient,
5317
5508
  error
5318
5509
  });
5510
+ observation.result = {
5511
+ disposition: "rejected",
5512
+ reason: "invalidJsonLd"
5513
+ };
5319
5514
  return new Response("Invalid JSON-LD.", {
5320
5515
  status: 400,
5321
5516
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5322
5517
  });
5323
5518
  }
5519
+ observation.verification.attempts.push({
5520
+ mechanism: "linkedData",
5521
+ subject: {
5522
+ id: null,
5523
+ pointer: ""
5524
+ },
5525
+ checks: [],
5526
+ status: "error",
5527
+ error
5528
+ });
5324
5529
  if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
5325
5530
  if (!skipSignatureVerification) deferredLdSignatureError = error;
5326
5531
  logger.debug("Failed to normalize JSON-LD for Linked Data Signatures; deferring to another authentication path only if it verifies:\n{error}", {
@@ -5332,6 +5537,7 @@ async function handleInboxInternal(request, parameters, span) {
5332
5537
  compactedJsonWithoutSig = detachSignature(compactedJson);
5333
5538
  try {
5334
5539
  ldSigVerified = await verifyCompactJsonLd(compactedJson, {
5540
+ ...signatureObservation,
5335
5541
  contextLoader: ctx.contextLoader,
5336
5542
  documentLoader: ctx.documentLoader,
5337
5543
  keyCache,
@@ -5345,6 +5551,10 @@ async function handleInboxInternal(request, parameters, span) {
5345
5551
  recipient,
5346
5552
  error
5347
5553
  });
5554
+ observation.result = {
5555
+ disposition: "rejected",
5556
+ reason: "invalidJsonLd"
5557
+ };
5348
5558
  return new Response("Invalid JSON-LD.", {
5349
5559
  status: 400,
5350
5560
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5364,6 +5574,10 @@ async function handleInboxInternal(request, parameters, span) {
5364
5574
  recipient,
5365
5575
  error: parseError
5366
5576
  });
5577
+ observation.result = {
5578
+ disposition: "rejected",
5579
+ reason: "invalidJsonLd"
5580
+ };
5367
5581
  return new Response("Invalid JSON-LD.", {
5368
5582
  status: 400,
5369
5583
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5384,7 +5598,7 @@ async function handleInboxInternal(request, parameters, span) {
5384
5598
  json
5385
5599
  });
5386
5600
  try {
5387
- activity = await Activity.fromJsonLd(compactedJsonWithoutSig, {
5601
+ activity = observation.activity = await Activity.fromJsonLd(compactedJsonWithoutSig, {
5388
5602
  ...ctx,
5389
5603
  contextLoader: getNormalizationContextLoader(ctx.contextLoader)
5390
5604
  });
@@ -5396,6 +5610,7 @@ async function handleInboxInternal(request, parameters, span) {
5396
5610
  activityVerified = true;
5397
5611
  if (!skipSignatureVerification && isPortableActivity(activity)) try {
5398
5612
  proofVerified = await verifyObject(Activity, jsonWithoutSig, {
5613
+ ...signatureObservation,
5399
5614
  contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
5400
5615
  documentLoader: ctx.documentLoader,
5401
5616
  keyCache,
@@ -5416,6 +5631,7 @@ async function handleInboxInternal(request, parameters, span) {
5416
5631
  });
5417
5632
  try {
5418
5633
  activity = await verifyObject(Activity, jsonWithoutSig, {
5634
+ ...signatureObservation,
5419
5635
  contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
5420
5636
  documentLoader: ctx.documentLoader,
5421
5637
  keyCache,
@@ -5451,6 +5667,10 @@ async function handleInboxInternal(request, parameters, span) {
5451
5667
  code: SpanStatusCode.ERROR,
5452
5668
  message: `Failed to parse activity:\n${error}`
5453
5669
  });
5670
+ observation.result = {
5671
+ disposition: "rejected",
5672
+ reason: "invalidActivity"
5673
+ };
5454
5674
  return new Response("Invalid activity.", {
5455
5675
  status: 400,
5456
5676
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5474,6 +5694,7 @@ async function handleInboxInternal(request, parameters, span) {
5474
5694
  if (activity == null) {
5475
5695
  if (!skipSignatureVerification) {
5476
5696
  const verification = await verifyRequestDetailed(request, {
5697
+ ...signatureObservation,
5477
5698
  contextLoader: ctx.contextLoader,
5478
5699
  documentLoader: ctx.documentLoader,
5479
5700
  timeWindow: signatureTimeWindow,
@@ -5484,6 +5705,14 @@ async function handleInboxInternal(request, parameters, span) {
5484
5705
  });
5485
5706
  if (verification.verified === false) {
5486
5707
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
5708
+ observation.authentication = {
5709
+ status: "rejected",
5710
+ reason: { type: "verificationFailed" }
5711
+ };
5712
+ observation.result = {
5713
+ disposition: "rejected",
5714
+ reason: "authentication"
5715
+ };
5487
5716
  const reason = verification.reason;
5488
5717
  const remoteHost = "keyId" in reason && reason.keyId != null ? getRemoteHost(reason.keyId) : void 0;
5489
5718
  getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure(reason.type, remoteHost);
@@ -5498,7 +5727,7 @@ async function handleInboxInternal(request, parameters, span) {
5498
5727
  });
5499
5728
  if (unverifiedActivityHandler == null) return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
5500
5729
  try {
5501
- activity = await Activity.fromJsonLd(jsonWithoutSig, ctx);
5730
+ activity = observation.activity = await Activity.fromJsonLd(jsonWithoutSig, ctx);
5502
5731
  } catch (error) {
5503
5732
  logger.error("Failed to parse activity:\n{error}", {
5504
5733
  recipient,
@@ -5514,6 +5743,10 @@ async function handleInboxInternal(request, parameters, span) {
5514
5743
  recipient
5515
5744
  });
5516
5745
  }
5746
+ observation.result = {
5747
+ disposition: "rejected",
5748
+ reason: "invalidActivity"
5749
+ };
5517
5750
  return new Response("Invalid activity.", {
5518
5751
  status: 400,
5519
5752
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5536,6 +5769,11 @@ async function handleInboxInternal(request, parameters, span) {
5536
5769
  try {
5537
5770
  response = await unverifiedActivityHandler(ctx, activity, reason);
5538
5771
  } catch (error) {
5772
+ observation.result = {
5773
+ disposition: "failed",
5774
+ reason: "listenerError",
5775
+ error
5776
+ };
5539
5777
  logger.error("An unexpected error occurred in unverified activity handler:\n{error}", {
5540
5778
  error,
5541
5779
  activity: json,
@@ -5552,7 +5790,10 @@ async function handleInboxInternal(request, parameters, span) {
5552
5790
  }
5553
5791
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
5554
5792
  }
5555
- if (response instanceof Response) return response;
5793
+ if (response instanceof Response) {
5794
+ observation.result = { disposition: "customResponse" };
5795
+ return response;
5796
+ }
5556
5797
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
5557
5798
  } else {
5558
5799
  if (inboxChallengePolicy?.enabled && inboxChallengePolicy.requestNonce) pendingNonceLabel = verification.signatureLabel;
@@ -5562,7 +5803,7 @@ async function handleInboxInternal(request, parameters, span) {
5562
5803
  httpSigKey = verification.key;
5563
5804
  }
5564
5805
  try {
5565
- activity = await Activity.fromJsonLd(jsonWithoutSig, {
5806
+ activity = observation.activity = await Activity.fromJsonLd(jsonWithoutSig, {
5566
5807
  ...ctx,
5567
5808
  contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader)
5568
5809
  });
@@ -5571,6 +5812,12 @@ async function handleInboxInternal(request, parameters, span) {
5571
5812
  return await respondInvalidActivity(error);
5572
5813
  }
5573
5814
  }
5815
+ observation.activity = activity;
5816
+ observation.stage = "policy";
5817
+ observation.result = {
5818
+ disposition: "rejected",
5819
+ reason: "authentication"
5820
+ };
5574
5821
  if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
5575
5822
  span.setAttribute("activitypub.activity.type", getTypeId(activity).href);
5576
5823
  span.addEvent("activitypub.activity.received", {
@@ -5582,6 +5829,13 @@ async function handleInboxInternal(request, parameters, span) {
5582
5829
  });
5583
5830
  if (!skipSignatureVerification && !proofVerified && (isPortableActivity(activity) || isPortableActorKey(httpSigKey))) {
5584
5831
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
5832
+ observation.authentication = {
5833
+ status: "rejected",
5834
+ reason: {
5835
+ type: "proofPolicy",
5836
+ policy: "portableActor"
5837
+ }
5838
+ };
5585
5839
  logger.error("The activity {activityId} of the portable actor {actorId} is not authenticated by a valid Object Integrity Proof.", {
5586
5840
  activity: json,
5587
5841
  recipient,
@@ -5599,20 +5853,36 @@ async function handleInboxInternal(request, parameters, span) {
5599
5853
  });
5600
5854
  }
5601
5855
  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
- });
5856
+ const rejection = await observeAttempt(signatureObservation, "objectIntegrity", async (verification) => {
5857
+ const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
5858
+ [verificationObservation]: verification,
5859
+ contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
5860
+ documentLoader: ctx.documentLoader,
5861
+ keyCache,
5862
+ meterProvider,
5863
+ tracerProvider
5864
+ });
5865
+ if (rejection != null) verification.attempt.reason = {
5866
+ type: "proofPolicy",
5867
+ reason: rejection
5868
+ };
5869
+ return rejection;
5870
+ }, (value) => value == null);
5609
5871
  if (rejection != null) {
5610
5872
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
5873
+ observation.authentication = {
5874
+ status: "rejected",
5875
+ reason: {
5876
+ type: "proofPolicy",
5877
+ policy: "portableActivity",
5878
+ detail: rejection
5879
+ }
5880
+ };
5611
5881
  logger.error("The portable activity {activityId} is not authenticated by an Object Integrity Proof of its own DID: {reason}", {
5612
5882
  activity: json,
5613
5883
  recipient,
5614
5884
  activityId: activity.id.href,
5615
- reason: rejection
5885
+ reason: rejection.type
5616
5886
  });
5617
5887
  span.setStatus({
5618
5888
  code: SpanStatusCode.ERROR,
@@ -5626,6 +5896,14 @@ async function handleInboxInternal(request, parameters, span) {
5626
5896
  }
5627
5897
  if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
5628
5898
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
5899
+ observation.authentication = {
5900
+ status: "rejected",
5901
+ reason: {
5902
+ type: "actorKeyMismatch",
5903
+ key: snapshotKey(httpSigKey),
5904
+ actorIds: activity.actorIds.map((id) => new URL(id.href))
5905
+ }
5906
+ };
5629
5907
  getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : getRemoteHost(httpSigKey.id));
5630
5908
  logger.error("The signer ({keyId}) and the actor ({actorId}) do not match.", {
5631
5909
  activity: json,
@@ -5645,13 +5923,35 @@ async function handleInboxInternal(request, parameters, span) {
5645
5923
  if (!skipSignatureVerification) {
5646
5924
  const compoundJson = Array.isArray(json) ? json : jsonWithoutSig;
5647
5925
  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
- });
5926
+ const compoundProof = await observeAttempt(signatureObservation, "objectIntegrity", async (verification) => {
5927
+ const result = await verifyCompoundPortableObjectProofs(compoundJson, INBOX_COMPOUND_PROOF_LIMITS, {
5928
+ [verificationObservation]: verification,
5929
+ documentLoader: ctx.documentLoader,
5930
+ keyCache,
5931
+ meterProvider,
5932
+ tracerProvider
5933
+ });
5934
+ if (result.status === "unsupported") verification.attempt.reason = {
5935
+ type: "proofPolicy",
5936
+ reason: result.reason
5937
+ };
5938
+ else if (!result.verified) {
5939
+ const failed = result.portableObjects.find((object) => !object.verified);
5940
+ verification.attempt.reason = {
5941
+ type: "proofPolicy",
5942
+ reason: failed?.verified === false ? failed.reason : { type: "invalidProof" }
5943
+ };
5944
+ }
5945
+ return result;
5946
+ }, (result) => result.status === "ok" && result.verified);
5654
5947
  if (compoundProof.status !== "ok" || !compoundProof.verified) {
5948
+ observation.authentication = {
5949
+ status: "rejected",
5950
+ reason: {
5951
+ type: "proofPolicy",
5952
+ policy: "compound"
5953
+ }
5954
+ };
5655
5955
  logger.error("Failed to verify compound portable Object Integrity Proofs.", {
5656
5956
  recipient,
5657
5957
  activity: json,
@@ -5670,11 +5970,20 @@ async function handleInboxInternal(request, parameters, span) {
5670
5970
  }
5671
5971
  if (pendingNonceLabel != null) {
5672
5972
  if (!await verifySignatureNonce(request, kv, kvPrefixes.acceptSignatureNonce, pendingNonceLabel)) {
5973
+ observation.authentication = {
5974
+ status: "rejected",
5975
+ reason: { type: "invalidNonce" }
5976
+ };
5673
5977
  getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("invalidNonce", httpSigKey?.id == null ? void 0 : getRemoteHost(httpSigKey.id));
5674
5978
  logger.error("Signature nonce verification failed (missing, expired, or replayed).", { recipient });
5675
5979
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
5676
5980
  }
5677
5981
  }
5982
+ observation.authentication = skipSignatureVerification ? { status: "skipped" } : {
5983
+ status: "verified",
5984
+ attempts: observation.verification.attempts.filter((attempt) => attempt.status === "verified")
5985
+ };
5986
+ observation.stage = "dispatch";
5678
5987
  const routeResult = await routeActivity({
5679
5988
  context: ctx,
5680
5989
  json,
@@ -5686,7 +5995,14 @@ async function handleInboxInternal(request, parameters, span) {
5686
5995
  inboxListeners,
5687
5996
  inboxContextFactory,
5688
5997
  listenerInboxContextFactory: ldSigVerified ? inboxContextFactory[rawInboxContextFactorySymbol] : void 0,
5689
- inboxErrorHandler,
5998
+ inboxErrorHandler: async (context, error) => {
5999
+ observation.result = {
6000
+ disposition: "failed",
6001
+ reason: "listenerError",
6002
+ error
6003
+ };
6004
+ await inboxErrorHandler?.(context, error);
6005
+ },
5690
6006
  kv,
5691
6007
  kvPrefixes,
5692
6008
  queue,
@@ -5722,30 +6038,44 @@ async function handleInboxInternal(request, parameters, span) {
5722
6038
  error
5723
6039
  });
5724
6040
  }
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.", {
6041
+ if (routeResult === "alreadyProcessed") {
6042
+ observation.result = { disposition: "duplicate" };
6043
+ return new Response(`Activity <${activity.id}> has already been processed.`, {
6044
+ status: 202,
6045
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6046
+ });
6047
+ } else if (routeResult === "missingActor") {
6048
+ observation.result = {
6049
+ disposition: "rejected",
6050
+ reason: "missingActor"
6051
+ };
6052
+ return new Response("Missing actor.", {
6053
+ status: 400,
6054
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6055
+ });
6056
+ } else if (routeResult === "enqueued") {
6057
+ observation.result = { disposition: "enqueued" };
6058
+ return new Response("Activity is enqueued.", {
6059
+ status: 202,
6060
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6061
+ });
6062
+ } else if (routeResult === "unsupportedActivity") {
6063
+ observation.result = { disposition: "unhandled" };
6064
+ return new Response("", {
6065
+ status: 202,
6066
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6067
+ });
6068
+ } else if (routeResult === "error") return new Response("Internal server error.", {
5742
6069
  status: 500,
5743
6070
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5744
6071
  });
5745
- else return new Response("", {
5746
- status: 202,
5747
- headers: { "Content-Type": "text/plain; charset=utf-8" }
5748
- });
6072
+ else {
6073
+ observation.result = { disposition: "processed" };
6074
+ return new Response("", {
6075
+ status: 202,
6076
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6077
+ });
6078
+ }
5749
6079
  }
5750
6080
  const PORTABLE_INBOX_FORWARDABLE_RESULTS = /* @__PURE__ */ new Set([
5751
6081
  "success",
@@ -5874,8 +6204,8 @@ var CustomCollectionHandler = class {
5874
6204
  * @param CollectionPage The CollectionPage constructor.
5875
6205
  * @param filterPredicate Optional filter predicate for items.
5876
6206
  */
5877
- constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
5878
- this.name = name$2;
6207
+ constructor(name$1, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
6208
+ this.name = name$1;
5879
6209
  this.values = values;
5880
6210
  this.context = context;
5881
6211
  this.callbacks = callbacks;
@@ -6411,19 +6741,19 @@ function isPortableActivity(activity) {
6411
6741
  */
6412
6742
  async function verifyPortableActivityId(activityId, jsonLd, options) {
6413
6743
  const expectedId = getCanonicalPortableId(activityId);
6414
- if (expectedId == null) return "malformed portable ID";
6744
+ if (expectedId == null) return { type: "invalidObjectId" };
6415
6745
  let verification;
6416
6746
  try {
6417
6747
  verification = await verifyPortableObjectProofWithRoot(jsonLd, options);
6418
6748
  } catch (error) {
6419
- if (error instanceof InvalidPortableObjectIdError) return "malformed portable ID";
6749
+ if (error instanceof InvalidPortableObjectIdError) return { type: "invalidObjectId" };
6420
6750
  if (!isPermanentActivityParseError(error)) throw error;
6421
- return "malformed document";
6751
+ return { type: "invalidDocument" };
6422
6752
  }
6423
6753
  const { result, root } = verification;
6424
- if (!result.verified) return result.reason.type;
6754
+ if (!result.verified) return result.reason;
6425
6755
  const rootId = root?.["@id"];
6426
- if (typeof rootId !== "string" || getCanonicalPortableId(parseIri(rootId)) !== expectedId) return "the verified document has another ID";
6756
+ if (typeof rootId !== "string" || getCanonicalPortableId(parseIri(rootId)) !== expectedId) return { type: "subjectMismatch" };
6427
6757
  return null;
6428
6758
  }
6429
6759
  //#endregion
@@ -6884,7 +7214,10 @@ function selectPortableActivityKey(activity, activityId, keys) {
6884
7214
  /**
6885
7215
  * Rejects an outgoing activity whose proofs Fedify's own inbox would reject
6886
7216
  * as unsupported: a document with portable objects in which some map carries
6887
- * a proof set or another value that is not a single proof map.
7217
+ * a proof set or another value that is not a single proof map, or in which
7218
+ * an embedded map carries a proof but not its own `@context`, e.g., a signed
7219
+ * object parsed from a received document and rebuilt under the activity's
7220
+ * context.
6888
7221
  *
6889
7222
  * @param jsonLd The compact JSON-LD document about to be delivered.
6890
7223
  * @param activityId The activity ID, for the error message.
@@ -6894,8 +7227,9 @@ function selectPortableActivityKey(activity, activityId, keys) {
6894
7227
  function assertSupportedCompoundProofShape(jsonLd, activityId) {
6895
7228
  if (!containsCompoundPortableObject(jsonLd)) return;
6896
7229
  const path = findUnsupportedCompoundProofShape(jsonLd);
6897
- if (path == null) return;
6898
- throw new TypeError(`Cannot send the activity ${activityId}: it embeds portable objects, but its proof at the JSON Pointer ${JSON.stringify(path)} is not a single proof map. The map-local compound-proof profile accepts exactly one direct proof per map, so Fedify inboxes would reject the activity. Sign each object with exactly one key.`);
7230
+ if (path != null) throw new TypeError(`Cannot send the activity ${activityId}: it embeds portable objects, but its proof at the JSON Pointer ${JSON.stringify(path)} is not a single proof map. The map-local compound-proof profile accepts exactly one direct proof per map, so Fedify inboxes would reject the activity. Sign each object with exactly one key.`);
7231
+ const embedded = findEmbeddedProofWithoutContext(jsonLd);
7232
+ if (embedded != null) throw new TypeError(`Cannot send the activity ${activityId}: it embeds portable objects, but the map at the JSON Pointer ${JSON.stringify(embedded)} carries a proof without its own @context. The map-local compound-proof profile verifies each proof-bearing map on its own, so Fedify inboxes would reject the activity. This happens when a signed object parsed from a received document, e.g., a Follow in an inbox listener, is embedded, as it is rebuilt under the activity's context and its proof no longer covers it. Refer to such an object by its ID instead, e.g., new Accept({ object: follow.id }), or embed an object that you signed with signObject().`);
6899
7233
  }
6900
7234
  //#endregion
6901
7235
  //#region src/federation/retry.ts
@@ -9312,7 +9646,7 @@ var FederationImpl = class extends FederationBuilderImpl {
9312
9646
  return await withContext({
9313
9647
  traceId: spanCtx.traceId,
9314
9648
  spanId: spanCtx.spanId
9315
- }, async () => {
9649
+ }, () => this.#observeInboxFetch(metricState, async () => {
9316
9650
  const logger = getLogger([
9317
9651
  "fedify",
9318
9652
  "federation",
@@ -9327,8 +9661,14 @@ var FederationImpl = class extends FederationBuilderImpl {
9327
9661
  tracer,
9328
9662
  metricState
9329
9663
  });
9664
+ if (metricState.inboxCompletion != null) metricState.inboxCompletion.observation.stage = "respond";
9330
9665
  if (metricState.endpoint !== "hashlink_media" && acceptsJsonLd(request)) response.headers.set("Vary", "Accept");
9331
9666
  } catch (error) {
9667
+ metricState.inboxCompletion?.observation.project(span, {
9668
+ type: "exception",
9669
+ stage: metricState.inboxCompletion.observation.stage,
9670
+ error
9671
+ });
9332
9672
  this.metrics.recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), { routeTemplate: metricState.routeTemplate });
9333
9673
  span.setStatus({
9334
9674
  code: SpanStatusCode.ERROR,
@@ -9367,7 +9707,7 @@ var FederationImpl = class extends FederationBuilderImpl {
9367
9707
  else if (response.status >= 400) logger.warn(logTpl, values);
9368
9708
  else logger.info(logTpl, values);
9369
9709
  return response;
9370
- });
9710
+ }));
9371
9711
  });
9372
9712
  });
9373
9713
  }
@@ -9409,8 +9749,17 @@ var FederationImpl = class extends FederationBuilderImpl {
9409
9749
  metricState.routeTemplate = route.template;
9410
9750
  metricState.endpoint = getEndpointCategory(route.name);
9411
9751
  span.updateName(`${request.method} ${route.template}`);
9412
- let context = this.#createContext(request, contextData);
9413
9752
  const routeName = route.name.replace(/:.*$/, "");
9753
+ let context;
9754
+ try {
9755
+ context = this.#createContext(request, contextData);
9756
+ } catch (error) {
9757
+ if (request.method !== "POST" || routeName !== "inbox" && routeName !== "sharedInbox") throw error;
9758
+ return await this.#reportInboxPreparationError(request, contextData, {
9759
+ kind: routeName === "inbox" ? "personal" : "shared",
9760
+ recipient: route.values.identifier ?? null
9761
+ }, error, span, metricState);
9762
+ }
9414
9763
  switch (routeName) {
9415
9764
  case "webfinger": return await handleWebFinger(request, {
9416
9765
  context,
@@ -9469,6 +9818,28 @@ var FederationImpl = class extends FederationBuilderImpl {
9469
9818
  });
9470
9819
  return response;
9471
9820
  }
9821
+ if (request.method === "POST" && (routeName === "inbox" || routeName === "sharedInbox")) {
9822
+ const recipient = route.values.identifier ?? null;
9823
+ const observation = new InboxObservation({
9824
+ kind: routeName === "inbox" ? "personal" : "shared",
9825
+ recipient
9826
+ });
9827
+ this.#setInboxCompletion(metricState, observation, () => context);
9828
+ return await observation.run(async () => {
9829
+ if (routeName === "inbox") context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: recipient }) });
9830
+ else if (this.sharedInboxKeyDispatcher != null) {
9831
+ const identity = await this.sharedInboxKeyDispatcher(context);
9832
+ if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
9833
+ }
9834
+ return await this.#handleInbox(request, {
9835
+ recipient,
9836
+ context,
9837
+ contextData,
9838
+ onNotFound,
9839
+ observation
9840
+ });
9841
+ }, span);
9842
+ }
9472
9843
  switch (routeName) {
9473
9844
  case "actor":
9474
9845
  case "actorAlias": {
@@ -9544,9 +9915,9 @@ var FederationImpl = class extends FederationBuilderImpl {
9544
9915
  onUnauthorized,
9545
9916
  onNotFound
9546
9917
  });
9547
- context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
9918
+ throw new Error("Unreachable inbox delivery.");
9548
9919
  case "sharedInbox":
9549
- if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
9920
+ if (this.sharedInboxKeyDispatcher != null) {
9550
9921
  const identity = await this.sharedInboxKeyDispatcher(context);
9551
9922
  if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
9552
9923
  }
@@ -9839,7 +10210,49 @@ var FederationImpl = class extends FederationBuilderImpl {
9839
10210
  }
9840
10211
  return await onNotFound(request);
9841
10212
  }
9842
- async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox }) {
10213
+ #setInboxCompletion(metricState, observation, getContext) {
10214
+ const handler = this.inboxRequestFinishedHandler;
10215
+ metricState.inboxCompletion = {
10216
+ observation,
10217
+ finish: (outcome) => observation.finish(getContext(), handler, outcome)
10218
+ };
10219
+ }
10220
+ async #observeInboxFetch(metricState, operation) {
10221
+ let response;
10222
+ try {
10223
+ response = await operation();
10224
+ } catch (error) {
10225
+ const completion = metricState.inboxCompletion;
10226
+ if (completion != null) await completion.finish({
10227
+ type: "exception",
10228
+ stage: completion.observation.stage,
10229
+ error
10230
+ });
10231
+ throw error;
10232
+ }
10233
+ const completion = metricState.inboxCompletion;
10234
+ if (completion != null) await completion.finish({
10235
+ type: "response",
10236
+ status: response.status,
10237
+ ...completion.observation.result
10238
+ });
10239
+ return response;
10240
+ }
10241
+ async #reportInboxPreparationError(request, contextData, inbox, error, span, metricState) {
10242
+ const unavailableLoader = () => Promise.reject(error);
10243
+ const context = new RequestContextImpl({
10244
+ url: new URL(request.url),
10245
+ request,
10246
+ federation: this,
10247
+ data: contextData,
10248
+ documentLoader: unavailableLoader,
10249
+ contextLoader: unavailableLoader
10250
+ });
10251
+ const observation = new InboxObservation(inbox);
10252
+ this.#setInboxCompletion(metricState, observation, () => context);
10253
+ return await observation.run(() => Promise.reject(error), span);
10254
+ }
10255
+ async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox, observation }) {
9843
10256
  if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
9844
10257
  const inboxContextFactory = context.toInboxContext.bind(context);
9845
10258
  inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
@@ -9863,7 +10276,8 @@ var FederationImpl = class extends FederationBuilderImpl {
9863
10276
  meterProvider: this.meterProvider,
9864
10277
  tracerProvider: this.tracerProvider,
9865
10278
  idempotencyStrategy: this.idempotencyStrategy,
9866
- portableInbox
10279
+ portableInbox,
10280
+ observation
9867
10281
  });
9868
10282
  }
9869
10283
  async #fetchPortableInbox(request, portable, { onNotFound, contextData, span, metricState }) {
@@ -9892,63 +10306,79 @@ var FederationImpl = class extends FederationBuilderImpl {
9892
10306
  metricState.endpoint = "inbox";
9893
10307
  span.updateName(`${request.method} ${metricState.routeTemplate}`);
9894
10308
  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.");
10309
+ let context;
10310
+ try {
10311
+ context = this.#createContext(request, contextData);
10312
+ } catch (error) {
10313
+ return await this.#reportInboxPreparationError(request, contextData, {
10314
+ kind: "portable",
10315
+ recipient: identifier
10316
+ }, error, span, metricState);
10317
+ }
10318
+ const observation = new InboxObservation({
10319
+ kind: "portable",
10320
+ recipient: identifier
10321
+ });
10322
+ this.#setInboxCompletion(metricState, observation, () => context);
10323
+ return await observation.run(async () => {
10324
+ const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
10325
+ const localOrigin = context.canonicalOrigin;
10326
+ const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof Tombstone ? null : actor, {
10327
+ authority: portable.portableRequest.authority,
10328
+ canonicalInboxId: portable.canonicalId,
10329
+ localOrigin
10330
+ });
10331
+ if (resolution.status === "rejected") {
10332
+ logger.debug("Not accepting a delivery to the portable inbox {inbox} on behalf of the actor {identifier}: {reason}.", {
10333
+ inbox: portable.canonicalId,
10334
+ identifier,
10335
+ reason: resolution.reason
10336
+ });
10337
+ return await onNotFound(request);
10338
+ }
10339
+ const { recipient } = resolution;
10340
+ if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
10341
+ const excludedOrigins = [localOrigin, new URL(request.url).origin];
10342
+ return await this.#handleInbox(request, {
10343
+ recipient: identifier,
10344
+ observation,
10345
+ context,
10346
+ contextData,
10347
+ onNotFound,
10348
+ portableInbox: {
10349
+ recipient,
10350
+ forward: async (activity, activityId, activityType, signatures) => {
10351
+ if (this.portableInboxForwarding.maxTargets < 1) return;
10352
+ if (this.kv.cas == null) {
10353
+ if (!this.#portableInboxForwardingWarned) {
10354
+ this.#portableInboxForwardingWarned = true;
10355
+ 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.");
10356
+ }
10357
+ return;
9927
10358
  }
9928
- return;
10359
+ await forwardPortableInboxActivity({
10360
+ recipient,
10361
+ activity,
10362
+ activityId,
10363
+ activityType,
10364
+ excludedOrigins,
10365
+ baseUrl: context.origin,
10366
+ kv: this.kv,
10367
+ kvPrefix: this.kvPrefixes.portableInboxForwarding,
10368
+ outboxQueue: this.outboxQueue,
10369
+ startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
10370
+ allowPrivateAddress: this.allowPrivateAddress,
10371
+ getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
10372
+ specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
10373
+ signatures,
10374
+ options: this.portableInboxForwarding,
10375
+ meterProvider: this.meterProvider,
10376
+ tracerProvider: this.tracerProvider
10377
+ });
9929
10378
  }
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
10379
  }
9950
- }
9951
- });
10380
+ });
10381
+ }, span);
9952
10382
  }
9953
10383
  /**
9954
10384
  * Gets this server's gateway key pairs for the portable actor that owns