Loading examples/blog/deno.json +2 −2 Original line number Diff line number Diff line Loading @@ -5,7 +5,7 @@ "check": "deno task fedify-codegen && deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx", "cli": "deno task fedify-codegen && echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -", "manifest": "deno task fedify-codegen && deno task cli manifest $(pwd)", "start": "deno task fedify-codegen && deno run -A --watch=static/,routes/ --unstable-temporal dev.ts", "start": "deno task fedify-codegen && deno run -A --watch=static/,routes/ --unstable-temporal --unstable-kv dev.ts", "build": "deno task fedify-codegen && deno run -A dev.ts build", "preview": "deno task fedify-codegen && deno run -A main.ts", "update": "deno run -A -r https://fresh.deno.dev/update ." Loading examples/blog/federation/mod.ts +1 −5 Original line number Diff line number Diff line import { Federation } from "fedify/federation/middleware.ts"; import { fetchDocumentLoader, kvCache } from "fedify/runtime/docloader.ts"; import { isActor } from "fedify/vocab/actor.ts"; import { Accept, Loading @@ -18,11 +17,8 @@ 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>({ treatHttps: true, documentLoader: kvCache({ loader: fetchDocumentLoader, kv: await openKv(), }), treatHttps: true, }); // Registers the actor dispatcher, which is responsible for creating a Loading federation/context.ts +14 −16 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 { OutboxMessage } from "./queue.ts"; import { Router, RouterError } from "./router.ts"; import { extractInboxes, sendActivity } from "./send.ts"; Loading @@ -9,13 +9,9 @@ import { extractInboxes, sendActivity } from "./send.ts"; * A context for a request. */ export class Context<TContextData> { #kv: Deno.Kv; #router: Router; /** * The document loader used for loading remote JSON-LD documents. */ readonly documentLoader: DocumentLoader; /** * The request object. */ Loading @@ -33,21 +29,21 @@ export class Context<TContextData> { /** * Create a new context. * @param kv The Deno KV object. * @param router The router used for the request. * @param documentLoader: The document loader used for JSON-LD context retrieval. * @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. */ constructor( kv: Deno.Kv, router: Router, documentLoader: DocumentLoader, request: Request, data: TContextData, treatHttps = false, ) { this.#kv = kv; this.#router = router; this.documentLoader = documentLoader; this.request = request; this.data = data; this.url = new URL(request.url); Loading Loading @@ -106,6 +102,7 @@ export class Context<TContextData> { activity: Activity, { preferSharedInbox }: { preferSharedInbox?: boolean } = {}, ): Promise<void> { // TODO: Give an id to the activity if it doesn't have one. const { keyId, privateKey } = sender; validateCryptoKey(privateKey, "private"); const inboxes = extractInboxes({ Loading @@ -113,13 +110,14 @@ export class Context<TContextData> { preferSharedInbox, }); for (const inbox of inboxes) { const successful = await sendActivity({ keyId, privateKey, activity, inbox, documentLoader: this.documentLoader, }); 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); } } } federation/middleware.ts +40 −5 Original line number Diff line number Diff line import { DocumentLoader, fetchDocumentLoader } from "../runtime/docloader.ts"; import { Actor } from "../vocab/actor.ts"; import { DocumentLoader, fetchDocumentLoader, kvCache, } from "../runtime/docloader.ts"; import { Activity } from "../vocab/mod.ts"; import { handleWebFinger } from "../webfinger/handler.ts"; import { Loading @@ -11,12 +14,15 @@ import { } from "./callback.ts"; import { Context } from "./context.ts"; import { handleActor, handleInbox, handleOutbox } from "./handler.ts"; import { OutboxMessage } from "./queue.ts"; import { Router, RouterError } from "./router.ts"; import { sendActivity } from "./send.ts"; /** * Parameters for initializing a {@link Federation} instance. */ export interface FederationParameters { kv: Deno.Kv; documentLoader?: DocumentLoader; treatHttps?: boolean; } Loading @@ -29,6 +35,7 @@ export interface FederationParameters { * web framework's router; see {@link Federation.handle}. */ export class Federation<TContextData> { #kv: Deno.Kv; #router: Router; #actorDispatcher?: ActorDispatcher<TContextData>; #outboxCallbacks?: { Loading @@ -47,13 +54,41 @@ export class Federation<TContextData> { /** * Create a new {@link Federation} instance. * @param parameters Parameters for initializing the instance. */ constructor({ documentLoader, treatHttps }: FederationParameters = {}) { constructor({ kv, documentLoader, treatHttps }: FederationParameters) { this.#kv = kv; this.#router = new Router(); this.#router.add("/.well-known/webfinger", "webfinger"); this.#inboxListeners = new Map(); this.#documentLoader = documentLoader ?? fetchDocumentLoader; this.#documentLoader = documentLoader ?? kvCache({ loader: fetchDocumentLoader, kv: kv, }); this.#treatHttps = treatHttps ?? false; kv.listenQueue(this.#listenQueue.bind(this)); } async #listenQueue(message: OutboxMessage): Promise<void> { const successful = await sendActivity({ keyId: new URL(message.keyId), privateKey: await crypto.subtle.importKey( "jwk", message.privateKey, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, true, ["sign"], ), activity: await Activity.fromJsonLd(message.activity, { documentLoader: this.#documentLoader, }), inbox: new URL(message.inbox), documentLoader: this.#documentLoader, }); if (!successful) { throw new Error("Failed to send activity"); } } /** Loading Loading @@ -181,8 +216,8 @@ export class Federation<TContextData> { return response instanceof Promise ? await response : response; } const context = new Context( this.#kv, this.#router, this.#documentLoader, request, contextData, this.#treatHttps, Loading federation/queue.ts 0 → 100644 +7 −0 Original line number Diff line number Diff line export interface OutboxMessage { type: "outbox"; keyId: string; privateKey: JsonWebKey; activity: unknown; inbox: string; } Loading
examples/blog/deno.json +2 −2 Original line number Diff line number Diff line Loading @@ -5,7 +5,7 @@ "check": "deno task fedify-codegen && deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx", "cli": "deno task fedify-codegen && echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -", "manifest": "deno task fedify-codegen && deno task cli manifest $(pwd)", "start": "deno task fedify-codegen && deno run -A --watch=static/,routes/ --unstable-temporal dev.ts", "start": "deno task fedify-codegen && deno run -A --watch=static/,routes/ --unstable-temporal --unstable-kv dev.ts", "build": "deno task fedify-codegen && deno run -A dev.ts build", "preview": "deno task fedify-codegen && deno run -A main.ts", "update": "deno run -A -r https://fresh.deno.dev/update ." Loading
examples/blog/federation/mod.ts +1 −5 Original line number Diff line number Diff line import { Federation } from "fedify/federation/middleware.ts"; import { fetchDocumentLoader, kvCache } from "fedify/runtime/docloader.ts"; import { isActor } from "fedify/vocab/actor.ts"; import { Accept, Loading @@ -18,11 +17,8 @@ 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>({ treatHttps: true, documentLoader: kvCache({ loader: fetchDocumentLoader, kv: await openKv(), }), treatHttps: true, }); // Registers the actor dispatcher, which is responsible for creating a Loading
federation/context.ts +14 −16 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 { OutboxMessage } from "./queue.ts"; import { Router, RouterError } from "./router.ts"; import { extractInboxes, sendActivity } from "./send.ts"; Loading @@ -9,13 +9,9 @@ import { extractInboxes, sendActivity } from "./send.ts"; * A context for a request. */ export class Context<TContextData> { #kv: Deno.Kv; #router: Router; /** * The document loader used for loading remote JSON-LD documents. */ readonly documentLoader: DocumentLoader; /** * The request object. */ Loading @@ -33,21 +29,21 @@ export class Context<TContextData> { /** * Create a new context. * @param kv The Deno KV object. * @param router The router used for the request. * @param documentLoader: The document loader used for JSON-LD context retrieval. * @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. */ constructor( kv: Deno.Kv, router: Router, documentLoader: DocumentLoader, request: Request, data: TContextData, treatHttps = false, ) { this.#kv = kv; this.#router = router; this.documentLoader = documentLoader; this.request = request; this.data = data; this.url = new URL(request.url); Loading Loading @@ -106,6 +102,7 @@ export class Context<TContextData> { activity: Activity, { preferSharedInbox }: { preferSharedInbox?: boolean } = {}, ): Promise<void> { // TODO: Give an id to the activity if it doesn't have one. const { keyId, privateKey } = sender; validateCryptoKey(privateKey, "private"); const inboxes = extractInboxes({ Loading @@ -113,13 +110,14 @@ export class Context<TContextData> { preferSharedInbox, }); for (const inbox of inboxes) { const successful = await sendActivity({ keyId, privateKey, activity, inbox, documentLoader: this.documentLoader, }); 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); } } }
federation/middleware.ts +40 −5 Original line number Diff line number Diff line import { DocumentLoader, fetchDocumentLoader } from "../runtime/docloader.ts"; import { Actor } from "../vocab/actor.ts"; import { DocumentLoader, fetchDocumentLoader, kvCache, } from "../runtime/docloader.ts"; import { Activity } from "../vocab/mod.ts"; import { handleWebFinger } from "../webfinger/handler.ts"; import { Loading @@ -11,12 +14,15 @@ import { } from "./callback.ts"; import { Context } from "./context.ts"; import { handleActor, handleInbox, handleOutbox } from "./handler.ts"; import { OutboxMessage } from "./queue.ts"; import { Router, RouterError } from "./router.ts"; import { sendActivity } from "./send.ts"; /** * Parameters for initializing a {@link Federation} instance. */ export interface FederationParameters { kv: Deno.Kv; documentLoader?: DocumentLoader; treatHttps?: boolean; } Loading @@ -29,6 +35,7 @@ export interface FederationParameters { * web framework's router; see {@link Federation.handle}. */ export class Federation<TContextData> { #kv: Deno.Kv; #router: Router; #actorDispatcher?: ActorDispatcher<TContextData>; #outboxCallbacks?: { Loading @@ -47,13 +54,41 @@ export class Federation<TContextData> { /** * Create a new {@link Federation} instance. * @param parameters Parameters for initializing the instance. */ constructor({ documentLoader, treatHttps }: FederationParameters = {}) { constructor({ kv, documentLoader, treatHttps }: FederationParameters) { this.#kv = kv; this.#router = new Router(); this.#router.add("/.well-known/webfinger", "webfinger"); this.#inboxListeners = new Map(); this.#documentLoader = documentLoader ?? fetchDocumentLoader; this.#documentLoader = documentLoader ?? kvCache({ loader: fetchDocumentLoader, kv: kv, }); this.#treatHttps = treatHttps ?? false; kv.listenQueue(this.#listenQueue.bind(this)); } async #listenQueue(message: OutboxMessage): Promise<void> { const successful = await sendActivity({ keyId: new URL(message.keyId), privateKey: await crypto.subtle.importKey( "jwk", message.privateKey, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, true, ["sign"], ), activity: await Activity.fromJsonLd(message.activity, { documentLoader: this.#documentLoader, }), inbox: new URL(message.inbox), documentLoader: this.#documentLoader, }); if (!successful) { throw new Error("Failed to send activity"); } } /** Loading Loading @@ -181,8 +216,8 @@ export class Federation<TContextData> { return response instanceof Promise ? await response : response; } const context = new Context( this.#kv, this.#router, this.#documentLoader, request, contextData, this.#treatHttps, Loading
federation/queue.ts 0 → 100644 +7 −0 Original line number Diff line number Diff line export interface OutboxMessage { type: "outbox"; keyId: string; privateKey: JsonWebKey; activity: unknown; inbox: string; }