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

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 (116) hide show
  1. package/README.md +3 -0
  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-DQP-q39h.mjs → assert_rejects-DN60FHPX.mjs} +2 -2
  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-Ond_h57y.mjs → builder-BCkBXxky.mjs} +60 -41
  9. package/dist/compat/mod.d.cts +1 -1
  10. package/dist/compat/mod.d.ts +1 -1
  11. package/dist/compat/outgoing-jsonld.test.mjs +3 -3
  12. package/dist/compat/public-audience.test.mjs +3 -3
  13. package/dist/compat/transformers.test.mjs +5 -5
  14. package/dist/{context-cSUMk2da.d.ts → context-DCtsSHDv.d.ts} +4 -293
  15. package/dist/{context-Ch-ZLyTQ.d.cts → context-DI2gRbyN.d.cts} +3 -294
  16. package/dist/{context-BAE7AKLA.mjs → context-DVoTs_wM.mjs} +1 -1
  17. package/dist/{deno-DVsHS7rA.mjs → deno-B_9yJW3w.mjs} +1 -1
  18. package/dist/{docloader-WsWfKaE5.mjs → docloader-BT89tyFr.mjs} +3 -3
  19. package/dist/federation/builder.test.mjs +138 -10
  20. package/dist/federation/collection.test.mjs +3 -3
  21. package/dist/federation/handler.test.mjs +12 -12
  22. package/dist/federation/idempotency.test.mjs +6 -6
  23. package/dist/federation/inbox.test.mjs +3 -3
  24. package/dist/federation/keycache.test.mjs +5 -5
  25. package/dist/federation/kv.test.mjs +3 -3
  26. package/dist/federation/metrics.test.mjs +231 -3
  27. package/dist/federation/middleware.test.mjs +88 -18
  28. package/dist/federation/mod.cjs +155 -3
  29. package/dist/federation/mod.d.cts +3 -2
  30. package/dist/federation/mod.d.ts +3 -2
  31. package/dist/federation/mod.js +153 -1
  32. package/dist/federation/mq.test.mjs +5 -5
  33. package/dist/federation/negotiation.test.mjs +4 -4
  34. package/dist/federation/retry.test.mjs +3 -3
  35. package/dist/federation/router.test.mjs +190 -9
  36. package/dist/federation/send.test.mjs +16 -16
  37. package/dist/federation/webfinger.test.mjs +5 -5
  38. package/dist/{getMachineId-bsd-BY01PL1n.mjs → getMachineId-bsd-etIyxDet.mjs} +1 -1
  39. package/dist/{getMachineId-darwin-Dr1gkBkp.mjs → getMachineId-darwin-D23zTf4g.mjs} +1 -1
  40. package/dist/{getMachineId-win-QEYwcJiy.mjs → getMachineId-win-Dpap6v5i.mjs} +1 -1
  41. package/dist/{http-CouJSFVK.js → http-CToqG5ap.js} +252 -20
  42. package/dist/{http-CubOB9wq.cjs → http-CWoeyogl.cjs} +263 -19
  43. package/dist/{http-DUV8ysti.mjs → http-Cyx5SNuu.mjs} +8 -6
  44. package/dist/{http-D6LP89UO.d.ts → http-VyDTd4G3.d.cts} +8 -1
  45. package/dist/{http-D6aw3j2U.d.cts → http-lf8Hsd91.d.ts} +8 -1
  46. package/dist/{key-BoWaYRHm.mjs → key-CkkMJBjF.mjs} +42 -17
  47. package/dist/{kv-cache-DBNpsneh.js → kv-cache-CuCn2xvM.js} +19 -2
  48. package/dist/{kv-cache-Dz31ATUT.cjs → kv-cache-DuEwFYcN.cjs} +19 -2
  49. package/dist/{kv-cache-DihufyAQ.mjs → kv-cache-VHFP42vY.mjs} +19 -1
  50. package/dist/{ld-B5K1mSuG.mjs → ld-k8yqD2a-.mjs} +3 -3
  51. package/dist/{metrics-C4attqv0.mjs → metrics-iRBg8jTk.mjs} +209 -2
  52. package/dist/{middleware-CmsDtIHI.cjs → middleware-BWLUrbS9.cjs} +137 -210
  53. package/dist/{middleware-BDKFRjue.mjs → middleware-CztxpARM.mjs} +1 -1
  54. package/dist/{middleware-Dtjz-hSk.js → middleware-D7FrhN9q.js} +101 -162
  55. package/dist/{middleware-t0jC8I99.mjs → middleware-DQEgdr83.mjs} +64 -36
  56. package/dist/{mod-BDhgfjP7.d.cts → mod-B0hW12_O.d.cts} +1 -1
  57. package/dist/mod-C504qevA.d.cts +173 -0
  58. package/dist/{mod-B-Lin9Sy.d.ts → mod-COIAjwRS.d.ts} +1 -1
  59. package/dist/{mod-DLrRb0dx.d.ts → mod-DFvNJcNb.d.ts} +54 -3
  60. package/dist/mod-wYfuXeDE.d.ts +173 -0
  61. package/dist/{mod-BR_BB0bh.d.cts → mod-yvIXFAEi.d.cts} +54 -3
  62. package/dist/mod.cjs +6 -6
  63. package/dist/mod.d.cts +6 -5
  64. package/dist/mod.d.ts +6 -5
  65. package/dist/mod.js +5 -5
  66. package/dist/mq-D-nlpY04.d.ts +208 -0
  67. package/dist/mq-D8uSFzxe.d.cts +208 -0
  68. package/dist/nodeinfo/client.test.mjs +4 -4
  69. package/dist/nodeinfo/handler.test.mjs +5 -5
  70. package/dist/nodeinfo/types.test.mjs +4 -4
  71. package/dist/otel/exporter.test.mjs +3 -3
  72. package/dist/{outgoing-jsonld-BNL8AC14.mjs → outgoing-jsonld-BgFLCJQ_.mjs} +1 -1
  73. package/dist/{owner-hDxI0ufu.mjs → owner-nmXdvXpc.mjs} +2 -2
  74. package/dist/{proof-BUWfVr6Q.cjs → proof-CcsIJLTn.cjs} +1 -1
  75. package/dist/{proof-DhVuz4bc.mjs → proof-DpwO1T4S.mjs} +5 -5
  76. package/dist/{proof-n60t8o9P.js → proof-NRmtrTDu.js} +1 -1
  77. package/dist/{send-BPhyR5Oo.mjs → send-DvX2tYyZ.mjs} +3 -3
  78. package/dist/sig/accept.test.mjs +1 -1
  79. package/dist/sig/http.test.mjs +13 -9
  80. package/dist/sig/key.test.mjs +104 -7
  81. package/dist/sig/ld.test.mjs +7 -7
  82. package/dist/sig/mod.cjs +2 -2
  83. package/dist/sig/mod.d.cts +2 -2
  84. package/dist/sig/mod.d.ts +2 -2
  85. package/dist/sig/mod.js +2 -2
  86. package/dist/sig/owner.test.mjs +6 -6
  87. package/dist/sig/proof.test.mjs +8 -8
  88. package/dist/{std__assert-BTEgfoJo.mjs → std__assert-BBjXFNOb.mjs} +4 -4
  89. package/dist/testing/mod.d.mts +1 -0
  90. package/dist/testing/mod.mjs +1 -1
  91. package/dist/utils/docloader.test.mjs +7 -7
  92. package/dist/utils/kv-cache.test.mjs +67 -2
  93. package/dist/utils/mod.cjs +1 -1
  94. package/dist/utils/mod.d.cts +1 -1
  95. package/dist/utils/mod.d.ts +1 -1
  96. package/dist/utils/mod.js +1 -1
  97. package/package.json +6 -7
  98. package/dist/mod-C6E8rkcz.d.ts +0 -63
  99. package/dist/mod-P9tE2WmM.d.cts +0 -63
  100. package/dist/router-BT_F5748.mjs +0 -114
  101. /package/dist/{accept-CgDcxvjV.mjs → accept-CceiKpCy.mjs} +0 -0
  102. /package/dist/{activity-listener-BeTGV3wc.mjs → activity-listener-tztVvlNb.mjs} +0 -0
  103. /package/dist/{assert_equals-Ew3jOFa3.mjs → assert_equals-C-ZRDbaf.mjs} +0 -0
  104. /package/dist/{client-Bneh_DYR.mjs → client-B_A6mfn3.mjs} +0 -0
  105. /package/dist/{collection-Cc3DVAhE.mjs → collection-CA3V5zyK.mjs} +0 -0
  106. /package/dist/{esm-sdtqOUPu.mjs → esm-BQRw925N.mjs} +0 -0
  107. /package/dist/{execAsync-Dxb7rNf3.mjs → execAsync-DCBrgFiV.mjs} +0 -0
  108. /package/dist/{getMachineId-linux-Bbhofx-s.mjs → getMachineId-linux-ObI47Hql.mjs} +0 -0
  109. /package/dist/{getMachineId-unsupported-dIOte2Ct.mjs → getMachineId-unsupported-Ddu-PFeh.mjs} +0 -0
  110. /package/dist/{keycache-BeU0LCII.mjs → keycache-BYMd8q7F.mjs} +0 -0
  111. /package/dist/{keys-CSYsOMFG.mjs → keys-C3kae-6B.mjs} +0 -0
  112. /package/dist/{kv-QHE0oeM3.mjs → kv-x2IvBUyq.mjs} +0 -0
  113. /package/dist/{negotiation-DDstyBvc.mjs → negotiation-CDW-_gUU.mjs} +0 -0
  114. /package/dist/{public-audience-c9zmYKgA.mjs → public-audience-N3pyOx2p.mjs} +0 -0
  115. /package/dist/{retry-_VvV0h9f.mjs → retry-v_sGLH1d.mjs} +0 -0
  116. /package/dist/{types-D09GN0uZ.mjs → types-BFowWFTT.mjs} +0 -0
