Volver a la wiki

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:

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;

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:

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

2. Primera llamada fetch a Agent

3. SSH iframe (ssh_client.js)

4. Descarga de logs

Garantías de seguridad

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

En Fase 3, el Agent .exe nuevo:

Casos extremos y resiliencia

Token fetch falla (red down, etc.)

Agent local no responde a /info

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

Headers ya existen en la llamada fetch

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

Usuarios del módulo

Véase también

Subir