Files
agenticSystem/mcp-server/auth/mcpTokens.js
2026-04-20 11:10:51 +00:00

96 lines
2.7 KiB
JavaScript

/**
* MCP Token Validation
*
* Valida tokens X-MCP-Secret contra Redis.
* El backend Python escribe la clave `mcp_tokens:<sha256_hex>` con metadata JSON:
* {
* "id": "...",
* "user": "superadmin",
* "project": "vegaasesores.com",
* "label": "MacBook Pro",
* "createdAt": 1234567890,
* "lastUsedAt": null | 1234567900
* }
*
* El plaintext es "acai_<43 chars>" y se hashea con sha256 hex en minusculas.
*/
import Redis from "ioredis";
import crypto from "node:crypto";
const REDIS_URL = process.env.REDIS_URL || "redis://redis:6379";
// Cliente Redis compartido (lazy init para no conectar si el MCP corre en modo stdio
// u otros escenarios donde el middleware HTTP nunca se invoque).
let redisClient = null;
function getRedis() {
if (redisClient) return redisClient;
try {
redisClient = new Redis(REDIS_URL, {
lazyConnect: false,
maxRetriesPerRequest: 3,
enableReadyCheck: false,
});
redisClient.on("error", (err) => {
console.error("[mcp-tokens] redis error:", err.message);
});
} catch (e) {
console.error("[mcp-tokens] no se pudo inicializar redis:", e.message);
redisClient = null;
}
return redisClient;
}
/**
* Hashea un string con SHA256 y devuelve hex en minusculas.
*/
export function sha256Hex(str) {
return crypto.createHash("sha256").update(str, "utf8").digest("hex");
}
/**
* Valida un X-MCP-Secret plaintext contra Redis.
* @param {string} secret - plaintext tipo "acai_xxx"
* @returns {Promise<{user: string, project: string, id: string} | null>}
*/
export async function validateMcpToken(secret) {
if (!secret || typeof secret !== "string") return null;
const r = getRedis();
if (!r) return null;
const sha = sha256Hex(secret);
const key = `mcp_tokens:${sha}`;
let raw;
try {
raw = await r.get(key);
} catch (err) {
console.error("[mcp-tokens] redis GET error:", err.message);
return null;
}
if (!raw) return null;
let meta;
try {
meta = JSON.parse(raw);
} catch {
return null;
}
if (!meta || !meta.user || !meta.project) return null;
// Actualizacion asincrona de lastUsedAt — no bloqueamos la request.
updateLastUsedAt(key, meta).catch((e) => {
console.error("[mcp-tokens] lastUsedAt update failed:", e.message);
});
return { user: meta.user, project: meta.project, id: meta.id || "" };
}
async function updateLastUsedAt(key, meta) {
const r = getRedis();
if (!r) return;
const next = { ...meta, lastUsedAt: Math.floor(Date.now() / 1000) };
await r.set(key, JSON.stringify(next));
}