@fedify/fedify 2.0.35-dev.2480 → 2.0.35

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 (79) hide show
  1. package/dist/{builder-cEjyBwpD.mjs → builder-CpxZPa7I.mjs} +3 -3
  2. package/dist/{client-CAM_bQXx.d.cts → client-BIBzqEIp.d.cts} +0 -1
  3. package/dist/{client-BUqbea1d.mjs → client-DGNacEx_.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-r8sjhltN.d.cts → context-CnoWxuoB.d.cts} +2 -2
  9. package/dist/{context-B2iX3heK.d.ts → context-lxjaUTAI.d.ts} +2 -2
  10. package/dist/{deno-BcnGeGr2.mjs → deno-BAtjc8bx.mjs} +1 -1
  11. package/dist/docloader-CyBB50Om.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/middleware.test.mjs +42 -23
  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-BD9izRp9.cjs → http-5u2wDmjl.cjs} +114 -56
  25. package/dist/{http-D0cjeL8a.mjs → http-9wCo74JF.mjs} +136 -49
  26. package/dist/{http-Bs6thuCC.d.cts → http-Cd8UD1ym.d.ts} +1 -2
  27. package/dist/{http-CDupyfOc.d.ts → http-DFqAb11b.d.cts} +1 -2
  28. package/dist/{http-IYRDhhnT.js → http-Dwj7bNhv.js} +113 -55
  29. package/dist/{inbox-D2fsxjqC.mjs → inbox-De2IaCfd.mjs} +1 -1
  30. package/dist/{key-BFzQ3Hef.mjs → key-BD-PBpNB.mjs} +1 -1
  31. package/dist/{kv-cache-C-RyNMt6.js → kv-cache-5TdAMHng.js} +60 -18
  32. package/dist/{kv-cache-B4sDuLjb.cjs → kv-cache-ivTOKx4b.cjs} +59 -17
  33. package/dist/{ld-D2bkYBIz.mjs → ld-34_mZbyP.mjs} +2 -2
  34. package/dist/{middleware-3mc-kq3v.mjs → middleware-BI7Hldd0.mjs} +1 -1
  35. package/dist/{middleware-DxX8ZL-q.cjs → middleware-Co4IpMca.cjs} +118 -33
  36. package/dist/{middleware-DRZp7Odv.js → middleware-DTJ9dnYC.js} +118 -33
  37. package/dist/{middleware-B2UC8mtN.cjs → middleware-DhwZ4gNG.cjs} +1 -1
  38. package/dist/{middleware-CXIBpN4y.mjs → middleware-DzaMP9BP.mjs} +12 -12
  39. package/dist/{mod-BkPB4hPe.d.cts → mod-BPDfu4Ey.d.cts} +2 -7
  40. package/dist/{mod-E7YM72JZ.d.ts → mod-BS-jg3Wx.d.ts} +1 -1
  41. package/dist/{mod-BKJyD7nk.d.ts → mod-DGP89q0u.d.ts} +2 -7
  42. package/dist/{mod-IOWUF5Yr.d.cts → mod-DVPS4yam.d.cts} +1 -1
  43. package/dist/{mod-CM_eR8hQ.d.ts → mod-I-34OMN3.d.ts} +1 -1
  44. package/dist/{mod-DDTLbfce.d.cts → mod-w1oyHseQ.d.cts} +1 -1
  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/{owner-BuJXJDhl.mjs → owner-CxBLBD2K.mjs} +2 -2
  56. package/dist/{proof-BJ_6nKhb.js → proof-D5IUmaZ0.js} +1 -1
  57. package/dist/{proof-JxVhLZA3.mjs → proof-DhW1tGUz.mjs} +2 -2
  58. package/dist/{proof-DfmHAmtr.cjs → proof-yX6qSpxk.cjs} +1 -1
  59. package/dist/{send-CDaWmeqV.mjs → send-CdfEXNS7.mjs} +117 -32
  60. package/dist/sig/http.test.mjs +411 -2
  61. package/dist/sig/key.test.mjs +2 -2
  62. package/dist/sig/ld.test.mjs +2 -2
  63. package/dist/sig/mod.cjs +2 -2
  64. package/dist/sig/mod.d.cts +2 -2
  65. package/dist/sig/mod.d.ts +2 -2
  66. package/dist/sig/mod.js +2 -2
  67. package/dist/sig/owner.test.mjs +1 -1
  68. package/dist/sig/proof.test.mjs +1 -1
  69. package/dist/{temporal-Bv6Zn1C5.mjs → temporal-BbywNJ81.mjs} +1 -1
  70. package/dist/testing/mod.d.mts +0 -1
  71. package/dist/{types-miAYfiGO.js → types-B1_pRFE_.js} +112 -63
  72. package/dist/{types-DRPjmHVV.cjs → types-DOvyymM4.cjs} +111 -62
  73. package/dist/utils/docloader.test.mjs +316 -32
  74. package/dist/utils/mod.cjs +1 -1
  75. package/dist/utils/mod.d.cts +1 -1
  76. package/dist/utils/mod.d.ts +1 -1
  77. package/dist/utils/mod.js +1 -1
  78. package/package.json +5 -5
  79. package/dist/docloader-BqDq3fDF.mjs +0 -82
