@kya-os/agentshield-nextjs
Version:
Next.js middleware for AgentShield AI agent detection
1,051 lines (1,041 loc) • 38.7 kB
JavaScript
import { loadRulesSync, mapVerificationMethod } from '@kya-os/agentshield-shared';
import { NextResponse } from '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-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 >= 85 && confidence < 100;
}
function getWasmConfidenceBoost(baseConfidence, reasons = []) {
if (reasons.some(
(r) => r.includes("signature_agent") && !r.includes("signature_headers_present")
)) {
return 100;
}
if (baseConfidence >= 85) {
return 95;
}
if (baseConfidence >= 70) {
return Math.min(baseConfidence * 1.1, 90);
}
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/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) => {
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 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 rules, EdgeAgentDetectorWithWasm, EdgeAgentDetectorWrapperWithWasm;
var init_edge_detector_with_wasm = __esm({
"src/edge-detector-with-wasm.ts"() {
init_wasm_loader();
rules = loadRulesSync();
EdgeAgentDetectorWithWasm = class {
constructor(enableWasm = true) {
this.enableWasm = enableWasm;
this.rules = rules;
}
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 };
}
};
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/edge-safe-detector.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/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_PATTERNS) {
if (pattern.test(userAgent)) {
confidence = 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, 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, 60);
reasons.push(`ai_headers:${foundAiHeaders.length}`);
}
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,
reasons,
verificationMethod: "pattern",
timestamp: /* @__PURE__ */ new Date(),
confidenceLevel: confidence >= 80 ? "high" : confidence >= 50 ? "medium" : "low"
// Updated to 0-100 scale
};
}
};
// 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 redisModuleName = "@upstash/redis";
const redisModule = await import(
/* webpackIgnore: true */
redisModuleName
);
const Redis = redisModule.Redis;
const redis = new Redis({
url: config.redis.url,
token: config.redis.token
});
return new RedisStorageAdapter(redis, config.ttl);
} catch (error) {
console.warn(
"[AgentShield] Redis storage requires @upstash/redis package. Install with: npm install @upstash/redis",
"\nFalling back to memory storage."
);
return new MemoryStorageAdapter();
}
}
return new MemoryStorageAdapter();
}
// src/enhanced-middleware.ts
var SessionManager = class {
sessionLastActivity = /* @__PURE__ */ new Map();
generateSessionId(ipAddress, userAgent) {
const now = Date.now();
const timeWindow = Math.floor(now / (5 * 60 * 1e3));
const baseKey = `${ipAddress || "unknown"}:${userAgent || "unknown"}`;
const windowKey = `${baseKey}:${timeWindow}`;
const lastActivity = this.sessionLastActivity.get(windowKey);
const shouldCreateNewSession = !lastActivity || now - lastActivity > 3e4;
this.sessionLastActivity.set(windowKey, now);
if (this.sessionLastActivity.size > 100) {
const cutoff = now - 6e5;
for (const [key, time] of this.sessionLastActivity.entries()) {
if (time < cutoff) {
this.sessionLastActivity.delete(key);
}
}
}
const data = shouldCreateNewSession ? `${windowKey}:${now}` : `${windowKey}:${lastActivity}`;
let hash = 0;
for (let i = 0; i < data.length; i++) {
const char = data.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16).padStart(12, "0").substring(0, 12);
}
};
function createEnhancedAgentShieldMiddleware(config = {}) {
let storageAdapter = null;
let storageInitPromise = null;
const getStorage = async () => {
if (storageAdapter) return storageAdapter;
if (storageInitPromise) return storageInitPromise;
storageInitPromise = createStorageAdapter(config.storage).then((adapter) => {
storageAdapter = adapter;
return adapter;
});
return storageInitPromise;
};
let detector = null;
let detectorInitPromise = null;
let wasmConfidenceUtils = null;
const getDetector = async (requestUrl) => {
if (detector) {
return detector;
}
if (detectorInitPromise) {
await detectorInitPromise;
return detector;
}
detectorInitPromise = (async () => {
const isEdgeRuntime = typeof globalThis.EdgeRuntime !== "undefined" || process.env.NEXT_RUNTIME === "edge";
if (isEdgeRuntime) {
if (process.env.NODE_ENV !== "production") {
console.debug("[AgentShield] Edge Runtime detected - using pattern detection");
}
detector = new EdgeSafeDetector();
} else {
try {
try {
const wasmUtils = await Promise.resolve().then(() => (init_wasm_confidence(), wasm_confidence_exports));
wasmConfidenceUtils = wasmUtils;
const wasmAvailable = await wasmUtils.checkWasmAvailability();
if (wasmAvailable) {
const { EdgeAgentDetectorWrapperWithWasm: EdgeAgentDetectorWrapperWithWasm2 } = await Promise.resolve().then(() => (init_edge_detector_with_wasm(), edge_detector_with_wasm_exports));
if (process.env.NODE_ENV !== "production") {
console.debug(
"[AgentShield] \u2705 WASM support detected - enhanced detection enabled"
);
}
detector = new EdgeAgentDetectorWrapperWithWasm2({ enableWasm: true });
if (requestUrl && "setBaseUrl" in detector) {
detector.setBaseUrl(requestUrl);
}
} else {
if (process.env.NODE_ENV !== "production") {
console.debug(
"[AgentShield] \u2139\uFE0F Using pattern-based detection (WASM not available)"
);
}
detector = new EdgeSafeDetector();
}
} catch (wasmError) {
if (process.env.NODE_ENV !== "production") {
console.debug("[AgentShield] WASM utilities not available, using pattern detection");
}
detector = new EdgeSafeDetector();
}
} catch (error) {
console.warn("[AgentShield] Failed to initialize enhanced detector, using fallback");
detector = new EdgeSafeDetector();
}
}
})();
await detectorInitPromise;
return detector;
};
const sessionManager = new SessionManager();
const sessionTrackingEnabled = config.sessionTracking?.enabled !== false;
return async (request) => {
const { pathname } = request.nextUrl;
if (config.skipPaths?.some((path) => pathname.startsWith(path))) {
return NextResponse.next();
}
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,
method: request.method,
timestamp: /* @__PURE__ */ new Date()
};
const activeDetector = await getDetector(request.url);
const result = await activeDetector.analyze(context);
let finalConfidence = result.confidence;
let verificationMethod = result.verificationMethod || "pattern";
if (result.isAgent && wasmConfidenceUtils) {
const reasons = result.reasons || [];
if (wasmConfidenceUtils.shouldIndicateWasmVerification(result.confidence)) {
finalConfidence = wasmConfidenceUtils.getWasmConfidenceBoost(result.confidence, reasons);
verificationMethod = wasmConfidenceUtils.getVerificationMethod(finalConfidence, reasons);
}
}
if (result.isAgent && finalConfidence >= (config.confidenceThreshold ?? 0.7)) {
if (sessionTrackingEnabled) {
const storage = await getStorage();
const sessionId = sessionManager.generateSessionId(
ipAddress || void 0,
userAgent || void 0
);
const event = {
eventId: `agent_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
sessionId,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
agentType: result.detectedAgent?.type || "unknown",
agentName: result.detectedAgent?.name || "Unknown",
confidence: finalConfidence,
path: pathWithQuery,
...userAgent && { userAgent },
...ipAddress && { ipAddress },
method: request.method,
detectionReasons: result.reasons || [],
verificationMethod,
detectionDetails: {
patterns: result.detectedAgent?.patterns,
behaviors: result.detectedAgent?.behaviors,
fingerprintMatches: result.detectedAgent?.fingerprints
}
};
try {
await storage.storeEvent(event);
let session = await storage.getSession(sessionId);
if (session) {
session.lastSeen = event.timestamp;
session.eventCount++;
if (!session.paths.includes(pathWithQuery)) {
session.paths.push(pathWithQuery);
}
session.averageConfidence = (session.averageConfidence * (session.eventCount - 1) + finalConfidence) / session.eventCount;
if (!session.verificationMethods.includes(verificationMethod)) {
session.verificationMethods.push(verificationMethod);
}
} else {
session = {
sessionId,
...ipAddress && { ipAddress },
...userAgent && { userAgent },
agentType: result.detectedAgent?.type || "unknown",
agentName: result.detectedAgent?.name || "Unknown",
firstSeen: event.timestamp,
lastSeen: event.timestamp,
eventCount: 1,
paths: [pathWithQuery],
averageConfidence: finalConfidence,
verificationMethods: [verificationMethod]
};
}
if (session) {
await storage.storeSession(session);
}
} catch (error) {
console.error("[AgentShield] Failed to store event:", error);
}
}
if (config.onDetection) {
await config.onDetection(result, context);
}
switch (config.onAgentDetected) {
case "block": {
const { status = 403, message = "Access denied: AI agent detected" } = config.blockedResponse || {};
const response2 = NextResponse.json(
{ error: message, detected: true, confidence: finalConfidence },
{ status }
);
response2.headers.set("x-agentshield-detected", "true");
response2.headers.set(
"x-agentshield-confidence",
String(Math.round(finalConfidence * 100))
);
response2.headers.set("x-agentshield-agent", result.detectedAgent?.name || "Unknown");
response2.headers.set("x-agentshield-verification", verificationMethod);
return response2;
}
case "log": {
const isInteresting = finalConfidence >= 0.9 || result.detectedAgent?.name?.toLowerCase().includes("chatgpt") || result.detectedAgent?.name?.toLowerCase().includes("perplexity") || verificationMethod === "signature";
if (isInteresting && process.env.NODE_ENV !== "production") {
console.debug(`[AgentShield] \u{1F916} AI Agent detected (${verificationMethod}):`, {
agent: result.detectedAgent?.name,
confidence: `${(finalConfidence * 100).toFixed(0)}%`,
path: pathWithQuery,
verification: verificationMethod
});
}
break;
}
}
}
const response = NextResponse.next();
if (result.isAgent) {
response.headers.set("x-agentshield-detected", "true");
response.headers.set("x-agentshield-confidence", String(Math.round(finalConfidence * 100)));
response.headers.set("x-agentshield-agent", result.detectedAgent?.name || "Unknown");
response.headers.set("x-agentshield-verification", verificationMethod);
if (finalConfidence > 0.9) {
response.headers.set("x-ai-visitor", "true");
response.headers.set("x-ai-confidence", finalConfidence.toString());
response.headers.set("x-ai-verification", verificationMethod);
}
}
return response;
};
}
export { createEnhancedAgentShieldMiddleware };
//# sourceMappingURL=enhanced-middleware.mjs.map
//# sourceMappingURL=enhanced-middleware.mjs.map