← Todos os Posts

Fonte única da verdade: SwiftData, MCP, iCloud

O Get Bananas tem três chamadores que podem escrever na mesma lista de compras. Um humano tocando linhas no app iOS. A Apple Intelligence roteando uma solicitação da Siri por meio de um AppIntent. Uma sessão do Claude Code chamando uma ferramenta MCP por stdio. A lista é uma coisa canônica na cabeça do usuário; a questão é onde ela mora no armazenamento e qual chamador vence quando eles discordam.

O post de síntese nomeou as três superfícies de um app iOS: humano, Apple Intelligence, agente. Cada superfície precisa ler e escrever no mesmo estado de domínio. Essa demanda é o que produz o erro arquitetural que aparece em apps demais: cada superfície ganha seu próprio armazenamento, as superfícies divergem, e o usuário vê três versões diferentes da sua lista dependendo de qual ele tocou por último. O padrão que sobrevive é uma fonte única da verdade com caminhos explícitos de sincronização entre as superfícies e o substrato.

O post nomeia as escolhas de substrato, as regras de resolução de conflitos que cada uma força e a arquitetura que se sustenta quando todas as três classes de chamadores podem escrever. O passo a passo usa o layout real do Get Bananas: SwiftData para estado dentro do app, um arquivo JSON no iCloud Drive para sincronização entre processos, um servidor MCP lendo e escrevendo no mesmo arquivo a partir de fora do sandbox do iOS.1

TL;DR

  • Três substratos compõem: SwiftData (em processo, rápido, com schema tipado), iCloud Drive (entre processos, baseado em arquivos, sincronizável), NSUbiquitousKeyValueStore (entre dispositivos, chave-valor, apenas para payloads pequenos).
  • Escolha qual substrato é a fonte da verdade por domínio. A resolução de conflitos é uma consequência forçada da escolha, não um problema separado.
  • As três superfícies (humano, Apple Intelligence, agente) interagem com o substrato por meio da camada de domínio. A camada de domínio é onde a política de resolução de conflitos vive.
  • Um timestamp lastModified em toda entidade mutável é a primitiva barata de resolução de conflitos; “o último escritor vence” é a política barata. Apps que precisam de garantias mais fortes pagam por elas com lógica de merge explícita.
  • Um servidor MCP fora do processo do app não consegue ler o SwiftData. A ponte é uma camada de serialização (arquivo JSON no iCloud Drive, container de App Group, etc.) com a qual tanto o leitor SwiftData dentro do app quanto o servidor MCP fora do processo podem conversar.

Os três substratos

A Apple oferece a apps iOS em produção três substratos nativos de persistência que aparecem em padrões entre processos e entre dispositivos. Cada um tem um formato específico; misturá-los sem um plano produz o problema de divergência.

SwiftData. Armazenamento persistente em processo apoiado pelo Core Data.2 Rápido, com schema tipado, consultável por meio de @Query, integrado ao sistema de observação do SwiftUI. O armazenamento pertence ao processo do app. Extensões do app (widgets, intents, share extensions) podem compartilhar um container SwiftData via App Group com configuração explícita; um processo MCP externo arbitrário rodando na máquina de um desenvolvedor fora do contexto de assinatura do app não consegue alcançar o container SwiftData de forma segura. Linhas são endereçáveis por PersistentIdentifier (em processo) ou por chaves naturais @Attribute(.unique) (entre processos, entre dispositivos). Migrações são declarativas via VersionedSchema e MigrationPlan (cobertas em O custo real do SwiftData é disciplina de schema).

iCloud Drive. Sincronização entre dispositivos baseada em arquivos, exposta por meio de URLs do FileManager no container iCloud do usuário.3 Os arquivos aparecem em todos os dispositivos em que o usuário está logado. A resolução de conflitos é no nível do arquivo: o iCloud usa NSFileVersion para rastrear edições concorrentes, e o app lê o log de conflitos para decidir o que manter. Arquivos no iCloud Drive são endereçáveis de fora do processo do app iOS: um servidor MCP no Mac pode abrir o mesmo arquivo JSON que o app iOS lê. O substrato é o que faz a integração MCP do Get Bananas funcionar.

