Saltar al contenido principal
GUÍA

Cómo añadir contexto de actor a una app de Node.js

Guía paso a paso para construir un middleware que registra quién o qué está detrás de cada solicitud, para que puedas estudiar el tráfico real antes de aplicar nada.

Toshendra Sharma

Por Toshendra SharmaFundador y CEO, AgentCredia · Updated November 13, 2026

Principiante · 9 min de lectura

¿Por qué empezar con contexto de actor en lugar de reglas?

La mayoría de los equipos que detectan agentes de IA en su tráfico recurren primero a una regla. Bloquear un user agent, añadir un challenge, endurecer un rate limit. El problema es que una regla escrita antes de entender el tráfico suele afectar a las personas equivocadas. Se bloquea a asistentes legítimos, se desafía a usuarios reales y la automatización que realmente preocupaba sigue entrando porque, para empezar, nunca se identificó.

El contexto de actor es el paso previo a las reglas. Para cada solicitud, registras lo que tu aplicación puede observar sobre quién o qué está actuando: una persona interactuando, una persona que trabaja con un asistente de IA, un agente autónomo o algo que todavía no puedes distinguir. Registras la evidencia detrás de esa visión y la mantienes junto al resultado de la solicitud. Después de una o dos semanas así, tienes algo que ninguna plantilla de reglas puede darte: un mapa de tu propio tráfico. Si todavía estás decidiendo si los agentes llegan o no a tu sitio, empieza con cómo saber si el tráfico del sitio web proviene de agentes de IA.

Esta guía construye esa capa en una aplicación de Node.js usando middleware simple. Funciona con Express, y las mismas ideas se trasladan a Fastify, Koa, Hono o un route handler de Next.js.

Qué vas a construir

Al final tendrás:

  1. Un middleware que extrae evidencias de actor de cada solicitud.
  2. Una clasificación pequeña y explícita con una base declarada para cada resultado.
  3. Registros estructurados de observación que nunca cambian cómo se gestiona la solicitud.
  4. Una forma de adjuntar el resultado a la solicitud para que los handlers posteriores puedan leerlo.

Nada de esto bloquea una solicitud. Eso es intencional.

Paso 1: ¿Qué evidencia puede ver realmente tu servidor?

Un servidor ve menos de lo que la gente supone. Antes de escribir código, enumera las señales disponibles en tu borde:

  • Firmas de la solicitud. Algunos clientes automatizados firman solicitudes usando HTTP Message Signatures (RFC 9421), enviando los encabezados Signature-Input y Signature, a menudo junto con un encabezado Signature-Agent que apunta a las claves publicadas por el operador, tal como describe el borrador del protocolo Web Bot Auth del IETF Web Bot Auth protocol draft.
  • Identidad declarada. Los clientes pueden nombrarse en el encabezado User-Agent, en un encabezado From, o mediante metadatos de protocolo como un nombre de cliente de MCP. Los clientes de MCP informan un nombre clientInfo elegido por sí mismos en la initialize request de la revisión 2025-11-25, y la revisión 2026-07-28 transporta la identidad del cliente como per-request metadata. Son declaraciones, y cualquiera puede copiarlas.
  • Contexto de autenticación. Si la solicitud lleva una cookie de sesión, un bearer token o una API key, y qué tipo de credencial es.
  • Forma del protocolo. Encabezados Accept, si el cliente solicita HTML o JSON, si carga recursos de la página y cómo maneja redirecciones y cookies.
  • Declaraciones de la aplicación. Tu propio producto puede saber cosas que la red no puede, como que un usuario ha elegido una opción de "AI assisted", o que una solicitud proviene de tu función de asistente integrada.

Cada señal tiene una fuerza distinta. Una firma validada se puede comprobar. Una cadena de user agent es una afirmación. Una cookie ausente es simplemente ausencia. Mantener todo esto separado es el objetivo de todo el ejercicio.

Paso 2: Modela el resultado explícitamente

Define la forma de un resultado de actor antes de escribir la lógica de detección. Un modelo pequeño y tipado mantiene a todo el mundo honesto sobre lo que significa una clasificación. Las cuatro clases usadas aquí se explican en Human, AI-assisted or agent: a practical taxonomy for application traffic.

TS
export type ActorClass = "human" | "human_ai" | "agent" | "unknown";
export type EvidenceBasis = "verified" | "declared" | "inferred" | "unknown";

export interface ActorEvidence {
  signaturePresent: boolean;
  signatureAgent?: string;
  declaredClient?: string;
  credentialType: "session" | "api_key" | "bearer" | "none";
  acceptsHtml: boolean;
  appDeclaredAssist: boolean;
}

