// packages/core/middlewares/src/tenantContext.mjs const VALID_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/; // schema seguro const UUID_RX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; function redact(obj) { // Evita loggear datos sensibles; muestra sólo lo útil para diagnóstico if (!obj || typeof obj !== 'object') return obj; const out = {}; for (const k of Object.keys(obj)) { if (['token', 'access_token', 'id_token', 'refresh_token'].includes(k)) { out[k] = '[redacted]'; } else if (k === 'sub' || k === 'email' || k === 'name') { out[k] = obj[k]; } else if (k === 'tenant') { const t = obj[k] || {}; out[k] = { id: t.id ?? null, schema: t.schema ?? null }; } else if (k === 'user') { const u = obj[k] || {}; out[k] = { sub: u.sub ?? null, email: u.email ?? null, default_tenant: u.default_tenant ?? u.defaultTenant ?? null, memberships: Array.isArray(u.memberships) ? `[${u.memberships.length}]` : null, }; } else { // no inundar el log; deja constancia de que existe out[k] = '[present]'; } } return out; } export function tenantContext(opts = {}) { const { requireUser = true, debug = false, log = console, // podés inyectar tu logger autoDeriveFromDefault = true, // callback opcional para buscar tenant (p.ej., en CORE) si no está en sesión // Debe devolver { id: uuid, schema: string } o null resolveTenant = null, schemaPrefixes = [ process.env.TENANT_SCHEMA_PREFIX || 'empresa_', ].filter(Boolean), } = opts; const diag = (msg, data) => { if (!debug) return; try { log.debug?.(`[tenantContext] ${msg}`, data !== undefined ? redact(data) : ''); } catch { /* noop */ } }; const setDiagHeader = (res, kv) => { if (!debug) return; const cur = res.getHeader('X-Tenant-Diag'); const base = typeof cur === 'string' ? String(cur) + '; ' : ''; res.setHeader('X-Tenant-Diag', base + kv); }; return async (req, res, next) => { try { diag('incoming', { sid: req.sessionID, headers: { accept: req.headers.accept } }); const sess = req.session; if (!sess) { setDiagHeader(res, 'no-session'); return res.status(401).json({ error: 'unauthenticated' }); } diag('session.present', { keys: Object.keys(sess) }); if (requireUser && !sess.user?.sub) { diag('user.missing', { session: sess }); setDiagHeader(res, 'no-user'); return res.status(401).json({ error: 'unauthenticated' }); } if (requireUser) diag('user.ok', sess.user); // 1) Leer tenant desde sesión let t = sess.tenant ?? null; diag('session.tenant', t); // 2) Derivar automáticamente si falta if ((!t?.id || !t?.schema) && autoDeriveFromDefault) { const fallbackId = sess.user?.tenant?.id || sess.user?.default_tenant || sess.user?.defaultTenant || null; if (fallbackId && UUID_RX.test(String(fallbackId))) { const prefix = String(schemaPrefixes[0] || 'empresa_'); const schema = `${prefix}${String(fallbackId).replace(/-/g, '').toLowerCase()}`; t = { id: String(fallbackId), schema }; sess.tenant = t; // persistir para siguientes requests diag('derived.fromDefault', t); setDiagHeader(res, 'derived-default'); } else { diag('derived.fromDefault.skipped', { fallbackId }); } } // 3) Resolver con callback si aún falta if ((!t?.id || !t?.schema) && typeof resolveTenant === 'function') { try { t = await resolveTenant(req, sess); if (t) { sess.tenant = t; diag('derived.fromResolver', t); setDiagHeader(res, 'derived-resolver'); } else { diag('resolver.returned-null'); } } catch (e) { diag('resolver.error', { message: e?.message }); } } // 4) Validaciones if (!t?.id || !t?.schema) { diag('missing-tenant.final'); setDiagHeader(res, 'missing-tenant'); return res.status(401).json({ error: 'Sesión inválida o tenant no seleccionado' }); } if (!UUID_RX.test(String(t.id))) { diag('invalid-tenant-id', t); setDiagHeader(res, 'bad-tenant-id'); return res.status(400).json({ error: 'TenantID inválido' }); } if (!VALID_IDENT.test(t.schema)) { diag('invalid-schema', t); setDiagHeader(res, 'bad-schema'); return res.status(400).json({ error: 'Schema inválido' }); } const okPrefix = schemaPrefixes.some(p => t.schema.toLowerCase().startsWith(String(p).toLowerCase()), ); if (!okPrefix) { diag('schema-prefix.rejected', { schema: t.schema, schemaPrefixes }); setDiagHeader(res, 'schema-prefix-rejected'); return res.status(400).json({ error: 'Schema no permitido' }); } // 5) OK req.tenant = { id: String(t.id), schema: String(t.schema) }; res.locals.tenant = req.tenant; setDiagHeader(res, `ok schema=${req.tenant.schema}`); diag('attach.req.tenant', req.tenant); return next(); } catch (err) { diag('exception', { message: err?.message }); return next(err); } }; }