@fedify/fedify 2.3.0-dev.1110 → 2.3.0-dev.1119

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 (97) hide show
  1. package/dist/{assert_rejects-B-qJtC9Z.mjs → assert_rejects-DQP-q39h.mjs} +27 -2
  2. package/dist/{builder-B-Y6fwSu.mjs → builder-Ond_h57y.mjs} +3 -3
  3. package/dist/compat/mod.d.cts +1 -1
  4. package/dist/compat/mod.d.ts +1 -1
  5. package/dist/compat/outgoing-jsonld.test.mjs +1 -1
  6. package/dist/compat/public-audience.test.mjs +1 -1
  7. package/dist/compat/transformers.test.mjs +2 -2
  8. package/dist/{context-C0C_sRha.d.cts → context-Ch-ZLyTQ.d.cts} +1 -1
  9. package/dist/{context-Dqgt8saU.d.ts → context-cSUMk2da.d.ts} +1 -1
  10. package/dist/{deno-hqC7tKJn.mjs → deno-DVsHS7rA.mjs} +1 -1
  11. package/dist/{docloader-BOEuuXkX.mjs → docloader-WsWfKaE5.mjs} +2 -2
  12. package/dist/federation/builder.test.mjs +3 -3
  13. package/dist/federation/collection.test.mjs +2 -2
  14. package/dist/federation/handler.test.mjs +8 -7
  15. package/dist/federation/idempotency.test.mjs +5 -5
  16. package/dist/federation/inbox.test.mjs +1 -1
  17. package/dist/federation/keycache.test.mjs +1 -1
  18. package/dist/federation/kv.test.mjs +2 -2
  19. package/dist/federation/metrics.test.d.mts +2 -0
  20. package/dist/federation/metrics.test.mjs +107 -0
  21. package/dist/federation/middleware.test.mjs +390 -10
  22. package/dist/federation/mod.cjs +1 -1
  23. package/dist/federation/mod.d.cts +2 -2
  24. package/dist/federation/mod.d.ts +2 -2
  25. package/dist/federation/mod.js +1 -1
  26. package/dist/federation/mq.test.mjs +2 -2
  27. package/dist/federation/negotiation.test.mjs +2 -2
  28. package/dist/federation/router.test.mjs +2 -2
  29. package/dist/federation/send.test.mjs +11 -11
  30. package/dist/federation/webfinger.test.mjs +3 -3
  31. package/dist/{getMachineId-bsd-etIyxDet.mjs → getMachineId-bsd-BY01PL1n.mjs} +1 -1
  32. package/dist/{getMachineId-darwin-D23zTf4g.mjs → getMachineId-darwin-Dr1gkBkp.mjs} +1 -1
  33. package/dist/{getMachineId-win-Dpap6v5i.mjs → getMachineId-win-QEYwcJiy.mjs} +1 -1
  34. package/dist/{http-O8MYWwk8.js → http-CouJSFVK.js} +461 -37
  35. package/dist/{http-DV0il3vk.cjs → http-CubOB9wq.cjs} +513 -35
  36. package/dist/{http-BDZeS5om.d.ts → http-D6LP89UO.d.ts} +7 -1
  37. package/dist/{http-C87EWkO0.d.cts → http-D6aw3j2U.d.cts} +7 -1
  38. package/dist/{http-BLopFpvC.mjs → http-DUV8ysti.mjs} +86 -37
  39. package/dist/{key-DW1EVmtP.mjs → key-BoWaYRHm.mjs} +1 -1
  40. package/dist/{kv-cache-C3NWWiTg.js → kv-cache-DBNpsneh.js} +1 -1
  41. package/dist/{kv-cache-Dya-TWMe.cjs → kv-cache-Dz31ATUT.cjs} +1 -1
  42. package/dist/{ld-BNkk2Yal.mjs → ld-B5K1mSuG.mjs} +60 -9
  43. package/dist/{send-hokVCPu6.mjs → metrics-C4attqv0.mjs} +124 -224
  44. package/dist/{middleware-D6FbOjuK.mjs → middleware-BDKFRjue.mjs} +1 -1
  45. package/dist/{middleware-DUWeXjZR.cjs → middleware-CmsDtIHI.cjs} +75 -309
  46. package/dist/{middleware-CjzI3aYo.js → middleware-Dtjz-hSk.js} +46 -280
  47. package/dist/{middleware-DA2WTBr4.mjs → middleware-t0jC8I99.mjs} +59 -34
  48. package/dist/{mod-DXY9JF28.d.cts → mod-B-Lin9Sy.d.ts} +25 -2
  49. package/dist/{mod-DHO9lk3D.d.ts → mod-BDhgfjP7.d.cts} +25 -2
  50. package/dist/{mod-B0rWmfW5.d.cts → mod-BR_BB0bh.d.cts} +1 -1
  51. package/dist/{mod-Dx3-hqyo.d.ts → mod-C6E8rkcz.d.ts} +1 -1
  52. package/dist/{mod-BhU_H1I_.d.ts → mod-DLrRb0dx.d.ts} +1 -1
  53. package/dist/{mod-CLPnQPsv.d.cts → mod-P9tE2WmM.d.cts} +1 -1
  54. package/dist/mod.cjs +4 -4
  55. package/dist/mod.d.cts +5 -5
  56. package/dist/mod.d.ts +5 -5
  57. package/dist/mod.js +4 -4
  58. package/dist/nodeinfo/client.test.mjs +2 -2
  59. package/dist/nodeinfo/handler.test.mjs +3 -3
  60. package/dist/nodeinfo/types.test.mjs +2 -2
  61. package/dist/otel/exporter.test.mjs +2 -2
  62. package/dist/{outgoing-jsonld-BgFLCJQ_.mjs → outgoing-jsonld-BNL8AC14.mjs} +1 -1
  63. package/dist/{owner-jvJAtR5O.mjs → owner-hDxI0ufu.mjs} +2 -2
  64. package/dist/{proof-BD92WeqV.cjs → proof-BUWfVr6Q.cjs} +78 -11
  65. package/dist/{proof-mfmHH9j0.mjs → proof-DhVuz4bc.mjs} +25 -7
  66. package/dist/{proof-5kT7OUPV.js → proof-n60t8o9P.js} +78 -11
  67. package/dist/send-BPhyR5Oo.mjs +225 -0
  68. package/dist/sig/accept.test.mjs +1 -1
  69. package/dist/sig/http.test.mjs +212 -6
  70. package/dist/sig/key.test.mjs +4 -4
  71. package/dist/sig/ld.test.mjs +138 -5
  72. package/dist/sig/mod.cjs +2 -2
  73. package/dist/sig/mod.d.cts +2 -2
  74. package/dist/sig/mod.d.ts +2 -2
  75. package/dist/sig/mod.js +2 -2
  76. package/dist/sig/owner.test.mjs +4 -4
  77. package/dist/sig/proof.test.mjs +167 -6
  78. package/dist/{std__assert-CRDpx_HF.mjs → std__assert-BTEgfoJo.mjs} +2 -27
  79. package/dist/utils/docloader.test.mjs +5 -5
  80. package/dist/utils/kv-cache.test.mjs +1 -1
  81. package/dist/utils/mod.cjs +1 -1
  82. package/dist/utils/mod.d.cts +1 -1
  83. package/dist/utils/mod.d.ts +1 -1
  84. package/dist/utils/mod.js +1 -1
  85. package/package.json +5 -5
  86. /package/dist/{accept-CceiKpCy.mjs → accept-CgDcxvjV.mjs} +0 -0
  87. /package/dist/{activity-listener-tztVvlNb.mjs → activity-listener-BeTGV3wc.mjs} +0 -0
  88. /package/dist/{client-B_A6mfn3.mjs → client-Bneh_DYR.mjs} +0 -0
  89. /package/dist/{collection-CA3V5zyK.mjs → collection-Cc3DVAhE.mjs} +0 -0
  90. /package/dist/{execAsync-DCBrgFiV.mjs → execAsync-Dxb7rNf3.mjs} +0 -0
  91. /package/dist/{getMachineId-linux-ObI47Hql.mjs → getMachineId-linux-Bbhofx-s.mjs} +0 -0
  92. /package/dist/{getMachineId-unsupported-Ddu-PFeh.mjs → getMachineId-unsupported-dIOte2Ct.mjs} +0 -0
  93. /package/dist/{keys-C3kae-6B.mjs → keys-CSYsOMFG.mjs} +0 -0
  94. /package/dist/{kv-x2IvBUyq.mjs → kv-QHE0oeM3.mjs} +0 -0
  95. /package/dist/{kv-cache-CiiNwT6W.mjs → kv-cache-DihufyAQ.mjs} +0 -0
  96. /package/dist/{public-audience-N3pyOx2p.mjs → public-audience-c9zmYKgA.mjs} +0 -0
  97. /package/dist/{types-BFowWFTT.mjs → types-D09GN0uZ.mjs} +0 -0
