- Si creas tu propio servidor ActivityPub, es fácil que te quedes bloqueado desde la primera solicitud
Followcon un401 Unauthorizedsin explicación, y Fedify es un framework de TypeScript que saca de tu código de aplicación la carga de firmas, JSON-LD, entrega y seguridad - La autenticación del fediverso usa tanto el borrador vencido
draft-cavage-http-signatures-12como el estándarRFC 9421; si se incluyen las firmas de documentos, hay que manejar cuatro mecanismos de firma y claves RSA y Ed25519 - Incluso una misma actividad ActivityPub puede llegar en JSON-LD como string, arreglo, objeto inline, referencia URI y otras formas, así que cuanto más la implementes por tu cuenta, más código defensivo se dispersa por toda la base de código
- En la entrega distribuida surgen problemas como las “publicaciones zombi”, donde un
Deletellega antes que unCreate, y se necesitan colas, reintentos, idempotencia, garantía de orden y circuit breakers - Fedify ofrece integraciones con 13 frameworks web, adaptadores para KV y colas de mensajes, CLI, linter, depurador y OpenTelemetry, lo que permite empezar a desarrollar apps federadas sin conocer los detalles de ActivityPub
Problemas al implementar ActivityPub por cuenta propia
- Para enviar la primera actividad
Followa Mastodon hay que preparar el JSON, firmar la solicitud HTTP y hacer elPOST, pero si falla puede volver solo una línea:401 Unauthorized- La causa puede ser un desfase de reloj en el encabezado
Date, un error en el hashDigest, las mayúsculas y minúsculas de(request-target), la forma de representar la clave pública, entre otras - Si el servidor remoto no explica el motivo, hay que depurar leyendo el código de otros servidores
- La causa puede ser un desfase de reloj en el encabezado
- Fedify nació durante la creación de Hollo, un servidor de microblogging de un solo usuario
- Cuando la carga de implementar ActivityPub empezó a devorar el desarrollo del producto, se convirtió en el framework necesario antes que la propia app
- Las dificultades se concentran principalmente en los estándares de firma, las formas de los documentos JSON-LD, la entrega distribuida, las prácticas de cada implementación y la configuración básica de seguridad
No hay un único estándar de firma
- Para la autenticación entre servidores se usan firmas HTTP, pero en el fediverso real conviven el borrador vencido draft-cavage-http-signatures-12 y el estándar RFC 9421
- No se sabe qué firma acepta cada servidor hasta intentarlo, así que hay que firmar con un método, y si lo rechaza, volver a firmar con otro y recordar por servidor cuál funcionó
- Este procedimiento se conoce como double-knocking
- Las firmas HTTP solo prueban quién envía la solicitud, por lo que en situaciones como el inbox forwarding, donde una actividad recibida se reenvía a un tercero, también se necesita una firma sobre el propio documento
- Los mecanismos necesarios suman cuatro en total, incluidos Linked Data Signatures y Object Integrity Proofs
- Hay que gestionar dos tipos de claves: RSA y Ed25519
La forma de los documentos JSON-LD cambia constantemente
- El formato de transporte de ActivityPub es JSON-LD, y una actividad
Createcon el mismo significado puede representarse de muchas manerasactorpuede ser un string URI o un objetoPersoninlinetopuede ser un solo string o un arregloobjectpuede ser tanto un objeto inline como una URI
- La dirección que indica el público también puede expresarse válidamente de tres formas:
https://www.w3.org/ns/activitystreams#Public,as:PublicyPublic - Para procesarlo conforme a la especificación, hay que normalizarlo con un procesador JSON-LD mediante expansión (expansion) y luego compactación (compaction)
- Muchas implementaciones lo tratan como “solo JSON” y se rompen silenciosamente con la forma emitida por algún servidor específico
- Si lo implementas por tu cuenta, aparecen por todas partes códigos defensivos para comprobar si un valor es string, arreglo, objeto o una URI que hay que obtener
Entrega distribuida y “publicaciones zombi”
- Si un usuario publica algo y justo después lo borra al notar un error de tipeo, el servidor envía
Createy luegoDelete, pero según las condiciones de la red el servidor receptor puede recibir primero elDelete- Si ignora el borrado de una publicación que todavía no existe y después procesa el
Create, la publicación que el autor cree eliminada seguirá existiendo en ese servidor
- Si ignora el borrado de una publicación que todavía no existe y después procesa el
- Si hay 5,000 seguidores, una sola publicación genera miles de entregas HTTP; si se procesan dentro del handler de la solicitud, la respuesta del botón de publicar se retrasa o el servidor puede caerse
- Incluso usando colas, hay que decidir el calendario de reintentos de entregas fallidas, el backoff exponencial, la cantidad de reintentos, la diferencia entre
500 Internal Server Errory410 Gone, la limpieza de seguidores de servidores desaparecidos y cómo manejar hosts con fallas prolongadas - Esta área se parece más a ingeniería de sistemas distribuidos que a una simple implementación de protocolo
La especificación por sí sola no resuelve la interoperabilidad
- Aunque se cumpla perfectamente la especificación, siguen existiendo problemas de interoperabilidad con las implementaciones reales del fediverso
- El secure mode de Mastodon usa authorized fetch, que exige firmas HTTP también para solicitudes
GET- Si ambos servidores están en secure mode, aparece un bloqueo: para obtener la clave pública del otro hay que firmar, y para verificar la firma el otro primero tendría que obtener mi clave pública
- La comunidad lo sortea firmando con un instance actor que representa al propio servidor, pero esto no está en la especificación
- Threads no puede parsear actividades donde
actorviene como objeto inline, así que al enviar a Threads hay que mandaractorcomo URI - Lemmy rechaza silenciosamente si faltan campos de actor
Groupque Mastodon no exige- Ejemplos son la moderators collection vinculada mediante
attributedToy la collectionfeatured
- Ejemplos son la moderators collection vinculada mediante
- Misskey tiene sus propias extensiones de vocabulario, y solo para quote post se usan tres nombres de propiedades distintos según la implementación
- La interoperabilidad no es una tarea que se ajusta una vez y se termina, sino un área que hay que mantener continuamente
El estado predeterminado de una implementación propia no es seguro
- Si se omite la verificación de firmas de las actividades entrantes, cualquiera puede inyectar un
Followo unDeletefalsificado - Si no se limita el cargador de documentos, una actividad maliciosa puede apuntar a
http://169.254.169.254/o a una red interna y convertir el servidor en un proxy SSRF - Si se omite la verificación del origen de objetos embebidos, cualquier servidor puede emitir un documento que parezca dicho por una persona específica
- Estas trampas no se notan de inmediato, y hasta que se explotan puede parecer que todo funciona
Áreas que Fedify maneja por ti
- Fedify es una biblioteca de TypeScript para crear apps de servidores federados con ActivityPub y estándares relacionados
- Se ejecuta en Deno, Node.js y Bun, y también soporta runtimes edge como Cloudflare Workers
- Su objetivo de diseño es sacar del código de la aplicación las firmas, JSON-LD, la entrega, las diferencias entre implementaciones y los detalles de seguridad
-
Manejo de firmas
- Si registras un actor dispatcher y un key pair dispatcher, puedes publicar un actor en el fediverso
- Todas las solicitudes salientes llevan firma
- Con claves RSA emite HTTP Signatures y Linked Data Signatures
- Si agregas una clave Ed25519, también adjunta Object Integrity Proofs
- Los cuatro mecanismos coexisten en una misma actividad, y el receptor verifica con el método más fuerte que entienda
- Fedify maneja directamente el double-knocking
- El primer contacto sale con RFC 9421 y, si es rechazado, reintenta con draft-cavage
- El método exitoso se cachea por servidor
- Si la respuesta de rechazo incluye un challenge
Accept-Signature, vuelve a firmar con los componentes solicitados por el servidor
- Las firmas entrantes se verifican antes de que el código de la aplicación las vea, y las actividades cuya verificación falla no llegan al listener
- Con solo registrar el actor dispatcher también se crea un servidor WebFinger RFC 7033, de modo que se pueda encontrar el actor con el formato
@alice@example.comen el buscador de Mastodon
-
Trabajar con tipos en lugar de JSON-LD
- Fedify ofrece unas 80 clases que cubren todo el Activity Vocabulary y las principales extensiones de proveedores
- Las clases tienen tipos y son inmutables, y sus accesores absorben las diferencias de forma de documento que permite JSON-LD
lookupObject()recibe un handle y ejecuta todo el proceso de búsqueda, incluido WebFinger discovery- Los accesores como
getFollowers()funcionan igual tanto si el valor es una referencia URI como si es un objeto inline, y los valores obtenidos quedan en caché - Las diferencias entre proveedores también quedan ocultas detrás de la API
- Las tres propiedades de quote
quoteUri,_misskey_quoteyquoteUrlse unifican detrás de una sola API junto conquotedel nuevo FEP-044f - La propiedad
isCatde Misskey también existe como tipo, por lo que puede manejarse con seguridad de tipos
- Las tres propiedades de quote
-
Infraestructura de entrega y garantía de orden
- Si conectas una cola de mensajes a
createFederation(), la entrega pasa a segundo plano y, ante fallos, se reintenta automáticamente con backoff exponencial hasta un máximo predeterminado de 10 veces - Cuando una publicación se entrega a miles de seguidores, entra en acción el two-stage fan-out
- Un único mensaje consolidado entra en la cola
- Un worker en segundo plano lo divide en tareas de entrega por servidor
- El botón de publicar responde de inmediato
- Como por los reintentos una misma actividad puede llegar dos veces, Fedify omite duplicados antes del handler mediante una caché de idempotencia que conserva las actividades procesadas durante 24 horas
- Si en la llamada a
sendActivity()especificas{ orderingKey: post.id }, las actividades que comparten el mismo orderingKey se entregan a cada servidor receptor en el orden en que se enviaron- Un
Deleteno puede adelantarse a unCreate - Las actividades con claves distintas salen en paralelo para mantener el rendimiento
- Un
- Ante
404 Not Foundo410 Gone, deja de reintentar y llama al handler de fallo permanente de entrega registrado - Si se envió a una shared inbox, también puede recibir la lista de seguidores que hay detrás para limpiar cuentas desaparecidas
- Para hosts que fallan repetidamente, el circuit breaker, activado por defecto, pausa la entrega y verifica periódicamente si se recuperaron
- Si conectas una cola de mensajes a
Prácticas por implementación y valores predeterminados de seguridad
- En authorized fetch, Fedify conecta
.authorize()al dispatcher y pasa la identidad verificada del solicitante al callback- Procesos como listas de bloqueo o colecciones privadas pueden escribirse como lógica de aplicación
- También hay patrones de soporte para el problema de interbloqueo con instance actors
- El problema de Threads con actores inline se maneja mediante el activity transformer, activado por defecto, que convierte a URI los actores inline de las actividades salientes
- La moderators collection que exige Lemmy puede exponerse en unas pocas líneas con la custom collection API, y el contexto JSON-LD de Lemmy viene incluido de antemano
- Cuando se descubre un nuevo problema de interoperabilidad, la corrección entra en Fedify, no en cada aplicación
- Los valores predeterminados de seguridad apuntan al lado seguro
- La verificación de firmas no es una función que haya que activar, sino una que se desactiva para pruebas
- El cargador de documentos bloquea por defecto rangos de direcciones privadas y loopback, y también considera DNS rebinding
- Para quedar expuesto a SSRF, hay que activar explícitamente una opción con un nombre que deja claro que es para pruebas
- Si el origen de un objeto embebido difiere del documento padre, el accesor no confía en él y lo vuelve a obtener desde el original
- Este modelo de seguridad basado en origen se basa en FEP-fe34
Stack existente y herramientas de desarrollo
- Fedify está diseñado para ajustarse a los stacks web existentes y ofrece integración con 13 frameworks web
- Express, Hono, Fastify, Koa, NestJS, Elysia
- Next.js, Nuxt, SvelteKit, Astro, SolidStart, Fresh
- El middleware se encarga de la negociación de contenido, de modo que la misma URL pueda servir HTML a los navegadores y JSON-LD al fediverso
- El almacenamiento propio de Fedify solo requiere una interfaz clave-valor
- Cuenta con adaptadores para Redis, PostgreSQL, MySQL/MariaDB, SQLite, Deno KV, Cloudflare Workers KV y en memoria
- Para colas de mensajes se ofrecen 8 opciones, incluidas PostgreSQL, Redis y AMQP/RabbitMQ; si ninguna encaja, se puede implementar la interfaz directamente
- Los datos de dominio pueden quedarse tal cual en la base de datos y el ORM existentes
- Si ya se opera federation con otra biblioteca, la guía de migración y los scripts de migración de datos permiten migrar desde activitypub-express y otros sin perder los seguidores existentes
- También se ofrecen paquetes de más alto nivel
@fedify/relayproporciona un servidor relay completo de ActivityPub con una sola llamada a función@fedify/backfillrecorre el fediverso y restaura hilos de conversación incompletos
-
Herramientas para el ciclo de desarrollo
fedify initgenera el scaffolding de un proyecto con una sola líneafedify tunnelexpone el servidor local por HTTPS para poder probarlo con Mastodon realfedify inboxlevanta un servidor inbox temporal para recibir las actividades que envía el servidorfedify lookuppermite inspeccionar objetos publicados por otros servidoresfedify lookup --authorized-fetchcrea un par de claves de un solo uso, levanta un servidor ActivityPub temporal y envía solicitudes firmadas a objetos detrás de secure mode@fedify/lintes un linter específico de ActivityPub que detecta 20 errores de interoperabilidad, como cuando a un actor le faltainbox- Con los mocks de
@fedify/testingse pueden ejecutar pruebas sin red @fedify/debuggerpermite adjuntar un dashboard de depuración con una sola línea para ver en tiempo real, desde el navegador, las actividades y los resultados de verificación de firmas- En producción trae instrumentación OpenTelemetry integrada, con 28 tipos de span y 37 métricas
- También se ofrecen una guía de monitoreo y la herramienta de pruebas de carga para ActivityPub
fedify bench - La documentación oficial se compone de un manual de 30 capítulos y 5 tutoriales, y cubre prácticas reales como consultas PromQL y reglas de alerta para ver el backlog de la cola, además de propiedades para que el avatar se muestre en Mastodon
Casos de uso actuales y cómo empezar
- Fedify ya se usa en servicios reales
- El servicio ActivityPub de Ghost
- Encyclia, que conecta registros de investigadores de ORCID con el fediverso
- SiliconBeest, que funciona de forma serverless en Cloudflare Workers
- La plataforma coreana de blogging Typo Blue
- Hollo, una plataforma de microblogging de un solo usuario
- Hackers' Pub, operada por la comunidad
- Los tutoriales ofrecen ejemplos por escala
- Un servidor de archivo único de unas decenas de líneas que Mastodon puede seguir
- Un servicio para compartir imágenes de unas 750 líneas que interopera con Pixelfed en follow, likes y comentarios
- El tutorial de plataforma comunitaria aborda federation bidireccional con el lemmy.ml real
- El objetivo de Fedify no es crear más expertos en ActivityPub, sino permitir que los desarrolladores creen apps federadas sin conocer los detalles de ActivityPub
- El comando para empezar es
npm init @fedify - Si se necesita ayuda, se puede usar la sala de Matrix o GitHub Discussions
1 comentarios
Opiniones en Lobste.rs
Esta es la razón por la que hay tantos forks entre proyectos de ActivityPub: es más fácil entender el enfoque de otra persona que implementar todo desde cero.
Lo que propone el autor no parece muy distinto de los forks de Misskey o Pleroma que se ven comúnmente. Las bibliotecas también tienen su propia perspectiva y enfoque, y no parecen dar demasiado control. Aun así, tienen la ventaja de no imponer la UI como ocurre al hacer fork de un servidor completo.
Desde la perspectiva de alguien que está implementando AP, la parte más difícil es que no hay una buena forma de usar JSON-LD correctamente. Si fuera fácil convertir objetos a una representación estándar, la interacción vendría de forma natural, pero usarlo como un verdadero documento enlazado es demasiado ineficiente, y usarlo como un documento JSON crudo te mata con un montón de casos excepcionales. Hasta ahora elegí el segundo enfoque y terminé muerto.
No es exactamente el mismo problema que en el mundo de JSON-LD, pero tampoco está completamente desconectado.
Dicho eso, creo que muchas tecnologías cercanas a JSON sufren problemas parecidos. Hay demasiadas formas de expresar el mismo esquema lógico con JSON Schema, y por eso interactuar con tecnologías alrededor de JSON Schema se vuelve ridículamente espantoso. Los esquemas de OpenAPI, en particular, son un horror parecido pero no igual, y ya es bastante malo incluso sin considerar la cantidad de versiones de borradores del esquema.
El servicio “MTA” de AP se encarga de enviar mensajes desde la bandeja de salida y recibir mensajes en la bandeja de entrada. Para este servicio, los documentos JSON-LD son casi datos opacos en bloque. Hace falta algo de parsing para identificar al remitente y al destinatario, pero no mucho más. El almacenamiento también podría basarse en archivos y, si no recuerdo mal, go-ap usa algo así.
El “MUA” de AP es la aplicación real. Es la parte que debe entender la semántica de JSON-LD. Podría usar algo como PostgreSQL para guardar los documentos como jsonb y ofrecer una forma amigable para SQL mediante columnas generadas y vistas. Así se podría decidir, según el tipo de objeto, cuál es la mejor forma de representar el documento.
Como otro ejemplo, un servicio de búsqueda también podría modelarse como actor y hacer que devuelva resultados a una bandeja de salida temporal.
Es una lista muy valiosa de comportamientos peculiares de varias implementaciones y sus mitigaciones.
Lamentablemente, GoActivityPub todavía no implementó ni la mitad de eso.
Agradecí que el artículo empezara con contenido técnico, pero hacia la mitad pareció desviarse hacia la promoción de su propio framework, y eso le quitó gracia a la lectura.
Me alegra que en algunos mundos que usan TypeScript quizá no haya que redescubrir estas particularidades de implementación. Pero, como modelo mental, si hubiera un registro de “en esta condición y situación se obtiene este resultado, y hace falta esta corrección”, también quienes no están en TypeScript —por ejemplo, el autor del proyecto hermano GoActivityPub— podrían beneficiarse de ese trabajo. Aquí se cubren algunas de esas cosas, pero el artículo es una instantánea de un momento concreto, y el proyecto parece querer acumular con el tiempo todos los bugs de interoperabilidad.
La alternativa actual, a mi juicio, es leer todos los mensajes de commit que no parecen escritos por una persona y distinguir entre bugs propios de Fedify y bugs de interoperabilidad.
Es especialmente irónico que el repositorio no lleve ese tipo de registro aun cuando parece estar “all in” con la IA. Lo que he escuchado en la promoción de los LLM es que automatizan tareas repetitivas. Entonces Claude podría crear issues en GitHub o, mejor aún, documentar en un archivo .md dentro del repositorio las observaciones y cómo Fedify las corrige. Incluso tienen su propio depurador y unas “mejores prácticas” que no sé qué significan, así que sería una tarea perfecta.
¿Por qué harías inline solicitudes a servicios de terceros? Eso es básico en aplicaciones web. Si tienes que comunicarte con un servicio de terceros, mándalo a un trabajo en segundo plano. Si la información no es necesaria para responder la solicitud, mándala a un trabajo en segundo plano. Los problemas que surgen por hacer este tipo de solicitudes dentro del manejador son como pisar un rastrillo y golpearte la cara: problemas autoinfligidos, no relacionados con ActivityPub.
Si falla una entrega, hay que reintentar; cómo programarlo, si usar backoff exponencial, cuántas veces intentarlo, si tratar un 500 Internal Server Error y un 410 Gone como el mismo tipo de fallo, todo eso también son problemas generales de desarrollo de aplicaciones web. Son problemas que aparecen al hacer solicitudes a servicios de terceros desde una cola de trabajos y no tienen relación con ActivityPub. La mayoría de los frameworks web tienen valores predeterminados razonables. Solo hace falta decidir en el punto donde se determina si reintentar según el error ocurrido. Reintentar un 410 es un desperdicio, pero no es un problema urgente. Aumentará la presión de memoria de la cola de trabajos, pero es poco probable que tumbe la aplicación en cuestión de horas.
“Mira si lo rechazan, vuelve a firmar de otra manera y recuerda qué método funcionó para cada servidor”… ¿qué diablos estoy leyendo? ¿Será por esto que el desarrollo de Mastodon es lento?
“Una publicación se convierte en miles de entregas HTTP”, y eso en Ruby, un lenguaje famoso por sobresalir en programación de sistemas de red y encolado.
Cuesta creerlo; está bien que lo hayan envuelto en una biblioteca, pero aun así...
Después de implementar ActivityPub en Java, llegué a la conclusión de que este tipo de protocolo entre servidores sería mejor construirlo simplemente sobre git.
Buena parte de la complejidad existe para volver a resolver problemas que git ya resuelve mejor. Si se modela como documentos JSON dentro de un repositorio git, no hay que lidiar con paginación. El protocolo ya garantiza que solo se envíen datos que no existen del otro lado, obtienes firmas de commits, también obtienes garantía de orden de eventos, se resuelven los problemas mencionados en este artículo y el historial sale gratis. Se podría formular algo parecido a la décima ley de Greenspun: estos protocolos contienen una implementación parcial de git, llena de bugs y lenta.
Este artículo se lee como un texto de baja calidad generado por IA.
Más específicamente, no entiendo por qué lo escribió en formato narrativo. Los hechos que transmite podrían haberse presentado de forma mucho más concisa y menos sesgada, y la narrativa tampoco resulta convincente. Sobre todo porque tiene muchas expresiones típicas de la IA.
No fue una lectura agradable. Aun así, agradezco que haya señalado los problemas, y espero poder leerlos y corregirlos de otra manera.