@kya-os/agentshield-nextjs
Version:
Next.js middleware for AgentShield AI agent detection
1,280 lines (1,274 loc) • 43.4 kB
JavaScript
import { NextResponse } from 'next/server';
import * as ed25519 from '@noble/ed25519';
import { sha512 } from '@noble/hashes/sha2.js';
import { loadRulesSync, mapVerificationMethod } from '@kya-os/agentshield-shared';
// src/create-middleware.ts
ed25519.etc.sha512Sync = (...m) => sha512(ed25519.etc.concatBytes(...m));
var KNOWN_KEYS = {
chatgpt: [
{
kid: "otMqcjr17mGyruktGvJU8oojQTSMHlVm7uO-lrcqbdg",
// ChatGPT's current Ed25519 public key (base64)
// Source: https://chatgpt.com/.well-known/http-message-signatures-directory
publicKey: "7F_3jDlxaquwh291MiACkcS3Opq88NksyHiakzS-Y1g",
validFrom: 1735689600,
// Jan 1, 2025 (nbf from OpenAI)
validUntil: 1769029093
// Jan 21, 2026 (exp from OpenAI)
}
]
};
var keyCache = /* @__PURE__ */ new Map();
var CACHE_TTL_MS = 5 * 60 * 1e3;
var CACHE_MAX_SIZE = 100;
function getApiBaseUrl() {
if (typeof window !== "undefined") {
return "/api/internal";
}
const baseUrl2 = process.env.NEXT_PUBLIC_APP_URL || process.env.NEXT_PUBLIC_API_URL || process.env.API_URL || (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : null);
if (baseUrl2) {
return baseUrl2.replace(/\/$/, "") + "/api/internal";
}
if (process.env.NODE_ENV !== "production") {
console.warn(
"[Signature] No base URL configured for server-side fetch. Using localhost fallback."
);
return "http://localhost:3000/api/internal";
}
console.error(
"[Signature] CRITICAL: No base URL configured for server-side fetch in production!"
);
return "/api/internal";
}
function cleanupExpiredCache() {
const now = Date.now();
const entriesToDelete = [];
for (const [agent, cached] of keyCache.entries()) {
if (now - cached.cachedAt > CACHE_TTL_MS) {
entriesToDelete.push(agent);
}
}
for (const agent of entriesToDelete) {
keyCache.delete(agent);
}
if (keyCache.size > CACHE_MAX_SIZE) {
const entries = Array.from(keyCache.entries()).map(([agent, cached]) => ({
agent,
cachedAt: cached.cachedAt
}));
entries.sort((a, b) => a.cachedAt - b.cachedAt);
const toRemove = entries.slice(0, keyCache.size - CACHE_MAX_SIZE);
for (const entry of toRemove) {
keyCache.delete(entry.agent);
}
}
}
async function fetchKeysFromApi(agent) {
if (keyCache.size > CACHE_MAX_SIZE) {
cleanupExpiredCache();
}
const cached = keyCache.get(agent);
if (cached && Date.now() - cached.cachedAt < CACHE_TTL_MS) {
return cached.keys;
}
if (typeof fetch === "undefined") {
console.warn("[Signature] fetch() not available in this environment");
return null;
}
try {
const apiBaseUrl = getApiBaseUrl();
const url = `${apiBaseUrl}/signature-keys?agent=${encodeURIComponent(agent)}`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json"
},
// 5 second timeout
signal: AbortSignal.timeout(5e3)
});
if (!response.ok) {
console.warn(`[Signature] Failed to fetch keys from API: ${response.status}`);
return null;
}
const data = await response.json();
if (!data.keys || !Array.isArray(data.keys) || data.keys.length === 0) {
console.warn(`[Signature] No keys returned from API for agent: ${agent}`);
return null;
}
keyCache.set(agent, {
keys: data.keys,
cachedAt: Date.now()
});
return data.keys;
} catch (error) {
console.warn("[Signature] Error fetching keys from API, using fallback", {
error: error instanceof Error ? error.message : "Unknown error",
agent
});
return null;
}
}
function isValidAgent(agent) {
return agent in KNOWN_KEYS;
}
async function getKeysForAgent(agent) {
const apiKeys = await fetchKeysFromApi(agent);
if (apiKeys && apiKeys.length > 0) {
return apiKeys;
}
if (isValidAgent(agent)) {
return KNOWN_KEYS[agent];
}
return [];
}
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");
}
function base64ToBytes(base64) {
let standardBase64 = base64.replace(/-/g, "+").replace(/_/g, "/");
const padding = standardBase64.length % 4;
if (padding) {
standardBase64 += "=".repeat(4 - padding);
}
const binaryString = atob(standardBase64);
return Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
}
async function verifyEd25519Signature(publicKeyBase64, signatureBase64, message) {
try {
const publicKeyBytes = base64ToBytes(publicKeyBase64);
const signatureBytes = base64ToBytes(signatureBase64);
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;
}
return ed25519.verify(signatureBytes, messageBytes, publicKeyBytes);
} catch (nobleError) {
console.warn("[Signature] @noble/ed25519 failed, trying Web Crypto fallback:", nobleError);
try {
const publicKeyBytes = base64ToBytes(publicKeyBase64);
const signatureBytes = base64ToBytes(signatureBase64);
const messageBytes = new TextEncoder().encode(message);
const publicKey = await crypto.subtle.importKey(
"raw",
publicKeyBytes.buffer,
{
name: "Ed25519",
namedCurve: "Ed25519"
},
false,
["verify"]
);
return await crypto.subtle.verify(
"Ed25519",
publicKey,
signatureBytes.buffer,
messageBytes
);
} catch (cryptoError) {
console.error("[Signature] Both @noble/ed25519 and Web Crypto failed:", {
nobleError: nobleError instanceof Error ? nobleError.message : "Unknown",
cryptoError: cryptoError instanceof Error ? cryptoError.message : "Unknown"
});
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 agentKey;
if (signatureAgent === '"https://chatgpt.com"' || signatureAgent?.includes("chatgpt.com")) {
agent = "ChatGPT";
agentKey = "chatgpt";
}
if (!agent || !agentKey) {
return {
isValid: false,
confidence: 0,
reason: "Unknown signature agent",
verificationMethod: "none"
};
}
const knownKeys = await getKeysForAgent(agentKey);
if (knownKeys.length === 0) {
return {
isValid: false,
confidence: 0,
reason: "No keys available for 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"];
if (!signatureAgent) {
return false;
}
const agentUrlStr = signatureAgent.replace(/^"+|"+$/g, "");
if (agentUrlStr === "https://chatgpt.com") {
return true;
}
try {
const agentUrl = new URL(agentUrlStr);
const allowedHosts = ["chatgpt.com", "www.chatgpt.com"];
return allowedHosts.includes(agentUrl.host);
} catch {
return false;
}
}
var rules = loadRulesSync();
var EdgeAgentDetector = class {
rules;
constructor() {
this.rules = rules;
}
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 * 100;
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 {
console.warn("[EdgeAgentDetector] Signature verification failed:", {
reason: signatureResult.reason,
agent: signatureResult.agent,
hasSignatureAgent: !!headers["signature-agent"] || !!headers["Signature-Agent"],
signatureAgentValue: headers["signature-agent"] || headers["Signature-Agent"]
});
confidence = Math.max(confidence, 30);
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, 20);
reasons.push("signature_verification_error");
}
}
const userAgent = input.userAgent || input.headers?.["user-agent"] || "";
if (userAgent) {
const userAgentEntries = Object.entries(this.rules.rules.userAgents);
const genericKeys = ["generic_bot", "dev_tools", "automation_tools"];
const sortedEntries = userAgentEntries.sort((a, b) => {
const aIsGeneric = genericKeys.includes(a[0]);
const bIsGeneric = genericKeys.includes(b[0]);
if (aIsGeneric && !bIsGeneric) return 1;
if (!aIsGeneric && bIsGeneric) return -1;
return 0;
});
for (const [agentKey, agentRule] of sortedEntries) {
const rule = agentRule;
const matched = rule.patterns.some((pattern) => {
const regex = new RegExp(pattern, "i");
return regex.test(userAgent);
});
if (matched) {
const agentType = this.getAgentType(agentKey);
const agentName = this.getAgentName(agentKey);
confidence = Math.max(confidence, rule.confidence * 100);
reasons.push(`known_pattern:${agentType}`);
if (!detectedAgent) {
detectedAgent = { type: agentType, name: agentName };
verificationMethod = "pattern";
}
break;
}
}
}
const suspiciousHeaders = this.rules.rules.headers.suspicious;
const foundAiHeaders = suspiciousHeaders.filter(
(headerRule) => normalizedHeaders[headerRule.name.toLowerCase()]
);
if (foundAiHeaders.length > 0) {
const maxConfidence = Math.max(...foundAiHeaders.map((h) => h.confidence * 100));
confidence = Math.max(confidence, maxConfidence);
reasons.push(`ai_headers:${foundAiHeaders.length}`);
}
const ip = input.ip || input.ipAddress;
if (ip && !normalizedHeaders["x-forwarded-for"] && !normalizedHeaders["x-real-ip"]) {
const ipRanges = "providers" in this.rules.rules.ipRanges ? this.rules.rules.ipRanges.providers : this.rules.rules.ipRanges;
for (const [provider, ipRule] of Object.entries(ipRanges)) {
if (!ipRule || typeof ipRule !== "object" || !("ranges" in ipRule) || !Array.isArray(ipRule.ranges))
continue;
const matched = ipRule.ranges.some((range) => {
const prefix = range.split("/")[0];
const prefixParts = prefix.split(".");
const ipParts = ip.split(".");
for (let i = 0; i < Math.min(prefixParts.length - 1, 2); i++) {
if (prefixParts[i] !== ipParts[i] && prefixParts[i] !== "0") {
return false;
}
}
return true;
});
if (matched) {
const rule = ipRule;
confidence = Math.max(confidence, rule.confidence * 40);
reasons.push(`cloud_provider:${provider}`);
break;
}
}
}
if (reasons.length > 2 && confidence < 100) {
confidence = Math.min(confidence * 1.2, 95);
}
confidence = Math.min(Math.max(confidence, 0), 100);
return {
isAgent: confidence > 30,
// Updated to 0-100 scale (was 0.3)
confidence,
detectionClass: confidence > 30 && detectedAgent ? { type: "AiAgent", agentType: detectedAgent.name } : confidence > 30 ? { type: "Unknown" } : { type: "Human" },
signals: [],
// Will be populated by enhanced detection engine in future tasks
...detectedAgent && { detectedAgent },
reasons,
...verificationMethod && {
verificationMethod
},
forgeabilityRisk: verificationMethod === "signature" ? "low" : confidence > 80 ? "medium" : "high",
// Updated to 0-100 scale
timestamp: /* @__PURE__ */ new Date()
};
}
/**
* Get agent type from rule key
*/
getAgentType(agentKey) {
const typeMap = {
openai_gptbot: "openai",
anthropic_claude: "anthropic",
perplexity_bot: "perplexity",
google_ai: "google",
microsoft_ai: "microsoft",
meta_ai: "meta",
cohere_bot: "cohere",
huggingface_bot: "huggingface",
generic_bot: "generic",
dev_tools: "dev",
automation_tools: "automation"
};
return typeMap[agentKey] || agentKey;
}
/**
* Get agent name from rule key
*/
getAgentName(agentKey) {
const nameMap = {
openai_gptbot: "ChatGPT/GPTBot",
anthropic_claude: "Claude",
perplexity_bot: "Perplexity",
google_ai: "Google AI",
microsoft_ai: "Microsoft Copilot",
meta_ai: "Meta AI",
cohere_bot: "Cohere",
huggingface_bot: "HuggingFace",
generic_bot: "Generic Bot",
dev_tools: "Development Tool",
automation_tools: "Automation Tool"
};
return nameMap[agentKey] || agentKey;
}
};
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/wasm-loader.ts
var wasmInstance = null;
var wasmExports = null;
var initPromise = null;
var WASM_PATH = "/wasm/agentshield_wasm_bg.wasm";
var baseUrl = null;
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) => {
if (process.env.NODE_ENV !== "production") {
console.debug("WASM:", ptr, len);
}
},
__wbindgen_throw: (ptr, len) => {
throw new Error(`WASM Error at ${ptr}, length ${len}`);
}
}
});
wasmInstance = instance;
wasmExports = instance.exports;
if (process.env.NODE_ENV !== "production") {
console.debug("[AgentShield] \u2705 WASM module initialized with streaming");
}
return;
} catch (streamError) {
if (!controller.signal.aborted) {
if (process.env.NODE_ENV !== "production") {
console.debug(
"[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) => {
if (process.env.NODE_ENV !== "production") {
console.debug("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;
if (process.env.NODE_ENV !== "production") {
console.debug("[AgentShield] \u2705 WASM module initialized via fallback");
}
} catch (fetchError) {
const error = fetchError;
if (error.name === "AbortError") {
console.warn(
"[AgentShield] WASM fetch timed out after 3 seconds - using pattern detection"
);
} else {
console.warn(
"[AgentShield] Failed to fetch WASM file:",
error.message || "Unknown error"
);
}
wasmExports = null;
}
} catch (error) {
console.error("[AgentShield] Failed to initialize WASM:", error);
wasmExports = null;
}
})();
await initPromise;
return !!wasmExports;
}
async function detectAgentWithWasm(_userAgent, _headers, _ipAddress) {
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 rules2 = loadRulesSync();
var EdgeAgentDetectorWithWasm = class {
constructor(enableWasm = true) {
this.enableWasm = enableWasm;
this.rules = rules2;
}
wasmEnabled = false;
initPromise = null;
baseUrl = null;
rules;
/**
* 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();
if (wasmAvailable) {
if (this.baseUrl) {
setWasmBaseUrl(this.baseUrl);
}
await initWasm();
this.wasmEnabled = true;
} else {
this.wasmEnabled = false;
}
} 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 = 85;
reasons.push("signature_agent:chatgpt");
detectedAgent = { type: "chatgpt", name: "ChatGPT" };
verificationMethod = "signature";
} else if (signaturePresent) {
confidence = Math.max(confidence, 40);
reasons.push("signature_present");
}
const userAgent = input.userAgent || input.headers?.["user-agent"] || "";
if (userAgent) {
for (const [agentKey, agentRule] of Object.entries(this.rules.rules.userAgents)) {
const matched = agentRule.patterns.some((pattern) => {
const regex = new RegExp(pattern, "i");
return regex.test(userAgent);
});
if (matched) {
const agentType = this.getAgentType(agentKey);
const agentName = this.getAgentName(agentKey);
confidence = Math.max(confidence, Math.round(agentRule.confidence * 0.85 * 100));
reasons.push(`known_pattern:${agentType}`);
if (!detectedAgent) {
detectedAgent = { type: agentType, name: agentName };
verificationMethod = "pattern";
}
break;
}
}
}
const suspiciousHeaders = this.rules.rules.headers.suspicious;
const foundAiHeaders = suspiciousHeaders.filter(
(headerRule) => normalizedHeaders[headerRule.name.toLowerCase()]
);
if (foundAiHeaders.length > 0) {
const maxConfidence = Math.max(...foundAiHeaders.map((h) => h.confidence));
confidence = Math.max(confidence, maxConfidence);
reasons.push(`ai_headers:${foundAiHeaders.length}`);
}
const ip = input.ip || input.ipAddress;
if (ip && !normalizedHeaders["x-forwarded-for"] && !normalizedHeaders["x-real-ip"]) {
const ipRanges = "providers" in this.rules.rules.ipRanges ? this.rules.rules.ipRanges.providers : this.rules.rules.ipRanges;
for (const [provider, ipRule] of Object.entries(ipRanges)) {
if (!ipRule || typeof ipRule !== "object" || !("ranges" in ipRule) || !Array.isArray(ipRule.ranges))
continue;
const matched = ipRule.ranges.some((range) => {
const prefix = range.split("/")[0];
const prefixParts = prefix.split(".");
const ipParts = ip.split(".");
for (let i = 0; i < Math.min(prefixParts.length - 1, 2); i++) {
if (prefixParts[i] !== ipParts[i] && prefixParts[i] !== "0") {
return false;
}
}
return true;
});
if (matched) {
confidence = Math.max(confidence, Math.round(ipRule.confidence * 0.4 * 100));
reasons.push(`cloud_provider:${provider}`);
break;
}
}
}
if (reasons.length > 2) {
confidence = Math.min(Math.round(confidence * 1.2), 95);
}
confidence = Math.min(Math.max(confidence, 0), 100);
return {
isAgent: confidence > 30,
// 30% threshold
confidence,
detectionClass: confidence > 30 && detectedAgent ? { type: "AiAgent", agentType: detectedAgent.name } : confidence > 30 ? { type: "Unknown" } : { type: "Human" },
signals: [],
// Will be populated by enhanced detection engine in future tasks
...detectedAgent && { detectedAgent },
reasons,
...verificationMethod && {
verificationMethod: mapVerificationMethod(verificationMethod)
},
forgeabilityRisk: confidence > 80 ? "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) {
const detectedAgent = wasmResult.agent ? this.mapAgentName(wasmResult.agent) : void 0;
return {
isAgent: wasmResult.isAgent,
confidence: wasmResult.confidence,
detectionClass: wasmResult.isAgent && detectedAgent ? { type: "AiAgent", agentType: detectedAgent.name } : wasmResult.isAgent ? { type: "Unknown" } : { type: "Human" },
signals: [],
// Will be populated by enhanced detection engine in future tasks
...detectedAgent && { detectedAgent },
reasons: [`wasm:${wasmResult.verificationMethod}`],
verificationMethod: mapVerificationMethod(wasmResult.verificationMethod),
forgeabilityRisk: wasmResult.verificationMethod === "signature" ? "low" : wasmResult.confidence > 90 ? "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 >= 85) {
patternResult.confidence = Math.min(95, patternResult.confidence + 10);
patternResult.reasons.push("wasm_enhanced");
}
return patternResult;
}
/**
* Get agent type from rule key
*/
getAgentType(agentKey) {
const typeMap = {
openai_gptbot: "openai",
anthropic_claude: "anthropic",
perplexity_bot: "perplexity",
google_ai: "google",
microsoft_ai: "microsoft",
meta_ai: "meta",
cohere_bot: "cohere",
huggingface_bot: "huggingface",
generic_bot: "generic",
dev_tools: "dev",
automation_tools: "automation"
};
return typeMap[agentKey] || agentKey;
}
/**
* Get agent name from rule key
*/
getAgentName(agentKey) {
const nameMap = {
openai_gptbot: "ChatGPT/GPTBot",
anthropic_claude: "Claude",
perplexity_bot: "Perplexity",
google_ai: "Google AI",
microsoft_ai: "Microsoft Copilot",
meta_ai: "Meta AI",
cohere_bot: "Cohere",
huggingface_bot: "HuggingFace",
generic_bot: "Generic Bot",
dev_tools: "Development Tool",
automation_tools: "Automation Tool"
};
return nameMap[agentKey] || agentKey;
}
/**
* 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 };
}
};
var 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/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);
}
}
};
// 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]) => {
if (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 NextResponse.next();
}
const existingSession = sessionTracker ? await sessionTracker.check(request) : null;
if (existingSession) {
const response2 = 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,
detectionClass: { type: "AiAgent" },
detectedAgent: {
type: "ai_agent",
name: existingSession.agent
},
timestamp: /* @__PURE__ */ new Date(),
verificationMethod: "behavioral",
reasons: ["Session continued"],
signals: []
},
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 ?? 70)) {
if (onDetection) {
const customResponse = await onDetection(request, result);
if (customResponse) {
return customResponse;
}
}
switch (onAgentDetected) {
case "block": {
const response2 = 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 NextResponse.redirect(new URL(redirectUrl, request.url));
case "rewrite":
return NextResponse.rewrite(new URL(rewriteUrl, request.url));
case "log":
if (process.env.NODE_ENV !== "production") {
console.debug("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 ?? 70)) {
detector.emit("agent.allowed", result, context);
}
break;
}
}
request.agentShield = {
result,
skipped: false
};
let response = 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 ?? 70)) {
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 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) : NextResponse.next();
};
}
export { createAgentShieldMiddleware2 as createAgentShieldMiddleware, createAgentShieldMiddleware2 as createMiddleware };
//# sourceMappingURL=create-middleware.mjs.map
//# sourceMappingURL=create-middleware.mjs.map