CULTIVA IA · Servicio Web

Revisión · Cloudflare Workers Best Practices

Skill: buenas-practicas-cloudflare-workers · Revisión automática de código y configuración
⚡ LeadFlow SaaS — leadflow-api Worker
3 /10
worker.ts + wrangler.toml
Revisado: 16 Jun 2026
7
Crítico — bloquea producción
3
Aviso — degradación / riesgo
2
Correcto — OK
12
Reglas verificadas
🔴 Seguridad 3 críticos
🔑
Secreto hardcodeado en código fuente worker.ts:37 · wrangler.toml:[vars]
CRÍTICO
La API key de Hunter.io está embebida en el source (hardcoded_secret_123) y API_KEY_SECRET aparece en wrangler.toml [vars], que se versiona en git. Cualquier persona con acceso al repositorio obtiene acceso a producción.
worker.ts — antes
- const enrichResp = await fetch(`https://hunter.io/api/v2/email-verifier?email=${lead.email}&api_key=hardcoded_secret_123`);
worker.ts — después
+ const enrichResp = await fetch(`https://hunter.io/api/v2/email-verifier?email=${lead.email}&api_key=${c.env.HUNTER_API_KEY}`);
wrangler.toml — antes
- [vars]
- API_KEY_SECRET = "sk-leadflow-prod-abc123"  # en git
Terminal — solución
+ wrangler secret put API_KEY_SECRET
+ wrangler secret put HUNTER_API_KEY
  # Eliminar [vars] con secretos del wrangler.toml
📖 Secrets rule
🎲
Math.random() para generar IDs de seguridad worker.ts:29
CRÍTICO
Math.random() es determinista y predecible; no debe usarse para generar IDs de recursos (leads). Un atacante puede predecir/enumerar IDs y acceder a datos de otros clientes.
Antes
- const leadId = Math.random().toString(36).slice(2);  // ❌ predecible
Después
+ const leadId = crypto.randomUUID();  // ✅ criptográficamente seguro
📖 Web Crypto rule
Comparación directa de secretos (timing side-channel) worker.ts:23
CRÍTICO
apiKey !== c.env.API_KEY_SECRET compara strings byte a byte y puede filtrar información de temporización. Un atacante puede inferir cuántos caracteres coinciden midiendo el tiempo de respuesta.
Antes
- if (apiKey !== c.env.API_KEY_SECRET) { return c.json({ error: 'Unauthorized' }, 401); }
Después
+ const encoder = new TextEncoder();
+ const a = encoder.encode(apiKey ?? '');
+ const b = encoder.encode(c.env.API_KEY_SECRET);
+ const match = a.byteLength === b.byteLength &&
+   crypto.subtle.timingSafeEqual(a, b);
+ if (!match) return c.json({ error: 'Unauthorized' }, 401);
📖 timingSafeEqual rule
🔴 Estado global y memoria 2 críticos
🌐
Variables globales mutables para estado de request worker.ts:6–7, 42
CRÍTICO
requestCount y recentLeads a nivel de módulo causan fugas de datos entre requests de diferentes clientes dentro de la misma instancia V8. El array recentLeads puede crecer indefinidamente (memory leak) y exponer leads de clientes a terceros.
Antes
- let requestCount = 0;             // ❌ fuga entre requests
- const recentLeads: any[] = [];    // ❌ memory leak + fuga de datos
Después — eliminar por completo
+ // Usar Workers Analytics Engine para métricas de requests
+ // Usar D1/KV para persistir leads — nunca módulo-global
📖 No global request state rule
🎈
Floating promise — Queue.send() sin await ni waitUntil worker.ts:45
CRÍTICO
c.env.LEADS_QUEUE.send() devuelve una Promise que no se awaita ni se pasa a ctx.waitUntil(). El runtime puede cancelar la operación antes de que complete, causando pérdida silenciosa de mensajes.
Antes
- c.env.LEADS_QUEUE.send({ leadId, lead, enrichData });  // ❌ floating promise
Después
+ // Opción A: await directo (añade latencia a la respuesta)
+ await c.env.LEADS_QUEUE.send({ leadId, lead, enrichData });

