@fedify/fedify 2.3.0-dev.994 → 2.3.0-pr.809.37

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 (138) hide show
  1. package/README.md +16 -7
  2. package/dist/{assert-DikXweDx.mjs → assert-OguE97r2.mjs} +1 -1
  3. package/dist/{assert_instance_of-C4Ri6VuN.mjs → assert_instance_of-DBC5X09g.mjs} +1 -1
  4. package/dist/{assert_not_equals--wG9hV7u.mjs → assert_not_equals-DkVK8oqV.mjs} +1 -1
  5. package/dist/{assert_rejects-B-qJtC9Z.mjs → assert_rejects-DN60FHPX.mjs} +28 -3
  6. package/dist/{assert_strict_equals-Dmjbg-bA.mjs → assert_strict_equals-XEgZAlrj.mjs} +1 -1
  7. package/dist/{assert_throws-4NwKEy2q.mjs → assert_throws-BOkhLGYc.mjs} +1 -1
  8. package/dist/{builder-yAlpiIqP.mjs → builder-Dl5Sdmc9.mjs} +96 -42
  9. package/dist/{chunk-nlSIicah.js → chunk-CRNNMoPX.js} +2 -2
  10. package/dist/circuit-breaker-CSWsyoef.mjs +337 -0
  11. package/dist/{client-z-8dc-e1.d.cts → client-CAM_bQXx.d.cts} +1 -0
  12. package/dist/{client-AtlibPOU.d.ts → client-CSddvgWN.d.ts} +1 -2
  13. package/dist/compat/mod.d.cts +2 -1
  14. package/dist/compat/mod.d.ts +2 -3
  15. package/dist/compat/mod.js +3 -3
  16. package/dist/compat/outgoing-jsonld.test.mjs +4 -4
  17. package/dist/compat/public-audience.test.mjs +4 -4
  18. package/dist/compat/transformers.test.mjs +5 -5
  19. package/dist/{context-BzH2-ajs.d.ts → context-BBVLF7lx.d.cts} +342 -269
  20. package/dist/{context-DJGagtNd.d.cts → context-BU6jSQdo.d.ts} +344 -266
  21. package/dist/{context-Dk_tacqz.mjs → context-DVoTs_wM.mjs} +64 -5
  22. package/dist/{deno-XBVlKnOX.mjs → deno-BID9qTPk.mjs} +1 -1
  23. package/dist/{docloader-DPp-X6gk.mjs → docloader-BYFuVwwi.mjs} +4 -4
  24. package/dist/{esm-DVILvP5e.mjs → esm-vrlUxr60.mjs} +4 -7
  25. package/dist/federation/builder.test.mjs +163 -11
  26. package/dist/federation/circuit-breaker.test.d.mts +2 -0
  27. package/dist/federation/circuit-breaker.test.mjs +446 -0
  28. package/dist/federation/collection.test.mjs +3 -3
  29. package/dist/federation/handler.test.mjs +1770 -29
  30. package/dist/federation/idempotency.test.mjs +5 -5
  31. package/dist/federation/inbox.test.mjs +4 -4
  32. package/dist/federation/keycache.test.mjs +5 -5
  33. package/dist/federation/kv.test.mjs +2 -2
  34. package/dist/federation/metrics.test.d.mts +2 -0
  35. package/dist/federation/metrics.test.mjs +634 -0
  36. package/dist/federation/middleware.test.mjs +4036 -146
  37. package/dist/federation/mod.cjs +195 -14
  38. package/dist/federation/mod.d.cts +6 -4
  39. package/dist/federation/mod.d.ts +6 -6
  40. package/dist/federation/mod.js +192 -14
  41. package/dist/federation/mq.test.mjs +176 -15
  42. package/dist/federation/negotiation.test.mjs +4 -4
  43. package/dist/federation/retry.test.mjs +3 -3
  44. package/dist/federation/router.test.mjs +190 -9
  45. package/dist/federation/send.test.mjs +153 -15
  46. package/dist/federation/temporal.test.d.mts +2 -0
  47. package/dist/federation/temporal.test.mjs +71 -0
  48. package/dist/federation/webfinger.test.mjs +150 -5
  49. package/dist/{http-B4rrzxtg.js → http-B47Jg9iX.js} +994 -64
  50. package/dist/{http-CSlLA54v.mjs → http-CO59u1Yk.mjs} +146 -47
  51. package/dist/{http-BrGqJDXL.cjs → http-CjZwUPiT.cjs} +1099 -61
  52. package/dist/{http-aQzN9Ayi.d.ts → http-VyDTd4G3.d.cts} +15 -3
  53. package/dist/{http-CrGuipxe.d.cts → http-lf8Hsd91.d.ts} +15 -1
  54. package/dist/{key-Bzx--sHD.mjs → key-Dqdlo3mG.mjs} +43 -18
  55. package/dist/{kv-CbLNp3zQ.d.cts → kv-D6hNiMTK.d.ts} +1 -0
  56. package/dist/{kv-cache-DI-Sk7-I.cjs → kv-cache-BQ1-zDKX.cjs} +20 -3
  57. package/dist/{kv-cache-EMIqoIuB.js → kv-cache-Dh2ZH9Gr.js} +20 -3
  58. package/dist/{kv-cache-U__xU4qR.mjs → kv-cache-DhLkLv0P.mjs} +21 -3
  59. package/dist/{kv-GFYnFoOl.d.ts → kv-gJ8LYbxX.d.cts} +1 -3
  60. package/dist/ld-JbCByP5P.mjs +626 -0
  61. package/dist/metrics-ColY3QKj.mjs +815 -0
  62. package/dist/{middleware-zjZ6AFCW.mjs → middleware-Buqzuiaf.mjs} +1 -1
  63. package/dist/{middleware-An4DjES4.cjs → middleware-CWQ3Yi07.cjs} +2318 -623
  64. package/dist/{middleware-K1g6bEkV.mjs → middleware-Cg4EyWsq.mjs} +1639 -373
  65. package/dist/{middleware-nidH7VZH.js → middleware-iAJY2aU-.js} +2260 -556
  66. package/dist/{mod-CR8soWa9.d.ts → mod-B0hW12_O.d.cts} +26 -4
  67. package/dist/mod-C0F6kvgS.d.cts +182 -0
  68. package/dist/{mod-Cr3f-ACa.d.cts → mod-COIAjwRS.d.ts} +26 -2
  69. package/dist/{mod-CMEbIaNh.d.cts → mod-DFvNJcNb.d.ts} +56 -4
  70. package/dist/mod-vPYVoa5n.d.ts +182 -0
  71. package/dist/{mod-CLgIXe9w.d.ts → mod-yvIXFAEi.d.cts} +56 -6
  72. package/dist/mod.cjs +9 -6
  73. package/dist/mod.d.cts +12 -10
  74. package/dist/mod.d.ts +12 -12
  75. package/dist/mod.js +11 -11
  76. package/dist/mq-D-nlpY04.d.ts +208 -0
  77. package/dist/mq-D8uSFzxe.d.cts +208 -0
  78. package/dist/{negotiation-SQvQgUqe.mjs → negotiation-DDstyBvc.mjs} +29 -0
  79. package/dist/nodeinfo/client.test.mjs +4 -4
  80. package/dist/nodeinfo/handler.test.mjs +4 -4
  81. package/dist/nodeinfo/mod.d.cts +2 -1
  82. package/dist/nodeinfo/mod.d.ts +2 -3
  83. package/dist/nodeinfo/mod.js +3 -3
  84. package/dist/nodeinfo/types.test.mjs +3 -3
  85. package/dist/otel/exporter.test.mjs +28 -25
  86. package/dist/otel/mod.cjs +6 -5
  87. package/dist/otel/mod.d.cts +5 -3
  88. package/dist/otel/mod.d.ts +5 -5
  89. package/dist/otel/mod.js +8 -7
  90. package/dist/{outgoing-jsonld-CNmZLixq.mjs → outgoing-jsonld-L_DbOaFe.mjs} +2 -2
  91. package/dist/{owner-C-tqfvxn.mjs → owner-B4LgOmzV.mjs} +2 -2
  92. package/dist/{owner-CptqhsOy.d.cts → owner-CnngXDNJ.d.ts} +2 -1
  93. package/dist/{owner-74ARJ5TL.d.ts → owner-DEvZuyOE.d.cts} +2 -3
  94. package/dist/{proof-BlmRPzrK.mjs → proof-BoSGOq9q.mjs} +26 -8
  95. package/dist/{proof-Bbbwf8_x.js → proof-CzhZuawt.js} +378 -15
  96. package/dist/{proof-BW89QMVB.cjs → proof-DvMI-3vq.cjs} +432 -15
  97. package/dist/runtime/mod.d.cts +1 -0
  98. package/dist/runtime/mod.d.ts +1 -2
  99. package/dist/runtime/mod.js +3 -3
  100. package/dist/{send-05pJLcb6.mjs → send-ByK1Q7DK.mjs} +72 -26
  101. package/dist/sig/http.test.mjs +356 -33
  102. package/dist/sig/key.test.mjs +103 -6
  103. package/dist/sig/ld.test.mjs +696 -7
  104. package/dist/sig/mod.cjs +2 -2
  105. package/dist/sig/mod.d.cts +4 -3
  106. package/dist/sig/mod.d.ts +4 -5
  107. package/dist/sig/mod.js +4 -4
  108. package/dist/sig/owner.test.mjs +6 -6
  109. package/dist/sig/proof.test.mjs +169 -8
  110. package/dist/{std__assert-CRDpx_HF.mjs → std__assert-BBjXFNOb.mjs} +5 -30
  111. package/dist/temporal-LpjyAjKb.mjs +95 -0
  112. package/dist/testing/mod.d.mts +110 -10
  113. package/dist/testing/mod.mjs +1 -2
  114. package/dist/{transformers-ve6e2xcg.js → transformers-BGMIq1cs.js} +2 -2
  115. package/dist/{types-hvL8ElAs.js → types-CAY3OdLq.js} +2 -2
  116. package/dist/utils/docloader.test.mjs +6 -6
  117. package/dist/utils/kv-cache.test.mjs +67 -2
  118. package/dist/utils/mod.cjs +1 -1
  119. package/dist/utils/mod.d.cts +2 -1
  120. package/dist/utils/mod.d.ts +2 -3
  121. package/dist/utils/mod.js +3 -3
  122. package/dist/vocab/cjs.test.mjs +1 -1
  123. package/dist/vocab/mod.d.cts +1 -0
  124. package/dist/vocab/mod.d.ts +1 -2
  125. package/dist/vocab/mod.js +2 -2
  126. package/package.json +16 -16
  127. package/dist/ld-DR78beiv.mjs +0 -279
  128. package/dist/middleware-BFBaR0mF.cjs +0 -4
  129. package/dist/mod-2d12ffz3.d.ts +0 -64
  130. package/dist/mod-D35TRn09.d.cts +0 -62
  131. package/dist/router-CrMLXoOr.mjs +0 -114
  132. package/dist/{activity-listener-ell7W1s9.mjs → activity-listener-tztVvlNb.mjs} +0 -0
  133. package/dist/{assert_equals-Ew3jOFa3.mjs → assert_equals-C-ZRDbaf.mjs} +0 -0
  134. package/dist/{client-D_1QpnWt.mjs → client-ByXmQhYD.mjs} +1 -1
  135. package/dist/{collection-D-HqUuA2.mjs → collection-Cc3DVAhE.mjs} +0 -0
  136. package/dist/{keycache-EGATflN-.mjs → keycache-BeU0LCII.mjs} +0 -0
  137. package/dist/{public-audience-DYFHzm_c.mjs → public-audience-Cvbr2Gzt.mjs} +1 -1
  138. /package/dist/{retry-bMXBL97A.mjs → retry-CXg_MBI-.mjs} +0 -0
@@ -1,32 +1,36 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as RouterError } from "./router-CrMLXoOr.mjs";
5
- import { n as version, t as name } from "./deno-XBVlKnOX.mjs";
4
+ import { n as version, t as name } from "./deno-BID9qTPk.mjs";
5
+ import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-ColY3QKj.mjs";
6
6
  import { t as formatAcceptSignature } from "./accept-CPkZzmGN.mjs";
7
- import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-Bzx--sHD.mjs";
8
- import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-CSlLA54v.mjs";
9
- import { t as getAuthenticatedDocumentLoader } from "./docloader-DPp-X6gk.mjs";
10
- import { n as kvCache } from "./kv-cache-U__xU4qR.mjs";
11
- import { a as signJsonLd, i as hasSignatureLike, o as verifyJsonLd, r as detachSignature } from "./ld-DR78beiv.mjs";
12
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-C-tqfvxn.mjs";
13
- import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-CNmZLixq.mjs";
14
- import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-BlmRPzrK.mjs";
15
- import { t as getNodeInfo } from "./client-D_1QpnWt.mjs";
7
+ import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-Dqdlo3mG.mjs";
8
+ import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-CO59u1Yk.mjs";
9
+ import { t as getAuthenticatedDocumentLoader } from "./docloader-BYFuVwwi.mjs";
10
+ import { n as kvCache } from "./kv-cache-DhLkLv0P.mjs";
11
+ import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-JbCByP5P.mjs";
12
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-B4LgOmzV.mjs";
13
+ import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-L_DbOaFe.mjs";
14
+ import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-BoSGOq9q.mjs";
15
+ import { t as getNodeInfo } from "./client-ByXmQhYD.mjs";
16
16
  import { t as nodeInfoToJson } from "./types-J53Kw7so.mjs";
17
- import { t as FederationBuilderImpl } from "./builder-yAlpiIqP.mjs";
18
- import { t as buildCollectionSynchronizationHeader } from "./collection-D-HqUuA2.mjs";
19
- import { t as KvKeyCache } from "./keycache-EGATflN-.mjs";
20
- import { t as acceptsJsonLd } from "./negotiation-SQvQgUqe.mjs";
21
- import { t as createExponentialBackoffPolicy } from "./retry-bMXBL97A.mjs";
22
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-05pJLcb6.mjs";
17
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-ByK1Q7DK.mjs";
18
+ import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-Dl5Sdmc9.mjs";
19
+ import { t as CircuitBreaker } from "./circuit-breaker-CSWsyoef.mjs";
20
+ import { t as buildCollectionSynchronizationHeader } from "./collection-Cc3DVAhE.mjs";
21
+ import { t as KvKeyCache } from "./keycache-BeU0LCII.mjs";
22
+ import { t as acceptsJsonLd } from "./negotiation-DDstyBvc.mjs";
23
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-LpjyAjKb.mjs";
24
+ import { t as createExponentialBackoffPolicy } from "./retry-CXg_MBI-.mjs";
25
+ import { getLogger, withContext } from "@logtape/logtape";
26
+ import { RouterError } from "@fedify/uri-template";
23
27
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
24
28
  import { lookupWebFinger } from "@fedify/webfinger";
25
- import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
26
- import { uniq } from "es-toolkit";
29
+ import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
27
30
  import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
28
- import { getLogger, withContext } from "@logtape/logtape";
29
31
  import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
32
+ import { uniq } from "es-toolkit";
33
+ import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
30
34
  import { domainToASCII } from "node:url";
31
35
  //#region src/compat/transformers.ts
