# Análisis completo del repositorio `kynda-poc`

**Fecha:** 2026-08-05 · **Fuente:** copia local `KYNDA/Github/kynda-poc-main` · **Método:** 5 auditorías paralelas (frontend, dominio/auth, seguridad Supabase/RLS, IA/KPIs, tests/CI) + lectura directa de la documentación funcional y contractual.

---

## 1. Veredicto ejecutivo

El repo es **mucho mejor de lo esperable en un "POC"** en ingeniería de base (esquema, cadenas QR, tests) y **mucho peor de lo prometido** en la propuesta de valor (IA, white-label, contenido social). Y tiene **3 vulnerabilidades críticas de seguridad explotables hoy**.

| Dimensión | Estado |
|---|---|
| Modelo de datos + cadenas QR | ⭐⭐⭐⭐⭐ Excelente (invariantes en BD, RPC serializado, triggers) |
| Testing | ⭐⭐⭐⭐ 57 tests integración + 18 e2e reales contra DB real |
| Funcionalidad construida (flujos usuario/admin/ops) | ⭐⭐⭐⭐ Casi producto, no POC |
| Seguridad efectiva | ⭐⭐ 3 críticos + 3 altos explotables |
| IA (el diferencial del pitch) | ⭐ 0% construido |
| White-label visual | ⭐ Inexistente |
| CI/CD y hardening producción | ⭐⭐ Con huecos serios |

**El veredicto previo (refactorizar, no reconstruir) queda REFORZADO**: la base merece conservarse; el trabajo real es (a) cerrar seguridad, (b) construir la capa IA prometida, (c) white-label, (d) hardening operativo.

---

## 2. La brecha del pitch: IA = 0%

El RFP original (`functional docs/functional requierements.md`) vendía como pilares: verificación de evidencias por IA (Módulo B), generación de contenido con LLM + generative media (Módulo C), antifraude, "autenticidad certificada por IA", Impact Index y EMV.

**Realidad en el código:**

- **Cero dependencias de IA.** Ni OpenAI, ni Anthropic, ni visión/OCR, ni nada en `package.json` ni `pnpm-lock.yaml`.
- **Validación 100 % manual**: dos botones Aprobar/Rechazar (`apps/web/app/(ops)/actions.ts:23,91,135`). El diagrama del MVP dibuja un paso "🤖 Validación IA ~15 seg" (`functional docs/mvp-easyfairs-flujo.md:35`) que en el código no existe.
- **"Contenido social" = plantilla de string fija**, etiquetada `stub` en el propio código: `"Hoy hemos pasado tiempo en ${campaña} con el equipo de ${marca}"` + hashtags (`(ops)/actions.ts:49-69`). No genera imagen. La tabla `generated_content` **se escribe pero jamás se lee** — contenido muerto.
- **KPIs = aritmética sobre valores tecleados a mano** (`packages/lib/src/kpi/aggregate.ts:40-53`): SUM/AVG/COUNT/MAX. Fuentes: `AUTO` (solo cuenta filas), `USER_EVIDENCE` (lo teclea el usuario), `ADMIN_ACTION` (lo teclea el admin). No se extrae ningún dato de las fotos.
- **ESRS/ODS**: catálogo de referencia para multi-select manual. El PDF con mapeo ESRS lo envía **Kynda Ops a mano el día +10** (`admin/campaigns/[id]/export/page.tsx:53`).
- `FRAUD_BLOCKED` y `ValidationKind.AUTO` existen como enums pero **nada los asigna nunca**. No hay motor antifraude.

El propio proveedor lo reconoce implícitamente: `docs/respuestas-kynda-hito1.md` redujo el Hito 1 a evidencias solo-foto + validación manual. Lo entregado es coherente con esa reducción — pero el diferencial del deck no está.

---

## 3. Seguridad: hallazgos críticos (explotables hoy)

### C-1 · Salto de tenant en el signup (rompe la multitenencia)
Los triggers de provisión (`20260603120000_reconcile_prod_to_main.sql:746-948`, `20260530900000_campaign_invitations.sql`) leen `invited_brand_id` de `raw_user_meta_data` — **campo controlable por el cliente** en `supabase.auth.signUp()`. Un atacante con la anon key pública (visible en el navegador) se registra pasando el UUID de una marca víctima y obtiene `brand_membership` ACTIVE en ese tenant → ve todas sus campañas, acciones, KPIs y configuración. No se verifica que exista invitación real.

