From db46dc9cf326a2eb28944a5b20334fb290ef112a Mon Sep 17 00:00:00 2001 From: Carlos Narro Date: Sun, 7 Jun 2026 19:00:36 +0200 Subject: [PATCH] =?UTF-8?q?Bypass=20de=20pruebas:=20solo=20se=20llaman=20n?= =?UTF-8?q?=C3=BAmeros=20en=20RETELL=5FALLOWED=5FNUMBERS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guarda en iniciarLlamadaSaliente: si RETELL_ALLOWED_NUMBERS tiene valor (CSV de E.164), solo se lanza la llamada a esos números; el resto se omite (devuelve null, funnel en simulado). Vacío = se llama a todos. Protege tanto el form (procesarLead) como el canal llamada (pedirLlamada). En prod = +34651194617. Co-Authored-By: Claude Opus 4.8 --- mvp/b2c/.env.example | 3 +++ mvp/b2c/src/lib/env.ts | 3 +++ mvp/b2c/src/lib/voice/retell.ts | 12 ++++++++++++ 3 files changed, 18 insertions(+) diff --git a/mvp/b2c/.env.example b/mvp/b2c/.env.example index e59faa9..82d1778 100644 --- a/mvp/b2c/.env.example +++ b/mvp/b2c/.env.example @@ -7,6 +7,9 @@ DATABASE_URL="postgresql://postgres:reformix@localhost:5432/reformix" RETELL_API_KEY="" RETELL_AGENT_ID="" RETELL_FROM_NUMBER="" # número de origen en E.164, p. ej. +34910000000 +# Allowlist de pruebas: si tiene valor (CSV de E.164), SOLO se llama a esos números; el resto se +# omite (la llamada no se lanza). Vaciar para volver a llamar a todos. Ej: "+34651194617" +RETELL_ALLOWED_NUMBERS="" # EP de ingesta del lead (/api/leads/:id/ingesta). Clave compartida que valida al llamante # externo (Authorization: Bearer ...). Sin ella, el EP responde 401. diff --git a/mvp/b2c/src/lib/env.ts b/mvp/b2c/src/lib/env.ts index 9fd3606..6b8a037 100644 --- a/mvp/b2c/src/lib/env.ts +++ b/mvp/b2c/src/lib/env.ts @@ -12,6 +12,8 @@ const schema = z.object({ RETELL_API_KEY: opcional, RETELL_AGENT_ID: opcional, RETELL_FROM_NUMBER: opcional, + // Allowlist de pruebas: si tiene valor (CSV de números), SOLO se llaman esos; vacío = todos. + RETELL_ALLOWED_NUMBERS: opcional, // EP de ingesta del lead: clave compartida que valida al llamante externo. FUNNEL_API_KEY: opcional, // SMTP para enviar el presupuesto y el enlace al formulario. @@ -32,6 +34,7 @@ export const env = schema.parse({ RETELL_API_KEY: process.env.RETELL_API_KEY, RETELL_AGENT_ID: process.env.RETELL_AGENT_ID, RETELL_FROM_NUMBER: process.env.RETELL_FROM_NUMBER, + RETELL_ALLOWED_NUMBERS: process.env.RETELL_ALLOWED_NUMBERS, FUNNEL_API_KEY: process.env.FUNNEL_API_KEY, SMTP_HOST: process.env.SMTP_HOST, SMTP_PORT: process.env.SMTP_PORT, diff --git a/mvp/b2c/src/lib/voice/retell.ts b/mvp/b2c/src/lib/voice/retell.ts index b622e5c..f02ee52 100644 --- a/mvp/b2c/src/lib/voice/retell.ts +++ b/mvp/b2c/src/lib/voice/retell.ts @@ -50,6 +50,18 @@ export async function iniciarLlamadaSaliente(opts: { const toNumber = normalizarTelefonoEs(opts.telefono); if (!toNumber) return null; + // Bypass de seguridad (fase de pruebas): si RETELL_ALLOWED_NUMBERS tiene valor, SOLO se llama a + // esos números; cualquier otro se omite (devuelve null, el funnel sigue en simulado). Vaciar la + // variable para volver a llamar a todos. + const allow = (env.RETELL_ALLOWED_NUMBERS ?? '') + .split(',') + .map((s) => normalizarTelefonoEs(s.trim())) + .filter((n): n is string => Boolean(n)); + if (allow.length > 0 && !allow.includes(toNumber)) { + console.warn(`Retell: ${toNumber} no está en RETELL_ALLOWED_NUMBERS; llamada omitida (bypass).`); + return null; + } + const body: Record = { from_number: env.RETELL_FROM_NUMBER, to_number: toNumber,