@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 CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@nuasite/components",
3
3
  "description": "Nua Site astro components.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.16.0",
5
+ "version": "0.16.1",
6
6
  "files": [
7
7
  "dist/**",
8
8
  "src/**",
@@ -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 mouseMovements = 0;
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
- let mouseMoveTimeout: NodeJS.Timeout;
293
- document.addEventListener('mousemove', () => {
294
- clearTimeout(mouseMoveTimeout);
295
- mouseMoveTimeout = setTimeout(() => {
296
- this.mouseMovements++;
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
- const inputs = form.querySelectorAll('input[type="text"], input[type="email"], textarea');
309
- inputs.forEach(input => {
310
- input.addEventListener('focus', () => {
311
- this.focusEvents++;
312
- this.fieldInteractions.add((input as HTMLInputElement).name);
313
- this.updateInteractionScore();
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
- private updateInteractionScore() {
319
- this.interactionScore =
320
- (this.mouseMovements > 0 ? 1 : 0) +
321
- (this.keyboardEvents > 5 ? 1 : 0) +
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
- // Missing expected browser features
330
- !window.navigator.webdriver === undefined,
331
- !window.navigator.plugins.length,
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
- window.navigator.userAgent.includes('HeadlessChrome'),
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.interactionScore < 2) return false;
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