Skip to content

Keep the storefront CMS usable when the platform GitHub token expires - #819

Open
vitorrgg wants to merge 6 commits into
mainfrom
fix/cms-github-proxy
Open

Keep the storefront CMS usable when the platform GitHub token expires#819
vitorrgg wants to merge 6 commits into
mainfrom
fix/cms-github-proxy

Conversation

@vitorrgg

Copy link
Copy Markdown
Member

Contexto

O CMS (/admin) do store 1024 (tiasonia) quebrou em 19/08 com "Repo "tiasonia/tiasonia" not found". A mensagem engana: é o hasWriteAccess() do Decap traduzindo um 401 Bad credentials do GitHub. O gh_token (ghu_, user-to-server) devolvido por ecomplus.app/api/github-installations expirou e não é renovado — nem refazendo o OAuth completo do GitHub App (reproduzido: POST /api/github-callback com 2xx não regrava o token). O proxy da plataforma (/api/{store}/git/github) injeta a mesma credencial morta, então não havia caminho de recuperação nenhum.

O que muda

O storefront deixa de confiar cegamente no token da plataforma e ganha uma cadeia com fallback:

  1. Sonda o gh_token (GET /user) antes de usá-lo;
  2. Se morto/ausente, cai para o proxy GitHub da própria loja (/_api, função feeds, com GITHUB_TOKEN do env) usando o token do SSO — a sonda bate em /_api/repos/{repo}, validando auth da loja + PAT + acesso ao repo de uma vez;
  3. Se nada autentica, mensagem explícita em pt-BR em vez do "Repo not found" do Decap.

