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 lafetchoriginal 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:
-
Identidad del Agent —
GET http://localhost:5050/info- Usa
origFetch(no interceptada) para evitar recursión. - Extrae
saas_info.agent_idoagent_id. - Si falla o no hay
agent_id, devuelvenull.
- Usa
-
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: "..." }.
- Solo responde si hay sesión activa +
-
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).
- Ante cualquier fallo (red, timeout, JSON, etc.), devuelve
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:
- Comprueba si el URL es de Agent.
- Si no, pasa directo a
origFetch()(no-op). - 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 aorigFetch()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):
- Fetch al URL (pasa por el interceptor → añade token si existe).
- Convierte response en blob.
- Crea ObjectURL temporal.
- Crea un
<a>condownloadattribute. - Simula click.
- 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.htmlcargaagent_auth.js(defer).- El IIFE ejecuta, reescribe
window.fetch, exponegetAgentLocalToken()ydownloadFromAgent(). - 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 triggereafetchToken()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 porTerminalToolbar.jsossh_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→ devuelvenull.- 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 iframessh_integration.js— Descarga de logsTerminalToolbar.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]]