{"_id":"@closerclick/closer-click-lobby","_rev":"5-734792aa7ba7e917651980a23c20b69a","name":"@closerclick/closer-click-lobby","dist-tags":{"latest":"0.1.5"},"versions":{"0.1.0":{"name":"@closerclick/closer-click-lobby","version":"0.1.0","keywords":["lobby","matchmaking","game","turn-based","p2p","webrtc","closer-click","web-of-trust"],"author":{"name":"seyacat"},"license":"MIT","_id":"@closerclick/closer-click-lobby@0.1.0","maintainers":[{"name":"mycloserclick","email":"admin@closer.click"}],"homepage":"https://github.com/closerclick/closer-click-lobby#readme","bugs":{"url":"https://github.com/closerclick/closer-click-lobby/issues"},"dist":{"shasum":"dea7ed7a91feb1246f1b03cac45938bffe504928","tarball":"https://registry.npmjs.org/@closerclick/closer-click-lobby/-/closer-click-lobby-0.1.0.tgz","fileCount":12,"integrity":"sha512-LE7K0BZIz9g1EFDP5EBVxACMvpn2d7e1RasL1Jc7twVSurn3pi75KBxxtfEujUApkWx4AMoZUk7KxmxRUBZkKA==","signatures":[{"sig":"MEQCIFYXHCY58zDl/j/MrW6+wV8D2uOP3C7ykFyR136gdNstAiAifk+gCC92fvrar6FE0Nt7hXerdy0XCj5nlylak3ec6Q==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":91579},"main":"src/index.js","type":"module","types":"src/index.d.ts","module":"src/index.js","exports":{".":{"types":"./src/index.d.ts","import":"./src/index.js"}},"scripts":{"test":"node --test"},"_npmUser":{"name":"mycloserclick","email":"admin@closer.click"},"repository":{"url":"git+https://github.com/closerclick/closer-click-lobby.git","type":"git"},"_npmVersion":"11.12.1","description":"Lobby + matchmaking reciclable para juegos del ecosistema CloserClick: salas, asientos, espectadores, motor de turnos autoritativo, reputación y contactos sobre el proxy/identity/reputation compartidos","directories":{},"_nodeVersion":"25.9.0","_hasShrinkwrap":false,"peerDependencies":{"@closerclick/closer-click-identity":">=0.5.0","@closerclick/closer-click-reputation":">=0.3.0","@closerclick/closer-click-proxy-client":">=0.1.0"},"peerDependenciesMeta":{"@closerclick/closer-click-reputation":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/closer-click-lobby_0.1.0_1780514653320_0.3780238083500409","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@closerclick/closer-click-lobby","version":"0.1.2","keywords":["lobby","matchmaking","game","turn-based","p2p","webrtc","closer-click","web-of-trust"],"author":{"name":"seyacat"},"license":"MIT","_id":"@closerclick/closer-click-lobby@0.1.2","maintainers":[{"name":"mycloserclick","email":"admin@closer.click"}],"homepage":"https://github.com/closerclick/closer-click-lobby#readme","bugs":{"url":"https://github.com/closerclick/closer-click-lobby/issues"},"dist":{"shasum":"cf92cf6a27e99b658a3a1f002ab2d4c0ba8e3edb","tarball":"https://registry.npmjs.org/@closerclick/closer-click-lobby/-/closer-click-lobby-0.1.2.tgz","fileCount":12,"integrity":"sha512-07ZQT3YqqJnhQVUGHZOpO9GAKoLaJsg6/ITYaNUOiaJprRa3+Mkk1LSJ8mpbgweibRyLh0+TCgmxsEkm4HNUig==","signatures":[{"sig":"MEQCIF857giGbC1VsObNbrGhxzyWiEdPXDODgbDWb5mtxbdQAiB8ym7dc875tlRE1ryHWTy+vWOEOKvseGUy9GUUauznGg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":93796},"main":"src/index.js","type":"module","types":"src/index.d.ts","module":"src/index.js","exports":{".":{"types":"./src/index.d.ts","import":"./src/index.js"}},"scripts":{"test":"node --test"},"_npmUser":{"name":"mycloserclick","email":"admin@closer.click"},"repository":{"url":"git+https://github.com/closerclick/closer-click-lobby.git","type":"git"},"_npmVersion":"11.12.1","description":"Lobby + matchmaking reciclable para juegos del ecosistema CloserClick: salas, asientos, espectadores, motor de turnos autoritativo, reputación y contactos sobre el proxy/identity/reputation compartidos","directories":{},"_nodeVersion":"25.9.0","_hasShrinkwrap":false,"peerDependencies":{"@closerclick/closer-click-identity":">=0.5.0","@closerclick/closer-click-reputation":">=0.3.0","@closerclick/closer-click-proxy-client":">=0.1.0"},"peerDependenciesMeta":{"@closerclick/closer-click-reputation":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/closer-click-lobby_0.1.2_1780527278932_0.6290307266596535","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@closerclick/closer-click-lobby","version":"0.1.3","keywords":["lobby","matchmaking","game","turn-based","p2p","webrtc","closer-click","web-of-trust"],"author":{"name":"seyacat"},"license":"MIT","_id":"@closerclick/closer-click-lobby@0.1.3","maintainers":[{"name":"mycloserclick","email":"admin@closer.click"}],"homepage":"https://github.com/closerclick/closer-click-lobby#readme","bugs":{"url":"https://github.com/closerclick/closer-click-lobby/issues"},"dist":{"shasum":"8c0f775849cb4df7d0421ba2f9783106db9ea178","tarball":"https://registry.npmjs.org/@closerclick/closer-click-lobby/-/closer-click-lobby-0.1.3.tgz","fileCount":12,"integrity":"sha512-pj5tXrI5NopVdyOnJxRg+8fVRAv9Of8cFPpRI2gmFDxIvLztMXR1W4VkirYTHf4b9AEPcrqQfaC+CdYeEFNuyQ==","signatures":[{"sig":"MEUCIFPSTAFgtTqaPCJ+pB1c6yPqwo/eb+2/O43ta07zdsohAiEA+YoIkiu/HD/EKYCk2MvRc1ewFVukyAUPMTRHfszMlJQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":93819},"main":"src/index.js","type":"module","types":"src/index.d.ts","module":"src/index.js","exports":{".":{"types":"./src/index.d.ts","import":"./src/index.js"}},"scripts":{"test":"node --test"},"_npmUser":{"name":"mycloserclick","email":"admin@closer.click"},"repository":{"url":"git+https://github.com/closerclick/closer-click-lobby.git","type":"git"},"_npmVersion":"11.12.1","description":"Lobby + matchmaking reciclable para juegos del ecosistema CloserClick: salas, asientos, espectadores, motor de turnos autoritativo, reputación y contactos sobre el proxy/identity/reputation compartidos","directories":{},"_nodeVersion":"25.9.0","_hasShrinkwrap":false,"peerDependencies":{"@closerclick/closer-click-identity":">=0.5.0","@closerclick/closer-click-reputation":">=0.3.0","@closerclick/closer-click-proxy-client":">=0.1.0"},"peerDependenciesMeta":{"@closerclick/closer-click-reputation":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/closer-click-lobby_0.1.3_1780530703683_0.7876085654650844","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@closerclick/closer-click-lobby","version":"0.1.4","keywords":["lobby","matchmaking","game","turn-based","p2p","webrtc","closer-click","web-of-trust"],"author":{"name":"seyacat"},"license":"MIT","_id":"@closerclick/closer-click-lobby@0.1.4","maintainers":[{"name":"mycloserclick","email":"admin@closer.click"}],"homepage":"https://github.com/closerclick/closer-click-lobby#readme","bugs":{"url":"https://github.com/closerclick/closer-click-lobby/issues"},"dist":{"shasum":"cccab6e32592193eb14ba7227e703ecef0c98742","tarball":"https://registry.npmjs.org/@closerclick/closer-click-lobby/-/closer-click-lobby-0.1.4.tgz","fileCount":12,"integrity":"sha512-hN5z+5HzcT2APggp+M62gNHW0TpiPuiFjS1oE1xEFyeixxF2Khyf8lfxxKMCCMSZiUakZXNmiUqwqk77UgqkGA==","signatures":[{"sig":"MEQCIEVXG/kBmubyON7lSLyLSt/nwtbREdbKXKkdO11CDt7qAiBSn3ntKnfcBbO6/5kKOBe+Sxus705fHjrnNI318OcFrg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":97654},"main":"src/index.js","type":"module","types":"src/index.d.ts","module":"src/index.js","exports":{".":{"types":"./src/index.d.ts","import":"./src/index.js"}},"scripts":{"test":"node --test"},"_npmUser":{"name":"mycloserclick","email":"admin@closer.click"},"repository":{"url":"git+https://github.com/closerclick/closer-click-lobby.git","type":"git"},"_npmVersion":"11.12.1","description":"Lobby + matchmaking reciclable para juegos del ecosistema CloserClick: salas, asientos, espectadores, motor de turnos autoritativo, reputación y contactos sobre el proxy/identity/reputation compartidos","directories":{},"_nodeVersion":"25.9.0","_hasShrinkwrap":false,"peerDependencies":{"@closerclick/closer-click-identity":">=0.5.0","@closerclick/closer-click-reputation":">=0.3.0","@closerclick/closer-click-proxy-client":">=0.1.0"},"peerDependenciesMeta":{"@closerclick/closer-click-reputation":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/closer-click-lobby_0.1.4_1780534480801_0.9697381345297418","host":"s3://npm-registry-packages-npm-production"}},"0.1.5":{"name":"@closerclick/closer-click-lobby","version":"0.1.5","description":"Lobby + matchmaking reciclable para juegos del ecosistema CloserClick: salas, asientos, espectadores, motor de turnos autoritativo, reputación y contactos sobre el proxy/identity/reputation compartidos","type":"module","main":"src/index.js","module":"src/index.js","types":"src/index.d.ts","exports":{".":{"types":"./src/index.d.ts","import":"./src/index.js"}},"scripts":{"test":"node --test"},"keywords":["lobby","matchmaking","game","turn-based","p2p","webrtc","closer-click","web-of-trust"],"peerDependencies":{"@closerclick/closer-click-proxy-client":">=0.1.0","@closerclick/closer-click-identity":">=0.5.0","@closerclick/closer-click-reputation":">=0.3.0"},"peerDependenciesMeta":{"@closerclick/closer-click-reputation":{"optional":true}},"author":{"name":"seyacat"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/closerclick/closer-click-lobby.git"},"_id":"@closerclick/closer-click-lobby@0.1.5","bugs":{"url":"https://github.com/closerclick/closer-click-lobby/issues"},"homepage":"https://github.com/closerclick/closer-click-lobby#readme","_nodeVersion":"25.9.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-vhW0Mexxt0l1CqFqcuRp7YORlaazkp5RaQ85uGs8MxmJz1JXRgEPy1UY+tshZ5byMx/XadWz1ddRTV3guoEXKQ==","shasum":"78856fa9629e7a7f1e0db24d3e22f5d5f9803586","tarball":"https://registry.npmjs.org/@closerclick/closer-click-lobby/-/closer-click-lobby-0.1.5.tgz","fileCount":12,"unpackedSize":98269,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICRu44cvk5KOu0NiPDLqJJvgXQomOsCPxFZM43glWleGAiEAu1LIRoCCMx+DYTOyoUIWnGAzKMQL5pZRO48vXvSngzI="}]},"_npmUser":{"name":"mycloserclick","email":"admin@closer.click"},"directories":{},"maintainers":[{"name":"mycloserclick","email":"admin@closer.click"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/closer-click-lobby_0.1.5_1780550225219_0.3969026374988036"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-03T19:24:13.111Z","modified":"2026-06-04T05:17:05.480Z","0.1.0":"2026-06-03T19:24:13.492Z","0.1.2":"2026-06-03T22:54:39.073Z","0.1.3":"2026-06-03T23:51:43.837Z","0.1.4":"2026-06-04T00:54:40.969Z","0.1.5":"2026-06-04T05:17:05.355Z"},"bugs":{"url":"https://github.com/closerclick/closer-click-lobby/issues"},"author":{"name":"seyacat"},"license":"MIT","homepage":"https://github.com/closerclick/closer-click-lobby#readme","keywords":["lobby","matchmaking","game","turn-based","p2p","webrtc","closer-click","web-of-trust"],"repository":{"type":"git","url":"git+https://github.com/closerclick/closer-click-lobby.git"},"description":"Lobby + matchmaking reciclable para juegos del ecosistema CloserClick: salas, asientos, espectadores, motor de turnos autoritativo, reputación y contactos sobre el proxy/identity/reputation compartidos","maintainers":[{"name":"mycloserclick","email":"admin@closer.click"}],"readme":"# @closerclick/closer-click-lobby\n\n**Lobby + matchmaking reciclable para cualquier juego del ecosistema CloserClick.**\n\nResuelve, una sola vez y para todos los juegos, lo que el ajedrez, el chat y\ncompañía venían reimplementando a mano: **descubrir/crear/unir salas, asientos,\nespectadores, autoridad de host (serverless), presencia, pausa/reconexión por\nidentidad, sincronización autoritativa con resync, verificación del oponente,\nmatchmaking filtrado por reputación y recibo de partida co-firmado.**\n\nEstá construido **sobre** los pilares compartidos — no reimplementa transporte ni\nidentidad:\n\n- `@closerclick/closer-click-proxy-client` — transporte (canales, `identify`,\n  `send`/`sendByPubkey`, WebRTC). **Una sola conexión**, reutilizable.\n- `@closerclick/closer-click-identity` — vault (firma, challenge/response,\n  **contactos**).\n- `@closerclick/closer-click-reputation` — web-of-trust (gate de admisión,\n  ranking, atestaciones `txBound`). *Opcional.*\n\nHeadless y framework-agnóstico (vanilla + EventEmitter). Sirve igual para Vue,\nvanilla o nativo-vía-WebView.\n\n---\n\n## Idea central\n\nEl **host es uno de los jugadores** (no hay servidor central: *tu info, en tu\nmáquina*). La librería separa dos cosas:\n\n1. **Lobby/Room** — toda la fontanería de salas/asientos/presencia/sync.\n2. **Motor de turnos** *(opcional)* — el juego aporta **funciones puras**\n   (`initialState`, `reducer`, `view`, `isOver`) y la lib valida en el host,\n   sincroniza y hace resync. El ajedrez queda casi trivial; las cartas funcionan\n   gracias a `view` (estado oculto por asiento) y al azar determinista sembrado.\n\n---\n\n## Instalación\n\n```bash\nnpm i @closerclick/closer-click-lobby \\\n      @closerclick/closer-click-proxy-client \\\n      @closerclick/closer-click-identity \\\n      @closerclick/closer-click-reputation\n```\n\n---\n\n## Quick start\n\n```js\nimport { createLobby } from '@closerclick/closer-click-lobby'\n\n// 1) Definí tu juego como funciones puras (ejemplo: \"tres en raya\")\nconst tictactoe = {\n  initialState: { board: Array(9).fill(null), turn: 'x' },\n  reducer (state, action, ctx) {            // ctx = { seat, seats, rng, now }\n    const mark = ctx.seat === 'x' ? 'x' : 'o'\n    if (state.turn !== mark) throw new Error('no-es-tu-turno')\n    if (state.board[action.cell]) throw new Error('ocupada')\n    const board = state.board.slice()\n    board[action.cell] = mark\n    return { board, turn: mark === 'x' ? 'o' : 'x' }\n  },\n  isOver (state) {\n    const L = [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]]\n    for (const [a,b,c] of L) {\n      const v = state.board[a]\n      if (v && v === state.board[b] && v === state.board[c]) return { winner: v, reason: 'line' }\n    }\n    if (state.board.every(Boolean)) return { winner: null, reason: 'draw' }\n    return null\n  }\n}\n\n// 2) Creá el lobby (conecta e identifica con el vault solo)\nconst lobby = await createLobby({\n  gameId: 'tictactoe',\n  seats: ['x', 'o'],\n  engine: tictactoe,\n  start: 'ready',                 // arranca cuando ambos asientos están \"listos\"\n  onSeatVacated: 'pause',         // si alguien se va: pausa + reconexión por pubkey\n  matchmaking: { minReputation: 0.15, preferContacts: true }\n})\n\n// 3) Partida rápida\nconst room = await lobby.quickMatch({ autoSeat: true })\n\nroom.on('update', s => render(s))                 // s.seats, s.status, s.spectators\nroom.on('state',  game => renderBoard(game.board))// vista del juego para MI asiento\nroom.on('ended',  r => alert(`Ganó ${r.winner}`))\n\n// 4) Jugar\nboardEl.onclick = cell => room.action({ cell })\n```\n\nCrear/listar/unir manualmente:\n\n```js\nconst room  = await lobby.createRoom({ name: 'Mi sala' })   // sos host\nconst rooms = await lobby.listRooms()                       // [{ roomId, name, openSeats, hostScore, isContact, ... }]\nconst room2 = await lobby.joinRoom(rooms[0].roomId)         // sos guest\n```\n\n---\n\n## Asientos, espectadores y ciclo de vida\n\n```js\nroom.takeSeat('x')     // tomar un asiento LIBRE (sin id → el primero libre)\nroom.setReady(true)    // start:'ready' arranca cuando todos los ocupados están listos\nroom.leaveSeat()       // volver a espectador\nroom.spectate()        // mirar sin jugar (recibe el estado, no puede actuar)\nroom.mySeat            // 'x' | 'o' | null\n```\n\n- **Sólo se toman asientos libres.** Para cambiar, el ocupante deja el suyo primero.\n- **Espectadores**: reciben todos los `state`/`event`; sus `action()` se rechazan.\n  Un espectador puede `takeSeat()` si hay un asiento libre (así \"otro toma el\n  lugar\"). Con `onSeatVacated:'fill'`, la cola de espectadores rellena automático.\n- **Estados**: `waiting → playing → ended`, con `paused` intermedio.\n\n### ¿Qué pasa si un jugador se va / se cae?\n\nConfigurable con `onSeatVacated` (y el asiento va por **pubkey del vault**, no por\ntoken efímero):\n\n| Política | Comportamiento |\n|---|---|\n| `'pause'` *(default)* | La partida se pausa; el mismo jugador **recupera su asiento** al reconectar (dentro de `disconnectGraceMs`). Si vence el grace, el asiento se libera. |\n| `'forfeit'` | Irse/caerse termina la partida a favor del resto (ranked). |\n| `'fill'` | El asiento se libera y el primer espectador en cola lo toma; la partida sigue (drop-in/drop-out). |\n\n---\n\n## Motor de turnos (autoritativo)\n\nEl host instancia el motor; los guests sólo reflejan la **vista** que reciben.\n\n```js\nconst engine = {\n  initialState,                 // valor | (rng) => estado   (rng determinista, sembrado)\n  reducer (state, action, ctx), // ctx = { seat, seats, rng, now }; throw Error(reason) para rechazar\n  view (state, seat),           // opcional: proyección por asiento (info oculta). Default: estado completo\n  isOver (state)                // opcional: { winner, reason } | null\n}\n```\n\n- **Cartas / info oculta**: usá `view(state, seat)` para devolver sólo lo que ese\n  asiento puede ver (su mano). Los espectadores reciben `view(state, null)`.\n- **Azar (barajar, dados)**: usá `ctx.rng()` (y el `rng` de `initialState`). Es\n  determinista por semilla → reproducible y registrable en el recibo.\n- **Sin motor**: si no pasás `engine`, la sala funciona en **modo relay**:\n  `room.send(data)` / `room.on('message')` y el juego sincroniza su propio estado.\n\n---\n\n## Reputación + contactos\n\nAtraviesa todo el flujo (inyectá `reputation` para activarlo):\n\n```js\n// Gate de admisión (bidireccional): el host rechaza joiners y el lobby filtra salas.\nmatchmaking: { requireVouched: true, minReputation: 0.2, preferContacts: true }\n\n// Invitar a un contacto (cola offline 24 h):\nlobby.inviteContact(pubkey, { roomId: room.roomId, name: 'Revancha?' })\nlobby.on('invite', inv => console.log('te invitaron a', inv.roomId))\n\n// Verificación anti-impersonación: al sentarse se hace challenge/response.\n// (requireVerify: true por defecto)\n\n// Post-partida: calificar y agregar a contactos, con recibo co-firmado → txBound.\nawait room.ratePlayer(oppPubkey, { confianza: 5, afinidad: 3 }, { notes: 'gg' })\n```\n\n### Recibo de partida co-firmado\n\nAl terminar, el host y cada co-jugador **co-firman** un recibo\n`{ a, b, ts, sigA, sigB }` (cada uno firma `{op:'receipt',a,b,ts}` con su vault).\n`ratePlayer` lo adjunta automáticamente, produciendo una atestación **`txBound`**:\nprueba verificable de que *esos dos realmente jugaron juntos* (anti-sybil).\n\n```js\nroom.on('receipt', ({ pubkey, receipt }) => { /* guardado para el rating */ })\nroom.matchReceipt(pubkey) // { a, b, ts, sigA, sigB } | null\n```\n\n---\n\n## Eventos de `Room`\n\n| Evento | Payload | Cuándo |\n|---|---|---|\n| `update` | `RoomState` | cambió asientos/estado/espectadores |\n| `state` | `game` | cambió la vista de juego para mi asiento |\n| `started` | `{ ts }` | arrancó la partida |\n| `ended` | `{ winner, reason }` | terminó |\n| `event` | `{ event, data }` | evento lateral genérico |\n| `chat` | `{ from, name, text, ts }` | mensaje de chat |\n| `message` | `data` | relay opaco (modo sin motor) |\n| `rejected` | `{ reason }` | tu acción fue rechazada |\n| `kicked` | `{ reason }` | el host te expulsó (verify/reputación) |\n| `closed` | `{ reason }` | el host cerró / se perdió |\n| `receipt` | `{ pubkey, receipt }` | recibo co-firmado listo |\n| `left` | — | saliste de la sala (`room.leave()`) |\n\n---\n\n## API\n\n```ts\ncreateLobby(opts): Promise<Lobby>\n\nLobby.createRoom(opts?): Promise<Room>\nLobby.joinRoom(roomId, opts?): Promise<Room>\nLobby.listRooms(opts?): Promise<RoomSummary[]>\nLobby.quickMatch(opts?): Promise<Room>\nLobby.inviteContact(pubkey, { roomId, name }): void\nLobby.listContacts(): Promise<PeerInfo[]>\nLobby.reputationOf(pubkey): Promise<AggregateResult>\n\nRoom.takeSeat(seat?) / leaveSeat() / setReady(b) / spectate()\nRoom.action(a) / chat(text) / send(data) / start()\nRoom.ratePlayer(pubkey, indicators, opts?) / matchReceipt(pubkey)\nRoom.mySeat / status / seats / spectators / game / result / state\nRoom.leave()\n```\n\nVer tipos completos en [`src/index.d.ts`](./src/index.d.ts).\n\n---\n\n## Notas de diseño / límites de v1\n\n- `roomId === token del host` (modelo probado por el ajedrez). Si el host\n  **reconecta**, su token cambia → la sala se considera nueva y los guests caen\n  (`onHostLost: 'end'`). La **migración de host** (elección determinista del\n  jugador de menor pubkey) está prevista como `onHostLost: 'migrate'` y queda como\n  extensión futura.\n- El recibo co-firmado se genera para los pares **host ↔ cada jugador** (cubre 1v1\n  completo, p.ej. ajedrez). Recibos entre guests en mesas de N>2 quedan pendientes.\n- Sincronización por **snapshot completo personalizado** por cambio (simple y\n  correcto para juegos por turnos). Deltas/patches son una optimización futura.\n\n## Tests\n\n```bash\nnpm test    # node --test\n```\n","readmeFilename":"README.md"}