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.
Incorporar coloca o nosso painel renderizado dentro de um retângulo na sua página. A outra resposta é ejetar — 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.
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 é 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
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
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,
});
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
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
scopeda resposta. Leia; não presuma.
Leia scope.fullyScoped — não é «os filtros foram aceitos»
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
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
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
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:
- O frame envia
EMBED_TOKEN_EXPIRED(ouEMBED_TOKEN_INVALID) para sua página. - Sua página pede ao seu backend um token novo.
- Sua página envia
UPDATE_TOKENpara 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
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
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
- Quem pode ver o quê — onde a filtragem por linha se encaixa no modelo de permissões mais amplo, e por que ela existe somente aqui.
- Publique e compartilhe — levando um painel a
publishede escolhendo seuview_mode. - Referência da API — a operação
embed-tokenspor completo.