Skip to content

feat: compartir el protocolo binario entre cliente y servidor - #79

Merged
leocagli merged 5 commits into
Bitcoindefi:mainfrom
ceshez:el-protocolo-binario-esta-duplicado-entre-cliente-y-servidor
Aug 28, 2026
Merged

feat: compartir el protocolo binario entre cliente y servidor#79
leocagli merged 5 commits into
Bitcoindefi:mainfrom
ceshez:el-protocolo-binario-esta-duplicado-entre-cliente-y-servidor

Conversation

@ceshez

@ceshez ceshez commented Aug 18, 2026

Copy link
Copy Markdown

Resumen

Extrae el contrato binario compartido a packages/protocol para que frontend y servidor consuman una única fuente de verdad. Esta entrega migra completamente el flujo cliente→servidor y deja centralizados los opcodes de ambas direcciones, las primitivas binarias, constantes y modelos compartidos.

También actualiza CI y Docker para construir el paquete antes que sus consumidores, y agrega una guía de pruebas manuales y automáticas.

Criterios de aceptación

  • Existe un paquete compartido con la definición del protocolo

    • Se creó @openao/protocol en packages/protocol.
    • Contiene opcodes, límites compartidos, tipos de mensajes, PacketReader, PacketWriter y codecs cliente→servidor.
  • Cliente y servidor lo importan, sin copias locales de lo migrado

    • El frontend importa el contrato desde @openao/protocol y delega en él la creación de los 30 paquetes cliente→servidor.
    • El servidor importa los mismos opcodes, lectores, escritores, tipos y constantes.
    • Se eliminó la implementación local basada en bytebuffer y las copias locales migradas.
  • Cambiar un opcode en el paquete compartido rompe la compilación de ambos lados

    • Ambos consumidores dependen del mapa de opcodes tipado exportado por el paquete.
    • Renombrar o eliminar una clave provoca errores en los typechecks del frontend y servidor.
    • Cambiar únicamente el valor numérico hace fallar los snapshots de bytes, detectando cambios incompatibles del wire protocol.
  • Hay tests de serialización y deserialización

    • 34 tests aprobados.
    • Existe un fixture y un round trip decode(encode(payload)) para cada uno de los 30 paquetes cliente→servidor migrados.
    • Los snapshots verifican los bytes exactos y también se prueban Unicode, opcodes desconocidos, paquetes truncados y bytes sobrantes.
  • El juego sigue funcionando igual

    • API verificada con respuesta 200.
    • Frontend verificado con respuesta 200.
    • WebSocket verificado con conexión abierta en el puerto 7666.
    • Se probó registro, creación de personaje, entrada a arena, carga de mapas y conexión del jugador.
    • El servidor cargó mapas, objetos, NPCs, crafting y fundición sin errores de protocolo.

Validación automática

  • packages/protocol: typecheck, 34/34 tests y build.
  • server: typecheck, lint y build.
  • frontend: typecheck, lint y build de producción.
  • Docker: imágenes de frontend y servidor construidas correctamente.
  • git diff --check: sin errores.

Documentación

Se agregó docs/protocol-testing.md con instrucciones para:

  • probar el contrato aislado;
  • comprobar ambos consumidores;
  • demostrar la protección ante cambios incompatibles;
  • validar manualmente el flujo dentro del juego.

Closes #28

@ceshez
ceshez marked this pull request as ready for review August 18, 2026 20:43
@ceshez
ceshez marked this pull request as draft August 19, 2026 02:57
@ceshez
ceshez marked this pull request as ready for review August 19, 2026 02:59
Comment thread server/src/server.ts
Comment on lines +477 to +483
const decodedPacket = pkg.decodeClientPacket(data as PacketPayload);
pkg.setData(data as PacketPayload);
const packageID = pkg.getPackageID();

if (decodedPacket.id !== packageID) {
return;
}

@gitar-bot gitar-bot Bot Aug 19, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance: Redundant full packet decode added to WS hot path

