@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.
Files changed (138) hide show
  1. package/README.md +16 -7
  2. package/dist/{assert-DikXweDx.mjs → assert-OguE97r2.mjs} +1 -1
  3. package/dist/{assert_instance_of-C4Ri6VuN.mjs → assert_instance_of-DBC5X09g.mjs} +1 -1
  4. package/dist/{assert_not_equals--wG9hV7u.mjs → assert_not_equals-DkVK8oqV.mjs} +1 -1
  5. package/dist/{assert_rejects-B-qJtC9Z.mjs → assert_rejects-DN60FHPX.mjs} +28 -3
  6. package/dist/{assert_strict_equals-Dmjbg-bA.mjs → assert_strict_equals-XEgZAlrj.mjs} +1 -1
  7. package/dist/{assert_throws-4NwKEy2q.mjs → assert_throws-BOkhLGYc.mjs} +1 -1
  8. package/dist/{builder-yAlpiIqP.mjs → builder-DSZ7FZmg.mjs} +96 -42
  9. package/dist/{chunk-nlSIicah.js → chunk-CRNNMoPX.js} +2 -2
  10. package/dist/circuit-breaker-CSWsyoef.mjs +337 -0
  11. package/dist/{client-z-8dc-e1.d.cts → client-CAM_bQXx.d.cts} +1 -0
  12. package/dist/{client-AtlibPOU.d.ts → client-CSddvgWN.d.ts} +1 -2
  13. package/dist/compat/mod.d.cts +2 -1
  14. package/dist/compat/mod.d.ts +2 -3
  15. package/dist/compat/mod.js +3 -3
  16. package/dist/compat/outgoing-jsonld.test.mjs +4 -4
  17. package/dist/compat/public-audience.test.mjs +4 -4
  18. package/dist/compat/transformers.test.mjs +5 -5
  19. package/dist/{context-BzH2-ajs.d.ts → context-BBVLF7lx.d.cts} +342 -269
  20. package/dist/{context-DJGagtNd.d.cts → context-BU6jSQdo.d.ts} +344 -266
  21. package/dist/{context-Dk_tacqz.mjs → context-DVoTs_wM.mjs} +64 -5
  22. package/dist/{deno-XBVlKnOX.mjs → deno-DCdM7d93.mjs} +1 -1
  23. package/dist/{docloader-DPp-X6gk.mjs → docloader-B7mVwA5_.mjs} +4 -4
  24. package/dist/{esm-DVILvP5e.mjs → esm-vrlUxr60.mjs} +4 -7
  25. package/dist/federation/builder.test.mjs +163 -11
  26. package/dist/federation/circuit-breaker.test.d.mts +2 -0
  27. package/dist/federation/circuit-breaker.test.mjs +446 -0
  28. package/dist/federation/collection.test.mjs +3 -3
  29. package/dist/federation/handler.test.mjs +1770 -29
  30. package/dist/federation/idempotency.test.mjs +5 -5
  31. package/dist/federation/inbox.test.mjs +4 -4
  32. package/dist/federation/keycache.test.mjs +5 -5
  33. package/dist/federation/kv.test.mjs +2 -2
  34. package/dist/federation/metrics.test.d.mts +2 -0
  35. package/dist/federation/metrics.test.mjs +634 -0
  36. package/dist/federation/middleware.test.mjs +4036 -146
  37. package/dist/federation/mod.cjs +195 -14
  38. package/dist/federation/mod.d.cts +6 -4
  39. package/dist/federation/mod.d.ts +6 -6
  40. package/dist/federation/mod.js +192 -14
  41. package/dist/federation/mq.test.mjs +176 -15
  42. package/dist/federation/negotiation.test.mjs +4 -4
  43. package/dist/federation/retry.test.mjs +3 -3
  44. package/dist/federation/router.test.mjs +190 -9
  45. package/dist/federation/send.test.mjs +153 -15
  46. package/dist/federation/temporal.test.d.mts +2 -0
  47. package/dist/federation/temporal.test.mjs +71 -0
  48. package/dist/federation/webfinger.test.mjs +150 -5
  49. package/dist/{http-CSlLA54v.mjs → http-BufbkLuW.mjs} +146 -47
  50. package/dist/{http-B4rrzxtg.js → http-Dh18nwJg.js} +994 -64
  51. package/dist/{http-aQzN9Ayi.d.ts → http-VyDTd4G3.d.cts} +15 -3
  52. package/dist/{http-BrGqJDXL.cjs → http-gOiudB3g.cjs} +1099 -61
  53. package/dist/{http-CrGuipxe.d.cts → http-lf8Hsd91.d.ts} +15 -1
  54. package/dist/{key-Bzx--sHD.mjs → key-DZdKLJXT.mjs} +43 -18
  55. package/dist/{kv-CbLNp3zQ.d.cts → kv-D6hNiMTK.d.ts} +1 -0
  56. package/dist/{kv-cache-DI-Sk7-I.cjs → kv-cache-BuK_nlpY.cjs} +20 -3
  57. package/dist/{kv-cache-U__xU4qR.mjs → kv-cache-C7SQnkGI.mjs} +21 -3
  58. package/dist/{kv-cache-EMIqoIuB.js → kv-cache-DNDSb6Po.js} +20 -3
  59. package/dist/{kv-GFYnFoOl.d.ts → kv-gJ8LYbxX.d.cts} +1 -3
  60. package/dist/ld-0Lsznacf.mjs +626 -0
  61. package/dist/metrics-BHeWd24f.mjs +815 -0
  62. package/dist/{middleware-zjZ6AFCW.mjs → middleware-BNANvDh2.mjs} +1 -1
  63. package/dist/{middleware-nidH7VZH.js → middleware-BgzRkDcm.js} +2260 -556
  64. package/dist/{middleware-An4DjES4.cjs → middleware-D-iES1nQ.cjs} +2318 -623
  65. package/dist/{middleware-K1g6bEkV.mjs → middleware-EV6J_eAc.mjs} +1639 -373
  66. package/dist/{mod-CR8soWa9.d.ts → mod-B0hW12_O.d.cts} +26 -4
  67. package/dist/mod-C0F6kvgS.d.cts +182 -0
  68. package/dist/{mod-Cr3f-ACa.d.cts → mod-COIAjwRS.d.ts} +26 -2
  69. package/dist/{mod-CMEbIaNh.d.cts → mod-DFvNJcNb.d.ts} +56 -4
  70. package/dist/mod-vPYVoa5n.d.ts +182 -0
  71. package/dist/{mod-CLgIXe9w.d.ts → mod-yvIXFAEi.d.cts} +56 -6
  72. package/dist/mod.cjs +9 -6
  73. package/dist/mod.d.cts +12 -10
  74. package/dist/mod.d.ts +12 -12
  75. package/dist/mod.js +11 -11
  76. package/dist/mq-D-nlpY04.d.ts +208 -0
  77. package/dist/mq-D8uSFzxe.d.cts +208 -0
  78. package/dist/{negotiation-SQvQgUqe.mjs → negotiation-DDstyBvc.mjs} +29 -0
  79. package/dist/nodeinfo/client.test.mjs +4 -4
  80. package/dist/nodeinfo/handler.test.mjs +4 -4
  81. package/dist/nodeinfo/mod.d.cts +2 -1
  82. package/dist/nodeinfo/mod.d.ts +2 -3
  83. package/dist/nodeinfo/mod.js +3 -3
  84. package/dist/nodeinfo/types.test.mjs +3 -3
  85. package/dist/otel/exporter.test.mjs +28 -25
  86. package/dist/otel/mod.cjs +6 -5
  87. package/dist/otel/mod.d.cts +5 -3
  88. package/dist/otel/mod.d.ts +5 -5
  89. package/dist/otel/mod.js +8 -7
  90. package/dist/{outgoing-jsonld-CNmZLixq.mjs → outgoing-jsonld-L_DbOaFe.mjs} +2 -2
  91. package/dist/{owner-C-tqfvxn.mjs → owner-Cl-1cAR8.mjs} +2 -2
  92. package/dist/{owner-CptqhsOy.d.cts → owner-CnngXDNJ.d.ts} +2 -1
  93. package/dist/{owner-74ARJ5TL.d.ts → owner-DEvZuyOE.d.cts} +2 -3
  94. package/dist/{proof-Bbbwf8_x.js → proof-CyXndf6-.js} +378 -15
  95. package/dist/{proof-BlmRPzrK.mjs → proof-Dnz8jtiQ.mjs} +26 -8
  96. package/dist/{proof-BW89QMVB.cjs → proof-tz35unOj.cjs} +432 -15
  97. package/dist/runtime/mod.d.cts +1 -0
  98. package/dist/runtime/mod.d.ts +1 -2
  99. package/dist/runtime/mod.js +3 -3
  100. package/dist/{send-05pJLcb6.mjs → send-DIbBYN5P.mjs} +72 -26
  101. package/dist/sig/http.test.mjs +356 -33
  102. package/dist/sig/key.test.mjs +103 -6
  103. package/dist/sig/ld.test.mjs +696 -7
  104. package/dist/sig/mod.cjs +2 -2
  105. package/dist/sig/mod.d.cts +4 -3
  106. package/dist/sig/mod.d.ts +4 -5
  107. package/dist/sig/mod.js +4 -4
  108. package/dist/sig/owner.test.mjs +6 -6
  109. package/dist/sig/proof.test.mjs +169 -8
  110. package/dist/{std__assert-CRDpx_HF.mjs → std__assert-BBjXFNOb.mjs} +5 -30
  111. package/dist/temporal-BEwob1Vg.mjs +95 -0
  112. package/dist/testing/mod.d.mts +110 -10
  113. package/dist/testing/mod.mjs +1 -2
  114. package/dist/{transformers-ve6e2xcg.js → transformers-BGMIq1cs.js} +2 -2
  115. package/dist/{types-hvL8ElAs.js → types-CAY3OdLq.js} +2 -2
  116. package/dist/utils/docloader.test.mjs +6 -6
  117. package/dist/utils/kv-cache.test.mjs +67 -2
  118. package/dist/utils/mod.cjs +1 -1
  119. package/dist/utils/mod.d.cts +2 -1
  120. package/dist/utils/mod.d.ts +2 -3
  121. package/dist/utils/mod.js +3 -3
  122. package/dist/vocab/cjs.test.mjs +1 -1
  123. package/dist/vocab/mod.d.cts +1 -0
  124. package/dist/vocab/mod.d.ts +1 -2
  125. package/dist/vocab/mod.js +2 -2
  126. package/package.json +16 -16
  127. package/dist/ld-DR78beiv.mjs +0 -279
  128. package/dist/middleware-BFBaR0mF.cjs +0 -4
  129. package/dist/mod-2d12ffz3.d.ts +0 -64
  130. package/dist/mod-D35TRn09.d.cts +0 -62
  131. package/dist/router-CrMLXoOr.mjs +0 -114
  132. package/dist/{activity-listener-ell7W1s9.mjs → activity-listener-tztVvlNb.mjs} +0 -0
  133. package/dist/{assert_equals-Ew3jOFa3.mjs → assert_equals-C-ZRDbaf.mjs} +0 -0
  134. package/dist/{client-D_1QpnWt.mjs → client-ByXmQhYD.mjs} +1 -1
  135. package/dist/{collection-D-HqUuA2.mjs → collection-Cc3DVAhE.mjs} +0 -0
  136. package/dist/{keycache-EGATflN-.mjs → keycache-BeU0LCII.mjs} +0 -0
  137. package/dist/{public-audience-DYFHzm_c.mjs → public-audience-Cvbr2Gzt.mjs} +1 -1
  138. /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-BrGqJDXL.cjs");
