UNPKG

@kya-os/agentshield-nextjs

Version:

Next.js middleware for AgentShield AI agent detection

1,486 lines (1,471 loc) 60.4 kB
'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