# Autenticação e visão geral (referência)

API pública da Nebular para **agents e automações**: gerencie **projetos, serviços, deploys, variáveis, domínios, volumes, bancos e templates** e as **galerias de arquivos** servidas pela CDN (`*.nebularcdn.com`). Todas as rotas ficam em `/v1`, usam apenas `Authorization: Bearer` e respondem JSON (`application/json; charset=utf-8`, sempre `Cache-Control: no-store`). Esta especificação é servida em `GET /v1/openapi.json` (pública) e é validada contra o código a cada mudança.

### Como obter um token
No painel: **Time → Configurações → Tokens de API**. A criação exige papel `admin` do time e confirmação recente de identidade (step-up). O token tem o formato `nbk_<…>`, é **exibido uma única vez** (guardamos apenas o hash SHA-256), vale 90 dias por padrão (máximo 1 ano) e pode ser revogado a qualquer momento. Ele pertence ao **time**, nunca a uma pessoa. Pode ser restrito a algumas galerias (`assets:*`) e/ou a alguns projetos (`project_ids`).

O token vai **somente** no cabeçalho `Authorization: Bearer nbk_…`. **Nunca** o envie em query string. Cookies são **ignorados** nestas rotas (não há CSRF por construção) e as rotas de sessão do painel não aceitam Bearer. Cada operação indica os escopos exigidos na extensão `x-required-scopes` (todos são necessários) e, quando um escopo opcional muda a resposta, em `x-optional-scopes`.

### Escopos
| Escopo | Permite |
|---|---|
| `assets:read` | listar galerias e arquivos, consultar detalhes e status de varredura |
| `assets:write` | enviar, versionar e excluir arquivos |
| `galleries:manage` | criar galerias |
| `projects:read` | ler projetos, serviços, deployments, domínios, volumes, bancos e backups |
| `projects:write` | criar, editar e apagar projetos e serviços |
| `deploy` | disparar, cancelar e reverter deploys (serviço e grupo), enviar código-fonte |
| `env:read` | ler variáveis (valores sensíveis vêm `null`) |
| `env:write` | criar, alterar e apagar variáveis |
| `env:reveal` | ler valores sensíveis, `resolved=true` e credenciais de banco. Exige `env:read` e que o token tenha sido criado com confirmação de identidade (step-up) |
| `domains:write` | anexar e remover domínios |
| `volumes:write` | anexar, aumentar e apagar o volume |
| `databases:write` | criar, alterar, apagar, rotacionar, backup e restore de bancos |
| `logs:read` | ler log de build e de execução |
| `templates:deploy` | listar e instanciar templates |

Token sem o escopo ⇒ `403 forbidden`. Projeto de outro time, ou fora dos `project_ids` do token, é **indistinguível de um projeto inexistente** (`404 not_found`). `GET /v1/me` e `GET /v1/limits` aceitam qualquer token válido.

### Projetos e serviços
`{p}` nas rotas aceita o **id** (26 caracteres) ou o **slug** do projeto; `{s}` aceita o **id** ou o **nome** do serviço. Todo projeto nasce com um serviço `web` padrão. Variáveis podem referenciar outros serviços com `${{servico.VAR}}` (ex.: `${{db.MYSQL_HOST}}`); essas referências definem a ordem do deploy de grupo e são validadas no disparo.

### Deploys são assíncronos
`POST …/deployments`, `POST …/deploy`, rollback, backup e restore respondem `202` com um recurso em andamento. Acompanhe por **polling** com backoff (comece em 2 s e dobre até 15 s; não há webhooks neste ciclo):

- deployment: `queued → building → deploying → ready`, ou `failed`, `blocked`, `canceled`, `superseded`; em `failed`, `error` traz `code`, `message` e `details`;
- deploy de grupo: `running → ready | failed | canceled`, com o estado de cada serviço em `items[].status`;
- job de banco: `queued → running → done | failed`;
- domínio: `pending_dns → verifying → active | error`.

### Idempotência
Todo `POST` que cria ou dispara aceita `Idempotency-Key` (8 a 128 caracteres `[A-Za-z0-9._:-]`). Mesma chave + mesmo pedido em 24 h devolve a **mesma resposta** (com `Idempotent-Replay: true`); mesma chave com pedido diferente ⇒ `422 idempotency_conflict`; chave malformada ⇒ `400`. Só respostas `2xx` são gravadas, então repetir depois de uma falha executa de verdade. O upload de arquivos de galeria tem regra própria (`409 idempotency_conflict`, mais abaixo).

### Paginação
Listas devolvem `{"items": […], "next_cursor": "…"|null, "has_more": bool}`. Passe `next_cursor` em `?cursor=` para a página seguinte e use `?limit=` de 1 a 200 (padrão 50). O cursor é opaco; cursor malformado ⇒ `400 invalid_request`. Deployments, backups e a lista de projetos vêm do mais novo para o mais antigo; as demais do mais antigo para o mais novo (variáveis por nome).

### Segredos
Variáveis marcadas `sensitive` nunca voltam em claro por padrão (`value: null`); a resposta de `PUT` nunca ecoa um valor sensível; logs saem com segredos mascarados. Credenciais de banco só saem em `POST …/credentials` (nunca `GET`), com `env:reveal`, e a leitura é auditada.

