@fedify/fedify 2.3.0-dev.994 → 2.3.0-pr.809.36
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-DSZ7FZmg.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-DCdM7d93.mjs} +1 -1
- package/dist/{docloader-DPp-X6gk.mjs → docloader-B7mVwA5_.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-CSlLA54v.mjs → http-BufbkLuW.mjs} +146 -47
- package/dist/{http-B4rrzxtg.js → http-Dh18nwJg.js} +994 -64
- package/dist/{http-aQzN9Ayi.d.ts → http-VyDTd4G3.d.cts} +15 -3
- package/dist/{http-BrGqJDXL.cjs → http-gOiudB3g.cjs} +1099 -61
- package/dist/{http-CrGuipxe.d.cts → http-lf8Hsd91.d.ts} +15 -1
- package/dist/{key-Bzx--sHD.mjs → key-DZdKLJXT.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-BuK_nlpY.cjs} +20 -3
- package/dist/{kv-cache-U__xU4qR.mjs → kv-cache-C7SQnkGI.mjs} +21 -3
- package/dist/{kv-cache-EMIqoIuB.js → kv-cache-DNDSb6Po.js} +20 -3
- package/dist/{kv-GFYnFoOl.d.ts → kv-gJ8LYbxX.d.cts} +1 -3
- package/dist/ld-0Lsznacf.mjs +626 -0
- package/dist/metrics-BHeWd24f.mjs +815 -0
- package/dist/{middleware-zjZ6AFCW.mjs → middleware-BNANvDh2.mjs} +1 -1
- package/dist/{middleware-nidH7VZH.js → middleware-BgzRkDcm.js} +2260 -556
- package/dist/{middleware-An4DjES4.cjs → middleware-D-iES1nQ.cjs} +2318 -623
- package/dist/{middleware-K1g6bEkV.mjs → middleware-EV6J_eAc.mjs} +1639 -373
- 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-Cl-1cAR8.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-Bbbwf8_x.js → proof-CyXndf6-.js} +378 -15
- package/dist/{proof-BlmRPzrK.mjs → proof-Dnz8jtiQ.mjs} +26 -8
- package/dist/{proof-BW89QMVB.cjs → proof-tz35unOj.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-DIbBYN5P.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-BEwob1Vg.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,21 +1,22 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
|
-
import "urlpattern-polyfill";
|
|
3
|
-
import { t as __exportAll } from "./chunk-
|
|
4
|
-
import { r as getDefaultActivityTransformers } from "./transformers-
|
|
5
|
-
import {
|
|
6
|
-
import { c as getKeyOwner,
|
|
7
|
-
import { n as getNodeInfo, t as nodeInfoToJson } from "./types-
|
|
8
|
-
import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-
|
|
2
|
+
import { URLPattern } from "urlpattern-polyfill";
|
|
3
|
+
import { t as __exportAll } from "./chunk-CRNNMoPX.js";
|
|
4
|
+
import { r as getDefaultActivityTransformers } from "./transformers-BGMIq1cs.js";
|
|
5
|
+
import { A as formatAcceptSignature, D as recordOutboxEnqueue, E as recordOutboxActivity, F as version, O as recordWebFingerHandle, P as name, S as recordCollectionTotalItems, T as recordInboxActivity, a as verifyRequestDetailed, b as recordCollectionPageItems, d as validateCryptoKey, f as getDurationMs, g as isAbortError, h as instrumentDocumentLoader, i as verifyRequest, k as registerQueueDepthGauge, m as getRemoteHost, n as parseRfc9421SignatureInput, o as exportJwk, p as getFederationMetrics, t as doubleKnock, u as importJwk, v as recordCircuitBreakerStateChange, w as recordFanoutRecipients, x as recordCollectionRequest, y as recordCollectionDispatchDuration } from "./http-Dh18nwJg.js";
|
|
6
|
+
import { _ as hasSignatureLike, b as signJsonLd, c as getKeyOwner, f as compactJsonLd, g as hasSignature, h as getNormalizationContextLoader, i as verifyObject, l as InvalidContextReferenceError, m as detachSignature, n as hasProofLike, o as normalizeOutgoingActivityJsonLd, r as signObject, s as doesActorOwnKey, u as assertSafeJsonLd, v as isClearlyMalformedContextReference, w as wrapContextLoaderForJsonLd, x as verifyCompactJsonLd, y as isInvalidUrlTypeError } from "./proof-CyXndf6-.js";
|
|
7
|
+
import { n as getNodeInfo, t as nodeInfoToJson } from "./types-CAY3OdLq.js";
|
|
8
|
+
import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DNDSb6Po.js";
|
|
9
9
|
import { getLogger, withContext } from "@logtape/logtape";
|
|
10
|
+
import { Router, RouterError, assertPath } from "@fedify/uri-template";
|
|
10
11
|
import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
11
|
-
import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
|
|
12
|
-
import { cloneDeep, uniq } from "es-toolkit";
|
|
13
|
-
import { Router } from "uri-template-router";
|
|
14
|
-
import { parseTemplate } from "url-template";
|
|
12
|
+
import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
|
|
15
13
|
import { encodeHex } from "byte-encodings/hex";
|
|
14
|
+
import { uniq } from "es-toolkit";
|
|
16
15
|
import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
|
|
17
16
|
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";
|
|
17
|
+
import jsonld from "@fedify/vocab-runtime/jsonld";
|
|
18
18
|
import { lookupWebFinger } from "@fedify/webfinger";
|
|
19
|
+
import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
|
|
19
20
|
import { domainToASCII } from "node:url";
|
|
20
21
|
//#region src/federation/activity-listener.ts
|
|
21
22
|
var ActivityListenerSet = class {
|
|
@@ -52,125 +53,33 @@ var ActivityListenerSet = class {
|
|
|
52
53
|
}
|
|
53
54
|
};
|
|
54
55
|
//#endregion
|
|
55
|
-
//#region src/federation/
|
|
56
|
-
|
|
57
|
-
const clone = new Router();
|
|
58
|
-
clone.nid = router.nid;
|
|
59
|
-
clone.fsm = cloneDeep(router.fsm);
|
|
60
|
-
clone.routeSet = new Set(router.routeSet);
|
|
61
|
-
clone.templateRouteMap = new Map(router.templateRouteMap);
|
|
62
|
-
clone.valueRouteMap = new Map(router.valueRouteMap);
|
|
63
|
-
clone.hierarchy = cloneDeep(router.hierarchy);
|
|
64
|
-
return clone;
|
|
65
|
-
}
|
|
56
|
+
//#region src/federation/builder.ts
|
|
57
|
+
const ACTOR_ALIAS_PREFIX = "actorAlias:";
|
|
66
58
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
59
|
+
* Route options shared by every dispatcher whose path must expose exactly
|
|
60
|
+
* one `{identifier}` variable bound to a single, non-empty value
|
|
61
|
+
* for `setOutboxDispatcher/Listener()`.
|
|
69
62
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
#templateStrings;
|
|
74
|
-
/**
|
|
75
|
-
* Whether to ignore trailing slashes when matching paths.
|
|
76
|
-
* @since 1.6.0
|
|
77
|
-
*/
|
|
78
|
-
trailingSlashInsensitive;
|
|
79
|
-
/**
|
|
80
|
-
* Create a new {@link Router}.
|
|
81
|
-
* @param options Options for the router.
|
|
82
|
-
*/
|
|
83
|
-
constructor(options = {}) {
|
|
84
|
-
this.#router = new Router();
|
|
85
|
-
this.#templates = {};
|
|
86
|
-
this.#templateStrings = {};
|
|
87
|
-
this.trailingSlashInsensitive = options.trailingSlashInsensitive ?? false;
|
|
88
|
-
}
|
|
89
|
-
clone() {
|
|
90
|
-
const clone = new Router$1({ trailingSlashInsensitive: this.trailingSlashInsensitive });
|
|
91
|
-
clone.#router = cloneInnerRouter(this.#router);
|
|
92
|
-
clone.#templates = { ...this.#templates };
|
|
93
|
-
clone.#templateStrings = { ...this.#templateStrings };
|
|
94
|
-
return clone;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Checks if a path name exists in the router.
|
|
98
|
-
* @param name The name of the path.
|
|
99
|
-
* @returns `true` if the path name exists, otherwise `false`.
|
|
100
|
-
*/
|
|
101
|
-
has(name) {
|
|
102
|
-
return name in this.#templates;
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Adds a new path rule to the router.
|
|
106
|
-
* @param template The path pattern.
|
|
107
|
-
* @param name The name of the path.
|
|
108
|
-
* @returns The names of the variables in the path pattern.
|
|
109
|
-
*/
|
|
110
|
-
add(template, name) {
|
|
111
|
-
if (!template.startsWith("/")) throw new RouterError("Path must start with a slash.");
|
|
112
|
-
const rule = this.#router.addTemplate(template, {}, name);
|
|
113
|
-
this.#templates[name] = parseTemplate(template);
|
|
114
|
-
this.#templateStrings[name] = template;
|
|
115
|
-
return new Set(rule.variables.map((v) => v.varname));
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Resolves a path name and values from a URL, if any match.
|
|
119
|
-
* @param url The URL to resolve.
|
|
120
|
-
* @returns The name of the path and its values, if any match. Otherwise,
|
|
121
|
-
* `null`.
|
|
122
|
-
*/
|
|
123
|
-
route(url) {
|
|
124
|
-
let match = this.#router.resolveURI(url);
|
|
125
|
-
if (match == null) {
|
|
126
|
-
if (!this.trailingSlashInsensitive) return null;
|
|
127
|
-
url = url.endsWith("/") ? url.replace(/\/+$/, "") : `${url}/`;
|
|
128
|
-
match = this.#router.resolveURI(url);
|
|
129
|
-
if (match == null) return null;
|
|
130
|
-
}
|
|
131
|
-
return {
|
|
132
|
-
name: match.matchValue,
|
|
133
|
-
template: this.#templateStrings[match.matchValue],
|
|
134
|
-
values: match.params
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Constructs a URL/path from a path name and values.
|
|
139
|
-
* @param name The name of the path.
|
|
140
|
-
* @param values The values to expand the path with.
|
|
141
|
-
* @returns The URL/path, if the name exists. Otherwise, `null`.
|
|
142
|
-
*/
|
|
143
|
-
build(name, values) {
|
|
144
|
-
if (name in this.#templates) return this.#templates[name].expand(values);
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
63
|
+
const identifierSingular = {
|
|
64
|
+
exact: true,
|
|
65
|
+
variables: { identifier: { operatables: [""] } }
|
|
147
66
|
};
|
|
148
67
|
/**
|
|
149
|
-
*
|
|
68
|
+
* Route options shared by every dispatcher whose path must expose
|
|
69
|
+
* `{identifier}` and `{+identifier}` variables bound to the same single,
|
|
70
|
+
* non-empty value for following setters:
|
|
71
|
+
* - `setActorDispatcher()` (actor path)
|
|
72
|
+
* - `setInboxDispatcher/Listener()` (inbox path)
|
|
73
|
+
* - `setFollowingDispatcher()` (following path)
|
|
74
|
+
* - `setFollowersDispatcher()` (followers path)
|
|
75
|
+
* - `setLikedDispatcher()` (liked path)
|
|
76
|
+
* - `setFeaturedDispatcher()` (featured path)
|
|
77
|
+
* - `setFeaturedTagsDispatcher()` (featured tags path)
|
|
150
78
|
*/
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
* @param message The error message.
|
|
155
|
-
*/
|
|
156
|
-
constructor(message) {
|
|
157
|
-
super(message);
|
|
158
|
-
this.name = "RouterError";
|
|
159
|
-
}
|
|
79
|
+
const identifierSingularAllowPlus = {
|
|
80
|
+
exact: true,
|
|
81
|
+
variables: { identifier: { operatables: ["", "+"] } }
|
|
160
82
|
};
|
|
161
|
-
//#endregion
|
|
162
|
-
//#region src/federation/builder.ts
|
|
163
|
-
function validateSingleIdentifierVariablePath(path, errorMessage) {
|
|
164
|
-
const operatorMatches = globalThis.Array.from(path.matchAll(/{([+#./;?&]?)([A-Za-z_][A-Za-z0-9_]*)}/g));
|
|
165
|
-
if (operatorMatches.length !== 1 || operatorMatches[0]?.[2] !== "identifier") throw new RouterError(errorMessage);
|
|
166
|
-
if (operatorMatches.some((match) => [
|
|
167
|
-
"?",
|
|
168
|
-
"&",
|
|
169
|
-
"#"
|
|
170
|
-
].includes(match[1]) && match[2] === "identifier")) throw new RouterError(errorMessage);
|
|
171
|
-
const variables = new Router$1().add(path, "outbox");
|
|
172
|
-
if (variables.size !== 1 || !variables.has("identifier")) throw new RouterError(errorMessage);
|
|
173
|
-
}
|
|
174
83
|
var FederationBuilderImpl = class {
|
|
175
84
|
router;
|
|
176
85
|
actorCallbacks;
|
|
@@ -203,12 +112,19 @@ var FederationBuilderImpl = class {
|
|
|
203
112
|
*/
|
|
204
113
|
#symbolRegistry = /* @__PURE__ */ new Map();
|
|
205
114
|
constructor() {
|
|
206
|
-
this.router = new Router
|
|
115
|
+
this.router = new Router();
|
|
207
116
|
this.objectCallbacks = {};
|
|
208
117
|
this.objectTypeIds = {};
|
|
209
118
|
this.collectionCallbacks = {};
|
|
210
119
|
this.collectionTypeIds = {};
|
|
211
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Builds the federation object.
|
|
123
|
+
* @param options Parameters for initializing the federation object.
|
|
124
|
+
* @returns The federation object.
|
|
125
|
+
* @throws {TypeError} If benchmark mode and `meterProvider` are both
|
|
126
|
+
* specified.
|
|
127
|
+
*/
|
|
212
128
|
async build(options) {
|
|
213
129
|
const { FederationImpl } = await Promise.resolve().then(() => middleware_exports);
|
|
214
130
|
const f = new FederationImpl(options);
|
|
@@ -246,8 +162,8 @@ var FederationBuilderImpl = class {
|
|
|
246
162
|
}
|
|
247
163
|
setActorDispatcher(path, dispatcher) {
|
|
248
164
|
if (this.router.has("actor")) throw new RouterError("Actor dispatcher already set.");
|
|
249
|
-
|
|
250
|
-
|
|
165
|
+
assertPath(path);
|
|
166
|
+
this.router.add(path, "actor", identifierSingularAllowPlus);
|
|
251
167
|
const callbacks = { dispatcher: async (context, identifier) => {
|
|
252
168
|
const actor = await this._getTracer().startActiveSpan("activitypub.dispatch_actor", {
|
|
253
169
|
kind: SpanKind.SERVER,
|
|
@@ -346,6 +262,15 @@ var FederationBuilderImpl = class {
|
|
|
346
262
|
callbacks.aliasMapper = mapper;
|
|
347
263
|
return setters;
|
|
348
264
|
},
|
|
265
|
+
mapActorAlias: (path, identifier) => {
|
|
266
|
+
if (identifier === "") throw new RouterError("Identifier cannot be empty.");
|
|
267
|
+
if (this.router.has(`actorAlias:${identifier}`)) throw new RouterError(`Actor alias for "${identifier}" already set.`);
|
|
268
|
+
if (Router.variables(path).size > 0) throw new RouterError("Path for actor alias must have no variables.");
|
|
269
|
+
const existingRoute = this.router.route(path);
|
|
270
|
+
if (existingRoute != null) throw new RouterError(`Actor alias path "${path}" conflicts with existing route "${existingRoute.name}".`);
|
|
271
|
+
this.router.add(path, `${ACTOR_ALIAS_PREFIX}${identifier}`);
|
|
272
|
+
return setters;
|
|
273
|
+
},
|
|
349
274
|
authorize(predicate) {
|
|
350
275
|
callbacks.authorizePredicate = predicate;
|
|
351
276
|
return setters;
|
|
@@ -355,17 +280,25 @@ var FederationBuilderImpl = class {
|
|
|
355
280
|
}
|
|
356
281
|
setNodeInfoDispatcher(path, dispatcher) {
|
|
357
282
|
if (this.router.has("nodeInfo")) throw new RouterError("NodeInfo dispatcher already set.");
|
|
358
|
-
if (
|
|
283
|
+
if (Router.variables(path).size !== 0) throw new RouterError("Path for NodeInfo dispatcher must have no variables.");
|
|
284
|
+
this.router.add(path, "nodeInfo");
|
|
359
285
|
this.nodeInfoDispatcher = dispatcher;
|
|
360
286
|
}
|
|
361
287
|
setWebFingerLinksDispatcher(dispatcher) {
|
|
362
288
|
this.webFingerLinksDispatcher = dispatcher;
|
|
363
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* The RFC 6570 template-literal `path` overloads were removed for
|
|
292
|
+
* type-checking efficiency, so the URI variable types can no longer be
|
|
293
|
+
* inferred from `path` (now typed as a plain `string`); to use them, specify
|
|
294
|
+
* the variable name through the `TParam` generic argument.
|
|
295
|
+
*/
|
|
364
296
|
setObjectDispatcher(cls, path, dispatcher) {
|
|
365
297
|
const routeName = `object:${cls.typeId.href}`;
|
|
366
298
|
if (this.router.has(routeName)) throw new RouterError(`Object dispatcher for ${cls.name} already set.`);
|
|
367
|
-
|
|
368
|
-
|
|
299
|
+
assertPath(path);
|
|
300
|
+
const variables = Router.variables(path);
|
|
301
|
+
this.router.add(path, routeName);
|
|
369
302
|
const callbacks = {
|
|
370
303
|
dispatcher: (ctx, values) => {
|
|
371
304
|
return this._getTracer().startActiveSpan("activitypub.dispatch_object", {
|
|
@@ -407,8 +340,8 @@ var FederationBuilderImpl = class {
|
|
|
407
340
|
if (this.router.has("inbox")) {
|
|
408
341
|
if (this.inboxPath !== path) throw new RouterError("Inbox dispatcher path must match inbox listener path.");
|
|
409
342
|
} else {
|
|
410
|
-
|
|
411
|
-
|
|
343
|
+
assertPath(path);
|
|
344
|
+
this.router.add(path, "inbox", identifierSingularAllowPlus);
|
|
412
345
|
this.inboxPath = path;
|
|
413
346
|
}
|
|
414
347
|
const callbacks = { dispatcher };
|
|
@@ -438,8 +371,8 @@ var FederationBuilderImpl = class {
|
|
|
438
371
|
if (this.router.has("outbox")) {
|
|
439
372
|
if (this.outboxPath !== path) throw new RouterError("Outbox dispatcher path must match outbox listener path.");
|
|
440
373
|
} else {
|
|
441
|
-
|
|
442
|
-
this.router.add(path, "outbox");
|
|
374
|
+
assertPath(path);
|
|
375
|
+
this.router.add(path, "outbox", identifierSingular);
|
|
443
376
|
this.outboxPath = path;
|
|
444
377
|
}
|
|
445
378
|
const callbacks = { dispatcher };
|
|
@@ -469,8 +402,8 @@ var FederationBuilderImpl = class {
|
|
|
469
402
|
if (this.router.has("outbox")) {
|
|
470
403
|
if (this.outboxPath !== outboxPath) throw new RouterError("Outbox listener path must match outbox dispatcher path.");
|
|
471
404
|
} else {
|
|
472
|
-
|
|
473
|
-
this.router.add(outboxPath, "outbox");
|
|
405
|
+
assertPath(outboxPath);
|
|
406
|
+
this.router.add(outboxPath, "outbox", identifierSingular);
|
|
474
407
|
this.outboxPath = outboxPath;
|
|
475
408
|
}
|
|
476
409
|
const listeners = this.outboxListeners = new ActivityListenerSet();
|
|
@@ -492,8 +425,8 @@ var FederationBuilderImpl = class {
|
|
|
492
425
|
}
|
|
493
426
|
setFollowingDispatcher(path, dispatcher) {
|
|
494
427
|
if (this.router.has("following")) throw new RouterError("Following collection dispatcher already set.");
|
|
495
|
-
|
|
496
|
-
|
|
428
|
+
assertPath(path);
|
|
429
|
+
this.router.add(path, "following", identifierSingularAllowPlus);
|
|
497
430
|
const callbacks = { dispatcher };
|
|
498
431
|
this.followingCallbacks = callbacks;
|
|
499
432
|
const setters = {
|
|
@@ -518,8 +451,8 @@ var FederationBuilderImpl = class {
|
|
|
518
451
|
}
|
|
519
452
|
setFollowersDispatcher(path, dispatcher) {
|
|
520
453
|
if (this.router.has("followers")) throw new RouterError("Followers collection dispatcher already set.");
|
|
521
|
-
|
|
522
|
-
|
|
454
|
+
assertPath(path);
|
|
455
|
+
this.router.add(path, "followers", identifierSingularAllowPlus);
|
|
523
456
|
const callbacks = { dispatcher };
|
|
524
457
|
this.followersCallbacks = callbacks;
|
|
525
458
|
const setters = {
|
|
@@ -544,8 +477,8 @@ var FederationBuilderImpl = class {
|
|
|
544
477
|
}
|
|
545
478
|
setLikedDispatcher(path, dispatcher) {
|
|
546
479
|
if (this.router.has("liked")) throw new RouterError("Liked collection dispatcher already set.");
|
|
547
|
-
|
|
548
|
-
|
|
480
|
+
assertPath(path);
|
|
481
|
+
this.router.add(path, "liked", identifierSingularAllowPlus);
|
|
549
482
|
const callbacks = { dispatcher };
|
|
550
483
|
this.likedCallbacks = callbacks;
|
|
551
484
|
const setters = {
|
|
@@ -570,8 +503,8 @@ var FederationBuilderImpl = class {
|
|
|
570
503
|
}
|
|
571
504
|
setFeaturedDispatcher(path, dispatcher) {
|
|
572
505
|
if (this.router.has("featured")) throw new RouterError("Featured collection dispatcher already set.");
|
|
573
|
-
|
|
574
|
-
|
|
506
|
+
assertPath(path);
|
|
507
|
+
this.router.add(path, "featured", identifierSingularAllowPlus);
|
|
575
508
|
const callbacks = { dispatcher };
|
|
576
509
|
this.featuredCallbacks = callbacks;
|
|
577
510
|
const setters = {
|
|
@@ -596,8 +529,8 @@ var FederationBuilderImpl = class {
|
|
|
596
529
|
}
|
|
597
530
|
setFeaturedTagsDispatcher(path, dispatcher) {
|
|
598
531
|
if (this.router.has("featuredTags")) throw new RouterError("Featured tags collection dispatcher already set.");
|
|
599
|
-
|
|
600
|
-
|
|
532
|
+
assertPath(path);
|
|
533
|
+
this.router.add(path, "featuredTags", identifierSingularAllowPlus);
|
|
601
534
|
const callbacks = { dispatcher };
|
|
602
535
|
this.featuredTagsCallbacks = callbacks;
|
|
603
536
|
const setters = {
|
|
@@ -625,12 +558,13 @@ var FederationBuilderImpl = class {
|
|
|
625
558
|
if (this.router.has("inbox")) {
|
|
626
559
|
if (this.inboxPath !== inboxPath) throw new RouterError("Inbox listener path must match inbox dispatcher path.");
|
|
627
560
|
} else {
|
|
628
|
-
|
|
629
|
-
|
|
561
|
+
assertPath(inboxPath);
|
|
562
|
+
this.router.add(inboxPath, "inbox", identifierSingularAllowPlus);
|
|
630
563
|
this.inboxPath = inboxPath;
|
|
631
564
|
}
|
|
632
565
|
if (sharedInboxPath != null) {
|
|
633
|
-
if (
|
|
566
|
+
if (Router.variables(sharedInboxPath).size !== 0) throw new RouterError("Path for shared inbox must have no variables.");
|
|
567
|
+
this.router.add(sharedInboxPath, "sharedInbox");
|
|
634
568
|
}
|
|
635
569
|
const listeners = this.inboxListeners = new ActivityListenerSet();
|
|
636
570
|
const setters = {
|
|
@@ -657,9 +591,21 @@ var FederationBuilderImpl = class {
|
|
|
657
591
|
};
|
|
658
592
|
return setters;
|
|
659
593
|
}
|
|
594
|
+
/**
|
|
595
|
+
* The RFC 6570 template-literal `path` overloads were removed for
|
|
596
|
+
* type-checking efficiency, so the URI variable types can no longer be
|
|
597
|
+
* inferred from `path` (now typed as a plain `string`); to use them, specify
|
|
598
|
+
* the variable name through the `TParam` generic argument.
|
|
599
|
+
*/
|
|
660
600
|
setCollectionDispatcher(name, itemType, path, dispatcher) {
|
|
661
601
|
return this.#setCustomCollectionDispatcher(name, "collection", itemType, path, dispatcher);
|
|
662
602
|
}
|
|
603
|
+
/**
|
|
604
|
+
* The RFC 6570 template-literal `path` overloads were removed for
|
|
605
|
+
* type-checking efficiency, so the URI variable types can no longer be
|
|
606
|
+
* inferred from `path` (now typed as a plain `string`); to use them, specify
|
|
607
|
+
* the variable name through the `TParam` generic argument.
|
|
608
|
+
*/
|
|
663
609
|
setOrderedCollectionDispatcher(name, itemType, path, dispatcher) {
|
|
664
610
|
return this.#setCustomCollectionDispatcher(name, "orderedCollection", itemType, path, dispatcher);
|
|
665
611
|
}
|
|
@@ -668,7 +614,9 @@ var FederationBuilderImpl = class {
|
|
|
668
614
|
const routeName = `${collectionType}:${this.#uniqueCollectionId(name)}`;
|
|
669
615
|
if (this.router.has(routeName)) throw new RouterError(`Collection dispatcher for ${strName} already set.`);
|
|
670
616
|
if (this.collectionCallbacks[name] != null) throw new RouterError(`Collection dispatcher for ${strName} already set.`);
|
|
671
|
-
|
|
617
|
+
assertPath(path);
|
|
618
|
+
if (Router.variables(path).size < 1) throw new RouterError("Path for collection dispatcher must have at least one variable.");
|
|
619
|
+
this.router.add(path, routeName);
|
|
672
620
|
const callbacks = { dispatcher };
|
|
673
621
|
this.collectionCallbacks[name] = callbacks;
|
|
674
622
|
this.collectionTypeIds[name] = itemType;
|
|
@@ -730,6 +678,338 @@ function createFederationBuilder() {
|
|
|
730
678
|
return new FederationBuilderImpl();
|
|
731
679
|
}
|
|
732
680
|
//#endregion
|
|
681
|
+
//#region src/federation/circuit-breaker.ts
|
|
682
|
+
const MAX_CUSTOM_FAILURE_HISTORY = 100;
|
|
683
|
+
/**
|
|
684
|
+
* Tracks reachability state for remote outbox delivery hosts.
|
|
685
|
+
* @since 2.3.0
|
|
686
|
+
*/
|
|
687
|
+
var CircuitBreaker = class {
|
|
688
|
+
#kv;
|
|
689
|
+
#prefix;
|
|
690
|
+
#options;
|
|
691
|
+
#now;
|
|
692
|
+
#stateChangeObserver;
|
|
693
|
+
constructor(options) {
|
|
694
|
+
this.#kv = options.kv;
|
|
695
|
+
this.#prefix = options.prefix;
|
|
696
|
+
this.#options = normalizeCircuitBreakerOptions(options.options ?? {});
|
|
697
|
+
this.#now = options.now ?? (() => Temporal.Now.instant());
|
|
698
|
+
this.#stateChangeObserver = options.stateChangeObserver;
|
|
699
|
+
}
|
|
700
|
+
get options() {
|
|
701
|
+
return this.#options;
|
|
702
|
+
}
|
|
703
|
+
capHeldDelay(heldSince, delay) {
|
|
704
|
+
const now = this.#now();
|
|
705
|
+
return now.until(this.#capHeldRetryAt(now, heldSince, now.add(delay)));
|
|
706
|
+
}
|
|
707
|
+
async beforeSend(remoteHost, message) {
|
|
708
|
+
const heldSince = parseHeldSince(message.circuitHeldSince);
|
|
709
|
+
const now = this.#now();
|
|
710
|
+
if (heldSince != null && Temporal.Instant.compare(heldSince.add(this.#options.heldActivityTtl), now) <= 0) return {
|
|
711
|
+
type: "drop",
|
|
712
|
+
heldSince
|
|
713
|
+
};
|
|
714
|
+
let lastConflictingState;
|
|
715
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
716
|
+
const oldState = await this.#get(remoteHost);
|
|
717
|
+
if (oldState == null || oldState.state === "closed") return {
|
|
718
|
+
type: "send",
|
|
719
|
+
probe: false
|
|
720
|
+
};
|
|
721
|
+
if (oldState.state === "half-open") {
|
|
722
|
+
const halfOpened = oldState.halfOpened == null ? void 0 : Temporal.Instant.from(oldState.halfOpened);
|
|
723
|
+
if (halfOpened != null) {
|
|
724
|
+
const staleAt = halfOpened.add(this.#options.recoveryDelay);
|
|
725
|
+
if (Temporal.Instant.compare(now, staleAt) < 0) {
|
|
726
|
+
const releaseAt = now.add(this.#options.releaseInterval);
|
|
727
|
+
const retryAt = Temporal.Instant.compare(releaseAt, staleAt) < 0 ? releaseAt : staleAt;
|
|
728
|
+
const cappedRetryAt = this.#capHeldRetryAt(now, heldSince, retryAt);
|
|
729
|
+
return {
|
|
730
|
+
type: "hold",
|
|
731
|
+
state: "half-open",
|
|
732
|
+
delay: now.until(cappedRetryAt),
|
|
733
|
+
heldSince: heldSince ?? now
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
const newState = {
|
|
738
|
+
...oldState,
|
|
739
|
+
state: "half-open",
|
|
740
|
+
halfOpened: now.toString()
|
|
741
|
+
};
|
|
742
|
+
if (await this.#replace(remoteHost, oldState, newState)) return {
|
|
743
|
+
type: "send",
|
|
744
|
+
probe: true
|
|
745
|
+
};
|
|
746
|
+
lastConflictingState = "half-open";
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
const probeAt = (oldState.opened == null ? now : Temporal.Instant.from(oldState.opened)).add(this.#options.recoveryDelay);
|
|
750
|
+
if (Temporal.Instant.compare(now, probeAt) < 0) {
|
|
751
|
+
const retryAt = this.#capHeldRetryAt(now, heldSince, probeAt);
|
|
752
|
+
return {
|
|
753
|
+
type: "hold",
|
|
754
|
+
state: "open",
|
|
755
|
+
delay: now.until(retryAt),
|
|
756
|
+
heldSince: heldSince ?? now
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const newState = {
|
|
760
|
+
...oldState,
|
|
761
|
+
state: "half-open",
|
|
762
|
+
halfOpened: now.toString()
|
|
763
|
+
};
|
|
764
|
+
if (await this.#replace(remoteHost, oldState, newState)) {
|
|
765
|
+
await this.#notifyStateChange(remoteHost, "open", "half-open");
|
|
766
|
+
return {
|
|
767
|
+
type: "send",
|
|
768
|
+
probe: true,
|
|
769
|
+
stateChange: {
|
|
770
|
+
previousState: "open",
|
|
771
|
+
newState: "half-open"
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
lastConflictingState = "open";
|
|
776
|
+
}
|
|
777
|
+
if (lastConflictingState != null) {
|
|
778
|
+
const retryAt = this.#capHeldRetryAt(now, heldSince, now.add(this.#options.releaseInterval));
|
|
779
|
+
return {
|
|
780
|
+
type: "hold",
|
|
781
|
+
state: lastConflictingState,
|
|
782
|
+
delay: now.until(retryAt),
|
|
783
|
+
heldSince: heldSince ?? now
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
throw new Error(`Failed to update circuit breaker state for ${remoteHost}`);
|
|
787
|
+
}
|
|
788
|
+
async recordSuccess(remoteHost) {
|
|
789
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
790
|
+
const oldState = await this.#get(remoteHost);
|
|
791
|
+
if (oldState == null) return void 0;
|
|
792
|
+
if (await this.#replace(remoteHost, oldState, void 0)) {
|
|
793
|
+
if (oldState.state !== "closed") {
|
|
794
|
+
await this.#notifyStateChange(remoteHost, oldState.state, "closed");
|
|
795
|
+
return {
|
|
796
|
+
previousState: oldState.state,
|
|
797
|
+
newState: "closed"
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
throw new Error(`Failed to update circuit breaker state for ${remoteHost}`);
|
|
804
|
+
}
|
|
805
|
+
async recordReachableFailure(remoteHost) {
|
|
806
|
+
return await this.recordSuccess(remoteHost);
|
|
807
|
+
}
|
|
808
|
+
async recordFailure(remoteHost) {
|
|
809
|
+
const now = this.#now();
|
|
810
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
811
|
+
const oldState = await this.#get(remoteHost);
|
|
812
|
+
if (oldState?.state === "open") return void 0;
|
|
813
|
+
const oldFailures = oldState?.failures.map(Temporal.Instant.from) ?? [];
|
|
814
|
+
const failures = this.#options.pruneFailures([...oldFailures, now], now);
|
|
815
|
+
let newState;
|
|
816
|
+
let transition;
|
|
817
|
+
if (oldState?.state === "half-open" || this.#options.failure(failures)) {
|
|
818
|
+
newState = {
|
|
819
|
+
state: "open",
|
|
820
|
+
failures: failures.map((t) => t.toString()),
|
|
821
|
+
opened: now.toString()
|
|
822
|
+
};
|
|
823
|
+
transition = [oldState?.state ?? "closed", "open"];
|
|
824
|
+
} else newState = {
|
|
825
|
+
state: "closed",
|
|
826
|
+
failures: failures.map((t) => t.toString())
|
|
827
|
+
};
|
|
828
|
+
if (await this.#replace(remoteHost, oldState, newState)) {
|
|
829
|
+
if (transition != null) {
|
|
830
|
+
await this.#notifyStateChange(remoteHost, transition[0], transition[1]);
|
|
831
|
+
return {
|
|
832
|
+
previousState: transition[0],
|
|
833
|
+
newState: transition[1]
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
throw new Error(`Failed to update circuit breaker state for ${remoteHost}`);
|
|
840
|
+
}
|
|
841
|
+
async dropActivity(remoteHost, details) {
|
|
842
|
+
try {
|
|
843
|
+
await this.#options.onActivityDrop?.(remoteHost, details);
|
|
844
|
+
} catch (error) {
|
|
845
|
+
getLogger([
|
|
846
|
+
"fedify",
|
|
847
|
+
"federation",
|
|
848
|
+
"circuit"
|
|
849
|
+
]).error("An unexpected error occurred in circuit breaker activity drop handler:\n{error}", {
|
|
850
|
+
remoteHost,
|
|
851
|
+
error
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async getState(remoteHost) {
|
|
856
|
+
return await this.#get(remoteHost);
|
|
857
|
+
}
|
|
858
|
+
#key(remoteHost) {
|
|
859
|
+
return [...this.#prefix, remoteHost];
|
|
860
|
+
}
|
|
861
|
+
#capHeldRetryAt(now, heldSince, retryAt) {
|
|
862
|
+
const expiresAt = (heldSince ?? now).add(this.#options.heldActivityTtl);
|
|
863
|
+
return Temporal.Instant.compare(expiresAt, retryAt) < 0 ? expiresAt : retryAt;
|
|
864
|
+
}
|
|
865
|
+
async #get(remoteHost) {
|
|
866
|
+
return parseCircuitBreakerKvState(await this.#kv.get(this.#key(remoteHost)));
|
|
867
|
+
}
|
|
868
|
+
async #replace(remoteHost, oldState, newState) {
|
|
869
|
+
const key = this.#key(remoteHost);
|
|
870
|
+
if (this.#kv.cas == null) {
|
|
871
|
+
if (newState == null) await this.#kv.delete(key);
|
|
872
|
+
else await this.#kv.set(key, newState);
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
return await this.#kv.cas(key, oldState, newState);
|
|
876
|
+
}
|
|
877
|
+
async #notifyStateChange(remoteHost, previousState, newState) {
|
|
878
|
+
try {
|
|
879
|
+
await this.#options.onStateChange?.(remoteHost, previousState, newState);
|
|
880
|
+
} catch (error) {
|
|
881
|
+
getLogger([
|
|
882
|
+
"fedify",
|
|
883
|
+
"federation",
|
|
884
|
+
"circuit"
|
|
885
|
+
]).error("An unexpected error occurred in circuit breaker state change handler:\n{error}", {
|
|
886
|
+
remoteHost,
|
|
887
|
+
previousState,
|
|
888
|
+
newState,
|
|
889
|
+
error
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
await this.#stateChangeObserver?.(remoteHost, previousState, newState);
|
|
894
|
+
} catch (error) {
|
|
895
|
+
getLogger([
|
|
896
|
+
"fedify",
|
|
897
|
+
"federation",
|
|
898
|
+
"circuit"
|
|
899
|
+
]).error("An unexpected error occurred in circuit breaker state change observer:\n{error}", {
|
|
900
|
+
remoteHost,
|
|
901
|
+
previousState,
|
|
902
|
+
newState,
|
|
903
|
+
error
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
/**
|
|
909
|
+
* Normalizes user-provided circuit breaker options into the internal policy
|
|
910
|
+
* shape used while processing queued outbox deliveries.
|
|
911
|
+
*
|
|
912
|
+
* @param options The public circuit breaker options supplied to Fedify.
|
|
913
|
+
* @returns The normalized failure predicate, failure pruning function,
|
|
914
|
+
* duration values, and optional callbacks with defaults applied.
|
|
915
|
+
* @throws {RangeError} If any configured duration is not positive.
|
|
916
|
+
* @throws {TypeError} If `failureThreshold` is not a positive integer.
|
|
917
|
+
*/
|
|
918
|
+
function normalizeCircuitBreakerOptions(options) {
|
|
919
|
+
const recoveryDelay = toInstantDuration(options.recoveryDelay ?? { minutes: 30 });
|
|
920
|
+
const heldActivityTtl = toInstantDuration(options.heldActivityTtl ?? { hours: 168 });
|
|
921
|
+
const releaseInterval = toInstantDuration(options.releaseInterval ?? { seconds: 1 });
|
|
922
|
+
assertPositiveDuration(recoveryDelay, "recoveryDelay");
|
|
923
|
+
assertPositiveDuration(heldActivityTtl, "heldActivityTtl");
|
|
924
|
+
assertPositiveDuration(releaseInterval, "releaseInterval");
|
|
925
|
+
let failure;
|
|
926
|
+
let pruneFailures;
|
|
927
|
+
if (options.failure == null) {
|
|
928
|
+
const failureThreshold = options.failureThreshold ?? 5;
|
|
929
|
+
if (!Number.isInteger(failureThreshold) || failureThreshold <= 0) throw new TypeError("failureThreshold must be a positive integer.");
|
|
930
|
+
const failureWindow = toInstantDuration(options.failureWindow ?? { minutes: 10 });
|
|
931
|
+
assertPositiveDuration(failureWindow, "failureWindow");
|
|
932
|
+
pruneFailures = (timestamps, now) => {
|
|
933
|
+
const earliest = now.subtract(failureWindow);
|
|
934
|
+
return timestamps.filter((timestamp) => Temporal.Instant.compare(timestamp, earliest) >= 0).slice(-failureThreshold);
|
|
935
|
+
};
|
|
936
|
+
failure = (timestamps) => {
|
|
937
|
+
if (timestamps.length < failureThreshold) return false;
|
|
938
|
+
const first = timestamps[timestamps.length - failureThreshold];
|
|
939
|
+
const last = timestamps[timestamps.length - 1];
|
|
940
|
+
return Temporal.Duration.compare(first.until(last), failureWindow) <= 0;
|
|
941
|
+
};
|
|
942
|
+
} else {
|
|
943
|
+
failure = options.failure;
|
|
944
|
+
pruneFailures = (timestamps) => timestamps.slice(-MAX_CUSTOM_FAILURE_HISTORY);
|
|
945
|
+
}
|
|
946
|
+
return {
|
|
947
|
+
failure,
|
|
948
|
+
pruneFailures,
|
|
949
|
+
recoveryDelay,
|
|
950
|
+
heldActivityTtl,
|
|
951
|
+
releaseInterval,
|
|
952
|
+
onStateChange: options.onStateChange,
|
|
953
|
+
onActivityDrop: options.onActivityDrop
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
function toInstantDuration(duration) {
|
|
957
|
+
const parsed = Temporal.Duration.from(duration);
|
|
958
|
+
return Temporal.Duration.from({ milliseconds: Math.trunc(parsed.total({
|
|
959
|
+
unit: "millisecond",
|
|
960
|
+
relativeTo: Temporal.PlainDateTime.from("2026-01-01T00:00:00")
|
|
961
|
+
})) });
|
|
962
|
+
}
|
|
963
|
+
function assertPositiveDuration(duration, name) {
|
|
964
|
+
if (Temporal.Duration.compare(duration, { seconds: 0 }) <= 0) throw new RangeError(`${name} must be a positive duration.`);
|
|
965
|
+
}
|
|
966
|
+
function parseHeldSince(value) {
|
|
967
|
+
if (value == null) return void 0;
|
|
968
|
+
try {
|
|
969
|
+
return Temporal.Instant.from(value);
|
|
970
|
+
} catch (error) {
|
|
971
|
+
getLogger([
|
|
972
|
+
"fedify",
|
|
973
|
+
"federation",
|
|
974
|
+
"circuit"
|
|
975
|
+
]).warn("Invalid circuitHeldSince value in queued outbox message: {value}", {
|
|
976
|
+
value,
|
|
977
|
+
error
|
|
978
|
+
});
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Parses a value loaded from the circuit breaker KV store.
|
|
984
|
+
*
|
|
985
|
+
* @param value The raw KV value to validate.
|
|
986
|
+
* @returns A circuit breaker state when `value` has a recognized state and
|
|
987
|
+
* valid instant strings, or `undefined` when the stored value is malformed.
|
|
988
|
+
*/
|
|
989
|
+
function parseCircuitBreakerKvState(value) {
|
|
990
|
+
const isInstantString = (v) => {
|
|
991
|
+
if (typeof v !== "string") return false;
|
|
992
|
+
try {
|
|
993
|
+
Temporal.Instant.from(v);
|
|
994
|
+
return true;
|
|
995
|
+
} catch {
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
if (typeof value !== "object" || value == null) return void 0;
|
|
1000
|
+
const record = value;
|
|
1001
|
+
if (record.state !== "closed" && record.state !== "open" && record.state !== "half-open") return;
|
|
1002
|
+
if (!Array.isArray(record.failures) || !record.failures.every((failure) => isInstantString(failure))) return;
|
|
1003
|
+
if (record.opened != null && !isInstantString(record.opened)) return;
|
|
1004
|
+
if (record.halfOpened != null && !isInstantString(record.halfOpened)) return;
|
|
1005
|
+
return {
|
|
1006
|
+
state: record.state,
|
|
1007
|
+
failures: record.failures,
|
|
1008
|
+
...record.opened == null ? {} : { opened: record.opened },
|
|
1009
|
+
...record.halfOpened == null ? {} : { halfOpened: record.halfOpened }
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
//#endregion
|
|
733
1013
|
//#region src/federation/collection.ts
|
|
734
1014
|
/**
|
|
735
1015
|
* Calculates the [partial follower collection digest][1].
|
|
@@ -770,7 +1050,7 @@ async function buildCollectionSynchronizationHeader(collectionId, actorIds) {
|
|
|
770
1050
|
}
|
|
771
1051
|
//#endregion
|
|
772
1052
|
//#region src/federation/inbox.ts
|
|
773
|
-
async function routeActivity({ context: ctx, json, activity, recipient, inboxListeners, inboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, tracerProvider, idempotencyStrategy }) {
|
|
1053
|
+
async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
|
|
774
1054
|
const logger = getLogger([
|
|
775
1055
|
"fedify",
|
|
776
1056
|
"federation",
|
|
@@ -807,6 +1087,7 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
|
|
|
807
1087
|
code: SpanStatusCode.UNSET,
|
|
808
1088
|
message: `Activity ${activity.id?.href} has already been processed.`
|
|
809
1089
|
});
|
|
1090
|
+
recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
|
|
810
1091
|
return "alreadyProcessed";
|
|
811
1092
|
}
|
|
812
1093
|
}
|
|
@@ -816,6 +1097,7 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
|
|
|
816
1097
|
code: SpanStatusCode.ERROR,
|
|
817
1098
|
message: "Missing actor."
|
|
818
1099
|
});
|
|
1100
|
+
recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
|
|
819
1101
|
return "missingActor";
|
|
820
1102
|
}
|
|
821
1103
|
span.setAttribute("activitypub.actor.id", activity.actorId.href);
|
|
@@ -827,7 +1109,9 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
|
|
|
827
1109
|
type: "inbox",
|
|
828
1110
|
id: crypto.randomUUID(),
|
|
829
1111
|
baseUrl: ctx.origin,
|
|
830
|
-
activity: json,
|
|
1112
|
+
activity: originalJson ?? json,
|
|
1113
|
+
...normalizedActivity == null ? {} : { normalizedActivity },
|
|
1114
|
+
...ldSignatureVerified == null ? {} : { ldSignatureVerified },
|
|
831
1115
|
identifier: recipient,
|
|
832
1116
|
attempt: 0,
|
|
833
1117
|
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -846,6 +1130,12 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
|
|
|
846
1130
|
});
|
|
847
1131
|
throw error;
|
|
848
1132
|
}
|
|
1133
|
+
getFederationMetrics(meterProvider).recordQueueTaskEnqueued({
|
|
1134
|
+
role: "inbox",
|
|
1135
|
+
queue,
|
|
1136
|
+
activityType: getTypeId(activity).href
|
|
1137
|
+
}, 0);
|
|
1138
|
+
recordInboxActivity(meterProvider, "queued", getTypeId(activity).href);
|
|
849
1139
|
logger.info("Activity {activityId} is enqueued.", {
|
|
850
1140
|
activityId: activity.id?.href,
|
|
851
1141
|
activity: json,
|
|
@@ -865,13 +1155,21 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
|
|
|
865
1155
|
code: SpanStatusCode.UNSET,
|
|
866
1156
|
message: `Unsupported activity type: ${getTypeId(activity).href}`
|
|
867
1157
|
});
|
|
1158
|
+
recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
|
|
868
1159
|
span.end();
|
|
869
1160
|
return "unsupportedActivity";
|
|
870
1161
|
}
|
|
871
1162
|
const { class: cls, listener } = dispatched;
|
|
872
1163
|
span.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
|
|
873
1164
|
try {
|
|
874
|
-
|
|
1165
|
+
const activityType = getTypeId(activity).href;
|
|
1166
|
+
const started = performance.now();
|
|
1167
|
+
try {
|
|
1168
|
+
const contextFactory = listenerInboxContextFactory ?? inboxContextFactory;
|
|
1169
|
+
await listener(contextFactory(recipient, contextFactory === inboxContextFactory ? json : originalJson ?? json, activity.id?.href, activityType), activity);
|
|
1170
|
+
} finally {
|
|
1171
|
+
getFederationMetrics(meterProvider).recordInboxProcessingDuration(activityType, getDurationMs(started));
|
|
1172
|
+
}
|
|
875
1173
|
} catch (error) {
|
|
876
1174
|
try {
|
|
877
1175
|
await inboxErrorHandler?.(ctx, error);
|
|
@@ -893,9 +1191,11 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
|
|
|
893
1191
|
code: SpanStatusCode.ERROR,
|
|
894
1192
|
message: String(error)
|
|
895
1193
|
});
|
|
1194
|
+
recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
|
|
896
1195
|
span.end();
|
|
897
1196
|
return "error";
|
|
898
1197
|
}
|
|
1198
|
+
recordInboxActivity(meterProvider, "processed", getTypeId(activity).href);
|
|
899
1199
|
if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal.Duration.from({ days: 1 }) });
|
|
900
1200
|
logger.info("Activity {activityId} has been processed.", {
|
|
901
1201
|
activityId: activity.id?.href,
|
|
@@ -1000,6 +1300,35 @@ var KvKeyCache = class {
|
|
|
1000
1300
|
};
|
|
1001
1301
|
//#endregion
|
|
1002
1302
|
//#region src/federation/negotiation.ts
|
|
1303
|
+
/*!
|
|
1304
|
+
* Adapted directly from negotiator at https://github.com/jshttp/negotiator/
|
|
1305
|
+
* which is licensed as follows:
|
|
1306
|
+
*
|
|
1307
|
+
* (The MIT License)
|
|
1308
|
+
*
|
|
1309
|
+
* Copyright (c) 2012-2014 Federico Romero
|
|
1310
|
+
* Copyright (c) 2012-2014 Isaac Z. Schlueter
|
|
1311
|
+
* Copyright (c) 2014-2015 Douglas Christopher Wilson
|
|
1312
|
+
*
|
|
1313
|
+
* Permission is hereby granted, free of charge, to any person obtaining
|
|
1314
|
+
* a copy of this software and associated documentation files (the
|
|
1315
|
+
* 'Software'), to deal in the Software without restriction, including
|
|
1316
|
+
* without limitation the rights to use, copy, modify, merge, publish,
|
|
1317
|
+
* distribute, sublicense, and/or sell copies of the Software, and to
|
|
1318
|
+
* permit persons to whom the Software is furnished to do so, subject to
|
|
1319
|
+
* the following conditions:
|
|
1320
|
+
*
|
|
1321
|
+
* The above copyright notice and this permission notice shall be
|
|
1322
|
+
* included in all copies or substantial portions of the Software.
|
|
1323
|
+
*
|
|
1324
|
+
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
1325
|
+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
1326
|
+
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
1327
|
+
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
1328
|
+
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
1329
|
+
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
1330
|
+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
1331
|
+
*/
|
|
1003
1332
|
function compareSpecs(a, b) {
|
|
1004
1333
|
return b.q - a.q || (b.s ?? 0) - (a.s ?? 0) || (a.o ?? 0) - (b.o ?? 0) || a.i - b.i || 0;
|
|
1005
1334
|
}
|
|
@@ -1062,7 +1391,123 @@ function acceptsJsonLd(request) {
|
|
|
1062
1391
|
return types.includes("application/activity+json") || types.includes("application/ld+json") || types.includes("application/json");
|
|
1063
1392
|
}
|
|
1064
1393
|
//#endregion
|
|
1394
|
+
//#region src/federation/temporal.ts
|
|
1395
|
+
function isPlainObject(value) {
|
|
1396
|
+
return typeof value === "object" && value != null && !Array.isArray(value);
|
|
1397
|
+
}
|
|
1398
|
+
function normalizeDateTimeLiteral(value) {
|
|
1399
|
+
return value.substring(19).match(/[Z+-]/) ? value : value + "Z";
|
|
1400
|
+
}
|
|
1401
|
+
function isMalformedDateTimeLiteral(value) {
|
|
1402
|
+
if (typeof value !== "string") return false;
|
|
1403
|
+
try {
|
|
1404
|
+
Temporal.Instant.from(normalizeDateTimeLiteral(value));
|
|
1405
|
+
return false;
|
|
1406
|
+
} catch {
|
|
1407
|
+
return true;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
function isMalformedDurationLiteral(value) {
|
|
1411
|
+
if (typeof value !== "string") return false;
|
|
1412
|
+
try {
|
|
1413
|
+
Temporal.Duration.from(value);
|
|
1414
|
+
return false;
|
|
1415
|
+
} catch {
|
|
1416
|
+
return true;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
const TEMPORAL_DATE_TIME_IRIS = new Set([
|
|
1420
|
+
"https://www.w3.org/ns/activitystreams#deleted",
|
|
1421
|
+
"https://www.w3.org/ns/activitystreams#endTime",
|
|
1422
|
+
"https://www.w3.org/ns/activitystreams#published",
|
|
1423
|
+
"https://www.w3.org/ns/activitystreams#startTime",
|
|
1424
|
+
"https://www.w3.org/ns/activitystreams#updated",
|
|
1425
|
+
"http://purl.org/dc/terms/created",
|
|
1426
|
+
"https://w3id.org/security#created"
|
|
1427
|
+
]);
|
|
1428
|
+
const TEMPORAL_DURATION_IRIS = new Set(["https://www.w3.org/ns/activitystreams#duration"]);
|
|
1429
|
+
const QUESTION_CLOSED_IRI = "https://www.w3.org/ns/activitystreams#closed";
|
|
1430
|
+
const XSD_DATE_TIME_IRI = "http://www.w3.org/2001/XMLSchema#dateTime";
|
|
1431
|
+
function hasMalformedExpandedDateTimeLiteral(value) {
|
|
1432
|
+
if (Array.isArray(value)) return value.some(hasMalformedExpandedDateTimeLiteral);
|
|
1433
|
+
return isPlainObject(value) && "@value" in value && isMalformedDateTimeLiteral(value["@value"]);
|
|
1434
|
+
}
|
|
1435
|
+
function hasMalformedExpandedQuestionClosedLiteral(value) {
|
|
1436
|
+
if (Array.isArray(value)) return value.some(hasMalformedExpandedQuestionClosedLiteral);
|
|
1437
|
+
if (!isPlainObject(value) || !("@value" in value)) return false;
|
|
1438
|
+
const literal = value["@value"];
|
|
1439
|
+
if (typeof literal === "boolean") return false;
|
|
1440
|
+
if (typeof literal !== "string") return false;
|
|
1441
|
+
if (value["@type"] !== XSD_DATE_TIME_IRI) return false;
|
|
1442
|
+
if (new Date(literal).toString() === "Invalid Date") return false;
|
|
1443
|
+
return isMalformedDateTimeLiteral(literal);
|
|
1444
|
+
}
|
|
1445
|
+
function hasMalformedExpandedDurationLiteral(value) {
|
|
1446
|
+
if (Array.isArray(value)) return value.some(hasMalformedExpandedDurationLiteral);
|
|
1447
|
+
return isPlainObject(value) && "@value" in value && isMalformedDurationLiteral(value["@value"]);
|
|
1448
|
+
}
|
|
1449
|
+
function hasMalformedKnownTemporalLiteralInternal(value, visited) {
|
|
1450
|
+
if (Array.isArray(value)) return value.some((item) => hasMalformedKnownTemporalLiteralInternal(item, visited));
|
|
1451
|
+
if (!isPlainObject(value)) return false;
|
|
1452
|
+
if (visited.has(value)) return false;
|
|
1453
|
+
visited.add(value);
|
|
1454
|
+
if ("@value" in value) return false;
|
|
1455
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1456
|
+
if (TEMPORAL_DATE_TIME_IRIS.has(key)) {
|
|
1457
|
+
if (hasMalformedExpandedDateTimeLiteral(child)) return true;
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
if (key === QUESTION_CLOSED_IRI) {
|
|
1461
|
+
if (hasMalformedExpandedQuestionClosedLiteral(child)) return true;
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
if (TEMPORAL_DURATION_IRIS.has(key)) {
|
|
1465
|
+
if (hasMalformedExpandedDurationLiteral(child)) return true;
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
if (hasMalformedKnownTemporalLiteralInternal(child, visited)) return true;
|
|
1469
|
+
}
|
|
1470
|
+
return false;
|
|
1471
|
+
}
|
|
1472
|
+
async function hasMalformedKnownTemporalLiteral(value, contextLoader) {
|
|
1473
|
+
try {
|
|
1474
|
+
return hasMalformedKnownTemporalLiteralInternal(await jsonld.expand(value, {
|
|
1475
|
+
documentLoader: getNormalizationContextLoader(contextLoader),
|
|
1476
|
+
keepFreeFloatingNodes: true
|
|
1477
|
+
}), /* @__PURE__ */ new Set());
|
|
1478
|
+
} catch {
|
|
1479
|
+
return false;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
//#endregion
|
|
1065
1483
|
//#region src/federation/handler.ts
|
|
1484
|
+
const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
|
|
1485
|
+
function isRemoteContextLoadingFailure$1(error) {
|
|
1486
|
+
return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
|
|
1487
|
+
}
|
|
1488
|
+
function isPermanentRemoteContextError$1(error) {
|
|
1489
|
+
if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
|
|
1490
|
+
const details = error.details;
|
|
1491
|
+
if (details?.code === "invalid remote context") return true;
|
|
1492
|
+
return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) && isClearlyMalformedContextReference(details.url);
|
|
1493
|
+
}
|
|
1494
|
+
function isInvalidJsonLdError(error) {
|
|
1495
|
+
if (!(error instanceof Error)) return false;
|
|
1496
|
+
const name = error.name;
|
|
1497
|
+
return name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
|
|
1498
|
+
}
|
|
1499
|
+
function isValidationTypeError(error) {
|
|
1500
|
+
return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || isInvalidUrlTypeError(error));
|
|
1501
|
+
}
|
|
1502
|
+
function isPermanentActivityParseError(error) {
|
|
1503
|
+
return isInvalidJsonLdError(error) || isValidationTypeError(error);
|
|
1504
|
+
}
|
|
1505
|
+
function hasHttpSignatureHeaders(request) {
|
|
1506
|
+
return request.headers.has("Signature") || request.headers.has("Signature-Input");
|
|
1507
|
+
}
|
|
1508
|
+
function hasObjectIntegrityProof(json) {
|
|
1509
|
+
return typeof json === "object" && json != null && "proof" in json;
|
|
1510
|
+
}
|
|
1066
1511
|
/**
|
|
1067
1512
|
* Handles an actor request.
|
|
1068
1513
|
* @template TContextData The context data to pass to the context.
|
|
@@ -1124,6 +1569,33 @@ async function handleObject(request, { values, context, objectDispatcher, author
|
|
|
1124
1569
|
Vary: "Accept"
|
|
1125
1570
|
} });
|
|
1126
1571
|
}
|
|
1572
|
+
const BUILT_IN_COLLECTION_METRIC_KINDS = new Set([
|
|
1573
|
+
"inbox",
|
|
1574
|
+
"outbox",
|
|
1575
|
+
"following",
|
|
1576
|
+
"followers",
|
|
1577
|
+
"liked",
|
|
1578
|
+
"featured",
|
|
1579
|
+
"featured_tags"
|
|
1580
|
+
]);
|
|
1581
|
+
function getCollectionMetricKind(name) {
|
|
1582
|
+
const normalized = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/\s+/g, "_");
|
|
1583
|
+
return BUILT_IN_COLLECTION_METRIC_KINDS.has(normalized) ? normalized : "custom";
|
|
1584
|
+
}
|
|
1585
|
+
function collectionAttributes(base, result, response) {
|
|
1586
|
+
return {
|
|
1587
|
+
...base,
|
|
1588
|
+
result,
|
|
1589
|
+
...response == null ? {} : { statusCode: response.status }
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
function recordCollectionMetrics(meterProvider, base, result, options = {}) {
|
|
1593
|
+
const attrs = collectionAttributes(base, result, options.response);
|
|
1594
|
+
recordCollectionRequest(meterProvider, attrs);
|
|
1595
|
+
if (options.dispatchDurationMs != null) recordCollectionDispatchDuration(meterProvider, options.dispatchDurationMs, attrs);
|
|
1596
|
+
if (options.itemCount != null) recordCollectionPageItems(meterProvider, options.itemCount, attrs);
|
|
1597
|
+
if (options.totalItems != null) recordCollectionTotalItems(meterProvider, options.totalItems, attrs);
|
|
1598
|
+
}
|
|
1127
1599
|
/**
|
|
1128
1600
|
* Handles a collection request.
|
|
1129
1601
|
* @template TItem The type of items in the collection.
|
|
@@ -1134,36 +1606,118 @@ async function handleObject(request, { values, context, objectDispatcher, author
|
|
|
1134
1606
|
* @param parameters The parameters for handling the collection.
|
|
1135
1607
|
* @returns A promise that resolves to an HTTP response.
|
|
1136
1608
|
*/
|
|
1137
|
-
async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, onUnauthorized, onNotFound }) {
|
|
1609
|
+
async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
|
|
1138
1610
|
const spanName = name$1.trim().replace(/\s+/g, "_");
|
|
1139
1611
|
tracerProvider = tracerProvider ?? trace.getTracerProvider();
|
|
1140
1612
|
const tracer = tracerProvider.getTracer(name, version);
|
|
1141
1613
|
const cursor = new URL(request.url).searchParams.get("cursor");
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1614
|
+
const metricBase = {
|
|
1615
|
+
kind: getCollectionMetricKind(name$1),
|
|
1616
|
+
page: cursor != null,
|
|
1617
|
+
dispatcher: "built_in"
|
|
1618
|
+
};
|
|
1619
|
+
let dispatchDurationMs;
|
|
1620
|
+
let itemCount;
|
|
1621
|
+
let totalItemCount;
|
|
1622
|
+
const finish = (response, result) => {
|
|
1623
|
+
recordCollectionMetrics(meterProvider, metricBase, result, {
|
|
1624
|
+
response,
|
|
1625
|
+
dispatchDurationMs,
|
|
1626
|
+
itemCount,
|
|
1627
|
+
totalItems: totalItemCount
|
|
1628
|
+
});
|
|
1629
|
+
return response;
|
|
1630
|
+
};
|
|
1631
|
+
try {
|
|
1632
|
+
if (collectionCallbacks == null) return finish(await onNotFound(request), "not_found");
|
|
1633
|
+
let collection;
|
|
1634
|
+
const baseUri = uriGetter(identifier);
|
|
1635
|
+
if (cursor == null) {
|
|
1636
|
+
const firstCursor = await collectionCallbacks.firstCursor?.(context, identifier);
|
|
1637
|
+
const totalItems = filter == null ? await collectionCallbacks.counter?.(context, identifier) : void 0;
|
|
1638
|
+
totalItemCount = totalItems == null ? void 0 : Number(totalItems);
|
|
1639
|
+
if (firstCursor == null) {
|
|
1640
|
+
const itemsOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection ${spanName}`, {
|
|
1641
|
+
kind: SpanKind.SERVER,
|
|
1642
|
+
attributes: {
|
|
1643
|
+
"activitypub.collection.id": baseUri.href,
|
|
1644
|
+
"activitypub.collection.type": OrderedCollection.typeId.href
|
|
1645
|
+
}
|
|
1646
|
+
}, async (span) => {
|
|
1647
|
+
if (totalItemCount != null) span.setAttribute("activitypub.collection.total_items", totalItemCount);
|
|
1648
|
+
const started = performance.now();
|
|
1649
|
+
try {
|
|
1650
|
+
const page = await collectionCallbacks.dispatcher(context, identifier, null, filter);
|
|
1651
|
+
dispatchDurationMs = getDurationMs(started);
|
|
1652
|
+
if (page == null) {
|
|
1653
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
1654
|
+
return await onNotFound(request);
|
|
1655
|
+
}
|
|
1656
|
+
const items = filterCollectionItems(page.items, name$1, filterPredicate);
|
|
1657
|
+
itemCount = items.length;
|
|
1658
|
+
span.setAttribute("fedify.collection.items", itemCount);
|
|
1659
|
+
return items;
|
|
1660
|
+
} catch (e) {
|
|
1661
|
+
if (dispatchDurationMs == null) dispatchDurationMs = getDurationMs(started);
|
|
1662
|
+
span.setStatus({
|
|
1663
|
+
code: SpanStatusCode.ERROR,
|
|
1664
|
+
message: String(e)
|
|
1665
|
+
});
|
|
1666
|
+
throw e;
|
|
1667
|
+
} finally {
|
|
1668
|
+
span.end();
|
|
1669
|
+
}
|
|
1670
|
+
});
|
|
1671
|
+
if (itemsOrResponse instanceof Response) return finish(itemsOrResponse, "not_found");
|
|
1672
|
+
collection = new OrderedCollection({
|
|
1673
|
+
id: baseUri,
|
|
1674
|
+
totalItems: totalItemCount ?? null,
|
|
1675
|
+
items: itemsOrResponse
|
|
1676
|
+
});
|
|
1677
|
+
} else {
|
|
1678
|
+
const lastCursor = await collectionCallbacks.lastCursor?.(context, identifier);
|
|
1679
|
+
const first = new URL(context.url);
|
|
1680
|
+
first.searchParams.set("cursor", firstCursor);
|
|
1681
|
+
let last = null;
|
|
1682
|
+
if (lastCursor != null) {
|
|
1683
|
+
last = new URL(context.url);
|
|
1684
|
+
last.searchParams.set("cursor", lastCursor);
|
|
1685
|
+
}
|
|
1686
|
+
collection = new OrderedCollection({
|
|
1687
|
+
id: baseUri,
|
|
1688
|
+
totalItems: totalItemCount ?? null,
|
|
1689
|
+
first,
|
|
1690
|
+
last
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
} else {
|
|
1694
|
+
const uri = new URL(baseUri);
|
|
1695
|
+
uri.searchParams.set("cursor", cursor);
|
|
1696
|
+
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
|
|
1150
1697
|
kind: SpanKind.SERVER,
|
|
1151
1698
|
attributes: {
|
|
1152
|
-
"activitypub.collection.id":
|
|
1153
|
-
"activitypub.collection.type":
|
|
1699
|
+
"activitypub.collection.id": uri.href,
|
|
1700
|
+
"activitypub.collection.type": OrderedCollectionPage.typeId.href,
|
|
1701
|
+
"fedify.collection.cursor": cursor
|
|
1154
1702
|
}
|
|
1155
1703
|
}, async (span) => {
|
|
1156
|
-
|
|
1704
|
+
const started = performance.now();
|
|
1157
1705
|
try {
|
|
1158
|
-
const page = await collectionCallbacks.dispatcher(context, identifier,
|
|
1706
|
+
const page = await collectionCallbacks.dispatcher(context, identifier, cursor, filter);
|
|
1707
|
+
dispatchDurationMs = getDurationMs(started);
|
|
1159
1708
|
if (page == null) {
|
|
1160
1709
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
1161
1710
|
return await onNotFound(request);
|
|
1162
1711
|
}
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
|
|
1712
|
+
const items = filterCollectionItems(page.items, name$1, filterPredicate);
|
|
1713
|
+
itemCount = items.length;
|
|
1714
|
+
span.setAttribute("fedify.collection.items", itemCount);
|
|
1715
|
+
return {
|
|
1716
|
+
...page,
|
|
1717
|
+
items
|
|
1718
|
+
};
|
|
1166
1719
|
} catch (e) {
|
|
1720
|
+
if (dispatchDurationMs == null) dispatchDurationMs = getDurationMs(started);
|
|
1167
1721
|
span.setStatus({
|
|
1168
1722
|
code: SpanStatusCode.ERROR,
|
|
1169
1723
|
message: String(e)
|
|
@@ -1173,87 +1727,44 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
1173
1727
|
span.end();
|
|
1174
1728
|
}
|
|
1175
1729
|
});
|
|
1176
|
-
if (
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
} else {
|
|
1183
|
-
const lastCursor = await collectionCallbacks.lastCursor?.(context, identifier);
|
|
1184
|
-
const first = new URL(context.url);
|
|
1185
|
-
first.searchParams.set("cursor", firstCursor);
|
|
1186
|
-
let last = null;
|
|
1187
|
-
if (lastCursor != null) {
|
|
1188
|
-
last = new URL(context.url);
|
|
1189
|
-
last.searchParams.set("cursor", lastCursor);
|
|
1190
|
-
}
|
|
1191
|
-
collection = new OrderedCollection({
|
|
1192
|
-
id: baseUri,
|
|
1193
|
-
totalItems: totalItems == null ? null : Number(totalItems),
|
|
1194
|
-
first,
|
|
1195
|
-
last
|
|
1196
|
-
});
|
|
1197
|
-
}
|
|
1198
|
-
} else {
|
|
1199
|
-
const uri = new URL(baseUri);
|
|
1200
|
-
uri.searchParams.set("cursor", cursor);
|
|
1201
|
-
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
|
|
1202
|
-
kind: SpanKind.SERVER,
|
|
1203
|
-
attributes: {
|
|
1204
|
-
"activitypub.collection.id": uri.href,
|
|
1205
|
-
"activitypub.collection.type": OrderedCollectionPage.typeId.href,
|
|
1206
|
-
"fedify.collection.cursor": cursor
|
|
1730
|
+
if (pageOrResponse instanceof Response) return finish(pageOrResponse, "not_found");
|
|
1731
|
+
const { items, prevCursor, nextCursor } = pageOrResponse;
|
|
1732
|
+
let prev = null;
|
|
1733
|
+
if (prevCursor != null) {
|
|
1734
|
+
prev = new URL(context.url);
|
|
1735
|
+
prev.searchParams.set("cursor", prevCursor);
|
|
1207
1736
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
1213
|
-
return await onNotFound(request);
|
|
1214
|
-
}
|
|
1215
|
-
span.setAttribute("fedify.collection.items", page.items.length);
|
|
1216
|
-
return page;
|
|
1217
|
-
} catch (e) {
|
|
1218
|
-
span.setStatus({
|
|
1219
|
-
code: SpanStatusCode.ERROR,
|
|
1220
|
-
message: String(e)
|
|
1221
|
-
});
|
|
1222
|
-
throw e;
|
|
1223
|
-
} finally {
|
|
1224
|
-
span.end();
|
|
1737
|
+
let next = null;
|
|
1738
|
+
if (nextCursor != null) {
|
|
1739
|
+
next = new URL(context.url);
|
|
1740
|
+
next.searchParams.set("cursor", nextCursor);
|
|
1225
1741
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1742
|
+
const partOf = new URL(context.url);
|
|
1743
|
+
partOf.searchParams.delete("cursor");
|
|
1744
|
+
collection = new OrderedCollectionPage({
|
|
1745
|
+
id: uri,
|
|
1746
|
+
prev,
|
|
1747
|
+
next,
|
|
1748
|
+
items,
|
|
1749
|
+
partOf
|
|
1750
|
+
});
|
|
1233
1751
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
next = new URL(context.url);
|
|
1237
|
-
next.searchParams.set("cursor", nextCursor);
|
|
1752
|
+
if (collectionCallbacks.authorizePredicate != null) {
|
|
1753
|
+
if (!await collectionCallbacks.authorizePredicate(context, identifier)) return finish(await onUnauthorized(request), "unauthorized");
|
|
1238
1754
|
}
|
|
1239
|
-
const
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1755
|
+
const jsonLd = await collection.toJsonLd(context);
|
|
1756
|
+
return finish(new Response(JSON.stringify(jsonLd), { headers: {
|
|
1757
|
+
"Content-Type": "application/activity+json",
|
|
1758
|
+
Vary: "Accept"
|
|
1759
|
+
} }), "served");
|
|
1760
|
+
} catch (e) {
|
|
1761
|
+
recordCollectionMetrics(meterProvider, metricBase, "error", {
|
|
1762
|
+
dispatchDurationMs,
|
|
1763
|
+
itemCount,
|
|
1764
|
+
totalItems: totalItemCount
|
|
1247
1765
|
});
|
|
1766
|
+
throw e;
|
|
1248
1767
|
}
|
|
1249
|
-
if (collectionCallbacks.authorizePredicate != null) {
|
|
1250
|
-
if (!await collectionCallbacks.authorizePredicate(context, identifier)) return await onUnauthorized(request);
|
|
1251
|
-
}
|
|
1252
|
-
const jsonLd = await collection.toJsonLd(context);
|
|
1253
|
-
return new Response(JSON.stringify(jsonLd), { headers: {
|
|
1254
|
-
"Content-Type": "application/activity+json",
|
|
1255
|
-
Vary: "Accept"
|
|
1256
|
-
} });
|
|
1257
1768
|
}
|
|
1258
1769
|
/**
|
|
1259
1770
|
* Filters collection items based on the provided predicate.
|
|
@@ -1517,7 +2028,7 @@ async function handleInbox(request, options) {
|
|
|
1517
2028
|
* @returns A promise that resolves to an HTTP response.
|
|
1518
2029
|
*/
|
|
1519
2030
|
async function handleInboxInternal(request, parameters, span) {
|
|
1520
|
-
const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, tracerProvider } = parameters;
|
|
2031
|
+
const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider } = parameters;
|
|
1521
2032
|
const logger = getLogger([
|
|
1522
2033
|
"fedify",
|
|
1523
2034
|
"federation",
|
|
@@ -1589,28 +2100,105 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1589
2100
|
});
|
|
1590
2101
|
}
|
|
1591
2102
|
const keyCache = new KvKeyCache(kv, kvPrefixes.publicKey, ctx);
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
2103
|
+
const jsonWithoutSig = detachSignature(json);
|
|
2104
|
+
const hasLdSignature = hasSignature(json);
|
|
2105
|
+
const canAttemptAlternateAuthAfterLdSignatureFailure = skipSignatureVerification || hasHttpSignatureHeaders(request) || hasObjectIntegrityProof(jsonWithoutSig);
|
|
2106
|
+
let deferredLdSignatureError = void 0;
|
|
2107
|
+
const respondInvalidActivity = async (error) => {
|
|
2108
|
+
logger.error("Failed to parse activity:\n{error}", {
|
|
2109
|
+
recipient,
|
|
2110
|
+
activity: json,
|
|
2111
|
+
error
|
|
2112
|
+
});
|
|
2113
|
+
try {
|
|
2114
|
+
await inboxErrorHandler?.(ctx, error);
|
|
2115
|
+
} catch (error) {
|
|
2116
|
+
logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
|
|
2117
|
+
error,
|
|
2118
|
+
activity: json,
|
|
2119
|
+
recipient
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
span.setStatus({
|
|
2123
|
+
code: SpanStatusCode.ERROR,
|
|
2124
|
+
message: `Failed to parse activity:\n${error}`
|
|
2125
|
+
});
|
|
2126
|
+
return new Response("Invalid activity.", {
|
|
2127
|
+
status: 400,
|
|
2128
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
2129
|
+
});
|
|
2130
|
+
};
|
|
2131
|
+
let compactedJson = json;
|
|
2132
|
+
let compactedJsonWithoutSig = jsonWithoutSig;
|
|
2133
|
+
let ldSigVerified = false;
|
|
2134
|
+
if (hasLdSignature) {
|
|
2135
|
+
try {
|
|
2136
|
+
compactedJson = await compactJsonLd(json, ctx.contextLoader);
|
|
2137
|
+
} catch (error) {
|
|
2138
|
+
if (isInvalidJsonLdError(error)) {
|
|
2139
|
+
logger.error("Failed to parse JSON-LD:\n{error}", {
|
|
2140
|
+
recipient,
|
|
2141
|
+
error
|
|
2142
|
+
});
|
|
2143
|
+
return new Response("Invalid JSON-LD.", {
|
|
2144
|
+
status: 400,
|
|
2145
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
|
|
2149
|
+
if (!skipSignatureVerification) deferredLdSignatureError = error;
|
|
2150
|
+
logger.debug("Failed to normalize JSON-LD for Linked Data Signatures; deferring to another authentication path only if it verifies:\n{error}", {
|
|
1603
2151
|
recipient,
|
|
1604
2152
|
error
|
|
1605
2153
|
});
|
|
1606
|
-
return new Response("Invalid JSON-LD.", {
|
|
1607
|
-
status: 400,
|
|
1608
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1609
|
-
});
|
|
1610
2154
|
}
|
|
1611
|
-
|
|
2155
|
+
if (compactedJson !== json) {
|
|
2156
|
+
compactedJsonWithoutSig = detachSignature(compactedJson);
|
|
2157
|
+
try {
|
|
2158
|
+
ldSigVerified = await verifyCompactJsonLd(compactedJson, {
|
|
2159
|
+
contextLoader: ctx.contextLoader,
|
|
2160
|
+
documentLoader: ctx.documentLoader,
|
|
2161
|
+
keyCache,
|
|
2162
|
+
meterProvider,
|
|
2163
|
+
tracerProvider
|
|
2164
|
+
});
|
|
2165
|
+
} catch (error) {
|
|
2166
|
+
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(error);
|
|
2167
|
+
if (isInvalidJsonLdError(error)) {
|
|
2168
|
+
logger.error("Failed to parse JSON-LD:\n{error}", {
|
|
2169
|
+
recipient,
|
|
2170
|
+
error
|
|
2171
|
+
});
|
|
2172
|
+
return new Response("Invalid JSON-LD.", {
|
|
2173
|
+
status: 400,
|
|
2174
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
|
|
2178
|
+
if (!skipSignatureVerification) try {
|
|
2179
|
+
await Object$1.fromJsonLd(compactedJson, {
|
|
2180
|
+
contextLoader: getNormalizationContextLoader(ctx.contextLoader),
|
|
2181
|
+
documentLoader: ctx.documentLoader,
|
|
2182
|
+
tracerProvider
|
|
2183
|
+
});
|
|
2184
|
+
} catch (parseError) {
|
|
2185
|
+
if (parseError instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(parseError);
|
|
2186
|
+
if (isInvalidJsonLdError(parseError)) {
|
|
2187
|
+
logger.error("Failed to parse JSON-LD:\n{error}", {
|
|
2188
|
+
recipient,
|
|
2189
|
+
error: parseError
|
|
2190
|
+
});
|
|
2191
|
+
return new Response("Invalid JSON-LD.", {
|
|
2192
|
+
status: 400,
|
|
2193
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
deferredLdSignatureError = parseError;
|
|
2197
|
+
}
|
|
2198
|
+
ldSigVerified = false;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
1612
2201
|
}
|
|
1613
|
-
const jsonWithoutSig = detachSignature(json);
|
|
1614
2202
|
let activity = null;
|
|
1615
2203
|
let activityVerified = false;
|
|
1616
2204
|
if (ldSigVerified) {
|
|
@@ -1618,7 +2206,16 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1618
2206
|
recipient,
|
|
1619
2207
|
json
|
|
1620
2208
|
});
|
|
1621
|
-
|
|
2209
|
+
try {
|
|
2210
|
+
activity = await Activity.fromJsonLd(compactedJsonWithoutSig, {
|
|
2211
|
+
...ctx,
|
|
2212
|
+
contextLoader: getNormalizationContextLoader(ctx.contextLoader)
|
|
2213
|
+
});
|
|
2214
|
+
} catch (error) {
|
|
2215
|
+
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(error);
|
|
2216
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
2217
|
+
return await respondInvalidActivity(error);
|
|
2218
|
+
}
|
|
1622
2219
|
activityVerified = true;
|
|
1623
2220
|
} else {
|
|
1624
2221
|
logger.debug("Linked Data Signatures are not verified.", {
|
|
@@ -1627,12 +2224,22 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1627
2224
|
});
|
|
1628
2225
|
try {
|
|
1629
2226
|
activity = await verifyObject(Activity, jsonWithoutSig, {
|
|
1630
|
-
contextLoader: ctx.contextLoader,
|
|
2227
|
+
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
1631
2228
|
documentLoader: ctx.documentLoader,
|
|
1632
2229
|
keyCache,
|
|
2230
|
+
meterProvider,
|
|
1633
2231
|
tracerProvider
|
|
1634
2232
|
});
|
|
1635
2233
|
} catch (error) {
|
|
2234
|
+
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(jsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(error);
|
|
2235
|
+
if (deferredLdSignatureError != null) {
|
|
2236
|
+
logger.debug("Object Integrity Proof fallback did not supersede a deferred Linked Data Signature failure:\n{error}", {
|
|
2237
|
+
recipient,
|
|
2238
|
+
error
|
|
2239
|
+
});
|
|
2240
|
+
activity = null;
|
|
2241
|
+
}
|
|
2242
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
1636
2243
|
logger.error("Failed to parse activity:\n{error}", {
|
|
1637
2244
|
recipient,
|
|
1638
2245
|
activity: json,
|
|
@@ -1677,10 +2284,14 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1677
2284
|
documentLoader: ctx.documentLoader,
|
|
1678
2285
|
timeWindow: signatureTimeWindow,
|
|
1679
2286
|
keyCache,
|
|
2287
|
+
meterProvider,
|
|
1680
2288
|
tracerProvider
|
|
1681
2289
|
});
|
|
1682
2290
|
if (verification.verified === false) {
|
|
2291
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
1683
2292
|
const reason = verification.reason;
|
|
2293
|
+
const remoteHost = "keyId" in reason && reason.keyId != null ? getRemoteHost(reason.keyId) : void 0;
|
|
2294
|
+
getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure(reason.type, remoteHost);
|
|
1684
2295
|
logger.error("Failed to verify the request's HTTP Signatures.", {
|
|
1685
2296
|
recipient,
|
|
1686
2297
|
reason: reason.type,
|
|
@@ -1755,7 +2366,15 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1755
2366
|
}
|
|
1756
2367
|
httpSigKey = verification.key;
|
|
1757
2368
|
}
|
|
1758
|
-
|
|
2369
|
+
try {
|
|
2370
|
+
activity = await Activity.fromJsonLd(jsonWithoutSig, {
|
|
2371
|
+
...ctx,
|
|
2372
|
+
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader)
|
|
2373
|
+
});
|
|
2374
|
+
} catch (error) {
|
|
2375
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
2376
|
+
return await respondInvalidActivity(error);
|
|
2377
|
+
}
|
|
1759
2378
|
}
|
|
1760
2379
|
if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
|
|
1761
2380
|
span.setAttribute("activitypub.activity.type", getTypeId(activity).href);
|
|
@@ -1767,6 +2386,8 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1767
2386
|
"http_signatures.key_id": httpSigKey?.id?.href ?? ""
|
|
1768
2387
|
});
|
|
1769
2388
|
if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
|
|
2389
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
2390
|
+
getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : getRemoteHost(httpSigKey.id));
|
|
1770
2391
|
logger.error("The signer ({keyId}) and the actor ({actorId}) do not match.", {
|
|
1771
2392
|
activity: json,
|
|
1772
2393
|
recipient,
|
|
@@ -1784,6 +2405,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1784
2405
|
}
|
|
1785
2406
|
if (pendingNonceLabel != null) {
|
|
1786
2407
|
if (!await verifySignatureNonce(request, kv, kvPrefixes.acceptSignatureNonce, pendingNonceLabel)) {
|
|
2408
|
+
getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("invalidNonce", httpSigKey?.id == null ? void 0 : getRemoteHost(httpSigKey.id));
|
|
1787
2409
|
logger.error("Signature nonce verification failed (missing, expired, or replayed).", { recipient });
|
|
1788
2410
|
return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
|
|
1789
2411
|
}
|
|
@@ -1791,15 +2413,20 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1791
2413
|
const routeResult = await routeActivity({
|
|
1792
2414
|
context: ctx,
|
|
1793
2415
|
json,
|
|
2416
|
+
originalJson: json,
|
|
2417
|
+
normalizedActivity: hasLdSignature && compactedJson !== json ? compactedJson : void 0,
|
|
2418
|
+
ldSignatureVerified: hasLdSignature ? ldSigVerified : void 0,
|
|
1794
2419
|
activity,
|
|
1795
2420
|
recipient,
|
|
1796
2421
|
inboxListeners,
|
|
1797
2422
|
inboxContextFactory,
|
|
2423
|
+
listenerInboxContextFactory: ldSigVerified ? inboxContextFactory[rawInboxContextFactorySymbol] : void 0,
|
|
1798
2424
|
inboxErrorHandler,
|
|
1799
2425
|
kv,
|
|
1800
2426
|
kvPrefixes,
|
|
1801
2427
|
queue,
|
|
1802
2428
|
span,
|
|
2429
|
+
meterProvider: parameters.meterProvider,
|
|
1803
2430
|
tracerProvider,
|
|
1804
2431
|
idempotencyStrategy: parameters.idempotencyStrategy
|
|
1805
2432
|
});
|
|
@@ -1840,11 +2467,31 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1840
2467
|
* @since 1.8.0
|
|
1841
2468
|
*/
|
|
1842
2469
|
const handleCustomCollection = exceptWrapper(_handleCustomCollection);
|
|
1843
|
-
|
|
2470
|
+
const pendingCollectionMetricRecorders = /* @__PURE__ */ new WeakMap();
|
|
2471
|
+
function deferPendingCollectionMetrics(error, recorder) {
|
|
2472
|
+
if (error == null || typeof error !== "object" && typeof error !== "function") return false;
|
|
2473
|
+
pendingCollectionMetricRecorders.set(error, recorder);
|
|
2474
|
+
return true;
|
|
2475
|
+
}
|
|
2476
|
+
function recordDeferredPendingCollectionMetrics(error, result, response) {
|
|
2477
|
+
if (error == null || typeof error !== "object" && typeof error !== "function") return;
|
|
2478
|
+
const recorder = pendingCollectionMetricRecorders.get(error);
|
|
2479
|
+
pendingCollectionMetricRecorders.delete(error);
|
|
2480
|
+
recorder?.(result, response);
|
|
2481
|
+
}
|
|
2482
|
+
async function _handleCustomCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate }) {
|
|
1844
2483
|
verifyDefined(callbacks);
|
|
1845
2484
|
await authIfNeeded(context, values, callbacks);
|
|
1846
2485
|
const cursor = new URL(request.url).searchParams.get("cursor");
|
|
1847
|
-
|
|
2486
|
+
const handler = new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, Collection, CollectionPage, filterPredicate).fetchCollection(cursor);
|
|
2487
|
+
try {
|
|
2488
|
+
const response = await handler.toJsonLd().then(respondAsActivity);
|
|
2489
|
+
handler.recordPendingCollectionMetrics("served", response);
|
|
2490
|
+
return response;
|
|
2491
|
+
} catch (e) {
|
|
2492
|
+
if (!deferPendingCollectionMetrics(e, (result, response) => handler.recordPendingCollectionMetrics(result, response))) handler.recordPendingCollectionMetrics("error");
|
|
2493
|
+
throw e;
|
|
2494
|
+
}
|
|
1848
2495
|
}
|
|
1849
2496
|
/**
|
|
1850
2497
|
* Handles an ordered collection request.
|
|
@@ -1858,11 +2505,19 @@ async function _handleCustomCollection(request, { name, values, context, tracerP
|
|
|
1858
2505
|
* @since 1.8.0
|
|
1859
2506
|
*/
|
|
1860
2507
|
const handleOrderedCollection = exceptWrapper(_handleOrderedCollection);
|
|
1861
|
-
async function _handleOrderedCollection(request, { name, values, context, tracerProvider, collectionCallbacks: callbacks, filterPredicate }) {
|
|
2508
|
+
async function _handleOrderedCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate }) {
|
|
1862
2509
|
verifyDefined(callbacks);
|
|
1863
2510
|
await authIfNeeded(context, values, callbacks);
|
|
1864
2511
|
const cursor = new URL(request.url).searchParams.get("cursor");
|
|
1865
|
-
|
|
2512
|
+
const handler = new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, OrderedCollection, OrderedCollectionPage, filterPredicate).fetchCollection(cursor);
|
|
2513
|
+
try {
|
|
2514
|
+
const response = await handler.toJsonLd().then(respondAsActivity);
|
|
2515
|
+
handler.recordPendingCollectionMetrics("served", response);
|
|
2516
|
+
return response;
|
|
2517
|
+
} catch (e) {
|
|
2518
|
+
if (!deferPendingCollectionMetrics(e, (result, response) => handler.recordPendingCollectionMetrics(result, response))) handler.recordPendingCollectionMetrics("error");
|
|
2519
|
+
throw e;
|
|
2520
|
+
}
|
|
1866
2521
|
}
|
|
1867
2522
|
/**
|
|
1868
2523
|
* Handling custom collections with support for pagination and filtering.
|
|
@@ -1877,6 +2532,15 @@ async function _handleOrderedCollection(request, { name, values, context, tracer
|
|
|
1877
2532
|
* @since 1.8.0
|
|
1878
2533
|
*/
|
|
1879
2534
|
var CustomCollectionHandler = class {
|
|
2535
|
+
name;
|
|
2536
|
+
values;
|
|
2537
|
+
context;
|
|
2538
|
+
callbacks;
|
|
2539
|
+
tracerProvider;
|
|
2540
|
+
meterProvider;
|
|
2541
|
+
Collection;
|
|
2542
|
+
CollectionPage;
|
|
2543
|
+
filterPredicate;
|
|
1880
2544
|
/**
|
|
1881
2545
|
* The tracer for telemetry.
|
|
1882
2546
|
* @type {Tracer}
|
|
@@ -1901,6 +2565,7 @@ var CustomCollectionHandler = class {
|
|
|
1901
2565
|
*/
|
|
1902
2566
|
#dispatcher;
|
|
1903
2567
|
#collection = null;
|
|
2568
|
+
#pendingCollectionMetrics = [];
|
|
1904
2569
|
/**
|
|
1905
2570
|
* Creates a new CustomCollection instance.
|
|
1906
2571
|
* @param name The name of the collection.
|
|
@@ -1912,12 +2577,13 @@ var CustomCollectionHandler = class {
|
|
|
1912
2577
|
* @param CollectionPage The CollectionPage constructor.
|
|
1913
2578
|
* @param filterPredicate Optional filter predicate for items.
|
|
1914
2579
|
*/
|
|
1915
|
-
constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), Collection, CollectionPage, filterPredicate) {
|
|
2580
|
+
constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
|
|
1916
2581
|
this.name = name$2;
|
|
1917
2582
|
this.values = values;
|
|
1918
2583
|
this.context = context;
|
|
1919
2584
|
this.callbacks = callbacks;
|
|
1920
2585
|
this.tracerProvider = tracerProvider;
|
|
2586
|
+
this.meterProvider = meterProvider;
|
|
1921
2587
|
this.Collection = Collection;
|
|
1922
2588
|
this.CollectionPage = CollectionPage;
|
|
1923
2589
|
this.filterPredicate = filterPredicate;
|
|
@@ -1970,10 +2636,12 @@ var CustomCollectionHandler = class {
|
|
|
1970
2636
|
const { prevCursor, nextCursor } = pages;
|
|
1971
2637
|
const partOf = new URL(id);
|
|
1972
2638
|
partOf.searchParams.delete("cursor");
|
|
2639
|
+
const items = this.filterItems(pages.items);
|
|
2640
|
+
this.recordPendingCollectionItemCount(true, items.length);
|
|
1973
2641
|
return {
|
|
1974
2642
|
id,
|
|
1975
2643
|
partOf,
|
|
1976
|
-
items
|
|
2644
|
+
items,
|
|
1977
2645
|
prev: this.appendToUrl(prevCursor),
|
|
1978
2646
|
next: this.appendToUrl(nextCursor)
|
|
1979
2647
|
};
|
|
@@ -1986,11 +2654,16 @@ var CustomCollectionHandler = class {
|
|
|
1986
2654
|
*/
|
|
1987
2655
|
async getProps(firstCursor) {
|
|
1988
2656
|
const lastCursor = await this.callbacks.lastCursor?.(this.context, this.values);
|
|
2657
|
+
const totalItems = await this.totalItems;
|
|
2658
|
+
if (totalItems != null) this.#pendingCollectionMetrics.push({
|
|
2659
|
+
page: false,
|
|
2660
|
+
totalItems: Number(totalItems)
|
|
2661
|
+
});
|
|
1989
2662
|
return {
|
|
1990
2663
|
id: this.#id,
|
|
1991
2664
|
first: this.appendToUrl(firstCursor),
|
|
1992
2665
|
last: this.appendToUrl(lastCursor),
|
|
1993
|
-
totalItems
|
|
2666
|
+
totalItems
|
|
1994
2667
|
};
|
|
1995
2668
|
}
|
|
1996
2669
|
/**
|
|
@@ -2000,10 +2673,12 @@ var CustomCollectionHandler = class {
|
|
|
2000
2673
|
async getPropsWithoutCursor() {
|
|
2001
2674
|
const totalItems = await this.totalItems;
|
|
2002
2675
|
const pages = await this.getPages({ totalItems });
|
|
2676
|
+
const items = this.filterItems(pages.items);
|
|
2677
|
+
this.recordPendingCollectionItemCount(false, items.length);
|
|
2003
2678
|
return {
|
|
2004
2679
|
id: this.#id,
|
|
2005
2680
|
totalItems,
|
|
2006
|
-
items
|
|
2681
|
+
items
|
|
2007
2682
|
};
|
|
2008
2683
|
}
|
|
2009
2684
|
/**
|
|
@@ -2038,12 +2713,24 @@ var CustomCollectionHandler = class {
|
|
|
2038
2713
|
* @returns A function that handles the span operation.
|
|
2039
2714
|
*/
|
|
2040
2715
|
spanPages = ({ totalItems = null, cursor = null }) => async (span) => {
|
|
2716
|
+
const pageMetricBase = this.metricBase(cursor !== null);
|
|
2717
|
+
const started = performance.now();
|
|
2041
2718
|
try {
|
|
2042
2719
|
if (totalItems !== null) span.setAttribute(this.ATTRS.TOTAL_ITEMS, totalItems);
|
|
2043
2720
|
const page = await this.dispatch(cursor);
|
|
2721
|
+
const durationMs = getDurationMs(started);
|
|
2044
2722
|
span.setAttribute(this.ATTRS.ITEMS, page.items.length);
|
|
2723
|
+
this.#pendingCollectionMetrics.push({
|
|
2724
|
+
page: pageMetricBase.page,
|
|
2725
|
+
dispatchDurationMs: durationMs,
|
|
2726
|
+
totalItems: totalItems == null ? void 0 : Number(totalItems)
|
|
2727
|
+
});
|
|
2045
2728
|
return page;
|
|
2046
2729
|
} catch (e) {
|
|
2730
|
+
this.#pendingCollectionMetrics.push({
|
|
2731
|
+
page: cursor !== null,
|
|
2732
|
+
dispatchDurationMs: getDurationMs(started)
|
|
2733
|
+
});
|
|
2047
2734
|
const message = e instanceof Error ? e.message : String(e);
|
|
2048
2735
|
span.setStatus({
|
|
2049
2736
|
code: SpanStatusCode.ERROR,
|
|
@@ -2070,6 +2757,37 @@ var CustomCollectionHandler = class {
|
|
|
2070
2757
|
filterItems(items) {
|
|
2071
2758
|
return filterCollectionItems(items, this.name, this.filterPredicate);
|
|
2072
2759
|
}
|
|
2760
|
+
metricBase(page) {
|
|
2761
|
+
return {
|
|
2762
|
+
kind: "custom",
|
|
2763
|
+
page,
|
|
2764
|
+
dispatcher: "custom"
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
metricAttributes(page, result, response) {
|
|
2768
|
+
return collectionAttributes(this.metricBase(page), result, response);
|
|
2769
|
+
}
|
|
2770
|
+
recordPendingCollectionItemCount(page, itemCount) {
|
|
2771
|
+
for (let i = this.#pendingCollectionMetrics.length - 1; i >= 0; i--) {
|
|
2772
|
+
const measurement = this.#pendingCollectionMetrics[i];
|
|
2773
|
+
if (measurement.page === page && measurement.dispatchDurationMs != null && measurement.itemCount == null) {
|
|
2774
|
+
measurement.itemCount = itemCount;
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
this.#pendingCollectionMetrics.push({
|
|
2779
|
+
page,
|
|
2780
|
+
itemCount
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
recordPendingCollectionMetrics(result, response) {
|
|
2784
|
+
for (const measurement of this.#pendingCollectionMetrics.splice(0)) {
|
|
2785
|
+
const attrs = this.metricAttributes(measurement.page, result, response);
|
|
2786
|
+
if (measurement.dispatchDurationMs != null) recordCollectionDispatchDuration(this.meterProvider, measurement.dispatchDurationMs, attrs);
|
|
2787
|
+
if (measurement.itemCount != null) recordCollectionPageItems(this.meterProvider, measurement.itemCount, attrs);
|
|
2788
|
+
if (measurement.totalItems != null) recordCollectionTotalItems(this.meterProvider, measurement.totalItems, attrs);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2073
2791
|
/**
|
|
2074
2792
|
* Appends a cursor to the URL if it exists.
|
|
2075
2793
|
* @param cursor The cursor to append, or null/undefined.
|
|
@@ -2133,14 +2851,36 @@ var CustomCollectionHandler = class {
|
|
|
2133
2851
|
*/
|
|
2134
2852
|
function exceptWrapper(handler) {
|
|
2135
2853
|
return async (request, handlerParams) => {
|
|
2854
|
+
const page = new URL(request.url).searchParams.get("cursor") != null;
|
|
2855
|
+
const { meterProvider } = handlerParams;
|
|
2856
|
+
const metricBase = {
|
|
2857
|
+
kind: "custom",
|
|
2858
|
+
page,
|
|
2859
|
+
dispatcher: "custom"
|
|
2860
|
+
};
|
|
2136
2861
|
try {
|
|
2137
|
-
|
|
2862
|
+
const response = await handler(request, handlerParams);
|
|
2863
|
+
recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "served", response));
|
|
2864
|
+
return response;
|
|
2138
2865
|
} catch (error) {
|
|
2139
2866
|
const { onNotFound, onUnauthorized } = handlerParams;
|
|
2140
2867
|
switch (error?.constructor) {
|
|
2141
|
-
case ItemsNotFoundError:
|
|
2142
|
-
|
|
2143
|
-
|
|
2868
|
+
case ItemsNotFoundError: {
|
|
2869
|
+
const response = await onNotFound(request);
|
|
2870
|
+
recordDeferredPendingCollectionMetrics(error, "not_found", response);
|
|
2871
|
+
recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "not_found", response));
|
|
2872
|
+
return response;
|
|
2873
|
+
}
|
|
2874
|
+
case UnauthorizedError: {
|
|
2875
|
+
const response = await onUnauthorized(request);
|
|
2876
|
+
recordDeferredPendingCollectionMetrics(error, "unauthorized", response);
|
|
2877
|
+
recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "unauthorized", response));
|
|
2878
|
+
return response;
|
|
2879
|
+
}
|
|
2880
|
+
default:
|
|
2881
|
+
recordDeferredPendingCollectionMetrics(error, "error");
|
|
2882
|
+
recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "error"));
|
|
2883
|
+
throw error;
|
|
2144
2884
|
}
|
|
2145
2885
|
}
|
|
2146
2886
|
};
|
|
@@ -2358,36 +3098,6 @@ function handleNodeInfoJrd(_request, context) {
|
|
|
2358
3098
|
return Promise.resolve(response);
|
|
2359
3099
|
}
|
|
2360
3100
|
//#endregion
|
|
2361
|
-
//#region src/federation/retry.ts
|
|
2362
|
-
/**
|
|
2363
|
-
* Creates an exponential backoff retry policy. The delay between retries
|
|
2364
|
-
* starts at the `initialDelay` and is multiplied by the `factor` for each
|
|
2365
|
-
* subsequent retry, up to the `maxDelay`. The policy will give up after
|
|
2366
|
-
* `maxAttempts` attempts. The actual delay is randomized to avoid
|
|
2367
|
-
* synchronization (jitter).
|
|
2368
|
-
* @param options The options for the policy.
|
|
2369
|
-
* @returns The retry policy.
|
|
2370
|
-
* @since 0.12.0
|
|
2371
|
-
*/
|
|
2372
|
-
function createExponentialBackoffPolicy(options = {}) {
|
|
2373
|
-
const initialDelay = Temporal.Duration.from(options.initialDelay ?? { seconds: 1 });
|
|
2374
|
-
const maxDelay = Temporal.Duration.from(options.maxDelay ?? { hours: 12 });
|
|
2375
|
-
const maxAttempts = options.maxAttempts ?? 10;
|
|
2376
|
-
const factor = options.factor ?? 2;
|
|
2377
|
-
const jitter = options.jitter ?? true;
|
|
2378
|
-
return ({ attempts }) => {
|
|
2379
|
-
if (attempts >= maxAttempts) return null;
|
|
2380
|
-
let milliseconds = initialDelay.total("millisecond");
|
|
2381
|
-
milliseconds *= factor ** attempts;
|
|
2382
|
-
if (jitter) {
|
|
2383
|
-
milliseconds *= 1 + Math.random();
|
|
2384
|
-
milliseconds = Math.round(milliseconds);
|
|
2385
|
-
}
|
|
2386
|
-
const delay = Temporal.Duration.from({ milliseconds });
|
|
2387
|
-
return Temporal.Duration.compare(delay, maxDelay) > 0 ? maxDelay : delay;
|
|
2388
|
-
};
|
|
2389
|
-
}
|
|
2390
|
-
//#endregion
|
|
2391
3101
|
//#region src/federation/send.ts
|
|
2392
3102
|
/**
|
|
2393
3103
|
* Extracts the inbox URLs from recipients.
|
|
@@ -2447,6 +3157,25 @@ function sendActivity(options) {
|
|
|
2447
3157
|
});
|
|
2448
3158
|
}
|
|
2449
3159
|
const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
|
|
3160
|
+
function getActivityActorId(activity) {
|
|
3161
|
+
if (!isRecord(activity)) return void 0;
|
|
3162
|
+
return getIdValue(activity.actor);
|
|
3163
|
+
}
|
|
3164
|
+
function getIdValue(value) {
|
|
3165
|
+
if (typeof value === "string" && value !== "") return value;
|
|
3166
|
+
if (value instanceof URL) return value.href;
|
|
3167
|
+
if (Array.isArray(value)) {
|
|
3168
|
+
for (const item of value) {
|
|
3169
|
+
const id = getIdValue(item);
|
|
3170
|
+
if (id != null) return id;
|
|
3171
|
+
}
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
if (isRecord(value)) return getIdValue(value.id);
|
|
3175
|
+
}
|
|
3176
|
+
function isRecord(value) {
|
|
3177
|
+
return typeof value === "object" && value != null;
|
|
3178
|
+
}
|
|
2450
3179
|
async function readLimitedResponseBody(response, maxBytes) {
|
|
2451
3180
|
if (response.body == null) return "";
|
|
2452
3181
|
const reader = response.body.getReader();
|
|
@@ -2474,12 +3203,15 @@ async function readLimitedResponseBody(response, maxBytes) {
|
|
|
2474
3203
|
if (truncated) result += "… (truncated)";
|
|
2475
3204
|
return result;
|
|
2476
3205
|
}
|
|
2477
|
-
async function sendActivityInternal({ activity, activityId, keys, inbox, headers, specDeterminer, tracerProvider }, span) {
|
|
3206
|
+
async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, headers, specDeterminer, meterProvider, tracerProvider }, span) {
|
|
2478
3207
|
const logger = getLogger([
|
|
2479
3208
|
"fedify",
|
|
2480
3209
|
"federation",
|
|
2481
3210
|
"outbox"
|
|
2482
3211
|
]);
|
|
3212
|
+
const federationMetrics = getFederationMetrics(meterProvider);
|
|
3213
|
+
const started = performance.now();
|
|
3214
|
+
let deliverySuccess = false;
|
|
2483
3215
|
headers = new Headers(headers);
|
|
2484
3216
|
headers.set("Content-Type", "application/activity+json");
|
|
2485
3217
|
const request = new Request(inbox, {
|
|
@@ -2506,34 +3238,49 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, headers
|
|
|
2506
3238
|
specDeterminer
|
|
2507
3239
|
});
|
|
2508
3240
|
} catch (error) {
|
|
3241
|
+
const transportError = error instanceof FetchError ? error : createFetchError(inbox.href, error);
|
|
2509
3242
|
logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
|
|
2510
3243
|
activityId,
|
|
2511
3244
|
inbox: inbox.href,
|
|
2512
|
-
error
|
|
3245
|
+
error: transportError
|
|
2513
3246
|
});
|
|
2514
|
-
|
|
3247
|
+
federationMetrics.recordDelivery(inbox, getDurationMs(started), false, activityType);
|
|
3248
|
+
throw transportError;
|
|
2515
3249
|
}
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
3250
|
+
try {
|
|
3251
|
+
if (!response.ok) {
|
|
3252
|
+
let error;
|
|
3253
|
+
try {
|
|
3254
|
+
error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
|
|
3255
|
+
} catch (_) {
|
|
3256
|
+
error = "";
|
|
3257
|
+
}
|
|
3258
|
+
logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
|
|
3259
|
+
activityId,
|
|
3260
|
+
inbox: inbox.href,
|
|
3261
|
+
status: response.status,
|
|
3262
|
+
statusText: response.statusText,
|
|
3263
|
+
error
|
|
3264
|
+
});
|
|
3265
|
+
throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error, response.headers);
|
|
2522
3266
|
}
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
inbox: inbox.href,
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
3267
|
+
deliverySuccess = true;
|
|
3268
|
+
const eventAttributes = {
|
|
3269
|
+
"activitypub.inbox.url": inbox.href,
|
|
3270
|
+
"activitypub.activity.id": activityId ?? ""
|
|
3271
|
+
};
|
|
3272
|
+
if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
|
|
3273
|
+
const actorId = getActivityActorId(activity);
|
|
3274
|
+
if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
|
|
3275
|
+
span.addEvent("activitypub.activity.sent", eventAttributes);
|
|
3276
|
+
} finally {
|
|
3277
|
+
federationMetrics.recordDelivery(inbox, getDurationMs(started), deliverySuccess, activityType);
|
|
2531
3278
|
}
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
3279
|
+
}
|
|
3280
|
+
function createFetchError(url, cause) {
|
|
3281
|
+
const error = new FetchError(url, cause instanceof Error ? cause.message : String(cause));
|
|
3282
|
+
error.cause = cause;
|
|
3283
|
+
return error;
|
|
2537
3284
|
}
|
|
2538
3285
|
/**
|
|
2539
3286
|
* An error that is thrown when an activity fails to send to a remote inbox.
|
|
@@ -2558,20 +3305,252 @@ var SendActivityError = class extends Error {
|
|
|
2558
3305
|
*/
|
|
2559
3306
|
responseBody;
|
|
2560
3307
|
/**
|
|
3308
|
+
* The response headers from the inbox.
|
|
3309
|
+
* @since 2.3.0
|
|
3310
|
+
*/
|
|
3311
|
+
responseHeaders;
|
|
3312
|
+
/**
|
|
2561
3313
|
* Creates a new {@link SendActivityError}.
|
|
2562
3314
|
* @param inbox The inbox URL.
|
|
2563
3315
|
* @param statusCode The HTTP status code.
|
|
2564
3316
|
* @param message The error message.
|
|
2565
3317
|
* @param responseBody The response body.
|
|
3318
|
+
* @param responseHeaders The response headers.
|
|
2566
3319
|
*/
|
|
2567
|
-
constructor(inbox, statusCode, message, responseBody) {
|
|
3320
|
+
constructor(inbox, statusCode, message, responseBody, responseHeaders) {
|
|
2568
3321
|
super(message);
|
|
2569
3322
|
this.name = "SendActivityError";
|
|
2570
3323
|
this.inbox = inbox;
|
|
2571
3324
|
this.statusCode = statusCode;
|
|
2572
3325
|
this.responseBody = responseBody;
|
|
3326
|
+
this.responseHeaders = new Headers(responseHeaders);
|
|
3327
|
+
}
|
|
3328
|
+
};
|
|
3329
|
+
//#endregion
|
|
3330
|
+
//#region src/federation/bench.ts
|
|
3331
|
+
/**
|
|
3332
|
+
* Metric reader owned by `benchmarkMode`.
|
|
3333
|
+
* @since 2.3.0
|
|
3334
|
+
*/
|
|
3335
|
+
var BenchmarkMetricReader = class extends MetricReader {
|
|
3336
|
+
onShutdown() {
|
|
3337
|
+
return Promise.resolve();
|
|
3338
|
+
}
|
|
3339
|
+
onForceFlush() {
|
|
3340
|
+
return Promise.resolve();
|
|
3341
|
+
}
|
|
3342
|
+
};
|
|
3343
|
+
/**
|
|
3344
|
+
* Creates the in-process OpenTelemetry meter provider used by benchmark mode.
|
|
3345
|
+
* @returns The meter provider and the metric reader attached to it.
|
|
3346
|
+
* @since 2.3.0
|
|
3347
|
+
*/
|
|
3348
|
+
function createBenchmarkMeterProvider() {
|
|
3349
|
+
const reader = new BenchmarkMetricReader();
|
|
3350
|
+
return {
|
|
3351
|
+
meterProvider: new MeterProvider$1({ readers: [reader] }),
|
|
3352
|
+
reader
|
|
3353
|
+
};
|
|
3354
|
+
}
|
|
3355
|
+
/**
|
|
3356
|
+
* Collects and serializes benchmark-mode metrics from a benchmark reader.
|
|
3357
|
+
* @param reader The benchmark metric reader to collect from.
|
|
3358
|
+
* @returns A server metric snapshot with any collection errors stringified.
|
|
3359
|
+
* @since 2.3.0
|
|
3360
|
+
*/
|
|
3361
|
+
async function collectBenchmarkMetrics(reader) {
|
|
3362
|
+
const result = await reader.collect();
|
|
3363
|
+
return {
|
|
3364
|
+
version: 1,
|
|
3365
|
+
source: "server",
|
|
3366
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3367
|
+
scopeMetrics: serializeScopeMetrics(result.resourceMetrics),
|
|
3368
|
+
errors: result.errors.map((error) => String(error))
|
|
3369
|
+
};
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* Handles `GET /.well-known/fedify/bench/stats`.
|
|
3373
|
+
* @param request The HTTP request to handle.
|
|
3374
|
+
* @param reader The benchmark metric reader to collect from.
|
|
3375
|
+
* @returns A JSON metric snapshot response, or `405 Method Not Allowed`.
|
|
3376
|
+
* @since 2.3.0
|
|
3377
|
+
*/
|
|
3378
|
+
async function handleBenchmarkStats(request, reader) {
|
|
3379
|
+
if (request.method !== "GET") return new Response("Method not allowed", {
|
|
3380
|
+
status: 405,
|
|
3381
|
+
headers: { "Allow": "GET" }
|
|
3382
|
+
});
|
|
3383
|
+
return jsonResponse(await collectBenchmarkMetrics(reader));
|
|
3384
|
+
}
|
|
3385
|
+
/**
|
|
3386
|
+
* Handles `POST /.well-known/fedify/bench/trigger`.
|
|
3387
|
+
*
|
|
3388
|
+
* The handler validates a benchmark trigger request, checks recipients against
|
|
3389
|
+
* server-controlled trigger options, and calls `Context.sendActivity()` to use
|
|
3390
|
+
* the target's normal outbox path.
|
|
3391
|
+
* @param request The HTTP request to handle.
|
|
3392
|
+
* @param context The Fedify context used to resolve actors and send activity.
|
|
3393
|
+
* @param options Server-controlled benchmark trigger delivery options.
|
|
3394
|
+
* @returns A JSON response describing the sent activity, or a validation error.
|
|
3395
|
+
* @since 2.3.0
|
|
3396
|
+
*/
|
|
3397
|
+
async function handleBenchmarkTrigger(request, context, options = {}) {
|
|
3398
|
+
if (request.method !== "POST") return new Response("Method not allowed", {
|
|
3399
|
+
status: 405,
|
|
3400
|
+
headers: { "Allow": "POST" }
|
|
3401
|
+
});
|
|
3402
|
+
let json;
|
|
3403
|
+
try {
|
|
3404
|
+
json = await request.json();
|
|
3405
|
+
} catch {
|
|
3406
|
+
return jsonResponse({ error: "Invalid JSON request body." }, 400);
|
|
3407
|
+
}
|
|
3408
|
+
try {
|
|
3409
|
+
const body = asRecord(json, "request body");
|
|
3410
|
+
const sender = parseSender(body.sender);
|
|
3411
|
+
const recipients = await parseRecipients(body.recipients, context);
|
|
3412
|
+
const activity = await parseActivity(body.activity, context);
|
|
3413
|
+
if (activity.id == null) throw new BenchmarkTriggerError("activity must have an id.");
|
|
3414
|
+
const activityId = activity.id.href;
|
|
3415
|
+
const inboxes = extractInboxes({ recipients });
|
|
3416
|
+
const inboxUrls = Object.keys(inboxes);
|
|
3417
|
+
if (inboxUrls.length < 1) throw new BenchmarkTriggerError("No valid recipient inboxes found. The recipients list must not be empty.");
|
|
3418
|
+
const unsafeInboxes = options.allowUnsafeRecipients ? [] : inboxUrls.filter((inbox) => !options.sinks?.has(inbox));
|
|
3419
|
+
if (unsafeInboxes.length > 0) return jsonResponse({
|
|
3420
|
+
error: "unsafe_recipient",
|
|
3421
|
+
unsafeInboxes
|
|
3422
|
+
}, 403);
|
|
3423
|
+
await context.sendActivity(sender, recipients, activity);
|
|
3424
|
+
return jsonResponse({
|
|
3425
|
+
version: 1,
|
|
3426
|
+
activityId,
|
|
3427
|
+
queueCorrelationId: activityId,
|
|
3428
|
+
recipientCount: recipients.length,
|
|
3429
|
+
inboxCount: inboxUrls.length
|
|
3430
|
+
}, 202);
|
|
3431
|
+
} catch (error) {
|
|
3432
|
+
if (error instanceof BenchmarkTriggerError) return jsonResponse({ error: error.message }, error.status);
|
|
3433
|
+
throw error;
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
var BenchmarkTriggerError = class extends Error {
|
|
3437
|
+
status;
|
|
3438
|
+
constructor(message, status = 400) {
|
|
3439
|
+
super(message);
|
|
3440
|
+
this.status = status;
|
|
2573
3441
|
}
|
|
2574
3442
|
};
|
|
3443
|
+
function parseSender(value) {
|
|
3444
|
+
const sender = asRecord(value, "sender");
|
|
3445
|
+
if (typeof sender.identifier === "string") return { identifier: sender.identifier };
|
|
3446
|
+
if (typeof sender.username === "string") return { username: sender.username };
|
|
3447
|
+
throw new BenchmarkTriggerError("sender must be { identifier } or { username }.");
|
|
3448
|
+
}
|
|
3449
|
+
async function parseRecipients(value, context) {
|
|
3450
|
+
if (!Array.isArray(value)) throw new BenchmarkTriggerError("recipients must be an array.");
|
|
3451
|
+
return await Promise.all(value.map(async (item) => {
|
|
3452
|
+
let object;
|
|
3453
|
+
try {
|
|
3454
|
+
object = await Object$1.fromJsonLd(item, {
|
|
3455
|
+
documentLoader: context.documentLoader,
|
|
3456
|
+
contextLoader: context.contextLoader
|
|
3457
|
+
});
|
|
3458
|
+
} catch (error) {
|
|
3459
|
+
throw new BenchmarkTriggerError(`Invalid ActivityPub recipient: ${error}`);
|
|
3460
|
+
}
|
|
3461
|
+
if (!isRecipient(object)) throw new BenchmarkTriggerError("each recipient must be an ActivityPub actor.");
|
|
3462
|
+
const recipient = object;
|
|
3463
|
+
if (recipient.id == null || recipient.inboxId == null) throw new BenchmarkTriggerError("each recipient must have id and inbox properties.");
|
|
3464
|
+
return recipient;
|
|
3465
|
+
}));
|
|
3466
|
+
}
|
|
3467
|
+
function isRecipient(value) {
|
|
3468
|
+
return value != null && typeof value === "object" && "id" in value && "inboxId" in value;
|
|
3469
|
+
}
|
|
3470
|
+
async function parseActivity(value, context) {
|
|
3471
|
+
try {
|
|
3472
|
+
return await Activity.fromJsonLd(value, {
|
|
3473
|
+
documentLoader: context.documentLoader,
|
|
3474
|
+
contextLoader: context.contextLoader
|
|
3475
|
+
});
|
|
3476
|
+
} catch (error) {
|
|
3477
|
+
throw new BenchmarkTriggerError(`Invalid ActivityPub activity: ${error}`);
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
function asRecord(value, name) {
|
|
3481
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) throw new BenchmarkTriggerError(`${name} must be an object.`);
|
|
3482
|
+
return value;
|
|
3483
|
+
}
|
|
3484
|
+
function jsonResponse(body, status = 200) {
|
|
3485
|
+
return new Response(JSON.stringify(body), {
|
|
3486
|
+
status,
|
|
3487
|
+
headers: { "Content-Type": "application/json" }
|
|
3488
|
+
});
|
|
3489
|
+
}
|
|
3490
|
+
function serializeScopeMetrics(resourceMetrics) {
|
|
3491
|
+
return resourceMetrics.scopeMetrics.map(serializeScope);
|
|
3492
|
+
}
|
|
3493
|
+
function serializeScope(scopeMetrics) {
|
|
3494
|
+
return {
|
|
3495
|
+
scope: {
|
|
3496
|
+
name: scopeMetrics.scope.name,
|
|
3497
|
+
version: scopeMetrics.scope.version
|
|
3498
|
+
},
|
|
3499
|
+
metrics: scopeMetrics.metrics.map(serializeMetric)
|
|
3500
|
+
};
|
|
3501
|
+
}
|
|
3502
|
+
function serializeMetric(metric) {
|
|
3503
|
+
return {
|
|
3504
|
+
name: metric.descriptor.name,
|
|
3505
|
+
description: metric.descriptor.description,
|
|
3506
|
+
unit: metric.descriptor.unit,
|
|
3507
|
+
dataPointType: serializeDataPointType(metric.dataPointType),
|
|
3508
|
+
dataPoints: metric.dataPoints.map((point) => ({
|
|
3509
|
+
attributes: { ...point.attributes },
|
|
3510
|
+
startTime: point.startTime,
|
|
3511
|
+
endTime: point.endTime,
|
|
3512
|
+
value: point.value
|
|
3513
|
+
}))
|
|
3514
|
+
};
|
|
3515
|
+
}
|
|
3516
|
+
function serializeDataPointType(dataPointType) {
|
|
3517
|
+
switch (dataPointType) {
|
|
3518
|
+
case DataPointType.HISTOGRAM: return "histogram";
|
|
3519
|
+
case DataPointType.EXPONENTIAL_HISTOGRAM: return "exponential_histogram";
|
|
3520
|
+
case DataPointType.GAUGE: return "gauge";
|
|
3521
|
+
case DataPointType.SUM: return "sum";
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
//#endregion
|
|
3525
|
+
//#region src/federation/retry.ts
|
|
3526
|
+
/**
|
|
3527
|
+
* Creates an exponential backoff retry policy. The delay between retries
|
|
3528
|
+
* starts at the `initialDelay` and is multiplied by the `factor` for each
|
|
3529
|
+
* subsequent retry, up to the `maxDelay`. The policy will give up after
|
|
3530
|
+
* `maxAttempts` attempts. The actual delay is randomized to avoid
|
|
3531
|
+
* synchronization (jitter).
|
|
3532
|
+
* @param options The options for the policy.
|
|
3533
|
+
* @returns The retry policy.
|
|
3534
|
+
* @since 0.12.0
|
|
3535
|
+
*/
|
|
3536
|
+
function createExponentialBackoffPolicy(options = {}) {
|
|
3537
|
+
const initialDelay = Temporal.Duration.from(options.initialDelay ?? { seconds: 1 });
|
|
3538
|
+
const maxDelay = Temporal.Duration.from(options.maxDelay ?? { hours: 12 });
|
|
3539
|
+
const maxAttempts = options.maxAttempts ?? 10;
|
|
3540
|
+
const factor = options.factor ?? 2;
|
|
3541
|
+
const jitter = options.jitter ?? true;
|
|
3542
|
+
return ({ attempts }) => {
|
|
3543
|
+
if (attempts >= maxAttempts) return null;
|
|
3544
|
+
let milliseconds = initialDelay.total("millisecond");
|
|
3545
|
+
milliseconds *= factor ** attempts;
|
|
3546
|
+
if (jitter) {
|
|
3547
|
+
milliseconds *= 1 + Math.random();
|
|
3548
|
+
milliseconds = Math.round(milliseconds);
|
|
3549
|
+
}
|
|
3550
|
+
const delay = Temporal.Duration.from({ milliseconds });
|
|
3551
|
+
return Temporal.Duration.compare(delay, maxDelay) > 0 ? maxDelay : delay;
|
|
3552
|
+
};
|
|
3553
|
+
}
|
|
2575
3554
|
//#endregion
|
|
2576
3555
|
//#region src/federation/webfinger.ts
|
|
2577
3556
|
const logger = getLogger([
|
|
@@ -2587,22 +3566,72 @@ const logger = getLogger([
|
|
|
2587
3566
|
* @returns The response to the request.
|
|
2588
3567
|
*/
|
|
2589
3568
|
async function handleWebFinger(request, options) {
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
});
|
|
2601
|
-
throw error;
|
|
2602
|
-
} finally {
|
|
2603
|
-
span.end();
|
|
3569
|
+
const meterProvider = options.meterProvider;
|
|
3570
|
+
const start = meterProvider == null ? 0 : performance.now();
|
|
3571
|
+
const scheme = computeResourceScheme(options.context.url.searchParams.get("resource"));
|
|
3572
|
+
let notFoundResponse;
|
|
3573
|
+
const wrappedOptions = {
|
|
3574
|
+
...options,
|
|
3575
|
+
async onNotFound(req) {
|
|
3576
|
+
const r = await options.onNotFound(req);
|
|
3577
|
+
notFoundResponse = r;
|
|
3578
|
+
return r;
|
|
2604
3579
|
}
|
|
2605
|
-
}
|
|
3580
|
+
};
|
|
3581
|
+
let response;
|
|
3582
|
+
try {
|
|
3583
|
+
if (options.tracer == null) response = await handleWebFingerInternal(request, wrappedOptions);
|
|
3584
|
+
else response = await options.tracer.startActiveSpan("webfinger.handle", { kind: SpanKind.SERVER }, async (span) => {
|
|
3585
|
+
try {
|
|
3586
|
+
const inner = await handleWebFingerInternal(request, wrappedOptions);
|
|
3587
|
+
span.setStatus({ code: inner.ok ? SpanStatusCode.UNSET : SpanStatusCode.ERROR });
|
|
3588
|
+
return inner;
|
|
3589
|
+
} catch (error) {
|
|
3590
|
+
span.setStatus({
|
|
3591
|
+
code: SpanStatusCode.ERROR,
|
|
3592
|
+
message: String(error)
|
|
3593
|
+
});
|
|
3594
|
+
throw error;
|
|
3595
|
+
} finally {
|
|
3596
|
+
span.end();
|
|
3597
|
+
}
|
|
3598
|
+
});
|
|
3599
|
+
return response;
|
|
3600
|
+
} finally {
|
|
3601
|
+
if (meterProvider != null) recordWebFingerHandle(meterProvider, {
|
|
3602
|
+
durationMs: Math.max(0, performance.now() - start),
|
|
3603
|
+
result: classifyWebFingerHandleResult(response, notFoundResponse),
|
|
3604
|
+
scheme,
|
|
3605
|
+
statusCode: response?.status
|
|
3606
|
+
});
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
const WEBFINGER_HANDLE_SCHEME_WHITELIST = new Set([
|
|
3610
|
+
"acct",
|
|
3611
|
+
"http",
|
|
3612
|
+
"https",
|
|
3613
|
+
"mailto"
|
|
3614
|
+
]);
|
|
3615
|
+
function isAllowedResourceScheme(scheme) {
|
|
3616
|
+
return WEBFINGER_HANDLE_SCHEME_WHITELIST.has(scheme);
|
|
3617
|
+
}
|
|
3618
|
+
function computeResourceScheme(resource) {
|
|
3619
|
+
if (resource == null) return void 0;
|
|
3620
|
+
const colon = resource.indexOf(":");
|
|
3621
|
+
if (colon <= 0) return void 0;
|
|
3622
|
+
const candidate = resource.substring(0, colon).toLowerCase();
|
|
3623
|
+
return isAllowedResourceScheme(candidate) ? candidate : "other";
|
|
3624
|
+
}
|
|
3625
|
+
function classifyWebFingerHandleResult(response, notFoundResponse) {
|
|
3626
|
+
if (response == null) return "error";
|
|
3627
|
+
if (notFoundResponse != null && response === notFoundResponse) return "not_found";
|
|
3628
|
+
switch (response.status) {
|
|
3629
|
+
case 200: return "resolved";
|
|
3630
|
+
case 400: return "invalid";
|
|
3631
|
+
case 404: return "not_found";
|
|
3632
|
+
case 410: return "tombstoned";
|
|
3633
|
+
default: return "error";
|
|
3634
|
+
}
|
|
2606
3635
|
}
|
|
2607
3636
|
async function handleWebFingerInternal(request, { context, host, actorDispatcher, actorHandleMapper, actorAliasMapper, onNotFound, span, webFingerLinksDispatcher }) {
|
|
2608
3637
|
if (actorDispatcher == null) {
|
|
@@ -2718,10 +3747,124 @@ var middleware_exports = /* @__PURE__ */ __exportAll({
|
|
|
2718
3747
|
OutboxContextImpl: () => OutboxContextImpl,
|
|
2719
3748
|
createFederation: () => createFederation
|
|
2720
3749
|
});
|
|
3750
|
+
const circuitBreakerCasWarningKvStores = /* @__PURE__ */ new WeakSet();
|
|
3751
|
+
let nextQueueDepthGaugeSourceId = 0;
|
|
3752
|
+
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})$");
|
|
3753
|
+
function parseRetryAfter(headers, now = Temporal.Now.instant()) {
|
|
3754
|
+
const value = headers.get("Retry-After");
|
|
3755
|
+
if (value == null) return void 0;
|
|
3756
|
+
const trimmed = value.trim();
|
|
3757
|
+
if (/^\d+$/.test(trimmed)) {
|
|
3758
|
+
const seconds = Number(trimmed);
|
|
3759
|
+
if (!Number.isFinite(seconds)) return void 0;
|
|
3760
|
+
return parseRetryAfterDuration({ seconds });
|
|
3761
|
+
}
|
|
3762
|
+
if (!retryAfterHttpDate.test(trimmed)) return void 0;
|
|
3763
|
+
const httpDate = trimmed.endsWith("GMT") ? trimmed : `${trimmed} GMT`;
|
|
3764
|
+
const retryAtMs = Date.parse(httpDate);
|
|
3765
|
+
if (Number.isNaN(retryAtMs)) return void 0;
|
|
3766
|
+
const nowMs = Number(now.epochMilliseconds);
|
|
3767
|
+
return parseRetryAfterDuration({ milliseconds: Math.max(0, retryAtMs - nowMs) });
|
|
3768
|
+
}
|
|
3769
|
+
function parseRetryAfterDuration(durationLike) {
|
|
3770
|
+
try {
|
|
3771
|
+
return Temporal.Duration.from(durationLike);
|
|
3772
|
+
} catch (error) {
|
|
3773
|
+
if (error instanceof RangeError) return void 0;
|
|
3774
|
+
throw error;
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
function clampNegativeDelay(delay) {
|
|
3778
|
+
return delay.sign < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay;
|
|
3779
|
+
}
|
|
3780
|
+
function getBenchmarkRelaxations(allowPrivateAddress, signatureTimeWindow) {
|
|
3781
|
+
const relaxations = [];
|
|
3782
|
+
if (allowPrivateAddress) relaxations.push({
|
|
3783
|
+
protection: "private_address_checks",
|
|
3784
|
+
effect: "disabled",
|
|
3785
|
+
effectiveValue: true
|
|
3786
|
+
});
|
|
3787
|
+
if (signatureTimeWindow === false) relaxations.push({
|
|
3788
|
+
protection: "http_signature_time_window",
|
|
3789
|
+
effect: "disabled",
|
|
3790
|
+
effectiveValue: false,
|
|
3791
|
+
secureDefaultSeconds: 3600
|
|
3792
|
+
});
|
|
3793
|
+
else try {
|
|
3794
|
+
const seconds = Temporal.Duration.from(signatureTimeWindow).total({ unit: "seconds" });
|
|
3795
|
+
if (seconds !== 3600) relaxations.push({
|
|
3796
|
+
protection: "http_signature_time_window",
|
|
3797
|
+
effect: "changed",
|
|
3798
|
+
effectiveSeconds: seconds,
|
|
3799
|
+
secureDefaultSeconds: 3600
|
|
3800
|
+
});
|
|
3801
|
+
} catch {}
|
|
3802
|
+
return relaxations;
|
|
3803
|
+
}
|
|
3804
|
+
function formatBenchmarkRelaxations(relaxations) {
|
|
3805
|
+
if (relaxations.length < 1) return "no benchmark-only protections relaxed";
|
|
3806
|
+
return relaxations.map((relaxation) => {
|
|
3807
|
+
switch (relaxation.protection) {
|
|
3808
|
+
case "private_address_checks": return "private address checks disabled (allowPrivateAddress=true)";
|
|
3809
|
+
case "http_signature_time_window":
|
|
3810
|
+
if (relaxation.effect === "disabled") return `HTTP Signature time window disabled (signatureTimeWindow=false)`;
|
|
3811
|
+
return `HTTP Signature time window set to ${relaxation.effectiveSeconds}s (secure default: ${relaxation.secureDefaultSeconds}s)`;
|
|
3812
|
+
}
|
|
3813
|
+
}).join("; ");
|
|
3814
|
+
}
|
|
3815
|
+
function getBenchmarkTriggerOptions(benchmarkOptions) {
|
|
3816
|
+
const sinks = benchmarkOptions.triggerSinks?.map((sink) => {
|
|
3817
|
+
try {
|
|
3818
|
+
return new URL(sink).href;
|
|
3819
|
+
} catch {
|
|
3820
|
+
throw new TypeError("benchmarkMode.triggerSinks must contain only URLs.");
|
|
3821
|
+
}
|
|
3822
|
+
});
|
|
3823
|
+
return {
|
|
3824
|
+
sinks: sinks == null ? void 0 : new Set(sinks),
|
|
3825
|
+
allowUnsafeRecipients: benchmarkOptions.allowUnsafeTriggerRecipients === true
|
|
3826
|
+
};
|
|
3827
|
+
}
|
|
3828
|
+
function maxDelay(first, second) {
|
|
3829
|
+
return Temporal.Duration.compare(first, second) >= 0 ? first : second;
|
|
3830
|
+
}
|
|
3831
|
+
function isTransportDeliveryError(error) {
|
|
3832
|
+
return error instanceof FetchError || isAbortError(error);
|
|
3833
|
+
}
|
|
3834
|
+
function toCircuitBreakerMetricState(state) {
|
|
3835
|
+
return state === "half-open" ? "half_open" : state;
|
|
3836
|
+
}
|
|
3837
|
+
function recordCircuitBreakerSpanEvent(span, remoteHost, change) {
|
|
3838
|
+
span.addEvent("activitypub.circuit_breaker.state_change", {
|
|
3839
|
+
"activitypub.remote.host": remoteHost,
|
|
3840
|
+
"activitypub.circuit_breaker.previous_state": toCircuitBreakerMetricState(change.previousState),
|
|
3841
|
+
"activitypub.circuit_breaker.state": toCircuitBreakerMetricState(change.newState)
|
|
3842
|
+
});
|
|
3843
|
+
}
|
|
3844
|
+
function recordCircuitBreakerHeldSpanEvent(span, remoteHost, state) {
|
|
3845
|
+
span.addEvent("activitypub.circuit_breaker.held", {
|
|
3846
|
+
"activitypub.remote.host": remoteHost,
|
|
3847
|
+
"activitypub.circuit_breaker.state": toCircuitBreakerMetricState(state)
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3850
|
+
function isRemoteContextLoadingFailure(error) {
|
|
3851
|
+
return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
|
|
3852
|
+
}
|
|
3853
|
+
function isPermanentRemoteContextError(error) {
|
|
3854
|
+
if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
|
|
3855
|
+
const details = error.details;
|
|
3856
|
+
if (details?.code === "invalid remote context") return true;
|
|
3857
|
+
return isRemoteContextLoadingFailure(error) && typeof details?.url === "string" && !URL.canParse(details.url) && isClearlyMalformedContextReference(details.url);
|
|
3858
|
+
}
|
|
3859
|
+
function isPermanentInboxParseError(error) {
|
|
3860
|
+
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));
|
|
3861
|
+
}
|
|
2721
3862
|
/**
|
|
2722
3863
|
* Create a new {@link Federation} instance.
|
|
2723
|
-
* @param
|
|
3864
|
+
* @param options Parameters for initializing the instance.
|
|
2724
3865
|
* @returns A new {@link Federation} instance.
|
|
3866
|
+
* @throws {TypeError} If benchmark mode and `meterProvider` are both
|
|
3867
|
+
* specified.
|
|
2725
3868
|
* @since 0.10.0
|
|
2726
3869
|
*/
|
|
2727
3870
|
function createFederation(options) {
|
|
@@ -2749,12 +3892,37 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2749
3892
|
skipSignatureVerification;
|
|
2750
3893
|
outboxRetryPolicy;
|
|
2751
3894
|
inboxRetryPolicy;
|
|
3895
|
+
circuitBreaker;
|
|
2752
3896
|
activityTransformers;
|
|
2753
3897
|
_tracerProvider;
|
|
3898
|
+
_meterProvider;
|
|
2754
3899
|
firstKnock;
|
|
2755
3900
|
inboxChallengePolicy;
|
|
3901
|
+
benchmarkMode;
|
|
3902
|
+
benchmarkMetricReader;
|
|
3903
|
+
benchmarkTriggerOptions;
|
|
3904
|
+
#queueDepthGaugeSourceId = `fedify-${(++nextQueueDepthGaugeSourceId).toString(36)}`;
|
|
3905
|
+
#queueDepthGaugeEntries = [];
|
|
3906
|
+
#queueDepthGaugeMeterProvider;
|
|
2756
3907
|
constructor(options) {
|
|
2757
3908
|
super();
|
|
3909
|
+
const benchmarkMode = options.benchmarkMode != null && options.benchmarkMode !== false;
|
|
3910
|
+
const benchmarkOptions = typeof options.benchmarkMode === "object" ? options.benchmarkMode : {};
|
|
3911
|
+
const hasCustomLoaderFactory = options.documentLoaderFactory != null || options.contextLoaderFactory != null;
|
|
3912
|
+
const allowPrivateAddress = options.allowPrivateAddress ?? (benchmarkMode && !hasCustomLoaderFactory ? true : false);
|
|
3913
|
+
const signatureTimeWindow = options.signatureTimeWindow ?? (benchmarkMode ? false : { hours: 1 });
|
|
3914
|
+
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.");
|
|
3915
|
+
if (benchmarkMode) {
|
|
3916
|
+
const relaxations = getBenchmarkRelaxations(allowPrivateAddress, signatureTimeWindow);
|
|
3917
|
+
const relaxationSummary = formatBenchmarkRelaxations(relaxations);
|
|
3918
|
+
getLogger([
|
|
3919
|
+
"fedify",
|
|
3920
|
+
"federation",
|
|
3921
|
+
"benchmark"
|
|
3922
|
+
]).warn(`Fedify benchmarkMode is enabled; ${relaxationSummary}. Benchmark endpoints are active and must not be used in production.`, { relaxations });
|
|
3923
|
+
}
|
|
3924
|
+
this.benchmarkMode = benchmarkMode;
|
|
3925
|
+
this.benchmarkTriggerOptions = benchmarkMode ? getBenchmarkTriggerOptions(benchmarkOptions) : {};
|
|
2758
3926
|
this.kv = options.kv;
|
|
2759
3927
|
this.kvPrefixes = {
|
|
2760
3928
|
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
@@ -2762,6 +3930,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2762
3930
|
publicKey: ["_fedify", "publicKey"],
|
|
2763
3931
|
httpMessageSignaturesSpec: ["_fedify", "httpMessageSignaturesSpec"],
|
|
2764
3932
|
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
|
|
3933
|
+
circuitBreaker: ["_fedify", "circuit"],
|
|
2765
3934
|
...options.kvPrefixes ?? {}
|
|
2766
3935
|
};
|
|
2767
3936
|
if (options.queue == null) {
|
|
@@ -2777,6 +3946,25 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2777
3946
|
this.outboxQueue = options.queue.outbox;
|
|
2778
3947
|
this.fanoutQueue = options.queue.fanout;
|
|
2779
3948
|
}
|
|
3949
|
+
if (options.circuitBreaker !== false && this.outboxQueue != null) {
|
|
3950
|
+
this.circuitBreaker = new CircuitBreaker({
|
|
3951
|
+
kv: options.kv,
|
|
3952
|
+
prefix: this.kvPrefixes.circuitBreaker,
|
|
3953
|
+
options: options.circuitBreaker,
|
|
3954
|
+
stateChangeObserver: (remoteHost, _previousState, newState) => {
|
|
3955
|
+
const metricState = toCircuitBreakerMetricState(newState);
|
|
3956
|
+
recordCircuitBreakerStateChange(this.meterProvider, remoteHost, metricState);
|
|
3957
|
+
}
|
|
3958
|
+
});
|
|
3959
|
+
if (options.kv.cas == null && !circuitBreakerCasWarningKvStores.has(options.kv)) {
|
|
3960
|
+
circuitBreakerCasWarningKvStores.add(options.kv);
|
|
3961
|
+
getLogger([
|
|
3962
|
+
"fedify",
|
|
3963
|
+
"federation",
|
|
3964
|
+
"circuit"
|
|
3965
|
+
]).warn("The configured key-value store does not support CAS; outbound delivery circuit breaker updates may race under concurrent workers.");
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
2780
3968
|
this.inboxQueueStarted = false;
|
|
2781
3969
|
this.outboxQueueStarted = false;
|
|
2782
3970
|
this.fanoutQueueStarted = false;
|
|
@@ -2802,48 +3990,109 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2802
3990
|
}
|
|
2803
3991
|
this.router.trailingSlashInsensitive = options.trailingSlashInsensitive ?? false;
|
|
2804
3992
|
this._initializeRouter();
|
|
2805
|
-
if (options.allowPrivateAddress || options.userAgent != null) {
|
|
3993
|
+
if (options.allowPrivateAddress === true || options.userAgent != null) {
|
|
2806
3994
|
if (options.documentLoaderFactory != null) throw new TypeError("Cannot set documentLoaderFactory with allowPrivateAddress or userAgent options.");
|
|
2807
3995
|
if (options.contextLoaderFactory != null) throw new TypeError("Cannot set contextLoaderFactory with allowPrivateAddress or userAgent options.");
|
|
2808
3996
|
if (options.authenticatedDocumentLoaderFactory != null) throw new TypeError("Cannot set authenticatedDocumentLoaderFactory with allowPrivateAddress or userAgent options.");
|
|
2809
3997
|
}
|
|
2810
|
-
const {
|
|
2811
|
-
this.allowPrivateAddress = allowPrivateAddress
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
3998
|
+
const { userAgent } = options;
|
|
3999
|
+
this.allowPrivateAddress = allowPrivateAddress;
|
|
4000
|
+
const userDocumentLoaderFactory = options.documentLoaderFactory;
|
|
4001
|
+
const userContextLoaderFactory = options.contextLoaderFactory;
|
|
4002
|
+
const userAuthFactory = options.authenticatedDocumentLoaderFactory;
|
|
4003
|
+
const builtinDocumentLoaderFactory = (opts) => kvCache({
|
|
4004
|
+
loader: getDocumentLoader({
|
|
4005
|
+
allowPrivateAddress: opts?.allowPrivateAddress ?? allowPrivateAddress,
|
|
4006
|
+
userAgent: opts?.userAgent ?? userAgent
|
|
4007
|
+
}),
|
|
4008
|
+
kv: options.kv,
|
|
4009
|
+
prefix: this.kvPrefixes.remoteDocument,
|
|
4010
|
+
meterProvider: this._meterProvider,
|
|
4011
|
+
kind: "object"
|
|
4012
|
+
});
|
|
4013
|
+
const builtinContextLoaderFactory = (opts) => kvCache({
|
|
4014
|
+
loader: getDocumentLoader({
|
|
4015
|
+
allowPrivateAddress: opts?.allowPrivateAddress ?? allowPrivateAddress,
|
|
4016
|
+
userAgent: opts?.userAgent ?? userAgent
|
|
4017
|
+
}),
|
|
4018
|
+
kv: options.kv,
|
|
4019
|
+
prefix: this.kvPrefixes.remoteDocument,
|
|
4020
|
+
meterProvider: this._meterProvider,
|
|
4021
|
+
kind: "context"
|
|
4022
|
+
});
|
|
4023
|
+
this.documentLoaderFactory = (opts) => instrumentDocumentLoader((userDocumentLoaderFactory ?? builtinDocumentLoaderFactory)(opts), {
|
|
4024
|
+
meterProvider: this._meterProvider,
|
|
4025
|
+
kind: "object",
|
|
4026
|
+
cacheEnabled: userDocumentLoaderFactory == null ? true : void 0
|
|
2821
4027
|
});
|
|
2822
|
-
|
|
2823
|
-
this.
|
|
2824
|
-
|
|
2825
|
-
|
|
4028
|
+
const resolvedContextLoaderFactory = userContextLoaderFactory ?? userDocumentLoaderFactory ?? builtinContextLoaderFactory;
|
|
4029
|
+
this.contextLoaderFactory = (opts) => instrumentDocumentLoader(resolvedContextLoaderFactory(opts), {
|
|
4030
|
+
meterProvider: this._meterProvider,
|
|
4031
|
+
kind: "context",
|
|
4032
|
+
cacheEnabled: userContextLoaderFactory == null && userDocumentLoaderFactory == null ? true : void 0
|
|
4033
|
+
});
|
|
4034
|
+
this.authenticatedDocumentLoaderFactory = (identity, factoryOpts) => instrumentDocumentLoader(userAuthFactory != null ? userAuthFactory(identity, factoryOpts) : getAuthenticatedDocumentLoader(identity, {
|
|
4035
|
+
allowPrivateAddress: factoryOpts?.allowPrivateAddress ?? allowPrivateAddress,
|
|
4036
|
+
userAgent: factoryOpts?.userAgent ?? userAgent,
|
|
2826
4037
|
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, options.firstKnock),
|
|
2827
4038
|
tracerProvider: this.tracerProvider
|
|
2828
|
-
})
|
|
4039
|
+
}), {
|
|
4040
|
+
meterProvider: this._meterProvider,
|
|
4041
|
+
kind: "object",
|
|
4042
|
+
cacheEnabled: userAuthFactory == null ? false : void 0
|
|
4043
|
+
});
|
|
2829
4044
|
this.userAgent = userAgent;
|
|
2830
4045
|
this.onOutboxError = options.onOutboxError;
|
|
2831
4046
|
this.permanentFailureStatusCodes = options.permanentFailureStatusCodes ?? [404, 410];
|
|
2832
|
-
this.signatureTimeWindow =
|
|
4047
|
+
this.signatureTimeWindow = signatureTimeWindow;
|
|
2833
4048
|
this.skipSignatureVerification = options.skipSignatureVerification ?? false;
|
|
2834
4049
|
this.inboxChallengePolicy = options.inboxChallengePolicy;
|
|
2835
4050
|
this.outboxRetryPolicy = options.outboxRetryPolicy ?? createExponentialBackoffPolicy();
|
|
2836
4051
|
this.inboxRetryPolicy = options.inboxRetryPolicy ?? createExponentialBackoffPolicy();
|
|
2837
4052
|
this.activityTransformers = options.activityTransformers ?? getDefaultActivityTransformers();
|
|
2838
4053
|
this._tracerProvider = options.tracerProvider;
|
|
4054
|
+
if (benchmarkMode) {
|
|
4055
|
+
const benchmarkMetrics = createBenchmarkMeterProvider();
|
|
4056
|
+
this._meterProvider = benchmarkMetrics.meterProvider;
|
|
4057
|
+
this.benchmarkMetricReader = benchmarkMetrics.reader;
|
|
4058
|
+
} else this._meterProvider = options.meterProvider;
|
|
4059
|
+
this.#queueDepthGaugeEntries = [
|
|
4060
|
+
{
|
|
4061
|
+
role: "inbox",
|
|
4062
|
+
queue: this.inboxQueue
|
|
4063
|
+
},
|
|
4064
|
+
{
|
|
4065
|
+
role: "outbox",
|
|
4066
|
+
queue: this.outboxQueue
|
|
4067
|
+
},
|
|
4068
|
+
{
|
|
4069
|
+
role: "fanout",
|
|
4070
|
+
queue: this.fanoutQueue
|
|
4071
|
+
}
|
|
4072
|
+
];
|
|
4073
|
+
this.#registerQueueDepthGauge(this._meterProvider ?? metrics.getMeterProvider());
|
|
2839
4074
|
this.firstKnock = options.firstKnock;
|
|
2840
4075
|
}
|
|
2841
4076
|
get tracerProvider() {
|
|
2842
4077
|
return this._tracerProvider ?? trace.getTracerProvider();
|
|
2843
4078
|
}
|
|
4079
|
+
get meterProvider() {
|
|
4080
|
+
const meterProvider = this._meterProvider ?? metrics.getMeterProvider();
|
|
4081
|
+
this.#registerQueueDepthGauge(meterProvider);
|
|
4082
|
+
return meterProvider;
|
|
4083
|
+
}
|
|
4084
|
+
#registerQueueDepthGauge(meterProvider) {
|
|
4085
|
+
if (meterProvider === this.#queueDepthGaugeMeterProvider) return;
|
|
4086
|
+
registerQueueDepthGauge(meterProvider, this.#queueDepthGaugeEntries, { sourceId: this.#queueDepthGaugeSourceId });
|
|
4087
|
+
this.#queueDepthGaugeMeterProvider = meterProvider;
|
|
4088
|
+
}
|
|
2844
4089
|
_initializeRouter() {
|
|
2845
4090
|
this.router.add("/.well-known/webfinger", "webfinger");
|
|
2846
4091
|
this.router.add("/.well-known/nodeinfo", "nodeInfoJrd");
|
|
4092
|
+
if (this.benchmarkMode) {
|
|
4093
|
+
this.router.add("/.well-known/fedify/bench/stats", "benchmarkStats");
|
|
4094
|
+
this.router.add("/.well-known/fedify/bench/trigger", "benchmarkTrigger");
|
|
4095
|
+
}
|
|
2847
4096
|
}
|
|
2848
4097
|
_getTracer() {
|
|
2849
4098
|
return this.tracerProvider.getTracer(name, version);
|
|
@@ -2875,79 +4124,124 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2875
4124
|
}
|
|
2876
4125
|
processQueuedTask(contextData, message) {
|
|
2877
4126
|
const tracer = this._getTracer();
|
|
2878
|
-
const extractedContext = propagation.extract(context.active(), message.traceContext);
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
4127
|
+
const extractedContext = propagation.extract(context.active(), message.traceContext);
|
|
4128
|
+
const meter = getFederationMetrics(this.meterProvider);
|
|
4129
|
+
return withContext({ messageId: message.id }, async () => {
|
|
4130
|
+
if (message.type === "fanout") {
|
|
4131
|
+
const common = {
|
|
4132
|
+
role: "fanout",
|
|
4133
|
+
queue: this.fanoutQueue,
|
|
4134
|
+
activityType: message.activityType
|
|
4135
|
+
};
|
|
4136
|
+
await tracer.startActiveSpan("activitypub.fanout", {
|
|
4137
|
+
kind: SpanKind.CONSUMER,
|
|
4138
|
+
attributes: { "activitypub.activity.type": message.activityType }
|
|
4139
|
+
}, extractedContext, async (span) => {
|
|
4140
|
+
const spanCtx = span.spanContext();
|
|
4141
|
+
return await withContext({
|
|
4142
|
+
traceId: spanCtx.traceId,
|
|
4143
|
+
spanId: spanCtx.spanId
|
|
4144
|
+
}, async () => {
|
|
4145
|
+
if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
|
|
4146
|
+
meter.recordQueueTaskStarted(common);
|
|
4147
|
+
meter.incrementQueueTaskInFlight(common);
|
|
4148
|
+
const startedAt = performance.now();
|
|
4149
|
+
let outcome = "completed";
|
|
4150
|
+
try {
|
|
4151
|
+
await this.#listenFanoutMessage(contextData, message);
|
|
4152
|
+
} catch (e) {
|
|
4153
|
+
const aborted = isAbortError(e);
|
|
4154
|
+
outcome = aborted ? "aborted" : "failed";
|
|
4155
|
+
if (!aborted) span.setStatus({
|
|
4156
|
+
code: SpanStatusCode.ERROR,
|
|
4157
|
+
message: String(e)
|
|
4158
|
+
});
|
|
4159
|
+
throw e;
|
|
4160
|
+
} finally {
|
|
4161
|
+
meter.recordQueueTaskOutcome(common, outcome, getDurationMs(startedAt));
|
|
4162
|
+
meter.decrementQueueTaskInFlight(common);
|
|
4163
|
+
span.end();
|
|
4164
|
+
}
|
|
4165
|
+
});
|
|
2901
4166
|
});
|
|
2902
|
-
})
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
spanId: spanCtx.spanId
|
|
2914
|
-
}, async () => {
|
|
2915
|
-
if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
|
|
2916
|
-
try {
|
|
2917
|
-
await this.#listenOutboxMessage(contextData, message, span);
|
|
2918
|
-
} catch (e) {
|
|
2919
|
-
span.setStatus({
|
|
2920
|
-
code: SpanStatusCode.ERROR,
|
|
2921
|
-
message: String(e)
|
|
2922
|
-
});
|
|
2923
|
-
throw e;
|
|
2924
|
-
} finally {
|
|
2925
|
-
span.end();
|
|
4167
|
+
} else if (message.type === "outbox") {
|
|
4168
|
+
const common = {
|
|
4169
|
+
role: "outbox",
|
|
4170
|
+
queue: this.outboxQueue,
|
|
4171
|
+
activityType: message.activityType
|
|
4172
|
+
};
|
|
4173
|
+
await tracer.startActiveSpan("activitypub.outbox", {
|
|
4174
|
+
kind: SpanKind.CONSUMER,
|
|
4175
|
+
attributes: {
|
|
4176
|
+
"activitypub.activity.type": message.activityType,
|
|
4177
|
+
"activitypub.activity.retries": message.attempt
|
|
2926
4178
|
}
|
|
4179
|
+
}, extractedContext, async (span) => {
|
|
4180
|
+
const spanCtx = span.spanContext();
|
|
4181
|
+
return await withContext({
|
|
4182
|
+
traceId: spanCtx.traceId,
|
|
4183
|
+
spanId: spanCtx.spanId
|
|
4184
|
+
}, async () => {
|
|
4185
|
+
if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
|
|
4186
|
+
meter.recordQueueTaskStarted(common);
|
|
4187
|
+
meter.incrementQueueTaskInFlight(common);
|
|
4188
|
+
const startedAt = performance.now();
|
|
4189
|
+
let outcome = "completed";
|
|
4190
|
+
try {
|
|
4191
|
+
await this.#listenOutboxMessage(contextData, message, span);
|
|
4192
|
+
} catch (e) {
|
|
4193
|
+
const aborted = isAbortError(e);
|
|
4194
|
+
outcome = aborted ? "aborted" : "failed";
|
|
4195
|
+
if (!aborted) span.setStatus({
|
|
4196
|
+
code: SpanStatusCode.ERROR,
|
|
4197
|
+
message: String(e)
|
|
4198
|
+
});
|
|
4199
|
+
throw e;
|
|
4200
|
+
} finally {
|
|
4201
|
+
meter.recordQueueTaskOutcome(common, outcome, getDurationMs(startedAt));
|
|
4202
|
+
meter.decrementQueueTaskInFlight(common);
|
|
4203
|
+
span.end();
|
|
4204
|
+
}
|
|
4205
|
+
});
|
|
2927
4206
|
});
|
|
2928
|
-
})
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
4207
|
+
} else if (message.type === "inbox") {
|
|
4208
|
+
const common = {
|
|
4209
|
+
role: "inbox",
|
|
4210
|
+
queue: this.inboxQueue
|
|
4211
|
+
};
|
|
4212
|
+
await tracer.startActiveSpan("activitypub.inbox", {
|
|
4213
|
+
kind: SpanKind.CONSUMER,
|
|
4214
|
+
attributes: { "activitypub.shared_inbox": message.identifier == null }
|
|
4215
|
+
}, extractedContext, async (span) => {
|
|
4216
|
+
const spanCtx = span.spanContext();
|
|
4217
|
+
return await withContext({
|
|
4218
|
+
traceId: spanCtx.traceId,
|
|
4219
|
+
spanId: spanCtx.spanId
|
|
4220
|
+
}, async () => {
|
|
4221
|
+
meter.recordQueueTaskStarted(common);
|
|
4222
|
+
meter.incrementQueueTaskInFlight(common);
|
|
4223
|
+
const startedAt = performance.now();
|
|
4224
|
+
let outcome = "completed";
|
|
4225
|
+
try {
|
|
4226
|
+
await this.#listenInboxMessage(contextData, message, span, (activityType) => {
|
|
4227
|
+
common.activityType = activityType;
|
|
4228
|
+
});
|
|
4229
|
+
} catch (e) {
|
|
4230
|
+
const aborted = isAbortError(e);
|
|
4231
|
+
outcome = aborted ? "aborted" : "failed";
|
|
4232
|
+
if (!aborted) span.setStatus({
|
|
4233
|
+
code: SpanStatusCode.ERROR,
|
|
4234
|
+
message: String(e)
|
|
4235
|
+
});
|
|
4236
|
+
throw e;
|
|
4237
|
+
} finally {
|
|
4238
|
+
meter.recordQueueTaskOutcome(common, outcome, getDurationMs(startedAt));
|
|
4239
|
+
meter.decrementQueueTaskInFlight(common);
|
|
4240
|
+
span.end();
|
|
4241
|
+
}
|
|
4242
|
+
});
|
|
2949
4243
|
});
|
|
2950
|
-
}
|
|
4244
|
+
}
|
|
2951
4245
|
});
|
|
2952
4246
|
}
|
|
2953
4247
|
async #listenFanoutMessage(data, message) {
|
|
@@ -3009,29 +4303,202 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3009
4303
|
if (rsaKeyPair == null && pair.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") rsaKeyPair = pair;
|
|
3010
4304
|
keys.push(pair);
|
|
3011
4305
|
}
|
|
4306
|
+
const loaderOptions = this.#getLoaderOptions(message.baseUrl);
|
|
4307
|
+
let parsedActorIds;
|
|
4308
|
+
const getActorIds = () => {
|
|
4309
|
+
parsedActorIds ??= (message.actorIds ?? []).flatMap((id) => {
|
|
4310
|
+
try {
|
|
4311
|
+
return [new URL(id)];
|
|
4312
|
+
} catch {
|
|
4313
|
+
logger.warn("Invalid actorId URL in OutboxMessage: {id}", { id });
|
|
4314
|
+
return [];
|
|
4315
|
+
}
|
|
4316
|
+
});
|
|
4317
|
+
return parsedActorIds;
|
|
4318
|
+
};
|
|
4319
|
+
const parseActivity = () => Activity.fromJsonLd(message.activity, {
|
|
4320
|
+
contextLoader: this.contextLoaderFactory(loaderOptions),
|
|
4321
|
+
documentLoader: rsaKeyPair == null ? this.documentLoaderFactory(loaderOptions) : this.authenticatedDocumentLoaderFactory(rsaKeyPair, loaderOptions),
|
|
4322
|
+
tracerProvider: this.tracerProvider
|
|
4323
|
+
});
|
|
4324
|
+
const enqueueHeldOutboxMessage = async (delay, heldSince) => {
|
|
4325
|
+
const { outboxQueue } = this;
|
|
4326
|
+
if (outboxQueue == null) return;
|
|
4327
|
+
const heldMessage = {
|
|
4328
|
+
...message,
|
|
4329
|
+
circuitHeld: true,
|
|
4330
|
+
circuitHeldSince: heldSince.toString()
|
|
4331
|
+
};
|
|
4332
|
+
await outboxQueue.enqueue(heldMessage, {
|
|
4333
|
+
delay: clampNegativeDelay(delay),
|
|
4334
|
+
orderingKey: message.orderingKey
|
|
4335
|
+
});
|
|
4336
|
+
getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
|
|
4337
|
+
role: "outbox",
|
|
4338
|
+
queue: outboxQueue,
|
|
4339
|
+
activityType: heldMessage.activityType
|
|
4340
|
+
}, heldMessage.attempt);
|
|
4341
|
+
};
|
|
4342
|
+
const dropHeldOutboxMessage = async (circuit, remoteHost, inbox, heldSince, activity) => {
|
|
4343
|
+
await circuit.dropActivity(remoteHost, {
|
|
4344
|
+
inbox,
|
|
4345
|
+
activity,
|
|
4346
|
+
activityId: message.activityId,
|
|
4347
|
+
activityType: message.activityType,
|
|
4348
|
+
actorIds: getActorIds(),
|
|
4349
|
+
heldSince
|
|
4350
|
+
});
|
|
4351
|
+
if (this.outboxPermanentFailureHandler != null) {
|
|
4352
|
+
const ctx = this.#createContext(new URL(message.baseUrl), _, { documentLoader: this.documentLoaderFactory(loaderOptions) });
|
|
4353
|
+
try {
|
|
4354
|
+
await this.outboxPermanentFailureHandler(ctx, {
|
|
4355
|
+
reason: "circuit-breaker-ttl",
|
|
4356
|
+
inbox,
|
|
4357
|
+
activity,
|
|
4358
|
+
error: new SendActivityError(inbox, 0, "Circuit breaker held activity expired.", ""),
|
|
4359
|
+
statusCode: 0,
|
|
4360
|
+
circuitHeldSince: heldSince,
|
|
4361
|
+
actorIds: getActorIds()
|
|
4362
|
+
});
|
|
4363
|
+
} catch (handlerError) {
|
|
4364
|
+
logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
|
|
4365
|
+
...logData,
|
|
4366
|
+
error: handlerError
|
|
4367
|
+
});
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
4370
|
+
recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
4371
|
+
};
|
|
3012
4372
|
try {
|
|
4373
|
+
const inbox = new URL(message.inbox);
|
|
4374
|
+
const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
|
|
4375
|
+
const remoteHost = getRemoteHost(inbox);
|
|
4376
|
+
let decision;
|
|
4377
|
+
if (circuit != null) try {
|
|
4378
|
+
decision = await circuit.beforeSend(remoteHost, message);
|
|
4379
|
+
} catch (circuitError) {
|
|
4380
|
+
getLogger([
|
|
4381
|
+
"fedify",
|
|
4382
|
+
"federation",
|
|
4383
|
+
"circuit"
|
|
4384
|
+
]).error("Failed to check circuit breaker state before sending; proceeding with delivery:\n{error}", {
|
|
4385
|
+
...logData,
|
|
4386
|
+
remoteHost,
|
|
4387
|
+
error: circuitError
|
|
4388
|
+
});
|
|
4389
|
+
}
|
|
4390
|
+
if (decision != null && circuit != null) {
|
|
4391
|
+
if (decision.type === "hold") {
|
|
4392
|
+
recordCircuitBreakerHeldSpanEvent(span, remoteHost, decision.state);
|
|
4393
|
+
await enqueueHeldOutboxMessage(decision.delay, decision.heldSince);
|
|
4394
|
+
return;
|
|
4395
|
+
}
|
|
4396
|
+
if (decision.type === "drop") {
|
|
4397
|
+
const activity = await parseActivity();
|
|
4398
|
+
await dropHeldOutboxMessage(circuit, remoteHost, inbox, decision.heldSince, activity);
|
|
4399
|
+
return;
|
|
4400
|
+
}
|
|
4401
|
+
if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
|
|
4402
|
+
}
|
|
3013
4403
|
await sendActivity({
|
|
3014
4404
|
keys,
|
|
3015
4405
|
activity: message.activity,
|
|
3016
4406
|
activityId: message.activityId,
|
|
3017
4407
|
activityType: message.activityType,
|
|
3018
|
-
inbox
|
|
4408
|
+
inbox,
|
|
3019
4409
|
sharedInbox: message.sharedInbox,
|
|
3020
4410
|
headers: new Headers(message.headers),
|
|
3021
4411
|
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock),
|
|
4412
|
+
meterProvider: this.meterProvider,
|
|
3022
4413
|
tracerProvider: this.tracerProvider
|
|
3023
4414
|
});
|
|
4415
|
+
if (circuit != null) try {
|
|
4416
|
+
const stateChange = await circuit.recordSuccess(remoteHost);
|
|
4417
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
4418
|
+
} catch (error) {
|
|
4419
|
+
getLogger([
|
|
4420
|
+
"fedify",
|
|
4421
|
+
"federation",
|
|
4422
|
+
"circuit"
|
|
4423
|
+
]).error("Failed to record successful delivery in circuit breaker state; the activity was already delivered:\n{error}", {
|
|
4424
|
+
...logData,
|
|
4425
|
+
remoteHost,
|
|
4426
|
+
error
|
|
4427
|
+
});
|
|
4428
|
+
}
|
|
3024
4429
|
} catch (error) {
|
|
3025
4430
|
span.setStatus({
|
|
3026
4431
|
code: SpanStatusCode.ERROR,
|
|
3027
4432
|
message: String(error)
|
|
3028
4433
|
});
|
|
3029
|
-
const
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
4434
|
+
const remoteHost = (() => {
|
|
4435
|
+
if (error instanceof SendActivityError) return getRemoteHost(error.inbox);
|
|
4436
|
+
try {
|
|
4437
|
+
return getRemoteHost(new URL(message.inbox));
|
|
4438
|
+
} catch (_) {
|
|
4439
|
+
logger.warn("Invalid inbox URL in queued outbox message: {inbox}", logData);
|
|
4440
|
+
return;
|
|
4441
|
+
}
|
|
4442
|
+
})();
|
|
4443
|
+
let retryAfterDelay;
|
|
4444
|
+
let circuitHold;
|
|
4445
|
+
let circuitDrop;
|
|
4446
|
+
let retryPolicyDelay;
|
|
4447
|
+
let policyDelayCalculated = false;
|
|
4448
|
+
const getPolicyDelay = () => {
|
|
4449
|
+
if (!policyDelayCalculated) {
|
|
4450
|
+
retryPolicyDelay = this.outboxRetryPolicy({
|
|
4451
|
+
elapsedTime: Temporal.Instant.from(message.started).until(Temporal.Now.instant()),
|
|
4452
|
+
attempts: message.attempt
|
|
4453
|
+
});
|
|
4454
|
+
policyDelayCalculated = true;
|
|
4455
|
+
}
|
|
4456
|
+
return retryPolicyDelay;
|
|
4457
|
+
};
|
|
4458
|
+
const isPermanentFailure = error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode);
|
|
4459
|
+
if (!isPermanentFailure && error instanceof SendActivityError && (error.statusCode === 429 || error.statusCode === 503)) retryAfterDelay = parseRetryAfter(error.responseHeaders);
|
|
4460
|
+
if (remoteHost != null && this.outboxQueue != null && this.circuitBreaker != null) try {
|
|
4461
|
+
if (error instanceof SendActivityError) {
|
|
4462
|
+
const { statusCode } = error;
|
|
4463
|
+
const stateChange = isPermanentFailure || statusCode === 429 || statusCode >= 400 && statusCode < 500 ? await this.circuitBreaker.recordReachableFailure(remoteHost) : statusCode >= 500 ? await this.circuitBreaker.recordFailure(remoteHost) : void 0;
|
|
4464
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
4465
|
+
} else if (isTransportDeliveryError(error)) {
|
|
4466
|
+
const stateChange = await this.circuitBreaker.recordFailure(remoteHost);
|
|
4467
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
4468
|
+
}
|
|
4469
|
+
if (!isPermanentFailure) {
|
|
4470
|
+
const circuitDecision = await this.circuitBreaker.beforeSend(remoteHost, message);
|
|
4471
|
+
if (circuitDecision.type === "hold") circuitHold = {
|
|
4472
|
+
delay: circuitDecision.delay,
|
|
4473
|
+
heldSince: circuitDecision.heldSince,
|
|
4474
|
+
remoteHost,
|
|
4475
|
+
state: circuitDecision.state
|
|
4476
|
+
};
|
|
4477
|
+
else if (circuitDecision.type === "drop") circuitDrop = {
|
|
4478
|
+
circuit: this.circuitBreaker,
|
|
4479
|
+
remoteHost,
|
|
4480
|
+
inbox: new URL(message.inbox),
|
|
4481
|
+
heldSince: circuitDecision.heldSince
|
|
4482
|
+
};
|
|
4483
|
+
}
|
|
4484
|
+
} catch (circuitError) {
|
|
4485
|
+
getLogger([
|
|
4486
|
+
"fedify",
|
|
4487
|
+
"federation",
|
|
4488
|
+
"circuit"
|
|
4489
|
+
]).error("Failed to update circuit breaker state after delivery failure; falling back to normal failure handling:\n{error}", {
|
|
4490
|
+
...logData,
|
|
4491
|
+
remoteHost,
|
|
4492
|
+
error: circuitError
|
|
4493
|
+
});
|
|
4494
|
+
}
|
|
4495
|
+
span.addEvent("activitypub.delivery.failed", {
|
|
4496
|
+
...remoteHost == null ? {} : { "activitypub.remote.host": remoteHost },
|
|
4497
|
+
"activitypub.delivery.attempt": message.attempt,
|
|
4498
|
+
"activitypub.delivery.permanent_failure": isPermanentFailure,
|
|
4499
|
+
...error instanceof SendActivityError ? { "http.response.status_code": error.statusCode } : {}
|
|
3034
4500
|
});
|
|
4501
|
+
const activity = await parseActivity();
|
|
3035
4502
|
try {
|
|
3036
4503
|
await this.onOutboxError?.(error, activity);
|
|
3037
4504
|
} catch (error) {
|
|
@@ -3040,7 +4507,12 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3040
4507
|
error
|
|
3041
4508
|
});
|
|
3042
4509
|
}
|
|
3043
|
-
if (
|
|
4510
|
+
if (circuitDrop != null) {
|
|
4511
|
+
await dropHeldOutboxMessage(circuitDrop.circuit, circuitDrop.remoteHost, circuitDrop.inbox, circuitDrop.heldSince, activity);
|
|
4512
|
+
return;
|
|
4513
|
+
}
|
|
4514
|
+
if (isPermanentFailure) {
|
|
4515
|
+
getFederationMetrics(this.meterProvider).recordPermanentFailure(error.inbox, error.statusCode);
|
|
3044
4516
|
logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not retrying.", {
|
|
3045
4517
|
...logData,
|
|
3046
4518
|
status: error.statusCode
|
|
@@ -3049,18 +4521,12 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3049
4521
|
const ctx = this.#createContext(new URL(message.baseUrl), _, { documentLoader: this.documentLoaderFactory(loaderOptions) });
|
|
3050
4522
|
try {
|
|
3051
4523
|
await this.outboxPermanentFailureHandler(ctx, {
|
|
4524
|
+
reason: "http",
|
|
3052
4525
|
inbox: new URL(message.inbox),
|
|
3053
4526
|
activity,
|
|
3054
4527
|
error,
|
|
3055
4528
|
statusCode: error.statusCode,
|
|
3056
|
-
actorIds: (
|
|
3057
|
-
try {
|
|
3058
|
-
return [new URL(id)];
|
|
3059
|
-
} catch {
|
|
3060
|
-
logger.warn("Invalid actorId URL in OutboxMessage: {id}", { id });
|
|
3061
|
-
return [];
|
|
3062
|
-
}
|
|
3063
|
-
})
|
|
4529
|
+
actorIds: getActorIds()
|
|
3064
4530
|
});
|
|
3065
4531
|
} catch (handlerError) {
|
|
3066
4532
|
logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
|
|
@@ -3069,37 +4535,62 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3069
4535
|
});
|
|
3070
4536
|
}
|
|
3071
4537
|
}
|
|
4538
|
+
recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
3072
4539
|
return;
|
|
3073
4540
|
}
|
|
3074
|
-
if (
|
|
4541
|
+
if (circuitHold != null && getPolicyDelay() != null) {
|
|
4542
|
+
logger.error("Failed to send activity {activityId} to {inbox}; holding because the remote host circuit is open:\n{error}", {
|
|
4543
|
+
...logData,
|
|
4544
|
+
error
|
|
4545
|
+
});
|
|
4546
|
+
recordCircuitBreakerHeldSpanEvent(span, circuitHold.remoteHost, circuitHold.state);
|
|
4547
|
+
const circuit = this.circuitBreaker;
|
|
4548
|
+
await enqueueHeldOutboxMessage(retryAfterDelay == null || circuit == null ? circuitHold.delay : circuit.capHeldDelay(circuitHold.heldSince, maxDelay(circuitHold.delay, retryAfterDelay)), circuitHold.heldSince);
|
|
4549
|
+
return;
|
|
4550
|
+
}
|
|
4551
|
+
if (this.outboxQueue?.nativeRetrial && retryAfterDelay == null) {
|
|
3075
4552
|
logger.error("Failed to send activity {activityId} to {inbox}; backend will handle retry:\n{error}", {
|
|
3076
4553
|
...logData,
|
|
3077
4554
|
error
|
|
3078
4555
|
});
|
|
3079
4556
|
throw error;
|
|
3080
4557
|
}
|
|
3081
|
-
const
|
|
3082
|
-
|
|
3083
|
-
attempts: message.attempt
|
|
3084
|
-
});
|
|
4558
|
+
const policyDelay = getPolicyDelay();
|
|
4559
|
+
const delay = policyDelay == null ? null : retryAfterDelay ?? policyDelay;
|
|
3085
4560
|
if (delay != null) {
|
|
3086
4561
|
logger.error("Failed to send activity {activityId} to {inbox} (attempt #{attempt}); retry...:\n{error}", {
|
|
3087
4562
|
...logData,
|
|
3088
4563
|
error
|
|
3089
4564
|
});
|
|
3090
|
-
|
|
4565
|
+
const retryMessage = {
|
|
3091
4566
|
...message,
|
|
3092
4567
|
attempt: message.attempt + 1
|
|
3093
|
-
}
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
4568
|
+
};
|
|
4569
|
+
const { outboxQueue } = this;
|
|
4570
|
+
if (outboxQueue != null) {
|
|
4571
|
+
await outboxQueue.enqueue(retryMessage, {
|
|
4572
|
+
delay: clampNegativeDelay(delay),
|
|
4573
|
+
orderingKey: message.orderingKey
|
|
4574
|
+
});
|
|
4575
|
+
getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
|
|
4576
|
+
role: "outbox",
|
|
4577
|
+
queue: outboxQueue,
|
|
4578
|
+
activityType: retryMessage.activityType
|
|
4579
|
+
}, retryMessage.attempt);
|
|
4580
|
+
recordOutboxActivity(this.meterProvider, "retried", retryMessage.activityType);
|
|
4581
|
+
}
|
|
4582
|
+
} else {
|
|
4583
|
+
logger.error("Failed to send activity {activityId} to {inbox} after {attempt} attempts; giving up:\n{error}", {
|
|
4584
|
+
...logData,
|
|
4585
|
+
error
|
|
4586
|
+
});
|
|
4587
|
+
recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
4588
|
+
}
|
|
3098
4589
|
return;
|
|
3099
4590
|
}
|
|
3100
4591
|
logger.info("Successfully sent activity {activityId} to {inbox}.", { ...logData });
|
|
3101
4592
|
}
|
|
3102
|
-
async #listenInboxMessage(ctxData, message, span) {
|
|
4593
|
+
async #listenInboxMessage(ctxData, message, span, onActivityType) {
|
|
3103
4594
|
const logger = getLogger([
|
|
3104
4595
|
"fedify",
|
|
3105
4596
|
"federation",
|
|
@@ -3112,68 +4603,37 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3112
4603
|
const identity = await this.sharedInboxKeyDispatcher(context);
|
|
3113
4604
|
if (identity != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
|
|
3114
4605
|
}
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
context.origin,
|
|
3121
|
-
activity.id.href
|
|
3122
|
-
];
|
|
3123
|
-
if (cacheKey != null) {
|
|
3124
|
-
if (await this.kv.get(cacheKey) === true) {
|
|
3125
|
-
logger.debug("Activity {activityId} has already been processed.", {
|
|
3126
|
-
activityId: activity.id?.href,
|
|
3127
|
-
activity: message.activity,
|
|
3128
|
-
recipient: message.identifier
|
|
3129
|
-
});
|
|
3130
|
-
return;
|
|
3131
|
-
}
|
|
3132
|
-
}
|
|
3133
|
-
await this._getTracer().startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (span) => {
|
|
3134
|
-
const dispatched = this.inboxListeners?.dispatchWithClass(activity);
|
|
3135
|
-
if (dispatched == null) {
|
|
3136
|
-
logger.error("Unsupported activity type:\n{activity}", {
|
|
3137
|
-
activityId: activity.id?.href,
|
|
3138
|
-
activity: message.activity,
|
|
3139
|
-
recipient: message.identifier,
|
|
3140
|
-
trial: message.attempt
|
|
3141
|
-
});
|
|
3142
|
-
span.setStatus({
|
|
3143
|
-
code: SpanStatusCode.ERROR,
|
|
3144
|
-
message: `Unsupported activity type: ${getTypeId(activity).href}`
|
|
3145
|
-
});
|
|
3146
|
-
span.end();
|
|
3147
|
-
return;
|
|
3148
|
-
}
|
|
3149
|
-
const { class: cls, listener } = dispatched;
|
|
3150
|
-
span.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
|
|
3151
|
-
try {
|
|
3152
|
-
await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, getTypeId(activity).href), activity);
|
|
3153
|
-
} catch (error) {
|
|
4606
|
+
await this._getTracer().startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (listenerSpan) => {
|
|
4607
|
+
let activity = null;
|
|
4608
|
+
let cacheKey = null;
|
|
4609
|
+
let activityType;
|
|
4610
|
+
const reportInboxError = async (error) => {
|
|
3154
4611
|
try {
|
|
3155
4612
|
await this.inboxErrorHandler?.(context, error);
|
|
3156
4613
|
} catch (error) {
|
|
3157
4614
|
logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
|
|
3158
4615
|
error,
|
|
3159
4616
|
trial: message.attempt,
|
|
3160
|
-
activityId: activity
|
|
4617
|
+
activityId: activity?.id?.href,
|
|
3161
4618
|
activity: message.activity,
|
|
3162
4619
|
recipient: message.identifier
|
|
3163
4620
|
});
|
|
3164
4621
|
}
|
|
4622
|
+
};
|
|
4623
|
+
const handleRetriableFailure = async (error) => {
|
|
4624
|
+
await reportInboxError(error);
|
|
3165
4625
|
if (this.inboxQueue?.nativeRetrial) {
|
|
3166
4626
|
logger.error("Failed to process the incoming activity {activityId}; backend will handle retry:\n{error}", {
|
|
3167
4627
|
error,
|
|
3168
|
-
activityId: activity
|
|
4628
|
+
activityId: activity?.id?.href,
|
|
3169
4629
|
activity: message.activity,
|
|
3170
4630
|
recipient: message.identifier
|
|
3171
4631
|
});
|
|
3172
|
-
|
|
4632
|
+
listenerSpan.setStatus({
|
|
3173
4633
|
code: SpanStatusCode.ERROR,
|
|
3174
4634
|
message: String(error)
|
|
3175
4635
|
});
|
|
3176
|
-
|
|
4636
|
+
listenerSpan.end();
|
|
3177
4637
|
throw error;
|
|
3178
4638
|
}
|
|
3179
4639
|
const delay = this.inboxRetryPolicy({
|
|
@@ -3184,34 +4644,165 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3184
4644
|
logger.error("Failed to process the incoming activity {activityId} (attempt #{attempt}); retry...:\n{error}", {
|
|
3185
4645
|
error,
|
|
3186
4646
|
attempt: message.attempt,
|
|
3187
|
-
activityId: activity
|
|
4647
|
+
activityId: activity?.id?.href,
|
|
3188
4648
|
activity: message.activity,
|
|
3189
4649
|
recipient: message.identifier
|
|
3190
4650
|
});
|
|
3191
|
-
|
|
4651
|
+
if (this.inboxQueue == null) {
|
|
4652
|
+
listenerSpan.setStatus({
|
|
4653
|
+
code: SpanStatusCode.ERROR,
|
|
4654
|
+
message: String(error)
|
|
4655
|
+
});
|
|
4656
|
+
listenerSpan.end();
|
|
4657
|
+
throw error;
|
|
4658
|
+
}
|
|
4659
|
+
const retryMessage = {
|
|
3192
4660
|
...message,
|
|
3193
4661
|
attempt: message.attempt + 1
|
|
3194
|
-
}
|
|
3195
|
-
|
|
3196
|
-
|
|
4662
|
+
};
|
|
4663
|
+
await this.inboxQueue.enqueue(retryMessage, { delay: clampNegativeDelay(delay) });
|
|
4664
|
+
if (activityType != null) {
|
|
4665
|
+
getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
|
|
4666
|
+
role: "inbox",
|
|
4667
|
+
queue: this.inboxQueue,
|
|
4668
|
+
activityType
|
|
4669
|
+
}, retryMessage.attempt);
|
|
4670
|
+
recordInboxActivity(this.meterProvider, "retried", activityType);
|
|
4671
|
+
}
|
|
4672
|
+
} else {
|
|
4673
|
+
logger.error("Failed to process the incoming activity {activityId} after {trial} attempts; giving up:\n{error}", {
|
|
4674
|
+
error,
|
|
4675
|
+
activityId: activity?.id?.href,
|
|
4676
|
+
activity: message.activity,
|
|
4677
|
+
recipient: message.identifier
|
|
4678
|
+
});
|
|
4679
|
+
if (activityType != null) recordInboxActivity(this.meterProvider, "abandoned", activityType);
|
|
4680
|
+
}
|
|
4681
|
+
listenerSpan.setStatus({
|
|
4682
|
+
code: SpanStatusCode.ERROR,
|
|
4683
|
+
message: String(error)
|
|
4684
|
+
});
|
|
4685
|
+
listenerSpan.end();
|
|
4686
|
+
};
|
|
4687
|
+
let dispatched;
|
|
4688
|
+
let parseInput = void 0;
|
|
4689
|
+
let parseContextLoader = context.contextLoader;
|
|
4690
|
+
try {
|
|
4691
|
+
const hasSignatureField = hasSignature(message.activity);
|
|
4692
|
+
const shouldParseFromNormalizedSignedPayload = message.ldSignatureVerified === true || message.normalizedActivity != null || message.ldSignatureVerified == null && hasSignatureField;
|
|
4693
|
+
const parseContext = hasSignatureField ? {
|
|
4694
|
+
...context,
|
|
4695
|
+
contextLoader: getNormalizationContextLoader(context.contextLoader)
|
|
4696
|
+
} : {
|
|
4697
|
+
...context,
|
|
4698
|
+
contextLoader: wrapContextLoaderForJsonLd(context.contextLoader)
|
|
4699
|
+
};
|
|
4700
|
+
parseContextLoader = parseContext.contextLoader;
|
|
4701
|
+
let normalizedActivity;
|
|
4702
|
+
if (shouldParseFromNormalizedSignedPayload) {
|
|
4703
|
+
normalizedActivity = message.normalizedActivity ?? await compactJsonLd(message.activity, context.contextLoader);
|
|
4704
|
+
assertSafeJsonLd(normalizedActivity);
|
|
4705
|
+
}
|
|
4706
|
+
parseInput = shouldParseFromNormalizedSignedPayload ? detachSignature(normalizedActivity) : hasSignatureField ? detachSignature(message.activity) : message.activity;
|
|
4707
|
+
activity = await Activity.fromJsonLd(parseInput, parseContext);
|
|
4708
|
+
activityType = getTypeId(activity).href;
|
|
4709
|
+
span.setAttribute("activitypub.activity.type", activityType);
|
|
4710
|
+
listenerSpan.setAttribute("activitypub.activity.type", activityType);
|
|
4711
|
+
onActivityType?.(activityType);
|
|
4712
|
+
if (activity.id != null) {
|
|
4713
|
+
span.setAttribute("activitypub.activity.id", activity.id.href);
|
|
4714
|
+
listenerSpan.setAttribute("activitypub.activity.id", activity.id.href);
|
|
4715
|
+
}
|
|
4716
|
+
cacheKey = activity.id == null ? null : [
|
|
4717
|
+
...this.kvPrefixes.activityIdempotence,
|
|
4718
|
+
context.origin,
|
|
4719
|
+
activity.id.href
|
|
4720
|
+
];
|
|
4721
|
+
if (cacheKey != null) {
|
|
4722
|
+
if (await this.kv.get(cacheKey) === true) {
|
|
4723
|
+
logger.debug("Activity {activityId} has already been processed.", {
|
|
4724
|
+
activityId: activity.id?.href,
|
|
4725
|
+
activity: message.activity,
|
|
4726
|
+
recipient: message.identifier
|
|
4727
|
+
});
|
|
4728
|
+
recordInboxActivity(this.meterProvider, "rejected", activityType);
|
|
4729
|
+
listenerSpan.end();
|
|
4730
|
+
return;
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
dispatched = this.inboxListeners?.dispatchWithClass(activity);
|
|
4734
|
+
} catch (error) {
|
|
4735
|
+
if (activity == null && error instanceof RangeError && await hasMalformedKnownTemporalLiteral(parseInput, parseContextLoader)) {
|
|
4736
|
+
await reportInboxError(error);
|
|
4737
|
+
logger.error("Failed to parse the queued incoming activity {activityId}:\n{error}", {
|
|
4738
|
+
error,
|
|
4739
|
+
trial: message.attempt,
|
|
4740
|
+
activityId: null,
|
|
4741
|
+
activity: message.activity,
|
|
4742
|
+
recipient: message.identifier
|
|
4743
|
+
});
|
|
4744
|
+
listenerSpan.setStatus({
|
|
4745
|
+
code: SpanStatusCode.ERROR,
|
|
4746
|
+
message: String(error)
|
|
4747
|
+
});
|
|
4748
|
+
listenerSpan.end();
|
|
4749
|
+
return;
|
|
4750
|
+
}
|
|
4751
|
+
if (isPermanentInboxParseError(error)) {
|
|
4752
|
+
await reportInboxError(error);
|
|
4753
|
+
logger.error("Failed to parse the queued incoming activity {activityId}:\n{error}", {
|
|
4754
|
+
error,
|
|
4755
|
+
trial: message.attempt,
|
|
4756
|
+
activityId: activity?.id?.href,
|
|
4757
|
+
activity: message.activity,
|
|
4758
|
+
recipient: message.identifier
|
|
4759
|
+
});
|
|
4760
|
+
listenerSpan.setStatus({
|
|
4761
|
+
code: SpanStatusCode.ERROR,
|
|
4762
|
+
message: String(error)
|
|
4763
|
+
});
|
|
4764
|
+
listenerSpan.end();
|
|
4765
|
+
return;
|
|
4766
|
+
}
|
|
4767
|
+
await handleRetriableFailure(error);
|
|
4768
|
+
return;
|
|
4769
|
+
}
|
|
4770
|
+
if (dispatched == null) {
|
|
4771
|
+
logger.error("Unsupported activity type:\n{activity}", {
|
|
3197
4772
|
activityId: activity.id?.href,
|
|
3198
4773
|
activity: message.activity,
|
|
3199
|
-
recipient: message.identifier
|
|
4774
|
+
recipient: message.identifier,
|
|
4775
|
+
trial: message.attempt
|
|
3200
4776
|
});
|
|
3201
|
-
|
|
4777
|
+
listenerSpan.setStatus({
|
|
3202
4778
|
code: SpanStatusCode.ERROR,
|
|
3203
|
-
message:
|
|
4779
|
+
message: `Unsupported activity type: ${activityType}`
|
|
3204
4780
|
});
|
|
3205
|
-
|
|
4781
|
+
recordInboxActivity(this.meterProvider, "rejected", activityType);
|
|
4782
|
+
listenerSpan.end();
|
|
4783
|
+
return;
|
|
4784
|
+
}
|
|
4785
|
+
const { class: cls, listener } = dispatched;
|
|
4786
|
+
listenerSpan.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
|
|
4787
|
+
try {
|
|
4788
|
+
const started = performance.now();
|
|
4789
|
+
try {
|
|
4790
|
+
await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, activityType), activity);
|
|
4791
|
+
} finally {
|
|
4792
|
+
getFederationMetrics(this.meterProvider).recordInboxProcessingDuration(activityType, getDurationMs(started));
|
|
4793
|
+
}
|
|
4794
|
+
recordInboxActivity(this.meterProvider, "processed", activityType);
|
|
4795
|
+
} catch (error) {
|
|
4796
|
+
await handleRetriableFailure(error);
|
|
3206
4797
|
return;
|
|
3207
4798
|
}
|
|
3208
4799
|
if (cacheKey != null) await this.kv.set(cacheKey, true, { ttl: Temporal.Duration.from({ days: 1 }) });
|
|
3209
4800
|
logger.info("Activity {activityId} has been processed.", {
|
|
3210
|
-
activityId: activity
|
|
4801
|
+
activityId: activity?.id?.href,
|
|
3211
4802
|
activity: message.activity,
|
|
3212
4803
|
recipient: message.identifier
|
|
3213
4804
|
});
|
|
3214
|
-
|
|
4805
|
+
listenerSpan.end();
|
|
3215
4806
|
});
|
|
3216
4807
|
}
|
|
3217
4808
|
startQueue(contextData, options = {}) {
|
|
@@ -3332,6 +4923,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3332
4923
|
sharedInbox: inboxes[inbox].sharedInbox,
|
|
3333
4924
|
headers: collectionSync == null ? void 0 : new Headers({ "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) }),
|
|
3334
4925
|
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock),
|
|
4926
|
+
meterProvider: this.meterProvider,
|
|
3335
4927
|
tracerProvider: this.tracerProvider
|
|
3336
4928
|
}));
|
|
3337
4929
|
await Promise.all(promises);
|
|
@@ -3379,9 +4971,11 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3379
4971
|
});
|
|
3380
4972
|
}
|
|
3381
4973
|
const { outboxQueue } = this;
|
|
3382
|
-
if (outboxQueue.enqueueMany == null) {
|
|
3383
|
-
const
|
|
3384
|
-
|
|
4974
|
+
if (outboxQueue.enqueueMany == null || orderingKey != null) {
|
|
4975
|
+
const errors = (await Promise.allSettled(messages.map(async (m) => {
|
|
4976
|
+
await outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey });
|
|
4977
|
+
recordOutboxEnqueue(this.meterProvider, outboxQueue, m.message);
|
|
4978
|
+
}))).filter((r) => r.status === "rejected").map((r) => r.reason);
|
|
3385
4979
|
if (errors.length > 0) {
|
|
3386
4980
|
logger.error("Failed to enqueue activity {activityId} to send later: {errors}", {
|
|
3387
4981
|
activityId: activity.id.href,
|
|
@@ -3390,30 +4984,24 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3390
4984
|
if (errors.length > 1) throw new AggregateError(errors, `Failed to enqueue activity ${activityId} to send later.`);
|
|
3391
4985
|
throw errors[0];
|
|
3392
4986
|
}
|
|
3393
|
-
} else
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
logger.error("Failed to enqueue activity {activityId} to send later: {
|
|
4987
|
+
} else {
|
|
4988
|
+
try {
|
|
4989
|
+
await outboxQueue.enqueueMany(messages.map((m) => m.message));
|
|
4990
|
+
} catch (error) {
|
|
4991
|
+
logger.error("Failed to enqueue activity {activityId} to send later: {error}", {
|
|
3398
4992
|
activityId: activity.id.href,
|
|
3399
|
-
|
|
4993
|
+
error
|
|
3400
4994
|
});
|
|
3401
|
-
|
|
3402
|
-
throw errors[0];
|
|
4995
|
+
throw error;
|
|
3403
4996
|
}
|
|
3404
|
-
|
|
3405
|
-
await outboxQueue.enqueueMany(messages.map((m) => m.message));
|
|
3406
|
-
} catch (error) {
|
|
3407
|
-
logger.error("Failed to enqueue activity {activityId} to send later: {error}", {
|
|
3408
|
-
activityId: activity.id.href,
|
|
3409
|
-
error
|
|
3410
|
-
});
|
|
3411
|
-
throw error;
|
|
4997
|
+
for (const m of messages) recordOutboxEnqueue(this.meterProvider, outboxQueue, m.message);
|
|
3412
4998
|
}
|
|
3413
4999
|
}
|
|
3414
5000
|
fetch(request, options) {
|
|
3415
5001
|
return withContext({ requestId: getRequestId(request) }, async () => {
|
|
3416
5002
|
const tracer = this._getTracer();
|
|
5003
|
+
const metricState = {};
|
|
5004
|
+
const metricStart = performance.now();
|
|
3417
5005
|
return await tracer.startActiveSpan(request.method, {
|
|
3418
5006
|
kind: SpanKind.SERVER,
|
|
3419
5007
|
attributes: {
|
|
@@ -3437,10 +5025,12 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3437
5025
|
response = await this.#fetch(request, {
|
|
3438
5026
|
...options,
|
|
3439
5027
|
span,
|
|
3440
|
-
tracer
|
|
5028
|
+
tracer,
|
|
5029
|
+
metricState
|
|
3441
5030
|
});
|
|
3442
5031
|
if (acceptsJsonLd(request)) response.headers.set("Vary", "Accept");
|
|
3443
5032
|
} catch (error) {
|
|
5033
|
+
getFederationMetrics(this.meterProvider).recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), { routeTemplate: metricState.routeTemplate });
|
|
3444
5034
|
span.setStatus({
|
|
3445
5035
|
code: SpanStatusCode.ERROR,
|
|
3446
5036
|
message: `${error}`
|
|
@@ -3453,6 +5043,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3453
5043
|
});
|
|
3454
5044
|
throw error;
|
|
3455
5045
|
}
|
|
5046
|
+
getFederationMetrics(this.meterProvider).recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), {
|
|
5047
|
+
statusCode: response.status,
|
|
5048
|
+
routeTemplate: metricState.routeTemplate
|
|
5049
|
+
});
|
|
3456
5050
|
if (span.isRecording()) {
|
|
3457
5051
|
span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
|
|
3458
5052
|
for (const [k, v] of response.headers) span.setAttribute(ATTR_HTTP_RESPONSE_HEADER(k), [v]);
|
|
@@ -3478,13 +5072,18 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3478
5072
|
});
|
|
3479
5073
|
});
|
|
3480
5074
|
}
|
|
3481
|
-
async #fetch(request, { onNotFound, onNotAcceptable, onUnauthorized, contextData, span, tracer }) {
|
|
5075
|
+
async #fetch(request, { onNotFound, onNotAcceptable, onUnauthorized, contextData, span, tracer, metricState }) {
|
|
3482
5076
|
onNotFound ??= notFound;
|
|
3483
5077
|
onNotAcceptable ??= notAcceptable;
|
|
3484
5078
|
onUnauthorized ??= unauthorized;
|
|
3485
5079
|
const url = new URL(request.url);
|
|
3486
5080
|
const route = this.router.route(url.pathname);
|
|
3487
|
-
if (route == null)
|
|
5081
|
+
if (route == null) {
|
|
5082
|
+
metricState.endpoint = "not_found";
|
|
5083
|
+
return await onNotFound(request);
|
|
5084
|
+
}
|
|
5085
|
+
metricState.routeTemplate = route.template;
|
|
5086
|
+
metricState.endpoint = getEndpointCategory(route.name);
|
|
3488
5087
|
span.updateName(`${request.method} ${route.template}`);
|
|
3489
5088
|
let context = this.#createContext(request, contextData);
|
|
3490
5089
|
const routeName = route.name.replace(/:.*$/, "");
|
|
@@ -3497,26 +5096,43 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3497
5096
|
actorAliasMapper: this.actorCallbacks?.aliasMapper,
|
|
3498
5097
|
webFingerLinksDispatcher: this.webFingerLinksDispatcher,
|
|
3499
5098
|
onNotFound,
|
|
3500
|
-
tracer
|
|
5099
|
+
tracer,
|
|
5100
|
+
meterProvider: this._meterProvider
|
|
3501
5101
|
});
|
|
3502
5102
|
case "nodeInfoJrd": return await handleNodeInfoJrd(request, context);
|
|
3503
5103
|
case "nodeInfo": return await handleNodeInfo(request, {
|
|
3504
5104
|
context,
|
|
3505
5105
|
nodeInfoDispatcher: this.nodeInfoDispatcher
|
|
3506
5106
|
});
|
|
5107
|
+
case "benchmarkStats": return await handleBenchmarkStats(request, this.benchmarkMetricReader);
|
|
5108
|
+
case "benchmarkTrigger": return await handleBenchmarkTrigger(request, context, this.benchmarkTriggerOptions);
|
|
5109
|
+
}
|
|
5110
|
+
if (request.method !== "POST" && !acceptsJsonLd(request)) {
|
|
5111
|
+
metricState.endpoint = "not_acceptable";
|
|
5112
|
+
const response = await onNotAcceptable(request);
|
|
5113
|
+
const collectionRoute = getCollectionMetricRoute(routeName);
|
|
5114
|
+
if (collectionRoute != null) recordCollectionRequest(this._meterProvider, {
|
|
5115
|
+
...collectionRoute,
|
|
5116
|
+
page: url.searchParams.get("cursor") != null,
|
|
5117
|
+
result: "not_acceptable",
|
|
5118
|
+
statusCode: response.status
|
|
5119
|
+
});
|
|
5120
|
+
return response;
|
|
3507
5121
|
}
|
|
3508
|
-
if (request.method !== "POST" && !acceptsJsonLd(request)) return await onNotAcceptable(request);
|
|
3509
5122
|
switch (routeName) {
|
|
3510
5123
|
case "actor":
|
|
3511
|
-
|
|
5124
|
+
case "actorAlias": {
|
|
5125
|
+
const identifier = route.name.startsWith("actorAlias:") ? route.name.substring(11) : route.values.identifier;
|
|
5126
|
+
context = this.#createContext(request, contextData, { invokedFromActorDispatcher: { identifier } });
|
|
3512
5127
|
return await handleActor(request, {
|
|
3513
|
-
identifier
|
|
5128
|
+
identifier,
|
|
3514
5129
|
context,
|
|
3515
5130
|
actorDispatcher: this.actorCallbacks?.dispatcher,
|
|
3516
5131
|
authorizePredicate: this.actorCallbacks?.authorizePredicate,
|
|
3517
5132
|
onUnauthorized,
|
|
3518
5133
|
onNotFound
|
|
3519
5134
|
});
|
|
5135
|
+
}
|
|
3520
5136
|
case "object": {
|
|
3521
5137
|
const typeId = route.name.replace(/^object:/, "");
|
|
3522
5138
|
const callbacks = this.objectCallbacks[typeId];
|
|
@@ -3562,6 +5178,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3562
5178
|
context,
|
|
3563
5179
|
collectionCallbacks: this.outboxCallbacks,
|
|
3564
5180
|
tracerProvider: this.tracerProvider,
|
|
5181
|
+
meterProvider: this._meterProvider,
|
|
3565
5182
|
onUnauthorized,
|
|
3566
5183
|
onNotFound
|
|
3567
5184
|
});
|
|
@@ -3573,20 +5190,23 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3573
5190
|
context,
|
|
3574
5191
|
collectionCallbacks: this.inboxCallbacks,
|
|
3575
5192
|
tracerProvider: this.tracerProvider,
|
|
5193
|
+
meterProvider: this._meterProvider,
|
|
3576
5194
|
onUnauthorized,
|
|
3577
5195
|
onNotFound
|
|
3578
5196
|
});
|
|
3579
5197
|
context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
|
|
3580
|
-
case "sharedInbox":
|
|
5198
|
+
case "sharedInbox": {
|
|
3581
5199
|
if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
|
|
3582
5200
|
const identity = await this.sharedInboxKeyDispatcher(context);
|
|
3583
5201
|
if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
|
|
3584
5202
|
}
|
|
3585
5203
|
if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
|
|
5204
|
+
const inboxContextFactory = context.toInboxContext.bind(context);
|
|
5205
|
+
inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
|
|
3586
5206
|
return await handleInbox(request, {
|
|
3587
5207
|
recipient: route.values.identifier ?? null,
|
|
3588
5208
|
context,
|
|
3589
|
-
inboxContextFactory
|
|
5209
|
+
inboxContextFactory,
|
|
3590
5210
|
kv: this.kv,
|
|
3591
5211
|
kvPrefixes: this.kvPrefixes,
|
|
3592
5212
|
queue: this.inboxQueue,
|
|
@@ -3598,9 +5218,11 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3598
5218
|
signatureTimeWindow: this.signatureTimeWindow,
|
|
3599
5219
|
skipSignatureVerification: this.skipSignatureVerification,
|
|
3600
5220
|
inboxChallengePolicy: this.inboxChallengePolicy,
|
|
5221
|
+
meterProvider: this.meterProvider,
|
|
3601
5222
|
tracerProvider: this.tracerProvider,
|
|
3602
5223
|
idempotencyStrategy: this.idempotencyStrategy
|
|
3603
5224
|
});
|
|
5225
|
+
}
|
|
3604
5226
|
case "following": return await handleCollection(request, {
|
|
3605
5227
|
name: "following",
|
|
3606
5228
|
identifier: route.values.identifier,
|
|
@@ -3608,6 +5230,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3608
5230
|
context,
|
|
3609
5231
|
collectionCallbacks: this.followingCallbacks,
|
|
3610
5232
|
tracerProvider: this.tracerProvider,
|
|
5233
|
+
meterProvider: this._meterProvider,
|
|
3611
5234
|
onUnauthorized,
|
|
3612
5235
|
onNotFound
|
|
3613
5236
|
});
|
|
@@ -3631,6 +5254,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3631
5254
|
filterPredicate: baseUrl != null ? ((i) => (i instanceof URL ? i.href : i.id?.href ?? "").startsWith(baseUrl)) : void 0,
|
|
3632
5255
|
collectionCallbacks: this.followersCallbacks,
|
|
3633
5256
|
tracerProvider: this.tracerProvider,
|
|
5257
|
+
meterProvider: this._meterProvider,
|
|
3634
5258
|
onUnauthorized,
|
|
3635
5259
|
onNotFound
|
|
3636
5260
|
});
|
|
@@ -3642,6 +5266,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3642
5266
|
context,
|
|
3643
5267
|
collectionCallbacks: this.likedCallbacks,
|
|
3644
5268
|
tracerProvider: this.tracerProvider,
|
|
5269
|
+
meterProvider: this._meterProvider,
|
|
3645
5270
|
onUnauthorized,
|
|
3646
5271
|
onNotFound
|
|
3647
5272
|
});
|
|
@@ -3652,6 +5277,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3652
5277
|
context,
|
|
3653
5278
|
collectionCallbacks: this.featuredCallbacks,
|
|
3654
5279
|
tracerProvider: this.tracerProvider,
|
|
5280
|
+
meterProvider: this._meterProvider,
|
|
3655
5281
|
onUnauthorized,
|
|
3656
5282
|
onNotFound
|
|
3657
5283
|
});
|
|
@@ -3662,6 +5288,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3662
5288
|
context,
|
|
3663
5289
|
collectionCallbacks: this.featuredTagsCallbacks,
|
|
3664
5290
|
tracerProvider: this.tracerProvider,
|
|
5291
|
+
meterProvider: this._meterProvider,
|
|
3665
5292
|
onUnauthorized,
|
|
3666
5293
|
onNotFound
|
|
3667
5294
|
});
|
|
@@ -3674,6 +5301,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3674
5301
|
values: route.values,
|
|
3675
5302
|
collectionCallbacks: callbacks,
|
|
3676
5303
|
tracerProvider: this.tracerProvider,
|
|
5304
|
+
meterProvider: this._meterProvider,
|
|
3677
5305
|
onUnauthorized,
|
|
3678
5306
|
onNotFound
|
|
3679
5307
|
});
|
|
@@ -3687,17 +5315,64 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3687
5315
|
values: route.values,
|
|
3688
5316
|
collectionCallbacks: callbacks,
|
|
3689
5317
|
tracerProvider: this.tracerProvider,
|
|
5318
|
+
meterProvider: this._meterProvider,
|
|
3690
5319
|
onUnauthorized,
|
|
3691
5320
|
onNotFound
|
|
3692
5321
|
});
|
|
3693
5322
|
}
|
|
3694
5323
|
default: {
|
|
5324
|
+
metricState.endpoint = "not_found";
|
|
3695
5325
|
const response = onNotFound(request);
|
|
3696
5326
|
return response instanceof Promise ? await response : response;
|
|
3697
5327
|
}
|
|
3698
5328
|
}
|
|
3699
5329
|
}
|
|
3700
5330
|
};
|
|
5331
|
+
function getEndpointCategory(routeName) {
|
|
5332
|
+
if (routeName.startsWith("object:")) return "object";
|
|
5333
|
+
if (routeName.startsWith("collection:") || routeName.startsWith("orderedCollection:")) return "collection";
|
|
5334
|
+
if (routeName.startsWith("actorAlias:")) return "actor";
|
|
5335
|
+
switch (routeName) {
|
|
5336
|
+
case "webfinger": return "webfinger";
|
|
5337
|
+
case "nodeInfoJrd":
|
|
5338
|
+
case "nodeInfo": return "nodeinfo";
|
|
5339
|
+
case "actor": return "actor";
|
|
5340
|
+
case "inbox": return "inbox";
|
|
5341
|
+
case "sharedInbox": return "shared_inbox";
|
|
5342
|
+
case "outbox": return "outbox";
|
|
5343
|
+
case "following": return "following";
|
|
5344
|
+
case "followers": return "followers";
|
|
5345
|
+
case "liked": return "liked";
|
|
5346
|
+
case "featured": return "featured";
|
|
5347
|
+
case "featuredTags": return "featured_tags";
|
|
5348
|
+
case "benchmarkStats":
|
|
5349
|
+
case "benchmarkTrigger": return "benchmark";
|
|
5350
|
+
default: return "not_found";
|
|
5351
|
+
}
|
|
5352
|
+
}
|
|
5353
|
+
function getCollectionMetricRoute(routeName) {
|
|
5354
|
+
switch (routeName) {
|
|
5355
|
+
case "inbox":
|
|
5356
|
+
case "outbox":
|
|
5357
|
+
case "following":
|
|
5358
|
+
case "followers":
|
|
5359
|
+
case "liked":
|
|
5360
|
+
case "featured": return {
|
|
5361
|
+
kind: routeName,
|
|
5362
|
+
dispatcher: "built_in"
|
|
5363
|
+
};
|
|
5364
|
+
case "featuredTags": return {
|
|
5365
|
+
kind: "featured_tags",
|
|
5366
|
+
dispatcher: "built_in"
|
|
5367
|
+
};
|
|
5368
|
+
case "collection":
|
|
5369
|
+
case "orderedCollection": return {
|
|
5370
|
+
kind: "custom",
|
|
5371
|
+
dispatcher: "custom"
|
|
5372
|
+
};
|
|
5373
|
+
default: return;
|
|
5374
|
+
}
|
|
5375
|
+
}
|
|
3701
5376
|
const FANOUT_THRESHOLD = 5;
|
|
3702
5377
|
var ContextImpl = class ContextImpl {
|
|
3703
5378
|
url;
|
|
@@ -3759,13 +5434,16 @@ var ContextImpl = class ContextImpl {
|
|
|
3759
5434
|
get tracerProvider() {
|
|
3760
5435
|
return this.federation.tracerProvider;
|
|
3761
5436
|
}
|
|
5437
|
+
get meterProvider() {
|
|
5438
|
+
return this.federation.meterProvider;
|
|
5439
|
+
}
|
|
3762
5440
|
getNodeInfoUri() {
|
|
3763
5441
|
const path = this.federation.router.build("nodeInfo", {});
|
|
3764
5442
|
if (path == null) throw new RouterError("No NodeInfo dispatcher registered.");
|
|
3765
5443
|
return new URL(path, this.canonicalOrigin);
|
|
3766
5444
|
}
|
|
3767
5445
|
getActorUri(identifier) {
|
|
3768
|
-
const path = this.federation.router.build("actor", { identifier });
|
|
5446
|
+
const path = this.federation.router.build(`actorAlias:${identifier}`, {}) ?? this.federation.router.build("actor", { identifier });
|
|
3769
5447
|
if (path == null) throw new RouterError("No actor dispatcher registered.");
|
|
3770
5448
|
return new URL(path, this.canonicalOrigin);
|
|
3771
5449
|
}
|
|
@@ -3831,8 +5509,8 @@ var ContextImpl = class ContextImpl {
|
|
|
3831
5509
|
type: "inbox",
|
|
3832
5510
|
identifier: void 0
|
|
3833
5511
|
};
|
|
3834
|
-
const identifier = route.values.identifier;
|
|
3835
|
-
if (route.name === "actor") return {
|
|
5512
|
+
const identifier = route.name.startsWith("actorAlias:") ? route.name.substring(11) : route.values.identifier;
|
|
5513
|
+
if (route.name === "actor" || route.name.startsWith("actorAlias:")) return {
|
|
3836
5514
|
type: "actor",
|
|
3837
5515
|
identifier
|
|
3838
5516
|
};
|
|
@@ -3998,6 +5676,7 @@ var ContextImpl = class ContextImpl {
|
|
|
3998
5676
|
contextLoader: options.contextLoader ?? this.contextLoader,
|
|
3999
5677
|
userAgent: options.userAgent ?? this.federation.userAgent,
|
|
4000
5678
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
5679
|
+
meterProvider: options.meterProvider ?? this.meterProvider,
|
|
4001
5680
|
allowPrivateAddress: this.federation.allowPrivateAddress
|
|
4002
5681
|
});
|
|
4003
5682
|
}
|
|
@@ -4024,6 +5703,7 @@ var ContextImpl = class ContextImpl {
|
|
|
4024
5703
|
...options,
|
|
4025
5704
|
userAgent: options.userAgent ?? this.federation.userAgent,
|
|
4026
5705
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
5706
|
+
meterProvider: options.meterProvider ?? this.federation._meterProvider,
|
|
4027
5707
|
allowPrivateAddress: this.federation.allowPrivateAddress
|
|
4028
5708
|
});
|
|
4029
5709
|
}
|
|
@@ -4180,6 +5860,12 @@ var ContextImpl = class ContextImpl {
|
|
|
4180
5860
|
};
|
|
4181
5861
|
if (!this.federation.manuallyStartQueue) this.federation._startQueueInternal(this.data);
|
|
4182
5862
|
await this.federation.fanoutQueue.enqueue(message, { orderingKey: options.orderingKey });
|
|
5863
|
+
getFederationMetrics(this.federation.meterProvider).recordQueueTaskEnqueued({
|
|
5864
|
+
role: "fanout",
|
|
5865
|
+
queue: this.federation.fanoutQueue,
|
|
5866
|
+
activityType: message.activityType
|
|
5867
|
+
}, 0);
|
|
5868
|
+
recordFanoutRecipients(this.federation.meterProvider, globalThis.Object.keys(message.inboxes).length, message.activityType);
|
|
4183
5869
|
return true;
|
|
4184
5870
|
}
|
|
4185
5871
|
async *getFollowers(identifier) {
|
|
@@ -4243,6 +5929,7 @@ var ContextImpl = class ContextImpl {
|
|
|
4243
5929
|
if (await verifyObject(Activity, json, {
|
|
4244
5930
|
contextLoader,
|
|
4245
5931
|
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
5932
|
+
meterProvider: this.meterProvider,
|
|
4246
5933
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
4247
5934
|
keyCache
|
|
4248
5935
|
}) == null) {
|
|
@@ -4301,6 +5988,7 @@ var ContextImpl = class ContextImpl {
|
|
|
4301
5988
|
const routeResult = await routeActivity({
|
|
4302
5989
|
context: this,
|
|
4303
5990
|
json,
|
|
5991
|
+
ldSignatureVerified: false,
|
|
4304
5992
|
activity,
|
|
4305
5993
|
recipient,
|
|
4306
5994
|
inboxListeners: this.federation.inboxListeners,
|
|
@@ -4310,6 +5998,7 @@ var ContextImpl = class ContextImpl {
|
|
|
4310
5998
|
kvPrefixes: this.federation.kvPrefixes,
|
|
4311
5999
|
queue: this.federation.inboxQueue,
|
|
4312
6000
|
span,
|
|
6001
|
+
meterProvider: this.federation.meterProvider,
|
|
4313
6002
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
4314
6003
|
idempotencyStrategy: this.federation.idempotencyStrategy
|
|
4315
6004
|
});
|
|
@@ -4384,6 +6073,7 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
4384
6073
|
contextLoader: options.contextLoader ?? this.contextLoader,
|
|
4385
6074
|
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
4386
6075
|
timeWindow: this.federation.signatureTimeWindow,
|
|
6076
|
+
meterProvider: this.meterProvider,
|
|
4387
6077
|
tracerProvider: options.tracerProvider ?? this.tracerProvider
|
|
4388
6078
|
});
|
|
4389
6079
|
}
|
|
@@ -4508,6 +6198,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
4508
6198
|
activityType: ctx.activityType,
|
|
4509
6199
|
inbox: new URL(inbox),
|
|
4510
6200
|
sharedInbox: inboxes[inbox].sharedInbox,
|
|
6201
|
+
meterProvider: ctx.meterProvider,
|
|
4511
6202
|
tracerProvider: ctx.tracerProvider,
|
|
4512
6203
|
specDeterminer: new KvSpecDeterminer(ctx.federation.kv, ctx.federation.kvPrefixes.httpMessageSignaturesSpec, ctx.federation.firstKnock)
|
|
4513
6204
|
}));
|
|
@@ -4558,8 +6249,10 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
4558
6249
|
}
|
|
4559
6250
|
const { outboxQueue } = ctx.federation;
|
|
4560
6251
|
if (outboxQueue.enqueueMany == null || orderingKey != null) {
|
|
4561
|
-
const
|
|
4562
|
-
|
|
6252
|
+
const errors = (await Promise.allSettled(messages.map(async (m) => {
|
|
6253
|
+
await outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey });
|
|
6254
|
+
recordOutboxEnqueue(ctx.federation.meterProvider, outboxQueue, m.message);
|
|
6255
|
+
}))).filter((r) => r.status === "rejected").map((r) => r.reason);
|
|
4563
6256
|
if (errors.length > 0) {
|
|
4564
6257
|
logger.error("Failed to enqueue activity {activityId} to forward later:\n{errors}", {
|
|
4565
6258
|
activityId: ctx.activityId,
|
|
@@ -4568,19 +6261,30 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
4568
6261
|
if (errors.length > 1) throw new AggregateError(errors, `Failed to enqueue activity ${ctx.activityId} to forward later.`);
|
|
4569
6262
|
throw errors[0];
|
|
4570
6263
|
}
|
|
4571
|
-
} else
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
activityId
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
6264
|
+
} else {
|
|
6265
|
+
try {
|
|
6266
|
+
await outboxQueue.enqueueMany(messages.map((m) => m.message));
|
|
6267
|
+
} catch (error) {
|
|
6268
|
+
logger.error("Failed to enqueue activity {activityId} to forward later:\n{error}", {
|
|
6269
|
+
activityId: ctx.activityId,
|
|
6270
|
+
error
|
|
6271
|
+
});
|
|
6272
|
+
throw error;
|
|
6273
|
+
}
|
|
6274
|
+
for (const m of messages) recordOutboxEnqueue(ctx.federation.meterProvider, outboxQueue, m.message);
|
|
4579
6275
|
}
|
|
4580
6276
|
return true;
|
|
4581
6277
|
}
|
|
4582
6278
|
var InboxContextImpl = class InboxContextImpl extends ContextImpl {
|
|
4583
6279
|
recipient;
|
|
6280
|
+
/**
|
|
6281
|
+
* The original received activity payload.
|
|
6282
|
+
*
|
|
6283
|
+
* Fedify may normalize a Linked Data Signature payload internally for safe
|
|
6284
|
+
* parsing, but forwarding must keep the sender's payload unchanged so
|
|
6285
|
+
* third-party signatures/proofs remain intact.
|
|
6286
|
+
* @internal
|
|
6287
|
+
*/
|
|
4584
6288
|
activity;
|
|
4585
6289
|
activityId;
|
|
4586
6290
|
activityType;
|
|
@@ -4713,4 +6417,4 @@ function getRequestId(request) {
|
|
|
4713
6417
|
return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
4714
6418
|
}
|
|
4715
6419
|
//#endregion
|
|
4716
|
-
export {
|
|
6420
|
+
export { SendActivityError as a, buildCollectionSynchronizationHeader as c, normalizeCircuitBreakerOptions as d, parseCircuitBreakerKvState as f, createExponentialBackoffPolicy as i, digest as l, middleware_exports as n, respondWithObject as o, createFederationBuilder as p, handleWebFinger as r, respondWithObjectIfAcceptable as s, createFederation as t, CircuitBreaker as u };
|