@@ -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-DflMZha5.mjs";
7
7
  import { t as assertRejects } from "../assert_rejects-WyYSTDDX.mjs";
8
- import { l as verifyRequest } from "../http-D0cjeL8a.mjs";
8
+ import { u as verifyRequest } from "../http-9wCo74JF.mjs";
9
9
  import { i as rsaPrivateKey2 } from "../keys-CCxRJhWa.mjs";
10
- import { t as getAuthenticatedDocumentLoader } from "../docloader-BqDq3fDF.mjs";
10
+ import { n as getAuthenticatedDocumentLoader, t as createAuthenticatedDocumentLoader } from "../docloader-CyBB50Om.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
@@ -58,21 +70,16 @@ test("getAuthenticatedDocumentLoader() validates redirects", async (t) => {
58
70
  privateRequestCount++;
59
71
  return Response.json({ private: true });
60
72
  });
73
+ const policyDeps = { openHop: publicPolicyHop(publicLookup) };
61
74
  await t.step("deny public-to-private redirects", async () => {
62
- const loader = getAuthenticatedDocumentLoader({
63
- keyId: new URL("https://example.com/key2"),
64
- privateKey: rsaPrivateKey2
65
- });
75
+ const loader = createAuthenticatedDocumentLoader(identity, {}, policyDeps);
66
76
  await assertRejects(() => loader("https://example.com/redirect-to-private"), UrlError);
67
77
  assertEquals(privateRequestCount, 0);
68
78
  });
69
79
  esm_default.get("https://example.com/redirect-to-public", () => Response.redirect("https://www.example.com/document", 302));
70
80
  esm_default.get("https://www.example.com/document", () => Response.json({ public: true }));
71
81
  await t.step("allow public-to-public redirects", async () => {
72
- assertEquals((await getAuthenticatedDocumentLoader({
73
- keyId: new URL("https://example.com/key2"),
74
- privateKey: rsaPrivateKey2
75
- })("https://example.com/redirect-to-public")).document, { public: true });
82
+ assertEquals((await createAuthenticatedDocumentLoader(identity, {}, policyDeps)("https://example.com/redirect-to-public")).document, { public: true });
76
83
  });
77
84
  await t.step("allow private redirects when explicitly enabled", async () => {
78
85
  assertEquals((await getAuthenticatedDocumentLoader({
@@ -102,10 +109,7 @@ test("getAuthenticatedDocumentLoader() cancellation", async (t) => {
102
109
  });
103
110
  }, 1e3);
104
111
  }));
105
- const loader = getAuthenticatedDocumentLoader({
106
- keyId: new URL("https://example.com/key2"),
107
- privateKey: rsaPrivateKey2
108
- });
112
+ const loader = createAuthenticatedDocumentLoader(identity, {}, directDeps);
109
113
  const controller = new AbortController();
110
114
  const promise = loader("https://example.com/slow-object", { signal: controller.signal });
111
115
  controller.abort();
@@ -116,10 +120,7 @@ test("getAuthenticatedDocumentLoader() cancellation", async (t) => {
116
120
  }
117
121
  });
118
122
  await t.step("immediate cancellation", async () => {
119
- const loader = getAuthenticatedDocumentLoader({
120
- keyId: new URL("https://example.com/key2"),
121
- privateKey: rsaPrivateKey2
122
- });
123
+ const loader = createAuthenticatedDocumentLoader(identity, {}, directDeps);
123
124
  const controller = new AbortController();
124
125
  controller.abort();
125
126
  await assertRejects(() => loader("https://example.com/object", { signal: controller.signal }), Error);
@@ -152,21 +153,17 @@ test("getAuthenticatedDocumentLoader() bounds JSON after redirects", async () =>
152
153
  esm_default.hardReset();
153
154
  }
154
155
  });
