@fedify/fedify 1.4.0-dev.630 → 1.4.0-dev.633

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,6 +8,31 @@ Version 1.4.0
8
8
 
9
9
  To be released.
10
10
 
11
+ - Document loader and context loader are now configurable with a factory
12
+ function for more flexibility.
13
+
14
+ - Deprecated `CreateFederationOptions.documentLoader` option.
15
+ Use `CreateFederationOptions.documentLoaderFactory` option instead.
16
+ - Deprecated `CreateFederationOptions.contextLoader` option.
17
+ Use `CreateFederationOptions.contextLoaderFactory` option instead.
18
+ - Added `DocumentLoaderFactory` type.
19
+ - Added `DocumentLoaderFactoryOptions` interface.
20
+ - Added the second parameter with `DocumentLoaderFactoryOptions` type
21
+ to `AuthenticatedDocumentLoaderFactory` type.
22
+ - `GetAuthenticatedDocumentLoaderOptions` interface became to extend
23
+ `DocumentLoaderFactoryOptions` interface.
24
+
25
+ - Introduced `ActivityTransformer`s for adjusting outgoing activities
26
+ before sending them so that some ActivityPub implementations with quirks
27
+ are satisfied.
28
+
29
+ - Added `@fedify/fedify/compat` module.
30
+ - Added `ActivityTransformer` type.
31
+ - Added `autoIdAssigner()` function.
32
+ - Added `actorDehydrator()` function.
33
+ - Added `defaultActivityTransformers` constant.
34
+ - Added `CreateFederationOptions.activityTransformers` option.
35
+
11
36
  - The `suppressError` option of Activity Vocabulary APIs,
12
37
  `traverseCollection()` function, and `Context.traverseCollection()` method
13
38
  now suppresses errors occurred JSON-LD processing.
@@ -43,6 +68,11 @@ To be released.
43
68
 
44
69
  - Added `allowPrivateAddress` option to `LookupWebFingerOptions` interface.
45
70
 