NSUbiquitousKeyValueStore. Armazenamento chave-valor entre dispositivos. Os limites públicos atuais da Apple são 1MB total por app, 1MB por valor, 1024 chaves, 128 caracteres UTF-16 por chave, com taxas de escrita limitadas.4 A resolução de conflitos é embutida: o sistema serializa as escritas e notifica todos os dispositivos na mudança. Apropriado para estado pequeno e de baixa frequência (configurações, última aba selecionada, um contador inteiro); inapropriado para dados de alta taxa de escrita ou cargas de trabalho onde o limite de taxa se torna o gargalo. O Return o usa para estado do timer entre dispositivos (o usuário inicia um timer no iPhone e o vê no Apple Watch); coberto em Cinco plataformas Apple, três arquivos compartilhados.

O quarto substrato, CloudKit, é a escolha que parece óbvia e que os apps do cluster rejeitaram explicitamente para integração MCP entre processos. O CloudKit oferece sincronização forte entre dispositivos com registros conscientes de conflitos, e a Apple disponibiliza CloudKit JS e CloudKit Web Services para ambientes não-Apple conversarem com bancos CloudKit públicos e privados. O tradeoff honesto é o custo de integração, não impossibilidade: um servidor MCP em Node.js alcançando um banco CloudKit privado precisa configurar autenticação do CloudKit Web Services, definições de schema e assinatura de requisições, o que é trabalho de engenharia material comparado a “abra um arquivo JSON”. O Get Bananas escolheu iCloud Drive mais um arquivo JSON porque o servidor MCP é um processo Node.js que precisa ler e escrever os mesmos dados que o app iOS vê, e I/O de arquivo comum é o caminho de menor resistência.1

A decisão: qual substrato sustenta a verdade

A questão não é qual substrato usar. A questão é qual substrato é a fonte da verdade para qual domínio. Os outros substratos ou fazem cache, espelham, ou ficam fora do caminho.

A matriz de decisão que sobrevive em produção para os apps do cluster:

Formato do domínio Fonte da verdade Por quê
Configurações, preferências, flags simples NSUbiquitousKeyValueStore Sincronização entre dispositivos é automática; colisões são serializadas; payload pequeno cabe
Estado transitório por dispositivo UserDefaults (sem sync) Local do dispositivo; não deve sobreviver a uma instalação nova em outro dispositivo
Coleção consultável em processo SwiftData @Query rápido, observação do SwiftUI, schema tipado; apenas em processo
Coleção em processo que precisa alcançar processos externos Arquivo JSON no iCloud Drive (export para disco) Tanto o leitor SwiftData no iOS quanto o servidor MCP externo podem ler o arquivo
Conteúdo grande por usuário (fotos, áudio, documentos) iCloud Drive (por arquivo) O iCloud do usuário é o armazenamento natural; o CloudKit pode ser camada por cima para sincronização mais rica
Estado de sessão entre dispositivos (timer rodando no iPhone, visível no Watch) NSUbiquitousKeyValueStore Cabe no envelope de tamanho; precisa da semântica de push entre dispositivos

A decisão molda a política de resolução de conflitos. Para a ponte app-local-mais-MCP-externo, o SwiftData não tem resolução de conflitos inerente entre processos; se dois chamadores escrevem na mesma linha, o último try context.save() vence. SwiftData apoiado pelo CloudKit e usando histórico persistente pode carregar semântica mais rica entre dispositivos, mas essa superfície é do lado iOS e não ajuda o caso MCP externo em Node.js. O iCloud Drive expõe conflitos como entradas NSFileVersion; o app precisa percorrê-las e escolher um vencedor. O NSUbiquitousKeyValueStore tem resolução de conflitos embutida no nível do valor.

A arquitetura do Get Bananas

O layout real do Get Bananas:

                     ┌────────────────────────────────────┐
                     │       Modelo mental do usuário      │
                     │       "minha lista de compras"      │
                     └─────────────────┬──────────────────┘
                                       │
                          ┌────────────┴───────────┐
                          │                        │
                  ┌───────▼────────┐      ┌───────▼─────────┐
                  │   App iOS      │      │  Servidor MCP   │
                  │  (Get Bananas) │      │  (Node.js)       │
                  └───────┬────────┘      └───────┬─────────┘
                          │                        │
              ┌───────────┴────────┐               │
              │                    │               │
       ┌──────▼──────┐    ┌────────▼──────────┐   │
       │  SwiftData   │    │  iCloud Drive     │◀──┘
       │ (em processo)│◀──▶│  shopping_list.   │
       │              │    │       json        │
       └──────────────┘    └───────────────────┘

  Leituras/escritas dentro do app fluem pelo SwiftData.
  Leituras/escritas entre processos fluem pelo arquivo JSON.
  Uma camada de sincronização iCloud (iCloudBackupManager) reconcilia as duas.

