@fedify/fedify 2.3.14 → 2.3.15

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 (87) hide show
  1. package/dist/{builder-C6yXExXl.mjs → builder-B6vSXYxn.mjs} +2 -2
  2. package/dist/{client-CAM_bQXx.d.cts → client-BIBzqEIp.d.cts} +0 -1
  3. package/dist/{client-1He2YuBF.mjs → client-DpUNYKla.mjs} +113 -64
  4. package/dist/{client-CSddvgWN.d.ts → client-jE7oGvRh.d.ts} +0 -1
  5. package/dist/compat/mod.d.cts +1 -1
  6. package/dist/compat/mod.d.ts +1 -1
  7. package/dist/compat/transformers.test.mjs +1 -1
  8. package/dist/{context-MOrCC7yl.d.cts → context-CE_Tj_Oc.d.cts} +28 -2
  9. package/dist/{context-BWbdE_MZ.d.ts → context-H_5tZXg-.d.ts} +28 -2
  10. package/dist/{deno-y3L4Oa94.mjs → deno-BCuL3mid.mjs} +1 -1
  11. package/dist/docloader-D2dr1_6B.mjs +125 -0
  12. package/dist/federation/builder.test.mjs +1 -1
  13. package/dist/federation/handler.test.mjs +5 -5
  14. package/dist/federation/idempotency.test.mjs +2 -2
  15. package/dist/federation/inbox.test.mjs +1 -1
  16. package/dist/federation/metrics.test.mjs +1 -1
  17. package/dist/federation/middleware.test.mjs +455 -29
  18. package/dist/federation/mod.cjs +1 -1
  19. package/dist/federation/mod.d.cts +2 -2
  20. package/dist/federation/mod.d.ts +2 -2
  21. package/dist/federation/mod.js +1 -1
  22. package/dist/federation/send.test.mjs +498 -88
  23. package/dist/federation/temporal.test.mjs +1 -1
  24. package/dist/federation/webfinger.test.mjs +1 -1
  25. package/dist/{http-DNM9KyIO.js → http-BkjxY3U0.js} +172 -103
  26. package/dist/{http-BCSf1KIY.mjs → http-CSDcThvz.mjs} +196 -98
  27. package/dist/{http-DQmNpfym.d.ts → http-CZPYFK3b.d.cts} +1 -2
  28. package/dist/{http-DiWQB8Mr.d.cts → http-DmP0sr2l.d.ts} +1 -2
  29. package/dist/{http-D77Wjrcg.cjs → http-gOkAiIe1.cjs} +173 -104
  30. package/dist/{inbox-NObX26c9.mjs → inbox-BHxez4d5.mjs} +2 -2
  31. package/dist/{key-APURZ0ky.mjs → key-D8IMQ72M.mjs} +2 -2
  32. package/dist/{kv-cache-Bg4utAuv.cjs → kv-cache-Alm5UmJa.cjs} +59 -17
  33. package/dist/{kv-cache-C69dzgQi.mjs → kv-cache-Blr9nDDp.mjs} +1 -1
  34. package/dist/{kv-cache-LBCwHNLu.js → kv-cache-CkXgU-ak.js} +60 -18
  35. package/dist/{ld-BoLqbDaL.mjs → ld-CVxxE1Pp.mjs} +3 -3
  36. package/dist/{metrics-DBSl8wEr.mjs → metrics-SYIZHFf_.mjs} +1 -1
  37. package/dist/{middleware-BQUQ0pPl.cjs → middleware-4yPyyoF5.cjs} +237 -99
  38. package/dist/{middleware-CZ8tI1qL.mjs → middleware-CfskJGfV.mjs} +81 -41
  39. package/dist/{middleware-DAqo2eOY.mjs → middleware-D3JWfiCu.mjs} +1 -1
  40. package/dist/{middleware-CRd1JmuL.js → middleware-DhN7c65B.js} +238 -100
  41. package/dist/{mod-LmD03uRA.d.ts → mod-CDFFESfL.d.ts} +1 -1
  42. package/dist/{mod-Bid79LFU.d.cts → mod-C_gFmeNq.d.cts} +1 -1
  43. package/dist/{mod-Co1M3Ux0.d.cts → mod-DDVkmZ_h.d.cts} +2 -8
  44. package/dist/{mod-B6PwkUMU.d.cts → mod-DDjtbkZO.d.cts} +1 -1
  45. package/dist/{mod-Br8yJNcE.d.ts → mod-DMjhnFbg.d.ts} +2 -8
  46. package/dist/{mod-TLEvgT1i.d.ts → mod-HQXFvOHs.d.ts} +1 -1
  47. package/dist/mod.cjs +5 -5
  48. package/dist/mod.d.cts +6 -6
  49. package/dist/mod.d.ts +6 -6
  50. package/dist/mod.js +5 -5
  51. package/dist/nodeinfo/client.test.mjs +337 -19
  52. package/dist/nodeinfo/handler.test.mjs +1 -1
  53. package/dist/nodeinfo/mod.cjs +1 -1
  54. package/dist/nodeinfo/mod.d.cts +1 -1
  55. package/dist/nodeinfo/mod.d.ts +1 -1
  56. package/dist/nodeinfo/mod.js +1 -1
  57. package/dist/otel/exporter.test.mjs +292 -30
  58. package/dist/otel/mod.cjs +36 -30
  59. package/dist/otel/mod.d.cts +13 -0
  60. package/dist/otel/mod.d.ts +13 -0
  61. package/dist/otel/mod.js +36 -30
  62. package/dist/{owner-DjKvngeb.mjs → owner-BluLVPf0.mjs} +2 -2
  63. package/dist/{proof-CWTnYhjS.cjs → proof-CR-HwI0l.cjs} +1 -1
  64. package/dist/{proof-D2oFOdNA.mjs → proof-Co6nQBVR.mjs} +3 -3
  65. package/dist/{proof-s9sy44kx.js → proof-Dg0d9QBo.js} +1 -1
  66. package/dist/{send-xDK5UnL-.mjs → send-D_DnqOFA.mjs} +171 -73
  67. package/dist/sig/http.test.mjs +528 -2
  68. package/dist/sig/key.test.mjs +2 -2
  69. package/dist/sig/ld.test.mjs +2 -2
  70. package/dist/sig/mod.cjs +2 -2
  71. package/dist/sig/mod.d.cts +2 -2
  72. package/dist/sig/mod.d.ts +2 -2
  73. package/dist/sig/mod.js +2 -2
  74. package/dist/sig/owner.test.mjs +1 -1
  75. package/dist/sig/proof.test.mjs +1 -1
  76. package/dist/{temporal-C6icc5n9.mjs → temporal-B8FUNkXB.mjs} +1 -1
  77. package/dist/testing/mod.d.mts +26 -0
  78. package/dist/{types-miAYfiGO.js → types-B1_pRFE_.js} +112 -63
  79. package/dist/{types-DRPjmHVV.cjs → types-DOvyymM4.cjs} +111 -62
  80. package/dist/utils/docloader.test.mjs +318 -34
  81. package/dist/utils/kv-cache.test.mjs +1 -1
  82. package/dist/utils/mod.cjs +1 -1
  83. package/dist/utils/mod.d.cts +1 -1
  84. package/dist/utils/mod.d.ts +1 -1
  85. package/dist/utils/mod.js +1 -1
  86. package/package.json +8 -8
  87. package/dist/docloader-yuyzZC9a.mjs +0 -83
