Sua chave de API está no F12 — e nem toda chave ali é um problema

Abrir o F12, procurar por key e encontrar alguma coisa é rotina. O que decide se você tem um problema não é a chave estar visível — é qual chave está.

Duas famílias de chave

Publicável. Existe para viajar no navegador. A anon key do Supabase, a pk_live_ do Stripe, a chave do Google Maps, a chave anônima do Firebase. Elas identificam o projeto, não autorizam operação. Quem protege não é o segredo — é outra coisa: RLS no Supabase, restrição por domínio no Maps, regras de segurança no Firebase.

Secreta. Autoriza operação em nome da sua conta. sk_live_ do Stripe, service_role do Supabase, chave da OpenAI, token do Resend ou do SendGrid, chave da AWS. No navegador, elas dão a quem encontrar exatamente o poder que dariam ao seu servidor.

O prefixo costuma dizer qual é qual: pk_ de publishable, sk_ de secret. service_role diz o que faz. sk-proj- da OpenAI também.

Encontrar as suas

# baixa os scripts da página e procura os padrões conhecidos
curl -s https://seusite.com.br | grep -oE 'src="[^"]+\.js"' | cut -d'"' -f2 | \
while read -r js; do
  curl -s "https://seusite.com.br/${js#/}" | grep -oE \
    'sk_live_[a-zA-Z0-9]+|sk-proj-[a-zA-Z0-9_-]+|AKIA[0-9A-Z]{16}|re_[a-zA-Z0-9_]+|SG\.[a-zA-Z0-9_.-]+'
done

No navegador dá para fazer o mesmo à mão: F12 → Sources → Ctrl+Shift+F → procure por sk_, secret, service_role, AKIA.

E não esqueça do sourcemap. Se o build publicou os .js.map, o seu código original está na web, com nomes de variável e comentários:

curl -s -o /dev/null -w "%{http_code}\n" https://seusite.com.br/_next/static/chunks/main.js.map

200 aí significa que ler o seu front ficou tão fácil quanto ler o repositório.

Como a chave secreta chega no navegador

Quase sempre por um destes três caminhos:

O prefixo público. NEXT_PUBLIC_OPENAI_API_KEY no Next, VITE_STRIPE_SECRET no Vite. O prefixo é uma instrução de build: "embuta isto no bundle". Quem não sabe disso usa o prefixo para "resolver" o valor que chegava undefined no componente — e o undefined some, junto com a proteção.

A chamada direta do navegador. O app chama a API da OpenAI direto do componente React. Para isso a chave precisa estar no navegador. Não há configuração que conserte: é a arquitetura da chamada.

O assistente escolheu o caminho curto. Pedir "integra a OpenAI nesse componente" a um gerador de código costuma produzir exatamente a chamada direta, porque é a que funciona em uma tentativa. Um proxy no servidor exige criar uma rota, e ele não faz isso sem você pedir.

O conserto: a chamada muda de lugar

Não existe forma segura de manter uma chave secreta no navegador. Ela precisa sair de lá — e a chamada vai junto.

// app/api/resumo/route.ts — roda no servidor
export async function POST(req: Request) {
  const { texto } = await req.json()

  const r = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      // sem NEXT_PUBLIC_: fica no servidor
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
    },
    body: JSON.stringify({
      model: "gpt-4o-mini",
      messages: [{ role: "user", content: texto }],
    }),
  })

  return Response.json(await r.json())
}

O componente passa a chamar /api/resumo. A chave nunca sai da máquina.

Duas coisas que essa rota ainda precisa, e que a versão direta não tinha como ter: exigir que o usuário esteja logado, e limitar quantas chamadas cada um faz. Sem isso você trocou "qualquer um usa a minha chave" por "qualquer um usa a minha rota" — a fatura chega igual no fim do mês.

Se a chave secreta já esteve no ar

Rotacione primeiro. No painel do provedor, gere uma nova e invalide a antiga. Só depois mexa no código — enquanto a chave antiga for válida, o conserto no repositório não protege nada, porque a que vazou continua funcionando.

Depois disso: procure no histórico do Git (ela costuma ter sido commitada antes de ir para o .env), desligue o sourcemap em produção, e confira a fatura e os logs de uso do serviço no período em que a chave esteve exposta.

O resumo

  • Chave no F12 não é diagnóstico. pk_ e anon podem ficar; sk_, service_role e AKIA não.
  • NEXT_PUBLIC_ e VITE_ significam "publique isto". Não são um jeito de fazer a variável funcionar.
  • Chave secreta no navegador não tem conserto por configuração — a chamada precisa mudar de lugar.
  • E a rota nova precisa de autenticação e limite, senão o problema só trocou de porta.
← Todos os posts