@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,32 +1,36 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { n as
|
|
5
|
-
import { n as
|
|
4
|
+
import { n as version, t as name } from "./deno-BID9qTPk.mjs";
|
|
5
|
+
import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-ColY3QKj.mjs";
|
|
6
6
|
import { t as formatAcceptSignature } from "./accept-CPkZzmGN.mjs";
|
|
7
|
-
import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-
|
|
8
|
-
import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-
|
|
9
|
-
import { t as getAuthenticatedDocumentLoader } from "./docloader-
|
|
10
|
-
import { n as kvCache } from "./kv-cache-
|
|
11
|
-
import { a as
|
|
12
|
-
import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-
|
|
13
|
-
import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-
|
|
14
|
-
import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-
|
|
15
|
-
import { t as getNodeInfo } from "./client-
|
|
7
|
+
import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-Dqdlo3mG.mjs";
|
|
8
|
+
import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-CO59u1Yk.mjs";
|
|
9
|
+
import { t as getAuthenticatedDocumentLoader } from "./docloader-BYFuVwwi.mjs";
|
|
10
|
+
import { n as kvCache } from "./kv-cache-DhLkLv0P.mjs";
|
|
11
|
+
import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-JbCByP5P.mjs";
|
|
12
|
+
import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-B4LgOmzV.mjs";
|
|
13
|
+
import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-L_DbOaFe.mjs";
|
|
14
|
+
import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-BoSGOq9q.mjs";
|
|
15
|
+
import { t as getNodeInfo } from "./client-ByXmQhYD.mjs";
|
|
16
16
|
import { t as nodeInfoToJson } from "./types-J53Kw7so.mjs";
|
|
17
|
-
import { t as
|
|
18
|
-
import { t as
|
|
19
|
-
import { t as
|
|
20
|
-
import { t as
|
|
21
|
-
import { t as
|
|
22
|
-
import {
|
|
17
|
+
import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-ByK1Q7DK.mjs";
|
|
18
|
+
import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-Dl5Sdmc9.mjs";
|
|
19
|
+
import { t as CircuitBreaker } from "./circuit-breaker-CSWsyoef.mjs";
|
|
20
|
+
import { t as buildCollectionSynchronizationHeader } from "./collection-Cc3DVAhE.mjs";
|
|
21
|
+
import { t as KvKeyCache } from "./keycache-BeU0LCII.mjs";
|
|
22
|
+
import { t as acceptsJsonLd } from "./negotiation-DDstyBvc.mjs";
|
|
23
|
+
import { t as hasMalformedKnownTemporalLiteral } from "./temporal-LpjyAjKb.mjs";
|
|
24
|
+
import { t as createExponentialBackoffPolicy } from "./retry-CXg_MBI-.mjs";
|
|
25
|
+
import { getLogger, withContext } from "@logtape/logtape";
|
|
26
|
+
import { RouterError } from "@fedify/uri-template";
|
|
23
27
|
import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
24
28
|
import { lookupWebFinger } from "@fedify/webfinger";
|
|
25
|
-
import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
|
|
26
|
-
import { uniq } from "es-toolkit";
|
|
29
|
+
import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
|
|
27
30
|
import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
|
|
28
|
-
import { getLogger, withContext } from "@logtape/logtape";
|
|
29
31
|
import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
|
|
32
|
+
import { uniq } from "es-toolkit";
|
|
33
|
+
import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
|
|
30
34
|
import { domainToASCII } from "node:url";
|
|
31
35
|
//#region src/compat/transformers.ts
|
|
32
36
|
const logger$1 = getLogger([
|
|
@@ -153,8 +157,203 @@ function handleNodeInfoJrd(_request, context) {
|
|
|
153
157
|
return Promise.resolve(response);
|
|
154
158
|
}
|
|
155
159
|
//#endregion
|
|
160
|
+
//#region src/federation/bench.ts
|
|
161
|
+
/**
|
|
162
|
+
* Metric reader owned by `benchmarkMode`.
|
|
163
|
+
* @since 2.3.0
|
|
164
|
+
*/
|
|
165
|
+
var BenchmarkMetricReader = class extends MetricReader {
|
|
166
|
+
onShutdown() {
|
|
167
|
+
return Promise.resolve();
|
|
168
|
+
}
|
|
169
|
+
onForceFlush() {
|
|
170
|
+
return Promise.resolve();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Creates the in-process OpenTelemetry meter provider used by benchmark mode.
|
|
175
|
+
* @returns The meter provider and the metric reader attached to it.
|
|
176
|
+
* @since 2.3.0
|
|
177
|
+
*/
|
|
178
|
+
function createBenchmarkMeterProvider() {
|
|
179
|
+
const reader = new BenchmarkMetricReader();
|
|
180
|
+
return {
|
|
181
|
+
meterProvider: new MeterProvider$1({ readers: [reader] }),
|
|
182
|
+
reader
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Collects and serializes benchmark-mode metrics from a benchmark reader.
|
|
187
|
+
* @param reader The benchmark metric reader to collect from.
|
|
188
|
+
* @returns A server metric snapshot with any collection errors stringified.
|
|
189
|
+
* @since 2.3.0
|
|
190
|
+
*/
|
|
191
|
+
async function collectBenchmarkMetrics(reader) {
|
|
192
|
+
const result = await reader.collect();
|
|
193
|
+
return {
|
|
194
|
+
version: 1,
|
|
195
|
+
source: "server",
|
|
196
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
197
|
+
scopeMetrics: serializeScopeMetrics(result.resourceMetrics),
|
|
198
|
+
errors: result.errors.map((error) => String(error))
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Handles `GET /.well-known/fedify/bench/stats`.
|
|
203
|
+
* @param request The HTTP request to handle.
|
|
204
|
+
* @param reader The benchmark metric reader to collect from.
|
|
205
|
+
* @returns A JSON metric snapshot response, or `405 Method Not Allowed`.
|
|
206
|
+
* @since 2.3.0
|
|
207
|
+
*/
|
|
208
|
+
async function handleBenchmarkStats(request, reader) {
|
|
209
|
+
if (request.method !== "GET") return new Response("Method not allowed", {
|
|
210
|
+
status: 405,
|
|
211
|
+
headers: { "Allow": "GET" }
|
|
212
|
+
});
|
|
213
|
+
return jsonResponse(await collectBenchmarkMetrics(reader));
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Handles `POST /.well-known/fedify/bench/trigger`.
|
|
217
|
+
*
|
|
218
|
+
* The handler validates a benchmark trigger request, checks recipients against
|
|
219
|
+
* server-controlled trigger options, and calls `Context.sendActivity()` to use
|
|
220
|
+
* the target's normal outbox path.
|
|
221
|
+
* @param request The HTTP request to handle.
|
|
222
|
+
* @param context The Fedify context used to resolve actors and send activity.
|
|
223
|
+
* @param options Server-controlled benchmark trigger delivery options.
|
|
224
|
+
* @returns A JSON response describing the sent activity, or a validation error.
|
|
225
|
+
* @since 2.3.0
|
|
226
|
+
*/
|
|
227
|
+
async function handleBenchmarkTrigger(request, context, options = {}) {
|
|
228
|
+
if (request.method !== "POST") return new Response("Method not allowed", {
|
|
229
|
+
status: 405,
|
|
230
|
+
headers: { "Allow": "POST" }
|
|
231
|
+
});
|
|
232
|
+
let json;
|
|
233
|
+
try {
|
|
234
|
+
json = await request.json();
|
|
235
|
+
} catch {
|
|
236
|
+
return jsonResponse({ error: "Invalid JSON request body." }, 400);
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const body = asRecord(json, "request body");
|
|
240
|
+
const sender = parseSender(body.sender);
|
|
241
|
+
const recipients = await parseRecipients(body.recipients, context);
|
|
242
|
+
const activity = await parseActivity(body.activity, context);
|
|
243
|
+
if (activity.id == null) throw new BenchmarkTriggerError("activity must have an id.");
|
|
244
|
+
const activityId = activity.id.href;
|
|
245
|
+
const inboxes = extractInboxes({ recipients });
|
|
246
|
+
const inboxUrls = Object.keys(inboxes);
|
|
247
|
+
if (inboxUrls.length < 1) throw new BenchmarkTriggerError("No valid recipient inboxes found. The recipients list must not be empty.");
|
|
248
|
+
const unsafeInboxes = options.allowUnsafeRecipients ? [] : inboxUrls.filter((inbox) => !options.sinks?.has(inbox));
|
|
249
|
+
if (unsafeInboxes.length > 0) return jsonResponse({
|
|
250
|
+
error: "unsafe_recipient",
|
|
251
|
+
unsafeInboxes
|
|
252
|
+
}, 403);
|
|
253
|
+
await context.sendActivity(sender, recipients, activity);
|
|
254
|
+
return jsonResponse({
|
|
255
|
+
version: 1,
|
|
256
|
+
activityId,
|
|
257
|
+
queueCorrelationId: activityId,
|
|
258
|
+
recipientCount: recipients.length,
|
|
259
|
+
inboxCount: inboxUrls.length
|
|
260
|
+
}, 202);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (error instanceof BenchmarkTriggerError) return jsonResponse({ error: error.message }, error.status);
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
var BenchmarkTriggerError = class extends Error {
|
|
267
|
+
status;
|
|
268
|
+
constructor(message, status = 400) {
|
|
269
|
+
super(message);
|
|
270
|
+
this.status = status;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
function parseSender(value) {
|
|
274
|
+
const sender = asRecord(value, "sender");
|
|
275
|
+
if (typeof sender.identifier === "string") return { identifier: sender.identifier };
|
|
276
|
+
if (typeof sender.username === "string") return { username: sender.username };
|
|
277
|
+
throw new BenchmarkTriggerError("sender must be { identifier } or { username }.");
|
|
278
|
+
}
|
|
279
|
+
async function parseRecipients(value, context) {
|
|
280
|
+
if (!Array.isArray(value)) throw new BenchmarkTriggerError("recipients must be an array.");
|
|
281
|
+
return await Promise.all(value.map(async (item) => {
|
|
282
|
+
let object;
|
|
283
|
+
try {
|
|
284
|
+
object = await Object$1.fromJsonLd(item, {
|
|
285
|
+
documentLoader: context.documentLoader,
|
|
286
|
+
contextLoader: context.contextLoader
|
|
287
|
+
});
|
|
288
|
+
} catch (error) {
|
|
289
|
+
throw new BenchmarkTriggerError(`Invalid ActivityPub recipient: ${error}`);
|
|
290
|
+
}
|
|
291
|
+
if (!isRecipient(object)) throw new BenchmarkTriggerError("each recipient must be an ActivityPub actor.");
|
|
292
|
+
const recipient = object;
|
|
293
|
+
if (recipient.id == null || recipient.inboxId == null) throw new BenchmarkTriggerError("each recipient must have id and inbox properties.");
|
|
294
|
+
return recipient;
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
function isRecipient(value) {
|
|
298
|
+
return value != null && typeof value === "object" && "id" in value && "inboxId" in value;
|
|
299
|
+
}
|
|
300
|
+
async function parseActivity(value, context) {
|
|
301
|
+
try {
|
|
302
|
+
return await Activity.fromJsonLd(value, {
|
|
303
|
+
documentLoader: context.documentLoader,
|
|
304
|
+
contextLoader: context.contextLoader
|
|
305
|
+
});
|
|
306
|
+
} catch (error) {
|
|
307
|
+
throw new BenchmarkTriggerError(`Invalid ActivityPub activity: ${error}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function asRecord(value, name) {
|
|
311
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) throw new BenchmarkTriggerError(`${name} must be an object.`);
|
|
312
|
+
return value;
|
|
313
|
+
}
|
|
314
|
+
function jsonResponse(body, status = 200) {
|
|
315
|
+
return new Response(JSON.stringify(body), {
|
|
316
|
+
status,
|
|
317
|
+
headers: { "Content-Type": "application/json" }
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function serializeScopeMetrics(resourceMetrics) {
|
|
321
|
+
return resourceMetrics.scopeMetrics.map(serializeScope);
|
|
322
|
+
}
|
|
323
|
+
function serializeScope(scopeMetrics) {
|
|
324
|
+
return {
|
|
325
|
+
scope: {
|
|
326
|
+
name: scopeMetrics.scope.name,
|
|
327
|
+
version: scopeMetrics.scope.version
|
|
328
|
+
},
|
|
329
|
+
metrics: scopeMetrics.metrics.map(serializeMetric)
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function serializeMetric(metric) {
|
|
333
|
+
return {
|
|
334
|
+
name: metric.descriptor.name,
|
|
335
|
+
description: metric.descriptor.description,
|
|
336
|
+
unit: metric.descriptor.unit,
|
|
337
|
+
dataPointType: serializeDataPointType(metric.dataPointType),
|
|
338
|
+
dataPoints: metric.dataPoints.map((point) => ({
|
|
339
|
+
attributes: { ...point.attributes },
|
|
340
|
+
startTime: point.startTime,
|
|
341
|
+
endTime: point.endTime,
|
|
342
|
+
value: point.value
|
|
343
|
+
}))
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function serializeDataPointType(dataPointType) {
|
|
347
|
+
switch (dataPointType) {
|
|
348
|
+
case DataPointType.HISTOGRAM: return "histogram";
|
|
349
|
+
case DataPointType.EXPONENTIAL_HISTOGRAM: return "exponential_histogram";
|
|
350
|
+
case DataPointType.GAUGE: return "gauge";
|
|
351
|
+
case DataPointType.SUM: return "sum";
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
//#endregion
|
|
156
355
|
//#region src/federation/inbox.ts
|
|
157
|
-
async function routeActivity({ context: ctx, json, activity, recipient, inboxListeners, inboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, tracerProvider, idempotencyStrategy }) {
|
|
356
|
+
async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
|
|
158
357
|
const logger = getLogger([
|
|
159
358
|
"fedify",
|
|
160
359
|
"federation",
|
|
@@ -191,6 +390,7 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
|
|
|
191
390
|
code: SpanStatusCode.UNSET,
|
|
192
391
|
message: `Activity ${activity.id?.href} has already been processed.`
|
|
193
392
|
});
|
|
393
|
+
recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
|
|
194
394
|
return "alreadyProcessed";
|
|
195
395
|
}
|
|
196
396
|
}
|
|
@@ -200,6 +400,7 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
|
|
|
200
400
|
code: SpanStatusCode.ERROR,
|
|
201
401
|
message: "Missing actor."
|
|
202
402
|
});
|
|
403
|
+
recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
|
|
203
404
|
return "missingActor";
|
|
204
405
|
}
|
|
205
406
|
span.setAttribute("activitypub.actor.id", activity.actorId.href);
|
|
@@ -211,7 +412,9 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
|
|
|
211
412
|
type: "inbox",
|
|
212
413
|
id: crypto.randomUUID(),
|
|
213
414
|
baseUrl: ctx.origin,
|
|
214
|
-
activity: json,
|
|
415
|
+
activity: originalJson ?? json,
|
|
416
|
+
...normalizedActivity == null ? {} : { normalizedActivity },
|
|
417
|
+
...ldSignatureVerified == null ? {} : { ldSignatureVerified },
|
|
215
418
|
identifier: recipient,
|
|
216
419
|
attempt: 0,
|
|
217
420
|
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -230,6 +433,12 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
|
|
|
230
433
|
});
|
|
231
434
|
throw error;
|
|
232
435
|
}
|
|
436
|
+
getFederationMetrics(meterProvider).recordQueueTaskEnqueued({
|
|
437
|
+
role: "inbox",
|
|
438
|
+
queue,
|
|
439
|
+
activityType: getTypeId(activity).href
|
|
440
|
+
}, 0);
|
|
441
|
+
recordInboxActivity(meterProvider, "queued", getTypeId(activity).href);
|
|
233
442
|
logger.info("Activity {activityId} is enqueued.", {
|
|
234
443
|
activityId: activity.id?.href,
|
|
235
444
|
activity: json,
|
|
@@ -249,13 +458,21 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
|
|
|
249
458
|
code: SpanStatusCode.UNSET,
|
|
250
459
|
message: `Unsupported activity type: ${getTypeId(activity).href}`
|
|
251
460
|
});
|
|
461
|
+
recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
|
|
252
462
|
span.end();
|
|
253
463
|
return "unsupportedActivity";
|
|
254
464
|
}
|
|
255
465
|
const { class: cls, listener } = dispatched;
|
|
256
466
|
span.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
|
|
257
467
|
try {
|
|
258
|
-
|
|
468
|
+
const activityType = getTypeId(activity).href;
|
|
469
|
+
const started = performance.now();
|
|
470
|
+
try {
|
|
471
|
+
const contextFactory = listenerInboxContextFactory ?? inboxContextFactory;
|
|
472
|
+
await listener(contextFactory(recipient, contextFactory === inboxContextFactory ? json : originalJson ?? json, activity.id?.href, activityType), activity);
|
|
473
|
+
} finally {
|
|
474
|
+
getFederationMetrics(meterProvider).recordInboxProcessingDuration(activityType, getDurationMs(started));
|
|
475
|
+
}
|
|
259
476
|
} catch (error) {
|
|
260
477
|
try {
|
|
261
478
|
await inboxErrorHandler?.(ctx, error);
|
|
@@ -277,9 +494,11 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
|
|
|
277
494
|
code: SpanStatusCode.ERROR,
|
|
278
495
|
message: String(error)
|
|
279
496
|
});
|
|
497
|
+
recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
|
|
280
498
|
span.end();
|
|
281
499
|
return "error";
|
|
282
500
|
}
|
|
501
|
+
recordInboxActivity(meterProvider, "processed", getTypeId(activity).href);
|
|
283
502
|
if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal.Duration.from({ days: 1 }) });
|
|
284
503
|
logger.info("Activity {activityId} has been processed.", {
|
|
285
504
|
activityId: activity.id?.href,
|
|
@@ -292,6 +511,33 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
|
|
|
292
511
|
}
|
|
293
512
|
//#endregion
|
|
294
513
|
//#region src/federation/handler.ts
|
|
514
|
+
const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
|
|
515
|
+
function isRemoteContextLoadingFailure$1(error) {
|
|
516
|
+
return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
|
|
517
|
+
}
|
|
518
|
+
function isPermanentRemoteContextError$1(error) {
|
|
519
|
+
if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
|
|
520
|
+
const details = error.details;
|
|
521
|
+
if (details?.code === "invalid remote context") return true;
|
|
522
|
+
return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) && isClearlyMalformedContextReference(details.url);
|
|
523
|
+
}
|
|
524
|
+
function isInvalidJsonLdError(error) {
|
|
525
|
+
if (!(error instanceof Error)) return false;
|
|
526
|
+
const name = error.name;
|
|
527
|
+
return name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
|
|
528
|
+
}
|
|
529
|
+
function isValidationTypeError(error) {
|
|
530
|
+
return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || isInvalidUrlTypeError(error));
|
|
531
|
+
}
|
|
532
|
+
function isPermanentActivityParseError(error) {
|
|
533
|
+
return isInvalidJsonLdError(error) || isValidationTypeError(error);
|
|
534
|
+
}
|
|
535
|
+
function hasHttpSignatureHeaders(request) {
|
|
536
|
+
return request.headers.has("Signature") || request.headers.has("Signature-Input");
|
|
537
|
+
}
|
|
538
|
+
function hasObjectIntegrityProof(json) {
|
|
539
|
+
return typeof json === "object" && json != null && "proof" in json;
|
|
540
|
+
}
|
|
295
541
|
/**
|
|
296
542
|
* Handles an actor request.
|
|
297
543
|
* @template TContextData The context data to pass to the context.
|
|
@@ -353,6 +599,33 @@ async function handleObject(request, { values, context, objectDispatcher, author
|
|
|
353
599
|
Vary: "Accept"
|
|
354
600
|
} });
|
|
355
601
|
}
|
|
602
|
+
const BUILT_IN_COLLECTION_METRIC_KINDS = new Set([
|
|
603
|
+
"inbox",
|
|
604
|
+
"outbox",
|
|
605
|
+
"following",
|
|
606
|
+
"followers",
|
|
607
|
+
"liked",
|
|
608
|
+
"featured",
|
|
609
|
+
"featured_tags"
|
|
610
|
+
]);
|
|
611
|
+
function getCollectionMetricKind(name) {
|
|
612
|
+
const normalized = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/\s+/g, "_");
|
|
613
|
+
return BUILT_IN_COLLECTION_METRIC_KINDS.has(normalized) ? normalized : "custom";
|
|
614
|
+
}
|
|
615
|
+
function collectionAttributes(base, result, response) {
|
|
616
|
+
return {
|
|
617
|
+
...base,
|
|
618
|
+
result,
|
|
619
|
+
...response == null ? {} : { statusCode: response.status }
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
function recordCollectionMetrics(meterProvider, base, result, options = {}) {
|
|
623
|
+
const attrs = collectionAttributes(base, result, options.response);
|
|
624
|
+
recordCollectionRequest(meterProvider, attrs);
|
|
625
|
+
if (options.dispatchDurationMs != null) recordCollectionDispatchDuration(meterProvider, options.dispatchDurationMs, attrs);
|
|
626
|
+
if (options.itemCount != null) recordCollectionPageItems(meterProvider, options.itemCount, attrs);
|
|
627
|
+
if (options.totalItems != null) recordCollectionTotalItems(meterProvider, options.totalItems, attrs);
|
|
628
|
+
}
|
|
356
629
|
/**
|
|
357
630
|
* Handles a collection request.
|
|
358
631
|
* @template TItem The type of items in the collection.
|
|
@@ -363,36 +636,118 @@ async function handleObject(request, { values, context, objectDispatcher, author
|
|
|
363
636
|
* @param parameters The parameters for handling the collection.
|
|
364
637
|
* @returns A promise that resolves to an HTTP response.
|
|
365
638
|
*/
|
|
366
|
-
async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, onUnauthorized, onNotFound }) {
|
|
639
|
+
async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
|
|
367
640
|
const spanName = name$1.trim().replace(/\s+/g, "_");
|
|
368
641
|
tracerProvider = tracerProvider ?? trace.getTracerProvider();
|
|
369
642
|
const tracer = tracerProvider.getTracer(name, version);
|
|
370
643
|
const cursor = new URL(request.url).searchParams.get("cursor");
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
644
|
+
const metricBase = {
|
|
645
|
+
kind: getCollectionMetricKind(name$1),
|
|
646
|
+
page: cursor != null,
|
|
647
|
+
dispatcher: "built_in"
|
|
648
|
+
};
|
|
649
|
+
let dispatchDurationMs;
|
|
650
|
+
let itemCount;
|
|
651
|
+
let totalItemCount;
|
|
652
|
+
const finish = (response, result) => {
|
|
653
|
+
recordCollectionMetrics(meterProvider, metricBase, result, {
|
|
654
|
+
response,
|
|
655
|
+
dispatchDurationMs,
|
|
656
|
+
itemCount,
|
|
657
|
+
totalItems: totalItemCount
|
|
658
|
+
});
|
|
659
|
+
return response;
|
|
660
|
+
};
|
|
661
|
+
try {
|
|
662
|
+
if (collectionCallbacks == null) return finish(await onNotFound(request), "not_found");
|
|
663
|
+
let collection;
|
|
664
|
+
const baseUri = uriGetter(identifier);
|
|
665
|
+
if (cursor == null) {
|
|
666
|
+
const firstCursor = await collectionCallbacks.firstCursor?.(context, identifier);
|
|
667
|
+
const totalItems = filter == null ? await collectionCallbacks.counter?.(context, identifier) : void 0;
|
|
668
|
+
totalItemCount = totalItems == null ? void 0 : Number(totalItems);
|
|
669
|
+
if (firstCursor == null) {
|
|
670
|
+
const itemsOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection ${spanName}`, {
|
|
671
|
+
kind: SpanKind.SERVER,
|
|
672
|
+
attributes: {
|
|
673
|
+
"activitypub.collection.id": baseUri.href,
|
|
674
|
+
"activitypub.collection.type": OrderedCollection.typeId.href
|
|
675
|
+
}
|
|
676
|
+
}, async (span) => {
|
|
677
|
+
if (totalItemCount != null) span.setAttribute("activitypub.collection.total_items", totalItemCount);
|
|
678
|
+
const started = performance.now();
|
|
679
|
+
try {
|
|
680
|
+
const page = await collectionCallbacks.dispatcher(context, identifier, null, filter);
|
|
681
|
+
dispatchDurationMs = getDurationMs(started);
|
|
682
|
+
if (page == null) {
|
|
683
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
684
|
+
return await onNotFound(request);
|
|
685
|
+
}
|
|
686
|
+
const items = filterCollectionItems(page.items, name$1, filterPredicate);
|
|
687
|
+
itemCount = items.length;
|
|
688
|
+
span.setAttribute("fedify.collection.items", itemCount);
|
|
689
|
+
return items;
|
|
690
|
+
} catch (e) {
|
|
691
|
+
if (dispatchDurationMs == null) dispatchDurationMs = getDurationMs(started);
|
|
692
|
+
span.setStatus({
|
|
693
|
+
code: SpanStatusCode.ERROR,
|
|
694
|
+
message: String(e)
|
|
695
|
+
});
|
|
696
|
+
throw e;
|
|
697
|
+
} finally {
|
|
698
|
+
span.end();
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
if (itemsOrResponse instanceof Response) return finish(itemsOrResponse, "not_found");
|
|
702
|
+
collection = new OrderedCollection({
|
|
703
|
+
id: baseUri,
|
|
704
|
+
totalItems: totalItemCount ?? null,
|
|
705
|
+
items: itemsOrResponse
|
|
706
|
+
});
|
|
707
|
+
} else {
|
|
708
|
+
const lastCursor = await collectionCallbacks.lastCursor?.(context, identifier);
|
|
709
|
+
const first = new URL(context.url);
|
|
710
|
+
first.searchParams.set("cursor", firstCursor);
|
|
711
|
+
let last = null;
|
|
712
|
+
if (lastCursor != null) {
|
|
713
|
+
last = new URL(context.url);
|
|
714
|
+
last.searchParams.set("cursor", lastCursor);
|
|
715
|
+
}
|
|
716
|
+
collection = new OrderedCollection({
|
|
717
|
+
id: baseUri,
|
|
718
|
+
totalItems: totalItemCount ?? null,
|
|
719
|
+
first,
|
|
720
|
+
last
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
const uri = new URL(baseUri);
|
|
725
|
+
uri.searchParams.set("cursor", cursor);
|
|
726
|
+
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
|
|
379
727
|
kind: SpanKind.SERVER,
|
|
380
728
|
attributes: {
|
|
381
|
-
"activitypub.collection.id":
|
|
382
|
-
"activitypub.collection.type":
|
|
729
|
+
"activitypub.collection.id": uri.href,
|
|
730
|
+
"activitypub.collection.type": OrderedCollectionPage.typeId.href,
|
|
731
|
+
"fedify.collection.cursor": cursor
|
|
383
732
|
}
|
|
384
733
|
}, async (span) => {
|
|
385
|
-
|
|
734
|
+
const started = performance.now();
|
|
386
735
|
try {
|
|
387
|
-
const page = await collectionCallbacks.dispatcher(context, identifier,
|
|
736
|
+
const page = await collectionCallbacks.dispatcher(context, identifier, cursor, filter);
|
|
737
|
+
dispatchDurationMs = getDurationMs(started);
|
|
388
738
|
if (page == null) {
|
|
389
739
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
390
740
|
return await onNotFound(request);
|
|
391
741
|
}
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
742
|
+
const items = filterCollectionItems(page.items, name$1, filterPredicate);
|
|
743
|
+
itemCount = items.length;
|
|
744
|
+
span.setAttribute("fedify.collection.items", itemCount);
|
|
745
|
+
return {
|
|
746
|
+
...page,
|
|
747
|
+
items
|
|
748
|
+
};
|
|
395
749
|
} catch (e) {
|
|
750
|
+
if (dispatchDurationMs == null) dispatchDurationMs = getDurationMs(started);
|
|
396
751
|
span.setStatus({
|
|
397
752
|
code: SpanStatusCode.ERROR,
|
|
398
753
|
message: String(e)
|
|
@@ -402,87 +757,44 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
402
757
|
span.end();
|
|
403
758
|
}
|
|
404
759
|
});
|
|
405
|
-
if (
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
} else {
|
|
412
|
-
const lastCursor = await collectionCallbacks.lastCursor?.(context, identifier);
|
|
413
|
-
const first = new URL(context.url);
|
|
414
|
-
first.searchParams.set("cursor", firstCursor);
|
|
415
|
-
let last = null;
|
|
416
|
-
if (lastCursor != null) {
|
|
417
|
-
last = new URL(context.url);
|
|
418
|
-
last.searchParams.set("cursor", lastCursor);
|
|
760
|
+
if (pageOrResponse instanceof Response) return finish(pageOrResponse, "not_found");
|
|
761
|
+
const { items, prevCursor, nextCursor } = pageOrResponse;
|
|
762
|
+
let prev = null;
|
|
763
|
+
if (prevCursor != null) {
|
|
764
|
+
prev = new URL(context.url);
|
|
765
|
+
prev.searchParams.set("cursor", prevCursor);
|
|
419
766
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
last
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
} else {
|
|
428
|
-
const uri = new URL(baseUri);
|
|
429
|
-
uri.searchParams.set("cursor", cursor);
|
|
430
|
-
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
|
|
431
|
-
kind: SpanKind.SERVER,
|
|
432
|
-
attributes: {
|
|
433
|
-
"activitypub.collection.id": uri.href,
|
|
434
|
-
"activitypub.collection.type": OrderedCollectionPage.typeId.href,
|
|
435
|
-
"fedify.collection.cursor": cursor
|
|
767
|
+
let next = null;
|
|
768
|
+
if (nextCursor != null) {
|
|
769
|
+
next = new URL(context.url);
|
|
770
|
+
next.searchParams.set("cursor", nextCursor);
|
|
436
771
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
} catch (e) {
|
|
447
|
-
span.setStatus({
|
|
448
|
-
code: SpanStatusCode.ERROR,
|
|
449
|
-
message: String(e)
|
|
450
|
-
});
|
|
451
|
-
throw e;
|
|
452
|
-
} finally {
|
|
453
|
-
span.end();
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
if (pageOrResponse instanceof Response) return pageOrResponse;
|
|
457
|
-
const { items, prevCursor, nextCursor } = pageOrResponse;
|
|
458
|
-
let prev = null;
|
|
459
|
-
if (prevCursor != null) {
|
|
460
|
-
prev = new URL(context.url);
|
|
461
|
-
prev.searchParams.set("cursor", prevCursor);
|
|
772
|
+
const partOf = new URL(context.url);
|
|
773
|
+
partOf.searchParams.delete("cursor");
|
|
774
|
+
collection = new OrderedCollectionPage({
|
|
775
|
+
id: uri,
|
|
776
|
+
prev,
|
|
777
|
+
next,
|
|
778
|
+
items,
|
|
779
|
+
partOf
|
|
780
|
+
});
|
|
462
781
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
next = new URL(context.url);
|
|
466
|
-
next.searchParams.set("cursor", nextCursor);
|
|
782
|
+
if (collectionCallbacks.authorizePredicate != null) {
|
|
783
|
+
if (!await collectionCallbacks.authorizePredicate(context, identifier)) return finish(await onUnauthorized(request), "unauthorized");
|
|
467
784
|
}
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
785
|
+
const jsonLd = await collection.toJsonLd(context);
|
|
786
|
+
return finish(new Response(JSON.stringify(jsonLd), { headers: {
|
|
787
|
+
"Content-Type": "application/activity+json",
|
|
788
|
+
Vary: "Accept"
|
|
789
|
+
} }), "served");
|
|
790
|
+
} catch (e) {
|
|
791
|
+
recordCollectionMetrics(meterProvider, metricBase, "error", {
|
|
792
|
+
dispatchDurationMs,
|
|
793
|
+
itemCount,
|
|
794
|
+
totalItems: totalItemCount
|
|
476
795
|
});
|
|
796
|
+
throw e;
|
|
477
797
|
}
|
|
478
|
-
if (collectionCallbacks.authorizePredicate != null) {
|
|
479
|
-
if (!await collectionCallbacks.authorizePredicate(context, identifier)) return await onUnauthorized(request);
|
|
480
|
-
}
|
|
481
|
-
const jsonLd = await collection.toJsonLd(context);
|
|
482
|
-
return new Response(JSON.stringify(jsonLd), { headers: {
|
|
483
|
-
"Content-Type": "application/activity+json",
|
|
484
|
-
Vary: "Accept"
|
|
485
|
-
} });
|
|
486
798
|
}
|
|
487
799
|
/**
|
|
488
800
|
* Filters collection items based on the provided predicate.
|
|
@@ -746,7 +1058,7 @@ async function handleInbox(request, options) {
|
|
|
746
1058
|
* @returns A promise that resolves to an HTTP response.
|
|
747
1059
|
*/
|
|
748
1060
|
async function handleInboxInternal(request, parameters, span) {
|
|
749
|
-
const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, tracerProvider } = parameters;
|
|
1061
|
+
const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider } = parameters;
|
|
750
1062
|
const logger = getLogger([
|
|
751
1063
|
"fedify",
|
|
752
1064
|
"federation",
|
|
@@ -818,28 +1130,105 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
818
1130
|
});
|
|
819
1131
|
}
|
|
820
1132
|
const keyCache = new KvKeyCache(kv, kvPrefixes.publicKey, ctx);
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1133
|
+
const jsonWithoutSig = detachSignature(json);
|
|
1134
|
+
const hasLdSignature = hasSignature(json);
|
|
1135
|
+
const canAttemptAlternateAuthAfterLdSignatureFailure = skipSignatureVerification || hasHttpSignatureHeaders(request) || hasObjectIntegrityProof(jsonWithoutSig);
|
|
1136
|
+
let deferredLdSignatureError = void 0;
|
|
1137
|
+
const respondInvalidActivity = async (error) => {
|
|
1138
|
+
logger.error("Failed to parse activity:\n{error}", {
|
|
1139
|
+
recipient,
|
|
1140
|
+
activity: json,
|
|
1141
|
+
error
|
|
828
1142
|
});
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1143
|
+
try {
|
|
1144
|
+
await inboxErrorHandler?.(ctx, error);
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
|
|
1147
|
+
error,
|
|
1148
|
+
activity: json,
|
|
1149
|
+
recipient
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
span.setStatus({
|
|
1153
|
+
code: SpanStatusCode.ERROR,
|
|
1154
|
+
message: `Failed to parse activity:\n${error}`
|
|
1155
|
+
});
|
|
1156
|
+
return new Response("Invalid activity.", {
|
|
1157
|
+
status: 400,
|
|
1158
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1159
|
+
});
|
|
1160
|
+
};
|
|
1161
|
+
let compactedJson = json;
|
|
1162
|
+
let compactedJsonWithoutSig = jsonWithoutSig;
|
|
1163
|
+
let ldSigVerified = false;
|
|
1164
|
+
if (hasLdSignature) {
|
|
1165
|
+
try {
|
|
1166
|
+
compactedJson = await compactJsonLd(json, ctx.contextLoader);
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
if (isInvalidJsonLdError(error)) {
|
|
1169
|
+
logger.error("Failed to parse JSON-LD:\n{error}", {
|
|
1170
|
+
recipient,
|
|
1171
|
+
error
|
|
1172
|
+
});
|
|
1173
|
+
return new Response("Invalid JSON-LD.", {
|
|
1174
|
+
status: 400,
|
|
1175
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
|
|
1179
|
+
if (!skipSignatureVerification) deferredLdSignatureError = error;
|
|
1180
|
+
logger.debug("Failed to normalize JSON-LD for Linked Data Signatures; deferring to another authentication path only if it verifies:\n{error}", {
|
|
832
1181
|
recipient,
|
|
833
1182
|
error
|
|
834
1183
|
});
|
|
835
|
-
return new Response("Invalid JSON-LD.", {
|
|
836
|
-
status: 400,
|
|
837
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
838
|
-
});
|
|
839
1184
|
}
|
|
840
|
-
|
|
1185
|
+
if (compactedJson !== json) {
|
|
1186
|
+
compactedJsonWithoutSig = detachSignature(compactedJson);
|
|
1187
|
+
try {
|
|
1188
|
+
ldSigVerified = await verifyCompactJsonLd(compactedJson, {
|
|
1189
|
+
contextLoader: ctx.contextLoader,
|
|
1190
|
+
documentLoader: ctx.documentLoader,
|
|
1191
|
+
keyCache,
|
|
1192
|
+
meterProvider,
|
|
1193
|
+
tracerProvider
|
|
1194
|
+
});
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(error);
|
|
1197
|
+
if (isInvalidJsonLdError(error)) {
|
|
1198
|
+
logger.error("Failed to parse JSON-LD:\n{error}", {
|
|
1199
|
+
recipient,
|
|
1200
|
+
error
|
|
1201
|
+
});
|
|
1202
|
+
return new Response("Invalid JSON-LD.", {
|
|
1203
|
+
status: 400,
|
|
1204
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
|
|
1208
|
+
if (!skipSignatureVerification) try {
|
|
1209
|
+
await Object$1.fromJsonLd(compactedJson, {
|
|
1210
|
+
contextLoader: getNormalizationContextLoader(ctx.contextLoader),
|
|
1211
|
+
documentLoader: ctx.documentLoader,
|
|
1212
|
+
tracerProvider
|
|
1213
|
+
});
|
|
1214
|
+
} catch (parseError) {
|
|
1215
|
+
if (parseError instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(parseError);
|
|
1216
|
+
if (isInvalidJsonLdError(parseError)) {
|
|
1217
|
+
logger.error("Failed to parse JSON-LD:\n{error}", {
|
|
1218
|
+
recipient,
|
|
1219
|
+
error: parseError
|
|
1220
|
+
});
|
|
1221
|
+
return new Response("Invalid JSON-LD.", {
|
|
1222
|
+
status: 400,
|
|
1223
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
deferredLdSignatureError = parseError;
|
|
1227
|
+
}
|
|
1228
|
+
ldSigVerified = false;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
841
1231
|
}
|
|
842
|
-
const jsonWithoutSig = detachSignature(json);
|
|
843
1232
|
let activity = null;
|
|
844
1233
|
let activityVerified = false;
|
|
845
1234
|
if (ldSigVerified) {
|
|
@@ -847,7 +1236,16 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
847
1236
|
recipient,
|
|
848
1237
|
json
|
|
849
1238
|
});
|
|
850
|
-
|
|
1239
|
+
try {
|
|
1240
|
+
activity = await Activity.fromJsonLd(compactedJsonWithoutSig, {
|
|
1241
|
+
...ctx,
|
|
1242
|
+
contextLoader: getNormalizationContextLoader(ctx.contextLoader)
|
|
1243
|
+
});
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(error);
|
|
1246
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
1247
|
+
return await respondInvalidActivity(error);
|
|
1248
|
+
}
|
|
851
1249
|
activityVerified = true;
|
|
852
1250
|
} else {
|
|
853
1251
|
logger.debug("Linked Data Signatures are not verified.", {
|
|
@@ -856,12 +1254,22 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
856
1254
|
});
|
|
857
1255
|
try {
|
|
858
1256
|
activity = await verifyObject(Activity, jsonWithoutSig, {
|
|
859
|
-
contextLoader: ctx.contextLoader,
|
|
1257
|
+
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
860
1258
|
documentLoader: ctx.documentLoader,
|
|
861
1259
|
keyCache,
|
|
1260
|
+
meterProvider,
|
|
862
1261
|
tracerProvider
|
|
863
1262
|
});
|
|
864
1263
|
} catch (error) {
|
|
1264
|
+
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(jsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(error);
|
|
1265
|
+
if (deferredLdSignatureError != null) {
|
|
1266
|
+
logger.debug("Object Integrity Proof fallback did not supersede a deferred Linked Data Signature failure:\n{error}", {
|
|
1267
|
+
recipient,
|
|
1268
|
+
error
|
|
1269
|
+
});
|
|
1270
|
+
activity = null;
|
|
1271
|
+
}
|
|
1272
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
865
1273
|
logger.error("Failed to parse activity:\n{error}", {
|
|
866
1274
|
recipient,
|
|
867
1275
|
activity: json,
|
|
@@ -906,10 +1314,14 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
906
1314
|
documentLoader: ctx.documentLoader,
|
|
907
1315
|
timeWindow: signatureTimeWindow,
|
|
908
1316
|
keyCache,
|
|
1317
|
+
meterProvider,
|
|
909
1318
|
tracerProvider
|
|
910
1319
|
});
|
|
911
1320
|
if (verification.verified === false) {
|
|
1321
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
912
1322
|
const reason = verification.reason;
|
|
1323
|
+
const remoteHost = "keyId" in reason && reason.keyId != null ? getRemoteHost(reason.keyId) : void 0;
|
|
1324
|
+
getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure(reason.type, remoteHost);
|
|
913
1325
|
logger.error("Failed to verify the request's HTTP Signatures.", {
|
|
914
1326
|
recipient,
|
|
915
1327
|
reason: reason.type,
|
|
@@ -984,7 +1396,15 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
984
1396
|
}
|
|
985
1397
|
httpSigKey = verification.key;
|
|
986
1398
|
}
|
|
987
|
-
|
|
1399
|
+
try {
|
|
1400
|
+
activity = await Activity.fromJsonLd(jsonWithoutSig, {
|
|
1401
|
+
...ctx,
|
|
1402
|
+
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader)
|
|
1403
|
+
});
|
|
1404
|
+
} catch (error) {
|
|
1405
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
1406
|
+
return await respondInvalidActivity(error);
|
|
1407
|
+
}
|
|
988
1408
|
}
|
|
989
1409
|
if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
|
|
990
1410
|
span.setAttribute("activitypub.activity.type", getTypeId(activity).href);
|
|
@@ -996,6 +1416,8 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
996
1416
|
"http_signatures.key_id": httpSigKey?.id?.href ?? ""
|
|
997
1417
|
});
|
|
998
1418
|
if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
|
|
1419
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
1420
|
+
getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : getRemoteHost(httpSigKey.id));
|
|
999
1421
|
logger.error("The signer ({keyId}) and the actor ({actorId}) do not match.", {
|
|
1000
1422
|
activity: json,
|
|
1001
1423
|
recipient,
|
|
@@ -1013,6 +1435,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1013
1435
|
}
|
|
1014
1436
|
if (pendingNonceLabel != null) {
|
|
1015
1437
|
if (!await verifySignatureNonce(request, kv, kvPrefixes.acceptSignatureNonce, pendingNonceLabel)) {
|
|
1438
|
+
getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("invalidNonce", httpSigKey?.id == null ? void 0 : getRemoteHost(httpSigKey.id));
|
|
1016
1439
|
logger.error("Signature nonce verification failed (missing, expired, or replayed).", { recipient });
|
|
1017
1440
|
return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
|
|
1018
1441
|
}
|
|
@@ -1020,15 +1443,20 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1020
1443
|
const routeResult = await routeActivity({
|
|
1021
1444
|
context: ctx,
|
|
1022
1445
|
json,
|
|
1446
|
+
originalJson: json,
|
|
1447
|
+
normalizedActivity: hasLdSignature && compactedJson !== json ? compactedJson : void 0,
|
|
1448
|
+
ldSignatureVerified: hasLdSignature ? ldSigVerified : void 0,
|
|
1023
1449
|
activity,
|
|
1024
1450
|
recipient,
|
|
1025
1451
|
inboxListeners,
|
|
1026
1452
|
inboxContextFactory,
|
|
1453
|
+
listenerInboxContextFactory: ldSigVerified ? inboxContextFactory[rawInboxContextFactorySymbol] : void 0,
|
|
1027
1454
|
inboxErrorHandler,
|
|
1028
1455
|
kv,
|
|
1029
1456
|
kvPrefixes,
|
|
1030
1457
|
queue,
|
|
1031
1458
|
span,
|
|
1459
|
+
meterProvider: parameters.meterProvider,
|
|
1032
1460
|
tracerProvider,
|
|
1033
1461
|
idempotencyStrategy: parameters.idempotencyStrategy
|
|
1034
1462
|
});
|
|
@@ -1069,11 +1497,31 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1069
1497
|
* @since 1.8.0
|
|
1070
1498
|
*/
|
|
1071
1499
|
const handleCustomCollection = exceptWrapper(_handleCustomCollection);
|
|
1072
|
-
|
|
1500
|
+
const pendingCollectionMetricRecorders = /* @__PURE__ */ new WeakMap();
|
|
1501
|
+
function deferPendingCollectionMetrics(error, recorder) {
|
|
1502
|
+
if (error == null || typeof error !== "object" && typeof error !== "function") return false;
|
|
1503
|
+
pendingCollectionMetricRecorders.set(error, recorder);
|
|
1504
|
+
return true;
|
|
1505
|
+
}
|
|
1506
|
+
function recordDeferredPendingCollectionMetrics(error, result, response) {
|
|
1507
|
+
if (error == null || typeof error !== "object" && typeof error !== "function") return;
|
|
1508
|
+
const recorder = pendingCollectionMetricRecorders.get(error);
|
|
1509
|
+
pendingCollectionMetricRecorders.delete(error);
|
|
1510
|
+
recorder?.(result, response);
|
|
1511
|
+
}
|
|
1512
|
+
async function _handleCustomCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate }) {
|
|
1073
1513
|
verifyDefined(callbacks);
|
|
1074
1514
|
await authIfNeeded(context, values, callbacks);
|
|
1075
1515
|
const cursor = new URL(request.url).searchParams.get("cursor");
|
|
1076
|
-
|
|
1516
|
+
const handler = new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, Collection, CollectionPage, filterPredicate).fetchCollection(cursor);
|
|
1517
|
+
try {
|
|
1518
|
+
const response = await handler.toJsonLd().then(respondAsActivity);
|
|
1519
|
+
handler.recordPendingCollectionMetrics("served", response);
|
|
1520
|
+
return response;
|
|
1521
|
+
} catch (e) {
|
|
1522
|
+
if (!deferPendingCollectionMetrics(e, (result, response) => handler.recordPendingCollectionMetrics(result, response))) handler.recordPendingCollectionMetrics("error");
|
|
1523
|
+
throw e;
|
|
1524
|
+
}
|
|
1077
1525
|
}
|
|
1078
1526
|
/**
|
|
1079
1527
|
* Handles an ordered collection request.
|
|
@@ -1087,11 +1535,19 @@ async function _handleCustomCollection(request, { name, values, context, tracerP
|
|
|
1087
1535
|
* @since 1.8.0
|
|
1088
1536
|
*/
|
|
1089
1537
|
const handleOrderedCollection = exceptWrapper(_handleOrderedCollection);
|
|
1090
|
-
async function _handleOrderedCollection(request, { name, values, context, tracerProvider, collectionCallbacks: callbacks, filterPredicate }) {
|
|
1538
|
+
async function _handleOrderedCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate }) {
|
|
1091
1539
|
verifyDefined(callbacks);
|
|
1092
1540
|
await authIfNeeded(context, values, callbacks);
|
|
1093
1541
|
const cursor = new URL(request.url).searchParams.get("cursor");
|
|
1094
|
-
|
|
1542
|
+
const handler = new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, OrderedCollection, OrderedCollectionPage, filterPredicate).fetchCollection(cursor);
|
|
1543
|
+
try {
|
|
1544
|
+
const response = await handler.toJsonLd().then(respondAsActivity);
|
|
1545
|
+
handler.recordPendingCollectionMetrics("served", response);
|
|
1546
|
+
return response;
|
|
1547
|
+
} catch (e) {
|
|
1548
|
+
if (!deferPendingCollectionMetrics(e, (result, response) => handler.recordPendingCollectionMetrics(result, response))) handler.recordPendingCollectionMetrics("error");
|
|
1549
|
+
throw e;
|
|
1550
|
+
}
|
|
1095
1551
|
}
|
|
1096
1552
|
/**
|
|
1097
1553
|
* Handling custom collections with support for pagination and filtering.
|
|
@@ -1106,6 +1562,15 @@ async function _handleOrderedCollection(request, { name, values, context, tracer
|
|
|
1106
1562
|
* @since 1.8.0
|
|
1107
1563
|
*/
|
|
1108
1564
|
var CustomCollectionHandler = class {
|
|
1565
|
+
name;
|
|
1566
|
+
values;
|
|
1567
|
+
context;
|
|
1568
|
+
callbacks;
|
|
1569
|
+
tracerProvider;
|
|
1570
|
+
meterProvider;
|
|
1571
|
+
Collection;
|
|
1572
|
+
CollectionPage;
|
|
1573
|
+
filterPredicate;
|
|
1109
1574
|
/**
|
|
1110
1575
|
* The tracer for telemetry.
|
|
1111
1576
|
* @type {Tracer}
|
|
@@ -1130,6 +1595,7 @@ var CustomCollectionHandler = class {
|
|
|
1130
1595
|
*/
|
|
1131
1596
|
#dispatcher;
|
|
1132
1597
|
#collection = null;
|
|
1598
|
+
#pendingCollectionMetrics = [];
|
|
1133
1599
|
/**
|
|
1134
1600
|
* Creates a new CustomCollection instance.
|
|
1135
1601
|
* @param name The name of the collection.
|
|
@@ -1141,12 +1607,13 @@ var CustomCollectionHandler = class {
|
|
|
1141
1607
|
* @param CollectionPage The CollectionPage constructor.
|
|
1142
1608
|
* @param filterPredicate Optional filter predicate for items.
|
|
1143
1609
|
*/
|
|
1144
|
-
constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), Collection, CollectionPage, filterPredicate) {
|
|
1610
|
+
constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
|
|
1145
1611
|
this.name = name$2;
|
|
1146
1612
|
this.values = values;
|
|
1147
1613
|
this.context = context;
|
|
1148
1614
|
this.callbacks = callbacks;
|
|
1149
1615
|
this.tracerProvider = tracerProvider;
|
|
1616
|
+
this.meterProvider = meterProvider;
|
|
1150
1617
|
this.Collection = Collection;
|
|
1151
1618
|
this.CollectionPage = CollectionPage;
|
|
1152
1619
|
this.filterPredicate = filterPredicate;
|
|
@@ -1199,10 +1666,12 @@ var CustomCollectionHandler = class {
|
|
|
1199
1666
|
const { prevCursor, nextCursor } = pages;
|
|
1200
1667
|
const partOf = new URL(id);
|
|
1201
1668
|
partOf.searchParams.delete("cursor");
|
|
1669
|
+
const items = this.filterItems(pages.items);
|
|
1670
|
+
this.recordPendingCollectionItemCount(true, items.length);
|
|
1202
1671
|
return {
|
|
1203
1672
|
id,
|
|
1204
1673
|
partOf,
|
|
1205
|
-
items
|
|
1674
|
+
items,
|
|
1206
1675
|
prev: this.appendToUrl(prevCursor),
|
|
1207
1676
|
next: this.appendToUrl(nextCursor)
|
|
1208
1677
|
};
|
|
@@ -1215,11 +1684,16 @@ var CustomCollectionHandler = class {
|
|
|
1215
1684
|
*/
|
|
1216
1685
|
async getProps(firstCursor) {
|
|
1217
1686
|
const lastCursor = await this.callbacks.lastCursor?.(this.context, this.values);
|
|
1687
|
+
const totalItems = await this.totalItems;
|
|
1688
|
+
if (totalItems != null) this.#pendingCollectionMetrics.push({
|
|
1689
|
+
page: false,
|
|
1690
|
+
totalItems: Number(totalItems)
|
|
1691
|
+
});
|
|
1218
1692
|
return {
|
|
1219
1693
|
id: this.#id,
|
|
1220
1694
|
first: this.appendToUrl(firstCursor),
|
|
1221
1695
|
last: this.appendToUrl(lastCursor),
|
|
1222
|
-
totalItems
|
|
1696
|
+
totalItems
|
|
1223
1697
|
};
|
|
1224
1698
|
}
|
|
1225
1699
|
/**
|
|
@@ -1229,10 +1703,12 @@ var CustomCollectionHandler = class {
|
|
|
1229
1703
|
async getPropsWithoutCursor() {
|
|
1230
1704
|
const totalItems = await this.totalItems;
|
|
1231
1705
|
const pages = await this.getPages({ totalItems });
|
|
1706
|
+
const items = this.filterItems(pages.items);
|
|
1707
|
+
this.recordPendingCollectionItemCount(false, items.length);
|
|
1232
1708
|
return {
|
|
1233
1709
|
id: this.#id,
|
|
1234
1710
|
totalItems,
|
|
1235
|
-
items
|
|
1711
|
+
items
|
|
1236
1712
|
};
|
|
1237
1713
|
}
|
|
1238
1714
|
/**
|
|
@@ -1267,12 +1743,24 @@ var CustomCollectionHandler = class {
|
|
|
1267
1743
|
* @returns A function that handles the span operation.
|
|
1268
1744
|
*/
|
|
1269
1745
|
spanPages = ({ totalItems = null, cursor = null }) => async (span) => {
|
|
1746
|
+
const pageMetricBase = this.metricBase(cursor !== null);
|
|
1747
|
+
const started = performance.now();
|
|
1270
1748
|
try {
|
|
1271
1749
|
if (totalItems !== null) span.setAttribute(this.ATTRS.TOTAL_ITEMS, totalItems);
|
|
1272
1750
|
const page = await this.dispatch(cursor);
|
|
1751
|
+
const durationMs = getDurationMs(started);
|
|
1273
1752
|
span.setAttribute(this.ATTRS.ITEMS, page.items.length);
|
|
1753
|
+
this.#pendingCollectionMetrics.push({
|
|
1754
|
+
page: pageMetricBase.page,
|
|
1755
|
+
dispatchDurationMs: durationMs,
|
|
1756
|
+
totalItems: totalItems == null ? void 0 : Number(totalItems)
|
|
1757
|
+
});
|
|
1274
1758
|
return page;
|
|
1275
1759
|
} catch (e) {
|
|
1760
|
+
this.#pendingCollectionMetrics.push({
|
|
1761
|
+
page: cursor !== null,
|
|
1762
|
+
dispatchDurationMs: getDurationMs(started)
|
|
1763
|
+
});
|
|
1276
1764
|
const message = e instanceof Error ? e.message : String(e);
|
|
1277
1765
|
span.setStatus({
|
|
1278
1766
|
code: SpanStatusCode.ERROR,
|
|
@@ -1299,6 +1787,37 @@ var CustomCollectionHandler = class {
|
|
|
1299
1787
|
filterItems(items) {
|
|
1300
1788
|
return filterCollectionItems(items, this.name, this.filterPredicate);
|
|
1301
1789
|
}
|
|
1790
|
+
metricBase(page) {
|
|
1791
|
+
return {
|
|
1792
|
+
kind: "custom",
|
|
1793
|
+
page,
|
|
1794
|
+
dispatcher: "custom"
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
metricAttributes(page, result, response) {
|
|
1798
|
+
return collectionAttributes(this.metricBase(page), result, response);
|
|
1799
|
+
}
|
|
1800
|
+
recordPendingCollectionItemCount(page, itemCount) {
|
|
1801
|
+
for (let i = this.#pendingCollectionMetrics.length - 1; i >= 0; i--) {
|
|
1802
|
+
const measurement = this.#pendingCollectionMetrics[i];
|
|
1803
|
+
if (measurement.page === page && measurement.dispatchDurationMs != null && measurement.itemCount == null) {
|
|
1804
|
+
measurement.itemCount = itemCount;
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
this.#pendingCollectionMetrics.push({
|
|
1809
|
+
page,
|
|
1810
|
+
itemCount
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
recordPendingCollectionMetrics(result, response) {
|
|
1814
|
+
for (const measurement of this.#pendingCollectionMetrics.splice(0)) {
|
|
1815
|
+
const attrs = this.metricAttributes(measurement.page, result, response);
|
|
1816
|
+
if (measurement.dispatchDurationMs != null) recordCollectionDispatchDuration(this.meterProvider, measurement.dispatchDurationMs, attrs);
|
|
1817
|
+
if (measurement.itemCount != null) recordCollectionPageItems(this.meterProvider, measurement.itemCount, attrs);
|
|
1818
|
+
if (measurement.totalItems != null) recordCollectionTotalItems(this.meterProvider, measurement.totalItems, attrs);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1302
1821
|
/**
|
|
1303
1822
|
* Appends a cursor to the URL if it exists.
|
|
1304
1823
|
* @param cursor The cursor to append, or null/undefined.
|
|
@@ -1362,14 +1881,36 @@ var CustomCollectionHandler = class {
|
|
|
1362
1881
|
*/
|
|
1363
1882
|
function exceptWrapper(handler) {
|
|
1364
1883
|
return async (request, handlerParams) => {
|
|
1884
|
+
const page = new URL(request.url).searchParams.get("cursor") != null;
|
|
1885
|
+
const { meterProvider } = handlerParams;
|
|
1886
|
+
const metricBase = {
|
|
1887
|
+
kind: "custom",
|
|
1888
|
+
page,
|
|
1889
|
+
dispatcher: "custom"
|
|
1890
|
+
};
|
|
1365
1891
|
try {
|
|
1366
|
-
|
|
1892
|
+
const response = await handler(request, handlerParams);
|
|
1893
|
+
recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "served", response));
|
|
1894
|
+
return response;
|
|
1367
1895
|
} catch (error) {
|
|
1368
1896
|
const { onNotFound, onUnauthorized } = handlerParams;
|
|
1369
1897
|
switch (error?.constructor) {
|
|
1370
|
-
case ItemsNotFoundError:
|
|
1371
|
-
|
|
1372
|
-
|
|
1898
|
+
case ItemsNotFoundError: {
|
|
1899
|
+
const response = await onNotFound(request);
|
|
1900
|
+
recordDeferredPendingCollectionMetrics(error, "not_found", response);
|
|
1901
|
+
recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "not_found", response));
|
|
1902
|
+
return response;
|
|
1903
|
+
}
|
|
1904
|
+
case UnauthorizedError: {
|
|
1905
|
+
const response = await onUnauthorized(request);
|
|
1906
|
+
recordDeferredPendingCollectionMetrics(error, "unauthorized", response);
|
|
1907
|
+
recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "unauthorized", response));
|
|
1908
|
+
return response;
|
|
1909
|
+
}
|
|
1910
|
+
default:
|
|
1911
|
+
recordDeferredPendingCollectionMetrics(error, "error");
|
|
1912
|
+
recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "error"));
|
|
1913
|
+
throw error;
|
|
1373
1914
|
}
|
|
1374
1915
|
}
|
|
1375
1916
|
};
|
|
@@ -1566,22 +2107,72 @@ const logger = getLogger([
|
|
|
1566
2107
|
* @returns The response to the request.
|
|
1567
2108
|
*/
|
|
1568
2109
|
async function handleWebFinger(request, options) {
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
});
|
|
1580
|
-
throw error;
|
|
1581
|
-
} finally {
|
|
1582
|
-
span.end();
|
|
2110
|
+
const meterProvider = options.meterProvider;
|
|
2111
|
+
const start = meterProvider == null ? 0 : performance.now();
|
|
2112
|
+
const scheme = computeResourceScheme(options.context.url.searchParams.get("resource"));
|
|
2113
|
+
let notFoundResponse;
|
|
2114
|
+
const wrappedOptions = {
|
|
2115
|
+
...options,
|
|
2116
|
+
async onNotFound(req) {
|
|
2117
|
+
const r = await options.onNotFound(req);
|
|
2118
|
+
notFoundResponse = r;
|
|
2119
|
+
return r;
|
|
1583
2120
|
}
|
|
1584
|
-
}
|
|
2121
|
+
};
|
|
2122
|
+
let response;
|
|
2123
|
+
try {
|
|
2124
|
+
if (options.tracer == null) response = await handleWebFingerInternal(request, wrappedOptions);
|
|
2125
|
+
else response = await options.tracer.startActiveSpan("webfinger.handle", { kind: SpanKind.SERVER }, async (span) => {
|
|
2126
|
+
try {
|
|
2127
|
+
const inner = await handleWebFingerInternal(request, wrappedOptions);
|
|
2128
|
+
span.setStatus({ code: inner.ok ? SpanStatusCode.UNSET : SpanStatusCode.ERROR });
|
|
2129
|
+
return inner;
|
|
2130
|
+
} catch (error) {
|
|
2131
|
+
span.setStatus({
|
|
2132
|
+
code: SpanStatusCode.ERROR,
|
|
2133
|
+
message: String(error)
|
|
2134
|
+
});
|
|
2135
|
+
throw error;
|
|
2136
|
+
} finally {
|
|
2137
|
+
span.end();
|
|
2138
|
+
}
|
|
2139
|
+
});
|
|
2140
|
+
return response;
|
|
2141
|
+
} finally {
|
|
2142
|
+
if (meterProvider != null) recordWebFingerHandle(meterProvider, {
|
|
2143
|
+
durationMs: Math.max(0, performance.now() - start),
|
|
2144
|
+
result: classifyWebFingerHandleResult(response, notFoundResponse),
|
|
2145
|
+
scheme,
|
|
2146
|
+
statusCode: response?.status
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
const WEBFINGER_HANDLE_SCHEME_WHITELIST = new Set([
|
|
2151
|
+
"acct",
|
|
2152
|
+
"http",
|
|
2153
|
+
"https",
|
|
2154
|
+
"mailto"
|
|
2155
|
+
]);
|
|
2156
|
+
function isAllowedResourceScheme(scheme) {
|
|
2157
|
+
return WEBFINGER_HANDLE_SCHEME_WHITELIST.has(scheme);
|
|
2158
|
+
}
|
|
2159
|
+
function computeResourceScheme(resource) {
|
|
2160
|
+
if (resource == null) return void 0;
|
|
2161
|
+
const colon = resource.indexOf(":");
|
|
2162
|
+
if (colon <= 0) return void 0;
|
|
2163
|
+
const candidate = resource.substring(0, colon).toLowerCase();
|
|
2164
|
+
return isAllowedResourceScheme(candidate) ? candidate : "other";
|
|
2165
|
+
}
|
|
2166
|
+
function classifyWebFingerHandleResult(response, notFoundResponse) {
|
|
2167
|
+
if (response == null) return "error";
|
|
2168
|
+
if (notFoundResponse != null && response === notFoundResponse) return "not_found";
|
|
2169
|
+
switch (response.status) {
|
|
2170
|
+
case 200: return "resolved";
|
|
2171
|
+
case 400: return "invalid";
|
|
2172
|
+
case 404: return "not_found";
|
|
2173
|
+
case 410: return "tombstoned";
|
|
2174
|
+
default: return "error";
|
|
2175
|
+
}
|
|
1585
2176
|
}
|
|
1586
2177
|
async function handleWebFingerInternal(request, { context, host, actorDispatcher, actorHandleMapper, actorAliasMapper, onNotFound, span, webFingerLinksDispatcher }) {
|
|
1587
2178
|
if (actorDispatcher == null) {
|
|
@@ -1689,10 +2280,124 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
|
|
|
1689
2280
|
}
|
|
1690
2281
|
//#endregion
|
|
1691
2282
|
//#region src/federation/middleware.ts
|
|
2283
|
+
const circuitBreakerCasWarningKvStores = /* @__PURE__ */ new WeakSet();
|
|
2284
|
+
let nextQueueDepthGaugeSourceId = 0;
|
|
2285
|
+
const retryAfterHttpDate = /* @__PURE__ */ new RegExp("^(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \\d{2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \\d{4} \\d{2}:\\d{2}:\\d{2} GMT|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), \\d{2}-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\\d{2} \\d{2}:\\d{2}:\\d{2} GMT|(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (?: \\d|\\d{2}) \\d{2}:\\d{2}:\\d{2} \\d{4})$");
|
|
2286
|
+
function parseRetryAfter(headers, now = Temporal.Now.instant()) {
|
|
2287
|
+
const value = headers.get("Retry-After");
|
|
2288
|
+
if (value == null) return void 0;
|
|
2289
|
+
const trimmed = value.trim();
|
|
2290
|
+
if (/^\d+$/.test(trimmed)) {
|
|
2291
|
+
const seconds = Number(trimmed);
|
|
2292
|
+
if (!Number.isFinite(seconds)) return void 0;
|
|
2293
|
+
return parseRetryAfterDuration({ seconds });
|
|
2294
|
+
}
|
|
2295
|
+
if (!retryAfterHttpDate.test(trimmed)) return void 0;
|
|
2296
|
+
const httpDate = trimmed.endsWith("GMT") ? trimmed : `${trimmed} GMT`;
|
|
2297
|
+
const retryAtMs = Date.parse(httpDate);
|
|
2298
|
+
if (Number.isNaN(retryAtMs)) return void 0;
|
|
2299
|
+
const nowMs = Number(now.epochMilliseconds);
|
|
2300
|
+
return parseRetryAfterDuration({ milliseconds: Math.max(0, retryAtMs - nowMs) });
|
|
2301
|
+
}
|
|
2302
|
+
function parseRetryAfterDuration(durationLike) {
|
|
2303
|
+
try {
|
|
2304
|
+
return Temporal.Duration.from(durationLike);
|
|
2305
|
+
} catch (error) {
|
|
2306
|
+
if (error instanceof RangeError) return void 0;
|
|
2307
|
+
throw error;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
function clampNegativeDelay(delay) {
|
|
2311
|
+
return delay.sign < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay;
|
|
2312
|
+
}
|
|
2313
|
+
function getBenchmarkRelaxations(allowPrivateAddress, signatureTimeWindow) {
|
|
2314
|
+
const relaxations = [];
|
|
2315
|
+
if (allowPrivateAddress) relaxations.push({
|
|
2316
|
+
protection: "private_address_checks",
|
|
2317
|
+
effect: "disabled",
|
|
2318
|
+
effectiveValue: true
|
|
2319
|
+
});
|
|
2320
|
+
if (signatureTimeWindow === false) relaxations.push({
|
|
2321
|
+
protection: "http_signature_time_window",
|
|
2322
|
+
effect: "disabled",
|
|
2323
|
+
effectiveValue: false,
|
|
2324
|
+
secureDefaultSeconds: 3600
|
|
2325
|
+
});
|
|
2326
|
+
else try {
|
|
2327
|
+
const seconds = Temporal.Duration.from(signatureTimeWindow).total({ unit: "seconds" });
|
|
2328
|
+
if (seconds !== 3600) relaxations.push({
|
|
2329
|
+
protection: "http_signature_time_window",
|
|
2330
|
+
effect: "changed",
|
|
2331
|
+
effectiveSeconds: seconds,
|
|
2332
|
+
secureDefaultSeconds: 3600
|
|
2333
|
+
});
|
|
2334
|
+
} catch {}
|
|
2335
|
+
return relaxations;
|
|
2336
|
+
}
|
|
2337
|
+
function formatBenchmarkRelaxations(relaxations) {
|
|
2338
|
+
if (relaxations.length < 1) return "no benchmark-only protections relaxed";
|
|
2339
|
+
return relaxations.map((relaxation) => {
|
|
2340
|
+
switch (relaxation.protection) {
|
|
2341
|
+
case "private_address_checks": return "private address checks disabled (allowPrivateAddress=true)";
|
|
2342
|
+
case "http_signature_time_window":
|
|
2343
|
+
if (relaxation.effect === "disabled") return `HTTP Signature time window disabled (signatureTimeWindow=false)`;
|
|
2344
|
+
return `HTTP Signature time window set to ${relaxation.effectiveSeconds}s (secure default: ${relaxation.secureDefaultSeconds}s)`;
|
|
2345
|
+
}
|
|
2346
|
+
}).join("; ");
|
|
2347
|
+
}
|
|
2348
|
+
function getBenchmarkTriggerOptions(benchmarkOptions) {
|
|
2349
|
+
const sinks = benchmarkOptions.triggerSinks?.map((sink) => {
|
|
2350
|
+
try {
|
|
2351
|
+
return new URL(sink).href;
|
|
2352
|
+
} catch {
|
|
2353
|
+
throw new TypeError("benchmarkMode.triggerSinks must contain only URLs.");
|
|
2354
|
+
}
|
|
2355
|
+
});
|
|
2356
|
+
return {
|
|
2357
|
+
sinks: sinks == null ? void 0 : new Set(sinks),
|
|
2358
|
+
allowUnsafeRecipients: benchmarkOptions.allowUnsafeTriggerRecipients === true
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
function maxDelay(first, second) {
|
|
2362
|
+
return Temporal.Duration.compare(first, second) >= 0 ? first : second;
|
|
2363
|
+
}
|
|
2364
|
+
function isTransportDeliveryError(error) {
|
|
2365
|
+
return error instanceof FetchError || isAbortError(error);
|
|
2366
|
+
}
|
|
2367
|
+
function toCircuitBreakerMetricState(state) {
|
|
2368
|
+
return state === "half-open" ? "half_open" : state;
|
|
2369
|
+
}
|
|
2370
|
+
function recordCircuitBreakerSpanEvent(span, remoteHost, change) {
|
|
2371
|
+
span.addEvent("activitypub.circuit_breaker.state_change", {
|
|
2372
|
+
"activitypub.remote.host": remoteHost,
|
|
2373
|
+
"activitypub.circuit_breaker.previous_state": toCircuitBreakerMetricState(change.previousState),
|
|
2374
|
+
"activitypub.circuit_breaker.state": toCircuitBreakerMetricState(change.newState)
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
function recordCircuitBreakerHeldSpanEvent(span, remoteHost, state) {
|
|
2378
|
+
span.addEvent("activitypub.circuit_breaker.held", {
|
|
2379
|
+
"activitypub.remote.host": remoteHost,
|
|
2380
|
+
"activitypub.circuit_breaker.state": toCircuitBreakerMetricState(state)
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
function isRemoteContextLoadingFailure(error) {
|
|
2384
|
+
return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
|
|
2385
|
+
}
|
|
2386
|
+
function isPermanentRemoteContextError(error) {
|
|
2387
|
+
if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
|
|
2388
|
+
const details = error.details;
|
|
2389
|
+
if (details?.code === "invalid remote context") return true;
|
|
2390
|
+
return isRemoteContextLoadingFailure(error) && typeof details?.url === "string" && !URL.canParse(details.url) && isClearlyMalformedContextReference(details.url);
|
|
2391
|
+
}
|
|
2392
|
+
function isPermanentInboxParseError(error) {
|
|
2393
|
+
return error instanceof Error && (error.name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError(error) || error.name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure(error)) || error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || isInvalidUrlTypeError(error));
|
|
2394
|
+
}
|
|
1692
2395
|
/**
|
|
1693
2396
|
* Create a new {@link Federation} instance.
|
|
1694
|
-
* @param
|
|
2397
|
+
* @param options Parameters for initializing the instance.
|
|
1695
2398
|
* @returns A new {@link Federation} instance.
|
|
2399
|
+
* @throws {TypeError} If benchmark mode and `meterProvider` are both
|
|
2400
|
+
* specified.
|
|
1696
2401
|
* @since 0.10.0
|
|
1697
2402
|
*/
|
|
1698
2403
|
function createFederation(options) {
|
|
@@ -1720,12 +2425,37 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
1720
2425
|
skipSignatureVerification;
|
|
1721
2426
|
outboxRetryPolicy;
|
|
1722
2427
|
inboxRetryPolicy;
|
|
2428
|
+
circuitBreaker;
|
|
1723
2429
|
activityTransformers;
|
|
1724
2430
|
_tracerProvider;
|
|
2431
|
+
_meterProvider;
|
|
1725
2432
|
firstKnock;
|
|
1726
2433
|
inboxChallengePolicy;
|
|
2434
|
+
benchmarkMode;
|
|
2435
|
+
benchmarkMetricReader;
|
|
2436
|
+
benchmarkTriggerOptions;
|
|
2437
|
+
#queueDepthGaugeSourceId = `fedify-${(++nextQueueDepthGaugeSourceId).toString(36)}`;
|
|
2438
|
+
#queueDepthGaugeEntries = [];
|
|
2439
|
+
#queueDepthGaugeMeterProvider;
|
|
1727
2440
|
constructor(options) {
|
|
1728
2441
|
super();
|
|
2442
|
+
const benchmarkMode = options.benchmarkMode != null && options.benchmarkMode !== false;
|
|
2443
|
+
const benchmarkOptions = typeof options.benchmarkMode === "object" ? options.benchmarkMode : {};
|
|
2444
|
+
const hasCustomLoaderFactory = options.documentLoaderFactory != null || options.contextLoaderFactory != null;
|
|
2445
|
+
const allowPrivateAddress = options.allowPrivateAddress ?? (benchmarkMode && !hasCustomLoaderFactory ? true : false);
|
|
2446
|
+
const signatureTimeWindow = options.signatureTimeWindow ?? (benchmarkMode ? false : { hours: 1 });
|
|
2447
|
+
if (benchmarkMode && options.meterProvider != null) throw new TypeError("benchmarkMode requires Fedify to own the meterProvider; OpenTelemetry metric readers cannot be added after a MeterProvider is constructed.");
|
|
2448
|
+
if (benchmarkMode) {
|
|
2449
|
+
const relaxations = getBenchmarkRelaxations(allowPrivateAddress, signatureTimeWindow);
|
|
2450
|
+
const relaxationSummary = formatBenchmarkRelaxations(relaxations);
|
|
2451
|
+
getLogger([
|
|
2452
|
+
"fedify",
|
|
2453
|
+
"federation",
|
|
2454
|
+
"benchmark"
|
|
2455
|
+
]).warn(`Fedify benchmarkMode is enabled; ${relaxationSummary}. Benchmark endpoints are active and must not be used in production.`, { relaxations });
|
|
2456
|
+
}
|
|
2457
|
+
this.benchmarkMode = benchmarkMode;
|
|
2458
|
+
this.benchmarkTriggerOptions = benchmarkMode ? getBenchmarkTriggerOptions(benchmarkOptions) : {};
|
|
1729
2459
|
this.kv = options.kv;
|
|
1730
2460
|
this.kvPrefixes = {
|
|
1731
2461
|
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
@@ -1733,6 +2463,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
1733
2463
|
publicKey: ["_fedify", "publicKey"],
|
|
1734
2464
|
httpMessageSignaturesSpec: ["_fedify", "httpMessageSignaturesSpec"],
|
|
1735
2465
|
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
|
|
2466
|
+
circuitBreaker: ["_fedify", "circuit"],
|
|
1736
2467
|
...options.kvPrefixes ?? {}
|
|
1737
2468
|
};
|
|
1738
2469
|
if (options.queue == null) {
|
|
@@ -1748,6 +2479,25 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
1748
2479
|
this.outboxQueue = options.queue.outbox;
|
|
1749
2480
|
this.fanoutQueue = options.queue.fanout;
|
|
1750
2481
|
}
|
|
2482
|
+
if (options.circuitBreaker !== false && this.outboxQueue != null) {
|
|
2483
|
+
this.circuitBreaker = new CircuitBreaker({
|
|
2484
|
+
kv: options.kv,
|
|
2485
|
+
prefix: this.kvPrefixes.circuitBreaker,
|
|
2486
|
+
options: options.circuitBreaker,
|
|
2487
|
+
stateChangeObserver: (remoteHost, _previousState, newState) => {
|
|
2488
|
+
const metricState = toCircuitBreakerMetricState(newState);
|
|
2489
|
+
recordCircuitBreakerStateChange(this.meterProvider, remoteHost, metricState);
|
|
2490
|
+
}
|
|
2491
|
+
});
|
|
2492
|
+
if (options.kv.cas == null && !circuitBreakerCasWarningKvStores.has(options.kv)) {
|
|
2493
|
+
circuitBreakerCasWarningKvStores.add(options.kv);
|
|
2494
|
+
getLogger([
|
|
2495
|
+
"fedify",
|
|
2496
|
+
"federation",
|
|
2497
|
+
"circuit"
|
|
2498
|
+
]).warn("The configured key-value store does not support CAS; outbound delivery circuit breaker updates may race under concurrent workers.");
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
1751
2501
|
this.inboxQueueStarted = false;
|
|
1752
2502
|
this.outboxQueueStarted = false;
|
|
1753
2503
|
this.fanoutQueueStarted = false;
|
|
@@ -1773,48 +2523,109 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
1773
2523
|
}
|
|
1774
2524
|
this.router.trailingSlashInsensitive = options.trailingSlashInsensitive ?? false;
|
|
1775
2525
|
this._initializeRouter();
|
|
1776
|
-
if (options.allowPrivateAddress || options.userAgent != null) {
|
|
2526
|
+
if (options.allowPrivateAddress === true || options.userAgent != null) {
|
|
1777
2527
|
if (options.documentLoaderFactory != null) throw new TypeError("Cannot set documentLoaderFactory with allowPrivateAddress or userAgent options.");
|
|
1778
2528
|
if (options.contextLoaderFactory != null) throw new TypeError("Cannot set contextLoaderFactory with allowPrivateAddress or userAgent options.");
|
|
1779
2529
|
if (options.authenticatedDocumentLoaderFactory != null) throw new TypeError("Cannot set authenticatedDocumentLoaderFactory with allowPrivateAddress or userAgent options.");
|
|
1780
2530
|
}
|
|
1781
|
-
const {
|
|
1782
|
-
this.allowPrivateAddress = allowPrivateAddress
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
2531
|
+
const { userAgent } = options;
|
|
2532
|
+
this.allowPrivateAddress = allowPrivateAddress;
|
|
2533
|
+
const userDocumentLoaderFactory = options.documentLoaderFactory;
|
|
2534
|
+
const userContextLoaderFactory = options.contextLoaderFactory;
|
|
2535
|
+
const userAuthFactory = options.authenticatedDocumentLoaderFactory;
|
|
2536
|
+
const builtinDocumentLoaderFactory = (opts) => kvCache({
|
|
2537
|
+
loader: getDocumentLoader({
|
|
2538
|
+
allowPrivateAddress: opts?.allowPrivateAddress ?? allowPrivateAddress,
|
|
2539
|
+
userAgent: opts?.userAgent ?? userAgent
|
|
2540
|
+
}),
|
|
2541
|
+
kv: options.kv,
|
|
2542
|
+
prefix: this.kvPrefixes.remoteDocument,
|
|
2543
|
+
meterProvider: this._meterProvider,
|
|
2544
|
+
kind: "object"
|
|
2545
|
+
});
|
|
2546
|
+
const builtinContextLoaderFactory = (opts) => kvCache({
|
|
2547
|
+
loader: getDocumentLoader({
|
|
2548
|
+
allowPrivateAddress: opts?.allowPrivateAddress ?? allowPrivateAddress,
|
|
2549
|
+
userAgent: opts?.userAgent ?? userAgent
|
|
2550
|
+
}),
|
|
2551
|
+
kv: options.kv,
|
|
2552
|
+
prefix: this.kvPrefixes.remoteDocument,
|
|
2553
|
+
meterProvider: this._meterProvider,
|
|
2554
|
+
kind: "context"
|
|
2555
|
+
});
|
|
2556
|
+
this.documentLoaderFactory = (opts) => instrumentDocumentLoader((userDocumentLoaderFactory ?? builtinDocumentLoaderFactory)(opts), {
|
|
2557
|
+
meterProvider: this._meterProvider,
|
|
2558
|
+
kind: "object",
|
|
2559
|
+
cacheEnabled: userDocumentLoaderFactory == null ? true : void 0
|
|
1792
2560
|
});
|
|
1793
|
-
|
|
1794
|
-
this.
|
|
1795
|
-
|
|
1796
|
-
|
|
2561
|
+
const resolvedContextLoaderFactory = userContextLoaderFactory ?? userDocumentLoaderFactory ?? builtinContextLoaderFactory;
|
|
2562
|
+
this.contextLoaderFactory = (opts) => instrumentDocumentLoader(resolvedContextLoaderFactory(opts), {
|
|
2563
|
+
meterProvider: this._meterProvider,
|
|
2564
|
+
kind: "context",
|
|
2565
|
+
cacheEnabled: userContextLoaderFactory == null && userDocumentLoaderFactory == null ? true : void 0
|
|
2566
|
+
});
|
|
2567
|
+
this.authenticatedDocumentLoaderFactory = (identity, factoryOpts) => instrumentDocumentLoader(userAuthFactory != null ? userAuthFactory(identity, factoryOpts) : getAuthenticatedDocumentLoader(identity, {
|
|
2568
|
+
allowPrivateAddress: factoryOpts?.allowPrivateAddress ?? allowPrivateAddress,
|
|
2569
|
+
userAgent: factoryOpts?.userAgent ?? userAgent,
|
|
1797
2570
|
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, options.firstKnock),
|
|
1798
2571
|
tracerProvider: this.tracerProvider
|
|
1799
|
-
})
|
|
2572
|
+
}), {
|
|
2573
|
+
meterProvider: this._meterProvider,
|
|
2574
|
+
kind: "object",
|
|
2575
|
+
cacheEnabled: userAuthFactory == null ? false : void 0
|
|
2576
|
+
});
|
|
1800
2577
|
this.userAgent = userAgent;
|
|
1801
2578
|
this.onOutboxError = options.onOutboxError;
|
|
1802
2579
|
this.permanentFailureStatusCodes = options.permanentFailureStatusCodes ?? [404, 410];
|
|
1803
|
-
this.signatureTimeWindow =
|
|
2580
|
+
this.signatureTimeWindow = signatureTimeWindow;
|
|
1804
2581
|
this.skipSignatureVerification = options.skipSignatureVerification ?? false;
|
|
1805
2582
|
this.inboxChallengePolicy = options.inboxChallengePolicy;
|
|
1806
2583
|
this.outboxRetryPolicy = options.outboxRetryPolicy ?? createExponentialBackoffPolicy();
|
|
1807
2584
|
this.inboxRetryPolicy = options.inboxRetryPolicy ?? createExponentialBackoffPolicy();
|
|
1808
2585
|
this.activityTransformers = options.activityTransformers ?? getDefaultActivityTransformers();
|
|
1809
2586
|
this._tracerProvider = options.tracerProvider;
|
|
2587
|
+
if (benchmarkMode) {
|
|
2588
|
+
const benchmarkMetrics = createBenchmarkMeterProvider();
|
|
2589
|
+
this._meterProvider = benchmarkMetrics.meterProvider;
|
|
2590
|
+
this.benchmarkMetricReader = benchmarkMetrics.reader;
|
|
2591
|
+
} else this._meterProvider = options.meterProvider;
|
|
2592
|
+
this.#queueDepthGaugeEntries = [
|
|
2593
|
+
{
|
|
2594
|
+
role: "inbox",
|
|
2595
|
+
queue: this.inboxQueue
|
|
2596
|
+
},
|
|
2597
|
+
{
|
|
2598
|
+
role: "outbox",
|
|
2599
|
+
queue: this.outboxQueue
|
|
2600
|
+
},
|
|
2601
|
+
{
|
|
2602
|
+
role: "fanout",
|
|
2603
|
+
queue: this.fanoutQueue
|
|
2604
|
+
}
|
|
2605
|
+
];
|
|
2606
|
+
this.#registerQueueDepthGauge(this._meterProvider ?? metrics.getMeterProvider());
|
|
1810
2607
|
this.firstKnock = options.firstKnock;
|
|
1811
2608
|
}
|
|
1812
2609
|
get tracerProvider() {
|
|
1813
2610
|
return this._tracerProvider ?? trace.getTracerProvider();
|
|
1814
2611
|
}
|
|
2612
|
+
get meterProvider() {
|
|
2613
|
+
const meterProvider = this._meterProvider ?? metrics.getMeterProvider();
|
|
2614
|
+
this.#registerQueueDepthGauge(meterProvider);
|
|
2615
|
+
return meterProvider;
|
|
2616
|
+
}
|
|
2617
|
+
#registerQueueDepthGauge(meterProvider) {
|
|
2618
|
+
if (meterProvider === this.#queueDepthGaugeMeterProvider) return;
|
|
2619
|
+
registerQueueDepthGauge(meterProvider, this.#queueDepthGaugeEntries, { sourceId: this.#queueDepthGaugeSourceId });
|
|
2620
|
+
this.#queueDepthGaugeMeterProvider = meterProvider;
|
|
2621
|
+
}
|
|
1815
2622
|
_initializeRouter() {
|
|
1816
2623
|
this.router.add("/.well-known/webfinger", "webfinger");
|
|
1817
2624
|
this.router.add("/.well-known/nodeinfo", "nodeInfoJrd");
|
|
2625
|
+
if (this.benchmarkMode) {
|
|
2626
|
+
this.router.add("/.well-known/fedify/bench/stats", "benchmarkStats");
|
|
2627
|
+
this.router.add("/.well-known/fedify/bench/trigger", "benchmarkTrigger");
|
|
2628
|
+
}
|
|
1818
2629
|
}
|
|
1819
2630
|
_getTracer() {
|
|
1820
2631
|
return this.tracerProvider.getTracer(name, version);
|
|
@@ -1845,80 +2656,125 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
1845
2656
|
await Promise.all(promises);
|
|
1846
2657
|
}
|
|
1847
2658
|
processQueuedTask(contextData, message) {
|
|
1848
|
-
const tracer = this._getTracer();
|
|
1849
|
-
const extractedContext = propagation.extract(context.active(), message.traceContext);
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
2659
|
+
const tracer = this._getTracer();
|
|
2660
|
+
const extractedContext = propagation.extract(context.active(), message.traceContext);
|
|
2661
|
+
const meter = getFederationMetrics(this.meterProvider);
|
|
2662
|
+
return withContext({ messageId: message.id }, async () => {
|
|
2663
|
+
if (message.type === "fanout") {
|
|
2664
|
+
const common = {
|
|
2665
|
+
role: "fanout",
|
|
2666
|
+
queue: this.fanoutQueue,
|
|
2667
|
+
activityType: message.activityType
|
|
2668
|
+
};
|
|
2669
|
+
await tracer.startActiveSpan("activitypub.fanout", {
|
|
2670
|
+
kind: SpanKind.CONSUMER,
|
|
2671
|
+
attributes: { "activitypub.activity.type": message.activityType }
|
|
2672
|
+
}, extractedContext, async (span) => {
|
|
2673
|
+
const spanCtx = span.spanContext();
|
|
2674
|
+
return await withContext({
|
|
2675
|
+
traceId: spanCtx.traceId,
|
|
2676
|
+
spanId: spanCtx.spanId
|
|
2677
|
+
}, async () => {
|
|
2678
|
+
if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
|
|
2679
|
+
meter.recordQueueTaskStarted(common);
|
|
2680
|
+
meter.incrementQueueTaskInFlight(common);
|
|
2681
|
+
const startedAt = performance.now();
|
|
2682
|
+
let outcome = "completed";
|
|
2683
|
+
try {
|
|
2684
|
+
await this.#listenFanoutMessage(contextData, message);
|
|
2685
|
+
} catch (e) {
|
|
2686
|
+
const aborted = isAbortError(e);
|
|
2687
|
+
outcome = aborted ? "aborted" : "failed";
|
|
2688
|
+
if (!aborted) span.setStatus({
|
|
2689
|
+
code: SpanStatusCode.ERROR,
|
|
2690
|
+
message: String(e)
|
|
2691
|
+
});
|
|
2692
|
+
throw e;
|
|
2693
|
+
} finally {
|
|
2694
|
+
meter.recordQueueTaskOutcome(common, outcome, getDurationMs(startedAt));
|
|
2695
|
+
meter.decrementQueueTaskInFlight(common);
|
|
2696
|
+
span.end();
|
|
2697
|
+
}
|
|
2698
|
+
});
|
|
1872
2699
|
});
|
|
1873
|
-
})
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
spanId: spanCtx.spanId
|
|
1885
|
-
}, async () => {
|
|
1886
|
-
if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
|
|
1887
|
-
try {
|
|
1888
|
-
await this.#listenOutboxMessage(contextData, message, span);
|
|
1889
|
-
} catch (e) {
|
|
1890
|
-
span.setStatus({
|
|
1891
|
-
code: SpanStatusCode.ERROR,
|
|
1892
|
-
message: String(e)
|
|
1893
|
-
});
|
|
1894
|
-
throw e;
|
|
1895
|
-
} finally {
|
|
1896
|
-
span.end();
|
|
2700
|
+
} else if (message.type === "outbox") {
|
|
2701
|
+
const common = {
|
|
2702
|
+
role: "outbox",
|
|
2703
|
+
queue: this.outboxQueue,
|
|
2704
|
+
activityType: message.activityType
|
|
2705
|
+
};
|
|
2706
|
+
await tracer.startActiveSpan("activitypub.outbox", {
|
|
2707
|
+
kind: SpanKind.CONSUMER,
|
|
2708
|
+
attributes: {
|
|
2709
|
+
"activitypub.activity.type": message.activityType,
|
|
2710
|
+
"activitypub.activity.retries": message.attempt
|
|
1897
2711
|
}
|
|
2712
|
+
}, extractedContext, async (span) => {
|
|
2713
|
+
const spanCtx = span.spanContext();
|
|
2714
|
+
return await withContext({
|
|
2715
|
+
traceId: spanCtx.traceId,
|
|
2716
|
+
spanId: spanCtx.spanId
|
|
2717
|
+
}, async () => {
|
|
2718
|
+
if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
|
|
2719
|
+
meter.recordQueueTaskStarted(common);
|
|
2720
|
+
meter.incrementQueueTaskInFlight(common);
|
|
2721
|
+
const startedAt = performance.now();
|
|
2722
|
+
let outcome = "completed";
|
|
2723
|
+
try {
|
|
2724
|
+
await this.#listenOutboxMessage(contextData, message, span);
|
|
2725
|
+
} catch (e) {
|
|
2726
|
+
const aborted = isAbortError(e);
|
|
2727
|
+
outcome = aborted ? "aborted" : "failed";
|
|
2728
|
+
if (!aborted) span.setStatus({
|
|
2729
|
+
code: SpanStatusCode.ERROR,
|
|
2730
|
+
message: String(e)
|
|
2731
|
+
});
|
|
2732
|
+
throw e;
|
|
2733
|
+
} finally {
|
|
2734
|
+
meter.recordQueueTaskOutcome(common, outcome, getDurationMs(startedAt));
|
|
2735
|
+
meter.decrementQueueTaskInFlight(common);
|
|
2736
|
+
span.end();
|
|
2737
|
+
}
|
|
2738
|
+
});
|
|
1898
2739
|
});
|
|
1899
|
-
})
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2740
|
+
} else if (message.type === "inbox") {
|
|
2741
|
+
const common = {
|
|
2742
|
+
role: "inbox",
|
|
2743
|
+
queue: this.inboxQueue
|
|
2744
|
+
};
|
|
2745
|
+
await tracer.startActiveSpan("activitypub.inbox", {
|
|
2746
|
+
kind: SpanKind.CONSUMER,
|
|
2747
|
+
attributes: { "activitypub.shared_inbox": message.identifier == null }
|
|
2748
|
+
}, extractedContext, async (span) => {
|
|
2749
|
+
const spanCtx = span.spanContext();
|
|
2750
|
+
return await withContext({
|
|
2751
|
+
traceId: spanCtx.traceId,
|
|
2752
|
+
spanId: spanCtx.spanId
|
|
2753
|
+
}, async () => {
|
|
2754
|
+
meter.recordQueueTaskStarted(common);
|
|
2755
|
+
meter.incrementQueueTaskInFlight(common);
|
|
2756
|
+
const startedAt = performance.now();
|
|
2757
|
+
let outcome = "completed";
|
|
2758
|
+
try {
|
|
2759
|
+
await this.#listenInboxMessage(contextData, message, span, (activityType) => {
|
|
2760
|
+
common.activityType = activityType;
|
|
2761
|
+
});
|
|
2762
|
+
} catch (e) {
|
|
2763
|
+
const aborted = isAbortError(e);
|
|
2764
|
+
outcome = aborted ? "aborted" : "failed";
|
|
2765
|
+
if (!aborted) span.setStatus({
|
|
2766
|
+
code: SpanStatusCode.ERROR,
|
|
2767
|
+
message: String(e)
|
|
2768
|
+
});
|
|
2769
|
+
throw e;
|
|
2770
|
+
} finally {
|
|
2771
|
+
meter.recordQueueTaskOutcome(common, outcome, getDurationMs(startedAt));
|
|
2772
|
+
meter.decrementQueueTaskInFlight(common);
|
|
2773
|
+
span.end();
|
|
2774
|
+
}
|
|
2775
|
+
});
|
|
1920
2776
|
});
|
|
1921
|
-
}
|
|
2777
|
+
}
|
|
1922
2778
|
});
|
|
1923
2779
|
}
|
|
1924
2780
|
async #listenFanoutMessage(data, message) {
|
|
@@ -1980,29 +2836,202 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
1980
2836
|
if (rsaKeyPair == null && pair.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") rsaKeyPair = pair;
|
|
1981
2837
|
keys.push(pair);
|
|
1982
2838
|
}
|
|
2839
|
+
const loaderOptions = this.#getLoaderOptions(message.baseUrl);
|
|
2840
|
+
let parsedActorIds;
|
|
2841
|
+
const getActorIds = () => {
|
|
2842
|
+
parsedActorIds ??= (message.actorIds ?? []).flatMap((id) => {
|
|
2843
|
+
try {
|
|
2844
|
+
return [new URL(id)];
|
|
2845
|
+
} catch {
|
|
2846
|
+
logger.warn("Invalid actorId URL in OutboxMessage: {id}", { id });
|
|
2847
|
+
return [];
|
|
2848
|
+
}
|
|
2849
|
+
});
|
|
2850
|
+
return parsedActorIds;
|
|
2851
|
+
};
|
|
2852
|
+
const parseActivity = () => Activity.fromJsonLd(message.activity, {
|
|
2853
|
+
contextLoader: this.contextLoaderFactory(loaderOptions),
|
|
2854
|
+
documentLoader: rsaKeyPair == null ? this.documentLoaderFactory(loaderOptions) : this.authenticatedDocumentLoaderFactory(rsaKeyPair, loaderOptions),
|
|
2855
|
+
tracerProvider: this.tracerProvider
|
|
2856
|
+
});
|
|
2857
|
+
const enqueueHeldOutboxMessage = async (delay, heldSince) => {
|
|
2858
|
+
const { outboxQueue } = this;
|
|
2859
|
+
if (outboxQueue == null) return;
|
|
2860
|
+
const heldMessage = {
|
|
2861
|
+
...message,
|
|
2862
|
+
circuitHeld: true,
|
|
2863
|
+
circuitHeldSince: heldSince.toString()
|
|
2864
|
+
};
|
|
2865
|
+
await outboxQueue.enqueue(heldMessage, {
|
|
2866
|
+
delay: clampNegativeDelay(delay),
|
|
2867
|
+
orderingKey: message.orderingKey
|
|
2868
|
+
});
|
|
2869
|
+
getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
|
|
2870
|
+
role: "outbox",
|
|
2871
|
+
queue: outboxQueue,
|
|
2872
|
+
activityType: heldMessage.activityType
|
|
2873
|
+
}, heldMessage.attempt);
|
|
2874
|
+
};
|
|
2875
|
+
const dropHeldOutboxMessage = async (circuit, remoteHost, inbox, heldSince, activity) => {
|
|
2876
|
+
await circuit.dropActivity(remoteHost, {
|
|
2877
|
+
inbox,
|
|
2878
|
+
activity,
|
|
2879
|
+
activityId: message.activityId,
|
|
2880
|
+
activityType: message.activityType,
|
|
2881
|
+
actorIds: getActorIds(),
|
|
2882
|
+
heldSince
|
|
2883
|
+
});
|
|
2884
|
+
if (this.outboxPermanentFailureHandler != null) {
|
|
2885
|
+
const ctx = this.#createContext(new URL(message.baseUrl), _, { documentLoader: this.documentLoaderFactory(loaderOptions) });
|
|
2886
|
+
try {
|
|
2887
|
+
await this.outboxPermanentFailureHandler(ctx, {
|
|
2888
|
+
reason: "circuit-breaker-ttl",
|
|
2889
|
+
inbox,
|
|
2890
|
+
activity,
|
|
2891
|
+
error: new SendActivityError(inbox, 0, "Circuit breaker held activity expired.", ""),
|
|
2892
|
+
statusCode: 0,
|
|
2893
|
+
circuitHeldSince: heldSince,
|
|
2894
|
+
actorIds: getActorIds()
|
|
2895
|
+
});
|
|
2896
|
+
} catch (handlerError) {
|
|
2897
|
+
logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
|
|
2898
|
+
...logData,
|
|
2899
|
+
error: handlerError
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
2904
|
+
};
|
|
1983
2905
|
try {
|
|
2906
|
+
const inbox = new URL(message.inbox);
|
|
2907
|
+
const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
|
|
2908
|
+
const remoteHost = getRemoteHost(inbox);
|
|
2909
|
+
let decision;
|
|
2910
|
+
if (circuit != null) try {
|
|
2911
|
+
decision = await circuit.beforeSend(remoteHost, message);
|
|
2912
|
+
} catch (circuitError) {
|
|
2913
|
+
getLogger([
|
|
2914
|
+
"fedify",
|
|
2915
|
+
"federation",
|
|
2916
|
+
"circuit"
|
|
2917
|
+
]).error("Failed to check circuit breaker state before sending; proceeding with delivery:\n{error}", {
|
|
2918
|
+
...logData,
|
|
2919
|
+
remoteHost,
|
|
2920
|
+
error: circuitError
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
if (decision != null && circuit != null) {
|
|
2924
|
+
if (decision.type === "hold") {
|
|
2925
|
+
recordCircuitBreakerHeldSpanEvent(span, remoteHost, decision.state);
|
|
2926
|
+
await enqueueHeldOutboxMessage(decision.delay, decision.heldSince);
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
if (decision.type === "drop") {
|
|
2930
|
+
const activity = await parseActivity();
|
|
2931
|
+
await dropHeldOutboxMessage(circuit, remoteHost, inbox, decision.heldSince, activity);
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
2934
|
+
if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
|
|
2935
|
+
}
|
|
1984
2936
|
await sendActivity({
|
|
1985
2937
|
keys,
|
|
1986
2938
|
activity: message.activity,
|
|
1987
2939
|
activityId: message.activityId,
|
|
1988
2940
|
activityType: message.activityType,
|
|
1989
|
-
inbox
|
|
2941
|
+
inbox,
|
|
1990
2942
|
sharedInbox: message.sharedInbox,
|
|
1991
2943
|
headers: new Headers(message.headers),
|
|
1992
2944
|
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock),
|
|
2945
|
+
meterProvider: this.meterProvider,
|
|
1993
2946
|
tracerProvider: this.tracerProvider
|
|
1994
2947
|
});
|
|
2948
|
+
if (circuit != null) try {
|
|
2949
|
+
const stateChange = await circuit.recordSuccess(remoteHost);
|
|
2950
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
2951
|
+
} catch (error) {
|
|
2952
|
+
getLogger([
|
|
2953
|
+
"fedify",
|
|
2954
|
+
"federation",
|
|
2955
|
+
"circuit"
|
|
2956
|
+
]).error("Failed to record successful delivery in circuit breaker state; the activity was already delivered:\n{error}", {
|
|
2957
|
+
...logData,
|
|
2958
|
+
remoteHost,
|
|
2959
|
+
error
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
1995
2962
|
} catch (error) {
|
|
1996
2963
|
span.setStatus({
|
|
1997
2964
|
code: SpanStatusCode.ERROR,
|
|
1998
2965
|
message: String(error)
|
|
1999
2966
|
});
|
|
2000
|
-
const
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2967
|
+
const remoteHost = (() => {
|
|
2968
|
+
if (error instanceof SendActivityError) return getRemoteHost(error.inbox);
|
|
2969
|
+
try {
|
|
2970
|
+
return getRemoteHost(new URL(message.inbox));
|
|
2971
|
+
} catch (_) {
|
|
2972
|
+
logger.warn("Invalid inbox URL in queued outbox message: {inbox}", logData);
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
})();
|
|
2976
|
+
let retryAfterDelay;
|
|
2977
|
+
let circuitHold;
|
|
2978
|
+
let circuitDrop;
|
|
2979
|
+
let retryPolicyDelay;
|
|
2980
|
+
let policyDelayCalculated = false;
|
|
2981
|
+
const getPolicyDelay = () => {
|
|
2982
|
+
if (!policyDelayCalculated) {
|
|
2983
|
+
retryPolicyDelay = this.outboxRetryPolicy({
|
|
2984
|
+
elapsedTime: Temporal.Instant.from(message.started).until(Temporal.Now.instant()),
|
|
2985
|
+
attempts: message.attempt
|
|
2986
|
+
});
|
|
2987
|
+
policyDelayCalculated = true;
|
|
2988
|
+
}
|
|
2989
|
+
return retryPolicyDelay;
|
|
2990
|
+
};
|
|
2991
|
+
const isPermanentFailure = error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode);
|
|
2992
|
+
if (!isPermanentFailure && error instanceof SendActivityError && (error.statusCode === 429 || error.statusCode === 503)) retryAfterDelay = parseRetryAfter(error.responseHeaders);
|
|
2993
|
+
if (remoteHost != null && this.outboxQueue != null && this.circuitBreaker != null) try {
|
|
2994
|
+
if (error instanceof SendActivityError) {
|
|
2995
|
+
const { statusCode } = error;
|
|
2996
|
+
const stateChange = isPermanentFailure || statusCode === 429 || statusCode >= 400 && statusCode < 500 ? await this.circuitBreaker.recordReachableFailure(remoteHost) : statusCode >= 500 ? await this.circuitBreaker.recordFailure(remoteHost) : void 0;
|
|
2997
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
2998
|
+
} else if (isTransportDeliveryError(error)) {
|
|
2999
|
+
const stateChange = await this.circuitBreaker.recordFailure(remoteHost);
|
|
3000
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
3001
|
+
}
|
|
3002
|
+
if (!isPermanentFailure) {
|
|
3003
|
+
const circuitDecision = await this.circuitBreaker.beforeSend(remoteHost, message);
|
|
3004
|
+
if (circuitDecision.type === "hold") circuitHold = {
|
|
3005
|
+
delay: circuitDecision.delay,
|
|
3006
|
+
heldSince: circuitDecision.heldSince,
|
|
3007
|
+
remoteHost,
|
|
3008
|
+
state: circuitDecision.state
|
|
3009
|
+
};
|
|
3010
|
+
else if (circuitDecision.type === "drop") circuitDrop = {
|
|
3011
|
+
circuit: this.circuitBreaker,
|
|
3012
|
+
remoteHost,
|
|
3013
|
+
inbox: new URL(message.inbox),
|
|
3014
|
+
heldSince: circuitDecision.heldSince
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
3017
|
+
} catch (circuitError) {
|
|
3018
|
+
getLogger([
|
|
3019
|
+
"fedify",
|
|
3020
|
+
"federation",
|
|
3021
|
+
"circuit"
|
|
3022
|
+
]).error("Failed to update circuit breaker state after delivery failure; falling back to normal failure handling:\n{error}", {
|
|
3023
|
+
...logData,
|
|
3024
|
+
remoteHost,
|
|
3025
|
+
error: circuitError
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
3028
|
+
span.addEvent("activitypub.delivery.failed", {
|
|
3029
|
+
...remoteHost == null ? {} : { "activitypub.remote.host": remoteHost },
|
|
3030
|
+
"activitypub.delivery.attempt": message.attempt,
|
|
3031
|
+
"activitypub.delivery.permanent_failure": isPermanentFailure,
|
|
3032
|
+
...error instanceof SendActivityError ? { "http.response.status_code": error.statusCode } : {}
|
|
2005
3033
|
});
|
|
3034
|
+
const activity = await parseActivity();
|
|
2006
3035
|
try {
|
|
2007
3036
|
await this.onOutboxError?.(error, activity);
|
|
2008
3037
|
} catch (error) {
|
|
@@ -2011,7 +3040,12 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2011
3040
|
error
|
|
2012
3041
|
});
|
|
2013
3042
|
}
|
|
2014
|
-
if (
|
|
3043
|
+
if (circuitDrop != null) {
|
|
3044
|
+
await dropHeldOutboxMessage(circuitDrop.circuit, circuitDrop.remoteHost, circuitDrop.inbox, circuitDrop.heldSince, activity);
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
if (isPermanentFailure) {
|
|
3048
|
+
getFederationMetrics(this.meterProvider).recordPermanentFailure(error.inbox, error.statusCode);
|
|
2015
3049
|
logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not retrying.", {
|
|
2016
3050
|
...logData,
|
|
2017
3051
|
status: error.statusCode
|
|
@@ -2020,18 +3054,12 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2020
3054
|
const ctx = this.#createContext(new URL(message.baseUrl), _, { documentLoader: this.documentLoaderFactory(loaderOptions) });
|
|
2021
3055
|
try {
|
|
2022
3056
|
await this.outboxPermanentFailureHandler(ctx, {
|
|
3057
|
+
reason: "http",
|
|
2023
3058
|
inbox: new URL(message.inbox),
|
|
2024
3059
|
activity,
|
|
2025
3060
|
error,
|
|
2026
3061
|
statusCode: error.statusCode,
|
|
2027
|
-
actorIds: (
|
|
2028
|
-
try {
|
|
2029
|
-
return [new URL(id)];
|
|
2030
|
-
} catch {
|
|
2031
|
-
logger.warn("Invalid actorId URL in OutboxMessage: {id}", { id });
|
|
2032
|
-
return [];
|
|
2033
|
-
}
|
|
2034
|
-
})
|
|
3062
|
+
actorIds: getActorIds()
|
|
2035
3063
|
});
|
|
2036
3064
|
} catch (handlerError) {
|
|
2037
3065
|
logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
|
|
@@ -2040,37 +3068,62 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2040
3068
|
});
|
|
2041
3069
|
}
|
|
2042
3070
|
}
|
|
3071
|
+
recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
2043
3072
|
return;
|
|
2044
3073
|
}
|
|
2045
|
-
if (
|
|
3074
|
+
if (circuitHold != null && getPolicyDelay() != null) {
|
|
3075
|
+
logger.error("Failed to send activity {activityId} to {inbox}; holding because the remote host circuit is open:\n{error}", {
|
|
3076
|
+
...logData,
|
|
3077
|
+
error
|
|
3078
|
+
});
|
|
3079
|
+
recordCircuitBreakerHeldSpanEvent(span, circuitHold.remoteHost, circuitHold.state);
|
|
3080
|
+
const circuit = this.circuitBreaker;
|
|
3081
|
+
await enqueueHeldOutboxMessage(retryAfterDelay == null || circuit == null ? circuitHold.delay : circuit.capHeldDelay(circuitHold.heldSince, maxDelay(circuitHold.delay, retryAfterDelay)), circuitHold.heldSince);
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
if (this.outboxQueue?.nativeRetrial && retryAfterDelay == null) {
|
|
2046
3085
|
logger.error("Failed to send activity {activityId} to {inbox}; backend will handle retry:\n{error}", {
|
|
2047
3086
|
...logData,
|
|
2048
3087
|
error
|
|
2049
3088
|
});
|
|
2050
3089
|
throw error;
|
|
2051
3090
|
}
|
|
2052
|
-
const
|
|
2053
|
-
|
|
2054
|
-
attempts: message.attempt
|
|
2055
|
-
});
|
|
3091
|
+
const policyDelay = getPolicyDelay();
|
|
3092
|
+
const delay = policyDelay == null ? null : retryAfterDelay ?? policyDelay;
|
|
2056
3093
|
if (delay != null) {
|
|
2057
3094
|
logger.error("Failed to send activity {activityId} to {inbox} (attempt #{attempt}); retry...:\n{error}", {
|
|
2058
3095
|
...logData,
|
|
2059
3096
|
error
|
|
2060
3097
|
});
|
|
2061
|
-
|
|
3098
|
+
const retryMessage = {
|
|
2062
3099
|
...message,
|
|
2063
3100
|
attempt: message.attempt + 1
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
3101
|
+
};
|
|
3102
|
+
const { outboxQueue } = this;
|
|
3103
|
+
if (outboxQueue != null) {
|
|
3104
|
+
await outboxQueue.enqueue(retryMessage, {
|
|
3105
|
+
delay: clampNegativeDelay(delay),
|
|
3106
|
+
orderingKey: message.orderingKey
|
|
3107
|
+
});
|
|
3108
|
+
getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
|
|
3109
|
+
role: "outbox",
|
|
3110
|
+
queue: outboxQueue,
|
|
3111
|
+
activityType: retryMessage.activityType
|
|
3112
|
+
}, retryMessage.attempt);
|
|
3113
|
+
recordOutboxActivity(this.meterProvider, "retried", retryMessage.activityType);
|
|
3114
|
+
}
|
|
3115
|
+
} else {
|
|
3116
|
+
logger.error("Failed to send activity {activityId} to {inbox} after {attempt} attempts; giving up:\n{error}", {
|
|
3117
|
+
...logData,
|
|
3118
|
+
error
|
|
3119
|
+
});
|
|
3120
|
+
recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
3121
|
+
}
|
|
2069
3122
|
return;
|
|
2070
3123
|
}
|
|
2071
3124
|
logger.info("Successfully sent activity {activityId} to {inbox}.", { ...logData });
|
|
2072
3125
|
}
|
|
2073
|
-
async #listenInboxMessage(ctxData, message, span) {
|
|
3126
|
+
async #listenInboxMessage(ctxData, message, span, onActivityType) {
|
|
2074
3127
|
const logger = getLogger([
|
|
2075
3128
|
"fedify",
|
|
2076
3129
|
"federation",
|
|
@@ -2083,68 +3136,37 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2083
3136
|
const identity = await this.sharedInboxKeyDispatcher(context);
|
|
2084
3137
|
if (identity != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
|
|
2085
3138
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
context.origin,
|
|
2092
|
-
activity.id.href
|
|
2093
|
-
];
|
|
2094
|
-
if (cacheKey != null) {
|
|
2095
|
-
if (await this.kv.get(cacheKey) === true) {
|
|
2096
|
-
logger.debug("Activity {activityId} has already been processed.", {
|
|
2097
|
-
activityId: activity.id?.href,
|
|
2098
|
-
activity: message.activity,
|
|
2099
|
-
recipient: message.identifier
|
|
2100
|
-
});
|
|
2101
|
-
return;
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
await this._getTracer().startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (span) => {
|
|
2105
|
-
const dispatched = this.inboxListeners?.dispatchWithClass(activity);
|
|
2106
|
-
if (dispatched == null) {
|
|
2107
|
-
logger.error("Unsupported activity type:\n{activity}", {
|
|
2108
|
-
activityId: activity.id?.href,
|
|
2109
|
-
activity: message.activity,
|
|
2110
|
-
recipient: message.identifier,
|
|
2111
|
-
trial: message.attempt
|
|
2112
|
-
});
|
|
2113
|
-
span.setStatus({
|
|
2114
|
-
code: SpanStatusCode.ERROR,
|
|
2115
|
-
message: `Unsupported activity type: ${getTypeId(activity).href}`
|
|
2116
|
-
});
|
|
2117
|
-
span.end();
|
|
2118
|
-
return;
|
|
2119
|
-
}
|
|
2120
|
-
const { class: cls, listener } = dispatched;
|
|
2121
|
-
span.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
|
|
2122
|
-
try {
|
|
2123
|
-
await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, getTypeId(activity).href), activity);
|
|
2124
|
-
} catch (error) {
|
|
3139
|
+
await this._getTracer().startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (listenerSpan) => {
|
|
3140
|
+
let activity = null;
|
|
3141
|
+
let cacheKey = null;
|
|
3142
|
+
let activityType;
|
|
3143
|
+
const reportInboxError = async (error) => {
|
|
2125
3144
|
try {
|
|
2126
3145
|
await this.inboxErrorHandler?.(context, error);
|
|
2127
3146
|
} catch (error) {
|
|
2128
3147
|
logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
|
|
2129
3148
|
error,
|
|
2130
3149
|
trial: message.attempt,
|
|
2131
|
-
activityId: activity
|
|
3150
|
+
activityId: activity?.id?.href,
|
|
2132
3151
|
activity: message.activity,
|
|
2133
3152
|
recipient: message.identifier
|
|
2134
3153
|
});
|
|
2135
3154
|
}
|
|
3155
|
+
};
|
|
3156
|
+
const handleRetriableFailure = async (error) => {
|
|
3157
|
+
await reportInboxError(error);
|
|
2136
3158
|
if (this.inboxQueue?.nativeRetrial) {
|
|
2137
3159
|
logger.error("Failed to process the incoming activity {activityId}; backend will handle retry:\n{error}", {
|
|
2138
3160
|
error,
|
|
2139
|
-
activityId: activity
|
|
3161
|
+
activityId: activity?.id?.href,
|
|
2140
3162
|
activity: message.activity,
|
|
2141
3163
|
recipient: message.identifier
|
|
2142
3164
|
});
|
|
2143
|
-
|
|
3165
|
+
listenerSpan.setStatus({
|
|
2144
3166
|
code: SpanStatusCode.ERROR,
|
|
2145
3167
|
message: String(error)
|
|
2146
3168
|
});
|
|
2147
|
-
|
|
3169
|
+
listenerSpan.end();
|
|
2148
3170
|
throw error;
|
|
2149
3171
|
}
|
|
2150
3172
|
const delay = this.inboxRetryPolicy({
|
|
@@ -2155,34 +3177,165 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2155
3177
|
logger.error("Failed to process the incoming activity {activityId} (attempt #{attempt}); retry...:\n{error}", {
|
|
2156
3178
|
error,
|
|
2157
3179
|
attempt: message.attempt,
|
|
2158
|
-
activityId: activity
|
|
3180
|
+
activityId: activity?.id?.href,
|
|
2159
3181
|
activity: message.activity,
|
|
2160
3182
|
recipient: message.identifier
|
|
2161
3183
|
});
|
|
2162
|
-
|
|
3184
|
+
if (this.inboxQueue == null) {
|
|
3185
|
+
listenerSpan.setStatus({
|
|
3186
|
+
code: SpanStatusCode.ERROR,
|
|
3187
|
+
message: String(error)
|
|
3188
|
+
});
|
|
3189
|
+
listenerSpan.end();
|
|
3190
|
+
throw error;
|
|
3191
|
+
}
|
|
3192
|
+
const retryMessage = {
|
|
2163
3193
|
...message,
|
|
2164
3194
|
attempt: message.attempt + 1
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
|
|
3195
|
+
};
|
|
3196
|
+
await this.inboxQueue.enqueue(retryMessage, { delay: clampNegativeDelay(delay) });
|
|
3197
|
+
if (activityType != null) {
|
|
3198
|
+
getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
|
|
3199
|
+
role: "inbox",
|
|
3200
|
+
queue: this.inboxQueue,
|
|
3201
|
+
activityType
|
|
3202
|
+
}, retryMessage.attempt);
|
|
3203
|
+
recordInboxActivity(this.meterProvider, "retried", activityType);
|
|
3204
|
+
}
|
|
3205
|
+
} else {
|
|
3206
|
+
logger.error("Failed to process the incoming activity {activityId} after {trial} attempts; giving up:\n{error}", {
|
|
3207
|
+
error,
|
|
3208
|
+
activityId: activity?.id?.href,
|
|
3209
|
+
activity: message.activity,
|
|
3210
|
+
recipient: message.identifier
|
|
3211
|
+
});
|
|
3212
|
+
if (activityType != null) recordInboxActivity(this.meterProvider, "abandoned", activityType);
|
|
3213
|
+
}
|
|
3214
|
+
listenerSpan.setStatus({
|
|
3215
|
+
code: SpanStatusCode.ERROR,
|
|
3216
|
+
message: String(error)
|
|
3217
|
+
});
|
|
3218
|
+
listenerSpan.end();
|
|
3219
|
+
};
|
|
3220
|
+
let dispatched;
|
|
3221
|
+
let parseInput = void 0;
|
|
3222
|
+
let parseContextLoader = context.contextLoader;
|
|
3223
|
+
try {
|
|
3224
|
+
const hasSignatureField = hasSignature(message.activity);
|
|
3225
|
+
const shouldParseFromNormalizedSignedPayload = message.ldSignatureVerified === true || message.normalizedActivity != null || message.ldSignatureVerified == null && hasSignatureField;
|
|
3226
|
+
const parseContext = hasSignatureField ? {
|
|
3227
|
+
...context,
|
|
3228
|
+
contextLoader: getNormalizationContextLoader(context.contextLoader)
|
|
3229
|
+
} : {
|
|
3230
|
+
...context,
|
|
3231
|
+
contextLoader: wrapContextLoaderForJsonLd(context.contextLoader)
|
|
3232
|
+
};
|
|
3233
|
+
parseContextLoader = parseContext.contextLoader;
|
|
3234
|
+
let normalizedActivity;
|
|
3235
|
+
if (shouldParseFromNormalizedSignedPayload) {
|
|
3236
|
+
normalizedActivity = message.normalizedActivity ?? await compactJsonLd(message.activity, context.contextLoader);
|
|
3237
|
+
assertSafeJsonLd(normalizedActivity);
|
|
3238
|
+
}
|
|
3239
|
+
parseInput = shouldParseFromNormalizedSignedPayload ? detachSignature(normalizedActivity) : hasSignatureField ? detachSignature(message.activity) : message.activity;
|
|
3240
|
+
activity = await Activity.fromJsonLd(parseInput, parseContext);
|
|
3241
|
+
activityType = getTypeId(activity).href;
|
|
3242
|
+
span.setAttribute("activitypub.activity.type", activityType);
|
|
3243
|
+
listenerSpan.setAttribute("activitypub.activity.type", activityType);
|
|
3244
|
+
onActivityType?.(activityType);
|
|
3245
|
+
if (activity.id != null) {
|
|
3246
|
+
span.setAttribute("activitypub.activity.id", activity.id.href);
|
|
3247
|
+
listenerSpan.setAttribute("activitypub.activity.id", activity.id.href);
|
|
3248
|
+
}
|
|
3249
|
+
cacheKey = activity.id == null ? null : [
|
|
3250
|
+
...this.kvPrefixes.activityIdempotence,
|
|
3251
|
+
context.origin,
|
|
3252
|
+
activity.id.href
|
|
3253
|
+
];
|
|
3254
|
+
if (cacheKey != null) {
|
|
3255
|
+
if (await this.kv.get(cacheKey) === true) {
|
|
3256
|
+
logger.debug("Activity {activityId} has already been processed.", {
|
|
3257
|
+
activityId: activity.id?.href,
|
|
3258
|
+
activity: message.activity,
|
|
3259
|
+
recipient: message.identifier
|
|
3260
|
+
});
|
|
3261
|
+
recordInboxActivity(this.meterProvider, "rejected", activityType);
|
|
3262
|
+
listenerSpan.end();
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
dispatched = this.inboxListeners?.dispatchWithClass(activity);
|
|
3267
|
+
} catch (error) {
|
|
3268
|
+
if (activity == null && error instanceof RangeError && await hasMalformedKnownTemporalLiteral(parseInput, parseContextLoader)) {
|
|
3269
|
+
await reportInboxError(error);
|
|
3270
|
+
logger.error("Failed to parse the queued incoming activity {activityId}:\n{error}", {
|
|
3271
|
+
error,
|
|
3272
|
+
trial: message.attempt,
|
|
3273
|
+
activityId: null,
|
|
3274
|
+
activity: message.activity,
|
|
3275
|
+
recipient: message.identifier
|
|
3276
|
+
});
|
|
3277
|
+
listenerSpan.setStatus({
|
|
3278
|
+
code: SpanStatusCode.ERROR,
|
|
3279
|
+
message: String(error)
|
|
3280
|
+
});
|
|
3281
|
+
listenerSpan.end();
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
if (isPermanentInboxParseError(error)) {
|
|
3285
|
+
await reportInboxError(error);
|
|
3286
|
+
logger.error("Failed to parse the queued incoming activity {activityId}:\n{error}", {
|
|
3287
|
+
error,
|
|
3288
|
+
trial: message.attempt,
|
|
3289
|
+
activityId: activity?.id?.href,
|
|
3290
|
+
activity: message.activity,
|
|
3291
|
+
recipient: message.identifier
|
|
3292
|
+
});
|
|
3293
|
+
listenerSpan.setStatus({
|
|
3294
|
+
code: SpanStatusCode.ERROR,
|
|
3295
|
+
message: String(error)
|
|
3296
|
+
});
|
|
3297
|
+
listenerSpan.end();
|
|
3298
|
+
return;
|
|
3299
|
+
}
|
|
3300
|
+
await handleRetriableFailure(error);
|
|
3301
|
+
return;
|
|
3302
|
+
}
|
|
3303
|
+
if (dispatched == null) {
|
|
3304
|
+
logger.error("Unsupported activity type:\n{activity}", {
|
|
2168
3305
|
activityId: activity.id?.href,
|
|
2169
3306
|
activity: message.activity,
|
|
2170
|
-
recipient: message.identifier
|
|
3307
|
+
recipient: message.identifier,
|
|
3308
|
+
trial: message.attempt
|
|
2171
3309
|
});
|
|
2172
|
-
|
|
3310
|
+
listenerSpan.setStatus({
|
|
2173
3311
|
code: SpanStatusCode.ERROR,
|
|
2174
|
-
message:
|
|
3312
|
+
message: `Unsupported activity type: ${activityType}`
|
|
2175
3313
|
});
|
|
2176
|
-
|
|
3314
|
+
recordInboxActivity(this.meterProvider, "rejected", activityType);
|
|
3315
|
+
listenerSpan.end();
|
|
3316
|
+
return;
|
|
3317
|
+
}
|
|
3318
|
+
const { class: cls, listener } = dispatched;
|
|
3319
|
+
listenerSpan.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
|
|
3320
|
+
try {
|
|
3321
|
+
const started = performance.now();
|
|
3322
|
+
try {
|
|
3323
|
+
await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, activityType), activity);
|
|
3324
|
+
} finally {
|
|
3325
|
+
getFederationMetrics(this.meterProvider).recordInboxProcessingDuration(activityType, getDurationMs(started));
|
|
3326
|
+
}
|
|
3327
|
+
recordInboxActivity(this.meterProvider, "processed", activityType);
|
|
3328
|
+
} catch (error) {
|
|
3329
|
+
await handleRetriableFailure(error);
|
|
2177
3330
|
return;
|
|
2178
3331
|
}
|
|
2179
3332
|
if (cacheKey != null) await this.kv.set(cacheKey, true, { ttl: Temporal.Duration.from({ days: 1 }) });
|
|
2180
3333
|
logger.info("Activity {activityId} has been processed.", {
|
|
2181
|
-
activityId: activity
|
|
3334
|
+
activityId: activity?.id?.href,
|
|
2182
3335
|
activity: message.activity,
|
|
2183
3336
|
recipient: message.identifier
|
|
2184
3337
|
});
|
|
2185
|
-
|
|
3338
|
+
listenerSpan.end();
|
|
2186
3339
|
});
|
|
2187
3340
|
}
|
|
2188
3341
|
startQueue(contextData, options = {}) {
|
|
@@ -2303,6 +3456,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2303
3456
|
sharedInbox: inboxes[inbox].sharedInbox,
|
|
2304
3457
|
headers: collectionSync == null ? void 0 : new Headers({ "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) }),
|
|
2305
3458
|
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock),
|
|
3459
|
+
meterProvider: this.meterProvider,
|
|
2306
3460
|
tracerProvider: this.tracerProvider
|
|
2307
3461
|
}));
|
|
2308
3462
|
await Promise.all(promises);
|
|
@@ -2350,9 +3504,11 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2350
3504
|
});
|
|
2351
3505
|
}
|
|
2352
3506
|
const { outboxQueue } = this;
|
|
2353
|
-
if (outboxQueue.enqueueMany == null) {
|
|
2354
|
-
const
|
|
2355
|
-
|
|
3507
|
+
if (outboxQueue.enqueueMany == null || orderingKey != null) {
|
|
3508
|
+
const errors = (await Promise.allSettled(messages.map(async (m) => {
|
|
3509
|
+
await outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey });
|
|
3510
|
+
recordOutboxEnqueue(this.meterProvider, outboxQueue, m.message);
|
|
3511
|
+
}))).filter((r) => r.status === "rejected").map((r) => r.reason);
|
|
2356
3512
|
if (errors.length > 0) {
|
|
2357
3513
|
logger.error("Failed to enqueue activity {activityId} to send later: {errors}", {
|
|
2358
3514
|
activityId: activity.id.href,
|
|
@@ -2361,30 +3517,24 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2361
3517
|
if (errors.length > 1) throw new AggregateError(errors, `Failed to enqueue activity ${activityId} to send later.`);
|
|
2362
3518
|
throw errors[0];
|
|
2363
3519
|
}
|
|
2364
|
-
} else
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
logger.error("Failed to enqueue activity {activityId} to send later: {
|
|
3520
|
+
} else {
|
|
3521
|
+
try {
|
|
3522
|
+
await outboxQueue.enqueueMany(messages.map((m) => m.message));
|
|
3523
|
+
} catch (error) {
|
|
3524
|
+
logger.error("Failed to enqueue activity {activityId} to send later: {error}", {
|
|
2369
3525
|
activityId: activity.id.href,
|
|
2370
|
-
|
|
3526
|
+
error
|
|
2371
3527
|
});
|
|
2372
|
-
|
|
2373
|
-
throw errors[0];
|
|
3528
|
+
throw error;
|
|
2374
3529
|
}
|
|
2375
|
-
|
|
2376
|
-
await outboxQueue.enqueueMany(messages.map((m) => m.message));
|
|
2377
|
-
} catch (error) {
|
|
2378
|
-
logger.error("Failed to enqueue activity {activityId} to send later: {error}", {
|
|
2379
|
-
activityId: activity.id.href,
|
|
2380
|
-
error
|
|
2381
|
-
});
|
|
2382
|
-
throw error;
|
|
3530
|
+
for (const m of messages) recordOutboxEnqueue(this.meterProvider, outboxQueue, m.message);
|
|
2383
3531
|
}
|
|
2384
3532
|
}
|
|
2385
3533
|
fetch(request, options) {
|
|
2386
3534
|
return withContext({ requestId: getRequestId(request) }, async () => {
|
|
2387
3535
|
const tracer = this._getTracer();
|
|
3536
|
+
const metricState = {};
|
|
3537
|
+
const metricStart = performance.now();
|
|
2388
3538
|
return await tracer.startActiveSpan(request.method, {
|
|
2389
3539
|
kind: SpanKind.SERVER,
|
|
2390
3540
|
attributes: {
|
|
@@ -2408,10 +3558,12 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2408
3558
|
response = await this.#fetch(request, {
|
|
2409
3559
|
...options,
|
|
2410
3560
|
span,
|
|
2411
|
-
tracer
|
|
3561
|
+
tracer,
|
|
3562
|
+
metricState
|
|
2412
3563
|
});
|
|
2413
3564
|
if (acceptsJsonLd(request)) response.headers.set("Vary", "Accept");
|
|
2414
3565
|
} catch (error) {
|
|
3566
|
+
getFederationMetrics(this.meterProvider).recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), { routeTemplate: metricState.routeTemplate });
|
|
2415
3567
|
span.setStatus({
|
|
2416
3568
|
code: SpanStatusCode.ERROR,
|
|
2417
3569
|
message: `${error}`
|
|
@@ -2424,6 +3576,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2424
3576
|
});
|
|
2425
3577
|
throw error;
|
|
2426
3578
|
}
|
|
3579
|
+
getFederationMetrics(this.meterProvider).recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), {
|
|
3580
|
+
statusCode: response.status,
|
|
3581
|
+
routeTemplate: metricState.routeTemplate
|
|
3582
|
+
});
|
|
2427
3583
|
if (span.isRecording()) {
|
|
2428
3584
|
span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
|
|
2429
3585
|
for (const [k, v] of response.headers) span.setAttribute(ATTR_HTTP_RESPONSE_HEADER(k), [v]);
|
|
@@ -2449,13 +3605,18 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2449
3605
|
});
|
|
2450
3606
|
});
|
|
2451
3607
|
}
|
|
2452
|
-
async #fetch(request, { onNotFound, onNotAcceptable, onUnauthorized, contextData, span, tracer }) {
|
|
3608
|
+
async #fetch(request, { onNotFound, onNotAcceptable, onUnauthorized, contextData, span, tracer, metricState }) {
|
|
2453
3609
|
onNotFound ??= notFound;
|
|
2454
3610
|
onNotAcceptable ??= notAcceptable;
|
|
2455
3611
|
onUnauthorized ??= unauthorized;
|
|
2456
3612
|
const url = new URL(request.url);
|
|
2457
3613
|
const route = this.router.route(url.pathname);
|
|
2458
|
-
if (route == null)
|
|
3614
|
+
if (route == null) {
|
|
3615
|
+
metricState.endpoint = "not_found";
|
|
3616
|
+
return await onNotFound(request);
|
|
3617
|
+
}
|
|
3618
|
+
metricState.routeTemplate = route.template;
|
|
3619
|
+
metricState.endpoint = getEndpointCategory(route.name);
|
|
2459
3620
|
span.updateName(`${request.method} ${route.template}`);
|
|
2460
3621
|
let context = this.#createContext(request, contextData);
|
|
2461
3622
|
const routeName = route.name.replace(/:.*$/, "");
|
|
@@ -2468,26 +3629,43 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2468
3629
|
actorAliasMapper: this.actorCallbacks?.aliasMapper,
|
|
2469
3630
|
webFingerLinksDispatcher: this.webFingerLinksDispatcher,
|
|
2470
3631
|
onNotFound,
|
|
2471
|
-
tracer
|
|
3632
|
+
tracer,
|
|
3633
|
+
meterProvider: this._meterProvider
|
|
2472
3634
|
});
|
|
2473
3635
|
case "nodeInfoJrd": return await handleNodeInfoJrd(request, context);
|
|
2474
3636
|
case "nodeInfo": return await handleNodeInfo(request, {
|
|
2475
3637
|
context,
|
|
2476
3638
|
nodeInfoDispatcher: this.nodeInfoDispatcher
|
|
2477
3639
|
});
|
|
3640
|
+
case "benchmarkStats": return await handleBenchmarkStats(request, this.benchmarkMetricReader);
|
|
3641
|
+
case "benchmarkTrigger": return await handleBenchmarkTrigger(request, context, this.benchmarkTriggerOptions);
|
|
3642
|
+
}
|
|
3643
|
+
if (request.method !== "POST" && !acceptsJsonLd(request)) {
|
|
3644
|
+
metricState.endpoint = "not_acceptable";
|
|
3645
|
+
const response = await onNotAcceptable(request);
|
|
3646
|
+
const collectionRoute = getCollectionMetricRoute(routeName);
|
|
3647
|
+
if (collectionRoute != null) recordCollectionRequest(this._meterProvider, {
|
|
3648
|
+
...collectionRoute,
|
|
3649
|
+
page: url.searchParams.get("cursor") != null,
|
|
3650
|
+
result: "not_acceptable",
|
|
3651
|
+
statusCode: response.status
|
|
3652
|
+
});
|
|
3653
|
+
return response;
|
|
2478
3654
|
}
|
|
2479
|
-
if (request.method !== "POST" && !acceptsJsonLd(request)) return await onNotAcceptable(request);
|
|
2480
3655
|
switch (routeName) {
|
|
2481
3656
|
case "actor":
|
|
2482
|
-
|
|
3657
|
+
case "actorAlias": {
|
|
3658
|
+
const identifier = route.name.startsWith("actorAlias:") ? route.name.substring(ACTOR_ALIAS_PREFIX.length) : route.values.identifier;
|
|
3659
|
+
context = this.#createContext(request, contextData, { invokedFromActorDispatcher: { identifier } });
|
|
2483
3660
|
return await handleActor(request, {
|
|
2484
|
-
identifier
|
|
3661
|
+
identifier,
|
|
2485
3662
|
context,
|
|
2486
3663
|
actorDispatcher: this.actorCallbacks?.dispatcher,
|
|
2487
3664
|
authorizePredicate: this.actorCallbacks?.authorizePredicate,
|
|
2488
3665
|
onUnauthorized,
|
|
2489
3666
|
onNotFound
|
|
2490
3667
|
});
|
|
3668
|
+
}
|
|
2491
3669
|
case "object": {
|
|
2492
3670
|
const typeId = route.name.replace(/^object:/, "");
|
|
2493
3671
|
const callbacks = this.objectCallbacks[typeId];
|
|
@@ -2533,6 +3711,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2533
3711
|
context,
|
|
2534
3712
|
collectionCallbacks: this.outboxCallbacks,
|
|
2535
3713
|
tracerProvider: this.tracerProvider,
|
|
3714
|
+
meterProvider: this._meterProvider,
|
|
2536
3715
|
onUnauthorized,
|
|
2537
3716
|
onNotFound
|
|
2538
3717
|
});
|
|
@@ -2544,20 +3723,23 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2544
3723
|
context,
|
|
2545
3724
|
collectionCallbacks: this.inboxCallbacks,
|
|
2546
3725
|
tracerProvider: this.tracerProvider,
|
|
3726
|
+
meterProvider: this._meterProvider,
|
|
2547
3727
|
onUnauthorized,
|
|
2548
3728
|
onNotFound
|
|
2549
3729
|
});
|
|
2550
3730
|
context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
|
|
2551
|
-
case "sharedInbox":
|
|
3731
|
+
case "sharedInbox": {
|
|
2552
3732
|
if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
|
|
2553
3733
|
const identity = await this.sharedInboxKeyDispatcher(context);
|
|
2554
3734
|
if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
|
|
2555
3735
|
}
|
|
2556
3736
|
if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
|
|
3737
|
+
const inboxContextFactory = context.toInboxContext.bind(context);
|
|
3738
|
+
inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
|
|
2557
3739
|
return await handleInbox(request, {
|
|
2558
3740
|
recipient: route.values.identifier ?? null,
|
|
2559
3741
|
context,
|
|
2560
|
-
inboxContextFactory
|
|
3742
|
+
inboxContextFactory,
|
|
2561
3743
|
kv: this.kv,
|
|
2562
3744
|
kvPrefixes: this.kvPrefixes,
|
|
2563
3745
|
queue: this.inboxQueue,
|
|
@@ -2569,9 +3751,11 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2569
3751
|
signatureTimeWindow: this.signatureTimeWindow,
|
|
2570
3752
|
skipSignatureVerification: this.skipSignatureVerification,
|
|
2571
3753
|
inboxChallengePolicy: this.inboxChallengePolicy,
|
|
3754
|
+
meterProvider: this.meterProvider,
|
|
2572
3755
|
tracerProvider: this.tracerProvider,
|
|
2573
3756
|
idempotencyStrategy: this.idempotencyStrategy
|
|
2574
3757
|
});
|
|
3758
|
+
}
|
|
2575
3759
|
case "following": return await handleCollection(request, {
|
|
2576
3760
|
name: "following",
|
|
2577
3761
|
identifier: route.values.identifier,
|
|
@@ -2579,6 +3763,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2579
3763
|
context,
|
|
2580
3764
|
collectionCallbacks: this.followingCallbacks,
|
|
2581
3765
|
tracerProvider: this.tracerProvider,
|
|
3766
|
+
meterProvider: this._meterProvider,
|
|
2582
3767
|
onUnauthorized,
|
|
2583
3768
|
onNotFound
|
|
2584
3769
|
});
|
|
@@ -2602,6 +3787,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2602
3787
|
filterPredicate: baseUrl != null ? ((i) => (i instanceof URL ? i.href : i.id?.href ?? "").startsWith(baseUrl)) : void 0,
|
|
2603
3788
|
collectionCallbacks: this.followersCallbacks,
|
|
2604
3789
|
tracerProvider: this.tracerProvider,
|
|
3790
|
+
meterProvider: this._meterProvider,
|
|
2605
3791
|
onUnauthorized,
|
|
2606
3792
|
onNotFound
|
|
2607
3793
|
});
|
|
@@ -2613,6 +3799,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2613
3799
|
context,
|
|
2614
3800
|
collectionCallbacks: this.likedCallbacks,
|
|
2615
3801
|
tracerProvider: this.tracerProvider,
|
|
3802
|
+
meterProvider: this._meterProvider,
|
|
2616
3803
|
onUnauthorized,
|
|
2617
3804
|
onNotFound
|
|
2618
3805
|
});
|
|
@@ -2623,6 +3810,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2623
3810
|
context,
|
|
2624
3811
|
collectionCallbacks: this.featuredCallbacks,
|
|
2625
3812
|
tracerProvider: this.tracerProvider,
|
|
3813
|
+
meterProvider: this._meterProvider,
|
|
2626
3814
|
onUnauthorized,
|
|
2627
3815
|
onNotFound
|
|
2628
3816
|
});
|
|
@@ -2633,6 +3821,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2633
3821
|
context,
|
|
2634
3822
|
collectionCallbacks: this.featuredTagsCallbacks,
|
|
2635
3823
|
tracerProvider: this.tracerProvider,
|
|
3824
|
+
meterProvider: this._meterProvider,
|
|
2636
3825
|
onUnauthorized,
|
|
2637
3826
|
onNotFound
|
|
2638
3827
|
});
|
|
@@ -2645,6 +3834,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2645
3834
|
values: route.values,
|
|
2646
3835
|
collectionCallbacks: callbacks,
|
|
2647
3836
|
tracerProvider: this.tracerProvider,
|
|
3837
|
+
meterProvider: this._meterProvider,
|
|
2648
3838
|
onUnauthorized,
|
|
2649
3839
|
onNotFound
|
|
2650
3840
|
});
|
|
@@ -2658,17 +3848,64 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2658
3848
|
values: route.values,
|
|
2659
3849
|
collectionCallbacks: callbacks,
|
|
2660
3850
|
tracerProvider: this.tracerProvider,
|
|
3851
|
+
meterProvider: this._meterProvider,
|
|
2661
3852
|
onUnauthorized,
|
|
2662
3853
|
onNotFound
|
|
2663
3854
|
});
|
|
2664
3855
|
}
|
|
2665
3856
|
default: {
|
|
3857
|
+
metricState.endpoint = "not_found";
|
|
2666
3858
|
const response = onNotFound(request);
|
|
2667
3859
|
return response instanceof Promise ? await response : response;
|
|
2668
3860
|
}
|
|
2669
3861
|
}
|
|
2670
3862
|
}
|
|
2671
3863
|
};
|
|
3864
|
+
function getEndpointCategory(routeName) {
|
|
3865
|
+
if (routeName.startsWith("object:")) return "object";
|
|
3866
|
+
if (routeName.startsWith("collection:") || routeName.startsWith("orderedCollection:")) return "collection";
|
|
3867
|
+
if (routeName.startsWith("actorAlias:")) return "actor";
|
|
3868
|
+
switch (routeName) {
|
|
3869
|
+
case "webfinger": return "webfinger";
|
|
3870
|
+
case "nodeInfoJrd":
|
|
3871
|
+
case "nodeInfo": return "nodeinfo";
|
|
3872
|
+
case "actor": return "actor";
|
|
3873
|
+
case "inbox": return "inbox";
|
|
3874
|
+
case "sharedInbox": return "shared_inbox";
|
|
3875
|
+
case "outbox": return "outbox";
|
|
3876
|
+
case "following": return "following";
|
|
3877
|
+
case "followers": return "followers";
|
|
3878
|
+
case "liked": return "liked";
|
|
3879
|
+
case "featured": return "featured";
|
|
3880
|
+
case "featuredTags": return "featured_tags";
|
|
3881
|
+
case "benchmarkStats":
|
|
3882
|
+
case "benchmarkTrigger": return "benchmark";
|
|
3883
|
+
default: return "not_found";
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
function getCollectionMetricRoute(routeName) {
|
|
3887
|
+
switch (routeName) {
|
|
3888
|
+
case "inbox":
|
|
3889
|
+
case "outbox":
|
|
3890
|
+
case "following":
|
|
3891
|
+
case "followers":
|
|
3892
|
+
case "liked":
|
|
3893
|
+
case "featured": return {
|
|
3894
|
+
kind: routeName,
|
|
3895
|
+
dispatcher: "built_in"
|
|
3896
|
+
};
|
|
3897
|
+
case "featuredTags": return {
|
|
3898
|
+
kind: "featured_tags",
|
|
3899
|
+
dispatcher: "built_in"
|
|
3900
|
+
};
|
|
3901
|
+
case "collection":
|
|
3902
|
+
case "orderedCollection": return {
|
|
3903
|
+
kind: "custom",
|
|
3904
|
+
dispatcher: "custom"
|
|
3905
|
+
};
|
|
3906
|
+
default: return;
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
2672
3909
|
const FANOUT_THRESHOLD = 5;
|
|
2673
3910
|
var ContextImpl = class ContextImpl {
|
|
2674
3911
|
url;
|
|
@@ -2730,13 +3967,16 @@ var ContextImpl = class ContextImpl {
|
|
|
2730
3967
|
get tracerProvider() {
|
|
2731
3968
|
return this.federation.tracerProvider;
|
|
2732
3969
|
}
|
|
3970
|
+
get meterProvider() {
|
|
3971
|
+
return this.federation.meterProvider;
|
|
3972
|
+
}
|
|
2733
3973
|
getNodeInfoUri() {
|
|
2734
3974
|
const path = this.federation.router.build("nodeInfo", {});
|
|
2735
3975
|
if (path == null) throw new RouterError("No NodeInfo dispatcher registered.");
|
|
2736
3976
|
return new URL(path, this.canonicalOrigin);
|
|
2737
3977
|
}
|
|
2738
3978
|
getActorUri(identifier) {
|
|
2739
|
-
const path = this.federation.router.build("actor", { identifier });
|
|
3979
|
+
const path = this.federation.router.build(`actorAlias:${identifier}`, {}) ?? this.federation.router.build("actor", { identifier });
|
|
2740
3980
|
if (path == null) throw new RouterError("No actor dispatcher registered.");
|
|
2741
3981
|
return new URL(path, this.canonicalOrigin);
|
|
2742
3982
|
}
|
|
@@ -2802,8 +4042,8 @@ var ContextImpl = class ContextImpl {
|
|
|
2802
4042
|
type: "inbox",
|
|
2803
4043
|
identifier: void 0
|
|
2804
4044
|
};
|
|
2805
|
-
const identifier = route.values.identifier;
|
|
2806
|
-
if (route.name === "actor") return {
|
|
4045
|
+
const identifier = route.name.startsWith("actorAlias:") ? route.name.substring(ACTOR_ALIAS_PREFIX.length) : route.values.identifier;
|
|
4046
|
+
if (route.name === "actor" || route.name.startsWith("actorAlias:")) return {
|
|
2807
4047
|
type: "actor",
|
|
2808
4048
|
identifier
|
|
2809
4049
|
};
|
|
@@ -2969,6 +4209,7 @@ var ContextImpl = class ContextImpl {
|
|
|
2969
4209
|
contextLoader: options.contextLoader ?? this.contextLoader,
|
|
2970
4210
|
userAgent: options.userAgent ?? this.federation.userAgent,
|
|
2971
4211
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
4212
|
+
meterProvider: options.meterProvider ?? this.meterProvider,
|
|
2972
4213
|
allowPrivateAddress: this.federation.allowPrivateAddress
|
|
2973
4214
|
});
|
|
2974
4215
|
}
|
|
@@ -2995,6 +4236,7 @@ var ContextImpl = class ContextImpl {
|
|
|
2995
4236
|
...options,
|
|
2996
4237
|
userAgent: options.userAgent ?? this.federation.userAgent,
|
|
2997
4238
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
4239
|
+
meterProvider: options.meterProvider ?? this.federation._meterProvider,
|
|
2998
4240
|
allowPrivateAddress: this.federation.allowPrivateAddress
|
|
2999
4241
|
});
|
|
3000
4242
|
}
|
|
@@ -3151,6 +4393,12 @@ var ContextImpl = class ContextImpl {
|
|
|
3151
4393
|
};
|
|
3152
4394
|
if (!this.federation.manuallyStartQueue) this.federation._startQueueInternal(this.data);
|
|
3153
4395
|
await this.federation.fanoutQueue.enqueue(message, { orderingKey: options.orderingKey });
|
|
4396
|
+
getFederationMetrics(this.federation.meterProvider).recordQueueTaskEnqueued({
|
|
4397
|
+
role: "fanout",
|
|
4398
|
+
queue: this.federation.fanoutQueue,
|
|
4399
|
+
activityType: message.activityType
|
|
4400
|
+
}, 0);
|
|
4401
|
+
recordFanoutRecipients(this.federation.meterProvider, globalThis.Object.keys(message.inboxes).length, message.activityType);
|
|
3154
4402
|
return true;
|
|
3155
4403
|
}
|
|
3156
4404
|
async *getFollowers(identifier) {
|
|
@@ -3214,6 +4462,7 @@ var ContextImpl = class ContextImpl {
|
|
|
3214
4462
|
if (await verifyObject(Activity, json, {
|
|
3215
4463
|
contextLoader,
|
|
3216
4464
|
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
4465
|
+
meterProvider: this.meterProvider,
|
|
3217
4466
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
3218
4467
|
keyCache
|
|
3219
4468
|
}) == null) {
|
|
@@ -3272,6 +4521,7 @@ var ContextImpl = class ContextImpl {
|
|
|
3272
4521
|
const routeResult = await routeActivity({
|
|
3273
4522
|
context: this,
|
|
3274
4523
|
json,
|
|
4524
|
+
ldSignatureVerified: false,
|
|
3275
4525
|
activity,
|
|
3276
4526
|
recipient,
|
|
3277
4527
|
inboxListeners: this.federation.inboxListeners,
|
|
@@ -3281,6 +4531,7 @@ var ContextImpl = class ContextImpl {
|
|
|
3281
4531
|
kvPrefixes: this.federation.kvPrefixes,
|
|
3282
4532
|
queue: this.federation.inboxQueue,
|
|
3283
4533
|
span,
|
|
4534
|
+
meterProvider: this.federation.meterProvider,
|
|
3284
4535
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
3285
4536
|
idempotencyStrategy: this.federation.idempotencyStrategy
|
|
3286
4537
|
});
|
|
@@ -3355,6 +4606,7 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
3355
4606
|
contextLoader: options.contextLoader ?? this.contextLoader,
|
|
3356
4607
|
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
3357
4608
|
timeWindow: this.federation.signatureTimeWindow,
|
|
4609
|
+
meterProvider: this.meterProvider,
|
|
3358
4610
|
tracerProvider: options.tracerProvider ?? this.tracerProvider
|
|
3359
4611
|
});
|
|
3360
4612
|
}
|
|
@@ -3479,6 +4731,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
3479
4731
|
activityType: ctx.activityType,
|
|
3480
4732
|
inbox: new URL(inbox),
|
|
3481
4733
|
sharedInbox: inboxes[inbox].sharedInbox,
|
|
4734
|
+
meterProvider: ctx.meterProvider,
|
|
3482
4735
|
tracerProvider: ctx.tracerProvider,
|
|
3483
4736
|
specDeterminer: new KvSpecDeterminer(ctx.federation.kv, ctx.federation.kvPrefixes.httpMessageSignaturesSpec, ctx.federation.firstKnock)
|
|
3484
4737
|
}));
|
|
@@ -3529,8 +4782,10 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
3529
4782
|
}
|
|
3530
4783
|
const { outboxQueue } = ctx.federation;
|
|
3531
4784
|
if (outboxQueue.enqueueMany == null || orderingKey != null) {
|
|
3532
|
-
const
|
|
3533
|
-
|
|
4785
|
+
const errors = (await Promise.allSettled(messages.map(async (m) => {
|
|
4786
|
+
await outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey });
|
|
4787
|
+
recordOutboxEnqueue(ctx.federation.meterProvider, outboxQueue, m.message);
|
|
4788
|
+
}))).filter((r) => r.status === "rejected").map((r) => r.reason);
|
|
3534
4789
|
if (errors.length > 0) {
|
|
3535
4790
|
logger.error("Failed to enqueue activity {activityId} to forward later:\n{errors}", {
|
|
3536
4791
|
activityId: ctx.activityId,
|
|
@@ -3539,19 +4794,30 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
3539
4794
|
if (errors.length > 1) throw new AggregateError(errors, `Failed to enqueue activity ${ctx.activityId} to forward later.`);
|
|
3540
4795
|
throw errors[0];
|
|
3541
4796
|
}
|
|
3542
|
-
} else
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
activityId
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
4797
|
+
} else {
|
|
4798
|
+
try {
|
|
4799
|
+
await outboxQueue.enqueueMany(messages.map((m) => m.message));
|
|
4800
|
+
} catch (error) {
|
|
4801
|
+
logger.error("Failed to enqueue activity {activityId} to forward later:\n{error}", {
|
|
4802
|
+
activityId: ctx.activityId,
|
|
4803
|
+
error
|
|
4804
|
+
});
|
|
4805
|
+
throw error;
|
|
4806
|
+
}
|
|
4807
|
+
for (const m of messages) recordOutboxEnqueue(ctx.federation.meterProvider, outboxQueue, m.message);
|
|
3550
4808
|
}
|
|
3551
4809
|
return true;
|
|
3552
4810
|
}
|
|
3553
4811
|
var InboxContextImpl = class InboxContextImpl extends ContextImpl {
|
|
3554
4812
|
recipient;
|
|
4813
|
+
/**
|
|
4814
|
+
* The original received activity payload.
|
|
4815
|
+
*
|
|
4816
|
+
* Fedify may normalize a Linked Data Signature payload internally for safe
|
|
4817
|
+
* parsing, but forwarding must keep the sender's payload unchanged so
|
|
4818
|
+
* third-party signatures/proofs remain intact.
|
|
4819
|
+
* @internal
|
|
4820
|
+
*/
|
|
3555
4821
|
activity;
|
|
3556
4822
|
activityId;
|
|
3557
4823
|
activityType;
|
|
@@ -3684,4 +4950,4 @@ function getRequestId(request) {
|
|
|
3684
4950
|
return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
3685
4951
|
}
|
|
3686
4952
|
//#endregion
|
|
3687
|
-
export {
|
|
4953
|
+
export { handleNodeInfo as _, OutboxContextImpl as a, autoIdAssigner as b, handleActor as c, handleInbox as d, handleObject as f, handleBenchmarkTrigger as g, respondWithObjectIfAcceptable as h, KvSpecDeterminer as i, handleCollection as l, respondWithObject as m, FederationImpl as n, createFederation as o, handleOutbox as p, InboxContextImpl as r, handleWebFinger as s, ContextImpl as t, handleCustomCollection as u, handleNodeInfoJrd as v, actorDehydrator as y };
|