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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/{builder-DvSQqk2l.mjs → builder-CBNa1Uw2.mjs} +8 -2
  2. package/dist/compat/mod.d.cts +1 -1
  3. package/dist/compat/mod.d.ts +1 -1
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{compound-proof-5bg_j_ye.mjs → compound-proof-CMPv4UK0.mjs} +20 -3
  6. package/dist/{context-CtI307_A.d.ts → context-7NyiqIcg.d.ts} +111 -2
  7. package/dist/{context-B6X-t8gI.d.cts → context-tsYPJ2Ew.d.cts} +111 -2
  8. package/dist/{deno-AmcRMq5z.mjs → deno-C8XaXmPB.mjs} +1 -1
  9. package/dist/{docloader-CIP_SG39.mjs → docloader-CpdFLwAg.mjs} +2 -2
  10. package/dist/federation/builder.test.mjs +1 -1
  11. package/dist/federation/compound-fanout.test.mjs +3 -3
  12. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  13. package/dist/federation/handler.test.mjs +76 -5
  14. package/dist/federation/hashlink-media.test.mjs +2 -2
  15. package/dist/federation/idempotency.test.mjs +2 -2
  16. package/dist/federation/inbox-report.test.d.mts +2 -0
  17. package/dist/federation/inbox-report.test.mjs +475 -0
  18. package/dist/federation/metrics.test.mjs +1 -1
  19. package/dist/federation/middleware.test.mjs +7 -7
  20. package/dist/federation/mod.cjs +1 -1
  21. package/dist/federation/mod.d.cts +4 -3
  22. package/dist/federation/mod.d.ts +4 -3
  23. package/dist/federation/mod.js +1 -1
  24. package/dist/federation/outgoing-proof.test.mjs +5 -5
  25. package/dist/federation/portable-actor.test.mjs +3 -3
  26. package/dist/federation/portable-chain.test.mjs +3 -3
  27. package/dist/federation/portable-collection.test.mjs +4 -4
  28. package/dist/federation/portable-delivery.test.mjs +2 -2
  29. package/dist/federation/portable-gateway-keys.test.mjs +4 -4
  30. package/dist/federation/portable-inbox.test.mjs +67 -6
  31. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  32. package/dist/federation/portable-route-activity.test.mjs +2 -2
  33. package/dist/federation/portable.test.mjs +3 -3
  34. package/dist/federation/send.test.mjs +3 -3
  35. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  36. package/dist/federation/tasks/tasks.test.mjs +2 -2
  37. package/dist/federation/temporal.test.mjs +1 -1
  38. package/dist/federation/webfinger.test.mjs +1 -1
  39. package/dist/{federation-BDzIgqOd.cjs → federation-D6giSGUT.cjs} +524 -118
  40. package/dist/{http-DkqWu-sm.mjs → http-BqSbUEiQ.mjs} +141 -98
  41. package/dist/{http-1fZ83jj7.cjs → http-Cki-npuA.cjs} +447 -110
  42. package/dist/{http-BGb3QioZ.js → http-OMtKlbx1.js} +430 -111
  43. package/dist/{http-BrwKINQn.d.cts → key-C1nv7_jD.d.ts} +638 -229
  44. package/dist/{http-DGx0KOws.d.ts → key-DYxuyd-s.d.cts} +638 -229
  45. package/dist/{key-CEsu7oaD.mjs → key-DecAtABm.mjs} +30 -3
  46. package/dist/{kv-cache-CJS4mImS.mjs → kv-cache-CWWlMv-s.mjs} +1 -1
  47. package/dist/{kv-cache-BEWuZSB4.cjs → kv-cache-D73yVcE5.cjs} +1 -1
  48. package/dist/{kv-cache-CGhEgt98.js → kv-cache-J0v3UbUn.js} +1 -1
  49. package/dist/{ld-Cf4CPWN_.mjs → ld-UsN-TIPh.mjs} +40 -5
  50. package/dist/{metrics-CocsZP0E.mjs → metrics-DWCn4VXX.mjs} +2 -1
  51. package/dist/{middleware-BfYke3YQ.js → middleware-BqzfIlUO.js} +524 -118
  52. package/dist/{middleware-Dxn0E9ES.mjs → middleware-CkjwiEBd.mjs} +512 -127
  53. package/dist/{middleware-Cne_jMZo.mjs → middleware-DHn2q0L1.mjs} +1 -1
  54. package/dist/{mod-C7_2j7VT.d.cts → mod-BojP2Bw3.d.cts} +1 -1
  55. package/dist/{mod-BlLwewYe.d.ts → mod-CavhAgLw.d.cts} +5 -255
  56. package/dist/{mod-VHYh1lTK.d.cts → mod-CtpF2BU5.d.ts} +5 -255
  57. package/dist/{mod-B8MI1mpi.d.ts → mod-DEZXwV6J.d.ts} +1 -1
  58. package/dist/{mod-DFL6IxYQ.d.cts → mod-Dw-nPxQh.d.cts} +1 -1
  59. package/dist/{mod-BhTaDR5H.d.ts → mod-Xj5P4s4d.d.ts} +1 -1
  60. package/dist/mod.cjs +4 -4
  61. package/dist/mod.d.cts +7 -7
  62. package/dist/mod.d.ts +7 -7
  63. package/dist/mod.js +4 -4
  64. package/dist/nodeinfo/handler.test.mjs +1 -1
  65. package/dist/{owner-DSJ7nFgy.mjs → owner-CEBn81LD.mjs} +3 -3
  66. package/dist/{portable-collection-r-mHg1PV.cjs → portable-collection-DqTUBKY7.cjs} +1 -1
  67. package/dist/{portable-collection-BuBV6xTo.js → portable-collection-ZkUNjtb7.js} +1 -1
  68. package/dist/{portable-collection-qskqXJrH.mjs → portable-collection-qFSYw4TG.mjs} +2 -2
  69. package/dist/{portable-key-woGH_1nn.mjs → portable-key-LlMkP-vz.mjs} +1 -1
  70. package/dist/{proof-8WiH_-Gq.mjs → proof-BHRf_jMG.mjs} +133 -16
  71. package/dist/{send-JKzZBgPU.mjs → send-DjG8DDU_.mjs} +3 -3
  72. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  73. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  74. package/dist/sig/compound-proof-verification.test.mjs +19 -2
  75. package/dist/sig/compound-proof.test.mjs +2 -2
  76. package/dist/sig/http.test.mjs +2 -2
  77. package/dist/sig/key.test.mjs +1 -1
  78. package/dist/sig/ld.test.mjs +2 -2
  79. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  80. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  81. package/dist/sig/mod.cjs +2 -2
  82. package/dist/sig/mod.d.cts +2 -2
  83. package/dist/sig/mod.d.ts +2 -2
  84. package/dist/sig/mod.js +2 -2
  85. package/dist/sig/owner.test.mjs +1 -1
  86. package/dist/sig/portable-collection.test.mjs +1 -1
  87. package/dist/sig/portable-interop.test.mjs +3 -3
  88. package/dist/sig/portable-key.test.mjs +6 -6
  89. package/dist/sig/proof.test.mjs +76 -3
  90. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  91. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  92. package/dist/sig/verification.test.d.mts +2 -0
  93. package/dist/sig/verification.test.mjs +310 -0
  94. package/dist/{temporal-CT3fRaMH.mjs → temporal-BRFH_dte.mjs} +1 -1
  95. package/dist/testing/kv.test.mjs +1 -1
  96. package/dist/testing/mod.d.mts +315 -28
  97. package/dist/utils/docloader.test.mjs +2 -2
  98. package/dist/utils/kv-cache.test.mjs +1 -1
  99. package/dist/utils/mod.cjs +1 -1
  100. package/dist/utils/mod.d.cts +1 -1
  101. package/dist/utils/mod.d.ts +1 -1
  102. package/dist/utils/mod.js +1 -1
  103. package/dist/verification-C3954hQh.mjs +5 -0
  104. package/dist/verification-Dpfcw8ml.mjs +105 -0
  105. package/package.json +7 -7
