dec-0131

Vista generada de dec-0131: Federación entre servidores: pares emparejados, concesiones acotadas que se canjean por SCT, contenido remoto como `SourceBinding` y Jellyfin como importación y sincronización

ImplementadoSin versión del tren todavía· generada desde docs/decisions/dec-0131-federacion-entre-servidores.md
track/docsdec-0124track/docs:DC10

Vista generada desde docs/decisions/dec-0131-federacion-entre-servidores.md. No se edita a mano: bun run docs:gen la regenera y bun run docs:check falla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.

CampoValor
EstadoPROPOSED
Fecha2026-10-01
Ficherodocs/decisions/dec-0131-federacion-entre-servidores.md

Por qué importa (del frontmatter del ADR):

Fija qué significa federar en Styx cuando hay dos servidores de dueños distintos (o dos instalaciones independientes del mismo dueño) y cómo se relaciona con Jellyfin: identidad de servidor por clave Ed25519 emparejada fuera de banda, peticiones servidor a servidor firmadas (RFC 9421), concesiones de biblioteca a un par acotadas, caducables, revocables y no recompartibles que viven en el servidor que concede, intercambio de cada concesión por SCT v1 de sesión sin cambiar su formato, contenido remoto como un SourceBinding más del catálogo federado de r08 (kind remote-styx) emparejado por huella de contenido e ids externos, feed de cambios por biblioteca con cursor, estado de usuario que no sale nunca de su servidor de origen, y la importación y sincronización con Jellyfin (usuarios como invitaciones, progreso, favoritos y bibliotecas) como relaciones del adaptador de dec-0035. Sin este ADR un executor mezclaría tres sentidos de "federado" (fuentes de r08, nodos de dec-0031 y servidores ajenos), crearía cuentas cruzadas o tokens largos que el daemon no sabe revocar, o abriría NATS entre servidores de dueños distintos.

Nodos del roadmap que lo citan en refs: ninguno.

Páginas de la documentación que lo citan: Servidores conectados y Jellyfin (especificado)

Texto del ADR

Leído de docs/decisions/dec-0131-federacion-entre-servidores.md, el fichero canónico.

dec-0131 — Federación entre servidores: pares emparejados, concesiones acotadas que se canjean por SCT, contenido remoto como SourceBinding y Jellyfin como importación y sincronización

  • Fecha: 2026-10-01
  • Estado: PROPOSED. No es un lock. waxin lo lockea vía AskUserQuestion, con las preguntas de §15 resueltas o diferidas de forma explícita. Hasta entonces no se escribe código de producción que dependa de él. El nodo outcome/server-federation es una alta propuesta queued en el model, pendiente de ratificar en P1 (§15).
  • Dirección de waxin (2026-10-02, punto 16 de sus decisiones; no lockea el ADR): ambas cosas: nodos propios con una sola biblioteca lógica y pares independientes. En el model, outcome/server-federation gana los milestones own-nodes y peers. La federación queda fuera de los alfas (excludes de alpha.1, dec-0133).
  • Propone: workflow de visión (rama w10/adr-federacion, desde w10/vision). Número reservado para este workflow: dec-0127 modelo de datos, dec-0128 dispositivos, dec-0129 transferencias, dec-0130 modo público y plataforma, dec-0131 federación (este). Los cuatro primeros se escriben en paralelo; este ADR los cita por número y no por contenido.
  • Dirección de waxin que cumple (2026-10-01, N6, docs/overview/vision-ledger.yml V-N06): "la federacion me suena q tambien hablada pero me cuadra tu idea y la tomo, ya que asi el poder tenr la misma en varios styx, o en styx y jellyfin, o poder sincronizar bibliotecas importarlas etc". La "idea" que waxin toma es la propuesta que se le hizo en la pregunta: compartir con amigos entre servidores con permisos acotados y caducables (SCT), sin cuentas cruzadas. La cita del texto de esa opción no está en las transcripciones guardadas; la formulación de N6 que da el workflow es la fuente.
  • Items del ledger que cubre: V-N06 (principal), V-PROD-07 (Jellyfin consumible, sustituto y no fuente), V-ARCH-13 (nodos first-class: aquí se separa nodo de par). Toca sin cerrar: V-N01 (metadatos en el fichero: §6.3), V-N07/V-N10 (descargas y offline de contenido compartido: §7.4, lo cierra dec-0129), V-N08 (modo público: §2.3, lo cierra dec-0130), V-UX-01 (el usuario "va a entender 0" de "federado": §2.1 y §11).
  • Cita:
    • r01: los bytes de vídeo no atraviesan JS ni NATS. Tampoco entre servidores.
    • r04: SeekableMediaSource. Un servidor remoto es una fuente más (§6.2).
    • r08: catálogo federado Work → Edition → MediaAsset → SourceBinding. Aquí "federado" significa varias fuentes por asset dentro de un servidor; este ADR lo reutiliza, no lo redefine (§2.1).
    • r16: frontier salvo blocker (§4.4 explica por qué la concesión no es un token delegable).
    • r17: un contenedor por authority. Este ADR no crea servicio nuevo (§8, P6).
    • r25: Federated Media Runtime. Sus "nodos federados" son de un mismo dueño.
    • r28: cada pieza termina en un consumidor real (§13).
    • dec-0031/dec-0032: Styx Node first-class y NATS leaf hub-and-spoke del mismo dueño. Este ADR no los toca y prohíbe extender NATS a un par (§3, regla 2).
    • dec-0035: Jellyfin sustituto, no fuente, con cuatro relaciones (importador, SourceBinding remoto, facade de API, exportador). Este ADR detalla 1, 2 y 4 para N6 (§9).
    • dec-0090: handoff con autoridad única. Un handoff entre servidores queda fuera (§14).
    • dec-0114: huella de contenido. Es la identidad que empareja assets entre servidores (§6.3).
    • dec-0117 (LOCKED): SCT, fronteras B1/B2, I1/I2. protocols/capability-token/SCT_SPEC.md: SCT v1 de 204 bytes, aud = nodo, sid abierto por playback-svc, vida ≤ 600 s.
    • dec-0118 (LOCKED): BFF, CORS, audit. dec-0119/dec-0120: spire y BUS_ROUTES.
    • dec-0121 (LOCKED): ingesta con conduit; la importación de bytes entre servidores la usa.
    • dec-0124 (PROPOSED): la página explicacion/federacion (estado especificado).
    • dec-0125 (PROPOSED): cuentas, hogares, concesiones de biblioteca (library_grants), invitaciones, permiso efectivo por intersección. Este ADR añade un tipo de beneficiario (§5.2).
    • dec-0126 (PROPOSED): metadatos en el fichero; su §13 deja fuera "la federación de ficheros con artwork", que cae aquí (§6.3).

