@fedify/fedify 2.2.19 → 2.2.20

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 (84) hide show
  1. package/dist/{builder-UmydCxtK.mjs → builder-BolQ56ab.mjs} +2 -2
  2. package/dist/{client-CAM_bQXx.d.cts → client-BIBzqEIp.d.cts} +0 -1
  3. package/dist/{client-9F_u23dc.mjs → client-DhxaVgOv.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-tJbL_Hkw.d.cts → context-CFQGaq5x.d.cts} +28 -2
  9. package/dist/{context-OkwEpc3-.d.ts → context-DdbLVV_k.d.ts} +28 -2
  10. package/dist/{deno-CD-8UlUi.mjs → deno-BUj4roJc.mjs} +1 -1
  11. package/dist/docloader-CbtF_0cD.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/middleware.test.mjs +389 -28
  17. package/dist/federation/mod.cjs +1 -1
  18. package/dist/federation/mod.d.cts +2 -2
  19. package/dist/federation/mod.d.ts +2 -2
  20. package/dist/federation/mod.js +1 -1
  21. package/dist/federation/send.test.mjs +445 -70
  22. package/dist/federation/temporal.test.mjs +1 -1
  23. package/dist/federation/webfinger.test.mjs +1 -1
  24. package/dist/{http-Dl00HXr4.d.ts → http-9IFUxdkp.d.cts} +1 -2
  25. package/dist/{http-DJK2udgX.js → http-C5g_VrA_.js} +172 -103
  26. package/dist/{http-BBBUbwKh.mjs → http-CQMVisM0.mjs} +195 -97
  27. package/dist/{http-DrUG4fPS.cjs → http-D8I7LBfx.cjs} +173 -104
  28. package/dist/{http-DqZhyEso.d.cts → http-SbfezERn.d.ts} +1 -2
  29. package/dist/{inbox-Dca_0fwc.mjs → inbox-CD8IDILN.mjs} +1 -1
  30. package/dist/{key-DolXUaLH.mjs → key-C3o3DGap.mjs} +1 -1
  31. package/dist/{kv-cache-DDm80t1K.js → kv-cache-CSKXXleR.js} +60 -18
  32. package/dist/{kv-cache-Bk3V1Tiu.cjs → kv-cache-DC9LcfoC.cjs} +59 -17
  33. package/dist/{ld-rTdIfZ63.mjs → ld-ijSr-0AB.mjs} +2 -2
  34. package/dist/{middleware-CpXoRHs3.mjs → middleware-CYAaUTi5.mjs} +78 -38
  35. package/dist/{middleware-BWOwEGpy.cjs → middleware-DNueR0GD.cjs} +184 -59
  36. package/dist/{middleware-B4-Rzdi3.cjs → middleware-DmcY2TWS.cjs} +1 -1
  37. package/dist/{middleware-BimXKBcP.js → middleware-sjhz30sm.js} +184 -59
  38. package/dist/{middleware-DYK_res8.mjs → middleware-yqqfI7oJ.mjs} +1 -1
  39. package/dist/{mod-B6LmugJ4.d.ts → mod-BRnwh2oy.d.ts} +2 -8
  40. package/dist/{mod-D2JyWtW3.d.cts → mod-BYWp77-z.d.cts} +1 -1
  41. package/dist/{mod-DQGjRJUG.d.ts → mod-BuRRdsfD.d.ts} +1 -1
  42. package/dist/{mod-ej_rLx8h.d.ts → mod-CM5_J_cx.d.ts} +1 -1
  43. package/dist/{mod-a6kmB8xD.d.cts → mod-CVSKw-5H.d.cts} +1 -1
  44. package/dist/{mod-V2vDnYNu.d.cts → mod-Co7LEK9X.d.cts} +2 -8
  45. package/dist/mod.cjs +5 -5
  46. package/dist/mod.d.cts +6 -6
  47. package/dist/mod.d.ts +6 -6
  48. package/dist/mod.js +5 -5
  49. package/dist/nodeinfo/client.test.mjs +337 -19
  50. package/dist/nodeinfo/handler.test.mjs +1 -1
  51. package/dist/nodeinfo/mod.cjs +1 -1
  52. package/dist/nodeinfo/mod.d.cts +1 -1
  53. package/dist/nodeinfo/mod.d.ts +1 -1
  54. package/dist/nodeinfo/mod.js +1 -1
  55. package/dist/otel/exporter.test.mjs +292 -30
  56. package/dist/otel/mod.cjs +36 -30
  57. package/dist/otel/mod.d.cts +13 -0
  58. package/dist/otel/mod.d.ts +13 -0
  59. package/dist/otel/mod.js +36 -30
  60. package/dist/{owner-CVfzyM5V.mjs → owner-B8kXzD0q.mjs} +2 -2
  61. package/dist/{proof-gOGM-sJS.mjs → proof-CBQOjOgA.mjs} +2 -2
  62. package/dist/{proof-CaQ4KG6y.js → proof-DKX6nASL.js} +1 -1
  63. package/dist/{proof-DhaTTn-P.cjs → proof-qiuGJCD7.cjs} +1 -1
  64. package/dist/{send-D35Va72B.mjs → send-DRpisX_f.mjs} +117 -32
  65. package/dist/sig/http.test.mjs +528 -2
  66. package/dist/sig/key.test.mjs +2 -2
  67. package/dist/sig/ld.test.mjs +2 -2
  68. package/dist/sig/mod.cjs +2 -2
  69. package/dist/sig/mod.d.cts +2 -2
  70. package/dist/sig/mod.d.ts +2 -2
  71. package/dist/sig/mod.js +2 -2
  72. package/dist/sig/owner.test.mjs +1 -1
  73. package/dist/sig/proof.test.mjs +1 -1
  74. package/dist/{temporal-CA4Y9hBJ.mjs → temporal-BQGEINad.mjs} +1 -1
  75. package/dist/testing/mod.d.mts +26 -1
  76. package/dist/{types-miAYfiGO.js → types-B1_pRFE_.js} +112 -63
  77. package/dist/{types-DRPjmHVV.cjs → types-DOvyymM4.cjs} +111 -62
  78. package/dist/utils/docloader.test.mjs +318 -34
  79. package/dist/utils/mod.cjs +1 -1
  80. package/dist/utils/mod.d.cts +1 -1
  81. package/dist/utils/mod.d.ts +1 -1
  82. package/dist/utils/mod.js +1 -1
  83. package/package.json +6 -6
  84. package/dist/docloader-D1ghurDJ.mjs +0 -83