32
36
  const logger$1 = getLogger([
@@ -153,8 +157,203 @@ function handleNodeInfoJrd(_request, context) {
153
157
  return Promise.resolve(response);
154
158
  }
155
159
  //#endregion
160
+ //#region src/federation/bench.ts
161
+ /**
162
+ * Metric reader owned by `benchmarkMode`.
163
+ * @since 2.3.0
164
+ */
165
+ var BenchmarkMetricReader = class extends MetricReader {
166
+ onShutdown() {
167
+ return Promise.resolve();
168
+ }
169
+ onForceFlush() {
170
+ return Promise.resolve();
171
+ }
172
+ };
173
+ /**
174
+ * Creates the in-process OpenTelemetry meter provider used by benchmark mode.
175
+ * @returns The meter provider and the metric reader attached to it.
176
+ * @since 2.3.0
177
+ */
178
+ function createBenchmarkMeterProvider() {
179
+ const reader = new BenchmarkMetricReader();
180
+ return {
181
+ meterProvider: new MeterProvider$1({ readers: [reader] }),
182
+ reader
183
+ };
184
+ }
185
+ /**
186
+ * Collects and serializes benchmark-mode metrics from a benchmark reader.
187
+ * @param reader The benchmark metric reader to collect from.
188
+ * @returns A server metric snapshot with any collection errors stringified.
189
+ * @since 2.3.0
190
+ */
191
+ async function collectBenchmarkMetrics(reader) {
192
+ const result = await reader.collect();
193
+ return {
194
+ version: 1,
195
+ source: "server",
196
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
197
+ scopeMetrics: serializeScopeMetrics(result.resourceMetrics),
198
+ errors: result.errors.map((error) => String(error))
199
+ };
200
+ }
201
+ /**
202
+ * Handles `GET /.well-known/fedify/bench/stats`.
203
+ * @param request The HTTP request to handle.
204
+ * @param reader The benchmark metric reader to collect from.
205
+ * @returns A JSON metric snapshot response, or `405 Method Not Allowed`.
206
+ * @since 2.3.0
207
+ */
208
+ async function handleBenchmarkStats(request, reader) {
209
+ if (request.method !== "GET") return new Response("Method not allowed", {
210
+ status: 405,
211
+ headers: { "Allow": "GET" }
212
+ });
213
+ return jsonResponse(await collectBenchmarkMetrics(reader));
214
+ }
215
+ /**
216
+ * Handles `POST /.well-known/fedify/bench/trigger`.
217
+ *
218
+ * The handler validates a benchmark trigger request, checks recipients against
219
+ * server-controlled trigger options, and calls `Context.sendActivity()` to use
220
+ * the target's normal outbox path.
221
+ * @param request The HTTP request to handle.
222
+ * @param context The Fedify context used to resolve actors and send activity.
223
+ * @param options Server-controlled benchmark trigger delivery options.
224
+ * @returns A JSON response describing the sent activity, or a validation error.
225
+ * @since 2.3.0
226
+ */
227
+ async function handleBenchmarkTrigger(request, context, options = {}) {
228
+ if (request.method !== "POST") return new Response("Method not allowed", {
229
+ status: 405,
230
+ headers: { "Allow": "POST" }
231
+ });
232
+ let json;
233
+ try {
234
+ json = await request.json();
235
+ } catch {
236
+ return jsonResponse({ error: "Invalid JSON request body." }, 400);
237
+ }
238
+ try {
239
+ const body = asRecord(json, "request body");
240
+ const sender = parseSender(body.sender);
241
+ const recipients = await parseRecipients(body.recipients, context);
242
+ const activity = await parseActivity(body.activity, context);
243
+ if (activity.id == null) throw new BenchmarkTriggerError("activity must have an id.");
244
+ const activityId = activity.id.href;
245
+ const inboxes = extractInboxes({ recipients });
246
+ const inboxUrls = Object.keys(inboxes);
247
+ if (inboxUrls.length < 1) throw new BenchmarkTriggerError("No valid recipient inboxes found. The recipients list must not be empty.");
248
+ const unsafeInboxes = options.allowUnsafeRecipients ? [] : inboxUrls.filter((inbox) => !options.sinks?.has(inbox));
249
+ if (unsafeInboxes.length > 0) return jsonResponse({
250
+ error: "unsafe_recipient",
251
+ unsafeInboxes
252
+ }, 403);
253
+ await context.sendActivity(sender, recipients, activity);
254
+ return jsonResponse({
255
+ version: 1,
256
+ activityId,
257
+ queueCorrelationId: activityId,
258
+ recipientCount: recipients.length,
259
+ inboxCount: inboxUrls.length
260
+ }, 202);
261
+ } catch (error) {
262
+ if (error instanceof BenchmarkTriggerError) return jsonResponse({ error: error.message }, error.status);
263
+ throw error;
264
+ }
265
+ }
266
+ var BenchmarkTriggerError = class extends Error {
267
+ status;
268
+ constructor(message, status = 400) {
269
+ super(message);
270
+ this.status = status;
271
+ }
272
+ };
273
+ function parseSender(value) {
274
+ const sender = asRecord(value, "sender");
275
+ if (typeof sender.identifier === "string") return { identifier: sender.identifier };
276
+ if (typeof sender.username === "string") return { username: sender.username };
277
+ throw new BenchmarkTriggerError("sender must be { identifier } or { username }.");
278
+ }
279
+ async function parseRecipients(value, context) {
280
+ if (!Array.isArray(value)) throw new BenchmarkTriggerError("recipients must be an array.");
281
+ return await Promise.all(value.map(async (item) => {
282
+ let object;
283
+ try {
284
+ object = await Object$1.fromJsonLd(item, {
285
+ documentLoader: context.documentLoader,
286
+ contextLoader: context.contextLoader
287
+ });
288
+ } catch (error) {
289
+ throw new BenchmarkTriggerError(`Invalid ActivityPub recipient: ${error}`);
290
+ }
291
+ if (!isRecipient(object)) throw new BenchmarkTriggerError("each recipient must be an ActivityPub actor.");
292
+ const recipient = object;
293
+ if (recipient.id == null || recipient.inboxId == null) throw new BenchmarkTriggerError("each recipient must have id and inbox properties.");
294
+ return recipient;
295
+ }));
296
+ }
297
+ function isRecipient(value) {
298
+ return value != null && typeof value === "object" && "id" in value && "inboxId" in value;
299
+ }
300
+ async function parseActivity(value, context) {
301
+ try {
302
+ return await Activity.fromJsonLd(value, {
303
+ documentLoader: context.documentLoader,
304
+ contextLoader: context.contextLoader
305
+ });
306
+ } catch (error) {
307
+ throw new BenchmarkTriggerError(`Invalid ActivityPub activity: ${error}`);
308
+ }
309
+ }
310
+ function asRecord(value, name) {
311
+ if (value == null || typeof value !== "object" || Array.isArray(value)) throw new BenchmarkTriggerError(`${name} must be an object.`);
312
+ return value;
313
+ }
314
+ function jsonResponse(body, status = 200) {
315
+ return new Response(JSON.stringify(body), {
316
+ status,
317
+ headers: { "Content-Type": "application/json" }
318
+ });
319
+ }
320
+ function serializeScopeMetrics(resourceMetrics) {
321
+ return resourceMetrics.scopeMetrics.map(serializeScope);
322
+ }
323
+ function serializeScope(scopeMetrics) {
324
+ return {
325
+ scope: {
326
+ name: scopeMetrics.scope.name,
327
+ version: scopeMetrics.scope.version
328
+ },
329
+ metrics: scopeMetrics.metrics.map(serializeMetric)
330
+ };
331
+ }
332
+ function serializeMetric(metric) {
333
+ return {
334
+ name: metric.descriptor.name,
335
+ description: metric.descriptor.description,
336
+ unit: metric.descriptor.unit,
337
+ dataPointType: serializeDataPointType(metric.dataPointType),
338
+ dataPoints: metric.dataPoints.map((point) => ({
339
+ attributes: { ...point.attributes },
340
+ startTime: point.startTime,
341
+ endTime: point.endTime,
342
+ value: point.value
343
+ }))
344
+ };
345
+ }
346
+ function serializeDataPointType(dataPointType) {
347
+ switch (dataPointType) {
348
+ case DataPointType.HISTOGRAM: return "histogram";
349
+ case DataPointType.EXPONENTIAL_HISTOGRAM: return "exponential_histogram";
350
+ case DataPointType.GAUGE: return "gauge";
351
+ case DataPointType.SUM: return "sum";
352
+ }
353
+ }
354
+ //#endregion
156
355
  //#region src/federation/inbox.ts
157
- async function routeActivity({ context: ctx, json, activity, recipient, inboxListeners, inboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, tracerProvider, idempotencyStrategy }) {
356
+ async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
158
357
  const logger = getLogger([
159
358
  "fedify",
160
359
  "federation",
@@ -191,6 +390,7 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
191
390
  code: SpanStatusCode.UNSET,
192
391
  message: `Activity ${activity.id?.href} has already been processed.`
193
392
  });
393
+ recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
194
394
  return "alreadyProcessed";
195
395
  }
196
396
  }
@@ -200,6 +400,7 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
200
400
  code: SpanStatusCode.ERROR,
201
401
  message: "Missing actor."
202
402
  });
403
+ recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
203
404
  return "missingActor";
204
405
  }
205
406
  span.setAttribute("activitypub.actor.id", activity.actorId.href);
@@ -211,7 +412,9 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
211
412
  type: "inbox",
212
413
  id: crypto.randomUUID(),
213
414
  baseUrl: ctx.origin,
214
- activity: json,
415
+ activity: originalJson ?? json,
416
+ ...normalizedActivity == null ? {} : { normalizedActivity },
417
+ ...ldSignatureVerified == null ? {} : { ldSignatureVerified },
215
418
  identifier: recipient,
216
419
  attempt: 0,
217
420
  started: (/* @__PURE__ */ new Date()).toISOString(),
@@ -230,6 +433,12 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
230
433
  });
231
434
  throw error;
232
435
  }
436
+ getFederationMetrics(meterProvider).recordQueueTaskEnqueued({
437
+ role: "inbox",
438
+ queue,
439
+ activityType: getTypeId(activity).href
440
+ }, 0);
441
+ recordInboxActivity(meterProvider, "queued", getTypeId(activity).href);
233
442
  logger.info("Activity {activityId} is enqueued.", {
234
443
  activityId: activity.id?.href,
235
444
  activity: json,
@@ -249,13 +458,21 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
249
458
  code: SpanStatusCode.UNSET,
250
459
  message: `Unsupported activity type: ${getTypeId(activity).href}`
251
460
  });
461
+ recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
252
462
  span.end();
253
463
  return "unsupportedActivity";
254
464
  }
255
465
  const { class: cls, listener } = dispatched;
256
466
  span.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
257
467
  try {
258
- await listener(inboxContextFactory(recipient, json, activity?.id?.href, getTypeId(activity).href), activity);
468
+ const activityType = getTypeId(activity).href;
469
+ const started = performance.now();
470
+ try {
471
+ const contextFactory = listenerInboxContextFactory ?? inboxContextFactory;
472
+ await listener(contextFactory(recipient, contextFactory === inboxContextFactory ? json : originalJson ?? json, activity.id?.href, activityType), activity);
473
+ } finally {
474
+ getFederationMetrics(meterProvider).recordInboxProcessingDuration(activityType, getDurationMs(started));
475
+ }
259
476
  } catch (error) {
260
477
  try {
261
478
  await inboxErrorHandler?.(ctx, error);
@@ -277,9 +494,11 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
277
494
  code: SpanStatusCode.ERROR,
278
495
  message: String(error)
279
496
  });
497
+ recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
280
498
  span.end();
281
499
  return "error";
282
500
  }
501
+ recordInboxActivity(meterProvider, "processed", getTypeId(activity).href);
283
502
  if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal.Duration.from({ days: 1 }) });
284
503
  logger.info("Activity {activityId} has been processed.", {
285
504
  activityId: activity.id?.href,
@@ -292,6 +511,33 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
292
511
  }
293
512
  //#endregion
294
513
  //#region src/federation/handler.ts
514
+ const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
515
+ function isRemoteContextLoadingFailure$1(error) {
516
+ return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
517
+ }
518
+ function isPermanentRemoteContextError$1(error) {
519
+ if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
520
+ const details = error.details;
521
+ if (details?.code === "invalid remote context") return true;
522
+ return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) && isClearlyMalformedContextReference(details.url);
523
+ }
524
+ function isInvalidJsonLdError(error) {
525
+ if (!(error instanceof Error)) return false;
526
+ const name = error.name;
527
+ return name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
528
+ }
529
+ function isValidationTypeError(error) {
530
+ return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || isInvalidUrlTypeError(error));
531
+ }
532
+ function isPermanentActivityParseError(error) {
533
+ return isInvalidJsonLdError(error) || isValidationTypeError(error);
534
+ }
535
+ function hasHttpSignatureHeaders(request) {
536
+ return request.headers.has("Signature") || request.headers.has("Signature-Input");
537
+ }
538
+ function hasObjectIntegrityProof(json) {
539
+ return typeof json === "object" && json != null && "proof" in json;
540
+ }
295
541
  /**
296
542
  * Handles an actor request.
297
543
  * @template TContextData The context data to pass to the context.
@@ -353,6 +599,33 @@ async function handleObject(request, { values, context, objectDispatcher, author
353
599
  Vary: "Accept"
354
600
  } });
355
601
  }
602
+ const BUILT_IN_COLLECTION_METRIC_KINDS = new Set([
603
+ "inbox",
604
+ "outbox",
605
+ "following",
606
+ "followers",
607
+ "liked",
608
+ "featured",
609
+ "featured_tags"
610
+ ]);
611
+ function getCollectionMetricKind(name) {
612
+ const normalized = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/\s+/g, "_");
613
+ return BUILT_IN_COLLECTION_METRIC_KINDS.has(normalized) ? normalized : "custom";
614
+ }
615
+ function collectionAttributes(base, result, response) {
616
+ return {
617
+ ...base,
618
+ result,
619
+ ...response == null ? {} : { statusCode: response.status }
620
+ };
621
+ }
622
+ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
623
+ const attrs = collectionAttributes(base, result, options.response);
624
+ recordCollectionRequest(meterProvider, attrs);
625
+ if (options.dispatchDurationMs != null) recordCollectionDispatchDuration(meterProvider, options.dispatchDurationMs, attrs);
626
+ if (options.itemCount != null) recordCollectionPageItems(meterProvider, options.itemCount, attrs);
627
+ if (options.totalItems != null) recordCollectionTotalItems(meterProvider, options.totalItems, attrs);
628
+ }
356
629
  /**
357
630
  * Handles a collection request.
358
631
  * @template TItem The type of items in the collection.
@@ -363,36 +636,118 @@ async function handleObject(request, { values, context, objectDispatcher, author
363
636
  * @param parameters The parameters for handling the collection.
364
637
  * @returns A promise that resolves to an HTTP response.
365
638
  */
366
- async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, onUnauthorized, onNotFound }) {
639
+ async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
367
640
  const spanName = name$1.trim().replace(/\s+/g, "_");
368
641
  tracerProvider = tracerProvider ?? trace.getTracerProvider();
369
642
  const tracer = tracerProvider.getTracer(name, version);
370
643
  const cursor = new URL(request.url).searchParams.get("cursor");
371
- if (collectionCallbacks == null) return await onNotFound(request);
372
- let collection;
373
- const baseUri = uriGetter(identifier);
374
- if (cursor == null) {
375
- const firstCursor = await collectionCallbacks.firstCursor?.(context, identifier);
376
- const totalItems = filter == null ? await collectionCallbacks.counter?.(context, identifier) : void 0;
377
- if (firstCursor == null) {
378
- const itemsOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection ${spanName}`, {
644
+ const metricBase = {
645
+ kind: getCollectionMetricKind(name$1),
646
+ page: cursor != null,
647
+ dispatcher: "built_in"
648
+ };
649
+ let dispatchDurationMs;
650
+ let itemCount;
651
+ let totalItemCount;
652
+ const finish = (response, result) => {
653
+ recordCollectionMetrics(meterProvider, metricBase, result, {
654
+ response,
655
+ dispatchDurationMs,
656
+ itemCount,
657
+ totalItems: totalItemCount
658
+ });
659
+ return response;
660
+ };
661
+ try {
662
+ if (collectionCallbacks == null) return finish(await onNotFound(request), "not_found");
663
+ let collection;
664
+ const baseUri = uriGetter(identifier);
665
+ if (cursor == null) {
666
+ const firstCursor = await collectionCallbacks.firstCursor?.(context, identifier);
667
+ const totalItems = filter == null ? await collectionCallbacks.counter?.(context, identifier) : void 0;
668
+ totalItemCount = totalItems == null ? void 0 : Number(totalItems);
669
+ if (firstCursor == null) {
670
+ const itemsOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection ${spanName}`, {
671
+ kind: SpanKind.SERVER,
672
+ attributes: {
673
+ "activitypub.collection.id": baseUri.href,
674
+ "activitypub.collection.type": OrderedCollection.typeId.href
675
+ }
676
+ }, async (span) => {
677
+ if (totalItemCount != null) span.setAttribute("activitypub.collection.total_items", totalItemCount);
678
+ const started = performance.now();
679
+ try {
680
+ const page = await collectionCallbacks.dispatcher(context, identifier, null, filter);
681
+ dispatchDurationMs = getDurationMs(started);
682
+ if (page == null) {
683
+ span.setStatus({ code: SpanStatusCode.ERROR });
684
+ return await onNotFound(request);
685
+ }
686
+ const items = filterCollectionItems(page.items, name$1, filterPredicate);
687
+ itemCount = items.length;
688
+ span.setAttribute("fedify.collection.items", itemCount);
689
+ return items;
690
+ } catch (e) {
691
+ if (dispatchDurationMs == null) dispatchDurationMs = getDurationMs(started);
692
+ span.setStatus({
693
+ code: SpanStatusCode.ERROR,
694
+ message: String(e)
695
+ });
696
+ throw e;
697
+ } finally {
698
+ span.end();
699
+ }
700
+ });
701
+ if (itemsOrResponse instanceof Response) return finish(itemsOrResponse, "not_found");
702
+ collection = new OrderedCollection({
703
+ id: baseUri,
704
+ totalItems: totalItemCount ?? null,
705
+ items: itemsOrResponse
706
+ });
707
+ } else {
708
+ const lastCursor = await collectionCallbacks.lastCursor?.(context, identifier);
709
+ const first = new URL(context.url);
710
+ first.searchParams.set("cursor", firstCursor);
711
+ let last = null;
712
+ if (lastCursor != null) {
713
+ last = new URL(context.url);
714
+ last.searchParams.set("cursor", lastCursor);
715
+ }
716
+ collection = new OrderedCollection({
717
+ id: baseUri,
718
+ totalItems: totalItemCount ?? null,
719
+ first,
720
+ last
721
+ });
722
+ }
723
+ } else {
724
+ const uri = new URL(baseUri);
725
+ uri.searchParams.set("cursor", cursor);
726
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
379
727
  kind: SpanKind.SERVER,
380
728
  attributes: {
381
- "activitypub.collection.id": baseUri.href,
382
- "activitypub.collection.type": OrderedCollection.typeId.href
729
+ "activitypub.collection.id": uri.href,
730
+ "activitypub.collection.type": OrderedCollectionPage.typeId.href,
731
+ "fedify.collection.cursor": cursor
383
732
  }
384
733
  }, async (span) => {
385
- if (totalItems != null) span.setAttribute("activitypub.collection.total_items", Number(totalItems));
734
+ const started = performance.now();
386
735
  try {
387
- const page = await collectionCallbacks.dispatcher(context, identifier, null, filter);
736
+ const page = await collectionCallbacks.dispatcher(context, identifier, cursor, filter);
737
+ dispatchDurationMs = getDurationMs(started);
388
738
  if (page == null) {
389
739
  span.setStatus({ code: SpanStatusCode.ERROR });
390
740
  return await onNotFound(request);
391
741
  }
392
- const { items } = page;
393
- span.setAttribute("fedify.collection.items", items.length);
394
- return items;
742
+ const items = filterCollectionItems(page.items, name$1, filterPredicate);
743
+ itemCount = items.length;
744
+ span.setAttribute("fedify.collection.items", itemCount);
745
+ return {
746
+ ...page,
747
+ items
748
+ };
395
749
  } catch (e) {
750
+ if (dispatchDurationMs == null) dispatchDurationMs = getDurationMs(started);
396
751
  span.setStatus({
397
752
  code: SpanStatusCode.ERROR,
398
753
  message: String(e)
@@ -402,87 +757,44 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
402
757
  span.end();
403
758
  }
404
759
  });
405
- if (itemsOrResponse instanceof Response) return itemsOrResponse;
406
- collection = new OrderedCollection({
407
- id: baseUri,
408
- totalItems: totalItems == null ? null : Number(totalItems),
409
- items: filterCollectionItems(itemsOrResponse, name$1, filterPredicate)
410
- });
411
- } else {
412
- const lastCursor = await collectionCallbacks.lastCursor?.(context, identifier);
413
- const first = new URL(context.url);
414
- first.searchParams.set("cursor", firstCursor);
415
- let last = null;
416
- if (lastCursor != null) {
417
- last = new URL(context.url);
418
- last.searchParams.set("cursor", lastCursor);
760
+ if (pageOrResponse instanceof Response) return finish(pageOrResponse, "not_found");
761
+ const { items, prevCursor, nextCursor } = pageOrResponse;
762
+ let prev = null;
763
+ if (prevCursor != null) {
764
+ prev = new URL(context.url);
765
+ prev.searchParams.set("cursor", prevCursor);
419
766
  }
420
- collection = new OrderedCollection({
421
- id: baseUri,
422
- totalItems: totalItems == null ? null : Number(totalItems),
423
- first,
424
- last
425
- });
426
- }
427
- } else {
428
- const uri = new URL(baseUri);
429
- uri.searchParams.set("cursor", cursor);
430
- const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
431
- kind: SpanKind.SERVER,
432
- attributes: {
433
- "activitypub.collection.id": uri.href,
434
- "activitypub.collection.type": OrderedCollectionPage.typeId.href,
435
- "fedify.collection.cursor": cursor
767
+ let next = null;
768
+ if (nextCursor != null) {
769
+ next = new URL(context.url);
770
+ next.searchParams.set("cursor", nextCursor);
436
771
  }
437
- }, async (span) => {
438
- try {
439
- const page = await collectionCallbacks.dispatcher(context, identifier, cursor, filter);
440
- if (page == null) {
441
- span.setStatus({ code: SpanStatusCode.ERROR });
442
- return await onNotFound(request);
443
- }
444
- span.setAttribute("fedify.collection.items", page.items.length);
445
- return page;
446
- } catch (e) {
447
- span.setStatus({
448
- code: SpanStatusCode.ERROR,
449
- message: String(e)
450
- });
451
- throw e;
452
- } finally {
453
- span.end();
454
- }
455
- });
456
- if (pageOrResponse instanceof Response) return pageOrResponse;
457
- const { items, prevCursor, nextCursor } = pageOrResponse;
458
- let prev = null;
459
- if (prevCursor != null) {
460
- prev = new URL(context.url);
461
- prev.searchParams.set("cursor", prevCursor);
772
+ const partOf = new URL(context.url);
773
+ partOf.searchParams.delete("cursor");
774
+ collection = new OrderedCollectionPage({
775
+ id: uri,
776
+ prev,
777
+ next,
778
+ items,
779
+ partOf
780
+ });
462
781
  }
