# GERADO — não edite. # # Derivado de openapi/zero-api-v1.yaml por `zerocontracts live`, contendo SÓ as # 130 operações com `x-zero-lifecycle: live` — as que o ambiente do Zero serve. # # É este arquivo que alimenta SDK, CLI e MCP. A separação é estrutural e não # uma convenção: um gerador de cliente não tem como produzir um método para uma # operação planejada, porque ela não está no documento que ele lê. openapi: 3.1.0 info: title: Zero API version: "1.0.0" summary: API governada da plataforma Zero. description: | Zero transforma código ou uma intenção em uma aplicação operável em produção. Esta é a **única** superfície de mutação da plataforma. UI, CLI, MCP e agentes usam exatamente esta API. Não existe caminho alternativo: nenhum cliente recebe kubeconfig, OpenRC, token administrativo ou acesso ao banco interno. Toda operação aqui declara se **existe hoje** ou se é desenho de fase futura — ver "Ciclo de vida de cada operação", logo abaixo. A afirmação do parágrafo anterior só é verificável por causa dessa distinção. ## Princípios do contrato - O usuário declara **conceitos do produto** (aplicação, serviço, banco, domínio), nunca objetos Kubernetes. Capacidade fora do catálogo é recusada com explicação. - Toda mutação demorada é **assíncrona e durável**: responde `202` com `operation_id` e o cliente acompanha em `/operations/{id}`. - Toda criação aceita `Idempotency-Key`. Repetir a mesma chave com o mesmo corpo devolve a mesma resposta; com corpo diferente devolve `409`. - Todo erro segue o mesmo formato (`Problem`), com mensagem em português e ação sugerida. Código de erro é estável; texto pode evoluir. - Escopo de tenant é obrigatório e derivado do token, nunca do corpo da requisição. ## Ciclo de vida de cada operação Toda operação declara `x-zero-lifecycle`, e ele tem exatamente dois valores: - **`live`** — implementada e servida no ambiente do Zero. É a API ativa. - **`planned`** — desenhada e **não implementada**. Chamá-la devolve `404`. Isto NÃO é decoração. Durante meses este contrato declarou 32 operações que nenhum handler atendia, ao lado de 113 que existiam, sem nada no documento distinguindo as duas — e ao mesmo tempo 9 operações centrais (criar organização, projeto e serviço) eram servidas sem constar daqui. Um gerador de cliente produzia métodos que sempre falham; um gerador de tools de agente produzia ferramentas que sempre respondem "não encontrado". **A superfície ativa é gerada, não conferida a olho.** `zero-api-v1.live.yaml` — no mesmo diretório — é derivado deste arquivo contendo SÓ as operações `live`. É ele que alimenta SDK, CLI e MCP. Um consumidor automático não tem como enxergar uma operação planejada, porque ela não está no documento que ele lê. Uma operação `planned` carrega também: - `x-zero-phase`: a fase do roadmap em que ela entra; - `x-zero-planned-reason`: por que ainda não existe. O portão `TestContratoDescreveAImplementacao`, no `zero-control-plane`, reprova o build quando `live` e implementação divergem em qualquer direção — rota, método, autenticação, streaming ou código de sucesso. ## Extensões - `x-zero-lifecycle`: `live` ou `planned`. Ver acima. - `x-zero-tool`: nome da tool MCP equivalente. O servidor MCP é gerado a partir deste contrato — não existe superfície agentic paralela. - `x-zero-destructive`: a operação destrói ou interrompe algo e exige confirmação explícita do humano quando disparada por um agente. - `x-zero-scope`: permissão mínima exigida. contact: name: NNumbers — produto Zero license: name: Proprietário — NNumbers identifier: LicenseRef-NNumbers-Proprietary servers: - url: "{baseUrl}/api/v1" description: >- A API pública do Zero. O padrão aponta para o ambiente da NNumbers; os clientes oficiais leem ZERO_API_BASE_URL para apontar outra instalação — nunca fixam host em código. variables: baseUrl: default: https://api.zero.nnumbers.com.br description: Base configurável (ZERO_API_BASE_URL). tags: - name: organizations - name: users - name: projects - name: environments - name: services - name: applications - name: builds - name: deployments - name: releases - name: domains - name: datastores - name: secrets - name: backups - name: operations - name: observability - name: usage - name: catalog - name: identity security: - bearerAuth: [] paths: # ────────────────────────────── organizations ─────────────────────────────── /organizations: get: operationId: listOrganizations x-zero-lifecycle: live x-zero-operator: organization.list summary: Lista as organizações do usuário autenticado. description: | A única rota da API que NÃO exige escopo de organização: ela responde justamente "de quais organizações eu faço parte?". Não é paginada. O número de organizações de uma pessoa é pequeno por natureza, e declarar cursor aqui prometeria uma continuação que o servidor não sabe produzir. tags: [organizations] x-zero-tool: zero.organizations.list x-zero-scope: organization:read responses: '200': description: Lista de organizações, cada uma com o papel de quem lê. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Organization' } '401': { $ref: '#/components/responses/Unauthorized' } post: operationId: createOrganization x-zero-lifecycle: live summary: Cria uma organização e torna quem chama dono dela. description: | O único ponto de mutação da plataforma sem tenant de onde partir — porque a organização é o que está sendo criado. A organização e a associação de `owner` nascem na MESMA transação. Não há instante em que uma organização exista sem dono: uma organização órfã seria administrável por ninguém, já que todo caminho de administração exige um membro para autorizar. `slug` é global e único. Sem ele, é derivado do nome. Duas requisições simultâneas com o mesmo slug: uma cria, e a outra recebe `409` `NAME_ALREADY_IN_USE` — ou, se trouxer `Idempotency-Key`, a mesma resposta da vencedora. **Não é autocadastro.** Só cria organização quem JÁ tem acesso ao Zero — membership em alguma organização, ou administração da plataforma. Existir no realm não basta: uma conta do realm sem acesso recebe `403 ZERO_ACCESS_REQUIRED`, e o caminho dela é alguém lhe conceder acesso. A organização nova nasce vinculada ao realm de quem a cria: é desse realm que a tela de Usuários lê o diretório. tags: [organizations] x-zero-tool: zero.organizations.create x-zero-scope: organization:write parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/OrganizationCreate' } responses: '201': description: Organização criada, com `role` igual a `owner`. content: application/json: schema: { $ref: '#/components/schemas/Organization' } '401': { $ref: '#/components/responses/Unauthorized' } '403': description: Quem chama ainda não tem acesso ao Zero (`ZERO_ACCESS_REQUIRED`). content: application/json: schema: { $ref: '#/components/schemas/Problem' } '409': { $ref: '#/components/responses/Conflict' } '422': { $ref: '#/components/responses/UnprocessableEntity' } # ── Por que NENHUMA rota de credencial é capacidade de agente ─────────── # # Elas não declaram `x-zero-operator` nem `x-zero-tool`, e a ausência é a # decisão: criar uma credencial é DELEGAR ACESSO, e delegar delegação a um # modelo de linguagem é a elevação de privilégio mais barata que existiria # nesta plataforma — bastaria convencê-lo a pedir uma credencial com mais # operações do que a conversa alcança. # # É o mesmo raciocínio que tirou a administração de identidade da superfície # que clientes alcançam: não é a mesma capacidade num lugar mais bonito, é a # mesma capacidade num lugar onde o caminho errado não chega. # # Quem gerencia credencial é uma pessoa, pelo console ou pela API, com # `manage_members`. A CLI as USA; ela não as cria. /organizations/{organizationId}/credentials: parameters: - $ref: '#/components/parameters/OrganizationId' get: operationId: listApplicationCredentials # Um AGENTE não administra credenciais: é a mesma regra de # `exigirPessoa` no control plane — o teto de quem concede depende de # quem concede ser uma pessoa, e uma credencial criando outra faria o # recorte deixar de significar alguma coisa. A exclusão é declarada # para a ausência da tool não parecer esquecimento. x-zero-mcp-exposed: false x-zero-lifecycle: live summary: Lista as credenciais de aplicação da organização. description: | Nunca devolve o segredo: ele existe uma vez, na criação e na rotação. O que volta é o prefixo, que identifica qual credencial está em qual cofre sem publicar o resto. Exige `manage_members`: a lista diz quantas portas de máquina a organização tem abertas e o que cada uma alcança. tags: [organizations] x-zero-scope: organization:manage_members responses: '200': description: Credenciais. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/ApplicationCredential' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } post: operationId: createApplicationCredential # Um AGENTE não administra credenciais: é a mesma regra de # `exigirPessoa` no control plane — o teto de quem concede depende de # quem concede ser uma pessoa, e uma credencial criando outra faria o # recorte deixar de significar alguma coisa. A exclusão é declarada # para a ausência da tool não parecer esquecimento. x-zero-mcp-exposed: false x-zero-lifecycle: live summary: Cria uma credencial de aplicação. description: | Para pipelines, agentes e integrações: ela autentica sem login humano e sem depender do refresh token de uma sessão pessoal. A credencial **pertence à organização**, não a quem a criou — a saída dessa pessoa não a revoga, porque o contrário derrubaria esteiras de produção por motivo administrativo. Quem a administra é quem tem `manage_members`. Duas dimensões, independentes: - **alcance**: `all_projects` alcança a organização inteira, **inclusive os projetos futuros**; `selected_projects` é lista **fechada** — projeto criado depois não entra sozinho. - **operações**: um subconjunto do vocabulário de permissões do produto. O pedido é medido contra quem concede: ninguém delega o que não tem, e o que passa do teto é **recusado** com a lista do que passou — em vez de criar uma credencial menor do que a pedida, que funcionaria até o dia em que a esteira chegasse na operação que sumiu. Governança (`manage_members`, `manage_billing`) **não é delegável**: uma credencial que governasse poderia ampliar a si mesma. Uma credencial de aplicação **não pode** chamar esta rota. tags: [organizations] x-zero-scope: organization:manage_members requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ApplicationCredentialCreate' } responses: '201': description: | Criada. **O segredo aparece nesta resposta e em nenhuma outra.** content: application/json: schema: { $ref: '#/components/schemas/ApplicationCredentialWithSecret' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /organizations/{organizationId}/credentials/{credentialId}: parameters: - $ref: '#/components/parameters/OrganizationId' - name: credentialId in: path required: true description: O identificador da credencial (`acr_…`). schema: { type: string } patch: operationId: updateApplicationCredential # Um AGENTE não administra credenciais: é a mesma regra de # `exigirPessoa` no control plane — o teto de quem concede depende de # quem concede ser uma pessoa, e uma credencial criando outra faria o # recorte deixar de significar alguma coisa. A exclusão é declarada # para a ausência da tool não parecer esquecimento. x-zero-mcp-exposed: false x-zero-lifecycle: live summary: Ajusta o alcance e as operações de uma credencial. description: | O mesmo teto da criação vale aqui — ampliar por esta rota seria o caminho óbvio para contornar a conferência feita lá. A lista de projetos **substitui**: quem tira um projeto dela espera que ele saia, e a redução passa a valer na requisição seguinte da credencial, sem caminho próprio de invalidação. tags: [organizations] x-zero-scope: organization:manage_members requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ApplicationCredentialUpdate' } responses: '200': description: Credencial atualizada. content: application/json: schema: { $ref: '#/components/schemas/ApplicationCredential' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } delete: operationId: revokeApplicationCredential # Um AGENTE não administra credenciais: é a mesma regra de # `exigirPessoa` no control plane — o teto de quem concede depende de # quem concede ser uma pessoa, e uma credencial criando outra faria o # recorte deixar de significar alguma coisa. A exclusão é declarada # para a ausência da tool não parecer esquecimento. x-zero-mcp-exposed: false x-zero-lifecycle: live x-zero-destructive: true summary: Revoga uma credencial de aplicação. description: | O prazo efetivo é a **requisição seguinte**: não há token intermediário, a credencial é conferida contra o banco a cada chamada, e revogar é um UPDATE que a próxima consulta lê. Idempotente: revogar de novo devolve o mesmo `204` e não move a data em que ela realmente parou de valer. tags: [organizations] x-zero-scope: organization:manage_members responses: '204': { description: Revogada. } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /organizations/{organizationId}/credentials/{credentialId}/rotate: parameters: - $ref: '#/components/parameters/OrganizationId' - name: credentialId in: path required: true description: O identificador da credencial (`acr_…`). schema: { type: string } post: operationId: rotateApplicationCredential # Um AGENTE não administra credenciais: é a mesma regra de # `exigirPessoa` no control plane — o teto de quem concede depende de # quem concede ser uma pessoa, e uma credencial criando outra faria o # recorte deixar de significar alguma coisa. A exclusão é declarada # para a ausência da tool não parecer esquecimento. x-zero-mcp-exposed: false x-zero-lifecycle: live summary: Troca o segredo de uma credencial. description: | Preserva identificador, alcance, operações e trilha de auditoria. O segredo anterior **morre no mesmo instante** — não há período de graça, e a ausência é deliberada: dois segredos válidos ao mesmo tempo seriam dois segredos para revogar quando um vazasse. tags: [organizations] x-zero-scope: organization:manage_members responses: '200': description: | Rotacionada. **O segredo novo aparece nesta resposta e em nenhuma outra.** content: application/json: schema: { $ref: '#/components/schemas/ApplicationCredentialWithSecret' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /organizations/{organizationId}/members: parameters: - $ref: '#/components/parameters/OrganizationId' get: operationId: listOrganizationMembers x-zero-operator: organization.members x-zero-lifecycle: live summary: Lista os membros e seus papéis. tags: [organizations] x-zero-tool: zero.organizations.members.list x-zero-scope: organization:read responses: '200': description: Membros. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/OrganizationMember' } '403': { $ref: '#/components/responses/Forbidden' } # ── Usuários do realm e acesso ao Zero ────────────────────────────────── # # O realm é a IDENTIDADE; o Zero é a AUTORIZAÇÃO. As duas coisas nunca se # misturam nestas rotas: # # /users quem EXISTE no realm da organização (lido do provedor # de identidade, na hora — o Zero não guarda cópia) # /users/{id}/access o que essa pessoa pode fazer AQUI (membership, papel e # Spaces, no banco do Zero) # # Não há convite. Conceder acesso a quem já existe grava a membership na hora # e manda um e-mail INFORMATIVO; criar alguém cria a conta no realm, grava a # membership e pede ao provedor o e-mail oficial de definição de senha. O Zero # nunca vê, define, gera nem transporta senha. # # `userId` é o identificador do usuário no realm (o `sub` que o token dele # apresenta). O realm é o da organização, e nunca vem do pedido. /organizations/{organizationId}/users: parameters: - $ref: '#/components/parameters/OrganizationId' get: operationId: listRealmUsers x-zero-operator: organization.users x-zero-lifecycle: live summary: Lista e procura os usuários do realm da organização, com o acesso de cada um ao Zero. description: | A busca e a paginação acontecem no provedor de identidade: `q` casa com nome, e-mail e nome de usuário. Contas de serviço (máquinas) nunca aparecem, e usuários de outro realm também não — o realm é o que está vinculado à organização. Quando o realm não delegou ao Zero a leitura do diretório, a resposta NÃO falha: `directory.available` sai `false`, com o motivo e as permissões que faltam, e `items` traz só quem já tem acesso (lido do banco do Zero). Esconder quem tem acesso por causa de quem o Zero não consegue listar seria pior que as duas coisas separadas. tags: [users] x-zero-tool: zero.users.list x-zero-scope: organization:manage_members parameters: - name: q in: query description: Trecho do nome, do e-mail ou do nome de usuário. schema: { type: string, maxLength: 100 } - name: first in: query description: Quantos resultados pular (paginação no provedor). schema: { type: integer, minimum: 0, maximum: 10000, default: 0 } - name: max in: query schema: { type: integer, minimum: 1, maximum: 100, default: 20 } responses: '200': description: Uma página do diretório, com o acesso ao Zero de cada pessoa. content: application/json: schema: { $ref: '#/components/schemas/RealmUserPage' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '503': description: O provedor de identidade não respondeu (`IDENTITY_PROVIDER_UNAVAILABLE`). content: application/json: schema: { $ref: '#/components/schemas/Problem' } post: operationId: createRealmUser x-zero-operator: organization.create_user x-zero-lifecycle: live summary: Cria um usuário no realm da organização e concede o acesso ao Zero. description: | Quatro passos, cada um com estado próprio na resposta — porque atravessam dois sistemas sem transação entre eles: 1. a identidade nasce no realm, SEM senha e com a ação obrigatória de definir a senha; 2. a membership nasce no Zero, com papel e Spaces; 3. o Zero enfileira o e-mail de conta criada (informativo, sem link de senha); 4. o provedor de identidade envia o e-mail OFICIAL com o link de definição de senha. Falhar num passo não desfaz os anteriores: repetir com a MESMA `Idempotency-Key` retoma de onde parou, sem criar segunda conta, segunda membership nem e-mail a mais. Se já existe identidade com esse e-mail ou nome de usuário no realm, nada é criado: a resposta é `409 IDENTITY_USER_ALREADY_EXISTS`, com a identidade existente em `existing_user` — para conceder o acesso a ela. tags: [users] x-zero-tool: zero.users.create x-zero-scope: organization:manage_members parameters: - name: Idempotency-Key in: header required: true schema: { type: string, minLength: 8, maxLength: 200 } description: | Obrigatória: é ela que faz um clique duplo, um retry ou uma retomada depois de falha parcial não criarem uma segunda conta. requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/RealmUserCreate' } responses: '201': description: | A criação andou. `steps` diz o que já aconteceu — inclusive o que ficou pendente, que se retoma repetindo o pedido. content: application/json: schema: { $ref: '#/components/schemas/RealmUserProvisioning' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': description: | `IDENTITY_USER_ALREADY_EXISTS` (com `existing_user`), `IDENTITY_REALM_NOT_DELEGATED`, `IDENTITY_NO_REALM_BOUND`, `IDEMPOTENCY_KEY_CONFLICT` ou `IDENTITY_PROVIDER_DENIED`. content: application/json: schema: { $ref: '#/components/schemas/Problem' } '422': { $ref: '#/components/responses/UnprocessableEntity' } '503': description: O provedor de identidade não respondeu (`IDENTITY_PROVIDER_UNAVAILABLE`). content: application/json: schema: { $ref: '#/components/schemas/Problem' } /organizations/{organizationId}/users/{userId}: parameters: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/RealmUserId' get: operationId: getRealmUser x-zero-operator: organization.user x-zero-lifecycle: live summary: Um usuário do realm, com o estado dele no realm e o acesso ao Zero. tags: [users] x-zero-tool: zero.users.get x-zero-scope: organization:manage_members responses: '200': description: O usuário. content: application/json: schema: { $ref: '#/components/schemas/RealmUser' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': description: O realm não delegou a leitura do diretório (`IDENTITY_REALM_NOT_DELEGATED`). content: application/json: schema: { $ref: '#/components/schemas/Problem' } '503': description: O provedor de identidade não respondeu (`IDENTITY_PROVIDER_UNAVAILABLE`). content: application/json: schema: { $ref: '#/components/schemas/Problem' } /organizations/{organizationId}/users/{userId}/access: parameters: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/RealmUserId' get: operationId: getUserAccess x-zero-operator: organization.user_access x-zero-lifecycle: live summary: O acesso de um usuário do realm a esta organização. description: | Lido só do Zero. `state: none` é resposta, e não erro: é o estado da maioria das pessoas de um realm compartilhado. tags: [users] x-zero-tool: zero.access.get x-zero-scope: organization:manage_members responses: '200': description: O acesso. content: application/json: schema: { $ref: '#/components/schemas/UserAccess' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } post: operationId: grantUserAccess x-zero-operator: organization.grant_access x-zero-lifecycle: live summary: Concede acesso ao Zero a um usuário que já existe no realm. description: | A membership vale na hora: não há aceite, e a próxima entrada da pessoa já cai na organização. O Zero enfileira um e-mail INFORMATIVO ("Seu acesso ao Zero foi concedido") com o link normal de entrada — sem token, sem senha, sem redefinição. Falhar o e-mail não desfaz o acesso; o estado dele sai em `notification` e o reenvio é `POST /users/{userId}/access/notification`. Conta desabilitada no realm é recusada (`IDENTITY_USER_DISABLED`): o provedor continuaria recusando a entrada, e o acesso concedido em silêncio pareceria funcionar. Pedir o acesso que a pessoa já tem é idempotente (`200`, sem e-mail novo); pedir OUTRO papel para quem já tem acesso é `409 ACCESS_ALREADY_GRANTED` — mudar papel é `PATCH`. tags: [users] x-zero-tool: zero.access.grant x-zero-scope: organization:manage_members requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/UserAccessInput' } responses: '200': description: A pessoa já tinha exatamente este acesso. Nada mudou. content: application/json: schema: { $ref: '#/components/schemas/UserAccess' } '201': description: Acesso concedido. content: application/json: schema: { $ref: '#/components/schemas/UserAccess' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': description: | `ACCESS_ALREADY_GRANTED`, `IDENTITY_USER_DISABLED`, `IDENTITY_REALM_NOT_DELEGATED` ou `IDENTITY_NO_REALM_BOUND`. content: application/json: schema: { $ref: '#/components/schemas/Problem' } '422': { $ref: '#/components/responses/UnprocessableEntity' } '503': description: O provedor de identidade não respondeu (`IDENTITY_PROVIDER_UNAVAILABLE`). content: application/json: schema: { $ref: '#/components/schemas/Problem' } patch: operationId: updateUserAccess x-zero-operator: organization.update_access x-zero-lifecycle: live summary: Troca o papel ou os Spaces de quem já tem acesso. description: | Não recria nada nem manda convite: é a mesma membership com outro papel. A auditoria registra quem mudou, de qual papel para qual e com quais Spaces. `admin` e `operator` alcançam todos os Spaces pelo papel; o papel `member` exige ao menos um. tags: [users] x-zero-tool: zero.access.update x-zero-scope: organization:manage_members requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/UserAccessInput' } responses: '200': description: Acesso alterado. content: application/json: schema: { $ref: '#/components/schemas/UserAccess' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': description: A troca deixaria a organização sem administrador (`LAST_ADMINISTRATOR_REQUIRED`). content: application/json: schema: { $ref: '#/components/schemas/Problem' } '422': { $ref: '#/components/responses/UnprocessableEntity' } delete: operationId: revokeUserAccess x-zero-operator: organization.revoke_access x-zero-lifecycle: live summary: Revoga o acesso ao Zero. O usuário continua existindo no realm. description: | Remove SÓ a membership (e os Spaces dela) no Zero. A conta no realm, a senha e o acesso a outros produtos continuam onde estavam — apagar o usuário do realm não é operação do Zero. O efeito é imediato e não espera o token expirar: a autorização de cada requisição lê a membership no banco. tags: [users] x-zero-tool: zero.access.revoke x-zero-destructive: true x-zero-scope: organization:manage_members responses: '204': { description: Acesso revogado. } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': description: Removeria o último administrador (`LAST_ADMINISTRATOR_REQUIRED`). content: application/json: schema: { $ref: '#/components/schemas/Problem' } /organizations/{organizationId}/users/{userId}/access/notification: parameters: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/RealmUserId' post: operationId: resendAccessNotification x-zero-operator: organization.resend_access_notification x-zero-lifecycle: live summary: Reenvia o e-mail do Zero que avisa do acesso concedido. description: | O e-mail é aviso, e não autorização: o acesso já vale. Reenviar é para quando o envio falhou ou a pessoa não o encontrou. Há limite de reenvios por pessoa (`RATE_LIMITED`), para que o aviso não vire spam. tags: [users] x-zero-tool: zero.access.resend_notification x-zero-scope: organization:manage_members responses: '202': description: O aviso voltou para a fila de envio. content: application/json: schema: { $ref: '#/components/schemas/AccessNotification' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '429': { $ref: '#/components/responses/TooManyRequests' } /organizations/{organizationId}/users/{userId}/password-setup: parameters: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/RealmUserId' post: operationId: resendPasswordSetup x-zero-operator: organization.resend_password_setup x-zero-lifecycle: live summary: Pede de novo ao provedor de identidade o e-mail oficial de definição de senha. description: | Só vale para quem foi criado pelo Zero nesta organização e AINDA NÃO definiu a senha. Para qualquer outra pessoa a resposta é `409 PASSWORD_SETUP_NOT_PENDING`: conceder acesso nunca redefine a senha de ninguém. Quem envia é o provedor de identidade, com o link dele. O Zero não vê, não guarda e não repassa o link. tags: [users] x-zero-tool: zero.users.resend_password_setup x-zero-scope: organization:manage_members responses: '202': description: O provedor de identidade aceitou enviar o e-mail. content: application/json: schema: { $ref: '#/components/schemas/PasswordSetupRequest' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': description: | `PASSWORD_SETUP_NOT_PENDING`, `IDENTITY_USER_DISABLED` ou `IDENTITY_REALM_NOT_DELEGATED`. content: application/json: schema: { $ref: '#/components/schemas/Problem' } '429': { $ref: '#/components/responses/TooManyRequests' } '502': description: | O provedor de identidade não enviou o e-mail (`IDENTITY_ACTION_EMAIL_FAILED`), com o motivo. content: application/json: schema: { $ref: '#/components/schemas/Problem' } '503': description: O provedor de identidade não respondeu (`IDENTITY_PROVIDER_UNAVAILABLE`). content: application/json: schema: { $ref: '#/components/schemas/Problem' } /projects/{projectId}/members: parameters: - $ref: '#/components/parameters/ProjectId' get: operationId: listProjectMembers x-zero-operator: project.members x-zero-lifecycle: live x-zero-tool: zero.projects.members.list x-zero-scope: organization:read summary: Quem opera este Space description: | A tela chama de Space; o modelo é o Projeto. Não há entidade nova entre organização e projeto. A lista mistura duas origens, e a coluna `origin` diz qual é qual: `project` recebeu ESTE Space explicitamente. É um `member`, e esta linha é a autorização dele. `organization` alcança todos os Spaces pelo papel — `admin` ou `operator` — e não tem atribuição para remover. Sem essa distinção a tela ofereceria "remover" a quem não tem o que remover, e quem administra concluiria que o botão está quebrado. tags: [Members] responses: '200': description: Quem opera este Space. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/SpaceAccess' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/members/{subject}: parameters: - $ref: '#/components/parameters/ProjectId' - name: subject in: path required: true schema: { type: string } description: O assinante, como aparece na listagem de membros da organização. put: operationId: grantProjectMember x-zero-operator: project.grant_member x-zero-lifecycle: live x-zero-tool: zero.projects.members.grant x-zero-scope: organization:manage_members summary: Dar a alguém acesso a este Space description: | Idempotente: atribuir de novo devolve o mesmo `204` e não cria segunda linha. Só faz sentido para quem tem o papel `member` — `admin` e `operator` já alcançam todos os Spaces pelo papel. Atribuir a eles responde `409` `SPACE_ACESSO_PELO_PAPEL` em vez de criar uma linha que não muda nada hoje e mentiria amanhã: se a pessoa fosse rebaixada a `member`, ela manteria o acesso por uma atribuição que ninguém lembra de ter criado. Exige `admin`. Decidir quem entra num Space é governança, e quem opera não amplia o próprio alcance nem o dos outros. tags: [Members] responses: '204': { description: Acesso concedido. } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': description: | A pessoa já alcança todos os Spaces pelo papel que tem. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } delete: operationId: revokeProjectMember x-zero-operator: project.revoke_member x-zero-lifecycle: live x-zero-tool: zero.projects.members.revoke x-zero-destructive: true x-zero-scope: organization:manage_members summary: Tirar o acesso a este Space description: | Idempotente: quem não tinha atribuição também recebe `204`. O que não acontece é registro de auditoria para uma revogação que não revogou nada. Não alcança quem tem acesso pelo PAPEL: tirar um `operator` deste Space exige mudar o papel dele, não desatribuir. tags: [Members] responses: '204': { description: "Acesso removido, ou já não existia." } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/source: get: operationId: getProjectSource x-zero-operator: project.source summary: De onde este projeto publica. description: | A origem é do PROJETO: um projeto, uma origem. Todos os serviços dele publicam do mesmo repositório e diferem apenas na pasta que constroem. Outro repositório é outro projeto. Projeto sem origem responde `configured: false` — é o estado inicial, e o que a tela mostra como "Configurar origem". Não é 404: o projeto existe. A resposta NUNCA traz credencial, chave de segredo nem nome de Secret. `authenticated` diz apenas se há uma conta conectada. tags: [Projeto] x-zero-lifecycle: live x-zero-scope: project:read x-zero-tool: zero.projects.source.get x-zero-destructive: false parameters: - $ref: '#/components/parameters/ProjectId' responses: '200': description: A origem do projeto, ou a ausência dela. content: application/json: schema: { $ref: '#/components/schemas/ProjectSource' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } put: operationId: setProjectSource x-zero-operator: project.set_source summary: Configura ou troca a origem do projeto. description: | Configurar quando já existe SUBSTITUI: a origem anterior é revogada, e o histórico responde de onde o projeto publicava quando cada versão subiu. Não é preciso desconectar antes. `credential` é o token da conta, e entra cifrado. Ele nunca volta em leitura nenhuma. Vazio mantém a credencial atual quando a origem já existe, e significa "repositório público" quando ela é nova. Endereço com credencial embutida é RECUSADO, não normalizado: quem cola um token num campo que não é de segredo precisa saber que ele não foi guardado. tags: [Projeto] x-zero-lifecycle: live x-zero-scope: project:manage_secrets x-zero-tool: zero.projects.source.set x-zero-destructive: false parameters: - $ref: '#/components/parameters/ProjectId' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ProjectSourceInput' } responses: '200': description: A origem configurada. content: application/json: schema: { $ref: '#/components/schemas/ProjectSource' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } delete: operationId: disconnectProjectSource x-zero-operator: project.disconnect_source summary: Desconecta a origem do projeto. description: | O projeto volta ao estado "sem origem", e a publicação seguinte recusa com `PROJECT_SOURCE_MISSING`. A credencial deixa de resolver na mesma requisição — não há fallback para identidade anterior. tags: [Projeto] x-zero-lifecycle: live x-zero-scope: project:manage_secrets x-zero-tool: zero.projects.source.disconnect x-zero-destructive: true parameters: - $ref: '#/components/parameters/ProjectId' responses: '200': description: | O estado resultante: `configured: false`. Devolver o estado em vez de 204 vazio responde "e agora?" sem uma segunda chamada. content: application/json: schema: { $ref: '#/components/schemas/ProjectSource' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /projects/{projectId}/source/oauth/{provider}/start: post: operationId: startProjectSourceAuthorization x-zero-operator: project.authorize_source summary: Começa a autorização no provedor, para este projeto. description: | Devolve o endereço para onde o navegador vai autorizar. O fluxo inteiro acontece dentro do projeto: o que ele produz é a credencial de UMA origem, nunca uma conexão que a empresa inteira compartilha. O `state` é de uso único e vale dez minutos. Ele carrega o projeto, e o callback NÃO confia nisso: resolve o projeto da própria URL e exige que os dois sejam o mesmo. Provedor que esta instalação não sabe autorizar responde `GIT_OAUTH_NOT_CONFIGURED` — ausência de capacidade da plataforma, e nunca permissão faltando de quem pede. tags: [Projeto] x-zero-lifecycle: live x-zero-scope: project:manage_secrets x-zero-tool: zero.projects.source.authorize x-zero-destructive: false parameters: - $ref: '#/components/parameters/ProjectId' - name: provider in: path required: true schema: { type: string, enum: [github, gitlab, bitbucket] } responses: '201': description: Para onde o navegador deve ir. content: application/json: schema: { $ref: '#/components/schemas/SourceAuthorizationStart' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /projects/{projectId}/source/oauth/callback: post: operationId: completeProjectSourceAuthorization x-zero-mcp-exposed: false # Fora do catálogo de agente DE PROPÓSITO. # # Estas três etapas só existem dentro de um navegador que acabou de voltar # do provedor: o `code` é material de credencial de uso único, e a # autorização pertence à sessão que a começou. Uma ferramenta de agente # aqui só funcionaria se alguém colasse o `code` — ou o `state` — numa # conversa, que é exatamente o que este desenho existe para tornar # desnecessário. # # O que o agente ALCANÇA é `project.authorize_source`: ele devolve o # endereço para a pessoa autorizar, e a conversa nunca carrega segredo. summary: Conclui a autorização e descobre a conta. description: | Troca o código por token FALANDO COM O PROVEDOR e descobre de quem é a credencial. O token nunca chega ao navegador. **Não cria a origem.** A credencial espera numa autorização que expira em trinta minutos, enquanto a pessoa escolhe o repositório. Gravar uma origem aqui daria ao projeto uma origem ativa apontando para lugar nenhum, e quem abandonasse o fluxo deixaria o projeto pior do que encontrou. `state` inexistente, expirado, já usado, de outro projeto ou de outra pessoa respondem a MESMA coisa: distinguir permitiria descobrir qual dos cinco é, e quem usa resolve os cinco começando de novo. tags: [Projeto] x-zero-lifecycle: live x-zero-scope: project:manage_secrets x-zero-destructive: false parameters: - $ref: '#/components/parameters/ProjectId' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/SourceAuthorizationCallback' } responses: '201': description: A identidade descoberta, e o prazo para escolher o repositório. content: application/json: schema: { $ref: '#/components/schemas/SourceAuthorization' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /projects/{projectId}/source/oauth/{authorizationId}: delete: operationId: abandonProjectSourceAuthorization x-zero-mcp-exposed: false # Fora do catálogo de agente DE PROPÓSITO. # # Estas três etapas só existem dentro de um navegador que acabou de voltar # do provedor: o `code` é material de credencial de uso único, e a # autorização pertence à sessão que a começou. Uma ferramenta de agente # aqui só funcionaria se alguém colasse o `code` — ou o `state` — numa # conversa, que é exatamente o que este desenho existe para tornar # desnecessário. # # O que o agente ALCANÇA é `project.authorize_source`: ele devolve o # endereço para a pessoa autorizar, e a conversa nunca carrega segredo. summary: Abandona o fluxo de autorização. description: | Existe para que fechar a tela não deixe uma credencial viva esperando meia hora. A origem atual do projeto NÃO é tocada: abandonar configurar não é desconectar. tags: [Projeto] x-zero-lifecycle: live x-zero-scope: project:manage_secrets x-zero-destructive: false parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/SourceAuthorizationId' responses: '204': { description: A autorização foi descartada. } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/source/oauth/{authorizationId}/repositories: get: operationId: listAuthorizedRepositories x-zero-mcp-exposed: false # Fora do catálogo de agente DE PROPÓSITO. # # Estas três etapas só existem dentro de um navegador que acabou de voltar # do provedor: o `code` é material de credencial de uso único, e a # autorização pertence à sessão que a começou. Uma ferramenta de agente # aqui só funcionaria se alguém colasse o `code` — ou o `state` — numa # conversa, que é exatamente o que este desenho existe para tornar # desnecessário. # # O que o agente ALCANÇA é `project.authorize_source`: ele devolve o # endereço para a pessoa autorizar, e a conversa nunca carrega segredo. summary: Os repositórios que a conta autorizada alcança. description: | A listagem é feita PELO SERVIDOR, com o token que nunca saiu dele. O que volta é só a lista — é por isso que esta rota existe, em vez de devolver o token e deixar a tela consultar o provedor. Uma página, no tamanho máximo que cada provedor aceita, ordenada pelo mais recente onde há ordenação: esta tela escolhe entre os repositórios que a pessoa mexeu por último, e quem tem mais que isso digita o endereço. tags: [Projeto] x-zero-lifecycle: live x-zero-scope: project:manage_secrets x-zero-destructive: false parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/SourceAuthorizationId' responses: '200': description: O que a conta alcança. content: application/json: schema: { $ref: '#/components/schemas/AuthorizedRepositories' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/previews: get: operationId: listProjectPreviews x-zero-operator: project.previews summary: Os previews de pull request do projeto. description: | Dois commits por preview, e os dois importam: `head_sha` é o último que o provedor informou, `running_sha` é o que está no ar. Quando uma atualização falha os dois divergem — e é exatamente aí que esta resposta precisa dizer a verdade inteira, em vez de anunciar o commit novo sobre um runtime que continua no antigo. `running_artifact_id` é o que a promoção usa: promover um preview é mandar esse artefato para outro serviço, sem reconstruir. tags: [Preview] x-zero-lifecycle: live x-zero-scope: project:read x-zero-tool: zero.previews.list x-zero-destructive: false parameters: - $ref: '#/components/parameters/ProjectId' responses: '200': description: Os previews, abertos primeiro. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/PullRequestPreview' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /git-events/{bindingId}: post: operationId: receiveGitEvent summary: Recebe um aviso de pull request do provedor de código. description: | **Não é uma rota de usuário.** Quem chama é o provedor de código, e o endereço é configurado uma vez, no repositório, quando alguém liga previews para ele. ## Como ela é autenticada Por assinatura HMAC-SHA256 sobre o corpo **cru**, com o segredo do vínculo — o mecanismo que GitHub, GitLab e Bitbucket já oferecem. Não há `Bearer` aqui: quem chama é uma máquina do provedor, que não tem identidade no provedor de identidade desta plataforma. Isso é mais estreito que um token, e não menos: um token da organização alcança tudo o que aquela pessoa alcança; este segredo vale para um repositório e para nada além dele. ## Por que o corpo nunca escolhe o destino O identificador do vínculo está na URL, e é ele que resolve organização e projeto — por uma linha que alguém cadastrou antes. O corpo do evento não tem como nomear organização, projeto ou ambiente, e a recusa não depende de alguém lembrar de validar: a estrutura interna não tem esses campos. ## Por que quase tudo responde 200 Um provedor que recebe erro repetidamente **desliga a entrega**. Evento fora do vocabulário, preview desligado para o repositório e reentrega do mesmo evento não são falhas — são o funcionamento normal de um webhook `at-least-once` — e respondem 200 dizendo o que foi feito. Assinatura inválida e vínculo inexistente respondem os dois `404`. Distinguir contaria a quem tenta adivinhar que o identificador estava certo e só faltava o segredo. tags: [Preview] x-zero-lifecycle: live # Quem chama é o provedor de código, autenticado por HMAC sobre o corpo # cru — não há Bearer, e por isso não há escopo a exigir. x-zero-scope: none # Um agente não entrega webhook. A entrega vem assinada com o segredo do # vínculo, que o agente não tem e não deve ter: expor isto como ferramenta # daria a um prompt a chance de forjar um evento de pull request em nome # de um repositório. A exclusão é DECLARADA para a ausência da tool não # parecer esquecimento — é a mesma regra das rotas de credencial. x-zero-mcp-exposed: false x-zero-destructive: false security: [] parameters: - name: bindingId in: path required: true description: O vínculo entre o repositório e o projeto. schema: { type: string, format: uuid } requestBody: required: true description: | O payload do provedor, na forma dele. A plataforma lê o que precisa — número do pull request, commit do HEAD, ação, e se veio de um fork — e ignora o resto. content: application/json: schema: type: object additionalProperties: true responses: '200': description: | O evento foi recebido. `resultado` diz o que a plataforma fez com ele. content: application/json: schema: { $ref: '#/components/schemas/GitEventResult' } '404': description: | Vínculo desconhecido **ou** assinatura inválida. Os dois respondem o mesmo, de propósito. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } '422': description: O corpo não pôde ser lido, ou é grande demais. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } /organizations/{organizationId}/alerts: parameters: - $ref: '#/components/parameters/OrganizationId' get: operationId: listOrganizationAlerts x-zero-lifecycle: live x-zero-operator: organization.alerts summary: O que precisa de atenção na organização. description: | Os alertas são avaliados NA LEITURA, a partir do estado publicado: publicação que falhou, serviço sem réplica pronta, endereço nunca reservado, zona de domínio que parou de servir. São as condições que quem publicou uma aplicação pode acionar. As regras de infraestrutura da plataforma — disco de nó, margem de pool, membro de balanceador — não aparecem aqui: elas são de quem opera, e misturá-las transformaria a tela num painel que o cliente não pode usar. tags: [organizations] x-zero-tool: zero.organizations.alerts.list x-zero-scope: organization:read responses: '200': description: Alertas avaliados agora. content: application/json: schema: { $ref: '#/components/schemas/AlertList' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/alerts: parameters: - $ref: '#/components/parameters/ProjectId' get: operationId: listProjectAlerts x-zero-lifecycle: live x-zero-operator: project.alerts summary: O que precisa de atenção neste projeto. tags: [projects] x-zero-tool: zero.projects.alerts.list x-zero-scope: project:read responses: '200': description: Alertas avaliados agora. content: application/json: schema: { $ref: '#/components/schemas/AlertList' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/security: parameters: - $ref: '#/components/parameters/ProjectId' get: operationId: getProjectSecurity x-zero-operator: project.security x-zero-lifecycle: live summary: Postura de segurança do projeto, derivada de fato. description: | Checklist determinístico, nunca score: um score agrega coisas que não se somam — um certificado vencendo e um segredo exposto viram "83%" — e a primeira coisa que alguém faz com um número é persegui-lo em vez de ler o que ele resume. Todo item carrega a FONTE do que afirma. Há duas classes, e a distinção é o que impede o relatório de virar propaganda: - **medido** — o banco responde sobre este projeto (existe endereço reservado? revisão compilada? segredo declarado como variável comum?); - **estrutural** — a propriedade vale porque a forma insegura é inexprimível: não há campo para pedir container privilegiado, hostPath ou imagem por tag, e o compilador aplica o contrário a toda revisão. O que a instalação não verifica aparece REPROVADO, e não some da lista: verde sem scanner é confiança sem lastro. tags: [projects] x-zero-tool: zero.projects.security.get x-zero-scope: project:read responses: '200': description: Relatório de segurança. content: application/json: schema: { $ref: '#/components/schemas/ProjectSecurity' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } # ──────────────────────────────── projects ────────────────────────────────── # A organização NÃO está no caminho, e isso é decisão de segurança, não # economia de rota: o escopo de tenant é derivado do token e do cabeçalho # `X-Zero-Organization`, nunca do corpo nem do caminho. Uma rota # `/organizations/{id}/projects` convidaria um cliente a acreditar que trocar o # identificador do caminho troca de tenant — e a resposta a essa tentativa seria # 404, depois de o cliente ter escrito código contando com o contrário. /projects: get: operationId: listProjects x-zero-lifecycle: live x-zero-operator: project.list summary: Lista os projetos da organização em escopo. description: | A organização vem do escopo da chamada. A listagem é de página única com teto de 200 projetos — não há cursor, e declarar um seria prometer uma paginação que o servidor não implementa. tags: [projects] x-zero-tool: zero.projects.list x-zero-scope: project:read responses: '200': description: Projetos. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Project' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } post: operationId: createProject x-zero-operator: project.create x-zero-lifecycle: live summary: Cria um projeto. description: | Criar um projeto é síncrono: é apenas registro no control plane. Responde `201` com o projeto, e não `202` com operação — não há trabalho para um worker reivindicar, e uma operação que nenhum worker pega ficaria `queued` para sempre. Por padrão o ambiente `production` nasce na MESMA transação, e vem embutido em `environments` na resposta. Isso evita uma segunda chamada logo depois de criar, e é o que garante que não existe projeto sem lugar onde publicar. tags: [projects] x-zero-tool: zero.projects.create x-zero-scope: project:write parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ProjectCreate' } responses: '201': description: Projeto criado, com os ambientes que nasceram junto. content: application/json: schema: { $ref: '#/components/schemas/Project' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /projects/{projectId}: parameters: - $ref: '#/components/parameters/ProjectId' get: operationId: getProject x-zero-lifecycle: live x-zero-operator: project.get summary: Detalha um projeto. tags: [projects] x-zero-tool: zero.projects.get x-zero-scope: project:read responses: '200': description: Projeto. content: application/json: schema: { $ref: '#/components/schemas/Project' } '404': { $ref: '#/components/responses/NotFound' } delete: operationId: deleteProject x-zero-operator: project.delete x-zero-lifecycle: live summary: Remove um projeto e tudo que existe dentro dele. description: | Assíncrono: responde `202` com a operação `project.delete`, e o projeto passa por `lifecycle_state` `deletion_requested` → `deleting` até sumir. **Enquanto a exclusão corre**, o projeto continua listado e legível, com o estado no campo `lifecycle_state` — é o que deixa a tela dizer "excluindo" em vez de fingir que acabou. Toda mutação nele é recusada com `409` `PROJECT_DELETING`. Repetir o `DELETE` devolve `202` com a MESMA operação; se ela falhou (`deletion_failed`), repetir tenta de novo. **Quando a operação termina**, o projeto deixou de existir para o produto: some das listas, `GET` e toda rota dele respondem `404`, e um novo `DELETE` também. O que era operacional é encerrado na mesma transação que o esconde — ambientes e serviços encerrados, origem revogada e a credencial dela destruída, autorizações em trânsito apagadas, cargas, rotas e namespaces removidos, endereço aposentado. O HISTÓRICO fica: auditoria, operações e publicações passadas continuam registradas, e não voltam a ser executáveis. tags: [projects] x-zero-tool: zero.projects.delete x-zero-destructive: true x-zero-scope: project:delete parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: false content: application/json: schema: { $ref: '#/components/schemas/ProjectDeleteRequest' } responses: '202': { $ref: '#/components/responses/Accepted' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '422': { $ref: '#/components/responses/UnprocessableEntity' } # ────────────────────────────── environments ──────────────────────────────── /projects/{projectId}/environments: parameters: - $ref: '#/components/parameters/ProjectId' # Não há `get` aqui: os ambientes de um projeto vêm embutidos em # `environments` na resposta de `GET /projects/{projectId}`. Uma rota # separada para o mesmo dado seria uma segunda opinião sobre a mesma # pergunta, e a divergente seria a que ninguém exercita. post: operationId: createEnvironment x-zero-operator: environment.create x-zero-lifecycle: live summary: Cria um ambiente. description: | O ambiente é a fronteira de isolamento. A criação provisiona namespace opaco, quota, limites e políticas de rede — nada disso é configurável pelo usuário. tags: [environments] x-zero-tool: zero.environments.create x-zero-scope: environment:write parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/EnvironmentCreate' } responses: '201': description: | Ambiente criado. É registro no control plane, não trabalho para um worker — por isso `201` com o recurso, e não `202` com operação. O estado nasce `provisioning` porque ninguém materializou o namespace ainda, e dizer `ready` aqui seria afirmar o que não aconteceu. content: application/json: schema: { $ref: '#/components/schemas/Environment' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /environments/{environmentId}: parameters: - $ref: '#/components/parameters/EnvironmentId' get: operationId: getEnvironment x-zero-lifecycle: live x-zero-operator: environment.get summary: Detalha um ambiente. tags: [environments] x-zero-tool: zero.environments.get x-zero-scope: environment:read responses: '200': description: Ambiente. content: application/json: schema: { $ref: '#/components/schemas/Environment' } '404': { $ref: '#/components/responses/NotFound' } /environments/{environmentId}/route: parameters: - $ref: '#/components/parameters/EnvironmentId' get: operationId: getEnvironmentRoute x-zero-operator: environment.route x-zero-lifecycle: live summary: O mundo alcança o que está publicado neste ambiente? description: | Três partes, separadas de propósito. O **endereço** vem da reserva em `platform_addresses`, nunca é composto pelo cliente: o certificado disponível cobre um nível de subdomínio, e um host montado em código não resolveria. Ambiente sem serviço `web` devolve `null`. O **estado** é o que a plataforma sabe. A **evidência** é até onde ela sabe. `state` só é `active` com `verification_level` igual a `public_edge` — o único degrau que significa "uma requisição de fora chegou". Enquanto ele não for alcançado, a rota é `pending_edge`, e anunciá-la como ativa seria prometer o que ninguém mediu. tags: [environments] x-zero-tool: zero.environments.route x-zero-scope: environment:read responses: '200': description: Estado da rota pública do ambiente. content: application/json: schema: { $ref: '#/components/schemas/EnvironmentRoute' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } # ──────────────────────────────── services ────────────────────────────────── /services: get: operationId: listServices x-zero-lifecycle: live x-zero-operator: service.list summary: Lista os serviços da organização em escopo. description: | Sem filtro, devolve os serviços de todos os ambientes da organização. Com `environment_id`, só os daquele ambiente. Um `environment_id` ilegível ou de outra organização devolve `200` com lista vazia, e não `422`: distinguir os dois casos seria uma forma barata de descobrir quais identificadores existem. tags: [services] x-zero-tool: zero.services.list x-zero-scope: service:read parameters: - name: environment_id in: query required: false schema: { type: string } description: Recorta a listagem por ambiente. responses: '200': description: Serviços. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Service' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } post: operationId: createService x-zero-operator: service.create x-zero-lifecycle: live summary: Cria um serviço. description: | Um serviço é `web`, `private_service`, `worker` ou `scheduled_job`. Tudo que não for declarado recebe default administrado pela plataforma: porta, health check, réplicas, limites, política de reinício, rollout, isolamento, logs e métricas. Responde `201` com o serviço, e não `202`: criar um serviço grava INTENÇÃO (`desired_spec`), não publica nada. A publicação é `POST /services/{serviceId}/deployments`, e é ela que responde `202`. Um serviço `web` tem o endereço público RESERVADO nesta mesma transação. Um endereço já tomado é recusado aqui — e não vinte minutos depois, como falha de publicação. tags: [services] x-zero-tool: zero.services.create x-zero-scope: service:write parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ServiceCreate' } responses: '201': description: Serviço criado, com a configuração declarada. content: application/json: schema: { $ref: '#/components/schemas/Service' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /services/{serviceId}: parameters: - $ref: '#/components/parameters/ServiceId' get: operationId: getService x-zero-lifecycle: live x-zero-operator: service.get summary: Detalha um serviço. tags: [services] x-zero-tool: zero.services.get x-zero-scope: service:read responses: '200': description: Serviço. content: application/json: schema: { $ref: '#/components/schemas/Service' } '404': { $ref: '#/components/responses/NotFound' } patch: operationId: updateService x-zero-lifecycle: live x-zero-operator: service.update summary: Atualiza a configuração do serviço. description: | Alterar configuração **não** altera a release ativa: cria uma nova revision, que precisa de um deployment para entrar em tráfego. tags: [services] x-zero-tool: zero.services.update x-zero-scope: service:write requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ServiceUpdate' } responses: '200': description: Serviço atualizado (nova revision pendente). content: application/json: schema: { $ref: '#/components/schemas/Service' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /services/{serviceId}/scale: parameters: - $ref: '#/components/parameters/ServiceId' post: operationId: scaleService x-zero-lifecycle: live x-zero-operator: service.scale summary: Ajusta tamanho e quantidade de instâncias. description: | Expresso em vocabulário de produto (`size` do catálogo e faixa de instâncias), nunca em CPU/memória crus nem em objetos de autoscaling. tags: [services] x-zero-tool: zero.services.scale x-zero-scope: service:write requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ScaleRequest' } responses: '202': { $ref: '#/components/responses/Accepted' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /services/{serviceId}/source/validate: parameters: - $ref: '#/components/parameters/ServiceId' post: operationId: validateServiceSource x-zero-operator: service.validate_source x-zero-lifecycle: live summary: Confere a origem do código sem publicar nada. description: | Existe para que "o endereço está errado" e "a conta não está conectada" apareçam enquanto a pessoa configura, e não vinte minutos depois no meio de uma publicação. A conferência é a primeira conversa com o provedor e para aí: nada é clonado, nada é construído, nenhuma release nasce. Confere a ORIGEM DO PROJETO — o repositório é dele, não do serviço. Sem corpo, confere a referência da origem e a pasta que o serviço declarou; o corpo só pode AJUSTAR a referência (`branch`) e a pasta (`subdirectory`). `repository` ou `image` no corpo são recusados com `422` `VALIDATION_FAILED` no campo `source.repository`: para conferir outro repositório, configure a origem do projeto, ou crie outro projeto. Exige escopo de **escrita**, e não de leitura: a chamada sai da plataforma com a credencial DA ORIGEM DO PROJETO e consome cota do provedor. Quem só lê não dispara requisição em nome de ninguém. `subdirectory_verified` é sempre `false` hoje: provar que o subdiretório existe exigiria ler a árvore do repositório, e ler a árvore é publicar. tags: [services] x-zero-tool: zero.services.source.validate x-zero-scope: service:write requestBody: required: false content: application/json: schema: { $ref: '#/components/schemas/SourceValidateRequest' } responses: '200': description: A origem é alcançável, e isto é o que foi resolvido. content: application/json: schema: { $ref: '#/components/schemas/SourceCheck' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } # ──────────────────────────── análise de aplicação ────────────────────────── # ───────────────────────────── builds e artifacts ─────────────────────────── # ─────────────────────────────── deployments ──────────────────────────────── /services/{serviceId}/deployments: parameters: - $ref: '#/components/parameters/ServiceId' get: operationId: listDeployments x-zero-lifecycle: live x-zero-operator: deployment.list summary: Histórico de publicações do serviço. tags: [deployments] x-zero-tool: zero.deployments.list x-zero-scope: service:read parameters: - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: Deployments. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Deployment' } next_cursor: { type: [string, 'null'] } post: operationId: createDeployment x-zero-lifecycle: live x-zero-operator: deployment.create summary: Publica o serviço — a partir do código, ou promovendo um artefato. description: | **Duas formas, e elas se excluem.** `source` publica a partir do CÓDIGO DA ORIGEM DO PROJETO: resolve a referência, constrói, verifica e sobe. Produz um artefato novo. O repositório NÃO viaja aqui — ele é da origem do projeto (`PUT /projects/{projectId}/source`). O corpo só ajusta a referência (`branch`) e a pasta (`subdirectory`); `repository` ou `image` são recusados com `422` `VALIDATION_FAILED` no campo `source.repository`. `artifact_id` **promove** um artefato que já existe, sem reconstruir nada. É a garantia central do produto: > build once, verify once, promote many A promoção não resolve origem, não chama o construtor e não recalcula digest. O que sobe em produção são os MESMOS bytes que o preview executou — não "o mesmo commit reconstruído", que produziria outro digest. Os dois juntos são recusados com `422`: a ambiguidade cairia no pior lado, porque quem manda os dois quase sempre acha que está pedindo "reconstrua a partir daqui e promova". Sem nenhum dos dois — corpo `{}` —, publica a origem do projeto na referência dela e na pasta que o serviço declarou. ## O que a promoção NÃO carrega Configuração e secrets são do AMBIENTE, nunca do artefato. A mesma imagem sobe em `development` com a escala e as variáveis de `development`, e em `production` com as de `production`. Promover não copia nada disso — e é isso que permite promover sem revisar a configuração de destino. ## Como acompanhar Responde `202` com uma operação, como qualquer publicação. Os eventos são os de sempre (`GET /deployments/{deploymentId}/events/stream`): não há uma segunda infraestrutura de progresso para promoção. tags: [deployments] x-zero-tool: zero.deployments.create x-zero-scope: deployment:write parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/DeploymentCreate' } responses: '202': { $ref: '#/components/responses/Accepted' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': description: | O serviço não existe — ou o `artifact_id` não existe, ou é de outra organização. As três respondem igual: distinguir permitiria enumerar artefatos alheios um identificador de cada vez. content: application/json: schema: { $ref: '#/components/schemas/Problem' } '409': { $ref: '#/components/responses/Conflict' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /deployments/{deploymentId}: parameters: - $ref: '#/components/parameters/DeploymentId' get: operationId: getDeployment x-zero-lifecycle: live x-zero-operator: deployment.get summary: Detalha uma publicação. tags: [deployments] x-zero-tool: zero.deployments.get x-zero-scope: service:read responses: '200': description: Deployment. content: application/json: schema: { $ref: '#/components/schemas/Deployment' } '404': { $ref: '#/components/responses/NotFound' } /deployments/{deploymentId}/logs: parameters: - $ref: '#/components/parameters/DeploymentId' get: operationId: listDeploymentLogs x-zero-lifecycle: live x-zero-operator: deployment.logs summary: A narrativa durável da publicação, paginada por cursor. description: | São os eventos da OPERAÇÃO que conduziu a publicação — `source` é sempre `operation`. Log do construtor é `GET /deployments/{deploymentId}/build-logs`; saída da aplicação é `GET /projects/{projectId}/runtime-logs`. Misturá-los aqui faria alguém procurar uma exceção da aplicação num lugar onde só existe narrativa da plataforma, e concluir que a exceção não aconteceu. O cursor é a SEQUÊNCIA do último evento lido, monotônica e estável dentro da operação: uma leitura incremental nunca pula nem repete linha. Um cursor por instante não garantiria isso, porque dois eventos podem cair no mesmo microssegundo. `next_cursor` só vem quando a página encheu. Publicação que ainda não tem operação devolve lista vazia — que é diferente de `404`. tags: [deployments, observability] x-zero-tool: zero.deployments.logs x-zero-scope: service:read parameters: - name: cursor in: query schema: { type: string } description: O `next_cursor` da página anterior. - name: limit in: query schema: { type: integer, default: 200, minimum: 1, maximum: 1000 } description: | Acima do teto, o teto vale — e não é erro: o limite é da plataforma, não engano de quem chamou. responses: '200': description: Página de eventos da publicação. content: application/json: schema: { $ref: '#/components/schemas/DeploymentLogPage' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /services/{serviceId}/rollback: parameters: - $ref: '#/components/parameters/ServiceId' post: operationId: rollbackService x-zero-lifecycle: live x-zero-operator: service.rollback summary: Volta o serviço para uma release anterior. description: | Operação de primeira classe. O histórico nunca é destruído: o rollback cria uma release NOVA apontando para a revisão restaurada. Só uma release com `rollback_eligible: true` é aceita como destino, e o servidor é quem decide (ver `Release.rollback_eligible`). Com `release_id`, uma release não elegível é recusada com `RELEASE_NOT_ROLLBACKABLE` e o motivo. Sem `release_id`, o destino é a release elegível mais recente; se não houver nenhuma, a mesma recusa. Antes de tocar o cluster, o artefato da revisão restaurada é reaberto no registry pelo digest. Se os bytes não existirem mais, a operação falha e o serviço continua como estava. tags: [deployments, releases] x-zero-tool: zero.deployments.rollback x-zero-destructive: true x-zero-scope: deployment:write parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: content: application/json: schema: type: object properties: release_id: { type: string } reason: { type: string, maxLength: 500, description: "Texto livre, para quem for ler o histórico. Ele NÃO escolhe o que o motor faz." } responses: '202': { $ref: '#/components/responses/Accepted' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /services/{serviceId}/releases: parameters: - $ref: '#/components/parameters/ServiceId' get: operationId: listReleases x-zero-lifecycle: live x-zero-operator: release.list summary: Releases do serviço (histórico imutável). tags: [releases] x-zero-tool: zero.releases.list x-zero-scope: service:read responses: '200': description: Releases. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Release' } /services/{serviceId}/security/scan: parameters: - $ref: '#/components/parameters/ServiceId' get: operationId: getServiceScan x-zero-lifecycle: live x-zero-operator: security.scan.get summary: Análise de vulnerabilidades da publicação que está no ar. description: | Responde sobre o DIGEST que está executando, e nunca sobre uma tag. A cadeia é publicação ativa → digest imutável → análise mais recente daquele digest. Três campos respondem perguntas diferentes e não se substituem: - `status` diz o que aconteceu com a ANÁLISE. `succeeded` é a única que concluiu; as demais (`failed`, `timed_out`, `registry_unavailable`, `scanner_error`, `invalid_report`) dizem que não se conseguiu olhar. - as contagens dizem o que foi encontrado. Elas só têm sentido quando `status` é `succeeded`: numa análise que falhou são todas zero, e zero ali NÃO significa imagem sem vulnerabilidade. - `decision` é o que a política do Zero concluiu. `scan_failed` é deliberadamente diferente de `allow`: "não sei" e "sei que não há" não são a mesma resposta. `scanned: false` significa que ninguém analisou este digest — não que ele esteja limpo. tags: [security] x-zero-tool: zero.security.scan x-zero-scope: service:read responses: '200': description: A análise da publicação ativa. content: application/json: schema: { $ref: '#/components/schemas/SecurityScan' } /services/{serviceId}/security/findings: parameters: - $ref: '#/components/parameters/ServiceId' get: operationId: listServiceFindings x-zero-lifecycle: live x-zero-operator: security.finding.list summary: Vulnerabilidades encontradas na publicação que está no ar. description: | A lista da análise mais recente que CONCLUIU sobre o digest ativo. Ordenada por gravidade — crítica primeiro — e paginada por cursor, que anda nessa mesma ordem. Lista vazia não é imagem limpa: ela também é a resposta quando nenhuma análise concluiu. Quem precisa distinguir lê `status` em `/services/{serviceId}/security/scan`. tags: [security] x-zero-tool: zero.security.findings.list x-zero-scope: service:read parameters: - name: severity in: query description: Filtra por severidade. schema: type: string enum: [critical, high, medium, low, unknown] - name: cursor in: query description: Marcador devolvido em `next_cursor` pela página anterior. schema: { type: string } - name: limit in: query schema: { type: integer, minimum: 1, maximum: 200, default: 50 } responses: '200': description: Uma página de vulnerabilidades. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/SecurityFinding' } scan_id: type: string description: A análise de onde estes achados vieram. next_cursor: type: string description: Ausente na última página. /releases/{releaseId}: parameters: - name: releaseId in: path required: true schema: { type: string } description: Identificador público da release (`rel_…`). get: operationId: getRelease x-zero-lifecycle: live x-zero-operator: release.get summary: Detalha uma ativação do histórico. description: | O histórico é append-only: o que esta rota devolve nunca muda depois de gravado, nem quando a release deixa de ser a ativa. É por isso que ela pode ser endereçada por identificador próprio — uma release é um FATO, não um estado. tags: [releases] x-zero-tool: zero.releases.get x-zero-scope: service:read responses: '200': description: Release. content: application/json: schema: { $ref: '#/components/schemas/Release' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } # ─────────────────────────────── datastores ───────────────────────────────── # ──────────────────────────────── secrets ─────────────────────────────────── /environments/{environmentId}/secrets: parameters: - $ref: '#/components/parameters/EnvironmentId' get: operationId: listSecrets x-zero-operator: environment.secrets x-zero-lifecycle: live summary: Lista os secrets (apenas metadados — valores nunca são devolvidos). tags: [secrets] x-zero-tool: zero.secrets.list x-zero-scope: secret:read responses: '200': description: Metadados de secrets. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/SecretMetadata' } # ──────────────────────────────── backups ─────────────────────────────────── # ─────────────────────────────── operations ───────────────────────────────── /operations/{operationId}: parameters: - $ref: '#/components/parameters/OperationId' get: operationId: getOperation x-zero-lifecycle: live x-zero-operator: operation.get summary: Estado de uma operação durável. tags: [operations] x-zero-tool: zero.operations.get x-zero-scope: operation:read responses: '200': description: Operação. content: application/json: schema: { $ref: '#/components/schemas/Operation' } '404': { $ref: '#/components/responses/NotFound' } /operations/{operationId}/events: parameters: - $ref: '#/components/parameters/OperationId' get: operationId: listOperationEvents x-zero-lifecycle: live x-zero-operator: operation.events summary: Progresso da operação, em página única. description: | NÃO é um fluxo. O contrato declarava `text/event-stream` aqui e o servidor nunca serviu isso — um cliente gerado a partir daquela declaração abriria um `EventSource` que recebe JSON e nunca dispara evento nenhum. Quem quer acompanhar ao vivo usa `GET /deployments/{deploymentId}/events/stream`, que é SSE de verdade, com retomada por `Last-Event-ID`. Devolve no máximo 200 eventos por chamada, a partir de `after_sequence`. tags: [operations] x-zero-tool: zero.operations.events x-zero-scope: operation:read parameters: - name: after_sequence in: query schema: { type: integer, format: int64 } description: A `sequence` do último evento já lido. responses: '200': description: Eventos. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/OperationEvent' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /operations/{operationId}/cancel: parameters: - $ref: '#/components/parameters/OperationId' post: operationId: cancelOperation x-zero-lifecycle: live x-zero-operator: operation.cancel summary: Pede cancelamento cooperativo. tags: [operations] x-zero-tool: zero.operations.cancel x-zero-destructive: true x-zero-scope: operation:write responses: '202': { $ref: '#/components/responses/Accepted' } '409': { $ref: '#/components/responses/Conflict' } # ────────────────────────────── diagnóstico ───────────────────────────────── # ─────────────────────────────────── uso ──────────────────────────────────── /organizations/{organizationId}/usage: parameters: - $ref: '#/components/parameters/OrganizationId' get: operationId: getUsage x-zero-operator: organization.usage x-zero-lifecycle: live summary: Consumo agregado da organização em uma janela. tags: [usage] x-zero-tool: zero.usage.get x-zero-scope: usage:read parameters: - name: from in: query required: true schema: { type: string, format: date-time } - name: to in: query required: true schema: { type: string, format: date-time } - name: group_by in: query schema: { type: string, enum: [project, environment, service, metric], default: metric } responses: '200': description: Uso agregado. content: application/json: schema: { $ref: '#/components/schemas/UsageSummary' } # ───────────────────────── projeções orientadas à tela ────────────────────── # # Elas são LEITURA sobre o mesmo domínio, com o mesmo escopo de tenant. Existem # porque a alternativa medida é o cliente remontar o estado do sistema com uma # requisição por cartão — foi assim que uma tela de projeto passou a fazer duas # por ambiente e a de um serviço, cinco em três níveis, com polling por cima. # # Vivem sob o mesmo prefixo, e não sob um `/console` à parte: um namespace por # consumidor viraria uma API paralela, e a CLI e as tools de agente precisam # exatamente das mesmas respostas. # # Nenhum campo destas respostas devolve zero para dado desconhecido. Onde a # plataforma não pode calcular honestamente, o valor é `null` — a diferença # entre "nenhum deployment falhou" e "não sabemos quantos falharam" é a # diferença entre um painel que se pode usar para operar e um que não. /organizations/{organizationId}/overview: parameters: - $ref: '#/components/parameters/OrganizationId' get: operationId: getOrganizationOverview x-zero-lifecycle: live x-zero-operator: organization.overview summary: Retrato da organização para a tela inicial. description: | Contagem de projetos por estado, deployments da janela com taxa de sucesso e duração mediana, série diária para o gráfico, últimos deployments, atividade recente e o checklist de configuração — tudo derivado de estado real. `success_rate` e `median_duration_seconds` são `null` quando não houve deployment terminal na janela. Zero por cento afirmaria que tudo falhou. tags: [organizations] x-zero-tool: zero.organizations.overview x-zero-scope: organization:read parameters: - name: window in: query description: Janela em dias (1 a 90). Fora da faixa, o padrão de 30 vale. schema: { type: integer, minimum: 1, maximum: 90, default: 30 } responses: '200': description: Retrato da organização. content: application/json: schema: { $ref: '#/components/schemas/OrganizationOverview' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /organizations/{organizationId}/projects/overview: parameters: - $ref: '#/components/parameters/OrganizationId' get: operationId: listProjectsOverview x-zero-operator: organization.projects_overview x-zero-lifecycle: live summary: Projetos com o cartão inteiro. description: | Cada item traz estado, endereço, contagem de serviços e o último deployment. `url` só vem preenchida quando a evidência alcançou `public_edge`: antes disso o endereço existe e ainda não responde, e oferecer o link seria convidar para uma página que não abre. tags: [projects] x-zero-tool: zero.projects.overview x-zero-scope: project:read responses: '200': description: Projetos da organização. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/ProjectCard' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /organizations/{organizationId}/deployments: parameters: - $ref: '#/components/parameters/OrganizationId' get: operationId: listOrganizationDeployments x-zero-operator: organization.deployments x-zero-lifecycle: live summary: Deployments da organização, do mais recente ao mais antigo. tags: [deployments] x-zero-tool: zero.organizations.deployments.list x-zero-scope: deployment:read responses: '200': description: Deployments. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/DeploymentListItem' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /organizations/{organizationId}/activity: parameters: - $ref: '#/components/parameters/OrganizationId' get: operationId: listOrganizationActivity x-zero-lifecycle: live x-zero-operator: organization.activity summary: Atividade da organização. description: | Derivada das OPERAÇÕES, e só delas. `audit_events` tem esquema e nenhum escritor, então um feed que prometesse mudanças de configuração mostraria uma seção permanentemente vazia. O que existe é publicação, reversão e cancelamento. tags: [organizations] x-zero-tool: zero.organizations.activity x-zero-scope: organization:read parameters: - name: project in: query schema: { type: string } - name: kind in: query schema: { type: string } responses: '200': description: Atividade. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/ActivityItem' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/overview: parameters: - $ref: '#/components/parameters/ProjectId' get: operationId: getProjectOverview x-zero-lifecycle: live x-zero-operator: project.overview summary: Retrato do projeto. description: | Estado agregado pelo PIOR estado entre os ambientes, production com o que está no ar, ambientes com seus serviços, histórico recente e as capacidades efetivamente disponíveis. Nada aspiracional entra em `capabilities`: um painel que liste o que a plataforma um dia terá é publicidade, não estado. tags: [projects] x-zero-tool: zero.projects.overview.get x-zero-scope: project:read responses: '200': description: Retrato do projeto. content: application/json: schema: { $ref: '#/components/schemas/ProjectOverview' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/deployments: parameters: - $ref: '#/components/parameters/ProjectId' get: operationId: listProjectDeployments x-zero-lifecycle: live x-zero-operator: project.deployments summary: Deployments do projeto. tags: [deployments] x-zero-tool: zero.projects.deployments x-zero-scope: deployment:read responses: '200': description: Deployments do projeto. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/DeploymentListItem' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/domains: parameters: - $ref: '#/components/parameters/ProjectId' get: operationId: listProjectDomains x-zero-operator: project.domains x-zero-lifecycle: live summary: Endereços públicos do projeto. description: | Superfície de leitura sobre as reservas de `platform_addresses` (ADR-021). `tls` e `routing` não são sondagens: são o que a publicação PROVOU, e só são verdadeiros com evidência `public_edge` — o único nível alcançado por uma requisição que entrou pela borda. tags: [domains] x-zero-tool: zero.projects.domains x-zero-scope: domain:read responses: '200': description: Endereços do projeto. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/ProjectDomain' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /deployments/{deploymentId}/timeline: parameters: - $ref: '#/components/parameters/DeploymentId' get: operationId: getDeploymentTimeline x-zero-lifecycle: live x-zero-operator: deployment.timeline summary: Linha do tempo de um deployment. description: | Os NOVE estágios reais do motor, na ordem dele, com carimbo, duração desde o anterior, a frase pública emitida e os fatos técnicos autorizados. Não existe estágio sem evento correspondente: "detectando framework" e "atribuindo domínio" não fazem parte do pipeline e não aparecem aqui. Num rollback, os estágios que não acontecem vêm como `skipped` — deixá-los pendentes faria a tela parecer travada num trabalho que nunca vai começar. tags: [deployments] x-zero-tool: zero.deployments.timeline x-zero-scope: deployment:read responses: '200': description: Linha do tempo. content: application/json: schema: { $ref: '#/components/schemas/DeploymentTimeline' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /organizations/{organizationId}/capabilities: parameters: - $ref: '#/components/parameters/OrganizationId' get: operationId: getOrganizationCapabilities x-zero-operator: organization.capabilities x-zero-lifecycle: live summary: O que esta instalação sabe fazer. description: | O console consulta uma vez por organização e usa a resposta para dizer a verdade nas telas cuja capacidade não existe — em vez de bater numa rota ausente e mostrar "não encontrado", que é o que 404 significaria ali. Os estados são três, e a distinção decide o que a pessoa faz a seguir: `pronta` (há dado, ou haverá assim que houver tráfego), `requer_configuracao` (a capacidade existe e falta ligá-la) e `indisponivel` (não existe nesta instalação; não há ação). Quase tudo é DERIVADO de fatos consultáveis — existe amostra? existe endereço reservado? —, e por isso a resposta muda sozinha quando a instalação muda. A exceção é `indisponivel`: quando não há produtor no binário, nenhum fato no banco poderia provar o contrário. tags: [organizations] x-zero-tool: zero.organizations.capabilities x-zero-scope: organization:read responses: '200': description: Capacidades desta instalação para esta organização. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Capability' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } # ───────────────────────── observabilidade (ADR-022) ──────────────────────── # # As três rotas leem a MESMA medição — o access log do Envoy agregado em # janelas de um minuto — em três recortes. Nenhuma delas fala com Prometheus, # com Loki ou com o kube-apiserver: o navegador não alcança nenhum dos três, e # a agregação já aconteceu na ingestão. # # `null` significa "não medido" em toda esta superfície, e 0 significa # "medido, e deu zero". Um cliente que trocar um pelo outro vai afirmar que um # serviço sem tráfego tem 0% de erro. /projects/{projectId}/observability/summary: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/ObservabilityWindow' - $ref: '#/components/parameters/ObservabilityFrom' - $ref: '#/components/parameters/ObservabilityTo' - $ref: '#/components/parameters/ObservabilityEnvironment' - $ref: '#/components/parameters/ObservabilityService' get: operationId: getObservabilitySummary x-zero-lifecycle: live x-zero-operator: observability.summary summary: Painel de tráfego e consumo do projeto. description: | Os cartões de abertura mais uma série. Quando `freshness.collecting` é falso, TODOS os cartões de requisição vêm nulos — inclusive os que seriam zero: a diferença entre "não passou requisição" e "nunca medimos aqui" é o que decide a frase que a tela mostra. `granularity_seconds` é escolhido pelo servidor a partir da largura da janela e vem na resposta: duas janelas com o mesmo desenho e resoluções diferentes contam histórias diferentes. tags: [observability] x-zero-tool: zero.observability.summary x-zero-scope: observability:read responses: '200': description: Painel do período. content: application/json: schema: { $ref: '#/components/schemas/ObservabilitySummary' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/observability/requests: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/ObservabilityWindow' - $ref: '#/components/parameters/ObservabilityFrom' - $ref: '#/components/parameters/ObservabilityTo' - $ref: '#/components/parameters/ObservabilityEnvironment' - $ref: '#/components/parameters/ObservabilityService' - name: route in: query required: false schema: { type: string } description: | Recorta por uma rota JÁ NORMALIZADA — o nome da `HTTPRoute`, ou `outras`. Não aceita URL: a URL nunca virou dimensão. - name: status_class in: query required: false schema: { type: string, enum: ['1xx', '2xx', '3xx', '4xx', '5xx', none] } get: operationId: getObservabilityRequests x-zero-lifecycle: live x-zero-operator: observability.requests summary: Tráfego do projeto no período. description: | Série, tabela de rotas e distribuição por classe de status. `truncated` soma duas verdades: a ingestão passou do teto de rotas distintas (e a linha `outras` existe), ou a tabela foi cortada na leitura. As duas levam à mesma frase — "esta lista não é tudo" — e separá-las pediria duas frases que dizem o mesmo. tags: [observability] x-zero-tool: zero.observability.requests x-zero-scope: observability:read responses: '200': description: Tráfego do período. content: application/json: schema: { $ref: '#/components/schemas/ObservedRequests' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/observability/runtime: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/ObservabilityWindow' - $ref: '#/components/parameters/ObservabilityFrom' - $ref: '#/components/parameters/ObservabilityTo' - $ref: '#/components/parameters/ObservabilityEnvironment' - $ref: '#/components/parameters/ObservabilityService' get: operationId: getObservabilityRuntime x-zero-lifecycle: live x-zero-operator: observability.runtime summary: Consumo do projeto no período. description: | CPU, memória e réplicas ao longo do tempo. Todo valor da série é nulável: uma janela sem amostra não teve consumo zero, teve consumo desconhecido — e o `metrics-server`, que é a origem, não tem histórico. tags: [observability] x-zero-tool: zero.observability.runtime x-zero-scope: observability:read responses: '200': description: Consumo do período. content: application/json: schema: { $ref: '#/components/schemas/ObservedRuntime' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } # ──────────────────────────────── logs ────────────────────────────────────── /deployments/{deploymentId}/build-logs: parameters: - $ref: '#/components/parameters/DeploymentId' - name: cursor in: query required: false schema: { type: string } description: | A `sequence` da última linha já recebida. A ordem é total dentro da publicação porque `sequence` é densa — `at` empata quando o compilador escreve centenas de linhas por segundo. - name: limit in: query required: false schema: { type: integer, minimum: 1, maximum: 1000, default: 200 } get: operationId: getBuildLogs x-zero-lifecycle: live x-zero-operator: deployment.build_logs summary: Saída do build de uma publicação. description: | A saída do compilador, do gerenciador de pacotes e do linker. Não confundir com a linha do tempo da operação (`/deployments/{id}/timeline`), que narra o que a PLATAFORMA fez. As mensagens chegam aqui já redigidas: a redaction acontece na escrita, e o que nunca foi gravado não vaza por este endpoint nem por nenhum outro. tags: [logs] x-zero-tool: zero.deployments.buildLogs x-zero-scope: deployment:read responses: '200': description: Página de log de build. content: application/json: schema: { $ref: '#/components/schemas/BuildLogPage' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/runtime-logs: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/ObservabilityFrom' - $ref: '#/components/parameters/ObservabilityTo' - $ref: '#/components/parameters/ObservabilityService' - name: level in: query required: false schema: { type: string, enum: [debug, info, warning, error] } - name: q in: query required: false schema: { type: string, maxLength: 200 } description: Busca por substring na mensagem já redigida. - name: cursor in: query required: false schema: { type: string } - name: limit in: query required: false schema: { type: integer, minimum: 1, maximum: 1000, default: 200 } get: operationId: getRuntimeLogs x-zero-lifecycle: live x-zero-operator: service.runtime_logs summary: Saída da aplicação em execução. description: | O que os processos do projeto escreveram em `stdout` e `stderr`, atravessando os serviços de todos os ambientes. Não há filtro por réplica: o identificador da instância é opaco e instável, e um filtro por ele produziria uma consulta que para de casar na próxima publicação. tags: [logs] x-zero-tool: zero.projects.runtimeLogs x-zero-scope: observability:read responses: '200': description: Página de log de aplicação. content: application/json: schema: { $ref: '#/components/schemas/RuntimeLogPage' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /deployments/{deploymentId}/build-logs/stream: parameters: - $ref: '#/components/parameters/DeploymentId' get: operationId: streamBuildLogs x-zero-lifecycle: live x-zero-tool: zero.deployments.buildLogs.stream summary: Acompanha o build em andamento (SSE). description: | `text/event-stream`. NÃO substitui a rota paginada: quem abre a tela carrega a página e só então acompanha o que vem depois. Um fluxo que tivesse de entregar o histórico seria uma paginação disfarçada, sem cursor e sem retomada. Retomada por `Last-Event-ID`, que carrega a `sequence` da última linha entregue. O fluxo termina sozinho quando a publicação chega a um estado terminal — um fluxo que fica aberto para sempre depois do build acabar é uma conexão que o servidor paga e ninguém lê. tags: [logs] x-zero-scope: deployment:read responses: '200': description: Fluxo de linhas de build. content: text/event-stream: schema: { type: string } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '429': { $ref: '#/components/responses/TooManyRequests' } /deployments/{deploymentId}/events/stream: parameters: - $ref: '#/components/parameters/DeploymentId' get: operationId: streamDeploymentEvents x-zero-lifecycle: live x-zero-tool: zero.deployments.events.stream summary: Acompanha os checkpoints da publicação (SSE). description: | `text/event-stream` sobre `operation_events` — os checkpoints reais do motor (`source_resolved`, `artifact_verified`, `revision_activated`, …). É a narrativa da OPERAÇÃO, e não a saída do compilador. tags: [deployments] x-zero-scope: deployment:read responses: '200': description: Fluxo de checkpoints. content: text/event-stream: schema: { type: string } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '429': { $ref: '#/components/responses/TooManyRequests' } # ───────────────────── configuração: variáveis e secrets ──────────────────── # # As mesmas seis operações existem nos três escopos — organização, projeto e # ambiente — com a MESMA forma. A herança é resolvida na leitura efetiva, e # não na escrita: declarar em dois escopos é legítimo, e é o mais específico # que vence. # # A chave vai no CAMINHO porque dentro de um escopo ela é o identificador do # recurso. O corpo carrega só o valor. /organizations/{organizationId}/variables: parameters: - $ref: '#/components/parameters/OrganizationId' get: operationId: listOrganizationVariables x-zero-operator: organization.variables x-zero-lifecycle: live summary: Variáveis declaradas na organização. tags: [configuration] x-zero-tool: zero.organizations.variables.list x-zero-scope: config:read responses: '200': { $ref: '#/components/responses/VariableList' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /organizations/{organizationId}/variables/{key}: parameters: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/ConfigKey' put: operationId: putOrganizationVariable x-zero-operator: organization.set_variable x-zero-lifecycle: live summary: Declara ou substitui uma variável da organização. tags: [configuration] x-zero-tool: zero.organizations.variables.put x-zero-scope: config:write requestBody: { $ref: '#/components/requestBodies/ConfigValue' } responses: '200': { $ref: '#/components/responses/Variable' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } delete: operationId: deleteOrganizationVariable x-zero-operator: organization.delete_variable x-zero-lifecycle: live summary: Remove a declaração da variável neste escopo. description: | Remove a DECLARAÇÃO deste escopo, e não necessariamente a variável: se houver declaração num escopo mais amplo, ela volta a valer. tags: [configuration] x-zero-tool: zero.organizations.variables.delete x-zero-scope: config:write responses: '204': { description: Declaração removida. } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /organizations/{organizationId}/secrets: parameters: - $ref: '#/components/parameters/OrganizationId' get: operationId: listOrganizationSecrets x-zero-operator: organization.secrets x-zero-lifecycle: live summary: Secrets declarados na organização (apenas metadados). tags: [configuration] x-zero-tool: zero.organizations.secrets.list x-zero-scope: secret:read responses: '200': { $ref: '#/components/responses/SecretList' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /organizations/{organizationId}/secrets/{key}: parameters: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/ConfigKey' put: operationId: putOrganizationSecret x-zero-lifecycle: live summary: Grava o valor de um secret da organização. description: | Gravar um secret é SUBSTITUIR. Não existe "editar": o valor anterior não é legível nem por quem administra, então não há o que editar — há um valor novo que passa a valer, e a versão sobe. tags: [configuration] x-zero-tool: zero.organizations.secrets.put x-zero-scope: secret:write requestBody: { $ref: '#/components/requestBodies/ConfigValue' } responses: '200': { $ref: '#/components/responses/Secret' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } delete: operationId: deleteOrganizationSecret x-zero-operator: organization.delete_secret x-zero-lifecycle: live summary: Remove a declaração do secret neste escopo. tags: [configuration] x-zero-tool: zero.organizations.secrets.delete x-zero-scope: secret:write responses: '204': { description: Declaração removida. } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/variables: parameters: - $ref: '#/components/parameters/ProjectId' get: operationId: listProjectVariables x-zero-operator: project.variables x-zero-lifecycle: live summary: Variáveis declaradas no projeto. tags: [configuration] x-zero-tool: zero.projects.variables.list x-zero-scope: config:read responses: '200': { $ref: '#/components/responses/VariableList' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/variables/{key}: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/ConfigKey' put: operationId: putProjectVariable x-zero-operator: project.set_variable x-zero-lifecycle: live summary: Declara ou substitui uma variável do projeto. tags: [configuration] x-zero-tool: zero.projects.variables.put x-zero-scope: config:write requestBody: { $ref: '#/components/requestBodies/ConfigValue' } responses: '200': { $ref: '#/components/responses/Variable' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } delete: operationId: deleteProjectVariable x-zero-operator: project.delete_variable x-zero-lifecycle: live summary: Remove a declaração da variável neste escopo. tags: [configuration] x-zero-tool: zero.projects.variables.delete x-zero-scope: config:write responses: '204': { description: Declaração removida. } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/secrets: parameters: - $ref: '#/components/parameters/ProjectId' get: operationId: listProjectSecrets x-zero-operator: project.secrets x-zero-lifecycle: live summary: Secrets declarados no projeto (apenas metadados). tags: [configuration] x-zero-tool: zero.projects.secrets.list x-zero-scope: secret:read responses: '200': { $ref: '#/components/responses/SecretList' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/secrets/{key}: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/ConfigKey' put: operationId: putProjectSecret x-zero-lifecycle: live summary: Grava o valor de um secret do projeto. tags: [configuration] x-zero-tool: zero.projects.secrets.put x-zero-scope: secret:write requestBody: { $ref: '#/components/requestBodies/ConfigValue' } responses: '200': { $ref: '#/components/responses/Secret' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } delete: operationId: deleteProjectSecret x-zero-operator: project.delete_secret x-zero-lifecycle: live summary: Remove a declaração do secret neste escopo. tags: [configuration] x-zero-tool: zero.projects.secrets.delete x-zero-scope: secret:write responses: '204': { description: Declaração removida. } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /environments/{environmentId}/variables: parameters: - $ref: '#/components/parameters/EnvironmentId' get: operationId: listEnvironmentVariables x-zero-operator: environment.variables x-zero-lifecycle: live summary: Variáveis declaradas no ambiente. tags: [configuration] x-zero-tool: zero.environments.variables.list x-zero-scope: config:read responses: '200': { $ref: '#/components/responses/VariableList' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /environments/{environmentId}/variables/{key}: parameters: - $ref: '#/components/parameters/EnvironmentId' - $ref: '#/components/parameters/ConfigKey' put: operationId: putEnvironmentVariable x-zero-operator: environment.set_variable x-zero-lifecycle: live summary: Declara ou substitui uma variável do ambiente. tags: [configuration] x-zero-tool: zero.environments.variables.put x-zero-scope: config:write requestBody: { $ref: '#/components/requestBodies/ConfigValue' } responses: '200': { $ref: '#/components/responses/Variable' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } delete: operationId: deleteEnvironmentVariable x-zero-operator: environment.delete_variable x-zero-lifecycle: live summary: Remove a declaração da variável neste escopo. tags: [configuration] x-zero-tool: zero.environments.variables.delete x-zero-scope: config:write responses: '204': { description: Declaração removida. } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /environments/{environmentId}/secrets/{key}: parameters: - $ref: '#/components/parameters/EnvironmentId' - $ref: '#/components/parameters/ConfigKey' put: operationId: putEnvironmentSecret x-zero-lifecycle: live summary: Grava o valor de um secret do ambiente. tags: [configuration] x-zero-tool: zero.environments.secrets.put x-zero-scope: secret:write requestBody: { $ref: '#/components/requestBodies/ConfigValue' } responses: '200': { $ref: '#/components/responses/Secret' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } delete: operationId: deleteEnvironmentSecret x-zero-operator: environment.delete_secret x-zero-lifecycle: live summary: Remove a declaração do secret neste escopo. tags: [configuration] x-zero-tool: zero.environments.secrets.delete x-zero-scope: secret:write responses: '204': { description: Declaração removida. } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /organizations/{organizationId}/domains/available: parameters: - $ref: '#/components/parameters/OrganizationId' - name: label in: query required: true schema: { type: string } get: operationId: checkDomainAvailabilityInOrganization x-zero-operator: organization.domain_availability x-zero-lifecycle: live summary: Conferir um endereço antes de o projeto existir. description: | A mesma pergunta de `/projects/{id}/domains/available`, no escopo da organização. Ela existe porque a tela de criação precisa conferir e sugerir ANTES de haver projeto — e a alternativa seria criar um projeto de mentira para poder perguntar. tags: [domains] x-zero-tool: zero.organizations.domains.check x-zero-scope: domain:read responses: '200': description: Resultado da conferência. content: application/json: schema: { $ref: '#/components/schemas/DomainCheck' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /organizations/{organizationId}/domains/suggestions: parameters: - $ref: '#/components/parameters/OrganizationId' - name: label in: query required: false schema: { type: string } get: operationId: suggestDomainsInOrganization x-zero-operator: organization.domain_suggestions x-zero-lifecycle: live summary: Sugerir endereços livres a partir de um nome. tags: [domains] x-zero-tool: zero.organizations.domains.suggest x-zero-scope: domain:read responses: '200': description: Sugestões livres. content: application/json: schema: type: object required: [items] properties: items: type: array items: type: object required: [label, hostname] properties: label: { type: string } hostname: { type: string } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/domains/available: parameters: - $ref: '#/components/parameters/ProjectId' - name: zone_id in: query required: false schema: { type: string } description: | Em qual zona conferir. Sem ele, a pergunta é sobre o domínio da plataforma. Com ele, sobre a zona do cliente — e a resposta muda: `loja` pode estar tomado sob `zero.nnumbers.com.br` e livre sob `apps.acme.com.br`, porque são espaços de nome distintos. - name: label in: query required: true schema: { type: string } description: | O texto que a pessoa digitou. É NORMALIZADO antes de validar — "Minha Loja" vira `minha-loja` —, porque recusar por causa de um espaço seria cobrar conhecimento de DNS de quem só quer um endereço. get: operationId: checkDomainAvailability x-zero-operator: project.domain_availability x-zero-lifecycle: live summary: Este texto pode virar endereço, e está livre? description: | Duas perguntas numa resposta: o texto é um label válido, e ele está livre AGORA. "Agora" é a palavra importante. Entre esta resposta e a escolha existe uma janela, e quem a fecha é o índice único de `platform_addresses` — não esta rota. Ela serve para a tela dizer "esse já é de alguém" antes do envio, nunca para dispensar a checagem atômica. A consulta de disponibilidade atravessa organizações de propósito: sob RLS, um endereço de outra organização seria invisível, e invisível seria lido como livre. A resposta diz apenas que existe — nunca de quem é. tags: [domains] x-zero-tool: zero.projects.domains.check x-zero-scope: domain:read responses: '200': description: Resultado da conferência. content: application/json: schema: { $ref: '#/components/schemas/DomainCheck' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/domains/suggestions: parameters: - name: zone_id in: query required: false schema: { type: string } description: Sugerir dentro desta zona; sem ele, sob o domínio da plataforma. - $ref: '#/components/parameters/ProjectId' - name: label in: query required: false schema: { type: string } description: | Semente das sugestões. Sem ela, o nome do projeto é usado — é o que faz a lista acompanhar o campo em vez de repetir sempre a mesma coisa. get: operationId: suggestDomains x-zero-operator: project.domain_suggestions x-zero-lifecycle: live summary: Endereços bons que estão livres. description: | Nomes que uma pessoa reconhece, e nunca um identificador. Um sufixo aleatório em base32 resolveria a unicidade e destruiria o endereço: ele não se dita ao telefone, não se digita de memória e não diz de quem é. A ordem é da melhor para a pior: o nome puro, depois o nome com um qualificador (`loja-nova`), depois com o slug da organização, e só então com um número. O número vem por último porque `loja-2` conta ao mundo que alguém pegou `loja` antes — e o qualificador não conta nada. Todas as sugestões devolvidas estão livres no instante da resposta. tags: [domains] x-zero-tool: zero.projects.domains.suggest x-zero-scope: domain:read responses: '200': description: Sugestões livres. content: application/json: schema: type: object required: [items] properties: items: type: array items: type: object required: [label, hostname] properties: label: { type: string } hostname: { type: string } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/domains/canonical: parameters: - $ref: '#/components/parameters/ProjectId' put: operationId: setCanonicalDomain x-zero-operator: project.set_canonical_domain x-zero-lifecycle: live summary: Escolher o endereço público do projeto. description: | Um serviço tem UM endereço público. Trocar reserva o novo, enfileira a publicação da versão que já está no ar com o endereço novo (sem reconstruir) e devolve essa operação em `operation_id`. O anterior fica em `releasing` até a publicação ficar pronta — rota programada e aplicação respondendo — e então é liberado na mesma transação da ativação: o nome volta a ficar livre e deixa de responder. Nunca há intervalo sem endereço: o anterior só é liberado depois de o novo responder. Se a publicação falhar, o anterior continua sendo o que responde, e a próxima publicação pronta conclui a troca. Serviço que nunca foi publicado não tem o que republicar: o anterior é liberado na hora e `operation_id` é nulo. Até 2026-09-21 o anterior virava alias "que continua respondendo" — o que era falso: a rota só leva o endereço canônico, e o alias passava a responder 404 na primeira republicação, enquanto a tela o mostrava recebendo tráfego. A recusa por nome tomado vem do índice único, e não da conferência anterior: duas pessoas pedindo o mesmo nome no mesmo segundo recebem uma o endereço e a outra uma recusa clara. tags: [domains] x-zero-tool: zero.projects.domains.set x-zero-scope: domain:write requestBody: required: true content: application/json: schema: type: object required: [label] properties: label: type: string description: O texto escolhido. Normalizado do mesmo jeito que na conferência. zone_id: type: [string, 'null'] description: | A zona de aplicação onde o endereço será reservado (ADR-023). Omitido ou nulo, o endereço nasce sob o domínio da plataforma — que continua sendo o caminho padrão e não exige nada do cliente. A zona precisa estar servindo. Reservar sob uma zona que ainda espera delegação produziria um endereço que não responde, e o console mostraria como pronto um link quebrado. responses: '200': description: Endereço trocado. content: application/json: schema: { $ref: '#/components/schemas/DomainChoice' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /projects/{projectId}/services: parameters: - $ref: '#/components/parameters/ProjectId' get: operationId: listProjectServices x-zero-lifecycle: live x-zero-operator: project.services summary: Serviços do projeto. description: | A mesma listagem de `/services`, recortada pelo projeto do caminho. Duas rotas e um handler: é a mesma pergunta com um recorte a mais, e duplicá-la criaria dois lugares para consertar a projeção de serviço. tags: [services] x-zero-tool: zero.projects.services x-zero-scope: service:read responses: '200': description: Serviços do projeto. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Service' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/configuration/effective: parameters: - $ref: '#/components/parameters/ProjectId' - name: environment in: query required: false schema: { type: string } description: | O ambiente para o qual resolver. A mesma chave tem valores diferentes em ambientes diferentes — é o ponto do modelo —, então a resposta diz para qual ambiente ela vale. get: operationId: getEffectiveConfiguration x-zero-lifecycle: live x-zero-operator: environment.configuration summary: A configuração que o workload realmente recebe. description: | A herança dos três escopos já resolvida: o mais específico vence, e cada chave declara de onde veio e o que ela sobrepôs. Secret aparece na lista com `value: null` e `secret: true`. Null e não string vazia: string vazia é um valor legítimo de variável, e confundir os dois faria a tela afirmar que um secret está em branco. tags: [configuration] x-zero-tool: zero.projects.configuration.effective x-zero-scope: config:read responses: '200': description: Configuração efetiva do ambiente. content: application/json: schema: { $ref: '#/components/schemas/EffectiveConfiguration' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/limits: parameters: - $ref: '#/components/parameters/ProjectId' get: operationId: listProjectLimits x-zero-operator: project.limits x-zero-lifecycle: live summary: Recursos e limites de cada serviço do projeto. description: | O declarado, o aplicado e o observado, lado a lado e por serviço. A conta do "efetivo" é a MESMA que o compilador usa ao gerar o workload — e não uma reimplementação que diverge no primeiro conserto. `applied` é null antes da primeira publicação; `observed` é null quando o amostrador nunca viu o serviço — null, nunca zero. tags: [services] x-zero-tool: zero.projects.limits x-zero-scope: service:read responses: '200': description: Recursos por serviço. content: application/json: schema: { $ref: '#/components/schemas/ProjectLimits' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/publications: parameters: - $ref: '#/components/parameters/ProjectId' get: operationId: listProjectPublications x-zero-operator: project.publications x-zero-lifecycle: live summary: Tentativas de publicação — inclusive as que morreram antes do Deployment. description: | Uma falha de branch, origem ou build inicial acontece antes de `revision_committed`, e o Deployment só nasce depois: a tentativa que morre ali não aparece na lista de Deployments. Esta rota conta TODAS as tentativas, com origem, ator, o último checkpoint alcançado, o erro público com ação, e — quando o Deployment nasceu — o vínculo com ele. tags: [deployments] x-zero-tool: zero.projects.publications x-zero-scope: service:read responses: '200': description: Tentativas, da mais recente para a mais antiga. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/PublicationAttempt' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /projects/{projectId}/publications/{operationId}/dismiss: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/OperationId' post: operationId: dismissPublicationAttempt x-zero-lifecycle: live summary: Dispensa uma tentativa encerrada da lista do projeto. description: | A lista de "tentativas que não viraram Deployment" é derivada: toda publicação encerrada sem Deployment fica nela, inclusive depois de dez publicações bem-sucedidas. Dispensar é o ato de quem olhou e entendeu — a linha sai da lista. O desfecho NÃO muda: a operação continua `failed`, o erro continua gravado e `GET /operations/{operationId}` continua respondendo tudo. Dispensar é sobre a lista, nunca sobre o fato. Só tentativa ENCERRADA se dispensa. Uma publicação em andamento responde 409: o gesto para ela é cancelar. tags: [deployments] x-zero-scope: service:write # Fora do catálogo de agente, de propósito: dispensar é dizer "eu olhei e # entendi". Um agente que dispensa esconde falha de quem precisa vê-la, e # o gesto perde exatamente o significado que ele tem. x-zero-mcp-exposed: false responses: '204': { description: Dispensada. A tentativa sai da lista do projeto. } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': description: A tentativa ainda está em andamento. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } /services/{serviceId}/resources: parameters: - $ref: '#/components/parameters/ServiceId' put: operationId: updateServiceResources x-zero-lifecycle: live x-zero-operator: service.resources summary: Declara CPU e memória (reservado e máximo) do serviço. description: | Os quatro campos são independentes: declarar só a memória mantém a CPU como está. `restore_defaults` remove os overrides e devolve o serviço aos padrões da plataforma. Recursos são configuração, e configuração muda por publicação: com uma revisão ativa, a resposta é `202` com a operação que republica o serviço — mesma imagem, novo bundle. Sem revisão ativa não há o que republicar: a intenção fica declarada, a resposta é `200`, e a primeira publicação a carrega. tags: [services] x-zero-tool: zero.services.resources x-zero-scope: service:write requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ResourcesUpdate' } responses: '200': description: Declarado; será aplicado na primeira publicação. content: application/json: schema: type: object required: [applied, effective] properties: applied: { type: boolean, const: false } effective: { $ref: '#/components/schemas/EffectiveResources' } '202': { $ref: '#/components/responses/Accepted' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } # ───────────────────────── zonas de aplicação (ADR-023) ───────────────────── # # A subzona que o cliente delega ao Zero. O produto inteiro dessas rotas cabe # numa frase: o cliente informa o domínio dele, publica quatro registros NS no # provedor DELE, e a partir daí as aplicações ganham endereço sob a marca dele. # # O que estas rotas NÃO fazem, e nunca farão: receber credencial do provedor # DNS do cliente, alterar registro fora da subzona, ou expor os nameservers # internos do provedor. Ver ADR-023. /organizations/{organizationId}/gateway-capabilities: parameters: - $ref: '#/components/parameters/OrganizationId' get: operationId: getGatewayCapabilities x-zero-operator: gateway.capabilities x-zero-lifecycle: live summary: O que esta instalação realmente oferece. description: | O console lê isto para não desenhar um botão que a API vai recusar. Cada capacidade indisponível traz `reason` — um código ESTÁVEL — e `message`, a frase pronta para a tela. O console decide o que mostrar a partir do código; comparar substring de mensagem quebra na primeira vez que alguém reescreve o texto. Nada aqui nomeia provedor de nuvem, balanceador ou quota interna. tags: [gateway] x-zero-tool: zero.organizations.gateway_capabilities.get x-zero-scope: domain:read responses: '200': description: O retrato das capacidades. content: application/json: schema: { $ref: '#/components/schemas/GatewayCapabilities' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /organizations/{organizationId}/gateways: parameters: - $ref: '#/components/parameters/OrganizationId' get: operationId: listGateways x-zero-operator: gateway.list x-zero-lifecycle: live summary: A conectividade externa da organização. tags: [gateway] x-zero-tool: zero.organizations.gateways.list x-zero-scope: domain:read responses: '200': description: Gateways vivos, com endereços e contagem de domínios. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Gateway' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } post: operationId: createGateway x-zero-operator: gateway.create x-zero-lifecycle: live summary: Criar a camada de conectividade externa. description: | Cria o gateway e já publica o endereço de SAÍDA compartilhado — que é observado, não alocado: é o endereço que a infraestrutura já aplica a quem sai sem endereço próprio. Nenhum campo aceita credencial de nuvem. A credencial é da infraestrutura do Zero e o cliente nunca a fornece. tags: [gateway] x-zero-tool: zero.organizations.gateways.create x-zero-scope: domain:write requestBody: required: false content: application/json: schema: type: object properties: name: type: string maxLength: 60 description: Nome visível. Vazio vira "Conectividade". class: type: string enum: [shared, dedicated] default: shared description: | `shared` divide a borda e separa por hostname. `dedicated` recebe endereço exclusivo — e endereço público é finito. responses: '201': description: Gateway criado. content: application/json: schema: { $ref: '#/components/schemas/Gateway' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } '503': description: A instalação não oferece endereços públicos gerenciados. content: application/json: schema: { $ref: '#/components/schemas/Problem' } /gateways/{gatewayId}: parameters: - $ref: '#/components/parameters/GatewayId' get: operationId: getGateway x-zero-operator: gateway.get x-zero-lifecycle: live summary: O estado da conectividade, com endereços e domínios. tags: [gateway] x-zero-tool: zero.gateways.get x-zero-scope: domain:read responses: '200': description: Estado composto — `ready` exige endereço pronto. content: application/json: schema: { $ref: '#/components/schemas/Gateway' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } delete: operationId: deleteGateway x-zero-operator: gateway.delete x-zero-lifecycle: live summary: Pedir a remoção da conectividade. description: | Assíncrona por necessidade: há endereço público para devolver, e responder "removido" antes disso criaria um recurso pago que ninguém mais encontra. A linha sobrevive até a devolução ser confirmada. tags: [gateway] x-zero-tool: zero.gateways.delete x-zero-scope: domain:write responses: '202': description: Remoção aceita; os endereços estão sendo devolvidos. content: application/json: schema: { $ref: '#/components/schemas/Gateway' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': description: Há domínios servindo tráfego. content: application/json: schema: { $ref: '#/components/schemas/Problem' } /gateways/{gatewayId}/domains: parameters: - $ref: '#/components/parameters/GatewayId' get: operationId: listGatewayDomains x-zero-operator: gateway.domains x-zero-lifecycle: live summary: Os domínios próprios servidos por este gateway. tags: [gateway] x-zero-tool: zero.gateways.domains.list x-zero-scope: domain:read responses: '200': description: Domínios vivos, com instrução de DNS quando há endereço pronto. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/GatewayDomain' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } post: operationId: createGatewayDomain x-zero-operator: gateway.add_domain x-zero-lifecycle: live summary: Cadastrar um domínio próprio. description: | O hostname é normalizado (minúsculo, sem ponto final, punycode) antes de ser gravado, e é único no MUNDO — é isso que impede uma organização de reivindicar o domínio de outra. No modo `customer_managed` a resposta traz UMA VEZ o registro TXT de confirmação. O valor não é recuperável depois: só o resumo dele fica gravado. tags: [gateway] x-zero-tool: zero.gateways.domains.create x-zero-scope: domain:write requestBody: required: true content: application/json: schema: type: object required: [hostname] properties: hostname: type: string example: api.acme.com.br dns_mode: type: string enum: [delegated_zone, customer_managed] default: customer_managed responses: '201': description: Domínio cadastrado. content: application/json: schema: { $ref: '#/components/schemas/GatewayDomain' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': description: O domínio já está em uso. A mensagem não diz de quem. content: application/json: schema: { $ref: '#/components/schemas/Problem' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /gateways/{gatewayId}/domains/{domainId}: parameters: - $ref: '#/components/parameters/GatewayId' - name: domainId in: path required: true schema: { type: string, examples: ["gwd_01JC5Z9K0000000000000000"] } delete: operationId: deleteGatewayDomain x-zero-operator: gateway.remove_domain x-zero-lifecycle: live summary: Remover um domínio próprio. description: | O hostname é único no MUNDO, e a remoção é o que o libera de novo. Sem ela, um domínio cadastrado e nunca confirmado bloquearia o dono legítimo para sempre — e quem bloqueou também não conseguiria desfazer. tags: [gateway] x-zero-tool: zero.gateways.domains.delete x-zero-scope: domain:write responses: '204': { description: Removido. O hostname volta a ficar disponível. } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /gateways/{gatewayId}/domains/{domainId}/verify: parameters: - $ref: '#/components/parameters/GatewayId' - name: domainId in: path required: true schema: { type: string, examples: ["gwd_01JC5Z9K0000000000000000"] } post: operationId: verifyGatewayDomain x-zero-operator: gateway.verify_domain x-zero-lifecycle: live summary: "Já configurei: conferir o DNS agora." description: >- Antecipa o que a reconciliação faria sozinha no próximo ciclo. Sem ele, quem acabou de publicar os registros espera sem saber se acertou — e é a dúvida, não a espera, que faz abrir chamado. O caminho é estreito de propósito: olha o DNS público e nada mais. A emissão do certificado e a publicação na borda continuam sendo da reconciliação. tags: [gateway] x-zero-tool: zero.gateways.domains.verify x-zero-scope: domain:write responses: '200': description: O domínio com a jornada e os registros atualizados. content: application/json: schema: { $ref: '#/components/schemas/GatewayDomain' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /gateways/{gatewayId}/routes: parameters: - $ref: '#/components/parameters/GatewayId' get: operationId: listGatewayRoutes x-zero-operator: gateway.routes x-zero-lifecycle: live summary: As rotas que fazem os domínios próprios servirem as aplicações. tags: [gateway] x-zero-tool: zero.gateways.routes.list x-zero-scope: domain:read responses: '200': description: Rotas vivas deste gateway. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/GatewayRoute' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } post: operationId: createGatewayRoute x-zero-operator: gateway.add_route x-zero-lifecycle: live summary: Ligar um domínio confirmado a um serviço. description: | Com certificado e sem rota, o endereço do cliente responde 404 sob HTTPS válido. É esta chamada que fecha o caminho. O domínio precisa estar CONFIRMADO. Servir tráfego num nome que ninguém provou controlar é exatamente o caminho por onde se pediria certificado para o domínio de terceiro. O ambiente não é informado: ele é derivado do serviço. Aceitar o par permitiria um pedido internamente inconsistente que só falharia na materialização, horas depois. tags: [gateway] x-zero-tool: zero.gateways.routes.create x-zero-scope: domain:write requestBody: required: true content: application/json: schema: type: object required: [domain_id, service_id] properties: domain_id: type: string examples: ["gwd_01JC5Z9K0000000000000000"] service_id: type: string examples: ["svc_01JC5Z9K0000000000000000"] path: type: string default: "/" examples: ["/api"] path_type: type: string enum: [prefix, exact] default: prefix responses: '201': description: Rota criada. Ela é publicada na borda pela reconciliação. content: application/json: schema: { $ref: '#/components/schemas/GatewayRoute' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /gateways/{gatewayId}/routes/{routeId}: parameters: - $ref: '#/components/parameters/GatewayId' - name: routeId in: path required: true schema: { type: string, examples: ["gwr_01JC5Z9K0000000000000000"] } put: operationId: replaceGatewayRoute x-zero-operator: gateway.replace_route x-zero-lifecycle: live summary: Editar destino, casamento, caminho e reescrita de uma rota. description: | Uma transição só, e não quatro campos. Editar em duas escritas abriria uma janela em que a rota vale com o caminho novo e o destino velho — e nessa janela o tráfego de alguém vai para o lugar errado. O corpo é o MESMO da criação, de propósito: uma edição parcial obrigaria a tela a saber o que NÃO mudou, que é a informação que envelhece entre a leitura e o clique. Depois da edição o status volta a `pending`: a rota mudou, e afirmar "pronta" antes de a borda ter aplicado seria prometer o que ainda não está no ar. tags: [gateway] x-zero-tool: zero.gateways.routes.replace x-zero-scope: domain:write requestBody: required: true content: application/json: schema: type: object required: [service_id] properties: domain_id: { type: string } service_id: { type: string } path: { type: string, examples: ["/web"] } path_type: { type: string, enum: [prefix, exact], default: prefix } rewrite_policy: type: string enum: [preserve, strip_prefix] default: preserve description: >- O que a APLICAÇÃO recebe. `preserve` é a transformação nula e o default; `strip_prefix` remove da requisição o prefixo declarado na rota, e só vale com `path_type: prefix` — remover o prefixo de um caminho exato produziria sempre `/`. responses: '200': description: Rota atualizada. content: application/json: schema: { $ref: '#/components/schemas/GatewayRoute' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '422': { $ref: '#/components/responses/UnprocessableEntity' } delete: operationId: deleteGatewayRoute x-zero-operator: gateway.remove_route x-zero-lifecycle: live summary: Desligar um endereço de um serviço. description: | O objeto sai da borda e o endereço deixa de responder por aquele caminho. O domínio continua cadastrado e confirmado — remover a rota não devolve o hostname ao mundo. tags: [gateway] x-zero-tool: zero.gateways.routes.delete x-zero-scope: domain:write responses: '204': { description: Removida. } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /gateways/{gatewayId}/routes/{routeId}/policy: parameters: - $ref: '#/components/parameters/GatewayId' - name: routeId in: path required: true schema: { type: string, examples: ["gwr_01JC5Z9K0000000000000000"] } get: operationId: getGatewayRoutePolicy x-zero-operator: gateway.route_policy x-zero-lifecycle: live summary: O que a borda exige antes de deixar a requisição passar. tags: [gateway] x-zero-tool: zero.gateways.routes.policy.get x-zero-scope: domain:read responses: '200': description: >- A política vigente. Rota sem política responde com auth_mode none. content: application/json: schema: { $ref: '#/components/schemas/GatewayRoutePolicy' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } put: operationId: setGatewayRoutePolicy x-zero-operator: gateway.set_route_policy x-zero-lifecycle: live summary: Definir autenticação e limite de tráfego da rota. description: | Substitui a política INTEIRA. Uma política parcial é ambígua justamente onde não pode ser: "não mandei `auth_mode`" significa manter ou remover? `jwt_jwks_uri` é validado antes de gravar. Quem busca esse endereço é a borda, de dentro da rede, então um endereço que resolva para a rede interna é recusado — e a recusa nomeia o campo. Limite de tráfego só é aceito quando a instalação conta de forma compartilhada entre as réplicas da borda. Sem isso, um limite de 100 viraria 100 por réplica, e a API recusa em vez de entregar outra coisa. tags: [gateway] x-zero-tool: zero.gateways.routes.policy.set x-zero-scope: domain:write requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/GatewayRoutePolicyInput' } responses: '200': description: Política gravada. A borda a aplica na reconciliação. content: application/json: schema: { $ref: '#/components/schemas/GatewayRoutePolicy' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /gateways/{gatewayId}/api-keys: parameters: - $ref: '#/components/parameters/GatewayId' get: operationId: listGatewayApiKeys x-zero-lifecycle: live summary: As chaves de API deste gateway. description: | O valor NUNCA aparece aqui. Só o prefixo, para a pessoa reconhecer qual chave é qual — o resto não é recuperável nem pela plataforma. tags: [gateway] x-zero-tool: zero.gateways.keys.list x-zero-scope: domain:read responses: '200': description: Chaves, incluindo as revogadas — uma chave revogada é o que alguém vai querer investigar. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/GatewayApiKey' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } post: operationId: createGatewayApiKey x-zero-lifecycle: live summary: Criar uma chave de API. description: | A resposta traz o valor UMA VEZ. Ele não é recuperável depois: o banco guarda o resumo SHA-256 e um ciphertext que só a materialização na borda decifra — nenhum caminho de leitura chega até ele. Repetir este POST cria outra chave em vez de devolver a primeira. O valor não passa pelo registro de idempotência de propósito: guardá-lo ali seria plaintext no banco com outro nome. tags: [gateway] x-zero-tool: zero.gateways.keys.create x-zero-scope: domain:write requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: type: string maxLength: 60 examples: ["integração de faturamento"] expires_in_days: type: integer description: Zero significa sem vencimento. maximum: 3650 responses: '201': description: Criada. `value` vem preenchido nesta resposta e em nenhuma outra. content: application/json: schema: { $ref: '#/components/schemas/GatewayApiKey' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /gateways/{gatewayId}/api-keys/{keyId}: parameters: - $ref: '#/components/parameters/GatewayId' - name: keyId in: path required: true schema: { type: string, examples: ["gwk_01JC5Z9K0000000000000000"] } delete: operationId: revokeGatewayApiKey x-zero-lifecycle: live summary: Revogar uma chave de API. description: | A linha fica, com carimbo de revogação — quem apaga não consegue responder depois se a chave existiu. O ciphertext sai: uma chave que não vale mais não precisa ser materializável. tags: [gateway] x-zero-tool: zero.gateways.keys.revoke x-zero-scope: domain:write responses: '204': { description: Revogada. A borda deixa de aceitá-la na próxima reconciliação. } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /organizations/{organizationId}/application-zones: parameters: - $ref: '#/components/parameters/OrganizationId' get: operationId: listApplicationZones x-zero-operator: zone.list x-zero-lifecycle: live summary: As zonas de domínio próprio da organização. tags: [domains] x-zero-tool: zero.organizations.zones.list x-zero-scope: domain:read responses: '200': description: Zonas vivas. Uma removida não aparece. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/ApplicationZone' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } post: operationId: createApplicationZone x-zero-operator: zone.create x-zero-lifecycle: live summary: Pedir uma zona sob o domínio da organização. description: | Recebe o domínio da empresa (`acme.com.br`) e o rótulo da subzona. O rótulo tem sugestão — `apps` — e é **editável**: quem já usa `apps` para outra coisa precisa poder escolher outro sem falar com ninguém. A profundidade é fixa em um rótulo. `a.b.acme.com.br` é recusado porque o certificado wildcard `*.apps.acme.com.br` não cobre dois níveis: aceitar criaria uma zona que parece funcionar e falha na primeira aplicação. Rótulos que quebrariam a empresa (`www`, `mail`, `_dmarc`, `ns1`…) são recusados com a razão escrita, não com "inválido". Responde `202`: a zona ainda vai ser criada no provedor. Os nameservers só aparecem depois disso — mostrá-los antes convidaria o cliente a delegar para uma zona que ainda não responde. tags: [domains] x-zero-tool: zero.organizations.zones.create x-zero-scope: domain:write requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ApplicationZoneCreate' } responses: '202': description: Pedido aceito. A zona nasce em `PENDING_PROVISIONING`. content: application/json: schema: { $ref: '#/components/schemas/ApplicationZone' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': description: | O nome já foi pedido. A resposta **não** revela de quem ele é — dizer "essa zona é da organização X" entregaria informação sobre um cliente a outro. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } '422': { $ref: '#/components/responses/UnprocessableEntity' } /organizations/{organizationId}/application-zones/preview: parameters: - $ref: '#/components/parameters/OrganizationId' - name: domain in: query required: true schema: { type: string, examples: ["acme.com.br"] } description: | O que a pessoa digitou. É normalizado antes de validar: `https://ACME.com.br/` vira `acme.com.br`, e um domínio internacionalizado é convertido para a forma ASCII. Recusar por causa de um `https://` colado seria cobrar conhecimento de DNS de quem só quer um endereço. - name: label in: query required: false schema: { type: string, default: apps } get: operationId: previewApplicationZone x-zero-operator: zone.preview x-zero-lifecycle: live summary: Como ficaria a zona, antes de pedir. description: | Existe para que a tela mostre o resultado enquanto a pessoa digita, sem criar nada. Devolve o domínio registrável derivado, o nome final da zona, os problemas encontrados — cada um com a razão — e um exemplo de endereço de aplicação, que é o que de fato responde a pergunta "e como vai ficar?". tags: [domains] x-zero-tool: zero.organizations.zones.preview x-zero-scope: domain:read responses: '200': description: Prévia. Um `ok` falso não é erro de requisição — é resposta. content: application/json: schema: { $ref: '#/components/schemas/ApplicationZonePreview' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /organizations/{organizationId}/application-zones/{zoneId}: parameters: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/ZoneId' get: operationId: getApplicationZone x-zero-operator: zone.get x-zero-lifecycle: live summary: Uma zona, com o que falta para ela ficar pronta. tags: [domains] x-zero-tool: zero.organizations.zones.get x-zero-scope: domain:read responses: '200': description: A zona. content: application/json: schema: { $ref: '#/components/schemas/ApplicationZone' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } delete: operationId: requestApplicationZoneRemoval x-zero-operator: zone.request_removal x-zero-lifecycle: live summary: Pedir a remoção da zona. description: | Não apaga nada de imediato, e isso é o ponto. A zona vai para `REMOVAL_REQUESTED` e **continua servindo**: apagá-la agora deixaria os endereços do cliente apontando para o vazio pelo tempo do cache. A ordem é: o cliente remove os NS no provedor dele, nós confirmamos que a delegação sumiu, a zona é removida do provedor e o **nome** fica em quarentena por 30 dias. Enquanto durar, o nome não é dado a ninguém. Se ainda houver aplicações publicadas sob a zona, responde `409` com a lista — remover com aplicações no ar seria derrubá-las sem aviso. tags: [domains] x-zero-tool: zero.organizations.zones.remove x-zero-scope: domain:write x-zero-destructive: true responses: '202': description: Remoção iniciada. Cancelável enquanto nada foi destruído. content: application/json: schema: { $ref: '#/components/schemas/ApplicationZone' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': description: Há endereços publicados sob a zona. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } /organizations/{organizationId}/application-zones/{zoneId}/verify: parameters: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/ZoneId' get: operationId: getApplicationZoneVerification x-zero-operator: zone.verification x-zero-lifecycle: live summary: O resultado da última verificação da delegação. description: | Devolve o que foi observado, não uma conclusão sem lastro: quais nameservers o **pai autoritativo** anuncia, quais faltam, quais sobram, e quando foi a última tentativa. A pergunta é feita ao servidor autoritativo do domínio pai, nunca a um resolver público: resolver responde do cache, e cache conta o que era verdade minutos atrás. Cache não é prova. tags: [domains] x-zero-tool: zero.organizations.zones.verification x-zero-scope: domain:read responses: '200': description: Última verificação conhecida. content: application/json: schema: { $ref: '#/components/schemas/ZoneVerification' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } post: operationId: verifyApplicationZone x-zero-operator: zone.verify x-zero-lifecycle: live summary: Conferir a delegação agora. description: | O botão "já configurei". Antecipa a verificação que o reconciliador faria sozinho — sem ele, quem acabou de publicar os NS esperaria o próximo ciclo sem saber se acertou. É limitado por taxa: repetir a consulta em rajada não faz a delegação propagar mais rápido e castiga os servidores do domínio do cliente. tags: [domains] x-zero-tool: zero.organizations.zones.verify x-zero-scope: domain:write responses: '200': description: Verificação executada. O corpo diz o que se viu. content: application/json: schema: { $ref: '#/components/schemas/ZoneVerification' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '429': description: Verificação pedida cedo demais. O corpo diz quando tentar. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } /organizations/{organizationId}/application-zones/{zoneId}/cancel-removal: parameters: - $ref: '#/components/parameters/OrganizationId' - $ref: '#/components/parameters/ZoneId' post: operationId: cancelApplicationZoneRemoval x-zero-operator: zone.cancel_removal x-zero-lifecycle: live summary: Desistir da remoção. description: | Só funciona enquanto nada foi destruído — em `REMOVAL_REQUESTED` ou `PENDING_UNDELEGATION`. Depois da quarentena não há volta: a zona já não existe no provedor, e ressuscitar o nome enquanto caches antigos ainda apontam para cá faria tráfego chegar em quem não o espera. tags: [domains] x-zero-tool: zero.organizations.zones.cancel_removal x-zero-scope: domain:write responses: '200': description: Remoção cancelada; a zona volta ao estado anterior. content: application/json: schema: { $ref: '#/components/schemas/ApplicationZone' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': description: Tarde demais — a zona já foi removida do provedor. content: application/problem+json: schema: { $ref: '#/components/schemas/Problem' } /domain-delegation/providers: get: operationId: listDomainDelegationProviders x-zero-operator: domain.delegation_providers x-zero-lifecycle: live summary: Tutoriais de delegação, por provedor de DNS. description: | O catálogo de instruções que a tela mostra depois de dar os quatro nameservers. A delegação é manual em **todo** provedor: integrar por API exigiria credencial do cliente, que é exatamente o que o ADR-023 descartou. O que o produto entrega no lugar é o passo a passo certo para cada painel, com a nomenclatura que aquele painel usa — "Registro NS" na AWS, "Add record → NS" na Cloudflare, "Editar zona" no Registro.br. Rota pública dentro da API autenticada: o conteúdo é documentação, igual para todos, e não depende de organização. tags: [domains] x-zero-tool: zero.domains.delegation_providers x-zero-scope: domain:read responses: '200': description: Catálogo. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/DelegationProviderGuide' } '401': { $ref: '#/components/responses/Unauthorized' } # ─────────────────────── administração de identidade ──────────────────────── # # Duas autoridades transversais, e nenhuma delas é papel de organização: # # platform_administrator adota realm, designa quem administra cada um # realm administrator administra as PESSOAS de um realm # # Um realm compartilhado serve VÁRIAS organizações, então o escopo aqui é o # realm — não o tenant. É por isso que estas rotas não aceitam o cabeçalho de # seleção de organização. # # ── Por que nenhuma delas é ferramenta de MCP ────────────────────────────── # # `x-zero-mcp-exposed: false` em todas, e a ausência de `x-zero-tool` é o que # tem efeito prático: um agente que pudesse conceder acesso ao Zero, designar # administrador ou criar pessoa no provedor de identidade seria uma escalada de # privilégio a um prompt de distância. Estas decisões são de gente. components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT description: | Token OIDC emitido pelo provedor de identidade configurado. O escopo de tenant vem sempre do token — nunca do corpo da requisição. parameters: GatewayId: name: gatewayId in: path required: true schema: { type: string, examples: ["gw_01JC5Z9K0000000000000000"] } OrganizationId: name: organizationId in: path required: true schema: { type: string, examples: ["org_01JC5Z9K0000000000000000"] } RealmUserId: name: userId in: path required: true description: | Identificador do usuário no realm da organização — o `sub` que o token dele apresenta. Não é e-mail: e-mail muda de dono, e o `sub` não. schema: { type: string, minLength: 1, maxLength: 255 } ProjectId: name: projectId in: path required: true schema: { type: string } SourceAuthorizationId: name: authorizationId in: path required: true description: | A autorização de OAuth em trânsito (`sau_…`), entre a volta do provedor e a escolha do repositório. Não é a origem: é a credencial esperando numa linha que expira sozinha. schema: { type: string } ZoneId: name: zoneId in: path required: true schema: { type: string } EnvironmentId: name: environmentId in: path required: true schema: { type: string } ServiceId: name: serviceId in: path required: true schema: { type: string } BuildId: name: buildId in: path required: true schema: { type: string } DeploymentId: name: deploymentId in: path required: true schema: { type: string } DatastoreId: name: datastoreId in: path required: true schema: { type: string } ConfigKey: name: key in: path required: true schema: { type: string, pattern: '^[A-Za-z_][A-Za-z0-9_]{0,127}$' } description: | O nome da variável ou do secret. Dentro de um escopo ele É o identificador do recurso, e é por isso que fica no caminho. ObservabilityWindow: name: window in: query required: false schema: { type: string, enum: ['1h', '6h', '24h', '7d', '30d', '90d'] } description: | Janela relativa. A lista é fechada: uma janela arbitrária produz um eixo que não bate com nenhum outro painel aberto ao lado, e comparar duas telas é metade do trabalho de quem investiga. Ignorada quando `from` vem junto. ObservabilityFrom: name: from in: query required: false schema: { type: string, format: date-time } description: Início da janela, RFC 3339. Alinhado ao balde pelo servidor. ObservabilityTo: name: to in: query required: false schema: { type: string, format: date-time } description: Fim da janela, RFC 3339. Padrão é agora. ObservabilityEnvironment: name: environment in: query required: false schema: { type: string } description: | Recorta por ambiente. Conferido contra a hierarquia do projeto, e não só contra o tenant: dentro de uma organização, um ambiente de OUTRO projeto produziria uma tela do projeto A mostrando o tráfego do B. ObservabilityService: name: service in: query required: false schema: { type: string } description: Recorta por serviço, conferido contra a hierarquia do projeto. OperationId: name: operationId in: path required: true schema: { type: string } Cursor: name: cursor in: query schema: { type: string } description: Cursor opaco devolvido em `next_cursor`. Limit: name: limit in: query schema: { type: integer, minimum: 1, maximum: 100, default: 20 } IdempotencyKey: name: Idempotency-Key in: header schema: { type: string, maxLength: 255 } description: | Repetir a mesma chave com o mesmo corpo devolve a resposta original. Mesma chave com corpo diferente devolve `409 IDEMPOTENCY_KEY_CONFLICT`. responses: Accepted: description: Aceito. Operação durável criada; acompanhe em `/operations/{id}`. headers: X-Operation-Id: schema: { type: string } content: application/json: schema: { $ref: '#/components/schemas/OperationRef' } Unauthorized: description: Autenticação ausente ou inválida. content: application/json: schema: { $ref: '#/components/schemas/Problem' } Forbidden: description: Sem permissão. content: application/json: schema: { $ref: '#/components/schemas/Problem' } NotFound: description: | Não existe — ou não pertence a esta organização. A resposta é idêntica nos dois casos, de propósito: não confirmamos a existência de recurso alheio. content: application/json: schema: { $ref: '#/components/schemas/Problem' } Conflict: description: Conflito com o estado atual. content: application/json: schema: { $ref: '#/components/schemas/Problem' } UnprocessableEntity: description: Entrada válida sintaticamente, mas recusada por regra de produto ou política. content: application/json: schema: { $ref: '#/components/schemas/Problem' } TooManyRequests: description: | Limite de uso excedido. Em fluxos SSE, é o teto de conexões simultâneas por organização — um fluxo aberto custa uma conexão de banco enquanto vive, e sem teto uma aba esquecida em cada máquina de um time esgota o pool para todo mundo. content: application/json: schema: { $ref: '#/components/schemas/Problem' } Variable: description: Variável declarada. content: application/json: schema: { $ref: '#/components/schemas/ConfigVariable' } VariableList: description: Variáveis declaradas NESTE escopo — sem as herdadas. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/ConfigVariable' } Secret: description: Metadados do secret. O valor nunca é devolvido. content: application/json: schema: { $ref: '#/components/schemas/ConfigSecret' } SecretList: description: Secrets declarados NESTE escopo, sem valor. content: application/json: schema: type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/ConfigSecret' } requestBodies: ConfigValue: required: true description: | Só o valor. A chave é o identificador e está no caminho — um corpo que também a aceitasse criaria a pergunta "e se divergir?", cuja única resposta segura é recusar. content: application/json: schema: type: object required: [value] properties: value: type: string maxLength: 32768 description: | Para secret, é o texto claro, e é a ÚNICA vez que ele atravessa esta API: nenhuma leitura posterior o devolve. schemas: Problem: type: object required: [code, title] description: Formato único de erro. Códigos vivem em `errors/catalog.yaml`. properties: code: { type: string, examples: ["APPLICATION_PORT_NOT_DETECTED"] } title: { type: string, examples: ["Não encontramos a porta da aplicação"] } detail: { type: string } action: type: string description: O que o usuário pode fazer. Nunca deixar vazio em erro acionável. operation_id: { type: [string, 'null'] } request_id: { type: string } retryable: { type: boolean } help: { type: [string, 'null'], format: uri } existing_user: description: | Presente em `IDENTITY_USER_ALREADY_EXISTS`: a identidade que já existe no realm, para conceder acesso a ela em vez de criar outra. $ref: '#/components/schemas/RealmUser' fields: type: array description: Presente em VALIDATION_FAILED. items: type: object required: [field, message] properties: field: { type: string } message: { type: string } OperationRef: type: object required: [operation_id, status] properties: operation_id: { type: string, examples: ["op_01JC5Z9K0000000000000000"] } status: { type: string, enum: [queued, running] } resource_id: { type: [string, 'null'] } Operation: type: object required: [id, kind, status, created_at] properties: id: { type: string } kind: type: string examples: ["environment.create", "deployment.run", "datastore.create", "datastore.restore"] status: type: string enum: [queued, running, compensating, cancelling, succeeded, failed, cancelled, superseded] description: | `superseded` não é falha: a operação foi substituída por uma intenção mais nova antes de ativar o que construiu. display_status: { type: string, description: Texto em português para a UX. } progress: { type: [number, 'null'], minimum: 0, maximum: 1 } organization_id: { type: string } resource_type: { type: [string, 'null'] } resource_id: { type: [string, 'null'] } attempt: { type: integer } max_attempts: { type: integer } error: { $ref: '#/components/schemas/Problem' } created_at: { type: string, format: date-time } started_at: { type: [string, 'null'], format: date-time } finished_at: { type: [string, 'null'], format: date-time } OperationEvent: type: object required: [sequence, at, level, message] properties: sequence: { type: integer, format: int64 } at: { type: string, format: date-time } level: { type: string, enum: [debug, info, warning, error] } state: { type: [string, 'null'] } message: { type: string } progress: { type: [number, 'null'] } data: { type: object, additionalProperties: true } # ─────────────────── schemas das projeções de tela ─────────────────── # # Todo campo numérico que a plataforma não pode calcular honestamente é # `null`, e o tipo declara isso: `[number, 'null']`. Um schema que # declarasse só `number` obrigaria a implementação a inventar zero. OrganizationOverview: type: object required: [organization, projects, deployments, series, recent_deployments, recent_activity, checklist] properties: organization: { $ref: '#/components/schemas/Organization' } projects: type: object required: [total, ready, degraded, building, never_deployed] properties: total: { type: integer } ready: { type: integer } degraded: { type: integer } building: { type: integer } never_deployed: type: integer description: Projetos que existem e nunca publicaram. Não são degradados. deployments: type: object required: [window_days, total, succeeded, failed, in_progress] properties: window_days: type: integer description: Janela de TODOS os números deste bloco. total: { type: integer } succeeded: { type: integer } failed: { type: integer } in_progress: { type: integer } success_rate: type: [number, 'null'] description: Null quando não houve deployment terminal na janela. median_duration_seconds: type: [number, 'null'] description: | Mediana, não média: um build travado move a média e não move a mediana. Null sem deployment terminal na janela. series: type: array description: Um ponto por dia da janela, inclusive os dias sem deployment. items: type: object required: [date, total, succeeded, failed] properties: date: { type: string, format: date } total: { type: integer } succeeded: { type: integer } failed: { type: integer } recent_deployments: type: array items: { $ref: '#/components/schemas/DeploymentListItem' } recent_activity: type: array items: { $ref: '#/components/schemas/ActivityItem' } checklist: type: array items: type: object required: [key, label, done] properties: key: { type: string } label: { type: string } done: { type: boolean } ProjectCard: type: object required: [id, name, state, display_state, service_count, environment_count, created_at, lifecycle_state] properties: id: { type: string } name: { type: string } display_name: { type: [string, 'null'] } lifecycle_state: type: string enum: [active, deletion_requested, deleting, deletion_failed] description: | `active` no uso normal. Os outros três só aparecem ENQUANTO uma exclusão corre ou depois de ela falhar — é o que deixa a tela dizer "excluindo" ou "a exclusão falhou" em vez de fingir que acabou. Projeto excluído não aparece em leitura nenhuma: não existe `deleted` aqui. deletion_operation_id: type: [string, 'null'] description: A operação `project.delete` em curso ou falhada; null quando `active`. state: type: string enum: [never_deployed, created, deploying, online, offline] display_state: { type: string } url: type: [string, 'null'] description: Só preenchida com evidência `public_edge`. hostname: { type: [string, 'null'] } service_count: { type: integer } environment_count: { type: integer } has_multiple_webs: type: boolean description: Quando falso, a camada Serviço fica escondida na interface (ADR-021). last_deployment: oneOf: - $ref: '#/components/schemas/DeploymentListItem' - type: 'null' created_at: { type: string, format: date-time } DeploymentListItem: type: object required: [id, status, display_status, project_id, project_name, service_id, service_name, environment, environment_kind, created_at] properties: id: { type: string } status: { type: string } display_status: type: string description: A frase que a interface mostra; o status interno é vocabulário que o usuário nunca aceitou aprender. project_id: { type: string } project_name: { type: string } service_id: { type: string } service_name: { type: string } environment: { type: string } environment_kind: { type: string } revision: type: [integer, 'null'] description: Null enquanto a revisão não foi comprometida. commit: { type: [string, 'null'] } commit_ref: { type: [string, 'null'] } repository: { type: [string, 'null'] } triggered_by: { type: [object, 'null'], additionalProperties: true } created_at: { type: string, format: date-time } finished_at: { type: [string, 'null'], format: date-time } duration_seconds: type: [number, 'null'] description: Null enquanto não terminou — tempo decorrido não é duração. release_id: type: [string, 'null'] description: A release que esta publicação ativou; null se não ativou. rollback_eligible: type: boolean description: | Se a release desta publicação pode ser destino de rollback agora. Decidido pelo servidor, pela mesma regra que o rollback aplica (ver `Release.rollback_eligible`). Para restaurar, envie este `release_id` ao rollback — sem ele, o servidor escolhe a elegível mais recente, que pode não ser esta. rollback_ineligible_reason: type: [string, 'null'] enum: [no_release, active, same_revision_active, availability_unconfirmed, bundle_unavailable, null] description: | Por que `rollback_eligible` é `false`; `null` quando é `true`. `no_release` — a publicação não chegou a ativar uma versão. Os demais valores são os de `Release.rollback_ineligible_reason`. error: oneOf: - $ref: '#/components/schemas/Problem' - type: 'null' ProjectOverview: type: object required: [project, state, display_state, environments, recent_deployments, capabilities] properties: project: { $ref: '#/components/schemas/Project' } state: { type: string } display_state: { type: string } production: oneOf: - $ref: '#/components/schemas/ProjectEnvironment' - type: 'null' environments: type: array items: { $ref: '#/components/schemas/ProjectEnvironment' } recent_deployments: type: array items: { $ref: '#/components/schemas/DeploymentListItem' } capabilities: type: array description: Capacidades efetivamente disponíveis agora, nunca as planejadas. items: type: object required: [key, label, available] properties: key: { type: string } label: { type: string } available: { type: boolean } ProjectEnvironment: type: object required: [id, name, kind, region, state, display_state, evidence_level, services] properties: id: { type: string } name: { type: string } kind: { type: string, enum: [development, staging, production, preview] } region: { type: string } state: type: string # O estado do ambiente considera a rota E os deployments. A rota # sozinha não distingue "endereço reservado na criação" (ADR-021) de # "publicado" — foi o defeito do card que dizia "Publicado" ao lado # de um header "Ainda não publicado". enum: [unpublished, pending_edge, active, never_deployed, deploying, failed, active_deploying, active_last_failed] display_state: { type: string } hostname: { type: [string, 'null'] } url: { type: [string, 'null'] } evidence_level: type: string enum: [none, local, api_server, runtime, public_edge] description: Valor cru, para o painel técnico. A interface o traduz. services: type: array items: type: object required: [id, name, type, status, display_status, desired_generation, unpublished_changes] properties: id: { type: string } name: { type: string } type: { type: string } status: { type: string } display_status: { type: string } hostname: { type: [string, 'null'] } desired_generation: { type: integer } active_generation: { type: [integer, 'null'] } current_revision: { type: [integer, 'null'] } artifact_digest: { type: [string, 'null'] } commit: { type: [string, 'null'] } unpublished_changes: { type: boolean } active_deployment: oneOf: - $ref: '#/components/schemas/DeploymentListItem' - type: 'null' last_deployment: oneOf: - $ref: '#/components/schemas/DeploymentListItem' - type: 'null' DeploymentTimeline: type: object required: [deployment_id, status, stages] properties: deployment_id: { type: string } status: { type: string } stages: type: array items: type: object required: [checkpoint, label, state] properties: checkpoint: type: string enum: - source_resolved - artifact_published - artifact_verified - revision_committed - bundle_compiled - bundle_applied - deployment_verified - revision_available - revision_activated description: Os nove estágios reais do motor, e nada além deles. label: { type: string } state: type: string enum: [pending, running, done, failed, skipped] description: | `skipped` é o estágio que não vai acontecer nesta execução — um rollback não constrói nem compila nada. reached_at: { type: [string, 'null'], format: date-time } duration_seconds: type: [number, 'null'] description: Desde o estágio anterior alcançado. Null no primeiro. message: { type: [string, 'null'] } action: { type: [string, 'null'] } details: type: object additionalProperties: true description: | Fatos técnicos autorizados (commit, digest, revisão, geração). É uma projeção filtrada do diário do motor: a lista de objetos aplicados não sai daqui. error: oneOf: - $ref: '#/components/schemas/Problem' - type: 'null' ProjectDomain: type: object required: [hostname, environment_id, environment, state, display_state, tls, routing, evidence_level] properties: hostname: { type: string } url: { type: [string, 'null'] } environment_id: { type: string } environment: { type: string } service_id: { type: [string, 'null'] } service_name: { type: [string, 'null'] } state: type: string enum: [unpublished, pending_edge, active, releasing] description: | `releasing` é o endereço ANTERIOR de uma troca: ele responde até a publicação com o endereço novo ficar pronta, e então é liberado. Nesse estado nada é afirmado sobre ele — `tls`, `routing` e `evidence_level` saem falsos e `none`. display_state: { type: string } tls: type: boolean description: O que a publicação provou, não uma sondagem feita agora. routing: { type: boolean } evidence_level: type: string enum: [none, local, api_server, runtime, public_edge] observed_at: { type: [string, 'null'], format: date-time } recommended_action: type: [string, 'null'] description: Null quando não há o que fazer. ActivityItem: type: object required: [id, at, kind, message, status] properties: id: { type: string } at: { type: string, format: date-time } kind: { type: string } message: type: string description: Frase pronta, em português. Não é template para a interface preencher. status: { type: string } project_id: { type: string } project_name: { type: string } service_id: { type: string } service_name: { type: string } actor: type: [string, 'null'] description: Null para o que a plataforma fez sozinha. Organization: type: object required: [id, name, slug, status, plan, role, created_at] properties: id: { type: string } name: { type: string } slug: { type: string, description: Identificador legível. Nunca é chave primária. } status: { type: string, enum: [active, suspended, terminated] } plan: { type: string } role: type: string enum: [admin, operator, member] description: | O papel de QUEM ESTÁ LENDO, e não um atributo da organização. Vem aqui porque toda tela que lista organizações precisa dele para decidir o que oferecer, e uma segunda chamada por organização seria a forma cara de responder a mesma pergunta. São três, e a fronteira entre eles não é "quanto poder": admin opera, e governa quem entra e o que se cobra operator opera aplicação e infraestrutura — publica, escala, troca DNS, apaga recurso — e não decide quem tem acesso member consulta O papel vale POR ORGANIZAÇÃO: a mesma identidade pode ser `admin` numa e `member` noutra. Por isso ele não existe como claim do token — uma claim global não teria onde guardar essa variação. A autoridade é o banco do Zero; o provedor de identidade responde quem é a pessoa, não o que ela pode. created_at: { type: string, format: date-time } SpaceAccess: type: object description: | Uma pessoa que opera este Space, e de onde vem o acesso dela. required: [subject, role, origin] properties: subject: { type: string } email: { type: string } role: { type: string, enum: [admin, operator, member] } origin: type: string enum: [project, organization] description: | `project` — recebeu este Space explicitamente; a linha é a autorização, e ela pode ser removida. `organization` — alcança todos os Spaces pelo papel; não há atribuição, e limitar essa pessoa exige mudar o papel. OrganizationCreate: type: object required: [name] properties: name: type: string minLength: 1 maxLength: 100 description: Nome de exibição. slug: type: string pattern: '^[a-z][a-z0-9-]{1,38}[a-z0-9]$' description: | De 3 a 40 caracteres: minúsculas, números e hífen. Único GLOBALMENTE. Sem valor, é derivado do nome; um nome que não produz slug válido é recusado como erro de campo, nunca aceito com um slug estranho. region: type: string description: | Região padrão da organização. Os ambientes criados sem região explícita herdam esta. Sem valor, vale a região padrão da instalação. OrganizationMember: type: object required: [subject, email, role, added_at] properties: subject: { type: string, description: Identificador do usuário no provedor OIDC. } email: type: string description: | Rótulo, nunca identidade — quem é membro é o par (issuer, subject). String vazia quando o provedor não informou; nunca `null`, porque a tela usa este campo como texto de fallback do nome. name: type: [string, 'null'] description: | Nome de exibição do provedor. `null` quando ele nunca informou um — derivá-lo do e-mail seria adivinhar. role: { type: string, enum: [admin, operator, member] } added_at: { type: string, format: date-time } ApplicationCredential: type: object required: [id, name, prefix, principal, scope_mode, includes_future_projects, projects, role, allowed_operations, expires_at, created_at, created_by, active] properties: id: { type: string, description: "`acr_…`" } name: { type: string, maxLength: 60 } description: { type: string, maxLength: 280 } prefix: type: string description: | O começo do segredo. Identifica qual credencial está em qual cofre sem publicar o resto — o valor inteiro não é recuperável. principal: type: string description: | O `sub` com que ela aparece na auditoria, como `actor`, com `actor_type = service_account`. É o mesmo valor de `id`: não há segunda verdade sobre quem agiu. scope_mode: type: string enum: [all_projects, selected_projects] includes_future_projects: type: boolean description: | Verdadeiro para `all_projects`. Publicado como campo próprio porque é a consequência que mais se erra ao escolher o modo, e uma tela que só mostrasse `scope_mode` obrigaria quem lê a saber isso de cor. projects: type: array items: { type: string } description: Vazio em `all_projects`; a lista fechada em `selected_projects`. role: type: string enum: [operator, member] description: | Derivado do alcance, não escolhido: `all_projects` → `operator`, `selected_projects` → `member`. Nunca `admin` — governança não é delegável a uma máquina. allowed_operations: type: array items: { type: string } expires_at: { type: string, format: date-time } revoked_at: { type: string, format: date-time } last_used_at: { type: string, format: date-time } rotated_at: { type: string, format: date-time } created_at: { type: string, format: date-time } created_by: type: string description: | Procedência, não posse: quem respondeu pela concessão. A saída dessa pessoa NÃO revoga a credencial. active: { type: boolean } ApplicationCredentialWithSecret: allOf: - $ref: '#/components/schemas/ApplicationCredential' - type: object required: [secret, secret_warning] properties: secret: type: string description: | O valor em claro. Aparece **uma vez**, aqui. Não há caminho de recuperação: quem não copiou rotaciona. secret_warning: { type: string } ApplicationCredentialCreate: type: object required: [name, scope_mode, allowed_operations, expires_at] properties: name: { type: string, maxLength: 60 } description: { type: string, maxLength: 280 } scope_mode: type: string enum: [all_projects, selected_projects] projects: type: array items: { type: string } description: | Obrigatório e não vazio em `selected_projects`; recusado em `all_projects`, onde uma lista seria ignorada. allowed_operations: type: array items: type: string enum: [read, write, deploy, manage_secrets, manage_domains, manage_data, delete_resource] description: | Governança (`manage_members`, `manage_billing`) não entra: ela é recusada com a razão, na criação, e não silenciosamente removida. expires_at: type: string format: date-time description: | Obrigatório e sem padrão. Entre 1 hora e 1 ano — credencial sem prazo é credencial que ninguém revoga. ApplicationCredentialUpdate: type: object properties: projects: type: array items: { type: string } description: Substitui a lista. Só em `selected_projects`. allowed_operations: type: array items: { type: string } Alert: type: object required: [id, name, condition, window, severity, state, project_id, environment, started_at, last_evaluated_at, observed_value, expected_value, acknowledged_at, evidence_link] properties: id: { type: string } name: { type: string } condition: type: string description: A frase que explica a regra, em linguagem de produto. window: { type: string } threshold: { type: [string, number, 'null'] } severity: { type: string, enum: [critical, warning, info] } state: type: string enum: [firing, resolved, pending, disabled] project_id: { type: [string, 'null'] } environment: { type: [string, 'null'] } started_at: type: [string, 'null'] format: date-time description: Quando o FATO que causou o alerta aconteceu. last_evaluated_at: type: [string, 'null'] format: date-time description: | O instante desta avaliação. Os alertas são função do estado atual, avaliada na leitura. observed_value: { type: [number, 'null'] } unit: type: [string, 'null'] description: | Unidade dos dois números — `réplicas`, `nameservers`, `endereços`. Sem ela a tela mostra "1 · 4" e quem lê não sabe 1 de quê. expected_value: { type: [number, 'null'] } acknowledged_at: type: [string, 'null'] format: date-time description: | Sempre null nesta versão: reconhecer exigiria persistir o alerta, e eles não são persistidos — são derivados do estado a cada leitura. evidence_link: type: [string, 'null'] description: Rota do console que mostra o que sustenta o alerta. AlertList: type: object required: [items, rules_configured] properties: items: type: array items: { $ref: '#/components/schemas/Alert' } rules_configured: type: boolean description: | Distingue "nada disparando" (notícia boa) de "ninguém configurou nada" (trabalho parado). As regras são do produto e não do usuário, então este campo é sempre verdadeiro — dizer o contrário mandaria a pessoa procurar uma tela de configuração que não existe. ProjectSecurity: type: object required: [checklist, tls, public_exposure, rate_limits_configured, last_verified_at] properties: checklist: type: array items: { $ref: '#/components/schemas/SecurityCheck' } tls: type: object required: [active, expires_at, issuer] properties: active: { type: boolean } expires_at: type: [string, 'null'] format: date-time description: | Null enquanto o control plane não observar o objeto do cert-manager. Preencher com o que a plataforma "costuma" fazer seria inventar a data que mais importa. issuer: { type: [string, 'null'] } public_exposure: type: boolean description: | O projeto reservou endereço público. É intenção de exposição, não varredura de portas. rate_limits_configured: { type: boolean } last_verified_at: type: [string, 'null'] format: date-time description: | Quando o último FATO que sustenta o relatório mudou — não a hora em que a tela foi aberta. SecurityCheck: type: object required: [key, label, ok, source, action, action_href] properties: key: { type: string } label: { type: string } ok: { type: boolean } source: type: string description: | De onde veio a afirmação. Nunca vazio — um item sem procedência é uma opinião com aparência de fato. action: { type: [string, 'null'] } action_href: type: [string, 'null'] description: Rota do CONSOLE para resolver o item, não da API. RealmUser: type: object description: | Uma pessoa do realm da organização. A identidade é do provedor; o Zero só a lê — e o que ele guarda dela é `access`. required: [id, username, email, first_name, last_name, name, enabled, email_verified, password_setup_pending, access] properties: id: type: string description: Identificador do usuário no realm (o `sub` do token dele). username: { type: string } email: { type: [string, 'null'] } first_name: { type: [string, 'null'] } last_name: { type: [string, 'null'] } name: type: [string, 'null'] description: Nome e sobrenome juntos. `null` quando o realm não tem nenhum dos dois. enabled: type: [boolean, 'null'] description: | `false` é conta desabilitada NO REALM: o provedor recusa a entrada, e o Zero não concede acesso a ela. `null` quando o diretório não pôde ser lido e a linha veio só do banco do Zero. email_verified: { type: [boolean, 'null'] } password_setup_pending: type: [boolean, 'null'] description: | A pessoa ainda não definiu a senha (ação obrigatória pendente no realm). É o que habilita o reenvio do e-mail oficial. created_at: { type: [string, 'null'], format: date-time } access: { $ref: '#/components/schemas/UserAccess' } RealmDirectory: type: object description: De qual realm a lista veio, e se o Zero consegue lê-lo. required: [realm, issuer, available, missing_permissions] properties: realm: type: [string, 'null'] description: | O realm de identidade ao qual a organização está vinculada — é dele, e só dele, que a lista vem. Não existe realm do produto: cada organização usa o seu, e o realm vem do vínculo da organização, nunca do pedido. issuer: { type: [string, 'null'] } available: type: boolean description: | `false` quando o Zero não consegue ler o diretório — e então `items` traz só quem já tem acesso, lido do banco do Zero. reason: type: [string, 'null'] enum: [no_realm_bound, realm_not_delegated, integration_incomplete, realm_not_active, provider_denied, provider_unavailable, null] description: | `realm_not_delegated` — este realm ainda não delegou ao Zero a administração de usuários (decisão do dono do realm, não defeito); a ação é delegar ESTE realm. `provider_denied` — delegou, e o provedor recusou uma permissão que devia existir. message: { type: [string, 'null'] } missing_permissions: type: array description: O que o Zero precisaria no realm para listar, criar e conceder. items: { type: string } can_create_users: type: boolean description: Se criar usuário NESTE realm é possível agora. RealmUserPage: type: object required: [directory, items, first, max, has_more] properties: directory: { $ref: '#/components/schemas/RealmDirectory' } items: type: array items: { $ref: '#/components/schemas/RealmUser' } first: { type: integer } max: { type: integer } has_more: type: boolean description: Há mais resultados depois desta página. UserAccess: type: object description: | O que uma pessoa do realm pode fazer nesta organização. É a ÚNICA coisa que o Zero guarda sobre ela, além do identificador. required: [user_id, state, role, project_ids] properties: user_id: { type: string } state: type: string enum: [none, granted] description: '`none` é o estado comum: existir no realm não dá acesso ao Zero.' role: type: [string, 'null'] enum: [admin, operator, member, null] project_ids: type: array description: Os Spaces de quem tem papel `member`. Vazio para `admin` e `operator`, que alcançam todos. items: { type: string } granted_at: { type: [string, 'null'], format: date-time } granted_by: { type: [string, 'null'] } notification: oneOf: - $ref: '#/components/schemas/AccessNotification' - type: 'null' UserAccessInput: type: object required: [role] properties: role: { type: string, enum: [admin, operator, member] } project_ids: type: array description: Obrigatório (ao menos um) para `member`; ignorado para os outros papéis. items: { type: string } AccessNotification: type: object description: | O e-mail do Zero que avisa a pessoa. `sent` quer dizer que o servidor de e-mail ACEITOU a mensagem — não que ela foi lida. required: [kind, state, attempts] properties: kind: { type: string, enum: [access_granted, account_created] } state: { type: string, enum: [queued, sending, sent, failed, suppressed] } attempts: { type: integer } error: type: [string, 'null'] enum: [email_missing, smtp_unavailable, smtp_rejected, not_configured, rate_limited, access_revoked, null] description: | `access_revoked` — o acesso foi revogado antes de o aviso sair, e ele não sai mais: um "acesso concedido" depois da revogação contaria uma história falsa. sent_at: { type: [string, 'null'], format: date-time } RealmUserCreate: type: object required: [email, first_name, last_name, role] properties: email: { type: string, format: email, maxLength: 254 } first_name: { type: string, minLength: 1, maxLength: 100 } last_name: { type: string, minLength: 1, maxLength: 100 } username: type: string maxLength: 100 description: Sem valor, o nome de usuário é o e-mail. role: { type: string, enum: [admin, operator, member] } project_ids: type: array items: { type: string } RealmUserProvisioning: type: object description: | Onde a criação está. Cada passo é independente e observável: identidade criada sem membership é um estado real, e aparece como tal. required: [id, state, user_id, steps, replayed] properties: id: { type: string } state: type: string enum: [requested, user_created, access_granted, activation_requested, ready, failed] user_id: { type: [string, 'null'] } replayed: type: boolean description: A chave já tinha sido usada; esta resposta retoma o mesmo pedido. error: type: [string, 'null'] description: O código do passo que falhou, quando algum falhou. steps: type: object required: [identity_created, membership_created, zero_email, password_setup_email] properties: identity_created: { type: boolean } membership_created: { type: boolean } zero_email: oneOf: - $ref: '#/components/schemas/AccessNotification' - type: 'null' password_setup_email: $ref: '#/components/schemas/PasswordSetupRequest' user: oneOf: - $ref: '#/components/schemas/RealmUser' - type: 'null' PasswordSetupRequest: type: object description: | O pedido ao provedor de identidade para enviar o e-mail OFICIAL de definição de senha. O link é do provedor; o Zero não o vê. required: [state] properties: state: { type: string, enum: [pending, requested, failed] } error: type: [string, 'null'] enum: [email_delivery_failed, email_missing, user_disabled, provider_denied, provider_unavailable, client_invalid, null] requested_at: { type: [string, 'null'], format: date-time } Project: type: object required: [id, organization_id, name, environment_count, created_at, lifecycle_state] properties: id: { type: string } organization_id: { type: string } name: { type: string } display_name: { type: [string, 'null'] } description: { type: [string, 'null'] } environment_count: { type: integer } created_at: { type: string, format: date-time } lifecycle_state: type: string enum: [active, deletion_requested, deleting, deletion_failed] description: | `active` no uso normal. Os outros três só aparecem ENQUANTO uma exclusão corre ou depois de ela falhar — é o que deixa a tela dizer "excluindo" ou "a exclusão falhou" em vez de fingir que acabou. Projeto excluído não aparece em leitura nenhuma: não existe `deleted` aqui. deletion_operation_id: type: [string, 'null'] description: A operação `project.delete` em curso ou falhada; null quando `active`. environments: type: array items: { $ref: '#/components/schemas/Environment' } description: | Vem preenchido só na CRIAÇÃO e na leitura de um projeto (`GET /projects/{projectId}`) — é o que evita uma segunda chamada logo depois de criar. Ausente na listagem, onde `environment_count` responde a pergunta que a tela faz. ProjectDeleteRequest: type: object properties: reason: type: string maxLength: 500 description: Por que o projeto está sendo excluído. Vai para a auditoria. ProjectCreate: type: object required: [name] properties: name: type: string pattern: '^[a-z][a-z0-9-]{1,38}[a-z0-9]$' description: Minúsculas, números e hífen. Único dentro da organização. display_name: { type: string, maxLength: 100 } description: { type: string, maxLength: 500 } create_default_environments: type: boolean default: true description: | Cria o ambiente `production` na mesma transação. Um projeto sem ambiente nenhum não aceita serviço, e descobrir isso só na criação do serviço seria uma etapa a mais sem motivo. region: type: string description: Região dos ambientes criados junto. Sem valor, a da organização. application_zone_id: type: string description: | Zona de domínio próprio escolhida na criação (ADR-023). Vazio mantém o domínio da plataforma. É conferida AQUI, e não na publicação: aceitar uma zona que não serve produziria um projeto cuja primeira publicação falha, longe da causa. hostname_label: type: string description: | O rótulo do endereço público escolhido na criação. É INTENÇÃO, não reserva: a reserva exige a tripla completa (projeto, ambiente, serviço), e o serviço só nasce na primeira publicação. A disponibilidade é conferida agora, com a possibilidade real de o nome ter sido levado nesse meio-tempo — o que é dito ao usuário quando acontece. organization_id: type: string description: | Aceito e IGNORADO. O escopo de tenant vem do token, nunca do corpo. Recusar o campo com `422` diria "campo desconhecido" para algo que é conhecido e deliberadamente sem efeito. Environment: type: object required: [id, project_id, name, kind, status] properties: id: { type: string } project_id: { type: string } name: { type: string } kind: { type: string, enum: [development, staging, production, preview] } region: { type: string } status: type: string enum: [provisioning, ready, degraded, deleting, deleted] description: | O estado OBSERVADO do ambiente, derivado do que há nele — nunca um campo que alguém escreveu e esqueceu. `provisioning` — há serviço declarado e nenhuma release ativada. `ready` — o ambiente existe e o que há nele está saudável. Inclui o ambiente VAZIO: sem serviço, nada falhou e nada está sendo preparado. `degraded` — há serviço publicado que não está saudável. É o que separa "ainda não subiu" de "subiu e quebrou". `deleting` / `deleted` — ciclo de vida. Estes vencem a evidência: um ambiente em remoção não é saudável porque os serviços dele ainda respondem. A EXPOSIÇÃO não entra aqui. Um ambiente só com worker não publica endereço nenhum e está perfeitamente `ready` — quem responde sobre o tráfego que chega é `GET /environments/{id}/route`, e a pergunta dele é outra. As duas superfícies nunca se contradizem porque derivam da mesma evidência. display_status: { type: string } service_count: { type: integer } created_at: { type: string, format: date-time } EnvironmentCreate: type: object required: [name] properties: name: type: string pattern: '^[a-z][a-z0-9-]{1,30}[a-z0-9]$' kind: type: string enum: [development, staging, production, preview] default: development description: Sem valor, `development`. region: type: string description: Região do catálogo. Sem valor, usa a região padrão da organização. organization_id: type: string description: Aceito e IGNORADO. O escopo sai do projeto endereçado. ServiceType: type: string enum: [web, private_service, worker, scheduled_job] description: | `web` recebe tráfego público pelo gateway. `private_service` só é alcançável dentro do ambiente. `worker` roda continuamente sem rota. `scheduled_job` está no vocabulário e **esta instalação ainda não o executa**: a criação é recusada com `SERVICE_TYPE_UNAVAILABLE`. Ele foi aceito por meses e o compilador de workload nunca leu o `schedule` — o que subia era um workload contínuo, e um trabalho que devia rodar uma vez por dia rodava sem parar, consumindo a quota do ambiente. Recusar é a resposta honesta enquanto o compilador não produzir um objeto agendado. Source: type: object description: | A origem como uma LEITURA a descreve — o serviço a HERDA da origem do projeto. `repository` vem na forma canônica da origem (`dono/repo`, ou `grupo/subgrupo/projeto` no GitLab), sem host. Para CONFIGURAR a origem use `PUT /projects/{projectId}/source` (`ProjectSourceInput`); para publicar ou conferir, `SourceAdjustment` — que não aceita repositório. oneOf: - type: object required: [type, repository] properties: type: { const: git } repository: { type: string, examples: ["acme/app"] } branch: { type: string } subdirectory: { type: [string, 'null'], description: Caminho da aplicação em monorepo. } - type: object required: [type, image] properties: type: { const: image } image: type: string description: Imagem OCI de origem autorizada. Referência por digest é preferida. SourceAdjustment: type: object description: | O que uma publicação — ou a conferência dela — pode AJUSTAR na origem do projeto: a referência e a pasta. Nada mais. O repositório é da ORIGEM DO PROJETO (`PUT /projects/{projectId}/source`) e não viaja aqui. `repository` ou `image` neste objeto são recusados com `422` `VALIDATION_FAILED` no campo `source.repository` — para publicar de outro repositório, configure a origem do projeto, ou crie outro projeto. Isto não é o mesmo objeto que `ProjectSourceInput` só porque os dois falam de origem: um configura de onde o projeto publica, o outro escolhe o que desta origem vai para o ar agora. additionalProperties: false properties: branch: type: string description: | Branch, tag ou commit desta publicação. Ausente, vale a referência da origem do projeto — e é por isso que não há `default` aqui: um `main` implícito passaria por cima da referência configurada. subdirectory: type: [string, 'null'] description: | A pasta, dentro do repositório do projeto, que este serviço constrói. Numa publicação ela FICA gravada como a pasta do serviço (`source_path`): a publicação seguinte, sem corpo, usa a mesma. type: type: string enum: [git] deprecated: true description: Aceito e ignorado. A forma da origem é a do projeto. SourceValidateRequest: type: object description: | Sem corpo, ou sem `source`, confere a origem do projeto na referência dela e na pasta do serviço. properties: source: { $ref: '#/components/schemas/SourceAdjustment' } ScaleMode: type: string enum: [fixed, automatic] description: | Quem decide o número de instâncias. `fixed` — o número é o que você escrever. `automatic` — o número sai da utilização observada, entre `min_instances` e `max_instances`. Um campo do modo automático enviado num serviço `fixed` é **recusado**, nunca ignorado: gravar uma intenção que nada executa é o defeito que esta enumeração existe para tornar impossível. Não há escala a zero. Uma aplicação online com zero instâncias é uma aplicação fora do ar sem ninguém ter dito isso. ScaleSpec: type: object description: | A política de escala do serviço. Ela vive no estado desejado do SERVIÇO, e não da revisão: publicar uma versão nova ou voltar para a anterior não apaga a intenção de escala. properties: size: type: string description: 'Tamanho do catálogo (ex.: `small`). Nunca CPU/memória crus.' mode: allOf: [{ $ref: '#/components/schemas/ScaleMode' }] description: 'Ausente mantém o modo atual do serviço.' min_instances: type: integer minimum: 1 maximum: 50 default: 1 description: | No modo `fixed`, o número de instâncias. No `automatic`, o piso — abaixo dele o autoscaler não desce. max_instances: type: integer minimum: 1 maximum: 50 description: | Só no modo `automatic`: o teto. No `fixed` não existe teto separado, e enviá-lo é recusado. O teto também é limitado pelo ORÇAMENTO do ambiente, que depende do tamanho de cada instância: 40 instâncias de 100m de CPU cabem, e 40 de 1 CPU não. A recusa diz quantas caberiam, e `ScaleView.max_allowed_instances` traz o mesmo número na leitura. cpu_target_percent: type: integer minimum: 1 maximum: 100 description: | Só no modo `automatic`: percentual da CPU RESERVADA por instância que o autoscaler tenta manter. O modo automático exige pelo menos um alvo — sem métrica ele não tem como decidir nada. memory_target_percent: type: integer minimum: 1 maximum: 100 description: | Só no modo `automatic`: percentual da memória reservada por instância. Com os dois alvos declarados vale a MAIOR das duas recomendações; o Zero não calcula média entre elas. ScaleView: type: object required: [mode, min_instances, max_instances, max_allowed_instances] properties: size: { type: string } mode: { $ref: '#/components/schemas/ScaleMode' } min_instances: { type: integer } max_instances: type: integer description: 'No modo `fixed`, igual a `min_instances`: não há intervalo.' cpu_target_percent: { type: integer } memory_target_percent: { type: integer } max_allowed_instances: type: integer description: | O teto do ambiente para o tamanho atual da instância. Está aqui para a interface poder dizer até onde dá ANTES de alguém digitar um número que será recusado. ServiceCreate: type: object required: [environment_id, name] properties: environment_id: type: string description: | O ambiente que recebe o serviço. Ele é o recurso endereçado — é dele que sai a organização, do mesmo jeito que uma rota com `{serviceId}` deduz do serviço. Ambiente de outra organização responde `404`, e não `403`: distinguir os dois revelaria a existência do recurso. name: type: string pattern: '^[a-z][a-z0-9-]{1,38}[a-z0-9]$' description: | Único dentro do ambiente. Nomes reservados pela plataforma são recusados aqui, no caminho de escrita — recusar só na compilação seria recusar depois de o serviço existir. type: allOf: - $ref: '#/components/schemas/ServiceType' default: web description: Sem valor, `web`. source_path: { type: string, description: "A pasta que este serviço constrói dentro do repositório do projeto. O repositório é do projeto; para publicar de outro, crie outro projeto." } source: allOf: - $ref: '#/components/schemas/SourceAdjustment' deprecated: true description: | Forma antiga do recorte: só `subdirectory` é lido, e vira `source_path`. `branch` é ignorado; `repository` ou `image` são recusados com `422`. Prefira `source_path`. port: type: [integer, 'null'] minimum: 1 maximum: 65535 description: Sem valor, o Zero detecta. command: { type: [string, 'null'], description: Sobrescreve o comando detectado. } schedule: type: [string, 'null'] description: Obrigatório para `scheduled_job`. Expressão cron de 5 campos, UTC. env: type: object additionalProperties: { type: string } description: Variáveis não sensíveis. Valores sensíveis vão em secrets. secret_refs: type: array items: { type: string } description: Nomes de secrets do ambiente a injetar. scale: { $ref: '#/components/schemas/ScaleSpec' } organization_id: type: string description: Aceito e IGNORADO. O escopo sai do ambiente endereçado. ServiceUpdate: type: object properties: source_path: { type: string, description: "A pasta que este serviço constrói dentro do repositório do projeto. O repositório é do projeto; para publicar de outro, crie outro projeto." } source: allOf: - $ref: '#/components/schemas/SourceAdjustment' deprecated: true description: | Forma antiga do recorte: só `subdirectory` é lido, e vira `source_path`. `branch` é ignorado; `repository` ou `image` são recusados com `422`. Prefira `source_path`. port: { type: [integer, 'null'] } command: { type: [string, 'null'] } schedule: { type: [string, 'null'] } env: type: object additionalProperties: { type: string } secret_refs: type: array items: { type: string } scale: { $ref: '#/components/schemas/ScaleSpec' } organization_id: type: string description: Aceito e IGNORADO. O escopo sai do serviço endereçado. Service: type: object required: [id, environment_id, name, type, status, display_status, unpublished_changes, desired_generation, verification_level, created_at] properties: id: { type: string } environment_id: { type: string } name: { type: string } type: { $ref: '#/components/schemas/ServiceType' } status: type: string enum: [created, deploying, online, offline, deleted] description: | O CICLO DE VIDA do serviço. Cada transição tem autor: alguém criou, publicou, pôs no ar, tirou do ar ou excluiu. SAÚDE não está aqui, e não é omissão. Saúde muda sem ninguém escrever — um serviço `online` cujas réplicas caem de madrugada não recebe atualização nenhuma —, então um campo persistido que pretendesse respondê-la estaria errado entre o instante em que a realidade muda e o instante em que alguém o atualiza. Quem responde sobre saúde é a evidência da publicação (`Deployment.verification_level`, cujo degrau `runtime` significa réplicas prontas) e as métricas de execução. `degraded`, `failed` e `deleting` estavam aqui e nunca foram escritos por nada: saíram do banco na migration 00067. display_status: type: string examples: ["Online", "Atenção necessária", "Não foi possível publicar"] url: { type: [string, 'null'], description: URL pública. Apenas para `web`. } current_release: { $ref: '#/components/schemas/ReleaseRef' } pending_revision: type: [string, 'null'] description: Revision criada por mudança de configuração e ainda não publicada. source_path: { type: string, description: "A pasta que este serviço constrói dentro do repositório do projeto. O repositório é do projeto; para publicar de outro, crie outro projeto." } source: { $ref: '#/components/schemas/Source' } scale: { $ref: '#/components/schemas/ScaleView' } port: { type: [integer, 'null'], description: Porta declarada. Null quando vale o padrão da plataforma. } command: { type: [string, 'null'], description: Comando declarado. Null quando vale o da imagem. } hostname: { type: [string, 'null'], description: Endereço reservado do serviço (ADR-021). } observed: type: [object, 'null'] description: | A última amostra do runtime — o que o cluster de fato executa. Null quando o amostrador nunca viu o serviço; nunca zero. Vem junto da projeção para que a tela não precise de uma requisição por cartão. properties: replicas_desired: { type: integer } replicas_ready: { type: integer } restarts: { type: integer } cpu_used_millicores: { type: [integer, 'null'] } memory_bytes_used: { type: [integer, 'null'] } autoscaler: type: [object, 'null'] description: | POR QUE o número de instâncias é o que é. Null quando a escala é fixa — o que é a maioria, e não uma ausência de dado. `display_state` vem pronto, já em linguagem de produto. Devolver `FailedGetResourceMetric` para o cliente traduzir criaria um dicionário do vocabulário do Kubernetes dentro de cada consumidor, que envelhece a cada versão menor do cluster. properties: state: { type: string, enum: [deciding, limited, stalled] } display_state: { type: string } observed_at: { type: string, format: date-time } unpublished_changes: type: boolean description: | A configuração declarada difere da que está no ar. É a diferença entre "alteramos para você" e "alteramos e aplicamos", e escondê-la faria a plataforma parecer ter feito algo que não fez. desired_generation: type: integer format: int64 description: A geração do estado desejado. active_generation: type: [integer, 'null'] format: int64 description: | A geração que está no ar. As duas juntas são a forma honesta de dizer "a plataforma alcançou o que foi pedido?". verification_level: { $ref: '#/components/schemas/VerificationLevel' } created_at: { type: string, format: date-time } ScaleRequest: allOf: [{ $ref: '#/components/schemas/ScaleSpec' }] description: | O mesmo corpo do campo `scale` de `PATCH /services/{serviceId}`. A diferença é quando vale: esta rota APLICA — cria uma publicação e devolve uma operação para acompanhar; o PATCH declara, e a declaração entra na publicação seguinte. PublicationAttempt: type: object required: [id, kind, reason, status, display_status, service_id, service_name, environment, source, checkpoint, deployment_id, error, created_at] properties: id: { type: string, description: Identificador da operação. } kind: { type: string, enum: [deployment.run, deployment.rollback] } reason: { type: string } status: { type: string } display_status: { type: string } actor: { type: [string, 'null'] } service_id: { type: string } service_name: { type: string } environment: { type: string } source: type: [object, 'null'] properties: repository: { type: string } branch: { type: string } path: { type: [string, 'null'] } checkpoint: type: [string, 'null'] description: O último alcançado — até onde a tentativa chegou. Null quando morreu antes do primeiro. deployment_id: type: [string, 'null'] description: O Deployment que nasceu desta tentativa, quando `revision_committed` aconteceu. error: oneOf: - $ref: '#/components/schemas/Problem' - type: 'null' created_at: { type: string, format: date-time } started_at: { type: [string, 'null'], format: date-time } finished_at: { type: [string, 'null'], format: date-time } ResourcesUpdate: type: object description: | CPU em milicores, memória em MiB. "Reservado" é o que o serviço tem garantido (request); "máximo" é o teto que ele pode encostar (limit). Campo ausente mantém o valor atual. properties: cpu_request_millicores: { type: integer, minimum: 1 } cpu_limit_millicores: { type: integer, minimum: 1 } memory_request_mib: { type: integer, minimum: 1 } memory_limit_mib: { type: integer, minimum: 1 } restore_defaults: type: boolean description: Remove os overrides e volta aos padrões da plataforma. EffectiveResources: type: object required: [cpu_request_millicores, cpu_limit_millicores, memory_request_mib, memory_limit_mib, origin, platform_max_cpu_millicores, platform_max_memory_mib] properties: cpu_request_millicores: { type: integer } cpu_limit_millicores: { type: integer } memory_request_mib: { type: integer } memory_limit_mib: { type: integer } origin: type: string enum: [plataforma, servico] description: De onde vêm os números — sem isto a pessoa não sabe se vê o que escolheu ou o que a plataforma escolheu por ela. platform_max_cpu_millicores: { type: integer } platform_max_memory_mib: { type: integer } ProjectLimits: type: object required: [project_id, platform_defaults, items] properties: project_id: { type: string } platform_defaults: { $ref: '#/components/schemas/EffectiveResources' } items: type: array items: type: object required: [service_id, service_name, environment, environment_kind, desired, applied, pending, observed] properties: service_id: { type: string } service_name: { type: string } environment: { type: string } environment_kind: { type: string } desired: { $ref: '#/components/schemas/EffectiveResources' } applied: oneOf: - $ref: '#/components/schemas/EffectiveResources' - type: 'null' pending: { type: boolean } observed: type: [object, 'null'] description: A última amostra do runtime. Null quando o amostrador nunca viu o serviço — null, nunca zero. properties: cpu_used_millicores: { type: [integer, 'null'] } cpu_limit_millicores: { type: [integer, 'null'] } memory_used_bytes: { type: [integer, 'null'] } memory_limit_bytes: { type: [integer, 'null'] } replicas_ready: { type: integer } observed_at: { type: string, format: date-time } ReleaseRef: type: [object, 'null'] properties: id: { type: string } revision: { type: integer } artifact_digest: { type: string } activated_at: { type: string, format: date-time } SourceCheck: type: object required: [valid, provider, repository, mutable, subdirectory_verified, checked_at] properties: valid: type: boolean description: | Sempre `true` numa resposta `200`. Origem inalcançável não devolve `valid: false` — devolve `422` com o código de erro que diz o que houve, porque "não deu" sem motivo não ajuda ninguém a consertar. provider: { type: string, examples: [github, gitlab, bitbucket] } repository: { type: string, description: "O endereço normalizado, sem credencial." } ref: { type: string, description: A referência resolvida. } ref_name: { type: string, description: O nome legível do branch ou tag. } ref_kind: { type: string, enum: [branch, tag, commit] } mutable: type: boolean description: | Resolver a mesma referência de novo pode dar outro commit. Um branch anda; omitir isso faria toda divergência posterior parecer anomalia. commit: { type: string, description: O commit que a referência apontava agora. } subdirectory: { type: [string, 'null'], description: O caminho normalizado. } subdirectory_verified: type: boolean description: | Hoje sempre `false`. Provar que o subdiretório existe exigiria ler a árvore do repositório, e ler a árvore é publicar. checked_at: { type: string, format: date-time } EnvironmentRoute: type: object required: [environment_id, state, display_state, evidence] properties: environment_id: { type: string } hostname: type: [string, 'null'] description: | O endereço RESERVADO do ambiente. Null quando não há serviço `web` — e um endereço inventado seria pior que a ausência, porque não resolveria. url: { type: [string, 'null'], description: '`https://` + `hostname`, quando há.' } state: type: string enum: [unpublished, pending_edge, active] description: | `unpublished` — nada exposto ao mundo. `pending_edge` — workload publicado, borda ainda não provada. `active` — exige evidência `public_edge`: tráfego externo verificado. display_state: { type: string } evidence: type: object required: [verification_level, exposed_services, healthy_services, observed_at] properties: verification_level: { $ref: '#/components/schemas/VerificationLevel' } exposed_services: { type: integer } healthy_services: type: integer description: | Serviços expostos cuja release ativa teve a disponibilidade CONFIRMADA na publicação: réplicas prontas observadas, evidência `runtime` ou superior. Uma release ativada sem essa confirmação não conta. observed_at: { type: string, format: date-time } DeploymentLogPage: type: object required: [items, next_cursor] properties: items: type: array items: { $ref: '#/components/schemas/LogEntry' } next_cursor: type: [string, 'null'] description: | Só vem preenchido quando a página encheu. Devolver cursor numa página incompleta faria todo cliente pedir uma página vazia a mais. Release: type: object required: [id, service_id, revision, status, generation, activated_at, rollback_eligible, rollback_ineligible_reason] properties: id: { type: string } service_id: { type: string } revision: { type: integer, description: Sequencial e imutável por serviço. } status: type: string enum: [active, superseded] description: | A vigência de um RESULTADO. `active` — é a que serve agora; uma por serviço. `superseded` — deixou de servir porque outra tomou o lugar. `deactivated_at` diz quando. Não há estado de "revertida": num modelo append-only nada é desfeito. Um rollback cria uma release NOVA apontando para a revisão restaurada, e a que sai vira `superseded` como em qualquer avanço. O que distingue os dois é o `kind` da operação que originou o deployment — e só ele: `reason` é o texto de quem publicou, e um cliente pode escrever qualquer coisa nele. } artifact_id: { type: [string, 'null'] } artifact_digest: { type: [string, 'null'] } deployment_id: { type: [string, 'null'] } revision_ref: type: [string, 'null'] description: A revisão imutável que esta ativação carrega. generation: type: integer format: int64 description: A geração do serviço no instante da ativação. activated_at: { type: string, format: date-time } deactivated_at: { type: [string, 'null'], format: date-time } rollback_eligible: type: boolean description: | Se esta release pode ser destino de um rollback AGORA. Quem decide é o servidor, pelos fatos que ele registrou; o cliente apresenta. Não é saúde atual. Uma release substituída não está rodando, e não há como medir a saúde dela agora. O que a torna elegível é ter SERVIDO com confirmação no passado e ainda ter o pacote aprovado: · não é a release ativa; · a revisão dela não é a que está ativa; · a disponibilidade foi confirmada na publicação que a criou (réplicas prontas observadas, evidência `runtime` ou superior); · o pacote aprovado da revisão existe. A existência dos bytes no registry não entra aqui, porque o servidor não a sabe sem ir ao registry. Ela é conferida na execução do rollback, antes de tocar o cluster. rollback_ineligible_reason: type: [string, 'null'] enum: [active, same_revision_active, availability_unconfirmed, bundle_unavailable, null] description: | Por que `rollback_eligible` é `false`; `null` quando é `true`. `active` — é a release que serve agora. `same_revision_active` — a mesma revisão já está no ar por outra release; voltar para ela não mudaria nada. `availability_unconfirmed` — a publicação que a criou nunca confirmou réplicas prontas. Ela pode ter funcionado, mas a plataforma não sabe. `bundle_unavailable` — o pacote aprovado da revisão não existe mais. reason: type: [string, 'null'] description: | O que uma PESSOA escreveu para explicar esta ativação. Texto, para a tela: não decide nada e não prova nada — quem publicou escolhe as palavras. O que distingue rollback de avanço é o `kind` da operação. SecurityScan: type: object description: | Uma execução do analisador sobre UM digest, com o que ela encontrou e o que a política do Zero decidiu a partir disso. required: [scanned, artifact_digest, status] properties: scanned: type: boolean description: | Existe análise deste digest. Falso significa que ninguém olhou — não que a imagem esteja limpa. artifact_digest: type: string description: O artefato analisado. É o mesmo que está publicado. artifact_repository: { type: string } status: type: string enum: [succeeded, failed, timed_out, registry_unavailable, scanner_error, invalid_report] description: | O desfecho da ANÁLISE, nunca da imagem. Só `succeeded` autoriza ler as contagens como resultado. scanned_at: type: [string, 'null'] format: date-time description: Quando o relatório foi gerado. Nulo quando não houve relatório. started_at: { type: string, format: date-time } requested_by: type: string enum: [build, rescan, manual] description: | `build` é a análise feita ao publicar; `rescan`, a diária sobre o que está no ar — a base de vulnerabilidades de hoje sabe o que a de ontem não sabia. scanner: { type: string, description: A ferramenta. } scanner_version: { type: string } vulnerability_db_version: type: string description: A base com que se olhou. É o que explica por que o mesmo digest mudou de resultado. critical_count: { type: integer } high_count: { type: integer } medium_count: { type: integer } low_count: { type: integer } unknown_count: type: integer description: Achados que o analisador não classificou. Não são de baixa severidade. fixable_count: type: integer description: Quantos têm versão corrigida publicada. findings_total: type: integer description: A soma das severidades. Zero com `status` de falha não é ausência de vulnerabilidade. policy: { type: string } policy_version: { type: integer } decision: type: string enum: [allow, allow_with_warning, block, scan_failed] description: | `allow` só quando a análise concluiu e não encontrou nada. `scan_failed` é "não consegui olhar", e nunca se confunde com os outros. decision_reason: { type: string } failure_reason: type: string description: Por que a análise não concluiu. Ausente quando concluiu. scan_id: { type: string } SecurityFinding: type: object required: [id, vulnerability_id, severity, package_name, installed_version, fix_state, target] properties: id: { type: string } vulnerability_id: { type: string, description: 'O identificador público, por exemplo CVE-2026-1.' } severity: type: string enum: [critical, high, medium, low, unknown] package_name: { type: string } installed_version: { type: string } fixed_version: type: string description: A versão que corrige. Ausente quando não há correção conhecida. fix_state: type: string enum: [available, unavailable, unknown] description: | `unknown` existe porque o analisador nem sempre afirma. Lê-lo como "não tem conserto" faz quem opera desistir de procurar a correção que existe. target: type: string description: Onde está o pacote. A mesma vulnerabilidade em dois alvos são dois consertos. title: { type: string } primary_url: { type: string } ProjectSource: type: object description: | A origem de código do PROJETO. Um projeto, uma origem: os serviços dele publicam do mesmo repositório e diferem só na pasta que constroem. Nunca traz credencial, chave de segredo nem nome de Secret — são detalhes internos, e expô-los transformaria a origem numa segunda configuração que a pessoa precisaria entender. required: [configured] properties: configured: type: boolean description: Falso é o estado inicial do projeto, não um erro. kind: type: string enum: [git, image] provider: type: string enum: [github, gitlab, bitbucket] account: type: string description: | A conta no provedor. É metade da identidade: duas contas de GitHub são dois acessos diferentes, e é por isso que o provedor sozinho não identifica a origem. repository: type: string description: | Forma canônica, sem host: `dono/repositório` no GitHub e no Bitbucket, `grupo/subgrupo/…/projeto` no GitLab — ou a imagem quando `kind` é `image`. ref: type: string description: A referência padrão do projeto — branch, tag ou commit. state: type: string enum: [connected, degraded, revoked] authenticated: type: boolean description: | Se há credencial guardada — sem dizer qual. É o que distingue repositório público de conta conectada, sem tocar no segredo. auth_method: type: string enum: [credential, oauth] description: | Como a origem foi autenticada. Os dois pedem gestos diferentes: reautorizar no provedor não é colar outro token, e só o `oauth` tem refresh que se renova sozinho. oauth_providers: type: array items: { type: string, enum: [github, gitlab, bitbucket] } description: | Os provedores que ESTA INSTALAÇÃO sabe autorizar. É da plataforma, não do projeto, e viaja aqui para a tela decidir entre "Autorizar no GitHub" e "Colar um token" sem uma segunda requisição. Lista vazia é legítima: significa que as aplicações OAuth ainda não foram registradas, e o caminho por credencial continua inteiro. SourceAuthorizationStart: type: object required: [authorization_url] properties: authorization_url: type: string description: Para onde o navegador vai autorizar, no provedor. SourceAuthorizationCallback: type: object required: [state, code] properties: state: type: string description: O valor de uso único devolvido pelo provedor. code: type: string description: | O código de autorização do provedor. Ele é trocado por token servidor a servidor e não é guardado. SourceAuthorization: type: object description: | Uma autorização concluída à espera da escolha do repositório. Ela NÃO é a origem do projeto: nenhuma origem foi escrita ainda. required: [id, provider, account, expires_at] properties: id: type: string description: Identificador `sau_…`, usado para listar e para concluir no PUT. provider: type: string enum: [github, gitlab, bitbucket] account: type: string description: | De quem é a credencial, descoberto no provedor. É o que permite reconhecer que a autorização foi feita com a conta errada ANTES de escolher o repositório. expires_at: type: string format: date-time description: Depois disto a credencial é descartada e o fluxo recomeça. AuthorizedRepositories: type: object required: [provider, account, repositories] properties: provider: type: string enum: [github, gitlab, bitbucket] account: type: string repositories: type: array items: { $ref: '#/components/schemas/AuthorizedRepository' } AuthorizedRepository: type: object required: [full_name, private] properties: full_name: type: string description: Forma canônica `dono/repositório`, minúscula — a mesma que a origem guarda. default_branch: type: string description: | O branch que o provedor considera principal. Vira a sugestão da tela, e evita publicar de `master` achando que escolheu `main`. private: type: boolean ProjectSourceInput: type: object required: [repository] properties: kind: type: string enum: [git, image] default: git provider: type: string enum: [github, gitlab, bitbucket] description: | Obrigatório quando `repository` não nomeia o host (`dono/repo`). Quando nomeia, o host decide, e um `provider` diferente dele é recusado com `422` no campo `provider` — nunca adivinhado. account: type: string description: | A identidade da CREDENCIAL no provedor — não o dono do repositório. Descoberta na autorização por OAuth; com token colado, opcional. repository: type: string description: | O repositório, em qualquer forma que a pessoa copia: `dono/repo`, `https://github.com/dono/repo`, `github.com/dono/repo`, `git@github.com:dono/repo.git`. GitHub e Bitbucket são `dono/repositório` exatos; o GitLab aceita namespace hierárquico — `grupo/subgrupo/…/projeto`, até 20 níveis de subgrupo. Grava-se a forma canônica: caminho em minúsculas, sem host, sem `.git`. A semântica exata é a tabela `validation/source-repository.json` deste repositório, que o control plane e o console provam nos testes. Recusas: host que não é GitHub, GitLab nem Bitbucket → `SOURCE_PROVIDER_NOT_ALLOWED`; endereço vazio, incompleto (só o dono, só o grupo) ou malformado (esquema, credencial embutida, porta, caractere, segmento a mais) → `VALIDATION_FAILED` no campo `repository`, com a frase do que falta. ref: type: string default: main authorization_id: type: string description: | Conclui um fluxo de OAuth: a conta e a credencial vêm da autorização, não do corpo. Exclusivo com `credential` — informar os dois é recusado, porque decidir qual vence produziria uma origem que declara uma identidade e publica com outra. credential: type: string format: password writeOnly: true description: | O token da conta. Entra cifrado e nunca volta. Vazio mantém a credencial atual quando a origem já existe, e significa "repositório público" quando ela é nova. PullRequestPreview: type: object description: | Um pull request e o preview dele. A identidade é o número no repositório vinculado — não a branch, que muda de nome, contém `/` e some. required: [id, number, state, head_sha, update_state, generation, updated_at] properties: id: { type: string, example: pvw_01J8XYZ } number: { type: integer } title: { type: [string, 'null'] } head_ref: type: [string, 'null'] description: A branch. É metadata — nada resolve por ela. author: { type: [string, 'null'] } from_fork: { type: boolean } state: { type: string, enum: [open, closed, merged] } head_sha: type: string description: O último commit que o provedor informou. É a INTENÇÃO. running_sha: type: [string, 'null'] description: O commit que está no ar. É o FATO, e pode diferir do `head_sha`. running_artifact_id: type: [string, 'null'] description: O artefato em execução. É este que a promoção reutiliza. running_digest: type: [string, 'null'] description: O digest do conteúdo em execução. running_release_id: { type: [string, 'null'] } url: type: [string, 'null'] description: Onde testar. Nulo enquanto não houver endereço ativo. update_state: type: string enum: [pending, running, ready, failed, refused] description: | O desfecho da última INTENÇÃO. `failed` com `running_sha` presente é o caso honesto: o commit novo não subiu, e o antigo continua no ar. update_error_code: { type: [string, 'null'] } generation: type: integer description: | Sobe a cada mudança de intenção. É o que faz uma publicação em voo não vencer ao terminar depois. updated_at: { type: string, format: date-time } GitEventResult: type: object description: | O que a plataforma fez com o aviso. `nada` é o desfecho mais comum e não é erro: é o que toda reentrega do mesmo evento produz. required: [resultado, pull_request] properties: resultado: type: string enum: [nada, publicar, encerrar, recusar, ignorado] description: | `publicar` construiu o commit e atualizou o preview; `encerrar` parou o runtime preservando artefato e histórico; `recusar` registrou por que não haverá preview; `nada` não mudou coisa alguma; `ignorado` é evento fora do vocabulário desta capacidade. pull_request: type: integer description: O número do pull request no repositório. motivo: type: string description: Por que o desfecho foi esse. Presente em `nada` e `recusar`. preview_id: type: string description: O preview afetado (`pvw_…`). Ausente quando nada mudou. generation: type: integer description: | A geração da intenção depois deste evento. Ela sobe a cada mudança, e é o que faz uma publicação em voo não vencer ao terminar depois. operation_id: type: string description: A publicação enfileirada (`op_…`), quando houve uma. DeploymentCreate: type: object description: | **Exatamente uma** origem: `source` OU `artifact_id`. Os dois juntos são recusados com `422` — quem manda os dois quase sempre acha que está pedindo "reconstrua a partir desta origem e promova", e são coisas diferentes. Sem nenhum dos dois — `{}` —, publica a origem do projeto na referência dela e na pasta que o serviço declarou. properties: source: allOf: - $ref: '#/components/schemas/SourceAdjustment' description: | Publicar a partir do CÓDIGO DA ORIGEM DO PROJETO: resolve a referência, constrói a imagem, verifica e sobe. Produz um artefato novo. Só ajusta `branch` e `subdirectory` — o repositório é o da origem do projeto. artifact_id: type: string description: | **Promover** um artefato que já existe (`art_…`), sem reconstruir nada. É a garantia central do produto: o digest que sobe aqui é o MESMO que subiu lá Não "a mesma origem reconstruída" — isso produziria outro digest, e nenhuma cadeia de build garante que dois builds do mesmo commit dão a mesma imagem. A fronteira é o **projeto**: o artefato precisa ter sido publicado por algum serviço do mesmo projeto. Promover é justamente mover entre serviços — o de `development` e o de `production` são diferentes —, então exigir o mesmo serviço tornaria a promoção impossível. Artefato de outra organização responde `404`; de outro projeto do mesmo tenant, `409 ARTIFACT_NOT_IN_PROJECT`; sem verificação registrada, `409 ARTIFACT_NOT_VERIFIED`. A **configuração é a do destino**. A mesma imagem sobe com a escala, as variáveis e os secrets do ambiente que a recebe: promover nunca copia configuração, e nunca copia secret. reason: { type: string, maxLength: 500, description: "Texto livre, para quem for ler o histórico. Ele NÃO escolhe o que o motor faz: o caminho da execução é decidido pelo servidor." } Deployment: type: object required: [id, service_id, status, display_status, verification_level, created_at] properties: id: { type: string } service_id: { type: string } status: type: string enum: [deploying, verifying, ready, failed, cancelled, superseded] description: | O desfecho de uma EXECUÇÃO — a tentativa de pôr uma revisão num ambiente. `deploying` — em curso. É o estado em que a execução nasce: a linha é criada depois de artefato e pacote, e por isso nunca houve um estado anterior a ela. `verifying` — o pacote foi aplicado e a verificação está medindo. `ready` — concluiu, e a revisão está servindo. `failed` — NÃO concluiu. `cancelled` — alguém interrompeu. `superseded` — era válida e outra publicação tomou o lugar. **Não é falha**: o usuário publicou de novo, e a mais nova venceu. As ETAPAS de uma execução não estão aqui — estão nos checkpoints da operação. E ROLLBACK não é um estado: ele é uma operação de kind próprio (`deployment.rollback`) que cria um deployment novo e uma release nova, como qualquer publicação. A execução de um rollback termina em `ready`. A UX principal usa `display_status`. display_status: type: string description: | A frase que a interface mostra. O estado interno é vocabulário que o usuário nunca concordou em aprender. revision_id: type: [string, 'null'] description: A revisão imutável que esta publicação produziu, quando já produziu. artifact_id: { type: [string, 'null'] } release_id: { type: [string, 'null'] } operation_id: type: [string, 'null'] description: | A operação durável que conduz a publicação. Nula na janela entre o registro nascer e o trabalho ser enfileirado. target_generation: type: [integer, 'null'] format: int64 description: A geração do serviço que esta publicação pretende colocar no ar. triggered_by: type: object additionalProperties: true description: Quem pediu, e por qual superfície. properties: actor: { type: string, description: Sujeito autenticado. } source: { type: string, enum: [ui, cli, api, mcp, git, system] } verification_level: $ref: '#/components/schemas/VerificationLevel' reason: { type: [string, 'null'], description: "O que uma PESSOA escreveu para explicar esta publicação. Texto, para a tela." } error: { $ref: '#/components/schemas/Problem' } created_at: { type: string, format: date-time } finished_at: { type: [string, 'null'], format: date-time } SecretMetadata: type: object required: [name, updated_at] description: Valor nunca é devolvido — nem para Owner. properties: name: { type: string } digest_preview: { type: string, description: "Hash truncado, para conferir mudança sem revelar valor." } used_by: type: array items: { type: string } updated_at: { type: string, format: date-time } updated_by: { type: string } LogSource: type: string enum: [operation, build, deploy, application] description: | As quatro origens de log, deliberadamente distintas porque nascem em momentos diferentes do ciclo de vida: - `operation` — passos, transições e erros da operação durável. - `build` — saída do processo que transformou source em artifact. - `deploy` — eventos do compilador de workload e do reconciler. - `application` — o que o processo do cliente escreveu em stdout/stderr. `application` **só existe depois que há workload executando**. Pedir essa origem antes disso devolve `APPLICATION_LOGS_UNAVAILABLE` — nunca uma lista vazia. Vazio significa "executou e não escreveu nada", que é um diagnóstico completamente diferente de "não executou". LogEntry: type: object required: [at, level, message, source] properties: at: { type: string, format: date-time } level: { type: string } message: { type: string, description: Já redigido — segredo conhecido nunca é devolvido. } source: { $ref: '#/components/schemas/LogSource' } state: type: [string, 'null'] description: | O estado da operação no instante em que esta linha foi escrita — `fetching`, `building`, `deploying`. Nulo nas linhas que não pertencem a um passo. É o que permite agrupar a narrativa por etapa sem reinterpretar o texto da mensagem, que é prosa e muda. instance: { type: [string, 'null'], description: Identificador opaco da instância. } VerificationLevel: type: string enum: [none, local, api_server, runtime, public_edge] description: | Até onde a verificação de uma publicação realmente chegou. Existe para que "verificado" nunca signifique coisas diferentes em contextos diferentes: - `none` — nada foi verificado. - `local` — aplicado e relido de um provider em processo, sem cluster. Não é evidência sobre o mundo: existe para que um ambiente de desenvolvimento não precise mentir dizendo `api_server`. - `api_server` — os objetos foram aceitos por um API server real e a reconciliação é idempotente. **Não** prova scheduling, pull de imagem, CNI, volume, probes executadas pelo kubelet nem tráfego. - `runtime` — o workload executou de fato: imagem baixada, container no ar, probes passando, logs da aplicação existindo. - `public_edge` — a rota pública responde com TLS válido de fora do cluster. Enquanto não houver cluster real, o máximo alcançável é `api_server`, e a API diz isso explicitamente em vez de deixar implícito. UsageSummary: type: object required: [period, categories, projection] description: | Consumo medido, agregado por categoria. Preço não faz parte da 0.0.1 — o ledger é a fundação, e inventar um valor em reais a partir de segundos de vCPU produziria um número que ninguém pode conferir. properties: period: type: object required: [from, to] properties: from: { type: string, format: date-time } to: { type: string, format: date-time } categories: type: array items: { $ref: '#/components/schemas/UsageCategory' } projection: { $ref: '#/components/schemas/UsageProjection' } UsageCategory: type: object required: [key, label, unit, total, limit, percent_used, by_project, series] properties: key: type: string enum: [cpu_seconds, memory_gb_seconds, storage_gb_hours, egress_gb, build_minutes, database_gb_hours, backup_gb_hours, object_storage_gb_hours, requests, custom_domains, dedicated_resource_hours, ai_operations] label: type: string description: O rótulo em linguagem de produto. `cpu_seconds` não é português. unit: { type: string } total: type: [number, 'null'] description: | Null é "não houve leitura no período"; zero é "houve leitura e o consumo foi nenhum". Colapsar os dois faz a tela afirmar que a aplicação não consumiu nada quando a coleta é que estava parada. limit: type: [number, 'null'] description: Null quando não existe cota para a categoria. percent_used: { type: [number, 'null'] } by_project: type: array description: Os maiores consumidores. Responde "onde está indo?". items: type: object required: [project_id, project_name, value] properties: project_id: { type: string } project_name: { type: string } value: { type: number } series: type: array description: | Um ponto por dia COM consumo. Dias sem medição não viram zero: zero afirmaria que houve leitura e o consumo foi nenhum. items: type: object required: [at, value] properties: at: { type: string, format: date-time } value: { type: number } UsageProjection: type: object required: [available, reason, values] description: | A projeção só aparece quando ela é possível. Extrapolar três dias de série para o mês inteiro produz um número com cara de previsão e valor de chute — e alguém dimensiona infraestrutura com base nele. properties: available: { type: boolean } reason: { type: [string, 'null'] } values: type: [object, 'null'] additionalProperties: { type: number } # ─────────────────────── observabilidade (ADR-022) ────────────────────── # # Convenção que vale para TODO schema desta seção: `null` no tipo # (`type: [number, 'null']`) significa "não medido", e a ausência dele # significa "medido". A distinção não é # estilística — ela é o produto. Um cliente que renderizar null como 0 vai # afirmar que um serviço parado responde em 0 ms com 0% de erro. CollectionFreshness: type: object required: [last_sample_at, window_seconds, collecting] properties: last_sample_at: type: [string, 'null'] format: date-time description: Quando a coleta produziu a amostra mais recente deste escopo. window_seconds: type: integer description: Largura da janela de ingestão. Toda série é múltipla dela. collecting: type: boolean description: | Existe coleta para este escopo, EM QUALQUER TEMPO — a pergunta não olha a janela consultada. É o que separa "não houve tráfego nas últimas 24h" de "a coleta nunca produziu amostra aqui": sem a distinção, as duas viram a mesma tela vazia e a pessoa não sabe se procura um problema de tráfego ou de instalação. TrafficPoint: type: object required: [at, requests, c2xx, c4xx, c5xx, p95_ms] properties: at: { type: string, format: date-time } requests: type: integer description: | Não é nulável: um balde que existe com zero requisições é um fato medido. O eixo vem completo, inclusive os baldes vazios — omiti-los faria a linha ligar dois instantes distantes como se fossem consecutivos, e é justamente o vale que interessa a quem investiga. c2xx: { type: integer } c4xx: { type: integer } c5xx: { type: integer } p95_ms: type: [number, 'null'] description: | Nulo quando o balde não teve requisição, ou teve poucas demais para o quantil. É o que faz a linha de latência ter BURACO em vez de mergulhar no zero — e o mergulho no zero se lê como "ficou rápido" quando significa "parou de responder". LatencyPercentiles: type: object required: [p50, p75, p90, p95, p99] properties: p50: { type: [number, 'null'] } p75: { type: [number, 'null'] } p90: { type: [number, 'null'] } p95: { type: [number, 'null'] } p99: type: [number, 'null'] description: | Cada quantil é nulo por conta própria: um p99 tirado de sete requisições é um número com três casas decimais e nenhuma informação, então ele vem nulo enquanto o p50 do mesmo período já tem valor. ObservedRoute: type: object required: [route, requests, error_rate, p95_ms] properties: route: type: string description: | Rota já NORMALIZADA: o nome da `HTTPRoute` quando existe, e o balde `outras` quando não. Nunca a URL, nunca a query string. requests: { type: integer } error_rate: type: [number, 'null'] description: Nulo quando não houve requisição. Ver `ObservabilitySummary`. p95_ms: { type: [number, 'null'] } StatusClassCount: type: object required: [status_class, requests] properties: status_class: { type: string, enum: ['1xx', '2xx', '3xx', '4xx', '5xx', none] } requests: { type: integer } ObservabilitySummary: type: object required: [freshness, requests, latency, bytes, runtime, series, granularity_seconds] properties: freshness: { $ref: '#/components/schemas/CollectionFreshness' } requests: type: object required: [total, rps, success, c3xx, c4xx, c5xx, error_rate] description: | Todos nulos quando `freshness.collecting` é falso — inclusive os que seriam zero. properties: total: { type: [integer, 'null'] } rps: { type: [number, 'null'] } success: { type: [integer, 'null'] } c3xx: { type: [integer, 'null'] } c4xx: { type: [integer, 'null'] } c5xx: { type: [integer, 'null'] } error_rate: type: [number, 'null'] description: | `(4xx + 5xx) / total`, ou nulo quando não houve requisição. Nulo e não zero: um serviço sem tráfego não tem 0% de erro, tem uma taxa que não existe — e é essa diferença que impede alguém de concluir que está tudo bem com um serviço que parou de receber requisição. latency: { $ref: '#/components/schemas/LatencyPercentiles' } bytes: type: object required: [in, out] properties: in: { type: [integer, 'null'] } out: { type: [integer, 'null'] } runtime: type: object required: - cpu_used_millicores - cpu_limit_millicores - memory_used_bytes - memory_limit_bytes - replicas_ready - replicas_desired - restarts description: | A amostra MAIS RECENTE, não "a de agora": o `metrics-server` não tem histórico e a amostra tem a idade que tem. `freshness.last_sample_at` é o que permite à tela dizer isso em vez de fingir tempo real. properties: cpu_used_millicores: { type: [integer, 'null'] } cpu_limit_millicores: type: [integer, 'null'] description: | Nulo quando nenhum serviço do escopo declarou limite. Nulo e não zero: zero significaria "sem CPU", e é o oposto de "sem teto". memory_used_bytes: { type: [integer, 'null'] } memory_limit_bytes: { type: [integer, 'null'] } replicas_ready: { type: [integer, 'null'] } replicas_desired: { type: [integer, 'null'] } restarts: { type: [integer, 'null'] } series: type: array items: { $ref: '#/components/schemas/TrafficPoint' } granularity_seconds: type: integer description: | Quanto tempo cada ponto cobre. Escolhido pelo servidor a partir da largura da janela, e devolvido porque duas janelas com o mesmo desenho e resoluções diferentes contam histórias diferentes. ObservedRequests: type: object required: [freshness, granularity_seconds, series, by_route, by_status, truncated] properties: freshness: { $ref: '#/components/schemas/CollectionFreshness' } granularity_seconds: { type: integer } series: type: array items: { $ref: '#/components/schemas/TrafficPoint' } by_route: type: array items: { $ref: '#/components/schemas/ObservedRoute' } by_status: type: array description: 'Só as classes que APARECERAM. Uma linha 1xx com zero numa tabela é ruído.' items: { $ref: '#/components/schemas/StatusClassCount' } truncated: type: boolean description: | A lista não é tudo — por teto de rotas distintas na ingestão ou por corte na leitura. A tela avisa: uma lista cortada em silêncio faz alguém concluir que a rota ausente não teve tráfego. RuntimePoint: type: object required: [at, cpu_millicores, memory_bytes, replicas_ready, replicas_desired, restarts] properties: at: { type: string, format: date-time } cpu_millicores: { type: [integer, 'null'] } memory_bytes: { type: [integer, 'null'] } replicas_ready: { type: [integer, 'null'] } replicas_desired: { type: [integer, 'null'] } restarts: type: [integer, 'null'] description: | Reinícios SOMADOS no balde, e não a média: "0,4 reinício" não é uma coisa que acontece com um processo. Nulo quando não houve amostra — "0 reinícios" seria uma afirmação sobre um período que ninguém observou. ObservedRuntime: type: object required: [freshness, series] properties: freshness: { $ref: '#/components/schemas/CollectionFreshness' } series: type: array items: { $ref: '#/components/schemas/RuntimePoint' } # ──────────────────────────────── logs ────────────────────────────────── LogLimits: type: object required: [line_limit_bytes, line_limit_reached, stream_limit_lines, stream_limit_reached, notice] description: | Viaja em TODA página, e não só quando algo foi cortado: um campo que só aparece às vezes é um campo que o cliente esquece de tratar. properties: line_limit_bytes: { type: integer } line_limit_reached: { type: boolean } stream_limit_lines: { type: integer } stream_limit_reached: { type: boolean } notice: type: [string, 'null'] description: | A frase de produto, em português, ou null quando não há o que avisar. `stream_limit_reached` não é uma frase que se mostre a alguém. BuildLogLine: type: object required: [sequence, at, stage, stream, message, truncated] properties: sequence: type: integer description: | Ordem total dentro da publicação, densa a partir de 1. É ela que ordena, e não `at`: um compilador escreve centenas de linhas por segundo e `at` empata. at: { type: string, format: date-time } stage: type: [string, 'null'] description: Checkpoint do motor em que a linha nasceu. stream: { type: string, enum: [stdout, stderr] } message: type: string description: Já redigida na escrita. Ver a regra 5 do control plane. truncated: { type: boolean } BuildLogPage: type: object required: [items, next_cursor, limits] properties: items: type: array items: { $ref: '#/components/schemas/BuildLogLine' } next_cursor: { type: [string, 'null'] } limits: { $ref: '#/components/schemas/LogLimits' } RuntimeLogLine: type: object required: [at, service_id, service, environment, replica, level, stream, message, truncated] properties: at: { type: string, format: date-time } service_id: { type: string } service: { type: string } environment: { type: string } replica: type: [string, 'null'] description: | Opaco de propósito, e nulo quando a origem não soube dizer. Nome de recurso de infraestrutura não é vocabulário de produto. level: { type: string, enum: [debug, info, warning, error] } stream: { type: string, enum: [stdout, stderr] } message: { type: string } truncated: { type: boolean } RuntimeLogPage: type: object required: [items, next_cursor, limits] properties: items: type: array items: { $ref: '#/components/schemas/RuntimeLogLine' } next_cursor: { type: [string, 'null'] } limits: { $ref: '#/components/schemas/LogLimits' } # ───────────────────────────── configuração ───────────────────────────── ConfigScope: type: string enum: [organization, project, environment] ConfigVariable: type: object required: [key, value, scope, version, updated_at, updated_by] properties: key: { type: string } value: { type: string } scope: { $ref: '#/components/schemas/ConfigScope' } version: { type: integer } updated_at: { type: string, format: date-time } updated_by: { type: [string, 'null'] } ConfigSecret: type: object required: [key, scope, version, updated_at, updated_by] description: | Sem `value`, sem prévia, sem resumo e sem TAMANHO. Um resumo de segredo curto é força bruta com dicionário, e o tamanho já elimina metade das hipóteses de quem estiver adivinhando. properties: key: { type: string } scope: { $ref: '#/components/schemas/ConfigScope' } version: type: integer description: Geração; cresce a cada substituição, incluindo as removidas. updated_at: { type: string, format: date-time } updated_by: { type: [string, 'null'] } EffectiveConfigurationKey: type: object required: [key, value, secret, origin, override, version, updated_at, updated_by] properties: key: { type: string } value: type: [string, 'null'] description: | Null quando `secret` é verdadeiro. Null e não string vazia: string vazia é um valor legítimo de variável, e confundir os dois faria a tela afirmar que um secret está em branco. secret: { type: boolean } origin: { $ref: '#/components/schemas/ConfigScope' } override: type: boolean description: | Esta declaração venceu outra. A tela avisa: remover a declaração local não remove a variável, faz ela voltar a um valor anterior que quem remove talvez não conheça. overridden_scopes: type: array description: Os escopos que perderam, do mais para o menos específico. items: { $ref: '#/components/schemas/ConfigScope' } version: { type: integer } updated_at: { type: string, format: date-time } updated_by: { type: [string, 'null'] } EffectiveConfiguration: type: object required: [project_id, environment_id, items] properties: project_id: { type: string } environment_id: type: string description: | O ambiente para o qual a resolução foi feita. Sem ele a resposta não diz de que configuração está falando — e a mesma chave tem valores diferentes em ambientes diferentes, que é o ponto do modelo. items: type: array items: { $ref: '#/components/schemas/EffectiveConfigurationKey' } Capability: type: object required: [key, state, reason, last_event_at, freshness_seconds, action] description: | Uma superfície do console, com o que é preciso para saber se ela funciona: por onde o dado é pedido (`route`), quem o escreve (`producer`), de onde ele veio (`source`) e de quando ele é (`last_event_at`). `producer` vazio é o que caracteriza `nao_implementada`: existe tela e endpoint, e não existe quem escreva o dado. Um portão do control plane recusa qualquer capacidade que declare produtor sem rota servida, e qualquer rota servida cuja capacidade não declare produtor. properties: key: type: string description: A superfície, no vocabulário do console. state: type: string enum: [pronta, sem_dados, nao_configurada, degradada, indisponivel, nao_implementada] description: | Cada estado leva a uma ação diferente de quem está olhando, e colapsá-los faz a pessoa agir errado. `sem_dados` é silêncio legítimo — o caminho está montado e não houve o que medir. `nao_configurada` é trabalho parado: falta um passo, e `action` diz qual. `degradada` é coleta que já entregou e atrasou — os números da tela continuam sendo os da última leitura, e a idade deles é a informação. `nao_implementada` não tem produtor no binário, e prometer ativação seria mentir. reason: type: string description: | A frase de produto que a tela mostra. Preenchida sempre que o estado não é `pronta` — um estado sem explicação faz a pessoa procurar a causa no lugar errado — e vazia quando é, porque uma explicação ao lado do que funciona ensina a ignorar o campo. route: type: string description: | Por onde o dado é pedido. Vai na resposta para que uma investigação não precise ler o código do console para saber o que a tela chamaria. producer: type: string description: Quem escreve o dado, no vocabulário de operação. source: type: string description: De onde o produtor tira o que escreve. last_event_at: type: [string, 'null'] format: date-time description: O último fato conhecido desta superfície. freshness_seconds: type: [integer, 'null'] description: | A idade desse fato. Null quando não há fato nenhum — e não zero, que significaria "acabou de acontecer". action: description: O que fazer, quando há o que fazer. type: [object, 'null'] required: [label, path] properties: label: { type: string } path: { type: string } DomainCheck: type: object required: [label, hostname, valid, available, reason] properties: label: type: string description: | O texto já normalizado. A tela mostra isto de volta no campo: quem digitou "Minha Loja!" vê `minha-loja` e entende a regra sem ler documentação. hostname: type: [string, 'null'] description: O endereço completo que resultaria. Null quando o label não serve. valid: type: boolean description: O texto pode virar um label DNS. available: type: [boolean, 'null'] description: | Null quando não foi consultado (porque o label já era inválido) — e não `false`, que diria "está tomado". reason: type: string description: | A frase de produto, vazia quando está tudo certo. As recusas são distintas porque as correções são distintas: caractere inválido, hífen na ponta, comprimento, nome reservado e já-em-uso pedem ações diferentes. DomainChoice: type: object required: [hostname, previous, changed, operation_id] properties: hostname: { type: string } previous: type: [string, 'null'] description: | O endereço anterior. Ele responde até o novo responder e é liberado nesse instante. Null quando nada mudou. changed: { type: boolean } operation_id: type: [string, 'null'] description: | A publicação que põe o endereço novo no ar e libera o anterior. Null quando nada mudou ou quando o serviço nunca foi publicado — aí não há o que republicar, e o anterior já foi liberado. notice: type: string description: | O que a tela precisa dizer depois da troca: o novo passa a responder quando a publicação terminar, e o anterior deixa de responder no mesmo instante. GatewayCapabilities: type: object description: | Uma resposta de sim ou não por capacidade, com o motivo do não. O padrão é NÃO prometer: uma instalação que não declarou uma capacidade a devolve indisponível, e a API recusa o pedido em vez de aceitar e entregar outra coisa. required: [shared_ingress, dedicated_ingress, shared_egress, dedicated_egress] properties: shared_ingress: { $ref: '#/components/schemas/GatewayCapability' } dedicated_ingress: { $ref: '#/components/schemas/GatewayCapability' } shared_egress: { $ref: '#/components/schemas/GatewayCapability' } dedicated_egress: { $ref: '#/components/schemas/GatewayCapability' } custom_domains: { $ref: '#/components/schemas/GatewayCapability' } tls: { $ref: '#/components/schemas/GatewayCapability' } routing: { $ref: '#/components/schemas/GatewayCapability' } api_key_auth: { $ref: '#/components/schemas/GatewayCapability' } jwt_auth: { $ref: '#/components/schemas/GatewayCapability' } rate_limit: { $ref: '#/components/schemas/GatewayCapability' } metrics: { $ref: '#/components/schemas/GatewayCapability' } GatewayCapability: type: object required: [available] properties: available: { type: boolean } reason: type: string enum: [not_configured, provider_capacity, not_implemented] description: Código estável. Ausente quando disponível. message: type: string description: Frase pronta para a tela, sem jargão de infraestrutura. Gateway: type: object description: | A camada de conectividade externa de uma organização. O `status` é COMPOSTO: derivado dos endereços, nunca escrito à mão — um gateway "pronto" com endereço em preparo seria uma tela que promete o que não funciona. required: [id, name, class, status, ingress, egress, domains] properties: id: { type: string, examples: ["gw_01JC5Z9K0000000000000000"] } name: { type: string } slug: { type: string } class: { type: string, enum: [shared, dedicated] } status: type: string enum: [provisioning, ready, degraded, cleanup_pending, deleted] display_status: { type: string, description: "Frase pronta para a tela, sem jargão." } ingress: { $ref: '#/components/schemas/GatewayAddressList' } egress: { $ref: '#/components/schemas/GatewayAddressList' } domains: type: object properties: ready: { type: integer } pending: { type: integer } error: { type: integer } created_at: { type: string, format: date-time } GatewayAddressList: type: object required: [addresses] properties: addresses: type: array items: { $ref: '#/components/schemas/GatewayAddress' } GatewayAddress: type: object description: | Um endereço público estável. O identificador interno do provedor NÃO aparece aqui: ele existe para a reconciliação e para o runbook, e devolvê-lo entregaria a topologia da nuvem sem ganho para quem lê. required: [id, purpose, state, managed] properties: id: { type: string, examples: ["gwa_01JC5Z9K0000000000000000"] } address: { type: string, examples: ["203.0.113.20"] } purpose: { type: string, enum: [ingress, egress, ingress_egress] } state: type: string enum: [allocating, allocated, associating, ready, degraded, replacement_required, releasing, released, error] description: | `replacement_required` é uma PARADA deliberada: o endereço mudou ou sumiu, e trocá-lo em silêncio quebraria listas de liberação que vivem fora do Zero. display_state: { type: string } managed: type: boolean description: | Falso quando o endereço é apenas OBSERVADO — o caso da saída compartilhada, que a infraestrutura já aplica e o Zero não cria. stable_since: { type: string, format: date-time } message: { type: string } GatewayApiKey: type: object required: [id, name, prefix, created_at, active] properties: id: { type: string, examples: ["gwk_01JC5Z9K0000000000000000"] } name: { type: string, examples: ["integração de faturamento"] } prefix: type: string description: O começo do valor, para reconhecer a chave numa lista. examples: ["zk_a3f9k2mq"] value: type: string description: | O valor em claro. Vem preenchido SOMENTE na resposta da criação. Não é recuperável depois — nem pela plataforma. examples: ["zk_a3f9k2mqxrt7vwbn4ph8scdz6y2j5k9e"] header: type: string description: O cabeçalho onde o chamador apresenta a chave. examples: ["X-API-Key"] expires_at: { type: [string, 'null'], format: date-time } last_used_at: { type: [string, 'null'], format: date-time } revoked_at: { type: [string, 'null'], format: date-time } created_at: { type: string, format: date-time } active: { type: boolean } GatewayRoutePolicyInput: type: object properties: auth_mode: type: string enum: [none, api_key, jwt] default: none jwt_issuer: { type: string, examples: ["https://login.acme.com.br/"] } jwt_audiences: type: array items: { type: string } description: | Obrigatória com JWT. Sem ela, um token emitido para OUTRA aplicação do mesmo provedor seria aceito. jwt_jwks_uri: type: string description: Precisa ser https e resolver para endereço público. examples: ["https://login.acme.com.br/.well-known/jwks.json"] rate_limit_requests: { type: integer, minimum: 1 } rate_limit_window_seconds: type: integer enum: [1, 60, 3600, 86400] rate_limit_scope: type: string enum: [route, client_ip, api_key] request_timeout_seconds: { type: integer, minimum: 1, maximum: 900 } cors_allowed_origins: type: array items: { type: string } allowed_methods: type: array items: { type: string } GatewayRoutePolicy: allOf: - $ref: '#/components/schemas/GatewayRoutePolicyInput' - type: object required: [auth_mode, display_auth_mode] properties: display_auth_mode: type: string description: A frase em português. examples: ["Chave de API"] GatewayRoute: type: object description: | A ligação entre um domínio confirmado, um caminho e um serviço. `environment_id` sai junto de `service_id` ainda que o cliente só informe o serviço: a tela precisa dizer para qual ambiente o endereço aponta, e obrigá-la a cruzar duas listas seria convidar a mostrar o ambiente errado. required: [id, domain_id, hostname, path, path_type, service_id, environment_id, enabled, status, display_status] properties: id: { type: string, examples: ["gwr_01JC5Z9K0000000000000000"] } domain_id: { type: string, examples: ["gwd_01JC5Z9K0000000000000000"] } hostname: { type: string, examples: ["api.acme.com.br"] } path: { type: string, examples: ["/"] } path_type: { type: string, enum: [prefix, exact] } service_id: { type: string, examples: ["svc_01JC5Z9K0000000000000000"] } environment_id: { type: string, examples: ["env_01JC5Z9K0000000000000000"] } enabled: { type: boolean } status: type: string enum: [pending, ready, degraded, error, removing] display_status: type: string description: A frase em português. Nenhum termo de infraestrutura. examples: ["Publicada"] applied_at: { type: [string, 'null'], format: date-time } GatewayDomain: type: object required: [id, hostname, dns_mode, status, tls_state] properties: id: { type: string, examples: ["gwd_01JC5Z9K0000000000000000"] } hostname: { type: string, description: "Já normalizado: minúsculo, sem ponto final, punycode." } dns_mode: { type: string, enum: [delegated_zone, customer_managed] } status: type: string enum: [pending_verification, verifying, verified, certificate_pending, ready, error, removing, deleted] display_status: { type: string } tls_state: { type: string, enum: [none, pending, ready, renewing, error] } verified_at: { type: string, format: date-time } certificate_expires_at: { type: string, format: date-time } journey: type: object description: >- Onde o domínio está e o que fazer agora, já resolvido pelo servidor. A tela NÃO recompõe isto: duas máquinas de estado para a mesma coisa divergem no dia em que uma ganhar um estado novo, e a que fica velha é sempre a da tela. required: [step, position, total, display] properties: step: type: string description: >- Os cinco passos alternam de quem é a vez, que é a única pergunta que a pessoa está fazendo: 1 e 2 pedem uma ação dela, 3 é conosco, 4 volta para ela, 5 é o fim. `conectado` não sai do estado do domínio — ele é a existência de uma rota, e sem ele a jornada pararia em "pronto para usar" sem nunca fechar. `erro` tem position 0: ele não é um degrau da escada, é a escada interrompida. enum: [configuracao, aguardando_dns, verificando, pronto, conectado, erro] position: { type: integer, description: 'De 1 a 5. 0 quando step é erro.' } total: { type: integer } display: { type: string, examples: ["Aguardando o seu DNS"] } next_action: type: string description: >- A frase do que fazer AGORA. Ausente quando não há nada a fazer, que é diferente de não sabermos. dns_records: type: array description: >- TODOS os registros que o cliente precisa publicar, juntos, com status por linha. Substitui dns_instructions e tls_instructions, que continuam por compatibilidade. Eles estavam em três lugares — a prova num diálogo que aparecia uma vez só, o CNAME e o A em letra miúda —, e quem fechasse o diálogo perdia o registro. A regra agora é: nenhuma informação necessária para concluir a configuração existe somente uma vez. items: type: object required: [purpose, type, name, value, status, display_status, note] properties: purpose: type: string enum: [ownership, tls, traffic] description: Código estável. A tela escolhe ícone e ordem por ele, nunca pelo texto. type: { type: string, examples: ["TXT", "CNAME", "A"] } name: { type: string } value: { type: array, items: { type: string } } status: type: string enum: [found, pending] description: O que a plataforma REALMENTE viu no DNS público. display_status: { type: string, examples: ["Ainda não vemos"] } note: { type: string } verification: type: object description: >- Compatibilidade. O mesmo registro está em dns_records, e é de lá que a tela deve ler — ali ele vem com status e ao lado dos outros dois. properties: record_name: { type: string, examples: ["_zero-verification.api.acme.com.br"] } record_type: { type: string, enum: [TXT] } record_value: { type: string } expires_at: { type: string, format: date-time } dns_instructions: type: object description: | Ausente enquanto não houver endereço de entrada PRONTO. Publicar uma instrução com endereço em preparo faria o cliente apontar o DNS para um lugar que ainda pode mudar. properties: record_name: { type: string } record_type: { type: string } record_value: { type: array, items: { type: string } } note: { type: string } ApplicationZone: type: object required: - id - base_domain - label - zone_name - status - display_status - message - serving - awaiting_customer - nameservers - created_at properties: id: { type: string } base_domain: type: string description: "O domínio registrável do cliente, normalizado. Ex.: `acme.com.br`." examples: ["acme.com.br"] label: type: string description: O rótulo delegado. Sugerido como `apps`, escolhido pelo cliente. examples: ["apps"] zone_name: type: string description: A zona completa. É ela que o cliente delega. examples: ["apps.acme.com.br"] status: type: string description: | Estado interno, do contrato `state-machines/application-zone.yaml`. Exposto ao lado do externo para que nenhum cliente da API precise reimplementar o mapeamento. enum: - PENDING_PROVISIONING - PENDING_DELEGATION - VERIFYING - ACTIVE - DEGRADED - PROVISIONING_FAILED - REMOVAL_REQUESTED - PENDING_UNDELEGATION - QUARANTINED - DELETED display_status: type: string enum: [preparing, waiting_customer, checking, ready, degraded, removing, removed, failed] message: type: string description: A frase em português que a tela mostra. Nunca menciona provedor nem SDK. examples: ["Aguardando a configuração no seu provedor"] serving: type: boolean description: | A zona pode receber endereços de projeto. `DEGRADED` serve — parar de servir por divergência possivelmente transitória de DNS derrubaria aplicação que está no ar. awaiting_customer: type: boolean description: | A bola está com quem contratou. É por este campo que a tela decide entre mostrar instruções e mostrar progresso. nameservers: type: array description: | Os quatro nomes que o cliente publica no DNS dele. **Vazio** enquanto a zona não existe no provedor: entregá-los antes convidaria a delegar para uma zona que ainda não responde. São sempre os nomes da marca (`ns1..ns4.dns.nnumbers.com.br`). Os nameservers internos do provedor não são expostos, aqui nem em lugar nenhum. items: { type: string } examples: [["ns1.dns.nnumbers.com.br", "ns2.dns.nnumbers.com.br", "ns3.dns.nnumbers.com.br", "ns4.dns.nnumbers.com.br"]] delegation_records: type: array description: | Os registros prontos para copiar, no formato que os painéis pedem. Existe separado de `nameservers` porque o que a pessoa cola no painel é uma linha inteira — nome, tipo, TTL e valor —, e montar isso na tela faria cada cliente da API montar de um jeito. items: type: object required: [name, type, value, ttl] properties: name: { type: string, examples: ["apps"] } fqdn: { type: string, examples: ["apps.acme.com.br"] } type: { type: string, const: "NS" } value: { type: string, examples: ["ns1.dns.nnumbers.com.br."] } ttl: { type: integer, examples: [3600] } provider_hint: type: [string, 'null'] description: | O provedor de DNS que o cliente disse usar, para escolher o tutorial. É uma **dica**, não um vínculo: nada no funcionamento depende dela, e errar só troca o texto das instruções. example_hostname: type: [string, 'null'] description: | Como ficaria o endereço de uma aplicação. É o campo que de fato responde "e como vai ficar?" — mais concreto que o nome da zona. examples: ["loja.apps.acme.com.br"] addresses_in_use: type: integer description: Quantos endereços de projeto já respondem sob esta zona. last_verified_at: { type: [string, 'null'], format: date-time } next_verification_at: type: [string, 'null'] format: date-time description: | Quando o reconciliador tentará de novo. A tela mostra isso para que esperar seja uma decisão informada, e não uma tela parada. claim_expires_at: type: [string, 'null'] format: date-time description: | Quando a reserva do nome vence se a delegação não aparecer. Sem prazo, pedir `apps.concorrente.com.br` seria reservar de graça o nome de outra empresa. activated_at: { type: [string, 'null'], format: date-time } removal_requested_at: { type: [string, 'null'], format: date-time } quarantine_until: type: [string, 'null'] format: date-time description: | Até quando o nome fica reservado depois de removido. Entregá-lo antes faria tráfego preso em cache chegar em outro cliente. created_at: { type: string, format: date-time } ApplicationZoneCreate: type: object required: [domain] properties: domain: type: string description: | O domínio da empresa, como a pessoa digitou. É normalizado: esquema, barra final, maiúsculas e forma internacionalizada são resolvidos antes de validar. examples: ["acme.com.br"] label: type: string default: apps description: | O rótulo da subzona. `apps` é **sugestão**, não contrato: quem já usa `apps` para outra coisa escolhe outro. Exatamente um rótulo — a profundidade é fixa por causa do certificado wildcard. examples: ["apps"] provider_hint: type: [string, 'null'] description: | Qual painel de DNS o cliente usa, só para escolher o tutorial. Nunca é credencial, nunca é consultado, e errar não quebra nada. examples: ["cloudflare"] ApplicationZonePreview: type: object required: [ok, input, base_domain, label, zone_name, problems] properties: ok: type: boolean description: | Se este pedido seria aceito. `false` aqui é resposta, não erro de requisição — a tela pergunta a cada tecla. input: { type: string, description: "O que foi recebido, sem normalizar." } base_domain: type: [string, 'null'] description: | O domínio registrável derivado pela Public Suffix List. Para `www.loja.acme.com.br` a derivação devolve `acme.com.br`: é sob o domínio registrável que a delegação faz sentido. label: { type: [string, 'null'] } zone_name: { type: [string, 'null'], examples: ["apps.acme.com.br"] } example_hostname: { type: [string, 'null'], examples: ["loja.apps.acme.com.br"] } suggested_labels: type: array description: Alternativas quando o rótulo pedido não serve. items: { type: string } examples: [["apps", "cloud", "sistemas", "plataforma"]] problems: type: array description: | Cada recusa com a **razão**, em português. Um rótulo como `mail` é sintaticamente válido e operacionalmente catastrófico: delegá-lo derruba o e-mail da empresa, e ninguém liga o incidente a "publiquei uma aplicação ontem". Dizer só "inválido" esconderia isso. items: type: object required: [code, message] properties: code: type: string enum: - DOMAIN_EMPTY - DOMAIN_MALFORMED - DOMAIN_IS_IP_ADDRESS - DOMAIN_IS_PUBLIC_SUFFIX - DOMAIN_TOO_DEEP - LABEL_EMPTY - LABEL_INVALID_CHARACTER - LABEL_LEADING_OR_TRAILING_HYPHEN - LABEL_TOO_LONG - LABEL_DANGEROUS - ZONE_TOO_LONG - ZONE_ALREADY_REQUESTED message: { type: string } field: { type: string, enum: [domain, label] } ZoneVerification: type: object required: [zone_name, ready, checked_at, diagnostics] properties: zone_name: { type: string } ready: type: boolean description: | As três condições ao mesmo tempo: o pai autoritativo anuncia o conjunto exato de NS, nossos servidores respondem autoritativamente, e ao menos um resolver público já enxerga a delegação. checked_at: { type: string, format: date-time } expected_nameservers: type: array items: { type: string } observed_nameservers: type: array description: O que o servidor autoritativo do domínio PAI anuncia agora. items: { type: string } missing: type: array description: O que falta publicar. É esta lista que a tela transforma em tarefa. items: { type: string } unexpected: type: array description: | NS além dos nossos respondendo pela zona. Não é detalhe: com dois operadores, a resposta muda conforme o resolver sorteia o servidor. items: { type: string } parent_servers_asked: type: array description: | A quem se perguntou. Está aqui porque o suporte precisa poder repetir a pergunta à mão e obter a mesma resposta. items: { type: string } public_resolvers_seeing: type: integer description: Quantos resolvers públicos já enxergam a delegação. diagnostics: type: array description: | Códigos estáveis. A interface escolhe texto e próximo passo a partir deles; o suporte procura por eles no log. items: type: object required: [code, message] properties: code: type: string enum: - DELEGATION_NOT_FOUND - DELEGATION_PARTIAL - DELEGATION_HAS_EXTRA_NAMESERVERS - PARENT_AUTHORITATIVE_UNREACHABLE - ZONE_NOT_AUTHORITATIVE - PUBLIC_PROPAGATION_PENDING - ZONE_CLAIM_EXPIRED message: { type: string } blame: type: string enum: [customer, platform, propagation] description: | De quem é a ação. Separa "você ainda não publicou" de "é conosco" de "é só esperar" — três situações que a mesma tela precisa apresentar de formas diferentes. next_attempt_at: { type: [string, 'null'], format: date-time } DelegationProviderGuide: type: object required: [id, name, steps] properties: id: type: string examples: ["cloudflare"] name: { type: string, examples: ["Cloudflare"] } aliases: type: array description: Como as pessoas chamam esse provedor, para a busca da tela. items: { type: string } console_url: { type: [string, 'null'] } record_type_label: type: string description: | Como AQUELE painel chama o registro NS. A palavra muda de painel para painel, e procurar "NS" onde está escrito "Servidor de nomes" é onde a pessoa desiste. examples: ["Add record → NS"] name_field_hint: type: string description: | O que digitar no campo de nome. É a pegadinha mais comum: alguns painéis querem só `apps`, outros querem `apps.acme.com.br` inteiro, e errar cria a delegação no lugar errado sem nenhuma mensagem de erro. examples: ["Digite apenas `apps` — a Cloudflare completa o domínio."] supports_ttl: { type: boolean } steps: type: array items: type: object required: [text] properties: text: { type: string } note: { type: [string, 'null'] } caveats: type: array description: O que costuma dar errado nesse painel específico. items: { type: string }