@kya-os/agentshield-nextjs
Version:
Next.js middleware for AgentShield AI agent detection
1,486 lines (1,471 loc) • 60.4 kB
JavaScript
'use strict';
var server = require('next/server');
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/wasm-loader.ts
function setWasmBaseUrl(url) {
baseUrl = url;
}
function getWasmUrl() {
if (baseUrl) {
try {
const url = new URL(baseUrl);
return `${url.origin}${WASM_PATH}`;
} catch {
return WASM_PATH;
}
}
return WASM_PATH;
}
async function initWasm() {
if (wasmExports) return true;
if (initPromise) {
await initPromise;
return !!wasmExports;
}
initPromise = (async () => {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3e3);
try {
const wasmUrl = getWasmUrl();
if (typeof WebAssembly.instantiateStreaming === "function") {
try {
const response2 = await fetch(wasmUrl, { signal: controller.signal });
clearTimeout(timeout);
if (!response2.ok) {
throw new Error(`Failed to fetch WASM: ${response2.status}`);
}
const streamResponse = response2.clone();
const { instance } = await WebAssembly.instantiateStreaming(
streamResponse,
{
wbg: {
__wbg_log_1d3ae13c3d5e6b8e: (ptr, len) => {
console.log("WASM:", ptr, len);
},
__wbindgen_throw: (ptr, len) => {
throw new Error(`WASM Error at ${ptr}, length ${len}`);
}
}
}
);
wasmInstance = instance;
wasmExports = instance.exports;
console.log("[AgentShield] \u2705 WASM module initialized with streaming");
return;
} catch (streamError) {
if (!controller.signal.aborted) {
console.log("[AgentShield] Streaming compilation failed, falling back to standard compilation");
} else {
throw streamError;
}
}
}
const response = await fetch(wasmUrl, { signal: controller.signal });
clearTimeout(timeout);
if (!response.ok) {
throw new Error(`Failed to fetch WASM: ${response.status}`);
}
const wasmArrayBuffer = await response.arrayBuffer();
const compiledModule = await WebAssembly.compile(wasmArrayBuffer);
const imports = {
wbg: {
__wbg_log_1d3ae13c3d5e6b8e: (ptr, len) => {
console.log("WASM:", ptr, len);
},
__wbindgen_throw: (ptr, len) => {
throw new Error(`WASM Error at ${ptr}, length ${len}`);
}
}
};
wasmInstance = await WebAssembly.instantiate(compiledModule, imports);
wasmExports = wasmInstance.exports;
console.log("[AgentShield] \u2705 WASM module initialized via fallback");
} catch (fetchError) {
if (fetchError.name === "AbortError") {
console.warn("[AgentShield] WASM fetch timed out after 3 seconds - using pattern detection");
} else {
console.warn("[AgentShield] Failed to fetch WASM file:", fetchError.message);
}
wasmExports = null;
}
} catch (error) {
console.error("[AgentShield] Failed to initialize WASM:", error);
wasmExports = null;
}
})();
await initPromise;
return !!wasmExports;
}
async function detectAgentWithWasm(userAgent, headers, ipAddress) {
const initialized = await initWasm();
if (!initialized || !wasmExports) {
return null;
}
try {
const headersJson = JSON.stringify(headers);
if (typeof wasmExports.detect_agent === "function") {
const result = wasmExports.detect_agent(
userAgent || "",
headersJson,
ipAddress || "",
(/* @__PURE__ */ new Date()).toISOString()
);
return {
isAgent: result.is_agent || false,
confidence: result.confidence || 0,
agent: result.agent,
verificationMethod: result.verification_method || "wasm",
riskLevel: result.risk_level || "low"
};
}
console.warn("[AgentShield] WASM exports do not include detect_agent function");
return null;
} catch (error) {
console.error("[AgentShield] WASM detection failed:", error);
return null;
}
}
async function getWasmVersion() {
const initialized = await initWasm();
if (!initialized || !wasmExports) {
return null;
}
if (typeof wasmExports.version === "function") {
return wasmExports.version();
}
return "unknown";
}
async function isWasmAvailable() {
try {
const initialized = await initWasm();
if (!initialized) return false;
const version = await getWasmVersion();
return version !== null;
} catch {
return false;
}
}
var wasmInstance, wasmExports, initPromise, WASM_PATH, baseUrl;
var init_wasm_loader = __esm({
"src/wasm-loader.ts"() {
wasmInstance = null;
wasmExports = null;
initPromise = null;
WASM_PATH = "/wasm/agentshield_wasm_bg.wasm";
baseUrl = null;
}
});
// src/edge-detector-with-wasm.ts
var edge_detector_with_wasm_exports = {};
__export(edge_detector_with_wasm_exports, {
EdgeAgentDetectorWithWasm: () => EdgeAgentDetectorWithWasm,
EdgeAgentDetectorWrapperWithWasm: () => EdgeAgentDetectorWrapperWithWasm
});
var AI_AGENT_PATTERNS2, CLOUD_PROVIDERS2, EdgeAgentDetectorWithWasm, EdgeAgentDetectorWrapperWithWasm;
var init_edge_detector_with_wasm = __esm({
"src/edge-detector-with-wasm.ts"() {
init_wasm_loader();
AI_AGENT_PATTERNS2 = [
{ pattern: /chatgpt-user/i, type: "chatgpt", name: "ChatGPT" },
{ pattern: /claude-web/i, type: "claude", name: "Claude" },
{ pattern: /perplexitybot/i, type: "perplexity", name: "Perplexity" },
{ pattern: /perplexity-user/i, type: "perplexity", name: "Perplexity" },
{ pattern: /perplexity-ai/i, type: "perplexity", name: "Perplexity" },
{ pattern: /perplexity/i, type: "perplexity", name: "Perplexity" },
{ pattern: /bingbot/i, type: "bing", name: "Bing AI" },
{ pattern: /anthropic-ai/i, type: "anthropic", name: "Anthropic" }
];
CLOUD_PROVIDERS2 = {
aws: ["54.", "52.", "35.", "18.", "3."],
gcp: ["35.", "34.", "104.", "107.", "108."],
azure: ["13.", "20.", "40.", "52.", "104."]
};
EdgeAgentDetectorWithWasm = class {
constructor(enableWasm = true) {
this.enableWasm = enableWasm;
}
wasmEnabled = false;
initPromise = null;
baseUrl = null;
/**
* Set the base URL for WASM loading in Edge Runtime
*/
setBaseUrl(url) {
this.baseUrl = url;
setWasmBaseUrl(url);
}
/**
* Initialize the detector (including WASM if enabled)
*/
async init() {
if (!this.enableWasm) {
this.wasmEnabled = false;
return;
}
if (this.initPromise) {
await this.initPromise;
return;
}
this.initPromise = (async () => {
try {
const wasmAvailable = await isWasmAvailable();
this.wasmEnabled = wasmAvailable;
if (this.wasmEnabled) {
console.log("[AgentShield] WASM detection enabled");
} else {
console.log("[AgentShield] WASM not available, using pattern detection");
}
} catch (error) {
console.error("[AgentShield] Failed to initialize WASM:", error);
this.wasmEnabled = false;
}
})();
await this.initPromise;
}
/**
* Pattern-based detection (fallback)
*/
async patternDetection(input) {
const reasons = [];
let detectedAgent;
let verificationMethod;
let confidence = 0;
const headers = input.headers || {};
const normalizedHeaders = {};
for (const [key, value] of Object.entries(headers)) {
normalizedHeaders[key.toLowerCase()] = value;
}
const signaturePresent = !!(normalizedHeaders["signature"] || normalizedHeaders["signature-input"]);
const signatureAgent = normalizedHeaders["signature-agent"];
if (signatureAgent?.includes("chatgpt.com")) {
confidence = 0.85;
reasons.push("signature_agent:chatgpt");
detectedAgent = { type: "chatgpt", name: "ChatGPT" };
verificationMethod = "signature";
} else if (signaturePresent) {
confidence = Math.max(confidence, 0.4);
reasons.push("signature_present");
}
const userAgent = input.userAgent || input.headers?.["user-agent"] || "";
if (userAgent) {
for (const { pattern, type, name } of AI_AGENT_PATTERNS2) {
if (pattern.test(userAgent)) {
const highConfidenceAgents = [
"chatgpt",
"claude",
"perplexity",
"anthropic"
];
const patternConfidence = highConfidenceAgents.includes(type) ? 0.85 : 0.5;
confidence = Math.max(confidence, patternConfidence);
reasons.push(`known_pattern:${type}`);
if (!detectedAgent) {
detectedAgent = { type, name };
verificationMethod = "pattern";
}
break;
}
}
}
const aiHeaders = [
"openai-conversation-id",
"openai-ephemeral-user-id",
"anthropic-client-id",
"x-goog-api-client",
"x-ms-copilot-id"
];
const foundAiHeaders = aiHeaders.filter(
(header) => normalizedHeaders[header]
);
if (foundAiHeaders.length > 0) {
confidence = Math.max(confidence, 0.6);
reasons.push(`ai_headers:${foundAiHeaders.length}`);
}
const ip = input.ip || input.ipAddress;
if (ip && !normalizedHeaders["x-forwarded-for"] && !normalizedHeaders["x-real-ip"]) {
for (const [provider, prefixes] of Object.entries(CLOUD_PROVIDERS2)) {
if (prefixes.some((prefix) => ip.startsWith(prefix))) {
confidence = Math.max(confidence, 0.4);
reasons.push(`cloud_provider:${provider}`);
break;
}
}
}
if (reasons.length > 2) {
confidence = Math.min(confidence * 1.2, 0.95);
}
return {
isAgent: confidence > 0.3,
confidence,
...detectedAgent && { detectedAgent },
reasons,
...verificationMethod && { verificationMethod },
forgeabilityRisk: confidence > 0.8 ? "medium" : "high",
timestamp: /* @__PURE__ */ new Date()
};
}
/**
* Analyze request with WASM enhancement when available
*/
async analyze(input) {
await this.init();
if (this.wasmEnabled) {
try {
const wasmResult = await detectAgentWithWasm(
input.userAgent || input.headers?.["user-agent"],
input.headers || {},
input.ip || input.ipAddress
);
if (wasmResult) {
console.log("[AgentShield] Using WASM detection result");
const detectedAgent = wasmResult.agent ? this.mapAgentName(wasmResult.agent) : void 0;
return {
isAgent: wasmResult.isAgent,
confidence: wasmResult.confidence,
...detectedAgent && { detectedAgent },
reasons: [`wasm:${wasmResult.verificationMethod}`],
verificationMethod: wasmResult.verificationMethod,
forgeabilityRisk: wasmResult.verificationMethod === "signature" ? "low" : wasmResult.confidence > 0.9 ? "medium" : "high",
timestamp: /* @__PURE__ */ new Date()
};
}
} catch (error) {
console.error("[AgentShield] WASM detection error:", error);
}
}
const patternResult = await this.patternDetection(input);
if (this.wasmEnabled && patternResult.confidence >= 0.85) {
patternResult.confidence = Math.min(0.95, patternResult.confidence + 0.1);
patternResult.reasons.push("wasm_enhanced");
if (!patternResult.verificationMethod) {
patternResult.verificationMethod = "wasm-enhanced";
}
}
return patternResult;
}
/**
* Map agent names from WASM to consistent format
*/
mapAgentName(agent) {
const lowerAgent = agent.toLowerCase();
if (lowerAgent.includes("chatgpt")) {
return { type: "chatgpt", name: "ChatGPT" };
} else if (lowerAgent.includes("claude")) {
return { type: "claude", name: "Claude" };
} else if (lowerAgent.includes("perplexity")) {
return { type: "perplexity", name: "Perplexity" };
} else if (lowerAgent.includes("bing")) {
return { type: "bing", name: "Bing AI" };
} else if (lowerAgent.includes("anthropic")) {
return { type: "anthropic", name: "Anthropic" };
}
return { type: "unknown", name: agent };
}
};
EdgeAgentDetectorWrapperWithWasm = class {
detector;
events = /* @__PURE__ */ new Map();
constructor(config) {
this.detector = new EdgeAgentDetectorWithWasm(config?.enableWasm ?? true);
if (config?.baseUrl) {
this.detector.setBaseUrl(config.baseUrl);
}
}
setBaseUrl(url) {
this.detector.setBaseUrl(url);
}
async analyze(input) {
const result = await this.detector.analyze(input);
if (result.isAgent && this.events.has("agent.detected")) {
const handlers = this.events.get("agent.detected") || [];
handlers.forEach((handler) => handler(result, input));
}
return result;
}
on(event, handler) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event).push(handler);
}
emit(event, ...args) {
const handlers = this.events.get(event) || [];
handlers.forEach((handler) => handler(...args));
}
async init() {
await this.detector.init();
}
};
}
});
// src/wasm-confidence.ts
var wasm_confidence_exports = {};
__export(wasm_confidence_exports, {
checkWasmAvailability: () => checkWasmAvailability,
getVerificationMethod: () => getVerificationMethod,
getWasmConfidenceBoost: () => getWasmConfidenceBoost,
shouldIndicateWasmVerification: () => shouldIndicateWasmVerification
});
async function checkWasmAvailability() {
try {
if (typeof WebAssembly === "undefined") {
return false;
}
if (!WebAssembly.instantiate || !WebAssembly.Module) {
return false;
}
return true;
} catch {
return false;
}
}
function shouldIndicateWasmVerification(confidence) {
return confidence >= 0.85 && confidence < 1;
}
function getWasmConfidenceBoost(baseConfidence, reasons = []) {
if (reasons.some(
(r) => r.includes("signature_agent") && !r.includes("signature_headers_present")
)) {
return 1;
}
if (baseConfidence >= 0.85) {
return 0.95;
}
if (baseConfidence >= 0.7) {
return Math.min(baseConfidence * 1.1, 0.9);
}
return baseConfidence;
}
function getVerificationMethod(confidence, reasons = []) {
if (reasons.some(
(r) => r.includes("signature_agent") && !r.includes("signature_headers_present")
)) {
return "signature";
}
if (shouldIndicateWasmVerification(confidence)) {
return "wasm-enhanced";
}
return "pattern";
}
var init_wasm_confidence = __esm({
"src/wasm-confidence.ts"() {
}
});
// src/signature-verifier.ts
var KNOWN_KEYS = {
chatgpt: [
{
kid: "otMqcjr17mGyruktGvJU8oojQTSMHlVm7uO-lrcqbdg",
// ChatGPT's current Ed25519 public key (base64)
publicKey: "7F_3jDlxaquwh291MiACkcS3Opq88NksyHiakzS-Y1g",
validFrom: (/* @__PURE__ */ new Date("2025-01-01")).getTime() / 1e3,
validUntil: (/* @__PURE__ */ new Date("2025-04-11")).getTime() / 1e3
}
]
};
function parseSignatureInput(signatureInput) {
try {
const match = signatureInput.match(/sig1=\((.*?)\);(.+)/);
if (!match) return null;
const [, headersList, params] = match;
const signedHeaders = headersList ? headersList.split(" ").map((h) => h.replace(/"/g, "").trim()).filter((h) => h.length > 0) : [];
const keyidMatch = params ? params.match(/keyid="([^"]+)"/) : null;
const createdMatch = params ? params.match(/created=(\d+)/) : null;
const expiresMatch = params ? params.match(/expires=(\d+)/) : null;
if (!keyidMatch || !keyidMatch[1]) return null;
return {
keyid: keyidMatch[1],
created: createdMatch && createdMatch[1] ? parseInt(createdMatch[1]) : void 0,
expires: expiresMatch && expiresMatch[1] ? parseInt(expiresMatch[1]) : void 0,
signedHeaders
};
} catch (error) {
console.error("[Signature] Failed to parse Signature-Input:", error);
return null;
}
}
function buildSignatureBase(method, path, headers, signedHeaders) {
const components = [];
for (const headerName of signedHeaders) {
let value;
switch (headerName) {
case "@method":
value = method.toUpperCase();
break;
case "@path":
value = path;
break;
case "@authority":
value = headers["host"] || headers["Host"] || "";
break;
default:
const key = Object.keys(headers).find(
(k) => k.toLowerCase() === headerName.toLowerCase()
);
value = key ? headers[key] || "" : "";
break;
}
components.push(`"${headerName}": ${value}`);
}
return components.join("\n");
}
async function verifyEd25519Signature(publicKeyBase64, signatureBase64, message) {
try {
const publicKeyBytes = Uint8Array.from(atob(publicKeyBase64), (c) => c.charCodeAt(0));
const signatureBytes = Uint8Array.from(atob(signatureBase64), (c) => c.charCodeAt(0));
const messageBytes = new TextEncoder().encode(message);
if (publicKeyBytes.length !== 32) {
console.error("[Signature] Invalid public key length:", publicKeyBytes.length);
return false;
}
if (signatureBytes.length !== 64) {
console.error("[Signature] Invalid signature length:", signatureBytes.length);
return false;
}
const publicKey = await crypto.subtle.importKey(
"raw",
publicKeyBytes,
{
name: "Ed25519",
namedCurve: "Ed25519"
},
false,
["verify"]
);
const isValid = await crypto.subtle.verify(
"Ed25519",
publicKey,
signatureBytes,
messageBytes
);
return isValid;
} catch (error) {
console.error("[Signature] Ed25519 verification failed:", error);
if (typeof window === "undefined") {
try {
console.warn("[Signature] Ed25519 not supported in this environment");
return false;
} catch {
return false;
}
}
return false;
}
}
async function verifyAgentSignature(method, path, headers) {
const signature = headers["signature"] || headers["Signature"];
const signatureInput = headers["signature-input"] || headers["Signature-Input"];
const signatureAgent = headers["signature-agent"] || headers["Signature-Agent"];
if (!signature || !signatureInput) {
return {
isValid: false,
confidence: 0,
reason: "No signature headers present",
verificationMethod: "none"
};
}
const parsed = parseSignatureInput(signatureInput);
if (!parsed) {
return {
isValid: false,
confidence: 0,
reason: "Invalid Signature-Input header",
verificationMethod: "none"
};
}
if (parsed.created) {
const now2 = Math.floor(Date.now() / 1e3);
const age = now2 - parsed.created;
if (age > 300) {
return {
isValid: false,
confidence: 0,
reason: "Signature expired (older than 5 minutes)",
verificationMethod: "none"
};
}
if (age < -30) {
return {
isValid: false,
confidence: 0,
reason: "Signature timestamp is in the future",
verificationMethod: "none"
};
}
}
let agent;
let knownKeys;
if (signatureAgent === '"https://chatgpt.com"' || signatureAgent?.includes("chatgpt.com")) {
agent = "ChatGPT";
knownKeys = KNOWN_KEYS.chatgpt;
}
if (!agent || !knownKeys) {
return {
isValid: false,
confidence: 0,
reason: "Unknown signature agent",
verificationMethod: "none"
};
}
const key = knownKeys.find((k) => k.kid === parsed.keyid);
if (!key) {
return {
isValid: false,
confidence: 0,
reason: `Unknown key ID: ${parsed.keyid}`,
verificationMethod: "none"
};
}
const now = Math.floor(Date.now() / 1e3);
if (now < key.validFrom || now > key.validUntil) {
return {
isValid: false,
confidence: 0,
reason: "Key is not valid at current time",
verificationMethod: "none"
};
}
const signatureBase = buildSignatureBase(method, path, headers, parsed.signedHeaders);
let signatureValue = signature;
if (signatureValue.startsWith("sig1=:")) {
signatureValue = signatureValue.substring(6);
}
if (signatureValue.endsWith(":")) {
signatureValue = signatureValue.slice(0, -1);
}
const isValid = await verifyEd25519Signature(
key.publicKey,
signatureValue,
signatureBase
);
if (isValid) {
return {
isValid: true,
agent,
keyid: parsed.keyid,
confidence: 1,
// 100% confidence for valid signature
verificationMethod: "signature"
};
} else {
return {
isValid: false,
confidence: 0,
reason: "Signature verification failed",
verificationMethod: "none"
};
}
}
function hasSignatureHeaders(headers) {
return !!((headers["signature"] || headers["Signature"]) && (headers["signature-input"] || headers["Signature-Input"]));
}
function isChatGPTSignature(headers) {
const signatureAgent = headers["signature-agent"] || headers["Signature-Agent"];
return signatureAgent === '"https://chatgpt.com"' || (signatureAgent?.includes("chatgpt.com") || false);
}
// src/edge-detector-wrapper.ts
var AI_AGENT_PATTERNS = [
{ pattern: /chatgpt-user/i, type: "chatgpt", name: "ChatGPT" },
{ pattern: /claude-web/i, type: "claude", name: "Claude" },
{ pattern: /perplexitybot/i, type: "perplexity", name: "Perplexity" },
{ pattern: /perplexity-user/i, type: "perplexity", name: "Perplexity" },
{ pattern: /perplexity-ai/i, type: "perplexity", name: "Perplexity" },
{ pattern: /perplexity/i, type: "perplexity", name: "Perplexity" },
// Fallback
{ pattern: /bingbot/i, type: "bing", name: "Bing AI" },
{ pattern: /anthropic-ai/i, type: "anthropic", name: "Anthropic" }
];
var CLOUD_PROVIDERS = {
aws: ["54.", "52.", "35.", "18.", "3."],
gcp: ["35.", "34.", "104.", "107.", "108."],
azure: ["13.", "20.", "40.", "52.", "104."]
};
var EdgeAgentDetector = class {
async analyze(input) {
const reasons = [];
let detectedAgent;
let verificationMethod;
let confidence = 0;
const headers = input.headers || {};
const normalizedHeaders = {};
for (const [key, value] of Object.entries(headers)) {
normalizedHeaders[key.toLowerCase()] = value;
}
if (hasSignatureHeaders(headers)) {
try {
const signatureResult = await verifyAgentSignature(
input.method || "GET",
input.url || "/",
headers
);
if (signatureResult.isValid) {
confidence = signatureResult.confidence;
reasons.push(`verified_signature:${signatureResult.agent?.toLowerCase() || "unknown"}`);
if (signatureResult.agent) {
detectedAgent = {
type: signatureResult.agent.toLowerCase(),
name: signatureResult.agent
};
}
verificationMethod = signatureResult.verificationMethod;
if (signatureResult.keyid) {
reasons.push(`keyid:${signatureResult.keyid}`);
}
} else {
confidence = Math.max(confidence, 0.3);
reasons.push("invalid_signature");
if (signatureResult.reason) {
reasons.push(`signature_error:${signatureResult.reason}`);
}
if (isChatGPTSignature(headers)) {
reasons.push("claims_chatgpt");
detectedAgent = { type: "chatgpt", name: "ChatGPT (unverified)" };
}
}
} catch (error) {
console.error("[EdgeAgentDetector] Signature verification error:", error);
confidence = Math.max(confidence, 0.2);
reasons.push("signature_verification_error");
}
}
const userAgent = input.userAgent || input.headers?.["user-agent"] || "";
if (userAgent) {
for (const { pattern, type, name } of AI_AGENT_PATTERNS) {
if (pattern.test(userAgent)) {
const highConfidenceAgents = [
"chatgpt",
"claude",
"perplexity",
"anthropic"
];
const patternConfidence = highConfidenceAgents.includes(type) ? 0.85 : 0.5;
confidence = Math.max(confidence, patternConfidence);
reasons.push(`known_pattern:${type}`);
if (!detectedAgent) {
detectedAgent = { type, name };
verificationMethod = "pattern";
}
break;
}
}
}
const aiHeaders = [
"openai-conversation-id",
"openai-ephemeral-user-id",
"anthropic-client-id",
"x-goog-api-client",
"x-ms-copilot-id"
];
const foundAiHeaders = aiHeaders.filter(
(header) => normalizedHeaders[header]
);
if (foundAiHeaders.length > 0) {
confidence = Math.max(confidence, 0.6);
reasons.push(`ai_headers:${foundAiHeaders.length}`);
}
const ip = input.ip || input.ipAddress;
if (ip && !normalizedHeaders["x-forwarded-for"] && !normalizedHeaders["x-real-ip"]) {
for (const [provider, prefixes] of Object.entries(CLOUD_PROVIDERS)) {
if (prefixes.some((prefix) => ip.startsWith(prefix))) {
confidence = Math.max(confidence, 0.4);
reasons.push(`cloud_provider:${provider}`);
break;
}
}
}
if (reasons.length > 2 && confidence < 1) {
confidence = Math.min(confidence * 1.2, 0.95);
}
return {
isAgent: confidence > 0.3,
confidence,
...detectedAgent && { detectedAgent },
reasons,
...verificationMethod && { verificationMethod },
forgeabilityRisk: verificationMethod === "signature" ? "low" : confidence > 0.8 ? "medium" : "high",
timestamp: /* @__PURE__ */ new Date()
};
}
};
var EdgeAgentDetectorWrapper = class {
detector;
events = /* @__PURE__ */ new Map();
constructor(_config) {
this.detector = new EdgeAgentDetector();
}
async analyze(input) {
const result = await this.detector.analyze(input);
if (result.isAgent && this.events.has("agent.detected")) {
const handlers = this.events.get("agent.detected") || [];
handlers.forEach((handler) => handler(result, input));
}
return result;
}
on(event, handler) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event).push(handler);
}
emit(event, ...args) {
const handlers = this.events.get(event) || [];
handlers.forEach((handler) => handler(...args));
}
async init() {
return;
}
};
// src/middleware.ts
init_edge_detector_with_wasm();
// src/session-tracker.ts
var EdgeSessionTracker = class {
config;
constructor(config) {
this.config = {
enabled: config.enabled,
cookieName: config.cookieName || "__agentshield_session",
cookieMaxAge: config.cookieMaxAge || 3600,
// 1 hour default
encryptionKey: config.encryptionKey || process.env.AGENTSHIELD_SECRET || "agentshield-default-key"
};
}
/**
* Track a new AI agent session
*/
async track(_request, response, result) {
try {
if (!this.config.enabled || !result.isAgent) {
return response;
}
const sessionData = {
id: crypto.randomUUID(),
agent: result.detectedAgent?.name || "unknown",
confidence: result.confidence,
detectedAt: Date.now(),
expires: Date.now() + this.config.cookieMaxAge * 1e3
};
const encrypted = await this.encrypt(JSON.stringify(sessionData));
response.cookies.set(this.config.cookieName, encrypted, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: this.config.cookieMaxAge,
path: "/"
});
return response;
} catch (error) {
if (process.env.DEBUG_AGENTSHIELD) {
console.warn("AgentShield: Failed to track session:", error);
}
return response;
}
}
/**
* Check for existing AI agent session
*/
async check(request) {
try {
if (!this.config.enabled) {
return null;
}
const cookie = request.cookies.get(this.config.cookieName);
if (!cookie?.value) {
return null;
}
const decrypted = await this.decrypt(cookie.value);
const session = JSON.parse(decrypted);
if (session.expires < Date.now()) {
return null;
}
return session;
} catch (error) {
if (process.env.DEBUG_AGENTSHIELD) {
console.warn("AgentShield: Failed to check session:", error);
}
return null;
}
}
/**
* Clear an existing session
*/
clear(response) {
try {
response.cookies.delete(this.config.cookieName);
} catch (error) {
if (process.env.DEBUG_AGENTSHIELD) {
console.warn("AgentShield: Failed to clear session:", error);
}
}
return response;
}
/**
* Simple encryption using Web Crypto API (Edge-compatible)
*/
async encrypt(data) {
try {
const key = this.config.encryptionKey;
const encoded = new TextEncoder().encode(data);
const obfuscated = new Uint8Array(encoded.length);
for (let i = 0; i < encoded.length; i++) {
obfuscated[i] = (encoded[i] || 0) ^ key.charCodeAt(i % key.length);
}
return btoa(
Array.from(obfuscated, (byte) => String.fromCharCode(byte)).join("")
);
} catch (error) {
return btoa(data);
}
}
/**
* Simple decryption (Edge-compatible)
*/
async decrypt(data) {
try {
const key = this.config.encryptionKey;
const decoded = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
const deobfuscated = new Uint8Array(decoded.length);
for (let i = 0; i < decoded.length; i++) {
deobfuscated[i] = (decoded[i] || 0) ^ key.charCodeAt(i % key.length);
}
return new TextDecoder().decode(deobfuscated);
} catch (error) {
return atob(data);
}
}
};
var StatelessSessionChecker = class {
static check(headers) {
try {
const agent = headers["x-agentshield-session-agent"];
const confidence = headers["x-agentshield-session-confidence"];
const sessionId = headers["x-agentshield-session-id"];
if (agent && confidence && sessionId) {
return {
id: sessionId,
agent,
confidence: parseFloat(confidence),
detectedAt: Date.now(),
expires: Date.now() + 36e5
// 1 hour
};
}
const cookieHeader = headers["cookie"];
if (cookieHeader && cookieHeader.includes("__agentshield_session=")) {
const match = cookieHeader.match(/__agentshield_session=([^;]+)/);
if (match && match[1]) {
try {
const decoded = atob(match[1]);
return JSON.parse(decoded);
} catch {
}
}
}
return null;
} catch {
return null;
}
}
static setHeaders(response, session) {
try {
if (response.setHeader) {
response.setHeader("X-AgentShield-Session-Agent", session.agent);
response.setHeader(
"X-AgentShield-Session-Confidence",
session.confidence.toString()
);
response.setHeader("X-AgentShield-Session-Id", session.id);
} else if (response.headers && response.headers.set) {
response.headers.set("x-agentshield-session-agent", session.agent);
response.headers.set(
"x-agentshield-session-confidence",
session.confidence.toString()
);
response.headers.set("x-agentshield-session-id", session.id);
}
} catch {
}
}
};
// src/middleware.ts
function createAgentShieldMiddleware(config = {}) {
const detector = config.enableWasm ? new EdgeAgentDetectorWrapperWithWasm({ enableWasm: true }) : new EdgeAgentDetectorWrapper(config);
const sessionTracker = config.sessionTracking?.enabled || config.enableWasm ? new EdgeSessionTracker({
enabled: true,
...config.sessionTracking
}) : null;
if (config.events) {
Object.entries(config.events).forEach(([event, handler]) => {
detector.on(event, handler);
});
}
const {
onAgentDetected = "log",
onDetection,
skipPaths = [],
blockedResponse = {
status: 403,
message: "Access denied: Automated agent detected",
headers: { "Content-Type": "application/json" }
},
redirectUrl = "/blocked",
rewriteUrl = "/blocked"
} = config;
return async (request) => {
try {
const shouldSkip = skipPaths.some((pattern) => {
if (typeof pattern === "string") {
return request.nextUrl.pathname.startsWith(pattern);
}
return pattern.test(request.nextUrl.pathname);
});
if (shouldSkip) {
request.agentShield = { skipped: true };
return server.NextResponse.next();
}
const existingSession = sessionTracker ? await sessionTracker.check(request) : null;
if (existingSession) {
const response2 = server.NextResponse.next();
response2.headers.set("x-agentshield-detected", "true");
response2.headers.set("x-agentshield-agent", existingSession.agent);
response2.headers.set(
"x-agentshield-confidence",
existingSession.confidence.toString()
);
response2.headers.set("x-agentshield-session", "continued");
response2.headers.set("x-agentshield-session-id", existingSession.id);
request.agentShield = {
result: {
isAgent: true,
confidence: existingSession.confidence,
detectedAgent: { name: existingSession.agent },
timestamp: /* @__PURE__ */ new Date(),
verificationMethod: "session"
},
session: existingSession,
skipped: false
};
const context2 = {
userAgent: request.headers.get("user-agent") || "",
ipAddress: (request.ip ?? request.headers.get("x-forwarded-for")) || "",
headers: Object.fromEntries(request.headers.entries()),
url: request.url,
method: request.method,
timestamp: /* @__PURE__ */ new Date()
};
detector.emit("agent.session.continued", existingSession, context2);
return response2;
}
const userAgent = request.headers.get("user-agent");
const ipAddress = request.ip ?? request.headers.get("x-forwarded-for");
const url = new URL(request.url);
const pathWithQuery = url.pathname + url.search;
const context = {
...userAgent && { userAgent },
...ipAddress && { ipAddress },
headers: Object.fromEntries(request.headers.entries()),
url: pathWithQuery,
// Use path instead of full URL for signature verification
method: request.method,
timestamp: /* @__PURE__ */ new Date()
};
const result = await detector.analyze(context);
if (result.isAgent && result.confidence >= (config.confidenceThreshold ?? 0.7)) {
if (onDetection) {
const customResponse = await onDetection(request, result);
if (customResponse) {
return customResponse;
}
}
switch (onAgentDetected) {
case "block": {
const response2 = server.NextResponse.json(
{
error: blockedResponse.message,
detected: true,
confidence: result.confidence,
timestamp: result.timestamp
},
{ status: blockedResponse.status }
);
if (blockedResponse.headers) {
Object.entries(blockedResponse.headers).forEach(
([key, value]) => {
response2.headers.set(key, value);
}
);
}
detector.emit("agent.blocked", result, context);
return response2;
}
case "redirect":
return server.NextResponse.redirect(new URL(redirectUrl, request.url));
case "rewrite":
return server.NextResponse.rewrite(new URL(rewriteUrl, request.url));
case "log":
console.warn("AgentShield: Agent detected", {
ipAddress: context.ipAddress,
userAgent: context.userAgent,
confidence: result.confidence,
reasons: result.reasons,
pathname: request.nextUrl.pathname
});
break;
case "allow":
default:
if (result.isAgent && result.confidence >= (config.confidenceThreshold ?? 0.7)) {
detector.emit("agent.allowed", result, context);
}
break;
}
}
request.agentShield = {
result,
skipped: false
};
let response = server.NextResponse.next();
response.headers.set("x-agentshield-detected", result.isAgent.toString());
response.headers.set(
"x-agentshield-confidence",
result.confidence.toString()
);
if (result.detectedAgent?.name) {
response.headers.set("x-agentshield-agent", result.detectedAgent.name);
}
if (sessionTracker && result.isAgent && result.confidence >= (config.confidenceThreshold ?? 0.7)) {
response = await sessionTracker.track(request, response, result);
response.headers.set("x-agentshield-session", "new");
detector.emit("agent.session.started", result, context);
}
return response;
} catch (error) {
console.error("AgentShield middleware error:", error);
return server.NextResponse.next();
}
};
}
// src/create-middleware.ts
var middlewareInstance = null;
var isInitializing = false;
var initPromise2 = null;
function createAgentShieldMiddleware2(config) {
return async function agentShieldMiddleware(request) {
if (!middlewareInstance) {
if (!isInitializing) {
isInitializing = true;
initPromise2 = (async () => {
middlewareInstance = createAgentShieldMiddleware(config);
return middlewareInstance;
})();
}
if (initPromise2) {
middlewareInstance = await initPromise2;
}
}
return middlewareInstance ? middlewareInstance(request) : server.NextResponse.next();
};
}
// src/edge-safe-detector.ts
var AI_AGENT_PATTERNS3 = [
{ pattern: /chatgpt-user/i, type: "chatgpt", name: "ChatGPT" },
{ pattern: /claude-web/i, type: "claude", name: "Claude" },
{ pattern: /perplexitybot/i, type: "perplexity", name: "Perplexity" },
{ pattern: /perplexity-user/i, type: "perplexity", name: "Perplexity" },
{ pattern: /perplexity/i, type: "perplexity", name: "Perplexity" },
{ pattern: /bingbot/i, type: "bing", name: "Bing AI" },
{ pattern: /anthropic-ai/i, type: "anthropic", name: "Anthropic" }
];
var EdgeSafeDetector = class {
async analyze(input) {
const reasons = [];
let detectedAgent;
let confidence = 0;
const headers = input.headers || {};
const normalizedHeaders = {};
for (const [key, value] of Object.entries(headers)) {
normalizedHeaders[key.toLowerCase()] = value;
}
const userAgent = input.userAgent || normalizedHeaders["user-agent"] || "";
if (userAgent) {
for (const { pattern, type, name } of AI_AGENT_PATTERNS3) {
if (pattern.test(userAgent)) {
confidence = 0.85;
reasons.push(`known_pattern:${type}`);
detectedAgent = { type, name };
break;
}
}
}
const hasChrome = userAgent.toLowerCase().includes("chrome");
const hasFirefox = userAgent.toLowerCase().includes("firefox");
const hasSafari = userAgent.toLowerCase().includes("safari");
const hasBrowserUA = hasChrome || hasFirefox || hasSafari;
if (hasBrowserUA) {
const hasSecChUa = !!normalizedHeaders["sec-ch-ua"];
const hasSecFetch = !!normalizedHeaders["sec-fetch-site"];
const hasAcceptLanguage = !!normalizedHeaders["accept-language"];
const hasCookies = !!normalizedHeaders["cookie"];
const missingHeaders = [];
if (!hasSecChUa && hasChrome) missingHeaders.push("sec-ch-ua");
if (!hasSecFetch) missingHeaders.push("sec-fetch");
if (!hasAcceptLanguage) missingHeaders.push("accept-language");
if (!hasCookies) missingHeaders.push("cookies");
if (missingHeaders.length >= 2) {
confidence = Math.max(confidence, 0.85);
reasons.push("browser_ua_missing_headers");
if (!detectedAgent && hasChrome && !hasSecChUa) {
detectedAgent = { type: "perplexity", name: "Perplexity" };
}
}
}
const aiHeaders = [
"openai-conversation-id",
"anthropic-client-id",
"x-goog-api-client"
];
const foundAiHeaders = aiHeaders.filter((h) => normalizedHeaders[h]);
if (foundAiHeaders.length > 0) {
confidence = Math.max(confidence, 0.6);
reasons.push(`ai_headers:${foundAiHeaders.length}`);
}
return {
isAgent: confidence > 0.3,
confidence,
detectedAgent,
reasons,
verificationMethod: "pattern",
timestamp: /* @__PURE__ */ new Date(),
confidenceLevel: confidence >= 0.8 ? "high" : confidence >= 0.5 ? "medium" : "low"
};
}
};
// src/storage/memory-adapter.ts
var MemoryStorageAdapter = class {
events = /* @__PURE__ */ new Map();
sessions = /* @__PURE__ */ new Map();
eventTimeline = [];
maxEvents = 1e3;
maxSessions = 100;
async storeEvent(event) {
const eventKey = `${event.timestamp}:${event.eventId}`;
this.events.set(eventKey, event);
this.eventTimeline.push({
timestamp: Date.parse(event.timestamp),
eventId: eventKey
});
this.eventTimeline.sort((a, b) => b.timestamp - a.timestamp);
if (this.eventTimeline.length > this.maxEvents) {
const removed = this.eventTimeline.splice(this.maxEvents);
removed.forEach((item) => this.events.delete(item.eventId));
}
}
async getRecentEvents(limit = 100) {
const recent = this.eventTimeline.slice(0, limit);
return recent.map((item) => this.events.get(item.eventId)).filter((event) => event !== void 0);
}
async getSessionEvents(sessionId) {
const events = [];
for (const event of this.events.values()) {
if (event.sessionId === sessionId) {
events.push(event);
}
}
return events.sort(
(a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp)
);
}
async storeSession(session) {
this.sessions.set(session.sessionId, session);
if (this.sessions.size > this.maxSessions) {
const sortedSessions = Array.from(this.sessions.entries()).sort((a, b) => Date.parse(b[1].lastSeen) - Date.parse(a[1].lastSeen));
const toRemove = sortedSessions.slice(this.maxSessions);
toRemove.forEach(([id]) => this.sessions.delete(id));
}
}
async getSession(sessionId) {
return this.sessions.get(sessionId) || null;
}
async getRecentSessions(limit = 10) {
const sorted = Array.from(this.sessions.values()).sort((a, b) => Date.parse(b.lastSeen) - Date.parse(a.lastSeen));
return sorted.slice(0, limit);
}
async cleanup(olderThan) {
const cutoff = olderThan.getTime();
this.eventTimeline = this.eventTimeline.filter((item) => {
if (item.timestamp < cutoff) {
this.events.delete(item.eventId);
return false;
}
return true;
});
for (const [id, session] of this.sessions.entries()) {
if (Date.parse(session.lastSeen) < cutoff) {
this.sessions.delete(id);
}
}
}
};
// src/storage/redis-adapter.ts
var RedisStorageAdapter = class {
redis;
ttl;
keyPrefix = "agent-shield";
constructor(redis, ttl = 86400) {
this.redis = redis;
this.ttl = ttl;
}
eventKey(timestamp, eventId) {
return `${this.keyPrefix}:events:${timestamp}:${eventId}`;
}
sessionKey(sessionId) {
return `${this.keyPrefix}:sessions:${sessionId}`;
}
timelineKey() {
return `${this.keyPrefix}:events:timeline`;
}
async storeEvent(event) {
const key = this.eventKey(event.timestamp, event.eventId);
await this.redis.setex(key, this.ttl, JSON.stringify(event));
await this.redis.zadd(this.timelineKey(), {
score: Date.parse(event.timestamp),
member: key
});
const count = await this.redis.zcard(this.timelineKey());
if (count && count > 1e3) {
await this.redis.zremrangebyrank(this.timelineKey(), 0, -1001);
}
}
async getRecentEvents(limit = 100) {
const keys = await this.redis.zrevrange(this.timelineKey(), 0, limit - 1);
if (!keys || keys.length === 0) {
return [];
}
const events = [];
for (const key of keys) {
const data = await this.redis.get(key);
if (data) {
try {
const event = typeof data === "string" ? JSON.parse(data) : data;
events.push(event);
} catch (e) {
console.error(`Failed to parse event ${key}:`, e);
}
}
}
return events;
}
async getSessionEvents(sessionId) {
const keys = await this.redis.zrevrange(this.timelineKey(), 0, -1);
if (!keys || keys.length === 0) {
return [];
}
const events = [];
for (const key of keys) {
const data = await this.redis.get(key);
if (data) {
try {
const event = typeof data === "string" ? JSON.parse(data) : data;
if (event.sessionId === sessionId) {
events.push(event);
}
} catch (e) {
console.error(`Failed to parse event ${key}:`, e);
}
}
}
return events;
}
async storeSession(session) {
const key = this.sessionKey(session.sessionId);
const existing = await this.redis.get(key);
if (existing) {
const existingSession = typeof existing === "string" ? JSON.parse(existing) : existing;
const methods = /* @__PURE__ */ new Set([
...existingSession.verificationMethods || [],
...session.verificationMethods
]);
const updatedSession = {
...existingSession,
lastSeen: session.lastSeen,
eventCount: session.eventCount,
paths: Array.from(/* @__PURE__ */ new Set([...existingSession.paths, ...session.paths])),
averageConfidence: session.averageConfidence,
verificationMethods: Array.from(methods)
};
await this.redis.setex(key, this.ttl, JSON.stringify(updatedSession));
} else {
await this.redis.setex(key, this.ttl, JSON.stringify(session));
}
}
async getSession(sessionId) {
const key = this.sessionKey(sessionId);
const data = await this.redis.get(key);
if (!data) {
return null;
}
try {
return typeof data === "string" ? JSON.parse(data) : data;
} catch (e) {
console.error(`Failed to parse session ${sessionId}:`, e);
return null;
}
}
async getRecentSessions(limit = 10) {
const pattern = `${this.keyPrefix}:sessions:*`;
const sessions = [];
let cursor = 0;
do {
const [nextCursor, keys] = await this.redis.scan(cursor, {
match: pattern,
count: 100
});
cursor = parseInt(nextCursor);
for (const key of keys) {
const data = await this.redis.get(key);
if (data) {
try {
const session = typeof data === "string" ? JSON.parse(data) : data;
sessions.push(session);
} catch (e) {
console.error(`Failed to parse session from ${key}:`, e);
}
}
}
} while (cursor !== 0 && sessions.length < limit * 2);
return sessions.sort((a, b) => Date.parse(b.lastSeen) - Date.parse(a.lastSeen)).slice(0, limit);
}
async cleanup(olderThan) {
const cutoff = olderThan.getTime();
await this.redis.zremrangebyrank(
this.timelineKey(),
0,
Math.floor(cutoff / 1e3)
);
}
};
// src/storage/index.ts
async function createStorageAdapter(config) {
if (!config || config.type === "memory") {
return new MemoryStorageAdapter();
}
if (config.type === "custom" && config.custom) {
return config.custom;
}
if (config.type === "redis" && config.redis) {
try {
const { Redis } = await import('@upstash/redis');
const redis = new Redis({
url: config.redis.url,
token: config.redis.token
});
return new RedisStorageAdapter(redis, config.ttl);
} catch (error) {
console.warn("[AgentShield] Failed to initialize Redis storage, falli