UNPKG

@kya-os/agentshield-nextjs

Version:

Next.js middleware for AgentShield AI agent detection

371 lines (368 loc) 12.1 kB
'use strict'; // 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; } }; exports.EdgeAgentDetectorWrapper = EdgeAgentDetectorWrapper; //# sourceMappingURL=edge-detector-wrapper.js.map //# sourceMappingURL=edge-detector-wrapper.js.map