Recipe: Firmar Widget Tokens server-to-server
Tu servidor firma localmente el Widget Token usando tu Shop API Key como secreto HMAC. Sin llamar a Neuroon. El widget recibe el token vía data-token y el backend de Neuroon valida la firma en cada request.
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 para tu cliente JavaScript.
- Si una key se filtra, rotas la key en el dashboard y los tokens activos expiran solos por TTL (24 h).
- Sin dependencia adicional: no llamas a Neuroon para cada token; firmar es una operación local de microsegundos.
Formato del token
Base64URL( shopId : unixTimestamp : HMAC-SHA256(hex)( "shopId:unixTimestamp", secret = shopApiKey ) )
shopId:shop_xxx…(tu Shop ID).unixTimestamp: segundos UTC en el momento de firmar.- La firma se calcula sobre
shopId:unixTimestampcon la Shop API Key como secreto HMAC. - El resultado es la concatenación con
:codificada en Base64 URL-safe (sin padding).
Estrategia de cache
Tu servidor debe:
- Cachear el token tras la primera firma.
- Re-firmar con margen de 5 min antes de las 24 h.
- Proteger con lock la sección crítica para evitar firmas concurrentes (no obligatorio: la firma es idempotente, pero ahorra trabajo).
- (Opcional) Distribuir la cache entre instancias (Redis, KV) si corres en serverless o multi-pod.
Implementaciones de referencia
import {createHmac} from 'node:crypto';
const SHOP_ID = process.env.NEUROON_SHOP_ID;
const API_KEY = process.env.NEUROON_API_KEY;
let cache = null; // { token, expiresAt: epoch ms }
function signToken() {
const ts = Math.floor(Date.now() / 1000);
const payload = `${SHOP_ID}:${ts}`;
const sig = createHmac('sha256', API_KEY).update(payload).digest('hex');
const raw = `${payload}:${sig}`;
const token = Buffer.from(raw, 'utf8').toString('base64url');
return {token, expiresAt: (ts + 24 * 3600) * 1000};
}
export function getWidgetToken() {
const now = Date.now();
if (cache && now < cache.expiresAt - 5 * 60_000) return cache.token;
cache = signToken();
return cache.token;
}
// Express endpoint that the frontend consumes
import express from 'express';
const router = express.Router();
router.get('/internal/neuroon-token', (_req, res) => {
res.json({token: getWidgetToken()});
});
export default router;Variante distribuida con Redis
En entornos serverless (Vercel, Lambda) la memoria local se pierde entre invocaciones frías. Usa Redis como cache compartida:
import Redis from 'ioredis';
import {createHmac} from 'node:crypto';
const redis = new Redis(process.env.REDIS_URL);
async function getWidgetTokenShared() {
const cached = await redis.get('neuroon:widget-token');
if (cached) {
const {token, expiresAt} = JSON.parse(cached);
if (Date.now() < expiresAt - 5 * 60_000) return token;
}
// sign locally
const ts = Math.floor(Date.now() / 1000);
const payload = `${process.env.NEUROON_SHOP_ID}:${ts}`;
const sig = createHmac('sha256', process.env.NEUROON_API_KEY).update(payload).digest('hex');
const token = Buffer.from(`${payload}:${sig}`, 'utf8').toString('base64url');
const expiresAt = (ts + 24 * 3600) * 1000;
await redis.set('neuroon:widget-token', JSON.stringify({token, expiresAt}), 'PX', 23 * 3600 * 1000);
return token;
}
Forzar rotación
Si quieres invalidar todos los tokens activos en frío (tras un incidente, rotación de credenciales), rota la Shop API Key en el Dashboard. Los Widget Tokens firmados con la key antigua dejan de validar inmediatamente; tu servidor empieza a firmar con la nueva en el próximo render.
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.