155
- test("getAuthenticatedDocumentLoader() logs DNS failures as such", { ignore: "Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted" }, async (t) => {
156
- const loader = getAuthenticatedDocumentLoader({
157
- keyId: new URL("https://example.com/key2"),
158
- privateKey: rsaPrivateKey2
159
- });
156
+ test("getAuthenticatedDocumentLoader() logs DNS failures as such", async (t) => {
160
157
  for (const result of [
161
158
  "throws",
162
159
  "empty",
163
160
  "private"
164
161
  ]) await t.step(result, async () => {
165
- const originalLookup = dns.lookup;
166
- dns.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
162
+ const lookup = () => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
167
163
  address: "127.0.0.1",
168
164
  family: 4
169
- }]));
165
+ }]);
166
+ const loader = createAuthenticatedDocumentLoader(identity, {}, { openHop: (url) => openPublicFetch(url, { lookup }) });
170
167
  const records = [];
171
168
  await configure({
172
169
  sinks: { buffer: (record) => records.push(record) },
@@ -200,7 +197,6 @@ test("getAuthenticatedDocumentLoader() logs DNS failures as such", { ignore: "De
200
197
  assertEquals(records[0].properties.error, error);
201
198
  } finally {
202
199
  await reset();
203
- dns.lookup = originalLookup;
204
200
  }
205
201
  });
206
202
  });
@@ -445,6 +441,211 @@ test("getAuthenticatedDocumentLoader() rejects cancellation before fetching", as
445
441
  esm_default.hardReset();
446
442
  }
447
443
  });
