ExplicacionArquitectura del sistema

Bus y contratos

Cómo hablan los servicios entre sí y con el daemon, de dónde salen los contratos y qué se genera de la tabla de rutas del bus.

Implementado. Los subjects concretos, sus versiones, topes y emisores están en la referencia generada del bus (referencia/bus/, desde BUS_ROUTES). Esta página explica el mecanismo.

Tres familias de mensajes

La comunicación entre servicios usa NATS JetStream como bus (dec-0005) con tres familias de subject:

FamiliaFormaSemántica
cmdcmd.dominio.accionOrden: pide un cambio y espera confirmación
qryqry.dominio.consultaConsulta: respuesta sin efectos
evtevt.dominio.eventoHecho ocurrido: sin respuesta

Los bytes de vídeo nunca viajan por el bus. Lo que sí cruza es control: abrir una sesión en el daemon (cmd.media.createSession), pedir los hechos de un fichero (qry.media.probe), dar de alta lo ingerido (cmd.catalog.registerIngested) o publicar señales de transporte (evt.media.transportSignals).

Un contrato por mensaje

Cada mensaje es un contrato definido en @styx/api-contracts (src/bus/): subject, familia, versión, tope de bytes y los esquemas JSON (TypeBox 1.x) de petición y de respuesta. Los contratos se definen una sola vez y de ahí salen:

  • la validación en los dos lados (entrada y salida, en cada frontera);
  • el espejo Zig que usa el daemon, generado desde los mismos esquemas;
  • la referencia del bus del sitio.

@styx/api-contracts es la fuente canónica contract-first (dec-0116): no se deriva de los tipos de Elysia y no importa elysia.

BUS_ROUTES: la tabla única de autorización

BUS_ROUTES (packages/api-contracts/src/bus/routes.ts) declara, para cada subject:

  • el servicio que atiende, y por qué transporte llega (nats o unix, este último es el socket de control del daemon);
  • el contrato;
  • la lista de emisores admitidos (allow), por identidad de servicio;
  • el rate (token bucket por emisor y subject), obligatorio.

El criterio de la lista de emisores es deliberado: nombra a quien hoy llama al handler en un camino de producción. Un handler sin emisor de servicio sólo admite styx-ops (herramientas de operación y tests, con identidad propia fuera del llavero de producción). No hay consumidores previstos.

Las identidades del bus son catalog-svc, identity-svc, media-daemon, playback-svc, realtime-svc, sources-svc, workers-svc, styx-ops y la web.

Qué se genera de la tabla

bun run gen:bus      # escribe
bun run check:bus    # falla si lo versionado difiere de lo generado

De BUS_ROUTES salen tres cosas, y ninguna se edita a mano:

  1. La policy de cada handler TypeScript: el servicio no la escribe, busHandler la toma de la tabla. Un handler sin policy hace que el servicio no arranque.
  2. El espejo Zig del daemon (native/zig/media-daemon/ipc/contracts.generated.zig): structs con validación y las policies del socket de control. Sin policy no compila.
  3. La ACL de NATS (deploy/nats/nats-authorization.conf): un usuario por servicio que publica exactamente lo que alguna lista de emisores le permite y suscribe sus handlers y su buzón. Un servicio sin subjects de publicación recibe deny explícito. Las cuentas de servicio no pueden crear ni cambiar streams de JetStream: los streams los provisiona el despliegue fuera de banda (bun run bus:streams).

El sobre

Todo mensaje viaja en el sobre v2 de spire: identificador, subject y versión de contrato, identidad del emisor, el JWT del usuario en cuyo nombre se actúa (si aplica), marca temporal y nonce anti-replay, plazo, ids de correlación y traza, y una firma Ed25519 sobre la cabecera y el digest del payload. La seguridad de ese sobre se explica en comunicaciones entre procesos.

Un único camino hacia el bus

Nada habla NATS ni el socket de control por su cuenta. @styx/bus es el único punto de los servicios que importa los transportes de spire, y un test de arquitectura falla si cualquier fichero de apps/, packages/ o tools/ importa nats, o si alguien fuera de @styx/bus importa los transportes. En Zig, la auditoría de seguridad prohíbe sockets sin pasar por el transporte de spire. Así mover un handler de dentro a fuera de proceso no cambia su semántica ni abre un hueco.