You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pippygames/docs/platform/client-communication-profil...

25 KiB

Clientes web y Flutter: comunicación bidireccional — propuesta v2

Estado: diseño del 6 de octubre de 2026. El usuario decide que los controles móviles pueden usar la web de la plataforma y, posteriormente, una app independiente Flutter solo de controles. El tablero y las vistas completas se representan en la web. Se prepara el contrato para ambos; no se desarrolla ahora una app ni un transporte nuevo. Complementa la API/eventos, la arquitectura, el perfil multidispositivo y la sincronización temporal. Los esquemas y binding v1 siguen sin cambios.

1. Modelo común, presentación y runtime distintos

presentationProfile: TVBoard|Desktop|Mobil decide la vista; surface: player|shared-display decide el papel autorizado. clientRuntime: web|flutter declara el cliente disponible para compatibilidad, sin conceder permisos. El móvil web puede representar el mando TVBoard (player-controller) o la vista completa Mobil (player-full). Flutter representa exclusivamente player-controller dentro de TVBoard, tanto con TV común como personal: controles por fase y datos privados necesarios para decidir, sin tablero completo. La TV web conserva public-board pasivo. Flutter no implica otra cuenta, plaza, servidor o reglas y no ofrece Mobil ni Desktop completos.

El servidor Go sigue siendo la autoridad. Cada cliente transforma una interacción en una acción semántica, valida/decodifica respuestas y representa su proyección. Las reglas, ofertas, secretos, azar, puntuación y recibos se deciden en el mismo servicio. La app no comunica jugadas directamente a la TV: ambas conexiones pasan por el servidor, aunque los dispositivos compartan WiFi.

Se separan adaptadores de transporte/autenticación, SDK de protocolo y renderer del juego. Los SDK TypeScript y Dart comparten esquemas de datos y fixtures, no código ejecutable por suposición. No viajan elementos DOM, objetos Svelte, widgets Flutter, callbacks o código descargable dentro de una respuesta de partida.

El acceso mediante dispositivos de TV contempla Silk en Fire TV, Google TV/Android TV, un futuro receptor Cast propio y navegador por HDMI. El adaptador de acceso a pantalla y su motor gráfico son ejes independientes: WebGL no es obligatorio. Un receptor HTML5 puede reutilizar contratos web, pero exige su binding de sesión/Origin y ciclo de vida comprobados; una app de TV futura debe declarar su runtime real. No se añade ahora un enum o SDK por inferencia. La app Flutter continúa siendo exclusivamente de controles.

Los modelos de datos compartidos concretan esa fuente neutral: JSON Schema y DTOs generados para TypeScript, Go y Dart. En errores, ambos SDK usan el mismo ErrorData y conservan código, origen, mensaje/argumentos, resultado y recuperación. Un fallo de red se normaliza con origen transporte y estado incierto cuando pueda haber commit; no se reduce a una cadena distinta en cada cliente. La capa de transporte añade correlación, sin modificar el error duradero del recibo. Hay fixtures base; la integración de validadores/adaptadores en red sigue pendiente.

2. Transporte propuesto

Se propone HTTPS JSON para acceso, catálogo, admisión/QR y gestión; WSS para partida bidireccional, con un binding versionado común a web y Flutter. La guía oficial de Flutter documenta enviar y recibir mensajes mediante WebSocket. La selección de librería y versión se hace al implementar; no añade dependencias ahora.

El binding futuro define ruta, subprotocolo, autenticación inicial, negociación, cierre y límites. Cada mensaje de texto UTF-8 contiene un sobre JSON completo; realtime.* se reserva al establecimiento de conexión y, tras autenticar/negociar, se admiten las familias publicadas platform.*, room.*, game.* y time.* según el rol. round.* identifica eventos visibles dentro de game.snapshot.events, no otra familia de sobres. Las versiones de transporte, protocolo de mensajes, proyección y renderer son contratos diferentes. No se cambia el sobre según sea Dart o JavaScript. Números, UUID, enums, fechas, ausencia/null y límites deben tener la misma interpretación en ambos.

