CreaRack-SL

Módulo agent_auth.js · Interceptor de fetch con token de Agent local

Ubicación

static/js/modules/agent_auth.js (96 LOC) — Cargado en templates/base.html con atributo defer, antes que otros módulos.

Propósito

Intercepta todas las llamadas window.fetch() dirigidas al Agent local (localhost:5050 / 127.0.0.1:5050) para añadir automáticamente el header Authorization: Bearer <token>, sin tocar los ~35 call-sites cliente uno a uno.

Parte de la Fase 2 del ADR decision--20260610--terminal-auth-local-cross-origin.

Arquitectura

IIFE (Immediately Invoked Function Expression)

El módulo envuelve toda su lógica en un IIFE anónimo para evitar contaminar el namespace global. Expone solo 2 funciones públicas en window:

  • window.getAgentLocalToken() → Promise que resuelve al token (o null)
  • window.downloadFromAgent(url, filename) → descarga vía fetch+blob

Componentes principales

1. Constantes y setup

const AGENT_HOSTS = ["localhost:5050", "127.0.0.1:5050"];
const origFetch = window.fetch.bind(window);
let tokenPromise = null;
  • AGENT_HOSTS: Hosts a los que aplicar la reescritura.
  • origFetch: Captura la fetch original antes de sobrescribirla (evita recursión infinita).
  • tokenPromise: Caché del token (Promise). Se resuelve solo una vez por carga de página.

2. Helper: urlOf(input)

Extrae el URL string desde un input que puede ser string o un objeto { url: string, ... } (segundo param de fetch).

function urlOf(input) {
    if (typeof input === "string") return input;
    if (input && typeof input.url === "string") return input.url;
    return "";
}

3. Helper: isAgentUrl(url)

Verifica si el URL pertenece a uno de los hosts del Agent.

function isAgentUrl(url) {
    return AGENT_HOSTS.some((h) => url.includes(h));
}

4. Función: fetchToken()

Obtiene el token de auth local una sola vez. Flujo:

  1. Identidad del Agent — GET http://localhost:5050/info

    • Usa origFetch (no interceptada) para evitar recursión.
    • Extrae saas_info.agent_id o agent_id.
    • Si falla o no hay agent_id, devuelve null.
  2. Token custodiado por SaaS — GET /api/agent/local-token?agent_id=<agentId>

    • Solo responde si hay sesión activa + fleet:view + agent pertenece al tenant.
    • Si no hay token aún (Fase 2, Agent no lo deposita hasta Fase 3), retorna 404 → null.
    • Si hay token, devuelve { local_token: "..." }.
  3. Manejo de errores — catch-all

    • Ante cualquier fallo (red, timeout, JSON, etc.), devuelve null.
    • No rompe nada: el llamador puede proceder sin token (comportamiento actual).
async function fetchToken() {
    try {
        const infoResp = await origFetch("http://localhost:5050/info");
        if (!infoResp.ok) return null;
        const info = await infoResp.json();
        const agentId = (info.saas_info && info.saas_info.agent_id) || info.agent_id;
        if (!agentId) return null;

        const tokResp = await origFetch(`/api/agent/local-token?agent_id=${encodeURIComponent(agentId)}`);
        if (!tokResp.ok) return null;
        const data = await tokResp.json();
        return data.local_token || null;
    } catch {
        return null;
    }
}

5. Getter memoizado: getAgentLocalToken()

Devuelve la Promise del token (resuelta en la primera llamada, cacheada en las siguientes).

function getAgentLocalToken() {
    if (!tokenPromise) tokenPromise = fetchToken();
    return tokenPromise;
}

6. Interceptor: window.fetch

Sustituye la fetch global con una envoltura que:

  1. Comprueba si el URL es de Agent.
  2. Si no, pasa directo a origFetch() (no-op).
  3. Si es de Agent:
    • Obtiene el token (async).
    • Si no hay token, llama a origFetch() sin cambios.
    • Si hay token, añade header Authorization: Bearer <token> y llama a origFetch() con los headers nuevos.
window.fetch = async function (input, init) {
    if (!isAgentUrl(urlOf(input))) return origFetch(input, init);

    const token = await getAgentLocalToken();
    if (!token) return origFetch(input, init);

    const opts = { ...(init || {}) };
    const headers = new Headers((init && init.headers) || (typeof input !== "string" && input.headers) || undefined);
    headers.set("Authorization", `Bearer ${token}`);
    opts.headers = headers;
    return origFetch(input, opts);
};

7. Función pública: downloadFromAgent(url, filename)

