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

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/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) {