### C-2 · Auto-aprobación de evidencias por el propio usuario (fabricación de impacto)
Las policies `execution_self_update` y `evidence_self_update` (`20260526000100_rls.sql:215-236`) permiten al dueño de la fila actualizarla, y el guard de la máquina de estados valida la transición pero **no el rol del actor** (permite `IN_VALIDATION → APPROVED`, `schema.sql:404`). Un usuario normal puede hacer `UPDATE ... SET status='APPROVED'` sobre su propia ejecución/evidencia, acreditándose impacto ESG sin pasar por validación. En una plataforma cuyo producto es "impacto verificado", esto es greenwashing a voluntad.

### C-3 · Server actions de Ops sin control de rol (escalada de privilegios)
`approveEvidence`, `rejectEvidence` y `requestClarification` (`(ops)/actions.ts`) **no comprueban ningún rol** y operan con `service_role` (salta RLS). El gate del layout no cubre las server actions (son endpoints POST invocables directamente). Resultado: **cualquier usuario autenticado, de cualquier marca, puede aprobar/rechazar evidencias de cualquier otra marca** conociendo el `evidenceId`. Las brand-admin actions sí tienen `requireBrandAdmin()` sistemático — el hueco es específico de Ops. (El gap está incluso admitido en `tests/e2e/specs/ops-flow.spec.ts:4-5`.)

### Altos
- **A-1** · `GRANT ALL` sobre todas las tablas y funciones de `public` a `anon`/`authenticated` (`20260603130000_fix_public_schema_grants.sql:22-29`). Anula revokes previos (p. ej. el de `audit_log`) y hace que cada nueva función SECURITY DEFINER quede ejecutable por anónimos por defecto.
- **A-2** · `audit_log` acepta INSERT con `with check (true)` + grant a `anon` (`rls.sql:293`): el log de auditoría —vendido como garantía RGPD— es **falsificable por cualquiera sin autenticar** (inyección de entradas con actor/brand arbitrarios).
- **A-3** · `current_brand_id()` con fallback a `user_metadata` (`rls.sql:20-29`), escribible por el usuario; las policies que solo cruzan ese valor (`enrollment_self_write`, `execution_self_insert`) se satisfacen para un brand arbitrario.

### Medios (resumen)
`guard_campaign_visibility` SECURITY DEFINER sin `search_path` fijado; **`db_ddl.md` divergente de las migraciones** (41 tablas en el dump vs ~30 versionadas — `fraud_signal`, `data_request`, `esrs_mapping`… existen en algún entorno con policies NO versionadas); policy de Storage que no ata el prefijo de brand a la evidencia; bucket `campaign-covers` público; los brand-admin ven media de evidencias rechazadas/pendientes (contra la intención declarada de "solo aprobadas").

### Lo que está bien en seguridad
RLS activado en el 100 % de las tablas; `brand_id` denormalizado con triggers de consistencia e inmutabilidad; el RPC `activate_chain_token` blindado (SECURITY DEFINER + REVOKE/GRANT + `FOR UPDATE`); purga RGPD correcta; consentimientos con IP/UA/versión/revocación; aislamiento de credenciales multi-cliente en `.claude/` (hooks) muy maduro.

**Matiz estructural importante:** como casi todo el acceso de la app usa `service_role` (salta RLS), la RLS —que está razonablemente bien escrita— casi nunca protege el camino real. Toda la autorización recae en código de aplicación… y ahí es donde falta (C-3).

---

## 4. White-label: inexistente

Para un producto vendido como "SaaS white-label multi-tenant":

- Colores **estáticos** en `globals.css` (sin CSS custom properties por tenant).
- Logo de **Kynda hardcodeado** en las tres superficies (`components/kyn/chrome.tsx:81`, `admin-shell.tsx:83`, `(ops)/layout.tsx:25`).
- La tabla `brand` **no tiene columnas de branding** (color, logo); la marca del cliente solo aparece como texto.

El multi-tenant es real a nivel de **datos** (host → `x-kynda-tenant` → `brand_id`), pero el white-label **visual** no existe.

---

## 5. Qué está realmente construido (y bien)