@@ -2,7 +2,7 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  import { getLogger } from "@logtape/logtape";
4
4
  import { CryptographicKey, Object as Object$1, isActor } from "@fedify/vocab";
5
- import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
5
+ import { SpanKind, SpanStatusCode, metrics, trace } from "@opentelemetry/api";
6
6
  import { encodeHex } from "byte-encodings/hex";
7
7
  import { Item, decodeDict, encodeDict, encodeItem } from "structured-field-values";
8
8
  import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
@@ -10,7 +10,7 @@ import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_URL_FULL } fro
10
10
  import { decodeBase64, encodeBase64 } from "byte-encodings/base64";
11
11
  //#region deno.json
12
12
  var name = "@fedify/fedify";
13
- var version = "2.3.0-dev.1110+3ba84466";
13
+ var version = "2.3.0-dev.1119+6cc02662";
14
14
  //#endregion
15
15
  //#region src/sig/accept.ts
16
16
  /**
@@ -151,6 +151,382 @@ function fulfillAcceptSignature(entry, localKeyId, localAlg) {
151
151
  };
152
152
  }
153
153
  //#endregion
154
+ //#region src/federation/metrics.ts
155
+ var FederationMetrics = class {
156
+ deliverySent;
157
+ deliveryPermanentFailure;
158
+ signatureVerificationFailure;
159
+ signatureVerificationDuration;
160
+ signatureKeyFetchDuration;
161
+ deliveryDuration;
162
+ inboxProcessingDuration;
163
+ httpServerRequestCount;
164
+ httpServerRequestDuration;
165
+ queueTaskEnqueued;
166
+ queueTaskStarted;
167
+ queueTaskCompleted;
168
+ queueTaskFailed;
169
+ queueTaskDuration;
170
+ queueTaskInFlight;
171
+ fanoutRecipients;
172
+ inboxActivity;
173
+ outboxActivity;
174
+ constructor(meterProvider) {
175
+ const meter = meterProvider.getMeter(name, version);
176
+ this.deliverySent = meter.createCounter("activitypub.delivery.sent", {
177
+ description: "ActivityPub delivery attempts.",
178
+ unit: "{attempt}"
179
+ });
180
+ this.deliveryPermanentFailure = meter.createCounter("activitypub.delivery.permanent_failure", {
181
+ description: "ActivityPub deliveries abandoned as permanent failures.",
182
+ unit: "{failure}"
183
+ });
184
+ this.signatureVerificationFailure = meter.createCounter("activitypub.signature.verification_failure", {
185
+ description: "ActivityPub signature verification failures.",
186
+ unit: "{failure}"
187
+ });
188
+ this.signatureVerificationDuration = meter.createHistogram("activitypub.signature.verification.duration", {
189
+ description: "Duration of ActivityPub signature verification, including local key lookup and remote key fetches.",
190
+ unit: "ms"
191
+ });
192
+ this.signatureKeyFetchDuration = meter.createHistogram("activitypub.signature.key_fetch.duration", {
193
+ description: "Duration of public key lookup performed during ActivityPub signature verification.",
194
+ unit: "ms"
195
+ });
196
+ this.deliveryDuration = meter.createHistogram("activitypub.delivery.duration", {
197
+ description: "Duration of ActivityPub delivery attempts.",
198
+ unit: "ms"
199
+ });
200
+ this.inboxProcessingDuration = meter.createHistogram("activitypub.inbox.processing_duration", {
201
+ description: "Duration of ActivityPub inbox listener processing.",
202
+ unit: "ms"
203
+ });
204
+ this.httpServerRequestCount = meter.createCounter("fedify.http.server.request.count", {
205
+ description: "HTTP requests handled by Federation.fetch().",
206
+ unit: "{request}"
207
+ });
208
+ this.httpServerRequestDuration = meter.createHistogram("fedify.http.server.request.duration", {
209
+ description: "Duration of HTTP requests handled by Federation.fetch().",
210
+ unit: "ms",
211
+ advice: { explicitBucketBoundaries: [
212
+ 5,
213
+ 10,
214
+ 25,
215
+ 50,
216
+ 75,
217
+ 100,
218
+ 250,
219
+ 500,
220
+ 750,
221
+ 1e3,
222
+ 2500,
223
+ 5e3,
224
+ 7500,
225
+ 1e4
226
+ ] }
227
+ });
228
+ this.queueTaskEnqueued = meter.createCounter("fedify.queue.task.enqueued", {
229
+ description: "Tasks Fedify enqueued for inbox, outbox, or fanout work.",
230
+ unit: "{task}"
231
+ });
232
+ this.queueTaskStarted = meter.createCounter("fedify.queue.task.started", {
233
+ description: "Tasks Fedify began processing as a queue worker.",
234
+ unit: "{task}"
235
+ });
236
+ this.queueTaskCompleted = meter.createCounter("fedify.queue.task.completed", {
237
+ description: "Queue tasks Fedify finished processing without throwing.",
238
+ unit: "{task}"
239
+ });
240
+ this.queueTaskFailed = meter.createCounter("fedify.queue.task.failed", {
241
+ description: "Queue tasks Fedify abandoned because processing threw.",
242
+ unit: "{task}"
243
+ });
244
+ this.queueTaskDuration = meter.createHistogram("fedify.queue.task.duration", {
245
+ description: "Duration of queue task processing in Fedify workers.",
246
+ unit: "ms",
247
+ advice: { explicitBucketBoundaries: [
248
+ 5,
249
+ 10,
250
+ 25,
251
+ 50,
252
+ 75,
253
+ 100,
254
+ 250,
255
+ 500,
256
+ 750,
257
+ 1e3,
258
+ 2500,
259
+ 5e3,
260
+ 7500,
261
+ 1e4
262
+ ] }
263
+ });
264
+ this.queueTaskInFlight = meter.createUpDownCounter("fedify.queue.task.in_flight", {
265
+ description: "Queue tasks currently being processed in this Fedify process.",
266
+ unit: "{task}"
267
+ });
268
+ this.fanoutRecipients = meter.createHistogram("activitypub.fanout.recipients", {
269
+ description: "Number of recipient inboxes produced by an ActivityPub fanout task.",
270
+ unit: "{recipient}"
271
+ });
272
+ this.inboxActivity = meter.createCounter("activitypub.inbox.activity", {
273
+ description: "ActivityPub activities observed at the inbox lifecycle level: queued, processed, retried, rejected, or abandoned.",
274
+ unit: "{activity}"
275
+ });
276
+ this.outboxActivity = meter.createCounter("activitypub.outbox.activity", {
277
+ description: "ActivityPub activities observed at the outbox lifecycle level: queued, retried, or abandoned. Per-recipient delivery counters live on `activitypub.delivery.*`.",
278
+ unit: "{activity}"
279
+ });
280
+ }
281
+ recordDelivery(inbox, durationMs, success, activityType) {
282
+ const deliveryAttributes = {
283
+ "activitypub.remote.host": getRemoteHost(inbox),
284
+ "activitypub.delivery.success": success
285
+ };
286
+ if (activityType != null) deliveryAttributes["activitypub.activity.type"] = activityType;
287
+ this.deliverySent.add(1, deliveryAttributes);
288
+ this.deliveryDuration.record(durationMs, deliveryAttributes);
289
+ }
290
+ recordPermanentFailure(inbox, statusCode) {
291
+ this.deliveryPermanentFailure.add(1, {
292
+ "activitypub.remote.host": getRemoteHost(inbox),
293
+ "http.response.status_code": statusCode
294
+ });
295
+ }
296
+ recordSignatureVerificationFailure(reason, remoteHost) {
297
+ const attributes = { "activitypub.verification.failure_reason": reason };
298
+ if (remoteHost != null) attributes["activitypub.remote.host"] = remoteHost;
299
+ this.signatureVerificationFailure.add(1, attributes);
300
+ }
301
+ recordSignatureVerificationDuration(durationMs, kind, result, extra = {}) {
302
+ const attributes = {
303
+ "activitypub.signature.kind": kind,
304
+ "activitypub.signature.result": result
305
+ };
306
+ if (extra.algorithm != null) attributes["http_signatures.algorithm"] = extra.algorithm;
307
+ if (extra.failureReason != null) attributes["http_signatures.failure_reason"] = extra.failureReason;
308
+ if (extra.ldType != null) attributes["ld_signatures.type"] = extra.ldType;
309
+ if (extra.cryptosuite != null) attributes["object_integrity_proofs.cryptosuite"] = extra.cryptosuite;
310
+ this.signatureVerificationDuration.record(durationMs, attributes);
311
+ }
312
+ recordSignatureKeyFetchDuration(durationMs, kind, result) {
313
+ this.signatureKeyFetchDuration.record(durationMs, {
314
+ "activitypub.signature.kind": kind,
315
+ "activitypub.signature.key_fetch.result": result
316
+ });
317
+ }
318
+ recordInboxProcessingDuration(activityType, durationMs) {
319
+ this.inboxProcessingDuration.record(durationMs, { "activitypub.activity.type": activityType });
320
+ }
321
+ recordHttpServerRequest(method, endpoint, durationMs, options = {}) {
322
+ const attributes = {
323
+ "http.request.method": normalizeHttpMethod(method),
324
+ "fedify.endpoint": endpoint
325
+ };
326
+ if (options.statusCode != null) attributes["http.response.status_code"] = options.statusCode;
327
+ if (options.routeTemplate != null) attributes["fedify.route.template"] = options.routeTemplate;
328
+ this.httpServerRequestCount.add(1, attributes);
329
+ this.httpServerRequestDuration.record(durationMs, attributes);
330
+ }
331
+ recordQueueTaskEnqueued(common, attempt) {
332
+ const attributes = buildQueueTaskAttributes(common);
333
+ attributes["fedify.queue.task.attempt"] = attempt;
334
+ this.queueTaskEnqueued.add(1, attributes);
335
+ }
336
+ recordQueueTaskStarted(common) {
337
+ this.queueTaskStarted.add(1, buildQueueTaskAttributes(common));
338
+ }
339
+ incrementQueueTaskInFlight(common) {
340
+ this.queueTaskInFlight.add(1, buildQueueTaskInFlightAttributes(common));
341
+ }
342
+ decrementQueueTaskInFlight(common) {
343
+ this.queueTaskInFlight.add(-1, buildQueueTaskInFlightAttributes(common));
344
+ }
345
+ recordQueueTaskOutcome(common, result, durationMs) {
346
+ const attributes = buildQueueTaskAttributes(common);
347
+ attributes["fedify.queue.task.result"] = result;
348
+ if (result === "completed") this.queueTaskCompleted.add(1, attributes);
349
+ else if (result === "failed") this.queueTaskFailed.add(1, attributes);
350
+ this.queueTaskDuration.record(durationMs, attributes);
351
+ }
352
+ recordFanoutRecipients(recipientCount, activityType) {
353
+ const attributes = {};
354
+ if (activityType != null) attributes["activitypub.activity.type"] = activityType;
355
+ this.fanoutRecipients.record(recipientCount, attributes);
356
+ }
357
+ recordInboxActivity(result, activityType) {
358
+ this.inboxActivity.add(1, buildActivityLifecycleAttributes(result, activityType));
359
+ }
360
+ recordOutboxActivity(result, activityType) {
361
+ this.outboxActivity.add(1, buildActivityLifecycleAttributes(result, activityType));
362
+ }
363
+ };
364
+ function buildActivityLifecycleAttributes(result, activityType) {
365
+ const attributes = { "activitypub.processing.result": result };
366
+ if (activityType != null) attributes["activitypub.activity.type"] = activityType;
367
+ return attributes;
368
+ }
369
+ function buildQueueTaskAttributes(common) {
370
+ const attributes = { "fedify.queue.role": common.role };
371
+ const backend = getQueueBackend(common.queue);
372
+ if (backend != null) attributes["fedify.queue.backend"] = backend;
373
+ const nativeRetrial = common.queue?.nativeRetrial;
374
+ if (typeof nativeRetrial === "boolean") attributes["fedify.queue.native_retrial"] = nativeRetrial;
375
+ if (common.activityType != null) attributes["activitypub.activity.type"] = common.activityType;
376
+ return attributes;
377
+ }
378
+ function buildQueueTaskInFlightAttributes(common) {
379
+ return buildQueueTaskAttributes({
380
+ role: common.role,
381
+ queue: common.queue
382
+ });
383
+ }
384
+ /**
385
+ * Returns the constructor name of the given message queue, when it is a
386
+ * meaningful identifier. Used as a best-effort `fedify.queue.backend`
387
+ * attribute on queue task metrics; returns `undefined` for plain object
388
+ * literals (whose constructor is `Object`) so the attribute does not appear
389
+ * with a non-informative value.
390
+ * @since 2.3.0
391
+ */
392
+ function getQueueBackend(queue) {
393
+ const name = queue?.constructor?.name;
394
+ if (name == null || name === "" || name === "Object") return void 0;
395
+ return name;
396
+ }
397
+ /**
398
+ * Records `fedify.queue.task.enqueued` for an outgoing outbox enqueue and,
399
+ * for the initial attempt, also records
400
+ * `activitypub.outbox.activity{queued}`.
401
+ *
402
+ * Both `Context.sendActivity()` and `OutboxContext.forwardActivity()` enqueue
403
+ * outbox messages with the same metric attributes (role, queue, activity
404
+ * type, attempt), so they share this helper rather than each defining a local
405
+ * closure. Retry enqueues (attempt > 0) intentionally do not record a
406
+ * second `activitypub.outbox.activity{queued}`; retries are reported as
407
+ * `result=retried` from the retry-scheduling site, which has the failure
408
+ * context.
409
+ * @since 2.3.0
410
+ */
411
+ function recordOutboxEnqueue(meterProvider, outboxQueue, message) {
412
+ const metrics = getFederationMetrics(meterProvider);
413
+ metrics.recordQueueTaskEnqueued({
414
+ role: "outbox",
415
+ queue: outboxQueue,
416
+ activityType: message.activityType
417
+ }, message.attempt);
418
+ if (message.attempt === 0) metrics.recordOutboxActivity("queued", message.activityType);
419
+ }
420
+ /**
421
+ * Records `activitypub.fanout.recipients` with the number of recipient
422
+ * inboxes a single fanout produced. The histogram is unitless count
423
+ * (one measurement per fanout enqueue). Recipient URLs are deliberately
424
+ * not recorded; only the activity type, when known.
425
+ * @since 2.3.0
426
+ */
427
+ function recordFanoutRecipients(meterProvider, recipientCount, activityType) {
428
+ getFederationMetrics(meterProvider).recordFanoutRecipients(recipientCount, activityType);
429
+ }
430
+ /**
431
+ * Records one `activitypub.inbox.activity` measurement. The
432
+ * `activitypub.processing.result` attribute is always present;
433
+ * `activitypub.activity.type` is recorded only when Fedify already knows
434
+ * the activity type.
435
+ * @since 2.3.0
436
+ */
437
+ function recordInboxActivity(meterProvider, result, activityType) {
438
+ getFederationMetrics(meterProvider).recordInboxActivity(result, activityType);
439
+ }
440
+ /**
441
+ * Records one `activitypub.outbox.activity` measurement. The
442
+ * `activitypub.processing.result` attribute is always present;
443
+ * `activitypub.activity.type` is recorded only when Fedify already knows
444
+ * the activity type (it is always known for outbox lifecycle events).
445
+ * @since 2.3.0
446
+ */
447
+ function recordOutboxActivity(meterProvider, result, activityType) {
448
+ getFederationMetrics(meterProvider).recordOutboxActivity(result, activityType);
449
+ }
450
+ /**
451
+ * Times an awaited public key fetch and records exactly one
452
+ * `activitypub.signature.key_fetch.duration` measurement, classifying the
453
+ * outcome as `hit`, `fetched`, or `error` based on the `cached` flag and
454
+ * whether the returned key is non-null. Errors thrown by the fetch are
455
+ * reported as `error` and rethrown, so verifier behavior is unchanged.
456
+ *
457
+ * Shared by the three signature verifiers (HTTP, Linked Data, Object
458
+ * Integrity Proofs); the only per-call variation is the
459
+ * `activitypub.signature.kind` attribute value.
460
+ * @since 2.3.0
461
+ */
462
+ async function measureSignatureKeyFetch(meterProvider, kind, fetch) {
463
+ const start = performance.now();
464
+ try {
465
+ const result = await fetch();
466
+ getFederationMetrics(meterProvider).recordSignatureKeyFetchDuration(getDurationMs(start), kind, result.key != null ? result.cached ? "hit" : "fetched" : "error");
467
+ return result;
468
+ } catch (error) {
469
+ getFederationMetrics(meterProvider).recordSignatureKeyFetchDuration(getDurationMs(start), kind, "error");
470
+ throw error;
471
+ }
472
+ }
473
+ /**
474
+ * Whether the given thrown value is an `AbortError`.
475
+ *
476
+ * `processQueuedTask` distinguishes aborted tasks (recorded as
477
+ * `fedify.queue.task.result=aborted`) from other failures so that backend
478
+ * shutdown signals do not inflate the `fedify.queue.task.failed` counter.
479
+ * @since 2.3.0
480
+ */
481
+ function isAbortError$1(error) {
482
+ if (error == null || typeof error !== "object") return false;
483
+ const name = error.name;
484
+ return typeof name === "string" && name === "AbortError";
485
+ }
486
+ const KNOWN_HTTP_METHODS = new Set([
487
+ "CONNECT",
488
+ "DELETE",
489
+ "GET",
490
+ "HEAD",
491
+ "OPTIONS",
492
+ "PATCH",
493
+ "POST",
494
+ "PUT",
495
+ "QUERY",
496
+ "TRACE"
497
+ ]);
498
+ function normalizeHttpMethod(method) {
499
+ const upper = method.toUpperCase();
500
+ return KNOWN_HTTP_METHODS.has(upper) ? upper : "_OTHER";
501
+ }
502
+ const federationMetrics = /* @__PURE__ */ new WeakMap();
503
+ /**
504
+ * Gets the cached Fedify metric instruments for a meter provider.
505
+ * @since 2.3.0
506
+ */
507
+ function getFederationMetrics(meterProvider = metrics.getMeterProvider()) {
508
+ let instruments = federationMetrics.get(meterProvider);
509
+ if (instruments == null) {
510
+ instruments = new FederationMetrics(meterProvider);
511
+ federationMetrics.set(meterProvider, instruments);
512
+ }
513
+ return instruments;
514
+ }
515
+ /**
516
+ * Gets the bounded remote host attribute value for a URL.
517
+ * @since 2.3.0
518
+ */
519
+ function getRemoteHost(url) {
520
+ return url.hostname;
521
+ }
522
+ /**
523
+ * Gets an elapsed duration in milliseconds from a `performance.now()` value.
524
+ * @since 2.3.0
525
+ */
526
+ function getDurationMs(start) {
527
+ return Math.max(0, performance.now() - start);
528
+ }
529
+ //#endregion
154
530
  //#region src/sig/key.ts
