Loading examples/blog/federation/mod.ts +1 −1 Original line number Diff line number Diff line Loading @@ -15,7 +15,7 @@ import { countPosts, getPosts } from "../models/post.ts"; // The `Federation<TContextData>` object is a registry that registers // federation-related callbacks: export const federation = new Federation<Deno.Kv>({ export const federation = new Federation<void>({ kv: await openKv(), treatHttps: true, }); Loading examples/blog/routes/_middleware.ts +1 −2 Original line number Diff line number Diff line import { FreshContext } from "$fresh/server.ts"; import { federation } from "../federation/mod.ts"; import { openKv } from "fedify/examples/blog/models/kv.ts"; export async function handler(request: Request, context: FreshContext) { return await federation.handle(request, { contextData: await openKv(), contextData: undefined, onNotFound: context.next.bind(context), async onNotAcceptable(_request: Request) { const response = await context.next(); Loading federation/callback.ts +6 −6 Original line number Diff line number Diff line Loading @@ -2,13 +2,13 @@ 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"; import { RequestContext } from "./context.ts"; /** * A callback that dispatches an {@link Actor} object. */ export type ActorDispatcher<TContextData> = ( context: Context<TContextData>, context: RequestContext<TContextData>, handle: string, key: CryptographicKey | null, ) => Actor | null | Promise<Actor | null>; Loading @@ -25,7 +25,7 @@ export type ActorKeyPairDispatcher<TContextData> = ( * A callback that dispatches an outbox. */ export type OutboxDispatcher<TContextData> = ( context: Context<TContextData>, context: RequestContext<TContextData>, handle: string, cursor: string | null, ) => Page<Activity> | null | Promise<Page<Activity> | null>; Loading @@ -34,7 +34,7 @@ export type OutboxDispatcher<TContextData> = ( * A callback that counts the number of activities in an outbox. */ export type OutboxCounter<TContextData> = ( context: Context<TContextData>, context: RequestContext<TContextData>, handle: string, ) => number | bigint | null | Promise<number | bigint | null>; Loading @@ -42,7 +42,7 @@ export type OutboxCounter<TContextData> = ( * A callback that returns a cursor for an outbox. */ export type OutboxCursor<TContextData> = ( context: Context<TContextData>, context: RequestContext<TContextData>, handle: string, ) => string | null | Promise<string | null>; Loading @@ -50,6 +50,6 @@ export type OutboxCursor<TContextData> = ( * A callback that listens for activities in an inbox. */ export type InboxListener<TContextData, TActivity extends Activity> = ( context: Context<TContextData>, context: RequestContext<TContextData>, activity: TActivity, ) => void | Promise<void>; federation/context.ts +12 −110 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 } from "./send.ts"; /** * A context for a request. * A context. */ export interface Context<TContextData> { /** * The request object. */ readonly request: Request; /** * The user-defined data associated with the context. */ readonly data: TContextData; /** * The URL of the request. */ readonly url: URL; /** * The document loader for loading remote JSON-LD documents. */ Loading Loading @@ -67,100 +52,17 @@ export interface Context<TContextData> { ): Promise<void>; } export class ContextImpl<TContextData> implements Context<TContextData> { #kv: Deno.Kv; #router: Router; #actorKeyPairDispatcher?: ActorKeyPairDispatcher<TContextData>; /** * A context for a request. */ export interface RequestContext<TContextData> extends Context<TContextData> { /** * The request object. */ 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:"; } getActorUri(handle: string): URL { const path = this.#router.build("actor", { handle }); if (path == null) { throw new RouterError("No actor dispatcher registered."); } return new URL(path, this.url); } getOutboxUri(handle: string): URL { const path = this.#router.build("outbox", { handle }); if (path == null) { throw new RouterError("No outbox dispatcher registered."); } return new URL(path, this.url); } getInboxUri(handle: string): URL { const path = this.#router.build("inbox", { handle }); if (path == null) { throw new RouterError("No inbox path registered."); } return new URL(path, this.url); } async sendActivity( sender: { keyId: URL; privateKey: CryptoKey } | { handle: string }, recipients: Actor | Actor[], activity: Activity, { preferSharedInbox }: { preferSharedInbox?: boolean } = {}, ): Promise<void> { if (activity.id == null) { activity = activity.clone({ id: new URL(`urn:uuid:${crypto.randomUUID()}`), }); } 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], preferSharedInbox, }); for (const inbox of inboxes) { const message: OutboxMessage = { type: "outbox", keyId: keyId.href, privateKey: await crypto.subtle.exportKey("jwk", privateKey), activity: await activity.toJsonLd({ expand: true }), inbox: inbox.href, }; this.#kv.enqueue(message); } } /** * The URL of the request. */ readonly url: URL; } federation/handler.ts +5 −5 Original line number Diff line number Diff line Loading @@ -7,7 +7,7 @@ import { OutboxCursor, OutboxDispatcher, } from "./callback.ts"; import { Context } from "./context.ts"; import { RequestContext } from "./context.ts"; import { verify } from "../httpsig/mod.ts"; import { DocumentLoader } from "../runtime/docloader.ts"; import { isActor } from "../vocab/actor.ts"; Loading @@ -29,7 +29,7 @@ function acceptsJsonLd(request: Request): boolean { } export function getActorKey<TContextData>( context: Context<TContextData>, context: RequestContext<TContextData>, handle: string, keyPair?: CryptoKeyPair | null, ): CryptographicKey | null { Loading @@ -43,7 +43,7 @@ export function getActorKey<TContextData>( export interface ActorHandlerParameters<TContextData> { handle: string; context: Context<TContextData>; context: RequestContext<TContextData>; documentLoader: DocumentLoader; actorDispatcher?: ActorDispatcher<TContextData>; actorKeyPairDispatcher?: ActorKeyPairDispatcher<TContextData>; Loading Loading @@ -94,7 +94,7 @@ export async function handleActor<TContextData>( export interface OutboxHandlerParameters<TContextData> { handle: string; context: Context<TContextData>; context: RequestContext<TContextData>; documentLoader: DocumentLoader; outboxDispatcher?: OutboxDispatcher<TContextData>; outboxCounter?: OutboxCounter<TContextData>; Loading Loading @@ -207,7 +207,7 @@ export async function handleOutbox<TContextData>( export interface InboxHandlerParameters<TContextData> { handle: string; context: Context<TContextData>; context: RequestContext<TContextData>; actorDispatcher?: ActorDispatcher<TContextData>; actorKeyPairDispatcher?: ActorKeyPairDispatcher<TContextData>; inboxListeners: Map< Loading Loading
examples/blog/federation/mod.ts +1 −1 Original line number Diff line number Diff line Loading @@ -15,7 +15,7 @@ import { countPosts, getPosts } from "../models/post.ts"; // The `Federation<TContextData>` object is a registry that registers // federation-related callbacks: export const federation = new Federation<Deno.Kv>({ export const federation = new Federation<void>({ kv: await openKv(), treatHttps: true, }); Loading
examples/blog/routes/_middleware.ts +1 −2 Original line number Diff line number Diff line import { FreshContext } from "$fresh/server.ts"; import { federation } from "../federation/mod.ts"; import { openKv } from "fedify/examples/blog/models/kv.ts"; export async function handler(request: Request, context: FreshContext) { return await federation.handle(request, { contextData: await openKv(), contextData: undefined, onNotFound: context.next.bind(context), async onNotAcceptable(_request: Request) { const response = await context.next(); Loading
federation/callback.ts +6 −6 Original line number Diff line number Diff line Loading @@ -2,13 +2,13 @@ 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"; import { RequestContext } from "./context.ts"; /** * A callback that dispatches an {@link Actor} object. */ export type ActorDispatcher<TContextData> = ( context: Context<TContextData>, context: RequestContext<TContextData>, handle: string, key: CryptographicKey | null, ) => Actor | null | Promise<Actor | null>; Loading @@ -25,7 +25,7 @@ export type ActorKeyPairDispatcher<TContextData> = ( * A callback that dispatches an outbox. */ export type OutboxDispatcher<TContextData> = ( context: Context<TContextData>, context: RequestContext<TContextData>, handle: string, cursor: string | null, ) => Page<Activity> | null | Promise<Page<Activity> | null>; Loading @@ -34,7 +34,7 @@ export type OutboxDispatcher<TContextData> = ( * A callback that counts the number of activities in an outbox. */ export type OutboxCounter<TContextData> = ( context: Context<TContextData>, context: RequestContext<TContextData>, handle: string, ) => number | bigint | null | Promise<number | bigint | null>; Loading @@ -42,7 +42,7 @@ export type OutboxCounter<TContextData> = ( * A callback that returns a cursor for an outbox. */ export type OutboxCursor<TContextData> = ( context: Context<TContextData>, context: RequestContext<TContextData>, handle: string, ) => string | null | Promise<string | null>; Loading @@ -50,6 +50,6 @@ export type OutboxCursor<TContextData> = ( * A callback that listens for activities in an inbox. */ export type InboxListener<TContextData, TActivity extends Activity> = ( context: Context<TContextData>, context: RequestContext<TContextData>, activity: TActivity, ) => void | Promise<void>;
federation/context.ts +12 −110 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 } from "./send.ts"; /** * A context for a request. * A context. */ export interface Context<TContextData> { /** * The request object. */ readonly request: Request; /** * The user-defined data associated with the context. */ readonly data: TContextData; /** * The URL of the request. */ readonly url: URL; /** * The document loader for loading remote JSON-LD documents. */ Loading Loading @@ -67,100 +52,17 @@ export interface Context<TContextData> { ): Promise<void>; } export class ContextImpl<TContextData> implements Context<TContextData> { #kv: Deno.Kv; #router: Router; #actorKeyPairDispatcher?: ActorKeyPairDispatcher<TContextData>; /** * A context for a request. */ export interface RequestContext<TContextData> extends Context<TContextData> { /** * The request object. */ 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:"; } getActorUri(handle: string): URL { const path = this.#router.build("actor", { handle }); if (path == null) { throw new RouterError("No actor dispatcher registered."); } return new URL(path, this.url); } getOutboxUri(handle: string): URL { const path = this.#router.build("outbox", { handle }); if (path == null) { throw new RouterError("No outbox dispatcher registered."); } return new URL(path, this.url); } getInboxUri(handle: string): URL { const path = this.#router.build("inbox", { handle }); if (path == null) { throw new RouterError("No inbox path registered."); } return new URL(path, this.url); } async sendActivity( sender: { keyId: URL; privateKey: CryptoKey } | { handle: string }, recipients: Actor | Actor[], activity: Activity, { preferSharedInbox }: { preferSharedInbox?: boolean } = {}, ): Promise<void> { if (activity.id == null) { activity = activity.clone({ id: new URL(`urn:uuid:${crypto.randomUUID()}`), }); } 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], preferSharedInbox, }); for (const inbox of inboxes) { const message: OutboxMessage = { type: "outbox", keyId: keyId.href, privateKey: await crypto.subtle.exportKey("jwk", privateKey), activity: await activity.toJsonLd({ expand: true }), inbox: inbox.href, }; this.#kv.enqueue(message); } } /** * The URL of the request. */ readonly url: URL; }
federation/handler.ts +5 −5 Original line number Diff line number Diff line Loading @@ -7,7 +7,7 @@ import { OutboxCursor, OutboxDispatcher, } from "./callback.ts"; import { Context } from "./context.ts"; import { RequestContext } from "./context.ts"; import { verify } from "../httpsig/mod.ts"; import { DocumentLoader } from "../runtime/docloader.ts"; import { isActor } from "../vocab/actor.ts"; Loading @@ -29,7 +29,7 @@ function acceptsJsonLd(request: Request): boolean { } export function getActorKey<TContextData>( context: Context<TContextData>, context: RequestContext<TContextData>, handle: string, keyPair?: CryptoKeyPair | null, ): CryptographicKey | null { Loading @@ -43,7 +43,7 @@ export function getActorKey<TContextData>( export interface ActorHandlerParameters<TContextData> { handle: string; context: Context<TContextData>; context: RequestContext<TContextData>; documentLoader: DocumentLoader; actorDispatcher?: ActorDispatcher<TContextData>; actorKeyPairDispatcher?: ActorKeyPairDispatcher<TContextData>; Loading Loading @@ -94,7 +94,7 @@ export async function handleActor<TContextData>( export interface OutboxHandlerParameters<TContextData> { handle: string; context: Context<TContextData>; context: RequestContext<TContextData>; documentLoader: DocumentLoader; outboxDispatcher?: OutboxDispatcher<TContextData>; outboxCounter?: OutboxCounter<TContextData>; Loading Loading @@ -207,7 +207,7 @@ export async function handleOutbox<TContextData>( export interface InboxHandlerParameters<TContextData> { handle: string; context: Context<TContextData>; context: RequestContext<TContextData>; actorDispatcher?: ActorDispatcher<TContextData>; actorKeyPairDispatcher?: ActorKeyPairDispatcher<TContextData>; inboxListeners: Map< Loading