import { getCoreRedis, getIamRedis } from "./redis.ts"; /** * Estrategia de cache con Redis (Fase 4e). * * Regla no negociable: toda llave que contenga datos de negocio DEBE * incluir el tenant_id (ver cacheKeyCore). Una llave global para datos * por-tenant reintroduce el mismo IDOR cross-tenant que Row Level Security * (Fase 4b) se propuso cerrar -- solo que en Redis en vez de Postgres. * * Redis no es fuente de verdad de nada: si una llave expira o se pierde, * la siguiente lectura recalcula desde Postgres. Por eso alcanza con TTL * corto para datos de lectura frecuente/escritura poco frecuente * (catálogos, agregados de listados) y no hace falta invalidación * explícita en la mayoría de los casos. */ export function cacheKeyCore(tenantId: number | null, ...parts: (string | number)[]): string { return `core:cache:tenant:${tenantId ?? "none"}:${parts.join(":")}`; } export function cacheKeyIam(tenantId: number | null, ...parts: (string | number)[]): string { return `iam:cache:tenant:${tenantId ?? "none"}:${parts.join(":")}`; } async function cached( redis: Awaited>, key: string, ttlSeconds: number, compute: () => Promise, ): Promise { const hit = await redis.get(key).catch(() => null); if (hit != null) { try { return JSON.parse(hit) as T; } catch { // llave corrupta/formato viejo -- recalcular sin fallar el request } } const value = await compute(); await redis.set(key, JSON.stringify(value), { EX: ttlSeconds }).catch(() => {}); return value; } export async function cacheCore(key: string, ttlSeconds: number, compute: () => Promise): Promise { const redis = await getCoreRedis(); return await cached(redis, key, ttlSeconds, compute); } export async function cacheIam(key: string, ttlSeconds: number, compute: () => Promise): Promise { const redis = await getIamRedis(); return await cached(redis, key, ttlSeconds, compute); } export async function invalidateCore(key: string): Promise { await (await getCoreRedis()).del(key).catch(() => {}); }