@fedify/webfinger 2.5.0-dev.2448 → 2.5.0-dev.2492

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