@nuasite/components 0.16.0 → 0.16.1
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/package.json +1 -1
- package/src/form/index.astro +47 -29
package/package.json
CHANGED
package/src/form/index.astro
CHANGED
|
@@ -112,13 +112,15 @@ const tokenId = `${tokenFieldName}_${formId}`
|
|
|
112
112
|
|
|
113
113
|
<script>
|
|
114
114
|
class AstroForm extends HTMLElement {
|
|
115
|
-
private interactionScore = 0;
|
|
116
115
|
private fieldInteractions = new Set<string>();
|
|
117
|
-
private
|
|
116
|
+
private hasPointerActivity = false;
|
|
118
117
|
private keyboardEvents = 0;
|
|
118
|
+
private inputEvents = 0;
|
|
119
119
|
private focusEvents = 0;
|
|
120
|
+
private scrolled = false;
|
|
120
121
|
private startTime = Date.now();
|
|
121
122
|
private honeypotFieldNames: string[] = []
|
|
123
|
+
private abortController = new AbortController()
|
|
122
124
|
|
|
123
125
|
connectedCallback() {
|
|
124
126
|
const form = this.querySelector('form')
|
|
@@ -289,50 +291,66 @@ const tokenId = `${tokenFieldName}_${formId}`
|
|
|
289
291
|
}
|
|
290
292
|
|
|
291
293
|
private setupInteractionTracking(form: HTMLFormElement) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
this.updateInteractionScore();
|
|
298
|
-
}, 100);
|
|
299
|
-
});
|
|
294
|
+
const signal = this.abortController.signal
|
|
295
|
+
|
|
296
|
+
document.addEventListener('mousemove', () => { this.hasPointerActivity = true }, { signal, once: true });
|
|
297
|
+
document.addEventListener('touchstart', () => { this.hasPointerActivity = true }, { signal, passive: true, once: true });
|
|
298
|
+
document.addEventListener('scroll', () => { this.scrolled = true }, { signal, passive: true, once: true });
|
|
300
299
|
|
|
301
300
|
form.addEventListener('keydown', (e) => {
|
|
302
301
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
303
302
|
this.keyboardEvents++;
|
|
304
|
-
this.updateInteractionScore();
|
|
305
303
|
}
|
|
306
|
-
});
|
|
304
|
+
}, { signal });
|
|
305
|
+
|
|
306
|
+
// Fires for autofill, paste, voice input, and virtual keyboards
|
|
307
|
+
form.addEventListener('input', (e) => {
|
|
308
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement) {
|
|
309
|
+
this.inputEvents++;
|
|
310
|
+
this.fieldInteractions.add(e.target.name);
|
|
311
|
+
}
|
|
312
|
+
}, { signal });
|
|
307
313
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
this.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
314
|
+
// Fires on selects, checkboxes, date pickers
|
|
315
|
+
form.addEventListener('change', (e) => {
|
|
316
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement) {
|
|
317
|
+
this.fieldInteractions.add(e.target.name);
|
|
318
|
+
}
|
|
319
|
+
}, { signal });
|
|
320
|
+
|
|
321
|
+
form.querySelectorAll<HTMLElement>('input:not([type="hidden"]):not([aria-hidden="true"]), textarea, select').forEach(input => {
|
|
322
|
+
input.addEventListener('focus', () => { this.focusEvents++ }, { signal });
|
|
315
323
|
});
|
|
316
324
|
}
|
|
317
325
|
|
|
318
|
-
|
|
319
|
-
this.
|
|
320
|
-
|
|
321
|
-
|
|
326
|
+
disconnectedCallback() {
|
|
327
|
+
this.abortController.abort()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private getInteractionScore(): number {
|
|
331
|
+
return (
|
|
332
|
+
(this.hasPointerActivity ? 1 : 0) +
|
|
333
|
+
(this.keyboardEvents > 5 || this.inputEvents > 2 ? 1 : 0) +
|
|
322
334
|
(this.focusEvents > 1 ? 1 : 0) +
|
|
323
|
-
(this.fieldInteractions.size > 1 ? 1 : 0)
|
|
335
|
+
(this.fieldInteractions.size > 1 ? 1 : 0) +
|
|
336
|
+
(this.scrolled ? 1 : 0)
|
|
337
|
+
);
|
|
324
338
|
}
|
|
325
339
|
|
|
326
340
|
private detectSuspiciousBrowser(): boolean {
|
|
341
|
+
const ua = window.navigator.userAgent
|
|
342
|
+
const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(ua)
|
|
343
|
+
|
|
327
344
|
// Check for headless browser indicators
|
|
328
345
|
const suspiciousIndicators = [
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
346
|
+
// WebDriver API presence (set to true in automated browsers)
|
|
347
|
+
navigator.webdriver === true,
|
|
348
|
+
// Empty plugins list is only suspicious on desktop — mobile browsers normally have none
|
|
349
|
+
!isMobile && !window.navigator.plugins.length,
|
|
332
350
|
!window.navigator.languages.length,
|
|
333
351
|
|
|
334
352
|
// Headless Chrome indicators
|
|
335
|
-
|
|
353
|
+
ua.includes('HeadlessChrome'),
|
|
336
354
|
window.outerWidth === 0,
|
|
337
355
|
window.outerHeight === 0,
|
|
338
356
|
|
|
@@ -350,7 +368,7 @@ const tokenId = `${tokenFieldName}_${formId}`
|
|
|
350
368
|
|
|
351
369
|
if (timeSpent < 3000) return false;
|
|
352
370
|
|
|
353
|
-
if (this.
|
|
371
|
+
if (this.getInteractionScore() < 2) return false;
|
|
354
372
|
|
|
355
373
|
for (const fieldName of this.honeypotFieldNames) {
|
|
356
374
|
const field = form.querySelector(`[name="${fieldName}"]`) as HTMLInputElement
|