463
- let next = null;
464
- if (nextCursor != null) {
465
- next = new URL(context.url);
466
- next.searchParams.set("cursor", nextCursor);
782
+ if (collectionCallbacks.authorizePredicate != null) {
783
+ if (!await collectionCallbacks.authorizePredicate(context, identifier)) return finish(await onUnauthorized(request), "unauthorized");
467
784
  }
468
- const partOf = new URL(context.url);
469
- partOf.searchParams.delete("cursor");
470
- collection = new OrderedCollectionPage({
471
- id: uri,
472
- prev,
473
- next,
474
- items: filterCollectionItems(items, name$1, filterPredicate),
475
- partOf
785
+ const jsonLd = await collection.toJsonLd(context);
786
+ return finish(new Response(JSON.stringify(jsonLd), { headers: {
787
+ "Content-Type": "application/activity+json",
788
+ Vary: "Accept"
789
+ } }), "served");
790
+ } catch (e) {
791
+ recordCollectionMetrics(meterProvider, metricBase, "error", {
792
+ dispatchDurationMs,
793
+ itemCount,
794
+ totalItems: totalItemCount
476
795
  });
796
+ throw e;
477
797
  }
478
- if (collectionCallbacks.authorizePredicate != null) {
479
- if (!await collectionCallbacks.authorizePredicate(context, identifier)) return await onUnauthorized(request);
480
- }
481
- const jsonLd = await collection.toJsonLd(context);
482
- return new Response(JSON.stringify(jsonLd), { headers: {
483
- "Content-Type": "application/activity+json",
484
- Vary: "Accept"
485
- } });
486
798
  }
487
799
  /**
488
800
  * Filters collection items based on the provided predicate.
@@ -746,7 +1058,7 @@ async function handleInbox(request, options) {
746
1058
  * @returns A promise that resolves to an HTTP response.
747
1059
  */
748
1060
  async function handleInboxInternal(request, parameters, span) {
749
- const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, tracerProvider } = parameters;
1061
+ const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider } = parameters;
750
1062
  const logger = getLogger([
751
1063
  "fedify",
752
1064
  "federation",
@@ -818,28 +1130,105 @@ async function handleInboxInternal(request, parameters, span) {
818
1130
  });
819
1131
  }
820
1132
  const keyCache = new KvKeyCache(kv, kvPrefixes.publicKey, ctx);
821
- let ldSigVerified;
822
- try {
823
- ldSigVerified = await verifyJsonLd(json, {
824
- contextLoader: ctx.contextLoader,
825
- documentLoader: ctx.documentLoader,
826
- keyCache,
827
- tracerProvider
1133
+ const jsonWithoutSig = detachSignature(json);
1134
+ const hasLdSignature = hasSignature(json);
1135
+ const canAttemptAlternateAuthAfterLdSignatureFailure = skipSignatureVerification || hasHttpSignatureHeaders(request) || hasObjectIntegrityProof(jsonWithoutSig);
1136
+ let deferredLdSignatureError = void 0;
1137
+ const respondInvalidActivity = async (error) => {
1138
+ logger.error("Failed to parse activity:\n{error}", {
1139
+ recipient,
1140
+ activity: json,
1141
+ error
828
1142
  });
829
- } catch (error) {
830
- if (error instanceof Error && error.name === "jsonld.SyntaxError") {
831
- logger.error("Failed to parse JSON-LD:\n{error}", {
1143
+ try {
1144
+ await inboxErrorHandler?.(ctx, error);
1145
+ } catch (error) {
1146
+ logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
1147
+ error,
1148
+ activity: json,
1149
+ recipient
1150
+ });
1151
+ }
1152
+ span.setStatus({
1153
+ code: SpanStatusCode.ERROR,
1154
+ message: `Failed to parse activity:\n${error}`
1155
+ });
1156
+ return new Response("Invalid activity.", {
1157
+ status: 400,
1158
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1159
+ });
1160
+ };
1161
+ let compactedJson = json;
1162
+ let compactedJsonWithoutSig = jsonWithoutSig;
1163
+ let ldSigVerified = false;
1164
+ if (hasLdSignature) {
1165
+ try {
1166
+ compactedJson = await compactJsonLd(json, ctx.contextLoader);
1167
+ } catch (error) {
1168
+ if (isInvalidJsonLdError(error)) {
1169
+ logger.error("Failed to parse JSON-LD:\n{error}", {
1170
+ recipient,
1171
+ error
1172
+ });
1173
+ return new Response("Invalid JSON-LD.", {
1174
+ status: 400,
1175
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1176
+ });
1177
+ }
1178
+ if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
1179
+ if (!skipSignatureVerification) deferredLdSignatureError = error;
1180
+ logger.debug("Failed to normalize JSON-LD for Linked Data Signatures; deferring to another authentication path only if it verifies:\n{error}", {
832
1181
  recipient,
833
1182
  error
834
1183
  });
835
- return new Response("Invalid JSON-LD.", {
836
- status: 400,
837
- headers: { "Content-Type": "text/plain; charset=utf-8" }
838
- });
839
1184
  }
840
- ldSigVerified = false;
1185
+ if (compactedJson !== json) {
1186
+ compactedJsonWithoutSig = detachSignature(compactedJson);
1187
+ try {
1188
+ ldSigVerified = await verifyCompactJsonLd(compactedJson, {
1189
+ contextLoader: ctx.contextLoader,
1190
+ documentLoader: ctx.documentLoader,
1191
+ keyCache,
1192
+ meterProvider,
1193
+ tracerProvider
1194
+ });
1195
+ } catch (error) {
1196
+ if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(error);
1197
+ if (isInvalidJsonLdError(error)) {
1198
+ logger.error("Failed to parse JSON-LD:\n{error}", {
1199
+ recipient,
1200
+ error
1201
+ });
1202
+ return new Response("Invalid JSON-LD.", {
1203
+ status: 400,
1204
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1205
+ });
1206
+ }
1207
+ if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
1208
+ if (!skipSignatureVerification) try {
1209
+ await Object$1.fromJsonLd(compactedJson, {
1210
+ contextLoader: getNormalizationContextLoader(ctx.contextLoader),
1211
+ documentLoader: ctx.documentLoader,
1212
+ tracerProvider
1213
+ });
1214
+ } catch (parseError) {
1215
+ if (parseError instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(parseError);
1216
+ if (isInvalidJsonLdError(parseError)) {
1217
+ logger.error("Failed to parse JSON-LD:\n{error}", {
1218
+ recipient,
1219
+ error: parseError
1220
+ });
1221
+ return new Response("Invalid JSON-LD.", {
1222
+ status: 400,
1223
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1224
+ });
1225
+ }
1226
+ deferredLdSignatureError = parseError;
1227
+ }
1228
+ ldSigVerified = false;
1229
+ }
1230
+ }
841
1231
  }
842
- const jsonWithoutSig = detachSignature(json);
843
1232
  let activity = null;
844
1233
  let activityVerified = false;
845
1234
  if (ldSigVerified) {
@@ -847,7 +1236,16 @@ async function handleInboxInternal(request, parameters, span) {
847
1236
  recipient,
848
1237
  json
849
1238
  });
850
- activity = await Activity.fromJsonLd(jsonWithoutSig, ctx);
1239
+ try {
1240
+ activity = await Activity.fromJsonLd(compactedJsonWithoutSig, {
1241
+ ...ctx,
1242
+ contextLoader: getNormalizationContextLoader(ctx.contextLoader)
1243
+ });
1244
+ } catch (error) {
1245
+ if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(error);
1246
+ if (!isPermanentActivityParseError(error)) throw error;
1247
+ return await respondInvalidActivity(error);
1248
+ }
851
1249
  activityVerified = true;
852
1250
  } else {
853
1251
  logger.debug("Linked Data Signatures are not verified.", {
@@ -856,12 +1254,22 @@ async function handleInboxInternal(request, parameters, span) {
856
1254
  });
857
1255
  try {
858
1256
  activity = await verifyObject(Activity, jsonWithoutSig, {
859
- contextLoader: ctx.contextLoader,
1257
+ contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
860
1258
  documentLoader: ctx.documentLoader,
861
1259
  keyCache,
1260
+ meterProvider,
862
1261
  tracerProvider
863
1262
  });
864
1263
  } catch (error) {
1264
+ if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(jsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(error);
1265
+ if (deferredLdSignatureError != null) {
1266
+ logger.debug("Object Integrity Proof fallback did not supersede a deferred Linked Data Signature failure:\n{error}", {
1267
+ recipient,
1268
+ error
1269
+ });
1270
+ activity = null;
1271
+ }
1272
+ if (!isPermanentActivityParseError(error)) throw error;
865
1273
  logger.error("Failed to parse activity:\n{error}", {
866
1274
  recipient,
867
1275
  activity: json,
@@ -906,10 +1314,14 @@ async function handleInboxInternal(request, parameters, span) {
906
1314
  documentLoader: ctx.documentLoader,
907
1315
  timeWindow: signatureTimeWindow,
908
1316
  keyCache,
1317
+ meterProvider,
909
1318
  tracerProvider
910
1319
  });
911
1320
  if (verification.verified === false) {
1321
+ if (deferredLdSignatureError != null) throw deferredLdSignatureError;
912
1322
  const reason = verification.reason;
1323
+ const remoteHost = "keyId" in reason && reason.keyId != null ? getRemoteHost(reason.keyId) : void 0;
1324
+ getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure(reason.type, remoteHost);
913
1325
  logger.error("Failed to verify the request's HTTP Signatures.", {
914
1326
  recipient,
915
1327
  reason: reason.type,
@@ -984,7 +1396,15 @@ async function handleInboxInternal(request, parameters, span) {
984
1396
  }
985
1397
  httpSigKey = verification.key;
986
1398
  }
987
- activity = await Activity.fromJsonLd(jsonWithoutSig, ctx);
1399
+ try {
1400
+ activity = await Activity.fromJsonLd(jsonWithoutSig, {
1401
+ ...ctx,
1402
+ contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader)
1403
+ });
1404
+ } catch (error) {
1405
+ if (!isPermanentActivityParseError(error)) throw error;
1406
+ return await respondInvalidActivity(error);
1407
+ }
988
1408
  }
989
1409
  if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
990
1410
  span.setAttribute("activitypub.activity.type", getTypeId(activity).href);
@@ -996,6 +1416,8 @@ async function handleInboxInternal(request, parameters, span) {
996
1416
  "http_signatures.key_id": httpSigKey?.id?.href ?? ""
997
1417
  });
998
1418
  if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
1419
+ if (deferredLdSignatureError != null) throw deferredLdSignatureError;
1420
+ getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : getRemoteHost(httpSigKey.id));
999
1421
  logger.error("The signer ({keyId}) and the actor ({actorId}) do not match.", {
1000
1422
  activity: json,
1001
1423
  recipient,
@@ -1013,6 +1435,7 @@ async function handleInboxInternal(request, parameters, span) {
1013
1435
  }
1014
1436
  if (pendingNonceLabel != null) {
1015
1437
  if (!await verifySignatureNonce(request, kv, kvPrefixes.acceptSignatureNonce, pendingNonceLabel)) {
1438
+ getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("invalidNonce", httpSigKey?.id == null ? void 0 : getRemoteHost(httpSigKey.id));
1016
1439
  logger.error("Signature nonce verification failed (missing, expired, or replayed).", { recipient });
1017
1440
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
1018
1441
  }
@@ -1020,15 +1443,20 @@ async function handleInboxInternal(request, parameters, span) {
1020
1443
  const routeResult = await routeActivity({
1021
1444
  context: ctx,
1022
1445
  json,
1446
+ originalJson: json,
1447
+ normalizedActivity: hasLdSignature && compactedJson !== json ? compactedJson : void 0,
1448
+ ldSignatureVerified: hasLdSignature ? ldSigVerified : void 0,
1023
1449
  activity,
1024
1450
  recipient,
1025
1451
  inboxListeners,
1026
1452
  inboxContextFactory,
1453
+ listenerInboxContextFactory: ldSigVerified ? inboxContextFactory[rawInboxContextFactorySymbol] : void 0,
1027
1454
  inboxErrorHandler,
1028
1455
  kv,
1029
1456
  kvPrefixes,
1030
1457
  queue,
1031
1458
  span,
1459
+ meterProvider: parameters.meterProvider,
1032
1460
  tracerProvider,
1033
1461
  idempotencyStrategy: parameters.idempotencyStrategy
1034
1462
  });
@@ -1069,11 +1497,31 @@ async function handleInboxInternal(request, parameters, span) {
1069
1497
  * @since 1.8.0
1070
1498
  */
1071
1499
  const handleCustomCollection = exceptWrapper(_handleCustomCollection);
1072
- async function _handleCustomCollection(request, { name, values, context, tracerProvider, collectionCallbacks: callbacks, filterPredicate }) {
1500
+ const pendingCollectionMetricRecorders = /* @__PURE__ */ new WeakMap();
1501
+ function deferPendingCollectionMetrics(error, recorder) {
1502
+ if (error == null || typeof error !== "object" && typeof error !== "function") return false;
1503
+ pendingCollectionMetricRecorders.set(error, recorder);
1504
+ return true;
1505
+ }
1506
+ function recordDeferredPendingCollectionMetrics(error, result, response) {
1507
+ if (error == null || typeof error !== "object" && typeof error !== "function") return;
1508
+ const recorder = pendingCollectionMetricRecorders.get(error);
1509
+ pendingCollectionMetricRecorders.delete(error);
1510
+ recorder?.(result, response);
1511
+ }
1512
+ async function _handleCustomCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate }) {
1073
1513
  verifyDefined(callbacks);
1074
1514
  await authIfNeeded(context, values, callbacks);
1075
1515
  const cursor = new URL(request.url).searchParams.get("cursor");
1076
- return await new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, Collection, CollectionPage, filterPredicate).fetchCollection(cursor).toJsonLd().then(respondAsActivity);
1516
+ const handler = new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, Collection, CollectionPage, filterPredicate).fetchCollection(cursor);
1517
+ try {
1518
+ const response = await handler.toJsonLd().then(respondAsActivity);
1519
+ handler.recordPendingCollectionMetrics("served", response);
1520
+ return response;
1521
+ } catch (e) {
1522
+ if (!deferPendingCollectionMetrics(e, (result, response) => handler.recordPendingCollectionMetrics(result, response))) handler.recordPendingCollectionMetrics("error");
1523
+ throw e;
1524
+ }
1077
1525
  }
1078
1526
  /**
1079
1527
  * Handles an ordered collection request.
@@ -1087,11 +1535,19 @@ async function _handleCustomCollection(request, { name, values, context, tracerP
1087
1535
  * @since 1.8.0
1088
1536
  */
1089
1537
  const handleOrderedCollection = exceptWrapper(_handleOrderedCollection);
1090
- async function _handleOrderedCollection(request, { name, values, context, tracerProvider, collectionCallbacks: callbacks, filterPredicate }) {
1538
+ async function _handleOrderedCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate }) {
1091
1539
  verifyDefined(callbacks);
1092
1540
  await authIfNeeded(context, values, callbacks);
1093
1541
  const cursor = new URL(request.url).searchParams.get("cursor");
1094
- return await new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, OrderedCollection, OrderedCollectionPage, filterPredicate).fetchCollection(cursor).toJsonLd().then(respondAsActivity);
1542
+ const handler = new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, OrderedCollection, OrderedCollectionPage, filterPredicate).fetchCollection(cursor);
1543
+ try {
1544
+ const response = await handler.toJsonLd().then(respondAsActivity);
1545
+ handler.recordPendingCollectionMetrics("served", response);
1546
+ return response;
1547
+ } catch (e) {
1548
+ if (!deferPendingCollectionMetrics(e, (result, response) => handler.recordPendingCollectionMetrics(result, response))) handler.recordPendingCollectionMetrics("error");
1549
+ throw e;
1550
+ }
1095
1551
  }
1096
1552
  /**
1097
1553
  * Handling custom collections with support for pagination and filtering.
@@ -1106,6 +1562,15 @@ async function _handleOrderedCollection(request, { name, values, context, tracer
1106
1562
  * @since 1.8.0
1107
1563
  */
1108
1564
  var CustomCollectionHandler = class {
1565
+ name;
1566
+ values;
1567
+ context;
1568
+ callbacks;
1569
+ tracerProvider;
1570
+ meterProvider;
1571
+ Collection;
1572
+ CollectionPage;
1573
+ filterPredicate;
1109
1574
  /**
1110
1575
  * The tracer for telemetry.
1111
1576
  * @type {Tracer}
@@ -1130,6 +1595,7 @@ var CustomCollectionHandler = class {
1130
1595
  */
1131
1596
  #dispatcher;
1132
1597
  #collection = null;
1598
+ #pendingCollectionMetrics = [];
1133
1599
  /**
1134
1600
  * Creates a new CustomCollection instance.
1135
1601
  * @param name The name of the collection.
@@ -1141,12 +1607,13 @@ var CustomCollectionHandler = class {
1141
1607
  * @param CollectionPage The CollectionPage constructor.
1142
1608
  * @param filterPredicate Optional filter predicate for items.
1143
1609
  */
1144
- constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), Collection, CollectionPage, filterPredicate) {
1610
+ constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
1145
1611
  this.name = name$2;
1146
1612
  this.values = values;
1147
1613
  this.context = context;
1148
1614
  this.callbacks = callbacks;
1149
1615
  this.tracerProvider = tracerProvider;
1616
+ this.meterProvider = meterProvider;
1150
1617
  this.Collection = Collection;
1151
1618
  this.CollectionPage = CollectionPage;
1152
1619
  this.filterPredicate = filterPredicate;
@@ -1199,10 +1666,12 @@ var CustomCollectionHandler = class {
1199
1666
  const { prevCursor, nextCursor } = pages;
1200
1667
  const partOf = new URL(id);
1201
1668
  partOf.searchParams.delete("cursor");
1669
+ const items = this.filterItems(pages.items);
1670
+ this.recordPendingCollectionItemCount(true, items.length);
1202
1671
  return {
1203
1672
  id,
1204
1673
  partOf,
1205
- items: this.filterItems(pages.items),
1674
+ items,
1206
1675
  prev: this.appendToUrl(prevCursor),
1207
1676
  next: this.appendToUrl(nextCursor)
1208
1677
  };
@@ -1215,11 +1684,16 @@ var CustomCollectionHandler = class {
1215
1684
  */
1216
1685
  async getProps(firstCursor) {
1217
1686
  const lastCursor = await this.callbacks.lastCursor?.(this.context, this.values);
1687
+ const totalItems = await this.totalItems;
1688
+ if (totalItems != null) this.#pendingCollectionMetrics.push({
1689
+ page: false,
1690
+ totalItems: Number(totalItems)
1691
+ });
1218
1692
  return {
1219
1693
  id: this.#id,
1220
1694
  first: this.appendToUrl(firstCursor),
1221
1695
  last: this.appendToUrl(lastCursor),
1222
- totalItems: await this.totalItems
1696
+ totalItems
1223
1697
  };
1224
1698
  }
1225
1699
  /**
@@ -1229,10 +1703,12 @@ var CustomCollectionHandler = class {
1229
1703
  async getPropsWithoutCursor() {
1230
1704
  const totalItems = await this.totalItems;
1231
1705
  const pages = await this.getPages({ totalItems });
1706
+ const items = this.filterItems(pages.items);
1707
+ this.recordPendingCollectionItemCount(false, items.length);
1232
1708
  return {
1233
1709
  id: this.#id,
1234
1710
  totalItems,
1235
- items: this.filterItems(pages.items)
1711
+ items
1236
1712
  };
1237
1713
  }
1238
1714
  /**
@@ -1267,12 +1743,24 @@ var CustomCollectionHandler = class {
1267
1743
  * @returns A function that handles the span operation.
1268
1744
  */
1269
1745
  spanPages = ({ totalItems = null, cursor = null }) => async (span) => {
1746
+ const pageMetricBase = this.metricBase(cursor !== null);
1747
+ const started = performance.now();
1270
1748
  try {
1271
1749
  if (totalItems !== null) span.setAttribute(this.ATTRS.TOTAL_ITEMS, totalItems);
1272
1750
  const page = await this.dispatch(cursor);
1751
+ const durationMs = getDurationMs(started);
1273
1752
  span.setAttribute(this.ATTRS.ITEMS, page.items.length);
1753
+ this.#pendingCollectionMetrics.push({
1754
+ page: pageMetricBase.page,
1755
+ dispatchDurationMs: durationMs,
1756
+ totalItems: totalItems == null ? void 0 : Number(totalItems)
1757
+ });
1274
1758
  return page;
1275
1759
  } catch (e) {
1760
+ this.#pendingCollectionMetrics.push({
1761
+ page: cursor !== null,
1762
+ dispatchDurationMs: getDurationMs(started)
1763
+ });
1276
1764
  const message = e instanceof Error ? e.message : String(e);
1277
1765
  span.setStatus({
1278
1766
  code: SpanStatusCode.ERROR,
@@ -1299,6 +1787,37 @@ var CustomCollectionHandler = class {
1299
1787
  filterItems(items) {
1300
1788
  return filterCollectionItems(items, this.name, this.filterPredicate);
1301
1789
  }
1790
+ metricBase(page) {
1791
+ return {
1792
+ kind: "custom",
1793
+ page,
1794
+ dispatcher: "custom"
1795
+ };
1796
+ }
1797
+ metricAttributes(page, result, response) {
1798
+ return collectionAttributes(this.metricBase(page), result, response);
1799
+ }
1800
+ recordPendingCollectionItemCount(page, itemCount) {
1801
+ for (let i = this.#pendingCollectionMetrics.length - 1; i >= 0; i--) {
1802
+ const measurement = this.#pendingCollectionMetrics[i];
1803
+ if (measurement.page === page && measurement.dispatchDurationMs != null && measurement.itemCount == null) {
1804
+ measurement.itemCount = itemCount;
1805
+ return;
1806
+ }
1807
+ }
1808
+ this.#pendingCollectionMetrics.push({
1809
+ page,
1810
+ itemCount
1811
+ });
1812
+ }
1813
+ recordPendingCollectionMetrics(result, response) {
1814
+ for (const measurement of this.#pendingCollectionMetrics.splice(0)) {
1815
+ const attrs = this.metricAttributes(measurement.page, result, response);
1816
+ if (measurement.dispatchDurationMs != null) recordCollectionDispatchDuration(this.meterProvider, measurement.dispatchDurationMs, attrs);
1817
+ if (measurement.itemCount != null) recordCollectionPageItems(this.meterProvider, measurement.itemCount, attrs);
1818
+ if (measurement.totalItems != null) recordCollectionTotalItems(this.meterProvider, measurement.totalItems, attrs);
1819
+ }
1820
+ }
1302
1821
  /**
1303
1822
  * Appends a cursor to the URL if it exists.
1304
1823
  * @param cursor The cursor to append, or null/undefined.
@@ -1362,14 +1881,36 @@ var CustomCollectionHandler = class {
1362
1881
  */
1363
1882
  function exceptWrapper(handler) {
1364
1883
  return async (request, handlerParams) => {
1884
+ const page = new URL(request.url).searchParams.get("cursor") != null;
1885
+ const { meterProvider } = handlerParams;
1886
+ const metricBase = {
1887
+ kind: "custom",
1888
+ page,
1889
+ dispatcher: "custom"
1890
+ };
1365
1891
  try {
1366
- return await handler(request, handlerParams);
1892
+ const response = await handler(request, handlerParams);
1893
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "served", response));
1894
+ return response;
1367
1895
  } catch (error) {
1368
1896
  const { onNotFound, onUnauthorized } = handlerParams;
1369
1897
  switch (error?.constructor) {
1370
- case ItemsNotFoundError: return await onNotFound(request);
1371
- case UnauthorizedError: return await onUnauthorized(request);
1372
- default: throw error;
1898
+ case ItemsNotFoundError: {
1899
+ const response = await onNotFound(request);
1900
+ recordDeferredPendingCollectionMetrics(error, "not_found", response);
1901
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "not_found", response));
1902
+ return response;
1903
+ }
1904
+ case UnauthorizedError: {
1905
+ const response = await onUnauthorized(request);
1906
+ recordDeferredPendingCollectionMetrics(error, "unauthorized", response);
1907
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "unauthorized", response));
1908
+ return response;
1909
+ }
1910
+ default:
1911
+ recordDeferredPendingCollectionMetrics(error, "error");
1912
+ recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "error"));
1913
+ throw error;
1373
1914
  }
