@fedify/webfinger 2.3.0-dev.994 → 2.3.0-pr.809.36

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
@@ -4,7 +4,7 @@ let _logtape_logtape = require("@logtape/logtape");
4
4
  let _opentelemetry_api = require("@opentelemetry/api");
5
5
  //#region deno.json
6
6
  var name = "@fedify/webfinger";
7
- var version = "2.3.0-dev.994+9071ca0a";
7
+ var version = "2.3.0-pr.809.36+c592d116";
8
8
  //#endregion
9
9
  //#region src/lookup.ts
10
10
  const logger = (0, _logtape_logtape.getLogger)([
@@ -13,6 +13,58 @@ const logger = (0, _logtape_logtape.getLogger)([
13
13
  "lookup"
14
14
  ]);
15
15
  const DEFAULT_MAX_REDIRECTION = 5;
16
+ const WEBFINGER_HISTOGRAM_BUCKETS = [
17
+ 5,
18
+ 10,
19
+ 25,
20
+ 50,
21
+ 75,
22
+ 100,
23
+ 250,
24
+ 500,
25
+ 750,
26
+ 1e3,
27
+ 2500,
28
+ 5e3,
29
+ 7500,
30
+ 1e4
31
+ ];
32
+ const webFingerInstruments = /* @__PURE__ */ new WeakMap();
33
+ function getWebFingerInstruments(meterProvider) {
34
+ let instruments = webFingerInstruments.get(meterProvider);
35
+ if (instruments == null) {
36
+ const meter = meterProvider.getMeter(name, version);
37
+ instruments = {
38
+ lookup: meter.createCounter("webfinger.lookup", {
39
+ description: "Outgoing WebFinger lookup attempts.",
40
+ unit: "{lookup}"
41
+ }),
42
+ lookupDuration: meter.createHistogram("webfinger.lookup.duration", {
43
+ description: "Duration of outgoing WebFinger lookups.",
44
+ unit: "ms",
45
+ advice: { explicitBucketBoundaries: [...WEBFINGER_HISTOGRAM_BUCKETS] }
46
+ })
47
+ };
48
+ webFingerInstruments.set(meterProvider, instruments);
49
+ }
50
+ return instruments;
51
+ }
52
+ function getResourceScheme(resource) {
53
+ if (typeof resource === "string") {
54
+ const colon = resource.indexOf(":");
55
+ return colon > 0 ? resource.substring(0, colon).toLowerCase() : "";
56
+ }
57
+ return resource.protocol.replace(/:$/, "").toLowerCase();
58
+ }
59
+ const WEBFINGER_LOOKUP_SCHEME_WHITELIST = new Set([
60
+ "acct",
61
+ "http",
62
+ "https",
63
+ "mailto"
64
+ ]);
65
+ function getMetricResourceScheme(scheme) {
66
+ return WEBFINGER_LOOKUP_SCHEME_WHITELIST.has(scheme) ? scheme : "other";
67
+ }
16
68
  /**
17
69
  * Looks up a WebFinger resource.
18
70
  * @param resource The resource URL to look up.
@@ -21,17 +73,25 @@ const DEFAULT_MAX_REDIRECTION = 5;
21
73
  * @since 0.2.0
22
74
  */
23
75
  async function lookupWebFinger(resource, options = {}) {
24
- return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version).startActiveSpan("webfinger.lookup", {
76
+ const tracer = (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version);
77
+ const scheme = getResourceScheme(resource);
78
+ return await tracer.startActiveSpan("webfinger.lookup", {
25
79
  kind: _opentelemetry_api.SpanKind.CLIENT,
26
80
  attributes: {
27
81
  "webfinger.resource": resource.toString(),
28
- "webfinger.resource.scheme": typeof resource === "string" ? resource.replace(/:.*$/, "") : resource.protocol.replace(/:$/, "")
82
+ "webfinger.resource.scheme": scheme
29
83
  }
30
84
  }, async (span) => {
85
+ const meterProvider = options.meterProvider;
86
+ const start = meterProvider == null ? 0 : performance.now();
87
+ let outcome = {
88
+ resource: null,
89
+ result: "error"
90
+ };
31
91
  try {
32
- const result = await lookupWebFingerInternal(resource, options);
33
- span.setStatus({ code: result === null ? _opentelemetry_api.SpanStatusCode.ERROR : _opentelemetry_api.SpanStatusCode.OK });
34
- return result;
92
+ outcome = await lookupWebFingerInternal(resource, options);
93
+ span.setStatus({ code: outcome.resource === null ? _opentelemetry_api.SpanStatusCode.ERROR : _opentelemetry_api.SpanStatusCode.OK });
94
+ return outcome.resource;
35
95
  } catch (error) {
36
96
  span.setStatus({
37
97
  code: _opentelemetry_api.SpanStatusCode.ERROR,
@@ -39,19 +99,41 @@ async function lookupWebFinger(resource, options = {}) {
39
99
  });
40
100
  throw error;
41
101
  } finally {
102
+ if (meterProvider != null) recordWebFingerLookup(meterProvider, Math.max(0, performance.now() - start), scheme, outcome);
42
103
  span.end();
43
104
  }
44
105
  });
45
106
  }
107
+ function recordWebFingerLookup(meterProvider, durationMs, scheme, outcome) {
108
+ const attributes = {
109
+ "webfinger.lookup.result": outcome.result,
110
+ "webfinger.resource.scheme": getMetricResourceScheme(scheme)
111
+ };
112
+ if (outcome.remoteHost != null) attributes["activitypub.remote.host"] = outcome.remoteHost;
113
+ if (outcome.statusCode != null) attributes["http.response.status_code"] = outcome.statusCode;
114
+ const instruments = getWebFingerInstruments(meterProvider);
115
+ instruments.lookup.add(1, attributes);
116
+ instruments.lookupDuration.record(durationMs, attributes);
117
+ }
46
118
  async function lookupWebFingerInternal(resource, options = {}) {
47
119
  if (typeof resource === "string") resource = new URL(resource);
48
120
  let protocol = "https:";
49
121
  let server;
50
- if (resource.protocol === "acct:") {
122
+ if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
51
123
  const atPos = resource.pathname.lastIndexOf("@");
52
- if (atPos < 0) return null;
124
+ if (atPos < 0) return {
125
+ resource: null,
126
+ result: "invalid"
127
+ };
53
128
  server = resource.pathname.substring(atPos + 1);
54
- if (server === "") return null;
129
+ if (server === "" || /[/?#]/.test(server)) return {
130
+ resource: null,
131
+ result: "invalid"
132
+ };
133
+ if (resource.protocol === "acct:" && (resource.search !== "" || resource.hash !== "")) return {
134
+ resource: null,
135
+ result: "invalid"
136
+ };
55
137
  } else {
56
138
  protocol = resource.protocol;
57
139
  server = resource.host;
@@ -60,6 +142,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
60
142
  url.searchParams.set("resource", resource.href);
61
143
  let redirected = 0;
62
144
  while (true) {
145
+ const remoteHost = url.host;
63
146
  logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
64
147
  let response;
65
148
  if (options.allowPrivateAddress !== true) try {
@@ -67,7 +150,11 @@ async function lookupWebFingerInternal(resource, options = {}) {
67
150
  } catch (e) {
68
151
  if (e instanceof _fedify_vocab_runtime.UrlError) {
69
152
  logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
70
- return null;
153
+ return {
154
+ resource: null,
155
+ result: "network_error",
156
+ remoteHost
157
+ };
71
158
  }
72
159
  throw e;
73
160
  }
@@ -85,22 +172,50 @@ async function lookupWebFingerInternal(resource, options = {}) {
85
172
  url: url.href,
86
173
  error
87
174
  });
88
- return null;
175
+ return {
176
+ resource: null,
177
+ result: "network_error",
178
+ remoteHost
179
+ };
89
180
  }
90
181
  if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
91
182
  redirected++;
92
183
  const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
93
- if (redirected >= maxRedirection) {
184
+ if (redirected > maxRedirection) {
94
185
  logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
95
- return null;
186
+ return {
187
+ resource: null,
188
+ result: "invalid",
189
+ statusCode: response.status,
190
+ remoteHost
191
+ };
192
+ }
193
+ let redirectedUrl;
194
+ try {
195
+ redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
196
+ } catch (e) {
197
+ logger.error("Invalid Location header while following WebFinger redirect: {error}", {
198
+ url: url.href,
199
+ error: e
200
+ });
201
+ return {
202
+ resource: null,
203
+ result: "invalid",
204
+ statusCode: response.status,
205
+ remoteHost
206
+ };
96
207
  }
97
- const redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
98
208
  if (redirectedUrl.protocol !== url.protocol) {
99
209
  logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
100
210
  protocol: url.protocol,
101
211
  redirectedProtocol: redirectedUrl.protocol
102
212
  });
103
- return null;
213
+ return {
214
+ resource: null,
215
+ result: "invalid",
216
+ statusCode: response.status,
217
+ remoteHost
218
+ };
104
219
  }
105
220
  url = redirectedUrl;
106
221
  continue;
@@ -111,14 +226,29 @@ async function lookupWebFingerInternal(resource, options = {}) {
111
226
  status: response.status,
112
227
  statusText: response.statusText
113
228
  });
114
- return null;
229
+ return {
230
+ resource: null,
231
+ result: response.status === 404 || response.status === 410 ? "not_found" : "error",
232
+ statusCode: response.status,
233
+ remoteHost
234
+ };
115
235
  }
116
236
  try {
117
- return await response.json();
237
+ return {
238
+ resource: await response.json(),
239
+ result: "found",
240
+ statusCode: response.status,
241
+ remoteHost
242
+ };
118
243
  } catch (e) {
119
244
  if (e instanceof SyntaxError) {
120
245
  logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
121
- return null;
246
+ return {
247
+ resource: null,
248
+ result: "invalid",
249
+ statusCode: response.status,
250
+ remoteHost
251
+ };
122
252
  }
123
253
  throw e;
124
254
  }
package/dist/mod.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { GetUserAgentOptions } from "@fedify/vocab-runtime";
2
- import { TracerProvider } from "@opentelemetry/api";
2
+ import { MeterProvider, TracerProvider } from "@opentelemetry/api";
3
3
 
4
4
  //#region src/jrd.d.ts
5
5
  /**
@@ -63,6 +63,28 @@ interface Link {
63
63
  //#endregion
64
64
  //#region src/lookup.d.ts
65
65
  /**
66
+ * The terminal classification of an outgoing {@link lookupWebFinger} call,
67
+ * recorded as the `webfinger.lookup.result` attribute on the
68
+ * `webfinger.lookup` counter and `webfinger.lookup.duration` histogram.
69
+ *
70
+ * - `found`: the lookup returned a {@link ResourceDescriptor}.
71
+ * - `not_found`: the remote responded with HTTP `404 Not Found` or
72
+ * `410 Gone`. Recorded together with `http.response.status_code`.
73
+ * - `invalid`: the remote responded with content Fedify could not parse
74
+ * into a {@link ResourceDescriptor} (JSON parse failure), the
75
+ * redirect chain exceeded `maxRedirection`, the remote redirected to
76
+ * a different protocol, or the queried `acct:` resource itself was
77
+ * malformed.
78
+ * - `network_error`: no HTTP response was received (the URL was
79
+ * rejected as a private address, `fetch()` threw, or an `AbortError`
80
+ * cancelled the request).
81
+ * - `error`: the remote responded with a non-2xx HTTP response that is
82
+ * neither `404` nor `410`, or any other unexpected failure bubbled up
83
+ * from the lookup.
84
+ * @since 2.3.0
85
+ */
86
+ type WebFingerLookupResult = "found" | "not_found" | "invalid" | "network_error" | "error";
87
+ /**
66
88
  * Options for {@link lookupWebFinger}.
67
89
  * @since 1.3.0
68
90
  */
@@ -95,6 +117,15 @@ interface LookupWebFingerOptions {
95
117
  */
96
118
  tracerProvider?: TracerProvider;
97
119
  /**
120
+ * The OpenTelemetry meter provider used to record the `webfinger.lookup`
121
+ * counter and `webfinger.lookup.duration` histogram. If omitted, no
122
+ * metric measurements are emitted (the helper is opt-in to avoid
123
+ * touching the global meter provider for callers that do not use
124
+ * OpenTelemetry).
125
+ * @since 2.3.0
126
+ */
127
+ meterProvider?: MeterProvider;
128
+ /**
98
129
  * AbortSignal for cancelling the request.
99
130
  * @since 1.8.0
100
131
  */
@@ -109,4 +140,4 @@ interface LookupWebFingerOptions {
109
140
  */
110
141
  declare function lookupWebFinger(resource: URL | string, options?: LookupWebFingerOptions): Promise<ResourceDescriptor | null>;
111
142
  //#endregion
112
- export { Link, LookupWebFingerOptions, ResourceDescriptor, lookupWebFinger };
143
+ export { Link, LookupWebFingerOptions, ResourceDescriptor, WebFingerLookupResult, lookupWebFinger };
package/dist/mod.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { GetUserAgentOptions } from "@fedify/vocab-runtime";
2
- import { TracerProvider } from "@opentelemetry/api";
2
+ import { MeterProvider, TracerProvider } from "@opentelemetry/api";
3
3
 
4
4
  //#region src/jrd.d.ts
5
5
  /**
@@ -63,6 +63,28 @@ interface Link {
63
63
  //#endregion
64
64
  //#region src/lookup.d.ts
65
65
  /**
66
+ * The terminal classification of an outgoing {@link lookupWebFinger} call,
67
+ * recorded as the `webfinger.lookup.result` attribute on the
68
+ * `webfinger.lookup` counter and `webfinger.lookup.duration` histogram.
69
+ *
70
+ * - `found`: the lookup returned a {@link ResourceDescriptor}.
71
+ * - `not_found`: the remote responded with HTTP `404 Not Found` or
72
+ * `410 Gone`. Recorded together with `http.response.status_code`.
73
+ * - `invalid`: the remote responded with content Fedify could not parse
74
+ * into a {@link ResourceDescriptor} (JSON parse failure), the
75
+ * redirect chain exceeded `maxRedirection`, the remote redirected to
76
+ * a different protocol, or the queried `acct:` resource itself was
77
+ * malformed.
78
+ * - `network_error`: no HTTP response was received (the URL was
79
+ * rejected as a private address, `fetch()` threw, or an `AbortError`
80
+ * cancelled the request).
81
+ * - `error`: the remote responded with a non-2xx HTTP response that is
82
+ * neither `404` nor `410`, or any other unexpected failure bubbled up
83
+ * from the lookup.
84
+ * @since 2.3.0
85
+ */
86
+ type WebFingerLookupResult = "found" | "not_found" | "invalid" | "network_error" | "error";
87
+ /**
66
88
  * Options for {@link lookupWebFinger}.
67
89
  * @since 1.3.0
68
90
  */
@@ -95,6 +117,15 @@ interface LookupWebFingerOptions {
95
117
  */
96
118
  tracerProvider?: TracerProvider;
97
119
  /**
120
+ * The OpenTelemetry meter provider used to record the `webfinger.lookup`
121
+ * counter and `webfinger.lookup.duration` histogram. If omitted, no
122
+ * metric measurements are emitted (the helper is opt-in to avoid
123
+ * touching the global meter provider for callers that do not use
124
+ * OpenTelemetry).
125
+ * @since 2.3.0
126
+ */
127
+ meterProvider?: MeterProvider;
128
+ /**
98
129
  * AbortSignal for cancelling the request.
99
130
  * @since 1.8.0
100
131
  */
@@ -109,4 +140,4 @@ interface LookupWebFingerOptions {
109
140
  */
110
141
  declare function lookupWebFinger(resource: URL | string, options?: LookupWebFingerOptions): Promise<ResourceDescriptor | null>;
111
142
  //#endregion
112
- export { Link, LookupWebFingerOptions, ResourceDescriptor, lookupWebFinger };
143
+ export { Link, LookupWebFingerOptions, ResourceDescriptor, WebFingerLookupResult, lookupWebFinger };
package/dist/mod.js CHANGED
@@ -3,7 +3,7 @@ import { getLogger } from "@logtape/logtape";
3
3
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
4
4
  //#region deno.json
5
5
  var name = "@fedify/webfinger";
6
- var version = "2.3.0-dev.994+9071ca0a";
6
+ var version = "2.3.0-pr.809.36+c592d116";
7
7
  //#endregion
8
8
  //#region src/lookup.ts
9
9
  const logger = getLogger([
@@ -12,6 +12,58 @@ const logger = getLogger([
12
12
  "lookup"
13
13
  ]);
14
14
  const DEFAULT_MAX_REDIRECTION = 5;
15
+ const WEBFINGER_HISTOGRAM_BUCKETS = [
16
+ 5,
17
+ 10,
18
+ 25,
19
+ 50,
20
+ 75,
21
+ 100,
22
+ 250,
23
+ 500,
24
+ 750,
25
+ 1e3,
26
+ 2500,
27
+ 5e3,
28
+ 7500,
29
+ 1e4
30
+ ];
31
+ const webFingerInstruments = /* @__PURE__ */ new WeakMap();
32
+ function getWebFingerInstruments(meterProvider) {
33
+ let instruments = webFingerInstruments.get(meterProvider);
34
+ if (instruments == null) {
35
+ const meter = meterProvider.getMeter(name, version);
36
+ instruments = {
37
+ lookup: meter.createCounter("webfinger.lookup", {
38
+ description: "Outgoing WebFinger lookup attempts.",
39
+ unit: "{lookup}"
40
+ }),
41
+ lookupDuration: meter.createHistogram("webfinger.lookup.duration", {
42
+ description: "Duration of outgoing WebFinger lookups.",
43
+ unit: "ms",
44
+ advice: { explicitBucketBoundaries: [...WEBFINGER_HISTOGRAM_BUCKETS] }
45
+ })
46
+ };
47
+ webFingerInstruments.set(meterProvider, instruments);
48
+ }
49
+ return instruments;
50
+ }
51
+ function getResourceScheme(resource) {
52
+ if (typeof resource === "string") {
53
+ const colon = resource.indexOf(":");
54
+ return colon > 0 ? resource.substring(0, colon).toLowerCase() : "";
55
+ }
56
+ return resource.protocol.replace(/:$/, "").toLowerCase();
57
+ }
58
+ const WEBFINGER_LOOKUP_SCHEME_WHITELIST = new Set([
59
+ "acct",
60
+ "http",
61
+ "https",
62
+ "mailto"
63
+ ]);
64
+ function getMetricResourceScheme(scheme) {
65
+ return WEBFINGER_LOOKUP_SCHEME_WHITELIST.has(scheme) ? scheme : "other";
66
+ }
15
67
  /**
16
68
  * Looks up a WebFinger resource.
17
69
  * @param resource The resource URL to look up.
@@ -20,17 +72,25 @@ const DEFAULT_MAX_REDIRECTION = 5;
20
72
  * @since 0.2.0
21
73
  */
22
74
  async function lookupWebFinger(resource, options = {}) {
23
- return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("webfinger.lookup", {
75
+ const tracer = (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version);
76
+ const scheme = getResourceScheme(resource);
77
+ return await tracer.startActiveSpan("webfinger.lookup", {
24
78
  kind: SpanKind.CLIENT,
25
79
  attributes: {
26
80
  "webfinger.resource": resource.toString(),
27
- "webfinger.resource.scheme": typeof resource === "string" ? resource.replace(/:.*$/, "") : resource.protocol.replace(/:$/, "")
81
+ "webfinger.resource.scheme": scheme
28
82
  }
29
83
  }, async (span) => {
84
+ const meterProvider = options.meterProvider;
85
+ const start = meterProvider == null ? 0 : performance.now();
86
+ let outcome = {
87
+ resource: null,
88
+ result: "error"
89
+ };
30
90
  try {
31
- const result = await lookupWebFingerInternal(resource, options);
32
- span.setStatus({ code: result === null ? SpanStatusCode.ERROR : SpanStatusCode.OK });
33
- return result;
91
+ outcome = await lookupWebFingerInternal(resource, options);
92
+ span.setStatus({ code: outcome.resource === null ? SpanStatusCode.ERROR : SpanStatusCode.OK });
93
+ return outcome.resource;
34
94
  } catch (error) {
35
95
  span.setStatus({
36
96
  code: SpanStatusCode.ERROR,
@@ -38,19 +98,41 @@ async function lookupWebFinger(resource, options = {}) {
38
98
  });
39
99
  throw error;
40
100
  } finally {
101
+ if (meterProvider != null) recordWebFingerLookup(meterProvider, Math.max(0, performance.now() - start), scheme, outcome);
41
102
  span.end();
42
103
  }
43
104
  });
44
105
  }
106
+ function recordWebFingerLookup(meterProvider, durationMs, scheme, outcome) {
107
+ const attributes = {
108
+ "webfinger.lookup.result": outcome.result,
109
+ "webfinger.resource.scheme": getMetricResourceScheme(scheme)
110
+ };
111
+ if (outcome.remoteHost != null) attributes["activitypub.remote.host"] = outcome.remoteHost;
112
+ if (outcome.statusCode != null) attributes["http.response.status_code"] = outcome.statusCode;
113
+ const instruments = getWebFingerInstruments(meterProvider);
114
+ instruments.lookup.add(1, attributes);
115
+ instruments.lookupDuration.record(durationMs, attributes);
116
+ }
45
117
  async function lookupWebFingerInternal(resource, options = {}) {
46
118
  if (typeof resource === "string") resource = new URL(resource);
47
119
  let protocol = "https:";
48
120
  let server;
49
- if (resource.protocol === "acct:") {
121
+ if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
50
122
  const atPos = resource.pathname.lastIndexOf("@");
51
- if (atPos < 0) return null;
123
+ if (atPos < 0) return {
124
+ resource: null,
125
+ result: "invalid"
126
+ };
52
127
  server = resource.pathname.substring(atPos + 1);
53
- if (server === "") return null;
128
+ if (server === "" || /[/?#]/.test(server)) return {
129
+ resource: null,
130
+ result: "invalid"
131
+ };
132
+ if (resource.protocol === "acct:" && (resource.search !== "" || resource.hash !== "")) return {
133
+ resource: null,
134
+ result: "invalid"
135
+ };
54
136
  } else {
55
137
  protocol = resource.protocol;
56
138
  server = resource.host;
@@ -59,6 +141,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
59
141
  url.searchParams.set("resource", resource.href);
60
142
  let redirected = 0;
61
143
  while (true) {
144
+ const remoteHost = url.host;
62
145
  logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
63
146
  let response;
64
147
  if (options.allowPrivateAddress !== true) try {
@@ -66,7 +149,11 @@ async function lookupWebFingerInternal(resource, options = {}) {
66
149
  } catch (e) {
67
150
  if (e instanceof UrlError) {
68
151
  logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
69
- return null;
152
+ return {
153
+ resource: null,
154
+ result: "network_error",
155
+ remoteHost
156
+ };
70
157
  }
71
158
  throw e;
72
159
  }
@@ -84,22 +171,50 @@ async function lookupWebFingerInternal(resource, options = {}) {
84
171
  url: url.href,
85
172
  error
86
173
  });
87
- return null;
174
+ return {
175
+ resource: null,
176
+ result: "network_error",
177
+ remoteHost
178
+ };
88
179
  }
89
180
  if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
90
181
  redirected++;
91
182
  const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
92
- if (redirected >= maxRedirection) {
183
+ if (redirected > maxRedirection) {
93
184
  logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
94
- return null;
185
+ return {
186
+ resource: null,
187
+ result: "invalid",
188
+ statusCode: response.status,
189
+ remoteHost
190
+ };
191
+ }
192
+ let redirectedUrl;
193
+ try {
194
+ redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
195
+ } catch (e) {
196
+ logger.error("Invalid Location header while following WebFinger redirect: {error}", {
197
+ url: url.href,
198
+ error: e
199
+ });
200
+ return {
201
+ resource: null,
202
+ result: "invalid",
203
+ statusCode: response.status,
204
+ remoteHost
205
+ };
95
206
  }
96
- const redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
97
207
  if (redirectedUrl.protocol !== url.protocol) {
98
208
  logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
99
209
  protocol: url.protocol,
100
210
  redirectedProtocol: redirectedUrl.protocol
101
211
  });
102
- return null;
212
+ return {
213
+ resource: null,
214
+ result: "invalid",
215
+ statusCode: response.status,
216
+ remoteHost
217
+ };
103
218
  }
104
219
  url = redirectedUrl;
105
220
  continue;
@@ -110,14 +225,29 @@ async function lookupWebFingerInternal(resource, options = {}) {
110
225
  status: response.status,
111
226
  statusText: response.statusText
112
227
  });
113
- return null;
228
+ return {
229
+ resource: null,
230
+ result: response.status === 404 || response.status === 410 ? "not_found" : "error",
231
+ statusCode: response.status,
232
+ remoteHost
233
+ };
114
234
  }
115
235
  try {
116
- return await response.json();
236
+ return {
237
+ resource: await response.json(),
238
+ result: "found",
239
+ statusCode: response.status,
240
+ remoteHost
241
+ };
117
242
  } catch (e) {
118
243
  if (e instanceof SyntaxError) {
119
244
  logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
120
- return null;
245
+ return {
246
+ resource: null,
247
+ result: "invalid",
248
+ statusCode: response.status,
249
+ remoteHost
250
+ };
121
251
  }
122
252
  throw e;
123
253
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/webfinger",
3
- "version": "2.3.0-dev.994+9071ca0a",
3
+ "version": "2.3.0-pr.809.36+c592d116",
4
4
  "homepage": "https://fedify.dev/",
5
5
  "repository": {
6
6
  "type": "git",
@@ -52,15 +52,15 @@
52
52
  "devDependencies": {
53
53
  "@types/node": "^24.2.1",
54
54
  "fetch-mock": "^12.5.4",
55
- "tsdown": "^0.21.6",
56
- "typescript": "^5.9.2",
55
+ "tsdown": "^0.22.0",
56
+ "typescript": "^6.0.0",
57
57
  "@fedify/fixture": "2.0.0"
58
58
  },
59
59
  "dependencies": {
60
- "@logtape/logtape": "^2.0.5",
61
- "@opentelemetry/api": "^1.9.0",
62
- "es-toolkit": "1.43.0",
63
- "@fedify/vocab-runtime": "2.3.0-dev.994+9071ca0a"
60
+ "@logtape/logtape": "^2.1.0",
61
+ "@opentelemetry/api": "^1.9.1",
62
+ "es-toolkit": "1.46.1",
63
+ "@fedify/vocab-runtime": "2.3.0-pr.809.36+c592d116"
64
64
  },
65
65
  "scripts": {
66
66
  "build:self": "tsdown",