- **Flujo de usuario completo**: explorar → inscribirse → evidencias (form dinámico por blueprint + fotos + GPS) → estado → cadenas → ajustes RGPD (consentimientos con revocación auditada).
- **Panel de marca completo**: editor de campañas de ~1.130 líneas (sesiones, objetivos, KPIs, ESRS/ODS, social, cadenas), catálogo clonable, cola de validación, informes con 3 pestañas, participantes (roles, bloqueo, allowlist, derecho al olvido), invitaciones, exports CSV, pulseras QR con impresión.
- **Cadenas + pulseras QR: la joya del repo.** Invariantes BR-1..BR-6 como constraints y triggers SQL (`20260612000000_chains.sql`), activación serializada con `FOR UPDATE` e idempotente, avance de cadena como trigger transaccional de la aprobación. Testado incluso con carrera concurrente.
- **Tests genuinos**: 57 de integración (RLS, aislamiento cross-brand con JWTs reales, máquinas de estado, RGPD, regresión de un bug real de producción) + 18 e2e Playwright (incluido el flujo de cadena completo con 2 usuarios). No son stubs.
- **RGPD en serio**: purga por retención vía cron con tombstones y auditoría, consentimientos granulares, revocación que corta acceso vía RLS.

Nota: `admin/dashboard`, `gallery`, `export`, `users`, `settings` que aparecen en el árbol de rutas son **redirects legacy** de una consolidación de navegación, no pantallas a medio hacer. La "galería de contenido" como tal no existe.

---

## 6. Deuda y riesgos de ingeniería

| Hueco | Detalle | Riesgo |
|---|---|---|
| Sin gate de calidad | `ci.yml` y `deploy.yml` corren en paralelo: CI rojo **no bloquea** migraciones a prod | Alto |
| Build ignora errores | `next.config.mjs:22,25`: `ignoreBuildErrors` + `ignoreDuringBuilds` → Vercel despliega con errores TS | Alto |
| Linting inexistente | Sin config ESLint en el repo; el job de CI llamado `typecheck-lint` no linta | Medio |
| Sin error boundaries | Ni un `error.tsx`/`not-found.tsx` en toda la app; Ops muestra el error crudo de Supabase al usuario | Medio |
| God-module | `apps/web/lib/db.ts` (1.941 líneas) concentra el 95 % del acceso a datos con service_role; la regla "solo `packages/lib` habla con la DB" no se cumple | Medio |
| Backups a medias | PITR sin activar (hoy 7 días vs ≥30 requeridos); **las fotos de evidencia (Storage) no se respaldan**; 0 ensayos de restauración | Alto |
| Máquina de estados de evidencia sin declarar | Campaña/acción tienen tabla de transiciones (`status.ts`); evidencia/ejecución no — transiciones ad-hoc en cada action | Medio |
| Duplicaciones | Validación duplicada (ops vs admin), 2 design systems, 2 StatCard, helpers de fecha copiados 3× | Bajo |
| Detalles | N+1 en `/mine`, doble UPDATE en `submitEvidence`, `as unknown as` extendido, notificaciones = icono decorativo, hero con foto Unsplash fija | Bajo |

---

## 7. Prioridades de remediación recomendadas

**P0 — Seguridad (antes de cualquier piloto real):**
1. C-1: derivar el brand del signup de una invitación válida en BD, nunca de `user_metadata`.
2. C-2: restringir transiciones a `APPROVED/REJECTED/FRAUD_BLOCKED` a staff (en policy o en el guard con `is_staff()`).
3. C-3: `requireOps()` + scoping por marca en las tres server actions de Ops.
4. A-1/A-2/A-3: revertir el `GRANT ALL`, cerrar el INSERT de `audit_log` a `service_role`, eliminar el fallback a `user_metadata` en `current_brand_id()`.

**P1 — Operativo:**
5. Gate de deploy (migraciones solo con CI verde) + reactivar errores TS en build.
6. PITR + backup de Storage + primer ensayo de restauración.
7. Reconciliar `db_ddl.md` con las migraciones (¿qué entorno tiene esas 11 tablas extra con policies sin versionar?).

**P2 — Producto (la propuesta de valor):**
8. Capa de IA de validación (el esquema ya la contempla: `ValidationKind.AUTO`, `suspicious_threshold`, `FRAUD_BLOCKED` — solo falta el motor).
9. White-label visual (columnas de branding en `brand` + CSS variables por tenant + logo dinámico).
10. Generación real de contenido social (hoy stub) y su galería.

---

*Informe generado a partir del análisis de las 36 migraciones SQL (~5.000 LOC), 117 ficheros TS/TSX, tests, CI, scripts y documentación funcional/contractual del repo.*