155
531
  /**
156
532
  * Checks if the given key is valid and supported. No-op if the key is valid,
@@ -799,6 +1175,27 @@ function parseKeyId(value) {
799
1175
  function getKeyFetchErrorName(error) {
800
1176
  return error.name || error.constructor.name || "Error";
801
1177
  }
1178
+ /**
1179
+ * Known draft-cavage `algorithm` parameter values, used to keep the
1180
+ * `http_signatures.algorithm` metric attribute on a bounded set. The header
1181
+ * field is attacker-controlled and not used to select the verification
1182
+ * algorithm, so unknown values are dropped from the metric to prevent
1183
+ * cardinality blow-up.
1184
+ */
1185
+ const DRAFT_KNOWN_ALGORITHMS = new Set([
1186
+ "ecdsa-sha256",
1187
+ "ecdsa-sha384",
1188
+ "ecdsa-sha512",
1189
+ "ed25519",
1190
+ "hs2019",
1191
+ "rsa-sha1",
1192
+ "rsa-sha256",
1193
+ "rsa-sha512"
1194
+ ]);
1195
+ function classifyHttpVerifyResult(result) {
1196
+ if (result.verified) return "verified";
1197
+ return result.reason.type === "noSignature" ? "missing" : "rejected";
1198
+ }
802
1199
  function recordVerificationResult(span, result) {
803
1200
  span.setAttribute("http_signatures.verified", result.verified);
804
1201
  if (result.verified === true) return;
@@ -842,27 +1239,37 @@ async function verifyRequestDetailed(request, options = {}) {
842
1239
  span.setAttribute(ATTR_URL_FULL, request.url);
843
1240
  for (const [name, value] of request.headers) span.setAttribute(ATTR_HTTP_REQUEST_HEADER(name), value);
844
1241
  }
1242
+ const start = performance.now();
1243
+ const metricsContext = {};
1244
+ let result;
1245
+ let threw = false;
845
1246
  try {
846
1247
  let spec = options.spec;
847
1248
  if (spec == null) spec = request.headers.has("Signature-Input") ? "rfc9421" : "draft-cavage-http-signatures-12";
848
- let result;
849
- if (spec === "rfc9421") result = await verifyRequestRfc9421(request, span, options);
850
- else result = await verifyRequestDraft(request, span, options);
1249
+ if (spec === "rfc9421") result = await verifyRequestRfc9421(request, span, metricsContext, options);
1250
+ else result = await verifyRequestDraft(request, span, metricsContext, options);
851
1251
  recordVerificationResult(span, result);
852
1252
  if (!result.verified) span.setStatus({ code: SpanStatusCode.ERROR });
853
1253
  return result;
854
1254
  } catch (error) {
1255
+ threw = true;
855
1256
  span.setStatus({
856
1257
  code: SpanStatusCode.ERROR,
857
1258
  message: String(error)
858
1259
  });
859
1260
  throw error;
860
1261
  } finally {
1262
+ const classified = threw ? "error" : classifyHttpVerifyResult(result);
1263
+ const failureReason = result != null && !result.verified && result.reason.type !== "noSignature" ? result.reason.type : void 0;
1264
+ getFederationMetrics(options.meterProvider).recordSignatureVerificationDuration(getDurationMs(start), "http", classified, {
1265
+ algorithm: metricsContext.algorithm,
1266
+ failureReason
1267
+ });
861
1268
  span.end();
862
1269
  }
863
1270
  });
