From bbeab139e6878dc38c091fbef3ee78a7a4288bc7 Mon Sep 17 00:00:00 2001 From: Tiago <70700766+tiagorangel1@users.noreply.github.com> Date: Wed, 2 Jul 2025 13:12:54 +0100 Subject: [PATCH] solver: add seeded challenges support --- docs/.vitepress/config.mjs | 3 +- docs/guide/solver.md | 43 +++++++++++----- solver/{example.js => example/list.js} | 2 +- solver/example/seeded.js | 19 +++++++ solver/index.js | 71 +++++++++++++++++++------- solver/package.json | 4 +- 6 files changed, 107 insertions(+), 35 deletions(-) rename solver/{example.js => example/list.js} (97%) create mode 100644 solver/example/seeded.js diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index bf0b9f7..50613db 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -158,8 +158,7 @@ export default withMermaid({ items: [ { text: "Server", link: "/guide/server.md" }, { text: "Widget", link: "/guide/widget.md" }, - { text: "Solver", link: "/guide/solver.md" }, - { text: "CLI", link: "/guide/cli.md" }, + { text: "Solver", link: "/guide/solver.md" } ], }, { diff --git a/docs/guide/solver.md b/docs/guide/solver.md index 002203f..bfb29a6 100644 --- a/docs/guide/solver.md +++ b/docs/guide/solver.md @@ -1,6 +1,6 @@ # @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. +`@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 as fast and efficient as the widget. Note that it can **only be used with Bun**. Node.js support is not planned. ## Installation @@ -10,10 +10,26 @@ bun add @cap.js/solver ## Usage +#### From seeded challenges + ```js import solver from "@cap.js/solver"; -const CHALLENGES = [ +console.log( + await solver("challenge token", { + c: 50, // challenge count + s: 32, // salt size + d: 4, // difficulty + }) +); +``` + +#### From challenge list + +```js +import solver from "@cap.js/solver"; + +const challenges = [ ["a5b6fda4aaed97cf61d7dd9259f733b5", "d455"], ["286bcc39249f9ee698314b600c32e40f", "f0ff"], ["501350aa7c46573cb604284554045703", "4971"], @@ -22,26 +38,29 @@ const CHALLENGES = [ /* ... */ ]; -console.log(await solver(CHALLENGES)); +console.log(await solver(challenges)); ``` **Output:** ```json -[ - ["a5b6fda4aaed97cf61d7dd9259f733b5", "d455", 67302], - ["286bcc39249f9ee698314b600c32e40f", "f0ff", 64511], - ["501350aa7c46573cb604284554045703", "4971", 40440], - ["a55c02f3b9b4cd088a5a7ee3d4941c14", "eab7", 27959], - ["5f3362c12e2779f56f4ef75b4494f5e6", "999f", 71259], - ... -] +[67302, 64511, 40440, 27959, 71259 /* ... */] ``` +The 2nd argument is optional but can always be provided. It's always an object. + +- For **all challenge types**, `workerCount` indicates the number of workers to use (default is the number of CPU cores). + +- For **all challenge types**, `onProgress` can also be used to provide a callback for progress updates. + +- For **seeded challenges only**, it is used to specify the number of solutions to generate, the size of the challenges, and the difficulty + ## FAQ ### Why is this needed? + Mainly for M2M interactions, where you want to solve Cap challenges on the server without user interaction. ### Doesn't this defeat the purpose of Cap? -Not really. Server-side solving is a core use case of proof-of-work CAPTCHAs like Cap or altcha. It's about proving effort, not necessarily involving a human. [Learn more](./effectiveness.md) \ No newline at end of file + +Not really. Server-side solving is a core use case of proof-of-work CAPTCHAs like Cap or altcha. It's about proving effort, not necessarily involving a human. [Learn more](./effectiveness.md) diff --git a/solver/example.js b/solver/example/list.js similarity index 97% rename from solver/example.js rename to solver/example/list.js index 9d8fd46..b79dead 100644 --- a/solver/example.js +++ b/solver/example/list.js @@ -1,4 +1,4 @@ -import solver from "./index.js"; +import solver from "../index.js"; const CHALLENGES = [ ["e455cea65e98bc3c36287f43769da211", "dceb"], diff --git a/solver/example/seeded.js b/solver/example/seeded.js new file mode 100644 index 0000000..490541c --- /dev/null +++ b/solver/example/seeded.js @@ -0,0 +1,19 @@ +import solver from "../index.js"; + +const timeStart = new Date().getTime(); +const solutions = await solver(Math.random().toString(), { + onProgress: (status) => { + process.stdout.moveCursor(0, -1); + process.stdout.clearScreenDown(); + console.log(`Progress: ${status.progress}%`); + }, + c: 50, + s: 30, + d: 4, +}); + +process.stdout.moveCursor(0, -1); +process.stdout.clearScreenDown(); + +console.log("Solutions:", solutions); +console.log("Time taken:", ((new Date().getTime()) - timeStart).toFixed(2), "ms"); \ No newline at end of file diff --git a/solver/index.js b/solver/index.js index 9ca3927..4c9cb8f 100644 --- a/solver/index.js +++ b/solver/index.js @@ -1,6 +1,34 @@ import { Worker } from "worker_threads"; import os from "os"; -import { performance } from "perf_hooks"; + +function prng(seed, length) { + function fnv1a(str) { + let hash = 2166136261; + for (let i = 0; i < str.length; i++) { + hash ^= str.charCodeAt(i); + hash += + (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24); + } + return hash >>> 0; + } + + let state = fnv1a(seed); + let result = ""; + + function next() { + state ^= state << 13; + state ^= state >>> 17; + state ^= state << 5; + return state >>> 0; + } + + while (result.length < length) { + const rnd = next(); + result += rnd.toString(16).padStart(8, "0"); + } + + return result.substring(0, length); +} const workerBlob = new Blob( [ @@ -18,8 +46,6 @@ try { parentPort.postMessage({ nonce, - salt, - target, challengeIndex, duration }); @@ -40,7 +66,22 @@ try { const workerUrl = URL.createObjectURL(workerBlob); -export default function wasmSolver(challenges, config = {}) { +export default function solve(challenge, config = {}) { + let challenges = challenge; + + if (!Array.isArray(challenges)) { + let i = 0; + + challenges = Array.from({ length: config.c }, () => { + i = i + 1; + + return [ + prng(`${challenges}${i}`, config.s), + prng(`${challenges}${i}d`, config.d), + ]; + }); + } + const totalChallenges = challenges.length; const numWorkers = config?.workerCount || Math.min(totalChallenges, os.cpus().length); @@ -50,9 +91,11 @@ export default function wasmSolver(challenges, config = {}) { let activeWorkers = 0; const results = new Array(totalChallenges); - return new Promise((resolve, reject) => { - const startTime = performance.now(); + if (totalChallenges === 0) { + resolve([]); + } + return new Promise((resolve, reject) => { function startWorker() { if (nextChallengeIndex < totalChallenges && activeWorkers < numWorkers) { const currentChallengeIndex = nextChallengeIndex; @@ -77,11 +120,7 @@ export default function wasmSolver(challenges, config = {}) { return; } - results[result.challengeIndex] = [ - result.salt, - result.target, - result.nonce, - ]; + results[result.challengeIndex] = result.nonce; challengesProcessed++; if (config?.onProgress) { @@ -97,10 +136,10 @@ export default function wasmSolver(challenges, config = {}) { } if (challengesProcessed === totalChallenges) { - resolve(results.filter(Boolean)); - } else { - startWorker(); + return resolve(results); } + + startWorker(); }); worker.on("error", (err) => { @@ -115,9 +154,5 @@ export default function wasmSolver(challenges, config = {}) { for (let i = 0; i < numWorkers; i++) { startWorker(); } - - if (totalChallenges === 0) { - resolve([]); - } }); } diff --git a/solver/package.json b/solver/package.json index fe99fad..91a8c84 100644 --- a/solver/package.json +++ b/solver/package.json @@ -1,11 +1,11 @@ { "name": "@cap.js/solver", - "version": "0.1.2", + "version": "0.1.3", "description": "Server-side solver for Cap, a lightweight, modern open-source CAPTCHA alternative designed using SHA-256 PoW.", "main": "index.js", "scripts": { "test": "bun ./example.js", - "npm:publish": "npm publish --access public" + "npm:publish": "bun publish --access public" }, "author": "Tiago", "license": "Apache-2.0",