# Regla: Seguridad — Next.js

Next.js con App Router tiene características de seguridad integradas, pero
requiere configuración explícita para ser seguro en producción. Esta regla
cubre las decisiones que el desarrollador debe tomar conscientemente.

---

## Validar variables de entorno al arrancar

Las variables de entorno no validadas causan errores crípticos en producción.
Validarlas al arrancar con un schema zod:

```ts
// lib/env.ts
import { z } from 'zod'

const EnvSchema = z.object({
  DATABASE_URL:        z.string().url(),
  NEXTAUTH_SECRET:     z.string().min(32),
  NEXTAUTH_URL:        z.string().url(),
  STRIPE_SECRET_KEY:   z.string().startsWith('sk_'),
  // Variables públicas (expuestas al cliente)
  NEXT_PUBLIC_APP_URL: z.string().url(),
})

export const env = EnvSchema.parse(process.env)
```

- Este módulo se importa en `next.config.js` para que falle al buildear, no en runtime.
- Si la validación falla: el build se detiene con mensaje claro del campo faltante.

---

## Server Actions: validar inputs server-side con zod

Las Server Actions son el nuevo endpoint de mutación. Reciben datos del cliente
y DEBEN validarlos como si fueran un endpoint de API público:

```ts
// app/facturas/actions.ts
'use server'

import { z } from 'zod'
import { auth } from '@/lib/auth'

const CrearFacturaSchema = z.object({
  clienteId: z.coerce.number().int().positive(),
  items: z.array(z.object({
    sku:    z.string().max(50),
    qty:    z.coerce.number().int().min(1).max(9999),
    precio: z.coerce.number().positive(),
  })).min(1),
})

export async function crearFactura(formData: FormData) {
  // Verificar autenticación
  const sesion = await auth()
  if (!sesion?.user) throw new Error('No autorizado')

  // Validar inputs
  const validado = CrearFacturaSchema.safeParse(Object.fromEntries(formData))
  if (!validado.success) {
    return { errores: validado.error.flatten().fieldErrors }
  }

  // Verificar que el cliente pertenece a la empresa del usuario (IDOR prevention)
  const cliente = await obtenerClienteDeLaEmpresa(
    validado.data.clienteId,
    sesion.user.empresaId,
  )
  if (!cliente) return { errores: { clienteId: ['Cliente no encontrado'] } }

  await facturaRepo.crear({ ...validado.data, empresaId: sesion.user.empresaId })
}
```

---

## CSRF: protección nativa en Server Actions

- Las Server Actions incluyen protección CSRF automática de Next.js mediante
  validación de origen. No requiere configuración adicional.
- Los Route Handlers (`route.ts`) NO tienen CSRF automático. Agregar verificación
  de origen si reciben mutaciones desde formularios con cookies de sesión:

```ts
// app/api/webhooks/stripe/route.ts
export async function POST(request: Request) {
  // Webhooks externos usan firma, no CSRF
  const firma = request.headers.get('stripe-signature')
  await verificarFirmaStripe(firma, await request.text())
  // ...
}
```

---

## Content Security Policy en next.config.js

```js
// next.config.js
const securityHeaders = [
  { key: 'X-DNS-Prefetch-Control',   value: 'on' },
  { key: 'X-Frame-Options',          value: 'SAMEORIGIN' },
  { key: 'X-Content-Type-Options',   value: 'nosniff' },
  { key: 'Referrer-Policy',          value: 'origin-when-cross-origin' },
  {
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      "script-src 'self' 'nonce-{NONCE}'",
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "connect-src 'self' https://api.stripe.com",
    ].join('; '),
  },
]

module.exports = {
  async headers() {
    return [{ source: '/(.*)', headers: securityHeaders }]
  },
}
```

---

## No exponer secretos al cliente

- Solo las variables con prefijo `NEXT_PUBLIC_` se exponen al bundle del cliente.
- NUNCA nombrar variables sensibles con `NEXT_PUBLIC_`:

```bash
# MAL — secreto expuesto al cliente
NEXT_PUBLIC_DATABASE_URL=postgresql://...
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_...

# BIEN — solo URLs públicas van con el prefijo
NEXT_PUBLIC_APP_URL=https://app.empresa.com
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...

# BIEN — secretos sin prefijo, solo disponibles en servidor
STRIPE_SECRET_KEY=sk_live_...
DATABASE_URL=postgresql://...
```

---

## Rate limiting en Route Handlers

```ts
// lib/rate-limit.ts
import { LRUCache } from 'lru-cache'

const rateLimit = new LRUCache<string, number>({
  max: 500,
  ttl: 60 * 1000, // 1 minuto
})

export function checkRateLimit(ip: string, limit = 30): boolean {
  const count = (rateLimit.get(ip) ?? 0) + 1
  rateLimit.set(ip, count)
  return count <= limit
}

// app/api/contacto/route.ts
export async function POST(request: Request) {
  const ip = request.headers.get('x-forwarded-for') ?? 'unknown'
  if (!checkRateLimit(ip, 5)) {
    return new Response('Too Many Requests', { status: 429 })
  }
  // ...
}
```

---

## Autenticación: NextAuth.js o Clerk

- NUNCA implementar autenticación custom con JWT manual y manejo de sesiones propio.
- **NextAuth.js (Auth.js v5)**: para proyectos con base de datos propia y necesidad
  de control total.
- **Clerk**: para proyectos que priorizan velocidad de implementación y no tienen
  requisitos de datos en territorio específico.
- Ambas opciones manejan correctamente: rotación de tokens, revocación de sesiones,
  CSRF, cookies seguras y OAuth.

---

## dangerouslySetInnerHTML: sanitizar siempre

```tsx
// MAL — XSS directo si contenido viene del usuario
<div dangerouslySetInnerHTML={{ __html: comentario.texto }} />

// BIEN — sanitizado antes de renderizar
import DOMPurify from 'isomorphic-dompurify'
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(comentario.texto) }} />
```

- Si `dangerouslySetInnerHTML` aparece en el codebase, es una señal de alerta
  en code review que requiere verificación explícita de la fuente del contenido.

---

## Checklist de seguridad antes de merge

- [ ] Variables de entorno validadas con zod en `lib/env.ts`
- [ ] Server Actions con validación zod y verificación de autenticación
- [ ] Server Actions con prevención de IDOR (datos filtrados por empresa/usuario)
- [ ] CSP configurada en `next.config.js`
- [ ] Sin variables sensibles con prefijo `NEXT_PUBLIC_`
- [ ] Rate limiting en Route Handlers públicos
- [ ] `dangerouslySetInnerHTML` con `DOMPurify.sanitize()` si existe
- [ ] `npm audit` sin vulnerabilidades HIGH o CRITICAL
