> Edição pública gerada de backend/sdk/README.md. Revisão oficina-2026-10-23.5; SDK 0.1.0-test.3. Links internos não distribuídos são identificados. # SDK de troféus — versão de testes Decisão de Murillo em **2026-10-17**: o backend passa a centralizar conquistas de todos os jogos. Novas integrações enviam apenas conquistas, sem score ou diário de partida. Cada jogo dispõe de `bronze-1`, `bronze-2`, `bronze-3`, `prata-1`, `prata-2`, `ouro-1` e `diamante-1`. São sete conquistas distintas por jogo, cada uma guardada uma vez por jogador; não são sete premiações globais para dividir entre jogadores. Os critérios de conquista são responsabilidade do jogo. ## Fluxo implementado 1. Murillo cadastra o jogo e suas origens com a CLI administrativa e entrega a chave pública ao criador. O jogo instala o módulo JavaScript. 2. Ao ocorrer a conquista, `ganhar(slot)` cria um token aleatório de 256 bits e um UUID v4. Envia somente UUID, slot e SHA-256 do token à API com a chave. Nenhum jogador, score, cookie ou evento intermediário sai do jogo. 3. A API valida jogo, origem, chave, slot e reenvios, guarda o hash no PostgreSQL e informa o prazo e a página central. O SDK monta o link com o token no **fragmento**: `/trofeus/#token=...`. O token não vai na query nem no GET. 4. O jogador abre o link. A página central retira o fragmento do endereço e mantém uma fila em `sessionStorage`, inclusive durante reload. Cada resposta confirma apenas o token enviado; links que chegam durante o POST permanecem na fila. O GET não concede troféu. A confirmação envia o token em POST à API, com credentials include e CORS restrito ao hub. 5. O backend reconhece a identidade pelo cookie HttpOnly existente, confere o hash e grava a conquista em transação. Link consumido por outro jogador é recusado. A coleção reúne os jogos, sem ranking por score. O servidor não lê storage/cache do navegador. `localStorage` pertence à origem que o criou; cookies são enviados pelo navegador conforme domínio e atributos. Referências técnicas consultadas em **2026-10-17**: [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies) e [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). Desde a publicação de **2026-10-21**, a central pertence a **HUB_ORIGIN**, no hub. O endereço antigo PUBLIC_ORIGIN/trofeus/ permanece como redirecionamento 302 sem HTML ou fragmento próprio, preservando links e SDKs existentes. O campo claim_url da API v1 permanece inalterado por compatibilidade. O cookie continua host-only na API; o hub usa credentials include. A fila antiga da origem da API não é copiada: reabra o link no jogo ou use renovarLink se venceu. Troféus já persistidos continuam na coleção. ## Instalar e usar no jogo Pacote ESM sem dependências: `@gamers-pelo-brasil/trofeus`, versão `0.1.0-test.3`. **Não publicado no npm**. Instalação pela pasta do código-fonte (com package.json), para quem tem acesso: ```sh npm install /caminho/para/backend/sdk ``` O ZIP público usa arquivos diretos e não requer npm. Para HTML sem bundler, comece pelo [exemplo completo e copiável](exemplo.html). Copie **exemplo.html e trofeus.js** para a mesma pasta, sirva por HTTPS ou localhost e preencha a API/chave entregues pelo operador. Não abra por `file://`. Em produção, use `api: 'https://api.gamerspelobrasil.tech'`, sem barra final nem `/api/v1`. O módulo publicado fica em `https://gamerspelobrasil.tech/criar/sdk/arquivos/trofeus.js`; o caminho antigo `https://api.gamerspelobrasil.tech/sdk/trofeus.js` redireciona para ele. Prefira copiar o módulo do ZIP para o próprio jogo, como no exemplo. A [central de resgate](https://gamerspelobrasil.tech/trofeus/) fica no hub. O link retornado pelo SDK passa pela API e redireciona para a central; use `receipt.claim_url` inteiro, sem remover nem enviar o fragmento ao servidor. Use somente API, chave e origens fornecidas pelo operador. Login Google depende da configuração do ambiente; o SDK não configura nem solicita credenciais de conta. 1. Peça ao operador a origem da API, a chave **pública**, as origens cadastradas do jogo e a correspondência entre desafios e os sete slots. O criador não precisa acessar banco, CLI ou credenciais administrativas. 2. Prepare o SDK antes de iniciar a partida. No exemplo, “Testar conexão” só diagnostica; “Emitir conquista real” representa um callback real e grava uma prova. Ligue cada callback a `registrarConquista(slot)`: ```js // Dentro do script do exemplo, depois de preparar o SDK: aoVencerPrimeiraFase(() => registrarConquista('bronze-1')); aoCompletarJogo(() => registrarConquista('ouro-1')); ``` Substitua `aoVencerPrimeiraFase`/`aoCompletarJogo` pelos eventos existentes no seu jogo; não crie regras de vitória novas. Cada slot tem sua própria linha, status, link **Receber na minha coleção**, **Tentar novamente** e **Gerar novo link**. 3. Falhou a rede? Use **Tentar novamente**. A conquista fica registrada antes do envio, mesmo se o callback ocorrer uma única vez. Após reload, prepare o SDK com a mesma API/chave: as linhas são restauradas sem precisar vencer de novo. 4. A central disse `CLAIM_USED` ou link vencido? Dispense o link antigo na central se for reutilizar a mesma aba. Volte à linha da conquista e use **Gerar novo link**. Esse ato explícito substitui a prova local daquele slot. Se a resposta da renovação se perder, use **Tentar novamente**, que reutiliza a prova recém-criada. Não clique de novo em renovar para repetir transporte. O novo link permite resgatar na identidade atual; não recupera a coleção antiga. 5. Teste dois slots, interrompa uma resposta e recarregue. Cada linha deve manter seu próprio botão; o retry não depende de reiniciar a partida. Abra receber, entre com Google se a central solicitar e confirme o resgate. Em ambientes com Google desativado, a central oferece uma identidade de teste. Para integração assistida por IA: forneça o HTML completo e o mapeamento de slots; peça para ligar apenas os callbacks reais a `registrarConquista`, preservando as linhas/retries, API/chave pública fornecidas e `credentials: 'omit'`. Cadastro, banco e configuração do proxy são do operador. Não peça login no jogo ou mudanças no sandbox/CORS do conteúdo hospedado. ### Chave, origem e configuração no jogo A chave `gpb_…` é pública e pode ficar no JavaScript. Use a chave do próprio jogo; ela identifica o cadastro, não autentica o jogador e não comprova a vitória. Não peça a chave ao jogador. Na versão integrada, preencha `api` e `chave` no código e prepare o SDK ao carregar o jogo, antes de habilitar a partida. O formulário do exemplo serve para o autor testar a configuração. ```js import {criarTrofeus} from './trofeus.js'; const sdk = criarTrofeus({ api: 'https://api.gamerspelobrasil.tech', chave: 'CHAVE_PUBLICA_ENTREGUE_PELO_OPERADOR' }); // Mantenha as linhas, os botões e a recuperação do exemplo completo. // Cada callback real chama registrarConquista(slot), que usa esta instância. ``` A origem é **protocolo + domínio + porta**, sem caminho nem barra final. `https://meu-jogo.pages.dev` é diferente de um subdomínio de preview, `http://localhost:8091` ou `http://127.0.0.1:8091`. Só funcionam as origens cadastradas para aquela chave. Servir por localhost habilita Web Crypto, mas **não libera a emissão real**: para testar sem gravar, use `testar(slot)` abaixo; para emitir conquistas locais de verdade, solicite ao operador o cadastro exato. Publicar o exemplo na origem autorizada também permite testar a chave real. Não use `mode: 'no-cors'`, não defina `Origin` manualmente e preserve `credentials: 'omit'`; o SDK cuida dos cabeçalhos e do preflight. Se o jogo tiver Content-Security-Policy, permita a API em `connect-src` e o local do módulo em `script-src`. Isso vale para o frontend externo administrado pelo autor; não altera o isolamento dos jogos hospedados pela plataforma. | Sintoma | Conferência e ação | | --- | --- | | `Chave do jogo inválida` ao preparar | Substitua o placeholder pela chave completa `gpb_` + 64 hexadecimais. | | `ORIGIN_NOT_ALLOWED` / erro de CORS ou `Failed to fetch` | Veja no Network o OPTIONS/POST: confira origem cadastrada, HTTPS, CSP e conectividade. O navegador pode ocultar a resposta quando CORS bloqueia. | | `INVALID_GAME_KEY` (403) | Confira se a chave está ativa e pertence ao jogo e à origem usados. | | `RATE_LIMITED` (429) | Aguarde um minuto e tente novamente com a mesma prova. | | `CLAIM_EXPIRED` / `CLAIM_USED` | Gere novo link por ação explícita no jogo; retry sozinho preserva o link antigo. | ### Testar em localhost sem gravar conquistas A partir do SDK `0.1.0-test.3`, use **a mesma chave pública do jogo** para verificar conexão, chave ativa e slot, mesmo sem cadastrar localhost: ```js const resultado = await sdk.testar('bronze-1'); // {mode: 'test', game_id: 'meu-jogo', trophy_id: 'bronze-1', // origin_registered: false, persisted: false} ``` Copie `exemplo.html` e `trofeus.js` do ZIP para a mesma pasta e sirva: ```sh python3 -m http.server 5173 --bind 127.0.0.1 ``` Abra `http://localhost:5173/exemplo.html`, preencha a API de produção e a chave, clique **Preparar SDK** e **Testar conexão (sem gravar)**. Não é necessário banco, CLI, conta ou uma chave de teste separada. O operador já deve ter cadastrado a chave do jogo; chave inventada, girada ou desativada retorna erro. O diagnóstico faz `POST /api/v1/trophies/check` com `X-Game-Key` e corpo `{"trophy_id":"bronze-1"}`. O servidor executa apenas leitura: não cria claim, token, sessão, troféu nem debita cotas SQL de emissão. O SDK não modifica as intenções em storage e não devolve link de resgate. `origin_registered: false` é esperado em localhost sem cadastro; `persisted` é sempre `false`. As defesas gerais da API continuam limitando excesso de requisições. Aceita `http://localhost:`, `http://127.0.0.1:` e `http://[::1]:` (ou HTTPS), com porta válida. LAN, `file://`/Origin `null` e previews não cadastrados continuam recusados. A exceção local vale somente para `/check`; `ganhar(slot)` e **Emitir conquista real** continuam gravando provas e exigindo origem cadastrada. Não use emissão como teste de conexão. Se `/check` responder 404, a API ainda não recebeu a versão com diagnóstico; se `testar` não existir, atualize o módulo. Não substitua o diagnóstico por emissão. ### Critérios de integração concluída Mapeie os desafios existentes para os slots que o jogo usar; não é obrigatório atribuir os sete de uma vez. Um link pronto significa que a prova foi emitida. O troféu só entra na coleção após o jogador confirmar o resgate na central. Valide um callback natural, dois slots independentes, falha de rede/retry, reload, renovação explícita e resgate/coleção com a conta do jogador. Instalar o módulo e obter HTTP 200 não substitui esse fluxo completo. ### Roteiro de validação com o criador O retorno do criador é parte do piloto: confirme o mapeamento de eventos existentes para slots antes do aceite. Os critérios pertencem ao jogo; não há exigência de dificuldade uniforme entre jogos. Cada slot é independente por jogo, mesmo quando dois jogos usam o mesmo identificador. Use estas três etapas e declare qual foi concluída: | Etapa | Como executar | O que comprova | | --- | --- | --- | | Diagnóstico sem gravação | `testar(slot)` com a chave pública | Conexão, chave ativa, slot e se a origem está cadastrada. Não cria prova ou link. | | Simulação no jogo | Acionar os eventos existentes com envio interceptado no ambiente de teste do autor | Callbacks, apresentação das conquistas e tratamento de falhas simuladas. Não comprova emissão ou coleção na central. | | Aceite real | Jogar na origem cadastrada, emitir pelo SDK, abrir o link e confirmar na central com a conta | Integração do jogo com emissão, login quando solicitado, resgate e coleção. A persistência deve ser conferida com o operador. | A simulação é útil e não impede o aceite real coordenado com o operador. Uma falha de rede provocada que apresenta **Tentar novamente** é um resultado esperado; para validar recuperação, é preciso também concluir o reenvio. Não use emissão para diagnosticar conexão nem execute emissões em massa na API pública. Faça os cenários de falha técnica em ambiente controlado. Antes de concluir a integração, confira: - **Eventos naturais:** cada slot dispara no momento definido pelo jogo. Teste também os casos que não devem premiar, o reinício da partida e, quando houver, a diferença entre chegar a uma rodada, concluí-la e terminar sem erros. Em celular, exercite os controles de toque usados para conquistar o troféu. - **Recuperação:** use dois slots; interrompa a resposta de uma emissão já aceita pela API, recarregue e prepare a mesma API/chave. Use **Tentar novamente** sem repetir a vitória: a prova deve ser a mesma, sem duplicação, com linhas e links independentes. Ver a mensagem de falha, sozinha, não comprova essa recuperação. - **Renovação:** para link vencido ou usado, gere novo link por ação explícita. Se a resposta da renovação se perder, tente novamente sem renovar outra vez. Nenhuma dessas ações deve apagar o link ou a intenção de outro slot. - **Conta e coleção:** em cada jogo, conquiste naturalmente no celular e siga **Receber na minha coleção** → central → Google, se solicitado → confirmação. Confira que o login preserva a pendência nesta aba e que o troféu permanece após reabrir a coleção e entrar com a mesma conta em outro navegador. O criador registra versão/URL do jogo, slot e evento, data/hora, aparelho, navegador, etapa testada e resultado observado. Não inclua tokens, cookies ou links pessoais. O operador confere a persistência independentemente; o criador não precisa de banco ou credenciais administrativas. Uma partida automática não substitui a conferência em aparelho físico; um ensaio sintético da equipe não substitui o callback natural do jogo. ### Nomes das conquistas no jogo e na coleção Você pode apresentar títulos e descrições próprios no jogo, associados aos slots. Na versão atual, a central exibe a categoria do troféu e o nome cadastrado do jogo; não recebe o título, a descrição ou o ícone personalizado da conquista. Por exemplo, “Escavador” associado a `bronze-1` continua sendo o bronze daquele jogo na coleção. Envie somente os campos do contrato pelo SDK. A personalização da central é uma evolução opcional e não bloqueia o piloto. ### API JavaScript e recuperação `ganhar(slot)` registra a intenção e envia/reenvia a mesma prova; chamadas simultâneas do mesmo slot compartilham a promessa. `pendentes()` lista os slots com intenção local, inclusive quando o envio falhou; não indica que houve resgate. `renovarLink(slot)` cria outro UUID/token **explicitamente**, sem consultar a identidade central. A renovação em paralelo a um envio do mesmo slot falha com `ISSUE_IN_PROGRESS`; espere a operação e use retry se necessário. O SDK não faz chamadas ao inicializar, polling ou login. A primeira conquista faz POST (e preflight quando necessário). Após resposta válida, reutiliza o link; falha de transporte preserva UUID/token. O prazo do servidor é **15 minutos**, sem renovar em reenvio. Não há substituição automática por relógio local: se a prova venceu, a UI oferece renovação explícita. Timeout de rede: 10 segundos; sem retry automático. O cache usa API/chave/slot. Sem storage disponível, a intenção fica apenas na memória da página e não sobrevive a reload. Apagar storage, mudar origem ou chave também impede recuperar essa intenção; localStorage não é conta central. ## Cadastrar, girar ou desativar a chave Para experimentar manualmente, com `DATABASE_URL` de testes já definida, execute na pasta `backend/` (terminal 1): ```sh make build HTTP_ADDR=127.0.0.1:8082 PUBLIC_ORIGIN=http://127.0.0.1:8082 HUB_ORIGIN=http://127.0.0.1:8081 ./bin/api ``` No terminal 2, com a mesma `DATABASE_URL`: ```sh ./bin/trofeus -jogo piloto -nome 'Meu jogo piloto' -origens 'http://localhost:8091' python3 -m http.server 8091 --bind 127.0.0.1 --directory sdk ``` No terminal 3, na pasta `hub/`, inicie a central local (com Go disponível): ```sh HTTP_ADDR=127.0.0.1:8081 API_ORIGIN=http://127.0.0.1:8082 go run . ``` Abra `http://localhost:8091/exemplo.html`, cole a chave pública retornada e clique em “Preparar SDK”, “Emitir conquista real” e “Receber na minha coleção”. O HTML abre a central em outra aba e mantém os links de cada slot no jogo. A página central oferece criar a identidade de teste somente se não houver sessão. As duas origens diferentes reproduzem a separação jogo/central. Use Ctrl+C para encerrar os processos de teste. Não é deploy nem publicação npm. Depois de iniciar a API para aplicar migrations, a partir de `backend/`, use `DATABASE_URL` de um banco **de testes** já configurado, sem imprimi-la: ```sh make build ./bin/trofeus -jogo piloto -nome 'Meu jogo piloto' -origens 'http://localhost:8091' ./bin/trofeus -acao girar-chave -jogo piloto ./bin/trofeus -acao desativar -jogo piloto ``` A saída contém a chave **pública** (`gpb_` + 64 hexadecimais), não a credencial administrativa do banco. Só existe administração por CLI; nenhuma rota HTTP pública cadastra jogos ou cria chaves. O banco guarda somente o hash da chave. Cadastro duplicado falha sem substituir o jogo. Rotação invalida a chave anterior e seus resgates pendentes; desativação bloqueia novas emissões e resgates. Ambas preservam a coleção já conquistada. A CLI está incluída no Dockerfile, sem executar mudanças no build. Cadastre de uma a dez origens exatas, separadas por vírgula, sem caminho/barra final; HTTPS obrigatório fora de loopback. Origem opaca `null` é recusada. Os jogos hospedados atuais abrem diretamente em nova aba, com origem opaca por cabeçalho CSP. Precisam de uma integração própria ainda não entregue; não se remove o isolamento nem se aceita Origin null para instalar o SDK. ## Contrato e fronteiras Contrato canônico: [OpenAPI](openapi.yaml), tag Troféus. Implementação: HTTP (referência interna; consulte o operador), transações (referência interna; consulte o operador) e migration 004 (referência interna; consulte o operador). | Operação | Identidade/autorização | Efeito | | --- | --- | --- | | `POST /api/v1/trophies/claims` | Chave pública e origem do jogo; sem sessão | Registra hash/UUID/slot e responde página e validade | | `GET /trofeus/` na API | Redirecionamento sem corpo | Leva à central do hub; não cria identidade nem grava conquista | | `POST /api/v1/trophies/redeem` | Cookie e Origin central | Resgata uma vez; reenvio do dono retorna a mesma conquista | | `GET /api/v1/players/me/trophies` | Cookie central | Até 1000 conquistas mais recentes, sem acesso entre jogadores | A chave no JavaScript e o cabeçalho Origin são copiáveis. Esta versão valida a autorização do cadastro e a integridade do resgate, **não prova que o desafio foi vencido**. O token é um comprovante ao portador: quem possuir o link pode resgatá-lo primeiro. Não há identidade do jogador vinculada na emissão; esse é o compromisso do fluxo sem login nos jogos. Não publique links pessoais como compartilhamento. Para um futuro modo verificado, o servidor do jogo poderá atestar conquistas com credencial privada; isso não está implementado na versão de browser. Limites por janela fixa de minuto, compartilhados no PostgreSQL entre processos: 4096 bytes por pedido; **10 provas novas por jogo/solicitante**, **60 provas novas por jogo** (teto agregado preservado), **120 pedidos válidos por jogo/solicitante**, incluindo reenvios/conflitos e novas emissões recusadas. Pedidos sem autorização ou malformados são recusados antes dessa admissão; proteção volumétrica desses pedidos cabe à borda. Retry idempotente não debita provas novas, mesmo com o teto agregado cheio; continua limitado pelos 120 pedidos. Falha transacional não deixa claim nem débito de prova nova; a admissão do pedido persiste separadamente. 429 inclui `Retry-After: 60`. Resgate mantém 60 tentativas autenticadas/jogador/minuto. Solicitante é o IP da conexão (IPv4 sem porta; IPv6 agrupado por /64), nunca um ID escolhido pelo browser. Só o hash desse agrupamento entra no contador. NAT compartilha a cota; uma rede distribuída ainda pode atingir o teto agregado. Isso reduz o abuso reproduzido e limita escrita, sem constituir proteção completa contra DDoS. Por padrão, cabeçalhos de proxy são ignorados. Atrás de proxy, o **operador** deve configurar `TRUSTED_PROXY_CIDRS` com CIDRs explícitos dos saltos controlados e validar que a borda remove ou acrescenta o IP real à direita de `X-Forwarded-For`. A API percorre a cadeia da direita para a esquerda até o primeiro IP não confiável; prefixos inventados à esquerda não criam novas cotas. Não use redes amplas que incluam clientes; cabeçalho ausente/inválido usa o IP do proxy e compartilha a cota. A publicação de testes foi conferida em 2026-10-19; configuração e provas operacionais pertencem ao ambiente (referência interna; consulte o operador). Hash, UUID e corpo devem coincidir para reenvio. Claims não consumidos vencem em 15 minutos. Reenvio do dono após consumo continua idempotente mesmo depois desse prazo, enquanto jogo/chave e sessão forem válidos. Não há limpeza automática das provas nesta versão; política e operação ficam no quadro. Nenhuma concessão depende de memória do processo Go. Não recebe URL arbitrária para redirecionar nem busca URLs do jogo. **Identidade central:** com Google habilitado pelo operador, o jogador entra na central e recupera a coleção pela conta. O jogo não recebe essa sessão nem solicita credenciais Google. Com Google desativado, a central oferece identidade de teste por cookie `defende_brasil_session`, HttpOnly, SameSite=Lax, host-only e Secure sob HTTPS. A duração de teste segue SESSION_TTL (padrão 24h, sem renovação). Perder ou expirar o cookie de teste não recupera a coleção pela UI: uma nova sessão cria outra identidade. Storage do jogo não é autenticação. A configuração de conta não é fornecida no ZIP; confira o modo oferecido pela central do ambiente disponibilizado. Não prometa migração de troféus de uma identidade de teste para uma conta. ## Reproduzir os ensaios Com Go no PATH, PostgreSQL descartável e Playwright/Chromium configurados conforme o ambiente (referência interna; consulte o operador), a partir de `backend/`: ```sh make trophies-test ``` O alvo exige `TEST_DATABASE_URL` de cluster dedicado e `PLAYWRIGHT_MODULE`; não aceita silenciosamente pular o navegador. Cada teste cria banco/role próprios e remove-os no final. O navegador usa sandbox ativo e portas loopback; o teste inicia/encerra apenas seus processos. O fixture externo serve o HTML completo e o SDK reais, com registro pela CLI e API em processo separado. Exercita callback único, resposta perdida, vários slots, troca de cookie, renovação explícita e fila central durante respostas atrasadas. Não altera jogos publicados. Testes: SDK (referência interna; consulte o operador), HTTP/banco (referência interna; consulte o operador), processo/CLI (referência interna; consulte o operador) e navegador (referência interna; consulte o operador). Evidências executadas pertencem ao relatório (referência interna; consulte o operador); existência dos testes não é prova de sucesso. ## Distribuição pública da oficina — 2026-10-18 O módulo e o exemplo são distribuídos sob [MIT](LICENSE.txt). A oficina em `/criar/sdk/` do hub prepara guia navegável e ZIP versionado sem exigir acesso ao repositório privado. Este README permanece dono do contrato: o gerador `hub/tools/build_oficina.py` copia módulo/exemplo byte a byte e adapta apenas links na edição pública do guia, identificando referências internas não distribuídas. Hashes de origem e dos arquivos do kit ficam no manifesto. Preparação local não comprova publicação do hub ou dos endpoints de troféus. Provas da distribuição pertencem ao guia do hub (referência interna; consulte o operador). ## Evolução prevista Primeira versão: transporte de conquista, sete slots fixos, cadastro por CLI, resgate e coleção central. Próxima integração: piloto escolhido e adaptado pelos criadores e abertura coordenada. Login Google está implementado separadamente e depende da configuração do ambiente; não faz parte da instalação do SDK. Nomes/descrições/ícones personalizados dos sete slots, painel de administração, paginação além de 1000 itens e atestado privado de um servidor de jogo são extensões, não requisitos já entregues. O legado de partidas/score continua disponível para compatibilidade; nenhuma partida antiga foi apagada ou convertida em troféu automaticamente.