leapfrog-mcp 0.6.2 → 0.6.3

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.
Files changed (57) hide show
  1. package/dist/adaptive-wait.d.ts +72 -0
  2. package/dist/adaptive-wait.js +695 -0
  3. package/dist/api-intelligence.d.ts +82 -0
  4. package/dist/api-intelligence.js +575 -0
  5. package/dist/captcha-solver.d.ts +26 -0
  6. package/dist/captcha-solver.js +547 -0
  7. package/dist/cdp-connector.d.ts +33 -0
  8. package/dist/cdp-connector.js +176 -0
  9. package/dist/consent-dismiss.d.ts +33 -0
  10. package/dist/consent-dismiss.js +358 -0
  11. package/dist/crash-recovery.d.ts +74 -0
  12. package/dist/crash-recovery.js +242 -0
  13. package/dist/domain-knowledge.d.ts +149 -0
  14. package/dist/domain-knowledge.js +449 -0
  15. package/dist/harness-intelligence.d.ts +65 -0
  16. package/dist/harness-intelligence.js +432 -0
  17. package/dist/humanize-fingerprint.d.ts +42 -0
  18. package/dist/humanize-fingerprint.js +161 -0
  19. package/dist/humanize-mouse.d.ts +95 -0
  20. package/dist/humanize-mouse.js +275 -0
  21. package/dist/humanize-pause.d.ts +48 -0
  22. package/dist/humanize-pause.js +111 -0
  23. package/dist/humanize-scroll.d.ts +67 -0
  24. package/dist/humanize-scroll.js +185 -0
  25. package/dist/humanize-typing.d.ts +60 -0
  26. package/dist/humanize-typing.js +258 -0
  27. package/dist/humanize-utils.d.ts +62 -0
  28. package/dist/humanize-utils.js +100 -0
  29. package/dist/intervention.d.ts +65 -0
  30. package/dist/intervention.js +591 -0
  31. package/dist/logger.d.ts +13 -0
  32. package/dist/logger.js +47 -0
  33. package/dist/network-intelligence.d.ts +70 -0
  34. package/dist/network-intelligence.js +424 -0
  35. package/dist/page-classifier.d.ts +33 -0
  36. package/dist/page-classifier.js +1000 -0
  37. package/dist/paginate.d.ts +42 -0
  38. package/dist/paginate.js +693 -0
  39. package/dist/recording.d.ts +72 -0
  40. package/dist/recording.js +934 -0
  41. package/dist/script-executor.d.ts +31 -0
  42. package/dist/script-executor.js +249 -0
  43. package/dist/session-hud.d.ts +20 -0
  44. package/dist/session-hud.js +134 -0
  45. package/dist/snapshot-differ.d.ts +26 -0
  46. package/dist/snapshot-differ.js +225 -0
  47. package/dist/ssrf.d.ts +28 -0
  48. package/dist/ssrf.js +290 -0
  49. package/dist/stealth-audit.d.ts +27 -0
  50. package/dist/stealth-audit.js +719 -0
  51. package/dist/stealth.d.ts +195 -0
  52. package/dist/stealth.js +1157 -0
  53. package/dist/tab-manager.d.ts +14 -0
  54. package/dist/tab-manager.js +306 -0
  55. package/dist/tiles-coordinator.d.ts +106 -0
  56. package/dist/tiles-coordinator.js +358 -0
  57. package/package.json +1 -1
