@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
@@ -1,10 +1,10 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  const require_rolldown_runtime = require("./rolldown-runtime-Dmm3t92W.cjs");
3
3
  const require_transformers = require("./transformers-BalCrSsr.cjs");
4
- const require_http = require("./http-1fZ83jj7.cjs");
4
+ const require_http = require("./http-BIhWdDf5.cjs");
5
5
  const require_types = require("./types-poIao40f.cjs");
6
- const require_portable_collection = require("./portable-collection-r-mHg1PV.cjs");
7
- const require_kv_cache = require("./kv-cache-BEWuZSB4.cjs");
6
+ const require_portable_collection = require("./portable-collection-cPMffcHJ.cjs");
7
+ const require_kv_cache = require("./kv-cache-mF8Zmjix.cjs");
8
8
  let _logtape_logtape = require("@logtape/logtape");
9
9
  let _fedify_uri_template = require("@fedify/uri-template");
10
10
  let _fedify_vocab = require("@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;
@@ -1447,6 +1453,111 @@ async function buildCollectionSynchronizationHeader(collectionId, actorIds) {
1447
1453
  return `collectionId="${collectionId}", url="${url}", digest="${(0, byte_encodings_hex.encodeHex)(await digest(actorIds))}"`;
1448
1454
  }
1449
1455
  //#endregion
1456
+ //#region src/federation/inbox-observation.ts
1457
+ /** Internal state owned by the ingress boundary, never serialized into queues. */
1458
+ var InboxObservation = class {
1459
+ inbox;
1460
+ payload = { status: "unavailable" };
1461
+ activity = null;
1462
+ authentication = { status: "notDetermined" };
1463
+ hasException = false;
1464
+ stage = "prepare";
1465
+ result = {
1466
+ disposition: "rejected",
1467
+ reason: "recipientNotFound"
1468
+ };
1469
+ verification = {
1470
+ attempts: [],
1471
+ subject: {
1472
+ id: null,
1473
+ pointer: ""
1474
+ },
1475
+ parsedObject: (object) => {
1476
+ if (object instanceof _fedify_vocab.Activity) {
1477
+ this.activity = object;
1478
+ this.verification.subject.id = object.id == null ? null : new URL(object.id.href);
1479
+ }
1480
+ }
1481
+ };
1482
+ constructor(inbox) {
1483
+ this.inbox = inbox;
1484
+ }
1485
+ /** Run an ingress operation and project telemetry; fetch owns completion. */
1486
+ async run(operation, span) {
1487
+ let response;
1488
+ let outcome;
1489
+ let threw = false;
1490
+ let originalError;
1491
+ try {
1492
+ response = await operation();
1493
+ outcome = {
1494
+ type: "response",
1495
+ status: response.status,
1496
+ ...this.result
1497
+ };
1498
+ } catch (error) {
1499
+ threw = true;
1500
+ this.hasException = true;
1501
+ originalError = error;
1502
+ outcome = {
1503
+ type: "exception",
1504
+ stage: this.stage,
1505
+ error
1506
+ };
1507
+ }
1508
+ if (span != null) this.project(span, outcome);
1509
+ if (threw) throw originalError;
1510
+ return response;
1511
+ }
1512
+ /** Complete standalone handler calls that have no enclosing fetch boundary. */
1513
+ async runAndFinish(getContext, handler, operation) {
1514
+ let response;
1515
+ try {
1516
+ response = await this.run(operation);
1517
+ } catch (error) {
1518
+ await this.finish(getContext(), handler, {
1519
+ type: "exception",
1520
+ stage: this.stage,
1521
+ error
1522
+ });
1523
+ throw error;
1524
+ }
1525
+ await this.finish(getContext(), handler, {
1526
+ type: "response",
1527
+ status: response.status,
1528
+ ...this.result
1529
+ });
1530
+ return response;
1531
+ }
1532
+ project(span, outcome) {
1533
+ span.setAttribute("activitypub.authentication.status", this.authentication.status);
1534
+ if (this.authentication.status === "rejected") span.setAttribute("activitypub.verification.failure_reason", this.authentication.reason.type);
1535
+ span.setAttribute("activitypub.inbox.disposition", outcome?.type === "exception" || this.hasException ? "exception" : this.result.disposition);
1536
+ }
1537
+ async finish(context, handler, outcome) {
1538
+ if (handler == null) return;
1539
+ const report = {
1540
+ inbox: this.inbox,
1541
+ payload: this.payload,
1542
+ activity: this.activity,
1543
+ attempts: this.verification.attempts,
1544
+ authentication: this.authentication,
1545
+ outcome
1546
+ };
1547
+ try {
1548
+ await handler(context, report);
1549
+ } catch (error) {
1550
+ try {
1551
+ (0, _logtape_logtape.getLogger)([
1552
+ "fedify",
1553
+ "federation",
1554
+ "inbox"
1555
+ ]).error("An unexpected error occurred in inbox request finished handler:\n{error}", { error });
1556
+ } catch {}
1557
+ }
1558
+ }
1559
+ };
1560
+ //#endregion
1450
1561
  //#region src/sig/compound-proof.ts
1451
1562
  const textEncoder = new TextEncoder();
1452
1563
  /**
@@ -1881,6 +1992,26 @@ function findUnsupportedCompoundProofShape(json) {
1881
1992
  return findInJsonMaps(json, (map, path) => Object.hasOwn(map, "proof") && !isJsonMap$1(map.proof) ? childPath(path, "proof") : void 0) ?? null;
1882
1993
  }
1883
1994
  /**
1995
+ * Finds the first embedded map that carries a direct `proof` but not its own
1996
+ * `@context`. The map-local compound-proof profile verifies each proof-bearing
1997
+ * map as a self-contained document, so the inbox rejects such a map with
1998
+ * `missingContext` whatever its proof. An object embedded with its captured
1999
+ * signed representation, e.g., by {@link signObject}, always keeps its own
2000
+ * `@context`; one rebuilt under its parent's context, e.g., an object parsed
2001
+ * from a received document, never does, and its proof no longer covers what
2002
+ * it has become anyway.
2003
+ *
2004
+ * The input must be a finite, already-materialized JSON tree; see
2005
+ * {@link containsCompoundPortableObject}.
2006
+ *
2007
+ * @returns The RFC 6901 JSON Pointer to the map, or `null` if every embedded
2008
+ * proof-bearing map has its own `@context`.
2009
+ * @internal
2010
+ */
2011
+ function findEmbeddedProofWithoutContext(json) {
2012
+ return findInJsonMaps(json, (map, path) => path !== "" && Object.hasOwn(map, "proof") && !Object.hasOwn(map, "@context") ? path : void 0) ?? null;
2013
+ }
2014
+ /**
1884
2015
  * Discovers direct literal proof-bearing maps in an immutable JSON snapshot.
1885
2016
  *
1886
2017
  * Discovery is bounded and atomic. Unsupported proof shapes, non-JSON input,
@@ -1935,7 +2066,23 @@ async function verifyDiscoveredProofDocuments(discovered, options) {
1935
2066
  verified: false,
1936
2067
  reason: { type: "missingContext" }
1937
2068
  });
1938
- const key = await require_http.verifyMapLocalProof(document.securedDocument, options);
2069
+ const inherited = options[require_http.verificationObservation];
2070
+ let subjectId = null;
2071
+ if (inherited != null && document.id != null) try {
2072
+ subjectId = (0, _fedify_vocab_runtime.parseIri)(document.id);
2073
+ } catch {}
2074
+ const key = await require_http.observeAttempt({ [require_http.verificationObservation]: inherited == null ? void 0 : {
2075
+ ...inherited,
2076
+ attempt: void 0,
2077
+ check: void 0,
2078
+ subject: {
2079
+ id: subjectId,
2080
+ pointer: document.path
2081
+ }
2082
+ } }, "objectIntegrity", (observation) => require_http.verifyMapLocalProof(document.securedDocument, {
2083
+ ...options,
2084
+ [require_http.verificationObservation]: inherited == null ? void 0 : observation
2085
+ }), (key) => key != null);
1939
2086
  return key == null ? Object.freeze({
1940
2087
  ...metadata,
1941
2088
  verified: false,
@@ -4579,13 +4726,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
4579
4726
  * @param parameters The parameters for handling the collection.
4580
4727
  * @returns A promise that resolves to an HTTP response.
4581
4728
  */
4582
- async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, portable, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
4583
- const spanName = name$1.trim().replace(/\s+/g, "_");
4729
+ async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, portable, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
4730
+ const spanName = name$2.trim().replace(/\s+/g, "_");
4584
4731
  tracerProvider = tracerProvider ?? _opentelemetry_api.trace.getTracerProvider();
4585
4732
  const tracer = tracerProvider.getTracer(require_http.name, require_http.version);
4586
4733
  const cursor = new URL(request.url).searchParams.get("cursor");
4587
4734
  const metricBase = {
4588
- kind: getCollectionMetricKind(name$1),
4735
+ kind: getCollectionMetricKind(name$2),
4589
4736
  page: cursor != null,
4590
4737
  dispatcher: "built_in"
4591
4738
  };
@@ -4631,7 +4778,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
4631
4778
  span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
4632
4779
  return await onNotFound(request);
4633
4780
  }
