@fedify/fedify
Version:
An ActivityPub server framework
582 lines (581 loc) • 24.8 kB
JavaScript
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 };