1374
1915
  }
1375
1916
  };
@@ -1566,22 +2107,72 @@ const logger = getLogger([
1566
2107
  * @returns The response to the request.
1567
2108
  */
1568
2109
  async function handleWebFinger(request, options) {
1569
- if (options.tracer == null) return await handleWebFingerInternal(request, options);
1570
- return await options.tracer.startActiveSpan("webfinger.handle", { kind: SpanKind.SERVER }, async (span) => {
1571
- try {
1572
- const response = await handleWebFingerInternal(request, options);
1573
- span.setStatus({ code: response.ok ? SpanStatusCode.UNSET : SpanStatusCode.ERROR });
1574
- return response;
1575
- } catch (error) {
1576
- span.setStatus({
1577
- code: SpanStatusCode.ERROR,
1578
- message: String(error)
1579
- });
1580
- throw error;
1581
- } finally {
1582
- span.end();
2110
+ const meterProvider = options.meterProvider;
2111
+ const start = meterProvider == null ? 0 : performance.now();
2112
+ const scheme = computeResourceScheme(options.context.url.searchParams.get("resource"));
2113
+ let notFoundResponse;
2114
+ const wrappedOptions = {
2115
+ ...options,
2116
+ async onNotFound(req) {
2117
+ const r = await options.onNotFound(req);
2118
+ notFoundResponse = r;
2119
+ return r;
1583
2120
  }
1584
- });
2121
+ };
2122
+ let response;
2123
+ try {
2124
+ if (options.tracer == null) response = await handleWebFingerInternal(request, wrappedOptions);
2125
+ else response = await options.tracer.startActiveSpan("webfinger.handle", { kind: SpanKind.SERVER }, async (span) => {
2126
+ try {
2127
+ const inner = await handleWebFingerInternal(request, wrappedOptions);
2128
+ span.setStatus({ code: inner.ok ? SpanStatusCode.UNSET : SpanStatusCode.ERROR });
2129
+ return inner;
2130
+ } catch (error) {
2131
+ span.setStatus({
2132
+ code: SpanStatusCode.ERROR,
2133
+ message: String(error)
2134
+ });
2135
+ throw error;
2136
+ } finally {
2137
+ span.end();
2138
+ }
2139
+ });
2140
+ return response;
2141
+ } finally {
2142
+ if (meterProvider != null) recordWebFingerHandle(meterProvider, {
2143
+ durationMs: Math.max(0, performance.now() - start),
2144
+ result: classifyWebFingerHandleResult(response, notFoundResponse),
2145
+ scheme,
2146
+ statusCode: response?.status
2147
+ });
2148
+ }
2149
+ }
2150
+ const WEBFINGER_HANDLE_SCHEME_WHITELIST = new Set([
2151
+ "acct",
2152
+ "http",
2153
+ "https",
2154
+ "mailto"
2155
+ ]);
2156
+ function isAllowedResourceScheme(scheme) {
2157
+ return WEBFINGER_HANDLE_SCHEME_WHITELIST.has(scheme);
2158
+ }
2159
+ function computeResourceScheme(resource) {
2160
+ if (resource == null) return void 0;
2161
+ const colon = resource.indexOf(":");
2162
+ if (colon <= 0) return void 0;
2163
+ const candidate = resource.substring(0, colon).toLowerCase();
2164
+ return isAllowedResourceScheme(candidate) ? candidate : "other";
2165
+ }
2166
+ function classifyWebFingerHandleResult(response, notFoundResponse) {
2167
+ if (response == null) return "error";
2168
+ if (notFoundResponse != null && response === notFoundResponse) return "not_found";
2169
+ switch (response.status) {
2170
+ case 200: return "resolved";
2171
+ case 400: return "invalid";
2172
+ case 404: return "not_found";
2173
+ case 410: return "tombstoned";
2174
+ default: return "error";
2175
+ }
1585
2176
  }
1586
2177
  async function handleWebFingerInternal(request, { context, host, actorDispatcher, actorHandleMapper, actorAliasMapper, onNotFound, span, webFingerLinksDispatcher }) {
1587
2178
  if (actorDispatcher == null) {
@@ -1689,10 +2280,124 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
1689
2280
  }
1690
2281
  //#endregion
1691
2282
  //#region src/federation/middleware.ts
2283
+ const circuitBreakerCasWarningKvStores = /* @__PURE__ */ new WeakSet();
2284
+ let nextQueueDepthGaugeSourceId = 0;
2285
+ const retryAfterHttpDate = /* @__PURE__ */ new RegExp("^(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \\d{2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \\d{4} \\d{2}:\\d{2}:\\d{2} GMT|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), \\d{2}-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\\d{2} \\d{2}:\\d{2}:\\d{2} GMT|(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (?: \\d|\\d{2}) \\d{2}:\\d{2}:\\d{2} \\d{4})$");
2286
+ function parseRetryAfter(headers, now = Temporal.Now.instant()) {
2287
+ const value = headers.get("Retry-After");
2288
+ if (value == null) return void 0;
2289
+ const trimmed = value.trim();
2290
+ if (/^\d+$/.test(trimmed)) {
2291
+ const seconds = Number(trimmed);
2292
+ if (!Number.isFinite(seconds)) return void 0;
2293
+ return parseRetryAfterDuration({ seconds });
2294
+ }
2295
+ if (!retryAfterHttpDate.test(trimmed)) return void 0;
2296
+ const httpDate = trimmed.endsWith("GMT") ? trimmed : `${trimmed} GMT`;
2297
+ const retryAtMs = Date.parse(httpDate);
2298
+ if (Number.isNaN(retryAtMs)) return void 0;
2299
+ const nowMs = Number(now.epochMilliseconds);
2300
+ return parseRetryAfterDuration({ milliseconds: Math.max(0, retryAtMs - nowMs) });
2301
+ }
2302
+ function parseRetryAfterDuration(durationLike) {
2303
+ try {
2304
+ return Temporal.Duration.from(durationLike);
2305
+ } catch (error) {
2306
+ if (error instanceof RangeError) return void 0;
2307
+ throw error;
2308
+ }
2309
+ }
2310
+ function clampNegativeDelay(delay) {
2311
+ return delay.sign < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay;
2312
+ }
2313
+ function getBenchmarkRelaxations(allowPrivateAddress, signatureTimeWindow) {
2314
+ const relaxations = [];
2315
+ if (allowPrivateAddress) relaxations.push({
2316
+ protection: "private_address_checks",
2317
+ effect: "disabled",
2318
+ effectiveValue: true
2319
+ });
2320
+ if (signatureTimeWindow === false) relaxations.push({
2321
+ protection: "http_signature_time_window",
2322
+ effect: "disabled",
2323
+ effectiveValue: false,
2324
+ secureDefaultSeconds: 3600
2325
+ });
2326
+ else try {
2327
+ const seconds = Temporal.Duration.from(signatureTimeWindow).total({ unit: "seconds" });
2328
+ if (seconds !== 3600) relaxations.push({
2329
+ protection: "http_signature_time_window",
2330
+ effect: "changed",
2331
+ effectiveSeconds: seconds,
2332
+ secureDefaultSeconds: 3600
2333
+ });
2334
+ } catch {}
2335
+ return relaxations;
2336
+ }
2337
+ function formatBenchmarkRelaxations(relaxations) {
2338
+ if (relaxations.length < 1) return "no benchmark-only protections relaxed";
2339
+ return relaxations.map((relaxation) => {
2340
+ switch (relaxation.protection) {
2341
+ case "private_address_checks": return "private address checks disabled (allowPrivateAddress=true)";
2342
+ case "http_signature_time_window":
2343
+ if (relaxation.effect === "disabled") return `HTTP Signature time window disabled (signatureTimeWindow=false)`;
2344
+ return `HTTP Signature time window set to ${relaxation.effectiveSeconds}s (secure default: ${relaxation.secureDefaultSeconds}s)`;
2345
+ }
2346
+ }).join("; ");
2347
+ }
2348
+ function getBenchmarkTriggerOptions(benchmarkOptions) {
2349
+ const sinks = benchmarkOptions.triggerSinks?.map((sink) => {
2350
+ try {
2351
+ return new URL(sink).href;
2352
+ } catch {
2353
+ throw new TypeError("benchmarkMode.triggerSinks must contain only URLs.");
2354
+ }
2355
+ });
2356
+ return {
2357
+ sinks: sinks == null ? void 0 : new Set(sinks),
2358
+ allowUnsafeRecipients: benchmarkOptions.allowUnsafeTriggerRecipients === true
2359
+ };
2360
+ }
2361
+ function maxDelay(first, second) {
2362
+ return Temporal.Duration.compare(first, second) >= 0 ? first : second;
2363
+ }
2364
+ function isTransportDeliveryError(error) {
2365
+ return error instanceof FetchError || isAbortError(error);
2366
+ }
2367
+ function toCircuitBreakerMetricState(state) {
2368
+ return state === "half-open" ? "half_open" : state;
2369
+ }
2370
+ function recordCircuitBreakerSpanEvent(span, remoteHost, change) {
2371
+ span.addEvent("activitypub.circuit_breaker.state_change", {
2372
+ "activitypub.remote.host": remoteHost,
2373
+ "activitypub.circuit_breaker.previous_state": toCircuitBreakerMetricState(change.previousState),
2374
+ "activitypub.circuit_breaker.state": toCircuitBreakerMetricState(change.newState)
2375
+ });
2376
+ }
2377
+ function recordCircuitBreakerHeldSpanEvent(span, remoteHost, state) {
2378
+ span.addEvent("activitypub.circuit_breaker.held", {
2379
+ "activitypub.remote.host": remoteHost,
2380
+ "activitypub.circuit_breaker.state": toCircuitBreakerMetricState(state)
2381
+ });
2382
+ }
2383
+ function isRemoteContextLoadingFailure(error) {
2384
+ return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
2385
+ }
2386
+ function isPermanentRemoteContextError(error) {
2387
+ if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
2388
+ const details = error.details;
2389
+ if (details?.code === "invalid remote context") return true;
2390
+ return isRemoteContextLoadingFailure(error) && typeof details?.url === "string" && !URL.canParse(details.url) && isClearlyMalformedContextReference(details.url);
2391
+ }
2392
+ function isPermanentInboxParseError(error) {
2393
+ return error instanceof Error && (error.name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError(error) || error.name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure(error)) || error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || isInvalidUrlTypeError(error));
2394
+ }
1692
2395
  /**
1693
2396
  * Create a new {@link Federation} instance.
1694
- * @param parameters Parameters for initializing the instance.
2397
+ * @param options Parameters for initializing the instance.
1695
2398
  * @returns A new {@link Federation} instance.
2399
+ * @throws {TypeError} If benchmark mode and `meterProvider` are both
2400
+ * specified.
1696
2401
  * @since 0.10.0
1697
2402
  */
1698
2403
  function createFederation(options) {
@@ -1720,12 +2425,37 @@ var FederationImpl = class extends FederationBuilderImpl {
1720
2425
  skipSignatureVerification;
1721
2426
  outboxRetryPolicy;
1722
2427
  inboxRetryPolicy;
2428
+ circuitBreaker;
1723
2429
  activityTransformers;
1724
2430
  _tracerProvider;
2431
+ _meterProvider;
1725
2432
  firstKnock;
1726
2433
  inboxChallengePolicy;
2434
+ benchmarkMode;
2435
+ benchmarkMetricReader;
2436
+ benchmarkTriggerOptions;
2437
+ #queueDepthGaugeSourceId = `fedify-${(++nextQueueDepthGaugeSourceId).toString(36)}`;
2438
+ #queueDepthGaugeEntries = [];
2439
+ #queueDepthGaugeMeterProvider;
1727
2440
  constructor(options) {
1728
2441
  super();
2442
+ const benchmarkMode = options.benchmarkMode != null && options.benchmarkMode !== false;
2443
+ const benchmarkOptions = typeof options.benchmarkMode === "object" ? options.benchmarkMode : {};
2444
+ const hasCustomLoaderFactory = options.documentLoaderFactory != null || options.contextLoaderFactory != null;
2445
+ const allowPrivateAddress = options.allowPrivateAddress ?? (benchmarkMode && !hasCustomLoaderFactory ? true : false);
2446
+ const signatureTimeWindow = options.signatureTimeWindow ?? (benchmarkMode ? false : { hours: 1 });
2447
+ if (benchmarkMode && options.meterProvider != null) throw new TypeError("benchmarkMode requires Fedify to own the meterProvider; OpenTelemetry metric readers cannot be added after a MeterProvider is constructed.");
2448
+ if (benchmarkMode) {
2449
+ const relaxations = getBenchmarkRelaxations(allowPrivateAddress, signatureTimeWindow);
2450
+ const relaxationSummary = formatBenchmarkRelaxations(relaxations);
2451
+ getLogger([
2452
+ "fedify",
2453
+ "federation",
2454
+ "benchmark"
2455
+ ]).warn(`Fedify benchmarkMode is enabled; ${relaxationSummary}. Benchmark endpoints are active and must not be used in production.`, { relaxations });
2456
+ }
2457
+ this.benchmarkMode = benchmarkMode;
2458
+ this.benchmarkTriggerOptions = benchmarkMode ? getBenchmarkTriggerOptions(benchmarkOptions) : {};
1729
2459
  this.kv = options.kv;
1730
2460
  this.kvPrefixes = {
1731
2461
  activityIdempotence: ["_fedify", "activityIdempotence"],
@@ -1733,6 +2463,7 @@ var FederationImpl = class extends FederationBuilderImpl {
1733
2463
  publicKey: ["_fedify", "publicKey"],
1734
2464
  httpMessageSignaturesSpec: ["_fedify", "httpMessageSignaturesSpec"],
1735
2465
  acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
2466
+ circuitBreaker: ["_fedify", "circuit"],
1736
2467
  ...options.kvPrefixes ?? {}
1737
2468
  };
1738
2469
  if (options.queue == null) {
@@ -1748,6 +2479,25 @@ var FederationImpl = class extends FederationBuilderImpl {
1748
2479
  this.outboxQueue = options.queue.outbox;
1749
2480
  this.fanoutQueue = options.queue.fanout;
1750
2481
  }
2482
+ if (options.circuitBreaker !== false && this.outboxQueue != null) {
2483
+ this.circuitBreaker = new CircuitBreaker({
2484
+ kv: options.kv,
2485
+ prefix: this.kvPrefixes.circuitBreaker,
2486
+ options: options.circuitBreaker,
2487
+ stateChangeObserver: (remoteHost, _previousState, newState) => {
2488
+ const metricState = toCircuitBreakerMetricState(newState);
2489
+ recordCircuitBreakerStateChange(this.meterProvider, remoteHost, metricState);
2490
+ }
2491
+ });
2492
+ if (options.kv.cas == null && !circuitBreakerCasWarningKvStores.has(options.kv)) {
2493
+ circuitBreakerCasWarningKvStores.add(options.kv);
2494
+ getLogger([
2495
+ "fedify",
2496
+ "federation",
2497
+ "circuit"
2498
+ ]).warn("The configured key-value store does not support CAS; outbound delivery circuit breaker updates may race under concurrent workers.");
2499
+ }
2500
+ }
1751
2501
  this.inboxQueueStarted = false;
1752
2502
  this.outboxQueueStarted = false;
1753
2503
  this.fanoutQueueStarted = false;
@@ -1773,48 +2523,109 @@ var FederationImpl = class extends FederationBuilderImpl {
1773
2523
  }
1774
2524
  this.router.trailingSlashInsensitive = options.trailingSlashInsensitive ?? false;
1775
2525
  this._initializeRouter();
1776
- if (options.allowPrivateAddress || options.userAgent != null) {
2526
+ if (options.allowPrivateAddress === true || options.userAgent != null) {
1777
2527
  if (options.documentLoaderFactory != null) throw new TypeError("Cannot set documentLoaderFactory with allowPrivateAddress or userAgent options.");
1778
2528
  if (options.contextLoaderFactory != null) throw new TypeError("Cannot set contextLoaderFactory with allowPrivateAddress or userAgent options.");
1779
2529
  if (options.authenticatedDocumentLoaderFactory != null) throw new TypeError("Cannot set authenticatedDocumentLoaderFactory with allowPrivateAddress or userAgent options.");
1780
2530
  }
1781
- const { allowPrivateAddress, userAgent } = options;
1782
- this.allowPrivateAddress = allowPrivateAddress ?? false;
1783
- this.documentLoaderFactory = options.documentLoaderFactory ?? ((opts) => {
1784
- return kvCache({
1785
- loader: getDocumentLoader({
1786
- allowPrivateAddress: opts?.allowPrivateAddress ?? allowPrivateAddress,
1787
- userAgent: opts?.userAgent ?? userAgent
1788
- }),
1789
- kv: options.kv,
1790
- prefix: this.kvPrefixes.remoteDocument
1791
- });
2531
+ const { userAgent } = options;
2532
+ this.allowPrivateAddress = allowPrivateAddress;
2533
+ const userDocumentLoaderFactory = options.documentLoaderFactory;
2534
+ const userContextLoaderFactory = options.contextLoaderFactory;
2535
+ const userAuthFactory = options.authenticatedDocumentLoaderFactory;
2536
+ const builtinDocumentLoaderFactory = (opts) => kvCache({
2537
+ loader: getDocumentLoader({
2538
+ allowPrivateAddress: opts?.allowPrivateAddress ?? allowPrivateAddress,
2539
+ userAgent: opts?.userAgent ?? userAgent
2540
+ }),
2541
+ kv: options.kv,
2542
+ prefix: this.kvPrefixes.remoteDocument,
2543
+ meterProvider: this._meterProvider,
2544
+ kind: "object"
2545
+ });
2546
+ const builtinContextLoaderFactory = (opts) => kvCache({
2547
+ loader: getDocumentLoader({
2548
+ allowPrivateAddress: opts?.allowPrivateAddress ?? allowPrivateAddress,
2549
+ userAgent: opts?.userAgent ?? userAgent
2550
+ }),
2551
+ kv: options.kv,
2552
+ prefix: this.kvPrefixes.remoteDocument,
2553
+ meterProvider: this._meterProvider,
2554
+ kind: "context"
2555
+ });
2556
+ this.documentLoaderFactory = (opts) => instrumentDocumentLoader((userDocumentLoaderFactory ?? builtinDocumentLoaderFactory)(opts), {
2557
+ meterProvider: this._meterProvider,
2558
+ kind: "object",
2559
+ cacheEnabled: userDocumentLoaderFactory == null ? true : void 0
1792
2560
  });
1793
- this.contextLoaderFactory = options.contextLoaderFactory ?? this.documentLoaderFactory;
1794
- this.authenticatedDocumentLoaderFactory = options.authenticatedDocumentLoaderFactory ?? ((identity) => getAuthenticatedDocumentLoader(identity, {
1795
- allowPrivateAddress,
1796
- userAgent,
2561
+ const resolvedContextLoaderFactory = userContextLoaderFactory ?? userDocumentLoaderFactory ?? builtinContextLoaderFactory;
2562
+ this.contextLoaderFactory = (opts) => instrumentDocumentLoader(resolvedContextLoaderFactory(opts), {
2563
+ meterProvider: this._meterProvider,
2564
+ kind: "context",
2565
+ cacheEnabled: userContextLoaderFactory == null && userDocumentLoaderFactory == null ? true : void 0
2566
+ });
2567
+ this.authenticatedDocumentLoaderFactory = (identity, factoryOpts) => instrumentDocumentLoader(userAuthFactory != null ? userAuthFactory(identity, factoryOpts) : getAuthenticatedDocumentLoader(identity, {
2568
+ allowPrivateAddress: factoryOpts?.allowPrivateAddress ?? allowPrivateAddress,
2569
+ userAgent: factoryOpts?.userAgent ?? userAgent,
1797
2570
  specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, options.firstKnock),
1798
2571
  tracerProvider: this.tracerProvider
1799
- }));
2572
+ }), {
2573
+ meterProvider: this._meterProvider,
2574
+ kind: "object",
2575
+ cacheEnabled: userAuthFactory == null ? false : void 0
2576
+ });
1800
2577
  this.userAgent = userAgent;
1801
2578
  this.onOutboxError = options.onOutboxError;
1802
2579
  this.permanentFailureStatusCodes = options.permanentFailureStatusCodes ?? [404, 410];
1803
- this.signatureTimeWindow = options.signatureTimeWindow ?? { hours: 1 };
2580
+ this.signatureTimeWindow = signatureTimeWindow;
1804
2581
  this.skipSignatureVerification = options.skipSignatureVerification ?? false;
1805
2582
  this.inboxChallengePolicy = options.inboxChallengePolicy;
1806
2583
  this.outboxRetryPolicy = options.outboxRetryPolicy ?? createExponentialBackoffPolicy();
1807
2584
  this.inboxRetryPolicy = options.inboxRetryPolicy ?? createExponentialBackoffPolicy();
1808
2585
  this.activityTransformers = options.activityTransformers ?? getDefaultActivityTransformers();
1809
2586
  this._tracerProvider = options.tracerProvider;
2587
+ if (benchmarkMode) {
2588
+ const benchmarkMetrics = createBenchmarkMeterProvider();
2589
+ this._meterProvider = benchmarkMetrics.meterProvider;
2590
+ this.benchmarkMetricReader = benchmarkMetrics.reader;
2591
+ } else this._meterProvider = options.meterProvider;
2592
+ this.#queueDepthGaugeEntries = [
2593
+ {
2594
+ role: "inbox",
2595
+ queue: this.inboxQueue
2596
+ },
2597
+ {
2598
+ role: "outbox",
2599
+ queue: this.outboxQueue
2600
+ },
2601
+ {
2602
+ role: "fanout",
2603
+ queue: this.fanoutQueue
2604
+ }
2605
+ ];
2606
+ this.#registerQueueDepthGauge(this._meterProvider ?? metrics.getMeterProvider());
1810
2607
  this.firstKnock = options.firstKnock;
1811
2608
  }
1812
2609
  get tracerProvider() {
1813
2610
  return this._tracerProvider ?? trace.getTracerProvider();
1814
2611
  }
2612
+ get meterProvider() {
2613
+ const meterProvider = this._meterProvider ?? metrics.getMeterProvider();
2614
+ this.#registerQueueDepthGauge(meterProvider);
2615
+ return meterProvider;
2616
+ }
2617
+ #registerQueueDepthGauge(meterProvider) {
2618
+ if (meterProvider === this.#queueDepthGaugeMeterProvider) return;
2619
+ registerQueueDepthGauge(meterProvider, this.#queueDepthGaugeEntries, { sourceId: this.#queueDepthGaugeSourceId });
2620
+ this.#queueDepthGaugeMeterProvider = meterProvider;
2621
+ }
1815
2622
  _initializeRouter() {
1816
2623
  this.router.add("/.well-known/webfinger", "webfinger");
1817
2624
  this.router.add("/.well-known/nodeinfo", "nodeInfoJrd");
2625
+ if (this.benchmarkMode) {
2626
+ this.router.add("/.well-known/fedify/bench/stats", "benchmarkStats");
2627
+ this.router.add("/.well-known/fedify/bench/trigger", "benchmarkTrigger");
2628
+ }
1818
2629
  }
1819
2630
  _getTracer() {
1820
2631
  return this.tracerProvider.getTracer(name, version);
@@ -1845,80 +2656,125 @@ var FederationImpl = class extends FederationBuilderImpl {
1845
2656
  await Promise.all(promises);
1846
2657
  }
1847
2658
  processQueuedTask(contextData, message) {
1848
- const tracer = this._getTracer();
1849
- const extractedContext = propagation.extract(context.active(), message.traceContext);
1850
- return withContext({ messageId: message.id }, async () => {
1851
- if (message.type === "fanout") await tracer.startActiveSpan("activitypub.fanout", {
1852
- kind: SpanKind.CONSUMER,
1853
- attributes: { "activitypub.activity.type": message.activityType }
1854
- }, extractedContext, async (span) => {
1855
- const spanCtx = span.spanContext();
1856
- return await withContext({
1857
- traceId: spanCtx.traceId,
1858
- spanId: spanCtx.spanId
1859
- }, async () => {
1860
- if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
1861
- try {
1862
- await this.#listenFanoutMessage(contextData, message);
1863
- } catch (e) {
1864
- span.setStatus({
1865
- code: SpanStatusCode.ERROR,
1866
- message: String(e)
1867
- });
1868
- throw e;
1869
- } finally {
1870
- span.end();
1871
- }
2659
+ const tracer = this._getTracer();
2660
+ const extractedContext = propagation.extract(context.active(), message.traceContext);
2661
+ const meter = getFederationMetrics(this.meterProvider);
2662
+ return withContext({ messageId: message.id }, async () => {
2663
+ if (message.type === "fanout") {
2664
+ const common = {
2665
+ role: "fanout",
2666
+ queue: this.fanoutQueue,
2667
+ activityType: message.activityType
2668
+ };
2669
+ await tracer.startActiveSpan("activitypub.fanout", {
2670
+ kind: SpanKind.CONSUMER,
2671
+ attributes: { "activitypub.activity.type": message.activityType }
2672
+ }, extractedContext, async (span) => {
2673
+ const spanCtx = span.spanContext();
2674
+ return await withContext({
2675
+ traceId: spanCtx.traceId,
2676
+ spanId: spanCtx.spanId
2677
+ }, async () => {
2678
+ if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
2679
+ meter.recordQueueTaskStarted(common);
2680
+ meter.incrementQueueTaskInFlight(common);
2681
+ const startedAt = performance.now();
2682
+ let outcome = "completed";
2683
+ try {
2684
+ await this.#listenFanoutMessage(contextData, message);
2685
+ } catch (e) {
2686
+ const aborted = isAbortError(e);
2687
+ outcome = aborted ? "aborted" : "failed";
2688
+ if (!aborted) span.setStatus({
2689
+ code: SpanStatusCode.ERROR,
2690
+ message: String(e)
2691
+ });
2692
+ throw e;
2693
+ } finally {
2694
+ meter.recordQueueTaskOutcome(common, outcome, getDurationMs(startedAt));
2695
+ meter.decrementQueueTaskInFlight(common);
2696
+ span.end();
2697
+ }
2698
+ });
1872
2699
  });
1873
- });
1874
- else if (message.type === "outbox") await tracer.startActiveSpan("activitypub.outbox", {
1875
- kind: SpanKind.CONSUMER,
1876
- attributes: {
1877
- "activitypub.activity.type": message.activityType,
1878
- "activitypub.activity.retries": message.attempt
1879
- }
1880
- }, extractedContext, async (span) => {
1881
- const spanCtx = span.spanContext();
1882
- return await withContext({
1883
- traceId: spanCtx.traceId,
1884
- spanId: spanCtx.spanId
1885
- }, async () => {
1886
- if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
1887
- try {
1888
- await this.#listenOutboxMessage(contextData, message, span);
1889
- } catch (e) {
1890
- span.setStatus({
1891
- code: SpanStatusCode.ERROR,
1892
- message: String(e)
1893
- });
1894
- throw e;
1895
- } finally {
1896
- span.end();
2700
+ } else if (message.type === "outbox") {
2701
+ const common = {
2702
+ role: "outbox",
2703
+ queue: this.outboxQueue,
2704
+ activityType: message.activityType
2705
+ };
2706
+ await tracer.startActiveSpan("activitypub.outbox", {
2707
+ kind: SpanKind.CONSUMER,
2708
+ attributes: {
2709
+ "activitypub.activity.type": message.activityType,
2710
+ "activitypub.activity.retries": message.attempt
1897
2711
  }
2712
+ }, extractedContext, async (span) => {
2713
+ const spanCtx = span.spanContext();
2714
+ return await withContext({
2715
+ traceId: spanCtx.traceId,
2716
+ spanId: spanCtx.spanId
2717
+ }, async () => {
2718
+ if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
2719
+ meter.recordQueueTaskStarted(common);
2720
+ meter.incrementQueueTaskInFlight(common);
2721
+ const startedAt = performance.now();
2722
+ let outcome = "completed";
2723
+ try {
2724
+ await this.#listenOutboxMessage(contextData, message, span);
2725
+ } catch (e) {
2726
+ const aborted = isAbortError(e);
2727
+ outcome = aborted ? "aborted" : "failed";
2728
+ if (!aborted) span.setStatus({
2729
+ code: SpanStatusCode.ERROR,
2730
+ message: String(e)
2731
+ });
2732
+ throw e;
2733
+ } finally {
2734
+ meter.recordQueueTaskOutcome(common, outcome, getDurationMs(startedAt));
2735
+ meter.decrementQueueTaskInFlight(common);
2736
+ span.end();
2737
+ }
2738
+ });
1898
2739
  });
1899
- });
1900
- else if (message.type === "inbox") await tracer.startActiveSpan("activitypub.inbox", {
1901
- kind: SpanKind.CONSUMER,
1902
- attributes: { "activitypub.shared_inbox": message.identifier == null }
1903
- }, extractedContext, async (span) => {
1904
- const spanCtx = span.spanContext();
1905
- return await withContext({
1906
- traceId: spanCtx.traceId,
1907
- spanId: spanCtx.spanId
1908
- }, async () => {
1909
- try {
1910
- await this.#listenInboxMessage(contextData, message, span);
1911
- } catch (e) {
1912
- span.setStatus({
1913
- code: SpanStatusCode.ERROR,
1914
- message: String(e)
1915
- });
1916
- throw e;
1917
- } finally {
1918
- span.end();
1919
- }
2740
+ } else if (message.type === "inbox") {
2741
+ const common = {
2742
+ role: "inbox",
2743
+ queue: this.inboxQueue
2744
+ };
2745
+ await tracer.startActiveSpan("activitypub.inbox", {
2746
+ kind: SpanKind.CONSUMER,
2747
+ attributes: { "activitypub.shared_inbox": message.identifier == null }
2748
+ }, extractedContext, async (span) => {
2749
+ const spanCtx = span.spanContext();
2750
+ return await withContext({
2751
+ traceId: spanCtx.traceId,
2752
+ spanId: spanCtx.spanId
2753
+ }, async () => {
2754
+ meter.recordQueueTaskStarted(common);
2755
+ meter.incrementQueueTaskInFlight(common);
2756
+ const startedAt = performance.now();
2757
+ let outcome = "completed";
2758
+ try {
2759
+ await this.#listenInboxMessage(contextData, message, span, (activityType) => {
2760
+ common.activityType = activityType;
2761
+ });
2762
+ } catch (e) {
2763
+ const aborted = isAbortError(e);
2764
+ outcome = aborted ? "aborted" : "failed";
2765
+ if (!aborted) span.setStatus({
2766
+ code: SpanStatusCode.ERROR,
2767
+ message: String(e)
2768
+ });
2769
+ throw e;
2770
+ } finally {
2771
+ meter.recordQueueTaskOutcome(common, outcome, getDurationMs(startedAt));
2772
+ meter.decrementQueueTaskInFlight(common);
2773
+ span.end();
2774
+ }
2775
+ });
1920
2776
  });
1921
- });
2777
+ }
1922
2778
  });
1923
2779
  }
1924
2780
  async #listenFanoutMessage(data, message) {
@@ -1980,29 +2836,202 @@ var FederationImpl = class extends FederationBuilderImpl {
1980
2836
  if (rsaKeyPair == null && pair.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") rsaKeyPair = pair;
1981
2837
  keys.push(pair);
1982
2838
  }
2839
+ const loaderOptions = this.#getLoaderOptions(message.baseUrl);
2840
+ let parsedActorIds;
2841
+ const getActorIds = () => {
2842
+ parsedActorIds ??= (message.actorIds ?? []).flatMap((id) => {
2843
+ try {
2844
+ return [new URL(id)];
2845
+ } catch {
2846
+ logger.warn("Invalid actorId URL in OutboxMessage: {id}", { id });
2847
+ return [];
2848
+ }
2849
+ });
2850
+ return parsedActorIds;
2851
+ };
2852
+ const parseActivity = () => Activity.fromJsonLd(message.activity, {
2853
+ contextLoader: this.contextLoaderFactory(loaderOptions),
2854
+ documentLoader: rsaKeyPair == null ? this.documentLoaderFactory(loaderOptions) : this.authenticatedDocumentLoaderFactory(rsaKeyPair, loaderOptions),
2855
+ tracerProvider: this.tracerProvider
2856
+ });
2857
+ const enqueueHeldOutboxMessage = async (delay, heldSince) => {
2858
+ const { outboxQueue } = this;
2859
+ if (outboxQueue == null) return;
2860
+ const heldMessage = {
2861
+ ...message,
2862
+ circuitHeld: true,
2863
+ circuitHeldSince: heldSince.toString()
2864
+ };
2865
+ await outboxQueue.enqueue(heldMessage, {
2866
+ delay: clampNegativeDelay(delay),
2867
+ orderingKey: message.orderingKey
2868
+ });
2869
+ getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
2870
+ role: "outbox",
2871
+ queue: outboxQueue,
2872
+ activityType: heldMessage.activityType
2873
+ }, heldMessage.attempt);
2874
+ };
2875
+ const dropHeldOutboxMessage = async (circuit, remoteHost, inbox, heldSince, activity) => {
2876
+ await circuit.dropActivity(remoteHost, {
2877
+ inbox,
2878
+ activity,
2879
+ activityId: message.activityId,
2880
+ activityType: message.activityType,
2881
+ actorIds: getActorIds(),
2882
+ heldSince
2883
+ });
2884
+ if (this.outboxPermanentFailureHandler != null) {
2885
+ const ctx = this.#createContext(new URL(message.baseUrl), _, { documentLoader: this.documentLoaderFactory(loaderOptions) });
2886
+ try {
2887
+ await this.outboxPermanentFailureHandler(ctx, {
2888
+ reason: "circuit-breaker-ttl",
2889
+ inbox,
2890
+ activity,
2891
+ error: new SendActivityError(inbox, 0, "Circuit breaker held activity expired.", ""),
2892
+ statusCode: 0,
2893
+ circuitHeldSince: heldSince,
2894
+ actorIds: getActorIds()
2895
+ });
2896
+ } catch (handlerError) {
2897
+ logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
2898
+ ...logData,
2899
+ error: handlerError
2900
+ });
2901
+ }
2902
+ }
2903
+ recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
2904
+ };
1983
2905
  try {
2906
+ const inbox = new URL(message.inbox);
2907
+ const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
2908
+ const remoteHost = getRemoteHost(inbox);
2909
+ let decision;
2910
+ if (circuit != null) try {
2911
+ decision = await circuit.beforeSend(remoteHost, message);
2912
+ } catch (circuitError) {
2913
+ getLogger([
2914
+ "fedify",
2915
+ "federation",
2916
+ "circuit"
2917
+ ]).error("Failed to check circuit breaker state before sending; proceeding with delivery:\n{error}", {
2918
+ ...logData,
2919
+ remoteHost,
2920
+ error: circuitError
2921
+ });
2922
+ }
2923
+ if (decision != null && circuit != null) {
2924
+ if (decision.type === "hold") {
2925
+ recordCircuitBreakerHeldSpanEvent(span, remoteHost, decision.state);
2926
+ await enqueueHeldOutboxMessage(decision.delay, decision.heldSince);
2927
+ return;
2928
+ }
2929
+ if (decision.type === "drop") {
2930
+ const activity = await parseActivity();
2931
+ await dropHeldOutboxMessage(circuit, remoteHost, inbox, decision.heldSince, activity);
2932
+ return;
2933
+ }
2934
+ if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
2935
+ }
1984
2936
  await sendActivity({
1985
2937
  keys,
1986
2938
  activity: message.activity,
1987
2939
  activityId: message.activityId,
1988
2940
  activityType: message.activityType,
1989
- inbox: new URL(message.inbox),
2941
+ inbox,
1990
2942
  sharedInbox: message.sharedInbox,
1991
2943
  headers: new Headers(message.headers),
1992
2944
  specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock),
2945
+ meterProvider: this.meterProvider,
1993
2946
  tracerProvider: this.tracerProvider
1994
2947
  });
2948
+ if (circuit != null) try {
2949
+ const stateChange = await circuit.recordSuccess(remoteHost);
2950
+ if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
2951
+ } catch (error) {
2952
+ getLogger([
2953
+ "fedify",
2954
+ "federation",
2955
+ "circuit"
2956
+ ]).error("Failed to record successful delivery in circuit breaker state; the activity was already delivered:\n{error}", {
2957
+ ...logData,
2958
+ remoteHost,
2959
+ error
2960
+ });
2961
+ }
1995
2962
  } catch (error) {
1996
2963
  span.setStatus({
1997
2964
  code: SpanStatusCode.ERROR,
1998
2965
  message: String(error)
1999
2966
  });
2000
- const loaderOptions = this.#getLoaderOptions(message.baseUrl);
2001
- const activity = await Activity.fromJsonLd(message.activity, {
2002
- contextLoader: this.contextLoaderFactory(loaderOptions),
2003
- documentLoader: rsaKeyPair == null ? this.documentLoaderFactory(loaderOptions) : this.authenticatedDocumentLoaderFactory(rsaKeyPair, loaderOptions),
2004
- tracerProvider: this.tracerProvider
2967
+ const remoteHost = (() => {
2968
+ if (error instanceof SendActivityError) return getRemoteHost(error.inbox);
2969
+ try {
2970
+ return getRemoteHost(new URL(message.inbox));
2971
+ } catch (_) {
2972
+ logger.warn("Invalid inbox URL in queued outbox message: {inbox}", logData);
2973
+ return;
2974
+ }
2975
+ })();
2976
+ let retryAfterDelay;
2977
+ let circuitHold;
2978
+ let circuitDrop;
2979
+ let retryPolicyDelay;
2980
+ let policyDelayCalculated = false;
2981
+ const getPolicyDelay = () => {
2982
+ if (!policyDelayCalculated) {
2983
+ retryPolicyDelay = this.outboxRetryPolicy({
2984
+ elapsedTime: Temporal.Instant.from(message.started).until(Temporal.Now.instant()),
2985
+ attempts: message.attempt
2986
+ });
2987
+ policyDelayCalculated = true;
2988
+ }
2989
+ return retryPolicyDelay;
2990
+ };
2991
+ const isPermanentFailure = error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode);
2992
+ if (!isPermanentFailure && error instanceof SendActivityError && (error.statusCode === 429 || error.statusCode === 503)) retryAfterDelay = parseRetryAfter(error.responseHeaders);
2993
+ if (remoteHost != null && this.outboxQueue != null && this.circuitBreaker != null) try {
2994
+ if (error instanceof SendActivityError) {
2995
+ const { statusCode } = error;
2996
+ const stateChange = isPermanentFailure || statusCode === 429 || statusCode >= 400 && statusCode < 500 ? await this.circuitBreaker.recordReachableFailure(remoteHost) : statusCode >= 500 ? await this.circuitBreaker.recordFailure(remoteHost) : void 0;
2997
+ if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
2998
+ } else if (isTransportDeliveryError(error)) {
2999
+ const stateChange = await this.circuitBreaker.recordFailure(remoteHost);
3000
+ if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
3001
+ }
3002
+ if (!isPermanentFailure) {
3003
+ const circuitDecision = await this.circuitBreaker.beforeSend(remoteHost, message);
3004
+ if (circuitDecision.type === "hold") circuitHold = {
3005
+ delay: circuitDecision.delay,
3006
+ heldSince: circuitDecision.heldSince,
3007
+ remoteHost,
3008
+ state: circuitDecision.state
3009
+ };
3010
+ else if (circuitDecision.type === "drop") circuitDrop = {
3011
+ circuit: this.circuitBreaker,
3012
+ remoteHost,
3013
+ inbox: new URL(message.inbox),
3014
+ heldSince: circuitDecision.heldSince
3015
+ };
3016
+ }
3017
+ } catch (circuitError) {
3018
+ getLogger([
3019
+ "fedify",
3020
+ "federation",
3021
+ "circuit"
3022
+ ]).error("Failed to update circuit breaker state after delivery failure; falling back to normal failure handling:\n{error}", {
3023
+ ...logData,
3024
+ remoteHost,
3025
+ error: circuitError
3026
+ });
3027
+ }
3028
+ span.addEvent("activitypub.delivery.failed", {
3029
+ ...remoteHost == null ? {} : { "activitypub.remote.host": remoteHost },
3030
+ "activitypub.delivery.attempt": message.attempt,
3031
+ "activitypub.delivery.permanent_failure": isPermanentFailure,
3032
+ ...error instanceof SendActivityError ? { "http.response.status_code": error.statusCode } : {}
2005
3033
  });
3034
+ const activity = await parseActivity();
2006
3035
  try {
2007
3036
  await this.onOutboxError?.(error, activity);
2008
3037
  } catch (error) {
@@ -2011,7 +3040,12 @@ var FederationImpl = class extends FederationBuilderImpl {
2011
3040
  error
2012
3041
  });
2013
3042
  }
2014
- if (error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode)) {
3043
+ if (circuitDrop != null) {
3044
+ await dropHeldOutboxMessage(circuitDrop.circuit, circuitDrop.remoteHost, circuitDrop.inbox, circuitDrop.heldSince, activity);
3045
+ return;
3046
+ }
3047
+ if (isPermanentFailure) {
3048
+ getFederationMetrics(this.meterProvider).recordPermanentFailure(error.inbox, error.statusCode);
2015
3049
  logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not retrying.", {
2016
3050
  ...logData,
2017
3051
  status: error.statusCode
@@ -2020,18 +3054,12 @@ var FederationImpl = class extends FederationBuilderImpl {
2020
3054
  const ctx = this.#createContext(new URL(message.baseUrl), _, { documentLoader: this.documentLoaderFactory(loaderOptions) });
2021
3055
  try {
2022
3056
  await this.outboxPermanentFailureHandler(ctx, {
3057
+ reason: "http",
2023
3058
  inbox: new URL(message.inbox),
2024
3059
  activity,
2025
3060
  error,
2026
3061
  statusCode: error.statusCode,
2027
- actorIds: (message.actorIds ?? []).flatMap((id) => {
2028
- try {
2029
- return [new URL(id)];
2030
- } catch {
2031
- logger.warn("Invalid actorId URL in OutboxMessage: {id}", { id });
2032
- return [];
2033
- }
2034
- })
3062
+ actorIds: getActorIds()
2035
3063
  });
2036
3064
  } catch (handlerError) {
2037
3065
  logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
@@ -2040,37 +3068,62 @@ var FederationImpl = class extends FederationBuilderImpl {
2040
3068
  });
2041
3069
  }
2042
3070
  }
3071
+ recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
2043
3072
  return;
2044
3073
  }
2045
- if (this.outboxQueue?.nativeRetrial) {
3074
+ if (circuitHold != null && getPolicyDelay() != null) {
3075
+ logger.error("Failed to send activity {activityId} to {inbox}; holding because the remote host circuit is open:\n{error}", {
3076
+ ...logData,
3077
+ error
3078
+ });
3079
+ recordCircuitBreakerHeldSpanEvent(span, circuitHold.remoteHost, circuitHold.state);
3080
+ const circuit = this.circuitBreaker;
3081
+ await enqueueHeldOutboxMessage(retryAfterDelay == null || circuit == null ? circuitHold.delay : circuit.capHeldDelay(circuitHold.heldSince, maxDelay(circuitHold.delay, retryAfterDelay)), circuitHold.heldSince);
3082
+ return;
3083
+ }
3084
+ if (this.outboxQueue?.nativeRetrial && retryAfterDelay == null) {
2046
3085
  logger.error("Failed to send activity {activityId} to {inbox}; backend will handle retry:\n{error}", {
2047
3086
  ...logData,
2048
3087
  error
2049
3088
  });
2050
3089
  throw error;
2051
3090
  }
2052
- const delay = this.outboxRetryPolicy({
2053
- elapsedTime: Temporal.Instant.from(message.started).until(Temporal.Now.instant()),
2054
- attempts: message.attempt
2055
- });
3091
+ const policyDelay = getPolicyDelay();
3092
+ const delay = policyDelay == null ? null : retryAfterDelay ?? policyDelay;
2056
3093
  if (delay != null) {
2057
3094
  logger.error("Failed to send activity {activityId} to {inbox} (attempt #{attempt}); retry...:\n{error}", {
2058
3095
  ...logData,
2059
3096
  error
2060
3097
  });
2061
- await this.outboxQueue?.enqueue({
3098
+ const retryMessage = {
2062
3099
  ...message,
2063
3100
  attempt: message.attempt + 1
2064
- }, { delay: Temporal.Duration.compare(delay, { seconds: 0 }) < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay });
2065
- } else logger.error("Failed to send activity {activityId} to {inbox} after {attempt} attempts; giving up:\n{error}", {
2066
- ...logData,
2067
- error
2068
- });
3101
+ };
3102
+ const { outboxQueue } = this;
3103
+ if (outboxQueue != null) {
3104
+ await outboxQueue.enqueue(retryMessage, {
3105
+ delay: clampNegativeDelay(delay),
3106
+ orderingKey: message.orderingKey
3107
+ });
3108
+ getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
3109
+ role: "outbox",
3110
+ queue: outboxQueue,
3111
+ activityType: retryMessage.activityType
3112
+ }, retryMessage.attempt);
3113
+ recordOutboxActivity(this.meterProvider, "retried", retryMessage.activityType);
3114
+ }
3115
+ } else {
3116
+ logger.error("Failed to send activity {activityId} to {inbox} after {attempt} attempts; giving up:\n{error}", {
3117
+ ...logData,
3118
+ error
3119
+ });
3120
+ recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
3121
+ }
2069
3122
  return;
2070
3123
  }
2071
3124
  logger.info("Successfully sent activity {activityId} to {inbox}.", { ...logData });
2072
3125
  }
2073
- async #listenInboxMessage(ctxData, message, span) {
3126
+ async #listenInboxMessage(ctxData, message, span, onActivityType) {
2074
3127
  const logger = getLogger([
2075
3128
  "fedify",
2076
3129
  "federation",
@@ -2083,68 +3136,37 @@ var FederationImpl = class extends FederationBuilderImpl {
2083
3136
  const identity = await this.sharedInboxKeyDispatcher(context);
2084
3137
  if (identity != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
2085
3138
  }
2086
- const activity = await Activity.fromJsonLd(message.activity, context);
2087
- span.setAttribute("activitypub.activity.type", getTypeId(activity).href);
2088
- if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
2089
- const cacheKey = activity.id == null ? null : [
2090
- ...this.kvPrefixes.activityIdempotence,
2091
- context.origin,
2092
- activity.id.href
2093
- ];
2094
- if (cacheKey != null) {
2095
- if (await this.kv.get(cacheKey) === true) {
2096
- logger.debug("Activity {activityId} has already been processed.", {
2097
- activityId: activity.id?.href,
2098
- activity: message.activity,
2099
- recipient: message.identifier
2100
- });
2101
- return;
2102
- }
2103
- }
2104
- await this._getTracer().startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (span) => {
2105
- const dispatched = this.inboxListeners?.dispatchWithClass(activity);
2106
- if (dispatched == null) {
2107
- logger.error("Unsupported activity type:\n{activity}", {
2108
- activityId: activity.id?.href,
2109
- activity: message.activity,
2110
- recipient: message.identifier,
2111
- trial: message.attempt
2112
- });
2113
- span.setStatus({
2114
- code: SpanStatusCode.ERROR,
2115
- message: `Unsupported activity type: ${getTypeId(activity).href}`
2116
- });
2117
- span.end();
2118
- return;
2119
- }
2120
- const { class: cls, listener } = dispatched;
2121
- span.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
2122
- try {
2123
- await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, getTypeId(activity).href), activity);
2124
- } catch (error) {
3139
+ await this._getTracer().startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (listenerSpan) => {
3140
+ let activity = null;
3141
+ let cacheKey = null;
3142
+ let activityType;
3143
+ const reportInboxError = async (error) => {
2125
3144
  try {
2126
3145
  await this.inboxErrorHandler?.(context, error);
2127
3146
  } catch (error) {
2128
3147
  logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
2129
3148
  error,
2130
3149
  trial: message.attempt,
2131
- activityId: activity.id?.href,
3150
+ activityId: activity?.id?.href,
2132
3151
  activity: message.activity,
2133
3152
  recipient: message.identifier
2134
3153
  });
2135
3154
  }
3155
+ };
3156
+ const handleRetriableFailure = async (error) => {
3157
+ await reportInboxError(error);
2136
3158
  if (this.inboxQueue?.nativeRetrial) {
2137
3159
  logger.error("Failed to process the incoming activity {activityId}; backend will handle retry:\n{error}", {
2138
3160
  error,
2139
- activityId: activity.id?.href,
3161
+ activityId: activity?.id?.href,
2140
3162
  activity: message.activity,
2141
3163
  recipient: message.identifier
2142
3164
  });
2143
- span.setStatus({
3165
+ listenerSpan.setStatus({
2144
3166
  code: SpanStatusCode.ERROR,
2145
3167
  message: String(error)
2146
3168
  });
2147
- span.end();
3169
+ listenerSpan.end();
2148
3170
  throw error;
2149
3171
  }
2150
3172
  const delay = this.inboxRetryPolicy({
@@ -2155,34 +3177,165 @@ var FederationImpl = class extends FederationBuilderImpl {
2155
3177
  logger.error("Failed to process the incoming activity {activityId} (attempt #{attempt}); retry...:\n{error}", {
2156
3178
  error,
2157
3179
  attempt: message.attempt,
2158
- activityId: activity.id?.href,
3180
+ activityId: activity?.id?.href,
2159
3181
  activity: message.activity,
2160
3182
  recipient: message.identifier
2161
3183
  });
2162
- await this.inboxQueue?.enqueue({
3184
+ if (this.inboxQueue == null) {
3185
+ listenerSpan.setStatus({
3186
+ code: SpanStatusCode.ERROR,
3187
+ message: String(error)
3188
+ });
3189
+ listenerSpan.end();
3190
+ throw error;
3191
+ }
3192
+ const retryMessage = {
2163
3193
  ...message,
2164
3194
  attempt: message.attempt + 1
2165
- }, { delay: Temporal.Duration.compare(delay, { seconds: 0 }) < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay });
2166
- } else logger.error("Failed to process the incoming activity {activityId} after {trial} attempts; giving up:\n{error}", {
2167
- error,
3195
+ };
3196
+ await this.inboxQueue.enqueue(retryMessage, { delay: clampNegativeDelay(delay) });
3197
+ if (activityType != null) {
3198
+ getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
3199
+ role: "inbox",
3200
+ queue: this.inboxQueue,
3201
+ activityType
3202
+ }, retryMessage.attempt);
3203
+ recordInboxActivity(this.meterProvider, "retried", activityType);
3204
+ }
3205
+ } else {
3206
+ logger.error("Failed to process the incoming activity {activityId} after {trial} attempts; giving up:\n{error}", {
3207
+ error,
3208
+ activityId: activity?.id?.href,
3209
+ activity: message.activity,
3210
+ recipient: message.identifier
3211
+ });
3212
+ if (activityType != null) recordInboxActivity(this.meterProvider, "abandoned", activityType);
3213
+ }
3214
+ listenerSpan.setStatus({
3215
+ code: SpanStatusCode.ERROR,
3216
+ message: String(error)
3217
+ });
3218
+ listenerSpan.end();
3219
+ };
3220
+ let dispatched;
3221
+ let parseInput = void 0;
3222
+ let parseContextLoader = context.contextLoader;
3223
+ try {
3224
+ const hasSignatureField = hasSignature(message.activity);
3225
+ const shouldParseFromNormalizedSignedPayload = message.ldSignatureVerified === true || message.normalizedActivity != null || message.ldSignatureVerified == null && hasSignatureField;
3226
+ const parseContext = hasSignatureField ? {
3227
+ ...context,
3228
+ contextLoader: getNormalizationContextLoader(context.contextLoader)
3229
+ } : {
3230
+ ...context,
3231
+ contextLoader: wrapContextLoaderForJsonLd(context.contextLoader)
3232
+ };
3233
+ parseContextLoader = parseContext.contextLoader;
3234
+ let normalizedActivity;
3235
+ if (shouldParseFromNormalizedSignedPayload) {
3236
+ normalizedActivity = message.normalizedActivity ?? await compactJsonLd(message.activity, context.contextLoader);
3237
+ assertSafeJsonLd(normalizedActivity);
3238
+ }
3239
+ parseInput = shouldParseFromNormalizedSignedPayload ? detachSignature(normalizedActivity) : hasSignatureField ? detachSignature(message.activity) : message.activity;
3240
+ activity = await Activity.fromJsonLd(parseInput, parseContext);
3241
+ activityType = getTypeId(activity).href;
3242
+ span.setAttribute("activitypub.activity.type", activityType);
3243
+ listenerSpan.setAttribute("activitypub.activity.type", activityType);
3244
+ onActivityType?.(activityType);
3245
+ if (activity.id != null) {
3246
+ span.setAttribute("activitypub.activity.id", activity.id.href);
3247
+ listenerSpan.setAttribute("activitypub.activity.id", activity.id.href);
3248
+ }
3249
+ cacheKey = activity.id == null ? null : [
3250
+ ...this.kvPrefixes.activityIdempotence,
3251
+ context.origin,
3252
+ activity.id.href
3253
+ ];
3254
+ if (cacheKey != null) {
3255
+ if (await this.kv.get(cacheKey) === true) {
3256
+ logger.debug("Activity {activityId} has already been processed.", {
3257
+ activityId: activity.id?.href,
3258
+ activity: message.activity,
3259
+ recipient: message.identifier
3260
+ });
3261
+ recordInboxActivity(this.meterProvider, "rejected", activityType);
3262
+ listenerSpan.end();
3263
+ return;
3264
+ }
3265
+ }
3266
+ dispatched = this.inboxListeners?.dispatchWithClass(activity);
3267
+ } catch (error) {
3268
+ if (activity == null && error instanceof RangeError && await hasMalformedKnownTemporalLiteral(parseInput, parseContextLoader)) {
3269
+ await reportInboxError(error);
3270
+ logger.error("Failed to parse the queued incoming activity {activityId}:\n{error}", {
3271
+ error,
3272
+ trial: message.attempt,
3273
+ activityId: null,
3274
+ activity: message.activity,
3275
+ recipient: message.identifier
3276
+ });
3277
+ listenerSpan.setStatus({
3278
+ code: SpanStatusCode.ERROR,
3279
+ message: String(error)
3280
+ });
3281
+ listenerSpan.end();
3282
+ return;
3283
+ }
3284
+ if (isPermanentInboxParseError(error)) {
3285
+ await reportInboxError(error);
3286
+ logger.error("Failed to parse the queued incoming activity {activityId}:\n{error}", {
3287
+ error,
3288
+ trial: message.attempt,
3289
+ activityId: activity?.id?.href,
3290
+ activity: message.activity,
3291
+ recipient: message.identifier
3292
+ });
3293
+ listenerSpan.setStatus({
3294
+ code: SpanStatusCode.ERROR,
3295
+ message: String(error)
3296
+ });
3297
+ listenerSpan.end();
3298
+ return;
3299
+ }
3300
+ await handleRetriableFailure(error);
3301
+ return;
3302
+ }
3303
+ if (dispatched == null) {
3304
+ logger.error("Unsupported activity type:\n{activity}", {
2168
3305
  activityId: activity.id?.href,
2169
3306
  activity: message.activity,
2170
- recipient: message.identifier
3307
+ recipient: message.identifier,
3308
+ trial: message.attempt
2171
3309
  });
2172
- span.setStatus({
3310
+ listenerSpan.setStatus({
2173
3311
  code: SpanStatusCode.ERROR,
2174
- message: String(error)
3312
+ message: `Unsupported activity type: ${activityType}`
2175
3313
  });
2176
- span.end();
3314
+ recordInboxActivity(this.meterProvider, "rejected", activityType);
3315
+ listenerSpan.end();
3316
+ return;
3317
+ }
3318
+ const { class: cls, listener } = dispatched;
3319
+ listenerSpan.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
3320
+ try {
3321
+ const started = performance.now();
3322
+ try {
3323
+ await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, activityType), activity);
3324
+ } finally {
3325
+ getFederationMetrics(this.meterProvider).recordInboxProcessingDuration(activityType, getDurationMs(started));
3326
+ }
3327
+ recordInboxActivity(this.meterProvider, "processed", activityType);
3328
+ } catch (error) {
3329
+ await handleRetriableFailure(error);
2177
3330
  return;
2178
3331
  }
2179
3332
  if (cacheKey != null) await this.kv.set(cacheKey, true, { ttl: Temporal.Duration.from({ days: 1 }) });
2180
3333
  logger.info("Activity {activityId} has been processed.", {
2181
- activityId: activity.id?.href,
3334
+ activityId: activity?.id?.href,
2182
3335
  activity: message.activity,
2183
3336
  recipient: message.identifier
2184
3337
  });
2185
- span.end();
3338
+ listenerSpan.end();
2186
3339
  });
2187
3340
  }
2188
3341
  startQueue(contextData, options = {}) {
@@ -2303,6 +3456,7 @@ var FederationImpl = class extends FederationBuilderImpl {
2303
3456
  sharedInbox: inboxes[inbox].sharedInbox,
2304
3457
  headers: collectionSync == null ? void 0 : new Headers({ "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) }),
2305
3458
  specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock),
3459
+ meterProvider: this.meterProvider,
2306
3460
  tracerProvider: this.tracerProvider
2307
3461
  }));
2308
3462
  await Promise.all(promises);
@@ -2350,9 +3504,11 @@ var FederationImpl = class extends FederationBuilderImpl {
2350
3504
  });
2351
3505
  }
2352
3506
  const { outboxQueue } = this;
2353
- if (outboxQueue.enqueueMany == null) {
2354
- const promises = messages.map((m) => outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }));
2355
- const errors = (await Promise.allSettled(promises)).filter((r) => r.status === "rejected").map((r) => r.reason);
3507
+ if (outboxQueue.enqueueMany == null || orderingKey != null) {
3508
+ const errors = (await Promise.allSettled(messages.map(async (m) => {
3509
+ await outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey });
3510
+ recordOutboxEnqueue(this.meterProvider, outboxQueue, m.message);
3511
+ }))).filter((r) => r.status === "rejected").map((r) => r.reason);
2356
3512
  if (errors.length > 0) {
2357
3513
  logger.error("Failed to enqueue activity {activityId} to send later: {errors}", {
2358
3514
  activityId: activity.id.href,
@@ -2361,30 +3517,24 @@ var FederationImpl = class extends FederationBuilderImpl {
2361
3517
  if (errors.length > 1) throw new AggregateError(errors, `Failed to enqueue activity ${activityId} to send later.`);
2362
3518
  throw errors[0];
2363
3519
  }
2364
- } else if (orderingKey != null) {
2365
- const promises = messages.map((m) => outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }));
2366
- const errors = (await Promise.allSettled(promises)).filter((r) => r.status === "rejected").map((r) => r.reason);
2367
- if (errors.length > 0) {
2368
- logger.error("Failed to enqueue activity {activityId} to send later: {errors}", {
3520
+ } else {
3521
+ try {
3522
+ await outboxQueue.enqueueMany(messages.map((m) => m.message));
3523
+ } catch (error) {
3524
+ logger.error("Failed to enqueue activity {activityId} to send later: {error}", {
2369
3525
  activityId: activity.id.href,
2370
- errors
3526
+ error
2371
3527
  });
2372
- if (errors.length > 1) throw new AggregateError(errors, `Failed to enqueue activity ${activityId} to send later.`);
2373
- throw errors[0];
3528
+ throw error;
2374
3529
  }
2375
- } else try {
2376
- await outboxQueue.enqueueMany(messages.map((m) => m.message));
2377
- } catch (error) {
2378
- logger.error("Failed to enqueue activity {activityId} to send later: {error}", {
2379
- activityId: activity.id.href,
2380
- error
2381
- });
2382
- throw error;
3530
+ for (const m of messages) recordOutboxEnqueue(this.meterProvider, outboxQueue, m.message);
2383
3531
  }
2384
3532
  }
2385
3533
  fetch(request, options) {
2386
3534
  return withContext({ requestId: getRequestId(request) }, async () => {
2387
3535
  const tracer = this._getTracer();
3536
+ const metricState = {};
3537
+ const metricStart = performance.now();
2388
3538
  return await tracer.startActiveSpan(request.method, {
2389
3539
  kind: SpanKind.SERVER,
2390
3540
  attributes: {
@@ -2408,10 +3558,12 @@ var FederationImpl = class extends FederationBuilderImpl {
2408
3558
  response = await this.#fetch(request, {
2409
3559
  ...options,
2410
3560
  span,
2411
- tracer
3561
+ tracer,
3562
+ metricState
2412
3563
  });
2413
3564
  if (acceptsJsonLd(request)) response.headers.set("Vary", "Accept");
2414
3565
  } catch (error) {
3566
+ getFederationMetrics(this.meterProvider).recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), { routeTemplate: metricState.routeTemplate });
2415
3567
  span.setStatus({
2416
3568
  code: SpanStatusCode.ERROR,
2417
3569
  message: `${error}`
@@ -2424,6 +3576,10 @@ var FederationImpl = class extends FederationBuilderImpl {
2424
3576
  });
2425
3577
  throw error;
2426
3578
  }
3579
+ getFederationMetrics(this.meterProvider).recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), {
3580
+ statusCode: response.status,
3581
+ routeTemplate: metricState.routeTemplate
3582
+ });
2427
3583
  if (span.isRecording()) {
2428
3584
  span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
2429
3585
  for (const [k, v] of response.headers) span.setAttribute(ATTR_HTTP_RESPONSE_HEADER(k), [v]);
@@ -2449,13 +3605,18 @@ var FederationImpl = class extends FederationBuilderImpl {
2449
3605
  });
2450
3606
  });
2451
3607
  }
2452
- async #fetch(request, { onNotFound, onNotAcceptable, onUnauthorized, contextData, span, tracer }) {
3608
+ async #fetch(request, { onNotFound, onNotAcceptable, onUnauthorized, contextData, span, tracer, metricState }) {
2453
3609
  onNotFound ??= notFound;
2454
3610
  onNotAcceptable ??= notAcceptable;
2455
3611
  onUnauthorized ??= unauthorized;
2456
3612
  const url = new URL(request.url);
2457
3613
  const route = this.router.route(url.pathname);
2458
- if (route == null) return await onNotFound(request);
3614
+ if (route == null) {
3615
+ metricState.endpoint = "not_found";
3616
+ return await onNotFound(request);
3617
+ }
3618
+ metricState.routeTemplate = route.template;
3619
+ metricState.endpoint = getEndpointCategory(route.name);
2459
3620
  span.updateName(`${request.method} ${route.template}`);
2460
3621
  let context = this.#createContext(request, contextData);
2461
3622
  const routeName = route.name.replace(/:.*$/, "");
@@ -2468,26 +3629,43 @@ var FederationImpl = class extends FederationBuilderImpl {
2468
3629
  actorAliasMapper: this.actorCallbacks?.aliasMapper,
2469
3630
  webFingerLinksDispatcher: this.webFingerLinksDispatcher,
2470
3631
  onNotFound,
2471
- tracer
3632
+ tracer,
3633
+ meterProvider: this._meterProvider
2472
3634
  });
2473
3635
  case "nodeInfoJrd": return await handleNodeInfoJrd(request, context);
2474
3636
  case "nodeInfo": return await handleNodeInfo(request, {
2475
3637
  context,
2476
3638
  nodeInfoDispatcher: this.nodeInfoDispatcher
2477
3639
  });
3640
+ case "benchmarkStats": return await handleBenchmarkStats(request, this.benchmarkMetricReader);
3641
+ case "benchmarkTrigger": return await handleBenchmarkTrigger(request, context, this.benchmarkTriggerOptions);
3642
+ }
3643
+ if (request.method !== "POST" && !acceptsJsonLd(request)) {
3644
+ metricState.endpoint = "not_acceptable";
3645
+ const response = await onNotAcceptable(request);
3646
+ const collectionRoute = getCollectionMetricRoute(routeName);
3647
+ if (collectionRoute != null) recordCollectionRequest(this._meterProvider, {
3648
+ ...collectionRoute,
3649
+ page: url.searchParams.get("cursor") != null,
3650
+ result: "not_acceptable",
3651
+ statusCode: response.status
3652
+ });
3653
+ return response;
2478
3654
  }
2479
- if (request.method !== "POST" && !acceptsJsonLd(request)) return await onNotAcceptable(request);
2480
3655
  switch (routeName) {
2481
3656
  case "actor":
2482
- context = this.#createContext(request, contextData, { invokedFromActorDispatcher: { identifier: route.values.identifier } });
3657
+ case "actorAlias": {
3658
+ const identifier = route.name.startsWith("actorAlias:") ? route.name.substring(ACTOR_ALIAS_PREFIX.length) : route.values.identifier;
3659
+ context = this.#createContext(request, contextData, { invokedFromActorDispatcher: { identifier } });
2483
3660
  return await handleActor(request, {
2484
- identifier: route.values.identifier,
3661
+ identifier,
2485
3662
  context,
2486
3663
  actorDispatcher: this.actorCallbacks?.dispatcher,
2487
3664
  authorizePredicate: this.actorCallbacks?.authorizePredicate,
2488
3665
  onUnauthorized,
2489
3666
  onNotFound
2490
3667
  });
3668
+ }
2491
3669
  case "object": {
2492
3670
  const typeId = route.name.replace(/^object:/, "");
2493
3671
  const callbacks = this.objectCallbacks[typeId];
@@ -2533,6 +3711,7 @@ var FederationImpl = class extends FederationBuilderImpl {
2533
3711
  context,
2534
3712
  collectionCallbacks: this.outboxCallbacks,
2535
3713
  tracerProvider: this.tracerProvider,
3714
+ meterProvider: this._meterProvider,
2536
3715
  onUnauthorized,
2537
3716
  onNotFound
2538
3717
  });
@@ -2544,20 +3723,23 @@ var FederationImpl = class extends FederationBuilderImpl {
2544
3723
  context,
2545
3724
  collectionCallbacks: this.inboxCallbacks,
2546
3725
  tracerProvider: this.tracerProvider,
3726
+ meterProvider: this._meterProvider,
2547
3727
  onUnauthorized,
2548
3728
  onNotFound
2549
3729
  });
2550
3730
  context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
2551
- case "sharedInbox":
3731
+ case "sharedInbox": {
2552
3732
  if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
2553
3733
  const identity = await this.sharedInboxKeyDispatcher(context);
2554
3734
  if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
2555
3735
  }
2556
3736
  if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
3737
+ const inboxContextFactory = context.toInboxContext.bind(context);
3738
+ inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
2557
3739
  return await handleInbox(request, {
2558
3740
  recipient: route.values.identifier ?? null,
2559
3741
  context,
2560
- inboxContextFactory: context.toInboxContext.bind(context),
3742
+ inboxContextFactory,
2561
3743
  kv: this.kv,
2562
3744
  kvPrefixes: this.kvPrefixes,
2563
3745
  queue: this.inboxQueue,
@@ -2569,9 +3751,11 @@ var FederationImpl = class extends FederationBuilderImpl {
2569
3751
  signatureTimeWindow: this.signatureTimeWindow,
2570
3752
  skipSignatureVerification: this.skipSignatureVerification,
2571
3753
  inboxChallengePolicy: this.inboxChallengePolicy,
3754
+ meterProvider: this.meterProvider,
2572
3755
  tracerProvider: this.tracerProvider,
2573
3756
  idempotencyStrategy: this.idempotencyStrategy
2574
3757
  });
3758
+ }
2575
3759
  case "following": return await handleCollection(request, {
2576
3760
  name: "following",
2577
3761
  identifier: route.values.identifier,
@@ -2579,6 +3763,7 @@ var FederationImpl = class extends FederationBuilderImpl {
2579
3763
  context,
2580
3764
  collectionCallbacks: this.followingCallbacks,
2581
3765
  tracerProvider: this.tracerProvider,
3766
+ meterProvider: this._meterProvider,
2582
3767
  onUnauthorized,
2583
3768
  onNotFound
2584
3769
  });
@@ -2602,6 +3787,7 @@ var FederationImpl = class extends FederationBuilderImpl {
2602
3787
  filterPredicate: baseUrl != null ? ((i) => (i instanceof URL ? i.href : i.id?.href ?? "").startsWith(baseUrl)) : void 0,
2603
3788
  collectionCallbacks: this.followersCallbacks,
2604
3789
  tracerProvider: this.tracerProvider,
3790
+ meterProvider: this._meterProvider,
2605
3791
  onUnauthorized,
2606
3792
  onNotFound
2607
3793
  });
@@ -2613,6 +3799,7 @@ var FederationImpl = class extends FederationBuilderImpl {
2613
3799
  context,
2614
3800
  collectionCallbacks: this.likedCallbacks,
2615
3801
  tracerProvider: this.tracerProvider,
3802
+ meterProvider: this._meterProvider,
2616
3803
  onUnauthorized,
2617
3804
  onNotFound
2618
3805
  });
@@ -2623,6 +3810,7 @@ var FederationImpl = class extends FederationBuilderImpl {
2623
3810
  context,
2624
3811
  collectionCallbacks: this.featuredCallbacks,
2625
3812
  tracerProvider: this.tracerProvider,
3813
+ meterProvider: this._meterProvider,
2626
3814
  onUnauthorized,
2627
3815
  onNotFound
2628
3816
  });
@@ -2633,6 +3821,7 @@ var FederationImpl = class extends FederationBuilderImpl {
2633
3821
  context,
2634
3822
  collectionCallbacks: this.featuredTagsCallbacks,
2635
3823
  tracerProvider: this.tracerProvider,
3824
+ meterProvider: this._meterProvider,
2636
3825
  onUnauthorized,
2637
3826
  onNotFound
2638
3827
  });
@@ -2645,6 +3834,7 @@ var FederationImpl = class extends FederationBuilderImpl {
2645
3834
  values: route.values,
2646
3835
  collectionCallbacks: callbacks,
2647
3836
  tracerProvider: this.tracerProvider,
3837
+ meterProvider: this._meterProvider,
2648
3838
  onUnauthorized,
2649
3839
  onNotFound
2650
3840
  });
@@ -2658,17 +3848,64 @@ var FederationImpl = class extends FederationBuilderImpl {
2658
3848
  values: route.values,
2659
3849
  collectionCallbacks: callbacks,
2660
3850
  tracerProvider: this.tracerProvider,
3851
+ meterProvider: this._meterProvider,
2661
3852
  onUnauthorized,
2662
3853
  onNotFound
2663
3854
  });