La API v1 documentada usa eventos platform:message y game:message de Socket.IO. Socket.IO y WebSocket simple no son intercambiables, como explica su documentación oficial. Mientras exista aquel binding, necesita un cliente compatible; el nuevo endpoint no recibe paquetes antiguos como si fueran JSON v2. Convivir/adaptar transportes nunca crea dos autoridades para una sala.

Una conexión WSS autenticada puede multiplexar las suscripciones autorizadas de plataforma y partida. Cada una mantiene revisión, cursor, subscriptionId y contrato propios. Se fijan límites de conexiones/suscripciones, tamaño, profundidad, frecuencia, buffers y colas, heartbeat, timeout y backoff con jitter. No se habilita long-polling, compresión o binario por inferencia: necesitan contrato y pruebas separados. Mensajería push futura tiene otra finalidad y no sustituye la partida en directo.

Para Cast directo se propone que el receptor reciba su proyección pública por HTTPS/WSS desde Go, mientras el móvil sigue enviando acciones a Go. El canal Cast lanza/vincula/gestiona la pantalla y no sustituye game.command, recibos o autoridad de reglas. Duplicar una pestaña pública es otra vía de visualización, con demora adicional que el ping del mando no mide. Todas mantienen los mismos alcances room|participant y flujos QR/invitación del perfil multidispositivo.

3. Autenticación web y nativa

La web mantiene cookie opaca HttpOnly, Secure, origen permitido y CSRF. La app necesita un perfil de sesión nativa que la plataforma emita y revoque: token opaco presentado por Authorization: Bearer en HTTPS, con caducidad y recuperación/renovación documentadas, ligado a la misma identidad y políticas. Declarar clientRuntime: flutter, un device ID o carecer de Origin no acredita ser una app ni permite saltar autenticación. El servidor distingue sesiones por su registro emitido, no por User-Agent.

No se incorpora una contraseña o secreto de aplicación común como credencial de todos los móviles. El perfil nativo debe definir emisión/rotación concurrente, almacenamiento protegido por el sistema operativo, logout, revocación, cambio de cuenta y pérdida del dispositivo antes de publicarse. Renovar no elimina plazos absolutos/restricciones de sesión. Si se adopta OAuth para ese acceso, el flujo nativo usa navegador externo y authorization code con PKCE según RFC 8252; no se da por contratado un proveedor ni por existente ese flujo.

Para unificar la autenticación WSS sin depender de cabeceras personalizadas del navegador, se propone este intercambio técnico, pendiente de esquemas/rutas:

  1. La conexión todavía sin acceso recibe realtime.challenge con nonce de conexión, binding y límites mínimos. Solo puede autenticar dentro de un plazo corto; no recibe sala, cuenta o juego.
  2. El cliente pide POST /api/v2/realtime/tickets por HTTPS con ese nonce. La web aporta cookie + origen/CSRF; la app aporta su sesión nativa válida. Una pantalla aporta exclusivamente su contexto limitado. El servidor deriva principal o vínculo, alcance y generación, y emite un ticket breve de un solo uso ligado a esa conexión y sesión.
  3. El cliente envía realtime.authenticate con el ticket por WSS. El servidor lo consume atómicamente, revalida la sesión/vínculo y responde realtime.authenticated. Entonces negocia platform.hello/platform.welcome y los contratos de partida. No se permite una suscripción antes de autenticación y negociación.

El ticket no aparece en query string, QR, subprotocolo, logs o analítica; su cuerpo y respuesta se excluyen de trazas y usan no-store. Las emisiones técnicas necesitan su propio intento/recuperación acotada y no se tratan como jugadas. Al reconectar se usa un ticket nuevo; un timeout de autenticación no reutiliza un secreto consumido. El ticket de TV nunca permite actuar como humano. Las cuotas y TTL definitivos forman parte del binding ejecutable, no de un valor supuesto aquí.