6
- const require_proof = require("./proof-BW89QMVB.cjs");
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-DI-Sk7-I.cjs");
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/router.ts
56
- function cloneInnerRouter(router) {
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
- * URL router and constructor based on URI Template
68
- * ([RFC 6570](https://tools.ietf.org/html/rfc6570)).
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
- var Router = class Router {
71
- #router;
72
- #templates;
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
- * An error thrown by the {@link Router}.
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
- var RouterError = class extends Error {
152
- /**
153
- * Create a new {@link RouterError}.
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(() => require("./middleware-BFBaR0mF.cjs"));
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
- const variables = this.router.add(path, "actor");
250
- if (variables.size !== 1 || !variables.has("identifier")) throw new RouterError("Path for actor dispatcher must have one variable: {identifier}");
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 (this.router.add(path, "nodeInfo").size !== 0) throw new RouterError("Path for NodeInfo dispatcher must have no variables.");
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
- const variables = this.router.add(path, routeName);
368
- if (variables.size < 1) throw new RouterError("Path for object dispatcher must have at least one variable.");
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
- const variables = this.router.add(path, "inbox");
411
- if (variables.size !== 1 || !variables.has("identifier")) throw new RouterError("Path for inbox dispatcher must have one variable: {identifier}");
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
- validateSingleIdentifierVariablePath(path, "Path for outbox dispatcher must have one variable: {identifier}");
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
- validateSingleIdentifierVariablePath(outboxPath, "Path for outbox must have one variable: {identifier}");
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
- const variables = this.router.add(path, "following");
496
- if (variables.size !== 1 || !variables.has("identifier")) throw new RouterError("Path for following collection dispatcher must have one variable: {identifier}");
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
- const variables = this.router.add(path, "followers");
522
- if (variables.size !== 1 || !variables.has("identifier")) throw new RouterError("Path for followers collection dispatcher must have one variable: {identifier}");
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
- const variables = this.router.add(path, "liked");
548
- if (variables.size !== 1 || !variables.has("identifier")) throw new RouterError("Path for liked collection dispatcher must have one variable: {identifier}");
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
- const variables = this.router.add(path, "featured");
574
- if (variables.size !== 1 || !variables.has("identifier")) throw new RouterError("Path for featured collection dispatcher must have one variable: {identifier}");
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
- const variables = this.router.add(path, "featuredTags");
600
- if (variables.size !== 1 || !variables.has("identifier")) throw new RouterError("Path for featured tags collection dispatcher must have one variable: {identifier}");
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
- const variables = this.router.add(inboxPath, "inbox");
629
- if (variables.size !== 1 || !variables.has("identifier")) throw new RouterError("Path for inbox must have one variable: {identifier}");
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 (this.router.add(sharedInboxPath, "sharedInbox").size !== 0) throw new RouterError("Path for shared inbox must have no variables.");
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
- if (this.router.add(path, routeName).size < 1) throw new RouterError("Path for collection dispatcher must have at least one variable.");
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
- await listener(inboxContextFactory(recipient, json, activity?.id?.href, (0, _fedify_vocab.getTypeId)(activity).href), activity);
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
- if (collectionCallbacks == null) return await onNotFound(request);
1143
- let collection;
1144
- const baseUri = uriGetter(identifier);
1145
- if (cursor == null) {
1146
- const firstCursor = await collectionCallbacks.firstCursor?.(context, identifier);
1147
- const totalItems = filter == null ? await collectionCallbacks.counter?.(context, identifier) : void 0;
1148
- if (firstCursor == null) {
1149
- const itemsOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection ${spanName}`, {
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": baseUri.href,
1153
- "activitypub.collection.type": _fedify_vocab.OrderedCollection.typeId.href
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
- if (totalItems != null) span.setAttribute("activitypub.collection.total_items", Number(totalItems));
1705
+ const started = performance.now();
1157
1706
  try {
1158
- const page = await collectionCallbacks.dispatcher(context, identifier, null, filter);
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 { items } = page;
1164
- span.setAttribute("fedify.collection.items", items.length);
1165
- return items;
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 (itemsOrResponse instanceof Response) return itemsOrResponse;
1177
- collection = new _fedify_vocab.OrderedCollection({
1178
- id: baseUri,
1179
- totalItems: totalItems == null ? null : Number(totalItems),
1180
- items: filterCollectionItems(itemsOrResponse, name$1, filterPredicate)
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
- }, async (span) => {
1209
- try {
1210
- const page = await collectionCallbacks.dispatcher(context, identifier, cursor, filter);
1211
- if (page == null) {
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
- if (pageOrResponse instanceof Response) return pageOrResponse;
1228
- const { items, prevCursor, nextCursor } = pageOrResponse;
1229
- let prev = null;
1230
- if (prevCursor != null) {
1231
- prev = new URL(context.url);
1232
- prev.searchParams.set("cursor", prevCursor);
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
- let next = null;
1235
- if (nextCursor != null) {
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 partOf = new URL(context.url);
1240
- partOf.searchParams.delete("cursor");
1241
- collection = new _fedify_vocab.OrderedCollectionPage({
1242
- id: uri,
1243
- prev,
1244
- next,
1245
- items: filterCollectionItems(items, name$1, filterPredicate),
1246
- partOf
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
- let ldSigVerified;
1593
- try {
1594
- ldSigVerified = await require_proof.verifyJsonLd(json, {
1595
- contextLoader: ctx.contextLoader,
1596
- documentLoader: ctx.documentLoader,
1597
- keyCache,
1598
- tracerProvider
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
- } catch (error) {
1601
- if (error instanceof Error && error.name === "jsonld.SyntaxError") {
1602
- logger.error("Failed to parse JSON-LD:\n{error}", {
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
- ldSigVerified = false;
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
- activity = await _fedify_vocab.Activity.fromJsonLd(jsonWithoutSig, ctx);
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
- activity = await _fedify_vocab.Activity.fromJsonLd(jsonWithoutSig, ctx);
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
- async function _handleCustomCollection(request, { name, values, context, tracerProvider, collectionCallbacks: callbacks, filterPredicate }) {
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
- return await new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, _fedify_vocab.Collection, _fedify_vocab.CollectionPage, filterPredicate).fetchCollection(cursor).toJsonLd().then(respondAsActivity);
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
- return await new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, _fedify_vocab.OrderedCollection, _fedify_vocab.OrderedCollectionPage, filterPredicate).fetchCollection(cursor).toJsonLd().then(respondAsActivity);
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: this.filterItems(pages.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: await this.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: this.filterItems(pages.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
- return await handler(request, handlerParams);
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: return await onNotFound(request);
2142
- case UnauthorizedError: return await onUnauthorized(request);
2143
- default: throw error;
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
- throw error;
3248
+ federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
3249
+ throw transportError;
2515
3250
  }
2516
- if (!response.ok) {
2517
- let error;
2518
- try {
2519
- error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
2520
- } catch (_) {
2521
- error = "";
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
- logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
2524
- activityId,
2525
- inbox: inbox.href,
2526
- status: response.status,
2527
- statusText: response.statusText,
2528
- error
2529
- });
2530
- throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error);
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
- span.addEvent("activitypub.activity.sent", {
2533
- "activitypub.activity.json": JSON.stringify(activity),
2534
- "activitypub.inbox.url": inbox.href,
2535
- "activitypub.activity.id": activityId ?? ""
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
- if (options.tracer == null) return await handleWebFingerInternal(request, options);
2591
- return await options.tracer.startActiveSpan("webfinger.handle", { kind: _opentelemetry_api.SpanKind.SERVER }, async (span) => {
2592
- try {
2593
- const response = await handleWebFingerInternal(request, options);
2594
- span.setStatus({ code: response.ok ? _opentelemetry_api.SpanStatusCode.UNSET : _opentelemetry_api.SpanStatusCode.ERROR });
2595
- return response;
2596
- } catch (error) {
2597
- span.setStatus({
2598
- code: _opentelemetry_api.SpanStatusCode.ERROR,
2599
- message: String(error)
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 parameters Parameters for initializing the instance.
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 { allowPrivateAddress, userAgent } = options;
2803
- this.allowPrivateAddress = allowPrivateAddress ?? false;
2804
- this.documentLoaderFactory = options.documentLoaderFactory ?? ((opts) => {
2805
- return require_kv_cache.kvCache({
2806
- loader: (0, _fedify_vocab_runtime.getDocumentLoader)({
2807
- allowPrivateAddress: opts?.allowPrivateAddress ?? allowPrivateAddress,
2808
- userAgent: opts?.userAgent ?? userAgent
2809
- }),
2810
- kv: options.kv,
2811
- prefix: this.kvPrefixes.remoteDocument
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
- this.contextLoaderFactory = options.contextLoaderFactory ?? this.documentLoaderFactory;
2815
- this.authenticatedDocumentLoaderFactory = options.authenticatedDocumentLoaderFactory ?? ((identity) => require_kv_cache.getAuthenticatedDocumentLoader(identity, {
2816
- allowPrivateAddress,
2817
- userAgent,
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 = options.signatureTimeWindow ?? { hours: 1 };
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") await tracer.startActiveSpan("activitypub.fanout", {
2873
- kind: _opentelemetry_api.SpanKind.CONSUMER,
2874
- attributes: { "activitypub.activity.type": message.activityType }
2875
- }, extractedContext, async (span) => {
2876
- const spanCtx = span.spanContext();
2877
- return await (0, _logtape_logtape.withContext)({
2878
- traceId: spanCtx.traceId,
2879
- spanId: spanCtx.spanId
2880
- }, async () => {
2881
- if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
2882
- try {
2883
- await this.#listenFanoutMessage(contextData, message);
2884
- } catch (e) {
2885
- span.setStatus({
2886
- code: _opentelemetry_api.SpanStatusCode.ERROR,
2887
- message: String(e)
2888
- });
2889
- throw e;
2890
- } finally {
2891
- span.end();
2892
- }
2893
- });
2894
- });
2895
- else if (message.type === "outbox") await tracer.startActiveSpan("activitypub.outbox", {
2896
- kind: _opentelemetry_api.SpanKind.CONSUMER,
2897
- attributes: {
2898
- "activitypub.activity.type": message.activityType,
2899
- "activitypub.activity.retries": message.attempt
2900
- }
2901
- }, extractedContext, async (span) => {
2902
- const spanCtx = span.spanContext();
2903
- return await (0, _logtape_logtape.withContext)({
2904
- traceId: spanCtx.traceId,
2905
- spanId: spanCtx.spanId
2906
- }, async () => {
2907
- if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
2908
- try {
2909
- await this.#listenOutboxMessage(contextData, message, span);
2910
- } catch (e) {
2911
- span.setStatus({
2912
- code: _opentelemetry_api.SpanStatusCode.ERROR,
2913
- message: String(e)
2914
- });
2915
- throw e;
2916
- } finally {
2917
- span.end();
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: new URL(message.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 loaderOptions = this.#getLoaderOptions(message.baseUrl);
3022
- const activity = await _fedify_vocab.Activity.fromJsonLd(message.activity, {
3023
- contextLoader: this.contextLoaderFactory(loaderOptions),
3024
- documentLoader: rsaKeyPair == null ? this.documentLoaderFactory(loaderOptions) : this.authenticatedDocumentLoaderFactory(rsaKeyPair, loaderOptions),
3025
- tracerProvider: this.tracerProvider
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 (error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode)) {
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: (message.actorIds ?? []).flatMap((id) => {
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 delay = this.outboxRetryPolicy({
3074
- elapsedTime: Temporal.Instant.from(message.started).until(Temporal.Now.instant()),
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
- await this.outboxQueue?.enqueue({
4566
+ const retryMessage = {
3083
4567
  ...message,
3084
4568
  attempt: message.attempt + 1
3085
- }, { delay: Temporal.Duration.compare(delay, { seconds: 0 }) < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay });
3086
- } else logger.error("Failed to send activity {activityId} to {inbox} after {attempt} attempts; giving up:\n{error}", {
3087
- ...logData,
3088
- error
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
- const activity = await _fedify_vocab.Activity.fromJsonLd(message.activity, context);
3108
- span.setAttribute("activitypub.activity.type", (0, _fedify_vocab.getTypeId)(activity).href);
3109
- if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
3110
- const cacheKey = activity.id == null ? null : [
3111
- ...this.kvPrefixes.activityIdempotence,
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.id?.href,
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.id?.href,
4629
+ activityId: activity?.id?.href,
3161
4630
  activity: message.activity,
3162
4631
  recipient: message.identifier
3163
4632
  });
3164
- span.setStatus({
4633
+ listenerSpan.setStatus({
3165
4634
  code: _opentelemetry_api.SpanStatusCode.ERROR,
3166
4635
  message: String(error)
3167
4636
  });
3168
- span.end();
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.id?.href,
4648
+ activityId: activity?.id?.href,
3180
4649
  activity: message.activity,
3181
4650
  recipient: message.identifier
3182
4651
  });
3183
- await this.inboxQueue?.enqueue({
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
- }, { delay: Temporal.Duration.compare(delay, { seconds: 0 }) < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay });
3187
- } else logger.error("Failed to process the incoming activity {activityId} after {trial} attempts; giving up:\n{error}", {
3188
- error,
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
- span.setStatus({
4778
+ listenerSpan.setStatus({
3194
4779
  code: _opentelemetry_api.SpanStatusCode.ERROR,
3195
- message: String(error)
4780
+ message: `Unsupported activity type: ${activityType}`
3196
4781
  });
3197
- span.end();
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.id?.href,
4802
+ activityId: activity?.id?.href,
3203
4803
  activity: message.activity,
3204
4804
  recipient: message.identifier
3205
4805
  });
3206
- span.end();
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 promises = messages.map((m) => outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }));
3376
- const errors = (await Promise.allSettled(promises)).filter((r) => r.status === "rejected").map((r) => r.reason);
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 if (orderingKey != null) {
3386
- const promises = messages.map((m) => outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }));
3387
- const errors = (await Promise.allSettled(promises)).filter((r) => r.status === "rejected").map((r) => r.reason);
3388
- if (errors.length > 0) {
3389
- logger.error("Failed to enqueue activity {activityId} to send later: {errors}", {
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
- errors
4994
+ error
3392
4995
  });
3393
- if (errors.length > 1) throw new AggregateError(errors, `Failed to enqueue activity ${activityId} to send later.`);
3394
- throw errors[0];
4996
+ throw error;
3395
4997
  }
3396
- } else try {
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) return await onNotFound(request);
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
- context = this.#createContext(request, contextData, { invokedFromActorDispatcher: { identifier: route.values.identifier } });
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: route.values.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: context.toInboxContext.bind(context),
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 promises = messages.map((m) => outboxQueue.enqueue(m.message, { orderingKey: m.orderingKey }));
4554
- const errors = (await Promise.allSettled(promises)).filter((r) => r.status === "rejected").map((r) => r.reason);
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 try {
4564
- await outboxQueue.enqueueMany(messages.map((m) => m.message));
4565
- } catch (error) {
4566
- logger.error("Failed to enqueue activity {activityId} to forward later:\n{error}", {
4567
- activityId: ctx.activityId,
4568
- error
4569
- });
4570
- throw error;
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, "ContextImpl", {
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 FederationImpl;
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() {