From 894c9e6ab92b643662e6f7cdc2cb84eeff3d787f Mon Sep 17 00:00:00 2001
From: tiago <70700766+tiagozip@users.noreply.github.com>
Date: Sun, 1 Mar 2026 13:20:48 +0000
Subject: [PATCH] feat: speculative challenges, jwt challenge tokens,
standalone optimizations, format releases widget@0.1.38; standalone@2.2.0
---
.github/ISSUE_TEMPLATE/bug_report.md | 2 +-
.github/ISSUE_TEMPLATE/feature_request.md | 5 +-
README.md | 7 +-
demo/basic-store/index.html | 45 +-
demo/localized/index.html | 45 +-
demo/main/index.html | 45 +-
demo/solver/package.json | 10 +-
.../.vitepress/theme/components/Benchmark.vue | 39 +-
docs/.vitepress/theme/components/Demo.vue | 12 +-
docs/.vitepress/theme/index.js | 20 +-
docs/.vitepress/theme/style.css | 19 +-
docs/guide/alternatives.md | 4 +-
docs/guide/benchmark.md | 2 +-
docs/guide/effectiveness.md | 2 +-
docs/guide/floating.md | 5 +-
docs/guide/invisible.md | 15 +-
docs/guide/middleware/elysia.md | 7 +-
docs/guide/middleware/express.md | 2 +-
docs/guide/middleware/hono.md | 7 +-
docs/guide/server.md | 6 +-
docs/guide/standalone.md | 6 +-
docs/guide/standalone/index.md | 12 +-
docs/guide/standalone/installation.md | 2 +-
docs/guide/standalone/options.md | 2 +-
docs/guide/troubleshooting/instrumentation.md | 2 +-
docs/guide/widget.md | 38 +-
docs/guide/workings.md | 2 +-
docs/index.md | 2 +-
docs/package.json | 8 +-
server/README.md | 2 +-
server/index.d.ts | 447 +++++------
server/index.js | 37 +-
server/package.json | 54 +-
solver/example/list.js | 46 +-
solver/example/seeded.js | 16 +-
solver/index.js | 20 +-
solver/package.json | 54 +-
standalone/package.json | 2 +-
standalone/public/index.html | 2 +-
standalone/public/tester.html | 2 +-
standalone/src/cap.js | 462 +++++++++---
standalone/src/db.js | 31 +-
standalone/src/index.js | 23 +-
standalone/src/instrumentation-worker.js | 136 ++--
standalone/src/instrumentation.js | 263 ++++---
standalone/src/ratelimit.js | 42 +-
standalone/src/server.js | 5 +-
wasm/build.js | 42 +-
wasm/package.json | 2 +-
wasm/src/README.md | 2 +-
wasm/src/browser/cap_wasm.d.ts | 35 +-
wasm/src/browser/cap_wasm.js | 139 +++-
wasm/src/browser/package.json | 10 +-
wasm/src/node/cap_wasm.js | 74 +-
wasm/src/node/package.json | 2 +-
wasm/src/package.json | 44 +-
wasm/src/rust/Cargo.toml | 2 +-
wasm/test/browser.html | 55 +-
wasm/test/node.js | 54 +-
wasm/test/node_odd_difficulty.js | 4 +-
widget/package.json | 10 +-
widget/src/README.md | 2 +-
widget/src/cap.compat.min.js | 409 +++++++++-
widget/src/cap.d.ts | 60 +-
widget/src/cap.min.js | 2 +-
widget/src/package.json | 36 +-
widget/src/src/cap-floating.js | 30 +-
widget/src/src/cap.css | 4 +
widget/src/src/cap.js | 696 ++++++++++++++----
widget/src/wasm-hashes.min.js | 677 ++++++++---------
widget/test.html | 18 +-
71 files changed, 2840 insertions(+), 1586 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 1de4ace..946f7d4 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -4,7 +4,6 @@ about: Open a bug report
title: "[bug] "
labels: bug
assignees: tiagozip
-
---
+
+
```
The following attributes are supported:
- `data-cap-floating`: The CSS selector of the `cap-widget` element you want to use.
- `data-cap-floating-position`: The position of the floating widget. Can be `top` or `bottom`.
-- `data-cap-floating-offset`: The offset of the floating widget from the trigger element.
\ No newline at end of file
+- `data-cap-floating-offset`: The offset of the floating widget from the trigger element.
diff --git a/docs/guide/invisible.md b/docs/guide/invisible.md
index b9486f8..d938ea9 100644
--- a/docs/guide/invisible.md
+++ b/docs/guide/invisible.md
@@ -4,7 +4,7 @@ You can use `new Cap({ ... })` in your client-side JavaScript to create a new Ca
```js
const cap = new Cap({
- apiEndpoint: "/api/"
+ apiEndpoint: "/api/",
});
const solution = await cap.solve();
@@ -15,10 +15,11 @@ You can also set up [event listeners](widget.md#supported-events):
```js
const cap = new Cap({
- apiEndpoint: "/api/"
+ apiEndpoint: "/api/",
});
-cap.addEventListener("progress", (event) => { // [!code focus]
+cap.addEventListener("progress", (event) => {
+ // [!code focus]
console.log(`Solving... ${event.detail.progress}% done`);
});
```
@@ -30,9 +31,11 @@ Behind the scenes, Cap creates a hidden `cap-widget` element and uses it to solv
The following methods are supported:
#### `new Cap({ ... })`
+
Creates a new Cap instance. If a 2nd argument is provided, it will use that element instead of creating a new one in memory.
**Arguments**
+
```json
{
apiEndpoint: ..., // api endpoint, similar to the widget `data-cap-api-endpoint` attribute
@@ -41,15 +44,19 @@ Creates a new Cap instance. If a 2nd argument is provided, it will use that elem
```
#### `cap.solve()`
+
Requests and solves a challenge.
**Output:** `{ token }`
#### `cap.token`
+
Returns the token from the latest solve
#### `cap.reset()`
+
Resets `cap.token`
#### `cap.addEventListener(..., function () { ... })`
-Listens for an event for the cap widget. See [supported events](widget.md#supported-events)
\ No newline at end of file
+
+Listens for an event for the cap widget. See [supported events](widget.md#supported-events)
diff --git a/docs/guide/middleware/elysia.md b/docs/guide/middleware/elysia.md
index 23c562e..b37de2a 100644
--- a/docs/guide/middleware/elysia.md
+++ b/docs/guide/middleware/elysia.md
@@ -20,12 +20,9 @@ new Elysia()
token_validity_hours: 32, // how long the token is valid for
tokens_store_path: ".data/tokensList.json",
token_size: 16, // token size in bytes
- verification_template_path: join(
- dirname(fileURLToPath(import.meta.url)),
- "./index.html"
- ),
+ verification_template_path: join(dirname(fileURLToPath(import.meta.url)), "./index.html"),
scoping: "scoped", // 'global' | 'scoped'
- })
+ }),
)
.get("/", () => "Hello Elysia!")
.listen(3000);
diff --git a/docs/guide/middleware/express.md b/docs/guide/middleware/express.md
index e87662f..11d2ca0 100644
--- a/docs/guide/middleware/express.md
+++ b/docs/guide/middleware/express.md
@@ -30,7 +30,7 @@ app.use(
token_size: 16,
verification_template_path: join(__dirname, "./index.html"),
*/
- })
+ }),
);
app.get("/", (req, res) => {
diff --git a/docs/guide/middleware/hono.md b/docs/guide/middleware/hono.md
index 068677b..92c51b0 100644
--- a/docs/guide/middleware/hono.md
+++ b/docs/guide/middleware/hono.md
@@ -21,11 +21,8 @@ app.use(
token_validity_hours: 32, // how long the token is valid for
tokens_store_path: ".data/tokensList.json",
token_size: 16, // token size in bytes
- verification_template_path: join(
- dirname(fileURLToPath(import.meta.url)),
- "./index.html"
- ),
- })
+ verification_template_path: join(dirname(fileURLToPath(import.meta.url)), "./index.html"),
+ }),
);
app.get("/", (c) => c.text("Hello Hono!"));
diff --git a/docs/guide/server.md b/docs/guide/server.md
index de2ded6..39f8863 100644
--- a/docs/guide/server.md
+++ b/docs/guide/server.md
@@ -66,9 +66,7 @@ const cap = new Cap({
LIMIT 1
`;
- return row
- ? { challenge: row.data, expires: Number(row.expires) }
- : null;
+ return row ? { challenge: row.data, expires: Number(row.expires) } : null;
},
delete: async (token) => {
@@ -237,7 +235,7 @@ if (!success) throw new Error("invalid cap token");
"state": {
"challengesList": {},
"tokensList": {}
- },
+ }
// deprecated:
diff --git a/docs/guide/standalone.md b/docs/guide/standalone.md
index 0549df9..cda1e1c 100644
--- a/docs/guide/standalone.md
+++ b/docs/guide/standalone.md
@@ -1,7 +1,7 @@
# Standalone server
> [!WARNING]
-> **You are viewing the docs for a legacy version of Cap's Standalone server.** It is no longer actively maintained and will only receive critical security updates. While v1 is still tagged and pullable, certain commands below might not work anymore.
+> **You are viewing the docs for a legacy version of Cap's Standalone server.** It is no longer actively maintained and will only receive critical security updates. While v1 is still tagged and pullable, certain commands below might not work anymore.
> [New docs](standalone/index.md)
## Installation
@@ -62,9 +62,7 @@ Make sure to replace:
Example:
```html
-
+
```
> [!TIP]
diff --git a/docs/guide/standalone/index.md b/docs/guide/standalone/index.md
index f8ab441..811929f 100644
--- a/docs/guide/standalone/index.md
+++ b/docs/guide/standalone/index.md
@@ -29,12 +29,12 @@ volumes:
::: tip Tips
-* Make sure to add an admin key to log into the web UI. It should be at least 12 characters long.
+- Make sure to add an admin key to log into the web UI. It should be at least 12 characters long.
-* If your port 3000 is already in use, feel free to change it to something else.
+- If your port 3000 is already in use, feel free to change it to something else.
-* If you're having trouble accessing the dashboard, add `network_mode: "host"` under the Cap service.
-:::
+- If you're having trouble accessing the dashboard, add `network_mode: "host"` under the Cap service.
+ :::
Start the container:
@@ -66,9 +66,7 @@ Make sure to replace:
Example:
```html
-
+
```
### Server-side
diff --git a/docs/guide/standalone/installation.md b/docs/guide/standalone/installation.md
index e56da8f..c882469 100644
--- a/docs/guide/standalone/installation.md
+++ b/docs/guide/standalone/installation.md
@@ -1,3 +1,3 @@
# Installation
-This section has moved to [Standalone](/guide/standalone/index.md).
\ No newline at end of file
+This section has moved to [Standalone](/guide/standalone/index.md).
diff --git a/docs/guide/standalone/options.md b/docs/guide/standalone/options.md
index 43b5106..30de6f8 100644
--- a/docs/guide/standalone/options.md
+++ b/docs/guide/standalone/options.md
@@ -68,4 +68,4 @@ Error messages are redacted by default and instead logged to the console. To dis
Cap standalone supports our JavaScript instrumentation challenges to defeat proof-of-work solvers, along with options to try stop headless browsers from solving them.
-You can turn on instrumentation challenges by toggling them on in your site key config. To block headless browsers, turn on "Attempt to block headless browsers"
\ No newline at end of file
+You can turn on instrumentation challenges by toggling them on in your site key config. To block headless browsers, turn on "Attempt to block headless browsers"
diff --git a/docs/guide/troubleshooting/instrumentation.md b/docs/guide/troubleshooting/instrumentation.md
index a4e7f55..5b9bdc7 100644
--- a/docs/guide/troubleshooting/instrumentation.md
+++ b/docs/guide/troubleshooting/instrumentation.md
@@ -64,4 +64,4 @@ AI agent browsers are also blocked — make sure you're using a standard browser
---
-If you've tried all of the above and still can't get through, consider reaching out to the site owner for further assistance. You may also alternatively [file an issue](https://github.com/tiagozip/cap/issues).
\ No newline at end of file
+If you've tried all of the above and still can't get through, consider reaching out to the site owner for further assistance. You may also alternatively [file an issue](https://github.com/tiagozip/cap/issues).
diff --git a/docs/guide/widget.md b/docs/guide/widget.md
index 29fb3ba..c8825e1 100644
--- a/docs/guide/widget.md
+++ b/docs/guide/widget.md
@@ -111,24 +111,6 @@ You can change the text on each label of the widget by setting the `data-cap-i18
## Customizing the widget
-### fetch function
-
-You can override the default browser fetch implementation by setting `window.CAP_CUSTOM_FETCH` to a custom function. This function will receive the URL and options as arguments and should return a promise that resolves to the response.
-
-```js
-window.CAP_CUSTOM_FETCH = function (url, options) {
- // … add your custom fetch implementation
-
- return fetch(url, options);
-};
-```
-
-## WASM URL
-
-You can override the default WASM URL by setting `window.CAP_CUSTOM_WASM_URL` to a custom URL. This URL will be used to load the WASM module. This defaults to `https://cdn.jsdelivr.net/npm/@cap.js/wasm@0.0.4/browser/cap_wasm.min.js`
-
-## Customizing
-
You can fully change how the widget looks by setting various CSS variables on the `cap-widget` element. The following CSS variables are supported:
```css
@@ -159,6 +141,26 @@ cap-widget {
Note: you _can_ technically hide the "Cap" label, but we kindly ask you to leave it visible. It's unobtrusive, doesn't track users, lightweight, and helps Cap grow.
+### fetch function
+
+You can override the default browser fetch implementation by setting `window.CAP_CUSTOM_FETCH` to a custom function. This function will receive the URL and options as arguments and should return a promise that resolves to the response.
+
+```js
+window.CAP_CUSTOM_FETCH = function (url, options) {
+ // … add your custom fetch implementation
+
+ return fetch(url, options);
+};
+```
+
+### Speculative challenges
+
+By default, Cap will sometimes automatically generate, solve and redeem a challenge while the user is active to make solving significantly faster. This is intentionally much slower than the normal solving speed and is designed to not impact user experience while still providing a significant speed boost.
+
+### WASM URL
+
+You can override the default WASM URL by setting `window.CAP_CUSTOM_WASM_URL` to a custom URL. This URL will be used to load the WASM module. This defaults to `https://cdn.jsdelivr.net/npm/@cap.js/wasm@0.0.4/browser/cap_wasm.min.js`
+
## Types
Cap's widget is fully typed. You can find the type definitions in the `cap.d.ts` file.
diff --git a/docs/guide/workings.md b/docs/guide/workings.md
index 666aa87..2eca3cc 100644
--- a/docs/guide/workings.md
+++ b/docs/guide/workings.md
@@ -2,7 +2,7 @@
By the way, this is a more technical explanation of how Cap works. If you're looking for a more general overview, check out the [Effectiveness](./effectiveness.md) page.
-***
+---
1. When Cap is initialized, it automatically registers a custom element for the widget in the browser
2. The widget creates a shadow DOM and appends all necessary elements to it
diff --git a/docs/index.md b/docs/index.md
index 61fa774..78c60a3 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -45,4 +45,4 @@ features:
- icon: 🌳
title: Open-source
details: Completely free & open-source under the Apache 2.0 license
----
\ No newline at end of file
+---
diff --git a/docs/package.json b/docs/package.json
index 4a00ade..d1e26d4 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -5,13 +5,13 @@
"build": "vitepress build",
"preview": "vitepress preview"
},
- "devDependencies": {
- "vue": "^3.5.13"
- },
"dependencies": {
"@cap.js/widget": "^0.1.18",
"vitepress": "^1.6.3",
"vitepress-plugin-llms": "^1.1.1",
"vitepress-plugin-mermaid": "^2.0.17"
+ },
+ "devDependencies": {
+ "vue": "^3.5.13"
}
-}
\ No newline at end of file
+}
diff --git a/server/README.md b/server/README.md
index 7419daa..d5bb945 100644
--- a/server/README.md
+++ b/server/README.md
@@ -2,4 +2,4 @@
Server-side challenge generator and verifier for Cap, a lightweight, modern open-source CAPTCHA alternative designed using SHA-256 PoW.
-**[Learn more](https://github.com/tiagozip/cap)**
\ No newline at end of file
+**[Learn more](https://github.com/tiagozip/cap)**
diff --git a/server/index.d.ts b/server/index.d.ts
index 3a88e5a..00db601 100644
--- a/server/index.d.ts
+++ b/server/index.d.ts
@@ -4,111 +4,128 @@ export = Cap;
* @extends EventEmitter
*/
declare class Cap extends EventEmitter {
- /**
- * Creates a new Cap instance
- * @param {Partial} [configObj] - Configuration object
- */
- constructor(configObj?: Partial);
- /** @type {Promise|null} */
- _cleanupPromise: Promise | null;
- /** @type {number} */
- _lastCleanup: number;
- /** @type {CapConfig} */
- config: CapConfig;
- /**
- * Performs cleanup if enough time has passed since last cleanup
- * @private
- * @returns {Promise}
- */
- private _lazyCleanup;
- /**
- * Retrieves challenge data from storage
- * @private
- * @param {string} token - Challenge token
- * @returns {Promise} Challenge data or null if not found
- */
- private _getChallenge;
- /**
- * Deletes challenge from storage
- * @private
- * @param {string} token - Challenge token
- * @returns {Promise}
- */
- private _deleteChallenge;
- /**
- * Generates a new challenge
- * @param {ChallengeConfig} [conf] - Challenge configuration
- * @returns {Promise<{ challenge: {c: number, s: number, d: number}, token?: string, expires: number }>} Challenge data
- */
- createChallenge(conf?: ChallengeConfig): Promise<{
- challenge: {
- c: number;
- s: number;
- d: number;
- };
- token?: string;
- expires: number;
- }>;
- /**
- * Redeems a challenge solution in exchange for a token
- * @param {Solution} param0 - Challenge solution data
- * @returns {Promise<{success: boolean, message?: string, token?: string, expires?: number}>}
- */
- redeemChallenge({ token, solutions }: Solution): Promise<{
- success: boolean;
- message?: string;
- token?: string;
- expires?: number;
- }>;
- /**
- * Retrieves token expiration from storage
- * @private
- * @param {string} tokenKey - Token key
- * @returns {Promise} Token expiration or null if not found
- */
- private _getToken;
- /**
- * Deletes token from storage
- * @private
- * @param {string} tokenKey - Token key
- * @returns {Promise}
- */
- private _deleteToken;
- /**
- * Validates a token
- * @param {string} token - The token to validate
- * @param {TokenConfig} [conf] - Validation configuration
- * @returns {Promise<{success: boolean}>}
- */
- validateToken(token: string, conf?: TokenConfig): Promise<{
- success: boolean;
- }>;
- /**
- * Loads tokens from the storage file
- * @private
- * @returns {Promise}
- */
- private _loadTokens;
- /**
- * Removes expired tokens and challenges from memory and storage
- * @private
- * @returns {Promise} - True if any tokens were changed/removed
- */
- private _cleanExpiredTokens;
- /**
- * Waits for the tokens list to be initialized
- * @private
- * @returns {Promise}
- */
- private _waitForTokensList;
- /**
- * Cleans up expired tokens and syncs state
- * @returns {Promise}
- */
- cleanup(): Promise;
+ /**
+ * Creates a new Cap instance
+ * @param {Partial} [configObj] - Configuration object
+ */
+ constructor(configObj?: Partial);
+ /** @type {Promise|null} */
+ _cleanupPromise: Promise | null;
+ /** @type {number} */
+ _lastCleanup: number;
+ /** @type {CapConfig} */
+ config: CapConfig;
+ /**
+ * Performs cleanup if enough time has passed since last cleanup
+ * @private
+ * @returns {Promise}
+ */
+ private _lazyCleanup;
+ /**
+ * Retrieves challenge data from storage
+ * @private
+ * @param {string} token - Challenge token
+ * @returns {Promise} Challenge data or null if not found
+ */
+ private _getChallenge;
+ /**
+ * Deletes challenge from storage
+ * @private
+ * @param {string} token - Challenge token
+ * @returns {Promise}
+ */
+ private _deleteChallenge;
+ /**
+ * Generates a new challenge
+ * @param {ChallengeConfig} [conf] - Challenge configuration
+ * @returns {Promise<{ challenge: {c: number, s: number, d: number}, token?: string, expires: number }>} Challenge data
+ */
+ createChallenge(conf?: ChallengeConfig): Promise<{
+ challenge: {
+ c: number;
+ s: number;
+ d: number;
+ };
+ token?: string;
+ expires: number;
+ }>;
+ /**
+ * Redeems a challenge solution in exchange for a token
+ * @param {Solution} param0 - Challenge solution data
+ * @returns {Promise<{success: boolean, message?: string, token?: string, expires?: number}>}
+ */
+ redeemChallenge({ token, solutions }: Solution): Promise<{
+ success: boolean;
+ message?: string;
+ token?: string;
+ expires?: number;
+ }>;
+ /**
+ * Retrieves token expiration from storage
+ * @private
+ * @param {string} tokenKey - Token key
+ * @returns {Promise} Token expiration or null if not found
+ */
+ private _getToken;
+ /**
+ * Deletes token from storage
+ * @private
+ * @param {string} tokenKey - Token key
+ * @returns {Promise}
+ */
+ private _deleteToken;
+ /**
+ * Validates a token
+ * @param {string} token - The token to validate
+ * @param {TokenConfig} [conf] - Validation configuration
+ * @returns {Promise<{success: boolean}>}
+ */
+ validateToken(
+ token: string,
+ conf?: TokenConfig,
+ ): Promise<{
+ success: boolean;
+ }>;
+ /**
+ * Loads tokens from the storage file
+ * @private
+ * @returns {Promise}
+ */
+ private _loadTokens;
+ /**
+ * Removes expired tokens and challenges from memory and storage
+ * @private
+ * @returns {Promise} - True if any tokens were changed/removed
+ */
+ private _cleanExpiredTokens;
+ /**
+ * Waits for the tokens list to be initialized
+ * @private
+ * @returns {Promise}
+ */
+ private _waitForTokensList;
+ /**
+ * Cleans up expired tokens and syncs state
+ * @returns {Promise}
+ */
+ cleanup(): Promise;
}
declare namespace Cap {
- export { Crypto, FsPromises, PathLike, ChallengeTuple, ChallengeData, ChallengeState, ChallengeConfig, TokenConfig, Solution, ChallengeStorage, TokenStorage, StorageHooks, CapConfig };
+ export {
+ Crypto,
+ FsPromises,
+ PathLike,
+ ChallengeTuple,
+ ChallengeData,
+ ChallengeState,
+ ChallengeConfig,
+ TokenConfig,
+ Solution,
+ ChallengeStorage,
+ TokenStorage,
+ StorageHooks,
+ CapConfig,
+ };
}
import { EventEmitter } from "node:events";
type Crypto = typeof import("node:crypto");
@@ -116,132 +133,132 @@ type FsPromises = typeof import("node:fs/promises");
type PathLike = import("fs").PathLike;
type ChallengeTuple = [string, string];
type ChallengeData = {
- /**
- * - Challenge configuration object
- */
- challenge: {
- c: number;
- s: number;
- d: number;
- };
- /**
- * - Expiration timestamp
- */
- expires: number;
+ /**
+ * - Challenge configuration object
+ */
+ challenge: {
+ c: number;
+ s: number;
+ d: number;
+ };
+ /**
+ * - Expiration timestamp
+ */
+ expires: number;
};
type ChallengeState = {
- /**
- * - Map of challenge tokens to challenge data
- */
- challengesList: Record;
- /**
- * - Map of token hashes to expiration timestamps
- */
- tokensList: Record;
+ /**
+ * - Map of challenge tokens to challenge data
+ */
+ challengesList: Record;
+ /**
+ * - Map of token hashes to expiration timestamps
+ */
+ tokensList: Record;
};
type ChallengeConfig = {
- /**
- * - Number of challenges to generate
- */
- challengeCount?: number | undefined;
- /**
- * - Size of each challenge in bytes
- */
- challengeSize?: number | undefined;
- /**
- * - Difficulty level of the challenge
- */
- challengeDifficulty?: number | undefined;
- /**
- * - Time in milliseconds until the challenge expires
- */
- expiresMs?: number | undefined;
- /**
- * - Whether to store the challenge in memory
- */
- store?: boolean | undefined;
+ /**
+ * - Number of challenges to generate
+ */
+ challengeCount?: number | undefined;
+ /**
+ * - Size of each challenge in bytes
+ */
+ challengeSize?: number | undefined;
+ /**
+ * - Difficulty level of the challenge
+ */
+ challengeDifficulty?: number | undefined;
+ /**
+ * - Time in milliseconds until the challenge expires
+ */
+ expiresMs?: number | undefined;
+ /**
+ * - Whether to store the challenge in memory
+ */
+ store?: boolean | undefined;
};
type TokenConfig = {
- /**
- * - Whether to keep the token after validation
- */
- keepToken?: boolean | undefined;
+ /**
+ * - Whether to keep the token after validation
+ */
+ keepToken?: boolean | undefined;
};
type Solution = {
- /**
- * - Challenge token
- */
- token: string;
- /**
- * - Array of challenge solutions
- */
- solutions: number[];
+ /**
+ * - Challenge token
+ */
+ token: string;
+ /**
+ * - Array of challenge solutions
+ */
+ solutions: number[];
};
type ChallengeStorage = {
- /**
- * - Store challenge data
- */
- store: (arg0: string, arg1: ChallengeData) => Promise;
- /**
- * - Retrieve challenge data
- */
- read: (arg0: string) => Promise;
- /**
- * - Delete challenge data
- */
- delete: (arg0: string) => Promise;
- /**
- * - Delete expired challenge tokens
- */
- deleteExpired: () => Promise;
+ /**
+ * - Store challenge data
+ */
+ store: (arg0: string, arg1: ChallengeData) => Promise;
+ /**
+ * - Retrieve challenge data
+ */
+ read: (arg0: string) => Promise;
+ /**
+ * - Delete challenge data
+ */
+ delete: (arg0: string) => Promise;
+ /**
+ * - Delete expired challenge tokens
+ */
+ deleteExpired: () => Promise;
};
type TokenStorage = {
- /**
- * - Store token with expiration
- */
- store: (arg0: string, arg1: number) => Promise;
- /**
- * - Retrieve token expiration
- */
- get: (arg0: string) => Promise;
- /**
- * - Delete token
- */
- delete: (arg0: string) => Promise;
- /**
- * - Delete expired token keys
- */
- deleteExpired: () => Promise;
+ /**
+ * - Store token with expiration
+ */
+ store: (arg0: string, arg1: number) => Promise;
+ /**
+ * - Retrieve token expiration
+ */
+ get: (arg0: string) => Promise;
+ /**
+ * - Delete token
+ */
+ delete: (arg0: string) => Promise;
+ /**
+ * - Delete expired token keys
+ */
+ deleteExpired: () => Promise;
};
type StorageHooks = {
- /**
- * - Challenge storage hooks
- */
- challenges?: ChallengeStorage | undefined;
- /**
- * - Token storage hooks
- */
- tokens?: TokenStorage | undefined;
+ /**
+ * - Challenge storage hooks
+ */
+ challenges?: ChallengeStorage | undefined;
+ /**
+ * - Token storage hooks
+ */
+ tokens?: TokenStorage | undefined;
};
type CapConfig = {
- /**
- * - Path to store the tokens file
- */
- tokens_store_path: string;
- /**
- * - State configuration
- */
- state: ChallengeState;
- /**
- * - Whether to disable the state file
- */
- noFSState: boolean;
- /**
- * - Whether to disable automatic cleanup of expired tokens and challenges
- */
- disableAutoCleanup?: boolean | undefined;
- /**
- * - Custom storage hooks for challenges and tokens
- */
- storage?: StorageHooks | undefined;
+ /**
+ * - Path to store the tokens file
+ */
+ tokens_store_path: string;
+ /**
+ * - State configuration
+ */
+ state: ChallengeState;
+ /**
+ * - Whether to disable the state file
+ */
+ noFSState: boolean;
+ /**
+ * - Whether to disable automatic cleanup of expired tokens and challenges
+ */
+ disableAutoCleanup?: boolean | undefined;
+ /**
+ * - Custom storage hooks for challenges and tokens
+ */
+ storage?: StorageHooks | undefined;
};
diff --git a/server/index.js b/server/index.js
index c35070d..51437f7 100644
--- a/server/index.js
+++ b/server/index.js
@@ -116,10 +116,7 @@ async function randomHex(bytesCount) {
async function sha256(str) {
if (crypto.webcrypto?.subtle) {
const enc = new TextEncoder();
- const hash = await crypto.webcrypto.subtle.digest(
- "SHA-256",
- enc.encode(str)
- );
+ const hash = await crypto.webcrypto.subtle.digest("SHA-256", enc.encode(str));
return Buffer.from(hash).toString("hex");
}
@@ -141,8 +138,7 @@ function prng(seed, length) {
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);
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
}
return hash >>> 0;
}
@@ -332,7 +328,7 @@ class Cap extends EventEmitter {
challenges.map(([salt, target], i) => {
if (typeof solutions[i] !== "number") return null;
return sha256(salt + solutions[i]).then((h) => [h, target]);
- })
+ }),
);
const isValid = hashes.every((pair) => pair?.[0].startsWith(pair[1]));
@@ -356,7 +352,7 @@ class Cap extends EventEmitter {
await fs.writeFile(
this.config.tokens_store_path,
JSON.stringify(this.config.state.tokensList),
- "utf8"
+ "utf8",
);
}
}
@@ -394,7 +390,7 @@ class Cap extends EventEmitter {
await fs.writeFile(
this.config.tokens_store_path,
JSON.stringify(this.config.state.tokensList),
- "utf8"
+ "utf8",
);
}
}
@@ -445,10 +441,7 @@ class Cap extends EventEmitter {
if (this.config.noFSState || this.config.storage?.tokens) return;
try {
- const dirPath = this.config.tokens_store_path
- .split("/")
- .slice(0, -1)
- .join("/");
+ const dirPath = this.config.tokens_store_path.split("/").slice(0, -1).join("/");
if (dirPath) {
await fs.mkdir(dirPath, { recursive: true });
@@ -465,9 +458,7 @@ class Cap extends EventEmitter {
this.config.state.tokensList = {};
}
} catch {
- console.warn(
- `[cap] Couldn't load or write tokens file, using empty state`
- );
+ console.warn(`[cap] Couldn't load or write tokens file, using empty state`);
this.config.state.tokensList = {};
}
}
@@ -491,11 +482,11 @@ class Cap extends EventEmitter {
await Promise.all(
expired.map(async (k) => {
await this._deleteChallenge(k);
- })
+ }),
);
} else {
console.warn(
- "[cap] challenge storage hooks provided but no deleteExpired, couldn't delete expired challenges"
+ "[cap] challenge storage hooks provided but no deleteExpired, couldn't delete expired challenges",
);
}
@@ -510,7 +501,7 @@ class Cap extends EventEmitter {
}
} else {
console.warn(
- "[cap] token storage hooks provided but no deleteExpired, couldn't delete expired tokens"
+ "[cap] token storage hooks provided but no deleteExpired, couldn't delete expired tokens",
);
}
@@ -548,15 +539,11 @@ class Cap extends EventEmitter {
this._cleanupPromise = (async () => {
const tokensChanged = await this._cleanExpiredTokens();
- if (
- tokensChanged &&
- !this.config.noFSState &&
- !this.config.storage?.tokens?.store
- ) {
+ if (tokensChanged && !this.config.noFSState && !this.config.storage?.tokens?.store) {
await fs.writeFile(
this.config.tokens_store_path,
JSON.stringify(this.config.state.tokensList),
- "utf8"
+ "utf8",
);
}
})();
diff --git a/server/package.json b/server/package.json
index be71511..bcb7226 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,62 +1,62 @@
{
"name": "@cap.js/server",
"version": "4.0.5",
- "author": "Tiago",
- "repository": {
- "type": "git",
- "url": "git+https://github.com/tiagozip/cap.git"
- },
- "main": "index.js",
- "bugs": {
- "url": "https://github.com/tiagozip/cap/issues"
- },
"description": "Server-side challenge generator and verifier for Cap, a lightweight, modern open-source CAPTCHA alternative designed using SHA-256 PoW.",
- "homepage": "https://github.com/tiagozip/cap#readme",
"keywords": [
- "security",
- "detection",
- "prevention",
- "defense",
- "protection",
+ "algorithm",
"anti-abuse",
"anti-automation",
"anti-bot",
"anti-ddos",
"anti-dos",
"anti-exploitation",
- "anti-spam",
"anti-scraping",
+ "anti-spam",
"attack-mitigation",
- "protocol",
+ "bots",
+ "captcha-alternative",
"client-server",
- "computational-puzzle",
"complexity",
- "puzzle",
+ "computational-puzzle",
"crypto",
"cryptographic",
- "algorithm",
"cybersecurity",
"ddos",
+ "defense",
+ "detection",
+ "filtering",
"hashcash",
"hcaptcha",
- "captcha-alternative",
- "verification",
"invisible",
"pow",
+ "prevention",
"proof-of-work",
+ "protection",
+ "protocol",
+ "puzzle",
"recaptcha",
+ "security",
"spam",
- "bots",
- "filtering",
- "turing-test"
+ "turing-test",
+ "verification"
],
+ "homepage": "https://github.com/tiagozip/cap#readme",
+ "bugs": {
+ "url": "https://github.com/tiagozip/cap/issues"
+ },
"license": "Apache-2.0",
+ "author": "Tiago",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/tiagozip/cap.git"
+ },
+ "type": "commonjs",
+ "main": "index.js",
+ "types": "index.d.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"publish": "bunx tsc && bun publish --access public"
},
- "type": "commonjs",
- "types": "index.d.ts",
"devDependencies": {
"@types/node": "^25.0.2",
"typescript": "^5.9.3"
diff --git a/solver/example/list.js b/solver/example/list.js
index 32a1dca..295ff2a 100644
--- a/solver/example/list.js
+++ b/solver/example/list.js
@@ -1,33 +1,33 @@
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"],
+ ["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"],
];
const timeStart = Date.now();
const solutions = await solver(CHALLENGES, {
- onProgress: (status) => {
- process.stdout.moveCursor(0, -1);
- process.stdout.clearScreenDown();
- console.log(`Progress: ${status.progress}%`);
- },
+ onProgress: (status) => {
+ process.stdout.moveCursor(0, -1);
+ process.stdout.clearScreenDown();
+ console.log(`Progress: ${status.progress}%`);
+ },
});
process.stdout.moveCursor(0, -1);
diff --git a/solver/example/seeded.js b/solver/example/seeded.js
index b79258f..f4d37d0 100644
--- a/solver/example/seeded.js
+++ b/solver/example/seeded.js
@@ -2,14 +2,14 @@ import solver from "../index.js";
const timeStart = Date.now();
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,
+ 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);
diff --git a/solver/index.js b/solver/index.js
index 7c44ab7..1c6a047 100644
--- a/solver/index.js
+++ b/solver/index.js
@@ -6,8 +6,7 @@ function prng(seed, length) {
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);
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
}
return hash >>> 0;
}
@@ -75,16 +74,12 @@ export default function solve(challenge, config = {}) {
challenges = Array.from({ length: config.c }, () => {
i = i + 1;
- return [
- prng(`${challenges}${i}`, config.s),
- prng(`${challenges}${i}d`, config.d),
- ];
+ 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);
+ const numWorkers = config?.workerCount || Math.min(totalChallenges, os.cpus().length);
let challengesProcessed = 0;
let nextChallengeIndex = 0;
@@ -112,10 +107,7 @@ export default function solve(challenge, config = {}) {
activeWorkers--;
if (result.error) {
- console.error(
- `Error in worker for challenge ${currentChallengeIndex}:`,
- result.error,
- );
+ console.error(`Error in worker for challenge ${currentChallengeIndex}:`, result.error);
reject(new Error(`Worker error: ${result.error}`));
return;
}
@@ -125,9 +117,7 @@ export default function solve(challenge, config = {}) {
if (config?.onProgress) {
config.onProgress({
- progress: Math.floor(
- (challengesProcessed / totalChallenges) * 100,
- ),
+ progress: Math.floor((challengesProcessed / totalChallenges) * 100),
currentChallenge: currentChallengeIndex,
challengesProcessed,
totalChallenges,
diff --git a/solver/package.json b/solver/package.json
index 8ec1b46..fbf506b 100644
--- a/solver/package.json
+++ b/solver/package.json
@@ -2,59 +2,59 @@
"name": "@cap.js/solver",
"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": "bun publish --access public"
- },
- "author": "Tiago",
- "license": "Apache-2.0",
- "repository": {
- "type": "git",
- "url": "git+https://github.com/tiagozip/cap.git"
- },
"keywords": [
- "security",
- "detection",
- "prevention",
- "defense",
- "protection",
+ "algorithm",
"anti-abuse",
"anti-automation",
"anti-bot",
"anti-ddos",
"anti-dos",
"anti-exploitation",
- "anti-spam",
"anti-scraping",
+ "anti-spam",
"attack-mitigation",
- "protocol",
+ "bots",
+ "captcha-alternative",
"client-server",
- "computational-puzzle",
"complexity",
- "puzzle",
+ "computational-puzzle",
"crypto",
"cryptographic",
- "algorithm",
"cybersecurity",
"ddos",
+ "defense",
+ "detection",
+ "filtering",
"hashcash",
"hcaptcha",
- "captcha-alternative",
- "verification",
"invisible",
"pow",
+ "prevention",
"proof-of-work",
+ "protection",
+ "protocol",
+ "puzzle",
"recaptcha",
+ "security",
"spam",
- "bots",
- "filtering",
- "turing-test"
+ "turing-test",
+ "verification"
],
+ "homepage": "https://github.com/tiagozip/cap#readme",
"bugs": {
"url": "https://github.com/tiagozip/cap/issues"
},
- "homepage": "https://github.com/tiagozip/cap#readme",
+ "license": "Apache-2.0",
+ "author": "Tiago",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/tiagozip/cap.git"
+ },
+ "main": "index.js",
+ "scripts": {
+ "test": "bun ./example.js",
+ "npm:publish": "bun publish --access public"
+ },
"dependencies": {
"@cap.js/wasm": "^0.0.4"
}
diff --git a/standalone/package.json b/standalone/package.json
index dec97a9..e66fe21 100644
--- a/standalone/package.json
+++ b/standalone/package.json
@@ -1,6 +1,6 @@
{
"name": "cap-standalone",
- "version": "2.1.5",
+ "version": "2.2.0",
"keywords": [
"algorithm",
"anti-abuse",
diff --git a/standalone/public/index.html b/standalone/public/index.html
index 87a59db..f787206 100644
--- a/standalone/public/index.html
+++ b/standalone/public/index.html
@@ -545,7 +545,7 @@
.save-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
- filter: grayscale(.3);
+ filter: grayscale(0.3);
}
.danger-section {
diff --git a/standalone/public/tester.html b/standalone/public/tester.html
index e8e3906..740f2cb 100644
--- a/standalone/public/tester.html
+++ b/standalone/public/tester.html
@@ -74,7 +74,7 @@
No logs
-
+