UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

582 lines (581 loc) • 24.8 kB
import "@js-temporal/polyfill"; import "urlpattern-polyfill"; globalThis.addEventListener = () => {}; import { n as RouterError, t as Router } from "./router-CrMLXoOr.mjs"; import { n as version, t as name } from "./deno-DMg4SgCb.mjs"; import { t as ActivityListenerSet } from "./activity-listener-ell7W1s9.mjs"; import { Tombstone, getTypeId } from "@fedify/vocab"; import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api"; import { getLogger } from "@logtape/logtape"; //#region src/federation/builder.ts function validateSingleIdentifierVariablePath(path, errorMessage) { const operatorMatches = globalThis.Array.from(path.matchAll(/{([+#./;?&]?)([A-Za-z_][A-Za-z0-9_]*)}/g)); if (operatorMatches.length !== 1 || operatorMatches[0]?.[2] !== "identifier") throw new RouterError(errorMessage); if (operatorMatches.some((match) => [ "?", "&", "#" ].includes(match[1]) && match[2] === "identifier")) throw new RouterError(errorMessage); const variables = new Router().add(path, "outbox"); if (variables.size !== 1 || !variables.has("identifier")) throw new RouterError(errorMessage); } var FederationBuilderImpl = class { router; actorCallbacks; nodeInfoDispatcher; webFingerLinksDispatcher; objectCallbacks; objectTypeIds; inboxPath; outboxPath; inboxCallbacks; outboxCallbacks; followingCallbacks; followersCallbacks; likedCallbacks; featuredCallbacks; featuredTagsCallbacks; inboxListeners; outboxListeners; inboxErrorHandler; outboxListenerErrorHandler; outboxAuthorizePredicate; sharedInboxKeyDispatcher; unverifiedActivityHandler; outboxPermanentFailureHandler; idempotencyStrategy; collectionTypeIds; collectionCallbacks; /** * Symbol registry for unique identification of unnamed symbols. */ #symbolRegistry = /* @__PURE__ */ new Map(); constructor() { this.router = new Router(); this.objectCallbacks = {}; this.objectTypeIds = {}; this.collectionCallbacks = {}; this.collectionTypeIds = {}; } async build(options) { const { FederationImpl } = await import("./middleware-madKLp2f.mjs"); const f = new FederationImpl(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.webFingerLinksDispatcher = this.webFingerLinksDispatcher; f.objectCallbacks = { ...this.objectCallbacks }; f.objectTypeIds = { ...this.objectTypeIds }; f.inboxPath = this.inboxPath; f.outboxPath = this.outboxPath; 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.outboxListeners = this.outboxListeners?.clone(); f.inboxErrorHandler = this.inboxErrorHandler; f.outboxListenerErrorHandler = this.outboxListenerErrorHandler; f.outboxAuthorizePredicate = this.outboxAuthorizePredicate; f.sharedInboxKeyDispatcher = this.sharedInboxKeyDispatcher; f.unverifiedActivityHandler = this.unverifiedActivityHandler; f.outboxPermanentFailureHandler = this.outboxPermanentFailureHandler; f.idempotencyStrategy = this.idempotencyStrategy; return f; } _getTracer() { return trace.getTracer(name, 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")) throw new RouterError("Path for actor dispatcher must have one variable: {identifier}"); const callbacks = { dispatcher: async (context, identifier) => { const actor = await this._getTracer().startActiveSpan("activitypub.dispatch_actor", { kind: SpanKind.SERVER, attributes: { "fedify.actor.identifier": identifier } }, async (span) => { try { const actor = await dispatcher(context, identifier); span.setAttribute("activitypub.actor.id", (actor?.id ?? context.getActorUri(identifier)).href); if (actor == null) span.setStatus({ code: SpanStatusCode.ERROR }); else span.setAttribute("activitypub.actor.type", getTypeId(actor).href); return actor; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) }); throw error; } finally { span.end(); } }); if (actor == null) return null; const logger = getLogger([ "fedify", "federation", "actor" ]); if (actor.id == null) logger.warn("Actor dispatcher returned an actor without an id property. Set the property with Context.getActorUri(identifier)."); else if (actor.id.href != context.getActorUri(identifier).href) logger.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 (actor instanceof Tombstone) return actor; if (this.followingCallbacks != null && this.followingCallbacks.dispatcher != null) { if (actor.followingId == null) logger.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.getFollowingUri(identifier).href) logger.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.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.getFollowersUri(identifier).href) logger.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.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.getOutboxUri(identifier).href) logger.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.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.getLikedUri(identifier).href) logger.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.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.getFeaturedUri(identifier).href) logger.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.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.getFeaturedTagsUri(identifier).href) logger.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.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.getInboxUri(identifier).href) logger.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.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.getInboxUri().href) logger.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.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.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) => { 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(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."); if (this.router.add(path, "nodeInfo").size !== 0) throw new RouterError("Path for NodeInfo dispatcher must have no variables."); this.nodeInfoDispatcher = dispatcher; } setWebFingerLinksDispatcher(dispatcher) { this.webFingerLinksDispatcher = 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) => { return this._getTracer().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")) throw new RouterError("Path for inbox dispatcher must have one variable: {identifier}"); 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.outboxCallbacks != null) throw new RouterError("Outbox dispatcher already set."); if (this.router.has("outbox")) { if (this.outboxPath !== path) throw new RouterError("Outbox dispatcher path must match outbox listener path."); } else { validateSingleIdentifierVariablePath(path, "Path for outbox dispatcher must have one variable: {identifier}"); this.router.add(path, "outbox"); this.outboxPath = path; } 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; } setOutboxListeners(outboxPath) { if (this.outboxListeners != null) throw new RouterError("Outbox listeners already set."); if (this.router.has("outbox")) { if (this.outboxPath !== outboxPath) throw new RouterError("Outbox listener path must match outbox dispatcher path."); } else { validateSingleIdentifierVariablePath(outboxPath, "Path for outbox must have one variable: {identifier}"); this.router.add(outboxPath, "outbox"); this.outboxPath = outboxPath; } const listeners = this.outboxListeners = new ActivityListenerSet(); const setters = { on(type, listener) { listeners.add(type, listener); return setters; }, onError: (handler) => { this.outboxListenerErrorHandler = handler; return setters; }, authorize: (predicate) => { this.outboxAuthorizePredicate = 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")) throw new RouterError("Path for following collection dispatcher must have one variable: {identifier}"); 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")) throw new RouterError("Path for followers collection dispatcher must have one variable: {identifier}"); 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")) throw new RouterError("Path for liked collection dispatcher must have one variable: {identifier}"); 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")) throw new RouterError("Path for featured collection dispatcher must have one variable: {identifier}"); 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")) throw new RouterError("Path for featured tags collection dispatcher must have one variable: {identifier}"); 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")) throw new RouterError("Path for inbox must have one variable: {identifier}"); this.inboxPath = inboxPath; } if (sharedInboxPath != null) { if (this.router.add(sharedInboxPath, "sharedInbox").size !== 0) throw new RouterError("Path for shared inbox must have no variables."); } const listeners = this.inboxListeners = new ActivityListenerSet(); const setters = { on(type, listener) { listeners.add(type, listener); return setters; }, onError: (handler) => { this.inboxErrorHandler = handler; return setters; }, onUnverifiedActivity: (handler) => { this.unverifiedActivityHandler = handler; return setters; }, setSharedKeyDispatcher: (dispatcher) => { this.sharedInboxKeyDispatcher = dispatcher; return setters; }, withIdempotency: (strategy) => { this.idempotencyStrategy = strategy; return setters; } }; return setters; } setCollectionDispatcher(name, itemType, path, dispatcher) { return this.#setCustomCollectionDispatcher(name, "collection", itemType, path, dispatcher); } setOrderedCollectionDispatcher(name, itemType, path, dispatcher) { return this.#setCustomCollectionDispatcher(name, "orderedCollection", itemType, path, dispatcher); } #setCustomCollectionDispatcher(name, collectionType, itemType, path, dispatcher) { const strName = String(name); const routeName = `${collectionType}:${this.#uniqueCollectionId(name)}`; if (this.router.has(routeName)) throw new RouterError(`Collection dispatcher for ${strName} already set.`); if (this.collectionCallbacks[name] != null) throw new RouterError(`Collection dispatcher for ${strName} already set.`); if (this.router.add(path, routeName).size < 1) throw new RouterError("Path for collection dispatcher must have at least one variable."); const callbacks = { dispatcher }; this.collectionCallbacks[name] = callbacks; this.collectionTypeIds[name] = itemType; 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; } /** * Get the URL path for a custom collection. * If the collection is not registered, returns null. * @template TParam The parameter names of the requested URL. * @param {string | symbol} name The name of the custom collection. * @param {TParam} values The values to fill in the URL parameters. * @returns {string | null} The URL path for the custom collection, or null if not registered. */ getCollectionPath(name, values) { if (!(name in this.collectionCallbacks)) return null; const routeName = this.#uniqueCollectionId(name); return this.router.build(`collection:${routeName}`, values) ?? this.router.build(`orderedCollection:${routeName}`, values); } setOutboxPermanentFailureHandler(handler) { this.outboxPermanentFailureHandler = handler; } /** * Converts a name (string or symbol) to a unique string identifier. * For symbols, generates and caches a UUID if not already present. * For strings, returns the string as-is. * @param name The name to convert to a unique identifier * @returns A unique string identifier */ #uniqueCollectionId(name) { if (typeof name === "string") return name; if (!this.#symbolRegistry.has(name)) this.#symbolRegistry.set(name, crypto.randomUUID()); return this.#symbolRegistry.get(name); } }; /** * Creates a new {@link FederationBuilder} instance. * @returns A new {@link FederationBuilder} instance. * @since 1.6.0 */ function createFederationBuilder() { return new FederationBuilderImpl(); } //#endregion export { createFederationBuilder as n, FederationBuilderImpl as t };