@fedify/fedify 2.1.29 → 2.1.31

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 (86) hide show
  1. package/dist/{builder-Mg2G3qj8.mjs → builder-isMRh_I6.mjs} +3 -3
  2. package/dist/{client-CAM_bQXx.d.cts → client-BIBzqEIp.d.cts} +0 -1
  3. package/dist/{client-CjDMsn4a.mjs → client-BZr7TGyu.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-DLLaDau8.d.ts → context-C0uGVzgk.d.ts} +28 -2
  9. package/dist/{context-BNwv9Q1v.d.cts → context-CLaLxi49.d.cts} +28 -2
  10. package/dist/{deno-BiYv5RT5.mjs → deno-CiaJqTcu.mjs} +1 -1
  11. package/dist/docloader-DOrwedT7.mjs +124 -0
  12. package/dist/federation/builder.test.mjs +1 -1
  13. package/dist/federation/handler.test.mjs +6 -6
  14. package/dist/federation/idempotency.test.mjs +2 -2
  15. package/dist/federation/inbox.test.mjs +1 -1
  16. package/dist/federation/keycache.test.mjs +18 -7
  17. package/dist/federation/middleware.test.mjs +380 -24
  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 +445 -70
  23. package/dist/federation/temporal.test.mjs +1 -1
  24. package/dist/federation/webfinger.test.mjs +1 -1
  25. package/dist/{http-Dl00HXr4.d.ts → http-9IFUxdkp.d.cts} +1 -2
  26. package/dist/{http-C1ga9WQu.mjs → http-A9mP2uM7.mjs} +194 -96
  27. package/dist/{http-Cginz7L0.cjs → http-CRiVlZd5.cjs} +188 -105
  28. package/dist/{http-B3rSb2GQ.js → http-Crml3gei.js} +187 -104
  29. package/dist/{http-DqZhyEso.d.cts → http-SbfezERn.d.ts} +1 -2
  30. package/dist/{inbox-BWzL0xMU.mjs → inbox-KESLudq4.mjs} +1 -1
  31. package/dist/{key-O-ATwIGS.mjs → key-Dg6-DV_O.mjs} +17 -3
  32. package/dist/{keycache-RFlZObU3.mjs → keycache-DahCaG_m.mjs} +1 -1
  33. package/dist/{kv-cache-BdylAE9w.cjs → kv-cache-Cqmm-yQa.cjs} +59 -15
  34. package/dist/{kv-cache-8Kg2CHiD.js → kv-cache-DQcyZL5T.js} +60 -16
  35. package/dist/{ld-C-fCGM-z.mjs → ld-COjDN6Nb.mjs} +2 -2
  36. package/dist/{middleware-CVRFS7PZ.cjs → middleware-BbgR7xOI.cjs} +185 -60
  37. package/dist/{middleware-VvrqPwHL.cjs → middleware-BtDQUCcz.cjs} +1 -1
  38. package/dist/{middleware-C9UCWOqw.mjs → middleware-COFlKeYn.mjs} +79 -39
  39. package/dist/{middleware-CxgFiKI3.mjs → middleware-CuQvczZd.mjs} +1 -1
  40. package/dist/{middleware-41MkquTV.js → middleware-DIW5S_5L.js} +185 -60
  41. package/dist/{mod-DNEgTI8M.d.ts → mod-BRnwh2oy.d.ts} +2 -7
  42. package/dist/{mod-BLDHhuW0.d.ts → mod-C7w-QVUv.d.ts} +1 -1
  43. package/dist/{mod-CWJNbyju.d.ts → mod-CFRocFeQ.d.ts} +1 -1
  44. package/dist/{mod-BmlfRBmU.d.cts → mod-CGUH8Ujp.d.cts} +1 -1
  45. package/dist/{mod-Cn9GJ5ex.d.cts → mod-Co7LEK9X.d.cts} +2 -7
  46. package/dist/{mod-DiuFTYUt.d.cts → mod-LOEOKbYd.d.cts} +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-C8IVASD3.mjs → owner-CFHq61cq.mjs} +2 -2
  63. package/dist/{proof-BbHMAkPG.cjs → proof-0JkSC_pT.cjs} +1 -1
  64. package/dist/{proof-cGfRPTiV.js → proof-DzksJFK7.js} +1 -1
  65. package/dist/{proof-CUdcBgvg.mjs → proof-KSHlbPCG.mjs} +2 -2
  66. package/dist/{send-BoV4dyVO.mjs → send-DaDe2YCb.mjs} +117 -32
  67. package/dist/sig/http.test.mjs +507 -3
  68. package/dist/sig/key.test.mjs +197 -3
  69. package/dist/sig/ld.test.mjs +47 -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 +46 -1
  75. package/dist/sig/proof.test.mjs +52 -1
  76. package/dist/{temporal-BJlnku_d.mjs → temporal-DNGH8o2E.mjs} +1 -1
  77. package/dist/testing/mod.d.mts +26 -1
  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 +380 -32
  81. package/dist/utils/mod.cjs +1 -1
  82. package/dist/utils/mod.d.cts +1 -1
  83. package/dist/utils/mod.d.ts +1 -1
  84. package/dist/utils/mod.js +1 -1
  85. package/package.json +7 -7
  86. package/dist/docloader-DrxXBFL8.mjs +0 -80
