La administración de BloomX se puede hacer también con comandos: una terminal en el navegador (/admin/profile/console) y una CLI por API (bloomx, paquete @kyntocg/bloomx-cli) que se conecta por HTTPS a la misma instancia. Ambas usan el mismo motor: un catálogo de comandos tipado cuyos handlers llaman a las MISMAS rutas /api/admin/* que la consola gráfica (misma validación, límites y auditoría), así que nada se duplica y nada queda fuera.
SSH real: no en Vercel
Vercel solo sirve HTTP(S): no puede exponer el puerto 22. bloomx ssh es una experiencia equivalente (un shell interactivo con autocompletado e historial) sobre HTTPS, no el protocolo SSH. Cómo ofrecer SSH de verdad con una pasarela está descrito en SSH real (opcional), sin implementarlo.
bloomx login https://mail.example.com pide correo, contraseña (sin eco) y el código MFA si aplica, y guarda un token (nunca la contraseña) en tu almacén de usuario con permisos 0600 (~/.config/bloomx/credentials.json, %APPDATA%\bloomx en Windows; jamás en el repositorio). La cuenta debe ser un administrador de esa instancia (permission_level ≥ 1) o la manager dueña del dominio de la instancia en el backend compartido. Tras el login la CLI muestra el dominio al que quedó acotada la sesión y todos los comandos operan solo sobre ese dominio: no existe una bandera --domain.
bash
bloomx login https://mail.example.com # interactivo
bloomx login https://mail.example.com --token bxa_... # con un token creado en la consola web (tokens create)
bloomx whoami # cuenta, rol, permission_level, dominio y ámbitos
bloomx profiles | bloomx use mail.acme.com # varias instancias (perfiles)
bloomx logout [--all] # revoca el token en el servidor
bloomx help # catálogo (solo lo que tu nivel permite)
bloomx help users sessions revoke # un comando
bloomx users list --status disabled --json | jq .
bloomx users create ana@example.com --name "Ana" # contraseña temporal, se muestra UNA vez
bloomx audit --event "admin.users.*" --from 2026-01-01
bloomx theme export > theme.json && bloomx theme import --file theme.json --dry-run
bloomx shell # REPL (alias: bloomx ssh): Tab, ↑/↓, Ctrl+C, Ctrl+L
Salida: tablas y texto con los colores de tu terminal (o del tema en la web); --json entrega datos en bruto para pipes; los comandos que devuelven CSV lo escriben a stdout (> users.csv) o con --out.
Entrada: --file ruta.json o --stdin (pipe) para comandos con JSON/CSV; en la consola web, --stdin abre un modo pegado (termina con una línea EOF).
Códigos de salida: 0 ok · 1 error del comando · 2 uso incorrecto · 3 sin permiso o sin sesión · 4 falta confirmación o step-up · 5 límite de tasa/tiempo · 6 red · 127 comando desconocido.
Consola web (/admin/profile/console, enlazada en el menú y en «Mi perfil»): historial con ↑/↓ (solo en la pestaña), Tab (comandos, subcomandos, banderas, valores y correos de usuarios), help, clear/Ctrl+L, exit, Ctrl+C que cancela, copiar salida, región role="log" accesible y diseño adaptable.
Importar archivos de correo: bloomx transfer import <archivo> sube por trozos con hash y reanudación (--resume); transfer download <job> --out baja una exportación. El navegador no puede leer archivos locales desde la terminal: para eso está el asistente de /admin/transfer.
El rol de administrador ya no es un sí/no: cada cuenta tiene un `permission_level` de 0 a 4 (columna "permission_level" de la tabla UserPermission, el mismo nombre en la API, la CLI y el JSON). El nivel efectivo es el máximo entre el del entorno y el de la base de datos. Cada ruta, comando y sección del menú declara su nivel mínimo en un único sitio (src/lib/admin-levels.ts); la comprobación vive en requireLevel(n) (rutas) y en el motor de comandos, de modo que ocultar un botón nunca es la única barrera (las rutas responden 403 aunque se llamen directamente). requireAdmin equivale a nivel ≥ 3.
Nivel
Nombre
Qué permite
0
user
Usuario normal: sin acceso al admin.
1
support
Solo lectura: usuarios, auditoría, estado del sistema, correo, spam y marca en lectura. Nada que escriba.
2
operator
Gestiona cuentas: crear, deshabilitar y reactivar usuarios, cuotas, sesiones (revocar), supresiones y listas de spam. No toca configuración global ni seguridad.
3
admin
Configuración de la instancia: marca/temas/landing, extensiones, retención, cuotas del dominio, política de spam, transferencias de buzones, restablecer contraseñas. Gestiona niveles 0-2.
4
superadmin
Todo: claves de firma, extensiones obligatorias y credenciales, restablecer MFA, purga de retención real y niveles de otros (0-4). Conceder el 4 exige confirmación y step-up con MFA.
`ADMIN_EMAILS` es semilla y rescate: esos correos son nivel 4 «fijado por entorno»; la consola y la CLI no pueden degradarlos (solo quitándolos de la variable). Compatibilidad total: quien hoy es admin queda en 4. ADMIN_EMAILS_LOCKED=true = modo solo-entorno (la gestión por consola/CLI se desactiva: nivel 4 si está en el entorno, 0 si no). La manager dueña del dominio se trata como nivel 4 en su instancia.
Quién asigna qué: solo un nivel estrictamente menor al propio (un superadmin asigna 0-4; conceder el 4 exige confirmación explícita --confirm-super). Nadie modifica a alguien de nivel igual o superior (un superadmin sí puede revocar a otro superadmin de consola, nunca al del entorno) ni a sí mismo: nadie se auto-escala ni se auto-degrada, y la instancia nunca se queda sin superadmin.
Cada cambio exige step-up con MFA (un código TOTP reciente; la manager usa su contraseña), exige que la cuenta exista y tenga MFA (o que el MFA sea obligatorio y se enrole en su primer acceso), respeta un máximo de 25 cuentas con nivel ≥ 3, tiene rate limit, se audita (admin.permissions.changed: quién, a quién, nivel anterior → nuevo, IP; historial inmutable en UserPermissionHistory) y avisa por correo a la cuenta y a los superadmins. Los cambios aplican a las sesiones abiertas en ≤ 30 s y bajar o quitar el nivel invalida de inmediato sus tokens de CLI (a nivel 0 también su sesión privilegiada).
Acciones sobre otras cuentas (deshabilitar, restablecer, cuotas, sesiones, desvincular...): el actor debe tener un nivel mayor que el de la cuenta objetivo (cannot_modify_peer_or_higher).
Menú y vistas: el menú oculta lo que tu nivel no permite; la vista Permisos (/admin/permissions) muestra la escala, las cuentas con nivel ≥ 1 (origen entorno/consola, quién lo concedió y cuándo), el selector «Asignar nivel» con step-up y el historial; la ficha de usuario muestra «Nivel de permisos: N · nombre» con un selector de solo los niveles que puedes asignar.
Tokens de CLI: heredan el nivel de la cuenta como tope (nunca más; si la cuenta baja, el token baja) y además tienen su ámbito: read desde el nivel 1, write y security desde el 2 (los comandos de seguridad siguen exigiendo su propio nivel).
bash
bloomx perms levels # la escala 0-4
bloomx perms list [--json] # cuentas con nivel >= 1, origen y estado
bloomx perms set ana@example.com 2 --note "helpdesk" # step-up con código MFA
bloomx perms set cto@example.com 4 --confirm-super # conceder superadmin
bloomx perms history --email ana@example.com
bloomx perms unlock ana@example.com # solo superadmin (ver sesión privilegiada)
Para toda cuenta con permission_level ≥ 1 solo puede haber una sesión privilegiada abierta a la vez, contando juntas la consola web de administración (/admin/**, incluida la consola de comandos) y el CLI interactivo (tokens de CLI). No afecta al correo web normal de la cuenta (que conserva sus sesiones habituales mientras no entre al admin).
La sesión nueva gana: en el primer uso de admin de una sesión web (su jti) o de un token interactivo que no es el dueño del slot (tabla PrivilegedSession, una fila por cuenta), la anterior se revoca en el acto en servidor (jti en RevokedSession o token revocado) y, al volver a usarse, recibe 401 `superseded` con un mensaje claro («Tu sesión se cerró porque iniciaste otra desde <dispositivo> (<IP>) a las <hora>»); la web redirige al login y el CLI explica qué hacer. Dos entradas simultáneas se serializan con un candado por cuenta: gana una y la otra queda revocada. Las renovaciones y refrescos conservan el jti: no son una sesión nueva.
Decisión sobre los tokens: por defecto todo token interactivo ocupa el slot, sin excepciones (también los de solo lectura creados desde la consola web). Los tokens «machine» (tokens create --machine: cron/CI) son la única clase que no lo ocupa: solo lectura, 24 h por defecto (máx. 7 d), nunca nivel 4 (tope 3), acotados por el nivel de la cuenta, emitidos solo con step-up MFA, listados y revocables (tokens list|revoke), con cada uso auditado e invalidados si la cuenta baja de nivel; respetan el bloqueo de la cuenta.
Aviso al cerrar la anterior: correo transaccional de la instancia (con la marca del dominio, es/en) y notificación push PWA a la cuenta, con qué sesión se cerró (web o CLI), IP, dispositivo, hora y qué hacer si no fuiste tú (cambiar contraseña y cerrar todas las sesiones). Dedupe: como mucho 1 aviso por minuto y cuenta; sin tokens ni datos sensibles. Si el reemplazo viene de una IP o dispositivo distinto, el aviso lo destaca y se envía además una alerta inmediata (correo + push) a todos los superadmins, con el evento de seguridad admin.session.superseded.suspicious.
Inactividad y tope: la sesión privilegiada se cierra tras 15 min sin actividad (configurable por instancia entre 5 y 60: session policy set --idle-minutes N, nivel 4 con MFA) y tiene un tope absoluto de 12 h; el siguiente uso recibe 401 `expired` (distinto de superseded). Los sondeos de estado de la cabecera no cuentan como actividad. La web revalida al volver a la pestaña, así que lo detecta al instante y redirige al login con el mensaje.
«Pelea de sesiones»: 3 reemplazos en 10 min (umbral y ventana configurables con session policy set --lock-threshold/--lock-window) bloquean el acceso privilegiado de la cuenta (web y CLI, también los tokens machine; el correo web normal no se toca) hasta que un superadmin la desbloquee (perms unlock <correo> o el botón de la vista Permisos, con step-up MFA). Al bloquear: correo + push al afectado (qué pasó y qué hacer), alerta a los demás superadmins y evento admin.session.lockout. Rescate: si queda bloqueado el único superadmin, define ADMIN_LOCKOUT_RESET=<correo> en el entorno, vuelve a desplegar, accede (el bloqueo se levanta) y quita la variable; con una manager dueña del dominio también hay desbloqueo desde su consola.
Los cierres normales (logout, «Cerrar sesión privilegiada», bloomx logout, caducidad) liberan el slot; bajar a nivel 0 revoca la sesión privilegiada. Las managers dueñas del dominio usan una cookie del backend compartido que esta instancia no puede revocar: el slot aplica a sus tokens de CLI, no a su sesión web de manager. La vista Permisos y «Mi perfil» muestran tu sesión privilegiada (dónde, desde cuándo, cuándo caduca) con el botón Cerrar sesión privilegiada (session show / session close).
Control
Referencia
Una sesión privilegiada por cuenta, el reemplazo revoca
Solo administradores: la consola web usa la cookie de admin (con la cabecera X-Requested-With como defensa CSRF adicional); la CLI usa tokensAuthorization: Bearer bxa_.... Cualquier otro caso es 401/403 (falla cerrado); el rol del token se revalida en cada petición.
Sin ejecución arbitraria: el analizador de línea (comillas y escapes) no es un shell (sin variables, pipes, globbing ni sustitución) y nunca ejecuta comandos del sistema; no lee archivos del servidor y los comandos nunca devuelven variables de entorno ni claves privadas (config get solo muestra una lista blanca y los secretos solo indican si existen).
Límites: línea de 8 KB, entrada de 512 KB, salida de 1 MB, 25 s por comando síncrono y 60 comandos/min por administrador (límite asíncrono, Redis si está configurado) más un límite por IP.
Riesgo, confirmación y step-up: cada comando es read, write, destructive o security. Los destructive y security exigen confirmación explícita (--yes o un y interactivo) y step-up (contraseña, código MFA o de recuperación; prueba HMAC de 10 min atada a tu cuenta; los de permisos y tokens exigen un código MFA).
Auditoría: cada comando ejecutado (y cada rechazo) se registra con el sistema de auditoría existente (usuario, comando saneado, riesgo, resultado, IP, agente, token por su id corto). Se redactan banderas y valores sensibles (contraseñas, tokens, claves, volcados JSON/CSV): nunca se registran. Se consulta con bloomx audit --event "admin.cli.*".
Aislamiento: funciona en cualquier instancia (nada fijado a un dominio) y no usa secretos globales del backend compartido; los tokens y los niveles son por instancia.
Se emiten tras iniciar sesión con contraseña y MFA, o desde la consola web con tokens create --name mi-equipo. En la base de datos solo vive el SHA-256 (tabla AdminCliToken); el valor se muestra una sola vez. Tienen nombre, ámbitos (read, write, security), caducidad (12 h por defecto, 30 d máximo), última IP/uso y se revocan al instante (tokens list, tokens revoke <id>, tokens revoke-all, bloomx logout). Máximo 10 activos por cuenta. Los de una manager guardan su cookie de sesión del backend cifrada para que los comandos que hablan con el backend funcionen igual que en la web.
Ámbito
Permite
Nivel mínimo
read
comandos read
1
write
read + write y destructive
2
security
todo (los comandos security exigen además step-up y su propio nivel)
Generado del catálogo real (159 comandos). Cada uno tiene descripción es/en, nivel mínimo, riesgo y permisos; bloomx help <comando> muestra sus argumentos y banderas. Globales: --json, --yes/-y, --help. Los comandos de este catálogo cubren todas las acciones de las vistas del admin (ver cobertura).
Un test comprueba que cada ruta `/api/admin/`** está cubierta por un comando o excluida con un motivo técnico concreto. Excluidas:
Ruta
Motivo y equivalente
GET /api/admin/extensions/stars
Favoritas del marketplace: preferencia personal de la interfaz web de cada administrador (ligada a su sesion de usuario), sin sentido para un token de CLI ni efecto en la instancia.
PUT /api/admin/extensions/stars
Favoritas del marketplace: preferencia personal de la interfaz web de cada administrador (ligada a su sesion de usuario), sin sentido para un token de CLI ni efecto en la instancia.
POST /api/admin/login
Es el login web del manager (fija la cookie auth_session). La CLI usa POST /api/admin/cli/login (token) y la consola web ya esta autenticada.
POST /api/admin/logout
Cierra la cookie de la sesion web; en la CLI el equivalente es bloomx logout (revoca el token) y tokens revoke.
GET /api/admin/retention
Endpoint del cron de Vercel (CRON_SECRET) que ejecuta la purga; el equivalente manual es retention run (simulacro por defecto, --apply para borrar), que usa POST /api/admin/retention/run.
POST /api/admin/retention
Alias POST del cron anterior; cubierto por retention run.
POST /api/admin/reprocess-attachments
Autentica con la cookie de SESION de usuario (getCurrentUser) y reprocesa correos del propio usuario o por id: no es una accion del guardia de admin y un token de CLI no tiene cookie de usuario. Para administradores existe el script npm run attachments:reprocess.
PUT /api/admin/profile/password
Cambia la contrasena y REEMITE la cookie de sesion del navegador (setSessionCookie): no tiene sentido con un token. Alternativas: la consola web (/admin/profile) y, para otros usuarios, users password-reset / users force-password-change.
GET /api/admin/profile
Depende de la cookie de usuario (getCurrentUser); el comando profile entrega lo mismo (identidad, MFA, sesiones) desde el actor autenticado.
GET /api/admin/profile/sessions
Marca "esta sesion" con la cookie del navegador; equivalente: users sessions me.
DELETE /api/admin/profile/sessions
Reemite la cookie de este navegador tras revocar; equivalente: users sessions revoke me.
DELETE /api/admin/profile/sessions/[jti]
Compara con la cookie actual para no cerrar "esta sesion"; equivalente: users sessions revoke me --jti <jti>.
Si necesitas el protocolo SSH de verdad, la opción es una pasarela en un VPS (o contenedor) con el puerto 22 abierto que proxea a esta API: nunca expone el shell del sistema, solo traduce SSH a llamadas HTTPS autenticadas.
Servidor SSH con ssh2 en el VPS: solo autenticación por clave pública (sin contraseñas), una clave por administrador, asociada a su cuenta y a un token de CLI guardado en el almacén del gateway (cifrado).
Cada sesión abre un pseudo-terminal que ejecuta el REPL de bloomx en proceso (sin /bin/sh): cada línea se envía a POST /api/admin/cli/exec con el token del administrador; el autocompletado usa GET /api/admin/cli/complete.
Hereda todas las garantías del servidor (niveles, ámbitos, step-up, auditoría, slot único de sesión privilegiada, inactividad): el gateway no decide nada. El step-up se pide en el terminal y se reenvía a POST /api/admin/cli/reauth.
Endurecimiento: ForceCommand/shell fijo, sin reenvío de puertos ni de agente, límites de conexiones por IP, fail2ban, registros enviados a tu SIEM y rotación de tokens (el slot único cierra el anterior al reconectar).