# Anexo 004 — Cloudflare: inserir domínio na conta Ligbox (wizard VM112) **Parent:** [spec.md](./spec.md) · Spec **004** onboard-funnel-events **Criado:** 2026-06-22 · **Roger** **VM:** **112** (`10.10.10.112`) · API wizard `:8090` **URL pública:** `https://onboard.ligbox.com.br/api/onboarding/` Este anexo documenta **onde vive o código**, **funções**, **endpoints**, **fluxo UI** e **specs relacionadas** para criar/inserir um domínio na **conta Cloudflare Ligbox/Ibytera** durante o onboarding (opção «Trazer DNS para o portal»). --- ## Resumo operacional | Acção | Quem | Quando no wizard | |-------|------|------------------| | Validar domínio (formato + Carbonio) | `POST /validate-domain` | Passo 0 — antes do CF | | **Criar zona na conta CF** | `POST /dns/cloudflare/provision-zone` | Passo DNS — «Trazer para o portal» | | Aplicar MX/SPF/DKIM/… | `POST /dns/cloudflare/apply` | Após NS no registrador (ou auto sandbox) | | Verificar propagação | `GET /dns/verify/{domain}` | Antes de criar conta | | Emitir `dns.applied` (funil Desk) | `apply_cloudflare_dns` router | Após apply OK — ver [research.md](./research.md) | **Nota:** `provision-zone` **cria a zona**; `apply` **escreve registos DNS** dentro da zona. São passos distintos. **Spec 037 (2026-06-22):** três contas Ligbox (`ligit`, `itecnologys`, `ibytera`). `GET /dns/resolve/{domain}` descobre a conta; domínio fora das três → BYO (`connect-custom`) ou registrador. Ver [037-dns-multi-cloudflare-orchestration](../037-dns-multi-cloudflare-orchestration/spec.md). --- ## Onde vive o código (VM112) | Caminho | Papel | |---------|--------| | `/opt/ligbox-wizard/backend/app/services/cloudflare.py` | Cliente API Cloudflare (`CloudflareDNS`) | | `/opt/ligbox-wizard/backend/app/routers/onboarding.py` | Rotas HTTP onboarding + hooks webhook funil | | `/opt/ligbox-wizard/backend/app/services/dns_verify.py` | Verificação MX/A após apply | | `/opt/ligbox-wizard/backend/app/config.py` | `cloudflare_account_id`, `load_cloudflare_token_from_secrets()` | | `/opt/ligbox-wizard/secrets/cloudflare.token` | Token API (não commitar) | | `/opt/ligbox-wizard/secrets/README.txt` | Instruções token | | Frontend: `frontend/src/App.jsx` (ou deploy equivalente) | `choosePortalDns()`, `applyPortalDnsRecords()` | | Serviço: `systemctl status ligbox-wizard` | API `:8090` | **Repo espelho (dev):** `ibytera-mail-portal` / `ligbox-wizard` no monorepo ou LAPTOP — deploy para VM112 via `scp` + restart. **Desk VM122 (só leitura):** `projects/ops-desk/api/app/cloudflare_dns.py` — `GET /api/v1/dns/cloudflare/records` — **não cria zona**. --- ## Configuração e credenciais | Variável / ficheiro | Descrição | |---------------------|-----------| | `secrets/cloudflare.token` | API Token Cloudflare (conta Ligbox/Ibytera) | | `settings.cloudflare_account_id` | Account ID onde zonas são criadas (`POST /zones` body `account.id`) | | `settings.mail_public_ip` | IP para registos A/MX em `mail_dns_records()` | | `settings.onboard_sandbox` | Se true → modo sandbox (sem CF real) | | Token prefix `sandbox_` | Também activa `is_cloudflare_sandbox()` | ### Constitution — segurança token Fonte: [`.specify/memory/constitution.md`](../../.specify/memory/constitution.md) > **Cloudflare API:** Tokens com scope mínimo (zone DNS, **não** account-wide). **Scopes mínimos para provision-zone (produção):** | Scope | Motivo | |-------|--------| | `Account - Zone - Edit` | Criar zona na conta | | `Zone - Create` | `POST /zones` | | `Zone - DNS - Edit` | `upsert_record` (MX, SPF, TXT, A) | | `Zone - Read` | `get_zone_by_name`, list records | Mensagem de erro no código se faltar permissão: ```text Token sem permissão para criar zonas. Adicione «Account - Zone - Edit» e «Zone - Create» ao API token Ibytera. ``` --- ## Funções que criam/inserem a zona (conta CF) Ficheiro: `backend/app/services/cloudflare.py` ### Helpers | Função | Descrição | |--------|-----------| | `is_cloudflare_sandbox(token?)` | `True` se `onboard_sandbox` ou token `sandbox_*` | | `sandbox_zone(domain)` | Zona fictícia para testes (sem API CF) | | `wizard_nameservers(zone)` | Extrai NS da zona para UI | | `mail_dns_records(domain, mail_ip, aliases?)` | Lista dicts MX/SPF/DKIM/A para apply | ### Classe `CloudflareDNS` | Método | API CF | Descrição | |--------|--------|-----------| | `verify_token()` | `GET /user/tokens/verify` ou equivalente | Valida token antes de operações | | `get_zone_by_name(domain)` | `GET /zones?name=` | Procura zona na conta | | **`create_zone(domain, account_id?)`** | **`POST /zones`** | **Cria zona `type: full` na conta Ibytera**; se já existe (1061/1093) devolve existente | | **`ensure_zone(domain)`** | composto | **`get_zone_by_name` → se null → `create_zone`** — **função principal de inserção** | | `list_zone_names()` | `GET /zones` paginado | Lista domínios na conta | | `list_records(zone_id)` | `GET /zones/{id}/dns_records` | Registos actuais | | `upsert_record(zone_id, type, name, content, …)` | POST/PUT dns_records | Idempotente (81058 = já existe) | | `fqdn(name, zone_name)` | — | Normaliza `@` e subdomínios | ### Corpo `create_zone` (produção) ```json { "name": "exemplo.com.br", "account": { "id": "" }, "type": "full", "jump_start": false } ``` --- ## Endpoints API (wizard — passo DNS onboard) Base: `/api/onboarding` · Header obrigatório: `X-Onboarding-Session: ` | Método | Path | Handler | Função | |--------|------|---------|--------| | POST | `/validate-domain` | `validate_domain` | Valida domínio; emite `onboarding.started` + `domain.validated` | | GET | `/dns/cloudflare/status/{domain}` | `cloudflare_zone_status` | Estado zona (configurado?, NS, pending) | | **POST** | **`/dns/cloudflare/provision-zone`** | **`provision_cloudflare_zone`** | **`ensure_zone(domain)` — insere na conta CF** | | **POST** | **`/dns/cloudflare/apply`** | **`apply_cloudflare_dns`** | Aplica registos mail; emite **`dns.applied`** | | GET | `/dns/verify/{domain}` | `verify_dns` | Verifica propagação DNS pública | | GET | `/dns/portal-onboarding/{domain}` | `portal_dns_onboarding` | Payload UI passos NS | | GET | `/dns/instructions/{domain}` | `dns_instructions` | Instruções alternativas DNS | | POST | `/account/create` | `create_account` | Conta Carbonio + infra (após DNS OK) | ### `provision-zone` — request/response **Request:** ```json { "domain": "exemplo.com.br" } ``` **Response (campos principais):** ```json { "domain": "exemplo.com.br", "zone_id": "…", "nameservers": ["…ns.cloudflare.com", "…"], "message": "Domínio … criado na Cloudflare Ligbox", "sandbox": false, "status": { "nameservers": ["…"] }, "steps": [ … ], "verification": { … } } ``` Em **sandbox**, `provision-zone` pode também aplicar registos simulados e preencher `verification`. ### `apply` — request ```json { "domain": "exemplo.com.br", "zone_id": "opcional-se-ja-conhecido", "mail_aliases": ["suporte", "vendas"] } ``` Se zona não existir na conta → HTTP **422** `zone_not_in_account` + `use_portal_onboarding: true`. --- ## Fluxo no wizard (UI) ```mermaid sequenceDiagram participant U as Cliente participant UI as Wizard App.jsx participant API as VM112 :8090 participant CF as Cloudflare API participant OPS as Desk VM122 U->>UI: Passo 0 — domínio UI->>API: POST /validate-domain API-->>OPS: webhook domain.validated (+ started 1x) U->>UI: Escolhe «DNS no portal Ligbox» UI->>API: POST /dns/cloudflare/provision-zone API->>CF: ensure_zone (create se necessário) CF-->>API: zone_id + nameservers API-->>UI: nameservers + passos U->>U: Altera NS no registrador UI->>API: POST /dns/cloudflare/apply API->>CF: upsert_record (MX, SPF, …) API-->>OPS: webhook dns.applied UI->>API: GET /dns/verify/{domain} U->>UI: Passo conta admin UI->>API: POST /account/create ``` ### Funções frontend (referência) | Função JS | Endpoint | |-----------|----------| | `choosePortalDns()` | `POST /onboarding/dns/cloudflare/provision-zone` | | `applyPortalDnsRecords()` / `applyPortalDns()` | `POST /onboarding/dns/cloudflare/apply` | | `refreshCfStatus()` | `GET /onboarding/dns/cloudflare/status/{domain}` | Ficheiro típico: `frontend/src/App.jsx` (passo DNS do onboard). --- ## Integração funil Desk (Spec 004) | Marco wizard | Evento webhook | Hook em `onboarding.py` | |--------------|----------------|------------------------| | Primeiro validate-domain OK | `onboarding.started` | `validate_domain` | | validate-domain OK | `domain.validated` | `validate_domain` | | **apply CF OK** | **`dns.applied`** | **`apply_cloudflare_dns`** (~L592) | | Infra pós-conta | `infra.synced` | `create_account` | | Fim OK | `onboarding.completed` | `create_account` | | Erro crítico | `onboarding.failed` | `create_account` except | Fase funil: `domain_validated` → **`dns_applied`** → `account_created` → … Ver [research.md](./research.md) tabela hook points. **Gap conhecido:** `provision-zone` **não** emite evento próprio — só cria zona. O funil avança para `dns_applied` no **apply**. --- ## Specs e documentos relacionados | # | Documento | Relação com Cloudflare onboard | |---|-----------|-------------------------------| | **004** | [spec.md](./spec.md) | Funil + `dns.applied` | | **004** | [research.md](./research.md) | Hook `apply_cloudflare_dns` | | **004** | [contracts/webhook-funnel-events.md](./contracts/webhook-funnel-events.md) | Payload eventos funil | | **001** | [specs/001-webhook-vm112-integration/spec.md](../001-webhook-vm112-integration/spec.md) | Webhook base + `domain.validated` | | **012** | [specs/012-abandoned-onboarding-lead/spec.md](../012-abandoned-onboarding-lead/spec.md) | `onboarding.started` em `account/create` (não validate) | | **016** | [specs/016-onboard-self-service-prefill/spec.md](../016-onboard-self-service-prefill/spec.md) | Pré-preenche domínio passo 0 | | **025** | [specs/025-wizard-onboarding-continuity/spec.md](../025-wizard-onboarding-continuity/spec.md) | Fluxo domínio/DNS idempotente | | **017** | [specs/017-vm112-domain-orchestration/spec.md](../017-vm112-domain-orchestration/spec.md) | **Purge** — apaga zona CF (passo 5) | | **010** | [specs/010-desk-assist-takeover/spec.md](../010-desk-assist-takeover/spec.md) | `dns.revalidate` / assist passo DNS | | **010** | [specs/010-admin-domain-validation/spec.md](../010-admin-domain-validation/spec.md) | Checks infra (não cria zona) | | **019** | [specs/019-email-migration-vm122-execution/spec.md](../019-email-migration-vm122-execution/spec.md) | MX CF bloqueado até gate DNS | | **035** | [specs/035-ligbox-mail-bundles-foss-openpanel/spec.md](../035-ligbox-mail-bundles-foss-openpanel/spec.md) | Wizard: MX, SPF, DMARC records | | **027** | [specs/027-desk-rbac-function-matrix/spec.md](../027-desk-rbac-function-matrix/spec.md) | RBAC `cloudflare_dns.read` Desk | | — | [constitution.md](../../.specify/memory/constitution.md) | Token CF scope mínimo | | — | `chat-bruto/CHAT_BRUTO_WIZARD_VM112_20260619.txt` | E2E `provision-zone`, sandbox, fixes | --- ## Testes rápidos (curl) ```bash SESSION="test-$(date +%s)" BASE="https://onboard.ligbox.com.br/api/onboarding" HDR=(-H "X-Onboarding-Session: $SESSION" -H "Content-Type: application/json") # 1. Validar domínio curl -s -X POST "$BASE/validate-domain" "${HDR[@]}" \ -d '{"domain":"exemplo.com.br"}' | jq . # 2. Estado CF curl -s "$BASE/dns/cloudflare/status/exemplo.com.br" "${HDR[@]}" | jq . # 3. Inserir zona na conta Ligbox curl -s -X POST "$BASE/dns/cloudflare/provision-zone" "${HDR[@]}" \ -d '{"domain":"exemplo.com.br"}' | jq . # 4. Aplicar registos mail (após NS) curl -s -X POST "$BASE/dns/cloudflare/apply" "${HDR[@]}" \ -d '{"domain":"exemplo.com.br"}' | jq . # 5. Verificar DNS curl -s "$BASE/dns/verify/exemplo.com.br" "${HDR[@]}" | jq . ``` LAN directo: substituir host por `http://10.10.10.112:8090/api/onboarding`. --- ## Erros comuns | Sintoma | Causa provável | Acção | |---------|----------------|-------| | `Token Cloudflare não configurado` | Falta `secrets/cloudflare.token` | Criar token + restart wizard | | `Token sem permissão para criar zonas` | Scopes insuficientes | Account Zone Edit + Zone Create | | `zone_not_in_account` no apply | `provision-zone` não foi corrido | Correr provision primeiro | | Zona noutra conta CF | Domínio já em CF cliente | Opção «manter no provedor» ou transfer | | Sandbox sem CF real | Token `sandbox_` ou `onboard_sandbox` | Esperado em dev | --- ## Manutenção deste anexo Actualizar quando: - Alterar `cloudflare.py` ou rotas em `onboarding.py` - Novos eventos funil ligados a CF - Mudança de conta Cloudflare ou scopes token - Refactor frontend passo DNS Após editar: `refresh-spec-driver.sh` no CT130 (regra `spec-driver-sync`).