A arquitetura tem três regras.

SwiftData é a fonte da verdade para consultas em processo. O app iOS lê do SwiftData para cada renderização da UI, cada lista apoiada por @Query, cada busca. As escritas passam primeiro pelo SwiftData; o model context salva; o SwiftUI re-renderiza.

O arquivo JSON é a fonte da verdade para o estado entre processos. Sempre que o app iOS salva no SwiftData, um gerenciador de backup do iCloud exporta o estado atual para um arquivo JSON no container iCloud Drive do usuário. Sempre que o servidor MCP escreve, ele escreve no mesmo arquivo JSON. O arquivo é a ponte.

Uma passagem de sincronização roda no lançamento do app iOS e após cada escrita entre processos. A lógica de sincronização hoje em produção (SyncManager.applyExport) trata o backup JSON como autoritativo a cada passagem: ele lê o arquivo JSON, casa cada linha com o SwiftData por UUID, sobrescreve linhas existentes com os valores do backup, adiciona linhas que o backup tem mas o SwiftData não, e deleta linhas que o SwiftData tem mas o backup não tem (com uma proteção contra corrupção caso um arquivo de backup vazio apague o banco local). A política é o backup vence no momento da sincronização, não “o último escritor por linha vence” por timestamp. Combinada com o app iOS re-exportando após cada save, a convergência em estado estacionário é rápida na prática: qualquer que seja o processo que escreveu mais recentemente produziu o JSON que a próxima sincronização lê.

A arquitetura troca complexidade por alcance entre processos. Um app SwiftData puro não precisa de nada disso; um app sem servidor MCP não precisa da ponte JSON; um app sem sincronização entre dispositivos não precisa do reconciliador. O Get Bananas precisa dos três porque todas as três classes de chamadores (humano via iOS, Apple Intelligence via App Intents no iOS, agente via MCP a partir da máquina de um desenvolvedor Mac) tocam na mesma lista de compras.

O caminho de upgrade: o último escritor por linha vence

A política em produção de “o backup vence no momento da sincronização” é barata e funciona para o caso de usuário único, com um único escritor ativo de cada vez. Ela tem dificuldade quando tanto o app iOS quanto o servidor MCP escrevem no arquivo JSON em sucessão próxima: qualquer que seja o processo que escreveu mais recentemente sobrescreve as mudanças do outro, inclusive para linhas que de fato não tinham conflito. A mitigação atual é o app iOS re-exportar após cada save do SwiftData, o que mantém o arquivo JSON aproximadamente alinhado com o estado mais recente dentro do app. O estado estacionário está bem; o caso genuinamente concorrente pode perder trabalho.

O upgrade mais barato é “o último escritor por linha vence” usando uma coluna lastModified: Date?. O modelo ShoppingItem já tem o campo por segurança de migração (coberto em O custo real do SwiftData é disciplina de schema), mas o export JSON e o servidor MCP atualmente não serializam nem honram esse campo. Passar lastModified pelo export e pelo applyExport mudaria a política de merge de “o backup vence” para “a linha mais nova vence”:

  • Ambos os lados têm um valor, um é mais recente. O mais recente vence. A linha do outro lado é atualizada.
  • Ambos os lados têm um valor, eles empatam. Desempate por chave primária, ou por superfície (o app iOS vence empates para favorecer a interação mais recente do usuário dentro do app).
  • Um lado tem um valor, o outro não. O lado com o valor vence.
  • Nenhum lado tem um valor. Ambas as linhas são dados da era pré-lastModified. O reconciliador carimba Date() para a próxima vez.

A política é barata, fácil de raciocinar e errada em aproximadamente 1% dos casos (edições concorrentes em campos diferentes da mesma linha). Para uma lista de compras, esse 1% não importa; para um editor de documentos, importa absolutamente. Apps que precisam de garantias mais fortes adicionam camadas de merge no nível do campo, CRDTs ou transformações operacionais sobre essa base; o Get Bananas ainda não precisou dessa complexidade, motivo pelo qual o LWW por linha está no roadmap e o merge mais rico, não.

