Vista generada de dec-0125: Cuentas, hogares, perfiles, invitaciones y acceso de dispositivos
docs/decisions/dec-0125-cuentas-hogares-perfiles-invitaciones-acceso.mdVista generada desde
docs/decisions/dec-0125-cuentas-hogares-perfiles-invitaciones-acceso.md. No se edita a mano:bun run docs:genla regenera ybun run docs:checkfalla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.
| Campo | Valor |
|---|---|
| Estado | LOCKED |
| Fecha | 2026-10-01 |
| Fichero | docs/decisions/dec-0125-cuentas-hogares-perfiles-invitaciones-acceso.md |
Por qué importa (del frontmatter del ADR):
Fija el modelo de cuentas de Styx (cuenta, identidades, hogar opcional, perfiles opcionales con PIN y clasificación, concesiones de biblioteca), las invitaciones estilo Wizarr ligadas al state OIDC, la autenticación passwordless por defecto (OIDC con passkeys, Pocket ID recomendado, IdP-agnóstico) con password local sólo por flag, las API keys y cuentas de servicio intercambiadas por JWT en el borde, y el device authorization grant (RFC 8628) de identity-svc para el login del CLI y el quick connect de TV. Sin este ADR cada superficie (web, TV, CLI, agentes) inventaría su propia forma de entrar y de repartir permisos, y los huecos de seguridad de dec-0118 volverían a abrirse por la puerta lateral de cada flujo nuevo.
Nodos del roadmap que lo citan en refs: track/identity/invitations, track/identity/cli-auth, track/identity/api-keys, track/identity/quick-connect, track/identity/accounts-households-profiles, track/identity/local-password
Páginas de la documentación que lo citan: Dispositivos, mando y handoff (especificado), Servidores conectados y Jellyfin (especificado), Modelo de datos (especificado), Modo público y Styx como plataforma (especificado), API para agentes y MCP (especificado), Comandos del CLI (especificado), Salida JSON y códigos de salida (especificado), API keys (especificado), Conectar la TV y el CLI (especificado), Hogares y perfiles (especificado), Iniciar sesión (especificado), Invitaciones (implementado), Entrar con el CLI (implementado), Visión (especificado), Primer arranque (especificado)
Leído de docs/decisions/dec-0125-cuentas-hogares-perfiles-invitaciones-acceso.md, el fichero canónico.
AskUserQuestion, lockea el ADR con las
decisiones que ya había tomado (passwordless con Pocket ID u otro OIDC, password local sólo
por flag, hogares y perfiles opcionales, invitaciones estilo Wizarr, API keys con scopes,
quick connect y login del CLI con RFC 8628, paridad headless) y con la enmienda que exige el
lock de dec-0127 P5: transferProfileState va a playback-svc y a catalog-svc, e identity
borra el perfil de origen sólo cuando los dos confirman. P1–P8 de §17 se lockean con la
recomendación del ADR como parámetros reversibles; P9 queda diferida. Todo está en
Lock (2026-10-02, waxin). Donde §1–§19 y el lock difieren, gana el
lock. Las enmiendas de §15.2 entran en vigor hoy.w9/docs), ticket track/docs#11. El nodo que lo implementaría
es track/identity; los milestones nuevos van como pregunta (§17, P9), no en el model.r16: frontier salvo blocker (passkeys vía OIDC, device grant estándar, token exchange).r17: identity-svc es la authority de credenciales; nada de esto vive en otro servicio.r20 §5: OIDC-first con ports ISessionStore/ITokenIssuer.r28: cada pieza nueva termina en un consumidor real (§16).dec-0087: identity authoritative sobre credenciales.dec-0113 (PROPOSED): JWT EdDSA propio + refresh opaco rotatorio. Este ADR añade claims
(§5.4) y no cambia el formato.dec-0118 (LOCKED): BFF sin tokens en el navegador, cookies __Host-, CSRF de doble
barrera, rotación al cambiar privilegios, auth_time reciente para lo sensible, roles
owner/admin/member/restricted, Authorized<R, A>, audit. Este ADR lo respeta entero y
propone dos enmiendas acotadas (§15.2).dec-0119/dec-0120: los eventos y queries nuevos van por spire (BUS_ROUTES).dec-0124 (PROPOSED): la doc como contrato; §6.1 (registro de operaciones, CLI, MCP) y §6.2
(este diseño) son de donde sale este ADR.dec-0123 (PROPOSED, w7/release-eng): el CLI styx es un único binario. Este ADR sólo
fija sus comandos de autenticación y de cuentas.Verificado sobre el árbol de w9/docs (base 922dedc de la integración AppSec).
| Pieza | Estado | Dónde |
|---|---|---|
OIDC code + PKCE S256 + state + nonce contra un OP real (Pocket ID), login pendiente sellado en __Host-styx_login | existe | apps/identity-svc/src/transport/http/routes/auth.ts (/auth/oidc/start, /auth/oidc/callback), core/credentials/login-seal.ts |
Sesión de navegador __Host-styx_sess, qry.identity.browserSession, CSRF doble barrera, Clear-Site-Data en logout | existe | routes/auth.ts, transport/http/cookies.ts, @styx/authz/csrf |
Access JWT EdDSA (iss, aud, sub, sid, name, iat, exp, jti), JWKS, refresh opaco rotatorio con reuse detection | existe | service/tokens/access-token-signer.ts, core/credentials/refresh-token.ts, dec-0113 |
Revocación por sesión, por actor y por admin, con recentAuth en las rutas de admin | existe | /auth/sessions/revoke-all, /auth/admin/sessions/:sessionId/revoke, /auth/admin/actors/:actorId/revoke |
Tabla identity.actors ((issuer, subject) → id, role con check de 4 valores, default restricted) | existe | service/db/schema.ts, migraciones 0000..0003 |
Rol sólo desde configuración (IDENTITY_{OWNER,ADMIN,MEMBER,DISABLED}_SUBJECTS), reconciliado en cada login y al arrancar | existe | config.ts, TS2-P3-05, TS-P2-03 |
Roles → permisos <recurso>:<acción> con alcance own/shared/any; restricted sin asset:* por tipo | existe | packages/authz/src/policy.ts (TS4-P2-02) |
Audit append-only audit.events, rate-limit por IP y por sesión, problem+json sin eco | existe | service/db/security-audit.ts, middleware/rate-limit.ts |
Proxy /auth del BFF, authedServerFn general, e2e con Pocket ID | falta | track/identity/sec SW20 partial |
| Perfiles, hogares, invitaciones, concesiones de biblioteca, API keys, cuentas de servicio, device grant, password local | nuevo | este ADR |
CLI styx (sólo existe styx-upload, de ingesta) | nuevo | apps/cli/README.md (TARGET sin código), dec-0123, ADR headless (track/docs#12) |
Dos restricciones del código actual chocan con D3 y este ADR las trata de frente:
apps/identity-svc/README.md). D3 pide password sólo tras
un flag. Este ADR lo mantiene prohibido por defecto y define el único camino permitido (§8.3).| Referencia | Qué hace | Qué se toma / qué no |
|---|---|---|
| Wizarr (invitaciones para Jellyfin/Plex/Emby) | Enlace de invitación con caducidad, usos o un solo uso, bibliotecas permitidas, duración de la cuenta, y un onboarding guiado (crear cuenta, descargar app, conectar al servidor). Limpieza de cuentas caducadas. SSO opcional. | Se toma el modelo de invitación (§7) y el onboarding por pasos configurable por el owner. No se toma la contraseña de invitación (el enlace ya es el secreto) ni la creación de cuenta con password. |
| Jellyfin Quick Connect | El dispositivo pide un código corto y un secreto, muestra el código, el usuario lo autoriza desde un cliente ya logueado y el dispositivo sondea con el secreto hasta recibir el token. | Es exactamente el device grant (RFC 8628) con otro nombre. Se adopta el estándar (§10) y la UX de Jellyfin (código grande en la TV, autorización desde el móvil). |
| Plex Home / usuarios gestionados | Una cuenta "home" con usuarios gestionados sin login propio, PIN de 4 dígitos por usuario y restricciones por clasificación y bibliotecas. Un usuario gestionado puede convertirse en cuenta propia. | Usuario gestionado = perfil (§6). PIN de perfil, restricción por clasificación y por bibliotecas, y la transferencia de perfil a cuenta propia (§7.2, profile-transfer). |
| Netflix (perfiles) | Varios perfiles por cuenta, cada uno con su historial, idioma y clasificación; perfil infantil; bloqueo de perfil con PIN; transferencia de perfil a una cuenta nueva. | Perfiles opcionales con un perfil primario siempre presente, selector "¿quién está viendo?" sólo si hay más de uno, PIN, nivel de madurez, transferencia. |
Pocket ID 2.x (OP de referencia, el que ya usa track/identity) | OIDC/OAuth 2.0 passkey-only. Registro con ALLOW_USER_SIGNUPS=disabled|withToken|open, tokens de registro con caducidad, límite de usos y grupos (desde 2.0). Grupos que restringen clientes OIDC. API de admin con cabecera X-API-KEY y, desde 2.10, APIs OAuth con permisos acotados. SCIM. Login por código de un solo uso por email sólo si se habilita. Soporta device authorization. | Adaptador de aprovisionamiento recomendado (§8.2): Styx crea un token de registro de un solo uso por invitación. Styx no delega el device grant en el OP (§10.1). Las rutas exactas de la API se fijan contra la versión fijada en el spike (P3). |
| RFC 8628 (OAuth 2.0 Device Authorization Grant) | device_code + user_code + verification_uri(_complete) + interval + expires_in; sondeo con authorization_pending, slow_down, access_denied, expired_token. §5.4 phishing remoto, §6.1 entropía del user_code. | Endpoint propio en identity-svc (§10), código numérico para mandos de TV, QR con verification_uri_complete, mitigaciones de §5.4 en la pantalla de aprobación. |
| WebAuthn / passkeys | Credencial ligada al origen, resistente a phishing, sin secreto compartido. | Las passkeys viven en el OP, no en Styx (§8.1). Styx sólo ve el resultado OIDC (amr, auth_time). |
Tokens con prefijo verificable (patrón de GitHub ghp_) | Prefijo fijo + secreto aleatorio + checksum CRC32: el secret scanning lo detecta sin falsos positivos y el servidor descarta basura sin tocar la base. | Formato de API key de §9.1. |
| NIST SP 800-63B-4 + OWASP Password Storage | Longitud mínima 15 si la password es factor único (8 con MFA), sin reglas de composición, lista de bloqueo; argon2id con memoria ≥ 19 MiB. | Requisitos del password opt-in (§8.3). |
(issuer, subject) → hogar opcional con roles propios → perfiles por cuenta, siempre
al menos uno (el primario), opcionales en la UI. Las bibliotecas se conceden a cuentas u
hogares y un perfil puede estrecharlas (§4).can() en @styx/authz lo calcula (§5).state OIDC dentro
del login sellado y canjeadas de forma atómica en el callback (§7).IDENTITY_PASSWORD_LOGIN=enabled, argon2id y TOTP (§8.3).agentSafe en el registro de operaciones de dec-0124 §6.1 (§11).Servidor (instalación)
├── Cuenta (actor) kind: human | service · rol de servidor · estado · caducidad
│ ├── Identidad 1..n (issuer, subject) OIDC, o local si el password está habilitado
│ ├── Perfil 1..n siempre ≥1 (primario) · PIN · madurez · bibliotecas · idioma
│ ├── API key 0..n sólo para kind human y service
│ └── Sesiones 0..n browser | native | device, cada una con su perfil
├── Hogar 0..n (opcional)
│ └── Miembro cuenta + rol de hogar (owner | manager | member) + supervisada
├── Concesión de biblioteca biblioteca → cuenta | hogar
└── Invitación crea cuenta, une a hogar o transfiere un perfilReglas:
actor de hoy (id estable de Styx, sub del JWT). Una
cuenta humana tiene al menos una identidad. Una de servicio no tiene identidades: sólo API keys
(§9.3).(issuer, subject) vive en actors. Pasa a una
tabla identities (1..n por cuenta) para poder cambiar de OP sin perder la cuenta, enlazar una
cuenta local con su OIDC, o tener dos OP a la vez. La migración es mecánica: una fila por actor.(actorId, profileId) y no hay dos caminos.| Forma | Cómo se monta | Ejemplo |
|---|---|---|
| Cuentas independientes | Cada persona tiene su cuenta (su passkey) y sus bibliotecas concedidas. Sin hogar. Un perfil por cuenta, invisible. | Amigos a los que waxin invita a una biblioteca de películas. |
| Cuenta familiar con perfiles | Una cuenta (una passkey, la de quien la gestiona) con un perfil por persona; perfiles infantiles con madurez y PIN en los adultos. | Una TV del salón con "Papá", "Mamá", "Peques". |
| Hogar de cuentas | Cada adulto su cuenta, todas en un hogar que comparte bibliotecas; los menores, cuentas supervisadas o perfiles de una cuenta adulta. | Familia donde cada uno usa su móvil y su passkey. |
Las tres conviven en el mismo servidor. Pasar de una a otra no pierde datos: un perfil se
convierte en cuenta propia con profile-transfer (§7.2) y una cuenta entra o sale de un hogar sin
tocar sus perfiles.
identity, Postgres)| Tabla | Columnas principales | Notas |
|---|---|---|
actors (existe) | + kind (human/service), status (active/disabled/expired), expires_at, role_source (config/db), created_via_invitation_id, access_version | issuer/subject se mueven a identities. access_version sube con cada cambio de acceso (§5.4). |
identities | actor_id, issuer, subject, created_at, last_login_at; único (issuer, subject) | issuer = urn:styx:local para cuentas con password (§8.3). |
households | id, name, created_by, created_at | |
household_members | household_id, actor_id (único), household_role (owner/manager/member), supervised | Todo hogar tiene ≥1 owner (check diferido en la transacción). |
profiles | id, actor_id, name, avatar_ref, is_primary, can_manage, kind (standard/kids), maturity_ceiling, allowed_library_ids (null = todas las de la cuenta), ui_language, audio_language, subtitle_language, pin_hash, pin_failed, pin_locked_until, deleted_at | Exactamente un is_primary por cuenta (índice parcial único). El primario tiene can_manage = true y no se borra. |
library_grants | library_id, grantee_kind (actor/household), grantee_id, granted_by, created_at | library_id es de catalog-svc; identity no valida su existencia en caliente (evento de borrado, §12.2). |
invitations | ver §7.1 | token_hash único. |
invitation_redemptions | invitation_id, actor_id, redeemed_at, ip_prefix | Append-only. Es el registro de usos que muestra la UI. |
api_keys | ver §9.1 | token_hash único. |
local_credentials | actor_id, password_hash, totp_secret_enc, totp_enabled_at, failed, locked_until, updated_at | Sólo existe si el password está habilitado (§8.3); la migración la crea, pero ningún código la lee con el flag apagado. |
En Valkey (prefijo identity:), además de lo que hay: la sesión gana profileId, kind
(browser/native/device), deviceName, profileLocked, y los pendientes del device grant
(§10.3). El estado por perfil (progreso, listas) no vive en identity: lo posee su servicio y
se indexa por (actorId, profileId); identity emite evt.identity.profileDeleted para que lo
borre (§12.2).
can()owner, admin, member, restricted, con ROLE_GRANTS de
@styx/authz. Es de la cuenta.owner, manager, member. Sólo da permisos sobre el hogar
(invitar a él, gestionar sus miembros, fijar restricciones a los supervisados, conceder al
hogar las bibliotecas que el propio manager ya ve). No da permisos de servidor.can_manage. Un
perfil con can_manage = false (todo perfil kids, y los que el owner de la cuenta marque)
no ejerce permisos de gestión aunque la cuenta sea admin u owner: desde el perfil
infantil de la TV del salón no se borran bibliotecas.permitido(principal, permiso, recurso) =
ROLE_GRANTS[cuenta.rol][permiso] cubre recurso (own | shared | any)
∧ (permiso es de gestión ⇒ perfil.can_manage)
∧ (recurso tiene biblioteca ⇒ biblioteca ∈ visibles(cuenta) ∩ perfil.allowed_library_ids)
∧ (recurso tiene clasificación ⇒ clasificación ≤ perfil.maturity_ceiling)
∧ (credencial con scopes ⇒ permiso ∈ scopes) (API key, device con scope, §9-§10)
∧ cuenta.status = activeTodo es intersección: ningún eje puede ampliar lo que da otro. Es el mismo principio que TS4-P2-02 (alcances explícitos) llevado a perfiles y credenciales.
Hoy shared cubre "lo suyo y lo que no tiene dueño" (ownerActorId null). Con concesiones,
shared pasa a significar "lo suyo y lo de las bibliotecas que se le han concedido": un
recurso sin dueño en una biblioteca no concedida queda fuera. Es un estrechamiento: ninguna
cuenta ve más que hoy. Para no romper la instalación de una sola persona, la primera arrancada
tras la migración concede todas las bibliotecas existentes al hogar o a las cuentas member+
existentes (decisión de migración, P6).
restricted sigue sin asset:* por tipo (TS4-P2-02 no cambia). Una concesión a un restricted
no le da reproducción: le da visibilidad para el día en que pase a member. El rol con el que
entra un invitado es member por defecto (§7.1).
IDENTITY_{OWNER,ADMIN,MEMBER,DISABLED}_SUBJECTS siguen mandando sobre las
identidades que nombran, en las dos direcciones, como hoy (role_source = config). Una cuenta
listada no puede cambiar de rol por la API ni por una invitación.role_source = db): el que les dio
su invitación o el que les ponga un owner/admin por la API. Por defecto, restricted
(deny-by-default, TS-P2-03, sin cambios).IDENTITY_ROLE_SOURCE = config | config+db. config es el comportamiento de
hoy (las invitaciones crean cuentas restricted con concesiones, inútiles para reproducir hasta
que la configuración las liste). config+db habilita el rol por invitación y por API. Qué valor
es el default tras el lock lo decide waxin (P4; recomendado config+db).access_version y rota o revoca las sesiones afectadas (dec-0118 §2.3: rotación al cambiar
privilegios), igual que hoy un cambio de rol por configuración.owner de servidor sólo por configuración o por el
arranque inicial (§8.4), nunca por invitación ni por API.Aditivos al access JWT de dec-0113 §1 (que no fija la lista como cerrada mientras sea
PROPOSED):
| Claim | Valor | Para qué |
|---|---|---|
pid | id del perfil activo | estado por perfil y restricciones |
acv | access_version de la cuenta al emitir | los servicios cachean el snapshot de acceso por (sub, pid, acv) y lo invalidan solos |
cred | browser | native | device | pat | service | audit y policies (p. ej. pat no puede crear API keys) |
scp | lista de scopes (sólo si la credencial está acotada: API key, device con scope) | intersección de §5.1 |
amr | métodos del login (hwk/swk de passkey vía OP, pwd+otp local) | exigir passkey para lo sensible si waxin lo quiere (P7) |
Las bibliotecas visibles y la madurez no van en el JWT (la lista puede ser larga y cambia):
un servicio que las necesita pide qry.identity.access { actorId, profileId } y cachea la
respuesta por acv. Mismo patrón que qry.identity.session: revocation-aware y con presupuesto
por actor (TS2-P2-01).
can_manage de la cuenta (el primario siempre).
Campos: nombre, avatar, tipo (standard/kids), madurez, bibliotecas, idiomas, PIN. Tope de
perfiles por cuenta configurable (IDENTITY_MAX_PROFILES, sin número fijado aquí).maturity_ceiling), a la que track/domain-media
mapea los sistemas de clasificación de cada país (como el ParentalRating numérico de
Jellyfin). El contenido sin clasificar se oculta a los perfiles kids por defecto
(configurable por perfil). Dónde vive la tabla de mapeo es de domain-media, no de este ADR.evt.security.profilePinLockout y aviso al primario. El PIN se cambia con
auth_time reciente de la cuenta (dec-0118 §2.3).POST /auth/profile/select { profileId, pin? }. Rota la credencial (dec-0118 §2.3 ya lo pide para "perfil adulto/infantil") y el
siguiente access JWT lleva el pid nuevo. El perfil activo vive en la sesión del servidor, no
en una cookie legible ni en el cliente.profileLocked): la TV de los peques aprobada con
"bloquear en este perfil" (§10.4) sólo cambia de perfil con el PIN de un perfil can_manage.evt.identity.profileDeleted, y los
dos dueños del estado por perfil (playback-svc y catalog-svc, dec-0127 P5) purgan su parte.| Campo | Valor |
|---|---|
kind | account (cuenta nueva independiente) · household (cuenta nueva o existente que entra en un hogar) · profile-transfer (§7.2) |
token_hash | SHA-256 del token (256 bits aleatorios, base64url). El token se muestra una vez |
short_code (opcional) | código tecleable XXXX-XXXX (Crockford base32, 40 bits) para dictarlo por teléfono; con límite de intentos propio |
expires_at | obligatorio; tope IDENTITY_INVITE_MAX_TTL (sin valor fijado aquí) |
max_uses / uses | 1 por defecto; ilimitado sólo lo crea un owner |
role | restricted | member (default). admin sólo lo crea el owner con auth_time reciente. owner nunca |
library_ids | bibliotecas concedidas a la cuenta nueva; ⊆ las que el creador puede conceder |
household_id, household_role, supervised | hogar de destino y rol en él (member por defecto; manager sólo si lo crea el owner del hogar) |
account_ttl | duración de la cuenta creada (la "account duration" de Wizarr): al vencer pasa a expired, sin sesiones |
profile_defaults | madurez, idiomas y tipo del perfil primario de la cuenta nueva |
idp_groups | grupos del OP que el adaptador asigna al aprovisionar (§8.2) |
note, created_by, created_at, revoked_at | para la lista de la UI y el audit |
Quién crea: owner/admin cualquier invitación dentro de su alcance; manager de hogar sólo
household a su hogar, con role ≤ member y bibliotecas ⊆ las del hogar. Crear exige auth_time
reciente (es gestionar usuarios, dec-0118 §2.3) y un perfil con can_manage.
profile-transfer lleva el profileId de origen. Al canjearla se crea una cuenta nueva cuyo
perfil primario hereda el estado del perfil de origen, y el perfil de origen se borra.
Enmendado en el lock (por
dec-0127P5): el estado por perfil tiene dos dueños. identity-svc mandatransferProfileStatea playback-svc (sesiones, progreso, preferencias de pista) y a catalog-svc (visto, favoritos, listas, proyección de progreso), y borra el perfil de origen sólo cuando los dos confirman. Es una saga de dos pasos idempotentes: si uno falla o no responde, se reintenta y el perfil de origen no se borra. El estado intermedio y la política de reintentos los fija el ticket con su consumidor. La crea el dueño de la cuenta de origen; es el caso "el peque ya tiene su móvil" y el "transferir perfil" de Netflix.
state OIDC)Invitado web (BFF) identity-svc OP (Pocket ID)
│ GET /join/<token> ─────────▶│ invite.preview(token) ─────────▶│ hash, valida, NO consume
│ │◀─ {servidor, quién invita, qué da, caduca} (sin datos personales)
│◀── Set-Cookie __Host-styx_invite (sellada, 15 min) + 303 /join (el token sale de la barra)
│ "Aceptar con passkey" ─────▶│ proxy /auth ───────────────────▶│ /auth/oidc/start?invite=1
│ │ │ abre __Host-styx_invite, mete
│ │ │ invitationId+tokenHash en el
│ │ │ login sellado junto a state/nonce
│ │ │ ¿adaptador con createSignupToken?
│ │ │ sí → token de registro del OP
│ │ │ (1 uso, TTL del login, grupos)
│◀──────────────── 302 al registro/autorización del OP ──────────│
│ registra passkey / inicia sesión en el OP ──────────────────────────────────────────────────────▶│
│◀──────────────────────────────── 302 /auth/oidc/callback?code&state ────────────────────────────│
│ ───────────────────────────▶│ ───────────────────────────────▶│ abre el sello, compara state,
│ │ │ canjea code, y EN UNA TRANSACCIÓN:
│ │ │ uses+1 si uses<max ∧ vigente ∧ no revocada
│ │ │ crea cuenta+identidad+perfil primario
│ │ │ (o une la existente al hogar)
│ │ │ concesiones, rol (salvo config), audit
│◀──────── sesión __Host-styx_sess + 302 /onboarding ────────────│Reglas del canje:
state. Un atacante no puede hacer que la víctima canjee su invitación
(invite-swap) ni reutilizar un state ajeno: el sello está atado a la cookie del navegador que
empezó el login.UPDATE … SET uses = uses + 1 WHERE uses < max_uses AND expires_at > now() AND revoked_at IS NULL RETURNING y la creación de la cuenta van en la misma transacción. Dos canjes
concurrentes del último uso: uno gana, el otro recibe IDENTITY_INVITE_EXHAUSTED.kind = account se rechaza (IDENTITY_ACCOUNT_EXISTS) y la
UI ofrece entrar con la cuenta existente; una invitación nunca cambia el rol de una cuenta que
ya existe. Con kind = household, la cuenta existente entra en el hogar./join lleva Referrer-Policy: no-referrer, el token no se registra en logs (redacción en la
capa de logging) y el GET /join/<token> cambia el token por la cookie sellada y redirige.invite.preview y /join son públicos: rate-limit por IP (los mismos buckets
de /auth/*) y el código corto con su propio contador de fallos.Tras el canje, /onboarding con pasos que el owner configura (texto de bienvenida en Markdown
sanitizado, W5 de dec-0118 §6.3), elegir nombre y avatar del perfil, crear más perfiles si la
cuenta es familiar, descargar apps, conectar la TV (lleva directo al quick connect, §10) y
"primer script con API key" si la cuenta es member+. Los pasos son datos (onboarding.steps)
y se gestionan por API como todo lo demás.
nonce, auth_time (para recentAuth) y, si lo hay, amr.deploy/ y en los e2e (SW20). Es
passkey-only, ligero y self-hosted. Cualquier OP OIDC conforme sirve (Authelia, Authentik,
Kanidm, Keycloak, Zitadel…): sin adaptador de aprovisionamiento funciona en modo manual
(§8.2).identities; la pantalla de login lista los
configurados. Uno solo por defecto.prompt=login + max_age=0 al OP y comprobación de
auth_time en el callback (dec-0118 §2.3).interface IIdentityProviderAdmin {
readonly capabilities: ReadonlySet<'signupToken' | 'createUser' | 'disableUser' | 'groups'>;
createSignupToken(input: {
ttlS: number;
groups: readonly string[];
}): Promise<Result<{ url: string }>>;
disableUser(subject: string): Promise<Result<void>>;
}| Adaptador | Capacidades | Credencial |
|---|---|---|
pocket-id | signupToken (requiere ALLOW_USER_SIGNUPS=withToken en el OP), groups, disableUser | API acotada del OP (APIs OAuth con permisos desde 2.10) en vez de STATIC_API_KEY; secreto por fichero montado, como la clave CSRF |
manual (default) | ninguna | — |
signupToken: cada invitación genera, al empezar su login, un token de registro del OP
de un solo uso con el TTL del login pendiente y los idp_groups. Nunca se crea por adelantado
(un token del OP sin consumir es un registro abierto).manual: el OP tiene que permitir que el invitado se registre (registro abierto, o el
admin lo crea a mano). La pantalla de la invitación lo explica. La invitación sigue siendo
la que da acceso a Styx: un usuario que se registre en el OP sin invitación entra como
restricted sin bibliotecas (deny-by-default de hoy).disableUser)./join y pulsa "continuar": el sello sigue vigente.IDENTITY_PASSWORD_LOGIN = disabled | enabled, disabled por defecto. Con disabled
ninguna ruta de password existe (no responden 403: no están montadas), y el guard de rutas lo
comprueba. Con enabled:
issuer = urn:styx:local. Se crean sólo por invitación o por un
admin; nunca por registro abierto.Bun.password), parámetros configurables con suelo OWASP (≥ 19 MiB,
t ≥ 2) y rehash transparente al subir parámetros.IDENTITY_PASSWORD_MFA = required | optional, required por
defecto. Secreto TOTP cifrado en reposo.IDENTITY_OWNER_SUBJECTS. Se mantiene y es lo recomendado para despliegues declarativos.styx server setup (CLI local, que lo lee del log o del socket de administración que
decida el ADR headless). La primera identidad que inicia sesión presentando ese código pasa a
owner (role_source = db), y el código se destruye. Es el asistente de primer arranque de
Jellyfin sin la ventana en la que cualquiera que llegue primero se queda el servidor.| Aspecto | Decisión |
|---|---|
| Formato | styx_pat_ + 32 caracteres base62 (≥ 190 bits aleatorios) + 6 base62 de CRC32 del cuerpo. Las de servicio, styx_sat_. Regex publicable para secret scanning: styx_(pat|sat)_[0-9A-Za-z]{38} |
| Checksum | El servidor descarta una key con CRC incorrecto antes de tocar la base (barato ante basura y fuerza bruta) y los escáneres no dan falsos positivos |
| Almacenamiento | SHA-256 del token completo, índice único (alta entropía: no hace falta un hash lento; mismo criterio que el refresh de dec-0113). Para identificarla en la UI se guarda display_prefix (styx_pat_Ab12…) |
| Mostrar | Una sola vez, al crearla. No se puede volver a leer |
| Campos | id, actor_id, name, scopes[], profile_id (opcional; si no, el primario), expires_at (obligatorio), created_at, created_from_session_id, last_used_at, last_used_ip_prefix, revoked_at |
| Caducidad | obligatoria; default y máximo configurables (IDENTITY_PAT_DEFAULT_TTL, IDENTITY_PAT_MAX_TTL, sin valores fijados aquí). Sin "nunca caduca" para cuentas humanas. Aviso en la UI y en styx key list antes de caducar |
| Scopes | Un scope es un permiso de @styx/authz (asset:play, asset:scan…) o un alias definido allí (styx:read, styx:library-admin…). Al crearla: scopes ⊆ permisos actuales de la cuenta, o IDENTITY_SCOPE_EXCEEDS_GRANTS. Al usarla: §5.1 interseca con el rol de ese momento (degradar a la cuenta degrada sus keys) |
| Crear | sesión interactiva con auth_time reciente y perfil can_manage. Una API key no puede crear API keys (cred = pat lo impide en la policy) |
| Último uso | se actualiza con rebote (≤ 1 escritura por minuto y key) para no meter Postgres en el hot path |
| Revocar | por la propia cuenta, por un admin, y en bloque al deshabilitar la cuenta |
| Tope | número máximo de keys por cuenta configurable |
Authorization: Bearer styx_pat_… al gateway de API (frontera
W2 de dec-0118 §1, sin home hoy; su forma la cierra el ADR headless) y el gateway la cambia por
un JWT corto con POST /auth/token en forma de token exchange (RFC 8693:
grant_type=urn:ietf:params:oauth:grant-type:token-exchange,
subject_token_type=urn:styx:token-type:api-key). El JWT lleva cred = pat, scp y pid. El
gateway lo cachea en memoria hasta su exp (≤ 5 min)./auth/token rechaza con
IDENTITY_KEY_IN_BROWSER_CONTEXT cualquier intercambio que traiga Cookie, Origin o
cabeceras Sec-Fetch-* de navegador; el BFF elimina Authorization de toda petición del
navegador; y los servicios no tienen CORS (SW12). Una API key pegada en una página no sirve.actors.kind = service: sin identidades OIDC, sin login interactivo, con un perfil técnico
primario y can_manage según su rol. Rol ≤ admin. Las crea un owner/admin.styx_sat_, cuya caducidad máxima puede ser mayor que la de
las personales (configurable) y que se rotan con styx key rotate (emite la nueva, la vieja
sigue viva un periodo de solape configurable y luego se revoca).actorKind = service.Pocket ID soporta el device grant, pero el resultado que necesita Styx es una sesión de Styx con un perfil elegido y, si se quiere, bloqueada en ese perfil. Eso no lo sabe el OP. identity- svc ya es el emisor de los tokens de Styx (dec-0113), así que es el servidor de autorización del grant. Funciona igual con cualquier OP, soporte o no el device grant.
| Ruta | Policy | Qué |
|---|---|---|
POST /auth/device/code | public, rate-limit | RFC 8628 §3.1–3.2. Entrada: client_id (cliente público registrado: styx-cli, styx-tv, styx-apple…), device_name, device_kind, scope?. Salida: device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval |
POST /auth/device/token | public (la autoriza el device_code) | RFC 8628 §3.4–3.5: authorization_pending, slow_down, access_denied, expired_token. Éxito: access JWT + refresh opaco de dec-0113 (sesión kind = device). Errores en formato OAuth (RFC 6749 §5.2), no problem+json, por interoperabilidad |
GET /auth/device/lookup?user_code | authenticated (vía BFF) | datos para la pantalla de confirmación; cuenta como intento |
POST /auth/device/approve | authenticated + CSRF (vía BFF) | { userCode, profileId, profileLocked, scope? } |
POST /auth/device/deny | authenticated + CSRF | rechaza; el dispositivo recibe access_denied |
verification_uri = https://<origen de la web>/link; verification_uri_complete =
https://<web>/link?code=48210937, que el dispositivo pinta como QR.
user_code: 8 dígitos, mostrados 4821 0937 (los mandos de TV teclean números; RFC 8628
§6.1 contempla códigos numéricos para dispositivos de entrada limitada). Son 26,6 bits.device_code: 256 bits aleatorios; Valkey guarda su SHA-256 (mismo criterio que el refresh).interval 5 s con slow_down si el dispositivo
sondea más rápido.lookup/approve con un código que no existe cuentan contra la sesión (5 por
minuto, 20 por hora) y contra la cuenta. Con P pendientes a la vez, cada intento acierta con
probabilidad P/10⁸: con P = 1 000 y 20 intentos por hora y cuenta, adivinar un código ajeno es
del orden de 2·10⁻⁴ por cuenta y hora, y las cuentas son por invitación. Si se quiere más
margen: 9 dígitos (P8)./auth/device/code es anónimo y escribe estado, así que tiene tope
por IP y tope global (la lección de TS3-P2-02/TS4-P2-01: un endpoint anónimo que llena estado
compartido es un DoS). Al llegar al tope global, 503 con Retry-After; los pendientes viven en
Valkey con TTL bajo el maxmemory ya configurado.La pantalla de /link (o la del móvil que escanea el QR) muestra, antes de aprobar:
Styx para TV, styx CLI);El QR rellena el código pero nunca aprueba solo: siempre hay un clic explícito sobre esa
pantalla. Aprobar no exige auth_time reciente salvo que se pida un perfil can_manage o scopes
de gestión (P7). Cada aprobación y cada rechazo van al audit
(evt.security.deviceApproved/deviceDenied) y el dispositivo aparece en "Dispositivos" con su
perfil, último uso y botón de revocar.
styx login [--server URL]: device grant con client_id = styx-cli. Imprime el código, la URL
y un QR en la terminal, y abre el navegador si hay entorno gráfico (--no-browser lo evita).
El refresh token va al almacén seguro del sistema (Keychain, Secret Service, Credential
Manager); sin almacén, a un fichero 0600 con un aviso explícito.styx login --with-token: lee una API key de stdin (nunca de argv, que queda en el
historial y en ps). STYX_TOKEN en el entorno para CI y agentes.styx auth status [--json], styx logout (revoca la sesión en el servidor, no sólo borra el
fichero), styx auth switch-profile.Entradas del registro de operaciones de dec-0124 §6.1 (la forma general del registro, del SDK y
del CLI la cierra el ADR headless, track/docs#12; aquí se fija qué operaciones hay). Todas por
identity-svc; la web las consume por el SDK desde sus server functions. agentSafe = no significa
que el CLI exige --yes y que no se exponen por MCP.
| Operación | HTTP (identity-svc) | Permiso / policy | CLI | agentSafe |
|---|---|---|---|---|
auth.methods.get | GET /auth/methods | public | styx auth methods | sí |
auth.session.get (existe) | GET /auth/session | authenticated | styx auth status | sí |
auth.sessions.revokeAll (existe) | POST /auth/sessions/revoke-all | actor:revoke-sessions own | styx auth logout --all | no |
auth.profile.select | POST /auth/profile/select | authenticated (+PIN) | styx auth switch-profile | sí |
auth.token.exchange | POST /auth/token | public (la autoriza la key) | — (lo hace el SDK) | — |
auth.device.code / .token | POST /auth/device/{code,token} | public | styx login | sí |
device.approve / .deny / .lookup | /auth/device/{approve,deny,lookup} | authenticated + CSRF | styx device approve <código> --profile <p> | no |
device.list / .revoke | GET /devices, DELETE /devices/:id | identity-session:revoke own/any | styx device list|revoke | list sí, revoke no |
account.me.get / .update | GET/PATCH /accounts/me | authenticated | styx account show|update | sí |
account.list / .get | GET /accounts, GET /accounts/:id | account:read any (admin) | styx account list|show <id> | sí |
account.update (rol, estado, caducidad) | PATCH /accounts/:id | account:manage any + recentAuth | styx account set <id> --role … --expires … | no |
account.delete | DELETE /accounts/:id | account:manage any + recentAuth | styx account delete <id> | no |
profile.list / .create / .update | /accounts/me/profiles[/:id] | profile:manage own (perfil can_manage) | styx profile list|create|update | sí |
profile.delete | DELETE /accounts/me/profiles/:id | profile:manage own + recentAuth | styx profile delete | no |
profile.pin.set / .clear | PUT/DELETE /accounts/me/profiles/:id/pin | profile:manage own + recentAuth | styx profile pin set|clear | no |
household.create / .get / .update | /households[/:id] | household:manage (rol de hogar) | styx household create|show|update | sí |
household.member.update / .remove | /households/:id/members/:actorId | household:manage + recentAuth | styx household member set|remove | no |
household.leave | POST /households/:id/leave | authenticated | styx household leave | no |
household.delete | DELETE /households/:id | owner del hogar + recentAuth | styx household delete | no |
grant.library.list / .set | /accounts/:id/libraries, /households/:id/libraries | library:grant (admin, o manager ⊆ lo suyo) + recentAuth en set | styx library grant|revoke|grants | list sí, set no |
invite.create | POST /invitations | invitation:create + recentAuth | styx invite create --libraries … --role … --uses … --expires … --household … | no |
invite.list / .get | GET /invitations[/:id] | invitation:read own/any | styx invite list|show | sí |
invite.revoke | DELETE /invitations/:id | invitation:create own/any | styx invite revoke | no |
invite.preview | POST /invitations/preview | public, rate-limit | styx invite inspect <enlace> | sí |
key.create | POST /accounts/me/keys | api-key:create own + recentAuth, cred ≠ pat | styx key create --scope … --expires … | no |
key.list / .revoke | GET/DELETE /accounts/me/keys[/:id] | api-key:read|revoke own (admin: any) | styx key list|revoke | list sí, revoke no |
serviceAccount.create / .list / .delete | /service-accounts[/:id] | service-account:manage (admin) + recentAuth | styx service-account … | no |
serviceAccount.key.create / .rotate | /service-accounts/:id/keys | service-account:manage + recentAuth | styx key create --service-account <id> / styx key rotate | no |
onboarding.get / .update | GET/PUT /onboarding | public (get) / server:manage (update) | styx server onboarding show|set | get sí, update no |
server.setup.claim | POST /setup/claim | public con código de configuración | styx server setup | no |
auth.password.* (sólo con flag) | /auth/password/{login,change}, /auth/mfa/totp/* | public (login, vía BFF) / authenticated | — (sólo web; el CLI usa device grant) | — |
Permisos nuevos en @styx/authz (RESOURCE_ACTIONS): account:{read,manage},
profile:manage, household:manage, library:grant, invitation:{create,read},
api-key:{create,read,revoke}, service-account:manage, server:manage. Sus alcances por rol
salen de §5 y los fija el ticket de implementación.
Las reglas generales (--json con esquema por comando, exit codes estables, --yes, --dry-run,
sin prompts sin TTY) son de dec-0124 §6.1 y del ADR headless. Específico de este dominio:
STYX_TOKEN), nunca con la sesión de una
persona. El camino recomendado es una cuenta de servicio con scopes de lectura y de la tarea.key.create, invite.create, account.update y todo lo que concede o escala es
agentSafe = no: un agente no se fabrica credenciales ni invita a nadie sin que una persona lo
confirme.--json nunca incluye secretos salvo en la respuesta de key create / invite create, que es la única vez que existen en claro, y esas dos marcan el campo secret para
que el agente sepa que no debe registrarlo.Un servidor MCP generado del registro (P5 de dec-0124) expondría como herramientas sólo las
operaciones agentSafe = sí de la tabla, autenticado con una API key de cuenta de servicio. Este
ADR sólo fija qué operaciones de cuentas serían elegibles; adoptarlo lo decide waxin en el ADR
headless.
@styx/api-contractsFicheros nuevos bajo packages/api-contracts/src/identity/ (o ampliación de identity.ts; lo
decide el ticket), TypeBox 1.x (dec-0116), cada esquema con description para el OpenAPI
(dec-0124 §7.1):
AccountSchema, AccountKindSchema, AccountStatusSchema, AccountUpdateBodySchema,
AccountListQuerySchema/ReplySchema, IdentityLinkSchema.ProfileSchema, ProfileCreateBodySchema, ProfileUpdateBodySchema,
ProfilePinBodySchema, ProfileSelectBodySchema, MaturityCeilingSchema.HouseholdSchema, HouseholdMemberSchema, HouseholdRoleSchema,
HouseholdMemberUpdateBodySchema.LibraryGrantSchema, LibraryGrantSetBodySchema.InvitationSchema, InvitationKindSchema, InvitationCreateBodySchema,
InvitationCreatedReplySchema (con el token, una vez), InvitationPreviewBodySchema/ReplySchema.ApiKeySchema, ApiKeyCreateBodySchema, ApiKeyCreatedReplySchema (con el
secreto, una vez), ScopeSchema, TokenExchangeBodySchema/ReplySchema.ServiceAccountSchema, ServiceAccountCreateBodySchema.DeviceCodeBodySchema/ReplySchema, DeviceTokenBodySchema,
DeviceTokenErrorSchema (OAuth), DeviceLookupReplySchema, DeviceApproveBodySchema,
DeviceSchema.AuthMethodsReplySchema, OnboardingStepsSchema,
SetupClaimBodySchema.SessionPrincipalSchema gana profileId, credentialKind, scopes?,
accessVersion; AccessSnapshotSchema nuevo.BUS_ROUTES, por spire)| Subject | Tipo | Productor → consumidores | Para qué |
|---|---|---|---|
qry.identity.access | qry | identity ← catalog, playback, realtime | snapshot de acceso (bibliotecas visibles, madurez, can_manage) por acv |
evt.identity.accessChanged | evt | identity → todos los que cachean | invalida cachés por (actorId, acv) |
evt.identity.accountDisabled | evt | identity → playback (cierra sesiones de reproducción), workers | corte inmediato |
evt.identity.profileDeleted | evt | identity → playback-svc y catalog-svc (dec-0127 P5) | purga del estado |
cmd.playback.transferProfileState y cmd.catalog.transferProfileState | cmd | identity → playback-svc y catalog-svc (dec-0127 P5) | profile-transfer (§7.2): el origen se borra sólo cuando los dos confirman |
evt.catalog.libraryDeleted | evt | catalog → identity | borra concesiones huérfanas |
evt.security.* nuevos | evt | identity → sink de audit | inviteRedeemed, inviteRejected, deviceApproved, deviceDenied, deviceCodeGuessing, apiKeyCreated, apiKeyRevoked, apiKeyRejected, profilePinLockout, passwordLoginFailed, setupClaimed |
IdentityErrorCode)IDENTITY_INVITE_INVALID, IDENTITY_INVITE_EXPIRED, IDENTITY_INVITE_EXHAUSTED,
IDENTITY_ACCOUNT_EXISTS, IDENTITY_ACCOUNT_DISABLED, IDENTITY_PROFILE_PIN_REQUIRED,
IDENTITY_PROFILE_PIN_LOCKED, IDENTITY_PROFILE_LIMIT, IDENTITY_SCOPE_EXCEEDS_GRANTS,
IDENTITY_KEY_IN_BROWSER_CONTEXT, IDENTITY_KEY_LIMIT, IDENTITY_PASSWORD_LOGIN_DISABLED
(sólo para el registro de operaciones: con el flag apagado las rutas no existen),
IDENTITY_DEVICE_CODE_CAPACITY, IDENTITY_SETUP_CLOSED. Los del device grant en el endpoint de
token usan los códigos OAuth de RFC 8628 §3.5.
| Pantalla | Quién | Qué hace |
|---|---|---|
/login | anónimo | botones por OP configurado ("Continuar con passkey"), formulario sólo si el password está habilitado, "¿tienes una invitación?" |
/join/<token> → /join | invitado | servidor, quién invita, qué da (bibliotecas, hogar), caducidad; "Aceptar con passkey"; estados caducada/agotada/ya tienes cuenta |
/onboarding | recién llegado | pasos configurables: bienvenida, perfil, más perfiles, apps, conectar la TV, API key |
/who | cuenta con >1 perfil | selector de perfiles a pantalla completa, avatar grande, PIN pad, "gestionar perfiles" |
/link | cualquiera logueado | teclear el código de la TV (o llegar por QR), pantalla de confirmación de §10.4, éxito |
/settings/account | cuenta | identidades enlazadas, sesiones y dispositivos (revocar), API keys (crear con scopes y caducidad, ver una vez, revocar), password/TOTP si aplica |
/settings/profiles | perfil can_manage | crear/editar perfiles: nombre, avatar, infantil, madurez, bibliotecas, idiomas, PIN; transferir perfil a cuenta propia |
/settings/household | miembro de hogar | miembros, roles, supervisados, invitar al hogar, bibliotecas del hogar, salir |
/admin/users | admin | lista con rol, origen del rol (config/db), estado, caducidad, último acceso; cambiar rol, deshabilitar, bibliotecas |
/admin/invitations | admin / manager | asistente de creación (tipo → bibliotecas → rol → hogar → usos → caducidad → duración de cuenta) y resultado con enlace, código corto, QR y "compartir"; lista con usos, estado y canjes |
/admin/service-accounts | admin | crear, keys, rotar, borrar |
/admin/onboarding | owner | editor de pasos |
/admin/auth | owner | sólo lectura: OP(s), adaptador y sus capacidades, flag de password, origen de roles; cada valor dice qué variable lo cambia |
4821 0937, QR, nombre del servidor, "abre
styx.example/link en tu móvil", cuenta atrás, botón "nuevo código". Navegable con el mando
(r58: Pi 3 no es un cliente degradado).styx login (código + QR en la terminal), styx auth status --json, styx invite create … --json
(devuelve enlace, código corto y caducidad), styx key create … --json (devuelve el secreto una
vez), styx device approve 48210937 --profile peques (aprobar la TV desde la terminal, con la
misma confirmación de §10.4 en texto).
| Amenaza | Mitigación |
|---|---|
| Invitación filtrada (reenviada, en un chat) | caducidad obligatoria, usos = 1 por defecto, revocación, lista de canjes visible; el canje pide además registrar una passkey |
| Invite-swap / CSRF de login con invitación ajena | el token viaja en el login sellado ligado al state y a la cookie del navegador que lo empezó (§7.3) |
| Escalada por invitación o por API | concesiones ⊆ las del creador, owner nunca por invitación, admin sólo por owner con auth_time reciente, roles de config fijos |
Fuerza bruta del user_code | 8 dígitos + límites por sesión y cuenta + tope de pendientes + audit deviceCodeGuessing (§10.3) |
| Phishing remoto del device grant (RFC 8628 §5.4) | pantalla de confirmación con dispositivo, red y hora; QR que nunca aprueba solo; aviso explícito (§10.4) |
DoS por endpoints anónimos (/device/code, /join) | topes por IP y global, estado en Valkey con TTL bajo maxmemory (lecciones TS3/TS4) |
| API key robada | prefijo para secret scanning, caducidad obligatoria, scopes mínimos, último uso visible, revocación inmediata en el intercambio |
| API key usada desde una página (XSS, extensión) | /auth/token rechaza contexto de navegador; el BFF quita Authorization; servicios sin CORS |
| Niño que sale de su perfil | profileLocked + PIN de gestor, bloqueo por fallos, perfiles kids sin can_manage |
| Fuga de la base de identity | sólo hashes (SHA-256 de tokens de alta entropía, argon2id de PIN y password), TOTP cifrado, audit append-only |
| Password habilitado | apagado por defecto, rutas no montadas sin flag, NIST 800-63B-4, TOTP requerido por defecto, sin ROPC (§8.3) |
| Toma del servidor recién instalado | owner por configuración, o código de configuración de un solo uso fuera de banda (§8.4) |
dec-0118 §2.1 (navegador sin tokens): todas las pantallas nuevas van por server functions
del BFF (authedServerFn, SW20) y por el proxy /auth. Ningún flujo nuevo entrega un token al
navegador. Las API keys no se aceptan desde él (§9.2).dec-0118 §2.2–§2.5: cookies, rotación (ahora también al cambiar de perfil, que ya estaba
escrito), auth_time para lo sensible, CSRF en toda mutación nueva.dec-0118 §3–§4: cada ruta nueva declara policy; cada id que llega del cliente (profileId,
householdId, invitationId, keyId, deviceId, actorId) se resuelve a Authorized<R, A>
antes del handler, con 404 para lo ajeno.dec-0113: formato del JWT y del refresh intactos; las sesiones device son sesiones nativas
de §2.4 de dec-0118.IDENTITY_ROLE_SOURCE (§5.3).IDENTITY_PASSWORD_LOGIN=enabled, con los requisitos de dec-0125 §8.3".@styx/authz: shared pasa de "sin dueño" a "concedido" (§5.2), y los permisos nuevos de
§11.1.dec-0113 §1: claims aditivos pid, acv, cred, scp, amr (§5.4).track/identity y track/identity/sec/auth del BFF, authedServerFn general, e2e
con Pocket ID). Todo lo de este ADR cuelga de ahí.track/identity siguen como están (pendientes de ratificar); este ADR no los toca.public de identity-svc (IDENTITY_PUBLIC_ROUTES) sólo las que §10.2/§11.1 marcan
public.| Pieza | Consumidor que la justifica |
|---|---|
| Perfiles | la web (selector y estado por perfil) y la TV del salón de waxin |
| Hogar | invitar a la familia con bibliotecas compartidas |
| Invitaciones + adaptador | el alta de cualquier persona que no sea el owner |
| Device grant | styx login y la app de TV (Pi 3, F-APPLIANCE-PI3) |
| API keys | scripts de waxin y agentes (D2); el hook de escaneo al importar |
| Cuentas de servicio | el runner de e2e y la automatización |
| Password local | ninguno hoy: por eso sólo existe tras un flag y su implementación es la última (P9) |
Contestadas o diferidas en el lock. Se conservan como registro.
pocket-id con tokens de registro (recomendado, tras un spike
contra la versión que se fije) o sólo modo manual al principio.IDENTITY_ROLE_SOURCE=config+db (recomendado) o config.member+ (recomendado, nadie pierde acceso) o empezar sin concesiones.amr) y auth_time reciente para aprobar dispositivos con perfil
gestor y para crear API keys (recomendado) o sólo auth_time.track/identity (propuesta; no se añaden al model hasta tu OK). Nombres
y orden; los gate items los redacta el ticket, con author ≠ verifier y mutantes:
track/identity/accounts — identidades separadas, hogares, perfiles, concesiones,
qry.identity.access, can() efectivo de §5.1, migración de shared y del rol (§5.3).track/identity/invites — invitaciones, canje atómico ligado al state, adaptador
pocket-id y modo manual, onboarding, código de configuración inicial.track/identity/devices — device grant, /link, quick connect de TV, login del CLI,
dispositivos y revocación.track/identity/keys — API keys, cuentas de servicio, /auth/token y el gateway de W2 (con
el ADR headless).track/identity/password — password local opt-in con TOTP. Último, sin consumidor hoy.especificado en guias/cuentas/ (dec-0124 §9.5).qry.identity.access y a cachearlo por
acv: un coste de bus acotado que ya tiene patrón (TS2-P2-01).dec-0127 P5 (lock del
2026-10-02): se reparte entre playback-svc y catalog-svc con la clave (actorId, profileId)
de este ADR, y los eventos de borrado y transferencia van a los dos (lock, punto 2).dec-0127 P5); la app de TV (F-APPLIANCE-PI3 / r59).Decisión de waxin vía AskUserQuestion (2026-10-02): lockear este ADR con las decisiones que ya
había tomado y con la enmienda que pide el lock de dec-0127 (LOCKED el mismo día, rama
w10/adr-datos).
IDENTITY_PASSWORD_LOGIN=enabled),
apagado por defecto, con argon2id y TOTP, sin ROPC (§8.3);state OIDC y canjeadas de forma atómica (§7);agentSafe (§11).dec-0127 P5 (aplicada en §6, §7.2, §12.2 y §19). El estado por perfil
tiene dos dueños: playback-svc (sesiones, progreso, preferencias de pista) y catalog-svc
(visto, favoritos, listas, proyección de progreso), con la clave (actorId, profileId) de este
ADR (accountId ≡ actorId).
transferProfileState se manda a playback-svc y a catalog-svc. identity-svc borra el
perfil de origen sólo cuando los dos confirman (saga de dos pasos idempotentes).evt.identity.profileDeleted lo consumen los dos y cada uno purga su parte.profile.state.export compone las dos exportaciones en el BFF o en el CLI, no en un
servicio (dec-0127 P5).pocket-id con tokens de registro, tras un spike contra la versión de
Pocket ID que se fije; el modo manual sigue disponible;IDENTITY_ROLE_SOURCE=config+db por defecto;member+ (nadie pierde acceso);amr) y auth_time reciente para aprobar dispositivos con perfil
gestor y para crear API keys;track/identity. El lock no los añade
a styx.model.yml. La lista de §17 (accounts, invites, devices, keys, password) queda como
propuesta para la wave que implemente este ADR, que la lleva a waxin con check:roadmap.IDENTITY_ROLE_SOURCE, TS2-P3-05), el password local tras el flag en el README de
identity-svc, shared = concedido en @styx/authz y los claims aditivos pid, acv,
cred, scp y amr sobre dec-0113 (que sigue PROPOSED: los claims entran cuando se lockee
su formato, sin cambiarlo).El lock desbloquea el código de producción de este ADR, en el orden de §15.3: primero SW20
(proxy /auth del BFF y authedServerFn), después el resto. Las páginas guias/cuentas/**
siguen en estado especificado hasta que haya código.
dec-0124
Vista generada de dec-0124: Documentación como contrato: Fumadocs, Diátaxis, frontmatter de contrato, referencia generada y guards que impiden posponerla
dec-0126
Vista generada de dec-0126: Metadatos incrustados en el propio fichero: plugin `metadata-embed`, escritura en el daemon y huella que los excluye