Saltar al contenido principal

Recipe: Generar Widget Tokens server-to-server

Esta es la pieza crítica de cualquier integración custom: el cliente no firma el token, lo solicita server-to-server con la Shop API Key y lo entrega al frontend en cada render. El token vive 24 horas y rota a discreción.

Tiempo estimado: 15-25 min la primera vez.

¿Por qué este patrón?

  • La Shop API Key (sk_…) jamás llega al navegador. Solo vive en tu backend.
  • El Widget Token es opaco (no JWT). Lo emite Neuroon, lo consume el widget en el navegador.
  • Si una key se filtra, rotas la key en el dashboard y los tokens activos siguen vivos hasta su TTL natural.

Aclaración: la documentación anterior describía un flujo con HMAC firmado por el cliente. No es real. Ver Authentication · Widget Token para la nota oficial.

El endpoint

POST/api/shops/{id}/widget-token
POST /api/shops/shop_xxxxxxxx/widget-token HTTP/1.1
Host: dev-api.neuroon.ai
X-Shop-API-Key: sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
User-Agent: my-app/1.0

Respuesta:

{
"token": "wt_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
}

El response sólo contiene token — no hay expiresAt. El TTL es fijo a 24 horas desde la emisión. Calcula la expiración localmente al recibir el token: expiresAt = now + 24h.

Estrategia de cache

Tu backend debe:

  1. Cachear el token tras emitirlo, anotando localmente issuedAt = now.
  2. Rotar a las 23 h desde issuedAt (1 h de margen antes del corte de 24 h).
  3. Proteger con lock la sección crítica para evitar emisiones concurrentes.
  4. (Opcional) Distribuir la cache entre instancias (Redis, KV) si corres en serverless o multi-pod.

Esto baja la presión sobre el endpoint de Neuroon y garantiza que el token nunca expire en mitad de un render.

Implementaciones de referencia

neuroon-token.js
import fetch from 'node-fetch';

const TOKEN_TTL_MS = 24 * 60 * 60 * 1000;     // 24 h fijos
const ROTATE_AFTER_MS = 23 * 60 * 60 * 1000;  // rotar a las 23 h

let cache = null; // { token, issuedAt: epoch ms }

async function fetchToken() {
const r = await fetch(
  `${process.env.NEUROON_API_URL}/api/shops/${process.env.NEUROON_SHOP_ID}/widget-token`,
  {
    method: 'POST',
    headers: {
      'X-Shop-API-Key': process.env.NEUROON_API_KEY,
      'User-Agent': 'my-app/1.0',
    },
  },
);
if (!r.ok) throw new Error(`Token issuance failed: ${r.status}`);
const body = await r.json();
return { token: body.token, issuedAt: Date.now() };
}

export async function getWidgetToken() {
const now = Date.now();
if (cache && now - cache.issuedAt < ROTATE_AFTER_MS) return cache.token;
cache = await fetchToken();
return cache.token;
}

// Express endpoint que el frontend consume
import express from 'express';
const router = express.Router();
router.get('/internal/neuroon-token', async (_req, res) => {
try {
  res.json({ token: await getWidgetToken() });
} catch (e) {
  res.status(502).json({ error: e.message });
}
});
export default router;

Variante distribuida con Redis

En entornos serverless (Vercel, Lambda, nuestro cloud) la memoria local se pierde entre invocaciones frías. Usa Redis como cache compartida:

import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);

const ROTATE_AFTER_S = 23 * 60 * 60; // TTL fijo 24 h; rotamos a las 23 h

async function getWidgetTokenShared() {
const cached = await redis.get('neuroon:widget-token');
if (cached) {
const { token, issuedAt } = JSON.parse(cached);
if (Date.now() / 1000 - issuedAt < ROTATE_AFTER_S) return token;
}

// Lock distribuido (SET NX EX) para evitar thundering herd
const ok = await redis.set('neuroon:widget-token:lock', '1', 'NX', 'EX', 30);
if (!ok) {
// Otra instancia está rotando: espera 200ms y reintenta el cache
await new Promise((r) => setTimeout(r, 200));
return getWidgetTokenShared();
}

try {
const { token } = await fetchToken();
const issuedAt = Math.floor(Date.now() / 1000);
// EX = 23 h: la entrada expira justo cuando toca rotar
await redis.set('neuroon:widget-token', JSON.stringify({ token, issuedAt }), 'EX', ROTATE_AFTER_S);
return token;
} finally {
await redis.del('neuroon:widget-token:lock');
}
}

Rotación de token

Si invalidas el token en frío (tras un incidente, rotación de credenciales), regenera invocando el mismo endpoint POST /api/shops/{shopId}/widget-token. Cada llamada emite un token nuevo. No hay revocación inmediata: el token previo sigue siendo válido hasta que expire por TTL (24 h, application.yml:271). Solo deja de usarlo en tus servidores y empieza a servir el nuevo.

El endpoint POST /api/tokens/refresh que aparece en la referencia OpenAPI no rota Widget Tokens — refresca JWTs de usuario del dashboard de Neuroon (TokenController.java:36).

Inyección en el frontend

Una vez tienes el token cacheado, tu frontend lo recibe vía:

  • SSR / RSC: el HTML imprime data-token="..." al renderizar.
  • SPA: tu API privada (GET /internal/neuroon-token) lo devuelve y un effect lo inyecta en el <script> del CDN.

Patrón completo en plugins/custom/standalone y plugins/custom/nextjs.

Validar

# Smoke test: emitir un token
curl -s -X POST "https://dev-api.neuroon.ai/api/shops/$NEUROON_SHOP_ID/widget-token" \
-H "X-Shop-API-Key: $NEUROON_API_KEY" -H "User-Agent: smoke-test/1.0"

# Probar el token contra widget search
curl -s "https://dev-api.neuroon.ai/api/widget/search?q=camiseta&limit=3" \
-H "X-Widget-Token: wt_zzzzzzzzzzzz..."

Errores frecuentes

SíntomaCausaSolución
401 Unauthorized al emitirAPI Key inválida o entorno cruzado (token DEV vs PROD)Comprueba la key y la URL base.
403 Forbidden al emitirAPI Key no autorizada para shopIdConfirma que la key pertenece al shop.
429 Too Many Requests al emitirPegándote al endpoint en cada requestImplementa cache con margen 5 min.
Frontend 401 X-Widget-TokenToken expirado o no impresoRe-renderiza la página o regenera el cache.

Próximos pasos