@kya-os/agentshield-nextjs 0.2.13 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-client.d.mts +6 -1
- package/dist/api-client.d.ts +6 -1
- package/dist/api-client.js.map +1 -1
- package/dist/api-client.mjs.map +1 -1
- package/dist/api-middleware.d.mts +13 -0
- package/dist/api-middleware.d.ts +13 -0
- package/dist/api-middleware.js +149 -19
- package/dist/api-middleware.js.map +1 -1
- package/dist/api-middleware.mjs +149 -19
- package/dist/api-middleware.mjs.map +1 -1
- package/dist/create-middleware.js +565 -487
- package/dist/create-middleware.js.map +1 -1
- package/dist/create-middleware.mjs +565 -487
- package/dist/create-middleware.mjs.map +1 -1
- package/dist/edge/index.js +69 -46
- package/dist/edge/index.js.map +1 -1
- package/dist/edge/index.mjs +69 -46
- package/dist/edge/index.mjs.map +1 -1
- package/dist/edge-detector-wrapper.js +9 -1
- package/dist/edge-detector-wrapper.js.map +1 -1
- package/dist/edge-detector-wrapper.mjs +9 -1
- package/dist/edge-detector-wrapper.mjs.map +1 -1
- package/dist/edge-runtime-loader.d.mts +1 -0
- package/dist/edge-runtime-loader.d.ts +1 -0
- package/dist/edge-runtime-loader.js +19 -3
- package/dist/edge-runtime-loader.js.map +1 -1
- package/dist/edge-runtime-loader.mjs +19 -3
- package/dist/edge-runtime-loader.mjs.map +1 -1
- package/dist/edge-wasm-middleware.d.mts +1 -0
- package/dist/edge-wasm-middleware.d.ts +1 -0
- package/dist/edge-wasm-middleware.js +10 -2
- package/dist/edge-wasm-middleware.js.map +1 -1
- package/dist/edge-wasm-middleware.mjs +11 -3
- package/dist/edge-wasm-middleware.mjs.map +1 -1
- package/dist/enhanced-middleware.js +48 -20
- package/dist/enhanced-middleware.js.map +1 -1
- package/dist/enhanced-middleware.mjs +49 -21
- package/dist/enhanced-middleware.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +263 -102
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +264 -103
- package/dist/index.mjs.map +1 -1
- package/dist/middleware.js +565 -487
- package/dist/middleware.js.map +1 -1
- package/dist/middleware.mjs +565 -487
- package/dist/middleware.mjs.map +1 -1
- package/dist/policy.d.mts +16 -4
- package/dist/policy.d.ts +16 -4
- package/dist/policy.js +14 -10
- package/dist/policy.js.map +1 -1
- package/dist/policy.mjs +14 -10
- package/dist/policy.mjs.map +1 -1
- package/dist/session-tracker.js +13 -19
- package/dist/session-tracker.js.map +1 -1
- package/dist/session-tracker.mjs +13 -19
- package/dist/session-tracker.mjs.map +1 -1
- package/dist/signature-verifier.js +9 -1
- package/dist/signature-verifier.js.map +1 -1
- package/dist/signature-verifier.mjs +9 -1
- package/dist/signature-verifier.mjs.map +1 -1
- package/dist/wasm-middleware.d.mts +1 -0
- package/dist/wasm-middleware.d.ts +1 -0
- package/dist/wasm-middleware.js +15 -15
- package/dist/wasm-middleware.js.map +1 -1
- package/dist/wasm-middleware.mjs +15 -15
- package/dist/wasm-middleware.mjs.map +1 -1
- package/package.json +23 -22
- package/wasm/agentshield_wasm.d.ts +379 -21
- package/wasm/agentshield_wasm.js +1409 -506
- package/wasm/agentshield_wasm_bg.wasm +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadRulesSync,
|
|
1
|
+
import { loadRulesSync, shouldEnforce, evaluateEnforcement, createEvaluationContext, evaluatePolicy, ENFORCEMENT_ACTIONS, PolicyConfigSchema, DEFAULT_POLICY, matchPath as matchPath$1, createPolicyFetcher, mapVerificationMethod } from '@kya-os/agentshield-shared';
|
|
2
2
|
export { DEFAULT_POLICY, ENFORCEMENT_ACTIONS, createEvaluationContext, evaluatePolicy } from '@kya-os/agentshield-shared';
|
|
3
3
|
import { NextResponse } from 'next/server';
|
|
4
4
|
import * as ed25519 from '@noble/ed25519';
|
|
@@ -232,7 +232,15 @@ var init_edge_detector_with_wasm = __esm({
|
|
|
232
232
|
}
|
|
233
233
|
const signaturePresent = !!(normalizedHeaders["signature"] || normalizedHeaders["signature-input"]);
|
|
234
234
|
const signatureAgent = normalizedHeaders["signature-agent"];
|
|
235
|
-
|
|
235
|
+
const isChatGPT = (() => {
|
|
236
|
+
try {
|
|
237
|
+
const url = new URL(signatureAgent?.replace(/^"|"$/g, "") || "");
|
|
238
|
+
return url.hostname === "chatgpt.com" || url.hostname.endsWith(".chatgpt.com");
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
})();
|
|
243
|
+
if (isChatGPT) {
|
|
236
244
|
confidence = 85;
|
|
237
245
|
reasons.push("signature_agent:chatgpt");
|
|
238
246
|
detectedAgent = { type: "chatgpt", name: "ChatGPT" };
|
|
@@ -759,7 +767,15 @@ async function verifyAgentSignature(method, path, headers) {
|
|
|
759
767
|
}
|
|
760
768
|
let agent;
|
|
761
769
|
let agentKey;
|
|
762
|
-
|
|
770
|
+
const isChatGPT = signatureAgent === '"https://chatgpt.com"' || (() => {
|
|
771
|
+
try {
|
|
772
|
+
const url = new URL(signatureAgent?.replace(/^"|"$/g, "") || "");
|
|
773
|
+
return url.hostname === "chatgpt.com" || url.hostname.endsWith(".chatgpt.com");
|
|
774
|
+
} catch {
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
})();
|
|
778
|
+
if (isChatGPT) {
|
|
763
779
|
agent = "ChatGPT";
|
|
764
780
|
agentKey = "chatgpt";
|
|
765
781
|
}
|
|
@@ -1056,11 +1072,6 @@ var EdgeAgentDetectorWrapper = class {
|
|
|
1056
1072
|
return;
|
|
1057
1073
|
}
|
|
1058
1074
|
};
|
|
1059
|
-
|
|
1060
|
-
// src/middleware.ts
|
|
1061
|
-
init_edge_detector_with_wasm();
|
|
1062
|
-
|
|
1063
|
-
// src/session-tracker.ts
|
|
1064
1075
|
var EdgeSessionTracker = class {
|
|
1065
1076
|
config;
|
|
1066
1077
|
constructor(config) {
|
|
@@ -1077,7 +1088,7 @@ var EdgeSessionTracker = class {
|
|
|
1077
1088
|
*/
|
|
1078
1089
|
async track(_request, response, result) {
|
|
1079
1090
|
try {
|
|
1080
|
-
if (!this.config.enabled || !result
|
|
1091
|
+
if (!this.config.enabled || !shouldEnforce(result)) {
|
|
1081
1092
|
return response;
|
|
1082
1093
|
}
|
|
1083
1094
|
const sessionData = {
|
|
@@ -1152,9 +1163,7 @@ var EdgeSessionTracker = class {
|
|
|
1152
1163
|
for (let i = 0; i < encoded.length; i++) {
|
|
1153
1164
|
obfuscated[i] = (encoded[i] || 0) ^ key.charCodeAt(i % key.length);
|
|
1154
1165
|
}
|
|
1155
|
-
return btoa(
|
|
1156
|
-
Array.from(obfuscated, (byte) => String.fromCharCode(byte)).join("")
|
|
1157
|
-
);
|
|
1166
|
+
return btoa(Array.from(obfuscated, (byte) => String.fromCharCode(byte)).join(""));
|
|
1158
1167
|
} catch (error) {
|
|
1159
1168
|
return btoa(data);
|
|
1160
1169
|
}
|
|
@@ -1179,9 +1188,9 @@ var EdgeSessionTracker = class {
|
|
|
1179
1188
|
var StatelessSessionChecker = class {
|
|
1180
1189
|
static check(headers) {
|
|
1181
1190
|
try {
|
|
1182
|
-
const agent = headers["
|
|
1183
|
-
const confidence = headers["
|
|
1184
|
-
const sessionId = headers["
|
|
1191
|
+
const agent = headers["kya-session-agent"];
|
|
1192
|
+
const confidence = headers["kya-session-confidence"];
|
|
1193
|
+
const sessionId = headers["kya-session-id"];
|
|
1185
1194
|
if (agent && confidence && sessionId) {
|
|
1186
1195
|
return {
|
|
1187
1196
|
id: sessionId,
|
|
@@ -1211,33 +1220,49 @@ var StatelessSessionChecker = class {
|
|
|
1211
1220
|
static setHeaders(response, session) {
|
|
1212
1221
|
try {
|
|
1213
1222
|
if (response.setHeader) {
|
|
1214
|
-
response.setHeader("
|
|
1215
|
-
response.setHeader(
|
|
1216
|
-
|
|
1217
|
-
session.confidence.toString()
|
|
1218
|
-
);
|
|
1219
|
-
response.setHeader("X-AgentShield-Session-Id", session.id);
|
|
1223
|
+
response.setHeader("KYA-Session-Agent", session.agent);
|
|
1224
|
+
response.setHeader("KYA-Session-Confidence", session.confidence.toString());
|
|
1225
|
+
response.setHeader("KYA-Session-Id", session.id);
|
|
1220
1226
|
} else if (response.headers && response.headers.set) {
|
|
1221
|
-
response.headers.set("
|
|
1222
|
-
response.headers.set(
|
|
1223
|
-
|
|
1224
|
-
session.confidence.toString()
|
|
1225
|
-
);
|
|
1226
|
-
response.headers.set("x-agentshield-session-id", session.id);
|
|
1227
|
+
response.headers.set("kya-session-agent", session.agent);
|
|
1228
|
+
response.headers.set("kya-session-confidence", session.confidence.toString());
|
|
1229
|
+
response.headers.set("kya-session-id", session.id);
|
|
1227
1230
|
}
|
|
1228
1231
|
} catch {
|
|
1229
1232
|
}
|
|
1230
1233
|
}
|
|
1231
1234
|
};
|
|
1232
1235
|
|
|
1233
|
-
// src/
|
|
1236
|
+
// src/utils.ts
|
|
1237
|
+
function getClientIp(request) {
|
|
1238
|
+
const forwardedFor = request.headers.get("x-forwarded-for");
|
|
1239
|
+
if (forwardedFor) {
|
|
1240
|
+
const ip = forwardedFor.split(",")[0]?.trim();
|
|
1241
|
+
if (ip) return ip;
|
|
1242
|
+
}
|
|
1243
|
+
const realIp = request.headers.get("x-real-ip");
|
|
1244
|
+
if (realIp) return realIp;
|
|
1245
|
+
const cfIp = request.headers.get("cf-connecting-ip");
|
|
1246
|
+
if (cfIp) return cfIp;
|
|
1247
|
+
const clientIp = request.headers.get("x-client-ip");
|
|
1248
|
+
if (clientIp) return clientIp;
|
|
1249
|
+
return void 0;
|
|
1250
|
+
}
|
|
1251
|
+
function safeHostname(url) {
|
|
1252
|
+
try {
|
|
1253
|
+
return new URL(url).hostname;
|
|
1254
|
+
} catch {
|
|
1255
|
+
return "this site";
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1234
1258
|
function createAgentShieldMiddleware(config = {}) {
|
|
1235
|
-
|
|
1259
|
+
let detector = config.enableWasm ? null : new EdgeAgentDetectorWrapper(config);
|
|
1260
|
+
let detectorInitPromise = null;
|
|
1236
1261
|
const sessionTracker = config.sessionTracking?.enabled || config.enableWasm ? new EdgeSessionTracker({
|
|
1237
1262
|
enabled: true,
|
|
1238
1263
|
...config.sessionTracking
|
|
1239
1264
|
}) : null;
|
|
1240
|
-
if (config.events) {
|
|
1265
|
+
if (detector && config.events) {
|
|
1241
1266
|
Object.entries(config.events).forEach(([event, handler]) => {
|
|
1242
1267
|
if (handler) {
|
|
1243
1268
|
detector.on(event, handler);
|
|
@@ -1258,6 +1283,23 @@ function createAgentShieldMiddleware(config = {}) {
|
|
|
1258
1283
|
} = config;
|
|
1259
1284
|
return async (request) => {
|
|
1260
1285
|
try {
|
|
1286
|
+
if (!detector) {
|
|
1287
|
+
if (!detectorInitPromise) {
|
|
1288
|
+
detectorInitPromise = (async () => {
|
|
1289
|
+
const { EdgeAgentDetectorWrapperWithWasm: EdgeAgentDetectorWrapperWithWasm2 } = await Promise.resolve().then(() => (init_edge_detector_with_wasm(), edge_detector_with_wasm_exports));
|
|
1290
|
+
detector = new EdgeAgentDetectorWrapperWithWasm2({ enableWasm: true });
|
|
1291
|
+
if (config.events) {
|
|
1292
|
+
Object.entries(config.events).forEach(([event, handler]) => {
|
|
1293
|
+
if (handler) {
|
|
1294
|
+
detector.on(event, handler);
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
})();
|
|
1299
|
+
}
|
|
1300
|
+
await detectorInitPromise;
|
|
1301
|
+
}
|
|
1302
|
+
const activeDetector = detector;
|
|
1261
1303
|
const shouldSkip = skipPaths.some((pattern) => {
|
|
1262
1304
|
if (typeof pattern === "string") {
|
|
1263
1305
|
return request.nextUrl.pathname.startsWith(pattern);
|
|
@@ -1271,11 +1313,11 @@ function createAgentShieldMiddleware(config = {}) {
|
|
|
1271
1313
|
const existingSession = sessionTracker ? await sessionTracker.check(request) : null;
|
|
1272
1314
|
if (existingSession) {
|
|
1273
1315
|
const response2 = NextResponse.next();
|
|
1274
|
-
response2.headers.set("
|
|
1275
|
-
response2.headers.set("
|
|
1276
|
-
response2.headers.set("
|
|
1277
|
-
response2.headers.set("
|
|
1278
|
-
response2.headers.set("
|
|
1316
|
+
response2.headers.set("kya-detected", "true");
|
|
1317
|
+
response2.headers.set("kya-agent", existingSession.agent);
|
|
1318
|
+
response2.headers.set("kya-confidence", existingSession.confidence.toString());
|
|
1319
|
+
response2.headers.set("kya-session", "continued");
|
|
1320
|
+
response2.headers.set("kya-session-id", existingSession.id);
|
|
1279
1321
|
request.agentShield = {
|
|
1280
1322
|
result: {
|
|
1281
1323
|
isAgent: true,
|
|
@@ -1295,17 +1337,17 @@ function createAgentShieldMiddleware(config = {}) {
|
|
|
1295
1337
|
};
|
|
1296
1338
|
const context2 = {
|
|
1297
1339
|
userAgent: request.headers.get("user-agent") || "",
|
|
1298
|
-
ipAddress: (request
|
|
1340
|
+
ipAddress: getClientIp(request) || "",
|
|
1299
1341
|
headers: Object.fromEntries(request.headers.entries()),
|
|
1300
1342
|
url: request.url,
|
|
1301
1343
|
method: request.method,
|
|
1302
1344
|
timestamp: /* @__PURE__ */ new Date()
|
|
1303
1345
|
};
|
|
1304
|
-
|
|
1346
|
+
activeDetector.emit("agent.session.continued", existingSession, context2);
|
|
1305
1347
|
return response2;
|
|
1306
1348
|
}
|
|
1307
1349
|
const userAgent = request.headers.get("user-agent");
|
|
1308
|
-
const ipAddress = request
|
|
1350
|
+
const ipAddress = getClientIp(request);
|
|
1309
1351
|
const url = new URL(request.url);
|
|
1310
1352
|
const pathWithQuery = url.pathname + url.search;
|
|
1311
1353
|
const context = {
|
|
@@ -1317,15 +1359,19 @@ function createAgentShieldMiddleware(config = {}) {
|
|
|
1317
1359
|
method: request.method,
|
|
1318
1360
|
timestamp: /* @__PURE__ */ new Date()
|
|
1319
1361
|
};
|
|
1320
|
-
const result = await
|
|
1321
|
-
|
|
1362
|
+
const result = await activeDetector.analyze(context);
|
|
1363
|
+
const decision = evaluateEnforcement(result, {
|
|
1364
|
+
confidenceThreshold: config.confidenceThreshold,
|
|
1365
|
+
defaultAction: onAgentDetected
|
|
1366
|
+
});
|
|
1367
|
+
if (decision.shouldNotify) {
|
|
1322
1368
|
if (onDetection) {
|
|
1323
1369
|
const customResponse = await onDetection(request, result);
|
|
1324
1370
|
if (customResponse) {
|
|
1325
1371
|
return customResponse;
|
|
1326
1372
|
}
|
|
1327
1373
|
}
|
|
1328
|
-
switch (
|
|
1374
|
+
switch (decision.action) {
|
|
1329
1375
|
case "block": {
|
|
1330
1376
|
const response2 = NextResponse.json(
|
|
1331
1377
|
{
|
|
@@ -1341,11 +1387,17 @@ function createAgentShieldMiddleware(config = {}) {
|
|
|
1341
1387
|
response2.headers.set(key, value);
|
|
1342
1388
|
});
|
|
1343
1389
|
}
|
|
1344
|
-
|
|
1390
|
+
activeDetector.emit("agent.blocked", result, context);
|
|
1345
1391
|
return response2;
|
|
1346
1392
|
}
|
|
1347
|
-
case "redirect":
|
|
1348
|
-
|
|
1393
|
+
case "redirect": {
|
|
1394
|
+
const redirectTarget = new URL(redirectUrl, request.url);
|
|
1395
|
+
const agentName = result.detectedAgent?.name;
|
|
1396
|
+
if (agentName && !redirectTarget.searchParams.has("agent")) {
|
|
1397
|
+
redirectTarget.searchParams.set("agent", agentName.toLowerCase());
|
|
1398
|
+
}
|
|
1399
|
+
return NextResponse.redirect(redirectTarget);
|
|
1400
|
+
}
|
|
1349
1401
|
case "rewrite":
|
|
1350
1402
|
return NextResponse.rewrite(new URL(rewriteUrl, request.url));
|
|
1351
1403
|
case "log":
|
|
@@ -1361,9 +1413,7 @@ function createAgentShieldMiddleware(config = {}) {
|
|
|
1361
1413
|
break;
|
|
1362
1414
|
case "allow":
|
|
1363
1415
|
default:
|
|
1364
|
-
|
|
1365
|
-
detector.emit("agent.allowed", result, context);
|
|
1366
|
-
}
|
|
1416
|
+
activeDetector.emit("agent.allowed", result, context);
|
|
1367
1417
|
break;
|
|
1368
1418
|
}
|
|
1369
1419
|
}
|
|
@@ -1372,15 +1422,15 @@ function createAgentShieldMiddleware(config = {}) {
|
|
|
1372
1422
|
skipped: false
|
|
1373
1423
|
};
|
|
1374
1424
|
let response = NextResponse.next();
|
|
1375
|
-
response.headers.set("
|
|
1376
|
-
response.headers.set("
|
|
1425
|
+
response.headers.set("kya-detected", result.isAgent.toString());
|
|
1426
|
+
response.headers.set("kya-confidence", result.confidence.toString());
|
|
1377
1427
|
if (result.detectedAgent?.name) {
|
|
1378
|
-
response.headers.set("
|
|
1428
|
+
response.headers.set("kya-agent", result.detectedAgent.name);
|
|
1379
1429
|
}
|
|
1380
|
-
if (sessionTracker &&
|
|
1430
|
+
if (sessionTracker && decision.shouldNotify) {
|
|
1381
1431
|
response = await sessionTracker.track(request, response, result);
|
|
1382
|
-
response.headers.set("
|
|
1383
|
-
|
|
1432
|
+
response.headers.set("kya-session", "new");
|
|
1433
|
+
activeDetector.emit("agent.session.started", result, context);
|
|
1384
1434
|
}
|
|
1385
1435
|
return response;
|
|
1386
1436
|
} catch (error) {
|
|
@@ -1721,8 +1771,6 @@ async function createStorageAdapter(config) {
|
|
|
1721
1771
|
}
|
|
1722
1772
|
return new MemoryStorageAdapter();
|
|
1723
1773
|
}
|
|
1724
|
-
|
|
1725
|
-
// src/enhanced-middleware.ts
|
|
1726
1774
|
var SessionManager = class {
|
|
1727
1775
|
sessionLastActivity = /* @__PURE__ */ new Map();
|
|
1728
1776
|
generateSessionId(ipAddress, userAgent) {
|
|
@@ -1829,7 +1877,7 @@ function createEnhancedAgentShieldMiddleware(config = {}) {
|
|
|
1829
1877
|
return NextResponse.next();
|
|
1830
1878
|
}
|
|
1831
1879
|
const userAgent = request.headers.get("user-agent");
|
|
1832
|
-
const ipAddress = request
|
|
1880
|
+
const ipAddress = getClientIp(request);
|
|
1833
1881
|
const url = new URL(request.url);
|
|
1834
1882
|
const pathWithQuery = url.pathname + url.search;
|
|
1835
1883
|
const context = {
|
|
@@ -1844,14 +1892,21 @@ function createEnhancedAgentShieldMiddleware(config = {}) {
|
|
|
1844
1892
|
const result = await activeDetector.analyze(context);
|
|
1845
1893
|
let finalConfidence = result.confidence;
|
|
1846
1894
|
let verificationMethod = result.verificationMethod || "pattern";
|
|
1847
|
-
if (result
|
|
1895
|
+
if (shouldEnforce(result) && wasmConfidenceUtils) {
|
|
1848
1896
|
const reasons = result.reasons || [];
|
|
1849
1897
|
if (wasmConfidenceUtils.shouldIndicateWasmVerification(result.confidence)) {
|
|
1850
1898
|
finalConfidence = wasmConfidenceUtils.getWasmConfidenceBoost(result.confidence, reasons);
|
|
1851
1899
|
verificationMethod = wasmConfidenceUtils.getVerificationMethod(finalConfidence, reasons);
|
|
1852
1900
|
}
|
|
1853
1901
|
}
|
|
1854
|
-
|
|
1902
|
+
const decision = evaluateEnforcement(
|
|
1903
|
+
{ ...result, confidence: finalConfidence },
|
|
1904
|
+
{
|
|
1905
|
+
confidenceThreshold: config.confidenceThreshold,
|
|
1906
|
+
defaultAction: config.onAgentDetected
|
|
1907
|
+
}
|
|
1908
|
+
);
|
|
1909
|
+
if (decision.shouldNotify) {
|
|
1855
1910
|
if (sessionTrackingEnabled) {
|
|
1856
1911
|
const storage = await getStorage();
|
|
1857
1912
|
const sessionId = sessionManager.generateSessionId(
|
|
@@ -1915,26 +1970,23 @@ function createEnhancedAgentShieldMiddleware(config = {}) {
|
|
|
1915
1970
|
if (config.onDetection) {
|
|
1916
1971
|
await config.onDetection(result, context);
|
|
1917
1972
|
}
|
|
1918
|
-
switch (
|
|
1973
|
+
switch (decision.action) {
|
|
1919
1974
|
case "block": {
|
|
1920
1975
|
const { status = 403, message = "Access denied: AI agent detected" } = config.blockedResponse || {};
|
|
1921
1976
|
const response2 = NextResponse.json(
|
|
1922
1977
|
{ error: message, detected: true, confidence: finalConfidence },
|
|
1923
1978
|
{ status }
|
|
1924
1979
|
);
|
|
1925
|
-
response2.headers.set("
|
|
1926
|
-
response2.headers.set(
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
);
|
|
1930
|
-
response2.headers.set("x-agentshield-agent", result.detectedAgent?.name || "Unknown");
|
|
1931
|
-
response2.headers.set("x-agentshield-verification", verificationMethod);
|
|
1980
|
+
response2.headers.set("kya-detected", "true");
|
|
1981
|
+
response2.headers.set("kya-confidence", String(Math.round(finalConfidence * 100)));
|
|
1982
|
+
response2.headers.set("kya-agent", result.detectedAgent?.name || "Unknown");
|
|
1983
|
+
response2.headers.set("kya-verification", verificationMethod);
|
|
1932
1984
|
return response2;
|
|
1933
1985
|
}
|
|
1934
1986
|
case "log": {
|
|
1935
1987
|
const isInteresting = finalConfidence >= 0.9 || result.detectedAgent?.name?.toLowerCase().includes("chatgpt") || result.detectedAgent?.name?.toLowerCase().includes("perplexity") || verificationMethod === "signature";
|
|
1936
1988
|
if (isInteresting && process.env.NODE_ENV !== "production") {
|
|
1937
|
-
console.debug(`[AgentShield]
|
|
1989
|
+
console.debug(`[AgentShield] AI Agent detected (${verificationMethod}):`, {
|
|
1938
1990
|
agent: result.detectedAgent?.name,
|
|
1939
1991
|
confidence: `${(finalConfidence * 100).toFixed(0)}%`,
|
|
1940
1992
|
path: pathWithQuery,
|
|
@@ -1947,14 +1999,14 @@ function createEnhancedAgentShieldMiddleware(config = {}) {
|
|
|
1947
1999
|
}
|
|
1948
2000
|
const response = NextResponse.next();
|
|
1949
2001
|
if (result.isAgent) {
|
|
1950
|
-
response.headers.set("
|
|
1951
|
-
response.headers.set("
|
|
1952
|
-
response.headers.set("
|
|
1953
|
-
response.headers.set("
|
|
2002
|
+
response.headers.set("kya-detected", "true");
|
|
2003
|
+
response.headers.set("kya-confidence", String(Math.round(finalConfidence * 100)));
|
|
2004
|
+
response.headers.set("kya-agent", result.detectedAgent?.name || "Unknown");
|
|
2005
|
+
response.headers.set("kya-verification", verificationMethod);
|
|
1954
2006
|
if (finalConfidence > 0.9) {
|
|
1955
|
-
response.headers.set("
|
|
1956
|
-
response.headers.set("
|
|
1957
|
-
response.headers.set("
|
|
2007
|
+
response.headers.set("kya-ai-visitor", "true");
|
|
2008
|
+
response.headers.set("kya-ai-confidence", finalConfidence.toString());
|
|
2009
|
+
response.headers.set("kya-ai-verification", verificationMethod);
|
|
1958
2010
|
}
|
|
1959
2011
|
}
|
|
1960
2012
|
return response;
|
|
@@ -2153,6 +2205,80 @@ function getAgentShieldClient(config) {
|
|
|
2153
2205
|
function resetAgentShieldClient() {
|
|
2154
2206
|
clientInstance = null;
|
|
2155
2207
|
}
|
|
2208
|
+
var MCP_I_DOCS_URL = "https://docs.knowthat.ai/mcp-i/getting-started";
|
|
2209
|
+
var DEFAULT_CONNECT_PATH = "/connect";
|
|
2210
|
+
function buildAgentInstructionResponse(request, decision, redirectUrl) {
|
|
2211
|
+
const resolved = resolveUrl(redirectUrl ?? DEFAULT_CONNECT_PATH, request.url);
|
|
2212
|
+
const agentName = decision.agentName || decision.agentType || "unknown";
|
|
2213
|
+
if (!resolved.searchParams.has("agent")) {
|
|
2214
|
+
resolved.searchParams.set("agent", agentName.toLowerCase());
|
|
2215
|
+
}
|
|
2216
|
+
const authUrl = resolved.toString();
|
|
2217
|
+
const hostname = safeHostname(request.url);
|
|
2218
|
+
const body = {
|
|
2219
|
+
// Markdown-formatted so clients that render markdown (Claude Desktop,
|
|
2220
|
+
// ChatGPT web) surface the URL as a clickable link. Tone mirrors the
|
|
2221
|
+
// gateway response so messaging stays consistent across platforms.
|
|
2222
|
+
message: `I can't access ${hostname} yet \u2014 this site checks AI assistants at the front door.
|
|
2223
|
+
|
|
2224
|
+
**To give me access, open this link:**
|
|
2225
|
+
[Connect securely to ${hostname}](${authUrl})
|
|
2226
|
+
|
|
2227
|
+
It only takes a moment and you won't need to do it again. Once you're done, ask me to try again and I'll connect through the verified channel automatically.`,
|
|
2228
|
+
user_action_required: {
|
|
2229
|
+
action: `Connect securely to ${hostname}`,
|
|
2230
|
+
url: authUrl,
|
|
2231
|
+
reason: `${hostname} checks AI assistants before they connect. Open the link to give your assistant a verified key.`
|
|
2232
|
+
},
|
|
2233
|
+
mcp_i: {
|
|
2234
|
+
version: "1.0",
|
|
2235
|
+
action: "authenticate",
|
|
2236
|
+
authorization_url: authUrl,
|
|
2237
|
+
flow: {
|
|
2238
|
+
type: "oauth2_delegation",
|
|
2239
|
+
steps: [
|
|
2240
|
+
"1. Direct your user to the authorization_url",
|
|
2241
|
+
"2. User reviews requested scopes and grants consent",
|
|
2242
|
+
"3. Receive delegation credential (JWT)",
|
|
2243
|
+
"4. Include credential in KYA-Delegation header",
|
|
2244
|
+
"5. Retry this request with the proof"
|
|
2245
|
+
]
|
|
2246
|
+
},
|
|
2247
|
+
retry_instructions: {
|
|
2248
|
+
header: "KYA-Delegation",
|
|
2249
|
+
format: "JWT delegation credential from authorization flow"
|
|
2250
|
+
},
|
|
2251
|
+
documentation: MCP_I_DOCS_URL
|
|
2252
|
+
},
|
|
2253
|
+
error: "mcp_authentication_required",
|
|
2254
|
+
code: "AGENT_REQUIRES_DELEGATION",
|
|
2255
|
+
detection: {
|
|
2256
|
+
agent_type: decision.agentType || "ai_agent",
|
|
2257
|
+
agent_name: decision.agentName || "Unknown Agent",
|
|
2258
|
+
confidence: decision.confidence
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
const response = NextResponse.json(body, { status: 401 });
|
|
2262
|
+
response.headers.set("WWW-Authenticate", `KYA realm="api", authorization_uri="${authUrl}"`);
|
|
2263
|
+
response.headers.set(
|
|
2264
|
+
"Link",
|
|
2265
|
+
`<${authUrl}>; rel="kya-authorize", <${MCP_I_DOCS_URL}>; rel="help"`
|
|
2266
|
+
);
|
|
2267
|
+
response.headers.set("KYA-Auth-Required", "true");
|
|
2268
|
+
response.headers.set("KYA-Auth-Url", authUrl);
|
|
2269
|
+
response.headers.set("KYA-Action", "instruct");
|
|
2270
|
+
response.headers.set("KYA-Detected-Agent", agentName);
|
|
2271
|
+
response.headers.set("KYA-Confidence", decision.confidence.toString());
|
|
2272
|
+
response.headers.set("Cache-Control", "no-store");
|
|
2273
|
+
return response;
|
|
2274
|
+
}
|
|
2275
|
+
function resolveUrl(target, baseUrl2) {
|
|
2276
|
+
try {
|
|
2277
|
+
return new URL(target, baseUrl2);
|
|
2278
|
+
} catch {
|
|
2279
|
+
return new URL(DEFAULT_CONNECT_PATH, baseUrl2);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2156
2282
|
|
|
2157
2283
|
// src/api-middleware.ts
|
|
2158
2284
|
function matchPath(path, pattern) {
|
|
@@ -2173,27 +2299,52 @@ function shouldIncludePath(path, includePaths) {
|
|
|
2173
2299
|
if (!includePaths || includePaths.length === 0) return true;
|
|
2174
2300
|
return includePaths.some((pattern) => matchPath(path, pattern));
|
|
2175
2301
|
}
|
|
2176
|
-
function buildBlockedResponse(decision, config) {
|
|
2302
|
+
function buildBlockedResponse(request, decision, config) {
|
|
2177
2303
|
const status = config.blockedResponse?.status ?? 403;
|
|
2178
2304
|
const message = config.blockedResponse?.message ?? decision.message ?? "Access denied";
|
|
2179
|
-
const
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2305
|
+
const recoveryUrl = resolveRecoveryUrl(request, config.redirectUrl || decision.redirectUrl);
|
|
2306
|
+
const body = {
|
|
2307
|
+
error: message,
|
|
2308
|
+
code: "AGENT_BLOCKED",
|
|
2309
|
+
reason: decision.reason,
|
|
2310
|
+
agentType: decision.agentType
|
|
2311
|
+
};
|
|
2312
|
+
if (recoveryUrl) {
|
|
2313
|
+
const hostname = safeHostname(request.url);
|
|
2314
|
+
body.user_action_required = {
|
|
2315
|
+
action: `Connect securely to ${hostname}`,
|
|
2316
|
+
url: recoveryUrl,
|
|
2317
|
+
reason: `${hostname} blocks unverified AI assistants. Open the link to give your assistant a verified key and try again.`
|
|
2318
|
+
};
|
|
2319
|
+
body.message = `I can't access ${hostname} \u2014 this site blocks unverified AI assistants.
|
|
2320
|
+
|
|
2321
|
+
**To give me access, open this link:**
|
|
2322
|
+
[Connect securely to ${hostname}](${recoveryUrl})
|
|
2323
|
+
|
|
2324
|
+
Once you're done, ask me to try again.`;
|
|
2325
|
+
}
|
|
2326
|
+
const response = NextResponse.json(body, { status });
|
|
2188
2327
|
if (config.blockedResponse?.headers) {
|
|
2189
2328
|
for (const [key, value] of Object.entries(config.blockedResponse.headers)) {
|
|
2190
2329
|
response.headers.set(key, value);
|
|
2191
2330
|
}
|
|
2192
2331
|
}
|
|
2193
|
-
|
|
2194
|
-
|
|
2332
|
+
if (recoveryUrl) {
|
|
2333
|
+
response.headers.set("Link", `<${recoveryUrl}>; rel="kya-authorize"`);
|
|
2334
|
+
response.headers.set("KYA-Auth-Url", recoveryUrl);
|
|
2335
|
+
}
|
|
2336
|
+
response.headers.set("KYA-Action", decision.action);
|
|
2337
|
+
response.headers.set("KYA-Reason", decision.reason);
|
|
2195
2338
|
return response;
|
|
2196
2339
|
}
|
|
2340
|
+
function resolveRecoveryUrl(request, target) {
|
|
2341
|
+
if (!target) return void 0;
|
|
2342
|
+
try {
|
|
2343
|
+
return new URL(target, request.url).toString();
|
|
2344
|
+
} catch {
|
|
2345
|
+
return void 0;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2197
2348
|
function buildRedirectResponse(request, decision, config) {
|
|
2198
2349
|
const redirectUrl = config.redirectUrl || decision.redirectUrl || "/blocked";
|
|
2199
2350
|
const url = new URL(redirectUrl, request.url);
|
|
@@ -2238,7 +2389,7 @@ function withAgentShield(config = {}) {
|
|
|
2238
2389
|
try {
|
|
2239
2390
|
const client2 = getClient();
|
|
2240
2391
|
const userAgent = request.headers.get("user-agent") || void 0;
|
|
2241
|
-
const ipAddress =
|
|
2392
|
+
const ipAddress = getClientIp(request);
|
|
2242
2393
|
const result = await client2.enforce({
|
|
2243
2394
|
headers: Object.fromEntries(request.headers.entries()),
|
|
2244
2395
|
userAgent,
|
|
@@ -2289,6 +2440,7 @@ function withAgentShield(config = {}) {
|
|
|
2289
2440
|
if (decision.isAgent && config.onAgentDetected) {
|
|
2290
2441
|
await config.onAgentDetected(request, decision);
|
|
2291
2442
|
}
|
|
2443
|
+
const redirectMode = config.redirectMode ?? "instruct";
|
|
2292
2444
|
switch (decision.action) {
|
|
2293
2445
|
case "block": {
|
|
2294
2446
|
if (config.customBlockedResponse) {
|
|
@@ -2297,10 +2449,15 @@ function withAgentShield(config = {}) {
|
|
|
2297
2449
|
if (config.onBlock === "redirect") {
|
|
2298
2450
|
return buildRedirectResponse(request, decision, config);
|
|
2299
2451
|
}
|
|
2300
|
-
return buildBlockedResponse(decision, config);
|
|
2452
|
+
return buildBlockedResponse(request, decision, config);
|
|
2301
2453
|
}
|
|
2302
|
-
case "redirect":
|
|
2303
|
-
|
|
2454
|
+
case "redirect":
|
|
2455
|
+
case "instruct": {
|
|
2456
|
+
if (redirectMode === "http" && decision.action === "redirect") {
|
|
2457
|
+
return buildRedirectResponse(request, decision, config);
|
|
2458
|
+
}
|
|
2459
|
+
const targetUrl = config.redirectUrl || decision.redirectUrl;
|
|
2460
|
+
return buildAgentInstructionResponse(request, decision, targetUrl);
|
|
2304
2461
|
}
|
|
2305
2462
|
case "challenge": {
|
|
2306
2463
|
return buildRedirectResponse(request, decision, config);
|
|
@@ -2310,10 +2467,10 @@ function withAgentShield(config = {}) {
|
|
|
2310
2467
|
default: {
|
|
2311
2468
|
const response = NextResponse.next();
|
|
2312
2469
|
if (decision.isAgent) {
|
|
2313
|
-
response.headers.set("
|
|
2314
|
-
response.headers.set("
|
|
2470
|
+
response.headers.set("KYA-Detected", "true");
|
|
2471
|
+
response.headers.set("KYA-Confidence", decision.confidence.toString());
|
|
2315
2472
|
if (decision.agentName) {
|
|
2316
|
-
response.headers.set("
|
|
2473
|
+
response.headers.set("KYA-Agent", decision.agentName);
|
|
2317
2474
|
}
|
|
2318
2475
|
}
|
|
2319
2476
|
return response;
|
|
@@ -2371,24 +2528,28 @@ function buildBlockedResponse2(decision, config) {
|
|
|
2371
2528
|
response.headers.set(key, value);
|
|
2372
2529
|
}
|
|
2373
2530
|
}
|
|
2374
|
-
response.headers.set("
|
|
2375
|
-
response.headers.set("
|
|
2376
|
-
response.headers.set("
|
|
2531
|
+
response.headers.set("KYA-Action", decision.action);
|
|
2532
|
+
response.headers.set("KYA-Reason", decision.reason);
|
|
2533
|
+
response.headers.set("KYA-Match-Type", decision.matchType);
|
|
2377
2534
|
return response;
|
|
2378
2535
|
}
|
|
2379
|
-
function buildRedirectResponse2(request, decision, config) {
|
|
2536
|
+
function buildRedirectResponse2(request, decision, config, detection) {
|
|
2380
2537
|
const redirectUrl = decision.redirectUrl || config.redirectUrl || "/blocked";
|
|
2381
2538
|
const url = new URL(redirectUrl, request.url);
|
|
2382
2539
|
url.searchParams.set("reason", decision.reason);
|
|
2383
2540
|
if (decision.ruleId) {
|
|
2384
2541
|
url.searchParams.set("ruleId", decision.ruleId);
|
|
2385
2542
|
}
|
|
2543
|
+
const agentName = detection?.detectedAgent?.name;
|
|
2544
|
+
if (agentName && !url.searchParams.has("agent")) {
|
|
2545
|
+
url.searchParams.set("agent", agentName.toLowerCase());
|
|
2546
|
+
}
|
|
2386
2547
|
return NextResponse.redirect(url);
|
|
2387
2548
|
}
|
|
2388
|
-
function buildChallengeResponse(request, decision, config) {
|
|
2389
|
-
return buildRedirectResponse2(request, decision, config);
|
|
2549
|
+
function buildChallengeResponse(request, decision, config, detection) {
|
|
2550
|
+
return buildRedirectResponse2(request, decision, config, detection);
|
|
2390
2551
|
}
|
|
2391
|
-
async function handlePolicyDecision(request, decision, config) {
|
|
2552
|
+
async function handlePolicyDecision(request, decision, config, detection) {
|
|
2392
2553
|
switch (decision.action) {
|
|
2393
2554
|
case ENFORCEMENT_ACTIONS.BLOCK:
|
|
2394
2555
|
if (config.customBlockedResponse) {
|
|
@@ -2396,9 +2557,9 @@ async function handlePolicyDecision(request, decision, config) {
|
|
|
2396
2557
|
}
|
|
2397
2558
|
return buildBlockedResponse2(decision, config);
|
|
2398
2559
|
case ENFORCEMENT_ACTIONS.REDIRECT:
|
|
2399
|
-
return buildRedirectResponse2(request, decision, config);
|
|
2560
|
+
return buildRedirectResponse2(request, decision, config, detection);
|
|
2400
2561
|
case ENFORCEMENT_ACTIONS.CHALLENGE:
|
|
2401
|
-
return buildChallengeResponse(request, decision, config);
|
|
2562
|
+
return buildChallengeResponse(request, decision, config, detection);
|
|
2402
2563
|
case ENFORCEMENT_ACTIONS.LOG:
|
|
2403
2564
|
console.log("[AgentShield] Policy decision (log):", {
|
|
2404
2565
|
path: request.nextUrl.pathname,
|
|
@@ -2472,7 +2633,7 @@ async function applyPolicy(request, detection, config) {
|
|
|
2472
2633
|
if (config.onPolicyDecision) {
|
|
2473
2634
|
await config.onPolicyDecision(request, decision, context);
|
|
2474
2635
|
}
|
|
2475
|
-
return await handlePolicyDecision(request, decision, config);
|
|
2636
|
+
return await handlePolicyDecision(request, decision, config, detection);
|
|
2476
2637
|
} catch (error) {
|
|
2477
2638
|
if (config.debug) {
|
|
2478
2639
|
console.error("[AgentShield] Policy evaluation error:", error);
|