Capítulo servidor de la guía Neblla — modos de ejecución, estado autoritario, handover y anti-cheat. El contrato base está en core.txt. # El servidor ## Tres modos de ejecución Los archivos de `server/` corren en una de tres "casas". Se cambia con un selector en `Settings → Server mode`, sin tocar la lógica del juego: 1. **peer + JavaScript** (default) — Neblla elige uno de los dispositivos conectados para ejecutar `room.js`. Ese dispositivo es el servidor durante la partida; al terminar la sala, desaparece. Coste cero. Riesgo: el dueño del dispositivo puede inspeccionar la lógica en DevTools y manipularla. 2. **peer + WebAssembly** — `room.ts` en AssemblyScript (subset estricto de TypeScript) se compila a `.wasm` en el navegador del dev al subirlo — sin build server. La lógica sigue en un peer, pero el estado vive en memoria lineal de WASM, opaca a DevTools. Detiene al hacker casual. La traducción JS→AssemblyScript es mecánica (el contrato del servidor es estricto: solo eventos, sin DOM, sin métodos de usuario) — una IA puede hacerla desde el editor integrado o vía MCP. 3. **infraestructura de Neblla** — un proceso por sala, estado persistente durante toda la sesión, clientes que no pueden inspeccionar ni impersonar el servidor. Mismo código fuente. El contrato que hace posible la portabilidad (se paga escribiendo así desde el día 1): - Los archivos de `server/` no tocan el DOM (`window` y `document` sombreados). - No existe `neblla.me` en server-side. No hay "yo" autoritario. - Cliente y servidor se hablan siempre por eventos (`tellServer`, `messageToServer`, `messageFromServer`). ## Elección del server peer El SDK elige al server peer al hacer `startRoom()`. Su identidad es opaca: no hay `neblla.server` ni `user.isServer`. Nunca escribas código que inspeccione el id del server peer. ## Estado autoritario — setState `neblla.setState(state)` **(SERVER)** emite el estado a todos los dispositivos de la sala — incluidos los peers de respaldo que tienen `room.js` cargado en standby: ```javascript // room.js (corre en el dispositivo-servidor) let state = neblla.state || { tablero: initialBoard(), turno: 0 }; neblla.on('messageToServer', event => { if (esLegal(state, event.data)) { state.tablero = aplicar(state.tablero, event.data); state.turno++; neblla.setState(state); // emite 'state' a todos, replica a backups } }); ``` ```javascript // client.js neblla.on('state', state => render(state)); console.log(neblla.state); // siempre el último valor ``` Sincronizar a nuevos jugadores: ```javascript // room.js neblla.on('memberJoinedRoom', () => { neblla.setState(gameState); // emite 'state' a todos }); ``` Usa `setState`, no `sendMessage`, para el estado: ```javascript // ❌ Mal — mensaje genérico: neblla.sendMessage({ type: 'state', state: newState }); // ✅ Bien — primitivo de estado: neblla.setState({ tablero, turno }); ``` `setState()` replica a los backups y emite `'state'` en una sola llamada. Si el server-peer cae, el handover es instantáneo porque los backups ya tienen el último state. ## Handover y sucesión (modo peer) El peer-servidor puede caerse en cualquier momento (pestaña cerrada, túnel, batería). Tres piezas lo resuelven: 1. `setState()` replica el estado a los peers de respaldo (arriba). 2. La sucesión está preacordada: cada dispositivo sabe quién es el siguiente en línea. Sin elección ni negociación. 3. La transición no espera al `disconnect`: cuando la conectividad del servidor pasa a `inactive`, el handover se dispara inmediatamente. El sucesor arranca `room.js` fresco con el último `neblla.state` ya en mano. Evento `'newServerPeer'` `{ room, isMe }` (`isMe` true si tú eres el nuevo peer). La pausa es imperceptible. El usuario que perdió la conexión conserva su periodo de gracia (más largo que el del servidor): si vuelve a tiempo, reentra como miembro normal, sin ser ya el servidor. En infraestructura nada de esto hace falta: el proceso es único y persistente. `setState()` sigue funcionando igual (emite el evento), pero no hay backups que mantener. ## Server sin DOM ```javascript // ❌ Mal — room.js toca el DOM: document.getElementById('counter').textContent = state.turno; // ✅ Bien — room.js solo cambia estado, el cliente reacciona: // room.js neblla.setState({ ...state, turno: state.turno + 1 }); // client.js neblla.on('state', state => { document.getElementById('counter').textContent = state.turno; }); ``` `document` y `window` están sombreados en server-side: el código mal escrito se rompe en desarrollo, no al cambiar de modo. La UI vive en `client.js`. ## tellServer, no broadcast filtrado ```javascript // ❌ Mal — broadcast con filtro manual: neblla.sendMessage({ type: 'move', square: 'e4', forServer: true }); // ✅ Bien — primitivo dedicado: // cliente neblla.tellServer({ move: 'e4' }); // room.js neblla.on('messageToServer', event => { // solo llegan aquí los mensajes para el servidor }); ``` `tellServer` no se difunde al resto de clientes. `tellServer` es no-op si la sala aún no tiene server peer. ## Protecciones siempre activas - **Datos privados aislados.** `neblla.me.data` es por usuario y vive en su sesión. Nunca llega a otros clientes, ni al peer que ejecuta `room.js`. Lo público va por `publicInfo`. - **Pagos mediados.** `neblla.buy()` abre un checkout de Stripe alojado por Neblla. `room.js` solo ve el resultado (compra confirmada), nunca datos de tarjeta ni la capacidad de falsificar compras. - **Backoffice con bandera.** El dashboard muestra datos, mensajes, actividad y `flagCount` de usuarios sospechosos. Desde ahí se banea, sin redeploy. ## Anti-cheat opt-in Relevante en modo peer: el peer-servidor podría anunciar resultados falsos (corre en un dispositivo que su dueño controla). En infraestructura no aplica. **`neblla.confirm(eventName, value)`** **(CLIENT)** — observar y reportar. Cada cliente llama a `confirm` cuando observa un evento crítico. El servidor recoge los reportes durante unos 3 segundos y comprueba: - ¿todos los valores son estrictamente iguales (`===`)? - ¿reportó al menos el 75% de la sala? Si las dos respuestas son sí, silencio. Si alguna falla, el SDK del servidor llama solo a `neblla.flag()`. Un peer-servidor mentiroso queda atrapado porque el `client.js` de cada testigo reporta su propio estado local. El `value` tiene que ser primitivo (string, number finito, boolean). No vale objeto, array, `null`, `undefined` ni `NaN` — cada afirmación debe ser un dato inequívoco que la sala pueda votar. ```javascript // client.js, al detectar el ganador anunciado por el server neblla.on('messageFromServer', event => { if (event.data.type === 'state' && event.data.state.ganador) { neblla.confirm('ganador-final', event.data.state.ganador); } }); ``` ```javascript // ❌ Mal — pasa un objeto: neblla.confirm('estado', { tablero, turno }); // el SDK lo rechaza // ✅ Bien — afirmaciones inequívocas: neblla.confirm('ganador', userId); neblla.confirm('score-final', 1250); ``` **`neblla.flag(reason?)`** **(BOTH)** — marca la sala como sospechosa. Se llama automáticamente cuando `confirm` falla; también a mano desde cualquier cliente o server-side. Una vez marcada, los siguientes `flag()` en la misma sala son no-ops (anti-troll). El `flagCount` sube en todos los usuarios de la sala (logueados lo persisten, anónimos en sesión) y se ve en el backoffice. Evento `'flagged'` `{ room, reason, flaggerId, userCount }` para que la app reaccione, una vez por sala. **Consejo práctico.** Lanza en modo peer todo lo que no sea dinero real. Pon `confirm()` en los eventos que importan (ganador, score final, envío a leaderboard). Pasa a infraestructura solo cuando cada resultado tenga que ser infalsificable en tiempo real. Al cambiar, los `confirm()` se quedan donde están: en infra son redundantes y el SDK los ignora sin coste de bandwidth.