- Ao criar um servidor ActivityPub por conta própria, é fácil esbarrar em um
401 Unauthorizedsem explicação já na primeira solicitação deFollow, e o Fedify é um framework TypeScript que tira da aplicação o peso de assinaturas, JSON-LD, entrega e segurança - A autenticação no fediverso usa ao mesmo tempo o rascunho expirado
draft-cavage-http-signatures-12e o padrãoRFC 9421, e, incluindo também as assinaturas de documento, é preciso lidar com quatro mecanismos de assinatura e chaves RSA e Ed25519 - A mesma atividade ActivityPub pode chegar no JSON-LD em vários formatos, como string, array, objeto inline ou referência URI, então, quanto mais se implementa tudo manualmente, mais código defensivo se espalha por toda a base
- Na entrega distribuída, surgem problemas como “post zumbi”, em que um
Deletechega antes de umCreate, exigindo fila, retry, idempotência, garantia de ordem e circuit breaker - O Fedify oferece integração com 13 frameworks web, adaptadores para KV e fila de mensagens, além de CLI, linter, debugger e OpenTelemetry, permitindo começar a desenvolver apps federados sem conhecimento detalhado de ActivityPub
Problemas encontrados ao implementar ActivityPub diretamente
- Para enviar a primeira atividade
Followao Mastodon, é preciso montar o JSON, assinar a requisição HTTP e fazer oPOST, mas, se falhar, pode voltar apenas uma linha com401 Unauthorized- A causa pode ser diferença de relógio no cabeçalho
Date, erro no hashDigest, maiúsculas e minúsculas em(request-target), forma de representação da chave pública etc. - Se o servidor remoto não informar o motivo, é preciso depurar lendo o código de outros servidores
- A causa pode ser diferença de relógio no cabeçalho
- O Fedify nasceu durante a criação do Hollo, um servidor de microblog para um único usuário
- Quando o peso de implementar ActivityPub começou a engolir o desenvolvimento do produto, ele acabou sendo transformado em um framework necessário antes mesmo do app
- As dificuldades se concentram principalmente em padrões de assinatura, formato de documentos JSON-LD, entrega distribuída, práticas específicas de cada implementação e configurações básicas de segurança
Não existe um único padrão de assinatura
- A autenticação entre servidores usa assinaturas HTTP, mas, no fediverso real, coexistem o rascunho expirado draft-cavage-http-signatures-12 e o padrão RFC 9421
- Não dá para saber qual assinatura cada servidor aceita antes de tentar, então é preciso assinar de um jeito, e, se for rejeitado, assinar de outro e memorizar por servidor qual funcionou
- Esse procedimento é chamado de double-knocking
- Como a assinatura HTTP só comprova o remetente da requisição, em situações como inbox forwarding, em que a atividade recebida é encaminhada a terceiros, também é necessário assinar o próprio documento
- Os mecanismos necessários somam quatro, incluindo Linked Data Signatures e Object Integrity Proofs
- Também é preciso gerenciar em conjunto dois tipos de chave: RSA e Ed25519
O formato dos documentos JSON-LD muda o tempo todo
- O formato de transporte do ActivityPub é JSON-LD, e até uma atividade
Createcom o mesmo significado pode ser representada de várias formasactorpode ser uma string URI ou um objetoPersoninlinetopode ser uma única string ou um arrayobjectpode ser tanto um objeto inline quanto uma URI
- Até o endereço que indica visibilidade pública aceita três representações válidas:
https://www.w3.org/ns/activitystreams#Public,as:PublicePublic - Para tratar isso conforme a especificação, é preciso normalizar com um processador JSON-LD, fazendo expansion e depois compaction
- Muitas implementações tratam isso como “apenas JSON” e acabam quebrando silenciosamente em formatos emitidos por servidores específicos
- Ao implementar manualmente, surgem por toda parte códigos defensivos para verificar se um valor é string, array, objeto ou uma URI que precisa ser buscada
Entrega distribuída e “post zumbi”
- Se um usuário publica um texto e logo em seguida o apaga ao notar um erro, o servidor envia
Createe depoisDelete, mas, dependendo da rede, o servidor receptor pode receber primeiro oDelete- Se ele ignorar a exclusão de um post que ainda não existe e depois processar o
Create, o texto que o autor acredita ter apagado continuará naquele servidor
- Se ele ignorar a exclusão de um post que ainda não existe e depois processar o
- Se houver 5 mil seguidores, um único post gera milhares de entregas HTTP, e, se isso for tratado dentro do handler da requisição, a resposta do botão de publicar pode atrasar ou o servidor pode cair
- Mesmo com fila, ainda é preciso definir agenda de retries para entregas com falha, backoff exponencial, número de tentativas, diferença entre
500 Internal Server Errore410 Gone, limpeza de seguidores em servidores desaparecidos e tratamento de hosts com falha prolongada - Essa área está mais próxima de engenharia de sistemas distribuídos do que de uma simples implementação de protocolo
Só a especificação não resolve a interoperabilidade
- Mesmo seguindo a especificação à risca, ainda restam problemas de interoperabilidade com implementações reais do fediverso
- O secure mode do Mastodon usa authorized fetch, que exige assinatura HTTP até para requisições
GET- Se os dois servidores estiverem em secure mode, surge um impasse: para buscar a chave pública do outro, é preciso assinar, mas, para validar a assinatura, o outro precisa antes buscar a minha chave pública
- A comunidade contorna isso assinando com um instance actor que representa o próprio servidor, mas isso não está na especificação
- O Threads não consegue fazer parse de atividades em que
actorvem como objeto inline, então, ao enviar para o Threads, é preciso mandaractorcomo URI - O Lemmy rejeita silenciosamente se faltarem campos de actor
Groupque o Mastodon não exige- Exemplos incluem a collection de moderadores ligada por
attributedToe a collectionfeatured
- Exemplos incluem a collection de moderadores ligada por
- O Misskey tem extensões próprias de vocabulário, e até para quote post há três nomes de propriedade usados por implementações diferentes
- Interoperabilidade não é algo que se ajusta uma vez e termina; é uma área que precisa de manutenção contínua
O estado padrão da implementação manual não é seguro
- Se a verificação de assinatura das atividades recebidas for ignorada, qualquer pessoa poderá injetar
FollowouDeletefalsos - Se o document loader não for restringido, uma atividade maliciosa pode apontar para
http://169.254.169.254/ou para a rede interna e transformar o servidor em um proxy SSRF - Se a verificação de origem de objetos embutidos for omitida, qualquer servidor pode publicar um documento como se tivesse sido dito por uma pessoa específica
- Essas armadilhas não ficam evidentes de imediato e, até serem exploradas, tudo pode parecer estar funcionando
O que o Fedify assume
- Fedify é uma biblioteca TypeScript para criar apps de servidor federado com ActivityPub e padrões relacionados
- Roda em Deno, Node.js e Bun, e também oferece suporte a runtimes de edge como Cloudflare Workers
- O objetivo do design é remover da lógica da aplicação assinaturas, JSON-LD, entrega, diferenças entre implementações e detalhes de segurança
-
Tratamento de assinaturas
- Ao registrar um actor dispatcher e um key pair dispatcher, é possível publicar um ator no fediverso
- Todas as requisições de saída recebem assinatura
- Com chaves RSA, ele gera HTTP Signatures e Linked Data Signatures
- Se você adicionar chaves Ed25519, também inclui Object Integrity Proofs
- Os quatro mecanismos podem coexistir em uma única atividade, e o destinatário valida usando a forma mais forte que entender
- O Fedify trata diretamente o double-knocking
- O primeiro contato sai via RFC 9421 e, se for rejeitado, tenta novamente com draft-cavage
- O método que funcionar é armazenado em cache por servidor
- Se a resposta de rejeição tiver um challenge
Accept-Signature, ele assina novamente com os componentes solicitados pelo servidor
- As assinaturas de entrada são validadas antes que o código da aplicação as veja, e atividades com falha na validação não chegam ao listener
- Só de registrar um actor dispatcher, também é criado um servidor WebFinger RFC 7033, permitindo encontrar atores no campo de busca do Mastodon no formato
@alice@example.com
-
Lida com tipos em vez de JSON-LD
- O Fedify oferece cerca de 80 classes que cobrem todo o Activity Vocabulary e as principais extensões de vendors
- As classes são tipadas e imutáveis, e os acessores absorvem as diferenças de formato de documento permitidas pelo JSON-LD
lookupObject()recebe um handle e executa todo o processo de busca, incluindo descoberta via WebFinger- Acessores como
getFollowers()funcionam da mesma forma, seja o valor uma referência URI ou um objeto inline, e os valores obtidos ficam em cache - Diferenças entre vendors também ficam escondidas atrás da API
- As três propriedades de citação
quoteUri,_misskey_quoteequoteUrlsão unificadas atrás de uma única API, junto comquotedo recém-aparecido FEP-044f - A propriedade
isCatdo Misskey também existe como tipo, permitindo tratamento com segurança de tipos
- As três propriedades de citação
-
Infraestrutura de entrega e garantia de ordem
- Se você conectar uma fila de mensagens ao
createFederation(), a entrega vai para o background e, em caso de falha, é repetida automaticamente com backoff exponencial até o padrão de 10 tentativas - Quando uma única postagem precisa ser entregue a milhares de seguidores, entra em ação o two-stage fan-out
- Uma única mensagem consolidada entra na fila
- Um worker em background a divide em tarefas de entrega por servidor
- O botão de publicar responde imediatamente
- Como uma atividade pode chegar duas vezes por causa de tentativas repetidas, o Fedify ignora duplicatas antes do handler com um cache de idempotência que guarda atividades processadas por 24 horas
- Se você passar
{ orderingKey: post.id }na chamada desendActivity(), as atividades que compartilham a mesma orderingKey são entregues a cada servidor destinatário na ordem em que foram enviadas- Um
Deletenão pode ultrapassar umCreate - Atividades com chaves diferentes seguem em paralelo para manter o throughput
- Um
- Em casos de
404 Not Foundou410 Gone, ele interrompe as novas tentativas e chama o handler de falha permanente de entrega registrado - Se o envio foi para uma shared inbox, ele também pode obter a lista de seguidores por trás dela para limpar contas que desapareceram
- Para hosts que falham repetidamente, o circuit breaker, ativado por padrão, suspende a entrega e verifica periodicamente a recuperação
- Se você conectar uma fila de mensagens ao
Práticas específicas de implementação e padrões de segurança
- No authorized fetch, o Fedify conecta
.authorize()ao dispatcher e entrega a identidade verificada do solicitante ao callback- Tratamentos como lista de bloqueio e coleções privadas podem ser escritos como lógica da aplicação
- Também há um padrão compatível com o problema de deadlock do instance actor
- O problema de actor inline do Threads é tratado por activity transformers, ativados por padrão, que convertem actors inline em URIs nas atividades de saída
- A collection de moderators exigida pelo Lemmy pode ser exposta em poucas linhas com a API de custom collection, e o contexto JSON-LD do Lemmy já vem incluído
- Quando um novo problema de interoperabilidade é encontrado, a correção entra no Fedify, não em cada aplicação individualmente
- Os padrões de segurança pendem para o lado mais seguro
- A verificação de assinatura não é um recurso que precisa ser ativado; é um recurso que só se desativa em testes
- O document loader bloqueia por padrão faixas de endereços privados e loopback, e também leva em conta DNS rebinding
- Para ficar exposto a SSRF, é preciso ativar explicitamente uma opção com nome que deixa claro que ela é para testes
- Se a origem de um objeto embutido for diferente da do documento pai, o acessor não confia nele e busca novamente a partir da origem
- Esse modelo de segurança baseado em origem se baseia no FEP-fe34
Stack existente e ferramentas de desenvolvimento
- O Fedify foi projetado para se adequar às stacks web existentes e oferece integração com 13 frameworks web
- Express, Hono, Fastify, Koa, NestJS, Elysia
- Next.js, Nuxt, SvelteKit, Astro, SolidStart, Fresh
- O middleware lida com a negociação de conteúdo, permitindo que a mesma URL forneça HTML para o navegador e JSON-LD para o fediverso
- O armazenamento do próprio Fedify exige apenas uma interface de chave-valor
- Há adaptadores para Redis, PostgreSQL, MySQL/MariaDB, SQLite, Deno KV, Cloudflare Workers KV e in-memory
- A fila de mensagens oferece 8 opções, incluindo PostgreSQL, Redis e AMQP/RabbitMQ; se nenhuma servir, é possível implementar a interface diretamente
- Os dados de domínio podem continuar no banco de dados e ORM existentes
- Se você já opera federation com outra biblioteca, o guia de migração e os scripts de migração de dados permitem migrar do activitypub-express e similares sem perder seguidores existentes
- Pacotes de nível superior também são oferecidos
@fedify/relayfornece um servidor relay ActivityPub completo com uma única chamada de função@fedify/backfillpercorre o fediverso e restaura threads de conversa incompletas
-
Ferramentas para o loop de desenvolvimento
fedify initfaz o scaffold do projeto em uma linhafedify tunnelexpõe o servidor local via HTTPS para permitir testes com um Mastodon realfedify inboxsobe um servidor inbox temporário para receber atividades enviadas pelo seu servidorfedify lookuppermite inspecionar objetos publicados por outros servidoresfedify lookup --authorized-fetchcria um par de chaves descartável e levanta um servidor ActivityPub temporário para enviar requisições assinadas a objetos atrás do secure mode@fedify/linté um linter específico para ActivityPub que detecta 20 bugs de interoperabilidade, como actors seminbox- Com os mocks de
@fedify/testing, é possível rodar testes sem rede @fedify/debuggeradiciona um dashboard de depuração com uma linha, permitindo ver em tempo real no navegador as atividades e os resultados da verificação de assinatura- Em produção, há instrumentação OpenTelemetry embutida, com 28 tipos de span e 37 métricas
- Também são oferecidos um guia de monitoramento e a ferramenta de teste de carga para ActivityPub
fedify bench - A documentação oficial é composta por um manual de 30 capítulos e 5 tutoriais, cobrindo práticas reais como consultas PromQL e regras de alerta para visualizar o backlog da fila, além de propriedades para fazer o avatar aparecer no Mastodon
Casos de uso reais e como começar
- O Fedify já é usado em serviços reais
- O serviço ActivityPub do Ghost
- Encyclia, que conecta registros de pesquisadores do ORCID ao fediverso
- SiliconBeest, que roda de forma serverless no Cloudflare Workers
- A plataforma coreana de blogs Typo Blue
- A plataforma de microblogging para um único usuário Hollo
- O Hackers' Pub, operado pela comunidade
- Os tutoriais oferecem exemplos para diferentes escalas
- Um servidor de arquivo único com algumas dezenas de linhas que o Mastodon pode seguir
- Um serviço de compartilhamento de imagens com cerca de 750 linhas interoperando com o Pixelfed em seguir, curtir e comentar
- O tutorial de plataforma comunitária cobre federation bidirecional com o lemmy.ml real
- O objetivo do Fedify não é criar mais especialistas em ActivityPub, mas permitir que desenvolvedores construam apps federados sem precisar conhecer os detalhes do ActivityPub
- O comando para começar é
npm init @fedify - Se precisar de ajuda, você pode usar o room no Matrix ou o GitHub Discussions
1 comentários
Opiniões no Lobste.rs
É por isso que há tantos forks uns dos outros em projetos ActivityPub: é mais fácil entender a abordagem de outra pessoa do que implementar tudo diretamente
O que o autor propõe também não parece muito diferente, na prática, dos forks de Misskey ou Pleroma que se veem com frequência. Bibliotecas também têm suas próprias perspectivas e abordagens, e não parecem dar tanto controle assim. Ainda assim, têm a vantagem de não impor a UI como acontece ao fazer fork de um servidor inteiro
Do ponto de vista de quem está implementando AP, a parte mais difícil é que não há uma boa maneira de usar JSON-LD corretamente. Se fosse possível converter objetos facilmente para uma representação padrão, a interoperabilidade viria naturalmente; mas, para usar como documentos realmente vinculados, é ineficiente demais, e, se você tratar como documentos JSON brutos, acaba morrendo em incontáveis casos de exceção. Até agora, escolhi a segunda abordagem e morri
Não é exatamente o mesmo problema do mundo JSON-LD, mas também não é totalmente alheio a ele
Dito isso, acho que boa parte das tecnologias adjacentes a JSON sofre problemas parecidos. Há maneiras demais em JSON Schema de representar o mesmo esquema lógico e, por causa disso, interagir com tecnologias ao redor de JSON Schema se torna ridiculamente horrível. Em especial, esquemas OpenAPI são um terror parecido, mas não idêntico; e, mesmo sem considerar a quantidade de versões de rascunho do schema, já é ruim o bastante
O serviço “MTA” de AP ficaria responsável por enviar mensagens a partir da outbox e receber mensagens na inbox. Do ponto de vista desse serviço, documentos JSON-LD são quase dados opacos em bloco. É preciso algum parsing para descobrir remetentes e destinatários, mas não muito além disso. O armazenamento também poderia ser baseado em arquivos e, se me lembro bem, o go-ap usa algo nessa linha
O “MUA” de AP é a aplicação de fato. É o lado que precisa entender a semântica do JSON-LD. Talvez seja possível usar algo como PostgreSQL para armazenar documentos como jsonb e oferecer uma forma mais amigável a SQL por meio de colunas geradas e views. Assim, dá para escolher, conforme o tipo de objeto, a forma mais adequada de representar o documento
Como outro exemplo, um serviço de busca também poderia ser modelado como ator e retornar resultados por uma outbox temporária
É uma lista extremamente valiosa de comportamentos peculiares de várias implementações e suas mitigações
Infelizmente, no GoActivityPub ainda não implementei nem metade disso
No começo, fiquei grato porque o texto começou com conteúdo técnico, mas do meio em diante pareceu mudar de rumo para uma promoção do próprio framework, e isso tirou o prazer da leitura
É bom que, em alguns mundos que usam TypeScript, talvez não seja preciso redescobrir essas peculiaridades de implementação. Mas, como modelo mental, se houvesse um registro do tipo “nestas condições e circunstâncias, este resultado acontece, e esta correção é necessária”, pessoas fora do TypeScript — por exemplo, o autor do projeto irmão GoActivityPub — também poderiam se beneficiar desse esforço. O texto aborda algumas dessas coisas, mas é um snapshot de um determinado momento, enquanto o projeto parece tentar acumular todos os bugs de interoperabilidade ao longo do tempo
A alternativa atual, pelo que vejo, é ler todas as mensagens de commit que não foram escritas por humanos e separar bugs do próprio Fedify de bugs de interoperabilidade
É especialmente irônico que o repositório não mantenha esse tipo de contabilidade, embora pareça ter apostado “tudo” em IA. O discurso que tenho ouvido sobre LLMs é que eles automatizam tarefas repetitivas. Então bastaria fazer o Claude criar issues no GitHub ou, melhor ainda, documentar em arquivos .md dentro do repositório os resultados observados e como o Fedify os corrige. Ele já tem seu próprio depurador e umas “boas práticas” que não sei bem o que significam, então seria uma tarefa perfeita
Por que executar inline requisições para serviços de terceiros? Isso é básico de aplicação web. Se você precisa se comunicar com um serviço de terceiros, mande para um job em background. Se a informação não é necessária para responder à requisição, mande para um job em background. Os problemas que surgem ao fazer esse tipo de requisição dentro do handler são autoprovocados, no nível de pisar em um ancinho e levar o cabo na cara; não têm nada a ver com ActivityPub
Se a entrega falhar, será preciso tentar de novo; como agendar isso, se usar backoff exponencial, quantas vezes tentar, se tratar 500 Internal Server Error e 410 Gone como o mesmo tipo de falha — tudo isso também é problema comum de desenvolvimento de aplicações web. É o tipo de problema que surge ao chamar serviços de terceiros a partir de uma fila de jobs e não tem relação com ActivityPub. A maioria dos frameworks web tem padrões razoáveis. Só é preciso decidir na hora em que a tentativa de novo depende do tipo de erro ocorrido. Tentar novamente um 410 é desperdício, mas não é um problema urgente a resolver. Vai aumentar a pressão de memória da fila de jobs, mas dificilmente derrubará a aplicação em poucas horas
“Veja se é rejeitado, assine de novo de outra forma e lembre qual método funcionou para cada servidor” — fico me perguntando o que é que estou lendo. Será que é por isso que o desenvolvimento do Mastodon é lento?
“Uma única publicação vira milhares de entregas HTTP”, ainda por cima em Ruby, uma linguagem famosa por ser excelente em programação de sistemas de rede e enfileiramento
É difícil de acreditar; é bom que tenham encapsulado isso numa biblioteca, mas ainda assim é meio complicado
Depois de implementar ActivityPub em Java, cheguei à conclusão de que esse tipo de protocolo entre servidores seria melhor construído simplesmente em cima de git
Boa parte da complexidade existe para resolver de novo problemas que o git já resolve melhor. Se isso for modelado como documentos JSON dentro de um repositório git, não é preciso lidar com paginação. O protocolo já garante enviar apenas dados que ainda não existem, você ganha assinatura de commits, garantia de ordenação de eventos, resolve também os problemas mencionados neste texto, e ainda ganha histórico de graça. Parece dar para formular algo parecido com a décima regra de Greenspun: protocolos desse tipo contêm uma meia implementação de git, bugada e lenta
Este texto soa como um texto de baixa qualidade gerado por IA
Mais especificamente, não entendo por que foi escrito em formato de história. Os fatos transmitidos aqui poderiam ter sido apresentados de forma muito mais concisa e menos enviesada, e a narrativa também não convence. Especialmente porque há muitas expressões típicas de IA
Não foi uma leitura agradável. Ainda assim, agradeço por apontar os problemas e espero conseguir ler sobre eles e corrigi-los de outra forma