1. Contexto: qué existe hoy

PiezaEstadoDónde
Catálogo con varias fuentes por asset (SourceBinding)existe el modelopackages/domain, r08
SCT v1 de sesión: firma playback-svc, verifica el daemonexisteprotocols/capability-token/SCT_SPEC.md, spire.cap
Concesiones de biblioteca a cuenta u hogarpropuestodec-0125 §4.1, tabla library_grants
Huella de contenidopropuesta, 0 hits en códigodec-0114 §3, dec-0126 §1.2
Metadatos en el ficheropropuestodec-0126
Nodos del mismo dueño, NATS leafdecidido, sin código (track/node-foundation queued)dec-0031, dec-0032
Integración Jellyfindecidida en cuatro relaciones, sin código (integrations/jellyfin/ vacío salvo README)dec-0035, outcome/jellyfin-compat queued
Servidor a servidor entre dueños distintosno existe ni en papeleste ADR
Dueño del estado por perfil (progreso, vistos, favoritos)sin dueñodec-0125 §19 lo deja abierto

Dos consecuencias:

  1. La palabra "federado" ya tiene dos sentidos lockeados en el repo (r08 fuentes, r25/dec-0031 nodos) y waxin pide un tercero. Sin separarlos, un executor colgaría la federación de outcome/federated-storage (que es storage, r26) o de track/node-foundation (que es un solo dueño y un solo bus).
  2. La sincronización de progreso con otro Styx o con Jellyfin no tiene contra qué sincronizar hasta que el estado por perfil tenga dueño (P5).

2. Vocabulario y alcance

2.1 Tres cosas distintas

Término en este repoQué esConfianzaADR
Catálogo federadoUn asset con varias fuentes (local, Storage Box, torrent, remoto)un servidorr08
NodoProceso de un mismo despliegue en otra máquina (home, VPS, NAS)un dueño, un bus NATS, un catálogodec-0031, dec-0032
Par (peer)Otro servidor Styx con su dueño, su identity, su catálogo y su busninguna por defecto; la concesióneste ADR
Adaptador JellyfinUn servidor Jellyfin al que Styx habla por su APIla de una API key de Jellyfindec-0035 + este ADR §9

En la UI la palabra "federación" no aparece (V-UX-01). El usuario ve "Servidores conectados" (pares), "Compartido contigo" (lo que otros le conceden) y "Importar de Jellyfin". "Federación" queda como término de arquitectura y de la doc de explicación.

2.2 Los cuatro casos de N6

#CasoMecanismo
F1La misma biblioteca en varios Styx del mismo dueño, una sola biblioteca lógicaNodos (dec-0031), no pares. Este ADR no lo cambia; sólo lo señala (§3, regla 1).
F2La misma biblioteca en varios Styx independientes (otro sitio, otra instalación)Par con concesión de biblioteca completa y vínculo de cuenta opcional (§5, §7.3).
F3Compartir con amigos entre servidores, sin cuentas cruzadasPar con concesión acotada y caducable, canjeada por SCT de sesión (§4, §5, §6).
F4Styx ↔ Jellyfin: importar usuarios, progreso y bibliotecas, sincronizar mientras convivanRelaciones 1, 2 y 4 de dec-0035, detalladas en §9.

Y una operación transversal: importar una biblioteca de un par (copiar catálogo y, opcionalmente, bytes) para dejar de depender de él (§7.5).