@@ -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-Cki-npuA.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-DqTUBKY7.cjs");
7
+ const require_kv_cache = require("./kv-cache-D73yVcE5.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
  /**
@@ -1935,7 +2046,23 @@ async function verifyDiscoveredProofDocuments(discovered, options) {
1935
2046
  verified: false,
1936
2047
  reason: { type: "missingContext" }
1937
2048
  });
1938
- const key = await require_http.verifyMapLocalProof(document.securedDocument, options);
2049
+ const inherited = options[require_http.verificationObservation];
2050
+ let subjectId = null;
2051
+ if (inherited != null && document.id != null) try {
2052
+ subjectId = (0, _fedify_vocab_runtime.parseIri)(document.id);
2053
+ } catch {}
2054
+ const key = await require_http.observeAttempt({ [require_http.verificationObservation]: inherited == null ? void 0 : {
2055
+ ...inherited,
2056
+ attempt: void 0,
2057
+ check: void 0,
2058
+ subject: {
2059
+ id: subjectId,
2060
+ pointer: document.path
2061
+ }
2062
+ } }, "objectIntegrity", (observation) => require_http.verifyMapLocalProof(document.securedDocument, {
2063
+ ...options,
2064
+ [require_http.verificationObservation]: inherited == null ? void 0 : observation
2065
+ }), (key) => key != null);
1939
2066
  return key == null ? Object.freeze({
1940
2067
  ...metadata,
1941
2068
  verified: false,
@@ -4579,13 +4706,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
4579
4706
  * @param parameters The parameters for handling the collection.
4580
4707
  * @returns A promise that resolves to an HTTP response.
4581
4708
  */
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, "_");
4709
+ async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, portable, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
4710
+ const spanName = name$2.trim().replace(/\s+/g, "_");
4584
4711
  tracerProvider = tracerProvider ?? _opentelemetry_api.trace.getTracerProvider();
4585
4712
  const tracer = tracerProvider.getTracer(require_http.name, require_http.version);
4586
4713
  const cursor = new URL(request.url).searchParams.get("cursor");
4587
4714
  const metricBase = {
4588
- kind: getCollectionMetricKind(name$1),
4715
+ kind: getCollectionMetricKind(name$2),
4589
4716
  page: cursor != null,
4590
4717
  dispatcher: "built_in"
4591
4718
  };
@@ -4631,7 +4758,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
4631
4758
  span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
4632
4759
  return await onNotFound(request);
4633
4760
  }
4634
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
4761
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
4635
4762
  itemCount = items.length;
4636
4763
  span.setAttribute("fedify.collection.items", itemCount);
4637
4764
  return items;
