@fedify/fedify 2.4.5-dev.2462 → 2.4.6-dev.2628

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 (119) hide show
  1. package/README.md +3 -0
  2. package/dist/{builder-BBUGd9wr.mjs → builder-kj5boG2l.mjs} +2 -2
  3. package/dist/{client-D-XP_Ccb.d.cts → client-3InDZbba.d.cts} +0 -1
  4. package/dist/{client-D-XP_Ccb.d.ts → client-3InDZbba.d.ts} +0 -1
  5. package/dist/{client-CEnNz8O7.mjs → client-5SIFzW3q.mjs} +113 -64
  6. package/dist/compat/mod.d.cts +1 -1
  7. package/dist/compat/mod.d.ts +1 -1
  8. package/dist/compat/transformers.test.mjs +1 -1
  9. package/dist/{compound-proof-CariICma.mjs → compound-proof-BoNAYSHt.mjs} +2 -2
  10. package/dist/{context-7NyiqIcg.d.ts → context-CVOOd9pD.d.ts} +28 -2
  11. package/dist/{context-tsYPJ2Ew.d.cts → context-D0gFIgev.d.cts} +28 -2
  12. package/dist/{deno-BO7-a3hz.mjs → deno-ksnVeCA7.mjs} +1 -1
  13. package/dist/docloader-BhVNi06k.mjs +136 -0
  14. package/dist/federation/builder.test.mjs +1 -1
  15. package/dist/federation/compound-fanout.test.mjs +6 -3
  16. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  17. package/dist/federation/handler.test.mjs +4 -4
  18. package/dist/federation/hashlink-media.test.mjs +2 -2
  19. package/dist/federation/idempotency.test.mjs +2 -2
  20. package/dist/federation/inbox-report.test.mjs +5 -5
  21. package/dist/federation/inbox.test.mjs +1 -1
  22. package/dist/federation/metrics.test.mjs +1 -1
  23. package/dist/federation/middleware.test.mjs +465 -32
  24. package/dist/federation/mod.cjs +1 -1
  25. package/dist/federation/mod.d.cts +3 -3
  26. package/dist/federation/mod.d.ts +3 -3
  27. package/dist/federation/mod.js +1 -1
  28. package/dist/federation/outgoing-proof.test.mjs +9 -6
  29. package/dist/federation/portable-actor.test.mjs +3 -3
  30. package/dist/federation/portable-chain.test.mjs +3 -3
  31. package/dist/federation/portable-collection.test.mjs +4 -4
  32. package/dist/federation/portable-delivery.test.mjs +2 -2
  33. package/dist/federation/portable-gateway-keys.test.mjs +11 -5
  34. package/dist/federation/portable-inbox.test.mjs +4 -4
  35. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  36. package/dist/federation/portable-route-activity.test.mjs +2 -2
  37. package/dist/federation/portable.test.mjs +3 -3
  38. package/dist/federation/send.test.mjs +498 -88
  39. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  40. package/dist/federation/tasks/tasks.test.mjs +2 -2
  41. package/dist/federation/temporal.test.mjs +1 -1
  42. package/dist/federation/webfinger.test.mjs +1 -1
  43. package/dist/{federation-Dd9IwOEi.cjs → federation-BtPz9sr3.cjs} +240 -102
  44. package/dist/{http-D7L2pqLT.mjs → http-BSARoDPh.mjs} +204 -106
  45. package/dist/{http-DLf8QYD6.cjs → http-CeVcKQMZ.cjs} +179 -110
  46. package/dist/{http-BVrOR6MG.js → http-rH230xCD.js} +178 -109
  47. package/dist/{inbox-DtEKOr3W.mjs → inbox-BdZenpZQ.mjs} +2 -2
  48. package/dist/{key-DYxuyd-s.d.cts → key-CyCcut3h.d.ts} +1 -2
  49. package/dist/{key-C1nv7_jD.d.ts → key-VEL1QqHF.d.cts} +1 -2
  50. package/dist/{kv-cache-Dkzuqs8B.cjs → kv-cache-BpGIONmd.cjs} +72 -27
  51. package/dist/{kv-cache-DoZoRS9p.js → kv-cache-CNfNI_95.js} +73 -28
  52. package/dist/{kv-cache-m3RIr811.mjs → kv-cache-Cxn7pNMx.mjs} +1 -1
  53. package/dist/{ld-PJLGA2MJ.mjs → ld-DJZrjNuN.mjs} +2 -2
  54. package/dist/{metrics-CGR9cI-Y.mjs → metrics-DtxsYIkN.mjs} +1 -1
  55. package/dist/{middleware-BxhBd7pw.js → middleware-DDvck2q9.js} +241 -103
  56. package/dist/{middleware-ByanhTvb.mjs → middleware-DblGj642.mjs} +82 -42
  57. package/dist/{middleware-DPI-Jp9w.mjs → middleware-DyEjt33n.mjs} +1 -1
  58. package/dist/{mod-DEZXwV6J.d.ts → mod-B1Qt5cQQ.d.ts} +2 -9
  59. package/dist/{mod-Xj5P4s4d.d.ts → mod-B5COx5Wd.d.ts} +1 -1
  60. package/dist/{mod-CavhAgLw.d.cts → mod-BTxJG7ZH.d.cts} +1 -1
  61. package/dist/{mod-CtpF2BU5.d.ts → mod-CpyjFA9s.d.ts} +1 -1
  62. package/dist/{mod-BojP2Bw3.d.cts → mod-makPMVMg.d.cts} +1 -1
  63. package/dist/{mod-Dw-nPxQh.d.cts → mod-ss0wBUH1.d.cts} +2 -9
  64. package/dist/mod.cjs +5 -5
  65. package/dist/mod.d.cts +6 -6
  66. package/dist/mod.d.ts +6 -6
  67. package/dist/mod.js +5 -5
  68. package/dist/nodeinfo/client.test.mjs +337 -19
  69. package/dist/nodeinfo/handler.test.mjs +1 -1
  70. package/dist/nodeinfo/mod.cjs +1 -1
  71. package/dist/nodeinfo/mod.d.cts +1 -1
  72. package/dist/nodeinfo/mod.d.ts +1 -1
  73. package/dist/nodeinfo/mod.js +1 -1
  74. package/dist/otel/exporter.test.mjs +292 -30
  75. package/dist/otel/mod.cjs +36 -30
  76. package/dist/otel/mod.d.cts +13 -0
  77. package/dist/otel/mod.d.ts +13 -0
  78. package/dist/otel/mod.js +36 -30
  79. package/dist/{owner-B5c276hH.mjs → owner-2vfqhUJw.mjs} +3 -3
  80. package/dist/{portable-collection-BMZY9qT-.js → portable-collection-B7AQ_dW7.js} +1 -1
  81. package/dist/{portable-collection-CR5uwjws.cjs → portable-collection-Cs--KD5A.cjs} +1 -1
  82. package/dist/{portable-collection-C88tBiy0.mjs → portable-collection-DKJwGO8S.mjs} +2 -2
  83. package/dist/{portable-key-DxTXUpCK.mjs → portable-key-DXGepvtD.mjs} +1 -1
  84. package/dist/{proof-DNu-qP0i.mjs → proof-DW0FK6Cp.mjs} +3 -3
  85. package/dist/{send-CdCifQs4.mjs → send-CqsocpvF.mjs} +174 -76
  86. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  87. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  88. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  89. package/dist/sig/compound-proof.test.mjs +2 -2
  90. package/dist/sig/http.test.mjs +553 -2
  91. package/dist/sig/key.test.mjs +1 -1
  92. package/dist/sig/ld.test.mjs +1 -1
  93. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  94. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  95. package/dist/sig/mod.cjs +2 -2
  96. package/dist/sig/mod.d.cts +2 -2
  97. package/dist/sig/mod.d.ts +2 -2
  98. package/dist/sig/mod.js +2 -2
  99. package/dist/sig/owner.test.mjs +1 -1
  100. package/dist/sig/portable-collection.test.mjs +1 -1
  101. package/dist/sig/portable-interop.test.mjs +3 -3
  102. package/dist/sig/portable-key.test.mjs +5 -5
  103. package/dist/sig/proof.test.mjs +4 -4
  104. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  105. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  106. package/dist/sig/verification.test.mjs +3 -3
  107. package/dist/{temporal-BuMu2bgP.mjs → temporal-Dm2E1-vs.mjs} +1 -1
  108. package/dist/testing/kv.test.mjs +1 -1
  109. package/dist/testing/mod.d.mts +26 -0
  110. package/dist/{types-_S2_MeLF.js → types-DUjZb4i6.js} +112 -63
  111. package/dist/{types-poIao40f.cjs → types-hKCCjii0.cjs} +111 -62
  112. package/dist/utils/docloader.test.mjs +344 -36
  113. package/dist/utils/kv-cache.test.mjs +1 -1
  114. package/dist/utils/mod.cjs +1 -1
  115. package/dist/utils/mod.d.cts +1 -1
  116. package/dist/utils/mod.d.ts +1 -1
  117. package/dist/utils/mod.js +1 -1
  118. package/package.json +7 -7
  119. package/dist/docloader-DQNRDYDb.mjs +0 -91
@@ -5,12 +5,21 @@ import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
6
6
  import { t as assertInstanceOf } from "../assert_instance_of-mrXWWNY-.mjs";
7
7
  import "../std__assert-DvMDRPes.mjs";
8
- import { a as parseProtocol, c as parseUsage, i as parseOutboundService, n as parseInboundService, o as parseServices, r as parseNodeInfo, s as parseSoftware, t as getNodeInfo } from "../client-CEnNz8O7.mjs";
8
+ import { a as parseOutboundService, c as parseSoftware, i as parseNodeInfo, l as parseUsage, n as getNodeInfoInternal, o as parseProtocol, r as parseInboundService, s as parseServices, t as getNodeInfo } from "../client-5SIFzW3q.mjs";
9
9
  import { configure, reset } from "@logtape/logtape";
10
10
  import { UrlError } from "@fedify/vocab-runtime";
11
11
  import { test } from "@fedify/fixture";
12
- import dns from "node:dns/promises";
12
+ import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
13
13
  //#region src/nodeinfo/client.test.ts
14
+ function getNodeInfoDirect(url, options) {
15
+ return getNodeInfoInternal(url, options, { openHop: openDirectFetch });
16
+ }
17
+ function getNodeInfoValidated(url, options) {
18
+ return getNodeInfoInternal(url, options, { openHop: (hopUrl) => new URL(hopUrl).hostname === "example.com" ? openDirectFetch(hopUrl) : openPublicFetch(hopUrl, { lookup: () => Promise.resolve([{
19
+ address: "93.184.215.14",
20
+ family: 4
21
+ }]) }) });
22
+ }
14
23
  test("getNodeInfo()", async (t) => {
15
24
  esm_default.spyGlobal();
16
25
  esm_default.get("https://example.com/.well-known/nodeinfo", { body: { links: [{
@@ -44,25 +53,25 @@ test("getNodeInfo()", async (t) => {
44
53
  }
45
54
  };
46
55
  await t.step("indirect", async () => {
47
- assertEquals(await getNodeInfo("https://example.com/"), expected);
48
- assertEquals(await getNodeInfo("https://example.com/", { parse: "none" }), rawExpected);
56
+ assertEquals(await getNodeInfoDirect("https://example.com/"), expected);
57
+ assertEquals(await getNodeInfoDirect("https://example.com/", { parse: "none" }), rawExpected);
49
58
  });
50
59
  await t.step("direct", async () => {
51
- assertEquals(await getNodeInfo("https://example.com/nodeinfo/2.1", { direct: true }), expected);
60
+ assertEquals(await getNodeInfoDirect("https://example.com/nodeinfo/2.1", { direct: true }), expected);
52
61
  });
53
62
  esm_default.removeRoutes();
54
63
  esm_default.get("https://example.com/.well-known/nodeinfo", { body: { links: [] } });
55
64
  await t.step("indirect: no links", async () => {
56
- assertEquals(await getNodeInfo("https://example.com/"), void 0);
65
+ assertEquals(await getNodeInfoDirect("https://example.com/"), void 0);
57
66
  });
58
67
  esm_default.removeRoutes();
59
68
  esm_default.get("https://example.com/.well-known/nodeinfo", { status: 404 });
60
69
  await t.step("indirect: 404", async () => {
61
- assertEquals(await getNodeInfo("https://example.com/"), void 0);
70
+ assertEquals(await getNodeInfoDirect("https://example.com/"), void 0);
62
71
  });
63
72
  await t.step("direct: 404", async () => {
64
- assertEquals(await getNodeInfo("https://example.com/nodeinfo/2.0", { direct: true }), void 0);
65
- assertEquals(await getNodeInfo("https://example.com/404", { direct: true }), void 0);
73
+ assertEquals(await getNodeInfoDirect("https://example.com/nodeinfo/2.0", { direct: true }), void 0);
74
+ assertEquals(await getNodeInfoDirect("https://example.com/404", { direct: true }), void 0);
66
75
  });
67
76
  esm_default.hardReset();
68
77
  });
@@ -79,7 +88,7 @@ test("getNodeInfo() blocks SSRF", async (t) => {
79
88
  internalFetched = true;
80
89
  return { body: { secret: "INTERNAL_DATA_LEAKED" } };
81
90
  });
82
- assertEquals(await getNodeInfo("https://example.com/", { parse: "none" }), void 0);
91
+ assertEquals(await getNodeInfoValidated("https://example.com/", { parse: "none" }), void 0);
83
92
  assertEquals(internalFetched, false);
84
93
  });
85
94
  await t.step("link href using a data: URL", async () => {
@@ -88,7 +97,7 @@ test("getNodeInfo() blocks SSRF", async (t) => {
88
97
  rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
89
98
  href: "data:application/json,{\"software\":{\"name\":\"x\"}}"
90
99
  }] } });
91
- assertEquals(await getNodeInfo("https://example.com/", { parse: "none" }), void 0);
100
+ assertEquals(await getNodeInfoValidated("https://example.com/", { parse: "none" }), void 0);
92
101
  });
93
102
  await t.step("redirect to a private address", async () => {
94
103
  esm_default.removeRoutes();
@@ -105,7 +114,7 @@ test("getNodeInfo() blocks SSRF", async (t) => {
105
114
  internalFetched = true;
106
115
  return { body: { secret: "INTERNAL_DATA_LEAKED" } };
107
116
  });
108
- assertEquals(await getNodeInfo("https://example.com/", { parse: "none" }), void 0);
117
+ assertEquals(await getNodeInfoValidated("https://example.com/", { parse: "none" }), void 0);
109
118
  assertEquals(internalFetched, false);
110
119
  });
111
120
  await t.step("direct fetch of a private address", async () => {
@@ -118,7 +127,7 @@ test("getNodeInfo() blocks SSRF", async (t) => {
118
127
  version: "1.0.0"
119
128
  } } };
120
129
  });
121
- assertEquals(await getNodeInfo("http://127.0.0.1:8080/nodeinfo", {
130
+ assertEquals(await getNodeInfoValidated("http://127.0.0.1:8080/nodeinfo", {
122
131
  direct: true,
123
132
  parse: "none"
124
133
  }), void 0);
@@ -998,17 +1007,16 @@ test("getNodeInfo() bounds both discovery and document responses", async () => {
998
1007
  esm_default.hardReset();
999
1008
  }
1000
1009
  });
1001
- test("getNodeInfo() logs DNS failures as such", { ignore: "Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted" }, async (t) => {
1010
+ test("getNodeInfo() logs DNS failures as such", async (t) => {
1002
1011
  for (const result of [
1003
1012
  "throws",
1004
1013
  "empty",
1005
1014
  "private"
1006
1015
  ]) await t.step(result, async () => {
1007
- const originalLookup = dns.lookup;
1008
- dns.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
1016
+ const lookup = () => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
1009
1017
  address: "127.0.0.1",
1010
1018
  family: 4
1011
- }]));
1019
+ }]);
1012
1020
  const records = [];
1013
1021
  await configure({
1014
1022
  sinks: { buffer: (record) => records.push(record) },
@@ -1023,7 +1031,7 @@ test("getNodeInfo() logs DNS failures as such", { ignore: "Deno" in globalThis &
1023
1031
  reset: true
1024
1032
  });
1025
1033
  try {
1026
- assertEquals(await getNodeInfo("https://dns-failure.invalid/"), void 0);
1034
+ assertEquals(await getNodeInfoInternal("https://dns-failure.invalid/", {}, { openHop: (url) => openPublicFetch(url, { lookup }) }), void 0);
1027
1035
  assertEquals(records.map((r) => [
1028
1036
  r.level,
1029
1037
  r.rawMessage,
@@ -1038,9 +1046,319 @@ test("getNodeInfo() logs DNS failures as such", { ignore: "Deno" in globalThis &
1038
1046
  assertEquals(error.reason, result === "private" ? "disallowed" : "dns");
1039
1047
  } finally {
1040
1048
  await reset();
1041
- dns.lookup = originalLookup;
1042
1049
  }
1043
1050
  });
1044
1051
  });
1052
+ function recordHops(respond) {
1053
+ const events = [];
1054
+ const unsettled = [];
1055
+ let opens = 0;
1056
+ let closes = 0;
1057
+ let responses = 0;
1058
+ return {
1059
+ events,
1060
+ async openHop(url) {
1061
+ opens++;
1062
+ events.push(`open ${url}`);
1063
+ const states = [];
1064
+ let closed = false;
1065
+ return {
1066
+ url: new URL(url),
1067
+ async fetch(input) {
1068
+ if (closed) throw new TypeError("The fetch session is closed.");
1069
+ const target = new URL(input instanceof Request ? input.url : input);
1070
+ const id = ++responses;
1071
+ events.push(`fetch ${target.href} #${id}`);
1072
+ const fake = respond(target);
1073
+ const state = {
1074
+ id,
1075
+ settled: false
1076
+ };
1077
+ states.push(state);
1078
+ let sent = false;
1079
+ const body = new ReadableStream({
1080
+ pull(controller) {
1081
+ if (!sent && fake.body != null && fake.body !== "") {
1082
+ sent = true;
1083
+ controller.enqueue(new TextEncoder().encode(fake.body));
1084
+ return;
1085
+ }
1086
+ state.settled = true;
1087
+ events.push(`consumed #${id}`);
1088
+ controller.close();
1089
+ },
1090
+ cancel() {
1091
+ state.settled = true;
1092
+ events.push(`cancel #${id}`);
1093
+ if (fake.cancelFails) throw new Error("Cancellation failed");
1094
+ if (fake.cancelNeverSettles) return new Promise(() => {});
1095
+ }
1096
+ }, { highWaterMark: 0 });
1097
+ const response = new Response(body, {
1098
+ status: fake.status ?? 200,
1099
+ headers: fake.headers
1100
+ });
1101
+ if (fake.url != null) Object.defineProperty(response, "url", { value: fake.url });
1102
+ return response;
1103
+ },
1104
+ async close() {
1105
+ if (closed) return;
1106
+ closed = true;
1107
+ closes++;
1108
+ for (const state of states) if (!state.settled) unsettled.push(`#${state.id}`);
1109
+ events.push(`close ${url}`);
1110
+ }
1111
+ };
1112
+ },
1113
+ assertInvariants() {
1114
+ assertEquals(closes, opens);
1115
+ assertEquals(unsettled, []);
1116
+ }
1117
+ };
1118
+ }
1119
+ test("getNodeInfo() closes each hop after cleaning up its body", async (t) => {
1120
+ const wellKnown = "https://example.com/.well-known/nodeinfo";
1121
+ const wellKnown2 = "https://example.com/.well-known/nodeinfo2";
1122
+ const documentUrl = "https://example.com/nodeinfo/2.1";
1123
+ const rawDocument = {
1124
+ software: {
1125
+ name: "foo",
1126
+ version: "1.2.3"
1127
+ },
1128
+ protocols: ["activitypub"],
1129
+ usage: {
1130
+ users: {},
1131
+ localPosts: 0,
1132
+ localComments: 0
1133
+ }
1134
+ };
1135
+ const descriptor = JSON.stringify({ links: [{
1136
+ rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
1137
+ href: documentUrl
1138
+ }] });
1139
+ await t.step("redirect", async () => {
1140
+ const recorder = recordHops((url) => {
1141
+ switch (url.href) {
1142
+ case wellKnown: return {
1143
+ status: 302,
1144
+ headers: { Location: "/.well-known/nodeinfo2" },
1145
+ body: "redirecting",
1146
+ url: "https://93.184.215.14/.well-known/nodeinfo"
1147
+ };
1148
+ case wellKnown2: return { body: descriptor };
1149
+ default: return { body: JSON.stringify(rawDocument) };
1150
+ }
1151
+ });
1152
+ assertEquals(await getNodeInfoInternal("https://example.com/", { parse: "none" }, { openHop: recorder.openHop }), rawDocument);
1153
+ assertEquals(recorder.events, [
1154
+ `open ${wellKnown}`,
1155
+ `fetch ${wellKnown} #1`,
1156
+ "cancel #1",
1157
+ `close ${wellKnown}`,
1158
+ `open ${wellKnown2}`,
1159
+ `fetch ${wellKnown2} #2`,
1160
+ "consumed #2",
1161
+ `close ${wellKnown2}`,
1162
+ `open ${documentUrl}`,
1163
+ `fetch ${documentUrl} #3`,
1164
+ "consumed #3",
1165
+ `close ${documentUrl}`
1166
+ ]);
1167
+ recorder.assertInvariants();
1168
+ });
1169
+ await t.step("non-2xx responses", async () => {
1170
+ const recorder = recordHops(() => ({
1171
+ status: 404,
1172
+ body: "not found"
1173
+ }));
1174
+ assertEquals(await getNodeInfoInternal("https://example.com/", {}, { openHop: recorder.openHop }), void 0);
1175
+ assertEquals(await getNodeInfoInternal(documentUrl, { direct: true }, { openHop: recorder.openHop }), void 0);
1176
+ assertEquals(recorder.events, [
1177
+ `open ${wellKnown}`,
1178
+ `fetch ${wellKnown} #1`,
1179
+ "cancel #1",
1180
+ `close ${wellKnown}`,
1181
+ `open ${documentUrl}`,
1182
+ `fetch ${documentUrl} #2`,
1183
+ "cancel #2",
1184
+ `close ${documentUrl}`
1185
+ ]);
1186
+ recorder.assertInvariants();
1187
+ });
1188
+ await t.step("too many redirections", async () => {
1189
+ const recorder = recordHops(() => ({
1190
+ status: 302,
1191
+ headers: { Location: "/.well-known/nodeinfo" },
1192
+ body: "redirecting"
1193
+ }));
1194
+ assertEquals(await getNodeInfoInternal("https://example.com/", {}, { openHop: recorder.openHop }), void 0);
1195
+ assertEquals(recorder.events.filter((e) => e.startsWith("open ")).length, 5);
1196
+ assertEquals(recorder.events.filter((e) => e.startsWith("cancel ")).length, 5);
1197
+ recorder.assertInvariants();
1198
+ });
1199
+ await t.step("failed redirect body cancellation", async () => {
1200
+ const recorder = recordHops((url) => {
1201
+ switch (url.href) {
1202
+ case wellKnown: return {
1203
+ status: 302,
1204
+ headers: { Location: "/.well-known/nodeinfo2" },
1205
+ body: "redirecting",
1206
+ cancelFails: true
1207
+ };
1208
+ case wellKnown2: return { body: descriptor };
1209
+ default: return {
1210
+ status: 404,
1211
+ body: "not found",
1212
+ cancelFails: true
1213
+ };
1214
+ }
1215
+ });
1216
+ const records = [];
1217
+ await configure({
1218
+ sinks: { buffer: (record) => records.push(record) },
1219
+ loggers: [{
1220
+ category: "fedify",
1221
+ sinks: ["buffer"],
1222
+ lowestLevel: "debug"
1223
+ }, {
1224
+ category: ["logtape", "meta"],
1225
+ sinks: []
1226
+ }],
1227
+ reset: true
1228
+ });
1229
+ try {
1230
+ assertEquals(await getNodeInfoInternal("https://example.com/", {}, { openHop: recorder.openHop }), void 0);
1231
+ const failures = records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}");
1232
+ assertEquals(failures.map((r) => [r.level, r.properties.url]), [["debug", wellKnown], ["debug", documentUrl]]);
1233
+ for (const { properties: { error } } of failures) {
1234
+ assertInstanceOf(error, Error);
1235
+ assertEquals(error.message, "Cancellation failed");
1236
+ }
1237
+ } finally {
1238
+ await reset();
1239
+ }
1240
+ assertEquals(recorder.events, [
1241
+ `open ${wellKnown}`,
1242
+ `fetch ${wellKnown} #1`,
1243
+ "cancel #1",
1244
+ `close ${wellKnown}`,
1245
+ `open ${wellKnown2}`,
1246
+ `fetch ${wellKnown2} #2`,
1247
+ "consumed #2",
1248
+ `close ${wellKnown2}`,
1249
+ `open ${documentUrl}`,
1250
+ `fetch ${documentUrl} #3`,
1251
+ "cancel #3",
1252
+ `close ${documentUrl}`
1253
+ ]);
1254
+ recorder.assertInvariants();
1255
+ });
1256
+ await t.step("never-settling body cancellation", async () => {
1257
+ const recorder = recordHops((url) => {
1258
+ switch (url.href) {
1259
+ case wellKnown: return {
1260
+ status: 302,
1261
+ headers: { Location: "/.well-known/nodeinfo2" },
1262
+ body: "redirecting",
1263
+ cancelNeverSettles: true
1264
+ };
1265
+ case wellKnown2: return { body: descriptor };
1266
+ default: return {
1267
+ status: 404,
1268
+ body: "not found",
1269
+ cancelNeverSettles: true
1270
+ };
1271
+ }
1272
+ });
1273
+ assertEquals(await settleWithin(getNodeInfoInternal("https://example.com/", {}, { openHop: recorder.openHop })), void 0);
1274
+ assertEquals(recorder.events, [
1275
+ `open ${wellKnown}`,
1276
+ `fetch ${wellKnown} #1`,
1277
+ "cancel #1",
1278
+ `close ${wellKnown}`,
1279
+ `open ${wellKnown2}`,
1280
+ `fetch ${wellKnown2} #2`,
1281
+ "consumed #2",
1282
+ `close ${wellKnown2}`,
1283
+ `open ${documentUrl}`,
1284
+ `fetch ${documentUrl} #3`,
1285
+ "cancel #3",
1286
+ `close ${documentUrl}`
1287
+ ]);
1288
+ recorder.assertInvariants();
1289
+ });
1290
+ await t.step("failure while consuming", async () => {
1291
+ const recorder = recordHops(() => ({ body: "not json" }));
1292
+ assertEquals(await getNodeInfoInternal("https://example.com/", {}, { openHop: recorder.openHop }), void 0);
1293
+ assertEquals(recorder.events, [
1294
+ `open ${wellKnown}`,
1295
+ `fetch ${wellKnown} #1`,
1296
+ "consumed #1",
1297
+ `close ${wellKnown}`
1298
+ ]);
1299
+ recorder.assertInvariants();
1300
+ });
1301
+ });
1302
+ /**
1303
+ * Rejects if the given promise does not settle within the timeout, so that
1304
+ * a hang fails the test instead of blocking it.
1305
+ */
1306
+ async function settleWithin(promise, ms = 5e3) {
1307
+ let timer;
1308
+ try {
1309
+ return await Promise.race([promise, new Promise((_, reject) => {
1310
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Did not settle within ${ms} ms.`)), ms);
1311
+ })]);
1312
+ } finally {
1313
+ clearTimeout(timer);
1314
+ }
1315
+ }
1316
+ function neverCancellableResponse(init) {
1317
+ return new Response(new ReadableStream({
1318
+ start(controller) {
1319
+ controller.enqueue(new TextEncoder().encode("never cancelled"));
1320
+ },
1321
+ cancel: () => new Promise(() => {})
1322
+ }), init);
1323
+ }
1324
+ test("getNodeInfo() does not wait for cancelling response bodies", async (t) => {
1325
+ const rawDocument = {
1326
+ software: {
1327
+ name: "foo",
1328
+ version: "1.2.3"
1329
+ },
1330
+ protocols: ["activitypub"],
1331
+ usage: {
1332
+ users: {},
1333
+ localPosts: 0,
1334
+ localComments: 0
1335
+ }
1336
+ };
1337
+ esm_default.mockGlobal();
1338
+ try {
1339
+ await t.step("redirect", async () => {
1340
+ esm_default.removeRoutes();
1341
+ esm_default.get("https://example.com/.well-known/nodeinfo", () => neverCancellableResponse({
1342
+ status: 302,
1343
+ headers: { Location: "/.well-known/nodeinfo2" }
1344
+ }));
1345
+ esm_default.get("https://example.com/.well-known/nodeinfo2", () => Response.json({ links: [{
1346
+ rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
1347
+ href: "https://example.com/nodeinfo/2.1"
1348
+ }] }));
1349
+ esm_default.get("https://example.com/nodeinfo/2.1", () => Response.json(rawDocument));
1350
+ assertEquals(await settleWithin(getNodeInfoDirect("https://example.com/", { parse: "none" })), rawDocument);
1351
+ });
1352
+ await t.step("non-2xx responses", async () => {
1353
+ esm_default.removeRoutes();
1354
+ esm_default.get("https://example.com/.well-known/nodeinfo", () => neverCancellableResponse({ status: 404 }));
1355
+ esm_default.get("https://example.com/nodeinfo/2.1", () => neverCancellableResponse({ status: 404 }));
1356
+ assertEquals(await settleWithin(getNodeInfoDirect("https://example.com/")), void 0);
1357
+ assertEquals(await settleWithin(getNodeInfoDirect("https://example.com/nodeinfo/2.1", { direct: true })), void 0);
1358
+ });
1359
+ } finally {
1360
+ esm_default.hardReset();
1361
+ }
1362
+ });
1045
1363
  //#endregion
1046
1364
  export {};
@@ -5,7 +5,7 @@ import { r as createRequestContext } from "../context-8Tu9kea0.mjs";
5
5
  import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
6
6
  import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
7
7
  import "../std__assert-DvMDRPes.mjs";
8
- import { D as handleNodeInfoJrd, E as handleNodeInfo, o as createFederation } from "../middleware-ByanhTvb.mjs";
8
+ import { D as handleNodeInfoJrd, E as handleNodeInfo, o as createFederation } from "../middleware-DblGj642.mjs";
9
9
  import { test } from "@fedify/fixture";
10
10
  //#region src/nodeinfo/handler.test.ts
11
11
  test("handleNodeInfo()", async () => {
@@ -1,6 +1,6 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const require_types = require("../types-poIao40f.cjs");
3
+ const require_types = require("../types-hKCCjii0.cjs");
4
4
  exports.getNodeInfo = require_types.getNodeInfo;
5
5
  exports.nodeInfoToJson = require_types.nodeInfoToJson;
6
6
  exports.parseNodeInfo = require_types.parseNodeInfo;
@@ -1,3 +1,3 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { a as InboundService, c as OutboundService, d as Software, f as Usage, i as parseNodeInfo, l as Protocol, n as ParseNodeInfoOptions, o as JsonValue, p as nodeInfoToJson, r as getNodeInfo, s as NodeInfo, t as GetNodeInfoOptions, u as Services } from "../client-D-XP_Ccb.cjs";
2
+ import { a as InboundService, c as OutboundService, d as Software, f as Usage, i as parseNodeInfo, l as Protocol, n as ParseNodeInfoOptions, o as JsonValue, p as nodeInfoToJson, r as getNodeInfo, s as NodeInfo, t as GetNodeInfoOptions, u as Services } from "../client-3InDZbba.cjs";
3
3
  export { type GetNodeInfoOptions, InboundService, JsonValue, NodeInfo, OutboundService, type ParseNodeInfoOptions, Protocol, Services, Software, Usage, getNodeInfo, nodeInfoToJson, parseNodeInfo };
@@ -1,3 +1,3 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { a as InboundService, c as OutboundService, d as Software, f as Usage, i as parseNodeInfo, l as Protocol, n as ParseNodeInfoOptions, o as JsonValue, p as nodeInfoToJson, r as getNodeInfo, s as NodeInfo, t as GetNodeInfoOptions, u as Services } from "../client-D-XP_Ccb.js";
2
+ import { a as InboundService, c as OutboundService, d as Software, f as Usage, i as parseNodeInfo, l as Protocol, n as ParseNodeInfoOptions, o as JsonValue, p as nodeInfoToJson, r as getNodeInfo, s as NodeInfo, t as GetNodeInfoOptions, u as Services } from "../client-3InDZbba.js";
3
3
  export { type GetNodeInfoOptions, InboundService, JsonValue, NodeInfo, OutboundService, type ParseNodeInfoOptions, Protocol, Services, Software, Usage, getNodeInfo, nodeInfoToJson, parseNodeInfo };
@@ -1,4 +1,4 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { n as getNodeInfo, r as parseNodeInfo, t as nodeInfoToJson } from "../types-_S2_MeLF.js";
3
+ import { n as getNodeInfo, r as parseNodeInfo, t as nodeInfoToJson } from "../types-DUjZb4i6.js";
4
4
  export { getNodeInfo, nodeInfoToJson, parseNodeInfo };