2.3 Fuera de alcance

  • Modo público (registro abierto, sin registro, web pública): dec-0130. Un servidor público puede además ser par; se decide allí.
  • Watch party: waxin, 2026-10-01: "watchparty d momento no".
  • Handoff y mando remoto entre dispositivos: dec-0128. Entre servidores distintos, no (§14).
  • Descargas y offline de contenido compartido: la política la da la concesión (§5.1); el mecanismo es dec-0129.
  • ActivityPub / fediverso público: no se adopta (§10, alternativa A5).

3. Decisión (resumen)

  1. Mismo dueño y una biblioteca = nodos. Si lo que se quiere es una biblioteca lógica en varias máquinas del mismo dueño, se usan nodos (dec-0031). Un par es otro servidor con su propio catálogo y su propio dueño, aunque el dueño sea la misma persona (F2).
  2. NATS nunca cruza entre pares. El bus de un servidor es suyo (dec-0032, BUS_ROUTES, ACL por servicio). Entre pares sólo hay HTTP firmado (§4.3) y bytes por el data plane (§6.2).
  3. Identidad de servidor = clave Ed25519. peerId = base32(sha256(clave pública)), al estilo de los device IDs de Syncthing. Se empareja fuera de banda con un enlace o QR de un solo uso (§4.2); no hay directorio central ni descubrimiento abierto.
  4. Peticiones servidor a servidor firmadas con HTTP Message Signatures (RFC 9421), Ed25519, con created, nonce y caché de replay, sobre HTTPS/HTTP3 (§4.3).
  5. La concesión vive en el servidor que concede. Es una fila (estado en el servidor), no un token portador delegable: acotada (bibliotecas o colecciones, permisos, calidad, concurrencia, ancho de banda), con caducidad obligatoria, revocable al instante y no recompartible (§5).
  6. La concesión se canjea por SCT v1 de sesión. El par pide una sesión al servidor que concede; su playback-svc la abre y firma una SCT con actor = fed:<peerId>:<handle>. El formato SCT no cambia y el daemon no aprende nada nuevo (§6.1).
  7. Sin cuentas cruzadas. El amigo no tiene cuenta en el servidor que concede. Su servidor atestigua un handle seudónimo y estable por concesión; el que concede audita, limita y revoca por handle sin saber quién es (§5.3).
  8. El contenido remoto es un SourceBinding más (kind remote-styx) en el catálogo del par que recibe, emparejado con lo local por huella (dec-0114) e ids externos (dec-0126). Si el amigo ya tiene la película, no se duplica: gana otra fuente (§6.3).
  9. Bytes por relay del servidor del amigo por defecto. El cliente del amigo sólo habla con su servidor; el daemon de ese servidor lee del par como SeekableMediaSource remota con SCT (§6.2). Directo cliente → par como optimización (P3).
  10. El estado de usuario no sale de su servidor. Progreso, vistos, favoritos y listas del amigo viven en su servidor. Entre servidores sólo viaja metadato público de la obra (el mismo que va dentro del fichero, dec-0126) y lo que la concesión declare (§7).
  11. Sincronización por feed de cambios con cursor, por biblioteca concedida, con notificación firmada opcional para bajar latencia. La detección barata usa el hash de metadatos que N1 pide guardar en la base (§7.1).
  12. Jellyfin: importador (usuarios como invitaciones de dec-0125, progreso, favoritos, bibliotecas), sincronización bidireccional de progreso mientras convivan, SourceBinding remoto y exportador (§9). Jellyfin no es par: no firma, no concede, no recibe SCT.

4. Identidad y confianza entre servidores

4.1 Documento de servidor

Cada servidor publica en GET /.well-known/styx/server (sin autenticar, cacheable):

{
  "peerId": "b32:…",
  "name": "Casa de waxin",
  "keys": [{ "kid": "…", "alg": "ed25519", "pub": "…", "notAfter": "…" }],
  "endpoints": { "fed": "https://styx.example/fed/v1" },
  "protocol": { "fed": [1] },
  "software": { "train": "…" }
}
  • keys admite actual y siguiente (rotación, igual que STYX_SCT_PUBLIC_KEYS). La clave de servidor es distinta de la de firma de SCT (A4 de dec-0117 no se reutiliza: separar propósito).
  • El documento va firmado por la clave del propio peerId (autocertificado, como las claves de servidor de Matrix). Un cambio de clave sin firma de la anterior rompe el emparejamiento y pide reemparejar (es la señal de compromiso o reinstalación).

4.2 Emparejamiento

  1. El dueño (o un admin con peer:manage) de A crea una invitación de par: enlace o QR con peerId de A, endpoint, código de un solo uso (≥ 128 bits), caducidad (por defecto 24 h) y, si se crea junto con una concesión, su id.
  2. El dueño de B la abre en su propio Styx. B descarga el documento de A, comprueba que el peerId coincide con el hash de la clave y presenta el código en una petición firmada con su clave.
  3. A comprueba código, caducidad y uso único (atómico, mismo patrón que el canje de invitación de dec-0125 §7.3), registra B con su clave fijada y responde firmado.
  4. Ambos muestran el código de verificación corto derivado de las dos claves (estilo SAS) por si el enlace viajó por un canal no fiable. Confirmarlo es opcional y queda auditado.