Descarga un archivo desde el Agent usando fetch+blob (para llevar headers) en lugar de window.open() (que no puede):

  1. Fetch al URL (pasa por el interceptor → añade token si existe).
  2. Convierte response en blob.
  3. Crea ObjectURL temporal.
  4. Crea un <a> con download attribute.
  5. Simula click.
  6. Limpia.

Se usa para descargas de logs de sesión SSH en:

  • ssh_integration.js (cuando se detiene la grabación)
  • TerminalToolbar.js (cuando se cierra una sesión)
async function downloadFromAgent(url, filename) {
    try {
        const resp = await window.fetch(url);
        if (!resp.ok) return false;
        const blob = await resp.blob();
        const objUrl = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = objUrl;
        a.download = filename || "download";
        document.body.appendChild(a);
        a.click();
        a.remove();
        URL.revokeObjectURL(objUrl);
        return true;
    } catch {
        return false;
    }
}

Flujo de integración

1. Primer load de página

  • base.html carga agent_auth.js (defer).
  • El IIFE ejecuta, reescribe window.fetch, expone getAgentLocalToken() y downloadFromAgent().
  • El token aún no se ha fetched (lazy).

2. Primera llamada fetch a Agent

  • Cualquier módulo llama fetch('http://localhost:5050/...').
  • El interceptor detecta que es Agent.
  • Llama a getAgentLocalToken(), que triggerea fetchToken() la primera vez.
  • Si hay token, se añade al header. Si no, se pasa sin cambios.
  • Las siguientes llamadas usan la misma Promise cacheada.

3. SSH iframe (ssh_client.js)

  • Cuando el iframe está listo, emite AGENT_READY.
  • El cliente SSH pregunta window.getAgentLocalToken().
  • Si hay token, envía { type: 'SET_AUTH_TOKEN', token } por postMessage (targetOrigin acotado).
  • Luego envía la config SSH normal.
  • Un Agent viejo ignora SET_AUTH_TOKEN (inocuo).

4. Descarga de logs

  • downloadFromAgent(url, filename) es llamada por TerminalToolbar.js o ssh_integration.js.
  • Internamente usa window.fetch() (interceptada), así que el token se añade automáticamente.
  • Crea el blob y descarga.

Garantías de seguridad

⚠️ Esta Fase 2 NO da seguridad por sí sola. Es fontanería:

  • ✅ El token se obtiene del SaaS (custodiado, validado contra sesión + permisos).
  • ✅ Se manda a cada llamada al Agent.
  • ✅ Es opcional (inocuo si aún no existe).
  • ❌ El Agent viejo ignora el token — no valida ni cierra conexiones.
  • ❌ CORS aún abierto en el Agent — cualquier origen puede llamar.

En Fase 3, el Agent .exe nuevo:

  • Exigirá el token en todas las llamadas.
  • Validará que sea válido y reciente.
  • Cerrará CORS: solo aceptará desde el SaaS.

Casos extremos y resiliencia

Token fetch falla (red down, etc.)

  • Devuelve null.
  • Las llamadas fetch pasan sin Authorization.
  • El sistema sigue funcionando (comportamiento pre-Fase 2).

Agent local no responde a /info

  • infoResp.ok === false → devuelve null.
  • Las llamadas pasan sin token.

SaaS retorna 404 en /api/agent/local-token

  • Indica que el token aún no está depositado (Fase 2 normal).
  • Devuelve null.
  • Las llamadas pasan sin token (aún no necesita).

Headers ya existen en la llamada fetch

  • Se crea un nuevo objeto Headers.
  • Se copia lo anterior (si existe).
  • Se sobrescribe Authorization.
  • Los demás headers se preservan.

Testing

Manual, en navegador con Agent local corriendo:

// En la consola del navegador:
await window.getAgentLocalToken()
// → Promise que resuelve al token (string) o null

window.downloadFromAgent('http://127.0.0.1:5050/terminal/session/<id>/log/download', 'test.log')
// → boolean (true si OK)

// Abre DevTools Network y llama a un endpoint del Agent:
fetch('http://localhost:5050/info')
// → Busca en la req el header Authorization: Bearer <token>

Dependencias

  • Cero: no importa nada. Solo sobrescribe window.fetch.

Usuarios del módulo

  • ssh_client.js — Obtiene token para iframe
  • ssh_integration.js — Descarga de logs
  • TerminalToolbar.js — Descarga de logs
  • Cualquier código que haga fetch() a Agent (automático)

Véase también

  • [[feature—terminal—auth-local-fase-2]]
  • [[decision—20260610—terminal-auth-local-cross-origin]]
  • [[entity—static—module—ssh-client]]
  • [[entity—static—class—terminaltoolbar]]
  • [[concept—security—cross-origin-local-auth]]