@fedify/webfinger 2.0.0-dev.0
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/LICENSE +20 -0
- package/deno.json +26 -0
- package/dist/lookup.test.cjs +1598 -0
- package/dist/lookup.test.d.cts +1 -0
- package/dist/lookup.test.d.ts +1 -0
- package/dist/lookup.test.js +1599 -0
- package/dist/mod.cjs +183 -0
- package/dist/mod.d.cts +113 -0
- package/dist/mod.d.ts +113 -0
- package/dist/mod.js +160 -0
- package/package.json +70 -0
- package/src/jrd.ts +67 -0
- package/src/lookup.test.ts +331 -0
- package/src/lookup.ts +226 -0
- package/src/mod.ts +7 -0
- package/tsdown.config.ts +20 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { test } from "@fedify/fixture";
|
|
2
|
+
import { withTimeout } from "es-toolkit";
|
|
3
|
+
import fetchMock from "fetch-mock";
|
|
4
|
+
import { deepStrictEqual } from "node:assert/strict";
|
|
5
|
+
import type { ResourceDescriptor } from "./jrd.ts";
|
|
6
|
+
import { lookupWebFinger } from "./lookup.ts";
|
|
7
|
+
|
|
8
|
+
test({
|
|
9
|
+
name: "lookupWebFinger()",
|
|
10
|
+
sanitizeOps: false,
|
|
11
|
+
sanitizeResources: false,
|
|
12
|
+
async fn(t) {
|
|
13
|
+
await t.step("invalid resource", async () => {
|
|
14
|
+
deepStrictEqual(await lookupWebFinger("acct:johndoe"), null);
|
|
15
|
+
deepStrictEqual(await lookupWebFinger(new URL("acct:johndoe")), null);
|
|
16
|
+
deepStrictEqual(await lookupWebFinger("acct:johndoe@"), null);
|
|
17
|
+
deepStrictEqual(await lookupWebFinger(new URL("acct:johndoe@")), null);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
await t.step("connection refused", async () => {
|
|
21
|
+
deepStrictEqual(
|
|
22
|
+
await lookupWebFinger("acct:johndoe@fedify-test.internal"),
|
|
23
|
+
null,
|
|
24
|
+
);
|
|
25
|
+
deepStrictEqual(
|
|
26
|
+
await lookupWebFinger("https://fedify-test.internal/foo"),
|
|
27
|
+
null,
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
fetchMock.spyGlobal();
|
|
32
|
+
fetchMock.get(
|
|
33
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
34
|
+
{ status: 404 },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
await t.step("not found", async () => {
|
|
38
|
+
deepStrictEqual(await lookupWebFinger("acct:johndoe@example.com"), null);
|
|
39
|
+
deepStrictEqual(await lookupWebFinger("https://example.com/foo"), null);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const expected: ResourceDescriptor = {
|
|
43
|
+
subject: "acct:johndoe@example.com",
|
|
44
|
+
links: [],
|
|
45
|
+
};
|
|
46
|
+
fetchMock.removeRoutes();
|
|
47
|
+
fetchMock.get(
|
|
48
|
+
"https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com",
|
|
49
|
+
{ body: expected },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
await t.step("acct", async () => {
|
|
53
|
+
deepStrictEqual(
|
|
54
|
+
await lookupWebFinger("acct:johndoe@example.com"),
|
|
55
|
+
expected,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const expected2: ResourceDescriptor = {
|
|
60
|
+
subject: "https://example.com/foo",
|
|
61
|
+
links: [],
|
|
62
|
+
};
|
|
63
|
+
fetchMock.removeRoutes();
|
|
64
|
+
fetchMock.get(
|
|
65
|
+
"https://example.com/.well-known/webfinger?resource=https%3A%2F%2Fexample.com%2Ffoo",
|
|
66
|
+
{ body: expected2 },
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
await t.step("https", async () => {
|
|
70
|
+
deepStrictEqual(
|
|
71
|
+
await lookupWebFinger("https://example.com/foo"),
|
|
72
|
+
expected2,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
fetchMock.removeRoutes();
|
|
77
|
+
fetchMock.get(
|
|
78
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
79
|
+
{ body: "not json" },
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
await t.step("invalid response", async () => {
|
|
83
|
+
deepStrictEqual(await lookupWebFinger("acct:johndoe@example.com"), null);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
fetchMock.removeRoutes();
|
|
87
|
+
fetchMock.get(
|
|
88
|
+
"begin:https://localhost/.well-known/webfinger?",
|
|
89
|
+
{
|
|
90
|
+
subject: "acct:test@localhost",
|
|
91
|
+
links: [
|
|
92
|
+
{
|
|
93
|
+
rel: "self",
|
|
94
|
+
type: "application/activity+json",
|
|
95
|
+
href: "https://localhost/actor",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
await t.step("private address", async () => {
|
|
102
|
+
deepStrictEqual(await lookupWebFinger("acct:test@localhost"), null);
|
|
103
|
+
deepStrictEqual(
|
|
104
|
+
await lookupWebFinger("acct:test@localhost", {
|
|
105
|
+
allowPrivateAddress: true,
|
|
106
|
+
}),
|
|
107
|
+
{
|
|
108
|
+
subject: "acct:test@localhost",
|
|
109
|
+
links: [
|
|
110
|
+
{
|
|
111
|
+
rel: "self",
|
|
112
|
+
type: "application/activity+json",
|
|
113
|
+
href: "https://localhost/actor",
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
fetchMock.removeRoutes();
|
|
121
|
+
fetchMock.get(
|
|
122
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
123
|
+
{
|
|
124
|
+
status: 302,
|
|
125
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
fetchMock.get(
|
|
129
|
+
"begin:https://example.com/.well-known/webfinger2",
|
|
130
|
+
{ body: expected },
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
await t.step("redirection", async () => {
|
|
134
|
+
deepStrictEqual(
|
|
135
|
+
await lookupWebFinger("acct:johndoe@example.com"),
|
|
136
|
+
expected,
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
fetchMock.removeRoutes();
|
|
141
|
+
fetchMock.get(
|
|
142
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
143
|
+
{
|
|
144
|
+
status: 302,
|
|
145
|
+
headers: { Location: "/.well-known/webfinger" },
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
await t.step("infinite redirection", async () => {
|
|
150
|
+
const result = await withTimeout(
|
|
151
|
+
() => lookupWebFinger("acct:johndoe@example.com"),
|
|
152
|
+
2000,
|
|
153
|
+
);
|
|
154
|
+
deepStrictEqual(result, null);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
fetchMock.removeRoutes();
|
|
158
|
+
fetchMock.get(
|
|
159
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
160
|
+
{
|
|
161
|
+
status: 302,
|
|
162
|
+
headers: { Location: "ftp://example.com/" },
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
await t.step("redirection to different protocol", async () => {
|
|
167
|
+
deepStrictEqual(await lookupWebFinger("acct:johndoe@example.com"), null);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
fetchMock.removeRoutes();
|
|
171
|
+
fetchMock.get(
|
|
172
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
173
|
+
{
|
|
174
|
+
status: 302,
|
|
175
|
+
headers: { Location: "https://localhost/" },
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
await t.step("redirection to private address", async () => {
|
|
180
|
+
deepStrictEqual(await lookupWebFinger("acct:johndoe@example.com"), null);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
fetchMock.removeRoutes();
|
|
184
|
+
let redirectCount = 0;
|
|
185
|
+
fetchMock.get(
|
|
186
|
+
"begin:https://example.com/.well-known/webfinger",
|
|
187
|
+
() => {
|
|
188
|
+
redirectCount++;
|
|
189
|
+
if (redirectCount < 3) {
|
|
190
|
+
return {
|
|
191
|
+
status: 302,
|
|
192
|
+
headers: {
|
|
193
|
+
Location: `/.well-known/webfinger?redirect=${redirectCount}`,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return { body: expected };
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
await t.step("custom maxRedirection", async () => {
|
|
202
|
+
// Test with maxRedirection: 2 (should fail)
|
|
203
|
+
redirectCount = 0;
|
|
204
|
+
deepStrictEqual(
|
|
205
|
+
await lookupWebFinger("acct:johndoe@example.com", {
|
|
206
|
+
maxRedirection: 2,
|
|
207
|
+
}),
|
|
208
|
+
null,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Test with maxRedirection: 3 (should succeed)
|
|
212
|
+
redirectCount = 0;
|
|
213
|
+
deepStrictEqual(
|
|
214
|
+
await lookupWebFinger("acct:johndoe@example.com", {
|
|
215
|
+
maxRedirection: 3,
|
|
216
|
+
}),
|
|
217
|
+
expected,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Test with default maxRedirection: 5 (should succeed)
|
|
221
|
+
redirectCount = 0;
|
|
222
|
+
deepStrictEqual(
|
|
223
|
+
await lookupWebFinger("acct:johndoe@example.com"),
|
|
224
|
+
expected,
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
fetchMock.removeRoutes();
|
|
229
|
+
fetchMock.get(
|
|
230
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
231
|
+
() =>
|
|
232
|
+
new Promise((resolve) => {
|
|
233
|
+
const timeoutId = setTimeout(() => {
|
|
234
|
+
resolve({ body: expected });
|
|
235
|
+
}, 1000);
|
|
236
|
+
|
|
237
|
+
return () => clearTimeout(timeoutId);
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
await t.step("request cancellation", async () => {
|
|
242
|
+
// Test cancelling a request immediately using AbortController
|
|
243
|
+
const controller = new AbortController();
|
|
244
|
+
const promise = lookupWebFinger("acct:johndoe@example.com", {
|
|
245
|
+
signal: controller.signal,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Abort the request right after starting it
|
|
249
|
+
controller.abort();
|
|
250
|
+
deepStrictEqual(await promise, null);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
fetchMock.removeRoutes();
|
|
254
|
+
let redirectCount2 = 0;
|
|
255
|
+
fetchMock.get(
|
|
256
|
+
"begin:https://example.com/.well-known/webfinger",
|
|
257
|
+
() => {
|
|
258
|
+
redirectCount2++;
|
|
259
|
+
if (redirectCount2 === 1) {
|
|
260
|
+
return {
|
|
261
|
+
status: 302,
|
|
262
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return new Promise((resolve) => {
|
|
266
|
+
const timeoutId = setTimeout(() => {
|
|
267
|
+
resolve({ body: expected });
|
|
268
|
+
}, 1000);
|
|
269
|
+
|
|
270
|
+
return () => clearTimeout(timeoutId);
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
await t.step("cancellation during redirection", async () => {
|
|
276
|
+
// Test cancelling a request during redirection process
|
|
277
|
+
const controller = new AbortController();
|
|
278
|
+
const promise = lookupWebFinger("acct:johndoe@example.com", {
|
|
279
|
+
signal: controller.signal,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Cancel during the delayed second request after redirection
|
|
283
|
+
setTimeout(() => controller.abort(), 100);
|
|
284
|
+
deepStrictEqual(await promise, null);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
fetchMock.removeRoutes();
|
|
288
|
+
fetchMock.get(
|
|
289
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
290
|
+
() =>
|
|
291
|
+
new Promise((resolve) => {
|
|
292
|
+
const timeoutId = setTimeout(() => {
|
|
293
|
+
resolve({ body: expected });
|
|
294
|
+
}, 500);
|
|
295
|
+
|
|
296
|
+
return () => clearTimeout(timeoutId);
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
await t.step("cancellation with immediate abort", async () => {
|
|
301
|
+
// Test starting a request with an already aborted AbortController
|
|
302
|
+
const controller = new AbortController();
|
|
303
|
+
controller.abort();
|
|
304
|
+
|
|
305
|
+
// Use a signal that was already aborted before starting the request
|
|
306
|
+
const result = await lookupWebFinger("acct:johndoe@example.com", {
|
|
307
|
+
signal: controller.signal,
|
|
308
|
+
});
|
|
309
|
+
deepStrictEqual(result, null);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
fetchMock.removeRoutes();
|
|
313
|
+
fetchMock.get(
|
|
314
|
+
"begin:https://example.com/.well-known/webfinger?",
|
|
315
|
+
{ body: expected },
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
await t.step("successful request with signal", async () => {
|
|
319
|
+
// Test successful request with a normal AbortController signal
|
|
320
|
+
const controller = new AbortController();
|
|
321
|
+
const result = await lookupWebFinger("acct:johndoe@example.com", {
|
|
322
|
+
signal: controller.signal,
|
|
323
|
+
});
|
|
324
|
+
deepStrictEqual(result, expected);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
fetchMock.hardReset();
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// cSpell: ignore johndoe
|
package/src/lookup.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getUserAgent,
|
|
3
|
+
type GetUserAgentOptions,
|
|
4
|
+
UrlError,
|
|
5
|
+
validatePublicUrl,
|
|
6
|
+
} from "@fedify/vocab-runtime";
|
|
7
|
+
import { getLogger } from "@logtape/logtape";
|
|
8
|
+
import {
|
|
9
|
+
SpanKind,
|
|
10
|
+
SpanStatusCode,
|
|
11
|
+
trace,
|
|
12
|
+
type TracerProvider,
|
|
13
|
+
} from "@opentelemetry/api";
|
|
14
|
+
import metadata from "../deno.json" with { type: "json" };
|
|
15
|
+
import type { ResourceDescriptor } from "./jrd.ts";
|
|
16
|
+
|
|
17
|
+
const logger = getLogger(["fedify", "webfinger", "lookup"]);
|
|
18
|
+
|
|
19
|
+
const DEFAULT_MAX_REDIRECTION = 5;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for {@link lookupWebFinger}.
|
|
23
|
+
* @since 1.3.0
|
|
24
|
+
*/
|
|
25
|
+
export interface LookupWebFingerOptions {
|
|
26
|
+
/**
|
|
27
|
+
* The options for making `User-Agent` header.
|
|
28
|
+
* If a string is given, it is used as the `User-Agent` header value.
|
|
29
|
+
* If an object is given, it is passed to {@link getUserAgent} to generate
|
|
30
|
+
* the `User-Agent` header value.
|
|
31
|
+
*/
|
|
32
|
+
userAgent?: GetUserAgentOptions | string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether to allow private IP addresses in the URL.
|
|
36
|
+
*
|
|
37
|
+
* Mostly useful for testing purposes. *Do not use this in production.*
|
|
38
|
+
*
|
|
39
|
+
* Turned off by default.
|
|
40
|
+
* @since 1.4.0
|
|
41
|
+
*/
|
|
42
|
+
allowPrivateAddress?: boolean;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The maximum number of redirections to follow.
|
|
46
|
+
* @default `5`
|
|
47
|
+
* @since 1.8.0
|
|
48
|
+
*/
|
|
49
|
+
maxRedirection?: number;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The OpenTelemetry tracer provider. If omitted, the global tracer provider
|
|
53
|
+
* is used.
|
|
54
|
+
*/
|
|
55
|
+
tracerProvider?: TracerProvider;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* AbortSignal for cancelling the request.
|
|
59
|
+
* @since 1.8.0
|
|
60
|
+
* @
|
|
61
|
+
*/
|
|
62
|
+
signal?: AbortSignal;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Looks up a WebFinger resource.
|
|
67
|
+
* @param resource The resource URL to look up.
|
|
68
|
+
* @param options Extra options for looking up the resource.
|
|
69
|
+
* @returns The resource descriptor, or `null` if not found.
|
|
70
|
+
* @since 0.2.0
|
|
71
|
+
*/
|
|
72
|
+
export async function lookupWebFinger(
|
|
73
|
+
resource: URL | string,
|
|
74
|
+
options: LookupWebFingerOptions = {},
|
|
75
|
+
): Promise<ResourceDescriptor | null> {
|
|
76
|
+
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
|
|
77
|
+
const tracer = tracerProvider.getTracer(
|
|
78
|
+
metadata.name,
|
|
79
|
+
metadata.version,
|
|
80
|
+
);
|
|
81
|
+
return await tracer.startActiveSpan(
|
|
82
|
+
"webfinger.lookup",
|
|
83
|
+
{
|
|
84
|
+
kind: SpanKind.CLIENT,
|
|
85
|
+
attributes: {
|
|
86
|
+
"webfinger.resource": resource.toString(),
|
|
87
|
+
"webfinger.resource.scheme": typeof resource === "string"
|
|
88
|
+
? resource.replace(/:.*$/, "")
|
|
89
|
+
: resource.protocol.replace(/:$/, ""),
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
async (span) => {
|
|
93
|
+
try {
|
|
94
|
+
const result = await lookupWebFingerInternal(resource, options);
|
|
95
|
+
span.setStatus({
|
|
96
|
+
code: result === null ? SpanStatusCode.ERROR : SpanStatusCode.OK,
|
|
97
|
+
});
|
|
98
|
+
return result;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
span.setStatus({
|
|
101
|
+
code: SpanStatusCode.ERROR,
|
|
102
|
+
message: String(error),
|
|
103
|
+
});
|
|
104
|
+
throw error;
|
|
105
|
+
} finally {
|
|
106
|
+
span.end();
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function lookupWebFingerInternal(
|
|
113
|
+
resource: URL | string,
|
|
114
|
+
options: LookupWebFingerOptions = {},
|
|
115
|
+
): Promise<ResourceDescriptor | null> {
|
|
116
|
+
if (typeof resource === "string") resource = new URL(resource);
|
|
117
|
+
let protocol = "https:";
|
|
118
|
+
let server: string;
|
|
119
|
+
if (resource.protocol === "acct:") {
|
|
120
|
+
const atPos = resource.pathname.lastIndexOf("@");
|
|
121
|
+
if (atPos < 0) return null;
|
|
122
|
+
server = resource.pathname.substring(atPos + 1);
|
|
123
|
+
if (server === "") return null;
|
|
124
|
+
} else {
|
|
125
|
+
protocol = resource.protocol;
|
|
126
|
+
server = resource.host;
|
|
127
|
+
}
|
|
128
|
+
let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
|
|
129
|
+
url.searchParams.set("resource", resource.href);
|
|
130
|
+
let redirected = 0;
|
|
131
|
+
while (true) {
|
|
132
|
+
logger.debug(
|
|
133
|
+
"Fetching WebFinger resource descriptor from {url}...",
|
|
134
|
+
{ url: url.href },
|
|
135
|
+
);
|
|
136
|
+
let response: Response;
|
|
137
|
+
if (options.allowPrivateAddress !== true) {
|
|
138
|
+
try {
|
|
139
|
+
await validatePublicUrl(url.href);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
if (e instanceof UrlError) {
|
|
142
|
+
logger.error(
|
|
143
|
+
"Invalid URL for WebFinger resource descriptor: {error}",
|
|
144
|
+
{ error: e },
|
|
145
|
+
);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
throw e;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
response = await fetch(url, {
|
|
153
|
+
headers: {
|
|
154
|
+
Accept: "application/jrd+json",
|
|
155
|
+
"User-Agent": typeof options.userAgent === "string"
|
|
156
|
+
? options.userAgent
|
|
157
|
+
: getUserAgent(options.userAgent),
|
|
158
|
+
},
|
|
159
|
+
redirect: "manual",
|
|
160
|
+
signal: options.signal,
|
|
161
|
+
});
|
|
162
|
+
} catch (error) {
|
|
163
|
+
logger.debug(
|
|
164
|
+
"Failed to fetch WebFinger resource descriptor: {error}",
|
|
165
|
+
{ url: url.href, error },
|
|
166
|
+
);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
if (
|
|
170
|
+
response.status >= 300 && response.status < 400 &&
|
|
171
|
+
response.headers.has("Location")
|
|
172
|
+
) {
|
|
173
|
+
redirected++;
|
|
174
|
+
const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
|
|
175
|
+
if (redirected >= maxRedirection) {
|
|
176
|
+
logger.error(
|
|
177
|
+
"Too many redirections ({redirections}) while fetching WebFinger " +
|
|
178
|
+
"resource descriptor.",
|
|
179
|
+
{ redirections: redirected },
|
|
180
|
+
);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const redirectedUrl = new URL(
|
|
184
|
+
response.headers.get("Location")!,
|
|
185
|
+
response.url == null || response.url === "" ? url : response.url,
|
|
186
|
+
);
|
|
187
|
+
if (redirectedUrl.protocol !== url.protocol) {
|
|
188
|
+
logger.error(
|
|
189
|
+
"Redirected to a different protocol ({protocol} to " +
|
|
190
|
+
"{redirectedProtocol}) while fetching WebFinger resource " +
|
|
191
|
+
"descriptor.",
|
|
192
|
+
{
|
|
193
|
+
protocol: url.protocol,
|
|
194
|
+
redirectedProtocol: redirectedUrl.protocol,
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
url = redirectedUrl;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!response.ok) {
|
|
203
|
+
logger.debug(
|
|
204
|
+
"Failed to fetch WebFinger resource descriptor: {status} {statusText}.",
|
|
205
|
+
{
|
|
206
|
+
url: url.href,
|
|
207
|
+
status: response.status,
|
|
208
|
+
statusText: response.statusText,
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
return await response.json() as ResourceDescriptor;
|
|
215
|
+
} catch (e) {
|
|
216
|
+
if (e instanceof SyntaxError) {
|
|
217
|
+
logger.debug(
|
|
218
|
+
"Failed to parse WebFinger resource descriptor as JSON: {error}",
|
|
219
|
+
{ error: e },
|
|
220
|
+
);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
throw e;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
package/src/mod.ts
ADDED
package/tsdown.config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { glob } from "node:fs/promises";
|
|
2
|
+
import { sep } from "node:path";
|
|
3
|
+
import { defineConfig } from "tsdown";
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
defineConfig({
|
|
7
|
+
entry: ["src/mod.ts"],
|
|
8
|
+
dts: true,
|
|
9
|
+
format: ["esm", "cjs"],
|
|
10
|
+
platform: "node",
|
|
11
|
+
external: [/^node:/],
|
|
12
|
+
}),
|
|
13
|
+
defineConfig({
|
|
14
|
+
entry: (await Array.fromAsync(glob(`src/**/*.test.ts`)))
|
|
15
|
+
.map((f) => f.replace(sep, "/")),
|
|
16
|
+
format: ["esm", "cjs"],
|
|
17
|
+
platform: "node",
|
|
18
|
+
external: [/^node:/],
|
|
19
|
+
}),
|
|
20
|
+
];
|