El amigo sin servidor propio no es un par: para él existe la invitación de cuenta de dec-0125, que sí crea una cuenta en A. Este ADR no inventa un tercer camino.

4.3 Peticiones servidor a servidor

  • HTTP Message Signatures (RFC 9421) con Ed25519. Componentes firmados: @method, @target-uri, content-digest (RFC 9530) cuando hay cuerpo, y los parámetros created, keyid, nonce.
  • Ventana de created ± 30 s (la misma tolerancia que la SCT), caché de nonce acotada y pre-reservada que rechaza al llenarse (el patrón ReplayCacheFull de la SCT).
  • Respuestas también firmadas: B no acepta un catálogo o una SCT que no venga firmada por la clave fijada de A.
  • El endpoint fed/v1 sólo existe si STYX_FEDERATION=enabled (apagado por defecto), escucha detrás del mismo borde que el resto de la API (dec-0118), con rate limit por peerId y 403 genérico sin enumerar (como H3 en la SCT).
  • SSRF: al emparejar, B sólo hace GET al host de la invitación, sin seguir redirecciones a otro host, con resolución que excluye rangos privados salvo STYX_FEDERATION_ALLOW_PRIVATE (pares en LAN) y con tamaño y plazo acotados.

4.4 Por qué la concesión no es un token delegable (Biscuit, UCAN)

Biscuit (bloques Ed25519 con Datalog, atenuación offline) y UCAN 1.0 (cadenas de delegación con DID) son el estado del arte de capabilities delegables. Se estudiaron y no se adoptan en v1:

  • N6 pide caducable y revocable; la delegación offline hace la revocación un problema distribuido (listas de revocación que el verificador tiene que consultar). Con la concesión en la fila del que concede, revocar es borrar la fila y cerrar sesiones.
  • N6 y la práctica de Plex (los usuarios no pueden recompartir un servidor compartido) piden no recompartir. La atenuación offline existe precisamente para recompartir.
  • El verificador de bytes ya existe y es mínimo: la SCT v1 de longitud fija verificada por spire.cap en Zig. Ni Biscuit ni UCAN tienen implementación Zig, y meter Datalog o DAG-CBOR en el daemon contradice dec-0117 (parsers mínimos, fuzz).
  • Cada canje ya pasa por el que concede (para abrir la sesión, I2 de dec-0117): no hay camino offline que ganar.

Queda como evolución (P4): si un día un par tiene que operar sin conexión con el que concede (descargas offline de contenido compartido, dec-0129), la concesión podría emitirse además como Biscuit atenuable verificado sólo en TS. No cambia el daemon.

5. Concesiones

5.1 Qué es una concesión

CampoContenido
grantIdid opaco
peerIdpar beneficiario
principalsany (cualquier usuario del par que su dueño autorice) o lista de handle (§5.3)
scopebibliotecas, colecciones o works concretos
permissionssubconjunto de browse, play, download, import (§7.5)
constraintscalidad o bitrate máximos, transcodificación permitida o no, streams simultáneos por concesión y por handle, ancho de banda, bytes/mes
maturitytecho de clasificación (el del par se interseca, nunca se amplía)
expiresAtobligatoria. Máximo configurable (STYX_FEDERATION_GRANT_MAX_TTL, P2)
resharesiempre false. No es un campo configurable; está para que el contrato lo diga
createdBy, createdAt, revokedAt, revokedByaudit

5.2 Encaje con dec-0125

  • Una concesión de par es una fila de library_grants con grantee_kind = peer y una tabla hija peer_grants con los campos de §5.1. Se propone como enmienda a dec-0125 §4.3, que entra con el lock de los dos.
  • Crear o ampliar una concesión exige peer:grant (owner o admin) y que quien concede tenga a su vez esas bibliotecas (dec-0125 §5.3: nadie concede lo que no tiene), con auth_time reciente.
  • El permiso efectivo de un handle remoto en A es concesión ∩ restricciones de A, y en B es además ∩ lo que el dueño de B permita a ese usuario (B no puede ampliar lo que A dio).

5.3 Sin cuentas cruzadas: el handle

  • B atestigua en cada petición de sesión qué usuario local la hace, como handle = base32(HMAC-SHA256(clave por concesión de B, accountId ‖ profileId))[0..16].
  • Es estable dentro de una concesión (A puede limitar, auditar y revocar por handle) y no correlacionable entre concesiones ni entre pares.
  • A nunca recibe nombre, email ni id real. Si el dueño de B quiere que A vea un nombre, lo pone como displayHint voluntario.
  • A confía en la atestación de B dentro de los límites de la concesión: un B malicioso puede mentir sobre qué usuario es, pero no superar los topes de la concesión entera. Por eso los topes de §5.1 existen también a nivel de concesión, no sólo de handle.