export interface ActorResult {
  actor: ActorClass;
  basis: EvidenceBasis;
  reasons: string[];
  evidence: ActorEvidence;
  observedAt: string;
}

Dos campos importan más. actor es la clase. basis dice por qué la crees. Un resultado de agente basado en una firma validada y un resultado de agente basado en una cadena de user agent son ambos "agent", pero merecen un tratamiento muy distinto más adelante, así que nunca los fusionas.

Paso 3: Extraer evidencia en middleware

La fase de extracción lee encabezados y estado de la solicitud. Aún no decide nada.

TS
import type { Request, Response, NextFunction } from "express";

const KNOWN_AGENT_TOKENS = ["bot", "crawler", "spider", "agent", "gpt", "claude"];

export function extractEvidence(req: Request): ActorEvidence {
  const ua = (req.get("user-agent") || "").toLowerCase();
  const auth = req.get("authorization") || "";
  const hasSession = Boolean(req.cookies?.session);

  let credentialType: ActorEvidence["credentialType"] = "none";
  if (hasSession) credentialType = "session";
  else if (auth.toLowerCase().startsWith("bearer ")) credentialType = "bearer";
  else if (req.get("x-api-key")) credentialType = "api_key";

  const declaredToken = KNOWN_AGENT_TOKENS.find((t) => ua.includes(t));

  return {
    signaturePresent: Boolean(req.get("signature") && req.get("signature-input")),
    signatureAgent: req.get("signature-agent") || undefined,
    declaredClient: declaredToken ? ua.slice(0, 200) : undefined,
    credentialType,
    acceptsHtml: (req.get("accept") || "").includes("text/html"),
    appDeclaredAssist: req.get("x-app-assist") === "1",
  };
}

Algunas notas sobre este código:

  • signaturePresent solo registra que existen encabezados de firma. La validación es un paso aparte, cubierto en la guía sobre cómo verificar solicitudes de agente firmadas.
  • La lista de tokens de user agent es intencionalmente simple. Produce una declaración, nunca una verificación.
  • x-app-assist representa cualquier cosa que tu propio frontend envíe cuando un usuario trabaja a través de tu función de IA integrada. Sustitúyelo por tu mecanismo real.

Paso 4: Clasificar con una base declarada

Ahora convierte la evidencia en un resultado. Mantén las reglas cortas, ordenadas de la evidencia más fuerte a la más débil, y haz siempre fallback a Unknown.

TS
export async function classify(
  evidence: ActorEvidence,
  verifySignature: () => Promise<boolean>,
): Promise<ActorResult> {
  const reasons: string[] = [];
  const observedAt = new Date().toISOString();

  if (evidence.signaturePresent && (await verifySignature())) {
    reasons.push("request signature validated");
    return { actor: "agent", basis: "verified", reasons, evidence, observedAt };
  }

  if (evidence.appDeclaredAssist) {
    reasons.push("application reported its built-in assistant");
    return { actor: "human_ai", basis: "declared", reasons, evidence, observedAt };
  }

  if (evidence.declaredClient) {
    reasons.push("client declared itself as automated");
    return { actor: "agent", basis: "declared", reasons, evidence, observedAt };
  }

  if (evidence.credentialType === "session" && evidence.acceptsHtml) {
    reasons.push("interactive browser session");
    return { actor: "human", basis: "inferred", reasons, evidence, observedAt };
  }

  reasons.push("insufficient evidence");
  return { actor: "unknown", basis: "unknown", reasons, evidence, observedAt };
}

Observa lo que esto no hace. Nunca convierte una declaración en una verificación. Nunca asigna Human a una solicitud que no tiene señal interactiva. Cuando no puede saberlo, dice Unknown. Unknown es una respuesta válida y útil, y te indica exactamente dónde termina tu visibilidad. Presentamos el caso completo en por qué Unknown es un resultado válido para el tráfico de IA.

Paso 5: Integrarlo en modo de observación

Monta el middleware pronto, adjunta el resultado a la solicitud y regístralo. No ramifiques en función de él.

TS
import express from "express";
import cookieParser from "cookie-parser";

const app = express();
app.use(cookieParser());

app.use(async (req, res, next) => {
  try {
    const evidence = extractEvidence(req);
    const result = await classify(evidence, async () => false);
    res.locals.actor = result;

    res.on("finish", () => {
      logActor({
        route: req.route?.path ?? req.path,
        method: req.method,
        status: res.statusCode,
        actor: result.actor,
        basis: result.basis,
        reasons: result.reasons,
      });
    });
  } catch {
    res.locals.actor = undefined;
  }
  next();
});

