@fedify/fedify 1.0.0-dev.396 → 1.0.0-dev.398
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGES.md +17 -0
- package/esm/federation/federation.js +1 -0
- package/esm/federation/handler.js +3 -3
- package/esm/federation/middleware.js +222 -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/esm/webfinger/handler.js +11 -0
- 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 +52 -2
- 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/webfinger/handler.d.ts.map +1 -1
- package/types/x/hono.d.ts +1 -1
package/CHANGES.md
CHANGED
@@ -28,6 +28,22 @@ 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
|
+
- `Context.sendActivity()` method now adds Object Integrity Proofs to
|
40
|
+
the activity to be sent only once. It had added Object Integrity Proofs
|
41
|
+
to the activity for every recipient before.
|
42
|
+
|
43
|
+
- WebFinger responses now include <http://webfinger.net/rel/avatar> links
|
44
|
+
if the `Actor` object returned by the actor dispatcher has `icon`/`icons`
|
45
|
+
property.
|
46
|
+
|
31
47
|
- The `fedify inbox` command now sends `Delete(Application)` activities when
|
32
48
|
it's terminated so that the peers can clean up data related to the temporary
|
33
49
|
actor. [[#135]]
|
@@ -39,6 +55,7 @@ To be released.
|
|
39
55
|
|
40
56
|
[Linked Data Signatures]: https://web.archive.org/web/20170923124140/https://w3c-dvcg.github.io/ld-signatures/
|
41
57
|
[#135]: https://github.com/dahlia/fedify/issues/135
|
58
|
+
[#137]: https://github.com/dahlia/fedify/issues/137
|
42
59
|
|
43
60
|
|
44
61
|
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
|
}
|
@@ -1336,7 +1402,7 @@ class ContextImpl {
|
|
1336
1402
|
throw new Error("If recipients is 'followers', sender must be an actor handle.");
|
1337
1403
|
}
|
1338
1404
|
expandedRecipients = [];
|
1339
|
-
for await (const recipient of this
|
1405
|
+
for await (const recipient of this.getFollowers(sender.handle)) {
|
1340
1406
|
expandedRecipients.push(recipient);
|
1341
1407
|
}
|
1342
1408
|
const collectionId = this.federation.router.build("followers", sender);
|
@@ -1349,7 +1415,7 @@ class ContextImpl {
|
|
1349
1415
|
}
|
1350
1416
|
return await this.federation.sendActivity(keys, expandedRecipients, activity, opts);
|
1351
1417
|
}
|
1352
|
-
async
|
1418
|
+
async *getFollowers(handle) {
|
1353
1419
|
if (this.federation.followersCallbacks == null) {
|
1354
1420
|
throw new Error("No followers collection dispatcher registered.");
|
1355
1421
|
}
|
@@ -1450,6 +1516,120 @@ class RequestContextImpl extends ContextImpl {
|
|
1450
1516
|
return this.#signedKeyOwner = await getKeyOwner(key, this);
|
1451
1517
|
}
|
1452
1518
|
}
|
1519
|
+
export class InboxContextImpl extends ContextImpl {
|
1520
|
+
activity;
|
1521
|
+
constructor(activity, options) {
|
1522
|
+
super(options);
|
1523
|
+
this.activity = activity;
|
1524
|
+
}
|
1525
|
+
async forwardActivity(forwarder, recipients, options) {
|
1526
|
+
const logger = getLogger(["fedify", "federation", "inbox"]);
|
1527
|
+
let keys;
|
1528
|
+
if ("handle" in forwarder) {
|
1529
|
+
keys = await this.getKeyPairsFromHandle(forwarder.handle);
|
1530
|
+
if (keys.length < 1) {
|
1531
|
+
throw new Error(`No key pair found for actor ${JSON.stringify(forwarder.handle)}.`);
|
1532
|
+
}
|
1533
|
+
}
|
1534
|
+
else if (Array.isArray(forwarder)) {
|
1535
|
+
if (forwarder.length < 1) {
|
1536
|
+
throw new Error("The forwarder's key pairs are empty.");
|
1537
|
+
}
|
1538
|
+
keys = forwarder;
|
1539
|
+
}
|
1540
|
+
else {
|
1541
|
+
keys = [forwarder];
|
1542
|
+
}
|
1543
|
+
let activityId = undefined;
|
1544
|
+
if (!hasSignature(this.activity)) {
|
1545
|
+
let hasProof;
|
1546
|
+
try {
|
1547
|
+
const activity = await Activity.fromJsonLd(this.activity, this);
|
1548
|
+
activityId = activity.id?.href;
|
1549
|
+
hasProof = await activity.getProof() != null;
|
1550
|
+
}
|
1551
|
+
catch {
|
1552
|
+
hasProof = false;
|
1553
|
+
}
|
1554
|
+
if (!hasProof) {
|
1555
|
+
if (options?.skipIfUnsigned)
|
1556
|
+
return;
|
1557
|
+
logger.warn("The received activity {activityId} is not signed; even if it is " +
|
1558
|
+
"forwarded to other servers as is, it may not be accepted by " +
|
1559
|
+
"them due to the lack of a signature/proof.");
|
1560
|
+
}
|
1561
|
+
}
|
1562
|
+
if (activityId == null && typeof this.activity === "object" &&
|
1563
|
+
this.activity != null) {
|
1564
|
+
activityId =
|
1565
|
+
"@id" in this.activity && typeof this.activity["@id"] === "string"
|
1566
|
+
? this.activity["@id"]
|
1567
|
+
: "id" in this.activity && typeof this.activity.id === "string"
|
1568
|
+
? this.activity.id
|
1569
|
+
: undefined;
|
1570
|
+
}
|
1571
|
+
if (recipients === "followers") {
|
1572
|
+
if (!("handle" in forwarder)) {
|
1573
|
+
throw new Error("If recipients is 'followers', forwarder must be an actor handle.");
|
1574
|
+
}
|
1575
|
+
const followers = [];
|
1576
|
+
for await (const recipient of this.getFollowers(forwarder.handle)) {
|
1577
|
+
followers.push(recipient);
|
1578
|
+
}
|
1579
|
+
recipients = followers;
|
1580
|
+
}
|
1581
|
+
const inboxes = extractInboxes({
|
1582
|
+
recipients: Array.isArray(recipients) ? recipients : [recipients],
|
1583
|
+
preferSharedInbox: options?.preferSharedInbox,
|
1584
|
+
excludeBaseUris: options?.excludeBaseUris,
|
1585
|
+
});
|
1586
|
+
logger.debug("Forwarding activity {activityId} to inboxes:\n{inboxes}", {
|
1587
|
+
inboxes: globalThis.Object.keys(inboxes),
|
1588
|
+
activityId,
|
1589
|
+
activity: this.activity,
|
1590
|
+
});
|
1591
|
+
if (options?.immediate || this.federation.queue == null) {
|
1592
|
+
if (options?.immediate) {
|
1593
|
+
logger.debug("Forwarding activity immediately without queue since immediate " +
|
1594
|
+
"option is set.");
|
1595
|
+
}
|
1596
|
+
else {
|
1597
|
+
logger.debug("Forwarding activity immediately without queue since queue is not " +
|
1598
|
+
"set.");
|
1599
|
+
}
|
1600
|
+
const promises = [];
|
1601
|
+
for (const inbox in inboxes) {
|
1602
|
+
promises.push(sendActivity({
|
1603
|
+
keys,
|
1604
|
+
activity: this.activity,
|
1605
|
+
activityId: activityId,
|
1606
|
+
inbox: new URL(inbox),
|
1607
|
+
}));
|
1608
|
+
}
|
1609
|
+
await Promise.all(promises);
|
1610
|
+
return;
|
1611
|
+
}
|
1612
|
+
logger.debug("Enqueuing activity {activityId} to forward later.", { activityId, activity: this.activity });
|
1613
|
+
const keyJwkPairs = [];
|
1614
|
+
for (const { keyId, privateKey } of keys) {
|
1615
|
+
const privateKeyJwk = await exportJwk(privateKey);
|
1616
|
+
keyJwkPairs.push({ keyId: keyId.href, privateKey: privateKeyJwk });
|
1617
|
+
}
|
1618
|
+
for (const inbox in inboxes) {
|
1619
|
+
const message = {
|
1620
|
+
type: "outbox",
|
1621
|
+
keys: keyJwkPairs,
|
1622
|
+
activity: this.activity,
|
1623
|
+
activityId,
|
1624
|
+
inbox,
|
1625
|
+
started: new Date().toISOString(),
|
1626
|
+
attempt: 0,
|
1627
|
+
headers: {},
|
1628
|
+
};
|
1629
|
+
this.federation.queue.enqueue(message);
|
1630
|
+
}
|
1631
|
+
}
|
1632
|
+
}
|
1453
1633
|
function notFound(_request) {
|
1454
1634
|
return new Response("Not Found", { status: 404 });
|
1455
1635
|
}
|
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/esm/webfinger/handler.js
CHANGED
@@ -82,6 +82,17 @@ export async function handleWebFinger(request, { context, actorDispatcher, actor
|
|
82
82
|
});
|
83
83
|
}
|
84
84
|
}
|
85
|
+
for await (const image of actor.getIcons()) {
|
86
|
+
if (image.url?.href == null)
|
87
|
+
continue;
|
88
|
+
const link = {
|
89
|
+
rel: "http://webfinger.net/rel/avatar",
|
90
|
+
href: image.url.href.toString(),
|
91
|
+
};
|
92
|
+
if (image.mediaType != null)
|
93
|
+
link.type = image.mediaType;
|
94
|
+
links.push(link);
|
95
|
+
}
|
85
96
|
const jrd = {
|
86
97
|
subject: resourceUrl.href,
|
87
98
|
aliases: resourceUrl.href === context.getActorUri(handle).href
|
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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"}
|