864
1271
  }
865
- async function verifyRequestDraft(request, span, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, tracerProvider } = {}) {
1272
+ async function verifyRequestDraft(request, span, metricsContext, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, meterProvider, tracerProvider } = {}) {
866
1273
  const logger = getLogger([
867
1274
  "fedify",
868
1275
  "sig",
@@ -1010,13 +1417,17 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
1010
1417
  const keyIdUrl = parseKeyId(keyId);
1011
1418
  if (keyIdUrl == null) return invalidSignatureResult(null);
1012
1419
  span?.setAttribute("http_signatures.key_id", keyId);
1013
- if ("algorithm" in sigValues) span?.setAttribute("http_signatures.algorithm", sigValues.algorithm);
1014
- const { key, cached, fetchError } = await fetchKeyDetailed(keyIdUrl, CryptographicKey, {
1420
+ if ("algorithm" in sigValues) {
1421
+ span?.setAttribute("http_signatures.algorithm", sigValues.algorithm);
1422
+ const normalizedAlgorithm = sigValues.algorithm.toLowerCase();
1423
+ if (DRAFT_KNOWN_ALGORITHMS.has(normalizedAlgorithm)) metricsContext.algorithm = normalizedAlgorithm;
1424
+ }
1425
+ const { key, cached, fetchError } = await measureSignatureKeyFetch(meterProvider, "http", () => fetchKeyDetailed(keyIdUrl, CryptographicKey, {
1015
1426
  documentLoader,
1016
1427
  contextLoader,
1017
1428
  keyCache,
1018
1429
  tracerProvider
1019
- });
1430
+ }));
1020
1431
  if (fetchError != null) return keyFetchErrorResult(keyIdUrl, fetchError);
1021
1432
  if (key == null) return invalidSignatureResult(keyIdUrl);
1022
1433
  const headerNames = headers.split(/\s+/g);
@@ -1038,7 +1449,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
1038
1449
  signature,
1039
1450
  message
1040
1451
  });
1041
- return await verifyRequestDetailed(originalRequest, {
1452
+ return await verifyRequestDraft(originalRequest, span, metricsContext, {
1042
1453
  documentLoader,
1043
1454
  contextLoader,
1044
1455
  timeWindow,
@@ -1046,7 +1457,9 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
1046
1457
  keyCache: {
1047
1458
  get: () => Promise.resolve(void 0),
1048
1459
  set: async (keyId, key) => await keyCache?.set(keyId, key)
1049
- }
1460
+ },
1461
+ meterProvider,
1462
+ tracerProvider
1050
1463
  });
1051
1464
  }
1052
1465
  logger.debug("Failed to verify with the fetched key {keyId}; signature {signature} is invalid. Check if the key is correct or if the signed message is correct. The message to sign is:\n{message}", {
@@ -1122,7 +1535,7 @@ async function verifyRfc9421ContentDigest(digestHeader, body) {
1122
1535
  }
1123
1536
  return false;
1124
1537
  }
1125
- async function verifyRequestRfc9421(request, span, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, tracerProvider } = {}) {
1538
+ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, meterProvider, tracerProvider } = {}) {
1126
1539
  const logger = getLogger([
1127
1540
  "fedify",
1128
1541
  "sig",
@@ -1156,9 +1569,14 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
1156
1569
  return invalidSignatureResult(null);
1157
1570
  }
1158
1571
  let failure = noSignatureResult();
1572
+ let failureAlgorithm;
1573
+ const setFailure = (result, algorithm) => {
1574
+ failure = result;
1575
+ failureAlgorithm = algorithm;
1576
+ };
1159
1577
  for (const sigName of signatureNames) {
1160
1578
  if (!signatures[sigName]) {
1161
- failure = invalidSignatureResult(parseKeyId(signatureInputs[sigName]?.keyId));
1579
+ setFailure(invalidSignatureResult(parseKeyId(signatureInputs[sigName]?.keyId)));
1162
1580
  continue;
1163
1581
  }
1164
1582
  const sigInput = signatureInputs[sigName];
@@ -1169,7 +1587,7 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
1169
1587
  signatureName: sigName,
1170
1588
  signatureInput: signatureInputHeader
1171
1589
  });
1172
- failure = invalidSignatureResult(null);
1590
+ setFailure(invalidSignatureResult(null));
1173
1591
  continue;
1174
1592
  }
