Skip to content
4 changes: 4 additions & 0 deletions SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@
* [Integraciones](apps/integraciones/README.md)
* [Iframes](apps/integraciones/iframes.md)
* [Power Apps](apps/integraciones/power-apps.md)
* [Extensiones](apps/extensiones/README.md)
* [Iframe](apps/extensiones/iframe.md)
* [Web Components](apps/extensiones/web-components.md)
* [Protocolo postMessage](apps/extensiones/postmessage.md)

## VPaaS

Expand Down
53 changes: 53 additions & 0 deletions apps/extensiones/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
description: >-
Las extensiones permiten embeber aplicaciones externas (iframe o web
components) dentro de los productos de Videsk, configurables por segmento
y servicio (calendario).
---

# Extensiones

Las extensiones son aplicaciones embebidas que se renderizan dentro de la Consola de Videsk. Pueden ser de dos tipos:

| Tipo | Descripcion | Aislamiento |
| --- | --- | --- |
| **iframe** | Carga una URL externa en un iframe con sandbox | Completo (proceso separado) |
| **web-component** | Carga un modulo JS que define un Custom Element | Parcial (mismo contexto) |

## Caracteristicas

- **Asociables por entidad**: cada extension se asocia desde el segmento o servicio (calendario) que la necesita, mediante su campo `extensions[]`. Esto permite configuraciones flexibles por equipo o tipo de calendario.
- **Placement flexible**: pueden renderizarse en el panel de llamada (`call-panel`), la barra lateral (`sidebar`) o como pagina independiente (`standalone`).
- **Permisos controlados**: los iframes reciben atributos `sandbox` y `allow` validados contra una lista blanca del backend.
- **Contexto de host**: la extension recibe datos del contexto actual (usuario, segmento, llamada, contacto) segun los scopes configurados.

## Flujo de configuracion

1. Crear la extension via `POST /extensions` (define que es: nombre, tipo, URL, permisos).
2. Asociar la extension al segmento o servicio via `PATCH /segments/:id` o `PATCH /services/:id` agregando el ID al campo `extensions[]`.

## Tipos de extension

### Iframe

Ideal para aplicaciones que necesitan aislamiento completo. La pagina embebida corre en un proceso separado del navegador, con permisos controlados por `sandbox` y `allow`.

{% content-ref url="iframe.md" %}
[iframe.md](iframe.md)
{% endcontent-ref %}

### Web Component

Ideal para integraciones ligeras que necesitan acceso directo al DOM del host. El modulo JS registra un Custom Element que se monta directamente en la consola.

{% content-ref url="web-components.md" %}
[web-components.md](web-components.md)
{% endcontent-ref %}

## Protocolo de comunicacion

Ambos tipos de extension se comunican con el host mediante un protocolo basado en `postMessage` (iframes) o `CustomEvent` (web components).

{% content-ref url="postmessage.md" %}
[postmessage.md](postmessage.md)
{% endcontent-ref %}
207 changes: 207 additions & 0 deletions apps/extensiones/iframe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
---
description: >-
Como configurar extensiones de tipo iframe en la Consola de Videsk,
incluyendo permisos, sandbox, templating dinamico de URL y requisitos de
seguridad.
---

# Iframe

Las extensiones de tipo `iframe` cargan una pagina web externa dentro de un iframe aislado. Es el mecanismo recomendado para la mayoria de integraciones.

## Requisitos

### HTTPS obligatorio

La URL de la extension **debe** servirse sobre `https://`. Contenido HTTP sera bloqueado por el navegador.

### Content-Security-Policy

Tu aplicacion debe permitir ser embebida por los dominios de Videsk:

```
Content-Security-Policy: frame-ancestors 'self' https://*.videsk.io;
```

Si tu app usa `X-Frame-Options`, eliminalo a favor de CSP.

### Cookies

Si la app embebida requiere sesion con cookies:

```
Set-Cookie: session=xxx; SameSite=None; Secure; HttpOnly
```

Si usa `localStorage` o `sessionStorage`, no es necesario.

## Esquema de configuracion

Al crear una extension via `POST /extensions`:

```json
{
"name": "Mi App",
"kind": "iframe",
"placement": "call-panel",
"iframe": {
"url": "https://app.example.com/embed?call={{encode call.id}}&agent={{encode user.email}}",
"allow": ["camera", "microphone", "clipboard-write"],
"sandbox": ["allow-scripts", "allow-same-origin", "allow-forms"],
"width": "100%",
"height": "500px"
},
"contextScopes": ["user", "segment", "call"]
}
```

Luego, asocia la extension al segmento o servicio (calendario) que corresponda:

```
PATCH /segments/:id
{ "extensions": ["<extension_id>"] }
```

```
PATCH /services/:id
{ "extensions": ["<extension_id>"] }
```

### Campos de `iframe`

| Campo | Tipo | Requerido | Descripcion |
| --- | --- | --- | --- |
| `url` | String | Si | URL HTTPS, admite templating handlebars |
| `allow` | String[] | No | Permisos del atributo `allow` del iframe |
| `sandbox` | String[] | No | Valores del atributo `sandbox` del iframe |
| `width` | String | No | Ancho CSS (default: `100%`) |
| `height` | String | No | Alto CSS (default: `100%`) |

## Templating dinamico de URL

El campo `iframe.url` admite expresiones [handlebars](https://handlebarsjs.com/) que se resuelven en la Consola justo antes de montar el iframe, usando el contexto de la llamada activa.

### Variables disponibles

| Variable | Descripcion |
| --- | --- |
| `user.id` | ID del agente que contesta |
| `user.email` | Email del agente |
| `user.firstname`, `user.lastname` | Nombre del agente |
| `segment._id`, `segment.name` | Segmento de la llamada |
| `call.id` | ID de la llamada |
| `account` | ID de la cuenta |
| `extraData.*` | Datos extra del cliente (IP, browser, location, etc.) |
| `form` | Valores del formulario base (completado por el cliente) |
| `agentForm` | Valores del formulario del agente |
| `tags` | Tags asociados a la llamada |

### Helpers

Se registran automaticamente todos los helpers del paquete `@videsk/handlebars-helpers` (los mismos que usan los webhooks: `#if`, `#each`, `#date`, `#phone`, `#jwt`, etc.).

Adicionalmente, para templating de URL:

| Helper | Descripcion |
| --- | --- |
| `{{encode valor}}` | URL-encode del valor (alias: `{{urlEncode valor}}`) |

{% hint style="warning" %}
La URL se compila con `noEscape: true` (sin HTML-escaping) porque no es HTML. Usa siempre `{{encode ...}}` en valores que puedan contener caracteres reservados en URLs (`&`, `=`, `?`, `/`, espacios, etc.).
{% endhint %}

### Ejemplos

```
https://app.ejemplo.com/embed?
call={{encode call.id}}
&agent={{encode user.email}}
&segmento={{encode segment.name}}
&ip={{encode extraData.ip}}
```

```
https://crm.ejemplo.com/contactos/{{encode call.id}}?rut={{encode form.rut}}
```

### Cuando se resuelve

La URL se resuelve **una sola vez** al momento de montar el iframe (inicio de la llamada o al habilitarse la extension). No se recompila cuando cambian valores reactivos (por ejemplo, mientras el agente llena el `agentForm`).

Si necesitas enviar datos a la app embebida en tiempo real durante la llamada, usa el protocolo `postMessage`.

## Permisos `allow`

Valores aceptados por el backend (otros seran rechazados):

| Valor | Descripcion |
| --- | --- |
| `camera` | Acceso a la camara |
| `microphone` | Acceso al microfono |
| `display-capture` | Captura de pantalla |
| `clipboard-write` | Escritura en clipboard |
| `clipboard-read` | Lectura de clipboard |
| `autoplay` | Reproduccion automatica de audio/video |
| `fullscreen` | Modo pantalla completa |
| `geolocation` | Acceso a ubicacion |
| `picture-in-picture` | Modo picture-in-picture |

**Valores por defecto** (si no se especifican):

```
camera; microphone; display-capture; clipboard-write; autoplay
```

## Permisos `sandbox`

Valores aceptados:

| Valor | Descripcion |
| --- | --- |
| `allow-scripts` | Ejecucion de JavaScript |
| `allow-same-origin` | Acceso a recursos del mismo origen |
| `allow-forms` | Envio de formularios |
| `allow-popups` | `window.open()` y links `target="_blank"` |
| `allow-popups-to-escape-sandbox` | Popups sin restricciones de sandbox |
| `allow-downloads` | Descargas de archivos |
| `allow-modals` | `alert()`, `confirm()`, `prompt()` |

**Valores por defecto:**

```
allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads
```

{% hint style="warning" %}
Valores como `allow-top-navigation` no son aceptados por seguridad, ya que permitirian a la extension navegar la ventana principal.
{% endhint %}

## HTML renderizado

El iframe resultante en la consola sera similar a:

```html
<iframe
src="https://app.example.com/embed?call=68123abc&agent=matias%40videsk.io"
allow="camera; microphone; display-capture; clipboard-write; autoplay"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads"
referrerpolicy="no-referrer"
loading="lazy"
/>
```

## Asociacion por entidad

Las extensiones se asocian a **segmentos** y/o **servicios** (calendarios) desde la propia entidad:

```
PATCH /segments/:segmentId
{ "extensions": ["ext_id_1", "ext_id_2"] }
```

```
PATCH /services/:serviceId
{ "extensions": ["ext_id_1"] }
```

Una misma extension puede estar asociada a multiples segmentos y servicios. Cada entidad controla que extensiones tiene habilitadas, lo que permite configuraciones flexibles por equipo o tipo de calendario.
124 changes: 124 additions & 0 deletions apps/extensiones/postmessage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
description: >-
Protocolo de comunicacion entre el host (Consola Videsk) y las extensiones
embebidas via iframe o web component.
---

# Protocolo postMessage

Las extensiones se comunican con la consola mediante un protocolo tipado. Para iframes se usa `window.postMessage`; para web components se usa `CustomEvent`.

## Handshake

Al cargar una extension, el host envia un mensaje de inicializacion:

### Host -> Extension

```json
{
"type": "videsk:init",
"context": {
"user": { "id": "abc123", "email": "agent@company.com" },
"segment": { "_id": "seg456", "name": "Ventas" },
"call": { "id": "call789" }
}
}
```

Los campos presentes en `context` dependen de los `contextScopes` configurados en la extension.

### Extension -> Host

La extension puede confirmar que esta lista:

```json
{
"type": "videsk:ready"
}
```

Si la extension envia `videsk:ready` antes de recibir el init, el host reenviara el `videsk:init`.

## Mensajes de la extension al host

### `videsk:resize`

Solicita un cambio de altura del contenedor:

```json
{
"type": "videsk:resize",
"height": 450
}
```

`height` debe ser un numero (pixeles).

### `videsk:emit`

Envia un evento personalizado al host, que se reenvia al backend via socket:

```json
{
"type": "videsk:emit",
"event": "form:submitted",
"payload": { "formId": "abc", "status": "success" }
}
```

En el backend, esto llega como `extension:message` por socket:

```json
{
"extensionId": "ext_id",
"event": "form:submitted",
"payload": { "formId": "abc", "status": "success" }
}
```

## Seguridad

### Iframes

- El host **valida `event.origin`** contra la URL registrada de la extension. Mensajes de otros origenes son ignorados.
- Solo se procesan mensajes cuyo `type` empiece con `videsk:`.
- El iframe corre con `sandbox` y `allow` controlados por el backend.
- Se usa `referrerpolicy="no-referrer"` para no filtrar datos del host.

### Web Components

- Los eventos usan `bubbles: false` para no propagarse al DOM del host.
- El script se carga con `crossOrigin="anonymous"`.
- Opcionalmente se puede verificar integridad con SRI (`integrity`).

## Ejemplo: iframe escuchando el init

```html
<script>
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'videsk:init') {
const { user, segment, call } = event.data.context;
document.getElementById('agent').textContent = user.email;
// Confirmar que estamos listos
event.source.postMessage({ type: 'videsk:ready' }, event.origin);
}
});
</script>
```

## Ejemplo: iframe solicitando resize

```html
<script>
function notifyResize() {
const height = document.body.scrollHeight;
window.parent.postMessage({
type: 'videsk:resize',
height: height,
}, 'https://console.videsk.io');
}

window.addEventListener('load', notifyResize);
new ResizeObserver(notifyResize).observe(document.body);
</script>
```
Loading