@@ -5,15 +5,30 @@ import { t as esm_default } from "../esm-DVILvP5e.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
6
6
  import "../std__assert-DNZ4Liil.mjs";
7
7
  import { t as assertRejects } from "../assert_rejects-WyYSTDDX.mjs";
8
- import { l as verifyRequest } from "../http-BBBUbwKh.mjs";
8
+ import { u as verifyRequest } from "../http-CQMVisM0.mjs";
9
9
  import { i as rsaPrivateKey2 } from "../keys-BAK-tUlf.mjs";
10
- import { t as getAuthenticatedDocumentLoader } from "../docloader-D1ghurDJ.mjs";
10
+ import { n as getAuthenticatedDocumentLoader, t as createAuthenticatedDocumentLoader } from "../docloader-CbtF_0cD.mjs";
11
11
  import { mockDocumentLoader, test } from "@fedify/fixture";
12
12
  import { FetchError, UrlError } from "@fedify/vocab-runtime";
13
+ import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
13
14
  import { configure, reset } from "@logtape/logtape";
14
- import dns from "node:dns/promises";
15
15
  import { deepStrictEqual, ok, rejects } from "node:assert/strict";
16
16
  //#region src/utils/docloader.test.ts
17
+ const identity = {
18
+ keyId: new URL("https://example.com/key2"),
19
+ privateKey: rsaPrivateKey2
20
+ };
21
+ const directDeps = { openHop: openDirectFetch };
22
+ function publicPolicyHop(lookup) {
23
+ return async (url) => {
24
+ await (await openPublicFetch(url, { lookup })).close();
25
+ return openDirectFetch(url);
26
+ };
27
+ }
28
+ const publicLookup = () => Promise.resolve([{
29
+ address: "8.8.8.8",
30
+ family: 4
31
+ }]);
17
32
  test("getAuthenticatedDocumentLoader()", async (t) => {
18
33
  esm_default.spyGlobal();
19
34
  esm_default.get("begin:https://example.com/object", async (cl) => {
@@ -25,10 +40,7 @@ test("getAuthenticatedDocumentLoader()", async (t) => {
25
40
  return new Response(JSON.stringify(v != null), { headers: { "Content-Type": "application/json" } });
26
41
  });
27
42
  await t.step("test", async () => {
28
- assertEquals(await (await getAuthenticatedDocumentLoader({
29
- keyId: new URL("https://example.com/key2"),
30
- privateKey: rsaPrivateKey2
31
- }))("https://example.com/object"), {
43
+ assertEquals(await createAuthenticatedDocumentLoader(identity, {}, directDeps)("https://example.com/object"), {
32
44
  contextUrl: null,
33
45
  documentUrl: "https://example.com/object",
34
46
  document: true
@@ -57,10 +69,10 @@ test("getAuthenticatedDocumentLoader()", async (t) => {
57
69
  const index = Number(cl.url.split("/").at(-1));
58
70
  return Response.redirect(`https://example.com/custom-too-many-redirects/${index + 1}`, 302);
59
71
  });
60
- const loader = getAuthenticatedDocumentLoader({
72
+ const loader = createAuthenticatedDocumentLoader({
61
73
  keyId: new URL("https://example.com/key2"),
62
74
  privateKey: rsaPrivateKey2
63
- }, { maxRedirection: 1 });
75
+ }, { maxRedirection: 1 }, directDeps);
64
76
  await assertRejects(() => loader("https://example.com/custom-too-many-redirects/0"), Error, "Too many redirections");
65
77
  assertEquals(requestCount, 2);
66
78
  esm_default.hardReset();
@@ -74,21 +86,16 @@ test("getAuthenticatedDocumentLoader() validates redirects", async (t) => {
74
86
  privateRequestCount++;
75
87
  return Response.json({ private: true });
76
88
  });
89
+ const policyDeps = { openHop: publicPolicyHop(publicLookup) };
77
90
  await t.step("deny public-to-private redirects", async () => {
78
- const loader = getAuthenticatedDocumentLoader({
79
- keyId: new URL("https://example.com/key2"),
80
- privateKey: rsaPrivateKey2
81
- });
91
+ const loader = createAuthenticatedDocumentLoader(identity, {}, policyDeps);
82
92
  await assertRejects(() => loader("https://example.com/redirect-to-private"), UrlError);
83
93
  assertEquals(privateRequestCount, 0);
84
94
  });
85
95
  esm_default.get("https://example.com/redirect-to-public", () => Response.redirect("https://www.example.com/document", 302));
86
96
  esm_default.get("https://www.example.com/document", () => Response.json({ public: true }));
87
97
  await t.step("allow public-to-public redirects", async () => {
88
- assertEquals((await getAuthenticatedDocumentLoader({
89
- keyId: new URL("https://example.com/key2"),
90
- privateKey: rsaPrivateKey2
91
- })("https://example.com/redirect-to-public")).document, { public: true });
98
+ assertEquals((await createAuthenticatedDocumentLoader(identity, {}, policyDeps)("https://example.com/redirect-to-public")).document, { public: true });
92
99
  });
93
100
  await t.step("allow private redirects when explicitly enabled", async () => {
94
101
  assertEquals((await getAuthenticatedDocumentLoader({
@@ -118,10 +125,7 @@ test("getAuthenticatedDocumentLoader() cancellation", async (t) => {
118
125
  });
119
126
  }, 1e3);
120
127
  }));
121
- const loader = getAuthenticatedDocumentLoader({
122
- keyId: new URL("https://example.com/key2"),
123
- privateKey: rsaPrivateKey2
124
- });
128
+ const loader = createAuthenticatedDocumentLoader(identity, {}, directDeps);
125
129
  const controller = new AbortController();
126
130
  const promise = loader("https://example.com/slow-object", { signal: controller.signal });
127
131
  controller.abort();
@@ -132,10 +136,7 @@ test("getAuthenticatedDocumentLoader() cancellation", async (t) => {
132
136
  }
133
137
  });
134
138
  await t.step("immediate cancellation", async () => {
135
- const loader = getAuthenticatedDocumentLoader({
136
- keyId: new URL("https://example.com/key2"),
137
- privateKey: rsaPrivateKey2
138
- });
139
+ const loader = createAuthenticatedDocumentLoader(identity, {}, directDeps);
139
140
  const controller = new AbortController();
140
141
  controller.abort();
141
142
  await assertRejects(() => loader("https://example.com/object", { signal: controller.signal }), Error);
@@ -168,21 +169,17 @@ test("getAuthenticatedDocumentLoader() bounds JSON after redirects", async () =>
168
169
  esm_default.hardReset();
169
170
  }
170
171
  });
171
- test("getAuthenticatedDocumentLoader() logs DNS failures as such", { ignore: "Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted" }, async (t) => {
172
- const loader = getAuthenticatedDocumentLoader({
173
- keyId: new URL("https://example.com/key2"),
174
- privateKey: rsaPrivateKey2
175
- });
172
+ test("getAuthenticatedDocumentLoader() logs DNS failures as such", async (t) => {
176
173
  for (const result of [
177
174
  "throws",
178
175
  "empty",
179
176
  "private"
180
177
  ]) await t.step(result, async () => {
181
- const originalLookup = dns.lookup;
182
- dns.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
178
+ const lookup = () => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
183
179
  address: "127.0.0.1",
184
180
  family: 4
185
- }]));
181
+ }]);
182
+ const loader = createAuthenticatedDocumentLoader(identity, {}, { openHop: (url) => openPublicFetch(url, { lookup }) });
186
183
  const records = [];
187
184
  await configure({
188
185
  sinks: { buffer: (record) => records.push(record) },
@@ -216,7 +213,6 @@ test("getAuthenticatedDocumentLoader() logs DNS failures as such", { ignore: "De
216
213
  assertEquals(records[0].properties.error, error);
217
214
  } finally {
218
215
  await reset();
219
- dns.lookup = originalLookup;
220
216
  }
221
217
  });
222
218
  });
@@ -461,6 +457,211 @@ test("getAuthenticatedDocumentLoader() rejects cancellation before fetching", as
461
457
  esm_default.hardReset();
462
458
  }
463
459
  });
460
+ test("getAuthenticatedDocumentLoader() closes each hop before opening the next", async (t) => {
461
+ await t.step("redirect", async () => {
462
+ const start = "https://example.com/lifecycle/start";
463
+ const next = "https://example.com/lifecycle/document";
464
+ const hops = recordHops((request) => request.url === start ? {
465
+ status: 302,
466
+ headers: { Location: next },
467
+ body: "Found"
468
+ } : {
469
+ headers: { "Content-Type": "application/activity+json" },
470
+ body: "{\"done\":true}"
471
+ });
472
+ deepStrictEqual(await createAuthenticatedDocumentLoader(identity, {}, hops)(start), {
473
+ contextUrl: null,
474
+ document: { done: true },
475
+ documentUrl: next
476
+ });
477
+ deepStrictEqual(hops.events, [
478
+ `open ${start}`,
479
+ `fetch #1 ${start}`,
480
+ "cancel #1",
481
+ `close ${start}`,
482
+ `open ${next}`,
483
+ `fetch #2 ${next}`,
484
+ "consumed #2",
485
+ `close ${next}`
486
+ ]);
487
+ hops.assertInvariants();
488
+ });
489
+ await t.step("Link header alternate", async () => {
490
+ const start = "https://example.com/lifecycle/page";
491
+ const next = "https://example.com/lifecycle/alternate";
492
+ const hops = recordHops((request) => request.url === start ? {
493
+ headers: {
494
+ "Content-Type": "text/plain",
495
+ Link: `<${next}>; rel="alternate"; type="application/activity+json"`
496
+ },
497
+ body: "not JSON"
498
+ } : {
499
+ headers: { "Content-Type": "application/activity+json" },
500
+ body: "{\"done\":true}"
501
+ });
502
+ deepStrictEqual((await createAuthenticatedDocumentLoader(identity, {}, hops)(start)).document, { done: true });
503
+ deepStrictEqual(hops.events, [
504
+ `open ${start}`,
505
+ `fetch #1 ${start}`,
506
+ "cancel #1",
507
+ `close ${start}`,
508
+ `open ${next}`,
509
+ `fetch #2 ${next}`,
510
+ "consumed #2",
511
+ `close ${next}`
512
+ ]);
513
+ hops.assertInvariants();
514
+ });
515
+ await t.step("one lookup per hop", async () => {
516
+ const start = "https://first.example/start";
517
+ const next = "https://second.example/document";
518
+ const lookups = [];
519
+ const lookup = (hostname) => {
520
+ lookups.push(hostname);
521
+ return Promise.resolve([{
522
+ address: "8.8.8.8",
523
+ family: 4
524
+ }]);
525
+ };
526
+ await withFetchStub((request) => new URL(request.url).pathname === "/start" ? Response.redirect(next, 302) : Response.json({ done: true }), async (fetched) => {
527
+ deepStrictEqual((await createAuthenticatedDocumentLoader(identity, {}, { openHop: (url) => openPublicFetch(url, { lookup }) })(start)).document, { done: true });
528
+ deepStrictEqual(lookups, ["first.example", "second.example"]);
529
+ deepStrictEqual(fetched.length, 2);
530
+ });
531
+ });
532
+ });
533
+ async function captureLogs(fn) {
534
+ const records = [];
535
+ await configure({
536
+ sinks: { buffer: (record) => records.push(record) },
537
+ loggers: [{
538
+ category: "fedify",
539
+ sinks: ["buffer"],
540
+ lowestLevel: "debug"
541
+ }, {
542
+ category: ["logtape", "meta"],
543
+ sinks: []
544
+ }],
545
+ reset: true
546
+ });
547
+ try {
548
+ await fn();
549
+ await new Promise((resolve) => setTimeout(resolve, 0));
550
+ } finally {
551
+ await reset();
552
+ }
553
+ return records;
554
+ }
555
+ function getCancelFailures(records) {
556
+ return records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}").map((r) => r.properties.error.message);
557
+ }
558
+ const linkAlternate = (next) => ({
559
+ headers: {
560
+ "Content-Type": "text/plain",
561
+ Link: `<${next}>; rel="alternate"; type="application/activity+json"`
562
+ },
563
+ body: "not JSON"
564
+ });
565
+ const jsonDocument = {
566
+ headers: { "Content-Type": "application/activity+json" },
567
+ body: "{\"done\":true}"
568
+ };
569
+ test("getAuthenticatedDocumentLoader() logs bodies failing to cancel and loads the next hop", async (t) => {
570
+ for (const kind of ["redirect", "Link header alternate"]) await t.step(kind, async () => {
571
+ const start = "https://example.com/lifecycle/cancel-failure";
572
+ const next = "https://example.com/lifecycle/cancel-failure-next";
573
+ const hops = recordHops((request) => request.url === start ? {
574
+ ...kind === "redirect" ? {
575
+ status: 302,
576
+ headers: { Location: next },
577
+ body: "Found"
578
+ } : linkAlternate(next),
579
+ failCancel: true
580
+ } : jsonDocument);
581
+ const loader = createAuthenticatedDocumentLoader(identity, {}, hops);
582
+ let document;
583
+ const records = await captureLogs(async () => {
584
+ document = (await loader(start)).document;
585
+ });
586
+ deepStrictEqual(document, { done: true });
587
+ deepStrictEqual(hops.events, [
588
+ `open ${start}`,
589
+ `fetch #1 ${start}`,
590
+ "cancel #1",
591
+ `close ${start}`,
592
+ `open ${next}`,
593
+ `fetch #2 ${next}`,
594
+ "consumed #2",
595
+ `close ${next}`
596
+ ]);
597
+ deepStrictEqual(getCancelFailures(records), ["cancel #1 failed"]);
598
+ hops.assertInvariants();
599
+ });
600
+ });
601
+ test("getAuthenticatedDocumentLoader() is not blocked by a body cancellation that never settles", async (t) => {
602
+ await t.step("redirect and Link header alternate", async () => {
603
+ const start = "https://example.com/lifecycle/hanging-start";
604
+ const page = "https://example.com/lifecycle/hanging-page";
605
+ const next = "https://example.com/lifecycle/hanging-document";
606
+ const hops = recordHops((request) => ({
607
+ ...request.url === start ? {
608
+ status: 302,
609
+ headers: { Location: page },
610
+ body: "Found"
611
+ } : request.url === page ? linkAlternate(next) : jsonDocument,
612
+ hangCancel: true
613
+ }));
614
+ deepStrictEqual((await createAuthenticatedDocumentLoader(identity, {}, hops)(start)).document, { done: true });
615
+ deepStrictEqual(hops.events, [
616
+ `open ${start}`,
617
+ `fetch #1 ${start}`,
618
+ "cancel #1",
619
+ `close ${start}`,
620
+ `open ${page}`,
621
+ `fetch #2 ${page}`,
622
+ "cancel #2",
623
+ `close ${page}`,
624
+ `open ${next}`,
625
+ `fetch #3 ${next}`,
626
+ "consumed #3",
627
+ `close ${next}`
628
+ ]);
629
+ hops.assertInvariants();
630
+ });
631
+ await t.step("not found", async () => {
632
+ const url = "https://example.com/lifecycle/hanging-missing";
633
+ const hops = recordHops(() => ({
634
+ status: 404,
635
+ body: "Not Found",
636
+ hangCancel: true
637
+ }));
638
+ await rejects(createAuthenticatedDocumentLoader(identity, {}, hops)(url), FetchError);
639
+ deepStrictEqual(hops.events.slice(0, 2), [`open ${url}`, `fetch #1 ${url}`]);
640
+ deepStrictEqual(hops.events.at(-1), `close ${url}`);
641
+ hops.assertInvariants();
642
+ });
643
+ });
644
+ test("getAuthenticatedDocumentLoader() completes with responses cloned by fetch-mock", async () => {
645
+ esm_default.mockGlobal();
646
+ try {
647
+ const base = "https://example.com/tee/";
648
+ esm_default.get(`${base}start`, () => new Response("Found", {
649
+ status: 302,
650
+ headers: { Location: `${base}page` }
651
+ }));
652
+ esm_default.get(`${base}page`, () => new Response("not JSON", { headers: {
653
+ "Content-Type": "text/plain",
654
+ Link: `<${base}document>; rel="alternate"; type="application/activity+json"`
655
+ } }));
656
+ esm_default.get(`${base}document`, () => new Response("{\"done\":true}", { headers: { "Content-Type": "application/activity+json" } }));
657
+ esm_default.get(`${base}missing`, () => new Response("Not Found", { status: 404 }));
658
+ const loader = createAuthenticatedDocumentLoader(identity, {}, directDeps);
659
+ deepStrictEqual((await loader(`${base}start`)).document, { done: true });
660
+ await rejects(loader(`${base}missing`), FetchError);
661
+ } finally {
662
+ esm_default.hardReset();
663
+ }
664
+ });
464
665
  const unsupportedUrls = [
465
666
  "ftp://example.com/collection",
466
667
  "file:///collection",
@@ -595,5 +796,88 @@ for (const url of unsupportedUrls) {
595
796
  });
596
797
  });
597
798
  }
799
+ function createRecordedResponse(events, index, init, cancelSuffix = () => "") {
800
+ const bytes = new TextEncoder().encode(init.body ?? "");
801
+ const chunkSize = init.chunkSize ?? 256;
802
+ let offset = 0;
803
+ let settled = false;
804
+ const body = new ReadableStream({
805
+ pull(controller) {
806
+ if (offset >= bytes.length) {
807
+ settled = true;
808
+ events.push(`consumed #${index}`);
809
+ controller.close();
810
+ return;
811
+ }
812
+ controller.enqueue(bytes.slice(offset, offset + chunkSize));
813
+ offset += chunkSize;
814
+ },
815
+ cancel() {
816
+ settled = true;
817
+ events.push(`cancel #${index}${cancelSuffix()}`);
818
+ if (init.failCancel) throw new Error(`cancel #${index} failed`);
819
+ if (init.hangCancel) return new Promise(() => {});
820
+ }
821
+ }, { highWaterMark: 0 });
822
+ return {
823
+ response: new Response(body, {
824
+ status: init.status ?? 200,
825
+ headers: init.headers
826
+ }),
827
+ settled: () => settled
828
+ };
829
+ }
830
+ function recordHops(respond) {
831
+ const events = [];
832
+ const requests = [];
833
+ const unsettledAtClose = [];
834
+ let opens = 0;
835
+ let closes = 0;
836
+ let fetches = 0;
837
+ return {
838
+ events,
839
+ requests,
840
+ openHop(url) {
841
+ opens++;
842
+ events.push(`open ${url}`);
843
+ const tracked = [];
844
+ let closed = false;
845
+ let closing = false;
846
+ return Promise.resolve({
847
+ url: new URL(url),
848
+ async fetch(input, init) {
849
+ if (closed) throw new TypeError("The session is closed.");
850
+ const request = new Request(input instanceof Request ? input.clone() : input, init);
851
+ const index = ++fetches;
852
+ requests.push(request);
853
+ events.push(`fetch #${index} ${request.url}`);
854
+ const recorded = createRecordedResponse(events, index, await respond(request, index), () => closing ? " by close" : "");
855
+ tracked.push({
856
+ ...recorded,
857
+ index
858
+ });
859
+ return recorded.response;
860
+ },
861
+ close() {
862
+ if (closed) return Promise.resolve();
863
+ closed = true;
864
+ closes++;
865
+ closing = true;
866
+ for (const { response, settled, index } of tracked) {
867
+ if (settled()) continue;
868
+ if (response.body?.locked) unsettledAtClose.push(index);
869
+ else response.body?.cancel().catch(() => {});
870
+ }
871
+ events.push(`close ${url}`);
872
+ return Promise.resolve();
873
+ }
874
+ });
875
+ },
876
+ assertInvariants() {
877
+ deepStrictEqual(opens, closes);
878
+ deepStrictEqual(unsettledAtClose, []);
879
+ }
880
+ };
881
+ }
598
882
  //#endregion
599
883
  export {};
@@ -1,6 +1,6 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
- const require_kv_cache = require("../kv-cache-Bk3V1Tiu.cjs");
4
+ const require_kv_cache = require("../kv-cache-DC9LcfoC.cjs");
5
5
  exports.getAuthenticatedDocumentLoader = require_kv_cache.getAuthenticatedDocumentLoader;
6
6
  exports.kvCache = require_kv_cache.kvCache;
@@ -1,3 +1,3 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-V2vDnYNu.cjs";
2
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-Co7LEK9X.cjs";
3
3
  export { getAuthenticatedDocumentLoader, kvCache };
@@ -1,3 +1,3 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-B6LmugJ4.js";
2
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-BRnwh2oy.js";
3
3
  export { getAuthenticatedDocumentLoader, kvCache };
package/dist/utils/mod.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../kv-cache-DDm80t1K.js";
3
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../kv-cache-CSKXXleR.js";
4
4
  export { getAuthenticatedDocumentLoader, kvCache };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/fedify",
3
- "version": "2.2.19",
3
+ "version": "2.2.20",
4
4
  "description": "An ActivityPub server framework",
5
5
  "keywords": [
6
6
  "ActivityPub",
@@ -141,7 +141,7 @@
141
141
  "dependencies": {
142
142
  "@js-temporal/polyfill": "^0.5.1",
143
143
  "@logtape/logtape": "^2.0.5",
144
- "@opentelemetry/api": "^1.9.0",
144
+ "@opentelemetry/api": "~1.9.0",
145
145
  "@opentelemetry/core": "^2.5.0",
146
146
  "@opentelemetry/sdk-trace-base": "^2.5.0",
147
147
  "@opentelemetry/semantic-conventions": "^1.39.0",
@@ -153,9 +153,9 @@
153
153
  "uri-template-router": "^1.0.0",
154
154
  "url-template": "^3.1.1",
155
155
  "urlpattern-polyfill": "^10.1.0",
156
- "@fedify/vocab": "2.2.19",
157
- "@fedify/vocab-runtime": "2.2.19",
158
- "@fedify/webfinger": "2.2.19"
156
+ "@fedify/vocab": "2.2.20",
157
+ "@fedify/webfinger": "2.2.20",
158
+ "@fedify/vocab-runtime": "2.2.20"
159
159
  },
160
160
  "devDependencies": {
161
161
  "@std/assert": "npm:@jsr/std__assert@^0.226.0",
@@ -168,7 +168,7 @@
168
168
  "typescript": "^6.0.0",
169
169
  "wrangler": "^4.17.0",
170
170
  "@fedify/fixture": "2.0.0",
171
- "@fedify/vocab-tools": "^2.2.19"
171
+ "@fedify/vocab-tools": "^2.2.20"
172
172
  },
173
173
  "scripts": {
174
174
  "build:self": "tsdown",
@@ -1,83 +0,0 @@
1
- import "@js-temporal/polyfill";
2
- import "urlpattern-polyfill";
3
- globalThis.addEventListener = () => {};
4
- import { s as validateCryptoKey } from "./key-DolXUaLH.mjs";
5
- import { n as doubleKnock } from "./http-BBBUbwKh.mjs";
6
- import { curry } from "es-toolkit";
7
- import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, validatePublicUrl } from "@fedify/vocab-runtime";
8
- import { getLogger } from "@logtape/logtape";
9
- //#region src/utils/docloader.ts
10
- const logger = getLogger([
11
- "fedify",
12
- "utils",
13
- "docloader"
14
- ]);
15
- const DEFAULT_MAX_REDIRECTION = 20;
16
- /**
17
- * Gets an authenticated {@link DocumentLoader} for the given identity.
18
- * Note that an authenticated document loader intentionally does not cache
19
- * the fetched documents.
20
- * At most 20 HTTP redirects and alternate document links are followed in total
21
- * per call. Revisiting a URL within that chain throws a {@link FetchError}.
22
- * @param identity The identity to get the document loader for.
23
- * The actor's key pair.
24
- * @param options The options for the document loader.
25
- * @returns The authenticated document loader.
26
- * @throws {TypeError} If the key is invalid or unsupported.
27
- * @since 0.4.0
28
- */
29
- function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, tracerProvider } = {}) {
30
- validateCryptoKey(identity.privateKey);
31
- async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
32
- options?.signal?.throwIfAborted();
33
- let currentUrl = new URL(url).href;
34
- await validateUrl(currentUrl);
35
- visited.add(currentUrl);
36
- const originalRequest = createActivityPubRequest(currentUrl, { userAgent });
37
- function follow(nextUrl) {
38
- options?.signal?.throwIfAborted();
39
- if (redirected >= DEFAULT_MAX_REDIRECTION) throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
40
- if (visited.has(nextUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${nextUrl}`);
41
- redirected++;
42
- }
43
- const response = await doubleKnock(originalRequest, identity, {
44
- maxRedirection,
45
- specDeterminer,
46
- log: curry(logRequest)(logger),
47
- tracerProvider,
48
- signal: options?.signal,
49
- validateRedirect: async (nextUrl) => {
50
- follow(nextUrl);
51
- await validateUrl(nextUrl);
52
- visited.add(nextUrl);
53
- currentUrl = nextUrl;
54
- }
55
- });
56
- return getRemoteDocument(currentUrl, response, (alternateUrl) => {
57
- follow(alternateUrl);
58
- return load(alternateUrl, options, redirected, visited);
59
- });
60
- }
61
- async function validateUrl(url) {
62
- const parsedUrl = new URL(url);
63
- const protocol = parsedUrl.protocol;
64
- if (protocol !== "http:" && protocol !== "https:") throw new UrlError(`Unsupported protocol: ${protocol}`);
65
- if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new UrlError("URLs with credentials are not allowed");
66
- if (!allowPrivateAddress) try {
67
- await validatePublicUrl(url);
68
- } catch (error) {
69
- if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
70
- url,
71
- error
72
- });
73
- else logger.error("Disallowed private URL: {url}", {
74
- url,
75
- error
76
- });
77
- throw error;
78
- }
79
- }
80
- return (url, options) => load(url, options);
81
- }
82
- //#endregion
83
- export { getAuthenticatedDocumentLoader as t };