@@ -8,15 +8,19 @@ import { r as assertFalse } from "../assert_throws-D4DNWrux.mjs";
8
8
  import { n as assertInstanceOf, t as assert } from "../assert-BhQd8QHP.mjs";
9
9
  import { t as assertNotEquals } from "../assert_not_equals-BkxW5QJI.mjs";
10
10
  import { t as assertRejects } from "../assert_rejects-WyYSTDDX.mjs";
11
- import { l as verifyRequest } from "../http-C1ga9WQu.mjs";
11
+ import { u as verifyRequest } from "../http-A9mP2uM7.mjs";
12
12
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-CtZLJq76.mjs";
13
- import { t as doesActorOwnKey } from "../owner-C8IVASD3.mjs";
14
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "../send-BoV4dyVO.mjs";
13
+ import { t as doesActorOwnKey } from "../owner-CFHq61cq.mjs";
14
+ import { i as sendActivity, n as extractInboxes, r as fetchWithValidatedRedirects, t as SendActivityError } from "../send-DaDe2YCb.mjs";
15
15
  import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
16
16
  import { Activity, Application, Endpoints, Group, Person, Service } from "@fedify/vocab";
17
17
  import { FetchError, UrlError } from "@fedify/vocab-runtime";
18
- import dns from "node:dns/promises";
18
+ import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
19
+ import { configure, reset } from "@logtape/logtape";
19
20
  //#region src/federation/send.test.ts
