96 lines
2.7 KiB
JavaScript
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));
|
|
}
|