@@ -0,0 +1,185 @@
1
+ // ─── Humanized Scrolling ───────────────────────────────────────────────────
2
+ //
3
+ // Inertial scroll simulation with ramp-up and momentum decay, variable
4
+ // scroll amounts, read-pause-scroll cycles, overshoot/scroll-back, and
5
+ // cursor-scroll correlation (sympathetic mouse movement).
6
+ //
7
+ // Ported from the validated humanize.js prototype (tested on 3090 box,
8
+ // statistically verified).
9
+ //
10
+ // Integration point: import { humanScroll } from "./humanize-scroll.js"
11
+ // then call humanScroll.scroll(page, distance) inside the act tool handler
12
+ // (src/index.ts) as an alternative to page.mouse.wheel().
13
+ //
14
+ // Standalone module — no cross-dependencies on other humanize modules.
15
+ import { gaussianRandom, clamp, humanDelay, sleep, isHumanizeEnabled, normalRandom } from "./humanize-utils.js";
16
+ // ─── Scroll Plan ───────────────────────────────────────────────────────────
17
+ /**
18
+ * Generate a human-like scroll plan for a given pixel distance.
19
+ * Simulates inertial scrolling with ease-out momentum decay.
20
+ *
21
+ * The scroll is broken into increments that start small (ramp-up / finger
22
+ * contact), hit peak velocity, then taper off (friction / momentum loss),
23
+ * similar to real touchpad scrolling.
24
+ *
25
+ * @param distance - Total scroll distance in pixels (positive = down)
26
+ * @param opts - Configuration options
27
+ * @returns Array of scroll steps with deltas and delays
28
+ */
29
+ export function humanScrollPlan(distance, opts = {}) {
30
+ const maxIncrement = opts.maxIncrement ?? 120;
31
+ const friction = opts.friction ?? 0.82;
32
+ const direction = distance >= 0 ? 1 : -1;
33
+ let remaining = Math.abs(distance);
34
+ const steps = [];
35
+ let cumulative = 0;
36
+ // Phase 1: Ramp-up (2-3 small increments to simulate finger contact)
37
+ const rampSteps = Math.floor(Math.random() * 2) + 2;
38
+ for (let i = 0; i < rampSteps && remaining > 0; i++) {
39
+ const fraction = (i + 1) / (rampSteps + 1);
40
+ const base = maxIncrement * fraction * 0.6;
41
+ const delta = Math.min(Math.round(base + gaussianRandom(0, 5)), remaining);
42
+ remaining -= delta;
43
+ cumulative += delta * direction;
44
+ steps.push({
45
+ delta: delta * direction,
46
+ delay: humanDelay(12, 25),
47
+ cumulative,
48
+ });
49
+ }
50
+ // Phase 2: Momentum decay (ease-out)
51
+ let velocity = maxIncrement + gaussianRandom(0, 15);
52
+ while (remaining > 2) {
53
+ velocity *= friction + gaussianRandom(0, 0.03);
54
+ velocity = clamp(velocity, 3, maxIncrement * 1.2);
55
+ const delta = Math.min(Math.round(velocity), remaining);
56
+ remaining -= delta;
57
+ cumulative += delta * direction;
58
+ steps.push({
59
+ delta: delta * direction,
60
+ delay: humanDelay(14, 30),
61
+ cumulative,
62
+ });
63
+ }
64
+ // Mop up any remainder
65
+ if (remaining > 0) {
66
+ cumulative += remaining * direction;
67
+ steps.push({
68
+ delta: remaining * direction,
69
+ delay: humanDelay(20, 50),
70
+ cumulative,
71
+ });
72
+ }
73
+ return steps;
74
+ }
75
+ // ─── HumanScroll Class ─────────────────────────────────────────────────────
76
+ export class HumanScroll {
77
+ /**
78
+ * Whether humanized scrolling is enabled.
79
+ * Returns false unless LEAP_HUMANIZE=true or LEAP_HUMANIZE=1 is set.
80
+ */
81
+ isEnabled() {
82
+ return isHumanizeEnabled();
83
+ }
84
+ /**
85
+ * Scroll the page by the given pixel distance using a human-like
86
+ * ramp-up + momentum decay pattern.
87
+ *
88
+ * Each step in the scroll plan is dispatched as a mouse wheel event
89
+ * with an inter-step delay to simulate real scrolling inertia.
90
+ *
91
+ * Enhancements over basic scroll:
92
+ * - Variable scroll amounts (normalRandom instead of fixed)
93
+ * - Read-pause-scroll cycles (800-3000ms reading gaps between chunks)
94
+ * - 10% chance of overshoot + scroll-back correction
95
+ * - Sympathetic cursor micro-movements during scrolling
96
+ *
97
+ * No-op if humanization is disabled.
98
+ *
99
+ * @param page - Playwright page instance
100
+ * @param distance - Total scroll distance in pixels (positive = down, negative = up)
101
+ */
102
+ async scroll(page, distance) {
103
+ if (!this.isEnabled())
104
+ return;
105
+ // Variable scroll amount: apply Gaussian variation to the requested distance.
106
+ // Humans rarely scroll an exact number of pixels — add +/-15% noise.
107
+ const variableDistance = distance === 0
108
+ ? 0
109
+ : Math.round(distance + gaussianRandom(0, Math.abs(distance) * 0.05));
110
+ // Break long scrolls into read-pause-scroll chunks.
111
+ // Each chunk is a "scroll gesture" followed by a reading pause.
112
+ const chunkSize = Math.round(Math.abs(normalRandom(350, 100)));
113
+ const effectiveChunk = Math.max(chunkSize, 80); // floor at 80px
114
+ const totalAbs = Math.abs(variableDistance);
115
+ const direction = variableDistance >= 0 ? 1 : -1;
116
+ let scrolled = 0;
117
+ let isFirstChunk = true;
118
+ while (scrolled < totalAbs) {
119
+ // Read-pause: wait between scroll gestures (skip before first chunk)
120
+ if (!isFirstChunk) {
121
+ const readPause = humanDelay(800, 3000);
122
+ await sleep(readPause);
123
+ }
124
+ isFirstChunk = false;
125
+ const chunkRemaining = Math.min(effectiveChunk, totalAbs - scrolled);
126
+ const plan = humanScrollPlan(chunkRemaining * direction);
127
+ // Sympathetic cursor movement: small random mouse moves during scrolling
128
+ // to simulate hand-on-trackpad/mouse-wheel posture
129
+ for (const step of plan) {
130
+ await sleep(step.delay);
131
+ await page.mouse.wheel(0, step.delta);
132
+ // Cursor-scroll correlation: ~30% chance of small sympathetic mouse move
133
+ if (Math.random() < 0.3) {
134
+ const sympatheticX = gaussianRandom(0, 1.5);
135
+ const sympatheticY = gaussianRandom(0, 2.0);
136
+ try {
137
+ await page.mouse.move(400 + sympatheticX, // approximate center-ish x with jitter
138
+ 300 + sympatheticY, // approximate center-ish y with jitter
139
+ { steps: 1 });
140
+ }
141
+ catch {
142
+ // Mouse move during scroll can fail if page is navigating — safe to ignore
143
+ }
144
+ }
145
+ }
146
+ scrolled += chunkRemaining;
147
+ }
148
+ // Overshoot + scroll-back: 10% of scroll operations overshoot then correct
149
+ if (totalAbs > 0 && Math.random() < 0.10) {
150
+ const overshootAmount = Math.round(50 + Math.random() * 50); // 50-100px
151
+ const overshootPlan = humanScrollPlan(overshootAmount * direction);
152
+ for (const step of overshootPlan) {
153
+ await sleep(step.delay);
154
+ await page.mouse.wheel(0, step.delta);
155
+ }
156
+ // Brief pause before correction (realizing overshoot)
157
+ await sleep(humanDelay(150, 400));
158
+ // Scroll back to correct
159
+ const correctionPlan = humanScrollPlan(overshootAmount * -direction);
160
+ for (const step of correctionPlan) {
161
+ await sleep(step.delay);
162
+ await page.mouse.wheel(0, step.delta);
163
+ }
164
+ }
165
+ }
166
+ /**
167
+ * Scroll with variable distance. Uses normalRandom(350, 100) to determine
168
+ * a natural scroll distance, then executes the humanized scroll.
169
+ *
170
+ * Useful when the caller just wants "scroll down some" without a specific
171
+ * pixel target.
172
+ *
173
+ * @param page - Playwright page instance
174
+ * @param direction - "down" (positive) or "up" (negative)
175
+ */
176
+ async scrollVariable(page, direction = "down") {
177
+ if (!this.isEnabled())
178
+ return;
179
+ const amount = Math.round(Math.abs(normalRandom(350, 100)));
180
+ const distance = direction === "down" ? amount : -amount;
181
+ await this.scroll(page, distance);
182
+ }
183
+ }
184
+ export const humanScroll = new HumanScroll();
185
+ export default humanScroll;
@@ -0,0 +1,60 @@
1
+ import type { Page } from "playwright-core";
2
+ export interface KeystrokeEvent {
3
+ /** The character to press (or '\b' for backspace) */
4
+ char: string;
5
+ /** Delay before this keystroke in ms (flight time / inter-key interval) */
6
+ delay: number;
7
+ /** Key dwell time in ms (how long the key is held down) */
8
+ dwell: number;
9
+ /** Whether this keystroke is a typo */
10
+ isTypo: boolean;
11
+ /** If non-null, the correct character that was retyped after a typo correction */
12
+ correction: string | null;
13
+ /** Whether this keystroke should overlap with the previous key release (rollover) */
14
+ rollover: boolean;
15
+ }
16
+ export interface TypeOptions {
17
+ /** Median inter-key delay in ms (log-normal center). Default: 200 */
18
+ baseDelay?: number;
19
+ /** Base probability of a typo per alphabetic character. Default: 0.02 */
20
+ typoRate?: number;
21
+ }
22
+ /**
23
+ * Generate a sequence of keystroke events with humanized timing and typo simulation.
24
+ *
25
+ * Timing model (research-backed):
26
+ * - Base delay: Log-normal with 200ms median, sigma 0.45 (right-skewed, heavy-tailed)
27
+ * - Bigram-aware: hand-alternation pairs 30% faster, same-finger pairs 40% slower
28
+ * - Longer pauses at word boundaries (~250ms median) and after punctuation
29
+ * - Burst typing: occasional faster sequences (50% of base delay)
30
+ * - Key dwell time: log-normal with 70ms median (50-130ms typical range)
31
+ * - Rollover: next key pressed before previous released on fast hand-alternation bigrams
32
+ *
33
+ * Typo model:
34
+ * - 2% base rate for alphabetic characters
35
+ * - Higher rate (4%) for characters with many adjacent keys
36
+ * - Typo character is chosen from QWERTY adjacency map
37
+ * - Correction: backspace + retype after a short pause (200-400ms)
38
+ */
39
+ export declare function humanTypeString(text: string, opts?: TypeOptions): KeystrokeEvent[];
40
+ export declare class HumanTyping {
41
+ /**
42
+ * Whether humanized typing is enabled.
43
+ * Returns false unless LEAP_HUMANIZE=true or LEAP_HUMANIZE=1 is set.
44
+ */
45
+ isEnabled(): boolean;
46
+ /**
47
+ * Type text with humanized inter-key delays, key dwell time, rollover,
48
+ * occasional typos, and corrections.
49
+ *
50
+ * Uses page.keyboard.down() / up() for full timing control:
51
+ * - Per-key flight time (variable IKI via log-normal distribution)
52
+ * - Per-key dwell time (variable hold duration)
53
+ * - Rollover simulation (next keydown before previous keyup)
54
+ *
55
+ * No-op if humanization is disabled.
56
+ */
57
+ typeText(page: Page, text: string, opts?: TypeOptions): Promise<void>;
58
+ }
59
+ export declare const humanTyping: HumanTyping;
60
+ export default humanTyping;
@@ -0,0 +1,258 @@
1
+ // ─── Humanized Typing ──────────────────────────────────────────────────────
2
+ //
3
+ // Log-normal–distributed inter-key delays, key dwell time, rollover typing,
4
+ // bigram-aware timing, burst typing, and a QWERTY-adjacency typo model with
5
+ // backspace correction.
6
+ //
7
+ // Research basis: 03-typing-humanization.md
8
+ // - Log-normal distribution (right-skewed, heavy-tailed — matches 136M
9
+ // keystrokes study, Monaco et al. 2021 log-logistic finding)
10
+ // - 200ms median IKI (~52 WPM, population mean)
11
+ // - Key dwell time 70ms median (50-130ms range)
12
+ // - Rollover typing on hand-alternation bigrams
13
+ // - Bigram-aware speed multipliers (hand alternation, same-finger)
14
+ //
15
+ // Integration point: import { humanTyping } from "./humanize-typing.js"
16
+ // then call humanTyping.typeText(page, text) inside the act tool handler
17
+ // (src/index.ts) as an alternative to page.keyboard.type().
18
+ //
19
+ // Standalone module — no cross-dependencies on other humanize modules.
20
+ import { humanDelay, logNormalDelay, sleep, isHumanizeEnabled } from "./humanize-utils.js";
21
+ // ─── QWERTY Adjacency Map ──────────────────────────────────────────────────
22
+ /**
23
+ * QWERTY adjacency map for simulating realistic typos.
24
+ * Each key maps to its physically adjacent keys on a standard QWERTY layout.
25
+ */
26
+ const QWERTY_ADJACENT = {
27
+ q: ["w", "a"], w: ["q", "e", "a", "s"], e: ["w", "r", "s", "d"],
28
+ r: ["e", "t", "d", "f"], t: ["r", "y", "f", "g"], y: ["t", "u", "g", "h"],
29
+ u: ["y", "i", "h", "j"], i: ["u", "o", "j", "k"], o: ["i", "p", "k", "l"],
30
+ p: ["o", "l"],
31
+ a: ["q", "w", "s", "z"], s: ["a", "w", "e", "d", "z", "x"],
32
+ d: ["s", "e", "r", "f", "x", "c"], f: ["d", "r", "t", "g", "c", "v"],
33
+ g: ["f", "t", "y", "h", "v", "b"], h: ["g", "y", "u", "j", "b", "n"],
34
+ j: ["h", "u", "i", "k", "n", "m"], k: ["j", "i", "o", "l", "m"],
35
+ l: ["k", "o", "p"],
36
+ z: ["a", "s", "x"], x: ["z", "s", "d", "c"], c: ["x", "d", "f", "v"],
37
+ v: ["c", "f", "g", "b"], b: ["v", "g", "h", "n"], n: ["b", "h", "j", "m"],
38
+ m: ["n", "j", "k"],
39
+ };
40
+ // ─── Bigram Timing ─────────────────────────────────────────────────────────
41
+ /**
42
+ * Hand-alternation bigrams — typed fastest because both hands work in parallel.
43
+ * (Salthouse 1986: 30-60ms faster than same-hand bigrams)
44
+ */
45
+ const ALTERNATING_BIGRAMS = new Set([
46
+ "th", "ht", "di", "id", "en", "ne", "ri", "ir", "to", "ot",
47
+ "do", "od", "gu", "ug", "ha", "ah", "fi", "if", "pe", "ep",
48
+ "wo", "ow", "bi", "ib", "tu", "ut", "ry", "yr", "ck", "kc",
49
+ ]);
50
+ /**
51
+ * Same-finger bigrams — typed slowest because one finger must travel between rows.
52
+ */
53
+ const SAME_FINGER_BIGRAMS = new Set([
54
+ "ed", "de", "nu", "un", "my", "ym", "ce", "ec", "rb", "br",
55
+ "ft", "tf", "ju", "uj", "ki", "ik", "lo", "ol", "ws", "sw",
56
+ ]);
57
+ /**
58
+ * Return a speed multiplier based on the bigram (previous + current character).
59
+ * Hand alternation = 0.7x (30% faster), same finger = 1.4x (40% slower).
60
+ */
61
+ function bigramMultiplier(prev, curr) {
62
+ const pair = (prev + curr).toLowerCase();
63
+ if (ALTERNATING_BIGRAMS.has(pair))
64
+ return 0.7;
65
+ if (SAME_FINGER_BIGRAMS.has(pair))
66
+ return 1.4;
67
+ return 1.0;
68
+ }
69
+ // ─── Dwell Time ────────────────────────────────────────────────────────────
70
+ /** Default dwell time config (research: 50-130ms range, median ~70ms) */
71
+ const DWELL_MEDIAN = 70;
72
+ const DWELL_SIGMA = 0.3;
73
+ /**
74
+ * Compute key dwell time (keydown-to-keyup duration) for a character.
75
+ * Backspace has a shorter dwell (~40ms). Regular keys ~70ms median.
76
+ */
77
+ function computeDwell(char) {
78
+ if (char === "\b")
79
+ return logNormalDelay(40, 0.25, 20);
80
+ return logNormalDelay(DWELL_MEDIAN, DWELL_SIGMA, 30);
81
+ }
82
+ // ─── Keystroke Plan ────────────────────────────────────────────────────────
83
+ /**
84
+ * Generate a sequence of keystroke events with humanized timing and typo simulation.
85
+ *
86
+ * Timing model (research-backed):
87
+ * - Base delay: Log-normal with 200ms median, sigma 0.45 (right-skewed, heavy-tailed)
88
+ * - Bigram-aware: hand-alternation pairs 30% faster, same-finger pairs 40% slower
89
+ * - Longer pauses at word boundaries (~250ms median) and after punctuation
90
+ * - Burst typing: occasional faster sequences (50% of base delay)
91
+ * - Key dwell time: log-normal with 70ms median (50-130ms typical range)
92
+ * - Rollover: next key pressed before previous released on fast hand-alternation bigrams
93
+ *
94
+ * Typo model:
95
+ * - 2% base rate for alphabetic characters
96
+ * - Higher rate (4%) for characters with many adjacent keys
97
+ * - Typo character is chosen from QWERTY adjacency map
98
+ * - Correction: backspace + retype after a short pause (200-400ms)
99
+ */
100
+ export function humanTypeString(text, opts = {}) {
101
+ const baseDelay = opts.baseDelay ?? 200;
102
+ const typoRate = opts.typoRate ?? 0.02;
103
+ const events = [];
104
+ let inBurst = false;
105
+ let burstRemaining = 0;
106
+ let prevChar = "";
107
+ // ── Fatigue model ─────────────────────────────────────────────────
108
+ // After ~30 seconds of continuous typing (estimated from char count × avg IKI),
109
+ // gradually slow down by 10-15%. This simulates reduced motor performance
110
+ // during sustained input — a strong human signal absent in bots.
111
+ const FATIGUE_ONSET_CHARS = 150; // ~30s at 200ms avg IKI
112
+ const FATIGUE_MAX_MULTIPLIER = 1.15; // 15% slower at peak fatigue
113
+ const FATIGUE_RAMP_CHARS = 100; // chars over which fatigue ramps from 1.0 to max
114
+ // Log-normal sigma: controls the right-skew / heavy tail.
115
+ // 0.45 produces mode ~170ms, mean ~215ms with occasional spikes to 400-600ms.
116
+ const ikiSigma = 0.45;
117
+ for (let i = 0; i < text.length; i++) {
118
+ const char = text[i];
119
+ const lower = char.toLowerCase();
120
+ const isAlpha = /[a-z]/.test(lower);
121
+ // ── Fatigue multiplier ───────────────────────────────────────
122
+ let fatigueMultiplier = 1.0;
123
+ if (i > FATIGUE_ONSET_CHARS) {
124
+ const fatigueProgress = Math.min(1.0, (i - FATIGUE_ONSET_CHARS) / FATIGUE_RAMP_CHARS);
125
+ fatigueMultiplier = 1.0 + fatigueProgress * (FATIGUE_MAX_MULTIPLIER - 1.0);
126
+ }
127
+ // ── Compute inter-key delay (flight time) ──────────────────────
128
+ let delay;
129
+ if (burstRemaining > 0) {
130
+ // Burst: 50% of base delay, still log-normal distributed
131
+ delay = logNormalDelay(Math.round(baseDelay * 0.5), 0.35, 25);
132
+ burstRemaining--;
133
+ if (burstRemaining === 0)
134
+ inBurst = false;
135
+ }
136
+ else if (char === " ") {
137
+ // Word boundary: research shows ~250ms median inter-word pause
138
+ delay = logNormalDelay(250, 0.4, 80);
139
+ }
140
+ else if (/[.,;:!?]/.test(char)) {
141
+ // Punctuation: slightly longer pause
142
+ delay = logNormalDelay(200, 0.4, 80);
143
+ }
144
+ else if (char === "\n") {
145
+ // Line break: cognitive pause
146
+ delay = logNormalDelay(400, 0.5, 150);
147
+ }
148
+ else {
149
+ // Regular character: log-normal around baseDelay with bigram adjustment
150
+ const multiplier = prevChar ? bigramMultiplier(prevChar, lower) : 1.0;
151
+ delay = logNormalDelay(Math.round(baseDelay * multiplier), ikiSigma, 40);
152
+ }
153
+ // Apply fatigue to the computed delay
154
+ delay = Math.round(delay * fatigueMultiplier);
155
+ // ── Cognitive pause: 12% chance of a think-pause at word starts ──
156
+ if (prevChar === " " && isAlpha && !inBurst && Math.random() < 0.12) {
157
+ delay = logNormalDelay(400, 0.5, 150);
158
+ }
159
+ // ── Burst initiation: ~8% chance after spaces ──────────────────
160
+ if (!inBurst && char === " " && Math.random() < 0.08) {
161
+ inBurst = true;
162
+ burstRemaining = Math.floor(Math.random() * 6) + 3; // 3-8 chars
163
+ }
164
+ // ── Rollover detection ─────────────────────────────────────────
165
+ // On hand-alternation bigrams with short flight times, the next key
166
+ // is pressed before the previous key is released. This is a strong
167
+ // human signal that sequential bots lack.
168
+ let rollover = false;
169
+ if (prevChar && delay < 180) {
170
+ const pair = (prevChar + lower).toLowerCase();
171
+ if (ALTERNATING_BIGRAMS.has(pair) && Math.random() < 0.3) {
172
+ rollover = true;
173
+ }
174
+ }
175
+ // ── Compute dwell time ─────────────────────────────────────────
176
+ const dwell = computeDwell(char);
177
+ // ── Typo check ─────────────────────────────────────────────────
178
+ const adjacents = QWERTY_ADJACENT[lower];
179
+ const effectiveRate = adjacents && adjacents.length >= 5 ? typoRate * 2 : typoRate;
180
+ if (isAlpha && Math.random() < effectiveRate && adjacents) {
181
+ const typoChar = adjacents[Math.floor(Math.random() * adjacents.length)];
182
+ const displayTypo = char === char.toUpperCase() ? typoChar.toUpperCase() : typoChar;
183
+ // Push the wrong keystroke
184
+ events.push({ char: displayTypo, delay, dwell: computeDwell(displayTypo), isTypo: true, correction: null, rollover });
185
+ // Push the backspace after a reaction delay
186
+ events.push({ char: "\b", delay: humanDelay(150, 350), dwell: computeDwell("\b"), isTypo: false, correction: null, rollover: false });
187
+ // Push the correct keystroke
188
+ events.push({ char, delay: humanDelay(50, 120), dwell, isTypo: false, correction: char, rollover: false });
189
+ }
190
+ else {
191
+ events.push({ char, delay, dwell, isTypo: false, correction: null, rollover });
192
+ }
193
+ prevChar = lower;
194
+ }
195
+ return events;
196
+ }
197
+ // ─── HumanTyping Class ─────────────────────────────────────────────────────
198
+ export class HumanTyping {
199
+ /**
200
+ * Whether humanized typing is enabled.
201
+ * Returns false unless LEAP_HUMANIZE=true or LEAP_HUMANIZE=1 is set.
202
+ */
203
+ isEnabled() {
204
+ return isHumanizeEnabled();
205
+ }
206
+ /**
207
+ * Type text with humanized inter-key delays, key dwell time, rollover,
208
+ * occasional typos, and corrections.
209
+ *
210
+ * Uses page.keyboard.down() / up() for full timing control:
211
+ * - Per-key flight time (variable IKI via log-normal distribution)
212
+ * - Per-key dwell time (variable hold duration)
213
+ * - Rollover simulation (next keydown before previous keyup)
214
+ *
215
+ * No-op if humanization is disabled.
216
+ */
217
+ async typeText(page, text, opts) {
218
+ if (!this.isEnabled())
219
+ return;
220
+ const events = humanTypeString(text, opts);
221
+ let prevKeyDown = null;
222
+ for (const event of events) {
223
+ const key = event.char === "\b" ? "Backspace" : event.char;
224
+ if (event.rollover && prevKeyDown) {
225
+ // Rollover: press next key BEFORE releasing previous key.
226
+ // Overlap by a portion of the flight time.
227
+ const overlap = logNormalDelay(25, 0.3, 10);
228
+ const preOverlapWait = Math.max(0, event.delay - overlap);
229
+ await sleep(preOverlapWait);
230
+ await page.keyboard.down(key);
231
+ await sleep(overlap);
232
+ await page.keyboard.up(prevKeyDown);
233
+ await sleep(event.dwell);
234
+ await page.keyboard.up(key);
235
+ prevKeyDown = null;
236
+ }
237
+ else {
238
+ // Release previous key if still held (shouldn't normally happen,
239
+ // but guard against it)
240
+ if (prevKeyDown) {
241
+ await page.keyboard.up(prevKeyDown);
242
+ prevKeyDown = null;
243
+ }
244
+ // Standard keystroke: wait flight time, then keydown, hold, keyup
245
+ await sleep(event.delay);
246
+ await page.keyboard.down(key);
247
+ await sleep(event.dwell);
248
+ await page.keyboard.up(key);
249
+ }
250
+ }
251
+ // Ensure final key is released
252
+ if (prevKeyDown) {
253
+ await page.keyboard.up(prevKeyDown);
254
+ }
255
+ }
256
+ }
257
+ export const humanTyping = new HumanTyping();
258
+ export default humanTyping;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Generate a normally-distributed random number using the Box-Muller transform.
3
+ * @param mean - Center of the distribution
4
+ * @param stddev - Standard deviation
5
+ * @returns A sample from N(mean, stddev^2)
6
+ */
7
+ export declare function gaussianRandom(mean?: number, stddev?: number): number;
8
+ /**
9
+ * Clamp a value between min and max.
10
+ */
11
+ export declare function clamp(val: number, min: number, max: number): number;
12
+ /**
13
+ * Generate a human-like delay (in ms) drawn from a Gaussian distribution.
14
+ * The result is clamped to [min, max] so it never produces absurd values.
15
+ *
16
+ * @param min - Minimum delay in ms
17
+ * @param max - Maximum delay in ms
18
+ * @returns Delay in ms, Gaussian-distributed around the midpoint
19
+ */
20
+ export declare function humanDelay(min?: number, max?: number): number;
21
+ /**
22
+ * Sleep for the specified number of milliseconds.
23
+ */
24
+ export declare function sleep(ms: number): Promise<void>;
25
+ /**
26
+ * Generate a log-normal–distributed random delay.
27
+ *
28
+ * Log-normal produces the right-skewed, heavy-tailed shape that matches
29
+ * real keystroke timing data (Monaco et al., 2021). The distribution has
30
+ * a mode slightly below the median and occasional long-tail spikes — a
31
+ * signature of human typing that Gaussian distributions lack.
32
+ *
33
+ * @param median - Median delay in ms (the 50th-percentile value)
34
+ * @param sigma - Shape parameter controlling spread / skew (0.3–0.5 typical)
35
+ * @param floor - Hard floor in ms (below 40ms is physically impossible)
36
+ * @returns Delay in ms, rounded to an integer
37
+ */
38
+ export declare function logNormalDelay(median: number, sigma: number, floor?: number): number;
39
+ /**
40
+ * Generate a normally-distributed random value with the given mean and stddev.
41
+ * Convenience alias for gaussianRandom — named to match the spec convention
42
+ * used across humanize modules (e.g., `normalRandom(350, 100)`).
43
+ */
44
+ export declare function normalRandom(mean: number, stddev: number): number;
45
+ /**
46
+ * Compute a Gaussian-distributed click offset within an element's bounding box.
47
+ *
48
+ * Real humans click in a Gaussian distribution around the center of a target
49
+ * element, NOT at the exact geometric center. Dead-center clicks are a known
50
+ * bot detection fingerprint (DataDome, PerimeterX track this pattern).
51
+ *
52
+ * @param center - The geometric center of the element on this axis
53
+ * @param elementSize - The element's width or height on this axis
54
+ * @param elementStart - The element's x or y origin on this axis
55
+ * @returns A coordinate within the element bounds, Gaussian-distributed around center
56
+ */
57
+ export declare function gaussianClickOffset(center: number, elementSize: number, elementStart: number): number;
58
+ /**
59
+ * Check whether the LEAP_HUMANIZE env var is enabled.
60
+ * Returns false unless LEAP_HUMANIZE is explicitly set to "true" or "1".
61
+ */
62
+ export declare function isHumanizeEnabled(): boolean;
@@ -0,0 +1,100 @@
1
+ // ─── Humanize Shared Utilities ─────────────────────────────────────────────
2
+ //
3
+ // Pure math primitives shared across all humanize-* modules.
4
+ // Zero external dependencies. All distributions are self-contained.
5
+ //
6
+ // Standalone module — no cross-dependencies on other leapfrog modules.
7
+ /**
8
+ * Generate a normally-distributed random number using the Box-Muller transform.
9
+ * @param mean - Center of the distribution
10
+ * @param stddev - Standard deviation
11
+ * @returns A sample from N(mean, stddev^2)
12
+ */
13
+ export function gaussianRandom(mean = 0, stddev = 1) {
14
+ let u, v, s;
15
+ do {
16
+ u = Math.random() * 2 - 1;
17
+ v = Math.random() * 2 - 1;
18
+ s = u * u + v * v;
19
+ } while (s >= 1 || s === 0);
20
+ const mul = Math.sqrt(-2 * Math.log(s) / s);
21
+ return mean + stddev * u * mul;
22
+ }
23
+ /**
24
+ * Clamp a value between min and max.
25
+ */
26
+ export function clamp(val, min, max) {
27
+ return Math.max(min, Math.min(max, val));
28
+ }
29
+ /**
30
+ * Generate a human-like delay (in ms) drawn from a Gaussian distribution.
31
+ * The result is clamped to [min, max] so it never produces absurd values.
32
+ *
33
+ * @param min - Minimum delay in ms
34
+ * @param max - Maximum delay in ms
35
+ * @returns Delay in ms, Gaussian-distributed around the midpoint
36
+ */
37
+ export function humanDelay(min = 50, max = 200) {
38
+ const mean = (min + max) / 2;
39
+ const stddev = (max - min) / 6; // 99.7% of values fall within [min, max]
40
+ return Math.round(clamp(gaussianRandom(mean, stddev), min, max));
41
+ }
42
+ /**
43
+ * Sleep for the specified number of milliseconds.
44
+ */
45
+ export function sleep(ms) {
46
+ return new Promise((resolve) => setTimeout(resolve, ms));
47
+ }
48
+ /**
49
+ * Generate a log-normal–distributed random delay.
50
+ *
51
+ * Log-normal produces the right-skewed, heavy-tailed shape that matches
52
+ * real keystroke timing data (Monaco et al., 2021). The distribution has
53
+ * a mode slightly below the median and occasional long-tail spikes — a
54
+ * signature of human typing that Gaussian distributions lack.
55
+ *
56
+ * @param median - Median delay in ms (the 50th-percentile value)
57
+ * @param sigma - Shape parameter controlling spread / skew (0.3–0.5 typical)
58
+ * @param floor - Hard floor in ms (below 40ms is physically impossible)
59
+ * @returns Delay in ms, rounded to an integer
60
+ */
61
+ export function logNormalDelay(median, sigma, floor = 40) {
62
+ const mu = Math.log(median);
63
+ const normal = gaussianRandom(0, 1);
64
+ return Math.max(floor, Math.round(Math.exp(mu + sigma * normal)));
65
+ }
66
+ /**
67
+ * Generate a normally-distributed random value with the given mean and stddev.
68
+ * Convenience alias for gaussianRandom — named to match the spec convention
69
+ * used across humanize modules (e.g., `normalRandom(350, 100)`).
70
+ */
71
+ export function normalRandom(mean, stddev) {
72
+ return gaussianRandom(mean, stddev);
73
+ }
74
+ /**
75
+ * Compute a Gaussian-distributed click offset within an element's bounding box.
76
+ *
77
+ * Real humans click in a Gaussian distribution around the center of a target
78
+ * element, NOT at the exact geometric center. Dead-center clicks are a known
79
+ * bot detection fingerprint (DataDome, PerimeterX track this pattern).
80
+ *
81
+ * @param center - The geometric center of the element on this axis
82
+ * @param elementSize - The element's width or height on this axis
83
+ * @param elementStart - The element's x or y origin on this axis
84
+ * @returns A coordinate within the element bounds, Gaussian-distributed around center
85
+ */
86
+ export function gaussianClickOffset(center, elementSize, elementStart) {
87
+ const sigma = elementSize * 0.15;
88
+ const offset = gaussianRandom(center, sigma);
89
+ // Clamp within element bounds with a 5% inset margin on each side
90
+ const margin = elementSize * 0.05;
91
+ return clamp(offset, elementStart + margin, elementStart + elementSize - margin);
92
+ }
93
+ /**
94
+ * Check whether the LEAP_HUMANIZE env var is enabled.
95
+ * Returns false unless LEAP_HUMANIZE is explicitly set to "true" or "1".
96
+ */
97
+ export function isHumanizeEnabled() {
98
+ const val = process.env.LEAP_HUMANIZE;
99
+ return val === "true" || val === "1";
100
+ }