Saltar al contenido
vocero.net

Documentación

El firmador del chat web

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

Versión en Markdown — para copiar entera o dársela a una IA

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:

{
  "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
node.js Node, Express o cualquier servidor JavaScript
cloudflare-worker.js Un Worker de Cloudflare
cloudflare-pages-function.js Un sitio en Cloudflare Pages
firmador.php PHP, incluido un WordPress a mano
node-con-identidad.js Una aplicación con sesión, donde el servidor ya sabe quién es
firmador_python.py 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.

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:

// 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:

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.

Última actualización: