UNPKG

@kya-os/agentshield-nextjs

Version:

Next.js middleware for AgentShield AI agent detection

588 lines (583 loc) 19.6 kB
'use strict'; var ed25519 = require('@noble/ed25519'); var sha2_js = require('@noble/hashes/sha2.js'); var agentshieldShared = require('@kya-os/agentshield-shared'); function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var ed25519__namespace = /*#__PURE__*/_interopNamespace(ed25519); // src/signature-verifier.ts ed25519__namespace.etc.sha512Sync = (...m) => sha2_js.sha512(ed25519__namespace.etc.concatBytes(...m)); var KNOWN_KEYS = { chatgpt: [ { kid: "otMqcjr17mGyruktGvJU8oojQTSMHlVm7uO-lrcqbdg", // ChatGPT's current Ed25519 public key (base64) // Source: https://chatgpt.com/.well-known/http-message-signatures-directory publicKey: "7F_3jDlxaquwh291MiACkcS3Opq88NksyHiakzS-Y1g", validFrom: 1735689600, // Jan 1, 2025 (nbf from OpenAI) validUntil: 1769029093 // Jan 21, 2026 (exp from OpenAI) } ] }; var keyCache = /* @__PURE__ */ new Map(); var CACHE_TTL_MS = 5 * 60 * 1e3; var CACHE_MAX_SIZE = 100; function getApiBaseUrl() { if (typeof window !== "undefined") { return "/api/internal"; } const baseUrl = process.env.NEXT_PUBLIC_APP_URL || process.env.NEXT_PUBLIC_API_URL || process.env.API_URL || (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : null); if (baseUrl) { return baseUrl.replace(/\/$/, "") + "/api/internal"; } if (process.env.NODE_ENV !== "production") { console.warn( "[Signature] No base URL configured for server-side fetch. Using localhost fallback." ); return "http://localhost:3000/api/internal"; } console.error( "[Signature] CRITICAL: No base URL configured for server-side fetch in production!" ); return "/api/internal"; } function cleanupExpiredCache() { const now = Date.now(); const entriesToDelete = []; for (const [agent, cached] of keyCache.entries()) { if (now - cached.cachedAt > CACHE_TTL_MS) { entriesToDelete.push(agent); } } for (const agent of entriesToDelete) { keyCache.delete(agent); } if (keyCache.size > CACHE_MAX_SIZE) { const entries = Array.from(keyCache.entries()).map(([agent, cached]) => ({ agent, cachedAt: cached.cachedAt })); entries.sort((a, b) => a.cachedAt - b.cachedAt); const toRemove = entries.slice(0, keyCache.size - CACHE_MAX_SIZE); for (const entry of toRemove) { keyCache.delete(entry.agent); } } } async function fetchKeysFromApi(agent) { if (keyCache.size > CACHE_MAX_SIZE) { cleanupExpiredCache(); } const cached = keyCache.get(agent); if (cached && Date.now() - cached.cachedAt < CACHE_TTL_MS) { return cached.keys; } if (typeof fetch === "undefined") { console.warn("[Signature] fetch() not available in this environment"); return null; } try { const apiBaseUrl = getApiBaseUrl(); const url = `${apiBaseUrl}/signature-keys?agent=${encodeURIComponent(agent)}`; const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json" }, // 5 second timeout signal: AbortSignal.timeout(5e3) }); if (!response.ok) { console.warn(`[Signature] Failed to fetch keys from API: ${response.status}`); return null; } const data = await response.json(); if (!data.keys || !Array.isArray(data.keys) || data.keys.length === 0) { console.warn(`[Signature] No keys returned from API for agent: ${agent}`); return null; } keyCache.set(agent, { keys: data.keys, cachedAt: Date.now() }); return data.keys; } catch (error) { console.warn("[Signature] Error fetching keys from API, using fallback", { error: error instanceof Error ? error.message : "Unknown error", agent }); return null; } } function isValidAgent(agent) { return agent in KNOWN_KEYS; } async function getKeysForAgent(agent) { const apiKeys = await fetchKeysFromApi(agent); if (apiKeys && apiKeys.length > 0) { return apiKeys; } if (isValidAgent(agent)) { return KNOWN_KEYS[agent]; } return []; } function parseSignatureInput(signatureInput) { try { const match = signatureInput.match(/sig1=\((.*?)\);(.+)/); if (!match) return null; const [, headersList, params] = match; const signedHeaders = headersList ? headersList.split(" ").map((h) => h.replace(/"/g, "").trim()).filter((h) => h.length > 0) : []; const keyidMatch = params ? params.match(/keyid="([^"]+)"/) : null; const createdMatch = params ? params.match(/created=(\d+)/) : null; const expiresMatch = params ? params.match(/expires=(\d+)/) : null; if (!keyidMatch || !keyidMatch[1]) return null; return { keyid: keyidMatch[1], created: createdMatch && createdMatch[1] ? parseInt(createdMatch[1]) : void 0, expires: expiresMatch && expiresMatch[1] ? parseInt(expiresMatch[1]) : void 0, signedHeaders }; } catch (error) { console.error("[Signature] Failed to parse Signature-Input:", error); return null; } } function buildSignatureBase(method, path, headers, signedHeaders) { const components = []; for (const headerName of signedHeaders) { let value; switch (headerName) { case "@method": value = method.toUpperCase(); break; case "@path": value = path; break; case "@authority": value = headers["host"] || headers["Host"] || ""; break; default: { const key = Object.keys(headers).find((k) => k.toLowerCase() === headerName.toLowerCase()); value = key ? headers[key] || "" : ""; break; } } components.push(`"${headerName}": ${value}`); } return components.join("\n"); } function base64ToBytes(base64) { let standardBase64 = base64.replace(/-/g, "+").replace(/_/g, "/"); const padding = standardBase64.length % 4; if (padding) { standardBase64 += "=".repeat(4 - padding); } const binaryString = atob(standardBase64); return Uint8Array.from(binaryString, (c) => c.charCodeAt(0)); } async function verifyEd25519Signature(publicKeyBase64, signatureBase64, message) { try { const publicKeyBytes = base64ToBytes(publicKeyBase64); const signatureBytes = base64ToBytes(signatureBase64); const messageBytes = new TextEncoder().encode(message); if (publicKeyBytes.length !== 32) { console.error("[Signature] Invalid public key length:", publicKeyBytes.length); return false; } if (signatureBytes.length !== 64) { console.error("[Signature] Invalid signature length:", signatureBytes.length); return false; } return ed25519__namespace.verify(signatureBytes, messageBytes, publicKeyBytes); } catch (nobleError) { console.warn("[Signature] @noble/ed25519 failed, trying Web Crypto fallback:", nobleError); try { const publicKeyBytes = base64ToBytes(publicKeyBase64); const signatureBytes = base64ToBytes(signatureBase64); const messageBytes = new TextEncoder().encode(message); const publicKey = await crypto.subtle.importKey( "raw", publicKeyBytes.buffer, { name: "Ed25519", namedCurve: "Ed25519" }, false, ["verify"] ); return await crypto.subtle.verify( "Ed25519", publicKey, signatureBytes.buffer, messageBytes ); } catch (cryptoError) { console.error("[Signature] Both @noble/ed25519 and Web Crypto failed:", { nobleError: nobleError instanceof Error ? nobleError.message : "Unknown", cryptoError: cryptoError instanceof Error ? cryptoError.message : "Unknown" }); return false; } } } async function verifyAgentSignature(method, path, headers) { const signature = headers["signature"] || headers["Signature"]; const signatureInput = headers["signature-input"] || headers["Signature-Input"]; const signatureAgent = headers["signature-agent"] || headers["Signature-Agent"]; if (!signature || !signatureInput) { return { isValid: false, confidence: 0, reason: "No signature headers present", verificationMethod: "none" }; } const parsed = parseSignatureInput(signatureInput); if (!parsed) { return { isValid: false, confidence: 0, reason: "Invalid Signature-Input header", verificationMethod: "none" }; } if (parsed.created) { const now2 = Math.floor(Date.now() / 1e3); const age = now2 - parsed.created; if (age > 300) { return { isValid: false, confidence: 0, reason: "Signature expired (older than 5 minutes)", verificationMethod: "none" }; } if (age < -30) { return { isValid: false, confidence: 0, reason: "Signature timestamp is in the future", verificationMethod: "none" }; } } let agent; let agentKey; if (signatureAgent === '"https://chatgpt.com"' || signatureAgent?.includes("chatgpt.com")) { agent = "ChatGPT"; agentKey = "chatgpt"; } if (!agent || !agentKey) { return { isValid: false, confidence: 0, reason: "Unknown signature agent", verificationMethod: "none" }; } const knownKeys = await getKeysForAgent(agentKey); if (knownKeys.length === 0) { return { isValid: false, confidence: 0, reason: "No keys available for agent", verificationMethod: "none" }; } const key = knownKeys.find((k) => k.kid === parsed.keyid); if (!key) { return { isValid: false, confidence: 0, reason: `Unknown key ID: ${parsed.keyid}`, verificationMethod: "none" }; } const now = Math.floor(Date.now() / 1e3); if (now < key.validFrom || now > key.validUntil) { return { isValid: false, confidence: 0, reason: "Key is not valid at current time", verificationMethod: "none" }; } const signatureBase = buildSignatureBase(method, path, headers, parsed.signedHeaders); let signatureValue = signature; if (signatureValue.startsWith("sig1=:")) { signatureValue = signatureValue.substring(6); } if (signatureValue.endsWith(":")) { signatureValue = signatureValue.slice(0, -1); } const isValid = await verifyEd25519Signature(key.publicKey, signatureValue, signatureBase); if (isValid) { return { isValid: true, agent, keyid: parsed.keyid, confidence: 1, // 100% confidence for valid signature verificationMethod: "signature" }; } else { return { isValid: false, confidence: 0, reason: "Signature verification failed", verificationMethod: "none" }; } } function hasSignatureHeaders(headers) { return !!((headers["signature"] || headers["Signature"]) && (headers["signature-input"] || headers["Signature-Input"])); } function isChatGPTSignature(headers) { const signatureAgent = headers["signature-agent"] || headers["Signature-Agent"]; if (!signatureAgent) { return false; } const agentUrlStr = signatureAgent.replace(/^"+|"+$/g, ""); if (agentUrlStr === "https://chatgpt.com") { return true; } try { const agentUrl = new URL(agentUrlStr); const allowedHosts = ["chatgpt.com", "www.chatgpt.com"]; return allowedHosts.includes(agentUrl.host); } catch { return false; } } var rules = agentshieldShared.loadRulesSync(); var EdgeAgentDetector = class { rules; constructor() { this.rules = rules; } async analyze(input) { const reasons = []; let detectedAgent; let verificationMethod; let confidence = 0; const headers = input.headers || {}; const normalizedHeaders = {}; for (const [key, value] of Object.entries(headers)) { normalizedHeaders[key.toLowerCase()] = value; } if (hasSignatureHeaders(headers)) { try { const signatureResult = await verifyAgentSignature( input.method || "GET", input.url || "/", headers ); if (signatureResult.isValid) { confidence = signatureResult.confidence * 100; reasons.push(`verified_signature:${signatureResult.agent?.toLowerCase() || "unknown"}`); if (signatureResult.agent) { detectedAgent = { type: signatureResult.agent.toLowerCase(), name: signatureResult.agent }; } verificationMethod = signatureResult.verificationMethod; if (signatureResult.keyid) { reasons.push(`keyid:${signatureResult.keyid}`); } } else { console.warn("[EdgeAgentDetector] Signature verification failed:", { reason: signatureResult.reason, agent: signatureResult.agent, hasSignatureAgent: !!headers["signature-agent"] || !!headers["Signature-Agent"], signatureAgentValue: headers["signature-agent"] || headers["Signature-Agent"] }); confidence = Math.max(confidence, 30); reasons.push("invalid_signature"); if (signatureResult.reason) { reasons.push(`signature_error:${signatureResult.reason}`); } if (isChatGPTSignature(headers)) { reasons.push("claims_chatgpt"); detectedAgent = { type: "chatgpt", name: "ChatGPT (unverified)" }; } } } catch (error) { console.error("[EdgeAgentDetector] Signature verification error:", error); confidence = Math.max(confidence, 20); reasons.push("signature_verification_error"); } } const userAgent = input.userAgent || input.headers?.["user-agent"] || ""; if (userAgent) { const userAgentEntries = Object.entries(this.rules.rules.userAgents); const genericKeys = ["generic_bot", "dev_tools", "automation_tools"]; const sortedEntries = userAgentEntries.sort((a, b) => { const aIsGeneric = genericKeys.includes(a[0]); const bIsGeneric = genericKeys.includes(b[0]); if (aIsGeneric && !bIsGeneric) return 1; if (!aIsGeneric && bIsGeneric) return -1; return 0; }); for (const [agentKey, agentRule] of sortedEntries) { const rule = agentRule; const matched = rule.patterns.some((pattern) => { const regex = new RegExp(pattern, "i"); return regex.test(userAgent); }); if (matched) { const agentType = this.getAgentType(agentKey); const agentName = this.getAgentName(agentKey); confidence = Math.max(confidence, rule.confidence * 100); reasons.push(`known_pattern:${agentType}`); if (!detectedAgent) { detectedAgent = { type: agentType, name: agentName }; verificationMethod = "pattern"; } break; } } } const suspiciousHeaders = this.rules.rules.headers.suspicious; const foundAiHeaders = suspiciousHeaders.filter( (headerRule) => normalizedHeaders[headerRule.name.toLowerCase()] ); if (foundAiHeaders.length > 0) { const maxConfidence = Math.max(...foundAiHeaders.map((h) => h.confidence * 100)); confidence = Math.max(confidence, maxConfidence); reasons.push(`ai_headers:${foundAiHeaders.length}`); } const ip = input.ip || input.ipAddress; if (ip && !normalizedHeaders["x-forwarded-for"] && !normalizedHeaders["x-real-ip"]) { const ipRanges = "providers" in this.rules.rules.ipRanges ? this.rules.rules.ipRanges.providers : this.rules.rules.ipRanges; for (const [provider, ipRule] of Object.entries(ipRanges)) { if (!ipRule || typeof ipRule !== "object" || !("ranges" in ipRule) || !Array.isArray(ipRule.ranges)) continue; const matched = ipRule.ranges.some((range) => { const prefix = range.split("/")[0]; const prefixParts = prefix.split("."); const ipParts = ip.split("."); for (let i = 0; i < Math.min(prefixParts.length - 1, 2); i++) { if (prefixParts[i] !== ipParts[i] && prefixParts[i] !== "0") { return false; } } return true; }); if (matched) { const rule = ipRule; confidence = Math.max(confidence, rule.confidence * 40); reasons.push(`cloud_provider:${provider}`); break; } } } if (reasons.length > 2 && confidence < 100) { confidence = Math.min(confidence * 1.2, 95); } confidence = Math.min(Math.max(confidence, 0), 100); return { isAgent: confidence > 30, // Updated to 0-100 scale (was 0.3) confidence, detectionClass: confidence > 30 && detectedAgent ? { type: "AiAgent", agentType: detectedAgent.name } : confidence > 30 ? { type: "Unknown" } : { type: "Human" }, signals: [], // Will be populated by enhanced detection engine in future tasks ...detectedAgent && { detectedAgent }, reasons, ...verificationMethod && { verificationMethod }, forgeabilityRisk: verificationMethod === "signature" ? "low" : confidence > 80 ? "medium" : "high", // Updated to 0-100 scale timestamp: /* @__PURE__ */ new Date() }; } /** * Get agent type from rule key */ getAgentType(agentKey) { const typeMap = { openai_gptbot: "openai", anthropic_claude: "anthropic", perplexity_bot: "perplexity", google_ai: "google", microsoft_ai: "microsoft", meta_ai: "meta", cohere_bot: "cohere", huggingface_bot: "huggingface", generic_bot: "generic", dev_tools: "dev", automation_tools: "automation" }; return typeMap[agentKey] || agentKey; } /** * Get agent name from rule key */ getAgentName(agentKey) { const nameMap = { openai_gptbot: "ChatGPT/GPTBot", anthropic_claude: "Claude", perplexity_bot: "Perplexity", google_ai: "Google AI", microsoft_ai: "Microsoft Copilot", meta_ai: "Meta AI", cohere_bot: "Cohere", huggingface_bot: "HuggingFace", generic_bot: "Generic Bot", dev_tools: "Development Tool", automation_tools: "Automation Tool" }; return nameMap[agentKey] || agentKey; } }; var EdgeAgentDetectorWrapper = class { detector; events = /* @__PURE__ */ new Map(); constructor(_config) { this.detector = new EdgeAgentDetector(); } async analyze(input) { const result = await this.detector.analyze(input); if (result.isAgent && this.events.has("agent.detected")) { const handlers = this.events.get("agent.detected") || []; handlers.forEach((handler) => handler(result, input)); } return result; } on(event, handler) { if (!this.events.has(event)) { this.events.set(event, []); } this.events.get(event).push(handler); } emit(event, ...args) { const handlers = this.events.get(event) || []; handlers.forEach((handler) => handler(...args)); } async init() { return; } }; exports.EdgeAgentDetectorWrapper = EdgeAgentDetectorWrapper; //# sourceMappingURL=edge-detector-wrapper.js.map //# sourceMappingURL=edge-detector-wrapper.js.map