PR #47 — Integración Lemon Squeezy Payments
feat/lemon-squeezy-payments → main · agente-codegen-v2 · 340 líneas · 3 archivos nuevos · 2 modificados
✕  Cambios Requeridos
Puntuación por eje
Corrección
5.5/10
Legibilidad
6.0/10
Arquitectura
5.0/10
Seguridad
2.0/10
Rendimiento
6.5/10
3 Critical
4 Important
3 Nit / Optional
0 Aprobados
⊘ 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/lemon y crear registros de compra sin pagar.
  • 3. XSS en SkillCarddangerouslySetInnerHTML con skill.description sin 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 any en SkillCard diluyen el contrato de la interfaz
components/SkillCard.tsx · línea 1
skill: 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?

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.

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