5.4 Revocación y caducidad

  • Revocar (o caducar) marca la fila, emite evt.federation.grantRevoked en el bus de A y A cierra por IPC las sesiones abiertas con actor = fed:<peerId>:* de esa concesión. La SCT ya garantiza que cerrar la sesión corta MoQT y H3/WT en la siguiente petición (SCT_SPEC, "Alcance de la revocación").
  • A notifica a B (firmado). B marca los SourceBinding remote-styx de esa concesión como no disponibles; no borra Works ni estado de usuario.
  • Romper el emparejamiento revoca todas las concesiones en ambos sentidos.

6. Reproducir contenido de un par

6.1 Canje por SCT (sin cambiar SCT v1)

cliente del amigo ──(BFF de B)──▶ playback-svc B
                                   │ POST fed/v1/sessions (firmada RFC 9421)
                                   │ { grantId, handle, assetRef, plan }
                                   ▼
                                 fed/v1 de A ──▶ identity A (concesión) ──▶ playback-svc A
                                                                          │ abre sesión por IPC (I2)
                                                                          │ SCT v1: aud=nodo A,
                                                                          │ actor=fed:<peerId>:<handle>,
                                                                          │ resource=asset, scope=read
                                   ◀──────────── { sessionId, sct, endpoints, exp } (firmada)
  • actor_id = "fed:" ‖ peerId ‖ ":" ‖ handle; el digest de actor de la SCT ya separa dominios, así que un actor federado nunca colisiona con una cuenta local.
  • assetRef es un id opaco por par (A no expone sus assetId internos ni {rootId, relPath}, dec-0117 I7). Evita correlación entre pares.
  • La vida de la SCT sigue siendo ≤ 600 s. Mientras no exista renovación (SEC-Z13), B pide sesión nueva al caducar; el plan de dec-0129 y la renovación de SEC-Z13 lo resuelven para los dos casos a la vez.

6.2 Camino de bytes

  • Relay por B (por defecto): el daemon de B abre una fuente RemoteStyxSource (SeekableMediaSource, r04) que lee de A por HTTP/3 con rangos y la SCT. El cliente del amigo sólo habla con B: misma sesión, mismo CSP, mismo CORS, mismo progreso. B puede servir desde su caché (r12) a varios usuarios suyos dentro de la concurrencia que A concede y puede remuxar o empaquetar con su byte runtime. Los bytes nunca pasan por JS (r01).
  • Directo cliente → A (optimización, P3): B devuelve al cliente los endpoints y la SCT de A; el navegador conecta por WebTransport/H3 a A. Ahorra el doble salto, pero exige que A admita el origen web de B (enmienda de CORS por par sobre dec-0118) y expone la IP del amigo a A.
  • Si A está detrás de CGNAT, ninguno de los dos funciona sin su borde (delivery-edge o el VPS hub de dec-0032). Es la misma restricción que tiene hoy el acceso remoto de cualquier cliente; este ADR no la resuelve (P7).

6.3 Emparejar con lo que ya hay

  • Lo que A comparte llega a B como proyección de catálogo: metadatos públicos de la obra, ids externos (IMDB, TMDB, TVDB2), huella de contenido (dec-0114, sobre payload según la enmienda de dec-0126), facts técnicos y artwork por hash.
  • B empareja: misma huella → mismo MediaAsset, se añade un SourceBinding remote-styx; mismos ids externos y otra huella → mismo Work/Edition, otro MediaAsset (otra calidad); nada → Work nuevo marcado como "de ". El selector de fuentes de r08 ya sabe elegir entre local y remota.
  • Los metadatos en el fichero (N1, dec-0126) hacen la proyección reconstruible: lo que A manda es lo mismo que va dentro del fichero, y si B importa los bytes (§7.5) el fichero ya lleva su metadato. El artwork se sirve por hash de contenido y B lo cachea como derivado (el modelo de dec-0127).

7. Sincronización e importación

7.1 Catálogo: feed de cambios

  • GET fed/v1/libraries/:grantScope/changes?cursor=… devuelve eventos ordenados (assetAdded, assetRemoved, metadataChanged{metaHash}, artworkChanged{hash}) y un cursor nuevo. Patrón de Syncthing (índice con versiones) y del /sync de Matrix, sin su complejidad: aquí el flujo es unidireccional (A publica, B consume).
  • Primera sincronización = feed desde cursor vacío, paginado.
  • metaHash es el hash de metadatos que N1 pide guardar en la base para detectar cambios barato: B sólo pide el detalle de lo que cambió.
  • A puede empujar una notificación firmada "hay cambios" a B (webhook); B sigue tirando del feed. Sin la notificación, B sondea con intervalo acotado.

7.2 Estado de usuario: no viaja en F3

En F3 (amigos) el progreso del amigo vive en B. A no lo ve ni lo guarda. A sólo guarda contadores de uso por handle para topes y audit, con retención acotada.

7.3 F2: vínculo de cuenta entre servidores del mismo dueño

