@fedify/fedify 2.5.0-dev.2446 → 2.5.0-dev.2492

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 (141) hide show
  1. package/README.md +3 -0
  2. package/dist/{activity-listener-C-BSbKI7.mjs → activity-listener-CELFAodc.mjs} +8 -4
  3. package/dist/{builder-Da8b9O6v.mjs → builder-DphK9v7f.mjs} +5 -5
  4. package/dist/{client-D-XP_Ccb.d.ts → client-3InDZbba.d.cts} +0 -1
  5. package/dist/{client-D-XP_Ccb.d.cts → client-3InDZbba.d.ts} +0 -1
  6. package/dist/{client-CEnNz8O7.mjs → client-5SIFzW3q.mjs} +113 -64
  7. package/dist/compat/mod.d.cts +1 -1
  8. package/dist/compat/mod.d.ts +1 -1
  9. package/dist/compat/transformers.test.mjs +1 -1
  10. package/dist/{compound-proof-D1djO5fa.mjs → compound-proof-cYtvlTgF.mjs} +2 -2
  11. package/dist/{context-8Tu9kea0.mjs → context-COHd1Rsh.mjs} +2 -0
  12. package/dist/{context-BdB54V-A.d.ts → context-DQJE9YfH.d.ts} +332 -6
  13. package/dist/{context-CQ9H8vNW.d.cts → context-DSuL3YWx.d.cts} +332 -6
  14. package/dist/{deno-eNpfj4oR.mjs → deno-iOAA7iRN.mjs} +1 -1
  15. package/dist/docloader-DCZkiTeF.mjs +136 -0
  16. package/dist/{exporter-DThFfe7P.mjs → exporter-DVG68ozH.mjs} +36 -30
  17. package/dist/federation/builder.test.mjs +1 -1
  18. package/dist/federation/compound-fanout.test.mjs +6 -3
  19. package/dist/federation/handler-compound-proof.test.mjs +5 -5
  20. package/dist/federation/handler.test.mjs +22 -7
  21. package/dist/federation/hashlink-media.test.mjs +2 -2
  22. package/dist/federation/idempotency.test.mjs +2 -2
  23. package/dist/federation/inbox-actorless.test.d.mts +2 -0
  24. package/dist/federation/inbox-actorless.test.mjs +487 -0
  25. package/dist/federation/inbox-authentication.test.d.mts +2 -0
  26. package/dist/federation/inbox-authentication.test.mjs +116 -0
  27. package/dist/federation/inbox-report.test.mjs +5 -5
  28. package/dist/federation/inbox-view.test.d.mts +2 -0
  29. package/dist/federation/inbox-view.test.mjs +369 -0
  30. package/dist/federation/inbox.test.mjs +22 -12
  31. package/dist/federation/metrics.test.mjs +1 -1
  32. package/dist/federation/middleware.test.mjs +470 -35
  33. package/dist/federation/mod.cjs +2 -1
  34. package/dist/federation/mod.d.cts +4 -4
  35. package/dist/federation/mod.d.ts +4 -4
  36. package/dist/federation/mod.js +8 -2
  37. package/dist/federation/mq.test.mjs +36 -1
  38. package/dist/federation/outgoing-proof.test.mjs +9 -6
  39. package/dist/federation/portable-actor.test.mjs +3 -3
  40. package/dist/federation/portable-chain.test.mjs +3 -3
  41. package/dist/federation/portable-collection.test.mjs +4 -4
  42. package/dist/federation/portable-delivery.test.mjs +2 -2
  43. package/dist/federation/portable-gateway-keys.test.mjs +13 -7
  44. package/dist/federation/portable-inbox.test.mjs +4 -4
  45. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  46. package/dist/federation/portable-route-activity.test.mjs +2 -2
  47. package/dist/federation/portable.test.mjs +3 -3
  48. package/dist/federation/send.test.mjs +502 -93
  49. package/dist/federation/tasks/enqueue.test.mjs +3 -3
  50. package/dist/federation/tasks/tasks.test.mjs +4 -4
  51. package/dist/federation/temporal.test.mjs +1 -1
  52. package/dist/federation/webfinger.test.mjs +2 -2
  53. package/dist/{federation-BVH7drJA.cjs → federation-D0AZw_aw.cjs} +1415 -236
  54. package/dist/{http-BUJ5SFh6.mjs → http-BoJCMnjY.mjs} +204 -106
  55. package/dist/{http-DlNxxW_5.js → http-ChXUU_3c.js} +319 -123
  56. package/dist/{http-BfW3wyQ4.cjs → http-kv1LZw-r.cjs} +339 -125
  57. package/dist/inbox-BjihTKLo.mjs +312 -0
  58. package/dist/inbox-authentication-DflPYlLL.mjs +171 -0
  59. package/dist/inbox-view-D-skypg-.mjs +385 -0
  60. package/dist/{key-nYGY4QB9.d.cts → key-DbLNT-vA.d.ts} +1 -2
  61. package/dist/{key-DhsBxxZN.d.ts → key-DlHT4R6k.d.cts} +1 -2
  62. package/dist/{kv-cache-D_m3A241.mjs → kv-cache-Cmsyke4d.mjs} +1 -1
  63. package/dist/{kv-cache-7015lbrx.js → kv-cache-DQcte7ES.js} +73 -28
  64. package/dist/{kv-cache-kkBB0vbI.cjs → kv-cache-Ddh5sMxH.cjs} +72 -27
  65. package/dist/ld-CWVpo_NM.d.cts +250 -0
  66. package/dist/{ld-CD2CsPxq.mjs → ld-CdvcOEXQ.mjs} +52 -9
  67. package/dist/ld-Inagww4R.d.ts +250 -0
  68. package/dist/{metrics-D3LHk6Z4.mjs → metrics-CpxvUrBz.mjs} +1 -1
  69. package/dist/{middleware-N9eM16XE.js → middleware-BHb5toqA.js} +1413 -247
  70. package/dist/{middleware-CCDXvXOG.mjs → middleware-BKs2DLUF.mjs} +1 -1
  71. package/dist/{middleware-CzoWvrdZ.mjs → middleware-CoBLrTKX.mjs} +533 -132
  72. package/dist/{mod-B0dGgQsg.d.cts → mod-6TQ4UQZb.d.cts} +1 -1
  73. package/dist/mod-B4kBw1-8.d.ts +154 -0
  74. package/dist/{mod-DkhfZC1u.d.ts → mod-B7j30IQf.d.ts} +2 -9
  75. package/dist/mod-C1nT74IG.d.cts +154 -0
  76. package/dist/{mod-D0bxB8jS.d.ts → mod-CIeBgsOC.d.ts} +1 -1
  77. package/dist/{mod-B5-wdzmJ.d.cts → mod-YvA4Rk8J.d.cts} +2 -9
  78. package/dist/mod.cjs +6 -5
  79. package/dist/mod.d.cts +9 -9
  80. package/dist/mod.d.ts +9 -9
  81. package/dist/mod.js +6 -6
  82. package/dist/{mq-Dm1Wmtm-.mjs → mq-eF96QxUT.mjs} +6 -0
  83. package/dist/nodeinfo/client.test.mjs +337 -19
  84. package/dist/nodeinfo/handler.test.mjs +2 -2
  85. package/dist/nodeinfo/mod.cjs +1 -1
  86. package/dist/nodeinfo/mod.d.cts +1 -1
  87. package/dist/nodeinfo/mod.d.ts +1 -1
  88. package/dist/nodeinfo/mod.js +1 -1
  89. package/dist/otel/exporter.test.mjs +257 -1
  90. package/dist/otel/mod.cjs +36 -30
  91. package/dist/otel/mod.d.cts +13 -0
  92. package/dist/otel/mod.d.ts +13 -0
  93. package/dist/otel/mod.js +36 -30
  94. package/dist/{owner-C5-FVX6t.mjs → owner-DTnVpUKA.mjs} +3 -3
  95. package/dist/{portable-collection-Cony3_LC.mjs → portable-collection-CJeYTL61.mjs} +2 -2
  96. package/dist/{portable-collection-C-Z1SKDy.cjs → portable-collection-DciwBIia.cjs} +1 -1
  97. package/dist/{portable-collection-CC2zNAje.js → portable-collection-TmtpzD4E.js} +1 -1
  98. package/dist/{portable-key-DNc_flH-.mjs → portable-key-DcYok4ki.mjs} +1 -1
  99. package/dist/{proof-Cryb1c70.mjs → proof-GH7Poos_.mjs} +98 -10
  100. package/dist/{send-AF25pWaJ.mjs → send-BgGJh6pv.mjs} +174 -76
  101. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  102. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  103. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  104. package/dist/sig/compound-proof.test.mjs +2 -2
  105. package/dist/sig/http.test.mjs +553 -2
  106. package/dist/sig/key.test.mjs +1 -1
  107. package/dist/sig/ld.test.mjs +1 -1
  108. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  109. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  110. package/dist/sig/mod.cjs +2 -2
  111. package/dist/sig/mod.d.cts +3 -3
  112. package/dist/sig/mod.d.ts +3 -3
  113. package/dist/sig/mod.js +2 -2
  114. package/dist/sig/owner.test.mjs +1 -1
  115. package/dist/sig/portable-collection.test.mjs +1 -1
  116. package/dist/sig/portable-interop.test.mjs +3 -3
  117. package/dist/sig/portable-key.test.mjs +5 -5
  118. package/dist/sig/proof.test.mjs +45 -5
  119. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  120. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  121. package/dist/sig/verification.test.mjs +3 -3
  122. package/dist/{temporal-BsAeuUJj.mjs → temporal-gJINzcL4.mjs} +1 -1
  123. package/dist/testing/kv.test.mjs +1 -1
  124. package/dist/testing/mod.d.mts +288 -2
  125. package/dist/testing/mod.mjs +2 -2
  126. package/dist/{testing-t8xDeJZ5.mjs → testing-Dbr21vZk.mjs} +1 -1
  127. package/dist/{types-_S2_MeLF.js → types-DUjZb4i6.js} +112 -63
  128. package/dist/{types-poIao40f.cjs → types-hKCCjii0.cjs} +111 -62
  129. package/dist/utils/docloader.test.mjs +344 -36
  130. package/dist/utils/kv-cache.test.mjs +1 -1
  131. package/dist/utils/mod.cjs +1 -1
  132. package/dist/utils/mod.d.cts +1 -1
  133. package/dist/utils/mod.d.ts +1 -1
  134. package/dist/utils/mod.js +1 -1
  135. package/package.json +7 -7
  136. package/dist/docloader-B-0nqOB1.mjs +0 -91
  137. package/dist/inbox-CHvMaIFX.mjs +0 -188
  138. package/dist/mod-BRpER-B0.d.ts +0 -320
  139. package/dist/mod-xAfaLS4T.d.cts +0 -320
  140. package/dist/owner-BF0L9cMt.d.cts +0 -78
  141. package/dist/owner-BSzr5-I8.d.ts +0 -78