@@ -11,7 +11,7 @@ let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conve
11
11
  let byte_encodings_base64 = require("byte-encodings/base64");
12
12
  //#region deno.json
13
13
  var name = "@fedify/fedify";
14
- var version = "2.3.0-dev.1119+6cc02662";
14
+ var version = "2.3.0-dev.1137+53a1f26d";
15
15
  //#endregion
16
16
  //#region src/sig/accept.ts
17
17
  /**
@@ -172,6 +172,11 @@ var FederationMetrics = class {
172
172
  fanoutRecipients;
173
173
  inboxActivity;
174
174
  outboxActivity;
175
+ keyLookup;
176
+ keyLookupDuration;
177
+ documentFetch;
178
+ documentFetchDuration;
179
+ documentCache;
175
180
  constructor(meterProvider) {
176
181
  const meter = meterProvider.getMeter(name, version);
177
182
  this.deliverySent = meter.createCounter("activitypub.delivery.sent", {
@@ -278,6 +283,58 @@ var FederationMetrics = class {
278
283
  description: "ActivityPub activities observed at the outbox lifecycle level: queued, retried, or abandoned. Per-recipient delivery counters live on `activitypub.delivery.*`.",
279
284
  unit: "{activity}"
280
285
  });
286
+ this.keyLookup = meter.createCounter("activitypub.key.lookup", {
287
+ description: "Public-key lookup attempts performed by Fedify, including both cache hits and remote fetches.",
288
+ unit: "{lookup}"
289
+ });
290
+ this.keyLookupDuration = meter.createHistogram("activitypub.key.lookup.duration", {
291
+ description: "Duration of public-key lookups performed by Fedify, including any remote fetch.",
292
+ unit: "ms",
293
+ advice: { explicitBucketBoundaries: [
294
+ 5,
295
+ 10,
296
+ 25,
297
+ 50,
298
+ 75,
299
+ 100,
300
+ 250,
301
+ 500,
302
+ 750,
303
+ 1e3,
304
+ 2500,
305
+ 5e3,
306
+ 7500,
307
+ 1e4
308
+ ] }
309
+ });
310
+ this.documentFetch = meter.createCounter("activitypub.document.fetch", {
311
+ description: "Remote JSON-LD document loader invocations made by Fedify-wrapped loaders.",
312
+ unit: "{fetch}"
313
+ });
314
+ this.documentFetchDuration = meter.createHistogram("activitypub.document.fetch.duration", {
315
+ description: "Duration of remote JSON-LD document loader invocations made by Fedify-wrapped loaders.",
316
+ unit: "ms",
317
+ advice: { explicitBucketBoundaries: [
318
+ 5,
319
+ 10,
320
+ 25,
321
+ 50,
322
+ 75,
323
+ 100,
324
+ 250,
325
+ 500,
326
+ 750,
327
+ 1e3,
328
+ 2500,
329
+ 5e3,
330
+ 7500,
331
+ 1e4
332
+ ] }
333
+ });
334
+ this.documentCache = meter.createCounter("activitypub.document.cache", {
335
+ description: "KV-backed document loader cache lookups, with `hit` or `miss` classification.",
336
+ unit: "{lookup}"
337
+ });
281
338
  }
282
339
  recordDelivery(inbox, durationMs, success, activityType) {
283
340
  const deliveryAttributes = {
@@ -361,6 +418,36 @@ var FederationMetrics = class {
361
418
  recordOutboxActivity(result, activityType) {
362
419
  this.outboxActivity.add(1, buildActivityLifecycleAttributes(result, activityType));
363
420
  }
421
+ recordKeyLookup(attrs) {
422
+ const attributes = {
423
+ "activitypub.lookup.kind": "public_key",
424
+ "activitypub.lookup.result": attrs.result,
425
+ "activitypub.cache.enabled": attrs.cacheEnabled
426
+ };
427
+ if (attrs.remoteUrl != null) attributes["activitypub.remote.host"] = getRemoteHost(attrs.remoteUrl);
428
+ if (attrs.statusCode != null) attributes["http.response.status_code"] = attrs.statusCode;
429
+ this.keyLookup.add(1, attributes);
430
+ this.keyLookupDuration.record(attrs.durationMs, attributes);
431
+ }
432
+ recordDocumentFetch(attrs) {
433
+ const attributes = {
434
+ "activitypub.lookup.kind": attrs.kind,
435
+ "activitypub.lookup.result": attrs.result
436
+ };
437
+ if (attrs.remoteUrl != null) attributes["activitypub.remote.host"] = getRemoteHost(attrs.remoteUrl);
438
+ if (attrs.cacheEnabled != null) attributes["activitypub.cache.enabled"] = attrs.cacheEnabled;
439
+ if (attrs.statusCode != null) attributes["http.response.status_code"] = attrs.statusCode;
440
+ this.documentFetch.add(1, attributes);
441
+ this.documentFetchDuration.record(attrs.durationMs, attributes);
442
+ }
443
+ recordDocumentCache(attrs) {
444
+ const attributes = {
445
+ "activitypub.lookup.kind": attrs.kind,
446
+ "activitypub.lookup.result": attrs.result
447
+ };
448
+ if (attrs.remoteUrl != null) attributes["activitypub.remote.host"] = getRemoteHost(attrs.remoteUrl);
449
+ this.documentCache.add(1, attributes);
450
+ }
364
451
  };
365
452
  function buildActivityLifecycleAttributes(result, activityType) {
366
453
  const attributes = { "activitypub.processing.result": result };
@@ -449,6 +536,125 @@ function recordOutboxActivity(meterProvider, result, activityType) {
449
536
  getFederationMetrics(meterProvider).recordOutboxActivity(result, activityType);
450
537
  }
451
538
  /**
539
+ * Records one measurement on `activitypub.key.lookup` (counter) and
540
+ * `activitypub.key.lookup.duration` (histogram) for a public-key lookup.
541
+ *
542
+ * `activitypub.lookup.kind` is always recorded as `public_key`; the result
543
+ * classification, remote host, HTTP status code (when an HTTP response was
544
+ * received), and `activitypub.cache.enabled` are recorded as attributes on
545
+ * both measurements. Full key URLs and key IDs are deliberately omitted to
546
+ * keep cardinality bounded.
547
+ * @since 2.3.0
548
+ */
549
+ function recordKeyLookup(meterProvider, attrs) {
550
+ getFederationMetrics(meterProvider).recordKeyLookup(attrs);
551
+ }
552
+ /**
553
+ * Records one measurement each on `activitypub.document.fetch` (counter)
554
+ * and `activitypub.document.fetch.duration` (histogram) for one remote
555
+ * JSON-LD document loader invocation, with bounded
556
+ * `activitypub.lookup.kind` and `activitypub.lookup.result` attributes
557
+ * plus the optional remote-host, cache-enabled, and HTTP status-code
558
+ * attributes. Counter and histogram are always recorded together so
559
+ * aggregate rate and latency views stay in sync.
560
+ * @since 2.3.0
561
+ */
562
+ function recordDocumentFetch(meterProvider, attrs) {
563
+ getFederationMetrics(meterProvider).recordDocumentFetch(attrs);
564
+ }
565
+ /**
566
+ * Records one `activitypub.document.cache` measurement, classifying the
567
+ * lookup as `hit` (the cache returned an entry) or `miss` (the cache was
568
+ * consulted and returned nothing, prompting a delegate fetch).
569
+ * @since 2.3.0
570
+ */
571
+ function recordDocumentCache(meterProvider, attrs) {
572
+ getFederationMetrics(meterProvider).recordDocumentCache(attrs);
573
+ }
574
+ /**
575
+ * Classifies a thrown value from a key or document fetch into the bounded
576
+ * {@link LookupResult} taxonomy and, when an HTTP response was received,
577
+ * surfaces its status code.
578
+ *
579
+ * - `FetchError` with a `Response` whose status is `404` or `410`:
580
+ * `result=not_found` and the response status code.
581
+ * - `FetchError` with any other `Response`: `result=error` and the
582
+ * response status code.
583
+ * - `FetchError` without a `Response`: `result=network_error`.
584
+ * - An `AbortError` (typically from a cancelled fetch): `result=network_error`.
585
+ * - A bare `TypeError` (the shape native `fetch()` raises on DNS, connect,
586
+ * and TLS failures before any response is observed):
587
+ * `result=network_error`.
588
+ * - Any other value: `result=error`.
589
+ * @since 2.3.0
590
+ */
591
+ function classifyFetchError(error) {
592
+ if (error instanceof _fedify_vocab_runtime.FetchError) {
593
+ if (error.response != null) {
594
+ const status = error.response.status;
595
+ return {
596
+ result: status === 404 || status === 410 ? "not_found" : "error",
597
+ statusCode: status
598
+ };
599
+ }
600
+ return { result: "network_error" };
601
+ }
602
+ if (isAbortError$1(error)) return { result: "network_error" };
603
+ if (error instanceof TypeError) return { result: "network_error" };
604
+ return { result: "error" };
605
+ }
606
+ /**
607
+ * Wraps a {@link DocumentLoader} so each invocation records one
608
+ * measurement on `activitypub.document.fetch` (counter) and one on
609
+ * `activitypub.document.fetch.duration` (histogram), classifying the
610
+ * outcome via {@link classifyFetchError} when the wrapped loader throws
611
+ * and as `fetched` on success. The wrapper rethrows whatever the
612
+ * wrapped loader throws so caller behavior is unchanged.
613
+ *
614
+ * The wrapper records the hostname of the requested URL on
615
+ * `activitypub.remote.host` when the URL parses; full URLs, paths, and
616
+ * query strings are deliberately excluded to keep cardinality bounded.
617
+ * HTTP status codes are recorded only when the failure carries a
618
+ * `Response` (currently, when the wrapped loader throws a
619
+ * {@link FetchError} with a non-`null` `response`).
620
+ * @since 2.3.0
621
+ */
622
+ function instrumentDocumentLoader(loader, options) {
623
+ const meterProvider = options.meterProvider;
624
+ if (meterProvider == null) return loader;
625
+ return async (url, opts) => {
626
+ const start = performance.now();
627
+ let remoteUrl;
628
+ try {
629
+ remoteUrl = new URL(url);
630
+ } catch {
631
+ remoteUrl = void 0;
632
+ }
633
+ try {
634
+ const result = await loader(url, opts);
635
+ recordDocumentFetch(meterProvider, {
636
+ durationMs: getDurationMs(start),
637
+ kind: options.kind,
638
+ result: "fetched",
639
+ remoteUrl,
640
+ cacheEnabled: options.cacheEnabled
641
+ });
642
+ return result;
643
+ } catch (error) {
644
+ const classified = classifyFetchError(error);
645
+ recordDocumentFetch(meterProvider, {
646
+ durationMs: getDurationMs(start),
647
+ kind: options.kind,
648
+ result: classified.result,
649
+ remoteUrl,
650
+ cacheEnabled: options.cacheEnabled,
651
+ statusCode: classified.statusCode
652
+ });
653
+ throw error;
654
+ }
655
+ };
656
+ }
657
+ /**
452
658
  * Times an awaited public key fetch and records exactly one
453
659
  * `activitypub.signature.key_fetch.duration` measurement, classifying the
454
660
  * outcome as `hit`, `fetched`, or `error` based on the `cached` flag and
@@ -828,24 +1034,48 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
828
1034
  };
829
1035
  }
830
1036
  async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, onFetchError) {
831
- const logger = (0, _logtape_logtape.getLogger)([
832
- "fedify",
833
- "sig",
834
- "key"
835
- ]);
836
- const keyId = cacheKey.href;
837
- const keyCache = options.keyCache;
838
- const cached = await getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger);
839
- if (cached?.key === null && cached.cached) return await onCachedUnavailable(cacheKey, keyId, keyCache, logger);
840
- if (cached != null) return cached;
841
- logger.debug("Fetching key {keyId} to verify signature...", { keyId });
842
- let document;
1037
+ const start = performance.now();
1038
+ let outcome = { result: "error" };
843
1039
  try {
844
- document = (await (options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)())(keyId)).document;
845
- } catch (error) {
846
- return await onFetchError(error, cacheKey, keyId, keyCache, logger);
1040
+ const logger = (0, _logtape_logtape.getLogger)([
1041
+ "fedify",
1042
+ "sig",
1043
+ "key"
1044
+ ]);
1045
+ const keyId = cacheKey.href;
1046
+ const keyCache = options.keyCache;
1047
+ const cached = await getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger);
1048
+ if (cached?.key === null && cached.cached) {
1049
+ const cachedUnavailable = await onCachedUnavailable(cacheKey, keyId, keyCache, logger);
1050
+ outcome = { result: "hit" };
1051
+ return cachedUnavailable;
1052
+ }
1053
+ if (cached != null) {
1054
+ outcome = { result: "hit" };
1055
+ return cached;
1056
+ }
1057
+ logger.debug("Fetching key {keyId} to verify signature...", { keyId });
1058
+ let document;
1059
+ try {
1060
+ document = (await (options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)())(keyId)).document;
1061
+ } catch (error) {
1062
+ const classified = classifyFetchError(error);
1063
+ const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
1064
+ outcome = classified;
1065
+ return errored;
1066
+ }
1067
+ const resolved = await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
1068
+ outcome = { result: resolved.key != null ? "fetched" : "invalid" };
1069
+ return resolved;
1070
+ } finally {
1071
+ recordKeyLookup(options.meterProvider, {
1072
+ durationMs: getDurationMs(start),
1073
+ result: outcome.result,
1074
+ remoteUrl: cacheKey,
1075
+ cacheEnabled: options.keyCache != null,
1076
+ statusCode: outcome.statusCode
1077
+ });
847
1078
  }
848
- return await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
849
1079
  }
850
1080
  async function fetchKeyInternal(keyId, cls, options = {}) {
851
1081
  return await fetchKeyWithResult(typeof keyId === "string" ? new URL(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
@@ -1427,7 +1657,8 @@ async function verifyRequestDraft(request, span, metricsContext, { documentLoade
1427
1657
  documentLoader,
1428
1658
  contextLoader,
1429
1659
  keyCache,
1430
- tracerProvider
1660
+ tracerProvider,
1661
+ meterProvider
1431
1662
  }));
1432
1663
  if (fetchError != null) return keyFetchErrorResult(keyIdUrl, fetchError);
1433
1664
  if (key == null) return invalidSignatureResult(keyIdUrl);
@@ -1642,7 +1873,8 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
1642
1873
  documentLoader,
1643
1874
  contextLoader,
1644
1875
  keyCache,
1645
- tracerProvider
1876
+ tracerProvider,
1877
+ meterProvider
1646
1878
  }));
1647
1879
  if (fetchError != null) {
1648
1880
  setFailure(keyFetchErrorResult(keyId, fetchError));
@@ -2011,6 +2243,12 @@ Object.defineProperty(exports, "importJwk", {
2011
2243
  return importJwk;
2012
2244
  }
2013
2245
  });
2246
+ Object.defineProperty(exports, "instrumentDocumentLoader", {
2247
+ enumerable: true,
2248
+ get: function() {
2249
+ return instrumentDocumentLoader;
2250
+ }
2251
+ });
2014
2252
  Object.defineProperty(exports, "isAbortError", {
2015
2253
  enumerable: true,
2016
2254
  get: function() {
@@ -2041,6 +2279,12 @@ Object.defineProperty(exports, "parseRfc9421SignatureInput", {
2041
2279
  return parseRfc9421SignatureInput;
2042
2280
  }
2043
2281
  });
2282
+ Object.defineProperty(exports, "recordDocumentCache", {
2283
+ enumerable: true,
2284
+ get: function() {
2285
+ return recordDocumentCache;
2286
+ }
2287
+ });
2044
2288
  Object.defineProperty(exports, "recordFanoutRecipients", {
2045
2289
  enumerable: true,
2046
2290
  get: function() {
@@ -1,10 +1,10 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-DVsHS7rA.mjs";
5
- import { a as measureSignatureKeyFetch, n as getFederationMetrics, t as getDurationMs } from "./metrics-C4attqv0.mjs";
6
- import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-CgDcxvjV.mjs";
7
- import { o as validateCryptoKey, r as fetchKeyDetailed } from "./key-BoWaYRHm.mjs";
4
+ import { n as version, t as name } from "./deno-B_9yJW3w.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-iRBg8jTk.mjs";
6
+ import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-CceiKpCy.mjs";
7
+ import { o as validateCryptoKey, r as fetchKeyDetailed } from "./key-CkkMJBjF.mjs";
8
8
  import { getLogger } from "@logtape/logtape";
9
9
  import { CryptographicKey } from "@fedify/vocab";
10
10
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -569,7 +569,8 @@ async function verifyRequestDraft(request, span, metricsContext, { documentLoade
569
569
  documentLoader,
570
570
  contextLoader,
571
571
  keyCache,
572
- tracerProvider
572
+ tracerProvider,
573
+ meterProvider
573
574
  }));
574
575
  if (fetchError != null) return keyFetchErrorResult(keyIdUrl, fetchError);
575
576
  if (key == null) return invalidSignatureResult(keyIdUrl);
@@ -784,7 +785,8 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
784
785
  documentLoader,
785
786
  contextLoader,
786
787
  keyCache,
787
- tracerProvider
788
+ tracerProvider,
789
+ meterProvider
788
790
  }));
789
791
  if (fetchError != null) {
790
792
  setFailure(keyFetchErrorResult(keyId, fetchError));
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { CryptographicKey, Multikey } from "@fedify/vocab";
3
- import { MeterProvider, TracerProvider } from "@opentelemetry/api";
4
3
  import { DocumentLoader } from "@fedify/vocab-runtime";
4
+ import { MeterProvider, TracerProvider } from "@opentelemetry/api";
5
5
 
6
6
  //#region src/sig/key.d.ts
7
7
  /**
@@ -52,6 +52,13 @@ interface FetchKeyOptions {
52
52
  * @since 1.3.0
53
53
  */
