-- --------------------------------------------------------------------------- -- De un negocio con un token global, a N negocios con credencial propia. -- -- Hasta aquí `crm_connections.location_id` ya era por negocio, pero el token -- vivía en la variable de entorno CRM_TOKEN, una sola para todo el proceso -- (platform/crm/client.ts). Con dos negocios eso usa el token del primero -- contra la subcuenta del segundo: 401 en el mejor caso, escritura en la -- subcuenta equivocada en el peor. -- -- El token se guarda CIFRADO con AES-256-GCM (platform/lib/crypto.ts). La clave -- maestra vive en CRM_MASTER_KEY, fuera de la base: quien consiga un volcado de -- Postgres no consigue los tokens de los clientes. -- --------------------------------------------------------------------------- ALTER TABLE crm_connections ADD COLUMN token_cipher bytea, ADD COLUMN token_nonce bytea, ADD COLUMN token_tag bytea, -- Los 6 últimos caracteres. Permite que la interfaz diga «termina en …f4a2c1» -- y detectar una rotación, sin exponer nunca la credencial. ADD COLUMN token_fingerprint text, ADD COLUMN token_updated_at timestamptz, -- MEDIDO (hallazgo 29): la subcuenta tiene 7 calendarios y la única cita real -- está en «Servicio Spa». Sin fijar cuál, empujar una cita al calendario del -- CRM sería adivinar a cuál. ADD COLUMN calendar_id text, -- La red de seguridad de mensajes pasa a ser POR NEGOCIO. Como variable de -- entorno global decidía por todas las cuentas a la vez: o se abrían los -- envíos reales para todas, o ninguna podía salir de pruebas. ADD COLUMN test_email text, ADD COLUMN allow_real_sends boolean NOT NULL DEFAULT false, -- Nombre legible de la subcuenta, para que la administración no tenga que -- reconocer cuentas por un identificador opaco. ADD COLUMN label text; -- La credencial va completa o no va. Media credencial produce un descifrado que -- falla en tiempo de petición, y eso es un fallo lejos de su causa. ALTER TABLE crm_connections ADD CONSTRAINT crm_connections_credencial_completa CHECK ( (token_cipher IS NULL AND token_nonce IS NULL AND token_tag IS NULL) OR (token_cipher IS NOT NULL AND token_nonce IS NOT NULL AND token_tag IS NOT NULL) ); COMMENT ON COLUMN crm_connections.token_cipher IS 'Token privado de la subcuenta, cifrado con AES-256-GCM. Nunca se devuelve por la API.'; COMMENT ON COLUMN crm_connections.token_fingerprint IS 'Los 6 ultimos caracteres del token. Lo unico de la credencial que puede salir del servidor.';