1175
1593
  if (!sigInput.created) {
@@ -1177,7 +1595,7 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
1177
1595
  signatureName: sigName,
1178
1596
  signatureInput: signatureInputHeader
1179
1597
  });
1180
- failure = invalidSignatureResult(keyId);
1598
+ setFailure(invalidSignatureResult(keyId));
1181
1599
  continue;
1182
1600
  }
1183
1601
  const signatureCreated = Temporal.Instant.fromEpochMilliseconds(sigInput.created * 1e3);
@@ -1189,14 +1607,14 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
1189
1607
  created: signatureCreated.toString(),
1190
1608
  now: now.toString()
1191
1609
  });
1192
- failure = invalidSignatureResult(keyId);
1610
+ setFailure(invalidSignatureResult(keyId));
1193
1611
  continue;
1194
1612
  } else if (Temporal.Instant.compare(signatureCreated, now.subtract(tw)) < 0) {
1195
1613
  logger.debug("Failed to verify; signature created time is too far in the past.", {
1196
1614
  created: signatureCreated.toString(),
1197
1615
  now: now.toString()
1198
1616
  });
1199
- failure = invalidSignatureResult(keyId);
1617
+ setFailure(invalidSignatureResult(keyId));
1200
1618
  continue;
1201
1619
  }