Si la misma persona tiene cuenta en A y en B (dos instalaciones independientes), puede vincularlas con su consentimiento en los dos lados (aprobación con auth_time reciente en ambos). El vínculo permite replicar su estado por perfil (progreso, vistos, favoritos):

  • registro LWW por (perfil, itemKey) con reloj lógico híbrido (HLC); itemKey = huella si hay, si no ids externos + temporada/episodio;
  • "visto" es monótono salvo desmarcado explícito, que viaja como lápida con HLC posterior;
  • no es una cuenta cruzada: son dos cuentas, cada una en su identity, que acuerdan replicar.

Depende de que el estado por perfil tenga dueño (P5).

7.4 Descargas y offline

download en la concesión permite pedir el fichero (o una variante generada al vuelo, N7) para reproducir sin conexión. El mecanismo (conduit, reanudación, generación al vuelo, caducidad offline) es dec-0129. Lo que fija este ADR: una descarga de contenido compartido cuenta contra los topes de la concesión y su caducidad offline no supera expiresAt.

7.5 Importar una biblioteca de un par

import en la concesión permite a B copiar contenido de A para dejar de depender de él:

  • catálogo: desde el feed (§7.1);
  • bytes: por conduit (dec-0121) hacia la raíz de ingesta de B, reanudable, con la misma SCT de scope read en el lado de A y scope ingest en el lado de B;
  • el fichero importado lleva sus metadatos dentro (N1), así que entra en el catálogo de B sin re-scan ni enriquecimiento.

Importar es la forma de mover una biblioteca entre instalaciones (F2) y de "llevarse" lo que un amigo concede con permiso explícito. Sin import, nada se copia de forma persistente salvo la caché derivada de B, que se purga al revocar.

8. Dónde vive cada pieza (r17)

PiezaDueño
Pares, claves fijadas, invitaciones de par, concesiones, handleidentity-svc (authority de credenciales y concesiones, dec-0087, dec-0125)
Endpoint fed/v1 (verificación RFC 9421, rate limit, replay)borde HTTP de la API (el gateway de W2 que cierra el ADR headless); librería pura @styx/federation
Proyección de catálogo y feed de cambioscatalog-svc
Sesiones para actores federados, SCTplayback-svc
RemoteStyxSourcedaemon (byte runtime), detrás de SeekableMediaSource
Adaptador Jellyfinintegrations/jellyfin/ sobre el plugin-sdk (dec-0033, r48), dentro de outcome/jellyfin-compat

No se crea federation-svc (P6): no hay authority nueva, sólo un tipo de beneficiario y un endpoint.

9. Jellyfin

dec-0035 fija cuatro relaciones. N6 pide "la misma en varios styx, o en styx y jellyfin, o poder sincronizar bibliotecas importarlas". Se detallan así:

9.1 Importador (relación 1)

QuéCómo
BibliotecasCada biblioteca de Jellyfin propone una raíz y una biblioteca de Styx. Las rutas se mapean si el daemon ve el mismo disco; si no, se ofrece SourceBinding remoto (9.2) o importar bytes por conduit.
ItemsEmparejados por huella cuando el fichero es accesible, y por ids de proveedor (ProviderIds) y ruta como apoyo, como hacen las herramientas de sincronización de vistos entre Jellyfin, Plex y Emby.
Metadatos y artworkNFO y artwork de Jellyfin como importación (dec-0126): pasan por el plugin de enriquecer/normalizar y, si la raíz lo permite, se escriben en el fichero (N1). Jellyfin no es fuente de verdad.
UsuariosCada usuario de Jellyfin propone una invitación de dec-0125 con sus bibliotecas ya concedidas. No se migran passwords (Jellyfin usa password local; Styx es passwordless por defecto).
Progreso, vistos, favoritosDesde UserData de Jellyfin (PlaybackPositionTicks, Played, PlayCount, IsFavorite, LastPlayedDate), aplicado al perfil de la cuenta que canjee la invitación.
Colecciones y listasComo colecciones de Styx de la cuenta que las importa.

El importador es idempotente y reanudable (cursor por biblioteca y por usuario) y deja un informe: emparejados, ambiguos (a revisión), no encontrados.

9.2 Convivencia: SourceBinding remoto y sincronización (relación 2)

  • Mientras waxin tenga Jellyfin funcionando (su biblioteca del Storage Box), Styx puede leer esa biblioteca como RemoteJellyfinSource y sincronizar progreso en los dos sentidos con una API key de Jellyfin y un mapeo de usuarios.
  • Reglas: LWW con la fecha de Jellyfin (LastPlayedDate) frente al HLC de Styx; "visto" monótono salvo desmarcado; nunca se escribe en Jellyfin nada que no sea UserData (sin metadatos, sin borrados).
  • La sincronización con Jellyfin es un plugin desactivable, no core. Se apaga sola al terminar la migración.

9.3 Exportador (relación 4)

Con N1 el exportador es casi gratis: el fichero ya lleva sus metadatos y su portada. Exportar añade, si se pide, NFO y artwork al lado para Jellyfin, y el progreso por usuario a su API.

9.4 Lo que Jellyfin no es