Tres detalles hacen que esto sea seguro:

  1. El bloque try garantiza que un fallo en la detección nunca afecte a la respuesta. La observación debe fallar abierto.
  2. El registro ocurre en finish, así capturas el código de estado real y luego puedes comparar clasificaciones con resultados.
  3. verifySignature está sustituido por false hasta que implementes la validación real. Eso mantiene las solicitudes firmadas en los buckets declarados o desconocidos en lugar de exagerarlas.

Paso 6: Registrar para análisis, no solo para depuración

Escribe logs que puedas agregar. Una línea estructurada por solicitud es suficiente.

TS
function logActor(entry: Record<string, unknown>) {
  process.stdout.write(JSON.stringify({ type: "actor_observation", ...entry }) + "\n");
}

Mantén los datos personales fuera de estos registros. Necesitas la ruta, la clase, la base, los motivos y el resultado. No necesitas direcciones IP completas, cookies en bruto ni cuerpos de solicitud para entender la mezcla de tráfico. Si conservas algún identificador para correlación, aplica un hash con una sal rotatoria y establece un periodo de retención corto.

Paso 7: Lee los datos antes de actuar

Ejecuta en modo de observación el tiempo suficiente para ver patrones semanales normales. Luego responde algunas preguntas:

  • ¿Qué rutas atraen agentes? Las rutas de documentación, catálogo y búsqueda suelen hacerlo. Los flujos de cuenta y checkout normalmente no deberían.
  • ¿Qué proporción de resultados de agente está verificada frente a declarada? Esa relación te indica cuánto puedes confiar de forma segura en la identidad cuando escribas políticas.
  • ¿Dónde se concentra Unknown? Una tasa alta de Unknown en una ruta suele significar que te falta una señal ahí, por ejemplo una página que nunca ejecuta tu código frontend.
  • ¿Difieren los resultados por clase? Compara tasas de error, pasos de conversión o reportes de abuso entre clases. De ahí salen las ideas reales de política.

Las respuestas a menudo sorprenderán. Los equipos descubren con regularidad que una gran parte de su "bot problem" en realidad es automatización legítima que usaría encantada un endpoint estructurado si existiera.

Paso 8: Exponer el resultado al resto de tu app

Una vez que los datos fluyen, deja que los handlers lean res.locals.actor para usos no coercitivos: etiquetas analíticas, elección de un formato de respuesta o registrar que un ticket de soporte provino de una sesión asistida por IA. Estos usos mejoran la experiencia sin arriesgar que nadie quede bloqueado.

TS
app.get("/docs/:page", (req, res) => {
  const actor = res.locals.actor as ActorResult | undefined;
  if (actor?.actor === "agent" && req.accepts("text/markdown")) {
    return res.type("text/markdown").send(renderMarkdown(req.params.page));
  }
  return res.render("doc", { page: req.params.page });
});

La aplicación de enforcement, como challenges, rate limits o comprobaciones step-up, llega más tarde, después de haber medido los falsos positivos. La guía sobre cómo desplegar políticas conscientes de actor cubre ese proceso.

¿Qué errores debes evitar?

  • Tratar el user agent como identidad. Es el encabezado más fácil de cambiar. Regístralo como una declaración. Incluso los operadores de IA lo dicen: la guidance for its hosted browser agent de Anthropic señala que cualquiera puede enviar el mismo encabezado User-Agent y recomienda comprobar sus direcciones IP publicadas antes de tratar el tráfico como auténtico.
  • Hacer fallback a Human por defecto. Si tu código dice Human cada vez que no encuentra señales de bot, tus números de Human estarán inflados y tus políticas confiarán en el tráfico equivocado.
  • Mezclar clase y base. Guarda siempre ambos. Un dashboard que solo muestra "Agent" oculta la diferencia entre un operador verificado y una cadena copiada.
  • Bloquear durante la observación. Incluso un bloqueo temporal invalida tus mediciones, porque los clientes bloqueados cambian su comportamiento.

Dónde encaja Proof of Actor

El middleware anterior es un buen punto de partida y muestra el modelo central: primero la evidencia, una base explícita para cada resultado, Unknown visible y las decisiones dejadas a tu aplicación. Proof of Actor amplía ese mismo modelo con mayor cobertura de señales en contextos de servidor, edge, navegador y MCP, y con analítica que te permite Detect, Understand y Route con confianza.

Si quieres ayuda para llevar esto más lejos en una app de producción, solicita acceso anticipado. Para ver el flujo de decisión completo, lee How Proof of Actor works.

Fuentes

Constrúyelo con nosotros.

Únete al programa Founding Developer de Proof of Actor.

¿Prefieres hablar primero? Habla con los fundadores