@fedify/fedify 1.3.0-dev.484 → 1.3.0-dev.487

Sign up to get free protection for your applications and to get access to all the features.
package/CHANGES.md CHANGED
@@ -8,9 +8,38 @@ Version 1.3.0
8
8
 
9
9
  To be released.
10
10
 
11
- - Fedify now makes HTTP requests with the proper `User-Agent` header.
11
+ - Fedify now makes HTTP requests with the proper `User-Agent` header. [[#162]]
12
12
 
13
13
  - Added `getUserAgent()` function.
14
+ - Added `GetUserAgentOptions` interface.
15
+ - Added `getDocumentLoader()` function.
16
+ - Added `GetDocumentLoaderOptions` interface.
17
+ - The type of `getAuthenticatedDocumentLoader()` function's second
18
+ parameter became `GetAuthenticatedDocumentLoaderOptions | undefined`
19
+ (was `boolean | undefined`).
20
+ - Added `GetAuthenticatedDocumentLoaderOptions` interface.
21
+ - Deprecated `fetchDocumentLoader()` function.
22
+ - Added `LookupObjectOptions.userAgent` option.
23
+ - Added the type of `getActorHandle()` function's second parameter became
24
+ `GetActorHandleOptions | undefined` (was `NormalizeActorHandleOptions |
25
+ undefined`).
26
+ - Added `GetActorHandleOptions` interface.
27
+ - Added the optional second parameter to `lookupWebFinger()` function.
28
+ - Added `LookupWebFingerOptions` interface.
29
+ - Added `GetNodeInfoOptions.userAgent` option.
30
+ - Added `-u`/--user-agent` option to `fedify lookup` subcommand.
31
+ - Added `-u`/--user-agent` option to `fedify node` subcommand.
32
+
33
+ [#162]: https://github.com/dahlia/fedify/issues/162
34
+
35
+
36
+ Version 1.2.3
37
+ -------------
38
+
39
+ Released on November 6, 2024.
40
+
41
+ - The `fedify node` subcommand now can recognize multiple values of
42
+ the `rel` attribute in the `<link>` HTML elements.
14
43
 
15
44
 
16
45
  Version 1.2.2
package/esm/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "@fedify/fedify",
3
- "version": "1.3.0-dev.484+e6a416f0",
3
+ "version": "1.3.0-dev.487+599b5a7b",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./mod.ts",
@@ -1,7 +1,7 @@
1
1
  import * as dntShim from "../_dnt.shims.js";
2
2
  import { getLogger, withContext } from "@logtape/logtape";
3
3
  import { handleNodeInfo, handleNodeInfoJrd } from "../nodeinfo/handler.js";
4
- import { fetchDocumentLoader, getAuthenticatedDocumentLoader, kvCache, } from "../runtime/docloader.js";
4
+ import { getAuthenticatedDocumentLoader, getDocumentLoader, kvCache, } from "../runtime/docloader.js";
5
5
  import { verifyRequest } from "../sig/http.js";
6
6
  import { exportJwk, importJwk, validateCryptoKey } from "../sig/key.js";
7
7
  import { hasSignature, signJsonLd } from "../sig/ld.js";
@@ -50,6 +50,7 @@ export class FederationImpl {
50
50
  documentLoader;
51
51
  contextLoader;
52
52
  authenticatedDocumentLoaderFactory;
53
+ userAgent;
53
54
  onOutboxError;
54
55
  signatureTimeWindow;
55
56
  skipSignatureVerification;
@@ -75,31 +76,34 @@ export class FederationImpl {
75
76
  this.router.add("/.well-known/nodeinfo", "nodeInfoJrd");
76
77
  this.objectCallbacks = {};
77
78
  this.objectTypeIds = {};
78
- if (options.allowPrivateAddress) {
79
+ if (options.allowPrivateAddress || options.userAgent != null) {
79
80
  if (options.documentLoader != null) {
80
- throw new TypeError("Cannot set documentLoader with allowPrivateAddress turned on.");
81
+ throw new TypeError("Cannot set documentLoader with allowPrivateAddress or " +
82
+ "userAgent options.");
81
83
  }
82
84
  else if (options.contextLoader != null) {
83
- throw new TypeError("Cannot set contextLoader with allowPrivateAddress turned on.");
85
+ throw new TypeError("Cannot set contextLoader with allowPrivateAddress or " +
86
+ "userAgent options.");
84
87
  }
85
88
  else if (options.authenticatedDocumentLoaderFactory != null) {
86
89
  throw new TypeError("Cannot set authenticatedDocumentLoaderFactory with " +
87
- "allowPrivateAddress turned on.");
90
+ "allowPrivateAddress or userAgent options.");
88
91
  }
89
92
  }
93
+ const { allowPrivateAddress, userAgent } = options;
90
94
  this.documentLoader = options.documentLoader ?? kvCache({
91
- loader: options.allowPrivateAddress
92
- ? (url) => fetchDocumentLoader(url, true)
93
- : fetchDocumentLoader,
95
+ loader: getDocumentLoader({ allowPrivateAddress, userAgent }),
94
96
  kv: options.kv,
95
97
  prefix: this.kvPrefixes.remoteDocument,
96
98
  });
97
99
  this.contextLoader = options.contextLoader ?? this.documentLoader;
98
100
  this.authenticatedDocumentLoaderFactory =
99
101
  options.authenticatedDocumentLoaderFactory ??
100
- (options.allowPrivateAddress
101
- ? (identity) => getAuthenticatedDocumentLoader(identity, true)
102
- : getAuthenticatedDocumentLoader);
102
+ ((identity) => getAuthenticatedDocumentLoader(identity, {
103
+ allowPrivateAddress,
104
+ userAgent,
105
+ }));
106
+ this.userAgent = userAgent;
103
107
  this.onOutboxError = options.onOutboxError;
104
108
  this.signatureTimeWindow = options.signatureTimeWindow ?? { hours: 1 };
105
109
  this.skipSignatureVerification = options.skipSignatureVerification ?? false;
@@ -1554,6 +1558,7 @@ export class ContextImpl {
1554
1558
  return lookupObject(identifier, {
1555
1559
  documentLoader: options.documentLoader ?? this.documentLoader,
1556
1560
  contextLoader: options.contextLoader ?? this.contextLoader,
1561
+ userAgent: options.userAgent ?? this.federation.userAgent,
1557
1562
  });
1558
1563
  }
1559
1564
  traverseCollection(collection, options = {}) {
@@ -1,6 +1,6 @@
1
1
  import { getLogger } from "@logtape/logtape";
2
2
  import { parse } from "../deps/jsr.io/@std/semver/1.0.3/mod.js";
3
- import { getUserAgent } from "../runtime/docloader.js";
3
+ import { getUserAgent, } from "../runtime/docloader.js";
4
4
  const logger = getLogger(["fedify", "nodeinfo", "client"]);
5
5
  export async function getNodeInfo(url, options = {}) {
6
6
  try {
@@ -10,7 +10,9 @@ export async function getNodeInfo(url, options = {}) {
10
10
  const wellKnownResponse = await fetch(wellKnownUrl, {
11
11
  headers: {
12
12
  Accept: "application/json",
13
- "User-Agent": getUserAgent(),
13
+ "User-Agent": typeof options.userAgent === "string"
14
+ ? options.userAgent
15
+ : getUserAgent(options.userAgent),
14
16
  },
15
17
  });
16
18
  if (!wellKnownResponse.ok) {
@@ -37,7 +39,9 @@ export async function getNodeInfo(url, options = {}) {
37
39
  const response = await fetch(nodeInfoUrl, {
38
40
  headers: {
39
41
  Accept: "application/json",
40
- "User-Agent": getUserAgent(),
42
+ "User-Agent": typeof options.userAgent === "string"
43
+ ? options.userAgent
44
+ : getUserAgent(options.userAgent),
41
45
  },
42
46
  });
43
47
  if (!response.ok) {
@@ -28,11 +28,13 @@ export class FetchError extends Error {
28
28
  this.url = typeof url === "string" ? new URL(url) : url;
29
29
  }
30
30
  }
31
- function createRequest(url) {
31
+ function createRequest(url, options = {}) {
32
32
  return new Request(url, {
33
33
  headers: {
34
34
  Accept: "application/activity+json, application/ld+json",
35
- "User-Agent": getUserAgent(),
35
+ "User-Agent": typeof options.userAgent === "string"
36
+ ? options.userAgent
37
+ : getUserAgent(options.userAgent),
36
38
  },
37
39
  redirect: "manual",
38
40
  });
@@ -128,6 +130,65 @@ async function getRemoteDocument(url, response, fetch) {
128
130
  documentUrl,
129
131
  };
130
132
  }
133
+ /**
134
+ * Creates a JSON-LD document loader that utilizes the browser's `fetch` API.
135
+ *
136
+ * The created loader preloads the below frequently used contexts by default
137
+ * (unless `options.ignorePreloadedContexts` is set to `true`):
138
+ *
139
+ * - <https://www.w3.org/ns/activitystreams>
140
+ * - <https://w3id.org/security/v1>
141
+ * - <https://w3id.org/security/data-integrity/v1>
142
+ * - <https://www.w3.org/ns/did/v1>
143
+ * - <https://w3id.org/security/multikey/v1>
144
+ * - <https://purl.archive.org/socialweb/webfinger>
145
+ * - <http://schema.org/>
146
+ * @param options Options for the document loader.
147
+ * @returns The document loader.
148
+ * @since 1.3.0
149
+ */
150
+ export function getDocumentLoader({ allowPrivateAddress, skipPreloadedContexts, userAgent } = {}) {
151
+ async function load(url) {
152
+ if (!skipPreloadedContexts && url in preloadedContexts) {
153
+ logger.debug("Using preloaded context: {url}.", { url });
154
+ return {
155
+ contextUrl: null,
156
+ document: preloadedContexts[url],
157
+ documentUrl: url,
158
+ };
159
+ }
160
+ if (!allowPrivateAddress) {
161
+ try {
162
+ await validatePublicUrl(url);
163
+ }
164
+ catch (error) {
165
+ if (error instanceof UrlError) {
166
+ logger.error("Disallowed private URL: {url}", { url, error });
167
+ }
168
+ throw error;
169
+ }
170
+ }
171
+ const request = createRequest(url, { userAgent });
172
+ logRequest(request);
173
+ const response = await fetch(request, {
174
+ // Since Bun has a bug that ignores the `Request.redirect` option,
175
+ // to work around it we specify `redirect: "manual"` here too:
176
+ // https://github.com/oven-sh/bun/issues/10754
177
+ redirect: "manual",
178
+ });
179
+ // Follow redirects manually to get the final URL:
180
+ if (response.status >= 300 && response.status < 400 &&
181
+ response.headers.has("Location")) {
182
+ return load(response.headers.get("Location"));
183
+ }
184
+ return getRemoteDocument(url, response, load);
185
+ }
186
+ return load;
187
+ }
188
+ const _fetchDocumentLoader = getDocumentLoader();
189
+ const _fetchDocumentLoader_allowPrivateAddress = getDocumentLoader({
190
+ allowPrivateAddress: true,
191
+ });
131
192
  /**
132
193
  * A JSON-LD document loader that utilizes the browser's `fetch` API.
133
194
  *
@@ -138,45 +199,20 @@ async function getRemoteDocument(url, response, fetch) {
138
199
  * - <https://w3id.org/security/data-integrity/v1>
139
200
  * - <https://www.w3.org/ns/did/v1>
140
201
  * - <https://w3id.org/security/multikey/v1>
202
+ * - <https://purl.archive.org/socialweb/webfinger>
203
+ * - <http://schema.org/>
141
204
  * @param url The URL of the document to load.
142
205
  * @param allowPrivateAddress Whether to allow fetching private network
143
206
  * addresses. Turned off by default.
144
207
  * @returns The remote document.
208
+ * @deprecated Use {@link getDocumentLoader} instead.
145
209
  */
146
- export async function fetchDocumentLoader(url, allowPrivateAddress = false) {
147
- if (url in preloadedContexts) {
148
- logger.debug("Using preloaded context: {url}.", { url });
149
- return {
150
- contextUrl: null,
151
- document: preloadedContexts[url],
152
- documentUrl: url,
153
- };
154
- }
155
- if (!allowPrivateAddress) {
156
- try {
157
- await validatePublicUrl(url);
158
- }
159
- catch (error) {
160
- if (error instanceof UrlError) {
161
- logger.error("Disallowed private URL: {url}", { url, error });
162
- }
163
- throw error;
164
- }
165
- }
166
- const request = createRequest(url);
167
- logRequest(request);
168
- const response = await fetch(request, {
169
- // Since Bun has a bug that ignores the `Request.redirect` option,
170
- // to work around it we specify `redirect: "manual"` here too:
171
- // https://github.com/oven-sh/bun/issues/10754
172
- redirect: "manual",
173
- });
174
- // Follow redirects manually to get the final URL:
175
- if (response.status >= 300 && response.status < 400 &&
176
- response.headers.has("Location")) {
177
- return fetchDocumentLoader(response.headers.get("Location"), allowPrivateAddress);
178
- }
179
- return getRemoteDocument(url, response, (url) => fetchDocumentLoader(url, allowPrivateAddress));
210
+ export function fetchDocumentLoader(url, allowPrivateAddress = false) {
211
+ logger.warn("fetchDocumentLoader() function is deprecated. " +
212
+ "Use getDocumentLoader() function instead.");
213
+ return (allowPrivateAddress
214
+ ? _fetchDocumentLoader_allowPrivateAddress
215
+ : _fetchDocumentLoader)(url);
180
216
  }
181
217
  /**
182
218
  * Gets an authenticated {@link DocumentLoader} for the given identity.
@@ -184,13 +220,12 @@ export async function fetchDocumentLoader(url, allowPrivateAddress = false) {
184
220
  * the fetched documents.
185
221
  * @param identity The identity to get the document loader for.
186
222
  * The actor's key pair.
187
- * @param allowPrivateAddress Whether to allow fetching private network
188
- * addresses. Turned off by default.
223
+ * @param options The options for the document loader.
189
224
  * @returns The authenticated document loader.
190
225
  * @throws {TypeError} If the key is invalid or unsupported.
191
226
  * @since 0.4.0
192
227
  */
193
- export function getAuthenticatedDocumentLoader(identity, allowPrivateAddress = false) {
228
+ export function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAgent } = {}) {
194
229
  validateCryptoKey(identity.privateKey);
195
230
  async function load(url) {
196
231
  if (!allowPrivateAddress) {
@@ -204,7 +239,7 @@ export function getAuthenticatedDocumentLoader(identity, allowPrivateAddress = f
204
239
  throw error;
205
240
  }
206
241
  }
207
- let request = createRequest(url);
242
+ let request = createRequest(url, { userAgent });
208
243
  request = await signRequest(request, identity.privateKey, identity.keyId);
209
244
  logRequest(request);
210
245
  const response = await fetch(request, {
@@ -273,13 +308,11 @@ export function kvCache({ loader, kv, prefix, rules }) {
273
308
  }
274
309
  /**
275
310
  * Gets the user agent string for the given application and URL.
276
- * @param app An optional application name and version, e.g., `"Hollo/1.0.0"`.
277
- * @param url An optional URL to append to the user agent string.
278
- * Usually the URL of the ActivityPub instance.
311
+ * @param options The options for making the user agent string.
279
312
  * @returns The user agent string.
280
313
  * @since 1.3.0
281
314
  */
282
- export function getUserAgent(app, url) {
315
+ export function getUserAgent({ software, url } = {}) {
283
316
  const fedify = `Fedify/${metadata.version}`;
284
317
  const runtime = "Deno" in dntShim.dntGlobalThis
285
318
  ? `Deno/${dntShim.Deno.version.deno}`
@@ -289,7 +322,7 @@ export function getUserAgent(app, url) {
289
322
  : "process" in dntShim.dntGlobalThis
290
323
  ? `Node.js/${process.version}`
291
324
  : null;
292
- const userAgent = app == null ? [fedify] : [app, fedify];
325
+ const userAgent = software == null ? [fedify] : [software, fedify];
293
326
  if (runtime != null)
294
327
  userAgent.push(runtime);
295
328
  if (url != null)
package/esm/sig/key.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as dntShim from "../_dnt.shims.js";
2
2
  import { getLogger } from "@logtape/logtape";
3
- import { fetchDocumentLoader, } from "../runtime/docloader.js";
3
+ import { getDocumentLoader, } from "../runtime/docloader.js";
4
4
  import { isActor } from "../vocab/actor.js";
5
5
  import { CryptographicKey, Object } from "../vocab/vocab.js";
6
6
  /**
@@ -123,7 +123,7 @@ cls, { documentLoader, contextLoader, keyCache } = {}) {
123
123
  logger.debug("Fetching key {keyId} to verify signature...", { keyId });
124
124
  let document;
125
125
  try {
126
- const remoteDocument = await (documentLoader ?? fetchDocumentLoader)(keyId);
126
+ const remoteDocument = await (documentLoader ?? getDocumentLoader())(keyId);
127
127
  document = remoteDocument.document;
128
128
  }
129
129
  catch (_) {
package/esm/sig/ld.js CHANGED
@@ -4,7 +4,7 @@ import { decodeBase64, encodeBase64 } from "../deps/jsr.io/@std/encoding/1.0.5/b
4
4
  import { encodeHex } from "../deps/jsr.io/@std/encoding/1.0.5/hex.js";
5
5
  // @ts-ignore TS7016
6
6
  import jsonld from "jsonld";
7
- import { fetchDocumentLoader, } from "../runtime/docloader.js";
7
+ import { getDocumentLoader, } from "../runtime/docloader.js";
8
8
  import { Activity, CryptographicKey, Object } from "../vocab/vocab.js";
9
9
  import { fetchKey, validateCryptoKey } from "./key.js";
10
10
  const logger = getLogger(["fedify", "sig", "ld"]);
@@ -200,7 +200,7 @@ export async function verifyJsonLd(jsonLd, options = {}) {
200
200
  async function hashJsonLd(jsonLd, contextLoader) {
201
201
  const canon = await jsonld.canonize(jsonLd, {
202
202
  format: "application/n-quads",
203
- documentLoader: contextLoader ?? fetchDocumentLoader,
203
+ documentLoader: contextLoader ?? getDocumentLoader(),
204
204
  });
205
205
  const encoder = new TextEncoder();
206
206
  const hash = await dntShim.crypto.subtle.digest("SHA-256", encoder.encode(canon));
package/esm/sig/owner.js CHANGED
@@ -1,4 +1,4 @@
1
- import { fetchDocumentLoader, } from "../runtime/docloader.js";
1
+ import { getDocumentLoader, } from "../runtime/docloader.js";
2
2
  import { isActor } from "../vocab/actor.js";
3
3
  import { CryptographicKey, Object as ASObject, } from "../vocab/vocab.js";
4
4
  export { exportJwk, generateCryptoKeyPair, importJwk } from "./key.js";
@@ -32,8 +32,8 @@ export async function doesActorOwnKey(activity, key, options) {
32
32
  * owner.
33
33
  */
34
34
  export async function getKeyOwner(keyId, options) {
35
- const documentLoader = options.documentLoader ?? fetchDocumentLoader;
36
- const contextLoader = options.contextLoader ?? fetchDocumentLoader;
35
+ const documentLoader = options.documentLoader ?? getDocumentLoader();
36
+ const contextLoader = options.contextLoader ?? getDocumentLoader();
37
37
  let object;
38
38
  if (keyId instanceof CryptographicKey) {
39
39
  object = keyId;
@@ -67,7 +67,7 @@ export function getActorClassByTypeName(typeName) {
67
67
  * ```
68
68
  *
69
69
  * @param actor The actor or actor URI to get the handle from.
70
- * @param options The options for normalizing the actor handle.
70
+ * @param options The extra options for getting the actor handle.
71
71
  * @returns The actor handle. It starts with `@` and is followed by the
72
72
  * username and domain, separated by `@` by default (it can be
73
73
  * customized with the options).
@@ -78,7 +78,9 @@ export function getActorClassByTypeName(typeName) {
78
78
  export async function getActorHandle(actor, options = {}) {
79
79
  const actorId = actor instanceof URL ? actor : actor.id;
80
80
  if (actorId != null) {
81
- const result = await lookupWebFinger(actorId);
81
+ const result = await lookupWebFinger(actorId, {
82
+ userAgent: options.userAgent,
83
+ });
82
84
  if (result != null) {
83
85
  const aliases = [...(result.aliases ?? [])];
84
86
  if (result.subject != null)
@@ -88,7 +90,7 @@ export async function getActorHandle(actor, options = {}) {
88
90
  if (match != null) {
89
91
  const hostname = new URL(`https://${match[2]}/`).hostname;
90
92
  if (hostname !== actorId.hostname &&
91
- !await verifyCrossOriginActorHandle(actorId.href, alias)) {
93
+ !await verifyCrossOriginActorHandle(actorId.href, alias, options.userAgent)) {
92
94
  continue;
93
95
  }
94
96
  return normalizeActorHandle(`@${match[1]}@${match[2]}`, options);
@@ -102,8 +104,8 @@ export async function getActorHandle(actor, options = {}) {
102
104
  }
103
105
  throw new TypeError("Actor does not have enough information to get the handle.");
104
106
  }
105
- async function verifyCrossOriginActorHandle(actorId, alias) {
106
- const response = await lookupWebFinger(alias);
107
+ async function verifyCrossOriginActorHandle(actorId, alias, userAgent) {
108
+ const response = await lookupWebFinger(alias, { userAgent });
107
109
  if (response == null)
108
110
  return false;
109
111
  for (const alias of response.aliases ?? []) {
@@ -1,7 +1,7 @@
1
1
  import * as dntShim from "../_dnt.shims.js";
2
2
  import { getLogger } from "@logtape/logtape";
3
3
  import { delay } from "../deps/jsr.io/@std/async/1.0.8/delay.js";
4
- import { fetchDocumentLoader, } from "../runtime/docloader.js";
4
+ import { getDocumentLoader, } from "../runtime/docloader.js";
5
5
  import { lookupWebFinger } from "../webfinger/lookup.js";
6
6
  import { Object } from "./vocab.js";
7
7
  const logger = getLogger(["fedify", "vocab", "lookup"]);
@@ -39,7 +39,8 @@ const handleRegexp = /^@?((?:[-A-Za-z0-9._~!$&'()*+,;=]|%[A-Fa-f0-9]{2})+)@([^@]
39
39
  * @since 0.2.0
40
40
  */
41
41
  export async function lookupObject(identifier, options = {}) {
42
- const documentLoader = options.documentLoader ?? fetchDocumentLoader;
42
+ const documentLoader = options.documentLoader ??
43
+ getDocumentLoader({ userAgent: options.userAgent });
43
44
  if (typeof identifier === "string") {
44
45
  const match = handleRegexp.exec(identifier);
45
46
  if (match)
@@ -57,7 +58,9 @@ export async function lookupObject(identifier, options = {}) {
57
58
  }
58
59
  }
59
60
  if (document == null) {
60
- const jrd = await lookupWebFinger(identifier);
61
+ const jrd = await lookupWebFinger(identifier, {
62
+ userAgent: options.userAgent,
63
+ });
61
64
  if (jrd?.links == null)
62
65
  return null;
63
66
  for (const l of jrd.links) {