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
/api/shops/{id}/widget-tokenPOST /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 hayexpiresAt. 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:
- Cachear el token tras emitirlo, anotando localmente
issuedAt = now. - Rotar a las 23 h desde
issuedAt(1 h de margen antes del corte de 24 h). - Proteger con lock la sección crítica para evitar emisiones concurrentes.
- (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
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/refreshque 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íntoma | Causa | Solución |
|---|---|---|
401 Unauthorized al emitir | API Key inválida o entorno cruzado (token DEV vs PROD) | Comprueba la key y la URL base. |
403 Forbidden al emitir | API Key no autorizada para shopId | Confirma que la key pertenece al shop. |
429 Too Many Requests al emitir | Pegándote al endpoint en cada request | Implementa cache con margen 5 min. |
Frontend 401 X-Widget-Token | Token expirado o no impreso | Re-renderiza la página o regenera el cache. |
Próximos pasos
plugins/custom/server-to-server— patrón cross-stack.- Authentication · Widget Token — TTL y rotación.
- Recipe · Next.js + React — caso real con App Router.