No es par. No tiene identidad de servidor, no firma, no concede ni canjea SCT. La facade de API (relación 3, Compatibility Profiles) es otra cosa: clientes de Jellyfin hablando con Styx.

10. Alternativas consideradas

#AlternativaPor qué no
A1Cuentas cruzadas (el amigo tiene cuenta en cada servidor)Es lo que N6 descarta. Además duplica passkeys, invitaciones y estado por servidor.
A2Directorio central (modelo plex.tv)Plex comparte libraries a cuentas de plex.tv: un tercero ve quién comparte con quién y su caída corta el acceso. Contradice "owned e2e".
A3NATS entre servidores (leaf o gateway hacia el par)Mezcla buses de dueños distintos, rompe BUS_ROUTES y las ACL por servicio, y saca el bus de su dominio de confianza (dec-0032).
A4Concesión como token portador largo (JWT, Biscuit, UCAN)Revocación difícil, recompartir posible, verificador nuevo en el daemon. §4.4. Biscuit queda como evolución sólo en TS (P4).
A5ActivityPub (seguir servidores como PeerTube)Pensado para difusión pública; su modelo es "seguir y recibir todo", no conceder con topes y caducidad. Sus firmas HTTP aún no han migrado a RFC 9421. Útil como referencia para modo público (dec-0130).
A6mTLS con certificados fijados (Syncthing)Buena identidad, mala con proxies inversos y bordes que terminan TLS (Coolify, delivery-edge). Se toma su peerId = hash(clave), no el transporte.
A7Replicar bytes siempre (redundancia de PeerTube)Para F3 es copiar la biblioteca de un amigo sin su permiso de import. Queda como opción explícita (§7.5), no como comportamiento por defecto.

Referencias consultadas: RFC 9421 (HTTP Message Signatures) y su adopción en el fediverso; especificación de Biscuit (Ed25519, atenuación offline con Datalog); UCAN 1.0-rc (delegación e invocación); ayuda de Plex sobre compartir servidores (sin recompartir, cuentas de plex.tv); redundancia de PeerTube; JellyPlex-Watched y WatchState (sincronización de vistos entre Jellyfin, Plex y Emby por ids de proveedor y nombre de fichero); DTO UserItemDataDto de la API de Jellyfin; device IDs de Syncthing. De ninguno se toma código: son referencias de diseño (regla sin GPL; la licencia de cada uno se comprueba si algún día se quisiera reutilizar algo).

11. Contratos a añadir (sin implementar)

  • protocols/federation/ nuevo: spec humana de fed/v1 (documento de servidor, emparejamiento, sesiones, feed, notificaciones, errores), schemaVersion, política de compatibilidad y vectors.json con firmas RFC 9421 válidas y alteradas (regla de protocols/CLAUDE.md).
  • @styx/api-contracts: PeerSchema, PeerInvitationSchema, PeerGrantSchema, FederatedSessionRequest/ReplySchema, CatalogChangeSchema, JellyfinImportPlanSchema.
  • Bus (BUS_ROUTES, por spire): evt.federation.peerPaired, .peerUnpaired, .grantCreated, .grantRevoked; qry.identity.peerGrant; cmd.catalog.applyRemoteChanges.
  • Operaciones headless (dec-0124 §6.1) con su CLI: styx peer invite|pair|list|unpair, styx share grant|revoke|list, styx import jellyfin --plan|--apply.
  • Errores: FEDERATION_DISABLED, PEER_UNKNOWN, PEER_KEY_MISMATCH, SIGNATURE_INVALID, GRANT_EXPIRED, GRANT_REVOKED, GRANT_SCOPE_DENIED, GRANT_LIMIT_REACHED.

12. Amenazas y mitigaciones

AmenazaMitigación
Par comprometido o maliciosoTopes por concesión además de por handle (§5.3); revocación inmediata; audit por peerId.
Recompartirreshare no existe; assetRef opaco por par; relay por B sin entregar la SCT al cliente por defecto.
Enumeración de catálogo ajenofed/v1 sólo responde a pares fijados; 403 genérico; el feed sólo cubre el scope concedido.
Agotamiento de A (A6 de dec-0117)concurrencia, ancho de banda y bytes/mes por concesión; rate limit por peerId; topes del daemon por actor.
Replay de peticiones S2Screated ± 30 s, nonce en caché acotada que rechaza al llenarse.
Robo de la clave de servidorrotación con firma de la anterior; sin firma, reemparejar; la clave vive fuera de la de SCT.
SSRF al emparejar§4.3.
Privacidad del amigohandle HMAC por concesión; relay por defecto (A no ve su IP); A no guarda su estado.
Fugas de rutas o ids internosassetRef opaco; nunca {rootId, relPath}; telemetría sin handle en claro (A8).
Jellyfin: API key con más poder del necesariose pide una key de un usuario admin sólo para importar; para la sincronización, tokens por usuario; nunca se escriben metadatos en Jellyfin.

13. Consumidor real de cada pieza (r28)