2664
3855
  }
2665
3856
  default: {
3857
+ metricState.endpoint = "not_found";
2666
3858
  const response = onNotFound(request);
2667
3859
  return response instanceof Promise ? await response : response;
2668
3860
  }
2669
3861
  }
2670
3862
  }
2671
3863
  };
3864
+ function getEndpointCategory(routeName) {
3865
+ if (routeName.startsWith("object:")) return "object";
3866
+ if (routeName.startsWith("collection:") || routeName.startsWith("orderedCollection:")) return "collection";
3867
+ if (routeName.startsWith("actorAlias:")) return "actor";
3868
+ switch (routeName) {
3869
+ case "webfinger": return "webfinger";
3870
+ case "nodeInfoJrd":
3871
+ case "nodeInfo": return "nodeinfo";
3872
+ case "actor": return "actor";
3873
+ case "inbox": return "inbox";
3874
+ case "sharedInbox": return "shared_inbox";
3875
+ case "outbox": return "outbox";
3876
+ case "following": return "following";
3877
+ case "followers": return "followers";
3878
+ case "liked": return "liked";
3879
+ case "featured": return "featured";
3880
+ case "featuredTags": return "featured_tags";
3881
+ case "benchmarkStats":
3882
+ case "benchmarkTrigger": return "benchmark";
3883
+ default: return "not_found";
3884
+ }
3885
+ }
3886
+ function getCollectionMetricRoute(routeName) {
3887
+ switch (routeName) {
3888
+ case "inbox":
3889
+ case "outbox":
3890
+ case "following":
3891
+ case "followers":
3892
+ case "liked":
3893
+ case "featured": return {
3894
+ kind: routeName,
3895
+ dispatcher: "built_in"
3896
+ };
3897
+ case "featuredTags": return {
3898
+ kind: "featured_tags",
3899
+ dispatcher: "built_in"
3900
+ };
3901
+ case "collection":
3902
+ case "orderedCollection": return {
3903
+ kind: "custom",
3904
+ dispatcher: "custom"
3905
+ };
3906
+ default: return;
3907
+ }
3908
+ }
2672
3909
  const FANOUT_THRESHOLD = 5;
