{"_id":"@007captcha/server","name":"@007captcha/server","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@007captcha/server","version":"0.1.0","description":"007captcha server-side token verification","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"license":"MIT","scripts":{"build":"tsup src/index.ts --format esm,cjs --dts --clean"},"_id":"@007captcha/server@0.1.0","_integrity":"sha512-KVnCHBYX7dOAF+uhfuOELwz2IzArBq/kaV7q3ADBuzpXTL263jfoz+kfkfZuU8AdlFnqFypPmYt1nXjc7YMRUw==","_resolved":"/tmp/a2049fd2b166c50e37b0947bb95fe0a8/007captcha-server-0.1.0.tgz","_from":"file:007captcha-server-0.1.0.tgz","_nodeVersion":"22.20.0","_npmVersion":"10.9.3","dist":{"integrity":"sha512-KVnCHBYX7dOAF+uhfuOELwz2IzArBq/kaV7q3ADBuzpXTL263jfoz+kfkfZuU8AdlFnqFypPmYt1nXjc7YMRUw==","shasum":"a248b5c29295e0ae1812f3ff8aa6baa8b00cbcdd","tarball":"https://registry.npmjs.org/@007captcha/server/-/server-0.1.0.tgz","fileCount":7,"unpackedSize":135420,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDSWesQWFthY+g13BbbrM/H88ST5bbyyt5e98GxOWjlDAIhAL12x5ljSe993MmWDqkc41T4VC7y9RoGaIZ6ozQ8aJu3"}]},"_npmUser":{"name":"mutkuoz","email":"thejupyter@gmail.com"},"directories":{},"maintainers":[{"name":"mutkuoz","email":"thejupyter@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/server_0.1.0_1774748051454_0.7116928013050914"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-29T01:34:11.345Z","0.1.0":"2026-03-29T01:34:11.654Z","modified":"2026-03-29T01:34:11.907Z"},"maintainers":[{"name":"mutkuoz","email":"thejupyter@gmail.com"}],"description":"007captcha server-side token verification","license":"MIT","readme":"<p align=\"center\">\n  <img src=\"../../007-logo.png\" alt=\"007captcha\" width=\"120\">\n</p>\n\n<h1 align=\"center\">@007captcha/server</h1>\n\n<p align=\"center\">\n  Server-side session management, analysis, and token verification for 007captcha.\n</p>\n\n<p align=\"center\">\n  <a href=\"https://www.npmjs.com/package/@007captcha/server\"><img src=\"https://img.shields.io/npm/v/@007captcha/server?color=111827\" alt=\"npm\"></a>\n  <a href=\"https://github.com/mutkuoz/007captcha/blob/main/LICENSE\"><img src=\"https://img.shields.io/github/license/mutkuoz/007captcha?color=111827\" alt=\"license\"></a>\n</p>\n\n---\n\nHandles everything security-sensitive: challenge generation, behavioral analysis, scoring, and HMAC-SHA256 token signing. The client widget acts as a thin rendering layer &mdash; all verification logic runs here.\n\n**Zero runtime dependencies.** Uses only Node.js built-in `crypto`.\n\n## Installation\n\n```bash\npnpm add @007captcha/server\n```\n\n## Token Verification\n\nVerify signed tokens from any challenge method:\n\n```ts\nimport { verify } from '@007captcha/server';\n\nconst result = await verify(token, SECRET);\n\nif (result.success) {\n  // Allow the request\n}\n```\n\n### `verify(token, secretKey): Promise<VerifyResult>`\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `success` | `boolean` | `true` if valid signature and not flagged as bot |\n| `score` | `number` | 0.0 (bot) to 1.0 (human) |\n| `method` | `string` | `'shape'`, `'maze'`, or `'ball'` |\n| `challenge` | `string` | Specific challenge identifier |\n| `verdict` | `string` | `'human'`, `'uncertain'`, or `'bot'` |\n| `timestamp` | `number` | When the challenge was completed |\n| `error` | `string?` | Reason if verification failed |\n\nTokens are single-use and expire after 5 minutes.\n\n## Challenge Managers\n\nEach challenge method has a session manager that handles the full lifecycle: session creation, challenge delivery, input analysis, and token signing. Create one instance per server process.\n\n### Ball &mdash; `BallChallengeManager`\n\nThe ball challenge generates a physics-based trajectory in real-time and streams rendered frames to the client via SSE. After the challenge, the user's cursor path is analyzed against the recorded trajectory.\n\n```ts\nimport { BallChallengeManager } from '@007captcha/server';\nconst ball = new BallChallengeManager(SECRET);\n```\n\n**Endpoints required:**\n\n| Method | Path | Handler |\n|--------|------|---------|\n| `POST` | `/captcha/ball/start` | `ball.createSession()` |\n| `GET` | `/captcha/ball/:id/stream` | `ball.startStreaming(id, onFrame, onEnd)` |\n| `POST` | `/captcha/ball/:id/verify` | `ball.verify(id, points, cursorStartT, origin)` |\n\n### Maze &mdash; `MazeChallengeManager`\n\nGenerates a procedural maze, renders it as a PNG, and solves it server-side. The client receives only the image and zone coordinates. Cursor path analysis runs entirely on the server.\n\n```ts\nimport { MazeChallengeManager } from '@007captcha/server';\nconst maze = new MazeChallengeManager(SECRET);\n```\n\n**Endpoints required:**\n\n| Method | Path | Handler |\n|--------|------|---------|\n| `POST` | `/captcha/maze/start` | `maze.createSession()` |\n| `POST` | `/captcha/maze/:id/verify` | `maze.verify(id, points, origin)` |\n\n### Shape &mdash; `ShapeChallengeManager`\n\nAssigns a random shape (circle, triangle, or square) and analyzes the user's drawing server-side. The client only knows which shape to draw &mdash; scoring and detection run here.\n\n```ts\nimport { ShapeChallengeManager } from '@007captcha/server';\nconst shape = new ShapeChallengeManager(SECRET);\n```\n\n**Endpoints required:**\n\n| Method | Path | Handler |\n|--------|------|---------|\n| `POST` | `/captcha/shape/start` | `shape.createSession()` |\n| `POST` | `/captcha/shape/:id/verify` | `shape.verify(id, points, origin)` |\n\n## Express Integration\n\nFull working example with all three methods:\n\n```js\nimport express from 'express';\nimport {\n  verify,\n  BallChallengeManager,\n  MazeChallengeManager,\n  ShapeChallengeManager,\n} from '@007captcha/server';\n\nconst app = express();\nconst SECRET = process.env.CAPTCHA_SECRET;\n\nconst ball  = new BallChallengeManager(SECRET);\nconst maze  = new MazeChallengeManager(SECRET);\nconst shape = new ShapeChallengeManager(SECRET);\n\napp.use(express.json());\n\n// Ball\napp.post('/captcha/ball/start', (req, res) => {\n  res.json(ball.createSession());\n});\n\napp.get('/captcha/ball/:id/stream', (req, res) => {\n  res.writeHead(200, {\n    'Content-Type': 'text/event-stream',\n    'Cache-Control': 'no-cache',\n    Connection: 'keep-alive',\n  });\n  let done = false;\n  const ok = ball.startStreaming(\n    req.params.id,\n    (frame) => res.write(`event: frame\\ndata: ${JSON.stringify(frame)}\\n\\n`),\n    ()      => { done = true; res.write('event: end\\ndata: {}\\n\\n'); res.end(); },\n  );\n  if (!ok) { res.end(); return; }\n  req.on('close', () => { if (!done) ball.cancelSession(req.params.id); });\n});\n\napp.post('/captcha/ball/:id/verify', (req, res) => {\n  const { points, cursorStartT, origin } = req.body;\n  res.json(ball.verify(req.params.id, points || [], cursorStartT || 0, origin || ''));\n});\n\n// Maze\napp.post('/captcha/maze/start', (req, res) => res.json(maze.createSession()));\napp.post('/captcha/maze/:id/verify', (req, res) => {\n  res.json(maze.verify(req.params.id, req.body.points || [], req.body.origin || ''));\n});\n\n// Shape\napp.post('/captcha/shape/start', (req, res) => res.json(shape.createSession()));\napp.post('/captcha/shape/:id/verify', (req, res) => {\n  res.json(shape.verify(req.params.id, req.body.points || [], req.body.origin || ''));\n});\n\n// Token verification\napp.post('/verify', async (req, res) => {\n  res.json(await verify(req.body.token || '', SECRET));\n});\n\napp.listen(3007);\n```\n\nSee [`examples/express-server/`](../../examples/express-server/) for the full demo with a UI.\n\n## Session Lifecycle\n\n- Sessions are stored in memory and auto-expire (60s for ball/shape, 120s for maze).\n- Each session can only be verified once.\n- Call `manager.destroy()` on server shutdown to clean up timers.\n- For horizontally scaled deployments, sessions are per-process &mdash; route challenge requests to the same instance (sticky sessions or a shared store).\n\n## License\n\n[MIT](../../LICENSE)\n","readmeFilename":"README.md","_rev":"1-f93081998657fafd1a63cd0fe8d37f8a"}