La conexión queda ligada a la autorización vigente y a una vida limitada por la sesión. Logout, suspensión, revocación de tutela/condiciones o cambio de cuenta corta suscripciones afectadas y limpia cachés privadas en ambos clientes. Antes de acciones y entregas se revalidan permisos; un token o ticket válido no convierte al usuario en elegible para cualquier juego o chat. Los permisos de menores son idénticos en web y app.

El desafío conserva identidad/generación de la conexión pendiente y el Origin observado por el servidor. Un ticket web solo se consume en esa conexión con origen explícitamente permitido; no se confía en un origen declarado en el JSON. Ausencia de Origin únicamente puede continuar con el perfil nativo acreditado por su sesión, nunca con una cookie web. Conexiones anónimas tienen cuotas y plazo antes de emitir desafío. El registro de emisión/consumo debe coordinarse entre el nodo HTTPS y el nodo WSS; perder el nodo invalida el desafío y requiere otro, sin reconstruir una autorización desde datos del cliente. Estas comprobaciones siguen las recomendaciones de OWASP para WebSocket.

4. Negociación y controles compatibles

La negociación declara binding/protocolos admitidos, runtime/build del cliente y tuplas exactas de presentación disponibles. game.sync.client propone clientRuntime y, por tupla, rendererId, rendererVersion y rendererApiVersion, además de las versiones de reglas/proyección/presentación. La solicitud identifica perfil/superficie; el servidor deriva la proyección, comprueba el paquete fijado y devuelve en game.snapshot el contrato aceptado. Runtime/build son metadatos de compatibilidad, no autoridad sobre actor o identidad ni prueba de integridad de la app. Instalar una app nueva no cambia reglas de una partida existente.

El paquete v2 propone presentation.profiles[].surfaces.<surface>.renderers, con implementaciones por runtime para la misma proyección. Un renderer web puede tener una entrada JavaScript/TypeScript del paquete. Flutter solo se declara para TVBoard / player / player-controller; la negociación rechaza combinaciones Flutter de tablero o vista completa aunque el cliente las solicite. Su renderer de controles referencia rendererId, versión/API y contratos que la app incluye y registra en su build; no presupone ejecutar .ts ni descargar/compilar Dart arbitrario. La elección del motor gráfico web no exige un motor gráfico equivalente en Flutter.

Para controles sencillos se puede publicar además un modelo declarativo versionado, interpretado por renderers previamente admitidos en ambos clientes: selección, botón, número, etiquetas traducibles y restricciones. Debe tener esquemas cerrados, accesibilidad y límites, sin scripts/HTML evaluables. El descriptor referencia acciones/parametrización permitidas; las ofertas de servidor siguen decidiendo qué puede ejecutarse. Los mandos privados complejos pueden requerir controles propios incluidos en la app, manteniendo su alcance de mando. La solución elegida se declara y se prueba por juego, no se supone un formulario universal que sustituya todas las presentaciones.

availableActions expresa acciones legales, payloads/esquemas y precondiciones; el renderer presenta controles por fase/rol y envía una acción estructurada. El servidor no envía un botón ejecutable ni confía en que un control deshabilitado impida un comando manual. Tocar, usar teclado o agitar produce el mismo tipo de acción; los datos brutos del sensor no se transmiten salvo un perfil explícito de habilidad.

No todos los juegos tienen controles Flutter al publicarse. El catálogo del cliente ofrece únicamente combinaciones verificadas de runtime/renderer/perfil; si falta soporte se explica y puede abrirse la web mediante un flujo autorizado. El acceso TV personal exige «Jugar solo en el móvil»: desde Flutter se abre obligatoriamente la vista web Mobil, mediante un paso declarado y probado que autentique al mismo principal y conserve sala/recibos. No se presupone que el navegador comparte sesión con la app ni se envía el token de sesión al navegador mediante URL. La transferencia de mando y presentación se confirma bajo la barrera de la sección 6, con snapshot actual en la web; solo entonces se revoca la TV personal. Si el destino no está preparado o el paso falla, se conserva el recorrido vigente y se recupera la operación sin crear otra plaza.

