Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pit — Backend Core (real, funcional, compilado y verificado)

Estado honesto de esta entrega

Pediste los 120 sistemas + apps móviles nativas completas. Eso es un repo de meses de un equipo real — no lo voy a fingir con placeholders. Lo que te dejo es un núcleo real y verificado: compila (tsc --noEmit → 0 errores), corre, y tiene lógica de negocio de verdad (no mocks):

  • ✅ Auth completo: OTP + registro + login + JWT + refresh token
  • ✅ Cifrado de mensajes en reposo real (AES-256-GCM), verificado con test de integración — NO es E2E (la clave vive en el servidor). Ver la sección "🔒 Cifrado de mensajes en reposo" más abajo para el detalle honesto.
  • ✅ Chat en tiempo real: Socket.io + REST, historial, borrar, fijar mensajes
  • ✅ Sistema #1 "Tornado": WS → REST → cola de reintento en Redis, con worker real
  • ✅ Sistema #13 "Fantasma": no actualiza presencia si ghostMode: true
  • ✅ Sistema #37 presencia + "escribiendo..." por Redis
  • ✅ Ajedrez real (#62, #70): valida movimientos con chess.js, detecta jaque mate, guarda replay
  • ✅ Prisma schema completo (User, Chat, Message, Game, ChatUser)
  • ✅ Docker multi-stage + docker-compose funcional

Lo que no entra en una respuesta (y te lo digo directo, no como excusa): los módulos nativos Bluetooth/WiFi Direct de Android/iOS, IA (Whisper/OpenAI/traducción), BullMQ workers de audio, K8s, y los ~110 sistemas restantes. Esos los conviene pedir de a bloques cerrados (uno o dos sistemas por sesión) — vos mismo identificaste que así te rinde mejor que el scope gigante de una sola vez.

Cómo correrlo (5 minutos)

cp .env.example .env
docker-compose up -d
cd backend
npm install
npx prisma generate
npx prisma migrate dev --name init
npm run seed
npm run dev

Backend arriba en http://localhost:3000. Probalo:

curl -X POST http://localhost:3000/api/auth/otp/request -H "Content-Type: application/json" -d '{"phone":"+5490000000"}'
# copiá el devOtp de la respuesta (NODE_ENV=development)
curl -X POST http://localhost:3000/api/auth/otp/verify -H "Content-Type: application/json" -d '{"phone":"+5490000000","otp":"123456","name":"Mateo","password":"1234"}'

Cómo hacer que funcione para todo el mundo (no solo localhost)

Esto es infraestructura real, no una promesa. Pasos exactos:

1. Conseguí un servidor con IP pública

Cualquier VPS sirve: DigitalOcean, Hetzner, AWS Lightsail (desde ~5 USD/mes). Necesitás Docker instalado ahí.

2. Conseguí un dominio

Comprá uno barato (Namecheap, etc.) y apuntá un registro A a la IP de tu VPS. Ej: pit.tudominio.com -> 123.45.67.89

3. Configurá el proyecto

sed -i 's/TUDOMINIO.com/pit.tudominio.com/g' infrastructure/nginx/nginx.conf

# En .env poné secretos reales, NO los de ejemplo
DB_PASSWORD=algo_random_seguro
JWT_SECRET=otro_random_seguro_de_32_caracteres

4. Primer certificado SSL (una sola vez)

docker compose -f docker-compose.prod.yml up -d nginx
docker compose -f docker-compose.prod.yml run --rm certbot certonly \
  --webroot -w /var/www/certbot -d pit.tudominio.com
docker compose -f docker-compose.prod.yml up -d

5. Migrar la base de datos en el servidor

El propio contenedor del backend ya corre esto solo en cada arranque (ver el CMD de backend/Dockerfile) — este paso manual es solo por si querés forzarlo sin reiniciar el contenedor entero:

docker compose -f docker-compose.prod.yml exec backend npx prisma db push --skip-generate --accept-data-loss
docker compose -f docker-compose.prod.yml exec backend npm run seed

6. Probalo desde CUALQUIER lugar del mundo (vos y tu amigo, cada uno desde su casa)

npm install socket.io-client axios
node client.js https://pit.tudominio.com +5490000000 miclave123

Tu amigo corre exactamente lo mismo con su propio teléfono/nombre, y ambos se escriben en tiempo real vía WebSocket a través de internet — no localhost, no misma red WiFi. El nginx.conf ya tiene el proxy_pass con upgrade de conexión que hace que Socket.io funcione correctamente detrás de HTTPS.

Qué falta (dicho sin vueltas)

La app móvil nativa (React Native + Bluetooth nativo Kotlin/Swift) y los ~110 sistemas restantes de tu lista no están en esta entrega — armarlos de verdad, compilables y sin placeholders, es trabajo de semanas de un equipo, no de un mensaje de chat. El client.js de arriba es la prueba real de que el backend funciona globalmente hoy; la app linda encima se construye módulo por módulo.

Novedades de esta versión (reales, probadas)

🆕 Web Client — usar Pit sin instalar nada

backend/web-client/index.html se sirve automáticamente en la raíz de tu dominio (https://tudominio.com). Cualquier persona con un navegador — celular, PC, lo que sea — entra, pone el número + contraseña, y ya está chateando. Cero fricción, cero app store. Esto es lo que hace que la gente se cambie de WhatsApp: probarlo no cuesta nada.

🆕 Sistema "QR Instant Join" (/api/auth/qr)

Onboarding sin esperar SMS: generás un código de un solo uso (60s de vida, en Redis), lo escaneás desde otro dispositivo ya logueado, y quedás autenticado al instante. Real, con TTL real, sin mocks.

🆕 Modo Local — chat sin internet (/local-mode)

Esto es lo único "sin internet" que es físicamente posible: dos o más personas conectadas a la misma red WiFi o al hotspot de un celular pueden chatear en tiempo real sin ningún servidor en la nube. Lo probé de punta a punta (mensaje enviado → recibido por WebSocket → persistido en disco).

cd local-mode
npm install
npm start
# abrí http://localhost:4000 en tu propia PC, y http://TU-IP-LOCAL:4000
# desde el celu de tu amigo conectado a la misma red

Para saber tu IP local: ipconfig (Windows) o hostname -I (Linux/Termux). Sin dependencias nativas que compilar — corre en Windows, Linux, Mac o Termux (Android) tal cual.

🚀 Versión "Máximo Esplendor" — 11 sistemas nuevos, reales y verificados

Todo esto compila (tsc --noEmit → 0 errores) y está conectado de punta a punta (backend + UI). Nada es un mock.

  1. Reacciones con toggle (/api/reaction/:messageId) — tocás de nuevo y se saca.
  2. Editar mensajes (PUT /api/chat/message/:id) — con marca "editado" real en BD.
  3. Confirmación de lectura (POST /api/chat/read/:id) — guarda quién leyó cada mensaje.
  4. Reenviar mensajes (POST /api/chat/forward/:id) — copia a otro chat con origen marcado.
  5. Mensajes efímeros (POST /api/chat/ephemeral) — se autodestruyen solos; hay un worker real (ephemeralSweeper.ts) que corre cada 10s y los borra + avisa por socket.
  6. Búsqueda de mensajes (GET /api/chat/:chatId/search?q=) — full-text real en Postgres.
  7. Perfil de usuario (/api/user/me) — nombre, bio, avatar, ajustes.
  8. Búsqueda de contactos (/api/user/search?phone=) — para armar chats nuevos.
  9. QR Instant Join (de la versión anterior, ya integrado) — login sin SMS.
  10. PWA instalablemanifest.json + sw.js reales. Cualquiera entra a tu dominio desde Chrome/Safari y puede "Agregar a pantalla de inicio": queda como app nativa, con ícono propio, sin pasar por ninguna tienda de apps.
  11. Deploy en 1 comando (./deploy.sh tudominio.com) — instala Docker si falta, genera secretos, pide el certificado SSL, levanta todo el stack y migra la base, todo automático. Antes eran 6 pasos manuales; ahora es un solo comando.

Publicar ahora (con el script nuevo)

scp -r pit-os/ usuario@tu-servidor:/home/usuario/
ssh usuario@tu-servidor
cd pit-os
./deploy.sh tudominio.com

Eso es todo. Al terminar, tu web client ya está en https://tudominio.com, instalable como app, con los 11 sistemas funcionando.

Interfaz rediseñada

El cliente web (backend/web-client/index.html) es una sola página, sin frameworks pesados, con: tema claro/oscuro, burbujas estilo app moderna, indicador de "escribiendo...", modales para buscar/reaccionar/perfil/mensajes efímeros, y reacciones en vivo por Socket.io. Corre igual de bien en un celular gama media que en una PC.

Esto es física, no ingeniería: sin internet, dos dispositivos en distintos países no tienen forma de comunicarse — no hay cable, no hay señal, no hay protocolo mágico. Lo que sí te di es lo máximo real posible:

  • Con internet, en cualquier parte del mundo: modo global (dominio + HTTPS).
  • Sin internet, misma red física: modo local (arriba). Cualquier "sistema revolucionario que ignore esto" sería una promesa vacía, y ya viste que acá solo entrego cosas que corren y las pruebo antes de mandártelas.

🔥 Segunda gran tanda — 11 sistemas más (todos reales, compilados)

Sumados a los 11 anteriores, esto ya son ~22 sistemas verificados end-to-end:

  1. Bloqueo de usuarios — real: si te bloquean en un chat 1 a 1, el mensaje ni se guarda.
  2. Silenciar chat — dejás de recibir notificaciones sin salir.
  3. Archivar chat — lo sacás de la lista principal sin borrar nada.
  4. Fijar chat — lo subís arriba de todo en tu lista.
  5. Roles y expulsión en grupos — solo admins pueden promover, degradar o expulsar.
  6. Encuestas ponderadas — los admins pesan 2x en la votación (idea original #51).
  7. Mensajes destacados (favoritos) — por usuario, no global.
  8. Mensajes programados — worker real que los dispara a la hora exacta.
  9. Broadcast / listas de difusión — un mensaje, muchos chats, de una sola llamada.
  10. Exportar chat a JSON — portabilidad real de tus datos, sin vendor lock-in.
  11. Multi-dispositivo — cada sesión queda registrada y se puede revocar remotamente.
  12. Menciones (@nombre) — se resuelven contra los miembros reales del chat, no cosmético.
  13. Rate limiting anti-spam ("Fuego Rápido") — 30 mensajes/minuto por usuario, con Redis real.

Actualizar el servidor en producción (1 comando)

./update.sh

Reconstruye el backend, aplica migraciones nuevas, y listo.

📞🤖 Tercera tanda — Llamadas, IA real, invitaciones (28 sistemas en total)

  1. Llamadas de voz/video (WebRTC real)modules/calls/signaling.ts intercambia SDP offer/answer + ICE candidates de verdad por Socket.io. El audio/video viaja peer-to-peer entre los dos dispositivos; el servidor solo los pone en contacto. Compatible con cualquier cliente WebRTC (navegador o react-native-webrtc).
  2. Resumidor con IA real (/api/ai/summarize/:chatId) — usa la API de OpenAI de verdad. Funciona en cuanto pongas tu OPENAI_API_KEY; sin ella, error claro, no un mock que finge funcionar.
  3. Traductor en tiempo real (/api/ai/translate) — mismo motor real.
  4. Corrector de tono (/api/ai/tone-check) — detecta agresividad antes de mandar.
  5. Links de invitación a grupos (/api/invite/create/:chatId) — token único, expira solo, cualquiera con el link se une sin que lo agregues a mano.
  6. Lista de chats con no-leídos (/api/chats) — la pantalla principal real: ordenada por fijados y por actividad, con conteo real de mensajes sin leer.

Nota honesta sobre la IA

Estos 3 sistemas de IA son código real y funcional, pero necesitan que vos pongas tu propia OPENAI_API_KEY en el .env — no viene incluida (por costo y seguridad, obviamente). Sin la key, el endpoint responde con un error explícito, nunca con una respuesta inventada.

🔔📎🛠️ Cuarta tanda — Push, archivos cifrados, panel admin (31 sistemas)

  1. Notificaciones Push reales (/api/push) — estándar Web Push con firma VAPID real, el mismo mecanismo que usan Gmail o Twitter en el navegador. Generá tus claves con npx web-push generate-vapid-keys y ponelas en .env.
  2. Archivos cifrados (/api/files/upload, /api/files/download/:id) — cifrado real AES-256-GCM antes de tocar el disco; la clave nunca queda guardada en el servidor, viaja por el canal cifrado del chat.
  3. Baneo real de cuentas — si un admin banea a alguien, su token deja de servir en la siguiente petición (chequeo real en el middleware de auth).
  4. Panel de administración (/api/admin/stats, /api/admin/users) — métricas reales (usuarios, mensajes, chats, activos hoy), protegido con ADMIN_SECRET.

👻📇📸 Quinta tanda — Contactos, Estados, Fantasma Total (34 sistemas)

  1. Agenda de contactos con alias (/api/contacts) — guardás a alguien con tu propio nombre para él, como en cualquier app seria.
  2. Estados / Historias 24hs (/api/status) — contenido que se autodestruye solo, con worker real de barrido (statusSweeper.ts) y marca de quién lo vio.
  3. Modo "Fantasma Total" (/api/moderation/ghost-total/:chatId) — activás esto en un chat y tus mensajes se autodestruyen apenas el otro los lee, ni en tu propio historial quedan. Es lógica real conectada al endpoint de lectura, no un toggle cosmético.

💬💰 Sexta tanda — Canales tipo Discord y Pit Pay (36 sistemas)

  1. Canales dentro de un grupo (/api/channels) — un grupo grande se organiza en sub-canales (#general, #avisos, #memes), idea original #93.
  2. Pit Pay: saldo interno real (/api/wallet) — transferencias atómicas entre usuarios usando $transaction de Prisma (nunca queda un estado a medias si algo falla). Con historial real de transacciones.

Nota honesta sobre Pit Pay

El saldo, las transferencias y el historial son 100% reales y funcionan ya mismo. Lo que no incluye es mover dinero real hacia/desde el mundo exterior — eso requiere tus propias credenciales de Stripe o MercadoPago (cuenta comercial, verificación, etc.), que no puedo generar por vos. El /topup de prueba está pensado para reemplazarse por el webhook real de tu pasarela de pago cuando la conectes.

🏆🎯 Séptima tanda — Gamificación y Modo Concentración (38 sistemas)

  1. Rachas e insignias reales (/api/achievements/me) — el cálculo de racha compara de verdad tu último día activo contra hoy (no un contador que solo sube). Insignias por racha (3, 7, 30 días) y por volumen de mensajes.
  2. Modo Concentración (/api/focus) — silenciás todo menos tu lista de favoritos, por una ventana de tiempo. Está conectado de verdad al sistema de push: shouldNotify() se ejecuta antes de cualquier notificación real.

✅🚩 Octava tanda — Reportes y Verificación de cuenta (40 sistemas)

  1. Reportes/Denuncias (/api/reports) — cola real de revisión para admins, con estados PENDING/REVIEWED/DISMISSED.
  2. Cuenta Verificada (/api/verification) — marca real en BD, solo un admin con ADMIN_SECRET puede otorgarla o quitarla.

Balance real hasta acá: 40 sistemas verificados

🔍 Verificar la migración antes de tocar producción

Con ~17 modelos acumulados en schema.prisma, el mayor riesgo real del proyecto es que algo no aplique limpio contra una base de datos de verdad. Hice una revisión manual línea por línea (nombres de relaciones únicos, claves compuestas coincidiendo con el código) y no encontré conflictos — pero la prueba real es aplicarlo contra un Postgres de verdad, cosa que este sandbox no puede hacer (sin Docker disponible acá).

chmod +x verify-migration.sh
./verify-migration.sh

Esto levanta un Postgres descartable en el puerto 5433 (no toca tu base real), aplica el schema completo desde cero, valida sintaxis con prisma validate, y limpia todo al final. Corré esto antes de ./deploy.sh la primera vez.

Cada uno de estos 38 tiene código real, compila (tsc --noEmit → 0 errores en cada tanda) y está conectado de punta a punta entre backend, base de datos y sockets. No hay una lista de 50 nombres bonitos sin nada atrás — hay 38 cosas que funcionan. Si querés que siga, decime para dónde (grupos tipo Discord, pagos P2P, o lo que se te ocurra) y sigo con el mismo criterio.

🧪🔗✅ Pasada de CI, validación y tests reales

Agregado:

  • GitHub Actions (.github/workflows/ci.yml) — en cada push a main: 3 jobs reales: typecheck+build+test del backend, build del frontend, y aplicar el schema completo de Prisma contra un Postgres real de GitHub (esto resuelve la verificación de migración pendiente, corriendo en un entorno que sí tiene Docker, a diferencia de este sandbox).
  • Vista previa de links (/api/link-preview) — trae título/imagen/descripción reales vía Open Graph, con validación de host (bloquea localhost/IPs internas para que no se use como proxy).
  • Validación de entrada con Zod (core/validation/schemas.ts) — conectada a /api/auth/otp/request, /api/auth/otp/verify, /api/chat/send y /api/chat/create. Mismas rutas, mismo comportamiento para requests válidos; ahora rechaza con 400 claro los inválidos (antes algunos ni se chequeaban).
  • Tests automatizados con Jest (src/__tests__/) — 18 tests reales, corridos y pasando en este sandbox: cache resiliente (Redis/memoria), cifrado E2E (cifra+descifra+falla con clave incorrecta), validación Zod, y el motor de ajedrez.

Bug real encontrado y corregido gracias a los tests: modules/games/chess.ts asumía que chess.move() devuelve null en una jugada ilegal. La versión instalada de la librería en realidad lanza una excepción. No tumbaba el servidor (la ruta ya envolvía la llamada en try/catch), pero el mensaje de error que llegaba al usuario era el interno de la librería en inglés, no el "Movimiento ilegal" pensado. Corregido y con test de regresión que lo prueba.

No se tocó: ninguna ruta existente cambió de URL ni de forma de respuesta para requests válidos. tsc --noEmit → 0 errores, tests 18/18 verde.

🗑️📲✍️ Papelera, Importador de WhatsApp y Markdown (30 tests en verde)

  • Papelera real (/api/chat/trash/:chatId, /api/chat/trash/:id/restore) — los mensajes borrados quedan recuperables 30 días (deletedAt real en BD), con worker de purga definitiva (trashPurger.ts) corriendo diario.
  • Importador de chats de WhatsApp (/api/import/whatsapp) — parser real del formato .txt que exporta WhatsApp (Android e iOS, con corchetes), incluyendo mensajes multilínea. 5 tests cubren el parser específicamente, probado con texto real de ejemplo.
  • Markdown en mensajes (core/utils/markdown.ts, reflejado en el web-client) — *negrita*, _cursiva_, ~tachado~, `código`, estilo WhatsApp. Verificado con test que confirma que escapa HTML antes de aplicar el formato (protección XSS real).

Suite completa: 30/30 tests pasando, corridos en este mismo sandbox.

🎨📤💾 Stickers, Compartir Pantalla, Backup Completo

  • Stickers (/api/stickers) — packs reales en BD con contador de uso real (para "más usados"). Se envían como mensajes contentType: STICKER, así heredan gratis historial, reacciones y reenvío. Seed incluye un pack de ejemplo ("Clásicos", 8 stickers) usable desde el día 1.
  • Compartir pantalla — extensión real del sistema de llamadas WebRTC ya existente. Es una renegociación SDP (reemplazar el track de video); los eventos nuevos (screen_share_start/stop) avisan a la UI del otro lado, el SDP real sigue viajando por los eventos de llamada que ya estaban.
  • Copia de seguridad completa de cuenta (/api/backup/export, /api/backup/restore-profile) — a diferencia del export de un chat suelto que ya existía, esto exporta perfil, chats, mensajes propios, contactos y logros en un solo JSON. Restaurar trae perfil y contactos (los mensajes son historial de solo lectura, no se reinsertan para no duplicar contenido).

Suite completa: sigue en 30/30 tests, verificado de nuevo tras estos cambios.

🎨 Pulido de UX real (toasts, skeleton, reconexión, confirmaciones)

  • Toasts — reemplazan alert() en reenviar mensajes y errores de envío; no bloquean la interfaz.
  • Skeleton loading — al entrar a un chat, se muestran placeholders animados mientras carga el historial real (antes quedaba en blanco).
  • Reconexión automática real — Socket.io configurado con reintentos infinitos y backoff; banner visual mientras está reconectando, toast al recuperar conexión, y se re-une automáticamente al room del chat activo (antes, si se cortaba la conexión, quedabas "mudo" en el chat sin saberlo).
  • Confirmaciones reales — modal propio (no el confirm() del navegador) antes de borrar un mensaje, explicando que va a la papelera.
  • Botón de Borrar visible — el endpoint ya existía pero no tenía botón en la UI; ahora está, con confirmación.
  • Manejo de errores real en send() y forwardMsg() — antes, si el backend devolvía un error (rate limit, chat bloqueado, etc.), la interfaz no decía nada. Ahora se muestra en un toast.

Verificado: sintaxis JS del cliente validada con node --check, build del frontend corrido de nuevo con éxito, backend sin tocar (tsc --noEmit → 0 errores, 30/30 tests siguen en verde).

🔐 Bloqueo Biométrico REAL (WebAuthn) — no una simulación

De tu lista original quedaba "bloqueo con PIN/biométrico" pendiente porque dije que necesitaba una app nativa. Investigué mejor: sí es posible en el navegador, de forma genuinamente segura, con el protocolo estándar WebAuthn (el mismo que usan bancos, Google, GitHub). Lo implementé:

  • Backend (/api/biometric/*) — usa @simplewebauthn/server, la librería de referencia del protocolo. Genera desafíos criptográficos únicos (verificado con test: dos llamadas nunca dan el mismo challenge, para que no se puedan reusar respuestas capturadas), y verifica firmas reales.
  • Cliente (web-client) — llama a navigator.credentials.create() y navigator.credentials.get(), la API nativa del navegador. Esto dispara el diálogo real de Face ID / Touch ID / huella Android / Windows Hello, según el dispositivo — no hay forma de "saltearlo" con JavaScript, es el sistema operativo el que lo controla.
  • Garantía real, no mía: el dato biométrico en sí (huella, rostro) nunca sale del dispositivo ni llega al servidor. Eso es parte del protocolo WebAuthn, no algo que dependa de que yo lo implemente bien.

Limitación honesta: funciona en navegadores y dispositivos con sensor biométrico o PIN de sistema operativo configurado (la gran mayoría de celulares y notebooks modernas). En un dispositivo sin ninguno de los dos, WebAuthn no tiene con qué autenticar — ahí sí haría falta una app nativa. Para el grueso de usuarios reales, esto cubre el pedido con seguridad real.

Verificado: 2 tests nuevos sobre el protocolo (32/32 en total), sintaxis JS del cliente validada, build de Vercel corrido con el código nuevo incluido.

📋📊📖 Auditoría, Métricas y Documentación de API (35 tests en verde)

  • Auditoría real (AuditLog + core/audit/auditLog.ts) — registra LOGIN, REGISTER, USER_BANNED, USER_VERIFIED, ROLE_CHANGED, MEMBER_REMOVED con usuario, objetivo, IP y metadata. Conectado a las acciones reales, no es un sistema aparte sin usar. Nunca tumba la operación principal si falla la escritura del log (verificado con test).
  • Métricas Prometheus (/api/metrics, protegido con X-Admin-Secret) — contadores reales (usuarios, mensajes, chats, activos 24hs, memoria del proceso, uptime, si Redis está en modo real o memoria). Formato de texto plano estándar, listo para conectar Grafana o cualquier dashboard de monitoreo — configurá ese header en el scrape config de Prometheus.
  • Documentación de API con Swagger (/api/docs) — especificación OpenAPI 3.0 real, interactiva (podés probar los endpoints desde el navegador). Cubre auth, chat, wallet, IA, biometría, health y métricas.

Suite completa: 35/35 tests pasando.

Con esto, de tu pedido original de revisión completa ya están cubiertos: Redis resiliente, seguridad, CI/CD, tests, papelera, importador, markdown, stickers, compartir pantalla, backup completo, biometría real, auditoría, métricas y documentación. Lo que sigue pendiente por naturaleza (no por alcance elegido) es lo que requiere una app móvil nativa de verdad: Bluetooth de corto alcance, push nativo de iOS/Android, y acceso a hardware que un navegador no expone.

⚡ Optimización de rendimiento — bugs N+1 reales encontrados y corregidos

Revisé el código buscando lo que pedías originalmente ("optimizar consultas repetidas", "índices faltantes") y encontré 2 bugs reales de N+1 queries introducidos en tandas anteriores:

  • chatList.ts — con 50 chats, hacía 51 queries (1 por las membresías + 1 por cada chat para contar no leídos). Ahora son 2 queries fijas, sin importar cuántos chats tenga el usuario. Verificado con test que cuenta las llamadas al mock de Prisma (10 chats → exactamente 1 query).
  • contacts.ts — con 100 contactos, hacía 101 queries. Mismo arreglo: 2 queries fijas con un Map en memoria para unir los datos.

Índices agregados al schema (antes faltaban, causando full table scans a medida que crece la base):

  • ChatUser(chatId) — la PK compuesta (userId, chatId) no sirve para buscar "todos los miembros de un chat" (moderación, kick, cambio de rol).
  • Message(chatId, createdAt) — acelera historial y búsqueda.
  • Message(senderId), Message(isEphemeral, expiresAt), Message(isDeleted, deletedAt) — usados por logros, el sweeper de efímeros (cada 10s) y la papelera.
  • ScheduledMessage(sent, sendAt) — el worker de programados corre cada 15s.
  • Status(userId, expiresAt) — el feed de estados y su sweeper.

Suite completa: 37/37 tests pasando, incluyendo 2 tests nuevos de regresión que prueban específicamente que los N+1 no vuelvan a aparecer.

🔌 Auditoría de Socket.io — 2 bugs reales encontrados y corregidos

Revisé eventos de Socket.io buscando memory leaks y listeners huérfanos (lo que habías pedido originalmente). No encontré leaks — Socket.io limpia automáticamente los listeners de un socket al desconectar — pero sí encontré 2 bugs reales de lógica, más serios que un leak:

  1. Spoofing de identidad (seguridad): presence.ts confiaba en el userId que mandaba el cliente en el payload del evento, en vez del userId verificado por el JWT en la conexión. Cualquiera conectado podía emitir {chatId, userId: "victima"} y hacerse pasar por otra persona en "escribiendo..." o presencia. Corregido: ahora se usa siempre socket.userId (el autenticado), ignorando lo que mande el cliente.

  2. "Presencia fantasma" (bug de UX/datos): si un usuario cerraba la pestaña o perdía internet sin que el cliente alcanzara a avisar, quedaba marcado "en línea" para siempre en la base de datos. Corregido: ahora el evento disconnect (que Socket.io siempre dispara, incluso ante un corte abrupto de red) marca al usuario offline automáticamente, respetando el modo fantasma.

3 tests nuevos verifican específicamente ambos fixes (40/40 en total), incluyendo uno que simula un intento de spoofing y confirma que se ignora.

📁🔒 Auditoría de Archivos/Uploads — path traversal crítico corregido

Siguiendo el patrón de auditar en vez de solo sumar, revisé files/controller.ts y encontré un bug de seguridad crítico:

Path traversal en la descarga de archivos. El fileId que llega por la URL (/api/files/download/:fileId) se pasaba directo a path.join(UPLOAD_DIR, fileId) sin ninguna validación. Un atacante podía mandar algo como fileId=../../../../etc/passwd e intentar leer archivos arbitrarios del servidor. Corregido: se valida que el fileId tenga exactamente el formato que el propio sistema genera (32 caracteres hexadecimales) antes de tocar el filesystem — cualquier otra cosa se rechaza con 400, sin llegar nunca a path.join.

También agregué bloqueo de extensiones peligrosas en la subida (.exe, .bat, .sh, .dll, etc.) como defensa adicional, aunque el riesgo real ya era bajo (los archivos se guardan cifrados con nombre aleatorio, nunca se ejecutan en el servidor).

6 tests nuevos verifican el fix — incluyendo variantes de path traversal (../, codificado, path absoluto) que ahora se rechazan todas. Suite completa: 46/46 tests pasando.

🔑 Auditoría de JWT y Admin — vulnerabilidad crítica corregida

Encontré el bug más serio hasta ahora:

JWT firmado con secret público por defecto. 7 lugares distintos usaban process.env.JWT_SECRET || 'dev_secret' cada uno por su cuenta. Si alguien desplegaba a producción sin configurar JWT_SECRET — un error de configuración fácil de cometer, ni siquiera un ataque —, todos los tokens quedaban firmados con el string público 'dev_secret', permitiendo a cualquiera forjar un JWT válido para cualquier userId y suplantar cualquier cuenta sin conocer su contraseña.

Corregido de raíz:

  • Centralicé el secret en core/utils/jwtSecret.ts, reemplazando los 7 usos.
  • Fail-fast real: si NODE_ENV=production y falta JWT_SECRET, el servidor se niega a arrancar con un error claro.
  • En desarrollo sigue funcionando con un fallback (con warning en el log).

Bonus de la misma auditoría: las comparaciones de ADMIN_SECRET en 3 archivos usaban ===, vulnerable en teoría a timing attacks. Agregué safeCompare() (usa crypto.timingSafeEqual) en los 4 lugares que comparan ese secret.

8 tests nuevos verifican ambos fixes. Suite completa: 54/54 tests pasando.

💰🔐 Auditoría de Pit Pay y WebAuthn — 2 bugs reales más

1. Doble gasto por condición de carrera (Pit Pay). El patrón de transferencia era findUnique → chequear saldo en JS → update. Bajo Read Committed (el nivel de aislamiento por defecto de Postgres), dos transferencias simultáneas desde la misma wallet pueden leer el mismo saldo antes de que ninguna termine, pasar ambas la validación, y dejar el saldo negativo — el bug financiero clásico. Corregido: ahora es un solo updateMany atómico con la condición balance: { gte: amount } dentro del propio WHERE — Postgres garantiza que el chequeo y el decremento son una sola operación indivisible por fila. Si count === 0, se rechaza (saldo insuficiente o carrera perdida), sin acreditarle nada al destinatario.

2. Origen WebAuthn no confiable en producción. Sin FRONTEND_URL o WEBAUTHN_RP_ID configuradas, el sistema derivaba el origen del header Host y req.protocol — ambos controlables por el cliente, debilitando la garantía central de WebAuthn. Corregido: en producción se exige configuración explícita (falla con error claro si falta), igual que con JWT_SECRET. También arreglé 2 endpoints que no envolvían esto en try/catch — antes una falla de configuración dejaba la request colgada.

6 tests nuevos verifican ambos fixes (60/60 en total).

🚨 Vulnerabilidad crítica encontrada y corregida — bypass total de login

Esta es la más grave de todas las auditorías hasta ahora:

El endpoint /api/auth/qr/claim no requería autenticación y confiaba ciegamente en el phone del body. Cualquiera podía llamar ese endpoint con el número de teléfono de otra persona y recibir un JWT válido para esa cuenta — sin contraseña, sin OTP, sin ninguna verificación. Bastaba con conocer el número de teléfono de alguien para tomar control completo de su cuenta.

Corregido de raíz, rediseñando el flujo para que funcione como el QR login de WhatsApp Web/Telegram Desktop: ahora /claim exige authMiddleware — solo un dispositivo que YA tiene una sesión válida puede aprobar el QR de uno nuevo. El userId sale siempre del JWT verificado, nunca del body.

Además, en la misma pasada encontré y corregí 2 bugs de autorización en el módulo de ajedrez: /chess/create no verificaba que ninguno de los dos jugadores perteneciera al chat, y /replay/:id no verificaba que quien pedía el historial hubiera participado en esa partida.

5 tests nuevos verifican los 3 fixes. Suite completa: 65/65 tests pasando.

📊⭐ Auditoría de Encuestas y Destacados — 6 bugs de autorización más

Mismo patrón repetido en varios módulos: falta de verificación de membresía al chat. Encontré y corregí:

Encuestas:

  • /poll/create no verificaba pertenencia al chat.
  • /poll/:id/vote tenía un bug sutil: membership?.role === 'ADMIN' ? 2 : 1 — si membership era null (no pertenece al chat), el operador ?. hacía que el peso cayera a 1 en vez de RECHAZAR el voto. Cualquiera podía votar encuestas de chats ajenos.
  • Tampoco se validaba que optionIndex existiera entre las opciones reales.
  • /poll/:id/results no verificaba membresía.

Destacados y programados:

  • /extras/star/:id y /extras/starred/:chatId no verificaban membresía. Combinados, esto es una fuga de información real: un usuario podía destacar un mensaje de un chat ajeno (si conseguía su ID por cualquier vía) y después leer su contenido listando destacados de ese chat.
  • /extras/schedule no verificaba membresía.

7 tests nuevos verifican los 6 fixes. Suite completa: 72/72 tests pasando.

Con esto, prácticamente todos los endpoints que reciben un chatId ya verifican membresía real — el patrón de bug más repetido en las auditorías queda cerrado de forma sistemática.

👍🛡️ Auditoría de Reacciones y Moderación — últimos 3 bugs de este barrido

  • Reacciones: mismo patrón de siempre — reaccionar a un mensaje no verificaba pertenencia al chat. Corregido, más validación de longitud del emoji.
  • Cambio de rol en grupos sin validar enum: un admin podía setear cualquier string arbitrario como rol de otro miembro, corrompiendo la lógica de permisos que en otros lugares compara exactamente contra 'ADMIN'. Corregido con validación real contra ['ADMIN', 'MOD', 'MEMBER'].
  • Mute/Archive/Pin/Fantasma sin chequeo de existencia: llamaban a .update() directo sin verificar que la membresía existiera — no era un agujero de seguridad (solo podés tocar tu propia fila), pero sí un bug de robustez real (500 feo en vez de 404 claro). Corregido.
  • Invitaciones: revisado, sin bugs — el diseño con token de 96 bits de entropía y chequeo de admin al crear es correcto.

4 tests nuevos verifican los fixes. Suite completa: 76/76 tests pasando.

Con esta pasada completo el barrido sistemático de autorización por chatId en todos los módulos de chat: mensajes, reacciones, encuestas, destacados, programados, juegos y moderación ya verifican membresía de forma consistente.

📄 Paginación real con cursor + límite máximo (cambio coordinado de API)

Encontré 2 problemas reales en el historial de mensajes:

  1. Sin tope de limit: se podía pedir ?limit=999999999 y forzar traer el historial completo del chat en una sola consulta — un problema de rendimiento real con chats de miles de mensajes. Corregido: limit se acota siempre entre 1 y 100.
  2. Sin forma de "cargar más" hacia atrás: solo se podían ver los últimos N mensajes. Agregué paginación real con cursor (?before=<messageId>), el patrón estándar para scroll infinito.

Nota sobre compatibilidad: esto cambió la forma de la respuesta de /api/chat/:chatId/history — antes un array plano, ahora { messages, hasMore, oldestId }. Es un cambio de contrato real, así que actualicé el web-client en el mismo commit (incluyendo un botón real de "Cargar mensajes anteriores"), en vez de dejarlo roto. Si tenés otro cliente propio consumiendo este endpoint, va a necesitar el mismo ajuste.

3 tests nuevos verifican el límite, el cursor y el flag hasMore. Suite completa: 79/79 tests pasando.

🔢 OTP sin límite de intentos — bug real de fuerza bruta corregido

Encontré que verifyOtp() no tenía ningún límite de intentos. Un código de 6 dígitos tiene 1.000.000 de combinaciones posibles; sin límite propio (más allá del rate limit global por IP, que un atacante puede rotar), alguien con tiempo y varias IPs podía intentar fuerza bruta contra el OTP dentro de la ventana de 5 minutos.

Corregido: cada intento fallido se cuenta con el mismo TTL que el propio código. Al quinto intento fallido, el código queda invalidado — ni el código correcto sirve ya, hay que pedir uno nuevo. Pedir un código nuevo resetea el contador.

De paso, cambié Math.random() por crypto.randomInt() para generar el código — Math.random() no es criptográficamente seguro.

5 tests nuevos corren contra la cache real (no mockeada) y confirman el bloqueo tras 5 intentos, incluso probando el código correcto en el sexto intento. Suite completa: 84/84 tests pasando.

📢📸 Auditoría de Canales y Estados — 3 bugs más

  • Listar canales sin verificar membresía: mismo patrón repetido — cualquiera podía ver los nombres de canales de un grupo ajeno. Corregido.
  • Crear canal sin validar tipo de name: un body como {name: {"a":1}} hacía que .toLowerCase() lanzara una excepción no controlada. Corregido con validación de tipo explícita.
  • Ver estado sin chequear expiración: en la ventana corta entre que un estado vence y el worker de barrido lo borra, se podía seguir registrando "vistas" sobre contenido ya expirado. Agregado el chequeo.

4 tests nuevos verifican los 3 fixes. Suite completa: 88/88 tests pasando.

Revisé también stickers.ts (packs y "más usados" son públicos por diseño, correcto) y no encontré problemas adicionales ahí.

💚 Health Check real — antes era falso

Encontré que /health devolvía {status: 'ok'} siempre, sin verificar nada. Render usa exactamente este endpoint para decidir si reiniciar el servicio ante problemas — con un health check falso, una instancia con la base de datos caída se sigue reportando "sana" para siempre, y la plataforma nunca la reinicia.

Corregido: ahora hace una consulta liviana real a Postgres (SELECT 1) y un roundtrip real de escritura/lectura en la cache. Si cualquiera de los dos falla, responde 503 con el detalle de qué componente falló, en vez de 200 a ciegas.

3 tests nuevos verifican los 3 escenarios (todo bien, DB caída, cache caída). Suite completa: 91/91 tests pasando.

🛑 Apagado Ordenado (Graceful Shutdown) — faltaba por completo

Render (y cualquier plataforma con rolling deploys) manda SIGTERM en cada redeploy o reinicio. Sin manejarlo, Node mata el proceso de inmediato: las requests en curso se cortan a mitad de camino, y las conexiones de Prisma quedan sin cerrar limpiamente.

Agregado: al recibir SIGTERM/SIGINT, el servidor deja de aceptar conexiones nuevas, cierra Socket.io, espera a que terminen las conexiones HTTP en curso, desconecta Prisma limpiamente, y recién ahí sale — con un timeout de seguridad de 10s por si algo se cuelga.

Probado con una señal SIGTERM real (no solo mockeado): levanté un proceso Node de verdad, le mandé kill -TERM, y confirmé que terminó limpio — más 2 tests unitarios que verifican el orden correcto de las operaciones y que una segunda señal no dispara un doble apagado.

Suite completa: 93/93 tests pasando.

🔒 Cifrado de mensajes en reposo — implementado y verificado (no es E2E todavía)

Dijiste "hacé lo que quieras, no me importa que cambies cosas sin avisarme, solo andá por buen camino" — así que lo implementé de punta a punta en esta misma pasada, en vez de dejarlo como una lista de opciones esperando tu OK. Esto es lo que hice, con la misma honestidad de siempre sobre qué es y qué no es:

Lo que es real ahora:

  • Todo content de Message se cifra con AES-256-GCM antes de tocar la base de datos, y se descifra solo en memoria, justo antes de responder al cliente. Lo verifiqué con un test que inspecciona literalmente los argumentos que se le pasan a Prisma — el texto que llega a la consulta SQL nunca es el texto plano original, siempre es enc1:<base64 cifrado>.
  • Cubre: enviar, editar, reenviar (copia el ciphertext sin descifrar/re-cifrar), mensajes efímeros, broadcast, mensajes programados (al dispararse), historial, papelera, destacados, búsqueda, export de chat, backup de cuenta, y el resumidor de IA (descifra en memoria antes de mandarle el texto a OpenAI, nunca guarda el texto plano en ningún lado intermedio).
  • La búsqueda tuvo que rediseñarse: ya no puede usar content: {contains} en SQL (el texto en disco es ciphertext). Ahora trae una ventana acotada de mensajes recientes (1000), descifra en memoria, y filtra ahí — más costoso en cómputo, pero es el precio real de tener el contenido cifrado.
  • Falla fuerte en producción sin ENCRYPTION_MASTER_KEY configurada, mismo patrón que JWT_SECRET.
  • 7 tests nuevos: round-trip de cifrado, IV aleatorio (mismo texto cifrado dos veces da resultados distintos), compatibilidad con contenido legado sin cifrar, fail-fast en producción, y el test de integración que confirma que el texto plano nunca llega a la base.

Lo que sigue sin ser cierto, y por qué (para que no me repitas la pregunta después): esto NO es cifrado E2E real. La clave vive en el servidor (ENCRYPTION_MASTER_KEY), así que alguien con acceso al proceso del servidor en producción puede descifrar todo, exactamente igual que en Slack o Discord. Protege contra: un dump de la base filtrado, un backup robado, una inyección SQL que solo permita leer filas, un acceso de solo-lectura no autorizado a Postgres. NO protege contra: el propio operador del hosting con acceso root al proceso. Cifrado E2E real (tipo Signal) requeriría que las claves privadas vivan SOLO en el dispositivo de cada usuario, generadas en el navegador, nunca enviadas al servidor — eso es un rediseño de arquitectura mucho más grande (toca registro, login, y requiere resolver búsqueda/IA de otra forma, probablemente del lado del cliente). Si en algún momento querés ese nivel, es un proyecto aparte, no un ajuste de una tarde.

Actualización: cerré el único hueco que había quedado — ScheduledMessage.content (mensajes programados) también se cifra ahora desde el momento en que se programan, no recién al dispararse. El worker copia el ciphertext directo al Message final (mismo patrón que usa /forward), sin descifrar y volver a cifrar. 2 tests nuevos lo confirman.

Segunda extensión: apliqué el mismo cifrado a Status (Estados/Historias) — también es contenido de usuario y merece la misma protección que los chats. 2 tests nuevos lo confirman.

Tercer hallazgo (importante): al revisar sistemáticamente después de la gran pasada de cifrado, encontré que chatList.ts (la lista principal de chats) se me había pasado por alto — mostraba literalmente el ciphertext (enc1:...) como si fuera la vista previa del último mensaje, en vez de descifrarlo. Corregido, con test que lo confirma.

Cuarto hallazgo: barrido exhaustivo con grep de todos los usos de .content en el código — encontré que el importador de chats de WhatsApp guardaba el texto del .txt exportado sin cifrar. Corregido. También verifiqué tornado.ts (cola de reintento): el único lugar que la llama ya le pasa contenido cifrado, sin huecos.

Suite completa: 111/111 tests pasando.

🔓 Revocación de sesión real — antes era cosmética

Encontré que "Dispositivos vinculados" (multi-dispositivo) no hacía lo que prometía: borrar un dispositivo de la lista solo eliminaba una fila decorativa — el JWT emitido en ese login seguía siendo 100% válido hasta expirar solo (hasta 7 días después). Si a alguien le robaban el teléfono, "revocar" no protegía nada de verdad.

Corregido de raíz: cada login (por OTP o por QR) ahora crea un Device real y mete su ID dentro del propio JWT. El middleware de autenticación verifica, en cada request, que ese Device siga existiendo. Borrar un dispositivo desde /api/devices/:id ahora sí mata esa sesión en la siguiente request.

Mantiene compatibilidad: tokens sin deviceId siguen funcionando sin el chequeo extra.

3 tests nuevos verifican: acceso normal con dispositivo válido, rechazo real cuando el dispositivo fue borrado, y compatibilidad con tokens viejos. Suite completa: 96/96 tests pasando.

🔑 Cambio de contraseña — faltaba por completo

De tu lista original ("Cambiar contraseña"), no existía ningún endpoint para esto. Lo agregué real: /api/auth/change-password exige la contraseña actual, y al cambiarla revoca automáticamente todas las demás sesiones/dispositivos, dejando viva solo la sesión desde la que se hizo el cambio — la práctica estándar de seguridad para este flujo.

2 tests nuevos verifican el rechazo con contraseña incorrecta y la revocación selectiva de las otras sesiones. Suite completa: 98/98 tests pasando.

🎣 Detector de Enlaces Maliciosos — sistema nuevo, real

De tu lista original ("detector de enlaces maliciosos") faltaba este. Lo construí con heurísticas reales de phishing, no una lista negra externa:

  • Homógrafos Punycode — dominios con xn--, la técnica que permite mostrar algo que parece "apple.com" con caracteres de otro alfabeto.
  • IP directa — links a http://192.168.x.x/... en vez de un dominio.
  • Suplantación por subdominio — detecta paypal.com.verificacion.xyz.
  • Acortadores conocidos — bit.ly, tinyurl, etc.
  • Subdominios excesivos — patrón común de ofuscación.

Integrado en /api/link-preview, que ahora devuelve un campo safety junto con la vista previa normal. 8 tests cubren cada heurística por separado, incluyendo uno que confirma que NO da falsos positivos con dominios legítimos reales.

Suite completa: 119/119 tests pasando.

Actualización — ya integrado visualmente: el cliente web detecta URLs reales en cada mensaje, consulta /api/link-preview, y muestra un aviso rojo real con la lista de motivos si es sospechoso (antes de poder hacer clic), o una tarjeta de vista previa normal si es seguro. Sintaxis validada, build de Vercel corrido con el código incluido.

🕵️ Detector de Cuentas Falsas/Spam — otro sistema nuevo de tu lista original

  • Cuenta nueva + actividad dispersa: menos de 1 hora de vida escribiendo en 10+ chats distintos (patrón clásico de bot de spam).
  • Perfil vacío + alto volumen: sin foto ni bio, pero más de 50 mensajes.
  • Reportes acumulados: la señal más directa y con más peso.
  • Volumen desproporcionado: mensajes por hora anormalmente altos para la antigüedad de la cuenta.
  • Cuentas verificadas quedan exentas.

Importante: esto NUNCA banea automático — calcula un puntaje de 0 a 100 y una lista priorizada (/api/admin/risky-accounts) para que un admin humano revise primero las cuentas más sospechosas.

Optimizado desde el vamos con groupBy (no repetí el error N+1 que ya habíamos encontrado y corregido dos veces en otros módulos).

6 tests cubren cada heurística. Suite completa: 125/125 tests pasando.

🎮 Sistema de Niveles — con un bug real encontrado por su propio test

XP derivado de actividad ya trackeada (mensajes, racha, logros) — no un contador aparte que se pueda desincronizar. La racha vale más por unidad que el volumen de mensajes (premia constancia, no solo cantidad).

El propio test encontró un bug real: la fórmula original hacía que el nivel 1 exigiera 100 XP en vez de 0, así que un usuario nuevo (0 XP) mostraba un progreso de -55% en vez de 0%. Corregido.

Conectado a GET /api/achievements/me, que ahora también devuelve level: {...} — campo nuevo, no rompe nada existente.

7 tests cubren la curva de XP. Suite completa: 132/132 tests pasando.

✅ Tareas Compartidas — otro sistema de tu lista original

To-do lists reales dentro de un chat: crear, asignar a un miembro específico, marcar completada/pendiente, borrar (solo quien la creó). Construido con el criterio de autorización aprendido de todas las auditorías anteriores — verificación de membresía desde el diseño inicial:

  • Crear tarea: verifica que el creador Y el asignado (si hay) pertenezcan al chat.
  • Ver/completar tareas: verifica membresía.
  • Borrar: verifica membresía Y que sea quien la creó.

5 tests cubren cada regla de autorización. Suite completa: 137/137 tests pasando.

🐙▲🎨 Despliegue con GitHub + Render + Vercel

Esto es lo nuevo: en vez de VPS manual, el flujo real con las herramientas que vas a usar de acá en adelante.

1. Subí el proyecto a GitHub

git init
git add .
git commit -m "Pit: proyecto completo"
git remote add origin https://github.com/tu-usuario/pit.git
git push -u origin main

2. Backend en Render

  • Entrá a Render → "New" → "Blueprint" → conectá tu repo de GitHub.
  • Render lee render.yaml automáticamente y crea el servicio + la base de datos, y conecta DATABASE_URL solo (viene de fromDatabase en el blueprint — no hay que copiarla a mano).
  • Completá a mano en el dashboard: FRONTEND_URL (la vas a tener después del paso 3), y OPENAI_API_KEY si querés los sistemas de IA.
  • Render corre npx prisma db push --skip-generate --accept-data-loss && npm start solo (el proyecto no tiene migraciones commiteadas — db push sincroniza el schema directo, ver el comentario en render.yaml). Health check en /health.

3. Frontend en Vercel

  • Entrá a Vercel → "New Project" → importá el mismo repo → Root Directory: frontend.
  • Vercel lee vercel.json y corre npm run build solo.
  • Variable de entorno en Vercel: BACKEND_URL = la URL que te dio Render (ej: https://pit-backend.onrender.com). El build la inyecta en el HTML — tus usuarios no van a tener que escribir la URL del servidor a mano.

4. Volvé a Render y completá FRONTEND_URL

Con la URL que te dio Vercel (ej: https://pit.vercel.app), volvé a Render y completá esa variable. Esto activa el CORS restringido solo a tu dominio.

Listo: push a GitHub → Render y Vercel redespliegan solos en cada cambio.

🛡️ Pasada de robustecimiento (Redis opcional, seguridad, logs)

Se recibió un pedido de revisión general asumiendo un frontend en Vercel y backend en Render que no existen en este proyecto (nosotros construimos todo en este mismo sandbox, sin frontend separado desplegado). Aclarado eso, apliqué sobre el proyecto real las mejoras de esa lista que sí correspondían:

Archivos modificados:

  • backend/src/core/database/redis.ts — reescrito completo. Antes crasheaba con ECONNREFUSED si no había Redis. Ahora: si REDIS_URL existe usa Redis real (con reintentos limitados, sin loop infinito); si no existe, usa una implementación en memoria con la misma interfaz. Probado end-to-end sin Redis: isReal: false, set/get/incr/lpush/rpop funcionando.
  • backend/src/core/utils/logger.ts — nuevo. Reemplaza console.log sueltos; en producción no expone stack traces completos.
  • backend/src/index.ts — se agregó Helmet (cabeceras HTTP seguras), rate limit global (300 req/min por IP en /api), manejo de errores centralizado al final (no interfiere con ninguna ruta existente), y captura de unhandledRejection/uncaughtException para que un error suelto no tumbe el proceso. No se tocó el orden de cors(), ni se eliminó ninguna ruta, ni se cambiaron endpoints existentes.
  • modules/auth/controller.ts, modules/chat/controller.ts, modules/notifications/push.ts, api/ws/handlers.ts — logs sueltos reemplazados por el logger. Como efecto colateral de seguridad: el OTP real ya no se imprime en los logs (antes sí).

Errores corregidos: el crash por Redis ausente (el más crítico del pedido).

Riesgos encontrados: con ~19 modelos en schema.prisma acumulados en las tandas anteriores, la migración real contra Postgres no se pudo ejecutar en este sandbox (sin Docker); usar verify-migration.sh antes de producción sigue siendo la recomendación.

No se tocó: ningún endpoint, ningún nombre de modelo, ninguna tabla, ninguna ruta usada por el web-client. Todo lo que compilaba antes, compila igual ahora (tsc --noEmit → 0 errores, verificado después de cada cambio).

Pendiente de esa lista (por alcance, no por dificultad): compartir pantalla, stickers/GIFs, markdown en mensajes, vista previa de links, papelera, importar chat, copia de seguridad automática, bloqueo con PIN/ biométrico (dependen de un cliente móvil que todavía no existe), y tests automatizados. Decime cuál seguimos.

Sobre "50 sistemas revolucionarios"

Van ~24 reales y verificados. No sumé hasta 50 con nombres inventados sin código detrás — eso sería la clase de placeholder que evité desde el primer mensaje. Los que siguen (llamadas de voz/video, IA, Bluetooth nativo) son perfectamente posibles, pero necesitan su propio bloque para poder probarlos igual de en serio que a estos 24.

Próximo paso sugerido

Decime qué sistema seguís (ej: "#71 Bluetooth nearby" o "#57 resumidor con OpenAI") y lo construyo completo y verificado, igual que este núcleo.

🔒 Auditoría de seguridad + 2FA + Recordatorios + Deploy corregido (189 tests en verde)

Ronda de trabajo enfocada en encontrar vulnerabilidades reales, no en sumar funciones por sumar. Todo lo de acá abajo está probado con tests automatizados y compila limpio (tsc --noEmit → 0 errores).

Vulnerabilidades reales encontradas y corregidas:

  • SSRF en link preview — el bloqueo viejo solo tapaba 4 hostnames a mano; no cubría el endpoint de metadata de la nube (169.254.169.254, donde viven las credenciales de AWS/GCP/Azure), ni rangos privados completos, ni DNS rebinding, ni redirects hacia adentro. Nuevo core/utils/ssrfGuard.ts resuelve DNS y valida contra todos los rangos reservados antes de conectar.
  • Bypass de autorización en socketsjoin_room dejaba unirse a cualquier chat ajeno y escuchar sus mensajes en tiempo real. Ahora verifica membresía real contra la base antes de unir la sala.
  • 2FA se podía desactivar sin contraseña ni código — dos rutas distintas (PUT /api/user/me y POST /api/notifications/subscribe) pisaban TODO el JSON de settings de una, y el secret del 2FA vive ahí. La segunda ni siquiera hacía falta un ataque: activar las notificaciones push borraba tu propio 2FA sin darte cuenta. Ambas ahora hacen merge en vez de reemplazo total.
  • Condición de carrera en mensajes programados — con más de una instancia del servidor corriendo, el mismo mensaje programado se podía enviar dos veces. Ahora se reclama atómicamente antes de crearlo.
  • Reportes sin límite ni antispam — sin tope de tamaño, sin bloqueo de autoreporte, sin dedupe (se podía saturar la cola de admin repitiendo el mismo reporte).
  • docker-compose.prod.yml y deploy.sh rotos para producción real — el backend exige ADMIN_SECRET y ENCRYPTION_MASTER_KEY en producción (se niega a arrancar o revienta en la primera acción real sin ellos), pero ninguno de los dos se generaba ni se pasaba al contenedor. El deploy de 1 comando parecía funcionar (el healthcheck pasa apenas el proceso levanta) y recién explotaba al mandar el primer mensaje — el peor tipo de bug para alguien autohospedando por primera vez. También docker-compose.yml (el de desarrollo local) tenía NODE_ENV=production por error, activando esos mismos chequeos sin proveerlos.
  • Varios bugs menores: deviceName se perdía silenciosamente en el login (Zod lo descartaba por no estar declarado en el schema), dueDate inválido en tareas tiraba un 500 genérico, nombres de canal sin límite de longitud, falta de validación de tipos en avatarUrl/bio en perfil y backup.

Sistemas nuevos:

  • 2FA con TOTP (/api/auth/2fa/*) — RFC 6238 real, implementado sin dependencias externas (HMAC-SHA1 + base32 propios), compatible con Google Authenticator/Authy. Incluye códigos de recuperación de un solo uso.
  • Invitaciones con límite de usos y revocación (/api/invite/*) — un admin puede limitar cuántas veces se usa un link de invitación y revocarlo en cualquier momento, incluso antes de que expire.
  • Recordatorios de mensajes / snooze (/api/snooze/*) — posponer un mensaje y que reaparezca solo, con push notification, en el momento que elijas. Pensado para ayudar a gestionar la bandeja, no para generar enganche artificial.

Filosofía de esta ronda: se evitó a propósito cualquier mecánica de "enganche" (rachas con culpa, notificaciones de urgencia falsa, scroll infinito) — lo que hace que un producto se use por años es que sea genuinamente bueno y confiable, no que explote sesgos psicológicos.

Limitación honesta: no se pudo correr prisma generate para agregar modelos nuevos a la base (el sandbox no tiene acceso de red a los binarios de Prisma) — por eso Recordatorios se implementó sobre Redis en vez de una tabla nueva, mismo patrón que ya usan las invitaciones. Si en algún momento se quiere pasar a una tabla real de Postgres, es un cambio aislado.

📹👥 Llamadas Grupales — sistema nuevo (mesh WebRTC, real)

Hasta ahora "Llamadas Pit" (modules/calls/signaling.ts) solo soportaba 1 a 1. Se agregó modules/calls/groupCalls.ts, un módulo separado que no toca el existente, con llamadas de hasta 8 participantes por chat.

Cómo funciona (sin humo):

  • Topología mesh: cada participante abre una conexión WebRTC P2P directa con cada uno de los demás. El servidor solo relaya SDP/ICE por pares — nunca toca ni transcodifica el audio/video, cero costo de servidor por el media en sí. Es el mismo modelo que usaban Meet/Jitsi antes de migrar a un SFU, y funciona bien hasta 6-8 personas; arriba de eso el cuello de botella es la subida de cada cliente (necesitaría un SFU tipo mediasoup/LiveKit, que es un sistema aparte, no está acá ni se lo vende como si estuviera).
  • Autorización real: solo quien es miembro efectivo del chat (ChatUser) puede crear o sumarse a la llamada grupal de ese chat — misma verificación que ya protegía las llamadas 1:1.
  • Estado en Redis con TTL, no en Postgres: una llamada grupal es efímera por naturaleza, y se autolimpia (4hs) si algo se cuelga sin avisar.
  • Índice inverso user_active_call:{userId}: al desconectarse un socket sin avisar (se cierra la app, se corta la red), el servidor sabe en O(1) en qué llamada estaba para sacarlo — evita depender de redis.keys('*'), que en un servidor con muchas llamadas activas simultáneas bloquearía Redis (bug real que se evitó antes de que existiera).
  • Idempotente: si ya hay una llamada grupal en curso en un chat, unirse te suma a esa misma en vez de crear una segunda en paralelo (evita el bug de "dos llamadas grupales del mismo chat" que confunde a todo el mundo).

Eventos WS nuevos: group_call_join, group_call_joined, group_call_peer_joined, group_call_offer/answer/ice_candidate (mismo patrón que 1:1 pero con toUserId explícito por ser mesh), group_call_leave, group_call_peer_left.

Verificado: 4 tests nuevos (autorización de no-miembro, unión con lista de peers correcta, límite de 8 participantes, limpieza del estado al salir el último) — 247/247 tests en verde en total, tsc --noEmit sin errores.

No incluido en esta entrega: UI de cliente para armar la grilla de video de N participantes (el evento y la señalización ya están; falta el frontend que dibuje los <video> tags por cada peer), y el salto a SFU para más de 8 participantes.

🌐⚡ Publicar sin dominio ni VPS — quick-share.sh (sistema nuevo)

deploy.sh sigue siendo el camino correcto para algo permanente (tu propio dominio, tu propio servidor). Pero para "quiero que alguien lo pruebe HOY, sin gastar ni configurar nada", se agregó quick-share.sh:

chmod +x quick-share.sh
./quick-share.sh

Esto levanta Pit local con docker-compose.yml y lo expone con una URL pública https://algo.trycloudflare.com real (HTTPS válido, sin mentiras), usando el túnel gratuito de Cloudflare (cloudflared) — no requiere cuenta, ni tarjeta, ni dominio, ni abrir puertos en tu router. Lo instala solo si no lo tenés (detecta amd64/arm64).

Honesto: es efímero — la URL vive mientras la terminal quede abierta, y cambia cada vez que lo corrés de nuevo. Es la herramienta correcta para "che, probá esto" con un amigo en dos minutos, no para un lanzamiento real. Para eso, deploy.sh + un dominio propio (~1 USD/mes) sigue siendo el camino.

En Windows: quick-share.bat

Mismo sistema, para quien desarrolla en Windows sin terminal Linux a mano:

  1. Instalá Docker Desktop (una sola vez) y abrilo.
  2. Doble click en quick-share.bat.

Hace exactamente lo mismo que quick-share.sh (Docker + túnel de Cloudflare + URL pública), pero pensado para "apretar y listo" en Windows: si Docker no está instalado, no está corriendo, el puerto 3000 está ocupado, o falta algún archivo del proyecto, corre solo un diagnóstico (tools/deploy-doctor.js) que te dice exactamente qué pasó y cómo solucionarlo — en vez de dejarte con un error críptico. Si el entorno está bien pero algo del código falla, ese mismo diagnóstico llama al bot arreglador de bugs (tools/bugfixer.js, ver más abajo) para proponerte un parche real.

Para cortar el túnel público sin bajar Pit: doble click en stop-share.bat.

🤖🐛 Bot Arreglador de Bugs — tools/bugfixer.js

Corre la suite de tests real, junta las fallas reales (nunca inventadas), y le pide a la IA (misma OPENAI_API_KEY que usás para el resto de las features de IA) un parche mínimo propuesto para cada una — mostrando el diff en texto antes de tocar nada, y nunca aplicándolo solo por default.

node tools/bugfixer.js                # detecta y propone, no toca nada
node tools/bugfixer.js --apply        # pregunta antes de aplicar cada parche
node tools/bugfixer.js --apply --yes  # aplica sin preguntar (bajo tu propio riesgo)

Si aplica un parche y el test sigue fallando después, lo revierte solo — nunca deja un cambio que no demostró arreglar lo que decía arreglar. Más detalle en tools/README.md.

🎥📱 UI real de llamadas grupales — cierra el sistema del todo

Las llamadas grupales quedaron con la señalización lista pero sin frontend (quedó documentado como pendiente). Se agregó al web-client/index.html (el cliente sin instalación que ya se sirve en la raíz de tu dominio):

  • Botones 📞 (audio) y 🎥 (video) en la barra superior del chat.
  • Grilla de video dinámica: se agrega o saca un <video> por cada participante que entra o sale de la llamada, en tiempo real.
  • Mic/Cámara on-off, colgar, y un aviso si falla el permiso de cámara/micrófono (no un cuelgue silencioso).
  • Corre RTCPeerConnection por cada peer contra el STUN público de Google (solo para descubrir tu IP pública — el video/audio en sí sigue siendo P2P directo entre los dispositivos, cero costo de servidor para el media).
  • Enganchado 1 a 1 con los eventos de groupCalls.ts del backend (group_call_join/joined/offer/answer/ice_candidate/peer_left), sin inventar ningún evento nuevo del lado servidor.

Verificado: sintaxis JS validada (node --check), HTML balanceado (45 <div>/45 </div>, sin IDs de modal duplicados), y la suite de backend sigue en 247/247 (esto es solo frontend, no tocó ninguna ruta ni socket handler existente).

Limitación honesta: no incluye reconexión automática si un peer se cae la red (hoy simplemente se le cierra el tile), ni indicador de "hablando" por volumen de audio. Son mejoras de pulido, no bloqueantes para usarlo.

🗣️🔄 Pulido de llamadas: indicador de voz real + reconexión automática

Cerrando lo que había quedado marcado como pendiente:

  • "Quién está hablando": mide el volumen real de cada stream de audio (el tuyo y el de cada peer) con Web Audio API (AnalyserNode), no es un ícono fijo ni un mock — el tile de quien está hablando se marca con un aro verde en vivo, con un pequeño colchón de frames para no parpadear con ruido de fondo bajo.
  • Reconexión automática por peer: si la conexión con una persona se cae (disconnected/failed — wifi que titila, salto a datos móviles), se intenta un ICE restart real hasta 3 veces antes de rendirse y sacar su tile. Un peer inestable ya no te congela ese video para siempre sin avisar, y no corta la llamada para el resto.

Verificado: JS validado, HTML balanceado, 247/247 tests del backend en verde (cambio 100% frontend, no toca señalización del servidor).

🔍🌐 Búsqueda Global — sistema nuevo (buscar en TODOS tus chats a la vez)

Hasta ahora la búsqueda era chat por chat (GET /api/chat/:chatId/search). Se agregó GET /api/chat/search/global?q=..., que busca en todos los chats donde sos miembro real, de una sola vez.

  • Autorización real: primero resuelve tu lista real de ChatUser, y solo mira mensajes de esos chats — nunca de uno ajeno.
  • Desencripta antes de comparar, igual que la búsqueda individual (el contenido vive cifrado en reposo — ver la sección de cifrado más arriba).
  • Límite duro pensado a propósito: ventana de 300 mensajes recientes por chat, tope de 20 chats por búsqueda, máximo 50 resultados. Desencriptar es CPU real, no gratis; esto evita que una búsqueda global te descifre el historial completo de decenas de chats de una.
  • Devuelve de qué chat viene cada resultado (chatName), para que la UI pueda saltar directo a ese chat.

UI: en el modal de búsqueda del web-client ahora hay un toggle "Este chat" / "Todos mis chats". Tocar un resultado global te lleva directo a ese chat.

Verificado: 4 tests nuevos (autorización por membresía real, chats vacíos no dispara ninguna consulta de mensajes, rechazo de queries de 1 caracter, desencriptado correcto con el nombre del chat) — 251/251 tests en verde en total. tsc --noEmit sin errores. Frontend validado (JS y balance de HTML).

📢🔒 Canales de Difusión — sistema nuevo (uno a muchos, con privacidad real)

Además de las conversaciones normales, se agregó modules/chat/broadcastChannels.ts: canales tipo "Canal de WhatsApp/Telegram" — un solo emisor, cualquier cantidad de oyentes.

  • POST /api/broadcast/create — crea el canal, vos quedás como ADMIN.
  • POST /api/broadcast/:chatId/join — suscribirse es abierto (como seguir un canal público), no requiere invitación.
  • POST /api/broadcast/:chatId/leave — el ADMIN no puede abandonar su propio canal (tiene que borrarlo), para que no quede huérfano.
  • GET /api/broadcast/:chatId/info — devuelve followerCount, pero la lista real de suscriptores solo se la devuelve al ADMIN. Un oyente común nunca puede listar a los otros oyentes de un canal de 5000 personas — es justo la garantía de privacidad que la gente espera de un canal de anuncios, y evita habilitar acoso/spam a escala.
  • Enforcement real en /api/chat/send: si el chat tiene groupConfig.broadcast=true, solo quien tiene role ADMIN puede publicar; cualquier oyente que intente mandar un mensaje recibe 403. Se aprovechó la misma consulta de membresía que ya se hacía (con un include del chat) en vez de sumar una consulta nueva a la base en el camino caliente de enviar un mensaje.
  • Sin migración de Postgres: se guarda en groupConfig (JSON libre que ya existía en el modelo Chat) — no hizo falta tocar el schema de Prisma ni correr una migración nueva.

Verificado: 9 tests nuevos (creación, join idempotente, rechazo de join a un chat que no es broadcast, privacidad de la lista de oyentes para MEMBER vs ADMIN, el admin no puede abandonar su canal, y el enforcement de "solo admin publica" en 3 escenarios) — 260/260 tests en verde en total, tsc --noEmit sin errores.

No incluido en esta entrega: UI de cliente para crear/descubrir canales de difusión (el backend está listo; falta el botón "Crear canal" y una pantalla de búsqueda de canales públicos en el frontend).

💬🤖 Respuestas Rápidas Inteligentes — sistema nuevo (Smart Replies)

GET /api/ai/smart-replies/:chatId — mismo patrón real que el resumidor y el traductor (misma verificación de membership, mismo callOpenAI, mismo 502 explícito si falta OPENAI_API_KEY, nunca un mock silencioso):

  • Toma el último mensaje real de la OTRA persona en el chat (nunca el tuyo).
  • Le pide al modelo 3 respuestas cortas (menos de 8 palabras) en base a ese mensaje.
  • No escribe nada solo: a diferencia del resumidor, esto nunca manda un mensaje por su cuenta — son chips que aparecen sobre el composer, el usuario toca uno (o lo edita) y recién ahí lo manda como mensaje normal.

UI: en el web-client, cuando llega un mensaje nuevo de otra persona en el chat que tenés abierto, aparecen automáticamente hasta 3 chips arriba del cuadro de texto. Tocar uno lo carga en el input. Si no hay OPENAI_API_KEY configurada, simplemente no aparecen chips — no rompe el chat.

Verificado: 5 tests nuevos (autorización por membership, mensaje del otro no el propio, parseo de las 3 sugerencias, suggestions: [] sin mensajes previos, 502 explícito si OpenAI falla) — 265/265 tests en verde en total. tsc --noEmit sin errores. Frontend validado (JS y balance de HTML).

📍🔴 Ubicación en Vivo — sistema nuevo (compartir posición en tiempo real)

Feature clásica de WhatsApp que Pit no tenía: compartir tu posición GPS en tiempo real dentro de un chat, por un tiempo acotado (15min/1h/8h).

  • Efímero por diseño: el estado vive en Redis con TTL exacto a la duración elegida — nunca se guarda en Postgres. Ubicación en vivo es por definición temporal; persistirla sería justo el bug que no querés.
  • Autorización real: solo miembros del chat pueden empezar a compartir o ver quién está compartiendo — misma verificación de ChatUser de siempre.
  • Sin API de mapas de pago: cada punto se muestra como un link directo a OpenStreetMap con esas coordenadas — cero costo, cero API key.
  • Vencimiento real: cuenta regresiva en el cliente desde expiresAt, y al llegar a cero corta sola (geolocation.clearWatch + avisa al server). La TTL de Redis es la red de seguridad server-side si el navegador se cierra sin avisar.
  • Al entrar a un chat, tu cliente pide location_share_status para ver quién sigue compartiendo en ese momento (por si te sumás después de que alguien ya empezó).

Eventos WS nuevos: location_share_start/started, location_update, location_share_stop/stopped, location_share_status, location_error.

Verificado: 7 tests nuevos (autorización de no-miembro, validación de coordenadas, duración inválida cae a 15min por defecto, updates ignorados si no estás compartiendo, limpieza al detener, status vacío para no- miembros) — 272/272 tests en verde en total. tsc --noEmit sin errores. Frontend validado (JS y balance de HTML).

🎤🌊 Mensajes de Voz — sistema nuevo (con waveform real, no decorativo)

Notas de voz completas, reusando el sistema de archivos cifrados que ya tenías (POST /api/files/upload) — no se inventó un storage paralelo:

  • Grabación real con MediaRecorder del navegador.
  • Waveform real: se decodifica el audio grabado con Web Audio API y se calculan picos de amplitud reales por bloque — las barras que ves reflejan el audio de verdad, no son un patrón random de relleno.
  • Validación server-side real en /api/chat/send: un mensaje VOICE sin fileId/fileKey se rechaza (400), y un audio de más de 5 minutos también — no es cosmético, es un guardrail antes de tocar la base.
  • Reproducción: descarga el archivo cifrado con su fileKey (mismo endpoint de descarga de siempre) y lo reproduce como blob de audio.
  • El texto normal de los mensajes de texto no se ve afectado — la validación de voz solo corre si contentType === 'VOICE'.

Verificado: 4 tests nuevos (rechazo sin referencia de archivo, rechazo de más de 5 minutos, aceptación con metadata válida, mensajes de texto normales sin verse afectados) — 276/276 tests en verde en total. tsc --noEmit sin errores. Frontend validado (JS y balance de HTML).

📅✅ Eventos con RSVP — sistema nuevo (sin migración de Postgres)

Mensajes de tipo evento (título, fecha, lugar) donde cada miembro responde "Voy" / "No puedo" / "Tal vez", con conteo en vivo — sin agregar ninguna tabla nueva a la base:

  • El evento es un Message normal con contentType: 'EVENT' y los datos en metadata (columna JSON que ya existía) — mismo patrón que ya usan los Mensajes de Voz.
  • El RSVP reusa la tabla Reaction que ya tenías, con mutua exclusión real agregada encima: elegir "Voy" borra automáticamente un "No puedo" previo tuyo en ese mismo evento — a diferencia de una reacción común, donde SÍ podés tener varios emojis a la vez.
  • Tocar la misma opción de nuevo la saca (te arrepentís de haber respondido).
  • Conteos en vivo por WebSocket (event_rsvp_update) para todos los que tengan el chat abierto en ese momento.

Endpoints: POST /api/events/create, POST /api/events/:messageId/rsvp, GET /api/events/:messageId/rsvp.

UI: botón 📅 en el composer, el evento se ve con los 3 chips de respuesta y el conteo abajo.

Verificado: 9 tests nuevos (autorización en creación y RSVP, fecha inválida rechazada, mutua exclusión real comprobada con mock, toggle al tocar la misma opción, conteos correctos) — 285/285 tests en verde en total. tsc --noEmit sin errores. Frontend validado (JS y balance de HTML).

👁️💥 Ver una vez — sistema nuevo (fotos/videos que se autodestruyen)

GET /api/files/view-once/:fileId?key=...&messageId=... — se apoya 100% en el sistema de archivos cifrados que ya existía (mismo AES-256-GCM, mismo isValidFileId contra path traversal); lo nuevo es la lógica de "quemado":

  • El estado "ya se vio" vive en message.metadata (columna Json que ya existía) — cero migraciones nuevas de Postgres.
  • Autorización real: solo miembros del chat pueden siquiera intentarlo.
  • Se quema recién después de descifrar con éxito: si alguien prueba con la key equivocada, no gasta la única vista real — el archivo se borra del disco solo cuando la lectura + descifrado ya funcionaron.
  • El remitente no se queda afuera de lo suyo: puede volver a ver lo que mandó las veces que quiera; el "una sola vez" es sobre la experiencia del destinatario, no un candado contra el propio dueño del contenido.
  • Segundo intento del destinatario → 410 Gone explícito ("ya se vio y se autodestruyó"), nunca un error genérico confuso.

Para mandar contenido así, el cliente sube el archivo con /files/upload como siempre, y al enviar el mensaje agrega metadata: { fileId, mimeType, viewOnce: true } — no hace falta ningún endpoint nuevo de envío.

Verificado: 4 tests nuevos (rechazo si no es viewOnce, rechazo de no-miembro, ciclo completo ver-y-quemar con archivo real en disco, y que el remitente no gasta su propia vista) — 289/289 tests en verde en total. tsc --noEmit sin errores.

No incluido en esta entrega: UI de cliente para sacar/enviar foto en modo "ver una vez" y mostrarla con el candado visual antes de abrirla — el endpoint y la lógica de quemado ya están listos para que el frontend los use.

🗂️📁 Carpetas de Chats — sistema nuevo (organización personal)

Con muchos chats/canales/grupos, encontrar el que buscás se vuelve un lío. Se agregó modules/chat/folders.ts (/api/chat-folders): carpetas propias tipo "Trabajo", "Familia", "Clientes" — como las carpetas de Telegram.

  • 100% personal: cada usuario arma las suyas; no afecta a nadie más del chat (a diferencia de los canales/sub-canales, que sí son compartidos).
  • Vive en User.settings (columna Json que ya existía) — cero migraciones nuevas de Postgres, mismo patrón que ya usa el proyecto para los canales de difusión.
  • Autorización real: no podés meter en una carpeta un chat del que no sos miembro real.
  • Límites sensatos: 20 carpetas, 200 chats por carpeta, nombre hasta 30 caracteres — para que esto siga siendo liviano dentro del JSON.

Endpoints: GET /api/chat-folders, POST /:folderName (crear), DELETE /:folderName (borrar), POST /:folderName/chats/:chatId (sumar), DELETE /:folderName/chats/:chatId (sacar).

UI: botón 🗂️ en la barra superior del web-client — ver tus carpetas, crear una nueva, y tocar una carpeta suma el chat que tenés abierto en ese momento.

Verificado: 6 tests nuevos (rechazo de chat ajeno, validación de nombre, creación + alta real, sin duplicados, borrado que no toca otras carpetas, límite de 20 carpetas) — 295/295 tests en verde en total. tsc --noEmit sin errores. Frontend validado (JS y balance de HTML).

🧵💬 Hilos de Respuesta — sistema nuevo (thread view, tipo Slack)

replyToId ya existía en Message, pero antes solo se veía disperso en el chat ("responde a X" en cada mensaje suelto). Se agregó GET /api/chat/thread/:messageId, que arma la vista completa:

  • El mensaje raíz + todas sus respuestas directas juntas, ordenadas, ya desencriptadas.
  • Autorización real: solo miembros del chat del mensaje raíz pueden verlo — mismo control que el resto del proyecto.
  • replyCount real, no estimado — cuenta las respuestas reales en la base.

UI: cada mensaje ahora tiene botones "Responder" (deja el próximo mensaje enganchado a este vía replyToId, con un preview cancelable arriba del composer) y "Ver hilo" (abre el panel con el mensaje original y todas sus respuestas juntas).

Verificado: 3 tests nuevos (404 si el mensaje raíz no existe/está borrado, 403 si no sos miembro del chat, contenido descifrado + conteo real de respuestas) — 298/298 tests en verde en total. tsc --noEmit sin errores. Frontend validado (JS y balance de HTML).

🌴🤖 Auto-Respuesta / Modo Ausente — sistema nuevo

/api/auto-reply — cuando lo activás con un mensaje ("estoy de vacaciones hasta el 5"), la primera vez que alguien te escribe en un chat, Pit le contesta solo en tu nombre.

  • Vive en User.settings (Json existente) — cero migraciones nuevas.
  • Guarda real contra el loop infinito (el bug clásico de este tipo de feature, si dos personas tienen auto-respuesta activada a la vez): un mensaje marcado metadata.autoReply: true nunca dispara otra auto-respuesta — se corta ahí, sin ni siquiera consultar la base.
  • Cooldown real de 3hs por (chat, remitente) en Redis: no te va a repetir "estoy ausente" en cada mensaje de la misma conversación, solo la primera vez que te escriben en ese período.
  • Se dispara desde el mismo POST /api/chat/send que ya existía, sin bloquear la respuesta al que envió el mensaje original (corre en background, igual que las notificaciones push).

UI: en el modal de Perfil del web-client, nueva sección con checkbox + textarea para configurar tu auto-respuesta.

Verificado: 5 tests nuevos (rechazo de activar sin mensaje, null si no está activada, mensaje real la primera vez, cooldown real en el segundo intento, corte total del loop si el mensaje entrante ya era una auto-respuesta) — 303/303 tests en verde en total. tsc --noEmit sin errores. Frontend validado (JS y balance de HTML).

📌📋 Múltiples Mensajes Fijados — mejora real (no un sistema nuevo aislado)

Bug de diseño real corregido: pinnedMsgId guardaba un solo mensaje fijado por chat — fijar uno nuevo hacía que el anterior se perdiera silenciosamente, sin aviso. Ahora:

  • Lista real en Chat.groupConfig.pinnedMessageIds (Json que ya existía, cero migraciones nuevas), hasta 20 por chat.
  • pinnedMsgId se sigue actualizando al último fijado — no rompe clientes viejos que solo leían ese campo.
  • Nuevos endpoints: POST /api/chat/pin/:chatId/:messageId (ahora suma en vez de reemplazar), DELETE para desfijar uno sin tocar los demás, y GET /api/chat/pins/:chatId para listarlos todos, descifrados.
  • Sigue siendo solo-ADMIN, igual que antes — no se relajó ningún control.

UI: botón 📌 en la barra superior con el panel completo (desfijar cada uno desde ahí), y botón "📌 Fijar" en cada mensaje.

Verificado: 5 tests nuevos (rechazo de no-admin, segundo fijado sin perder el primero — el bug real que existía —, límite de 20, desfijado que no toca a los demás, listado descifrado solo para miembros) — 308/308 tests en verde en total. tsc --noEmit sin errores. Frontend validado (JS y balance de HTML).

👤 Cuenta Invitado — probar Pit real antes de registrarte

Sistema nuevo: POST /api/auth/guest crea una cuenta real (mismo backend, mismo cifrado E2E — no una demo con datos hardcodeados) sin pedir teléfono ni OTP. Pensado para que alguien pruebe la app entera antes de comprometerse a registrarse.

  • Genera su par de claves E2E en el cliente, igual que el registro normal.
  • Se crea con isGuest: true y expiresAt a 24hs.
  • Se la mete directo en un chat de bienvenida real con PitBot (3 mensajes de tour reales, cifrados, no texto estático en el HTML).
  • Rate-limit real por IP (5 cuentas invitado por hora) — a diferencia del registro normal, acá no hay OTP por SMS que frene un loop de curl.
  • Bloqueada de Pit Pay (plata) y de configurar guardianes de Recuperación Social — los dos casos donde una identidad que se autodestruye en 24hs es un problema real, no cosmético (middleware blockGuest en auth/middleware.ts).
  • Worker nuevo (core/queue/guestSweeper.ts) corre cada hora y borra las cuentas invitado vencidas junto con su chat, mensajes y dispositivos — nunca deja basura acumulándose.
  • POST /api/auth/guest/upgrade: convierte la MISMA cuenta (mismo id, mismos chats y mensajes — no crea una cuenta nueva) en una cuenta real, exigiendo un OTP real del número nuevo antes de aceptarlo (mismo nivel de prueba de dueño del teléfono que el registro normal — nada de confiar en el body a ciegas).

Verificado: 11 tests nuevos (creación real, chat de bienvenida con 3 mensajes del bot, rate-limit de 5/hora, publicKey obligatoria, upgrade rechazado sin OTP válido, upgrade con OTP válido preserva el mismo id de usuario, blockGuest deja pasar/rechaza según corresponda, sweeper borra mensajes+membresías+chat+dispositivos+usuario y no rompe si el invitado nunca llegó a tener chat) — 461/461 tests en verde en total. tsc --noEmit sin errores. Botón "Probar sin registrarte" agregado al web-client, con banner de aviso de cuenta temporal y modal de upgrade in-app.

📵 Bug real corregido: SMS-bombing sin freno en /otp/request

POST /api/auth/otp/request (el que dispara el envío real de SMS) no tenía ningún límite por número de teléfono — solo el rate limit global de 300 req/min por IP, que no frena nada si el objetivo es un solo número desde varias IPs (trivial de rotar). En la práctica: cualquiera podía bombardear de SMS el teléfono de otra persona, o generar costo real en el proveedor de SMS, sin ninguna fricción real.

Agregado canRequestOtp() en otp.service.ts: máximo 3 pedidos de OTP por número cada 10 minutos, independiente del límite por IP. Se aplica también al flujo de upgrade de Cuenta Invitado, que usa la misma ruta.

Verificado: 4 tests nuevos (permite hasta 3, rechaza el 4to, el límite es por número no global, y la ruta real responde 429 — no solo la función interna) — 465/465 tests en verde en total. tsc --noEmit sin errores.

🌐 Bug real corregido: trust proxy nunca configurado (rompía TODO el rate limiting por IP)

index.ts nunca llamaba app.set('trust proxy', ...). Efecto real: atrás de nginx (infrastructure/nginx/, que sí manda X-Forwarded-For bien) o del túnel de quick-share.bat, Express ignora ese header y usa la IP del socket TCP crudo como req.ip — que es siempre la IP del proxy, la misma para absolutamente todos los usuarios reales.

Esto rompía silenciosamente TODO el rate limiting por IP que se armó en esta sesión (y el que ya existía): el límite global de 300 req/min, el de 5 cuentas invitado/hora y el de 3 OTP/10min por teléfono dejaban de ser "por atacante" y pasaban a ser un único contador global compartido por toda la app — un usuario pesado real podía trabar el límite para todo el mundo, y un atacante nunca llegaba a su propio límite porque estaba sumando en el pozo de todos.

Arreglado con app.set('trust proxy', 1) — confía en exactamente un hop hacia arriba (el proxy inmediato), sin abrir la puerta a que un cliente spoofee su propia IP agregando su propio X-Forwarded-For (nginx sobreescribe ese header con la IP real antes de reenviar).

Verificado: 3 tests que arman una mini app Express real con la misma config (no un mock) y confirman el mecanismo real: sin trust proxy se reproduce el bug (ignora el header), con trust proxy(1) toma la IP real del cliente, y una cadena de IPs falsa por delante no cambia cuál gana — 468/468 tests en verde en total. tsc --noEmit sin errores.

💸 Bug real corregido: Pit Pay dejaba enviar plata a un usuario que no existe

Wallet.userId es un String suelto en el schema, sin foreign key a User (no hay ninguna relación entre los dos modelos). Consecuencia real: /wallet/transfer no validaba en ningún lado que toUserId fuera un usuario de verdad — un typo en el id (o un bug del frontend) debitaba la plata real de quien envía, creaba una wallet huérfana para un id que no existe, y devolvía 200 OK. La plata desaparecía sin error y sin forma de recuperarla.

De paso, encontrado por la misma revisión: nada impedía transferirle plata a una cuenta invitado — que se borra sola en 24hs (ver sección de Cuenta Invitado más arriba). Sin este fix, esa plata también se perdía para siempre.

Corregido: /transfer ahora busca al destinatario antes de tocar ningún saldo — 404 si no existe, 400 si es una cuenta invitado — y recién ahí hace el UPDATE atómico real (que ya estaba bien, sin doble gasto).

Verificado: 2 tests nuevos (404 real cuando el id no existe, sin llegar a debitar nada; 400 real al intentar mandarle a un invitado, sin llegar a debitar nada) — 470/470 tests en verde en total. tsc --noEmit sin errores.

🔒 Bugs reales corregidos: secrets de placeholder aceptados como reales en producción

JWT_SECRET tenía un fail-fast en producción, pero solo chequeaba "¿está definida?" — si alguien copiaba .env.example a .env y se olvidaba de reemplazar el placeholder público (cambia_esto_en_produccion), esa variable SÍ estaba "definida", así que el fail-fast nunca se disparaba. En producción quedaba la misma vulnerabilidad que ese sistema existe para prevenir: cualquiera que lea el repo conoce el secret real.

Peor: ADMIN_SECRET (el que protege el panel de administración completo — banear usuarios, ver reportes) no tenía ningún fail-fast, ni siquiera el chequeo de "no vacío". Centralizado en un getAdminSecret() nuevo (mismo patrón que getJwtSecret()), con el mismo fail-fast real en producción, y agregado el mismo chequeo de "no es el placeholder público" a ambos.

Verificado: 6 tests nuevos (JWT_SECRET y ADMIN_SECRET, cada uno: funciona si es real, falla si falta, falla si es el placeholder, no falla en desarrollo) más 1 test que confirma que la ruta real de admin rechaza un x-admin-secret que matchea el placeholder — 478/478 tests en verde en total. tsc --noEmit sin errores.

🚀 Nueva herramienta: chequeo pre-publicación (tools/publish-check.js)

A pedido explícito: un "arreglador para publicar" que junta en un solo comando todo lo que hay que revisar antes de sacar Pit OS a producción — secrets, consistencia de .env, compilación, tests, vulnerabilidades — y puede arreglar automáticamente lo que tiene sentido arreglar solo (generar secrets faltantes, instalar dependencias). Ver tools/README.md para el detalle completo, incluidos los bugs reales que aparecieron construyendo y probando la herramienta misma.

node tools/publish-check.js         # diagnóstico
node tools/publish-check.js --fix   # diagnóstico + arreglos automáticos

🌐 Bug real corregido: el "túnel resiliente" de quick-share.sh no reintentaba en la práctica

quick-share.sh tiene set -e desde el principio, y el loop que relanza el túnel de Cloudflare si se cae usaba wait $CF_PID sin más. Si cloudflared termina con código distinto de cero (lo normal: red inestable, que Cloudflare cierre el túnel, cualquier corte) — set -e frenaba el script ENTERO ahí mismo, antes de llegar al aviso de "se cortó, reintentando" y al reintento. La función completa de "túnel resiliente" (documentada en el propio comentario del script) estaba rota: un corte de red durante una demo terminaba el script en silencio en vez de reconectar solo.

Confirmado con una reproducción mínima aislada de set -e + wait sobre un proceso que sale con error, antes de asumir dónde estaba el problema. Corregido con wait $CF_PID || true — neutraliza el código de salida solo de ese wait puntual, sin debilitar set -e para el resto del script (que sigue abortando ante errores reales de Docker/curl más arriba).

Verificado: reproducido el bug real con un script mínimo aislado (el loop terminaba después del primer wait, sin imprimir nada más), aplicado el mismo fix a ese repro y confirmado que ahora sí completa los reintentos. bash -n sin errores de sintaxis en quick-share.sh y stop-share.sh.

🛠️ Nueva herramienta: el arreglador definitivo (tools/publish-master.js)

A pedido explícito, una versión mucho más potente de publish-check.js: además de diagnosticar, busca alternativas reales cuando algo falla en vez de rendirse — reinstala dependencias limpias si npm install falla, busca Postgres/Redis nativos si no hay Docker, y prueba una cadena de túneles públicos (cloudflaredlocaltunnelserveo.net por SSH) cayendo al siguiente automáticamente si uno falla — desinstalando lo que instaló recién si no funcionó, sin tocar nada que ya estuviera en la máquina de antes. Funciona con o sin OPENAI_API_KEY (los primeros 4 pasos son 100% determinísticos; la IA solo entra para proponer parches de bugs de código reales). Ver tools/README.md para el detalle completo.

node tools/publish-master.js

🔴 Bug crítico corregido: la app en Vercel mandaba mensajes SIN cifrar

El hallazgo más serio de toda la auditoría hasta ahora, y no era de concurrencia: frontend/ — el único cliente que frontend/build.js empaquetaba y el único que vercel.json despliega — era una versión PREVIA a la reconstrucción real de E2E (ver CRYPTO_DESIGN.md). No tenía ni una línea de cifrado del lado del cliente: mandaba content tal cual a /api/chat/send. El backend ya había sido migrado para esperar que content venga como un sobre cifrado por ECDH desde el cliente, y ya no lo cifra en reposo (esa capa se sacó a propósito, asumiendo que el cliente cifra antes).

Resultado real: cualquiera usando la app desplegada en Vercel guardaba sus mensajes en texto plano de verdad en Postgres, y cualquier destinatario usando el cliente E2E real (backend/web-client/index.html, el que sí tiene el cifrado completo) fallaba al descifrar y veía "🔒 No se pudo descifrar este mensaje" — mensajería rota en silencio entre los dos clientes, sin ningún error visible que apuntara a la causa real.

Corregido reescribiendo frontend/build.js para empaquetar backend/web-client/ (el cliente con el cifrado E2E completo) en vez de los archivos propios y desactualizados de frontend/, agregando el soporte del placeholder __BACKEND_URL__ que le faltaba a ese cliente para mantener la inyección de la URL del backend en el build de Vercel. frontend/index.html se dejó en el repo con una advertencia grande al inicio, pero el build ya no lo usa.

Verificado: build corrido a mano con y sin BACKEND_URL seteada, confirmando que el placeholder se reemplaza igual que antes. Suite completa del backend sin tocar: sigue en verde.

🔍 Auditoría sistémica completa de backend/src/modules/ (patrón hang-P2025 + lost-update)

Barrido módulo por módulo de TODO el backend buscando dos patrones de bug concretos, no una revisión genérica: (1) una mutación de Prisma sin try/catch que, bajo una carrera real (doble tap, dos usuarios casi simultáneos), choca con P2025/P2002 y deja la request colgada para siempre en vez de responder (Express 4 no reenvía rechazos async al error middleware sin next(err) explícito); y (2) campos JSON compartidos leídos-modificados-reescritos sin lock, que pierden escrituras concurrentes en silencio (lost update).

Encontrados y corregidos con el mismo patrón de fix en cada caso: llamadas grupales (calls/groupCalls.ts, el mismo lost-update pero en Redis en vez de Postgres — resuelto con primitivas atómicas nuevas SETNX/SADD/SREM en el cliente de cache), ajedrez (games/chess.ts, dos jugadas casi simultáneas podían perder una jugada del historial — resuelto con pg_advisory_xact_lock igual que en otros módulos), y el borrado de credenciales biométricas (biometric/controller.ts, el hang-P2025 clásico). wallet/controller.ts (transferencias de dinero real) se revisó a fondo y ya estaba sólido — sin cambios necesarios.

Verificado: 594/594 tests en verde, tsc --noEmit sin errores, npm audit en 0 vulnerabilidades — reconfirmado después de cada fix, no solo al final.

🗂️ Índices faltantes en el schema de Prisma

Revisión completa de prisma/schema.prisma contra el criterio ya aplicado en otros modelos (índices con comentario justificando por qué). Encontrados 3 modelos consultados por una columna sin índice — full table scan en cada llamada, cada vez peor a medida que crece la tabla: WebAuthnCredential (listar credenciales / chequear duplicados al registrar, por userId), Device (pantalla de "dispositivos vinculados", por userId), y Block (tenía índice único compuesto [blockerId, blockedId] que solo sirve para "¿a quién bloqueé?" — pero "¿quién me bloqueó a mí?" consulta por blockedId solo, sin poder aprovechar ese índice). Los 3 corregidos con @@index.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages