INTEGRITY Документация

Webhooks

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

быстрый старт

import { Agent, getAgentByName, routeAgentRequest } from "agents";

export class WebhookAgent extends Agent {
	async onRequest(request) {
		if (request.method !== "POST") {
			return new Response("Method not allowed", { status: 405 });
		}

		const rawBody = await request.text();
		const signature = request.headers.get("X-Hub-Signature-256");
		if (
			!(await verifyGitHubWebhook(rawBody, signature, this.env.WEBHOOK_SECRET))
		) {
			return new Response("Invalid signature", { status: 401 });
		}

		let payload;
		try {
			payload = JSON.parse(rawBody);
		} catch {
			return new Response("Invalid payload", { status: 400 });
		}

		await this.processEvent(payload);
		return new Response("OK");
	}

	async processEvent(payload) {
		// Store event, update state, trigger actions...
	}
}

async function verifyGitHubWebhook(rawBody, signature, secret) {
	if (!signature || !/^sha256=[0-9a-f]{64}$/i.test(signature)) return false;

	const encoder = new TextEncoder();
	const key = await crypto.subtle.importKey(
		"raw",
		encoder.encode(secret),
		{ name: "HMAC", hash: "SHA-256" },
		false,
		["verify"],
	);
	const signatureBytes = Uint8Array.from(
		signature.slice("sha256=".length).match(/.{2}/g) ?? [],
		(byte) => Number.parseInt(byte, 16),
	);

	return crypto.subtle.verify(
		"HMAC",
		key,
		signatureBytes,
		encoder.encode(rawBody),
	);
}

export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		if (url.pathname === "/webhooks/github" && request.method === "POST") {
			const rawBody = await request.clone().text();
			const signature = request.headers.get("X-Hub-Signature-256");
			if (
				!(await verifyGitHubWebhook(rawBody, signature, env.WEBHOOK_SECRET))
			) {
				return new Response("Invalid signature", { status: 401 });
			}

			let payload;
			try {
				payload = JSON.parse(rawBody);
			} catch {
				return new Response("Invalid payload", { status: 400 });
			}

			const repository = payload.repository?.full_name;
			if (!repository) {
				return new Response("Missing repository", { status: 400 });
			}

			const agentName = repository.toLowerCase().replace(/\//g, "-");
			const agent = await getAgentByName(env.WebhookAgent, agentName);
			return agent.fetch(request);
		}

		return (
			(await routeAgentRequest(request, env)) ||
			new Response("Not found", { status: 404 })
		);
	},
};
import { Agent, getAgentByName, routeAgentRequest } from "agents";

export class WebhookAgent extends Agent<Env> {
	async onRequest(request: Request): Promise<Response> {
		if (request.method !== "POST") {
			return new Response("Method not allowed", { status: 405 });
		}

		const rawBody = await request.text();
		const signature = request.headers.get("X-Hub-Signature-256");
		if (
			!(await verifyGitHubWebhook(
				rawBody,
				signature,
				this.env.WEBHOOK_SECRET,
			))
		) {
			return new Response("Invalid signature", { status: 401 });
		}

		let payload: unknown;
		try {
			payload = JSON.parse(rawBody);
		} catch {
			return new Response("Invalid payload", { status: 400 });
		}

		await this.processEvent(payload);
		return new Response("OK");
	}

	private async processEvent(payload: unknown) {
		// Store event, update state, trigger actions...
	}
}

