Перейти к основному содержимому
РУКОВОДСТВО

Как добавить контекст акторов в приложение Node.js

Пошаговое руководство по созданию middleware, которое записывает, кто или что стоит за каждым запросом, чтобы вы могли изучить реальный трафик до применения каких-либо правил.

Toshendra Sharma

Автор Toshendra SharmaОснователь и CEO, AgentCredia · Updated November 13, 2026

Начальный · 9 мин чтения

Зачем начинать с контекста акторов, а не с правил?

Большинство команд, заметивших AI-агентов в своем трафике, сначала тянется к правилу. Заблокировать user agent, добавить challenge, ужесточить rate limit. Проблема в том, что правило, написанное до понимания трафика, обычно задевает не тех. Легитимные помощники блокируются, настоящим пользователям показывают challenge, а автоматизация, которой вы и правда опасались, продолжает ходить, потому что с самого начала никак себя не обозначила.

Контекст актора - это шаг перед правилами. Для каждого запроса вы записываете, что ваше приложение может наблюдать о том, кто или что действует: интерактивный человек, человек, работающий с AI-ассистентом, автономный агент или что-то, что пока нельзя уверенно различить. Вы фиксируете основание этого вывода и храните его рядом с результатом запроса. Через неделю или две у вас появляется то, что не даст ни один шаблон правила: карта собственного трафика. Если вы еще решаете, вообще ли агенты доходят до вашего сайта, начните с как определить, что трафик сайта идет от AI-агентов.

Это руководство строит такой слой в приложении Node.js с помощью обычного middleware. Оно работает с Express, и те же идеи переносятся на Fastify, Koa, Hono или обработчик маршрута Next.js.

Что вы построите

К концу у вас будет:

  1. middleware, которое извлекает доказательства актора из каждого запроса.
  2. Небольшая, явная классификация с указанным основанием для каждого результата.
  3. Структурированные журналы наблюдения, которые никогда не меняют способ обработки запроса.
  4. Способ прикрепить результат к запросу, чтобы последующие обработчики могли его прочитать.

Ничто здесь не блокирует запрос. Это сделано намеренно.

Шаг 1: Какие доказательства ваш сервер действительно может видеть?

Сервер видит меньше, чем обычно предполагают. Прежде чем писать код, перечислите сигналы, доступные на вашем краю:

  • Подписи запроса. Некоторые автоматизированные клиенты подписывают запросы с помощью HTTP Message Signatures (RFC 9421), отправляя заголовки Signature-Input и Signature, часто вместе с заголовком Signature-Agent, который указывает на опубликованные ключи оператора, как описано в черновике протокола IETF Web Bot Auth.
  • Заявленная идентичность. Клиенты могут называть себя в заголовке User-Agent, в заголовке From или через метаданные протокола, например имя клиента MCP. Клиенты MCP сообщают выбранное ими самим имя clientInfo в запросе initialize редакции 2025-11-25, а редакция 2026-07-28 передает идентичность клиента как метаданные на каждый запрос. Это заявления, и скопировать их может кто угодно.
  • Контекст аутентификации. Есть ли у запроса cookie сессии, bearer token или API key, и что это за тип учетных данных.
  • Форма протокола. Заголовки Accept, запрашивает ли клиент HTML или JSON, загружает ли он ресурсы страницы и как обрабатывает redirects и cookies.
  • Заявления приложения. Ваш собственный продукт может знать то, чего не видит сеть, например что пользователь выбрал вариант "AI assisted" или что запрос пришел из встроенной функции ассистента.

У каждого сигнала разная сила. Проверяемую подпись можно валидировать. Строка user agent - это утверждение. Отсутствующий cookie - это просто отсутствие. Разделять эти вещи - и есть смысл всей работы.

Шаг 2: Явно смоделируйте результат

Определите форму результата актора до того, как писать логику определения. Небольшая типизированная модель помогает всем честно понимать, что означает классификация. Четыре класса, используемые здесь, описаны в 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;
}

Наиболее важны два поля. actor - это класс. basis говорит, почему вы так считаете. Результат агента, основанный на проверяемой подписи, и результат агента, основанный на строке user agent, оба относятся к "agent", но в дальнейшем им нужен совершенно разный подход, поэтому их нельзя смешивать.

Шаг 3: Извлекайте доказательства в middleware

Этап извлечения читает заголовки и состояние запроса. Он еще ничего не решает.

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",
  };
}

Несколько замечаний по этому коду:

  • signaturePresent только фиксирует наличие заголовков подписи. Валидация - отдельный шаг, он описан в руководстве по проверке подписанных запросов агентов.
  • Список токенов user agent намеренно прост. Он создает заявление, но никогда не дает валидации.
  • x-app-assist здесь обозначает любой механизм, который ваш frontend отправляет, когда пользователь работает через встроенную AI-функцию. Замените его на ваш реальный механизм.

Шаг 4: Классифицируйте с указанным основанием