1202
1620
  }
@@ -1204,34 +1622,34 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
1204
1622
  const contentDigestHeader = request.headers.get("Content-Digest");
1205
1623
  if (!contentDigestHeader) {
1206
1624
  logger.debug("Failed to verify; Content-Digest header required but not found.", { components: sigInput.components });
1207
- failure = invalidSignatureResult(keyId);
1625
+ setFailure(invalidSignatureResult(keyId));
1208
1626
  continue;
1209
1627
  }
1210
1628
  if (!await verifyRfc9421ContentDigest(contentDigestHeader, await request.arrayBuffer())) {
1211
1629
  logger.debug("Failed to verify; Content-Digest verification failed.", { contentDigest: contentDigestHeader });
1212
- failure = invalidSignatureResult(keyId);
1630
+ setFailure(invalidSignatureResult(keyId));
1213
1631
  continue;
1214
1632
  }
1215
1633
  }
1216
1634
  span?.setAttribute("http_signatures.key_id", sigInput.keyId);
1217
1635
  span?.setAttribute("http_signatures.created", sigInput.created.toString());
1218
1636
  if (keyId == null) {
1219
- failure = invalidSignatureResult(null);
1637
+ setFailure(invalidSignatureResult(null));
1220
1638
  continue;
1221
1639
  }
