@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
@@ -2,7 +2,7 @@ import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { n as RouterError, t as Router } from "./router-CrMLXoOr.mjs";
5
- import { n as version, t as name } from "./deno-CD-8UlUi.mjs";
5
+ import { n as version, t as name } from "./deno-BUj4roJc.mjs";
6
6
  import { t as ActivityListenerSet } from "./activity-listener-ell7W1s9.mjs";
7
7
  import { Tombstone, getTypeId } from "@fedify/vocab";
8
8
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
@@ -59,7 +59,7 @@ var FederationBuilderImpl = class {
59
59
  this.collectionTypeIds = {};
60
60
  }
61
61
  async build(options) {
62
- const { FederationImpl } = await import("./middleware-DYK_res8.mjs");
62
+ const { FederationImpl } = await import("./middleware-yqqfI7oJ.mjs");
63
63
  const f = new FederationImpl(options);
64
64
  const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive;
65
65
  f.router = this.router.clone();
@@ -1,6 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { GetUserAgentOptions } from "@fedify/vocab-runtime";
3
-
4
3
  //#region src/nodeinfo/types.d.ts
5
4
  /**
6
5
  * The type of the result of parsing JSON.
@@ -1,7 +1,8 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { FetchError, UrlError, getUserAgent, validatePublicUrl } from "@fedify/vocab-runtime";
4
+ import { FetchError, UrlError, getUserAgent } from "@fedify/vocab-runtime";
5
+ import { openPublicFetch } from "@fedify/vocab-runtime/internal";
5
6
  import { getLogger } from "@logtape/logtape";
6
7
  //#region src/utils/body.ts
7
8
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -85,23 +86,34 @@ const logger = getLogger([
85
86
  ]);
86
87
  const DEFAULT_MAX_REDIRECTION = 5;
87
88
  /**
88
- * Fetches a URL while guarding against SSRF. Before every request—including
89
- * each redirect hop—the destination is validated with
90
- * {@link validatePublicUrl}, so that loopback, link-local, and private
91
- * addresses, as well as non-HTTP(S) schemes such as `data:`, are rejected.
92
- * Redirects are followed manually with a cap, so that a public URL cannot
93
- * bounce the request to an internal address.
89
+ * Fetches a URL while guarding against SSRF. Every request—including each
90
+ * redirect hop—is made in its own session opened with `openPublicFetch()`,
91
+ * which rejects loopback, link-local, and private addresses, as well as
92
+ * non-HTTP(S) schemes such as `data:`, and connects only to the validated
93
+ * addresses where the runtime supports it. Redirects are followed manually
94
+ * with a cap, so that a public URL cannot bounce the request to an internal
95
+ * address.
96
+ *
97
+ * The final response is passed to `consume`, which must consume or cancel its
98
+ * body; the session is closed after `consume` settles. The cancellation of
99
+ * redirect response bodies is started before their sessions are closed.
94
100
  * @param url The URL to fetch.
95
101
  * @param headers The request headers.
96
- * @returns The final response, or `null` if the URL is disallowed, its
97
- * hostname cannot be resolved, or there are too many redirects.
102
+ * @param consume The function to consume the final response with.
103
+ * @param deps The dependencies.
104
+ * @returns The value returned by `consume`, or `null` if the URL is
105
+ * disallowed, its hostname cannot be resolved, or there are too many
106
+ * redirects.
107
+ * @internal
98
108
  */
99
- async function fetchPublicUrl(url, headers) {
109
+ async function fetchPublicUrl(url, headers, consume, deps = {}) {
110
+ const openHop = deps.openHop ?? ((hopUrl) => openPublicFetch(hopUrl));
100
111
  let current = url instanceof URL ? url : new URL(url);
101
112
  let redirected = 0;
102
113
  while (true) {
114
+ let session;
103
115
  try {
104
- await validatePublicUrl(current.href);
116
+ session = await openHop(current.href);
105
117
  } catch (e) {
106
118
  if (e instanceof UrlError) {
107
119
  if (e.reason === "dns") logger.error("DNS lookup failed for {url}: {error}", {
@@ -116,52 +128,89 @@ async function fetchPublicUrl(url, headers) {
116
128
  }
117
129
  throw e;
118
130
  }
119
- const response = await fetch(current, {
120
- headers,
121
- redirect: "manual"
122
- });
123
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
124
- redirected++;
125
- if (redirected >= DEFAULT_MAX_REDIRECTION) {
126
- logger.error("Too many redirections ({redirections}) while fetching {url}.", {
127
- redirections: redirected,
128
- url: current.href
129
- });
130
- return null;
131
- }
132
- const next = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? current : response.url);
133
- if (next.protocol !== current.protocol) {
134
- logger.error("Refused a redirect to a different protocol ({protocol} to {redirectedProtocol}) while fetching NodeInfo.", {
135
- protocol: current.protocol,
136
- redirectedProtocol: next.protocol
137
- });
138
- return null;
139
- }
140
- current = next;
141
- continue;
131
+ let location;
132
+ try {
133
+ const response = await session.fetch(current, {
134
+ headers,
135
+ redirect: "manual"
136
+ });
137
+ if (response.status < 300 || response.status >= 400 || !response.headers.has("Location")) return await consume(response);
138
+ location = response.headers.get("Location");
139
+ cancelBody(response, current);
140
+ } finally {
141
+ await session.close();
142
+ }
143
+ redirected++;
144
+ if (redirected >= DEFAULT_MAX_REDIRECTION) {
145
+ logger.error("Too many redirections ({redirections}) while fetching {url}.", {
146
+ redirections: redirected,
147
+ url: current.href
148
+ });
149
+ return null;
150
+ }
151
+ const next = new URL(location, current);
152
+ if (next.protocol !== current.protocol) {
153
+ logger.error("Refused a redirect to a different protocol ({protocol} to {redirectedProtocol}) while fetching NodeInfo.", {
154
+ protocol: current.protocol,
155
+ redirectedProtocol: next.protocol
156
+ });
157
+ return null;
142
158
  }
143
- return response;
159
+ current = next;
144
160
  }
145
161
  }
162
+ /**
163
+ * Starts cancelling a response body that is not read, without waiting for it
164
+ * to settle. The cancellation of a branch of a tee (e.g., a body cloned by
165
+ * a fetch mock) settles only after the other branch is cancelled too, per
166
+ * the Streams standard, so awaiting it could block forever. A failure is
167
+ * only logged.
168
+ */
169
+ function cancelBody(response, url) {
170
+ response.body?.cancel().catch((error) => {
171
+ logger.debug("Failed to cancel the response body from {url}: {error}", {
172
+ url: url.toString(),
173
+ error
174
+ });
175
+ });
176
+ }
146
177
  async function getNodeInfo(url, options = {}) {
178
+ return await getNodeInfoInternal(url, options);
179
+ }
180
+ /**
181
+ * The implementation of {@link getNodeInfo}, which is not exported from any
182
+ * entry point.
183
+ * @param url The base URL of the server, or the URL of the NodeInfo document
184
+ * if `options.direct` is turned on.
185
+ * @param options Options for fetching the NodeInfo document.
186
+ * @param deps The dependencies.
187
+ * @returns The NodeInfo document if it could be fetched successfully.
188
+ * Otherwise, `undefined` is returned.
189
+ * @internal
190
+ */
191
+ async function getNodeInfoInternal(url, options = {}, deps = {}) {
147
192
  try {
193
+ const headers = {
194
+ Accept: "application/json",
195
+ "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
196
+ };
148
197
  let nodeInfoUrl = url;
149
198
  if (!options.direct) {
150
199
  const wellKnownUrl = new URL("/.well-known/nodeinfo", url);
151
- const wellKnownResponse = await fetchPublicUrl(wellKnownUrl, {
152
- Accept: "application/json",
153
- "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
154
- });
155
- if (wellKnownResponse == null) return void 0;
156
- if (!wellKnownResponse.ok) {
157
- logger.error("Failed to fetch {url}: {status} {statusText}", {
158
- url: wellKnownUrl.href,
159
- status: wellKnownResponse.status,
160
- statusText: wellKnownResponse.statusText
161
- });
162
- return;
163
- }
164
- const wellKnownRd = JSON.parse(await readBoundedText(wellKnownResponse, MAX_BODY_SIZE, wellKnownUrl));
200
+ const wellKnown = await fetchPublicUrl(wellKnownUrl, headers, async (response) => {
201
+ if (!response.ok) {
202
+ logger.error("Failed to fetch {url}: {status} {statusText}", {
203
+ url: wellKnownUrl.href,
204
+ status: response.status,
205
+ statusText: response.statusText
206
+ });
207
+ cancelBody(response, wellKnownUrl);
208
+ return;
209
+ }
210
+ return { rd: JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, wellKnownUrl)) };
211
+ }, deps);
212
+ if (wellKnown == null) return void 0;
213
+ const wellKnownRd = wellKnown.rd;
165
214
  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);
166
215
  if (link == null || link.href == null) {
167
216
  logger.error("Failed to find a NodeInfo document link from {url}: {resourceDescriptor}", {
@@ -172,20 +221,20 @@ async function getNodeInfo(url, options = {}) {
172
221
  }
173
222
  nodeInfoUrl = link.href;
174
223
  }
175
- const response = await fetchPublicUrl(nodeInfoUrl, {
176
- Accept: "application/json",
177
- "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
178
- });
179
- if (response == null) return void 0;
180
- if (!response.ok) {
181
- logger.error("Failed to fetch NodeInfo document from {url}: {status} {statusText}", {
182
- url: nodeInfoUrl.toString(),
183
- status: response.status,
184
- statusText: response.statusText
185
- });
186
- return;
187
- }
188
- const data = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, nodeInfoUrl));
224
+ const document = await fetchPublicUrl(nodeInfoUrl, headers, async (response) => {
225
+ if (!response.ok) {
226
+ logger.error("Failed to fetch NodeInfo document from {url}: {status} {statusText}", {
227
+ url: nodeInfoUrl.toString(),
228
+ status: response.status,
229
+ statusText: response.statusText
230
+ });
231
+ cancelBody(response, nodeInfoUrl);
232
+ return;
233
+ }
234
+ return { data: JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, nodeInfoUrl)) };
235
+ }, deps);
236
+ if (document == null) return void 0;
237
+ const { data } = document;
189
238
  if (options.parse === "none") return data;
190
239
  return parseNodeInfo(data, { tryBestEffort: options.parse === "best-effort" }) ?? void 0;
191
240
  } catch (error) {
@@ -389,4 +438,4 @@ function parseUsage(data, options = {}) {
389
438
  };
390
439
  }
391
440
  //#endregion
392
- export { parseProtocol as a, parseUsage as c, readBoundedText as d, parseOutboundService as i, BodyTooLargeError as l, parseInboundService as n, parseServices as o, parseNodeInfo as r, parseSoftware as s, getNodeInfo as t, MAX_BODY_SIZE as u };
441
+ export { parseOutboundService as a, parseSoftware as c, MAX_BODY_SIZE as d, readBoundedText as f, parseNodeInfo as i, parseUsage as l, getNodeInfoInternal as n, parseProtocol as o, parseInboundService as r, parseServices as s, getNodeInfo as t, BodyTooLargeError as u };
@@ -1,6 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { GetUserAgentOptions } from "@fedify/vocab-runtime";
3
-
4
3
  //#region src/nodeinfo/types.d.ts
5
4
  /**
6
5
  * The type of the result of parsing JSON.
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { Ot as ActivityTransformer, n as Context } from "../context-tJbL_Hkw.cjs";
2
+ import { Ot as ActivityTransformer, n as Context } from "../context-CFQGaq5x.cjs";
3
3
  import { Activity } from "@fedify/vocab";
4
4
 
5
5
  //#region src/compat/transformers.d.ts
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { Ot as ActivityTransformer, n as Context } from "../context-OkwEpc3-.js";
2
+ import { Ot as ActivityTransformer, n as Context } from "../context-DdbLVV_k.js";
3
3
  import { Activity } from "@fedify/vocab";
4
4
 
5
5
  //#region src/compat/transformers.d.ts
@@ -4,7 +4,7 @@ globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
5
  import { n as assertInstanceOf, t as assert } from "../assert-BhQd8QHP.mjs";
6
6
  import { t as MemoryKvStore } from "../kv-mI4Ix2EU.mjs";
7
- import { n as FederationImpl, v as actorDehydrator, y as autoIdAssigner } from "../middleware-CpXoRHs3.mjs";
7
+ import { n as FederationImpl, v as actorDehydrator, y as autoIdAssigner } from "../middleware-CYAaUTi5.mjs";
8
8
  import { test } from "@fedify/fixture";
9
9
  import { Follow, Person } from "@fedify/vocab";
10
10
  //#region src/compat/transformers.test.ts
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { o as JsonValue, s as NodeInfo, t as GetNodeInfoOptions } from "./client-CAM_bQXx.cjs";
3
- import { o as VerifyRequestFailureReason, t as HttpMessageSignaturesSpec } from "./http-DqZhyEso.cjs";
2
+ import { o as JsonValue, s as NodeInfo, t as GetNodeInfoOptions } from "./client-BIBzqEIp.cjs";
3
+ import { o as VerifyRequestFailureReason, t as HttpMessageSignaturesSpec } from "./http-9IFUxdkp.cjs";
4
4
  import { n as GetKeyOwnerOptions } from "./owner-B3hNQCG8.cjs";
5
5
  import { n as KvStore, t as KvKey } from "./kv-CZKl1IUS.cjs";
6
6
  import { Activity, Actor, Collection, CryptographicKey, Hashtag, Link, LookupObjectOptions, Multikey, Object as Object$1, Recipient, Tombstone, TraverseCollectionOptions } from "@fedify/vocab";
@@ -2447,6 +2447,11 @@ interface RequestContext<TContextData> extends Context<TContextData> {
2447
2447
  * Gets the public key of the sender, if any exists and it is verified.
2448
2448
  * Otherwise, `null` is returned.
2449
2449
  *
2450
+ * Successful results are reused for this request regardless of the options.
2451
+ * Pending checks and `null` results are reused for the same effective
2452
+ * document and context loaders, including failed lookups. Rejected checks
2453
+ * can be retried. Shared work uses the first caller's tracer provider.
2454
+ *
2450
2455
  * This can be used for implementing [authorized fetch] (also known as
2451
2456
  * secure mode) in ActivityPub.
2452
2457
  *
@@ -2460,6 +2465,11 @@ interface RequestContext<TContextData> extends Context<TContextData> {
2460
2465
  * Gets the public key of the sender, if any exists and it is verified.
2461
2466
  * Otherwise, `null` is returned.
2462
2467
  *
2468
+ * Successful results are reused for this request regardless of the options.
2469
+ * Pending checks and `null` results are reused for the same effective
2470
+ * document and context loaders, including failed lookups. Rejected checks
2471
+ * can be retried. Shared work uses the first caller's tracer provider.
2472
+ *
2463
2473
  * This can be used for implementing [authorized fetch] (also known as
2464
2474
  * secure mode) in ActivityPub.
2465
2475
  *
@@ -2479,6 +2489,14 @@ interface RequestContext<TContextData> extends Context<TContextData> {
2479
2489
  * Gets the owner of the signed key, if any exists and it is verified.
2480
2490
  * Otherwise, `null` is returned.
2481
2491
  *
2492
+ * Verified keys and owners are reused for this request regardless of the
2493
+ * options. Pending checks are shared for the same effective document and
2494
+ * context loaders. A `null` result from an owner lookup after key
2495
+ * verification is retained for those loaders. A `null` result caused by
2496
+ * an unverified key is not retained, allowing an owner lookup after another
2497
+ * loader verifies the key. Rejected checks can be retried. Shared work
2498
+ * uses the first caller's tracer provider.
2499
+ *
2482
2500
  * This can be used for implementing [authorized fetch] (also known as
2483
2501
  * secure mode) in ActivityPub.
2484
2502
  *
@@ -2493,6 +2511,14 @@ interface RequestContext<TContextData> extends Context<TContextData> {
2493
2511
  * Gets the owner of the signed key, if any exists and it is verified.
2494
2512
  * Otherwise, `null` is returned.
2495
2513
  *
2514
+ * Verified keys and owners are reused for this request regardless of the
2515
+ * options. Pending checks are shared for the same effective document and
2516
+ * context loaders. A `null` result from an owner lookup after key
2517
+ * verification is retained for those loaders. A `null` result caused by
2518
+ * an unverified key is not retained, allowing an owner lookup after another
2519
+ * loader verifies the key. Rejected checks can be retried. Shared work
2520
+ * uses the first caller's tracer provider.
2521
+ *
2496
2522
  * This can be used for implementing [authorized fetch] (also known as
2497
2523
  * secure mode) in ActivityPub.
2498
2524
  *
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { o as JsonValue, s as NodeInfo, t as GetNodeInfoOptions } from "./client-CSddvgWN.js";
3
- import { o as VerifyRequestFailureReason, t as HttpMessageSignaturesSpec } from "./http-Dl00HXr4.js";
2
+ import { o as JsonValue, s as NodeInfo, t as GetNodeInfoOptions } from "./client-jE7oGvRh.js";
3
+ import { o as VerifyRequestFailureReason, t as HttpMessageSignaturesSpec } from "./http-SbfezERn.js";
4
4
  import { n as GetKeyOwnerOptions } from "./owner-CiBPcC6F.js";
5
5
  import { n as KvStore, t as KvKey } from "./kv-CxaI4eGo.js";
6
6
  import { Activity, Actor, Collection, CryptographicKey, Hashtag, Link, LookupObjectOptions, Multikey, Object as Object$1, Recipient, Tombstone, TraverseCollectionOptions } from "@fedify/vocab";
@@ -2447,6 +2447,11 @@ interface RequestContext<TContextData> extends Context<TContextData> {
2447
2447
  * Gets the public key of the sender, if any exists and it is verified.
2448
2448
  * Otherwise, `null` is returned.
2449
2449
  *
2450
+ * Successful results are reused for this request regardless of the options.
2451
+ * Pending checks and `null` results are reused for the same effective
2452
+ * document and context loaders, including failed lookups. Rejected checks
2453
+ * can be retried. Shared work uses the first caller's tracer provider.
2454
+ *
2450
2455
  * This can be used for implementing [authorized fetch] (also known as
2451
2456
  * secure mode) in ActivityPub.
2452
2457
  *
@@ -2460,6 +2465,11 @@ interface RequestContext<TContextData> extends Context<TContextData> {
2460
2465
  * Gets the public key of the sender, if any exists and it is verified.
2461
2466
  * Otherwise, `null` is returned.
2462
2467
  *
2468
+ * Successful results are reused for this request regardless of the options.
2469
+ * Pending checks and `null` results are reused for the same effective
2470
+ * document and context loaders, including failed lookups. Rejected checks
2471
+ * can be retried. Shared work uses the first caller's tracer provider.
2472
+ *
2463
2473
  * This can be used for implementing [authorized fetch] (also known as
2464
2474
  * secure mode) in ActivityPub.
2465
2475
  *
@@ -2479,6 +2489,14 @@ interface RequestContext<TContextData> extends Context<TContextData> {
2479
2489
  * Gets the owner of the signed key, if any exists and it is verified.
2480
2490
  * Otherwise, `null` is returned.
2481
2491
  *
2492
+ * Verified keys and owners are reused for this request regardless of the
2493
+ * options. Pending checks are shared for the same effective document and
2494
+ * context loaders. A `null` result from an owner lookup after key
2495
+ * verification is retained for those loaders. A `null` result caused by
2496
+ * an unverified key is not retained, allowing an owner lookup after another
2497
+ * loader verifies the key. Rejected checks can be retried. Shared work
2498
+ * uses the first caller's tracer provider.
2499
+ *
2482
2500
  * This can be used for implementing [authorized fetch] (also known as
2483
2501
  * secure mode) in ActivityPub.
2484
2502
  *
@@ -2493,6 +2511,14 @@ interface RequestContext<TContextData> extends Context<TContextData> {
2493
2511
  * Gets the owner of the signed key, if any exists and it is verified.
2494
2512
  * Otherwise, `null` is returned.
2495
2513
  *
2514
+ * Verified keys and owners are reused for this request regardless of the
2515
+ * options. Pending checks are shared for the same effective document and
2516
+ * context loaders. A `null` result from an owner lookup after key
2517
+ * verification is retained for those loaders. A `null` result caused by
2518
+ * an unverified key is not retained, allowing an owner lookup after another
2519
+ * loader verifies the key. Rejected checks can be retried. Shared work
2520
+ * uses the first caller's tracer provider.
2521
+ *
2496
2522
  * This can be used for implementing [authorized fetch] (also known as
2497
2523
  * secure mode) in ActivityPub.
2498
2524
  *
@@ -3,6 +3,6 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  //#region deno.json
5
5
  var name = "@fedify/fedify";
6
- var version = "2.2.19";
6
+ var version = "2.2.20";
7
7
  //#endregion
8
8
  export { version as n, name as t };
@@ -0,0 +1,125 @@
1
+ import "@js-temporal/polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { s as validateCryptoKey } from "./key-C3o3DGap.mjs";
5
+ import { r as doubleKnockScoped } from "./http-CQMVisM0.mjs";
6
+ import { curry } from "es-toolkit";
7
+ import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest } from "@fedify/vocab-runtime";
8
+ import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
9
+ import { getLogger } from "@logtape/logtape";
10
+ //#region src/utils/docloader.ts
11
+ const logger = getLogger([
12
+ "fedify",
13
+ "utils",
14
+ "docloader"
15
+ ]);
16
+ const DEFAULT_MAX_REDIRECTION = 20;
17
+ /**
18
+ * Gets an authenticated {@link DocumentLoader} for the given identity.
19
+ * Note that an authenticated document loader intentionally does not cache
20
+ * the fetched documents.
21
+ * At most 20 HTTP redirects and alternate document links are followed in total
22
+ * per call. Revisiting a URL within that chain throws a {@link FetchError}.
23
+ * @param identity The identity to get the document loader for.
24
+ * The actor's key pair.
25
+ * @param options The options for the document loader.
26
+ * @returns The authenticated document loader.
27
+ * @throws {TypeError} If the key is invalid or unsupported.
28
+ * @since 0.4.0
29
+ */
30
+ function getAuthenticatedDocumentLoader(identity, options = {}) {
31
+ return createAuthenticatedDocumentLoader(identity, options);
32
+ }
33
+ /**
34
+ * The placeholder that the alternate document callback of
35
+ * `getRemoteDocument()` returns, so that the alternate document is loaded
36
+ * after the current hop is closed.
37
+ */
38
+ const ALTERNATE_PLACEHOLDER = Object.freeze({
39
+ contextUrl: null,
40
+ document: null,
41
+ documentUrl: "about:blank"
42
+ });
43
+ /**
44
+ * The implementation of {@link getAuthenticatedDocumentLoader}. This is not
45
+ * exported from any entry point.
46
+ * @param identity The identity to get the document loader for.
47
+ * @param options The options for the document loader.
48
+ * @param deps The dependencies, for tests.
49
+ * @returns The authenticated document loader.
50
+ * @throws {TypeError} If the key is invalid or unsupported.
51
+ */
52
+ function createAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, tracerProvider } = {}, deps = {}) {
53
+ validateCryptoKey(identity.privateKey);
54
+ async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
55
+ options?.signal?.throwIfAborted();
56
+ let currentUrl = new URL(url).href;
57
+ validateUrl(currentUrl);
58
+ visited.add(currentUrl);
59
+ const originalRequest = createActivityPubRequest(currentUrl, { userAgent });
60
+ function follow(nextUrl) {
61
+ options?.signal?.throwIfAborted();
62
+ if (redirected >= DEFAULT_MAX_REDIRECTION) throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
63
+ if (visited.has(nextUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${nextUrl}`);
64
+ redirected++;
65
+ }
66
+ const result = await doubleKnockScoped(originalRequest, identity, {
67
+ maxRedirection,
68
+ specDeterminer,
69
+ log: curry(logRequest)(logger),
70
+ tracerProvider,
71
+ signal: options?.signal,
72
+ validateRedirect: (nextUrl) => {
73
+ follow(nextUrl);
74
+ validateUrl(nextUrl);
75
+ visited.add(nextUrl);
76
+ currentUrl = nextUrl;
77
+ }
78
+ }, (hopUrl) => openHop(hopUrl, options?.signal), async (response) => {
79
+ let alternateUrl;
80
+ const document = await getRemoteDocument(currentUrl, response, (url) => {
81
+ alternateUrl = url;
82
+ return Promise.resolve(ALTERNATE_PLACEHOLDER);
83
+ });
84
+ if (alternateUrl == null) return { document };
85
+ if (!response.bodyUsed && response.body?.locked !== true) response.body?.cancel().catch((error) => {
86
+ logger.debug("Failed to cancel the response body from {url}: {error}", {
87
+ url: currentUrl,
88
+ error
89
+ });
90
+ });
91
+ return { alternateUrl };
92
+ });
93
+ if ("document" in result) return result.document;
94
+ follow(result.alternateUrl);
95
+ return load(result.alternateUrl, options, redirected, visited);
96
+ }
97
+ function openUnvalidatedHop(url, signal) {
98
+ if (deps.openHop != null) return deps.openHop(url);
99
+ return allowPrivateAddress ? openDirectFetch(url) : openPublicFetch(url, { signal });
100
+ }
101
+ async function openHop(url, signal) {
102
+ try {
103
+ return await openUnvalidatedHop(url, signal);
104
+ } catch (error) {
105
+ if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
106
+ url,
107
+ error
108
+ });
109
+ else logger.error("Disallowed private URL: {url}", {
110
+ url,
111
+ error
112
+ });
113
+ throw error;
114
+ }
115
+ }
116
+ function validateUrl(url) {
117
+ const parsedUrl = new URL(url);
118
+ const protocol = parsedUrl.protocol;
119
+ if (protocol !== "http:" && protocol !== "https:") throw new UrlError(`Unsupported protocol: ${protocol}`);
120
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new UrlError("URLs with credentials are not allowed");
121
+ }
122
+ return (url, options) => load(url, options);
123
+ }
124
+ //#endregion
125
+ export { getAuthenticatedDocumentLoader as n, createAuthenticatedDocumentLoader as t };
@@ -7,7 +7,7 @@ import { r as assertExists } from "../std__assert-DNZ4Liil.mjs";
7
7
  import { t as assertThrows } from "../assert_throws-D4DNWrux.mjs";
8
8
  import { t as assertStrictEquals } from "../assert_strict_equals-Dmjbg-bA.mjs";
9
9
  import { t as MemoryKvStore } from "../kv-mI4Ix2EU.mjs";
10
- import { n as createFederationBuilder } from "../builder-UmydCxtK.mjs";
10
+ import { n as createFederationBuilder } from "../builder-BolQ56ab.mjs";
11
11
  import { test } from "@fedify/fixture";
12
12
  import { Activity, Note, Person } from "@fedify/vocab";
13
13
  //#region src/federation/builder.test.ts
@@ -7,13 +7,13 @@ import "../std__assert-DNZ4Liil.mjs";
7
7
  import { n as assertInstanceOf, t as assert } from "../assert-BhQd8QHP.mjs";
8
8
  import { t as assertRejects } from "../assert_rejects-WyYSTDDX.mjs";
9
9
  import { r as parseAcceptSignature } from "../accept-Dd__NiUL.mjs";
10
- import { a as generateCryptoKeyPair } from "../key-DolXUaLH.mjs";
11
- import { s as signRequest } from "../http-BBBUbwKh.mjs";
10
+ import { a as generateCryptoKeyPair } from "../key-C3o3DGap.mjs";
11
+ import { c as signRequest } from "../http-CQMVisM0.mjs";
12
12
  import { a as rsaPrivateKey3, c as rsaPublicKey3, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-BAK-tUlf.mjs";
13
13
  import { t as MemoryKvStore } from "../kv-mI4Ix2EU.mjs";
14
- import { a as compactJsonLd, p as signJsonLd } from "../ld-rTdIfZ63.mjs";
15
- import { r as signObject } from "../proof-gOGM-sJS.mjs";
16
- import { c as handleActor, d as handleInbox, f as handleObject, h as respondWithObjectIfAcceptable, l as handleCollection, m as respondWithObject, o as createFederation, p as handleOutbox, u as handleCustomCollection } from "../middleware-CpXoRHs3.mjs";
14
+ import { a as compactJsonLd, p as signJsonLd } from "../ld-ijSr-0AB.mjs";
15
+ import { r as signObject } from "../proof-CBQOjOgA.mjs";
16
+ import { c as handleActor, d as handleInbox, f as handleObject, h as respondWithObjectIfAcceptable, l as handleCollection, m as respondWithObject, o as createFederation, p as handleOutbox, u as handleCustomCollection } from "../middleware-CYAaUTi5.mjs";
17
17
  import { t as ActivityListenerSet } from "../activity-listener-ell7W1s9.mjs";
18
18
  import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
19
19
  import { Activity, Create, CryptographicKey, Multikey, Note, Person, Tombstone } from "@fedify/vocab";
@@ -5,8 +5,8 @@ import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
5
  import "../std__assert-DNZ4Liil.mjs";
6
6
  import { n as ed25519PrivateKey, r as ed25519PublicKey, t as ed25519Multikey } from "../keys-BAK-tUlf.mjs";
7
7
  import { t as MemoryKvStore } from "../kv-mI4Ix2EU.mjs";
8
- import { r as signObject } from "../proof-gOGM-sJS.mjs";
9
- import { o as createFederation } from "../middleware-CpXoRHs3.mjs";
8
+ import { r as signObject } from "../proof-CBQOjOgA.mjs";
9
+ import { o as createFederation } from "../middleware-CYAaUTi5.mjs";
10
10
  import { mockDocumentLoader, test } from "@fedify/fixture";
11
11
  import { Create, Follow, Person } from "@fedify/vocab";
12
12
  //#region src/federation/idempotency.test.ts
@@ -4,7 +4,7 @@ globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
5
  import { r as assertFalse, t as assertThrows } from "../assert_throws-D4DNWrux.mjs";
6
6
  import { t as ActivityListenerSet } from "../activity-listener-ell7W1s9.mjs";
7
- import { t as actorsHaveSameOrigin } from "../inbox-Dca_0fwc.mjs";
7
+ import { t as actorsHaveSameOrigin } from "../inbox-CD8IDILN.mjs";
8
8
  import { test } from "@fedify/fixture";
9
9
  import { Activity, Create, Invite, Offer, Update } from "@fedify/vocab";
10
10
  //#region src/federation/inbox.test.ts