2 puntos por GN⁺ 5 시간 전 | 1 comentarios | Compartir por WhatsApp
  • Si creas tu propio servidor ActivityPub, es fácil que te quedes bloqueado desde la primera solicitud Follow con un 401 Unauthorized sin 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-12 como el estándar RFC 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 Delete llega antes que un Create, 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 Follow a Mastodon hay que preparar el JSON, firmar la solicitud HTTP y hacer el POST, 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 hash Digest, 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
  • 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ó
  • 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

La forma de los documentos JSON-LD cambia constantemente

  • El formato de transporte de ActivityPub es JSON-LD, y una actividad Create con el mismo significado puede representarse de muchas maneras
    • actor puede ser un string URI o un objeto Person inline
    • to puede ser un solo string o un arreglo
    • object puede 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:Public y Public
  • 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 Create y luego Delete, pero según las condiciones de la red el servidor receptor puede recibir primero el Delete
    • 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 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 Error y 410 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 actor viene como objeto inline, así que al enviar a Threads hay que mandar actor como URI
  • Lemmy rechaza silenciosamente si faltan campos de actor Group que Mastodon no exige
    • Ejemplos son la moderators collection vinculada mediante attributedTo y la collection featured
  • 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 Follow o un Delete falsificado
  • 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.com en 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_quote y quoteUrl se unifican detrás de una sola API junto con quote del nuevo FEP-044f
      • La propiedad isCat de Misskey también existe como tipo, por lo que puede manejarse con seguridad de tipos
  • 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 Delete no puede adelantarse a un Create
      • Las actividades con claves distintas salen en paralelo para mantener el rendimiento
    • Ante 404 Not Found o 410 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

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/relay proporciona un servidor relay completo de ActivityPub con una sola llamada a función
    • @fedify/backfill recorre el fediverso y restaura hilos de conversación incompletos
  • Herramientas para el ciclo de desarrollo

    • fedify init genera el scaffolding de un proyecto con una sola línea
    • fedify tunnel expone el servidor local por HTTPS para poder probarlo con Mastodon real
    • fedify inbox levanta un servidor inbox temporal para recibir las actividades que envía el servidor
    • fedify lookup permite inspeccionar objetos publicados por otros servidores
    • fedify lookup --authorized-fetch crea 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/lint es un linter específico de ActivityPub que detecta 20 errores de interoperabilidad, como cuando a un actor le falta inbox
    • Con los mocks de @fedify/testing se pueden ejecutar pruebas sin red
    • @fedify/debugger permite 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
  • 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

 
GN⁺ 5 시간 전
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.

    • En particular, si pensamos en las firmas, el problema de la “representación estándar de un objeto” se vuelve aún más importante. La antigua canonicalización de XML existía precisamente por este problema de las firmas: garantizar que la serialización en bytes del receptor coincidiera con la del emisor.
      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.
    • He estado pensando en implementar un servidor AP, pero todavía no empecé, así que tómese esto con mucha cautela. Una cosa que podría ayudar es dividir la aplicación en servicios más pequeños y apoyarse más en el modelo de actores para que parezca una interfaz “integrada”. Por ejemplo, se puede aprender de la separación entre MTA y MUA en un servidor de correo.
      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.

    • De verdad exagera problemas triviales y los presenta como si fueran un fracaso de ActivityPub. Por ejemplo: si tienes 5.000 seguidores, una publicación se convierte en miles de entregas HTTP; si haces eso directamente dentro del manejador de la solicitud, la respuesta del botón de publicar tarda 30 segundos o el servidor se cae, así que usa una cola.
      ¿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.

    • git no es una gran elección porque el historial depende de los commits padre. Sin embargo, un protocolo de gossip con árbol de Merkle que funcione con una estrategia de negociación similar podría encajar bien.
  • 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.