CreaRack-SL

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

  1. Descubrimiento (/info GET)

    • Llamada al Agent sin token: GET http://localhost:8384/info
    • Respuesta incluye metadatos: { agent_id, version, ...}
    • No requiere Bearer (en EXEMPT_PATHS)
  2. Recuperación (GET /info tras login SaaS)

    • User hace login en crearack.com
    • Frontend (login.html) redirige a /info tras 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
  3. Inyección en requests

    • Toda llamada a /api/* (local) añade header: Authorization: Bearer <token>
    • Middleware local (terminal/middleware.py) valida con verify_local_token()
  4. Cacheo con retries

    • Antes (Agent 2.1.0): getAgentLocalToken() memoizaba el resultado UNA vez. Si primera obtención devolvía null (Agent arrancando sin token depositado), promesa quedaba cached → página condenada a 401s perpetuos.
    • Ahora (Agent 2.1.1): null resetea la promesa → siguiente llamada reintenta → eventual éxito sin F5.

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 null si 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:

  1. Primera llamada: tokenPromise = null → ejecuta fetchToken()
  2. Si devuelve token: cachea + devuelve
  3. Si devuelve null: resetea tokenPromise = null para siguiente llamada (reintento)
  4. Siguiente llamada: tokenPromise = null nuevamente → reexecuta fetchToken()

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)

  1. .exe arranca, genera token, lo persiste (DPAPI)
  2. Frontend carga, intenta getAgentLocalToken()
  3. SaaS aún no ha depositado el token en su base de datos
  4. fetchToken() → null
  5. getAgentLocalToken() cachea el null → tokenPromise = Promise<null>
  6. Todas las llamadas posteriores usan token null
  7. Middleware local rechaza (sin Bearer válido) → 401s perpetuos
  8. 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. 1ª llamada devuelve null → cachea fallo
  2. 2ª llamada: tokenPromise fue reseteado → reintenta fetchToken()
  3. Mientras tanto, Agent depositó token en SaaS
  4. fetchToken() ahora devuelve token válido
  5. Token se cachea, rest of requests OK

Variables de control

Variable (local)Propósito
tokenPromiseAlmacena la promesa del token para memoización (con retries en fallos)
agentTokenAlias/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 el null indefinidamente → 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:8384 deben 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]]