@@ -11,19 +11,22 @@ import { t as assertNotEquals } from "../assert_not_equals-CpPK2t89.mjs";
11
11
  import "../std__assert-DvMDRPes.mjs";
12
12
  import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
13
13
  import { t as assert } from "../assert-D1Eafmd_.mjs";
14
- import { f as verifyRequest } from "../http-BUJ5SFh6.mjs";
14
+ import { p as verifyRequest } from "../http-BoJCMnjY.mjs";
15
15
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
16
- import { t as doesActorOwnKey } from "../owner-C5-FVX6t.mjs";
17
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "../send-AF25pWaJ.mjs";
18
- import { t as FedifySpanExporter } from "../exporter-DThFfe7P.mjs";
16
+ import { t as doesActorOwnKey } from "../owner-DTnVpUKA.mjs";
17
+ import { i as sendActivity, n as extractInboxes, r as fetchWithValidatedRedirects, t as SendActivityError } from "../send-BgGJh6pv.mjs";
18
+ import { t as FedifySpanExporter } from "../exporter-DVG68ozH.mjs";
19
19
  import { configure, getLogger, reset, withContext } from "@logtape/logtape";
20
20
  import { Activity, Application, Endpoints, Group, Person, Service } from "@fedify/vocab";
21
21
  import { FetchError, UrlError } from "@fedify/vocab-runtime";