@@ -1,12 +1,13 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-y3L4Oa94.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics } from "./metrics-DBSl8wEr.mjs";
6
- import { n as doubleKnock } from "./http-BCSf1KIY.mjs";
4
+ import { n as version, t as name } from "./deno-BCuL3mid.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics } from "./metrics-SYIZHFf_.mjs";
6
+ import { r as doubleKnockScoped } from "./http-CSDcThvz.mjs";
7
7
  import { getLogger } from "@logtape/logtape";
8
8
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
9
- import { FetchError, UrlError, validatePublicUrl } from "@fedify/vocab-runtime";
9
+ import { FetchError, UrlError } from "@fedify/vocab-runtime";
10
+ import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
10
11
  //#region src/federation/send.ts
11
12
  /**
12
13
  * Extracts the inbox URLs from recipients.
@@ -39,9 +40,10 @@ function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
39
40
  *
40
41
  * @param parameters The parameters for sending the activity.
41
42
  * See also {@link SendActivityParameters}.
43
+ * @param deps The dependencies, for tests.
42
44
  * @throws {Error} If the activity fails to send.
43
45
  */
44
- function sendActivity(options) {
46
+ function sendActivity(options, deps = {}) {
45
47
  const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
46
48
  return tracerProvider.getTracer(name, version).startActiveSpan("activitypub.send_activity", {
47
49
  kind: SpanKind.CLIENT,
@@ -53,7 +55,7 @@ function sendActivity(options) {
53
55
  await sendActivityInternal({
54
56
  ...options,
55
57
  tracerProvider
56
- }, span);
58
+ }, span, deps);
57
59
  } catch (e) {
58
60
  span.setStatus({
59
61
  code: SpanStatusCode.ERROR,
@@ -106,13 +108,14 @@ async function readLimitedResponseBody(response, maxBytes) {
106
108
  totalBytes += value.length;
107
109
  }
108
110
  } finally {
111
+ if (truncated) reader.cancel().catch(() => {});
109
112
  reader.releaseLock();
110
113
  }
111
114
  let result = chunks.join("");
112
115
  if (truncated) result += "… (truncated)";
113
116
  return result;
114
117
  }
115
- async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
118
+ async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span, deps) {
116
119
  const logger = getLogger([
117
120
  "fedify",
118
121
  "federation",
@@ -121,9 +124,11 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
121
124
  const federationMetrics = getFederationMetrics(meterProvider);
122
125
  const started = performance.now();
123
126
  let deliverySuccess = false;
124
- async function validateUrl(url) {
125
- if (!allowPrivateAddress) try {
126
- await validatePublicUrl(url);
127
+ const openUnvalidatedHop = deps.openHop ?? (allowPrivateAddress ? openDirectFetch : (url) => openPublicFetch(url));
128
+ async function openHop(url) {
129
+ let session;
130
+ try {
131
+ session = await openUnvalidatedHop(url);
127
132
  } catch (error) {
128
133
  if (error instanceof UrlError && error.reason === "dns") {
129
134
  logger.error("DNS resolution failed for URL: {url}", {
@@ -140,78 +145,86 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
140
145
  });
141
146
  throw error;
142
147
  }
148
+ return session;
143
149
  }
150
+ let initialSession;
144
151
  try {
145
- await validateUrl(inbox.href);
152
+ initialSession = await openHop(inbox.href);
146
153
  } catch (error) {
147
154
  if (error instanceof FetchError) federationMetrics.recordDelivery(inbox, getDurationMs(started), false, activityType);
148
155
  throw error;
149
156
  }
150
- headers = new Headers(headers);
151
- headers.set("Content-Type", "application/activity+json");
152
- const request = new Request(inbox, {
153
- method: "POST",
154
- headers,
155
- body: JSON.stringify(activity),
156
- redirect: "manual"
157
- });
158
- let rsaKey = null;
159
- for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
160
- rsaKey = key;
161
- break;
162
- }
163
- if (rsaKey == null) logger.warn("No supported key found to sign the request to {inbox}. The request will be sent without a signature. In order to sign the request, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
164
- inbox: inbox.href,
165
- keys: keys.map((pair) => ({
166
- keyId: pair.keyId.href,
167
- privateKey: pair.privateKey
168
- }))
169
- });
170
- let response;
157
+ let initialPending = true;
158
+ const openDeliveryHop = (url) => {
159
+ if (initialPending) {
160
+ initialPending = false;
161
+ return Promise.resolve(initialSession);
162
+ }
163
+ return openHop(url);
164
+ };
171
165
  try {
172
- response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await doubleKnock(request, rsaKey, {
173
- tracerProvider,
174
- specDeterminer,
175
- validateRedirect: validateUrl
166
+ headers = new Headers(headers);
167
+ headers.set("Content-Type", "application/activity+json");
168
+ const request = new Request(inbox, {
169
+ method: "POST",
170
+ headers,
171
+ body: JSON.stringify(activity),
172
+ redirect: "manual"
176
173
  });
177
- } catch (error) {
178
- const failure = error instanceof FetchError || error instanceof UrlError ? error : createFetchError(inbox.href, error);
179
- logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
180
- activityId,
174
+ let rsaKey = null;
175
+ for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
176
+ rsaKey = key;
177
+ break;
178
+ }
179
+ if (rsaKey == null) logger.warn("No supported key found to sign the request to {inbox}. The request will be sent without a signature. In order to sign the request, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
181
180
  inbox: inbox.href,
182
- error: failure
181
+ keys: keys.map((pair) => ({
182
+ keyId: pair.keyId.href,
183
+ privateKey: pair.privateKey
184
+ }))
183
185
  });
184
- federationMetrics.recordDelivery(inbox, getDurationMs(started), false, activityType);
185
- throw failure;
186
- }
187
- try {
188
- if (!response.ok) {
189
- let error;
190
- try {
191
- error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
192
- } catch (_) {
193
- error = "";
194
- }
195
- logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
186
+ let outcome;
187
+ try {
188
+ outcome = rsaKey == null ? await fetchWithValidatedRedirects(request, openDeliveryHop, (response) => consumeSendActivityResponse(response, inbox)) : await doubleKnockScoped(request, rsaKey, {
189
+ tracerProvider,
190
+ specDeterminer
191
+ }, openDeliveryHop, (response) => consumeSendActivityResponse(response, inbox));
192
+ } catch (error) {
193
+ const failure = error instanceof FetchError || error instanceof UrlError ? error : createFetchError(inbox.href, error);
194
+ logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
196
195
  activityId,
197
196
  inbox: inbox.href,
198
- status: response.status,
199
- statusText: response.statusText,
200
- error
197
+ error: failure
201
198
  });
202
- throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error, response.headers);
199
+ federationMetrics.recordDelivery(inbox, getDurationMs(started), false, activityType);
200
+ throw failure;
201
+ }
202
+ try {
203
+ if (!outcome.ok) {
204
+ const { status, statusText, error, responseHeaders } = outcome;
205
+ logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
206
+ activityId,
207
+ inbox: inbox.href,
208
+ status,
209
+ statusText,
210
+ error
211
+ });
212
+ throw new SendActivityError(inbox, status, `Failed to send activity ${activityId} to ${inbox.href} (${status} ${statusText}):\n${error}`, error, responseHeaders);
213
+ }
214
+ deliverySuccess = true;
215
+ const eventAttributes = {
216
+ "activitypub.inbox.url": inbox.href,
217
+ "activitypub.activity.id": activityId ?? ""
218
+ };
219
+ if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
220
+ const actorId = getActivityActorId(activity);
221
+ if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
222
+ span.addEvent("activitypub.activity.sent", eventAttributes);
223
+ } finally {
224
+ federationMetrics.recordDelivery(inbox, getDurationMs(started), deliverySuccess, activityType);
203
225
  }
204
- deliverySuccess = true;
205
- const eventAttributes = {
206
- "activitypub.inbox.url": inbox.href,
207
- "activitypub.activity.id": activityId ?? ""
208
- };
209
- if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
210
- const actorId = getActivityActorId(activity);
211
- if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
212
- span.addEvent("activitypub.activity.sent", eventAttributes);
213
226
  } finally {
214
- federationMetrics.recordDelivery(inbox, getDurationMs(started), deliverySuccess, activityType);
227
+ if (initialPending) await initialSession.close();
215
228
  }
216
229
  }
217
230
  function createFetchError(url, cause) {
@@ -219,10 +232,75 @@ function createFetchError(url, cause) {
219
232
  error.cause = cause;
220
233
  return error;
221
234
  }
222
- async function fetchWithValidatedRedirects(request, validateUrl) {
235
+ /**
236
+ * Starts cancelling the body of a response without waiting for it to settle.
237
+ * The cancellation of a branch of a tee (e.g., a body cloned by a fetch mock)
238
+ * settles only after the other branch is cancelled too, per the Streams
239
+ * standard, so awaiting it could block forever. Failures are only logged.
240
+ */
241
+ function cancelResponseBody(response, url) {
242
+ response.body?.cancel().catch((error) => {
243
+ getLogger([
244
+ "fedify",
245
+ "federation",
246
+ "outbox"
247
+ ]).debug("Failed to cancel the response body from {url}: {error}", {
248
+ url,
249
+ error
250
+ });
251
+ });
252
+ }
253
+ /**
254
+ * Reads what is needed from the final response of a delivery while its
255
+ * session is open: the body of a successful response is cancelled, and at
256
+ * most 1 KiB of an error response body is read.
257
+ */
258
+ async function consumeSendActivityResponse(response, inbox) {
259
+ if (response.ok) {
260
+ cancelResponseBody(response, response.url || inbox.href);
261
+ return { ok: true };
262
+ }
263
+ let error;
264
+ try {
265
+ error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
266
+ } catch (_) {
267
+ error = "";
268
+ }
269
+ return {
270
+ ok: false,
271
+ status: response.status,
272
+ statusText: response.statusText,
273
+ error,
274
+ responseHeaders: new Headers(response.headers)
275
+ };
276
+ }
277
+ /**
278
+ * Sends a request without signing it, following redirects manually like the
279
+ * Fetch standard does. Every hop is opened with `openHop()`, which validates
280
+ * the destination, after the previous hop is closed. The final response is
281
+ * passed to `consume()`, and its session is closed after that.
282
+ * This is exported only for tests; it is not exported from any entry point.
283
+ * @param request The request to send.
284
+ * @param openHop The function to open a session for each hop.
285
+ * @param consume The function to consume the final response.
286
+ * @returns The value returned by `consume`.
287
+ */
288
+ async function fetchWithValidatedRedirects(request, openHop, consume) {
289
+ const logger = getLogger([
290
+ "fedify",
291
+ "federation",
292
+ "outbox"
293
+ ]);
223
294
  let body = await request.clone().text();
224
295
  for (let redirects = 0;; redirects++) {
225
- const response = await fetch(request, { redirect: "manual" });
296
+ const session = await openHop(request.url);
297
+ let response;
298
+ try {
299
+ response = await session.fetch(request, { redirect: "manual" });
300
+ } catch (error) {
301
+ await closeAfterError(session, logger);
302
+ throw error;
303
+ }
226
304
  const location = response.headers.get("Location");
227
305
  if (![
228
306
  301,
@@ -230,11 +308,21 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
230
308
  303,
231
309
  307,
232
310
  308
233
- ].includes(response.status) || location == null) return response;
234
- await response.body?.cancel();
311
+ ].includes(response.status) || location == null) try {
312
+ return await consume(response);
313
+ } catch (error) {
314
+ logger.debug("Failed to consume the response from {url}; closing the session: {error}", {
315
+ url: request.url,
316
+ error
317
+ });
318
+ throw error;
319
+ } finally {
320
+ await session.close();
321
+ }
322
+ cancelResponseBody(response, request.url);
323
+ await session.close();
235
324
  if (redirects >= 20) throw new TypeError("Too many redirects");
236
325
  const url = new URL(location, request.url);
237
- await validateUrl(url.href);
238
326
  const headers = new Headers(request.headers);
239
327
  if (url.origin !== new URL(request.url).origin) {
240
328
  headers.delete("Authorization");
@@ -262,6 +350,16 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
262
350
  });
263
351
  }
264
352
  }
353
+ async function closeAfterError(session, logger) {
354
+ try {
355
+ await session.close();
356
+ } catch (error) {
357
+ logger.debug("Failed to close the session for {url}: {error}", {
358
+ url: session.url.href,
359
+ error
360
+ });
361
+ }
362
+ }
265
363
  /**
266
364
  * An error that is thrown when an activity fails to send to a remote inbox.
267
365
  * It contains structured information about the failure, including the HTTP
@@ -307,4 +405,4 @@ var SendActivityError = class extends Error {
307
405
  }
308
406
  };
309
407
  //#endregion
310
- export { extractInboxes as n, sendActivity as r, SendActivityError as t };
408
+ export { sendActivity as i, extractInboxes as n, fetchWithValidatedRedirects as r, SendActivityError as t };