+ // Opción B: post-response con waitUntil (recomendado para no bloquear)
+ const ctx = c.executionCtx;
+ ctx.waitUntil(c.env.LEADS_QUEUE.send({ leadId, lead, enrichData }));
📖 waitUntil rule · Floating promises rule
🟡 Rendimiento y arquitectura 3 avisos
💾
await response.text() sobre datos no acotados worker.ts:27, 37
AVISO
Dos llamadas a .text(): el body del request y la respuesta de Hunter.io. Con el límite de 128 MB del Worker, payloads grandes agotan la memoria. Para el body del lead usar .json() directamente; para la API de enrichment limitar con getReader() o verificar Content-Length.
Antes
- const body = await c.req.text();
- const lead = JSON.parse(body);
- const enrichData = await enrichResp.text();
Después
+ const lead = await c.req.json();  // Hono parsea directamente
+ const enrichData = await enrichResp.json();  // parse tipado, no texto crudo
📖 Streaming rule
🌐
Llamada a REST API de Cloudflare desde el Worker worker.ts:31
AVISO
La llamada a api.cloudflare.com desde dentro del Worker añade una red hop innecesaria, overhead de autenticación y latencia. Usar service bindings o bindings de plataforma donde sea posible. Si la lógica es necesaria, moverla fuera del hot path (queue / workflow).
Antes
- const cfResp = await fetch(`https://api.cloudflare.com/client/v4/accounts/${lead.accountId}/workers/scripts`, { ... });
Después — usar service binding
+ // Declarar en wrangler.toml:
+ // [[services]]
+ // binding = "CF_ADMIN"
+ // service = "leadflow-admin-worker"
+ const result = await c.env.CF_ADMIN.fetch(new Request('/scripts/' + lead.accountId));
📖 Bindings over REST rule · Service bindings rule
📝
Interface Env escrita a mano — puede desincronizarse worker.ts:9–16
AVISO
La interfaz Env está escrita manualmente. Si alguien añade un binding en wrangler.toml sin actualizar la interfaz (o viceversa) se rompe la seguridad de tipos en silencio.
Antes
- interface Env {
-   DB: D1Database;  // escrito a mano — puede divergir
-   LEADS_QUEUE: Queue;
- }
Después — generar con wrangler
+ $ wrangler types  # genera worker-configuration.d.ts automáticamente
+ # En tsconfig.json:
+ # "include": ["src", "worker-configuration.d.ts"]
📖 wrangler types rule
⚙️ Revisión de wrangler.toml 5 problemas
Campo Estado Encontrado Corrección
compatibility_date ✘ Falta compatibility_date = "2026-06-16"
compatibility_flags ✘ Falta compatibility_flags = ["nodejs_compat"]
observability ✘ Falta [observability]
enabled = true
head_sampling_rate = 1
[vars] con secretos ✘ Crítico API_KEY_SECRET = "sk-..." Eliminar; usar wrangler secret put
Bindings D1 / KV / Queue ✔ OK Correctamente declarados
Formato JSONC vs TOML ⚠ Sugerencia wrangler.toml (válido) Migrar a wrangler.jsonc para nuevas features
✅ Checklist de producción — resumen
  • compatibility_date configurado
  • nodejs_compat habilitado
  • Secretos en wrangler secret, no en [vars]
  • Sin secretos hardcodeados en source
  • crypto.randomUUID() para IDs, no Math.random()
  • timingSafeEqual para comparar secretos
  • Sin estado global mutable por request
  • Todas las Promises awaitadas o en waitUntil
  • Sin llamadas REST API de Cloudflare desde Worker
  • response.text() reemplazado por .json() acotado
  • Bindings D1, KV y Queue declarados
  • Estructura Hono correcta con tipos genéricos

✅ wrangler.toml corregido

wrangler.toml — versión producción
+ name = "leadflow-api"
+ main = "src/worker.ts"
+ compatibility_date = "2026-06-16"
+ compatibility_flags = ["nodejs_compat"]

+ [observability]
+ enabled = true
+ head_sampling_rate = 1

+ # ✅ Sin [vars] con secretos — usar: wrangler secret put API_KEY_SECRET
+ #                                    wrangler secret put HUNTER_API_KEY

+ [[d1_databases]]
+ binding = "DB"
+ database_name = "leadflow-prod"
+ database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

+ [[queues.producers]]
+ binding = "LEADS_QUEUE"
+ queue = "lead-processing"

+ [[kv_namespaces]]
+ binding = "SESSION_CACHE"
+ id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"