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