import { pool } from "../db/pool.ts"; import { cifrar, descifrar, huella } from "../lib/crypto.ts"; import { loadEnv } from "../lib/env.ts"; /** * Todo lo que hace falta para hablar con la subcuenta de UN negocio. * * Sustituye al `locationId: string` suelto que antes viajaba por once firmas. * Es un objeto y no dos parámetros a propósito: dos `string` seguidos se pueden * cruzar sin que el compilador diga nada, y cruzarlos aquí significa mandar el * token de un cliente a la subcuenta de otro. * * Es el ÚNICO sitio del código donde el token existe descifrado, y solo en * memoria. Ni se registra, ni se audita, ni sale por la API. */ export interface CrmCtx { businessId: number; locationId: string; token: string; } interface FilaCredencial { location_id: string; token_cipher: Buffer | null; token_nonce: Buffer | null; token_tag: Buffer | null; } /** * Carga la credencial del negocio y la descifra. * * Distingue «no vinculado» de «vinculado sin token» a propósito: son dos * situaciones con dos arreglos distintos, y un solo mensaje para las dos manda * a quien lo lea a mirar donde no es. */ export async function ctxDe(businessId: number): Promise { const { rows } = await pool.query( `SELECT location_id, token_cipher, token_nonce, token_tag FROM crm_connections WHERE business_id = $1`, [businessId] ); const c = rows[0]; if (!c) { throw { status: 409, error: "Este negocio no está vinculado a Bucéfalo CRM" }; } if (!c.token_cipher || !c.token_nonce || !c.token_tag) { throw { status: 409, error: "Este negocio no tiene token de Bucéfalo CRM. Vincúlalo desde la consola de administración.", }; } return { businessId, locationId: c.location_id, token: descifrar({ cipher: c.token_cipher, nonce: c.token_nonce, tag: c.token_tag }), }; } /** * Guarda o rota la credencial de un negocio. Idempotente. * * La etiqueta se conserva si no se manda otra: al rotar un token caducado nadie * quiere volver a teclear el nombre de la subcuenta, y perderlo en silencio * dejaría la consola llena de cuentas sin identificar. */ export async function guardarCredencial( businessId: number, locationId: string, token: string, label?: string ): Promise { const c = cifrar(token); await pool.query( `INSERT INTO crm_connections (business_id, location_id, token_cipher, token_nonce, token_tag, token_fingerprint, token_updated_at, label) VALUES ($1,$2,$3,$4,$5,$6, now(), $7) ON CONFLICT (business_id) DO UPDATE SET location_id = EXCLUDED.location_id, token_cipher = EXCLUDED.token_cipher, token_nonce = EXCLUDED.token_nonce, token_tag = EXCLUDED.token_tag, token_fingerprint = EXCLUDED.token_fingerprint, token_updated_at = now(), label = COALESCE(EXCLUDED.label, crm_connections.label)`, [businessId, locationId, c.cipher, c.nonce, c.tag, huella(token), label ?? null] ); } /** * Desvincula: borra la credencial y **conserva** la conexión y todo lo ya * sincronizado. Quitar el token no es motivo para tirar 3 200 contactos, sus * conversaciones y la atribución que costó traer. */ export async function olvidarCredencial(businessId: number): Promise { await pool.query( `UPDATE crm_connections SET token_cipher = NULL, token_nonce = NULL, token_tag = NULL, token_fingerprint = NULL, token_updated_at = NULL WHERE business_id = $1`, [businessId] ); } /** * Contexto construido desde el entorno, **solo para los scripts de sondeo**. * * El servidor nunca debe usar esto: sus credenciales salen de la base, por * negocio, vía `ctxDe`. Aquí existe porque los spikes se lanzan a mano contra * la subcuenta que esté configurada en `platform/.env`, antes incluso de que * exista una fila en `crm_connections`. */ export function ctxDesdeEnv(businessId = 0): CrmCtx { // Carga el .env explícitamente: depender de que otro import lo haya hecho // antes funciona por casualidad y se rompe al reordenar los imports. loadEnv(); const locationId = process.env.CRM_LOCATION_ID; const token = process.env.CRM_TOKEN; if (!locationId || !token) { throw new Error( "Faltan CRM_LOCATION_ID y/o CRM_TOKEN en platform/.env — este script los necesita para hablar con la subcuenta." ); } return { businessId, locationId, token }; }