444
+ test("getAuthenticatedDocumentLoader() closes each hop before opening the next", async (t) => {
445
+ await t.step("redirect", async () => {
446
+ const start = "https://example.com/lifecycle/start";
447
+ const next = "https://example.com/lifecycle/document";
448
+ const hops = recordHops((request) => request.url === start ? {
449
+ status: 302,
450
+ headers: { Location: next },
451
+ body: "Found"
452
+ } : {
453
+ headers: { "Content-Type": "application/activity+json" },
454
+ body: "{\"done\":true}"
455
+ });
456
+ deepStrictEqual(await createAuthenticatedDocumentLoader(identity, {}, hops)(start), {
457
+ contextUrl: null,
458
+ document: { done: true },
459
+ documentUrl: next
460
+ });
461
+ deepStrictEqual(hops.events, [
462
+ `open ${start}`,
463
+ `fetch #1 ${start}`,
464
+ "cancel #1",
465
+ `close ${start}`,
466
+ `open ${next}`,
467
+ `fetch #2 ${next}`,
468
+ "consumed #2",
469
+ `close ${next}`
470
+ ]);
471
+ hops.assertInvariants();
472
+ });
473
+ await t.step("Link header alternate", async () => {
474
+ const start = "https://example.com/lifecycle/page";
475
+ const next = "https://example.com/lifecycle/alternate";
476
+ const hops = recordHops((request) => request.url === start ? {
477
+ headers: {
478
+ "Content-Type": "text/plain",
479
+ Link: `<${next}>; rel="alternate"; type="application/activity+json"`
480
+ },
481
+ body: "not JSON"
482
+ } : {
483
+ headers: { "Content-Type": "application/activity+json" },
484
+ body: "{\"done\":true}"
485
+ });
486
+ deepStrictEqual((await createAuthenticatedDocumentLoader(identity, {}, hops)(start)).document, { done: true });
487
+ deepStrictEqual(hops.events, [
488
+ `open ${start}`,
489
+ `fetch #1 ${start}`,
490
+ "cancel #1",
491
+ `close ${start}`,
492
+ `open ${next}`,
493
+ `fetch #2 ${next}`,
494
+ "consumed #2",
495
+ `close ${next}`
496
+ ]);
497
+ hops.assertInvariants();
498
+ });
499
+ await t.step("one lookup per hop", async () => {
500
+ const start = "https://first.example/start";
501
+ const next = "https://second.example/document";
502
+ const lookups = [];
503
+ const lookup = (hostname) => {
504
+ lookups.push(hostname);
505
+ return Promise.resolve([{
506
+ address: "8.8.8.8",
507
+ family: 4
508
+ }]);
509
+ };
510
+ await withFetchStub((request) => new URL(request.url).pathname === "/start" ? Response.redirect(next, 302) : Response.json({ done: true }), async (fetched) => {
511
+ deepStrictEqual((await createAuthenticatedDocumentLoader(identity, {}, { openHop: (url) => openPublicFetch(url, { lookup }) })(start)).document, { done: true });
512
+ deepStrictEqual(lookups, ["first.example", "second.example"]);
513
+ deepStrictEqual(fetched.length, 2);
514
+ });
515
+ });
516
+ });
517
+ async function captureLogs(fn) {
518
+ const records = [];
519
+ await configure({
520
+ sinks: { buffer: (record) => records.push(record) },
521
+ loggers: [{
522
+ category: "fedify",
523
+ sinks: ["buffer"],
524
+ lowestLevel: "debug"
525
+ }, {
526
+ category: ["logtape", "meta"],
527
+ sinks: []
528
+ }],
529
+ reset: true
530
+ });
531
+ try {
532
+ await fn();
533
+ await new Promise((resolve) => setTimeout(resolve, 0));
534
+ } finally {
535
+ await reset();
536
+ }
537
+ return records;
538
+ }
539
+ function getCancelFailures(records) {
540
+ return records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}").map((r) => r.properties.error.message);
541
+ }
542
+ const linkAlternate = (next) => ({
543
+ headers: {
544
+ "Content-Type": "text/plain",
545
+ Link: `<${next}>; rel="alternate"; type="application/activity+json"`
546
+ },
547
+ body: "not JSON"
548
+ });
549
+ const jsonDocument = {
550
+ headers: { "Content-Type": "application/activity+json" },
551
+ body: "{\"done\":true}"
552
+ };
553
+ test("getAuthenticatedDocumentLoader() logs bodies failing to cancel and loads the next hop", async (t) => {
554
+ for (const kind of ["redirect", "Link header alternate"]) await t.step(kind, async () => {
555
+ const start = "https://example.com/lifecycle/cancel-failure";
556
+ const next = "https://example.com/lifecycle/cancel-failure-next";
557
+ const hops = recordHops((request) => request.url === start ? {
558
+ ...kind === "redirect" ? {
559
+ status: 302,
560
+ headers: { Location: next },
561
+ body: "Found"
562
+ } : linkAlternate(next),
563
+ failCancel: true
564
+ } : jsonDocument);
565
+ const loader = createAuthenticatedDocumentLoader(identity, {}, hops);
566
+ let document;
567
+ const records = await captureLogs(async () => {
568
+ document = (await loader(start)).document;
569
+ });
570
+ deepStrictEqual(document, { done: true });
571
+ deepStrictEqual(hops.events, [
572
+ `open ${start}`,
573
+ `fetch #1 ${start}`,
574
+ "cancel #1",
575
+ `close ${start}`,
576
+ `open ${next}`,
577
+ `fetch #2 ${next}`,
578
+ "consumed #2",
579
+ `close ${next}`
580
+ ]);
581
+ deepStrictEqual(getCancelFailures(records), ["cancel #1 failed"]);
582
+ hops.assertInvariants();
583
+ });
584
+ });
585
+ test("getAuthenticatedDocumentLoader() is not blocked by a body cancellation that never settles", async (t) => {
586
+ await t.step("redirect and Link header alternate", async () => {
587
+ const start = "https://example.com/lifecycle/hanging-start";
588
+ const page = "https://example.com/lifecycle/hanging-page";
589
+ const next = "https://example.com/lifecycle/hanging-document";
590
+ const hops = recordHops((request) => ({
591
+ ...request.url === start ? {
592
+ status: 302,
593
+ headers: { Location: page },
594
+ body: "Found"
595
+ } : request.url === page ? linkAlternate(next) : jsonDocument,
596
+ hangCancel: true
597
+ }));
598
+ deepStrictEqual((await createAuthenticatedDocumentLoader(identity, {}, hops)(start)).document, { done: true });
599
+ deepStrictEqual(hops.events, [
600
+ `open ${start}`,
601
+ `fetch #1 ${start}`,
602
+ "cancel #1",
603
+ `close ${start}`,
604
+ `open ${page}`,
605
+ `fetch #2 ${page}`,
606
+ "cancel #2",
607
+ `close ${page}`,
608
+ `open ${next}`,
609
+ `fetch #3 ${next}`,
610
+ "consumed #3",
611
+ `close ${next}`
612
+ ]);
613
+ hops.assertInvariants();
614
+ });
615
+ await t.step("not found", async () => {
616
+ const url = "https://example.com/lifecycle/hanging-missing";
617
+ const hops = recordHops(() => ({
618
+ status: 404,
619
+ body: "Not Found",
620
+ hangCancel: true
621
+ }));
622
+ await rejects(createAuthenticatedDocumentLoader(identity, {}, hops)(url), FetchError);
623
+ deepStrictEqual(hops.events.slice(0, 2), [`open ${url}`, `fetch #1 ${url}`]);
624
+ deepStrictEqual(hops.events.at(-1), `close ${url}`);
625
+ hops.assertInvariants();
626
+ });
627
+ });
628
+ test("getAuthenticatedDocumentLoader() completes with responses cloned by fetch-mock", async () => {
629
+ esm_default.mockGlobal();
630
+ try {
631
+ const base = "https://example.com/tee/";
632
+ esm_default.get(`${base}start`, () => new Response("Found", {
633
+ status: 302,
634
+ headers: { Location: `${base}page` }
635
+ }));
636
+ esm_default.get(`${base}page`, () => new Response("not JSON", { headers: {
637
+ "Content-Type": "text/plain",
638
+ Link: `<${base}document>; rel="alternate"; type="application/activity+json"`
639
+ } }));
640
+ esm_default.get(`${base}document`, () => new Response("{\"done\":true}", { headers: { "Content-Type": "application/activity+json" } }));
641
+ esm_default.get(`${base}missing`, () => new Response("Not Found", { status: 404 }));
642
+ const loader = createAuthenticatedDocumentLoader(identity, {}, directDeps);
643
+ deepStrictEqual((await loader(`${base}start`)).document, { done: true });
644
+ await rejects(loader(`${base}missing`), FetchError);
645
+ } finally {
646
+ esm_default.hardReset();
647
+ }
648
+ });
448
649
  const unsupportedUrls = [
449
650
  "ftp://example.com/collection",
450
651
  "file:///collection",
@@ -579,5 +780,88 @@ for (const url of unsupportedUrls) {
579
780
  });
