This release is 10 versions behind 1.5.0 — the latest version of @fedify/fedify. Jump to latest
Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Built and signed on GitHub Actions
An ActivityPub/fediverse server framework
This package works with Node.js, Deno, Bun


JSR Score
94%
Published
3 months ago (1.4.0-dev.596+acf76ef5)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264import { getLogger } from "jsr:@logtape/logtape@^0.8.0"; import { SpanKind, SpanStatusCode, trace, type TracerProvider, } from "npm:@opentelemetry/api@^1.9.0"; import metadata from "../deno.json" with { type: "json" }; import { signRequest } from "../sig/http.ts"; import type { Recipient } from "../vocab/actor.ts"; /** * Parameters for {@link extractInboxes}. */ export interface ExtractInboxesParameters { /** * Actors to extract the inboxes from. */ recipients: Recipient[]; /** * Whether to prefer the shared inbox over the personal inbox. * Defaults to `false`. */ preferSharedInbox?: boolean; /** * The base URIs to exclude from the recipients' inboxes. It is useful * for excluding the recipients having the same shared inbox with the sender. * * Note that the only `origin` parts of the `URL`s are compared. * * @since 0.9.0 */ excludeBaseUris?: URL[]; } /** * Extracts the inbox URLs from recipients. * @param parameters The parameters to extract the inboxes. * See also {@link ExtractInboxesParameters}. * @returns The inboxes as a map of inbox URL to actor URIs. */ export function extractInboxes( { recipients, preferSharedInbox, excludeBaseUris }: ExtractInboxesParameters, ): Record<string, { actorIds: Set<string>; sharedInbox: boolean }> { const inboxes: Record< string, { actorIds: Set<string>; sharedInbox: boolean } > = {}; for (const recipient of recipients) { let inbox: URL | null; let sharedInbox = false; if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) { inbox = recipient.endpoints.sharedInbox; sharedInbox = true; } else { inbox = recipient.inboxId; } if (inbox != null && recipient.id != null) { if ( excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin) ) { continue; } inboxes[inbox.href] ??= { actorIds: new Set(), sharedInbox }; inboxes[inbox.href].actorIds.add(recipient.id.href); } } return inboxes; } /** * A key pair for an actor who sends an activity. * @since 0.10.0 */ export interface SenderKeyPair { /** * The actor's private key to sign the request. */ privateKey: CryptoKey; /** * The public key ID that corresponds to the private key. */ keyId: URL; } /** * Parameters for {@link sendActivity}. */ export interface SendActivityParameters { /** * The activity to send. */ activity: unknown; /** * The activity ID to send. * @since 1.0.0 */ activityId?: string | null; /** * The qualified URI of the activity type. * @since 1.3.0 */ activityType?: string; /** * The key pairs of the sender to sign the request. It must not be empty. * @since 0.10.0 */ keys: SenderKeyPair[]; /** * The inbox URL to send the activity to. */ inbox: URL; /** * Whether the inbox is a shared inbox. * @since 1.3.0 */ sharedInbox?: boolean; /** * Additional headers to include in the request. */ headers?: Headers; /** * The tracer provider for tracing the request. * If omitted, the global tracer provider is used. * @since 1.3.0 */ tracerProvider?: TracerProvider; } /** * Sends an {@link Activity} to an inbox. * * @param parameters The parameters for sending the activity. * See also {@link SendActivityParameters}. * @throws {Error} If the activity fails to send. */ export function sendActivity( options: SendActivityParameters, ): Promise<void> { const tracerProvider = options.tracerProvider ?? trace.getTracerProvider(); const tracer = tracerProvider.getTracer(metadata.name, metadata.version); return tracer.startActiveSpan( "activitypub.send_activity", { kind: SpanKind.CLIENT, attributes: { "activitypub.shared_inbox": options.sharedInbox ?? false, }, }, async (span) => { if (options.activityId != null) { span.setAttribute("activitypub.activity.id", options.activityId); } if (options.activityType != null) { span.setAttribute("activitypub.activity.type", options.activityType); } try { await sendActivityInternal({ ...options, tracerProvider }); } catch (e) { span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) }); throw e; } finally { span.end(); } }, ); } async function sendActivityInternal( { activity, activityId, keys, inbox, headers, tracerProvider, }: SendActivityParameters, ): Promise<void> { const logger = getLogger(["fedify", "federation", "outbox"]); headers = new Headers(headers); headers.set("Content-Type", "application/activity+json"); let request = new Request(inbox, { method: "POST", headers, body: JSON.stringify(activity), }); let rsaKey: SenderKeyPair | null = null; for (const key of keys) { if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") { rsaKey = key; break; } } if (rsaKey == null) { logger.warn( "No supported key found to sign the request to {inbox}. " + "The request will be sent without a signature. " + "In order to sign the request, at least one RSASSA-PKCS1-v1_5 key " + "must be provided.", { inbox: inbox.href, keys: keys.map((pair) => ({ keyId: pair.keyId.href, privateKey: pair.privateKey, })), }, ); } else { request = await signRequest( request, rsaKey.privateKey, rsaKey.keyId, { tracerProvider }, ); } let response: Response; try { response = await fetch(request); } catch (error) { logger.error( "Failed to send activity {activityId} to {inbox}:\n{error}", { activityId, inbox: inbox.href, error, }, ); throw error; } if (!response.ok) { let error; try { error = await response.text(); } catch (_) { error = ""; } logger.error( "Failed to send activity {activityId} to {inbox} ({status} " + "{statusText}):\n{error}", { activityId, inbox: inbox.href, status: response.status, statusText: response.statusText, error, }, ); throw new Error( `Failed to send activity ${activityId} to ${inbox.href} ` + `(${response.status} ${response.statusText}):\n${error}`, ); } }