2673
3910
  var ContextImpl = class ContextImpl {
2674
3911
  url;
@@ -2730,13 +3967,16 @@ var ContextImpl = class ContextImpl {
2730
3967
  get tracerProvider() {
2731
3968
  return this.federation.tracerProvider;
2732
3969
  }
3970
+ get meterProvider() {
3971
+ return this.federation.meterProvider;
3972
+ }
2733
3973
  getNodeInfoUri() {
2734
3974
  const path = this.federation.router.build("nodeInfo", {});
2735
3975
  if (path == null) throw new RouterError("No NodeInfo dispatcher registered.");
2736
3976
  return new URL(path, this.canonicalOrigin);
2737
3977
  }
2738
3978
  getActorUri(identifier) {
2739
- const path = this.federation.router.build("actor", { identifier });
3979
+ const path = this.federation.router.build(`actorAlias:${identifier}`, {}) ?? this.federation.router.build("actor", { identifier });
2740
3980
  if (path == null) throw new RouterError("No actor dispatcher registered.");
2741
3981
  return new URL(path, this.canonicalOrigin);
2742
3982
  }
@@ -2802,8 +4042,8 @@ var ContextImpl = class ContextImpl {
2802
4042
  type: "inbox",
2803
4043
  identifier: void 0
2804
4044
  };
2805
- const identifier = route.values.identifier;
2806
- if (route.name === "actor") return {
4045
+ const identifier = route.name.startsWith("actorAlias:") ? route.name.substring(ACTOR_ALIAS_PREFIX.length) : route.values.identifier;
4046
+ if (route.name === "actor" || route.name.startsWith("actorAlias:")) return {
2807
4047
  type: "actor",
2808
4048
  identifier
2809
4049
  };
@@ -2969,6 +4209,7 @@ var ContextImpl = class ContextImpl {
2969
4209
  contextLoader: options.contextLoader ?? this.contextLoader,
2970
4210
  userAgent: options.userAgent ?? this.federation.userAgent,
2971
4211
  tracerProvider: options.tracerProvider ?? this.tracerProvider,
4212
+ meterProvider: options.meterProvider ?? this.meterProvider,
2972
4213
  allowPrivateAddress: this.federation.allowPrivateAddress
2973
4214
  });
2974
4215
  }
@@ -2995,6 +4236,7 @@ var ContextImpl = class ContextImpl {
2995
4236
  ...options,
2996
4237
  userAgent: options.userAgent ?? this.federation.userAgent,
2997
4238
  tracerProvider: options.tracerProvider ?? this.tracerProvider,
4239
+ meterProvider: options.meterProvider ?? this.federation._meterProvider,
2998
4240
  allowPrivateAddress: this.federation.allowPrivateAddress
2999
4241
  });
3000
4242
  }
@@ -3151,6 +4393,12 @@ var ContextImpl = class ContextImpl {
3151
4393
  };
3152
4394
  if (!this.federation.manuallyStartQueue) this.federation._startQueueInternal(this.data);
3153
4395
  await this.federation.fanoutQueue.enqueue(message, { orderingKey: options.orderingKey });
4396
+ getFederationMetrics(this.federation.meterProvider).recordQueueTaskEnqueued({
4397
+ role: "fanout",
4398
+ queue: this.federation.fanoutQueue,
4399
+ activityType: message.activityType
4400
+ }, 0);
4401
+ recordFanoutRecipients(this.federation.meterProvider, globalThis.Object.keys(message.inboxes).length, message.activityType);
3154
4402
  return true;
3155
4403
  }
3156
4404
  async *getFollowers(identifier) {
@@ -3214,6 +4462,7 @@ var ContextImpl = class ContextImpl {
3214
4462
  if (await verifyObject(Activity, json, {
3215
4463
  contextLoader,
3216
4464
  documentLoader: options.documentLoader ?? this.documentLoader,
4465
+ meterProvider: this.meterProvider,
3217
4466
  tracerProvider: options.tracerProvider ?? this.tracerProvider,
3218
4467
  keyCache
3219
4468
  }) == null) {
@@ -3272,6 +4521,7 @@ var ContextImpl = class ContextImpl {
3272
4521
  const routeResult = await routeActivity({
3273
4522
  context: this,
3274
4523
  json,
4524
+ ldSignatureVerified: false,
3275
4525
  activity,
3276
4526
  recipient,
3277
4527
  inboxListeners: this.federation.inboxListeners,
@@ -3281,6 +4531,7 @@ var ContextImpl = class ContextImpl {
3281
4531
  kvPrefixes: this.federation.kvPrefixes,
3282
4532
  queue: this.federation.inboxQueue,
3283
4533
  span,
4534
+ meterProvider: this.federation.meterProvider,
3284
4535
  tracerProvider: options.tracerProvider ?? this.tracerProvider,
3285
4536
  idempotencyStrategy: this.federation.idempotencyStrategy
3286
4537
  });
@@ -3355,6 +4606,7 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
3355
4606
  contextLoader: options.contextLoader ?? this.contextLoader,
3356
4607
  documentLoader: options.documentLoader ?? this.documentLoader,
3357
4608
  timeWindow: this.federation.signatureTimeWindow,
4609
+ meterProvider: this.meterProvider,
3358
4610
  tracerProvider: options.tracerProvider ?? this.tracerProvider
3359
4611
  });
3360
4612
  }
@@ -3479,6 +4731,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
3479
4731
  activityType: ctx.activityType,
3480
4732
  inbox: new URL(inbox),
3481
4733
  sharedInbox: inboxes[inbox].sharedInbox,
4734
+ meterProvider: ctx.meterProvider,
3482
4735
  tracerProvider: ctx.tracerProvider,
3483
4736
  specDeterminer: new KvSpecDeterminer(ctx.federation.kv, ctx.federation.kvPrefixes.httpMessageSignaturesSpec, ctx.federation.firstKnock)
3484
4737
  }));
