Files
unibus/app.md
T
Egutierrez 1c9325104c feat(embeddednats): UNIBUS_NATS_MONITOR flag decoupled from debug log
Add a dedicated UNIBUS_NATS_MONITOR=1 toggle that opens the embedded
nats-server monitoring HTTP endpoint (127.0.0.1:8222, loopback only) so a
local metrics scraper can read /varz, /connz and /jsz for server-level
metrics (msgs/s, connections, KV bucket msgs, RAFT leader per stream,
restarts).

Previously the monitoring endpoint was only reachable via UNIBUS_NATS_DEBUG=1,
which is coupled to the verbose nats-server debug log: enabling the endpoint
also wrote routes/RAFT/room subjects to journald in clear, which regresses the
hardened posture (issue 0007). The two concerns are now decoupled.

The toggle computation is extracted to a pure function
natsLogOpts(debugEnv, monitorEnv) (noLog, debug, trace, monitor): MONITOR=1
opens the endpoint while keeping the log quiet (NoLog true / Debug false). The
inverse coupling is preserved for backward compatibility (DEBUG still implies
MONITOR). The 127.0.0.1 bind stays hardcoded — the monitoring endpoint has no
auth and must never be reachable from the network.

Deploy wiring versioned: additive systemd drop-in
membershipd-cluster.service.d/nats-monitor.conf (Environment=UNIBUS_NATS_MONITOR=1)
plus a "NATS server metrics" section in the cluster README with the rolling
activation runbook (magnus -> homer -> datardos) gated on R3 reconvergence
(followers 2/2) between nodes.

Tests: pure decoupling table (monitor on => log NOT debug; debug => monitor;
default closed) + a real embedded server with MONITOR=1 asserting /varz answers
200 on loopback:8222, and a server without the flag with the endpoint closed.
100% additive: behavior is identical without the flag. Bump app.md 0.10.0 ->
0.11.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:57:46 +02:00