As três classes de chamadores e o substrato

Mapeando as classes de chamadores de Três superfícies para as decisões de substrato:

A superfície humana escreve via SwiftData. Um usuário tocando uma checkbox no app iOS dispara, pela camada SwiftUI, uma função de domínio que muta a linha SwiftData, carimba lastModified = Date() no modelo e salva o model context. O export iCloud escreve o estado atual no arquivo JSON. O servidor MCP pega o novo estado na próxima leitura.

A superfície Apple Intelligence escreve via SwiftData. Um AppIntent invocado pela Siri roda no processo do app iOS e alcança a mesma função de domínio que a superfície humana usa. O estado SwiftData muta, o lastModified do modelo é atualizado, e o export JSON captura o novo estado.

A superfície do agente escreve via arquivo JSON. Uma chamada de ferramenta MCP a partir de uma sessão do Claude Code em um Mac muta o arquivo JSON diretamente (com lock de arquivo para lidar com escritas concorrentes do app iOS). Da próxima vez que o app iOS for lançado ou sincronizar, SyncManager.applyExport lê o arquivo, percorre os itens por UUID, atualiza linhas que existem em ambos os lados com os valores do backup, adiciona linhas que o backup tem e deleta linhas que o backup omite (com a proteção de backup vazio). A política em produção é o backup vence no momento da sincronização; o caminho de upgrade adiciona lastModified ao JSON para que a política possa mudar para a linha mais nova vence.

A assimetria é real e intencional. As superfícies humana e Apple Intelligence ambas rodam dentro do processo do app iOS e usam o SwiftData nativamente. A superfície do agente roda fora do processo do app iOS e usa o arquivo JSON porque é o substrato que ela consegue alcançar. O reconciliador é o que mantém as duas metades juntas.

Quando esse padrão é a resposta errada

Alguns casos em que o padrão da ponte JSON está errado.

Dados com alta taxa de escrita. Um editor de documentos ao vivo com muitas edições por segundo não consegue pagar o custo de serializar a coleção inteira para um arquivo JSON a cada escrita. A resposta certa são transformações operacionais ou CRDTs contra um backend real.

Requisitos de consistência forte. Um livro-razão de transações financeiras não tolera “o último escritor vence” no arquivo JSON. A resposta certa é o CloudKit (ou um banco de dados do lado do servidor) com semântica de transação explícita.

Colaboração multiusuário onde os usuários veem as edições uns dos outros em tempo real. A sincronização do iCloud Drive é eventual, não em tempo real. O usuário fechando o app em um dispositivo e abrindo em outro vê o estado que sincronizou; o usuário vendo o cursor de outro usuário se movendo por um documento, não. A resposta certa é um framework de colaboração em tempo real (yjs, automerge ou uma camada WebSocket customizada).

Casos em que o agente e o usuário são identidades diferentes. O padrão do Get Bananas assume que o agente (o chamador MCP) e o usuário humano (o usuário do app iOS) são a mesma pessoa, apenas operando entre processos. Se o agente está agindo em nome de uma identidade diferente (uma lista compartilhada, um admin, um bot automatizado), o arquivo JSON no iCloud Drive do usuário é o substrato errado; persistência multiusuário com auth explícita é necessária.

O padrão se encaixa no caso de usuário único, eventualmente consistente, entre processos. A maioria dos apps com integrações MCP é exatamente esse caso; alguns não são.

O que eu construiria diferente

Três padrões que os apps do cluster ou já lançaram ou gostariam de ter lançado.

Tornar a serialização JSON explícita, não implícita. A primeira versão do Get Bananas exportava cada escrita SwiftData para JSON em um hook de save. A segunda versão tornou o export um passo explícito que o app chama quando o estado se estabilizou. A mudança reduziu escritas redundantes e deixou claro quando o estado entre processos havia sido publicado. Um hook implícito de save em cada mutação produz I/O demais para qualquer coleção não trivial.

Versionar o schema do arquivo JSON. O arquivo JSON tem seu próprio schema, independente do VersionedSchema do SwiftData. Quando o schema do SwiftData muda (digamos, adicionando um campo), a serialização JSON tem que acompanhar. A correção barata é colocar um campo schemaVersion: Int no topo do JSON; o reconciliador o lê e aplica a interpretação certa. Sem versionamento, um app iOS v2 lendo um arquivo JSON v1 escrito por um servidor MCP antigo bate em corrupção silenciosa de dados.

