UNPKG

@kya-os/agentshield-nextjs

Version:

Next.js middleware for AgentShield AI agent detection

526 lines (448 loc) 15.8 kB
#!/usr/bin/env node /** * AgentShield Edge Runtime WASM Setup Script * * This script helps Next.js users set up WASM support for Edge Runtime (middleware). * It copies necessary files and creates the required configuration. */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); // Colors for console output const colors = { reset: '\x1b[0m', bright: '\x1b[1m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', red: '\x1b[31m', }; function log(message, color = colors.reset) { console.log(`${color}${message}${colors.reset}`); } function error(message) { console.error(`${colors.red}❌ ${message}${colors.reset}`); } function success(message) { log(`✅ ${message}`, colors.green); } function info(message) { log(`ℹ️ ${message}`, colors.blue); } function warning(message) { log(`⚠️ ${message}`, colors.yellow); } async function main() { log('\n🚀 AgentShield Edge Runtime WASM Setup\n', colors.bright); // Step 1: Check if we're in a Next.js project const packageJsonPath = path.join(process.cwd(), 'package.json'); if (!fs.existsSync(packageJsonPath)) { error( 'No package.json found. Please run this command from your Next.js project root.' ); process.exit(1); } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const hasNext = packageJson.dependencies?.next || packageJson.devDependencies?.next; if (!hasNext) { warning('Next.js not detected in package.json. Continuing anyway...'); } // Step 2: Check if AgentShield is installed const agentShieldPath = path.join( process.cwd(), 'node_modules', '@kya-os', 'agentshield-nextjs' ); if (!fs.existsSync(agentShieldPath)) { error('@kya-os/agentshield-nextjs not found in node_modules.'); info('Please install it first:'); console.log(' npm install @kya-os/agentshield-nextjs'); console.log(' # or'); console.log(' pnpm add @kya-os/agentshield-nextjs'); process.exit(1); } // Step 3: Create wasm directory const wasmDir = path.join(process.cwd(), 'wasm'); if (!fs.existsSync(wasmDir)) { fs.mkdirSync(wasmDir, { recursive: true }); success('Created wasm/ directory'); } // Step 4: Copy WASM file const sourceWasm = path.join( agentShieldPath, 'dist', 'wasm', 'agentshield_wasm_bg.wasm' ); const destWasm = path.join(wasmDir, 'agentshield_wasm_bg.wasm'); if (fs.existsSync(sourceWasm)) { fs.copyFileSync(sourceWasm, destWasm); const sizeKB = (fs.statSync(destWasm).size / 1024).toFixed(1); const sizeMB = (fs.statSync(destWasm).size / 1024 / 1024).toFixed(2); const sizeStr = sizeKB < 1024 ? `${sizeKB}KB` : `${sizeMB}MB`; success(`Copied WASM file to wasm/ (${sizeStr})`); } else { // Try alternative location const altSourceWasm = path.join( agentShieldPath, 'wasm', 'agentshield_wasm_bg.wasm' ); if (fs.existsSync(altSourceWasm)) { fs.copyFileSync(altSourceWasm, destWasm); const sizeKB = (fs.statSync(destWasm).size / 1024).toFixed(1); const sizeMB = (fs.statSync(destWasm).size / 1024 / 1024).toFixed(2); const sizeStr = sizeKB < 1024 ? `${sizeKB}KB` : `${sizeMB}MB`; success(`Copied WASM file to wasm/ (${sizeStr})`); } else { error( 'WASM file not found in the package. The package may be corrupted.' ); process.exit(1); } } // Step 5: Create TypeScript definitions const dtsContent = `// Auto-generated by agentshield-setup-edge // TypeScript definitions for AgentShield WASM in Edge Runtime declare module '*.wasm?module' { const wasmModule: WebAssembly.Module; export default wasmModule; } export interface AgentShieldWasm { detect_agent(metadata: string): string; get_version(): string; memory: WebAssembly.Memory; } export interface DetectionResult { isAgent: boolean; confidence: number; agent?: string; verificationMethod: 'cryptographic' | 'pattern'; riskLevel?: 'low' | 'medium' | 'high' | 'critical'; timestamp: string; } `; const dtsPath = path.join(wasmDir, 'agentshield.d.ts'); fs.writeFileSync(dtsPath, dtsContent); success('Created TypeScript definitions'); // Step 6: Create lib directory and Edge loader const libDir = path.join(process.cwd(), 'lib'); if (!fs.existsSync(libDir)) { fs.mkdirSync(libDir, { recursive: true }); } const edgeLoaderContent = `/** * AgentShield Edge Runtime Loader * Auto-generated by agentshield-setup-edge * * This module provides WASM-based AI agent detection for Next.js Edge Runtime. */ import type { NextRequest } from 'next/server'; import wasmModule from '../wasm/agentshield_wasm_bg.wasm?module'; let wasmInstance: WebAssembly.Instance | null = null; let wasmMemory: WebAssembly.Memory | null = null; interface WasmExports { detect_agent(metadataPtr: number, metadataLen: number): number; get_version(): number; __wbindgen_malloc(size: number): number; __wbindgen_free(ptr: number, size: number): void; __wbindgen_realloc(ptr: number, oldSize: number, newSize: number): number; memory: WebAssembly.Memory; } async function initWasm(): Promise<void> { if (wasmInstance) return; wasmMemory = new WebAssembly.Memory({ initial: 17, maximum: 256 }); const imports = { wbg: { __wbindgen_throw: (ptr: number, len: number) => { throw new Error(readString(ptr, len)); }, __wbg_log_: (ptr: number, len: number) => { console.log(readString(ptr, len)); }, }, env: { memory: wasmMemory, }, }; wasmInstance = await WebAssembly.instantiate(wasmModule, imports); } function readString(ptr: number, len: number): string { if (!wasmInstance || !wasmMemory) throw new Error('WASM not initialized'); const memory = new Uint8Array(wasmMemory.buffer); const bytes = memory.slice(ptr, ptr + len); return new TextDecoder().decode(bytes); } function writeString(str: string): [number, number] { if (!wasmInstance) throw new Error('WASM not initialized'); const exports = wasmInstance.exports as unknown as WasmExports; const encoded = new TextEncoder().encode(str); const ptr = exports.__wbindgen_malloc(encoded.length); const memory = new Uint8Array(exports.memory.buffer); memory.set(encoded, ptr); return [ptr, encoded.length]; } export interface DetectionResult { isAgent: boolean; confidence: number; agent?: string; verificationMethod: 'cryptographic' | 'pattern'; riskLevel?: 'low' | 'medium' | 'high' | 'critical'; timestamp?: string; } export interface AgentShieldOptions { enableWasm?: boolean; onAgentDetected?: (result: DetectionResult) => void; blockAgents?: boolean; allowedAgents?: string[]; } /** * Create an AgentShield middleware instance */ export function createAgentShieldMiddleware(options: AgentShieldOptions = {}) { let initialized = false; return { async init(): Promise<void> { if (initialized) return; if (options.enableWasm !== false) { try { await initWasm(); initialized = true; console.log('✅ AgentShield WASM initialized in Edge Runtime'); } catch (error) { console.warn('⚠️ WASM initialization failed, using pattern detection:', error); initialized = true; } } else { initialized = true; } }, async detect(request: NextRequest): Promise<DetectionResult> { const metadata = { userAgent: request.headers.get('user-agent') || '', ipAddress: request.ip || request.headers.get('x-forwarded-for')?.split(',')[0] || '', headers: Object.fromEntries(request.headers.entries()), }; // Try WASM detection first if (wasmInstance && options.enableWasm !== false) { try { const exports = wasmInstance.exports as unknown as WasmExports; const [ptr, len] = writeString(JSON.stringify(metadata)); const resultPtr = exports.detect_agent(ptr, len); const resultLen = 1024; // Assume max result size const resultStr = readString(resultPtr, resultLen); exports.__wbindgen_free(ptr, len); exports.__wbindgen_free(resultPtr, resultLen); const result = JSON.parse(resultStr); const detection: DetectionResult = { ...result, verificationMethod: 'cryptographic', timestamp: new Date().toISOString(), }; if (options.onAgentDetected && detection.isAgent) { options.onAgentDetected(detection); } return detection; } catch (error) { console.error('WASM detection failed:', error); // Fall through to pattern detection } } // Fallback: Pattern-based detection return patternDetection(metadata, options); }, isInitialized(): boolean { return initialized; }, getVerificationMethod(): 'cryptographic' | 'pattern' { return wasmInstance ? 'cryptographic' : 'pattern'; }, }; } /** * Pattern-based detection fallback */ function patternDetection( metadata: { userAgent: string; ipAddress: string; headers: Record<string, string> }, options: AgentShieldOptions ): DetectionResult { const userAgent = metadata.userAgent.toLowerCase(); // Known AI agent patterns with confidence scores const aiAgentPatterns = [ { pattern: /chatgpt-user/i, name: 'ChatGPT', confidence: 0.95 }, { pattern: /claude-web/i, name: 'Claude', confidence: 0.95 }, { pattern: /anthropic/i, name: 'Anthropic', confidence: 0.90 }, { pattern: /openai/i, name: 'OpenAI', confidence: 0.90 }, { pattern: /gpt-crawler/i, name: 'GPT Crawler', confidence: 0.95 }, { pattern: /copilot/i, name: 'GitHub Copilot', confidence: 0.85 }, { pattern: /bard/i, name: 'Google Bard', confidence: 0.85 }, { pattern: /gemini/i, name: 'Google Gemini', confidence: 0.85 }, { pattern: /perplexity/i, name: 'Perplexity', confidence: 0.85 }, ]; // Check headers for AI-specific patterns const suspiciousHeaders = [ 'x-openai-', 'x-anthropic-', 'x-ai-', 'x-llm-', ]; let headerBoost = 0; for (const [key, value] of Object.entries(metadata.headers)) { if (suspiciousHeaders.some(prefix => key.toLowerCase().startsWith(prefix))) { headerBoost = 0.1; break; } } // Check user agent patterns for (const { pattern, name, confidence } of aiAgentPatterns) { if (pattern.test(userAgent)) { const finalConfidence = Math.min(confidence + headerBoost, 1.0); const result: DetectionResult = { isAgent: true, confidence: finalConfidence, agent: name, verificationMethod: 'pattern', riskLevel: finalConfidence > 0.9 ? 'high' : 'medium', timestamp: new Date().toISOString(), }; if (options.onAgentDetected) { options.onAgentDetected(result); } return result; } } // Not detected as AI agent return { isAgent: false, confidence: 0.85, verificationMethod: 'pattern', timestamp: new Date().toISOString(), }; } export default createAgentShieldMiddleware; `; const edgeLoaderPath = path.join(libDir, 'agentshield-edge.ts'); fs.writeFileSync(edgeLoaderPath, edgeLoaderContent); success('Created Edge Runtime loader at lib/agentshield-edge.ts'); // Step 7: Create example middleware if it doesn't exist const middlewarePath = path.join(process.cwd(), 'middleware.ts'); if (!fs.existsSync(middlewarePath)) { const middlewareContent = `import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import { createAgentShieldMiddleware } from './lib/agentshield-edge'; // Initialize AgentShield const agentShield = createAgentShieldMiddleware({ enableWasm: true, onAgentDetected: (result) => { console.log(\`🤖 AI Agent detected: \${result.agent} (confidence: \${(result.confidence * 100).toFixed(1)}%)\`); }, }); export async function middleware(request: NextRequest) { // Initialize WASM on first request await agentShield.init(); // Detect AI agents const detection = await agentShield.detect(request); // Log detection results if (detection.isAgent) { console.log(\`AI Agent detected on \${request.url}:\`, detection); // Optional: Add headers to track AI agent traffic const response = NextResponse.next(); response.headers.set('X-Agent-Detected', detection.agent || 'unknown'); response.headers.set('X-Agent-Confidence', detection.confidence.toString()); return response; // Optional: Block AI agents // if (detection.confidence > 0.9) { // return NextResponse.json( // { error: 'AI agent access denied' }, // { status: 403 } // ); // } } return NextResponse.next(); } export const config = { matcher: [ /* * Match all request paths except for the ones starting with: * - api/admin (protect admin APIs) * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) */ '/((?!_next/static|_next/image|favicon.ico).*)', ], }; `; fs.writeFileSync(middlewarePath, middlewareContent); success('Created example middleware.ts'); } else { info('middleware.ts already exists - skipping creation'); info('See lib/agentshield-edge.ts for integration instructions'); } // Step 8: Update next.config.js if needed const nextConfigPath = path.join(process.cwd(), 'next.config.js'); const nextConfigMjsPath = path.join(process.cwd(), 'next.config.mjs'); const configPath = fs.existsSync(nextConfigPath) ? nextConfigPath : fs.existsSync(nextConfigMjsPath) ? nextConfigMjsPath : null; if (configPath) { const configContent = fs.readFileSync(configPath, 'utf8'); if (!configContent.includes('wasm')) { warning( 'Your next.config.js may need to be updated to handle WASM files.' ); console.log('\nAdd this to your webpack config:\n'); console.log( colors.yellow + ` webpack: (config) => { config.module.rules.push({ test: /\\.wasm$/, type: 'asset/resource', }); return config; },` + colors.reset ); } } // Step 9: Final instructions console.log('\n' + colors.bright + '🎉 Setup Complete!' + colors.reset); console.log( '\n' + colors.green + 'AgentShield Edge Runtime WASM is now configured!' + colors.reset ); console.log('\nNext steps:'); console.log('1. Review the generated files:'); console.log(' - wasm/agentshield_wasm_bg.wasm (WASM binary)'); console.log(' - lib/agentshield-edge.ts (Edge Runtime loader)'); console.log(' - middleware.ts (Example implementation)'); console.log('\n2. Test your setup:'); console.log(' npm run dev'); console.log(' # Visit your site and check the console for detection logs'); console.log('\n3. Deploy to Vercel:'); console.log(' git add .'); console.log(' git commit -m "Add AgentShield WASM for Edge Runtime"'); console.log(' git push'); console.log( '\n' + colors.blue + 'Documentation: https://github.com/kya-os/agentshield#edge-runtime' + colors.reset ); console.log( colors.blue + 'Issues: https://github.com/kya-os/agentshield/issues' + colors.reset ); } // Run the setup main().catch(err => { error('Setup failed: ' + err.message); console.error(err); process.exit(1); });