Every inbound packet is now fully decoded twice: pkg.decodeClientPacket() parses the entire payload (including JSON.parse for marketAction/retosAction and an O(n) Object.entries(SERVER_PACKET_ID).find() reverse lookup per packet in getServerPacketName), and then protocol.handleData re-reads the same bytes through the real handlers. The only use of the decoded result is the decodedPacket.id !== packageID guard, but decodedPacket.id is derived from the same first byte that getPackageID() reads, so the check can never be true — it is dead code. This doubles per-packet parsing cost in the server's hottest path (movement/attack/ping traffic) for no functional benefit.

Remove the redundant decodeClientPacket pre-pass and its always-false id guard; keep validation inside the individual handlers.:

pkg.setData(data as PacketPayload);
const packageID = pkg.getPackageID();

trackClientActivity(ws, packageID);

protocol.handleData(ws, packageID);

Was this helpful? React with 👍 / 👎

Comment on lines +147 to +161
export function decodeClientPacket(input: BinaryInput): DecodedClientPacket {
const reader = new PacketReader(input);
const id = reader.getByte();
const type = getServerPacketName(id);
let payload: ClientPacketPayloads[ServerPacketName];

switch (type) {
case "connectCharacter":
payload = { ticket: reader.getString(), typeGame: reader.getByte(), idChar: reader.getByte() };
break;
case "position":
payload = { heading: reader.getByte(), moveId: reader.getInt() };
break;
case "click":
payload = { x: reader.getByte(), y: reader.getByte(), button: reader.getByte() };

@gitar-bot gitar-bot Bot Aug 19, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Security: Malformed packets bypass flood tracking and spam error log

decodeClientPacket runs before trackClientActivity and throws a RangeError on any unknown opcode, truncated packet, or trailing bytes. Because the throw is caught and routed to dumpError before tracking happens, a connected client can flood the server with malformed packets that (a) completely bypass the per-connection PPS/interval anti-flood accounting in trackClientActivity, and (b) trigger a dumpError call for every bad packet, enabling log/telegram spam. Previously an unknown/garbage packet was still counted by trackClientActivity and then dropped as a no-op. If the pre-decode is kept, move trackClientActivity before it (or track inside the catch) so malformed traffic is still rate-limited and not logged per-packet.

Drop the pre-decode so malformed packets follow the same tracked, silent no-op path as before instead of throwing and skipping rate limiting.:

pkg.setData(data as PacketPayload);
const packageID = pkg.getPackageID();

trackClientActivity(ws, packageID);

protocol.handleData(ws, packageID);

Was this helpful? React with 👍 / 👎

@ceshez

ceshez commented Aug 22, 2026

Copy link
Copy Markdown
Author

hi could you re reun the workflows so i can know if theres something to change thank you

Comment thread server/package.json
"build": "pnpm run clean && tsc && node scripts/copy-assets.cjs",
"predev": "pnpm run protocol:build",
"dev": "NODE_ENV=development tsx watch src/server.ts",
"test": "tsx --test tests/**/*.test.ts",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Quality: client-activity policy test not wired into CI

The new test:client-activity script (testClientActivityPolicy.ts) guards the intentional ping-vs-activity invariant, but CI only runs pnpm test which globs tests/**/*.test.ts; the policy script lives under src/scripts/ and is never executed in CI. The invariant it protects (pings feed liveness but not AFK metrics) can silently regress. Add it to CI, e.g. run pnpm run test:client-activity alongside pnpm test, or move it into tests/.

Was this helpful? React with 👍 / 👎

@leocagli

Copy link
Copy Markdown
Collaborator

Revisé esta contra el main de hoy, que quedó verde por primera vez. El trabajo
está bien y el problema que resuelve es real. Falta una cosa antes de mergear,
y es del tipo que el CI no puede ver.

Los docker-compose dejan de construir

Reescribiste server/Dockerfile y frontend/Dockerfile asumiendo que el contexto
de build es la raíz del repo:

COPY packages/protocol/...
COPY server .

y actualizaste .github/workflows/ci.yml acorde, con context: . y
file: ./server/Dockerfile. Eso está bien y por eso el job Docker build checks
pasa en verde.

Pero los seis docker-compose que viven adentro de server/ y frontend/
(docker-compose.yml, docker-compose.test.yml, y los blue/green del frontend)
siguen con:

build:
  context: .

Para Compose, ese . es el directorio del propio archivo, no la raíz del repo.
Desde ahí packages/ y server/ no existen.

Reproducido con docker 29.1.3 sobre tu rama:

$ cd server && docker compose build
failed to solve: failed to compute cache key: ... "/packages/protocol": not found

$ cd frontend && docker compose build
failed to solve: ... "/frontend": not found

La misma imagen con contexto raíz sí construye:

$ docker build -f ./server/Dockerfile .     # ok

O sea que lo único roto es el contexto que pasan los compose.

Por qué esto importa más de lo que parece: el CI queda verde porque su job
docker-build sí lo actualizaste. El fallo aparece recién al desplegar, que es el
peor momento para descubrirlo.

El arreglo es en los seis archivos:

build:
  context: ../
  dockerfile: server/Dockerfile

(y el equivalente para frontend). Ajustá también los volumes relativos si alguno
depende del contexto viejo.

Lo otro, que no bloquea pero conviene mirar

En server/src/server.ts:545, el decode estricto convierte cada paquete
malformado en una excepción con stack completo. Antes esos paquetes caían por una
rama que los contabilizaba en la telemetría anti-bot; ahora salen por el catch y
dejan de contarse. O sea que un cliente que manda basura a propósito pasa a ser
invisible para esa métrica, y encima llena el log.

No es bloqueante y puede que sea intencional. Si lo es, decilo en el cuerpo de la
PR así queda registrado; si no, conviene contar el paquete antes de tirar.

Sobre lo demás

Miré el resto con lente de seguridad porque tocás .github/workflows/ci.yml,
.dockerignore y dos Dockerfiles, que es lo que corre código en el CI de la
organización. No encontré nada: no hay dependencias nuevas, ni postinstall,
ni pasos que filtren secretos. La revisión levantó una sospecha sobre habilitar el
script de instalación de esbuild y quedó descartada al mirarlo.

Arreglá los compose y la mergeamos. Aviso aparte: los PRs de fork quedan en
action_required y el CI no corre hasta que alguien lo aprueba a mano. Nadie lo
estaba haciendo, por eso tu PR nunca mostró resultado hasta hoy. Ya lo destrabé;
cuando empujes el arreglo apruebo la corrida.

@ceshez

ceshez commented Aug 28, 2026

Copy link
Copy Markdown
Author

Revisé esta contra el main de hoy, que quedó verde por primera vez. El trabajo está bien y el problema que resuelve es real. Falta una cosa antes de mergear, y es del tipo que el CI no puede ver.

Los docker-compose dejan de construir

Reescribiste server/Dockerfile y frontend/Dockerfile asumiendo que el contexto de build es la raíz del repo:

COPY packages/protocol/...
COPY server .

y actualizaste .github/workflows/ci.yml acorde, con context: . y file: ./server/Dockerfile. Eso está bien y por eso el job Docker build checks pasa en verde.

Pero los seis docker-compose que viven adentro de server/ y frontend/ (docker-compose.yml, docker-compose.test.yml, y los blue/green del frontend) siguen con:

build:
  context: .

Para Compose, ese . es el directorio del propio archivo, no la raíz del repo. Desde ahí packages/ y server/ no existen.

Reproducido con docker 29.1.3 sobre tu rama:

$ cd server && docker compose build
failed to solve: failed to compute cache key: ... "/packages/protocol": not found

$ cd frontend && docker compose build
failed to solve: ... "/frontend": not found

La misma imagen con contexto raíz sí construye:

$ docker build -f ./server/Dockerfile .     # ok

O sea que lo único roto es el contexto que pasan los compose.

Por qué esto importa más de lo que parece: el CI queda verde porque su job docker-build sí lo actualizaste. El fallo aparece recién al desplegar, que es el peor momento para descubrirlo.

El arreglo es en los seis archivos:

build:
  context: ../
  dockerfile: server/Dockerfile

(y el equivalente para frontend). Ajustá también los volumes relativos si alguno depende del contexto viejo.

Lo otro, que no bloquea pero conviene mirar

En server/src/server.ts:545, el decode estricto convierte cada paquete malformado en una excepción con stack completo. Antes esos paquetes caían por una rama que los contabilizaba en la telemetría anti-bot; ahora salen por el catch y dejan de contarse. O sea que un cliente que manda basura a propósito pasa a ser invisible para esa métrica, y encima llena el log.

No es bloqueante y puede que sea intencional. Si lo es, decilo en el cuerpo de la PR así queda registrado; si no, conviene contar el paquete antes de tirar.

Sobre lo demás

Miré el resto con lente de seguridad porque tocás .github/workflows/ci.yml, .dockerignore y dos Dockerfiles, que es lo que corre código en el CI de la organización. No encontré nada: no hay dependencias nuevas, ni postinstall, ni pasos que filtren secretos. La revisión levantó una sospecha sobre habilitar el script de instalación de esbuild y quedó descartada al mirarlo.

Arreglá los compose y la mergeamos. Aviso aparte: los PRs de fork quedan en action_required y el CI no corre hasta que alguien lo aprueba a mano. Nadie lo estaba haciendo, por eso tu PR nunca mostró resultado hasta hoy. Ya lo destrabé; cuando empujes el arreglo apruebo la corrida.

ok perfecto muchas gracias por el feedback empezare a trabajar en esto!!!

@gitar-bot

gitar-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 0 resolved / 4 findings

Extracts the shared binary protocol to packages/protocol for a single source of truth, but the WebSocket hot path now decodes every inbound packet twice (in decodeClientPacket and again in handlers), with the guard check being dead code. Additionally, malformed packets bypass flood tracking before trackClientActivity runs, enabling log spam; the shutdown sequence races exit(0) against WebSocket close frames; and the new test:client-activity policy test is not wired into CI. These issues should be resolved before merge.

⚠️ Performance: Redundant full packet decode added to WS hot path

📄 server/src/server.ts:477-483 📄 packages/protocol/src/clientPackets.ts:147-161

Every inbound packet is now fully decoded twice: pkg.decodeClientPacket() parses the entire payload (including JSON.parse for marketAction/retosAction and an O(n) Object.entries(SERVER_PACKET_ID).find() reverse lookup per packet in getServerPacketName), and then protocol.handleData re-reads the same bytes through the real handlers. The only use of the decoded result is the decodedPacket.id !== packageID guard, but decodedPacket.id is derived from the same first byte that getPackageID() reads, so the check can never be true — it is dead code. This doubles per-packet parsing cost in the server's hottest path (movement/attack/ping traffic) for no functional benefit.

Remove the redundant decodeClientPacket pre-pass and its always-false id guard; keep validation inside the individual handlers.
pkg.setData(data as PacketPayload);
const packageID = pkg.getPackageID();

trackClientActivity(ws, packageID);

protocol.handleData(ws, packageID);
⚠️ Security: Malformed packets bypass flood tracking and spam error log

📄 server/src/server.ts:472-486 📄 packages/protocol/src/clientPackets.ts:147-161 📄 packages/protocol/src/clientPackets.ts:237-239 📄 packages/protocol/src/clientPackets.ts:244-252

decodeClientPacket runs before trackClientActivity and throws a RangeError on any unknown opcode, truncated packet, or trailing bytes. Because the throw is caught and routed to dumpError before tracking happens, a connected client can flood the server with malformed packets that (a) completely bypass the per-connection PPS/interval anti-flood accounting in trackClientActivity, and (b) trigger a dumpError call for every bad packet, enabling log/telegram spam. Previously an unknown/garbage packet was still counted by trackClientActivity and then dropped as a no-op. If the pre-decode is kept, move trackClientActivity before it (or track inside the catch) so malformed traffic is still rate-limited and not logged per-packet.

Drop the pre-decode so malformed packets follow the same tracked, silent no-op path as before instead of throwing and skipping rate limiting.
pkg.setData(data as PacketPayload);
const packageID = pkg.getPackageID();

trackClientActivity(ws, packageID);

protocol.handleData(ws, packageID);
💡 Bug: exit(0) races WebSocket close(1001) handshake during shutdown

📄 server/src/server.ts:236-246

In closeClients() each client is closed with client.close(1001, SHUTDOWN_CLIENT_MESSAGE), then run() immediately calls options.exit(0) synchronously in the finally block. WebSocket.close() only initiates the closing handshake asynchronously, so process.exit(0) terminates the process before the 1001 close frame (and its reason) is flushed, and clients see an abrupt TCP reset rather than a clean close code. Impact is limited because the human-readable shutdown message is already delivered earlier in notifyClients(), but if a clean close code matters, defer the exit (e.g. setImmediate/short delay) until after close frames are written.

💡 Quality: client-activity policy test not wired into CI

📄 server/package.json:31 📄 server/package.json:38 📄 .github/workflows/ci.yml:160-162

The new test:client-activity script (testClientActivityPolicy.ts) guards the intentional ping-vs-activity invariant, but CI only runs pnpm test which globs tests/**/*.test.ts; the policy script lives under src/scripts/ and is never executed in CI. The invariant it protects (pings feed liveness but not AFK metrics) can silently regress. Add it to CI, e.g. run pnpm run test:client-activity alongside pnpm test, or move it into tests/.

🤖 Prompt for agents
Code Review: Extracts the shared binary protocol to `packages/protocol` for a single source of truth, but the WebSocket hot path now decodes every inbound packet twice (in `decodeClientPacket` and again in handlers), with the guard check being dead code. Additionally, malformed packets bypass flood tracking before `trackClientActivity` runs, enabling log spam; the shutdown sequence races `exit(0)` against WebSocket close frames; and the new `test:client-activity` policy test is not wired into CI. These issues should be resolved before merge.

1. ⚠️ Performance: Redundant full packet decode added to WS hot path
   Files: server/src/server.ts:477-483, packages/protocol/src/clientPackets.ts:147-161

   Every inbound packet is now fully decoded twice: `pkg.decodeClientPacket()` parses the entire payload (including `JSON.parse` for marketAction/retosAction and an O(n) `Object.entries(SERVER_PACKET_ID).find()` reverse lookup per packet in `getServerPacketName`), and then `protocol.handleData` re-reads the same bytes through the real handlers. The only use of the decoded result is the `decodedPacket.id !== packageID` guard, but `decodedPacket.id` is derived from the same first byte that `getPackageID()` reads, so the check can never be true — it is dead code. This doubles per-packet parsing cost in the server's hottest path (movement/attack/ping traffic) for no functional benefit.

   Fix (Remove the redundant decodeClientPacket pre-pass and its always-false id guard; keep validation inside the individual handlers.):
   pkg.setData(data as PacketPayload);
   const packageID = pkg.getPackageID();
   
   trackClientActivity(ws, packageID);
   
   protocol.handleData(ws, packageID);

2. ⚠️ Security: Malformed packets bypass flood tracking and spam error log
   Files: server/src/server.ts:472-486, packages/protocol/src/clientPackets.ts:147-161, packages/protocol/src/clientPackets.ts:237-239, packages/protocol/src/clientPackets.ts:244-252

   `decodeClientPacket` runs before `trackClientActivity` and throws a RangeError on any unknown opcode, truncated packet, or trailing bytes. Because the throw is caught and routed to `dumpError` before tracking happens, a connected client can flood the server with malformed packets that (a) completely bypass the per-connection PPS/interval anti-flood accounting in `trackClientActivity`, and (b) trigger a `dumpError` call for every bad packet, enabling log/telegram spam. Previously an unknown/garbage packet was still counted by `trackClientActivity` and then dropped as a no-op. If the pre-decode is kept, move `trackClientActivity` before it (or track inside the catch) so malformed traffic is still rate-limited and not logged per-packet.

   Fix (Drop the pre-decode so malformed packets follow the same tracked, silent no-op path as before instead of throwing and skipping rate limiting.):
   pkg.setData(data as PacketPayload);
   const packageID = pkg.getPackageID();
   
   trackClientActivity(ws, packageID);
   
   protocol.handleData(ws, packageID);

3. 💡 Bug: exit(0) races WebSocket close(1001) handshake during shutdown
   Files: server/src/server.ts:236-246

   In `closeClients()` each client is closed with `client.close(1001, SHUTDOWN_CLIENT_MESSAGE)`, then `run()` immediately calls `options.exit(0)` synchronously in the `finally` block. `WebSocket.close()` only initiates the closing handshake asynchronously, so `process.exit(0)` terminates the process before the 1001 close frame (and its reason) is flushed, and clients see an abrupt TCP reset rather than a clean close code. Impact is limited because the human-readable shutdown message is already delivered earlier in `notifyClients()`, but if a clean close code matters, defer the exit (e.g. `setImmediate`/short delay) until after close frames are written.

4. 💡 Quality: client-activity policy test not wired into CI
   Files: server/package.json:31, server/package.json:38, .github/workflows/ci.yml:160-162

   The new `test:client-activity` script (`testClientActivityPolicy.ts`) guards the intentional ping-vs-activity invariant, but CI only runs `pnpm test` which globs `tests/**/*.test.ts`; the policy script lives under `src/scripts/` and is never executed in CI. The invariant it protects (pings feed liveness but not AFK metrics) can silently regress. Add it to CI, e.g. run `pnpm run test:client-activity` alongside `pnpm test`, or move it into `tests/`.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Important

Your trial ends in 5 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.

Was this helpful? React with 👍 / 👎 | Gitar

@ceshez
ceshez force-pushed the el-protocolo-binario-esta-duplicado-entre-cliente-y-servidor branch from 2574dc0 to 2a3da42 Compare August 28, 2026 01:30
@ceshez

ceshez commented Aug 28, 2026

Copy link
Copy Markdown
Author

Hola @leocagli , ya apliqué la corrección solicitada en los seis archivos docker-compose: todos usan ahora la raíz del repositorio como contexto y declaran explícitamente el Dockerfile correspondiente. También verifiqué que los volumes del servidor continúan resolviéndose correctamente.
Probé localmente los seis builds —server, server test, frontend, frontend test, blue y green— y todos finalizaron correctamente.
Los workflows CI y Secret Scan quedaron en action_required por tratarse de una PR desde un fork. ¿Podrías aprobar y ejecutar las corridas cuando tengas un momento?

@leocagli

Copy link
Copy Markdown
Collaborator

Verificado y mergeado. Gracias por el arreglo rápido.

Comprobé que el problema que había señalado quedó resuelto corriendo el build de verdad, que es como lo había reproducido:

$ cd server && docker compose build
 server  Built

Antes eso moría con failed to compute cache key: "/packages/protocol": not found. Los seis compose ahora pasan context: ../ con el dockerfile: explícito, que es exactamente lo que hacía falta.

Sobre lo otro que había mencionado, el decode estricto en server.ts:545 que saca los paquetes malformados de la telemetría anti-bot: no bloquea, así que va aparte. Si fue intencional dejalo escrito en alguna issue para que quede registrado; si no, lo miramos después.

@leocagli
leocagli merged commit b72c72c into Bitcoindefi:main Aug 28, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

El protocolo binario esta duplicado entre cliente y servidor

2 participants