580
781
  });
581
782
  }
783
+ function createRecordedResponse(events, index, init, cancelSuffix = () => "") {
784
+ const bytes = new TextEncoder().encode(init.body ?? "");
785
+ const chunkSize = init.chunkSize ?? 256;
786
+ let offset = 0;
787
+ let settled = false;
788
+ const body = new ReadableStream({
789
+ pull(controller) {
790
+ if (offset >= bytes.length) {
791
+ settled = true;
792
+ events.push(`consumed #${index}`);
793
+ controller.close();
794
+ return;
795
+ }
796
+ controller.enqueue(bytes.slice(offset, offset + chunkSize));
797
+ offset += chunkSize;
798
+ },
799
+ cancel() {
800
+ settled = true;
801
+ events.push(`cancel #${index}${cancelSuffix()}`);
802
+ if (init.failCancel) throw new Error(`cancel #${index} failed`);
803
+ if (init.hangCancel) return new Promise(() => {});
804
+ }
805
+ }, { highWaterMark: 0 });
806
+ return {
807
+ response: new Response(body, {
808
+ status: init.status ?? 200,
809
+ headers: init.headers
810
+ }),
811
+ settled: () => settled
812
+ };
813
+ }
814
+ function recordHops(respond) {
815
+ const events = [];
816
+ const requests = [];
817
+ const unsettledAtClose = [];
818
+ let opens = 0;
819
+ let closes = 0;
820
+ let fetches = 0;
821
+ return {
822
+ events,
823
+ requests,
824
+ openHop(url) {
825
+ opens++;
826
+ events.push(`open ${url}`);
827
+ const tracked = [];
828
+ let closed = false;
829
+ let closing = false;
830
+ return Promise.resolve({
831
+ url: new URL(url),
832
+ async fetch(input, init) {
833
+ if (closed) throw new TypeError("The session is closed.");
834
+ const request = new Request(input instanceof Request ? input.clone() : input, init);
835
+ const index = ++fetches;
836
+ requests.push(request);
837
+ events.push(`fetch #${index} ${request.url}`);
838
+ const recorded = createRecordedResponse(events, index, await respond(request, index), () => closing ? " by close" : "");
839
+ tracked.push({
840
+ ...recorded,
841
+ index
842
+ });
843
+ return recorded.response;
844
+ },
845
+ close() {
846
+ if (closed) return Promise.resolve();
847
+ closed = true;
848
+ closes++;
849
+ closing = true;
850
+ for (const { response, settled, index } of tracked) {
851
+ if (settled()) continue;
852
+ if (response.body?.locked) unsettledAtClose.push(index);
853
+ else response.body?.cancel().catch(() => {});
854
+ }
855
+ events.push(`close ${url}`);
856
+ return Promise.resolve();
857
+ }
858
+ });
859
+ },
860
+ assertInvariants() {
861
+ deepStrictEqual(opens, closes);
862
+ deepStrictEqual(unsettledAtClose, []);
863
+ }
864
+ };
865
+ }
582
866
  //#endregion
583
867
  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-B4sDuLjb.cjs");
4
+ const require_kv_cache = require("../kv-cache-ivTOKx4b.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-BkPB4hPe.cjs";
2
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-BPDfu4Ey.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-BKJyD7nk.js";
2
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../mod-DGP89q0u.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-C-RyNMt6.js";
3
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "../kv-cache-5TdAMHng.js";
4
4
  export { getAuthenticatedDocumentLoader, kvCache };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/fedify",
3
- "version": "2.0.35-dev.2480+2330a2e7",
3
+ "version": "2.0.35",
4
4
  "description": "An ActivityPub server framework",
5
5
  "keywords": [
6
6
  "ActivityPub",
@@ -144,9 +144,9 @@
144
144
  "uri-template-router": "^1.0.0",
145
145
  "url-template": "^3.1.1",
146
146
  "urlpattern-polyfill": "^10.1.0",
147
- "@fedify/vocab-runtime": "2.0.35-dev.2480+2330a2e7",
148
- "@fedify/vocab": "2.0.35-dev.2480+2330a2e7",
149
- "@fedify/webfinger": "2.0.35-dev.2480+2330a2e7"
147
+ "@fedify/vocab": "2.0.35",
148
+ "@fedify/webfinger": "2.0.35",
149
+ "@fedify/vocab-runtime": "2.0.35"
150
150
  },
151
151
  "devDependencies": {
152
152
  "@std/assert": "npm:@jsr/std__assert@^0.226.0",
@@ -159,7 +159,7 @@
159
159
  "typescript": "^6.0.0",
160
160
  "wrangler": "^4.17.0",
161
161
  "@fedify/fixture": "2.0.0",
162
- "@fedify/vocab-tools": "^2.0.35-dev.2480+2330a2e7"
162
+ "@fedify/vocab-tools": "^2.0.35"
163
163
  },
164
164
  "scripts": {
165
165
  "build:self": "tsdown",
@@ -1,82 +0,0 @@
1
- import "@js-temporal/polyfill";
2
- import "urlpattern-polyfill";
3
- globalThis.addEventListener = () => {};
4
- import { o as validateCryptoKey } from "./key-BFzQ3Hef.mjs";
5
- import { n as doubleKnock } from "./http-D0cjeL8a.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, 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
- specDeterminer,
45
- log: curry(logRequest)(logger),
46
- tracerProvider,
47
- signal: options?.signal,
48
- validateRedirect: async (nextUrl) => {
49
- follow(nextUrl);
50
- await validateUrl(nextUrl);
51
- visited.add(nextUrl);
52
- currentUrl = nextUrl;
53
- }
54
- });
55
- return getRemoteDocument(currentUrl, response, (alternateUrl) => {
56
- follow(alternateUrl);
57
- return load(alternateUrl, options, redirected, visited);
58
- });
59
- }
60
- async function validateUrl(url) {
61
- const parsedUrl = new URL(url);
62
- const protocol = parsedUrl.protocol;
63
- if (protocol !== "http:" && protocol !== "https:") throw new UrlError(`Unsupported protocol: ${protocol}`);
64
- if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new UrlError("URLs with credentials are not allowed");
65
- if (!allowPrivateAddress) try {
66
- await validatePublicUrl(url);
67
- } catch (error) {
68
- if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
69
- url,
70
- error
71
- });
72
- else logger.error("Disallowed private URL: {url}", {
73
- url,
74
- error
75
- });
76
- throw error;
77
- }
78
- }
79
- return (url, options) => load(url, options);
80
- }
81
- //#endregion
82
- export { getAuthenticatedDocumentLoader as t };