PHPackages                             willybahuaud/gaitcha - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Security](/categories/security)
4. /
5. willybahuaud/gaitcha

ActiveLibrary[Security](/categories/security)

willybahuaud/gaitcha
====================

Frictionless, self-hosted CAPTCHA combining behavioral analysis, random field names, and HMAC tokens.

v0.6.0(1mo ago)23↓100%1GPL-2.0-or-laterPHPPHP &gt;=7.4

Since Mar 7Pushed 1mo agoCompare

[ Source](https://github.com/willybahuaud/gaitcha)[ Packagist](https://packagist.org/packages/willybahuaud/gaitcha)[ RSS](/packages/willybahuaud-gaitcha/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (1)Versions (10)Used By (1)

Gaitcha
=======

[](#gaitcha)

Self-hosted behavioral captcha. A simple checkbox analyzes how the user interacts with it — mouse trajectory, keyboard timing, touch gestures — to tell humans from bots. No third-party dependency, no tracking, no friction.

Why
---

[](#why)

Most captcha solutions either rely on third-party services (sending user data to external servers) or use proof-of-work challenges that automated browsers can solve trivially.

Gaitcha takes a different approach: it watches **how** the user reaches and checks a visible checkbox. Humans hesitate, deviate, decelerate, click slightly off-center. Bots click perfectly, instantly, without inertia. The behavioral log is scored server-side — no external API, no user fingerprinting, fully stateless.

Quick Start
-----------

[](#quick-start)

### Install

[](#install)

```
composer require willybahuaud/gaitcha
```

Then build the JS client:

```
npm install && npm run build
```

This generates `dist/gaitcha.min.js` — copy it to your public assets directory and include it in your HTML.

### HTML

[](#html)

```

    Send

```

### PHP — Init endpoint

[](#php--init-endpoint)

```
use Gaitcha\Config;
use Gaitcha\AbstractEndpoint;

$config = new Config([
    'secret' => 'your-secret-key-at-least-32-characters',
]);

class CaptchaEndpoint extends AbstractEndpoint
{
    protected function sendJsonResponse(array $data): void
    {
        header('Content-Type: application/json');
        echo json_encode($data);
    }

    public function handle(): void
    {
        $this->sendJsonResponse($this->handleInit());
    }
}

$endpoint = new CaptchaEndpoint($config);
$endpoint->handle();
```

### PHP — Validation

[](#php--validation)

```
use Gaitcha\Config;
use Gaitcha\ValidationOrchestrator;

$config       = new Config(['secret' => 'your-secret-key-at-least-32-characters']);
$orchestrator = new ValidationOrchestrator($config);
$result       = $orchestrator->validate($_POST);

if ($result->isAccepted()) {
    // Process the form.
} else {
    // $result->getReason():
    // token_absent | token_invalid | token_expired
    // token_already_used | score_insufficient | log_malformed
}
```

### Manual JS init

[](#manual-js-init)

```
const instance = Gaitcha.init(document.querySelector('#my-form'), '/captcha/init', {
    label: 'I am not a robot',
    container: document.getElementById('captcha-slot'), // optional target element
    theme: 'auto', // 'light' (default), 'dark', or 'auto' (follows OS preference)
});
```

`init()` returns an instance with `destroy()` and `reset()` (see [Widget reset](#widget-reset) below).

How It Works
------------

[](#how-it-works)

1. The form loads normally — no captcha field
2. On the first interaction signal (mousemove, touchstart, focus, keydown), an Ajax request fetches a signed token and a random field name
3. A self-contained widget (checkbox + badge) is injected into the form
4. The JS collects interaction events: mouse moves, touch moves (with pressure and contact radius when available), keyboard tabs, and timing data
5. When the user checks the widget, the behavioral log is serialized immediately — ready for both classic form submits and AJAX-based plugins
6. The server verifies the token (signature + TTL) and scores the behavior across multiple signals

The scoring engine detects three profiles and uses the one that matches the check event:

- **Mouse** — trajectory shape, non-linearity, speed variation, angular jitter, direction reversals, endpoint deceleration, click offset, anti-Bezier signals, anti-CDP signals (coalesced events average, screen coordinate delta)
- **Keyboard** — focus-to-key timing, dwell time variance, navigation pattern (Tab/Shift+Tab)
- **Touch** — same trajectory signals as mouse, plus touch-specific data: pressure variance across the swipe, contact radius variance, and tap gesture analysis (duration, force, radiusX/Y on the final tap)

Multiple "kill signals" cause immediate rejection: interaction under 100ms, no movement before click, pixel-perfect center click/tap. If the primary profile doesn't kill, a secondary profile is scored when data exists — the highest score wins (benefit of the doubt for the human).

Widget
------

[](#widget)

The widget is a self-contained UI component injected at runtime: custom checkbox with animated states (idle, loading with spinner, checked with bounce), a "gaitcha" badge, and hidden inputs for the token and behavioral log. All styles are injected via a single `` tag — no external CSS file needed.

### Theming

[](#theming)

Three modes, set via the `theme` option in `Gaitcha.init()` (not available as an HTML attribute — auto-init always uses `light`):

ValueBehavior`'light'`Light background (default)`'dark'`Dark background, forced`'auto'`Follows OS preference via `prefers-color-scheme`All CSS variables are scoped to `.gaitcha-widget` (no `:root` pollution). Every property uses `!important` to survive third-party form plugin CSS that tends to override everything.

### Responsive layout

[](#responsive-layout)

The widget is fluid (`width: 100%`, `max-width: 260px`). On narrow containers, a CSS container query on the content area switches the badge to compact mode — the brand name collapses to a "g" overlay on the shield icon. No media queries, so it adapts to the actual available space regardless of viewport size.

### Widget reset

[](#widget-reset)

After a server-side rejection on AJAX forms, the widget needs to go back to an unchecked state so the user can retry. Two ways to do it:

```
// Via the instance returned by init()
const instance = Gaitcha.init(form, endpoint, options);
// ... after rejection:
instance.reset();

// Or via the static API
Gaitcha.reset(form);
```

`reset()` unchecks the widget, clears the behavioral log, and fetches a fresh token from the server. The user gets a clean slate for a new attempt.

Configuration
-------------

[](#configuration)

OptionTypeDefaultDescription`secret`string**required**HMAC secret key (min 32 characters)`ttl`int`120`Token validity duration (seconds)`score_threshold`float`0.5`Minimum behavioral score (0.0–1.0)`debug`bool`false`Include scoring details in the response`no_js_fallback`string`'reject'``'reject'` or `'allow'` when JS is disabled`token_field_name`string`'_ct'`Hidden field name for the signed token`field_prefix`string`'_gc_'`Prefix for generated field names`anti_replay`bool`false`Reject reused tokens (requires a `token_store`)`token_store`TokenStoreInterface`null`Storage backend for anti-replay### Anti-replay

[](#anti-replay)

```
use Gaitcha\Config;
use Gaitcha\FileTokenStore;

$config = new Config([
    'secret'       => 'your-secret-key-at-least-32-characters',
    'anti_replay'  => true,
    'token_store'  => new FileTokenStore('/tmp/gaitcha-tokens.json'),
]);
```

`FileTokenStore` works for moderate traffic. For high-traffic sites, implement `TokenStoreInterface` with Redis or your database — the `checkAndAdd()` method must be atomic (e.g. `SETNX` for Redis, `INSERT ... ON CONFLICT` for SQL).

### HTML attributes

[](#html-attributes)

AttributeDescription`data-gaitcha`Enables Gaitcha on the form`data-gaitcha-endpoint`Init endpoint URL (default: `/captcha/init`)`data-gaitcha-label`Checkbox label (default: "Je ne suis pas un robot")`data-gaitcha-container`ID of a DOM element where the checkbox should be injectedLimits
------

[](#limits)

- Not bulletproof against targeted attacks with headed browsers and behavioral simulation — but that level of effort is better addressed by rate limiting
- Requires JavaScript (configurable fallback for no-JS users)
- Designed to stop mass spam, not to protect high-value targets

Development
-----------

[](#development)

```
composer install && npm install

# PHP tests
composer test

# Build JS (→ dist/gaitcha.min.js)
npm run build

# Demo (watch + PHP server)
npm run dev &
npm run serve
# → http://localhost:8080
```

WordPress plugin
----------------

[](#wordpress-plugin)

Using WordPress? Check out [Gaitcha for WordPress](https://github.com/willybahuaud/gaitcha-for-wp) — a ready-made plugin with connectors for CF7, Gravity Forms, WPForms, Fluent Forms, Formidable, Ninja Forms, WS Form, Elementor Pro, and native WordPress forms (login, register, lost password, comments).

Author
------

[](#author)

[Willy Bahuaud](https://wabeo.fr) — WordPress Architect

License
-------

[](#license)

GPL-2.0-or-later

###  Health Score

36

—

LowBetter than 81% of packages

Maintenance94

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity29

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

7

Last Release

58d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/0e8a7bedebdd7f13546cf2503217cced5340f74c2b64ec00e47e32b16412bb95?d=identicon)[willybahuaud](/maintainers/willybahuaud)

---

Top Contributors

[![willybahuaud](https://avatars.githubusercontent.com/u/1868803?v=4)](https://github.com/willybahuaud "willybahuaud (22 commits)")

---

Tags

anti-spambehavioral-analysisbot-detectioncaptchaphpself-hostedspam-protectionwordpress

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/willybahuaud-gaitcha/health.svg)

```
[![Health](https://phpackages.com/badges/willybahuaud-gaitcha/health.svg)](https://phpackages.com/packages/willybahuaud-gaitcha)
```

###  Alternatives

[defuse/php-encryption

Secure PHP Encryption Library

3.9k162.4M212](/packages/defuse-php-encryption)[roave/security-advisories

Prevents installation of composer packages with known security vulnerabilities: no API, simply require it

2.9k97.3M6.4k](/packages/roave-security-advisories)[mews/purifier

Laravel 5/6/7/8/9/10 HtmlPurifier Package

2.0k16.7M112](/packages/mews-purifier)[robrichards/xmlseclibs

A PHP library for XML Security

41278.1M118](/packages/robrichards-xmlseclibs)[bjeavons/zxcvbn-php

Realistic password strength estimation PHP library based on Zxcvbn JS

86917.5M63](/packages/bjeavons-zxcvbn-php)[enlightn/security-checker

A PHP dependency vulnerabilities scanner based on the Security Advisories Database.

33732.2M110](/packages/enlightn-security-checker)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