71
+ - Added more log messages using the [LogTape] library. Currently the below
72
+ logger categories are used:
73
+
74
+ - `["fedify", "compat", "transformers"]`
75
+
46
76
  - Added `-t`/`--traverse` option to the `fedify lookup` subcommand. [[#195]]
47
77
 
48
78
  - Added `-S`/`--suppress-errors` option to the `fedify lookup` subcommand.
@@ -0,0 +1,2 @@
1
+ export * from "./transformers.js";
2
+ export * from "./types.js";
@@ -0,0 +1,85 @@
1
+ import * as dntShim from "../_dnt.shims.js";
2
+ import { getLogger } from "@logtape/logtape";
3
+ const logger = getLogger(["fedify", "compat", "transformers"]);
4
+ /**
5
+ * An activity transformer that assigns a new random ID to an activity if it
6
+ * does not already have one. This is useful for ensuring that activities
7
+ * have an ID before they are sent to other servers.
8
+ *
9
+ * The generated ID is a URN UUID like:
10
+ *
11
+ * ```
12
+ * urn:uuid:12345678-1234-5678-1234-567812345678
13
+ * ```
14
+ * @param activity The activity to assign an ID to.
15
+ * @return The activity with an ID assigned.
16
+ * @since 1.4.0
17
+ */
18
+ export function autoIdAssigner(activity) {
19
+ if (activity.id != null)
20
+ return activity;
21
+ const id = new URL(`urn:uuid:${dntShim.crypto.randomUUID()}`);
22
+ logger.warn("As the activity to send does not have an id, a new id {id} has " +
23
+ "been generated for it. However, it is recommended to explicitly " +
24
+ "set the id for the activity.", { id: id.href });
25
+ return activity.clone({ id });
26
+ }
27
+ /**
28
+ * An activity transformer that dehydrates the actor property of an activity
29
+ * so that it only contains the actor's URI. For example, suppose we have an
30
+ * activity like this:
31
+ *
32
+ * ```typescript
33
+ * import { Follow, Person } from "@fedify/fedify/vocab";
34
+ * const input = new Follow({
35
+ * id: new URL("http://example.com/activities/1"),
36
+ * actor: new Person({
37
+ * id: new URL("http://example.com/actors/1"),
38
+ * name: "Alice",
39
+ * preferredUsername: "alice",
40
+ * }),
41
+ * object: new Person({
42
+ * id: new URL("http://example.com/actors/2"),
43
+ * name: "Bob",
44
+ * preferredUsername: "bob",
45
+ * }),
46
+ * });
47
+ * ```
48
+ *
49
+ * The result of applying this transformer would be:
50
+ *
51
+ * ```typescript
52
+ * import { Follow, Person } from "@fedify/fedify/vocab";
53
+ * const output = new Follow({
54
+ * id: new URL("http://example.com/activities/1"),
55
+ * actor: new URL("http://example.com/actors/1"),
56
+ * object: new Person({
57
+ * id: new URL("http://example.com/actors/2"),
58
+ * name: "Bob",
59
+ * preferredUsername: "bob",
60
+ * }),
61
+ * });
62
+ * ```
63
+ *
64
+ * As some ActivityPub implementations like Threads fail to deal with inlined
65
+ * actor objects, this transformer can be used to work around this issue.
66
+ * @param activity The activity to dehydrate the actor property of.
67
+ * @returns The dehydrated activity.
68
+ * @since 1.4.0
69
+ */
70
+ export function actorDehydrator(activity) {
71
+ if (activity.actorIds.length < 1)
72
+ return activity;
73
+ return activity.clone({
74
+ actors: activity.actorIds,
75
+ });
76
+ }
77
+ /**
78
+ * The default activity transformers that are applied to all outgoing
79
+ * activities.
80
+ * @since 1.4.0
81
+ */
82
+ export const defaultActivityTransformers = [
83
+ autoIdAssigner,
84
+ actorDehydrator,
85
+ ];
@@ -0,0 +1 @@
1
+ export {};
package/esm/deno.js CHANGED
@@ -1,9 +1,10 @@
1
1
  export default {
2
2
  "name": "@fedify/fedify",
3
- "version": "1.4.0-dev.630+203d67a0",
3
+ "version": "1.4.0-dev.633+1856bfff",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./mod.ts",
7
+ "./compat": "./compat/mod.ts",
7
8
  "./federation": "./federation/mod.ts",
8
9
  "./nodeinfo": "./nodeinfo/mod.ts",
9
10
  "./runtime": "./runtime/mod.ts",
@@ -3,6 +3,7 @@ import { verifyObject } from "../mod.js";
3
3
  import { getLogger, withContext } from "@logtape/logtape";
4
4
  import { context, propagation, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
5
5
  import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL, } from "@opentelemetry/semantic-conventions";
6
+ import { defaultActivityTransformers } from "../compat/transformers.js";
6
7
  import metadata from "../deno.js";
7
8
  import { getNodeInfo } from "../nodeinfo/client.js";
8
9
  import { handleNodeInfo, handleNodeInfoJrd } from "../nodeinfo/handler.js";
@@ -56,8 +57,8 @@ export class FederationImpl {
56
57
  inboxListeners;
57
58
  inboxErrorHandler;
58
59
  sharedInboxKeyDispatcher;
59
- documentLoader;
60
- contextLoader;
60
+ documentLoaderFactory;
61
+ contextLoaderFactory;
61
62
  authenticatedDocumentLoaderFactory;
62
63
  allowPrivateAddress;
63
64
  userAgent;
@@ -66,8 +67,10 @@ export class FederationImpl {
66
67
  skipSignatureVerification;
67
68
  outboxRetryPolicy;
68
69
  inboxRetryPolicy;
70
+ activityTransformers;
69
71
  tracerProvider;
70
72
  constructor(options) {
73
+ const logger = getLogger(["fedify", "federation"]);
71
74
  this.kv = options.kv;
72
75
  this.kvPrefixes = {
73
76
  ...({
@@ -115,12 +118,42 @@ export class FederationImpl {
115
118
  }
116
119
  const { allowPrivateAddress, userAgent } = options;
117
120
  this.allowPrivateAddress = allowPrivateAddress ?? false;
118
- this.documentLoader = options.documentLoader ?? kvCache({
119
- loader: getDocumentLoader({ allowPrivateAddress, userAgent }),
120
- kv: options.kv,
121
- prefix: this.kvPrefixes.remoteDocument,
122
- });
123
- this.contextLoader = options.contextLoader ?? this.documentLoader;
121
+ if (options.documentLoader != null) {
122
+ if (options.documentLoaderFactory != null) {
123
+ throw new TypeError("Cannot set both documentLoader and documentLoaderFactory options " +
124
+ "at a time; use documentLoaderFactory only.");
125
+ }
126
+ this.documentLoaderFactory = () => options.documentLoader;
127
+ logger.warn("The documentLoader option is deprecated; use documentLoaderFactory " +
128
+ "option instead.");
129
+ }
130
+ else {
131
+ this.documentLoaderFactory = options.documentLoaderFactory ??
132
+ ((opts) => {
133
+ return kvCache({
134
+ loader: getDocumentLoader({
135
+ allowPrivateAddress: opts?.allowPrivateAddress ??
136
+ allowPrivateAddress,
137
+ userAgent: opts?.userAgent ?? userAgent,
138
+ }),
139
+ kv: options.kv,
140
+ prefix: this.kvPrefixes.remoteDocument,
141
+ });
142
+ });
143
+ }
144
+ if (options.contextLoader != null) {
145
+ if (options.contextLoaderFactory != null) {
146
+ throw new TypeError("Cannot set both contextLoader and contextLoaderFactory options " +
147
+ "at a time; use contextLoaderFactory only.");
148
+ }
149
+ this.contextLoaderFactory = () => options.contextLoader;
150
+ logger.warn("The contextLoader option is deprecated; use contextLoaderFactory " +
151
+ "option instead.");
152
+ }
153
+ else {
154
+ this.contextLoaderFactory = options.contextLoaderFactory ??
155
+ this.documentLoaderFactory;
156
+ }
124
157
  this.authenticatedDocumentLoaderFactory =
125
158
  options.authenticatedDocumentLoaderFactory ??
126
159
  ((identity) => getAuthenticatedDocumentLoader(identity, {
@@ -135,6 +168,8 @@ export class FederationImpl {
135
168
  createExponentialBackoffPolicy();
136
169
  this.inboxRetryPolicy = options.inboxRetryPolicy ??
137
170
  createExponentialBackoffPolicy();
171
+ this.activityTransformers = options.activityTransformers ??
172
+ defaultActivityTransformers;
138
173
  this.tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
139
174
  }
140
175
  #getTracer() {
@@ -252,11 +287,12 @@ export class FederationImpl {
252
287
  }
253
288
  catch (error) {
254
289
  span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
290
+ const loaderOptions = this.#getLoaderOptions(message.baseUrl);
255
291
  const activity = await Activity.fromJsonLd(message.activity, {
256
- contextLoader: this.contextLoader,
292
+ contextLoader: this.contextLoaderFactory(loaderOptions),
257
293
  documentLoader: rsaKeyPair == null
258
- ? this.documentLoader
259
- : this.authenticatedDocumentLoaderFactory(rsaKeyPair),
294
+ ? this.documentLoaderFactory(loaderOptions)
295
+ : this.authenticatedDocumentLoaderFactory(rsaKeyPair, loaderOptions),
260
296
  tracerProvider: this.tracerProvider,
261
297
  });
262
298
  try {
@@ -435,11 +471,14 @@ export class FederationImpl {
435
471
  url.hash = "";
436
472
  url.search = "";
437
473
  }
474
+ const loaderOptions = this.#getLoaderOptions(url.origin);
438
475
  const ctxOptions = {
439
476
  url,
440
477
  federation: this,
441
478
  data: contextData,
442
- documentLoader: opts.documentLoader ?? this.documentLoader,
479
+ documentLoader: opts.documentLoader ??
480
+ this.documentLoaderFactory(loaderOptions),
481
+ contextLoader: this.contextLoaderFactory(loaderOptions),
443
482
  };
444
483
  if (request == null)
445
484
  return new ContextImpl(ctxOptions);
@@ -450,6 +489,18 @@ export class FederationImpl {
450
489
  invokedFromObjectDispatcher: opts.invokedFromObjectDispatcher,
451
490
  });
452
491
  }
492
+ #getLoaderOptions(origin) {
493
+ origin = typeof origin === "string"
494
+ ? new URL(origin).origin
495
+ : origin.origin;
496
+ return {
497
+ allowPrivateAddress: this.allowPrivateAddress,
498
+ userAgent: typeof this.userAgent === "string" ? this.userAgent : {
499
+ url: origin,
500
+ ...this.userAgent,
501
+ },
502
+ };
503
+ }
453
504
  setNodeInfoDispatcher(path, dispatcher) {
454
505
  if (this.router.has("nodeInfo")) {
455
506
  throw new RouterError("NodeInfo dispatcher already set.");
@@ -1038,21 +1089,17 @@ export class FederationImpl {
1038
1089
  }
1039
1090
  async sendActivity(keys, recipients, activity, options, span) {
1040
1091
  const logger = getLogger(["fedify", "federation", "outbox"]);
1041
- const { preferSharedInbox, immediate, excludeBaseUris, collectionSync, contextData, } = options;
1092
+ const { preferSharedInbox, immediate, excludeBaseUris, collectionSync, contextData, origin, } = options;
1042
1093
  if (keys.length < 1) {
1043
1094
  throw new TypeError("The sender's keys must not be empty.");
1044
1095
  }
1045
1096
  for (const { privateKey } of keys) {
1046
1097
  validateCryptoKey(privateKey, "private");
1047
1098
  }
1048
- if (activity.id == null) {
1049
- const id = new URL(`urn:uuid:${dntShim.crypto.randomUUID()}`);
1050
- activity = activity.clone({ id });
1051
- logger.warn("As the activity to send does not have an id, a new id {id} has " +
1052
- "been generated for it. However, it is recommended to explicitly " +
1053
- "set the id for the activity.", { id: id.href });
1099
+ for (const activityTransformer of this.activityTransformers) {
1100
+ activity = activityTransformer(activity);
1054
1101
  }
1055
- span?.setAttribute("activitypub.activity.id", activity.id.href);
1102
+ span?.setAttribute("activitypub.activity.id", activity?.id?.href ?? "");
1056
1103
  if (activity.actorId == null) {
1057
1104
  logger.error("Activity {activityId} to send does not have an actor.", { activity, activityId: activity?.id?.href });
1058
1105
  throw new TypeError("The activity to send must have at least one actor property.");
@@ -1076,6 +1123,7 @@ export class FederationImpl {
1076
1123
  else if (keys.length < 1) {
1077
1124
  throw new TypeError("The keys must not be empty.");
1078
1125
  }
1126
+ const contextLoader = this.contextLoaderFactory(this.#getLoaderOptions(origin));
1079
1127
  const activityId = activity.id.href;
1080
1128
  let proofCreated = false;
1081
1129
  let rsaKey = null;
@@ -1087,7 +1135,7 @@ export class FederationImpl {
1087
1135
  }
1088
1136
  if (privateKey.algorithm.name === "Ed25519") {
1089
1137
  activity = await signObject(activity, privateKey, keyId, {
1090
- contextLoader: this.contextLoader,
1138
+ contextLoader,
1091
1139
  tracerProvider: this.tracerProvider,
1092
1140
  });
1093
1141
  proofCreated = true;
@@ -1095,7 +1143,7 @@ export class FederationImpl {
1095
1143
  }
1096
1144
  let jsonLd = await activity.toJsonLd({
1097
1145
  format: "compact",
1098
- contextLoader: this.contextLoader,
1146
+ contextLoader,
1099
1147
  });
1100
1148
  if (rsaKey == null) {
1101
1149
  logger.warn("No supported key found to create a Linked Data signature for " +
@@ -1111,7 +1159,7 @@ export class FederationImpl {
1111
1159
  }
1112
1160
  else {
1113
1161
  jsonLd = await signJsonLd(jsonLd, rsaKey.privateKey, rsaKey.keyId, {
1114
- contextLoader: this.contextLoader,
1162
+ contextLoader,
1115
1163
  tracerProvider: this.tracerProvider,
1116
1164
  });
1117
1165
  }
@@ -1167,6 +1215,7 @@ export class FederationImpl {
1167
1215
  const message = {
1168
1216
  type: "outbox",
1169
1217
  id: dntShim.crypto.randomUUID(),
1218
+ baseUrl: origin,
1170
1219
  keys: keyJwkPairs,
1171
1220
  activity: jsonLd,
1172
1221
  activityId: activity.id?.href,
@@ -1458,12 +1507,14 @@ export class ContextImpl {
1458
1507
  federation;
1459
1508
  data;
1460
1509
  documentLoader;
1510
+ contextLoader;
1461
1511
  invokedFromActorKeyPairsDispatcher;
1462
- constructor({ url, federation, data, documentLoader, invokedFromActorKeyPairsDispatcher, }) {
1512
+ constructor({ url, federation, data, documentLoader, contextLoader, invokedFromActorKeyPairsDispatcher, }) {
1463
1513
  this.url = url;
1464
1514
  this.federation = federation;
1465
1515
  this.data = data;
1466
1516
  this.documentLoader = documentLoader;
1517
+ this.contextLoader = contextLoader;
1467
1518
  this.invokedFromActorKeyPairsDispatcher =
1468
1519
  invokedFromActorKeyPairsDispatcher;
1469
1520
  }
@@ -1473,6 +1524,7 @@ export class ContextImpl {
1473
1524
  federation: this.federation,
1474
1525
  data: this.data,
1475
1526
  documentLoader: this.documentLoader,
1527
+ contextLoader: this.contextLoader,
1476
1528
  invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher,
1477
1529
  });
1478
1530
  }
@@ -1485,9 +1537,6 @@ export class ContextImpl {
1485
1537
  get origin() {
1486
1538
  return this.url.origin;
1487
1539
  }
1488
- get contextLoader() {
1489
- return this.federation.contextLoader;
1490
- }
1491
1540
  get tracerProvider() {
1492
1541
  return this.federation.tracerProvider;
1493
1542
  }
@@ -1928,6 +1977,7 @@ export class ContextImpl {
1928
1977
  }
1929
1978
  const opts = {
1930
1979
  contextData: this.data,
1980
+ origin: this.origin,
1931
1981
  ...options,
1932
1982
  };
1933
1983
  let expandedRecipients;
@@ -2322,6 +2372,7 @@ export class InboxContextImpl extends ContextImpl {
2322
2372
  const message = {
2323
2373
  type: "outbox",
2324
2374
  id: dntShim.crypto.randomUUID(),
2375
+ baseUrl: this.origin,
2325
2376
  keys: keyJwkPairs,
2326
2377
  activity: this.activity,
2327
2378
  activityId: this.activityId,
package/esm/mod.js CHANGED
@@ -41,6 +41,7 @@
41
41
  *
42
42
  * @module
43
43
  */
44
+ export * from "./compat/mod.js";
44
45
  export * from "./federation/mod.js";
45
46
  export * from "./nodeinfo/mod.js";
46
47
  export * from "./runtime/mod.js";