@fedify/fedify 0.8.0-dev.164 → 0.8.0-dev.167

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of @fedify/fedify might be problematic. Click here for more details.

package/CHANGES.md CHANGED
@@ -10,7 +10,13 @@ To be released.
10
10
 
11
11
  - The CLI toolchain for testing and debugging is now available on JSR:
12
12
  [@fedify/cli]. You can install it with
13
- `deno install -A -n fedify jsr:@fedify/cli`.
13
+ `deno install -A --unstable-fs --unstable-kv --unstable-temporal -n fedify
14
+ jsr:@fedify/cli`, or download a standalone exectuable from the [releases]
15
+ page.
16
+
17
+ - Added `fedify` command.
18
+ - Added `fedify lookup` subcommand.
19
+ - Added `fedify inbox` subcommand.
14
20
 
15
21
  - Implemented [followers collection synchronization mechanism][FEP-8fcf].
16
22
 
@@ -40,6 +46,26 @@ To be released.
40
46
  parameter became `CollectionDispatcher<Recipient, TContextData, URL>`
41
47
  (was `CollectionDispatcher<Actor | URL, TContextData>`).
42
48
 
49
+ - Some of the responsibility of a document loader was separated to a context
50
+ loader and a document loader.
51
+
52
+ - Added `contextLoader` option to constructors, `fromJsonLd()` static
53
+ methods, `clone()` methods, and all non-scalar accessors (`get*()`) of
54
+ Activity Vocabulary classes.
55
+ - Renamed `documentLoader` option to `contextLoader` in `toJsonLd()`
56
+ methods of Activity Vocabulary objects.
57
+ - Added `contextLoader` option to `LookupObjectOptions` interface.
58
+ - Added `contextLoader` property to `Context` interface.
59
+ - Added `contextLoader` option to `FederationParameters` interface.
60
+ - Renamed `documentLoader` option to `contextLoader` in
61
+ `RespondWithObjectOptions` interface.
62
+ - Added `GetKeyOwnerOptions` interface.
63
+ - The type of the second parameter of `getKeyOwner()` function became
64
+ `GetKeyOwnerOptions` (was `DocumentLoader`).
65
+ - Added `DoesActorOwnKeyOptions` interface.
66
+ - The type of the third parameter of `doesActorOwnKey()` function became
67
+ `DoesActorOwnKeyOptions` (was `DocumentLoader`).
68
+
43
69
  - Removed the dependency on *@js-temporal/polyfill* on Deno, and Fedify now
44
70
  requires `--unstable-temporal` flag. On other runtime, it still depends
45
71
  on *@js-temporal/polyfill*.
@@ -54,7 +80,13 @@ To be released.
54
80
  - Fixed a bug where the authenticated document loader had thrown `InvalidUrl`
55
81
  error when the URL redirection was involved in Bun.
56
82
 
83
+ - Fixed a bug of `lookupObject()` that it had failed to look up the actor
84
+ object when WebFinger response had no links with
85
+ `"type": "application/activity+json"` but had `"type":
86
+ "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""`.
87
+
57
88
  [@fedify/cli]: https://jsr.io/@fedify/cli
89
+ [releases]: https://github.com/dahlia/fedify/releases
58
90
  [FEP-8fcf]: https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md
59
91
 
60
92
 
@@ -176,7 +176,7 @@ export async function handleInbox(request, { handle, context, kv, kvPrefix, acto
176
176
  return await onNotFound(request);
177
177
  }
178
178
  }