340 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: unibus
lang: go
domain: infra
version: 0.11.0
description: "Bus de mensajería unificado sobre NATS+JetStream con cifrado E2E por room (megolm/olm reducido): service de membresía/claves, librería cliente y peers demo."
tags: [service, messaging, nats, e2e]
uses_functions:
- generate_identity_go_cybersecurity
- seal_aead_go_cybersecurity
- open_aead_go_cybersecurity
- seal_key_box_go_cybersecurity
- open_key_box_go_cybersecurity
- sign_ed25519_go_cybersecurity
- verify_ed25519_go_cybersecurity
uses_types: []
framework: ""
entry_point: "cmd/membershipd"
dir_path: "projects/message_bus/apps/unibus"
repo_url: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/unibus"
service:
port: 8470
health_endpoint: /healthz
health_timeout_s: 3
systemd_unit: unibus-membershipd.service
systemd_scope: user
restart_policy: always
runtime: systemd-user
pc_targets:
- lucas-linux
is_local_only: false
e2e_checks:
- id: build
cmd: "CGO_ENABLED=0 go build ./..."
timeout_s: 180
- id: vet
cmd: "CGO_ENABLED=0 go vet ./..."
timeout_s: 120
- id: unit
cmd: "CGO_ENABLED=0 go test ./..."
timeout_s: 180
- id: smoke
cmd: "CGO_ENABLED=0 go build -o /tmp/unibus_membershipd ./cmd/membershipd && /tmp/unibus_membershipd --http-port 18470 --nats-port 14222 --db /tmp/unibus_smoke.db --store-dir /tmp/unibus_blobs --nats-store /tmp/unibus_js &"
health: "http://127.0.0.1:18470/healthz"
timeout_s: 30
---
## Qué es
`unibus` es un bus de mensajería unificado sobre NATS + JetStream. Una capa fina
encima de NATS aporta lo que NATS no tiene: membresía de rooms, invitaciones con
reparto de clave, y cifrado extremo a extremo (E2E) del payload por room, con
rotación de clave activa (forward secrecy) al expulsar a un miembro.
Todos los participantes —procesos/workers, interfaces de chat humanas, agentes
LLM— son peers de primera clase que hablan el mismo protocolo y se diferencian
solo por las rooms a las que se unen y por lo que hacen con lo que reciben.
### Piezas
| Componente | Ruta | Rol |
|---|---|---|
| `membershipd` | `cmd/membershipd` | Service (control plane): metadata de rooms, directorio de miembros, reparto de claves selladas, object store de media. Arranca NATS embebido si no se pasa `--nats-url`. |
| librería cliente | `pkg/client` | La API que consume cualquier peer. Crear/unirse a rooms, invitar, publicar/suscribir, request/reply, kick con rotación de clave, media cifrada. |
| `worker` | `cmd/worker` | Peer demo: publica un contador incremental cada segundo a una room cleartext. |
| `chat` | `cmd/chat` | Peer demo: suscriptor en vivo (modo simple) + demo de cifrado E2E y forward secrecy (`--demo-encrypted`). |
### Dos planos
- **Control plane**: HTTP autoritativo en `membershipd`. Quién está en cada room,
sus claves públicas, y la clave de room `K` sellada por epoch para cada miembro.
- **Data plane**: NATS. Los mensajes (frames) viajan por subject; los blobs de
media NO viajan por el bus, se cifran y se suben al object store, y por NATS
solo viaja una referencia (hash + nonce).
### Criptografía (importada del registry, NO reescrita)
El cifrado E2E se compone de las 7 primitivas del capability group
`e2e-messaging` (`docs/capabilities/e2e-messaging.md`), importadas del paquete
`fn-registry/functions/cybersecurity`. Esta app no reimplementa ninguna
primitiva criptográfica.
## Ejemplo
Demo end-to-end con `go run` (NATS embebido, nada que instalar):
```bash
cd projects/message_bus/apps/unibus
# 1. Service de membresía/claves (NATS embebido en :4250, HTTP en :8470)
go run ./cmd/membershipd
# 2. En otra terminal: peer publicador (proceso) — publica ticks cada 1s
go run ./cmd/worker
# 3. En otra terminal: peer suscriptor (chat humano) — imprime cada tick en vivo
go run ./cmd/chat
# 4. Demo de cifrado E2E + forward secrecy (contra el membershipd ya corriendo):
# A crea room cifrada, invita a B, A publica (B descifra), A expulsa a B,
# A publica de nuevo en el nuevo epoch (B ya NO puede descifrar).
go run ./cmd/chat --demo-encrypted
```
Para apuntar a un NATS externo en producción: `--nats-url nats://host:4222` en
`membershipd`, `worker` y `chat`.
## Cuando usarla
- Cuando necesites un tejido de mensajería donde procesos, humanos y agentes LLM
sean peers uniformes (mismo protocolo, distinta política por room).
- Cuando quieras rooms cifradas E2E con forward secrecy (paridad con Matrix) sin
montar un Synapse: `room.ModeMatrix`.
- Cuando quieras fan-out cleartext rápido para telemetría/coordinación de
procesos: `room.ModeNATS`.
- Como sustituto de la capa de transporte Matrix de `agents_and_robots` (fase
posterior; v1 valida el bus de forma autónoma).
## Gotchas
- **El service NO está endurecido (v1).** No hay TLS, ni rate-limit, ni auth en
las rutas GET de lectura. Confía en la red interna. Las rutas mutantes
(`/rooms`, `/invite`, `/rekey`) sí exigen firma Ed25519 del owner sobre los
bytes canónicos de la request. Endurecer es fase posterior.
- **Gestión de usuarios: storage unificado, alta por dos vías.** El allowlist de
usuarios vive en el MISMO store que las rooms (`pkg/membership.Store`): SQLite en
single-node, JetStream KV replicado (`UNIBUS_users`) en cluster. El `Server` ya
tiene ese store privilegiado abierto (es quien sirve el KV en cada nodo), así que
expone `GET/POST /users` y `POST /users/{signpub}/revoke` como API HTTP admin-only,
simétrica con las rutas de rooms: el panel de administración firma como admin y el
server ejecuta la mutación contra el mismo store. El panel NO necesita `--db`, ni la
identidad interna, ni correr en un nodo del cluster; funciona idéntico en single-node
y cluster. La autorización es default-deny: solo un firmante que el store confirma como
`role == "admin"` activo pasa, cualquier otro recibe 403 (encima de la firma+nonce+TLS
ya existentes). La CLI `membershipd user add --store kv` sigue existiendo SOLO para
sembrar el admin #0 (bootstrap del huevo-gallina: sin un admin sembrado no hay quién
firme el primer `POST /users`); a partir de ahí toda la gestión es HTTP admin-only. El
alta es idempotente igual que la CLI: re-alta de una clave ya registrada = 409, sin
sobrescribir ni elevar rol; el revoke es un flip de status (sin hard-delete), auditable.
- **Identidad = secreto crítico.** El archivo de identidad (`worker.id`,
`chat.id`) contiene las claves privadas (Ed25519 + X25519). Se escribe 0600.
Perderlo = mensajes ilegibles, sin recuperación. Trátalo como una clave SSH.
- **Las rooms reciben un ULID fresco al crearse.** No hay "crear o unirse por
nombre": cada `CreateRoom` produce un room nuevo. Los peers demo cleartext
comparten el *subject* (NATS enruta por subject), así que worker→chat funcionan
aunque cada uno tenga su propio room id mapeado al mismo subject.
- **La media no viaja por el bus.** `PublishMedia` cifra, sube al object store y
publica solo un `BlobRef`. El receptor, si ve `Frame.Blob != nil`, descarga y
descifra con `FetchMedia`. El frame de media NO lleva payload inline (su nonce
vive en `BlobRef.Nonce`); `Subscribe` no intenta descifrar payloads vacíos.
- **Forward secrecy depende del rekey.** `Kick` rota `K` a un epoch nuevo y la
re-sella solo para los miembros restantes. El expulsado pierde acceso a los
mensajes publicados después del kick, pero conserva los anteriores (las claves
de epochs pasados no se borran: cifraban datos que ya podía leer).
- **NATS embebido escribe JetStream en disco.** `--nats-store` apunta a
`local_files/jetstream`; borrarlo resetea el historial persistido.
- **Build sin CGO.** Usa el driver `modernc.org/sqlite` (pure-Go) y el paquete
`cybersecurity` del registry compila limpio con `CGO_ENABLED=0`. NO requiere
`fts5` ni `gcc`.
## Convención de subjects
```
proc.<svc>.<canal> telemetría/coordinación de procesos (proc.test.ticks)
rpc.<svc> request/reply (rpc.indexer)
room.<grupo> chat humano/grupo (room.general)
agent.<nombre>.{in,out} inbox/outbox de agente LLM (agent.scout.in)
```
## Capability growth log
- v0.11.0 (2026-06-07) — flag dedicado `UNIBUS_NATS_MONITOR` que abre el endpoint
de monitoring HTTP del nats-server embebido (`127.0.0.1:8222`, loopback only) de
forma DESACOPLADA del debug-log. Antes el monitoring solo se abría con
`UNIBUS_NATS_DEBUG=1`, que además encendía el log verboso del nats-server
(rutas/RAFT/subjects a journald en claro) — incompatible con el endurecimiento
del issue 0007. El cómputo de los toggles se extrae a una función pura
`natsLogOpts(debugEnv, monitorEnv) (noLog, debug, trace, monitor)`: `MONITOR=1`
abre el endpoint dejando el log en silencio (`NoLog` true / `Debug` false), y se
mantiene el acoplamiento inverso por compatibilidad (`DEBUG` sigue implicando
`MONITOR`). El bind loopback `127.0.0.1` queda hardcoded — el monitoring NUNCA es
público y no lleva auth; lo lee un scraper local que empuja a VictoriaMetrics
(dashboard `unibus-nats` en `fleet_monitoring`). Se versiona el cableado de
deploy: drop-in systemd aditivo `membershipd-cluster.service.d/nats-monitor.conf`
(`Environment=UNIBUS_NATS_MONITOR=1`) + sección "NATS server metrics" en el
README del cluster con el runbook de activación rolling (magnus→homer→datardos)
y gate de reconvergencia R3 (`followers 2/2`) entre nodos. Tests nuevos: tabla
pura del desacoplamiento (monitor on ⇒ log NO debug; debug ⇒ monitor; default
cerrado) + server real con `MONITOR=1` que confirma `/varz` 200 en loopback:8222
y server sin flag con el endpoint cerrado. Cambios 100% aditivos: sin el flag el
comportamiento es idéntico; build/test verdes.
- v0.10.0 (2026-06-07) — API HTTP admin-only de gestión de usuarios, cerrando la
última asimetría del control plane: las rooms tenían superficie HTTP firmada
(`POST /rooms`, etc.) pero los users solo se gestionaban por CLI local o acceso
directo al store. Se añaden `GET /users` (lista completa, incluidos revocados),
`POST /users` (alta `{sign_pub, handle, role}`: valida hex de 64 chars + role en
`{admin, member}`, 409 idempotente que no sobrescribe ni eleva rol) y
`POST /users/{signpub}/revoke` (flip de status, sin hard-delete). Los tres pasan por
un helper `requireAdmin` default-deny que confirma contra el store que el firmante
autenticado es un user `role == "admin"` activo (el endpoint id es un hash one-way de
la clave, así que el contexto lleva ahora también el `sign_pub` hex del firmante para
resolver `GetUser`); cualquier otro firmante recibe 403, encima de la firma+nonce+TLS+
enforce ya heredadas del middleware. NO se abre conexión KV nueva ni se usa la identidad
interna: el server escribe vía su `s.store` privilegiado, el MISMO que las rooms (SQLite
single-node, KV `UNIBUS_users` en cluster). `pkg/client` gana `ListUsers/AddUser/RevokeUser`
(tipo plano `UserInfo`) firmando como admin, así la pestaña Users del panel deja de
necesitar `--db`/acceso KV directo. La CLI `membershipd user add --store kv` queda SOLO
para sembrar el admin #0 (bootstrap). La validación de `sign_pub` se unifica en
`membership.ValidateSignPubHex`, reusada por la CLI y los handlers. Tests nuevos:
no-admin → 403 en los tres endpoints, roundtrip admin add→list→revoke, y validación
(hex inválido → 400, role inválido → 400, re-alta → 409), más un test de cliente contra
un membershipd embebido. Cambios 100% aditivos: el comportamiento single-node y de las
rutas de rooms no cambia; vet/build/test verdes.
- v0.9.0 (2026-06-07) — cierre de los gaps que el despliegue del cluster (report
0011) dejó abiertos (report 0012). (GAP A) Nueva capability `membershipd user
add|list|revoke --store kv`: alta/baja de usuarios contra el KV replicado del
cluster EN MARCHA, sin el procedimiento de parar-sembrar-rearrancar. Usa la
conexión interna privilegiada — el daemon persiste su identidad de servicio con
`--internal-id-file` (cada nodo genera/carga la suya, 0600 junto a las claves TLS)
y la CLI, ejecutada por loopback en un nodo, presenta esa nkey que el
autenticador reconoce con permisos plenos de JetStream; ninguna identidad de
usuario normal puede tocar los buckets `KV_UNIBUS_*` bajo la ACL por-subject. El
alta es idempotente (re-alta de la misma clave = `ErrUserExists` explícito, sin
sobrescribir ni elevar rol), commitea con quórum 2/3 (HA, imprime
`followers_current`) y rechaza un destino remoto sin `--ca` (igual que
`migrate-to-kv`). (GAP B) Nuevo `cmd/clientcheck`: verificación end-to-end real
con un cliente autenticado (identidad operator, nkey+TLS+https) que crea una room
E2E, publica y recibe descifrado contra el cluster vivo, incluido un nodo parado a
media transmisión donde el cliente hace failover a un superviviente y sigue
recibiendo con cero pérdida (quórum 2/3) — el plano de datos que el chaos test del
0011 nunca probó. (GAP C) Runbook `deploy/cluster/README.md` corregido: el orden
de arranque "magnus solo y verifica healthz" deadlockeaba (un nodo solo no tiene
quórum del meta-group y nunca sirve healthz); se documenta el arranque por quórum,
que R1 es un SPOF inservible (ir directo a R3) y la nueva vía de alta con el
cluster vivo. La plantilla de deploy (unit + `deploy-cluster.sh`) emite ya
`INTERNAL_ID_FILE` y el flag. Verificado contra los 3 VPS reales (magnus + homer +
datardos); posture enforce+ACL+TLS+R3 intacta.
- v0.8.0 (2026-06-07) — completar y endurecer el cluster (issue 0006, fases
0006a0006g) que cierra los bloqueantes de la auditoría dedicada del cluster
(report 0008) y cablea el control plane descentralizado que 0003 dejó a medias.
(0006a) Se cablea el nonce replicado en el binario: un nodo con `--cluster-name`
usa el bucket JetStream KV compartido obligatoriamente (fail-fast si no se crea),
cerrando el replay cross-node (N3); el "ciclo bootstrap" se resuelve con una
identidad interna efímera que el authenticator reconoce (full perms) y una
conexión in-process privilegiada. (0006b) Se cierra la fuga del control plane
por `$JS.API.>` (N2): la ACL pasa a un allow-set cerrado por-room (JS API solo de
los streams `UNIBUS_<room>` del peer), dejando `KV_UNIBUS_*`/`OBJ_*` fuera del
set y, por tanto, denegados. (0006c) Se cablea el store KV descentralizado
(`--store kv|sqlite`, default sqlite = baseline idéntico) con un `storeHolder`
fail-closed que rompe el ciclo bootstrap del authenticator. (0006d) Posture
homogénea: un nodo rechaza unirse al cluster sin `enforce`, y `/healthz` publica
la posture (N1). (0006e) Todos los clientes llaman `RefreshSession` tras cambios
de membresía (N4), de modo que la ACL es usable bajo enforce sin desactivarla.
(0006f) Bajos: secreto de cluster fuera de argv (`--cluster-pass-file`/env +
inyección en routes), `migrate-to-kv` rechaza target remoto sin `--ca`, y docs
de CA separada para routes + R1 SPOF vs R3 HA. (0006g) Material de deploy del
cluster de 3 nodos (magnus+homer+datardos) en `deploy/cluster/` (certs, unit,
script de despliegue dry-run, runbook) — sin tocar ningún VPS. Toda la
regresión de auditorías previas + los ataques 0008 siguen verdes; govulncheck 0
alcanzables. Branch-by-abstraction: con `--store sqlite` el single-node sigue
idéntico y desplegable en todo momento.
- v0.7.0 (2026-06-07) — hardening de seguridad 2 (issue 0005, fases 0005a0005e)
que cierra los hallazgos nuevos de la re-auditoría red-team (report 0006) y
lleva el veredicto de exposición pública a "sí-con-condiciones". (0005a) Bump de
`github.com/nats-io/nats-server/v2` v2.10.22→v2.11.15 y de la toolchain a
go1.26.4: `govulncheck ./...` pasa de 16 vulnerabilidades alcanzables (14 del
servidor NATS embebido + 2 de la stdlib) a 0. (0005b) `client.processFrame`
ahora descarta cualquier frame sin firma en una room `SignMsgs` (antes verificaba
solo si la firma venía presente, lo que permitía suplantar `Sender` con
`Sig==nil`). (0005c) Nuevo limiter global de bytes en vuelo
(`pkg/membership.inflightLimiter`) que acota la memoria agregada que el control
plane bufferiza bajo concurrencia (el límite por-request y el rate-limit por-IP
no acotaban el total): un flood concurrente multi-IP se descarta con 503 en vez
de crecer sin techo (el RSS deja de escalar con N). (0005d) El guard de arranque
`validateBootConfig` ahora exige `--tls-cert/--tls-key` en bind no-loopback (un
control plane público sin TLS servía metadata en claro). (0005e) Se cablea por
fin en `membershipd` la ACL por subject que ya existía huérfana desde 0003e
(`busauth.NewNkeyAuthenticatorACL` + nuevo adaptador `busauth.PermissionsFromSubjects`
sobre `membership.SubjectACLFor`): un registrado no-miembro ya no puede
`Subscribe(">")` y captar los subjects/advisories de rooms ajenas. Residuales
documentados: `$JS.API.>` sigue compartido (cierre completo = NATS accounts por
identidad, diferido) y los clientes deben `RefreshSession` tras cambios de
membresía (chat/worker aún no lo hacen). El comportamiento de un solo nodo no
cambia y master sigue verde.
- v0.6.0 (2026-06-07) — descentralización / alta disponibilidad (issue 0003,
fases 0003a0003e), report 0006. El servidor NATS embebido gana soporte de
cluster con routes autenticadas (secreto de cluster) y TLS mutuo de nodo
(`pkg/embeddednats.ClusterConfig` + `busauth.RouteTLSConfig`, reusando la CA
del 0001). El control plane (`pkg/membership.Store`) pasa a interfaz por
branch-by-abstraction: `sqliteStore` (default) + `jetstreamStore` nuevo sobre
JetStream KV replicado (réplicas configurables R1→R3), con `IsAuthorized`
fail-closed ante pérdida de quorum. `membershipd migrate-to-kv` mueve el
estado SQLite→KV de forma idempotente con backup previo. Los blobs
(`pkg/blobstore.Store`, ahora interfaz) ganan un backend NATS Object Store
replicado además del disco. El cliente acepta listas de seeds NATS y de
control planes con failover/reconnect nativo, el anti-replay pasa a un store
de nonces compartido en KV con TTL (cierra el agujero de replay multi-nodo), y
se implementa la ACL por subject derivada de pertenencia (audit H4 residual:
`busauth.NewNkeyAuthenticatorACL` + `membership.SubjectACLFor` +
`client.RefreshSession`). Todo viaja detrás del flag `decentralized` (off):
el comportamiento de un solo nodo (SQLite + disco) no cambia y master sigue
verde. El despliegue multi-nodo real (0003f) lo ejecuta el humano.
- v0.5.0 (2026-06-07) — hardening de seguridad (issue 0004) que cierra los
hallazgos de la auditoría red-team (report 0004) y lleva el veredicto de
exposición pública de "NO" a "sí-con-condiciones". Anti-DoS pre-auth
(`http.MaxBytesReader` por ruta + rechazo por `Content-Length` + rate-limit
por IP + `MaxHeaderBytes`); guard de fail-open que prohíbe arrancar con bind
público o TLS sin `--bus-auth enforce`; autorización por pertenencia en los GET
de room (metadata y clave sellada solo para miembros / el propio endpoint);
rooms cleartext deshabilitadas en bind público (contenido siempre E2E, mínimo
defensivo del data plane mientras la ACL por subject llega con 0003); TLS en el
control plane HTTP con la CA propia y cliente que exige `https` cuando hay CA;
y los medios H6/H7/H12 (owner ligado al firmante, `IsAuthorized` antes del
nonce-cache con poda O(expired) + cap, errores genéricos al cliente). Cada
hallazgo lleva su test adversarial `TestAudit_*` portado como regresión.
- v0.4.0 (2026-06-07) — descubrimiento de rooms: `GET /members/{endpoint}/rooms`
lista las rooms de un endpoint con su metadata y rol, y `client.ListMyRooms()`
lo consume. El control plane es pull (no hay push de invitaciones), así que un
peer recién invitado a una room cifrada la descubre por polling y luego hace
`Join` + `Subscribe`. Pieza base para que los bots de `agents_and_robots`
hablen por el bus en vez de Matrix (modelo "todo son rooms", E2E).
- v0.3.0 (2026-06-06) — `membershipd` se convierte en service de verdad: flag
`--bind` (default 127.0.0.1) que gobierna a la vez el HTTP de control y el NATS
embebido (`embeddednats.StartHost`), de modo que con `--bind 0.0.0.0` un
teléfono o PC de la LAN conecta a ambos planos. Se añade un systemd-user unit
(`deploy/unibus-membershipd.service`, `Restart=always`) + `deploy/install.sh`
idempotente, y el bloque `service:` queda completo (systemd-user, restart
always, health `/healthz`). El `Frame` (pkg/frame) gana threading aditivo
(`ThreadID`, `ReplyTo`) y un tipo `REACT`, con `PublishReply`/`React` en el
cliente — la base para que bots de chat hablen por el bus (fase 2). Cambios
100% aditivos: el wire de los frames no-threaded es idéntico y los tests
existentes siguen verdes.
- v0.2.0 (2026-06-03) — el playground gana un benchmark de rendimiento
(`GET /api/bench`, SSE): un publisher inunda una room con miles de mensajes a
N subscribers y una gráfica en vivo anima el throughput. Expone las dos
políticas como flags independientes (JetStream/`Persist` y encriptación
E2E/`Encrypt`) más tamaño de payload, de modo que se mide el coste de cada
capa (core NATS vs JetStream vs E2E vs E2E+JetStream) usando la librería
cliente real, sin reimplementar nada.