Skip to content

About

A sliding puzzle CAPTCHA with two identical gaps and a curved, wobbling piece path — so solving it means deciding which gap fits. Zero dependencies, with FastAPI and Express reference servers.

Topics

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

Wobble-Captcha

A sliding puzzle CAPTCHA with two identical gaps and a piece that travels a curved, vertically wobbling path — so solving it means deciding which gap the piece belongs to, not just how far to drag.

Zero dependencies, ~2200 lines of vanilla ES modules, works without a build step.

Live demo → · 简体中文 · Protocol · Security model

The puzzle piece rises and falls along a curved path as the slider moves, past two identical gaps; it snaps into the right-hand one, where the lamp stand continues through it.

Two identical gaps, one piece. The lamp stand only lines up through one of them — that continuity is the whole puzzle.

The demo runs a mock server inside your browser so it can be hosted on GitHub Pages. It generates and grades challenges on the client, which means it is a visual demo and not a security boundary. Production requires a server — two reference implementations are included.


Why two gaps

A conventional slider CAPTCHA has one notch. Finding a single dark puzzle-shaped region in an image is a few lines of OpenCV, so the puzzle collapses into "find the blob, drag there".

Two identical gaps break that. The piece carries the pristine image content that used to sit at the target, so exactly one gap's surroundings continue it. A bot must do local patch matching rather than blob detection — and if it cannot, it is down to a coin flip, with one attempt per challenge.

The generator enforces the two properties that make this honest: gaps are only placed on content with enough local detail for the piece to be recognisable, and the two gaps must look sufficiently different from each other that only one can be right. Without those checks the puzzle is unsolvable for humans too.

docs/SECURITY.md states exactly what this stops and what it does not, with measured numbers.


Quick start

Try it locally

git clone https://github.com/Evil0ctal/Wobble-Captcha.git
cd Wobble-Captcha
python3 -m http.server 8080

Open http://127.0.0.1:8080.

Embed it

<link rel="stylesheet" href="/wobble-captcha/styles/captcha.css">
<div id="captcha"></div>

<script type="module">
  import { createCaptcha } from "/wobble-captcha/src/index.js";

  const captcha = createCaptcha("#captcha", {
    mock: false,
    apiBase: "https://api.example.com",
    getCsrfToken: () => document.querySelector('meta[name="csrf-token"]')?.content
  });

  captcha.addEventListener("success", ({ detail }) => {
    // Send this with the real form submission. The business endpoint redeems it.
    document.querySelector("#captcha-token").value = detail.verificationToken;
  });
</script>

createCaptcha returns a WobbleCaptcha, which is an EventTarget emitting success, failure and statechange. A captcha:success event also bubbles through the DOM if you would rather listen on a container.

Options

Option Default Purpose
mock false Run the in-browser mock instead of calling a server. Demo only.
apiBase "" Origin of the challenge service.
challengePath /api/captcha/challenge
verifyPath /api/captcha/verify
requestTimeoutMs 8000
requestCredentials "same-origin" Use "include" for a cross-origin API.
getCsrfToken null Function returning a token for the x-csrf-token header.
locale null null detects from navigator.languages. "zh-CN" or "en" to force.
traceMaxPoints 180 Bounded 24–500.
refreshCooldownMs 700 Minimum interval between challenge requests.
keyboardStep 0.0125 Progress per arrow key; Shift multiplies by 4.
debug false Log diagnostics to the console.
gapCount 2 Mock only. 2–4; see the security notes before raising it.

Running a real server

Both reference implementations are complete: challenge generation, grading, single-use challenges bound to a client, token issuance and rate limiting.

Python (FastAPI)

cd server/python
pip install -r requirements.txt
WOBBLE_BACKGROUNDS=../../assets uvicorn wobble_captcha.app:app --reload --port 8000

Node (Express)

cd server/node
npm install
WOBBLE_BACKGROUNDS=../../assets/raster npm start

@napi-rs/canvas cannot read SVG, so convert the bundled backgrounds to PNG first. The Node server imports its path generation and grading directly from src/ — the browser mock, the test suite and the server run literally the same security-bearing code.

Then set mock: false and apiBase: "http://127.0.0.1:8000" in config.js.


Project layout

src/
  core/        math, challenge validation, trace recording   — DOM-free, unit tested
  api/
    fetch-api.js         HTTP client
    mock/                in-browser challenge service (demo only)
      path.js            gap placement — the security-critical part
      scoring.js         grading and trajectory analysis
      generator.js       scene assembly
      painter.js         canvas drawing
  ui/          widget and markup
  i18n/        zh-CN, en
styles/        widget stylesheet (themed, dark-mode aware) + demo chrome
server/        FastAPI and Express reference implementations
tests/         node:test suite
docs/          protocol, security model, audit log

Accessibility

  • Full keyboard operation: arrows, Shift+arrows, Home, End, Enter/Space.
  • role="slider" with live aria-valuenow; status changes announced via aria-live.
  • Keyboard submissions are exempt from trajectory scoring and given a 240-second budget — grading a keyboard user on a pointer's motion curve locks them out.
  • Dark mode follows the system setting, with an explicit override.
  • prefers-reduced-motion is respected.

Testing

npm test                                   # 75 tests
cd server/python && python3 -m pytest -q   # 30 tests

Both suites include regression tests for the two vulnerabilities described in docs/SECURITY.md.


Browser support

Chrome/Edge 88+, Firefox 90+, Safari 15.4+. Requires ES modules, Pointer Events, aspect-ratio and AbortController. Safari below 17 ignores the canvas filter used for per-challenge colour variation; everything else still works.


License

Apache-2.0

About

A sliding puzzle CAPTCHA with two identical gaps and a curved, wobbling piece path — so solving it means deciding which gap fits. Zero dependencies, with FastAPI and Express reference servers.

Topics

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages