@fedify/fedify 2.3.0-dev.1114 → 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.
- package/dist/{builder-YlEusQth.mjs → builder-DckAhD27.mjs} +2 -2
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{context-cSUMk2da.d.ts → context-Cq18Gplu.d.ts} +3 -208
- package/dist/{context-Ch-ZLyTQ.d.cts → context-tc6VOOOL.d.cts} +3 -208
- package/dist/{deno-CF3jMgip.mjs → deno--CS-SBS9.mjs} +1 -1
- package/dist/{docloader-BENj6vQ4.mjs → docloader-k6huZLQL.mjs} +2 -2
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/handler.test.mjs +2 -2
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/metrics.test.d.mts +2 -0
- package/dist/federation/metrics.test.mjs +335 -0
- package/dist/federation/middleware.test.mjs +444 -6
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +3 -2
- package/dist/federation/mod.d.ts +3 -2
- package/dist/federation/mod.js +1 -1
- package/dist/federation/send.test.mjs +3 -3
- package/dist/federation/webfinger.test.mjs +1 -1
- package/dist/{http-CKCgOPkX.cjs → http-CJfvRL7D.cjs} +352 -22
- package/dist/{http-BmOZYc-8.mjs → http-IywnQdiX.mjs} +7 -5
- package/dist/{http-D6LP89UO.d.ts → http-VyDTd4G3.d.cts} +8 -1
- package/dist/{http-CpzZ9zsb.js → http-cqujdCRz.js} +323 -23
- package/dist/{http-D6aw3j2U.d.cts → http-lf8Hsd91.d.ts} +8 -1
- package/dist/{key-B4I8H5Lc.mjs → key-Df3tMleh.mjs} +42 -17
- package/dist/{kv-cache-DY-XWOqM.cjs → kv-cache-L0SMQkcd.cjs} +19 -2
- package/dist/{kv-cache-Wc5ezcVW.js → kv-cache-pEejzYq4.js} +19 -2
- package/dist/{kv-cache-DihufyAQ.mjs → kv-cache-q9Ec2ryS.mjs} +19 -1
- package/dist/{ld-B5D5THhl.mjs → ld-BGwiJpl3.mjs} +3 -3
- package/dist/{metrics-ek3ilf6c.mjs → metrics-BTOMkW8C.mjs} +280 -5
- package/dist/{middleware-EqTYPG4F.cjs → middleware-B2rtdpFV.cjs} +75 -28
- package/dist/{middleware-DlcecZMq.mjs → middleware-BB0IbDow.mjs} +84 -37
- package/dist/{middleware-EI7OU6BR.mjs → middleware-Dnql59Y8.mjs} +1 -1
- package/dist/{middleware-CuZbBw-N.js → middleware-DtOddSVg.js} +75 -28
- package/dist/{mod-BDhgfjP7.d.cts → mod-B0hW12_O.d.cts} +1 -1
- package/dist/{mod-B-Lin9Sy.d.ts → mod-COIAjwRS.d.ts} +1 -1
- package/dist/{mod-C6E8rkcz.d.ts → mod-CajNYYkt.d.ts} +1 -1
- package/dist/{mod-DLrRb0dx.d.ts → mod-DFvNJcNb.d.ts} +54 -3
- package/dist/{mod-P9tE2WmM.d.cts → mod-DnzgcPcy.d.cts} +1 -1
- package/dist/{mod-BR_BB0bh.d.cts → mod-yvIXFAEi.d.cts} +54 -3
- package/dist/mod.cjs +4 -4
- package/dist/mod.d.cts +6 -5
- package/dist/mod.d.ts +6 -5
- package/dist/mod.js +4 -4
- package/dist/mq-D-nlpY04.d.ts +208 -0
- package/dist/mq-D8uSFzxe.d.cts +208 -0
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-DO810N24.mjs → owner-CIt4hvmM.mjs} +2 -2
- package/dist/{proof-DIoqrKnX.cjs → proof-B1_u25UV.cjs} +1 -1
- package/dist/{proof-BgfyWv7b.mjs → proof-BYlrRSmZ.mjs} +3 -3
- package/dist/{proof-Vd8-1EWh.js → proof-DMGIjHYH.js} +1 -1
- package/dist/{send-CAYXdUTk.mjs → send-DJFpze7B.mjs} +3 -3
- package/dist/sig/http.test.mjs +6 -2
- package/dist/sig/key.test.mjs +99 -2
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.d.cts +2 -2
- package/dist/sig/mod.d.ts +2 -2
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/proof.test.mjs +1 -1
- package/dist/utils/docloader.test.mjs +2 -2
- package/dist/utils/kv-cache.test.mjs +67 -2
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- 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.
|
|
13
|
+
var version = "2.3.0-dev.1131+553b59b8";
|
|
14
14
|
//#endregion
|
|
15
15
|
//#region src/sig/accept.ts
|
|
16
16
|
/**
|
|
@@ -168,6 +168,14 @@ var FederationMetrics = class {
|
|
|
168
168
|
queueTaskFailed;
|
|
169
169
|
queueTaskDuration;
|
|
170
170
|
queueTaskInFlight;
|
|
171
|
+
fanoutRecipients;
|
|
172
|
+
inboxActivity;
|
|
173
|
+
outboxActivity;
|
|
174
|
+
keyLookup;
|
|
175
|
+
keyLookupDuration;
|
|
176
|
+
documentFetch;
|
|
177
|
+
documentFetchDuration;
|
|
178
|
+
documentCache;
|
|
171
179
|
constructor(meterProvider) {
|
|
172
180
|
const meter = meterProvider.getMeter(name, version);
|
|
173
181
|
this.deliverySent = meter.createCounter("activitypub.delivery.sent", {
|
|
@@ -262,6 +270,70 @@ var FederationMetrics = class {
|
|
|
262
270
|
description: "Queue tasks currently being processed in this Fedify process.",
|
|
263
271
|
unit: "{task}"
|
|
264
272
|
});
|
|
273
|
+
this.fanoutRecipients = meter.createHistogram("activitypub.fanout.recipients", {
|
|
274
|
+
description: "Number of recipient inboxes produced by an ActivityPub fanout task.",
|
|
275
|
+
unit: "{recipient}"
|
|
276
|
+
});
|
|
277
|
+
this.inboxActivity = meter.createCounter("activitypub.inbox.activity", {
|
|
278
|
+
description: "ActivityPub activities observed at the inbox lifecycle level: queued, processed, retried, rejected, or abandoned.",
|
|
279
|
+
unit: "{activity}"
|
|
280
|
+
});
|
|
281
|
+
this.outboxActivity = meter.createCounter("activitypub.outbox.activity", {
|
|
282
|
+
description: "ActivityPub activities observed at the outbox lifecycle level: queued, retried, or abandoned. Per-recipient delivery counters live on `activitypub.delivery.*`.",
|
|
283
|
+
unit: "{activity}"
|
|
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
|
+
});
|
|
265
337
|
}
|
|
266
338
|
recordDelivery(inbox, durationMs, success, activityType) {
|
|
267
339
|
const deliveryAttributes = {
|
|
@@ -334,7 +406,53 @@ var FederationMetrics = class {
|
|
|
334
406
|
else if (result === "failed") this.queueTaskFailed.add(1, attributes);
|
|
335
407
|
this.queueTaskDuration.record(durationMs, attributes);
|
|
336
408
|
}
|
|
409
|
+
recordFanoutRecipients(recipientCount, activityType) {
|
|
410
|
+
const attributes = {};
|
|
411
|
+
if (activityType != null) attributes["activitypub.activity.type"] = activityType;
|
|
412
|
+
this.fanoutRecipients.record(recipientCount, attributes);
|
|
413
|
+
}
|
|
414
|
+
recordInboxActivity(result, activityType) {
|
|
415
|
+
this.inboxActivity.add(1, buildActivityLifecycleAttributes(result, activityType));
|
|
416
|
+
}
|
|
417
|
+
recordOutboxActivity(result, activityType) {
|
|
418
|
+
this.outboxActivity.add(1, buildActivityLifecycleAttributes(result, activityType));
|
|
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
|
+
}
|
|
337
450
|
};
|
|
451
|
+
function buildActivityLifecycleAttributes(result, activityType) {
|
|
452
|
+
const attributes = { "activitypub.processing.result": result };
|
|
453
|
+
if (activityType != null) attributes["activitypub.activity.type"] = activityType;
|
|
454
|
+
return attributes;
|
|
455
|
+
}
|
|
338
456
|
function buildQueueTaskAttributes(common) {
|
|
339
457
|
const attributes = { "fedify.queue.role": common.role };
|
|
340
458
|
const backend = getQueueBackend(common.queue);
|
|
@@ -364,20 +482,176 @@ function getQueueBackend(queue) {
|
|
|
364
482
|
return name;
|
|
365
483
|
}
|
|
366
484
|
/**
|
|
367
|
-
* Records `fedify.queue.task.enqueued` for an outgoing outbox enqueue
|
|
485
|
+
* Records `fedify.queue.task.enqueued` for an outgoing outbox enqueue and,
|
|
486
|
+
* for the initial attempt, also records
|
|
487
|
+
* `activitypub.outbox.activity{queued}`.
|
|
368
488
|
*
|
|
369
489
|
* Both `Context.sendActivity()` and `OutboxContext.forwardActivity()` enqueue
|
|
370
490
|
* outbox messages with the same metric attributes (role, queue, activity
|
|
371
491
|
* type, attempt), so they share this helper rather than each defining a local
|
|
372
|
-
* closure.
|
|
492
|
+
* closure. Retry enqueues (attempt > 0) intentionally do not record a
|
|
493
|
+
* second `activitypub.outbox.activity{queued}`; retries are reported as
|
|
494
|
+
* `result=retried` from the retry-scheduling site, which has the failure
|
|
495
|
+
* context.
|
|
373
496
|
* @since 2.3.0
|
|
374
497
|
*/
|
|
375
498
|
function recordOutboxEnqueue(meterProvider, outboxQueue, message) {
|
|
376
|
-
getFederationMetrics(meterProvider)
|
|
499
|
+
const metrics = getFederationMetrics(meterProvider);
|
|
500
|
+
metrics.recordQueueTaskEnqueued({
|
|
377
501
|
role: "outbox",
|
|
378
502
|
queue: outboxQueue,
|
|
379
503
|
activityType: message.activityType
|
|
380
504
|
}, message.attempt);
|
|
505
|
+
if (message.attempt === 0) metrics.recordOutboxActivity("queued", message.activityType);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Records `activitypub.fanout.recipients` with the number of recipient
|
|
509
|
+
* inboxes a single fanout produced. The histogram is unitless count
|
|
510
|
+
* (one measurement per fanout enqueue). Recipient URLs are deliberately
|
|
511
|
+
* not recorded; only the activity type, when known.
|
|
512
|
+
* @since 2.3.0
|
|
513
|
+
*/
|
|
514
|
+
function recordFanoutRecipients(meterProvider, recipientCount, activityType) {
|
|
515
|
+
getFederationMetrics(meterProvider).recordFanoutRecipients(recipientCount, activityType);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Records one `activitypub.inbox.activity` measurement. The
|
|
519
|
+
* `activitypub.processing.result` attribute is always present;
|
|
520
|
+
* `activitypub.activity.type` is recorded only when Fedify already knows
|
|
521
|
+
* the activity type.
|
|
522
|
+
* @since 2.3.0
|
|
523
|
+
*/
|
|
524
|
+
function recordInboxActivity(meterProvider, result, activityType) {
|
|
525
|
+
getFederationMetrics(meterProvider).recordInboxActivity(result, activityType);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Records one `activitypub.outbox.activity` measurement. The
|
|
529
|
+
* `activitypub.processing.result` attribute is always present;
|
|
530
|
+
* `activitypub.activity.type` is recorded only when Fedify already knows
|
|
531
|
+
* the activity type (it is always known for outbox lifecycle events).
|
|
532
|
+
* @since 2.3.0
|
|
533
|
+
*/
|
|
534
|
+
function recordOutboxActivity(meterProvider, result, activityType) {
|
|
535
|
+
getFederationMetrics(meterProvider).recordOutboxActivity(result, activityType);
|
|
536
|
+
}
|
|
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
|
+
};
|
|
381
655
|
}
|
|
382
656
|
/**
|
|
383
657
|
* Times an awaited public key fetch and records exactly one
|
|
@@ -759,24 +1033,48 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
|
|
|
759
1033
|
};
|
|
760
1034
|
}
|
|
761
1035
|
async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, onFetchError) {
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
"sig",
|
|
765
|
-
"key"
|
|
766
|
-
]);
|
|
767
|
-
const keyId = cacheKey.href;
|
|
768
|
-
const keyCache = options.keyCache;
|
|
769
|
-
const cached = await getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger);
|
|
770
|
-
if (cached?.key === null && cached.cached) return await onCachedUnavailable(cacheKey, keyId, keyCache, logger);
|
|
771
|
-
if (cached != null) return cached;
|
|
772
|
-
logger.debug("Fetching key {keyId} to verify signature...", { keyId });
|
|
773
|
-
let document;
|
|
1036
|
+
const start = performance.now();
|
|
1037
|
+
let outcome = { result: "error" };
|
|
774
1038
|
try {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
+
});
|
|
778
1077
|
}
|
|
779
|
-
return await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
|
|
780
1078
|
}
|
|
781
1079
|
async function fetchKeyInternal(keyId, cls, options = {}) {
|
|
782
1080
|
return await fetchKeyWithResult(typeof keyId === "string" ? new URL(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
|
|
@@ -1358,7 +1656,8 @@ async function verifyRequestDraft(request, span, metricsContext, { documentLoade
|
|
|
1358
1656
|
documentLoader,
|
|
1359
1657
|
contextLoader,
|
|
1360
1658
|
keyCache,
|
|
1361
|
-
tracerProvider
|
|
1659
|
+
tracerProvider,
|
|
1660
|
+
meterProvider
|
|
1362
1661
|
}));
|
|
1363
1662
|
if (fetchError != null) return keyFetchErrorResult(keyIdUrl, fetchError);
|
|
1364
1663
|
if (key == null) return invalidSignatureResult(keyIdUrl);
|
|
@@ -1573,7 +1872,8 @@ async function verifyRequestRfc9421(request, span, metricsContext, { documentLoa
|
|
|
1573
1872
|
documentLoader,
|
|
1574
1873
|
contextLoader,
|
|
1575
1874
|
keyCache,
|
|
1576
|
-
tracerProvider
|
|
1875
|
+
tracerProvider,
|
|
1876
|
+
meterProvider
|
|
1577
1877
|
}));
|
|
1578
1878
|
if (fetchError != null) {
|
|
1579
1879
|
setFailure(keyFetchErrorResult(keyId, fetchError));
|
|
@@ -1876,4 +2176,4 @@ function timingSafeEqual(a, b) {
|
|
|
1876
2176
|
return result === 0;
|
|
1877
2177
|
}
|
|
1878
2178
|
//#endregion
|
|
1879
|
-
export {
|
|
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-
|
|
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
|
|
310
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
5
|
-
import {
|
|
6
|
-
import { n as fetchKey, o as validateCryptoKey } from "./key-
|
|
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";
|