21
+ function sendDirect(options) {
22
+ return sendActivity(options, { openHop: openDirectFetch });
23
+ }
20
24
  test("extractInboxes()", () => {
21
25
  const recipients = [
22
26
  new Person({
@@ -152,7 +156,7 @@ test("sendActivity()", async (t) => {
152
156
  "id": "https://example.com/activity",
153
157
  "actor": "https://example.com/person"
154
158
  };
155
- await sendActivity({
159
+ await sendDirect({
156
160
  activity,
157
161
  keys: [{
158
162
  privateKey: rsaPrivateKey2,
@@ -168,7 +172,7 @@ test("sendActivity()", async (t) => {
168
172
  assertEquals(request?.headers.get("Content-Type"), "application/activity+json");
169
173
  assertEquals(request?.headers.get("X-Test"), "test");
170
174
  httpSigVerified = null;
171
- await assertRejects(() => sendActivity({
175
+ await assertRejects(() => sendDirect({
172
176
  activity: {
173
177
  ...activity,
174
178
  actor: "https://example.com/person2"
@@ -196,7 +200,7 @@ test("sendActivity()", async (t) => {
196
200
  "id": "https://example.com/activity",
197
201
  "actor": "https://example.com/person"
198
202
  };
199
- await assertRejects(() => sendActivity({
203
+ await assertRejects(() => sendDirect({
200
204
  activity,
201
205
  activityId: "https://example.com/activity",
202
206
  keys: [{
@@ -214,7 +218,7 @@ test("sendActivity()", async (t) => {
214
218
  "actor": "https://example.com/person"
215
219
  };
216
220
  try {
217
- await sendActivity({
221
+ await sendDirect({
218
222
  activity,
219
223
  activityId: "https://example.com/activity",
220
224
  keys: [{
@@ -243,7 +247,7 @@ test("sendActivity()", async (t) => {
243
247
  "actor": "https://example.com/person"
244
248
  };
245
249
  try {
246
- await sendActivity({
250
+ await sendDirect({
247
251
  activity,
248
252
  activityId: "https://example.com/activity",
249
253
  keys: [{
@@ -272,7 +276,7 @@ test("sendActivity()", async (t) => {
272
276
  "actor": "https://example.com/person"
273
277
  };
274
278
  try {
275
- await sendActivity({
279
+ await sendDirect({
276
280
  activity,
277
281
  activityId: "https://example.com/activity",
278
282
  keys: [{
@@ -302,7 +306,7 @@ test("sendActivity()", async (t) => {
302
306
  "actor": "https://example.com/person"
303
307
  };
304
308
  try {
305
- await sendActivity({
309
+ await sendDirect({
306
310
  activity,
307
311
  activityId: "https://example.com/activity",
308
312
  keys: [{
@@ -329,7 +333,7 @@ test("sendActivity()", async (t) => {
329
333
  "actor": "https://example.com/person"
330
334
  };
331
335
  try {
332
- await sendActivity({
336
+ await sendDirect({
333
337
  activity,
334
338
  activityId: "https://example.com/activity",
335
339
  keys: [{
@@ -358,7 +362,7 @@ test("sendActivity()", async (t) => {
358
362
  "actor": "https://example.com/person"
359
363
  };
360
364
  try {
361
- await sendActivity({
365
+ await sendDirect({
362
366
  activity,
363
367
  activityId: "https://example.com/activity",
364
368
  keys: [{
@@ -382,7 +386,7 @@ test("sendActivity() records OpenTelemetry span events", async (t) => {
382
386
  await t.step("successful send", async () => {
383
387
  esm_default.get("https://example.com/", { status: 404 });
384
388
  esm_default.post("https://example.com/inbox", { status: 202 });
385
- await sendActivity({
389
+ await sendDirect({
386
390
  activity: {
387
391
  "@context": "https://www.w3.org/ns/activitystreams",
388
392
  type: "Create",
@@ -549,67 +553,438 @@ for (const signed of [false, true]) {
549
553
  });
550
554
  });
551
555
  }
552
- for (const signed of [false, true]) test({
553
- name: `sendActivity() classifies DNS failures (signed: ${signed})`,
554
- ignore: "Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted",
555
- async fn(t) {
556
- const activity = {
557
- type: "Create",
558
- id: "https://example.com/activity"
556
+ for (const signed of [false, true]) test(`sendActivity() classifies DNS failures (signed: ${signed})`, async (t) => {
557
+ const activity = {
558
+ type: "Create",
559
+ id: "https://example.com/activity"
560
+ };
561
+ const destination = "https://delivery.invalid/inbox";
562
+ const publicInbox = "https://8.8.8.8/inbox";
563
+ const keys = signed ? [{
564
+ privateKey: rsaPrivateKey2,
565
+ keyId: rsaPublicKey2.id
566
+ }] : [];
567
+ for (const result of [
568
+ "throws",
569
+ "empty",
570
+ "cname",
571
+ "private"
572
+ ]) for (const redirected of [false, true]) await t.step(`${result}, redirected: ${redirected}`, async () => {
573
+ const resolverError = /* @__PURE__ */ new Error("Resolver unavailable");
574
+ const lookups = [];
575
+ const lookup = (hostname) => {
576
+ lookups.push(hostname);
577
+ assertEquals(hostname, "delivery.invalid");
578
+ if (result === "throws") return Promise.reject(resolverError);
579
+ return Promise.resolve(result === "empty" ? [] : [{
580
+ address: result === "private" ? "127.0.0.1" : "alias.invalid.",
581
+ family: 4
582
+ }]);
559
583
  };
560
- const destination = "https://delivery.invalid/inbox";
561
- const publicInbox = "https://8.8.8.8/inbox";
562
- const keys = signed ? [{
563
- privateKey: rsaPrivateKey2,
564
- keyId: rsaPublicKey2.id
565
- }] : [];
566
- for (const result of [
567
- "throws",
568
- "empty",
569
- "cname",
570
- "private"
571
- ]) for (const redirected of [false, true]) await t.step(`${result}, redirected: ${redirected}`, async () => {
572
- const originalLookup = dns.lookup;
573
- const resolverError = /* @__PURE__ */ new Error("Resolver unavailable");
574
- const lookups = [];
575
- dns.lookup = ((hostname, options) => {
576
- lookups.push(hostname);
577
- assertEquals(hostname, "delivery.invalid");
578
- assertEquals(options, { all: true });
579
- if (result === "throws") return Promise.reject(resolverError);
580
- return Promise.resolve(result === "empty" ? [] : [{
581
- address: result === "private" ? "127.0.0.1" : "alias.invalid.",
582
- family: 4
583
- }]);
584
+ try {
585
+ esm_default.mockGlobal().catch(202);
586
+ if (redirected) esm_default.route(publicInbox, {
587
+ status: 307,
588
+ headers: { Location: destination }
584
589
  });
585
- try {
586
- esm_default.mockGlobal().catch(202);
587
- if (redirected) esm_default.route(publicInbox, {
588
- status: 307,
589
- headers: { Location: destination }
590
- });
591
- const send = () => sendActivity({
592
- activity,
593
- keys,
594
- inbox: new URL(redirected ? publicInbox : destination)
595
- });
596
- if (result === "private") assertEquals((await assertRejects(send, UrlError)).reason, "disallowed");
597
- else {
598
- const error = await assertRejects(send, FetchError);
599
- assertEquals(error.url.href, destination);
600
- assertInstanceOf(error.cause, UrlError);
601
- assertEquals(error.cause.reason, "dns");
602
- assertEquals(error.cause.cause, result === "throws" ? resolverError : void 0);
603
- }
604
- assertEquals(lookups, ["delivery.invalid"]);
605
- assertEquals(esm_default.callHistory.calls(destination).length, 0);
606
- assertEquals(esm_default.callHistory.calls().length, redirected ? 1 : 0);
607
- } finally {
608
- dns.lookup = originalLookup;
609
- esm_default.hardReset();
590
+ const send = () => sendActivity({
591
+ activity,
592
+ keys,
593
+ inbox: new URL(redirected ? publicInbox : destination)
594
+ }, { openHop: (url) => openPublicFetch(url, { lookup }) });
595
+ if (result === "private") assertEquals((await assertRejects(send, UrlError)).reason, "disallowed");
596
+ else {
597
+ const error = await assertRejects(send, FetchError);
598
+ assertEquals(error.url.href, destination);
599
+ assertInstanceOf(error.cause, UrlError);
600
+ assertEquals(error.cause.reason, "dns");
601
+ assertEquals(error.cause.cause, result === "throws" ? resolverError : void 0);
610
602
  }
603
+ assertEquals(lookups, ["delivery.invalid"]);
604
+ assertEquals(esm_default.callHistory.calls(destination).length, 0);
605
+ assertEquals(esm_default.callHistory.calls().length, redirected ? 1 : 0);
606
+ } finally {
607
+ esm_default.hardReset();
608
+ }
609
+ });
610
+ });
611
+ const lifecycleActivity = {
612
+ "@context": "https://www.w3.org/ns/activitystreams",
613
+ type: "Create",
614
+ id: "https://example.com/activity",
615
+ actor: "https://example.com/person"
616
+ };
617
+ const signedKeys = [{
618
+ privateKey: rsaPrivateKey2,
619
+ keyId: rsaPublicKey2.id
620
+ }];
621
+ const unsignedKeys = [{
622
+ privateKey: ed25519PrivateKey,
623
+ keyId: ed25519Multikey.id
624
+ }];
625
+ test("sendActivity() cancels a successful response body", async (t) => {
626
+ for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
627
+ const inbox = "https://example.com/lifecycle/inbox";
628
+ const hops = recordHops(() => ({
629
+ status: 202,
630
+ body: "Accepted"
631
+ }));
632
+ await sendActivity({
633
+ activity: lifecycleActivity,
634
+ keys: signed ? signedKeys : unsignedKeys,
635
+ inbox: new URL(inbox)
636
+ }, { openHop: hops.openHop });
637
+ assertEquals(hops.events, [
638
+ `open ${inbox}`,
639
+ `fetch #1 ${inbox}`,
640
+ "cancel #1",
641
+ `close ${inbox}`
642
+ ]);
643
+ hops.assertInvariants();
644
+ });
645
+ });
646
+ test("sendActivity() reads at most 1 KiB of an error response body", async (t) => {
647
+ for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
648
+ const inbox = "https://example.com/lifecycle/error";
649
+ const hops = recordHops(() => ({
650
+ status: 500,
651
+ body: "x".repeat(4096)
652
+ }));
653
+ const error = await assertRejects(() => sendActivity({
654
+ activity: lifecycleActivity,
655
+ activityId: lifecycleActivity.id,
656
+ keys: signed ? signedKeys : unsignedKeys,
657
+ inbox: new URL(inbox)
658
+ }, { openHop: hops.openHop }), SendActivityError);
659
+ assertEquals(error.responseBody, "x".repeat(1024) + "… (truncated)");
660
+ assert(error.message.includes("… (truncated)"));
661
+ assertEquals(hops.events, [
662
+ `open ${inbox}`,
663
+ `fetch #1 ${inbox}`,
664
+ "cancel #1",
665
+ ...signed ? [`fetch #2 ${inbox}`, "cancel #2"] : [],
666
+ `close ${inbox}`
667
+ ]);
668
+ hops.assertInvariants();
669
+ });
670
+ });
671
+ test("sendActivity() closes each redirect hop before opening the next", async (t) => {
672
+ for (const [signed, status] of [[true, 307], [false, 308]]) await t.step(`signed: ${signed}, ${status}`, async () => {
673
+ const inbox = "https://example.com/lifecycle/redirect";
674
+ const next = "https://example.com/lifecycle/redirected";
675
+ const hops = recordHops((request) => request.url === inbox ? {
676
+ status,
677
+ headers: { Location: next },
678
+ body: "Moved"
679
+ } : {
680
+ status: 202,
681
+ body: "Accepted"
611
682
  });
683
+ await sendActivity({
684
+ activity: lifecycleActivity,
685
+ keys: signed ? signedKeys : unsignedKeys,
686
+ inbox: new URL(inbox)
687
+ }, { openHop: hops.openHop });
688
+ assertEquals(hops.events, [
689
+ `open ${inbox}`,
690
+ `fetch #1 ${inbox}`,
691
+ "cancel #1",
692
+ `close ${inbox}`,
693
+ `open ${next}`,
694
+ `fetch #2 ${next}`,
695
+ "cancel #2",
696
+ `close ${next}`
697
+ ]);
698
+ assertEquals(hops.requests[1].method, "POST");
699
+ assertEquals(await hops.requests[1].text(), JSON.stringify(lifecycleActivity));
700
+ hops.assertInvariants();
701
+ });
702
+ });
703
+ async function captureLogs(fn) {
704
+ const records = [];
705
+ await configure({
706
+ sinks: { buffer: (record) => records.push(record) },
707
+ loggers: [{
708
+ category: "fedify",
709
+ sinks: ["buffer"],
710
+ lowestLevel: "debug"
711
+ }, {
712
+ category: ["logtape", "meta"],
713
+ sinks: []
714
+ }],
715
+ reset: true
716
+ });
717
+ try {
718
+ await fn();
719
+ await new Promise((resolve) => setTimeout(resolve, 0));
720
+ } finally {
721
+ await reset();
612
722
  }
723
+ return records;
724
+ }
725
+ function getCancelFailureUrls(records) {
726
+ return records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}").map((r) => r.properties.url);
727
+ }
728
+ test("fetchWithValidatedRedirects() logs a redirect body failing to cancel and follows the redirect", async (t) => {
729
+ const url = "https://example.com/lifecycle/cancel-failure";
730
+ const next = "https://example.com/lifecycle/cancel-failure-final";
731
+ const respond = (_, index) => index === 1 ? {
732
+ status: 302,
733
+ headers: { Location: next },
734
+ body: "Found",
735
+ failCancel: true
736
+ } : {
737
+ status: 202,
738
+ body: "Accepted"
739
+ };
740
+ await t.step("fetchWithValidatedRedirects()", async () => {
741
+ const hops = recordHops(respond);
742
+ let result;
743
+ const records = await captureLogs(async () => {
744
+ result = await fetchWithValidatedRedirects(new Request(url, {
745
+ method: "POST",
746
+ body: "activity"
747
+ }), hops.openHop, (response) => response.text());
748
+ });
749
+ assertEquals(result, "Accepted");
750
+ assertEquals(hops.events, [
751
+ `open ${url}`,
752
+ `fetch #1 ${url}`,
753
+ "cancel #1",
754
+ `close ${url}`,
755
+ `open ${next}`,
756
+ `fetch #2 ${next}`,
757
+ "consumed #2",
758
+ `close ${next}`
759
+ ]);
760
+ assertEquals(getCancelFailureUrls(records), [url]);
761
+ hops.assertInvariants();
762
+ });
763
+ for (const signed of [true, false]) await t.step(`signed sendActivity(): ${signed}`, async () => {
764
+ const hops = recordHops(respond);
765
+ const records = await captureLogs(() => sendActivity({
766
+ activity: lifecycleActivity,
767
+ keys: signed ? signedKeys : unsignedKeys,
768
+ inbox: new URL(url)
769
+ }, { openHop: hops.openHop }));
770
+ assertEquals(hops.events, [
771
+ `open ${url}`,
772
+ `fetch #1 ${url}`,
773
+ "cancel #1",
774
+ `close ${url}`,
775
+ `open ${next}`,
776
+ `fetch #2 ${next}`,
777
+ "cancel #2",
778
+ `close ${next}`
779
+ ]);
780
+ assertEquals(getCancelFailureUrls(records), [url]);
781
+ hops.assertInvariants();
782
+ });
783
+ });
784
+ test("fetchWithValidatedRedirects() closes the session when consuming fails", async () => {
785
+ const url = "https://example.com/lifecycle/consume-failure";
786
+ const hops = recordHops(() => ({
787
+ status: 200,
788
+ body: "x".repeat(1024)
789
+ }));
790
+ await assertRejects(() => fetchWithValidatedRedirects(new Request(url, {
791
+ method: "POST",
792
+ body: "activity"
793
+ }), hops.openHop, () => Promise.reject(/* @__PURE__ */ new Error("consume failed"))), Error, "consume failed");
794
+ assertEquals(hops.events, [
795
+ `open ${url}`,
796
+ `fetch #1 ${url}`,
797
+ "cancel #1 by close",
798
+ `close ${url}`
799
+ ]);
800
+ hops.assertInvariants();
801
+ });
802
+ test("sendActivity() logs a successful response body failing to cancel", async (t) => {
803
+ for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
804
+ const inbox = "https://example.com/lifecycle/cancel-failure-inbox";
805
+ const hops = recordHops(() => ({
806
+ status: 202,
807
+ body: "Accepted",
808
+ failCancel: true
809
+ }));
810
+ const records = await captureLogs(() => sendActivity({
811
+ activity: lifecycleActivity,
812
+ keys: signed ? signedKeys : unsignedKeys,
813
+ inbox: new URL(inbox)
814
+ }, { openHop: hops.openHop }));
815
+ assertEquals(hops.events, [
816
+ `open ${inbox}`,
817
+ `fetch #1 ${inbox}`,
818
+ "cancel #1",
819
+ `close ${inbox}`
820
+ ]);
821
+ assertEquals(getCancelFailureUrls(records), [inbox]);
822
+ hops.assertInvariants();
823
+ });
824
+ });
825
+ test("sendActivity() is not blocked by a body cancellation that never settles", async (t) => {
826
+ for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
827
+ const inbox = "https://example.com/lifecycle/hanging-cancel";
828
+ const next = "https://example.com/lifecycle/hanging-cancel-final";
829
+ const hops = recordHops((request) => request.url === inbox ? {
830
+ status: 302,
831
+ headers: { Location: next },
832
+ body: "Found",
833
+ hangCancel: true
834
+ } : {
835
+ status: 202,
836
+ body: "Accepted",
837
+ hangCancel: true
838
+ });
839
+ await sendActivity({
840
+ activity: lifecycleActivity,
841
+ keys: signed ? signedKeys : unsignedKeys,
842
+ inbox: new URL(inbox)
843
+ }, { openHop: hops.openHop });
844
+ assertEquals(hops.events, [
845
+ `open ${inbox}`,
846
+ `fetch #1 ${inbox}`,
847
+ "cancel #1",
848
+ `close ${inbox}`,
849
+ `open ${next}`,
850
+ `fetch #2 ${next}`,
851
+ "cancel #2",
852
+ `close ${next}`
853
+ ]);
854
+ hops.assertInvariants();
855
+ });
856
+ await t.step("truncated error body", async () => {
857
+ const inbox = "https://example.com/lifecycle/hanging-error";
858
+ const hops = recordHops(() => ({
859
+ status: 500,
860
+ body: "x".repeat(4096),
861
+ hangCancel: true
862
+ }));
863
+ assertEquals((await assertRejects(() => sendActivity({
864
+ activity: lifecycleActivity,
865
+ keys: unsignedKeys,
866
+ inbox: new URL(inbox)
867
+ }, { openHop: hops.openHop }), SendActivityError)).responseBody, "x".repeat(1024) + "… (truncated)");
868
+ assertEquals(hops.events, [
869
+ `open ${inbox}`,
870
+ `fetch #1 ${inbox}`,
871
+ "cancel #1",
872
+ `close ${inbox}`
873
+ ]);
874
+ hops.assertInvariants();
875
+ });
613
876
  });
877
+ test("sendActivity() completes with responses cloned by fetch-mock", async (t) => {
878
+ for (const signed of [true, false]) await t.step(`signed: ${signed}`, async () => {
879
+ esm_default.spyGlobal();
880
+ try {
881
+ esm_default.post("https://example.com/tee/inbox", () => new Response("Moved", {
882
+ status: 307,
883
+ headers: { Location: "https://example.com/tee/final" }
884
+ }));
885
+ esm_default.post("https://example.com/tee/final", () => new Response("Accepted", { status: 202 }));
886
+ esm_default.post("https://example.com/tee/error", () => new Response("x".repeat(4096), { status: 404 }));
887
+ const keys = signed ? signedKeys : unsignedKeys;
888
+ await sendDirect({
889
+ activity: lifecycleActivity,
890
+ keys,
891
+ inbox: new URL("https://example.com/tee/inbox")
892
+ });
893
+ assertEquals(esm_default.callHistory.calls("https://example.com/tee/final").length, 1);
894
+ const error = await assertRejects(() => sendDirect({
895
+ activity: lifecycleActivity,
896
+ keys,
897
+ inbox: new URL("https://example.com/tee/error")
898
+ }), SendActivityError);
899
+ assertEquals(error.statusCode, 404);
900
+ assertEquals(error.responseBody, "x".repeat(1024) + "… (truncated)");
901
+ } finally {
902
+ esm_default.hardReset();
903
+ }
904
+ });
905
+ });
906
+ function createRecordedResponse(events, index, init, cancelSuffix = () => "") {
907
+ const bytes = new TextEncoder().encode(init.body ?? "");
908
+ const chunkSize = init.chunkSize ?? 256;
909
+ let offset = 0;
910
+ let settled = false;
911
+ const body = new ReadableStream({
912
+ pull(controller) {
913
+ if (offset >= bytes.length) {
914
+ settled = true;
915
+ events.push(`consumed #${index}`);
916
+ controller.close();
917
+ return;
918
+ }
919
+ controller.enqueue(bytes.slice(offset, offset + chunkSize));
920
+ offset += chunkSize;
921
+ },
922
+ cancel() {
923
+ settled = true;
924
+ events.push(`cancel #${index}${cancelSuffix()}`);
925
+ if (init.failCancel) throw new Error(`cancel #${index} failed`);
926
+ if (init.hangCancel) return new Promise(() => {});
927
+ }
928
+ }, { highWaterMark: 0 });
929
+ return {
930
+ response: new Response(body, {
931
+ status: init.status ?? 200,
932
+ headers: init.headers
933
+ }),
934
+ settled: () => settled
935
+ };
936
+ }
937
+ function recordHops(respond) {
938
+ const events = [];
939
+ const requests = [];
940
+ const unsettledAtClose = [];
941
+ let opens = 0;
942
+ let closes = 0;
943
+ let fetches = 0;
944
+ return {
945
+ events,
946
+ requests,
947
+ openHop(url) {
948
+ opens++;
949
+ events.push(`open ${url}`);
950
+ const tracked = [];
951
+ let closed = false;
952
+ let closing = false;
953
+ return Promise.resolve({
954
+ url: new URL(url),
955
+ async fetch(input, init) {
956
+ if (closed) throw new TypeError("The session is closed.");
957
+ const request = new Request(input instanceof Request ? input.clone() : input, init);
958
+ const index = ++fetches;
959
+ requests.push(request);
960
+ events.push(`fetch #${index} ${request.url}`);
961
+ const recorded = createRecordedResponse(events, index, await respond(request, index), () => closing ? " by close" : "");
962
+ tracked.push({
963
+ ...recorded,
964
+ index
965
+ });
966
+ return recorded.response;
967
+ },
968
+ close() {
969
+ if (closed) return Promise.resolve();
970
+ closed = true;
971
+ closes++;
972
+ closing = true;
973
+ for (const { response, settled, index } of tracked) {
974
+ if (settled()) continue;
975
+ if (response.body?.locked) unsettledAtClose.push(index);
976
+ else response.body?.cancel().catch(() => {});
977
+ }
978
+ events.push(`close ${url}`);
979
+ return Promise.resolve();
980
+ }
981
+ });
982
+ },
983
+ assertInvariants() {
984
+ assertEquals(opens, closes);
985
+ assertEquals(unsettledAtClose, []);
986
+ }
987
+ };
988
+ }
614
989
  //#endregion
615
990
  export {};
@@ -4,7 +4,7 @@ globalThis.addEventListener = () => {};
4
4
  import "../std__assert-DflMZha5.mjs";
5
5
  import { r as assertFalse } from "../assert_throws-D4DNWrux.mjs";
6
6
  import { t as assert } from "../assert-BhQd8QHP.mjs";
7
- import { t as hasMalformedKnownTemporalLiteral } from "../temporal-BJlnku_d.mjs";
7
+ import { t as hasMalformedKnownTemporalLiteral } from "../temporal-DNGH8o2E.mjs";
8
8
  import { test } from "@fedify/fixture";
9
9
  //#region src/federation/temporal.test.ts
10
10
  test("hasMalformedKnownTemporalLiteral() detects expanded proof timestamps", async () => {
@@ -5,7 +5,7 @@ import { n as createRequestContext } from "../context-Juj6bdHC.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
6
6
  import "../std__assert-DflMZha5.mjs";
7
7
  import { t as MemoryKvStore } from "../kv-HvthQAHr.mjs";
8
- import { a as createFederation, o as handleWebFinger } from "../middleware-C9UCWOqw.mjs";
8
+ import { a as createFederation, o as handleWebFinger } from "../middleware-COFlKeYn.mjs";
9
9
  import { test } from "@fedify/fixture";
10
10
  import { Image, Link, Person } from "@fedify/vocab";
11
11
  //#region src/federation/webfinger.test.ts
@@ -1,8 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { Actor, CryptographicKey, Multikey } from "@fedify/vocab";
3
- import { TracerProvider } from "@opentelemetry/api";
4
3
  import { DocumentLoader } from "@fedify/vocab-runtime";
5
-
4
+ import { TracerProvider } from "@opentelemetry/api";
6
5
  //#region src/sig/key.d.ts
7
6
  /**
8
7
  * Generates a key pair which is appropriate for Fedify.