54
54
  tracerProvider?: TracerProvider;
55
+ /**
56
+ * The OpenTelemetry meter provider to use for recording
57
+ * `activitypub.key.lookup` and `activitypub.key.lookup.duration`. If
58
+ * omitted, the global meter provider is used.
59
+ * @since 2.3.0
60
+ */
61
+ meterProvider?: MeterProvider;
55
62
  }
56
63
  /**
57
64
  * The result of {@link fetchKey}.
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { CryptographicKey, Multikey } from "@fedify/vocab";
3
- import { DocumentLoader } from "@fedify/vocab-runtime";
4
3
  import { MeterProvider, TracerProvider } from "@opentelemetry/api";
4
+ import { DocumentLoader } from "@fedify/vocab-runtime";
5
5
 
6
6
  //#region src/sig/key.d.ts
7
7
  /**
@@ -52,6 +52,13 @@ interface FetchKeyOptions {
52
52
  * @since 1.3.0
53
53
  */
54
54
  tracerProvider?: TracerProvider;
55
+ /**
56
+ * The OpenTelemetry meter provider to use for recording
57
+ * `activitypub.key.lookup` and `activitypub.key.lookup.duration`. If
58
+ * omitted, the global meter provider is used.
59
+ * @since 2.3.0
60
+ */
61
+ meterProvider?: MeterProvider;
55
62
  }