async function verifyGitHubWebhook(
	rawBody: string,
	signature: string | null,
	secret: string,
): Promise<boolean> {
	if (!signature || !/^sha256=[0-9a-f]{64}$/i.test(signature)) return false;

	const encoder = new TextEncoder();
	const key = await crypto.subtle.importKey(
		"raw",
		encoder.encode(secret),
		{ name: "HMAC", hash: "SHA-256" },
		false,
		["verify"],
	);
	const signatureBytes = Uint8Array.from(
		signature.slice("sha256=".length).match(/.{2}/g) ?? [],
		(byte) => Number.parseInt(byte, 16),
	);

	return crypto.subtle.verify(
		"HMAC",
		key,
		signatureBytes,
		encoder.encode(rawBody),
	);
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);

		if (url.pathname === "/webhooks/github" && request.method === "POST") {
			const rawBody = await request.clone().text();
			const signature = request.headers.get("X-Hub-Signature-256");
			if (
				!(await verifyGitHubWebhook(
					rawBody,
					signature,
					env.WEBHOOK_SECRET,
				))
			) {
				return new Response("Invalid signature", { status: 401 });
			}

			let payload: { repository?: { full_name?: string } };
			try {
				payload = JSON.parse(rawBody);
			} catch {
				return new Response("Invalid payload", { status: 400 });
			}

			const repository = payload.repository?.full_name;
			if (!repository) {
				return new Response("Missing repository", { status: 400 });
			}

			const agentName = repository.toLowerCase().replace(/\//g, "-");
			const agent = await getAgentByName(env.WebhookAgent, agentName);
			return agent.fetch(request);
		}

		return (
			(await routeAgentRequest(request, env)) ||
			new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

Сценарии использования

Сочетание вебхуков с агентами позволяет реализовать паттерны, в которых каждая внешняя сущность получает собственный изолированный экземпляр агента с сохранением состояния.

Инструменты для разработчиков

Сценарий использования Описание
GitHub Repo Monitor Один агент на репозиторий отслеживает коммиты, PR, issues и звёзды
CI/CD Pipeline Agent Реагируйте на события сборки и развёртывания, уведомляйте о сбоях, отслеживайте историю развёртываний
Трекер Linear/Jira Автоматически сортировать обращения, распределять их по содержимому, отслеживать время решения

Электронная коммерция и платежи

Сценарий использования Описание
Stripe Customer Agent Один агент на клиента отслеживает платежи, подписки и споры
Shopify Order Agent Жизненный цикл заказа от создания до выполнения с синхронизацией остатков склада
Сверка платежей Сопоставлять события вебхуков с внутренними записями, отмечать расхождения

Коммуникация и уведомления

Сценарий использования Описание
Twilio SMS/Voice Разговорные агенты, запускаемые входящими сообщениями или звонками
Slack Bot Отвечать на слэш-команды, нажатия кнопок и интерактивные сообщения
Отслеживание электронной почты События доставки SendGrid/Mailgun, обработка отказов доставки, аналитика вовлечённости

IoT и инфраструктура

Сценарий использования Описание
Телеметрия устройства Один агент на устройство обрабатывает потоки данных с датчиков
Агрегация оповещений Собирайте оповещения из PagerDuty, Datadog или другой системы мониторинга
Автоматизация дома Реагируйте на триггеры IFTTT/Zapier с сохранением состояния

Интеграции SaaS

Сценарий использования Описание
CRM Sync Обновления контактов и сделок в Salesforce/HubSpot
Calendar Agent Уведомления о событиях Google Calendar и планирование
Отправки форм Вебхуки форм Typeform, Tally или собственных форм с последующими действиями

Маршрутизация вебхуков агентам

Ключевой паттерн: сначала проверяется подлинность исходного запроса ещё до его разбора, и только затем на основе аутентифицированных данных payload определяется идентичность Agent. Подпись тела запроса не подтверждает подлинность произвольного сегмента URL или произвольного заголовка.

Извлечение сущности из payload

Большинство вебхуков включают идентификатор в полезной нагрузке:

export default {
	async fetch(request, env) {
		const url = new URL(request.url);
		if (request.method === "POST" && url.pathname === "/webhooks/github") {
			const rawBody = await request.clone().text();
			const signature = request.headers.get("X-Hub-Signature-256");
			if (
				!(await verifyGitHubWebhook(rawBody, signature, env.WEBHOOK_SECRET))
			) {
				return new Response("Invalid signature", { status: 401 });
			}

			let payload;
			try {
				payload = JSON.parse(rawBody);
			} catch {
				return new Response("Invalid payload", { status: 400 });
			}

			const repository = payload.repository?.full_name;
			if (!repository) {
				return new Response("Missing repository", { status: 400 });
			}

			const agentName = repository.toLowerCase().replace(/\//g, "-");
			const agent = await getAgentByName(env.RepoAgent, agentName);
			return agent.fetch(request);
		}
	},
};
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);
		if (request.method === "POST" && url.pathname === "/webhooks/github") {
			const rawBody = await request.clone().text();
			const signature = request.headers.get("X-Hub-Signature-256");
			if (
				!(await verifyGitHubWebhook(
					rawBody,
					signature,
					env.WEBHOOK_SECRET,
				))
			) {
				return new Response("Invalid signature", { status: 401 });
			}

			let payload: { repository?: { full_name?: string } };
			try {
				payload = JSON.parse(rawBody);
			} catch {
				return new Response("Invalid payload", { status: 400 });
			}

			const repository = payload.repository?.full_name;
			if (!repository) {
				return new Response("Missing repository", { status: 400 });
			}

			const agentName = repository.toLowerCase().replace(/\//g, "-");
			const agent = await getAgentByName(env.RepoAgent, agentName);
			return agent.fetch(request);
		}
	},
} satisfies ExportedHandler<Env>;

Валидируйте идентификаторы сущностей в URL

Подпись тела запроса от провайдера не аутентифицирует URL webhook. Если URL содержит идентификатор сущности, сравните его с соответствующей идентичностью из проверенной полезной нагрузки провайдера и отклоняйте несовпадение перед вызовом getAgentByName().

Извлеките идентификационные данные Slack из проверенного тела запроса

Slack не отправляет аутентифицированный X-Slack-Team-Id заголовок маршрутизации. Проверьте помеченную временем подпись Slack и окно replay относительно необработанного тела запроса, затем прочитайте team_id из проверенного события или тела формы.

Проверка подписи

Всегда проверяйте подписи вебхуков, прежде чем доверять полезной нагрузке или обрабатывать её.

Паттерн GitHub HMAC-SHA256

Быстрого старта verifyGitHubWebhook() хелпер проверяет добавленную GitHub sha256=<hex> подпись поверх необработанного тела запроса с crypto.subtle.verify(). Этот формат специфичен для GitHub. Другие провайдеры используют другое кодирование подписи, другие подписываемые данные, проверки временных меток и защиту от повторного воспроизведения. Обратитесь к документации провайдера по ссылке в разделе Типичные поставщики вебхуков.

Заголовки для конкретных провайдеров

Провайдер Заголовок подписи Алгоритм
GitHub X-Hub-Signature-256 HMAC-SHA256
Stripe Stripe-Signature HMAC-SHA256 (с временной меткой)
Twilio X-Twilio-Signature HMAC-SHA1
Slack X-Slack-Signature HMAC-SHA256 (с временной меткой)
Shopify X-Shopify-Hmac-Sha256 HMAC-SHA256 (base64)

Обработка вебхуков

Handler onRequest

Используйте onRequest() чтобы обрабатывать входящие вебхуки в вашем агенте. Если Worker ещё не проверил запрос, проверьте его перед разбором тела. В этом примере повторно используется код из раздела «Быстрый старт» verifyGitHubWebhook() вспомогательная функция:

export class WebhookAgent extends Agent {
	async onRequest(request) {
		// 1. Validate method
		if (request.method !== "POST") {
			return new Response("Method not allowed", { status: 405 });
		}

		// 2. Get the GitHub event type
		const eventType = request.headers.get("X-GitHub-Event") ?? "unknown";

		// 3. Verify the GitHub signature
		const signature = request.headers.get("X-Hub-Signature-256");
		const body = await request.text();

		if (
			!(await verifyGitHubWebhook(body, signature, this.env.WEBHOOK_SECRET))
		) {
			return new Response("Invalid signature", { status: 401 });
		}

		// 4. Parse and process
		const payload = JSON.parse(body);
		await this.handleEvent(eventType, payload);

		// 5. Respond quickly
		return new Response("OK", { status: 200 });
	}

	async handleEvent(type, payload) {
		// Update state (broadcasts to connected clients)
		this.setState({
			...this.state,
			lastEventType: type,
			lastEventTime: new Date().toISOString(),
		});

		// Store in SQL for history
		this
			.sql`INSERT INTO events (type, payload, timestamp) VALUES (${type}, ${JSON.stringify(payload)}, ${Date.now()})`;
	}
}
export class WebhookAgent extends Agent {
	async onRequest(request: Request): Promise<Response> {
		// 1. Validate method
		if (request.method !== "POST") {
			return new Response("Method not allowed", { status: 405 });
		}

		// 2. Get the GitHub event type
		const eventType = request.headers.get("X-GitHub-Event") ?? "unknown";

		// 3. Verify the GitHub signature
		const signature = request.headers.get("X-Hub-Signature-256");
		const body = await request.text();

		if (
			!(await verifyGitHubWebhook(body, signature, this.env.WEBHOOK_SECRET))
		) {
			return new Response("Invalid signature", { status: 401 });
		}

		// 4. Parse and process
		const payload = JSON.parse(body);
		await this.handleEvent(eventType, payload);

		// 5. Respond quickly
		return new Response("OK", { status: 200 });
	}

	private async handleEvent(type: string, payload: unknown) {
		// Update state (broadcasts to connected clients)
		this.setState({
			...this.state,
			lastEventType: type,
			lastEventTime: new Date().toISOString(),
		});

		// Store in SQL for history
		this
			.sql`INSERT INTO events (type, payload, timestamp) VALUES (${type}, ${JSON.stringify(payload)}, ${Date.now()})`;
	}
}

Сохранение событий вебхуков

Используйте SQLite, чтобы сохранять события webhook для истории и повторного воспроизведения.

Схема таблицы событий

class WebhookAgent extends Agent {
	async onStart() {
		this.sql`
      CREATE TABLE IF NOT EXISTS events (
        id TEXT PRIMARY KEY,
        type TEXT NOT NULL,
        action TEXT,
        title TEXT NOT NULL,
        description TEXT,
        url TEXT,
        actor TEXT,
        payload TEXT,
        timestamp TEXT NOT NULL
      )
    `;

		this.sql`
      CREATE INDEX IF NOT EXISTS idx_events_timestamp
      ON events(timestamp DESC)
    `;
	}
}
class WebhookAgent extends Agent {
	async onStart(): Promise<void> {
		this.sql`
      CREATE TABLE IF NOT EXISTS events (
        id TEXT PRIMARY KEY,
        type TEXT NOT NULL,
        action TEXT,
        title TEXT NOT NULL,
        description TEXT,
        url TEXT,
        actor TEXT,
        payload TEXT,
        timestamp TEXT NOT NULL
      )
    `;

		this.sql`
      CREATE INDEX IF NOT EXISTS idx_events_timestamp
      ON events(timestamp DESC)
    `;
	}
}

Очистите старые события

Предотвращение неограниченного роста путём сохранения только последних событий:

// Keep last 100 events
this.sql`
  DELETE FROM events WHERE id NOT IN (
    SELECT id FROM events ORDER BY timestamp DESC LIMIT 100
  )
`;

// Or delete events older than 30 days
this.sql`
  DELETE FROM events
  WHERE timestamp < datetime('now', '-30 days')
`;
// Keep last 100 events
this.sql`
  DELETE FROM events WHERE id NOT IN (
    SELECT id FROM events ORDER BY timestamp DESC LIMIT 100
  )
`;

// Or delete events older than 30 days
this.sql`
  DELETE FROM events
  WHERE timestamp < datetime('now', '-30 days')
`;

Запросы событий

import { Agent, callable } from "agents";

class WebhookAgent extends Agent {
	@callable()
	getEvents(limit = 20) {
		return [
			...this.sql`
      SELECT * FROM events
      ORDER BY timestamp DESC
      LIMIT ${limit}
    `,
		];
	}

	@callable()
	getEventsByType(type, limit = 20) {
		return [
			...this.sql`
      SELECT * FROM events
      WHERE type = ${type}
      ORDER BY timestamp DESC
      LIMIT ${limit}
    `,
		];
	}
}
import { Agent, callable } from "agents";

class WebhookAgent extends Agent {
	@callable()
	getEvents(limit = 20) {
		return [
			...this.sql`
      SELECT * FROM events
      ORDER BY timestamp DESC
      LIMIT ${limit}
    `,
		];
	}

	@callable()
	getEventsByType(type: string, limit = 20) {
		return [
			...this.sql`
      SELECT * FROM events
      WHERE type = ${type}
      ORDER BY timestamp DESC
      LIMIT ${limit}
    `,
		];
	}
}

Вещание в реальном времени

Когда приходит вебхук, обновите состояние агента, чтобы оно автоматически разошлось всем подключённым клиентам WebSocket.

class WebhookAgent extends Agent {
	async processWebhook(eventType, payload) {
		// Update state - this automatically broadcasts to all connected clients
		this.setState({
			...this.state,
			stats: payload.stats,
			lastEvent: {
				type: eventType,
				timestamp: new Date().toISOString(),
			},
		});
	}
}
class WebhookAgent extends Agent {
	private async processWebhook(eventType: string, payload: WebhookPayload) {
		// Update state - this automatically broadcasts to all connected clients
		this.setState({
			...this.state,
			stats: payload.stats,
			lastEvent: {
				type: eventType,
				timestamp: new Date().toISOString(),
			},
		});
	}
}

На стороне клиента:

import { useAgent } from "agents/react";

function Dashboard() {
	const [state, setState] = useState(null);

	const agent = useAgent({
		agent: "webhook-agent",
		name: "my-entity-id",
		onStateUpdate: (newState) => {
			setState(newState); // Automatically updates when webhooks arrive
		},
	});

	return <div>Last event: {state?.lastEvent?.type}</div>;
}

Паттерны

Дедупликация событий

Предотвращение обработки повторяющихся событий с помощью идентификаторов событий:

class WebhookAgent extends Agent {
	async handleEvent(eventId, payload) {
		// Check if already processed
		const existing = [
			...this.sql`
      SELECT id FROM events WHERE id = ${eventId}
    `,
		];

		if (existing.length > 0) {
			console.log(`Event ${eventId} already processed, skipping`);
			return;
		}

		// Process and store
		await this.processPayload(payload);
		this.sql`INSERT INTO events (id, ...) VALUES (${eventId}, ...)`;
	}
}
class WebhookAgent extends Agent {
	async handleEvent(eventId: string, payload: unknown) {
		// Check if already processed
		const existing = [
			...this.sql`
      SELECT id FROM events WHERE id = ${eventId}
    `,
		];

		if (existing.length > 0) {
			console.log(`Event ${eventId} already processed, skipping`);
			return;
		}

		// Process and store
		await this.processPayload(payload);
		this.sql`INSERT INTO events (id, ...) VALUES (${eventId}, ...)`;
	}
}

Отвечать быстро, обрабатывать асинхронно

Поставщики вебхуков ожидают быстрых ответов. Для ресурсоёмкой обработки используйте очередь:

class WebhookAgent extends Agent {
	async onRequest(request) {
		const payload = await request.json();

		// Quick validation
		if (!this.isValid(payload)) {
			return new Response("Invalid", { status: 400 });
		}

		// Queue heavy processing
		await this.queue("processWebhook", payload);

		// Respond immediately
		return new Response("Accepted", { status: 202 });
	}

	async processWebhook(payload) {
		// Heavy processing happens here, after response sent
		await this.enrichData(payload);
		await this.notifyDownstream(payload);
		await this.updateAnalytics(payload);
	}
}
class WebhookAgent extends Agent {
	async onRequest(request: Request): Promise<Response> {
		const payload = await request.json();

		// Quick validation
		if (!this.isValid(payload)) {
			return new Response("Invalid", { status: 400 });
		}

		// Queue heavy processing
		await this.queue("processWebhook", payload);

		// Respond immediately
		return new Response("Accepted", { status: 202 });
	}

	async processWebhook(payload: WebhookPayload) {
		// Heavy processing happens here, after response sent
		await this.enrichData(payload);
		await this.notifyDownstream(payload);
		await this.updateAnalytics(payload);
	}
}

Если асинхронная задача представляет собой один ход чата Think, используйте submitMessages() вместо этого. Он сразу возвращает устойчивый идентификатор отправки и позволяет повторным попыткам использовать ключ идемпотентности вместо дублирования хода сообщения:

const submission = await this.submitMessages(messages, {
	idempotencyKey: payload.id,
});

return Response.json(
	{ submissionId: submission.submissionId },
	{ status: 202 },
);
const submission = await this.submitMessages(messages, {
	idempotencyKey: payload.id,
});

return Response.json(
	{ submissionId: submission.submissionId },
	{ status: 202 },
);

Если вебхук отвечает за побочные эффекты приложения вокруг хода, например за восстановление треда провайдера и публикацию видимого ответа, используйте startFiber() вокруг этой задачи. Управляемые fiber сохраняют статус, устраняют дублирование повторных попыток провайдера и позволяют onFiberRecovered() или resolveFiber() записывает результат восстановления на уровне приложения.

Маршрутизация между несколькими провайдерами

Используйте одну типизированную вспомогательную функцию для проверки и разбора, специфичных для провайдера. Она должна проверять исходный запрос согласно документации провайдера, указанной в Типичные поставщики вебхуков, затем выведите agentName только из проверенного тела запроса.

export default {
	async fetch(request, env) {
		const url = new URL(request.url);
		if (request.method === "POST" && url.pathname.startsWith("/webhooks/")) {
			const verified = await verifyAndParseWebhook(request.clone(), env);
			if (!verified) {
				return new Response("Invalid signature", { status: 401 });
			}

			switch (verified.provider) {
				case "github":
					return (
						await getAgentByName(env.GitHubAgent, verified.agentName)
					).fetch(request);
				case "stripe":
					return (
						await getAgentByName(env.StripeAgent, verified.agentName)
					).fetch(request);
				case "slack":
					return (
						await getAgentByName(env.SlackAgent, verified.agentName)
					).fetch(request);
			}
		}

		return (
			(await routeAgentRequest(request, env)) ??
			new Response("Not found", { status: 404 })
		);
	},
};
type VerifiedWebhook =
	| { provider: "github"; agentName: string }
	| { provider: "stripe"; agentName: string }
	| { provider: "slack"; agentName: string };

declare function verifyAndParseWebhook(
	request: Request,
	env: Env,
): Promise<VerifiedWebhook | null>;

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);
		if (request.method === "POST" && url.pathname.startsWith("/webhooks/")) {
			const verified = await verifyAndParseWebhook(request.clone(), env);
			if (!verified) {
				return new Response("Invalid signature", { status: 401 });
			}

			switch (verified.provider) {
				case "github":
					return (
						await getAgentByName(env.GitHubAgent, verified.agentName)
					).fetch(request);
				case "stripe":
					return (
						await getAgentByName(env.StripeAgent, verified.agentName)
					).fetch(request);
				case "slack":
					return (
						await getAgentByName(env.SlackAgent, verified.agentName)
					).fetch(request);
			}
		}

		return (
			(await routeAgentRequest(request, env)) ??
			new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

Отправка исходящих вебхуков

Agents также могут отправлять вебхуки во внешние сервисы:

export class NotificationAgent extends Agent {
	async notifySlack(message) {
		const response = await fetch(this.env.SLACK_WEBHOOK_URL, {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({ text: message }),
		});

		if (!response.ok) {
			throw new Error(`Slack notification failed: ${response.status}`);
		}
	}

	async sendSignedWebhook(url, payload) {
		const body = JSON.stringify(payload);
		const signature = await this.sign(body, this.env.WEBHOOK_SECRET);

		await fetch(url, {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
				"X-Signature": signature,
			},
			body,
		});
	}
}
export class NotificationAgent extends Agent {
	async notifySlack(message: string) {
		const response = await fetch(this.env.SLACK_WEBHOOK_URL, {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({ text: message }),
		});

		if (!response.ok) {
			throw new Error(`Slack notification failed: ${response.status}`);
		}
	}

	async sendSignedWebhook(url: string, payload: unknown) {
		const body = JSON.stringify(payload);
		const signature = await this.sign(body, this.env.WEBHOOK_SECRET);

		await fetch(url, {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
				"X-Signature": signature,
			},
			body,
		});
	}
}

Рекомендации по безопасности

  1. Всегда проверяйте подписи - Никогда не доверяйте непроверенным вебхукам.
  2. Используйте секреты окружения - Храните секреты с помощью wrangler secret put, а не в коде.
  3. Отвечать быстро - Возвращайте 200/202 в течение нескольких секунд, чтобы избежать повторных попыток.
  4. Валидируйте полезные нагрузки - проверяйте обязательные поля перед обработкой.
  5. Регистрируйте отклонения - Отслеживайте недействительные подписи для мониторинга безопасности.
  6. Используйте HTTPS - URL вебхуков всегда должны использовать TLS.
// Store secrets securely
// wrangler secret put GITHUB_WEBHOOK_SECRET

// Access in agent
const secret = this.env.GITHUB_WEBHOOK_SECRET;
// Store secrets securely
// wrangler secret put GITHUB_WEBHOOK_SECRET

// Access in agent
const secret = this.env.GITHUB_WEBHOOK_SECRET;

Типичные поставщики вебхуков

Провайдер Документация
GitHub События и полезные нагрузки вебхуков ↗
Stripe Подписи вебхуков ↗
Twilio Валидируйте запросы webhook ↗
Slack Проверка запросов ↗
Shopify Проверка вебхуков ↗
SendGrid Вебхук событий ↗
Linear Webhooks ↗

Следующие шаги

Задачи очереди

Обработка фоновых задач.

Маршрутизация электронной почты

Обрабатывайте входящие письма в своём агенте.

Agents API

Полный справочник API для Agents SDK.