PiezaConsumidor
Importador Jellyfinwaxin migrando su biblioteca de Jellyfin del Storage Box (V-PLAY-03) con sus usuarios y vistos
Sincronización con Jellyfinla convivencia durante esa migración
Pares + concesiones + canje por SCTcompartir con un amigo que tenga su propio Styx (F3)
Feed de cambiosla biblioteca "Compartido contigo" de B
Vínculo de cuenta + LWW de progresoF2: dos instalaciones de waxin (p. ej. casa y otra ubicación) sin unirlas como nodos
Importar de un parmover una biblioteca entre instalaciones

Si una pieza no tiene consumidor cuando toque implementarla, no se implementa. El orden recomendado es Jellyfin primero (consumidor inmediato), pares después.

14. Qué no decide

  • Handoff ni mando remoto entre servidores distintos (dec-0090 es intra-servidor; dec-0128).
  • Descubrimiento de pares en la LAN o por directorio. El emparejamiento es siempre explícito.
  • El dueño del estado por perfil (P5), que dec-0125 §19 también deja abierto.
  • La renovación de SCT (SEC-Z13).
  • La ratificación de los nodos y milestones en styx.model.yml y su gate (P1).
  • Nombres finales de rutas, subjects y esquemas (los fija el ticket con su consumidor).

15. Preguntas para waxin (bloquean el lock)

  • P1 — Dónde cuelga en el roadmap. Recomendado: la parte Jellyfin (§9) dentro de outcome/jellyfin-compat como está, y un nodo nuevo outcome/server-federation (queued) con dependsOn: [track/identity, outcome/first-vertical] para pares y concesiones. Alternativa: colgarlo todo de outcome/jellyfin-compat. El nodo recomendado ya es alta propuesta queued en el model, sin gate, pendiente de tu OK; si eliges la alternativa, se borra.
  • P2 — Caducidad máxima de una concesión. Recomendado: obligatoria, por defecto 90 días, máximo configurable hasta 1 año, renovable a mano. O sin máximo (sólo obligatoria).
  • P3 — Camino de bytes por defecto. Recomendado: relay por el servidor del amigo, directo como optimización opt-in por concesión. O directo por defecto.
  • P4 — Formato de la concesión. Recomendado: estado en el servidor que concede (v1) y Biscuit sólo si las descargas offline compartidas lo piden. O Biscuit desde v1.
  • P5 — Dueño del estado por perfil (progreso, vistos, favoritos). Lo necesita F2 y el importador de Jellyfin. ¿catalog-svc, playback-svc o un servicio nuevo? (pregunta compartida con dec-0125 §19).
  • P6 — Sin federation-svc. Recomendado: identity + catalog + playback + librería @styx/federation, sin servicio nuevo. O un servicio dedicado si el endpoint fed/v1 crece.
  • P7 — Servidores tras CGNAT. ¿Se acepta que un servidor sólo pueda conceder si tiene borde alcanzable (delivery-edge o VPS), o se investiga relay por un tercero/hole punching real?
  • P8 — Jellyfin bidireccional. Sincronizar progreso en los dos sentidos durante la convivencia (recomendado) o sólo importar una vez.
  • P9 — Recompartir. Prohibido siempre (recomendado, como Plex) o permitido con atenuación explícita del que concede.

16. Qué fija el lock y qué queda reversible

  • Fija el lock: la separación nodo/par/adaptador (§2.1); que NATS no cruza (§3.2); identidad por clave con emparejamiento explícito (§4.1–4.2); que la concesión vive en el que concede, es caducable, revocable y no recompartible (§5); que se canjea por SCT v1 sin cambiar su formato (§6.1); que el contenido remoto es un SourceBinding (§6.3); que el estado de usuario no sale de su servidor salvo vínculo explícito (§7.2–7.3); que Jellyfin no es par (§9.4).
  • Reversible sin lock: TTLs y topes, el camino de bytes por defecto (P3), el intervalo de sondeo del feed, los componentes firmados exactos, los textos de la UI y los nombres de rutas, subjects y esquemas.

17. Consecuencias

  • identity-svc gana pares, invitaciones de par y concesiones a pares; library_grants gana un tipo de beneficiario (enmienda propuesta a dec-0125).
  • catalog-svc gana el feed de cambios y el kind remote-styx de SourceBinding.
  • playback-svc abre sesiones para actores fed:*; el daemon gana RemoteStyxSource pero no cambia su verificación de SCT.
  • integrations/jellyfin/ deja de ser un README cuando haya consumidor (la migración de waxin).
  • La página explicacion/federacion (estado especificado) se ata a este ADR (dec-0124).
  • El ledger de visión puede mapear V-N06 a este ADR; el GAP sigue abierto hasta el lock y el nodo de P1.

Back-refs

  • dec-0035 recibirá un banner "detallado por dec-0131" en el lock, no antes.
  • dec-0125 §4.3 y §5.2 reciben la enmienda de §5.2 en el lock de ambos.
  • Relacionados: r08, r25, dec-0031, dec-0032, dec-0114, dec-0117, dec-0121, dec-0126, y los reservados dec-0127, dec-0128, dec-0129, dec-0130.