5. Acciones e información en ambos sentidos

Mensaje/operación Dirección Finalidad
platform.hello / platform.welcome Cliente → servidor / servidor → cliente Negociar versiones, conexión y límites después de autenticar.
game.sync Cliente → servidor Solicitar su contrato y vista; nueva suscripción con barrera.
game.snapshot Servidor → cliente Estado autorizado, contexto, fase, ofertas, resultado y eventos de su proyección. También entrega actualizaciones confirmadas; no exige sondeo continuo.
game.command Cliente → servidor Acción semántica con UUID estable, controlGeneration y precondición de la oferta. No envía actor, resultado ni estado global nuevo.
game.ack Servidor → emisor autorizado Recibo aplicado/rechazado; informa sobre el comando, no sustituye el snapshot.
game.command-status Cliente → servidor Consultar un comando incierto con su UUID. El contrato vigente responde con recibo/ack o game.command-unknown; no se inventa otro mensaje implícito.
game.cursor / game.error Servidor → cliente Detectar retraso/huecos y recuperación explícita, sin filtrar datos privados.
game.unsubscribe Cliente → servidor Cerrar su suscripción; no retira al participante de la sala.
time.probe, time.sample, time.ready, time.readiness propuestos Según operación Medición y disponibilidad técnica de la conexión autorizada. Los estados/eventos de ronda llegan en snapshots, ligados a época/ronda/contrato.

Ejemplo ilustrativo del mismo comando v2 enviado por web o Flutter; requiere los futuros esquemas y un juego que declare esa acción:

{
  "protocolVersion": 2,
  "type": "game.command",
  "matchId": "cc749a64-c8e4-4d74-a913-54b39f4c9089",
  "commandId": "30c474dd-030a-4e0e-883f-c8a6d636c8c9",
  "controlGeneration": 3,
  "expected": { "kind": "match", "revision": 12 },
  "action": { "type": "drop", "payload": { "column": 3 } }
}

La secuencia es: el cliente recibe una oferta, guarda el sobre/UUID antes de enviar, representa «pendiente» y lo transmite. El servidor autentica/autoriza, comprueba recibo y precondición, ejecuta reglas y confirma estado + recibo + outbox. Devuelve game.ack al principal emisor y distribuye snapshots de esa revisión: mando privado a su móvil, vista completa a sus suscripciones compatibles y tablero público a las TVs autorizadas. Acuse y vista pueden llegar en distinto orden. TV no recibe la mano ni el recibo personal y carece de comandos.

Esa secuencia presupone mando activo adquirido y fuente temporal autorizada. Una segunda conexión no recibe ofertas ejecutables ni el estímulo de reacción por una ruta alternativa; se aplica la sección siguiente antes de actuar. La consulta de un recibo confirmado precede a las precondiciones de generación/turno actuales, para recuperar sin repetir el efecto.

sequenceDiagram
    participant C as Móvil web o Flutter
    participant S as Servidor Go
    participant T as TV autorizada
    C->>S: game.sync (perfil y contratos)
    S-->>C: game.snapshot (vista y ofertas propias)
    C->>S: game.command (UUID, generación, precondición, acción)
    Note over S: Validar y confirmar estado, recibo y outbox
    S-->>C: game.ack (recibo propio)
    S-->>C: game.snapshot (proyección personal)
    S-->>T: game.snapshot (proyección pública)

