Skip to content

Worker API

The stock React widget calls POST /challenge, optional probe routes, and POST /verify. The worker also exposes WebAuthn registration routes and POST /siteverify.

POST /challenge

Creates a new verification challenge.

Request

{
"site_key": "your-site-key",
"stable_id": "user-123",
"client_capabilities": {
"reduced_motion": false,
"webauthn_available": true,
"pointer_types": ["mouse"]
}
}

Response

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"challenge_type": "maze_stroop",
"maze_seed": 827461,
"maze_width": 8,
"maze_height": 8,
"maze_difficulty": 0.6,
"pow_challenge": "a1b2c3d4e5f6...",
"pow_difficulty": 18,
"site_key": "your-site-key",
"created_at": 1711727880000,
"expires_at": 1711728000,
"requirements": {
"probe": {
"mode": "required",
"required_completion_count": 1
},
"webauthn": {
"mode": "preferred"
}
},
"probes": [
{
"id": "probe-1",
"type": "color_tap",
"instruction": "Tap the blue cell",
"target_color": "blue",
"distractor_colors": ["red", "green"],
"cells": [
{ "x": 4, "y": 2, "color": "blue", "isTarget": true }
],
"trigger_cell": { "x": 3, "y": 2 }
}
],
"webauthn_request_options": {
"challenge": "base64url",
"rpId": "example.com",
"timeout": 60000,
"userVerification": "required",
"allowCredentials": [
{ "id": "credential-id", "type": "public-key", "transports": ["internal"] }
]
}
}
FieldTypeDescription
idstringUnique challenge identifier
challenge_type’maze’ | ‘maze_stroop’Whether probes were injected
maze_seednumberSeed for deterministic maze generation
maze_widthnumberMaze grid width
maze_heightnumberMaze grid height
maze_difficultynumberDifficulty parameter
pow_challengestringHex string for proof-of-work input
pow_difficultynumberRequired leading zero bits
site_keystringSite key the challenge was issued for
created_atnumberCreation time in milliseconds
expires_atnumberExpiry time in milliseconds
cell_sizenumberServer-authoritative maze cell size
requirementsChallengeRequirementsServer-declared probe and WebAuthn requirements
probesStroopProbe[]Present when the challenge type is maze_stroop
webauthn_request_optionsWebAuthnRequestOptionsJSONPresent when the supplied stable_id has registered credentials and the server wants a WebAuthn assertion

POST /probe/arm

Arms a probe after the client reaches the trigger cell.

Request

{
"challenge_id": "550e8400-e29b-41d4-a716-446655440000",
"site_key": "your-site-key",
"session_id": "user-session-id",
"probe_id": "probe-1",
"events": [
{ "x": 0.126, "y": 0.124, "t": 12, "type": "down" },
{ "x": 0.125, "y": 0.189, "t": 28, "type": "move" }
]
}

Response

{
"success": true,
"probe_ticket": "eyJhbGciOiJIUzI1NiJ9...",
"armed_at": 1711728001234,
"deadline_at": 1711728006234
}

POST /probe/complete

Completes an armed probe and returns a signed completion token.

Request

{
"challenge_id": "550e8400-e29b-41d4-a716-446655440000",
"session_id": "user-session-id",
"probe_ticket": "eyJhbGciOiJIUzI1NiJ9...",
"tapped_cell": { "x": 4, "y": 2 }
}

Response

{
"success": true,
"completion_token": "eyJhbGciOiJIUzI1NiJ9..."
}

POST /verify

Validates a completed challenge submission.

Request

{
"challenge_id": "550e8400-e29b-41d4-a716-446655440000",
"site_key": "your-site-key",
"session_id": "user-session-id",
"maze_seed": 827461,
"events": [
{ "x": 0.126, "y": 0.124, "t": 12, "type": "down" },
{ "x": 0.125, "y": 0.189, "t": 28, "type": "move" }
],
"pow_proof": { "nonce": 48291, "hash": "00af2c..." },
"public_key": "eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Iiwi...",
"signature": "MEQCIA...",
"timestamp": 1711728001234,
"stable_id": "user-123",
"probe_completion_tokens": ["eyJhbGciOiJIUzI1NiJ9..."],
"webauthn": {
"id": "credential-id",
"rawId": "credential-id",
"type": "public-key",
"response": {
"clientDataJSON": "base64url",
"authenticatorData": "base64url",
"signature": "base64url",
"userHandle": "base64url"
}
}
}

Required fields

FieldTypeNotes
challenge_idstringMust reference an unconsumed challenge
site_keystringMust match the issued challenge
session_idstringBound into the token
maze_seednumberPresent in the request shape; server trusts the challenge seed instead
eventsRawEvent[]Raw interaction stream
pow_proof{ nonce: number; hash: string }Submitted proof-of-work
public_keystringBase64-encoded JWK
signaturestringECDSA P-256 signature over challenge_id:site_key:expires_at
timestampnumberClient timestamp

Optional fields

FieldTypeNotes
stable_idstringRequired for WebAuthn and reputation-backed flows
cell_sizenumberClient-reported cell size; server uses issued challenge.cell_size
input_type’mouse’ | ‘touch’ | ‘keyboard’Observability hint only
rate_limit_bindingstringServer-derived binding; browser clients should not set this directly
probe_completion_tokensstring[]Required when the challenge includes probes
webauthnAuthenticationResponseJSONRequired when requirements.webauthn.mode === 'required'

Response (success)

{
"success": true,
"token": "eyJhbGciOiJIUzI1NiJ9..."
}

Response (failure)

{
"success": false,
"score": 0,
"error_code": "behavioral_rejected"
}

The token is:

  • bound to the session_id
  • single-use
  • valid for 60 seconds

POST /webauthn/register/options

Starts credential registration for a stable_id.

Request

{
"site_key": "your-site-key",
"stable_id": "user-123"
}

Response

{
"session_id": "registration-session-id",
"options": {
"challenge": "base64url",
"rp": { "id": "example.com", "name": "Cerno" },
"user": {
"id": "base64url",
"name": "user-123",
"displayName": "user-123"
}
}
}

POST /webauthn/register/verify

Consumes the registration session and stores the credential.

Request

{
"session_id": "registration-session-id",
"response": {
"id": "credential-id",
"rawId": "credential-id",
"type": "public-key",
"response": {
"clientDataJSON": "base64url",
"attestationObject": "base64url"
}
}
}

Response

{
"success": true,
"credential_id": "credential-id"
}

Token verification

Use verifyToken from @cernosh/server to validate tokens in your own routes:

import { verifyToken } from '@cernosh/server'
const result = await verifyToken(token, {
secret: process.env.CERNO_SECRET!,
sessionId: req.session.id,
store: config.store,
})
if (!result.valid) {
return res.status(403).json({ error: 'Verification required' })
}