@@ -3529,8 +4782,10 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
3529
4782
  }
3530
4783
  const { outboxQueue } = ctx.federation;
3531
4784
  if (outboxQueue.enqueueMany == null || orderingKey != null) {
3532
- const promises = messages.map((m) => outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }));
3533
- const errors = (await Promise.allSettled(promises)).filter((r) => r.status === "rejected").map((r) => r.reason);
4785
+ const errors = (await Promise.allSettled(messages.map(async (m) => {
4786
+ await outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey });
4787
+ recordOutboxEnqueue(ctx.federation.meterProvider, outboxQueue, m.message);
4788
+ }))).filter((r) => r.status === "rejected").map((r) => r.reason);
3534
4789
  if (errors.length > 0) {
3535
4790
  logger.error("Failed to enqueue activity {activityId} to forward later:\n{errors}", {
3536
4791
  activityId: ctx.activityId,
@@ -3539,19 +4794,30 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
3539
4794
  if (errors.length > 1) throw new AggregateError(errors, `Failed to enqueue activity ${ctx.activityId} to forward later.`);
3540
4795
  throw errors[0];
3541
4796
  }
3542
- } else try {
3543
- await outboxQueue.enqueueMany(messages.map((m) => m.message));
3544
- } catch (error) {
3545
- logger.error("Failed to enqueue activity {activityId} to forward later:\n{error}", {
3546
- activityId: ctx.activityId,
3547
- error
3548
- });
3549
- throw error;
4797
+ } else {
4798
+ try {
4799
+ await outboxQueue.enqueueMany(messages.map((m) => m.message));
4800
+ } catch (error) {
4801
+ logger.error("Failed to enqueue activity {activityId} to forward later:\n{error}", {
4802
+ activityId: ctx.activityId,
4803
+ error
4804
+ });
4805
+ throw error;
4806
+ }
4807
+ for (const m of messages) recordOutboxEnqueue(ctx.federation.meterProvider, outboxQueue, m.message);
3550
4808
  }
3551
4809
  return true;
3552
4810
  }
3553
4811
  var InboxContextImpl = class InboxContextImpl extends ContextImpl {
3554
4812
  recipient;
4813
+ /**
4814
+ * The original received activity payload.
4815
+ *
4816
+ * Fedify may normalize a Linked Data Signature payload internally for safe
4817
+ * parsing, but forwarding must keep the sender's payload unchanged so
4818
+ * third-party signatures/proofs remain intact.
4819
+ * @internal
4820
+ */
3555
4821
  activity;
3556
4822
  activityId;
3557
4823
  activityType;
@@ -3684,4 +4950,4 @@ function getRequestId(request) {
3684
4950
  return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
3685
4951
  }
3686
4952
  //#endregion
3687
- export { handleNodeInfoJrd as _, OutboxContextImpl as a, handleActor as c, handleInbox as d, handleObject as f, handleNodeInfo as g, respondWithObjectIfAcceptable as h, KvSpecDeterminer as i, handleCollection as l, respondWithObject as m, FederationImpl as n, createFederation as o, handleOutbox as p, InboxContextImpl as r, handleWebFinger as s, ContextImpl as t, handleCustomCollection as u, actorDehydrator as v, autoIdAssigner as y };
4953
+ export { handleNodeInfo as _, OutboxContextImpl as a, autoIdAssigner as b, handleActor as c, handleInbox as d, handleObject as f, handleBenchmarkTrigger as g, respondWithObjectIfAcceptable as h, KvSpecDeterminer as i, handleCollection as l, respondWithObject as m, FederationImpl as n, createFederation as o, handleOutbox as p, InboxContextImpl as r, handleWebFinger as s, ContextImpl as t, handleCustomCollection as u, handleNodeInfoJrd as v, actorDehydrator as y };