diff --git a/demo/index.html b/demo/demo/index.html
similarity index 100%
rename from demo/index.html
rename to demo/demo/index.html
diff --git a/demo/package.json b/demo/demo/package.json
similarity index 100%
rename from demo/package.json
rename to demo/demo/package.json
diff --git a/demo/server.js b/demo/demo/server.js
similarity index 100%
rename from demo/server.js
rename to demo/demo/server.js
diff --git a/demo/solver/.gitignore b/demo/solver/.gitignore
new file mode 100644
index 0000000..a14702c
--- /dev/null
+++ b/demo/solver/.gitignore
@@ -0,0 +1,34 @@
+# dependencies (bun install)
+node_modules
+
+# output
+out
+dist
+*.tgz
+
+# code coverage
+coverage
+*.lcov
+
+# logs
+logs
+_.log
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# caches
+.eslintcache
+.cache
+*.tsbuildinfo
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
diff --git a/demo/solver/README.md b/demo/solver/README.md
new file mode 100644
index 0000000..3d2fb84
--- /dev/null
+++ b/demo/solver/README.md
@@ -0,0 +1,15 @@
+# solver
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run:
+
+```bash
+bun run index.js
+```
+
+This project was created using `bun init` in bun v1.2.9. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
diff --git a/demo/solver/bun.lock b/demo/solver/bun.lock
new file mode 100644
index 0000000..1499d88
--- /dev/null
+++ b/demo/solver/bun.lock
@@ -0,0 +1,32 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "solver",
+ "dependencies": {
+ "@cap.js/solver": "latest",
+ },
+ "devDependencies": {
+ "@types/bun": "latest",
+ },
+ "peerDependencies": {
+ "typescript": "^5",
+ },
+ },
+ },
+ "packages": {
+ "@cap.js/solver": ["@cap.js/solver@0.0.2", "", {}, "sha512-DMEth1+iQgRDHibb4alEeYhV32lRpGVkiIVXk9tS7s8So6CV8Gd1U+/KWqB+k7K6yHUqWzRkVQP7INOLUTyqlA=="],
+
+ "@types/bun": ["@types/bun@1.2.9", "", { "dependencies": { "bun-types": "1.2.9" } }, "sha512-epShhLGQYc4Bv/aceHbmBhOz1XgUnuTZgcxjxk+WXwNyDXavv5QHD1QEFV0FwbTSQtNq6g4ZcV6y0vZakTjswg=="],
+
+ "@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="],
+
+ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
+
+ "bun-types": ["bun-types@1.2.9", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw=="],
+
+ "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
+
+ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+ }
+}
diff --git a/demo/solver/index.js b/demo/solver/index.js
new file mode 100644
index 0000000..240122b
--- /dev/null
+++ b/demo/solver/index.js
@@ -0,0 +1,24 @@
+import solver from "@cap.js/solver";
+
+const CHALLENGES = [
+ ["e455cea65e98bc3c36287f43769da211", "dceb"],
+ ["fb8d25f6abac5aa9b6360051f37e010b", "93f1"],
+ ["91ef47db578fbeb2565d3f9c82bb7960", "3698"],
+ ["b7ad7667486a691cda8ef297098f64a7", "d72a"],
+ ["1aca3fb7cef7a2be0dee563ed4136758", "3b58"],
+ ["d9ec39af92b430e5a329274d8aa58fa8", "e1d3"],
+ ["781a3cc9217d73c908a321d3fdabd62f", "22c6"],
+ ["e37a0752c9ac2f3d2517747fde373ac9", "f6f1"],
+ ["bba070197569f322beda5b240f639a95", "4751"],
+ ["89297515aeac646bee9653ba405e0beb", "a7de"],
+ ["444571a0d5039c15be6141d6cd8434f9", "a783"],
+ ["ba75f2bf8e9b92cc32caa17237a52d14", "7e30"],
+ ["22bfc18ba8e3ecee080c5d1ef64ed6e9", "5fcf"],
+ ["885fb78ff76b4eddd2f5bc04ac5ee673", "93e5"],
+ ["308758072931bb3b254a7b1ed351d04a", "3e49"],
+ ["724f89bb167db4b881e1dc7b0949ac8f", "b82e"],
+ ["8b79506e4630de15be225c18623eff65", "f0e5"],
+ ["0c21ade6e63a4e37b13cb8b087f31863", "65c9"],
+];
+
+console.log(await solver(CHALLENGES));
diff --git a/demo/solver/jsconfig.json b/demo/solver/jsconfig.json
new file mode 100644
index 0000000..9c62f74
--- /dev/null
+++ b/demo/solver/jsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ // Environment setup & latest features
+ "lib": ["ESNext"],
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "allowJs": true,
+
+ // Bundler mode
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+
+ // Best practices
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+
+ // Some stricter flags (disabled by default)
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false
+ }
+}
diff --git a/demo/solver/package.json b/demo/solver/package.json
new file mode 100644
index 0000000..613b479
--- /dev/null
+++ b/demo/solver/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "solver",
+ "module": "index.js",
+ "type": "module",
+ "private": true,
+ "devDependencies": {
+ "@types/bun": "latest"
+ },
+ "peerDependencies": {
+ "typescript": "^5"
+ },
+ "dependencies": {
+ "@cap.js/solver": "latest"
+ }
+}
diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs
index f837d46..6b04bb7 100644
--- a/docs/.vitepress/config.mjs
+++ b/docs/.vitepress/config.mjs
@@ -33,7 +33,8 @@ export default defineConfig({
text: 'Libraries',
items: [
{ text: '@cap.js/server', link: '/guide/server.md' },
- { text: '@cap.js/widget', link: '/guide/widget.md' }
+ { text: '@cap.js/widget', link: '/guide/widget.md' },
+ { text: '@cap.js/solver', link: '/guide/solver.md' }
]
},
{ text: "Demo", link: "https://cap-starter.glitch.me/" },
diff --git a/docs/guide/solver.md b/docs/guide/solver.md
new file mode 100644
index 0000000..cbcc5e6
--- /dev/null
+++ b/docs/guide/solver.md
@@ -0,0 +1,39 @@
+# @cap.js/solver
+
+`@cap.js/solver` is a standalone library that can be used to solve Cap challenges from the server. It's extremely simple (no dependencies, one single file) yet very fast and efficient. Note that it can only be used with Bun.
+
+## Installation
+
+```bash
+bun add @cap.js/solver
+```
+
+## Usage
+
+```js
+import solver from "@cap.js/solver";
+
+const CHALLENGES = [
+ ["a5b6fda4aaed97cf61d7dd9259f733b5", "d455"],
+ ["286bcc39249f9ee698314b600c32e40f", "f0ff"],
+ ["501350aa7c46573cb604284554045703", "4971"],
+ ["a55c02f3b9b4cd088a5a7ee3d4941c14", "eab7"],
+ ["5f3362c12e2779f56f4ef75b4494f5e6", "999f"],
+ /* ... */
+];
+
+console.log(await solver(CHALLENGES));
+```
+
+**Output:**
+
+```json
+[
+ ["a5b6fda4aaed97cf61d7dd9259f733b5", "d455", 67302],
+ ["286bcc39249f9ee698314b600c32e40f", "f0ff", 64511],
+ ["501350aa7c46573cb604284554045703", "4971", 40440],
+ ["a55c02f3b9b4cd088a5a7ee3d4941c14", "eab7", 27959],
+ ["5f3362c12e2779f56f4ef75b4494f5e6", "999f", 71259],
+ ...
+]
+```
diff --git a/solver/README.md b/solver/README.md
new file mode 100644
index 0000000..c4754ee
--- /dev/null
+++ b/solver/README.md
@@ -0,0 +1,2 @@
+# Server solver for the Cap CAPTCHA
+See [https://cap.tiagorangel.com](https://cap.tiagorangel.com)
\ No newline at end of file
diff --git a/solver/example.js b/solver/example.js
new file mode 100644
index 0000000..c6f707b
--- /dev/null
+++ b/solver/example.js
@@ -0,0 +1,24 @@
+import solver from "./index.js";
+
+const CHALLENGES = [
+ ["e455cea65e98bc3c36287f43769da211", "dceb"],
+ ["fb8d25f6abac5aa9b6360051f37e010b", "93f1"],
+ ["91ef47db578fbeb2565d3f9c82bb7960", "3698"],
+ ["b7ad7667486a691cda8ef297098f64a7", "d72a"],
+ ["1aca3fb7cef7a2be0dee563ed4136758", "3b58"],
+ ["d9ec39af92b430e5a329274d8aa58fa8", "e1d3"],
+ ["781a3cc9217d73c908a321d3fdabd62f", "22c6"],
+ ["e37a0752c9ac2f3d2517747fde373ac9", "f6f1"],
+ ["bba070197569f322beda5b240f639a95", "4751"],
+ ["89297515aeac646bee9653ba405e0beb", "a7de"],
+ ["444571a0d5039c15be6141d6cd8434f9", "a783"],
+ ["ba75f2bf8e9b92cc32caa17237a52d14", "7e30"],
+ ["22bfc18ba8e3ecee080c5d1ef64ed6e9", "5fcf"],
+ ["885fb78ff76b4eddd2f5bc04ac5ee673", "93e5"],
+ ["308758072931bb3b254a7b1ed351d04a", "3e49"],
+ ["724f89bb167db4b881e1dc7b0949ac8f", "b82e"],
+ ["8b79506e4630de15be225c18623eff65", "f0e5"],
+ ["0c21ade6e63a4e37b13cb8b087f31863", "65c9"],
+];
+
+console.log(await solver(CHALLENGES));
diff --git a/solver/index.js b/solver/index.js
new file mode 100644
index 0000000..7d8f160
--- /dev/null
+++ b/solver/index.js
@@ -0,0 +1,92 @@
+import { Worker } from "worker_threads";
+import os from "os";
+
+const workerBlob = new Blob(
+ [
+ `
+import crypto from "crypto";
+import { parentPort, workerData } from "worker_threads";
+
+const { salt, target } = workerData;
+
+const saltBuffer = Buffer.from(salt);
+const targetLen = target.length;
+const targetBytes = Math.ceil(targetLen / 2);
+
+let nonce = 0;
+
+while (true) {
+ const nonceStr = nonce.toString();
+
+ const hash = crypto.createHash("sha256");
+
+ hash.update(saltBuffer);
+ hash.update(nonceStr);
+
+ const hashBuffer = hash.digest();
+
+ const hashPrefixHex = hashBuffer.toString("hex", 0, targetBytes);
+
+ if (hashPrefixHex.startsWith(target)) {
+ parentPort.postMessage({
+ nonce: Number(nonce),
+ salt,
+ target,
+ });
+ break;
+ }
+
+ nonce++;
+}`,
+ ],
+ {
+ type: "application/typescript",
+ }
+);
+const workerUrl = URL.createObjectURL(workerBlob);
+
+export default function solver(challenges, config) {
+ const totalChallenges = challenges.length;
+ const numWorkers =
+ config?.workerCount || Math.min(totalChallenges, 14, os.cpus().length);
+
+ let challengesProcessed = 0;
+ let nextChallengeIndex = 0;
+ let activeWorkers = 0;
+ const results = [];
+
+ return new Promise((resolve) => {
+ function startWorker() {
+ if (nextChallengeIndex < totalChallenges && activeWorkers < numWorkers) {
+ const currentChallengeIndex = nextChallengeIndex;
+ const [salt, target] = challenges[currentChallengeIndex];
+ nextChallengeIndex++;
+ activeWorkers++;
+
+ const worker = new Worker(workerUrl, {
+ workerData: { salt, target, challengeIndex: currentChallengeIndex },
+ });
+
+ worker.on("message", (result) => {
+ results.push([result.salt, result.target, result.nonce]);
+ challengesProcessed++;
+ activeWorkers--;
+
+ if (challengesProcessed === totalChallenges) {
+ resolve(results);
+ } else {
+ startWorker();
+ }
+ });
+ }
+ }
+
+ for (let i = 0; i < numWorkers; i++) {
+ startWorker();
+ }
+
+ if (totalChallenges === 0) {
+ resolve([]);
+ }
+ });
+}
diff --git a/solver/package.json b/solver/package.json
new file mode 100644
index 0000000..f6d2cad
--- /dev/null
+++ b/solver/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@cap.js/solver",
+ "version": "0.0.2",
+ "description": "Server-side solver for Cap's SHA-256 PoW captchas",
+ "main": "index.js",
+ "scripts": {
+ "test": "bun ./example.js",
+ "npm:publish": "npm publish --access public"
+ },
+ "author": "Tiago",
+ "license": "AGPL-3.0-or-later",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/tiagorangel1/cap.git"
+ },
+ "keywords": [
+ "captcha",
+ "recaptcha"
+ ],
+ "bugs": {
+ "url": "https://github.com/tiagorangel1/cap/issues"
+ },
+ "homepage": "https://github.com/tiagorangel1/cap#readme"
+}