O proxy da loja existia mas nunca poderia ter funcionado — faltavam consumidores para expor os defeitos:

  • Header Authorization lido com slice(7) ("Bearer "), mas o Decap manda token *** (não configurável) → 1 caractere do access token era cortado;
  • Query string descartada (req.path) → ?ref={branch} perdido, leituras sempre na branch padrão (erro silencioso);
  • Content-Length em unidades UTF-16 → commits com as mensagens padrão (aspas curvas “”) rejeitados pelo fetch;
  • Sem rota para /user, /repos/:o/:r bare (o hasWriteAccess()permissions.push dali) e /search/** (notas de entrada);
  • /repos/:o/:r/pulls bare (Decap lista/cria PRs do editorial workflow ali) não casava com o rewrite {git,contents,...}/**;
  • O proxy esperava o catálogo inteiro ser buscado antes de responder — e a promise de módulo, uma vez rejeitada, deixava a função em 500 permanente. Agora responde antes e independente do catálogo.

/user responde com o usuário autenticado da loja (autoria correta nos commits) e o endpoint do repo reporta permissions.push — o acesso de escrita é concedido pelo edit_storefront da Store API, não pela permissão do dono do GITHUB_TOKEN.

Como validar

Loja com GITHUB_TOKEN (PAT fine-grained: Contents RW, Pull requests RW, Metadata R) no env do grupo many, CLI atualizado (regenera o firebase.json com os rewrites) e deploy de many + ssr. O /admin deve autenticar mesmo com o gh_token da plataforma expirado. Sem GITHUB_TOKEN, deve exibir a mensagem de credencial expirada em vez de "Repo not found".

Atenção na revisão

  • Semântica de **//*/* nos rewrites do Firebase Hosting (mantive ambos por segurança);
  • Popup de login do Decap no fluxo de fallback reusa o handshake existente, mas nunca rodou com api_root próprio;
  • Sem teste automatizado deste caminho — não há infra de teste no storefront na main para pendurar.

Não corrige a causa raiz (backend do ecomplus.app não renova nem regrava o ghu_) — tratada em issue separada.

🤖 Generated with Claude Code

vitorrgg and others added 5 commits August 24, 2026 16:21
Decap CMS could never actually work through this proxy, it only lacked
consumers to surface the defects:

- Answer `/user` with the authenticated store user, so the CMS editor is
  identified as the commit author instead of the token owner
- Report `permissions.push` on the bare repository endpoint: write access is
  granted by `edit_storefront` on store auth, not by the `GITHUB_TOKEN` owner
- Accept the `token` auth scheme Decap sends (a fixed 7 char offset for
  "Bearer " was dropping one character of the store access token)
- Keep the query string when proxying (`?ref={branch}` was lost, so reads
  always resolved against the default branch)
- Send the actual byte count on Content-Length (default commit messages
  contain curly quotes, so requests with them were rejected by fetch)
- Forward `/search/**`, used by Decap for entry notes
- Serve the proxy before and regardless of catalog fetching, so the CMS does
  not go down nor wait when the products API is slow or failing

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The GitHub proxy rewrite required a `{git,contents,issues,branches,pulls,
commits}` segment, so `/_api/user`, `/_api/repos/{owner}/{repo}`, bare
`/pulls` (Decap lists and creates PRs there) and `/_api/search/**` fell
through to SSR. Replaced by `/_api/repos/**` plus the auth endpoints.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…xpires

The /admin page trusted the `gh_token` returned by the platform without ever
checking it, and had no fallback. An expired credential surfaced as Decap's
misleading "Repo not found" error, with no way to edit content until someone
reissued the token on the platform side.

- Probe the credential on `GET /user` before trusting it
- Fall back to the store own GitHub proxy (`/_api`, `feeds` function) with
  the SSO token, probing the repo endpoint since it checks store auth,
  `GITHUB_TOKEN` and repository access at once
- Show an explicit message when no backend can authenticate, instead of
  letting Decap blame the repository

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ints

Review follow-ups on the proxy:

- Pin proxied requests to `GITHUB_REPO` and allowlist CMS subresources
  (`git`, `contents`, `issues`, `branches`, `pulls`, `commits`), in the
  function itself since hosting rewrites are bypassable via the direct
  function URL. Without this, any store user with `edit_storefront` could
  reach `/hooks`, `/keys` or arbitrary repositories through the PAT
- Restore CORS and preflight handling on proxy responses (regression on
  85bdcae), now answered by the proxy itself
- `Cache-Control: private, no-store` on authenticated responses
- Guard the module level catalog promise against unhandled rejection now
  that proxy requests skip awaiting it

Covered by unit tests exercising the requests Decap CMS actually sends.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A transient network error on the backend probe was discarding a working
token, downgrading a healthy CMS session to the error screen. Now only 401
and 403 invalidate; probes also get a shorter 5s timeout so the failure
path stays responsive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vitorrgg

Copy link
Copy Markdown
Member Author

Revisão adversarial feita sobre a versão inicial; achados corrigidos nos 2 últimos commits:

  • Segurança (crítico): o proxy encaminhava qualquer sub-rota de qualquer repo com o PAT da loja, gated só por edit_storefront — e o rewrite restritivo de hosting não protege a URL direta da função. Agora o próprio proxy pina o {owner}/{repo} ao GITHUB_REPO e restringe sub-rotas às que o Decap usa (git, contents, issues, branches, pulls, commits; /search/issues só escopado ao repo).
  • Regressão: o desvio do proxy antes do bloco CORS do serve-feeds removia o comportamento adicionado em 85bdcae (preflight caía em 406). CORS + OPTIONS agora respondidos pelo próprio proxy.
  • Resiliência: falha transitória de rede no probe descartava token válido — agora só 401/403 invalidam.
  • Cache-Control: private, no-store nas respostas autenticadas e guarda contra unhandled rejection da promise do catálogo.

Validação: 20 testes de unidade em packages/feeds/tests/ (sem script de teste no pacote — CI inalterado; rodar com npx vitest run packages/feeds/tests) cobrindo os requests reais do Decap 3, a matriz do ACL e o Content-Length multibyte; mais smoke read-only contra a API real do GitHub confirmando preservação de ?ref (shas distintos por branch) e injeção de permissions.push. Não exercitado: o fluxo do popup de login no navegador — recomendo deploy de validação numa loja antes da release.

Proxy requests now skip the serve-feeds path that set CSP, nosniff and
X-Frame-Options, so the proxy sets them itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant