@fedify/webfinger 2.4.5-dev.2453 → 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.
package/dist/mod.js CHANGED
@@ -1,9 +1,10 @@
1
- import { FetchError, UrlError, getUserAgent, validatePublicUrl } from "@fedify/vocab-runtime";
2
- import { getLogger } from "@logtape/logtape";
3
1
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
2
+ import { FetchError, UrlError, getUserAgent } from "@fedify/vocab-runtime";
3
+ import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
4
+ import { getLogger } from "@logtape/logtape";
4
5
  //#region deno.json
5
6
  var name = "@fedify/webfinger";
6
- var version = "2.4.5-dev.2453+1304e862";
7
+ var version = "2.4.5";
7
8
  //#endregion
8
9
  //#region src/body.ts
9
10
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -79,13 +80,223 @@ async function readBoundedText(message, maxBytes, url) {
79
80
  }
80
81
  }
81
82
  //#endregion
82
- //#region src/lookup.ts
83
+ //#region src/lookup-impl.ts
83
84
  const logger = getLogger([
84
85
  "fedify",
85
86
  "webfinger",
86
87
  "lookup"
87
88
  ]);
88
89
  const DEFAULT_MAX_REDIRECTION = 5;
90
+ /**
91
+ * Looks up a WebFinger resource, opening a fetch session per hop. This is
92
+ * the implementation of {@link lookupWebFinger}, which is not exported from
93
+ * the package entry point.
94
+ * @param resource The resource URL to look up.
95
+ * @param options Extra options for looking up the resource.
96
+ * @param deps The dependencies.
97
+ * @returns The resource descriptor and terminal lookup classification.
98
+ * @internal
99
+ */
100
+ async function lookupWebFingerInternal(resource, options = {}, deps = {}) {
101
+ if (typeof resource === "string") resource = new URL(resource);
102
+ let protocol = "https:";
103
+ let server;
104
+ if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
105
+ const atPos = resource.pathname.lastIndexOf("@");
106
+ if (atPos < 0) return {
107
+ resource: null,
108
+ result: "invalid"
109
+ };
110
+ server = resource.pathname.substring(atPos + 1);
111
+ if (server === "" || /[/?#]/.test(server)) return {
112
+ resource: null,
113
+ result: "invalid"
114
+ };
115
+ if (resource.protocol === "acct:" && (resource.search !== "" || resource.hash !== "")) return {
116
+ resource: null,
117
+ result: "invalid"
118
+ };
119
+ } else {
120
+ protocol = resource.protocol;
121
+ server = resource.host;
122
+ }
123
+ let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
124
+ url.searchParams.set("resource", resource.href);
125
+ const openHop = deps.openHop ?? ((hopUrl) => options.allowPrivateAddress === true ? openDirectFetch(hopUrl) : openPublicFetch(hopUrl, { signal: options.signal }));
126
+ let redirected = 0;
127
+ while (true) {
128
+ logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
129
+ const remoteHost = url.host;
130
+ const session = await openSession(url, openHop, options.signal);
131
+ if (session == null) return {
132
+ resource: null,
133
+ result: "network_error",
134
+ remoteHost
135
+ };
136
+ let hop;
137
+ try {
138
+ hop = await fetchHop(session, url, options);
139
+ } finally {
140
+ await session.close();
141
+ }
142
+ if ("outcome" in hop) return hop.outcome;
143
+ redirected++;
144
+ const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
145
+ if (redirected > maxRedirection) {
146
+ logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
147
+ return {
148
+ resource: null,
149
+ result: "invalid",
150
+ statusCode: hop.statusCode,
151
+ remoteHost
152
+ };
153
+ }
154
+ let redirectedUrl;
155
+ try {
156
+ redirectedUrl = new URL(hop.redirect, url);
157
+ } catch (error) {
158
+ logger.error("Invalid Location header while following WebFinger redirect: {error}", {
159
+ url: url.href,
160
+ error
161
+ });
162
+ return {
163
+ resource: null,
164
+ result: "invalid",
165
+ statusCode: hop.statusCode,
166
+ remoteHost
167
+ };
168
+ }
169
+ if (redirectedUrl.protocol !== url.protocol) {
170
+ logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
171
+ protocol: url.protocol,
172
+ redirectedProtocol: redirectedUrl.protocol
173
+ });
174
+ return {
175
+ resource: null,
176
+ result: "invalid",
177
+ statusCode: hop.statusCode,
178
+ remoteHost
179
+ };
180
+ }
181
+ url = redirectedUrl;
182
+ }
183
+ }
184
+ /**
185
+ * Opens a fetch session for a hop. A disallowed URL, a failed DNS lookup,
186
+ * or an aborted lookup yields `null` instead of an error.
187
+ */
188
+ async function openSession(url, openHop, signal) {
189
+ try {
190
+ return await openHop(url.href);
191
+ } catch (e) {
192
+ if (e instanceof UrlError) {
193
+ if (e.reason === "dns") logger.debug("DNS lookup failed for {url}", {
194
+ url: url.href,
195
+ error: e
196
+ });
197
+ else logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
198
+ return null;
199
+ }
200
+ if (signal?.aborted) {
201
+ logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
202
+ url: url.href,
203
+ error: e
204
+ });
205
+ return null;
206
+ }
207
+ throw e;
208
+ }
209
+ }
210
+ /**
211
+ * Fetches a hop with the given session, and consumes the response body or
212
+ * starts cancelling it before returning. The caller closes the session.
213
+ */
214
+ async function fetchHop(session, url, options) {
215
+ let response;
216
+ try {
217
+ response = await session.fetch(url, {
218
+ headers: {
219
+ Accept: "application/jrd+json",
220
+ "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
221
+ },
222
+ redirect: "manual",
223
+ signal: options.signal
224
+ });
225
+ } catch (error) {
226
+ logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
227
+ url: url.href,
228
+ error
229
+ });
230
+ return { outcome: {
231
+ resource: null,
232
+ result: "network_error",
233
+ remoteHost: url.host
234
+ } };
235
+ }
236
+ if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
237
+ cancelBody(response, url);
238
+ return {
239
+ redirect: response.headers.get("Location"),
240
+ statusCode: response.status
241
+ };
242
+ }
243
+ if (!response.ok) {
244
+ logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
245
+ url: url.href,
246
+ status: response.status,
247
+ statusText: response.statusText
248
+ });
249
+ cancelBody(response, url);
250
+ return { outcome: {
251
+ resource: null,
252
+ result: response.status === 404 || response.status === 410 ? "not_found" : "error",
253
+ statusCode: response.status,
254
+ remoteHost: url.host
255
+ } };
256
+ }
257
+ try {
258
+ return { outcome: {
259
+ resource: JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url)),
260
+ result: "found",
261
+ statusCode: response.status,
262
+ remoteHost: url.host
263
+ } };
264
+ } catch (e) {
265
+ if (e instanceof BodyTooLargeError) return { outcome: {
266
+ resource: null,
267
+ result: "invalid",
268
+ statusCode: response.status,
269
+ remoteHost: url.host
270
+ } };
271
+ if (e instanceof SyntaxError) {
272
+ logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
273
+ return { outcome: {
274
+ resource: null,
275
+ result: "invalid",
276
+ statusCode: response.status,
277
+ remoteHost: url.host
278
+ } };
279
+ }
280
+ throw e;
281
+ }
282
+ }
283
+ /**
284
+ * Starts cancelling a response body that is not read, without waiting for it
285
+ * to settle. The cancellation of a branch of a tee (e.g., a body cloned by
286
+ * a fetch mock) settles only after the other branch is cancelled too, per
287
+ * the Streams standard, so awaiting it could block forever. A failure is
288
+ * only logged.
289
+ */
290
+ function cancelBody(response, url) {
291
+ response.body?.cancel().catch((error) => {
292
+ logger.debug("Failed to cancel the response body from {url}: {error}", {
293
+ url: url.href,
294
+ error
295
+ });
296
+ });
297
+ }
298
+ //#endregion
299
+ //#region src/lookup.ts
89
300
  const WEBFINGER_HISTOGRAM_BUCKETS = [
90
301
  5,
91
302
  10,
@@ -146,6 +357,14 @@ function getMetricResourceScheme(scheme) {
146
357
  * @since 0.2.0
147
358
  */
148
359
  async function lookupWebFinger(resource, options = {}) {
360
+ return await lookupWebFingerWithDependencies(resource, options);
361
+ }
362
+ /**
363
+ * The lookup implementation with injectable hop sessions for tests.
364
+ * Not exported from the package entry point.
365
+ * @internal
366
+ */
367
+ async function lookupWebFingerWithDependencies(resource, options = {}, deps = {}) {
149
368
  const tracer = (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version);
150
369
  const scheme = getResourceScheme(resource);
151
370
  return await tracer.startActiveSpan("webfinger.lookup", {
@@ -162,7 +381,7 @@ async function lookupWebFinger(resource, options = {}) {
162
381
  result: "error"
163
382
  };
164
383
  try {
165
- outcome = await lookupWebFingerInternal(resource, options);
384
+ outcome = await lookupWebFingerInternal(resource, options, deps);
166
385
  span.setStatus({ code: outcome.resource === null ? SpanStatusCode.ERROR : SpanStatusCode.OK });
167
386
  return outcome.resource;
168
387
  } catch (error) {
@@ -188,154 +407,5 @@ function recordWebFingerLookup(meterProvider, durationMs, scheme, outcome) {
188
407
  instruments.lookup.add(1, attributes);
189
408
  instruments.lookupDuration.record(durationMs, attributes);
190
409
  }
191
- async function lookupWebFingerInternal(resource, options = {}) {
192
- if (typeof resource === "string") resource = new URL(resource);
193
- let protocol = "https:";
194
- let server;
195
- if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
196
- const atPos = resource.pathname.lastIndexOf("@");
197
- if (atPos < 0) return {
198
- resource: null,
199
- result: "invalid"
200
- };
201
- server = resource.pathname.substring(atPos + 1);
202
- if (server === "" || /[/?#]/.test(server)) return {
203
- resource: null,
204
- result: "invalid"
205
- };
206
- if (resource.protocol === "acct:" && (resource.search !== "" || resource.hash !== "")) return {
207
- resource: null,
208
- result: "invalid"
209
- };
210
- } else {
211
- protocol = resource.protocol;
212
- server = resource.host;
213
- }
214
- let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
215
- url.searchParams.set("resource", resource.href);
216
- let redirected = 0;
217
- while (true) {
218
- const remoteHost = url.host;
219
- logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
220
- let response;
221
- if (options.allowPrivateAddress !== true) try {
222
- await validatePublicUrl(url.href);
223
- } catch (e) {
224
- if (e instanceof UrlError) {
225
- if (e.reason === "dns") logger.debug("DNS lookup failed for {url}", {
226
- url: url.href,
227
- error: e
228
- });
229
- else logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
230
- return {
231
- resource: null,
232
- result: "network_error",
233
- remoteHost
234
- };
235
- }
236
- throw e;
237
- }
238
- try {
239
- response = await fetch(url, {
240
- headers: {
241
- Accept: "application/jrd+json",
242
- "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
243
- },
244
- redirect: "manual",
245
- signal: options.signal
246
- });
247
- } catch (error) {
248
- logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
249
- url: url.href,
250
- error
251
- });
252
- return {
253
- resource: null,
254
- result: "network_error",
255
- remoteHost
256
- };
257
- }
258
- if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
259
- redirected++;
260
- const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
261
- if (redirected > maxRedirection) {
262
- logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
263
- return {
264
- resource: null,
265
- result: "invalid",
266
- statusCode: response.status,
267
- remoteHost
268
- };
269
- }
270
- let redirectedUrl;
271
- try {
272
- redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
273
- } catch (e) {
274
- logger.error("Invalid Location header while following WebFinger redirect: {error}", {
275
- url: url.href,
276
- error: e
277
- });
278
- return {
279
- resource: null,
280
- result: "invalid",
281
- statusCode: response.status,
282
- remoteHost
283
- };
284
- }
285
- if (redirectedUrl.protocol !== url.protocol) {
286
- logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
287
- protocol: url.protocol,
288
- redirectedProtocol: redirectedUrl.protocol
289
- });
290
- return {
291
- resource: null,
292
- result: "invalid",
293
- statusCode: response.status,
294
- remoteHost
295
- };
296
- }
297
- url = redirectedUrl;
298
- continue;
299
- }
300
- if (!response.ok) {
301
- logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
302
- url: url.href,
303
- status: response.status,
304
- statusText: response.statusText
305
- });
306
- return {
307
- resource: null,
308
- result: response.status === 404 || response.status === 410 ? "not_found" : "error",
309
- statusCode: response.status,
310
- remoteHost
311
- };
312
- }
313
- try {
314
- return {
315
- resource: JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url)),
316
- result: "found",
317
- statusCode: response.status,
318
- remoteHost
319
- };
320
- } catch (e) {
321
- if (e instanceof BodyTooLargeError) return {
322
- resource: null,
323
- result: "invalid",
324
- statusCode: response.status,
325
- remoteHost
326
- };
327
- if (e instanceof SyntaxError) {
328
- logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
329
- return {
330
- resource: null,
331
- result: "invalid",
332
- statusCode: response.status,
333
- remoteHost
334
- };
335
- }
336
- throw e;
337
- }
338
- }
339
- }
340
410
  //#endregion
341
- export { lookupWebFinger };
411
+ export { lookupWebFinger, lookupWebFingerWithDependencies };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/webfinger",
3
- "version": "2.4.5-dev.2453+1304e862",
3
+ "version": "2.4.5",
4
4
  "homepage": "https://fedify.dev/",
5
5
  "repository": {
6
6
  "type": "git",
@@ -55,9 +55,9 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@logtape/logtape": "^2.2.0",
58
- "@opentelemetry/api": "^1.9.1",
58
+ "@opentelemetry/api": "~1.9.1",
59
59
  "es-toolkit": "1.46.1",
60
- "@fedify/vocab-runtime": "2.4.5-dev.2453+1304e862"
60
+ "@fedify/vocab-runtime": "2.4.5"
61
61
  },
62
62
  "scripts": {
63
63
  "build:self": "tsdown",