La retransmisión puede duplicar entregas; la identidad idempotente sigue siendo partida + principal + commandId, independiente de runtime, conexión y perfil. Reenviar el mismo sobre no repite su efecto. Diferentes UUID representan intenciones diferentes y se someten a ofertas/precondiciones; no se deduplican por parecido de payload. Un callback de transporte o una pulsación animada no demuestra que hubo commit.

6. Cortes, segundo plano y cambio de cliente

Ambos SDK mantienen estados de conexión, negociación, sincronización, pendiente y recuperación explícitos. Guardan de forma acotada y aislada por principal los sobres pendientes, último contrato/revisión y datos necesarios para recuperar recibos; secretos de partida y credenciales tienen otra política de caché/almacenamiento. Al cambiar de cuenta no se reproducen los comandos de la anterior. No se guarda una cola ilimitada ni se genera otro UUID automáticamente tras timeout.

Al reconectar o volver al primer plano: reautenticar cuando proceda, negociar, obtener snapshot actual, consultar recibos inciertos y reintentar exactamente el sobre admisible. unknown no prueba que un envío previo no vaya a confirmarse. Una nueva intención requiere oferta actual y revisión explícita del usuario, sin alterar fecha/precondición de la intención pendiente. Se ignoran mensajes de suscripciones viejas y eventos ya aplicados.

No se promete mantener un socket activo cuando el sistema suspenda la app. Pasar a segundo plano no equivale a retirada, y al volver se sincronizan estado y reloj. Sensores, animación, muestras y colas de input se detienen/descartan según el perfil; una respuesta de una ronda cerrada no se publica después como actual. La app futura conserva los límites/empates/neutralización del perfil temporal, sin recibir más compensación por declararse nativa.

Usar otra sesión web/nativa del mismo principal recupera la misma membresía; autenticarse con otra cuenta no la transfiere. Cambiar cliente es una reconexión y negociación de renderer, distinta de cambiar TVBoard a Mobil. Ambas operaciones preservan recibos y respetan barreras/puntos seguros de ronda. El paso seguro se concreta mediante la generación de mando siguiente, sin presuponer almacenamiento compartido entre web y app.

Continuidad del mando entre conexiones

La propuesta v2 mantiene un mando activo por membresía humana/partida y una controlGeneration duradera. Otras conexiones autorizadas pueden leer su vista cuando lo permita el perfil temporal. game.snapshot.context.control comunica { generation, canSend } a clientes humanos; la TV no recibe ese control. Solo el mando activo obtiene ofertas ejecutables. Abrir una pestaña o negociar game.sync no roba el mando anterior ni incrementa la generación.

Tras autenticar el nuevo cliente, una solicitud explícita de transferencia identifica su conexión autorizada y la generación esperada. El servidor comprueba que pertenece al mismo principal, su renderer/perfil y la membresía. Bajo la misma serialización que comandos e ingresos duraderos, resuelve o rechaza definitivamente los ingresos antiguos pendientes, incrementa la generación y cambia el mando. La nueva conexión recibe un snapshot posterior a esa barrera antes de actuar. Si también cambia presentación, ambas operaciones se coordinan en el mismo punto seguro; una ronda de reacción difiere la transferencia hasta entre rondas.

La primera adquisición parte de generación 0 sin mando y utiliza la misma operación. La API de transferencias devuelve un recibo inmutable de solicitud y una operación consultable; pending no habilita acciones. La confirmación inicial de entrada/listo puede coordinar esa adquisición para la conexión propia, sin deducirla del orden de conexión. Si cambia solo presentación en el mismo cliente, se conserva el mando y se aplica la barrera de suscripción; un cambio conjunto usa una sola operación. Una reconexión a otro socket requiere readquisición explícita, que la interfaz puede presentar como «Continuar aquí», sin exigir conocer los UUID del dispositivo perdido.

