@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
package/dist/mod.cjs
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
const __fedify_vocab_runtime = __toESM(require("@fedify/vocab-runtime"));
|
|
25
|
+
const __logtape_logtape = __toESM(require("@logtape/logtape"));
|
|
26
|
+
const __opentelemetry_api = __toESM(require("@opentelemetry/api"));
|
|
27
|
+
|
|
28
|
+
//#region deno.json
|
|
29
|
+
var name = "@fedify/webfinger";
|
|
30
|
+
var version = "2.0.0";
|
|
31
|
+
var license = "MIT";
|
|
32
|
+
var exports$1 = { ".": "./src/mod.ts" };
|
|
33
|
+
var description = "WebFinger client library for Fedify";
|
|
34
|
+
var author = {
|
|
35
|
+
"name": "Hong Minhee",
|
|
36
|
+
"email": "hong@minhee.org",
|
|
37
|
+
"url": "https://hongminhee.org/"
|
|
38
|
+
};
|
|
39
|
+
var imports = {
|
|
40
|
+
"es-toolkit": "npm:es-toolkit@^1.42.0",
|
|
41
|
+
"fetch-mock": "npm:fetch-mock@^12.5.4"
|
|
42
|
+
};
|
|
43
|
+
var exclude = ["dist", "node_modules"];
|
|
44
|
+
var tasks = {
|
|
45
|
+
"check": "deno fmt --check && deno lint && deno check src/*.ts",
|
|
46
|
+
"test": "deno test"
|
|
47
|
+
};
|
|
48
|
+
var deno_default = {
|
|
49
|
+
name,
|
|
50
|
+
version,
|
|
51
|
+
license,
|
|
52
|
+
exports: exports$1,
|
|
53
|
+
description,
|
|
54
|
+
author,
|
|
55
|
+
imports,
|
|
56
|
+
exclude,
|
|
57
|
+
tasks
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/lookup.ts
|
|
62
|
+
const logger = (0, __logtape_logtape.getLogger)([
|
|
63
|
+
"fedify",
|
|
64
|
+
"webfinger",
|
|
65
|
+
"lookup"
|
|
66
|
+
]);
|
|
67
|
+
const DEFAULT_MAX_REDIRECTION = 5;
|
|
68
|
+
/**
|
|
69
|
+
* Looks up a WebFinger resource.
|
|
70
|
+
* @param resource The resource URL to look up.
|
|
71
|
+
* @param options Extra options for looking up the resource.
|
|
72
|
+
* @returns The resource descriptor, or `null` if not found.
|
|
73
|
+
* @since 0.2.0
|
|
74
|
+
*/
|
|
75
|
+
async function lookupWebFinger(resource, options = {}) {
|
|
76
|
+
const tracerProvider = options.tracerProvider ?? __opentelemetry_api.trace.getTracerProvider();
|
|
77
|
+
const tracer = tracerProvider.getTracer(deno_default.name, deno_default.version);
|
|
78
|
+
return await tracer.startActiveSpan("webfinger.lookup", {
|
|
79
|
+
kind: __opentelemetry_api.SpanKind.CLIENT,
|
|
80
|
+
attributes: {
|
|
81
|
+
"webfinger.resource": resource.toString(),
|
|
82
|
+
"webfinger.resource.scheme": typeof resource === "string" ? resource.replace(/:.*$/, "") : resource.protocol.replace(/:$/, "")
|
|
83
|
+
}
|
|
84
|
+
}, async (span) => {
|
|
85
|
+
try {
|
|
86
|
+
const result = await lookupWebFingerInternal(resource, options);
|
|
87
|
+
span.setStatus({ code: result === null ? __opentelemetry_api.SpanStatusCode.ERROR : __opentelemetry_api.SpanStatusCode.OK });
|
|
88
|
+
return result;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
span.setStatus({
|
|
91
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
|
92
|
+
message: String(error)
|
|
93
|
+
});
|
|
94
|
+
throw error;
|
|
95
|
+
} finally {
|
|
96
|
+
span.end();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async function lookupWebFingerInternal(resource, options = {}) {
|
|
101
|
+
if (typeof resource === "string") resource = new URL(resource);
|
|
102
|
+
let protocol = "https:";
|
|
103
|
+
let server;
|
|
104
|
+
if (resource.protocol === "acct:") {
|
|
105
|
+
const atPos = resource.pathname.lastIndexOf("@");
|
|
106
|
+
if (atPos < 0) return null;
|
|
107
|
+
server = resource.pathname.substring(atPos + 1);
|
|
108
|
+
if (server === "") return null;
|
|
109
|
+
} else {
|
|
110
|
+
protocol = resource.protocol;
|
|
111
|
+
server = resource.host;
|
|
112
|
+
}
|
|
113
|
+
let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
|
|
114
|
+
url.searchParams.set("resource", resource.href);
|
|
115
|
+
let redirected = 0;
|
|
116
|
+
while (true) {
|
|
117
|
+
logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
|
|
118
|
+
let response;
|
|
119
|
+
if (options.allowPrivateAddress !== true) try {
|
|
120
|
+
await (0, __fedify_vocab_runtime.validatePublicUrl)(url.href);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
if (e instanceof __fedify_vocab_runtime.UrlError) {
|
|
123
|
+
logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
throw e;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
response = await fetch(url, {
|
|
130
|
+
headers: {
|
|
131
|
+
Accept: "application/jrd+json",
|
|
132
|
+
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : (0, __fedify_vocab_runtime.getUserAgent)(options.userAgent)
|
|
133
|
+
},
|
|
134
|
+
redirect: "manual",
|
|
135
|
+
signal: options.signal
|
|
136
|
+
});
|
|
137
|
+
} catch (error) {
|
|
138
|
+
logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
|
|
139
|
+
url: url.href,
|
|
140
|
+
error
|
|
141
|
+
});
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
145
|
+
redirected++;
|
|
146
|
+
const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
|
|
147
|
+
if (redirected >= maxRedirection) {
|
|
148
|
+
logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
|
|
152
|
+
if (redirectedUrl.protocol !== url.protocol) {
|
|
153
|
+
logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
|
|
154
|
+
protocol: url.protocol,
|
|
155
|
+
redirectedProtocol: redirectedUrl.protocol
|
|
156
|
+
});
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
url = redirectedUrl;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
|
|
164
|
+
url: url.href,
|
|
165
|
+
status: response.status,
|
|
166
|
+
statusText: response.statusText
|
|
167
|
+
});
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
return await response.json();
|
|
172
|
+
} catch (e) {
|
|
173
|
+
if (e instanceof SyntaxError) {
|
|
174
|
+
logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
throw e;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
//#endregion
|
|
183
|
+
exports.lookupWebFinger = lookupWebFinger;
|
package/dist/mod.d.cts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { GetUserAgentOptions } from "@fedify/vocab-runtime";
|
|
2
|
+
import { TracerProvider } from "@opentelemetry/api";
|
|
3
|
+
|
|
4
|
+
//#region src/jrd.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Describes a resource. See also
|
|
7
|
+
* [RFC 7033 section 4.4](https://datatracker.ietf.org/doc/html/rfc7033#section-4.4).
|
|
8
|
+
*/
|
|
9
|
+
interface ResourceDescriptor {
|
|
10
|
+
/**
|
|
11
|
+
* A URI that identifies the entity that this descriptor describes.
|
|
12
|
+
*/
|
|
13
|
+
subject?: string;
|
|
14
|
+
/**
|
|
15
|
+
* URIs that identify the same entity as the `subject`.
|
|
16
|
+
*/
|
|
17
|
+
aliases?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* Conveys additional information about the `subject` of this descriptor.
|
|
20
|
+
*/
|
|
21
|
+
properties?: Record<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* Links to other resources.
|
|
24
|
+
*/
|
|
25
|
+
links?: Link[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Represents a link. See also
|
|
29
|
+
* [RFC 7033 section 4.4.4](https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4).
|
|
30
|
+
*/
|
|
31
|
+
interface Link {
|
|
32
|
+
/**
|
|
33
|
+
* The link's relation type, which is either a URI or a registered relation
|
|
34
|
+
* type (see [RFC 5988](https://datatracker.ietf.org/doc/html/rfc5988)).
|
|
35
|
+
*/
|
|
36
|
+
rel: string;
|
|
37
|
+
/**
|
|
38
|
+
* The media type of the target resource (see
|
|
39
|
+
* [RFC 6838](https://datatracker.ietf.org/doc/html/rfc6838)).
|
|
40
|
+
*/
|
|
41
|
+
type?: string;
|
|
42
|
+
/**
|
|
43
|
+
* A URI pointing to the target resource.
|
|
44
|
+
*/
|
|
45
|
+
href?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Human-readable titles describing the link relation. If the language is
|
|
48
|
+
* unknown or unspecified, the key is `"und"`.
|
|
49
|
+
*/
|
|
50
|
+
titles?: Record<string, string>;
|
|
51
|
+
/**
|
|
52
|
+
* Conveys additional information about the link relation.
|
|
53
|
+
*/
|
|
54
|
+
properties?: Record<string, string>;
|
|
55
|
+
/**
|
|
56
|
+
* A URI Template (RFC 6570) that can be used to construct URIs by
|
|
57
|
+
* substituting variables. Used primarily for subscription endpoints
|
|
58
|
+
* where parameters like account URIs need to be dynamically inserted.
|
|
59
|
+
* @since 1.9.0
|
|
60
|
+
*/
|
|
61
|
+
template?: string;
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/lookup.d.ts
|
|
65
|
+
/**
|
|
66
|
+
* Options for {@link lookupWebFinger}.
|
|
67
|
+
* @since 1.3.0
|
|
68
|
+
*/
|
|
69
|
+
interface LookupWebFingerOptions {
|
|
70
|
+
/**
|
|
71
|
+
* The options for making `User-Agent` header.
|
|
72
|
+
* If a string is given, it is used as the `User-Agent` header value.
|
|
73
|
+
* If an object is given, it is passed to {@link getUserAgent} to generate
|
|
74
|
+
* the `User-Agent` header value.
|
|
75
|
+
*/
|
|
76
|
+
userAgent?: GetUserAgentOptions | string;
|
|
77
|
+
/**
|
|
78
|
+
* Whether to allow private IP addresses in the URL.
|
|
79
|
+
*
|
|
80
|
+
* Mostly useful for testing purposes. *Do not use this in production.*
|
|
81
|
+
*
|
|
82
|
+
* Turned off by default.
|
|
83
|
+
* @since 1.4.0
|
|
84
|
+
*/
|
|
85
|
+
allowPrivateAddress?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* The maximum number of redirections to follow.
|
|
88
|
+
* @default `5`
|
|
89
|
+
* @since 1.8.0
|
|
90
|
+
*/
|
|
91
|
+
maxRedirection?: number;
|
|
92
|
+
/**
|
|
93
|
+
* The OpenTelemetry tracer provider. If omitted, the global tracer provider
|
|
94
|
+
* is used.
|
|
95
|
+
*/
|
|
96
|
+
tracerProvider?: TracerProvider;
|
|
97
|
+
/**
|
|
98
|
+
* AbortSignal for cancelling the request.
|
|
99
|
+
* @since 1.8.0
|
|
100
|
+
* @
|
|
101
|
+
*/
|
|
102
|
+
signal?: AbortSignal;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Looks up a WebFinger resource.
|
|
106
|
+
* @param resource The resource URL to look up.
|
|
107
|
+
* @param options Extra options for looking up the resource.
|
|
108
|
+
* @returns The resource descriptor, or `null` if not found.
|
|
109
|
+
* @since 0.2.0
|
|
110
|
+
*/
|
|
111
|
+
declare function lookupWebFinger(resource: URL | string, options?: LookupWebFingerOptions): Promise<ResourceDescriptor | null>;
|
|
112
|
+
//#endregion
|
|
113
|
+
export { Link, LookupWebFingerOptions, ResourceDescriptor, lookupWebFinger };
|
package/dist/mod.d.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { GetUserAgentOptions } from "@fedify/vocab-runtime";
|
|
2
|
+
import { TracerProvider } from "@opentelemetry/api";
|
|
3
|
+
|
|
4
|
+
//#region src/jrd.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Describes a resource. See also
|
|
7
|
+
* [RFC 7033 section 4.4](https://datatracker.ietf.org/doc/html/rfc7033#section-4.4).
|
|
8
|
+
*/
|
|
9
|
+
interface ResourceDescriptor {
|
|
10
|
+
/**
|
|
11
|
+
* A URI that identifies the entity that this descriptor describes.
|
|
12
|
+
*/
|
|
13
|
+
subject?: string;
|
|
14
|
+
/**
|
|
15
|
+
* URIs that identify the same entity as the `subject`.
|
|
16
|
+
*/
|
|
17
|
+
aliases?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* Conveys additional information about the `subject` of this descriptor.
|
|
20
|
+
*/
|
|
21
|
+
properties?: Record<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* Links to other resources.
|
|
24
|
+
*/
|
|
25
|
+
links?: Link[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Represents a link. See also
|
|
29
|
+
* [RFC 7033 section 4.4.4](https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4).
|
|
30
|
+
*/
|
|
31
|
+
interface Link {
|
|
32
|
+
/**
|
|
33
|
+
* The link's relation type, which is either a URI or a registered relation
|
|
34
|
+
* type (see [RFC 5988](https://datatracker.ietf.org/doc/html/rfc5988)).
|
|
35
|
+
*/
|
|
36
|
+
rel: string;
|
|
37
|
+
/**
|
|
38
|
+
* The media type of the target resource (see
|
|
39
|
+
* [RFC 6838](https://datatracker.ietf.org/doc/html/rfc6838)).
|
|
40
|
+
*/
|
|
41
|
+
type?: string;
|
|
42
|
+
/**
|
|
43
|
+
* A URI pointing to the target resource.
|
|
44
|
+
*/
|
|
45
|
+
href?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Human-readable titles describing the link relation. If the language is
|
|
48
|
+
* unknown or unspecified, the key is `"und"`.
|
|
49
|
+
*/
|
|
50
|
+
titles?: Record<string, string>;
|
|
51
|
+
/**
|
|
52
|
+
* Conveys additional information about the link relation.
|
|
53
|
+
*/
|
|
54
|
+
properties?: Record<string, string>;
|
|
55
|
+
/**
|
|
56
|
+
* A URI Template (RFC 6570) that can be used to construct URIs by
|
|
57
|
+
* substituting variables. Used primarily for subscription endpoints
|
|
58
|
+
* where parameters like account URIs need to be dynamically inserted.
|
|
59
|
+
* @since 1.9.0
|
|
60
|
+
*/
|
|
61
|
+
template?: string;
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/lookup.d.ts
|
|
65
|
+
/**
|
|
66
|
+
* Options for {@link lookupWebFinger}.
|
|
67
|
+
* @since 1.3.0
|
|
68
|
+
*/
|
|
69
|
+
interface LookupWebFingerOptions {
|
|
70
|
+
/**
|
|
71
|
+
* The options for making `User-Agent` header.
|
|
72
|
+
* If a string is given, it is used as the `User-Agent` header value.
|
|
73
|
+
* If an object is given, it is passed to {@link getUserAgent} to generate
|
|
74
|
+
* the `User-Agent` header value.
|
|
75
|
+
*/
|
|
76
|
+
userAgent?: GetUserAgentOptions | string;
|
|
77
|
+
/**
|
|
78
|
+
* Whether to allow private IP addresses in the URL.
|
|
79
|
+
*
|
|
80
|
+
* Mostly useful for testing purposes. *Do not use this in production.*
|
|
81
|
+
*
|
|
82
|
+
* Turned off by default.
|
|
83
|
+
* @since 1.4.0
|
|
84
|
+
*/
|
|
85
|
+
allowPrivateAddress?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* The maximum number of redirections to follow.
|
|
88
|
+
* @default `5`
|
|
89
|
+
* @since 1.8.0
|
|
90
|
+
*/
|
|
91
|
+
maxRedirection?: number;
|
|
92
|
+
/**
|
|
93
|
+
* The OpenTelemetry tracer provider. If omitted, the global tracer provider
|
|
94
|
+
* is used.
|
|
95
|
+
*/
|
|
96
|
+
tracerProvider?: TracerProvider;
|
|
97
|
+
/**
|
|
98
|
+
* AbortSignal for cancelling the request.
|
|
99
|
+
* @since 1.8.0
|
|
100
|
+
* @
|
|
101
|
+
*/
|
|
102
|
+
signal?: AbortSignal;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Looks up a WebFinger resource.
|
|
106
|
+
* @param resource The resource URL to look up.
|
|
107
|
+
* @param options Extra options for looking up the resource.
|
|
108
|
+
* @returns The resource descriptor, or `null` if not found.
|
|
109
|
+
* @since 0.2.0
|
|
110
|
+
*/
|
|
111
|
+
declare function lookupWebFinger(resource: URL | string, options?: LookupWebFingerOptions): Promise<ResourceDescriptor | null>;
|
|
112
|
+
//#endregion
|
|
113
|
+
export { Link, LookupWebFingerOptions, ResourceDescriptor, lookupWebFinger };
|
package/dist/mod.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { UrlError, getUserAgent, validatePublicUrl } from "@fedify/vocab-runtime";
|
|
2
|
+
import { getLogger } from "@logtape/logtape";
|
|
3
|
+
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
4
|
+
|
|
5
|
+
//#region deno.json
|
|
6
|
+
var name = "@fedify/webfinger";
|
|
7
|
+
var version = "2.0.0";
|
|
8
|
+
var license = "MIT";
|
|
9
|
+
var exports = { ".": "./src/mod.ts" };
|
|
10
|
+
var description = "WebFinger client library for Fedify";
|
|
11
|
+
var author = {
|
|
12
|
+
"name": "Hong Minhee",
|
|
13
|
+
"email": "hong@minhee.org",
|
|
14
|
+
"url": "https://hongminhee.org/"
|
|
15
|
+
};
|
|
16
|
+
var imports = {
|
|
17
|
+
"es-toolkit": "npm:es-toolkit@^1.42.0",
|
|
18
|
+
"fetch-mock": "npm:fetch-mock@^12.5.4"
|
|
19
|
+
};
|
|
20
|
+
var exclude = ["dist", "node_modules"];
|
|
21
|
+
var tasks = {
|
|
22
|
+
"check": "deno fmt --check && deno lint && deno check src/*.ts",
|
|
23
|
+
"test": "deno test"
|
|
24
|
+
};
|
|
25
|
+
var deno_default = {
|
|
26
|
+
name,
|
|
27
|
+
version,
|
|
28
|
+
license,
|
|
29
|
+
exports,
|
|
30
|
+
description,
|
|
31
|
+
author,
|
|
32
|
+
imports,
|
|
33
|
+
exclude,
|
|
34
|
+
tasks
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/lookup.ts
|
|
39
|
+
const logger = getLogger([
|
|
40
|
+
"fedify",
|
|
41
|
+
"webfinger",
|
|
42
|
+
"lookup"
|
|
43
|
+
]);
|
|
44
|
+
const DEFAULT_MAX_REDIRECTION = 5;
|
|
45
|
+
/**
|
|
46
|
+
* Looks up a WebFinger resource.
|
|
47
|
+
* @param resource The resource URL to look up.
|
|
48
|
+
* @param options Extra options for looking up the resource.
|
|
49
|
+
* @returns The resource descriptor, or `null` if not found.
|
|
50
|
+
* @since 0.2.0
|
|
51
|
+
*/
|
|
52
|
+
async function lookupWebFinger(resource, options = {}) {
|
|
53
|
+
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
|
|
54
|
+
const tracer = tracerProvider.getTracer(deno_default.name, deno_default.version);
|
|
55
|
+
return await tracer.startActiveSpan("webfinger.lookup", {
|
|
56
|
+
kind: SpanKind.CLIENT,
|
|
57
|
+
attributes: {
|
|
58
|
+
"webfinger.resource": resource.toString(),
|
|
59
|
+
"webfinger.resource.scheme": typeof resource === "string" ? resource.replace(/:.*$/, "") : resource.protocol.replace(/:$/, "")
|
|
60
|
+
}
|
|
61
|
+
}, async (span) => {
|
|
62
|
+
try {
|
|
63
|
+
const result = await lookupWebFingerInternal(resource, options);
|
|
64
|
+
span.setStatus({ code: result === null ? SpanStatusCode.ERROR : SpanStatusCode.OK });
|
|
65
|
+
return result;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
span.setStatus({
|
|
68
|
+
code: SpanStatusCode.ERROR,
|
|
69
|
+
message: String(error)
|
|
70
|
+
});
|
|
71
|
+
throw error;
|
|
72
|
+
} finally {
|
|
73
|
+
span.end();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async function lookupWebFingerInternal(resource, options = {}) {
|
|
78
|
+
if (typeof resource === "string") resource = new URL(resource);
|
|
79
|
+
let protocol = "https:";
|
|
80
|
+
let server;
|
|
81
|
+
if (resource.protocol === "acct:") {
|
|
82
|
+
const atPos = resource.pathname.lastIndexOf("@");
|
|
83
|
+
if (atPos < 0) return null;
|
|
84
|
+
server = resource.pathname.substring(atPos + 1);
|
|
85
|
+
if (server === "") return null;
|
|
86
|
+
} else {
|
|
87
|
+
protocol = resource.protocol;
|
|
88
|
+
server = resource.host;
|
|
89
|
+
}
|
|
90
|
+
let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
|
|
91
|
+
url.searchParams.set("resource", resource.href);
|
|
92
|
+
let redirected = 0;
|
|
93
|
+
while (true) {
|
|
94
|
+
logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
|
|
95
|
+
let response;
|
|
96
|
+
if (options.allowPrivateAddress !== true) try {
|
|
97
|
+
await validatePublicUrl(url.href);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
if (e instanceof UrlError) {
|
|
100
|
+
logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
throw e;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
response = await fetch(url, {
|
|
107
|
+
headers: {
|
|
108
|
+
Accept: "application/jrd+json",
|
|
109
|
+
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
110
|
+
},
|
|
111
|
+
redirect: "manual",
|
|
112
|
+
signal: options.signal
|
|
113
|
+
});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
|
|
116
|
+
url: url.href,
|
|
117
|
+
error
|
|
118
|
+
});
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
122
|
+
redirected++;
|
|
123
|
+
const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
|
|
124
|
+
if (redirected >= maxRedirection) {
|
|
125
|
+
logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
|
|
129
|
+
if (redirectedUrl.protocol !== url.protocol) {
|
|
130
|
+
logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
|
|
131
|
+
protocol: url.protocol,
|
|
132
|
+
redirectedProtocol: redirectedUrl.protocol
|
|
133
|
+
});
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
url = redirectedUrl;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
|
|
141
|
+
url: url.href,
|
|
142
|
+
status: response.status,
|
|
143
|
+
statusText: response.statusText
|
|
144
|
+
});
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
return await response.json();
|
|
149
|
+
} catch (e) {
|
|
150
|
+
if (e instanceof SyntaxError) {
|
|
151
|
+
logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
throw e;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
//#endregion
|
|
160
|
+
export { lookupWebFinger };
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fedify/webfinger",
|
|
3
|
+
"version": "2.0.0-dev.0",
|
|
4
|
+
"homepage": "https://fedify.dev/",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/fedify-dev/fedify.git",
|
|
8
|
+
"directory": "packages/webfinger"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/fedify-dev/fedify/issues"
|
|
12
|
+
},
|
|
13
|
+
"funding": [
|
|
14
|
+
"https://opencollective.com/fedify",
|
|
15
|
+
"https://github.com/sponsors/dahlia"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"deno": ">=2.0.0",
|
|
19
|
+
"node": ">=22.0.0",
|
|
20
|
+
"bun": ">=1.1.0"
|
|
21
|
+
},
|
|
22
|
+
"description": "WebFinger client library for ActivityPub",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./dist/mod.cjs",
|
|
25
|
+
"module": "./dist/mod.js",
|
|
26
|
+
"types": "./dist/mod.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": {
|
|
30
|
+
"import": "./dist/mod.d.ts",
|
|
31
|
+
"require": "./dist/mod.d.cts",
|
|
32
|
+
"default": "./dist/mod.d.ts"
|
|
33
|
+
},
|
|
34
|
+
"import": "./dist/mod.js",
|
|
35
|
+
"require": "./dist/mod.cjs",
|
|
36
|
+
"default": "./dist/mod.js"
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"Fedify",
|
|
42
|
+
"WebFinger",
|
|
43
|
+
"ActivityPub",
|
|
44
|
+
"Fediverse"
|
|
45
|
+
],
|
|
46
|
+
"author": {
|
|
47
|
+
"name": "Hong Minhee",
|
|
48
|
+
"email": "hong@minhee.org",
|
|
49
|
+
"url": "https://hongminhee.org/"
|
|
50
|
+
},
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^24.2.1",
|
|
54
|
+
"fetch-mock": "^12.5.4",
|
|
55
|
+
"tsdown": "^0.12.9",
|
|
56
|
+
"typescript": "^5.9.3",
|
|
57
|
+
"@fedify/fixture": "2.0.0"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@logtape/logtape": "^1.3.5",
|
|
61
|
+
"@opentelemetry/api": "^1.9.0",
|
|
62
|
+
"es-toolkit": "1.43.0",
|
|
63
|
+
"@fedify/vocab-runtime": "2.0.0"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "tsdown",
|
|
67
|
+
"prepublish": "tsdown",
|
|
68
|
+
"test": "tsdown && cd dist/ && node --test"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/jrd.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Describes a resource. See also
|
|
3
|
+
* [RFC 7033 section 4.4](https://datatracker.ietf.org/doc/html/rfc7033#section-4.4).
|
|
4
|
+
*/
|
|
5
|
+
export interface ResourceDescriptor {
|
|
6
|
+
/**
|
|
7
|
+
* A URI that identifies the entity that this descriptor describes.
|
|
8
|
+
*/
|
|
9
|
+
subject?: string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* URIs that identify the same entity as the `subject`.
|
|
13
|
+
*/
|
|
14
|
+
aliases?: string[];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Conveys additional information about the `subject` of this descriptor.
|
|
18
|
+
*/
|
|
19
|
+
properties?: Record<string, string>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Links to other resources.
|
|
23
|
+
*/
|
|
24
|
+
links?: Link[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Represents a link. See also
|
|
29
|
+
* [RFC 7033 section 4.4.4](https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4).
|
|
30
|
+
*/
|
|
31
|
+
export interface Link {
|
|
32
|
+
/**
|
|
33
|
+
* The link's relation type, which is either a URI or a registered relation
|
|
34
|
+
* type (see [RFC 5988](https://datatracker.ietf.org/doc/html/rfc5988)).
|
|
35
|
+
*/
|
|
36
|
+
rel: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The media type of the target resource (see
|
|
40
|
+
* [RFC 6838](https://datatracker.ietf.org/doc/html/rfc6838)).
|
|
41
|
+
*/
|
|
42
|
+
type?: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A URI pointing to the target resource.
|
|
46
|
+
*/
|
|
47
|
+
href?: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Human-readable titles describing the link relation. If the language is
|
|
51
|
+
* unknown or unspecified, the key is `"und"`.
|
|
52
|
+
*/
|
|
53
|
+
titles?: Record<string, string>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Conveys additional information about the link relation.
|
|
57
|
+
*/
|
|
58
|
+
properties?: Record<string, string>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A URI Template (RFC 6570) that can be used to construct URIs by
|
|
62
|
+
* substituting variables. Used primarily for subscription endpoints
|
|
63
|
+
* where parameters like account URIs need to be dynamically inserted.
|
|
64
|
+
* @since 1.9.0
|
|
65
|
+
*/
|
|
66
|
+
template?: string;
|
|
67
|
+
}
|