1222
- const { key, cached, fetchError } = await fetchKeyDetailed(keyId, CryptographicKey, {
1640
+ const { key, cached, fetchError } = await measureSignatureKeyFetch(meterProvider, "http", () => fetchKeyDetailed(keyId, CryptographicKey, {
1223
1641
  documentLoader,
1224
1642
  contextLoader,
1225
1643
  keyCache,
1226
1644
  tracerProvider
1227
- });
1645
+ }));
1228
1646
  if (fetchError != null) {
1229
- failure = keyFetchErrorResult(keyId, fetchError);
1647
+ setFailure(keyFetchErrorResult(keyId, fetchError));
1230
1648
  continue;
1231
1649
  }
1232
1650
  if (!key) {
1233
1651
  logger.debug("Failed to fetch key: {keyId}", { keyId: sigInput.keyId });
1234
- failure = invalidSignatureResult(keyId);
1652
+ setFailure(invalidSignatureResult(keyId));
1235
1653
  continue;
1236
1654
  }
1237
1655
  let alg = sigInput.alg?.toLowerCase();
@@ -1243,12 +1661,13 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
1243
1661
  }
1244
1662
  if (alg) span?.setAttribute("http_signatures.algorithm", alg);
1245
1663
  const algorithm = alg && rfc9421AlgorithmMap[alg];
