UNPKG

@kya-os/agentshield-nextjs

Version:

Next.js middleware for AgentShield AI agent detection

161 lines (159 loc) 5.91 kB
import { NextResponse } from 'next/server'; import { createEvaluationContext, evaluatePolicy, ENFORCEMENT_ACTIONS, PolicyConfigSchema, DEFAULT_POLICY, matchPath, createPolicyFetcher } from '@kya-os/agentshield-shared'; export { DEFAULT_POLICY, ENFORCEMENT_ACTIONS, createEvaluationContext, evaluatePolicy } from '@kya-os/agentshield-shared'; // src/policy.ts function createContextFromDetection(detection, request) { return createEvaluationContext({ agentType: detection.detectedAgent?.type, agentName: detection.detectedAgent?.name, agentVendor: detection.detectedAgent?.vendor, confidence: detection.confidence, riskLevel: detection.riskLevel, path: request.nextUrl.pathname, method: request.method, signatureVerified: detection.verificationMethod === "signature", isAuthenticated: false, // TODO: integrate with auth userAgent: request.headers.get("user-agent") || void 0 }); } function evaluatePolicyForDetection(detection, request, policy) { const context = createContextFromDetection(detection, request); return evaluatePolicy(policy, context); } function buildBlockedResponse(decision, config) { const status = config.blockedResponse?.status ?? 403; const message = config.blockedResponse?.message ?? decision.message ?? "Access denied"; const response = NextResponse.json( { error: message, code: "POLICY_BLOCKED", reason: decision.reason, ruleId: decision.ruleId, matchType: decision.matchType }, { status } ); if (config.blockedResponse?.headers) { for (const [key, value] of Object.entries(config.blockedResponse.headers)) { response.headers.set(key, value); } } response.headers.set("X-AgentShield-Action", decision.action); response.headers.set("X-AgentShield-Reason", decision.reason); response.headers.set("X-AgentShield-MatchType", decision.matchType); return response; } function buildRedirectResponse(request, decision, config) { const redirectUrl = decision.redirectUrl || config.redirectUrl || "/blocked"; const url = new URL(redirectUrl, request.url); url.searchParams.set("reason", decision.reason); if (decision.ruleId) { url.searchParams.set("ruleId", decision.ruleId); } return NextResponse.redirect(url); } function buildChallengeResponse(request, decision, config) { return buildRedirectResponse(request, decision, config); } async function handlePolicyDecision(request, decision, config) { switch (decision.action) { case ENFORCEMENT_ACTIONS.BLOCK: if (config.customBlockedResponse) { return await config.customBlockedResponse(request, decision); } return buildBlockedResponse(decision, config); case ENFORCEMENT_ACTIONS.REDIRECT: return buildRedirectResponse(request, decision, config); case ENFORCEMENT_ACTIONS.CHALLENGE: return buildChallengeResponse(request, decision, config); case ENFORCEMENT_ACTIONS.LOG: console.log("[AgentShield] Policy decision (log):", { path: request.nextUrl.pathname, action: decision.action, reason: decision.reason, matchType: decision.matchType, ruleId: decision.ruleId }); return null; // Continue to allow case ENFORCEMENT_ACTIONS.ALLOW: default: return null; } } var fetcherCache = /* @__PURE__ */ new Map(); function getFetcherCacheKey(config) { return `${config.apiUrl ?? "default"}:${config.apiKey ?? ""}:${config.cacheTtlSeconds ?? "default"}`; } function getPolicyFetcher(config) { if (!config) { throw new Error("fetchPolicy config required"); } const cacheKey = getFetcherCacheKey(config); let fetcher = fetcherCache.get(cacheKey); if (!fetcher) { const fetcherConfig = { apiBaseUrl: config.apiUrl || "https://kya.vouched.id", apiKey: config.apiKey, cacheTtlSeconds: config.cacheTtlSeconds }; fetcher = createPolicyFetcher(fetcherConfig); fetcherCache.set(cacheKey, fetcher); } return fetcher; } async function getPolicy(config) { if (config.policy) { return PolicyConfigSchema.parse({ ...DEFAULT_POLICY, ...config.policy }); } if (config.fetchPolicy) { try { const fetcher = getPolicyFetcher(config.fetchPolicy); return await fetcher.getPolicy(config.fetchPolicy.projectId); } catch (error) { if (config.debug) { console.warn("[AgentShield] Policy fetch failed, using fallback:", error); } return PolicyConfigSchema.parse({ ...DEFAULT_POLICY, ...config.fallbackPolicy || {} }); } } return PolicyConfigSchema.parse(DEFAULT_POLICY); } async function applyPolicy(request, detection, config) { try { const path = request.nextUrl.pathname; if (config.skipPaths?.some((pattern) => matchPath(path, pattern))) { return null; } if (config.includePaths && config.includePaths.length > 0) { if (!config.includePaths.some((pattern) => matchPath(path, pattern))) { return null; } } const policy = await getPolicy(config); const context = createContextFromDetection(detection, request); const decision = evaluatePolicy(policy, context); if (config.onPolicyDecision) { await config.onPolicyDecision(request, decision, context); } return await handlePolicyDecision(request, decision, config); } catch (error) { if (config.debug) { console.error("[AgentShield] Policy evaluation error:", error); } if (config.failOpen !== false) { return null; } return NextResponse.json( { error: "Security check failed", code: "POLICY_ERROR" }, { status: 503 } ); } } export { applyPolicy, buildBlockedResponse, buildChallengeResponse, buildRedirectResponse, createContextFromDetection, evaluatePolicyForDetection, getPolicy, handlePolicyDecision }; //# sourceMappingURL=policy.mjs.map //# sourceMappingURL=policy.mjs.map