Теперь превратите доказательства в результат. Держите правила короткими, располагайте их от самого сильного доказательства к самому слабому и всегда завершайте ветку на 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 };
}

Обратите внимание, чего здесь нет. Здесь никогда не происходит повышения заявления до валидации. Здесь не присваивается Human запросу, в котором нет интерактивного сигнала. Когда определить нельзя, код говорит Unknown. Unknown - это корректный и полезный ответ, и он точно показывает, где заканчивается ваша видимость. Полный разбор мы приводим в почему Unknown - это корректный результат для AI-трафика.

Шаг 5: Подключите это в режиме наблюдения

Подключите middleware как можно раньше, прикрепите результат к запросу и запишите его в лог. Не ветвитесь по нему.

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();
});

Три детали делают это безопасным:

  1. Блок try гарантирует, что сбой в определении никогда не повлияет на ответ. Наблюдение должно fail open.
  2. Логирование происходит на finish, поэтому вы фиксируете реальный статус-код и позже можете сравнить классификации с результатами.
  3. verifySignature по умолчанию заглушен как false, пока вы не реализуете настоящую проверку. Это удерживает подписанные запросы в корзинах declared или unknown, а не завышает их статус.

Шаг 6: Логируйте для анализа, а не только для отладки

Пишите логи, которые можно агрегировать. Достаточно одной структурированной строки на запрос.

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

Не включайте в эти логи персональные данные. Вам нужен маршрут, класс, основание, причины и результат. Для понимания структуры трафика не нужны полные IP-адреса, сырые cookies или тела запросов. Если вы все же храните какой-либо идентификатор для сопоставления, хэшируйте его с rotating salt и задайте короткий срок хранения.

Шаг 7: Изучите данные до того, как действовать

Запустите режим наблюдения достаточно долго, чтобы увидеть обычные недельные паттерны. Затем ответьте на несколько вопросов:

  • Какие маршруты привлекают агентов? Обычно это документация, каталог и поисковые endpoint. Обычно не должны быть такими account и checkout flows.
  • Какова доля результатов agent, которые подтверждены, а не только заявлены? Это соотношение показывает, насколько можно безопасно полагаться на идентичность при написании политик.
  • Где сосредоточен Unknown? Высокая доля Unknown на маршруте часто означает, что там не хватает сигнала, например на странице, которая никогда не запускает код вашего frontend.
  • Отличаются ли результаты по классам? Сравните уровни ошибок, этапы конверсии или отчеты о злоупотреблениях между классами. Именно отсюда обычно возникают реальные идеи для политики.

Ответы часто удивляют. Команды регулярно обнаруживают, что большая часть их "bot problem" на самом деле - это легитимная автоматизация, которая с радостью использовала бы структурированный endpoint, если бы он существовал.

Шаг 8: Сделайте результат доступным остальной части приложения

Когда данные начинают поступать, дайте обработчикам читать res.locals.actor для нефорсирующих сценариев: аналитические теги, выбор формата ответа или запись того, что обращение в службу поддержки пришло из AI-assisted сессии. Такие сценарии улучшают опыт, не создавая риска блокировки.

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 });
});

Принуждение, например challenge, rate limits или step-up checks, приходит позже, после того как вы измерили false positives. Процесс внедрения actor-aware policy описан в отдельном руководстве.

Каких ошибок следует избегать?

  • Считать user agent идентичностью. Это самый простой заголовок для изменения. Записывайте его как заявление. Даже операторы AI так говорят: в рекомендациях Anthropic для его hosted browser agent указано, что любой может отправить тот же заголовок User-Agent, и рекомендуется проверять опубликованные IP-адреса, прежде чем считать трафик подлинным.
  • По умолчанию ставить Human. Если код назначает Human всякий раз, когда не находит сигналов бота, ваши числа Human будут завышены, а политики начнут доверять не тому трафику.
  • Смешивать класс и основание. Всегда храните оба значения. Панель, где показан только "Agent", скрывает разницу между подтвержденным оператором и скопированной строкой.
  • Блокировать во время наблюдения. Даже временная блокировка искажает измерения, потому что заблокированные клиенты меняют свое поведение.

Как сюда вписывается Proof of Actor

Приведенное выше middleware - хорошая отправная точка, и оно показывает основную модель: сначала доказательства, для каждого результата - явное основание, Unknown остается видимым, а решения остаются за вашим приложением. Proof of Actor строится на той же модели, но с более глубоким покрытием сигналов на уровнях server, edge, browser и MCP, а также с аналитикой, которая позволяет Detect, Understand и Route уверенно.

Если вам нужна помощь, чтобы пойти дальше в production-приложении, подайте заявку на ранний доступ. Чтобы увидеть полный поток принятия решения, прочитайте How Proof of Actor works.

Источники

Постройте это вместе с нами.

Присоединяйтесь к founding developer program Proof of Actor.

Предпочитаете сначала поговорить? Свяжитесь с основателями