capjs-core 0.1.6 → 0.1.7

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/src/agents.js ADDED
@@ -0,0 +1,700 @@
1
+ import {
2
+ createHash,
3
+ createPublicKey,
4
+ verify as cryptoVerify,
5
+ } from "node:crypto";
6
+
7
+ const RANK = { low: 1, medium: 2, high: 3 };
8
+
9
+ export const AGENT_ARTIFACTS = {
10
+ cic_active: {
11
+ agent: "claude-in-chrome",
12
+ confidence: "high",
13
+ detail: "Claude in Chrome agent overlay in the page",
14
+ },
15
+ cic_shared: {
16
+ agent: "claude-in-chrome",
17
+ confidence: "medium",
18
+ detail: "tab is shared with Claude in Chrome",
19
+ },
20
+ browser_use: {
21
+ agent: "browser-use",
22
+ confidence: "high",
23
+ detail: "browser-use element highlighting in the page",
24
+ },
25
+ cic_legacy: {
26
+ agent: "claude-in-chrome",
27
+ confidence: "medium",
28
+ detail: "older Claude in Chrome animation stylesheet in the page",
29
+ },
30
+ codex: {
31
+ agent: "codex",
32
+ confidence: "medium",
33
+ detail: "Codex agent overlay in the page",
34
+ },
35
+ browserflow: {
36
+ agent: "browserflow",
37
+ confidence: "medium",
38
+ detail: "Browserflow container in the page",
39
+ },
40
+ thunderbit: {
41
+ agent: "thunderbit",
42
+ confidence: "medium",
43
+ detail: "Thunderbit overlay or side bar in the page",
44
+ },
45
+ fellou: {
46
+ agent: "fellou",
47
+ confidence: "medium",
48
+ detail: "Fellou browser globals in the page",
49
+ },
50
+ stagehand: {
51
+ agent: "stagehand",
52
+ confidence: "high",
53
+ detail: "Stagehand v3 injected globals in the page",
54
+ },
55
+ twocaptcha: {
56
+ agent: "2captcha-solver",
57
+ confidence: "medium",
58
+ detail: "2Captcha solver extension helpers in the page",
59
+ },
60
+ comet: {
61
+ agent: "perplexity-comet",
62
+ confidence: "medium",
63
+ detail: "Perplexity Comet agent stylesheet or stop button in the page",
64
+ },
65
+ genspark: {
66
+ agent: "genspark",
67
+ confidence: "medium",
68
+ detail: "Genspark browser navigator property or agent float bar",
69
+ },
70
+ sigma: {
71
+ agent: "sigma",
72
+ confidence: "medium",
73
+ detail: "Sigma browser global in the page",
74
+ },
75
+ manus: {
76
+ agent: "manus",
77
+ confidence: "medium",
78
+ detail: "Manus browser operator global or action mask in the page",
79
+ },
80
+ antigravity: {
81
+ agent: "google-antigravity",
82
+ confidence: "medium",
83
+ detail: "Google Antigravity tab marker on the document",
84
+ },
85
+ cursor: {
86
+ agent: "cursor",
87
+ confidence: "medium",
88
+ detail: "Cursor browser agent global in the page",
89
+ },
90
+ skyvern: {
91
+ agent: "skyvern",
92
+ confidence: "medium",
93
+ detail: "Skyvern DOM markers or globals in the page",
94
+ },
95
+ rtrvr: {
96
+ agent: "rtrvr",
97
+ confidence: "high",
98
+ detail: "rtrvr.ai agent working the page",
99
+ },
100
+ rtrvr_installed: {
101
+ agent: "rtrvr",
102
+ confidence: "low",
103
+ detail: "rtrvr.ai extension installed",
104
+ },
105
+ harpa_installed: {
106
+ agent: "harpa",
107
+ confidence: "low",
108
+ detail: "HARPA AI extension installed",
109
+ },
110
+ monica: {
111
+ agent: "monica",
112
+ confidence: "high",
113
+ detail: "Monica agent browsing mask in the page",
114
+ },
115
+ monica_installed: {
116
+ agent: "monica",
117
+ confidence: "low",
118
+ detail: "Monica extension installed",
119
+ },
120
+ sider: {
121
+ agent: "sider",
122
+ confidence: "high",
123
+ detail: "Sider agent glow border in the page",
124
+ },
125
+ sider_installed: {
126
+ agent: "sider",
127
+ confidence: "low",
128
+ detail: "Sider extension installed",
129
+ },
130
+ maxai_installed: {
131
+ agent: "maxai",
132
+ confidence: "low",
133
+ detail: "MaxAI extension installed",
134
+ },
135
+ nova_act: {
136
+ agent: "nova-act",
137
+ confidence: "high",
138
+ detail: "Amazon Nova Act element ids in the page",
139
+ },
140
+ agentql: {
141
+ agent: "agentql",
142
+ confidence: "high",
143
+ detail: "AgentQL element ids in the page",
144
+ },
145
+ hyperagent: {
146
+ agent: "hyperagent",
147
+ confidence: "high",
148
+ detail: "HyperAgent globals in the page",
149
+ },
150
+ agent_browser: {
151
+ agent: "agent-browser",
152
+ confidence: "high",
153
+ detail: "agent-browser snapshot markers in the page",
154
+ },
155
+ do_browser: {
156
+ agent: "do-browser",
157
+ confidence: "high",
158
+ detail: "Do Browser agent glow in the page",
159
+ },
160
+ nanobrowser: {
161
+ agent: "nanobrowser",
162
+ confidence: "high",
163
+ detail: "Nanobrowser agent border in the page",
164
+ },
165
+ dassi: {
166
+ agent: "dassi",
167
+ confidence: "medium",
168
+ detail: "dassi agent overlay in the page",
169
+ },
170
+ kimi: {
171
+ agent: "kimi",
172
+ confidence: "high",
173
+ detail: "Kimi WebBridge agent visuals in the page",
174
+ },
175
+ magnitude: {
176
+ agent: "magnitude",
177
+ confidence: "medium",
178
+ detail: "Magnitude cursor and typing effects in the page",
179
+ },
180
+ lavague: {
181
+ agent: "lavague",
182
+ confidence: "medium",
183
+ detail: "LaVague element highlighting in the page",
184
+ },
185
+ kernel: {
186
+ agent: "kernel",
187
+ confidence: "medium",
188
+ detail: "Kernel browser event injection in the page",
189
+ },
190
+ };
191
+
192
+ export function agentFromCodes(codes) {
193
+ const found = [];
194
+ for (const code of codes ?? []) {
195
+ const a = Object.hasOwn(AGENT_ARTIFACTS, code) && AGENT_ARTIFACTS[code];
196
+ if (a)
197
+ found.push({
198
+ agent: a.agent,
199
+ confidence: a.confidence,
200
+ evidence: `artifact:${code}`,
201
+ });
202
+ }
203
+ return found;
204
+ }
205
+
206
+ const UA_TOKENS = [
207
+ [/\bClaude-Agent\/\d/, "claude-agent", "high"],
208
+ [/\bGoogle-Agent\b/, "google-agent", "high"],
209
+ [/\bManus-User\/\d/, "manus", "high"],
210
+ [/\bChatGPT-User\/\d/, "chatgpt-user", "high"],
211
+ [/\bGrokAgent\b/, "grok-bot", "medium"],
212
+ [/\bAgent-NovaAct\/\d/, "nova-act", "high"],
213
+ [/\bTwinAgent\/\d/, "twin", "high"],
214
+ [/\bCloudflareBrowserRenderingCrawler\/\d/, "cloudflare-browser-run", "high"],
215
+ ];
216
+
217
+ const SIGNATURE_AGENTS = {
218
+ "chatgpt.com": "chatgpt-agent",
219
+ "agent.bot.goog": "google-agent",
220
+ "api.manus.im": "manus",
221
+ "cursorusercontent.com": "grok-bot",
222
+ "www.browserbase.com": "browserbase",
223
+ "www.kernel.sh": "kernel",
224
+ "www.hyperbrowser.ai": "hyperbrowser",
225
+ "api.hyperbrowser.ai": "hyperbrowser",
226
+ "api.anchorbrowser.io": "anchor-browser",
227
+ "web-bot-auth.cloudflare-browser-rendering-085.workers.dev":
228
+ "cloudflare-browser-run",
229
+ "api.agi.tech": "agi-agent",
230
+ };
231
+
232
+ const SIGNATURE_AGENT_SUFFIXES = [
233
+ [/^[a-z0-9-]+\.keydirectory\.signer\.[a-z0-9-]+\.on\.aws$/, "aws-agentcore"],
234
+ ];
235
+
236
+ const signatureAgentFor = (host) =>
237
+ SIGNATURE_AGENTS[host] ??
238
+ SIGNATURE_AGENT_SUFFIXES.find(([re]) => re.test(host))?.[1] ??
239
+ null;
240
+
241
+ function header(headers, name) {
242
+ if (!headers) return null;
243
+ if (typeof headers.get === "function") return headers.get(name);
244
+ const lower = name.toLowerCase();
245
+ for (const [k, v] of Object.entries(headers)) {
246
+ if (k.toLowerCase() === lower) return Array.isArray(v) ? v.join(", ") : v;
247
+ }
248
+ return null;
249
+ }
250
+
251
+ export function parseSignatureAgent(value) {
252
+ if (!value || typeof value !== "string") return [];
253
+ const out = [];
254
+ const re = /(?:([a-z*][a-z0-9_.*-]*)=)?"(https:\/\/[^"]+)"/gi;
255
+ for (const m of value.matchAll(re))
256
+ out.push({ label: m[1] ?? null, url: m[2] });
257
+ return out;
258
+ }
259
+
260
+ function hostOf(url) {
261
+ try {
262
+ return new URL(url).host.toLowerCase();
263
+ } catch {
264
+ return null;
265
+ }
266
+ }
267
+
268
+ export function agentFromHeaders(headers) {
269
+ const found = [];
270
+ const ua = header(headers, "user-agent") || "";
271
+ for (const [re, agent, confidence] of UA_TOKENS) {
272
+ if (re.test(ua)) {
273
+ found.push({ agent, confidence, evidence: `ua:${agent}` });
274
+ }
275
+ }
276
+
277
+ const sa = header(headers, "signature-agent");
278
+ for (const { url } of parseSignatureAgent(sa)) {
279
+ const host = hostOf(url);
280
+ if (!host) continue;
281
+ found.push({
282
+ agent: signatureAgentFor(host) ?? `signed:${host}`,
283
+ confidence: "medium",
284
+ evidence: `signature_agent:${host}`,
285
+ signatureAgent: url,
286
+ });
287
+ }
288
+
289
+ const platform = (header(headers, "sec-ch-ua-platform") || "").replace(
290
+ /"/g,
291
+ "",
292
+ );
293
+ if (platform) {
294
+ const uaPlatform = /Windows/.test(ua)
295
+ ? "Windows"
296
+ : /Mac OS X|Macintosh/.test(ua)
297
+ ? "macOS"
298
+ : /Android/.test(ua)
299
+ ? "Android"
300
+ : /Linux|X11/.test(ua)
301
+ ? "Linux"
302
+ : null;
303
+ if (
304
+ uaPlatform &&
305
+ platform !== uaPlatform &&
306
+ !(uaPlatform === "Linux" && platform === "Chrome OS")
307
+ ) {
308
+ found.push({
309
+ agent: null,
310
+ confidence: "low",
311
+ evidence: `ua_platform_mismatch:${uaPlatform}/${platform}`,
312
+ });
313
+ }
314
+ }
315
+ return found;
316
+ }
317
+
318
+ const DIRECTORY_PATH = "/.well-known/http-message-signatures-directory";
319
+ const DIRECTORY_TTL_MS = 60 * 60 * 1000;
320
+ const directoryCache = new Map();
321
+
322
+ function okpThumbprint(jwk) {
323
+ const canon = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x });
324
+ return createHash("sha256").update(canon).digest("base64url");
325
+ }
326
+
327
+ async function loadDirectory(origin, fetchImpl, now) {
328
+ const hit = directoryCache.get(origin);
329
+ if (hit && now - hit.at < DIRECTORY_TTL_MS) return hit.keys;
330
+ const res = await fetchImpl(origin + DIRECTORY_PATH, {
331
+ headers: { accept: "application/http-message-signatures-directory+json" },
332
+ signal: AbortSignal.timeout?.(3000),
333
+ });
334
+ if (!res.ok) throw new Error(`directory ${res.status}`);
335
+ const body = await res.json();
336
+ const keys = new Map();
337
+ for (const jwk of body?.keys ?? []) {
338
+ if (jwk?.kty !== "OKP" || jwk?.crv !== "Ed25519" || !jwk.x) continue;
339
+ keys.set(jwk.kid || okpThumbprint(jwk), jwk);
340
+ keys.set(okpThumbprint(jwk), jwk);
341
+ }
342
+ directoryCache.set(origin, { at: now, keys });
343
+ return keys;
344
+ }
345
+
346
+ function splitDict(value) {
347
+ const out = new Map();
348
+ let depth = 0;
349
+ let quote = false;
350
+ let start = 0;
351
+ const push = (end) => {
352
+ const part = value.slice(start, end).trim();
353
+ const eq = part.indexOf("=");
354
+ if (eq > 0) out.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
355
+ };
356
+ for (let i = 0; i < value.length; i++) {
357
+ const c = value[i];
358
+ if (c === '"' && value[i - 1] !== "\\") quote = !quote;
359
+ else if (!quote && c === "(") depth++;
360
+ else if (!quote && c === ")") depth--;
361
+ else if (!quote && depth === 0 && c === ",") {
362
+ push(i);
363
+ start = i + 1;
364
+ }
365
+ }
366
+ push(value.length);
367
+ return out;
368
+ }
369
+
370
+ function parseParams(str) {
371
+ const params = {};
372
+ for (const m of str.matchAll(/;([a-z]+)=("(?:[^"\\]|\\.)*"|-?\d+)/g)) {
373
+ params[m[1]] = m[2].startsWith('"') ? m[2].slice(1, -1) : Number(m[2]);
374
+ }
375
+ return params;
376
+ }
377
+
378
+ function componentValue(name, req, url) {
379
+ switch (name) {
380
+ case "@method":
381
+ return req.method.toUpperCase();
382
+ case "@authority":
383
+ return (header(req.headers, "host") || url.host).toLowerCase();
384
+ case "@path":
385
+ return url.pathname;
386
+ case "@query":
387
+ return url.search || "?";
388
+ case "@target-uri":
389
+ return url.href;
390
+ case "@scheme":
391
+ return url.protocol.replace(":", "");
392
+ default: {
393
+ if (name.startsWith("@")) return null;
394
+ const v = header(req.headers, name);
395
+ return v === null || v === undefined ? null : String(v).trim();
396
+ }
397
+ }
398
+ }
399
+
400
+ export async function verifyWebBotAuth(req, opts = {}) {
401
+ const fetchImpl = opts.fetch ?? globalThis.fetch;
402
+ const nowMs = opts.now ?? Date.now();
403
+ const skew = opts.maxSkewSec ?? 60;
404
+ try {
405
+ const sa = parseSignatureAgent(header(req.headers, "signature-agent"));
406
+ const input = header(req.headers, "signature-input");
407
+ const sig = header(req.headers, "signature");
408
+ if (!sa.length || !input || !sig)
409
+ return { verified: false, reason: "unsigned" };
410
+ const url = new URL(req.url);
411
+ const inputs = splitDict(input);
412
+ const sigs = splitDict(sig);
413
+
414
+ for (const [label, def] of inputs) {
415
+ const params = parseParams(def.slice(def.lastIndexOf(")") + 1));
416
+ if (params.tag !== "web-bot-auth") continue;
417
+ if (params.alg && params.alg !== "ed25519") continue;
418
+ const nowSec = Math.floor(nowMs / 1000);
419
+ if (typeof params.created === "number" && params.created > nowSec + skew)
420
+ return { verified: false, reason: "created_in_future" };
421
+ if (typeof params.expires === "number" && params.expires < nowSec - skew)
422
+ return { verified: false, reason: "expired" };
423
+
424
+ const listStr = def.slice(def.indexOf("(") + 1, def.lastIndexOf(")"));
425
+ const comps = [
426
+ ...listStr.matchAll(
427
+ /"([^"]+)"((?:;[a-z]+(?:=(?:"[^"]*"|[^;\s"]+))?)*)/g,
428
+ ),
429
+ ];
430
+ if (!comps.some(([, n, rawParams]) => n === "@authority" && !rawParams))
431
+ return { verified: false, reason: "authority_not_signed" };
432
+ if (
433
+ opts.authority &&
434
+ componentValue("@authority", req, url)?.toLowerCase() !==
435
+ opts.authority.toLowerCase()
436
+ )
437
+ return { verified: false, reason: "wrong_authority" };
438
+ const lines = [];
439
+ for (const [, n, rawParams] of comps) {
440
+ let v = componentValue(n, req, url);
441
+ if (v === null)
442
+ return { verified: false, reason: `missing_component:${n}` };
443
+ const key = /;key="([^"]*)"/.exec(rawParams)?.[1];
444
+ if (key !== undefined) {
445
+ v = splitDict(v).get(key);
446
+ if (v === undefined)
447
+ return { verified: false, reason: `missing_member:${n}:${key}` };
448
+ }
449
+ lines.push(`"${n}"${rawParams}: ${v}`);
450
+ }
451
+ lines.push(`"@signature-params": ${def}`);
452
+ const base = Buffer.from(lines.join("\n"), "utf8");
453
+
454
+ const rawSig = sigs.get(label);
455
+ const m = rawSig && /^:([A-Za-z0-9+/=_-]+):$/.exec(rawSig);
456
+ if (!m) return { verified: false, reason: "malformed_signature" };
457
+ const signature = Buffer.from(m[1], "base64");
458
+
459
+ const agentUrl =
460
+ sa.find((s) => s.label === label)?.url ??
461
+ sa.find((s) => !s.label)?.url ??
462
+ sa[0].url;
463
+ const agent = new URL(agentUrl);
464
+ if (agent.protocol !== "https:" || agent.port)
465
+ return {
466
+ verified: false,
467
+ reason: "untrusted_signature_agent",
468
+ origin: agent.origin,
469
+ };
470
+ const origin = agent.origin;
471
+ const host = agent.host.toLowerCase();
472
+ if (
473
+ !signatureAgentFor(host) &&
474
+ !(opts.trustedOrigins ?? []).includes(origin)
475
+ ) {
476
+ return { verified: false, reason: "untrusted_signature_agent", origin };
477
+ }
478
+ const keys = await loadDirectory(origin, fetchImpl, nowMs);
479
+ const jwk = keys.get(params.keyid);
480
+ if (!jwk) return { verified: false, reason: "unknown_key", origin };
481
+ const key = createPublicKey({
482
+ key: { kty: "OKP", crv: "Ed25519", x: jwk.x },
483
+ format: "jwk",
484
+ });
485
+ if (cryptoVerify(null, base, key, signature)) {
486
+ if (typeof opts.nonceCheck === "function") {
487
+ if (!params.nonce)
488
+ return { verified: false, reason: "missing_nonce", origin };
489
+ const ttlMs = Math.max(
490
+ 1000,
491
+ ((params.expires ?? nowMs / 1000 + 300) - nowMs / 1000 + skew) *
492
+ 1000,
493
+ );
494
+ if (
495
+ (await opts.nonceCheck(`${origin} ${params.nonce}`, ttlMs)) !== true
496
+ ) {
497
+ return { verified: false, reason: "replayed_signature", origin };
498
+ }
499
+ }
500
+ return {
501
+ verified: true,
502
+ agent: signatureAgentFor(host) ?? `signed:${host}`,
503
+ origin,
504
+ keyid: params.keyid,
505
+ };
506
+ }
507
+ return { verified: false, reason: "bad_signature", origin };
508
+ }
509
+ return { verified: false, reason: "no_web_bot_auth_signature" };
510
+ } catch (err) {
511
+ return { verified: false, reason: `error:${err?.message ?? err}` };
512
+ }
513
+ }
514
+
515
+ export function clearAgentDirectoryCache() {
516
+ directoryCache.clear();
517
+ }
518
+
519
+ export function agentFromTrace(t) {
520
+ const found = [];
521
+
522
+ const a = t.a;
523
+ if (a && a.k === 0 && a.pt === 0 && a.u >= 0) {
524
+ const before = t.m.filter((p) => p[4] === 0 && p[0] <= a.t);
525
+ const last = before[before.length - 1];
526
+ const recent = before.filter((p) => p[0] > a.t - 1000);
527
+ const atPress = last && Math.hypot(last[1] - a.x, last[2] - a.y) < 1;
528
+ const warp = atPress && recent.length <= 2;
529
+ const gap = last ? a.t - last[0] : null;
530
+ const dwell = a.u - a.t;
531
+ const exactCenter = Math.abs(a.cx) < 0.01 && Math.abs(a.cy) < 0.01;
532
+ const intCoords = Number.isInteger(a.x) && Number.isInteger(a.y);
533
+ const fine = t.res <= 2;
534
+
535
+ if (warp && fine) {
536
+ if (gap < 6 && dwell < 4) {
537
+ found.push({
538
+ agent: "cdp-automation",
539
+ confidence: "medium",
540
+ evidence: "click:cdp_default",
541
+ });
542
+ } else if (
543
+ exactCenter &&
544
+ gap >= 35 &&
545
+ gap <= 90 &&
546
+ dwell >= 65 &&
547
+ dwell <= 130
548
+ ) {
549
+ found.push({
550
+ agent: "browser-use",
551
+ confidence: "medium",
552
+ evidence: "click:browser_use",
553
+ });
554
+ } else if (
555
+ intCoords &&
556
+ dwell >= 9 &&
557
+ dwell <= 30 &&
558
+ gap >= 80 &&
559
+ gap <= 900
560
+ ) {
561
+ found.push({
562
+ agent: "claude-in-chrome",
563
+ confidence: "medium",
564
+ evidence: "click:claude_in_chrome",
565
+ });
566
+ } else if (
567
+ intCoords &&
568
+ Math.abs(a.cx) <= 1 &&
569
+ Math.abs(a.cy) <= 1 &&
570
+ gap >= 4 &&
571
+ gap <= 15 &&
572
+ dwell >= 4 &&
573
+ dwell <= 15
574
+ ) {
575
+ found.push({
576
+ agent: "gemini-in-chrome",
577
+ confidence: "medium",
578
+ evidence: "click:chrome_actor",
579
+ });
580
+ } else if (exactCenter && a.w >= 16 && a.h >= 16) {
581
+ found.push({
582
+ agent: null,
583
+ confidence: "medium",
584
+ evidence: "click:exact_center_warp",
585
+ });
586
+ }
587
+ }
588
+ }
589
+
590
+ if (
591
+ a &&
592
+ a.k === 0 &&
593
+ a.pt === 0 &&
594
+ a.pr === 0 &&
595
+ a.u >= 0 &&
596
+ a.u - a.t < 1 &&
597
+ Math.abs(a.cx) <= 0.5 &&
598
+ Math.abs(a.cy) <= 0.5 &&
599
+ !t.m.some((p) => p[4] === 0 && p[0] <= a.t && p[0] > a.t - 1000)
600
+ ) {
601
+ found.push({
602
+ agent: null,
603
+ confidence: "low",
604
+ evidence: "click:ax_simulated",
605
+ });
606
+ }
607
+
608
+ if (t.res <= 2 && t.k.length >= 7) {
609
+ const iv = [];
610
+ const held = t.k.map((k) => k[1]).filter((d) => d >= 0);
611
+ for (let i = 1; i < t.k.length; i++) {
612
+ const d = t.k[i][0] - t.k[i - 1][0];
613
+ if (d >= 0 && d < 2000) iv.push(d);
614
+ }
615
+ const mid = (xs) => [...xs].sort((x, y) => x - y)[xs.length >> 1];
616
+ const spread = (xs) => {
617
+ const s = [...xs].sort((x, y) => x - y);
618
+ return s[Math.floor(s.length * 0.75)] - s[Math.floor(s.length * 0.25)];
619
+ };
620
+ if (
621
+ iv.length >= 6 &&
622
+ held.length >= 6 &&
623
+ mid(held) >= 20 &&
624
+ mid(held) <= 32 &&
625
+ mid(iv) >= 42 &&
626
+ mid(iv) <= 62 &&
627
+ spread(iv) <= 8 &&
628
+ spread(held) <= 6
629
+ ) {
630
+ found.push({
631
+ agent: "gemini-in-chrome",
632
+ confidence: "medium",
633
+ evidence: "typing:chrome_actor",
634
+ });
635
+ }
636
+ }
637
+
638
+ if ((t.n[10] ?? 0) >= 2) {
639
+ found.push({
640
+ agent: null,
641
+ confidence: "low",
642
+ evidence: "typing:phantom_shift",
643
+ });
644
+ }
645
+
646
+ if (t.res <= 2 && t.k.length >= 7) {
647
+ const iv = [];
648
+ for (let i = 1; i < t.k.length; i++) {
649
+ const d = t.k[i][0] - t.k[i - 1][0];
650
+ if (d >= 0 && d < 2000) iv.push(d);
651
+ }
652
+ iv.sort((x, y) => x - y);
653
+ if (iv.length >= 6 && iv[iv.length >> 1] < 25) {
654
+ found.push({
655
+ agent: null,
656
+ confidence: "medium",
657
+ evidence: "typing:superhuman",
658
+ });
659
+ }
660
+ }
661
+
662
+ if ((t.n[8] ?? 0) > 0) {
663
+ found.push({
664
+ agent: null,
665
+ confidence: "low",
666
+ evidence: "typing:keyless_insert",
667
+ });
668
+ }
669
+ if ((t.n[9] ?? 0) > 0) {
670
+ found.push({ agent: null, confidence: "low", evidence: "input:untrusted" });
671
+ }
672
+ return found;
673
+ }
674
+
675
+ export function summarizeAgent(found, verification) {
676
+ if (verification?.verified) {
677
+ found = [
678
+ {
679
+ agent: verification.agent,
680
+ confidence: "high",
681
+ evidence: `web_bot_auth:${verification.origin}`,
682
+ },
683
+ ...found.filter((f) => !f.evidence.startsWith("signature_agent:")),
684
+ ];
685
+ }
686
+ if (!found.length) return null;
687
+ let best = 0;
688
+ for (const f of found) best = Math.max(best, RANK[f.confidence]);
689
+ const named = [...found]
690
+ .filter((f) => f.agent)
691
+ .sort((x, y) => RANK[y.confidence] - RANK[x.confidence])[0];
692
+ const confidence = Object.keys(RANK).find((k) => RANK[k] === best);
693
+ return {
694
+ detected: !!named && RANK[named.confidence] >= RANK.medium,
695
+ name: named?.agent ?? null,
696
+ confidence,
697
+ verified: verification?.verified === true,
698
+ evidence: found.map((f) => f.evidence),
699
+ };
700
+ }