feat: seeded challenges and many other improvements!

BREAKING for server->widget. if you're using an old version of the widget with the new server, it will not work. old server versions with the new widget will work fine.
This commit is contained in:
Tiago
2025-07-02 12:45:37 +01:00
parent 9ff2451076
commit c104dcb992
12 changed files with 197 additions and 84 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
import fs from "fs/promises";
import { Elysia, file } from "elysia";
import Cap from "@cap.js/server";
import Cap from "../../server/index.js";
const cap = new Cap({
tokens_store_path: ".data/tokensList.json",
+15 -11
View File
@@ -16,10 +16,14 @@ declare class Cap extends EventEmitter<[never]> {
/**
* Generates a new challenge
* @param {ChallengeConfig} [conf] - Challenge configuration
* @returns {{ challenge: Array<ChallengeTuple>, token?: string, expires: number }} Challenge data
* @returns {{ challenge: {c: number, s: number, d: number}, token?: string, expires: number }} Challenge data
*/
createChallenge(conf?: ChallengeConfig): {
challenge: Array<ChallengeTuple>;
challenge: {
c: number;
s: number;
d: number;
};
token?: string;
expires: number;
};
@@ -77,17 +81,17 @@ type PathLike = import("fs").PathLike;
type ChallengeTuple = [string, string];
type ChallengeData = {
/**
* - Array of [salt, target] tuples
* - Challenge configuration object
*/
challenge: Array<ChallengeTuple>;
challenge: {
c: number;
s: number;
d: number;
};
/**
* - Expiration timestamp
*/
expires: number;
/**
* - Challenge token
*/
token: string;
};
type ChallengeState = {
/**
@@ -129,13 +133,13 @@ type TokenConfig = {
};
type Solution = {
/**
* - The challenge token
* - Challenge token
*/
token: string;
/**
* - Array of [salt, target, solution] tuples
* - Array of challenge solutions
*/
solutions: Array<[string, string, number]>;
solutions: number[];
};
type CapConfig = {
/**
+79 -35
View File
@@ -14,9 +14,11 @@
/**
* @typedef {Object} ChallengeData
* @property {Array<ChallengeTuple>} challenge - Array of [salt, target] tuples
* @property {Object} challenge - Challenge configuration object
* @property {number} challenge.c - Number of challenges
* @property {number} challenge.s - Size of each challenge
* @property {number} challenge.d - Difficulty level
* @property {number} expires - Expiration timestamp
* @property {string} token - Challenge token
*/
/**
@@ -41,8 +43,8 @@
/**
* @typedef {Object} Solution
* @property {string} token - The challenge token
* @property {Array<[string, string, number]>} solutions - Array of [salt, target, solution] tuples
* @property {string} token - Challenge token
* @property {number[]} solutions - Array of challenge solutions
*/
/**
@@ -60,6 +62,45 @@ const { EventEmitter } = require("events");
const DEFAULT_TOKENS_STORE = ".data/tokensList.json";
/**
* Generates a deterministic hex string of given length from a string seed
*
* @param {string} seed - Initial seed value
* @param {number} length - Output hex string length
* @returns {string} Deterministic hex string generated from the seed
*/
function prng(seed, length) {
/**
* @param {string} str
*/
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);
}
/**
* Main Cap class
* @extends EventEmitter
@@ -107,43 +148,28 @@ class Cap extends EventEmitter {
/**
* Generates a new challenge
* @param {ChallengeConfig} [conf] - Challenge configuration
* @returns {{ challenge: Array<ChallengeTuple>, token?: string, expires: number }} Challenge data
* @returns {{ challenge: {c: number, s: number, d: number}, token?: string, expires: number }} Challenge data
*/
createChallenge(conf) {
this._cleanExpiredTokens();
/** @type {Array<ChallengeTuple>} */
const challenges = Array.from(
{ length: (conf && conf.challengeCount) || 50 },
() =>
/** @type {ChallengeTuple} */ ([
crypto
.randomBytes(Math.ceil(((conf && conf.challengeSize) || 32) / 2))
.toString("hex")
.slice(0, (conf && conf.challengeSize) || 32),
crypto
.randomBytes(
Math.ceil(((conf && conf.challengeDifficulty) || 4) / 2)
)
.toString("hex")
.slice(0, (conf && conf.challengeDifficulty) || 4),
])
);
/** @type {{c: number, s: number, d: number}} */
const challenge = {
c: (conf && conf.challengeCount) || 50,
s: (conf && conf.challengeSize) || 32,
d: (conf && conf.challengeDifficulty) || 4,
};
const token = crypto.randomBytes(25).toString("hex");
const expires = Date.now() + ((conf && conf.expiresMs) || 600000);
if (conf && conf.store === false) {
return { challenge: challenges, expires };
return { challenge, expires };
}
this.config.state.challengesList[token] = {
challenge: challenges,
expires,
token,
};
this.config.state.challengesList[token] = { expires, challenge };
return { challenge: challenges, token, expires };
return { challenge, token, expires };
}
/**
@@ -152,7 +178,12 @@ class Cap extends EventEmitter {
* @returns {Promise<{success: boolean, message?: string, token?: string, expires?: number}>}
*/
async redeemChallenge({ token, solutions }) {
if (!token || !solutions) {
if (
!token ||
!solutions ||
!Array.isArray(solutions) ||
solutions.some((s) => typeof s !== "number")
) {
return { success: false, message: "Invalid body" };
}
@@ -166,13 +197,26 @@ class Cap extends EventEmitter {
delete this.config.state.challengesList[token];
const isValid = challengeData.challenge.every(([salt, target]) => {
const solution = solutions.find(([s, t]) => s === salt && t === target);
let i = 0;
const challenges = Array.from(
{ length: challengeData.challenge.c },
() => {
i = i + 1;
return [
prng(`${token}${i}`, challengeData.challenge.s),
prng(`${token}${i}d`, challengeData.challenge.d),
];
}
);
const isValid = challenges.every(([salt, target], i) => {
return (
solution &&
solutions[i] &&
crypto
.createHash("sha256")
.update(salt + solution[2])
.update(salt + solutions[i])
.digest("hex")
.startsWith(target)
);
@@ -218,7 +262,7 @@ class Cap extends EventEmitter {
if (!(conf && conf.keepToken)) {
delete this.config.state.tokensList[key];
}
if (!this.config.noFSState) {
await fs.writeFile(
this.config.tokens_store_path,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@cap.js/server",
"version": "1.0.16",
"version": "2.0.0",
"author": "Tiago",
"repository": {
"type": "git",
+20 -18
View File
@@ -2,14 +2,14 @@
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "app",
"name": "cap-standalone",
"dependencies": {
"@cap.js/server": "^1.0.13",
"@cap.js/server": "^1.0.16",
"@elysiajs/cors": "^1.3.3",
"@elysiajs/static": "^1.3.0",
"@elysiajs/swagger": "^1.3.0",
"elysia": "^1.3.1",
"elysia-rate-limit": "^4.3.0",
"@elysiajs/swagger": "^1.3.1",
"elysia": "^1.3.5",
"elysia-rate-limit": "^4.4.0",
},
"devDependencies": {
"bun-types": "latest",
@@ -19,13 +19,13 @@
"packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@cap.js/server": ["@cap.js/server@1.0.13", "", { "dependencies": { "@types/node": "^22.14.1", "typescript": "^5.8.3" } }, "sha512-JcEd/XdqheDSaoBdW0YK4o2lpxlFiCipcUcvgr8ZHscprYVXOTpk7fpagiXqJAJSr68dwyaYk+h8WBv42R4QTw=="],
"@cap.js/server": ["@cap.js/server@1.0.16", "", { "dependencies": { "@types/node": "^22.14.1", "typescript": "^5.8.3" } }, "sha512-sUutpIiSdFwVkyHJWMQqdfz7DDD3vE/uT7pA80fV2yOWA/wQRlNkIGhQNsWBazwhkPhA2bFsLLIhPvp8MUv10w=="],
"@elysiajs/cors": ["@elysiajs/cors@1.3.3", "", { "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-mYIU6PyMM6xIJuj7d27Vt0/wuzVKIEnFPjcvlkyd7t/m9xspAG37cwNjFxVOnyvY43oOd2I/oW2DB85utXpA2Q=="],
"@elysiajs/static": ["@elysiajs/static@1.3.0", "", { "dependencies": { "node-cache": "^5.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-7mWlj2U/AZvH27IfRKqpUjDP1W9ZRldF9NmdnatFEtx0AOy7YYgyk0rt5hXrH6wPcR//2gO2Qy+k5rwswpEhJA=="],
"@elysiajs/swagger": ["@elysiajs/swagger@1.3.0", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-0fo3FWkDRPNYpowJvLz3jBHe9bFe6gruZUyf+feKvUEEMG9ZHptO1jolSoPE0ffFw1BgN1/wMsP19p4GRXKdfg=="],
"@elysiajs/swagger": ["@elysiajs/swagger@1.3.1", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ=="],
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="],
@@ -33,17 +33,17 @@
"@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.37", "", {}, "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw=="],
"@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="],
"@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="],
"bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="],
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
"clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
@@ -51,9 +51,9 @@
"debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="],
"elysia": ["elysia@1.3.1", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-En41P6cDHcHtQ0nvfsn9ayB+8ahQJqG1nzvPX8FVZjOriFK/RtZPQBtXMfZDq/AsVIk7JFZGFEtAVEmztNJVhQ=="],
"elysia": ["elysia@1.3.5", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-XVIKXlKFwUT7Sta8GY+wO5reD9I0rqAEtaz1Z71UgJb61csYt8Q3W9al8rtL5RgumuRR8e3DNdzlUN9GkC4KDw=="],
"elysia-rate-limit": ["elysia-rate-limit@4.3.0", "", { "dependencies": { "@alloc/quick-lru": "5.2.0", "debug": "4.3.4" }, "peerDependencies": { "elysia": ">= 1.0.0" } }, "sha512-TCquBhMqUK+Wce+2RoaIk6byhJ6KMtO316PYp4Mc1GKkBH7qKPQH7Y5qVLauBkzx69wBYgFwMrtuz8yToG3JtQ=="],
"elysia-rate-limit": ["elysia-rate-limit@4.4.0", "", { "dependencies": { "@alloc/quick-lru": "5.2.0", "debug": "4.3.4" }, "peerDependencies": { "elysia": ">= 1.0.0" } }, "sha512-pyQdFEdjgf5ELx5CAEfOZ2IWhPaYv8WIQMrXimzHzslsJ9awDHoK6rcF9K7k/yAOh4qB1UhiasNeMMBGtxAwYQ=="],
"exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="],
@@ -61,7 +61,7 @@
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
"file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="],
"file-type": ["file-type@21.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.7", "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg=="],
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
@@ -77,11 +77,9 @@
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
"peek-readable": ["peek-readable@7.0.0", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="],
"strtok3": ["strtok3@10.3.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw=="],
"strtok3": ["strtok3@10.2.2", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="],
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
"token-types": ["token-types@6.0.3", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
@@ -89,16 +87,20 @@
"uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
"zod": ["zod@3.25.67", "", {}, "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw=="],
"@cap.js/server/@types/node": ["@types/node@22.16.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ=="],
"@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="],
"@tokenizer/inflate/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"@cap.js/server/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
"@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "cap-standalone",
"version": "2.0.2",
"version": "2.0.3",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "bun run --watch src/index.js",
@@ -45,12 +45,12 @@
"turing-test"
],
"dependencies": {
"@cap.js/server": "^1.0.13",
"@cap.js/server": "^1.0.16",
"@elysiajs/cors": "^1.3.3",
"@elysiajs/static": "^1.3.0",
"@elysiajs/swagger": "^1.3.0",
"elysia": "^1.3.1",
"elysia-rate-limit": "^4.3.0"
"@elysiajs/swagger": "^1.3.1",
"elysia": "^1.3.5",
"elysia-rate-limit": "^4.4.0"
},
"devDependencies": {
"bun-types": "latest"
+1 -1
View File
@@ -15,7 +15,7 @@
text-align: center;
margin: 0px;
padding: 0px;
margin-top: 10%;
margin-top: 14vh;
background-color: #181818;
color: #e8e8ea;
}
+1 -1
View File
@@ -1 +1 @@
(()=>{const t=(e,o,n,i)=>{o.onclick=null;const s=parseInt(o.getAttribute("data-cap-floating-offset"))||8,a=o.getAttribute("data-cap-floating-position")||"top",l=o.getBoundingClientRect();Object.assign(n.style,{display:"block",position:"absolute",zIndex:"99999"});const c=l.left+(l.width-n.offsetWidth)/2,r=Math.min(c,window.innerWidth-n.offsetWidth);n.style.top="top"===a?`${Math.max(window.scrollY,l.top-n.offsetHeight-s+window.scrollY)}px`:`${Math.min(l.bottom+s+window.scrollY,window.innerHeight-n.offsetHeight+window.scrollY)}px`,n.style.left=`${Math.max(r,2)}px`,n.solve(),n.addEventListener("solve",({detail:s})=>{o.setAttribute("data-cap-token",s.token),o.setAttribute("data-cap-progress","done"),setTimeout(()=>{i.forEach(t=>{o.addEventListener("click",t),t.call(o,e)}),setTimeout(()=>{o.onclick=null,i.forEach(t=>o.removeEventListener("click",t)),o.onclick=e=>t(e,o,n,i)},50)},500),setTimeout(()=>{n.style.display="none"},700)})},e=e=>{const o=e.getAttribute("data-cap-floating");if(!o)return;const n=document.querySelector(o);if(!document.contains(n)&&!n.solve)throw new Error(`[cap floating] "${o}" doesn't exist or isn't a Cap widget`);n.style.display="none";const i=[e.onclick].filter(Boolean);"function"==typeof getEventListeners&&i.push(...(getEventListeners(e).click||[]).map(t=>t.listener)),i.length&&(e.onclick=null,i.forEach(t=>e.removeEventListener("click",t))),e.addEventListener("click",o=>{o.stopImmediatePropagation(),o.preventDefault(),t(o,e,n,i)})},o=t=>{e(t),t.querySelectorAll("[data-cap-floating]").forEach(e)};o(document.body),new MutationObserver(t=>t.forEach(t=>t.addedNodes.forEach(t=>{t.nodeType===Node.ELEMENT_NODE&&o(t)}))).observe(document.body,{childList:!0,subtree:!0})})();
(()=>{const t=(e,o,n,i)=>{o.onclick=null;const s=parseInt(o.getAttribute("data-cap-floating-offset"))||8,a=o.getAttribute("data-cap-floating-position")||"top",l=o.getBoundingClientRect();Object.assign(n.style,{display:"block",position:"absolute",zIndex:"99999",opacity:"0",transform:"scale(0.98)",marginTop:"-4px",boxShadow:"rgba(0, 0, 0, 0.05) 0px 6px 24px 0px",borderRadius:"14px",transition:"opacity 0.15s, margin-top 0.2s, transform 0.2s"}),setTimeout(()=>{n.style.transform="scale(1)",n.style.opacity="1",n.style.marginTop="0"},5);const r=l.left+(l.width-n.offsetWidth)/2,c=Math.min(r,window.innerWidth-n.offsetWidth);n.style.top="top"===a?`${Math.max(window.scrollY,l.top-n.offsetHeight-s+window.scrollY)}px`:`${Math.min(l.bottom+s+window.scrollY,window.innerHeight-n.offsetHeight+window.scrollY)}px`,n.style.left=`${Math.max(c,2)}px`,n.solve(),n.addEventListener("solve",({detail:s})=>{o.setAttribute("data-cap-token",s.token),o.setAttribute("data-cap-progress","done"),setTimeout(()=>{i.forEach(t=>{o.addEventListener("click",t),t.call(o,e)}),setTimeout(()=>{o.onclick=null,i.forEach(t=>o.removeEventListener("click",t)),o.onclick=e=>t(e,o,n,i)},50)},500),setTimeout(()=>{n.style.transform="scale(0.98)",n.style.opacity="0",n.style.marginTop="-4px"},500),setTimeout(()=>{n.style.display="none"},700)})},e=e=>{const o=e.getAttribute("data-cap-floating");if(!o)return;const n=document.querySelector(o);if(!document.contains(n)&&!n.solve)throw new Error(`[cap floating] "${o}" doesn't exist or isn't a Cap widget`);n.style.display="none";const i=[e.onclick].filter(Boolean);"function"==typeof getEventListeners&&i.push(...(getEventListeners(e).click||[]).map(t=>t.listener)),i.length&&(e.onclick=null,i.forEach(t=>e.removeEventListener("click",t))),e.addEventListener("click",o=>{o.stopImmediatePropagation(),o.preventDefault(),t(o,e,n,i)})},o=t=>{e(t),t.querySelectorAll("[data-cap-floating]").forEach(e)};o(document.body),new MutationObserver(t=>t.forEach(t=>t.addedNodes.forEach(t=>{t.nodeType===Node.ELEMENT_NODE&&o(t)}))).observe(document.body,{childList:!0,subtree:!0})})();
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@cap.js/widget",
"version": "0.1.24",
"version": "0.1.25",
"description": "Client-side widget for Cap, a lightweight, modern open-source CAPTCHA alternative designed using SHA-256 PoW.",
"keywords": [
"security",
+68 -6
View File
@@ -143,7 +143,10 @@
this.#div.setAttribute(
"aria-label",
this.getI18nText("verifying-aria-label", "Verifying you're a human, please wait")
this.getI18nText(
"verifying-aria-label",
"Verifying you're a human, please wait"
)
);
this.dispatchEvent("progress", { progress: 0 });
@@ -157,7 +160,56 @@
method: "POST",
})
).json();
const solutions = await this.solveChallenges(challenge);
let challenges = challenge;
if (!Array.isArray(challenges)) {
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);
}
let i = 0;
challenges = Array.from({ length: challenge.c }, () => {
i = i + 1;
return [
prng(`${token}${i}`, challenge.s),
prng(`${token}${i}d`, challenge.d),
];
});
}
const solutions = await this.solveChallenges(challenges);
const resp = await (
await capFetch(`${apiEndpoint}redeem`, {
@@ -189,14 +241,20 @@
this.#div.setAttribute(
"aria-label",
this.getI18nText("verified-aria-label", "We have verified you're a human, you may now continue")
this.getI18nText(
"verified-aria-label",
"We have verified you're a human, you may now continue"
)
);
return { success: true, token: this.token };
} catch (err) {
this.#div.setAttribute(
"aria-label",
this.getI18nText("error-aria-label", "An error occurred, please try again")
this.getI18nText(
"error-aria-label",
"An error occurred, please try again"
)
);
this.error(err.message);
throw err;
@@ -249,7 +307,8 @@
this.dispatchEvent("progress", {
progress: Math.round((completed / total) * 100),
});
resolve([salt, target, data.nonce]);
resolve(data.nonce);
};
worker.onerror = (err) => {
@@ -309,7 +368,10 @@
this.#div.classList.add("captcha");
this.#div.setAttribute("role", "button");
this.#div.setAttribute("tabindex", "0");
this.#div.setAttribute("aria-label", this.getI18nText("verify-aria-label", "Click to verify you're a human"));
this.#div.setAttribute(
"aria-label",
this.getI18nText("verify-aria-label", "Click to verify you're a human")
);
this.#div.setAttribute("aria-live", "polite");
this.#div.setAttribute("disabled", "true");
this.#div.innerHTML = `<div class="checkbox" part="checkbox"></div><p part="label">${this.getI18nText(
+4 -3
View File
@@ -3,9 +3,10 @@
typeof WebAssembly !== "object" ||
typeof WebAssembly?.instantiate !== "function"
) {
self.onmessage = async ({ data: { salt, target } }) => {
// Fallback solver in case WASM is not available
// fallback worker for environments without wasm
// this is much slower than the wasm version
self.onmessage = async ({ data: { salt, target } }) => {
let nonce = 0;
const batchSize = 50000;
let processed = 0;
@@ -48,7 +49,7 @@
processed += batchSize;
} catch (error) {
console.error("[cap worker] fallback worker error", error);
console.error("[cap worker]", error);
self.postMessage({
found: false,
error: error.message,