Fazer lock no arquivo JSON, não assuma coordenação. Tanto o app iOS quanto o servidor MCP escrevem no arquivo JSON. Sem um NSFileCoordinator (em processo, lado iOS) e um lock de arquivo (fora do processo, na máquina do desenvolvedor), escritas concorrentes podem produzir um arquivo corrompido. O servidor MCP do Get Bananas usa um lock de arquivo no estilo flock no JSON; o app iOS usa NSFileCoordinator para suas escritas; o arquivo raramente é disputado na prática, mas o cinto de segurança é barato.

O que o padrão significa para apps lançando no iOS 26+

Três conclusões.

  1. Escolha uma fonte da verdade por domínio. Os outros substratos fazem cache, espelham ou ficam fora do caminho. SwiftData para consultas em processo, JSON no iCloud Drive para pontes entre processos, NSUbiquitousKeyValueStore para estado pequeno entre dispositivos. A resolução de conflitos é uma consequência a jusante da escolha.

  2. lastModified mais “o último escritor vence” é o caso base barato. A maioria dos apps não precisa de garantias mais fortes. O 1% dos casos que precisam de merge no nível do campo ou CRDTs é caro de adicionar; não pague o custo até que o formato dos dados exija.

  3. O reconciliador é a peça que sustenta a carga. Quando o SwiftData e o arquivo JSON discordam, o reconciliador decide. O reconciliador roda no lançamento do app, após escritas entre processos e após eventos de sincronização do iCloud. As regras são simples; a disciplina é de fato rodá-lo.

O cluster Apple Ecosystem completo: App Intents tipados para a superfície Apple Intelligence; servidores MCP para a superfície do agente; a questão de roteamento entre eles; Foundation Models para recursos LLM on-device dentro do app; a distinção LLM entre runtime e ferramental; a síntese de três superfícies de um app iOS; Live Activities para a máquina de estados da Lock Screen do iOS; o contrato de runtime do watchOS no Apple Watch; internals do SwiftUI para o substrato da superfície humana; modelo mental espacial do RealityKit para cenas visionOS; disciplina de schema do SwiftData para persistência; padrões Liquid Glass para a camada visual; shipping multiplataforma para alcance entre dispositivos. O hub está em Série Apple Ecosystem. Para contexto mais amplo de iOS com agentes de IA, veja o guia de Desenvolvimento de Agentes iOS.

FAQ

Por que não usar o CloudKit para sincronização entre processos?

O CloudKit oferece sincronização forte entre dispositivos com registros conscientes de conflitos, e o CloudKit JS / CloudKit Web Services da Apple permitem que stacks não-Apple alcancem um banco CloudKit privado. A restrição é o custo de integração: um servidor MCP em Node.js usando CloudKit precisa lidar com a auth do CloudKit (chaves servidor a servidor ou tokens em nível de usuário), declarações de schema e requisições assinadas. iCloud Drive mais um arquivo JSON é I/O de arquivo comum, que é o tradutor universal. O CloudKit é a escolha certa quando o time está disposto a pagar o custo de integração e quer a sincronização mais forte e a semântica de conflitos do CloudKit; a ponte JSON é a escolha certa quando “abra um arquivo” basta para o formato dos dados.

Como você lida com conflitos quando dois chamadores escrevem ao mesmo tempo?

A política em produção do Get Bananas é “o backup vence no momento da sincronização”: SyncManager.applyExport percorre itens por UUID e sobrescreve linhas locais a partir do backup, com uma proteção contra um backup vazio apagar bons dados locais. O caminho de upgrade é “o último escritor por linha vence” baseado em lastModified, que o modelo já carrega mas que ainda não é serializado pela ponte JSON. Adicioná-lo resolveria os ~99% de conflitos onde um lado é genuinamente mais novo; o 1% restante (edições concorrentes em campos diferentes da mesma linha) é raro o suficiente para os apps até agora pularem merge no nível do campo ou CRDTs. Apps com requisitos de consistência mais fortes adicionam camadas de merge mais ricas por cima.

Onde o SwiftData se encaixa se o iCloud Drive é a fonte da verdade para o estado entre processos?

