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

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 (68) hide show
  1. package/dist/{builder-Ond_h57y.mjs → builder-DckAhD27.mjs} +2 -2
  2. package/dist/compat/mod.d.cts +1 -1
  3. package/dist/compat/mod.d.ts +1 -1
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{context-cSUMk2da.d.ts → context-Cq18Gplu.d.ts} +3 -208
  6. package/dist/{context-Ch-ZLyTQ.d.cts → context-tc6VOOOL.d.cts} +3 -208
  7. package/dist/{deno-DVsHS7rA.mjs → deno--CS-SBS9.mjs} +1 -1
  8. package/dist/{docloader-WsWfKaE5.mjs → docloader-k6huZLQL.mjs} +2 -2
  9. package/dist/federation/builder.test.mjs +1 -1
  10. package/dist/federation/handler.test.mjs +2 -2
  11. package/dist/federation/idempotency.test.mjs +2 -2
  12. package/dist/federation/metrics.test.mjs +229 -1
  13. package/dist/federation/middleware.test.mjs +64 -6
  14. package/dist/federation/mod.cjs +1 -1
  15. package/dist/federation/mod.d.cts +3 -2
  16. package/dist/federation/mod.d.ts +3 -2
  17. package/dist/federation/mod.js +1 -1
  18. package/dist/federation/send.test.mjs +3 -3
  19. package/dist/federation/webfinger.test.mjs +1 -1
  20. package/dist/{http-CubOB9wq.cjs → http-CJfvRL7D.cjs} +263 -19
  21. package/dist/{http-DUV8ysti.mjs → http-IywnQdiX.mjs} +7 -5
  22. package/dist/{http-D6LP89UO.d.ts → http-VyDTd4G3.d.cts} +8 -1
  23. package/dist/{http-CouJSFVK.js → http-cqujdCRz.js} +252 -20
  24. package/dist/{http-D6aw3j2U.d.cts → http-lf8Hsd91.d.ts} +8 -1
  25. package/dist/{key-BoWaYRHm.mjs → key-Df3tMleh.mjs} +42 -17
  26. package/dist/{kv-cache-Dz31ATUT.cjs → kv-cache-L0SMQkcd.cjs} +19 -2
  27. package/dist/{kv-cache-DBNpsneh.js → kv-cache-pEejzYq4.js} +19 -2
  28. package/dist/{kv-cache-DihufyAQ.mjs → kv-cache-q9Ec2ryS.mjs} +19 -1
  29. package/dist/{ld-B5K1mSuG.mjs → ld-BGwiJpl3.mjs} +3 -3
  30. package/dist/{metrics-C4attqv0.mjs → metrics-BTOMkW8C.mjs} +209 -2
  31. package/dist/{middleware-CmsDtIHI.cjs → middleware-B2rtdpFV.cjs} +45 -17
  32. package/dist/{middleware-t0jC8I99.mjs → middleware-BB0IbDow.mjs} +54 -26
  33. package/dist/{middleware-BDKFRjue.mjs → middleware-Dnql59Y8.mjs} +1 -1
  34. package/dist/{middleware-Dtjz-hSk.js → middleware-DtOddSVg.js} +45 -17
  35. package/dist/{mod-BDhgfjP7.d.cts → mod-B0hW12_O.d.cts} +1 -1
  36. package/dist/{mod-B-Lin9Sy.d.ts → mod-COIAjwRS.d.ts} +1 -1
  37. package/dist/{mod-C6E8rkcz.d.ts → mod-CajNYYkt.d.ts} +1 -1
  38. package/dist/{mod-DLrRb0dx.d.ts → mod-DFvNJcNb.d.ts} +54 -3
  39. package/dist/{mod-P9tE2WmM.d.cts → mod-DnzgcPcy.d.cts} +1 -1
  40. package/dist/{mod-BR_BB0bh.d.cts → mod-yvIXFAEi.d.cts} +54 -3
  41. package/dist/mod.cjs +4 -4
  42. package/dist/mod.d.cts +6 -5
  43. package/dist/mod.d.ts +6 -5
  44. package/dist/mod.js +4 -4
  45. package/dist/mq-D-nlpY04.d.ts +208 -0
  46. package/dist/mq-D8uSFzxe.d.cts +208 -0
  47. package/dist/nodeinfo/handler.test.mjs +1 -1
  48. package/dist/{owner-hDxI0ufu.mjs → owner-CIt4hvmM.mjs} +2 -2
  49. package/dist/{proof-BUWfVr6Q.cjs → proof-B1_u25UV.cjs} +1 -1
  50. package/dist/{proof-DhVuz4bc.mjs → proof-BYlrRSmZ.mjs} +3 -3
  51. package/dist/{proof-n60t8o9P.js → proof-DMGIjHYH.js} +1 -1
  52. package/dist/{send-BPhyR5Oo.mjs → send-DJFpze7B.mjs} +3 -3
  53. package/dist/sig/http.test.mjs +6 -2
  54. package/dist/sig/key.test.mjs +99 -2
  55. package/dist/sig/ld.test.mjs +2 -2
  56. package/dist/sig/mod.cjs +2 -2
  57. package/dist/sig/mod.d.cts +2 -2
  58. package/dist/sig/mod.d.ts +2 -2
  59. package/dist/sig/mod.js +2 -2
  60. package/dist/sig/owner.test.mjs +1 -1
  61. package/dist/sig/proof.test.mjs +1 -1
  62. package/dist/utils/docloader.test.mjs +2 -2
  63. package/dist/utils/kv-cache.test.mjs +67 -2
  64. package/dist/utils/mod.cjs +1 -1
  65. package/dist/utils/mod.d.cts +1 -1
  66. package/dist/utils/mod.d.ts +1 -1
  67. package/dist/utils/mod.js +1 -1
  68. package/package.json +6 -6
