🔐 Auditoría de Arquitectura de API Keys
Análisis profesional de la gestión de credenciales en ProjectOS
Fecha: Enero 2025
📊 Resumen Ejecutivo
ProjectOS actualmente tiene una arquitectura híbrida de API keys con dos sistemas:
| Sistema | Ubicación | Proveedores | Estado |
|---|---|---|---|
| Centralizado | resolveApiKey() en aiService.js |
AI providers (Gemini, OpenAI, Anthropic, Groq, xAI, DeepSeek) | ✅ Maduro |
| Disperso | Variables de entorno directas | Comunicaciones, Media, Email | ⚠️ Necesita migración |
Objetivo del Usuario
- Mantener en Cloudflare Secrets: Solo
GEMINI_API_KEY(respaldo) - Mover a BD por organización: Twilio, ElevenLabs, Meta WhatsApp, SendGrid, Mailgun, etc.
- Filosofía: Menos secrets en infraestructura, más configuración en base de datos
🗄️ Tablas de Base de Datos Existentes
1. organization_api_keys (Migration 0045)
CREATE TABLE organization_api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
organization_id TEXT NOT NULL, -- FK a organizations.id
provider TEXT NOT NULL, -- 'openai', 'gemini', 'twilio', etc.
api_key TEXT NOT NULL, -- La API key encriptada
config TEXT DEFAULT NULL, -- JSON con configuración adicional
is_active INTEGER DEFAULT 1,
notes TEXT DEFAULT NULL,
usage_limit INTEGER DEFAULT NULL,
usage_count INTEGER DEFAULT 0,
UNIQUE(organization_id, provider)
);
Campo config para metadata adicional:
- Twilio:
{"account_sid": "...", "phone_number": "+1..."} - WhatsApp Meta:
{"phone_number_id": "...", "business_id": "..."} - Mailgun:
{"domain": "mg.example.com", "from": "noreply@..."}
2. system_api_keys (Migration 0037)
CREATE TABLE system_api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider TEXT NOT NULL,
tier TEXT DEFAULT 'ALL', -- 'ALL', 'FREE', 'PERSONAL', etc.
api_key TEXT NOT NULL,
config TEXT DEFAULT NULL,
is_active INTEGER DEFAULT 1,
UNIQUE(provider, tier)
);
Uso: Keys compartidas del sistema para usuarios sin organización o modo inherit.
3. tier_assignments.custom_api_keys (campo JSON)
-- En tabla tier_assignments
custom_api_keys TEXT -- JSON: {"gemini": "AIza...", "openai": "sk-..."}
Uso: Keys personales de usuarios individuales.
🔄 Sistema Centralizado Actual (resolveApiKey)
Archivo: functions/utils/aiService.js
Cadena de Prioridad
1. User's custom key → tier_assignments.custom_api_keys
2. Organization key → organization_api_keys (si api_key_mode='organization')
3. System key (tier) → system_api_keys WHERE tier = user_tier
4. System key (ALL) → system_api_keys WHERE tier = 'ALL'
5. Environment variable → env.{PROVIDER}_API_KEY
6. (Fallback a Workers AI si disponible)
Proveedores Registrados (STANDARD_PROVIDERS)
| Provider | Tipo | Base URL | Env Key |
|---|---|---|---|
cloudflare-ai |
workers-ai | - | - |
gemini |
gemini | generativelanguage.googleapis.com | GEMINI_API_KEY |
openai |
openai | api.openai.com | OPENAI_API_KEY |
anthropic |
anthropic | api.anthropic.com | ANTHROPIC_API_KEY |
groq |
openai | api.groq.com | GROQ_API_KEY |
xai |
openai | api.x.ai | XAI_API_KEY |
deepseek |
openai | api.deepseek.com | DEEPSEEK_API_KEY |
⚠️ Servicios NO Integrados al Sistema Centralizado
🔴 Categoría: Comunicaciones (SMS/WhatsApp/Voice)
| Servicio | Archivos | Variables Env | Complejidad |
|---|---|---|---|
| Twilio SMS | twilio/send-sms.js, WorkflowExecutor.js |
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER | Alta (3 vars) |
| Twilio WhatsApp | twilio/send-whatsapp.js, WorkflowExecutor.js |
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_WHATSAPP_NUMBER | Alta (3 vars) |
| Meta WhatsApp | whatsapp/*.js, WorkflowExecutor.js, SmartPipelineService.js |
WHATSAPP_ACCESS_TOKEN, WHATSAPP_PHONE_NUMBER_ID, META_APP_SECRET | Alta (3 vars) |
| Twilio Voice | twilio/make-call.js, webhooks/twilio-call.js |
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER | Alta |
📁 Archivos afectados (14):
functions/api/twilio/send-sms.js
functions/api/twilio/send-whatsapp.js
functions/api/twilio/make-call.js
functions/api/twilio/message-status/[messageId].js
functions/api/whatsapp/upload-media.js
functions/api/webhooks/twilio-call.js
functions/api/webhooks/whatsapp.js
functions/api/workflows/WorkflowExecutor.js (múltiples usos)
🟠 Categoría: Voz/TTS/STT
| Servicio | Archivos | Variables Env | Estado |
|---|---|---|---|
| ElevenLabs | elevenlabs/*.js, tts/index.js, WorkflowExecutor.js, ElevenLabsMediaStream.js |
ELEVENLABS_API_KEY, ELEVENLABS_AGENT_ID, ELEVENLABS_PHONE_NUMBER_ID | ⚠️ Parcialmente migrado en tts/index.js |
| Google TTS | tts/index.js |
GOOGLE_TTS_API_KEY | ⚠️ Parcialmente migrado |
| Azure TTS | tts/index.js |
AZURE_TTS_KEY, AZURE_TTS_REGION | ⚠️ Parcialmente migrado |
📁 Archivos afectados (8):
functions/api/elevenlabs/voices.js
functions/api/elevenlabs/tts.js
functions/api/elevenlabs/signed-url.js
functions/api/elevenlabs/media-stream.js
functions/api/tts/index.js (ya usa resolveApiKey + fallback)
functions/api/workflows/WorkflowExecutor.js
functions/api/whatsapp-local/SmartPipelineService.js
src/durable-objects/ElevenLabsMediaStream.js
🟡 Categoría: Email
| Servicio | Archivos | Variables Env | Complejidad |
|---|---|---|---|
| Resend | emailService.js, WorkflowExecutor.js |
RESEND_API_KEY | Baja (1 var) |
| SendGrid | emailService.js, WorkflowExecutor.js |
SENDGRID_API_KEY, SENDGRID_FROM | Media (2 vars) |
| Mailgun | WorkflowExecutor.js |
MAILGUN_API_KEY, MAILGUN_DOMAIN, MAILGUN_FROM | Alta (3 vars) |
📁 Archivos afectados (2):
functions/utils/emailService.js
functions/api/workflows/WorkflowExecutor.js
🟢 Categoría: Generación de Imágenes
| Servicio | Archivos | Variables Env | Estado |
|---|---|---|---|
| Stability AI | ai-images/index.js |
STABILITY_API_KEY | Custom provider soportado |
| Replicate | ai-images/index.js, SmartPipelineService.js |
REPLICATE_API_KEY | Custom provider soportado |
📐 Arquitectura Propuesta
Fase 1: Extender STANDARD_PROVIDERS
Agregar categorías de servicio no-AI al sistema de resolución:
// functions/utils/serviceProviders.js (NUEVO)
export const SERVICE_PROVIDERS = {
// === COMUNICACIONES ===
'twilio': {
type: 'twilio',
category: 'communications',
required_keys: ['account_sid', 'auth_token'],
optional_config: ['phone_number', 'whatsapp_number'],
env_keys: {
account_sid: 'TWILIO_ACCOUNT_SID',
auth_token: 'TWILIO_AUTH_TOKEN',
phone_number: 'TWILIO_PHONE_NUMBER',
whatsapp_number: 'TWILIO_WHATSAPP_NUMBER'
}
},
'meta-whatsapp': {
type: 'meta-whatsapp',
category: 'communications',
required_keys: ['access_token', 'phone_number_id'],
optional_config: ['app_secret', 'business_id'],
env_keys: {
access_token: 'WHATSAPP_ACCESS_TOKEN',
phone_number_id: 'WHATSAPP_PHONE_NUMBER_ID',
app_secret: 'META_APP_SECRET'
}
},
// === VOZ/AUDIO ===
'elevenlabs': {
type: 'elevenlabs',
category: 'voice',
required_keys: ['api_key'],
optional_config: ['agent_id', 'phone_number_id'],
env_keys: {
api_key: 'ELEVENLABS_API_KEY',
agent_id: 'ELEVENLABS_AGENT_ID',
phone_number_id: 'ELEVENLABS_PHONE_NUMBER_ID'
}
},
'google-tts': {
type: 'google-tts',
category: 'voice',
required_keys: ['api_key'],
env_keys: { api_key: 'GOOGLE_TTS_API_KEY' }
},
'azure-tts': {
type: 'azure-tts',
category: 'voice',
required_keys: ['api_key'],
optional_config: ['region'],
env_keys: {
api_key: 'AZURE_TTS_KEY',
region: 'AZURE_TTS_REGION'
}
},
// === EMAIL ===
'resend': {
type: 'resend',
category: 'email',
required_keys: ['api_key'],
env_keys: { api_key: 'RESEND_API_KEY' }
},
'sendgrid': {
type: 'sendgrid',
category: 'email',
required_keys: ['api_key'],
optional_config: ['from_email'],
env_keys: {
api_key: 'SENDGRID_API_KEY',
from_email: 'SENDGRID_FROM'
}
},
'mailgun': {
type: 'mailgun',
category: 'email',
required_keys: ['api_key', 'domain'],
optional_config: ['from_email'],
env_keys: {
api_key: 'MAILGUN_API_KEY',
domain: 'MAILGUN_DOMAIN',
from_email: 'MAILGUN_FROM'
}
},
// === IMÁGENES ===
'stability-ai': {
type: 'stability-ai',
category: 'images',
required_keys: ['api_key'],
env_keys: { api_key: 'STABILITY_API_KEY' }
},
'replicate': {
type: 'replicate',
category: 'images',
required_keys: ['api_key'],
env_keys: { api_key: 'REPLICATE_API_KEY' }
}
};
Fase 2: Crear resolveServiceCredentials()
// functions/utils/serviceProviders.js
/**
* Resolver credenciales de servicio completas
* Similar a resolveApiKey pero para servicios con múltiples credenciales
*
* @param {object} env - Cloudflare env
* @param {string} provider - e.g., 'twilio', 'meta-whatsapp', 'elevenlabs'
* @param {string} organizationId - ID de org (opcional)
* @returns {Promise<{credentials: object, source: string, config: object}>}
*/
export async function resolveServiceCredentials(env, provider, organizationId = null) {
const providerDef = SERVICE_PROVIDERS[provider];
if (!providerDef) {
throw new Error(`Unknown provider: ${provider}`);
}
// 1. Buscar en organization_api_keys
if (organizationId) {
try {
const orgKey = await env.DB.prepare(`
SELECT api_key, config FROM organization_api_keys
WHERE organization_id = ? AND provider = ? AND is_active = 1
`).bind(organizationId, provider).first();
if (orgKey?.api_key) {
// api_key contiene el credential principal
// config contiene las credenciales adicionales en JSON
const config = orgKey.config ? JSON.parse(orgKey.config) : {};
return {
credentials: {
[providerDef.required_keys[0]]: orgKey.api_key,
...config
},
source: 'organization',
config
};
}
} catch (e) {}
}
// 2. Buscar en system_api_keys
try {
const sysKey = await env.DB.prepare(`
SELECT api_key, config FROM system_api_keys
WHERE provider = ? AND is_active = 1 AND tier = 'ALL'
`).bind(provider).first();
if (sysKey?.api_key) {
const config = sysKey.config ? JSON.parse(sysKey.config) : {};
return {
credentials: {
[providerDef.required_keys[0]]: sysKey.api_key,
...config
},
source: 'system',
config
};
}
} catch (e) {}
// 3. Fallback a environment variables
const credentials = {};
let hasAllRequired = true;
for (const [key, envKey] of Object.entries(providerDef.env_keys)) {
credentials[key] = env[envKey] || null;
if (providerDef.required_keys.includes(key) && !credentials[key]) {
hasAllRequired = false;
}
}
if (hasAllRequired) {
return { credentials, source: 'env', config: {} };
}
return { credentials: null, source: null, config: {} };
}
Fase 3: Modificar Estructura de organization_api_keys
La tabla actual soporta un api_key + config JSON, suficiente para servicios multi-credencial:
-- Ejemplo de inserción para Twilio:
INSERT INTO organization_api_keys
(organization_id, provider, api_key, config)
VALUES
('org_123', 'twilio', 'AUTH_TOKEN_VALUE',
'{"account_sid": "AC...", "phone_number": "+1234567890", "whatsapp_number": "+1987654321"}');
-- Ejemplo para Meta WhatsApp:
INSERT INTO organization_api_keys
(organization_id, provider, api_key, config)
VALUES
('org_123', 'meta-whatsapp', 'ACCESS_TOKEN_VALUE',
'{"phone_number_id": "123456789", "app_secret": "abc123"}');
📋 Plan de Migración
Prioridad Alta (Impacto inmediato)
| # | Servicio | Archivos a Modificar | Esfuerzo |
|---|---|---|---|
| 1 | ElevenLabs | 6 archivos | 2 horas |
| 2 | Twilio (SMS/Voice) | 4 archivos | 2 horas |
| 3 | Meta WhatsApp | 3 archivos | 1.5 horas |
Prioridad Media (Workflows)
| # | Servicio | Archivos a Modificar | Esfuerzo |
|---|---|---|---|
| 4 | WorkflowExecutor | 1 archivo (múltiples puntos) | 3 horas |
| 5 | SmartPipelineService | 1 archivo | 2 horas |
Prioridad Baja (Email)
| # | Servicio | Archivos a Modificar | Esfuerzo |
|---|---|---|---|
| 6 | Email providers | 2 archivos | 1 hora |
🛡️ Consideraciones de Seguridad
Encriptación de Keys en BD
// Considerar usar Cloudflare Workers secrets para la key de encriptación
// y encriptar todas las API keys en la base de datos
import { encrypt, decrypt } from '../utils/encryption.js';
// Al guardar
const encryptedKey = await encrypt(apiKey, env.ENCRYPTION_KEY);
// Al leer
const decryptedKey = await decrypt(encryptedKey, env.ENCRYPTION_KEY);
Rate Limiting por Organización
La tabla organization_api_keys ya tiene campos usage_limit y usage_count que se pueden usar.
✅ Checklist de Implementación
- Crear
functions/utils/serviceProviders.jscon SERVICE_PROVIDERS - Implementar
resolveServiceCredentials() - Migrar
functions/api/elevenlabs/*.js(4 archivos) - Migrar
functions/api/twilio/*.js(4 archivos) - Migrar
functions/api/whatsapp/*.js(2 archivos) - Migrar
WorkflowExecutor.js(puntos de comunicación) - Migrar
SmartPipelineService.js - Migrar
emailService.js - Actualizar
ElevenLabsMediaStream.js(Durable Object) - Crear UI de administración para gestionar keys por organización
- Documentar nuevo sistema en
/doc/architecture/
🔑 Secrets Finales en Cloudflare
Después de la migración completa, solo estos secrets serían necesarios en Cloudflare:
| Secret | Propósito | Notas |
|---|---|---|
GEMINI_API_KEY |
Fallback para AI cuando no hay key en BD | Único AI key requerido |
ENCRYPTION_KEY |
Encriptar/desencriptar keys en BD | Generado una vez |
Todos los demás (Twilio, ElevenLabs, Meta, Email) estarían en:
organization_api_keys→ Para organizacionessystem_api_keys→ Para usuarios sin organización
Documento generado como parte de la auditoría de arquitectura de ProjectOS