@tsdevstack/react-bot-detection 0.1.4

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.
@@ -0,0 +1,366 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { afterEach, beforeEach, describe, expect, it, rs } from "@rstest/core";
3
+ import { cleanup, render, screen, waitFor } from "@testing-library/react";
4
+ import user_event from "@testing-library/user-event";
5
+ import { BotProtectedForm } from "./bot-protected-form.js";
6
+ import { useBotDetection } from "../hooks/use-bot-detection.js";
7
+ import { useHoneypot } from "../hooks/use-honeypot.js";
8
+ afterEach(()=>{
9
+ cleanup();
10
+ });
11
+ rs.mock('../hooks/use-bot-detection', ()=>({
12
+ useBotDetection: rs.fn(()=>({
13
+ botScore: 0,
14
+ detectionReasons: [],
15
+ isBot: false,
16
+ handleFieldFocus: rs.fn(),
17
+ handleFormSubmit: rs.fn(()=>({
18
+ score: 0,
19
+ reasons: []
20
+ })),
21
+ stats: {
22
+ mouseMovements: 10,
23
+ typingEvents: 20,
24
+ focusEvents: 5,
25
+ timeSpent: 5000
26
+ }
27
+ }))
28
+ }));
29
+ rs.mock('../hooks/use-honeypot', ()=>({
30
+ useHoneypot: rs.fn(()=>({
31
+ isBotDetected: false,
32
+ HoneypotComponent: ()=>/*#__PURE__*/ jsx("div", {
33
+ "data-testid": "honeypot"
34
+ })
35
+ }))
36
+ }));
37
+ const mockUseBotDetection = useBotDetection;
38
+ const mockUseHoneypot = useHoneypot;
39
+ describe('BotProtectedForm', ()=>{
40
+ const mockOnSubmit = rs.fn();
41
+ beforeEach(()=>{
42
+ rs.clearAllMocks();
43
+ mockOnSubmit.mockResolvedValue(void 0);
44
+ mockUseBotDetection.mockReturnValue({
45
+ botScore: 0,
46
+ detectionReasons: [],
47
+ isBot: false,
48
+ handleFieldFocus: rs.fn(),
49
+ handleFormSubmit: rs.fn(()=>({
50
+ score: 0,
51
+ reasons: []
52
+ })),
53
+ stats: {
54
+ mouseMovements: 10,
55
+ typingEvents: 20,
56
+ focusEvents: 5,
57
+ timeSpent: 5000
58
+ }
59
+ });
60
+ mockUseHoneypot.mockReturnValue({
61
+ isBotDetected: false,
62
+ HoneypotComponent: ()=>/*#__PURE__*/ jsx("div", {
63
+ "data-testid": "honeypot"
64
+ })
65
+ });
66
+ });
67
+ describe('Rendering', ()=>{
68
+ it('should render children and submit button', ()=>{
69
+ render(/*#__PURE__*/ jsx(BotProtectedForm, {
70
+ onSubmit: mockOnSubmit,
71
+ children: /*#__PURE__*/ jsx("input", {
72
+ name: "email",
73
+ "data-testid": "email"
74
+ })
75
+ }));
76
+ expect(screen.getByTestId('email')).not.toBeNull();
77
+ expect(screen.getByRole('button', {
78
+ name: 'Submit'
79
+ })).not.toBeNull();
80
+ });
81
+ it('should render honeypot component', ()=>{
82
+ render(/*#__PURE__*/ jsx(BotProtectedForm, {
83
+ onSubmit: mockOnSubmit,
84
+ children: /*#__PURE__*/ jsx("input", {
85
+ name: "email"
86
+ })
87
+ }));
88
+ expect(screen.getByTestId('honeypot')).not.toBeNull();
89
+ });
90
+ it('should use custom submit button text', ()=>{
91
+ render(/*#__PURE__*/ jsx(BotProtectedForm, {
92
+ onSubmit: mockOnSubmit,
93
+ submitButtonText: "Send",
94
+ children: /*#__PURE__*/ jsx("input", {
95
+ name: "email"
96
+ })
97
+ }));
98
+ expect(screen.getByRole('button', {
99
+ name: 'Send'
100
+ })).not.toBeNull();
101
+ });
102
+ it('should apply className to form', ()=>{
103
+ render(/*#__PURE__*/ jsx(BotProtectedForm, {
104
+ onSubmit: mockOnSubmit,
105
+ className: "my-form-class",
106
+ children: /*#__PURE__*/ jsx("input", {
107
+ name: "email"
108
+ })
109
+ }));
110
+ const form = screen.getByRole('button').closest('form');
111
+ expect(form?.classList.contains('my-form-class')).toBe(true);
112
+ });
113
+ });
114
+ describe('Custom Button Component', ()=>{
115
+ it('should use custom ButtonComponent', ()=>{
116
+ const CustomButton = ({ children, disabled, type })=>/*#__PURE__*/ jsxs("button", {
117
+ type: type,
118
+ disabled: disabled,
119
+ "data-testid": "custom-button",
120
+ children: [
121
+ "Custom: ",
122
+ children
123
+ ]
124
+ });
125
+ render(/*#__PURE__*/ jsx(BotProtectedForm, {
126
+ onSubmit: mockOnSubmit,
127
+ ButtonComponent: CustomButton,
128
+ children: /*#__PURE__*/ jsx("input", {
129
+ name: "email"
130
+ })
131
+ }));
132
+ expect(screen.getByTestId('custom-button')).not.toBeNull();
133
+ expect(screen.getByText('Custom: Submit')).not.toBeNull();
134
+ });
135
+ });
136
+ describe('Form Submission', ()=>{
137
+ it('should call onSubmit with FormData and bot detection result', async ()=>{
138
+ const user = user_event.setup();
139
+ render(/*#__PURE__*/ jsx(BotProtectedForm, {
140
+ onSubmit: mockOnSubmit,
141
+ children: /*#__PURE__*/ jsx("input", {
142
+ name: "email",
143
+ defaultValue: "test@example.com"
144
+ })
145
+ }));
146
+ await user.click(screen.getByRole('button', {
147
+ name: 'Submit'
148
+ }));
149
+ await waitFor(()=>{
150
+ expect(mockOnSubmit).toHaveBeenCalledTimes(1);
151
+ });
152
+ const [formData, botResult] = mockOnSubmit.mock.calls[0];
153
+ expect(formData).toBeInstanceOf(FormData);
154
+ expect(formData.get('email')).toBe('test@example.com');
155
+ expect(botResult).toMatchObject({
156
+ score: 0,
157
+ reasons: [],
158
+ isBot: false,
159
+ honeypotTriggered: false
160
+ });
161
+ });
162
+ it('should show loading text while submitting', async ()=>{
163
+ const user = user_event.setup();
164
+ let resolveSubmit;
165
+ mockOnSubmit.mockImplementation(()=>new Promise((resolve)=>{
166
+ resolveSubmit = resolve;
167
+ }));
168
+ render(/*#__PURE__*/ jsx(BotProtectedForm, {
169
+ onSubmit: mockOnSubmit,
170
+ submitButtonText: "Submit",
171
+ loadingButtonText: "Processing...",
172
+ children: /*#__PURE__*/ jsx("input", {
173
+ name: "email"
174
+ })
175
+ }));
176
+ await user.click(screen.getByRole('button', {
177
+ name: 'Submit'
178
+ }));
179
+ await waitFor(()=>{
180
+ expect(screen.getByRole('button', {
181
+ name: 'Processing...'
182
+ })).not.toBeNull();
183
+ });
184
+ resolveSubmit();
185
+ await waitFor(()=>{
186
+ expect(screen.getByRole('button', {
187
+ name: 'Submit'
188
+ })).not.toBeNull();
189
+ });
190
+ });
191
+ });
192
+ describe('Bot Detection Blocking', ()=>{
193
+ it('should not call onSubmit when behavioral bot detection triggers', async ()=>{
194
+ const user = user_event.setup();
195
+ mockUseBotDetection.mockReturnValue({
196
+ botScore: 100,
197
+ detectionReasons: [
198
+ 'Suspicious behavior'
199
+ ],
200
+ isBot: true,
201
+ handleFieldFocus: rs.fn(),
202
+ handleFormSubmit: rs.fn(()=>({
203
+ score: 100,
204
+ reasons: [
205
+ 'Suspicious behavior'
206
+ ]
207
+ })),
208
+ stats: {
209
+ mouseMovements: 0,
210
+ typingEvents: 0,
211
+ focusEvents: 0,
212
+ timeSpent: 100
213
+ }
214
+ });
215
+ render(/*#__PURE__*/ jsx(BotProtectedForm, {
216
+ onSubmit: mockOnSubmit,
217
+ children: /*#__PURE__*/ jsx("input", {
218
+ name: "email"
219
+ })
220
+ }));
221
+ await user.click(screen.getByRole('button', {
222
+ name: 'Submit'
223
+ }));
224
+ expect(mockOnSubmit).not.toHaveBeenCalled();
225
+ });
226
+ it('should not call onSubmit when honeypot is triggered', async ()=>{
227
+ const user = user_event.setup();
228
+ mockUseHoneypot.mockReturnValue({
229
+ isBotDetected: true,
230
+ HoneypotComponent: ()=>/*#__PURE__*/ jsx("div", {
231
+ "data-testid": "honeypot"
232
+ })
233
+ });
234
+ render(/*#__PURE__*/ jsx(BotProtectedForm, {
235
+ onSubmit: mockOnSubmit,
236
+ children: /*#__PURE__*/ jsx("input", {
237
+ name: "email"
238
+ })
239
+ }));
240
+ await user.click(screen.getByRole('button', {
241
+ name: 'Submit'
242
+ }));
243
+ expect(mockOnSubmit).not.toHaveBeenCalled();
244
+ });
245
+ it('should not disable submit button based on stale bot detection state', ()=>{
246
+ mockUseBotDetection.mockReturnValue({
247
+ botScore: 100,
248
+ detectionReasons: [
249
+ 'Suspicious behavior'
250
+ ],
251
+ isBot: true,
252
+ handleFieldFocus: rs.fn(),
253
+ handleFormSubmit: rs.fn(),
254
+ stats: {
255
+ mouseMovements: 0,
256
+ typingEvents: 0,
257
+ focusEvents: 0,
258
+ timeSpent: 100
259
+ }
260
+ });
261
+ render(/*#__PURE__*/ jsx(BotProtectedForm, {
262
+ onSubmit: mockOnSubmit,
263
+ children: /*#__PURE__*/ jsx("input", {
264
+ name: "email"
265
+ })
266
+ }));
267
+ const button = screen.getByRole('button', {
268
+ name: 'Submit'
269
+ });
270
+ expect(button.disabled).toBe(false);
271
+ });
272
+ });
273
+ describe('onBotDetected callback', ()=>{
274
+ it('should call onBotDetected when form detects a bot during submission', async ()=>{
275
+ const user = user_event.setup();
276
+ const mockOnBotDetected = rs.fn();
277
+ mockUseBotDetection.mockReturnValue({
278
+ botScore: 0,
279
+ detectionReasons: [],
280
+ isBot: false,
281
+ handleFieldFocus: rs.fn(),
282
+ handleFormSubmit: rs.fn(()=>({
283
+ score: 80,
284
+ reasons: [
285
+ 'Too fast submission'
286
+ ]
287
+ })),
288
+ stats: {
289
+ mouseMovements: 0,
290
+ typingEvents: 0,
291
+ focusEvents: 0,
292
+ timeSpent: 100
293
+ }
294
+ });
295
+ render(/*#__PURE__*/ jsx(BotProtectedForm, {
296
+ onSubmit: mockOnSubmit,
297
+ onBotDetected: mockOnBotDetected,
298
+ children: /*#__PURE__*/ jsx("input", {
299
+ name: "email"
300
+ })
301
+ }));
302
+ await user.click(screen.getByRole('button', {
303
+ name: 'Submit'
304
+ }));
305
+ await waitFor(()=>{
306
+ expect(mockOnBotDetected).toHaveBeenCalledWith(expect.objectContaining({
307
+ score: 80,
308
+ isBot: true,
309
+ reasons: expect.arrayContaining([
310
+ 'Too fast submission'
311
+ ])
312
+ }));
313
+ });
314
+ expect(mockOnSubmit).not.toHaveBeenCalled();
315
+ });
316
+ });
317
+ describe('Debug Panel', ()=>{
318
+ it('should not render debug panel by default', ()=>{
319
+ render(/*#__PURE__*/ jsx(BotProtectedForm, {
320
+ onSubmit: mockOnSubmit,
321
+ children: /*#__PURE__*/ jsx("input", {
322
+ name: "email"
323
+ })
324
+ }));
325
+ expect(screen.queryByText('Bot Detection Debug')).toBeNull();
326
+ });
327
+ it('should render debug panel when showDebugPanel is true', async ()=>{
328
+ render(/*#__PURE__*/ jsx(BotProtectedForm, {
329
+ onSubmit: mockOnSubmit,
330
+ showDebugPanel: true,
331
+ children: /*#__PURE__*/ jsx("input", {
332
+ name: "email"
333
+ })
334
+ }));
335
+ await waitFor(()=>{
336
+ expect(screen.getByText('Bot Detection Debug')).not.toBeNull();
337
+ });
338
+ });
339
+ });
340
+ describe('Error Handling', ()=>{
341
+ it('should handle submission errors gracefully', async ()=>{
342
+ const user = user_event.setup();
343
+ const consoleError = rs.spyOn(console, 'error').mockImplementation(()=>{});
344
+ mockOnSubmit.mockRejectedValue(new Error('Network error'));
345
+ render(/*#__PURE__*/ jsx(BotProtectedForm, {
346
+ onSubmit: mockOnSubmit,
347
+ children: /*#__PURE__*/ jsx("input", {
348
+ name: "email"
349
+ })
350
+ }));
351
+ await user.click(screen.getByRole('button', {
352
+ name: 'Submit'
353
+ }));
354
+ await waitFor(()=>{
355
+ expect(consoleError).toHaveBeenCalledWith('Form submission error:', expect.any(Error));
356
+ });
357
+ await waitFor(()=>{
358
+ const button = screen.getByRole('button', {
359
+ name: 'Submit'
360
+ });
361
+ expect(button.disabled).toBe(false);
362
+ });
363
+ consoleError.mockRestore();
364
+ });
365
+ });
366
+ });
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ /**
3
+ * Default honeypot field names - attractive to bots but NOT to browser autofill.
4
+ *
5
+ * We use prefixed names that:
6
+ * 1. Look like real fields to bots scanning for input names
7
+ * 2. Don't match browser autofill heuristics (email, phone, url, address patterns)
8
+ *
9
+ * The prefix "hp_" makes them unique enough to avoid autofill while
10
+ * the suffixes still look like fields bots want to fill.
11
+ */
12
+ export declare const DEFAULT_HONEYPOT_FIELDS: readonly ["hp_contact_info", "hp_website_url", "hp_fax_number", "hp_company_name"];
13
+ export interface HoneypotProps {
14
+ /** Callback when bot is detected (any honeypot field was filled) */
15
+ onBotDetected: () => void;
16
+ /** Custom field names (defaults to email_confirm, website, url, phone_number) */
17
+ fieldNames?: string[];
18
+ }
19
+ /**
20
+ * Hidden honeypot fields that bots typically fill out.
21
+ *
22
+ * These fields are invisible to humans but visible to bots
23
+ * that parse the DOM looking for form fields to fill.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * <Honeypot onBotDetected={() => setIsBot(true)} />
28
+ * ```
29
+ */
30
+ export declare function Honeypot({ onBotDetected, fieldNames, }: HoneypotProps): React.ReactElement;
@@ -0,0 +1,40 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useCallback, useRef } from "react";
4
+ const DEFAULT_HONEYPOT_FIELDS = [
5
+ 'hp_contact_info',
6
+ 'hp_website_url',
7
+ 'hp_fax_number',
8
+ 'hp_company_name'
9
+ ];
10
+ function Honeypot({ onBotDetected, fieldNames = DEFAULT_HONEYPOT_FIELDS }) {
11
+ const hasTriggered = useRef(false);
12
+ const handleFieldChange = useCallback((e)=>{
13
+ if ('' !== e.target.value.trim() && !hasTriggered.current) {
14
+ hasTriggered.current = true;
15
+ onBotDetected();
16
+ }
17
+ }, [
18
+ onBotDetected
19
+ ]);
20
+ return /*#__PURE__*/ jsx("div", {
21
+ style: {
22
+ position: 'absolute',
23
+ left: '-9999px',
24
+ opacity: 0,
25
+ pointerEvents: 'none'
26
+ },
27
+ "aria-hidden": "true",
28
+ children: fieldNames.map((fieldName)=>/*#__PURE__*/ jsx("input", {
29
+ type: "text",
30
+ name: fieldName,
31
+ onChange: handleFieldChange,
32
+ tabIndex: -1,
33
+ autoComplete: "new-password",
34
+ "aria-hidden": "true",
35
+ "data-lpignore": "true",
36
+ "data-1p-ignore": "true"
37
+ }, fieldName))
38
+ });
39
+ }
40
+ export { DEFAULT_HONEYPOT_FIELDS, Honeypot };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,186 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { afterEach, describe, expect, it, rs } from "@rstest/core";
4
+ import { act, cleanup, fireEvent, render } from "@testing-library/react";
5
+ import { DEFAULT_HONEYPOT_FIELDS, Honeypot } from "./honeypot.js";
6
+ afterEach(()=>{
7
+ cleanup();
8
+ });
9
+ describe('Honeypot', ()=>{
10
+ describe('rendering', ()=>{
11
+ it('should render without crashing', ()=>{
12
+ const onBotDetected = rs.fn();
13
+ const { container } = render(/*#__PURE__*/ jsx(Honeypot, {
14
+ onBotDetected: onBotDetected
15
+ }));
16
+ expect(container.firstChild).not.toBeNull();
17
+ });
18
+ it('should render hidden fields', ()=>{
19
+ const onBotDetected = rs.fn();
20
+ render(/*#__PURE__*/ jsx(Honeypot, {
21
+ onBotDetected: onBotDetected
22
+ }));
23
+ const hiddenWrapper = document.querySelector('[aria-hidden="true"]');
24
+ expect(hiddenWrapper).not.toBeNull();
25
+ });
26
+ it('should position fields off-screen', ()=>{
27
+ const onBotDetected = rs.fn();
28
+ render(/*#__PURE__*/ jsx(Honeypot, {
29
+ onBotDetected: onBotDetected
30
+ }));
31
+ const hiddenWrapper = document.querySelector('[aria-hidden="true"]');
32
+ expect(hiddenWrapper.style.position).toBe('absolute');
33
+ expect(hiddenWrapper.style.left).toBe('-9999px');
34
+ });
35
+ it('should render all default honeypot fields', ()=>{
36
+ const onBotDetected = rs.fn();
37
+ render(/*#__PURE__*/ jsx(Honeypot, {
38
+ onBotDetected: onBotDetected
39
+ }));
40
+ for (const fieldName of DEFAULT_HONEYPOT_FIELDS)expect(document.querySelector(`[name="${fieldName}"]`)).not.toBeNull();
41
+ });
42
+ });
43
+ describe('bot detection', ()=>{
44
+ it('should not call onBotDetected when fields are empty', ()=>{
45
+ const onBotDetected = rs.fn();
46
+ render(/*#__PURE__*/ jsx(Honeypot, {
47
+ onBotDetected: onBotDetected
48
+ }));
49
+ expect(onBotDetected).not.toHaveBeenCalled();
50
+ });
51
+ it('should call onBotDetected when any field is filled', async ()=>{
52
+ const onBotDetected = rs.fn();
53
+ render(/*#__PURE__*/ jsx(Honeypot, {
54
+ onBotDetected: onBotDetected
55
+ }));
56
+ const websiteField = document.querySelector('[name="hp_website_url"]');
57
+ await act(async ()=>{
58
+ fireEvent.change(websiteField, {
59
+ target: {
60
+ value: 'spam-value'
61
+ }
62
+ });
63
+ });
64
+ expect(onBotDetected).toHaveBeenCalledTimes(1);
65
+ });
66
+ it('should trigger on first field filled', async ()=>{
67
+ const onBotDetected = rs.fn();
68
+ render(/*#__PURE__*/ jsx(Honeypot, {
69
+ onBotDetected: onBotDetected
70
+ }));
71
+ const contactField = document.querySelector('[name="hp_contact_info"]');
72
+ await act(async ()=>{
73
+ fireEvent.change(contactField, {
74
+ target: {
75
+ value: 'bot@spam.com'
76
+ }
77
+ });
78
+ });
79
+ expect(onBotDetected).toHaveBeenCalledTimes(1);
80
+ });
81
+ it('should only trigger once even if multiple fields filled', async ()=>{
82
+ const onBotDetected = rs.fn();
83
+ render(/*#__PURE__*/ jsx(Honeypot, {
84
+ onBotDetected: onBotDetected
85
+ }));
86
+ const websiteField = document.querySelector('[name="hp_website_url"]');
87
+ const faxField = document.querySelector('[name="hp_fax_number"]');
88
+ await act(async ()=>{
89
+ fireEvent.change(websiteField, {
90
+ target: {
91
+ value: 'spam1'
92
+ }
93
+ });
94
+ fireEvent.change(faxField, {
95
+ target: {
96
+ value: 'spam2'
97
+ }
98
+ });
99
+ });
100
+ expect(onBotDetected).toHaveBeenCalledTimes(1);
101
+ });
102
+ it('should not trigger on whitespace-only input', async ()=>{
103
+ const onBotDetected = rs.fn();
104
+ render(/*#__PURE__*/ jsx(Honeypot, {
105
+ onBotDetected: onBotDetected
106
+ }));
107
+ const websiteField = document.querySelector('[name="hp_website_url"]');
108
+ await act(async ()=>{
109
+ fireEvent.change(websiteField, {
110
+ target: {
111
+ value: ' '
112
+ }
113
+ });
114
+ });
115
+ expect(onBotDetected).not.toHaveBeenCalled();
116
+ });
117
+ });
118
+ describe('accessibility', ()=>{
119
+ it('should have aria-hidden attribute on wrapper', ()=>{
120
+ const onBotDetected = rs.fn();
121
+ render(/*#__PURE__*/ jsx(Honeypot, {
122
+ onBotDetected: onBotDetected
123
+ }));
124
+ const hiddenWrapper = document.querySelector('[aria-hidden="true"]');
125
+ expect(hiddenWrapper).not.toBeNull();
126
+ });
127
+ it('should have tabindex -1 on fields', ()=>{
128
+ const onBotDetected = rs.fn();
129
+ render(/*#__PURE__*/ jsx(Honeypot, {
130
+ onBotDetected: onBotDetected
131
+ }));
132
+ const field = document.querySelector('[name="hp_contact_info"]');
133
+ expect(field.tabIndex).toBe(-1);
134
+ });
135
+ it('should have autocomplete new-password on fields to prevent autofill', ()=>{
136
+ const onBotDetected = rs.fn();
137
+ render(/*#__PURE__*/ jsx(Honeypot, {
138
+ onBotDetected: onBotDetected
139
+ }));
140
+ const field = document.querySelector('[name="hp_contact_info"]');
141
+ expect(field.getAttribute('autocomplete')).toBe('new-password');
142
+ });
143
+ it('should have password manager ignore attributes', ()=>{
144
+ const onBotDetected = rs.fn();
145
+ render(/*#__PURE__*/ jsx(Honeypot, {
146
+ onBotDetected: onBotDetected
147
+ }));
148
+ const field = document.querySelector('[name="hp_contact_info"]');
149
+ expect(field.getAttribute('data-lpignore')).toBe('true');
150
+ expect(field.getAttribute('data-1p-ignore')).toBe('true');
151
+ });
152
+ });
153
+ describe('custom field names', ()=>{
154
+ it('should use custom field names', ()=>{
155
+ const onBotDetected = rs.fn();
156
+ render(/*#__PURE__*/ jsx(Honeypot, {
157
+ onBotDetected: onBotDetected,
158
+ fieldNames: [
159
+ 'custom_trap',
160
+ 'fake_phone'
161
+ ]
162
+ }));
163
+ expect(document.querySelector('[name="custom_trap"]')).not.toBeNull();
164
+ expect(document.querySelector('[name="fake_phone"]')).not.toBeNull();
165
+ expect(document.querySelector('[name="email_confirm"]')).toBeNull();
166
+ });
167
+ it('should trigger detection on custom fields', async ()=>{
168
+ const onBotDetected = rs.fn();
169
+ render(/*#__PURE__*/ jsx(Honeypot, {
170
+ onBotDetected: onBotDetected,
171
+ fieldNames: [
172
+ 'my_honeypot'
173
+ ]
174
+ }));
175
+ const customField = document.querySelector('[name="my_honeypot"]');
176
+ await act(async ()=>{
177
+ fireEvent.change(customField, {
178
+ target: {
179
+ value: 'bot-filled'
180
+ }
181
+ });
182
+ });
183
+ expect(onBotDetected).toHaveBeenCalled();
184
+ });
185
+ });
186
+ });
@@ -0,0 +1,4 @@
1
+ export { Honeypot, DEFAULT_HONEYPOT_FIELDS } from './honeypot';
2
+ export type { HoneypotProps } from './honeypot';
3
+ export { BotProtectedForm } from './bot-protected-form';
4
+ export type { BotProtectedFormProps } from './bot-protected-form';
@@ -0,0 +1,3 @@
1
+ import { DEFAULT_HONEYPOT_FIELDS, Honeypot } from "./honeypot.js";
2
+ import { BotProtectedForm } from "./bot-protected-form.js";
3
+ export { BotProtectedForm, DEFAULT_HONEYPOT_FIELDS, Honeypot };
@@ -0,0 +1,4 @@
1
+ export { useBotDetection } from './use-bot-detection';
2
+ export type { UseBotDetectionReturn } from './use-bot-detection';
3
+ export { useHoneypot } from './use-honeypot';
4
+ export type { UseHoneypotReturn } from './use-honeypot';
@@ -0,0 +1,3 @@
1
+ import { useBotDetection } from "./use-bot-detection.js";
2
+ import { useHoneypot } from "./use-honeypot.js";
3
+ export { useBotDetection, useHoneypot };
@@ -0,0 +1,31 @@
1
+ import type { BotDetectionResult, BotDetectionStats } from '../types';
2
+ export interface UseBotDetectionReturn {
3
+ botScore: number;
4
+ detectionReasons: string[];
5
+ isBot: boolean;
6
+ analyzeBehavior: () => BotDetectionResult;
7
+ handleFieldFocus: () => void;
8
+ handleFormSubmit: () => BotDetectionResult;
9
+ stats: BotDetectionStats;
10
+ }
11
+ /**
12
+ * Hook for behavioral bot detection.
13
+ *
14
+ * Tracks mouse movements, typing patterns, and form interactions
15
+ * to detect automated form submissions.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * const { handleFormSubmit, isBot } = useBotDetection();
20
+ *
21
+ * const onSubmit = () => {
22
+ * const result = handleFormSubmit();
23
+ * if (result.isBot) {
24
+ * console.log('Bot detected:', result.reasons);
25
+ * return;
26
+ * }
27
+ * // Proceed with submission
28
+ * };
29
+ * ```
30
+ */
31
+ export declare function useBotDetection(): UseBotDetectionReturn;