# Incorporando um painel

Incorporar é o ponto em que um painel deixa de ser algo que sua equipe abre e passa a fazer parte de um produto que seus clientes usam. A Vibedasher tem duas formas de fazer isso, e a diferença não é cosmética — ela decide quem pode ler os dados.

|                                      | Via pública               | Via de token assinado                                        |
| ------------------------------------ | ------------------------- | ------------------------------------------------------------ |
| `view_mode` do painel                | `public`                  | `protected`                                                  |
| Quem pode visualizar                 | qualquer pessoa com a URL | somente um visualizador para quem seu backend gerou um token |
| Filtragem por linha por visualizador | não                       | **sim**, aplicada no servidor                                |
| O que você incorpora                 | uma URL                   | uma URL mais um token de curta duração                       |

Um painel `private` não é incorporável por nenhuma das duas vias. Ambas as vias exigem que o painel esteja **publicado** primeiro — veja [Publique e compartilhe](/pt-BR/guides/publish-and-share.md).

Incorporar é uma das duas respostas

Incorporar coloca *o nosso* painel renderizado dentro de um retângulo na sua página. A outra resposta é [ejetar](/pt-BR/guides/eject-your-dashboard.md) — pegar o código-fonte React e SQL e renderizá-lo você mesmo, nativamente, com a gente fora do caminho de renderização e apenas o motor de dados por baixo. Qual das duas você quer é decidido principalmente por duas perguntas: visualizadores diferentes precisam ver linhas diferentes (incorporar é a única via que faz isso), e o painel precisa se parecer e se comportar como o resto do seu app (só ejetar te dá isso). As contrapartidas estão lado a lado em **[Incorporar ou ejetar](/pt-BR/guides/embed-or-eject.md)**.

## A via pública[​](#a-via-pública "Link direto para A via pública")

Se o painel é publicado com `view_mode: public`, um iframe é toda a integração:

```
<iframe

  src="https://cloud.vibedasher.com/v/<slug>"

  width="100%"

  height="800"

  style="border: 0;"

  title="Vibedasher dashboard"

  sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"

  allowfullscreen

  loading="lazy"

></iframe>
```

`allow-same-origin` parece alarmante ao lado de `allow-scripts` e é seguro aqui especificamente porque o frame é **cross-origin em relação à sua página**: ele concede ao renderizador sua própria origem, não a sua. `allow-top-navigation` está deliberadamente ausente, então o frame não pode navegar sua página para outro lugar. `allow-downloads` é o que faz a exportação de gráfico em PNG funcionar.