O SwiftData é a fonte da verdade para consultas em processo. O app iOS lê o SwiftData para cada renderização da UI, cada @Query, cada busca. O SwiftData é rápido, com schema tipado e integrado ao sistema de observação do SwiftUI. Quando o app iOS escreve, a mudança vai primeiro para o SwiftData, depois é exportada para o arquivo JSON. O arquivo JSON é a fonte da verdade para leituras entre processos (a visão do servidor MCP); o SwiftData é a fonte da verdade para leituras em processo (a visão da UI iOS). Eles ficam alinhados pelo reconciliador.

E o NSUbiquitousKeyValueStore para a própria lista de compras?

O NSUbiquitousKeyValueStore tem limite de 1MB total por app e 1MB por valor com escritas limitadas, e serializa em um dicionário de 1024 chaves. Uma lista de compras com centenas de itens mais histórico pode caber em contagem de bytes, mas escrever mudanças por item através do throttle é o formato errado; atualizações em massa de coleção competem pelo orçamento de taxa contra tudo o mais que o app armazena. O substrato certo para coleções é ou SwiftData (em processo) ou iCloud Drive (entre processos). Reserve o NSUbiquitousKeyValueStore para estado chave-valor pequeno e de baixa frequência: configurações, a última aba selecionada, um contador, uma sobreposição de feature flag.

Como sei qual substrato escolher para um novo domínio no meu app?

Três perguntas em ordem. Primeira: alguma coisa fora do processo do app iOS precisa ler ou escrever neste domínio? Se sim, você precisa de iCloud Drive (baseado em arquivos, I/O de arquivo comum) ou CloudKit (via frameworks da Apple ou CloudKit Web Services a partir de stacks não-Apple) ou um servidor que você controla. Se não, o SwiftData é o padrão. Segunda: isso precisa sincronizar entre os dispositivos do usuário? Se sim, o substrato precisa suportar isso (o iCloud Drive suporta, o SwiftData não, a menos que pareado com sincronização iCloud). Terceira: qual o tamanho do payload e com que frequência ele muda? Pequeno + baixa frequência mora no NSUbiquitousKeyValueStore; tudo o mais precisa de uma camada de persistência real.

Referências


  1. Análise do autor em Dois ecossistemas de agentes, uma lista de compras, 29 de abril de 2026, e a camada de sincronização JSON no iCloud Drive do projeto Get Bananas em Banana List/iCloudBackupManager.swift. A arquitetura combina SwiftData com um arquivo JSON no container iCloud Drive do usuário que um servidor MCP externo lê e escreve. 

  2. Apple Developer, “SwiftData” e “Adding and editing persistent data in your app”. A macro @Model, as restrições @Attribute e a relação com o NSManagedObjectModel do Core Data. Análise do autor em O custo real do SwiftData é disciplina de schema cobre VersionedSchema e MigrationPlan para evolução segura de schema. 

  3. Apple Developer, “Synchronizing documents in the iCloud environment”. Sincronização entre dispositivos baseada em arquivos, resolução de conflitos via NSFileVersion e a API NSFileCoordinator para escritas seguras em processo contra arquivos compartilhados. 

  4. Apple Developer, “NSUbiquitousKeyValueStore”. Armazenamento chave-valor entre dispositivos. Limites atuais da Apple: 1MB total por app, 1MB por valor, 1024 chaves, 128 caracteres UTF-16 por chave, taxa de escrita limitada. Análise do autor em Cinco plataformas Apple, três arquivos compartilhados cobre o padrão de timer entre dispositivos que o Return entrega usando essa API. 

Artigos relacionados

Servidor MCP ao lado de um app iOS: dois ecossistemas de agentes, uma lista

Get Bananas roda em iOS, macOS, watchOS e visionOS. Também vive dentro do Claude Desktop como um servidor MCP. A ponte: …

17 min de leitura

App Intents vs MCP: A Questão do Roteamento

Dois protocolos, um app. App Intents expõem seu app à Apple Intelligence. MCP expõe o mesmo domínio para Claude, ChatGPT…

14 min de leitura

Seu agente tem um intermediário que você não verificou

Pesquisadores testaram 28 routers de API LLM. 17 tocaram em credenciais canário da AWS. Um drenou ETH de uma chave priva…

13 min de leitura