|
|
# API y eventos de plataforma — propuesta v1 para Juegoland v2
|
|
|
|
|
|
**Estado:** borrador revisado el 6 de octubre de 2026; salvo las rutas iniciales de catálogo/bootstrap descritas en el [handoff](../HANDOFF-2026-10-06.md), no implementado. La revisión introduce cambios incompatibles respecto del borrador anterior; no implica un cambio de protocolo desplegado. Se completa con el [modelo](../platform-spec-v2.md), [protección del menor](child-safety.md) y registros de revisión del [5](review-2026-10-05.md) y [6 de octubre](review-2026-10-06.md). El protocolo `game.*` conserva su [contrato v1](../game-engine/format-and-protocol.md); el [perfil multidispositivo](../game-engine/multi-device-profile.md) propone su versión 2.
|
|
|
|
|
|
## 1. Convenciones y autorización
|
|
|
|
|
|
La API y el servidor/motor de `games2` usan Go por decisión del usuario. Los contratos de dominio no dependen del lenguaje. [Arquitectura](architecture.md) propone un backend modular; implementar reglas y cambiar el binding documentado Socket.IO por WebSocket son trabajos separados, aún pendientes. Este documento conserva el perfil Socket.IO explícito hasta versionar y probar su sustituto. Las [políticas de producto](product-policies.md) concretan P1–P8 y son la referencia para plazos y admisión.
|
|
|
|
|
|
HTTP nuevo bajo `/api/v2`. `/api` anterior debe pasar por las mismas políticas durante la migración. JSON UTF-8, fechas UTC con `Z`, UUID para entidades de plataforma; `gameId`, versiones, digests y asientos siguen el formato del motor. Revisiones son enteros seguros no negativos. La excepción explícita son marcas temporales de ronda con sufijo `Ms`: intervalos monotónicos en milisegundos ligados a `clockEpoch`, según el [perfil temporal](../game-engine/synchronization-profile.md#4-reloj-medición-y-autoridad), no fechas UTC.
|
|
|
|
|
|
Los [modelos compartidos](shared-data-models.md) fijan primitivas, mensajes y `ErrorData` para Go, Svelte y Flutter desde una única fuente JSON Schema con DTOs generados. Hay verificación estructural de ese contrato base; OpenAPI y los esquemas completos de flujos/sobres v2 siguen pendientes. La adopción del nuevo error en runtime es un corte explícito, no una modificación silenciosa de los mensajes `game.*` v1.
|
|
|
|
|
|
Las lecturas con texto humano negocian idioma por `Accept-Language` y declaran `Content-Language` y `Vary: Accept-Language`; claves, códigos, permisos y revisiones no cambian por lengua. La selección explícita y el fallback se definen en [localización](localization.md). En el catálogo inicial, cada ficha declara su `locale` real para no ocultar una traducción ausente.
|
|
|
|
|
|
Las solicitudes tienen esquemas cerrados por versión. Se rechazan campos desconocidos, claves JSON duplicadas, Unicode inválido, números no finitos y exceso de bytes/profundidad antes de ejecutar. Claves duplicadas requieren validación en el decodificador de texto del transporte: Zod/JSON Schema sobre el objeto ya parseado no las detectan. Las respuestas permiten extensiones opcionales compatibles; tipos/códigos nuevos requieren una estrategia de fallback o una nueva versión.
|
|
|
|
|
|
| Requisito de acceso | Significado |
|
|
|
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| Público seguro | Proyección para anónimo/edad desconocida, sin información social no revisada. |
|
|
|
| Sesión | Principal válido; no basta con que exista una cookie. |
|
|
|
| Juego | Sesión, cuenta y condiciones habilitadas, membresía/rol y política de contenido/admisión. |
|
|
|
| Chat | Juego + mayoría de edad acreditada + SMS vigente + sin restricción de comunicación/bloqueo; también para lectura e historial. |
|
|
|
| Propietario | Principal dueño del recurso; nunca un `userId` autoritativo suministrado en el cuerpo. |
|
|
|
| Seguridad | Reportes, bloqueo, logout y gestión de datos siguen accesibles sin chat/SMS; las condiciones nuevas no impiden reportar o salir. |
|
|
|
|
|
|
Los menores y edades desconocidas no reciben chat por ninguna ruta, socket o adaptador. Ser anfitrión, tutor o tener un token no cambia esto. Los DTO no publican teléfono, prueba de edad, condición de menor ni causa privada de restricciones de terceros.
|
|
|
|
|
|
El perfil web HTTP usa cookie opaca; mutaciones y resolución de secretos exigen origen permitido y token CSRF ligado al contexto de navegador/sesión. Sus sockets exigen origen y CSRF en handshake además de autenticación. `bootstrap` emite un contexto anónimo aleatorio para vincular solicitudes iniciales; no acepta uno inventado por el cliente. Tras cambiar de identidad se rota ese contexto.
|
|
|
|
|
|
El [perfil web/Flutter propuesto](client-communication-profile.md) añade un adaptador de sesión nativa emitida/revocable por la misma plataforma, con credencial HTTPS y autenticación WSS mediante ticket breve de conexión. No aplica a una app una comprobación de cookie/CSRF de navegador como sustituto de identidad, ni acepta ausencia de `Origin`/etiqueta Flutter como autorización. Son extensiones futuras con esquemas y pruebas propios; el binding Socket.IO v1 y las rutas ejecutables actuales no adquieren soporte nativo por existir esta propuesta. Permisos, DTO y recibos de dominio son comunes a ambos clientes.
|
|
|
|
|
|
Éxito de lectura: `{ requestId, data }`. Éxito de mutación: `{ requestId, commandId, duplicate, receipt, data? }`; `data` es una proyección autorizada en ese momento, no contenido privado guardado para repetir sin reautorizar. `requestId` lo genera el servidor en HTTP; el `requestId` de consulta socket es UUID del cliente para correlación y no sustituye la traza interna.
|
|
|
|
|
|
## 2. Idempotencia y recibos
|
|
|
|
|
|
Mutaciones de dominio HTTP llevan `Idempotency-Key` UUID y `issuedAt` UTC en el cuerpo. Los comandos socket llevan `commandId` e `issuedAt`. Un registro común de plataforma se identifica por **principal + commandId**, independiente del transporte. La huella incluye operación, recurso, precondiciones, `issuedAt` y payload validado con [JCS](https://www.rfc-editor.org/rfc/rfc8785.html); excluye cookie, CSRF y datos de transporte. Los comandos `game.*` conservan su registro y forma propios.
|
|
|
|
|
|
No se conserva el JSON canónico en claro dentro del recibo. Si incluye OTP, teléfono o token, la huella se calcula con HMAC y clave separada para impedir comprobaciones por diccionario sobre secretos cortos. Los errores y trazas tampoco incorporan esos valores.
|
|
|
|
|
|
Orden obligatorio: autenticar, buscar recibo propio, comparar huella, comprobar autorización actual para datos de respuesta y, si es nuevo, autorizar la operación y validar precondiciones en la transacción. Una restricción de membresía posterior no impide recuperar el recibo mínimo de una salida propia. Nunca se devuelve una vieja instantánea privada al repetir un comando.
|
|
|
|
|
|
La ausencia de recibo no reserva la clave: solicitudes simultáneas se serializan mediante restricción única y la misma transacción del cambio. Repetición exacta devuelve recibo idéntico con `duplicate: true`; otro contenido da `COMMAND_ID_REUSED` sin sobrescribir el anterior. Rechazos de negocio definitivos se guardan; fallo de infraestructura, límite o autorización no consumen un comando como rechazo de negocio.
|
|
|
|
|
|
```json
|
|
|
{
|
|
|
"status": "applied",
|
|
|
"resource": {
|
|
|
"kind": "room",
|
|
|
"id": "1093543c-d383-437c-a427-251d39d63e42"
|
|
|
},
|
|
|
"changed": true,
|
|
|
"revisions": { "room": 6, "match": null }
|
|
|
}
|
|
|
```
|
|
|
|
|
|
Recibo rechazado al adoptar el perfil común v2: `{ status: "rejected", error: ErrorData, resource, observedRevisions }`, con `error.origin: server` y `error.outcome: rejected`. El error es duradero y no se traduce ni cambia al repetir; la correlación del intento pertenece al sobre. Los contratos v1 que usaban `code` se normalizan mediante adaptador explícito. Chat aplicado añade `messageId` y `chatSequence`, sin texto. No-op devuelve `changed: false`, revisión actual y ningún incremento. Un ack de creación de desafío SMS confirma que se aceptó la tarea, no que se verificó el teléfono.
|
|
|
|
|
|
Para comandos nuevos, `issuedAt` no puede tener más de 24 h ni estar más de 5 min en el futuro; `bootstrap.serverTime` permite corregir reloj. Si existe recibo, se consulta antes de caducidad/precondiciones. Los recibos de sala/chat duran tanto como sus datos; el resto al menos 24 h. Tras purgarlos, el sobre original ya caducado se rechaza en vez de aplicarse de nuevo. Una intención revisada por el usuario puede crear otro UUID; el cliente nunca cambia UUID o fecha automáticamente para reintentar.
|
|
|
|
|
|
`GET /api/v2/commands/{commandId}` y `platform.command-status` devuelven `found` con recibo mínimo propio, o `unknown`. `unknown` no prueba que un envío retrasado no vaya a confirmarse. Se reenvía exactamente el mismo sobre mientras sea admisible. No se expone el resultado de otro principal.
|
|
|
|
|
|
### Operaciones que emiten secretos
|
|
|
|
|
|
Login, alta y recuperación no reutilizan ciegamente el mecanismo de recibos de dominio ni guardan credenciales en su huella/log. P6 fija correo/contraseña, recuperación y registro de intento de autenticación propio: respuesta cifrada recuperable durante 5 min, ligada al contexto y prueba original, sin reemitir sesiones revocadas. La sección 9 concreta flujos; faltan esquemas ejecutables y pruebas de emisión concurrente.
|
|
|
|
|
|
Alta de invitado debe vincularse al contexto anónimo emitido en `bootstrap` y a su intento, con unicidad para no crear dos identidades al perder la respuesta. Repetir solo puede recuperar esa emisión dentro de su ventana, no una identidad convertida posteriormente en cuenta. Una emisión de sesión recuperable usa respuesta cifrada, vinculada al contexto original, durante un máximo de 5 min; luego exige el flujo de acceso correspondiente. El invitado queda limitado a juego individual hasta completar requisitos.
|
|
|
|
|
|
Creación/rotación de invitación almacena hash para validar el token y una copia cifrada de respuesta durante 24 h para el mismo principal/comando. La consulta de estado nunca devuelve el secreto. Un reintento autorizado durante la ventana puede recuperarlo; después devuelve recibo e `invitationId` con `secretAvailable: false`. Rotar después es una intención nueva y revoca el token anterior. No se prolonga caducidad al repetir.
|
|
|
|
|
|
## 3. HTTP
|
|
|
|
|
|
Las tablas indican operaciones mínimas y campos relevantes. Las políticas de credenciales, supervisión y plazos personales están desarrolladas en P6–P8; el proveedor de edad, los esquemas ejecutables y procedimientos de evidencia deben concretarse antes de habilitarlas.
|
|
|
|
|
|
### Sesión, cuenta y protección
|
|
|
|
|
|
| Método y ruta | Solicitud / datos | Acceso |
|
|
|
| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
|
| `GET /api/v2/bootstrap` | `serverTime`, versiones, `defaultLocale`, `supportedLocales`, límites, sesión propia mínima, condiciones y capacidades de protección | Público seguro, `no-store`; CSRF propio fuera de vistas públicas. |
|
|
|
| `POST /api/v2/terms/accept` | `version`, `accepted: true` → recibo | Contexto propio o sesión; versión exacta. |
|
|
|
| `POST /api/v2/terms/revoke` | Versión/referencia de aceptación → registro revocado | Propietario; invalida capacidades protegidas de todas sus sesiones. |
|
|
|
| `POST /api/v2/auth/guest` | Nombre seguro → identidad/cookie | Contexto anónimo, aceptación; sin reservar asiento. |
|
|
|
| `POST /api/v2/auth/register`, `/api/v2/auth/login` | Alta pendiente / credencial → sesión con capacidades reales | P6 y sección 9; nunca se acepta edad acreditada del cliente. |
|
|
|
| `POST /api/v2/auth/logout` | Revocar sesión actual | Seguridad; puede repetirse sin sesión vigente. |
|
|
|
| `GET /api/v2/me/sessions`, `POST /api/v2/me/sessions/{sessionId}/revoke` | Sesiones propias mínimas / revocación | Propietario; no devuelve hashes/tokens. |
|
|
|
| `GET /api/v2/me` | Perfil y preferencias propias con revisión | Propietario; separado del perfil público. |
|
|
|
| `PATCH /api/v2/me/profile` | `expectedRevision`, campos permitidos, `avatarMediaId?` | Propietario y políticas de contenido. |
|
|
|
| `GET /api/v2/me/safety` | Estados mínimos de edad/teléfono, capacidades y `safetyRevision` | Propietario; no expone pruebas ni número completo. |
|
|
|
| `POST /api/v2/me/age-checks` | Inicio de comprobación → referencia de proveedor | Cuenta; umbral acreditado por proveedor evaluado, nunca prueba autodeclarada. |
|
|
|
| `POST /api/v2/me/phone-challenges` | `phoneNumber`, finalidad → desafío y tiempos, HTTP 202 | Cuenta adulta acreditada; acción explícita, límites SMS. |
|
|
|
| `POST /api/v2/me/phone-challenges/{challengeId}/verify` | `code` → estado propio | Principal/sesión del desafío, vigente e intentos disponibles; código nunca en URL. |
|
|
|
| `POST /api/v2/me/phone/revoke` | Desvinculación → capacidades actualizadas | Propietario con reautenticación; corta chat. |
|
|
|
| `POST /api/v2/me/exports`, `POST /api/v2/me/deletion-requests` | Solicitud → trabajo y estado | Propietario, comprobación reciente de titularidad y P8; no requieren chat/SMS. |
|
|
|
|
|
|
Callbacks de edad/SMS son interfaces servidor a servidor autenticadas, con audiencia/nonce, firma, vencimiento y dedupe. No existe un endpoint público para establecer `verified`, `adult` o `guardian`. Los códigos fallidos tienen contador atómico; reenviar no reinicia la cuota acumulada. Un SMS entregado no cambia por sí mismo la elegibilidad.
|
|
|
|
|
|
### Catálogo, salas, invitaciones y medios
|
|
|
|
|
|
| Método y ruta | Solicitud / datos | Acceso y precondición |
|
|
|
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
|
| `GET /api/v2/catalog` | Fichas aptas y paginadas | Público seguro; una proyección personalizada no entra en caché público. |
|
|
|
| `GET /api/v2/games/{gameId}/versions/{version}` | Distribución pública, digests, estado de publicación | Política de contenido; retirar no convierte en secreto lo ya publicado. |
|
|
|
| `GET /api/v2/rooms?scope=public\|mine` | Resumen paginado | `public` seguro; `mine` exige sesión. Sin chat, tokens, lista infantil o estado privado. |
|
|
|
| `POST /api/v2/rooms` | `game: { id, version, packageDigest, dependencyLockDigest }`, capacidad, visibilidad, nombre, configuración; `modeId` y `presentationProfile` inicial al adoptar perfil v2; `tvBoardSetupToken?` para preparación desde TV | Juego; fija la selección exacta o rechaza `PACKAGE_NOT_CREATABLE`. En preparación TV confirma sala, anfitrión, vínculo y QR de entrada en una unidad de trabajo. |
|
|
|
| `GET /api/v2/rooms/{roomId}` | `RoomView` | Solo miembros/participantes históricos autorizados, también en salas públicas. |
|
|
|
| `POST /api/v2/rooms/{roomId}/memberships` | `invitationToken?` o `screenEntryToken?`, excluyentes según `entryMethod` → membresía | Juego; validación atómica de capacidad, finalidad/generación del token, estado y protección. Un miembro actual no ocupa otra plaza. |
|
|
|
| `POST /api/v2/invitations/resolve` | Token → ficha segura mínima y `roomId` | Lectura sin consumo, CSRF y límites; no usa Idempotency-Key. Token ausente/inválido sin detalle. |
|
|
|
| `GET /api/v2/rooms/{roomId}/invitations` | Metadatos/revisiones, nunca tokens | Anfitrión autorizado. |
|
|
|
| `POST /api/v2/rooms/{roomId}/invitations` | `expectedRoomRevision`, vencimiento/usos → ID/token | Anfitrión en espera; valida cuotas y política. |
|
|
|
| `POST /api/v2/invitations/{invitationId}/revoke` | `expectedRevision` → recibo | Anfitrión actual; token deja de admitir entradas. |
|
|
|
| `POST /api/v2/invitations/{invitationId}/rotate` | `expectedRevision` → nuevo secreto | Anfitrión actual; revoca anterior en la misma transacción. |
|
|
|
| `POST /api/v2/me/media` | Imagen acotada → `mediaId`, estado de validación | Cuenta elegible; menores/edad desconocida solo presets. |
|
|
|
| `GET /api/v2/me/media/{mediaId}` | Estado propio `pending\|ready\|rejected` | Propietario; solo `ready` puede usarse como avatar. |
|
|
|
|
|
|
La URL compartida propone `/join#invite=TOKEN`: el fragmento no se envía en la solicitud HTTP. El cliente lo lleva a memoria, limpia la barra mediante reemplazo del historial y lo resuelve por POST; la pantalla usa `Referrer-Policy: no-referrer`, `Cache-Control: no-store` y no carga terceros. El usuario debe reabrir su enlace si pierde el contexto antes de entrar. No se registran cuerpos con tokens.
|
|
|
|
|
|
Gestión de invitación tiene `invitationRevision`; no aumenta `roomRevision` salvo que cambie también el estado/configuración de sala. La comprobación de anfitrión se hace bajo bloqueo de sala, aunque la precondición sea sobre invitación.
|
|
|
|
|
|
Al adoptar el perfil v2, `entryMethod: standard|screen-qr` se deriva del modo publicado y queda fijado en la sala. Crear la mesa presencial `TVBoard` usa `screen-qr`; sus rutas de emisión, rotación, resolución o consumo de invitaciones se rechazan. `Desktop`/`Mobil` usan el flujo convencional, que también puede admitir `TVBoard` como presentación personal del jugador invitado si el paquete lo declara. Elegir una presentación no cambia el método de entrada existente.
|
|
|
|
|
|
### Pantallas compartidas (extensión futura del perfil multidispositivo)
|
|
|
|
|
|
Estas rutas se reservan para salas cuyo `modeId` permita `interaction.sharedDisplay` en el [perfil v2](../game-engine/multi-device-profile.md). Distinguen pantalla común `displayScope: room` y TV personal `displayScope: participant`; el servidor deriva propietario/alcance del flujo autorizado y del paquete, nunca de un campo que conceda permisos por sí solo. No forman parte de la base ejecutable actual ni conceden acceso a un espectador remoto. La pantalla obtiene un contexto de dispositivo anónimo y acotado por `bootstrap`, separado de cuentas y membresías; todas las respuestas llevan `Cache-Control: private, no-store`.
|
|
|
|
|
|
| Método y ruta | Solicitud / datos | Acceso y efecto |
|
|
|
| --- | --- | --- |
|
|
|
| `POST /api/v2/displays/challenges` | `issuedAt` → `challengeId`, `challengeToken` opaco de al menos 128 bits, `expiresAt`, QR con ese token en fragmento y código manual opcional | Contexto de pantalla emitido por servidor, origen y CSRF válidos; sin `roomId` ni datos de partida. `Idempotency-Key` liga un único desafío a ese contexto. |
|
|
|
| `GET /api/v2/displays/current` | Estado propio `waiting\|paired\|revoked\|expired`, caducidad y, solo si está vinculado, `roomId`, `displayId` y alcance; en pantalla común de mesa en espera, juego público apto y QR de entrada vigente | Únicamente el contexto autorizado; no devuelve lista de sala, membresía propietaria de otra persona ni secretos de juego. El QR de altas nunca se entrega a una TV personal. |
|
|
|
| `GET /api/v2/rooms/{roomId}/displays` | `displayRevision`, pantallas autorizadas, estado y caducidad, sin código de emparejamiento | Gestión de pantalla común por permiso; miembro con TV personal solo obtiene su propio vínculo. |
|
|
|
| `POST /api/v2/rooms/{roomId}/displays` | `issuedAt`, exactamente uno de `challengeToken` o `challengeCode`, alcance solicitado compatible, `expectedDisplayRevision` → recibo, `displayId` | Consume el desafío y ambos alias; deriva el alcance: común exige gestión de sala; personal exige membresía propia. En protegidas interviene el responsable autorizado. No recibe un asiento elegido por cliente como autoridad. |
|
|
|
| `POST /api/v2/rooms/{roomId}/displays/{displayId}/revoke` | `issuedAt`, `expectedDisplayRevision` → recibo | Gestor autorizado de pantalla común, propietario de TV personal o responsable autorizado; invalida inmediatamente sesión y suscripción correspondientes. |
|
|
|
|
|
|
El límite propuesto es 5 minutos y 5 intentos fallidos por desafío reconocido, además de cuotas por contexto, principal, IP y sala. Los códigos inexistentes también consumen cuota de búsqueda para impedir enumeración. El alias manual opcional usa 10 símbolos base32 aleatorios (50 bits); **no** satisface el requisito de 128 bits del token QR ni sirve de sesión. Ambos identifican el mismo desafío de un solo uso y se invalidan juntos. Colisiones de alias se rechazan/regeneran al crear; todos los límites se fijan en los futuros esquemas antes de habilitar el flujo.
|
|
|
|
|
|
Los secretos se generan criptográficamente; nunca se registran ni transmiten en URL de petición, analítica o referer. El QR usa el token en fragmento; la aplicación lo pasa a memoria, limpia el historial y lo entrega solo en el cuerpo del POST, sin cargar terceros. Repetir exactamente la creación con la misma clave recupera la respuesta cifrada únicamente durante su vigencia y desde el contexto original; fuera de ella devuelve recibo sin secreto. Emitir otro desafío revoca el anterior de ese contexto; no renueva intentos acumulados. No se reutilizan códigos de cuenta infantil ni invitaciones.
|
|
|
|
|
|
Código inválido, caducado, consumido o ajeno produce el mismo `DISPLAY_CHALLENGE_UNAVAILABLE`; no se revela qué salas o pantallas existen. Revisión de pantallas obsoleta produce `STALE_DISPLAY_REVISION`; modo incompatible, `DISPLAY_MODE_UNAVAILABLE`; cuota de una pantalla ocupada, `DISPLAY_LIMIT_REACHED`. Ninguno de estos rechazos devuelve el código ni la identidad de la pantalla ajena.
|
|
|
|
|
|
La vinculación toma el bloqueo de sala, verifica permiso actual, política de protección, `modeId` compatible, desafío vigente y `expectedDisplayRevision`. Incrementa `displayRevision`; inicio de partida y vinculación compiten bajo el mismo bloqueo para que `required-at-start` no observe una pantalla fantasma. La propuesta admite una pantalla común por sala y una personal por membresía humana, con cuotas técnicas globales; reemplazar una personal exige confirmación y revocación atómica de la anterior. El vínculo caduca al cerrar la sala o a las 8 horas, lo que ocurra antes, y se revoca si se pierde la autorización responsable en una sala protegida o la membresía propietaria de una TV personal. Renovarlo requiere un desafío nuevo. Revocar o caducar no adjudica el resultado del juego: se aplica `onLoss` del modo, subordinado a la política temporal durante una ronda sin resolver. El cambio de vista de una TV personal afecta solo a su jugador; neutralizar una comparación de reacción puede afectar a toda esa ronda.
|
|
|
|
|
|
Una sesión de pantalla solo puede negociar `game.*` v2 para `surface: "shared-display"` de la partida vinculada. No recibe `RoomView`, `room.sync`, `presence.update`, chat, avisos personales ni lista de miembros, y se rechazan `room.command`, `game.command` y consultas de recibos personales. El servidor deriva sala/partida del vínculo, revalida autorización en la entrega y cancela la suscripción al revocar. En espera, reconexión o error la pantalla borra la proyección anterior; no muestra identificadores de menores ni texto libre de participantes.
|
|
|
|
|
|
### Preparación TVBoard, QR de entrada y enlace de mando (propuesta v2)
|
|
|
|
|
|
Son dos finalidades distintas: `tvboard-entry` admite personas en una mesa presencial sin invitaciones; `controller-link` continúa el acceso de un jugador invitado y enlaza exclusivamente su TV y móvil. Las rutas y nombres siguientes son diseño pendiente de OpenAPI, esquemas, implementación y fixtures:
|
|
|
|
|
|
| Método y ruta propuestos | Solicitud / datos | Autoridad y efecto |
|
|
|
| --- | --- | --- |
|
|
|
| `POST /api/v2/tvboard/setups` | Juego/versiones públicos seleccionados y perfil `TVBoard` → preparación, QR y caducidad | Contexto TV con origen/CSRF/límites; no crea sala, anfitrión ni membresía. Solo juegos aptos para contexto público. |
|
|
|
| `POST /api/v2/tvboard/setups/resolve` | Token de preparación → selección pública apta | No consume la preparación ni entrega datos privados. La confirmación usa `POST /rooms` con `tvBoardSetupToken`. |
|
|
|
| `POST /api/v2/rooms/{roomId}/screen-entries` | `issuedAt`, `expectedEntryRevision` → acceso QR nuevo/rotado | Anfitrión de mesa `screen-qr` en espera con TV vinculada; token cifrado recuperable por contexto autorizado durante vigencia. No crea invitación. |
|
|
|
| `POST /api/v2/screen-entries/resolve` | Token de entrada QR → ficha de juego/mesa apta | Sin consumo ni lista de participantes; la entrada usa `POST /memberships` con `screenEntryToken` y permisos actuales. |
|
|
|
| `POST /api/v2/displays/controller-links` | Invitación introducida en Smart TV → desafío/QR personal y ficha apta | Contexto TV; no consume usos ni crea otra sala/plaza. Conserva contexto pendiente cifrado con caducidad corta. |
|
|
|
| `POST /api/v2/controller-links/resolve` | Desafío de enlace → flujo pendiente de juego y acceso | No entrega el token de invitación en claro ni acceso a la partida. Finalidad/contexto acotados. |
|
|
|
| `POST /api/v2/controller-links/{challengeId}/confirm` | `issuedAt`, token de desafío y confirmación desde móvil → recibo, membresía y vínculo de TV personal | Sesión humana elegible; revalida/consume invitación, crea o recupera membresía y vincula pantalla atómicamente. En miembro actual no vuelve a consumir invitación. En protegidas requiere autorización responsable. |
|
|
|
| `POST /api/v2/rooms/{roomId}/presentation-changes` | `issuedAt`, `targetProfile`, `expectedPresentationRevision` → recibo de solicitud y `operationId` | Miembro humano solicita su propio cambio compatible; el servidor deriva membresía y vínculo. Para TV personal → `Mobil`, aplica barrera y revoca su TV al completar. Conserva plaza/recibos. El perfil temporal puede diferir la aplicación hasta entre rondas. |
|
|
|
| `POST /api/v2/rooms/{roomId}/control-transfers` | `issuedAt`, `matchId`, `expectedControlGeneration`, `targetConnectionId`; si cambia perfil, `targetProfile` y `expectedPresentationRevision` → recibo de solicitud y `operationId` | Adquisición/transferencia de mando propio a conexión autenticada del mismo principal; valida renderer y punto seguro. No acepta actor ni membresía ajena. Generación inicial 0 sin mando; adquirir incrementa bajo barrera. |
|
|
|
| `GET /api/v2/rooms/{roomId}/operations/{operationId}` | Estado actual `pending\|applied\|rejected\|cancelled`, revisión de operación y generaciones resultantes cuando corresponda | Propietario de la solicitud; reautoriza datos. Si perdió acceso conserva solo resultado mínimo propio, sin snapshot privado. |
|
|
|
|
|
|
Preparar el QR no otorga derechos ni reserva plaza. En la creación presencial, confirmación autorizada del anfitrión, sala `waiting`, membresía, vínculo común y acceso QR se confirman juntos. El QR de preparación se sustituye por el de entrada; ambos tienen finalidades/generaciones distintas. La mesa es privada y no emite ni consume invitaciones. El QR de altas sirve durante espera para varias personas, sujeto a capacidad descontando plazas virtuales, permisos, caducidad y límites; reintentar desde una membresía actual no consume otra plaza. Inicio, cierre, transferencia de anfitrión o revocación/expiración de TV invalidan ese acceso, bajo el mismo bloqueo que las altas.
|
|
|
|
|
|
Los tokens QR usan aleatoriedad criptográfica de al menos 128 bits, finalidad y hash/MAC de validación, recuperación cifrada acotada y rutas con token en fragmento seguido de POST. El alias manual de vinculación tiene el tratamiento específico de la sección anterior; no se rebaja la entropía del QR. No se registran cuerpos secretos, ni se cargan terceros/analítica; respuestas `private, no-store` y `no-referrer`. Se propone caducidad máxima de 5 minutos para QR de altas y desafíos, con cuotas/valores definitivos por fijar. La rotación conserva la sala y revoca generaciones anteriores; consultar/escaneo no renueva vigencia. Errores de token no revelan si hubo caducidad, consumo, otra sala o un usuario.
|
|
|
|
|
|
Cambios de presentación y transferencias de mando tienen un recibo **inmutable de aceptación de solicitud**, independiente de su operación duradera, que puede permanecer `pending`. `applied` en ese recibo acredita que se registró la solicitud, no que ya cambió la pantalla o el mando; el recurso identifica la operación y la UI consulta su estado hasta terminar. Reintentar el mismo UUID devuelve el mismo recibo/operación. La operación revalida permisos, precondiciones, conexión destino y generación antes de aplicar; no altera recibos previos. Al cambiar cliente y perfil a la vez se usa una sola transferencia con ambos campos, sin dos solicitudes que compitan. Las solicitudes propias se serializan y un conflicto de generación termina rechazado; la pérdida de respuesta no repite adquisición. Su barrera drena/rechaza ingresos antiguos y entrega snapshot antes de permitir nuevas acciones, según [continuidad de mando](client-communication-profile.md#continuidad-del-mando-entre-conexiones).
|
|
|
|
|
|
La operación conserva `expiresAt` UTC y una política de espera acotada fijada al aceptarla; consultar o reintentar no amplía el plazo. Si desaparece la membresía, cierra la sala o vence la espera sin punto seguro, termina `cancelled` sin transferir; una precondición incompatible termina `rejected`. Un worker recuperado solo puede aplicar una operación pendiente, vigente y de la generación actual. Los límites y códigos de motivo se cierran en los esquemas v2; no se publica un flujo que pueda quedar pendiente indefinidamente.
|
|
|
|
|
|
El QR personal no puede utilizarse para altas `screen-qr`, ni el QR común para reclamar un mando personal. Confirmar una admisión nueva con la invitación revocada/agotada falla sin crear vínculo. Si el principal ya conserva una membresía autorizada, se valida ese acceso para vincular su propia TV y no se vuelve a consumir la invitación. Un timeout tras commit se resuelve consultando/reintentando el mismo comando, nunca admitiendo a otra persona o regenerando usos. Las tareas pendientes revalidan autorización. Tras confirmar, el móvil controla su plaza y la TV solo recibe la proyección pública; cerrar la suscripción visual no elimina la membresía ni reabre la invitación.
|
|
|
|
|
|
La distribución `game.*` v2 negocia perfil y contrato por suscripción: `Desktop`/`Mobil` reciben `player-full`, el mando `TVBoard` recibe `player-controller` y cada TV recibe `public-board`. El servidor deriva finalidad, alcance y destinatario del vínculo y la sesión; comandos y recibos pertenecen al principal autorizado, nunca al dispositivo elegido por el cliente. La [sección de mensajes](../game-engine/multi-device-profile.md#4-mensajes-de-partida-v2) especifica tuplas, barreras, outbox, reautorización y cambio de presentación. No se añade un broadcast de estado privado ni se reutiliza v1 como si entendiera estos campos.
|
|
|
|
|
|
El recorrido de invitación y TV personal exige `Mobil` y la opción «Jugar solo en el móvil». La solicitud de cambio es idempotente y recuperable; no consume usos de invitación, abandona la sala ni revoca TVs ajenas. Un cambio pendiente conserva la fuente temporal de la ronda actual. Su aplicación revalida membresía/versión y publica la nueva generación; el móvil negocia el contrato completo y descarta entregas del anterior. Reabrir otra pestaña para leer no se interpreta por sí solo como petición de desvincular TV.
|
|
|
|
|
|
El [perfil de sincronización v2](../game-engine/synchronization-profile.md) propone `time.probe`/`time.sample` y `time.ready`/`time.readiness` como control técnico. El estado temporal va en `game.snapshot.context.timing` y los eventos `round.*` en `game.snapshot.events`, ligados a época, generación, revisión y suscripción; no son sobres independientes. Los plazos y resultados se derivan en servidor. Respuestas privadas se registran con ingreso confiable duradero y se evalúan tras una recogida acotada; un timestamp cliente o el orden de commit no decide ganador. Los detalles y límites requieren esquemas/binding nuevos: no son mensajes admitidos hoy por v1.
|
|
|
|
|
|
La app Flutter futura es **solo de controles**: usa las mismas acciones, ofertas, snapshots `player-controller` y consultas de recibos que el mando web. Su runtime/renderer se declara en negociación, sin elegir actor o destino de TV; se rechazan `player-full` y `public-board` para Flutter. El paso a móvil completo abre `Mobil` web y coordina autenticación y transferencia de mando/presentación, conservando membresía y recibos. El [perfil de comunicación](client-communication-profile.md#5-acciones-e-información-en-ambos-sentidos) describe la secuencia completa y propone `POST /api/v2/realtime/tickets` como emisión técnica ligada a desafío WSS, sesión/vínculo y generación. Esa ruta no es una mutación de juego, no sigue un recibo de jugada ni existe hoy. Se implementa antes de ofrecer la app, junto a su perfil de acceso nativo; ninguno reemplaza las validaciones de admisión/QR.
|
|
|
|
|
|
### Chat, avisos y seguridad
|
|
|
|
|
|
| Método y ruta | Datos | Acceso |
|
|
|
| ------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
|
|
|
| `GET /api/v2/rooms/{roomId}/messages` | Página histórica, `historyEpoch`, cursor | Chat; se aplica corte por retirada y redacciones actuales. |
|
|
|
| `GET /api/v2/me/matches` | Historial/resultados proyectados | Propietario; no copia resultado privado del motor. |
|
|
|
| `GET /api/v2/me/notifications` | Página, `inboxRevision`, no leídos | Propietario; sin texto de chat ni datos ocultos. |
|
|
|
| `POST /api/v2/me/notifications/{notificationId}/read` | Lectura monotónica → recibo | Propietario; incrementa bandeja solo si cambia. |
|
|
|
| `POST /api/v2/me/blocks`, `POST /api/v2/me/blocks/{blockId}/revoke` | Destinatario autorizado / desbloqueo | Seguridad, sesión; no revela quién bloqueó al solicitante. |
|
|
|
| `POST /api/v2/safety/reports` | Recurso/persona, categoría, nota opcional → referencia | Seguridad; sin requisito de chat/SMS; contexto de reporte para no autenticados con límites. |
|
|
|
| `GET /api/v2/me/reports/{reportId}` | Estado propio y respuesta mínima | Reportante, con verificación de titularidad. |
|
|
|
| `GET /api/v2/commands/{commandId}` | Recibo propio `found\|unknown` | Sesión; puede consultar tras salir o perder permiso de chat, sin recibir contenido. |
|
|
|
|
|
|
La consola de moderación y los derechos de acceso a evidencia son una interfaz administrativa separada, con roles y objetivos definidos en políticas y operaciones en la sección 9. Debe implementarse antes de comunicación pública. Los reportes sin cuenta no se exponen por ID adivinable: requieren el contexto de reporte emitido, sin revelar datos a terceros.
|
|
|
|
|
|
### Paginación y caché
|
|
|
|
|
|
Listas por cursor opaco ligado a principal, filtros, orden y versión de política; por defecto 20 y máximo 50 elementos. Orden total por fecha de creación + UUID; navegación histórica de chat por `createdSequence`. No offsets ilimitados. Cursor vencido, contexto cambiado o historial redactado da `CURSOR_EXPIRED` y obliga a reiniciar la lista. Un cursor no es autorización.
|
|
|
|
|
|
Las listas móviles como salas públicas no prometen un snapshot transaccional entre páginas: se pueden actualizar entre lecturas; cliente deduplica IDs y refresca la primera página. Chat usa el contrato específico de la sección 6. Las respuestas privadas llevan `private, no-store`; catálogo público puede usar ETag según idioma/proyección segura y assets públicos inmutables por digest. La concurrencia se controla con revisiones de dominio explícitas, no con un ETag que mezcle proyecciones de usuarios.
|
|
|
|
|
|
## 4. Conexión y sobres Socket.IO
|
|
|
|
|
|
Esta sección conserva el binding v1 previsto. El [binding común futuro para web y Flutter](client-communication-profile.md#2-transporte-propuesto) separa transporte WSS de mensajes de aplicación, con autenticación inicial y renderer negociado; sustituirlo requiere contrato ejecutable y pruebas, no cambiar el nombre de una librería cliente.
|
|
|
|
|
|
Eventos en namespace principal: `platform:message` y `game:message`. El segundo mantiene `protocolVersion: 1` y su propio sync en el contrato vigente. La pantalla compartida requiere negociación futura de `game.*` v2 y un contexto de dispositivo restringido; no se conecta al contrato v1 fingiendo ser observador. Los acuses de plataforma son mensajes de aplicación; el callback de Socket.IO no reemplaza un recibo persistido.
|
|
|
|
|
|
Antes de suscribirse se envía un mensaje de negociación independiente:
|
|
|
|
|
|
```json
|
|
|
{
|
|
|
"type": "platform.hello",
|
|
|
"requestId": "bdf2ea3d-1473-4c75-a0f6-b2e56807bc99",
|
|
|
"supportedVersions": [1]
|
|
|
}
|
|
|
```
|
|
|
|
|
|
Respuesta `platform.welcome`: `requestId`, `selectedVersion: 1`, `connectionId` UUID, `serverTime`, límites y `safetyRevision` propia. Sin versión compatible: `platform.negotiation-error` con código y versiones admitidas, sin datos privados, y cierre. No se interpreta `room.sync` para negociar chat o avisos.
|
|
|
|
|
|
Para una pantalla vinculada, el binding futuro negocia `platform.hello` con `supportedVersions: [2]` y el contexto de dispositivo. Su `platform.welcome` v2 identifica `connectionKind: "shared-display"`, `connectionId`, `serverTime`, alcance/generación del vínculo derivados y límites; no incluye `safetyRevision` de una cuenta inexistente. Solo admite sincronización/cancelación de su partida por `game.*` v2 y control técnico de conexión/reloj negociado (`time.*` propuesto). Cualquier `platform.*` de sala, chat, presencia, avisos o comandos se rechaza sin datos. La sesión queda ligada al `roomId` autorizado en servidor, no a un identificador pedido por el cliente. Una única marca `serverTime` no sustituye los intercambios de sincronización y medición.
|
|
|
|
|
|
Después todos los sobres llevan `platformProtocolVersion: 1` y `type`. Peticiones de lectura llevan `requestId`; mutaciones, `commandId` e `issuedAt`. Flujos llevan `subscriptionId` y recurso. Identificadores de suscripción son propios de conexión/principal y se invalidan al desconectar, cambiar identidad o revocar acceso.
|
|
|
|
|
|
| Mensaje | Dirección | Campos específicos |
|
|
|
| ------------------------- | --------- | --------------------------------------------------------------------------------------------------------------- |
|
|
|
| `room.sync` | C → S | `requestId, roomId`. Sustituye suscripción anterior de esa sala/conexión. |
|
|
|
| `room.snapshot` | S → C | `roomId, subscriptionId, revision, delivery, observedAt, room`. |
|
|
|
| `room.command` | C → S | `roomId, commandId, issuedAt, expectedRevision, expectedMatch, action`. |
|
|
|
| `room.ack` | S → C | `roomId, commandId, duplicate, receipt`. |
|
|
|
| `room.cursor` | S → C | `roomId, subscriptionId, revision`. |
|
|
|
| `room.unsubscribe` | C → S | `requestId, roomId, subscriptionId`. Solo suscripción propia. |
|
|
|
| `platform.unsubscribed` | S → C | `requestId, subscriptionId`. Baja idempotente, sin alterar membresía. |
|
|
|
| `platform.command-status` | C → S | `requestId, commandId`. Sirve también para chat y mutaciones HTTP. |
|
|
|
| `platform.command-result` | S → C | `requestId, commandId, status: found\|unknown`, `receipt` si encontrado. |
|
|
|
| `chat.sync` | C → S | `requestId, roomId`. Solo miembros actuales elegibles para chat. |
|
|
|
| `chat.snapshot` | S → C | `roomId, subscriptionId, requestId, sequence, historyEpoch, messages, historyCursor`. Cola reciente autorizada. |
|
|
|
| `chat.send` | C → S | `roomId, commandId, issuedAt, text`. Autor derivado del servidor. |
|
|
|
| `chat.ack` | S → C | `roomId, commandId, duplicate, receipt`. |
|
|
|
| `chat.event` | S → C | `roomId, subscriptionId, eventId, fromSequence, sequence, historyEpoch, event`. |
|
|
|
| `chat.cursor` | S → C | `roomId, subscriptionId, sequence, historyEpoch`. |
|
|
|
| `chat.unsubscribe` | C → S | `requestId, roomId, subscriptionId`; respuesta `platform.unsubscribed`. |
|
|
|
| `presence.update` | S → C | `roomId, subscriptionId` de sala, `presenceSequence, observedAt, expiresAt, members`. |
|
|
|
| `lobby.changed` | S → C | Señal sin IDs ni actividad de salas privadas; solo cambios del listado público. |
|
|
|
| `notification.changed` | S → C | `inboxRevision` propia; se consulta por HTTP. |
|
|
|
| `platform.access-changed` | S → C | `eventId`, `safetyRevision` propia, recursos/suscripciones invalidados y capacidades propias mínimas. |
|
|
|
| `platform.error` | S → C | `code, messageKey, recovery, requestId? , commandId?, roomId?, subscriptionId?, retryAfterMs?`. |
|
|
|
|
|
|
El servidor no comparte suscripciones personales entre usuarios. Las invalidaciones de seguridad no dependen de recibir otro movimiento: la revocación corta directamente la entrega en servidor. Si el cliente pierde el aviso, el siguiente intento sigue rechazado.
|
|
|
|
|
|
`platform.access-changed` se deduplica por `eventId`, no solo por `safetyRevision`: perder membresía puede invalidar una suscripción sin cambiar la edad/teléfono de la cuenta. Toda invalidación de la suscripción actual se aplica aunque esa revisión de protección siga igual.
|
|
|
|
|
|
## 5. Sala, acciones y sincronización
|
|
|
|
|
|
`RoomView` contiene nombre/visibilidad aptos para destinatario, estado, capacidad, anfitrión, miembros visibles, configuración pública, paquete fijado, política/plazos, capacidades propias y `matchRef` nulo o con `matchId`, tupla del motor y `minimumRevision`. Miembros: `membershipId`, alias/avatar proyectados, estado, listo, plaza provisional y `seatId` nulo hasta inicio. No incluye token de invitación, historial de chat, teléfono/edad ni estado interno del motor. Crear fija `admissionMode: individual|adult|protected` y `communicationMode: off|adult-verified`: solo `adult` admite el segundo; `individual` tiene capacidad 1 y `protected` siempre es privado. En el perfil futuro incorpora `modeId`, `entryMethod` y perfiles disponibles; seleccionar presentación no cambia esos permisos. Los valores se validan contra políticas y paquete, no conceden permisos por sí mismos ni revelan la edad de un participante.
|
|
|
|
|
|
Las capacidades son indicaciones evaluadas en `observedAt`, pueden caducar por tiempo/permiso y siempre se revalidan. No todo cambio de permiso incrementa `roomRevision`: una invalidación de acceso exige nuevo sync aunque esa revisión sea igual.
|
|
|
|
|
|
```json
|
|
|
{
|
|
|
"platformProtocolVersion": 1,
|
|
|
"type": "room.command",
|
|
|
"roomId": "1093543c-d383-437c-a427-251d39d63e42",
|
|
|
"commandId": "04de01b1-771f-4775-9db0-3137399c464b",
|
|
|
"issuedAt": "2026-10-05T16:00:00Z",
|
|
|
"expectedRevision": 5,
|
|
|
"expectedMatch": null,
|
|
|
"action": { "type": "set-ready", "ready": true }
|
|
|
}
|
|
|
```
|
|
|
|
|
|
| Acción | Payload y permiso |
|
|
|
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| `set-ready` | `ready: boolean`; propio miembro en espera. Inicio automático según política. |
|
|
|
| `configure` | Capacidad/configuración permitida; anfitrión en espera. Invalida todos los listo; juego/digest no cambian. |
|
|
|
| `choose-color` | `colorId` ofrecido por la política de preparación del juego; propio miembro en espera. El adaptador valida la configuración de inicio; no infiere posibilidades por nombre de juego. |
|
|
|
| `set-invite-message` | Texto limitado y aprobado por política de contenido; anfitrión en espera. No se muestra libremente a menores. |
|
|
|
| `transfer-host` | `targetMembershipId` activo; anfitrión en espera, con efectos P2. |
|
|
|
| `cancel` | Anfitrión en espera; cancelación explícita. |
|
|
|
| `leave` | Miembro; en espera sale, en partida retira mediante motor. El anfitrión responsable de una partida protegida activa provoca cancelación de seguridad sin resultado competitivo según P7. |
|
|
|
| `close-inactive` | Participante activo elegible según política y flujo del motor; cancela sin resultado competitivo. |
|
|
|
|
|
|
En espera `expectedMatch = null`. En retirada/cierre activo es obligatorio `{ matchId, revision }` exacto. Revisión de sala correcta con partida obsoleta da `STALE_MATCH_REVISION`. El servicio bloquea sala y partida y confirma el enlace al motor y su recibo en una sola transacción. El controlador no llama al protocolo de red `game.command` contra sí mismo ni duplica la retirada en dos registros independientes.
|
|
|
|
|
|
Barrera de sync: instalar recepción y buffer acotado, leer revisión R/proyección confirmada, emitir snapshot con `delivery: { kind: "sync", requestId }`, descartar buffer ≤R y publicar R+1… con `delivery: { kind: "live", fromRevision }`. Duplicados se descartan; huecos o overflow exigen nuevo sync. Un snapshot completo leído en R+k no se etiqueta como R+1.
|
|
|
|
|
|
La respuesta de salida puede preceder a la revocación de suscripción o viceversa; el recibo se puede consultar por identidad. Un cliente conserva cada flujo separado. Si llega final de sala con `minimumRevision = 13` y juego sigue en 12, solicita `game.sync` y espera la vista autorizada de al menos 13. Una instantánea de juego más reciente que sala tampoco implica corrupción.
|
|
|
|
|
|
`minimumRevision` registra la última revisión de partida coordinada con un cambio de sala, empezando en 0; no promete ser la última jugada existente ni obliga a aumentar la revisión de sala en cada jugada.
|
|
|
|
|
|
## 6. Chat, redacción y recuperación
|
|
|
|
|
|
`ChatMessage`: `messageId, authorMembershipId, authorDisplay, createdAt, createdSequence, status: visible|redacted, text`. En lápida `text = null`; no hay copia del original en payload público. Normalización NFC, texto no vacío de hasta 500 puntos de código y 2 KiB UTF-8, sin HTML ni adjuntos. Longitudes se verifican tras normalizar y la huella idempotente usa el texto validado. Cuerpos no conformes se rechazan, no se truncan.
|
|
|
|
|
|
`sequence` ordena **eventos**, no solo mensajes. `message.created` crea un mensaje; `message.redacted` apunta a su ID, añade secuencia nueva y aumenta `historyEpoch`. No se reemite texto anterior a la redacción en un replay. Un bloqueo personal genera invalidación de acceso y nuevos marcadores sin contenido donde proceda; no cambia el mensaje global ni revela el autor filtrado.
|
|
|
|
|
|
Cada `chat.sync` empieza de nuevo con los últimos 50 mensajes como máximo, limitados además por bytes. Se instala primero la suscripción, se lee snapshot R y se liberan después eventos >R. La navegación de mensajes anteriores se hace por HTTP con `historyCursor`; no retiene una conexión entera mientras pagina todo el historial. La reconexión muestra esa cola reciente, sin repetir sonidos; no promete reproducción exhaustiva de todos los eventos desconectados.
|
|
|
|
|
|
Un evento vivo tiene `sequence = fromSequence + 1`. Hueco, cursor adelantado o epoch superior provoca nuevo sync y descarta el historial cacheado. Una página HTTP incluye `historyEpoch`; el cliente no mezcla una página de epoch anterior al ya observado. En servidor se revalida permiso/epoch antes de publicar una página o outbox. Así una respuesta retrasada no restaura texto que ya fue moderado.
|
|
|
|
|
|
La membresía almacena `chatJoinedAfterSequence` al entrar y, al retirarse, `chatLeftAtSequence`. Solo mensajes con `createdSequence` posterior al primero y no superior al segundo son legibles; el segundo es nulo mientras continúa. Se sincronizan únicamente eventos que puedan proyectarse dentro de ese intervalo, sin revelar autores/texto fuera de él. El antiguo miembro retirado consulta ese historial por HTTP si conserva elegibilidad adulto/SMS y ve redacciones posteriores; no tiene suscripción viva. Al perder elegibilidad tampoco conserva historial. La paginación aplica políticas actuales, no las de cuando se creó el cursor.
|
|
|
|
|
|
Las salas protegidas/mixtas rechazan `chat.sync`, envío e historial incluso para adultos con SMS. El mensaje se purga a los 30 días desde creación; el borrado aumenta `historyEpoch` e invalida cursores/cachés igual que una redacción. No se conserva su texto en outbox/replay fuera del período; el caso de moderación, si existe, tiene almacenamiento y permisos distintos. Un mensaje purgado puede conservar un marcador mínimo mientras el flujo siga existiendo, sin texto ni datos del autor.
|
|
|
|
|
|
## 7. Límites, avisos y recuperación
|
|
|
|
|
|
Valores iniciales para conformidad, anunciados en `bootstrap/welcome`:
|
|
|
|
|
|
| Límite | Valor propuesto |
|
|
|
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
|
| JSON de entrada HTTP / plataforma socket | 64 KiB / 16 KiB; profundidad 32. |
|
|
|
| Salida de plataforma / página | 256 KiB; reducir elementos y devolver cursor si se alcanza. |
|
|
|
| Sobre de juego | 64 KiB según protocolo del motor; se valida por separado. |
|
|
|
| Suscripciones por conexión | Hasta 5 salas y 5 chats; total máximo 10. |
|
|
|
| Buffer por suscripción al sincronizar | 256 eventos o 1 MiB, lo que ocurra antes. |
|
|
|
| Cursores sala/chat | Cada 15 s; detectar incluso la última entrega perdida. |
|
|
|
| Presencia | Latido observado cada 15 s, TTL 45 s, secuencia por suscripción. |
|
|
|
| Consulta de avisos mientras hay foco | Al conectar, recuperar foco y cada 30 s; `inboxRevision` cambia también al leer. |
|
|
|
| Chat | 15 envíos/10 s por principal y 100/min por sala como valores iniciales, además de límites globales. |
|
|
|
| Avatar | Hasta 1 MiB de entrada y 1024×1024 píxeles decodificados; PNG/JPEG/WebP estáticos, nueva codificación antes de publicar. |
|
|
|
|
|
|
Configurar el límite agregado de Socket.IO no sustituye los límites específicos de mensaje. El servidor rechaza entradas demasiado grandes antes de ejecutar y corta clientes lentos sin crecimiento ilimitado. Una presencia caducada pasa a desconocida, no provoca derrota. Lecturas periódicas se suspenden en segundo plano; al volver se resincroniza.
|
|
|
|
|
|
Al reconectar: obtener sesión/capacidades actuales, negociar plataforma, sincronizar salas permitidas, solo chats elegibles y partidas asociadas; consultar recibos inciertos y reintentar el mismo sobre. Las proyecciones privadas se aíslan por identidad y se eliminan al cambiarla. No se restaura acceso por el `socket.id` antiguo ni por la recuperación automática de Socket.IO.
|
|
|
|
|
|
## 8. Errores y compatibilidad
|
|
|
|
|
|
El [modelo de errores compartido](shared-data-models.md#2-núcleo-común-del-error) define `ErrorData` idéntico para Go, TypeScript/Svelte y Dart/Flutter. HTTP lo envuelve en [Problem Details, RFC 9457](https://www.rfc-editor.org/rfc/rfc9457.html): `type`, `title`, `status`, `correlation` y `error`. Los campos `code`, mensaje, resultado y recuperación están **solo** en `error`; no se duplican planos con valores discrepantes. La URN se deriva del código y debe documentarse antes de publicar. No hay mensajes de excepción, payloads privados ni identificadores de cuentas ajenas en errores.
|
|
|
|
|
|
| HTTP | Códigos y uso | Recuperación |
|
|
|
| --------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
|
| 400 / 413 | `INVALID_REQUEST`, `PAYLOAD_TOO_LARGE` | Corregir intención; no reintento automático. |
|
|
|
| 401 | `SESSION_REQUIRED`, `SESSION_EXPIRED` | Reautenticar y limpiar proyecciones. |
|
|
|
| 403 | `TERMS_REQUIRED`, `CHAT_NOT_ELIGIBLE`, `ACCOUNT_RESTRICTED`, `FORBIDDEN` | Mostrar requisitos propios o retirar acceso. |
|
|
|
| 404 | `RESOURCE_NOT_FOUND` | También para recursos privados cuya existencia no se autoriza. |
|
|
|
| 409 | `STALE_ROOM_REVISION`, `STALE_MATCH_REVISION`, `STALE_RESOURCE_REVISION`, `ROOM_FULL`, `ROOM_NOT_WAITING`, `PACKAGE_NOT_CREATABLE` | Sincronizar y revisión humana de una nueva intención. |
|
|
|
| 409 | `COMMAND_ID_REUSED`, `COMMAND_EXPIRED`, `CURSOR_EXPIRED`, `TERMS_VERSION_CHANGED` | No cambiar UUID en automático; reiniciar consulta/flujo pertinente. |
|
|
|
| 404 / 409 | `INVITATION_UNAVAILABLE` para resolver token; `INVITATION_CONFLICT` para propietario con revisión obsoleta | Mensaje genérico al visitante; refrescar al propietario. |
|
|
|
| 403 / 409 | `DISPLAY_MODE_UNAVAILABLE`, `DISPLAY_LIMIT_REACHED`, `STALE_DISPLAY_REVISION` | Elegir modo compatible o refrescar listado; sin datos ajenos. |
|
|
|
| 409 | `STALE_CONTROL_GENERATION`, `STALE_PRESENTATION_REVISION` | Recuperar recibo/operación propia y snapshot; una nueva adquisición requiere intención explícita y generación vigente. |
|
|
|
| 404 / 409 | `DISPLAY_CHALLENGE_UNAVAILABLE` | Crear otro desafío en la pantalla; no explica qué condición falló. |
|
|
|
| 429 / 503 | `RATE_LIMITED`, `TEMPORARY_UNAVAILABLE` | `Retry-After` HTTP / `retryAfterMs` socket; mismo comando si sigue admisible. |
|
|
|
|
|
|
La falta de revisión obligatoria es `400 INVALID_REQUEST` en este diseño, porque las precondiciones son campos del dominio. No se usa **428 para aceptar condiciones**: RFC 6585 lo define para exigir una petición condicional ([sección 3](https://www.rfc-editor.org/rfc/rfc6585.html#section-3)). Si en el futuro se añade `If-Match`, deben definirse aparte 428 por ausencia y 412 por condición no satisfecha.
|
|
|
|
|
|
El binding v2 usa `ErrorMessagePayload { correlation, error }` en `platform.error`/`game.error`, sin fingir estado HTTP. La tabla de Socket.IO v1 mantiene su forma antigua hasta implementar/versionar el puente. Rechazos definitivos de sala/chat llevan el mismo `error` en recibo; fallos previos o transitorios usan mensaje de error. Una operación incierta usa `error.outcome: unknown` y `recovery.action: resolve-pending`, sin afirmar rechazo ni generar otro UUID. `CHAT_NOT_ELIGIBLE` no detalla estados de otros usuarios. Los errores de negociación usan el mismo núcleo cuando el binding admite el modelo; si no hay versión común, solo se entrega diagnóstico público mínimo y se cierra.
|
|
|
|
|
|
```json
|
|
|
{
|
|
|
"type": "urn:juegoland:problem:room-full",
|
|
|
"title": "La sala está completa",
|
|
|
"status": 409,
|
|
|
"correlation": {
|
|
|
"requestId": "f8aa2fa1-aec8-41c8-a27b-644e999df03a",
|
|
|
"commandId": "30c474dd-030a-4e0e-883f-c8a6d636c8c9"
|
|
|
},
|
|
|
"error": {
|
|
|
"modelVersion": 1,
|
|
|
"origin": "server",
|
|
|
"code": "ROOM_FULL",
|
|
|
"category": "conflict",
|
|
|
"outcome": "rejected",
|
|
|
"message": { "key": "errors.roomFull", "args": {} },
|
|
|
"recovery": { "action": "change-selection" },
|
|
|
"violations": []
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
Cambiar significado, tipo obligatorio o regla de autorización requiere revisar compatibilidad; un campo opcional de respuesta no autoriza campos nuevos de solicitud. Versiones de cliente incompatibles deben actualizarse antes de comunicar o ver un juego, incluida la API antigua. La fase siguiente produce OpenAPI, esquemas discriminados y pruebas desde una fuente común, con ejemplos de éxito, errores y carreras; este documento no declara que esos artefactos ya existan.
|
|
|
|
|
|
## 9. Flujos derivados de las políticas de producto
|
|
|
|
|
|
Operaciones de diseño que deben incorporarse a OpenAPI y pruebas. Los métodos/rutas siguientes sustituyen la ambigüedad del primer borrador; no se afirma que existan endpoints funcionando. Mutaciones de dominio mantienen recibos comunes; autenticación y tokens de credencial usan intentos específicos, vinculados al contexto anónimo/sesión y con respuesta secreta recuperable acotada según P6. No se incluye contraseña o token en una huella persistida de dominio.
|
|
|
|
|
|
### Cuenta y recuperación
|
|
|
|
|
|
| Operación | Contrato mínimo |
|
|
|
| ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| `POST /api/v2/auth/register` | `authAttemptId`, correo, contraseña, versión de condiciones; crea cuenta pendiente o convierte invitado propio sin cambiar `userId`. Declarar mayoría no la acredita. El correo de respuesta no enumera cuentas existentes. |
|
|
|
| `POST /api/v2/auth/email-verifications` | Solicitar envío/reenvío para contexto propio; 202 genérico, cuota y tarea deduplicada. |
|
|
|
| `POST /api/v2/auth/email-verifications/complete` | Token del enlace + contexto; consumir atómicamente, verificar correo sin elevar edad/tutela/SMS. |
|
|
|
| `POST /api/v2/auth/login` | `authAttemptId`, correo, contraseña, `rememberMe`; servidor decide si admite sesión larga. Credencial inválida con respuesta uniforme, sin distinguir correo inexistente. |
|
|
|
| `POST /api/v2/auth/reauthenticate` | Prueba de credencial y segundo factor cuando corresponda; resultado ligado a sesión/finalidad, válido 5 min; no recibe `reauthenticated: true` del cliente. |
|
|
|
| `POST /api/v2/auth/recovery-requests` | Correo y contexto; 202 genérico, sin iniciar sesión ni revelar estado. |
|
|
|
| `POST /api/v2/auth/recoveries/complete` | Intento, token y nueva contraseña; consumo de un uso + credencial + revocación de sesiones/tokens + suspensión de privilegios sensibles en una transacción. Exige después login. |
|
|
|
| `POST /api/v2/me/password` | Reautenticación, nueva contraseña; revocar otras sesiones y rotar la propia de forma atómica. No confundir cambio autenticado con recuperación por canal perdido. |
|
|
|
| `POST /api/v2/me/email-changes`, `POST /api/v2/me/email-changes/{changeId}/complete` | Reautenticación, nuevo correo / token; unicidad y verificación antes de cambiar, aviso al anterior, sin transferir cuenta por correo escrito. |
|
|
|
| `GET /api/v2/me/exports/{exportId}` | Estado propio; la descarga se emite solo tras comprobación vigente de titularidad, un uso, 24 h y sin URL/token en logs. |
|
|
|
| `GET /api/v2/me/deletion-requests/{requestId}` | Estado propio y excepciones pertinentes; tras revocación, acceso con credencial de seguimiento específica, sin reactivar sesión social. |
|
|
|
|
|
|
El contrato infantil de creación de credencial y emparejamiento requiere un esquema aparte; no se reutiliza el PIN actual. El desafío de emparejamiento se emite por responsable con vínculo activo, propósito/principal/dispositivo específicos, caduca en 5 min, admite 5 intentos y se consume una vez. Ningún dato de tutor o menor ajeno se revela antes de autorización. MFA administrativo es requisito de activación: endpoints WebAuthn/challenges, recuperación y validación de origen/RP deben especificarse antes de abrir la consola, no simularse con SMS.
|
|
|
|
|
|
### Tutela, contactos y capacidades
|
|
|
|
|
|
| Operación | Contrato mínimo |
|
|
|
| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| `POST /api/v2/me/supervision-requests` | Responsable adulto acreditado y reautenticado; referencia de proceso de evidencia y alcance. No enlaza un menor existente solo por correo/nick. |
|
|
|
| `GET /api/v2/me/supervisions` | Vínculos propios y estado mínimo; sin consulta arbitraria por `childUserId`. |
|
|
|
| `POST /api/v2/me/supervisions/{supervisionId}/children` | Vínculo de alta validado; crear cuenta tutelada, preset y políticas. La evidencia, edad exacta y credenciales no se publican en perfil. |
|
|
|
| `POST /api/v2/me/supervisions/{supervisionId}/revoke` | Reautenticación, revisión del vínculo, motivo privado acotado; incrementar protección y revocar contactos/permisos dependientes. |
|
|
|
| `POST /api/v2/me/supervisions/{supervisionId}/contact-authorizations` | Referencia de principal autorizado mediante intercambio privado, alcance y caducidad; no directorio público de menores. Para dos menores exige aprobación de ambos responsables. |
|
|
|
| `POST /api/v2/me/contact-authorizations/{authorizationId}/revoke` | Titular responsable, revisión; corta nuevas admisiones y aplica cancelación de seguridad si comparten partida protegida activa. |
|
|
|
| `POST /api/v2/me/supervision-disputes`, `POST /api/v2/me/supervision-release-requests` | Ayuda/corrección o emancipación; verificación adecuada y revisión, sin transferencia automática de control ni chat. |
|
|
|
|
|
|
Evidencia y callbacks de tutela/edad son interfaces internas del adaptador elegido; no aceptan afirmaciones de estado del navegador. La revocación produce `platform.access-changed`, invalida suscripciones y vuelve a proyectar vistas propias; a los demás solo indisponibilidad genérica. La revisión del vínculo no sustituye `safetyRevision` ni viceversa. En una sala protegida la cancelación por seguridad usa el núcleo con su revisión actual, no una revisión aportada por el responsable sobre una partida a la que quizá no tiene acceso.
|
|
|
|
|
|
### Moderación y publicación interna
|
|
|
|
|
|
| Operación | Contrato mínimo |
|
|
|
| --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| `GET /api/v2/me/restrictions` | Motivo notificable, alcance/vigencia y posibilidad de recurso; sin identidad del reportante ni reglas internas de detección. |
|
|
|
| `POST /api/v2/me/appeals`, `GET /api/v2/me/appeals/{appealId}` | Restricción/caso propio, nota acotada, estado y decisión; identidad verificada aunque esté suspendida, sin exigir chat/SMS. |
|
|
|
| `GET /internal/v2/moderation/cases`, `GET /internal/v2/moderation/cases/{caseId}` | MFA, rol, asignación y acceso auditado; paginación acotada, sin exportación masiva de evidencia por defecto. |
|
|
|
| `POST /internal/v2/moderation/cases/{caseId}/decisions` | `expectedRevision`, acción, motivo, alcance y duración; recibo idempotente + restricción + auditoría + invalidaciones confirmados juntos. |
|
|
|
| `POST /internal/v2/moderation/cases/{caseId}/reviews` | Revisión por otra persona, precondición de caso y resolución auditable; no sobreescribe la decisión previa. |
|
|
|
|
|
|
Las interfaces internas no se hacen públicas por compartir un binario Go: ruta/red/roles separados y ningún acceso con cookie de jugador. Publicación de juegos se realiza solo por pipeline/consola interna del equipo con permiso de publicación; no existe `POST /api/v2/games` para usuarios. El identificador de build/artefacto debe corresponder a paquete validado y reglas disponibles, no a una URL ejecutable elegida por el solicitante.
|