@cv-challenge/server
v1.4.0
Published
Server-side engine and Express adapter for the CV Challenge interactive verification flow.
Maintainers
Readme
@cv-challenge/server
Server-side renderer, token manager, and Express adapter for CV Challenge.
Install
pnpm add @cv-challenge/serverRequirements
- ffmpeg available in PATH
- OpenCV for opencv4nodejs
Engine usage
import Motion3DChallenge from '@cv-challenge/server';
const engine = new Motion3DChallenge(180, 60, 3, 20);
const { videoBuffer, hitbox, debug } = await engine.generate({ failureCount: 3 });
const ok = engine.validate({ x: 42, y: 12 }, hitbox);Constructor defaults:
- width: 180
- height: 60
- durationSec: 3
- objectCount: 20
generate optionally accepts { failureCount } to shrink cube scale and increase cube count as failures rise.
Express adapter
import express from 'express';
import Motion3DChallenge, {
createChallengeExpressRouter,
createChallengeTokenManager
} from '@cv-challenge/server';
const app = express();
app.use(express.json({ limit: '1mb' }));
const engine = new Motion3DChallenge();
const tokenManager = createChallengeTokenManager<{ sessionId: string }>({
secret: process.env.CHALLENGE_JWT_SECRET ?? 'dev-only-change-me',
tokenTtlSec: 20,
successTokenTtlSec: 60
});
const router = createChallengeExpressRouter<{ sessionId: string }>({
challenge: engine,
tokenManager,
onChallenge: ({ req }) => String(req.headers['x-session-id'] ?? req.ip ?? ''),
onVerified: async ({ req }) => {
const sessionId = String(req.headers['x-session-id'] ?? '');
if (!sessionId) return null;
return { expiresInSec: 60, payload: { sessionId } };
},
validateSuccessToken: (payload, { req }) => {
const sessionId = String(req.headers['x-session-id'] ?? '');
return payload.payload?.sessionId === sessionId;
},
debug: 'info'
});
app.use(router);Routes
GET /challenge
- Response:
video/webm - Headers:
X-Challenge-TokenX-Challenge-Expires-AtX-Challenge-Expires-In
- Optional request header:
X-Challenge-Success-Token
- 429 response when
onChallengeidentifies an active challenge:- Body:
{ error: "challenge-already-issued", challengeExpiresAt, challengeExpiresIn } - Header:
Retry-After
- Body:
- 429 response when backoff is active for the requester:
- Body:
{ error: "challenge-backoff", backoffExpiresAt, backoffExpiresIn } - Header:
Retry-After
- Body:
POST /challenge/verify
- Body:
{ token: string, x: number, y: number } - Response:
{ success, reload, successToken, successTokenExpiresAt, successTokenExpiresIn }
Token behavior
- Challenge tokens are encrypted with the provided secret.
- Success tokens are encoded only (alg "none"), intended as a short-lived hint to skip cold start.
- Use
validateSuccessTokento bind success tokens to your own session or user data. - Success tokens are invalidated after 3 consecutive failed verifications tied to them.
- Failed verification blacklists the challenge token JTI until expiry.
API options
createChallengeTokenManager(options)
secret(required): encryption key for challenge tokens.tokenTtlSec(default 20): challenge token lifetime.successTokenTtlSec(default 60): success token lifetime.- Pass a generic type parameter to type the success token payload.
createChallengeExpressRouter(options)
challenge(required): the engine instance.tokenManager(required): token manager fromcreateChallengeTokenManager.onChallenge: optional callback that returns a unique key for the requester (session id, IP, etc). When provided, only one active challenge per key is allowed; additionalGET /challengerequests return429until the prior challenge is verified or expires.backoff: optional per-key verification backoff (requiresonChallenge). Defaults to a 10-minute window, reset on success, and a schedule of[0, 0, 2000, 5000, 10000, 20000, 35000, 55000, 75000]ms (cap 75s). Expired challenges that are never verified count as a failure within the window. Setenabled: falseto disable whenonChallengeis present.onVerified: optional callback; returnundefinedfor default success token, object to override TTL/payload, ornullto skip.validateSuccessToken: optional validator for decoded success token payloads.debug:"none"|"error"|"info"(default"none").- Pass a matching generic type parameter to type
SuccessTokenPayload.