@@ -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.1119+6cc02662";
13
+ var version = "2.3.0-dev.1131+553b59b8";
14
14
  //#endregion
15
15
  //#region src/sig/accept.ts
16
16
  /**
@@ -171,6 +171,11 @@ var FederationMetrics = class {
171
171
  fanoutRecipients;
172
172
  inboxActivity;
173
173
  outboxActivity;
174
+ keyLookup;
175
+ keyLookupDuration;
176
+ documentFetch;
177
+ documentFetchDuration;
178
+ documentCache;
174
179
  constructor(meterProvider) {
175
180
  const meter = meterProvider.getMeter(name, version);
176
181
  this.deliverySent = meter.createCounter("activitypub.delivery.sent", {
@@ -277,6 +282,58 @@ var FederationMetrics = class {
277
282
  description: "ActivityPub activities observed at the outbox lifecycle level: queued, retried, or abandoned. Per-recipient delivery counters live on `activitypub.delivery.*`.",
278
283
  unit: "{activity}"
279
284
  });
285
+ this.keyLookup = meter.createCounter("activitypub.key.lookup", {
286
+ description: "Public-key lookup attempts performed by Fedify, including both cache hits and remote fetches.",
287
+ unit: "{lookup}"
288
+ });
289
+ this.keyLookupDuration = meter.createHistogram("activitypub.key.lookup.duration", {
290
+ description: "Duration of public-key lookups performed by Fedify, including any remote fetch.",
291
+ unit: "ms",
292
+ advice: { explicitBucketBoundaries: [
293
+ 5,
294
+ 10,
295
+ 25,
296
+ 50,
297
+ 75,
298
+ 100,
299
+ 250,
300
+ 500,
301
+ 750,
302
+ 1e3,
303
+ 2500,
304
+ 5e3,
305
+ 7500,
306
+ 1e4
307
+ ] }
308
+ });
309
+ this.documentFetch = meter.createCounter("activitypub.document.fetch", {
310
+ description: "Remote JSON-LD document loader invocations made by Fedify-wrapped loaders.",
311
+ unit: "{fetch}"
312
+ });
313
+ this.documentFetchDuration = meter.createHistogram("activitypub.document.fetch.duration", {
314
+ description: "Duration of remote JSON-LD document loader invocations made by Fedify-wrapped loaders.",
315
+ unit: "ms",
316
+ advice: { explicitBucketBoundaries: [
317
+ 5,
318
+ 10,
319
+ 25,
320
+ 50,
321
+ 75,
322
+ 100,
323
+ 250,
324
+ 500,
325
+ 750,
326
+ 1e3,
327
+ 2500,
328
+ 5e3,
329
+ 7500,
330
+ 1e4
331
+ ] }
332
+ });
333
+ this.documentCache = meter.createCounter("activitypub.document.cache", {
334
+ description: "KV-backed document loader cache lookups, with `hit` or `miss` classification.",
335
+ unit: "{lookup}"
336
+ });
280
337
  }
281
338
  recordDelivery(inbox, durationMs, success, activityType) {
282
339
  const deliveryAttributes = {
@@ -360,6 +417,36 @@ var FederationMetrics = class {
360
417
  recordOutboxActivity(result, activityType) {
361
418
  this.outboxActivity.add(1, buildActivityLifecycleAttributes(result, activityType));
362
419
  }
420
+ recordKeyLookup(attrs) {
421
+ const attributes = {
422
+ "activitypub.lookup.kind": "public_key",
423
+ "activitypub.lookup.result": attrs.result,
424
+ "activitypub.cache.enabled": attrs.cacheEnabled
425
+ };
426
+ if (attrs.remoteUrl != null) attributes["activitypub.remote.host"] = getRemoteHost(attrs.remoteUrl);
427
+ if (attrs.statusCode != null) attributes["http.response.status_code"] = attrs.statusCode;
428
+ this.keyLookup.add(1, attributes);
429
+ this.keyLookupDuration.record(attrs.durationMs, attributes);
430
+ }
431
+ recordDocumentFetch(attrs) {
432
+ const attributes = {
433
+ "activitypub.lookup.kind": attrs.kind,
434
+ "activitypub.lookup.result": attrs.result
435
+ };
436
+ if (attrs.remoteUrl != null) attributes["activitypub.remote.host"] = getRemoteHost(attrs.remoteUrl);
437
+ if (attrs.cacheEnabled != null) attributes["activitypub.cache.enabled"] = attrs.cacheEnabled;
438
+ if (attrs.statusCode != null) attributes["http.response.status_code"] = attrs.statusCode;
439
+ this.documentFetch.add(1, attributes);
440
+ this.documentFetchDuration.record(attrs.durationMs, attributes);
441
+ }
442
+ recordDocumentCache(attrs) {
443
+ const attributes = {
444
+ "activitypub.lookup.kind": attrs.kind,
445
+ "activitypub.lookup.result": attrs.result
446
+ };
447
+ if (attrs.remoteUrl != null) attributes["activitypub.remote.host"] = getRemoteHost(attrs.remoteUrl);
448
+ this.documentCache.add(1, attributes);
449
+ }
363
450
  };
364
451
  function buildActivityLifecycleAttributes(result, activityType) {
365
452
  const attributes = { "activitypub.processing.result": result };
@@ -448,6 +535,125 @@ function recordOutboxActivity(meterProvider, result, activityType) {
448
535
  getFederationMetrics(meterProvider).recordOutboxActivity(result, activityType);
449
536
  }
450
537
  /**
538
+ * Records one measurement on `activitypub.key.lookup` (counter) and
539
+ * `activitypub.key.lookup.duration` (histogram) for a public-key lookup.
540
+ *
541
+ * `activitypub.lookup.kind` is always recorded as `public_key`; the result
542
+ * classification, remote host, HTTP status code (when an HTTP response was
543
+ * received), and `activitypub.cache.enabled` are recorded as attributes on
544
+ * both measurements. Full key URLs and key IDs are deliberately omitted to
545
+ * keep cardinality bounded.
546
+ * @since 2.3.0
547
+ */
548
+ function recordKeyLookup(meterProvider, attrs) {
549
+ getFederationMetrics(meterProvider).recordKeyLookup(attrs);
550
+ }
551
+ /**
552
+ * Records one measurement each on `activitypub.document.fetch` (counter)
553
+ * and `activitypub.document.fetch.duration` (histogram) for one remote
554
+ * JSON-LD document loader invocation, with bounded
555
+ * `activitypub.lookup.kind` and `activitypub.lookup.result` attributes
556
+ * plus the optional remote-host, cache-enabled, and HTTP status-code
557
+ * attributes. Counter and histogram are always recorded together so
558
+ * aggregate rate and latency views stay in sync.
559
+ * @since 2.3.0
560
+ */
561
+ function recordDocumentFetch(meterProvider, attrs) {
562
+ getFederationMetrics(meterProvider).recordDocumentFetch(attrs);
563
+ }
564
+ /**
565
+ * Records one `activitypub.document.cache` measurement, classifying the
566
+ * lookup as `hit` (the cache returned an entry) or `miss` (the cache was
567
+ * consulted and returned nothing, prompting a delegate fetch).
568
+ * @since 2.3.0
569
+ */
570
+ function recordDocumentCache(meterProvider, attrs) {
571
+ getFederationMetrics(meterProvider).recordDocumentCache(attrs);
572
+ }
573
+ /**
574
+ * Classifies a thrown value from a key or document fetch into the bounded
575
+ * {@link LookupResult} taxonomy and, when an HTTP response was received,
576
+ * surfaces its status code.
577
+ *
578
+ * - `FetchError` with a `Response` whose status is `404` or `410`:
579
+ * `result=not_found` and the response status code.
580
+ * - `FetchError` with any other `Response`: `result=error` and the
581
+ * response status code.
582
+ * - `FetchError` without a `Response`: `result=network_error`.
583
+ * - An `AbortError` (typically from a cancelled fetch): `result=network_error`.
584
+ * - A bare `TypeError` (the shape native `fetch()` raises on DNS, connect,
585
+ * and TLS failures before any response is observed):
586
+ * `result=network_error`.
587
+ * - Any other value: `result=error`.
588
+ * @since 2.3.0
589
+ */
590
+ function classifyFetchError(error) {
591
+ if (error instanceof FetchError) {
592
+ if (error.response != null) {
593
+ const status = error.response.status;
594
+ return {
595
+ result: status === 404 || status === 410 ? "not_found" : "error",
596
+ statusCode: status
597
+ };
598
+ }
599
+ return { result: "network_error" };
600
+ }
601
+ if (isAbortError$1(error)) return { result: "network_error" };
602
+ if (error instanceof TypeError) return { result: "network_error" };
603
+ return { result: "error" };
604
+ }
605
+ /**
606
+ * Wraps a {@link DocumentLoader} so each invocation records one
607
+ * measurement on `activitypub.document.fetch` (counter) and one on
608
+ * `activitypub.document.fetch.duration` (histogram), classifying the
609
+ * outcome via {@link classifyFetchError} when the wrapped loader throws
610
+ * and as `fetched` on success. The wrapper rethrows whatever the
611
+ * wrapped loader throws so caller behavior is unchanged.
612
+ *
613
+ * The wrapper records the hostname of the requested URL on
614
+ * `activitypub.remote.host` when the URL parses; full URLs, paths, and
615
+ * query strings are deliberately excluded to keep cardinality bounded.
616
+ * HTTP status codes are recorded only when the failure carries a
617
+ * `Response` (currently, when the wrapped loader throws a
618
+ * {@link FetchError} with a non-`null` `response`).
619
+ * @since 2.3.0
620
+ */
621
+ function instrumentDocumentLoader(loader, options) {
622
+ const meterProvider = options.meterProvider;
623
+ if (meterProvider == null) return loader;
624
+ return async (url, opts) => {
625
+ const start = performance.now();
626
+ let remoteUrl;
627
+ try {
628
+ remoteUrl = new URL(url);
629
+ } catch {
630
+ remoteUrl = void 0;
631
+ }
632
+ try {
633
+ const result = await loader(url, opts);
634
+ recordDocumentFetch(meterProvider, {
635
+ durationMs: getDurationMs(start),
636
+ kind: options.kind,
637
+ result: "fetched",
638
+ remoteUrl,
639
+ cacheEnabled: options.cacheEnabled
640
+ });
641
+ return result;
642
+ } catch (error) {
643
+ const classified = classifyFetchError(error);
644
+ recordDocumentFetch(meterProvider, {
645
+ durationMs: getDurationMs(start),
646
+ kind: options.kind,
647
+ result: classified.result,
648
+ remoteUrl,
649
+ cacheEnabled: options.cacheEnabled,
650
+ statusCode: classified.statusCode
651
+ });
652
+ throw error;
653
+ }
654
+ };
655
+ }
656
+ /**
451
657
  * Times an awaited public key fetch and records exactly one
452
658
  * `activitypub.signature.key_fetch.duration` measurement, classifying the
453
659
  * outcome as `hit`, `fetched`, or `error` based on the `cached` flag and
@@ -827,24 +1033,48 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
827
1033
  };
828
1034
  }
829
1035
  async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, onFetchError) {
830
- const logger = getLogger([
831
- "fedify",
832
- "sig",
833
- "key"
834
- ]);
835
- const keyId = cacheKey.href;
836
- const keyCache = options.keyCache;
837
- const cached = await getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger);
838
- if (cached?.key === null && cached.cached) return await onCachedUnavailable(cacheKey, keyId, keyCache, logger);
839
- if (cached != null) return cached;
840
- logger.debug("Fetching key {keyId} to verify signature...", { keyId });
841
- let document;
1036
+ const start = performance.now();
1037
+ let outcome = { result: "error" };
842
1038
  try {
843
- document = (await (options.documentLoader ?? getDocumentLoader())(keyId)).document;
844
- } catch (error) {
845
- return await onFetchError(error, cacheKey, keyId, keyCache, logger);
1039
+ const logger = getLogger([
1040
+ "fedify",
1041
+ "sig",
1042
+ "key"
1043
+ ]);
1044
+ const keyId = cacheKey.href;
1045
+ const keyCache = options.keyCache;
1046
+ const cached = await getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger);
1047
+ if (cached?.key === null && cached.cached) {
1048
+ const cachedUnavailable = await onCachedUnavailable(cacheKey, keyId, keyCache, logger);
1049
+ outcome = { result: "hit" };
1050
+ return cachedUnavailable;
1051
+ }
1052
+ if (cached != null) {
1053
+ outcome = { result: "hit" };
1054
+ return cached;
1055
+ }
1056
+ logger.debug("Fetching key {keyId} to verify signature...", { keyId });
1057
+ let document;
1058
+ try {
1059
+ document = (await (options.documentLoader ?? getDocumentLoader())(keyId)).document;
1060
+ } catch (error) {
1061
+ const classified = classifyFetchError(error);
1062
+ const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
1063
+ outcome = classified;
1064
+ return errored;
1065
+ }
1066
+ const resolved = await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
1067
+ outcome = { result: resolved.key != null ? "fetched" : "invalid" };
1068
+ return resolved;
1069
+ } finally {
1070
+ recordKeyLookup(options.meterProvider, {
1071
+ durationMs: getDurationMs(start),
1072
+ result: outcome.result,
1073
+ remoteUrl: cacheKey,
1074
+ cacheEnabled: options.keyCache != null,
1075
+ statusCode: outcome.statusCode
1076
+ });
846
1077
  }
847
- return await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
848
1078
  }
849
1079
  async function fetchKeyInternal(keyId, cls, options = {}) {
850
1080
  return await fetchKeyWithResult(typeof keyId === "string" ? new URL(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
@@ -1426,7 +1656,8 @@ async function verifyRequestDraft(request, span, metricsContext, { documentLoade
1426
1656
  documentLoader,
1427
1657
  contextLoader,
1428
1658
  keyCache,
1429
- tracerProvider
1659
+ tracerProvider,
1660
+ meterProvider
1430
1661
  }));
1431
1662
  if (fetchError != null) return keyFetchErrorResult(keyIdUrl, fetchError);
1432
1663
  if (key == null) return invalidSignatureResult(keyIdUrl);
@@ -1641,7 +1872,8 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
1641
1872
  documentLoader,
1642
1873
  contextLoader,
1643
1874
  keyCache,
1644
- tracerProvider
1875
+ tracerProvider,
1876
+ meterProvider
1645
1877
  }));
1646
1878
  if (fetchError != null) {
1647
1879
  setFailure(keyFetchErrorResult(keyId, fetchError));
@@ -1944,4 +2176,4 @@ function timingSafeEqual(a, b) {
1944
2176
  return result === 0;
1945
2177
  }
1946
2178
  //#endregion
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 };
2179
+ export { formatAcceptSignature as C, name as D, validateAcceptSignature as E, version as O, recordOutboxEnqueue as S, parseAcceptSignature as T, measureSignatureKeyFetch as _, verifyRequestDetailed as a, recordInboxActivity as b, fetchKeyDetailed as c, validateCryptoKey as d, getDurationMs as f, isAbortError$1 as g, instrumentDocumentLoader 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, recordDocumentCache as v, fulfillAcceptSignature as w, recordOutboxActivity as x, recordFanoutRecipients as y };
@@ -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--CS-SBS9.mjs";
5
+ import { f as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-BTOMkW8C.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,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-CJfvRL7D.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,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-cqujdCRz.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,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-BTOMkW8C.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--CS-SBS9.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-BTOMkW8C.mjs";
6
+ import { n as fetchKey, o as validateCryptoKey } from "./key-Df3tMleh.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";