Este host publica o scaffold. Não há catálogo aqui.
# Winna Vitrine vazia da Winna na org `aranduOS`. Não há catálogo desenhado aqui. O contrato de uso está nestes documentos, e a identidade pública está em [`loja.json`](loja.json). O `npm run build` junta os textos numa página só. O host sem domínio próprio é `winna.preview.aranduos.com`. | Doc | Conteúdo | |---|---| | [`docs/loja.md`](docs/loja.md) | Tenant id, admin, preview, checkout | | [`docs/local.md`](docs/local.md) | Instalar, testar e correr o BFF na tua máquina | | [`docs/bff.md`](docs/bff.md) | `/api` sem o bearer no browser | | [`docs/checkout.md`](docs/checkout.md) | Carrinho aqui, pagamento no checkout hospedado | | [`docs/dominios.md`](docs/dominios.md) | Domínio próprio sem token de DNS | | [`docs/cursor-claude.md`](docs/cursor-claude.md) | MCP da loja no Cursor/Claude, OAuth | | [`docs/sdk.md`](docs/sdk.md) | Cliente, token no servidor, o que cada exemplo chama | | [`docs/deploy.md`](docs/deploy.md) | Pull request, Pages, o que não entra neste repositório | | [`docs/b2b.md`](docs/b2b.md) | Empresas e orçamentos | | [`docs/audiencia.md`](docs/audiencia.md) | Tráfego, públicos e base por estágio | | [`docs/funil.md`](docs/funil.md) | Funil de pré-venda | | [`AGENTS.md`](AGENTS.md) | Regras para quem edita com um agente | Exemplos, em TypeScript, sem efeito ao importar: - [`examples/b2b.ts`](examples/b2b.ts) - [`examples/audiencia.ts`](examples/audiencia.ts) - [`examples/funil.ts`](examples/funil.ts) - [`examples/checkout.ts`](examples/checkout.ts) Catálogo e carrinho passam pelo `/api` ([`docs/bff.md`](docs/bff.md)). O pagamento é o checkout hospedado ([`docs/checkout.md`](docs/checkout.md)).
# Audiência Há três leituras, nenhuma delas é o catálogo. `audienceOverview` com `source=first_party` é o tráfego medido pelos eventos da própria loja: sessões, pessoas, novos, páginas. `source=ga4` lê o que foi ingerido do GA4. Scope `reports:read`. `from` e `to` são date-time. `listMediaAudiences` lista os públicos que a loja monta a partir de quem comprou (compradores, valor, recorrentes, em risco, sumidos, fiéis), com contagem elegível, sem anonimizado e sem opt-out. Lista de prospecção não entra: sem dado first-party com consentimento, não se sobe. `exploreCustomerBase` é a base pela mesma régua de estágio do core: `new`, `repeat`, `loyal`, `at_risk`, `dormant`, e `never_bought` para quem se cadastrou e não comprou. Scope `customers:read`. O corpo pode ir vazio de filtros. A página traz facetas e o resumo. O exemplo é [`examples/audiencia.ts`](../examples/audiencia.ts).
# B2B Empresa e orçamento são da tag `b2b`. A leitura pede `companies:read`. `listCompanies` devolve a página de empresas. `listSalesQuotes` filtra por `company_id` e por `status`. Criar empresa (`createCompany`) exige `legal_name` e `document`, scope `companies:write`, e não está no exemplo para ninguém o disparar ao importar o ficheiro. Enviar, aceitar ou rejeitar um orçamento são comandos à parte (`sendSalesQuote`, `acceptSalesQuote`, `rejectSalesQuote`). Também são escrita, também ficam no servidor. O exemplo é [`examples/b2b.ts`](../examples/b2b.ts).
# BFF O browser da vitrine não fala com `api.mira-dev.tech` e não vê o bearer. Fala com `/api/...` neste host. Quem acrescenta `Authorization` e `X-Tenant-ID` é a Function em [`functions/api/[[path]].js`](../functions/api/[[path]].js). Em produção a variável `COMMERCE_STOREFRONT_BEARER` é `secret_text` do projecto Pages. Não está no repositório, não é `NEXT_PUBLIC_` e não se lê daqui. Em local, `npm run dev:bff` usa o token que tu criaste no admin. O código é o mesmo. ## Prova No host publicado, sem token nenhum: | Pedido | Resposta | |---|---| | `GET /api/members/me` | 200, com o `member_id` de `loja.json` | | `GET /api/storefront/products?limit=1` | 200 | | `GET /api/orders` | 403 | `503` com `bff_token_missing` quer dizer que a publicação não tem a variável. Não se pede o valor. Abre-se uma issue neste repositório. ## O que passa A lista é [`scripts/bff-allowlist.mjs`](../scripts/bff-allowlist.mjs). É a allowlist do core para o bearer de vitrine: catálogo, composição de checkout, sessão de checkout, frete, preço e o conteúdo público da loja. Qualquer outro path responde 403, mesmo que o token tenha scope para ele. `..` e barra codificada no path respondem 400. ## Sessão de checkout `POST /api/checkout/cart` e `POST /api/checkout/sessions` podem devolver `session_token` uma vez. A Function tira esse campo do JSON e grava-o no cookie `checkout_session` (`HttpOnly`, `Secure`, `SameSite=Lax`). No pedido seguinte o cookie volta como `X-Checkout-Session-Token`. Não o metas em `localStorage` nem em query string. ## O que não fazer - Não pôr o bearer no HTML, no JavaScript do browser, nem numa variável `NEXT_PUBLIC_`. - Não fazer de `/api` um proxy aberto. - Não acrescentar `orders` à allowlist para "ver um pedido". Pedido do comprador é a sessão dele, no checkout hospedado.
# Checkout
O checkout da Winna não se constrói neste repositório. É a app hospedada em `loja.checkout`:
`https://winna-checkout.mira-dev.tech/compra`
O tenant sai desse hostname. Não se manda `member_id` na query.
## Da vitrine para o checkout
1. A página chama o BFF, não a API.
```json
POST /api/checkout/cart
{
"channel": "web",
"op": "add",
"items": [{ "sku": "<sku>", "quantity": 1 }]
}
```
2. A resposta traz o carrinho. O `session_token` fica no cookie, não no corpo.
3. O browser segue para `https://winna-checkout.mira-dev.tech/compra`. O pagamento, a entrega e a confirmação são aquela app.
`op` é `add`, `set` ou `remove`. Preço no body é ignorado: quem calcula é o core (`composeCheckoutCart`).
O exemplo [`examples/checkout.ts`](../examples/checkout.ts) só monta este pedido. Não corre ao importar.
## O que fica neste repo
- O link para o checkout.
- A chamada de carrinho pelo BFF.
- Domínio próprio da vitrine, em `loja.json`. Isso não muda o host do checkout.# Cursor e Claude — MCP da loja, por OAuth
Isto liga o Cursor ou o Claude à **Winna**, para cadastrar produto, gravar configuração e importar ficheiro. A autenticação é OAuth 2.1 (PKCE). Não se cola API key.
O admin da loja é `https://winna.aranduos.com`. O `member_id` está em [`loja.json`](../loja.json). Não é segredo. O browser de consentimento entra nesse admin.
## O que não fazer
- Não commitar `.cursor/mcp.json`, `~/.cursor/mcp.json`, `~/.claude.json` nem `~/.claude/mcp.json`. O `.gitignore` ignora `.cursor/mcp.json`.
- Não pôr `headers`, `X-API-Key`, `X-Tenant-ID`, `mcp_live_…` nem bearer na config. O tenant já vai no path.
- Não usar o endpoint legado `https://api.mira-dev.tech/mcp`. Esse é de bot e de CI.
- Não apontar o mesmo servidor a outra loja. O nome desta é `mira-commerce-winna`.
## Configuração
O `<member_id>` é o campo `member_id` de `loja.json` (`9fed737f-7a30-4014-84c4-577a0862b451`).
```json
{
"mcpServers": {
"mira-commerce-winna": {
"url": "https://api.mira-dev.tech/tenants/<member_id>/mcp"
}
}
}
```
No Cursor, isto fica no `mcp.json` **local** (do projecto ou do utilizador). No Claude Code, Claude Desktop ou Codex, a mesma entrada, com a chave `url`. No Windsurf, se o schema pedir, use `serverUrl` no lugar de `url`. Continua sem headers.
Reinicie o cliente. Na primeira chamada ele descobre o OAuth, regista um client público e abre o browser. Entre **já autenticado** em `https://winna.aranduos.com` e autorize **Operação**. Só leitura não cria produto, não grava configuração e não aplica importação.
O access token dura cerca de 12 horas e renova sozinho com o refresh (cerca de 30 dias). Não gere outro token a cada erro. Reconecte só se a sessão foi revogada ou se o papel no admin mudou. Sessões antigas revogam-se em `https://winna.aranduos.com/admin/settings/mcp-tokens`.
Se uma tool `api_*` responder `insufficient scope`, o token foi emitido antes do papel ter esse scope. Revogue e autorize de novo em Operação.
## Como achar a tool
O `tools/list` não traz as mil operações. Traz o núcleo e duas meta-tools:
1. `mira_search_tools` — procura pelo nome.
2. `mira_get_tool_schema` — devolve o corpo de **uma** tool.
Procure, leia o schema, chame. Não invente campos.
Para anunciar só o catálogo: `https://api.mira-dev.tech/tenants/<member_id>/mcp/x/catalog`. A suite muda o que aparece, não o que o token pode fazer.
O catálogo público, sem credencial, é `https://api.mira-dev.tech/mcp/catalog`. Tool listada ali e ausente no Cursor é scope ou suite, não uma tool em falta.
## Cadastro de produto
Preset **Operação**, scope `catalog:write`.
1. `mira_search_tools` com `produto`.
2. `mira_get_tool_schema` em `api_create_product`.
3. `api_create_product`. SKU à parte: `api_create_product_sku`. Código de barras: `api_create_product_barcode`.
4. Confirme com `search_products` ou `api_list_products` antes de criar de novo.
O prompt `operar-catalogo` (`prompts/get`) descreve a ordem. Leia `mira://docs/business-rules` antes da primeira escrita.
## Configurações
A configuração geral da loja é `api_get_tenant_settings` (`settings:read`) e `api_update_tenant_settings` (`settings:write`). O update pode pedir step-up: a pessoa confirma no admin. O agente não fabrica esse token.
Outras configurações são outras tools. Procure `settings` e leia o schema. Exemplos que existem: `api_get_presales_settings`, `api_get_product_feed_settings`, `api_get_payment_link_settings`. Não assuma que uma serve para a outra.
## Importações
A prévia não grava. Aplicar grava.
1. `api_list_import_targets` — destinos e colunas. É daqui que se sabe a planilha. Não há lista embutida neste repositório.
2. `api_create_import` — scope `imports:write`. Corpo JSON com o ficheiro em `content_base64` ou `content_text`, mais `target`. Devolve a prévia: contagens e linhas com problema. Nada foi para o razão.
3. Corrija linha a linha com `api_update_import_row` se o schema o permitir.
4. `api_apply_import` — só as linhas `ready`. Inválida, duplicada ou excluída fica de fora. O resultado diz quantas.
Não chame o apply no mesmo turno da prévia sem a pessoa ter visto as contagens.# Deploy A Winna publica no Cloudflare Pages, projecto `storefront-winna`, conta da Mirá. O host sem domínio próprio é `winna.preview.aranduos.com`. Este repositório não tem workflow de deploy e não tem Actions secrets. O Pages, na conta da Mirá, corre `npm run build`. Isso gera `out/index.html` com o texto destes documentos. O token da Cloudflare não entra aqui. O `CNAME` de `winna.preview.aranduos.com` aponta para `storefront-winna.pages.dev`. ## Como pedir uma publicação 1. Fork privado. 2. Pull request do fork para `main` deste repositório. 3. O check que decide é o que corre a partir de `main`, sem segredo. Um workflow trazido pelo PR pode ficar verde. Não conta. 4. O merge em `main` é o deploy. Não corras `deploy-k8s.yml` nem `deploy-pages.yml`. Esses ficheiros saíram. O core recusa actualizar o pod (ADR 0247). ## O que a Mirá faz, e tu não - Gravar o bearer como `secret_text` do projecto Pages e voltar a publicar. O valor não se lê daqui. A prova está em [`bff.md`](bff.md). - Criar o `CNAME` de `winna.preview.aranduos.com` para o projecto Pages. - Registar o tenant no core. Sem essa linha, o host não é oficial. ## Ambiente local Ver [`local.md`](local.md). `.env.example` lista as variáveis com valor vazio. O `.env` fica de fora do git. Domínio próprio: [`dominios.md`](dominios.md). BFF: [`bff.md`](bff.md). Checkout: [`checkout.md`](checkout.md). A identidade da loja: [`loja.json`](../loja.json).
# Domínios Há quatro hosts. Só um se edita neste repositório. | Host | Quem muda | Onde | |---|---|---| | `winna.preview.aranduos.com` | Já é a vitrine sem domínio próprio | `loja.preview` | | `winna.aranduos.com` | Admin. Não é a vitrine | `loja.admin` | | `winna-checkout.mira-dev.tech` | Checkout hospedado | `loja.checkout` | | O domínio da marca | Tu, neste ficheiro | `loja.custom_hosts` | ## Domínio próprio Acrescenta o hostname em `custom_hosts` e abre o pull request. Sem `https://`, sem path. ```json "custom_hosts": ["www.exemplo.com"] ``` O PR é o pedido. O registo DNS fica na zona da Mirá. Tu não recebes `CLOUDFLARE_API_TOKEN` nem entras na conta Cloudflare. Não cries o CNAME à mão e não peças o token. Enquanto esse registo não existir, o endereço que funciona é o `preview`. O merge em `main` publica o código nesse host sem um passo extra. Não uses o label `preview` como slug de loja. `preview.aranduos.com` é da plataforma.
# Funil de vendas `presalesFunnelReport` é o funil de pré-venda: entrada, conversão entre estágios, tempo mediano, primeira resposta e motivos de perda. Scope `presales:read`. O corpo leva `from` e `to` como instante. O dia civil é o fuso do tenant. `group_by` recorta por `source`, `channel`, `campaign`, `owner`, `stage` ou `state`. O exemplo agrupa por `stage`. Métrica que a base não mede volta ausente. A cobertura diz porquê. Não se trata ausência como zero. A agenda do vendedor (`getCustomerAgenda`) e a base (`exploreCustomerBase`) usam a mesma régua de estágio. O funil não se recalcula na vitrine. O exemplo é [`examples/funil.ts`](../examples/funil.ts).
# Testes locais Não precisas de chave da Cloudflare, de bearer de produção, nem de acesso ao cluster. ```bash cp .env.example .env npm install npm run test:local npm run typecheck npm run build ``` `npm install` usa o teu `GITHUB_TOKEN` com `read:packages` (ver `.npmrc`). `test:local` lê `loja.json` e recusa segredo nesse ficheiro. `typecheck` e `build` não chamam a API. ## Token teu No `.env`: - `COMMERCE_TOKEN` — token que criaste em `https://winna.aranduos.com/admin/settings/api-tokens`, com o scope do exemplo (`companies:read`, `reports:read`, `growth:read`, `presales:read`, `customers:read`). Serve para B2B, audiência e funil, no servidor. - `COMMERCE_STOREFRONT_BEARER` — outro token teu, criado na mesma tela, só para o BFF local. Em produção este nome é uma variável do Pages que tu não lês. Localmente o valor é o teu, e não sai da máquina. O `member_id` já está em `loja.json`. Não o copies para o git outra vez. ## BFF na tua máquina ```bash npm run dev:bff ``` Ouve em `http://127.0.0.1:8788`. Só encaminha os paths da allowlist. Sem o teu token no `.env`, sai com erro e não abre a porta. ```bash curl -sS http://127.0.0.1:8788/api/members/me ``` `GET /api/orders` tem de responder 403. Se responder 200, a allowlist falhou: não publiques. O checkout local usa o mesmo processo. O `session_token` volta num cookie `HttpOnly`, não no JSON.
# Loja A identidade pública da Winna está em [`loja.json`](../loja.json), na raiz. É o que precisas para desenvolver. Não é segredo. | Campo | Uso | |---|---| | `member_id` | UUID do tenant. Vai no header `X-Tenant-ID` e no path do MCP. O mesmo valor que o admin mostra. | | `api` | `https://api.mira-dev.tech` | | `admin` | Admin da loja, `https://winna.aranduos.com` | | `preview` | Vitrine sem domínio próprio, `https://winna.preview.aranduos.com` | | `checkout` | Checkout hospedado, `https://winna-checkout.mira-dev.tech` | | `custom_hosts` | Domínios próprios desta vitrine. Lista de hostnames, sem `https://` | O `member_id` vazio significa que o tenant ainda não foi registado. Não inventes um UUID. Tokens não entram neste ficheiro. O teu token de staff cria-se no admin, em `https://winna.aranduos.com/admin/settings/api-tokens`, e fica só no `.env` da tua máquina. O valor aparece uma vez, na criação. Não o peças à Mirá e não o commits.
# SDK
Pacote: `@mira-dev-tech/commerce-client-sdk`, tag `next` (o contrato publicado). Cada função é uma operação do OpenAPI. O nome é o `operationId`.
## Instalar
O registo é o GitHub Packages. O `GITHUB_TOKEN` do `.npmrc` é um token **teu**, só com `read:packages`. Não é token da plataforma.
```bash
cp .env.example .env
npm install
npm run typecheck
```
## Cliente
`examples/client.ts` cria o cliente com `COMMERCE_TOKEN`. O `member_id` vem de `loja.json` e sai no header `X-Tenant-ID`. Não o coloques outra vez no `fetch`.
O token fica no processo que corre no servidor. Código que o browser descarrega não importa `@mira-dev-tech/commerce-client-sdk` com esse token. O bearer da vitrine (`COMMERCE_STOREFRONT_BEARER`) também não serve para estes exemplos: ele é da superfície pública (catálogo, checkout). B2B, audiência e funil pedem scope de staff.
| Exemplo | Operações | Scope |
|---|---|---|
| `examples/b2b.ts` | `listCompanies`, `listSalesQuotes` | `companies:read` |
| `examples/audiencia.ts` | `audienceOverview`, `listMediaAudiences`, `exploreCustomerBase` | `reports:read`, `growth:read`, `customers:read` |
| `examples/funil.ts` | `presalesFunnelReport` | `presales:read` |
As funções devolvem `{ data }`. Não há chamada no import: quem quiser os dados chama a função.
Catálogo e checkout, quando fizerem falta, importam-se de `@mira-dev-tech/commerce-client-sdk/storefront-api` (`listStorefrontProducts`, `composeCheckout`). Esse subpath é a superfície da vitrine. B2B, audiência e funil estão no pacote completo e continuam no servidor.