Skip to content

Repository files navigation

OpenAge - Privacy-first age verification for the web

Privacy-first age verification for the web

OpenAge runs face tracking, liveness checks, and age estimation on-device. Use it as a drop-in age gate with a checkbox-style widget, modal flow, or button binding.

Version Downloads License Demo

At a Glance

Browser-side Server-backed UI
On-device face analysis Optional WASM verification Widget popup, inline embed, bind
No raw camera upload Signed sessions and tokens Normal, compact, invisible
Serverless soft gates Hosted or custom backend Auto, light, dark

Install

npm install @tn3w/openage
<script src="https://cdn.jsdelivr.net/npm/@tn3w/openage/dist/openage.min.js"></script>

Quick Start

CDN

<div class="openage" data-sitekey="ag_live_xxxx" data-callback="onVerified"></div>

<script src="https://cdn.jsdelivr.net/npm/@tn3w/openage/dist/openage.min.js"></script>
<script>
    function onVerified(token) {
        console.log('verified', token);
    }
</script>

npm

import OpenAge from '@tn3w/openage';

OpenAge.render('#gate', {
    mode: 'serverless',
    layout: 'inline',
    minAge: 18,
    callback: (token) => console.log(token),
    errorCallback: (error) => console.error(error),
});

Inline Embed

OpenAge.render('#gate', {
    mode: 'serverless',
    layout: 'inline',
    minAge: 18,
});

layout: 'inline' removes the checkbox shell and renders the verification panel directly in the container. The first verification step starts immediately after loading.

Bound Flow

OpenAge.bind('#buy-btn', {
    sitekey: 'ag_live_xxxx',
    callback: (token) => submitForm(token),
});

Modes

Mode Backend Use case
serverless none client-only soft gates
sitekey OpenAge hosted production verification
custom your server self-hosted verification

serverless keeps everything local and returns a client-signed token. sitekey and custom use a server session and a WASM VM for stronger checks.

WASM Attestation (server modes)

In sitekey/custom mode the challenge algorithm runs as QuickJS bytecode inside a WebAssembly VM, and the VM seals its output so the server can detect tampering.

Sealed I/O. All VM inputs and outputs are authenticated:

  • Challenge bytecode and encrypted models carry an HMAC-SHA256 tag (HMAC(sign_key, header‖nonce‖ciphertext)); the VM refuses to run or decrypt anything whose tag does not verify, so the decrypt export is not an oracle.
  • The VM result is ChaCha20-encrypted (fresh CSPRNG nonce per response — no nonce reuse) and HMAC-signed. The server verifies the MAC before trusting any field.

Key rotation = the security budget. Keys (decrypt/encrypt/sign) and export names are baked into each WASM build by build.cjs and obfuscated (renames, split/masked key material, dead code, decoys). A static key in a binary is always extractable given enough time, so the server rebuilds a fresh WASM globally every OPENAGE_ROTATE_INTERVAL (default 15 min) and keeps the previous build valid only for OPENAGE_ROTATE_GRACE (default 5 min) so in-flight sessions finish. An extracted key is worthless once its build is pruned: forging requires extraction faster than interval + grace. Sessions are pinned to the build they started under.

Defence in depth on the server. The client owns the camera and JS, so the VM only signs attacker-influenceable numbers — the server therefore independently re-validates liveness from the motion history, enforces age sanity bounds, requires the VM integrity counter to advance every round, and rejects replayed challenge nonces. Treat the on-device result as one signal, not proof.

How Verification Works

  1. Camera — single permission request. Grant starts immediately; deny, missing, or busy device each show a clear message and a working retry.
  2. Positioning — waits for one well-lit, centered face.
  3. Liveness — randomized head-movement tasks (turn, nod, blink, move closer). Must pass before any age decision.
  4. Age — age is sampled across many frames during the liveness motion (varied poses → less single-frame bias), then a robust decision is made.

Age decision

decide(samples, minAge) (exported from the main module) turns per-frame age readings into one outcome:

  • Drops low-confidence frames, needs a minimum number of usable samples.
  • Uses the median of bias-corrected ages. The model over-reads young faces, so a downward correction is applied below 25 (largest for teens, fading to zero by 25) — this makes sub-18 detection stricter.
  • Rejects unstable reads (high spread between frames).
  • pass only when the corrected median clears minAge by a safety margin.
  • retry for borderline ages near minAge (e.g. 18–19) or unreliable reads — the user can try again.
  • fail when clearly under minAge.

Thresholds live in src/constants.js (PASS_MARGIN, BORDERLINE_BAND, MIN_FACE_SCORE, MAX_AGE_SPREAD, MIN_AGE_SAMPLES). Everything runs on-device; no images leave the browser.

Core API

OpenAge.render(container, params);
OpenAge.open(params);
OpenAge.bind(element, params);

OpenAge.reset(widgetId);
OpenAge.remove(widgetId);
OpenAge.getToken(widgetId);
OpenAge.execute(widgetId);

await OpenAge.challenge(params);

Runtime errors keep the popup open long enough to explain what happened (camera blocked, missing, or in use), then return to a retryable state. Borderline or unreliable age reads also resolve to retry, so a real adult can simply try again.

Main Params

Param Values
mode serverless, sitekey, custom
layout widget, inline
theme light, dark, auto
size normal, compact, invisible
minAge number, default 18
sitekey required for hosted mode
server required for custom mode

Demo

cd server
pip install -r requirements.txt
python server.py

The repository also includes demo/, a minimal GitHub Pages build that loads the jsDelivr bundle for @tn3w/openage in inline embedded serverless mode.

Source Layout

File Responsibility
index.js Public API, auto-render, event emitter
widget.js Checkbox/popup/inline shell, positioning, state
ui.js SVG icons, HTML templates, theme detection
styles.js Shadow-DOM CSS (single stylesheet string)
challenge.js Flow orchestration + camera/permission handling
detect.js Face tracking, positioning, liveness, age, decision
net.js Token signing, transport, WASM VM client
constants.js URLs and tunable thresholds

Development

npm install
npm test
npm run build
npm run dev

Optional server:

cd server
pip install -r requirements.txt
python server.py

Formatting

pip install black isort
isort . && black .
npx prtfm
clang-format -i server/wasm/src/*.c server/wasm/src/*.h

License

Apache 2.0

About

Privacy-first age check that keeps face processing on the device but still gives the server something it can verify.

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages