@fedify/fedify 2.4.5-dev.2462 → 2.4.5

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-CUAVOHg6.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-BNjXclzO.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-DcJISdwu.mjs} +1 -1
  13. package/dist/docloader-CP1UxMLL.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-Bkfq2YpS.cjs} +240 -102
  44. package/dist/{http-DLf8QYD6.cjs → http-DHVH0DqC.cjs} +179 -110
  45. package/dist/{http-D7L2pqLT.mjs → http-DuGmr8J8.mjs} +204 -106
  46. package/dist/{http-BVrOR6MG.js → http-R0ty0Udi.js} +178 -109
  47. package/dist/{inbox-DtEKOr3W.mjs → inbox-Bl1Y0s_A.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-BtBSBZi9.cjs} +72 -27
  51. package/dist/{kv-cache-m3RIr811.mjs → kv-cache-DAV2_U44.mjs} +1 -1
  52. package/dist/{kv-cache-DoZoRS9p.js → kv-cache-FnLtmATH.js} +73 -28
  53. package/dist/{ld-PJLGA2MJ.mjs → ld-Cy5GqbvS.mjs} +2 -2
  54. package/dist/{metrics-CGR9cI-Y.mjs → metrics-CW8LoN7n.mjs} +1 -1
  55. package/dist/{middleware-DPI-Jp9w.mjs → middleware-CdP-lz8Q.mjs} +1 -1
  56. package/dist/{middleware-ByanhTvb.mjs → middleware-D2fk45uN.mjs} +82 -42
  57. package/dist/{middleware-BxhBd7pw.js → middleware-D4rQARvq.js} +241 -103
  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-CbQZqFim.mjs} +3 -3
  80. package/dist/{portable-collection-BMZY9qT-.js → portable-collection-B5Aryi8I.js} +1 -1
  81. package/dist/{portable-collection-CR5uwjws.cjs → portable-collection-C_Kmvl3q.cjs} +1 -1
  82. package/dist/{portable-collection-C88tBiy0.mjs → portable-collection-D2JYDadi.mjs} +2 -2
  83. package/dist/{portable-key-DxTXUpCK.mjs → portable-key-Cc-ctNU2.mjs} +1 -1
  84. package/dist/{proof-DNu-qP0i.mjs → proof-UN4CW55z.mjs} +3 -3
  85. package/dist/{send-CdCifQs4.mjs → send-BzbX2e6v.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-B7Wvxrjs.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
@@ -1,6 +1,7 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  let _logtape_logtape = require("@logtape/logtape");
3
3
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
4
+ let _fedify_vocab_runtime_internal = require("@fedify/vocab-runtime/internal");
4
5
  //#region src/utils/body.ts
5
6
  /** Decoded JSON body limit: 16 MiB. @internal */
6
7
  const MAX_BODY_SIZE = 16 * 1024 * 1024;
@@ -83,23 +84,34 @@ const logger = (0, _logtape_logtape.getLogger)([
83
84
  ]);
84
85
  const DEFAULT_MAX_REDIRECTION = 5;
85
86
  /**
86
- * Fetches a URL while guarding against SSRF. Before every request—including
87
- * each redirect hop—the destination is validated with
88
- * {@link validatePublicUrl}, so that loopback, link-local, and private
89
- * addresses, as well as non-HTTP(S) schemes such as `data:`, are rejected.
90
- * Redirects are followed manually with a cap, so that a public URL cannot
91
- * bounce the request to an internal address.
87
+ * Fetches a URL while guarding against SSRF. Every request—including each
88
+ * redirect hop—is made in its own session opened with `openPublicFetch()`,
89
+ * which rejects loopback, link-local, and private addresses, as well as
90
+ * non-HTTP(S) schemes such as `data:`, and connects only to the validated
91
+ * addresses where the runtime supports it. Redirects are followed manually
92
+ * with a cap, so that a public URL cannot bounce the request to an internal
93
+ * address.
94
+ *
95
+ * The final response is passed to `consume`, which must consume or cancel its
96
+ * body; the session is closed after `consume` settles. The cancellation of
97
+ * redirect response bodies is started before their sessions are closed.
92
98
  * @param url The URL to fetch.
93
99
  * @param headers The request headers.
94
- * @returns The final response, or `null` if the URL is disallowed, its
95
- * hostname cannot be resolved, or there are too many redirects.
100
+ * @param consume The function to consume the final response with.
101
+ * @param deps The dependencies.
102
+ * @returns The value returned by `consume`, or `null` if the URL is
103
+ * disallowed, its hostname cannot be resolved, or there are too many
104
+ * redirects.
105
+ * @internal
96
106
  */
97
- async function fetchPublicUrl(url, headers) {
107
+ async function fetchPublicUrl(url, headers, consume, deps = {}) {
108
+ const openHop = deps.openHop ?? ((hopUrl) => (0, _fedify_vocab_runtime_internal.openPublicFetch)(hopUrl));
98
109
  let current = url instanceof URL ? url : new URL(url);
99
110
  let redirected = 0;
100
111
  while (true) {
112
+ let session;
101
113
  try {
102
- await (0, _fedify_vocab_runtime.validatePublicUrl)(current.href);
114
+ session = await openHop(current.href);
103
115
  } catch (e) {
104
116
  if (e instanceof _fedify_vocab_runtime.UrlError) {
105
117
  if (e.reason === "dns") logger.error("DNS lookup failed for {url}: {error}", {
@@ -114,52 +126,89 @@ async function fetchPublicUrl(url, headers) {
114
126
  }
115
127
  throw e;
116
128
  }
117
- const response = await fetch(current, {
118
- headers,
119
- redirect: "manual"
120
- });
121
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
122
- redirected++;
123
- if (redirected >= DEFAULT_MAX_REDIRECTION) {
124
- logger.error("Too many redirections ({redirections}) while fetching {url}.", {
125
- redirections: redirected,
126
- url: current.href
127
- });
128
- return null;
129
- }
130
- const next = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? current : response.url);
131
- if (next.protocol !== current.protocol) {
132
- logger.error("Refused a redirect to a different protocol ({protocol} to {redirectedProtocol}) while fetching NodeInfo.", {
133
- protocol: current.protocol,
134
- redirectedProtocol: next.protocol
135
- });
136
- return null;
137
- }
138
- current = next;
139
- continue;
129
+ let location;
130
+ try {
131
+ const response = await session.fetch(current, {
132
+ headers,
133
+ redirect: "manual"
134
+ });
135
+ if (response.status < 300 || response.status >= 400 || !response.headers.has("Location")) return await consume(response);
136
+ location = response.headers.get("Location");
137
+ cancelBody(response, current);
138
+ } finally {
139
+ await session.close();
140
+ }
141
+ redirected++;
142
+ if (redirected >= DEFAULT_MAX_REDIRECTION) {
143
+ logger.error("Too many redirections ({redirections}) while fetching {url}.", {
144
+ redirections: redirected,
145
+ url: current.href
146
+ });
147
+ return null;
148
+ }
149
+ const next = new URL(location, current);
150
+ if (next.protocol !== current.protocol) {
151
+ logger.error("Refused a redirect to a different protocol ({protocol} to {redirectedProtocol}) while fetching NodeInfo.", {
152
+ protocol: current.protocol,
153
+ redirectedProtocol: next.protocol
154
+ });
155
+ return null;
140
156
  }
141
- return response;
157
+ current = next;
142
158
  }
143
159
  }
160
+ /**
161
+ * Starts cancelling a response body that is not read, without waiting for it
162
+ * to settle. The cancellation of a branch of a tee (e.g., a body cloned by
163
+ * a fetch mock) settles only after the other branch is cancelled too, per
164
+ * the Streams standard, so awaiting it could block forever. A failure is
165
+ * only logged.
166
+ */
167
+ function cancelBody(response, url) {
168
+ response.body?.cancel().catch((error) => {
169
+ logger.debug("Failed to cancel the response body from {url}: {error}", {
170
+ url: url.toString(),
171
+ error
172
+ });
173
+ });
174
+ }
144
175
  async function getNodeInfo(url, options = {}) {
176
+ return await getNodeInfoInternal(url, options);
177
+ }
178
+ /**
179
+ * The implementation of {@link getNodeInfo}, which is not exported from any
180
+ * entry point.
181
+ * @param url The base URL of the server, or the URL of the NodeInfo document
182
+ * if `options.direct` is turned on.
183
+ * @param options Options for fetching the NodeInfo document.
184
+ * @param deps The dependencies.
185
+ * @returns The NodeInfo document if it could be fetched successfully.
186
+ * Otherwise, `undefined` is returned.
187
+ * @internal
188
+ */
189
+ async function getNodeInfoInternal(url, options = {}, deps = {}) {
145
190
  try {
191
+ const headers = {
192
+ Accept: "application/json",
193
+ "User-Agent": typeof options.userAgent === "string" ? options.userAgent : (0, _fedify_vocab_runtime.getUserAgent)(options.userAgent)
194
+ };
146
195
  let nodeInfoUrl = url;
147
196
  if (!options.direct) {
148
197
  const wellKnownUrl = new URL("/.well-known/nodeinfo", url);
149
- const wellKnownResponse = await fetchPublicUrl(wellKnownUrl, {
150
- Accept: "application/json",
151
- "User-Agent": typeof options.userAgent === "string" ? options.userAgent : (0, _fedify_vocab_runtime.getUserAgent)(options.userAgent)
152
- });
153
- if (wellKnownResponse == null) return void 0;
154
- if (!wellKnownResponse.ok) {
155
- logger.error("Failed to fetch {url}: {status} {statusText}", {
156
- url: wellKnownUrl.href,
157
- status: wellKnownResponse.status,
158
- statusText: wellKnownResponse.statusText
159
- });
160
- return;
161
- }
162
- const wellKnownRd = JSON.parse(await readBoundedText(wellKnownResponse, MAX_BODY_SIZE, wellKnownUrl));
198
+ const wellKnown = await fetchPublicUrl(wellKnownUrl, headers, async (response) => {
199
+ if (!response.ok) {
200
+ logger.error("Failed to fetch {url}: {status} {statusText}", {
201
+ url: wellKnownUrl.href,
202
+ status: response.status,
203
+ statusText: response.statusText
204
+ });
205
+ cancelBody(response, wellKnownUrl);
206
+ return;
207
+ }
208
+ return { rd: JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, wellKnownUrl)) };
209
+ }, deps);
210
+ if (wellKnown == null) return void 0;
211
+ const wellKnownRd = wellKnown.rd;
163
212
  const link = wellKnownRd?.links?.find((link) => link != null && "rel" in link && (link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.0" || link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.1") && "href" in link && link.href != null);
164
213
  if (link == null || link.href == null) {
165
214
  logger.error("Failed to find a NodeInfo document link from {url}: {resourceDescriptor}", {
@@ -170,20 +219,20 @@ async function getNodeInfo(url, options = {}) {
170
219
  }
171
220
  nodeInfoUrl = link.href;
172
221
  }
173
- const response = await fetchPublicUrl(nodeInfoUrl, {
174
- Accept: "application/json",
175
- "User-Agent": typeof options.userAgent === "string" ? options.userAgent : (0, _fedify_vocab_runtime.getUserAgent)(options.userAgent)
176
- });
177
- if (response == null) return void 0;
178
- if (!response.ok) {
179
- logger.error("Failed to fetch NodeInfo document from {url}: {status} {statusText}", {
180
- url: nodeInfoUrl.toString(),
181
- status: response.status,
182
- statusText: response.statusText
183
- });
184
- return;
185
- }
186
- const data = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, nodeInfoUrl));
222
+ const document = await fetchPublicUrl(nodeInfoUrl, headers, async (response) => {
223
+ if (!response.ok) {
224
+ logger.error("Failed to fetch NodeInfo document from {url}: {status} {statusText}", {
225
+ url: nodeInfoUrl.toString(),
226
+ status: response.status,
227
+ statusText: response.statusText
228
+ });
229
+ cancelBody(response, nodeInfoUrl);
230
+ return;
231
+ }
232
+ return { data: JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, nodeInfoUrl)) };
233
+ }, deps);
234
+ if (document == null) return void 0;
235
+ const { data } = document;
187
236
  if (options.parse === "none") return data;
188
237
  return parseNodeInfo(data, { tryBestEffort: options.parse === "best-effort" }) ?? void 0;
189
238
  } catch (error) {