@@ -4673,7 +4800,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
4673
4800
  } else {
4674
4801
  const uri = new URL(portable == null ? baseUri : pageBase);
4675
4802
  uri.searchParams.set("cursor", cursor);
4676
- const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
4803
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
4677
4804
  kind: _opentelemetry_api.SpanKind.SERVER,
4678
4805
  attributes: {
4679
4806
  "activitypub.collection.id": uri.href,
@@ -4689,7 +4816,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
4689
4816
  span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
4690
4817
  return await onNotFound(request);
4691
4818
  }
4692
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
4819
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
4693
4820
  itemCount = items.length;
4694
4821
  span.setAttribute("fedify.collection.items", itemCount);
4695
4822
  return {
@@ -5170,6 +5297,16 @@ async function handleMediaUpload(request, { identifier, context: ctx, mediaUploa
5170
5297
  * @returns A promise that resolves to an HTTP response.
5171
5298
  */
5172
5299
  async function handleInbox(request, options) {
5300
+ if (options.observation == null) {
5301
+ const observation = new InboxObservation({
5302
+ kind: options.portableInbox != null ? "portable" : options.recipient == null ? "shared" : "personal",
5303
+ recipient: options.recipient
5304
+ });
5305
+ return await observation.runAndFinish(() => options.context, options.inboxRequestFinishedHandler, () => handleInbox(request, {
5306
+ ...options,
5307
+ observation
5308
+ }));
5309
+ }
5173
5310
  return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(require_http.name, require_http.version).startActiveSpan("activitypub.inbox", {
5174
5311
  kind: options.queue == null ? _opentelemetry_api.SpanKind.SERVER : _opentelemetry_api.SpanKind.PRODUCER,
5175
5312
  attributes: { "activitypub.shared_inbox": options.recipient == null }
@@ -5178,12 +5315,14 @@ async function handleInbox(request, options) {
5178
5315
  try {
5179
5316
  return await handleInboxInternal(request, options, span);
5180
5317
  } catch (e) {
5318
+ options.observation.hasException = true;
5181
5319
  span.setStatus({
5182
5320
  code: _opentelemetry_api.SpanStatusCode.ERROR,
5183
5321
  message: String(e)
5184
5322
  });
5185
5323
  throw e;
5186
5324
  } finally {
5325
+ options.observation.project(span);
5187
5326
  span.end();
5188
5327
  }
5189
5328
  });
@@ -5198,6 +5337,8 @@ async function handleInbox(request, options) {
5198
5337
  */
5199
5338
  async function handleInboxInternal(request, parameters, span) {
5200
5339
  const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, maxHttpSignatures, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider, portableInbox } = parameters;
5340
+ const observation = parameters.observation;
5341
+ const signatureObservation = { [require_http.verificationObservation]: observation.verification };
5201
5342
  const logger = (0, _logtape_logtape.getLogger)([
5202
5343
  "fedify",
5203
5344
  "federation",
@@ -5222,6 +5363,10 @@ async function handleInboxInternal(request, parameters, span) {
5222
5363
  }
5223
5364
  }
5224
5365
  if (request.bodyUsed) {
5366
+ observation.result = {
5367
+ disposition: "failed",
5368
+ reason: "bodyUnavailable"
5369
+ };
5225
5370
  logger.error("Request body has already been read.", { recipient });
5226
5371
  span.setStatus({
5227
5372
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -5232,6 +5377,10 @@ async function handleInboxInternal(request, parameters, span) {
5232
5377
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5233
5378
  });
5234
5379
  } else if (request.body?.locked) {
5380
+ observation.result = {
5381
+ disposition: "failed",
5382
+ reason: "bodyUnavailable"
5383
+ };
5235
5384
  logger.error("Request body is locked.", { recipient });
5236
5385
  span.setStatus({
5237
5386
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -5242,11 +5391,20 @@ async function handleInboxInternal(request, parameters, span) {
5242
5391
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5243
5392
  });
5244
5393
  }
5394
+ observation.stage = "parse";
5395
+ observation.result = {
5396
+ disposition: "rejected",
5397
+ reason: "invalidJson"
5398
+ };
5245
5399
  let json;
5246
5400
  try {
5247
5401
  json = JSON.parse(await require_types.readBoundedText(request.clone(), require_types.MAX_BODY_SIZE, request.url));
5248
5402
  } catch (error) {
5249
5403
  if (error instanceof require_types.BodyTooLargeError) {
5404
+ observation.result = {
5405
+ disposition: "rejected",
5406
+ reason: "bodyTooLarge"
5407
+ };
5250
5408
  request.body?.cancel(error).catch(() => {});
5251
5409
  span.setStatus({
5252
5410
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -5276,6 +5434,15 @@ async function handleInboxInternal(request, parameters, span) {
5276
5434
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5277
5435
  });
5278
5436
  }
5437
+ observation.payload = {
5438
+ status: "parsed",
5439
+ value: structuredClone(json)
5440
+ };
5441
+ observation.result = {
5442
+ disposition: "rejected",
5443
+ reason: "invalidActivity"
5444
+ };
5445
+ observation.stage = "verify";
5279
5446
  const keyCache = new KvKeyCache(kv, kvPrefixes.publicKey, {
5280
5447
  documentLoader: ctx.documentLoader,
5281
5448
  contextLoader: ctx.contextLoader,
@@ -5305,6 +5472,10 @@ async function handleInboxInternal(request, parameters, span) {
5305
5472
  code: _opentelemetry_api.SpanStatusCode.ERROR,
5306
5473
  message: `Failed to parse activity:\n${error}`
5307
5474
  });
5475
+ observation.result = {
5476
+ disposition: "rejected",
5477
+ reason: "invalidActivity"
5478
+ };
5308
5479
  return new Response("Invalid activity.", {
5309
5480
  status: 400,
5310
5481
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5322,11 +5493,25 @@ async function handleInboxInternal(request, parameters, span) {
5322
5493
  recipient,
5323
5494
  error
5324
5495
  });
5496
+ observation.result = {
5497
+ disposition: "rejected",
5498
+ reason: "invalidJsonLd"
5499
+ };
5325
5500
  return new Response("Invalid JSON-LD.", {
5326
5501
  status: 400,
5327
5502
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5328
5503
  });
5329
5504
  }
5505
+ observation.verification.attempts.push({
5506
+ mechanism: "linkedData",
5507
+ subject: {
5508
+ id: null,
5509
+ pointer: ""
5510
+ },
5511
+ checks: [],
5512
+ status: "error",
5513
+ error
5514
+ });
5330
5515
  if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
5331
5516
  if (!skipSignatureVerification) deferredLdSignatureError = error;
5332
5517
  logger.debug("Failed to normalize JSON-LD for Linked Data Signatures; deferring to another authentication path only if it verifies:\n{error}", {
@@ -5338,6 +5523,7 @@ async function handleInboxInternal(request, parameters, span) {
5338
5523
  compactedJsonWithoutSig = require_http.detachSignature(compactedJson);
5339
5524
  try {
5340
5525
  ldSigVerified = await require_http.verifyCompactJsonLd(compactedJson, {
5526
+ ...signatureObservation,
5341
5527
  contextLoader: ctx.contextLoader,
5342
5528
  documentLoader: ctx.documentLoader,
5343
5529
  keyCache,
@@ -5351,6 +5537,10 @@ async function handleInboxInternal(request, parameters, span) {
5351
5537
  recipient,
5352
5538
  error
5353
5539
  });
5540
+ observation.result = {
5541
+ disposition: "rejected",
5542
+ reason: "invalidJsonLd"
5543
+ };
5354
5544
  return new Response("Invalid JSON-LD.", {
5355
5545
  status: 400,
5356
5546
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5370,6 +5560,10 @@ async function handleInboxInternal(request, parameters, span) {
5370
5560
  recipient,
5371
5561
  error: parseError
5372
5562
  });
5563
+ observation.result = {
5564
+ disposition: "rejected",
5565
+ reason: "invalidJsonLd"
5566
+ };
5373
5567
  return new Response("Invalid JSON-LD.", {
5374
5568
  status: 400,
5375
5569
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5390,7 +5584,7 @@ async function handleInboxInternal(request, parameters, span) {
5390
5584
  json
5391
5585
  });
5392
5586
  try {
5393
- activity = await _fedify_vocab.Activity.fromJsonLd(compactedJsonWithoutSig, {
5587
+ activity = observation.activity = await _fedify_vocab.Activity.fromJsonLd(compactedJsonWithoutSig, {
5394
5588
  ...ctx,
5395
5589
  contextLoader: require_http.getNormalizationContextLoader(ctx.contextLoader)
5396
5590
  });
@@ -5402,6 +5596,7 @@ async function handleInboxInternal(request, parameters, span) {
5402
5596
  activityVerified = true;
5403
5597
  if (!skipSignatureVerification && isPortableActivity(activity)) try {
5404
5598
  proofVerified = await require_http.verifyObject(_fedify_vocab.Activity, jsonWithoutSig, {
5599
+ ...signatureObservation,
5405
5600
  contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader),
5406
5601
  documentLoader: ctx.documentLoader,
5407
5602
  keyCache,
@@ -5422,6 +5617,7 @@ async function handleInboxInternal(request, parameters, span) {
5422
5617
  });
5423
5618
  try {
5424
5619
  activity = await require_http.verifyObject(_fedify_vocab.Activity, jsonWithoutSig, {
5620
+ ...signatureObservation,
5425
5621
  contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader),
5426
5622
  documentLoader: ctx.documentLoader,
5427
5623
  keyCache,
@@ -5457,6 +5653,10 @@ async function handleInboxInternal(request, parameters, span) {
5457
5653
  code: _opentelemetry_api.SpanStatusCode.ERROR,
5458
5654
  message: `Failed to parse activity:\n${error}`
5459
5655
  });
5656
+ observation.result = {
5657
+ disposition: "rejected",
5658
+ reason: "invalidActivity"
5659
+ };
5460
5660
  return new Response("Invalid activity.", {
5461
5661
  status: 400,
5462
5662
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5480,6 +5680,7 @@ async function handleInboxInternal(request, parameters, span) {
5480
5680
  if (activity == null) {
5481
5681
  if (!skipSignatureVerification) {
5482
5682
  const verification = await require_http.verifyRequestDetailed(request, {
5683
+ ...signatureObservation,
5483
5684
  contextLoader: ctx.contextLoader,
5484
5685
  documentLoader: ctx.documentLoader,
5485
5686
  timeWindow: signatureTimeWindow,
@@ -5490,6 +5691,14 @@ async function handleInboxInternal(request, parameters, span) {
5490
5691
  });
5491
5692
  if (verification.verified === false) {
5492
5693
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
5694
+ observation.authentication = {
5695
+ status: "rejected",
5696
+ reason: { type: "verificationFailed" }
5697
+ };
5698
+ observation.result = {
5699
+ disposition: "rejected",
5700
+ reason: "authentication"
5701
+ };
5493
5702
  const reason = verification.reason;
5494
5703
  const remoteHost = "keyId" in reason && reason.keyId != null ? require_http.getRemoteHost(reason.keyId) : void 0;
5495
5704
  require_http.getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure(reason.type, remoteHost);
@@ -5504,7 +5713,7 @@ async function handleInboxInternal(request, parameters, span) {
5504
5713
  });
5505
5714
  if (unverifiedActivityHandler == null) return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
5506
5715
  try {
5507
- activity = await _fedify_vocab.Activity.fromJsonLd(jsonWithoutSig, ctx);
5716
+ activity = observation.activity = await _fedify_vocab.Activity.fromJsonLd(jsonWithoutSig, ctx);
5508
5717
  } catch (error) {
5509
5718
  logger.error("Failed to parse activity:\n{error}", {
5510
5719
  recipient,
@@ -5520,6 +5729,10 @@ async function handleInboxInternal(request, parameters, span) {
5520
5729
  recipient
5521
5730
  });
5522
5731
  }
5732
+ observation.result = {
5733
+ disposition: "rejected",
5734
+ reason: "invalidActivity"
5735
+ };
5523
5736
  return new Response("Invalid activity.", {
5524
5737
  status: 400,
5525
5738
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -5542,6 +5755,11 @@ async function handleInboxInternal(request, parameters, span) {
5542
5755
  try {
5543
5756
  response = await unverifiedActivityHandler(ctx, activity, reason);
5544
5757
  } catch (error) {
5758
+ observation.result = {
5759
+ disposition: "failed",
5760
+ reason: "listenerError",
5761
+ error
5762
+ };
5545
5763
  logger.error("An unexpected error occurred in unverified activity handler:\n{error}", {
5546
5764
  error,
5547
5765
  activity: json,
@@ -5558,7 +5776,10 @@ async function handleInboxInternal(request, parameters, span) {
5558
5776
  }
5559
5777
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
5560
5778
  }
5561
- if (response instanceof Response) return response;
5779
+ if (response instanceof Response) {
5780
+ observation.result = { disposition: "customResponse" };
5781
+ return response;
5782
+ }
5562
5783
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
5563
5784
  } else {
5564
5785
  if (inboxChallengePolicy?.enabled && inboxChallengePolicy.requestNonce) pendingNonceLabel = verification.signatureLabel;
@@ -5568,7 +5789,7 @@ async function handleInboxInternal(request, parameters, span) {
5568
5789
  httpSigKey = verification.key;
5569
5790
  }
5570
5791
  try {
5571
- activity = await _fedify_vocab.Activity.fromJsonLd(jsonWithoutSig, {
5792
+ activity = observation.activity = await _fedify_vocab.Activity.fromJsonLd(jsonWithoutSig, {
5572
5793
  ...ctx,
5573
5794
  contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader)
5574
5795
  });
@@ -5577,6 +5798,12 @@ async function handleInboxInternal(request, parameters, span) {
5577
5798
  return await respondInvalidActivity(error);
5578
5799
  }
5579
5800
  }
5801
+ observation.activity = activity;
5802
+ observation.stage = "policy";
5803
+ observation.result = {
5804
+ disposition: "rejected",
5805
+ reason: "authentication"
5806
+ };
5580
5807
  if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
5581
5808
  span.setAttribute("activitypub.activity.type", (0, _fedify_vocab.getTypeId)(activity).href);
5582
5809
  span.addEvent("activitypub.activity.received", {
@@ -5588,6 +5815,13 @@ async function handleInboxInternal(request, parameters, span) {
5588
5815
  });
5589
5816
  if (!skipSignatureVerification && !proofVerified && (isPortableActivity(activity) || isPortableActorKey(httpSigKey))) {
5590
5817
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
5818
+ observation.authentication = {
5819
+ status: "rejected",
5820
+ reason: {
5821
+ type: "proofPolicy",
5822
+ policy: "portableActor"
5823
+ }
5824
+ };
5591
5825
  logger.error("The activity {activityId} of the portable actor {actorId} is not authenticated by a valid Object Integrity Proof.", {
5592
5826
  activity: json,
5593
5827
  recipient,
@@ -5605,20 +5839,36 @@ async function handleInboxInternal(request, parameters, span) {
5605
5839
  });
5606
5840
  }
5607
5841
  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
- });
5842
+ const rejection = await require_http.observeAttempt(signatureObservation, "objectIntegrity", async (verification) => {
5843
+ const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
5844
+ [require_http.verificationObservation]: verification,
5845
+ contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader),
5846
+ documentLoader: ctx.documentLoader,
5847
+ keyCache,
5848
+ meterProvider,
5849
+ tracerProvider
5850
+ });
5851
+ if (rejection != null) verification.attempt.reason = {
5852
+ type: "proofPolicy",
5853
+ reason: rejection
5854
+ };
5855
+ return rejection;
5856
+ }, (value) => value == null);
5615
5857
  if (rejection != null) {
5616
5858
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
5859
+ observation.authentication = {
5860
+ status: "rejected",
5861
+ reason: {
5862
+ type: "proofPolicy",
5863
+ policy: "portableActivity",
5864
+ detail: rejection
5865
+ }
5866
+ };
5617
5867
  logger.error("The portable activity {activityId} is not authenticated by an Object Integrity Proof of its own DID: {reason}", {
5618
5868
  activity: json,
5619
5869
  recipient,
5620
5870
  activityId: activity.id.href,
5621
- reason: rejection
5871
+ reason: rejection.type
5622
5872
  });
5623
5873
  span.setStatus({
5624
5874
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -5632,6 +5882,14 @@ async function handleInboxInternal(request, parameters, span) {
5632
5882
  }
5633
5883
  if (httpSigKey != null && !await require_portable_collection.doesActorOwnKey(activity, httpSigKey, ctx)) {
5634
5884
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
5885
+ observation.authentication = {
5886
+ status: "rejected",
5887
+ reason: {
5888
+ type: "actorKeyMismatch",
5889
+ key: require_http.snapshotKey(httpSigKey),
5890
+ actorIds: activity.actorIds.map((id) => new URL(id.href))
5891
+ }
5892
+ };
5635
5893
  require_http.getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : require_http.getRemoteHost(httpSigKey.id));
5636
5894
  logger.error("The signer ({keyId}) and the actor ({actorId}) do not match.", {
5637
5895
  activity: json,
@@ -5651,13 +5909,35 @@ async function handleInboxInternal(request, parameters, span) {
5651
5909
  if (!skipSignatureVerification) {
5652
5910
  const compoundJson = Array.isArray(json) ? json : jsonWithoutSig;
5653
5911
  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
- });
5912
+ const compoundProof = await require_http.observeAttempt(signatureObservation, "objectIntegrity", async (verification) => {
5913
+ const result = await verifyCompoundPortableObjectProofs(compoundJson, INBOX_COMPOUND_PROOF_LIMITS, {
5914
+ [require_http.verificationObservation]: verification,
5915
+ documentLoader: ctx.documentLoader,
5916
+ keyCache,
5917
+ meterProvider,
5918
+ tracerProvider
5919
+ });
5920
+ if (result.status === "unsupported") verification.attempt.reason = {
5921
+ type: "proofPolicy",
5922
+ reason: result.reason
5923
+ };
5924
+ else if (!result.verified) {
5925
+ const failed = result.portableObjects.find((object) => !object.verified);
5926
+ verification.attempt.reason = {
5927
+ type: "proofPolicy",
5928
+ reason: failed?.verified === false ? failed.reason : { type: "invalidProof" }
5929
+ };
5930
+ }
5931
+ return result;
5932
+ }, (result) => result.status === "ok" && result.verified);
5660
5933
  if (compoundProof.status !== "ok" || !compoundProof.verified) {
5934
+ observation.authentication = {
5935
+ status: "rejected",
5936
+ reason: {
5937
+ type: "proofPolicy",
5938
+ policy: "compound"
5939
+ }
5940
+ };
5661
5941
  logger.error("Failed to verify compound portable Object Integrity Proofs.", {
5662
5942
  recipient,
5663
5943
  activity: json,
@@ -5676,11 +5956,20 @@ async function handleInboxInternal(request, parameters, span) {
5676
5956
  }
5677
5957
  if (pendingNonceLabel != null) {
5678
5958
  if (!await verifySignatureNonce(request, kv, kvPrefixes.acceptSignatureNonce, pendingNonceLabel)) {
5959
+ observation.authentication = {
5960
+ status: "rejected",
5961
+ reason: { type: "invalidNonce" }
5962
+ };
5679
5963
  require_http.getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("invalidNonce", httpSigKey?.id == null ? void 0 : require_http.getRemoteHost(httpSigKey.id));
5680
5964
  logger.error("Signature nonce verification failed (missing, expired, or replayed).", { recipient });
5681
5965
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
5682
5966
  }
5683
5967
  }
5968
+ observation.authentication = skipSignatureVerification ? { status: "skipped" } : {
5969
+ status: "verified",
5970
+ attempts: observation.verification.attempts.filter((attempt) => attempt.status === "verified")
5971
+ };
5972
+ observation.stage = "dispatch";
5684
5973
  const routeResult = await routeActivity({
5685
5974
  context: ctx,
5686
5975
  json,
@@ -5692,7 +5981,14 @@ async function handleInboxInternal(request, parameters, span) {
5692
5981
  inboxListeners,
5693
5982
  inboxContextFactory,
5694
5983
  listenerInboxContextFactory: ldSigVerified ? inboxContextFactory[rawInboxContextFactorySymbol] : void 0,
5695
- inboxErrorHandler,
5984
+ inboxErrorHandler: async (context, error) => {
5985
+ observation.result = {
5986
+ disposition: "failed",
5987
+ reason: "listenerError",
5988
+ error
5989
+ };
5990
+ await inboxErrorHandler?.(context, error);
5991
+ },
5696
5992
  kv,
5697
5993
  kvPrefixes,
5698
5994
  queue,
@@ -5728,30 +6024,44 @@ async function handleInboxInternal(request, parameters, span) {
5728
6024
  error
5729
6025
  });
5730
6026
  }
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.", {
6027
+ if (routeResult === "alreadyProcessed") {
6028
+ observation.result = { disposition: "duplicate" };
6029
+ return new Response(`Activity <${activity.id}> has already been processed.`, {
6030
+ status: 202,
6031
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6032
+ });
6033
+ } else if (routeResult === "missingActor") {
6034
+ observation.result = {
6035
+ disposition: "rejected",
6036
+ reason: "missingActor"
6037
+ };
6038
+ return new Response("Missing actor.", {
6039
+ status: 400,
6040
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6041
+ });
6042
+ } else if (routeResult === "enqueued") {
6043
+ observation.result = { disposition: "enqueued" };
6044
+ return new Response("Activity is enqueued.", {
6045
+ status: 202,
6046
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6047
+ });
6048
+ } else if (routeResult === "unsupportedActivity") {
6049
+ observation.result = { disposition: "unhandled" };
6050
+ return new Response("", {
6051
+ status: 202,
6052
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6053
+ });
6054
+ } else if (routeResult === "error") return new Response("Internal server error.", {
5748
6055
  status: 500,
5749
6056
  headers: { "Content-Type": "text/plain; charset=utf-8" }
5750
6057
  });
5751
- else return new Response("", {
5752
- status: 202,
5753
- headers: { "Content-Type": "text/plain; charset=utf-8" }
5754
- });
6058
+ else {
6059
+ observation.result = { disposition: "processed" };
6060
+ return new Response("", {
6061
+ status: 202,
6062
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6063
+ });
6064
+ }
5755
6065
  }
5756
6066
  const PORTABLE_INBOX_FORWARDABLE_RESULTS = /* @__PURE__ */ new Set([
5757
6067
  "success",
@@ -5880,8 +6190,8 @@ var CustomCollectionHandler = class {
5880
6190
  * @param CollectionPage The CollectionPage constructor.
5881
6191
  * @param filterPredicate Optional filter predicate for items.
5882
6192
  */
5883
- constructor(name$2, values, context, callbacks, tracerProvider = _opentelemetry_api.trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
5884
- this.name = name$2;
6193
+ constructor(name$1, values, context, callbacks, tracerProvider = _opentelemetry_api.trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
6194
+ this.name = name$1;
5885
6195
  this.values = values;
5886
6196
  this.context = context;
5887
6197
  this.callbacks = callbacks;
@@ -6417,19 +6727,19 @@ function isPortableActivity(activity) {
6417
6727
  */
6418
6728
  async function verifyPortableActivityId(activityId, jsonLd, options) {
6419
6729
  const expectedId = require_http.getCanonicalPortableId(activityId);
6420
- if (expectedId == null) return "malformed portable ID";
6730
+ if (expectedId == null) return { type: "invalidObjectId" };
6421
6731
  let verification;
6422
6732
  try {
6423
6733
  verification = await require_http.verifyPortableObjectProofWithRoot(jsonLd, options);
6424
6734
  } catch (error) {
6425
- if (error instanceof require_http.InvalidPortableObjectIdError) return "malformed portable ID";
6735
+ if (error instanceof require_http.InvalidPortableObjectIdError) return { type: "invalidObjectId" };
6426
6736
  if (!isPermanentActivityParseError(error)) throw error;
6427
- return "malformed document";
6737
+ return { type: "invalidDocument" };
6428
6738
  }
6429
6739
  const { result, root } = verification;
6430
- if (!result.verified) return result.reason.type;
6740
+ if (!result.verified) return result.reason;
6431
6741
  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";
6742
+ if (typeof rootId !== "string" || require_http.getCanonicalPortableId((0, _fedify_vocab_runtime.parseIri)(rootId)) !== expectedId) return { type: "subjectMismatch" };
6433
6743
  return null;
6434
6744
  }
6435
6745
  //#endregion
@@ -9409,7 +9719,7 @@ var FederationImpl = class extends FederationBuilderImpl {
9409
9719
  return await (0, _logtape_logtape.withContext)({
9410
9720
  traceId: spanCtx.traceId,
9411
9721
  spanId: spanCtx.spanId
9412
- }, async () => {
9722
+ }, () => this.#observeInboxFetch(metricState, async () => {
9413
9723
  const logger = (0, _logtape_logtape.getLogger)([
9414
9724
  "fedify",
9415
9725
  "federation",
@@ -9424,8 +9734,14 @@ var FederationImpl = class extends FederationBuilderImpl {
9424
9734
  tracer,
9425
9735
  metricState
9426
9736
  });
9737
+ if (metricState.inboxCompletion != null) metricState.inboxCompletion.observation.stage = "respond";
9427
9738
  if (metricState.endpoint !== "hashlink_media" && acceptsJsonLd(request)) response.headers.set("Vary", "Accept");
9428
9739
  } catch (error) {
9740
+ metricState.inboxCompletion?.observation.project(span, {
9741
+ type: "exception",
9742
+ stage: metricState.inboxCompletion.observation.stage,
9743
+ error
9744
+ });
9429
9745
  this.metrics.recordHttpServerRequest(request.method, metricState.endpoint ?? "error", require_http.getDurationMs(metricStart), { routeTemplate: metricState.routeTemplate });
9430
9746
  span.setStatus({
9431
9747
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -9464,7 +9780,7 @@ var FederationImpl = class extends FederationBuilderImpl {
9464
9780
  else if (response.status >= 400) logger.warn(logTpl, values);
9465
9781
  else logger.info(logTpl, values);
9466
9782
  return response;
9467
- });
9783
+ }));
9468
9784
  });
9469
9785
  });
9470
9786
  }
@@ -9506,8 +9822,17 @@ var FederationImpl = class extends FederationBuilderImpl {
9506
9822
  metricState.routeTemplate = route.template;
9507
9823
  metricState.endpoint = getEndpointCategory(route.name);
9508
9824
  span.updateName(`${request.method} ${route.template}`);
9509
- let context = this.#createContext(request, contextData);
9510
9825
  const routeName = route.name.replace(/:.*$/, "");
9826
+ let context;
9827
+ try {
9828
+ context = this.#createContext(request, contextData);
9829
+ } catch (error) {
9830
+ if (request.method !== "POST" || routeName !== "inbox" && routeName !== "sharedInbox") throw error;
9831
+ return await this.#reportInboxPreparationError(request, contextData, {
9832
+ kind: routeName === "inbox" ? "personal" : "shared",
9833
+ recipient: route.values.identifier ?? null
9834
+ }, error, span, metricState);
9835
+ }
9511
9836
  switch (routeName) {
9512
9837
  case "webfinger": return await handleWebFinger(request, {
9513
9838
  context,
@@ -9566,6 +9891,28 @@ var FederationImpl = class extends FederationBuilderImpl {
9566
9891
  });
9567
9892
  return response;
9568
9893
  }
9894
+ if (request.method === "POST" && (routeName === "inbox" || routeName === "sharedInbox")) {
9895
+ const recipient = route.values.identifier ?? null;
9896
+ const observation = new InboxObservation({
9897
+ kind: routeName === "inbox" ? "personal" : "shared",
9898
+ recipient
9899
+ });
9900
+ this.#setInboxCompletion(metricState, observation, () => context);
9901
+ return await observation.run(async () => {
9902
+ if (routeName === "inbox") context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: recipient }) });
9903
+ else if (this.sharedInboxKeyDispatcher != null) {
9904
+ const identity = await this.sharedInboxKeyDispatcher(context);
9905
+ if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
9906
+ }
9907
+ return await this.#handleInbox(request, {
9908
+ recipient,
9909
+ context,
9910
+ contextData,
9911
+ onNotFound,
9912
+ observation
9913
+ });
9914
+ }, span);
9915
+ }
9569
9916
  switch (routeName) {
9570
9917
  case "actor":
9571
9918
  case "actorAlias": {
@@ -9641,9 +9988,9 @@ var FederationImpl = class extends FederationBuilderImpl {
9641
9988
  onUnauthorized,
9642
9989
  onNotFound
9643
9990
  });
9644
- context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
9991
+ throw new Error("Unreachable inbox delivery.");
9645
9992
  case "sharedInbox":
9646
- if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
9993
+ if (this.sharedInboxKeyDispatcher != null) {
9647
9994
  const identity = await this.sharedInboxKeyDispatcher(context);
9648
9995
  if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
9649
9996
  }
@@ -9936,7 +10283,49 @@ var FederationImpl = class extends FederationBuilderImpl {
9936
10283
  }
9937
10284
  return await onNotFound(request);
9938
10285
  }
9939
- async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox }) {
10286
+ #setInboxCompletion(metricState, observation, getContext) {
10287
+ const handler = this.inboxRequestFinishedHandler;
10288
+ metricState.inboxCompletion = {
10289
+ observation,
10290
+ finish: (outcome) => observation.finish(getContext(), handler, outcome)
10291
+ };
10292
+ }
10293
+ async #observeInboxFetch(metricState, operation) {
10294
+ let response;
10295
+ try {
10296
+ response = await operation();
10297
+ } catch (error) {
10298
+ const completion = metricState.inboxCompletion;
10299
+ if (completion != null) await completion.finish({
10300
+ type: "exception",
10301
+ stage: completion.observation.stage,
10302
+ error
10303
+ });
10304
+ throw error;
10305
+ }
10306
+ const completion = metricState.inboxCompletion;
10307
+ if (completion != null) await completion.finish({
10308
+ type: "response",
10309
+ status: response.status,
10310
+ ...completion.observation.result
10311
+ });
10312
+ return response;
10313
+ }
10314
+ async #reportInboxPreparationError(request, contextData, inbox, error, span, metricState) {
10315
+ const unavailableLoader = () => Promise.reject(error);
10316
+ const context = new RequestContextImpl({
10317
+ url: new URL(request.url),
10318
+ request,
10319
+ federation: this,
10320
+ data: contextData,
10321
+ documentLoader: unavailableLoader,
10322
+ contextLoader: unavailableLoader
10323
+ });
10324
+ const observation = new InboxObservation(inbox);
10325
+ this.#setInboxCompletion(metricState, observation, () => context);
10326
+ return await observation.run(() => Promise.reject(error), span);
10327
+ }
10328
+ async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox, observation }) {
9940
10329
  if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
9941
10330
  const inboxContextFactory = context.toInboxContext.bind(context);
9942
10331
  inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
@@ -9960,7 +10349,8 @@ var FederationImpl = class extends FederationBuilderImpl {
9960
10349
  meterProvider: this.meterProvider,
9961
10350
  tracerProvider: this.tracerProvider,
9962
10351
  idempotencyStrategy: this.idempotencyStrategy,
9963
- portableInbox
10352
+ portableInbox,
10353
+ observation
9964
10354
  });
9965
10355
  }
9966
10356
  async #fetchPortableInbox(request, portable, { onNotFound, contextData, span, metricState }) {
@@ -9989,63 +10379,79 @@ var FederationImpl = class extends FederationBuilderImpl {
9989
10379
  metricState.endpoint = "inbox";
9990
10380
  span.updateName(`${request.method} ${metricState.routeTemplate}`);
9991
10381
  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.");
10382
+ let context;
10383
+ try {
10384
+ context = this.#createContext(request, contextData);
10385
+ } catch (error) {
10386
+ return await this.#reportInboxPreparationError(request, contextData, {
10387
+ kind: "portable",
10388
+ recipient: identifier
10389
+ }, error, span, metricState);
10390
+ }
10391
+ const observation = new InboxObservation({
10392
+ kind: "portable",
10393
+ recipient: identifier
10394
+ });
10395
+ this.#setInboxCompletion(metricState, observation, () => context);
10396
+ return await observation.run(async () => {
10397
+ const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
10398
+ const localOrigin = context.canonicalOrigin;
10399
+ const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof _fedify_vocab.Tombstone ? null : actor, {
10400
+ authority: portable.portableRequest.authority,
10401
+ canonicalInboxId: portable.canonicalId,
10402
+ localOrigin
10403
+ });
10404
+ if (resolution.status === "rejected") {
10405
+ logger.debug("Not accepting a delivery to the portable inbox {inbox} on behalf of the actor {identifier}: {reason}.", {
10406
+ inbox: portable.canonicalId,
10407
+ identifier,
10408
+ reason: resolution.reason
10409
+ });
10410
+ return await onNotFound(request);
10411
+ }
10412
+ const { recipient } = resolution;
10413
+ if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
10414
+ const excludedOrigins = [localOrigin, new URL(request.url).origin];
10415
+ return await this.#handleInbox(request, {
10416
+ recipient: identifier,
10417
+ observation,
10418
+ context,
10419
+ contextData,
10420
+ onNotFound,
10421
+ portableInbox: {
10422
+ recipient,
10423
+ forward: async (activity, activityId, activityType, signatures) => {
10424
+ if (this.portableInboxForwarding.maxTargets < 1) return;
10425
+ if (this.kv.cas == null) {
10426
+ if (!this.#portableInboxForwardingWarned) {
10427
+ this.#portableInboxForwardingWarned = true;
10428
+ 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.");
10429
+ }
10430
+ return;
10024
10431
  }
10025
- return;
10432
+ await forwardPortableInboxActivity({
10433
+ recipient,
10434
+ activity,
10435
+ activityId,
10436
+ activityType,
10437
+ excludedOrigins,
10438
+ baseUrl: context.origin,
10439
+ kv: this.kv,
10440
+ kvPrefix: this.kvPrefixes.portableInboxForwarding,
10441
+ outboxQueue: this.outboxQueue,
10442
+ startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
10443
+ allowPrivateAddress: this.allowPrivateAddress,
10444
+ getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
10445
+ specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
10446
+ signatures,
10447
+ options: this.portableInboxForwarding,
10448
+ meterProvider: this.meterProvider,
10449
+ tracerProvider: this.tracerProvider
10450
+ });
10026
10451
  }
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
10452
  }
10047
- }
10048
- });
10453
+ });
10454
+ }, span);
10049
10455
  }
10050
10456
  /**
10051
10457
  * Gets this server's gateway key pairs for the portable actor that owns