Server SDK
The @cernosh/server package is the authoritative side of the system. It issues challenges,
consumes them exactly once, verifies proof-of-work and signatures, validates maze solves from raw
events, verifies server-timed probes and WebAuthn assertions, and mints short-lived tokens.
Installation
npm install @cernosh/serverFor a portable production adapter:
npm install @cernosh/server @cernosh/server-redisConfiguration
import { MemoryStore } from '@cernosh/server'import type { ServerConfig } from '@cernosh/server'
const config: ServerConfig = { secret: process.env.CERNO_SECRET!, store: new MemoryStore(), enableSecretFeatures: true, enableProbes: false, webAuthn: { mode: 'off', rpId: 'example.com', expectedOrigin: 'https://example.com', }, mode: 'development', scoreThreshold: 0.6,}API
createChallenge(config, request)
Generates a challenge and persists it with a TTL. The returned object can include:
challenge_type- maze geometry and seed
- proof-of-work challenge and difficulty
- server-controlled
cell_size - optional
probes requirementswebauthn_request_options
const challenge = await createChallenge(config, { site_key: 'your-site-key', stable_id: 'user-123', client_capabilities: { reduced_motion: false, webauthn_available: true, },})You can also pass server-derived request context for adaptive proof-of-work and rate-limit binding:
const challenge = await createChallenge(config, { site_key: 'your-site-key', stable_id: 'user-123', client_capabilities: { reduced_motion: false, webauthn_available: true, }, client_signals: { ip: req.ip, userAgent: req.headers['user-agent'], failedAttempts: 2, trustScore: 0.82, }, rate_limit_binding: 'server-derived-binding',})validateSubmission(config, request)
Runs the full server pipeline:
- Input bounds check
- Rate limit
- Challenge consume
- Site key check
- Public-key binding check
- Expiry check
- PoW verification
- ECDSA signature verification
- Optional WebAuthn verification
- Event renormalization and maze validation
- Server-timed probe completion token validation
- Behavioral scoring
const result = await validateSubmission(config, { challenge_id: '...', site_key: 'your-site-key', session_id: 'user-session-id', maze_seed: 12345, events: [{ x: 0.1, y: 0.2, t: 1234567890, type: 'down' }], pow_proof: { nonce: 12345, hash: '00ab...' }, public_key: 'base64-jwk', signature: 'base64-signature', timestamp: Date.now(), stable_id: 'user-123', probe_completion_tokens: ['eyJhbGciOiJIUzI1NiJ9...'], webauthn: assertion,})On success the function returns:
{ success: true, token: 'eyJ...', score: 0 }The behavioral score is intentionally not exposed in the public success path. Use onValidation
for observability if you need internals.
verifyToken(token, options)
Verifies a JWT token in your own routes. Checks signature, expiry, session binding, and single-use enforcement.
const result = await verifyToken(token, { secret: process.env.CERNO_SECRET!, sessionId: req.session.id, store: config.store,})siteverify(request, options)
Provides a server-to-server verification shape similar to reCAPTCHA:
import { siteverify } from '@cernosh/server'
const result = await siteverify({ token, session_id: req.session.id,}, { secret: process.env.CERNO_SECRET!, store: config.store,})Probe helpers
Use these when you expose the full worker contract yourself:
import { armProbe, completeProbe, verifyProbeCompletionTokens,} from '@cernosh/server'Probe timing is server-authored:
armProbe()validates the trace prefix and returns a signedprobe_ticketcompleteProbe()consumes the ticket and returns a signedcompletion_tokenvalidateSubmission()acceptsprobe_completion_tokens, not raw reaction times
WebAuthn helpers
import { beginWebAuthnRegistration, completeWebAuthnRegistration, verifyWebAuthnAuthentication,} from '@cernosh/server'Registration is explicit:
beginWebAuthnRegistration()issues browser-safe creation optionscompleteWebAuthnRegistration()verifies attestation and stores the credentialcreateChallenge()includeswebauthn_request_optionswhen a registeredstable_idis presentvalidateSubmission()verifies the assertion when the challenge requires or prefers it
CaptchaStore
Implement the CaptchaStore interface for any storage backend:
interface CaptchaStore { capabilities: { atomicChallengeConsume: boolean atomicTokenConsume: boolean strongConsistency: boolean productionReady: boolean } setChallenge(id: string, data: Challenge, ttlMs: number): Promise<void> getChallenge(id: string): Promise<Challenge | null> deleteChallenge(id: string): Promise<void> consumeChallenge(id: string): Promise<Challenge | null> consumeToken(tokenId: string, ttlMs: number): Promise<boolean> incrementRate(key: string, windowMs: number): Promise<number> setProbeArmSession?(id: string, data: ProbeArmSessionData, ttlMs: number): Promise<void> consumeProbeArmSession?(id: string): Promise<ProbeArmSessionData | null> setWebAuthnRegistrationSession?(id: string, data: WebAuthnRegistrationSessionData, ttlMs: number): Promise<void> consumeWebAuthnRegistrationSession?(id: string): Promise<WebAuthnRegistrationSessionData | null> listWebAuthnCredentials?(stableId: string, siteKey: string): Promise<WebAuthnCredentialRecord[]> saveWebAuthnCredential?(credential: WebAuthnCredentialRecord): Promise<void> updateWebAuthnCredentialCounter?( stableId: string, siteKey: string, credentialId: string, nextCounter: number, ): Promise<void> setReputation?(key: string, data: ReputationData, ttlMs: number): Promise<void> getReputation?(key: string): Promise<ReputationData | null>}