Servicio de autenticación Agent en frontend (`agent_auth.js`)
Propósito
Recuperar y cachear el token Bearer local del Agent en el frontend (navegador), inyectarlo en requests HTTP/WS hacia la API local del Agent (localhost:8384).
Función pública: getAgentLocalToken() -> Promise<string | null>
Ubicación: static/js/modules/agent_auth.js
Arquitectura
Ciclo de obtención del token
-
Descubrimiento (
/infoGET)- Llamada al Agent sin token:
GET http://localhost:8384/info - Respuesta incluye metadatos:
{ agent_id, version, ...} - No requiere Bearer (en EXEMPT_PATHS)
- Llamada al Agent sin token:
-
Recuperación (
GET /infotras login SaaS)- User hace login en crearack.com
- Frontend (login.html) redirige a
/infotras iniciar sesión SaaS - SaaS incluye en la respuesta el token local que depositó el Agent en handshake inicial
- Frontend almacena en variable local
agentToken
-
Inyección en requests
- Toda llamada a
/api/*(local) añade header:Authorization: Bearer <token> - Middleware local (
terminal/middleware.py) valida converify_local_token()
- Toda llamada a
-
Cacheo con retries
- Antes (Agent 2.1.0):
getAgentLocalToken()memoizaba el resultado UNA vez. Si primera obtención devolvíanull(Agent arrancando sin token depositado), promesa quedaba cached → página condenada a 401s perpetuos. - Ahora (Agent 2.1.1):
nullresetea la promesa → siguiente llamada reintenta → eventual éxito sin F5.
- Antes (Agent 2.1.0):
Componentes
fetchToken() -> Promise<string | null>
- Propósito: solicitar token al backend del SaaS
- Entrada: nada (usa contexto de sesión)
- Salida: token local del Agent o
nullsi pre-activación / sin sesión - HTTP: GET a endpoint SaaS (ej.
GET /api/agent/local-token) - Error handling: catch →
null
getAgentLocalToken() -> Promise<string | null>
- Propósito: acceso público (cachea pero con retries en fallos)
- Entrada: nada
- Salida: token o
null - Caché: SÍ, pero temporal — se resetea si falla
- Implementación:
let tokenPromise = null;
function getAgentLocalToken() {
// Memoiza el token UNA vez por carga de página, pero NO cachea el fallo:
// si la 1ª obtención devuelve null (Agent recién arrancado, token aún no
// depositado, o sin sesión), se resetea la promesa para reintentar en la
// siguiente llamada.
if (!tokenPromise) {
tokenPromise = fetchToken().then((token) => {
if (!token) tokenPromise = null; // Reset si falla
return token;
});
}
return tokenPromise;
}
Lógica:
- Primera llamada:
tokenPromise = null→ ejecutafetchToken() - Si devuelve token: cachea + devuelve
- Si devuelve
null: reseteatokenPromise = nullpara siguiente llamada (reintento) - Siguiente llamada:
tokenPromise = nullnuevamente → reexecutafetchToken()
Flujo de uso
En requests HTTP
// ej. GET /api/cluster/list
async function listClusters() {
const token = await getAgentLocalToken();
const headers = {};
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const response = await fetch("http://localhost:8384/api/cluster/list", {
method: "GET",
headers,
});
if (response.status === 401 && token) {
// Reintentar sin token o con token fresco
return listClusters();
}
return response.json();
}
En WebSocket (consola del Agent)
// ej. terminal.js
async function connectTerminal() {
const token = await getAgentLocalToken();
const ws = new WebSocket("ws://localhost:8384/ws/terminal", {
// Suele ir en mensaje de handshake, no en header HTTP
});
ws.onopen = () => {
ws.send(JSON.stringify({
type: "connect",
auth_token: token, // Enviado en mensaje, no en header
}));
};
}
Integración con bootstrap del Agent
Problema (Agent 2.1.0)
.exearranca, genera token, lo persiste (DPAPI)- Frontend carga, intenta
getAgentLocalToken() - SaaS aún no ha depositado el token en su base de datos
fetchToken()→nullgetAgentLocalToken()cachea elnull→tokenPromise = Promise<null>- Todas las llamadas posteriores usan token
null - Middleware local rechaza (sin Bearer válido) → 401s perpetuos
- Dashboard queda bloqueado hasta F5
Solución (Agent 2.1.1)
tokenPromise = fetchToken().then((token) => {
if (!token) tokenPromise = null; // <<< NUEVO: resetea en fallo
return token;
});
Ahora:
- 1ª llamada devuelve
null→ cachea fallo - 2ª llamada:
tokenPromisefue reseteado → reintentafetchToken() - Mientras tanto, Agent depositó token en SaaS
fetchToken()ahora devuelve token válido- Token se cachea, rest of requests OK
Variables de control
| Variable (local) | Propósito |
|---|---|
tokenPromise | Almacena la promesa del token para memoización (con retries en fallos) |
agentToken | Alias/almacenamiento alternativo si ya está en sesión |
Integración con middleware HTTP
El middleware local valida todo request:
# terminal/middleware.py
def __call__(self, scope, receive, send):
auth_header = dict(scope.get("headers", [])).get(b"authorization", b"").decode()
if path not in EXEMPT_PATHS:
if auth_required(auth_manager):
if not auth_header.startswith("Bearer "):
return 401_response()
token = auth_header.split(" ")[1]
if not verify_local_token(token):
return 401_response()
return self.app(scope, receive, send)
Cuando getAgentLocalToken() inyecta el Bearer, middleware lo valida contra verify_local_token().
Historias de versión
Agent 2.1.0 (s128)
- Token Bearer requerido en API local
- ❌
getAgentLocalToken()cachea elnullindefinidamente → 401 perpetuos en bootstrap
Agent 2.1.1 (s129 — Fix arranque)
- ✅
getAgentLocalToken()resetea promesa en fallos → retries automáticos - ✅ Integra con flag
AGENT_LOCAL_AUTH_ENFORCE(gate desactivable)
Requisitos de cliente (navegador)
- JS ES2020+: async/await, Promise, fetch API
- CORS: requests a
localhost:8384deben permitir CORS desde origen SaaS - Sesión SaaS: token depositado en sesión tras handshake inicial del Agent
Véase también
- [[feature—terminal—s129-fix-agent-2-1-1-arranque]]
- [[entity—terminal—service—local-token]]
- [[concept—terminal—bootstrap-agent]]
- [[concept—saas—multi-tenancy]]