Loading CHANGES.md +5 −0 Original line number Diff line number Diff line Loading @@ -95,6 +95,11 @@ To be released. - Added `VerifyProofOptions` interface. - Added `fetchKey()` function. - Added `FetchKeyOptions` interface. - Added `SenderKeyPair` interface. - The type of `Federation.sendActivity()` method's first parameter became `SenderKeyPair[]` (was `{ keyId: URL; privateKey: CryptoKey }`). - The `Context.sendActivity()` method's first parameter now accepts `SenderKeyPair[]` as well. - Added `context` option to `Object.toJsonLd()` method. This applies to any subclasses of the `Object` class too. Loading examples/blog/import_map.g.json +1 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ "@std/yaml": "jsr:@std/yaml@^0.224.0", "asn1js": "npm:asn1js@^3.0.5", "fast-check": "npm:fast-check@^3.18.0", "json-canon": "npm:json-canon@^1.0.1", "jsonld": "npm:jsonld@^8.3.2", "mock_fetch": "https://deno.land/x/mock_fetch@0.3.0/mod.ts", "multibase": "npm:multibase@^4.0.6", Loading federation/context.ts +5 −4 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ import type { Multikey, Object, } from "../vocab/mod.ts"; import type { SenderKeyPair } from "./send.ts"; /** * A context. Loading Loading @@ -157,13 +158,13 @@ export interface Context<TContextData> { /** * Sends an activity to recipients' inboxes. * @param sender The sender's handle or the sender's key pair. * @param sender The sender's handle or the sender's key pair(s). * @param recipients The recipients of the activity. * @param activity The activity to send. * @param options Options for sending the activity. */ sendActivity( sender: { keyId: URL; privateKey: CryptoKey } | { handle: string }, sender: SenderKeyPair | SenderKeyPair[] | { handle: string }, recipients: Recipient | Recipient[], activity: Activity, options?: SendActivityOptions, Loading Loading @@ -240,13 +241,13 @@ export interface RequestContext<TContextData> extends Context<TContextData> { /** * Sends an activity to recipients' inboxes. * @param sender The sender's handle or the sender's key pair. * @param sender The sender's handle or the sender's key pair(s). * @param recipients The recipients of the activity. * @param activity The activity to send. * @param options Options for sending the activity. */ sendActivity( sender: { keyId: URL; privateKey: CryptoKey } | { handle: string }, sender: SenderKeyPair | SenderKeyPair[] | { handle: string }, recipients: Recipient | Recipient[], activity: Activity, options?: SendActivityOptions, Loading federation/middleware.ts +55 −26 Original line number Diff line number Diff line Loading @@ -50,9 +50,9 @@ import { } from "./handler.ts"; import type { KvKey, KvStore } from "./kv.ts"; import type { MessageQueue } from "./mq.ts"; import type { OutboxMessage } from "./queue.ts"; import type { OutboxMessage, SenderKeyJwkPair } from "./queue.ts"; import { Router, RouterError } from "./router.ts"; import { extractInboxes, sendActivity } from "./send.ts"; import { extractInboxes, sendActivity, type SenderKeyPair } from "./send.ts"; /** * Parameters for initializing a {@link Federation} instance. Loading Loading @@ -268,7 +268,7 @@ export class Federation<TContextData> { async #listenQueue(message: OutboxMessage): Promise<void> { const logger = getLogger(["fedify", "federation", "outbox"]); const logData = { keyId: message.keyId, keyIds: message.keys.map((pair) => pair.keyId), inbox: message.inbox, activity: message.activity, trial: message.trial, Loading @@ -276,18 +276,30 @@ export class Federation<TContextData> { }; let activity: Activity | null = null; try { const keyId = new URL(message.keyId); const privateKey = await importJwk(message.privateKey, "private"); const documentLoader = this.#authenticatedDocumentLoaderFactory( { keyId, privateKey }, ); const keys: SenderKeyPair[] = []; let rsaKeyPair: SenderKeyPair | null = null; for (const { keyId, privateKey } of message.keys) { const pair: SenderKeyPair = { keyId: new URL(keyId), privateKey: await importJwk(privateKey, "private"), }; if ( rsaKeyPair == null && pair.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5" ) { rsaKeyPair = pair; } keys.push(pair); } const documentLoader = rsaKeyPair == null ? this.#documentLoader : this.#authenticatedDocumentLoaderFactory(rsaKeyPair); activity = await Activity.fromJsonLd(message.activity, { documentLoader, contextLoader: this.#contextLoader, }); await sendActivity({ keyId, privateKey, keys, activity, inbox: new URL(message.inbox), contextLoader: this.#contextLoader, Loading Loading @@ -1032,20 +1044,26 @@ export class Federation<TContextData> { * Sends an activity to recipients' inboxes. You would typically use * {@link Context.sendActivity} instead of this method. * * @param sender The sender's key pair. * @param keys The sender's key pairs. * @param recipients The recipients of the activity. * @param activity The activity to send. * @param options Options for sending the activity. * @throws {TypeError} If the activity to send does not have an actor. */ async sendActivity( { keyId, privateKey }: { keyId: URL; privateKey: CryptoKey }, keys: SenderKeyPair[], recipients: Recipient | Recipient[], activity: Activity, { preferSharedInbox, immediate, excludeBaseUris, collectionSync }: SendActivityInternalOptions = {}, ): Promise<void> { const logger = getLogger(["fedify", "federation", "outbox"]); if (keys.length < 1) { throw new TypeError("The sender's keys must not be empty."); } for (const { privateKey } of keys) { validateCryptoKey(privateKey, "private"); } if (activity.actorId == null) { logger.error( "Activity {activityId} to send does not have an actor.", Loading @@ -1061,7 +1079,6 @@ export class Federation<TContextData> { id: new URL(`urn:uuid:${crypto.randomUUID()}`), }); } validateCryptoKey(privateKey, "private"); const inboxes = extractInboxes({ recipients: Array.isArray(recipients) ? recipients : [recipients], preferSharedInbox, Loading Loading @@ -1089,8 +1106,7 @@ export class Federation<TContextData> { for (const inbox in inboxes) { promises.push( sendActivity({ keyId, privateKey, keys, activity, inbox: new URL(inbox), contextLoader: this.#contextLoader, Loading @@ -1111,15 +1127,18 @@ export class Federation<TContextData> { "Enqueuing activity {activityId} to send later.", { activityId: activity.id?.href, activity }, ); const keyJwkPairs: SenderKeyJwkPair[] = []; for (const { keyId, privateKey } of keys) { const privateKeyJwk = await exportJwk(privateKey); keyJwkPairs.push({ keyId: keyId.href, privateKey: privateKeyJwk }); } const activityJson = await activity.toJsonLd({ contextLoader: this.#contextLoader, }); for (const inbox in inboxes) { const message: OutboxMessage = { type: "outbox", keyId: keyId.href, privateKey: privateKeyJwk, keys: keyJwkPairs, activity: activityJson, inbox, trial: 0, Loading Loading @@ -1642,20 +1661,30 @@ class ContextImpl<TContextData> implements Context<TContextData> { } async sendActivity( sender: { keyId: URL; privateKey: CryptoKey } | { handle: string }, sender: SenderKeyPair | SenderKeyPair[] | { handle: string }, recipients: Recipient | Recipient[] | "followers", activity: Activity, options: SendActivityOptions = {}, ): Promise<void> { let senderPair: { keyId: URL; privateKey: CryptoKey }; let keys: SenderKeyPair[]; if ("handle" in sender) { const keyPair = await this.getRsaKeyPairFromHandle(sender.handle); if (keyPair == null) { throw new Error(`No key pair found for actor ${sender.handle}`); keys = await this.getKeyPairsFromHandle( this.#url, this.data, sender.handle, ); if (keys.length < 1) { throw new Error( `No key pair found for actor ${JSON.stringify(sender.handle)}.`, ); } } else if (Array.isArray(sender)) { if (sender.length < 1) { throw new Error("The sender's key pairs are empty."); } senderPair = keyPair; keys = sender; } else { senderPair = sender; keys = [sender]; } const opts: SendActivityInternalOptions = { ...options }; let expandedRecipients: Recipient[]; Loading @@ -1681,7 +1710,7 @@ class ContextImpl<TContextData> implements Context<TContextData> { expandedRecipients = [recipients]; } return await this.#federation.sendActivity( senderPair, keys, expandedRecipients, activity, opts, Loading federation/mod.ts +1 −0 Original line number Diff line number Diff line Loading @@ -15,3 +15,4 @@ export * from "./kv.ts"; export * from "./middleware.ts"; export * from "./mq.ts"; export * from "./router.ts"; export { type SenderKeyPair } from "./send.ts"; Loading
CHANGES.md +5 −0 Original line number Diff line number Diff line Loading @@ -95,6 +95,11 @@ To be released. - Added `VerifyProofOptions` interface. - Added `fetchKey()` function. - Added `FetchKeyOptions` interface. - Added `SenderKeyPair` interface. - The type of `Federation.sendActivity()` method's first parameter became `SenderKeyPair[]` (was `{ keyId: URL; privateKey: CryptoKey }`). - The `Context.sendActivity()` method's first parameter now accepts `SenderKeyPair[]` as well. - Added `context` option to `Object.toJsonLd()` method. This applies to any subclasses of the `Object` class too. Loading
examples/blog/import_map.g.json +1 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ "@std/yaml": "jsr:@std/yaml@^0.224.0", "asn1js": "npm:asn1js@^3.0.5", "fast-check": "npm:fast-check@^3.18.0", "json-canon": "npm:json-canon@^1.0.1", "jsonld": "npm:jsonld@^8.3.2", "mock_fetch": "https://deno.land/x/mock_fetch@0.3.0/mod.ts", "multibase": "npm:multibase@^4.0.6", Loading
federation/context.ts +5 −4 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ import type { Multikey, Object, } from "../vocab/mod.ts"; import type { SenderKeyPair } from "./send.ts"; /** * A context. Loading Loading @@ -157,13 +158,13 @@ export interface Context<TContextData> { /** * Sends an activity to recipients' inboxes. * @param sender The sender's handle or the sender's key pair. * @param sender The sender's handle or the sender's key pair(s). * @param recipients The recipients of the activity. * @param activity The activity to send. * @param options Options for sending the activity. */ sendActivity( sender: { keyId: URL; privateKey: CryptoKey } | { handle: string }, sender: SenderKeyPair | SenderKeyPair[] | { handle: string }, recipients: Recipient | Recipient[], activity: Activity, options?: SendActivityOptions, Loading Loading @@ -240,13 +241,13 @@ export interface RequestContext<TContextData> extends Context<TContextData> { /** * Sends an activity to recipients' inboxes. * @param sender The sender's handle or the sender's key pair. * @param sender The sender's handle or the sender's key pair(s). * @param recipients The recipients of the activity. * @param activity The activity to send. * @param options Options for sending the activity. */ sendActivity( sender: { keyId: URL; privateKey: CryptoKey } | { handle: string }, sender: SenderKeyPair | SenderKeyPair[] | { handle: string }, recipients: Recipient | Recipient[], activity: Activity, options?: SendActivityOptions, Loading
federation/middleware.ts +55 −26 Original line number Diff line number Diff line Loading @@ -50,9 +50,9 @@ import { } from "./handler.ts"; import type { KvKey, KvStore } from "./kv.ts"; import type { MessageQueue } from "./mq.ts"; import type { OutboxMessage } from "./queue.ts"; import type { OutboxMessage, SenderKeyJwkPair } from "./queue.ts"; import { Router, RouterError } from "./router.ts"; import { extractInboxes, sendActivity } from "./send.ts"; import { extractInboxes, sendActivity, type SenderKeyPair } from "./send.ts"; /** * Parameters for initializing a {@link Federation} instance. Loading Loading @@ -268,7 +268,7 @@ export class Federation<TContextData> { async #listenQueue(message: OutboxMessage): Promise<void> { const logger = getLogger(["fedify", "federation", "outbox"]); const logData = { keyId: message.keyId, keyIds: message.keys.map((pair) => pair.keyId), inbox: message.inbox, activity: message.activity, trial: message.trial, Loading @@ -276,18 +276,30 @@ export class Federation<TContextData> { }; let activity: Activity | null = null; try { const keyId = new URL(message.keyId); const privateKey = await importJwk(message.privateKey, "private"); const documentLoader = this.#authenticatedDocumentLoaderFactory( { keyId, privateKey }, ); const keys: SenderKeyPair[] = []; let rsaKeyPair: SenderKeyPair | null = null; for (const { keyId, privateKey } of message.keys) { const pair: SenderKeyPair = { keyId: new URL(keyId), privateKey: await importJwk(privateKey, "private"), }; if ( rsaKeyPair == null && pair.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5" ) { rsaKeyPair = pair; } keys.push(pair); } const documentLoader = rsaKeyPair == null ? this.#documentLoader : this.#authenticatedDocumentLoaderFactory(rsaKeyPair); activity = await Activity.fromJsonLd(message.activity, { documentLoader, contextLoader: this.#contextLoader, }); await sendActivity({ keyId, privateKey, keys, activity, inbox: new URL(message.inbox), contextLoader: this.#contextLoader, Loading Loading @@ -1032,20 +1044,26 @@ export class Federation<TContextData> { * Sends an activity to recipients' inboxes. You would typically use * {@link Context.sendActivity} instead of this method. * * @param sender The sender's key pair. * @param keys The sender's key pairs. * @param recipients The recipients of the activity. * @param activity The activity to send. * @param options Options for sending the activity. * @throws {TypeError} If the activity to send does not have an actor. */ async sendActivity( { keyId, privateKey }: { keyId: URL; privateKey: CryptoKey }, keys: SenderKeyPair[], recipients: Recipient | Recipient[], activity: Activity, { preferSharedInbox, immediate, excludeBaseUris, collectionSync }: SendActivityInternalOptions = {}, ): Promise<void> { const logger = getLogger(["fedify", "federation", "outbox"]); if (keys.length < 1) { throw new TypeError("The sender's keys must not be empty."); } for (const { privateKey } of keys) { validateCryptoKey(privateKey, "private"); } if (activity.actorId == null) { logger.error( "Activity {activityId} to send does not have an actor.", Loading @@ -1061,7 +1079,6 @@ export class Federation<TContextData> { id: new URL(`urn:uuid:${crypto.randomUUID()}`), }); } validateCryptoKey(privateKey, "private"); const inboxes = extractInboxes({ recipients: Array.isArray(recipients) ? recipients : [recipients], preferSharedInbox, Loading Loading @@ -1089,8 +1106,7 @@ export class Federation<TContextData> { for (const inbox in inboxes) { promises.push( sendActivity({ keyId, privateKey, keys, activity, inbox: new URL(inbox), contextLoader: this.#contextLoader, Loading @@ -1111,15 +1127,18 @@ export class Federation<TContextData> { "Enqueuing activity {activityId} to send later.", { activityId: activity.id?.href, activity }, ); const keyJwkPairs: SenderKeyJwkPair[] = []; for (const { keyId, privateKey } of keys) { const privateKeyJwk = await exportJwk(privateKey); keyJwkPairs.push({ keyId: keyId.href, privateKey: privateKeyJwk }); } const activityJson = await activity.toJsonLd({ contextLoader: this.#contextLoader, }); for (const inbox in inboxes) { const message: OutboxMessage = { type: "outbox", keyId: keyId.href, privateKey: privateKeyJwk, keys: keyJwkPairs, activity: activityJson, inbox, trial: 0, Loading Loading @@ -1642,20 +1661,30 @@ class ContextImpl<TContextData> implements Context<TContextData> { } async sendActivity( sender: { keyId: URL; privateKey: CryptoKey } | { handle: string }, sender: SenderKeyPair | SenderKeyPair[] | { handle: string }, recipients: Recipient | Recipient[] | "followers", activity: Activity, options: SendActivityOptions = {}, ): Promise<void> { let senderPair: { keyId: URL; privateKey: CryptoKey }; let keys: SenderKeyPair[]; if ("handle" in sender) { const keyPair = await this.getRsaKeyPairFromHandle(sender.handle); if (keyPair == null) { throw new Error(`No key pair found for actor ${sender.handle}`); keys = await this.getKeyPairsFromHandle( this.#url, this.data, sender.handle, ); if (keys.length < 1) { throw new Error( `No key pair found for actor ${JSON.stringify(sender.handle)}.`, ); } } else if (Array.isArray(sender)) { if (sender.length < 1) { throw new Error("The sender's key pairs are empty."); } senderPair = keyPair; keys = sender; } else { senderPair = sender; keys = [sender]; } const opts: SendActivityInternalOptions = { ...options }; let expandedRecipients: Recipient[]; Loading @@ -1681,7 +1710,7 @@ class ContextImpl<TContextData> implements Context<TContextData> { expandedRecipients = [recipients]; } return await this.#federation.sendActivity( senderPair, keys, expandedRecipients, activity, opts, Loading
federation/mod.ts +1 −0 Original line number Diff line number Diff line Loading @@ -15,3 +15,4 @@ export * from "./kv.ts"; export * from "./middleware.ts"; export * from "./mq.ts"; export * from "./router.ts"; export { type SenderKeyPair } from "./send.ts";