Incrustar un panel
Incrustar es el punto en el que un panel deja de ser algo que tu equipo abre y se convierte en parte de un producto que usan tus clientes. Vibedasher tiene dos formas de hacerlo, y la diferencia no es cosmética — decide quién puede leer los datos.
| Carril público | Carril de token firmado | |
|---|---|---|
view_mode del panel | public | protected |
| Quién puede verlo | cualquiera con la URL | solo un visor para el que tu backend generó un token |
| Filtrado de fila por visor | no | sí, aplicado en el servidor |
| Qué incrustas | una URL | una URL más un token de corta duración |
Un panel private no es incrustable por ninguno de los dos carriles. Ambos carriles
requieren que el panel esté publicado primero — mira
Publicar y compartir.
Incrustar pone nuestro panel renderizado dentro de un rectángulo de tu página. La otra respuesta es expulsar — llevarte el código React y SQL y renderizarlo tú mismo, de forma nativa, con nosotros fuera de la ruta de renderizado y solo el motor de datos por debajo. Cuál de los dos quieres lo deciden sobre todo dos preguntas: ¿tienen que ver filas distintas visores distintos? (incrustar es el único carril que hace eso), y ¿tiene el panel que verse y comportarse como el resto de tu aplicación? (solo expulsar te da eso). Las contrapartidas están puestas una al lado de la otra en Incrustar o expulsar.
El carril público
Si el panel está publicado con view_mode: public, un iframe es toda la integración:
<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 junto a allow-scripts y es seguro aquí
específicamente porque el frame es de origen cruzado respecto a tu página: le concede al
renderizador su propio origen, no el tuyo. allow-top-navigation está deliberadamente
ausente, así que el frame no puede navegar tu página fuera de ella. allow-downloads es lo
que hace funcionar la exportación de gráficos a PNG.
El fragmento funciona desde cualquier página que controles — no hay nada que configurar de nuestro lado primero. La audiencia bajo la que publicas es el control que importa aquí, y El límite del marco es donde se expone esa elección.
El diálogo Compartir en la aplicación te da este fragmento ya rellenado. Cógelo de ahí en lugar de ensamblar la URL a mano.
El carril de token firmado
Este es el que vale la pena entender, porque es el único sitio donde Vibedasher hace seguridad a nivel de fila.
Tu backend genera el token — tú no firmas nada
Vibedasher firma el token él mismo, ES256, con su propia clave rotatoria. Tu backend pide uno con su clave 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,
});
No tienes ningún material de firma ni lo necesitas — genera los tokens como se muestra aquí.
O con HTTP plano, desde cualquier lenguaje:
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}
El cuerpo de la petición acepta:
| Campo | Obligatorio | Notas |
|---|---|---|
vizId | sí | Exactamente un panel. Un token no puede cubrir una carpeta o una cuenta. |
filters | no | Los filtros de fila. Mira más abajo. |
viewerId | no | Un id de asiento opaco. Solo auditoría y medición — no es una entrada de control de acceso. No confíes en él para acotar nada. |
ttlSeconds | no | Por defecto 600. Suelo 60, techo máximo 86 400 (24 h). Los valores fuera de rango se acotan al generar el token y se vuelven a comprobar al verificarlo. La verificación admite un pequeño margen de desfase de reloj por encima de exp, limitado a 120 s y a la mitad de la vida útil del propio token: así la vida útil efectiva nunca supera 1,5× expires_in y, a partir de un TTL de 240 s, el margen es el fijo de 120 s. |
apiKeyId | no | Solo necesario al generarlo desde una sesión de navegador, para vincular el token a la lista de dominios permitidos y la revocación de una clave. |
El account_id se estampa desde tu credencial y nunca se lee del cuerpo.
Generar el token falla con un mensaje que nombra la comprobación que falló — estás generando un token, no presentándolo, así que el error te dice qué cambiar en el panel o en la clave:
| Estado | Detalle | Solución |
|---|---|---|
403 | la visualización pertenece a otra cuenta | genera con una clave de API de la cuenta propietaria |
403 | la visualización no se puede incrustar con un token: su visibilidad debe ser protected | cambia la visibilidad del panel a Protegido en el diálogo Compartir |
404 | la visualización no está publicada | publícala primero |
403 | no hay ninguna clave de API utilizable a la que vincular el token | la clave falta, está inactiva, caducada o pertenece a otra cuenta |
404 | Viz no encontrada | comprueba vizId |
Un panel public es rechazado por este carril — ya tiene el carril anónimo, y mezclar
los dos sería una forma de creer que tienes filtros de fila cuando no los tienes.
Filtros de fila
filters es un mapa de nombre de columna a un escalar o una lista. Un escalar se convierte
en =, una lista en IN, y varias claves se combinan con Y. Las claves son el sqlName de
la columna tal como lo informa GET /v1/datasets/{id} — el nombre que expone la vista SQL,
no el nombre visible.
La plataforma no añade una cláusula WHERE a tu consulta; reescribe cada referencia a un
conjunto de datos como una relación filtrada, conservando el alias original:
FROM orders
-> FROM (SELECT * FROM "orders" WHERE "tenant" = 'acme') AS orders
Eso es lo que hace que el alcance sea no ampliable, y no solo que esté presente. Lo que
la página pida se aplica fuera de la relación filtrada, así que solo puede estrecharla más
— WHERE tenant = 'globex' OR 1=1 sigue viendo un solo inquilino, una subconsulta
envolvente envuelve la relación filtrada, ambas ramas de un UNION se reescriben, y una CTE
que oculta el alias, una referencia calificada por esquema y cualquier función que lea
archivos se rechazan de plano.
Tres propiedades lo hacen auditable:
- Los filtros provienen únicamente del token verificado. Nunca se leen del cuerpo de la petición, así que un visor hurgando en la pestaña de red no puede ampliar su propio alcance.
- Una clave de filtro que no nombra ninguna columna de los conjuntos de datos del panel se
rechaza al emitir —
403, nombrando la clave y listando las columnas que sí existen. No se firma ningún token. Lo mismo ocurre con un valor que no puede convertirse al tipo de su columna. Un filtro que coincidiera silenciosamente con todo sería una fuga de datos disfrazada de control de seguridad, así que se rechaza antes de poder convertirse en un enlace que entregas a un cliente. - La emisión te dice qué permite el token, en el campo
scopede su respuesta. Léelo; no lo supongas.
Lee scope.fullyScoped — no es «los filtros fueron aceptados»
Un panel puede leer más de un conjunto de datos, y un filtro solo puede aplicarse a un
conjunto que tenga esa columna — no se puede filtrar por una columna que una tabla no
tiene. Así, un panel sobre orders y price_list, con alcance {"tenant": "acme"}, filtra
orders y lee price_list entero. La página carga, cada mosaico se dibuja, cada número
es plausible, y nada en la petición parece incorrecto.
Eso es correcto y deseado cuando el segundo conjunto es una tabla de consulta compartida —
una lista de precios, un catálogo de planes, un conjunto de etiquetas. No es lo que
quieres cuando el segundo conjunto contiene filas por cliente bajo una columna con otro
nombre (tenant_id en vez de tenant, customer, org). La respuesta de emisión es donde
descubres en cuál de los dos casos estás:
{
"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 | Qué hacer con él |
|---|---|
fullyScoped | El que hay que afirmar. true solo cuando cada conjunto de datos que el panel lee está acotado por estos filtros. Cualquier otra cosa es false. |
filtered[].predicate | El SQL exacto aplicado a ese conjunto de datos. Es la frase que se pone delante de un revisor de seguridad. |
unfiltered[] | Conjuntos de datos que este token lee enteros. Decide, para cada uno, si es una tabla compartida o un hueco. |
unresolvedDatasetIds | Conjuntos cuyo esquema no pudo leerse. No es lo mismo que «sin filtrar»: «lee todo» y «no sabemos qué lee esto» son respuestas distintas, y ninguna se funde en la otra. |
Una comprobación de producción razonable en tu ruta de emisión es negarte a devolver el
token cuando scope.fullyScoped sea false y esperabas true — tú sabes cuáles de tus
paneles deben ser por cliente, y la plataforma no.
Presenta el token
El frame no puede fijar una cabecera, así que el token viaja en la URL:
<iframe
src="https://cloud.vibedasher.com/v/<slug>?embed_token=<token>"
sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"
allowfullscreen
></iframe>
Las respuestas de incrustación se envían con Referrer-Policy: no-referrer para que el
token no se filtre en una cabecera Referer en subpeticiones. Sigue siendo una credencial
portadora en una URL, que es la razón real por la que el TTL por defecto es de diez minutos:
genera uno por carga de página, mantenlo corto, y renuévalo en lugar de subir el TTL.
Configura tu lista de dominios permitidos
Una clave de API puede llevar una lista de dominios desde los que se aceptarán sus tokens de
incrustación. Una lista vacía significa permitir todo. Es una decisión deliberada para
que la funcionalidad funcione en el momento en que la pruebas, y significa que una clave sin
configurar no protege nada a nivel de origen. Configura allowed_domains en la clave antes
de pasar a producción.
El ciclo de renovación del token
Un token de diez minutos en un panel que alguien deja abierto toda la tarde necesita renovarse, y el renderizador te avisa cuándo:
- El frame envía
EMBED_TOKEN_EXPIRED(oEMBED_TOKEN_INVALID) a tu página. - Tu página le pide a tu backend un token nuevo.
- Tu página envía
UPDATE_TOKENal 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');
}
});
Comprueba siempre e.origin. UPDATE_TOKEN es el único mensaje que un host de
terceros puede enviar hacia dentro — todo lo demás se ignora.
Lo que tu página realmente recibe
Los mensajes que recibe un host de terceros son una lista fija y cerrada de trece. Esta es la lista completa:
| Mensaje | Cuándo |
|---|---|
READY | El renderizador arrancó. |
LOADED | El panel terminó de cargar. |
RESIZE | La altura del contenido cambió — úsalo para dimensionar el iframe. |
BUNDLE_STARTED / BUNDLE_COMPLETED / BUNDLE_FAILED / BUNDLE_RETRY | Ciclo de vida del build del código del panel. |
RUNTIME_ERROR | El código del panel lanzó un error. |
EMPTY_DATA | Una consulta no devolvió filas. |
RENDER_HEALTH | Salud de renderizado por mosaico. |
EMBED_TOKEN_EXPIRED / EMBED_TOKEN_INVALID / EMBED_UNAVAILABLE | Ciclo de vida del token — mira arriba. |
Cualquier otra cosa que puedas ver referenciada es interna. En particular, un conjunto de
mensajes se declara explícitamente como nunca reenviado a un host de terceros —
VIZ_DATA, FILES, UPDATE_TOKEN saliente, SQL_ERROR, CONSOLE_ERROR,
REQUEST_NEW_AUTH_TOKEN, ELEMENT_SELECTED, FILTER_STATE_CHANGED — porque llevan el
código fuente de tu panel o el estado bruto de sus consultas.
El límite del marco
Dos propiedades deciden sobre qué puede construirse una integración, y merece la pena leer las dos antes de diseñar alrededor del marco en lugar de después.
El marco es dueño de su propio estado de filtrado. Los mensajes que recibe un anfitrión externo son los listados arriba; el estado de filtrado del panel no está entre ellos, en ninguna dirección. El marco es una vista autónoma: condúcelo por la URL y el token que emites, y déjale ser dueño de lo que ocurre dentro. Si tu interfaz necesita compartir una selección con el panel, pon esa selección en el predicado de filtrado del token, que la plataforma añade a cada consulta que autoriza — del lado del servidor, y con la misma respuesta para cada tarjeta.
Elige la audiencia pensando en el enmarcado. Un panel publicado como public es una URL
pública, con todo lo que eso implica; el carril de token firmado es el que hay que usar
cuando importa quién mira y desde dónde, y la lista de dominios permitidos de una clave de
API es lo que ata sus respuestas a tu origen.
El carril de consulta con token firmado corre en eu-central-1. Ahí vive el motor de
consulta de las incrustaciones protegidas. Si los datos de tu cuenta están en otra región,
háblanos antes de construir sobre este carril.
Próximos pasos
- Quién puede ver qué — dónde encaja el filtrado a nivel de fila en el modelo de permisos más amplio, y por qué existe solo aquí.
- Publicar y compartir — llevar un panel a
publishedy elegir suview_mode. - API Reference — la operación
embed-tokenscompleta.