# El firmador del chat web — Vocero

> La guía para desarrolladores: el contrato del aval, la identidad avalada, y qué mirar cuando quien escribe sigue apareciendo como anónimo.

Guía oficial: https://vocero.net/documentacion/chat-web/firmador/ · Más documentación: https://vocero.net/llms.txt

**Sin firma no se abre ninguna conversación.** Estos archivos son el firmador ya escrito, listo para copiar: van en el servidor del sitio, guardan la clave secreta del canal y le entregan un aval al widget cada vez que alguien abre el chat.

El porqué, en corto: la comunicación va del navegador del visitante a Vocero, y ninguna capa del protocolo certifica de qué sitio salió. Lo único que lo contesta es un secreto que el atacante no pueda leer, o sea uno guardado del lado del servidor del sitio.

## Lo que hay que saber antes de copiar nada

- **La clave pública** (`wc_…`) va en la etiqueta `<script>` de la página. Está a la vista y no protege nada.
- **La clave secreta** (`wcs_…`) va **solo en el servidor**. Nunca en el HTML, nunca en un archivo JavaScript que el navegador descargue, nunca en un repositorio público.
- **El aval se emite en el momento, no se escribe en la página.** Si el servidor estampara la firma dentro del HTML, cualquiera baja esa página mil veces y se lleva mil firmas válidas. Por eso el widget la pide cuando la persona abre el chat.
- Vale **dos minutos**, se usa **una sola vez** y queda atado a **ese visitante**.

## Qué tiene que devolver

El widget le pide al sitio, por `POST`, una dirección propia —por ejemplo `/vocero-aval`— y espera este JSON:

```json
{
  "visitorId": "v_xxx",
  "issuedAt": 1757030400,
  "signature": "…"
}
```

Donde `signature` es el HMAC-SHA256, en hexadecimal, de estas tres partes unidas con saltos de línea:

```
<clave pública>
<identificador del visitante>
<issuedAt>
```

Si el widget no manda un `visitorId` —es la primera visita— el firmador genera uno.

## Los archivos

| Archivo                                                        | Para                                                              |
| -------------------------------------------------------------- | ----------------------------------------------------------------- |
| <a href="/firmadores/node.js" download>node.js</a>                                           | Node, Express o cualquier servidor JavaScript                     |
| <a href="/firmadores/cloudflare-worker.js" download>cloudflare-worker.js</a>                 | Un Worker de Cloudflare                                           |
| <a href="/firmadores/cloudflare-pages-function.js" download>cloudflare-pages-function.js</a> | Un sitio en Cloudflare **Pages**                                  |
| <a href="/firmadores/firmador.php" download>firmador.php</a>                                 | PHP, incluido un WordPress a mano                                 |
| <a href="/firmadores/node-con-identidad.js" download>node-con-identidad.js</a>               | Una aplicación **con sesión**, donde el servidor ya sabe quién es |
| <a href="/firmadores/firmador_python.py" download>firmador_python.py</a>                     | Python —FastAPI, Django, Flask—, **con identidad**                |

En WooCommerce y en Shopify no hace falta copiar nada: lo trae el plugin.

**Pages y Workers no son intercambiables**, aunque los dos sean de Cloudflare y
el firmador sea el mismo: un Worker exporta `export default { fetch }` y una
Pages Function exporta `onRequestPost`. Poner el del Worker en `functions/` no
da ningún error — el archivo se publica y la dirección devuelve 404, o peor, el
HTML del sitio.

## Cuando la aplicación ya sabe quién es

En un sitio público quien escribe es un desconocido. Dentro de una aplicación con sesión no: el servidor **ya sabe quién es**, y hacérselo escribir es pedirle un dato que se puede equivocar y que además no prueba nada.

Para eso el aval lleva una segunda firma, la de la **identidad**, con su propia cadena y su propia vigencia:

|               | Qué firma                                             | Vale      |
| ------------- | ----------------------------------------------------- | --------- |
| **Aval**      | `siteKey · visitorId · issuedAt`                      | 2 minutos |
| **Identidad** | `siteKey · visitorId · externalId · email · issuedAt` | 1 hora    |

Las vigencias son distintas a propósito: el aval se pide cada vez que alguien abre el chat, y la identidad se firma una vez al cargar la página — entre que carga y la persona se decide a escribir puede pasar un rato largo.

**El nombre viaja pero no se firma.** Lo avalado son los identificadores: es lo que decide si se le puede dar información de una cuenta. Firmar también el nombre obligaría a re-firmar cada vez que alguien se corrige el apellido, sin ganar nada.

Y la diferencia no es comodidad: sobre un nombre **declarado** —que lo manda el navegador y cualquiera lo cambia por el de otro— un agente no debería actuar; sobre uno **avalado** sí.

⚠️ Los datos del usuario salen de **la sesión del servidor**, nunca del cuerpo del pedido. Tomándolos de lo que manda el navegador, cualquiera se avala como cualquiera y la firma deja de probar nada.