game.command v2 incluye controlGeneration en su sobre y huella idempotente. Para un UUID ya confirmado se devuelve el recibo propio antes de comprobar la generación antigua. Un comando nuevo de una generación sustituida se rechaza con STALE_CONTROL_GENERATION; ningún paquete retrasado del antiguo cliente puede aplicarse después de la barrera. La consulta de recibos propios sigue disponible desde cualquier sesión autorizada del principal. Así, aunque el nuevo cliente no conozca los UUID guardados por el anterior, recupera el estado confirmado sin que una intención anterior aparezca después. No inventa un comando para repetirla; una acción nueva requiere decisión del jugador y oferta nueva.

La desconexión por sí sola no retira al jugador ni transfiere mando automáticamente. Si se pierde la conexión destino durante una transferencia, se consulta su recibo y se repite una adquisición autorizada; nunca quedan dos mandos activos por un timeout. En cambio de cuenta, el nuevo principal no hereda la generación. Este registro es de plataforma; no convierte controlGeneration en identidad de actor, no cambia los UUID de recibos y no se aplica a las instancias IA internas.

7. QR y enlaces de acceso

La URL HTTPS de la plataforma identifica la finalidad del flujo. La web permite escanear sin instalar una app; la app puede leer ese mismo QR y continuar mediante las mismas rutas HTTPS. Los enlaces verificados del sistema pueden abrir la app instalada, con alternativa web. Flutter documenta deep linking para Android, iOS y web; dominio/rutas, asociación de plataforma y manejo del fragmento deben probarse en dispositivos reales.

Se mantienen finalidades distintas: preparación de mesa, tvboard-entry para altas sin invitaciones y controller-link para la TV personal de un invitado. Un lector QR no convierte un código en sesión ni acredita edad/tutela. Solo acepta origen/ruta de plataforma permitidos, interpreta el flujo y presenta confirmación; tokens siguen opacos, breves y validados por servidor, en fragmento/POST según el contrato. No se coloca una credencial permanente en un enlace, ni se añade el secreto a query string para resolver un fallo de deep link.

Autenticación de la app, acceso a sala y emparejamiento de TV son flujos diferentes. Redirecciones/callbacks de autenticación usan su propia correlación/finalidad y no reutilizan QR de juego. Cualquier paso web ↔ app requiere autorización del mismo principal, no copia implícita de cookies o tokens entre runtimes. Pérdida de respuesta, doble escaneo y app instalada/no instalada conservan los mismos límites/idempotencia de admisión.

8. Verificación antes de habilitar Flutter

Publicar binding y esquemas neutrales, fixtures de sobres/vistas/controladores y SDK TypeScript/Dart conformes. JSON Schema describe datos y debe complementarse con validación de relaciones, claves duplicadas en texto, rutas y límites. Las reglas Go, DTO de SDK y renderers se prueban contra los mismos vectores; generar tipos no sustituye validar mensajes recibidos.

La matriz compara mando web y Flutter con los mismos permisos, acciones, resultados y proyección player-controller; errores y valores límite; versiones de app antiguas; renderer ausente; QR instalado/no instalado; sesiones caducadas/revocadas; cambio de cuenta; cortes antes/después de commit, duplicados y mensajes tardíos; suspensión móvil y reloj. Comprueba que Flutter no negocia player-full ni public-board y que «Jugar solo en el móvil» transfiere a Mobil web, sin perder membresía/recibos ni revocar la TV antes de completar el paso. Incluye adquisición inicial, transferencia concurrente, operación pendiente/rechazada, destino desconectado, envío tardío del mando antiguo y recuperación desde otro cliente sin sus UUID. Se prueba también intento web de obtener ticket nativo omitiendo Origin y reutilización de tickets entre nodos. Se exige aislamiento de secretos entre suscripciones/TVs, topes de memoria y equivalencia de accesibilidad/idiomas de controles. No se necesita instalar Flutter ahora: primero se materializan los contratos y el SDK web; la app se añade después con su perfil de sesión y controles verificados.

Powered by TurnKey Linux.