UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

1,362 lines (1,353 loc) • 132 kB
import { Temporal } from "@js-temporal/polyfill"; import { URLPattern } from "urlpattern-polyfill"; import { getDefaultActivityTransformers } from "./transformers-ghwJuzGY.js"; import { deno_default, getDocumentLoader, kvCache } from "./docloader-b8Rvguvo.js"; import { Activity, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, getTypeId } from "./actor-E2ennwBD.js"; import { lookupWebFinger } from "./lookup-Q9UB5zay.js"; import { exportJwk, importJwk, validateCryptoKey } from "./key-BNa6EpJl.js"; import { doubleKnock, verifyRequest } from "./http-DLI_ZQS-.js"; import { detachSignature, doesActorOwnKey, getKeyOwner, hasSignature, signJsonLd, signObject, verifyJsonLd, verifyObject } from "./proof-Blo9MEVW.js"; import { getNodeInfo, nodeInfoToJson } from "./types-CGJsS8Cg.js"; import { getAuthenticatedDocumentLoader } from "./authdocloader-CFeH0dJ-.js"; import { lookupObject, traverseCollection } from "./vocab-C3GyhklV.js"; import { getLogger, withContext } from "@logtape/logtape"; import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api"; import { encodeHex } from "byte-encodings/hex"; import { cloneDeep } from "@es-toolkit/es-toolkit"; import { Router } from "uri-template-router"; import { parseTemplate } from "url-template"; import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions"; import { domainToASCII } from "node:url"; //#region federation/inbox.ts var InboxListenerSet = class InboxListenerSet { #listeners; constructor() { this.#listeners = /* @__PURE__ */ new Map(); } clone() { const clone = new InboxListenerSet(); clone.#listeners = new Map(this.#listeners); return clone; } add(type, listener) { if (this.#listeners.has(type)) throw new TypeError("Listener already set for this type."); this.#listeners.set(type, listener); } dispatchWithClass(activity) { let cls = activity.constructor; const inboxListeners = this.#listeners; if (inboxListeners == null) return null; while (true) { if (inboxListeners.has(cls)) break; if (cls === Activity) return null; cls = globalThis.Object.getPrototypeOf(cls); } const listener = inboxListeners.get(cls); return { class: cls, listener }; } dispatch(activity) { return this.dispatchWithClass(activity)?.listener ?? null; } }; async function routeActivity({ context: ctx, json, activity, recipient, inboxListeners, inboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, tracerProvider }) { const logger$1 = getLogger([ "fedify", "federation", "inbox" ]); const cacheKey = activity.id == null ? null : [ ...kvPrefixes.activityIdempotence, ctx.origin, activity.id.href ]; if (cacheKey != null) { const cached = await kv.get(cacheKey); if (cached === true) { logger$1.debug("Activity {activityId} has already been processed.", { activityId: activity.id?.href, activity: json, recipient }); span.setStatus({ code: SpanStatusCode.UNSET, message: `Activity ${activity.id?.href} has already been processed.` }); return "alreadyProcessed"; } } if (activity.actorId == null) { logger$1.error("Missing actor.", { activity: json }); span.setStatus({ code: SpanStatusCode.ERROR, message: "Missing actor." }); return "missingActor"; } span.setAttribute("activitypub.actor.id", activity.actorId.href); if (queue != null) { const carrier = {}; propagation.inject(context.active(), carrier); try { await queue.enqueue({ type: "inbox", id: crypto.randomUUID(), baseUrl: ctx.origin, activity: json, identifier: recipient, attempt: 0, started: (/* @__PURE__ */ new Date()).toISOString(), traceContext: carrier }); } catch (error) { logger$1.error("Failed to enqueue the incoming activity {activityId}:\n{error}", { error, activityId: activity.id?.href, activity: json, recipient }); span.setStatus({ code: SpanStatusCode.ERROR, message: `Failed to enqueue the incoming activity ${activity.id?.href}.` }); throw error; } logger$1.info("Activity {activityId} is enqueued.", { activityId: activity.id?.href, activity: json, recipient }); return "enqueued"; } tracerProvider = tracerProvider ?? trace.getTracerProvider(); const tracer = tracerProvider.getTracer(deno_default.name, deno_default.version); return await tracer.startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (span$1) => { const dispatched = inboxListeners?.dispatchWithClass(activity); if (dispatched == null) { logger$1.error("Unsupported activity type:\n{activity}", { activity: json, recipient }); span$1.setStatus({ code: SpanStatusCode.UNSET, message: `Unsupported activity type: ${getTypeId(activity).href}` }); span$1.end(); return "unsupportedActivity"; } const { class: cls, listener } = dispatched; span$1.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`); try { await listener(inboxContextFactory(recipient, json, activity?.id?.href, getTypeId(activity).href), activity); } catch (error) { try { await inboxErrorHandler?.(ctx, error); } catch (error$1) { logger$1.error("An unexpected error occurred in inbox error handler:\n{error}", { error: error$1, activityId: activity.id?.href, activity: json, recipient }); } logger$1.error("Failed to process the incoming activity {activityId}:\n{error}", { error, activityId: activity.id?.href, activity: json, recipient }); span$1.setStatus({ code: SpanStatusCode.ERROR, message: String(error) }); span$1.end(); return "error"; } if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal.Duration.from({ days: 1 }) }); logger$1.info("Activity {activityId} has been processed.", { activityId: activity.id?.href, activity: json, recipient }); span$1.end(); return "success"; }); } //#endregion //#region federation/router.ts function cloneInnerRouter(router) { const clone = new Router(); clone.nid = router.nid; clone.fsm = cloneDeep(router.fsm); clone.routeSet = new Set(router.routeSet); clone.templateRouteMap = new Map(router.templateRouteMap); clone.valueRouteMap = new Map(router.valueRouteMap); clone.hierarchy = cloneDeep(router.hierarchy); return clone; } /** * URL router and constructor based on URI Template * ([RFC 6570](https://tools.ietf.org/html/rfc6570)). */ var Router$1 = class Router$1 { #router; #templates; #templateStrings; /** * Whether to ignore trailing slashes when matching paths. * @since 1.6.0 */ trailingSlashInsensitive; /** * Create a new {@link Router}. * @param options Options for the router. */ constructor(options = {}) { this.#router = new Router(); this.#templates = {}; this.#templateStrings = {}; this.trailingSlashInsensitive = options.trailingSlashInsensitive ?? false; } clone() { const clone = new Router$1({ trailingSlashInsensitive: this.trailingSlashInsensitive }); clone.#router = cloneInnerRouter(this.#router); clone.#templates = { ...this.#templates }; clone.#templateStrings = { ...this.#templateStrings }; return clone; } /** * Checks if a path name exists in the router. * @param name The name of the path. * @returns `true` if the path name exists, otherwise `false`. */ has(name) { return name in this.#templates; } /** * Adds a new path rule to the router. * @param template The path pattern. * @param name The name of the path. * @returns The names of the variables in the path pattern. */ add(template, name) { if (!template.startsWith("/")) throw new RouterError("Path must start with a slash."); const rule = this.#router.addTemplate(template, {}, name); this.#templates[name] = parseTemplate(template); this.#templateStrings[name] = template; return new Set(rule.variables.map((v) => v.varname)); } /** * Resolves a path name and values from a URL, if any match. * @param url The URL to resolve. * @returns The name of the path and its values, if any match. Otherwise, * `null`. */ route(url) { let match = this.#router.resolveURI(url); if (match == null) { if (!this.trailingSlashInsensitive) return null; url = url.endsWith("/") ? url.replace(/\/+$/, "") : `${url}/`; match = this.#router.resolveURI(url); if (match == null) return null; } return { name: match.matchValue, template: this.#templateStrings[match.matchValue], values: match.params }; } /** * Constructs a URL/path from a path name and values. * @param name The name of the path. * @param values The values to expand the path with. * @returns The URL/path, if the name exists. Otherwise, `null`. */ build(name, values) { if (name in this.#templates) return this.#templates[name].expand(values); return null; } }; /** * An error thrown by the {@link Router}. */ var RouterError = class extends Error { /** * Create a new {@link RouterError}. * @param message The error message. */ constructor(message) { super(message); this.name = "RouterError"; } }; //#endregion //#region federation/builder.ts var FederationBuilderImpl = class { router; actorCallbacks; nodeInfoDispatcher; objectCallbacks; objectTypeIds; inboxPath; inboxCallbacks; outboxCallbacks; followingCallbacks; followersCallbacks; likedCallbacks; featuredCallbacks; featuredTagsCallbacks; inboxListeners; inboxErrorHandler; sharedInboxKeyDispatcher; constructor() { this.router = new Router$1(); this.objectCallbacks = {}; this.objectTypeIds = {}; } async build(options) { const { FederationImpl: FederationImpl$1 } = await import("./middleware-B9LOv9Q4.js"); const f = new FederationImpl$1(options); const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive; f.router = this.router.clone(); f.router.trailingSlashInsensitive = trailingSlashInsensitiveValue; f._initializeRouter(); f.actorCallbacks = this.actorCallbacks == null ? void 0 : { ...this.actorCallbacks }; f.nodeInfoDispatcher = this.nodeInfoDispatcher; f.objectCallbacks = { ...this.objectCallbacks }; f.objectTypeIds = { ...this.objectTypeIds }; f.inboxPath = this.inboxPath; f.inboxCallbacks = this.inboxCallbacks == null ? void 0 : { ...this.inboxCallbacks }; f.outboxCallbacks = this.outboxCallbacks == null ? void 0 : { ...this.outboxCallbacks }; f.followingCallbacks = this.followingCallbacks == null ? void 0 : { ...this.followingCallbacks }; f.followersCallbacks = this.followersCallbacks == null ? void 0 : { ...this.followersCallbacks }; f.likedCallbacks = this.likedCallbacks == null ? void 0 : { ...this.likedCallbacks }; f.featuredCallbacks = this.featuredCallbacks == null ? void 0 : { ...this.featuredCallbacks }; f.featuredTagsCallbacks = this.featuredTagsCallbacks == null ? void 0 : { ...this.featuredTagsCallbacks }; f.inboxListeners = this.inboxListeners?.clone(); f.inboxErrorHandler = this.inboxErrorHandler; f.sharedInboxKeyDispatcher = this.sharedInboxKeyDispatcher; return f; } _getTracer() { return trace.getTracer(deno_default.name, deno_default.version); } setActorDispatcher(path, dispatcher) { if (this.router.has("actor")) throw new RouterError("Actor dispatcher already set."); const variables = this.router.add(path, "actor"); if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for actor dispatcher must have one variable: {identifier}"); if (variables.has("handle")) getLogger([ "fedify", "federation", "actor" ]).warn("The {{handle}} variable in the actor dispatcher path is deprecated. Use {{identifier}} instead."); const callbacks = { dispatcher: async (context$1, identifier) => { const actor = await this._getTracer().startActiveSpan("activitypub.dispatch_actor", { kind: SpanKind.SERVER, attributes: { "fedify.actor.identifier": identifier } }, async (span) => { try { const actor$1 = await dispatcher(context$1, identifier); span.setAttribute("activitypub.actor.id", (actor$1?.id ?? context$1.getActorUri(identifier)).href); if (actor$1 == null) span.setStatus({ code: SpanStatusCode.ERROR }); else span.setAttribute("activitypub.actor.type", getTypeId(actor$1).href); return actor$1; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) }); throw error; } finally { span.end(); } }); if (actor == null) return null; const logger$1 = getLogger([ "fedify", "federation", "actor" ]); if (actor.id == null) logger$1.warn("Actor dispatcher returned an actor without an id property. Set the property with Context.getActorUri(identifier)."); else if (actor.id.href != context$1.getActorUri(identifier).href) logger$1.warn("Actor dispatcher returned an actor with an id property that does not match the actor URI. Set the property with Context.getActorUri(identifier)."); if (this.followingCallbacks != null && this.followingCallbacks.dispatcher != null) { if (actor.followingId == null) logger$1.warn("You configured a following collection dispatcher, but the actor does not have a following property. Set the property with Context.getFollowingUri(identifier)."); else if (actor.followingId.href != context$1.getFollowingUri(identifier).href) logger$1.warn("You configured a following collection dispatcher, but the actor's following property does not match the following collection URI. Set the property with Context.getFollowingUri(identifier)."); } if (this.followersCallbacks != null && this.followersCallbacks.dispatcher != null) { if (actor.followersId == null) logger$1.warn("You configured a followers collection dispatcher, but the actor does not have a followers property. Set the property with Context.getFollowersUri(identifier)."); else if (actor.followersId.href != context$1.getFollowersUri(identifier).href) logger$1.warn("You configured a followers collection dispatcher, but the actor's followers property does not match the followers collection URI. Set the property with Context.getFollowersUri(identifier)."); } if (this.outboxCallbacks != null && this.outboxCallbacks.dispatcher != null) { if (actor?.outboxId == null) logger$1.warn("You configured an outbox collection dispatcher, but the actor does not have an outbox property. Set the property with Context.getOutboxUri(identifier)."); else if (actor.outboxId.href != context$1.getOutboxUri(identifier).href) logger$1.warn("You configured an outbox collection dispatcher, but the actor's outbox property does not match the outbox collection URI. Set the property with Context.getOutboxUri(identifier)."); } if (this.likedCallbacks != null && this.likedCallbacks.dispatcher != null) { if (actor?.likedId == null) logger$1.warn("You configured a liked collection dispatcher, but the actor does not have a liked property. Set the property with Context.getLikedUri(identifier)."); else if (actor.likedId.href != context$1.getLikedUri(identifier).href) logger$1.warn("You configured a liked collection dispatcher, but the actor's liked property does not match the liked collection URI. Set the property with Context.getLikedUri(identifier)."); } if (this.featuredCallbacks != null && this.featuredCallbacks.dispatcher != null) { if (actor?.featuredId == null) logger$1.warn("You configured a featured collection dispatcher, but the actor does not have a featured property. Set the property with Context.getFeaturedUri(identifier)."); else if (actor.featuredId.href != context$1.getFeaturedUri(identifier).href) logger$1.warn("You configured a featured collection dispatcher, but the actor's featured property does not match the featured collection URI. Set the property with Context.getFeaturedUri(identifier)."); } if (this.featuredTagsCallbacks != null && this.featuredTagsCallbacks.dispatcher != null) { if (actor?.featuredTagsId == null) logger$1.warn("You configured a featured tags collection dispatcher, but the actor does not have a featuredTags property. Set the property with Context.getFeaturedTagsUri(identifier)."); else if (actor.featuredTagsId.href != context$1.getFeaturedTagsUri(identifier).href) logger$1.warn("You configured a featured tags collection dispatcher, but the actor's featuredTags property does not match the featured tags collection URI. Set the property with Context.getFeaturedTagsUri(identifier)."); } if (this.router.has("inbox")) { if (actor.inboxId == null) logger$1.warn("You configured inbox listeners, but the actor does not have an inbox property. Set the property with Context.getInboxUri(identifier)."); else if (actor.inboxId.href != context$1.getInboxUri(identifier).href) logger$1.warn("You configured inbox listeners, but the actor's inbox property does not match the inbox URI. Set the property with Context.getInboxUri(identifier)."); if (actor.endpoints == null || actor.endpoints.sharedInbox == null) logger$1.warn("You configured inbox listeners, but the actor does not have a endpoints.sharedInbox property. Set the property with Context.getInboxUri()."); else if (actor.endpoints.sharedInbox.href != context$1.getInboxUri().href) logger$1.warn("You configured inbox listeners, but the actor's endpoints.sharedInbox property does not match the shared inbox URI. Set the property with Context.getInboxUri()."); } if (callbacks.keyPairsDispatcher != null) { if (actor.publicKeyId == null) logger$1.warn("You configured a key pairs dispatcher, but the actor does not have a publicKey property. Set the property with Context.getActorKeyPairs(identifier)."); if (actor.assertionMethodId == null) logger$1.warn("You configured a key pairs dispatcher, but the actor does not have an assertionMethod property. Set the property with Context.getActorKeyPairs(identifier)."); } return actor; } }; this.actorCallbacks = callbacks; const setters = { setKeyPairsDispatcher: (dispatcher$1) => { callbacks.keyPairsDispatcher = (ctx, identifier) => this._getTracer().startActiveSpan("activitypub.dispatch_actor_key_pairs", { kind: SpanKind.SERVER, attributes: { "activitypub.actor.id": ctx.getActorUri(identifier).href, "fedify.actor.identifier": identifier } }, async (span) => { try { return await dispatcher$1(ctx, identifier); } catch (e) { span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) }); throw e; } finally { span.end(); } }); return setters; }, mapHandle(mapper) { callbacks.handleMapper = mapper; return setters; }, mapAlias(mapper) { callbacks.aliasMapper = mapper; return setters; }, authorize(predicate) { callbacks.authorizePredicate = predicate; return setters; } }; return setters; } setNodeInfoDispatcher(path, dispatcher) { if (this.router.has("nodeInfo")) throw new RouterError("NodeInfo dispatcher already set."); const variables = this.router.add(path, "nodeInfo"); if (variables.size !== 0) throw new RouterError("Path for NodeInfo dispatcher must have no variables."); this.nodeInfoDispatcher = dispatcher; } setObjectDispatcher(cls, path, dispatcher) { const routeName = `object:${cls.typeId.href}`; if (this.router.has(routeName)) throw new RouterError(`Object dispatcher for ${cls.name} already set.`); const variables = this.router.add(path, routeName); if (variables.size < 1) throw new RouterError("Path for object dispatcher must have at least one variable."); const callbacks = { dispatcher: (ctx, values) => { const tracer = this._getTracer(); return tracer.startActiveSpan("activitypub.dispatch_object", { kind: SpanKind.SERVER, attributes: { "fedify.object.type": cls.typeId.href, ...globalThis.Object.fromEntries(globalThis.Object.entries(values).map(([k, v]) => [`fedify.object.values.${k}`, v])) } }, async (span) => { try { const object = await dispatcher(ctx, values); span.setAttribute("activitypub.object.id", (object?.id ?? ctx.getObjectUri(cls, values)).href); if (object == null) span.setStatus({ code: SpanStatusCode.ERROR }); else span.setAttribute("activitypub.object.type", getTypeId(object).href); return object; } catch (e) { span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) }); throw e; } finally { span.end(); } }); }, parameters: variables }; this.objectCallbacks[cls.typeId.href] = callbacks; this.objectTypeIds[cls.typeId.href] = cls; const setters = { authorize(predicate) { callbacks.authorizePredicate = predicate; return setters; } }; return setters; } setInboxDispatcher(path, dispatcher) { if (this.inboxCallbacks != null) throw new RouterError("Inbox dispatcher already set."); if (this.router.has("inbox")) { if (this.inboxPath !== path) throw new RouterError("Inbox dispatcher path must match inbox listener path."); } else { const variables = this.router.add(path, "inbox"); if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for inbox dispatcher must have one variable: {identifier}"); if (variables.has("handle")) getLogger([ "fedify", "federation", "inbox" ]).warn("The {{handle}} variable in the inbox dispatcher path is deprecated. Use {{identifier}} instead."); this.inboxPath = path; } const callbacks = { dispatcher }; this.inboxCallbacks = callbacks; const setters = { setCounter(counter) { callbacks.counter = counter; return setters; }, setFirstCursor(cursor) { callbacks.firstCursor = cursor; return setters; }, setLastCursor(cursor) { callbacks.lastCursor = cursor; return setters; }, authorize(predicate) { callbacks.authorizePredicate = predicate; return setters; } }; return setters; } setOutboxDispatcher(path, dispatcher) { if (this.router.has("outbox")) throw new RouterError("Outbox dispatcher already set."); const variables = this.router.add(path, "outbox"); if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for outbox dispatcher must have one variable: {identifier}"); if (variables.has("handle")) getLogger([ "fedify", "federation", "outbox" ]).warn("The {{handle}} variable in the outbox dispatcher path is deprecated. Use {{identifier}} instead."); const callbacks = { dispatcher }; this.outboxCallbacks = callbacks; const setters = { setCounter(counter) { callbacks.counter = counter; return setters; }, setFirstCursor(cursor) { callbacks.firstCursor = cursor; return setters; }, setLastCursor(cursor) { callbacks.lastCursor = cursor; return setters; }, authorize(predicate) { callbacks.authorizePredicate = predicate; return setters; } }; return setters; } setFollowingDispatcher(path, dispatcher) { if (this.router.has("following")) throw new RouterError("Following collection dispatcher already set."); const variables = this.router.add(path, "following"); if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for following collection dispatcher must have one variable: {identifier}"); if (variables.has("handle")) getLogger([ "fedify", "federation", "collection" ]).warn("The {{handle}} variable in the following collection dispatcher path is deprecated. Use {{identifier}} instead."); const callbacks = { dispatcher }; this.followingCallbacks = callbacks; const setters = { setCounter(counter) { callbacks.counter = counter; return setters; }, setFirstCursor(cursor) { callbacks.firstCursor = cursor; return setters; }, setLastCursor(cursor) { callbacks.lastCursor = cursor; return setters; }, authorize(predicate) { callbacks.authorizePredicate = predicate; return setters; } }; return setters; } setFollowersDispatcher(path, dispatcher) { if (this.router.has("followers")) throw new RouterError("Followers collection dispatcher already set."); const variables = this.router.add(path, "followers"); if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for followers collection dispatcher must have one variable: {identifier}"); if (variables.has("handle")) getLogger([ "fedify", "federation", "collection" ]).warn("The {{handle}} variable in the followers collection dispatcher path is deprecated. Use {{identifier}} instead."); const callbacks = { dispatcher }; this.followersCallbacks = callbacks; const setters = { setCounter(counter) { callbacks.counter = counter; return setters; }, setFirstCursor(cursor) { callbacks.firstCursor = cursor; return setters; }, setLastCursor(cursor) { callbacks.lastCursor = cursor; return setters; }, authorize(predicate) { callbacks.authorizePredicate = predicate; return setters; } }; return setters; } setLikedDispatcher(path, dispatcher) { if (this.router.has("liked")) throw new RouterError("Liked collection dispatcher already set."); const variables = this.router.add(path, "liked"); if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for liked collection dispatcher must have one variable: {identifier}"); if (variables.has("handle")) getLogger([ "fedify", "federation", "collection" ]).warn("The {{handle}} variable in the liked collection dispatcher path is deprecated. Use {{identifier}} instead."); const callbacks = { dispatcher }; this.likedCallbacks = callbacks; const setters = { setCounter(counter) { callbacks.counter = counter; return setters; }, setFirstCursor(cursor) { callbacks.firstCursor = cursor; return setters; }, setLastCursor(cursor) { callbacks.lastCursor = cursor; return setters; }, authorize(predicate) { callbacks.authorizePredicate = predicate; return setters; } }; return setters; } setFeaturedDispatcher(path, dispatcher) { if (this.router.has("featured")) throw new RouterError("Featured collection dispatcher already set."); const variables = this.router.add(path, "featured"); if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for featured collection dispatcher must have one variable: {identifier}"); if (variables.has("handle")) getLogger([ "fedify", "federation", "collection" ]).warn("The {{handle}} variable in the featured collection dispatcher path is deprecated. Use {{identifier}} instead."); const callbacks = { dispatcher }; this.featuredCallbacks = callbacks; const setters = { setCounter(counter) { callbacks.counter = counter; return setters; }, setFirstCursor(cursor) { callbacks.firstCursor = cursor; return setters; }, setLastCursor(cursor) { callbacks.lastCursor = cursor; return setters; }, authorize(predicate) { callbacks.authorizePredicate = predicate; return setters; } }; return setters; } setFeaturedTagsDispatcher(path, dispatcher) { if (this.router.has("featuredTags")) throw new RouterError("Featured tags collection dispatcher already set."); const variables = this.router.add(path, "featuredTags"); if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for featured tags collection dispatcher must have one variable: {identifier}"); if (variables.has("handle")) getLogger([ "fedify", "federation", "collection" ]).warn("The {{handle}} variable in the featured tags collection dispatcher path is deprecated. Use {{identifier}} instead."); const callbacks = { dispatcher }; this.featuredTagsCallbacks = callbacks; const setters = { setCounter(counter) { callbacks.counter = counter; return setters; }, setFirstCursor(cursor) { callbacks.firstCursor = cursor; return setters; }, setLastCursor(cursor) { callbacks.lastCursor = cursor; return setters; }, authorize(predicate) { callbacks.authorizePredicate = predicate; return setters; } }; return setters; } setInboxListeners(inboxPath, sharedInboxPath) { if (this.inboxListeners != null) throw new RouterError("Inbox listeners already set."); if (this.router.has("inbox")) { if (this.inboxPath !== inboxPath) throw new RouterError("Inbox listener path must match inbox dispatcher path."); } else { const variables = this.router.add(inboxPath, "inbox"); if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for inbox must have one variable: {identifier}"); this.inboxPath = inboxPath; if (variables.has("handle")) getLogger([ "fedify", "federation", "inbox" ]).warn("The {{handle}} variable in the inbox path is deprecated. Use {{identifier}} instead."); } if (sharedInboxPath != null) { const siVars = this.router.add(sharedInboxPath, "sharedInbox"); if (siVars.size !== 0) throw new RouterError("Path for shared inbox must have no variables."); } const listeners = this.inboxListeners = new InboxListenerSet(); const setters = { on(type, listener) { listeners.add(type, listener); return setters; }, onError: (handler) => { this.inboxErrorHandler = handler; return setters; }, setSharedKeyDispatcher: (dispatcher) => { this.sharedInboxKeyDispatcher = dispatcher; return setters; } }; return setters; } }; /** * Creates a new {@link FederationBuilder} instance. * @returns A new {@link FederationBuilder} instance. * @since 1.6.0 */ function createFederationBuilder() { return new FederationBuilderImpl(); } //#endregion //#region federation/collection.ts /** * Calculates the [partial follower collection digest][1]. * * [1]: https://w3id.org/fep/8fcf#partial-follower-collection-digest * @param uris The URIs to calculate the digest. Duplicate URIs are ignored. * @returns The digest. */ async function digest(uris) { const processed = /* @__PURE__ */ new Set(); const encoder = new TextEncoder(); const result = new Uint8Array(32); for (const uri of uris) { const u = uri instanceof URL ? uri.href : uri; if (processed.has(u)) continue; processed.add(u); const encoded = encoder.encode(u); const digest$1 = new Uint8Array(await crypto.subtle.digest("SHA-256", encoded)); for (let i = 0; i < 32; i++) result[i] ^= digest$1[i]; } return result; } /** * Builds [`Collection-Synchronization`][1] header content. * * [1]: https://w3id.org/fep/8fcf#the-collection-synchronization-http-header * * @param collectionId The sender's followers collection URI. * @param actorIds The actor URIs to digest. * @returns The header content. */ async function buildCollectionSynchronizationHeader(collectionId, actorIds) { const [anyActorId] = actorIds; const baseUrl = new URL(anyActorId); const url = new URL(collectionId); url.searchParams.set("base-url", `${baseUrl.origin}/`); const hash = encodeHex(await digest(actorIds)); return `collectionId="${collectionId}", url="${url}", digest="${hash}"`; } //#endregion //#region federation/keycache.ts var KvKeyCache = class { kv; prefix; options; nullKeys; constructor(kv, prefix, options = {}) { this.kv = kv; this.prefix = prefix; this.nullKeys = /* @__PURE__ */ new Set(); this.options = options; } async get(keyId) { if (this.nullKeys.has(keyId.href)) return null; const serialized = await this.kv.get([...this.prefix, keyId.href]); if (serialized == null) return void 0; try { return await CryptographicKey.fromJsonLd(serialized, this.options); } catch { try { return await Multikey.fromJsonLd(serialized, this.options); } catch { await this.kv.delete([...this.prefix, keyId.href]); return void 0; } } } async set(keyId, key) { if (key == null) { this.nullKeys.add(keyId.href); await this.kv.delete([...this.prefix, keyId.href]); return; } this.nullKeys.delete(keyId.href); const serialized = await key.toJsonLd(this.options); await this.kv.set([...this.prefix, keyId.href], serialized); } }; //#endregion //#region federation/negotiation.ts function compareSpecs(a, b) { return b.q - a.q || (b.s ?? 0) - (a.s ?? 0) || (a.o ?? 0) - (b.o ?? 0) || a.i - b.i || 0; } function isQuality(spec) { return spec.q > 0; } const simpleMediaTypeRegExp = /^\s*([^\s\/;]+)\/([^;\s]+)\s*(?:;(.*))?$/; function splitKeyValuePair(str) { const [key, value] = str.split("="); return [key.toLowerCase(), value]; } function parseMediaType(str, i) { const match = simpleMediaTypeRegExp.exec(str); if (!match) return; const [, type, subtype, parameters] = match; if (!type || !subtype) return; const params = Object.create(null); let q = 1; if (parameters) { const kvps = parameters.split(";").map((p) => p.trim()).map(splitKeyValuePair); for (const [key, val] of kvps) { const value = val && val[0] === `"` && val[val.length - 1] === `"` ? val.slice(1, val.length - 1) : val; if (key === "q" && value) { q = parseFloat(value); break; } params[key] = value; } } return { type, subtype, params, i, o: void 0, q, s: void 0 }; } function parseAccept(accept) { const accepts = accept.split(",").map((p) => p.trim()); const mediaTypes = []; for (const [index, accept$1] of accepts.entries()) { const mediaType = parseMediaType(accept$1.trim(), index); if (mediaType) mediaTypes.push(mediaType); } return mediaTypes; } function getFullType(spec) { return `${spec.type}/${spec.subtype}`; } function preferredMediaTypes(accept) { const accepts = parseAccept(accept === void 0 ? "*/*" : accept ?? ""); return accepts.filter(isQuality).sort(compareSpecs).map(getFullType); } //#endregion //#region federation/handler.ts function acceptsJsonLd(request) { const accept = request.headers.get("Accept"); const types = accept ? preferredMediaTypes(accept) : ["*/*"]; if (types == null) return true; if (types[0] === "text/html" || types[0] === "application/xhtml+xml") return false; return types.includes("application/activity+json") || types.includes("application/ld+json") || types.includes("application/json"); } async function handleActor(request, { identifier, context: context$1, actorDispatcher, authorizePredicate, onNotFound, onNotAcceptable, onUnauthorized }) { const logger$1 = getLogger([ "fedify", "federation", "actor" ]); if (actorDispatcher == null) { logger$1.debug("Actor dispatcher is not set.", { identifier }); return await onNotFound(request); } const actor = await actorDispatcher(context$1, identifier); if (actor == null) { logger$1.debug("Actor {identifier} not found.", { identifier }); return await onNotFound(request); } if (!acceptsJsonLd(request)) return await onNotAcceptable(request); if (authorizePredicate != null) { let key = await context$1.getSignedKey(); key = key?.clone({}, { $warning: { category: [ "fedify", "federation", "actor" ], message: "The third parameter of AuthorizePredicate is deprecated in favor of RequestContext.getSignedKey() method. The third parameter will be removed in a future release." } }) ?? null; let keyOwner = await context$1.getSignedKeyOwner(); keyOwner = keyOwner?.clone({}, { $warning: { category: [ "fedify", "federation", "actor" ], message: "The fourth parameter of AuthorizePredicate is deprecated in favor of RequestContext.getSignedKeyOwner() method. The fourth parameter will be removed in a future release." } }) ?? null; if (!await authorizePredicate(context$1, identifier, key, keyOwner)) return await onUnauthorized(request); } const jsonLd = await actor.toJsonLd(context$1); return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json", Vary: "Accept" } }); } async function handleObject(request, { values, context: context$1, objectDispatcher, authorizePredicate, onNotFound, onNotAcceptable, onUnauthorized }) { if (objectDispatcher == null) return await onNotFound(request); const object = await objectDispatcher(context$1, values); if (object == null) return await onNotFound(request); if (!acceptsJsonLd(request)) return await onNotAcceptable(request); if (authorizePredicate != null) { let key = await context$1.getSignedKey(); key = key?.clone({}, { $warning: { category: [ "fedify", "federation", "object" ], message: "The third parameter of ObjectAuthorizePredicate is deprecated in favor of RequestContext.getSignedKey() method. The third parameter will be removed in a future release." } }) ?? null; let keyOwner = await context$1.getSignedKeyOwner(); keyOwner = keyOwner?.clone({}, { $warning: { category: [ "fedify", "federation", "object" ], message: "The fourth parameter of ObjectAuthorizePredicate is deprecated in favor of RequestContext.getSignedKeyOwner() method. The fourth parameter will be removed in a future release." } }) ?? null; if (!await authorizePredicate(context$1, values, key, keyOwner)) return await onUnauthorized(request); } const jsonLd = await object.toJsonLd(context$1); return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json", Vary: "Accept" } }); } async function handleCollection(request, { name, identifier, uriGetter, filter, filterPredicate, context: context$1, collectionCallbacks, tracerProvider, onUnauthorized, onNotFound, onNotAcceptable }) { const spanName = name.trim().replace(/\s+/g, "_"); tracerProvider = tracerProvider ?? trace.getTracerProvider(); const tracer = tracerProvider.getTracer(deno_default.name, deno_default.version); const url = new URL(request.url); const cursor = url.searchParams.get("cursor"); if (collectionCallbacks == null) return await onNotFound(request); let collection; const baseUri = uriGetter(identifier); if (cursor == null) { const firstCursor = await collectionCallbacks.firstCursor?.(context$1, identifier); const totalItems = filter == null ? await collectionCallbacks.counter?.(context$1, identifier) : void 0; if (firstCursor == null) { const itemsOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection ${spanName}`, { kind: SpanKind.SERVER, attributes: { "activitypub.collection.id": baseUri.href, "activitypub.collection.type": OrderedCollection.typeId.href } }, async (span) => { if (totalItems != null) span.setAttribute("activitypub.collection.total_items", Number(totalItems)); try { const page = await collectionCallbacks.dispatcher(context$1, identifier, null, filter); if (page == null) { span.setStatus({ code: SpanStatusCode.ERROR }); return await onNotFound(request); } const { items } = page; span.setAttribute("fedify.collection.items", items.length); return items; } catch (e) { span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) }); throw e; } finally { span.end(); } }); if (itemsOrResponse instanceof Response) return itemsOrResponse; collection = new OrderedCollection({ id: baseUri, totalItems: totalItems == null ? null : Number(totalItems), items: filterCollectionItems(itemsOrResponse, name, filterPredicate) }); } else { const lastCursor = await collectionCallbacks.lastCursor?.(context$1, identifier); const first = new URL(context$1.url); first.searchParams.set("cursor", firstCursor); let last = null; if (lastCursor != null) { last = new URL(context$1.url); last.searchParams.set("cursor", lastCursor); } collection = new OrderedCollection({ id: baseUri, totalItems: totalItems == null ? null : Number(totalItems), first, last }); } } else { const uri = new URL(baseUri); uri.searchParams.set("cursor", cursor); const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name}`, { kind: SpanKind.SERVER, attributes: { "activitypub.collection.id": uri.href, "activitypub.collection.type": OrderedCollectionPage.typeId.href, "fedify.collection.cursor": cursor } }, async (span) => { try { const page = await collectionCallbacks.dispatcher(context$1, identifier, cursor, filter); if (page == null) { span.setStatus({ code: SpanStatusCode.ERROR }); return await onNotFound(request); } span.setAttribute("fedify.collection.items", page.items.length); return page; } catch (e) { span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) }); throw e; } finally { span.end(); } }); if (pageOrResponse instanceof Response) return pageOrResponse; const { items, prevCursor, nextCursor } = pageOrResponse; let prev = null; if (prevCursor != null) { prev = new URL(context$1.url); prev.searchParams.set("cursor", prevCursor); } let next = null; if (nextCursor != null) { next = new URL(context$1.url); next.searchParams.set("cursor", nextCursor); } const partOf = new URL(context$1.url); partOf.searchParams.delete("cursor"); collection = new OrderedCollectionPage({ id: uri, prev, next, items: filterCollectionItems(items, name, filterPredicate), partOf }); } if (!acceptsJsonLd(request)) return await onNotAcceptable(request); if (collectionCallbacks.authorizePredicate != null) { let key = await context$1.getSignedKey(); key = key?.clone({}, { $warning: { category: [ "fedify", "federation", "collection" ], message: "The third parameter of AuthorizePredicate is deprecated in favor of RequestContext.getSignedKey() method. The third parameter will be removed in a future release." } }) ?? null; let keyOwner = await context$1.getSignedKeyOwner(); keyOwner = keyOwner?.clone({}, { $warning: { category: [ "fedify", "federation", "collection" ], message: "The fourth parameter of AuthorizePredicate is deprecated in favor of RequestContext.getSignedKeyOwner() method. The fourth parameter will be removed in a future release." } }) ?? null; if (!await collectionCallbacks.authorizePredicate(context$1, identifier, key, keyOwner)) return await onUnauthorized(request); } const jsonLd = await collection.toJsonLd(context$1); return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json", Vary: "Accept" } }); } function filterCollectionItems(items, collectionName, filterPredicate) { const result = []; let logged = false; for (const item of items) { let mappedItem; if (item instanceof Object$1 || item instanceof Link || item instanceof URL) mappedItem = item; else if (item.id == null) continue; else mappedItem = item.id; if (filterPredicate != null && !filterPredicate(item)) { if (!logged) { getLogger([ "fedify", "federation", "collection" ]).warn(`The ${collectionName} collection apparently does not implement filtering. This may result in a large response payload. Please consider implementing filtering for the collection. See also: https://fedify.dev/manual/collections#filtering-by-server`); logged = true; } continue; } result.push(mappedItem); } return result; } async function handleInbox(request, options) { const tracerProvider = options.tracerProvider ?? trace.getTracerProvider(); const tracer = tracerProvider.getTracer(deno_default.name, deno_default.version); return await tracer.startActiveSpan("activitypub.inbox", { kind: options.queue == null ? SpanKind.SERVER : SpanKind.PRODUCER, attributes: { "activitypub.shared_inbox": options.recipient == null } }, async (span) => { if (options.recipient != null) span.setAttribute("fedify.inbox.recipient", options.recipient); try { return await handleInboxInternal(request, options, span); } catch (e) { span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) }); throw e; } finally { span.end(); } }); } async function handleInboxInternal(request, { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, queue, actorDispatcher, inboxListeners, inboxErrorHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, tracerProvider }, span) { const logger$1 = getLogger([ "fedify", "federation", "inbox" ]); if (actorDispatcher == null) { logger$1.error("Actor dispatcher is not set.", { recipient }); span.setStatus({ code: SpanStatusCode.ERROR, message: "Actor dispatcher is not set." }); return await onNotFound(request); } else if (recipient != null) { const actor = await actorDispatcher(ctx, recipient); if (actor == null) { logger$1.error("Actor {recipient} not found.", { recipient }); span.setStatus({ code: SpanStatusCode.ERROR, message: `Actor ${recipient} not found.` }); return await onNotFound(request); } } if (request.bodyUsed) { logger$1.error("Request body has already been read.", { recipient }); span.setStatus({ code: SpanStatusCode.ERROR, message: "Request body has already been read." }); return new Response("Internal server error.", { status: 500, headers: { "Content-Type": "text/plain; charset=utf-8" } }); } else if (request.body?.locked) { logger$1.error("Request body is locked.", { recipient }); span.setStatus({ code: SpanStatusCode.ERROR, message: "Request body is locked." }); return new Response("Internal server error.", { status: 500, headers: { "Content-Type": "text/plain; charset=utf-8" } }); } let json; try { json = await request.clone().json(); } catch (error) { logger$1.error("Failed to parse JSON:\n{error}", { recipient, error }); try { await inboxErrorHandler?.(ctx, error); } catch (error$1) { logger$1.error("An unexpected error occurred in inbox error handler:\n{error}", { error: error$1, activity: json, recipient }); } span.setStatus({ code: SpanStatusCode.ERROR, message: `Failed to parse JSON:\n${error}` }); return new Response("Invalid JSON.", { status: 400, headers: { "Content-Type": "text/plain; charset=utf-8" } }); } const keyCache = new KvKeyCache(kv, kvPrefixes.publicKey, ctx); let ldSigVerified; try { ldSigVerified = await verifyJsonLd(json, { contextLoader: ctx.contextLoader, documentLoader: ctx.documentLoader, keyCache, tracerProvider }); } catch (error) { if (error instanceof Error && error.name === "jsonld.SyntaxError") { logger$1.error("Failed to parse JSON-LD:\n{error}", { recipient, error }); return new Response("Invalid JSON-LD.", { status: 400, headers: { "Content-Type": "text/plain; charset=utf-8" } }); } ldSigVerified = false; } const jsonWithoutSig = detachSignature(json); let activity = null; if (ldSigVerified) { logger$1.debug("Linked Data Signatures are verified.", { recipient, json }); activity = await Activity.fromJsonLd(jsonWithoutSig, ctx); } else { logger$1.debug("Linked Data Signatures are not verified.", { recipient, json }); try { activity = await verifyObject(Activity, jsonWithoutSig, { contextLoader: ctx.contextLoader, documentLoader: ctx.documentLoader, keyCache, tracerProvider }); } catch (error) { logger$1.error("Failed to parse activity:\n{error}", { recipient, activity: json, error }); try { await inboxErrorHandler?.(ctx, error); } catch (error$1) { logger$1.error("An unexpected error occurred in inbox error handler:\n{error}", { error: error$1, activity: json, recipient }); } span.setStatus({ code: SpanStatusCode.ERROR, message: `Failed to parse activity:\n${error}` }); return new Response("Invalid activity.", { status: 400, headers: { "Content-Type": "text/plain; charset=utf-8" } }); } if (activity == null) logger$1.debug("Object Integrity Proofs are not verified.", { recipient, activity: json }); else logger$1.debug("Object Integrity Proofs are verified.", { recipient, activity: json }); } let httpSigKey = null; if (activity == null) { if (!skipSignatureVerification) { const key = await verifyRequest(request, { contextLoader: ctx.contextLoader, documentLoader: ctx.documentLoader, timeWindow: signatureTimeWindow, keyCache, tracerProvider }); if (key == null) { logger$1.error("Failed to verify the request's HTTP Signatures.", { recipient }); span.setStatus({ code: SpanStatusCode.ERROR, message: `Failed to verify the request's HTTP Signatures.` }); const response = new Response("Failed to verify the request signature.", { status: 401, headers: { "Content-Type": "text/plain; charset=utf-8" } }); return response; } else logger$1.debug("HTTP Signatures are verified.", { recipient }); httpSigKey = key; } activity = await Activity.fromJsonLd(jsonWithoutSig, ctx); } if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href); span.setAttribute("activitypub.activity.type", getTypeId(activity).href); const routeResult = await routeActivity({ context: ctx, json, activity, recipient, inboxListeners, inboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span