## Lo que el firmador tiene que rechazar

Los tres archivos traen una revisión de sus dos variables antes de firmar, y
**conviene no sacarla**. Cubre los dos errores de la familia, que son el mismo
mirado de los dos lados:

- **Falta una variable.** Sin la revisión, `VOCERO_SITE_KEY` sin cargar hace
  firmar sobre la cadena `"undefined"`: sale una firma perfectamente formada y
  perfectamente inútil. Del otro lado se ve un `AVAL_INVALIDO` que no dice cuál
  de las dos falta, y se busca el problema en el secreto —que estaba bien—.
  Pasó el 2026-09-08 y costó media noche.
- **La secreta en el lugar de la pública.** Las dos son cadenas y nada las
  distingue a simple vista, salvo el prefijo: `wc_` la pública, `wcs_` la
  secreta. Confundirlas además **pone la secreta en el HTML**, que es lo único
  que este canal no puede permitir.

Con la revisión, el firmador devuelve un 500 que dice exactamente qué falta, y
se ve en un `curl` antes de tocar el sitio.

## Si tu aplicación autentica con un token, no con cookie

**Es el caso de casi cualquier aplicación de una sola página**, y hay que
saberlo antes de escribir el firmador con identidad.

Lo que el widget le pide al sitio —el aval y el contexto— va con
`credentials: 'same-origin'`, así que **solo lleva cookies**. Si tu sesión viaja
en una cabecera `Authorization` puesta por JavaScript, esos pedidos llegan a tu
backend **sin ninguna credencial**, y no tiene cómo saber quién es el usuario
por más que el endpoint esté detrás de la sesión.

La salida es que **la página los entregue**, usando el token que ya tiene:

```js
// Antes de que cargue el widget.
window.voceroAval = async (visitorId) => {
  const r = await fetch('/api/vocero-aval', {
    method: 'POST',
    headers: { 'content-type': 'application/json', authorization: `Bearer ${miToken}` },
    body: JSON.stringify(visitorId ? { visitorId } : {}),
  });
  return r.json();
};
```

Y si además querés mandar contexto que cambia por visitante:

```js
window.voceroDynamicContext = async (visitante) => {
  const r = await fetch(`/api/vocero-contexto?visitante=${visitante}`, {
    headers: { authorization: `Bearer ${miToken}` },
  });
  return r.json();
};
```

El firmador del backend es el mismo de esta carpeta; lo único que cambia es
quién lo llama. **Si tu sesión sí va por cookie, no hace falta nada de esto**:
el widget lo pide solo y las cookies viajan.

## Si quien escribe sigue apareciendo como anónimo

El chat abre, así que **el aval está bien**: lo que falla es la identidad, que
se firma aparte. No hay ningún error a la vista, ni en la consola del navegador
ni en el panel, así que conviene descartar en este orden:

1. **Se firmó para otro visitante.** La identidad se ata al `visitorId` —esa es
   toda la gracia: la de otro no sirve— así que hay que firmar **el visitante
   que llega en el pedido**, no uno nuevo. Es el error más común, y el más
   fácil de cometer si el firmador genera un identificador cuando no lo recibe.
2. **La cadena está mal armada.** Son cinco partes en este orden:
   `siteKey`, `visitorId`, `externalId`, `email`, `issuedAt`, separadas por
   saltos de línea. **El nombre no va.** Una parte de más o de menos da una
   firma perfectamente formada y perfectamente inútil.
3. **Venció.** La identidad dura una hora; el aval, dos minutos. Si el firmador
   la emite al cargar la página y la persona escribe mucho después, hay que
   emitirla al pedir el aval y no antes.
4. **Tu aplicación autentica por cabecera y no por cookie.** Ver la sección de
   arriba: el firmador está contestando correctamente que no hay sesión, porque
   el pedido le llega sin credencial.

Y una que no es un error: si la conversación ya estaba abierta **antes** de que
la persona se identificara, el nombre aparece recién con el mensaje siguiente.
Lo que ya se publicó en el grupo no se reescribe.

## Qué conviene agregarle

El firmador es una dirección pública del sitio del cliente, así que **conviene limitarle la frecuencia**: un aval por visitante cada pocos segundos alcanza de sobra.

Con una salvedad, para no cortarse el pie: el widget también le pide el aval **al retomar una sesión**, o sea una vez por carga de página mientras la conversación siga viva. Es lo que permite identificar a quien entró sin sesión, se identificó y recién después abrió el chat — el orden más normal de todos. Un límite de uno cada varios minutos deja a esa persona como anónima. Sin eso, alguien puede pedirle avales en bucle — y esa es la única superficie que le queda a un atacante una vez que la firma es obligatoria.

---

Los pasos de instalación —el plugin de WooCommerce, el fragmento de Shopify, la etiqueta— están en [la guía del chat web](/documentacion/chat-web/).