22
22
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
23
- import dns from "node:dns/promises";
23
+ import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
24
24
  import { AggregationTemporality, InMemoryMetricExporter, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
25
25
  import { AsyncLocalStorage } from "node:async_hooks";
26
26
  //#region src/federation/send.test.ts
27
+ function sendDirect(options) {
28
+ return sendActivity(options, { openHop: openDirectFetch });
29
+ }
27
30
  test("extractInboxes()", () => {
28
31
  const recipients = [
29
32
  new Person({
@@ -159,7 +162,7 @@ test("sendActivity()", async (t) => {
159
162
  "id": "https://example.com/activity",
160
163
  "actor": "https://example.com/person"
161
164
  };
162
- await sendActivity({
165
+ await sendDirect({
163
166
  activity,
164
167
  keys: [{
165
168
  privateKey: rsaPrivateKey2,
@@ -175,7 +178,7 @@ test("sendActivity()", async (t) => {
175
178
  assertEquals(request?.headers.get("Content-Type"), "application/activity+json");
176
179
  assertEquals(request?.headers.get("X-Test"), "test");
177
180
  httpSigVerified = null;
178
- await assertRejects(() => sendActivity({
181
+ await assertRejects(() => sendDirect({
179
182
  activity: {
180
183
  ...activity,
181
184
  actor: "https://example.com/person2"
@@ -204,7 +207,7 @@ test("sendActivity()", async (t) => {
204
207
  "id": "https://example.com/activity",
205
208
  "actor": "https://example.com/person"
206
209
  };
207
- await assertRejects(() => sendActivity({
210
+ await assertRejects(() => sendDirect({
208
211
  activity,
209
212
  activityId: "https://example.com/activity",
210
213
  keys: [{
@@ -222,7 +225,7 @@ test("sendActivity()", async (t) => {
222
225
  "actor": "https://example.com/person"
223
226
  };
224
227
  try {
225
- await sendActivity({
228
+ await sendDirect({
226
229
  activity,
227
230
  activityId: "https://example.com/activity",
228
231
  keys: [{
@@ -257,7 +260,7 @@ test("sendActivity()", async (t) => {
257
260
  });
258
261
  throw failure;
259
262
  });
260
- const error = await assertRejects(() => sendActivity({
263
+ const error = await assertRejects(() => sendDirect({
261
264
  activity,
262
265
  activityId: "https://example.com/activity",
263
266
  keys: [{
@@ -282,7 +285,7 @@ test("sendActivity()", async (t) => {
282
285
  "actor": "https://example.com/person"
283
286
  };
284
287
  try {
285
- await sendActivity({
288
+ await sendDirect({
286
289
  activity,
287
290
  activityId: "https://example.com/activity",
288
291
  keys: [{
@@ -311,7 +314,7 @@ test("sendActivity()", async (t) => {
311
314
  "actor": "https://example.com/person"
312
315
  };
313
316
  try {
314
- await sendActivity({
317
+ await sendDirect({
315
318
  activity,
316
319
  activityId: "https://example.com/activity",
317
320
  keys: [{
@@ -341,7 +344,7 @@ test("sendActivity()", async (t) => {
341
344
  "actor": "https://example.com/person"
342
345
  };
343
346
  try {
344
- await sendActivity({
347
+ await sendDirect({
345
348
  activity,
346
349
  activityId: "https://example.com/activity",
347
350
  keys: [{
@@ -368,7 +371,7 @@ test("sendActivity()", async (t) => {
368
371
  "actor": "https://example.com/person"
369
372
  };
370
373
  try {
371
- await sendActivity({
374
+ await sendDirect({
372
375
  activity,
373
376
  activityId: "https://example.com/activity",
374
377
  keys: [{
@@ -397,7 +400,7 @@ test("sendActivity()", async (t) => {
397
400
  "actor": "https://example.com/person"
398
401
  };
399
402
  try {
400
- await sendActivity({
403
+ await sendDirect({
401
404
  activity,
402
405
  activityId: "https://example.com/activity",
403
406
  keys: [{
@@ -421,7 +424,7 @@ test("sendActivity() records OpenTelemetry span events", async (t) => {
421
424
  await t.step("successful send", async () => {
422
425
  esm_default.get("https://example.com/", { status: 404 });
423
426
  esm_default.post("https://example.com/inbox", { status: 202 });
424
- await sendActivity({
427
+ await sendDirect({
425
428
  activity: {
426
429
  "@context": "https://www.w3.org/ns/activitystreams",
427
430
  type: "Create",
@@ -504,7 +507,7 @@ test("sendActivity() aligns the LogTape context with its own span", async (t) =>
504
507
  try {
505
508
  esm_default.post("https://example.com/inbox-warn", { status: 202 });
506
509
  await withContext({ requestId: "outer-request-warn" }, async () => {
507
- await sendActivity({
510
+ await sendDirect({
508
511
  ...deliveryParams("https://example.com/activity/warn", "https://example.com/inbox-warn", tracerProvider),
509
512
  keys: [{
510
513
  keyId: ed25519Multikey.id,
@@ -542,7 +545,7 @@ test("sendActivity() aligns the LogTape context with its own span", async (t) =>
542
545
  body: "Internal Server Error"
543
546
  });
544
547
  await withContext(outerContext, async () => {
545
- await assertRejects(() => sendActivity(deliveryParams("https://example.com/activity/failing", "https://example.com/inbox-failing", tracerProvider)), SendActivityError);
548
+ await assertRejects(() => sendDirect(deliveryParams("https://example.com/activity/failing", "https://example.com/inbox-failing", tracerProvider)), SendActivityError);
546
549
  getLogger([
547
550
  "fedify",
548
551
  "federation",
@@ -578,7 +581,7 @@ test("sendActivity() aligns the LogTape context with its own span", async (t) =>
578
581
  try {
579
582
  esm_default.post("https://example.com/inbox-ok", { status: 202 });
580
583
  await withContext(outerContext, async () => {
581
- await sendActivity(deliveryParams("https://example.com/activity/ok", "https://example.com/inbox-ok", tracerProvider));
584
+ await sendDirect(deliveryParams("https://example.com/activity/ok", "https://example.com/inbox-ok", tracerProvider));
582
585
  getLogger([
583
586
  "fedify",
584
587
  "federation",
@@ -619,7 +622,7 @@ test("sendActivity() aligns the LogTape context with its own span", async (t) =>
619
622
  };
620
623
  esm_default.post("https://example.com/inbox-concurrent-a", handler());
621
624
  esm_default.post("https://example.com/inbox-concurrent-b", handler());
622
- const [resultA, resultB] = await Promise.allSettled([sendActivity(deliveryParams("https://example.com/activity/a", "https://example.com/inbox-concurrent-a", tracerProvider)).finally(release), sendActivity(deliveryParams("https://example.com/activity/b", "https://example.com/inbox-concurrent-b", tracerProvider)).finally(release)]);
625
+ const [resultA, resultB] = await Promise.allSettled([sendDirect(deliveryParams("https://example.com/activity/a", "https://example.com/inbox-concurrent-a", tracerProvider)).finally(release), sendDirect(deliveryParams("https://example.com/activity/b", "https://example.com/inbox-concurrent-b", tracerProvider)).finally(release)]);
623
626
  assertEquals(resultA.status, "rejected");
624
627
  assertEquals(resultB.status, "rejected");
625
628
  const spans = exporter.getSpans("activitypub.send_activity");
@@ -647,6 +650,41 @@ test("sendActivity() aligns the LogTape context with its own span", async (t) =>
647
650
  esm_default.hardReset();
648
651
  }
649
652
  });
653
+ test("sendActivity() validates the initial policy before serializing", async () => {
654
+ const [meterProvider, recorder] = createTestMeterProvider();
655
+ let serialized = false;
656
+ await assertRejects(() => sendActivity({
657
+ activity: { toJSON() {
658
+ serialized = true;
659
+ throw new Error("must not serialize");
660
+ } },
661
+ keys: [],
662
+ inbox: new URL("http://127.0.0.1/inbox"),
663
+ meterProvider
664
+ }), UrlError);
665
+ assertFalse(serialized);
666
+ assertEquals(recorder.getMeasurements("activitypub.delivery.sent"), []);
667
+ });
668
+ test("sendActivity() closes the initial session when serialization fails", async () => {
669
+ const [meterProvider, recorder] = createTestMeterProvider();
670
+ const url = "https://example.com/lifecycle/serialization-failure";
671
+ const hops = recordHops(() => ({
672
+ status: 202,
673
+ body: "Accepted"
674
+ }));
675
+ const failure = /* @__PURE__ */ new Error("serialization failed");
676
+ assertEquals(await assertRejects(() => sendActivity({
677
+ activity: { toJSON() {
678
+ throw failure;
679
+ } },
680
+ keys: [],
681
+ inbox: new URL(url),
682
+ meterProvider
683
+ }, { openHop: hops.openHop }), Error, "serialization failed"), failure);
684
+ assertEquals(hops.events, [`open ${url}`, `close ${url}`]);
685
+ assertEquals(recorder.getMeasurements("activitypub.delivery.sent"), []);
686
+ hops.assertInvariants();
687
+ });
650
688
  test("sendActivity() records OpenTelemetry delivery metrics", async (t) => {
651
689
  const [meterProvider, recorder] = createTestMeterProvider();
652
690
  esm_default.spyGlobal();
@@ -887,85 +925,456 @@ for (const signed of [false, true]) {
887
925
  });
888
926
  });
889
927
  }
890
- for (const signed of [false, true]) test({
891
- name: `sendActivity() classifies DNS failures (signed: ${signed})`,
892
- ignore: "Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted",
893
- async fn(t) {
894
- const activity = {
895
- type: "Create",
896
- id: "https://example.com/activity"
928
+ for (const signed of [false, true]) test(`sendActivity() classifies DNS failures (signed: ${signed})`, async (t) => {
929
+ const activity = {
930
+ type: "Create",
931
+ id: "https://example.com/activity"
932
+ };
933
+ const destination = "https://delivery.invalid/inbox";
934
+ const publicInbox = "https://8.8.8.8/inbox";
935
+ const keys = signed ? [{
936
+ privateKey: rsaPrivateKey2,
937
+ keyId: rsaPublicKey2.id
938
+ }] : [];
939
+ for (const result of [
940
+ "throws",
941
+ "empty",
942
+ "cname",
943
+ "private"
944
+ ]) for (const redirected of [false, true]) await t.step(`${result}, redirected: ${redirected}`, async () => {
945
+ const [meterProvider, recorder] = createTestMeterProvider();
946
+ const resolverError = /* @__PURE__ */ new Error("Resolver unavailable");
947
+ const lookups = [];
948
+ const lookup = (hostname) => {
949
+ lookups.push(hostname);
950
+ assertEquals(hostname, "delivery.invalid");
951
+ if (result === "throws") return Promise.reject(resolverError);
952
+ return Promise.resolve(result === "empty" ? [] : [{
953
+ address: result === "private" ? "127.0.0.1" : "alias.invalid.",
954
+ family: 4
955
+ }]);
897
956
  };
898
- const destination = "https://delivery.invalid/inbox";
899
- const publicInbox = "https://8.8.8.8/inbox";
900
- const keys = signed ? [{
901
- privateKey: rsaPrivateKey2,
902
- keyId: rsaPublicKey2.id
903
- }] : [];
904
- for (const result of [
905
- "throws",
906
- "empty",
907
- "cname",
908
- "private"
909
- ]) for (const redirected of [false, true]) await t.step(`${result}, redirected: ${redirected}`, async () => {
910
- const [meterProvider, recorder] = createTestMeterProvider();
911
- const originalLookup = dns.lookup;
912
- const resolverError = /* @__PURE__ */ new Error("Resolver unavailable");
913
- const lookups = [];
914
- dns.lookup = ((hostname, options) => {
915
- lookups.push(hostname);
916
- assertEquals(hostname, "delivery.invalid");
917
- assertEquals(options, { all: true });
918
- if (result === "throws") return Promise.reject(resolverError);
919
- return Promise.resolve(result === "empty" ? [] : [{
920
- address: result === "private" ? "127.0.0.1" : "alias.invalid.",
921
- family: 4
922
- }]);
957
+ try {
958
+ esm_default.mockGlobal().catch(202);
959
+ if (redirected) esm_default.route(publicInbox, {
960
+ status: 307,
961
+ headers: { Location: destination }
923
962
  });
924
- try {
925
- esm_default.mockGlobal().catch(202);
926
- if (redirected) esm_default.route(publicInbox, {
927
- status: 307,
928
- headers: { Location: destination }
929
- });
930
- const send = () => sendActivity({
931
- activity,
932
- activityType: "https://www.w3.org/ns/activitystreams#Create",
933
- meterProvider,
934
- keys,
935
- inbox: new URL(redirected ? publicInbox : destination)
963
+ const send = () => sendActivity({
964
+ activity,
965
+ activityType: "https://www.w3.org/ns/activitystreams#Create",
966
+ meterProvider,
967
+ keys,
968
+ inbox: new URL(redirected ? publicInbox : destination)
969
+ }, { openHop: (url) => openPublicFetch(url, { lookup }) });
970
+ if (result === "private") assertEquals((await assertRejects(send, UrlError)).reason, "disallowed");
971
+ else {
972
+ const error = await assertRejects(send, FetchError);
973
+ assertEquals(error.url.href, destination);
974
+ assertInstanceOf(error.cause, UrlError);
975
+ assertEquals(error.cause.reason, "dns");
976
+ assertEquals(error.cause.cause, result === "throws" ? resolverError : void 0);
977
+ }
978
+ const expectedCount = result === "private" && !redirected ? 0 : 1;
979
+ const sent = recorder.getMeasurements("activitypub.delivery.sent");
980
+ const durations = recorder.getMeasurements("activitypub.delivery.duration");
981
+ assertEquals(sent.length, expectedCount);
982
+ assertEquals(durations.length, expectedCount);
983
+ if (expectedCount === 1) {
984
+ assertEquals(sent[0].value, 1);
985
+ assertEquals(sent[0].attributes, {
986
+ "activitypub.remote.host": redirected ? "8.8.8.8" : "delivery.invalid",
987
+ "activitypub.activity.type": "https://www.w3.org/ns/activitystreams#Create",
988
+ "activitypub.delivery.success": false
936
989
  });
937
- if (result === "private") assertEquals((await assertRejects(send, UrlError)).reason, "disallowed");
938
- else {
939
- const error = await assertRejects(send, FetchError);
940
- assertEquals(error.url.href, destination);
941
- assertInstanceOf(error.cause, UrlError);
942
- assertEquals(error.cause.reason, "dns");
943
- assertEquals(error.cause.cause, result === "throws" ? resolverError : void 0);
944
- }
945
- const expectedCount = result === "private" && !redirected ? 0 : 1;
946
- const sent = recorder.getMeasurements("activitypub.delivery.sent");
947
- const durations = recorder.getMeasurements("activitypub.delivery.duration");
948
- assertEquals(sent.length, expectedCount);
949
- assertEquals(durations.length, expectedCount);
950
- if (expectedCount === 1) {
951
- assertEquals(sent[0].value, 1);
952
- assertEquals(sent[0].attributes, {
953
- "activitypub.remote.host": redirected ? "8.8.8.8" : "delivery.invalid",
954
- "activitypub.activity.type": "https://www.w3.org/ns/activitystreams#Create",
955
- "activitypub.delivery.success": false
956
- });
957
- assertEquals(durations[0].attributes, sent[0].attributes);
958
- assertGreaterOrEqual(durations[0].value, 0);
959
- }
960
- assertEquals(lookups, ["delivery.invalid"]);
961
- assertEquals(esm_default.callHistory.calls(destination).length, 0);
962
- assertEquals(esm_default.callHistory.calls().length, redirected ? 1 : 0);
963
- } finally {
964
- dns.lookup = originalLookup;
965
- esm_default.hardReset();
990
+ assertEquals(durations[0].attributes, sent[0].attributes);
991
+ assertGreaterOrEqual(durations[0].value, 0);
966
992
  }
993
+ assertEquals(lookups, ["delivery.invalid"]);
994
+ assertEquals(esm_default.callHistory.calls(destination).length, 0);
995
+ assertEquals(esm_default.callHistory.calls().length, redirected ? 1 : 0);
996
+ } finally {
997
+ esm_default.hardReset();
998
+ }
999
+ });
1000
+ });
1001
+ const lifecycleActivity = {
1002
+ "@context": "https://www.w3.org/ns/activitystreams",
1003
+ type: "Create",
1004
+ id: "https://example.com/activity",
1005
+ actor: "https://example.com/person"
1006
+ };
1007
+ const signedKeys = [{
1008
+ privateKey: rsaPrivateKey2,
1009
+ keyId: rsaPublicKey2.id
1010
+ }];
1011
+ const unsignedKeys = [{
1012
+ privateKey: ed25519PrivateKey,
1013
+ keyId: ed25519Multikey.id
1014
+ }];
1015
+ test("sendActivity() cancels a successful response body", async (t) => {
1016
+ for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
1017
+ const inbox = "https://example.com/lifecycle/inbox";
1018
+ const hops = recordHops(() => ({
1019
+ status: 202,
1020
+ body: "Accepted"
1021
+ }));
1022
+ await sendActivity({
1023
+ activity: lifecycleActivity,
1024
+ keys: signed ? signedKeys : unsignedKeys,
1025
+ inbox: new URL(inbox)
1026
+ }, { openHop: hops.openHop });
1027
+ assertEquals(hops.events, [
1028
+ `open ${inbox}`,
1029
+ `fetch #1 ${inbox}`,
1030
+ "cancel #1",
1031
+ `close ${inbox}`
1032
+ ]);
1033
+ hops.assertInvariants();
1034
+ });
1035
+ });
1036
+ test("sendActivity() reads at most 1 KiB of an error response body", async (t) => {
1037
+ for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
1038
+ const inbox = "https://example.com/lifecycle/error";
1039
+ const hops = recordHops(() => ({
1040
+ status: 500,
1041
+ body: "x".repeat(4096)
1042
+ }));
1043
+ const error = await assertRejects(() => sendActivity({
1044
+ activity: lifecycleActivity,
1045
+ activityId: lifecycleActivity.id,
1046
+ keys: signed ? signedKeys : unsignedKeys,
1047
+ inbox: new URL(inbox)
1048
+ }, { openHop: hops.openHop }), SendActivityError);
1049
+ assertEquals(error.responseBody, "x".repeat(1024) + "… (truncated)");
1050
+ assert(error.message.includes("… (truncated)"));
1051
+ assertEquals(hops.events, [
1052
+ `open ${inbox}`,
1053
+ `fetch #1 ${inbox}`,
1054
+ "cancel #1",
1055
+ ...signed ? [`fetch #2 ${inbox}`, "cancel #2"] : [],
1056
+ `close ${inbox}`
1057
+ ]);
1058
+ hops.assertInvariants();
1059
+ });
1060
+ });
1061
+ test("sendActivity() closes each redirect hop before opening the next", async (t) => {
1062
+ for (const [signed, status] of [[true, 307], [false, 308]]) await t.step(`signed: ${signed}, ${status}`, async () => {
1063
+ const inbox = "https://example.com/lifecycle/redirect";
1064
+ const next = "https://example.com/lifecycle/redirected";
1065
+ const hops = recordHops((request) => request.url === inbox ? {
1066
+ status,
1067
+ headers: { Location: next },
1068
+ body: "Moved"
1069
+ } : {
1070
+ status: 202,
1071
+ body: "Accepted"
967
1072
  });
1073
+ await sendActivity({
1074
+ activity: lifecycleActivity,
1075
+ keys: signed ? signedKeys : unsignedKeys,
1076
+ inbox: new URL(inbox)
1077
+ }, { openHop: hops.openHop });
1078
+ assertEquals(hops.events, [
1079
+ `open ${inbox}`,
1080
+ `fetch #1 ${inbox}`,
1081
+ "cancel #1",
1082
+ `close ${inbox}`,
1083
+ `open ${next}`,
1084
+ `fetch #2 ${next}`,
1085
+ "cancel #2",
1086
+ `close ${next}`
1087
+ ]);
1088
+ assertEquals(hops.requests[1].method, "POST");
1089
+ assertEquals(await hops.requests[1].text(), JSON.stringify(lifecycleActivity));
1090
+ hops.assertInvariants();
1091
+ });
1092
+ });
1093
+ async function captureLogs(fn) {
1094
+ const records = [];
1095
+ await configure({
1096
+ sinks: { buffer: (record) => records.push(record) },
1097
+ loggers: [{
1098
+ category: "fedify",
1099
+ sinks: ["buffer"],
1100
+ lowestLevel: "debug"
1101
+ }, {
1102
+ category: ["logtape", "meta"],
1103
+ sinks: []
1104
+ }],
1105
+ reset: true
1106
+ });
1107
+ try {
1108
+ await fn();
1109
+ await new Promise((resolve) => setTimeout(resolve, 0));
1110
+ } finally {
1111
+ await reset();
968
1112
  }
1113
+ return records;
1114
+ }
1115
+ function getCancelFailureUrls(records) {
1116
+ return records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}").map((r) => r.properties.url);
1117
+ }
1118
+ test("fetchWithValidatedRedirects() logs a redirect body failing to cancel and follows the redirect", async (t) => {
1119
+ const url = "https://example.com/lifecycle/cancel-failure";
1120
+ const next = "https://example.com/lifecycle/cancel-failure-final";
1121
+ const respond = (_, index) => index === 1 ? {
1122
+ status: 302,
1123
+ headers: { Location: next },
1124
+ body: "Found",
1125
+ failCancel: true
1126
+ } : {
1127
+ status: 202,
1128
+ body: "Accepted"
1129
+ };
1130
+ await t.step("fetchWithValidatedRedirects()", async () => {
1131
+ const hops = recordHops(respond);
1132
+ let result;
1133
+ const records = await captureLogs(async () => {
1134
+ result = await fetchWithValidatedRedirects(new Request(url, {
1135
+ method: "POST",
1136
+ body: "activity"
1137
+ }), hops.openHop, (response) => response.text());
1138
+ });
1139
+ assertEquals(result, "Accepted");
1140
+ assertEquals(hops.events, [
1141
+ `open ${url}`,
1142
+ `fetch #1 ${url}`,
1143
+ "cancel #1",
1144
+ `close ${url}`,
1145
+ `open ${next}`,
1146
+ `fetch #2 ${next}`,
1147
+ "consumed #2",
1148
+ `close ${next}`
1149
+ ]);
1150
+ assertEquals(getCancelFailureUrls(records), [url]);
1151
+ hops.assertInvariants();
1152
+ });
1153
+ for (const signed of [true, false]) await t.step(`signed sendActivity(): ${signed}`, async () => {
1154
+ const hops = recordHops(respond);
1155
+ const records = await captureLogs(() => sendActivity({
1156
+ activity: lifecycleActivity,
1157
+ keys: signed ? signedKeys : unsignedKeys,
1158
+ inbox: new URL(url)
1159
+ }, { openHop: hops.openHop }));
1160
+ assertEquals(hops.events, [
1161
+ `open ${url}`,
1162
+ `fetch #1 ${url}`,
1163
+ "cancel #1",
1164
+ `close ${url}`,
1165
+ `open ${next}`,
1166
+ `fetch #2 ${next}`,
1167
+ "cancel #2",
1168
+ `close ${next}`
1169
+ ]);
1170
+ assertEquals(getCancelFailureUrls(records), [url]);
1171
+ hops.assertInvariants();
1172
+ });
1173
+ });
1174
+ test("fetchWithValidatedRedirects() closes the session when consuming fails", async () => {
1175
+ const url = "https://example.com/lifecycle/consume-failure";
1176
+ const hops = recordHops(() => ({
1177
+ status: 200,
1178
+ body: "x".repeat(1024)
1179
+ }));
1180
+ await assertRejects(() => fetchWithValidatedRedirects(new Request(url, {
1181
+ method: "POST",
1182
+ body: "activity"
1183
+ }), hops.openHop, () => Promise.reject(/* @__PURE__ */ new Error("consume failed"))), Error, "consume failed");
1184
+ assertEquals(hops.events, [
1185
+ `open ${url}`,
1186
+ `fetch #1 ${url}`,
1187
+ "cancel #1 by close",
1188
+ `close ${url}`
1189
+ ]);
1190
+ hops.assertInvariants();
1191
+ });
1192
+ test("sendActivity() logs a successful response body failing to cancel", async (t) => {
1193
+ for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
1194
+ const inbox = "https://example.com/lifecycle/cancel-failure-inbox";
1195
+ const hops = recordHops(() => ({
1196
+ status: 202,
1197
+ body: "Accepted",
1198
+ failCancel: true
1199
+ }));
1200
+ const records = await captureLogs(() => sendActivity({
1201
+ activity: lifecycleActivity,
1202
+ keys: signed ? signedKeys : unsignedKeys,
1203
+ inbox: new URL(inbox)
1204
+ }, { openHop: hops.openHop }));
1205
+ assertEquals(hops.events, [
1206
+ `open ${inbox}`,
1207
+ `fetch #1 ${inbox}`,
1208
+ "cancel #1",
1209
+ `close ${inbox}`
1210
+ ]);
1211
+ assertEquals(getCancelFailureUrls(records), [inbox]);
1212
+ hops.assertInvariants();
1213
+ });
1214
+ });
1215
+ test("sendActivity() is not blocked by a body cancellation that never settles", async (t) => {
1216
+ for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
1217
+ const inbox = "https://example.com/lifecycle/hanging-cancel";
1218
+ const next = "https://example.com/lifecycle/hanging-cancel-final";
1219
+ const hops = recordHops((request) => request.url === inbox ? {
1220
+ status: 302,
1221
+ headers: { Location: next },
1222
+ body: "Found",
1223
+ hangCancel: true
1224
+ } : {
1225
+ status: 202,
1226
+ body: "Accepted",
1227
+ hangCancel: true
1228
+ });
1229
+ await sendActivity({
1230
+ activity: lifecycleActivity,
1231
+ keys: signed ? signedKeys : unsignedKeys,
1232
+ inbox: new URL(inbox)
1233
+ }, { openHop: hops.openHop });
1234
+ assertEquals(hops.events, [
1235
+ `open ${inbox}`,
1236
+ `fetch #1 ${inbox}`,
1237
+ "cancel #1",
1238
+ `close ${inbox}`,
1239
+ `open ${next}`,
1240
+ `fetch #2 ${next}`,
1241
+ "cancel #2",
1242
+ `close ${next}`
1243
+ ]);
1244
+ hops.assertInvariants();
1245
+ });
1246
+ await t.step("truncated error body", async () => {
1247
+ const inbox = "https://example.com/lifecycle/hanging-error";
1248
+ const hops = recordHops(() => ({
1249
+ status: 500,
1250
+ body: "x".repeat(4096),
1251
+ hangCancel: true
1252
+ }));
1253
+ assertEquals((await assertRejects(() => sendActivity({
1254
+ activity: lifecycleActivity,
1255
+ keys: unsignedKeys,
1256
+ inbox: new URL(inbox)
1257
+ }, { openHop: hops.openHop }), SendActivityError)).responseBody, "x".repeat(1024) + "… (truncated)");
1258
+ assertEquals(hops.events, [
1259
+ `open ${inbox}`,
1260
+ `fetch #1 ${inbox}`,
1261
+ "cancel #1",
1262
+ `close ${inbox}`
1263
+ ]);
1264
+ hops.assertInvariants();
1265
+ });
969
1266
  });
1267
+ test("sendActivity() completes with responses cloned by fetch-mock", async (t) => {
1268
+ for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
1269
+ esm_default.spyGlobal();
1270
+ try {
1271
+ esm_default.post("https://example.com/tee/inbox", () => new Response("Moved", {
1272
+ status: 307,
1273
+ headers: { Location: "https://example.com/tee/final" }
1274
+ }));
1275
+ esm_default.post("https://example.com/tee/final", () => new Response("Accepted", { status: 202 }));
1276
+ esm_default.post("https://example.com/tee/error", () => new Response("x".repeat(4096), { status: 404 }));
1277
+ const keys = signed ? signedKeys : unsignedKeys;
1278
+ await sendDirect({
1279
+ activity: lifecycleActivity,
1280
+ keys,
1281
+ inbox: new URL("https://example.com/tee/inbox")
1282
+ });
1283
+ assertEquals(esm_default.callHistory.calls("https://example.com/tee/final").length, 1);
1284
+ const error = await assertRejects(() => sendDirect({
1285
+ activity: lifecycleActivity,
1286
+ keys,
1287
+ inbox: new URL("https://example.com/tee/error")
1288
+ }), SendActivityError);
1289
+ assertEquals(error.statusCode, 404);
1290
+ assertEquals(error.responseBody, "x".repeat(1024) + "… (truncated)");
1291
+ } finally {
1292
+ esm_default.hardReset();
1293
+ }
1294
+ });
1295
+ });
1296
+ function createRecordedResponse(events, index, init, cancelSuffix = () => "") {
1297
+ const bytes = new TextEncoder().encode(init.body ?? "");
1298
+ const chunkSize = init.chunkSize ?? 256;
1299
+ let offset = 0;
1300
+ let settled = false;
1301
+ const body = new ReadableStream({
1302
+ pull(controller) {
1303
+ if (offset >= bytes.length) {
1304
+ settled = true;
1305
+ events.push(`consumed #${index}`);
1306
+ controller.close();
1307
+ return;
1308
+ }
1309
+ controller.enqueue(bytes.slice(offset, offset + chunkSize));
1310
+ offset += chunkSize;
1311
+ },
1312
+ cancel() {
1313
+ settled = true;
1314
+ events.push(`cancel #${index}${cancelSuffix()}`);
1315
+ if (init.failCancel) throw new Error(`cancel #${index} failed`);
1316
+ if (init.hangCancel) return new Promise(() => {});
1317
+ }
1318
+ }, { highWaterMark: 0 });
1319
+ return {
1320
+ response: new Response(body, {
1321
+ status: init.status ?? 200,
1322
+ headers: init.headers
1323
+ }),
1324
+ settled: () => settled
1325
+ };
1326
+ }
1327
+ function recordHops(respond) {
1328
+ const events = [];
1329
+ const requests = [];
1330
+ const unsettledAtClose = [];
1331
+ let opens = 0;
1332
+ let closes = 0;
1333
+ let fetches = 0;
1334
+ return {
1335
+ events,
1336
+ requests,
1337
+ openHop(url) {
1338
+ opens++;
1339
+ events.push(`open ${url}`);
1340
+ const tracked = [];
1341
+ let closed = false;
1342
+ let closing = false;
1343
+ return Promise.resolve({
1344
+ url: new URL(url),
1345
+ async fetch(input, init) {
1346
+ if (closed) throw new TypeError("The session is closed.");
1347
+ const request = new Request(input instanceof Request ? input.clone() : input, init);
1348
+ const index = ++fetches;
1349
+ requests.push(request);
1350
+ events.push(`fetch #${index} ${request.url}`);
1351
+ const recorded = createRecordedResponse(events, index, await respond(request, index), () => closing ? " by close" : "");
1352
+ tracked.push({
1353
+ ...recorded,
1354
+ index
1355
+ });
1356
+ return recorded.response;
1357
+ },
1358
+ close() {
1359
+ if (closed) return Promise.resolve();
1360
+ closed = true;
1361
+ closes++;
1362
+ closing = true;
1363
+ for (const { response, settled, index } of tracked) {
1364
+ if (settled()) continue;
1365
+ if (response.body?.locked) unsettledAtClose.push(index);
1366
+ else response.body?.cancel().catch(() => {});
1367
+ }
1368
+ events.push(`close ${url}`);
1369
+ return Promise.resolve();
1370
+ }
1371
+ });
1372
+ },
1373
+ assertInvariants() {
1374
+ assertEquals(opens, closes);
1375
+ assertEquals(unsettledAtClose, []);
1376
+ }
1377
+ };
1378
+ }
970
1379
  //#endregion
971
1380
  export {};