4634
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
4781
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
4635
4782
  itemCount = items.length;
4636
4783
  span.setAttribute("fedify.collection.items", itemCount);
4637
4784
  return items;
@@ -4650,7 +4797,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
4650
4797
  collection = new _fedify_vocab.OrderedCollection({
4651
4798
  id: baseUri,
4652
4799
  attribution,
4653
- totalItems: totalItemCount ?? null,
4800
+ totalItems: totalItemCount ?? (portable == null ? null : itemsOrResponse.length),
4654
4801
  items: itemsOrResponse
4655
4802
  });
4656
4803
  } else {
@@ -4673,7 +4820,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
4673
4820
  } else {
4674
4821
  const uri = new URL(portable == null ? baseUri : pageBase);
4675
4822
  uri.searchParams.set("cursor", cursor);
4676
- const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
4823
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
4677
4824
  kind: _opentelemetry_api.SpanKind.SERVER,
4678
4825
  attributes: {
4679
4826
  "activitypub.collection.id": uri.href,
@@ -4689,7 +4836,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
4689
4836
  span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
4690
4837
  return await onNotFound(request);
4691
4838
  }
4692
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
4839
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
4693
4840
  itemCount = items.length;
4694
4841
  span.setAttribute("fedify.collection.items", itemCount);
4695
4842
  return {
@@ -5170,6 +5317,16 @@ async function handleMediaUpload(request, { identifier, context: ctx, mediaUploa
5170
5317
  * @returns A promise that resolves to an HTTP response.
5171
5318
  */
5172
5319
  async function handleInbox(request, options) {
5320
+ if (options.observation == null) {
5321
+ const observation = new InboxObservation({
5322
+ kind: options.portableInbox != null ? "portable" : options.recipient == null ? "shared" : "personal",
5323
+ recipient: options.recipient
5324
+ });
5325
+ return await observation.runAndFinish(() => options.context, options.inboxRequestFinishedHandler, () => handleInbox(request, {
5326
+ ...options,
5327
+ observation
5328
+ }));
5329
+ }
5173
5330
  return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(require_http.name, require_http.version).startActiveSpan("activitypub.inbox", {
5174
5331
  kind: options.queue == null ? _opentelemetry_api.SpanKind.SERVER : _opentelemetry_api.SpanKind.PRODUCER,
5175
5332
  attributes: { "activitypub.shared_inbox": options.recipient == null }
@@ -5178,12 +5335,14 @@ async function handleInbox(request, options) {
5178
5335
  try {
5179
5336
  return await handleInboxInternal(request, options, span);
5180
5337
  } catch (e) {
5338
+ options.observation.hasException = true;
5181
5339
  span.setStatus({
5182
5340
  code: _opentelemetry_api.SpanStatusCode.ERROR,
5183
5341
  message: String(e)
5184
5342
  });
5185
5343
  throw e;
5186
5344
  } finally {
5345
+ options.observation.project(span);
5187
5346
  span.end();
5188
5347
  }
5189
5348
  });
@@ -5198,6 +5357,8 @@ async function handleInbox(request, options) {
5198
5357
  */
5199
5358
  async function handleInboxInternal(request, parameters, span) {
5200
5359
  const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, maxHttpSignatures, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider, portableInbox } = parameters;
5360
+ const observation = parameters.observation;
5361
+ const signatureObservation = { [require_http.verificationObservation]: observation.verification };
5201
5362
  const logger = (0, _logtape_logtape.getLogger)([
5202
5363
  "fedify",
5203
5364
  "federation",
@@ -5222,6 +5383,10 @@ async function handleInboxInternal(request, parameters, span) {
5222
5383
  }
5223
5384
  }
5224
5385
  if (request.bodyUsed) {
5386
+ observation.result = {
5387
+ disposition: "failed",
5388
+ reason: "bodyUnavailable"
5389
+ };
5225
5390
  logger.error("Request body has already been read.", { recipient });
5226
5391
  span.setStatus({
5227
5392
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -5232,6 +5397,10 @@ async function handleInboxInternal(request, parameters, span) {
5232
5397
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5233
5398
  });
5234
5399
  } else if (request.body?.locked) {
5400
+ observation.result = {
5401
+ disposition: "failed",
5402
+ reason: "bodyUnavailable"
5403
+ };
5235
5404
  logger.error("Request body is locked.", { recipient });
5236
5405
  span.setStatus({
5237
5406
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -5242,11 +5411,20 @@ async function handleInboxInternal(request, parameters, span) {
5242
5411
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5243
5412
  });
5244
5413
  }
5414
+ observation.stage = "parse";
5415
+ observation.result = {
5416
+ disposition: "rejected",
5417
+ reason: "invalidJson"
5418
+ };
5245
5419
  let json;
5246
5420
  try {
5247
5421
  json = JSON.parse(await require_types.readBoundedText(request.clone(), require_types.MAX_BODY_SIZE, request.url));
5248
5422
  } catch (error) {
5249
5423
  if (error instanceof require_types.BodyTooLargeError) {
5424
+ observation.result = {
5425
+ disposition: "rejected",
5426
+ reason: "bodyTooLarge"
5427
+ };
5250
5428
  request.body?.cancel(error).catch(() => {});
5251
5429
  span.setStatus({
5252
5430
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -5276,6 +5454,15 @@ async function handleInboxInternal(request, parameters, span) {
5276
5454
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5277
5455
  });
5278
5456
  }
5457
+ observation.payload = {
5458
+ status: "parsed",
5459
+ value: structuredClone(json)
5460
+ };
5461
+ observation.result = {
5462
+ disposition: "rejected",
5463
+ reason: "invalidActivity"
5464
+ };
5465
+ observation.stage = "verify";
5279
5466
  const keyCache = new KvKeyCache(kv, kvPrefixes.publicKey, {
5280
5467
  documentLoader: ctx.documentLoader,
5281
5468
  contextLoader: ctx.contextLoader,
@@ -5305,6 +5492,10 @@ async function handleInboxInternal(request, parameters, span) {
5305
5492
  code: _opentelemetry_api.SpanStatusCode.ERROR,
5306
5493
  message: `Failed to parse activity:\n${error}`
5307
5494
  });
5495
+ observation.result = {
5496
+ disposition: "rejected",
5497
+ reason: "invalidActivity"
5498
+ };
5308
5499
  return new Response("Invalid activity.", {
5309
5500
  status: 400,
5310
5501
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5322,11 +5513,25 @@ async function handleInboxInternal(request, parameters, span) {
5322
5513
  recipient,
5323
5514
  error
5324
5515
  });
5516
+ observation.result = {
5517
+ disposition: "rejected",
5518
+ reason: "invalidJsonLd"
5519
+ };
5325
5520
  return new Response("Invalid JSON-LD.", {
5326
5521
  status: 400,
5327
5522
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5328
5523
  });
5329
5524
  }
5525
+ observation.verification.attempts.push({
5526
+ mechanism: "linkedData",
5527
+ subject: {
5528
+ id: null,
5529
+ pointer: ""
5530
+ },
5531
+ checks: [],
5532
+ status: "error",
5533
+ error
5534
+ });
5330
5535
  if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
5331
5536
  if (!skipSignatureVerification) deferredLdSignatureError = error;
5332
5537
  logger.debug("Failed to normalize JSON-LD for Linked Data Signatures; deferring to another authentication path only if it verifies:\n{error}", {
@@ -5338,6 +5543,7 @@ async function handleInboxInternal(request, parameters, span) {
5338
5543
  compactedJsonWithoutSig = require_http.detachSignature(compactedJson);
5339
5544
  try {
5340
5545
  ldSigVerified = await require_http.verifyCompactJsonLd(compactedJson, {
5546
+ ...signatureObservation,
5341
5547
  contextLoader: ctx.contextLoader,
5342
5548
  documentLoader: ctx.documentLoader,
5343
5549
  keyCache,
@@ -5351,6 +5557,10 @@ async function handleInboxInternal(request, parameters, span) {
5351
5557
  recipient,
5352
5558
  error
5353
5559
  });
5560
+ observation.result = {
5561
+ disposition: "rejected",
5562
+ reason: "invalidJsonLd"
5563
+ };
5354
5564
  return new Response("Invalid JSON-LD.", {
5355
5565
  status: 400,
5356
5566
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5370,6 +5580,10 @@ async function handleInboxInternal(request, parameters, span) {
5370
5580
  recipient,
5371
5581
  error: parseError
5372
5582
  });
5583
+ observation.result = {
5584
+ disposition: "rejected",
5585
+ reason: "invalidJsonLd"
5586
+ };
5373
5587
  return new Response("Invalid JSON-LD.", {
5374
5588
  status: 400,
5375
5589
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5390,7 +5604,7 @@ async function handleInboxInternal(request, parameters, span) {
5390
5604
  json
5391
5605
  });
5392
5606
  try {
5393
- activity = await _fedify_vocab.Activity.fromJsonLd(compactedJsonWithoutSig, {
5607
+ activity = observation.activity = await _fedify_vocab.Activity.fromJsonLd(compactedJsonWithoutSig, {
5394
5608
  ...ctx,
5395
5609
  contextLoader: require_http.getNormalizationContextLoader(ctx.contextLoader)
5396
5610
  });
@@ -5402,6 +5616,7 @@ async function handleInboxInternal(request, parameters, span) {
5402
5616
  activityVerified = true;
5403
5617
  if (!skipSignatureVerification && isPortableActivity(activity)) try {
5404
5618
  proofVerified = await require_http.verifyObject(_fedify_vocab.Activity, jsonWithoutSig, {
5619
+ ...signatureObservation,
5405
5620
  contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader),
5406
5621
  documentLoader: ctx.documentLoader,
5407
5622
  keyCache,
@@ -5422,6 +5637,7 @@ async function handleInboxInternal(request, parameters, span) {
5422
5637
  });
5423
5638
  try {
5424
5639
  activity = await require_http.verifyObject(_fedify_vocab.Activity, jsonWithoutSig, {
5640
+ ...signatureObservation,
5425
5641
  contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader),
5426
5642
  documentLoader: ctx.documentLoader,
5427
5643
  keyCache,
@@ -5457,6 +5673,10 @@ async function handleInboxInternal(request, parameters, span) {
5457
5673
  code: _opentelemetry_api.SpanStatusCode.ERROR,
5458
5674
  message: `Failed to parse activity:\n${error}`
5459
5675
  });
5676
+ observation.result = {
5677
+ disposition: "rejected",
5678
+ reason: "invalidActivity"
5679
+ };
5460
5680
  return new Response("Invalid activity.", {
5461
5681
  status: 400,
5462
5682
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5480,6 +5700,7 @@ async function handleInboxInternal(request, parameters, span) {
5480
5700
  if (activity == null) {
5481
5701
  if (!skipSignatureVerification) {
5482
5702
  const verification = await require_http.verifyRequestDetailed(request, {
5703
+ ...signatureObservation,
5483
5704
  contextLoader: ctx.contextLoader,
5484
5705
  documentLoader: ctx.documentLoader,
5485
5706
  timeWindow: signatureTimeWindow,
@@ -5490,6 +5711,14 @@ async function handleInboxInternal(request, parameters, span) {
5490
5711
  });
5491
5712
  if (verification.verified === false) {
5492
5713
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
5714
+ observation.authentication = {
5715
+ status: "rejected",
5716
+ reason: { type: "verificationFailed" }
5717
+ };
5718
+ observation.result = {
5719
+ disposition: "rejected",
5720
+ reason: "authentication"
5721
+ };
5493
5722
  const reason = verification.reason;
5494
5723
  const remoteHost = "keyId" in reason && reason.keyId != null ? require_http.getRemoteHost(reason.keyId) : void 0;
5495
5724
  require_http.getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure(reason.type, remoteHost);
@@ -5504,7 +5733,7 @@ async function handleInboxInternal(request, parameters, span) {
5504
5733
  });
5505
5734
  if (unverifiedActivityHandler == null) return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
5506
5735
  try {
5507
- activity = await _fedify_vocab.Activity.fromJsonLd(jsonWithoutSig, ctx);
5736
+ activity = observation.activity = await _fedify_vocab.Activity.fromJsonLd(jsonWithoutSig, ctx);
5508
5737
  } catch (error) {
5509
5738
  logger.error("Failed to parse activity:\n{error}", {
5510
5739
  recipient,
@@ -5520,6 +5749,10 @@ async function handleInboxInternal(request, parameters, span) {
5520
5749
  recipient
5521
5750
  });
5522
5751
  }
5752
+ observation.result = {
5753
+ disposition: "rejected",
5754
+ reason: "invalidActivity"
5755
+ };
5523
5756
  return new Response("Invalid activity.", {
5524
5757
  status: 400,
5525
5758
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5542,6 +5775,11 @@ async function handleInboxInternal(request, parameters, span) {
5542
5775
  try {
5543
5776
  response = await unverifiedActivityHandler(ctx, activity, reason);
5544
5777
  } catch (error) {
5778
+ observation.result = {
5779
+ disposition: "failed",
5780
+ reason: "listenerError",
5781
+ error
5782
+ };
5545
5783
  logger.error("An unexpected error occurred in unverified activity handler:\n{error}", {
5546
5784
  error,
5547
5785
  activity: json,
@@ -5558,7 +5796,10 @@ async function handleInboxInternal(request, parameters, span) {
5558
5796
  }
5559
5797
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
5560
5798
  }
5561
- if (response instanceof Response) return response;
5799
+ if (response instanceof Response) {
5800
+ observation.result = { disposition: "customResponse" };
5801
+ return response;
5802
+ }
5562
5803
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
5563
5804
  } else {
5564
5805
  if (inboxChallengePolicy?.enabled && inboxChallengePolicy.requestNonce) pendingNonceLabel = verification.signatureLabel;
@@ -5568,7 +5809,7 @@ async function handleInboxInternal(request, parameters, span) {
5568
5809
  httpSigKey = verification.key;
5569
5810
  }
5570
5811
  try {
5571
- activity = await _fedify_vocab.Activity.fromJsonLd(jsonWithoutSig, {
5812
+ activity = observation.activity = await _fedify_vocab.Activity.fromJsonLd(jsonWithoutSig, {
5572
5813
  ...ctx,
5573
5814
  contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader)
5574
5815
  });
@@ -5577,6 +5818,12 @@ async function handleInboxInternal(request, parameters, span) {
5577
5818
  return await respondInvalidActivity(error);
5578
5819
  }
5579
5820
  }
5821
+ observation.activity = activity;
5822
+ observation.stage = "policy";
5823
+ observation.result = {
5824
+ disposition: "rejected",
5825
+ reason: "authentication"
5826
+ };
5580
5827
  if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
5581
5828
  span.setAttribute("activitypub.activity.type", (0, _fedify_vocab.getTypeId)(activity).href);
5582
5829
  span.addEvent("activitypub.activity.received", {
@@ -5588,6 +5835,13 @@ async function handleInboxInternal(request, parameters, span) {
5588
5835
  });
5589
5836
  if (!skipSignatureVerification && !proofVerified && (isPortableActivity(activity) || isPortableActorKey(httpSigKey))) {
5590
5837
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
5838
+ observation.authentication = {
5839
+ status: "rejected",
5840
+ reason: {
5841
+ type: "proofPolicy",
5842
+ policy: "portableActor"
5843
+ }
5844
+ };
5591
5845
  logger.error("The activity {activityId} of the portable actor {actorId} is not authenticated by a valid Object Integrity Proof.", {
5592
5846
  activity: json,
5593
5847
  recipient,
@@ -5605,20 +5859,36 @@ async function handleInboxInternal(request, parameters, span) {
5605
5859
  });
5606
5860
  }
5607
5861
  if (!skipSignatureVerification && activity.id != null && require_http.isPortableId(activity.id)) {
5608
- const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
5609
- contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader),
5610
- documentLoader: ctx.documentLoader,
5611
- keyCache,
5612
- meterProvider,
5613
- tracerProvider
5614
- });
5862
+ const rejection = await require_http.observeAttempt(signatureObservation, "objectIntegrity", async (verification) => {
5863
+ const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
5864
+ [require_http.verificationObservation]: verification,
5865
+ contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader),
5866
+ documentLoader: ctx.documentLoader,
5867
+ keyCache,
5868
+ meterProvider,
5869
+ tracerProvider
5870
+ });
5871
+ if (rejection != null) verification.attempt.reason = {
5872
+ type: "proofPolicy",
5873
+ reason: rejection
5874
+ };
5875
+ return rejection;
5876
+ }, (value) => value == null);
5615
5877
  if (rejection != null) {
5616
5878
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
5879
+ observation.authentication = {
5880
+ status: "rejected",
5881
+ reason: {
5882
+ type: "proofPolicy",
5883
+ policy: "portableActivity",
5884
+ detail: rejection
5885
+ }
5886
+ };
5617
5887
  logger.error("The portable activity {activityId} is not authenticated by an Object Integrity Proof of its own DID: {reason}", {
5618
5888
  activity: json,
5619
5889
  recipient,
5620
5890
  activityId: activity.id.href,
5621
- reason: rejection
5891
+ reason: rejection.type
5622
5892
  });
5623
5893
  span.setStatus({
5624
5894
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -5632,6 +5902,14 @@ async function handleInboxInternal(request, parameters, span) {
5632
5902
  }
5633
5903
  if (httpSigKey != null && !await require_portable_collection.doesActorOwnKey(activity, httpSigKey, ctx)) {
5634
5904
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
5905
+ observation.authentication = {
5906
+ status: "rejected",
5907
+ reason: {
5908
+ type: "actorKeyMismatch",
5909
+ key: require_http.snapshotKey(httpSigKey),
5910
+ actorIds: activity.actorIds.map((id) => new URL(id.href))
5911
+ }
5912
+ };
5635
5913
  require_http.getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : require_http.getRemoteHost(httpSigKey.id));
5636
5914
  logger.error("The signer ({keyId}) and the actor ({actorId}) do not match.", {
5637
5915
  activity: json,
@@ -5651,13 +5929,35 @@ async function handleInboxInternal(request, parameters, span) {
5651
5929
  if (!skipSignatureVerification) {
5652
5930
  const compoundJson = Array.isArray(json) ? json : jsonWithoutSig;
5653
5931
  if (inspectCompoundPortableObjectApplicability(compoundJson, INBOX_COMPOUND_PROOF_LIMITS) !== "absent") {
5654
- const compoundProof = await verifyCompoundPortableObjectProofs(compoundJson, INBOX_COMPOUND_PROOF_LIMITS, {
5655
- documentLoader: ctx.documentLoader,
5656
- keyCache,
5657
- meterProvider,
5658
- tracerProvider
5659
- });
5932
+ const compoundProof = await require_http.observeAttempt(signatureObservation, "objectIntegrity", async (verification) => {
5933
+ const result = await verifyCompoundPortableObjectProofs(compoundJson, INBOX_COMPOUND_PROOF_LIMITS, {
5934
+ [require_http.verificationObservation]: verification,
5935
+ documentLoader: ctx.documentLoader,
5936
+ keyCache,
5937
+ meterProvider,
5938
+ tracerProvider
5939
+ });
5940
+ if (result.status === "unsupported") verification.attempt.reason = {
5941
+ type: "proofPolicy",
5942
+ reason: result.reason
5943
+ };
5944
+ else if (!result.verified) {
5945
+ const failed = result.portableObjects.find((object) => !object.verified);
5946
+ verification.attempt.reason = {
5947
+ type: "proofPolicy",
5948
+ reason: failed?.verified === false ? failed.reason : { type: "invalidProof" }
5949
+ };
5950
+ }
5951
+ return result;
5952
+ }, (result) => result.status === "ok" && result.verified);
5660
5953
  if (compoundProof.status !== "ok" || !compoundProof.verified) {
5954
+ observation.authentication = {
5955
+ status: "rejected",
5956
+ reason: {
5957
+ type: "proofPolicy",
5958
+ policy: "compound"
5959
+ }
5960
+ };
5661
5961
  logger.error("Failed to verify compound portable Object Integrity Proofs.", {
5662
5962
  recipient,
5663
5963
  activity: json,
@@ -5676,11 +5976,20 @@ async function handleInboxInternal(request, parameters, span) {
5676
5976
  }
5677
5977
  if (pendingNonceLabel != null) {
5678
5978
  if (!await verifySignatureNonce(request, kv, kvPrefixes.acceptSignatureNonce, pendingNonceLabel)) {
5979
+ observation.authentication = {
5980
+ status: "rejected",
5981
+ reason: { type: "invalidNonce" }
5982
+ };
5679
5983
  require_http.getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("invalidNonce", httpSigKey?.id == null ? void 0 : require_http.getRemoteHost(httpSigKey.id));
5680
5984
  logger.error("Signature nonce verification failed (missing, expired, or replayed).", { recipient });
5681
5985
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
5682
5986
  }
5683
5987
  }
5988
+ observation.authentication = skipSignatureVerification ? { status: "skipped" } : {
5989
+ status: "verified",
5990
+ attempts: observation.verification.attempts.filter((attempt) => attempt.status === "verified")
5991
+ };
5992
+ observation.stage = "dispatch";
5684
5993
  const routeResult = await routeActivity({
5685
5994
  context: ctx,
5686
5995
  json,
@@ -5692,7 +6001,14 @@ async function handleInboxInternal(request, parameters, span) {
5692
6001
  inboxListeners,
5693
6002
  inboxContextFactory,
5694
6003
  listenerInboxContextFactory: ldSigVerified ? inboxContextFactory[rawInboxContextFactorySymbol] : void 0,
5695
- inboxErrorHandler,
6004
+ inboxErrorHandler: async (context, error) => {
6005
+ observation.result = {
6006
+ disposition: "failed",
6007
+ reason: "listenerError",
6008
+ error
6009
+ };
6010
+ await inboxErrorHandler?.(context, error);
6011
+ },
5696
6012
  kv,
5697
6013
  kvPrefixes,
5698
6014
  queue,
@@ -5728,30 +6044,44 @@ async function handleInboxInternal(request, parameters, span) {
5728
6044
  error
5729
6045
  });
5730
6046
  }
5731
- if (routeResult === "alreadyProcessed") return new Response(`Activity <${activity.id}> has already been processed.`, {
5732
- status: 202,
5733
- headers: { "Content-Type": "text/plain; charset=utf-8" }
5734
- });
5735
- else if (routeResult === "missingActor") return new Response("Missing actor.", {
5736
- status: 400,
5737
- headers: { "Content-Type": "text/plain; charset=utf-8" }
5738
- });
5739
- else if (routeResult === "enqueued") return new Response("Activity is enqueued.", {
5740
- status: 202,
5741
- headers: { "Content-Type": "text/plain; charset=utf-8" }
5742
- });
5743
- else if (routeResult === "unsupportedActivity") return new Response("", {
5744
- status: 202,
5745
- headers: { "Content-Type": "text/plain; charset=utf-8" }
5746
- });
5747
- else if (routeResult === "error") return new Response("Internal server error.", {
6047
+ if (routeResult === "alreadyProcessed") {
6048
+ observation.result = { disposition: "duplicate" };
6049
+ return new Response(`Activity <${activity.id}> has already been processed.`, {
6050
+ status: 202,
6051
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6052
+ });
6053
+ } else if (routeResult === "missingActor") {
6054
+ observation.result = {
6055
+ disposition: "rejected",
6056
+ reason: "missingActor"
6057
+ };
6058
+ return new Response("Missing actor.", {
6059
+ status: 400,
6060
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6061
+ });
6062
+ } else if (routeResult === "enqueued") {
6063
+ observation.result = { disposition: "enqueued" };
6064
+ return new Response("Activity is enqueued.", {
6065
+ status: 202,
6066
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6067
+ });
6068
+ } else if (routeResult === "unsupportedActivity") {
6069
+ observation.result = { disposition: "unhandled" };
6070
+ return new Response("", {
6071
+ status: 202,
6072
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6073
+ });
6074
+ } else if (routeResult === "error") return new Response("Internal server error.", {
5748
6075
  status: 500,
5749
6076
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5750
6077
  });
5751
- else return new Response("", {
5752
- status: 202,
5753
- headers: { "Content-Type": "text/plain; charset=utf-8" }
5754
- });
6078
+ else {
6079
+ observation.result = { disposition: "processed" };
6080
+ return new Response("", {
6081
+ status: 202,
6082
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6083
+ });
6084
+ }
5755
6085
  }
5756
6086
  const PORTABLE_INBOX_FORWARDABLE_RESULTS = /* @__PURE__ */ new Set([
5757
6087
  "success",
@@ -5880,8 +6210,8 @@ var CustomCollectionHandler = class {
5880
6210
  * @param CollectionPage The CollectionPage constructor.
5881
6211
  * @param filterPredicate Optional filter predicate for items.
5882
6212
  */
5883
- constructor(name$2, values, context, callbacks, tracerProvider = _opentelemetry_api.trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
5884
- this.name = name$2;
6213
+ constructor(name$1, values, context, callbacks, tracerProvider = _opentelemetry_api.trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
6214
+ this.name = name$1;
5885
6215
  this.values = values;
5886
6216
  this.context = context;
5887
6217
  this.callbacks = callbacks;
@@ -6417,19 +6747,19 @@ function isPortableActivity(activity) {
6417
6747
  */
6418
6748
  async function verifyPortableActivityId(activityId, jsonLd, options) {
6419
6749
  const expectedId = require_http.getCanonicalPortableId(activityId);
6420
- if (expectedId == null) return "malformed portable ID";
6750
+ if (expectedId == null) return { type: "invalidObjectId" };
6421
6751
  let verification;
6422
6752
  try {
6423
6753
  verification = await require_http.verifyPortableObjectProofWithRoot(jsonLd, options);
6424
6754
  } catch (error) {
6425
- if (error instanceof require_http.InvalidPortableObjectIdError) return "malformed portable ID";
6755
+ if (error instanceof require_http.InvalidPortableObjectIdError) return { type: "invalidObjectId" };
6426
6756
  if (!isPermanentActivityParseError(error)) throw error;
6427
- return "malformed document";
6757
+ return { type: "invalidDocument" };
6428
6758
  }
6429
6759
  const { result, root } = verification;
6430
- if (!result.verified) return result.reason.type;
6760
+ if (!result.verified) return result.reason;
6431
6761
  const rootId = root?.["@id"];
6432
- if (typeof rootId !== "string" || require_http.getCanonicalPortableId((0, _fedify_vocab_runtime.parseIri)(rootId)) !== expectedId) return "the verified document has another ID";
6762
+ if (typeof rootId !== "string" || require_http.getCanonicalPortableId((0, _fedify_vocab_runtime.parseIri)(rootId)) !== expectedId) return { type: "subjectMismatch" };
6433
6763
  return null;
6434
6764
  }
6435
6765
  //#endregion
@@ -6978,7 +7308,10 @@ function selectPortableActivityKey(activity, activityId, keys) {
6978
7308
  /**
6979
7309
  * Rejects an outgoing activity whose proofs Fedify's own inbox would reject
6980
7310
  * as unsupported: a document with portable objects in which some map carries
6981
- * a proof set or another value that is not a single proof map.
7311
+ * a proof set or another value that is not a single proof map, or in which
7312
+ * an embedded map carries a proof but not its own `@context`, e.g., a signed
7313
+ * object parsed from a received document and rebuilt under the activity's
7314
+ * context.
6982
7315
  *
6983
7316
  * @param jsonLd The compact JSON-LD document about to be delivered.
6984
7317
  * @param activityId The activity ID, for the error message.
@@ -6988,8 +7321,9 @@ function selectPortableActivityKey(activity, activityId, keys) {
6988
7321
  function assertSupportedCompoundProofShape(jsonLd, activityId) {
6989
7322
  if (!containsCompoundPortableObject(jsonLd)) return;
6990
7323
  const path = findUnsupportedCompoundProofShape(jsonLd);
6991
- if (path == null) return;
6992
- 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.`);
7324
+ 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.`);
7325
+ const embedded = findEmbeddedProofWithoutContext(jsonLd);
7326
+ 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().`);
6993
7327
  }
6994
7328
  //#endregion
6995
7329
  //#region src/federation/retry.ts
@@ -9409,7 +9743,7 @@ var FederationImpl = class extends FederationBuilderImpl {
9409
9743
  return await (0, _logtape_logtape.withContext)({
9410
9744
  traceId: spanCtx.traceId,
9411
9745
  spanId: spanCtx.spanId
9412
- }, async () => {
9746
+ }, () => this.#observeInboxFetch(metricState, async () => {
9413
9747
  const logger = (0, _logtape_logtape.getLogger)([
9414
9748
  "fedify",
9415
9749
  "federation",
@@ -9424,8 +9758,14 @@ var FederationImpl = class extends FederationBuilderImpl {
9424
9758
  tracer,
9425
9759
  metricState
9426
9760
  });
9761
+ if (metricState.inboxCompletion != null) metricState.inboxCompletion.observation.stage = "respond";
9427
9762
  if (metricState.endpoint !== "hashlink_media" && acceptsJsonLd(request)) response.headers.set("Vary", "Accept");
9428
9763
  } catch (error) {
9764
+ metricState.inboxCompletion?.observation.project(span, {
9765
+ type: "exception",
9766
+ stage: metricState.inboxCompletion.observation.stage,
9767
+ error
9768
+ });
9429
9769
  this.metrics.recordHttpServerRequest(request.method, metricState.endpoint ?? "error", require_http.getDurationMs(metricStart), { routeTemplate: metricState.routeTemplate });
9430
9770
  span.setStatus({
9431
9771
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -9464,7 +9804,7 @@ var FederationImpl = class extends FederationBuilderImpl {
9464
9804
  else if (response.status >= 400) logger.warn(logTpl, values);
9465
9805
  else logger.info(logTpl, values);
9466
9806
  return response;
9467
- });
9807
+ }));
9468
9808
  });
9469
9809
  });
9470
9810
  }
@@ -9506,8 +9846,17 @@ var FederationImpl = class extends FederationBuilderImpl {
9506
9846
  metricState.routeTemplate = route.template;
9507
9847
  metricState.endpoint = getEndpointCategory(route.name);
9508
9848
  span.updateName(`${request.method} ${route.template}`);
9509
- let context = this.#createContext(request, contextData);
9510
9849
  const routeName = route.name.replace(/:.*$/, "");
9850
+ let context;
9851
+ try {
9852
+ context = this.#createContext(request, contextData);
9853
+ } catch (error) {
9854
+ if (request.method !== "POST" || routeName !== "inbox" && routeName !== "sharedInbox") throw error;
9855
+ return await this.#reportInboxPreparationError(request, contextData, {
9856
+ kind: routeName === "inbox" ? "personal" : "shared",
9857
+ recipient: route.values.identifier ?? null
9858
+ }, error, span, metricState);
9859
+ }
9511
9860
  switch (routeName) {
9512
9861
  case "webfinger": return await handleWebFinger(request, {
9513
9862
  context,
@@ -9566,6 +9915,28 @@ var FederationImpl = class extends FederationBuilderImpl {
9566
9915
  });
9567
9916
  return response;
9568
9917
  }
9918
+ if (request.method === "POST" && (routeName === "inbox" || routeName === "sharedInbox")) {
9919
+ const recipient = route.values.identifier ?? null;
9920
+ const observation = new InboxObservation({
9921
+ kind: routeName === "inbox" ? "personal" : "shared",
9922
+ recipient
9923
+ });
9924
+ this.#setInboxCompletion(metricState, observation, () => context);
9925
+ return await observation.run(async () => {
9926
+ if (routeName === "inbox") context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: recipient }) });
9927
+ else if (this.sharedInboxKeyDispatcher != null) {
9928
+ const identity = await this.sharedInboxKeyDispatcher(context);
9929
+ if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
9930
+ }
9931
+ return await this.#handleInbox(request, {
9932
+ recipient,
9933
+ context,
9934
+ contextData,
9935
+ onNotFound,
9936
+ observation
9937
+ });
9938
+ }, span);
9939
+ }
9569
9940
  switch (routeName) {
9570
9941
  case "actor":
9571
9942
  case "actorAlias": {
@@ -9641,9 +10012,9 @@ var FederationImpl = class extends FederationBuilderImpl {
9641
10012
  onUnauthorized,
9642
10013
  onNotFound
9643
10014
  });
9644
- context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
10015
+ throw new Error("Unreachable inbox delivery.");
9645
10016
  case "sharedInbox":
9646
- if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
10017
+ if (this.sharedInboxKeyDispatcher != null) {
9647
10018
  const identity = await this.sharedInboxKeyDispatcher(context);
9648
10019
  if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
9649
10020
  }
@@ -9936,7 +10307,49 @@ var FederationImpl = class extends FederationBuilderImpl {
9936
10307
  }
9937
10308
  return await onNotFound(request);
9938
10309
  }
9939
- async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox }) {
10310
+ #setInboxCompletion(metricState, observation, getContext) {
10311
+ const handler = this.inboxRequestFinishedHandler;
10312
+ metricState.inboxCompletion = {
10313
+ observation,
10314
+ finish: (outcome) => observation.finish(getContext(), handler, outcome)
10315
+ };
10316
+ }
10317
+ async #observeInboxFetch(metricState, operation) {
10318
+ let response;
10319
+ try {
10320
+ response = await operation();
10321
+ } catch (error) {
10322
+ const completion = metricState.inboxCompletion;
10323
+ if (completion != null) await completion.finish({
10324
+ type: "exception",
10325
+ stage: completion.observation.stage,
10326
+ error
10327
+ });
10328
+ throw error;
10329
+ }
10330
+ const completion = metricState.inboxCompletion;
10331
+ if (completion != null) await completion.finish({
10332
+ type: "response",
10333
+ status: response.status,
10334
+ ...completion.observation.result
10335
+ });
10336
+ return response;
10337
+ }
10338
+ async #reportInboxPreparationError(request, contextData, inbox, error, span, metricState) {
10339
+ const unavailableLoader = () => Promise.reject(error);
10340
+ const context = new RequestContextImpl({
10341
+ url: new URL(request.url),
10342
+ request,
10343
+ federation: this,
10344
+ data: contextData,
10345
+ documentLoader: unavailableLoader,
10346
+ contextLoader: unavailableLoader
10347
+ });
10348
+ const observation = new InboxObservation(inbox);
10349
+ this.#setInboxCompletion(metricState, observation, () => context);
10350
+ return await observation.run(() => Promise.reject(error), span);
10351
+ }
10352
+ async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox, observation }) {
9940
10353
  if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
9941
10354
  const inboxContextFactory = context.toInboxContext.bind(context);
9942
10355
  inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
@@ -9960,7 +10373,8 @@ var FederationImpl = class extends FederationBuilderImpl {
9960
10373
  meterProvider: this.meterProvider,
9961
10374
  tracerProvider: this.tracerProvider,
9962
10375
  idempotencyStrategy: this.idempotencyStrategy,
9963
- portableInbox
10376
+ portableInbox,
10377
+ observation
9964
10378
  });
9965
10379
  }
9966
10380
  async #fetchPortableInbox(request, portable, { onNotFound, contextData, span, metricState }) {
@@ -9989,63 +10403,79 @@ var FederationImpl = class extends FederationBuilderImpl {
9989
10403
  metricState.endpoint = "inbox";
9990
10404
  span.updateName(`${request.method} ${metricState.routeTemplate}`);
9991
10405
  const identifier = route.values.identifier;
9992
- let context = this.#createContext(request, contextData);
9993
- const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
9994
- const localOrigin = context.canonicalOrigin;
9995
- const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof _fedify_vocab.Tombstone ? null : actor, {
9996
- authority: portable.portableRequest.authority,
9997
- canonicalInboxId: portable.canonicalId,
9998
- localOrigin
9999
- });
10000
- if (resolution.status === "rejected") {
10001
- logger.debug("Not accepting a delivery to the portable inbox {inbox} on behalf of the actor {identifier}: {reason}.", {
10002
- inbox: portable.canonicalId,
10003
- identifier,
10004
- reason: resolution.reason
10005
- });
10006
- return await onNotFound(request);
10007
- }
10008
- const { recipient } = resolution;
10009
- if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
10010
- const excludedOrigins = [localOrigin, new URL(request.url).origin];
10011
- return await this.#handleInbox(request, {
10012
- recipient: identifier,
10013
- context,
10014
- contextData,
10015
- onNotFound,
10016
- portableInbox: {
10017
- recipient,
10018
- forward: async (activity, activityId, activityType, signatures) => {
10019
- if (this.portableInboxForwarding.maxTargets < 1) return;
10020
- if (this.kv.cas == null) {
10021
- if (!this.#portableInboxForwardingWarned) {
10022
- this.#portableInboxForwardingWarned = true;
10023
- 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.");
10406
+ let context;
10407
+ try {
10408
+ context = this.#createContext(request, contextData);
10409
+ } catch (error) {
10410
+ return await this.#reportInboxPreparationError(request, contextData, {
10411
+ kind: "portable",
10412
+ recipient: identifier
10413
+ }, error, span, metricState);
10414
+ }
10415
+ const observation = new InboxObservation({
10416
+ kind: "portable",
10417
+ recipient: identifier
10418
+ });
10419
+ this.#setInboxCompletion(metricState, observation, () => context);
10420
+ return await observation.run(async () => {
10421
+ const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
10422
+ const localOrigin = context.canonicalOrigin;
10423
+ const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof _fedify_vocab.Tombstone ? null : actor, {
10424
+ authority: portable.portableRequest.authority,
10425
+ canonicalInboxId: portable.canonicalId,
10426
+ localOrigin
10427
+ });
10428
+ if (resolution.status === "rejected") {
10429
+ logger.debug("Not accepting a delivery to the portable inbox {inbox} on behalf of the actor {identifier}: {reason}.", {
10430
+ inbox: portable.canonicalId,
10431
+ identifier,
10432
+ reason: resolution.reason
10433
+ });
10434
+ return await onNotFound(request);
10435
+ }
10436
+ const { recipient } = resolution;
10437
+ if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
10438
+ const excludedOrigins = [localOrigin, new URL(request.url).origin];
10439
+ return await this.#handleInbox(request, {
10440
+ recipient: identifier,
10441
+ observation,
10442
+ context,
10443
+ contextData,
10444
+ onNotFound,
10445
+ portableInbox: {
10446
+ recipient,
10447
+ forward: async (activity, activityId, activityType, signatures) => {
10448
+ if (this.portableInboxForwarding.maxTargets < 1) return;
10449
+ if (this.kv.cas == null) {
10450
+ if (!this.#portableInboxForwardingWarned) {
10451
+ this.#portableInboxForwardingWarned = true;
10452
+ 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.");
10453
+ }
10454
+ return;
10024
10455
  }
10025
- return;
10456
+ await forwardPortableInboxActivity({
10457
+ recipient,
10458
+ activity,
10459
+ activityId,
10460
+ activityType,
10461
+ excludedOrigins,
10462
+ baseUrl: context.origin,
10463
+ kv: this.kv,
10464
+ kvPrefix: this.kvPrefixes.portableInboxForwarding,
10465
+ outboxQueue: this.outboxQueue,
10466
+ startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
10467
+ allowPrivateAddress: this.allowPrivateAddress,
10468
+ getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
10469
+ specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
10470
+ signatures,
10471
+ options: this.portableInboxForwarding,
10472
+ meterProvider: this.meterProvider,
10473
+ tracerProvider: this.tracerProvider
10474
+ });
10026
10475
  }
10027
- await forwardPortableInboxActivity({
10028
- recipient,
10029
- activity,
10030
- activityId,
10031
- activityType,
10032
- excludedOrigins,
10033
- baseUrl: context.origin,
10034
- kv: this.kv,
10035
- kvPrefix: this.kvPrefixes.portableInboxForwarding,
10036
- outboxQueue: this.outboxQueue,
10037
- startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
10038
- allowPrivateAddress: this.allowPrivateAddress,
10039
- getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
10040
- specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
10041
- signatures,
10042
- options: this.portableInboxForwarding,
10043
- meterProvider: this.meterProvider,
10044
- tracerProvider: this.tracerProvider
10045
- });
10046
10476
  }
10047
- }
10048
- });
10477
+ });
10478
+ }, span);
10049
10479
  }
10050
10480
  /**
10051
10481
  * Gets this server's gateway key pairs for the portable actor that owns