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

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.

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)