@fedify/fedify 1.0.0-dev.397 → 1.0.0-dev.399
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 +16 -0
- package/esm/federation/federation.js +1 -0
- package/esm/federation/handler.js +3 -3
- package/esm/federation/middleware.js +224 -42
- package/esm/federation/mod.js +2 -1
- package/esm/federation/send.js +9 -63
- package/esm/sig/ld.js +7 -1
- package/esm/sig/mod.js +1 -1
- package/package.json +1 -1
- package/types/federation/callback.d.ts +5 -2
- package/types/federation/callback.d.ts.map +1 -1
- package/types/federation/context.d.ts +55 -3
- package/types/federation/context.d.ts.map +1 -1
- package/types/federation/federation.d.ts +462 -0
- package/types/federation/federation.d.ts.map +1 -0
- package/types/federation/handler.d.ts +3 -2
- package/types/federation/handler.d.ts.map +1 -1
- package/types/federation/middleware.d.ts +125 -426
- package/types/federation/middleware.d.ts.map +1 -1
- package/types/federation/mod.d.ts +2 -1
- package/types/federation/mod.d.ts.map +1 -1
- package/types/federation/queue.d.ts +1 -0
- package/types/federation/queue.d.ts.map +1 -1
- package/types/federation/send.d.ts +7 -14
- package/types/federation/send.d.ts.map +1 -1
- package/types/sig/ld.d.ts +11 -0
- package/types/sig/ld.d.ts.map +1 -1
- package/types/sig/mod.d.ts +1 -1
- package/types/sig/mod.d.ts.map +1 -1
- package/types/x/hono.d.ts +1 -1
package/CHANGES.md
CHANGED
@@ -28,6 +28,21 @@ To be released.
|
|
28
28
|
- Added `attachSignature()` function.
|
29
29
|
- Added `detachSignature()` function.
|
30
30
|
|
31
|
+
- In inbox listeners, a received activity now can be forwarded to another
|
32
|
+
server. [[#137]]
|
33
|
+
|
34
|
+
- Added `InboxContext` interface.
|
35
|
+
- Added `ForwardActivityOptions` interface.
|
36
|
+
- The first parameter of the `InboxListener` callback type became
|
37
|
+
`InboxContext` (was `Context`).
|
38
|
+
|
39
|
+
- The `Context.parseUri()` method's parameter type became `URL | null`
|
40
|
+
(was `URL`).
|
41
|
+
|
42
|
+
- `Context.sendActivity()` method now adds Object Integrity Proofs to
|
43
|
+
the activity to be sent only once. It had added Object Integrity Proofs
|
44
|
+
to the activity for every recipient before.
|
45
|
+
|
31
46
|
- WebFinger responses now include <http://webfinger.net/rel/avatar> links
|
32
47
|
if the `Actor` object returned by the actor dispatcher has `icon`/`icons`
|
33
48
|
property.
|
@@ -43,6 +58,7 @@ To be released.
|
|
43
58
|
|
44
59
|
[Linked Data Signatures]: https://web.archive.org/web/20170923124140/https://w3c-dvcg.github.io/ld-signatures/
|
45
60
|
[#135]: https://github.com/dahlia/fedify/issues/135
|
61
|
+
[#137]: https://github.com/dahlia/fedify/issues/137
|
46
62
|
|
47
63
|
|
48
64
|
Version 0.15.1
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -175,7 +175,7 @@ function filterCollectionItems(items, collectionName, filterPredicate) {
|
|
175
175
|
}
|
176
176
|
return result;
|
177
177
|
}
|
178
|
-
export async function handleInbox(request, { handle, context, kv, kvPrefixes, queue, actorDispatcher, inboxListeners, inboxErrorHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, }) {
|
178
|
+
export async function handleInbox(request, { handle, context, inboxContextFactory, kv, kvPrefixes, queue, actorDispatcher, inboxListeners, inboxErrorHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, }) {
|
179
179
|
const logger = getLogger(["fedify", "federation", "inbox"]);
|
180
180
|
if (actorDispatcher == null) {
|
181
181
|
logger.error("Actor dispatcher is not set.", { handle });
|
@@ -343,7 +343,7 @@ export async function handleInbox(request, { handle, context, kv, kvPrefixes, qu
|
|
343
343
|
attempt: 0,
|
344
344
|
started: new Date().toISOString(),
|
345
345
|
});
|
346
|
-
logger.info("Activity {activityId} is
|
346
|
+
logger.info("Activity {activityId} is enqueued.", { activityId: activity.id?.href, activity: json });
|
347
347
|
return new Response("Activity is enqueued.", {
|
348
348
|
status: 202,
|
349
349
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
@@ -358,7 +358,7 @@ export async function handleInbox(request, { handle, context, kv, kvPrefixes, qu
|
|
358
358
|
});
|
359
359
|
}
|
360
360
|
try {
|
361
|
-
await listener(
|
361
|
+
await listener(inboxContextFactory(json), activity);
|
362
362
|
}
|
363
363
|
catch (error) {
|
364
364
|
try {
|
@@ -4,7 +4,9 @@ import { handleNodeInfo, handleNodeInfoJrd } from "../nodeinfo/handler.js";
|
|
4
4
|
import { fetchDocumentLoader, getAuthenticatedDocumentLoader, kvCache, } from "../runtime/docloader.js";
|
5
5
|
import { verifyRequest } from "../sig/http.js";
|
6
6
|
import { exportJwk, importJwk, validateCryptoKey } from "../sig/key.js";
|
7
|
+
import { hasSignature, signJsonLd } from "../sig/ld.js";
|
7
8
|
import { getKeyOwner } from "../sig/owner.js";
|
9
|
+
import { signObject } from "../sig/proof.js";
|
8
10
|
import { lookupObject } from "../vocab/lookup.js";
|
9
11
|
import { Activity, CryptographicKey, Multikey, } from "../vocab/vocab.js";
|
10
12
|
import { handleWebFinger } from "../webfinger/handler.js";
|
@@ -23,7 +25,7 @@ import { extractInboxes, sendActivity } from "./send.js";
|
|
23
25
|
export function createFederation(options) {
|
24
26
|
return new FederationImpl(options);
|
25
27
|
}
|
26
|
-
class FederationImpl {
|
28
|
+
export class FederationImpl {
|
27
29
|
kv;
|
28
30
|
kvPrefixes;
|
29
31
|
queue;
|
@@ -128,45 +130,44 @@ class FederationImpl {
|
|
128
130
|
keyIds: message.keys.map((pair) => pair.keyId),
|
129
131
|
inbox: message.inbox,
|
130
132
|
activity: message.activity,
|
133
|
+
activityId: message.activityId,
|
131
134
|
attempt: message.attempt,
|
132
135
|
headers: message.headers,
|
133
136
|
};
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
pair.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
145
|
-
rsaKeyPair = pair;
|
146
|
-
}
|
147
|
-
keys.push(pair);
|
137
|
+
const keys = [];
|
138
|
+
let rsaKeyPair = null;
|
139
|
+
for (const { keyId, privateKey } of message.keys) {
|
140
|
+
const pair = {
|
141
|
+
keyId: new URL(keyId),
|
142
|
+
privateKey: await importJwk(privateKey, "private"),
|
143
|
+
};
|
144
|
+
if (rsaKeyPair == null &&
|
145
|
+
pair.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
146
|
+
rsaKeyPair = pair;
|
148
147
|
}
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
activity = await Activity.fromJsonLd(message.activity, {
|
153
|
-
documentLoader,
|
154
|
-
contextLoader: this.contextLoader,
|
155
|
-
});
|
148
|
+
keys.push(pair);
|
149
|
+
}
|
150
|
+
try {
|
156
151
|
await sendActivity({
|
157
152
|
keys,
|
158
|
-
activity,
|
153
|
+
activity: message.activity,
|
154
|
+
activityId: message.activityId,
|
159
155
|
inbox: new URL(message.inbox),
|
160
|
-
contextLoader: this.contextLoader,
|
161
156
|
headers: new Headers(message.headers),
|
162
157
|
});
|
163
158
|
}
|
164
159
|
catch (error) {
|
160
|
+
const activity = await Activity.fromJsonLd(message.activity, {
|
161
|
+
contextLoader: this.contextLoader,
|
162
|
+
documentLoader: rsaKeyPair == null
|
163
|
+
? this.documentLoader
|
164
|
+
: this.authenticatedDocumentLoaderFactory(rsaKeyPair),
|
165
|
+
});
|
165
166
|
try {
|
166
167
|
this.onOutboxError?.(error, activity);
|
167
168
|
}
|
168
169
|
catch (error) {
|
169
|
-
logger.error("An unexpected error occurred in onError handler:\n{error}", { ...logData, error
|
170
|
+
logger.error("An unexpected error occurred in onError handler:\n{error}", { ...logData, error });
|
170
171
|
}
|
171
172
|
const delay = this.outboxRetryPolicy({
|
172
173
|
elapsedTime: dntShim.Temporal.Instant.from(message.started).until(dntShim.Temporal.Now.instant()),
|
@@ -174,7 +175,7 @@ class FederationImpl {
|
|
174
175
|
});
|
175
176
|
if (delay != null) {
|
176
177
|
logger.error("Failed to send activity {activityId} to {inbox} (attempt " +
|
177
|
-
"#{attempt}); retry...:\n{error}", { ...logData, error
|
178
|
+
"#{attempt}); retry...:\n{error}", { ...logData, error });
|
178
179
|
this.queue?.enqueue({
|
179
180
|
...message,
|
180
181
|
attempt: message.attempt + 1,
|
@@ -186,11 +187,11 @@ class FederationImpl {
|
|
186
187
|
}
|
187
188
|
else {
|
188
189
|
logger.error("Failed to send activity {activityId} to {inbox} after {attempt} " +
|
189
|
-
"attempts; giving up:\n{error}", { ...logData, error
|
190
|
+
"attempts; giving up:\n{error}", { ...logData, error });
|
190
191
|
}
|
191
192
|
return;
|
192
193
|
}
|
193
|
-
logger.info("Successfully sent activity {activityId} to {inbox}.", { ...logData
|
194
|
+
logger.info("Successfully sent activity {activityId} to {inbox}.", { ...logData });
|
194
195
|
}
|
195
196
|
async #listenInboxMessage(ctxData, message) {
|
196
197
|
const logger = getLogger(["fedify", "federation", "inbox"]);
|
@@ -234,7 +235,7 @@ class FederationImpl {
|
|
234
235
|
return;
|
235
236
|
}
|
236
237
|
try {
|
237
|
-
await listener(context, activity);
|
238
|
+
await listener(context.toInboxContext(message.activity), activity);
|
238
239
|
}
|
239
240
|
catch (error) {
|
240
241
|
try {
|
@@ -782,8 +783,6 @@ class FederationImpl {
|
|
782
783
|
logger.error("Activity {activityId} to send does not have an actor.", { activity, activityId: activity?.id?.href });
|
783
784
|
throw new TypeError("The activity to send must have at least one actor property.");
|
784
785
|
}
|
785
|
-
if (!this.manuallyStartQueue)
|
786
|
-
this.#startQueue(contextData);
|
787
786
|
if (activity.id == null) {
|
788
787
|
activity = activity.clone({
|
789
788
|
id: new URL(`urn:uuid:${dntShim.crypto.randomUUID()}`),
|
@@ -799,21 +798,78 @@ class FederationImpl {
|
|
799
798
|
activityId: activity.id?.href,
|
800
799
|
activity,
|
801
800
|
});
|
801
|
+
if (activity.id == null) {
|
802
|
+
throw new TypeError("The activity to send must have an id.");
|
803
|
+
}
|
804
|
+
if (activity.actorId == null) {
|
805
|
+
throw new TypeError("The activity to send must have at least one actor property.");
|
806
|
+
}
|
807
|
+
else if (keys.length < 1) {
|
808
|
+
throw new TypeError("The keys must not be empty.");
|
809
|
+
}
|
810
|
+
const activityId = activity.id.href;
|
811
|
+
let proofCreated = false;
|
812
|
+
let rsaKey = null;
|
813
|
+
for (const { keyId, privateKey } of keys) {
|
814
|
+
validateCryptoKey(privateKey, "private");
|
815
|
+
if (rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
816
|
+
rsaKey = { keyId, privateKey };
|
817
|
+
continue;
|
818
|
+
}
|
819
|
+
if (privateKey.algorithm.name === "Ed25519") {
|
820
|
+
activity = await signObject(activity, privateKey, keyId, {
|
821
|
+
contextLoader: this.contextLoader,
|
822
|
+
});
|
823
|
+
proofCreated = true;
|
824
|
+
}
|
825
|
+
}
|
826
|
+
let jsonLd = await activity.toJsonLd({
|
827
|
+
format: "compact",
|
828
|
+
contextLoader: this.contextLoader,
|
829
|
+
});
|
830
|
+
if (rsaKey == null) {
|
831
|
+
logger.warn("No supported key found to create a Linked Data signature for " +
|
832
|
+
"the activity {activityId}. The activity will be sent without " +
|
833
|
+
"a Linked Data signature. In order to create a Linked Data " +
|
834
|
+
"signature, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
|
835
|
+
activityId,
|
836
|
+
keys: keys.map((pair) => ({
|
837
|
+
keyId: pair.keyId.href,
|
838
|
+
privateKey: pair.privateKey,
|
839
|
+
})),
|
840
|
+
});
|
841
|
+
}
|
842
|
+
else {
|
843
|
+
jsonLd = await signJsonLd(jsonLd, rsaKey.privateKey, rsaKey.keyId, {
|
844
|
+
contextLoader: this.contextLoader,
|
845
|
+
});
|
846
|
+
}
|
847
|
+
if (!proofCreated) {
|
848
|
+
logger.warn("No supported key found to create a proof for the activity {activityId}. " +
|
849
|
+
"The activity will be sent without a proof. " +
|
850
|
+
"In order to create a proof, at least one Ed25519 key must be provided.", {
|
851
|
+
activityId,
|
852
|
+
keys: keys.map((pair) => ({
|
853
|
+
keyId: pair.keyId.href,
|
854
|
+
privateKey: pair.privateKey,
|
855
|
+
})),
|
856
|
+
});
|
857
|
+
}
|
802
858
|
if (immediate || this.queue == null) {
|
803
859
|
if (immediate) {
|
804
860
|
logger.debug("Sending activity immediately without queue since immediate option " +
|
805
|
-
"is set.", { activityId: activity.id
|
861
|
+
"is set.", { activityId: activity.id.href, activity: jsonLd });
|
806
862
|
}
|
807
863
|
else {
|
808
|
-
logger.debug("Sending activity immediately without queue since queue is not set.", { activityId: activity.id
|
864
|
+
logger.debug("Sending activity immediately without queue since queue is not set.", { activityId: activity.id.href, activity: jsonLd });
|
809
865
|
}
|
810
866
|
const promises = [];
|
811
867
|
for (const inbox in inboxes) {
|
812
868
|
promises.push(sendActivity({
|
813
869
|
keys,
|
814
|
-
activity,
|
870
|
+
activity: jsonLd,
|
871
|
+
activityId: activity.id?.href,
|
815
872
|
inbox: new URL(inbox),
|
816
|
-
contextLoader: this.contextLoader,
|
817
873
|
headers: collectionSync == null ? undefined : new Headers({
|
818
874
|
"Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox]),
|
819
875
|
}),
|
@@ -822,20 +878,20 @@ class FederationImpl {
|
|
822
878
|
await Promise.all(promises);
|
823
879
|
return;
|
824
880
|
}
|
825
|
-
logger.debug("Enqueuing activity {activityId} to send later.", { activityId: activity.id
|
881
|
+
logger.debug("Enqueuing activity {activityId} to send later.", { activityId: activity.id.href, activity: jsonLd });
|
826
882
|
const keyJwkPairs = [];
|
827
883
|
for (const { keyId, privateKey } of keys) {
|
828
884
|
const privateKeyJwk = await exportJwk(privateKey);
|
829
885
|
keyJwkPairs.push({ keyId: keyId.href, privateKey: privateKeyJwk });
|
830
886
|
}
|
831
|
-
|
832
|
-
|
833
|
-
});
|
887
|
+
if (!this.manuallyStartQueue)
|
888
|
+
this.#startQueue(contextData);
|
834
889
|
for (const inbox in inboxes) {
|
835
890
|
const message = {
|
836
891
|
type: "outbox",
|
837
892
|
keys: keyJwkPairs,
|
838
|
-
activity:
|
893
|
+
activity: jsonLd,
|
894
|
+
activityId: activity.id?.href,
|
839
895
|
inbox,
|
840
896
|
started: new Date().toISOString(),
|
841
897
|
attempt: 0,
|
@@ -968,6 +1024,7 @@ class FederationImpl {
|
|
968
1024
|
return await handleInbox(request, {
|
969
1025
|
handle: route.values.handle ?? null,
|
970
1026
|
context,
|
1027
|
+
inboxContextFactory: context.toInboxContext.bind(context),
|
971
1028
|
kv: this.kv,
|
972
1029
|
kvPrefixes: this.kvPrefixes,
|
973
1030
|
queue: this.queue,
|
@@ -1064,6 +1121,15 @@ class ContextImpl {
|
|
1064
1121
|
this.invokedFromActorKeyPairsDispatcher =
|
1065
1122
|
invokedFromActorKeyPairsDispatcher;
|
1066
1123
|
}
|
1124
|
+
toInboxContext(activity) {
|
1125
|
+
return new InboxContextImpl(activity, {
|
1126
|
+
url: this.url,
|
1127
|
+
federation: this.federation,
|
1128
|
+
data: this.data,
|
1129
|
+
documentLoader: this.documentLoader,
|
1130
|
+
invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher,
|
1131
|
+
});
|
1132
|
+
}
|
1067
1133
|
get hostname() {
|
1068
1134
|
return this.url.hostname;
|
1069
1135
|
}
|
@@ -1165,6 +1231,8 @@ class ContextImpl {
|
|
1165
1231
|
return new URL(path, this.url);
|
1166
1232
|
}
|
1167
1233
|
parseUri(uri) {
|
1234
|
+
if (uri == null)
|
1235
|
+
return null;
|
1168
1236
|
if (uri.origin !== this.url.origin)
|
1169
1237
|
return null;
|
1170
1238
|
const route = this.federation.router.route(uri.pathname);
|
@@ -1336,7 +1404,7 @@ class ContextImpl {
|
|
1336
1404
|
throw new Error("If recipients is 'followers', sender must be an actor handle.");
|
1337
1405
|
}
|
1338
1406
|
expandedRecipients = [];
|
1339
|
-
for await (const recipient of this
|
1407
|
+
for await (const recipient of this.getFollowers(sender.handle)) {
|
1340
1408
|
expandedRecipients.push(recipient);
|
1341
1409
|
}
|
1342
1410
|
const collectionId = this.federation.router.build("followers", sender);
|
@@ -1349,7 +1417,7 @@ class ContextImpl {
|
|
1349
1417
|
}
|
1350
1418
|
return await this.federation.sendActivity(keys, expandedRecipients, activity, opts);
|
1351
1419
|
}
|
1352
|
-
async
|
1420
|
+
async *getFollowers(handle) {
|
1353
1421
|
if (this.federation.followersCallbacks == null) {
|
1354
1422
|
throw new Error("No followers collection dispatcher registered.");
|
1355
1423
|
}
|
@@ -1450,6 +1518,120 @@ class RequestContextImpl extends ContextImpl {
|
|
1450
1518
|
return this.#signedKeyOwner = await getKeyOwner(key, this);
|
1451
1519
|
}
|
1452
1520
|
}
|
1521
|
+
export class InboxContextImpl extends ContextImpl {
|
1522
|
+
activity;
|
1523
|
+
constructor(activity, options) {
|
1524
|
+
super(options);
|
1525
|
+
this.activity = activity;
|
1526
|
+
}
|
1527
|
+
async forwardActivity(forwarder, recipients, options) {
|
1528
|
+
const logger = getLogger(["fedify", "federation", "inbox"]);
|
1529
|
+
let keys;
|
1530
|
+
if ("handle" in forwarder) {
|
1531
|
+
keys = await this.getKeyPairsFromHandle(forwarder.handle);
|
1532
|
+
if (keys.length < 1) {
|
1533
|
+
throw new Error(`No key pair found for actor ${JSON.stringify(forwarder.handle)}.`);
|
1534
|
+
}
|
1535
|
+
}
|
1536
|
+
else if (Array.isArray(forwarder)) {
|
1537
|
+
if (forwarder.length < 1) {
|
1538
|
+
throw new Error("The forwarder's key pairs are empty.");
|
1539
|
+
}
|
1540
|
+
keys = forwarder;
|
1541
|
+
}
|
1542
|
+
else {
|
1543
|
+
keys = [forwarder];
|
1544
|
+
}
|
1545
|
+
let activityId = undefined;
|
1546
|
+
if (!hasSignature(this.activity)) {
|
1547
|
+
let hasProof;
|
1548
|
+
try {
|
1549
|
+
const activity = await Activity.fromJsonLd(this.activity, this);
|
1550
|
+
activityId = activity.id?.href;
|
1551
|
+
hasProof = await activity.getProof() != null;
|
1552
|
+
}
|
1553
|
+
catch {
|
1554
|
+
hasProof = false;
|
1555
|
+
}
|
1556
|
+
if (!hasProof) {
|
1557
|
+
if (options?.skipIfUnsigned)
|
1558
|
+
return;
|
1559
|
+
logger.warn("The received activity {activityId} is not signed; even if it is " +
|
1560
|
+
"forwarded to other servers as is, it may not be accepted by " +
|
1561
|
+
"them due to the lack of a signature/proof.");
|
1562
|
+
}
|
1563
|
+
}
|
1564
|
+
if (activityId == null && typeof this.activity === "object" &&
|
1565
|
+
this.activity != null) {
|
1566
|
+
activityId =
|
1567
|
+
"@id" in this.activity && typeof this.activity["@id"] === "string"
|
1568
|
+
? this.activity["@id"]
|
1569
|
+
: "id" in this.activity && typeof this.activity.id === "string"
|
1570
|
+
? this.activity.id
|
1571
|
+
: undefined;
|
1572
|
+
}
|
1573
|
+
if (recipients === "followers") {
|
1574
|
+
if (!("handle" in forwarder)) {
|
1575
|
+
throw new Error("If recipients is 'followers', forwarder must be an actor handle.");
|
1576
|
+
}
|
1577
|
+
const followers = [];
|
1578
|
+
for await (const recipient of this.getFollowers(forwarder.handle)) {
|
1579
|
+
followers.push(recipient);
|
1580
|
+
}
|
1581
|
+
recipients = followers;
|
1582
|
+
}
|
1583
|
+
const inboxes = extractInboxes({
|
1584
|
+
recipients: Array.isArray(recipients) ? recipients : [recipients],
|
1585
|
+
preferSharedInbox: options?.preferSharedInbox,
|
1586
|
+
excludeBaseUris: options?.excludeBaseUris,
|
1587
|
+
});
|
1588
|
+
logger.debug("Forwarding activity {activityId} to inboxes:\n{inboxes}", {
|
1589
|
+
inboxes: globalThis.Object.keys(inboxes),
|
1590
|
+
activityId,
|
1591
|
+
activity: this.activity,
|
1592
|
+
});
|
1593
|
+
if (options?.immediate || this.federation.queue == null) {
|
1594
|
+
if (options?.immediate) {
|
1595
|
+
logger.debug("Forwarding activity immediately without queue since immediate " +
|
1596
|
+
"option is set.");
|
1597
|
+
}
|
1598
|
+
else {
|
1599
|
+
logger.debug("Forwarding activity immediately without queue since queue is not " +
|
1600
|
+
"set.");
|
1601
|
+
}
|
1602
|
+
const promises = [];
|
1603
|
+
for (const inbox in inboxes) {
|
1604
|
+
promises.push(sendActivity({
|
1605
|
+
keys,
|
1606
|
+
activity: this.activity,
|
1607
|
+
activityId: activityId,
|
1608
|
+
inbox: new URL(inbox),
|
1609
|
+
}));
|
1610
|
+
}
|
1611
|
+
await Promise.all(promises);
|
1612
|
+
return;
|
1613
|
+
}
|
1614
|
+
logger.debug("Enqueuing activity {activityId} to forward later.", { activityId, activity: this.activity });
|
1615
|
+
const keyJwkPairs = [];
|
1616
|
+
for (const { keyId, privateKey } of keys) {
|
1617
|
+
const privateKeyJwk = await exportJwk(privateKey);
|
1618
|
+
keyJwkPairs.push({ keyId: keyId.href, privateKey: privateKeyJwk });
|
1619
|
+
}
|
1620
|
+
for (const inbox in inboxes) {
|
1621
|
+
const message = {
|
1622
|
+
type: "outbox",
|
1623
|
+
keys: keyJwkPairs,
|
1624
|
+
activity: this.activity,
|
1625
|
+
activityId,
|
1626
|
+
inbox,
|
1627
|
+
started: new Date().toISOString(),
|
1628
|
+
attempt: 0,
|
1629
|
+
headers: {},
|
1630
|
+
};
|
1631
|
+
this.federation.queue.enqueue(message);
|
1632
|
+
}
|
1633
|
+
}
|
1634
|
+
}
|
1453
1635
|
function notFound(_request) {
|
1454
1636
|
return new Response("Not Found", { status: 404 });
|
1455
1637
|
}
|
package/esm/federation/mod.js
CHANGED
@@ -6,9 +6,10 @@
|
|
6
6
|
export * from "./callback.js";
|
7
7
|
export * from "./collection.js";
|
8
8
|
export * from "./context.js";
|
9
|
+
export * from "./federation.js";
|
9
10
|
export { respondWithObject, respondWithObjectIfAcceptable, } from "./handler.js";
|
10
11
|
export * from "./kv.js";
|
11
|
-
export
|
12
|
+
export { createFederation, } from "./middleware.js";
|
12
13
|
export * from "./mq.js";
|
13
14
|
export * from "./retry.js";
|
14
15
|
export * from "./router.js";
|
package/esm/federation/send.js
CHANGED
@@ -1,8 +1,5 @@
|
|
1
1
|
import { getLogger } from "@logtape/logtape";
|
2
2
|
import { signRequest } from "../sig/http.js";
|
3
|
-
import { validateCryptoKey } from "../sig/key.js";
|
4
|
-
import { signJsonLd } from "../sig/ld.js";
|
5
|
-
import { signObject } from "../sig/proof.js";
|
6
3
|
/**
|
7
4
|
* Extracts the inbox URLs from recipients.
|
8
5
|
* @param parameters The parameters to extract the inboxes.
|
@@ -33,73 +30,22 @@ export function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris
|
|
33
30
|
* See also {@link SendActivityParameters}.
|
34
31
|
* @throws {Error} If the activity fails to send.
|
35
32
|
*/
|
36
|
-
export async function sendActivity({ activity, keys, inbox,
|
33
|
+
export async function sendActivity({ activity, activityId, keys, inbox, headers, }) {
|
37
34
|
const logger = getLogger(["fedify", "federation", "outbox"]);
|
38
|
-
if (activity.id == null) {
|
39
|
-
throw new TypeError("The activity to send must have an id.");
|
40
|
-
}
|
41
|
-
if (activity.actorId == null) {
|
42
|
-
throw new TypeError("The activity to send must have at least one actor property.");
|
43
|
-
}
|
44
|
-
else if (keys.length < 1) {
|
45
|
-
throw new TypeError("The keys must not be empty.");
|
46
|
-
}
|
47
|
-
const activityId = activity.id.href;
|
48
|
-
let proofCreated = false;
|
49
|
-
let rsaKey = null;
|
50
|
-
for (const { keyId, privateKey } of keys) {
|
51
|
-
validateCryptoKey(privateKey, "private");
|
52
|
-
if (rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
53
|
-
rsaKey = { keyId, privateKey };
|
54
|
-
continue;
|
55
|
-
}
|
56
|
-
if (privateKey.algorithm.name === "Ed25519") {
|
57
|
-
activity = await signObject(activity, privateKey, keyId, {
|
58
|
-
documentLoader,
|
59
|
-
contextLoader,
|
60
|
-
});
|
61
|
-
proofCreated = true;
|
62
|
-
}
|
63
|
-
}
|
64
|
-
let jsonLd = await activity.toJsonLd({
|
65
|
-
format: "compact",
|
66
|
-
contextLoader,
|
67
|
-
});
|
68
|
-
if (rsaKey == null) {
|
69
|
-
logger.warn("No supported key found to create a Linked Data signature for " +
|
70
|
-
"the activity {activityId}. The activity will be sent without " +
|
71
|
-
"a Linked Data signature. In order to create a Linked Data " +
|
72
|
-
"signature, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
|
73
|
-
activityId,
|
74
|
-
keys: keys.map((pair) => ({
|
75
|
-
keyId: pair.keyId.href,
|
76
|
-
privateKey: pair.privateKey,
|
77
|
-
})),
|
78
|
-
});
|
79
|
-
}
|
80
|
-
else {
|
81
|
-
jsonLd = await signJsonLd(jsonLd, rsaKey.privateKey, rsaKey.keyId, {
|
82
|
-
contextLoader,
|
83
|
-
});
|
84
|
-
}
|
85
|
-
if (!proofCreated) {
|
86
|
-
logger.warn("No supported key found to create a proof for the activity {activityId}. " +
|
87
|
-
"The activity will be sent without a proof. " +
|
88
|
-
"In order to create a proof, at least one Ed25519 key must be provided.", {
|
89
|
-
activityId,
|
90
|
-
keys: keys.map((pair) => ({
|
91
|
-
keyId: pair.keyId.href,
|
92
|
-
privateKey: pair.privateKey,
|
93
|
-
})),
|
94
|
-
});
|
95
|
-
}
|
96
35
|
headers = new Headers(headers);
|
97
36
|
headers.set("Content-Type", "application/activity+json");
|
98
37
|
let request = new Request(inbox, {
|
99
38
|
method: "POST",
|
100
39
|
headers,
|
101
|
-
body: JSON.stringify(
|
40
|
+
body: JSON.stringify(activity),
|
102
41
|
});
|
42
|
+
let rsaKey = null;
|
43
|
+
for (const key of keys) {
|
44
|
+
if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
45
|
+
rsaKey = key;
|
46
|
+
break;
|
47
|
+
}
|
48
|
+
}
|
103
49
|
if (rsaKey == null) {
|
104
50
|
logger.warn("No supported key found to sign the request to {inbox}. " +
|
105
51
|
"The request will be sent without a signature. " +
|
package/esm/sig/ld.js
CHANGED
@@ -73,7 +73,13 @@ export async function signJsonLd(jsonLd, privateKey, keyId, options) {
|
|
73
73
|
const signature = await createSignature(jsonLd, privateKey, keyId, options);
|
74
74
|
return attachSignature(jsonLd, signature);
|
75
75
|
}
|
76
|
-
|
76
|
+
/**
|
77
|
+
* Checks if the given JSON-LD document has a Linked Data Signature.
|
78
|
+
* @param jsonLd The JSON-LD document to check.
|
79
|
+
* @returns `true` if the document has a signature; `false` otherwise.
|
80
|
+
* @since 1.0.0
|
81
|
+
*/
|
82
|
+
export function hasSignature(jsonLd) {
|
77
83
|
if (typeof jsonLd !== "object" || jsonLd == null)
|
78
84
|
return false;
|
79
85
|
if ("signature" in jsonLd) {
|
package/esm/sig/mod.js
CHANGED
@@ -5,6 +5,6 @@
|
|
5
5
|
*/
|
6
6
|
export { signRequest, verifyRequest, } from "./http.js";
|
7
7
|
export { exportJwk, generateCryptoKeyPair, importJwk, } from "./key.js";
|
8
|
-
export
|
8
|
+
export { attachSignature, createSignature, detachSignature, signJsonLd, verifyJsonLd, verifySignature, } from "./ld.js";
|
9
9
|
export { doesActorOwnKey, getKeyOwner, } from "./owner.js";
|
10
10
|
export * from "./proof.js";
|
package/package.json
CHANGED
@@ -4,7 +4,7 @@ import type { Actor } from "../vocab/actor.js";
|
|
4
4
|
import type { Activity, CryptographicKey } from "../vocab/mod.js";
|
5
5
|
import type { Object } from "../vocab/vocab.js";
|
6
6
|
import type { PageItems } from "./collection.js";
|
7
|
-
import type { Context, RequestContext } from "./context.js";
|
7
|
+
import type { Context, InboxContext, RequestContext } from "./context.js";
|
8
8
|
import type { SenderKeyPair } from "./send.js";
|
9
9
|
/**
|
10
10
|
* A callback that dispatches a {@link NodeInfo} object.
|
@@ -86,12 +86,15 @@ export type CollectionCursor<TContext extends Context<TContextData>, TContextDat
|
|
86
86
|
*
|
87
87
|
* @typeParam TContextData The context data to pass to the {@link Context}.
|
88
88
|
* @typeParam TActivity The type of activity to listen for.
|
89
|
+
* @param context The inbox context.
|
90
|
+
* @param activity The activity that was received.
|
89
91
|
*/
|
90
|
-
export type InboxListener<TContextData, TActivity extends Activity> = (context:
|
92
|
+
export type InboxListener<TContextData, TActivity extends Activity> = (context: InboxContext<TContextData>, activity: TActivity) => void | Promise<void>;
|
91
93
|
/**
|
92
94
|
* A callback that handles errors in an inbox.
|
93
95
|
*
|
94
96
|
* @typeParam TContextData The context data to pass to the {@link Context}.
|
97
|
+
* @param context The inbox context.
|
95
98
|
*/
|
96
99
|
export type InboxErrorHandler<TContextData> = (context: Context<TContextData>, error: Error) => void | Promise<void>;
|
97
100
|
/**
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"callback.d.ts","sourceRoot":"","sources":["../../src/federation/callback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;
|
1
|
+
{"version":3,"file":"callback.d.ts","sourceRoot":"","sources":["../../src/federation/callback.ts"],"names":[],"mappings":"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"}
|