56
63
  /**
57
64
  * The result of {@link fetchKey}.
@@ -1,7 +1,8 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-DVsHS7rA.mjs";
4
+ import { n as version, t as name } from "./deno-B_9yJW3w.mjs";
5
+ import { f as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-iRBg8jTk.mjs";
5
6
  import { getLogger } from "@logtape/logtape";
6
7
  import { CryptographicKey, Object as Object$1, isActor } from "@fedify/vocab";
7
8
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
@@ -306,24 +307,48 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
306
307
  };
307
308
  }
308
309
  async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, onFetchError) {
309
- const logger = getLogger([
310
- "fedify",
311
- "sig",
312
- "key"
313
- ]);
314
- const keyId = cacheKey.href;
315
- const keyCache = options.keyCache;
316
- const cached = await getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger);
317
- if (cached?.key === null && cached.cached) return await onCachedUnavailable(cacheKey, keyId, keyCache, logger);
318
- if (cached != null) return cached;
319
- logger.debug("Fetching key {keyId} to verify signature...", { keyId });
320
- let document;
310
+ const start = performance.now();
311
+ let outcome = { result: "error" };
321
312
  try {
322
- document = (await (options.documentLoader ?? getDocumentLoader())(keyId)).document;
323
- } catch (error) {
324
- return await onFetchError(error, cacheKey, keyId, keyCache, logger);
313
+ const logger = getLogger([
314
+ "fedify",
315
+ "sig",
316
+ "key"
317
+ ]);
318
+ const keyId = cacheKey.href;
319
+ const keyCache = options.keyCache;
320
+ const cached = await getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger);
321
+ if (cached?.key === null && cached.cached) {
322
+ const cachedUnavailable = await onCachedUnavailable(cacheKey, keyId, keyCache, logger);
323
+ outcome = { result: "hit" };
324
+ return cachedUnavailable;
325
+ }
326
+ if (cached != null) {
327
+ outcome = { result: "hit" };
328
+ return cached;
329
+ }
330
+ logger.debug("Fetching key {keyId} to verify signature...", { keyId });
331
+ let document;
332
+ try {
333
+ document = (await (options.documentLoader ?? getDocumentLoader())(keyId)).document;
334
+ } catch (error) {
335
+ const classified = classifyFetchError(error);
336
+ const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
337
+ outcome = classified;
338
+ return errored;
339
+ }
340
+ const resolved = await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
341
+ outcome = { result: resolved.key != null ? "fetched" : "invalid" };
342
+ return resolved;
343
+ } finally {
344
+ recordKeyLookup(options.meterProvider, {
345
+ durationMs: getDurationMs(start),
346
+ result: outcome.result,
347
+ remoteUrl: cacheKey,
348
+ cacheEnabled: options.keyCache != null,
349
+ statusCode: outcome.statusCode
350
+ });
325
351
  }
326
- return await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
327
352
  }
328
353
  async function fetchKeyInternal(keyId, cls, options = {}) {
329
354
  return await fetchKeyWithResult(typeof keyId === "string" ? new URL(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
@@ -1,6 +1,6 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { d as validateCryptoKey, t as doubleKnock } from "./http-CouJSFVK.js";
3
+ import { d as validateCryptoKey, t as doubleKnock, v as recordDocumentCache } from "./http-CToqG5ap.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  import { curry } from "es-toolkit";
6
6
  import { UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, validatePublicUrl } from "@fedify/vocab-runtime";
@@ -55,10 +55,25 @@ const logger = getLogger([
55
55
  * @param parameters The parameters for the cache.
56
56
  * @returns The decorated document loader which is cache-enabled.
57
57
  */
