@fedify/fedify 2.3.0-dev.994 → 2.3.0-pr.809.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -7
- package/dist/{assert-DikXweDx.mjs → assert-OguE97r2.mjs} +1 -1
- package/dist/{assert_instance_of-C4Ri6VuN.mjs → assert_instance_of-DBC5X09g.mjs} +1 -1
- package/dist/{assert_not_equals--wG9hV7u.mjs → assert_not_equals-DkVK8oqV.mjs} +1 -1
- package/dist/{assert_rejects-B-qJtC9Z.mjs → assert_rejects-DN60FHPX.mjs} +28 -3
- package/dist/{assert_strict_equals-Dmjbg-bA.mjs → assert_strict_equals-XEgZAlrj.mjs} +1 -1
- package/dist/{assert_throws-4NwKEy2q.mjs → assert_throws-BOkhLGYc.mjs} +1 -1
- package/dist/{builder-yAlpiIqP.mjs → builder-Dl5Sdmc9.mjs} +96 -42
- package/dist/{chunk-nlSIicah.js → chunk-CRNNMoPX.js} +2 -2
- package/dist/circuit-breaker-CSWsyoef.mjs +337 -0
- package/dist/{client-z-8dc-e1.d.cts → client-CAM_bQXx.d.cts} +1 -0
- package/dist/{client-AtlibPOU.d.ts → client-CSddvgWN.d.ts} +1 -2
- package/dist/compat/mod.d.cts +2 -1
- package/dist/compat/mod.d.ts +2 -3
- package/dist/compat/mod.js +3 -3
- package/dist/compat/outgoing-jsonld.test.mjs +4 -4
- package/dist/compat/public-audience.test.mjs +4 -4
- package/dist/compat/transformers.test.mjs +5 -5
- package/dist/{context-BzH2-ajs.d.ts → context-BBVLF7lx.d.cts} +342 -269
- package/dist/{context-DJGagtNd.d.cts → context-BU6jSQdo.d.ts} +344 -266
- package/dist/{context-Dk_tacqz.mjs → context-DVoTs_wM.mjs} +64 -5
- package/dist/{deno-XBVlKnOX.mjs → deno-BID9qTPk.mjs} +1 -1
- package/dist/{docloader-DPp-X6gk.mjs → docloader-BYFuVwwi.mjs} +4 -4
- package/dist/{esm-DVILvP5e.mjs → esm-vrlUxr60.mjs} +4 -7
- package/dist/federation/builder.test.mjs +163 -11
- package/dist/federation/circuit-breaker.test.d.mts +2 -0
- package/dist/federation/circuit-breaker.test.mjs +446 -0
- package/dist/federation/collection.test.mjs +3 -3
- package/dist/federation/handler.test.mjs +1770 -29
- package/dist/federation/idempotency.test.mjs +5 -5
- package/dist/federation/inbox.test.mjs +4 -4
- package/dist/federation/keycache.test.mjs +5 -5
- package/dist/federation/kv.test.mjs +2 -2
- package/dist/federation/metrics.test.d.mts +2 -0
- package/dist/federation/metrics.test.mjs +634 -0
- package/dist/federation/middleware.test.mjs +4036 -146
- package/dist/federation/mod.cjs +195 -14
- package/dist/federation/mod.d.cts +6 -4
- package/dist/federation/mod.d.ts +6 -6
- package/dist/federation/mod.js +192 -14
- package/dist/federation/mq.test.mjs +176 -15
- package/dist/federation/negotiation.test.mjs +4 -4
- package/dist/federation/retry.test.mjs +3 -3
- package/dist/federation/router.test.mjs +190 -9
- package/dist/federation/send.test.mjs +153 -15
- package/dist/federation/temporal.test.d.mts +2 -0
- package/dist/federation/temporal.test.mjs +71 -0
- package/dist/federation/webfinger.test.mjs +150 -5
- package/dist/{http-B4rrzxtg.js → http-B47Jg9iX.js} +994 -64
- package/dist/{http-CSlLA54v.mjs → http-CO59u1Yk.mjs} +146 -47
- package/dist/{http-BrGqJDXL.cjs → http-CjZwUPiT.cjs} +1099 -61
- package/dist/{http-aQzN9Ayi.d.ts → http-VyDTd4G3.d.cts} +15 -3
- package/dist/{http-CrGuipxe.d.cts → http-lf8Hsd91.d.ts} +15 -1
- package/dist/{key-Bzx--sHD.mjs → key-Dqdlo3mG.mjs} +43 -18
- package/dist/{kv-CbLNp3zQ.d.cts → kv-D6hNiMTK.d.ts} +1 -0
- package/dist/{kv-cache-DI-Sk7-I.cjs → kv-cache-BQ1-zDKX.cjs} +20 -3
- package/dist/{kv-cache-EMIqoIuB.js → kv-cache-Dh2ZH9Gr.js} +20 -3
- package/dist/{kv-cache-U__xU4qR.mjs → kv-cache-DhLkLv0P.mjs} +21 -3
- package/dist/{kv-GFYnFoOl.d.ts → kv-gJ8LYbxX.d.cts} +1 -3
- package/dist/ld-JbCByP5P.mjs +626 -0
- package/dist/metrics-ColY3QKj.mjs +815 -0
- package/dist/{middleware-zjZ6AFCW.mjs → middleware-Buqzuiaf.mjs} +1 -1
- package/dist/{middleware-An4DjES4.cjs → middleware-CWQ3Yi07.cjs} +2318 -623
- package/dist/{middleware-K1g6bEkV.mjs → middleware-Cg4EyWsq.mjs} +1639 -373
- package/dist/{middleware-nidH7VZH.js → middleware-iAJY2aU-.js} +2260 -556
- package/dist/{mod-CR8soWa9.d.ts → mod-B0hW12_O.d.cts} +26 -4
- package/dist/mod-C0F6kvgS.d.cts +182 -0
- package/dist/{mod-Cr3f-ACa.d.cts → mod-COIAjwRS.d.ts} +26 -2
- package/dist/{mod-CMEbIaNh.d.cts → mod-DFvNJcNb.d.ts} +56 -4
- package/dist/mod-vPYVoa5n.d.ts +182 -0
- package/dist/{mod-CLgIXe9w.d.ts → mod-yvIXFAEi.d.cts} +56 -6
- package/dist/mod.cjs +9 -6
- package/dist/mod.d.cts +12 -10
- package/dist/mod.d.ts +12 -12
- package/dist/mod.js +11 -11
- package/dist/mq-D-nlpY04.d.ts +208 -0
- package/dist/mq-D8uSFzxe.d.cts +208 -0
- package/dist/{negotiation-SQvQgUqe.mjs → negotiation-DDstyBvc.mjs} +29 -0
- package/dist/nodeinfo/client.test.mjs +4 -4
- package/dist/nodeinfo/handler.test.mjs +4 -4
- package/dist/nodeinfo/mod.d.cts +2 -1
- package/dist/nodeinfo/mod.d.ts +2 -3
- package/dist/nodeinfo/mod.js +3 -3
- package/dist/nodeinfo/types.test.mjs +3 -3
- package/dist/otel/exporter.test.mjs +28 -25
- package/dist/otel/mod.cjs +6 -5
- package/dist/otel/mod.d.cts +5 -3
- package/dist/otel/mod.d.ts +5 -5
- package/dist/otel/mod.js +8 -7
- package/dist/{outgoing-jsonld-CNmZLixq.mjs → outgoing-jsonld-L_DbOaFe.mjs} +2 -2
- package/dist/{owner-C-tqfvxn.mjs → owner-B4LgOmzV.mjs} +2 -2
- package/dist/{owner-CptqhsOy.d.cts → owner-CnngXDNJ.d.ts} +2 -1
- package/dist/{owner-74ARJ5TL.d.ts → owner-DEvZuyOE.d.cts} +2 -3
- package/dist/{proof-BlmRPzrK.mjs → proof-BoSGOq9q.mjs} +26 -8
- package/dist/{proof-Bbbwf8_x.js → proof-CzhZuawt.js} +378 -15
- package/dist/{proof-BW89QMVB.cjs → proof-DvMI-3vq.cjs} +432 -15
- package/dist/runtime/mod.d.cts +1 -0
- package/dist/runtime/mod.d.ts +1 -2
- package/dist/runtime/mod.js +3 -3
- package/dist/{send-05pJLcb6.mjs → send-ByK1Q7DK.mjs} +72 -26
- package/dist/sig/http.test.mjs +356 -33
- package/dist/sig/key.test.mjs +103 -6
- package/dist/sig/ld.test.mjs +696 -7
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.d.cts +4 -3
- package/dist/sig/mod.d.ts +4 -5
- package/dist/sig/mod.js +4 -4
- package/dist/sig/owner.test.mjs +6 -6
- package/dist/sig/proof.test.mjs +169 -8
- package/dist/{std__assert-CRDpx_HF.mjs → std__assert-BBjXFNOb.mjs} +5 -30
- package/dist/temporal-LpjyAjKb.mjs +95 -0
- package/dist/testing/mod.d.mts +110 -10
- package/dist/testing/mod.mjs +1 -2
- package/dist/{transformers-ve6e2xcg.js → transformers-BGMIq1cs.js} +2 -2
- package/dist/{types-hvL8ElAs.js → types-CAY3OdLq.js} +2 -2
- package/dist/utils/docloader.test.mjs +6 -6
- package/dist/utils/kv-cache.test.mjs +67 -2
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +2 -1
- package/dist/utils/mod.d.ts +2 -3
- package/dist/utils/mod.js +3 -3
- package/dist/vocab/cjs.test.mjs +1 -1
- package/dist/vocab/mod.d.cts +1 -0
- package/dist/vocab/mod.d.ts +1 -2
- package/dist/vocab/mod.js +2 -2
- package/package.json +16 -16
- package/dist/ld-DR78beiv.mjs +0 -279
- package/dist/middleware-BFBaR0mF.cjs +0 -4
- package/dist/mod-2d12ffz3.d.ts +0 -64
- package/dist/mod-D35TRn09.d.cts +0 -62
- package/dist/router-CrMLXoOr.mjs +0 -114
- package/dist/{activity-listener-ell7W1s9.mjs → activity-listener-tztVvlNb.mjs} +0 -0
- package/dist/{assert_equals-Ew3jOFa3.mjs → assert_equals-C-ZRDbaf.mjs} +0 -0
- package/dist/{client-D_1QpnWt.mjs → client-ByXmQhYD.mjs} +1 -1
- package/dist/{collection-D-HqUuA2.mjs → collection-Cc3DVAhE.mjs} +0 -0
- package/dist/{keycache-EGATflN-.mjs → keycache-BeU0LCII.mjs} +0 -0
- package/dist/{public-audience-DYFHzm_c.mjs → public-audience-Cvbr2Gzt.mjs} +1 -1
- /package/dist/{retry-bMXBL97A.mjs → retry-CXg_MBI-.mjs} +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
|
-
import "urlpattern-polyfill";
|
|
2
|
+
import { URLPattern } from "urlpattern-polyfill";
|
|
3
3
|
import { getLogger } from "@logtape/logtape";
|
|
4
4
|
import { CryptographicKey, Object as Object$1, isActor } from "@fedify/vocab";
|
|
5
|
-
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
5
|
+
import { SpanKind, SpanStatusCode, metrics, trace } from "@opentelemetry/api";
|
|
6
6
|
import { encodeHex } from "byte-encodings/hex";
|
|
7
7
|
import { Item, decodeDict, encodeDict, encodeItem } from "structured-field-values";
|
|
8
8
|
import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
|
|
@@ -10,7 +10,7 @@ import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_URL_FULL } fro
|
|
|
10
10
|
import { decodeBase64, encodeBase64 } from "byte-encodings/base64";
|
|
11
11
|
//#region deno.json
|
|
12
12
|
var name = "@fedify/fedify";
|
|
13
|
-
var version = "2.3.0-
|
|
13
|
+
var version = "2.3.0-pr.809.37+0574ba5c";
|
|
14
14
|
//#endregion
|
|
15
15
|
//#region src/sig/accept.ts
|
|
16
16
|
/**
|
|
@@ -151,6 +151,814 @@ function fulfillAcceptSignature(entry, localKeyId, localAlg) {
|
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
//#endregion
|
|
154
|
+
//#region src/federation/metrics.ts
|
|
155
|
+
var FederationMetrics = class {
|
|
156
|
+
deliverySent;
|
|
157
|
+
deliveryPermanentFailure;
|
|
158
|
+
signatureVerificationFailure;
|
|
159
|
+
signatureVerificationDuration;
|
|
160
|
+
signatureKeyFetchDuration;
|
|
161
|
+
deliveryDuration;
|
|
162
|
+
inboxProcessingDuration;
|
|
163
|
+
httpServerRequestCount;
|
|
164
|
+
httpServerRequestDuration;
|
|
165
|
+
queueTaskEnqueued;
|
|
166
|
+
queueTaskStarted;
|
|
167
|
+
queueTaskCompleted;
|
|
168
|
+
queueTaskFailed;
|
|
169
|
+
queueTaskDuration;
|
|
170
|
+
queueTaskInFlight;
|
|
171
|
+
queueDepth;
|
|
172
|
+
fanoutRecipients;
|
|
173
|
+
inboxActivity;
|
|
174
|
+
outboxActivity;
|
|
175
|
+
circuitBreakerStateChange;
|
|
176
|
+
keyLookup;
|
|
177
|
+
keyLookupDuration;
|
|
178
|
+
documentFetch;
|
|
179
|
+
documentFetchDuration;
|
|
180
|
+
documentCache;
|
|
181
|
+
webFingerHandle;
|
|
182
|
+
webFingerHandleDuration;
|
|
183
|
+
collectionRequest;
|
|
184
|
+
collectionDispatchDuration;
|
|
185
|
+
collectionPageItems;
|
|
186
|
+
collectionTotalItems;
|
|
187
|
+
constructor(meterProvider) {
|
|
188
|
+
const meter = meterProvider.getMeter(name, version);
|
|
189
|
+
this.deliverySent = meter.createCounter("activitypub.delivery.sent", {
|
|
190
|
+
description: "ActivityPub delivery attempts.",
|
|
191
|
+
unit: "{attempt}"
|
|
192
|
+
});
|
|
193
|
+
this.deliveryPermanentFailure = meter.createCounter("activitypub.delivery.permanent_failure", {
|
|
194
|
+
description: "ActivityPub deliveries abandoned as permanent failures.",
|
|
195
|
+
unit: "{failure}"
|
|
196
|
+
});
|
|
197
|
+
this.signatureVerificationFailure = meter.createCounter("activitypub.signature.verification_failure", {
|
|
198
|
+
description: "ActivityPub signature verification failures.",
|
|
199
|
+
unit: "{failure}"
|
|
200
|
+
});
|
|
201
|
+
this.signatureVerificationDuration = meter.createHistogram("activitypub.signature.verification.duration", {
|
|
202
|
+
description: "Duration of ActivityPub signature verification, including local key lookup and remote key fetches.",
|
|
203
|
+
unit: "ms",
|
|
204
|
+
advice: { explicitBucketBoundaries: [
|
|
205
|
+
.1,
|
|
206
|
+
.25,
|
|
207
|
+
.5,
|
|
208
|
+
1,
|
|
209
|
+
2.5,
|
|
210
|
+
5,
|
|
211
|
+
10,
|
|
212
|
+
25,
|
|
213
|
+
50,
|
|
214
|
+
100,
|
|
215
|
+
250,
|
|
216
|
+
500,
|
|
217
|
+
1e3
|
|
218
|
+
] }
|
|
219
|
+
});
|
|
220
|
+
this.signatureKeyFetchDuration = meter.createHistogram("activitypub.signature.key_fetch.duration", {
|
|
221
|
+
description: "Duration of public key lookup performed during ActivityPub signature verification.",
|
|
222
|
+
unit: "ms"
|
|
223
|
+
});
|
|
224
|
+
this.deliveryDuration = meter.createHistogram("activitypub.delivery.duration", {
|
|
225
|
+
description: "Duration of ActivityPub delivery attempts.",
|
|
226
|
+
unit: "ms"
|
|
227
|
+
});
|
|
228
|
+
this.inboxProcessingDuration = meter.createHistogram("activitypub.inbox.processing_duration", {
|
|
229
|
+
description: "Duration of ActivityPub inbox listener processing.",
|
|
230
|
+
unit: "ms"
|
|
231
|
+
});
|
|
232
|
+
this.httpServerRequestCount = meter.createCounter("fedify.http.server.request.count", {
|
|
233
|
+
description: "HTTP requests handled by Federation.fetch().",
|
|
234
|
+
unit: "{request}"
|
|
235
|
+
});
|
|
236
|
+
this.httpServerRequestDuration = meter.createHistogram("fedify.http.server.request.duration", {
|
|
237
|
+
description: "Duration of HTTP requests handled by Federation.fetch().",
|
|
238
|
+
unit: "ms",
|
|
239
|
+
advice: { explicitBucketBoundaries: [
|
|
240
|
+
5,
|
|
241
|
+
10,
|
|
242
|
+
25,
|
|
243
|
+
50,
|
|
244
|
+
75,
|
|
245
|
+
100,
|
|
246
|
+
250,
|
|
247
|
+
500,
|
|
248
|
+
750,
|
|
249
|
+
1e3,
|
|
250
|
+
2500,
|
|
251
|
+
5e3,
|
|
252
|
+
7500,
|
|
253
|
+
1e4
|
|
254
|
+
] }
|
|
255
|
+
});
|
|
256
|
+
this.queueTaskEnqueued = meter.createCounter("fedify.queue.task.enqueued", {
|
|
257
|
+
description: "Tasks Fedify enqueued for inbox, outbox, or fanout work.",
|
|
258
|
+
unit: "{task}"
|
|
259
|
+
});
|
|
260
|
+
this.queueTaskStarted = meter.createCounter("fedify.queue.task.started", {
|
|
261
|
+
description: "Tasks Fedify began processing as a queue worker.",
|
|
262
|
+
unit: "{task}"
|
|
263
|
+
});
|
|
264
|
+
this.queueTaskCompleted = meter.createCounter("fedify.queue.task.completed", {
|
|
265
|
+
description: "Queue tasks Fedify finished processing without throwing.",
|
|
266
|
+
unit: "{task}"
|
|
267
|
+
});
|
|
268
|
+
this.queueTaskFailed = meter.createCounter("fedify.queue.task.failed", {
|
|
269
|
+
description: "Queue tasks Fedify abandoned because processing threw.",
|
|
270
|
+
unit: "{task}"
|
|
271
|
+
});
|
|
272
|
+
this.queueTaskDuration = meter.createHistogram("fedify.queue.task.duration", {
|
|
273
|
+
description: "Duration of queue task processing in Fedify workers.",
|
|
274
|
+
unit: "ms",
|
|
275
|
+
advice: { explicitBucketBoundaries: [
|
|
276
|
+
5,
|
|
277
|
+
10,
|
|
278
|
+
25,
|
|
279
|
+
50,
|
|
280
|
+
75,
|
|
281
|
+
100,
|
|
282
|
+
250,
|
|
283
|
+
500,
|
|
284
|
+
750,
|
|
285
|
+
1e3,
|
|
286
|
+
2500,
|
|
287
|
+
5e3,
|
|
288
|
+
7500,
|
|
289
|
+
1e4
|
|
290
|
+
] }
|
|
291
|
+
});
|
|
292
|
+
this.queueTaskInFlight = meter.createUpDownCounter("fedify.queue.task.in_flight", {
|
|
293
|
+
description: "Queue tasks currently being processed in this Fedify process.",
|
|
294
|
+
unit: "{task}"
|
|
295
|
+
});
|
|
296
|
+
this.queueDepth = meter.createObservableGauge("fedify.queue.depth", {
|
|
297
|
+
description: "Messages waiting in configured Fedify queues, as reported by the queue backend.",
|
|
298
|
+
unit: "{message}"
|
|
299
|
+
});
|
|
300
|
+
this.fanoutRecipients = meter.createHistogram("activitypub.fanout.recipients", {
|
|
301
|
+
description: "Number of recipient inboxes produced by an ActivityPub fanout task.",
|
|
302
|
+
unit: "{recipient}"
|
|
303
|
+
});
|
|
304
|
+
this.inboxActivity = meter.createCounter("activitypub.inbox.activity", {
|
|
305
|
+
description: "ActivityPub activities observed at the inbox lifecycle level: queued, processed, retried, rejected, or abandoned.",
|
|
306
|
+
unit: "{activity}"
|
|
307
|
+
});
|
|
308
|
+
this.outboxActivity = meter.createCounter("activitypub.outbox.activity", {
|
|
309
|
+
description: "ActivityPub activities observed at the outbox lifecycle level: queued, retried, or abandoned. Per-recipient delivery counters live on `activitypub.delivery.*`.",
|
|
310
|
+
unit: "{activity}"
|
|
311
|
+
});
|
|
312
|
+
this.circuitBreakerStateChange = meter.createCounter("activitypub.circuit_breaker.state_change", {
|
|
313
|
+
description: "Outbound ActivityPub delivery circuit breaker changes.",
|
|
314
|
+
unit: "{change}"
|
|
315
|
+
});
|
|
316
|
+
this.keyLookup = meter.createCounter("activitypub.key.lookup", {
|
|
317
|
+
description: "Public-key lookup attempts performed by Fedify, including both cache hits and remote fetches.",
|
|
318
|
+
unit: "{lookup}"
|
|
319
|
+
});
|
|
320
|
+
this.keyLookupDuration = meter.createHistogram("activitypub.key.lookup.duration", {
|
|
321
|
+
description: "Duration of public-key lookups performed by Fedify, including any remote fetch.",
|
|
322
|
+
unit: "ms",
|
|
323
|
+
advice: { explicitBucketBoundaries: [
|
|
324
|
+
5,
|
|
325
|
+
10,
|
|
326
|
+
25,
|
|
327
|
+
50,
|
|
328
|
+
75,
|
|
329
|
+
100,
|
|
330
|
+
250,
|
|
331
|
+
500,
|
|
332
|
+
750,
|
|
333
|
+
1e3,
|
|
334
|
+
2500,
|
|
335
|
+
5e3,
|
|
336
|
+
7500,
|
|
337
|
+
1e4
|
|
338
|
+
] }
|
|
339
|
+
});
|
|
340
|
+
this.documentFetch = meter.createCounter("activitypub.document.fetch", {
|
|
341
|
+
description: "Remote JSON-LD document loader invocations made by Fedify-wrapped loaders.",
|
|
342
|
+
unit: "{fetch}"
|
|
343
|
+
});
|
|
344
|
+
this.documentFetchDuration = meter.createHistogram("activitypub.document.fetch.duration", {
|
|
345
|
+
description: "Duration of remote JSON-LD document loader invocations made by Fedify-wrapped loaders.",
|
|
346
|
+
unit: "ms",
|
|
347
|
+
advice: { explicitBucketBoundaries: [
|
|
348
|
+
5,
|
|
349
|
+
10,
|
|
350
|
+
25,
|
|
351
|
+
50,
|
|
352
|
+
75,
|
|
353
|
+
100,
|
|
354
|
+
250,
|
|
355
|
+
500,
|
|
356
|
+
750,
|
|
357
|
+
1e3,
|
|
358
|
+
2500,
|
|
359
|
+
5e3,
|
|
360
|
+
7500,
|
|
361
|
+
1e4
|
|
362
|
+
] }
|
|
363
|
+
});
|
|
364
|
+
this.documentCache = meter.createCounter("activitypub.document.cache", {
|
|
365
|
+
description: "KV-backed document loader cache lookups, with `hit` or `miss` classification.",
|
|
366
|
+
unit: "{lookup}"
|
|
367
|
+
});
|
|
368
|
+
this.webFingerHandle = meter.createCounter("webfinger.handle", {
|
|
369
|
+
description: "Incoming WebFinger requests handled by Fedify, classified by terminal outcome.",
|
|
370
|
+
unit: "{request}"
|
|
371
|
+
});
|
|
372
|
+
this.webFingerHandleDuration = meter.createHistogram("webfinger.handle.duration", {
|
|
373
|
+
description: "Duration of incoming WebFinger request handling in Fedify.",
|
|
374
|
+
unit: "ms",
|
|
375
|
+
advice: { explicitBucketBoundaries: [
|
|
376
|
+
5,
|
|
377
|
+
10,
|
|
378
|
+
25,
|
|
379
|
+
50,
|
|
380
|
+
75,
|
|
381
|
+
100,
|
|
382
|
+
250,
|
|
383
|
+
500,
|
|
384
|
+
750,
|
|
385
|
+
1e3,
|
|
386
|
+
2500,
|
|
387
|
+
5e3,
|
|
388
|
+
7500,
|
|
389
|
+
1e4
|
|
390
|
+
] }
|
|
391
|
+
});
|
|
392
|
+
this.collectionRequest = meter.createCounter("activitypub.collection.request", {
|
|
393
|
+
description: "ActivityPub collection and collection-page requests handled by Fedify.",
|
|
394
|
+
unit: "{request}"
|
|
395
|
+
});
|
|
396
|
+
this.collectionDispatchDuration = meter.createHistogram("activitypub.collection.dispatch.duration", {
|
|
397
|
+
description: "Duration of ActivityPub collection dispatcher callbacks.",
|
|
398
|
+
unit: "ms",
|
|
399
|
+
advice: { explicitBucketBoundaries: [
|
|
400
|
+
5,
|
|
401
|
+
10,
|
|
402
|
+
25,
|
|
403
|
+
50,
|
|
404
|
+
75,
|
|
405
|
+
100,
|
|
406
|
+
250,
|
|
407
|
+
500,
|
|
408
|
+
750,
|
|
409
|
+
1e3,
|
|
410
|
+
2500,
|
|
411
|
+
5e3,
|
|
412
|
+
7500,
|
|
413
|
+
1e4
|
|
414
|
+
] }
|
|
415
|
+
});
|
|
416
|
+
this.collectionPageItems = meter.createHistogram("activitypub.collection.page.items", {
|
|
417
|
+
description: "Number of items Fedify materialized for an ActivityPub collection response.",
|
|
418
|
+
unit: "{item}"
|
|
419
|
+
});
|
|
420
|
+
this.collectionTotalItems = meter.createHistogram("activitypub.collection.total_items", {
|
|
421
|
+
description: "Total item count reported by ActivityPub collection counters.",
|
|
422
|
+
unit: "{item}"
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
recordDelivery(inbox, durationMs, success, activityType) {
|
|
426
|
+
const deliveryAttributes = {
|
|
427
|
+
"activitypub.remote.host": getRemoteHost(inbox),
|
|
428
|
+
"activitypub.delivery.success": success
|
|
429
|
+
};
|
|
430
|
+
if (activityType != null) deliveryAttributes["activitypub.activity.type"] = activityType;
|
|
431
|
+
this.deliverySent.add(1, deliveryAttributes);
|
|
432
|
+
this.deliveryDuration.record(durationMs, deliveryAttributes);
|
|
433
|
+
}
|
|
434
|
+
recordPermanentFailure(inbox, statusCode) {
|
|
435
|
+
this.deliveryPermanentFailure.add(1, {
|
|
436
|
+
"activitypub.remote.host": getRemoteHost(inbox),
|
|
437
|
+
"http.response.status_code": statusCode
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
recordSignatureVerificationFailure(reason, remoteHost) {
|
|
441
|
+
const attributes = { "activitypub.verification.failure_reason": reason };
|
|
442
|
+
if (remoteHost != null) attributes["activitypub.remote.host"] = remoteHost;
|
|
443
|
+
this.signatureVerificationFailure.add(1, attributes);
|
|
444
|
+
}
|
|
445
|
+
recordSignatureVerificationDuration(durationMs, kind, result, extra = {}) {
|
|
446
|
+
const attributes = {
|
|
447
|
+
"activitypub.signature.kind": kind,
|
|
448
|
+
"activitypub.signature.result": result
|
|
449
|
+
};
|
|
450
|
+
if (extra.algorithm != null) attributes["http_signatures.algorithm"] = extra.algorithm;
|
|
451
|
+
if (extra.failureReason != null) attributes["http_signatures.failure_reason"] = extra.failureReason;
|
|
452
|
+
if (extra.ldType != null) attributes["ld_signatures.type"] = extra.ldType;
|
|
453
|
+
if (extra.cryptosuite != null) attributes["object_integrity_proofs.cryptosuite"] = extra.cryptosuite;
|
|
454
|
+
this.signatureVerificationDuration.record(durationMs, attributes);
|
|
455
|
+
}
|
|
456
|
+
recordSignatureKeyFetchDuration(durationMs, kind, result) {
|
|
457
|
+
this.signatureKeyFetchDuration.record(durationMs, {
|
|
458
|
+
"activitypub.signature.kind": kind,
|
|
459
|
+
"activitypub.signature.key_fetch.result": result
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
recordInboxProcessingDuration(activityType, durationMs) {
|
|
463
|
+
this.inboxProcessingDuration.record(durationMs, { "activitypub.activity.type": activityType });
|
|
464
|
+
}
|
|
465
|
+
recordHttpServerRequest(method, endpoint, durationMs, options = {}) {
|
|
466
|
+
const attributes = {
|
|
467
|
+
"http.request.method": normalizeHttpMethod(method),
|
|
468
|
+
"fedify.endpoint": endpoint
|
|
469
|
+
};
|
|
470
|
+
if (options.statusCode != null) attributes["http.response.status_code"] = options.statusCode;
|
|
471
|
+
if (options.routeTemplate != null) attributes["fedify.route.template"] = options.routeTemplate;
|
|
472
|
+
this.httpServerRequestCount.add(1, attributes);
|
|
473
|
+
this.httpServerRequestDuration.record(durationMs, attributes);
|
|
474
|
+
}
|
|
475
|
+
recordQueueTaskEnqueued(common, attempt) {
|
|
476
|
+
const attributes = buildQueueTaskAttributes(common);
|
|
477
|
+
attributes["fedify.queue.task.attempt"] = attempt;
|
|
478
|
+
this.queueTaskEnqueued.add(1, attributes);
|
|
479
|
+
}
|
|
480
|
+
recordQueueTaskStarted(common) {
|
|
481
|
+
this.queueTaskStarted.add(1, buildQueueTaskAttributes(common));
|
|
482
|
+
}
|
|
483
|
+
incrementQueueTaskInFlight(common) {
|
|
484
|
+
this.queueTaskInFlight.add(1, buildQueueTaskInFlightAttributes(common));
|
|
485
|
+
}
|
|
486
|
+
decrementQueueTaskInFlight(common) {
|
|
487
|
+
this.queueTaskInFlight.add(-1, buildQueueTaskInFlightAttributes(common));
|
|
488
|
+
}
|
|
489
|
+
recordQueueTaskOutcome(common, result, durationMs) {
|
|
490
|
+
const attributes = buildQueueTaskAttributes(common);
|
|
491
|
+
attributes["fedify.queue.task.result"] = result;
|
|
492
|
+
if (result === "completed") this.queueTaskCompleted.add(1, attributes);
|
|
493
|
+
else if (result === "failed") this.queueTaskFailed.add(1, attributes);
|
|
494
|
+
this.queueTaskDuration.record(durationMs, attributes);
|
|
495
|
+
}
|
|
496
|
+
recordFanoutRecipients(recipientCount, activityType) {
|
|
497
|
+
const attributes = {};
|
|
498
|
+
if (activityType != null) attributes["activitypub.activity.type"] = activityType;
|
|
499
|
+
this.fanoutRecipients.record(recipientCount, attributes);
|
|
500
|
+
}
|
|
501
|
+
recordInboxActivity(result, activityType) {
|
|
502
|
+
this.inboxActivity.add(1, buildActivityLifecycleAttributes(result, activityType));
|
|
503
|
+
}
|
|
504
|
+
recordOutboxActivity(result, activityType) {
|
|
505
|
+
this.outboxActivity.add(1, buildActivityLifecycleAttributes(result, activityType));
|
|
506
|
+
}
|
|
507
|
+
recordCircuitBreakerStateChange(remoteHost, state) {
|
|
508
|
+
this.circuitBreakerStateChange.add(1, {
|
|
509
|
+
"activitypub.remote.host": remoteHost,
|
|
510
|
+
"activitypub.circuit_breaker.state": state
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
recordKeyLookup(attrs) {
|
|
514
|
+
const attributes = {
|
|
515
|
+
"activitypub.lookup.kind": "public_key",
|
|
516
|
+
"activitypub.lookup.result": attrs.result,
|
|
517
|
+
"activitypub.cache.enabled": attrs.cacheEnabled
|
|
518
|
+
};
|
|
519
|
+
if (attrs.remoteUrl != null) attributes["activitypub.remote.host"] = getRemoteHost(attrs.remoteUrl);
|
|
520
|
+
if (attrs.statusCode != null) attributes["http.response.status_code"] = attrs.statusCode;
|
|
521
|
+
this.keyLookup.add(1, attributes);
|
|
522
|
+
this.keyLookupDuration.record(attrs.durationMs, attributes);
|
|
523
|
+
}
|
|
524
|
+
recordDocumentFetch(attrs) {
|
|
525
|
+
const attributes = {
|
|
526
|
+
"activitypub.lookup.kind": attrs.kind,
|
|
527
|
+
"activitypub.lookup.result": attrs.result
|
|
528
|
+
};
|
|
529
|
+
if (attrs.remoteUrl != null) attributes["activitypub.remote.host"] = getRemoteHost(attrs.remoteUrl);
|
|
530
|
+
if (attrs.cacheEnabled != null) attributes["activitypub.cache.enabled"] = attrs.cacheEnabled;
|
|
531
|
+
if (attrs.statusCode != null) attributes["http.response.status_code"] = attrs.statusCode;
|
|
532
|
+
this.documentFetch.add(1, attributes);
|
|
533
|
+
this.documentFetchDuration.record(attrs.durationMs, attributes);
|
|
534
|
+
}
|
|
535
|
+
recordDocumentCache(attrs) {
|
|
536
|
+
const attributes = {
|
|
537
|
+
"activitypub.lookup.kind": attrs.kind,
|
|
538
|
+
"activitypub.lookup.result": attrs.result
|
|
539
|
+
};
|
|
540
|
+
if (attrs.remoteUrl != null) attributes["activitypub.remote.host"] = getRemoteHost(attrs.remoteUrl);
|
|
541
|
+
this.documentCache.add(1, attributes);
|
|
542
|
+
}
|
|
543
|
+
recordWebFingerHandle(attrs) {
|
|
544
|
+
const attributes = { "webfinger.handle.result": attrs.result };
|
|
545
|
+
if (attrs.scheme != null) attributes["webfinger.resource.scheme"] = attrs.scheme;
|
|
546
|
+
if (attrs.statusCode != null) attributes["http.response.status_code"] = attrs.statusCode;
|
|
547
|
+
this.webFingerHandle.add(1, attributes);
|
|
548
|
+
this.webFingerHandleDuration.record(attrs.durationMs, attributes);
|
|
549
|
+
}
|
|
550
|
+
recordCollectionRequest(attrs) {
|
|
551
|
+
this.collectionRequest.add(1, buildCollectionAttributes(attrs));
|
|
552
|
+
}
|
|
553
|
+
recordCollectionDispatchDuration(durationMs, attrs) {
|
|
554
|
+
this.collectionDispatchDuration.record(durationMs, buildCollectionAttributes(attrs));
|
|
555
|
+
}
|
|
556
|
+
recordCollectionPageItems(itemCount, attrs) {
|
|
557
|
+
this.collectionPageItems.record(itemCount, buildCollectionAttributes(attrs));
|
|
558
|
+
}
|
|
559
|
+
recordCollectionTotalItems(totalItems, attrs) {
|
|
560
|
+
this.collectionTotalItems.record(totalItems, buildCollectionAttributes(attrs));
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
function buildCollectionAttributes(attrs) {
|
|
564
|
+
const attributes = {
|
|
565
|
+
"activitypub.collection.kind": attrs.kind,
|
|
566
|
+
"activitypub.collection.page": attrs.page,
|
|
567
|
+
"activitypub.collection.result": attrs.result,
|
|
568
|
+
"fedify.collection.dispatcher": attrs.dispatcher
|
|
569
|
+
};
|
|
570
|
+
if (attrs.statusCode != null) attributes["http.response.status_code"] = attrs.statusCode;
|
|
571
|
+
return attributes;
|
|
572
|
+
}
|
|
573
|
+
function buildActivityLifecycleAttributes(result, activityType) {
|
|
574
|
+
const attributes = { "activitypub.processing.result": result };
|
|
575
|
+
if (activityType != null) attributes["activitypub.activity.type"] = activityType;
|
|
576
|
+
return attributes;
|
|
577
|
+
}
|
|
578
|
+
function buildQueueTaskAttributes(common) {
|
|
579
|
+
const attributes = { "fedify.queue.role": common.role };
|
|
580
|
+
const backend = getQueueBackend(common.queue);
|
|
581
|
+
if (backend != null) attributes["fedify.queue.backend"] = backend;
|
|
582
|
+
const nativeRetrial = common.queue?.nativeRetrial;
|
|
583
|
+
if (typeof nativeRetrial === "boolean") attributes["fedify.queue.native_retrial"] = nativeRetrial;
|
|
584
|
+
if (common.activityType != null) attributes["activitypub.activity.type"] = common.activityType;
|
|
585
|
+
return attributes;
|
|
586
|
+
}
|
|
587
|
+
function buildQueueTaskInFlightAttributes(common) {
|
|
588
|
+
return buildQueueTaskAttributes({
|
|
589
|
+
role: common.role,
|
|
590
|
+
queue: common.queue
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Returns the constructor name of the given message queue, when it is a
|
|
595
|
+
* meaningful identifier. Used as a best-effort `fedify.queue.backend`
|
|
596
|
+
* attribute on queue task metrics; returns `undefined` for plain object
|
|
597
|
+
* literals (whose constructor is `Object`) so the attribute does not appear
|
|
598
|
+
* with a non-informative value.
|
|
599
|
+
* @since 2.3.0
|
|
600
|
+
*/
|
|
601
|
+
function getQueueBackend(queue) {
|
|
602
|
+
const name = queue?.constructor?.name;
|
|
603
|
+
if (name == null || name === "" || name === "Object") return void 0;
|
|
604
|
+
return name;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Registers a callback for observing queue backend depth.
|
|
608
|
+
* @since 2.3.0
|
|
609
|
+
*/
|
|
610
|
+
function registerQueueDepthGauge(meterProvider, entries, options = {}) {
|
|
611
|
+
const uniqueQueues = /* @__PURE__ */ new Map();
|
|
612
|
+
for (const { role, queue } of entries) {
|
|
613
|
+
if (queue?.getDepth == null) continue;
|
|
614
|
+
const roles = uniqueQueues.get(queue);
|
|
615
|
+
if (roles == null) uniqueQueues.set(queue, [role]);
|
|
616
|
+
else if (!roles.includes(role)) roles.push(role);
|
|
617
|
+
}
|
|
618
|
+
if (uniqueQueues.size < 1) return;
|
|
619
|
+
const queueEntries = Array.from(uniqueQueues.entries());
|
|
620
|
+
getFederationMetrics(meterProvider).queueDepth.addCallback(async (observableResult) => {
|
|
621
|
+
await Promise.all(queueEntries.map(async ([queue, roles]) => {
|
|
622
|
+
let depth;
|
|
623
|
+
try {
|
|
624
|
+
depth = await queue.getDepth();
|
|
625
|
+
} catch {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (depth == null) return;
|
|
629
|
+
const attributes = buildQueueDepthAttributes(queue, roles, options);
|
|
630
|
+
observableResult.observe(depth.queued, {
|
|
631
|
+
...attributes,
|
|
632
|
+
"fedify.queue.depth.state": "queued"
|
|
633
|
+
});
|
|
634
|
+
if (depth.ready != null) observableResult.observe(depth.ready, {
|
|
635
|
+
...attributes,
|
|
636
|
+
"fedify.queue.depth.state": "ready"
|
|
637
|
+
});
|
|
638
|
+
if (depth.delayed != null) observableResult.observe(depth.delayed, {
|
|
639
|
+
...attributes,
|
|
640
|
+
"fedify.queue.depth.state": "delayed"
|
|
641
|
+
});
|
|
642
|
+
}));
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
function buildQueueDepthAttributes(queue, roles, options) {
|
|
646
|
+
const sortedRoles = roles.toSorted();
|
|
647
|
+
const role = sortedRoles.length === 1 ? sortedRoles[0] : "shared";
|
|
648
|
+
const attributes = { "fedify.queue.role": role };
|
|
649
|
+
if (options.sourceId != null) attributes["fedify.federation.instance_id"] = options.sourceId;
|
|
650
|
+
if (role === "shared") attributes["fedify.queue.roles"] = sortedRoles.join(",");
|
|
651
|
+
const backend = getQueueBackend(queue);
|
|
652
|
+
if (backend != null) attributes["fedify.queue.backend"] = backend;
|
|
653
|
+
const nativeRetrial = queue.nativeRetrial;
|
|
654
|
+
if (typeof nativeRetrial === "boolean") attributes["fedify.queue.native_retrial"] = nativeRetrial;
|
|
655
|
+
return attributes;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Records `fedify.queue.task.enqueued` for an outgoing outbox enqueue and,
|
|
659
|
+
* for the initial attempt, also records
|
|
660
|
+
* `activitypub.outbox.activity{queued}`.
|
|
661
|
+
*
|
|
662
|
+
* Both `Context.sendActivity()` and `OutboxContext.forwardActivity()` enqueue
|
|
663
|
+
* outbox messages with the same metric attributes (role, queue, activity
|
|
664
|
+
* type, attempt), so they share this helper rather than each defining a local
|
|
665
|
+
* closure. Retry enqueues (attempt > 0) intentionally do not record a
|
|
666
|
+
* second `activitypub.outbox.activity{queued}`; retries are reported as
|
|
667
|
+
* `result=retried` from the retry-scheduling site, which has the failure
|
|
668
|
+
* context.
|
|
669
|
+
* @since 2.3.0
|
|
670
|
+
*/
|
|
671
|
+
function recordOutboxEnqueue(meterProvider, outboxQueue, message) {
|
|
672
|
+
const metrics = getFederationMetrics(meterProvider);
|
|
673
|
+
metrics.recordQueueTaskEnqueued({
|
|
674
|
+
role: "outbox",
|
|
675
|
+
queue: outboxQueue,
|
|
676
|
+
activityType: message.activityType
|
|
677
|
+
}, message.attempt);
|
|
678
|
+
if (message.attempt === 0) metrics.recordOutboxActivity("queued", message.activityType);
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Records `activitypub.fanout.recipients` with the number of recipient
|
|
682
|
+
* inboxes a single fanout produced. The histogram is unitless count
|
|
683
|
+
* (one measurement per fanout enqueue). Recipient URLs are deliberately
|
|
684
|
+
* not recorded; only the activity type, when known.
|
|
685
|
+
* @since 2.3.0
|
|
686
|
+
*/
|
|
687
|
+
function recordFanoutRecipients(meterProvider, recipientCount, activityType) {
|
|
688
|
+
getFederationMetrics(meterProvider).recordFanoutRecipients(recipientCount, activityType);
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Records one `activitypub.inbox.activity` measurement. The
|
|
692
|
+
* `activitypub.processing.result` attribute is always present;
|
|
693
|
+
* `activitypub.activity.type` is recorded only when Fedify already knows
|
|
694
|
+
* the activity type.
|
|
695
|
+
* @since 2.3.0
|
|
696
|
+
*/
|
|
697
|
+
function recordInboxActivity(meterProvider, result, activityType) {
|
|
698
|
+
getFederationMetrics(meterProvider).recordInboxActivity(result, activityType);
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Records one `activitypub.outbox.activity` measurement. The
|
|
702
|
+
* `activitypub.processing.result` attribute is always present;
|
|
703
|
+
* `activitypub.activity.type` is recorded only when Fedify already knows
|
|
704
|
+
* the activity type (it is always known for outbox lifecycle events).
|
|
705
|
+
* @since 2.3.0
|
|
706
|
+
*/
|
|
707
|
+
function recordOutboxActivity(meterProvider, result, activityType) {
|
|
708
|
+
getFederationMetrics(meterProvider).recordOutboxActivity(result, activityType);
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Records one outbound delivery circuit breaker state transition.
|
|
712
|
+
* @since 2.3.0
|
|
713
|
+
*/
|
|
714
|
+
function recordCircuitBreakerStateChange(meterProvider, remoteHost, state) {
|
|
715
|
+
getFederationMetrics(meterProvider).recordCircuitBreakerStateChange(remoteHost, state);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Records one measurement on `activitypub.key.lookup` (counter) and
|
|
719
|
+
* `activitypub.key.lookup.duration` (histogram) for a public-key lookup.
|
|
720
|
+
*
|
|
721
|
+
* `activitypub.lookup.kind` is always recorded as `public_key`; the result
|
|
722
|
+
* classification, remote host, HTTP status code (when an HTTP response was
|
|
723
|
+
* received), and `activitypub.cache.enabled` are recorded as attributes on
|
|
724
|
+
* both measurements. Full key URLs and key IDs are deliberately omitted to
|
|
725
|
+
* keep cardinality bounded.
|
|
726
|
+
* @since 2.3.0
|
|
727
|
+
*/
|
|
728
|
+
function recordKeyLookup(meterProvider, attrs) {
|
|
729
|
+
getFederationMetrics(meterProvider).recordKeyLookup(attrs);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Records one measurement each on `activitypub.document.fetch` (counter)
|
|
733
|
+
* and `activitypub.document.fetch.duration` (histogram) for one remote
|
|
734
|
+
* JSON-LD document loader invocation, with bounded
|
|
735
|
+
* `activitypub.lookup.kind` and `activitypub.lookup.result` attributes
|
|
736
|
+
* plus the optional remote-host, cache-enabled, and HTTP status-code
|
|
737
|
+
* attributes. Counter and histogram are always recorded together so
|
|
738
|
+
* aggregate rate and latency views stay in sync.
|
|
739
|
+
* @since 2.3.0
|
|
740
|
+
*/
|
|
741
|
+
function recordDocumentFetch(meterProvider, attrs) {
|
|
742
|
+
getFederationMetrics(meterProvider).recordDocumentFetch(attrs);
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Records one `activitypub.document.cache` measurement, classifying the
|
|
746
|
+
* lookup as `hit` (the cache returned an entry) or `miss` (the cache was
|
|
747
|
+
* consulted and returned nothing, prompting a delegate fetch).
|
|
748
|
+
* @since 2.3.0
|
|
749
|
+
*/
|
|
750
|
+
function recordDocumentCache(meterProvider, attrs) {
|
|
751
|
+
getFederationMetrics(meterProvider).recordDocumentCache(attrs);
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Records one measurement on `webfinger.handle` (counter) and
|
|
755
|
+
* `webfinger.handle.duration` (histogram) for an incoming WebFinger
|
|
756
|
+
* request handled by Fedify. Counter and histogram are always recorded
|
|
757
|
+
* together, with `webfinger.handle.result` set to one of `resolved`,
|
|
758
|
+
* `invalid`, `not_found`, `tombstoned`, or `error`. The queried
|
|
759
|
+
* resource string is deliberately excluded; it remains on the
|
|
760
|
+
* `webfinger.handle` span for trace-level investigation.
|
|
761
|
+
* @since 2.3.0
|
|
762
|
+
*/
|
|
763
|
+
function recordWebFingerHandle(meterProvider, attrs) {
|
|
764
|
+
getFederationMetrics(meterProvider).recordWebFingerHandle(attrs);
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Records one `activitypub.collection.request` measurement for a
|
|
768
|
+
* collection or collection-page request handled by Fedify.
|
|
769
|
+
* @since 2.3.0
|
|
770
|
+
*/
|
|
771
|
+
function recordCollectionRequest(meterProvider, attrs) {
|
|
772
|
+
getFederationMetrics(meterProvider).recordCollectionRequest(attrs);
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Records one `activitypub.collection.dispatch.duration` measurement for a
|
|
776
|
+
* collection dispatcher callback invocation.
|
|
777
|
+
* @since 2.3.0
|
|
778
|
+
*/
|
|
779
|
+
function recordCollectionDispatchDuration(meterProvider, durationMs, attrs) {
|
|
780
|
+
getFederationMetrics(meterProvider).recordCollectionDispatchDuration(durationMs, attrs);
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Records one `activitypub.collection.page.items` measurement when Fedify
|
|
784
|
+
* has materialized collection items in memory.
|
|
785
|
+
* @since 2.3.0
|
|
786
|
+
*/
|
|
787
|
+
function recordCollectionPageItems(meterProvider, itemCount, attrs) {
|
|
788
|
+
getFederationMetrics(meterProvider).recordCollectionPageItems(itemCount, attrs);
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Records one `activitypub.collection.total_items` measurement when a
|
|
792
|
+
* collection counter has already reported a total item count.
|
|
793
|
+
* @since 2.3.0
|
|
794
|
+
*/
|
|
795
|
+
function recordCollectionTotalItems(meterProvider, totalItems, attrs) {
|
|
796
|
+
getFederationMetrics(meterProvider).recordCollectionTotalItems(totalItems, attrs);
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Classifies a thrown value from a key or document fetch into the bounded
|
|
800
|
+
* {@link LookupResult} taxonomy and, when an HTTP response was received,
|
|
801
|
+
* surfaces its status code.
|
|
802
|
+
*
|
|
803
|
+
* - `FetchError` with a `Response` whose status is `404` or `410`:
|
|
804
|
+
* `result=not_found` and the response status code.
|
|
805
|
+
* - `FetchError` with any other `Response`: `result=error` and the
|
|
806
|
+
* response status code.
|
|
807
|
+
* - `FetchError` without a `Response`: `result=network_error`.
|
|
808
|
+
* - An `AbortError` (typically from a cancelled fetch): `result=network_error`.
|
|
809
|
+
* - A bare `TypeError` (the shape native `fetch()` raises on DNS, connect,
|
|
810
|
+
* and TLS failures before any response is observed):
|
|
811
|
+
* `result=network_error`.
|
|
812
|
+
* - Any other value: `result=error`.
|
|
813
|
+
* @since 2.3.0
|
|
814
|
+
*/
|
|
815
|
+
function classifyFetchError(error) {
|
|
816
|
+
if (error instanceof FetchError) {
|
|
817
|
+
if (error.response != null) {
|
|
818
|
+
const status = error.response.status;
|
|
819
|
+
return {
|
|
820
|
+
result: status === 404 || status === 410 ? "not_found" : "error",
|
|
821
|
+
statusCode: status
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
return { result: "network_error" };
|
|
825
|
+
}
|
|
826
|
+
if (isAbortError$1(error)) return { result: "network_error" };
|
|
827
|
+
if (error instanceof TypeError) return { result: "network_error" };
|
|
828
|
+
return { result: "error" };
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Wraps a {@link DocumentLoader} so each invocation records one
|
|
832
|
+
* measurement on `activitypub.document.fetch` (counter) and one on
|
|
833
|
+
* `activitypub.document.fetch.duration` (histogram), classifying the
|
|
834
|
+
* outcome via {@link classifyFetchError} when the wrapped loader throws
|
|
835
|
+
* and as `fetched` on success. The wrapper rethrows whatever the
|
|
836
|
+
* wrapped loader throws so caller behavior is unchanged.
|
|
837
|
+
*
|
|
838
|
+
* The wrapper records the host of the requested URL, including any
|
|
839
|
+
* non-default port, on `activitypub.remote.host` when the URL parses; full
|
|
840
|
+
* URLs, paths, and query strings are deliberately excluded to keep
|
|
841
|
+
* cardinality bounded.
|
|
842
|
+
* HTTP status codes are recorded only when the failure carries a
|
|
843
|
+
* `Response` (currently, when the wrapped loader throws a
|
|
844
|
+
* {@link FetchError} with a non-`null` `response`).
|
|
845
|
+
* @since 2.3.0
|
|
846
|
+
*/
|
|
847
|
+
function instrumentDocumentLoader(loader, options) {
|
|
848
|
+
const meterProvider = options.meterProvider;
|
|
849
|
+
if (meterProvider == null) return loader;
|
|
850
|
+
return async (url, opts) => {
|
|
851
|
+
const start = performance.now();
|
|
852
|
+
let remoteUrl;
|
|
853
|
+
try {
|
|
854
|
+
remoteUrl = new URL(url);
|
|
855
|
+
} catch {
|
|
856
|
+
remoteUrl = void 0;
|
|
857
|
+
}
|
|
858
|
+
try {
|
|
859
|
+
const result = await loader(url, opts);
|
|
860
|
+
recordDocumentFetch(meterProvider, {
|
|
861
|
+
durationMs: getDurationMs(start),
|
|
862
|
+
kind: options.kind,
|
|
863
|
+
result: "fetched",
|
|
864
|
+
remoteUrl,
|
|
865
|
+
cacheEnabled: options.cacheEnabled
|
|
866
|
+
});
|
|
867
|
+
return result;
|
|
868
|
+
} catch (error) {
|
|
869
|
+
const classified = classifyFetchError(error);
|
|
870
|
+
recordDocumentFetch(meterProvider, {
|
|
871
|
+
durationMs: getDurationMs(start),
|
|
872
|
+
kind: options.kind,
|
|
873
|
+
result: classified.result,
|
|
874
|
+
remoteUrl,
|
|
875
|
+
cacheEnabled: options.cacheEnabled,
|
|
876
|
+
statusCode: classified.statusCode
|
|
877
|
+
});
|
|
878
|
+
throw error;
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Times an awaited public key fetch and records exactly one
|
|
884
|
+
* `activitypub.signature.key_fetch.duration` measurement, classifying the
|
|
885
|
+
* outcome as `hit`, `fetched`, or `error` based on the `cached` flag and
|
|
886
|
+
* whether the returned key is non-null. Errors thrown by the fetch are
|
|
887
|
+
* reported as `error` and rethrown, so verifier behavior is unchanged.
|
|
888
|
+
*
|
|
889
|
+
* Shared by the three signature verifiers (HTTP, Linked Data, Object
|
|
890
|
+
* Integrity Proofs); the only per-call variation is the
|
|
891
|
+
* `activitypub.signature.kind` attribute value.
|
|
892
|
+
* @since 2.3.0
|
|
893
|
+
*/
|
|
894
|
+
async function measureSignatureKeyFetch(meterProvider, kind, fetch) {
|
|
895
|
+
const start = performance.now();
|
|
896
|
+
try {
|
|
897
|
+
const result = await fetch();
|
|
898
|
+
getFederationMetrics(meterProvider).recordSignatureKeyFetchDuration(getDurationMs(start), kind, result.key != null ? result.cached ? "hit" : "fetched" : "error");
|
|
899
|
+
return result;
|
|
900
|
+
} catch (error) {
|
|
901
|
+
getFederationMetrics(meterProvider).recordSignatureKeyFetchDuration(getDurationMs(start), kind, "error");
|
|
902
|
+
throw error;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Whether the given thrown value is an `AbortError`.
|
|
907
|
+
*
|
|
908
|
+
* `processQueuedTask` distinguishes aborted tasks (recorded as
|
|
909
|
+
* `fedify.queue.task.result=aborted`) from other failures so that backend
|
|
910
|
+
* shutdown signals do not inflate the `fedify.queue.task.failed` counter.
|
|
911
|
+
* @since 2.3.0
|
|
912
|
+
*/
|
|
913
|
+
function isAbortError$1(error) {
|
|
914
|
+
if (error == null || typeof error !== "object") return false;
|
|
915
|
+
const name = error.name;
|
|
916
|
+
return typeof name === "string" && name === "AbortError";
|
|
917
|
+
}
|
|
918
|
+
const KNOWN_HTTP_METHODS = new Set([
|
|
919
|
+
"CONNECT",
|
|
920
|
+
"DELETE",
|
|
921
|
+
"GET",
|
|
922
|
+
"HEAD",
|
|
923
|
+
"OPTIONS",
|
|
924
|
+
"PATCH",
|
|
925
|
+
"POST",
|
|
926
|
+
"PUT",
|
|
927
|
+
"QUERY",
|
|
928
|
+
"TRACE"
|
|
929
|
+
]);
|
|
930
|
+
function normalizeHttpMethod(method) {
|
|
931
|
+
const upper = method.toUpperCase();
|
|
932
|
+
return KNOWN_HTTP_METHODS.has(upper) ? upper : "_OTHER";
|
|
933
|
+
}
|
|
934
|
+
const federationMetrics = /* @__PURE__ */ new WeakMap();
|
|
935
|
+
/**
|
|
936
|
+
* Gets the cached Fedify metric instruments for a meter provider.
|
|
937
|
+
* @since 2.3.0
|
|
938
|
+
*/
|
|
939
|
+
function getFederationMetrics(meterProvider = metrics.getMeterProvider()) {
|
|
940
|
+
let instruments = federationMetrics.get(meterProvider);
|
|
941
|
+
if (instruments == null) {
|
|
942
|
+
instruments = new FederationMetrics(meterProvider);
|
|
943
|
+
federationMetrics.set(meterProvider, instruments);
|
|
944
|
+
}
|
|
945
|
+
return instruments;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Gets the bounded remote host attribute value for a URL.
|
|
949
|
+
* @since 2.3.0
|
|
950
|
+
*/
|
|
951
|
+
function getRemoteHost(url) {
|
|
952
|
+
return url.host;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Gets an elapsed duration in milliseconds from a `performance.now()` value.
|
|
956
|
+
* @since 2.3.0
|
|
957
|
+
*/
|
|
958
|
+
function getDurationMs(start) {
|
|
959
|
+
return Math.max(0, performance.now() - start);
|
|
960
|
+
}
|
|
961
|
+
//#endregion
|
|
154
962
|
//#region src/sig/key.ts
|
|
155
963
|
/**
|
|
156
964
|
* Checks if the given key is valid and supported. No-op if the key is valid,
|
|
@@ -451,24 +1259,48 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
|
|
|
451
1259
|
};
|
|
452
1260
|
}
|
|
453
1261
|
async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, onFetchError) {
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
"sig",
|
|
457
|
-
"key"
|
|
458
|
-
]);
|
|
459
|
-
const keyId = cacheKey.href;
|
|
460
|
-
const keyCache = options.keyCache;
|
|
461
|
-
const cached = await getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger);
|
|
462
|
-
if (cached?.key === null && cached.cached) return await onCachedUnavailable(cacheKey, keyId, keyCache, logger);
|
|
463
|
-
if (cached != null) return cached;
|
|
464
|
-
logger.debug("Fetching key {keyId} to verify signature...", { keyId });
|
|
465
|
-
let document;
|
|
1262
|
+
const start = performance.now();
|
|
1263
|
+
let outcome = { result: "error" };
|
|
466
1264
|
try {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
1265
|
+
const logger = getLogger([
|
|
1266
|
+
"fedify",
|
|
1267
|
+
"sig",
|
|
1268
|
+
"key"
|
|
1269
|
+
]);
|
|
1270
|
+
const keyId = cacheKey.href;
|
|
1271
|
+
const keyCache = options.keyCache;
|
|
1272
|
+
const cached = await getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger);
|
|
1273
|
+
if (cached?.key === null && cached.cached) {
|
|
1274
|
+
const cachedUnavailable = await onCachedUnavailable(cacheKey, keyId, keyCache, logger);
|
|
1275
|
+
outcome = { result: "hit" };
|
|
1276
|
+
return cachedUnavailable;
|
|
1277
|
+
}
|
|
1278
|
+
if (cached != null) {
|
|
1279
|
+
outcome = { result: "hit" };
|
|
1280
|
+
return cached;
|
|
1281
|
+
}
|
|
1282
|
+
logger.debug("Fetching key {keyId} to verify signature...", { keyId });
|
|
1283
|
+
let document;
|
|
1284
|
+
try {
|
|
1285
|
+
document = (await (options.documentLoader ?? getDocumentLoader())(keyId)).document;
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
const classified = classifyFetchError(error);
|
|
1288
|
+
const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
|
|
1289
|
+
outcome = classified;
|
|
1290
|
+
return errored;
|
|
1291
|
+
}
|
|
1292
|
+
const resolved = await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
|
|
1293
|
+
outcome = { result: resolved.key != null ? "fetched" : "invalid" };
|
|
1294
|
+
return resolved;
|
|
1295
|
+
} finally {
|
|
1296
|
+
recordKeyLookup(options.meterProvider, {
|
|
1297
|
+
durationMs: getDurationMs(start),
|
|
1298
|
+
result: outcome.result,
|
|
1299
|
+
remoteUrl: cacheKey,
|
|
1300
|
+
cacheEnabled: options.keyCache != null,
|
|
1301
|
+
statusCode: outcome.statusCode
|
|
1302
|
+
});
|
|
470
1303
|
}
|
|
471
|
-
return await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
|
|
472
1304
|
}
|
|
473
1305
|
async function fetchKeyInternal(keyId, cls, options = {}) {
|
|
474
1306
|
return await fetchKeyWithResult(typeof keyId === "string" ? new URL(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
|
|
@@ -496,6 +1328,7 @@ async function fetchKeyInternal(keyId, cls, options = {}) {
|
|
|
496
1328
|
//#endregion
|
|
497
1329
|
//#region src/sig/http.ts
|
|
498
1330
|
const DEFAULT_MAX_REDIRECTION = 20;
|
|
1331
|
+
const DOUBLE_KNOCK_TRANSPORT_RETRY_DELAY_MS = 100;
|
|
499
1332
|
/**
|
|
500
1333
|
* Signs a request using the given private key.
|
|
501
1334
|
* @param request The request to sign.
|
|
@@ -798,6 +1631,27 @@ function parseKeyId(value) {
|
|
|
798
1631
|
function getKeyFetchErrorName(error) {
|
|
799
1632
|
return error.name || error.constructor.name || "Error";
|
|
800
1633
|
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Known draft-cavage `algorithm` parameter values, used to keep the
|
|
1636
|
+
* `http_signatures.algorithm` metric attribute on a bounded set. The header
|
|
1637
|
+
* field is attacker-controlled and not used to select the verification
|
|
1638
|
+
* algorithm, so unknown values are dropped from the metric to prevent
|
|
1639
|
+
* cardinality blow-up.
|
|
1640
|
+
*/
|
|
1641
|
+
const DRAFT_KNOWN_ALGORITHMS = new Set([
|
|
1642
|
+
"ecdsa-sha256",
|
|
1643
|
+
"ecdsa-sha384",
|
|
1644
|
+
"ecdsa-sha512",
|
|
1645
|
+
"ed25519",
|
|
1646
|
+
"hs2019",
|
|
1647
|
+
"rsa-sha1",
|
|
1648
|
+
"rsa-sha256",
|
|
1649
|
+
"rsa-sha512"
|
|
1650
|
+
]);
|
|
1651
|
+
function classifyHttpVerifyResult(result) {
|
|
1652
|
+
if (result.verified) return "verified";
|
|
1653
|
+
return result.reason.type === "noSignature" ? "missing" : "rejected";
|
|
1654
|
+
}
|
|
801
1655
|
function recordVerificationResult(span, result) {
|
|
802
1656
|
span.setAttribute("http_signatures.verified", result.verified);
|
|
803
1657
|
if (result.verified === true) return;
|
|
@@ -841,27 +1695,37 @@ async function verifyRequestDetailed(request, options = {}) {
|
|
|
841
1695
|
span.setAttribute(ATTR_URL_FULL, request.url);
|
|
842
1696
|
for (const [name, value] of request.headers) span.setAttribute(ATTR_HTTP_REQUEST_HEADER(name), value);
|
|
843
1697
|
}
|
|
1698
|
+
const start = performance.now();
|
|
1699
|
+
const metricsContext = {};
|
|
1700
|
+
let result;
|
|
1701
|
+
let threw = false;
|
|
844
1702
|
try {
|
|
845
1703
|
let spec = options.spec;
|
|
846
1704
|
if (spec == null) spec = request.headers.has("Signature-Input") ? "rfc9421" : "draft-cavage-http-signatures-12";
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
else result = await verifyRequestDraft(request, span, options);
|
|
1705
|
+
if (spec === "rfc9421") result = await verifyRequestRfc9421(request, span, metricsContext, options);
|
|
1706
|
+
else result = await verifyRequestDraft(request, span, metricsContext, options);
|
|
850
1707
|
recordVerificationResult(span, result);
|
|
851
1708
|
if (!result.verified) span.setStatus({ code: SpanStatusCode.ERROR });
|
|
852
1709
|
return result;
|
|
853
1710
|
} catch (error) {
|
|
1711
|
+
threw = true;
|
|
854
1712
|
span.setStatus({
|
|
855
1713
|
code: SpanStatusCode.ERROR,
|
|
856
1714
|
message: String(error)
|
|
857
1715
|
});
|
|
858
1716
|
throw error;
|
|
859
1717
|
} finally {
|
|
1718
|
+
const classified = threw ? "error" : classifyHttpVerifyResult(result);
|
|
1719
|
+
const failureReason = result != null && !result.verified && result.reason.type !== "noSignature" ? result.reason.type : void 0;
|
|
1720
|
+
getFederationMetrics(options.meterProvider).recordSignatureVerificationDuration(getDurationMs(start), "http", classified, {
|
|
1721
|
+
algorithm: metricsContext.algorithm,
|
|
1722
|
+
failureReason
|
|
1723
|
+
});
|
|
860
1724
|
span.end();
|
|
861
1725
|
}
|
|
862
1726
|
});
|
|
863
1727
|
}
|
|
864
|
-
async function verifyRequestDraft(request, span, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, tracerProvider } = {}) {
|
|
1728
|
+
async function verifyRequestDraft(request, span, metricsContext, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, meterProvider, tracerProvider } = {}) {
|
|
865
1729
|
const logger = getLogger([
|
|
866
1730
|
"fedify",
|
|
867
1731
|
"sig",
|
|
@@ -1009,13 +1873,18 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
|
|
|
1009
1873
|
const keyIdUrl = parseKeyId(keyId);
|
|
1010
1874
|
if (keyIdUrl == null) return invalidSignatureResult(null);
|
|
1011
1875
|
span?.setAttribute("http_signatures.key_id", keyId);
|
|
1012
|
-
if ("algorithm" in sigValues)
|
|
1013
|
-
|
|
1876
|
+
if ("algorithm" in sigValues) {
|
|
1877
|
+
span?.setAttribute("http_signatures.algorithm", sigValues.algorithm);
|
|
1878
|
+
const normalizedAlgorithm = sigValues.algorithm.toLowerCase();
|
|
1879
|
+
if (DRAFT_KNOWN_ALGORITHMS.has(normalizedAlgorithm)) metricsContext.algorithm = normalizedAlgorithm;
|
|
1880
|
+
}
|
|
1881
|
+
const { key, cached, fetchError } = await measureSignatureKeyFetch(meterProvider, "http", () => fetchKeyDetailed(keyIdUrl, CryptographicKey, {
|
|
1014
1882
|
documentLoader,
|
|
1015
1883
|
contextLoader,
|
|
1016
1884
|
keyCache,
|
|
1017
|
-
tracerProvider
|
|
1018
|
-
|
|
1885
|
+
tracerProvider,
|
|
1886
|
+
meterProvider
|
|
1887
|
+
}));
|
|
1019
1888
|
if (fetchError != null) return keyFetchErrorResult(keyIdUrl, fetchError);
|
|
1020
1889
|
if (key == null) return invalidSignatureResult(keyIdUrl);
|
|
1021
1890
|
const headerNames = headers.split(/\s+/g);
|
|
@@ -1037,7 +1906,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
|
|
|
1037
1906
|
signature,
|
|
1038
1907
|
message
|
|
1039
1908
|
});
|
|
1040
|
-
return await
|
|
1909
|
+
return await verifyRequestDraft(originalRequest, span, metricsContext, {
|
|
1041
1910
|
documentLoader,
|
|
1042
1911
|
contextLoader,
|
|
1043
1912
|
timeWindow,
|
|
@@ -1045,7 +1914,9 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
|
|
|
1045
1914
|
keyCache: {
|
|
1046
1915
|
get: () => Promise.resolve(void 0),
|
|
1047
1916
|
set: async (keyId, key) => await keyCache?.set(keyId, key)
|
|
1048
|
-
}
|
|
1917
|
+
},
|
|
1918
|
+
meterProvider,
|
|
1919
|
+
tracerProvider
|
|
1049
1920
|
});
|
|
1050
1921
|
}
|
|
1051
1922
|
logger.debug("Failed to verify with the fetched key {keyId}; signature {signature} is invalid. Check if the key is correct or if the signed message is correct. The message to sign is:\n{message}", {
|
|
@@ -1121,7 +1992,7 @@ async function verifyRfc9421ContentDigest(digestHeader, body) {
|
|
|
1121
1992
|
}
|
|
1122
1993
|
return false;
|
|
1123
1994
|
}
|
|
1124
|
-
async function verifyRequestRfc9421(request, span, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, tracerProvider } = {}) {
|
|
1995
|
+
async function verifyRequestRfc9421(request, span, metricsContext, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, meterProvider, tracerProvider } = {}) {
|
|
1125
1996
|
const logger = getLogger([
|
|
1126
1997
|
"fedify",
|
|
1127
1998
|
"sig",
|
|
@@ -1155,9 +2026,14 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
|
|
|
1155
2026
|
return invalidSignatureResult(null);
|
|
1156
2027
|
}
|
|
1157
2028
|
let failure = noSignatureResult();
|
|
2029
|
+
let failureAlgorithm;
|
|
2030
|
+
const setFailure = (result, algorithm) => {
|
|
2031
|
+
failure = result;
|
|
2032
|
+
failureAlgorithm = algorithm;
|
|
2033
|
+
};
|
|
1158
2034
|
for (const sigName of signatureNames) {
|
|
1159
2035
|
if (!signatures[sigName]) {
|
|
1160
|
-
|
|
2036
|
+
setFailure(invalidSignatureResult(parseKeyId(signatureInputs[sigName]?.keyId)));
|
|
1161
2037
|
continue;
|
|
1162
2038
|
}
|
|
1163
2039
|
const sigInput = signatureInputs[sigName];
|
|
@@ -1168,7 +2044,7 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
|
|
|
1168
2044
|
signatureName: sigName,
|
|
1169
2045
|
signatureInput: signatureInputHeader
|
|
1170
2046
|
});
|
|
1171
|
-
|
|
2047
|
+
setFailure(invalidSignatureResult(null));
|
|
1172
2048
|
continue;
|
|
1173
2049
|
}
|
|
1174
2050
|
if (!sigInput.created) {
|
|
@@ -1176,7 +2052,7 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
|
|
|
1176
2052
|
signatureName: sigName,
|
|
1177
2053
|
signatureInput: signatureInputHeader
|
|
1178
2054
|
});
|
|
1179
|
-
|
|
2055
|
+
setFailure(invalidSignatureResult(keyId));
|
|
1180
2056
|
continue;
|
|
1181
2057
|
}
|
|
1182
2058
|
const signatureCreated = Temporal.Instant.fromEpochMilliseconds(sigInput.created * 1e3);
|
|
@@ -1188,14 +2064,14 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
|
|
|
1188
2064
|
created: signatureCreated.toString(),
|
|
1189
2065
|
now: now.toString()
|
|
1190
2066
|
});
|
|
1191
|
-
|
|
2067
|
+
setFailure(invalidSignatureResult(keyId));
|
|
1192
2068
|
continue;
|
|
1193
2069
|
} else if (Temporal.Instant.compare(signatureCreated, now.subtract(tw)) < 0) {
|
|
1194
2070
|
logger.debug("Failed to verify; signature created time is too far in the past.", {
|
|
1195
2071
|
created: signatureCreated.toString(),
|
|
1196
2072
|
now: now.toString()
|
|
1197
2073
|
});
|
|
1198
|
-
|
|
2074
|
+
setFailure(invalidSignatureResult(keyId));
|
|
1199
2075
|
continue;
|
|
1200
2076
|
}
|
|
1201
2077
|
}
|
|
@@ -1203,34 +2079,35 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
|
|
|
1203
2079
|
const contentDigestHeader = request.headers.get("Content-Digest");
|
|
1204
2080
|
if (!contentDigestHeader) {
|
|
1205
2081
|
logger.debug("Failed to verify; Content-Digest header required but not found.", { components: sigInput.components });
|
|
1206
|
-
|
|
2082
|
+
setFailure(invalidSignatureResult(keyId));
|
|
1207
2083
|
continue;
|
|
1208
2084
|
}
|
|
1209
2085
|
if (!await verifyRfc9421ContentDigest(contentDigestHeader, await request.arrayBuffer())) {
|
|
1210
2086
|
logger.debug("Failed to verify; Content-Digest verification failed.", { contentDigest: contentDigestHeader });
|
|
1211
|
-
|
|
2087
|
+
setFailure(invalidSignatureResult(keyId));
|
|
1212
2088
|
continue;
|
|
1213
2089
|
}
|
|
1214
2090
|
}
|
|
1215
2091
|
span?.setAttribute("http_signatures.key_id", sigInput.keyId);
|
|
1216
2092
|
span?.setAttribute("http_signatures.created", sigInput.created.toString());
|
|
1217
2093
|
if (keyId == null) {
|
|
1218
|
-
|
|
2094
|
+
setFailure(invalidSignatureResult(null));
|
|
1219
2095
|
continue;
|
|
1220
2096
|
}
|
|
1221
|
-
const { key, cached, fetchError } = await fetchKeyDetailed(keyId, CryptographicKey, {
|
|
2097
|
+
const { key, cached, fetchError } = await measureSignatureKeyFetch(meterProvider, "http", () => fetchKeyDetailed(keyId, CryptographicKey, {
|
|
1222
2098
|
documentLoader,
|
|
1223
2099
|
contextLoader,
|
|
1224
2100
|
keyCache,
|
|
1225
|
-
tracerProvider
|
|
1226
|
-
|
|
2101
|
+
tracerProvider,
|
|
2102
|
+
meterProvider
|
|
2103
|
+
}));
|
|
1227
2104
|
if (fetchError != null) {
|
|
1228
|
-
|
|
2105
|
+
setFailure(keyFetchErrorResult(keyId, fetchError));
|
|
1229
2106
|
continue;
|
|
1230
2107
|
}
|
|
1231
2108
|
if (!key) {
|
|
1232
2109
|
logger.debug("Failed to fetch key: {keyId}", { keyId: sigInput.keyId });
|
|
1233
|
-
|
|
2110
|
+
setFailure(invalidSignatureResult(keyId));
|
|
1234
2111
|
continue;
|
|
1235
2112
|
}
|
|
1236
2113
|
let alg = sigInput.alg?.toLowerCase();
|
|
@@ -1242,12 +2119,13 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
|
|
|
1242
2119
|
}
|
|
1243
2120
|
if (alg) span?.setAttribute("http_signatures.algorithm", alg);
|
|
1244
2121
|
const algorithm = alg && rfc9421AlgorithmMap[alg];
|
|
2122
|
+
const candidateAlgorithm = algorithm ? alg : void 0;
|
|
1245
2123
|
if (!algorithm) {
|
|
1246
2124
|
logger.debug("Failed to verify; unsupported algorithm: {algorithm}", {
|
|
1247
2125
|
algorithm: sigInput.alg,
|
|
1248
2126
|
supported: Object.keys(rfc9421AlgorithmMap)
|
|
1249
2127
|
});
|
|
1250
|
-
|
|
2128
|
+
setFailure(invalidSignatureResult(keyId));
|
|
1251
2129
|
continue;
|
|
1252
2130
|
}
|
|
1253
2131
|
let signatureBase;
|
|
@@ -1258,20 +2136,22 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
|
|
|
1258
2136
|
error,
|
|
1259
2137
|
signatureInput: sigInput
|
|
1260
2138
|
});
|
|
1261
|
-
|
|
2139
|
+
setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
|
|
1262
2140
|
continue;
|
|
1263
2141
|
}
|
|
1264
2142
|
const signatureBaseBytes = new TextEncoder().encode(signatureBase);
|
|
1265
2143
|
span?.setAttribute("http_signatures.signature", encodeHex(sigBytes));
|
|
1266
2144
|
try {
|
|
1267
|
-
if (await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes))
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
2145
|
+
if (await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes)) {
|
|
2146
|
+
metricsContext.algorithm = candidateAlgorithm;
|
|
2147
|
+
return {
|
|
2148
|
+
verified: true,
|
|
2149
|
+
key,
|
|
2150
|
+
signatureLabel: sigName
|
|
2151
|
+
};
|
|
2152
|
+
} else if (cached) {
|
|
1273
2153
|
logger.debug("Failed to verify with cached key {keyId}; retrying with fresh key...", { keyId: sigInput.keyId });
|
|
1274
|
-
return await
|
|
2154
|
+
return await verifyRequestRfc9421(originalRequest, span, metricsContext, {
|
|
1275
2155
|
documentLoader,
|
|
1276
2156
|
contextLoader,
|
|
1277
2157
|
timeWindow,
|
|
@@ -1280,14 +2160,16 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
|
|
|
1280
2160
|
get: () => Promise.resolve(void 0),
|
|
1281
2161
|
set: async (keyId, key) => await keyCache?.set(keyId, key)
|
|
1282
2162
|
},
|
|
1283
|
-
spec: "rfc9421"
|
|
2163
|
+
spec: "rfc9421",
|
|
2164
|
+
meterProvider,
|
|
2165
|
+
tracerProvider
|
|
1284
2166
|
});
|
|
1285
2167
|
} else {
|
|
1286
2168
|
logger.debug("Failed to verify signature with fetched key {keyId}; signature invalid.", {
|
|
1287
2169
|
keyId: sigInput.keyId,
|
|
1288
2170
|
signatureBase
|
|
1289
2171
|
});
|
|
1290
|
-
|
|
2172
|
+
setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
|
|
1291
2173
|
}
|
|
1292
2174
|
} catch (error) {
|
|
1293
2175
|
logger.debug("Error during signature verification: {error}", {
|
|
@@ -1295,9 +2177,10 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
|
|
|
1295
2177
|
keyId: sigInput.keyId,
|
|
1296
2178
|
algorithm: sigInput.alg
|
|
1297
2179
|
});
|
|
1298
|
-
|
|
2180
|
+
setFailure(invalidSignatureResult(keyId), candidateAlgorithm);
|
|
1299
2181
|
}
|
|
1300
2182
|
}
|
|
2183
|
+
metricsContext.algorithm = failureAlgorithm;
|
|
1301
2184
|
return failure;
|
|
1302
2185
|
}
|
|
1303
2186
|
/**
|
|
@@ -1324,6 +2207,59 @@ function createRedirectRequest(request, location, body) {
|
|
|
1324
2207
|
cache: request.cache
|
|
1325
2208
|
});
|
|
1326
2209
|
}
|
|
2210
|
+
async function fetchDoubleKnockRequest(request, signedRequest, signal) {
|
|
2211
|
+
const maxAttempts = request.method === "GET" || request.method === "HEAD" ? 2 : 1;
|
|
2212
|
+
for (let attempt = 1;; attempt++) try {
|
|
2213
|
+
return await fetch(signedRequest, {
|
|
2214
|
+
redirect: "manual",
|
|
2215
|
+
signal
|
|
2216
|
+
});
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
const abortedSignal = getAbortedSignal(signal, request.signal, signedRequest.signal);
|
|
2219
|
+
if (abortedSignal != null) throw getAbortReason(abortedSignal);
|
|
2220
|
+
if (isAbortError(error)) throw error;
|
|
2221
|
+
if (attempt >= maxAttempts) throw createFetchError(request.url, error);
|
|
2222
|
+
await sleep(DOUBLE_KNOCK_TRANSPORT_RETRY_DELAY_MS, signal, request.signal, signedRequest.signal);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
function createFetchError(url, cause) {
|
|
2226
|
+
const error = new FetchError(url, cause instanceof Error ? cause.message : String(cause));
|
|
2227
|
+
error.cause = cause;
|
|
2228
|
+
return error;
|
|
2229
|
+
}
|
|
2230
|
+
function isAbortError(error) {
|
|
2231
|
+
return error instanceof Error && error.name === "AbortError";
|
|
2232
|
+
}
|
|
2233
|
+
async function sleep(ms, ...signals) {
|
|
2234
|
+
const abortSignals = signals.filter((signal) => signal != null);
|
|
2235
|
+
const abortedSignal = getAbortedSignal(...abortSignals);
|
|
2236
|
+
if (abortedSignal != null) throw getAbortReason(abortedSignal);
|
|
2237
|
+
if (abortSignals.length < 1) {
|
|
2238
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
await new Promise((resolve, reject) => {
|
|
2242
|
+
const removeAbortListeners = () => {
|
|
2243
|
+
for (const signal of abortSignals) signal.removeEventListener("abort", handleAbort);
|
|
2244
|
+
};
|
|
2245
|
+
const timeout = setTimeout(() => {
|
|
2246
|
+
removeAbortListeners();
|
|
2247
|
+
resolve();
|
|
2248
|
+
}, ms);
|
|
2249
|
+
function handleAbort(event) {
|
|
2250
|
+
clearTimeout(timeout);
|
|
2251
|
+
removeAbortListeners();
|
|
2252
|
+
reject(getAbortReason(event.currentTarget));
|
|
2253
|
+
}
|
|
2254
|
+
for (const signal of abortSignals) signal.addEventListener("abort", handleAbort, { once: true });
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
function getAbortedSignal(...signals) {
|
|
2258
|
+
return signals.find((signal) => signal?.aborted);
|
|
2259
|
+
}
|
|
2260
|
+
function getAbortReason(signal) {
|
|
2261
|
+
return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
2262
|
+
}
|
|
1327
2263
|
/**
|
|
1328
2264
|
* Performs a double-knock request to the given URL. For the details of
|
|
1329
2265
|
* double-knocking, see
|
|
@@ -1350,10 +2286,7 @@ async function doubleKnockInternal(request, identity, options, redirected = 0, v
|
|
|
1350
2286
|
body
|
|
1351
2287
|
});
|
|
1352
2288
|
log?.(signedRequest);
|
|
1353
|
-
let response = await
|
|
1354
|
-
redirect: "manual",
|
|
1355
|
-
signal
|
|
1356
|
-
});
|
|
2289
|
+
let response = await fetchDoubleKnockRequest(request, signedRequest, signal);
|
|
1357
2290
|
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
1358
2291
|
if (redirected >= maximumRedirection) throw new FetchError(request.url, `Too many redirections (${redirected + 1})`);
|
|
1359
2292
|
const redirectRequest = createRedirectRequest(request, response.headers.get("Location"), body);
|
|
@@ -1428,10 +2361,7 @@ async function doubleKnockInternal(request, identity, options, redirected = 0, v
|
|
|
1428
2361
|
body
|
|
1429
2362
|
});
|
|
1430
2363
|
log?.(signedRequest);
|
|
1431
|
-
response = await
|
|
1432
|
-
redirect: "manual",
|
|
1433
|
-
signal
|
|
1434
|
-
});
|
|
2364
|
+
response = await fetchDoubleKnockRequest(request, signedRequest, signal);
|
|
1435
2365
|
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
1436
2366
|
if (redirected >= maximumRedirection) throw new FetchError(request.url, `Too many redirections (${redirected + 1})`);
|
|
1437
2367
|
const redirectRequest = createRedirectRequest(request, response.headers.get("Location"), body);
|
|
@@ -1472,4 +2402,4 @@ function timingSafeEqual(a, b) {
|
|
|
1472
2402
|
return result === 0;
|
|
1473
2403
|
}
|
|
1474
2404
|
//#endregion
|
|
1475
|
-
export { version as _, verifyRequestDetailed as a, fetchKeyDetailed as c, validateCryptoKey as d,
|
|
2405
|
+
export { formatAcceptSignature as A, recordDocumentCache as C, recordOutboxEnqueue as D, recordOutboxActivity as E, version as F, parseAcceptSignature as M, validateAcceptSignature as N, recordWebFingerHandle as O, name as P, recordCollectionTotalItems as S, recordInboxActivity as T, measureSignatureKeyFetch as _, verifyRequestDetailed as a, recordCollectionPageItems as b, fetchKeyDetailed as c, validateCryptoKey as d, getDurationMs as f, isAbortError$1 as g, instrumentDocumentLoader as h, verifyRequest as i, fulfillAcceptSignature as j, registerQueueDepthGauge as k, 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, recordCircuitBreakerStateChange as v, recordFanoutRecipients as w, recordCollectionRequest as x, recordCollectionDispatchDuration as y };
|