@surbee/cipher 0.1.0 → 0.2.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/README.md +95 -0
- package/dist/checks/index.d.mts +215 -0
- package/dist/checks/index.d.ts +215 -0
- package/dist/checks/index.js +1157 -0
- package/dist/checks/index.mjs +60 -0
- package/dist/chunk-P2MIOVFQ.mjs +1104 -0
- package/dist/index.d.mts +38 -244
- package/dist/index.d.ts +38 -244
- package/dist/index.js +1716 -35
- package/dist/index.mjs +649 -35
- package/dist/types-C8t_T3bP.d.mts +251 -0
- package/dist/types-C8t_T3bP.d.ts +251 -0
- package/package.json +16 -4
- package/src/checks/behavioral.ts +0 -527
- package/src/checks/content.ts +0 -372
- package/src/checks/device.ts +0 -384
- package/src/checks/index.ts +0 -59
- package/src/checks/timing.ts +0 -256
- package/src/cipher.ts +0 -225
- package/src/index.ts +0 -75
- package/src/tiers.ts +0 -507
- package/src/types.ts +0 -366
- package/test/cipher.test.ts +0 -245
- package/test/fixtures.ts +0 -627
- package/tsconfig.json +0 -20
package/dist/index.mjs
CHANGED
|
@@ -1,64 +1,625 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import {
|
|
2
|
+
runBehavioralChecks,
|
|
3
|
+
runContentChecks,
|
|
4
|
+
runDeviceChecks,
|
|
5
|
+
runTimingChecks
|
|
6
|
+
} from "./chunk-P2MIOVFQ.mjs";
|
|
7
|
+
|
|
8
|
+
// src/tiers.ts
|
|
9
|
+
var CHECKS = {
|
|
10
|
+
// ============================================
|
|
11
|
+
// TIER 1 - Basic Behavioral (Offline)
|
|
12
|
+
// ============================================
|
|
13
|
+
rapid_completion: {
|
|
14
|
+
id: "rapid_completion",
|
|
15
|
+
name: "Rapid Completion",
|
|
16
|
+
description: "Detects impossibly fast survey completion",
|
|
17
|
+
tier: 1,
|
|
18
|
+
category: "behavioral",
|
|
19
|
+
offline: true
|
|
7
20
|
},
|
|
8
|
-
|
|
9
|
-
|
|
21
|
+
uniform_timing: {
|
|
22
|
+
id: "uniform_timing",
|
|
23
|
+
name: "Uniform Timing",
|
|
24
|
+
description: "Detects robotic consistent response times",
|
|
25
|
+
tier: 1,
|
|
26
|
+
category: "behavioral",
|
|
27
|
+
offline: true
|
|
28
|
+
},
|
|
29
|
+
low_interaction: {
|
|
30
|
+
id: "low_interaction",
|
|
31
|
+
name: "Low Interaction",
|
|
32
|
+
description: "Detects minimal mouse/keyboard activity",
|
|
33
|
+
tier: 1,
|
|
34
|
+
category: "behavioral",
|
|
35
|
+
offline: true
|
|
36
|
+
},
|
|
37
|
+
straight_line_answers: {
|
|
38
|
+
id: "straight_line_answers",
|
|
39
|
+
name: "Straight-Lining",
|
|
40
|
+
description: "Detects selecting same option repeatedly",
|
|
41
|
+
tier: 1,
|
|
42
|
+
category: "content",
|
|
43
|
+
offline: true
|
|
44
|
+
},
|
|
45
|
+
impossibly_fast: {
|
|
46
|
+
id: "impossibly_fast",
|
|
47
|
+
name: "Speed Reading",
|
|
48
|
+
description: "Detects reading faster than humanly possible",
|
|
49
|
+
tier: 1,
|
|
50
|
+
category: "timing",
|
|
51
|
+
offline: true
|
|
52
|
+
},
|
|
53
|
+
minimal_effort: {
|
|
54
|
+
id: "minimal_effort",
|
|
55
|
+
name: "Minimal Effort",
|
|
56
|
+
description: "Detects very short or low-quality text responses",
|
|
57
|
+
tier: 1,
|
|
58
|
+
category: "content",
|
|
59
|
+
offline: true
|
|
60
|
+
},
|
|
61
|
+
// ============================================
|
|
62
|
+
// TIER 2 - Device/Automation (Offline)
|
|
63
|
+
// ============================================
|
|
64
|
+
excessive_paste: {
|
|
65
|
+
id: "excessive_paste",
|
|
66
|
+
name: "Excessive Paste",
|
|
67
|
+
description: "Detects heavy copy-paste behavior",
|
|
68
|
+
tier: 2,
|
|
69
|
+
category: "behavioral",
|
|
70
|
+
offline: true
|
|
71
|
+
},
|
|
72
|
+
pointer_spikes: {
|
|
73
|
+
id: "pointer_spikes",
|
|
74
|
+
name: "Pointer Velocity Spikes",
|
|
75
|
+
description: "Detects unnatural mouse movement patterns",
|
|
76
|
+
tier: 2,
|
|
77
|
+
category: "behavioral",
|
|
78
|
+
offline: true
|
|
79
|
+
},
|
|
80
|
+
webdriver_detected: {
|
|
81
|
+
id: "webdriver_detected",
|
|
82
|
+
name: "WebDriver Detection",
|
|
83
|
+
description: "Detects Selenium/automation tools",
|
|
84
|
+
tier: 2,
|
|
85
|
+
category: "device",
|
|
86
|
+
offline: true
|
|
87
|
+
},
|
|
88
|
+
automation_detected: {
|
|
89
|
+
id: "automation_detected",
|
|
90
|
+
name: "Automation Detection",
|
|
91
|
+
description: "Detects headless browsers and bots",
|
|
92
|
+
tier: 2,
|
|
93
|
+
category: "device",
|
|
94
|
+
offline: true
|
|
95
|
+
},
|
|
96
|
+
no_plugins: {
|
|
97
|
+
id: "no_plugins",
|
|
98
|
+
name: "Missing Plugins",
|
|
99
|
+
description: "Detects suspicious browser configurations",
|
|
100
|
+
tier: 2,
|
|
101
|
+
category: "device",
|
|
102
|
+
offline: true
|
|
103
|
+
},
|
|
104
|
+
suspicious_user_agent: {
|
|
105
|
+
id: "suspicious_user_agent",
|
|
106
|
+
name: "Suspicious User Agent",
|
|
107
|
+
description: "Detects bot-like user agent strings",
|
|
108
|
+
tier: 2,
|
|
109
|
+
category: "device",
|
|
110
|
+
offline: true
|
|
111
|
+
},
|
|
112
|
+
device_fingerprint_mismatch: {
|
|
113
|
+
id: "device_fingerprint_mismatch",
|
|
114
|
+
name: "Device Mismatch",
|
|
115
|
+
description: "Detects inconsistent device characteristics",
|
|
116
|
+
tier: 2,
|
|
117
|
+
category: "device",
|
|
118
|
+
offline: true
|
|
119
|
+
},
|
|
120
|
+
screen_anomaly: {
|
|
121
|
+
id: "screen_anomaly",
|
|
122
|
+
name: "Screen Anomaly",
|
|
123
|
+
description: "Detects impossible screen dimensions",
|
|
124
|
+
tier: 2,
|
|
125
|
+
category: "device",
|
|
126
|
+
offline: true
|
|
127
|
+
},
|
|
128
|
+
suspicious_pauses: {
|
|
129
|
+
id: "suspicious_pauses",
|
|
130
|
+
name: "Suspicious Pauses",
|
|
131
|
+
description: "Detects unusual gaps in activity",
|
|
132
|
+
tier: 2,
|
|
133
|
+
category: "timing",
|
|
134
|
+
offline: true
|
|
135
|
+
},
|
|
136
|
+
// ============================================
|
|
137
|
+
// TIER 3 - Enhanced Behavioral + Light AI
|
|
138
|
+
// ============================================
|
|
139
|
+
robotic_typing: {
|
|
140
|
+
id: "robotic_typing",
|
|
141
|
+
name: "Robotic Typing",
|
|
142
|
+
description: "Detects uniform keystroke timing",
|
|
143
|
+
tier: 3,
|
|
144
|
+
category: "behavioral",
|
|
145
|
+
offline: true
|
|
146
|
+
},
|
|
147
|
+
mouse_teleporting: {
|
|
148
|
+
id: "mouse_teleporting",
|
|
149
|
+
name: "Mouse Teleporting",
|
|
150
|
+
description: "Detects large instant mouse jumps",
|
|
151
|
+
tier: 3,
|
|
152
|
+
category: "behavioral",
|
|
153
|
+
offline: true
|
|
154
|
+
},
|
|
155
|
+
no_corrections: {
|
|
156
|
+
id: "no_corrections",
|
|
157
|
+
name: "No Corrections",
|
|
158
|
+
description: "Detects perfect typing with no backspaces",
|
|
159
|
+
tier: 3,
|
|
160
|
+
category: "behavioral",
|
|
161
|
+
offline: true
|
|
162
|
+
},
|
|
163
|
+
excessive_tab_switching: {
|
|
164
|
+
id: "excessive_tab_switching",
|
|
165
|
+
name: "Tab Switching",
|
|
166
|
+
description: "Detects frequent tab/window changes",
|
|
167
|
+
tier: 3,
|
|
168
|
+
category: "content",
|
|
169
|
+
offline: true
|
|
170
|
+
},
|
|
171
|
+
window_focus_loss: {
|
|
172
|
+
id: "window_focus_loss",
|
|
173
|
+
name: "Focus Loss",
|
|
174
|
+
description: "Detects extended periods away from survey",
|
|
175
|
+
tier: 3,
|
|
176
|
+
category: "content",
|
|
177
|
+
offline: true
|
|
178
|
+
},
|
|
179
|
+
ai_content_basic: {
|
|
180
|
+
id: "ai_content_basic",
|
|
181
|
+
name: "AI Content (Basic)",
|
|
182
|
+
description: "Light AI-generated text detection",
|
|
183
|
+
tier: 3,
|
|
184
|
+
category: "ai",
|
|
185
|
+
offline: false
|
|
186
|
+
},
|
|
187
|
+
contradiction_basic: {
|
|
188
|
+
id: "contradiction_basic",
|
|
189
|
+
name: "Contradiction (Basic)",
|
|
190
|
+
description: "Basic response consistency check",
|
|
191
|
+
tier: 3,
|
|
192
|
+
category: "ai",
|
|
193
|
+
offline: false
|
|
194
|
+
},
|
|
195
|
+
// ============================================
|
|
196
|
+
// TIER 4 - Advanced Analysis
|
|
197
|
+
// ============================================
|
|
198
|
+
hover_behavior: {
|
|
199
|
+
id: "hover_behavior",
|
|
200
|
+
name: "Hover Patterns",
|
|
201
|
+
description: "Analyzes mouse hover behavior before clicks",
|
|
202
|
+
tier: 4,
|
|
203
|
+
category: "behavioral",
|
|
204
|
+
offline: true
|
|
205
|
+
},
|
|
206
|
+
scroll_patterns: {
|
|
207
|
+
id: "scroll_patterns",
|
|
208
|
+
name: "Scroll Patterns",
|
|
209
|
+
description: "Analyzes reading/scrolling behavior",
|
|
210
|
+
tier: 4,
|
|
211
|
+
category: "behavioral",
|
|
212
|
+
offline: true
|
|
213
|
+
},
|
|
214
|
+
mouse_acceleration: {
|
|
215
|
+
id: "mouse_acceleration",
|
|
216
|
+
name: "Mouse Acceleration",
|
|
217
|
+
description: "Analyzes natural mouse acceleration",
|
|
218
|
+
tier: 4,
|
|
219
|
+
category: "behavioral",
|
|
220
|
+
offline: true
|
|
221
|
+
},
|
|
222
|
+
vpn_detection: {
|
|
223
|
+
id: "vpn_detection",
|
|
224
|
+
name: "VPN Detection",
|
|
225
|
+
description: "Detects VPN/proxy usage",
|
|
226
|
+
tier: 4,
|
|
227
|
+
category: "network",
|
|
228
|
+
offline: false
|
|
229
|
+
},
|
|
230
|
+
datacenter_ip: {
|
|
231
|
+
id: "datacenter_ip",
|
|
232
|
+
name: "Datacenter IP",
|
|
233
|
+
description: "Detects cloud/datacenter IPs",
|
|
234
|
+
tier: 4,
|
|
235
|
+
category: "network",
|
|
236
|
+
offline: false
|
|
237
|
+
},
|
|
238
|
+
plagiarism_basic: {
|
|
239
|
+
id: "plagiarism_basic",
|
|
240
|
+
name: "Plagiarism (Basic)",
|
|
241
|
+
description: "Quick check for copied content",
|
|
242
|
+
tier: 4,
|
|
243
|
+
category: "ai",
|
|
244
|
+
offline: false
|
|
245
|
+
},
|
|
246
|
+
quality_assessment: {
|
|
247
|
+
id: "quality_assessment",
|
|
248
|
+
name: "Quality Assessment",
|
|
249
|
+
description: "AI assessment of response quality",
|
|
250
|
+
tier: 4,
|
|
251
|
+
category: "ai",
|
|
252
|
+
offline: false
|
|
253
|
+
},
|
|
254
|
+
semantic_analysis: {
|
|
255
|
+
id: "semantic_analysis",
|
|
256
|
+
name: "Semantic Analysis",
|
|
257
|
+
description: "AI analysis of response meaning",
|
|
258
|
+
tier: 4,
|
|
259
|
+
category: "ai",
|
|
260
|
+
offline: false
|
|
261
|
+
},
|
|
262
|
+
// ============================================
|
|
263
|
+
// TIER 5 - Maximum (Opus 4.5)
|
|
264
|
+
// ============================================
|
|
265
|
+
ai_content_full: {
|
|
266
|
+
id: "ai_content_full",
|
|
267
|
+
name: "AI Content (Full)",
|
|
268
|
+
description: "Comprehensive AI-generated text detection",
|
|
269
|
+
tier: 5,
|
|
270
|
+
category: "ai",
|
|
271
|
+
offline: false
|
|
272
|
+
},
|
|
273
|
+
contradiction_full: {
|
|
274
|
+
id: "contradiction_full",
|
|
275
|
+
name: "Contradiction (Full)",
|
|
276
|
+
description: "Deep semantic contradiction analysis",
|
|
277
|
+
tier: 5,
|
|
278
|
+
category: "ai",
|
|
279
|
+
offline: false
|
|
280
|
+
},
|
|
281
|
+
plagiarism_full: {
|
|
282
|
+
id: "plagiarism_full",
|
|
283
|
+
name: "Plagiarism (Full)",
|
|
284
|
+
description: "Comprehensive plagiarism detection",
|
|
285
|
+
tier: 5,
|
|
286
|
+
category: "ai",
|
|
287
|
+
offline: false
|
|
288
|
+
},
|
|
289
|
+
fraud_ring_detection: {
|
|
290
|
+
id: "fraud_ring_detection",
|
|
291
|
+
name: "Fraud Ring",
|
|
292
|
+
description: "Detects coordinated fraud attempts",
|
|
293
|
+
tier: 5,
|
|
294
|
+
category: "ai",
|
|
295
|
+
offline: false
|
|
296
|
+
},
|
|
297
|
+
answer_sharing: {
|
|
298
|
+
id: "answer_sharing",
|
|
299
|
+
name: "Answer Sharing",
|
|
300
|
+
description: "Detects identical answers across respondents",
|
|
301
|
+
tier: 5,
|
|
302
|
+
category: "ai",
|
|
303
|
+
offline: false
|
|
304
|
+
},
|
|
305
|
+
coordinated_timing: {
|
|
306
|
+
id: "coordinated_timing",
|
|
307
|
+
name: "Coordinated Timing",
|
|
308
|
+
description: "Detects synchronized submissions",
|
|
309
|
+
tier: 5,
|
|
310
|
+
category: "ai",
|
|
311
|
+
offline: false
|
|
312
|
+
},
|
|
313
|
+
device_sharing: {
|
|
314
|
+
id: "device_sharing",
|
|
315
|
+
name: "Device Sharing",
|
|
316
|
+
description: "Detects same device across respondents",
|
|
317
|
+
tier: 5,
|
|
318
|
+
category: "ai",
|
|
319
|
+
offline: false
|
|
320
|
+
},
|
|
321
|
+
tor_detection: {
|
|
322
|
+
id: "tor_detection",
|
|
323
|
+
name: "Tor Detection",
|
|
324
|
+
description: "Detects Tor exit node IPs",
|
|
325
|
+
tier: 5,
|
|
326
|
+
category: "network",
|
|
327
|
+
offline: false
|
|
328
|
+
},
|
|
329
|
+
proxy_detection: {
|
|
330
|
+
id: "proxy_detection",
|
|
331
|
+
name: "Proxy Detection",
|
|
332
|
+
description: "Detects proxy server usage",
|
|
333
|
+
tier: 5,
|
|
334
|
+
category: "network",
|
|
335
|
+
offline: false
|
|
336
|
+
},
|
|
337
|
+
timezone_validation: {
|
|
338
|
+
id: "timezone_validation",
|
|
339
|
+
name: "Timezone Validation",
|
|
340
|
+
description: "Validates timezone consistency",
|
|
341
|
+
tier: 5,
|
|
342
|
+
category: "network",
|
|
343
|
+
offline: true
|
|
344
|
+
},
|
|
345
|
+
baseline_deviation: {
|
|
346
|
+
id: "baseline_deviation",
|
|
347
|
+
name: "Baseline Deviation",
|
|
348
|
+
description: "Compares against established behavioral baseline",
|
|
349
|
+
tier: 5,
|
|
350
|
+
category: "ai",
|
|
351
|
+
offline: false
|
|
352
|
+
},
|
|
353
|
+
perplexity_analysis: {
|
|
354
|
+
id: "perplexity_analysis",
|
|
355
|
+
name: "Perplexity Analysis",
|
|
356
|
+
description: "Statistical text predictability analysis",
|
|
357
|
+
tier: 5,
|
|
358
|
+
category: "ai",
|
|
359
|
+
offline: false
|
|
360
|
+
},
|
|
361
|
+
burstiness_analysis: {
|
|
362
|
+
id: "burstiness_analysis",
|
|
363
|
+
name: "Burstiness Analysis",
|
|
364
|
+
description: "Sentence length variation analysis",
|
|
365
|
+
tier: 5,
|
|
366
|
+
category: "ai",
|
|
367
|
+
offline: false
|
|
368
|
+
}
|
|
10
369
|
};
|
|
11
|
-
var
|
|
370
|
+
var TIERS = {
|
|
12
371
|
1: {
|
|
13
372
|
name: "Basic",
|
|
14
|
-
description: "Essential fraud detection with behavioral heuristics",
|
|
15
|
-
|
|
373
|
+
description: "Essential fraud detection with behavioral heuristics. Free, fully offline.",
|
|
374
|
+
checks: {
|
|
375
|
+
behavioral: ["rapid_completion", "uniform_timing", "low_interaction"],
|
|
376
|
+
timing: ["impossibly_fast"],
|
|
377
|
+
device: [],
|
|
378
|
+
content: ["straight_line_answers", "minimal_effort"],
|
|
379
|
+
network: [],
|
|
380
|
+
ai: []
|
|
381
|
+
},
|
|
382
|
+
aiModel: null,
|
|
383
|
+
offline: true,
|
|
384
|
+
estimatedCostPerResponse: 0
|
|
16
385
|
},
|
|
17
386
|
2: {
|
|
18
387
|
name: "Standard",
|
|
19
|
-
description: "Adds device fingerprinting and automation detection",
|
|
20
|
-
|
|
388
|
+
description: "Adds device fingerprinting and automation detection. Free, fully offline.",
|
|
389
|
+
checks: {
|
|
390
|
+
behavioral: ["rapid_completion", "uniform_timing", "low_interaction", "excessive_paste", "pointer_spikes"],
|
|
391
|
+
timing: ["impossibly_fast", "suspicious_pauses"],
|
|
392
|
+
device: ["webdriver_detected", "automation_detected", "no_plugins", "suspicious_user_agent", "device_fingerprint_mismatch", "screen_anomaly"],
|
|
393
|
+
content: ["straight_line_answers", "minimal_effort"],
|
|
394
|
+
network: [],
|
|
395
|
+
ai: []
|
|
396
|
+
},
|
|
397
|
+
aiModel: null,
|
|
398
|
+
offline: true,
|
|
399
|
+
estimatedCostPerResponse: 0
|
|
21
400
|
},
|
|
22
401
|
3: {
|
|
23
402
|
name: "Enhanced",
|
|
24
|
-
description: "Adds
|
|
25
|
-
|
|
403
|
+
description: "Adds Claude Sonnet 4.5 for content quality analysis. ~$0.003 per response.",
|
|
404
|
+
checks: {
|
|
405
|
+
behavioral: ["rapid_completion", "uniform_timing", "low_interaction", "excessive_paste", "pointer_spikes", "robotic_typing", "mouse_teleporting", "no_corrections"],
|
|
406
|
+
timing: ["impossibly_fast", "suspicious_pauses"],
|
|
407
|
+
device: ["webdriver_detected", "automation_detected", "no_plugins", "suspicious_user_agent", "device_fingerprint_mismatch", "screen_anomaly"],
|
|
408
|
+
content: ["straight_line_answers", "minimal_effort", "excessive_tab_switching", "window_focus_loss"],
|
|
409
|
+
network: [],
|
|
410
|
+
ai: ["ai_content_basic", "contradiction_basic"]
|
|
411
|
+
},
|
|
412
|
+
aiModel: "claude-sonnet-4-5-20250514",
|
|
413
|
+
offline: false,
|
|
414
|
+
estimatedCostPerResponse: 3e-3
|
|
26
415
|
},
|
|
27
416
|
4: {
|
|
28
417
|
name: "Advanced",
|
|
29
|
-
description: "Full behavioral analysis
|
|
30
|
-
|
|
418
|
+
description: "Full behavioral analysis + semantic AI. ~$0.01 per response.",
|
|
419
|
+
checks: {
|
|
420
|
+
behavioral: ["rapid_completion", "uniform_timing", "low_interaction", "excessive_paste", "pointer_spikes", "robotic_typing", "mouse_teleporting", "no_corrections", "hover_behavior", "scroll_patterns", "mouse_acceleration"],
|
|
421
|
+
timing: ["impossibly_fast", "suspicious_pauses"],
|
|
422
|
+
device: ["webdriver_detected", "automation_detected", "no_plugins", "suspicious_user_agent", "device_fingerprint_mismatch", "screen_anomaly"],
|
|
423
|
+
content: ["straight_line_answers", "minimal_effort", "excessive_tab_switching", "window_focus_loss"],
|
|
424
|
+
network: ["vpn_detection", "datacenter_ip"],
|
|
425
|
+
ai: ["ai_content_basic", "contradiction_basic", "plagiarism_basic", "quality_assessment", "semantic_analysis"]
|
|
426
|
+
},
|
|
427
|
+
aiModel: "claude-sonnet-4-5-20250514",
|
|
428
|
+
offline: false,
|
|
429
|
+
estimatedCostPerResponse: 0.01
|
|
31
430
|
},
|
|
32
431
|
5: {
|
|
33
432
|
name: "Maximum",
|
|
34
|
-
description: "All
|
|
35
|
-
|
|
433
|
+
description: "All 40+ checks with Claude Opus 4.5. Maximum accuracy. ~$0.05 per response.",
|
|
434
|
+
checks: {
|
|
435
|
+
behavioral: ["rapid_completion", "uniform_timing", "low_interaction", "excessive_paste", "pointer_spikes", "robotic_typing", "mouse_teleporting", "no_corrections", "hover_behavior", "scroll_patterns", "mouse_acceleration"],
|
|
436
|
+
timing: ["impossibly_fast", "suspicious_pauses"],
|
|
437
|
+
device: ["webdriver_detected", "automation_detected", "no_plugins", "suspicious_user_agent", "device_fingerprint_mismatch", "screen_anomaly"],
|
|
438
|
+
content: ["straight_line_answers", "minimal_effort", "excessive_tab_switching", "window_focus_loss"],
|
|
439
|
+
network: ["vpn_detection", "datacenter_ip", "tor_detection", "proxy_detection", "timezone_validation"],
|
|
440
|
+
ai: ["ai_content_full", "contradiction_full", "plagiarism_full", "quality_assessment", "semantic_analysis", "fraud_ring_detection", "answer_sharing", "coordinated_timing", "device_sharing", "baseline_deviation", "perplexity_analysis", "burstiness_analysis"]
|
|
441
|
+
},
|
|
442
|
+
aiModel: "claude-opus-4-5-20250514",
|
|
443
|
+
offline: false,
|
|
444
|
+
estimatedCostPerResponse: 0.05
|
|
36
445
|
}
|
|
37
446
|
};
|
|
447
|
+
function getChecksForTier(tier) {
|
|
448
|
+
const config = TIERS[tier];
|
|
449
|
+
return [
|
|
450
|
+
...config.checks.behavioral,
|
|
451
|
+
...config.checks.timing,
|
|
452
|
+
...config.checks.device,
|
|
453
|
+
...config.checks.content,
|
|
454
|
+
...config.checks.network,
|
|
455
|
+
...config.checks.ai
|
|
456
|
+
];
|
|
457
|
+
}
|
|
458
|
+
function getOfflineChecksForTier(tier) {
|
|
459
|
+
return getChecksForTier(tier).filter((id) => CHECKS[id].offline);
|
|
460
|
+
}
|
|
461
|
+
function estimateCost(tier, responses) {
|
|
462
|
+
return TIERS[tier].estimatedCostPerResponse * responses;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/scoring.ts
|
|
466
|
+
function checkName(checkId) {
|
|
467
|
+
return CHECKS[checkId]?.name ?? checkId;
|
|
468
|
+
}
|
|
469
|
+
function clamp01(n) {
|
|
470
|
+
return Math.max(0, Math.min(1, n));
|
|
471
|
+
}
|
|
472
|
+
function noisyOr(scores) {
|
|
473
|
+
return 1 - scores.reduce((product, s) => product * (1 - clamp01(s)), 1);
|
|
474
|
+
}
|
|
475
|
+
function offlineRequestId() {
|
|
476
|
+
return `req_local_${Math.random().toString(36).slice(2, 12)}`;
|
|
477
|
+
}
|
|
478
|
+
function buildSummary(checks, score, recommendation) {
|
|
479
|
+
const failed = checks.filter((c) => !c.passed);
|
|
480
|
+
let verdict;
|
|
481
|
+
if (score >= 0.85) verdict = "High-quality legitimate response";
|
|
482
|
+
else if (score >= 0.7) verdict = "Likely legitimate with minor concerns";
|
|
483
|
+
else if (score >= 0.5) verdict = "Borderline quality \u2014 manual review recommended";
|
|
484
|
+
else if (score >= 0.3) verdict = "Multiple issues detected \u2014 likely low quality";
|
|
485
|
+
else verdict = "Suspected fraudulent or bot response";
|
|
486
|
+
const issues = failed.map((c) => c.details ?? checkName(c.checkId));
|
|
487
|
+
const positives = [];
|
|
488
|
+
const failedIds = new Set(failed.map((c) => c.checkId));
|
|
489
|
+
if (![...failedIds].some((id) => id.includes("timing") || id === "rapid_completion" || id === "impossibly_fast")) {
|
|
490
|
+
positives.push("Response timing appears natural");
|
|
491
|
+
}
|
|
492
|
+
if (![...failedIds].some((id) => id.includes("automation") || id === "webdriver_detected")) {
|
|
493
|
+
positives.push("No automation tools detected");
|
|
494
|
+
}
|
|
495
|
+
if (![...failedIds].some((id) => id.includes("ai_content"))) {
|
|
496
|
+
positives.push("Content appears human-written");
|
|
497
|
+
}
|
|
498
|
+
if (!failedIds.has("minimal_effort")) {
|
|
499
|
+
positives.push("Response shows reasonable effort");
|
|
500
|
+
}
|
|
501
|
+
let suggestion;
|
|
502
|
+
if (recommendation === "keep") suggestion = "Response can be accepted as-is";
|
|
503
|
+
else if (recommendation === "review") suggestion = "Consider manual review before accepting";
|
|
504
|
+
else suggestion = "Response should be rejected or flagged for investigation";
|
|
505
|
+
return { verdict, issues, positives, suggestion };
|
|
506
|
+
}
|
|
507
|
+
function buildResult(checks, tier, thresholds, processingTimeMs) {
|
|
508
|
+
const failedChecks = checks.filter((c) => !c.passed);
|
|
509
|
+
const passedChecks = checks.filter((c) => c.passed);
|
|
510
|
+
const hardSuspicion = noisyOr(failedChecks.map((c) => c.score));
|
|
511
|
+
const softMean = checks.length > 0 ? passedChecks.reduce((sum, c) => sum + c.score, 0) / checks.length : 0;
|
|
512
|
+
const softSuspicion = Math.min(0.35, softMean);
|
|
513
|
+
const suspicion = 1 - (1 - hardSuspicion) * (1 - softSuspicion);
|
|
514
|
+
const score = clamp01(1 - suspicion);
|
|
515
|
+
const passed = score >= thresholds.fail;
|
|
516
|
+
let recommendation;
|
|
517
|
+
if (score >= thresholds.review) recommendation = "keep";
|
|
518
|
+
else if (score >= thresholds.fail) recommendation = "review";
|
|
519
|
+
else recommendation = "discard";
|
|
520
|
+
const flags = failedChecks.map((c) => checkName(c.checkId));
|
|
521
|
+
const summary = buildSummary(checks, score, recommendation);
|
|
522
|
+
const strongestSignal = checks.reduce((max, c) => Math.max(max, c.score), 0);
|
|
523
|
+
const confidence = clamp01(0.5 + checks.length * 0.015 + strongestSignal * 0.2);
|
|
524
|
+
return {
|
|
525
|
+
score,
|
|
526
|
+
passed,
|
|
527
|
+
recommendation,
|
|
528
|
+
confidence,
|
|
529
|
+
flags,
|
|
530
|
+
summary,
|
|
531
|
+
checks,
|
|
532
|
+
meta: {
|
|
533
|
+
tier,
|
|
534
|
+
processingTimeMs,
|
|
535
|
+
checksRun: checks.length,
|
|
536
|
+
checksPassed: passedChecks.length,
|
|
537
|
+
requestId: offlineRequestId(),
|
|
538
|
+
timestamp: Date.now()
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function buildBatchResult(results, submissions, crossAnalysis) {
|
|
543
|
+
const total = results.length;
|
|
544
|
+
const passed = results.filter((r) => r.recommendation === "keep").length;
|
|
545
|
+
const review = results.filter((r) => r.recommendation === "review").length;
|
|
546
|
+
const failed = results.filter((r) => r.recommendation === "discard").length;
|
|
547
|
+
const avgScore = total > 0 ? results.reduce((sum, r) => sum + r.score, 0) / total : 0;
|
|
548
|
+
const batch = {
|
|
549
|
+
results,
|
|
550
|
+
summary: { total, passed, review, failed, avgScore }
|
|
551
|
+
};
|
|
552
|
+
if (crossAnalysis) {
|
|
553
|
+
batch.fraudIndicators = detectFraudIndicators(submissions);
|
|
554
|
+
}
|
|
555
|
+
return batch;
|
|
556
|
+
}
|
|
557
|
+
function detectFraudIndicators(submissions) {
|
|
558
|
+
const seen = /* @__PURE__ */ new Map();
|
|
559
|
+
const duplicateAnswers = [];
|
|
560
|
+
submissions.forEach((sub, index) => {
|
|
561
|
+
const key = sub.responses.map((r) => `${r.question}=${r.answer}`).join("|");
|
|
562
|
+
if (seen.has(key)) {
|
|
563
|
+
const id = sub.context?.surveyId ?? `submission_${index}`;
|
|
564
|
+
duplicateAnswers.push(id);
|
|
565
|
+
} else {
|
|
566
|
+
seen.set(key, index);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
return {
|
|
570
|
+
duplicateAnswers,
|
|
571
|
+
coordinatedTiming: false,
|
|
572
|
+
deviceSharing: false,
|
|
573
|
+
fraudRingScore: clamp01(duplicateAnswers.length / Math.max(1, submissions.length))
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/cipher.ts
|
|
578
|
+
var DEFAULT_CONFIG = {
|
|
579
|
+
tier: 3,
|
|
580
|
+
thresholds: {
|
|
581
|
+
fail: 0.4,
|
|
582
|
+
review: 0.7
|
|
583
|
+
},
|
|
584
|
+
debug: false,
|
|
585
|
+
offline: false,
|
|
586
|
+
endpoint: "https://api.surbee.com/v1/cipher"
|
|
587
|
+
};
|
|
38
588
|
var Cipher = class {
|
|
39
589
|
config;
|
|
40
|
-
constructor(config) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
590
|
+
constructor(config = {}) {
|
|
591
|
+
const offline = config.offline ?? DEFAULT_CONFIG.offline;
|
|
592
|
+
if (config.apiKey) {
|
|
593
|
+
if (!config.apiKey.startsWith("cipher_sk_") && !config.apiKey.startsWith("cipher_pk_")) {
|
|
594
|
+
throw new Error("Invalid API key format. Keys should start with cipher_sk_ or cipher_pk_");
|
|
595
|
+
}
|
|
596
|
+
} else if (!offline) {
|
|
597
|
+
throw new Error(
|
|
598
|
+
"API key is required for online validation. Pass { offline: true } to run tier 1\u20132 checks locally, or get a key from Settings > API Keys in your Surbee dashboard."
|
|
599
|
+
);
|
|
46
600
|
}
|
|
47
601
|
this.config = {
|
|
48
|
-
apiKey: config.apiKey,
|
|
602
|
+
apiKey: config.apiKey ?? "",
|
|
49
603
|
tier: config.tier ?? DEFAULT_CONFIG.tier,
|
|
50
604
|
thresholds: {
|
|
51
605
|
fail: config.thresholds?.fail ?? DEFAULT_CONFIG.thresholds.fail,
|
|
52
606
|
review: config.thresholds?.review ?? DEFAULT_CONFIG.thresholds.review
|
|
53
607
|
},
|
|
54
608
|
debug: config.debug ?? DEFAULT_CONFIG.debug,
|
|
609
|
+
offline,
|
|
55
610
|
endpoint: config.endpoint ?? DEFAULT_CONFIG.endpoint
|
|
56
611
|
};
|
|
57
612
|
}
|
|
58
613
|
/**
|
|
59
|
-
* Validate a single response
|
|
614
|
+
* Validate a single response.
|
|
615
|
+
*
|
|
616
|
+
* Runs locally when `offline: true`, otherwise calls Surbee's validation
|
|
617
|
+
* engine (which can run the AI-powered checks for tiers 3–5).
|
|
60
618
|
*/
|
|
61
619
|
async validate(input) {
|
|
620
|
+
if (this.config.offline) {
|
|
621
|
+
return this.validateSync(input);
|
|
622
|
+
}
|
|
62
623
|
const response = await this.request("/validate", {
|
|
63
624
|
tier: this.config.tier,
|
|
64
625
|
thresholds: this.config.thresholds,
|
|
@@ -67,32 +628,80 @@ var Cipher = class {
|
|
|
67
628
|
return response;
|
|
68
629
|
}
|
|
69
630
|
/**
|
|
70
|
-
* Validate
|
|
631
|
+
* Validate a single response synchronously, fully on-device.
|
|
632
|
+
*
|
|
633
|
+
* Only the offline checks for the configured tier are evaluated; AI-powered
|
|
634
|
+
* checks (tiers 3+) are skipped. No API key or network access required.
|
|
635
|
+
*/
|
|
636
|
+
validateSync(input) {
|
|
637
|
+
const start = Date.now();
|
|
638
|
+
const checks = this.runOfflineChecks(input);
|
|
639
|
+
return buildResult(checks, this.config.tier, this.config.thresholds, Date.now() - start);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Validate multiple responses in batch.
|
|
643
|
+
*
|
|
644
|
+
* Runs locally when `offline: true`, otherwise calls Surbee's batch endpoint.
|
|
71
645
|
*/
|
|
72
646
|
async validateBatch(input) {
|
|
647
|
+
const crossAnalysis = input.crossAnalysis ?? this.config.tier === 5;
|
|
648
|
+
if (this.config.offline) {
|
|
649
|
+
const results = input.submissions.map((submission) => this.validateSync(submission));
|
|
650
|
+
return buildBatchResult(results, input.submissions, crossAnalysis);
|
|
651
|
+
}
|
|
73
652
|
const response = await this.request("/validate/batch", {
|
|
74
653
|
tier: this.config.tier,
|
|
75
654
|
thresholds: this.config.thresholds,
|
|
76
655
|
submissions: input.submissions,
|
|
77
|
-
crossAnalysis
|
|
656
|
+
crossAnalysis
|
|
78
657
|
});
|
|
79
658
|
return response;
|
|
80
659
|
}
|
|
81
660
|
/**
|
|
82
|
-
*
|
|
661
|
+
* Run the offline checks that apply to the configured tier.
|
|
662
|
+
*/
|
|
663
|
+
runOfflineChecks(input) {
|
|
664
|
+
const { responses, behavioralMetrics, deviceInfo, context } = input;
|
|
665
|
+
const all = [
|
|
666
|
+
...runTimingChecks(responses, behavioralMetrics, context),
|
|
667
|
+
...runBehavioralChecks(behavioralMetrics),
|
|
668
|
+
...runContentChecks(responses, behavioralMetrics),
|
|
669
|
+
...runDeviceChecks(deviceInfo)
|
|
670
|
+
];
|
|
671
|
+
const byId = new Map(all.map((c) => [c.checkId, c]));
|
|
672
|
+
const offlineIds = getOfflineChecksForTier(this.config.tier);
|
|
673
|
+
return offlineIds.map((id) => byId.get(id)).filter((c) => Boolean(c));
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Estimate the per-response API cost for a number of responses at the
|
|
677
|
+
* configured tier (tiers 1–2 are free).
|
|
678
|
+
*/
|
|
679
|
+
estimateCost(responses = 1) {
|
|
680
|
+
return estimateCost(this.config.tier, responses);
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Get tier information, including the list of checks it runs.
|
|
83
684
|
*/
|
|
84
685
|
getTierInfo(tier) {
|
|
85
686
|
const t = tier ?? this.config.tier;
|
|
86
|
-
|
|
687
|
+
const config = TIERS[t];
|
|
688
|
+
const checks = getChecksForTier(t);
|
|
689
|
+
return {
|
|
690
|
+
tier: t,
|
|
691
|
+
name: config.name,
|
|
692
|
+
description: config.description,
|
|
693
|
+
checks,
|
|
694
|
+
checksCount: checks.length,
|
|
695
|
+
aiModel: config.aiModel,
|
|
696
|
+
offline: config.offline,
|
|
697
|
+
estimatedCostPerResponse: config.estimatedCostPerResponse
|
|
698
|
+
};
|
|
87
699
|
}
|
|
88
700
|
/**
|
|
89
701
|
* Get all available tiers
|
|
90
702
|
*/
|
|
91
703
|
getAllTiers() {
|
|
92
|
-
return
|
|
93
|
-
tier: Number(tier),
|
|
94
|
-
...info
|
|
95
|
-
}));
|
|
704
|
+
return [1, 2, 3, 4, 5].map((tier) => this.getTierInfo(tier));
|
|
96
705
|
}
|
|
97
706
|
/**
|
|
98
707
|
* Check API key validity and credits
|
|
@@ -105,6 +714,11 @@ var Cipher = class {
|
|
|
105
714
|
* Make API request to Surbee
|
|
106
715
|
*/
|
|
107
716
|
async request(path, body) {
|
|
717
|
+
if (!this.config.apiKey) {
|
|
718
|
+
throw this.createError(401, {
|
|
719
|
+
message: "This Cipher instance has no API key. Provide one to use online validation, or call validateSync() for offline checks."
|
|
720
|
+
});
|
|
721
|
+
}
|
|
108
722
|
const url = `${this.config.endpoint}${path}`;
|
|
109
723
|
if (this.config.debug) {
|
|
110
724
|
console.log(`[Cipher] POST ${url}`);
|