# 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](/es/guides/publish-and-share.md).

Incrustar es una de las dos respuestas

Incrustar pone *nuestro* panel renderizado dentro de un rectángulo de tu página. La otra respuesta es [expulsar](/es/guides/eject-your-dashboard.md) — 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](/es/guides/embed-or-eject.md)**.

## El carril público[​](#el-carril-público "Enlace directo al 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](#the-boundary-of-the-embed) 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[​](#el-carril-de-token-firmado "Enlace directo al 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[​](#tu-backend-genera-el-token--tú-no-firmas-nada "Enlace directo al 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 existe ningún "secreto de incrustación"

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[​](#filtros-de-fila "Enlace directo al 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 `scope` de su respuesta. Léelo; no lo supongas.

#### Lee `scope.fullyScoped` — no es «los filtros fueron aceptados»[​](#read-fullyscoped "Enlace directo al read-fullyscoped")

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[​](#presenta-el-token "Enlace directo al 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[​](#configura-tu-lista-de-dominios-permitidos "Enlace directo al 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[​](#el-ciclo-de-renovación-del-token "Enlace directo al 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:

1. El frame envía **`EMBED_TOKEN_EXPIRED`** (o `EMBED_TOKEN_INVALID`) a tu página.
2. Tu página le pide a tu backend un token nuevo.
3. Tu página envía **`UPDATE_TOKEN`** al 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[​](#lo-que-tu-página-realmente-recibe "Enlace directo al 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[​](#the-boundary-of-the-embed "Enlace directo al 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[​](#próximos-pasos "Enlace directo al Próximos pasos")

* **[Quién puede ver qué](/es/guides/who-can-see-what.md)** — 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](/es/guides/publish-and-share.md)** — llevar un panel a `published` y elegir su `view_mode`.
* **[API Reference](/es/reference/vibedasher-sdk-api.md)** — la operación `embed-tokens` completa.