58
- function kvCache({ loader, kv, prefix, rules }) {
58
+ function kvCache({ loader, kv, prefix, rules, meterProvider, kind }) {
59
59
  const keyPrefix = prefix ?? ["_fedify", "remoteDocument"];
60
60
  rules ??= [[new URLPattern({}), Temporal.Duration.from({ minutes: 5 })]];
61
61
  for (const [p, duration] of rules) if (Temporal.Duration.compare(duration, { days: 30 }) > 0) throw new TypeError("The maximum cache duration is 30 days: " + (p instanceof URLPattern ? `${p.protocol}://${p.username}:${p.password}@${p.hostname}:${p.port}/${p.pathname}?${p.search}#${p.hash}` : p.toString()));
62
+ const lookupKind = kind ?? "object";
63
+ function emitCacheMetric(url, result) {
64
+ if (meterProvider == null) return;
65
+ let remoteUrl;
66
+ try {
67
+ remoteUrl = new URL(url);
68
+ } catch {
69
+ remoteUrl = void 0;
70
+ }
71
+ recordDocumentCache(meterProvider, {
72
+ kind: lookupKind,
73
+ result,
74
+ remoteUrl
75
+ });
76
+ }
62
77
  return async (url, options) => {
63
78
  if (url in preloadedContexts) {
64
79
  logger.debug("Using preloaded context: {url}.", { url });
@@ -81,6 +96,7 @@ function kvCache({ loader, kv, prefix, rules }) {
81
96
  });
82
97
  }
83
98
  if (cache == null) {
99
+ emitCacheMetric(url, "miss");
84
100
  const remoteDoc = await loader(url, options);
85
101
  try {
86
102
  await kv.set(key, remoteDoc, { ttl: match });
@@ -92,6 +108,7 @@ function kvCache({ loader, kv, prefix, rules }) {
92
108
  }
93
109
  return remoteDoc;
94
110
  }
111
+ emitCacheMetric(url, "hit");
95
112
  return cache;
96
113
  };
97
114
  }
@@ -1,7 +1,7 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  require("./chunk-DDcVe30Y.cjs");
4
- const require_http = require("./http-CubOB9wq.cjs");
4
+ const require_http = require("./http-CWoeyogl.cjs");
5
5
  let _logtape_logtape = require("@logtape/logtape");
6
6
  let es_toolkit = require("es-toolkit");
7
7
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
@@ -56,10 +56,25 @@ const logger = (0, _logtape_logtape.getLogger)([
56
56
  * @param parameters The parameters for the cache.
57
57
  * @returns The decorated document loader which is cache-enabled.
58
58
  */
59
- function kvCache({ loader, kv, prefix, rules }) {
59
+ function kvCache({ loader, kv, prefix, rules, meterProvider, kind }) {
60
60
  const keyPrefix = prefix ?? ["_fedify", "remoteDocument"];
61
61
  rules ??= [[new URLPattern({}), Temporal.Duration.from({ minutes: 5 })]];
62
62
  for (const [p, duration] of rules) if (Temporal.Duration.compare(duration, { days: 30 }) > 0) throw new TypeError("The maximum cache duration is 30 days: " + (p instanceof URLPattern ? `${p.protocol}://${p.username}:${p.password}@${p.hostname}:${p.port}/${p.pathname}?${p.search}#${p.hash}` : p.toString()));
63
+ const lookupKind = kind ?? "object";
64
+ function emitCacheMetric(url, result) {
65
+ if (meterProvider == null) return;
66
+ let remoteUrl;
67
+ try {
68
+ remoteUrl = new URL(url);
69
+ } catch {
70
+ remoteUrl = void 0;
71
+ }
72
+ require_http.recordDocumentCache(meterProvider, {
73
+ kind: lookupKind,
74
+ result,
75
+ remoteUrl
76
+ });
77
+ }
63
78
  return async (url, options) => {
64
79
  if (url in _fedify_vocab_runtime.preloadedContexts) {
65
80
  logger.debug("Using preloaded context: {url}.", { url });
@@ -82,6 +97,7 @@ function kvCache({ loader, kv, prefix, rules }) {
82
97
  });
83
98
  }
84
99
  if (cache == null) {
100
+ emitCacheMetric(url, "miss");
85
101
  const remoteDoc = await loader(url, options);
86
102
  try {
87
103
  await kv.set(key, remoteDoc, { ttl: match });
@@ -93,6 +109,7 @@ function kvCache({ loader, kv, prefix, rules }) {
93
109
  }
94
110
  return remoteDoc;
95
111
  }
112
+ emitCacheMetric(url, "hit");
96
113
  return cache;
97
114
  };
98
115
  }
@@ -1,6 +1,7 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
+ import { c as recordDocumentCache } from "./metrics-iRBg8jTk.mjs";
4
5
  import { getLogger } from "@logtape/logtape";
5
6
  import { preloadedContexts } from "@fedify/vocab-runtime";
6
7
  //#region src/utils/kv-cache.ts
@@ -44,10 +45,25 @@ var MockKvStore = class {
44
45
  * @param parameters The parameters for the cache.
45
46
  * @returns The decorated document loader which is cache-enabled.
46
47
  */
47
- function kvCache({ loader, kv, prefix, rules }) {
48
+ function kvCache({ loader, kv, prefix, rules, meterProvider, kind }) {
48
49
  const keyPrefix = prefix ?? ["_fedify", "remoteDocument"];
49
50
  rules ??= [[new URLPattern({}), Temporal.Duration.from({ minutes: 5 })]];
50
51
  for (const [p, duration] of rules) if (Temporal.Duration.compare(duration, { days: 30 }) > 0) throw new TypeError("The maximum cache duration is 30 days: " + (p instanceof URLPattern ? `${p.protocol}://${p.username}:${p.password}@${p.hostname}:${p.port}/${p.pathname}?${p.search}#${p.hash}` : p.toString()));
52
+ const lookupKind = kind ?? "object";
53
+ function emitCacheMetric(url, result) {
54
+ if (meterProvider == null) return;
55
+ let remoteUrl;
56
+ try {
57
+ remoteUrl = new URL(url);
58
+ } catch {
59
+ remoteUrl = void 0;
60
+ }
61
+ recordDocumentCache(meterProvider, {
62
+ kind: lookupKind,
63
+ result,
64
+ remoteUrl
65
+ });
66
+ }
51
67
  return async (url, options) => {
52
68
  if (url in preloadedContexts) {
53
69
  logger.debug("Using preloaded context: {url}.", { url });
@@ -70,6 +86,7 @@ function kvCache({ loader, kv, prefix, rules }) {
70
86
  });
71
87
  }
72
88
  if (cache == null) {
89
+ emitCacheMetric(url, "miss");
73
90
  const remoteDoc = await loader(url, options);
74
91
  try {
75
92
  await kv.set(key, remoteDoc, { ttl: match });
@@ -81,6 +98,7 @@ function kvCache({ loader, kv, prefix, rules }) {
81
98
  }
82
99
  return remoteDoc;
83
100
  }
101
+ emitCacheMetric(url, "hit");
84
102
  return cache;
85
103
  };
86
104
  }
@@ -1,9 +1,9 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-DVsHS7rA.mjs";
5
- import { a as measureSignatureKeyFetch, n as getFederationMetrics, t as getDurationMs } from "./metrics-C4attqv0.mjs";
6
- import { n as fetchKey, o as validateCryptoKey } from "./key-BoWaYRHm.mjs";
4
+ import { n as version, t as name } from "./deno-B_9yJW3w.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-iRBg8jTk.mjs";
6
+ import { n as fetchKey, o as validateCryptoKey } from "./key-CkkMJBjF.mjs";
7
7
  import { getLogger } from "@logtape/logtape";
8
8
  import { Activity, CryptographicKey, Object as Object$1, getTypeId } from "@fedify/vocab";
9
9
  import { SpanStatusCode, trace } from "@opentelemetry/api";