@fedify/fedify 0.10.0-dev.187 → 0.10.0-dev.194
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 +24 -0
- package/esm/federation/handler.js +2 -4
- package/esm/federation/middleware.js +431 -270
- package/esm/runtime/key.js +23 -3
- package/esm/sig/key.js +44 -16
- package/esm/testing/fixtures/example.com/key4 +7 -0
- package/esm/testing/fixtures/example.com/person2 +6 -0
- package/package.json +2 -1
- package/types/federation/callback.d.ts +16 -0
- package/types/federation/callback.d.ts.map +1 -1
- package/types/federation/context.d.ts +24 -2
- package/types/federation/context.d.ts.map +1 -1
- package/types/federation/handler.d.ts.map +1 -1
- package/types/federation/middleware.d.ts +14 -4
- package/types/federation/middleware.d.ts.map +1 -1
- package/types/runtime/key.d.ts +2 -0
- package/types/runtime/key.d.ts.map +1 -1
- package/types/sig/key.d.ts +5 -2
- package/types/sig/key.d.ts.map +1 -1
- package/types/testing/context.d.ts.map +1 -1
- package/types/testing/keys.d.ts.map +1 -1
@@ -146,6 +146,11 @@ export class Federation {
|
|
146
146
|
logger.info("Successfully sent activity {activityId} to {inbox}.", { ...logData, activityId: activity?.id?.href });
|
147
147
|
}
|
148
148
|
createContext(urlOrRequest, contextData) {
|
149
|
+
return urlOrRequest instanceof Request
|
150
|
+
? this.#createContext(urlOrRequest, contextData)
|
151
|
+
: this.#createContext(urlOrRequest, contextData);
|
152
|
+
}
|
153
|
+
#createContext(urlOrRequest, contextData, opts = {}) {
|
149
154
|
const request = urlOrRequest instanceof Request ? urlOrRequest : null;
|
150
155
|
const url = urlOrRequest instanceof URL
|
151
156
|
? new URL(urlOrRequest)
|
@@ -157,252 +162,28 @@ export class Federation {
|
|
157
162
|
}
|
158
163
|
if (this.#treatHttps)
|
159
164
|
url.protocol = "https:";
|
160
|
-
const
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
if (keyPair == null) {
|
168
|
-
throw new Error(`No key pair found for actor ${JSON.stringify(handle)}`);
|
169
|
-
}
|
170
|
-
return {
|
171
|
-
keyId: new URL(`${context.getActorUri(handle)}#main-key`),
|
172
|
-
privateKey: keyPair.privateKey,
|
173
|
-
};
|
174
|
-
};
|
175
|
-
const getAuthenticatedDocumentLoader = this.#authenticatedDocumentLoaderFactory;
|
176
|
-
function getDocumentLoader(identity) {
|
177
|
-
if ("handle" in identity) {
|
178
|
-
const keyPair = getKeyPairFromHandle(identity.handle);
|
179
|
-
return keyPair.then((pair) => getAuthenticatedDocumentLoader(pair));
|
180
|
-
}
|
181
|
-
return getAuthenticatedDocumentLoader(identity);
|
182
|
-
}
|
183
|
-
const context = {
|
165
|
+
const ctxOptions = {
|
166
|
+
url,
|
167
|
+
federation: this,
|
168
|
+
router: this.#router,
|
169
|
+
objectTypeIds: this.#objectTypeIds,
|
170
|
+
objectCallbacks: this.#objectCallbacks,
|
171
|
+
actorCallbacks: this.#actorCallbacks,
|
184
172
|
data: contextData,
|
185
|
-
documentLoader: this.#documentLoader,
|
173
|
+
documentLoader: opts.documentLoader ?? this.#documentLoader,
|
186
174
|
contextLoader: this.#contextLoader,
|
187
|
-
|
188
|
-
const path = this.#router.build("nodeInfo", {});
|
189
|
-
if (path == null) {
|
190
|
-
throw new RouterError("No NodeInfo dispatcher registered.");
|
191
|
-
}
|
192
|
-
return new URL(path, url);
|
193
|
-
},
|
194
|
-
getActorUri: (handle) => {
|
195
|
-
const path = this.#router.build("actor", { handle });
|
196
|
-
if (path == null) {
|
197
|
-
throw new RouterError("No actor dispatcher registered.");
|
198
|
-
}
|
199
|
-
return new URL(path, url);
|
200
|
-
},
|
201
|
-
getObjectUri: (
|
202
|
-
// deno-lint-ignore no-explicit-any
|
203
|
-
cls, values) => {
|
204
|
-
const callbacks = this.#objectCallbacks[cls.typeId.href];
|
205
|
-
if (callbacks == null) {
|
206
|
-
throw new RouterError("No object dispatcher registered.");
|
207
|
-
}
|
208
|
-
for (const param of callbacks.parameters) {
|
209
|
-
if (!(param in values)) {
|
210
|
-
throw new TypeError(`Missing parameter: ${param}`);
|
211
|
-
}
|
212
|
-
}
|
213
|
-
const path = this.#router.build(`object:${cls.typeId.href}`, values);
|
214
|
-
if (path == null) {
|
215
|
-
throw new RouterError("No object dispatcher registered.");
|
216
|
-
}
|
217
|
-
return new URL(path, url);
|
218
|
-
},
|
219
|
-
getOutboxUri: (handle) => {
|
220
|
-
const path = this.#router.build("outbox", { handle });
|
221
|
-
if (path == null) {
|
222
|
-
throw new RouterError("No outbox dispatcher registered.");
|
223
|
-
}
|
224
|
-
return new URL(path, url);
|
225
|
-
},
|
226
|
-
getInboxUri: (handle) => {
|
227
|
-
if (handle == null) {
|
228
|
-
const path = this.#router.build("sharedInbox", {});
|
229
|
-
if (path == null) {
|
230
|
-
throw new RouterError("No shared inbox path registered.");
|
231
|
-
}
|
232
|
-
return new URL(path, url);
|
233
|
-
}
|
234
|
-
const path = this.#router.build("inbox", { handle });
|
235
|
-
if (path == null) {
|
236
|
-
throw new RouterError("No inbox path registered.");
|
237
|
-
}
|
238
|
-
return new URL(path, url);
|
239
|
-
},
|
240
|
-
getFollowingUri: (handle) => {
|
241
|
-
const path = this.#router.build("following", { handle });
|
242
|
-
if (path == null) {
|
243
|
-
throw new RouterError("No following collection path registered.");
|
244
|
-
}
|
245
|
-
return new URL(path, url);
|
246
|
-
},
|
247
|
-
getFollowersUri: (handle) => {
|
248
|
-
const path = this.#router.build("followers", { handle });
|
249
|
-
if (path == null) {
|
250
|
-
throw new RouterError("No followers collection path registered.");
|
251
|
-
}
|
252
|
-
return new URL(path, url);
|
253
|
-
},
|
254
|
-
parseUri: (uri) => {
|
255
|
-
if (uri.origin !== url.origin)
|
256
|
-
return null;
|
257
|
-
const route = this.#router.route(uri.pathname);
|
258
|
-
if (route == null)
|
259
|
-
return null;
|
260
|
-
else if (route.name === "actor") {
|
261
|
-
return { type: "actor", handle: route.values.handle };
|
262
|
-
}
|
263
|
-
else if (route.name.startsWith("object:")) {
|
264
|
-
const typeId = route.name.replace(/^object:/, "");
|
265
|
-
return {
|
266
|
-
type: "object",
|
267
|
-
class: this.#objectTypeIds[typeId],
|
268
|
-
typeId: new URL(typeId),
|
269
|
-
values: route.values,
|
270
|
-
};
|
271
|
-
}
|
272
|
-
else if (route.name === "inbox") {
|
273
|
-
return { type: "inbox", handle: route.values.handle };
|
274
|
-
}
|
275
|
-
else if (route.name === "sharedInbox") {
|
276
|
-
return { type: "inbox" };
|
277
|
-
}
|
278
|
-
else if (route.name === "outbox") {
|
279
|
-
return { type: "outbox", handle: route.values.handle };
|
280
|
-
}
|
281
|
-
else if (route.name === "following") {
|
282
|
-
return { type: "following", handle: route.values.handle };
|
283
|
-
}
|
284
|
-
else if (route.name === "followers") {
|
285
|
-
return { type: "followers", handle: route.values.handle };
|
286
|
-
}
|
287
|
-
return null;
|
288
|
-
},
|
289
|
-
getHandleFromActorUri(actorUri) {
|
290
|
-
getLogger(["fedify", "federation"]).warn("Context.getHandleFromActorUri() is deprecated; " +
|
291
|
-
"use Context.parseUri() instead.");
|
292
|
-
const result = this.parseUri(actorUri);
|
293
|
-
if (result?.type === "actor")
|
294
|
-
return result.handle;
|
295
|
-
return null;
|
296
|
-
},
|
297
|
-
getActorKey: async (handle) => {
|
298
|
-
let keyPair = this.#actorCallbacks?.keyPairDispatcher?.(contextData, handle);
|
299
|
-
if (keyPair instanceof Promise)
|
300
|
-
keyPair = await keyPair;
|
301
|
-
if (keyPair == null)
|
302
|
-
return null;
|
303
|
-
return new CryptographicKey({
|
304
|
-
id: new URL(`${context.getActorUri(handle)}#main-key`),
|
305
|
-
owner: context.getActorUri(handle),
|
306
|
-
publicKey: keyPair.publicKey,
|
307
|
-
});
|
308
|
-
},
|
309
|
-
getDocumentLoader,
|
310
|
-
sendActivity: async (sender, recipients, activity, options = {}) => {
|
311
|
-
const senderPair = "handle" in sender
|
312
|
-
? await getKeyPairFromHandle(sender.handle)
|
313
|
-
: sender;
|
314
|
-
const opts = { ...options };
|
315
|
-
let expandedRecipients;
|
316
|
-
if (Array.isArray(recipients)) {
|
317
|
-
expandedRecipients = recipients;
|
318
|
-
}
|
319
|
-
else if (recipients === "followers") {
|
320
|
-
if (!("handle" in sender)) {
|
321
|
-
throw new Error("If recipients is 'followers', sender must be an actor handle.");
|
322
|
-
}
|
323
|
-
expandedRecipients = [];
|
324
|
-
for await (const recipient of this.#getFollowers(reqCtx, sender.handle)) {
|
325
|
-
expandedRecipients.push(recipient);
|
326
|
-
}
|
327
|
-
const collectionId = this.#router.build("followers", sender);
|
328
|
-
opts.collectionSync = collectionId == null
|
329
|
-
? undefined
|
330
|
-
: new URL(collectionId, url).href;
|
331
|
-
}
|
332
|
-
else {
|
333
|
-
expandedRecipients = [recipients];
|
334
|
-
}
|
335
|
-
return await this.sendActivity(senderPair, expandedRecipients, activity, opts);
|
336
|
-
},
|
175
|
+
authenticatedDocumentLoaderFactory: this.#authenticatedDocumentLoaderFactory,
|
337
176
|
};
|
338
177
|
if (request == null)
|
339
|
-
return
|
340
|
-
|
341
|
-
|
342
|
-
const timeWindow = this.#signatureTimeWindow;
|
343
|
-
const reqCtx = {
|
344
|
-
...context,
|
178
|
+
return new ContextImpl(ctxOptions);
|
179
|
+
return new RequestContextImpl({
|
180
|
+
...ctxOptions,
|
345
181
|
request,
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
}
|
352
|
-
return await this.#actorCallbacks.dispatcher({
|
353
|
-
...reqCtx,
|
354
|
-
getActor(handle2) {
|
355
|
-
getLogger(["fedify", "federation"]).warn("RequestContext.getActor({getActorHandle}) is invoked from " +
|
356
|
-
"the actor dispatcher ({actorDispatcherHandle}); " +
|
357
|
-
"this may cause an infinite loop.", { getActorHandle: handle2, actorDispatcherHandle: handle });
|
358
|
-
return reqCtx.getActor(handle2);
|
359
|
-
},
|
360
|
-
}, handle, await context.getActorKey(handle));
|
361
|
-
},
|
362
|
-
getObject: async (cls, values) => {
|
363
|
-
const callbacks = this.#objectCallbacks[cls.typeId.href];
|
364
|
-
if (callbacks == null) {
|
365
|
-
throw new Error("No object dispatcher registered.");
|
366
|
-
}
|
367
|
-
for (const param of callbacks.parameters) {
|
368
|
-
if (!(param in values)) {
|
369
|
-
throw new TypeError(`Missing parameter: ${param}`);
|
370
|
-
}
|
371
|
-
}
|
372
|
-
return await callbacks.dispatcher({
|
373
|
-
...reqCtx,
|
374
|
-
getObject(cls2, values2) {
|
375
|
-
getLogger(["fedify", "federation"]).warn("RequestContext.getObject({getObjectClass}, " +
|
376
|
-
"{getObjectValues}) is invoked from the object dispatcher " +
|
377
|
-
"({actorDispatcherClass}, {actorDispatcherValues}); " +
|
378
|
-
"this may cause an infinite loop.", {
|
379
|
-
getObjectClass: cls2.name,
|
380
|
-
getObjectValues: values2,
|
381
|
-
actorDispatcherClass: cls.name,
|
382
|
-
actorDispatcherValues: values,
|
383
|
-
});
|
384
|
-
return reqCtx.getObject(cls2, values2);
|
385
|
-
},
|
386
|
-
}, values);
|
387
|
-
},
|
388
|
-
async getSignedKey() {
|
389
|
-
if (signedKey !== undefined)
|
390
|
-
return signedKey;
|
391
|
-
return signedKey = await verifyRequest(request, {
|
392
|
-
...context,
|
393
|
-
timeWindow,
|
394
|
-
});
|
395
|
-
},
|
396
|
-
async getSignedKeyOwner() {
|
397
|
-
if (signedKeyOwner !== undefined)
|
398
|
-
return signedKeyOwner;
|
399
|
-
const key = await this.getSignedKey();
|
400
|
-
if (key == null)
|
401
|
-
return signedKeyOwner = null;
|
402
|
-
return signedKeyOwner = await getKeyOwner(key, context);
|
403
|
-
},
|
404
|
-
};
|
405
|
-
return reqCtx;
|
182
|
+
signatureTimeWindow: this.#signatureTimeWindow,
|
183
|
+
followersCallbacks: this.#followersCallbacks,
|
184
|
+
invokedFromActorDispatcher: opts.invokedFromActorDispatcher,
|
185
|
+
invokedFromObjectDispatcher: opts.invokedFromObjectDispatcher,
|
186
|
+
});
|
406
187
|
}
|
407
188
|
/**
|
408
189
|
* Registers a NodeInfo dispatcher.
|
@@ -431,7 +212,7 @@ export class Federation {
|
|
431
212
|
* ``` typescript
|
432
213
|
* federation.setActorDispatcher(
|
433
214
|
* "/users/{handle}",
|
434
|
-
* async (ctx, handle
|
215
|
+
* async (ctx, handle) => {
|
435
216
|
* return new Person({
|
436
217
|
* id: ctx.getActorUri(handle),
|
437
218
|
* preferredUsername: handle,
|
@@ -531,8 +312,20 @@ export class Federation {
|
|
531
312
|
};
|
532
313
|
this.#actorCallbacks = callbacks;
|
533
314
|
const setters = {
|
315
|
+
setKeyPairsDispatcher(dispatcher) {
|
316
|
+
callbacks.keyPairsDispatcher = dispatcher;
|
317
|
+
return setters;
|
318
|
+
},
|
534
319
|
setKeyPairDispatcher(dispatcher) {
|
535
|
-
|
320
|
+
getLogger(["fedify", "federation", "actor"]).warn("The ActorCallbackSetters.setKeyPairDispatcher() method is " +
|
321
|
+
"deprecated. Use the ActorCallbackSetters.setKeyPairsDispatcher() " +
|
322
|
+
"instead.");
|
323
|
+
callbacks.keyPairsDispatcher = async (ctxData, handle) => {
|
324
|
+
const key = await dispatcher(ctxData, handle);
|
325
|
+
if (key == null)
|
326
|
+
return [];
|
327
|
+
return [key];
|
328
|
+
};
|
536
329
|
return setters;
|
537
330
|
},
|
538
331
|
authorize(predicate) {
|
@@ -708,29 +501,6 @@ export class Federation {
|
|
708
501
|
};
|
709
502
|
return setters;
|
710
503
|
}
|
711
|
-
async *#getFollowers(context, handle) {
|
712
|
-
if (this.#followersCallbacks == null) {
|
713
|
-
throw new Error("No followers collection dispatcher registered.");
|
714
|
-
}
|
715
|
-
const result = await this.#followersCallbacks.dispatcher(context, handle, null);
|
716
|
-
if (result != null) {
|
717
|
-
for (const recipient of result.items)
|
718
|
-
yield recipient;
|
719
|
-
return;
|
720
|
-
}
|
721
|
-
if (this.#followersCallbacks.firstCursor == null) {
|
722
|
-
throw new Error("No first cursor dispatcher registered for followers collection.");
|
723
|
-
}
|
724
|
-
let cursor = await this.#followersCallbacks.firstCursor(context, handle);
|
725
|
-
while (cursor != null) {
|
726
|
-
const result = await this.#followersCallbacks.dispatcher(context, handle, cursor);
|
727
|
-
if (result == null)
|
728
|
-
break;
|
729
|
-
for (const recipient of result.items)
|
730
|
-
yield recipient;
|
731
|
-
cursor = result.nextCursor ?? null;
|
732
|
-
}
|
733
|
-
}
|
734
504
|
/**
|
735
505
|
* Assigns the URL path for the inbox and starts setting inbox listeners.
|
736
506
|
*
|
@@ -926,7 +696,7 @@ export class Federation {
|
|
926
696
|
const response = onNotFound(request);
|
927
697
|
return response instanceof Promise ? await response : response;
|
928
698
|
}
|
929
|
-
let context = this
|
699
|
+
let context = this.#createContext(request, contextData);
|
930
700
|
switch (route.name.replace(/:.*$/, "")) {
|
931
701
|
case "webfinger":
|
932
702
|
return await handleWebFinger(request, {
|
@@ -942,6 +712,9 @@ export class Federation {
|
|
942
712
|
nodeInfoDispatcher: this.#nodeInfoDispatcher,
|
943
713
|
});
|
944
714
|
case "actor":
|
715
|
+
context = this.#createContext(request, contextData, {
|
716
|
+
invokedFromActorDispatcher: { handle: route.values.handle },
|
717
|
+
});
|
945
718
|
return await handleActor(request, {
|
946
719
|
handle: route.values.handle,
|
947
720
|
context,
|
@@ -954,6 +727,10 @@ export class Federation {
|
|
954
727
|
case "object": {
|
955
728
|
const typeId = route.name.replace(/^object:/, "");
|
956
729
|
const callbacks = this.#objectCallbacks[typeId];
|
730
|
+
const cls = this.#objectTypeIds[typeId];
|
731
|
+
context = this.#createContext(request, contextData, {
|
732
|
+
invokedFromObjectDispatcher: { cls, values: route.values },
|
733
|
+
});
|
957
734
|
return await handleObject(request, {
|
958
735
|
values: route.values,
|
959
736
|
context,
|
@@ -975,12 +752,11 @@ export class Federation {
|
|
975
752
|
onNotAcceptable,
|
976
753
|
});
|
977
754
|
case "inbox":
|
978
|
-
context = {
|
979
|
-
...context,
|
755
|
+
context = this.#createContext(request, contextData, {
|
980
756
|
documentLoader: await context.getDocumentLoader({
|
981
757
|
handle: route.values.handle,
|
982
758
|
}),
|
983
|
-
};
|
759
|
+
});
|
984
760
|
// falls through
|
985
761
|
case "sharedInbox":
|
986
762
|
return await handleInbox(request, {
|
@@ -1031,6 +807,391 @@ export class Federation {
|
|
1031
807
|
}
|
1032
808
|
}
|
1033
809
|
}
|
810
|
+
class ContextImpl {
|
811
|
+
#url;
|
812
|
+
#federation;
|
813
|
+
#router;
|
814
|
+
#objectTypeIds;
|
815
|
+
objectCallbacks;
|
816
|
+
actorCallbacks;
|
817
|
+
data;
|
818
|
+
documentLoader;
|
819
|
+
contextLoader;
|
820
|
+
#authenticatedDocumentLoaderFactory;
|
821
|
+
constructor({ url, federation, router, objectTypeIds, objectCallbacks, actorCallbacks, data, documentLoader, contextLoader, authenticatedDocumentLoaderFactory, }) {
|
822
|
+
this.#url = url;
|
823
|
+
this.#federation = federation;
|
824
|
+
this.#router = router;
|
825
|
+
this.#objectTypeIds = objectTypeIds;
|
826
|
+
this.objectCallbacks = objectCallbacks;
|
827
|
+
this.actorCallbacks = actorCallbacks;
|
828
|
+
this.data = data;
|
829
|
+
this.documentLoader = documentLoader;
|
830
|
+
this.contextLoader = contextLoader;
|
831
|
+
this.#authenticatedDocumentLoaderFactory =
|
832
|
+
authenticatedDocumentLoaderFactory;
|
833
|
+
}
|
834
|
+
getNodeInfoUri() {
|
835
|
+
const path = this.#router.build("nodeInfo", {});
|
836
|
+
if (path == null) {
|
837
|
+
throw new RouterError("No NodeInfo dispatcher registered.");
|
838
|
+
}
|
839
|
+
return new URL(path, this.#url);
|
840
|
+
}
|
841
|
+
getActorUri(handle) {
|
842
|
+
const path = this.#router.build("actor", { handle });
|
843
|
+
if (path == null) {
|
844
|
+
throw new RouterError("No actor dispatcher registered.");
|
845
|
+
}
|
846
|
+
return new URL(path, this.#url);
|
847
|
+
}
|
848
|
+
getObjectUri(
|
849
|
+
// deno-lint-ignore no-explicit-any
|
850
|
+
cls, values) {
|
851
|
+
const callbacks = this.objectCallbacks[cls.typeId.href];
|
852
|
+
if (callbacks == null) {
|
853
|
+
throw new RouterError("No object dispatcher registered.");
|
854
|
+
}
|
855
|
+
for (const param of callbacks.parameters) {
|
856
|
+
if (!(param in values)) {
|
857
|
+
throw new TypeError(`Missing parameter: ${param}`);
|
858
|
+
}
|
859
|
+
}
|
860
|
+
const path = this.#router.build(`object:${cls.typeId.href}`, values);
|
861
|
+
if (path == null) {
|
862
|
+
throw new RouterError("No object dispatcher registered.");
|
863
|
+
}
|
864
|
+
return new URL(path, this.#url);
|
865
|
+
}
|
866
|
+
getOutboxUri(handle) {
|
867
|
+
const path = this.#router.build("outbox", { handle });
|
868
|
+
if (path == null) {
|
869
|
+
throw new RouterError("No outbox dispatcher registered.");
|
870
|
+
}
|
871
|
+
return new URL(path, this.#url);
|
872
|
+
}
|
873
|
+
getInboxUri(handle) {
|
874
|
+
if (handle == null) {
|
875
|
+
const path = this.#router.build("sharedInbox", {});
|
876
|
+
if (path == null) {
|
877
|
+
throw new RouterError("No shared inbox path registered.");
|
878
|
+
}
|
879
|
+
return new URL(path, this.#url);
|
880
|
+
}
|
881
|
+
const path = this.#router.build("inbox", { handle });
|
882
|
+
if (path == null) {
|
883
|
+
throw new RouterError("No inbox path registered.");
|
884
|
+
}
|
885
|
+
return new URL(path, this.#url);
|
886
|
+
}
|
887
|
+
getFollowingUri(handle) {
|
888
|
+
const path = this.#router.build("following", { handle });
|
889
|
+
if (path == null) {
|
890
|
+
throw new RouterError("No following collection path registered.");
|
891
|
+
}
|
892
|
+
return new URL(path, this.#url);
|
893
|
+
}
|
894
|
+
getFollowersUri(handle) {
|
895
|
+
const path = this.#router.build("followers", { handle });
|
896
|
+
if (path == null) {
|
897
|
+
throw new RouterError("No followers collection path registered.");
|
898
|
+
}
|
899
|
+
return new URL(path, this.#url);
|
900
|
+
}
|
901
|
+
parseUri(uri) {
|
902
|
+
if (uri.origin !== this.#url.origin)
|
903
|
+
return null;
|
904
|
+
const route = this.#router.route(uri.pathname);
|
905
|
+
if (route == null)
|
906
|
+
return null;
|
907
|
+
else if (route.name === "actor") {
|
908
|
+
return { type: "actor", handle: route.values.handle };
|
909
|
+
}
|
910
|
+
else if (route.name.startsWith("object:")) {
|
911
|
+
const typeId = route.name.replace(/^object:/, "");
|
912
|
+
return {
|
913
|
+
type: "object",
|
914
|
+
class: this.#objectTypeIds[typeId],
|
915
|
+
typeId: new URL(typeId),
|
916
|
+
values: route.values,
|
917
|
+
};
|
918
|
+
}
|
919
|
+
else if (route.name === "inbox") {
|
920
|
+
return { type: "inbox", handle: route.values.handle };
|
921
|
+
}
|
922
|
+
else if (route.name === "sharedInbox") {
|
923
|
+
return { type: "inbox" };
|
924
|
+
}
|
925
|
+
else if (route.name === "outbox") {
|
926
|
+
return { type: "outbox", handle: route.values.handle };
|
927
|
+
}
|
928
|
+
else if (route.name === "following") {
|
929
|
+
return { type: "following", handle: route.values.handle };
|
930
|
+
}
|
931
|
+
else if (route.name === "followers") {
|
932
|
+
return { type: "followers", handle: route.values.handle };
|
933
|
+
}
|
934
|
+
return null;
|
935
|
+
}
|
936
|
+
getHandleFromActorUri(actorUri) {
|
937
|
+
getLogger(["fedify", "federation"]).warn("Context.getHandleFromActorUri() is deprecated; " +
|
938
|
+
"use Context.parseUri() instead.");
|
939
|
+
const result = this.parseUri(actorUri);
|
940
|
+
if (result?.type === "actor")
|
941
|
+
return result.handle;
|
942
|
+
return null;
|
943
|
+
}
|
944
|
+
async getActorKeyPairs(handle) {
|
945
|
+
let keyPairs;
|
946
|
+
try {
|
947
|
+
keyPairs = await this.getKeyPairsFromHandle(this.#url, this.data, handle);
|
948
|
+
}
|
949
|
+
catch (_) {
|
950
|
+
getLogger(["fedify", "federation", "actor"])
|
951
|
+
.warn("No actor key pairs dispatcher registered.");
|
952
|
+
return [];
|
953
|
+
}
|
954
|
+
const owner = this.getActorUri(handle);
|
955
|
+
const result = [];
|
956
|
+
for (const keyPair of keyPairs) {
|
957
|
+
const newPair = {
|
958
|
+
...keyPair,
|
959
|
+
cryptographicKey: new CryptographicKey({
|
960
|
+
id: keyPair.keyId,
|
961
|
+
owner,
|
962
|
+
publicKey: keyPair.publicKey,
|
963
|
+
}),
|
964
|
+
};
|
965
|
+
result.push(newPair);
|
966
|
+
}
|
967
|
+
return result;
|
968
|
+
}
|
969
|
+
async getKeyPairsFromHandle(url, contextData, handle) {
|
970
|
+
const logger = getLogger(["fedify", "federation", "actor"]);
|
971
|
+
if (this.actorCallbacks?.keyPairsDispatcher == null) {
|
972
|
+
throw new Error("No actor key pairs dispatcher registered.");
|
973
|
+
}
|
974
|
+
const path = this.#router.build("actor", { handle });
|
975
|
+
if (path == null) {
|
976
|
+
logger.warn("No actor dispatcher registered.");
|
977
|
+
return [];
|
978
|
+
}
|
979
|
+
const actorUri = new URL(path, url);
|
980
|
+
const keyPairs = await this.actorCallbacks?.keyPairsDispatcher(contextData, handle);
|
981
|
+
if (keyPairs.length < 1) {
|
982
|
+
logger.warn("No key pairs found for actor {handle}.", { handle });
|
983
|
+
}
|
984
|
+
let i = 0;
|
985
|
+
const result = [];
|
986
|
+
for (const keyPair of keyPairs) {
|
987
|
+
result.push({
|
988
|
+
...keyPair,
|
989
|
+
keyId: new URL(
|
990
|
+
// For backwards compatibility, the first key is always the #main-key:
|
991
|
+
i == 0 ? `#main-key` : `#key-${i + 1}`, actorUri),
|
992
|
+
});
|
993
|
+
i++;
|
994
|
+
}
|
995
|
+
return result;
|
996
|
+
}
|
997
|
+
async getActorKey(handle) {
|
998
|
+
getLogger(["fedify", "federation", "actor"]).warn("Context.getActorKey() method is deprecated; " +
|
999
|
+
"use Context.getActorKeyPairs() method instead.");
|
1000
|
+
let keyPair;
|
1001
|
+
try {
|
1002
|
+
keyPair = await this.getRsaKeyPairFromHandle(handle);
|
1003
|
+
}
|
1004
|
+
catch (_) {
|
1005
|
+
return null;
|
1006
|
+
}
|
1007
|
+
if (keyPair == null)
|
1008
|
+
return null;
|
1009
|
+
return new CryptographicKey({
|
1010
|
+
id: keyPair.keyId,
|
1011
|
+
owner: this.getActorUri(handle),
|
1012
|
+
publicKey: keyPair.publicKey,
|
1013
|
+
});
|
1014
|
+
}
|
1015
|
+
async getRsaKeyPairFromHandle(handle) {
|
1016
|
+
const keyPairs = await this.getKeyPairsFromHandle(this.#url, this.data, handle);
|
1017
|
+
for (const keyPair of keyPairs) {
|
1018
|
+
const { privateKey } = keyPair;
|
1019
|
+
if (privateKey.algorithm.name === "RSASSA-PKCS1-v1_5" &&
|
1020
|
+
privateKey.algorithm.hash
|
1021
|
+
.name ===
|
1022
|
+
"SHA-256") {
|
1023
|
+
return keyPair;
|
1024
|
+
}
|
1025
|
+
}
|
1026
|
+
getLogger(["fedify", "federation", "actor"]).warn("No RSA-PKCS#1-v1.5 SHA-256 key found for actor {handle}.", { handle });
|
1027
|
+
return null;
|
1028
|
+
}
|
1029
|
+
getDocumentLoader(identity) {
|
1030
|
+
if ("handle" in identity) {
|
1031
|
+
const keyPair = this.getRsaKeyPairFromHandle(identity.handle);
|
1032
|
+
return keyPair.then((pair) => pair == null
|
1033
|
+
? this.documentLoader
|
1034
|
+
: this.#authenticatedDocumentLoaderFactory(pair));
|
1035
|
+
}
|
1036
|
+
return this.#authenticatedDocumentLoaderFactory(identity);
|
1037
|
+
}
|
1038
|
+
async sendActivity(sender, recipients, activity, options = {}) {
|
1039
|
+
let senderPair;
|
1040
|
+
if ("handle" in sender) {
|
1041
|
+
const keyPair = await this.getRsaKeyPairFromHandle(sender.handle);
|
1042
|
+
if (keyPair == null) {
|
1043
|
+
throw new Error(`No key pair found for actor ${sender.handle}`);
|
1044
|
+
}
|
1045
|
+
senderPair = keyPair;
|
1046
|
+
}
|
1047
|
+
else {
|
1048
|
+
senderPair = sender;
|
1049
|
+
}
|
1050
|
+
const opts = { ...options };
|
1051
|
+
let expandedRecipients;
|
1052
|
+
if (Array.isArray(recipients)) {
|
1053
|
+
expandedRecipients = recipients;
|
1054
|
+
}
|
1055
|
+
else if (recipients === "followers") {
|
1056
|
+
if (!("handle" in sender)) {
|
1057
|
+
throw new Error("If recipients is 'followers', sender must be an actor handle.");
|
1058
|
+
}
|
1059
|
+
expandedRecipients = [];
|
1060
|
+
for await (const recipient of this.getFollowers(sender.handle)) {
|
1061
|
+
expandedRecipients.push(recipient);
|
1062
|
+
}
|
1063
|
+
const collectionId = this.#router.build("followers", sender);
|
1064
|
+
opts.collectionSync = collectionId == null
|
1065
|
+
? undefined
|
1066
|
+
: new URL(collectionId, this.#url).href;
|
1067
|
+
}
|
1068
|
+
else {
|
1069
|
+
expandedRecipients = [recipients];
|
1070
|
+
}
|
1071
|
+
return await this.#federation.sendActivity(senderPair, expandedRecipients, activity, opts);
|
1072
|
+
}
|
1073
|
+
getFollowers(_handle) {
|
1074
|
+
throw new Error('"followers" recipients are not supported in Context. ' +
|
1075
|
+
"Use RequestContext instead.");
|
1076
|
+
}
|
1077
|
+
}
|
1078
|
+
class RequestContextImpl extends ContextImpl {
|
1079
|
+
#options;
|
1080
|
+
#followersCallbacks;
|
1081
|
+
#signatureTimeWindow;
|
1082
|
+
#invokedFromActorDispatcher;
|
1083
|
+
#invokedFromObjectDispatcher;
|
1084
|
+
request;
|
1085
|
+
url;
|
1086
|
+
constructor(options) {
|
1087
|
+
super(options);
|
1088
|
+
this.#options = options;
|
1089
|
+
this.#followersCallbacks = options.followersCallbacks;
|
1090
|
+
this.#signatureTimeWindow = options.signatureTimeWindow;
|
1091
|
+
this.#invokedFromActorDispatcher = options.invokedFromActorDispatcher;
|
1092
|
+
this.#invokedFromObjectDispatcher = options.invokedFromObjectDispatcher;
|
1093
|
+
this.request = options.request;
|
1094
|
+
this.url = options.url;
|
1095
|
+
}
|
1096
|
+
async *getFollowers(handle) {
|
1097
|
+
if (this.#followersCallbacks == null) {
|
1098
|
+
throw new Error("No followers collection dispatcher registered.");
|
1099
|
+
}
|
1100
|
+
const result = await this.#followersCallbacks.dispatcher(this, handle, null);
|
1101
|
+
if (result != null) {
|
1102
|
+
for (const recipient of result.items)
|
1103
|
+
yield recipient;
|
1104
|
+
return;
|
1105
|
+
}
|
1106
|
+
if (this.#followersCallbacks.firstCursor == null) {
|
1107
|
+
throw new Error("No first cursor dispatcher registered for followers collection.");
|
1108
|
+
}
|
1109
|
+
let cursor = await this.#followersCallbacks.firstCursor(this, handle);
|
1110
|
+
while (cursor != null) {
|
1111
|
+
const result = await this.#followersCallbacks.dispatcher(this, handle, cursor);
|
1112
|
+
if (result == null)
|
1113
|
+
break;
|
1114
|
+
for (const recipient of result.items)
|
1115
|
+
yield recipient;
|
1116
|
+
cursor = result.nextCursor ?? null;
|
1117
|
+
}
|
1118
|
+
}
|
1119
|
+
async getActor(handle) {
|
1120
|
+
if (this.actorCallbacks == null ||
|
1121
|
+
this.actorCallbacks.dispatcher == null) {
|
1122
|
+
throw new Error("No actor dispatcher registered.");
|
1123
|
+
}
|
1124
|
+
if (this.#invokedFromActorDispatcher != null) {
|
1125
|
+
getLogger(["fedify", "federation"]).warn("RequestContext.getActor({getActorHandle}) is invoked from " +
|
1126
|
+
"the actor dispatcher ({actorDispatcherHandle}); " +
|
1127
|
+
"this may cause an infinite loop.", {
|
1128
|
+
getActorHandle: handle,
|
1129
|
+
actorDispatcherHandle: this.#invokedFromActorDispatcher.handle,
|
1130
|
+
});
|
1131
|
+
}
|
1132
|
+
let rsaKey;
|
1133
|
+
try {
|
1134
|
+
rsaKey = await this.getRsaKeyPairFromHandle(handle);
|
1135
|
+
}
|
1136
|
+
catch (_) {
|
1137
|
+
rsaKey = null;
|
1138
|
+
}
|
1139
|
+
return await this.actorCallbacks.dispatcher(new RequestContextImpl({
|
1140
|
+
...this.#options,
|
1141
|
+
invokedFromActorDispatcher: { handle },
|
1142
|
+
}), handle, rsaKey == null ? null : new CryptographicKey({
|
1143
|
+
id: rsaKey.keyId,
|
1144
|
+
owner: this.getActorUri(handle),
|
1145
|
+
publicKey: rsaKey.publicKey,
|
1146
|
+
}));
|
1147
|
+
}
|
1148
|
+
async getObject(
|
1149
|
+
// deno-lint-ignore no-explicit-any
|
1150
|
+
cls, values) {
|
1151
|
+
const callbacks = this.objectCallbacks[cls.typeId.href];
|
1152
|
+
if (callbacks == null) {
|
1153
|
+
throw new Error("No object dispatcher registered.");
|
1154
|
+
}
|
1155
|
+
for (const param of callbacks.parameters) {
|
1156
|
+
if (!(param in values)) {
|
1157
|
+
throw new TypeError(`Missing parameter: ${param}`);
|
1158
|
+
}
|
1159
|
+
}
|
1160
|
+
if (this.#invokedFromObjectDispatcher != null) {
|
1161
|
+
getLogger(["fedify", "federation"]).warn("RequestContext.getObject({getObjectClass}, " +
|
1162
|
+
"{getObjectValues}) is invoked from the object dispatcher " +
|
1163
|
+
"({actorDispatcherClass}, {actorDispatcherValues}); " +
|
1164
|
+
"this may cause an infinite loop.", {
|
1165
|
+
getObjectClass: cls.name,
|
1166
|
+
getObjectValues: values,
|
1167
|
+
actorDispatcherClass: this.#invokedFromObjectDispatcher.cls.name,
|
1168
|
+
actorDispatcherValues: this.#invokedFromObjectDispatcher.values,
|
1169
|
+
});
|
1170
|
+
}
|
1171
|
+
return await callbacks.dispatcher(new RequestContextImpl({
|
1172
|
+
...this.#options,
|
1173
|
+
invokedFromObjectDispatcher: { cls, values },
|
1174
|
+
}), values);
|
1175
|
+
}
|
1176
|
+
#signedKey = undefined;
|
1177
|
+
async getSignedKey() {
|
1178
|
+
if (this.#signedKey !== undefined)
|
1179
|
+
return this.#signedKey;
|
1180
|
+
return this.#signedKey = await verifyRequest(this.request, {
|
1181
|
+
...this,
|
1182
|
+
timeWindow: this.#signatureTimeWindow,
|
1183
|
+
});
|
1184
|
+
}
|
1185
|
+
#signedKeyOwner = undefined;
|
1186
|
+
async getSignedKeyOwner() {
|
1187
|
+
if (this.#signedKeyOwner !== undefined)
|
1188
|
+
return this.#signedKeyOwner;
|
1189
|
+
const key = await this.getSignedKey();
|
1190
|
+
if (key == null)
|
1191
|
+
return this.#signedKeyOwner = null;
|
1192
|
+
return this.#signedKeyOwner = await getKeyOwner(key, this);
|
1193
|
+
}
|
1194
|
+
}
|
1034
1195
|
function notFound(_request) {
|
1035
1196
|
return new Response("Not Found", { status: 404 });
|
1036
1197
|
}
|