⊘ CAMBIOS REQUERIDOS — No fusionar
Este PR tiene 3 issues críticos que bloquean la fusión. El más grave es una clave API hardcodeada en el repositorio que expone el secreto en el historial de git. La ausencia de verificación de firma en el webhook abre la puerta a eventos fraudulentos. El XSS en dangerouslySetInnerHTML puede ejecutar código arbitrario. Estos tres issues deben resolverse antes de continuar.
- 1. Secreto hardcodeado — API key de Lemon Squeezy incluida literalmente en
lib/lemon.ts. Rotar la clave inmediatamente y mover a env var. - 2. Webhook sin verificación de firma — Cualquier atacante puede enviar eventos falsos a
/api/webhooks/lemony crear registros de compra sin pagar. - 3. XSS en SkillCard —
dangerouslySetInnerHTMLconskill.descriptionsin sanitizar permite inyección de scripts si la descripción proviene de fuentes externas.
Eje 1 — Corrección
5.5 / 10
Important
Errores de DB silenciados con console.log
api/webhooks/lemon/route.ts · línea 21
Cuando la inserción en Supabase falla, el webhook devuelve
{ ok: true } igualmente. Lemon Squeezy no reintentará el evento, la compra se pierde silenciosamente y el cliente nunca recibe su skill.
// ❌ Error swallowed — el evento se marca como procesado aunque falló
if (error) console.log('DB error:', error)
return NextResponse.json({ ok: true })
// ✅ Lanzar error 500 para forzar reintento automático de Lemon
if (error) {
console.error('Purchase insert failed:', error.message)
return NextResponse.json({ error: 'DB failure' }, { status: 500 })
}
Acción requerida: Retornar HTTP 500 en caso de error de DB. Lemon Squeezy reintentará el evento automáticamente.
Important
No hay tests para ninguna de las tres funciones nuevas
Repositorio completo
El PR añade
POST /api/webhooks/lemon, createCheckout() y checkPurchaseAccess() sin ningún test. Código de pagos sin cobertura de regresión es directamente un riesgo de revenue.
Acción requerida: Añadir al menos: test del webhook con evento válido, test de webhook con firma inválida (debe devolver 401), test de
createCheckout con mock de axios, test de getAllPurchases.
Nit
created_at redundante — Supabase lo gestiona automáticamente
api/webhooks/lemon/route.ts · línea 18
Si la tabla tiene
created_at DEFAULT now(), pasar el valor desde el cliente es redundante y puede introducir desfase de zona horaria si el servidor del agente no está en UTC.
Eje 2 — Legibilidad y Simplicidad
6.0 / 10
Nit
Tipos
components/SkillCard.tsx · línea 1
any en SkillCard diluyen el contrato de la interfazskill: any, user: any elimina las ventajas de TypeScript. Si skill.variant_id cambia de nombre en el modelo, el compilador no lo detecta.
function SkillCard({ skill, user }: { skill: any, user: any })
interface Skill { id: string; name: string; description: string; price: number; variant_id: string }
interface User { email: string }
function SkillCard({ skill, user }: { skill: Skill; user: User })
Optional
Acceso profundo a body.data.attributes sin destructuring
api/webhooks/lemon/route.ts
Múltiples accesos encadenados hacen el código frágil si Lemon cambia su schema. Mejor destructurar y tipar el payload.
Eje 3 — Arquitectura
5.0 / 10
Important
Cliente Supabase instanciado en el módulo raíz del webhook
api/webhooks/lemon/route.ts · línea 3
El cliente se crea a nivel de módulo, lo que lo convierte en un singleton compartido entre invocaciones de edge. Si en el futuro se añaden tests, será imposible mockear sin refactorizar. El patrón establecido en el proyecto es usar un factory
createServerClient() dentro del handler.
Acción requerida: Mover la instanciación dentro de la función
POST() o usar el factory existente en lib/supabase/server.ts.
Important
getAllPurchases() usa supabase sin importarlo en lib/lemon.ts
lib/lemon.ts · línea 29
getAllPurchases() referencia supabase que no está importado en ese archivo. El código no compilaría. Además, esta función mezcla dos responsabilidades distintas (API de Lemon + acceso a BD) en un solo módulo. Separar en lib/purchases.ts.
Eje 4 — Seguridad
2.0 / 10
Critical
API key hardcodeada en el código fuente
lib/lemon.ts · líneas 3–4
La clave
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test_key_abc123 está literalmente en el código. Una vez fusionado, permanece en el historial de git para siempre aunque se elimine en el siguiente commit.
// ❌ NUNCA — hardcoded secret en repo
const API_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test_key_abc123'
// ✅ Cargar desde entorno
const API_KEY = process.env.LEMON_SQUEEZY_API_KEY
if (!API_KEY) throw new Error('LEMON_SQUEEZY_API_KEY not set')
Acción crítica: 1) Rotar la clave en el panel de Lemon Squeezy AHORA. 2) Añadir
LEMON_SQUEEZY_API_KEY a las env vars de Vercel. 3) Añadir *.env* y un patrón /eyJ/ al .gitignore + pre-commit hook con trufflehog.
Critical
Webhook sin verificación de firma — fraude garantizado
api/webhooks/lemon/route.ts · línea 8
El comentario
// TODO: verify signature later es una bomba de reloj. Cualquier atacante puede hacer un POST a /api/webhooks/lemon con un payload falso de order_created para registrar compras sin pagar.
// ❌ TODO sin fecha ni ticket → nunca se resuelve
const signature = req.headers.get('x-signature')
// TODO: verify signature later
// ✅ Verificar HMAC-SHA256 antes de procesar nada
const rawBody = await req.text()
const signature = req.headers.get('x-signature') ?? ''
const expected = createHmac('sha256', process.env.LEMON_WEBHOOK_SECRET!).update(rawBody).digest('hex')
if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
}
Acción requerida: Implementar verificación HMAC antes de fusionar. Usar
timingSafeEqual para evitar timing attacks. Agregar LEMON_WEBHOOK_SECRET a las env vars.
Critical
XSS: dangerouslySetInnerHTML con datos externos sin sanitizar
components/SkillCard.tsx · línea 14
skill.description proviene de Supabase y, si la descripción fue alguna vez ingresada por un usuario o cargada vía CSV, puede contener HTML arbitrario. Un administrador comprometido o un ataque de segunda orden podría ejecutar JavaScript en el navegador de todos los compradores.
// ❌ XSS directo
<p dangerouslySetInnerHTML={{ __html: skill.description }} />
// ✅ Opción A: texto plano (más seguro)
<p>{skill.description}</p>
// ✅ Opción B: sanitizar antes de renderizar HTML
import DOMPurify from 'dompurify'
<p dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(skill.description) }} />
Acción requerida: Si no se necesita HTML en la descripción, usar interpolación directa. Si sí se necesita, instalar y usar DOMPurify.
Eje 5 — Rendimiento
6.5 / 10
Optional
getAllPurchases() sin paginación ni filtros
lib/lemon.ts · línea 28
select('*') sin límite cargará toda la tabla. En el panel de admin, cuando CULTIVA tenga 10.000+ compras, esto saturará la conexión de Supabase y la memoria del edge function.
// ❌ Sin límite
.select('*')
// ✅ Con paginación
.select('id, email, product_name, amount, status, created_at')
.order('created_at', { ascending: false })
.range(offset, offset + 50)
Nit
axios cuando ya existe fetch nativo en Next.js 14
lib/lemon.ts · línea 1
Next.js 14 incluye fetch con caché integrado y soporte de edge runtime. Añadir axios sube el bundle ~30KB sin beneficio real para esta llamada.
Considera: Reemplazar axios por fetch nativo para mantener el bundle limpio y permitir el revalidate cache de Next.js.
Código Muerto Identificado
DEAD CODE IDENTIFICADO — ¿Eliminar?
•
•
→ Eliminar ambos una vez que se implemente la verificación de firma.
•
setLoading(false) en SkillCard.tsx línea 9 — es código inalcanzable: la redirección ocurre antes.•
const signature = req.headers.get('x-signature') en webhook línea 8 — variable declarada pero nunca usada (el TODO nunca se llega a ejecutar).→ Eliminar ambos una vez que se implemente la verificación de firma.
Revisión de Dependencias
Important
Nueva dependencia axios — no justificada
¿El stack existente lo resuelve? Sí, con fetch nativo.
Tamaño: ~30KB gzip en bundle.
Mantenimiento: Activo, pero no añade valor aquí.
Vulnerabilidades: No conocidas en la versión añadida.
Licencia: MIT ✓
Veredicto: eliminar y usar fetch nativo.
Tamaño: ~30KB gzip en bundle.
Mantenimiento: Activo, pero no añade valor aquí.
Vulnerabilidades: No conocidas en la versión añadida.
Licencia: MIT ✓
Veredicto: eliminar y usar fetch nativo.
Checklist de Revisión Completa
✕
Contexto entendido + spec verificada
✕
Tests cubren el cambio adecuadamente
✕
Casos edge gestionados
✓
Nombres descriptivos y consistentes
!
Lógica directa sin tricks
✕
Sigue patrones existentes del proyecto
✕
Sin secrets en código o logs
✕
Input validado en boundaries
✕
Sin vulnerabilidades de inyección
✕
Verificación de auth donde es necesario
✕
Sin patrones N+1
✕
Paginación en endpoints de lista
✕
Tests pasan (no hay tests)
✕
Build pasa (no verificado)
✕
Verificación manual documentada
⊘
VEREDICTO: REQUEST CHANGES — 3 Critical, 4 Important deben resolverse antes de fusionar
Revisión generada por CULTIVA IA · Skill: revision-codigo-calidad-multidimensional · 2026-06-18
5 ejes
Next.js 14
Lemon Squeezy
Supabase
código IA