1664
+ const candidateAlgorithm = algorithm ? alg : void 0;
1246
1665
  if (!algorithm) {
1247
1666
  logger.debug("Failed to verify; unsupported algorithm: {algorithm}", {
1248
1667
  algorithm: sigInput.alg,
1249
1668
  supported: Object.keys(rfc9421AlgorithmMap)
1250
1669
  });
1251
- failure = invalidSignatureResult(keyId);
1670
+ setFailure(invalidSignatureResult(keyId));
1252
1671
  continue;
1253
1672
  }
1254
1673
  let signatureBase;
@@ -1259,20 +1678,22 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
1259
1678
  error,
1260
1679
  signatureInput: sigInput
1261
1680
  });
1262
- failure = invalidSignatureResult(keyId);
1681
+ setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
1263
1682
  continue;
1264
1683
  }
1265
1684
  const signatureBaseBytes = new TextEncoder().encode(signatureBase);
1266
1685
  span?.setAttribute("http_signatures.signature", encodeHex(sigBytes));
1267
1686
  try {
1268
- if (await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes)) return {
1269
- verified: true,
1270
- key,
1271
- signatureLabel: sigName
1272
- };
1273
- else if (cached) {
1687
+ if (await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes)) {
1688
+ metricsContext.algorithm = candidateAlgorithm;
1689
+ return {
1690
+ verified: true,
1691
+ key,
1692
+ signatureLabel: sigName
1693
+ };
1694
+ } else if (cached) {
1274
1695
  logger.debug("Failed to verify with cached key {keyId}; retrying with fresh key...", { keyId: sigInput.keyId });
1275
- return await verifyRequestDetailed(originalRequest, {
1696
+ return await verifyRequestRfc9421(originalRequest, span, metricsContext, {
1276
1697
  documentLoader,
1277
1698
  contextLoader,
1278
1699
  timeWindow,
@@ -1281,14 +1702,16 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
1281
1702
  get: () => Promise.resolve(void 0),
1282
1703
  set: async (keyId, key) => await keyCache?.set(keyId, key)
1283
1704
  },
1284
- spec: "rfc9421"
1705
+ spec: "rfc9421",
1706
+ meterProvider,
1707
+ tracerProvider
1285
1708
  });
1286
1709
  } else {
1287
1710
  logger.debug("Failed to verify signature with fetched key {keyId}; signature invalid.", {
1288
1711
  keyId: sigInput.keyId,
1289
1712
  signatureBase
1290
1713
  });
1291
- failure = invalidSignatureResult(keyId);
1714
+ setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
1292
1715
  }
1293
1716
  } catch (error) {
1294
1717
  logger.debug("Error during signature verification: {error}", {
@@ -1296,9 +1719,10 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
1296
1719
  keyId: sigInput.keyId,
1297
1720
  algorithm: sigInput.alg
1298
1721
  });
1299
- failure = invalidSignatureResult(keyId);
1722
+ setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
1300
1723
  }
1301
1724
  }
1725
+ metricsContext.algorithm = failureAlgorithm;
1302
1726
  return failure;
1303
1727
  }
1304
1728
  /**
@@ -1520,4 +1944,4 @@ function timingSafeEqual(a, b) {
1520
1944
  return result === 0;
1521
1945
  }
1522
1946
  //#endregion
1523
- export { version as _, verifyRequestDetailed as a, fetchKeyDetailed as c, validateCryptoKey as d, formatAcceptSignature as f, name as g, validateAcceptSignature as h, verifyRequest as i, generateCryptoKeyPair as l, parseAcceptSignature as m, parseRfc9421SignatureInput as n, exportJwk as o, fulfillAcceptSignature as p, signRequest as r, fetchKey as s, doubleKnock as t, importJwk as u };
1947
+ export { parseAcceptSignature as C, version as E, fulfillAcceptSignature as S, name as T, recordFanoutRecipients as _, verifyRequestDetailed as a, recordOutboxEnqueue as b, fetchKeyDetailed as c, validateCryptoKey as d, getDurationMs as f, measureSignatureKeyFetch as g, isAbortError$1 as h, verifyRequest as i, generateCryptoKeyPair as l, getRemoteHost as m, parseRfc9421SignatureInput as n, exportJwk as o, getFederationMetrics as p, signRequest as r, fetchKey as s, doubleKnock as t, importJwk as u, recordInboxActivity as v, validateAcceptSignature as w, formatAcceptSignature as x, recordOutboxActivity as y };