Loading examples/blog/federation/mod.ts +13 −9 Original line number Diff line number Diff line Loading @@ -4,7 +4,6 @@ import { Accept, Activity, Create, CryptographicKey, Follow, Note, Person, Loading @@ -25,7 +24,7 @@ export const federation = new Federation<Deno.Kv>({ // `Actor` object (`Person` in this case) for a given actor URI. // The actor dispatch is not only used for the actor URI, but also for // the WebFinger resource: federation.setActorDispatcher("/users/{handle}", async (ctx, handle) => { federation.setActorDispatcher("/users/{handle}", async (ctx, handle, key) => { const blog = await getBlog(); if (blog == null) return null; else if (blog.handle !== handle) return null; Loading @@ -37,12 +36,17 @@ federation.setActorDispatcher("/users/{handle}", async (ctx, handle) => { url: new URL("/", ctx.request.url), outbox: ctx.getOutboxUri(handle), inbox: ctx.getInboxUri(handle), publicKey: new CryptographicKey({ id: new URL(`${ctx.getActorUri(handle)}#main-key`), owner: ctx.getActorUri(handle), publicKey: blog.publicKey, }), publicKey: key, }); }) .setKeyPairDispatcher(async (_ctxData, handle) => { const blog = await getBlog(); if (blog == null) return null; else if (blog.handle !== handle) return null; return { publicKey: blog.publicKey, privateKey: blog.privateKey, }; }); // Registers the outbox dispatcher, which is responsible for listing Loading Loading @@ -107,7 +111,7 @@ federation.setInboxListeners("/users/{handle}/inbox") const recipient = await follow.getActor(ctx); if (!isActor(recipient)) return; await ctx.sendActivity( { keyId: new URL(`${actorUri}#main-key`), privateKey: blog.privateKey }, { handle: blog.handle }, recipient, new Accept({ actor: actorUri, Loading @@ -115,7 +119,7 @@ federation.setInboxListeners("/users/{handle}/inbox") }), ); }) .on(Undo, async (ctx, undo) => { .on(Undo, (_ctx, undo) => { console.log({ undo }); }) .onError((e) => console.error(e)); federation/callback.ts +10 −0 Original line number Diff line number Diff line import { Actor } from "../vocab/actor.ts"; import { CryptographicKey } from "../vocab/mod.ts"; import { Activity } from "../vocab/mod.ts"; import { Page } from "./collection.ts"; import { Context } from "./context.ts"; Loading @@ -9,8 +10,17 @@ import { Context } from "./context.ts"; export type ActorDispatcher<TContextData> = ( context: Context<TContextData>, handle: string, key: CryptographicKey | null, ) => Actor | null | Promise<Actor | null>; /** * A callback that dispatches a key pair for an actor. */ export type ActorKeyPairDispatcher<TContextData> = ( contextData: TContextData, handle: string, ) => CryptoKeyPair | null | Promise<CryptoKeyPair | null>; /** * A callback that dispatches an outbox. */ Loading federation/context.ts +74 −35 Original line number Diff line number Diff line import { validateCryptoKey } from "../httpsig/key.ts"; import { DocumentLoader } from "../runtime/docloader.ts"; import { Actor } from "../vocab/actor.ts"; import { Activity } from "../vocab/mod.ts"; import { ActorKeyPairDispatcher } from "./callback.ts"; import { OutboxMessage } from "./queue.ts"; import { Router, RouterError } from "./router.ts"; import { extractInboxes, sendActivity } from "./send.ts"; import { extractInboxes } from "./send.ts"; /** * A context for a request. */ export class Context<TContextData> { #kv: Deno.Kv; #router: Router; export interface Context<TContextData> { /** * The request object. */ Loading @@ -28,33 +27,75 @@ export class Context<TContextData> { readonly url: URL; /** * Create a new context. * @param kv The Deno KV object. * @param router The router used for the request. * @param request The request object. * @param data The user-defined data associated with the context. * @param treatHttps Whether to treat the request as HTTPS even if it's not. * The document loader for loading remote JSON-LD documents. */ readonly documentLoader: DocumentLoader; /** * Builds the URI of an actor with the given handle. * @param handle The actor's handle. * @returns The actor's URI. */ getActorUri(handle: string): URL; /** * Builds the URI of an actor's outbox with the given handle. * @param handle The actor's handle. * @returns The actor's outbox URI. */ getOutboxUri(handle: string): URL; /** * Builds the URI of an actor's inbox with the given handle. * @param handle The actor's handle. * @returns The actor's inbox URI. */ getInboxUri(handle: string): URL; /** * Sends an activity to recipients' inboxes. * @param sender The sender's handle or sender's key pair. * @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 }, recipients: Actor | Actor[], activity: Activity, options?: { preferSharedInbox?: boolean }, ): Promise<void>; } export class ContextImpl<TContextData> implements Context<TContextData> { #kv: Deno.Kv; #router: Router; #actorKeyPairDispatcher?: ActorKeyPairDispatcher<TContextData>; readonly request: Request; readonly data: TContextData; readonly url: URL; readonly documentLoader: DocumentLoader; constructor( kv: Deno.Kv, router: Router, request: Request, data: TContextData, documentLoader: DocumentLoader, actorKeyPairDispatcher?: ActorKeyPairDispatcher<TContextData>, treatHttps = false, ) { this.#kv = kv; this.#router = router; this.#actorKeyPairDispatcher = actorKeyPairDispatcher; this.request = request; this.data = data; this.documentLoader = documentLoader; this.url = new URL(request.url); if (treatHttps) this.url.protocol = "https:"; } /** * Builds the URI of an actor with the given handle. * @param handle The actor's handle. * @returns The actor's URI. */ getActorUri(handle: string): URL { const path = this.#router.build("actor", { handle }); if (path == null) { Loading @@ -63,11 +104,6 @@ export class Context<TContextData> { return new URL(path, this.url); } /** * Builds the URI of an actor's outbox with the given handle. * @param handle The actor's handle. * @returns The actor's outbox URI. */ getOutboxUri(handle: string): URL { const path = this.#router.build("outbox", { handle }); if (path == null) { Loading @@ -76,11 +112,6 @@ export class Context<TContextData> { return new URL(path, this.url); } /** * Builds the URI of an actor's inbox with the given handle. * @param handle The actor's handle. * @returns The actor's inbox URI. */ getInboxUri(handle: string): URL { const path = this.#router.build("inbox", { handle }); if (path == null) { Loading @@ -89,15 +120,8 @@ export class Context<TContextData> { return new URL(path, this.url); } /** * Sends an activity to recipients' inboxes. * @param sender The sender's handle or sender's key pair. * @param recipients The recipients of the activity. * @param activity The activity to send. * @param options Options for sending the activity. */ async sendActivity( sender: { keyId: URL; privateKey: CryptoKey }, sender: { keyId: URL; privateKey: CryptoKey } | { handle: string }, recipients: Actor | Actor[], activity: Activity, { preferSharedInbox }: { preferSharedInbox?: boolean } = {}, Loading @@ -107,7 +131,22 @@ export class Context<TContextData> { id: new URL(`urn:uuid:${crypto.randomUUID()}`), }); } const { keyId, privateKey } = sender; let keyId, privateKey; if ("handle" in sender) { if (this.#actorKeyPairDispatcher == null) { throw new Error("No actor key pair dispatcher registered."); } let keyPair = this.#actorKeyPairDispatcher(this.data, sender.handle); if (keyPair instanceof Promise) keyPair = await keyPair; if (keyPair == null) { throw new Error(`No key pair found for actor ${sender.handle}`); } keyId = new URL(`${this.getActorUri(sender.handle)}#main-key`); privateKey = keyPair.privateKey; } else { keyId = sender.keyId; privateKey = sender.privateKey; } validateCryptoKey(privateKey, "private"); const inboxes = extractInboxes({ recipients: Array.isArray(recipients) ? recipients : [recipients], Loading federation/handler.ts +33 −2 Original line number Diff line number Diff line import { accepts } from "https://deno.land/std@0.217.0/http/mod.ts"; import { ActorDispatcher, ActorKeyPairDispatcher, InboxListener, OutboxCounter, OutboxCursor, Loading @@ -15,6 +16,7 @@ import { OrderedCollection, OrderedCollectionPage, } from "../vocab/mod.ts"; import { CryptographicKey } from "../vocab/mod.ts"; function acceptsJsonLd(request: Request): boolean { const types = accepts(request); Loading @@ -26,11 +28,25 @@ function acceptsJsonLd(request: Request): boolean { types.includes("application/json"); } export function getActorKey<TContextData>( context: Context<TContextData>, handle: string, keyPair?: CryptoKeyPair | null, ): CryptographicKey | null { if (keyPair == null) return null; return new CryptographicKey({ id: new URL(`${context.getActorUri(handle)}#main-key`), owner: context.getActorUri(handle), publicKey: keyPair.publicKey, }); } export interface ActorHandlerParameters<TContextData> { handle: string; context: Context<TContextData>; documentLoader: DocumentLoader; actorDispatcher?: ActorDispatcher<TContextData>; actorKeyPairDispatcher?: ActorKeyPairDispatcher<TContextData>; onNotFound(request: Request): Response | Promise<Response>; onNotAcceptable(request: Request): Response | Promise<Response>; } Loading @@ -42,6 +58,7 @@ export async function handleActor<TContextData>( context, documentLoader, actorDispatcher, actorKeyPairDispatcher, onNotFound, onNotAcceptable, }: ActorHandlerParameters<TContextData>, Loading @@ -54,7 +71,13 @@ export async function handleActor<TContextData>( const response = onNotAcceptable(request); return response instanceof Promise ? await response : response; } const actor = await actorDispatcher(context, handle); const keyPair = actorKeyPairDispatcher?.(context.data, handle); const key = getActorKey( context, handle, keyPair instanceof Promise ? await keyPair : keyPair, ); const actor = await actorDispatcher(context, handle, key); if (actor == null) { const response = onNotFound(request); return response instanceof Promise ? await response : response; Loading Loading @@ -186,6 +209,7 @@ export interface InboxHandlerParameters<TContextData> { handle: string; context: Context<TContextData>; actorDispatcher?: ActorDispatcher<TContextData>; actorKeyPairDispatcher?: ActorKeyPairDispatcher<TContextData>; inboxListeners: Map< new (...args: unknown[]) => Activity, InboxListener<TContextData, Activity> Loading @@ -201,6 +225,7 @@ export async function handleInbox<TContextData>( handle, context, actorDispatcher, actorKeyPairDispatcher, inboxListeners, inboxErrorHandler, documentLoader, Loading @@ -211,7 +236,13 @@ export async function handleInbox<TContextData>( const response = onNotFound(request); return response instanceof Promise ? await response : response; } else { const promise = actorDispatcher(context, handle); const keyPair = actorKeyPairDispatcher?.(context.data, handle); const key = getActorKey( context, handle, keyPair instanceof Promise ? await keyPair : keyPair, ); const promise = actorDispatcher(context, handle, key); const actor = promise instanceof Promise ? await promise : promise; if (actor == null) { const response = onNotFound(request); Loading federation/middleware.ts +67 −20 Original line number Diff line number Diff line Loading @@ -7,12 +7,13 @@ import { Activity } from "../vocab/mod.ts"; import { handleWebFinger } from "../webfinger/handler.ts"; import { ActorDispatcher, ActorKeyPairDispatcher, InboxListener, OutboxCounter, OutboxCursor, OutboxDispatcher, } from "./callback.ts"; import { Context } from "./context.ts"; import { ContextImpl } from "./context.ts"; import { handleActor, handleInbox, handleOutbox } from "./handler.ts"; import { OutboxMessage } from "./queue.ts"; import { Router, RouterError } from "./router.ts"; Loading @@ -37,13 +38,8 @@ export interface FederationParameters { export class Federation<TContextData> { #kv: Deno.Kv; #router: Router; #actorDispatcher?: ActorDispatcher<TContextData>; #outboxCallbacks?: { dispatcher: OutboxDispatcher<TContextData>; counter?: OutboxCounter<TContextData>; firstCursor?: OutboxCursor<TContextData>; lastCursor?: OutboxCursor<TContextData>; }; #actorCallbacks?: ActorCallbacks<TContextData>; #outboxCallbacks?: OutboxCallbacks<TContextData>; #inboxListeners: Map< new (...args: unknown[]) => Activity, InboxListener<TContextData, Activity> Loading Loading @@ -103,7 +99,7 @@ export class Federation<TContextData> { setActorDispatcher( path: string, dispatcher: ActorDispatcher<TContextData>, ): void { ): ActorCallbackSetters<TContextData> { if (this.#router.has("actor")) { throw new RouterError("Actor dispatcher already set."); } Loading @@ -113,7 +109,17 @@ export class Federation<TContextData> { "Path for actor dispatcher must have one variable: {handle}", ); } this.#actorDispatcher = dispatcher; const callbacks: ActorCallbacks<TContextData> = { dispatcher }; this.#actorCallbacks = callbacks; const setters: ActorCallbackSetters<TContextData> = { setKeyPairDispatcher: ( dispatcher: ActorKeyPairDispatcher<TContextData>, ) => { callbacks.keyPairDispatcher = dispatcher; return setters; }, }; return setters; } /** Loading @@ -138,12 +144,7 @@ export class Federation<TContextData> { "Path for outbox dispatcher must have one variable: {handle}", ); } const callbacks: { dispatcher: OutboxDispatcher<TContextData>; counter?: OutboxCounter<TContextData>; firstCursor?: OutboxCursor<TContextData>; lastCursor?: OutboxCursor<TContextData>; } = { dispatcher }; const callbacks: OutboxCallbacks<TContextData> = { dispatcher }; this.#outboxCallbacks = callbacks; const setters: OutboxCallbackSetters<TContextData> = { setCounter(counter: OutboxCounter<TContextData>) { Loading Loading @@ -215,18 +216,20 @@ export class Federation<TContextData> { const response = onNotFound(request); return response instanceof Promise ? await response : response; } const context = new Context( const context = new ContextImpl( this.#kv, this.#router, request, contextData, this.#documentLoader, this.#actorCallbacks?.keyPairDispatcher, this.#treatHttps, ); switch (route.name) { case "webfinger": return await handleWebFinger(request, { context, actorDispatcher: this.#actorDispatcher, actorDispatcher: this.#actorCallbacks?.dispatcher, onNotFound, }); case "actor": Loading @@ -234,7 +237,8 @@ export class Federation<TContextData> { handle: route.values.handle, context, documentLoader: this.#documentLoader, actorDispatcher: this.#actorDispatcher, actorDispatcher: this.#actorCallbacks?.dispatcher, actorKeyPairDispatcher: this.#actorCallbacks?.keyPairDispatcher, onNotFound, onNotAcceptable, }); Loading @@ -255,7 +259,8 @@ export class Federation<TContextData> { handle: route.values.handle, context, documentLoader: this.#documentLoader, actorDispatcher: this.#actorDispatcher, actorDispatcher: this.#actorCallbacks?.dispatcher, actorKeyPairDispatcher: this.#actorCallbacks?.keyPairDispatcher, inboxListeners: this.#inboxListeners, inboxErrorHandler: this.#inboxErrorHandler, onNotFound, Loading @@ -274,6 +279,45 @@ export interface FederationHandlerParameters<TContextData> { onNotAcceptable(request: Request): Response | Promise<Response>; } interface ActorCallbacks<TContextData> { dispatcher?: ActorDispatcher<TContextData>; keyPairDispatcher?: ActorKeyPairDispatcher<TContextData>; } /** * Additional settings for the actor dispatcher. * * ``` typescript * const federation = new Federation<void>({ ... }); * federation.setActorDispatcher("/users/{handle}", async (ctx, handle, key) => { * ... * }) * .setKeyPairDispatcher(async (ctxData, handle) => { * ... * }); * ``` */ export interface ActorCallbackSetters<TContextData> { /** * Sets the key pair dispatcher for actors. * @param dispatcher A callback that returns the key pair for an actor. * @returns The setters object so that settings can be chained. */ setKeyPairDispatcher( dispatcher: ActorKeyPairDispatcher<TContextData>, ): ActorCallbackSetters<TContextData>; } interface OutboxCallbacks<TContextData> { dispatcher: OutboxDispatcher<TContextData>; counter?: OutboxCounter<TContextData>; firstCursor?: OutboxCursor<TContextData>; lastCursor?: OutboxCursor<TContextData>; } /** * Additional settings for the outbox dispatcher. */ export interface OutboxCallbackSetters<TContextData> { setCounter( counter: OutboxCounter<TContextData>, Loading @@ -288,6 +332,9 @@ export interface OutboxCallbackSetters<TContextData> { ): OutboxCallbackSetters<TContextData>; } /** * Registry for inbox listeners for different activity types. */ export interface InboxListenerSetter<TContextData> { on<TActivity extends Activity>( // deno-lint-ignore no-explicit-any Loading Loading
examples/blog/federation/mod.ts +13 −9 Original line number Diff line number Diff line Loading @@ -4,7 +4,6 @@ import { Accept, Activity, Create, CryptographicKey, Follow, Note, Person, Loading @@ -25,7 +24,7 @@ export const federation = new Federation<Deno.Kv>({ // `Actor` object (`Person` in this case) for a given actor URI. // The actor dispatch is not only used for the actor URI, but also for // the WebFinger resource: federation.setActorDispatcher("/users/{handle}", async (ctx, handle) => { federation.setActorDispatcher("/users/{handle}", async (ctx, handle, key) => { const blog = await getBlog(); if (blog == null) return null; else if (blog.handle !== handle) return null; Loading @@ -37,12 +36,17 @@ federation.setActorDispatcher("/users/{handle}", async (ctx, handle) => { url: new URL("/", ctx.request.url), outbox: ctx.getOutboxUri(handle), inbox: ctx.getInboxUri(handle), publicKey: new CryptographicKey({ id: new URL(`${ctx.getActorUri(handle)}#main-key`), owner: ctx.getActorUri(handle), publicKey: blog.publicKey, }), publicKey: key, }); }) .setKeyPairDispatcher(async (_ctxData, handle) => { const blog = await getBlog(); if (blog == null) return null; else if (blog.handle !== handle) return null; return { publicKey: blog.publicKey, privateKey: blog.privateKey, }; }); // Registers the outbox dispatcher, which is responsible for listing Loading Loading @@ -107,7 +111,7 @@ federation.setInboxListeners("/users/{handle}/inbox") const recipient = await follow.getActor(ctx); if (!isActor(recipient)) return; await ctx.sendActivity( { keyId: new URL(`${actorUri}#main-key`), privateKey: blog.privateKey }, { handle: blog.handle }, recipient, new Accept({ actor: actorUri, Loading @@ -115,7 +119,7 @@ federation.setInboxListeners("/users/{handle}/inbox") }), ); }) .on(Undo, async (ctx, undo) => { .on(Undo, (_ctx, undo) => { console.log({ undo }); }) .onError((e) => console.error(e));
federation/callback.ts +10 −0 Original line number Diff line number Diff line import { Actor } from "../vocab/actor.ts"; import { CryptographicKey } from "../vocab/mod.ts"; import { Activity } from "../vocab/mod.ts"; import { Page } from "./collection.ts"; import { Context } from "./context.ts"; Loading @@ -9,8 +10,17 @@ import { Context } from "./context.ts"; export type ActorDispatcher<TContextData> = ( context: Context<TContextData>, handle: string, key: CryptographicKey | null, ) => Actor | null | Promise<Actor | null>; /** * A callback that dispatches a key pair for an actor. */ export type ActorKeyPairDispatcher<TContextData> = ( contextData: TContextData, handle: string, ) => CryptoKeyPair | null | Promise<CryptoKeyPair | null>; /** * A callback that dispatches an outbox. */ Loading
federation/context.ts +74 −35 Original line number Diff line number Diff line import { validateCryptoKey } from "../httpsig/key.ts"; import { DocumentLoader } from "../runtime/docloader.ts"; import { Actor } from "../vocab/actor.ts"; import { Activity } from "../vocab/mod.ts"; import { ActorKeyPairDispatcher } from "./callback.ts"; import { OutboxMessage } from "./queue.ts"; import { Router, RouterError } from "./router.ts"; import { extractInboxes, sendActivity } from "./send.ts"; import { extractInboxes } from "./send.ts"; /** * A context for a request. */ export class Context<TContextData> { #kv: Deno.Kv; #router: Router; export interface Context<TContextData> { /** * The request object. */ Loading @@ -28,33 +27,75 @@ export class Context<TContextData> { readonly url: URL; /** * Create a new context. * @param kv The Deno KV object. * @param router The router used for the request. * @param request The request object. * @param data The user-defined data associated with the context. * @param treatHttps Whether to treat the request as HTTPS even if it's not. * The document loader for loading remote JSON-LD documents. */ readonly documentLoader: DocumentLoader; /** * Builds the URI of an actor with the given handle. * @param handle The actor's handle. * @returns The actor's URI. */ getActorUri(handle: string): URL; /** * Builds the URI of an actor's outbox with the given handle. * @param handle The actor's handle. * @returns The actor's outbox URI. */ getOutboxUri(handle: string): URL; /** * Builds the URI of an actor's inbox with the given handle. * @param handle The actor's handle. * @returns The actor's inbox URI. */ getInboxUri(handle: string): URL; /** * Sends an activity to recipients' inboxes. * @param sender The sender's handle or sender's key pair. * @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 }, recipients: Actor | Actor[], activity: Activity, options?: { preferSharedInbox?: boolean }, ): Promise<void>; } export class ContextImpl<TContextData> implements Context<TContextData> { #kv: Deno.Kv; #router: Router; #actorKeyPairDispatcher?: ActorKeyPairDispatcher<TContextData>; readonly request: Request; readonly data: TContextData; readonly url: URL; readonly documentLoader: DocumentLoader; constructor( kv: Deno.Kv, router: Router, request: Request, data: TContextData, documentLoader: DocumentLoader, actorKeyPairDispatcher?: ActorKeyPairDispatcher<TContextData>, treatHttps = false, ) { this.#kv = kv; this.#router = router; this.#actorKeyPairDispatcher = actorKeyPairDispatcher; this.request = request; this.data = data; this.documentLoader = documentLoader; this.url = new URL(request.url); if (treatHttps) this.url.protocol = "https:"; } /** * Builds the URI of an actor with the given handle. * @param handle The actor's handle. * @returns The actor's URI. */ getActorUri(handle: string): URL { const path = this.#router.build("actor", { handle }); if (path == null) { Loading @@ -63,11 +104,6 @@ export class Context<TContextData> { return new URL(path, this.url); } /** * Builds the URI of an actor's outbox with the given handle. * @param handle The actor's handle. * @returns The actor's outbox URI. */ getOutboxUri(handle: string): URL { const path = this.#router.build("outbox", { handle }); if (path == null) { Loading @@ -76,11 +112,6 @@ export class Context<TContextData> { return new URL(path, this.url); } /** * Builds the URI of an actor's inbox with the given handle. * @param handle The actor's handle. * @returns The actor's inbox URI. */ getInboxUri(handle: string): URL { const path = this.#router.build("inbox", { handle }); if (path == null) { Loading @@ -89,15 +120,8 @@ export class Context<TContextData> { return new URL(path, this.url); } /** * Sends an activity to recipients' inboxes. * @param sender The sender's handle or sender's key pair. * @param recipients The recipients of the activity. * @param activity The activity to send. * @param options Options for sending the activity. */ async sendActivity( sender: { keyId: URL; privateKey: CryptoKey }, sender: { keyId: URL; privateKey: CryptoKey } | { handle: string }, recipients: Actor | Actor[], activity: Activity, { preferSharedInbox }: { preferSharedInbox?: boolean } = {}, Loading @@ -107,7 +131,22 @@ export class Context<TContextData> { id: new URL(`urn:uuid:${crypto.randomUUID()}`), }); } const { keyId, privateKey } = sender; let keyId, privateKey; if ("handle" in sender) { if (this.#actorKeyPairDispatcher == null) { throw new Error("No actor key pair dispatcher registered."); } let keyPair = this.#actorKeyPairDispatcher(this.data, sender.handle); if (keyPair instanceof Promise) keyPair = await keyPair; if (keyPair == null) { throw new Error(`No key pair found for actor ${sender.handle}`); } keyId = new URL(`${this.getActorUri(sender.handle)}#main-key`); privateKey = keyPair.privateKey; } else { keyId = sender.keyId; privateKey = sender.privateKey; } validateCryptoKey(privateKey, "private"); const inboxes = extractInboxes({ recipients: Array.isArray(recipients) ? recipients : [recipients], Loading
federation/handler.ts +33 −2 Original line number Diff line number Diff line import { accepts } from "https://deno.land/std@0.217.0/http/mod.ts"; import { ActorDispatcher, ActorKeyPairDispatcher, InboxListener, OutboxCounter, OutboxCursor, Loading @@ -15,6 +16,7 @@ import { OrderedCollection, OrderedCollectionPage, } from "../vocab/mod.ts"; import { CryptographicKey } from "../vocab/mod.ts"; function acceptsJsonLd(request: Request): boolean { const types = accepts(request); Loading @@ -26,11 +28,25 @@ function acceptsJsonLd(request: Request): boolean { types.includes("application/json"); } export function getActorKey<TContextData>( context: Context<TContextData>, handle: string, keyPair?: CryptoKeyPair | null, ): CryptographicKey | null { if (keyPair == null) return null; return new CryptographicKey({ id: new URL(`${context.getActorUri(handle)}#main-key`), owner: context.getActorUri(handle), publicKey: keyPair.publicKey, }); } export interface ActorHandlerParameters<TContextData> { handle: string; context: Context<TContextData>; documentLoader: DocumentLoader; actorDispatcher?: ActorDispatcher<TContextData>; actorKeyPairDispatcher?: ActorKeyPairDispatcher<TContextData>; onNotFound(request: Request): Response | Promise<Response>; onNotAcceptable(request: Request): Response | Promise<Response>; } Loading @@ -42,6 +58,7 @@ export async function handleActor<TContextData>( context, documentLoader, actorDispatcher, actorKeyPairDispatcher, onNotFound, onNotAcceptable, }: ActorHandlerParameters<TContextData>, Loading @@ -54,7 +71,13 @@ export async function handleActor<TContextData>( const response = onNotAcceptable(request); return response instanceof Promise ? await response : response; } const actor = await actorDispatcher(context, handle); const keyPair = actorKeyPairDispatcher?.(context.data, handle); const key = getActorKey( context, handle, keyPair instanceof Promise ? await keyPair : keyPair, ); const actor = await actorDispatcher(context, handle, key); if (actor == null) { const response = onNotFound(request); return response instanceof Promise ? await response : response; Loading Loading @@ -186,6 +209,7 @@ export interface InboxHandlerParameters<TContextData> { handle: string; context: Context<TContextData>; actorDispatcher?: ActorDispatcher<TContextData>; actorKeyPairDispatcher?: ActorKeyPairDispatcher<TContextData>; inboxListeners: Map< new (...args: unknown[]) => Activity, InboxListener<TContextData, Activity> Loading @@ -201,6 +225,7 @@ export async function handleInbox<TContextData>( handle, context, actorDispatcher, actorKeyPairDispatcher, inboxListeners, inboxErrorHandler, documentLoader, Loading @@ -211,7 +236,13 @@ export async function handleInbox<TContextData>( const response = onNotFound(request); return response instanceof Promise ? await response : response; } else { const promise = actorDispatcher(context, handle); const keyPair = actorKeyPairDispatcher?.(context.data, handle); const key = getActorKey( context, handle, keyPair instanceof Promise ? await keyPair : keyPair, ); const promise = actorDispatcher(context, handle, key); const actor = promise instanceof Promise ? await promise : promise; if (actor == null) { const response = onNotFound(request); Loading
federation/middleware.ts +67 −20 Original line number Diff line number Diff line Loading @@ -7,12 +7,13 @@ import { Activity } from "../vocab/mod.ts"; import { handleWebFinger } from "../webfinger/handler.ts"; import { ActorDispatcher, ActorKeyPairDispatcher, InboxListener, OutboxCounter, OutboxCursor, OutboxDispatcher, } from "./callback.ts"; import { Context } from "./context.ts"; import { ContextImpl } from "./context.ts"; import { handleActor, handleInbox, handleOutbox } from "./handler.ts"; import { OutboxMessage } from "./queue.ts"; import { Router, RouterError } from "./router.ts"; Loading @@ -37,13 +38,8 @@ export interface FederationParameters { export class Federation<TContextData> { #kv: Deno.Kv; #router: Router; #actorDispatcher?: ActorDispatcher<TContextData>; #outboxCallbacks?: { dispatcher: OutboxDispatcher<TContextData>; counter?: OutboxCounter<TContextData>; firstCursor?: OutboxCursor<TContextData>; lastCursor?: OutboxCursor<TContextData>; }; #actorCallbacks?: ActorCallbacks<TContextData>; #outboxCallbacks?: OutboxCallbacks<TContextData>; #inboxListeners: Map< new (...args: unknown[]) => Activity, InboxListener<TContextData, Activity> Loading Loading @@ -103,7 +99,7 @@ export class Federation<TContextData> { setActorDispatcher( path: string, dispatcher: ActorDispatcher<TContextData>, ): void { ): ActorCallbackSetters<TContextData> { if (this.#router.has("actor")) { throw new RouterError("Actor dispatcher already set."); } Loading @@ -113,7 +109,17 @@ export class Federation<TContextData> { "Path for actor dispatcher must have one variable: {handle}", ); } this.#actorDispatcher = dispatcher; const callbacks: ActorCallbacks<TContextData> = { dispatcher }; this.#actorCallbacks = callbacks; const setters: ActorCallbackSetters<TContextData> = { setKeyPairDispatcher: ( dispatcher: ActorKeyPairDispatcher<TContextData>, ) => { callbacks.keyPairDispatcher = dispatcher; return setters; }, }; return setters; } /** Loading @@ -138,12 +144,7 @@ export class Federation<TContextData> { "Path for outbox dispatcher must have one variable: {handle}", ); } const callbacks: { dispatcher: OutboxDispatcher<TContextData>; counter?: OutboxCounter<TContextData>; firstCursor?: OutboxCursor<TContextData>; lastCursor?: OutboxCursor<TContextData>; } = { dispatcher }; const callbacks: OutboxCallbacks<TContextData> = { dispatcher }; this.#outboxCallbacks = callbacks; const setters: OutboxCallbackSetters<TContextData> = { setCounter(counter: OutboxCounter<TContextData>) { Loading Loading @@ -215,18 +216,20 @@ export class Federation<TContextData> { const response = onNotFound(request); return response instanceof Promise ? await response : response; } const context = new Context( const context = new ContextImpl( this.#kv, this.#router, request, contextData, this.#documentLoader, this.#actorCallbacks?.keyPairDispatcher, this.#treatHttps, ); switch (route.name) { case "webfinger": return await handleWebFinger(request, { context, actorDispatcher: this.#actorDispatcher, actorDispatcher: this.#actorCallbacks?.dispatcher, onNotFound, }); case "actor": Loading @@ -234,7 +237,8 @@ export class Federation<TContextData> { handle: route.values.handle, context, documentLoader: this.#documentLoader, actorDispatcher: this.#actorDispatcher, actorDispatcher: this.#actorCallbacks?.dispatcher, actorKeyPairDispatcher: this.#actorCallbacks?.keyPairDispatcher, onNotFound, onNotAcceptable, }); Loading @@ -255,7 +259,8 @@ export class Federation<TContextData> { handle: route.values.handle, context, documentLoader: this.#documentLoader, actorDispatcher: this.#actorDispatcher, actorDispatcher: this.#actorCallbacks?.dispatcher, actorKeyPairDispatcher: this.#actorCallbacks?.keyPairDispatcher, inboxListeners: this.#inboxListeners, inboxErrorHandler: this.#inboxErrorHandler, onNotFound, Loading @@ -274,6 +279,45 @@ export interface FederationHandlerParameters<TContextData> { onNotAcceptable(request: Request): Response | Promise<Response>; } interface ActorCallbacks<TContextData> { dispatcher?: ActorDispatcher<TContextData>; keyPairDispatcher?: ActorKeyPairDispatcher<TContextData>; } /** * Additional settings for the actor dispatcher. * * ``` typescript * const federation = new Federation<void>({ ... }); * federation.setActorDispatcher("/users/{handle}", async (ctx, handle, key) => { * ... * }) * .setKeyPairDispatcher(async (ctxData, handle) => { * ... * }); * ``` */ export interface ActorCallbackSetters<TContextData> { /** * Sets the key pair dispatcher for actors. * @param dispatcher A callback that returns the key pair for an actor. * @returns The setters object so that settings can be chained. */ setKeyPairDispatcher( dispatcher: ActorKeyPairDispatcher<TContextData>, ): ActorCallbackSetters<TContextData>; } interface OutboxCallbacks<TContextData> { dispatcher: OutboxDispatcher<TContextData>; counter?: OutboxCounter<TContextData>; firstCursor?: OutboxCursor<TContextData>; lastCursor?: OutboxCursor<TContextData>; } /** * Additional settings for the outbox dispatcher. */ export interface OutboxCallbackSetters<TContextData> { setCounter( counter: OutboxCounter<TContextData>, Loading @@ -288,6 +332,9 @@ export interface OutboxCallbackSetters<TContextData> { ): OutboxCallbackSetters<TContextData>; } /** * Registry for inbox listeners for different activity types. */ export interface InboxListenerSetter<TContextData> { on<TActivity extends Activity>( // deno-lint-ignore no-explicit-any Loading