### Limites
- Plano do time: serviços, memória, volumes, bancos e deploys por hora (`GET /v1/limits`). Ao atingir ⇒ `422 quota_exceeded`.
- Corpo JSON até 16 KiB (variáveis até 96 KiB por variável e 1 MiB no lote; serviços e templates até 64 KiB); código-fonte (multipart) até 512 MB.
- **Galerias:** 25 MB por arquivo (imagens também limitadas a 40 megapixels); cota por time no bucket compartilhado de 5 GB e 10 000 arquivos. Nomes de arquivo: até 4 segmentos separados por `/` (3 barras), cada um em `[a-z0-9._-]`, até 200 caracteres, sem `..`, sem segmento iniciando com `.` e com o primeiro segmento diferente de 12 caracteres hexadecimais minúsculos; únicos dentro da galeria. Nome inválido ⇒ `400 invalid_request`.
- Rate-limit por token e por IP: ao receber `429`, espere `Retry-After` segundos.

### Classes de arquivo e como cada uma é servida
O tipo é detectado pelo **conteúdo** (magic bytes), nunca pela extensão ou pelo `Content-Type` enviado. Extensão divergente do conteúdo em executável/documento é recusada (`415 extension_mismatch`).

| Classe | Exemplos | Servido como |
|---|---|---|
| `image` | png, jpeg, webp, gif, avif | inline, transformável (`?w=`, `?q=`) |
| `svg` | svg | inline só se passar na varredura de SVG, com CSP restritiva |
| `font` | woff2, woff, ttf, otf | inline, CORS `*` |
| `media` | mp4, webm, mp3, ogg | inline, com `Accept-Ranges` |
| `document` | pdf, txt, csv, json | `Content-Disposition: attachment`; texto sempre `text/plain` |
| `markup` | html, htm, xhtml, xml | nunca renderiza: `attachment` + `text/plain` + CSP `sandbox` |
| `archive` | zip, tar.gz, 7z | `attachment`; arquivo cifrado ou impossível de varrer é recusado |
| `binary` | exe, msi, dmg, apk, deb, appimage, desconhecido | `attachment`, `application/octet-stream` |

### Varredura (nada é publicado antes de `clean`)
Todo upload entra em quarentena e passa por antivírus, regras YARA, reputação por hash (só o hash sai da nossa infra), varredura específica de SVG e decodificação segura de imagens. Detectado ⇒ `422` e a amostra não é guardada. Scanner indisponível ⇒ o arquivo fica `pending` e **não é publicado** (resposta `202`; consulte o `GET` do arquivo até `scan.status` sair de `pending`). Todo dia os arquivos publicados são reescaneados com as assinaturas novas: uma detecção move o arquivo para `quarantined` (deixa de ser servido, responde 451, o cache é purgado e o time é avisado).

### Erros
Todo erro tem o formato `{"error": "<code>", "message": "…", "details"?: {…}}`; `message` é legível (pt-BR) e `error` é **estável** para tratamento programático (enumerado em `Error.error`). Algumas respostas trazem `details` (ex.: `references` em `unresolved_reference`/`reference_cycle`: apenas nomes, nunca valores). Os `5xx` e `429` trazem `Retry-After` quando repetir faz sentido. Nos corpos de **serviço**, **variável** e **deploy**, um campo desconhecido (ex.: `memory_mb` solto no topo; o certo é `{"resources": {"memory_mb": 1024}}`) é recusado com `400 invalid_request`, e a mensagem nomeia o campo, em vez de ser ignorado em silêncio.

| Status | `error` |
|---|---|
| 400 | `invalid_request`, `bad_json`, `bad_body` |
| 401 | `unauthorized` (genérico de propósito: token ausente, inválido, expirado ou revogado) |
| 403 | `forbidden` |
| 404 | `not_found` |
| 409 | `name_taken`, `slug_taken`, `project_quota_exceeded`, `project_not_empty`, `deploy_in_progress`, `nothing_to_rollback`, `not_cancelable`, `sensitive_lowered`, `duplicate_variable`, `domain_taken`, `volume_exists`, `volume_in_use`, `volume_attached`, `backup_not_ready`, `gallery_not_empty`, `idempotency_conflict` (upload de galeria) |
| 413 | `too_large`, `payload_too_large` |
| 415 | `unsupported_media_type`, `unsupported_type`, `extension_mismatch`, `class_not_allowed` |
| 422 | `quota_exceeded`, `idempotency_conflict`, `unresolved_reference`, `reference_cycle`, `invalid_reference`, `reference_too_deep`, `value_too_large`, `malware_detected`, `svg_unsafe`, `archive_encrypted`, `archive_bomb`, `archive_unsupported` |
| 429 | `rate_limited` |
| 500 | `internal` |
| 503 | `unavailable`, `authz_unavailable`, `database_unavailable` |

### Espelho do painel
O painel usa as mesmas operações em `/api/teams/{tid}/…` (sessão + CSRF, com confirmação de identidade em ações destrutivas). Essas rotas não fazem parte desta API pública e não aceitam Bearer.

## Servidores

- `https://api.nebularws.com` — Produção

## Esquemas de segurança

- `bearer`: http (bearer) Token de API de time (`nbk_…`) criado em Time → Configurações → Tokens de API. Somente no cabeçalho `Authorization: Bearer`; nunca em query string. Cookies são ignorados nestas rotas.