O trecho funciona a partir de qualquer página que você controle — nada precisa ser configurado do nosso lado antes. A audiência sob a qual você publica é o controle que importa aqui, e [A fronteira do frame](#the-boundary-of-the-embed) é onde essa escolha é apresentada.

O diálogo de compartilhamento no app te dá esse trecho já preenchido. Pegue-o de lá em vez de montar a URL manualmente.

## A via de token assinado[​](#a-via-de-token-assinado "Link direto para A via de token assinado")

Essa é a que vale a pena entender, porque é o único lugar onde a Vibedasher faz **segurança por linha**.

### Seu backend gera o token — você não assina nada[​](#seu-backend-gera-o-token--você-não-assina-nada "Link direto para Seu backend gera o token — você não assina nada")

A Vibedasher assina o token ela mesma, ES256, com sua própria chave rotativa. Seu backend pede um usando sua chave de API:

```
from vibedasher.models import EmbedTokenRequest, EmbedTokenRequestFilters



resp = client.embed_tokens.create_embed(

    EmbedTokenRequest(

        viz_id=1234,

        filters=EmbedTokenRequestFilters.from_dict({"region": "EMEA"}),

        ttl_seconds=600,

    )

)

print(resp.token, resp.expires_in)
```

```
const resp = await client.embedTokens.createEmbed({

  vizId: 1234,

  filters: { region: 'EMEA' },

  ttlSeconds: 600,

});
```

Não existe "segredo de incorporação"

Você não guarda nenhum material de assinatura e não precisa de nenhum — emita tokens como mostrado aqui.

Ou com HTTP puro, a partir de qualquer linguagem:

```
curl -X POST https://api.vibedasher.com/api/v1/embed/token \

  -H "X-Api-Key: $VIBEDASHER_API_KEY" \

  -H "Content-Type: application/json" \

  -d '{"vizId": 1234, "ttlSeconds": 600, "filters": {"region": "EMEA"}}'

# → {"token": "<ES256 JWT>", "token_type": "embed", "expires_in": 600}
```

O corpo da requisição aceita:

| Campo        | Obrigatório | Notas                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `vizId`      | **sim**     | Exatamente um painel. Um token não pode cobrir uma pasta ou uma conta.                                                                                                                                                                                                                                                                                                                                                               |
| `filters`    | não         | Os filtros de linha. Veja abaixo.                                                                                                                                                                                                                                                                                                                                                                                                    |
| `viewerId`   | não         | Um id de assento opaco. **Apenas auditoria e medição — não é uma entrada de controle de acesso.** Não confie nele para delimitar o escopo de nada.                                                                                                                                                                                                                                                                                   |
| `ttlSeconds` | não         | Padrão **600**. Piso 60, teto rígido 86 400 (24 h). Valores fora do intervalo são limitados na geração *e* reverificados na validação. A validação admite uma pequena margem de desvio de relógio acima de `exp`, limitada a 120 s **e** à metade do tempo de vida do próprio token — assim o tempo de vida efetivamente aplicado nunca passa de 1,5× `expires_in` e, a partir de um TTL de 240 s, a margem é o valor fixo de 120 s. |
| `apiKeyId`   | não         | Necessário apenas ao gerar a partir de uma sessão de navegador, para vincular o token à lista de domínios permitidos e à revogação de uma chave.                                                                                                                                                                                                                                                                                     |

O `account_id` é carimbado a partir da sua credencial e nunca é lido do corpo.

A geração falha com uma mensagem que nomeia a verificação que falhou — você está emitindo um token, não apresentando um, então o erro diz o que mudar no painel ou na chave:

| Status | Detalhe                                                                                        | Correção                                                                        |
| ------ | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `403`  | a visualização pertence a outra conta                                                          | emita com uma chave de API da conta proprietária                                |
| `403`  | a visualização não pode ser incorporada com um token: sua visibilidade precisa ser `protected` | mude a visibilidade do painel para **Protegido** no diálogo de compartilhamento |
| `404`  | a visualização não está publicada                                                              | publique-a primeiro                                                             |
| `403`  | nenhuma chave de API utilizável para vincular o token                                          | a chave está ausente, inativa, expirada ou pertence a outra conta               |
| `404`  | Viz não encontrada                                                                             | confira o `vizId`                                                               |

**Um painel `public` é rejeitado por esta via** — ele já tem a via anônima, e misturar as duas seria um jeito de achar que você tem filtros de linha quando não tem.

### Filtros de linha[​](#filtros-de-linha "Link direto para Filtros de linha")

`filters` é um mapa de nome de coluna para um escalar ou uma lista. Um escalar vira `=`, uma lista vira `IN`, e várias chaves são combinadas com E. As chaves são o `sqlName` da coluna como `GET /v1/datasets/{id}` o informa — o nome que a view SQL expõe, não o nome de exibição.

A plataforma não acrescenta uma cláusula `WHERE` à sua consulta; ela **reescreve cada referência a um conjunto de dados como uma relação filtrada**, preservando o alias original:

```
FROM orders

  ->  FROM (SELECT * FROM "orders" WHERE "tenant" = 'acme') AS orders
```

É isso que torna o escopo **não ampliável**, e não apenas presente. O que a página pedir se aplica *fora* da relação filtrada, então só pode estreitá-la ainda mais — `WHERE tenant = 'globex' OR 1=1` continua vendo um único inquilino, uma subconsulta envolvente envolve a relação filtrada, os dois braços de um `UNION` são reescritos, e uma CTE que sombreia o alias, uma referência qualificada por esquema e qualquer função que leia arquivos são recusadas de imediato.

Três propriedades tornam isso auditável:

* **Os filtros vêm apenas do token verificado.** Nunca são lidos do corpo da requisição, então um visualizador fuçando na aba de rede não consegue ampliar o próprio escopo.
* **Uma chave de filtro que não nomeia nenhuma coluna dos conjuntos de dados do painel é recusada na emissão** — `403`, nomeando a chave e listando os nomes de coluna que existem. Nenhum token é assinado. O mesmo vale para um valor que não possa ser convertido para o tipo da sua coluna. Um filtro que silenciosamente correspondesse a tudo seria um vazamento de dados vestido de controle de segurança, então é recusado antes de virar um link que você entrega a um cliente.
* **A emissão diz o que o token permite**, no campo `scope` da resposta. Leia; não presuma.

#### Leia `scope.fullyScoped` — não é «os filtros foram aceitos»[​](#read-fullyscoped "Link direto para read-fullyscoped")

Um painel pode ler mais de um conjunto de dados, e um filtro só pode se aplicar a um conjunto que **tenha aquela coluna** — não se filtra por uma coluna que a tabela não tem. Assim, um painel sobre `orders` e `price_list`, com escopo `{"tenant": "acme"}`, filtra `orders` e lê `price_list` **inteiro**. A página carrega, cada bloco desenha, cada número é plausível, e nada na requisição parece errado.

Isso é correto e desejado quando o segundo conjunto é uma tabela de consulta compartilhada — uma tabela de preços, um catálogo de planos, um conjunto de rótulos. **Não** é o que você quer quando o segundo conjunto guarda linhas por cliente sob uma coluna com outro nome (`tenant_id` em vez de `tenant`, `customer`, `org`). A resposta da emissão é onde você descobre em qual dos casos está:

```
{

  "token": "<ES256 JWT>", "token_type": "embed", "expires_in": 600,

  "scope": {

    "vizId": 1234,

    "filtered":   [{"datasetId": 7, "datasetName": "orders",

                    "predicate": "\"tenant\" = 'acme'", "columns": ["tenant"]}],

    "unfiltered": [{"datasetId": 8, "datasetName": "price_list",

                    "predicate": null, "columns": []}],

    "unresolvedDatasetIds": [],

    "fullyScoped": false,

    "meaning": "PARTIALLY SCOPED — read this before handing the link out. ..."

  }

}
```

| Campo                  | O que fazer com ele                                                                                                                                                       |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fullyScoped`          | **O campo em que se deve asseverar.** `true` somente quando todo conjunto de dados que o painel lê está delimitado por esses filtros. Qualquer outra coisa é `false`.     |
| `filtered[].predicate` | O SQL exato aplicado àquele conjunto. É a frase para colocar diante de um revisor de segurança.                                                                           |
| `unfiltered[]`         | Conjuntos que este token lê **inteiros**. Decida, para cada um, se é uma tabela compartilhada ou uma brecha.                                                              |
| `unresolvedDatasetIds` | Conjuntos cujo esquema não pôde ser lido. Não é o mesmo que não filtrado: «lê tudo» e «não sabemos o que isto lê» são respostas diferentes, e nenhuma é fundida na outra. |

Uma verificação de produção razoável na sua rota de emissão é recusar devolver o token quando `scope.fullyScoped` for `false` e você esperava `true` — você sabe quais dos seus painéis devem ser por cliente, e a plataforma não.

### Apresente o token[​](#apresente-o-token "Link direto para Apresente o token")

O frame não pode definir um cabeçalho, então o token viaja na URL:

```
<iframe

  src="https://cloud.vibedasher.com/v/<slug>?embed_token=<token>"

  sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"

  allowfullscreen

></iframe>
```

Respostas de incorporação são enviadas com `Referrer-Policy: no-referrer` para que o token não vaze em um cabeçalho `Referer` em sub-requisições. Ainda assim é uma credencial bearer em uma URL, que é a razão real pela qual o TTL padrão é de dez minutos: gere um por carregamento de página, mantenha-o curto, e renove-o em vez de aumentar o TTL.

### Configure sua lista de domínios permitidos[​](#configure-sua-lista-de-domínios-permitidos "Link direto para Configure sua lista de domínios permitidos")

Uma chave de API pode carregar uma lista de domínios a partir dos quais seus tokens de incorporação serão honrados. **Uma lista vazia significa permitir tudo.** Essa é uma decisão deliberada para que o recurso funcione no momento em que você o testa, e significa que uma chave não configurada não protege nada no nível de origem. Configure `allowed_domains` na chave antes de ir para produção.

## O ciclo de renovação de token[​](#o-ciclo-de-renovação-de-token "Link direto para O ciclo de renovação de token")

Um token de dez minutos em um painel que alguém deixa aberto a tarde toda precisa ser renovado, e o renderizador te avisa quando:

1. O frame envia **`EMBED_TOKEN_EXPIRED`** (ou `EMBED_TOKEN_INVALID`) para sua página.
2. Sua página pede ao seu backend um token novo.
3. Sua página envia **`UPDATE_TOKEN`** para dentro do frame: `{ type: 'UPDATE_TOKEN', token: '<new>' }`.

```
window.addEventListener('message', async (e) => {

  if (e.origin !== 'https://cloud.vibedasher.com') return;

  if (e.data?.type === 'EMBED_TOKEN_EXPIRED' || e.data?.type === 'EMBED_TOKEN_INVALID') {

    const token = await fetch('/api/vibedasher-embed-token').then((r) => r.text());

    frame.contentWindow.postMessage({ type: 'UPDATE_TOKEN', token }, 'https://cloud.vibedasher.com');

  }

});
```

Sempre verifique `e.origin`. `UPDATE_TOKEN` é a **única** mensagem que um host terceiro pode enviar para dentro — tudo o mais é ignorado.

## O que sua página realmente recebe[​](#o-que-sua-página-realmente-recebe "Link direto para O que sua página realmente recebe")

As mensagens que um host terceiro recebe são uma lista fixa e permitida de treze. Esta é a lista completa:

| Mensagem                                                                 | Quando                                                      |
| ------------------------------------------------------------------------ | ----------------------------------------------------------- |
| `READY`                                                                  | O renderizador inicializou.                                 |
| `LOADED`                                                                 | O painel terminou de carregar.                              |
| `RESIZE`                                                                 | A altura do conteúdo mudou — use para dimensionar o iframe. |
| `BUNDLE_STARTED` / `BUNDLE_COMPLETED` / `BUNDLE_FAILED` / `BUNDLE_RETRY` | Ciclo de vida do build do código do painel.                 |
| `RUNTIME_ERROR`                                                          | O código do painel lançou um erro.                          |
| `EMPTY_DATA`                                                             | Uma consulta não retornou linhas.                           |
| `RENDER_HEALTH`                                                          | Saúde de renderização por tile.                             |
| `EMBED_TOKEN_EXPIRED` / `EMBED_TOKEN_INVALID` / `EMBED_UNAVAILABLE`      | Ciclo de vida do token — veja acima.                        |

Qualquer outra coisa que você veja referenciada é interna. Em particular, um conjunto de mensagens é explicitamente **nunca** encaminhado para um host terceiro — `VIZ_DATA`, `FILES`, `UPDATE_TOKEN` de saída, `SQL_ERROR`, `CONSOLE_ERROR`, `REQUEST_NEW_AUTH_TOKEN`, `ELEMENT_SELECTED`, `FILTER_STATE_CHANGED` — porque carregam ou o código-fonte do seu painel ou seu estado bruto de consulta.

## A fronteira do frame[​](#the-boundary-of-the-embed "Link direto para A fronteira do frame")

Duas propriedades decidem sobre o que uma integração pode ser construída, e vale a pena ler as duas antes de projetar em torno do frame, não depois.

**O frame é dono do seu próprio estado de filtro.** As mensagens que um host externo recebe são as listadas acima; o estado de filtro do painel não está entre elas, em nenhuma das direções. O frame é uma visão autossuficiente: conduza-o pela URL e pelo token que você emite, e deixe que ele seja dono do que acontece lá dentro. Se a sua interface precisa compartilhar uma seleção com o painel, coloque essa seleção no predicado de filtro do token, que a plataforma anexa a toda consulta que autoriza — no servidor, e com a mesma resposta para cada tile.

**Escolha a audiência pensando no enquadramento.** Um painel publicado como `public` é uma URL pública, com tudo o que decorre disso; a via de token assinado é a que se usa quando importa quem está olhando e de onde, e a lista de domínios permitidos de uma chave de API é o que amarra suas respostas à sua origem.

**A via de consulta com token assinado roda em `eu-central-1`.** É lá que vive o runtime de consulta dos embeds protegidos. Se os dados da sua conta estão em outra região, fale com a gente antes de construir nessa via.

## Próximos passos[​](#próximos-passos "Link direto para Próximos passos")

* **[Quem pode ver o quê](/pt-BR/guides/who-can-see-what.md)** — onde a filtragem por linha se encaixa no modelo de permissões mais amplo, e por que ela existe somente aqui.
* **[Publique e compartilhe](/pt-BR/guides/publish-and-share.md)** — levando um painel a `published` e escolhendo seu `view_mode`.
* **[Referência da API](/pt-BR/reference/vibedasher-sdk-api.md)** — a operação `embed-tokens` por completo.