179
- const key = await verify(request, context.documentLoader);
179
+ const key = await verify(request, context.documentLoader, context.contextLoader);
180
180
  if (key == null) {
181
181
  logger.error("Failed to verify the request signature.", { handle });
182
182
  const response = new Response("Failed to verify the request signature.", {
@@ -233,7 +233,7 @@ export async function handleInbox(request, { handle, context, kv, kvPrefix, acto
233
233
  });
234
234
  return response;
235
235
  }
236
- if (!await doesActorOwnKey(activity, key, context.documentLoader)) {
236
+ if (!await doesActorOwnKey(activity, key, context)) {
237
237
  logger.error("The signer ({keyId}) and the actor ({actorId}) do not match.", { activity: json, keyId: key.id?.href, actorId: activity.actorId.href });
238
238
  const response = new Response("The signer and the actor do not match.", {
239
239
  status: 401,
@@ -32,6 +32,7 @@ export class Federation {
32
32
  #inboxListeners;
33
33
  #inboxErrorHandler;
34
34
  #documentLoader;
35
+ #contextLoader;
35
36
  #authenticatedDocumentLoaderFactory;
36
37
  #treatHttps;
37
38
  #onOutboxError;
@@ -40,7 +41,7 @@ export class Federation {
40
41
  * Create a new {@link Federation} instance.
41
42
  * @param parameters Parameters for initializing the instance.
42
43
  */
43
- constructor({ kv, kvPrefixes, queue, documentLoader, authenticatedDocumentLoaderFactory, treatHttps, onOutboxError, backoffSchedule, }) {
44
+ constructor({ kv, kvPrefixes, queue, documentLoader, contextLoader, authenticatedDocumentLoaderFactory, treatHttps, onOutboxError, backoffSchedule, }) {
44
45
  this.#kv = kv;
45
46
  this.#kvPrefixes = {
46
47
  ...({
@@ -61,6 +62,7 @@ export class Federation {
61
62
  kv: kv,
62
63
  prefix: this.#kvPrefixes.remoteDocument,
63
64
  });
65
+ this.#contextLoader = contextLoader ?? this.#documentLoader;
64
66
  this.#authenticatedDocumentLoaderFactory =
65
67
  authenticatedDocumentLoaderFactory ??
66
68
  getAuthenticatedDocumentLoader;
@@ -98,13 +100,14 @@ export class Federation {
98
100
  const documentLoader = this.#authenticatedDocumentLoaderFactory({ keyId, privateKey });
99
101
  activity = await Activity.fromJsonLd(message.activity, {
100
102
  documentLoader,
103
+ contextLoader: this.#contextLoader,
101
104
  });
102
105
  await sendActivity({
103
106
  keyId,
104
107
  privateKey,
105
108
  activity,
106
109
  inbox: new URL(message.inbox),
107
- documentLoader,
110
+ contextLoader: this.#contextLoader,
108
111
  headers: new Headers(message.headers),
109
112
  });
110
113
  }
@@ -169,6 +172,7 @@ export class Federation {
169
172
  const context = {
170
173
  data: contextData,
171
174
  documentLoader: this.#documentLoader,
175
+ contextLoader: this.#contextLoader,
172
176
  getNodeInfoUri: () => {
173
177
  const path = this.#router.build("nodeInfo", {});
174
178
  if (path == null) {
@@ -337,7 +341,7 @@ export class Federation {
337
341
  async getSignedKey() {
338
342
  if (signedKey !== undefined)
339
343
  return signedKey;
340
- return signedKey = await verify(request, context.documentLoader);
344
+ return signedKey = await verify(request, context.documentLoader, context.contextLoader);
341
345
  },
342
346
  async getSignedKeyOwner() {
343
347
  if (signedKeyOwner !== undefined)
@@ -345,7 +349,7 @@ export class Federation {
345
349
  const key = await this.getSignedKey();
346
350
  if (key == null)
347
351
  return signedKeyOwner = null;
348
- return signedKeyOwner = await getKeyOwner(key, context.documentLoader);
352
+ return signedKeyOwner = await getKeyOwner(key, context);
349
353
  },
350
354
  };
351
355
  return reqCtx;
@@ -698,7 +702,6 @@ export class Federation {
698
702
  activityId: activity.id?.href,
699
703
  activity,
700
704
  });
701
- const documentLoader = this.#authenticatedDocumentLoaderFactory({ keyId, privateKey });
702
705
  if (immediate || this.#queue == null) {
703
706
  if (immediate) {
704
707
  logger.debug("Sending activity immediately without queue since immediate option " +
@@ -714,7 +717,7 @@ export class Federation {
714
717
  privateKey,
715
718
  activity,
716
719
  inbox: new URL(inbox),
717
- documentLoader,
720
+ contextLoader: this.#contextLoader,
718
721
  headers: collectionSync == null ? undefined : new Headers({
719
722
  "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox]),
720
723
  }),
@@ -725,7 +728,9 @@ export class Federation {
725
728
  }
726
729
  logger.debug("Enqueuing activity {activityId} to send later.", { activityId: activity.id?.href, activity });
727
730
  const privateKeyJwk = await exportJwk(privateKey);
728
- const activityJson = await activity.toJsonLd({ documentLoader });
731
+ const activityJson = await activity.toJsonLd({
732
+ contextLoader: this.#contextLoader,
733
+ });
729
734
  for (const inbox in inboxes) {
730
735
  const message = {
731
736
  type: "outbox",
@@ -26,12 +26,12 @@ export function extractInboxes({ recipients, preferSharedInbox }) {
26
26
  * See also {@link SendActivityParameters}.
27
27
  * @throws {Error} If the activity fails to send.
28
28
  */
29
- export async function sendActivity({ activity, privateKey, keyId, inbox, documentLoader, headers, }) {
29
+ export async function sendActivity({ activity, privateKey, keyId, inbox, contextLoader, headers, }) {
30
30
  const logger = getLogger(["fedify", "federation", "outbox"]);
31
31
  if (activity.actorId == null) {
32
32
  throw new TypeError("The activity to send must have at least one actor property.");
33
33
  }
34
- const jsonLd = await activity.toJsonLd({ documentLoader });
34
+ const jsonLd = await activity.toJsonLd({ contextLoader });
35
35
  headers = new Headers(headers);
36
36
  headers.set("Content-Type", "application/activity+json");
37
37
  let request = new Request(inbox, {
@@ -8,6 +8,7 @@ import * as dntShim from "../_dnt.shims.js";
8
8
  import { getLogger } from "@logtape/logtape";
9
9
  import { equals } from "../deps/jsr.io/@std/bytes/0.224.0/mod.js";
10
10
  import { decodeBase64, encodeBase64 } from "../deps/jsr.io/@std/encoding/0.224.0/base64.js";
11
+ import { fetchDocumentLoader, } from "../runtime/docloader.js";
11
12
  import { isActor } from "../vocab/actor.js";
12
13
  import { CryptographicKey, Object as ASObject, } from "../vocab/vocab.js";
13
14
  import { validateCryptoKey } from "./key.js";
@@ -68,12 +69,13 @@ const supportedHashAlgorithms = {
68
69
  * under the hood.
69
70
  * @param request The request to verify.
70
71
  * @param documentLoader The document loader to use for fetching the public key.
72
+ * @param contextLoader The context loader to use for JSON-LD context retrieval.
71
73
  * @param currentTime The current time. If not specified, the current time is
72
74
  * used. This is useful for testing.
73
75
  * @returns The public key of the verified signature, or `null` if the signature
74
76
  * could not be verified.
75
77
  */
76
- export async function verify(request, documentLoader, currentTime) {
78
+ export async function verify(request, documentLoader, contextLoader, currentTime) {
77
79
  const logger = getLogger(["fedify", "httpsig", "verify"]);
78
80
  request = request.clone();
79
81
  const dateHeader = request.headers.get("Date");
@@ -159,13 +161,19 @@ export async function verify(request, documentLoader, currentTime) {
159
161
  }
160
162
  let object;
161
163
  try {
162
- object = await ASObject.fromJsonLd(document, { documentLoader });
164
+ object = await ASObject.fromJsonLd(document, {
165
+ documentLoader,
166
+ contextLoader,
167
+ });
163
168
  }
164
169
  catch (e) {
165
170
  if (!(e instanceof TypeError))
166
171
  throw e;
167
172
  try {
168
- object = await CryptographicKey.fromJsonLd(document, { documentLoader });
173
+ object = await CryptographicKey.fromJsonLd(document, {
174
+ documentLoader,
175
+ contextLoader,
176
+ });
169
177
  }
170
178
  catch (e) {
171
179
  if (e instanceof TypeError) {
@@ -179,7 +187,7 @@ export async function verify(request, documentLoader, currentTime) {
179
187
  if (object instanceof CryptographicKey)
180
188
  key = object;
181
189
  else if (isActor(object)) {
182
- for await (const k of object.getPublicKeys({ documentLoader })) {
190
+ for await (const k of object.getPublicKeys({ documentLoader, contextLoader })) {
183
191
  if (k.id?.href === keyId) {
184
192
  key = k;
185
193
  break;
@@ -231,14 +239,14 @@ export async function verify(request, documentLoader, currentTime) {
231
239
  * Checks if the actor of the given activity owns the specified key.
232
240
  * @param activity The activity to check.
233
241
  * @param key The public key to check.
234
- * @param documentLoader The document loader to use for fetching the actor.
242
+ * @param options Options for checking the key ownership.
235
243
  * @returns Whether the actor is the owner of the key.
236
244
  */
237
- export async function doesActorOwnKey(activity, key, documentLoader) {
245
+ export async function doesActorOwnKey(activity, key, options) {
238
246
  if (key.ownerId != null) {
239
247
  return key.ownerId.href === activity.actorId?.href;
240
248
  }
241
- const actor = await activity.getActor({ documentLoader });
249
+ const actor = await activity.getActor(options);
242
250
  if (actor == null || !isActor(actor))
243
251
  return false;
244
252
  for (const publicKeyId of actor.publicKeyIds) {
@@ -248,14 +256,18 @@ export async function doesActorOwnKey(activity, key, documentLoader) {
248
256
  return false;
249
257
  }
250
258
  /**
251
- * Gets the actor that owns the specified key. Returns `null` if the key has no known owner.
259
+ * Gets the actor that owns the specified key. Returns `null` if the key has no
260
+ * known owner.
252
261
  *
253
262
  * @param keyId The ID of the key to check, or the key itself.
254
- * @param documentLoader The document loader to use for fetching the key and its owner.
255
- * @returns The actor that owns the key, or `null` if the key has no known owner.
263
+ * @param options Options for getting the key owner.
264
+ * @returns The actor that owns the key, or `null` if the key has no known
265
+ * owner.
256
266
  * @since 0.7.0
257
267
  */
258
- export async function getKeyOwner(keyId, documentLoader) {
268
+ export async function getKeyOwner(keyId, options) {
269
+ const documentLoader = options.documentLoader ?? fetchDocumentLoader;
270
+ const contextLoader = options.contextLoader ?? fetchDocumentLoader;
259
271
  let object;
260
272
  if (keyId instanceof CryptographicKey) {
261
273
  object = keyId;
@@ -273,13 +285,19 @@ export async function getKeyOwner(keyId, documentLoader) {
273
285
  return null;
274
286
  }
275
287
  try {
276
- object = await ASObject.fromJsonLd(keyDoc, { documentLoader });
288
+ object = await ASObject.fromJsonLd(keyDoc, {
289
+ documentLoader,
290
+ contextLoader,
291
+ });
277
292
  }
278
293
  catch (e) {
279
294
  if (!(e instanceof TypeError))
280
295
  throw e;
281
296
  try {
282
- object = await CryptographicKey.fromJsonLd(keyDoc, { documentLoader });
297
+ object = await CryptographicKey.fromJsonLd(keyDoc, {
298
+ documentLoader,
299
+ contextLoader,
300
+ });
283
301
  }
284
302
  catch (e) {
285
303
  if (e instanceof TypeError)
@@ -292,7 +310,7 @@ export async function getKeyOwner(keyId, documentLoader) {
292
310
  if (object instanceof CryptographicKey) {
293
311
  if (object.ownerId == null)
294
312
  return null;
295
- owner = await object.getOwner({ documentLoader });
313
+ owner = await object.getOwner({ documentLoader, contextLoader });
296
314
  }
297
315
  else if (isActor(object)) {
298
316
  owner = object;
@@ -57,7 +57,8 @@ export async function lookupObject(identifier, options = {}) {
57
57
  if (jrd?.links == null)
58
58
  return null;
59
59
  for (const l of jrd.links) {
60
- if (l.type !== "application/activity+json" || l.rel !== "self")
60
+ if (l.type !== "application/activity+json" &&
61
+ !l.type?.match(/application\/ld\+json;\s*profile="https:\/\/www.w3.org\/ns\/activitystreams"/) || l.rel !== "self")
61
62
  continue;
62
63
  try {
63
64
  const remoteDoc = await documentLoader(l.href);
@@ -73,7 +74,10 @@ export async function lookupObject(identifier, options = {}) {
73
74
  if (document == null)
74
75
  return null;
75
76
  try {
76
- return await Object.fromJsonLd(document, { documentLoader });
77
+ return await Object.fromJsonLd(document, {
78
+ documentLoader,
79
+ contextLoader: options.contextLoader,
80
+ });
77
81
  }
78
82
  catch (e) {
79
83
  if (e instanceof TypeError)